Skip to content

Refactor: host build graph, eager completion watermark, 4 AICPU schedulers - #1618

Open
raphael-s-steiner wants to merge 1 commit into
hw-native-sys:mainfrom
huawei-csl:refactor/eager-completion-mark
Open

Refactor: host build graph, eager completion watermark, 4 AICPU schedulers#1618
raphael-s-steiner wants to merge 1 commit into
hw-native-sys:mainfrom
huawei-csl:refactor/eager-completion-mark

Conversation

@raphael-s-steiner

@raphael-s-steiner raphael-s-steiner commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Refactor: host build graph, eager completion watermark, 4 AICPU schedulers

Base: main · Branch: refactor/eager-completion-mark · 1 commit, 15 files (+651/-466)

One of two possible versions of host-build-graph completion-watermark refactor the other one being #1619.

This eager one is more performant due to minimal work, but has delicate synchronization logic.

Summary

Reverts the 3S+1P scheduler split back to 4 uniform AICPU schedulers, and reworks
completed_watermark maintenance to be eager rather than lazy. Eager updates require
completion_flags to become a reusable, thread-safe structure (previously a single
byte per slot, implicitly host-only), which in turn needed a failsafe against the
flag-slot-reuse deadlock the old design couldn't hit.

1. Revert: 3 schedulers + 1 dedicated resolution thread → 4 AICPU schedulers

The earlier design split AICPU threads into 3 core-owning scheduler (S) threads plus
1 core-less resolution (P) thread that alone drained completions, published
completion_flags, drained wake lists, and advanced the watermark — funneling all
completion resolution through a single thread.

This PR removes that split entirely:

  • CompletedTaskQueue (the per-S → P SPSC handoff ring) and run_resolution_thread
    are deleted (scheduler_context.h, scheduler_dispatch.cpp).
  • p_thread_idx() / p_thread_idx_ are gone; assign_cores_to_threads no longer
    reserves the last thread as core-less, and the aicpu_thread_num >= 2 floor (1 S +
    1 P) is dropped — active_sched_threads_ = aicpu_thread_num_ again, so all 4
    threads own cores and resolve their own completions.
  • aicpu_executor.cpp calls resolve_and_dispatch uniformly instead of branching on
    whether a thread is the P thread.
  • Async mailbox polling and dummy/predicate-failed-task retirement (previously P's
    job) move back into each scheduler thread's own resolve_and_dispatch loop.

2. Completion watermark: eager updates

completed_watermark is now advanced eagerly by every completer, not just
opportunistically:

  • Every completion path — device (on_mixed_task_complete), its deferred retry
    (retry_set_completion_flags), and the host orchestrator's inline hidden-alloc
    completion — calls update_completed_watermark(thread_idx, my_id) exactly once,
    immediately after that id's own completion_flags entry is actually visible.
  • The call is a no-op unless my_id is exactly the current watermark frontier; only
    the completer landing at the frontier does the CAS-advance walk over the full
    contiguous completed prefix. Out-of-order completers defer to whoever completes the
    frontier task later.
  • Per-thread cached_completed_watermark avoids re-reading the atomic on every
    is_completion_flag_set check by falling back to a cached watermark value.
  • Semantics flip slightly: completed_watermark is now "lowest id not yet
    guaranteed complete" (was "highest id guaranteed complete"), so comparisons flip
    from >= to > at call sites (wait_for_tensor_ready, reclaim gates, etc.).

3. completion_flags: fewer flags than tasks, slots reused

completion_flags changes from a uint8_t[task_window_size] byte array (host-only
writer, implicitly one-shot) to an int32_t[task_window_size] array where each entry
stores either -1 (pending) or the local_id that owns it. Because the stored value
is the id itself rather than a boolean, a slot can be safely reused across laps:
the array no longer needs to be sized to the total task count, only to the ring's
task window — local_id and local_id + task_window_size share a slot, and reuse is
gated on completed_watermark having certified the slot's previous occupant first.

  • flag_index() bit-reindexes local_id & task_window_mask (swaps low
    shuffle_lower_bits bits into the high position) so consecutive ids land on
    different cachelines, keeping update_completed_watermark's linear scan
    cache-friendly.
  • set_completion_flag (host, blocking) and the new try_set_completion_flag
    (device, non-blocking) both gate the store on the previous occupant being
    certified; try_set_completion_flag returns false instead of spinning when it
    isn't.
  • is_completion_flag_set falls back to completed_watermark so a slot that's been
    overwritten by a later lap still reports the earlier id as complete.

