pytorch - 💡(How to fix) Fix Switching from CPU float32 to CUDA float32 introduces NaN/Inf on valid float32 boundary inputs, causing catastrophic downstream prediction divergence [9 comments, 4 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
pytorch/pytorch#178059Fetched 2026-04-08 01:12:23
View on GitHub
Comments
9
Participants
4
Timeline
28
Reactions
0
Author
Timeline (top)
commented ×9mentioned ×7subscribed ×7labeled ×5

Root Cause

This behavior is critical because once NaN/Inf appears in intermediate layers, it propagates to downstream nodes and can have catastrophic impact on the model, ultimately causing inconsistent label predictions and severe reliability/safety consequences.

Fix Action

Fix / Workaround

CPU: Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 40 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 48 On-line CPU(s) list: 0-47 Vendor ID: GenuineIntel Model name: QEMU Virtual CPU version 2.5+ CPU family: 15 Model: 107 Thread(s) per core: 1 Core(s) per socket: 48 Socket(s): 1 Stepping: 1 BogoMIPS: 4190.15 Flags: fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx lm constant_tsc nopl xtopology cpuid tsc_known_freq pni ssse3 fma cx16 sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c hypervisor lahf_lm abm cpuid_fault pti bmi1 avx2 bmi2 avx512f avx512dq avx512cd avx512bw avx512vl Hypervisor vendor: KVM Virtualization type: full L1d cache: 1.5 MiB (48 instances) L1i cache: 1.5 MiB (48 instances) L2 cache: 192 MiB (48 instances) L3 cache: 16 MiB (1 instance) NUMA node(s): 1 NUMA node0 CPU(s): 0-47 Vulnerability Gather data sampling: Not affected Vulnerability Itlb multihit: KVM: Mitigation: VMX unsupported Vulnerability L1tf: Mitigation; PTE Inversion Vulnerability Mds: Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown Vulnerability Meltdown: Mitigation; PTI Vulnerability Mmio stale data: Unknown: No mitigations Vulnerability Reg file data sampling: Not affected Vulnerability Retbleed: Not affected Vulnerability Spec rstack overflow: Not affected Vulnerability Spec store bypass: Vulnerable Vulnerability Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization Vulnerability Spectre v2: Mitigation; Retpolines; STIBP disabled; RSB filling; PBRSB-eIBRS Not affected; BHI Retpoline Vulnerability Srbds: Not affected Vulnerability Tsx async abort: Not affected Vulnerability Vmscape: Not affected

Code Example

from pathlib import Path

import torch
import torch.nn as nn

MODEL_NAME = "SRGAN"
ITERATION = 48
ITER_INDEX = ITERATION - 1  # input files are 0-based -> iter_047.pt
BASE = Path(__file__).resolve().parent
WEIGHT_PATH = BASE / "sd.pt"
INPUT_PATH = BASE / "input.pt"


def load_conv1():
    sd = torch.load(str(WEIGHT_PATH), map_location="cpu")
    w = sd["weight"].float()
    b = sd["bias"].float()

    conv = nn.Conv2d(3, 64, kernel_size=9, padding=4, bias=True).eval()
    conv.weight.data.copy_(w)
    conv.bias.data.copy_(b)
    return conv


def run(device: str):
    x = torch.load(str(INPUT_PATH), map_location="cpu").float()
    print(f"input min={x.min().item()}, max={x.max().item()}")

    m = load_conv1().to(device)
    with torch.no_grad():
        y = m(x.to(device)).float()
    print(
        f"mode {device}+float32: has_nan={bool(torch.isnan(y).any())}, has_inf={bool(torch.isinf(y).any())}"
    )


if __name__ == "__main__":
    run("cpu")
    run("cuda")

---

input min=1.4951369295671402e+29, max=3.4028234663852886e+38
mode cpu+float32: has_nan=False, has_inf=False
input min=1.4951369295671402e+29, max=3.4028234663852886e+38
mode cuda+float32: has_nan=True, has_inf=True
RAW_BUFFERClick to expand / collapse

🐛 Describe the bug

I reproduced a severe numerical stability issue using a minimal script that runs the same first Conv2d layer with identical input and weights under two execution modes:

cpu+float32 cuda+float32 Observed output:

input min=1.4951369295671402e+29, max=3.4028234663852886e+38 mode cpu+float32: has_nan=False, has_inf=False mode cuda+float32: has_nan=True, has_inf=True After switching execution mode (CPU -> CUDA), NaN and Inf appear, even though the input values are large but still legal float32 values (the maximum is near FLT_MAX, not an invalid out-of-range value by itself).

PyTorch documentation mentions that edge-case large values may cause slight numerical differences across backends. However, this is far beyond a slight mismatch: the CUDA path produces non-finite values (NaN/Inf) while CPU remains finite on the same input and same weights.

This behavior is critical because once NaN/Inf appears in intermediate layers, it propagates to downstream nodes and can have catastrophic impact on the model, ultimately causing inconsistent label predictions and severe reliability/safety consequences.

Expected Behavior Switching execution mode between CPU float32 and CUDA float32 should not introduce non-finite values (NaN/Inf) for valid float32 inputs. Differences, if any, should remain within small numerical tolerance.

Actual Behavior CPU float32 path remains finite. CUDA float32 path produces NaN and Inf. Non-finite values propagate through downstream layers and can cause final prediction inconsistency.

from pathlib import Path

import torch
import torch.nn as nn

MODEL_NAME = "SRGAN"
ITERATION = 48
ITER_INDEX = ITERATION - 1  # input files are 0-based -> iter_047.pt
BASE = Path(__file__).resolve().parent
WEIGHT_PATH = BASE / "sd.pt"
INPUT_PATH = BASE / "input.pt"


def load_conv1():
    sd = torch.load(str(WEIGHT_PATH), map_location="cpu")
    w = sd["weight"].float()
    b = sd["bias"].float()

    conv = nn.Conv2d(3, 64, kernel_size=9, padding=4, bias=True).eval()
    conv.weight.data.copy_(w)
    conv.bias.data.copy_(b)
    return conv


def run(device: str):
    x = torch.load(str(INPUT_PATH), map_location="cpu").float()
    print(f"input min={x.min().item()}, max={x.max().item()}")

    m = load_conv1().to(device)
    with torch.no_grad():
        y = m(x.to(device)).float()
    print(
        f"mode {device}+float32: has_nan={bool(torch.isnan(y).any())}, has_inf={bool(torch.isinf(y).any())}"
    )


if __name__ == "__main__":
    run("cpu")
    run("cuda")
input min=1.4951369295671402e+29, max=3.4028234663852886e+38
mode cpu+float32: has_nan=False, has_inf=False
input min=1.4951369295671402e+29, max=3.4028234663852886e+38
mode cuda+float32: has_nan=True, has_inf=True

demo.zip

Versions

PyTorch version: 2.10.0+cu126 Is debug build: False CUDA used to build PyTorch: 12.6 ROCm used to build PyTorch: N/A

OS: Ubuntu 24.04.3 LTS (x86_64) GCC version: (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 Clang version: Could not collect CMake version: Could not collect Libc version: glibc-2.39

Python version: 3.10.19 | packaged by conda-forge | (main, Jan 26 2026, 23:45:08) [GCC 14.3.0] (64-bit runtime) Python platform: Linux-6.8.0-90-generic-x86_64-with-glibc2.39 Is CUDA available: True CUDA runtime version: 12.6.20 CUDA_MODULE_LOADING set to: GPU models and configuration: GPU 0: NVIDIA GeForce RTX 3090 GPU 1: NVIDIA GeForce RTX 3090

Nvidia driver version: 560.35.03 cuDNN version: Could not collect Is XPU available: False HIP runtime version: N/A MIOpen runtime version: N/A Is XNNPACK available: True Caching allocator config: N/A

CPU: Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 40 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 48 On-line CPU(s) list: 0-47 Vendor ID: GenuineIntel Model name: QEMU Virtual CPU version 2.5+ CPU family: 15 Model: 107 Thread(s) per core: 1 Core(s) per socket: 48 Socket(s): 1 Stepping: 1 BogoMIPS: 4190.15 Flags: fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx lm constant_tsc nopl xtopology cpuid tsc_known_freq pni ssse3 fma cx16 sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c hypervisor lahf_lm abm cpuid_fault pti bmi1 avx2 bmi2 avx512f avx512dq avx512cd avx512bw avx512vl Hypervisor vendor: KVM Virtualization type: full L1d cache: 1.5 MiB (48 instances) L1i cache: 1.5 MiB (48 instances) L2 cache: 192 MiB (48 instances) L3 cache: 16 MiB (1 instance) NUMA node(s): 1 NUMA node0 CPU(s): 0-47 Vulnerability Gather data sampling: Not affected Vulnerability Itlb multihit: KVM: Mitigation: VMX unsupported Vulnerability L1tf: Mitigation; PTE Inversion Vulnerability Mds: Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown Vulnerability Meltdown: Mitigation; PTI Vulnerability Mmio stale data: Unknown: No mitigations Vulnerability Reg file data sampling: Not affected Vulnerability Retbleed: Not affected Vulnerability Spec rstack overflow: Not affected Vulnerability Spec store bypass: Vulnerable Vulnerability Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization Vulnerability Spectre v2: Mitigation; Retpolines; STIBP disabled; RSB filling; PBRSB-eIBRS Not affected; BHI Retpoline Vulnerability Srbds: Not affected Vulnerability Tsx async abort: Not affected Vulnerability Vmscape: Not affected

Versions of relevant libraries: [pip3] numpy==1.26.4 [pip3] nvidia-cublas-cu12==12.6.4.1 [pip3] nvidia-cuda-cupti-cu12==12.6.80 [pip3] nvidia-cuda-nvrtc-cu12==12.6.77 [pip3] nvidia-cuda-runtime-cu12==12.6.77 [pip3] nvidia-cudnn-cu12==9.10.2.21 [pip3] nvidia-cufft-cu12==11.3.0.4 [pip3] nvidia-curand-cu12==10.3.7.77 [pip3] nvidia-cusolver-cu12==11.7.1.2 [pip3] nvidia-cusparse-cu12==12.5.4.2 [pip3] nvidia-cusparselt-cu12==0.7.1 [pip3] nvidia-nccl-cu12==2.27.5 [pip3] nvidia-nvjitlink-cu12==12.6.85 [pip3] nvidia-nvtx-cu12==12.6.77 [pip3] onnxruntime-gpu==1.23.2 [pip3] optree==0.18.0 [pip3] pytorch-triton==3.2.0+git4b3bb1f8 [pip3] torch==2.10.0+cu126 [pip3] torchaudio==2.11.0.dev20260127+cu126 [pip3] torchvision==0.25.0+cu126 [pip3] triton==3.6.0+git9844da95 [conda] numpy 1.26.4 pypi_0 pypi [conda] nvidia-cublas-cu12 12.6.4.1 pypi_0 pypi [conda] nvidia-cuda-cupti-cu12 12.6.80 pypi_0 pypi [conda] nvidia-cuda-nvrtc-cu12 12.6.77 pypi_0 pypi [conda] nvidia-cuda-runtime-cu12 12.6.77 pypi_0 pypi [conda] nvidia-cudnn-cu12 9.10.2.21 pypi_0 pypi [conda] nvidia-cufft-cu12 11.3.0.4 pypi_0 pypi [conda] nvidia-curand-cu12 10.3.7.77 pypi_0 pypi [conda] nvidia-cusolver-cu12 11.7.1.2 pypi_0 pypi [conda] nvidia-cusparse-cu12 12.5.4.2 pypi_0 pypi [conda] nvidia-cusparselt-cu12 0.7.1 pypi_0 pypi [conda] nvidia-nccl-cu12 2.27.5 pypi_0 pypi [conda] nvidia-nvjitlink-cu12 12.6.85 pypi_0 pypi [conda] nvidia-nvtx-cu12 12.6.77 pypi_0 pypi [conda] optree 0.18.0 pypi_0 pypi [conda] pytorch-triton 3.2.0+git4b3bb1f8 pypi_0 pypi [conda] torch 2.10.0+cu126 pypi_0 pypi [conda] torchaudio 2.11.0.dev20260127+cu126 pypi_0 pypi [conda] torchvision 0.25.0+cu126 pypi_0 pypi [conda] triton 3.6.0+git9844da95 pypi_0 pypi

cc @svekars @sekyondaMeta @AlannaBurke

extent analysis

Fix Plan

The issue is caused by numerical instability when running the Conv2d layer on CUDA with float32 inputs. To fix this, we can try the following steps:

  • Use half precision: Try running the model with half precision (float16) to reduce the numerical instability.
  • Use a different convolution implementation: Try using a different convolution implementation, such as torch.nn.functional.conv2d, to see if the issue is specific to the nn.Conv2d module.
  • Add input normalization: Try normalizing the input values to reduce the range of values and prevent overflow.

Here is an example of how to implement these fixes:

import torch
import torch.nn as nn
import torch.nn.functional as F

# ... (rest of the code remains the same)

def run(device: str):
    x = torch.load(str(INPUT_PATH), map_location="cpu").float()
    print(f"input min={x.min().item()}, max={x.max().item()}")

    # Use half precision
    x_half = x.half()
    m = load_conv1().to(device).half()
    with torch.no_grad():
        y_half = m(x_half.to(device))
    print(
        f"mode {device}+float16: has_nan={bool(torch.isnan(y_half).any())}, has_inf={bool(torch.isinf(y_half).any())}"
    )

    # Use a different convolution implementation
    m = load_conv1().to(device)
    with torch.no_grad():
        y_func = F.conv2d(x.to(device), m.weight, m.bias, padding=m.padding, stride=m.stride)
    print(
        f"mode {device}+float32 (functional): has_nan={bool(torch.isnan(y_func).any())}, has_inf={bool(torch.isinf(y_func).any())}"
    )

    # Add input normalization
    x_norm = x / x.max()
    m = load_conv1().to(device)
    with torch.no_grad():
        y_norm = m(x_norm.to(device))
    print(
        f"mode {device}+float32 (normalized): has_nan={bool(torch.isnan(y_norm).any())}, has_inf={bool(torch.isinf(y_norm).any())}"
    )

Verification

To verify that the fix worked, run the modified code and check the output for NaN and Inf values. If the issue is resolved, the output should not contain any NaN or Inf values.

Extra Tips

  • When working with large input values, it's essential to consider the numerical stability of the model and take steps to prevent overflow and underflow.
  • Using half precision or a different convolution implementation can help reduce numerical instability, but may also affect the model's accuracy.
  • Normalizing the input values can help prevent overflow and underflow, but may also affect the model's behavior.
  • It's crucial

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING