Learning 09 · the model's blueprint
We just wrote Config::load. It reads a ~1 KB text file and hands
back thirteen numbers — a strange thing to sit beside a 1.4 GB pile of
weights and call an equal partner. But it is one, and the reason is more surprising than
it looks.
Useless apart
model.safetensors alone is a flat blob:
596M numbers with no grammar. Nothing in it says a [2048,1024] matrix is
q_proj, or that there are 28 blocks, or where one layer ends. config.json
alone is the opposite — a recipe with no ingredients: the shape of a
network with no learned values in it. Fold one into the other and you have something that
runs.
fs inspect is this handshake: it proves the tensors in
the .safetensors file are exactly the ones the config implies, at the shapes
it implies. A checksum can't do that — only the config says what “correct” means.Twenty-six keys, three piles
The fields sort into three groups — and the sorting itself is the lesson.
These are the learning 05 legend; every
weight matrix's shape is a product of them. Get one wrong and fs inspect lights up.
| field | value | symbol |
|---|---|---|
vocab_size | 151936 | V |
hidden_size | 1024 | H |
num_hidden_layers | 28 | L |
head_dim | 128 | d |
num_attention_heads | 16 | — |
num_key_value_heads | 8 | — |
intermediate_size | 3072 | I |
| field | value | where it's spent |
|---|---|---|
rms_norm_eps | 1e-6 | RMSNorm's divide-by-zero guard (M2) |
rope_theta | 1000000 | RoPE base frequency (M2) |
tie_word_embeddings | true | reuse embeddings as lm_head — changes the expected tensor set (M1) |
bos_token_id / eos_token_id | 151643 / 151645 | sequence markers (M3) |
max_position_embeddings | 40960 | context-length ceiling |
A field is safe to skip for one of two reasons — it's training-only, or its value happens to match what we hardcoded:
| field | value | why we can skip it |
|---|---|---|
architectures | ["Qwen3ForCausalLM"] | names the code class to build. We are that class — see below. |
model_type | "qwen3" | HF registry key that resolves to the same class. |
hidden_act | "silu" | we hardcode SwiGLU; if this said "gelu" our engine would be wrong, not reconfigurable. |
torch_dtype | "bfloat16" | a hint; we read each tensor's dtype from the safetensors header instead (authoritative). |
attention_bias | false | q/k/v/o have no bias — matches our bias-free Linear. |
attention_dropout · initializer_range | 0.0 · 0.02 | training-only; inference never touches them. |
rope_scaling | null | no long-context frequency scaling; if set we'd bend RoPE. |
sliding_window · use_sliding_window | null · false | sliding-window attention off; full causal attention is correct. |
use_cache · transformers_version | true · "4.51.0" | an M4 decision regardless · pure metadata. |
attention_bias, rope_scaling, or
sliding_window and the architecture silently changes under us while our code
keeps computing the old one. The fail-loud move — a good future hardening of
Config::load — is to assert these, not skip them. Ignoring a
field and asserting a field look identical until the day the value differs.
The spine
They configure code that already knows the network. Look at the field doing the heavy lifting:
"architectures": ["Qwen3ForCausalLM"]
That's not data — it's a pointer to a class, a ~1500-line file
in the transformers library whose body is the architecture: embed →
(RMSNorm → RoPE → GQA attention → residual → RMSNorm → SwiGLU → residual) × L → RMSNorm →
tied lm_head. The wiring, the mask, the order of operations, which norm, which
activation — all of that lives in code. The config only fills in the free
numbers of a structure that's already fixed.
architectures names the constructor; the other
fields are its dials.
So the “two files” story is really three things, and one is usually invisible:
transformers — so “a model” collapses to config + weights. We don't get
that luxury: fs is the code column. That's why we can ignore
hidden_act: we baked SwiGLU into our own source.Because the field standardized. Modern LLMs converged, hard, on one
template: the decoder-only transformer block, stacked L times. Within
it, models differ almost entirely in (1) the ~7 dimension numbers and (2) a handful of
“which variant” switches — norm type, activation, positional scheme, attention flavor
(MHA → GQA → MLA). A dozen numbers span the whole current family because the family is a
monoculture — a fact about this moment in the field, not a law of neural nets.
MoE, MLA, a new RoPE scaling, a sliding window: new
config fields + new code branches. HF configs are open JSON dicts, so adding
num_experts or q_lora_rank is trivial — but the field is
inert until code reads it. The schema grows by accretion, one field per capability.
State-space models (Mamba), diffusion LMs, RNN revivals:
a different vocabulary entirely — Mamba has d_state,
d_conv, expand and no num_attention_heads.
The JSON container is universal; the words inside are architecture-specific and mean
nothing without that architecture's code.
config.json + model.safetensors
are only two of them, and they're only enough because the third — the architecture code — is
assumed. On this project we're building that third thing, so we see the whole machine.
architectures names it; everything else in config.json is just its
arguments.
[out,in], head_dim
decoupling, GQA).fs inspect as the
config↔weights handshake; docs/m1-weights.md (owed) states “need both” from
the weights side.rms_norm_eps /
rope_theta and the hardcoded SwiGLU / causal mask (the code half)
finally run.src/config.rs — Config, the typed extractors, and the
fail-loud “no silent defaults” stance this note argues for extending to Pile C. · Full
note: docs/learnings/09-config.md.