4. Deadlock failsafe for flag-slot reuse

The correctness argument for reuse is: task t must not depend on a task with id
>= t + task_window_size, which holds automatically since task ids follow the
dependency graph's topological order. But rather than assume that invariant always
holds, a failsafe absorbs a violation instead of deadlocking:

  • When try_set_completion_flag fails inside on_mixed_task_complete, the task id is
    pushed onto a per-thread min-heap (failed_heap_of_set_completion_flag) instead of
    the thread spinning or blocking. Wake-list drain and the watermark update for that
    id are skipped and deferred.
  • Each dispatch-loop iteration calls retry_set_completion_flags, which retries the
    smallest pending id in the heap; on success it drains that task's wake list and
    advances the watermark — its one deferred chance, taken later instead of never.
  • Expected steady state: the heap is empty and drains on the very next retry when it
    isn't — it exists purely as a backstop, not a normal-path mechanism.

Also in this diff

  • docs/RUNTIME_LOGIC.md (§6.2, §7.2, §8.2, §8.4) rewritten to match the above.
  • New unit test test_hbg_shared_memory.cpp covers the shuffle_higher_bits
    invariant flag_index() depends on (rejects a task_window_size too small to
    provide shuffle_lower_bits of headroom, which would otherwise be a negative
    shift / UB).
  • PTO2SharedMemoryRingHeader grows from 256 → 576 bytes and
    PTO2SharedMemoryHeader from 320 → 640 bytes (new cached_completed_watermark
    array + wider completion_flags entries); layout static_asserts updated
    accordingly.

Performance

Comparison against main and #1619 for both device wall-clock and kernel only (as measured by tracr)

A226CE25-BED4-4002-B56C-8E6CE81FA767 5F71AA5E-3B03-40EF-80BE-714FE8597F7E 6C3BA7A0-E4F9-40B5-B67D-EACF477455DE

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c3b65ca-83d4-4f19-85b5-9d6dbfa88263

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR removes the dedicated resolution thread from the AICPU scheduler, unifying all threads under a single scheduling model. Completion flags change from byte-based to int32 identity stamps with stricter watermark semantics, requiring updates across shared memory, orchestrator, runtime, and scheduler code, plus a per-thread retry mechanism, documentation, and a new test.

Changes

Scheduler unification and completion-flag rework

