Skip to content

host_build_graph: dedicated resolution thread (3S+1P) - #1544

Merged
ChaoZheng109 merged 3 commits into
hw-native-sys:mainfrom
ChaoZheng109:hbg-3s1p-prototype
Jul 29, 2026
Merged

host_build_graph: dedicated resolution thread (3S+1P)#1544
ChaoZheng109 merged 3 commits into
hw-native-sys:mainfrom
ChaoZheng109:hbg-3s1p-prototype

Conversation

@ChaoZheng109

Copy link
Copy Markdown
Collaborator

What

Split the host_build_graph AICPU threads into 3 core-owning schedulers (S) + 1 core-less resolution thread (P) (generally: aicpu_thread_num - 1 S threads + 1 P, enabled at ≥2 threads).

  • S polls the COND registers of its own AICore cores, detects FIN, frees the core immediately (core reuse never waits on P), and hands the finished task to P over a per-S SPSC queue (single-producer/single-consumer, plain release/acquire, no CAS).
  • P owns no cores. It drains the S→P queues and runs on_task_complete for every finished task — publish completion_flags, drain the wake list, advance completed_watermark. P owns completed_tasks_ and the terminal completed_ flip, so the S threads keep dispatching until P has resolved the whole graph (the host's wait_for_consumers never sees a stranded prefix).

Because P is the sole producer of the ready queues, the enqueue CAS never contends and the wake-list drain is single-threaded. Async deferred completions and dependency-only retirement (dummy + predicate-failed tasks) also move to P, so the S loop stays purely core-local — poll own COND, dispatch own cores — with no per-iteration polling of the shared mailbox / dummy queue.

Scope: a2a3 host_build_graph only.

Why

Under the polling-completion model (#1435), the 4 symmetric scheduler threads each do completion and resolution and dispatch, so the wake-list CAS and the MPMC ready_queues[] enqueue contend across all 4. Concentrating resolution on one thread removes that contention and keeps the dispatch threads' hot loop core-local.

Performance (a2a3, single die, pinned device, L2-swimlane device-execution span)

Same binary A/B (4S = this change reverted, 3S+1P = this change):

workload shape util 4S 3S+1P Δ
paged_attention Case1 (65536 tasks) wide, scheduler-bound 67% 29.5 ms 25.4 ms −14% (4 runs, 26.0–26.4→25.4 stable)
dep-bound A/B wide (matmul/add, 128 chains) wide, parallel 88% 0.44 ms 0.43 ms −3.8%
qwen3-14B decode, 40 layers, batch16/seq3500 wide, compute-bound 98% 738 ms 740 ms ~0% (no headroom)
dep-bound A/B deep (single serial chain) serial, latency-bound 60% 2.39 ms 2.44 ms +2.3%
deep_chain (512-deep add-scalar) serial, latency-bound 47% 3.2 µs/edge 3.35 µs/edge +4.7%

Reading:

  • Scheduler-bound + parallel → speedup (pa −14%): the scheduler-thread completion phase roughly halves as resolution contention leaves the hot path (3.55M→1.74M cyc).
  • Compute-bound → neutral (qwen batch16 util 98%): the scheduler is not the bottleneck; 3S+1P still cuts scheduler work ~13% but there is no idle headroom to reclaim.
  • Serial/latency-bound → small cost (deep +2–5%): each dependency edge pays one extra S→P→S cross-core round-trip that a serial chain cannot hide behind parallel work.

The dep-bound A/B holds kernels and tile size constant and varies only the dependency structure, isolating the direction split to the resolution path.

Correctness: matmul, bgemm, paged_attention (Case1 + Case2 SPMD), vector_example, predicated_dispatch, and the 2-layer qwen3 decode graph all pass on 3S+1P.

Notes / open questions

  • Async completions are polled by P but a workload with no async (all cases here) never exercises that path; correctness holds either way (on_task_complete is internally CAS-safe).
  • The serial cost is inherent to splitting detect→resolve→dispatch across two cores (one 64-byte slot-state cache line + the ready_queue line bounce S↔P per edge). A conditional single-consumer inline fast path was considered but not included — it risks re-introducing ready-queue contention on the wide case (real workloads are majority fanout-1), so it needs its own measurement before landing.

🤖 Generated with Claude Code

Split the AICPU threads into (aicpu_thread_num - 1) core-owning schedulers
(S) plus one core-less resolution thread (P); enabled whenever there are
at least two threads. Each S thread detects FIN on its own cores, frees
the core immediately, and hands the finished task to P over a per-S SPSC
queue. P runs on_task_complete for every finished task — publish
completion_flags, drain the wake list, advance the watermark — and owns
completed_tasks_ and the terminal completed_ flip, so the schedulers keep
dispatching until P has resolved the whole graph and the host's
wait_for_consumers never sees a stranded prefix.

Because P is the sole producer of the ready queues, the enqueue CAS never
contends and the wake-list drain is single-threaded. Async deferred
completions and dependency-only retirement (dummy tasks and
predicate-failed tasks, both routed to dummy_ready_queue) also move to P,
so the scheduler loop stays purely core-local — poll own COND, dispatch
own cores — with no per-iteration polling of the shared mailbox or dummy
queue.

Measured on a2a3, paged_attention batch=256 (65536 tasks) on one die:
29.5ms (4S) -> 25.4ms (3S+1P), -14%. The scheduler-thread completion
phase roughly halves as the resolution contention leaves the hot path.
The dependency-bound A/B (matmul/add, tile size held constant) confirms
the direction splits with graph shape: a wide parallel graph speeds up,
a single serial chain costs a few percent from the extra S->P hop.
@coderabbitai

coderabbitai Bot commented Jul 28, 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: aa4581ec-a854-40ec-a9a8-0da60113814e

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

The scheduler adds a dedicated resolution thread for configurations with at least two AICPU threads. Scheduler threads enqueue completed tasks into SPSC queues, while the resolution thread processes task completions, deferred completions, and dummy-ready tasks.

Changes

Dedicated resolution thread

Layer / File(s) Summary
Resolution-thread contract and setup
src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_context.h, src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp
Adds SPSC completed-task queues and P-thread state, assigns the last AICPU thread as the resolution thread, and initializes bounded queue storage during orchestration setup.
Completion handoff
src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_completion.cpp
Queues completed slots for the dedicated resolution thread when enabled, while retaining the existing inline completion path otherwise.
Resolution execution and dispatch wiring
src/a2a3/runtime/host_build_graph/aicpu/aicpu_executor.cpp, src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_dispatch.cpp
Runs the dedicated resolution loop and routes deferred-completion and dummy-ready-task resolution away from the P thread’s scheduler path.

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

Sequence Diagram(s)

sequenceDiagram
  participant AICPUExecutor
  participant SchedulerThreads
  participant SPQueues
  participant ResolutionThread
  participant SchedulerContext
  AICPUExecutor->>SchedulerThreads: run resolve_and_dispatch
  SchedulerThreads->>SPQueues: enqueue completed slot
  AICPUExecutor->>ResolutionThread: run run_resolution_thread
  ResolutionThread->>SPQueues: dequeue completed slot
  ResolutionThread->>SchedulerContext: call on_task_complete
  ResolutionThread->>SchedulerContext: resolve deferred and dummy tasks
Loading

Possibly related PRs

Poem

A rabbit hops where queues align,
S threads pass completed signs.
One P thread resolves the trail,
Deferred tasks and dummies sail.
Ring buffers hum through scheduler night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a dedicated resolution thread for host_build_graph.
Description check ✅ Passed The description is detailed and directly matches the implemented 3S+1P scheduler/resolution thread changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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: 2

🧹 Nitpick comments (3)
src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp (1)

1050-1063: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Capacity is sized by total_tasks, but the queue only needs to hold in-flight completions.

At the 1<<20 ceiling this allocates 8 MB per scheduler thread (24 MB for 3S) even though the number of unresolved completions in flight is bounded by the ring's task window, not by the total task count. Sizing from ring.task_window_size (rounded up, floored at 256) would keep the same no-spin property with far less memory.

Also, when total_tasks > (1u << 20) the capacity is clamped below total_tasks, so the push() backpressure spin can engage — that contradicts the "never fires in practice" claim in the CompletedTaskQueue header comment. The spin is safe (P drains independently), but the comment should be softened.

♻️ Suggested sizing
     if (p_thread_mode_) {
         uint64_t cap = 256;
-        while (cap < static_cast<uint64_t>(total_tasks) && cap < (1u << 20)) {
+        // In-flight completions are bounded by the task window, not by the
+        // total task count.
+        const uint64_t needed = static_cast<uint64_t>(total_tasks);
+        while (cap < needed && cap < (1u << 20)) {
             cap <<= 1;
         }
#!/bin/bash
rg -nP --type=cpp -C3 '\btask_window_size' src/a2a3/runtime/host_build_graph | head -60
🤖 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 1050 - 1063, Size each per-thread CompletedTaskQueue in the
p_thread_mode_ initialization block from ring.task_window_size instead of
total_tasks, rounding up to a power of two with a minimum capacity of 256 and no
total-task-based ceiling. Update the CompletedTaskQueue header comment to avoid
claiming push backpressure never occurs, while preserving that its spin remains
safe because P drains independently.
src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_context.h (1)

54-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider guarding init against a non-power-of-two capacity and deleting copy/move.

mask = capacity_pow2 - 1 silently corrupts the ring if a future caller passes a non-power-of-two. Also, the raw owning buf with no rule-of-5 handling means an accidental copy of SchedulerContext would double-free.

♻️ Suggested hardening
     void init(uint64_t capacity_pow2) {
+        debug_assert(capacity_pow2 != 0 && (capacity_pow2 & (capacity_pow2 - 1)) == 0);
         cap = capacity_pow2;
🤖 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_context.h`
around lines 54 - 64, Harden SchedulerContext::init by validating that
capacity_pow2 is nonzero and a power of two before computing mask, using the
existing project error-handling convention for invalid input. Make
SchedulerContext non-copyable and non-movable (or otherwise implement complete
ownership-safe rule-of-five behavior) so buf cannot be double-freed through
accidental copies or moves; preserve destroy’s ownership cleanup.
src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_dispatch.cpp (1)

986-991: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

P returns without the DFX/PMU teardown that resolve_and_dispatch performs.

resolve_and_dispatch ends with log_l2_swimlane_summary, l2_swimlane_aicpu_flush, l2_swimlane_aicpu_flush_sched_phase_buffer, dump_args_flush and pmu_aicpu_flush_buffers (Lines 1530-1554), and resets sched_l2_swimlane_[thread_idx] at entry. P does neither, so any per-thread DFX records written under thread_idx == p_thread_idx_ (e.g. from on_task_complete / poll_and_complete in profiling builds) are dropped. Worth adding at least the swimlane/dump flushes for P.

🤖 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_dispatch.cpp`
around lines 986 - 991, Update P to perform the same per-thread DFX/PMU teardown
as resolve_and_dispatch before returning completed_tasks_: call
log_l2_swimlane_summary, l2_swimlane_aicpu_flush,
l2_swimlane_aicpu_flush_sched_phase_buffer, dump_args_flush, and
pmu_aicpu_flush_buffers for p_thread_idx_. Also reset
sched_l2_swimlane_[thread_idx] on entry so profiling records are retained and
flushed consistently.
🤖 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/scheduler/scheduler_completion.cpp`:
- Around line 186-194: Update the p_thread_mode_ branch in the scheduler
completion path so sched_->tasks_completed is incremented when the finished task
is handed to P, matching the completed_tasks_ update performed by P. Use the
existing atomic fetch_add accounting mechanism and ensure this applies when
task_complete and t.running_done are observed without changing non-P completion
behavior.

In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_dispatch.cpp`:
- Around line 969-986: Update P’s stall-detection block in the scheduler
dispatch flow to mirror the running-task guard used by resolve_and_dispatch:
only latch PTO2_ERROR_SCHEDULER_TIMEOUT and call emergency_shutdown when
self_owns_running_task(thread_idx) is true, or when no thread owns a running
task and the existing outstanding-work conditions hold. Otherwise refresh
last_progress_ts and continue spinning, preventing long-running or
startup-skewed tasks from triggering a false timeout.

---

Nitpick comments:
In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp`:
- Around line 1050-1063: Size each per-thread CompletedTaskQueue in the
p_thread_mode_ initialization block from ring.task_window_size instead of
total_tasks, rounding up to a power of two with a minimum capacity of 256 and no
total-task-based ceiling. Update the CompletedTaskQueue header comment to avoid
claiming push backpressure never occurs, while preserving that its spin remains
safe because P drains independently.

In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_context.h`:
- Around line 54-64: Harden SchedulerContext::init by validating that
capacity_pow2 is nonzero and a power of two before computing mask, using the
existing project error-handling convention for invalid input. Make
SchedulerContext non-copyable and non-movable (or otherwise implement complete
ownership-safe rule-of-five behavior) so buf cannot be double-freed through
accidental copies or moves; preserve destroy’s ownership cleanup.

In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_dispatch.cpp`:
- Around line 986-991: Update P to perform the same per-thread DFX/PMU teardown
as resolve_and_dispatch before returning completed_tasks_: call
log_l2_swimlane_summary, l2_swimlane_aicpu_flush,
l2_swimlane_aicpu_flush_sched_phase_buffer, dump_args_flush, and
pmu_aicpu_flush_buffers for p_thread_idx_. Also reset
sched_l2_swimlane_[thread_idx] on entry so profiling records are retained and
flushed consistently.
🪄 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: d0492dc7-e40b-495c-a248-5e1c41c361f6

📥 Commits

Reviewing files that changed from the base of the PR and between f293be3 and b9e3e0d.

📒 Files selected for processing (5)
  • src/a2a3/runtime/host_build_graph/aicpu/aicpu_executor.cpp
  • 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

Comment thread src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_completion.cpp Outdated
Comment thread src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_dispatch.cpp Outdated
…eue sizing

- P's idle timeout now only latches when work is outstanding AND no thread
  owns a running task (mirrors resolve_and_dispatch); a task that legitimately
  runs longer than the budget refreshes last_progress_ts instead of
  false-latching PTO2_ERROR_SCHEDULER_TIMEOUT.
- P resets its per-thread swimlane state on entry and flushes swimlane /
  dump-args / PMU buffers on exit, so DFX records written under p_thread_idx_
  are not dropped.
- P mirrors completed_tasks_ into the SIMPLER_SCHED_PROFILING tasks_completed
  counter, which the scheduler threads' completed_this_turn no longer feeds.
- Size each CompletedTaskQueue to min(total_tasks, ring task window) instead of
  total_tasks capped at 1<<20 — the window already bounds in-flight completions,
  so there is no ceiling that could clamp capacity below the working set and
  engage the backpressure spin. Assert power-of-two capacity; note the atomic
  cursors already make the queue non-copyable.
@ChaoZheng109

ChaoZheng109 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Addressed in d0f145aa:

Actionable

  • P false-timeout guard (scheduler_dispatch.cpp) — fixed. P's idle timeout now latches only when work is outstanding and no_thread_owns_running_task(), mirroring resolve_and_dispatch; a task that legitimately runs longer than the budget refreshes last_progress_ts instead of aborting the run. This was a real robustness bug (a long kernel / startup skew could false-latch PTO2_ERROR_SCHEDULER_TIMEOUT), so thanks for catching it.
  • P DFX/PMU teardown (scheduler_dispatch.cpp) — fixed. P resets its per-thread swimlane state on entry and flushes swimlane / dump-args / PMU on exit (the AICore-keyed flushes iterate an empty core list since P owns none; the sched-phase-buffer flush is the one that matters).
  • SCHED_PROFILING tasks_completed (scheduler_dispatch.cpp) — fixed. P now mirrors completed_tasks_ into the profiling counter, since the scheduler threads' completed_this_turn no longer feeds it in P mode.

Nitpicks

  • Queue capacity — now sized to min(total_tasks, ring task window), no 1<<20 ceiling. The window already bounds in-flight completions, so capacity is never clamped below the working set and the backpressure spin stays a pure correctness backstop. Header comment softened accordingly.
  • init hardening — added a power-of-two debug_assert. The std::atomic cursors already make CompletedTaskQueue (and therefore SchedulerContext) non-copyable/non-movable, so the accidental-copy double-free can't occur; called that out in the comment rather than adding redundant deleted members.

Performance (this fixed commit d0f145aa vs 4S baseline). Re-ran all five reference workloads on the same die (dev6), paired same-session, L2-swimlane device-execution span. The fixes are on init / idle-timeout / teardown / profiling paths only — never the hot SP-drain + resolve + dispatch path — so the 3S+1P benefit is intact (a same-binary fixed-vs-pre-fix check was within run-to-run noise across all five):

workload shape 4S 3S+1P (fixed) Δ
pa Case1 (wide, 65536 tasks) scheduler-bound 29.68 ms 24.44 ms −17.6%
dep-bound wide parallel 0.43 ms 0.42 ms −1.1%
qwen3-14B decode, 40L batch16 compute-bound (util 98%) 35.85 ms 35.78 ms −0.2%
dep-bound deep serial / latency 2.39 ms 2.46 ms +2.8%
deep_chain (512 serial) serial / latency 1.61 ms 1.70 ms +6.1%

Direction is unchanged from the original write-up: scheduler-bound wide graphs speed up (−17.6% on pa here), compute-bound is neutral (no headroom), and a pure serial chain pays a few percent for the extra S→P hop that it cannot hide behind parallel work. All five pass (--skip-golden for the perf runs; correctness verified separately on matmul/bgemm/paged_attention/vector_example/predicated_dispatch).

🤖 Generated with Claude Code

…back branches

The dedicated resolution (P) thread is not optional: like tmr's scheduler +
orchestrator split, host_build_graph needs at least two AICPU threads (one
core-owning scheduler + one resolution thread). A single thread cannot both own
cores and resolve on a dedicated thread, and no config runs fewer than two
threads (scene tests use 3 or 4; the default is 3).

Require aicpu_thread_num >= 2 in assign_cores_to_threads and remove the
`p_thread_mode_` flag and every branch on it. This deletes the retained inline
completion path from complete_slot_task and the per-iteration async-poll and
dummy/predicate-drain blocks from resolve_and_dispatch — those were dead in the
only supported configuration and only added predicted branches to the
scheduler's hot loop. Async, dummy and predicate resolution live solely in
run_resolution_thread now; the scheduler loop goes straight from completion
detection to dispatch.

Behavior is unchanged for the >= 2 thread configs (all of them). matmul, bgemm,
paged_attention, vector_example and predicated_dispatch (the dummy / predicate
path, now P-only) all pass.
@ChaoZheng109
ChaoZheng109 merged commit 453ff71 into hw-native-sys:main Jul 29, 2026
17 checks passed
@ChaoZheng109

Copy link
Copy Markdown
Collaborator Author

Follow-up: rigorous 100-round re-measurement (corrects the single-run numbers above)

The per-workload percentages I posted earlier were single-run and, for the
sub-3 ms dependency cases, dominated by noise. Re-measured properly:

Method — same physical card (a2a3 silicon, device 6, task-submit-locked),
100 rounds per workload, drop round 0 (warm-up), IQR-trim outliers (1.5×IQR
fence), then mean. Device time from the runner_run.device_wall STRACE marker.
cv = coefficient of variation of the trimmed set. 3S+1P is main @ 453ff71b;
4S baseline is the pre-#1544 runtime (96d02914) rebuilt against the same HEAD.

workload scale 4S 3S+1P Δ cv
paged_attention (scheduler-bound) 65792 tiny tasks 28.016 ms 25.013 ms −10.7% 0.2%
qwen3 2-layer (compute-bound) 36.222 ms 35.952 ms −0.7% 0.1%
deep_big (serial chain, latency-bound) 4096 serial 19.048 ms 19.091 ms +0.2% 0.3%
wide_big (parallel, many tasks) 49152 tasks 21.266 ms 21.288 ms +0.1% 0.2%

The earlier small-case results were measurement artifacts. At 1–2 ms the same
workloads showed deep_chain +9.4%, dep-deep +5.7%, dep-wide +3.8% — which read
like a regression. Scaled to the same ~20 ms magnitude as pa/qwen, those deltas
collapse to ±0.2% (noise). The gap was a one-off fixed cost (one fewer
dispatch thread + a single S→P handoff per completion) that is a large fraction
of a 1 ms run and negligible in a 20 ms one — not a per-task cost that
accumulates with scale. There is no real regression on dependency-bound work.

Why pa is the only real win, and it is not diluted: the win requires the
scheduler to be the bottleneck — many tiny tasks, low per-task compute
(pa: util 67%). There, eliminating the multi-producer contention on the ready
queue / wake lists (single P producer) is worth −10.7%. My synthetic dep cases
use 128×128 matmul, so even at 49152 tasks the cores stay the bottleneck
(compute-bound, like qwen) → neutral. Heavier or longer tasks push toward
compute-bound, away from the regime 3S+1P helps.

Net: 3S+1P is a scenario-specific optimization — a solid, reproducible
−10.7% on scheduler-throughput-bound graphs (many small tasks), neutral on
serial-latency-bound and compute-bound graphs, and no measured regression at
realistic scale.

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