← TinyGPT · docs · devlog · roadmap · speedup
source: docs/prds/quantized-inference-swift.md · view on GitHub ↗

Quantized inference in tinygpt’s HFModel — Swift-side QuantizedLinear

Status: PRD (2026-06-09). Phase 1 SHIPPED 2026-06-10 (serve --quantize); native packed load (the design below) still open.

2026-06-10 Phase 1 findings — serve --quantize int4|int8

Shipped (commit d3b7879): in-memory MLXNN.quantize after load, same mechanic as sample --quantize. Skipped under --lora; for adapters, bake first (bake-loraserve --quantize). Quantize config folded into the KV prompt-cache fingerprint.

Measured on the shipping planner (pace-planner-v9-lora/baked-hf, Qwen3-0.6B, M5 Pro, release build, fm-fixtures-v2 with v9 compose-v2 prompt + v9 schema + serve-side --grammar):

configfm-fixtures-v2decode (best of 3)verdict
fp169/15 (60.0%)92 tok/sbaseline
int8 g649/15 (60.0%)212 tok/s (2.3×)zero quality loss — recommended
int4 g648/15 (53.3%)282 tok/s (3.1×)-1 fixture: fails no-regression gate

Problem

mlx_lm convert -q --q-bits 4 produces MLX-quantized safetensors:

Tinygpt’s HFModel.swift::makeMLXArray (line 442) fatal-errors on U32 dtype. The loader has no concept of quantized weights.

Result: v9-LoRA baked-hf is 1.4 GB fp16. Quantized to Q4 is 331 MB (4.2× smaller). But tinygpt serve cannot load it → Pace ships fp16.

Target outcome

Tinygpt serve loads mlx_lm convert -q output natively. Pace bundles 331 MB instead of 1.4 GB. Inference uses MLX’s quantized_matmul ops directly on packed weights — no dequant-at-load.

Estimated formula payoff:

Design

1. Detect quantization at load

In HFModelLoader.load(), parse config.json["quantization"] (or quantization_config). If present, set a quantized: QuantizationSpec on the load result with {bits, groupSize, mode}.

2. Detect quantized tensors during weight load

HFModel.swift::loadShard, walking each tensor:

3. Extend makeMLXArray for U32

Add a U32 case that builds MLXArray(uint32-buffer, shape) without conversion.

4. Wrap Linears as QuantizedLinear at model construction

The hardest part. Today TinyGPTModelHF creates standard Linear instances. Need:

// At construction time, if quantizedSpec present:
class HFAttention {
    @ModuleInfo(key: "q_proj") var qProj: Linear  // → QuantizedLinear
    ...
}

Two paths:

Recommended: A, with a post-load step that replaces each targeted Linear with a QuantizedLinear via model.update(modules:). mlx-swift’s Module.update(modules:) allows substituting child modules.

5. Wire scales + biases into the QuantizedLinear’s params

After substituting LinearQuantizedLinear, write the loaded uint32 weight + bf16 scales + bf16 biases into the new module’s params via model.update(parameters:).

6. Verify forward pass

mlx-swift’s QuantizedLinear.callAsFunction does quantized_matmul automatically. No changes needed in attention/MLP code — they call qProj(x) which dispatches polymorphically.

7. Smoke + accuracy test

Scope estimate

~1 full day of focused Swift:

Compatibility

Alternative — dequant-at-load (rejected)

Could use dequant_mlx_generic.py to dequant Q4 → fp16 before loading. Saves disk only, no RAM/speed wins. Defeats the formula payoff. Reject.

Alternative — switch Pace to mlx_lm.server (rejected)

mlx_lm has its own serving with native quantized model support. But Pace’s plan per tinygpt/docs/prds/scope-narrowing-tinygpt-as-factory.md is embedded MLX-Swift inference (no HTTP at runtime). mlx_lm is Python — doesn’t fit. Reject.

Done when