feat: --trim-backbone-rows — run TTT steps >= 1 only on rows that can still emit loss - #771
feat: --trim-backbone-rows — run TTT steps >= 1 only on rows that can still emit loss#771julyanghar wants to merge 6 commits into
Conversation
|
/gemini review |
|
I just pushed two more commits to this branch, and I owe you an explanation for why they landed After the benchmarks in the description, I went looking for the point where row trimming stops What I found. A trimmed step cannot use flash attention: the kept rows are not contiguous in
Why it matters for the default recipe. Supervision density is not a knob, it is the fraction
Short chats are dense because the user asks one line and the model answers at length; long samples The fix. A per-sample cost guard,
Savings at low density are unchanged (the 1.6% delta is the guard's own arithmetic); the loss at The two commits are +218 lines over 8 files (56 lines of guard in |
… still emit loss Follow-up to sgl-project#705 (trim_loss_positions) and the RFC in sgl-project#706. On prompt-heavy data most positions never contribute gradient, yet the draft backbone re-runs all of them at every TTT step. With training.trim_backbone_rows=true, steps >= 1 forward only R_i = union_{j>=i} { s - j : s in sup, 0 <= s - j < chunk }, the nested union of rows that can still emit loss at this or a later step (hidden states chain between steps, so a row needed later must be forwarded now). Step 0 stays full-length: it provides the K/V context every later row attends to. Mechanics - One shared trimmed-attention path for sdpa/fa/usp (flex is rejected at config validation): grouped-query layout throughout, explicit causal mask from absolute positions (non-contiguous row sets are exactly what flash kernels cannot express), diagonal-private K/V row-aligned via nested-set index maps, per-step activation checkpointing. - Long step-0 K/V (>= 8192) switch to a single fused memory-efficient SDPA call per step: query groups fold into the row axis, diagonal K/V join as extra columns behind a boolean mask, softmax never reaches HBM. - Under USP the trimmed steps bypass ring/Ulysses entirely (per-rank row counts differ): step-0 K/V are all-gathered once with the autograd-aware all_gather (backward = reduce-scatter sum), and enabling is agreed rank-uniformly (all_reduce MIN) so mixed trim/full ranks cannot deadlock. - Guards: requires trim_loss_positions; rejects flex_attention, compact_teacher, mrope drafts, non-contiguous position_ids. Verification - CPU golden tables pin the R_i union math (incl. overlap-tail bound, dead steps, the naive fixed-sup reading is detected by a negative control). - Equivalence vs the untrimmed path: all-ones mask (trivial-equality bound), prompt-heavy/block-boundary/isolated masks, grad-norm check, on sdpa, fa, and 4-rank ring4 USP; the fused SDPA core is additionally forced through every case via a patched threshold. Benchmark (Llama3-8B dims, TTT 7, 16-block 8% supervised mask, 48GB cards) - 8k single-GPU sdpa: baseline OOMs, trimmed runs (38.2 GB, 1.16 s/step) - 32k ring4: 17.9 -> 8.6 GB/rank, 1.68 -> 0.62 s/step - 64k ring4: 32.9 -> 14.9 GB/rank, 4.60 -> 1.81 s/step - 16k single-GPU sdpa: both OOM (step-0's materialized sdpa attention is the binding constraint; fa's step 0 does not have it) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1e-5 was calibrated on one GPU model; CI's hardware measures 1.2e-5. 1e-4 still sits ~250x below the smallest discrete semantic error (~loss/C). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Row trimming replaces flash attention with an explicit masked-matmul kernel that costs more per row, so it only pays while the kept-row sets are small. Measured here (8192-token chunk, TTT=7, RTX 6000 Ada): flash attention runs at ~16.7 us/row against 28-62 us/row for the trimmed kernel, so step time breaks even near 40% density and degrades to 2.4x slower at full supervision, where no row can be dropped at all. On ShareGPT that regime is the common case, not the exception: samples under 4096 tokens are 99.7% of the corpus and their median supervised fraction is 0.92. Compare the two sides directly -- trimmed rows sum_i |R_i| against full rows (k-1) * chunk -- and keep the B path only while the former stays under trim_backbone_rows_max_density (default 0.35, set 1.0 to always trim). The decision is per sample rather than per step: |R_i| barely varies across steps for contiguous supervision, and a mixed full/trimmed unroll would need per-backend K/V layout conversion plus an extra head-dimension gather under USP. Under USP the ranks agree by MIN, for the same reason the enable flag does: a mixed decision would mismatch collectives and hang. Existing equivalence tests pin the guard off so they keep exercising the trimmed kernels; new tests cover both sides of the threshold, that the guard actually fires, and that the decision is rank-uniform on 4 ranks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Motivation
Follow-up to #705 (
--trim-loss-positions, merged) and the RFC in #706. On prompt-heavy training data (agent/RAG traces, long-context SFT — often ~90% loss-masked), #705 removed the vocab-sized teacher/logits/loss work at masked positions, but the draft backbone still runs full-length attention + MLP for every position at every TTT step, even though masked positions' outputs never reach a live gradient.One correction to the RFC first: #706 proposed forwarding "only the supervised rows" at steps 2..k, and quoted an equivalence check and a memory table for a prototype built that way. That row selection is wrong — loss rows shift left once per TTT step (the same shifted-mask semantics that #705's review uncovered), and the prototype's self-check shared the same wrong assumption, so it validated itself. Both the old equivalence claim and the old numbers should be considered retracted; this PR is a from-scratch implementation of the corrected math, with tests designed so that the historical wrong reading is explicitly detected (a negative control fails on it).
The corrected row set: at step i, forward
R_i = ∪_{j≥i} { s − j : s ∈ sup, 0 ≤ s − j < chunk }— every row that can still emit loss at this or a later step (hidden states chain between steps, so a row needed later must be forwarded now). The sets are nested (R_i ⊆ R_{i−1}), which keeps all row bookkeeping tosearchsortedlookups. Step 0 always stays full-length: it produces the K/V context every later row attends to. On contiguous supervised blocks (real SFT data) the union barely inflates: 8% supervised ⇒ steps ≥ 1 forward ~8% of rows.Modifications
training.trim_backbone_rows(default off; requirestrim_loss_positions; validated againstflex_attention,compact_teacher, mrope drafts, non-contiguousposition_ids), wired through capability/planning/strategy exactly liketrim_loss_positions._build_trim_packadditionally emits per-step row sets and nested-set index maps (dead steps padded so kernels and collectives stay rank-aligned).llama3_eagle.py: grouped-query layout, explicit causal mask from absolute positions (non-contiguous row sets are exactly what flash kernels cannot express), diagonal-private K/V from later steps row-aligned via the index maps, activation checkpointing per step/row-chunk.[rows × L]tensor ever reaches HBM.all_gather(backward = reduce-scatter sum — correct when each rank owns a different loss), and enabling is agreed rank-uniformly (all_reduceMIN) so mixed trim/full ranks cannot deadlock.Related Issues
RFC #706 (this implements it, with the row-set math corrected as described above). Builds on #705.
Accuracy Test
tests/test_runtime/test_trim_backbone_rows.py:R_iunion math (overlap-tail bound, dead-step padding, unreachable-supervision fallback, non-USP back-compat) — these run in CI even without GPUs.trim_loss_positionson): all-supervised mask (trivial-equality boundary — any row/position error is exposed without tolerance cover), prompt-heavy / block-boundary / isolated-point masks, plus a total-grad-norm check; on sdpa, fa, and 4-rank ring4 USP. The fused SDPA core is additionally forced through every case via a patched threshold.suprow selection and confirms the equivalence tests detect it (rel. err 1.7e-2 at step 1, step 0 exact — the fingerprint of the shifted-mask bug).Benchmark & Profiling
Llama3-8B dims (hidden 4096, target vocab 128256, draft vocab 32000), TTT 7, 16-block 8% supervised mask, RTX 6000 Ada 48 GB, batch 1. Baseline =
trim_loss_positionsonly; trimmed = +trim_backbone_rows. Peak =torch.cuda.max_memory_allocatedper rank, mean of 3 steps after warmup:The savings compose with #705's (both are on in the baseline column already).
Applicability boundary and the cost guard
Row trimming is not free: a trimmed step cannot use flash attention (the kept rows are not
contiguous), so it runs the explicit masked-matmul kernel, which costs more per row. Fewer rows
dropped and a higher per-row cost stack up, so the win shrinks as supervision gets denser and
eventually inverts.
Measured on an 8192-token sequence, TTT=7, single RTX 6000 Ada,
fabackend, both arms with--trim-loss-positions:Supervision density is a property of the data, not a tunable: it is the fraction of tokens written
by the assistant (
_apply_loss_mask_from_chat_templatemarks assistant spans only). Over the120,675 ShareGPT samples shipped with this repo, samples under 4096 tokens (99.7% of them) have a
median density of 0.92, while samples at or above 4096 tokens have a median of 0.056. Short
chats are dense; long samples are long because of pasted material, not longer answers.
training.trim_backbone_rows_max_density(default 0.35) therefore gates the feature per sample:if
sum_i |R_i| > tau * (k-1) * chunk, the sample runs the full-length path and anINFOlogexplains why. Under USP the decision is agreed across ranks with
all_reduce(MIN), since ranks canotherwise land on opposite sides of the budget and mismatch collectives (verified: removing the
agreement hangs a 4-rank run past a 180 s timeout).
With the guard, low-density data keeps the full benefit (9.03 GB / 0.325 s at 8%, versus 9.03 GB /
0.320 s without the guard) and dense data degrades to the baseline instead of running 2.45x slower
(32.68 GB / 1.586 s versus the baseline's 32.68 GB / 1.548 s).
Practical consequence worth stating plainly: on the default ShareGPT recipe the guard will keep
--trim-backbone-rowsswitched off. This feature targets long-context and agent-trajectory data,where supervision is sparse — not the short-chat default.
The decision is per sample rather than per step:
|R_i|barely varies across steps for contiguoussupervision, and a mixed trimmed/full unroll would need per-backend K/V layout conversion plus an
extra head-dimension gather under USP — new collectives for a safety guard is a poor trade.
Checklist