Stop quantizing every layer to the same 8 bits. Let a reinforcement-learning agent assign each layer its own bitwidth — guided not by FLOP estimates but by direct latency and energy feedback measured on the actual chip.
You have a trained neural network and a phone. The network is 50 MB of 32-bit floats and runs too slowly to be useful. The standard fix is quantization: replace each float weight with a low-bit integer. Round everything to 8 bits and you cut size by 4× and speed up the math. Done — except you've left a lot on the table.
Here is the catch the paper opens with. Layers are not equally fragile. The first layer reads raw pixels and is famously sensitive; squeeze it to 2 bits and accuracy collapses. A fat middle layer with millions of redundant weights barely notices going to 4 bits. So a single global bitwidth is a blunt instrument: it over-spends bits where they're wasted and under-spends them where they matter.
Suppose your model has N layers and you may pick any bitwidth from 1 to 8 for both the weights and the activations of each layer. That's 8 choices for weights × 8 for activations = 64 combinations per layer, and the layers multiply:
Plug in ResNet-50 (N = 50 layers): the search space is 8100 ≈ 1090. The paper notes this is larger than the number of particles in the universe (~1080). You cannot grid-search this. You cannot intuit it.
| Approach to picking bits | What it does | Why it falls short |
|---|---|---|
| Uniform (e.g. PACT, all 4-bit) | Same bitwidth for every layer | Ignores per-layer redundancy & hardware behavior |
| Hand-rules ("more bits in first/last layer") | Expert heuristics | Don't transfer across models or chips; sub-optimal |
| FLOP / model-size proxy | Minimize a cheap surrogate | Doesn't match real latency/energy on the chip |
| HAQ (this paper) | RL learns per-layer bits from measured HW feedback | — |
Engineers love proxy metrics because they're cheap to compute. But the paper's Table 1 is a quiet bombshell: the same MobileNet-V1 quantization policy gives 16.3 ms latency on one accelerator and 117 ms on another. A policy optimized for chip A is not optimal for chip B. FLOPs can't see this — they don't know about cache locality, memory bandwidth, or whether a layer is compute-bound or memory-bound. Real chips do, and HAQ measures them directly.
Picking bitwidths layer-by-layer under a hardware budget is a sequential decision problem with a delayed reward. That is exactly the shape of a reinforcement-learning problem. HAQ's move is to treat the quantizer as an agent walking down the network, one layer at a time, each step choosing that layer's bitwidth.
Two design decisions carry the whole paper. First, the action is the bitwidth itself — the agent doesn't tweak training, it directly authors the quantization policy. Second, the environment is the hardware. Most quantization papers stop at "fewer bits = smaller model." HAQ closes the loop with the chip, so the policy it learns is co-designed with the silicon. The paper shows the learned policies for edge vs cloud accelerators are drastically different — and explains why using the roofline model (Chapter 8).
Everything that follows builds this loop piece by piece: how a weight actually becomes 4 bits (Ch 2), what the agent sees and does (Ch 3), how the chip talks back (Ch 4), the agent in motion (Ch 5–6), how budgets are enforced (Ch 7), and what the learned policies reveal (Ch 8).
Before the agent can choose a bitwidth, we need to know exactly what "quantize this layer to b bits" does to a number. HAQ uses plain linear (uniform) quantization because linearly-quantized models only need fixed-point arithmetic — the cheap thing hardware is built for.
Take a weight w in layer k. Two steps:
Reading every symbol: clamp(w, c) truncates w into the range [−c, c] (a hard ceiling and floor). s is the step size — the gap between adjacent representable values. With b bits you get 2b levels; for signed values that's 2b−1−1 positive steps on each side. Dividing by s, rounding, and multiplying back snaps w onto the nearest grid point.
Where do you put the ruler's ends? Too wide and most of your few ticks land in empty space; too narrow and you clip away large weights. HAQ picks c to make the quantized weight distribution look as much like the original as possible, by minimizing KL-divergence:
In words: DKL measures how much information is lost when you replace the true weight histogram Wk with its quantized version. Sweep candidate clip values x, quantize, and keep the x that distorts the distribution least. Activations are handled identically except the range is [0, c] not [−c, c], because they're ReLU outputs and never negative.
Let w = 0.62 and clip c = 1.0.
| Bits b | Levels 2b−1−1 | Step s = c/levels | round(w/s) | Quantized w | Error |
|---|---|---|---|---|---|
| 2 | 1 | 1.000 | round(0.62)=1 | 1.000 | 0.380 |
| 4 | 7 | 0.1429 | round(4.34)=4 | 0.571 | 0.049 |
| 8 | 127 | 0.00787 | round(78.7)=79 | 0.622 | 0.002 |
The 2-bit ruler has one tick per side, so 0.62 rounds all the way to 1.0 — a 0.38 error. The 8-bit ruler nails it to 0.002. This is the lever the agent pulls: more bits buys precision but costs memory and compute. Use the widget below to feel it.
The agent processes the network layer by layer, and for each layer it takes two steps: one to set the weight bitwidth, one to set the activation bitwidth. At each step it observes a 10-dimensional feature vector describing the current layer.
For a convolution layer:
Every symbol, plainly: k = layer index (where am I in the stack); cin, cout = input/output channel counts; skernel = kernel size; sstride = stride; sfeat = input feature-map size; nparams = parameter count; idw = is-this-depthwise flag (1/0); iw/a = is-this-the-weight-step-or-activation-step flag; ak−1 = the action the agent took on the previous step (so it has memory of context). Fully-connected layers use the same vector with kernel→1, stride→0, and the depthwise flag→0. Every dimension is normalized to [0,1] so no single feature dominates.
A natural instinct is a discrete action — pick directly from {2,3,…,8}. HAQ deliberately avoids this. The reason is subtle and worth internalizing: discrete categories lose order. To a softmax, "2 bits" and "8 bits" are just two unrelated labels; it can't feel that 2 is "more aggressive" than 4 which is "more aggressive" than 8. So HAQ uses a continuous action ak ∈ [0,1] and rounds it to a bitwidth:
With bmin=2, bmax=8 this maps the continuous knob onto 7 equal bins. Worked example: ak=0.0 → round(1.5)=2; ak=0.5 → round(2−0.5+0.5×7)=round(5.0)=5; ak=1.0 → round(8.5)=8 (clamped). The continuous knob means a tiny nudge in policy is a tiny nudge in bitwidth — gradients flow smoothly, which is exactly what an actor-critic agent needs.
This is the chapter that makes HAQ "hardware-aware." The agent could be rewarded with FLOPs or model size — both cheap to compute. HAQ refuses, and the reason is the heart of the paper.
The key lens is operation intensity: arithmetic operations performed per byte of memory accessed (OPs/byte). The roofline model says a layer is either compute-bound (lots of math per byte — the chip's ALUs are the bottleneck) or memory-bound (little math per byte — moving data is the bottleneck).
This single number explains HAQ's most famous result. In MobileNet, depthwise convolutions have low operation intensity — they reuse data poorly, so they're memory-bound; their cost is dominated by moving activations. Pointwise (1×1) convolutions have high operation intensity — they're compute-bound. So if a chip is starved for memory bandwidth, cutting activation bits on the depthwise layers (less data to move) buys the most speed for the least accuracy loss. The agent discovers this on its own.
This is HAQ's core figure (the per-layer policy bars of Figures 3–4) made interactive — and turned into the actual optimization the agent solves. You're looking at a small MobileNet-style stack: alternating depthwise (memory-bound, low OPs/byte) and pointwise (compute-bound) layers, plus a sensitive first and last layer.
Each layer has its own bitwidth slider. As you change bits, watch three things update live: the model's size, its latency on the selected chip, and the predicted accuracy drop. The "Run HAQ agent" button performs a greedy version of HAQ's search — it spends the budget where it's cheapest in accuracy, exactly the trade-off the RL agent learns.
Notice the policy the agent lands on is not "everyone to 4 bits." It's lumpy — fat tolerant layers go low, sensitive landmark layers stay high, and the memory-bound layers on the edge chip get squeezed on activations. That lumpiness is the value of mixed precision, and it's invisible to any uniform-bitwidth scheme.
We have the loop's shape. Now the engine. Two questions remain: what reward drives the agent, and what algorithm trains it?
You might expect the reward to balance accuracy against latency. It doesn't — and the reason is elegant. HAQ already enforces the resource budget by clipping the action space (Chapter 7), so every policy the agent is allowed to propose already fits the budget. With the constraint handled structurally, the reward is freed to care about one thing:
Here accorig is the full-precision model's top-1 accuracy (on the training set), accquant is the quantized model's accuracy after a quick one-epoch finetune, and λ=0.1 scales it. The reward is the accuracy you gave up — almost always negative — and the agent's job is to make it as close to zero as possible while living inside the budget.
Because the action is continuous, HAQ uses Deep Deterministic Policy Gradient (DDPG) — an off-policy actor-critic method. The actor maps an observation to an action (the bitwidth knob, squashed by a sigmoid into [0,1]). The critic estimates the value of a state-action pair so the actor knows which direction to move. One step = one layer's weight-or-activation decision; one episode = a full pass over all layers.
HAQ tweaks the Bellman target slightly. The discount γ=1 (every layer's decision matters equally toward the final accuracy), and a baseline B — an exponential moving average of past rewards — is subtracted to cut variance:
Exploration is driven by adding truncated Gaussian noise to the actor's output, with noise std starting at 0.5 and decaying ×0.99 per episode — anneal from "try wild bitwidths" to "commit to the good policy."
# HAQ's core loop, distilled to the essential moves (pseudocode) for episode in range(N_EPISODES): bits = [] for k, layer in enumerate(net.layers): # one episode = walk all layers for kind in ['weight', 'act']: # two steps per layer O = observe(layer, kind, last_action) # 10-dim state, normalized [0,1] a = actor(O) + noise() # continuous action in [0,1] b = round(B_MIN - 0.5 + a*(B_MAX - B_MIN + 1)) # -> bitwidth 2..8 bits.append(b); last_action = a bits = clip_to_budget(bits, net, hw_sim, budget) # Ch.7: shrink until it fits qnet = quantize(net, bits) # linear quant, KL-chosen clip c acc = finetune_one_epoch(qnet, subset=100) # recover, fast R = 0.1 * (acc - acc_origin) # reward = accuracy delta ddpg.update(transitions, R, baseline) # actor + critic step noise.decay(0.99)
How does HAQ guarantee the final model actually fits the latency/energy/size limit? Not through the reward — through the action space itself. This is the trick that lets the reward stay pure.
After the agent has proposed a bitwidth for every layer, HAQ measures the policy's total resource cost on the hardware simulator. If it exceeds the budget, HAQ sequentially decreases the bitwidth of each layer until the constraint is finally satisfied. The agent's proposal sets the relative priorities (which layers it wanted to protect); the clipping step enforces the absolute budget.
Concretely: a policy is a length-2N vector of bitwidths (weights + activations per layer). The hardware simulator maps that vector → (latency, energy, size). If size > budget, decrement the layer that loses the least accuracy per bit saved, re-measure, repeat. The agent then sees, via the reward, that aggressive proposals get clipped into mediocre accuracy — so it learns to pre-allocate bits in a way that survives clipping. The constraint shapes the policy indirectly, through experience.
HAQ was evaluated on ImageNet with MobileNet-V1/V2 against PACT (a strong uniform-bitwidth baseline) across three constraint types: latency, energy, and model size, on edge (Xilinx Zynq-7020) and cloud (Xilinx VU9P) accelerators.
| Constraint / chip | Result vs fixed 8-bit |
|---|---|
| Latency (BISMO edge & cloud) | 1.4× – 1.95× faster, negligible accuracy loss |
| Latency (BitFusion, MobileNet-V1) | 2× faster (20.08 ms → 11.09 ms), almost no accuracy drop |
| Energy (BitFusion) | ~2× lower energy, nearly no accuracy loss |
| vs PACT at equal latency | Consistently higher top-1 accuracy on both edge and cloud |
Concretely on the edge accelerator at the 4-bit-equivalent budget, MobileNet-V1 PACT scored 62.44% top-1; HAQ's flexible policy hit 67.40% at the same latency — a ~5-point gain for free, just by spending bits more wisely.
The most cited part of HAQ isn't the accuracy table — it's the interpretation. The agent rediscovered hardware physics on its own:
Same network, opposite policies — driven entirely by the chip's roofline. The agent also learned the human heuristic for free: keep more bits in the first and last layers. No one told it; the reward did. Toggle the chip below and watch the policy flip.
HAQ sits at the intersection of three lineages, and pulls from each.
| Piece | Formula | What each symbol means |
|---|---|---|
| Search space | 82N | N layers, 8 weight-bit × 8 act-bit choices each |
| Linear quant | round(clamp(w,c)/s)×s | c = clip range, s = c/(2b−1−1) step size, b = bits |
| Clip choice | c = argminx DKL(W ‖ quant(W,b,x)) | pick c that distorts the weight histogram least |
| Action→bits | b = round(bmin−0.5 + a(bmax−bmin+1)) | a ∈[0,1] continuous; bmin=2, bmax=8 |
| Reward | R = λ(accquant − accorig) | λ=0.1; accuracy delta after 1-epoch finetune |
| Q-target | Q̂ = R − B + γQ(O',w(O')) | γ=1, B = EMA baseline (variance reduction) |
| Op intensity | #MACs / #bytes | low → memory-bound (cut bits to save bandwidth) |