Storage, strides and views

PyTorch · Expert · 14 min

Memory layout explains view errors, silent aliasing bugs and many performance differences.

A tensor is a view onto a flat storage: a pointer plus a storage_offset, a shape and a stride tuple. Element [i, j] lives at offset + i * stride[0] + j * stride[1].

Many operations only create a new view with different metadata and no copy: slicing, view, transpose, permute, expand, unsqueeze, narrow, diagonal. They share storage with the original, so writing through one changes the other.

A tensor is contiguous when its strides are the row-major ones for its shape. After a transpose it is not, and view (which never copies) may fail; reshape copies in that case, and .contiguous() makes a compact copy explicitly. expand uses stride 0 to repeat data without memory, which is why writing into an expanded tensor is an error.

Layout affects speed. Kernels prefer contiguous inputs, and convolutions on modern GPUs are fastest in channels_last layout: the logical shape stays N, C, H, W, but strides store channels innermost. Convert both model and inputs with .to(memory_format=torch.channels_last).

Check aliasing with untyped_storage().data_ptr(), and remember that small slices of huge tensors keep the whole storage alive; .clone() the slice if you want to free the rest.

Example

import torch

x = torch.arange(6).view(2, 3)
print(x.stride())                          # (3, 1)
t = x.t()                                  # transpose: new strides, same memory
print(t.stride(), t.is_contiguous())       # (1, 3) False
print(t.untyped_storage().data_ptr() == x.untyped_storage().data_ptr())  # True

try:
    t.view(6)
except RuntimeError:
    print("view() needs compatible strides")
print(t.contiguous().view(6))              # tensor([0, 3, 1, 4, 2, 5])

e = torch.tensor([1, 2, 3]).expand(4, 3)
print(e.stride())                          # (0, 1)  rows repeat without copying

img = torch.randn(8, 3, 32, 32).to(memory_format=torch.channels_last)
print(img.shape, img.stride())             # torch.Size([8, 3, 32, 32]) (3072, 1, 96, 3)

Remember

  • Tensor = storage + offset + shape + strides
  • Views share memory; contiguous()/clone() copy
  • channels_last keeps NCHW shape but stores C innermost

Watch out for

  • Keeping a small slice of a huge tensor around, which keeps the entire storage in memory.

Check yourself

  1. x has shape (4, 5) and is contiguous. What is x.t().stride()?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pytorch.org/docs/stable/tensor_view.html

Builds on

Unlocks

Nothing builds on this yet.