- Key idea
- C = AB: C[i, j] = row i of A · column j of B; (m × n)(n × p) = m × p; inner sizes must match; order matters; matrix × vector = weighted sum of columns
- Real world
- Three customers’ shopping lists (rows) times a price table with a column for each of two shops gives a 3 × 2 table: what each customer would pay at each shop.
- In an LLM
- Q = X·W_Q turns 10 tokens × 768 numbers into 10 queries of 64 numbers each: a (10 × 768)(768 × 64) multiplication, 491,520 multiply-adds, for one head of one layer.
Rows meet columns
is many dot products organised in a grid. To compute :
Cell of is the dot product of row of with column of .
For the dot product to make sense, a row of and a column of must have the same length. So the number of columns of must equal the number of rows of . The result has as many rows as and as many columns as :
The two inner sizes must match and disappear; the outer sizes stay.
Watch it, one product at a time
Press play. The highlighted row of meets the highlighted column of ; each pair is multiplied, the products are added into a running sum, and the cell fills in. Then the same row meets the next column. Change the sizes with the buttons, and click any finished cell of to jump back to it.
Things to notice:
- has cells, each needing multiplications, so the whole product is multiply-adds. Double all three sizes and the work goes up 8 times.
- The cells don’t depend on each other. A GPU computes thousands of them at the same time, which is why models are built from matrix multiplications.
- Order matters. and are usually different, and often only one of them exists.
Three customers each want some bread, milk and apples: a matrix of quantities, one row per customer. Two shops each have a price for each item: a matrix, one column per shop. Multiply them and you get a table: row = customer, column = shop, entry = that customer’s bill at that shop. Each entry is one shopping-bill dot product; the multiplication does all six at once.
Do the shapes fit?
Most bugs in model code are shape bugs, and most are caught by checking inner sizes. Try the presets, which are real multiplications inside GPT-2 small reading 10 tokens:
set your own sizes
Ten tokens arrive as , shape . The query matrix of one head is . Inner sizes 768 and 768 match, so is : one 64-number query per token, costing multiply-adds. Keys come out the same shape. To score every query against every key we need , so we multiply by , not : that is the reason for the transpose in . See queries, keys and values for the full pipeline.
A matrix times a vector
When is a single column , the product is a vector. There are two ways to see it:
- Row view: each output number is one row of dotted with .
- Column view: the output is a weighted sum of ’s columns, with the numbers in as the weights: .
The column view is the key to the next lesson. Drag the columns and slide the weights:
Transformer code puts vectors in rows instead, and writes . Then the same idea reads: the output is a weighted sum of ’s rows, with the weights taken from . Embedding lookup is the extreme case: a one-hot puts weight 1 on a single row.
Rules
- Associative: . You can choose which product to do first; the cost can differ a lot.
- Distributive: .
- Not commutative: in general .
- Identity: the identity matrix (1s on the diagonal, 0s elsewhere) changes nothing: .
- Transpose of a product: .
Cost
takes multiplications and about as many additions, so about floating-point operations (FLOPs). For a model with parameters, a forward pass costs about FLOPs per token, because nearly every parameter takes part in one multiply-add. See what a forward pass costs.
In code
import numpy as np
A = np.array([[1, 2, -1],
[0, 3, 2]]) # 2 × 3
B = np.array([[ 2, 1],
[ 0, 1],
[-1, 3]]) # 3 × 2
A @ B # [[3, 0], [-2, 9]], shape (2, 2)
B @ A # also legal here, but shape (3, 3)
# C[i, j] by hand
C = np.zeros((2, 2))
for i in range(2):
for j in range(2):
for k in range(3):
C[i, j] += A[i, k] * B[k, j]
import torch
X = torch.randn(10, 768)
Wq = torch.randn(768, 64)
(X @ Wq).shape # torch.Size([10, 64])
Q = X @ Wq; K = X @ torch.randn(768, 64)
(Q @ K.T).shape # torch.Size([10, 10])
# Q @ K -> RuntimeError: mat1 and mat2 shapes cannot be multiplied (10x64 and 10x64)Further reading
- 3Blue1Brown, Essence of linear algebra, chapter 4, on multiplication as composing transformations.
- Why GPUs love it: matrix multiply is compute-bound with high data reuse; see the NVIDIA matrix multiplication performance guide.
- Faster-than-cubic algorithms exist in theory: Strassen, 1969; in practice GPUs use the plain method in tiles.