[Performance] host_build_graph: shrink ready-queue capacity 65536 -> 8192 - #1762
Conversation
📝 WalkthroughWalkthroughThe per-shape ready-queue capacity changed from 65536 to 8192. Comments now define the capacity as a power-of-two bound based on peak concurrent occupancy. Queue overflow continues to use ChangesReady Queue Capacity
Estimated code review effort: 2 (Simple) | ~5 minutes Possibly related issues
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 |
791f4e3 to
ea9f4c7
Compare
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/runtime/pto_runtime2_types.h`:
- Around line 97-103: Protect graph queues before applying the shared
PTO2_READY_QUEUE_SIZE capacity: either validate a separate capacity for
graph_ready_queue and graph_prepare_queue, or add overflow detection and
PTO2_ERROR_READY_QUEUE_OVERFLOW handling in the graph scheduling path around
graph_ready_queue.push(slot_state). Ensure graph-queue occupancy is covered by
the capacity sizing so failed pushes cannot silently drop tasks or stall
execution.
🪄 Autofix
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: 3dd7cc12-d96e-491d-a28f-13b450450f7d
📒 Files selected for processing (1)
src/a2a3/runtime/host_build_graph/runtime/pto_runtime2_types.h
The per-shape ready queue is a Vyukov ring buffer whose capacity bounds peak CONCURRENT occupancy (enqueue_pos - dequeue_pos), not total task count. Slots recycle, so capacity need only exceed the most tasks ever simultaneously ready in any one queue. reserve_layout allocates 9 such queues (3 ready + 3 sync + dummy + graph_ready + graph_prepare) at PTO2_READY_QUEUE_SIZE * 24 B each, so at 65536 they occupy ~13.5 MB of the per-run runtime arena. That arena is rebuilt (the queue seed loop) and uploaded H2D on every bind, making the oversized queues the dominant cost of both phases. Peak occupancy measured across the HBG onboard suite (bgemm, matmul, vector, paged_attention, graph_execution x4, predicated) maxes at 106 (qwen3 3-layer graph); paged_attention is 1, bgemm 64. 8192 gives 77x headroom over the observed max while cutting the queue footprint 8x (to ~1.7 MB). Effect (paged_attention, a2a3 onboard, median of 50 rounds): host wall 8627 -> 5037 us (-41.6%) arena_upload 2667 -> 360 us (-86%) arena_build 2623 -> 1490 us (-43%) device_wall 71 -> 76 us (unchanged) Golden clean on paged_attention / bgemm / graph_execution / graph_mix_spmd; no READY_QUEUE_OVERFLOW latched. All nine queues share PTO2_READY_QUEUE_SIZE, so the shared cut also makes the graph_ready path safe-fail: push_ready_routed previously ignored the return of graph_ready_queue.push(), so a full graph_ready_queue would silently drop the task and stall the run, unlike the ready/sync/dummy queues that latch PTO2_ERROR_READY_QUEUE_OVERFLOW. Route the graph task through the same checked path so every queue that shares the constant reports overflow as a named error rather than an anonymous forward-progress timeout. (graph_prepare_queue pushes are while(!push) spin-retries that already wait for space instead of dropping.) Scope: a2a3 host_build_graph only. a5 HBG carries the same constant but is left unchanged pending an a5 peak-occupancy measurement (different core counts yield a different ready burst); tensormap_and_ringbuffer is a separate runtime and out of scope.
ea9f4c7 to
29e4738
Compare
…h_ready safe-fail (#1773) Mirror of #1762 for a5. a5 gained Graph execution in #1733, which also copied a2a3's pre-#1762 push_ready_routed — including the graph_ready push that ignored its return value, so a full graph_ready_queue would silently drop a task and stall. Apply the same two changes #1762 landed for a2a3: - PTO2_READY_QUEUE_SIZE 65536 -> 8192. The Vyukov ring bounds peak concurrent occupancy (enqueue_pos - dequeue_pos), not total task count, so capacity need only exceed the worst-case ready burst with margin. - push_ready_routed: route the GRAPH task through the same checked push as ready/sync/dummy so a full graph_ready_queue latches PTO2_ERROR_READY_QUEUE_OVERFLOW (named emergency_shutdown) instead of a silent drop. a5 push_ready_routed was byte-identical to a2a3 pre-#1762, so this is a 1:1 mirror; both regions are now identical across the two arches. The PTO2_ERROR_READY_QUEUE_OVERFLOW 104 code was already added to a5's error enum by #1733, so no enum change is needed here.
Human Summary
The 65k ready queue capacity is excessive; it is filled with the initial set of ready tasks and then during runtime as scheduler threads detect ready-to-go tasks, and; emptied, as tasks are ultimately scheduled for execution. The capacity of this queue then obeys to a runtime amplitude between how fast schedulers can add them and how AI cores can execute them.
The queue use amplitude never seems to get anywhere near the maximum here, which seems to be a couple orders of magnitude larger than it needs to be, at least. This PR safely reduces the size of the queue, reducing its memory footprint, but also the computational need to move this array from host to device before kernel execution.
AI Summary
PR notes —
host_build_graph: shrink ready-queue capacity 65536 → 8192hbg-ready-queue-8192(commit29e4738c, offupstream/maincf0fbc06)host_build_graphonlyTL;DR
The per-shape ready queue is a Vyukov ring buffer whose capacity bounds peak
concurrent occupancy, not total task count. At 65536 the nine ready queues
occupy ~13.5 MB of the per-run runtime arena, which is rebuilt and uploaded H2D
on every bind. Measured peak occupancy across the whole HBG onboard suite is
≤ 106 (paged_attention = 1, bgemm = 64), so 65536 was ~600–65000× oversized.
Dropping to 8192 keeps 77–128× headroom and cuts host bind wall by −41.6% on
paged_attention (8627 → 5037 µs), golden-clean, with device time unchanged.
Why the constant was oversized
PTO2ReadyQueueis a Vyukov MPMC ring (slots[capacity],enqueue_pos,dequeue_pos,mask = capacity − 1). Two facts:a queue of capacity C processes unlimited tasks as long as no more than C
are ever simultaneously enqueued-but-unconsumed (
enqueue_pos − dequeue_pos).65536 is therefore a worst-case peak-occupancy bound, not one-slot-per-task.
PTO2SchedulerState::reserve_layoutallocates 9 full-size queues — 3 ready + 3 ready-sync + dummy + graph_ready
capacity × sizeof(PTO2ReadyQueueSlot)(24 B). At 65536that is
9 × 65536 × 24 B ≈ 13.5 MB. This region is re-seeded (arena_build,the
sequence = iloop over every slot) and copied H2D (arena_upload) onevery
bind. After PR [Performance] HBG: up to 99% host-side overhead reduction #1659 skipped the ~8.5 MB orchestrator block from theupload, these queues are the single largest remaining bind cost.
So the queues were being rebuilt and re-shipped at 65536-slot size every run to
hold, in practice, ≤ 106 live entries.
The change
8192is a power of two (required — index ispos & mask) and gives 77×headroom over the observed suite max (106) and 128× over the steady per-run
max (bgemm 64).
Graph-queue guardrail (second half of the diff)
All nine queues share
PTO2_READY_QUEUE_SIZE, so the shared cut also has to makethe graph_ready path safe-fail.
push_ready_routedpreviously ignored thereturn of
graph_ready_queue.push(), so a full graph_ready_queue would silentlydrop the task and stall the run — unlike ready/sync/dummy, which latch
PTO2_ERROR_READY_QUEUE_OVERFLOW. The graph task is now routed through the samechecked path, so every queue that shares the constant reports overflow as a
named error rather than an anonymous forward-progress timeout.
graph_prepare_queuepushes (scheduler_cold_path.cpp, scheduler_dispatch.cpp)are already
while (!push_tagged(...))spin-retries that wait for space insteadof dropping, so they are left unchanged.
Measurements
All on a2a3 onboard,
paged_attention, median of 50 rounds, from the[STRACE]bind span tree. The three capacities were measured back-to-back onone locked device to control for contention.
Bind-phase breakdown (µs)
Delta subtotals — Host / Device / Total
Why 8192 and not lower: 65536 → 8192 captures 91% of the achievable win
(−3591 µs); 8192 → 2048 adds only −346 µs (−6.9%, within host-wall noise) while
cutting overflow headroom 4×. The queue is no longer the bottleneck at 8192 —
arena_uploadis already down to 360 µs. Device wall is flat throughout (noon-NPU regression).
Correctness & peak-occupancy evidence
Peak occupancy is measured with a high-water mark on
enqueue_pos − dequeue_possampled at each push (a near-upper-bound: occupancy only rises at an enqueue, so
its max is always caught at a push instant). Suite results:
At both 8192 and 2048: golden clean on paged_attention / bgemm / graph_execution
/ graph_mix_spmd, no
READY_QUEUE_OVERFLOW, no 507018.Risk analysis
What happens if the cap is ever too low (documented for reviewers):
push_taggedreturnsfalseon a full ring (never overwrites an occupied slot → no corruption);push_ready_routedlatchesPTO2_ERROR_READY_QUEUE_OVERFLOW; the scheduler'sidle/exit check sees it and
emergency_shutdowns → the run aborts promptlywith a named error, not a hang, not a wrong answer.
graph_ready_queue.push()usedto ignore its return (silent drop → stall); it now routes through the same
checked path and latches
PTO2_ERROR_READY_QUEUE_OVERFLOW. So all queues thatshare the constant are uniformly safe-fail.
while (!push_tagged(...))spin-retries that wait for space rather than dropping.
Scope
host_build_graphonly. a5 HBG carries the same 65536 constant but isleft unchanged pending an a5 peak-occupancy measurement — a5 has different
core counts, hence a different boot ready-burst.
tensormap_and_ringbufferis out of scope — a separate runtime that usesthe queue differently; not analyzed here.
Follow-ups (not in this PR)
wide_dispatchcoverage — the sim-only SPMD-spill stress(
tests/st/host_build_graph_wide_dispatch/) is the designated wide-SPMDworst case; its kernels don't compile in our container (
_Float16on hostgcc). If CI's sim lane exercises it, that closes the last worst-case gap.
orch_reinit(~1.25 ms) — now the biggest single bind sub-phase andqueue-independent (the orchestrator is initialized twice per bind: device-SM
in
arena_build, then re-initialized against host-SM inrun_host_orchestration). The next lever after this PR.Reproduce