LLM Developer DocsLLM Docs

Loading the search index ...

↑ ↓ move, Enter open, Esc close

GitHub

Notation & Shapes

The tensor conventions the code uses, which code name each math symbol stands for, matrix multiplication, and how the parameters add up.

This page fixes the notation for the rest of the Mathematics section. Every symbol below names something real in the code, and every size is read from config.py.

Conventions

These are how the code arranges its numbers, not general rules about transformers.

  • A token is a row. One token's vector is a row with d numbers.
  • A sequence is a matrix. A sequence of T tokens has shape [T x d].
  • A linear layer is X @ W + b. X is on the left, so W has one row per input feature. The bias b is added to every row.
  • There is no batch axis. forward and loss_and_grads take one sequence at a time. batch_loss_and_grads in train.py is a Python loop over the windows of a batch (train.py:17).
  • Heads add one axis. Multi-head attention works on [H x T x d_h]. The head comes first.

Symbols and code names

Values come from config.py. V has no value there, because it depends on the corpus.

SymbolMeaningconfig.pyValueIn the model code
TTTokens in one sequence

Inside the model, T is read from the array, not from config. Generation can use fewer than seq_len.

seq_len16T, d = Xn.shape transformer.py:71
Tmax⁡T_{\max}Rows in the position table, the longest allowed sequencecontext_len32cfg.context_len transformer.py:22
ddWidth of each token's vectoremb_dim32cfg.emb_dim transformer.py:18
HHAttention headsnum_heads2cfg.num_heads transformer.py:72
dhd_hWidth of one head

Nothing reads config.head_dim. The code computes the same value in place.

head_dim16dh = d // H transformer.py:73
FFWidth of the feed-forward hidden layerff_dim128cfg.ff_dim transformer.py:18
LLTransformer blocksnum_layers2cfg.num_layers transformer.py:27
VVVocabulary size

It depends on the corpus, so config.py has no value for it. Inside attention_forward the local name V is the value matrix, not this.

not in config.pydepends on the corpustok.vocab_size train.py:48

A letter can mean something else in a local variable. The notes in the table name the case that matters most: V inside attention_forward.

Matrix multiplication

What is it?

Matrix multiplicationDerived from implementation

Basic operations

The code writes every linear layer as X @ W + b, where X has one row per token. The @ operator multiplies each row of X by the matrix W, and b is added to every row of the result.

Why does the model need it?

It is how the model changes the width of token vectors and mixes their features. Query, key and value, both feed-forward layers, the attention output and the head are all this one formula.

Mathematics

One entry of the product #Link to equation: One entry of the product
(XW)tj=∑kXtkWkj(XW)_{tj} = \sum_{k} X_{tk} W_{kj}
The shape rule #Link to equation: The shape rule
X∈RT×a,  W∈Ra×c  ⇒  XW∈RT×cX \in \mathbb{R}^{T \times a},\; W \in \mathbb{R}^{a \times c} \;\Rightarrow\; XW \in \mathbb{R}^{T \times c}
A linear layer, with b added to every row #Link to equation: A linear layer, with b added to every row
Ytj=(XW)tj+bjY_{tj} = (XW)_{tj} + b_j

What does each symbol mean?

SymbolMeaningIn codeShape
XXInput, one row per tokenX(T, a)
WWWeight matrixW(a, c)
bbBias, added to every rowb(c,)
YYOutput, one row per tokennot named in the code(T, c)
aaInput width. It is d, or F for the second feed-forward layernot named in the codescalar
ccOutput width. It is d, F or V, depending on the layernot named in the codescalar

Tiny numerical example

Edit any cell. Switch W to three columns and watch the shape of the result change, while the rows stay the same.

X @ W and X @ W + bIllustrative ExampleHand-picked numbers that you can edit. These are not learned weights. Results come from the math engine, and tests compare it with the real Python on the starting values.
X
k0k1k2
t0
t1

W
j0j1
k0
k1
k2

b
j0j1
b

(2, 3) @ (3, 2) → (2, 2): columns of X must equal rows of W. The result has the rows of X and the columns of W.

Row 0, column 0 of the product: 1 × 0.5 + 2 × 1.5 + -1 × 2 = 1.5

