Skip to content

Tensor Internals— Storage, Views & Strides

Overview

A tensor is not a box of numbers. It is a recipe: a reference to a flat storage (the actual memory) plus a shape and a stride (how to walk that memory). Understanding this split is the difference between shipping code and debugging aliasing bugs at 2am.

  • Storage: the raw 1-D block of memory (.storage(), .data_ptr()).
  • Strides: the tuple telling you how many elements to skip per dimension.
  • View: a tensor sharing the same storage with a different stride recipe.
  • Copy: a tensor with its own storage; changes are isolated.

"View" operations are free (no data movement). "Copy" operations cost memory bandwidth.


The Anatomy of a Tensor

What a view really is

import torch

x = torch.arange(12).reshape(3, 4)
print(x)
print("base storage:", x.storage())
print("data_ptr:", x.data_ptr())
print("shape:", tuple(x.shape))
print("strides:", x.stride()) # (4, 1) -> move +4 elems per row, +1 per col
print("contiguous:", x.is_contiguous())

A contiguous row-major tensor always has stride (size[n-1]*...*size[1],..., 1).

view, transpose, squeeze share storage

y = x.transpose(0, 1) # shares storage with x
print("y strides:", y.stride()) # (1, 4) -- reversed walk
print("stroke:", y.is_contiguous()) # False

# Aliasing
y[0, 0] = 999
print(x[0, 0]) # 999 -- SAME storage!

Broadcast is a zero-copy view

x = torch.zeros(1, 4)
bc, stride = torch.broadcast_tensors(x[None,:,:], torch.zeros(2, 1, 4))
print("new_stride:", bc[0].stride()) # still walks only 4 elems

-

View vs Copy: Know the Family

Operations that return views (share memory):

  • .view(), .reshape() (when possible), .transpose(), .permute(), .t(), .squeeze(), .unsqueeze(), .expand(), slicing x[1:3], .flatten()/.reshape that can reuse layout.

Operations that return copies (new memory):

  • .clone(), .numpy() (returns new buffer), .to() when dtype/device changes, .contiguous() when already non-contiguous, arithmetic ops, .reshape() when it can't share.

Safely detaching shared storage

base = torch.arange(6).reshape(2, 3)
v = base[:,:2] # view
c = base[:,:2].clone() # copy

base[0, 0] = -123
print(v[0, 0]) # -123 (shared)
print(c[0, 0]) # 0 (isolated)

reshape vs view vs contiguous

Call Returns When it copies
.view() always a view raises if incompatible layout
.reshape() view if possible, else copy when layout incompatible
.contiguous() copy if needed, else self when already non-contiguous
x = torch.arange(12).reshape(3, 4).transpose(0, 1) # non-contiguous

try:
 x.view(2, 6) # ValueError: cannot view this layout
except ValueError as e:
 print("view fails:", e)

print(x.reshape(2, 6).is_contiguous()) # works -> copied, contiguous
print(x.contiguous() is x) # False (copy created)

-

Strides with Slicing, Step & Permute

x = torch.arange(20).reshape(4, 5)

sl = x[::2,::2] # every other row/col
print("slice strides:", sl.stride()) # walks with step 2 -> (10, 2)
print("slice shape:", sl.shape) # (2, 3)

p = x.permute(1, 0)
print("perm strides:", p.stride()) # (1, 5)

Slicing with a step creates non-contiguous tensors that are slow in loops; fix with .contiguous() when it matters.

-

torch.empty, as_strided— Master of Storage

as_strided lets you define an arbitrary view over raw storage. It's how quantized/attention kernels and some libraries build exotic views. Use with extreme care.

storage = torch.arange(9)
twin = torch.as_strided(storage, size=(3, 3), stride=(3, 1))
print(twin)
print("overlaps storage:", twin.storage().data_ptr() == storage.data_ptr())

as_strided bypasses all safety checks; aliasing & overlapping memory here is your responsibility.


Detection & Debugging Snippets

def describe(t: torch.Tensor):
 print(
 f"shape={tuple(t.shape)} strides={t.stride()} "
 f"contig={t.is_contiguous()} ptr={t.data_ptr()} "
 f"storage_nbytes={t.storage().nbytes() if t.storage() else 0}"
)

a = torch.randn(4, 4)
b = a.t()
describe(a) # ptr=0x... shape=(4,4) contig=True
describe(b) # contig=False -- but SAME ptr family

-

Key Takeaways

  • Tensors = storage + strides, not boxes of numbers.
  • view/transpose/slicing = zero-copy; .clone()/arithmetic = new memory.
  • .reshape() copies when the layout forbids a view; .view() raises instead.
  • Aliased storage is the #1 source of silent bugs in hand-rolled layers.
  • Broadcast and expand are free; beware expand sharing a single row's storage.

-