Learning 10 · what each tensor is, and how we know

Anatomy of a transformer block

Context: M1, building expected_tensors · Status: living

Learning 05 told us the shapes. This note answers the two questions it leaves open — the ones you hit the moment you try to list a model's tensors: what is each one? and where does that knowledge come from?

The second question is the important one, so it goes first. You cannot derive an architecture from a config, and you should not derive it from memory — memory is a lossy cache, and it was already wrong about lm_head. The source of truth is the file's header and the reference implementation.
📖 Inference Engineering (Kiely) · §2.1–2.2 (p.42–52) 🔧 modeling_qwen3.py · the reference forward pass 🧭 Raschka · QK-norm / GQA / SwiGLU comparison

Part 1 · where the architecture comes from

Not memory, not the config — a chain of trust

The tempting shortcut is "I know transformers, I'll write the blocks from memory." Two problems. Memory is a lossy cache and it's already been wrong — it said "tied embeddings ⟹ no lm_head.weight"; the real file has one (Part 3). And you cannot derive an architecture from config.json — the thesis of learning 09: a config parameterizes a structure that lives in code. Nothing in it announces q_norm.

So where does it come from? From the model's creators, down a chain of trust — each link verified against the one above by reproducing its numbers:

Qwen team (Alibaba) designs · trains · documents → report + reference code + weights verify: reproduce the creators' outputs ← golden vectors HuggingFace transformers implements modeling_qwen3.py from the report + reference verify: our logits match, to tolerance ← golden vectors Failed Star (us) read HF as spec · run HF as oracle · write our own Rust
creator — invents the design reference — the canonical code us — read + run + verify
The design choices — QK-norm, GQA, SwiGLU — were invented by the creators and documented. Everyone downstream implements a documented design and confirms it by getting the same numbers.
The evidence is in the file's own header. modeling_qwen3.py opens with Copyright 2025 The Qwen team, Alibaba Group and the HuggingFace Inc. team. — the creator and the reference library named as co-authors. The chain of trust isn't a metaphor; it's a copyright line. (The file is even auto-generated from modular_qwen3.py, which the model team often contributes directly.)

What transformers is for us — three roles, kept distinct

Spec & oracle

Read it to learn what tensors exist and how they're wired (the names are the PyTorch attribute paths: self.q_proj…self_attn.q_proj.weight). Run it at M2 to dump golden logits and assert our output matches.

Never copied

We copy the architecture, never the code. The weights are only meaningful under Qwen3's exact computation, so our math must match — but we express it in our own tight Rust and prove equivalence with numbers, not by diffing source.

Three sources of truth, in order of authority

sourceanswersfor fs inspect
the file headerwhat tensors exist + shapesground truth we cross-check against
the reference codehow each tensor is used (the forward pass)how we know what to implement (M2)
the configthe dims that size everythinglearning 09

So expected_tensors derives the spec from the config, then diffs it against the header. Any disagreement is a loud failure at load time — not quiet garbage in M2.

Part 2 · tensor by tensor

What each one is

Straight from the real header: 311 tensors = 3 global + 11 per block × 28 layers. A block is two sub-layers — attention, then an MLP — each wrapped as norm → sub-layer → add back to the residual stream. That wrapping is why you see two layernorms per block plus two more norms inside attention:

H-wide residual stream RMSNorm input_ln Attention q·k·v_proj → QK-norm → attention → o_proj + RMSNorm post_attn_ln SwiGLU MLP gate·up → SiLU·× → down_proj +
residual stream (H) RMSNorm (scale) attention SwiGLU MLP
Pre-norm: every sub-layer reads a normalized copy of the bus and adds its result back. Two sub-layers ⟹ two layernorms; QK-norm is a third and fourth norm inside attention.

The 3 global tensors

tensorshapewhat it is
model.embed_tokens.weight[V, H]the vocabulary table — a token id is a row index (a gather, not a matmul). See learning 04.
model.norm.weight[H]one final RMSNorm on the output stream before the head.
lm_head.weight[V, H]the output projection: final H-vector → V logits. Tied to embed_tokens — Part 3.

The 11 tensors of one block

tensorshaperole
input_layernorm[H]RMSNorm the bus before attention (a scale vector, not a matrix).
self_attn.q_proj[2048, 1024]the query: "what am I looking for?" (16 heads × 128)
self_attn.k_proj[1024, 1024]the key: "what do I offer as a match?" (8 kv heads × 128)
self_attn.v_proj[1024, 1024]the value: "what do I carry if matched?"
self_attn.q_norm[128]QK-norm on each query vector before scoring — the one you can't guess.
self_attn.k_norm[128]QK-norm on each key vector.
self_attn.o_proj[1024, 2048]project the concatenated head outputs back onto the H-wide bus.
post_attention_layernorm[H]RMSNorm the bus before the MLP.
mlp.gate_proj[3072, 1024]SwiGLU gate: H → I, then squashed by SiLU.
mlp.up_proj[3072, 1024]SwiGLU value: H → I, multiplied by the gated activation.
mlp.down_proj[1024, 3072]I → H, back onto the bus.
QK-norm — the poster child for "read the reference, don't reason." GPT-2 and Llama-1 have no such tensors; nothing about attention requires them. You know they exist only because the reference code has them and the header lists q_norm.weight / k_norm.weight. No config field announces them.
self.q_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps)   # and k_norm
...
query_states = self.q_norm(self.q_proj(hidden_states)...)            # applied before attention

SwiGLU is why there are two "up" projections. A vanilla FFN is down(act(up(x))); SwiGLU splits the up into a gate and a value:

MLP(x) = down_proj( SiLU(gate_proj(x)) · up_proj(x) )
                     └── the gate ──┘   └─ the value ─┘     (· = element-wise)

Two [I,H] tensors + one [H,I] is the SwiGLU signature; a single up-projection would be a plain FFN.

Part 3 · read, don't assume

The lm_head surprise

config.json has tie_word_embeddings: true. "Tied" means the output projection is the embedding table — the same weights, used twice. It's a statement about the math, not the file.

Memory said "tied ⟹ the file omits lm_head.weight." The header says otherwise — lm_head.weight [151936, 1024] is present, and its raw bytes are byte-for-byte identical to embed_tokens. Qwen3-0.6B is tied and ships a redundant copy. The consequences:

countvalue
stored tensors311
stored params (naive sum)751,632,384 (~751M)
logical params (dedup the tied copy)596,049,920 — the "0.6B"
embedding share of logical26.1%

A quarter of the model is the vocabulary table — stored twice. So expected_tensors marks lm_head optional when tied, and cross_check reports stored and logical separately, flagging the redundant copy as a note, not a failure. Had we coded from the memory-assumption, the cross-check would have called a real tensor an "extra" and reported a false failure on the real model.

The lesson, one line: derive the expectation from the config, but confirm it against the header and the reference — never against recall.

fs inspect models/qwen3-0.6b now prints exactly this reconciliation — the deduped 596M, the redundant-copy note, the 26.1% share — all derived from config.json and checked against the file, none of it hard-coded. See the abridged output in M1 · load the weights.