Skip to content

2. Shape is not a memory address

Time: 45–60 minutes. Prerequisite: lesson 1. Edit: strides.py.

A contiguous matrix

Suppose flat storage is [0,1,2,3,4,5,6,7,8,9]. Treat it as two rows of five:

row 0:  0 1 2 3 4
row 1:  5 6 7 8 9

The physical distance from one row start to the next is five elements. The distance from one column to the next is one element. These distances are strides.

storage_index = storage_offset + row * row_stride + column * column_stride

For row 1, column 2: 0 + 1*5 + 2*1 = 7.

Crop each row without copying

Select the first three columns. The logical shape is now 2-by-3, but the second row still starts at storage index 5:

view row 0: 0 1 2      (3 and 4 remain in storage)
view row 1: 5 6 7      (8 and 9 remain in storage)

If a kernel uses row * logical_width + column, it will read [3,4,5] for the second row. The correct row stride remains 5. No amount of tail masking can repair an incorrect address formula for valid elements.

Transpose and slice offsets

For storage [0,1,2,3,4,5] interpreted as 2-by-3, a transposed 3-by-2 view has row stride 1 and column stride 3. Its rows are [0,3], [1,4], [2,5]. For a slice starting at the second stored element, add storage_offset=1.

In PyTorch, a tensor's data_ptr() already refers to its first logical element. Our Python exercise explicitly includes storage_offset because it indexes the original backing list. Do not add PyTorch's storage offset again to an already adjusted tensor pointer in a Triton kernel.

Implement and inspect

Implement offset() first. Use it in read_view() with one loop per logical dimension. The checker includes a transpose even though the portfolio's row kernels intentionally reject noncontiguous columns.

python -m learning.explain strides
python -m learning.check strides

Optional: with PyTorch installed, inspect a real view. A CPU-only install is enough for this snippet (python -m pip install torch in PowerShell), or use the GPU environment:

import torch

x = torch.arange(10).reshape(2, 5)
y = x[:, :3]
print(y.shape, y.stride())  # shape [2,3], strides (5,1)
print(y.is_contiguous())  # False
print(y.contiguous().stride())  # (3,1); this allocates/copies

Read c.rows in contracts.py and STRIDE in _row_sum. The implementation accepts row gaps while requiring contiguous columns. This keeps the addressing and coalescing discussion manageable.

Exit check: a view has shape (3,2), strides (6,2), base offset 1. Find the six storage indices. Explain why calling .contiguous() inside a timed wrapper would change the benchmark's workload. Next: reductions.