Repository navigation
Description
Summary
F.cross_entropy with reduction="mean" on a spatial (4-D) fp16 input returns inf, then NaN with an all-zero gradient, once the per-work-group partial sums exceed the fp16 range, even though the true loss is ~3.5. The XPU nll_loss2d forward reduces each work group in acc_type_device<scalar_t, kXPU> (fp32) but casts the result back to scalar_t and accumulates it into fp16 global accumulators, and output/total_weight are allocated in the input dtype.
Found by an upstream alignment scan of the window [2026-10-15T00:00:00Z, 2026-10-16T00:00:00Z). Upstream source: pytorch/pytorch#201120 (reported for CUDA, aten/src/ATen/native/cuda/NLLLoss2d.cu, open, no fix landed). XPU has its own kernels, so the CUDA fix will not cover XPU.
Observed XPU behavior
Intel(R) Arc(TM) Pro B60 Graphics, torch 2.16.0.dev20261015+xpu:
| pixels (HxW) | XPU fp16 loss | fp64 CPU reference | all-zero grad |
|---|---|---|---|
| 128x128 = 16384 | 3.513671875 | 3.5134 | no |
| 181x181 = 32761 | inf |
3.5003 | no |
| 256x256 = 65536 | nan |
3.5044 | yes |
The signature matches the upstream CUDA report exactly: the loss sum overflows to inf first, then total_weight overflows and inf/inf gives NaN; the backward divides by the overflowed total_weight, so the input gradient is silently all zero. Autocast is unaffected because autocast runs nll_loss in fp32.
Target-path evidence
The reproducer keeps the tensors on XPU for the whole measured op (input.device=xpu:0, dtype=torch.float16, loss.device=xpu:0, grad.device=xpu:0) and compares against an fp64 CPU reference computed from the identical initialized inputs, so only the XPU kernels are measured. The code path is:
src/ATen/native/xpu/LossNLL2d.cpp:58-59-outputandtotal_weightare allocated withself.options(), i.e. fp16.src/ATen/native/xpu/sycl/LossNLL2dKernels.cpp:123-128- each work group reduces inaccscalar_t(fp32) and then doesatomicAdd(sycl_global_ptr<scalar_t>(total_weight), static_cast<scalar_t>(acc_weight_reduce))and the same foroutput, so the cross-work-group accumulation is fp16.src/ATen/native/xpu/sycl/LossNLL2dKernels.cpp:137-*output /= *total_weightin fp16.src/ATen/native/xpu/sycl/LossNLL2dKernels.cpp:324- backward computes-(size_average ? *grad_output / *total_weight : *grad_output), which is 0 oncetotal_weightisinf.
Reproducer
import torch
import torch.nn.functional as F
torch.manual_seed(0)
C = 21
for H in (128, 181, 256):
x_cpu = torch.randn(1, C, H, H, dtype=torch.half)
y_cpu = torch.randint(0, C, (1, H, H))
x = x_cpu.detach().to("xpu").requires_grad_(True)
y = y_cpu.to("xpu")
loss = F.cross_entropy(x, y)
loss.backward()
ref = F.cross_entropy(x_cpu.double(), y_cpu).item()
print(
f"{H * H} px: loss {loss.item()} (fp64 {ref:.4f}), "
f"all-zero grad: {bool((x.grad == 0).all())}"
)Output:
16384 px: loss 3.513671875 (fp64 3.5134), all-zero grad: False
32761 px: loss inf (fp64 3.5003), all-zero grad: False
65536 px: loss nan (fp64 3.5044), all-zero grad: True
Environment
torch 2.16.0.dev20261015+xpu
python 3.13.13 (/opt/conda/bin/python3.13)
torch path /opt/conda/lib/python3.13/site-packages/torch/__init__.py
xpu device Intel(R) Arc(TM) Pro B60 Graphics
Ownership
The accumulation and the output allocation are XPU code in this repository (LossNLL2d.cpp, LossNLL2dKernels.cpp), so the fix is independent XPU work: allocate output/total_weight in the accumulate type (or accumulate into fp32 scratch) and cast only the final mean back to the input dtype, matching what the upstream issue proposes for NLLLoss2d.cu. Upstream issue 201120 is CUDA-scoped and has no linked fix PR, and no intel/torch-xpu-ops issue tracks this signature (the closest, #5050, is tolerance flakiness in the fused linear_cross_entropy, a different op and root cause). Since cross-work-group accumulation is also the source of nondeterminism here, the fix may be worth coordinating with the upstream direction in pytorch/pytorch#201120.
This issue was produced by an automated upstream-alignment scan with an AI assistant; the evidence above was gathered on an XPU host and independently reviewed.
Activity
laifenxiawucha commentedon Oct 20, 2026
@torchxpubot fix
torchxpubot commentedon Oct 20, 2026
Automated fix session for this issue. Stages are appended below as they complete.
Issue Triage
Automated by issue-triage.
Reproduce
Ran the issue's reproducer against nightly
torch 2.16.0.dev20261019+xpuonIntel(R) Arc(TM) Pro B60 Graphics(BMG).F.cross_entropyfp16 4-D mean, 181x181 pxloss inf(fp64 reference 3.5003)F.cross_entropyfp16 4-D mean, 256x256 pxloss nan,all-zero grad: TrueA direct
aten::nll_loss2d_forwardcomparison shows CPU fp16 saturatesidentically, so this is not an XPU-vs-CPU divergence but an fp16
accumulation defect that XPU shares with CPU/CUDA; the XPU kernels are
ours to fix, as the issue notes.
Automated by fix-reproduce.
Root Cause
Analyzed at
torch-xpu-ops@76cc8324, pytorchorigin/main@9544ec5e.The reduction path of the XPU
nll_loss2dforward reduces each work group inthe accumulate type but then leaves the accumulate type to combine work
groups:
src/ATen/native/xpu/sycl/LossNLL2dKernels.cpp:122-129castsacc_weight_reduce/input_sum_reduceback toscalar_tand atomically addsthem into the fp16
output/total_weightscalars, andLossNLL2dKernels.cpp:137then divides in fp16. For a 4-D fp16 input of181x181 pixels the loss sum is ~1.1e5, past the fp16 maximum of 65504, so
outputsaturates toinf; at 256x256 the weight sum (65536) overflows tooand
inf/infyieldsNaN. The backward atLossNLL2dKernels.cpp:324dividesby that same
total_weight, so once it isinfevery gradient is exactlyzero.
The in-repo 1-D sibling already does this correctly:
nll_lossreduces in asingle work group and performs the mean division in the accumulate type,
casting only the final values out
(
src/ATen/native/xpu/sycl/LossNLLKernel.cpp:152-156). The 2-D kernel needsthe same, adapted to its multi-work-group atomics: accumulate into an
accscalar_tscratch buffer and let a finalize kernel computeoutput = static_cast<scalar_t>(sum / weight_sum).The public dtypes cannot change: CUDA allocates both outputs with
self.options()(aten/src/ATen/native/cuda/NLLLoss2d.cu:502-503) andaten.nll_loss2d_forwardhas a registered decomposition(
torch/_decomp/decompositions.py:4932) that fixes the meta dtype oftotal_weightto the input dtype. Sototal_weightstays fp16 and stillsaturates above 65504 elements; the loss becomes correct in every case, and
the gradient is correct whenever the total weight is representable in fp16.
Automated by fix-root-cause.
Implement Result
torch-xpu-ops@76cc8324src/ATen/native/xpu/sycl/LossNLL2dKernels.cpp(+41/-25): the forward reduce kernel now atomically accumulates into anaccscalar_tscratch buffer, and a newnll_loss2d_forward_finalize_kernelperforms the mean division inaccscalar_tand casts the loss and total weight back to the input dtype (it replaces the fp16nll_loss2d_forward_average_kernel).test/regressions/test_nll_loss2d_fp16_overflow.py(+66): new regression test.accscalar_treduction, which is what overflowed; the 1-Dnll_losskernel already keeps both in the accumulate type and casts once at the end (src/ATen/native/xpu/sycl/LossNLLKernel.cpp:152-156).Automated by fix-implement.
Verify
Source build of pytorch
origin/main@9544ec5ewith torch-xpu-ops at the fixcommit,
TORCH_XPU_ARCH_LIST=bmg,pvc, onIntel(R) Arc(TM) Pro B60 Graphics.The baseline and fixed builds differ only by
src/ATen/native/xpu/sycl/LossNLL2dKernels.cpp: the kernel hunk was revertedin the same tree and rebuilt to get the Before column, so this is a true A/B
on one build rather than a wheel-vs-source comparison.
test_cross_entropy_fp16_spatial_mean_H_181test_cross_entropy_fp16_spatial_mean_H_256test_cross_entropy_fp16_spatial_mean_backwardloss infloss 3.5(fp64 3.5003)loss nan, all-zero gradloss 3.50390625(fp64 3.5044), all-zero gradBefore was
loss infat 32761 px andloss nanwith an all-zero gradient at65536 px. The backward case passes on both builds: it pins the gradient
semantics but is not the regression guard. The 256x256 gradient is still
all-zero, which this fix cannot change --
total_weightis returned in theinput dtype (CPU and CUDA do the same, and the registered
aten.nll_loss2d_forwarddecomposition fixes its meta dtype), so a weight sumof 65536 saturates to
infin fp16 whatever the kernel does. CPU fp16 returnsnanand all-zero gradients for both sizes, so XPU is now strictly betterthan the CPU reference here.
Regression checks on the fixed build:
test/regressions/test_nll_loss2d_fp16_overflow.pytest/xpu/test_nn_xpu.py -k "nll_loss"test/xpu/test_ops_xpu.py -k "nll_loss or cross_entropy"lintrunner -aon both changed files, including CLANGFORMAT and CLANGTIDY)Automated by fix-verify.
Summary
Outcome: IMPLEMENTING(fix_verified)
The XPU
nll_loss2dforward was the only step of an otherwise fp32 reductionthat fell back to the input dtype: it cast each work group's partial sums back
to
scalar_t, atomically added them into the fp16output/total_weightscalars, and divided in fp16, so a mean-reduced fp16
F.cross_entropyover afew tens of thousands of pixels overflowed to
infand thenNaN. The forwardnow accumulates into an accumulate-type scratch buffer and a finalize kernel
does the division there, casting only the final loss and total weight back --
the same shape as the 1-D
nll_losskernel in this repo. On a source build thereported
inf/NaNlosses become 3.5 and 3.504 against fp64 references of3.5003 and 3.5044, the new regression test fails with the kernel hunk reverted
in the same tree, and the
nll_loss/cross_entropysuites stay green. Onereported symptom is out of reach:
total_weightis returned in the input dtypeby schema, so beyond 65504 total weight it still saturates to
infand thatgradient is still all-zero -- CPU and CUDA behave the same way, and
pytorch/pytorch#201120tracks the shared defect upstream.agent/fix-issue-572276cc832470c07a51e9d3595317d4b459a0ae5677src/ATen/native/xpu/sycl/LossNLL2dKernels.cpp,test/regressions/test_nll_loss2d_fp16_overflow.pyGenerated by fix job.
Review
Copy into a new comment, keep ONE verdict, add a note. Two verdicts in
one comment are dropped as ambiguous.
On a batch run add
item=<pytorch issue>to say which sub-item theverdict is about, in the bold text and in the marker -- the marker
is invisible, so the bold line is all a reader sees. One comment per
sub-item, since a bare verdict on a batch does not say which test it
means and is dropped.
run=<id>only when judging a run other than thenewest, in both places too.
A CI DISABLED sub-item,
item=from the fan-out table:Another sub-item of the same batch, judged on an earlier run:
An
[xpu-alignment]issue that should not have been filed -- one issueper difference, so no
item=:laifenxiawucha commentedon Oct 21, 2026
weak-rejected
notes: Local testing confirms the issue, but the proposed algorithm only fixes forward loss while all-zero gradients persist at 256×256.
daisyden commentedon Nov 10, 2026
label-issue: #5722
Root cause:
nll_loss2dforward casts fp32 work-group sums to fp16 before globalatomicAdd(src/ATen/native/xpu/sycl/LossNLL2dKernels.cpp:122-129)into fp16
output/total_weight(src/ATen/native/xpu/LossNLL2d.cpp:58-59), overflowing to inf/NaN; owner torch-xpu-ops.typeBugissue_typeempty; mean cross_entropy returns inf/NaN instead of ~3.5.testtest: utmodulemodule: reductionpriorityMediumdtypedtype: float16dtype=torch.float16; fp16 global atomics).AccuracytrainingApplied to this issue. Generated by a local
label-issuerun.