Yvinec, Dapogny & Bailly (Datakalab / Sorbonne), 2023

NUPES: Non-Uniform Quantization
via Power Exponent Search

Most quantizers spread their integer grid evenly — but weights pile up near zero and a few outliers blow the range wide open. NUPES bends the grid with a single learnable exponent a, then learns brand-new quantized weights over the whole grid by gradient descent. State-of-the-art W4/A4 on ConvNets, ViTs, and LLMs.

Prerequisites: Uniform quantization + Gradient descent
10
Chapters
6
Simulations

Chapter 0: The Problem

You trained a beautiful 13-billion-parameter language model. Each weight is a 32-bit float. That is 52 gigabytes just for the weights — more than fits on a single consumer GPU, and every matrix multiply drags all of those bytes across the memory bus. You want to ship it. You want it to run on one card. You want it fast.

The lever is quantization: replace each 32-bit float with a small integer — say a 4-bit number, one of only 16 possible values. Memory drops 8×. Integer matmuls run faster and cooler. The catch is obvious: you are throwing away almost all the resolution of every number. Do it carelessly and the model's accuracy collapses.

The core question: Given only the trained weights — no original training data, no labels — can we choose 16 integer levels per layer so cleverly that a 4-bit model behaves almost exactly like the 32-bit one? And can we do it for LLMs, whose weights hide a few monstrous outlier values?

Why "data-free" is the hard, valuable mode

There are two worlds in quantization. In data-driven (or calibration-based) quantization you keep a small slice of data — maybe 1,000 examples — and fine-tune the quantized model to match the original. In data-free quantization you get nothing but the weight tensors. No data ever touches the process.

Data-free matters for two concrete reasons the paper highlights:

The shape of the data we must compress

Here is the fact that drives the entire paper. Neural network weights are not spread evenly between their minimum and maximum. They cluster in a tall, narrow bell shape around zero, with thin tails reaching far out. The histogram below is what a real weight tensor looks like — drag the outlier slider and watch what one extreme value does.

A real weight distribution — interactive
Most weights crowd near zero. The grid (vertical ticks) is spread evenly across the whole range — so it spends most of its 16 levels covering empty tail space.

Notice the tension. An even integer grid spends its precious 16 levels uniformly across the range. But almost all the actual weights live in a thin spike near zero. Most of the grid is wasted on near-empty tail regions, and the spike — where the action is — gets only two or three levels. That mismatch is the disease. NUPES is the cure.

Why is an evenly spread integer grid a poor fit for neural network weights?

Chapter 1: The Key Insight

If the grid is in the wrong place, there are two ways to fix it. The first is to move the grid — bunch the levels near zero where the weights live, and spread them out in the tails where weights are rare. That is non-uniform quantization.

The naive way to do this is a lookup table (codebook): you store 16 arbitrary float levels and snap each weight to the nearest one. It fits the distribution perfectly — but it breaks the math. A multiply w × x on a hardware integer unit assumes the integer codes are scalars you can multiply. If your codes are arbitrary table indices, every multiply needs a lookup, which kills the speed you bought.

NUPES's first insight: don't pick arbitrary levels. Bend the grid with a power function — map each weight through x → sign(x)·|x|a before quantizing uniformly. Because (xy)a = xaya, a power function turns multiplications into multiplications. The integer codes stay real numbers you can multiply on normal hardware. One knob — the exponent a — controls how hard the grid bends toward zero.

That is the PowerQuant idea, from the same authors' earlier paper. It already beat everything else on data-free quantization. But it had two stubborn weaknesses, and fixing them is what NUPES is:

Weakness 1: rigid weights
PowerQuant can only fine-tune by rounding each weight up or down by one level. On a bent grid that is a disaster: one step near zero is microscopic, one step in the tail is a cliff.
Fix 1: learn the whole weight
NUPES learns a brand new integer for every weight, free to move by many levels. No longer "round up or down" — relearn the quantized value outright.
Weakness 2: one global a
PowerQuant found one exponent for the whole network by grid search, frozen forever.
Fix 2: learn a per layer by SGD
NUPES makes a a trainable parameter — a different exponent per layer, optimized by gradient descent alongside the weights. The quantizer itself is now learned.

So in one breath: NUPES = PowerQuant's bendable grid + the ability to relearn every quantized weight + the ability to learn the bend itself by gradient descent, all numerically stabilized. We will build each piece from the ground up.

