Linear algebra for LLMsPart 4: Tensors, shapes and einsum

Reshape vs transpose

8 min

Memory is one flat list

A tensor’s numbers sit in one long line. Row-major order (PyTorch’s default) stores row 1, then row 2: the last axis changes fastest.

Write

Row-major: the last axis changes fastest. [[1,2,3],[4,5,6]][[1,2,3],[4,5,6]] → 1 2 3 4 5 6.

Reshape re-cuts the same list

→reshape(3, 2)

reshape (or view) keeps the flat order and cuts it into new rows: 1 2 | 3 4 | 5 6. The numbers don’t move in memory.

Transpose is a different thing

→.T

The transpose is also 3×23 \times 2, but its rows are [1,4],[2,5],[3,6][1, 4], [2, 5], [3, 6]. Same shape as the reshape, different matrix. PyTorch transposes by swapping the strides (how far to jump in memory for each axis), so nothing is copied, and the result is no longer laid out in row-major order: it’s non-contiguous.

Write

reshape(3, 2) of [[1,2,3],[4,5,6]][[1,2,3],[4,5,6]] = [[1,2],[3,4],[5,6]][[1,2],[3,4],[5,6]]. Transpose = [[1,4],[2,5],[3,6]][[1,4],[2,5],[3,6]].

Splitting heads, step 1: view

view(T, h, dₕ)

Two tokens, d=4d = 4 features, h=2h = 2 heads of dh=2d_h = 2. view(2, 2, 2) cuts each token’s 4 features into 2 heads: head 0 gets features 1–2, head 1 gets features 3–4. Shape (T,h,dh)(T, h, d_h). Correct so far.

Step 2: transpose heads to the front

transpose(0, 1) swaps the token and head axes: (h,T,dh)(h, T, d_h). Each head now holds one row per token, made of that token’s own slice of features. Attention can run on each head separately.

Write

Split heads: view(T, h, d_h) then transpose(0, 1) → (h,T,dh)(h, T, d_h).

The bug: view straight into (h,T,dh)(h, T, d_h)

view(2, 2, 2) read as (h,T,dh)(h, T, d_h) cuts the flat list a₁ a₂ a₃ a₄ b₁ b₂ b₃ b₄ in the wrong places. Head 0’s “two tokens” are really two halves of token a; token b vanished from it. The shape is right, so no error, but attention now mixes features as if they were tokens.

Write

view(h, T, d_h) directly scrambles tokens. Always view to (T,h,dh)(T, h, d_h) first, then transpose.

With a batch, and back again

q = x.view(B, T, h, d_h).transpose(1, 2)      # (B, h, T, d_h)
# ... attention per head ...
out = out.transpose(1, 2).reshape(B, T, h * d_h)   # (B, T, d), heads merged

After transpose, the tensor is non-contiguous, so .view would fail. .reshape copies when it has to (or write .contiguous().view(...)).

✏️ Your turn

Which shape?

xx

With h=3h = 3 heads, write the shape of x.view(2, 6, 3, 4).transpose(1, 2), and what each axis means.

Work it out on paper, then go to the next card to check.

Answer

The view gives (2,6,3,4)(2, 6, 3, 4) = (batch, token, head, feature); swapping axes 1 and 2 puts heads before tokens.

📌 Recap

Reshape vs transpose in one card

  • Memory is flat and row-major: the last axis changes fastest.
  • reshape/view re-cuts the flat order; transpose swaps axes (strides) and makes the tensor non-contiguous.
  • Split heads: view(B, T, h, d_h).transpose(1, 2). Viewing straight to (B,h,T,dh)(B, h, T, d_h) scrambles tokens without an error.
  • Merge back: transpose(1, 2).reshape(B, T, d).

Write

The four lines above.