Skip to content

perf(tmr): barrier-free per-thread scheduler init (a2a3 + a5) - #1345

Merged
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
huawei-csl:perf-orchestrator-decouple-draft
Jul 20, 2026
Merged

perf(tmr): barrier-free per-thread scheduler init (a2a3 + a5)#1345
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
huawei-csl:perf-orchestrator-decouple-draft

Conversation

@SergioMartin86

@SergioMartin86 SergioMartin86 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Human Summary

This PR brings the following optimizations to AICPU kernel initialization:

  • The orchestrator thread does not take part in (or waits for) AI core handshaking. It goes straight into creating the task graph
  • The scheduler threads only handshake (as a batch) the AI cores that they manage.
  • Initialization barriers are removed. Once the scheduler threads are done handshaking, they can start working, regardless of what the other scheduler threads are doing.

This optimization improves only the fixed-cost of initialization so it yields higher gains on small kernels. On larger kernels, the gains are diluted out per Amdahl's Law.

Update — a5 port + fixes

Since the original a2a3-only draft:

  • Ported to a5 (src/a5/.../tensormap_and_ringbuffer) — same restructure. a5 shares the 1:2 blocked layout but has RUNTIME_MAX_WORKER=108 (36 AIC + 72 AIV) vs a2a3's 72; every count derives from cores_total_num_, so the port is count-agnostic. assign_own_clusters mirrors each arch's own post_handshake_init: the a2a3 variant also does the AsyncCtx constant-prefill / slab-clear, while a5 does sub_block_id only (a5 writes AsyncCtx per-dispatch in build_payload).
  • Divisibility guardpre_handshake_init now fails fast when cores_total_num_ % 3 != 0 instead of silently leaving the tail AIV cores in no cluster (which would hang at the op-execute timeout). Holds for the serial path too.
  • Comment cleanup — dropped the [SPIKE …] planning tags and rewrote the change-narration comments per the repo's comment rules.
  • CI/lint — fixed clang-format (short-loop) and markdownlint (MD040 fenced-block language, MD060 table alignment) so pre-commit is green.

Verification: a2a3sim (33) + a5sim (26) tensormap_and_ringbuffer suites green; all required CI checks pass, including st-onboard-a2a3 and st-onboard-a5 on real silicon — the a5 onboard run confirms the blocked [0,N/3) AIC / [N/3,N) AIV handshake-enumeration assumption holds on a5 hardware.

⚠️ Measured on the current base (48d92e8), a2a3 onboard, 100 rounds/case: the isolated init/preamble saving is a fixed ~11 µs/run, consistent across 8 workloads regardless of size (the device_wall − effective gap drops 10–12 µs in every case; the larger device_wall swings are orch/sched compute run-to-run variance this PR does not touch). The ~63 µs / −25% numbers in the AI Writeup below are against an older pre-#1310 baseline whose preamble was still ~200 µs — today's main has already cut that, so the removable barrier + leader post_handshake_init is worth ~11 µs. Still real and size-independent (so it helps high-frequency per-op dispatch), just an order of magnitude smaller than the original headline.

AI Writeup

tl;dr for humans

Every AICPU run, the scheduler init still puts an all-thread barrier around
the AICore handshake: all threads greet the 72 cores, then the leader serially
builds the core→thread assignment and prefills every core's dispatch payload,
before any thread starts working. This PR removes that barrier. Each scheduler
thread greets only the cores it will dispatch to (computable up front from
the fixed, blocked 1 AIC : 2 AIV layout), self-assigns them, and starts
dispatching to its own cores as soon as they're up — no waiting for peers, no
leader-serialized setup. The orchestrator thread likewise skips the handshake
and starts building the graph immediately.

Net: ~63 µs of per-run scheduler init removed, entirely in the preamble, by
parallelizing the handshake + core assignment across the threads that own the
cores. The effect scales inversely with run size — a big win on small /
per-op workloads, and neutral (never a regression) on whole-model
decode/prefill where the fixed cost amortizes.

What changes

  1. Orchestrator decouple. The orchestrator (tidx == nthreads-1) doesn't
    dispatch to cores, so it skips the handshake and returns to run() to build
    the graph immediately, deriving AIC/AIV counts from the fixed 1:2 ratio.
  2. Barrier-free per-thread scheduler init. Each scheduler thread handshakes
    only its owned clusters (ci = {ci, N/3+2ci, N/3+2ci+1}, owned by ci % active_sched_threads_) via handshake_owned_clusters (batched 4-phase
    protocol, 2 barriers) and self-assigns them via assign_own_clusters — no
    all-thread barrier, no leader-serialized post_handshake_init. pre_handshake _init derives the counts + resets the run counters. DFX builds keep a
    profiling-only barrier (pmu needs all physical_core_ids_); serial /
    single-thread fall back to the original path.

