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: writing through one view changes the other
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_stridedbypasses 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
expandare free; bewareexpandsharing a single row's storage.