X @ W, shape (2, 2)
1.5-0.5
6.750.125
X @ W + b, shape (2, 2)
1.6-0.7
6.85-0.075

Tensor shapes

The rule above, at three of the places the model uses it.

Query projection, width stays the sametransformer.py:75
  1. Xn(T, d)(16, 32)
  2. Q(T, d)(16, 32)

T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt. d = emb_dim = 32.

First feed-forward layer, width growstransformer.py:113
  1. Xn(T, d)(16, 32)
  2. Z(T, F)(16, 128)

T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt. d = emb_dim = 32. F = ff_dim = 128.

Linear head, width becomes the vocabulary sizetransformer.py:166
  1. Xf(T, d)(16, 32)
  2. logits(T, V)(16, V)

T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt. d = emb_dim = 32. V depends on the corpus. It is not a value in config.py.

Actual implementation

ops.pylinear_forwardGitHub: ops.py
def linear_forward(X, W, b):    return X @ W + b

Code and math connection

EquationRoleLinesCode
One entry of the productComputes itops.py:8return X @ W + b
The shape ruleComputes itops.py:8return X @ W + b
A linear layer, with b added to every rowComputes itops.py:8return X @ W + b
A linear layer, with b added to every rowWritten inlinetransformer.py:75Q = Xn @ params[p+"wq"] + params[p+"bq"] # (T,d), QUERY
A linear layer, with b added to every rowWritten inlinetransformer.py:76K = Xn @ params[p+"wk"] + params[p+"bk"] # KEY
A linear layer, with b added to every rowWritten inlinetransformer.py:77V = Xn @ params[p+"wv"] + params[p+"bv"] # VALUE
A linear layer, with b added to every rowWritten inlinetransformer.py:85out = O @ params[p + "wo"] + params[p + "bo"]
A linear layer, with b added to every rowWritten inlinetransformer.py:113Z = Xn @ params[p + "ff1_w"] + params[p + "ff1_b"]
A linear layer, with b added to every rowWritten inlinetransformer.py:116return A @ params[p + "ff2_w"] + params[p + "ff2_b"], dict(Xn=Xn, Z=Z, A=A)
A linear layer, with b added to every rowWritten inlinetransformer.py:166logits = Xf @ params["head_w"] + params["head_b"]
A linear layer, with b added to every rowTests ittests/test_ops.py:15assert np.allclose(linear_forward(X, W, b), [[-1, 3, 1], [-1, 2, -1]])

The backward pass of this layer, linear_backward, is on Linear / ReLU / Initialisation.

Where is it used?

ops.linear_forward is defined, but only a test calls it. transformer.py does not import it and writes the formula in place instead, at the lines listed here.

Source

ops.py, lines 7-8, linear_forward

PDF reference

Section 4.3, Eq. 19, p. 6

Used by

Not called by the pipeline.

Tested by: tests.test_ops.test_linear_worked_example

Written inline in transformer.py, line 75. Query. Key and value are on the next two lines.

Written inline in transformer.py, line 85. Attention output.

Written inline in transformer.py, line 113. First feed-forward layer.

Written inline in transformer.py, line 116. Second feed-forward layer.

Written inline in transformer.py, line 166. The head.

Status
Derived from implementation

Implemented in

NoteInline equivalent: the pipeline does not call ops.linear_forward. It computes the same formula in place at transformer.py:75, transformer.py:76, transformer.py:77, transformer.py:85, transformer.py:113, transformer.py:116, transformer.py:166.
EquationRoleFileFunctionLinesGitHub
One entry of the productComputes itops.pylinear_forward8GitHub: ops.py, line 8
The shape ruleComputes itops.pylinear_forward8GitHub: ops.py, line 8
A linear layer, with b added to every rowComputes itops.pylinear_forward8GitHub: ops.py, line 8
A linear layer, with b added to every rowWritten inlinetransformer.pyattention_forward75GitHub: transformer.py, line 75
A linear layer, with b added to every rowWritten inlinetransformer.pyattention_forward76GitHub: transformer.py, line 76
A linear layer, with b added to every rowWritten inlinetransformer.pyattention_forward77GitHub: transformer.py, line 77
A linear layer, with b added to every rowWritten inlinetransformer.pyattention_forward85GitHub: transformer.py, line 85
A linear layer, with b added to every rowWritten inlinetransformer.pyffn_forward113GitHub: transformer.py, line 113
A linear layer, with b added to every rowWritten inlinetransformer.pyffn_forward116GitHub: transformer.py, line 116
A linear layer, with b added to every rowWritten inlinetransformer.pyforward166GitHub: transformer.py, line 166