Why it's safe: the blocked [AIC…][AIV…] layout and 1:2 ratio are
compile-time-and-hardware-enforced invariants (identical on sim + onboard), so
each thread's owned cluster set is exact and known before the handshake.

Mechanism

The saving is entirely in the preamble phase. hc_post, strace_timing --tree, this PR vs baseline:

phase this PR baseline Δ
preamble 155.6 µs 218.4 µs −62.8 µs
arena_wire / sm_reset / orch ∥ sched / post_orch ~unchanged ~unchanged ~0

A/B benchmark — 37 single-die pypto-lib workloads, 20 rounds

.so-swap A/B, this PR vs the pre-change baseline, default config,
device_wall median over 20 rounds, devices 2–5 (unlocked, verified-idle),
a2a3 / Ascend 910B2. 35/37 faster, zero regressions.

Workload Baseline µs This PR µs Δ Device
ds-v4/hc_post 314.5 234.3 -25.5%
qwen3-14b/token_embed 313.1 241.1 -23.0%
ds-v4/hc_head 328.6 254.1 -22.7%
ds-v4/expert_shared 360.5 282.3 -21.7%
ds-v4/gate 336.9 271.3 -19.5%
ds-v4/decode_compressor_ratio4 354.2 286.2 -19.2%
ds-v4/decode_indexer_compressor 360.1 291.3 -19.1%
ds-v4/prefill_compressor_ratio128 402.1 326.5 -18.8%
ds-v4/rmsnorm 183.2 149.6 -18.3%
ds-v4/decode_indexer 393.1 327.1 -16.8%
ds-v4/hc_pre 244.2 204.2 -16.4%
ds-v4/decode_compressor_ratio128 362.6 303.9 -16.2%
ds-v4/decode_sparse_attn 515.7 433.6 -15.9%
ds-v4/prefill_compressor_ratio4 431.0 364.1 -15.5%
ds-v4/decode_sparse_attn_swa 495.7 423.4 -14.6%
ds-v4/decode_attention_swa 617.4 531.0 -14.0%
ds-v4/decode_sparse_attn_hca 492.1 423.6 -13.9%
ds-v4/prefill_indexer_compressor 521.5 451.8 -13.4%
ds-v4/decode_attention_hca 686.0 597.2 -12.9%
qwen3-14b/greedy_sample 550.0 489.6 -11.0%
ds-v4/decode_attention_csa 712.8 634.8 -10.9%
ds-v4/prefill_indexer 669.6 603.8 -9.8%
ds-v4/prefill_sparse_attn 874.4 802.9 -8.2%
ds-v4/expert_routed 799.0 735.6 -7.9%
ds-v4/qkv_proj_rope 470.9 435.3 -7.5%
ds-v4/prefill_attention_hca 1523.3 1444.6 -5.2%
ds-v4/prefill_attention_swa 1497.5 1420.8 -5.1%
ds-v4/prefill_attention_csa 1929.2 1859.7 -3.6%
qwen3-32b/decode 2202.6 2123.3 -3.6%
ds-v3.2/decode_back 2415.2 2330.4 -3.5%
ds-v4/mtp_projection 1516.8 1478.8 -2.5%
qwen3-14b/decode_layer 1220.6 1203.2 -1.4%
ds-v3.2/prefill_back 10778.5 10711.5 -0.6%
qwen3-32b/decode_4d 2727.5 2716.3 -0.4%
qwen3-14b/decode_tq (40L) 123577.1 123405.4 -0.1%
qwen3-14b/prefill_tq_draft 467239.2 467461.6 +0.1%
qwen3-14b/prefill_fwd 38167.9 38228.2 +0.2%

By size (the ~63 µs is a fixed per-run cost, so % impact ≈ 63 µs / run):

workload size (device_wall) n median Δ Device
tiny < 1 ms (component kernels) 25 −15.9%
medium 1–10 ms 8 −3.6%
large > 10 ms (whole-model) 4 −0.0%

Reading the numbers — single-round vs amortized

