Skip to content

Quantization: INT8, INT4, GPTQ, AWQ, and GGUF

8 min read

Raw storage for 70 billion weights

70 × 10⁹ × 16/8 bytes = 140 GB

Common scale: 0–140 decimal GB. Payload only; metadata, mixed-precision tensors, runtime buffers, and KV cache add memory. A bit count alone does not specify a quantizer or its quality.

A 70-billion-parameter model needs roughly 140 GB just for raw 16-bit weights. Four-bit storage reduces that payload to 35 GB. The practical questions are what else the format stores, how much the approximation changes the model, and whether the runtime can use it efficiently.

Quantization replaces values with a smaller set of representable values. Uniform integer grids are a useful starting point, though floating-point formats and nonuniform codebooks make different choices.

A scale turns integers back into weights

For a symmetric bb-bit example, choose qmax⁡=2b−1−1q_{\max}=2^{b-1}-1 and

s=max⁡i∣wi∣qmax⁡,qi=clip⁡(round⁡(wi/s),−qmax⁡,qmax⁡),w^i=sqi.s=\frac{\max_i|w_i|}{q_{\max}},\qquad q_i=\operatorname{clip}\left(\operatorname{round}(w_i/s),-q_{\max},q_{\max}\right),\qquad \hat w_i=sq_i.

At four bits, this symmetric construction uses codes −7 through 7: fifteen levels, with one of the sixteen possible bit patterns unused. Other schemes use an asymmetric range and a zero point. An all-zero group needs special handling so that its scale is not used as a zero divisor.

Without clipping, rounding to a uniform grid introduces at most half a grid step of absolute error. That bound says little about the downstream model: different weights have different effects, and errors accumulate through many layers.

bits/weight:
scale group:
-3-2-10123-2.80+2.80synthetic weights · 32×128 tensorgrey = original · amber = snapped to INT4 at one scale per row

Scroll horizontally to inspect the diagram.

Bar height counts weights. Both distributions use the same 0–877 scale for this selection; the height scale changes with the controls.

bit width
INT4
15 symmetric levels
rmse
0.0431
lower = better
max error
0.198
worst single weight
range ratio
29%
99th pct / |w|max

A deterministic synthetic weight distribution, quantized with a symmetric uniform grid. Both histograms use the same count scale; concentrated quantized bins can be taller than the original bins.

FP16 itself also has finitely many representable values. Its spacing changes with magnitude, unlike the uniform integer grid shown here.

Outliers can spend the available range

If most values are near zero but a few are much larger, a single scale must cover those extremes. Small values then share relatively coarse grid steps. Using separate scales for groups can reduce this problem, at the cost of storing more scales and handling more groups.

Constructed 2 × 12 tensor. One value is 12; others lie in [-1, 1]. Color uses one fixed ±12 scale. The selected column is held unchanged in this calculation; FP16 rounding is not simulated. This mixed-precision example is not AWQ.

row 0one scale ≈ 1.714 · bin step is huge
0.00
Δ 0.10
0.10
0.00
Δ -0.40
-0.40
0.00
Δ 0.22
0.22
0.00
Δ 0.05
0.05
0.00
Δ -0.18
-0.18
0.00
Δ 0.31
0.31
12.00
Δ 0.00
12.00
0.00
Δ -0.05
-0.05
0.00
Δ 0.12
0.12
0.00
Δ 0.28
0.28
0.00
Δ -0.22
-0.22
0.00
Δ 0.06
0.06
row 1one scale ≈ 1.714 · small weights snap to 0
0.00
Δ -0.10
-0.10
0.00
Δ 0.30
0.30
0.00
Δ -0.05
-0.05
0.00
Δ 0.18
0.18
0.00
Δ 0.22
0.22
0.00
Δ -0.11
-0.11
0.00
Δ 0.08
0.08
0.00
Δ 0.14
0.14
0.00
Δ -0.02
-0.02
0.00
Δ 0.09
0.09
0.00
Δ -0.19
-0.19
0.00
Δ 0.11
0.11
scheme
one scale for the whole tensor
row 0 rmse
0.203
row 1 rmse
0.152
rest-of-row-0 rmse
0.212
row 0 excluding column 6

A constructed matrix compares tensor-wide quantization, group scales, and keeping one outlier column at higher precision. The mixed-precision example is not AWQ.

Outliers in activations and outliers in weights are related but distinct problems. LLM.int8() uses a mixed-precision decomposition for activation outlier dimensions. Keeping selected values at high precision can help, but it also complicates execution. A format needs a kernel that supports its layout efficiently.

GPTQ protects layer outputs

Rounding weights independently minimizes a local weight error. GPTQ instead uses calibration activations to approximate how a layer's output changes. For a weight row and calibration matrix XX, the reconstruction objective has the form

∥(w−w^)X∥22.\left\|(w-\hat w)X\right\|_2^2.

Correlations in XXTXX^T matter: changing another unquantized weight can compensate for some output error from a weight just rounded. GPTQ uses damped second-order information to choose these updates efficiently.

Sequential error compensation on a constructed quadratic

One row, INT3 scale 0.1833. The calibration input is all ones; damping λ = 0.1 adds a weight-error penalty. This small example illustrates GPTQ’s inverse-Hessian correction.

w[0] free0.42 → 0.420
w[1] free-0.18 → -0.180
w[2] free0.31 → 0.310
w[3] free-0.55 → -0.550
w[4] free0.09 → 0.090
w[5] free0.27 → 0.270
w[6] free-0.12 → -0.120
w[7] free0.38 → 0.380

Move the slider to quantize one weight and compensate using the remaining weights.

Final rounding objective
0.06676
Final compensated objective
0.00767

