Skip to content

Commit b01c722

Browse files
kmbandyclaude
andcommitted
fix(dsws): route first-time wave entry through _alloc/_init (Pool-T7 brick)
The CONV=1 seed block sent first-time wave entry through the .Ldispatch re-dispatch trampoline, which lands on _follow and deliberately skips _alloc/_init. That is correct for RE-dispatch (a wave that already ran them once after a bail) but fatal for first entry: the three per-wave initializers live ONLY inside _alloc/_init -- (a) s_alloc_vgpr 32 -- the DYNVGPR per-wave lean allocator handshake, (b) INITFLAG==0xACED -- the LDS rendezvous with the claimer, (c) s35 = 0 -- the local last-seen-epoch baseline. First-entry-via-_follow skipped all three, so followers read EPOCH/STI from uninitialized LDS with a garbage s35 before the claimer published INITFLAG, desynced from the epoch clock, never reached their _quiesce bail, and QUIESCE_CNT never reached WAVES-1 -> the claimer spun forever in .Lclaimer_wait_done -> GPU hang -> MODE1 reset -> desktop brick. This is why Phase-A CONV=0 ran clean (its .else entry goes .Lbfeed/.Lafeed/.Lcompute -> full _alloc/_init/_follow) but the first CONV=1 dispatch hung with conversions provably dormant: seed->dispatch entry routing was the only new always-active path. Entry and re-dispatch are NOT interchangeable. Fix (CONV=1-only): the 3 seed arms now s_branch .Lcompute/.Lafeed/.Lbfeed (full role entry, s59 pre-seeded) instead of .Ldispatch. Bails still go _quiesce -> .Ldispatch -> _follow (re-dispatch unchanged). Corrected the .Ldispatch header comment to RE-DISPATCH ONLY (and fixed its inaccurate "already-consumed INITFLAG" note -- INITFLAG is written once and never cleared; the real reason re-dispatch skips _init is it would reset s35). CPU model (dsws_ctrl_model.cpp): added the first-entry contract (WaveEntry/simulate_first_entry/entry_safe/claimer_quiesce_converges) + a Pool-T7 repro test (TDD RED->GREEN): the buggy routing deterministically fails to converge (reproduces the hang), the fix converges. Offline validation: CONV=0 .text sha256 byte-identical to HEAD on all 3 mixes (4c2a2b/6c1a1b/2c3a3b); CONV=1 .text differs as intended; RGA 0-spill, max-live VGPR 83 <= NFV=112; CPU model ALL PASS. Independently Codex-reviewed offline (no blocker/major). GPU re-baseline pending kmbandy greenlight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0132aDSBLwusCJ4KzHQTnvdu
1 parent 3dbb8c9 commit b01c722

3 files changed

Lines changed: 81 additions & 8 deletions

File tree