These harnesses run one round per process, so device_wall includes the
per-run scheduler init. That's why standalone component kernels show large wins
while whole-model runs (decode_tq 40L −0.1%, prefill_tq_draft +0.1%) are
neutral — the ~63 µs amortizes across a 120–470 ms forward. For reference, the
in-repo scene test benchmark_bgemm (100 rounds in one warm process, init
amortized) shows ~−0.7%. So end-to-end impact depends on serving
granularity
: per-op / per-layer dispatch pays the init each time (large win);
whole-model-per-step amortizes it (neutral, never a regression).

Correctness

Golden PASS on the current base: benchmark_bgemm, alternating_matmul_add,
batch_paged_attention, qwen3_14b_decode; decode_tq 4L/8L complete +
validate. Zero regressions across the 37-workload × 20-round sweep above.

Scope / follow-ups

  • a5 sim-verified, onboard pending — the port passes a5sim golden, but the
    self-assign path assumes the AICore handshake reports cores in blocked
    [0,N/3) AIC / [N/3,N) AIV order; no a5 doc guarantees that enumeration, so
    run a5 onboard golden before relying on it.
  • Confirm the target serving path's dispatch granularity (per-layer / per-op vs
    whole-model) to know the end-to-end impact.
  • The barrier-free handshake-failure path (abort_and_shutdown) is documented
    as bounded/best-effort: a peer past run()'s is_completed() gate isn't
    joined, but its own cores are valid and the host device reset that follows a
    handshake failure reaps every core. Still not race-free, and
    emergency_shutdown isn't exercised by golden (the invalid-physical_core_id
    trigger needs a hw/sim fault-injection hook the UT harness lacks).
  • On the decoupled path, handshake_partition / post_handshake_init /
    assign_cores_to_threads now serve only the serial/single-thread fallback +
    the DFX barrier — keep or prune with the fallback's fate.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change decouples orchestrator startup from scheduler handshaking, adds owned-cluster handshake and assignment helpers, moves task-count initialization earlier, preserves the serial fallback, and records implementation details and measurements in investigation documentation.

Changes

Orchestrator and scheduler initialization

