pytorch - 💡(How to fix) Fix `torch.compile` produces different uint8 quantization values for E8M0 `log2 → ceil → clamp → uint8` pipeline compared to eager mode [1 comments, 1 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#179570Fetched 2026-04-08 03:00:16
View on GitHub
Comments
1
Participants
1
Timeline
17
Reactions
0
Author
Participants
Timeline (top)
mentioned ×7subscribed ×7labeled ×2commented ×1

Error Message

Error logs

No error — both modes succeed, but produce different uint8 outputs:

Code Example

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

class E8M0LogQuantizer(nn.Module):
    """E8M0 quantization via log-domain: abs → log2 → ceil → clamp → uint8."""
    def __init__(self, eps=1e-7, bias=127):
        super().__init__()
        self.eps = eps
        self.bias = bias

    def forward(self, x):
        x_abs = torch.abs(x) + self.eps
        log2_val = torch.log2(x_abs)
        ceil_val = torch.ceil(log2_val)
        clamped = torch.clamp(ceil_val, min=-127, max=127)
        encoded = (clamped + self.bias).to(torch.uint8)
        return encoded


class LogDomainCNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        # Feature extraction
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(32)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(64)
        self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.bn3 = nn.BatchNorm2d(128)
        self.pool = nn.AdaptiveAvgPool2d(4)

        # E8M0 log-domain quantizer
        self.quantizer = E8M0LogQuantizer()

        # Classifier
        self.fc = nn.Linear(128 * 4 * 4, num_classes)

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.relu(self.bn3(self.conv3(x)))
        x = self.pool(x)

        # E8M0 log-domain quantization
        encoded = self.quantizer(x)

        # Classification from float features
        cls = x.flatten(1)
        logits = self.fc(cls)
        return logits, encoded


# NOTE: CPU — the log2→ceil pipeline bug reproduces on CPU
device = "cpu"
torch.manual_seed(42)
model = LogDomainCNN().to(device).eval()
# Positive-biased input (abs + offset) to ensure valid log2 range
x = torch.abs(torch.randn(4, 3, 64, 64, device=device)) + 0.1

# Eager: deterministic
with torch.no_grad():
    ref_logits, ref_enc = model(x)
    ref_logits2, ref_enc2 = model(x)
print(f"Eager deterministic (logits): {(ref_logits - ref_logits2).abs().max().item():.6e}")
print(f"Eager deterministic (encoded): {(ref_enc.int() - ref_enc2.int()).abs().max().item()}")

# Compiled
torch._dynamo.reset()
compiled = torch.compile(model, backend="inductor")
with torch.no_grad():
    comp_logits, comp_enc = compiled(x)

logit_diff = (ref_logits.float() - comp_logits.float()).abs()
enc_diff = (ref_enc.int() - comp_enc.int()).abs()
print(f"Logits max_diff: {logit_diff.max().item():.6e}")
print(f"Encoded uint8 mismatches: {(enc_diff > 0).sum().item()} / {ref_enc.numel()}")
print(f"Encoded max_diff: {enc_diff.max().item()}")

---

Eager deterministic (logits): 0.000000e+00
Eager deterministic (encoded): 0
Logits and encoded uint8 values differ systematically under torch.compile

---

PyTorch version: 2.12.0.dev20260327+cu126
Python: 3.10.12
OS: Ubuntu 22.04.5 LTS (WSL2)
GPU: NVIDIA GeForce RTX 3060 Laptop GPU
CUDA: 12.6
RAW_BUFFERClick to expand / collapse

🐛 Describe the bug

torch.compile with inductor backend produces numerically different uint8-encoded features for a CNN model that uses E8M0 log-domain quantization (torch.abs() + eps → torch.log2() → torch.ceil() → torch.clamp(-127, 127) → add bias 127 → to(uint8)). Eager mode is perfectly deterministic (max_var=0 across repeated runs), confirming this is a systematic computation difference introduced by Inductor, not GPU non-determinism.

The ceil(log2(x)) chain is extremely sensitive to tiny floating-point differences: when a value is near an integer boundary (e.g., log2(x) ≈ 3.99999 vs 4.00001), ceil rounds in opposite directions, producing uint8 values that differ by 1 per boundary crossing. Inductor may fuse or reorder the log2→ceil chain differently from eager, causing these boundary divergences.

Minimal reproducer

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

class E8M0LogQuantizer(nn.Module):
    """E8M0 quantization via log-domain: abs → log2 → ceil → clamp → uint8."""
    def __init__(self, eps=1e-7, bias=127):
        super().__init__()
        self.eps = eps
        self.bias = bias

    def forward(self, x):
        x_abs = torch.abs(x) + self.eps
        log2_val = torch.log2(x_abs)
        ceil_val = torch.ceil(log2_val)
        clamped = torch.clamp(ceil_val, min=-127, max=127)
        encoded = (clamped + self.bias).to(torch.uint8)
        return encoded


class LogDomainCNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        # Feature extraction
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(32)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(64)
        self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.bn3 = nn.BatchNorm2d(128)
        self.pool = nn.AdaptiveAvgPool2d(4)

        # E8M0 log-domain quantizer
        self.quantizer = E8M0LogQuantizer()

        # Classifier
        self.fc = nn.Linear(128 * 4 * 4, num_classes)

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.relu(self.bn3(self.conv3(x)))
        x = self.pool(x)

        # E8M0 log-domain quantization
        encoded = self.quantizer(x)

        # Classification from float features
        cls = x.flatten(1)
        logits = self.fc(cls)
        return logits, encoded


# NOTE: CPU — the log2→ceil pipeline bug reproduces on CPU
device = "cpu"
torch.manual_seed(42)
model = LogDomainCNN().to(device).eval()
# Positive-biased input (abs + offset) to ensure valid log2 range
x = torch.abs(torch.randn(4, 3, 64, 64, device=device)) + 0.1

# Eager: deterministic
with torch.no_grad():
    ref_logits, ref_enc = model(x)
    ref_logits2, ref_enc2 = model(x)
print(f"Eager deterministic (logits): {(ref_logits - ref_logits2).abs().max().item():.6e}")
print(f"Eager deterministic (encoded): {(ref_enc.int() - ref_enc2.int()).abs().max().item()}")

# Compiled
torch._dynamo.reset()
compiled = torch.compile(model, backend="inductor")
with torch.no_grad():
    comp_logits, comp_enc = compiled(x)

logit_diff = (ref_logits.float() - comp_logits.float()).abs()
enc_diff = (ref_enc.int() - comp_enc.int()).abs()
print(f"Logits max_diff: {logit_diff.max().item():.6e}")
print(f"Encoded uint8 mismatches: {(enc_diff > 0).sum().item()} / {ref_enc.numel()}")
print(f"Encoded max_diff: {enc_diff.max().item()}")

Behavior summary

ModeResultNotes
EagerReference outputPerfectly deterministic across runs (max_var=0)
torch.compile(backend="inductor")Different outputuint8 quantized values differ at log2/ceil boundaries

Notes

  • Eager mode is perfectly deterministic (max_var=0), ruling out non-determinism.
  • The bug reproduces on CPU — not GPU-specific.
  • The log2 → ceil pipeline acts as a precision amplifier: even ULP-level float differences near integer boundaries cause ceil to round in opposite directions, flipping uint8 values by ±1.
  • Multiple boundary crossings across a 128-channel feature map accumulate to larger max_diff values.
  • Inductor may optimize the ceil(log2(x)) chain or fuse it with upstream operations, changing the intermediate precision.

Error logs

No error — both modes succeed, but produce different uint8 outputs:

Eager deterministic (logits): 0.000000e+00
Eager deterministic (encoded): 0
Logits and encoded uint8 values differ systematically under torch.compile

Versions

PyTorch version: 2.12.0.dev20260327+cu126
Python: 3.10.12
OS: Ubuntu 22.04.5 LTS (WSL2)
GPU: NVIDIA GeForce RTX 3060 Laptop GPU
CUDA: 12.6

cc @chauhang @penguinwu @ezyang @msaroufim @bdhirsh @anijain2305

topic: fuzzer

extent analysis

TL;DR

The most likely fix for the systematic computation difference introduced by Inductor is to modify the torch.compile configuration or the model's quantization pipeline to maintain consistent intermediate precision.

Guidance

  • Investigate the torch.compile backend configuration to see if there are any options to control the optimization or fusion of the log2 → ceil pipeline.
  • Consider adding a custom implementation of the log2 → ceil pipeline that ensures consistent intermediate precision, potentially using fixed-point arithmetic or careful handling of boundary cases.
  • Verify that the issue persists across different hardware configurations, such as CPU and GPU, to determine if the problem is specific to a particular device or backend.
  • Review the PyTorch documentation and release notes to see if there are any known issues or changes related to the inductor backend and quantization pipelines.

Example

# Custom implementation of log2 → ceil pipeline with consistent intermediate precision
def custom_log2_ceil(x):
    # Use fixed-point arithmetic or careful handling of boundary cases
    # to ensure consistent intermediate precision
    log2_val = torch.log2(x)
    ceil_val = torch.ceil(log2_val)
    return ceil_val

class E8M0LogQuantizer(nn.Module):
    def forward(self, x):
        x_abs = torch.abs(x) + self.eps
        log2_val = custom_log2_ceil(x_abs)
        # ... rest of the quantization pipeline ...

Notes

The provided code snippet and issue description suggest that the problem is related to the optimization or fusion of the log2 → ceil pipeline by the inductor backend. However, without further information or experimentation, it is difficult to provide a definitive solution. The suggested custom implementation of the log2 → ceil pipeline is a potential workaround, but its effectiveness depends on the specific requirements and constraints of the project.

Recommendation

Apply a workaround, such as the custom implementation of the log2 → ceil pipeline, to maintain consistent intermediate precision and ensure deterministic output. This approach allows for more control over the quantization pipeline and can help mitigate the systematic computation difference introduced by Inductor.

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