Reduced-precision `nn.Linear` (addmm) on Intel XPU (Arc B580 / Battlemage) gives wrong output — fp16 returns all-zero, bf16 returns NaN; fp32 on the same GPU and fp16 on CPU both give correct results
- Dominant language
- Python
- Stars
- 113
- Forks
- 128
- Avg merge
- 5d 13h
- Merged PRs (30d)
- 107
Description
# Reduced-precision `nn.Linear` (addmm) on Intel XPU (Arc B580 / Battlemage) gives wrong output — fp16 returns all-zero, bf16 returns NaN; fp32 on the same GPU and fp16 on CPU both give correct results
## Summary
A single `nn.Linear` forward pass on an Intel Arc B580 (Xe2/"Battlemage") XPU device gives incorrect output for **both** reduced-precision dtypes tested, for an input and weights that are both small, finite, and well within the representable range of either format:
- **fp16 on XPU:** output is **entirely zero** across the whole tensor.
- **bf16 on XPU:** output contains **NaN**, and the non-NaN portion of the tensor sits suspiciously close to the *weight* tensor's own value range rather than a plausible matrix-multiply result — suggesting the accumulation/multiply may not be happening as expected rather than merely overflowing.
- **fp32 on the same XPU device** and **fp16 on CPU** (same input, same weights) both give the numerically correct result.
This was found while debugging a downstream bug where an SDXL UNet produces 100% NaN latents on this GPU (see linked ComfyUI issue below); this reproduction isolates it to a single linear layer, independent of any model or framework beyond PyTorch itself.
## Environment
- **GPU:** Intel Arc B580, 12GB VRAM (Xe2 / Battlemage architecture)
- **OS:** Windows
- **Python:** 3.11.7
- **PyTorch:** `2.15.0.dev20260903+xpu` (nightly). Also reproduces on `2.15.0.dev20260831+xpu` (four days apart, same behavior).
- **Driver stack:** Intel oneAPI runtime as bundled with the `+xpu` wheel (`intel-sycl-rt`, `dpcpp-cpp-rt`, `mkl` etc., all `2026.1.0`), `--oneapi-device-selector level_zero:0` explicitly set.
## Minimal reproduction
```python
import torch
print(f"PyTorch version: {torch.__version__}")
print(f"XPU available: {torch.xpu.is_available()}")
print(f"Device: {torch.xpu.get_device_name(0)}")
print()
torch.manual_seed(0)
# Dimensions match the timestep-embedding projection in Stable Diffusion XL's UNet
x = torch.randn(2, 320, dtype=torch.float16, device='xpu')
layer = torch.nn.Linear(320, 1280, dtype=torch.float16, device='xpu')
out = layer(x)
print("=== fp16 Linear(320, 1280) on XPU ===")
print(f"Input - NaN: {torch.isnan(x).any().item()} | min/max: {x.min().item():.4f}/{x.max().item():.4f}")
print(f"Weight - NaN: {torch.isnan(layer.weight).any().item()} | min/max: {layer.weight.min().item():.4f}/{layer.weight.max().item():.4f}")
print(f"Output - NaN: {torch.isnan(out).any().item()} | min/max: {out.min().item():.4f}/{out.max().item():.4f}")
print()
# Same weights/input, fp32 on XPU
x32 = x.to(torch.float32)
layer32 = torch.nn.Linear(320, 1280, dtype=torch.float32, device='xpu')
layer32.weight.data = layer.weight.data.to(torch.float32)
layer32.bias.data = layer.bias.data.to(torch.float32)
out32 = layer32(x32)
print("=== Same weights/input, fp32 on XPU ===")
print(f"Output - NaN: {torch.isnan(out32).any().item()} | min/max: {out32.min().item():.4f}/{out32.max().item():.4f}")
# Same weights/input, fp16 on CPU
x_cpu = x.to('cpu')
layer_cpu = torch.nn.Linear(320, 1280, dtype=torch.float16, device='cpu')
layer_cpu.weight.data = layer.weight.data.to('cpu')
layer_cpu.bias.data = layer.bias.data.to('cpu')
out_cpu = layer_cpu(x_cpu)
print()
print("=== Same weights/input, fp16 on CPU ===")
print(f"Output - NaN: {torch.isnan(out_cpu).any().item()} | min/max: {out_cpu.min().item():.4f}/{out_cpu.max().item():.4f}")
# Same weights/input, bf16 on XPU
x_bf16 = x.to(torch.bfloat16)
layer_bf16 = torch.nn.Linear(320, 1280, dtype=torch.bfloat16, device='xpu')
layer_bf16.weight.data = layer.weight.data.to(torch.bfloat16)
layer_bf16.bias.data = layer.bias.data.to(torch.bfloat16)
out_bf16 = layer_bf16(x_bf16)
print()
print("=== Same weights/input, bf16 on XPU ===")
print(f"Output - NaN: {torch.isnan(out_bf16).any().item()} | min/max: {out_bf16.min().item():.4f}/{out_bf16.max().item():.4f}")
```
## Actual output
```
PyTorch version: 2.15.0.dev20260903+xpu
XPU available: True
Device: Intel(R) Arc(TM) B580 Graphics
=== fp16 Linear(320, 1280) on XPU ===
Input - NaN: False | min/max: -3.1289/3.3164
Weight - NaN: False | min/max: -0.0559/0.0559
Output - NaN: False | min/max: 0.0000/0.0000
=== Same weights/input, fp32 on XPU ===
Output - NaN: False | min/max: -1.7401/1.6903
=== Same weights/input, fp16 on CPU ===
Output - NaN: False | min/max: -1.7402/1.6904
=== Same weights/input, bf16 on XPU ===
Output - NaN: True | min/max: -0.0559/0.0557
```
## Expected behavior
Both the fp16-on-XPU and bf16-on-XPU outputs should be numerically close to the fp32-on-XPU and fp16-on-CPU outputs (min/max around -1.74/1.69, matching to within each format's precision). Instead:
- fp16-on-XPU is **exactly zero** across the entire output tensor.
- bf16-on-XPU contains **NaN**, and its non-NaN min/max (-0.0559/0.0557) lands almost exactly on the *weight tensor's own* min/max (-0.0559/0.0559) rather than anywhere near the correct output range — i.e. the surviving values look like they could be an artifact of the weights themselves leaking through, not a completed matrix multiply.
## Analysis
- Input and weights are both finite, well-conditioned (roughly [-3.3, 3.3] and [-0.056, 0.056] respectively) — nowhere near either fp16's (±65504) or bf16's (~±3.4e38) representable range limits, so this is not an overflow/saturation issue for either format.
- Changing only the dtype (fp16→fp32) while keeping device=XPU fixes it.
- Changing only the device (XPU→CPU) while keeping dtype=fp16 fixes it.
- bf16 on the same XPU device fails differently from fp16 (NaN instead of zero), but still fails — so this is not fp16-specific, it affects reduced-precision GEMM on this backend generally.
- The bf16 output's suspicious proximity to the weight tensor's own range (rather than to the correct result, or to random garbage) may indicate the accumulation/reduction step of the GEMM is not executing as expected on this backend, rather than a pure numerical precision issue.
- So the failure is specific to **reduced precision (fp16 or bf16) + XPU** for this operation (`nn.Linear` → `addmm`), not to any one dtype, and not to this GPU in fp32.
- Downstream impact (for context, not part of this report's scope): in Stable Diffusion XL, this exact shape (320→1280) is the model's timestep-embedding projection. A zeroed/NaN embedding here plausibly propagates into GroupNorm layers further into the UNet (dividing by a near-zero variance), which is consistent with the 100% NaN UNet output observed in the linked downstream report — and consistent with switching between fp16-unet and bf16-unet in that report producing the same end symptom (100% NaN) despite being different intermediate failure modes at this layer.
## Suggested next steps for maintainers
- Reproduce with plain `torch.addmm`/`torch.matmul` directly (bypassing `nn.Linear`) to rule out anything in the `nn.Linear` wrapper itself.
- Vary the two dimensions (320, 1280) independently to see if the zeroing/NaN is specific to these sizes or general to reduced-precision GEMM on this backend/architecture (Xe2/Battlemage specifically — would be useful to know if this also reproduces on Alchemist (Arc A-series) or Lunar Lake/Meteor Lake iGPUs).
- Check whether disabling any oneDNN/oneMKL kernel selection heuristic (e.g. forcing a non-XMX/non-tensor-core path) changes the result — if XMX (Intel's tensor-core-equivalent matrix engine) is involved in kernel selection for this shape/dtype combination, that would be a natural place to look first, especially given the bf16 output pattern noted above.
## Related
This was found while investigating a downstream ComfyUI bug where SDXL sampling on the same GPU produces a 100% NaN output latent, reproducible with both fp16-unet and bf16-unet — see [[Comfy-Org/ComfyUI#14743](https://github.com/Comfy-Org/ComfyUI/issues/14743)](https://github.com/Comfy-Org/ComfyUI/issues/14743) for the full symptom, environment, and elimination of every other variable (VAE, checkpoint, resolution, sampler, attention backend) down to this specific operation.
Possibly related: [[intel/torch-xpu-ops#1255](https://github.com/intel/torch-xpu-ops/issues/1255)](https://github.com/intel/torch-xpu-ops/issues/1255) reports fp16/bf16 accuracy failures specifically on BMG (Battlemage) across several Torchbench/Timm models — this report may be a minimal, isolated reproduction of the same underlying kernel issue.
## Where to file
Primary suggestion: **`intel/torch-xpu-ops`** (owns the XPU backend kernel implementations dispatched by PyTorch for ops like `addmm`/`linear`). Cross-reference or cross-post to **`pytorch/pytorch`** if `torch-xpu-ops` maintainers determine it belongs upstream instead (e.g. if it's a dispatch/registration issue rather than a kernel numerics issue).
Contributor guide
Research direction
Start by running the minimal reproduction on the Intel Arc B580 XPU, then reproduce the failure with plain torch.addmm or torch.matmul to separate the operation from the nn.Linear wrapper. Vary the 320 and 1280 dimensions and compare reduced-precision results with fp32 XPU and fp16 CPU baselines. Done means fp16 and bf16 outputs are finite and numerically close to the reference results.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend, machine-learning
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100