ggml/src/ggml-cuda/aiter-integration/rdna4_fp8_gemm/spike/dvgpr_occ/dsws_ctrl_model.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,43 @@ inline bool pool_fits_lean(uint32_t n_pool, uint32_t vlean, uint32_t budget) {
9393
inline bool quiesce_ready_pool(uint32_t quiesce_cnt, uint32_t n_pool) {
9494
return quiesce_cnt >= n_pool - 1;
9595
}
96+
97+
// ---- Pool-T7 root cause: first-entry contract (dispatch vs re-dispatch) ----
98+
//
99+
// A wave's FIRST-time entry and its RE-dispatch (after a per-super-tile bail) are NOT
100+
// interchangeable. First entry must run, in order, the per-role _alloc and _init blocks:
101+
// (a) ran_alloc -- s_alloc_vgpr 32, the DYNVGPR per-wave lean allocator handshake,
102+
// (b) waited_initflag -- spin until the claimer publishes INITFLAG == 0xACED (LDS ready),
103+
// (c) seeded_epoch -- s35 = 0, the local last-seen-epoch baseline,
104+
// THEN falls into _follow. Re-dispatch legitimately skips (a)/(b)/(c): the wave already ran
105+
// them once and conv_apply already sized its footprint -- so the scalar-only .Ldispatch
106+
// trampoline lands straight on _follow. The bug: the seed arms pointed FIRST entry at
107+
// .Ldispatch too, so first-time followers skip _alloc/_init and desync from the epoch clock.
108+
struct WaveEntry { bool ran_alloc; bool waited_initflag; bool seeded_epoch; };
109+
110+
// Where a wave's first-time entry lands. LAND_FOLLOW == seed arms branch to .Ldispatch (the
111+
// buggy routing); LAND_ROLE_ENTRY == seed arms branch to .Lbfeed/.Lafeed/.Lcompute (the fix).
112+
enum EntryLanding { LAND_FOLLOW, LAND_ROLE_ENTRY };
113+
114+
inline WaveEntry simulate_first_entry(EntryLanding land) {
115+
// Only the role entry labels chain _alloc -> _init -> _follow; landing on _follow skips both.
116+
bool full = (land == LAND_ROLE_ENTRY);
117+
return WaveEntry{full, full, full};
118+
}
119+
120+
// A wave is correctly initialized for the follow/quiesce protocol iff it ran all three.
121+
inline bool entry_safe(const WaveEntry& w) {
122+
return w.ran_alloc && w.waited_initflag && w.seeded_epoch;
123+
}
124+
125+
// End-to-end handshake: does the claimer's per-super-tile QUIESCE_CNT >= WAVES-1 gate ever
126+
// close? Every non-claimer follower (wid0 is the claimer and never bails) must reach its
127+
// _quiesce bail once per super-tile; only a follower that entered safely is synced to the
128+
// epoch clock and reliably does. An unsafe entry is a deterministic code-path defect (garbage
129+
// s35 + skipped INITFLAG in identically-compiled waves), so ALL seed-entered followers desync
130+
// -> zero reliable bumps -> the claimer spins forever in .Lclaimer_wait_done.
131+
inline bool claimer_quiesce_converges(EntryLanding land, uint32_t waves) {
132+
uint32_t n_followers = waves - 1;
133+
uint32_t n_safe = entry_safe(simulate_first_entry(land)) ? n_followers : 0;
134+
return quiesce_ready_pool(n_safe, waves);
135+
}

ggml/src/ggml-cuda/aiter-integration/rdna4_fp8_gemm/spike/dvgpr_occ/occ_kernel_dsws.s

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -657,18 +657,24 @@ occ_kernel:
657657
.if CONV_COOLDOWN > 0
658658
s_mov_b32 s66, 0 // Task 4: init cooldown ctr (un-cooled at entry)
659659
.endif
660+
// FIRST-time entry seeds the role reg s59 then falls into the role's FULL entry
661+
// (.Lcompute/.Lafeed/.Lbfeed -> _alloc -> _init -> _follow). It must NOT jump to
662+
// .Ldispatch: that trampoline lands on _follow and is correct ONLY for RE-dispatch
663+
// (a wave that already ran _alloc/_init once). First entry via _follow would skip the
664+
// s_alloc_vgpr 32 allocator handshake, the INITFLAG==0xACED rendezvous, and the s35=0
665+
// epoch seed -> followers desync and the claimer hangs in .Lclaimer_wait_done (Pool-T7 brick).
660666
s_cmp_lt_u32 s24, NBFEED
661667
s_cbranch_scc1 .Lseed_bfeed
662668
s_cmp_lt_u32 s24, (NBFEED+NAFEED)
663669
s_cbranch_scc1 .Lseed_afeed
664670
s_mov_b32 s59, NCOMP_SLOT
665-
s_branch .Ldispatch
671+
s_branch .Lcompute
666672
.Lseed_afeed:
667673
s_mov_b32 s59, NAFEED_SLOT
668-
s_branch .Ldispatch
674+
s_branch .Lafeed
669675
.Lseed_bfeed:
670676
s_mov_b32 s59, NBFEED_SLOT
671-
s_branch .Ldispatch
677+
s_branch .Lbfeed
672678
.else
673679
s_cmp_lt_u32 s24, NBFEED
674680
s_cbranch_scc1 .Lbfeed
@@ -1241,11 +1247,15 @@ occ_kernel:
12411247

12421248
.if DSWS2_CONV
12431249
// ============================================================================================
1244-
// Universal role dispatcher (Task 3): scalar-only trampoline. Reads the role register s59
1245-
// (seeded at entry / flipped by conv_apply on a role conversion) and branches to the matching
1246-
// role's per-epoch _follow loop. Lands on _follow (NOT _alloc/_init): the wave's VGPR footprint
1247-
// is already correct (seed or conv_apply set it) and INIT already ran once -- re-entering
1248-
// _alloc would wrongly resize, and _init would deadlock on the already-consumed INITFLAG.
1250+
// Role RE-DISPATCH trampoline (Task 3): scalar-only. Reads the role register s59 (flipped by
1251+
// conv_apply on a role conversion, else unchanged) and branches to the matching role's
1252+
// per-epoch _follow loop. RE-DISPATCH ONLY -- reached from a per-super-tile _quiesce bail,
1253+
// never from first-time entry (the seed arms fall into the full role entry so _alloc/_init
1254+
// run once; see the seed block). Lands on _follow (NOT _alloc/_init): the wave's VGPR
1255+
// footprint is already correct (conv_apply set it) and INIT already ran -- re-entering _alloc
1256+
// would wrongly resize, and re-running _init would reset s35=0, breaking the "wait for the
1257+
// NEXT epoch" contract (the wave would re-process the current super-tile). INITFLAG is written
1258+
// once (0xACED) and never cleared, so it is the s35 reset -- not INITFLAG -- that _follow preserves.
12491259
// s35 (last-seen-epoch) is untouched here, which is what makes a re-dispatched wave wait for
12501260
// the NEXT epoch at the top of its new role's _follow loop. Scalar-only (s59 read + s_branch) ->
12511261
// adds ZERO OOR/VGPR exposure.

ggml/src/ggml-cuda/aiter-integration/rdna4_fp8_gemm/spike/dvgpr_occ/test_dsws_ctrl_model.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,29 @@ int main() {
9595
assert( quiesce_ready_pool(11, 12) && !quiesce_ready_pool(10, 12));
9696
printf("dsws_ctrl_model: dispatch/cooldown/pool OK\n");
9797

98+
// ---- Pool-T7 brick repro: first-time entry MUST run _alloc/_init before _follow ----
99+
// A wave's first entry has to (a) commit its lean VGPR alloc (s_alloc_vgpr 32), (b) wait the
100+
// claimer's INITFLAG==0xACED LDS rendezvous, and (c) seed its local epoch (s35=0) -- all of which
101+
// live only inside the per-role _alloc/_init blocks. Landing first entry on _follow (what the
102+
// .Ldispatch re-dispatch trampoline does) skips all three -> followers desync from the epoch clock,
103+
// never reach their _quiesce bail, and the claimer spins forever in .Lclaimer_wait_done (the brick).
104+
{
105+
WaveEntry via_dispatch = simulate_first_entry(LAND_FOLLOW); // seed arms -> .Ldispatch (buggy)
106+
assert(!via_dispatch.ran_alloc && !via_dispatch.waited_initflag && !via_dispatch.seeded_epoch);
107+
assert(!entry_safe(via_dispatch)); // first entry via _follow is UNSAFE
108+
109+
WaveEntry via_role = simulate_first_entry(LAND_ROLE_ENTRY); // seed arms -> .Lbfeed/.Lafeed/.Lcompute
110+
assert(via_role.ran_alloc && via_role.waited_initflag && via_role.seeded_epoch);
111+
assert(entry_safe(via_role)); // first entry via role label is SAFE
112+
113+
// End-to-end handshake: mix 4c2a2b -> WAVES=8, so 7 non-claimer followers must each bump QUIESCE_CNT
114+
// once per super-tile. Unsafe entry desyncs EVERY seed-entered follower (deterministic code-path
115+
// defect, not a timing race -> all 7 lost), so the claimer's QUIESCE_CNT>=WAVES-1 gate never closes.
116+
assert(!claimer_quiesce_converges(LAND_FOLLOW, 8)); // BUG: reproduces the Pool-T7 hang
117+
assert( claimer_quiesce_converges(LAND_ROLE_ENTRY, 8)); // FIX: claimer advances
118+
}
119+
printf("dsws_ctrl_model: entry-contract (Pool-T7 repro) OK\n");
120+
98121
printf("dsws_ctrl_model: ALL PASS\n");
99122
return 0;
100123
}

0 commit comments

Comments
 (0)