pytorch - 💡(How to fix) Fix [Inductor] `F.hardtanh` compiled backward silently drops gradient at bf16 boundary values

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…

Root Cause

The hardtanh_backward decomposition in torch/_decomp/decompositions.py (line 212):

@register_decomposition(aten.hardtanh_backward)
@out_wrapper("grad_input")
def hardtanh_backward(grad_output, self, min_val, max_val):
    return torch.where((self <= min_val) | (self >= max_val), 0.0, grad_output)

The issue is a semantic mismatch between the decomposition and the eager CUDA kernel at the dtype boundary:

For x = bf16(0.7) = 0.69921875, max_val = 0.7:

ComparisonResultGradient
Eager (CUDA kernel)bf16(x) < float(0.7)0.6992 < 0.7within bounds1.0
Compiled (decomposition)bf16(x) >= bf16(0.7)0.6992 >= 0.6992at boundary0.0

The eager CUDA kernel compares the bf16 input against the original float scalar, while the compiled decomposition comparison effectively uses bf16 precision for the bound, causing boundary values to be misclassified as "clamped".

Code Example

import torch
import torch.nn.functional as F

# bf16(0.7) = 0.69921875 (bf16 rounds 0.7 down)
x = torch.tensor([0.69921875], dtype=torch.bfloat16, device='cuda', requires_grad=True)

# Eager: gradient passes through (value is within bounds)
F.hardtanh(x, min_val=-0.7, max_val=0.7).backward()
print(f"eager grad:    {x.grad}")  # tensor([1.], ...) ← correct

# Compiled: gradient is zeroed
x2 = torch.tensor([0.69921875], dtype=torch.bfloat16, device='cuda', requires_grad=True)
torch._dynamo.reset()
torch.compile(lambda y: F.hardtanh(y, min_val=-0.7, max_val=0.7))(x2).backward()
print(f"compiled grad: {x2.grad}")  # tensor([0.], ...)WRONG

---

@register_decomposition(aten.hardtanh_backward)
@out_wrapper("grad_input")
def hardtanh_backward(grad_output, self, min_val, max_val):
    return torch.where((self <= min_val) | (self >= max_val), 0.0, grad_output)
RAW_BUFFERClick to expand / collapse

🐛 Describe the bug

🐛 Describe the bug

torch.compile(F.hardtanh) backward pass produces incorrect gradients for bfloat16 tensors when the input value equals the bf16 representation of the bound. The compiled backward zeros the gradient where eager correctly passes it through.

This can silently corrupt training when using hardtanh activation with bf16 (e.g., AMP or bf16 training). Roughly half of all non-exact bound values are affected.

Minimal reproducer

import torch
import torch.nn.functional as F

# bf16(0.7) = 0.69921875 (bf16 rounds 0.7 down)
x = torch.tensor([0.69921875], dtype=torch.bfloat16, device='cuda', requires_grad=True)

# Eager: gradient passes through (value is within bounds)
F.hardtanh(x, min_val=-0.7, max_val=0.7).backward()
print(f"eager grad:    {x.grad}")  # tensor([1.], ...) ← correct

# Compiled: gradient is zeroed
x2 = torch.tensor([0.69921875], dtype=torch.bfloat16, device='cuda', requires_grad=True)
torch._dynamo.reset()
torch.compile(lambda y: F.hardtanh(y, min_val=-0.7, max_val=0.7))(x2).backward()
print(f"compiled grad: {x2.grad}")  # tensor([0.], ...) ← WRONG

Expected: x2.grad = tensor([1.]) (same as eager — value is within the hardtanh bounds).
Actual: x2.grad = tensor([0.]) (gradient incorrectly dropped).

fp32 is NOT affected — both eager and compiled give grad=1.0 for the same input value.

Scope

This is not a single-value edge case. Testing all bound values in [0.01, 9.99] with step 0.01:

  • 477 out of ~930 non-exact bound values trigger the bug (any bound whose bf16 representation rounds DOWN)
  • Both upper and lower boundaries are affected
  • torch.clamp backward is NOT affected (different codepath)
  • relu6 is NOT affected (bounds 0 and 6 are exactly representable)

Root cause

The hardtanh_backward decomposition in torch/_decomp/decompositions.py (line 212):

@register_decomposition(aten.hardtanh_backward)
@out_wrapper("grad_input")
def hardtanh_backward(grad_output, self, min_val, max_val):
    return torch.where((self <= min_val) | (self >= max_val), 0.0, grad_output)

The issue is a semantic mismatch between the decomposition and the eager CUDA kernel at the dtype boundary:

For x = bf16(0.7) = 0.69921875, max_val = 0.7:

ComparisonResultGradient
Eager (CUDA kernel)bf16(x) < float(0.7)0.6992 < 0.7within bounds1.0
Compiled (decomposition)bf16(x) >= bf16(0.7)0.6992 >= 0.6992at boundary0.0

The eager CUDA kernel compares the bf16 input against the original float scalar, while the compiled decomposition comparison effectively uses bf16 precision for the bound, causing boundary values to be misclassified as "clamped".

Versions

Versions

  • PyTorch: 2.13.0.dev20260521+cu126
  • GPU: NVIDIA RTX A6000 (sm_86)
  • CUDA: 12.6 (bundled)
  • Python: 3.11
  • OS: Linux

cc @chauhang @penguinwu

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