PyTorch / Storage, restoration, and transformed execution / Lesson 13
Views, contiguity, and in-place updates
Separate logical indexing, shared storage, and gradient history before mutating or reshaping tensor state.
On this page
Lesson goals
- Read shape and stride as different descriptions.
- Distinguish views, possible copies, clone, and detach.
- Explain why an in-place change can invalidate a backward calculation.
Shape alone does not determine storage traversal
A dense tensor can describe the same storage through different shapes, offsets, and strides. For a contiguous two-by-three row layout, advancing one row moves three elements and advancing one column moves one. Transposing exchanges logical axes and their strides without necessarily copying elements. The resulting three-by-two view can be noncontiguous even though its values form a perfectly regular table. Contiguity is a storage property relative to a memory format, not evidence that axis meanings or units are correct. Inspect shape, stride, and the intended indexing contract together before choosing an operation that assumes a particular layout.
Copy behavior belongs to the selected operation
The illustrative program transposes a CPU float64 tensor and calls contiguous on that noncontiguous view before changing the original. The transpose sees the new ninety-nine because it shares storage; the materialized copy retains zero. contiguous can return the existing tensor when its condition is already satisfied, so this example does not imply that every call creates independent storage. A view operation requires a compatible relationship between shape and strides. reshape and flatten may return a view or a copy under their documented conditions; code should not depend on an unspecified ownership outcome.
import torch
x = torch.arange(6,dtype=torch.float64,device="cpu").reshape(2,3)
transposed = x.t()
owned = transposed.contiguous()
x[0,0] = 99.0
print("strides", x.stride(), transposed.stride())
print("first values", transposed[0,0].item(), owned[0,0].item())
print("contiguous", transposed.is_contiguous(), owned.is_contiguous())
strides (3, 1) (1, 3)
first values 99.0 0.0
contiguous False True
The transpose shares storage while contiguous copies this noncontiguous input. The tensors do not require gradients; no tensor layout or mutation was executed in torch.
History and storage independence are separate questions
clone creates copied tensor data while normally retaining a differentiable relationship when grad mode and inputs require it. detach stops the autograd relationship but shares storage with its source. Consequently, a detached tensor is not automatically an independent snapshot that can be changed harmlessly. A deliberate detached independent value often needs both operations, such as detach().clone(), under an explicit purpose. The list companion illustrates storage ownership only and cannot reproduce these gradient semantics. Choose copying for ownership and gradient control for differentiation, rather than using either word as a substitute for the other.
storage = [float(x) for x in range(6)]
def transpose_row(i):
return [storage[i+j*3] for j in range(2)]
owned = [transpose_row(i) for i in range(3)]
storage[0] = 99.0
print("shared first row", transpose_row(0))
print("copied first row", owned[0])
print("transpose offsets", [[i+j*3 for j in range(2)] for i in range(3)])
shared first row [99.0, 3.0]
copied first row [0.0, 3.0]
transpose offsets [[0, 3], [1, 4], [2, 5]]
The executed list-offset model illustrates aliasing and ownership. It does not implement Tensor strides, view eligibility, version counters, or autograd checks.
In-place modification interacts with saved backward values
Operations that end with an underscore or assignment through an index can modify existing tensor storage. Autograd may need an earlier value saved during the forward computation, and version tracking can detect that it was changed before backward. Leaf tensors requiring gradients also have documented restrictions on tracked in-place updates. A no-grad block can be appropriate for a planned optimizer-style update, but does not make arbitrary mutation between forward and backward correct. Do not silence an error by detaching everything or using hidden storage shortcuts; identify the mathematical state the derivative is supposed to use.
Test aliasing at the actual boundary that needs it
Use a tiny numbered tensor, an asymmetric shape, and a controlled change to distinguish value equality from storage independence. Keep both input and expected unchanged snapshots where a test depends on them. Check saved model state separately because a mapping of tensor references can change with the model unless copied appropriately. Optimize memory or layout only after verifying the full computation and measuring the relevant environment. The framework program is unexecuted and uses no gradients; the standard-library companion checks offsets and list copies only, with no tensor kernel, backward pass, or memory-performance claim.
Common errors and fixes
A detached snapshot changes when the source changes
- Why it happens
- detach removed history but retained shared storage.
- How to fix it
- Create an intentional independent copy when the snapshot contract requires it.
- Prevent it next time
- Test a controlled mutation separately from equal initial values.
A reshape is assumed always to copy or always to share
- Why it happens
- Ownership was inferred from a convenient API name.
- How to fix it
- Use the documented operation and explicitly request copying when necessary.
- Prevent it next time
- Keep a noncontiguous example in the ownership checks.
Backward fails after an intermediate value is overwritten
- Why it happens
- The gradient calculation depended on saved forward state that changed.
- How to fix it
- Avoid or redesign the in-place change under the intended derivative contract.
- Prevent it next time
- Test the forward/backward boundary before adding memory optimizations.
Check your understanding
1. Why does the transposed first value become ninety-nine while owned retains zero?
Show worked solution
The transpose indexes the original storage at offset zero, so the source assignment is visible through that view. contiguous materialized independent storage for this noncontiguous input before the assignment. Its copied first element therefore remains zero. The conclusion depends on this layout and timing, not a promise that every contiguous call copies.
2. Why is detach alone insufficient for an independently mutable diagnostic snapshot?
Show worked solution
It stops the gradient path but does not provide independent storage. Changing either alias can affect the other and may interfere with saved values still needed elsewhere. Decide whether the snapshot must be detached, copied, or both, and verify that intended contract on a small fixture.
Key takeaways
- Shape, stride, storage ownership, and gradient history differ.
- An operationโs copy behavior needs an explicit contract.
- Preserve the forward state required by the intended derivative.