In short: there is one model class and no per-family code. ModelConfig is a base recipe (pre-norm
RMSNorm, RoPE, grouped-query attention, a gated FFN) plus a dozen switches (q/k/v bias, QK-norm, sliding window,
sandwich norms, embedding scale, experts), read from config.json or GGUF metadata. WeightSource
names every tensor by its GGUF name and maps Hugging Face names onto it, so both formats load through the same
code, and matrices stay views into the mmapped file. DecoderLM::Forward runs any number of tokens at
positions cache.size() onward: embed, then per layer norm → q/k/v → RoPE → store in the KV cache →
one-pass online-softmax attention → output projection added into the residual, then norm → FFN (or top-k experts)
added into the residual. After a prefill, decoding one token at a time allocates nothing, and each family is
pinned by golden logits from a float64 reference on toy models in both formats, plus real-checkpoint goldens.
The concepts are on /ml-models: decoder-only LLMs, attention,
the KV cache, positional encoding and
mixture of experts. The code is in rml/llm:
config, weight_source, decoder, attention, rope,
kv_cache and kernels. The files it reads are on Model files,
the weight formats it multiplies on Quantization, the token ids it takes on
Tokenizers, and what is done with its logits on Generation.
The model config
config.h states the design in its header: every supported model is one recipe with switches, and
the architectures differ only in which switches they flip
(config.h L4–20). ModelConfig carries the sizes and
those switches (L59–111):
| Item | Type / default | Meaning | Source |
|---|---|---|---|
architecture | std::string | The Hugging Face model_type ("llama", "qwen3_moe"); it also selects tensor names. | L60–61 |
vocab_size, hidden_size, intermediate_size, num_layers | int64, 0 | Sizes. For a mixture of experts intermediate_size is each expert's width. | L62–66 |
num_heads, num_kv_heads, head_dim, max_position | int64, 0 | Attention shape; q_size() = heads × head_dim, kv_size() = kv heads × head_dim. | L67–70, L109–110 |
rms_norm_eps, rope_theta, rope_scaling | 1e-6, 10000, none | Norm epsilon, RoPE base and one of five scaling schemes (see RoPE). | L71–73 |
tie_word_embeddings | bool, false | The output head reuses the embedding matrix. | L74–75 |
attention_bias, qk_norm, qk_norm_whole | bool, false | Biases on q, k and v; RMSNorm on each head's queries and keys, or (OLMoE) across all heads at once. | L76–79 |
activation | GluActivation, kSilu | SiLU (SwiGLU) or GELU-tanh (GeGLU). | L80 |
embedding_scale, sandwich_norms, attention_scale | 1, false, unset | Gemma's switches: embeddings times a constant, a second norm on each branch's output, and a query-key scale other than 1/√head_dim. | L81–87 |
sliding_window, sliding_layers, rope_local_theta | unset, empty, unset | A window each token attends within; which layers use it (empty means all); the RoPE base of sliding layers. | L88–95 |
num_experts, num_experts_per_token, normalize_expert_weights | 0, 0, true | Zero experts means a dense FFN; otherwise top-k routing, with the chosen weights optionally renormalized to sum to one. | L96–100 |
bos_token_id, eos_token_ids | optional, list | Special ids the chat example uses as stop tokens. | L101–102 |
From config.json
ParseHfConfig accepts eight model_types: llama, mistral,
mixtral, qwen2, qwen3, qwen3_moe, gemma3_text and
olmoe (config.cc L23–24), and returns
Unsupported for anything else (L292–367). Along the way it:
- takes the activation from
hidden_activationorhidden_act(SiLU orgelu_pytorch_tanh) and rejectsmlp_bias; - defaults
num_key_value_headsto the head count andhead_dimto hidden / heads, andrms_norm_epsto 1e-5 for Mixtral and OLMoE, 1e-6 otherwise, the defaults of transformers' config classes (L326–329); - reads RoPE from
rope_parameters(preferring itsfull_attentionset) orrope_scaling(L83–113); - derives the switches from the architecture: Gemma ties its head by default; Qwen2 always has q/k/v bias; Qwen3, Qwen3-MoE, Gemma and OLMoE have QK-norm, OLMoE's spanning every head (L339–345);
- rejects OLMo's
clip_qkvand Qwen'suse_sliding_window, and readssliding_windowonly for non-Qwen models, since Qwen configs carry one they do not use (L346–356); - reads the expert keys per family (ParseMoe, L185–206) and Gemma's extras (ParseGemma, L115–162), then the BOS and EOS ids, and finally
ValidateConfig.
ValidateConfig checks every size is positive, the heads split evenly into kv groups, head_dim is even
(RoPE rotates pairs), the epsilon, theta, scaling factor and scales are positive, sliding_layers has one
flag per layer, and experts per token lie in [1, num_experts]
(L257–290).
From GGUF metadata
ModelConfigFromGguf reads general.architecture and then the <arch>.* keys
(L379–493). GGUF names differ: llama becomes
mixtral when it has an expert_count, qwen3moe becomes qwen3_moe and
gemma3 becomes gemma3_text. rml's fixtures write Mistral as llama with an
attention.sliding_window key (make_llm_fixtures.py L196–197),
so it loads as a Llama with a window. Partial rotary embeddings are rejected. The switches GGUF has no key for are read off the tensor table
instead (L467–473): a missing output.weight means a tied
head, a blk.0.attn_q.bias means q/k/v bias, a blk.0.attn_q_norm.weight means QK-norm (whole when
its length is not head_dim), and a rope_freqs.weight tensor means Llama 3.1's precomputed frequency factors.
The vocabulary size falls back to the embedding's row count.
Expert weights are renormalized for every MoE architecture but OLMoE unless the file has
expert_weights_norm, following llama.cpp (L421–436). Gemma 3's
GGUF keys get their own reader: an explicit gemma3.attention.scale, or llama.cpp's default (the 27B model,
62 blocks, scales by hidden / heads, the rest by head_dim); the local RoPE base; and the sliding pattern as a period or
one flag per layer (L208–253).
ParameterCount counts attention, FFN or experts plus router, two or four norms per layer, the final
norm and the embedding, the head only when untied (L495–506);
rml's memory tables are built from that count (Models and memory).
Weights: WeightSource
A model's tensors are looked up by their GGUF names whichever file holds them; a Hugging Face checkpoint is read through the matching name, and matrices are views into the mapped files while only norm weights and biases are copied out as f32 (weight_source.h L4–11).
GGUF name (after blk.N.) | Hugging Face name (after model.layers.N.) |
|---|---|
attn_norm.weight | input_layernorm.weight |
attn_q / attn_k / attn_v .weight, .bias | self_attn.q_proj / k_proj / v_proj .weight, .bias |
attn_output.weight | self_attn.o_proj.weight |
attn_q_norm.weight, attn_k_norm.weight | self_attn.q_norm.weight, self_attn.k_norm.weight |
ffn_norm.weight | post_attention_layernorm.weight (Gemma: pre_feedforward_layernorm.weight) |
post_attention_norm.weight, post_ffw_norm.weight | Gemma only: post_attention_layernorm.weight, post_feedforward_layernorm.weight |
ffn_gate / ffn_up / ffn_down .weight | mlp.gate_proj / up_proj / down_proj .weight |
ffn_gate_inp.weight (router) | block_sparse_moe.gate.weight (Mixtral) or mlp.gate.weight |
token_embd, output_norm, output (global) | model.embed_tokens, model.norm, lm_head |
weight_source.cc L33–88. Note how Gemma reuses the name
post_attention_layernorm for a different norm: in Llama it is the norm before the FFN, in Gemma the
sandwich norm after attention, so Gemma's table is checked first.
Matrix(name, rows, cols) fetches the raw bytes, checks the shape, and wraps them in a
WeightMatrix: a type, a row and column count, and a span of the mapping
(L134–167; kernels.h L22–37).
GGUF tensors keep their stored type (F32, F16, BF16, Q8_0, Q4_K, …); safetensors tensors must be f32, f16 or bf16.
MakeWeightMatrix checks the columns are whole blocks and the byte count is exact, and dequantizes one
block as a probe so integer types, which have block layouts but no meaning as weights, are refused
(kernels.cc L43–64).
Experts come two ways (L169–197): GGUF stacks each
layer's experts into one [experts, rows, cols] tensor (ffn_gate_exps, ffn_up_exps,
ffn_down_exps) and expert e is the e-th slice of its bytes; Hugging Face stores one matrix
per expert, block_sparse_moe.experts.E.w1/w3/w2 in Mixtral and mlp.experts.E.gate_proj/up_proj/down_proj
in Qwen3-MoE and OLMoE. Vector converts a norm or bias to f32, and adds 1 to a Hugging Face Gemma norm
weight, because Gemma's RMSNorm scales by (1 + w) and GGUF converters already store 1 + w
(L199–217). One RmsNorm then serves every family.
A known gap: for a safetensors tensor that is not aligned, Raw keeps a span of a copy that is freed when it
returns; see Model files.
DecoderLM::LoadLayers asks for every tensor the config implies (q/k/v biases only with
attention_bias, q/k norms only with qk_norm, sandwich norms only for Gemma, a router and
num_experts × 3 expert matrices instead of a dense FFN for MoE), so a missing or misshapen tensor fails
at load with its file name and shapes, not mid-forward (decoder.cc L100–159).
The embedding is a WeightMatrix too; Forward dequantizes just the rows of the tokens it sees
(L226–233).
One forward pass
Each block is pre-norm: x += o_proj(attention(rope(q(norm(x))), rope(k(norm(x))), v(norm(x)))) then
x += ffn(norm(x)), and Gemma normalizes each branch's output too before the add
(decoder.h L10–25).
Forward(tokens, cache, logits, all_logits) runs n tokens at positions
cache.size() onward; the same call is a prefill (n = prompt length) or one decode step (n = 1)
(decoder.cc L204–273).
Step by step:
- Checks. An empty input, an id outside the vocabulary, a
logitsspan of the wrong size or a cache made for another model isInvalidArgument; a full cache isOutOfRangefromcache.Extend(n), and in every case nothing has changed yet (L206–223). - Scratch.
Reserve(n)grows eachWorkspacebuffer to the largest call so far: x, normed, q, k, v, the attention output, Gemma's branch buffer, the gate, the router scores, the cos and sin tables, and the int8 input scratch sized for the largest matrix (L165–197). After a prefill, a one-token step never grows anything. - Embed. Each token's embedding row is dequantized into x, times
embedding_scale(√hidden for Gemma); then the cos and sin of every rotary pair are computed once per position, for the global RoPE and, when there is one, the local one (L226–242). - Attention half.
RmsNormeach row into normed, thenResidualrunsAttentioninto the stream (L289–325):MatMulManycomputes q, k and v; biases are added; QK-norm normalizes each head (or each whole row); RoPE rotates q and k in place; k and v go to the cache;Attendwrites the attention output; and the output projection is aMatMulwithaccumulate = true, so it adds straight into x. - FFN half.
RmsNormagain, thenDenseFfn(MatMulGluwrites act(gate) × up, the down projection accumulates into x) orMoeFfn(L327–331). - Sandwich norms. With
sandwich_norms,Residualzeroes a branch buffer, runs the block into it, normalizes each row with the post norm and only then adds it to x (L275–287). - Logits. Only the rows that need logits (the last, or all with
all_logits) get the final norm and the output product (L266–272).
The kernels underneath
The arithmetic lives in kernels.h, written as plain loops over f32 activations and weights in their
stored format; a quantized row is expanded a few blocks at a time, never as a whole matrix
(kernels.h L4–10). Everything here is parallel over the output rows
of the weight in grains of 16 (kernels.cc L149–156) and never allocates.
| Function | Signature | What it does | Source |
|---|---|---|---|
MatMul | (w, x, n, y, pool, scratch, accumulate) | y[n, rows] = x · wᵀ. With scratch for a quantized type, x is quantized to int8 once and each output is quant::VecDot against the stored row; otherwise each weight row is expanded 256 elements at a time on the stack and reused for up to 64 tokens. accumulate makes it y +=. | L173–190 |
MatMulMany | (ws, x, n, ys, pool, scratch) | Several matrices sharing an input (q, k, v): one quantization, every row of every matrix in one ParallelFor. | L192–216 |
MatMulGlu | (gate, up, act, x, n, y, pool, scratch) | The gate and matching up row in one task, writing act(g) × u directly; no gate or up buffer. | L218–238 |
RmsNorm | (x, weight, size, eps, out) | x / √(mean(x²) + eps) × weight. | L250–259 |
Glu, Softmax | SiLU x / (1 + e⁻ˣ) or PyTorch's tanh GELU, times up; max-subtracted softmax in place. | L158–169, L261–278 | |
MatMulRef | (w, x, n, y) | The reference: expand a whole row, then dot. | L240–248 |
The three fusions (shared input, gated FFN, residual add as accumulate) and their measurements are on Kernels; the int8 dot products on Quantization.
Attention and the KV cache
The paged cache
KvCache keeps every earlier token's keys and values, in pages of page_size tokens
(NewCache uses 16), each page holding that span for every layer. A page table maps a position to its
page, and all pages come out of one arena reserved up front, so nothing is allocated while decoding and untouched
pages are never made resident (kv_cache.h L4–19). The arena is
calloced because large zeroed allocations come straight from mmap
(kv_cache.cc L20–62).
| Item | Signature | Meaning | Source |
|---|---|---|---|
Create | (config, max_tokens, page_size = 16, dtype = kF32) | Room for max_tokens rounded up to pages; f32, f16 or i8. (DecoderLM::NewCache defaults to f16.) | h L41–44 |
Extend | (n) → Status | Counts n more positions and maps pages from the free stack; OutOfRange, changing nothing, past capacity. | cc L100–112 |
Truncate, Clear | (size) | Forget positions from size on and push whole pages back; the last freed is reused first. Generation reuses a shared prefix and speculative decoding rolls back rejected drafts with it. | cc L114–121 |
Store | (layer, position, key, value) | Writes kv_size floats of each, converted to the cache's dtype. | cc L64–82 |
Key/Value, KeyF16/…, KeyI8/… | (layer, position) | One position's row in the cache's own dtype; an int8 row is its scales then its values. | h L71–94 |
A row is one position's keys (or values) for one layer, all kv heads: kv_size entries. Inside the arena
rows are ordered page, then layer, then key/value, then slot:
row = ((page · num_layers + layer) · 2 + which) · page_size + position mod page_size
(kv_cache.h L102–109), so one layer's keys for a page are
page_size contiguous rows.
An int8 cache stores each group of channels as round(x / s) with one f32 scale s = max|x| / 127. The group is the largest power of two dividing head_dim, at most 32, so groups never straddle two heads, and a row is its scales first (keeping them 4-byte aligned) then its values padded to a multiple of 4 (L37–46, L84–98).
Worked example: Llama 3.1 8B. 32 layers, 8 kv heads of 128, so kv_size = 1024.
| dtype | row_bytes | per token (32 layers × K and V) | 8192 tokens |
|---|---|---|---|
| f32 | 4096 | 256 KiB | 2 GiB |
| f16 (default) | 2048 | 128 KiB | 1 GiB (1.07 GB, as models.md L33–39 budgets) |
| i8 (group 32: 32 scales + 1024 values) | 128 + 1024 = 1152 | 72 KiB | 576 MiB |
Attend
Query i at position start + i attends to every cached position j from the window start (0,
or pos − window + 1) to itself, through the kv head its query head shares with its group, and Attend
does it in one pass of online softmax: a running maximum and normalizer per query head, the partial output rescaled
when the maximum grows, no score array ever materialized (attention.h L4–16).
Attend picks AttendGroup<float>, <Float16> or
<int8_t> by the cache's dtype and runs n × num_kv_heads tasks on the pool with grain 1
(attention.cc L141–166). Inside a task
(L67–129) keys are taken 32 at a time, each key row widened to
f32 into a stack buffer as it is read (HeadRow, L42–65) and dotted
against every query in the group with four partial sums (L24–37).
Per-task state lives on the stack, which is why the group is limited to 16 query heads and head_dim to 512
(attention.h L34–37). AttendRef is the definition:
every score, a softmax, a weighted sum (L168–213).
For Llama 3.1 8B a decode step is 8 tasks per layer, each serving 4 query heads. rml's own measurement of one decode
step's attention at that shape, on one x86 core of the development container: 1.3 ms at 512 tokens and 25 ms at 4096
with an f32 cache, against 3.3 and 64 ms for AttendRef; 32 ms with f16 (no F16C on that SSE2 build) and
19 ms with int8 at 4096 (kernels.md L170–194, from
attention_benchmark).
Rotary embeddings
Pair i of a head's dimensions turns at inv_freq[i] = θ−2i / head_dim radians per position, so
q · k depends only on the distance between two tokens; the scaling schemes stretch the slow pairs so a model trained
on a short context reads positions beyond it (rope.h L4–11).
Rope::Create computes the frequencies and applies the scaling once
(rope.cc L73–112); Angles gives each pair's cos and sin at a
position, times YaRN's attention factor (L114–121); Apply
rotates heads in place (L123–140).
| Scaling | What it does to inv_freq | Read from | Source |
|---|---|---|---|
kNone | Unchanged. | "default", or no key | |
kLinear | Every frequency ÷ factor (position interpolation). | "linear"; GGUF rope.scaling.type | L91–93 |
kLlama3 | Wavelengths longer than original / low_freq_factor ÷ factor; shorter than original / high_freq_factor kept; a smooth blend between. | "llama3" | L20–37 |
kFrequencyFactors | Each frequency ÷ its entry in rope_freqs.weight: the same ramp, precomputed per dimension. | GGUF Llama 3.1 files | L97–106 |
kYarn | Pairs turning more than beta_fast times over the original context kept, fewer than beta_slow times ÷ factor, a linear ramp between; cos and sin × (0.1 ln factor + 1). | "yarn"; GGUF | L39–69 |
Worked example: Llama 3.1 8B (factor 8, low and high frequency factors 1 and 4, original context 8192; models.md L26–28). Pairs whose wavelength 2π / inv_freq is under 8192 / 4 = 2048 positions keep their frequency, those over 8192 / 1 = 8192 turn eight times slower, and between them smooth = (8192 / wavelength − 1) / 3 blends the two, so a pair with a wavelength of 4096 positions gets (1 − 1/3) of the slowed frequency plus 1/3 of the original.
Which dimensions pair. Hugging Face's rotate_half pairs dimension i with i + head_dim/2
(kHalf); llama.cpp's converter permutes Llama-family q and k rows so that pairs are adjacent, (2i, 2i + 1)
(kInterleaved) (rope.h L22–29). rml does not undo the
permutation: it rotates in whichever order the file uses. FromHf always uses kHalf;
FromGguf uses kInterleaved for llama, mistral and mixtral and kHalf for the rest
(decoder.cc L51–56, L66–79).
Two bases. When the config has rope_local_theta (Gemma 3), Create builds a
second Rope with that base and no scaling, and sliding layers use it while global layers use the main
one (decoder.cc L81–98, L307–311).
| Family | RoPE in rml's tests and targets |
|---|---|
| Llama 3.1 | llama3 scaling from config.json; kFrequencyFactors from GGUF. The toy llama fixture uses llama3 scaling with an original context of 64. |
| Qwen2, Qwen3 | No scaling at θ = 10⁶ for Qwen3 8B (models.md L53–54); YaRN is supported and the toy qwen3 fixture uses it (factor 4). |
| Gemma 3 | Global layers at rope_theta with its scaling (the toy fixture: linear, factor 2); sliding layers at rope_local_base_freq (default 10000), unscaled. |
Fixture settings from make_llm_fixtures.py L47–98.
Mixture of experts
With experts, the FFN half is MoeFfn (decoder.cc L333–369).
The router is one more WeightMatrix (ffn_gate_inp, [num_experts, hidden]); a
MatMul scores every token against every expert, and then, token by token:
- softmax the token's router scores over all experts;
- pick the top k by repeated selection, ties going to the lower expert id, summing the chosen probabilities;
- for each chosen expert, weight = pe / total when
normalize_expert_weights, else pe; run that expert'sMatMulGluand down projection on the one token, and add weight × output into the residual stream.
Experts run one token at a time, so the gate buffer is one row wide (L174–177), and chosen ids sit in a stack array of 256, the most experts a model may have (L339–342). In a prefill that means one expert product per token per chosen expert rather than grouping tokens by expert; at decode, where n = 1, it is the same work.
| Family | Experts and width | Renormalize top-k? | Source |
|---|---|---|---|
| Mixtral | num_local_experts, num_experts_per_tok; width intermediate_size | Always | config.cc L186–189 |
| Qwen3-MoE | num_experts, num_experts_per_tok; width moe_intermediate_size. A decoder_sparse_step other than 1 or any mlp_only_layers (dense layers mixed in) is Unsupported. | norm_topk_prob, default false | L190–199 |
| OLMoE | num_experts, num_experts_per_tok | norm_topk_prob, default false (GGUF: off unless expert_weights_norm) | L200–204 |
OLMoE-1B-7B (64 experts, 8 active) is rml's MoE proof on a real checkpoint; Qwen3-30B-A3B does not fit the 16 GB budget (models.md L73–96).
The eight families
Family (model_type) | What differs from the base recipe | GGUF architecture | Golden tests |
|---|---|---|---|
Llama (llama) | The base: RMSNorm, RoPE (Llama 3 scaling), GQA, SwiGLU. | llama, interleaved RoPE | toy llama (safetensors, GGUF, Q8_0 GGUF); real SmolLM2-135M-Instruct, TinyLlama-1.1B-Chat |
Mistral (mistral) | A sliding window on every layer. | llama + attention.sliding_window | toy mistral (window 4) |
Mixtral (mixtral) | Routed experts (eight, two per token in the real model), always renormalized. | llama + expert_count | toy mixtral (4 experts, top 2) |
Qwen2 (qwen2) | Biases on q, k and v; its unused sliding_window ignored. | qwen2, half RoPE | toy qwen2 |
Qwen3 (qwen3) | Per-head RMSNorm on q and k; explicit head_dim. | qwen3 | toy qwen3 (YaRN); real Qwen3-0.6B |
Qwen3-MoE (qwen3_moe) | Qwen3 with routed experts of moe_intermediate_size. | qwen3moe | toy qwen3_moe |
Gemma 3 (gemma3_text) | GeGLU (tanh GELU), (1 + w) RMSNorm, sandwich norms, √hidden embedding scale, 1/√query_pre_attn_scalar, sliding layers (every 6th global by default) with their own RoPE base, tied head; logit soft-capping rejected. | gemma3 | toy gemma3 (one sliding, one global layer); real gemma-3-1b-it |
OLMoE (olmoe) | Experts without renormalization; QK-norm across all heads; eps 1e-5. | olmoe | toy olmoe; real OLMoE-1B-7B-0924-Instruct |
Sources: config.cc L292–367,
decoder_test.cc L100–115,
real_model_test.cc L130–134. The config.h header's
family list (L13–20) names seven and leaves out OLMoE, which the parser
supports.
How the goldens are made
tools/make_llm_fixtures.py writes one two-layer toy model per family, a few thousand parameters (vocab 64,
hidden 32, 4 heads over 2 kv heads), twice: a Hugging Face directory with bf16 safetensors, and a GGUF file laid out
as llama.cpp's converter lays it out (f16 matrices, f32 vectors, Llama q/k rows permuted, experts stacked, Gemma norms
as 1 + w). The weights are exactly representable in both bf16 and f16, so both files hold the same model, and
logits.json holds every prompt position's logits from a float64 numpy forward pass that follows the
Hugging Face modeling code (L1–16,
L31–45). The Llama variant also gets a Q8_0 GGUF.
real_model_test checks downloaded checkpoints against goldens from
tools/make_real_model_goldens.py: tokenizer ids, the chat prompt, the top-8 logits at every prompt
position within 2e-3, and an 8-token greedy continuation. It skips when RML_MODELS_DIR lacks the model
(real_model_test.cc L1–6,
L31–33). rml reports worst f32 logit errors of 1.4e-4
(SmolLM2-135M-Instruct), 1.1e-4 (TinyLlama-1.1B-Chat and Qwen3-0.6B), 1.7e-4 (Gemma 3 1B) and 1.3e-4
(OLMoE-1B-7B-Instruct), run in the development container; CI downloads only SmolLM2. Llama 3.1 8B at Q8_0 / Q4, the
milestone model, has no golden yet and is planned
(progress.md L58–59).
Tests
| File | Tests | What they pin down |
|---|---|---|
config_test.cc | 19 | SmolLM2 and Llama 3.1 configs; Qwen2 bias and ignored window; Qwen3 QK-norm and head_dim; Mistral window; each MoE family's keys; OLMoE whole-row norms; Gemma 3 from sliding_window_pattern and from layer_types and rope_parameters; GGUF metadata, tensor-table switches, Llama-with-experts as Mixtral, Gemma 3 keys; unsupported and malformed inputs. |
weight_source_test.cc | 2 | GGUF matrices, vectors and expert slices; safetensors read through Hugging Face names. |
decoder_test.cc | 2 × 17 cases, + 5 | Prefill and token-by-token decoding (page size 4, so several pages) match the float64 reference for every family in both formats, within 2e-4 plus 2e-4 relative (3e-2 for Q8_0, whose activations are rounded to int8); bad inputs leave the cache untouched; f16 within 1e-2 and int8 within 5% of an f32 cache; threaded output bit-identical to serial on Mixtral. |
decoder_alloc_test.cc | 1 × 8 cases | Sixteen decode steps after a prefill make zero heap allocations (it counts every operator new), serial and on 4 threads, dense, Q8_0, Qwen3, three MoE models, and an int8 cache. |
attention_test.cc | 2 × 9 shapes, + 2 | Attend within 2e-5 of AttendRef for f32, f16 and int8 caches, threaded equal to serial, windows and GQA groups up to 16; finite for large scores; a single key copies its value; oversized groups die. |
rope_test.cc | 9 | Frequencies follow θ; half and interleaved styles agree on permuted heads; scores depend only on distance; each scaling scheme; odd head_dim rejected. |
kv_cache_test.cc | 11 | Growth a page at a time; disjoint slots; truncation returns pages; overflow changes nothing; f16 rounding; int8 group sizes and per-group quantization, zero rows. |
kernels_test.cc | 14 (five parameterized) | MatMul equals the reference for every type; the quantized path is the int8 dot of each row and falls back without scratch; fused kernels bit-identical to their unfused sequence; RMSNorm, SwiGLU, GeGLU, stable softmax. |
real_model_test.cc | 3 × 5 models | Real checkpoints, when downloaded (above). |
Two benchmarks go with them: attention_benchmark (one
decode step of a Llama-3-8B layer's attention, per cache dtype) and
decoder_benchmark (one decode step of a random-weight
model with TinyLlama-1.1B's layer shape but two layers and an 8192-token vocabulary, Q8_0 and Q4_0, at 1 and 4
threads; L28–38). Their results are on
Kernels. Comparisons against llama.cpp on the Mac are still
planned (progress.md L71).