Zhe, Lin, Chandrasekhar & Girod (Stanford / I²R A*STAR), ICIP 2019

Optimal Bit Allocation for
Compressing Weights & Activations

Stop giving every layer the same number of bits. Treat quantization as a rate–distortion problem, and a one-line Lagrangian condition tells you exactly how many bits each weight and activation layer should get — no retraining required.

Prerequisites: What quantization is + partial derivatives
10
Chapters
6
Simulations
0
Retraining

Chapter 0: The Problem

You trained VGG-16. It works beautifully: 70.8% top-1 on ImageNet. Then you try to ship it to a phone and hit a wall. The weights and activations together are 579 MB. Every inference, those values must be loaded from off-chip memory into the chip — and that memory traffic, not the arithmetic, is what drains the battery and stalls the pipeline.

The obvious fix is quantization: store each number with fewer bits. A 32-bit float becomes an 8-bit integer, and you've cut the memory 4×. Push to 4 bits and it's 8×. So far, so standard.

The core question: when you have a fixed bit budget — say "3 bits per value on average across the whole network" — should every layer get exactly 3 bits? Or can you do better by giving some layers more bits and others fewer, while keeping the average at 3?

Why equal bits is leaving accuracy on the table

Here's the intuition that drives the whole paper. Layers are not equally sensitive. Quantizing the weights of one layer might barely nudge the network's output, while quantizing another layer with the same coarseness wrecks the prediction. If you spend bits equally, you over-pay the robust layers and starve the fragile ones.

A photographer knows this instinctively: spend your dynamic range where the eye looks. The same idea, applied to a neural network's 50+ tensors of weights and activations, is what this paper formalizes — and it does it with a classical rate–distortion argument from signal compression, not a new training trick.

A worked sense of scale

The two networks the paper studies, to fix the numbers in your head:

NetworkWeightsActivationsTotal valuesFull-precision size
VGG-16138M13M151M579 MB
ResNet-5022M8M30M116 MB

Notice the activations. Most compression papers of the era quantized only weights and left activations in float. But for ResNet-50, activations are 8M of 30M values — over a quarter. You cannot ignore them if memory is the bottleneck. This paper compresses both, in one unified budget.

