Skip to content

Cartridge — train a corpus into a reusable KV prefix

Distill a document into the KV cache of a virtual p-token prefix, then serve it like any cached prompt — corpus-grounded answers with zero corpus tokens in the prompt.

Cartridges (arXiv 2506.06266; design record docs/CARTRIDGES.md) compress a document into the KV cache of a virtual p-token prefix by in-process self-study distillation — the model interviews itself about the corpus, and the teacher-with-context distills into a small trainable cache that is then served like any cached prompt. Needs an f32/f16/bf16 GGUF (gradients flow through the frozen weights). The multi-document sibling — one cartridge per document with in-process retrieval — is examples/cartridge_fleet.

The exact demo below uses the repository's own README.md as the corpus and Qwen3-0.6B-f16.gguf. Measured on an M1 Max: the training command runs ~4.5 minutes end-to-end — 3.3 min of self-study (32 conversations at ≈6 s each: chunk sampling, both bot generations, teacher scoring, backward) plus model load, capture init, save, and the three serve answers (the 5.4k-token ICL prefill is the slow tail). The CLI prints its own timing per step and a self-study: ... s/conversation summary.

Getting the weights

Weights are not part of this repository. The walkthrough uses Qwen3-0.6B-f16.gguf; neither bartowski/Qwen_Qwen3-0.6B-GGUF nor unsloth/Qwen3-0.6B-GGUF ships an f16, so download a bf16 and transcode it — the "f16 Qwen3" recipe from docs/RUNNING-MODELS.md, made concrete (the hf CLI comes from pip install -U huggingface_hub):

mkdir -p models
hf download bartowski/Qwen_Qwen3-0.6B-GGUF Qwen_Qwen3-0.6B-bf16.gguf --local-dir models
zig build export-gguf -Doptimize=ReleaseFast -- --from-gguf models/Qwen_Qwen3-0.6B-bf16.gguf \
  --out models/Qwen3-0.6B-f16.gguf --dtype f16

unsloth's file is Qwen3-0.6B-BF16.gguf — same two commands, different names. The f16 base is the walkthrough's default (--model defaults to models/Qwen3-0.6B-f16.gguf); quantized GGUFs train cartridges too — the base stays frozen, and the gemma arm below runs on Q6_K.

The gemma arm uses the quantized MoE GGUF:

mkdir -p models
hf download unsloth/gemma-4-26B-A4B-it-GGUF gemma-4-26B-A4B-it-UD-Q6_K.gguf --local-dir models

Gemma-family weights are distributed under Google's Gemma Terms of Use. The google/… originals on Hugging Face are gated behind accepting those terms; the unsloth GGUF conversions were not gated at the time of writing, but the terms still apply to the weights either way.

Corpus files

A corpus is plain text: any UTF-8 file works (the walkthrough uses the repository's own markdown), up to 16 MiB per file. --corpus repeats and also accepts a directory — its top-level .md files in sorted order, other entries ignored. Training and the --equiv gate need a corpus that tokenizes to at least --p + 2 tokens (--p defaults to 512) and training additionally to at least --chunk-min + 2; anything shorter exits with CorpusTooShort — lower --p/--chunk-min for small documents.

Walkthrough

1. Acceptance gate (~10 s)

An UNTRAINED cartridge built from the model's own K/V rows for the first 256 corpus tokens must score the next 128 tokens exactly like the real prefill.

zig build cartridge -Doptimize=ReleaseFast -- --model models/Qwen3-0.6B-f16.gguf \
  --corpus README.md --p 256 --suffix-max 128 --equiv

Expected output ends with:

prefill-equivalence over 128 suffix tokens: max |dlogit| 0.000000, ... greedy agreement 128/128
PASS: untrained corpus-init cartridge is behaviorally identical to the real prefill

2. Self-study training + save (~3.5 min on an M1 Max)

32 synthesized conversations (8 optimizer steps x 4-conversation accumulation), teacher top-20 targets, Adam lr 2e-3 (the default; docs/CARTRIDGES.md explains why the paper's 2e-2 needs its 65k-token batches). Prints per-step distill loss and the held-out loss before/after, then saves.

zig build cartridge -Doptimize=ReleaseFast -- --model models/Qwen3-0.6B-f16.gguf \
  --corpus README.md --p 256 --steps 8 --chunk-min 256 --chunk-max 512 \
  --max-q 64 --max-a 160 --seed 7 --draft-ref \
  --save /tmp/fucina-cartridge-readme.safetensors

3. Serve the saved cartridge

Geometry is recovered from the safetensors header; --corpus is optional (enables the ICL column). Expected: the [cartridge, 256 KV rows] and [ICL, ~5.3k KV rows] answers agree ("Fucina is a CPU-first tensor/autograd runtime and LLM inference engine written in pure Zig 0.16.") while [bare model] hallucinates.

zig build cartridge -Doptimize=ReleaseFast -- --model models/Qwen3-0.6B-f16.gguf \
  --load /tmp/fucina-cartridge-readme.safetensors \
  --ask "What is Fucina, in one sentence?" --corpus README.md

Training with --draft-ref embeds the corpus token ids in the artifact (8 bytes/token), making it self-contained for speculative serving: --spec-serve builds the corpus suffix automaton ONCE at load (~1 ms per 5k tokens) and the corpus drafts the answer — no --corpus needed, and nothing is constructed per generation call. Output is byte-identical to plain decoding (lossless verification); +12-16% tok/s on long corpus-grounded answers. Artifacts without the entry fall back to drafting from --corpus.

zig build cartridge -Doptimize=ReleaseFast -- --model models/Qwen3-0.6B-f16.gguf \
  --load /tmp/fucina-cartridge-readme.safetensors --spec-serve \
  --ask "What backends and hardware does Fucina support?"

4. Multi-document corpora

--corpus repeats, and a directory takes its top-level .md files in sorted order; every file is prefixed with a # Document: <path> header and each training chunk carries a one-line provenance description. The repo's own documentation (19 files, ~328k tokens — far beyond a sane prefill) is a corpus in one flag set; the ICL comparison column truncates at --icl-max (default 4096) tokens. Measured (M1 Max): ~8 min end-to-end — 64 conversations in 6.6 min at the SAME ~6 s/conversation as the 5k-token corpus. The --equiv gate is bitwise on this corpus too.

zig build cartridge -Doptimize=ReleaseFast -- --model models/Qwen3-0.6B-f16.gguf \
  --corpus README.md --corpus AGENTS.md --corpus docs \
  --p 512 --steps 16 --chunk-min 256 --chunk-max 512 --max-q 64 --max-a 160 \
  --seed 7 --save /tmp/fucina-cartridge-docs.safetensors

5. Full-coverage runs

Checkpoint every N steps (atomic, same --save path) and resume from a checkpoint (rows only; Adam moments restart). 2048 conversations over the full docs ~ 4-5 h on an M1 Max, ~15 min per checkpoint interval below. GPU note: with the batched pipeline -Dgpu=metal is ~1.2x faster per conversation on an M1 Max at 0.6B (~2x at 1.7B with --gen-batch 16) — see docs/CARTRIDGES.md "Acceleration".

zig build cartridge -Doptimize=ReleaseFast -- --model models/Qwen3-0.6B-f16.gguf \
  --corpus README.md --corpus AGENTS.md --corpus docs \
  --p 1024 --steps 512 --accum 4 --max-a 192 --seed 7 \
  --save cartridge-full.safetensors --save-every 32 --draft-ref
# resume after an interruption:
#   ... --resume cartridge-full.safetensors --steps 256 --save cartridge-full.safetensors --save-every 32

6. Serve it over HTTP

Every conversation of the OpenAI-compatible server preloads the cartridge — requests answer from the corpus with ZERO corpus tokens in the prompt, and cross-request KV reuse (cached_tokens) operates on the real tokens past the prefix. See examples/lmserve and docs/LMSERVER.md.

zig build lmserve -Doptimize=ReleaseFast -- models/Qwen3-0.6B-f16.gguf \
  --port 8080 --cartridge /tmp/fucina-cartridge-readme.safetensors
# curl -s http://127.0.0.1:8080/v1/chat/completions -H 'Content-Type: application/json' \
#   -d '{"model":"m","messages":[{"role":"user","content":"What is Fucina, in one sentence?"}]}'

gemma routing

gemma GGUFs (dense or MoE) route to the gemma4 trainer arm; --equiv runs the acceptance gate with the model's shape-sensitivity envelope printed first (quantized-MoE stacks are not GEMM-shape-invariant — see docs/CARTRIDGES.md "gemma4"). Train via the gemma4 trainer API; serve via lmserve --cartridge.

zig build cartridge -Doptimize=ReleaseFast -- --model models/gemma-4-26B-A4B-it-UD-Q6_K.gguf \
  --corpus README.md --equiv --p 64 --suffix-max 32

Knobs

flag meaning
--p N prefix rows
--frozen N attention-sink rows, default 1
--steps / --accum / --lr optimizer
--chunk-min / --chunk-max corpus spans
--top-k N teacher entries/token
--max-q / --max-a bot budgets
--seed N self-study sampling seed (see below)
--icl-max N ICL comparison context cap
--save-every N checkpoint interval
--resume PATH resume from a checkpoint (rows only; Adam moments restart)
--draft-ref embed the corpus ids in the artifact for --spec-serve
--gen-batch N generation stream width, decoupled from --accum; a multiple of it — wider batches amortize the decode weight stream
--spec-b / --spec-serve lossless speculative decoding: self-study bot B / corpus-drafted serving; see docs/CARTRIDGES.md
--checkpoint recompute-in-backward per layer: measured 1.7B peak RSS 15.7 -> 7.9 GB with a byte-identical trained artifact, and faster; qwen3 single-cartridge path
--no-pack flat-memory per-conversation backward; the default packs the accumulation group into one forward/backward — gradient-identical, and the group's generations run as lockstep batched streams either way: measured 1.57x conversations/s over the sequential pipeline

Seed and coverage

Generation is sampled (bot A, temperature 0.6), so conversations — and the trained rows — vary with --seed; the answers above are what the pinned seed produces. Per-conversation cost is INDEPENDENT of corpus size (only the sampled chunk enters the teacher's context — the 328k-token corpus trains at the same ~6 s/conversation as the 5k-token one); corpus size instead sets the COVERAGE budget: 64 conversations sample ~7% of the full documentation, so knowledge quality on big corpora scales with conversation count (the paper's regime is tens of thousands). These demo budgets show the mechanism, not the ceiling.

Shared knobs

Build discipline (-Doptimize=ReleaseFast, -Dcpu when cross-compiling), GPU offload (-Dgpu=metal / -Dgpu=cuda), MoE expert streaming, global thread/BLAS knobs, and -Dllguidance constrained decoding are shared machinery — see docs/RUNNING-MODELS.md. Model-specific note: GPU builds accelerate self-study training itself (prefill-shaped GEMMs offload; step 5 above and docs/CARTRIDGES.md "Acceleration").