Heads

What is it?

HeadsImplemented

Attention

Attention splits each token's vector into H parts of width d_h. The third axis is the only axis the code adds to a sequence, so a head array has the shape H x T x d_h.

Why does the model need it?

Each head computes its own attention weights from its own part of the vector. The split keeps the columns of each head together, so merging them restores the original array.

Mathematics

Width of one head #Link to equation: Width of one head
dh=dH,d mod H=0d_h = \frac{d}{H}, \qquad d \bmod H = 0

config.py asserts the divisibility when it is imported.

Head h owns one block of columns #Link to equation: Head h owns one block of columns
Mt,i(h)=Mt,  hdh+iM^{(h)}_{t,i} = M_{t,\; h d_h + i}

What does each symbol mean?

SymbolMeaningIn codeShape
MMA matrix to splitM(T, d)
M(h)M^{(h)}Head h of the split arraynot named in the code(T, d_h) for each head; (H, T, d_h) together
dhd_hWidth of one headdhscalar
HHNumber of headsHscalar
hhHead index, from 0not named in the codescalar
iiColumn index inside a head, from 0not named in the codescalar

Tiny numerical example

Illustrative ExampleHand-picked input. Values computed by running transformer.split_heads; they are not learned values.
Input M
[[1, 2, 3, 4], [5, 6, 7, 8]]
Input H
2
Width of the input d
4
Split array, head 0 then head 1
[[[1, 2], [5, 6]], [[3, 4], [7, 8]]]

Head 0 holds columns 0 and 1, and head 1 holds columns 2 and 3.

Tensor shapes

Split, for the query matrixtransformer.py:79
  1. Q(T, d)(16, 32)
  2. Qh(H, T, dh)(2, 16, 16)

T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt. d = emb_dim = 32. H = num_heads = 2. dh = head_dim = 16.

Merge, after the weighted valuestransformer.py:84
  1. A @ Vh(H, T, dh)(2, 16, 16)
  2. O(T, d)(16, 32)

H = num_heads = 2. T = seq_len = 16. T is at most seq_len: generation uses the last seq_len ids, or fewer for a short prompt. dh = head_dim = 16. d = emb_dim = 32.

Actual implementation

