Skip to content

dynamo.compile: nn.Linear on 4D (B,H,W,C) + reshape gives wrong results; identical 3D path is correct #4379

Description

@robbysun

Bug description

torch_tensorrt.dynamo.compile produces silently wrong numerical results (~23% relative error) when nn.Linear is applied to a 4D tensor (B, H, W, C) and followed by a reshape that splits the channel dimension. The semantically identical computation using a pre-flattened 3D tensor (B, L, C) gives correct results (~0.1% error, just bf16 rounding).

This is a silent correctness bug — no error or warning is raised. We first encountered it while compiling a SAM3 ViT image encoder, where window-attention blocks receive (B*nWindows, ws, ws, C) input: ~50% relative error on features, with detections completely suppressed.

Minimal reproducer

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

class Attn4D(nn.Module):
    """QKV projection on 4D input — the broken pattern."""
    def __init__(self, C, nH):
        super().__init__()
        self.qkv = nn.Linear(C, C * 3)
        self.proj = nn.Linear(C, C)
        self.nH = nH

    def forward(self, x):                                  # (B, H, W, C)
        B, H, W, C = x.shape; L = H * W; dH = C // self.nH
        qkv = self.qkv(x).reshape(B, L, 3, self.nH, dH)  # ← linear on 4D
        q, k, v = qkv.permute(2, 0, 3, 1, 4).unbind(0)
        out = F.scaled_dot_product_attention(q, k, v)
        out = out.view(B, self.nH, H, W, -1).permute(0, 2, 3, 1, 4).reshape(B, H, W, C)
        return self.proj(out)

class Attn3D(nn.Module):
    """Identical math, but input flattened to 3D before linear."""
    def __init__(self, C, nH):
        super().__init__()
        self.qkv = nn.Linear(C, C * 3)
        self.proj = nn.Linear(C, C)
        self.nH = nH

    def forward(self, x):                                  # (B, H, W, C)
        B, H, W, C = x.shape; L = H * W; dH = C // self.nH
        qkv = self.qkv(x.reshape(B, L, C)).reshape(B, L, 3, self.nH, dH)  # ← flatten first
        q, k, v = qkv.permute(2, 0, 3, 1, 4).unbind(0)
        out = F.scaled_dot_product_attention(q, k, v)
        out = out.view(B, self.nH, L, -1).permute(0, 2, 1, 3).reshape(B, L, C)
        return self.proj(out).reshape(B, H, W, C)

B, H, W, C, nH = 9, 24, 24, 256, 8
device = torch.device("cuda")
torch.manual_seed(0)
x = torch.randn(B, H, W, C, device=device, dtype=torch.bfloat16)

m4 = Attn4D(C, nH).bfloat16().to(device).eval()
m3 = Attn3D(C, nH).bfloat16().to(device).eval()
# Share identical weights so PyTorch outputs are equal
m3.qkv.weight, m3.qkv.bias = m4.qkv.weight, m4.qkv.bias
m3.proj.weight, m3.proj.bias = m4.proj.weight, m4.proj.bias

with torch.no_grad():
    pt4 = m4(x)
    pt3 = m3(x)

print(f"PyTorch 4D vs 3D max-diff: {(pt4 - pt3).abs().max():.2e}")  # ~0

def compile_trt(model, x):
    with torch.no_grad():
        ep = torch.export.export(model, (x,))
    tep = torch_tensorrt.dynamo.compile(
        ep, inputs=[x],
        enabled_precisions={torch.bfloat16},
        use_explicit_typing=False,
        device=torch_tensorrt.Device(gpu_id=0),
    )
    torch_tensorrt.save(tep, "/tmp/repro.ep", inputs=[x])
    return torch_tensorrt.load("/tmp/repro.ep").module()

fn3 = compile_trt(m3, x)
fn4 = compile_trt(m4, x)

with torch.no_grad():
    trt3 = fn3(x)
    trt4 = fn4(x)

ref = pt4.float()
print(f"TRT 3D rel_err: {(trt3.float()-ref).abs().mean()/ref.abs().mean():.5f}")  # ~0.001
print(f"TRT 4D rel_err: {(trt4.float()-ref).abs().mean()/ref.abs().mean():.5f}")  # ~0.23 ← WRONG

Output on RTX 4080:

PyTorch 4D vs 3D max-diff: 0.00e+00
TRT 3D rel_err: 0.00117   ← correct (just bf16 rounding)
TRT 4D rel_err: 0.23456   ← wrong

Additional observations

All individual sub-operations are correct. When nn.Linear, reshape, permute, unbind, RoPE, scaled_dot_product_attention, and the output projection are each compiled and tested in isolation (with pre-computed PyTorch inputs), they all give accurate results (<0.5% error). The wrong result only appears when the full computation is compiled as a single TRT subgraph with a single output.

Returning intermediate tensors as outputs "fixes" it. If the compiled module returns (q, k, v, sdpa_out, final_out) instead of just final_out, all outputs including final_out are accurate. This suggests TRT applies an optimization (possibly a fused MHA kernel) when there is only one output, and that optimized path has a layout bug.

Optimization level makes no difference. Tested optimization_level=0, 1, 2 — all produce the same ~23% error on the 4D path.

Hypothesis. TRT may use NHWC-style internal memory layout for nn.Linear output when the input is a 4D tensor (B, H, W, C). The subsequent reshape(B, L, 3, nH, dH) then misinterprets the memory layout, scrambling the Q/K/V split. The 3D path avoids this because TRT uses standard row-major layout for 3D tensors.

Workaround

Flatten 4D input to 3D before nn.Linear, then reshape back after:

# Before: self.qkv(x)            where x is (B, H, W, C)
# After:  self.qkv(x.reshape(B, H*W, C)).reshape(B, H, W, C_out)

Environment

torch-tensorrt 2.10.0
torch 2.10.0+cu128
tensorrt 10.14.1.48.post1
CUDA 13.0
GPU NVIDIA GeForce RTX 4080
Python 3.12

Related

Discovered and diagnosed by @robbysun with assistance from Claude Code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions