Tensors & Neural Plumbing intermediate 7 min read 12 flashcards

Views, Aliasing, and In-Place Operations

A transpose returns a new tensor object that shares its old storage, which is why in-place edits leak across variables you thought were separate and why autograd raises "a variable needed for gradient computation has been modified".

b = a.transpose(0, 1) allocates no memory. It creates a second tensor object pointing at the same bytes with a different stride tuple. So does slicing, reshape when it can avoid a copy, expand, and permute. Write into b and a changes, which is the correct and documented behaviour, and also the source of a whole family of bugs that produce wrong numbers instead of exceptions.

A tensor is metadata over a buffer

Three things define a tensor: a pointer into a storage buffer, a shape, and a stride tuple giving the step in elements along each dimension. A contiguous \((4, 8)\) float tensor has strides \((8, 1)\). Its transpose has shape \((8, 4)\) and strides \((1, 8)\), over the identical buffer. Nothing was moved.

This is why .view() sometimes fails where .reshape() succeeds. view insists on producing a new stride tuple over the existing buffer and raises if no valid one exists; reshape falls back to a copy. It is also why .contiguous() is a real operation with a real cost, not a formality: kernels that assume unit stride on the last dimension need the bytes actually laid out that way.

Two failures, one cause

Silent aliasing. Storing a slice as history, then mutating the parent, rewrites the history. buffer[i] = x followed by logs.append(buffer[i]) appends a view; the next write to buffer[i] changes what was already appended. Nothing errors. The classic instance is keeping hidden[:, -1] per step in a loop and finding every entry equal to the last one.

Autograd version-counter errors. Every storage carries a version counter, incremented by any in-place write. When an op saves a tensor for backward, it records the version it saw; at backward time it compares. A mismatch raises "one of the variables needed for gradient computation has been modified by an inplace operation". The check exists because the saved value would otherwise be silently wrong, and it is deliberately conservative: it flags the write even when the specific gradient formula happens not to need the original.

Which ops are safe in place depends on the gradient formula, not on intuition. \(y = x^2\) needs \(x\) for its backward, so x.pow_(2) destroys it. ReLU's backward needs only the sign, recoverable from the output, so relu_ is allowed. add_ is fine because addition's gradient needs neither operand.

The escape hatches, and what each costs

.detach() returns a view sharing storage but cut from the graph. It stops gradients, not aliasing, and .detach() followed by an in-place write still corrupts the original and still bumps the version counter. .clone() copies data and stays in the graph, which is the usual correct fix and costs an allocation. .detach().clone() gives an independent tensor. with torch.no_grad(): suppresses graph recording entirely and is what optimiser step functions use to mutate parameters in place.

When it breaks

Memory savings from in-place ops are usually smaller than expected and sometimes negative. Fusing an activation in place saves one activation-sized buffer, but if autograd then has to keep the input alive anyway because a later op needs it, nothing was saved and a version-counter risk was added.

The failure surface widens under torch.compile and custom kernels. A compiled region may reorder or fuse operations, and a custom op that mutates its input without declaring the mutation escapes the version counter entirely, giving wrong gradients with no error at all. Mutation has to be declared in the op schema for the checker to see it.

Distributed training adds aliasing across process boundaries. Gradient buckets in DDP and flattened parameter shards in FSDP are large buffers that individual parameter tensors view into, so a well-meaning in-place edit of one parameter's .grad can write into a bucket mid-reduction. The result is a silent numerical divergence between ranks rather than a crash, which is among the least pleasant classes of bug to chase.

Check yourself

12 flashcards for this concept

Click a card to reveal the answer.

Drill the whole track