Why does NUPES use a power function instead of an arbitrary 16-entry lookup table?

Chapter 2: Uniform Quantization — and its two pitfalls

Before we bend anything, let us nail down the baseline so the bending makes sense. Uniform quantization takes a real number x, divides by a scale s, rounds to the nearest integer, and clips into the representable range. For b bits the range is the integers from −2b−1 to 2b−1−1 — for 4 bits, that is −8 to 7, sixteen levels.

Q(x) = round( x / s(X) ),    s(X) = maxx∈X|x| / (2b−1 − 1)

Read every symbol. X is the whole weight tensor. s(X) is the scale — the size of one quantization step in real units. It is set so the largest-magnitude weight lands exactly on the top integer (here, 7). round(·) snaps to the nearest integer; Q(x) is the resulting integer code. To recover an approximate float you go back: Q−1(Q(x)) = s(X) · Q(x). It will not equal the original x — that gap is the quantization error.

Worked example — quantize three real weights to int4

Say a tensor's largest magnitude is max|x| = 2.0. Then s = 2.0 / 7 ≈ 0.2857. Now quantize:

weight xx / sround → code qdequant s·qerror
2.0007.0072.0000.000
0.0500.17500.0000.050
0.4301.50520.5710.141

Look at row two. The weight 0.050 — a perfectly real, common, near-zero value — gets crushed to the integer 0. It vanishes. With only ~3 levels covering the dense spike near zero, tiny weights are systematically annihilated. That is pitfall number one.

Pitfall 1 — error blind to density. Uniform quantization gives the same step size everywhere. A rare extreme weight and a hyper-common near-zero weight suffer the same absolute error budget — even though there are thousands of times more weights near zero. The error is uniform; the data is not.

Pitfall 2 — outliers stretch the grid

Now suppose a single weight is 17.0 while everything else lives in [−1, 1]. The scale becomes s = 17 / 7 ≈ 2.43. Every weight in [−1.2, 1.2] now rounds to 0 — because one level is now 2.43 wide. The outlier hijacks the entire grid, and the rest of the layer collapses to near-ternary (just −1, 0, +1). Use the slider in Chapter 0 again and you will see this happen visually: push the outlier out and the grid ticks spread until the central spike falls between two ticks.

Two pitfalls, one root cause. Both come from forcing a uniform grid onto a non-uniform distribution. Pitfall 1 wastes resolution; pitfall 2 lets one value steal it. Bending the grid toward zero fixes both at once — that is exactly what the power exponent does.
A layer has weights in [−1,1] plus one outlier at 17. In int4 uniform quantization, what happens to a typical weight of 0.8?

Chapter 3: PowerQuant — bending the grid

We want a grid whose steps are small near zero (where weights crowd) and large far out (where weights are rare). The trick: don't change the quantizer — change the input to it. Pass each weight through a warping function t first, quantize uniformly in the warped space, and the levels, mapped back, come out non-uniformly spaced.

Q(x) = round( t(x) / s(t(X)) ),   with the transform  t(x) = sign(x)·|x|a

Why this transform and not any other? The authors imposed one hard requirement: the quantizer must preserve multiplication, because that is what neural network layers do. Formally they wanted t(x)·t(y) = t(xy). The functions satisfying that on the positive reals are exactly the power functions t(x)=xa — they are the automorphisms of (ℝ+, ×). The sign(x) wrapper just extends it to negatives. So the power function is not a guess; it is the only family that keeps the algebra intact.

What a means, intuitively. Think of a as a "zoom lens on the small stuff."
a = 1|x|1=|x|, the identity. This is exactly plain uniform quantization. No bending.
a < 1 (e.g. 0.5) → small values get magnified more than large ones, so they spread apart in warped space and receive more integer levels. The grid bends toward zero.
• The authors found a ≈ 0.5 (square root) near-optimal — and conveniently cheap, since the inverse is just squaring.

Worked example — the same near-zero weight, now with a = 0.5

Reuse the tensor from Chapter 2: max|x| = 2.0, but now warp with a = 0.5 (square root) before quantizing. First warp the range: t(2.0) = 2.0^0.5 = 1.414, so the warped scale is s = 1.414 / 7 = 0.2020.

weight xt(x)=√xt(x)/scode qdequant (s·q)1/aerror
2.0001.4147.0072.0000.000
0.0500.2241.1110.0410.009
0.4300.6563.2530.3670.063

