typedecide.export.quantize¶
Quantisation tiers for the browser build.
typedecide.export.quantize
¶
Quantisation tiers for the browser build.
Quantisation is the second-largest lever after pruning, and the only one that
can cost accuracy. This module keeps the tiers in one table so the size
arithmetic in docs/SPEED.md and the code that produces the files cannot drift
apart, and names each tier the way transformers.js does (model_q4f16.onnx), so
web/models.json can point at the output without renaming anything.
One thing is specific to this project and worth saying out loud: a typed readout
does not sample. It compares at most len(LETTERS) logits and takes an argmax.
Perplexity, which is what most quantisation write-ups report, is the wrong
number to watch. What matters is whether the margin between the top two answer
letters survives, and the honest way to find out is to score the same eval set
at each tier with typedecide.evaluation and compare balanced_accuracy and
order_consistency. A tier that keeps accuracy but collapses
order_consistency has turned the model into a position-guesser.
QUANTIZATION_MODES
module-attribute
¶
QUANTIZATION_MODES: dict[str, QuantizationTier] = {
"fp32": QuantizationTier(
name="fp32",
suffix="",
bits_per_weight=32.0,
compute_dtype="fp32",
summary="The export's own format. Lossless and unshippable: four bytes a weight is about four times what a browser should be asked to download.",
),
"fp16": QuantizationTier(
name="fp16",
suffix="fp16",
bits_per_weight=16.0,
compute_dtype="fp16",
summary="Half the bytes, and the native compute dtype on WebGPU. Accuracy loss on a letter argmax is expected to be negligible; treat this as the reference point the smaller tiers are compared against, not as free.",
),
"q8": QuantizationTier(
name="q8",
suffix="q8",
bits_per_weight=8.0,
compute_dtype="fp32",
summary="Per-channel int8 weights, float activations. The conservative choice when a 4-bit tier moves accuracy more than you are willing to accept.",
),
"q4": QuantizationTier(
name="q4",
suffix="q4",
bits_per_weight=4.0 + 16.0 / DEFAULT_BLOCK_SIZE,
compute_dtype="fp32",
summary="4-bit block-quantised MatMul weights with an fp16 scale per block of 32, float32 compute. Smaller download than q8, same compute cost.",
),
"q4f16": QuantizationTier(
name="q4f16",
suffix="q4f16",
bits_per_weight=4.0 + 16.0 / DEFAULT_BLOCK_SIZE,
compute_dtype="fp16",
summary="4-bit weights and fp16 compute. What web/models.json asks for: the smallest download that keeps WebGPU on its fast path. Also the tier most likely to cost accuracy, so it is the one to measure rather than assume.",
),
}
QuantizationTier
dataclass
¶
QuantizationTier(
name: str,
suffix: str,
bits_per_weight: float,
compute_dtype: str,
summary: str,
)
One tier: what it stores, what it computes in, and what it is likely to cost.
bytes_for
¶
Weight bytes for a model of parameters parameters at this tier.
Source code in src/typedecide/export/quantize.py
quantize
¶
quantize(
model_path: Path | str,
*,
mode: str = "q4f16",
output_path: Path | str | None = None,
block_size: int = DEFAULT_BLOCK_SIZE
) -> Path
Quantise an exported ONNX model and return the file written.
model_path is the .onnx file or the directory export_onnx wrote. The
output is named the way transformers.js expects (model_q4f16.onnx) and
placed beside the input unless output_path says otherwise.
mode="fp32" is a no-op that returns the input unchanged, so a pipeline can
pass the tier through without branching.
Quantise after pruning, not before. Pruning removes rows; quantising first means spending bits on rows that are about to be deleted, and on a tied model it also forces the quantiser to pick one representation for a table that the input embedding and the unembedding no longer share.
Source code in src/typedecide/export/quantize.py
resolve_mode
¶
resolve_mode(mode: str) -> QuantizationTier
The tier mode names, or an error listing the ones that exist.