Skip to content

fix(scheduler): make sync_start drain retries generation-safe - #1461

Merged
ChaoZheng109 merged 1 commit into
hw-native-sys:mainfrom
Leaf-Salix:fix/sync-start-drain-generation
Jul 28, 2026
Merged

fix(scheduler): make sync_start drain retries generation-safe#1461
ChaoZheng109 merged 1 commit into
hw-native-sys:mainfrom
Leaf-Salix:fix/sync-start-drain-generation

Conversation

@Leaf-Salix

@Leaf-Salix Leaf-Salix commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #1455.

The sync_start drain protocol reused acknowledgement, election, and staging state across insufficient-resource retries without identifying which retry attempt produced each value. A delayed scheduler could therefore cross the old acknowledgement barrier, resume after another scheduler reset the state, and combine locally valid values from two attempts. The resulting split barrier could not make progress and eventually surfaced as AICPU timeout 507018.

Fix

  • Add drain_attempt and advance it whenever a new drain acknowledgement round starts.
  • Replace the reusable shared acknowledgement mask with generation-tagged per-scheduler tokens.
  • Reduce acknowledgements through a binary tree: each scheduler waits for at most two child tokens, publishes one subtree token, and waits for the root token before staging. This keeps fan-in O(1) per scheduler and the critical path O(log N), without a shared read-modify-write hotspot or a per-spin O(N) scan.
  • Use scheduler thread 0 as the fixed coordinator. Scheduler indices are contiguous, so thread 0 is present whenever a scheduler exists. This removes the election CAS/state and prevents a stale participant from becoming the owner of a later attempt.
  • Make acknowledgement, staging, and retry waits exit when their captured attempt no longer matches the live generation.

The protocol is applied consistently to:

  • A2/A3 tensormap_and_ringbuffer;
  • A2/A3 host_build_graph;
  • A5 tensormap_and_ringbuffer.

Regression coverage

The final tree contains no SIMPLER_DRAIN_ABA_TEST runtime hook, test-only production error code, or test-only scan in the scheduler hot path.

  • SyncStartDrainAttemptTest.LateAckCannotSatisfyNextAttemptBarrier covers generation isolation directly against the production token helpers for A2/A3 and A5.
  • Normal and stress spmd_sync_start scene tests exercise the real concurrent drain path.
  • The deterministic one-time pre-fix/fixed comparison, rejected protocol variants, and benchmark evidence are preserved in docs/investigations/2026-07-sync-start-drain-retry-aba.md.

Validation

Final pushed head: 9429177, based on upstream 1c475a70.

  • GitHub CI passed: run 30268542241.
  • A fresh editable rebuild of all affected runtime variants passed.
  • test_scheduler_state, test_a5_scheduler_state, and test_error_code_names passed.
  • A5sim normal and stress sync_start passed.
  • A3 hardware through task-submit passed:
    • TMR normal: task_20260727_053540_328311513473;
    • TMR stress: task_20260727_053615_329667622840;
    • host-build-graph sync-start/core-count coverage: task_20260727_053659_33145449300.
  • Modified-file pre-commit passed, including clang-format, clang-tidy, cpplint, and markdownlint.

Performance

A locked-device 100-round ABBA comparison of the retained fixed-coordinator tree against upstream a53cc168 measured normal sync_start effectively flat (Device -0.91%, Effective -0.83%) and stress about 3% faster (Device -2.93%, Effective -3.02%).

A later five-cycle, 1,000-sample stress comparison against 1c475a70 measured the PR about 4% slower in the pooled mean, but paired cycle deltas ranged from -3.25% to +18.90% and the cycle-level confidence interval crossed zero. The case was strongly multimodal, so neither the earlier improvement nor the later regression was established as a stable effect. Plain non-drain cases showed no consistent regression.

