test(lm): fix the batched-gradient assertion that has kept CI red for a month - #693
test(lm): fix the batched-gradient assertion that has kept CI red for a month#693Hotragn wants to merge 1 commit into
Conversation
…lute `test_backward_with_multiple_invokers` has failed on every push to `main` and `dev` since it landed in ndif-team#671, so the test job has been red for a month. The assertion is not satisfiable: assert torch.allclose(ref_grad, batched_grad, atol=1e-5) `d(sum lm_head.output)/d(h[5].attn.c_proj.output)` reaches ~4e4 in float32, where one ULP is already ~4e-3 -- 400x the tolerance. Running the same prompt batched and unbatched reorders the reductions in six layers of backward, so the two results differ by ~1e-5 *relative* (0.7 absolute) no matter what nnsight does. The gradient itself is correct. Reproducing nnsight's batch shape in plain HuggingFace (`retain_grad` on the same submodule, `logits[i:i+1].sum()`) gives a bit-identical discrepancy, and in float64 nnsight's batched gradient matches the unbatched reference to 5e-11 relative. The spread is float32 reduction order. Assert on a normalised norm ratio instead. It is scale-free and unaffected by individual near-zero entries, where cancellation makes elementwise relative error large even for a correct tensor. Measured spread is ~1e-5; the threshold is 1e-4. The test also could not detect the bug class it exists to guard. It used the same prompt for both invokes, so returning invoke 0's gradient -- or the whole batch -- was indistinguishable from success. It now uses distinct prompts, adds a negative control asserting the result does *not* match any other invoke's gradient (measured ~1.2 relative, four orders above the threshold), and parametrises over which invoke takes the gradient: first, middle and last of two and three invokes, so each batch-slice offset is exercised rather than only the final one. Confirmed with a mutant that forces the gradient to be read from batch row 0: the new test fails in all three cases where the target is not row 0, while the old test could not distinguish the mutant from correct code because it failed against both.
|
Closing this — it doesn't apply to
Worth noting what is missing on |
Summary
tests/test_lm.py::TestGradients::test_backward_with_multiple_invokershas failed on every push tomainanddevsince it landed in #671 (2026-07-25). The test job has been red for a month:The gradient is correct. The assertion is not satisfiable.
Why the assertion cannot pass
d(sum lm_head.output)/d(h[5].attn.c_proj.output)reaches ~4e4 in float32, where a single ULP is already ~4e-3 — 400× the tolerance. Running the same prompt batched vs. unbatched reorders the reductions through six layers of backward, so the two differ by ~1e-5 relative (0.7 absolute) regardless of what nnsight does.Two independent controls confirm nnsight is not the source:
Pure HuggingFace, no nnsight.
retain_grad()on the same submodule,logits[i:i+1].sum().backward(), same batch shape — reproduces the discrepancy bit for bit:float64. The same comparison, same code,
dtype=torch.float64:The error collapses by nine orders of magnitude when the mantissa grows. It is float32 reduction order, not a slicing bug.
I also checked the forward pass separately, in case left-padding was involved: nnsight's left-padded batch matches pure HF's left-padded batch exactly (
1.37e-04on logits of magnitude 155, i.e. ~1e-6 relative), and dropping the derivedposition_idsmoves that to9.08e+01— so #673's derivation is doing its job.The test also could not catch its own bug class
Both invokes used the same prompt:
With identical inputs, returning invoke 0's gradient — or the entire batch — is indistinguishable from success. The test guards
MissedProviderError(#664) but not "is this my gradient", which is the harder half.It also only ever exercised the last invoke of two, so the batch-slice offset under test was always the same one.
Changes
_grad_rel_err) instead ofallclose. It is scale-free and insensitive to individual near-zero entries, where cancellation makes the elementwise relative error large (0.06) even for a correct tensor. Measured spread is ~1e-5; threshold is 1e-4.Does the new test have teeth?
Mutation test — force the gradient to always be read from batch row 0:
Result
Full CI test list on CPU, this branch:
Green, with strictly more coverage than before (5 parametrised cases where there was 1).
Test-only change — no source files touched.
Run with
transformers5.15.1 /torch2.9.1 on CPU.