Skip to content

Bridges: DSA to LLMs

Bit manipulation to quantization

Squeezing models into fewer bits

A model's weights are 32-bit floats. Quantization is the bit manipulation turned on those weights - snap each one to a coarse integer grid, pack several into a word, and a datacentre model fits on one GPU.

A trained network is millions or billions of numbers, each stored as a 32-bit float - 4 bytes of sign, exponent, and mantissa buying about seven decimal digits of precision. But inference barely notices that precision. So we quantize: throw away most of the bits, keep a few. Pick a range, chop it into a handful of evenly spaced rungs, and store each weight as the index of its nearest rung - an int8 or an int4. That index is a plain integer, and integers pack: eight int4 codes slide into one 32-bit word using the exact shift, mask, and OR you learned as bit tricks. The reward is 4-8× less memory, for a sliver of accuracy.

The idea in one line

A float can name almost any real number on a fine continuum. Quantization replaces that continuum with a ruler that has only 16 (int4) or 256 (int8) rungs, then stores each weight as which rung it's closest to. Reading it back means multiplying the rung index by a step size. You lose the wobble between rungs - the quantization error - but you trade 32 bits per weight for 4 or 8, and low-bit integers pack tightly into machine words. Same masks and shifts as a bitset; new payload.

Continuous weights, a handful of rungs

Fixed vs floating point is really a question of where the bits go. A float spends its 32 bits on a movable decimal point - huge dynamic range, fine resolution near zero. An integer spends all its bits on magnitude, so to represent a fractional weight you must agree on a scale: one integer step equals some fixed real amount. Choose fewer bits and you get fewer rungs, coarser steps, and - the whole point - a smaller number to store. The mapping from a real weight to its nearest integer rung, and back, is called affine quantization.

The affine map: scale, round, clamp

Fix the weight range and a bit width , giving rungs. The scale is the real-world width of one rung; the integer code is the rounded, clamped rung index:

Quantize, then dequantize

Here is round-to-nearest and pins the result into the representable range , so is an unsigned integer that fits in exactly bits. To read the weight back, undo the scaling:

The zero-point is the integer code that stands for true . Two sources of error live in these formulas: rounding - is at most half a rung, - and clamping, which flattens any weight outside onto the end rung. Fewer bits means a larger , so the rounding floor rises.

Snap a weight, watch the word fill

Below is one real weight sitting on the number line. Drag the bit-width slider from fp32 down to int8 to int4 and the ruler grows coarser: the weight snaps to the nearest teal rung, and the red gap is the quantization error you just accepted. Drag the weight slider (or the number line itself) to move it between rungs. Watch the memory bar shrink and the 32-bit word fill with more, smaller weights as the bits drop.

Quantization number line. Drag the bit width from fp32 to int8 to int4, drag the weight (or the line itself), and watch the memory bar shrink as the 32-bit word packs.
−1.0+1.0error 0.063ŵ 0.067w 0.130
fp32int8int4
true weight w0.130
scale s (rung width)0.1333
integer code q8 = 1000
dequantized ŵ0.067
error |w − ŵ|0.0633
max error s/20.0667
Memory for one billion weights
fp32
4.0 GB · 1×
int8
1.0 GB · 4×
int4
0.5 GB · 8×
One 32-bit machine word, bit by bit
1000
w0 live
1100
w1
0101
w2
1110
w3
0000
w4
1001
w5
0011
w6
1011
w7

int4: 8 weights pack into one 32-bit word - shift + mask + OR, no wasted bits (8×).

Packing is bit manipulation, verbatim

An int4 code is a nibble - four bits, values -. Storing eight of them as eight separate bytes would waste half of every byte. Instead you pack: shift the second code up four places and OR it onto the first, byte = (hi << 4) | lo. Eight nibbles fill one 32-bit word with zero slack. Unpacking is the inverse pair you already know - shift down, mask a nibble: hi = (byte >> 4) & 0xF. The exponent-and-mantissa view of the fp32 slot above is the same fixed-width bit-field reasoning behind flags and bitsets, just with different fields.

The trade you're making, in code

The whole pipeline is a scale, a round, a clamp - then the bit-packing you met as a party trick. Everything here is one expression doing what precision-preserving code cannot: forget information on purpose, cheaply.

import numpy as np

def quantize(w, bits, wmin, wmax):
    N     = 2 ** bits                  # levels: 16 for int4, 256 for int8
    scale = (wmax - wmin) / (N - 1)   # real width of one rung
    q     = np.round((w - wmin) / scale) # snap to the nearest integer code
    return np.clip(q, 0, N - 1).astype(np.uint8), scale

def dequantize(q, scale, wmin):
    return wmin + scale * q            # read the float back off the grid

# --- pack two int4 codes into one byte: the shift + mask + OR bit tricks ---
hi, lo  = 12, 9                    # two 4-bit weights, each 0..15
byte    = (hi << 4) | lo             # 1100 1001  ->  201
back_hi = (byte >> 4) & 0xF          # 12  (shift down, mask a nibble)
back_lo =  byte        & 0xF          # 9   (mask the low nibble)

Where naive quantization breaks

One scale for a whole tensor assumes the weights fill their range evenly. A single outlier - one weight far larger than the rest - stretches , inflating so every ordinary weight rounds coarsely and loses precision. Real recipes fight this with per-channel or per-group scales (a separate for each block of weights) - the idea behind formats like GGUF's block-quantized types, which store, say, 32 int4 codes plus one shared fp16 scale per block. Below roughly 4 bits, accuracy falls off a cliff; int8 is usually near-lossless, int4 is the aggressive-but-workable edge.

Check yourself

Storing a weight as an int4 code instead of a 32-bit float makes it, per weight:

The quantization error on an in-range weight comes from:

Recall: write the affine quantize and dequantize steps, and say exactly where the error comes from.

Try to state it, then check.

Lock it in

  • Quantization replaces the float continuum with a coarse ruler of rungs and stores each weight as the index of its nearest rung (int8 or int4).
  • Affine quantization is scale, round, clamp: , read back as .
  • Error comes from rounding (, and grows as bits shrink) plus clamping outliers onto the end rung - which is why per-channel or per-group scales exist.
  • Low-bit codes pack with the same shift, mask, and OR as a bitset (eight int4 nibbles per 32-bit word), buying 4-8x less memory for a sliver of accuracy.

Primary source

The Hugging Face LLM Course covers quantization in its efficiency material - scale, zero-point, int8/int4, and the accuracy-vs-memory trade for real models. For the low-level packing beneath it - the shifts, masks, and nibble tricks that squeeze several integers into one word - Sean Anderson's canonical Bit Twiddling Hacks remains the reference, and it is the same page the bit-manipulation lesson drew from.

Ask your teacher

Want to see per-channel scales, symmetric vs asymmetric quantization, or how a GGUF block actually lays out its 32 codes plus a shared scale? Ask and I'll build it. Here is the tie that closes the Connections track: quantization is bit manipulation applied to LLM weights - the same masks, shifts, and ORs, aimed at 32-bit floats instead of a bitset. It is why a 70-billion-parameter model that needs 280 GB in fp32 fits in 35 GB of int4, close enough to run on a single GPU rather than a rack. Every LLM idea in this course has had a DSA idea underneath it - attention was graphs, embeddings were vectors, and now the model's very footprint is bit twiddling. That was the whole point.