๐ŸŽ“ A1Learner
๐Ÿ”

PyTorch Quantization-Aware Training

Quantization-aware training trains a model with fake quantization inside its forward pass. Fake quantization rounds values to int8 steps and then expands them back to floats.

Rounding to a few levels

A coarse grid makes the rounding visible here. Five levels pull each weight to its nearest step.

import torch

def fake_quant(w, levels=5):
    scale = w.detach().abs().max() / (levels // 2)
    return torch.round(w / scale).clamp(-(levels // 2), levels // 2) * scale

w = torch.tensor([-0.30, 0.10, 0.55, 0.90])
print([round(v, 2) for v in fake_quant(w).tolist()])

Rounding alone blocks gradients

The round function has a gradient of zero almost everywhere. A loss built on rounded weights gives the weight a zero gradient.

import torch

w = torch.tensor([0.4], requires_grad=True)
y = torch.round(w * 127) / 127
loss = y.sum()
loss.backward()
print(w.grad.item())

The straight-through fix

The detach trick adds the rounding gap as a constant. The same loss now hands the weight a gradient of one.

import torch

w = torch.tensor([0.4], requires_grad=True)
rounded = torch.round(w * 127) / 127
fake = w + (rounded - w).detach()
loss = fake.sum()
loss.backward()
print(w.grad.item())

Example

import torch
import torch.nn.functional as F

def fake_quant(w):
    scale = (w.detach().abs().max() / 127.0).clamp(min=1e-8)
    rounded = torch.round(w / scale).clamp(-127, 127) * scale
    return w + (rounded - w).detach()

class FakeQuantLinear(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = torch.nn.Linear(4, 1)

    def forward(self, x):
        weight = fake_quant(self.linear.weight)
        return F.linear(x, weight, self.linear.bias)

torch.manual_seed(0)
x = torch.randn(60, 4)
y = (x[:, 0] * 2 - x[:, 1] + 0.5).reshape(-1, 1)

def train(make):
    torch.manual_seed(7)
    model = make()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.05)
    loss_fn = torch.nn.MSELoss()
    for step in range(120):
        optimizer.zero_grad()
        loss = loss_fn(model(x), y)
        loss.backward()
        optimizer.step()
    return model, loss.item()

plain, plain_loss = train(lambda: torch.nn.Linear(4, 1))
aware, aware_loss = train(FakeQuantLinear)
with torch.no_grad():
    gap = (plain(x) - aware(x)).abs().mean().item()
print(f"Plain final loss: {plain_loss:.4f}")
print(f"Fake-quant final loss: {aware_loss:.4f}")
print(f"Mean output gap: {gap:.4f}")

Key points

  • Scale: the gap between two neighbouring int8 levels for a tensor
  • Straight-through: gradients pass a rounding step as if nothing changed
  • Use w + (rounded - w).detach() to round in forward and keep gradients flowing
  • Only the weights are rounded in this small version, and the bias stays float
  • A model trained with rounding in the loop is ready for rounding later

In this example, two models fit the same seeded regression data. Both models start from the same seed, so their first weights match. The plain model is one Linear layer trained for 120 steps. The aware model wraps the same layer and rounds its weight on every forward pass. Its rounding uses 127 levels and the straight-through trick from the snippets. The closing lines print both final losses and the mean gap between their outputs. The fake-quant loss lands at 0.0003, and the mean output gap stays at 0.0179.

Note

Library QAT adds observers, which are helpers that watch value ranges in training

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Python Playground

Exercise

Why does fake quantization use the detach trick?