Intuition What this page is
The parent note gave you the formulas. Here we exercise every corner of them: tiny sequences, huge batches, the degenerate "all-zero weights" case, the limit where quantization error vanishes, a real deployment sizing problem, and an exam twist that punishes linear thinking. Each example tells you which cell of the scenario matrix it fills.
Before any numbers, one reminder of the two formulas everything below leans on:
Definition Conventions used on this page (read once)
b = bit-width : bits per stored weight. It sets the number of integer codes to 2 b ; the number of gaps between the smallest and largest code is 2 b − 1 (hence that appears in s ).
"Levels" on this page always means the number of gaps/intervals between representable ticks, 2 b − 1 — the quantity in the denominator of s . Why gaps, not ticks? Because s is a distance (real units per step), and n ticks along a ruler leave only n − 1 steps between them. See the fence-post figure below.
round ( ⋅ ) = round-half-up (ties go to the larger integer), the convention we use throughout; a different tie rule can shift a borderline q by 1 , so it must be fixed. The tie-breaking figure below shows exactly when this bites.
Memory units : 1 MB = 102 4 2 bytes (mebibyte), 1 GB = 102 4 3 bytes (gibibyte). Every "GB" and "MB" below means the binary (GiB/MiB) unit that GPUs report, unless a step explicitly divides by a power of 1000 .
2 b − 1 gaps", not "2 b levels" — the fence-post picture
Think of the representable values as fence posts and the scale s as the length of one fence panel between two posts. If you plant 2 b posts, you build exactly 2 b − 1 panels. The range r ma x − r min is the total fence length, so one panel is s = ( r ma x − r min ) / ( 2 b − 1 ) . Counting posts (2 b ) instead of panels (2 b − 1 ) is the single most common off-by-one in quantization — the figure shows both counts side by side.
Intuition What the zero-point
z actually does — the alignment picture
The integer axis q = 0 , 1 , 2 , … is just evenly spaced ticks. The zero-point z is a shift that decides which integer tick sits exactly on real value 0.0 . Look at the figure: without z the grid starts at r min and 0.0 may fall between ticks (it wouldn't reconstruct exactly). Setting z = round ( − r min / s ) slides the whole integer ruler left/right so that q = z lands precisely on r = 0 . That is why r ^ = s ( q − z ) : subtract z to measure "how many steps away from zero", then scale. Geometrically z is the origin of the integer grid expressed in real units .
Every serving/quantization question is really one of these cells. The examples below are labelled [C1]…[C10] so you can see the whole space is covered. The figure gives the same map at a glance.
Cell
Case class
What breaks / what's tested
C1
KV cache — small/typical
plug-in sanity, unit tracking
C2
KV cache — batch scaling limit
when does it OOM? linear-in-B growth
C3
KV cache — degenerate S = 1
prefill of a 1-token prompt, tail-block waste
C4
Paged vs naive waste
fragmentation arithmetic, block-size choice
C5
Quantize — interior value
normal rounding, bounded error
C6
Quantize — boundary / clip
value at/over r ma x saturates
C7
Quantize — degenerate range r min = r ma x
division by zero, the flat-tensor case
C8
Quantize — limit b → ∞
error → 0 , why more bits help nonlinearly
C9
Real-world word problem
"how many users fit?" full GPU budget
C10
Exam twist
INT4 vs INT8 error ratio — beat the linear trap
[C1] KV cache for a small model — get the units right
A 6-layer model, d m o d e l = 512 , FP16. Serve 1 request of 128 tokens. How many megabytes of KV cache?
Forecast: guess — kilobytes, or megabytes? (It surprises people how small a small model is.)
Identify each symbol: B = 1 , S = 128 , L = 6 , d m o d e l = 512 , p = 2 .
Why this step? Every mistake here is a wrong symbol, not wrong math — name them first.
Multiply per the formula: 2 ⋅ 1 ⋅ 128 ⋅ 6 ⋅ 512 ⋅ 2 = 1 , 572 , 864 bytes.
Why this step? Direct plug-in; the leading 2 is K+V, the trailing 2 is bytes/FP16-element — two different 2's , don't merge them.
Convert: 1 , 572 , 864/102 4 2 = 1.5 MiB.
Why this step? MB here means mebibyte (102 4 2 bytes) — the unit GPUs report; state it so "MB" is never ambiguous.
Verify: halve the precision to INT8 (p = 1 ) → should halve to 0.75 MiB. It does. Units: [ — ] ⋅ [ — ] ⋅ [ tok ] ⋅ [ layer ] ⋅ [ val/tok/layer ] ⋅ [ byte/val ] = bytes. ✓
[C2] KV cache — batch scaling until OOM
Same as parent 's Llama-2-13B (L = 40 , d m o d e l = 5120 , FP16), S = 4096 per request. A GPU has 40 GiB free for KV cache. What is the largest batch B that fits?
Forecast: the parent said 1 request ≈ 3.1 GB. So… 12? 13?
Per-request KV: 2 ⋅ 1 ⋅ 4096 ⋅ 40 ⋅ 5120 ⋅ 2 = 3 , 355 , 443 , 200 bytes. In gibibytes: 3 , 355 , 443 , 200/102 4 3 = 3.125 GiB.
Why this step? B is a clean linear multiplier — solve for one request in the same unit as the budget (GiB), then divide. Note: in decimal GB (/100 0 3 ) this is 3.355 GB — different number, so we lock everything to GiB.
B ma x = ⌊ 40/3.125 ⌋ = ⌊ 12.8 ⌋ = 12 .
Why this step? You cannot fit a fractional request; floor, don't round.
Leftover: 40 − 12 × 3.125 = 2.5 GiB idle — not enough for a 13th.
Why this step? Confirms the floor was necessary and shows the "wasted tail" of the budget.
Verify: 12 × 3.125 = 37.5 ≤ 40 ✓ and 13 × 3.125 = 40.625 > 40 ✗. The boundary is respected exactly. This is precisely why quantizing the weights (freeing HBM) lets you push B higher.
[C3] Degenerate input — a 1-token sequence
Same 13B model, but a request with S = 1 (just the BOS token, generation not yet started). KV bytes? And with a PagedAttention block_size=16, how much is allocated ?
Forecast: basically zero cache… but a whole block gets reserved. How much of it is wasted?
KV for S = 1 : 2 ⋅ 1 ⋅ 1 ⋅ 40 ⋅ 5120 ⋅ 2 = 819 , 200 bytes ≈ 0.78 MiB.
Why this step? Sanity floor — the smallest non-empty case, checks the formula doesn't blow up at S = 1 .
Paging allocates whole blocks. 1 token needs 1 block of 16 → 16 tokens' worth reserved: 2 ⋅ 16 ⋅ 40 ⋅ 5120 ⋅ 2 = 13 , 107 , 200 bytes ≈ 12.5 MiB.
Why this step? Blocks are the unit of allocation — you can't half-fill hardware; this is the tail-block cost.
Wasted fraction of that block: ( 16 − 1 ) /16 = 93.75% of one block.
Why this step? Shows the worst-case tail waste is bounded by block_size-1 tokens — exactly the parent's claim, made concrete.
Verify: the wasted bytes = ( 16 − 1 ) × ( 2 ⋅ 40 ⋅ 5120 ⋅ 2 ) = 12 , 288 , 000 ≈ 11.7 MiB, and 819 , 200 + 12 , 288 , 000 = 13 , 107 , 200 = the block size. ✓ The parts add to the whole.
[C4] Paged vs naive fragmentation — the whole point of vLLM
max_len = 2048, block_size = 16. A request actually uses 100 tokens. Compare naive waste vs paged waste (in tokens).
Figure C4 — Two horizontal bars. Top ("Naive"): a small green "used = 100" segment followed by a huge light-red "wasted = 1948" segment out to 2048. Bottom ("Paged"): the same green 100 plus a tiny yellow 12-token tail, with blue vertical lines marking the 16-token block boundaries. The visual point: the red naive-waste bar dwarfs the yellow paged tail.
Forecast: naive reserves 2048, uses 100. Paged only reserves blocks. Guess the ratio.
Naive waste = max_len − actual = 2048 − 100 = 1948 tokens.
Why this step? Naive reserves the contiguous max up front — the light-red bar in the figure.
Paged: blocks needed = ⌈ 100/16 ⌉ = 7 blocks = 112 token-slots.
Why this step? Ceiling because the 100th token still needs a 7th block (holds tokens 97–112).
Paged waste = 112 − 100 = 12 tokens (the yellow tail in the figure).
Why this step? Only the final partial block is under-full — this is the ≤ block_size − 1 bound.
Verify: 12 ≤ 16 − 1 = 15 ✓. Waste reduction factor = 1948/12 ≈ 162 × less wasted memory for this request. Look at the figure: the red naive bar dwarfs the tiny yellow paged tail.
[C5] Quantize an interior weight to INT4 — normal case
Weight tensor range [ − 0.6 , 0.9 ] , bit-width b = 4 . Quantize and dequantize r = 0.35 .
Figure C5 — A real-number line from −0.6 to 0.9 dotted with yellow ticks (the 16 representable values, one every s=0.1). The red dot marks the true weight 0.35; a red arrow snaps it down to the nearest yellow tick, the blue dot at 0.4 (code q=10). The gap between them, labelled "error = 0.05 = s/2", is the whole story of information loss.
Forecast: we lose precision. Guess the error magnitude before computing (hint: half a step).
Ticks (representable codes) = 2 b = 16 ; gaps between them = 2 4 − 1 = 15 . Scale s = ( 0.9 − ( − 0.6 )) /15 = 1.5/15 = 0.1 .
Why this step? s = "real units per gap". We divide by the number of gaps (15), not the number of ticks (16) — 16 fence-posts have 15 spaces between them (recall the fence-post figure). s sets how coarse the grid in the figure is.
Zero-point z = round ( − ( − 0.6 ) /0.1 ) = round ( 6 ) = 6 .
Why this step? z is the integer that maps back to real 0.0 ; it aligns the grid so 0 lands on an exact tick (the alignment figure).
q = clip ( round ( 0.35/0.1 ) + 6 , 0 , 15 ) = clip ( round ( 3.5 ) + 6 , 0 , 15 ) . With round-half-up, round ( 3.5 ) = 4 , so q = 4 + 6 = 10 .
Why this step? Round to nearest tick (minimum error); the tie rule (half-up) matters at exactly 3.5 — it is what sends us to 4 not 3 . Then shift by z ; 10 is inside [ 0 , 15 ] so no clip.
Dequantize r ^ = 0.1 ( 10 − 6 ) = 0.4 . Error = ∣0.4 − 0.35∣ = 0.05 .
Why this step? r ^ is where the red dot snaps to on the grid; error is the gap.
Verify: error 0.05 = s /2 = 0.1/2 ✓ — exactly the half-step bound, so rounding behaved. In the figure the red dot at 0.35 snaps to the nearest yellow tick at 0.4.
[C6] Boundary & clip case — an outlier weight
Same setup (s = 0.1 , z = 6 , range [ − 0.6 , 0.9 ] ). Quantize (a) r = 0.9 exactly, and (b) an outlier r = 1.4 .
Forecast: 0.9 is the top of range — which integer? And 1.4 is outside — what stops overflow?
(a) q = round ( 0.9/0.1 ) + 6 = 9 + 6 = 15 ; in [ 0 , 15 ] , no clip. r ^ = 0.1 ( 15 − 6 ) = 0.9 , error 0 .
Why this step? Range endpoints land exactly on the top/bottom tick — the design intent; zero error there.
(b) round ( 1.4/0.1 ) + 6 = 14 + 6 = 20 , which exceeds 15 .
Why this step? This is the overflow the parent warned about — an outlier wants a tick that doesn't exist.
clip forces q = 15 . r ^ = 0.1 ( 15 − 6 ) = 0.9 . Error = ∣0.9 − 1.4∣ = 0.5 .
Why this step? Saturation, not wrap-around: the outlier caps at the max representable value.
Verify: the clipped error 0.5 ≫ s /2 = 0.05 ✓ — this is why AWQ/GPTQ protect outlier channels: a single un-clipped-badly weight can carry 10 × the normal error. Endpoint 0.9 giving error 0 confirms the grid is anchored correctly. See GPTQ and AWQ and Quantization Fundamentals .
[C7] Degenerate range — a constant (flat) tensor
A tensor where every weight equals 0.3 , so r min = r ma x = 0.3 . Quantize to INT4. What happens to s ?
Forecast: s = ( 0.3 − 0.3 ) /15 = 0/15 = 0 … and then we divide by s . Uh oh.
Naïve scale: s = ( r ma x − r min ) /15 = 0 .
Why this step? Expose the trap — a constant tensor has zero dynamic range, so the formula divides by zero next.
q = round ( r / s ) divides by zero → undefined . The real implementation guards this.
Why this step? Every quantizer has this edge case; you must handle it, not crash.
Standard fix: when the range is 0 , clamp the scale to a small positive floor s ← max ( s , ε ) with a fixed ε , and set z = clip ( round ( − r min / s ) , 0 , 2 b − 1 ) . Real frameworks use a tiny default (PyTorch/ONNX use ε ≈ 2 − 12 ≈ 2.4 × 1 0 − 4 ); the exact value does not matter because a constant tensor carries no information — one code suffices to reconstruct it exactly. For readability of the arithmetic only (not a realistic default) take the illustrative value ε = 0.1 : then z = clip ( round ( − 0.3/0.1 ) , 0 , 15 ) = clip ( − 3 , 0 , 15 ) = 0 , q = clip ( round ( 0.3/0.1 ) + 0 , 0 , 15 ) = 3 , r ^ = 0.1 ( 3 − 0 ) = 0.3 — exact .
Why this step? The ε -floor is the documented convention precisely so a flat/dead channel never divides by zero and reconstructs its constant value exactly. The chosen ε = 0.1 is a round number for hand-calculation; any positive floor gives the same exact reconstruction of a constant.
Verify: with the ε -floor s = 0.1 : q = 3 ∈ [ 0 , 15 ] is valid, no division by zero, and r ^ = 0.3 = r exactly ✓. Re-run with the realistic ε = 2 − 12 : z = clip ( round ( − 0.3 ⋅ 4096 ) , 0 , 15 ) = 0 , q = clip ( round ( 0.3 ⋅ 4096 ) , 0 , 15 ) = 15 (clipped), r ^ = 2 − 12 ( 15 − 0 ) ≈ 0.00366 = 0.3 — so a tiny ε does not reconstruct a large constant! The honest lesson: production quantizers special-case a constant channel by storing the value directly, not by trusting s → 0 . Always test r min = r ma x — pruned/dead channels really do go flat.
[C8] Limiting behaviour — what happens as bits → ∞
Fixed range [ − 1 , 1 ] . Compute the step s and the worst-case error bound s /2 for b = 4 , 8 , 16 . What does the limit tell you?
Forecast: more bits = less error. But how fast does error shrink per bit?
s ( b ) = 2 b − 1 1 − ( − 1 ) = 2 b − 1 2 . Worst error = s /2 = 2 b − 1 1 .
Why this step? The error bound is purely a function of the number of gaps (2 b − 1 ); more gaps, finer grid.
b = 4 : 1/15 ≈ 0.0667 ; b = 8 : 1/255 ≈ 0.00392 ; b = 16 : 1/65535 ≈ 1.5 × 1 0 − 5 .
Why this step? Concrete numbers show error doesn't halve per bit — it drops by roughly × 2 Δ b .
Limit: as b → ∞ , 2 b − 1 → ∞ so error → 0 — FP16 is just quantization with a huge level count.
Why this step? Frames FP16 vs INT4 as the same idea at different resolutions; the trade is memory (bytes/element) vs error.
Verify: ratio b = 4 to b = 8 error: ( 1/15 ) / ( 1/255 ) = 255/15 = 17 ✓ — INT4 error is ~17× INT8's, not 2× (foreshadows [C10]). Error is monotonically decreasing in b ✓.
[C9] Real-world word problem — "how many users can this box serve?"
An A100-80GB (treat as 80 GiB usable). The FP16 Llama-2-13B weights take 26 GiB . You quantize to INT4 (W4A16), shrinking weights to ~6.5 GiB . Reserve 4 GiB for activations/overhead. Each user's KV cache (from [C2]) is 3.125 GiB at S = 4096 , FP16. How many more concurrent users does quantizing the weights unlock?
Forecast: freeing ~19.5 GiB of weight memory — how many 3.125 GiB users is that?
KV budget, FP16 weights: 80 − 26 − 4 = 50 GiB. Users = ⌊ 50/3.125 ⌋ = ⌊ 16.0 ⌋ = 16 .
Why this step? Subtract the fixed costs (weights + overhead) first; what's left is the KV pool. All terms in GiB so they add cleanly.
KV budget, INT4 weights: 80 − 6.5 − 4 = 69.5 GiB. Users = ⌊ 69.5/3.125 ⌋ = ⌊ 22.24 ⌋ = 22 .
Why this step? Same subtraction with the smaller weight footprint — the freed HBM becomes KV room.
Extra users unlocked = 22 − 16 = 6 .
Why this step? The deliverable — quantization's payoff here is batch size , hence throughput (Throughput vs Latency Tradeoffs ).
Verify: 16 × 3.125 = 50.0 ≤ 50 ✓ (exactly full) and 17 × 3.125 = 53.125 > 50 ✗; 22 × 3.125 = 68.75 ≤ 69.5 ✓ and 23 × 3.125 = 71.875 > 69.5 ✗. Note the win is memory-driven, not arithmetic-driven — see the parent's mistake box on W4A16 dequant. Related: GPU Memory & HBM Bandwidth , Batching Strategies .
[C10] Exam twist — beat the linear trap
"INT8 uses twice the bits of INT4, so INT4 should be about twice as lossy." True or false, with numbers. (Range [ − 1 , 1 ] .)
Forecast: it feels like 2×. Is it?
The denominator of s is the number of gaps 2 b − 1 , not the bit-width b : INT8 has 255 gaps, INT4 has 15 .
Why this step? The whole trap is confusing "bits" (linear, 8 vs 4 ) with "gaps between ticks" (exponential, 2 b − 1 ).
Error ∝ s ∝ 1/ ( 2 b − 1 ) . Ratio = ( 2 8 − 1 ) / ( 2 4 − 1 ) = 255/15 = 17 .
Why this step? Error is set by step size, which shrinks exponentially in bit-width — so the true factor is 17×, not 2×.
Conclusion: False. INT4 is ~17 × lossier per step than INT8, which is exactly why 4-bit needs group-wise scales + outlier protection (GPTQ and AWQ ) to stay usable.
Why this step? Connects the number to real engineering: the nonlinearity is why AWQ/GPTQ exist.
Verify: 255/15 = 17 ✓ (an integer, satisfyingly). Sanity: doubling bits squares-ish the level count (2 8 = ( 2 4 ) 2 = 256 ), so error dropping ~17× not 2× is consistent. ✓
Recall Every cell, one line each
Per-request KV for 13B at S = 4096 , FP16 ::: ≈ 3.125 GiB.
Largest batch in a 40 GiB KV pool at that size ::: 12 (floor of 12.8).
Paged vs naive waste for 100 tokens, max_len 2048, block 16 ::: 12 tokens vs 1948 tokens (~162× less).
Quantizing r = 0.35 in [ − 0.6 , 0.9 ] INT4 gives r ^ and error ::: r ^ = 0.4 , error 0.05 = s /2 .
What guards the r min = r ma x case ::: floor the scale to a small positive ε (or store the constant directly) so you never divide by zero.
True INT4-vs-INT8 error ratio for the same range ::: ~17× (not 2×), because gaps are 2 b − 1 .
Why s divides by 2 b − 1 not 2 b ::: s is a step length; 2 b ticks leave 2 b − 1 gaps (fence-post rule).
What the zero-point z does geometrically ::: shifts the integer grid so tick q = z lands exactly on real value 0.0 .
Mnemonic "Bits are linear, gaps are exponential."
Add one bit → double the ticks → halve the step. Four bits between INT4 and INT8 → 2 4 = 16 × -ish (exactly 255/15 = 17 × ) less error. Never reason about quantization loss linearly in bit-count.