SSRQ: Structured Stochastic Rounding for
Quantization Aware Post Training on FP3
Most low-bit quantization stops at FP4 / INT4. SSRQ is the first to close the FP3–FP16 gap: packed FP3 is ~81% smaller than FP16 (\(5.33\times\)), one-shot PTQ collapses (71.3% on mlp9), and a short SSRQ post-training step recovers 94.2% — within ~1.5 pp of FP16 float (95.7%) — with a plain RTNE FP3 deploy checkpoint.
Deploy (RTNE) test accuracy on MNIST mlp9, seeds
161–164. One-shot PTQ-FP3 collapses (~71%). Scratch SRC recovers some ground.
Warm-starting from FP16 @ 5000 and continuing with SSRQ closes most of the
gap to FP16 float (~95.7%), beating post-FP16 SRC by ~2–4 pp across
\(R\in\{2,3,4,6,8\}\).
Why FP3 is a different regime
Compact formats are cheap to store and multiply, but many weights are not exactly representable on the lattice. Train in high precision and snap once, and you often lose accuracy. Train for that lattice — update wide, round back each step — and you can find better deploy weights. That is the setting of Graphcore’s stochastic-rounding work and ARITH 2025. Push the lattice one step coarser — to FP3 — and the story gets harder: PTQ variance explodes, and scratch SRC alone leaves a clear gap to FP16.
The storage win is real. Format alias fp3 =
p3109_k3p2sf is a 3-bit signed finite lattice
(7 values). Against an FP16 master (2 bytes/param), a pure packed FP3
dump is \(16/3 \approx 5.33\times\) smaller — about 81%
less weight storage — before counting a tiny per-tensor scale header:
| Model | #params | FP16 master | Pure FP3 (packed) | Saving vs FP16 |
|---|---|---|---|---|
mlp9 |
79,488 | 155.3 KiB | 29.1 KiB | 5.33× / 81.2% |
tf4 |
202,496 | 395.5 KiB | 74.2 KiB | 5.33× / 81.2% |
Sizes count every dim ≥ 1 parameter (same coverage as deploy
RTNE). FP3 column is bit-packed at 3 bits/param; a float32 scale per
tensor adds <0.3 KiB. Activations / optimizer state are not included.
On the same mlp9 probe (depth 9, hidden 64), after FP16 training
for 5000 steps:
| Deploy snap | Test acc (mean ± std) |
|---|---|
| FP16 · float | 95.69% ± 0.54 |
| PTQ fp8 · RTNE | 95.71% ± 0.45 |
| PTQ fp6 · RTNE | 95.70% ± 0.64 |
| PTQ fp4 · RTNE | 90.55% ± 1.52 |
| PTQ fp3 · RTNE | 71.29% ± 9.29 |
Same test split (seed 161) for every run.
FP8/FP6 PTQ is essentially free. FP4 already hurts. FP3 is a cliff: one-shot RTNE is not a deployable policy — you pay the 81% size cut, then lose most of the accuracy unless you train for that lattice. That is the setting where quantization-aware training with stochastic rounding matters.
SRC: centered stochastic rounding
Round-to-nearest-even (RTNE) is the obvious deploy snap, but during training it stalls on a coarse lattice: once updates fall below half a bin, the rounded weight stops moving. Stochastic rounding gets optimization unstuck: ideal SR rounds \(x\) up or down with probability given by the fractional part (\(\operatorname{SR}(x,u)=\lfloor x+u\rfloor\), \(u\sim U[0,1]\)), so \(\mathbb{E}_u[\operatorname{SR}(x,u)]=x\).
Hardware does not hand you infinite-precision \(u\). With an \(R\)-bit integer \(K\in\{0,\ldots,2^R-1\}\), the floor form \(\lfloor x+K/2^R\rfloor\) is biased for small \(R\). Graphcore’s fix is to center the comparator — call it SRC. On a weight matrix, ordinary SRC draws a fresh independent \(K_{ij}\) at every entry each step:
Optimizer math stays wide; the forward / backward see the rounded weights. At the end of training, drop the randomness and deploy with one RTNE snap — same footprint as PTQ.
Same story, more detail: Graphcore blog · ARITH 2025 · arith25-stochastic-rounding.
Motivation: post-train the lattice, not the network from scratch
On FP3, one-shot PTQ collapses and scratch QAT plateaus below FP16. Continuing from an FP16 master with a short quantization-aware post-training step recovers most of the gap — so the hard work is settling weights onto the lattice, not rediscovering the task under a brutal codebook. Stochastic rounding (SRC) already works in that post-training role, but leaves a few points on the table.
The mental model is where the noise comes from:
- SRC is pure stochastic. Each draw of \(K\) is fresh randomness — no data-driven factor, no memory of which round-up / round-down choices helped the loss.
- SRC is element-wise IID. Entries do not share structure: nothing is learned jointly across rows, columns, or layers. The only coupling is through the weight update itself.
So SSRQ keeps Graphcore’s SRC rounder unchanged and only changes how \(K\) is proposed — used as a short continuation after the high-precision run:
- Train masters in FP16 (here 5000 steps).
- Load those masters; run a short QAT with SSRQ (or SRC) onto FP3.
- Discard train-time noise params \(a,b\) and \(\xi\); deploy with one RTNE snap.
Inference footprint matches PTQ / SRC: ordinary FP3 weights, no learned dither, no lookup table. The only change is which FP3 points the masters settle on before the final snap.
SSRQ: same rounder, a structured draw of \(K\)
Structured Stochastic Rounding (SSRQ) does not invent a new FP3 cast. It keeps \(\operatorname{SRC}(\,\cdot\,;K)\) exactly. The only change is how \(K\) is proposed: mix fresh Uniform noise \(\xi\) with a learned row–column preference, then feed that \(K\) into the unchanged Graphcore cast:
For each weight matrix \(W\in\mathbb{R}^{m\times n}\), the vectors \(a\in\mathbb{R}^{m}\) and \(b\in\mathbb{R}^{n}\) are learnable logits — one scalar per row and per column — trained only during QAT (not part of deploy). Entry \((i,j)\) shares structure through the rank-1 field \(a_i+b_j\): rows with large \(a_i\) (and columns with large \(b_j\)) get a systematically different rounding bias. That is cheap: \(m+n\) extras instead of \(mn\). \(\sigma\) turns those logits into a rounding preference in \((0,1)\): near \(0\) or \(1\) pushes the dither toward rounding down or up; at init \(a=b=\mathbf{0}\) gives \(\sigma(0)=\tfrac12\) (no preference). Defaults \(\alpha=\beta=0.5\); \(\xi\) is redrawn each step. Forward / backward see the rounded weights; the STE is identity. At deploy, drop \(a\), \(b\), and \(\xi\), and snap with \(W^{\mathrm{deploy}}=\operatorname{RTNE}(\operatorname{scaled}(W))\).
Fake-quant (this work) vs inplace cast
Graphcore’s SR story is often told in an inplace training setting:
cast weights into the target format as you go, on hardware that can round
in the datapath. This write-up uses the usual ML-engineering cousin —
STE fake quantization: keep a wide master \(W\) (FP32/FP16),
build a quantized view \(W^q\) each step via
functional_call, and send gradients back to the master. Deploy
is then one RTNE snap of that master onto FP3. Same rounding math; different
bookkeeping — and it runs on ordinary PyTorch without custom inplace
kernels.
Scope of the quantizer during QAT: weights only. Activations
stay in full precision (FP16/FP32 matmuls). In the common weight/activation
bit-width shorthand, training/deploy here is
W3A16 — 3-bit FP3 weights, wide activations.
We also post-hoc evaluate the same RTNE FP3 weights under
W3A8
(fake-quant activations to INT8 or FP8 at every Linear/Conv input)
— see W3A8. That is an inference stress test,
not activation QAT.
Mid-layer (5.weight, 64×64) during SSRQ post-FP16 QAT
(\(R=8\), \(\alpha=\beta=0.5\), seed 161). Top: float master weights —
almost unchanged from step 1→2000 (the FP16 basin is already good).
Bottom: structured preference \(\sigma(a_i+b_j)\) — starts flat (~0.5),
then grows a separable row/column grid that biases which FP3 neighbors
get chosen. At deploy, \(a,b\) are discarded; only the settled weights
remain.
Scratch SRC on FP3
Train from random init with IID SRC onto FP3, then RTNE-deploy. That already beats PTQ (~71%), but on this seed grid it plateaus around the high 80s to low 90s — still several points short of FP16 float (~95.7%).
| Scratch SRC · FP3 · RTNE | @ 2000 | @ 5000 |
|---|---|---|
| \(R=2\) | 83.14 ± 5.13 | 90.12 ± 1.77 |
| \(R=3\) | 85.56 ± 2.59 | 84.84 ± 5.23 |
| \(R=4\) | 88.80 ± 3.06 | 90.25 ± 1.25 |
| \(R=6\) | 86.49 ± 1.50 | 88.14 ± 4.41 |
| \(R=8\) | 87.78 ± 1.86 | 90.44 ± 0.98 |
That is why SSRQ prefers a warm start: the FP16 run already found a good basin; the short post-QAT only has to settle those weights onto FP3, not rediscover the task under a brutal lattice.
Post-FP16 results · mlp9
Load FP16 masters @ 5000, continue QAT on FP3 for 1000 or 2000 steps.
Report deploy RTNE accuracy (mean ± std over 4 seeds).
All cells below are W3A16: FP3 weights,
float activations.
| FP16 @ 5000 | \(R\) | W3A16 · FP3 weights · float acts |
|||
|---|---|---|---|---|---|
| + SRC @ 1000 | + SRC @ 2000 | + SSRQ @ 1000 | + SSRQ @ 2000 | ||
| 95.69% ± 0.54 (n=4) |
2 | 89.15 ± 2.05 | 90.10 ± 0.92 | 93.06 ± 0.39 | 93.70 ± 0.63 |
| 3 | 89.59 ± 0.69 | 91.46 ± 0.61 | 93.20 ± 0.69 | 93.54 ± 0.72 | |
| 4 | 89.75 ± 0.77 | 91.75 ± 0.54 | 93.14 ± 0.79 | 93.66 ± 0.63 | |
| 6 | 89.51 ± 2.02 | 91.99 ± 0.63 | 93.50 ± 0.55 | 93.94 ± 0.89 | |
| 8 | 89.42 ± 1.30 | 92.00 ± 0.31 | 93.38 ± 0.38 | 94.16 ± 0.40 | |
- SSRQ beats SRC at every \((R, \text{budget})\) pair in this grid.
- Even \(R=2\) SSRQ post-QAT (~93.7% @ 2000) beats long scratch SRC (~90%).
- Gap to FP16 shrinks to ~1.5 pp at best; gap to PTQ-FP3 is ~+23 pp.
- Extra train storage for \(a,b\) is ~2% of weights on
mlp9; deploy drops them.
Ablation · \(\alpha\) / \(\beta\) at \(R=4\)
Recall the dither mix \(r_{ij}=(\alpha\,\xi_{ij}+\beta\,\sigma(a_i+b_j))\bmod 1\). Default is \(\alpha=\beta=0.5\). Same post-FP16 recipe (\(R=4\), 2000 steps, seeds 161–164), turn one term off on both probes:
| SSRQ mix | Meaning | mlp9 |
tf4 |
|---|---|---|---|
| \(\alpha=0.5\), \(\beta=0.5\) | default mix | 93.66 ± 0.63 | 85.21 ± 1.92 |
| \(\alpha=0\), \(\beta=1\) | structure only (no IID \(\xi\)) | 63.49 ± 6.23 | 70.59 ± 7.34 |
| \(\alpha=1\), \(\beta=0\) | noise only (no \(\sigma(a+b)\)) | 91.05 ± 1.48 | 76.80 ± 8.26 |
| SRC post-FP16 (IID control) | 91.75 ± 0.54 | 74.09 ± 7.31 | |
- Structure alone hurts on both models — without fresh \(\xi\), deploy
drops hard (
mlp9~−30 pp;tf4~−15 pp). - Noise alone (\(\alpha=1,\beta=0\)) lands near SRC on both probes.
- The default mix wins on both: IID dither and learned preference are both needed.
Post-FP16 results · tf4
Same recipe on MNIST tf4 (4-layer transformer): FP16 masters @
5000, then SRC/SSRQ post-QAT onto FP3 for 1000 or 2000 steps. Seeds 161–164.
No scratch-SRC grid on this probe — chart is PTQ vs post-FP16 only.
Deploy (RTNE) test accuracy on MNIST tf4. PTQ-FP3 collapses
(~54%). Post-FP16 SSRQ recovers to the mid-80s across
\(R\in\{2,3,4,6,8\}\), beating post-FP16 SRC by ~10–15 pp and closing
most of the gap to FP16 float (~88.2%).
| Deploy snap | Test acc (mean ± std) |
|---|---|
| FP16 · float | 88.16% ± 0.43 |
| PTQ fp8 · RTNE | 88.01% ± 0.39 |
| PTQ fp6 · RTNE | 87.83% ± 0.46 |
| PTQ fp4 · RTNE | 85.64% ± 0.40 |
| PTQ fp3 · RTNE | 54.11% ± 6.66 |
Post-QAT cells below are W3A16: FP3 weights,
float activations.
| FP16 @ 5000 | \(R\) | W3A16 · FP3 weights · float acts |
|||
|---|---|---|---|---|---|
| + SRC @ 1000 | + SRC @ 2000 | + SSRQ @ 1000 | + SSRQ @ 2000 | ||
| 88.16% ± 0.43 (n=4) |
2 | 71.30 ± 9.97 | 73.03 ± 3.12 | 84.71 ± 3.02 | 85.75 ± 1.50 |
| 3 | 68.04 ± 15.59 | 74.96 ± 5.32 | 84.78 ± 1.28 | 85.38 ± 1.51 | |
| 4 | 74.99 ± 6.90 | 74.09 ± 7.31 | 84.22 ± 2.06 | 85.21 ± 1.92 | |
| 6 | 71.88 ± 7.37 | 71.61 ± 3.83 | 84.53 ± 1.10 | 84.97 ± 2.74 | |
| 8 | 69.95 ± 16.29 | 74.74 ± 5.26 | 85.38 ± 1.65 | 85.60 ± 1.44 | |
- Best cell: SSRQ post-FP16, \(R=2\), 2000 steps → 85.75% ± 1.50 vs PTQ-FP3 54.11% and FP16 88.16% (~+31.6 pp over PTQ; ~2.4 pp shy of FP16).
- SSRQ beats SRC at every \((R, \text{budget})\) pair; SRC is high-variance on this grid.
- SSRQ is flatter in \(R\) than on
mlp9— even \(R=2\) is competitive.
Post-hoc W3A8 · FP3 weights, 8-bit activations
Take the SSRQ post-FP16 @ 2000 RTNE masters (weight-only QAT,
W3A16 train) and re-evaluate with fake-quantized activations on
every Linear/Conv input: per-tensor symmetric INT8, or RTNE
FP8.
No activation training — inference stress test only. Full MNIST test set
(10k); mean ± std over seeds 161–164.
W3A8 · mlp9 · SSRQ post-FP16 @ 2000
| \(R\) | W3A16FP3 W · float A |
W3A8 · INT8 AFP3 W · INT8 acts |
W3A8 · FP8 AFP3 W · FP8 acts |
Δ INT8 pp vs W3A16 |
Δ FP8 pp vs W3A16 |
|---|---|---|---|---|---|
| 2 | 93.96 ± 0.24 | 93.89 ± 0.29 | 93.93 ± 0.24 | −0.07 | −0.04 |
| 3 | 93.69 ± 0.74 | 93.51 ± 0.86 | 93.66 ± 0.72 | −0.18 | −0.03 |
| 4 | 93.86 ± 0.36 | 93.34 ± 0.67 | 93.85 ± 0.37 | −0.53 | −0.01 |
| 6 | 94.08 ± 0.62 | 93.99 ± 0.74 | 94.05 ± 0.65 | −0.08 | −0.03 |
| 8 | 94.18 ± 0.40 | 94.02 ± 0.65 | 94.20 ± 0.43 | −0.16 | +0.02 |
W3A8 · tf4 · SSRQ post-FP16 @ 2000
| \(R\) | W3A16FP3 W · float A |
W3A8 · INT8 AFP3 W · INT8 acts |
W3A8 · FP8 AFP3 W · FP8 acts |
Δ INT8 pp vs W3A16 |
Δ FP8 pp vs W3A16 |
|---|---|---|---|---|---|
| 2 | 87.17 ± 1.43 | 87.14 ± 1.42 | 87.07 ± 1.54 | −0.03 | −0.10 |
| 3 | 86.67 ± 1.63 | 86.62 ± 1.60 | 86.66 ± 1.70 | −0.05 | −0.01 |
| 4 | 86.70 ± 1.93 | 86.69 ± 1.91 | 86.71 ± 1.86 | −0.01 | +0.01 |
| 6 | 86.37 ± 2.24 | 86.36 ± 2.25 | 86.32 ± 2.20 | −0.02 | −0.05 |
| 8 | 86.89 ± 1.32 | 86.87 ± 1.33 | 86.86 ± 1.32 | −0.02 | −0.03 |
W3A8barely moves the needle: typically <0.2 pp vsW3A16(worst cell ~0.5 pp on mlp9 \(R=4\) INT8).- INT8 and FP8 tracks agree closely on both
mlp9andtf4— 8-bit acts are essentially free on these SSRQ FP3 checkpoints.
Conclusion
Ultra-narrow formats buy storage, but one-shot PTQ assumes a float solution
already near a good lattice point — on FP3 that fails. Stochastic rounding
keeps weights moving between bins; SSRQ adds the missing piece by drawing
the dither with a cheap shared preference instead of IID noise, so training
can bias neighbor choice without changing the deploy cast. An \(\alpha/\beta\)
ablation at \(R=4\) on both mlp9 and tf4
confirms both pieces matter: structure alone collapses,
noise alone ≈ SRC, and the default mix wins. The workable
recipe is post-train the lattice: warm-start from a wide-precision
basin, settle with a short structured QAT, then snap. Rounding structure is
train-only; deploy stays a plain deterministic checkpoint. Post-hoc
W3A8 is nearly free on these checkpoints — 8-bit acts do not
undo the FP3-weight recovery. Natural next steps are richer shared rounding
preferences, joint weight–activation QAT when narrower acts are a real
target, and the same structured-dither idea on larger models or other
ultra-narrow formats — including hardware paths that cast inplace.
Cite
If you use this writeup, please cite it as a blog post. Also listed on Google Scholar.
@misc{yang2026ssrq,
title = {SSRQ: Structured Stochastic Rounding for Quantization Aware Post Training on FP3},
author = {Yang, Tsun-Yi},
year = {2026},
month = {7},
howpublished = {Blog post},
url = {https://shamangary.github.io/post/ssrq-fp3/}
}