TorchAO Static FP8 Quantization (Linear)#
This example calibrates a two-layer Linear model with TorchAO observers, then rewrites those layers so activations and weights are quantized to FP8 (e4m3). Export keeps explicit quantize_affine_float8_non_decomposed / dequantize_affine_float8_non_decomposed nodes, which Torch-TensorRT maps to IQuantizeLayer / IDequantizeLayer.
Contrast with quantize_linear_fp8_woq, which quantizes weights only and needs no calibration. Static FP8 can run the GEMM itself in FP8 (Tensor Cores) after Q/DQ fusion.
Graph after export (one Linear):
::
- BF16 act ──► Q ──► FP8 act ──► DQ ──┐
▼
- aten.linear
▲
FP8 weight ──► DQ ──────────────────┘
Requirements:
NVIDIA GPU with FP8 support (Hopper or newer)
torchaotorch-tensorrtwith the TorchAO float8_non_decomposed converters
Imports#
This example lives in examples/dynamo/torchao/. Move that directory off the front of sys.path so import torchao resolves the PyPI package instead of this folder.
[ ]:
import sys
from pathlib import Path
_EXAMPLE_DIR = str(Path(__file__).resolve().parent)
if sys.path and Path(sys.path[0]).resolve() == Path(_EXAMPLE_DIR):
sys.path.pop(0)
import torch
import torch_tensorrt as torchtrt
sys.path.insert(0, _EXAMPLE_DIR)
from static_fp8_utils import quantize_static_fp8
def sqnr(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
"""Signal-to-quantization-noise ratio in dB (higher is closer)."""
a = a.float().flatten()
b = b.float().flatten()
signal = torch.norm(a)
noise = torch.norm(a - b)
return 20 * torch.log10(signal / noise.clamp_min(1e-12))
Define a small two-layer Linear model#
[ ]:
class LinearModel(torch.nn.Module):
def __init__(self, in_features=256, hidden=512, out_features=128):
super().__init__()
self.linear1 = torch.nn.Linear(in_features, hidden, bias=False)
self.linear2 = torch.nn.Linear(hidden, out_features, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.linear2(self.linear1(x))
model = LinearModel().eval().to(dtype=torch.bfloat16, device="cuda")
example_input = torch.randn(32, 256, dtype=torch.bfloat16, device="cuda")
Calibrate, then insert static FP8 Q/DQ#
Observers record per-tensor activation min/max and per-channel weight min/max over a few forward passes. Those scales are baked into QuantizedLinearQDQ.
[ ]:
with torch.no_grad():
fp_out = model(example_input)
quantize_static_fp8(model, (example_input,), calibration_steps=10)
with torch.no_grad():
quant_out = model(example_input)
print(f"eager SQNR after static FP8: {sqnr(fp_out, quant_out):.2f} dB")
Export and compile#
The exported graph should contain quantize_affine_float8_non_decomposed (activations) and dequantize_affine_float8_non_decomposed (activations and weights).
[ ]:
exp_program = torch.export.export(model, (example_input,), strict=True)
exp_program.graph_module.print_readable()
trt_model = torchtrt.dynamo.compile(
exp_program,
inputs=[example_input],
min_block_size=1,
require_full_compilation=True,
)
with torch.no_grad():
trt_out = trt_model(example_input)
if isinstance(trt_out, (list, tuple)):
trt_out = trt_out[0]
print(f"TRT SQNR vs quantized eager: {sqnr(quant_out, trt_out):.2f} dB")
print(trt_out)