Layer / File(s) Summary
Remove dedicated resolution thread; unify scheduler threads
.../aicpu/aicpu_executor.cpp, .../scheduler/scheduler_context.h, .../scheduler/scheduler_cold_path.cpp, .../scheduler/scheduler_dispatch.cpp, .../scheduler/scheduler_completion.cpp
All AICPU threads now invoke resolve_and_dispatch. The dedicated resolution thread, its CompletedTaskQueue, run_resolution_thread, and p_thread_idx() are removed. Async completion polling and dummy-task drain run inline on every scheduler thread, and consumer resolution completes directly through on_task_complete.
Identity-based completion flags and watermark storage
.../runtime/pto_shared_memory.h, .../runtime/shared/pto_shared_memory.cpp, .../runtime/pto_async_wait.h, .../runtime/pto_runtime2_types.h
Completion flags change from atomic uint8_t bytes to atomic int32_t identity stamps with shuffled indexing and cached per-thread watermark values. try_set_completion_flag is added for non-blocking publication. init_header now validates task-window sizes and returns bool, initializing flags to -1 and watermark to 0. Slot reclamation now requires the watermark to strictly exceed the consumer ID.
Orchestrator and runtime watermark call sites
.../orchestrator_core/pto_orchestrator.cpp, .../orchestrator_core/pto_runtime2.cpp
Comments and initialization constants reflect the -1 seed and strict-greater watermark condition. Host-originated pre-completed tasks in alloc_tensors explicitly set flags and advance the watermark. Consumer-readiness polling uses an inclusive watermark comparison.
Per-thread completion-flag retry heap and thread-aware fanin
.../scheduler/pto_scheduler.h, .../shared/pto_runtime2_init.cpp
A per-thread min-heap retries failed completion-flag publications. fanin_satisfied, classify_fanin_state, register_wake, drain_wake_list, and on_task_complete all become thread-index aware, and retry_set_completion_flags is added along with heap cleanup during scheduler destruction.
Runtime logic documentation updates
.../docs/RUNTIME_LOGIC.md
Documentation describes identity-based completion flags, host inline completion, deferred flag-slot reuse, and the exclusive watermark frontier.
Shared-memory unit test
tests/ut/cpp/CMakeLists.txt, tests/ut/cpp/a2a3/test_hbg_shared_memory.cpp
A new test target and test file verify the shuffle_higher_bits invariant, including rejection of task window sizes below the shuffle floor.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SchedulerThread
  participant CompletionFlags
  participant WakeList
  participant Watermark

  SchedulerThread->>CompletionFlags: try_set_completion_flag(thread_idx, local_id)
  alt flag set successfully
    CompletionFlags-->>SchedulerThread: success
    SchedulerThread->>WakeList: drain_wake_list(thread_idx)
    SchedulerThread->>Watermark: update_completed_watermark(thread_idx, local_id)
  else reuse not yet certified
    CompletionFlags-->>SchedulerThread: failure
    SchedulerThread->>SchedulerThread: push to failed_heap_of_set_completion_flag
    SchedulerThread->>SchedulerThread: retry_set_completion_flags(thread_idx) later
  end
Loading

Possibly related PRs

  • hw-native-sys/simpler#1619: Implements the same architectural refactoring restoring N full AICPU schedulers, replacing byte completion flags with int32 stamps, and adding per-thread retry heaps across the identical files.
  • hw-native-sys/simpler#1536: Introduces the completion-flag helper APIs and shared-memory layout that this PR extends with thread_idx parameters and int32_t atomics.
  • hw-native-sys/simpler#1544: Introduced the dedicated resolution-thread architecture that this PR reverses by removing run_resolution_thread and p_thread_idx.

Poem

A rabbit hops through threads all night,
No more one runner holds the light.
Every burrow now can flag and mark,
Watermarks rise past the dark.
Stamps of int32, IDs so true,
Hop, hop, hooray — the scheduling's new! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the scheduler refactor, eager completion watermark, and four-AICPU-scheduler design.
Description check ✅ Passed The description directly explains the scheduler refactor, completion-watermark redesign, synchronization safeguards, tests, and performance results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp (1)

699-712: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Lower the assigned thread count instead of returning 0 for the all-active case.