Compare to Chapter 2. The near-zero weight 0.050 that vanished under uniform quantization (error 0.050) now lands on level 1 and survives with error 0.009 — five times better. We spent a level we used to waste in the tail to rescue the dense spike. That is the whole win of bending the grid.

python
# PowerQuant: the entire operator in 6 lines
def power_quant(x, a=0.5, bits=4):
    qmax = 2**(bits-1) - 1                 # 7 for int4
    t    = torch.sign(x) * x.abs()**a       # warp toward zero
    s    = t.abs().max() / qmax             # scale in warped space
    q    = torch.clamp(torch.round(t / s), -qmax-1, qmax)   # int code
    deq  = torch.sign(q) * (s * q.abs())**(1/a)  # unwarp on the way back
    return q, deq
What does setting the exponent a = 1 reduce PowerQuant to?

Chapter 4: The Grid — see the bend live (showcase)

This is the heart of the paper made interactive. Below is a real bell-shaped weight tensor (with an adjustable outlier). The vertical ticks are the quantization levels — the only 2b values a weight can become. Drag exponent a from 1 (uniform) down toward 0.3 and watch the levels migrate toward zero, crowding where the weights actually live. Drag bits to see how many levels you get. The red bar is the total reconstruction error — your job is to make it small.

Power exponent & the quantization grid — showcase
At a = 1 the grid is even. Lower it and levels pull inward — the dense spike near zero gets the resolution it deserves.
This is the data-free search. The "Auto-find best a" button does exactly what PowerQuant did originally: sweep a and pick the one minimizing total reconstruction error Σ∥W − Q−1(Q(W))∥2 — using only the weights, no data. It is a 1-D search, and the error is locally convex around its unique minimum, so plain Nelder-Mead finds it. The whole expensive idea fits in a button.

Play with the outlier slider while watching the error bar. With a = 1, pushing the outlier out makes the error explode — every other weight collapses to zero. With a ≈ 0.5, the bent grid keeps near-zero weights alive even as the outlier stretches the range. The bend is what tames outliers. This is precisely why the method shines on LLMs (Chapter 7).

As you lower a from 1 toward 0.3 in the simulation, what happens to the quantization levels?

Chapter 5: Learn the Weights — not just round them

So far the grid is good. But two weights that round to the same integer still have different true values — and we have only used the weights, never checked how the layer's output changes. Calibration-based methods (GPTQ / AdaRound) fix that: feed a tiny batch through, compare the quantized layer's output to the original's, and nudge the quantization to match.

AdaRound's nudge is tiny by design. For each weight it learns one number ε ∈ [0,1] that decides whether to round the floored weight up or down — a one-level wiggle, nothing more.

minε ∥ fl(Xfp, Wl) − fl( Xq, ⌊W/s⌋ + σ(ε) ) ∥2

Here fl is layer l; Xfp the original (full-precision) input to that layer; Xq the quantized input from upstream; ⌊W/s⌋ the floored integer code; and σ(ε)∈[0,1] the learnable "round up by this much." The loss says: pick rounding so the quantized layer reproduces the float layer's output.