Layer / File(s) Summary
Pre-handshake state and lifecycle contract
src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h, src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp, docs/investigations/2026-07-orchestrator-decouple-preamble.md
Adds lifecycle helpers and accessors, initializes core layout and task counts before handshake, and documents deterministic cluster ownership.
Owned-cluster handshake and assignment
src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp
Handshakes only owned clusters, assigns their runtime state without the former leader-serialized initialization, supports profiling completion, and shuts down on handshake failure.
Decoupled executor startup and count derivation
src/a2a3/runtime/tensormap_and_ringbuffer/aicpu/aicpu_executor.cpp, docs/investigations/*, docs/investigations/README.md
Uses the decoupled path for multi-threaded non-serial execution, retains the legacy path otherwise, derives AIC/AIV counts from total cores, and records timing results and open items.

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

Possibly related PRs

  • hw-native-sys/simpler#1176: Introduced related orchestrator-to-scheduler startup synchronization used by this decoupled initialization path.

Sequence Diagram(s)

sequenceDiagram
  participant AicpuExecutor
  participant Orchestrator
  participant SchedulerThreads
  participant SchedulerContext
  participant AICores
  AicpuExecutor->>Orchestrator: Start core-agnostic task-graph initialization
  AicpuExecutor->>SchedulerThreads: Start scheduler initialization
  SchedulerThreads->>SchedulerContext: Handshake owned clusters
  SchedulerContext->>AICores: Collect reports and publish task pointers
  AICores-->>SchedulerContext: Return handshake state
  SchedulerThreads->>SchedulerContext: Assign owned clusters
  SchedulerContext-->>SchedulerThreads: Return dispatch-ready state
  Orchestrator-->>AicpuExecutor: Return from decoupled initialization
Loading

Poem

A bunny hops past barriers bright,
Threads claim clusters left and right.
The graph keeps building, handshakes fly,
Core counts bloom beneath the sky.
Less waiting, more dispatch in sight—
Schedulers spring to life just right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title captures the main change: barrier-free per-thread scheduler initialization, though it also mentions a5, which is not in this changeset.
Description check ✅ Passed The description matches the changeset by explaining orchestrator decoupling, barrier removal, and scheduler-owned core handshakes.

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request parallelizes the per-core AICore handshake during initialization across multiple AICPU threads to reduce preamble overhead. It splits the initialization process into pre-handshake, parallel partitioned handshake, and post-handshake phases for both the a2a3 and a5 architectures. Additionally, for a2a3, it introduces an optimization allowing the orchestrator thread to skip the handshake and build the task graph concurrently. The handshake protocol itself is simplified by combining reports and using register polling to verify that register windows are open. There are no review comments, so no further feedback is provided.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@SergioMartin86
SergioMartin86 force-pushed the perf-orchestrator-decouple-draft branch 2 times, most recently from efa0e94 to 3a4c7ac Compare July 20, 2026 08:09
@SergioMartin86
SergioMartin86 marked this pull request as ready for review July 20, 2026 08:16

@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

🤖 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 `@docs/investigations/2026-07-orchestrator-decouple-preamble.md`:
- Around line 103-106: Run markdownlint-cli2 with the --fix option on the
documented investigation file to align the delimiter and header pipes in both
flagged tables, including the preamble-µs and workload-size tables. Preserve all
table content and Unicode characters; only apply the formatter’s spacing
corrections.
- Around line 21-24: Update the fenced code block in the orchestration timeline
near device_wall to specify the text language tag, preserving its contents and
formatting.
🪄 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

Run ID: a86e442e-877d-4255-8107-bc5d4e6c1605

📥 Commits

Reviewing files that changed from the base of the PR and between 321b88e and 3a4c7ac.

📒 Files selected for processing (5)
  • docs/investigations/2026-07-orchestrator-decouple-preamble.md
  • docs/investigations/README.md
  • src/a2a3/runtime/tensormap_and_ringbuffer/aicpu/aicpu_executor.cpp
  • src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp
  • src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h

Comment thread docs/investigations/2026-07-orchestrator-decouple-preamble.md Outdated
Comment thread docs/investigations/2026-07-orchestrator-decouple-preamble.md Outdated
@SergioMartin86 SergioMartin86 changed the title [Draft] [Performance] Decouple Orchestrator from handshaking [Performance] Decouple Orchestrator from handshaking Jul 20, 2026
@ChaoWao
ChaoWao force-pushed the perf-orchestrator-decouple-draft branch from 3a4c7ac to 4122f0f Compare July 20, 2026 09:17
@ChaoWao ChaoWao changed the title [Performance] Decouple Orchestrator from handshaking [Performance] Barrier-free per-thread scheduler init (a2a3 + a5) Jul 20, 2026
@ChaoWao
ChaoWao force-pushed the perf-orchestrator-decouple-draft branch from 4122f0f to 41494d1 Compare July 20, 2026 10:39
Remove the all-thread AICore-handshake barrier from the per-run scheduler
init on the tensormap_and_ringbuffer runtime, in two parts:

1. Orchestrator decouple: the orchestrator thread does not dispatch to
   cores, so it skips the handshake and returns to run() to build the graph
   immediately, deriving its AIC/AIV counts from the fixed 1:2 cluster ratio
   (known pre-handshake) instead of post_handshake_init.

2. Barrier-free per-thread scheduler init: each scheduler thread handshakes
   only the clusters it will dispatch to (blocked-layout ownership:
   cluster ci = {ci, N/3+2ci, N/3+2ci+1}, owned by ci % active_sched_threads_)
   and self-assigns them via assign_own_clusters, then returns straight to
   run() -- no all-thread barrier and no leader-serialized
   post_handshake_init. A thread starts dispatching to its own cores as soon
   as they come up, independent of peers still handshaking.
   handshake_owned_clusters uses handshake_partition's batched 4-phase
   protocol (2 barriers, not per-core). DFX builds keep a profiling-only
   barrier (pmu needs all physical_core_ids_); serial and single-thread fall
   back to the original path.

A cache-warming variant of the ownership handshake measured null; only the
barrier removal it structurally enables gives the saving.

The blocked [AIC..][AIV..] layout and fixed 1:2 AIC:AIV ratio are
compile-time-and-hardware-enforced invariants (identical on sim and onboard),
so pre-handshake ownership is exact. pre_handshake_init now guards
cores_total_num_ % 3 == 0 up front: a non-multiple-of-3 count would leave the
tail AIV cores in no cluster (unhandshaked, silent hang at the op-execute
timeout), so it fails fast with a clear error instead. This holds for the
serial path too (assign_cores_to_threads pairs aiv_worker_ids_[2*ci]).

Ported to a5 (same restructure). a5 shares the 1:2 blocked layout but has
RUNTIME_MAX_WORKER=108 (36 AIC + 72 AIV) vs a2a3's 72; every count is derived
from cores_total_num_, so the port is count-agnostic. assign_own_clusters
mirrors each arch's own post_handshake_init: the a2a3 variant also does the
AsyncCtx constant-prefill / slab-clear, while a5 does sub_block_id only (a5
writes AsyncCtx per-dispatch in build_payload).

Measured on the current base (48d92e8), a2a3 onboard, 100 rounds/case: the
isolated init/preamble saving is a fixed ~11 us/run, consistent across 8
workloads regardless of size (the device_wall-minus-effective gap drops
10-12 us in every case; the larger device_wall swings are orch/sched compute
run-to-run variance this PR does not touch). The earlier ~63 us figure was
against a pre-hw-native-sys#1310 baseline whose preamble was still ~200 us; today's main
has already cut that, so the removable barrier + leader post_handshake_init is
worth ~11 us. Size-independent, so it still helps high-frequency per-op
dispatch. Golden passes (bgemm, alternating_matmul_add, batch_paged_attention,
qwen3_14b_decode).

Scope: a2a3 + a5 both validated -- a2a3sim (33) + a5sim (26) tmr suites green,
and a2a3 + a5 onboard st CI green (the a5 onboard run confirms the blocked
[0,N/3) AIC / [N/3,N) AIV handshake-enumeration assumption holds on a5 silicon).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ChaoWao ChaoWao changed the title [Performance] Barrier-free per-thread scheduler init (a2a3 + a5) perf(tmr): barrier-free per-thread scheduler init (a2a3 + a5) Jul 20, 2026
@ChaoWao
ChaoWao merged commit ee40a7f into hw-native-sys:main Jul 20, 2026
3 checks passed
@ChaoWao
ChaoWao deleted the perf-orchestrator-decouple-draft branch July 20, 2026 10:43
ChaoWao added a commit to huawei-csl/simpler that referenced this pull request Jul 21, 2026
…ian device speedup)

Replace the tensormap_and_ringbuffer scheduler's fanin wiring (per-edge
dep_pool + fanin_refcount/fanout_refcount atomics) with a polling completion
design: a per-slot completion_flags byte array, a monotonic per-ring
completed_watermark, and an intrusive wake_list for last-fanin notification.
The orchestrator no longer wires every producer→consumer edge into a shared
dep pool; consumers scan their inline fanin and either observe completion via
the watermark or self-register on the one unmet producer's wake list.

Fanin edges are stored inline in the payload (fanin_local_ids/fanin_ring_ids,
PTO2_MAX_FANIN=128); heap reclamation is gated on last_consumer_local_id vs the
per-ring watermark. Early-dispatch, predicated dispatch, and sync_start are
preserved. Adopts hw-native-sys#1345's barrier-free, batched per-thread scheduler init.

Cold-path init, dispatch, cold-path, and completion logic are implemented in
.cpp translation units (scheduler_{dispatch,cold_path,completion}.cpp,
pto_orchestrator.cpp, shared/pto_*.cpp); only the per-task/per-completion
hot paths stay inline in headers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ChaoWao added a commit to huawei-csl/simpler that referenced this pull request Jul 21, 2026
…ian device speedup)

Replace the tensormap_and_ringbuffer scheduler's fanin wiring (per-edge
dep_pool + fanin_refcount/fanout_refcount atomics) with a polling completion
design: a per-slot completion_flags byte array, a monotonic per-ring
completed_watermark, and an intrusive wake_list for last-fanin notification.
The orchestrator no longer wires every producer→consumer edge into a shared
dep pool; consumers scan their inline fanin and either observe completion via
the watermark or self-register on the one unmet producer's wake list.

Fanin edges are stored inline in the payload (fanin_local_ids/fanin_ring_ids,
PTO2_MAX_FANIN=128); heap reclamation is gated on last_consumer_local_id vs the
per-ring watermark. Early-dispatch, predicated dispatch, and sync_start are
preserved. Adopts hw-native-sys#1345's barrier-free, batched per-thread scheduler init.

Cold-path init, dispatch, cold-path, and completion logic are implemented in
.cpp translation units (scheduler_{dispatch,cold_path,completion}.cpp,
pto_orchestrator.cpp, shared/pto_*.cpp); only the per-task/per-completion
hot paths stay inline in headers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants