Shape is a lie your framework tells you

Lesson 3 · strides and layout · the arithmetic underneath every tl.load

Lesson 2 proved your kernels are usually memory-bound. So the next question is the only one that matters: which bytes, in what order? That is decided entirely by strides — and once you see them, a whole class of mysterious 5× slowdowns stops being mysterious.

The definition that replaces the mystery

A tensor is four things, and only one of them is data:

PartWhat it isYour analogy
data pointerOne flat, 1-D block of memory.A malloc'd buffer. That's it.
shapeHow you'd like to index it, e.g. (4, 6).Pure metadata. Costs nothing, moves nothing.
stridesHow far to step, in elements, per index.The multipliers in your address arithmetic.
dtypeHow many bytes one element takes.The scale factor from elements to bytes.

There is no 2-D memory. There never was. There is a line of bytes and a formula:

The address formula: a logical index becomes a linear offset via the strides.

Listen first: the offset of logical position i, j equals i times stride zero plus j times stride one. For a normal row-major tensor of shape four by six, stride zero is six and stride one is one — stepping down a row jumps six elements, stepping across a column jumps one. So element two, three lives at two times six plus three, which is fifteen. That is the whole idea: shape tells you what is legal to ask for, strides tell you where it actually is.

Now the part that pays your rent. Transposing does not move data. It swaps the shape and swaps the strides, and returns a view onto the same buffer:

Transpose as a metadata edit: shape and strides swap, the buffer is untouched.

Listen first: after a transpose the shape becomes six by four and the strides become one and six. Not a single byte was copied. But notice what happened to the inner dimension: the stride along a row used to be one, and now it is six. Walking across a row of the transposed view means jumping six elements at a time through memory, which is exactly the access pattern that wrecks bandwidth.

Why Triton kernels take strides as arguments

In Lesson 1 the vector-add kernel computed offsets = block_start + tl.arange(0, BLOCK_SIZE) — a 1-D case where the stride is 1 and invisible. The moment you write a 2-D kernel, the stride stops being invisible and becomes a parameter you are handed:

A 2-D Triton kernel: the caller passes strides, and the kernel does the address arithmetic.

Listen first: the kernel takes a pointer plus two strides, stride_m and stride_n. It builds a row range and a column range with tl.arange, then forms a two-dimensional block of addresses: row offsets times stride_m, added to column offsets times stride_n. The colon-None and None-colon indexing is broadcasting — turning two 1-D ranges into a 2-D tile of addresses. The important point is that the kernel never assumes the data is contiguous. It is told the strides, so the same kernel works on a normal tensor and on a transposed view — at very different speeds.

This is the single most useful thing to internalise about Triton: you own the addresses. The compiler will happily coalesce, vectorise and swizzle — but only around the access pattern you described. It cannot fix a stride you chose.

Micro-world: watch the address arithmetic happen

Logical index → linear memory

The top grid is the shape you think in. The strip below it is the one flat buffer that actually exists. Click any cell to see the arithmetic.

Logical view — what your code indexes

Physical memory — the one buffer that exists (element offsets)

Check yourself

Three questions on layout

1. A (4, 6) row-major tensor. What is the element offset of logical index (2, 3)?
2. You take x.T of that tensor. How many bytes are copied?
3. Reading one row of that transposed view touches memory how?
Cold-recall defense (one breath):
A tensor is a flat buffer + shape + strides + dtype. Memory is 1-D; shape is metadata; offset = Σ index[k] * stride[k] in elements. Transpose and slicing are free — they swap or scale strides and copy nothing — but they change the inner stride, and the inner stride is what decides whether a read is one packed burst or many scattered ones. Triton kernels take strides as arguments precisely because you own the addresses; the compiler optimises around the pattern you chose, it cannot choose a better one for you.

Primary source

Read torch.Tensor.stride and then is_contiguous (~5 min together). Then, in any Python REPL with torch: make a tensor, print .shape, .stride() and .is_contiguous(), transpose it, and print them again. Thirty seconds of that beats any amount of reading.

💬 I'm your teacher for this — ask me followups any time. Good next question: "when is .contiguous() worth the copy?" The answer is a bandwidth calculation, and you already have the tools for it from Lesson 2.

Read next

Lesson 3 · Zain's AI Inference Lab · source: PyTorch tensor docs