Why this breaks on a bent grid. AdaRound assumes every "one-level wiggle" is roughly the same size. True on a uniform grid. False on PowerQuant's grid — near zero, one level is microscopic; in the tail, one level is a cliff. Allowing the same ±1 wiggle everywhere is incoherent: it can't make a meaningful correction near zero, and it makes a wild one in the tail. Naively bolting AdaRound onto PowerQuant hurts accuracy (the paper's Table 1).

NUPES's move: relearn the integer outright

Instead of learning a fractional rounding offset, NUPES learns the whole quantized integer as a single free parameter ε ∈ ℤ — initialized at the current code, but allowed to move by many levels, up or down. A near-zero weight can jump several steps to compensate; a tail weight can be held still. To keep that integer differentiable, NUPES borrows the differentiable soft quantization (DSQ) function:

dsq(ε) = (1/β)·tanh( β(ε−⌊ε⌋−½) ) / (2·tanh(β/2)) + ⌊ε⌋ + ½

Don't panic at the symbols — the picture is simple. dsq is a soft staircase: a smooth, differentiable curve that hugs the integer steps. β is a steepness knob. Small β → gentle ramps you can slide a value across between integers (gradients flow, weights move freely). Large β → sharp steps that snap ε firmly onto an integer (so the final result is a true int). NUPES schedules β: start soft to let weights migrate, end steep to lock them onto the grid.

DSQ: the soft staircase — interactive
Dashed = hard round (a true staircase). Solid = dsq. Low β is a smooth ramp (trainable); high β snaps to integers (deployable). NUPES uses a constant β=20 schedule — steep enough to behave like real rounding, gentle enough to train.
Two wins for free. Because NUPES stores only ε (the learned code) and not both the floored weight and a rounding offset, it needs half the memory during optimization and computes one loss term instead of two. On transformers — where weights dominate the memory budget — that halving is what lets the method scale to LLMs at all.
What is the key difference between AdaRound and NUPES's weight optimization?

Chapter 6: Learn the Exponent — making the quantizer trainable

PowerQuant froze one a for the whole network. But Chapter 4 showed every layer has a different distribution — the first layer is flat, deep layers are peaky. They want different bends. NUPES's second contribution: make a a per-layer trainable parameter, learned by gradient descent alongside the weights. To do that we need the gradient of xa with respect to a:

∂(xa) / ∂a = xa · log(x)

Clean on paper. A nightmare in practice. The authors hit three landmines, and the fix for each is a single line in their Algorithm 1. This is a beautiful "concept → realization" moment — the math is one line, the engineering is the contribution.

Landmine 1: log(0) = −∞
Weights near zero make log(x) blow up — the gradient becomes infinite and training crashes after ~4 steps. Fix: clip |x| to at least 1e−6 before the log (num. stability).
Landmine 2: the scale moves too
a appears in both the numerator and inside the scale s. But s can be computed analytically from the tensor range — it should not get a gradient. Fix: recompute the scale fresh each step with the new a, but block gradients through it (update scale, no ∇).
Landmine 3: batch imbalance
a gets a gradient from the weights (fixed count) and from the inputs (count = batch size). Adding them lets batch size dominate. Fix: average each contribution independently, then combine (balanced grads).
Ablation receipts (Table 4). Each fix earns its keep. On ResNet-50 W4/A4: with none of these the run crashes. Add num-stability alone → 46.6%. Add balanced grads → 59.0%. Isolate the scale update → 62.5%. On RetinaNet, isolating the scale was the decisive step — without it the score is 4.1; with it, 22.4 (a 1000%+ jump over PowerQuant's 2.24). The engineering details are not garnish; they are the whole dish.
python
# NUPES Algorithm 1: gradient for the exponent a (the three fixes)
# scales are recomputed but DETACHED — no gradient flows through them
scale_x = scale(sign(X) * X.abs()**a).detach()      # fix 2
scale_w = scale(sign(W) * W.abs()**a).detach()      # fix 2

Xc = X.abs().clamp(min=1e-6)                        # fix 1: no log(0)
Wc = W.abs().clamp(min=1e-6)                        # fix 1

g_inputs  = (Xc**a * Xc.log() / scale_x).mean()       # fix 3: average each
g_weights = (Wc**a * Wc.log() / scale_w).mean()       # fix 3
grad_a    = g_inputs + g_weights                       # then combine

The payoff (Figure 6 in the paper): the learned exponents land in a ∈ [0.213, 0.774] across layers — vindicating the old hand-picked 0.5 — but now each layer gets its own value, and the first layer reliably wants a higher a (its distribution is flatter, closer to uniform). The quantizer is no longer a hyperparameter; it is learned.

Why must the scale s be recomputed each step but with its gradient blocked?

Chapter 7: Outliers & LLMs — where the method earns its name

Everything above was warm-up for the case that actually matters in 2023: large language models. Their weight and activation distributions have a vicious feature ConvNets don't — massive outliers. The paper measures how far the most extreme value sits from the mean, in standard deviations:

Outlier reach: ResNet vs LLM — interactive
ResNets stay within ~8σ. LLM weights reach beyond 17σ — farther than int4 can even represent (max code 7). One such neuron forces the scale so wide that nearly all its other weights round to zero: an entire neuron quantized to ternary.
Why this is fatal for uniform quantization. An int4 grid has 16 levels. If the outlier is 17σ out, the scale must cover 17 standard deviations with those 16 levels — so the central ±1σ bulk, where the real information lives, falls inside a single level. The neuron's output becomes noise. This is why DFQ and SQuant collapse on LLMs (Chapter 8's table).

Now recall Chapter 4: bending the grid toward zero with a < 1 packs many fine levels into that central bulk while still reaching the outlier with the coarse outer levels. The outlier is represented; the bulk is preserved. The power transform is, almost by accident, an outlier-handling mechanism — which is why NUPES is so strong exactly where uniform methods are weakest.

The honest caveat the paper is upfront about. NUPES quantizes weights beautifully (W4/A16 on LLMs). It does not claim to fully solve activation outliers, which are even nastier and time-varying. And unlike QLoRA/OPTQ, NUPES insists on integer-only inference — no mixed float/int hardware path. That is a deliberate constraint: real speedup needs real integer matmuls, not a floating-point escape hatch.
Why does a 17σ outlier destroy a uniform int4 quantization of an LLM layer?

Chapter 8: Experiments — does it actually work?

The headline result (paper's Figure 1, Table 3): in the brutal W4/A4 setting — 4-bit weights and 4-bit activations — NUPES reaches near full-precision accuracy on both ConvNets and Transformers, where every prior method is strong on one and weak on the other. Toggle the chart below between architectures and watch the methods line up.

W4/A4 accuracy vs prior methods — interactive
Dashed line = full precision. Data-free methods (DFQ, SQuant) collapse on transformers. NUPES (learn W & a) sits closest to the ceiling everywhere.

The ablation that proves each piece matters

NUPES has two switches: learn the weights, learn the exponent. The paper turns them on one at a time (Table 3):

NUPES variantResNet-50ViT-b16RetinaNet (mAP)
learn a only62.574.622.4
learn W only68.879.632.7
learn W & a70.780.133.1
full precision76.281.037.3

Both knobs help; together they help most. The biggest single jump is learning the weights (the Chapter 5 contribution), but learning the exponent on top recovers the last few points — and on the hardest cases (RetinaNet went from PowerQuant's 2.24 to 33.1 mAP) it is the difference between a broken model and a working one.

One surprising ablation worth internalizing (Table 2). For the β schedule, the authors expected a fancy cosine schedule (inherited from AdaRound) to win. It didn't. A constant β = 20 beat every other schedule. The lesson: allowing weights to shift by more than one level is good — but it should happen rarely. A steep-but-constant staircase permits big moves only when truly warranted, and that restraint is what generalizes.
In the ablation, which single component gives the largest accuracy jump?

Chapter 9: Connections & cheat sheet

NUPES sits at the intersection of three lineages: non-uniform quantization (codebooks, log-quant), gradient-based PTQ (AdaRound, BrecQ), and LLM-specific outlier handling (LLM.int8, QLoRA, GPTQ/OPTQ). Its distinctive bet is that a single learnable scalar per layer — the power exponent — plus the freedom to relearn each integer code, beats both the rigidity of fixed grids and the fragility of full codebooks, while staying integer-only.

Uniform quant (a=1)
Even grid. Simple, hardware-native, but blind to the bell shape and helpless against outliers.
PowerQuant
Bend the grid with one global a found by data-free search. Multiplication-preserving. Best data-free method of its day.
NUPES
Per-layer a learned by SGD + relearn every integer code via DSQ + numerical-stability tricks. SOTA W4/A4 on ConvNets, ViTs, LLMs.

Cheat sheet — every symbol in one place

SymbolMeaningPlain-language analogy
Q(x)Quantize x to an integer codeSnap a real number to the nearest grid line
s(X)Scale = max|X| / (2b−1−1)How many real units one grid step is worth
bBit-widthHow many grid lines you can afford (2b)
aPower exponent, t(x)=sign(x)|x|aZoom knob: a<1 bends grid toward zero; a=1 is uniform
εLearned quantized value (NUPES) or rounding offset (AdaRound)NUPES: the new integer; AdaRound: which way to round
dsq(ε), βDifferentiable soft staircase & its steepnessA trainable ramp that hardens into real rounding
∂xa/∂a = xalog(x)Gradient that makes a learnableThe line that needs three stability fixes to not explode

Where to go next

The mastery test. You should now be able to: derive the power operator from the multiplication-preserving requirement; explain on a whiteboard why a<1 helps both the bell shape and outliers; quantize a real weight by hand under uniform and power schemes; and name the three numerical fixes that make ∂xa/∂a trainable. If a colleague asks "why not just use a lookup table?" you can answer in one sentence: it breaks integer multiplication.
In one sentence, what is NUPES's contribution over PowerQuant?