transformer.pysplit_headsGitHub: transformer.py
def split_heads(M, H):    T, d = M.shape    return M.reshape(T, H, d // H).transpose(1, 0, 2)
transformer.pymerge_headsGitHub: transformer.py
def merge_heads(Mh):    H, T, dh = Mh.shape    return Mh.transpose(1, 0, 2).reshape(T, H * dh)

Code and math connection

EquationRoleLinesCode
Width of one headComputes ittransformer.py:73dh = d // H # width of one head
Width of one headWritten inlinetransformer.py:94dh = cfg.emb_dim // H
Width of one headWritten inlineconfig.py:44head_dim = emb_dim // num_heads # width of one head, 32 / 2 = 16
Head h owns one block of columnsComputes ittransformer.py:62return M.reshape(T, H, d // H).transpose(1, 0, 2)
Head h owns one block of columnsComputes ittransformer.py:67return Mh.transpose(1, 0, 2).reshape(T, H * dh)
Head h owns one block of columnsTests ittests/test_transformer.py:61Mh = split_heads(M, 2)

Where is it used?

PDF reference

Section 3, Eq. 1, p. 3

Status
Implemented

Implemented in

NoteInline equivalent: the pipeline calls transformer.attention_forward, and also computes the same formula in place, with no call, at transformer.py:94, config.py:44.
EquationRoleFileFunctionLinesGitHub
Width of one headComputes ittransformer.pyattention_forward73GitHub: transformer.py, line 73
Width of one headWritten inlinetransformer.pyattention_backward94GitHub: transformer.py, line 94
Width of one headWritten inlineconfig.pyhead_dim44GitHub: config.py, line 44
Head h owns one block of columnsComputes ittransformer.pysplit_heads62GitHub: transformer.py, line 62
Head h owns one block of columnsComputes ittransformer.pymerge_heads67GitHub: transformer.py, line 67

Attention covers what each head computes.

Parameter count

Note

This section is test-only mathematics. count_params is not part of the pipeline: only a test calls it, and training does not print this number.

What is it?

Parameter countImplemented

Initialization

The number of scalars in the arrays init_params creates. The code only ever computes it as a sum of array sizes, in count_params. The PDF states the formula below as Eq. 2. The code never evaluates that formula, and it holds because of the shapes in init_params.

Why does the model need it?

The model does not need this number to run. It tells a reader how the model's size depends on each setting, and only V depends on the corpus. Only tests call count_params.

Mathematics

Add the scalars of every array init_params creates. The names, shapes and values in this table come from calling init_params and from config.py.

GroupNamesScalarsWith configRepeats
Token tabletok_embV × d32VOnce
Position tablepos_embT_max × d1,024Once
LayerNorm 1b{l}.ln1_g, b{l}.ln1_bd + d32 + 32Once per block
Q, K, V and output weightsb{l}.wq, b{l}.wk, b{l}.wv, b{l}.wod × d + d × d + d × d + d × d1,024 + 1,024 + 1,024 + 1,024Once per block
Q, K, V and output biasesb{l}.bq, b{l}.bk, b{l}.bv, b{l}.bod + d + d + d32 + 32 + 32 + 32Once per block
LayerNorm 2b{l}.ln2_g, b{l}.ln2_bd + d32 + 32Once per block
First feed-forward layerb{l}.ff1_w, b{l}.ff1_bd × F + F4,096 + 128Once per block
Second feed-forward layerb{l}.ff2_w, b{l}.ff2_bd × F + d4,096 + 32Once per block
Final LayerNormlnf_g, lnf_bd + d32 + 32Once
Linear headhead_w, head_bV × d + V32V + VOnce
One block, the rows marked once per block12,704

The rows marked once per block add up to one block:

Scalars in one block #Link to equation: Scalars in one block
Nblock=4d2+2dF+F+9dN_{\text{block}} = 4d^2 + 2dF + F + 9d

Multiply that by L, and collect the remaining rows by what they depend on. tok_emb and the head give (2d + 1)V. pos_emb and the final LayerNorm give (T_max + 2)d.

Scalars in the whole model #Link to equation: Scalars in the whole model
N=(2d+1)V+(Tmax⁡+2)d+L(4d2+2dF+F+9d)N = (2d+1)V + (T_{\max}+2)d + L\left(4d^2 + 2dF + F + 9d\right)

With the values in config.py, and V left as a symbol:

Implemented
N=65 V+26,496N = 65\,V + 26{,}496

What does each symbol mean?

SymbolMeaningIn codeShape
NNTotal scalarscount_params(params)scalar
NblockN_{\text{block}}Scalars one pass of the layer loop createsnot named in the codescalar
VVVocabulary sizecfg.vocab_sizescalar
Tmax⁡T_{\max}Rows in the position tablecfg.context_lenscalar
ddWidth of each token's vectorcfg.emb_dimscalar
FFWidth of the feed-forward hidden layercfg.ff_dimscalar
LLNumber of blockscfg.num_layersscalar

Tiny numerical example

The formula and count_params on small configurationsIllustrative ExampleHand-picked configurations. The count_params column comes from running init_params and count_params in Python. These are not this model's parameters, except that the last row uses the shape values in config.py, with the V that tests/test_transformer.py uses.
VT_maxdFLFormulacount_params
117463597597
1356101539539
932405555
3003232128245,99645,996

Tensor shapes

Every array's shape is listed on Architecture. The feed-forward pair shows why dF appears twice in the formula.

The two feed-forward weight matrices of one blocktransformer.py:36
  1. ff1_w(d, F)(32, 128)
  2. ff2_w(F, d)(128, 32)

d = emb_dim = 32. F = ff_dim = 128.

Actual implementation

transformer.pycount_paramsGitHub: transformer.py
def count_params(params):    return int(sum(v.size for v in params.values()))
transformer.pyinit_paramsGitHub: transformer.py
def init_params(cfg: ModelConfig, rng):    d, F, V = cfg.emb_dim, cfg.ff_dim, cfg.vocab_size    p = {        "tok_emb": xavier_uniform(V, d, rng), # lookup table with one row per vocabulary token        "pos_emb": xavier_uniform(cfg.context_len, d, rng) # second table with one row per position, weighted average of values        # input to first block is tok_emb + pos_emb        # this makes cat at first position and fifth position different in a sentence    }    for l in range(cfg.num_layers):        b = f"b{l}." # key        p[b + "ln1_g"], p[b + "ln1_b"] = np.ones(d), np.zeros(d) # layer norm        for n in "qkvo":            p[b + "w" + n] = xavier_uniform(d, d, rng)            p[b + "b" + n] = np.zeros(d)        p[b + "ln2_g"], p[b + "ln2_b"] = np.ones(d), np.zeros(d)        p[b + "ff1_w"], p[b + "ff1_b"] = xavier_uniform(d, F, rng), np.zeros(F)        p[b + "ff2_w"], p[b + "ff2_b"] = xavier_uniform(F, d, rng), np.zeros(d)    p["lnf_g"], p["lnf_b"] = np.ones(d), np.zeros(d)    p["head_w"], p["head_b"] = xavier_uniform(d, V, rng), np.zeros(V)    return p

Code and math connection

EquationRoleLinesCode
Scalars in one blockComputes ittransformer.py:29p[b + "ln1_g"], p[b + "ln1_b"] = np.ones(d), np.zeros(d) # layer norm
Scalars in one blockComputes ittransformer.py:32p[b + "w" + n] = xavier_uniform(d, d, rng)
Scalars in one blockComputes ittransformer.py:33p[b + "b" + n] = np.zeros(d)
Scalars in one blockComputes ittransformer.py:35p[b + "ln2_g"], p[b + "ln2_b"] = np.ones(d), np.zeros(d)
Scalars in one blockComputes ittransformer.py:36p[b + "ff1_w"], p[b + "ff1_b"] = xavier_uniform(d, F, rng), np.zeros(F)
Scalars in one blockComputes ittransformer.py:37p[b + "ff2_w"], p[b + "ff2_b"] = xavier_uniform(F, d, rng), np.zeros(d)
Scalars in the whole modelComputes ittransformer.py:45return int(sum(v.size for v in params.values()))
Scalars in the whole modelComputes ittransformer.py:21"tok_emb": xavier_uniform(V, d, rng), # lookup table with one row per vocabulary token
Scalars in the whole modelComputes ittransformer.py:22"pos_emb": xavier_uniform(cfg.context_len, d, rng) # second table with one row per position, weighted average of values
Scalars in the whole modelComputes ittransformer.py:39p["lnf_g"], p["lnf_b"] = np.ones(d), np.zeros(d)
Scalars in the whole modelComputes ittransformer.py:40p["head_w"], p["head_b"] = xavier_uniform(d, V, rng), np.zeros(V)
Scalars in the whole modelTests ittests/test_transformer.py:19assert count_params(params) == 45_996

Where is it used?

PDF reference

Section 3, Eq. 2, p. 3

Status
Implemented

Implemented in

EquationRoleFileFunctionLinesGitHub
Scalars in one blockComputes ittransformer.pyinit_params29GitHub: transformer.py, line 29
Scalars in one blockComputes ittransformer.pyinit_params32GitHub: transformer.py, line 32
Scalars in one blockComputes ittransformer.pyinit_params33GitHub: transformer.py, line 33
Scalars in one blockComputes ittransformer.pyinit_params35GitHub: transformer.py, line 35
Scalars in one blockComputes ittransformer.pyinit_params36GitHub: transformer.py, line 36
Scalars in one blockComputes ittransformer.pyinit_params37GitHub: transformer.py, line 37
Scalars in the whole modelComputes ittransformer.pycount_params45GitHub: transformer.py, line 45
Scalars in the whole modelComputes ittransformer.pyinit_params21GitHub: transformer.py, line 21
Scalars in the whole modelComputes ittransformer.pyinit_params22GitHub: transformer.py, line 22
Scalars in the whole modelComputes ittransformer.pyinit_params39GitHub: transformer.py, line 39
Scalars in the whole modelComputes ittransformer.pyinit_params40GitHub: transformer.py, line 40