nthreads == 1 becomes aicpu_thread_num_ = 1, which makes assign_cores_to_threads() return false because scheduler threads are configured to be fewer than nthreads. Also avoid assigning aicpu_thread_num_ == MAX_AICPU_THREADS: assign_cores_to_threads() then loops over all core_trackers_/array entries while aic_count_ == 0, so aic_count_ / active_sched_threads_ yields 0 and no cores are registered (same as returning 0 early with aicpu_thread_num_ = 2).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp`
around lines 699 - 712, Update the scheduler-thread count calculation that feeds
SchedulerContext::assign_cores_to_threads() so the all-active case lowers the
count instead of returning 0: when nthreads == 1, set aicpu_thread_num_ to 1
only if that satisfies the configured constraint, otherwise reduce it to a valid
value; never assign MAX_AICPU_THREADS, particularly when aic_count_ == 0.
Preserve a positive thread count that lets assign_cores_to_threads() complete
without zero-cluster division or empty core registration.
🧹 Nitpick comments (3)
tests/ut/cpp/CMakeLists.txt (1)

670-697: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate boilerplate from add_a2a3_hbg_runtime_test.

Lines 674-697 repeat the include directories, link libraries, and test-registration code from add_a2a3_hbg_runtime_test (lines 122-147). Only the extra compiled sources differ: pto_shared_memory.cpp here versus scope_stats_collector_aicpu.cpp in the function.

Generalize the function to accept an extra-sources list. This removes the duplicate block and keeps future host-build-graph test targets consistent.

♻️ Proposed refactor
-function(add_a2a3_hbg_runtime_test name src)
+function(add_a2a3_hbg_runtime_test name src)
+    set(extra_srcs ${ARGN})
     add_executable(${name}
         ${src}
         ${CMAKE_SOURCE_DIR}/stubs/test_stubs.cpp
-        ${CMAKE_SOURCE_DIR}/../../../src/common/platform/shared/aicpu/scope_stats_collector_aicpu.cpp
+        ${extra_srcs}
     )
     ...
 endfunction()

Then define the new test as:

add_a2a3_hbg_runtime_test(test_hbg_shared_memory
    a2a3/test_hbg_shared_memory.cpp
    ${CMAKE_SOURCE_DIR}/../../../src/a2a3/runtime/host_build_graph/runtime/shared/pto_shared_memory.cpp
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/cpp/CMakeLists.txt` around lines 670 - 697, Update
add_a2a3_hbg_runtime_test to accept and append an extra-sources list when
creating the executable, while retaining its existing include directories, link
libraries, test registration, and labels. Replace the standalone
test_hbg_shared_memory target block with an add_a2a3_hbg_runtime_test call
passing its test source and pto_shared_memory.cpp.
src/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_orchestrator.cpp (1)

1064-1079: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct the watermark comment.

The code is right. The explanation is not. update_completed_watermark walks forward with is_completion_flag_set(next), so a later device completer that lands exactly on the frontier does walk past this pre-set flag. The precise reason for the explicit host call is narrower: only a completer whose local_id equals the current watermark advances it, so if the frontier already sits at this task's id, no other completer will ever call with that id.

📝 Proposed comment fix
-        // every consumer register_wakes on a producer that never runs on device and
-        // the run hangs. update_completed_watermark only advances when called with
-        // local_id equal to the current watermark, so this task's own call is the
-        // only chance to move the watermark past it — a later on-device completer
-        // whose local_id no longer matches the (still-stuck) watermark will no-op,
-        // not walk past this pre-set flag on our behalf.
+        // every consumer register_wakes on a producer that never runs on device and
+        // the run hangs. update_completed_watermark advances only when its local_id
+        // equals the current watermark. No device thread ever calls it with THIS
+        // task's local_id, so if the frontier already sits at this id, only this
+        // call can move it forward. (A later device completer that does land on the
+        // frontier walks over this pre-set flag as part of its prefix walk.)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_orchestrator.cpp`
around lines 1064 - 1079, Update the multi-line comment block preceding the
set_completion_flag and update_completed_watermark calls to correct the
explanation of watermark advancement behavior. Replace the incorrect statement
that a later device completer will no-op and not walk past the pre-set flag with
the accurate explanation that update_completed_watermark walks forward using
is_completion_flag_set, so device completers landing on the frontier do walk
past pre-set flags. Clarify the narrower and actual reason for the explicit host
call: only a completer whose local_id equals the current watermark advances it,
so if the frontier already sits at this task's local_id (done_local), no other
completer will ever call with that matching id to move the watermark forward.
src/a2a3/runtime/host_build_graph/runtime/scheduler/pto_scheduler.h (1)

474-515: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Replace realloc with arena or fixed-storage for the failed-completion heap.

FailedCompletionFlagHeap is allocated from the AICPU scheduler state but still calls stdlibc realloc/free and aborts on allocation failure. Since this heap is only needed in rare completion-flag CAS contention, use a small fixed capacity or the scheduler arena instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/pto_scheduler.h` around
lines 474 - 515, Update FailedCompletionFlagHeap to avoid stdlibc realloc/free
and allocation-failure aborts by using fixed-capacity storage or allocation from
the scheduler arena. Preserve push/pop heap behavior and ensure destroy performs
only the corresponding non-stdlib cleanup, with capacity sized for the rare
completion-flag contention use case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/a2a3/runtime/host_build_graph/runtime/shared/pto_shared_memory.cpp`:
- Around line 153-160: Update the task_window_sizes validation loop in the
shared-memory initialization path to reject zero values before calling
__builtin_ctzll and reject any non-power-of-two value. Keep the existing
shuffle_lower_bits constraint, ensuring every accepted size is a nonzero power
of two with sufficient trailing zero bits before task_window_size is assigned.

