You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
torch.nn.functional.cross_entropy(..., reduction='none'), when combined with a downstream reduction (e.g. (per_token_loss * weights).sum() / weights.sum()), produces a plausible forward-pass loss value and a normal-looking autograd graph (NllLossBackward0) on torch-directml, but the backward pass silently yields an exactly-zero gradient for all upstream trainable parameters. No exception, warning, or NaN is raised — the run appears to train normally (the loss value itself is correct), but no learning occurs.
This is distinct from the already-tracked masked_fill uint8-overflow issue (#702) — it is a separate failure in the cross_entropy(reduction='none') backward kernel path itself, not in a preceding mask-construction op.
Given identical logits/labels for one real training batch:
Loss computation path
Loss value
Backward grad norm on a LoRA lora_B param
F.cross_entropy(reduction='none') then weighted mean, .backward()
9.8083
0.0 (exactly zero)
Manual log_softmax + gather NLL, same weighting, .backward()
9.8083 (identical)
0.0534
HF-internal model(..., labels=...) default mean-reduction cross_entropy path (separate control run, same batch/model state)
(not directly comparable value; separate forward)
0.0699
The loss value computed by the reduction='none' path is numerically identical to the manual reimplementation (both 9.8083 to 4 dp) — so the forward pass and the reduction math are correct. Only the backward gradient silently vanishes for the fused reduction='none' kernel path. .grad is not None (so requires_grad and graph connectivity are intact) — it is a real tensor containing all zeros.
This was originally discovered during real LoRA fine-tuning: a 6-epoch training run using this loss path completed without any error, logged what looked like a plausible flat loss curve (~9.5–9.6 throughout, no explosion/NaN), and finished normally — but post-hoc inspection showed all 120 lora_B tensors were still exactly PEFT's zero-init value (i.e. the optimizer had received a zero gradient at every one of 444 update steps). A second run using the manual log_softmax+gather reimplementation (only that one line changed) produced real, diversified nonzero lora_B values and a real loss collapse (9.6 → 0.009) on the same data/hyperparameters — confirming the fused reduction='none' backward is the sole cause.
Why this is a high-severity, easy-to-miss bug
cross_entropy(reduction='none') is a completely standard pattern for any form of per-token loss weighting (e.g. cold-start/curriculum weighting, focal loss, token-class-balanced loss). On this DirectML build, a training script written this way will run start-to-finish with no exception and a plausible loss log, giving no obvious signal that gradients never flowed downstream — it only surfaces if a user separately inspects trained-parameter deltas, which most training scripts do not do by default.
Minimal reproduction
Repro script (self-contained, requires torch, torch-directml, transformers, peft, and a local causal-LM checkpoint + a JSONL file of {"input_text": ..., "output_json": ...} records — no proprietary or sensitive data, any small local text works):
TEST A (F.cross_entropy reduction='none'): loss=9.8083 lora_B.grad_norm=0.0
TEST B (manual log_softmax+gather): loss=9.8083 lora_B.grad_norm=0.05341910570859909
Expected behaviour
F.cross_entropy(reduction='none') backward should produce the same (nonzero) gradient as the mathematically equivalent manual log_softmax+gather computation, given the loss values themselves already agree to within floating-point precision.
Workaround (used in our project)
Replace F.cross_entropy(..., reduction='none') in any per-token/weighted-loss path with a manual log_softmax(...).gather(...) NLL computation, and add a hard pre-flight check (one real batch forward/backward before the full training loop, asserting a trainable parameter's .grad.norm() > 1e-9, hard-aborting the run otherwise) to catch this class of silent-zero-gradient failure mechanically rather than relying on post-hoc inspection.
Searched existing open/closed issues for cross_entropy, reduction, NllLoss, zero gradient, silent, lora gradient, grad is None against this repo before filing; found no existing report of this exact symptom.
Discovered during real LoRA adapter training work on a small (135M) model; happy to share additional diagnostic output (LoRA weight-tensor inspection before/after training) if useful, with no proprietary data involved (synthetic/internal structured-extraction task only).
Summary
torch.nn.functional.cross_entropy(..., reduction='none'), when combined with a downstream reduction (e.g.(per_token_loss * weights).sum() / weights.sum()), produces a plausible forward-pass loss value and a normal-looking autograd graph (NllLossBackward0) ontorch-directml, but the backward pass silently yields an exactly-zero gradient for all upstream trainable parameters. No exception, warning, or NaN is raised — the run appears to train normally (the loss value itself is correct), but no learning occurs.This is distinct from the already-tracked
masked_filluint8-overflow issue (#702) — it is a separate failure in thecross_entropy(reduction='none')backward kernel path itself, not in a preceding mask-construction op.Environment
torch: 2.4.1+cputorch-directml: 0.2.5.dev240914transformers: 4.46.3peft: 0.20.0privateuseone:0)HuggingFaceTB/SmolLM2-135M+ LoRA adapter (PEFT, r=8, alpha=16, target_modules q/k/v/o_proj),attn_implementation="eager"Observed behaviour
Given identical logits/labels for one real training batch:
lora_BparamF.cross_entropy(reduction='none')then weighted mean,.backward()log_softmax+gatherNLL, same weighting,.backward()model(..., labels=...)default mean-reductioncross_entropypath (separate control run, same batch/model state)The loss value computed by the
reduction='none'path is numerically identical to the manual reimplementation (both 9.8083 to 4 dp) — so the forward pass and the reduction math are correct. Only the backward gradient silently vanishes for the fusedreduction='none'kernel path..gradis notNone(sorequires_gradand graph connectivity are intact) — it is a real tensor containing all zeros.This was originally discovered during real LoRA fine-tuning: a 6-epoch training run using this loss path completed without any error, logged what looked like a plausible flat loss curve (~9.5–9.6 throughout, no explosion/NaN), and finished normally — but post-hoc inspection showed all 120
lora_Btensors were still exactly PEFT's zero-init value (i.e. the optimizer had received a zero gradient at every one of 444 update steps). A second run using the manuallog_softmax+gatherreimplementation (only that one line changed) produced real, diversified nonzerolora_Bvalues and a real loss collapse (9.6 → 0.009) on the same data/hyperparameters — confirming the fusedreduction='none'backward is the sole cause.Why this is a high-severity, easy-to-miss bug
cross_entropy(reduction='none')is a completely standard pattern for any form of per-token loss weighting (e.g. cold-start/curriculum weighting, focal loss, token-class-balanced loss). On this DirectML build, a training script written this way will run start-to-finish with no exception and a plausible loss log, giving no obvious signal that gradients never flowed downstream — it only surfaces if a user separately inspects trained-parameter deltas, which most training scripts do not do by default.Minimal reproduction
Repro script (self-contained, requires
torch,torch-directml,transformers,peft, and a local causal-LM checkpoint + a JSONL file of{"input_text": ..., "output_json": ...}records — no proprietary or sensitive data, any small local text works):Actual output on our hardware/build:
Expected behaviour
F.cross_entropy(reduction='none')backward should produce the same (nonzero) gradient as the mathematically equivalent manuallog_softmax+gathercomputation, given the loss values themselves already agree to within floating-point precision.Workaround (used in our project)
Replace
F.cross_entropy(..., reduction='none')in any per-token/weighted-loss path with a manuallog_softmax(...).gather(...)NLL computation, and add a hard pre-flight check (one real batch forward/backward before the full training loop, asserting a trainable parameter's.grad.norm() > 1e-9, hard-aborting the run otherwise) to catch this class of silent-zero-gradient failure mechanically rather than relying on post-hoc inspection.Additional notes
masked_filluint8-overflow (RuntimeError: value cannot be converted to type uint8_t without overflow with masked_fill in GPT-Neo causal mask creation on DirectML. torch.where works. #702); our repro includes the standardmasked_fillworkaround for that separate issue so it does not interfere with reproducing this one.cross_entropy,reduction,NllLoss,zero gradient,silent,lora gradient,grad is Noneagainst this repo before filing; found no existing report of this exact symptom.