fix: guard backward_dw() bias-grad backfill against a legitimately-empty bgrad - #3400
Open
nvegesna-netizen wants to merge 1 commit into
Open
Conversation
…pty bgrad TransformerEngineBaseModule.backward_dw() unconditionally assumes the bgrad popped from wgrad_store is a real gradient tensor whenever use_bias is True, and crashes with AttributeError: 'NoneType' object has no attribute 'to' if it isn't. In FP8 mode, grad_output_preprocess() always computes the bias gradient eagerly (via bgrad_quantize or an unfused sum), regardless of delay_wgrad_compute. That value flows through the module's ordinary backward() return into normal autograd, which sets bias.grad. If the training framework's own gradient-accumulation hook then consumes bias.grad and resets it to None before backward_dw() runs (a common pattern for frameworks that manage their own master-gradient buffers, e.g. to fold .grad into a separate main_grad and free it for reuse), backward_dw()'s `if bias_tensor.grad is None:` check is satisfied even though there is nothing new to backfill -- the real gradient was already correctly handled by the eager path. Meanwhile, in linear.py's wgrad-GEMM closure, `"bias": (bias if (grad_bias is None and not bwd_args.fp8) else None)` deliberately skips computing bias grad in the wgrad step whenever grad_bias was already set, which in FP8 mode it always is -- so bgrad popped from wgrad_store is legitimately empty in this case, and backward_dw() crashes trying to use it anyway. Guard on bgrad actually holding data before assigning, matching the existing (grad_bias is not None and grad_bias.numel() != 0) pattern already used for the equivalent case in GroupedLinear.backward_dw(). Skipping is correct here rather than computing bgrad in the wgrad step too, since the latter would double-count a gradient that was already folded into the framework's own gradient buffer by the eager path.
Contributor
Greptile SummaryThe PR prevents delayed weight-gradient backfill from dereferencing an absent or empty bias gradient after FP8 bias-gradient handling has already occurred.
Confidence Score: 5/5The PR appears safe to merge, with the new guard correctly handling the legitimately absent FP8 bias-gradient value. Current delayed-wgrad producers supply either a real bias-gradient tensor or Important Files Changed
Reviews (1): Last reviewed commit: "fix: guard backward_dw() bias-grad backf..." | Re-trigger Greptile |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TransformerEngineBaseModule.backward_dw()crashes withAttributeError: 'NoneType' object has no attribute 'to'whendelay_wgrad_computeis enabled together with FP8 and a biased
Linear/LayerNormLinearlayer, under atraining setup where something external resets
bias.gradback toNonebetween themodule's ordinary
backward()and the laterbackward_dw()call (e.g. agradient-accumulation hook that folds
.gradinto its own master-gradient buffer andclears it for reuse — a common pattern in large-scale training frameworks).
Root cause
grad_output_preprocess()always computes the bias gradient eagerly(via
bgrad_quantizeor an unfused sum), regardless ofdelay_wgrad_compute. Thatvalue flows through the module's ordinary
backward()return into normal autograd,which sets
bias.grad.bias.gradand resets it toNone(asdescribed above) before
backward_dw()runs,backward_dw()'sif bias_tensor.grad is None:check is satisfied even though there is nothing newto backfill — the real gradient was already correctly handled by the eager path.
linear.py's wgrad-GEMM closure builds its kwargs as"bias": (bias if (grad_bias is None and not bwd_args.fp8) else None)— in FP8mode this is always
None, since step 1 already computed the real gradient. So thewgrad GEMM never computes a bias grad in FP8 mode, and
bgradpopped fromwgrad_storeinbackward_dw()is legitimately empty.backward_dw()doesn't distinguish "nothing to backfill because it was alreadyhandled" from "genuinely missing" — it unconditionally tries
bgrad.to(...),crashing on the empty value from step 3.
This only affects FP8 (non-FP8 layers genuinely defer bias-grad computation to the
wgrad GEMM —
grad_output_preprocess's non-FP8 branch returnsgrad_bias=Noneonpurpose there, so
bgradis real in that path andbackward_dw()correctly appliesit; this fix doesn't touch that path, since
bgradis non-empty there).Fix
Guard on
bgradactually holding data before assigning, matching the existinggrad_bias is not None and grad_bias.numel() != 0pattern already used for theequivalent situation in
GroupedLinear.backward_dw(). Skipping is correct here ratherthan computing
bgradin the wgrad step too, since the latter would double-count agradient that was already folded into the caller's own gradient buffer by the eager
path — a silent numerics bug, strictly worse than the crash it would replace.
Testing
I don't have GPU access in the environment I used to prepare this change, so I could
not run TE's own test suite directly against it. What I have validated, on real
GPU hardware, training a MoE-style model with FP8 delayed scaling and a training
framework that performs the external-
.grad-reset pattern described above (aninterleaved-pipeline-parallel schedule invoking
backward_dw()after eachmicrobatch's backward pass, with a biased QKV projection):
bgrad.to(...)line,on the unmodified module.
point with the same configuration.
different module's analogous bug) and confirmed this specific crash still
reproduces without this change — i.e. this fix is independently necessary, not
incidental to something else.
On coverage in this repo's own test suite:
tests/pytorch/test_numerics.pyhastest_linear_accuracy_delay_wgrad_compute, which already exercisesdelay_wgrad_computewith
bias=True, but it doesn't currently reproduce this bug because (a) it'sparametrized only over
torch.float32/float16/bfloat16, never FP8, and (b) itsharness calls
loss.backward()immediately followed byblock.backward_dw()withnothing in between resetting
bias.grad— sobias.gradis simply left set from theordinary backward pass, and
backward_dw()'sif bias_tensor.grad is None:guardskips the crashing branch entirely in that harness. Reproducing this bug in that test
would need an FP8-enabled parametrization plus an explicit
bias.grad = Nonebetweenbackward()andbackward_dw()to model the external-consumer pattern. I'm notproposing that test addition myself in this PR since I can't execute it locally to
confirm it's correct, but wanted to flag the precise gap for maintainers, since it's a
straightforward extension of the existing harness if useful.