---

Outside diff comments:
In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp`:
- Around line 699-712: Update the scheduler-thread count calculation that feeds
SchedulerContext::assign_cores_to_threads() so the all-active case lowers the
count instead of returning 0: when nthreads == 1, set aicpu_thread_num_ to 1
only if that satisfies the configured constraint, otherwise reduce it to a valid
value; never assign MAX_AICPU_THREADS, particularly when aic_count_ == 0.
Preserve a positive thread count that lets assign_cores_to_threads() complete
without zero-cluster division or empty core registration.

---

Nitpick comments:
In
`@src/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_orchestrator.cpp`:
- Around line 1064-1079: Update the multi-line comment block preceding the
set_completion_flag and update_completed_watermark calls to correct the
explanation of watermark advancement behavior. Replace the incorrect statement
that a later device completer will no-op and not walk past the pre-set flag with
the accurate explanation that update_completed_watermark walks forward using
is_completion_flag_set, so device completers landing on the frontier do walk
past pre-set flags. Clarify the narrower and actual reason for the explicit host
call: only a completer whose local_id equals the current watermark advances it,
so if the frontier already sits at this task's local_id (done_local), no other
completer will ever call with that matching id to move the watermark forward.

In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/pto_scheduler.h`:
- Around line 474-515: Update FailedCompletionFlagHeap to avoid stdlibc
realloc/free and allocation-failure aborts by using fixed-capacity storage or
allocation from the scheduler arena. Preserve push/pop heap behavior and ensure
destroy performs only the corresponding non-stdlib cleanup, with capacity sized
for the rare completion-flag contention use case.

In `@tests/ut/cpp/CMakeLists.txt`:
- Around line 670-697: Update add_a2a3_hbg_runtime_test to accept and append an
extra-sources list when creating the executable, while retaining its existing
include directories, link libraries, test registration, and labels. Replace the
standalone test_hbg_shared_memory target block with an add_a2a3_hbg_runtime_test
call passing its test source and pto_shared_memory.cpp.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44dad46c-6e3c-47e9-97aa-96c14a2fab67

📥 Commits

Reviewing files that changed from the base of the PR and between f2a20ca and 7eb4bda.

📒 Files selected for processing (16)
  • src/a2a3/runtime/host_build_graph/aicpu/aicpu_executor.cpp
  • src/a2a3/runtime/host_build_graph/docs/RUNTIME_LOGIC.md
  • src/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_orchestrator.cpp
  • src/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_runtime2.cpp
  • src/a2a3/runtime/host_build_graph/runtime/pto_async_wait.h
  • src/a2a3/runtime/host_build_graph/runtime/pto_runtime2_types.h
  • src/a2a3/runtime/host_build_graph/runtime/pto_shared_memory.h
  • src/a2a3/runtime/host_build_graph/runtime/scheduler/pto_scheduler.h
  • src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp
  • src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_completion.cpp
  • src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_context.h
  • src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_dispatch.cpp
  • src/a2a3/runtime/host_build_graph/runtime/shared/pto_runtime2_init.cpp
  • src/a2a3/runtime/host_build_graph/runtime/shared/pto_shared_memory.cpp
  • tests/ut/cpp/CMakeLists.txt
  • tests/ut/cpp/a2a3/test_hbg_shared_memory.cpp
💤 Files with no reviewable changes (1)
  • src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_context.h

…ulers

Co-authored-by: noabauma <noah.baumann@h-partners.com>

Co-authored-by: Sergio Martin <eienburuu@gmail.com>
@raphael-s-steiner
raphael-s-steiner force-pushed the refactor/eager-completion-mark branch from 7eb4bda to a3a637f Compare July 31, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant