User guide
This guide covers building Krafla, getting a model into the .mlb format, and running it.
Requirements
- A C11 compiler: GCC or Clang. For x86-64 you can also use MSVC through CMake.
- GNU Make.
- Python 3 with numpy, for the model tools. Install pytest as well if you want to run the test suite.
- For ARM cross builds:
gcc-aarch64-linux-gnuorgcc-arm-linux-gnueabihf, plusqemu-userto run the results on an x86 host.
Prebuilt binaries
Each release has static binaries for every ISA:
| Archive | Builds |
|---|---|
krafla-VER-linux-x86_64.tar.gz | sse2, sse41, avx2, avxvnni, avx512vnni |
krafla-VER-linux-aarch64.tar.gz | neon, neon_dotprod, neon_i8mm, sve |
krafla-VER-linux-armv7hf.tar.gz | neon, scalar |
krafla-VER-windows-x86_64.zip | sse2, sse41, avx2, avxvnni, avx512vnni |
The top-level ml_run, ml_server, ml_bench and ml_tok (.cmd on Windows) try the builds in bin/ from the fastest down and start the first one the CPU supports. To choose a build yourself, set KRAFLA_ISA, for example KRAFLA_ISA=avx2. The variable’s old name, ISA_LM_ISA, still works. The archives also hold the model tools tools/convert.py, tools/convert_tokenizer.py and tools/perplexity.py, which need Python 3 with numpy.
scripts/package_release.sh builds the same archives locally, and scripts/smoke_release.sh runs them. To cut a release, merge a version bump into main that raises ML_VERSION in core/ml.h to X.Y.Z and renames ## Unreleased in CHANGELOG.md to ## X.Y.Z — date. Before merging, run python3 scripts/sync_docs.py to refresh the site’s changelog page. .github/workflows/auto-release.yml then sees the new version, and release.yml builds and runs the archives, publishes release vX.Y.Z and creates the tag on that commit. Pushing a tag vX.Y.Z also still works. CI fails a pull request that raises ML_VERSION without adding its CHANGELOG section. Zenodo archives each published release and gives it a DOI; the concept DOI 10.5281/zenodo.23880653 resolves to the latest one. Once Zenodo has minted the release’s own DOI, .github/workflows/zenodo-doi.yml adds it to the release notes (it reads Zenodo’s public API, so it needs no token; run it by hand for an older release).
Converted models are published on Hugging Face by .github/workflows/hf-models.yml, which is run by hand. For each model in scripts/hf_models.json, it uses that commit’s tools to convert the Q4_0 and Q8_0 files, the mixed file if the model has one, and the tokenizer. It then measures them on the runner (wikitext-2 perplexity, ml_bench speed, one chat reply) and writes the model card (scripts/hf_publish.py). With upload on, it pushes them to <account>/<model>-krafla, using the repository secret HF_TOKEN, and then brings the account’s “Krafla” collection in line (.github/workflows/hf-collection.yml, scripts/hf_collection.py): every published model of the catalog, smallest first, each noted with the Krafla version it needs, its --chat template and the size of its Q4_0 file. python3 scripts/hf_publish.py MODEL --text wiki.txt does the same locally, without uploading, and python3 scripts/hf_collection.py --owner ACCOUNT --dry-run shows what the collection sync would change.
Building
make ISA=<isa> [THREADS=1|pthread] [ASM=0|1] [CROSS=<prefix>] [DEBUG=1] [EXTRA_CFLAGS=...]
The build writes ml_run, ml_server, ml_bench, ml_tok and the test binaries to build/<isa>[-<arch>][-pt][-asm][-dbg]/. Each set of options gets its own directory, so object files built with different flags never mix.
Choosing the ISA
ISA | Runs on |
|---|---|
scalar | anything with a C compiler |
sse2 | every x86-64 CPU |
sse41 | Intel Penryn/Nehalem and later, AMD Bulldozer and later |
avx2 | Intel Haswell and later, AMD Zen and later |
avxvnni | Intel Alder Lake and later, AMD Zen 5 |
avx512vnni | Intel Cascade Lake, Ice Lake, Sapphire Rapids; AMD Zen 4 and later |
neon | ARMv7 with NEON (Cortex-A7, A9, A15 and others), any AArch64 |
neon_dotprod | ARMv8.2 dotprod (Cortex-A55/A76 and later, Apple M1 and later, Graviton 2 and later) |
neon_i8mm | dotprod + int8 matrix multiply (Cortex-A510/A710/X2 and later, Neoverse N2/V1, Apple M2 and later, Graviton 3 and later) |
sve | SVE CPUs (Neoverse V1/V2/N2, A64FX, Graviton 3 and later) |
To find what a Linux machine supports, run grep -o -w -E 'sse4_1|avx2|avx_vnni|avx512_vnni|asimddp|i8mm|sve' /proc/cpuinfo | sort -u.
If you start a binary on a CPU that lacks its ISA, it exits with code 3 instead of crashing and names the missing feature:
error: CPU lacks avx512-vnni required by ISA=avx512vnni
Cross builds
make ISA=neon CROSS=arm-linux-gnueabihf- # ARMv7 hard-float
make ISA=sve CROSS=aarch64-linux-gnu- test e2e # runs under qemu-aarch64 -cpu max
make ISA=sve CROSS=aarch64-linux-gnu- QEMU_CPU=max,sve-max-vq=1 test # SVE at 128 bits
Cross builds are linked statically, so qemu does not need a sysroot. When CROSS targets another architecture, make test and make e2e run the binaries through qemu automatically.
CMake (x86-64, including MSVC)
cmake -S . -B build/cmake -DML_ISA=avx2 -DCMAKE_BUILD_TYPE=Release
cmake --build build/cmake && ctest --test-dir build/cmake
Set -DCMAKE_BUILD_TYPE=Release yourself; without it CMake builds without optimization. The pthread pool (-DML_THREADS=ON) is not available with MSVC.
Getting a model
Converted models on Hugging Face
These models are converted and published by .github/workflows/hf-models.yml (see Prebuilt binaries), so you can use them without installing Python. They are also gathered, smallest first, in the Krafla collection on Hugging Face:
| Model | Q4_0 | Q8_0 | Perplexity Q4_0 / Q8_0 | --chat | Krafla |
|---|---|---|---|---|---|
| SmolLM2 135M Instruct | 72 MiB | 136 MiB | 31.6 / 23.5 | smollm | 0.8.0 |
| SmolLM2 360M Instruct | 194 MiB | 367 MiB | 21.7 / 17.5 | smollm | 0.8.0 |
| Qwen2.5 0.5B Instruct | 265 MiB | 501 MiB | 21.7 / 19.0 | qwen | 0.8.0 |
| Qwen3 0.6B | 320 MiB | 604 MiB | 31.7 / 27.9 | chatml | 0.8.0 |
| Qwen3.5 0.8B | 406 MiB | 764 MiB | 27.5 / 23.9 | qwen3.5-nothink | 0.9.0 |
| TinyLlama 1.1B Chat | 590 MiB | 1115 MiB | 10.6 / 10.3 | zephyr | 0.8.0 |
| Qwen2.5 1.5B Instruct | 829 MiB | 1565 MiB | 14.7 / 12.7 | qwen | 0.8.0 |
| Qwen3 1.7B | 923 MiB | 1744 MiB | 24.0 / 21.0 | chatml | 0.8.0 |
| Qwen3.5 2B | 1012 MiB | 1908 MiB | 18.2 / 16.5 | qwen3.5-nothink | 0.9.0 |
| SmolLM3 3B | 1650 MiB | 3116 MiB | 13.8 / 11.6 | chatml | 0.8.0 |
| Qwen3 4B Instruct 2507 | 2158 MiB | 4076 MiB | 14.1 / 13.1 | chatml | 0.8.0 |
| Qwen3.5 4B | 2261 MiB | 4264 MiB | 13.3 / 12.2 | qwen3.5-nothink | 0.9.0 |
| Qwen2.5 7B Instruct | 4086 MiB | 7718 MiB | 9.9 / 9.0 | qwen | 0.8.0 |
| Qwen3 8B | 4395 MiB | 8300 MiB | 13.1 / 12.5 | chatml | 0.9.0 |
| Qwen3.5 9B | 4810 MiB | 9076 MiB | 10.7 / 10.1 | qwen3.5-nothink | 0.9.0 |
Each repository holds the Q4_0 and Q8_0 files, tokenizer.mlt, SHA256SUMS and a model card with the speed of each file. All but the two SmolLM2 repositories also hold a mixed file: Q4_0 with some matrices in Q8_0. Perplexity is measured on wikitext-2 (4 chunks of 512 tokens) and is comparable only between files of the same model. For SmolLM2, take the Q8_0 file. The files need the Krafla version in the last column or later. They record the model’s full context, of which ml_run and ml_server use 4096 positions unless you pass --ctx N. The Qwen3.5 models answer without thinking with qwen3.5-nothink; --chat qwen3.5 lets them think first. Models by others with the krafla tag are listed at https://huggingface.co/models?other=krafla.
curl -LO https://huggingface.co/jonpark0/Qwen2.5-0.5B-Instruct-krafla/resolve/main/Qwen2.5-0.5B-Instruct-Q4_0.mlb
curl -LO https://huggingface.co/jonpark0/Qwen2.5-0.5B-Instruct-krafla/resolve/main/tokenizer.mlt
./ml_run Qwen2.5-0.5B-Instruct-Q4_0.mlb -z tokenizer.mlt --chat qwen -t 4
From a llama2.c checkpoint
python3 tools/convert.py llama2c stories15M.bin stories15M.mlb --wtype q8
Use the matching llama2.c tokenizer.bin as it is.
From a Hugging Face checkpoint
The converter reads Llama (including SmolLM2), SmolLM3, Mistral, Qwen2 (Qwen2.5), Qwen3, Gemma 2 and Gemma 3 checkpoints. It takes a directory that contains config.json and one or more *.safetensors files:
python3 tools/convert.py hf ./TinyLlama-1.1B model.mlb --wtype q4
- Weight types: F32, F16 and BF16 safetensors are supported.
- Tied embeddings: if the checkpoint has no
lm_head, the file is written with a shared classifier. - Rotary layout: the q/k projections are permuted from the Hugging Face rotate-half layout to the interleaved pairs the runtime uses.
- RoPE scaling:
rope_scalingof typellama3(Llama 3.1 and 3.2),linearandyarn(for example Qwen2.5 and Qwen3 beyond 32K tokens, as their model cards describe) are supported, with transformers’ formulas. The converter refuses other types (dynamic, longrope) instead of writing a model that would compute wrong outputs. - Architectures:
model_typemust bellama,mistral,qwen2,qwen3,qwen3_5(Qwen3.5 and Qwen3.6, dense),smollm3,gemma2,gemma3_text(Gemma 3 1B) orgemma3(Gemma 3 4B, 12B and 27B). SmolLM3 leaves out RoPE in every fourth layer (no_rope_layers); the converter marks those layers. Qwen3.5 puts three linear-attention layers (the gated delta rule) before every attention layer, gates the attention output and rotates a quarter of each head; the converter takes its language model and leaves out the vision tower and the multi-token prediction head. Its(1 + w)norm weights are stored with the 1 added, and the linear layers’bandaprojections stay in Q8_0 in every file. Thegemma3checkpoints are image-text models: the converter takes the text model and leaves the vision tower out. Biases are accepted only on Qwen2’s q, k and v projections. Supported features: ahead_dimother thanhidden_size / num_attention_heads; Qwen3’s per-head q/k RMSNorm; for Gemma, GELU, block output norms, embedding and attention scales, and local/global layers; and Gemma 2’s soft-capping of attention scores and logits. Gemma’s(1 + w)norm weights are stored with the 1 added. Other architectures, such as Phi and mixture-of-experts models, are refused. - Memory: tensors are converted to float32 one at a time, so converting a 4B model needs about as much memory as its largest tensor.
- Sliding window: in a sliding-window layer, each position attends to the last
sliding_windowpositions. Every layer of Mistral v0.1 is one; in other models,layer_typesor Qwen2’smax_window_layersselects which layers are. - Context length: the file records the longest context the model allows,
max_position_embeddings(often 131072 for Llama 3), or less with--max-seq. The KV cache is sized when the model runs, by--ctx. - Scales: each block’s scale is stored as
f16by default (Q4_0 at 4.5 bits per weight, Q8_0 at 8.5).--scales f32keeps the older 4-byte scales (5 and 9 bits per weight). With f32 scales every file is 5–10% larger, and perplexity changes by at most 0.15%. Files of both kinds load in every build. - Tokenizer: convert the checkpoint’s
tokenizer.jsonseparately, see Tokenizers.
Tokenizers
ml_run -z reads either a llama2.c tokenizer.bin or an .mlt file converted from a Hugging Face tokenizer.json:
python3 tools/convert_tokenizer.py ./Llama-3.2-1B-Instruct/tokenizer.json llama3.mlt
printf "Hello, world" | build/avx2/ml_tok llama3.mlt # 128000 9906 11 1917
echo "9906 11 1917" | build/avx2/ml_tok llama3.mlt --decode # Hello, world
These kinds are supported, and they encode exactly as the Hugging Face tokenizers library does (the tests compare them on hundreds of texts):
| Kind | Models | Notes |
|---|---|---|
| SentencePiece-style BPE with byte fallback | Llama 2, TinyLlama, Mistral, Vicuna, Gemma 3 | spaces become ▁ (Gemma adds none before the text), characters outside the vocabulary become <0xXX> byte tokens |
| byte-level BPE with the Llama 3 pre-tokenizer | Llama 3, 3.1, 3.2 | text is split by the Llama 3 regex (letters, numbers, contractions, white space) before merging |
| byte-level BPE with NFC and the Qwen2 pre-tokenizer | Qwen2, Qwen2.5, Qwen3 | text is normalized to NFC, then split as Llama 3 but with one digit per number chunk |
| byte-level BPE with NFC and the Qwen3.5 pre-tokenizer | Qwen3.5, Qwen3.6 | as Qwen2, with combining marks (\p{M}) kept with the letters |
| byte-level BPE with the GPT-2 pre-tokenizer | SmolLM, SmolLM2, GPT-2 | text is split by the GPT-2 regex, after splitting off every digit if the tokenizer does (SmolLM2) |
The converter refuses anything else (WordPiece, Unigram, other normalizers or pre-tokenizers) rather than write a tokenizer that would encode differently.
- BOS: added when the tokenizer’s template adds it (
<s>,<|begin_of_text|>); Qwen2, Qwen3 and SmolLM2 add none. - Special tokens written in the prompt, such as
<|eot_id|>, are encoded as the special tokens. - Stopping: without
--chat, generation stops at the tokenizer’s BOS or EOS (the EOS is, for example,</s>or<|end_of_text|>). Instruct models often end a turn with another token: pass it with--eosto stop there instead, for example--eos 128009(<|eot_id|>) for Llama 3 Instruct.
Chat
--chat TEMPLATE wraps each message in the model’s chat template, keeps the conversation in the KV cache, and stops each reply at the template’s end token:
build/avx2/ml_run llama3.mlb -z llama3.mlt --chat llama3 --system "You are a helpful assistant." -t 4
> What is the capital of France?
The capital of France is Paris.
> And of Germany?
The capital of Germany is Berlin.
| Template | Models | Reply ends at |
|---|---|---|
llama3 | Llama 3, 3.1, 3.2 Instruct | <|eot_id|> |
zephyr | TinyLlama Chat, Zephyr | </s> |
llama2 | Llama 2 Chat, Mistral Instruct (without --system) | </s> |
qwen | Qwen2.5 Instruct; without --system it uses Qwen’s default system prompt | <|im_end|> |
smollm | SmolLM2 Instruct; without --system it uses SmolLM2’s default system prompt | <|im_end|> |
gemma | Gemma 3 Instruct; --system goes at the start of the first user turn | <end_of_turn> |
chatml | Qwen3, SmolLM3 and other ChatML models; a system turn only with --system | <|im_end|> |
qwen3.5 | Qwen3.5 and Qwen3.6 with reasoning: ChatML, the reply opened with <think> (the default of their 4B and larger templates) | <|im_end|> |
qwen3.5-nothink | Qwen3.5 and Qwen3.6 without reasoning: ChatML, the reply after an empty think block (the default of the 0.8B and 2B templates) | <|im_end|> |
Qwen3 writes a <think>...</think> section before it answers. Ending a message with /no_think asks it to skip the reasoning. The KV cache keeps earlier replies with their reasoning, whereas Qwen3’s own template drops it from the history.
Type one turn per line on stdin (a > prompt appears on a terminal), or pass a single turn with -i; end with Ctrl-D. -n caps each reply; without it a reply may use the rest of the context (--ctx), and when the context is full ml_run prints a message and ends the conversation. -T, -p, --top-k and --repeat-penalty apply as usual.
Without --chat, write the template yourself with -i and stop with --eos (see Tokenizers).
A random test model
python3 tools/gen_random.py --out rand.mlb --wtype q4 --dim 256 --layers 4 --vocab 512
python3 tools/gen_tokenizer.py --vocab 512 --out tok.bin
Q8, Q4, or mixed?
The converter writes weights in two formats. In the rest of this guide, “Q8” and “Q4” are short for Q8_0 and Q4_0:
| Format | Bits per weight | Notes |
|---|---|---|
Q8_0 | 8.5 (9 with --scales f32) | close to F32 quality |
Q4_0 | 4.5 (5 with --scales f32) | about half the memory traffic of Q8_0, so faster decoding; costs some quality |
Choose Q8 for accuracy and Q4 for size. In between, --q8 keeps some matrix classes of a Q4 model in Q8 (emb, cls, qk, v, o, up, down):
python3 tools/convert.py hf ./Llama-3.2-1B-Instruct llama.mlb --wtype q4 --q8 v,o,down
Perplexity of Llama 3.2 1B on 2044 tokens of wikitext-2, measured with tools/perplexity.py. All files use f16 scales (the converter’s default); sizes are in MiB. Decode speed is on 4 vCPUs with the avx512vnni build.
| Weights | Size | Perplexity | Decode -t 4 |
|---|---|---|---|
| Q8 | 1252 MiB | 17.27 | 35.0 tok/s |
Q4 + Q8 v,o,down | 831 MiB | 17.89 | 47.7 tok/s |
Q4 + Q8 v,down | 799 MiB | 18.10 | |
| Q4 | 663 MiB | 19.91 | 64.1 tok/s |
The down projection (down) and the value and output projections (v, o) matter most. The Q8 KV cache costs nothing measurable here: --kv q8 on the Q8 model gives 17.27. The embedding and classifier (emb, cls) are large for a 128K vocabulary and help little (Q4 + emb,cls: 788 MiB, 19.87).
Qwen2.5 0.5B Instruct on the same text and machine. Qwen2.5 adds no BOS, and the F32 numpy reference scores 19.00:
| Weights | Size | Perplexity | Decode -t 4 |
|---|---|---|---|
| Q8 | 500 MiB | 18.98 | 87.3 tok/s |
Q4 + Q8 emb,v,o,down | 390 MiB | 20.06 | 107.2 tok/s |
| Q4 | 265 MiB | 21.74 | 141.5 tok/s |
Small models lose more to Q4: here up and down matter most, then v.
The converter fits each Q4 block’s scale by weighted least squares over a few candidate mappings, rather than dividing the block’s maximum by 7, as earlier versions of the converter did (Llama 3.2 1B Q4: 19.91 instead of 20.46; Qwen2.5 0.5B: 21.73 instead of 23.58). The file format and the kernels are unchanged, so older Q4 files still load; convert again to get the better scales. See the quantization rule.
Other models on the same text, all with f16 scales. The F32 column is the numpy reference, and the Q4 column uses the converter’s default rule, which fits the scales of every model but Gemma 3 (see below):
| Model | F32 | Q8 | Q4 |
|---|---|---|---|
| Qwen3 0.6B | 28.10 | 27.90 | 31.69 |
| SmolLM2 135M Instruct | 23.44 | 23.62 | 31.60 |
| Gemma 3 1B Instruct | 42.32 | 41.97 | 49.83 |
| Gemma 3 4B Instruct | — | 24.30 | 23.31 |
For Gemma 3 4B, the F32 reference was not run over the whole text. On its first 96 tokens, the Q8 logits are within KL 0.0021 of it, with the same argmax at 97% of the positions. Its Q8 file takes 3932 MiB and its Q4 file 2083 MiB.
Gemma 3 1B loses the most to Q4: with the fit its perplexity rises to 53.25, and keeping emb,cls,v,down in Q8 brings it to 47.97 at 783 MiB. llama.cpp’s plain Q4_0 rounding (--q4-rule round) does better on Gemma 3 (49.83 at 536 MiB), though not on the other models in the table below. Google’s quantization-aware checkpoint (gemma-3-1b-it-qat-q4_0-unquantized) was trained for exactly that rounding; rounded, it scores 40.50 at 536 MiB, below its own Q8 (42.53) and below the regular checkpoint’s F32 reference. The fit gives 47.43 on it. The converter therefore rounds Gemma 3 checkpoints by default and fits the others; --q4-rule fit|round overrides it.
| Q4 rule (f16 scales) | Qwen2.5 0.5B | Llama 3.2 1B | Qwen3 0.6B | SmolLM2 135M | Gemma 3 1B | Gemma 3 1B QAT |
|---|---|---|---|---|---|---|
| fit | 21.73 | 19.90 | 31.69 | 31.60 | 53.25 | 47.43 |
| round | 22.45 | 19.94 | 31.75 | 32.31 | 49.83 | 40.50 |
An importance matrix (--imatrix) tells the fit which weights matter most. It records how large each input channel’s activations are on sample text, and the fit then quantizes the weights that multiply large activations more precisely. Collect it with ml_run --imatrix on a Q8 conversion of the same model (each run adds to the file), then convert to Q4 with it:
python3 tools/convert.py hf ./Qwen2.5-0.5B-Instruct q8.mlb --wtype q8
python3 tools/perplexity.py build/avx2/ml_run q8.mlb tok.mlt sample.txt --ml-tok build/avx2/ml_tok \
--ctx 512 --chunks 32 --extra "--imatrix qwen.imat"
python3 tools/convert.py hf ./Qwen2.5-0.5B-Instruct q4.mlb --wtype q4 --imatrix qwen.imat
A tied embedding (Qwen2.5, Llama 3.2) is also the classifier and takes the classifier’s importance; an untied one is only a lookup and keeps the plain fit. Q8 matrices ignore the importance matrix.
Perplexity of Qwen2.5 0.5B Instruct Q4 on the wikitext-2 test set, measured with tools/perplexity.py --ctx 512. The importance matrix was collected on the first 32K tokens of the wikitext-2 training set (--chunks 64):
| Weights | 4 chunks | 16 chunks | --llama-cpp, 4 chunks |
|---|---|---|---|
| Q8 | 19.01 | 22.10 | 13.33 |
| Q4 | 21.72 | 26.47 | 15.13 |
Q4 --imatrix | 20.92 | 25.30 | 14.52 |
It closes a quarter to a third of the gap to Q8. The gain depends on the sample text: an importance matrix collected on TinyStories gives 25.74 on 16 chunks. Collect it on text similar to what the model will be used for.
Running
build/avx2/ml_run model.mlb -z tokenizer.bin -i "Once upon a time" -n 128 -T 0.8 -p 0.9
build/avx2/ml_run model.mlb --tokens 1,450,3681 -n 32 # ids in, ids out
build/avx2/ml_run model.mlb --tokens 1,450,3681 --dump-logits l.f32
| Option | Meaning |
|---|---|
-z FILE | Tokenizer: llama2.c tokenizer.bin, or .mlt from tools/convert_tokenizer.py. Needed for -i and for text output. |
-i TEXT | Prompt text. Encoded with BOS. |
--tokens LIST | Prompt as comma-separated token ids. Each id must be below vocab. |
-n N | Positions to run, prompt included. Default 64; capped at --ctx. |
--ctx N | Context length: the positions the KV cache holds, from 1 to the file’s max_seq. Default max_seq, but at most 4096, so that a file converted at the model’s full context (128K positions for Llama 3.1) does not reserve gigabytes of KV cache unless asked to. Logits do not depend on it. |
-T F | Temperature. 0 means greedy decoding (ties go to the lowest id). Default 0. |
-p F | Top-p in [0, 1]. Values of 0 or 1 sample from the full distribution. Default 0.9. |
--top-k N | Sample only from the N most likely tokens, then apply top-p among them. 0 (default) turns it off; 1 is greedy at any temperature. |
--repeat-penalty F | Repetition penalty: divide the positive logit of each token seen in the last --repeat-last-n tokens by F and multiply a negative one, as Hugging Face does. Applies to greedy decoding too. 1 (default) turns it off; 1.1–1.3 is typical. |
--repeat-last-n N | How many recent tokens, prompt included, the penalty looks at. Default 64. |
-s N | Seed for the xorshift64* RNG. Default 1. |
-t N | Threads, 1 to 64. Only builds made with THREADS=pthread use more than one. |
--dump-logits F | Write float32 logits [n_tokens][vocab] for every --tokens position, then exit. |
--batch N | Prompt tokens per batched prefill step, 1 to 128. Default 128; 1 processes the prompt token by token. The output is identical either way. |
--kv TYPE | KV cache type: f32 (default), f16 or q8. f16 rounds K and V to half precision: half the memory at the same speed, and perplexity 21.71 against 21.74 for Qwen2.5 0.5B Q4. q8 stores K and V as int8 with one scale per position and KV head. It uses about 3.8× less memory and speeds up attention on long contexts, but raises perplexity on the same test to 22.82. |
--pack-cache | Save the packed weights next to the model (MODEL.pk<G>, G = pack group) and map them from there on later runs instead of packing again. The cache is checked against the model and the build’s layout and rewritten when stale. |
--no-pack | Keep weights row-major instead of repacking them for the packed kernels. Uses less memory during load, but runs slower. |
--profile | Print the share of forward-pass time per stage (qkv, attention, wo, ffn, classifier, other). |
--chat TEMPLATE | Chat with the model: llama3, zephyr, llama2, qwen, smollm, chatml, gemma, qwen3.5 or qwen3.5-nothink, see Chat. Needs -z. |
--system TEXT | System prompt for --chat. |
--eos ID | Stop as soon as ID is generated. Without it, a run with -z stops at the tokenizer’s BOS or EOS, and a run without -z uses every -n position. |
To serve a model to other programs over HTTP with the OpenAI API, use ml_server instead; see Server.
When it finishes, ml_run prints two lines to stderr:
prefill: P tokens in M ms (R tok/s), when the prompt has more than one token;positions: N, tok/s: X, threads: T, isa: I, packed: G, kv: K, whereGis the pack group the weights were repacked to, or 0, andKthe KV cache type.
Exit codes:
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | load or runtime error (bad file, out of memory) |
| 2 | usage error |
| 3 | the CPU lacks the ISA the binary was built for |
Threads
Build with THREADS=pthread and pass -t N. The pool waits by spinning briefly, then sleeps, so consecutive operations within a token cost no system calls. Matrix-vector products smaller than 2^18 weights, and small attention steps, run on the calling thread only.
On a 4-vCPU VM with the avx512vnni build, -t 4 decodes a Q4 model with 4 layers and dim 2048 at 503 tok/s, against 140 tok/s for -t 1. Threading never changes the output: -t N produces logits bit-identical to -t 1.
Performance notes
- Q4 or Q8: prefer Q4 on large models. Decoding is limited by memory bandwidth, and Q4 moves 45% fewer bytes. With packed kernels, Q4 is the faster format on every ISA that has them.
- Prefill: long prompts are prefilled in batches of 128 tokens, which is 2–6× faster than token by token, depending on the ISA and the model size.
- Long contexts: attention grows with the position and, past a few thousand tokens, takes a third or more of the time.
--kv f16halves the KV cache at no measurable cost;--kv q8cuts its size and memory traffic by about 3.8×, at some cost in perplexity (see--kv). - Memory: packing makes one copy of the weights at load, a matrix at a time, releasing each original’s pages as it goes, so the peak is about the model plus its largest matrix. With
--no-packthere is no copy at all, and--pack-cachemaps a copy saved by an earlier run. - Start-up: packing takes time at every start: 1.8 s for Llama 3.2 1B Q4 on one core (0.7 s with
-t 4). With--pack-cachelater starts map the packed weights directly and take 0.13 s, at the cost of a second copy on disk.
Limits
- Dimensions:
dim,hidden_dim,n_layersand head counts can be at mostvocabandmax_seqcan be at most 2^24.
- Shapes:
dimandhidden_dimmust be multiples of 32,head_dimmust be even, andn_headsmust be a multiple ofn_kv_heads. - Memory: the KV cache uses
n_layers × ctx × kv_dim × 8bytes,n_layers × ctx × kv_dim × 4with--kv f16, orn_layers × ctx × (kv_dim + 4 × n_kv_heads) × 2with--kv q8, wherectxis--ctx. All size arithmetic is overflow-checked, so on 32-bit ARMv7 an oversized model fails without of memoryinstead of crashing. - Architecture: Llama-family decoders (Llama, Mistral, Qwen2, Qwen3, SmolLM3), Gemma 2 and 3, and Qwen3.5’s hybrid of linear attention and attention: RMSNorm, SwiGLU or GELU, GQA, RoPE (on part of each head, or none in chosen layers), biases only on the q, k and v projections (Qwen2), optional per-head q/k RMSNorm (Qwen3), block output norms and soft-capping (Gemma), a sliding window and a second RoPE base for chosen layers, and the gated delta rule with a gated attention output (Qwen3.5).
tools/convert.pyrefuses other architectures, including mixture of experts (Qwen3.5 35B-A3B and larger). - Linear attention: a linear-attention layer keeps a fixed state instead of a KV cache, which cannot be rewound: a run that does not continue the last one starts over from position 0.
ml_serverreuses a previous request only when the new prompt continues all of it.