Cai, Yao, Dong, Gholami, Mahoney & Keutzer (Berkeley / Peking U.), 2020

ZeroQ: Mixed-Precision Quantization
With No Data At All

Compress a trained network to 4-bit weights without ever touching the training set — by reconstructing synthetic "Distilled Data" from the batch-norm statistics baked into the model, then using it to find the optimal per-layer bit-width on a Pareto frontier. End to end in under 30 seconds, no fine-tuning.

Prerequisites: Quantization basics + Batch normalization + KL divergence
10
Chapters
6
Simulations

Chapter 0: The Problem

You trained a beautiful ResNet50 on a hospital's medical-imaging dataset. It works. Now you need it on a phone in the radiology ward: smaller, faster, lower power. The obvious move is quantization — store the weights as 4- or 8-bit integers instead of 32-bit floats. Smaller model, faster integer ALUs, less memory traffic (which is where most of the energy goes).

But there is a catch that nobody tells you about until you try it. Quantizing a 32-bit model down to low precision wrecks its accuracy. The standard fix is quantization-aware fine-tuning: retrain for a few epochs so the weights adjust to the coarse grid. To do that, you need the training data.

The wall: The training data is medical scans. It is governed by privacy law. It cannot leave the hospital, it cannot go to the cloud, and the vendor who built the model never gets to see it. So fine-tuning is off the table. Worse — many "post-training" methods still need at least a handful of real samples just to measure activation ranges. In a true MLaaS setting (you ship a model, the user's data stays theirs), you have zero samples.

What "zero-shot" really means here

Read the constraint precisely, because it shapes everything:

That last point is the loophole the paper exploits. A trained model is not just weights — it carries batch-normalization statistics: the running mean and variance of every layer's activations, computed over the real training data and frozen into the model. That is a fingerprint of the data, sitting right there in the file.

Why two things break without data

Quantization needs data for two distinct jobs, and ZeroQ has to solve both:

JobWhat it needs data forWhat breaks without it
Clipping range [a,b]Find the min/max of each layer's activations so you know what range to map onto the integer gridGuess the range wrong → clip useful values or waste bits on empty range
Sensitivity Ωi(k)Measure how much accuracy each layer loses at each bit-width, to decide which layers to keep high-precisionNo way to rank layers → can't do smart mixed-precision

The naïve escape — feed in random Gaussian noise to estimate ranges and sensitivities — sounds reasonable and fails badly. We will see it fail in Chapter 4. Gaussian noise produces the wrong ordering of which layers are sensitive, which means it picks the wrong layers to compress.

Why can't we just use standard quantization-aware fine-tuning in the settings ZeroQ targets?

Chapter 1: The Key Insight

Here is the whole paper in one sentence. The model already contains a compressed description of its training data — in its batch-norm statistics — so we can synthesize fake images that "look like" the real data to the network, and use those fake images to do everything quantization needs.

Every batch-norm layer stored a running mean μi and variance σi2 of its input activations, averaged over the real training set during training. Those numbers say: "when real data flows through me, layer i's activations have this mean and this spread." That is a statistical fingerprint of the data, frozen into the weights.

The insight: Start from random noise and optimize the pixels (not the weights) so that when the noise is pushed through the frozen network, the activation statistics at every layer match the stored batch-norm mean and variance. The result — Distilled Data — is synthetic input that the network "experiences" as if it were real training data.

Three moves, in order

1 · Distill
Optimize a small batch of synthetic images so their per-layer activation mean/variance match the model's stored BN statistics. (Ch 3)
2 · Probe sensitivity
Feed Distilled Data through the model; for each layer and each candidate bit-width, measure the KL-divergence between full-precision and quantized outputs. That is Ωi(k). (Ch 4)
3 · Pick bits
Choose a per-layer bit-width assignment that minimizes total sensitivity subject to a model-size budget — read off a Pareto frontier. (Ch 5)

What flows where

Notice what is being optimized at each stage, because it is easy to confuse:

The entire procedure for ResNet50 takes under 30 seconds on an 8-GPU box: ~3s to distill data, ~12s to measure sensitivity across all layers and bit-widths, ~14s to run the Pareto search. That is 0.5% of a single training epoch. We build each stage from scratch next.

What does ZeroQ optimize when generating Distilled Data?

Chapter 2: Quantization, Concretely

Before the clever part, let's be precise about what "quantize a tensor to k bits" actually means, because the rest of the paper rests on it. ZeroQ uses asymmetric uniform quantization. "Uniform" means the integer levels are evenly spaced; "asymmetric" means the range need not be centered at zero.

Take any tensor (a weight matrix or a layer's activations). First clip it to a range [a, b]. Then chop that range into 2k−1 equal intervals of width

Δ = (b − a) / (2k − 1)

Each real value v maps to the nearest grid point, stored as an unsigned integer in [0, 2k−1]:

q = round( (clip(v, a, b) − a) / Δ ),   v̂ = a + q·Δ

Here v is the original 32-bit value, a, b are the clipping bounds, k is the bit-width (e.g. 4), Δ is the step size (the gap between adjacent representable values), q is the stored integer, and is the dequantized approximation. The error |v − v̂| is at most Δ/2.

Why this matters for ZeroQ: The clipping range [a, b] for activations can only be known if you watch real activations flow. That is precisely the thing you have no data for. Distilled Data solves it — push synthetic data through, watch its activation ranges, use those as [a, b].

Worked example: 4-bit weights

Suppose a weight tensor lives in [a, b] = [−0.8, 1.2] and we quantize to k = 4 bits. Then there are 24−1 = 15 intervals:

Δ = (1.2 − (−0.8)) / 15 = 2.0 / 15 = 0.1333

A weight v = 0.37 maps to q = round((0.37 + 0.8)/0.1333) = round(8.78) = 9, which dequantizes to v̂ = −0.8 + 9·0.1333 = 0.40. The error is |0.37 − 0.40| = 0.03 — under Δ/2 = 0.067. The whole tensor now needs 4 bits/value instead of 32: an shrink.

Drag the bit-width below and watch a smooth signal get snapped onto the grid. Notice how few levels 2-bit gives you, and how the error explodes — this is exactly why some layers cannot go that low.

Uniform quantization of a signalInteractive

The smooth orange curve is the original signal. The teal staircase is its k-bit quantization. Watch the step count and the error band.

k=4 → 15 levels, Δ=0.133
For asymmetric uniform quantization to k bits over range [a,b], what is the step size Δ?

Chapter 3: Distilled Data

This is the heart of the paper. We need synthetic input that the network treats as if it were real training data — without ever seeing real data. The signal we exploit is sitting in every batch-norm layer.

What batch norm secretly stored

During training, the i-th BN layer watched activations zi (the output of conv layer i) and kept a running average of their mean and variance. After training, the model file contains, for every BN layer:

These were computed from millions of real images. They are a low-dimensional summary of "what the data looks like at every depth of the network."

The distillation objective

We start with a batch of random noise xr (drawn from a standard Gaussian) and treat the pixels themselves as the variables to optimize. We push xr through the frozen network and read off the actual mean μ̃ir and std σ̃ir of the activations our noise produces at each layer. Then we minimize the gap between those and the stored values:

minxr   ∑i=0..L ‖ μ̃ir − μi22  +  ‖ σ̃ir − σi22

Symbol by symbol: xr is the reconstructed (distilled) image batch we are solving for; μ̃ir, σ̃ir are the mean/std of xr's activations measured at layer i right now in the forward pass; μi, σi are the BN stats stored in the model; L is the number of BN layers. The i=0 term pins the input itself to zero mean, unit variance (standard preprocessing).

The gradient flows backward into the image. The weights never change. We backprop the statistic-matching loss all the way to the pixels and take gradient steps on xr. After ~hundreds of iterations, the noise reorganizes into something with real local structure — edges, textures — because that is what makes the deep-layer statistics line up. This is the same machinery as "deep dream" / feature visualization, repurposed for calibration.

Algorithm 1, in plain steps

StepOperationTensor shapes
0xr ← Gaussian noise[N, 3, 224, 224] for ImageNet (N≈32)
1Read μi, σi from each BN layereach [Ci] (per-channel)
2Forward xr, gather intermediate activationsactivation i: [N, Ci, Hi, Wi]
3Compute μ̃ir, σ̃ir over N,H,W dimseach [Ci]
4Loss = Eq. 3; backprop to xr; update xrgradient ∂L/∂xr has xr's shape
5Repeat 2–4 for a few hundred iterations

Below: watch noise turn into structure as the statistic-matching loss falls. Each "iteration" nudges the synthetic batch's activation statistics toward the stored BN targets. The loss curve is the ‖μ̃−μ‖2+‖σ̃−σ‖2 sum dropping.

Distilling data from BN statisticsInteractive

Left: the synthetic batch's per-layer activation stats (bars) converging onto the stored BN targets (dashed). Right: the matching loss over iterations. Press Step to run a few iterations; Distill to run to convergence.

iter 0 · loss —
In Distilled Data generation, what is held fixed and what is updated?

Chapter 4: Sensitivity — Which Layers Can Take the Hit?

Now we have synthetic data that the network experiences as real. Time for job two: figure out which layers can be aggressively quantized and which must stay high-precision. Not all layers are equal — quantizing a fragile early layer can destroy accuracy, while a robust layer shrugs it off.

Defining sensitivity

The metric is beautifully direct. Quantize only layer i to k bits, leave everything else at full precision, and ask: how much did the model's output change? Measure that change as KL-divergence between the full-precision output distribution and the partially-quantized one, averaged over the Distilled Data:

Ωi(k) = (1/Ndist) ∑j KL( M(θ; xj),  M(θ̃i(k); xj) )

where M(θ; xj) is the full-precision model's softmax output on Distilled image xj; M(θ̃i(k); xj) is the same model with only layer i quantized to k bits; KL measures how far the quantized output distribution drifted from the original; Ndist is the number of Distilled images. Small Ωi(k) ⇒ layer i barely cares about k-bit quantization ⇒ safe to compress.

Why KL, not accuracy drop? We have no labels for the Distilled Data — it is synthetic. KL between full-precision and quantized outputs needs no labels: it just asks "did the quantization change what the model says?" A label-free sensitivity metric is exactly what a zero-shot setting demands.

The experiment that justifies everything

The paper's pivotal plot (Figure 2, left): measure each ResNet50 layer's sensitivity to 4-bit quantization three ways — using real training data, using Gaussian noise, and using Distilled Data. If Distilled Data is a good stand-in, its sensitivity curve should track the real-data curve. It does. Gaussian noise does not — for the first few layers its ordering is literally reversed.

This is the make-or-break result. If Gaussian noise gave the same sensitivity ranking as real data, you wouldn't need Distilled Data at all. The fact that it gets the ordering wrong means a Gaussian-calibrated mixed-precision scheme protects the wrong layers — and that is why prior data-free methods collapsed at low bit-widths.

Toggle the data source below and watch the per-layer sensitivity curves. Notice how the Distilled curve hugs the real-data curve, while Gaussian diverges — especially in early layers where the ordering flips.

Layer sensitivity: real vs Distilled vs GaussianInteractive

Each curve is Ωi(4) across layers (log scale). Real = ground truth. Toggle which calibration source to overlay.

overlap(Distilled, Real) high · Gaussian off
Why does ZeroQ measure sensitivity with KL-divergence rather than accuracy drop?

Chapter 5: The Pareto Frontier (Showcase)

We can now rank layers. But mixed-precision means choosing a bit-width for every layer, and the choices interact through a budget. For ResNet50 with bit options {2,4,8} over ~50 layers, the search space is 350 ≈ 7.2×1023 configurations. You cannot brute-force that.

The key simplifying assumption

ZeroQ assumes layer sensitivities are independent: the cost of quantizing layer i to k bits is Ωi(k), regardless of what the other layers do. This is an approximation (errors can compound), but it makes the total perturbation a simple sum, and that is what makes the search cheap.

The optimization: for a target model size Starget, pick the bit assignment {ki} that minimizes total sensitivity while fitting the budget:

min{ki} Ωsum = ∑i=1..L Ωi(ki)   s.t.   ∑i=1..L Pi·ki ≤ Starget

ki is the bit-width chosen for layer i; Ωi(ki) is that layer's sensitivity at that bit-width (from Ch 4); Pi is layer i's parameter count, so Pi·ki is its size in bits; Starget is the size budget. Total cost of building the sensitivity table is only O(m·L) — m bit options times L layers — because each Ωi(k) is measured independently.

Why "Pareto frontier"? Sweep Starget across all sizes. For each, the optimizer (a small dynamic program) returns the minimum achievable total sensitivity. Plot size on x, minimum sensitivity on y — that curve is the Pareto frontier: every point is "the best accuracy you can buy at this model size." You set a size budget, drop a vertical line, and read off the bit assignment. No manual tuning, no RL search.

Worked mini-example: 3 layers

Suppose three layers with parameter counts P = [4, 2, 1] (millions), bit options {2,4,8}, and a sensitivity table (lower = more robust):

LayerPiΩ(2)Ω(4)Ω(8)
L14M0.900.200.02
L22M0.300.050.01
L31M0.080.020.005

Budget Starget = 30 Mbit. All-8-bit costs (4+2+1)·8 = 56 Mbit — too big. The optimizer wants to spend its precious bits where they reduce sensitivity most. The big, sensitive L1 is where high bits pay off; the tiny robust L3 can go to 2-bit nearly free. A good assignment: L1=8 (32 Mbit)… too big. Try L1=4, L2=4, L3=2 → 16+8+2 = 26 Mbit ≤ 30, total Ω = 0.20+0.05+0.08 = 0.33. The interactive sim below runs this exact search live — drag the budget and watch the assignment snap to the lowest-sensitivity point that fits.

Pareto-frontier bit allocation (live solver)Showcase · Interactive

Each dot is one mixed-precision configuration (size vs total sensitivity). The lower envelope is the Pareto frontier. Drag the size budget — the solver picks the minimum-sensitivity config under the budget and shows the chosen per-layer bits. Toggle layers to see how sensitivity reshapes the frontier.

budget 30 Mbit
What simplifying assumption lets ZeroQ avoid the exponential search over bit configurations?

Chapter 6: The Full Pipeline

Let's assemble the three stages into the end-to-end ZeroQ flow and trace exactly what data crosses each boundary. This is the diagram you should be able to redraw from memory.

ZeroQ pipeline — click a stageInteractive

Click each box to see what enters and leaves that stage.

Click a stage above to inspect its data flow.

The boundaries, spelled out

StageInOutCost (ResNet50)
Distill DataTrained model + its BN stats {μii}Synthetic batch xr [32,3,224,224]~3s
Sensitivityxr + model + bit options {2,4,8}Table Ωi(k), shape [L × m]~12s
Pareto searchΩ table + Pi + StargetBit assignment {ki}~14s
Quantize{ki} + xr activation rangesMixed-precision integer modelnegligible
Failure mode — what happens if you skip distillation? Feed Gaussian noise instead. The activation ranges [a,b] you read off will be wrong (real activations are not Gaussian after a few nonlinear layers), so your clipping is off; and the sensitivity ordering is wrong (Ch 4), so the Pareto search protects the wrong layers. Both errors compound. This is precisely why naïve data-free quantization degrades sharply below 8-bit.

One subtlety the paper buries

The independence assumption in the Pareto step is not exactly true — quantization errors do interact across layers. The appendix relaxes this without an exponential blowup, but the headline method uses the independent approximation and it works because the errors are small enough that the additive surrogate ranks configurations correctly. The lesson: a "wrong" model that ranks options correctly is good enough for a search.

What does the sensitivity stage produce as its output?

Chapter 7: The Core Idea as Code

Here is the heart of ZeroQ — Distilled Data generation — as runnable PyTorch. The trick that makes it tiny: hook the BN layers to capture their inputs, compare measured stats to the stored running stats, and backprop into the image.

python
import torch, torch.nn as nn

def distill_data(model, batch=32, iters=500, lr=0.5, img=(3,224,224), dev='cuda'):
    model.eval().to(dev)

    # 1) Grab the stored BN targets and register hooks that capture inputs.
    bn_targets, captured = [], []
    def make_hook(slot):
        def hook(mod, inp, out):
            captured[slot] = inp[0]          # activations entering this BN layer
        return hook
    for m in model.modules():
        if isinstance(m, nn.BatchNorm2d):
            bn_targets.append((m.running_mean.detach(),
                               m.running_var.detach().sqrt()))   # (mu_i, sigma_i)
    captured = [None] * len(bn_targets)
    bns = [m for m in model.modules() if isinstance(m, nn.BatchNorm2d)]
    for i, m in enumerate(bns):
        m.register_forward_hook(make_hook(i))

    # 2) The IMAGE is the variable we optimize; weights stay frozen.
    x = torch.randn(batch, *img, device=dev, requires_grad=True)
    opt = torch.optim.Adam([x], lr=lr)

    for it in range(iters):
        opt.zero_grad()
        model(x)                                  # forward fills `captured`
        loss = 0.0
        # input layer: pin to zero-mean / unit-var (standard preprocessing, i=0)
        loss += (x.mean()**2) + (x.var() - 1.0)**2
        for i, a in enumerate(captured):
            mu_t, sig_t = bn_targets[i]
            mu_r  = a.mean(dim=(0, 2, 3))         # per-channel mean of synthetic activations
            sig_r = a.var(dim=(0, 2, 3)).sqrt()   # per-channel std
            loss += ((mu_r - mu_t)**2).sum() + ((sig_r - sig_t)**2).sum()
        loss.backward()                           # gradient flows into x, not weights
        opt.step()
    return x.detach()                             # the Distilled Data

Read the three load-bearing lines. requires_grad=True on x (not the weights) makes the image the optimization variable. The per-channel mean/std over dims (0,2,3) reduces a [N,C,H,W] activation to a [C] statistic, matching the shape of the stored running_mean. And loss.backward() sends gradients all the way back to the pixels.

Concept → realization: Notice there is no dataset, no DataLoader, no labels anywhere. The only "data" is model.running_mean / running_var — numbers already inside the checkpoint. That is what "zero-shot" looks like in code: the training set is reconstructed from the model itself.

Sensitivity is then a short loop: for each layer i and bit k, quantize just that layer, run the Distilled Data through both versions, and accumulate KL(full_softmax, quant_softmax). The Pareto step is a dynamic program over the resulting table.

In the code, why does x have requires_grad=True while the model is in eval() with no optimizer over its parameters?

Chapter 8: Experiments

Does data-free actually compete with data-hungry methods? The headline numbers say yes — often beating methods that do use real data and fine-tuning.

The result that surprised people

On MobileNetV2, a notoriously quantization-fragile compact model, ZeroQ at W8A8 lands within 0.12% of the FP32 baseline and beats DFQ (the previous data-free SOTA) by 1.71% — with no data and no fine-tuning. On ResNet18 at the brutal W4A4 setting, ZeroQ hits 69.05%, beating DFC's fine-tuned 68.06%. Read that again: no-data-no-finetune beat with-data-with-finetune.

Accuracy vs model size — ZeroQ vs baselinesInteractive

Top-1 accuracy vs compressed size. Pick a model. ✓/✗ markers note whether each method needed data (D) or fine-tuning (FT).

ResNet50

The numbers, exact

ModelMethodNo data?No FT?W/A bitsSize (MB)Top-1
ResNet50Baseline (FP32)32/3297.4977.72
ResNet50OMSE (no data)4/3212.2870.06
ResNet50ZeroQMP/812.1775.80
MobileNetV2DFQ8/83.3471.20
MobileNetV2ZeroQ8/83.3472.91
ResNet18DFC (with FT)4/45.5868.06
ResNet18ZeroQ†MP/45.5769.05

ZeroQ† uses percentile clipping (instead of min/max) to set [a,b] — helpful at low precision because it ignores activation outliers. It even extends beyond classification: on COCO object detection with RetinaNet, ZeroQ at W8A8 matched the 36.4 mAP baseline exactly, and DFQ's weight-equalization trick doesn't obviously transfer to detection.

The honest caveat: Mixed-precision "W4 average" does not mean every layer is 4-bit. Sensitive layers (often the first and last) stay at 8-bit; robust middle layers drop to 2-bit; the average lands near 4. The accuracy comes from that uneven allocation — which is only possible because Distilled Data gave a trustworthy sensitivity ranking.
What was the headline surprise in the ResNet18 W4A4 comparison?

Chapter 9: Connections & Cheat Sheet

ZeroQ sits at the intersection of three ideas you've probably met separately: post-training quantization, mixed-precision (Hessian/HAWQ-style) bit allocation, and feature-visualization-by-inversion. Its contribution was gluing them so that no real data is needed at any point.

Where it fits

Inverts ideas from
HAWQ (Hessian-based mixed precision, but needs data) · DeepDream/feature inversion (image optimization) · DFQ (data-free via weight equalization)
ZeroQ's twist
Replace "real data for calibration + sensitivity" with BN-statistic-matched Distilled Data; replace expensive RL/NAS bit search with an O(mL) Pareto frontier
Led to
A wave of BN-statistics data-free quantization & data-free distillation methods (incl. follow-ups like SQuant)

Related lessons

Cheat sheet — every key equation

Symbol / Eq.MeaningWhen you use it
Δ = (b−a)/(2k−1)quant step size over clip range [a,b] at k bitsMapping floats → integers
Eq.3: minxri‖μ̃ir−μi2+‖σ̃ir−σi2Distillation loss — match synthetic activation stats to stored BN statsStage 1: generate Distilled Data
Eq.2: Ωi(k) = meanj KL(M(θ;xj), M(θ̃i(k);xj))Sensitivity of layer i at k bits (output KL on Distilled Data)Stage 2: rank layers
Eq.4: min ∑Ωi(ki) s.t. ∑Piki≤StargetPareto bit allocation under size budgetStage 3: choose per-layer bits
The mastery test: You should now be able to (1) explain to a colleague why a private-data customer can still get a 4-bit model; (2) derive the receptive — er, the quantization step Δ and the distillation loss on a whiteboard; (3) answer "why not just use Gaussian noise?" with the sensitivity-ordering argument; and (4) sketch the ~30-line PyTorch that reconstructs data from a checkpoint's BN stats.
In one line, what is ZeroQ's core enabling trick?