In short: rml's budget is a 16 GB Apple Silicon Mac with the inference process under about 11 GB peak RSS, and
docs/models.md works the numbers for each target from its config.json: weights at a given
bits per weight, plus a KV cache, prefill scratch and runtime overhead. Llama 3.1 8B fits at Q8_0 (about 10.2 GB, tight)
and Q4_K_M (about 6.6 GB, the performance target) but not bf16; Qwen3 8B is the second dense architecture;
Qwen3-30B-A3B does not fit, so OLMoE-1B-7B proves the MoE path. In the code, ParameterCount reproduces the
parameter counts and its tests pin them. Five real checkpoints, from SmolLM2-135M to OLMoE-1B-7B, pass golden tests;
the 8B milestone models and every measurement of peak RSS are still planned.
The models page sits beside the rest of rml's planning documents: the milestone in
progress.md L5–8 is that rml-chat loads Llama 3.1 8B
Instruct at 4-bit by mmap, stays under about 11 GB peak RSS, matches the reference top-1 tokens and decodes within
1.5× of llama.cpp, and the README's goal says the same (README.md L8–10).
How weights stay mapped is on Model files; the block formats behind "bits per weight" are on
Quantization; the concepts are quantization and
the KV cache.
The budget and the formula
The process gets about 11 GB so macOS, the window server and a browser keep about 5 GB. Weights are mmapped and only count toward RSS as pages are touched, but decode touches every weight on every token, so in steady state the whole file is resident: the document plans as if it all counts (models.md L3–6). Every table on the page is built from the same parts (L8–22):
| Item | Formula or figure | Meaning | Source |
|---|---|---|---|
P | attention + MLP + norms + embeddings + LM head | Parameter count computed from each config.json, not taken from the model's marketing name. | L10–11 |
| Bits per weight | bf16 16, Q8_0 8.5, Q4_0 4.5, Q4_K ≈ 4.5; a llama.cpp Q4_K_M file ≈ 4.9 | Q8_0 is 32 int8 values plus one f16 scale per block; Q4_K_M keeps part of ffn_down, attn_v and the output at Q6_K. | L12–14 |
| KV cache | 2 × layers × kv_heads × head_dim × context × bytes | Keys and values for every layer and position. | L15 |
| Activations | < 10 MB for decode; 512 × max(ffn, q_dim) floats per buffer for a 512-token prefill chunk | A few hidden-size vectors per layer when decoding; at most a few hundred MB for prefill. Logits only for the last prompt token. | L16–18 |
| Runtime | 300 MB | Code, tokenizer tables, thread stacks, allocator slack. | L19 |
Figures are decimal GB, and the document asks that they be checked against the downloaded file size before anyone
relies on them (L21–22). Two of its assumptions run ahead of the code. The prefill
row assumes 512-token chunks, but Session::Generate runs the whole unmatched prompt in one
Forward at the pin, so chunked prefill is planned (see
Generation). And no code at the pin calls madvise, so dropping each
layer's pages after a full-precision golden run (L41–43) is
planned too.
The parameter count in code
ParameterCount(const ModelConfig&) is the same
sum, counting a tied head once (config.cc L495–506): per layer,
the Q, K, V and output projections, any q/k/v bias and QK-norm weights, three hidden × intermediate
matrices per expert (one expert for a dense model) plus the router, and two norm vectors (four with Gemma 3's sandwich
norms); then the final norm and the embedding, doubled when the head is untied. The config tests pin it for each
family, so the numbers docs/models.md quotes are checked in CI:
| Config | ParameterCount | models.md says | Test |
|---|---|---|---|
SmolLM2-135M (committed smollm2_135m.config.json) | 134,515,008 | 0.135 B | config_test.cc L27–50 |
Llama 3.1 8B (committed llama31_8b.config.json) | 8,030,261,248 | 8.03 B | L52–65 |
| Qwen2 0.5B-shaped (inline) | 494,032,768 | — | L67–80 |
| Qwen3 0.6B-shaped (inline) | 596,049,920 | — | L82–95 |
| Mixtral 8×7B-shaped (inline) | 46,702,792,704 | — | L107–120 |
| OLMoE-1B-7B (inline) | 6,919,161,856 | 6.9 B | L136–161 |
| Gemma 3 1B (inline) | 999,885,952 | — | L163–186 |
Qwen3 8B and Qwen3-30B-A3B have no config in the tests; their counts (8.19 B and 30.5 B) come only from the document.
Milestone: Llama 3.1 8B Instruct
32 layers, hidden 4096, 32 query heads and 8 KV heads (GQA 4:1), head_dim 128, SwiGLU FFN 14336, vocab 128256, an
untied head, RoPE θ = 500000 with llama3 frequency scaling (factor 8, low/high factors 1 and 4, original
context 8192). P = 8.03 B, of which the embedding and the untied head are 0.53 B each
(models.md L24–31); the same scaling fields are what
ParsesLlama31RopeScaling checks.
| Component | bf16 | Q8_0 | Q4_K_M |
|---|---|---|---|
| Weights | 16.06 GB | 8.53 GB | ~4.9 GB |
| KV cache, 8K ctx, f16 | 1.07 GB | 1.07 GB | 1.07 GB |
| Prefill scratch (512-token chunk) | 0.3 GB | 0.3 GB | 0.3 GB |
| Runtime | 0.3 GB | 0.3 GB | 0.3 GB |
| Total | ~17.7 GB, does not fit | ~10.2 GB, fits, tight | ~6.6 GB, fits |
The consequences (L41–45): bf16 cannot be the runtime format, so full-precision
checks on the 8B model either page through the mmap layer by layer or run Q8_0 against bf16 reference logits; Q8_0 fits
with about 0.8 GB to spare and an int8 KV cache adds headroom; Q4_K_M is the main performance target. rml can run both
quantized formats: DotActivationType pairs Q8_0 and Q4_0 weights with Q8_0 activations, and Q4_K, Q5_K
and Q6_K (the mix inside a Q4_K_M file) with Q8_K (dot.cc
L285–297).
Worked example: the KV cache
With the Llama 3.1 8B shape, one position in one layer stores kv_heads × head_dim = 8 × 128 = 1024 keys
and as many values. At f16 and an 8192-token context:
2 (K, V) × 32 layers × 8 kv_heads × 128 head_dim × 8192 tokens × 2 bytes
= 1,073,741,824 bytes ≈ 1.07 GB
rml's KvCache holds exactly that per position and layer as a row of row_bytes()
(kv_cache.h L55–56): 4096 bytes at f32, 2048 at f16. Its int8
mode is not quite half of f16, because each group of quant_group() channels (at most 32) carries one f32
scale, 1.125 bytes per entry (L13–18): a 1152-byte row here,
so about 0.60 GB for 8K tokens where models.md L44 rounds the int8 cache to 0.54 GB
by leaving the scales out. The cache grows a 16-token page at a time out of an arena reserved up front, and pages it
never touches are never made resident. KvCache::Create defaults to f32
(L40–43), but rml-chat asks for f16 and a 4096-token
context unless told otherwise (main.cc L48–49), which is
0.54 GB for this model. The layout is drawn on The decoder.
Why the memory number is also the speed number
Decode is memory bound: each token reads every weight once. At Q4_K_M (about 4.9 GB a token) the ceiling is about 14 tok/s on an M1 (68 GB/s), 20 on an M2 or M3 (100 GB/s) and 24 on an M4 (120 GB/s), and achieved GB/s as a fraction of that is meant to be the headline benchmark (L47–49). That benchmark, beside llama.cpp on the same file, is planned (progress.md L71); see Kernels.
Second architecture: Qwen3 8B
36 layers, hidden 4096, 32 query and 8 KV heads, head_dim 128, FFN 12288, vocab 151936, an untied head, per-head
RMSNorm on Q and K before RoPE, θ = 1000000; P = 8.19 B
(models.md L51–56).
| Component | Q8_0 | Q4_K_M |
|---|---|---|
| Weights | 8.70 GB | ~5.0 GB |
| KV cache, 8K, f16 | 1.21 GB | 1.21 GB |
| Scratch + runtime | 0.6 GB | 0.6 GB |
| Total | ~10.5 GB, fits, very tight | ~6.8 GB, fits |
The table is L58–63. Qwen3 differs from Llama only in config-driven ways, which is
the point of choosing it: the config-driven DecoderLM has to absorb QK-norm, a different θ and no RoPE
scaling with no new model class (L65–66); how it does is on
The decoder. The document also flags that newer releases (Qwen3.5, newer Gemma
and Llama generations) could not be checked from a sandbox without Hugging Face access, and asks that the table be
redone before Phase 6, preferring a newer dense ~8B model if one fits (L68–71).
Mixture of experts
A sparse model touches only its active experts per token, but the whole file still has to be stored somewhere. Mixture of experts explains the routing; here is what it does to the budget (models.md L73–96):
| Model | Shape | P (total / active) | Footprint | Verdict |
|---|---|---|---|---|
| Qwen3-30B-A3B | 48 layers, hidden 2048, 128 experts (8 active, FFN 768), 4 KV heads | 30.5 B / 3.0 B | Q4_K_M ~19 GB with KV; Q3_K_M ~15 GB | Does not fit: over physical RAM, or over the 11 GB budget |
| OLMoE-1B-7B | 16 layers, hidden 2048, 64 experts (8 active, FFN 1024), 16 KV heads, vocab 50304 | 6.9 B / ~1.3 B | Q8_0 ~8.5 GB, Q4_K_M ~5.2 GB with a 4K f16 cache | The MoE path proof |
| Qwen1.5-MoE-A2.7B | — | 14.3 B | ~8.8 GB at Q4 | Fallback if OLMoE's router needed special handling |
Streaming experts from SSD through the page cache is rejected: which experts a token touches changes every token, so the resident set still grows toward the whole file and the OS would thrash (L83–85). OLMoE won out without special handling; its real-weight golden passes, and a Qwen1.5-MoE golden is listed only as a later step (progress.md L293).
Status per model
Three sources describe where each model stands, and they are not equally current. The README's table
(README.md L249–256) lists only the targets and test models;
progress.md L58–59 lists the real-weight goldens that have run; and
the code says what is actually checked: real_model_test's case list
(real_model_test.cc L130–134) and the golden files
under rml/llm/testdata/goldens.
| Model | Role | Formats (README) | Real-weight golden | Status at the pin |
|---|---|---|---|---|
| Llama 3.1 8B Instruct | Milestone | GGUF Q4_K_M, Q8_0; safetensors bf16 | none | planned: needs the Mac (progress.md L291–292); config and parameter count tested |
| Qwen3 8B | Second architecture | GGUF Q4_K_M, Q8_0 | none | planned; the family is covered by Qwen3-0.6B's golden |
| OLMoE-1B-7B-0924-Instruct | MoE proof | GGUF Q4_K_M, Q8_0; safetensors bf16 | OLMoE-1B-7B-0924-Instruct.json | Golden-tested, worst logit error 1.3e-4 |
| SmolLM2-135M-Instruct | CI test model | safetensors, GGUF | SmolLM2-135M-Instruct.json | Golden-tested (1.4e-4), and the only checkpoint CI downloads |
| TinyLlama-1.1B-Chat-v1.0 | Local test model, Llama-shaped | safetensors, GGUF | TinyLlama-1.1B-Chat-v1.0.json | Golden-tested (1.1e-4) |
| Qwen3-0.6B | Qwen3 family check | not in the README | Qwen3-0.6B.json | Golden-tested (1.1e-4) |
| gemma-3-1b-it | Gemma 3 family check | not in the README | gemma-3-1b-it.json | Golden-tested (1.7e-4); progress.md L79 still calls real Gemma 3 weights untested |
The worst-error figures are the ones progress.md L58–59 reports, all in f32 and all run in the development container; the test itself allows 2e-3 per logit (real_model_test.cc L33). The README's status line says phases 1–7 are done apart from the macOS-only work: real-weight golden tests, NEON and Metal kernels, and the llama.cpp comparison (README.md L15–16).
Test models
| Model | Shape | P | bf16 | Q8_0 | Use |
|---|---|---|---|---|---|
| Random toy config | 2 layers, hidden 64, 4 heads, 2 KV heads, vocab 256 | ~0.1 M | — | — | Unit tests, no download |
| SmolLM2-135M | 30 layers, hidden 576, 9 heads, 3 KV heads, tied head | 0.135 B | 0.27 GB | 0.14 GB | CI golden tests |
| TinyLlama 1.1B | 22 layers, hidden 2048, 32 heads, 4 KV heads | 1.10 B | 2.20 GB | 1.17 GB | Local golden tests |
SmolLM2-135M uses the Llama block with a tied head and GQA 3:1, so it runs the same code path as the 8B target at a
size CI can download and run in seconds (models.md L98–107). The toy configs are
generated by tools/make_llm_fixtures.py and live in the repository; how they and the real goldens are made
is on Build and testing.
Getting the weights
Weights live in a gitignored models/ directory and are never committed
(AGENTS.md L40).
tools/download_model.py fetches a Hugging Face
checkpoint into models/<name> using only the standard library:
| Item | Behaviour | Source |
|---|---|---|
| Arguments | repo (owner/name), --revision (default main), --dest (default models/<name>) | L49–55 |
| Files | The six configs rml reads (config.json, generation_config.json, model.safetensors.index.json, special_tokens_map.json, tokenizer.json, tokenizer_config.json) and top-level *.safetensors, sharded or not | L34–45 |
| Pinning | Resolves the revision to a commit through the hub API and downloads every file from that commit | L57–68 |
| Resume | Skips a file already present at the expected size; writes to .partial and retries a short transfer up to 3 times | L64–78 |
| Gated models | Llama and Gemma need a token with access in $HF_TOKEN, sent as a bearer header | L27–31 |
Only safetensors are downloaded; GGUF files such as the README's llama-3.1-8b-q4_k_m.gguf are fetched by
hand. CI uses the script for one model, SmolLM2-135M-Instruct at a pinned revision, cached between runs
(ci.yml L48,
L56–63).
Tests
| File | Tests | What it pins |
|---|---|---|
config_test.cc | 19 | Each family's parsed config, and ParameterCount for seven of them, including the committed Llama 3.1 8B and SmolLM2-135M configs. |
real_model_test.cc | 3 per model, 5 models | Tokenizer ids, the chat prompt, and top-8 logits plus an 8-token greedy continuation against each golden; skipped when $RML_MODELS_DIR has no copy (L1–6). |