The constraint that makes this elegant: the paper achieves all of this without re-training the quantized network. Re-training is expensive, needs the original training data (which you often don't have), and might not even converge. The method is just: measure each layer's sensitivity, then solve a small optimization. Minutes, not GPU-days.
Two budgets, same average bits — different accuracy

Five layers, each with a different sensitivity (bar height = how much output error one fewer bit causes). Both allocations average exactly 3 bits/layer. Toggle between equal and unequal and watch the total output error at the bottom. Unequal allocation pours bits into the steep, sensitive layers.

Why might unequal bit allocation beat equal allocation at the same average bit-rate?

Chapter 1: The Key Insight

Strip the paper to its skeleton and it is one analogy: allocating bits to network layers is the same problem as allocating bits to frequency bands in JPEG or to subbands in an audio codec. Compression engineers solved this decades ago with rate–distortion theory. This paper imports that machinery wholesale.

The argument has exactly three moving parts, and once you see them the rest is bookkeeping:

1. Distortion
Quantizing layer i with bit-rate R causes a measurable output error D⃗(R). More bits → less error.
2. Additivity
The total output error of quantizing ALL layers is (approximately) the SUM of each layer's individual error. They don't interact.
3. Lagrangian
Minimize total error subject to a fixed total bit budget. The optimum: every layer sits at the SAME error-vs-rate slope.
The insight in one sentence: at the optimal allocation, the marginal benefit of one more bit is equal across all layers. If layer A would drop its error more than layer B by getting an extra bit, move a bit from B to A. Keep doing that until no swap helps — that's when all slopes are equal. This is the classic equal-slope (Pareto) condition for resource allocation, the same condition economists use to allocate a budget across goods.

Why this is the right framing

The reason additivity (part 2) holds is the quiet miracle that makes everything tractable. A neural network is continuously differentiable from input to output. Quantization adds a tiny error to each tensor. For small errors, a first-order Taylor expansion says the output perturbation is approximately linear in those input errors — and linear perturbations add. We will verify this empirically in Chapter 4 and derive it in Chapter 5.

What the whole pipeline is, in one breath

For each weight layer and each activation layer: sweep the bit-rate, measure the output error at each rate, fit a distortion-vs-rate curve. Then run a tiny allocator that, given a total budget, picks the bit-rate for every layer so all slopes match. Quantize at those rates, entropy-code the indices, done. No gradients, no training data, polynomial time.

What property lets the paper treat bit allocation as a clean optimization problem?

Chapter 2: Quantization & the Rate Knob

Before we can allocate bits, we need to be precise about what a "bit-rate" buys us. The paper uses uniform scalar quantization followed by entropy coding. Let's unpack both, because the rate knob lives here.

Uniform scalar quantization

Take a tensor of floats. Pick a step size Δ. Round every value to the nearest multiple of Δ. The integer index of that multiple is what you store. Smaller Δ means finer bins, less rounding error, but more distinct indices → more bits. Larger Δ means coarse bins, big rounding error, few indices → few bits. The step size IS the rate knob.

q(w) = Δ · round(w / Δ)

Why entropy coding, not just "use B bits"

If a layer's quantized values land in, say, 30 distinct bins, naive fixed-width storage needs ⌈log₂ 30⌉ = 5 bits each. But the bins are not used equally — values cluster near zero. Entropy coding (e.g. Huffman/arithmetic coding) gives common indices short codes and rare indices long codes, so the average bits per value drops to the empirical entropy of the index distribution.

Why this matters for the framework: because we entropy-code, "bit-rate" is a smooth, continuous quantity (average bits/value), not a discrete "use 4-bit integers." That smoothness is exactly what lets us take derivatives in Chapter 5. The rate of layer i is Ri = (#values) × H(indices), where H is entropy in bits.
Step size Δ trades distortion against rate

A bell-shaped weight distribution (gray) and its quantization bins (orange grid). Drag Δ. Watch the readout: small Δ → many bins, high entropy (high rate), small rounding error (low distortion); large Δ → few bins, low rate, big distortion. This single curve — distortion vs rate — is what the allocator reasons about.

python
import numpy as np

def quantize(w, delta):
    idx = np.round(w / delta)              # integer index per value
    return idx * delta, idx               # reconstructed value, index

def entropy_bits(idx):
    """average bits/value after ideal entropy coding"""
    _, counts = np.unique(idx, return_counts=True)
    p = counts / counts.sum()
    return -np.sum(p * np.log2(p))      # Shannon entropy H

w = np.random.randn(100000)            # a layer's weights
qw, idx = quantize(w, delta=0.2)
rate = entropy_bits(idx)               # avg bits/value
dist = np.mean((w - qw) ** 2)         # quantization MSE
In this framework, what physically controls a layer's bit-rate?

Chapter 3: Output Error — the Right Thing to Measure

Here is a subtle but crucial choice. When you quantize layer i's weights, the obvious thing to measure is how much those weights changed — the quantization MSE of that tensor. The paper measures something else: how much the network's final output changed.

Why? Because a big change in a robust layer's weights might leave the output almost untouched, while a tiny change in a sensitive layer can flip the prediction. The thing you actually care about is the output, so measure error there.

The definition, symbol by symbol

Given network and input image I, the clean output is the vector V = ℱ(I) (think: the pre-softmax logits). Quantize the weights of layer i only, and the output becomes . The output error from quantizing those weights is:

DWi = E[ d(V, V̂) ] / dim(V)
The data flow, concretely: to get one point on layer i's distortion-rate curve: (1) pick a step size Δ for layer i; (2) quantize only that tensor, leave everything else full-precision; (3) run ~500 images through the modified net; (4) average the squared output difference vs the clean net; (5) record (rate, distortion). Repeat for several Δ values → one curve per layer. Repeat for every weight tensor and every activation tensor.
Distortion–Rate curve for one layer — the slope is everything

A convex, decreasing distortion-vs-rate curve. Drag rate to slide the operating point and read the slope (the orange tangent) — this slope, ∂D/∂R, is the quantity the Lagrangian equalizes across layers. Drag sensitivity to see fragile layers (steep) vs robust layers (flat). A fragile layer's slope stays steep longer, so it deserves more bits.

Why does the paper measure output error rather than the per-layer weight quantization MSE?

Chapter 4: Additivity — the Load-Bearing Assumption

Everything rests on one claim: if you quantize many layers at once, the total output error equals the sum of the errors you'd get from quantizing each layer alone.

D = DW1 + … + DWL + DA1 + … + DAL

Read it carefully. The left side is the error when all weights and activations are quantized simultaneously. The right side is a sum of terms, each measured by quantizing one tensor in isolation. If this holds, you can characterize each layer independently and never have to test combinations — that's the difference between polynomial and exponential work.

Why it (approximately) holds — the Taylor argument: the network is smooth. Write quantization as adding a small noise εi to tensor i. First-order Taylor expansion of the output around the clean point gives ΔV ≈ ∑i Ji εi, where Ji is the Jacobian of the output w.r.t. tensor i. If the noises across layers are roughly uncorrelated and small, the squared-error cross terms E[(Jiεi)·(Jjεj)] vanish, leaving a sum of per-layer squared terms. That's additivity.

The paper's verification (Figure 1)

The authors don't just assert it — they test it. Quantize layer 5's weights and layer 10's weights together, plot the true joint output error (y-axis) against the sum of the two individual errors (x-axis). If additivity holds, every point lands on the 45° diagonal. In VGG-16 they did this for weight–weight, activation–activation, and weight–activation pairs. The points hug the diagonal.

Additivity test — true joint error vs sum of individual errors

Each dot is a pair of quantized layers: x = (error of layer A alone) + (error of layer B alone), y = true error of quantizing both. The gray line is the perfect-additivity diagonal. Drag interaction up to simulate large quantization noise (where the Taylor approximation breaks and points drift off the line). At small noise — the regime the paper operates in — additivity is tight.

What the paper quietly assumes (and you should know): additivity is first-order. It is excellent at the moderate bit-rates the paper targets (3–8 bits) but would degrade at extreme low precision (binary/ternary), where quantization noise is no longer "small" and the cross terms matter. This is exactly why the method shines in the mixed-precision regime and isn't designed to compete with binary-weight networks.
Additivity of output error is justified by:

Chapter 5: The Lagrangian — Deriving the Equal-Slope Rule

Now the payoff. We have a sum of per-layer errors (additivity) and a sum of per-layer rates. We want to minimize total error subject to a fixed total budget. This is a constrained optimization, and the tool is a Lagrange multiplier.

The constrained problem

minR   ∑i ( DWi + DAi )    s.t.    ∑i ( RWi + RAi ) = Rtotal

In words: choose the bit-rates RWi, RAi for every weight and activation layer to make total output error smallest, while the total bits used equals your budget Rtotal.

Fold the constraint into a single cost

A Lagrange multiplier λ turns the constrained problem into an unconstrained one. We build a cost J that subtracts λ times the rate — spending bits is "penalized" at price λ:

J = ∑i ( DWi + DAi ) − λ · ∑i ( RWi + RAi )

Think of λ as the exchange rate between error and bits: "how much output error am I willing to accept to save one bit?" High λ → bits are expensive → aggressive compression. Low λ → bits are cheap → keep more precision. Sweep λ and you trace the entire rate–distortion frontier.

Set the derivatives to zero

At the optimum, J is flat in every variable. Take the partial derivative of J with respect to one layer's rate, say RWi. Only that layer's distortion term and that layer's rate term depend on it:

∂J / ∂RWi = ∂DWi / ∂RWi − λ = 0

Rearrange, and do the same for activations. The result is the entire paper in one line:

∂DWi / ∂RWi = ∂DAj / ∂RAj = λ   for all i, j
The equal-slope condition, stated plainly: at the optimal allocation, every layer — weight or activation — operates at the same slope on its distortion-rate curve, and that common slope is −λ. If two layers had different slopes, you could move a bit from the shallow-slope layer (where it buys little error reduction) to the steep-slope layer (where it buys a lot) and reduce total error. The only stable point is when all slopes match. This is the classic Pareto condition for optimal resource allocation in economics.
When does this hold? The equal-slope optimum is valid as long as (1) errors are additive (Ch 4), (2) rates are additive, (3) each D(R) curve is convex, and (4) the resulting rates come out positive. Crucially, the paper notes it makes no assumption about the specific quantizer or coder — only that you can plot distortion vs rate. That generality is why it beats hand-tuned schemes.
Equal-slope optimization — sweep λ, watch slopes line up

Three layers with different sensitivities, each shown as its own D–R curve. The operating point on each is placed where the slope equals −λ. Drag λ: as bits get cheaper (small λ), every point slides right to higher rate, but they always sit at the same slope. The total bits and total distortion update live — this is one point on the rate–distortion frontier.

At the optimal bit allocation, what is equal across all layers?

Chapter 6: The Allocator Algorithm (the showcase)

The equal-slope condition tells us what the answer looks like. Now: how do we actually compute it? The paper finds the Pareto-optimal allocation in polynomial time with a simple λ-sweep. Here is the recipe, and then you'll run it.

Step 1 — Profile
For each layer, sweep step sizes → get a set of (rate, distortion) operating points. One D–R curve per layer.
Step 2 — Price bits
Pick a λ. For each layer independently, choose the operating point whose slope is closest to −λ.
Step 3 — Check budget
Sum the chosen rates. Too many bits? Raise λ (make bits pricier). Too few? Lower λ. Binary-search λ.
↻ until budget met
Why a λ-sweep works: each layer's "best point for this λ" is found independently — no joint search over all layers' bit-widths (which would be exponential). And total rate is monotonic in λ: higher λ always means fewer total bits. Monotonicity makes the search a clean binary search. That's how an allocation over 50+ tensors solves in milliseconds.
Run the allocator — set a budget, watch bits flow to sensitive layers

Six layers, sorted left-to-right by sensitivity (most sensitive first). Bars = bits assigned. Drag budget to set the average bits/layer. Toggle Optimal vs Equal: the optimal allocator (a binary search on λ) front-loads bits onto the steep, sensitive layers and starves the flat, robust ones — while the equal scheme paints every bar the same. The total output distortion readout shows how much the optimal allocation saves.

The allocator as runnable code

python
import numpy as np

# curves[i] = list of (rate, distortion) points for layer i, rate ascending
def slope_at(points, k):
    """local slope dD/dR between point k and k-1 (negative)"""
    (r0,d0),(r1,d1) = points[k-1], points[k]
    return (d1 - d0) / (r1 - r0)

def alloc_for_lambda(curves, lam):
    """each layer picks the highest rate whose slope is still steeper than -lam"""
    chosen = []
    for pts in curves:
        k = 0
        while k+1 < len(pts) and -slope_at(pts, k+1) > lam:
            k += 1
        chosen.append(pts[k])
    return chosen                               # (rate, dist) per layer

def allocate(curves, budget):
    """binary-search lambda so total rate == budget"""
    lo, hi = 1e-6, 1e6
    for _ in range(60):
        lam = (lo + hi) / 2
        pts = alloc_for_lambda(curves, lam)
        rate = sum(r for r,_ in pts)
        if rate > budget: lo = lam       # too many bits -> raise price
        else: hi = lam                      # too few -> lower price
    return alloc_for_lambda(curves, hi)
Why can the allocator solve the joint problem in polynomial time instead of searching all bit-width combinations?

Chapter 7: Joint Weight + Activation Allocation

A key contribution that's easy to skim past: the paper allocates bits to weights and activations in one shared budget, not two separate ones. This is not just tidiness — it measurably lowers output error.

Separate vs joint — what changes

The naive approach (separate) gives weights their own budget and activations their own budget, and optimizes each pool independently. The paper's approach (joint) throws all weight tensors and all activation tensors into one pool and applies a single λ across everything.

Why does joint win? Because the equal-slope condition must hold across the entire network. If activations happen to be more sensitive than weights at the current allocation, a separate scheme can't react — it's locked into its weight/activation split. A joint scheme simply moves a few bits from weights to activations until all slopes match.

What the paper observed (Figure 4, VGG-16 at 3 avg bits): the joint allocation pushed the weight bit-rates slightly down and the activation bit-rates slightly up compared to separate allocation. The result: output error dropped from 3.71 to 3.57 on VGG-16, and 1.97 to 1.88 on ResNet-50 — same total bits, lower error, purely by letting bits cross the weight/activation boundary. The framework discovered, on its own, that VGG-16's activations were under-served by the separate scheme.

The data-flow reason activations matter

Remember the tensor accounting from Chapter 0: ResNet-50 is 22M weights + 8M activations. If you only ever compress weights (as most prior work did), the 8M activations — over a quarter of the memory traffic — stay in full float and dominate the remaining footprint. Joint allocation is what lets a single knob squeeze both, which is the only way to actually hit an 8× total-memory target.

Separate vs Joint allocation — let bits cross the W/A boundary

Left group = weight layers, right group = activation layers (activations here are more sensitive). Separate fixes each group's total bits, so activations stay starved. Joint uses one λ everywhere — watch bits migrate from weights to the hungrier activations and the total distortion drop. Same total bits, lower error.

Why does jointly allocating bits to weights AND activations beat allocating to each pool separately?

Chapter 8: Experiments & Results

The proof is in the numbers. The authors evaluate on VGG-16 (70.8% baseline top-1) and ResNet-50 (75.2% baseline) over the ImageNet validation set, comparing against a wall of contemporary methods: BWN, TWN, INQ, FGQ, IAOI, AQ, CLIP-Q, SYQ, TBSQ.

Headline numbers

NetworkAvg bits (W & A)CompressionTop-1 accuracy loss
VGG-168 bits0% (no loss)
VGG-164 bits0.2%
VGG-163 bits>10×<1%
ResNet-508 bits0.4%
ResNet-504 bits1.0%

The remarkable line is VGG-16 at 3 average bits — over 10× smaller — with under 1% accuracy loss, and no retraining. The method outperforms every baseline on both networks except TBSQ on VGG-16, and TBSQ only wins after retraining (which can even exceed the original accuracy). Without retraining, this framework is state-of-the-art.

The honest comparison the paper makes: TBSQ retrains and reaches 71.7% on VGG-16 — higher than the 70.8% baseline — because retraining lets the network adapt to quantization. This paper deliberately forgoes that. Its pitch is: when you can't or won't retrain (no data, no compute, deployment constraints), here is the best you can do, and it's competitive with methods that do retrain.

What the per-layer allocation revealed

When the authors plotted the learned bit allocation (Figures 4–5), a pattern emerged: the allocation is strongly unequal, and it differs between weights and activations and across depth. They also note one practical choice — they do not quantize the last layer's activations, because the final logits feed directly into the prediction and are too sensitive to coarsen. This is the equal-slope rule talking: that tensor's slope stays steep, so it always demands more bits.

Accuracy vs total size — equal allocation vs this paper vs baselines

A stylized version of the paper's Figure 2 (VGG-16). The orange curve is the proposed method's accuracy–size frontier; the dim curve is equal allocation; dots are individual baselines. Drag total size to slide along the budget axis and read the accuracy gap — the unequal-allocation curve sits well above equal allocation across the whole range.

What is the headline advantage of this method over a retraining-based method like TBSQ?

Chapter 9: Connections & Cheat Sheet

You can now re-derive this paper on a whiteboard. Let's place it in the landscape and leave you with a reference card.

Where this sits

ApproachRetrain?Bit-widthIdea
BWN / TWNYes1–2 bit, uniformBinary/ternary weights, all layers same
Deep Compression (Han)YesmixedPrune + cluster + Huffman, retrained
TBSQ / SYQYeslow, learnedLearn quantization during training
This paperNomixed, optimalLagrangian R–D bit allocation, post-hoc
HAWQ (later, 2019+)SometimesmixedUse Hessian eigenvalues as the sensitivity signal
The lineage: this is rate–distortion bit allocation (Shoham & Gersho, 1988; the bedrock of JPEG and MPEG) applied to neural networks. Its successor ideas — like HAWQ and mixed-precision via the Hessian — replace the empirical D–R curves with a second-order (Hessian-based) sensitivity measure, but the skeleton is identical: per-layer sensitivity → allocate bits where sensitivity is highest. Even modern LLM quantization (GPTQ, AWQ, per-layer mixed precision) is the same instinct: not all weights deserve the same precision.

The cheat sheet

SymbolMeaningPlain-English
DWioutput error from quantizing layer i's weightshow much this layer's quantization hurts the prediction
RWitotal bits for layer i's weightsstorage spent on this tensor
Δquantizer step sizethe rate knob: small = fine = many bits
λLagrange multiplierprice of one bit, in units of error
∂D/∂R = −λequal-slope optimumevery layer at the same error-per-bit slope

The recipe, memorized: (1) profile each tensor's distortion-vs-rate curve on a few hundred images; (2) binary-search λ so every layer picks the operating point at slope −λ and the total rate hits your budget; (3) quantize at those step sizes, entropy-code, ship. No gradients. No retraining.

Limitations to keep honest about

Related lessons

If quantization is new, start with the foundations; if the optimization felt fast, slow down on the math.

The one idea to walk away with: compression is allocation, and optimal allocation equalizes marginal cost. Whether it's JPEG frequency bands, audio subbands, neural-net layers, or LLM weight groups — spend your bits where the next bit buys the most error reduction. The equal-slope condition is the universal answer to "where?"

"The rate-distortion approach is intuitively pleasing: the slopes of error-vs-rate must be equal — exactly the well-known Pareto condition for optimal resource allocation."