Round every weight in a 7B model down to 4 bits with naive round-to-nearest and perplexity falls off a cliff. Do the same thing but scale up ~1% of the weight channels first, and the model barely notices. That gap — same bit budget, wildly different accuracy — is the whole story of activation-aware weight quantization (AWQ), and the surprising part is which 1% of channels you have to protect.
TL;DR
- Activation-aware weight quantization (AWQ) keeps 4-bit LLMs close to FP16 accuracy by protecting a small set of salient weight channels instead of quantizing all weights equally.
- Salient channels are picked by activation magnitude, not weight magnitude — the channels that see large inputs dominate the output error.
- Instead of storing those channels in mixed precision (hardware-hostile), AWQ scales the salient weight channel up by
sand divides the matching activation bys, cutting that channel's quantization error by roughly1/s. - The scale
sis found by a cheap per-channel grid search over activation statistics from a small calibration set — no backprop, no weight reconstruction, so it doesn't overfit the calibration data. - Practical payoff:
w4a16(4-bit weights, 16-bit activations, group size 128) with ~4x smaller weights and faster memory-bound decode, at near-lossless quality. It's whatautoawqand vLLM's AWQ path ship.
Why does naive 4-bit quantization wreck LLM accuracy?
Because round-to-nearest (RTN) treats every weight as equally disposable, and they aren't. Quantizing a weight w to N bits with a group scale Δ means storing Q(w) = Δ · round(w/Δ), where Δ = max(|w|) / 2^(N-1) over the group. The per-weight rounding error is uniform on [0, Δ/2]. At 4 bits, Δ is large, so the absolute error is large.
That error only matters when it lands on a weight that contributes a lot to the output. In y = Wx, the output error from quantizing a weight in channel j is proportional to Δ · roundErr · x_j. The channels where x_j is consistently large are the ones that turn small rounding errors into large output errors. Transformer activations are famously spiky — a handful of channels carry outlier magnitudes across nearly every token. Those are the channels you cannot afford to round carelessly.
The naive fix is mixed precision: keep the salient channels in FP16 and quantize the rest to INT4. It works for accuracy and destroys throughput — a GEMM that interleaves FP16 and INT4 lanes wrecks memory coalescing and needs custom kernels. AWQ's contribution is getting the accuracy of protecting those channels without mixed precision.
How does AWQ decide which weights to protect?
By activation magnitude, measured on a calibration set — not by looking at the weights at all. This is the counterintuitive core of the paper (Lin et al., 2023). If you pick salient channels by weight magnitude, you get almost no benefit over RTN. If you pick them by the average magnitude of the activations feeding each input channel, protecting even ~1% recovers most of the lost accuracy.
The intuition: a large weight multiplied by a tiny activation contributes little; a modest weight multiplied by a persistent activation outlier contributes a lot. Error at the output is an activation-weighted quantity, so salience has to be defined in activation space.
# Per-input-channel activation importance from a small calibration set.
# X_cal: [n_tokens, in_features] activations feeding this linear layer.
import torch
def channel_importance(X_cal: torch.Tensor) -> torch.Tensor:
# Mean absolute activation per input channel; this ranks salience.
return X_cal.abs().mean(dim=0) # -> [in_features]
How does AWQ protect channels without mixed precision?
It rescales. For a salient input channel, multiply its weight column by a scale s > 1 and divide the corresponding activation by s. The product (w·s)·(x/s) = w·x is mathematically unchanged in full precision, but the quantization behaves differently.
Here's why it helps. After scaling, the group's Δ is set by max(|w·s|). If you scale only a few channels and the group maximum is dominated by other channels, Δ barely moves. The quantized contribution of the scaled channel becomes Δ · roundErr · (x/s) — the same Δ, the same rounding error, but the activation term is now divided by s. Net effect: that channel's output error shrinks by roughly 1/s. You've spent the model's fixed 4-bit budget disproportionately on the channels that matter.
The trade-off is that pushing s too high inflates max(|w·s|), which raises Δ for the whole group and hurts everyone else. So s is not free — it's a per-channel knob you tune.
# AWQ scale search: s = act_importance ** alpha, grid-search alpha.
# W: [out_features, in_features], sx: activation importance [in_features]
def search_scale(W, X_cal, sx, quantize_fn, n_grid=20):
best_err, best_s = float("inf"), None
y_ref = X_cal @ W.T # FP16 reference output
for i in range(n_grid):
alpha = i / n_grid # 0 -> no scaling, 1 -> full
s = sx.clamp(min=1e-4) ** alpha # per-input-channel scale
Ws = W * s.view(1, -1) # scale weight columns up
Wq = quantize_fn(Ws) / s.view(1, -1) # quantize, then fold s back
y = X_cal @ Wq.T
err = (y - y_ref).pow(2).mean().item()
if err < best_err:
best_err, best_s = err, s
return best_s
Two things worth noting. First, alpha in [0, 1] interpolates between "don't scale" and "scale by full activation importance"; the optimum is usually interior, so the grid search matters. Second, the objective is plain output MSE against the FP16 reference — no gradients, no Hessians, no iterative reconstruction. One pass over a small calibration set per layer.
How is AWQ different from GPTQ?
GPTQ is reconstruction-based; AWQ is scaling-based. GPTQ quantizes weights column by column and uses second-order (Hessian) information to update the remaining unquantized weights so they compensate for the error already introduced. It's accurate but expensive, and because it fits weights to minimize error on the calibration set, it can overfit that set — quality on out-of-distribution or instruction-tuned workloads can degrade.
AWQ never updates weights beyond a per-channel multiply. It searches for scales that minimize output error and stops. That has three consequences:
- Data efficiency. It needs a small calibration set and is far less sensitive to what's in it, because a per-channel scalar can't memorize the calibration distribution the way full weight reconstruction can.
- Generalization. The authors report it holds up across domains and on instruction-tuned models where reconstruction methods can wobble.
- Simplicity and speed. No Hessian inversion, no backward pass. Quantizing a model is fast, and the folded scales leave you with a clean INT4 weight matrix plus a per-channel scale vector — trivially kernel-friendly.
Neither dominates universally. GPTQ's error compensation can edge ahead in some settings, and modern toolchains support both. But AWQ's robustness on real, instruction-following models is why it became a default for served 4-bit checkpoints.
What do you actually deploy — and where does it pay off?
The common target is w4a16: 4-bit weights, 16-bit activations, group size 128. Activations stay in FP16/BF16; only the weights are compressed. In practice:
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
model_path = "your/instruct-model"
model = AutoAWQForCausalLM.from_pretrained(model_path)
tok = AutoTokenizer.from_pretrained(model_path)
quant_config = {
"zero_point": True,
"q_group_size": 128, # scale/zero shared per 128 weights
"w_bit": 4,
"version": "GEMM", # kernel variant
}
# Calibration is a few hundred short sequences — small and cheap.
model.quantize(tok, quant_config=quant_config)
model.save_quantized("your/instruct-model-awq")
The win shows up during decode. Autoregressive generation is memory-bandwidth-bound — each token streams the entire weight matrix from HBM. Cut the weights from 16 bits to 4 and you move roughly a quarter of the bytes per token, which is why AWQ INT4 improves decode throughput and shrinks the model to fit smaller GPUs, not just saves disk. Group size 128 is the usual accuracy/overhead balance: finer groups (smaller Δ per group) recover a bit more accuracy at the cost of storing more scales.
Where it doesn't help: prefill of long prompts is compute-bound, so 4-bit weights don't speed that up much, and since activations stay FP16, AWQ alone doesn't shrink the KV cache — that's a separate axis (and a separate accuracy cliff). Pair AWQ weight quantization with a KV-cache strategy if long-context memory is your bottleneck.
Does AWQ keep 4-bit LLMs accurate?
Yes — that's the point of activation-aware weight quantization. The reason 4-bit models usually degrade is that round-to-nearest spends its tiny bit budget uniformly, wasting precision on channels that barely affect the output while under-protecting the ~1% of channels that see activation outliers. AWQ identifies those salient channels by activation magnitude, scales them up before quantizing (and folds the inverse scale into the activation), and thereby cuts their output error by roughly 1/s without any mixed-precision storage. Because it only searches per-channel scales against an FP16 reference — no weight reconstruction — it stays cheap to compute and doesn't overfit the calibration set, which is why w4a16 AWQ checkpoints hold near-FP16 quality on real instruction-tuned models and ship as a default in autoawq and vLLM.
Top comments (0)