Some layers of a network shrug off being squeezed into 2 bits; others fall apart. EvoQ measures which is which, then runs a genetic search — seeded by that sensitivity — to find a per-layer bit-width assignment that hits a size budget with the least accuracy loss, all from a handful of calibration images and no retraining.
You have a trained network — say a ResNet — that works beautifully on the GPU you trained it on. Now someone wants it on a phone, or a doorbell camera, or a $3 microcontroller. The weights are stored as 32-bit floating-point numbers. There are tens of millions of them. That is too much memory, too much bandwidth, and too much energy for the target device.
The obvious fix is quantization: replace each 32-bit float with a small integer — 8 bits, 4 bits, maybe even 2. Fewer bits means a smaller model and cheaper arithmetic. An 8-bit model is 4× smaller than a 32-bit one. A 4-bit model is 8× smaller.
The lazy approach is uniform quantization: pick one bit-width — say 4 — and apply it to every layer. It is simple, but it leaves enormous value on the table, for one stubborn reason:
Layers are not equally fragile. The first convolution that touches raw pixels, and the final classifier that produces logits, are often extremely sensitive — drop them to 4 bits and accuracy craters. But a wide middle layer with thousands of redundant channels might survive at 2 bits with no visible damage. Forcing a single bit-width on all of them means you either over-spend bits on the robust layers or destroy the fragile ones.
So we want mixed precision: a possibly different bit-width per layer. But now count the choices. Suppose each of L layers can be 2, 4, or 8 bits. The number of assignments is 3L.
| Layers L | Bit choices | Configurations 3L | If each took 1 sec to evaluate |
|---|---|---|---|
| 10 | {2,4,8} | 59,049 | ~16 hours |
| 20 | {2,4,8} | ~3.5 billion | ~110 years |
| 50 (a ResNet-50) | {2,4,8} | ~7×1023 | longer than the age of the universe |
You cannot try them all. You cannot even try a millionth of them. And every "evaluation" means running the quantized network on data to see how much accuracy you lost — not free. This is the wall EvoQ is built to get around.
One more constraint makes EvoQ's setting realistic. In post-training quantization (PTQ), you often do not have the full training set, the training pipeline, or the GPU-weeks to retrain. You have a finished model and maybe a few hundred unlabeled images. So every trick has to work cheaply, from little data, with no gradient-based fine-tuning.
We are trapped between two facts: the search space is astronomically large (Chapter 0), and a brute-force search is impossible. EvoQ's answer is a two-part idea, and each part attacks one of those facts.
Sensitivity alone is not enough. Knowing each layer's fragility tells you a ranking, but not the exact joint assignment that hits a specific size budget. Sensitivities are measured one layer at a time; the real network's accuracy under combined quantization has interactions a per-layer score can't capture.
Evolution alone is not enough. A genetic search over 350 configurations, started from random noise, wastes thousands of expensive evaluations before it stumbles onto anything decent. The whole point of PTQ is to be cheap — a slow search defeats the purpose.
Together they are complementary: sensitivity gives a warm start and a bias for mutation; evolution finds the joint assignment and handles the interactions. That synergy — "sensitivity-guided evolutionary search" — is the whole name of the paper.
Before we can talk about how many bits, we need to be precise about what quantizing a tensor actually does to the numbers. The idea is simple: take a continuous range of real values and snap each one to the nearest entry in a small grid.
For a weight tensor with values in [wmin, wmax], choose a bit-width b. That gives 2b integer levels (e.g. b=4 → 16 levels). The step size (also called scale) is how far apart two adjacent grid points are:
Here s is the scale (the gap between levels), b is the bit-width, and the denominator 2b−1 is the number of gaps between the 2b grid points. To quantize a value w, you round it onto the grid and back:
Read it left to right: divide by the step to get an integer index, round to the nearest grid index, multiply back by s to return to real units, shift by wmin. The gap between w and q(w) is the quantization error — the noise we inject.
A bell-shaped cloud of real weights (gray) snapped onto a b-bit grid (orange ticks). Watch the error bars grow as you cut bits. The number under the canvas is the average absolute quantization error — the noise injected into this layer.
A layer with n weights at b bits costs n · b bits of storage. The whole-model size is just the sum over layers — and that sum is the budget the search must respect. We will track it precisely in Chapter 5.
Chapter 2 showed that low bits inject more noise. But the same amount of injected noise hurts different layers by wildly different amounts. Sensitivity is the answer to one clean question:
A layer is sensitive if that single change causes a big shift in the outputs; it is robust if the outputs barely move. We will make "shift in outputs" precise in Chapter 4. For now, build the intuition.
Each bar is one layer's sensitivity at the bit-width you choose — taller means more fragile. Notice the classic "U": the first and last layers spike, the middle dips. Lower the bit-width and every bar rises, but the fragile ones rise fastest. (Profile is illustrative, shaped like real ResNet/MobileNet measurements.)
"How much does the network's behavior change?" needs a number. And the constraint is brutal: we have no labels and only a small calibration set — a few dozen to a few hundred unlabeled images. We cannot measure accuracy directly (no labels) and we cannot afford the full validation set. So we measure how the output distribution shifts.
Let P be the network's output distribution (the softmax over classes) on the calibration data at full precision, and Qℓ,b the output when layer ℓ alone is quantized to b bits. The sensitivity is a distance between P and Q. Three metrics are standard (the closely-related literature evaluates exactly these kinds):
| Metric | Formula (per sample, then averaged) | What it captures |
|---|---|---|
| KL divergence | Σi Pi log(Pi / Qi) | How much the predicted class distribution shifts. Punishes confident-but-wrong moves heavily. |
| MSE / L2 | Σi (Pi − Qi)2 | Raw squared output drift. Simple, cheap, no log issues. |
| SQNR (signal-to-quant-noise) | 10 log10( ‖P‖2 / ‖P−Q‖2 ) | Error relative to signal size, in decibels. Higher = more robust; we negate it to make "bigger = more sensitive." |
Symbols, in plain terms: Pi is the full-precision probability for class i; Qi is the same probability after quantizing layer ℓ; the sum runs over all classes. KL is the divergence of the language-model-style "next token" sense — how surprised P is by Q.
That last box is the deliverable of this chapter: a small L × B table where entry S[\ell][b] says "how badly layer \ell hurts at b bits." It costs only L \cdot B forward passes — cheap — and every later step reads from it instead of re-running the network.
Now we formalize what the evolutionary search is actually searching over, and what "good" means. This is the contract between the search and the world.
A single candidate solution is a vector — one bit-width per layer. We'll call it a genome (borrowing the genetic-algorithm word we'll need in Chapter 6):
Each gene b\ell is the bit-width assigned to layer \ell. A genome for an 8-layer net might be [8,4,2,2,4,2,4,8] — full precision on the fragile ends, squeezed in the robust middle.
Size. If layer \ell has n\ell weights, the model's quantized size is:
Cost / damage. We need a cheap proxy for accuracy loss. The natural one reuses the sensitivity table from Chapter 4 — sum the per-layer damage of the chosen bits:
This is an approximation — it ignores cross-layer interactions — but it's nearly free to compute (just table lookups), so the search can evaluate millions of genomes with it. The true accuracy of promising genomes is checked occasionally with a real forward pass on calibration data.
Eight layers, sized like a real net (fat middle). Click any bar to change its bit-width. Watch Size and Cost trade off, and whether you're under the dashed budget line. Try to get Cost low while staying under budget — that's the search's job, by hand.
We have a space of genomes and a way to score them. An evolutionary (genetic) algorithm finds good genomes by mimicking natural selection: keep a population of candidates, let the good ones reproduce, occasionally mutate, and repeat. No gradients, no enumeration.
The search maximizes fitness. The clean way to honor the constraint is a penalty on infeasible genomes:
Here \lambda is a large penalty weight. The first term rewards low damage; the second term punishes any overshoot of the budget. A feasible, low-cost genome has high fitness; an over-budget genome is crushed by the penalty so it loses the selection step.
Let's run a single generation on a tiny 4-layer network so every operator is concrete. All numbers below are illustrative but internally consistent.
Four layers with weight counts n = [100, 400, 400, 100] (fat middle, like a real net). Bit choices \{2,4,8\}. Budget: model must use ≤ 50% of the all-8-bit size. The all-8-bit size is (100+400+400+100)\times 8 = 8000 bits, so Budget = 4000 bits.
From Chapter 4's sensitivity table (illustrative), the damage S[\ell][b] is:
| Layer | S at 8b | S at 4b | S at 2b |
|---|---|---|---|
| L1 (first, fragile) | 0.00 | 0.30 | 1.50 |
| L2 (wide, robust) | 0.00 | 0.04 | 0.20 |
| L3 (wide, robust) | 0.00 | 0.05 | 0.22 |
| L4 (last, fragile) | 0.00 | 0.35 | 1.60 |
| Genome | bits | Size = Σ n·b | Feasible? (≤4000) | Cost = Σ S |
|---|---|---|---|---|
| A | [8, 4, 4, 8] | 800+1600+1600+800 = 4800 | NO (4800>4000) | 0+0.04+0.05+0 = 0.09 |
| B | [4, 2, 2, 4] | 400+800+800+400 = 2400 | YES | 0.30+0.20+0.22+0.35 = 1.07 |
A is low-cost but over budget; B is feasible but pricey (it cut the fragile layers). Neither is great. Evolution mixes them.
Child C = [A₁, A₂ | B₃, B₄] = [8, 4, 2, 4].
C is already better than B (cost 0.61 vs 1.07) and feasible. Crossover combined "keep L1 precise" from A with "squeeze L3" from B.
Mutate C at layer 4: 4→8 bits (give the fragile last layer more precision). Child C' = [8, 4, 2, 8].
This is the heart of EvoQ, made live. We run the full evolutionary search over per-layer bit configs for an illustrative network, and you control the one knob the paper is named for: does the initial population get seeded by sensitivity, or start random?
The plot is the accuracy–size plane — every dot is one genome. Up-and-left is the dream (small model, high accuracy). The frontier of best-possible tradeoffs is the Pareto front: genomes where you can't improve one axis without sacrificing the other. Watch the population evolve toward that front. Then toggle seeding off and watch how many more generations a random start needs.
● population · ● best-so-far · — Pareto front discovered · | budget line. Press Step for one generation, Run to animate, Reset to restart. Flip seeding and compare generation counts to reach the front. (Accuracy proxy is illustrative, built from a U-shaped sensitivity profile.)
Here is the whole method as runnable Python. It reads the precomputed sensitivity table, seeds the population with it, and runs the genetic loop. Read it as the executable summary of Chapters 4–8.
python import numpy as np # --- Inputs from Chapter 4 / Chapter 5 --- # n[l] : weight count of layer l (size accounting) # S[l][b] : sensitivity/damage of layer l at b bits (lookup table) # BITS : allowed bit-widths, e.g. [2, 4, 8] BITS = [2, 4, 8] def size_bits(g, n): return sum(n[l] * g[l] for l in range(len(g))) def cost(g, S): # cheap accuracy-loss proxy: sum of per-layer damage return sum(S[l][g[l]] for l in range(len(g))) def fitness(g, n, S, budget, lam=1e3): over = max(0, size_bits(g, n) - budget) return -cost(g, S) - lam * over # penalize budget violation def seed_genome(n, S, budget): # SENSITIVITY-GUIDED START: fragile layers -> more bits. # rank layers by how much they hurt at the lowest bit-width frag = sorted(range(len(n)), key=lambda l: -S[l][BITS[0]]) g = [BITS[0]] * len(n) # start everyone at the cheapest bits for l in frag: # spend bits on the fragile first for b in reversed(BITS): # try high bits down to low g[l] = b if size_bits(g, n) <= budget: break g[l] = BITS[0] # can't afford -> stay low return g def mutate(g, rate): g = list(g) for l in range(len(g)): if np.random.rand() < rate: g[l] = np.random.choice(BITS) # explore return g def crossover(a, b): cut = np.random.randint(1, len(a)) return a[:cut] + b[cut:] # exploit: splice parents def evoq(n, S, budget, pop=30, gens=40, mut=0.2, seeded=True): L = len(n) # --- initialize population --- if seeded: base = seed_genome(n, S, budget) P = [mutate(base, 0.15) for _ in range(pop)] # jitter the warm start else: P = [[np.random.choice(BITS) for _ in range(L)] for _ in range(pop)] for _ in range(gens): P.sort(key=lambda g: -fitness(g, n, S, budget)) # 1. select elite = P[:pop // 2] # survivors = parents children = [] while len(children) < pop - len(elite): a, b = elite[np.random.randint(len(elite))], elite[np.random.randint(len(elite))] child = mutate(crossover(a, b), mut) # 2-3. cross + mutate children.append(child) P = elite + children # 4. next gen P.sort(key=lambda g: -fitness(g, n, S, budget)) return P[0] # best per-layer bit-width assignment
Every method has edges where it breaks. Knowing them is what separates understanding from memorizing. Here are the honest limits of sensitivity-guided evolutionary PTQ.
Cost(g) = \Sigma S[\ell][b_\ell] assumes layer damages add. They don't always. Quantizing two adjacent layers can compound — the second layer receives already-corrupted activations and amplifies them. The fix is to occasionally validate promising genomes with a real forward pass on calibration data, not just the table. EvoQ's "limited data" is exactly for these spot-checks.
Sensitivity is measured on a handful of images. If those images don't cover the input distribution (e.g. all daytime photos for a model that also sees night), the sensitivity table is wrong, and the search confidently optimizes for the wrong thing. Garbage calibration → garbage bit allocation.
The search happily proposes [8,2,4,2,8,...]. But many accelerators only run 8-bit or 4-bit kernels efficiently; a 2-bit or 3-bit layer may fall back to slow emulation, erasing the speed gain. The bit set \{2,4,8\} must match what the target hardware actually accelerates, or "smaller" doesn't mean "faster."
Because EvoQ does not retrain, it can only reallocate the precision the trained weights already tolerate. At very aggressive budgets, quantization-aware training (QAT) — which fine-tunes weights to be robust to quantization — can reach bit-widths PTQ cannot. EvoQ trades that last bit of accuracy for not needing the training pipeline, full data, or GPU-weeks. That trade is the value proposition, and it's the right call when retraining isn't available.
| Situation | Symptom | Mitigation |
|---|---|---|
| Layer interactions | Predicted cost < real accuracy loss | Validate top genomes with real forward pass |
| Tiny/biased calibration | Looks great, fails on real data | Larger, representative calibration set |
| Unsupported bit-widths | Smaller but not faster | Restrict BITS to hardware-native widths |
| Extreme budget | Accuracy floor hit | Use QAT, or relax the budget |
EvoQ sits at the intersection of three big ideas: quantization, search/optimization, and the realities of deploying models without retraining. Here's where it fits and what to remember.
| Method | How bits are chosen | Needs training? | Tradeoff |
|---|---|---|---|
| Uniform PTQ | One bit-width for all layers | No | Simplest; wastes/destroys precision unevenly |
| HAWQ (Hessian-aware) | Hessian eigenvalues rank sensitivity | No (PTQ) / yes (QAT variants) | Principled sensitivity, heavier to compute |
| HAQ (RL-based) | Reinforcement-learning agent picks bits | Trains a policy | Powerful but expensive to set up |
| EvoQ | Sensitivity-seeded evolutionary search | No | Cheap, limited data, gradient-free; PTQ accuracy floor |
| QAT | Often uniform; weights fine-tuned | Yes (full pipeline) | Best low-bit accuracy; needs data + compute |
| Symbol | Meaning | Plain analogy |
|---|---|---|
| bℓ | bit-width of layer ℓ | how fine the ruler is for that layer |
| s = range / (2b−1) | quantization step (scale) | gap between ruler ticks |
| g = [b1..bL] | genome: a full bit assignment | one candidate recipe |
| S[ℓ][b] | sensitivity table | how much pain layer ℓ feels at b bits |
| Size(g) = Σ nℓbℓ | model size in bits | total luggage weight |
| Cost(g) = Σ S[ℓ][bℓ] | accuracy-loss proxy | total predicted pain |
| Fitness = −Cost − λ·over | what the GA maximizes | low pain, inside the bag |
| KL / MSE / SQNR | distance(P, Q) metrics | how far outputs drifted |
• Transformer — the kind of large model that most needs compression to deploy
• Veanors hub — more paper walkthroughs, including other Model Compression methods
• MDPs & RL — the framework behind RL-based bit-allocation methods like HAQ