Skip to content

← Shapes and Strides step 6 of 9

Easy Primitives

Fold the heads into the batch

Multi-head attention holds activations as:

(batch, heads, length, dim)

and then wants to hand them to something that takes a single batch axis. The standard move is to fold heads into batch:

(batch, heads, length, dim)  ->  (batch * heads, length, dim)

Every head becomes its own item in a bigger batch. Nothing about the numbers changes; only the label on the first axis does.

Why it is free

Adjacent axes can always be merged as a view, because their elements are already adjacent in memory in exactly the right order. Look at the strides:

(2, 3, 4, 5)   strides (60, 20, 5, 1)
(6, 4, 5)      strides (20, 5, 1)

Axis 0 stepped 60 and axis 1 stepped 20 with three of them; the merged axis steps 20 with six. Same buffer, same order, one fewer number in the shape.

Where it stops being free

Merge two axes that are not adjacent in memory and there is nothing to merge:

x.permute(0, 2, 1, 3).reshape(batch * length, heads, dim)   # copies

After the permute, the elements of the two leading axes are interleaved with everything else. No stride describes the result, so reshape allocates. This is why the standard implementation permutes after merging where it can, and why contiguous() shows up next to view in every attention implementation you will read.

Your task

def fold_heads(x: torch.Tensor) -> torch.Tensor

x is (batch, heads, length, dim). Return (batch * heads, length, dim) without copying the buffer.

The starter uses torch.cat over a list comprehension, which is what this looks like before you know about strides. It produces the right tensor and allocates the whole thing.