Fix wrong grad_weight/grad_bias for fused linear cross entropy with reduction='none'#1298
Open
kashif wants to merge 2 commits into
Open
Fix wrong grad_weight/grad_bias for fused linear cross entropy with reduction='none'#1298kashif wants to merge 2 commits into
kashif wants to merge 2 commits into
Conversation
With reduction='none' the loss is per-token, so backward gets a per-token grad_output. element_mul_kernel loads it as a scalar, so every gradient was being scaled by grad_output[0]. grad_weight is a sum over tokens, so the upstream weight has to be applied to each row of the logits gradient before that sum. Once it is summed there is no scalar you can multiply by to fix it. So for reduction='none' we now defer the gradient to backward, where grad_output is known, and fold it in per row before projecting. mean/sum keep the existing compute-in-forward path. Adds a regression test with a non-uniform grad_output. The existing tests only ever pass torch.ones, which hides the bug.
kashif
added a commit
to kashif/Liger-Kernel
that referenced
this pull request
Jul 11, 2026
Same bug as the Triton backend (see linkedin#1298): grad_weight is a sum over tokens, so a per-token grad_output has to be folded into the logits gradient before the sum, not applied to the result afterwards. Defer the gradient to backward for reduction='none' and fold grad_output in per row. mean/sum are unchanged. Adds a regression test with a non-uniform grad_output.
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
LigerFusedLinearCrossEntropyFunctionreturns wronggrad_weightandgrad_biaswhenreduction="none"and the upstream gradient is not uniform.With
reduction="none"the loss is a per-token vector, sobackward()receives a per-tokengrad_outputof shape(BT,). That is the whole point of the mode (token weighting, masking, per-sample scaling). Butelement_mul_kernelloadsgrad_outputwith a scalartl.load(grad_output_ptr), so every gradient ends up multiplied bygrad_output[0].There is already a comment in the code acknowledging this:
# Need extra calculations for backward if reduction=='none'. Not supporting reduction='none' now.The failure is silent. Forward is correct, backward returns plausible-looking numbers that are simply wrong.
Why it cannot be patched in backward
Forward computes the logits gradient per chunk and immediately projects it, collapsing the token dimension:
By the time backward runs we hold
sum_i (g_i outer x_i). What we need issum_i go_i * (g_i outer x_i). Those differ by a per-token weight applied inside the sum, and no scalar multiply on the already-summed result can recover it. The information is gone.grad_inputis different. It is per-row and never summed, so scaling it row-wise afterwards is valid.Fix
For
reduction="none", defer the gradient computation to backward, wheregrad_outputis known, and fold it into each row of the logits gradient before projecting.meanandsumkeep the existing compute-in-forward fast path, so the common case is unchanged.The mechanism already existed:
use_token_scalingdoes exactly this per-row scaling before the projection. This reuses it with the upstream gradient.Cost is one extra chunked GEMM in backward for
reduction="none"only. Memory is unchanged, still chunked, still never materializing the fullBT x Vlogits.Test
Adds
test_correctness_reduction_none_per_token_grad_output, which backprops a non-uniformgrad_outputand comparesgrad_input,grad_weightandgrad_biasagainst plain autograd.The existing tests only ever call
loss.backward(torch.ones_like(loss)). With uniform ones the scaling is a no-op, which is why this went unnoticed.Before the fix (errors vs autograd):
grad_output = onesgrad_output = randgrad_inputgrad_weightgrad_biasAfter the fix all three match autograd to 1e-4.
Rest of the suite is unchanged: 125 passed, plus the full
test_cross_entropy.py.