A model's weights are normally 32-bit floats. Quantizing them to 8-bit integers shrinks memory (and often speeds up inference) by roughly 4x, at the cost of a small, mathematically bounded amount of precision -- this is exactly what llama.cpp/GGML's quantized model formats (Q8_0 and friends) do.
Implement:
quantize(values) -> (quantized, scale)-- findscale = max(abs(values)) / 127(so the largest-magnitude value maps to exactly -127 or 127). Each value maps toround(value / scale), clamped to[-127, 127].dequantize(quantized, scale) -> floats-- the inverse: eachquantized[i]maps back toquantized[i] * scale. This recovers an APPROXIMATION of the original value, not an exact one -- that's the tradeoff.
Handle the all-zero input case in quantize by returning
([0] * len(values), 1.0) -- there's nothing meaningful to scale.
Example
quantize([1.0, -1.0, 0.5]) # -> ([127, -127, 64], 1/127 approx.)
The missing link: one scale for a whole tensor is a real liability
A single global scale is derived from the single LARGEST-magnitude value in
values. That's fine when values are all similar in size -- but real model
weights aren't: one rare outlier weight can be 100x the size of everything
else around it. With a single global scale, that one outlier forces a coarse
scale onto the ENTIRE tensor, quietly destroying precision for every other
value that had nothing to do with the outlier.
The fix real formats use (GGML's Q8_0 and friends): split values into fixed-
size blocks and give each block its OWN independent scale. An outlier in one
block no longer affects any other block's precision at all.
Implement quantize_blocked(values, block_size) -- split values into
consecutive chunks of block_size (the last chunk may be shorter), call your
own quantize() on each chunk independently, and return
(all_quantized_values_concatenated_in_order, [scale_per_block, ...]). Then
dequantize_blocked(quantized, scales, block_size) -- the inverse, using
scales[i] to dequantize() the i-th block of quantized.
Constraints
- The bound that makes this useful: for every value,
|value - dequantize(quantize(value))| <= scale / 2-- rounding to the nearest integer can never be off by more than half a step. The same bound applies per-block for the blocked versions, using that block's own scale. - Quantized values must always land in
[-127, 127]. quantize_blockedwithblock_size >= len(values)(one block covering everything) must produce identical output to plainquantize.
Real-world grounding
- GGML's quantization formats, including the actual per-block layout (32
values per block for Q8_0):
src/ggml-quants.c
Real quantization schemes get even more sophisticated on top of this (4-bit formats with even tighter packing, calibration against real activation distributions rather than just the raw weight values) -- but "confine an outlier's damage to its own block" is the core idea underneath block-wise quantization specifically.