[Performance] HBG: up to 99% host-side overhead reduction - #1659
Conversation
📝 WalkthroughWalkthroughThe host runtime now uses uniquely owned shared-memory storage, initializes only control regions, and uploads the live payload prefix plus complete control segments to the device. ChangesShared-memory staging
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/host/runtime_maker.cpp`:
- Around line 537-548: Validate total_tasks before computing payload_prefix_end
or performing relocation/copy operations: require it to be non-negative and no
greater than eff_task_window_sizes[0]. Reject invalid values early, preserving
the existing copy behavior only for valid task counts.
🪄 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: e9f8a9e0-e5d7-420b-a483-e1085fb0681e
📒 Files selected for processing (1)
src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp
184c2cd to
ed90f0c
Compare
677289d to
ab534b5
Compare
ab534b5 to
f4c36af
Compare
ChaoZheng109
left a comment
There was a problem hiding this comment.
The mechanism holds. The load-bearing invariant — device-side reads are bounded
by current_task_index everywhere — checks out at every site: classify_partition
iterates [0, submitted), update_completed_watermark walks bounded by
fc.current_task_index, fanin lookups go through fanin_local_ids[] (always
< total_tasks), and relocate_host_orch_image walks [0, count). The two claims
the PR body asserts without proof also hold:
- The skipped orch block is genuinely host-only.
rt->orchestratorand
rt->schedulerare embedded by value inPTO2Runtimeatoff_runtime, inside
the "rest" region that still ships whole, soscheduler_cold_path.cpp:1082
readingrt->orchestrator.inline_completed_tasksis safe. The scheduler/AICPU
sources contain zero dereferences oftensor_map/scope_tasks/
fanin_seen_epoch. - Dropping
active_mask = ActiveMask{}frominit_header_per_ringis safe —
prepare_task:442rewrites it unconditionally. Betweenreset_for_reuse()and
the explicit stores inprepare_task, every functional field of
PTO2TaskSlotStateis covered; only the_async_padfiller is left
uninitialized, which is inert.
The +1 sentinel writeup in the PR description is excellent — "a lock-free queue's
read set extends one element past its write set" is exactly the kind of finding
that deserves to be recorded where the next person will hit it.
None of the items below is a correctness bug in the current code. They are
guardrails around the new invariants, plus the doc/comment drift the change leaves
behind.
1. The new "≤ total_tasks pushes per ready queue" invariant has no guardrail, and violating it fails silently
The whole bounding scheme rests on each task being pushed into a big ready queue at
most once. That holds across every push site today (push_ready_routed's three
branches, fed by the wake drain, register_wake re-classification, boot classify,
the cold path, and cancel_early_sync_drain). The problem is what happens when it
stops holding:
push_taggedreads the target slot's Vyukov sequence. On an unseeded /
un-uploaded slot that value is stale garbage, anddiff < 0makes it
return false— which reads as "queue full" but here means "slot was never
shipped".push_ready_routedignores the return value, so a ready task is
silently dropped and the run hangs.- If the stale sequence lands on
diff > 0instead, the push spins forever.
Both surface as SCHEDULER_TIMEOUT / host-side 507018 — precisely the failure
this PR already had to debug once. This is not only about unseeded slots:
PTO2_READY_QUEUE_SIZE is 65536 while the task window can be larger
(paged_attention runs 131072), so a genuinely full queue is reachable too and is
discarded just as silently.
Minimum ask: check the push() return value in push_ready_routed and
report_fatal on false. That converts a silent hang into a named error at zero
layout cost. Optionally, record the seeded prefix length in PTO2ReadyQueue (there
is spare room in _pad0) and fault on enqueue_pos >= seeded_slots, which also
catches the spin case.
2. No regression test, though the harness is already in place
tests/ut/cpp/CMakeLists.txt already has add_a2a3_hbg_runtime_test() (currently
driving test_hbg_task_allocator and test_hbg_tensormap), and
tests/ut/cpp/a2a3/test_ready_queue.cpp already covers the Vyukov queue. A
test_hbg_ready_queue.cpp pinning this PR's core semantics is cheap:
- seed with
slot_init_count = N+1, write a stale (too-large) sequence into slots
[N+1, capacity), pushN, then assertpop_batchreturnsNand
terminates; - the
slot_init_count = Ncontrol case (the first cut described in the PR body)
should be shown to hang, under a timeout assertion.
Per .claude/rules/discipline.md §3, the intermittent hang this PR fixed currently
has only a pytest-xdist stress run as its barrier, and nothing in-tree.
3. Doc / comment drift — reset_for_reuse() moved, four descriptions did not
.claude/rules/doc-consistency.md §4 wants these in the same commit:
| Location | Current text | Status |
|---|---|---|
docs/RUNTIME_LOGIC.md §8.4 |
"reset_for_reuse() runs once at init (pto_shared_memory.cpp)" |
stale |
runtime/pto_runtime2_types.h (reset_for_reuse doc block) |
"Runs once per slot at init (pto_shared_memory.cpp)" | stale |
runtime/shared/pto_runtime2_init.cpp:95-97 |
"Per-slot SM-side initialization … lives in init_header_per_ring so the AICPU performs it during SM reset" |
stale |
runtime/pto_shared_memory.h (completion_flags field) |
"Zeroed host-side at init." | stale |
Please also add the new H2D contract to RUNTIME_LOGIC.md: which segments are
shipped bounded to total_tasks, why the orch block is not shipped at all, and why
the ready-queue prefix is total_tasks + 1. That contract currently lives only in a
comment in runtime_maker.cpp, and it is the most easily broken thing in the
change.
4. Host-side style: prefer modern C++ over the C array idiom
.claude/rules/codestyle.md §8 — runtime_maker.cpp:933-955 uses
const size_t big_q_slots[] with sizeof(a) / sizeof(a[0]) in both loops.
std::array plus a range-for reads better and removes the two hand-written
divisions.
Two comments are change-narration rather than present-tense facts
(.claude/rules/comments.md): pto_orchestrator.cpp:406 ("rather than pre-zeroing
the whole task window in init_header_per_ring") and pto_shared_memory.cpp:184
("not swept over the whole task window here"). Both would read better stated as the
invariant that holds now; the before/after belongs in the commit message.
5. The arena slicing silently depends on the reservation order
orch_start = layout.orch.off_fanin_seen_epoch and
rest_start = sq.off_early_dispatch_queue_slots[0] assume the order in
runtime_reserve_layout (sm_handle → orch → sched(big queues → small queues) →
runtime → mailbox). That order is correct today, but any future reordering ships a
wrong image with no diagnostic. A few always_asserts would pin it:
orch_start < sq.off_ready_queue_slots[0]- the seven big-queue offsets are strictly ascending
rest_start >= last_big_queue_offset + capacity * sizeof(PTO2ReadyQueueSlot)
6. Nothing enforces the new "every device-read field is written at submit" contract
The SM buffer is no longer zero-filled, so the fields nobody writes —
payload.tensors[tensor_count..], fanin_local_ids[fanin_count..],
_fanin_reserved, slot_state._async_pad — read as zero by allocator accident,
not by construction. The exposure is a future one: add a field to
PTO2TaskDescriptor / PTO2TaskPayload / PTO2TaskSlotState, miss the
submit-path write, and it passes every test here and fails non-deterministically
on device.
Please pin the contract with a cpput test that owns its own SM buffer: fill it with
0xAA before init_per_ring, submit a representative mix (multi-fanin, zero-fanin
root, predicated, hidden-alloc via alloc_tensors, dummy), then assert that for
every slot in [0, total_tasks) no device-read field still holds poison —
descriptor task_id / kernel_id[] / packed_buffer_base / packed_buffer_end;
slot_state task_state / active_mask / task_attrs / total_required_subtasks /
logical_block_num / last_consumer_local_id / payload / task / wake-list;
payload tensor_count / scalar_count / fanin_count /
fanin_local_ids[0..fanin_count) / predicate.op / the early-dispatch atomics;
and completion_flags[slot].
test_orchestrator_fanin.cpp already has this shape against the tmr tree;
add_a2a3_hbg_runtime_test() needs pto_orchestrator.cpp and shared/*.cpp added
to link an HBG equivalent.
Nice work — the measurement, the A/B table and the root-cause writeup are all well
above the bar. The requests above are about making the new invariants enforceable by
the tree rather than by the reader.
c9a8b51 to
453b483
Compare
|
Thanks @ChaoZheng109 for the thorough pass — especially for independently walking the Heads-up on a significant change since the review. All six items were implemented as requested, but rebasing onto current So I reverted the ready-queue bounding entirely. The queues now ship in full; the PR keeps the dominant, graph-safe part of the win — the init-on-write bounded SM upload plus dropping the host-only orchestrator block from the arena upload. That takes bgemm bind from 77 ms to ~11.6 ms and paged_attention from 403 ms to ~7.8 ms (−85% / −98%); the extra ~6.7 ms the queue bounding bought is gone. This reframes a few of your items — details inline. 1. Guardrail on queue pushes Done, and now more relevant than ever. 2. Regression test for the sentinel — withdrawn The sentinel (and its 3. Doc / comment drift Done. The three stale 4. Host-side style The 5. Arena slicing order asserts Simplified along with the revert: the only slicing left is dropping the orchestrator block, so 6. SM-poison test Done — Validation: full HBG a2a3 scene-test suite green including |
HBG's per-dispatch wall is 96-99.6% host bind, and bind was dominated by rebuilding and H2D-uploading full, ring-sized host structures every run -- the shared-memory mirror and the ~20 MB prebuilt runtime arena -- even though a run touches a tiny fraction. The device boots scheduler-only and reads no SM slot past total_tasks, so the SM mirror is made init-on-write and shipped bounded to the task count, and the arena's host-only orchestrator block is dropped from the upload. Shared memory: - descriptors and payloads are written per task at submit; slot_states and completion_flags are reset per slot in orch::prepare_task as it is claimed, dropping the window-wide reset loop in init_header_per_ring; only the header is zeroed on the host; each segment is H2D-uploaded bounded to [0, total_tasks). Runtime arena: - skip uploading the orchestrator block (fanin_seen_epoch / scope / tensormap, ~8.5 MB): host-only dep-computation scratch the AICPU scheduler never reads. The scheduler block onward (ready queues, runtime header, mailbox) still ships whole. - drop the redundant per-entry stores in the tensormap reset (the preceding memset already zeroes the link pointers and producer_task_id). The ready queues are shipped in full, not bounded to total_tasks: graph execution (hw-native-sys#1444) replays a cached GRAPH task that the device Scheduler expands into on-device nodes, and those nodes push into the ready queues past the host task count, so every queue slot must carry a valid Vyukov sequence on the device. Every scheduler-read field is initialized at submit, so nothing depends on the removed blanket zeros; reads are bounded by current_task_index. total_tasks is range-checked before it sizes the SM copies. Single-ring (PTO2_MAX_RING_DEPTH == 1). Guardrails and tests: - push_ready_routed latches PTO2_ERROR_READY_QUEUE_OVERFLOW when a push finds no free slot (a genuinely full queue, or a task window / graph expansion past ready-queue capacity), turning a would-be silent drop and forward-progress stall into a named error. - bind_callable_to_runtime_impl always_asserts orch_start <= orch_end before slicing the orchestrator block out of the upload. - test_hbg_submit_poison fills the SM window with 0xAA, submits a representative mix (real mixed task with tensors + scalar, multi-fanin consumer, hidden-alloc, dummy) and asserts every device-read slot field is written, not left poison -- pinning the init-on-write contract so a future unwritten device-read field fails in-tree rather than non-deterministically on device. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MqeALZTPEnDXTnbYfcnPfq
453b483 to
abf4f67
Compare
runtime_init_data_from_layout initialized the orchestrator against the device SM while building the prebuilt arena, but its only caller (the host bind path in runtime_maker) immediately re-initializes the orchestrator against the host SM in run_host_orchestration, once the host SM buffer is allocated, then relocates it for the device. The first init is therefore dead work: its arena content (the tensormap and fanin_seen_epoch memsets) is overwritten by the re-init, and the orchestrator arena block is not uploaded to the device at all (the AICPU boots scheduler-only, per hw-native-sys#1659). The AICPU never calls runtime_init_data_from_layout, so no device path depends on it either. Initialize only the scheduler here and let run_host_orchestration own the single orchestrator init. rt->orchestrator stays zeroed (from the existing memset) until that point; the intervening runtime_wire_arena_pointers only sets pointers and does not read orchestrator state. Effect (paged_attention, a2a3 onboard, median of 50 rounds): the arena-build phase of bind drops from ~2565 us to ~1373 us (-46%) by removing the redundant tensormap init, cutting host bind wall from ~8677 us to ~7494 us (-13.6%). Device time unchanged. Testing: full host_build_graph golden suite (22 passed, 2 skipped; the 2 failures, run_stream_reuse and worker_async_fifo, reproduce identically on a baseline build and are unrelated -- arena-bank-commit path, untouched here). Adds a concurrent_prepare_stress white-box test that drives a two-deep overlapping pipeline over both arena banks (each run prepared while its predecessor is active), golden-checked over many iterations; it passes identically on this change and a baseline build, exercising the concurrent prepare/bind path this change touches.
runtime_init_data_from_layout initialized the orchestrator against the device SM while building the prebuilt arena, but its only caller (the host bind path in runtime_maker) immediately re-initializes the orchestrator against the host SM in run_host_orchestration, once the host SM buffer is allocated, then relocates it for the device. The first init is therefore dead work: its arena content (the tensormap and fanin_seen_epoch memsets) is overwritten by the re-init, and the orchestrator arena block is not uploaded to the device at all (the AICPU boots scheduler-only, per hw-native-sys#1659). The AICPU never calls runtime_init_data_from_layout, so no device path depends on it either. Initialize only the scheduler here and let run_host_orchestration own the single orchestrator init. rt->orchestrator stays zeroed (from the existing memset) until that point; the intervening runtime_wire_arena_pointers only sets pointers and does not read orchestrator state. Effect (paged_attention, a2a3 onboard, median of 50 rounds): the arena-build phase of bind drops from ~2565 us to ~1373 us (-46%) by removing the redundant tensormap init, cutting host bind wall from ~8677 us to ~7494 us (-13.6%). Device time unchanged. Testing: full host_build_graph golden suite (22 passed, 2 skipped; the 2 failures, run_stream_reuse and worker_async_fifo, reproduce identically on a baseline build and are unrelated -- arena-bank-commit path, untouched here). Adds a concurrent_prepare_stress white-box test that drives a two-deep overlapping pipeline over both arena banks (each run prepared while its predecessor is active), golden-checked over many iterations; it passes identically on this change and a baseline build, exercising the concurrent prepare/bind path this change touches.
runtime_init_data_from_layout initialized the orchestrator against the device SM while building the prebuilt arena, but its only caller (the host bind path in runtime_maker) immediately re-initializes the orchestrator against the host SM in run_host_orchestration, once the host SM buffer is allocated, then relocates it for the device. The first init is therefore dead work: its arena content (the tensormap and fanin_seen_epoch memsets) is overwritten by the re-init, and the orchestrator arena block is not uploaded to the device at all (the AICPU boots scheduler-only, per hw-native-sys#1659). The AICPU never calls runtime_init_data_from_layout, so no device path depends on it either. Initialize only the scheduler here and let run_host_orchestration own the single orchestrator init. rt->orchestrator stays zeroed (from the existing memset) until that point; the intervening runtime_wire_arena_pointers only sets pointers and does not read orchestrator state. Effect (paged_attention, a2a3 onboard, median of 50 rounds): the arena-build phase of bind drops from ~2565 us to ~1373 us (-46%) by removing the redundant tensormap init, cutting host bind wall from ~8677 us to ~7494 us (-13.6%). Device time unchanged. Applies the identical change to the a5 host_build_graph sibling (near-duplicate tree, same double-init pattern, no arch-level divergence): the orchestrator init removed from runtime_init_data_from_layout plus the matching doc-comment updates in pto_runtime2.h and pto_runtime2_init.cpp. All four safety invariants hold on a5 as on a2a3 -- single caller of the 8-arg runtime_init_data_from_layout (the host bind path in runtime_maker), the surviving orchestrator re-init against the host SM in run_host_orchestration, the orchestrator arena block never uploaded, and runtime_wire_arena_pointers between the two only setting pointers. a5 is compile-verified (host lib links clean); not perf-measured, as this box is a2a3 silicon. Testing: full host_build_graph golden suite (22 passed, 2 skipped; the 2 failures, run_stream_reuse and worker_async_fifo, reproduce identically on a baseline build and are unrelated -- arena-bank-commit path, untouched here). Adds a concurrent_prepare_stress white-box test that drives a two-deep overlapping pipeline over both arena banks (each run prepared while its predecessor is active), golden-checked over many iterations; it passes identically on this change and a baseline build, exercising the concurrent prepare/bind path this change touches.
Fixes hw-native-sys#1716 only. HBG is one runtime kept in sync across two arch trees (hw-native-sys#1706), so a5 still paid the full pre-hw-native-sys#1659 host cost: a boot-time blanket slot reset, whole-window completion-flag zero, full sm_size SM upload and full arena_size arena upload, and no ready-queue overflow guard. Port the diff onto the a5 tree file-for-file: - Bounded SM H2D: allocate the host SM uninitialized, zero only the header, and upload each segment (descriptors / payloads / slot_states / completion_flags) bounded to [0, total_tasks). total_tasks is range-checked before it sizes the copies. - Init-on-write: per-slot reset_for_reuse() + completion-flag clear move from the boot-time whole-window loop into orch::prepare_task as each slot is claimed. The unclaimed tail is neither initialized, uploaded, nor read. - Skip the host-only orchestrator block (fanin_seen_epoch / scope_tasks / TensorMap) from the arena H2D; an always_assert(orch_start <= orch_end) guards the layout order before slicing it out. - Latch PTO2_ERROR_READY_QUEUE_OVERFLOW from push_ready_routed instead of dropping a ready task into an anonymous forward-progress stall. - Drop the redundant per-entry tensormap stores the preceding memset already zeroed. The ready queues ship in full, mirroring a2a3, so the two trees stay identical for hw-native-sys#1715 to build on — a5 does not bound them locally just because graph execution is not yet on a5. Adds tests/ut/cpp/a5/test_hbg_submit_poison.cpp pinning the "every device-read SM field is written at submit" contract, and RUNTIME_LOGIC.md §3.1 documenting the bounded-upload contract. Validated: 92/92 cpput, 8/8 a5sim hbg scene tests (vector_example, paged_attention, prepared_callable), a5 + a5sim runtimes compile clean.
) runtime_init_data_from_layout initialized the orchestrator against the device SM while building the prebuilt arena, but its only caller (the host bind path in runtime_maker) immediately re-initializes the orchestrator against the host SM in run_host_orchestration, once the host SM buffer is allocated, then relocates it for the device. The first init is therefore dead work: its arena content (the tensormap and fanin_seen_epoch memsets) is overwritten by the re-init, and the orchestrator arena block is not uploaded to the device at all (the AICPU boots scheduler-only, per #1659). The AICPU never calls runtime_init_data_from_layout, so no device path depends on it either. Initialize only the scheduler here and let run_host_orchestration own the single orchestrator init. rt->orchestrator stays zeroed (from the existing memset) until that point; the intervening runtime_wire_arena_pointers only sets pointers and does not read orchestrator state. Effect (paged_attention, a2a3 onboard, median of 50 rounds): the arena-build phase of bind drops from ~2565 us to ~1373 us (-46%) by removing the redundant tensormap init, cutting host bind wall from ~8677 us to ~7494 us (-13.6%). Device time unchanged. Applies the identical change to the a5 host_build_graph sibling (near-duplicate tree, same double-init pattern, no arch-level divergence): the orchestrator init removed from runtime_init_data_from_layout plus the matching doc-comment updates in pto_runtime2.h and pto_runtime2_init.cpp. All four safety invariants hold on a5 as on a2a3 -- single caller of the 8-arg runtime_init_data_from_layout (the host bind path in runtime_maker), the surviving orchestrator re-init against the host SM in run_host_orchestration, the orchestrator arena block never uploaded, and runtime_wire_arena_pointers between the two only setting pointers. a5 is compile-verified (host lib links clean); not perf-measured, as this box is a2a3 silicon. Testing: full host_build_graph golden suite (22 passed, 2 skipped; the 2 failures, run_stream_reuse and worker_async_fifo, reproduce identically on a baseline build and are unrelated -- arena-bank-commit path, untouched here). Adds a concurrent_prepare_stress white-box test that drives a two-deep overlapping pipeline over both arena banks (each run prepared while its predecessor is active), golden-checked over many iterations; it passes identically on this change and a baseline build, exercising the concurrent prepare/bind path this change touches.
* Fix: port hbg host-overhead reduction (#1659) to a5 Fixes #1716 only. HBG is one runtime kept in sync across two arch trees (#1706), so a5 still paid the full pre-#1659 host cost: a boot-time blanket slot reset, whole-window completion-flag zero, full sm_size SM upload and full arena_size arena upload, and no ready-queue overflow guard. Port the diff onto the a5 tree file-for-file: - Bounded SM H2D: allocate the host SM uninitialized, zero only the header, and upload each segment (descriptors / payloads / slot_states / completion_flags) bounded to [0, total_tasks). total_tasks is range-checked before it sizes the copies. - Init-on-write: per-slot reset_for_reuse() + completion-flag clear move from the boot-time whole-window loop into orch::prepare_task as each slot is claimed. The unclaimed tail is neither initialized, uploaded, nor read. - Skip the host-only orchestrator block (fanin_seen_epoch / scope_tasks / TensorMap) from the arena H2D; an always_assert(orch_start <= orch_end) guards the layout order before slicing it out. - Latch PTO2_ERROR_READY_QUEUE_OVERFLOW from push_ready_routed instead of dropping a ready task into an anonymous forward-progress stall. - Drop the redundant per-entry tensormap stores the preceding memset already zeroed. The ready queues ship in full, mirroring a2a3, so the two trees stay identical for #1715 to build on — a5 does not bound them locally just because graph execution is not yet on a5. Adds tests/ut/cpp/a5/test_hbg_submit_poison.cpp pinning the "every device-read SM field is written at submit" contract, and RUNTIME_LOGIC.md §3.1 documenting the bounded-upload contract. Validated: 92/92 cpput, 8/8 a5sim hbg scene tests (vector_example, paged_attention, prepared_callable), a5 + a5sim runtimes compile clean. * a5 hbg: align pto_orchestrator C-arrays with a2a3 (std::array) char message[1024] -> std::array<char, 1024> (call sites use .data()/.size()); int32_t kernel_ids_capture[3] -> std::array<int32_t, PTO2_SUBTASK_SLOT_COUNT>, removing the hardcoded 3. After this a5/host_build_graph pto_orchestrator.cpp is byte-identical to the a2a3 build.
Human Summary
The host side of HBG was taking 20x more time than the device side. This was due to an unnecessary resetting (zeroing) and copying of the entire scheduling workspace. This PR reduces the H2D transfer to the minimal required information, and the work structures are initialized-on-use, rather than pre-zeroed.
AI Summary
PR: host_build_graph — init-on-write bounded SM upload + skip orch block
Branch
hbg-sm-init-on-write, rebased ontoupstream/main(0659745d, includes #1444graph execution). One commit, +324/−38 across 12 files (core + a guardrail, a regression
test, and doc/comment updates from review).
TL;DR
HBG's per-dispatch wall is 96–99.6% host
bind, andbindwas dominated byrebuilding and H2D-uploading full, ring-sized host structures every run — the
shared-memory mirror and the ~20 MB prebuilt runtime arena — even though a run touches
a tiny fraction of either. The device boots scheduler-only and reads no SM slot past
total_tasks, so the SM mirror is made init-on-write and shipped bounded to the taskcount, and the arena's ~8.5 MB host-only orchestrator block is dropped from the
upload.
Result — host
bind, A/B vs upstream (a2a3):(bgemm/paged re-measured post-revert; matmul/vector were not re-measured but sit in the same
~8–12 ms small-kernel range — the SM-mirror bind floor once the payload/window uploads are
bounded.) Device time is unchanged (~0.05–0.1 ms) — the whole win drops into per-dispatch
latency. Note: an earlier revision also bounded the ready-queue upload, taking bgemm bind to
~4 ms; that was reverted for #1444 graph-execution compatibility (see Fix), so the queue
portion of the win is not in this PR.
Problem
Splitting
bind(via[STRACE]markers) put nearly all of it inrun_host_orchestration/bind_callable_to_runtime_impl, in two structures that are both sized by ring capacity andrebuilt+reuploaded every run:
host_sm_buf(sm_size, 0)): 81 MB (default ring) to 651 MB(4 GB ring), of which ~97% is the payload segment.
tensormap (65536 entries).
A run uses a few dozen of the 16k–131k slots, so almost all of the alloc/zero/init and the
uploads is work on capacity that is never touched.
Fix
The device reads no slot past
total_tasks. So:Shared memory — descriptors and payloads are written per task at submit; slot_states and
completion_flags are reset per slot in
orch::prepare_taskas it is claimed (dropping thewindow-wide reset loop in
init_header_per_ring); only the header is zeroed on the host;each segment is H2D-uploaded bounded to
[0, total_tasks).Runtime arena —
host-only dep-computation scratch the AICPU scheduler never reads. Everything from the
scheduler block onward (ready queues, runtime header, mailbox) still ships whole.
memsetalreadyzeroes the link pointers and
producer_task_id).The ready queues are not bounded to
total_tasks. An earlier revision seeded/shipped thembounded (with a
+1sentinel for the batched-dequeue boundary), but that is incompatible with#1444 graph execution: a replayed GRAPH task is expanded by the device Scheduler into on-device
nodes that push into the ready queues past the host task count, so every queue slot must carry
a valid Vyukov sequence on the device. The queues therefore ship in full. (This cost the queue
portion of the arena win but leaves the dominant SM win intact — see below.)
total_tasksis range-checked before it sizes the SM copies. Single-ring (PTO2_MAX_RING_DEPTH == 1).Why it is safe
Every scheduler-read SM field is explicitly initialized at submit, so nothing depends on the
removed blanket zeros; reads are bounded by
current_task_index. The orchestrator block ishost-only (verified: zero AICPU-scheduler references); the tensormap-reset change is a pure
dedup of what
memsetalready wrote. The ready queues ship in full, so graph execution'son-device node expansion pushes into fully-seeded slots.
Validation
100-round golden × 4 workloads PASS (bgemm, matmul, vector, paged_attention). The full HBG
a2a3 scene-test suite is green — including the
graph_executioncases (28 passed, 0 failedper pass) and
dep_gen— under pytest-xdist cross-run stress with 0 scheduler stalls / 0op-execute timeouts, and ut-cpp is 83/83. (The
graph_executioncompatibility is load-bearing:the queue-bounding revision failed those, which is why the ready queues now ship in full.)
Guardrails and tests (from review)
push_ready_routedchecks the push return andlatches
PTO2_ERROR_READY_QUEUE_OVERFLOWon failure — a genuinely full queue (taskwindow or graph-node expansion past the 65536 capacity) would otherwise drop a ready
task and stall. Zero cost on the success path.
bind_callable_to_runtime_implalways_assertsorch_start <= orch_endbefore slicing the host-only orchestrator block out of theupload, so a future
runtime_reserve_layoutreorder faults instead of shipping amisaligned image.
tests/ut/cpp/a2a3/test_hbg_submit_poison.cppfills the SM window with0xAA, submits a representative mix (real mixed task with tensors + scalar, multi-faninconsumer, hidden-alloc, dummy), and asserts every device-read slot field carries a real
value, not poison — so a future device-read field added without a submit-path write fails
in-tree instead of non-deterministically on device.
reset_for_reuserelocation (init →prepare_task) isreflected everywhere it was described, and the H2D contract is documented in
RUNTIME_LOGIC.md §3.1.Scope / not done
host_build_graphonly.execution's on-device node expansion. Recovering that ~6.7 ms would need a device-side
scheme that seeds the slots a graph run actually reaches; left as a follow-up.
print_statsscans the whole entry pool and dereferences entries, and the pool has arecycling free-list, so bounding its reset is high-risk for a sub-noise gain. The 8 MB
memsetstays.