LA-WARMUP-V2 · issue #59 · Qwen2.5-1.5B-Instruct + GSM8K · 1×H200 · vanilla GRPO (no-KL, no-entropy), flat LR 1e-6, 100 steps, val every 25 · locked substrate: PowerSGD rank-77, anchor cadence = delay_K = 20, signed_ema (α=0.25, β=0.50), resp=1024 · final deliverable, 2026-07-04
Contents: 1 Result · 2 What each run is · 3 Mechanism · 4 The four exercises E1–E4 · 5 E4 & softening · 6 Q-health · 7 Anchor-owned Q (C8a) · 8 Comm efficiency · 9 Findings · 10 Theory · 11 Implementation · 12 Caveats · 13 Links
Held-out GSM8K accuracy (val-core/openai/gsm8k/acc/mean@1, 1319 prompts) at the four validation
checkpoints. DENSE is the dashed ceiling; the collapse arms (C7 on current code, A0 historical) both dive to
0.064. C2 holds a flat near-dense plateau; C1 overshoots at step 75 then softens; C8a rises steadily but
pays a warmup tax.
Final ranking by val@100: DENSE 0.776 > C2 0.738 > C8a 0.686 > C1 0.582 > C7 0.390 (collapsed).
| arm | idea | v@0 | v@25 | v@50 | v@75 | v@100 | collapse? |
|---|---|---|---|---|---|---|---|
| DENSE | dense GRPO ceiling, comm_eff OFF (byte-identical upstream) | — | 0.751 | 0.773 | 0.785 | 0.776 | no (−0.009) |
| C2 | proj + correct fires-only (fresh signs, cadence=20) | (0.083) | 0.510 | 0.732 | 0.730 | 0.738 | no (stable) |
| C8a | anchor-owned Q kept fresh via projection (stale-Q warmup) | — | 0.631 | 0.672 | 0.679 | 0.686 | no (stable) |
| C1 | proj + correct every tick (stale signs, cadence=1) | 0.083 | 0.477 | 0.636 | 0.762 | 0.582 | no (softens −0.180) |
| C7 | pure stale-anchor floor: owns_q, no projection, stale θ + stale rollouts | — | 0.626 | 0.605 | 0.064 | 0.390 | YES @75 |
| C0 / A0 | historical control on older code (owns_q, stale-M from fire 1) | 0.080 | 0.388 | 0.064 | — | — | YES @50 |
val@0 is identical across the config-only comm-eff arms (0.083); DENSE has no anchor so its val@0 is not
run. Every number is drawn from the per-run train.log / wandb (see §13).
Shared substrate (identical across every arm): Qwen2.5-1.5B-Instruct + GSM8K, 1×H200, vanilla GRPO (no KL, no entropy bonus), constant LR 1e-6 (no decay), 100 training steps, val every 25 (the epoch-1→2 data boundary is at step 58 = 7473/128). The comm-eff arms add PowerSGD rank-77 activation compression, anchor cadence = delay_K = 20, and the signed_ema merger (α=0.25, β=0.50), resp=1024. The arms differ only in the four knobs below — everything else is byte-identical. ("step=" in the comm_eff logs is an optimizer tick = 2× the training step.)
| arm | Q owner | projection (lookahead) | warmup M (steps 1–29) | sign cadence | rollouts | what it isolates |
|---|---|---|---|---|---|---|
| DENSE | — (OFF) | — | — (no anchor) | — | full grads | Dense GRPO, no compression, no anchor — the quality ceiling. |
| C1 | fast (owns_q=false, Q←orth(V) every step) | learned-linear, engages step 30 (min_snaps=2) | no_correct (M cold → zero correction) | every tick (cadence=1) | current-step (matched) | The full redesign; correction folded continuously (stale signs between fires). |
| C2 | fast (owns_q=false) | learned-linear, engages step 30 | no_correct | fires only (cadence=20, fresh signs) | current-step (matched) | Same as C1 but correct only when sign(M) is fresh — the E4 test. Winner. |
| C7 | anchor (owns_q=true) | OFF (no projection ever) | stale_correct (M from raw stale θ[t−20], from fire 1) | every tick | stale paired | Pure pre-lookahead comm-eff GRPO = the naive method. The collapse floor. |
| C8a | anchor (owns_q=true, Q from θ̂ once engaged) | learned-linear, engages step 30 | stale_correct (forced — no_correct illegal with owns_q=true) | every tick | current-step when projecting | Anchor owns Q, kept fresh via projection (fully anchor-centric architecture). |
| C0 / A0 | anchor (owns_q=true) | learned, late (fire 4, step ~40) | stale_correct (from fire 1) | every tick | stale paired | Historical control on older code; collapsed. C7 is its fresh same-code equivalent. |
The four knobs that vary: anchor.owns_q (who rebuilds the PowerSGD basis Q),
anchor.lookahead_* (whether/when the projector engages), anchor.warmup_mode
(no_correct = skip the anchor pass while M would be stale vs stale_correct = today's raw
stale correction), and spectral.cadence (fold the held M every tick vs only on fresh-M fires).
The fast net trains on PowerSGD-compressed (rank-77) boundary activations: its per-target gradient keeps the right magnitude but has corrupted signs on the coordinates the codec dropped. A clean "anchor" gradient could repair those signs — but in pipeline-parallel the anchor lags by K=20 ticks, where the stale weights have rotated ~orthogonal to the live ones (k-collapse, the mechanism that killed A0). The method makes the anchor gradient both fresh (via linear projection) and clean (measured at the projected point on the current batch), then folds only its signs into the trusted fast-net magnitude.
The 5 SOLID invariants (all verified live at 0 / dev=0 in the logs): no-peek staleness cross-rank determinism (dev=0) magnitude/sign separation cold-M guard anchor isolation
The redesign is defined by four decisions, each posed as an exercise and verified against a specific run.
| ex | question | choice made | how it was verified |
|---|---|---|---|
| E1 | Who owns / refreshes the PowerSGD basis Q? | Fast net owns Q, rebuilt from orth(V) every step (owns_q=false). | C1 recon 0.975→0.029 in ~7 steps, then flat; grad_norm settles ~1.2 by step 7. No cold-Q blowup. Contrast: C7/C8a (owns_q) hold recon 0.975 with grad_norm in the 100s–1200s through step 9. |
| E2 | What does warmup do before projection is legal? | no_correct — skip the anchor pass entirely, zero correction, until projection is possible. | Merger counters: coldM_fallbacks == corrected (196==196, every pass a no-op) for steps 1–29, flips to coldM_fallbacks=0 the instant M is set at step 30. warmup_no_correct_skips locks at 2. |
| E3 | When is projection legal, and then correct + tune? | Earliest-legal projection (min_snapshots=2), then correct + learn the residual every fire. | First projection at fire 60 = step 30: src_ticks=[39,19], h=20, g=20 → coeffs=(2.0000,−1.0000) (exact seed, r=0); 196 matrices projected / 142 excluded; batch=current_step. All isolation guards = 0; cross_rank_max_rel_dev = 0. |
| E4 | Fold the held M every tick, or only on a fresh-sign fire? | Fresh-sign-only (cadence=20) — the winner: C2 > C1. | C2 converges faster (0.732@50 vs 0.636) and ends higher & stable (0.738 vs 0.582); C1 overshoots to 0.762@75 then softens. The correction is large either way (rel_change ≈ 1.0, ~57% of signs flipped) — the win is about when to apply it. |
C1 folds the held M every tick (signs up to 19 ticks stale between fires); C2 folds it only on the fire tick when sign(M) is fresh. Side-by-side, the two share the same early trajectory but diverge in the epoch-2 window: C2 plateaus, C1 gives back its overshoot.
Two per-step signals from the CSVs. (a) reconstruction error: fast-owned Q (C1) drops from 0.975 to ~0.03 within ~7 steps and stays flat. The owns_q arms (C7, C8a) hold Q frozen at 0.975 through step 9, then snap to ~0.04 at the first fire (step 10) when the anchor rebuilds Q. (b) gradient norm (log scale): C1/C2 settle near ~1–2; C7 blows to the hundreds–thousands during the cold-Q window and later spikes into the ignition that precedes its collapse.
Panel (b) is drawn on a log axis so the fast-owned arms' calm ~1–2 range and C7's early hundreds/thousands and its late ignition spike are visible on one plot. C7's grad_norm hit 1201 at step 5 and spikes again near the collapse ignition; the ignition precedes the val@75 dive to 0.064.
C7 and C8a start identically — both owns_q=true, both begin with a cold/stale Q rebuilt at
stale weights. The single difference is projection: C7 never projects (raw stale θ + stale rollouts);
C8a projects from step 30, so the anchor rebuilds Q from the projected θ̂ (≈θ_t, fresh) instead of θ[t−K]. The
outcome is the whole story of k-collapse.
| arm | projection | Q source after warmup | v@50 | v@75 | v@100 | grad_norm @75 | resp_len final | outcome |
|---|---|---|---|---|---|---|---|---|
| C7 | none | stale θ[t−20] | 0.605 | 0.064 | 0.390 | ignition (30 @50) | 739 | COLLAPSE |
| C8a | from step 30 | projected θ̂ (≈θ_t) | 0.672 | 0.679 | 0.686 | 1.42 | 351 | HELD |
C7 doesn't just lose accuracy; it drifts into a degenerate policy. Its response length balloons from ~250 to ~1000 tokens and its entropy caves toward ~0.4 — the classic long-rambling collapse — while healthy arms hold resp_len ~200–250.
C7 response length climbs across steps ~55–75 to ~1000 tokens as the policy degenerates, then partially unwinds to ~730 during the erratic partial recovery; entropy over the same window falls to ~0.35–0.42. The healthy arms (C1 dashed shown) stay in a tight ~200–250 band.
The training-path boundary traffic (activation + gradient across pipeline-stage boundaries) is
bytes_ratio = 0.050 of the dense-equivalent — a ≈20× reduction. The whole point of the study
is that this buys near-dense quality: C2's 0.738 sits within 0.038 of the dense 0.776.
Headline: ~20× less boundary communication for a final-accuracy gap of just 0.038 against the dense ceiling — the project's north star. The method pays an early-learning tax (val@25: dense 0.751 vs C2 0.510) that the correction largely closes by step 50 (C2 0.732 vs dense 0.773).
t4qymvvu).spectral.cadence.imdo7bsb).coldM_fallbacks == corrected (every pass a no-op → zero correction) on every tick before the
first projection; flips to coldM_fallbacks=0 the instant M is set at step 30.
warmup_no_correct_skips locks at 2 (double-skip is a delay_K=20 replay-granularity artifact; both
fires 20 & 40 resolve to the same snapshot_tick=19).
src_ticks=[39,19], h=20, g=20 → coeffs=(2.0000,−1.0000)
= exact frozen AsyncPP seed (r=0), 196 decoder matrices projected / 142 excluded, batch=current_step
(matched rollouts). Correction magnitude rel_change ≈ 1.0 across matrices (anchor flips sign on ~half the coords).
All isolation guards = 0 (anchor_optimizer_steps / rollouts_generated / rewards_recomputed / grad_corrected).
cross_rank_max_rel_dev = 0 (Q and residual DP-identical).
c3eie0ls.The 4-stage loop (§3) rests on four design choices, each with a stated reason:
2θ[τ0]−θ[τ1].The 5 SOLID invariants, all verified live in the logs:
cross_rank_max_rel_dev = 0 for both Q and the learned residual
(DP-identical across ranks).All changes are additive; defaults are byte-identical to upstream verl (with the method's knobs at their
defaults, training is unchanged). Commit 7a9653a0 on vast-ai-workload.
comm_eff.anchor:
warmup_mode ∈ {stale_correct (default), no_correct} and
lookahead_min_snapshots ∈ {−1 (default), [2, n_points]}.no_correct is illegal with owns_q=true).warmup_no_correct_skips; the ring gains min_points — a single
readiness gate shared by the no_correct skip and the projected-vs-fallback branch.7 files touched:
| file | role |
|---|---|
verl/workers/config/comm_eff.py | the two new knobs + 4 validation rules |
verl/trainer/config/actor/actor.yaml | knob defaults (byte-identical behavior) |
verl/workers/comm_eff/lookahead.py | earliest-legal projection + min_snapshots gate |
verl/workers/engine/fsdp/transformer_impl.py | FSDP integration of the anchor loop |
verl/workers/comm_eff/state.py | ring min_points readiness gate + skip counter |
examples/grpo_trainer/vast_comm_eff_baseline_...sh | launcher wiring for the new knobs |
tests/… | 15 new unit tests |
snapshot_tick=19 under delay_K=20 replay granularity, so the second is skipped. This is
a replay-granularity artifact and is benign (warmup_no_correct_skips locks at 2).wandb project: verl_compression_research
(entity shamanework-pl).
| arm | wandb run | final val |
|---|---|---|
| DENSE | ytthgqdg | 0.776 (ceiling) |
| C1 | aevasghy | 0.582 |
| C2 | t4qymvvu | 0.738 (winner) |
| C7 | imdo7bsb | 0.390 (collapsed @75) |
| C8a | c3eie0ls | 0.686 |
| C0 / A0 | a6d174tp | 0.064 (historical collapse) |
Final deliverable for issue #59 (LA-WARMUP-V2). Self-contained (inline CSS + inline SVG, no external
assets); renders offline. All headline numbers from
research/runs/LA-WARMUP-V2/FINDINGS.md; all full-resolution curves computed from the per-step CSVs in
research/runs/LA-WARMUP-V2/data/.