Alternatives were also measured and rejected: a packed shared atomic regressed normal/stress Device time by about 16%/19%, removing the root broadcast regressed it by about 10%/8%, and rotating the coordinator produced no stable benefit. The generation-tagged tree with fixed coordinator therefore remains the best measured design.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The sync-start drain protocol now uses attempt-scoped acknowledgements and elections to reject stale retry state. Runtime wiring exposes a deterministic ABA test mode, with lifecycle resets, unit coverage, and an SPMD reproducer.

Changes

Sync-start drain protocol

Layer / File(s) Summary
Attempt-scoped drain state
src/*/scheduler/scheduler_types.h
drain_attempt and per-thread acknowledgement tracking replace the shared acknowledgement mask; helper functions validate barriers and elections by attempt.
Attempt-aware scheduler flow
src/*/scheduler/scheduler_completion.cpp
Drain retries, waits, stale-election handling, resource fallback, and finalization now terminate or advance when the attempt changes.
Lifecycle and ABA test wiring
src/*/scheduler/scheduler_cold_path.cpp, src/a2a3/runtime/tensormap_and_ringbuffer/{host,runtime}/*, docs/dynamic-linking.md
Scheduler state is reset for attempts and acknowledgements, while SIMPLER_DRAIN_ABA_TEST is propagated into the device launch descriptor.
Unit and SPMD validation
tests/ut/cpp/*/test_scheduler_state.cpp, tests/st/a2a3/.../spmd_sync_start_drain_aba_hook/*
Tests cover late acknowledgements and stale elections; the new orchestration and pressure kernels reproduce the interleaving.

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

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant SchedulerThreads
  participant DrainState
  participant Runtime
  SchedulerThreads->>DrainState: enter drain and advance drain_attempt
  SchedulerThreads->>DrainState: publish per-thread acknowledgement
  SchedulerThreads->>DrainState: elect matching-attempt worker
  DrainState-->>SchedulerThreads: accept attempt or invalidate cohort
  SchedulerThreads->>Runtime: finalize or retry sync-start drain
Loading

Poem

A bunny hops through attempts anew,
Old ACKs fade; fresh rounds come through.
Elections guard the drain-door tight,
ABA shadows vanish from sight.
Kernels spin and tests declare:
The scheduler’s safe in its carrot lair!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Linked Issues check ✅ Passed The changes implement the #1455 requirements: attempt tagging, stale-thread rejection, and deterministic ABA validation.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes appear; the added tests, docs, and helper/runtime support all serve the ABA fix.
Title check ✅ Passed It clearly summarizes the main change: making sync_start drain retries generation-safe.
Description check ✅ Passed It directly describes the drain ABA fix, the new attempt tagging, and the added validation.

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 10436722dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@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: 4

🤖 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 472-487: In SchedulerContext::enter_drain_mode, advance and
publish drain_attempt before clearing drain_worker_elected, matching the
ordering used by handle_drain_mode and the shared parent implementation. Keep
pending_task publication and the remaining coordination resets intact, but
ensure stale-round election checks cannot observe the new task before the
attempt changes.

In
`@src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_completion.cpp`:
- Around line 503-521: In SchedulerContext::enter_drain_mode, advance and
publish drain_attempt before clearing drain_worker_elected. Preserve the
existing reset sequence and pending_task publication, but ensure the new
generation is visible before election state is reset, matching the ordering
already used by handle_drain_mode.

In
`@src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_completion.cpp`:
- Around line 416-430: Reorder the initialization in
SchedulerContext::enter_drain_mode so drain_attempt is incremented and published
before clearing drain_worker_elected, matching the ordering used by
handle_drain_mode’s insufficient-resource path and the other scheduler variants.
Preserve the existing drain-state initialization and ownership logic.

In
`@tests/st/a2a3/tensormap_and_ringbuffer/spmd_sync_start_drain_aba_hook/test_spmd_sync_start_drain_aba_hook.py`:
- Line 19: Update the test setup around SIMPLER_DRAIN_ABA_TEST to assign "1"
unconditionally, ensuring the ABA hook is exercised regardless of the inherited
environment. Scope the override to this test and restore the prior environment
value during teardown, including removing the variable when it was previously
unset.
🪄 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: 88fb1af7-bff2-4d96-a640-1199859fa07a

📥 Commits

Reviewing files that changed from the base of the PR and between d0bc661 and 1043672.

📒 Files selected for processing (23)
  • docs/dynamic-linking.md
  • 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_types.h
  • src/a2a3/runtime/tensormap_and_ringbuffer/common/pto_runtime_status.h
  • src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
  • src/a2a3/runtime/tensormap_and_ringbuffer/runtime/runtime.h
  • src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp
  • src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_completion.cpp
  • src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h
  • src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_types.h
  • src/a2a3/runtime/tensormap_and_ringbuffer/runtime/shared/runtime.cpp
  • src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp
  • src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_completion.cpp
  • src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h
  • src/a5/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_types.h
  • tests/st/a2a3/tensormap_and_ringbuffer/spmd_sync_start_drain_aba_hook/kernels/aic/kernel_spmd_mix_pressure.cpp
  • tests/st/a2a3/tensormap_and_ringbuffer/spmd_sync_start_drain_aba_hook/kernels/aiv/kernel_spmd_mix_pressure.cpp
  • tests/st/a2a3/tensormap_and_ringbuffer/spmd_sync_start_drain_aba_hook/kernels/orchestration/spmd_sync_start_drain_aba_hook_orch.cpp
  • tests/st/a2a3/tensormap_and_ringbuffer/spmd_sync_start_drain_aba_hook/test_spmd_sync_start_drain_aba_hook.py
  • tests/ut/cpp/a2a3/test_scheduler_state.cpp
  • tests/ut/cpp/a5/test_scheduler_state.cpp

Comment thread src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_completion.cpp Outdated
@Leaf-Salix
Leaf-Salix force-pushed the fix/sync-start-drain-generation branch 2 times, most recently from 51dd422 to e7d4362 Compare July 27, 2026 02:11

@ChaoZheng109 ChaoZheng109 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the generation-tagging approach for the #1455 ABA — the fix looks correct and the tri-part rollout (tmr / host_build_graph / a5) is consistent. A few redundancy / simplification notes below; none affect correctness. Separately, I'm running a hardware benchmark covering both plain cases and sync_start cases to confirm no serious regression, and will follow up with the numbers.

Comment thread src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_types.h Outdated
// insufficient resources).
while (drain_state_.drain_stage_go.load(std::memory_order_acquire) == 0) {
if (is_completed()) return;
if (drain_state_.sync_start_pending.load(std::memory_order_acquire) == 0) return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Likely-dead defensive guard. A non-elected thread only spins here while drain_stage_go == 0, i.e. before the owner has released staging. The owner sets sync_start_pending = 0 only in finalize, which is gated on drain_stage_done_mask being complete — but a thread parked in this loop hasn't set its own stage_done bit yet, so the owner cannot have finalized while this thread is here. The only other writer that zeroes sync_start_pending is teardown, already covered by is_completed(). So this condition looks unreachable inside this loop. Suggest removing it or documenting the intended case. Same in a5 :582 and host_build_graph :737.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agreed. I removed the sync_start_pending == 0 escape condition from all three scheduler variants.

A valid owner cannot finalize the task before every participating thread has completed the staging barrier. Therefore a follower waiting for stage_go cannot observe the pending task being cleared for the same attempt.

Task teardown is handled separately by the existing completion path, so this guard was both unreachable for a valid attempt and unnecessary for teardown.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow-up for the current version,
34a250e:

The unreachable sync_start_pending == 0 escape remains removed in all three
scheduler variants. The follower's stage_go wait now exits only for runtime
completion or an attempt-generation change.

I also removed the follower's later pending_task reload/null guard. The fixed
coordinator cannot clear the pending task until every participant has staged
and published its stage_done bit, so neither guard is reachable for a valid
attempt; teardown remains covered by is_completed().

};
static_assert(sizeof(SyncStartDrainState) == 64);

inline uint32_t sync_start_drain_ack_mask_for_attempt(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Perf note (not redundant). The old drain_ack_mask gave the ack barrier an O(1) atomic load per spin iteration; sync_start_drain_ack_mask_for_attempt now performs thread_count acquire-loads every iteration. N is small (<=4) so the absolute cost is tiny, but this is on the drain spin path, so it's worth confirming the sync_start benchmark cases don't regress. Optional micro-opt: only rescan the per-thread array when the cheap single drain_attempt load still matches the current round.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for pointing this out. I replaced the production O(N) acknowledgement scan rather than adding another full rescan after the attempt load.

The production synchronization now uses a generation-tagged binary-tree reduction:

  • each worker waits for at most two child tokens;
  • each worker publishes one subtree-ready token;
  • the root publishes the generation-ready token;
  • non-root workers wait for that root token.

This gives O(1) fan-in per worker and an O(log N) critical path, without shared read-modify-write contention.

The only remaining O(N) scan is an explicitly test-only helper used by the ABA test hook to inspect independently published arrival tokens. It is not reachable from the production scheduler path.

I am posting the updated current-base/current-head benchmark results in the PR conversation. Those results are mixed, so I am keeping the performance concern open.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow-up for the current version,
34a250e:

The production O(N) acknowledgement scan remains removed. The final protocol
uses a generation-tagged binary-tree reduction:

  • each scheduler waits for at most two child tokens;
  • each scheduler publishes one subtree token;
  • followers wait for the root token before entering the staging decision;
  • thread 0 is the fixed coordinator, so there is no election CAS.

This gives O(1) fan-in per scheduler and an O(log N) critical path without a
shared RMW hotspot. The only remaining O(N) scan is explicitly test-only and
is used by the deterministic ABA hook.

I also tested alternatives before retaining the tree. A packed shared O(1)
atomic regressed normal/stress Device time by about 16%/19%, removing the root
broadcast regressed it by about 10%/8%, and a rotating coordinator showed no
stable benefit.

The latest stress-only follow-up used five independent
A100-B100-B100-A100 cycles, for 1,000 rounds per version. Its pooled PR delta
was about +4%, while the paired cycle deltas ranged from -3.25% to +18.90% and
the cycle-level interval crossed zero because the case is strongly multimodal.
The full result and corrected interpretation are recorded
here.

@ChaoZheng109

Copy link
Copy Markdown
Collaborator

Re: my note above about benchmarking — this is better run on the author's side so the numbers come from a clean, controlled setup. @Leaf-Salix, could you run a hardware benchmark (a2a3, tensormap_and_ringbuffer) comparing this branch against its merge-base and confirm there's no serious regression?

This PR changes the sync_start drain hot path — the ack barrier goes from a single O(1) drain_ack_mask load per spin to an O(N) sync_start_drain_ack_mask_for_attempt scan, plus an extra drain_attempt acquire-load at election and inside each spin-wait — so a perf check is warranted.

Please make sure it covers both:

  • plain / non-drain cases — to confirm relocating drain_ack_attempts_[] into SchedulerContext and the SyncStartDrainState layout change don't perturb the normal dispatch path. e.g. alternating_matmul_add, paged_attention_unroll, batch_paged_attention.
  • sync_start / drain cases — the code that actually changed. e.g. spmd_sync_start, spmd_sync_start_stress.

tools/benchmark_rounds.sh is a convenient vehicle: set TMR_EXAMPLE_CASES to those five and run -r tensormap_and_ringbuffer -n 50; it reports Avg Effective / Orch / Sched per case. The Orch / Sched deltas on the sync_start cases are the key numbers; plain cases should be within noise.

One build gotcha worth flagging: after switching to this branch, rebuilding with only pip install -e . in a reused venv can leave a stale _task_interface nanobind .so, which surfaces as validate_launch_aicpu_num: launch_aicpu_num (0) must be in range [1,4] with every case failing. A fresh venv per tree (or forcing a nanobind rebuild) avoids it.

@Leaf-Salix
Leaf-Salix force-pushed the fix/sync-start-drain-generation branch from 03852b0 to 1ee0152 Compare July 27, 2026 07:26

Copy link
Copy Markdown
Contributor

Follow-up to @ChaoZheng109's benchmark request.

After rebasing, I discarded the earlier comparison against the old merge-base and reran the benchmark against the exact current commits:

  • upstream/base: ae29a2c0fa8c5d5ff7fc1688a8156fc8c708bac2
  • PR head: 1ee0152b366ea0015130bf4bea6cdc30c97687c8

Setup:

  • separate checkout-local venvs for base and head;
  • each checkout force-rebuilt and installed its own _task_interface;
  • same a2a3 device through task-submit;
  • ABBA order: base -> head -> head -> base;
  • 100 rounds per block, giving 200 samples per version and case;
  • all measured blocks completed 100/100 rounds.

Positive deltas below mean the PR is slower:

Case Effective Orch Sched 10% trimmed Effective / Sched
alternating_matmul_add -0.36% -0.35% -0.41% -0.59% / -0.70%
paged_attention_unroll Case1 +0.09% -2.77% +0.05% +0.27% / +0.23%
paged_attention_unroll Case2 -0.02% +3.44% -0.01% -0.14% / -0.16%
batch_paged_attention +3.24% +2.94% +3.24% +4.19% / +4.19%
spmd_sync_start +11.81% +6.25% +11.26% +12.16% / +11.86%
spmd_sync_start_stress -14.89% -17.80% -14.75% -17.92% / -17.76%

The alternating-matmul and paged-attention Effective/Sched results are essentially flat. The stress sync-start case improves substantially.

However, batch_paged_attention regresses by about 3-4%, and the normal spmd_sync_start case regresses by about 11-12%. Therefore I cannot claim that the current head has no serious performance regression yet.

The production O(N) acknowledgement scan has been removed, but the current implementation still needs further optimization for the normal sync-start path. I am keeping this performance concern open while investigating that regression.

@Leaf-Salix
Leaf-Salix force-pushed the fix/sync-start-drain-generation branch 2 times, most recently from 456f793 to 34a250e Compare July 27, 2026 10:52
@Leaf-Salix

Copy link
Copy Markdown
Contributor Author

Follow-up to the performance review after updating the implementation.

The branch is now rebased and squashed to one commit:

  • current upstream/rebase base:
    1c475a7016f0bbc8a3d6cb7515acff6c14769905
  • current PR head: 34a250e4fa5a65a40ad2e839c87ab54f920f1623

The hardware runs below used upstream
a53cc16855e17d98474667f2f05eaabb00f78468 and PR head
456f793e16721ee294e272711f5a187bea7b899f. Upstream advanced during the
run by one commit, 1c475a70, which only changes .github/workflows/ci.yml
to skip the broken A5 runner. Rebasing onto it changed no runtime source file,
so the measured runtime sources and binaries are identical to the current
runtime diff.

What changed

I removed the production O(N) acknowledgement scan and then tested several
alternatives before settling on the current protocol:

  1. Packed shared atomic (O(1) logical mask check): rejected. Packing the
    generation and ack mask into one atomic created a shared LL/SC hotspot.
    Relative to the then-current PR head, normal/stress Device time regressed by
    about 16%/19%.
  2. One-way tree without root broadcast: rejected. Followers moved directly
    to polling stage_go; the resulting cache-line pressure regressed
    normal/stress Device time by about 10%/8%.
  3. Rotating coordinator: rejected. It did not show a stable benefit
    (normal regressed about 3%, while stress improved only about 1% in the ABBA
    mean) and retained extra coordinator-selection complexity.
  4. Fixed coordinator plus generation-tagged tree: retained. Scheduler
    thread 0 is always present because active scheduler indices are contiguous
    [0, sched_thread_num). Fixing the coordinator removes the election CAS and
    election state. Each scheduler waits for at most two children and publishes
    one subtree token, so fan-in is O(1) per scheduler and the critical path is
    O(log N), without a shared RMW hotspot. The root token keeps followers on
    read-only acknowledgement tokens until reduction completes, shortening the
    concentrated polling window on stage_go.

The fixed-coordinator protocol was audited across a2a3 TMR, a2a3
host-build-graph, and a5 TMR. The release/acquire chain provides transitive
happens-before from children to the root and then to followers; attempt-tagged
tokens prevent stale acknowledgements from satisfying a later attempt. I also
removed the now-unreachable pending-task guards/reload, removed the election
field/helpers, and added layout assertions preserving the 64-byte shared state
and the offsets of subsequent fields.

The fixed coordinator does make drain finalization sensitive to temporary
descheduling of thread 0. This is a latency trade-off rather than a correctness
or teardown issue: active AICPU scheduler threads are resident for the runtime
lifetime, and the N=1..4 hardware coverage did not expose a stall or regression.

Validation

  • Full editable build passed for a2a3/a5, sim/onboard, TMR/HBG.
  • A2/A3 and A5 scheduler C++ UT: 21/21 each.
  • A2/A3 hardware covered 1, 2, 3, and 4 active schedulers, including normal,
    stress, MIX spill, the deterministic ABA hook, a non-power-of-two tree, and
    host-build-graph sync_start.
  • A5 sim covered normal, stress, and MIX spill.
  • Modified-file pre-commit checks and Linux clang-tidy passed.

Final performance against current upstream

Both trees were fully rebuilt in separate checkout-local venvs, including their
own _task_interface. Tests used the same A2/A3 device through task-submit,
in A/B/C/D order (upstream, PR, PR, upstream), with 100 rounds per block and
200 samples per version/case. Deltas are
mean(PR B,C) / mean(upstream A,D) - 1; positive means the PR is slower.

Plain/non-drain cases (task_20260727_034355_113633628979):

Case Device Effective Orch Sched Trimmed Device
alternating_matmul_add -1.34% -1.37% -1.37% -1.43% -1.59%
paged_attention_unroll Case1 +1.24% +1.21% -0.25% +1.16% +1.80%
paged_attention_unroll Case2 +0.21% +0.15% -1.63% +0.18% +0.16%
batch_paged_attention -2.07% -2.09% -3.19% -2.09% -2.86%

Sync-start cases (task_20260727_033740_105692917182):

Case Device Effective Orch Sched Trimmed Device
spmd_sync_start -0.91% -0.83% +3.50% -0.52% -0.14%
spmd_sync_start_stress -2.93% -3.02% -0.99% -2.99% -4.56%

The earlier normal sync_start and batch-paged-attention regressions are no
longer present: normal sync_start is effectively flat overall, stress improves
by about 3% (4.6% trimmed Device), and the plain cases show no consistent
regression. Based on the protocol experiments and the final current-upstream
ABBA, I kept the generation-tagged O(log N) tree with a fixed coordinator.

@Leaf-Salix

Copy link
Copy Markdown
Contributor Author

Follow-up to the performance comment above: I expanded
spmd_sync_start_stress/Case1 because its earlier 200-sample ABBA results
varied substantially between runs.

Versions:

  • merge-base/upstream:
    1c475a7016f0bbc8a3d6cb7515acff6c14769905
  • PR head: 34a250e4fa5a65a40ad2e839c87ab54f920f1623

The new run used five independent A100-B100-B100-A100 cycles on the same
A2/A3 device through task-submit, giving 1,000 valid rounds per version.
Each 100-round block starts a new test process. Positive deltas mean the PR is
slower.

Task: task_20260727_040431_141082028719

Pooled 1,000-round result

Metric Merge-base PR Mean delta 10% trimmed delta
Device 459.47 us 477.51 us +3.93% +3.66%
Effective 437.06 us 454.85 us +4.07% +3.64%
Orch 59.42 us 60.95 us +2.58% +3.21%
Sched 431.44 us 449.26 us +4.13% +3.56%

Paired results by cycle

Cycle Device Effective Orch Sched
1 -3.06% -3.25% -1.16% -3.14%
2 +2.41% +2.52% -4.44% +2.46%
3 +0.82% +0.85% -2.33% +0.90%
4 +3.19% +3.30% +4.02% +3.48%
5 +18.07% +18.90% +19.35% +18.90%
Median +2.41% +2.52% -1.16% +2.46%

Treating each cycle, rather than each round, as the correlated sampling unit,
the Effective geometric-mean delta is +4.20%, with a wide 95% paired
log-ratio interval of [-5.41%, +14.80%].

The round-level distribution is strongly multimodal:

Version Effective <375 us 375..500 us >=500 us
Merge-base 416 (41.6%) 241 (24.1%) 343 (34.3%)
PR 309 (30.9%) 460 (46.0%) 231 (23.1%)

This is not a small number of outliers, so 10% trimming does not remove the
process/scheduling-state effect. The fifth cycle also demonstrates how one
state imbalance can dominate the pooled mean.

I therefore no longer interpret the earlier -3% 200-sample result as a
stable improvement; likewise, the still earlier roughly -15% result was not
stable. The current 1,000-round batch has the PR about 4% slower in the pooled
mean and about 2.5% slower in the median paired cycle. However, the cycle-level
interval still crosses zero, so the data does not reliably separate a possible
roughly 2-4% protocol cost from the large cross-process mode drift.

const char *serial_env = std::getenv("SIMPLER_TMR_SERIAL_ORCH_SCHED_ENABLE");
runtime->dev.serial_orch_sched =
serial_env && (serial_env[0] == '1' || serial_env[0] == 't' || serial_env[0] == 'T');
const char *drain_aba_env = std::getenv("SIMPLER_DRAIN_ABA_TEST");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove the SIMPLER_DRAIN_ABA_TEST runtime hook entirely — the fix is already covered without it, and this hook spreads test-only code across production files.

The generation-tagging fix (drain_attempt + tree ack barrier + tri-state election) is self-contained and stands on its own. Its regression protection already exists without any runtime hook:

  • SyncStartDrainAttemptTest (tests/ut/cpp/{a2a3,a5}/test_scheduler_state.cpp) directly exercises the two failure modes — LateAckCannotSatisfyNextAttemptBarrier and StaleParticipantCannotOwnNextAttempt — against the helpers, no hardware/hook needed.
  • The normal + stress scene tests (spmd_sync_start, spmd_sync_start_stress) cover the real concurrent drain path; stress run repeatedly is a probabilistic regression net.

Against that, the hook is a deterministic one-time proof whose permanent cost is out of proportion to its benefit:

  • ~70 lines of test-only code smeared across 9 production files, including cross-cutting ones unrelated to the scheduler fix — common/runtime_status/error_names.h, runtime/runtime.h, common/pto_runtime_status.h, runtime/shared/runtime.cpp, host/runtime_maker.cpp.
  • A new env-var behavior gate (SIMPLER_DRAIN_ABA_TEST) — this is exactly the permanent configuration surface that .claude/rules/env-macro-gating.md says needs explicit maintainer sign-off and should be avoided; a stray =1 in any environment deliberately stalls scheduler thread 1 on every drain.
  • A production error code PTO2_ERROR_DRAIN_ABA_TEST_NOT_EXERCISED = 104 occupying the shared status enum purely to serve the test.
  • Test-injection logic (thread-1 park loop + O(N) test scan + NOT_EXERCISED latch) inside the drain hot path handle_drain_mode.
  • Asymmetric: the hook is a2a3 TMR only, while the fix lands in all three variants — so it doesn't even cover the code it guards uniformly.

Note that #ifdef-gating it would only keep it out of the shipped binary and off the hot path — the source lines and the cross-cutting file churn still remain in the tree and the diff. Since correctness is already validated, the cleanest outcome is to delete the hook outright rather than gate it. The one-time "pre-fix hangs / post-fix passes" evidence is valuable as a record — keep it in the PR description or a docs/investigations/ entry, not permanently in the code.

Concretely, please remove:

  • host/runtime_maker.cpp — the SIMPLER_DRAIN_ABA_TEST getenv block + LOG_INFO_V0 (this anchor).
  • runtime/runtime.hbool drain_aba_test_mode;
  • runtime/shared/runtime.cppdev.drain_aba_test_mode = false;
  • common/pto_runtime_status.hPTO2_ERROR_DRAIN_ABA_TEST_NOT_EXERCISED.
  • common/runtime_status/error_names.h — the three DRAIN_ABA_TEST_NOT_EXERCISED cases.
  • runtime/scheduler/scheduler_context.hdrain_aba_test_mode_, drain_test_victim_armed_.
  • runtime/scheduler/scheduler_cold_path.cpp — the drain_aba_test_mode_ assignment/reset.
  • runtime/scheduler/scheduler_completion.cpp — the drain_aba_test_mode_ branches, the thread-1 park block, and the arrival-token publication used only by it.
  • runtime/scheduler/scheduler_types.h — the test-only sync_start_drain_test_ack_mask_for_attempt scan (keep the production token helpers).
  • tests/st/a2a3/.../spmd_sync_start_drain_aba_hook/ and the PTO2_ERROR_DRAIN_ABA_TEST_NOT_EXERCISED entry in tests/ut/cpp/common/test_error_code_names.cpp.

Keep the unit tests in test_scheduler_state.cpp — those are the right, hook-free regression barrier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, agreed. I removed the runtime hook entirely in 9429177.

The deletion covers all of the test-only production surface listed here:

  • the SIMPLER_DRAIN_ABA_TEST getenv/log and launch-descriptor plumbing;
  • drain_aba_test_mode_, drain_test_victim_armed_, the arrival publication, thread-1 park loop, and NOT_EXERCISED latch in the scheduler path;
  • PTO2_ERROR_DRAIN_ABA_TEST_NOT_EXERCISED and its name/description/hint mappings;
  • the test-only O(N) scan/arrival-token helpers from all three scheduler variants;
  • the dedicated spmd_sync_start_drain_aba_hook scene and the common error-code test entry.

Only the production attempt/subtree-token helpers remain. LateAckCannotSatisfyNextAttemptBarrier is retained for both A2/A3 and A5 and now exercises only those production helpers.

One detail changed after the review snapshot: StaleParticipantCannotOwnNextAttempt exercised the former tri-state election. The current implementation has removed drain_worker_elected, sync_start_drain_try_elect(), SyncStartDrainElectResult, and the election CAS from all three variants; scheduler thread 0 is now the fixed coordinator. That election-specific test therefore no longer has a production mechanism to exercise, so I did not reintroduce a test-only helper or a dummy test for it.

I preserved the one-time deterministic evidence in docs/investigations/2026-07-sync-start-drain-retry-aba.md, including the hook interleaving, pre-fix failure, protocol alternatives, and performance results.

Validation after removing the hook:

  • test_scheduler_state, test_a5_scheduler_state, and test_error_code_names passed;
  • A5sim normal/stress passed;
  • A3 spmd_sync_start, spmd_sync_start_stress, and host-build-graph sync-start coverage passed through task-submit;
  • modified-file pre-commit, including clang-tidy and markdownlint, passed.

- Tag per-thread tree acknowledgements with the drain attempt
- Use scheduler thread 0 as the fixed staging coordinator
- Remove the deterministic runtime hook and retain hook-free regression coverage
- Preserve the 64-byte shared-state layout and record the investigation
@Leaf-Salix
Leaf-Salix force-pushed the fix/sync-start-drain-generation branch from 34a250e to 9429177 Compare July 27, 2026 13:03
@Leaf-Salix Leaf-Salix changed the title fix(scheduler): tag sync_start drain ack/election state by generation fix(scheduler): make sync_start drain retries generation-safe Jul 27, 2026
@ChaoZheng109
ChaoZheng109 merged commit a74485d into hw-native-sys:main Jul 28, 2026
16 checks passed
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.

[Bug] sync_start drain can deadlock when generation-less ack/election state is reused across retry attempts

3 participants