Symptom
Rare (3 occurrences over ~150 GRPO steps across two engine builds): a training step logs vtc=0 with hundreds of micro-batches, producing garbage step metrics (masked 2384-3052%, kl 35-42, loss 42-59) and exploded pre-clip grad norms (0.77-0.96). The zero-lr guard from #72 neutralizes it (policy untouched), but the root cause is open.
Established by investigation (2026-08-04)
ValidTokenCount is per-micro in the GRPO path: forward_for_grpo unconditionally zeroes Losses/ValidTokenCount/CorrectCount every micro (dsl_model_execution.cpp:795). The trainer's step-end get_valid_token_count(0) therefore returns the last micro's count, not a step total. Verified: a healthy step logged vtc=136 and its last micro's loss-mask count is exactly 136 (decoded from the transport batch).
- On corrupted steps the CE kernels did run and write
Losses (logged loss ≈ raw -logprob sum over one micro's ~125-170 valid tokens), while vtc read 0 — same kernels write both (fused_lm_head_loss.cpp:417-441), so a stuck kv_sweep skip (fused_lm_head_loss.cpp:242) is ruled out (it would zero both).
- Data ruled out: both preserved batches decode cleanly; no micro has a zero loss-mask; last micros carry 125-167 valid tokens (
rollouts_step73_FORENSIC, rollouts_step82_FORENSIC in the pool-D output dir, plus the cap3-era step-14 batch).
dispatch_pp_apply_optimizer's host overwrite clamps to ≥1 — ruled out.
Remaining candidates
- The last micro's
chunked_cross_entropy_forward/fused_cross_entropy_forward contributing 0 to the vtc atomic while writing losses (kernel-internal accumulation edge, possibly interacting with lmhead_chunks nano-slicing or the chunked phase-A/B schedule).
- A post-backward re-zeroing of
ValidTokenCount racing the step-end read (any path that calls fill_zero on run-state loss buffers between the last backward_grpo and the read).
Next step
Deterministic replay: feed a forensic batch through a single-GPU debug trainer (same config: fp8-hybrid, sequence_chunks 4, lmhead_chunks 12, single-sample bins) with SUROGATE_CHUNK_TRACE=1 and a vtc D2H readback after each micro — bisects kernel-edge vs post-hoc-zeroing in one run. Batches and configs preserved; replay harness is the open work.
Also worth considering
Step-end consumers treating vtc as a step total (metric normalization; global_norm_sqrt token scale) may deserve an explicit step-accumulated counter regardless of this bug — the per-micro semantics make every step-level metric a last-micro sample.
🤖 Generated with Claude Code
Symptom
Rare (3 occurrences over ~150 GRPO steps across two engine builds): a training step logs
vtc=0with hundreds of micro-batches, producing garbage step metrics (masked 2384-3052%, kl 35-42, loss 42-59) and exploded pre-clip grad norms (0.77-0.96). The zero-lr guard from #72 neutralizes it (policy untouched), but the root cause is open.Established by investigation (2026-08-04)
ValidTokenCountis per-micro in the GRPO path:forward_for_grpounconditionally zeroesLosses/ValidTokenCount/CorrectCountevery micro (dsl_model_execution.cpp:795). The trainer's step-endget_valid_token_count(0)therefore returns the last micro's count, not a step total. Verified: a healthy step logged vtc=136 and its last micro's loss-mask count is exactly 136 (decoded from the transport batch).Losses(logged loss ≈ raw -logprob sum over one micro's ~125-170 valid tokens), while vtc read 0 — same kernels write both (fused_lm_head_loss.cpp:417-441), so a stuckkv_sweepskip (fused_lm_head_loss.cpp:242) is ruled out (it would zero both).rollouts_step73_FORENSIC,rollouts_step82_FORENSICin the pool-D output dir, plus the cap3-era step-14 batch).dispatch_pp_apply_optimizer's host overwrite clamps to ≥1 — ruled out.Remaining candidates
chunked_cross_entropy_forward/fused_cross_entropy_forwardcontributing 0 to the vtc atomic while writing losses (kernel-internal accumulation edge, possibly interacting with lmhead_chunks nano-slicing or the chunked phase-A/B schedule).ValidTokenCountracing the step-end read (any path that callsfill_zeroon run-state loss buffers between the lastbackward_grpoand the read).Next step
Deterministic replay: feed a forensic batch through a single-GPU debug trainer (same config: fp8-hybrid, sequence_chunks 4, lmhead_chunks 12, single-sample bins) with
SUROGATE_CHUNK_TRACE=1and a vtc D2H readback after each micro — bisects kernel-edge vs post-hoc-zeroing in one run. Batches and configs preserved; replay harness is the open work.Also worth considering
Step-end consumers treating vtc as a step total (metric normalization;
global_norm_sqrttoken scale) may deserve an explicit step-accumulated counter regardless of this bug — the per-micro semantics make every step-level metric a last-micro sample.🤖 Generated with Claude Code