- Key idea
- Tensors generalise vectors and matrices to any number of axes; shape (B, T, d); slicing removes axes; reshape regroups without moving data; broadcasting repeats along size-1 axes; einsum names axes and sums the missing ones
- Real world
- A colour video is a 4-axis tensor: frames × height × width × 3 colour channels. One frame is a 3-axis slice, one pixel is a vector of 3 numbers, one brightness value is a single number.
- In an LLM
- GPT-2 trains on blocks of shape (batch, tokens, features), for example (8, 1024, 768): 8 texts of 1,024 tokens, 768 numbers per token. Inside attention it reshapes them to (8, 12, 1024, 64), one slice per head.
More axes
A is a block of numbers with any number of axes (also called dimensions; numpy calls the count ndim). The list of sizes along the axes is its shape:
| Axes | Name | Example shape | In an LLM |
|---|---|---|---|
| 0 | scalar | () | the loss, a learning rate |
| 1 | vector | (768,) | one token’s hidden state |
| 2 | matrix | (1024, 768) | one text: a row per token |
| 3 | 3-axis tensor | (8, 1024, 768) | a batch of 8 texts |
| 4 | 4-axis tensor | (8, 12, 1024, 1024) | attention weights: batch, head, query, key |
Nothing new happens mathematically. A tensor is a convenient way to do the same matrix operations on many matrices at once, which is exactly what GPUs are good at.
Slicing
Indexing a tensor picks out part of it. Fixing an index removes that axis; a colon : keeps it. Pick slices of a small (3, 5, 4) batch:
A 10-second clip at 30 frames per second, 1080 × 1920 pixels, in colour, is a tensor of shape (300, 1080, 1920, 3), about 1.9 billion numbers. video[0] is the first frame, shape (1080, 1920, 3). video[:, 540, 960] is the centre pixel’s colour over time, shape (300, 3). video[..., 0] is the red channel of everything, shape (300, 1080, 1920). Video models and image models read their inputs with exactly these slices.
Reshaping into heads
A reshape (view in PyTorch) reads the same numbers, in the same order, with a different shape. The total count must stay the same: . A transpose of two axes swaps them. Multi-head attention uses both to give each head its own slice of every token:
GPT-2 small computes queries for all heads at once: has shape (B, T, 768). It is viewed as (B, T, 12, 64), splitting each token’s 768 numbers into 12 groups of 64, and transposed to (B, 12, T, 64). Now each of the 12 heads has an ordinary (T, 64) matrix per text, and one batched multiplication computes every head’s scores. At the end the process runs backwards to (B, T, 768). See multi-head attention.
Broadcasting
Adding a bias of 768 numbers to a (T, 768) matrix should add it to every row. makes that automatic: line the two shapes up from the right; each pair of sizes must be equal, or one of them must be 1 (or missing), in which case that array is repeated along that axis.
Broadcasting is everywhere in model code: adding biases, scaling each token by its own norm in RMSNorm, and adding a (T, T) causal mask to scores of shape (B, 12, T, T).
Einsum: shapes as letters
(Einstein summation) writes a tensor operation by naming every axis with a letter. The rule fits in a sentence: letters that appear in the inputs but not in the output are multiplied over and summed; the others are kept. The string reads like a shape annotation, which is why model code uses it so much:
Rules of thumb
- Reshape never moves data in memory (when the tensor is contiguous); transpose changes the strides, so a following
viewmay need.contiguous()orreshape. - Batched matrix multiply
@works on the last two axes and broadcasts over the rest: (B, 12, T, 64) @ (B, 12, 64, T) → (B, 12, T, T). - Broadcasting compares shapes from the right: (8, 1, 1024) and (1024,) fit; (8, 1024) and (8,) do not.
In code
import torch
B, T, D, H = 8, 1024, 768, 12
x = torch.randn(B, T, D)
x[0].shape, x[:, 1].shape, x[..., 0].shape # (1024, 768), (8, 768), (8, 1024)
q = x.view(B, T, H, D // H).transpose(1, 2) # (8, 12, 1024, 64)
k = q # stand-in
scores = torch.einsum("bhqd,bhkd->bhqk", q, k) # (8, 12, 1024, 1024)
same = q @ k.transpose(-2, -1) # identical
bias = torch.randn(D)
(x + bias).shape # broadcast over B and T: (8, 1024, 768)
x / x.norm(dim=-1, keepdim=True) # (8, 1024, 1) broadcast across 768Further reading
- NumPy broadcasting rules and torch.einsum.
- Readable tensor code with named axes: einops (
rearrange(q, "b t (h d) -> b h t d", h=12)), described in Rogozhnikov, 2022. - The shapes of GPT-2’s forward pass in a few hundred lines: nanoGPT.