Objective: (sum of weight errors)² + 0.1 × sum of squared weight errors. These are computed toy losses, not model perplexities or a guarantee that compensation always helps.

A small computed example with H = 11ᵀ + 0.1I. Quantizing a weight redistributes its residual according to the remaining quadratic objective. The displayed loss is this toy objective, not model perplexity.

Minimizing this layer-output objective can increase ordinary weight RMSE: the calibration activations determine which errors matter most.

AWQ rescales important channels

AWQ uses activation statistics to identify weight channels worth protecting. It scales selected weight channels up before quantization and compensates by scaling the corresponding input activations down. For a diagonal scale matrix SS,

WX=(WS)(S−1X).WX=(WS)(S^{-1}X).

Before quantization, these are the same product. Quantizing the rescaled weights changes which errors the integer grid introduces. AWQ searches for useful scales while retaining a low-bit weight representation; it does not simply store its important weights in FP16.

SmoothQuant addresses activation outliers by moving difficulty from activations into weights through an equivalent rescaling, with a different objective.

Bit width is not a quality score

Computed reconstruction error on eight toy weights

0.12 → 0.200
-0.31 → -0.400
0.08 → 0.000
0.77 → 0.800
-0.64 → -0.600
0.26 → 0.200
1.40 → 1.400
-0.19 → -0.200

Scale 0.2000; RMSE 0.0582.

Weight reconstruction error is not perplexity or task accuracy. Comparing GPTQ, AWQ, and rounding requires the same model, calibration data, grouping, tokenizer, and evaluation protocol.

Change the grid width and inspect the reconstruction error of eight stated weights. The RMSE is calculated from those values; it is not an empirical model-quality curve.

Four bits means sixteen possible bit patterns, not half the accuracy of eight bits. To evaluate a quantized checkpoint, compare the same base model, tokenizer, prompts, context lengths, and evaluation protocol. Measure the tasks that matter to the application, including long-context or structured-output behavior when relevant.

Perplexities from different datasets or tokenizers cannot establish a universal ranking of quantization methods. A small average change can also hide a larger change on a particular task.

70 billion parameters: raw payload only, decimal GB

16-bit payload140 GB
8-bit payload70 GB
4-bit payload35 GB
2-bit payload17.5 GB

Add scales, zero points, higher-precision tensors, padding, runtime workspace, and the KV cache before deciding whether a checkpoint fits.

Raw payload for a rounded 70B parameter count. Scales, zero points, higher-precision tensors, alignment, runtime buffers, and KV cache are additional memory.

Storage savings and execution speed

A W4A16 kernel stores weights at four bits while consuming 16-bit activations. It may unpack and dequantize weights into a floating-point computation; it is not necessarily an integer matrix multiply. W8A8 and floating-point low-precision kernels have different hardware paths.

Reducing weight traffic can help a bandwidth-limited decode step. It does not imply a fourfold end-to-end speedup: dequantization, kernel efficiency, batching, attention, KV traffic, and host overhead still contribute. A representation that saves memory can even run slower on an unsupported or poorly matched backend.

The KV cache has its own dtype and scaling scheme. Quantizing weights alone does not reduce KV payload, and a runtime's dtype setting does not automatically produce a valid quantized checkpoint.

GGUF describes more than a bit count

GGUF is a container used by llama.cpp and related tools. The tensors inside can use different quantization layouts. Block metadata means a nominal four-bit scheme occupies more than four bits per weight.

GGML tensor blocks: payload plus metadata

Q4_0: 32 weights4.5 bits/weight

16 payload bytes + 2-byte scale

Q8_0: 32 weights8.5 bits/weight

32 payload bytes + 2-byte scale

Q4_K: 256 weights4.5 bits/weight

128 payload bytes + 16 bytes of scales/minima

Q6_K: 256 weights6.5625 bits/weight

192 payload bytes + 18 bytes of scales

Q4_K_M is a model-level recipe mixing tensor types; it does not have one fixed bits-per-weight value. These storage calculations do not predict model quality.

Actual block payload sizes from ggml's type definitions. Q4_K_M is a model-level mixture of tensor types, so it has no single exact bits-per-weight value.

For example, Q4_0 stores 32 weights in 18 bytes, including its scale: 4.5 bits per weight. Q4_K also averages 4.5 bits per weight within its 256-weight superblock, using a different arrangement of scale information. Equal storage density does not make the two quantizers equivalent.

Try the rounding rule

import numpy as np
 
weights = np.array([-0.53, -0.21, -0.08, 0.0, 0.06, 0.17, 0.31, 0.55])
 
def quantize_symmetric(values, bits):
    if not isinstance(bits, int) or not 2 <= bits <= 8:
        raise ValueError("This int8-backed example supports 2 through 8 bits")
    qmax = 2 ** (bits - 1) - 1
    maximum = np.max(np.abs(values))
    scale = maximum / qmax if maximum else 1.0
    codes = np.clip(np.rint(values / scale), -qmax, qmax).astype(np.int8)
    return codes, scale, codes.astype(float) * scale
 
codes, scale, restored = quantize_symmetric(weights, 4)
print("codes:", codes)
print("scale:", scale)
print("RMSE:", np.sqrt(np.mean((weights - restored) ** 2)))
assert np.max(np.abs(weights - restored)) <= scale / 2 + 1e-12
assert np.all(quantize_symmetric(np.zeros(8), 4)[2] == 0)

The NumPy codes above are stored as eight-bit integers for convenience; this teaching example does not pack two four-bit codes into one byte. Real formats must specify that packing and their metadata.

When one device still cannot meet the memory or latency requirement, the next option is parallelism across devices.

Additional reading