9.1e Tensor shapes, strides, and contiguity in memory

📦 Mathematical Imperative 📖 The Mathematics of Deep Learning

When working with deep learning frameworks like PyTorch or TensorFlow, engineers often focus on tensor values and shapes. However, understanding how tensors are laid out in memory is critical for performance optimization, debugging, and avoiding subtle bugs. This section introduces three fundamental concepts: shapes, strides, and memory contiguity.


📐 What Is a Tensor Shape?

A tensor's shape defines its dimensionality and the size of each dimension. It tells you how many elements exist along each axis.

  • A 1D tensor (vector) with 6 elements has shape (6,)
  • A 2D tensor (matrix) with 2 rows and 3 columns has shape (2, 3)
  • A 3D tensor (e.g., a batch of RGB images) might have shape (32, 3, 224, 224) — 32 images, 3 color channels, 224x224 pixels each

Key point: Shape describes the logical structure, not the physical memory layout.


🧭 What Are Strides?

Strides define how many memory positions you must skip to move one step along each dimension. Strides are expressed in number of elements (not bytes).

  • For a contiguous 2D tensor of shape (2, 3), the strides are (3, 1)
  • Moving one row forward skips 3 elements
  • Moving one column forward skips 1 element

  • For a 3D tensor of shape (2, 3, 4), contiguous strides would be (12, 4, 1)

  • Moving along dimension 0 skips 12 elements (3 × 4)
  • Moving along dimension 1 skips 4 elements
  • Moving along dimension 2 skips 1 element

Why strides matter: They allow frameworks to create "views" of tensors without copying data. For example, transposing a matrix simply swaps strides instead of rearranging elements in memory.


🔗 Contiguity in Memory

A tensor is contiguous when its elements are stored in a single, uninterrupted block of memory in row-major order (C-style layout). This is the default layout in most deep learning frameworks.

Contiguous tensor characteristics: - Elements are stored sequentially in memory - Strides follow the formula: stride[i] = shape[i+1] * stride[i+1] - Operations like .view() or .reshape() work without copying data - Hardware (GPUs) can access contiguous memory much faster

Non-contiguous tensors arise from operations like: - Transposing (.T or .transpose()) - Slicing with non-unit steps - Certain indexing operations

Non-contiguous tensors still store valid data, but accessing elements requires jumping around in memory, which is slower.


🕵️ How to Check Contiguity

In PyTorch, you can check if a tensor is contiguous using:

For reference:

import torch

tensor_a = torch.randn(2, 3)
print(tensor_a.is_contiguous())

📤 Output: True

For reference:

tensor_b = tensor_a.T  # Transpose
print(tensor_b.is_contiguous())

📤 Output: False

To make a non-contiguous tensor contiguous, call .contiguous():

For reference:

tensor_c = tensor_b.contiguous()
print(tensor_c.is_contiguous())

📤 Output: True


📊 Visual Representation: Tensor Shapes and Strides Mapping

This diagram maps how a multi-dimensional tensor shape (dimensions) maps to raw contiguous memory layout using strides.

flowchart LR TensorShape["Logical Shape (2, 3)"] --> Strides["Strides: (3, 1)"] Strides --> Memory["Physical Contiguous Memory (1D Array)"] classDef cpu fill:#eafaf1,stroke:#76b900,stroke-width:2px,rx:6px,ry:6px; classDef memory fill:#f0f7ff,stroke:#3498db,stroke-width:1.5px,rx:4px,ry:4px; classDef system fill:#f1f5f9,stroke:#64748b,stroke-width:1.5px; class Strides cpu; class Memory memory; class TensorShape system;

📊 Comparison: Contiguous vs. Non-Contiguous Tensors

Feature Contiguous Tensor Non-Contiguous Tensor
Memory layout Sequential, single block Scattered, multiple blocks
Access speed Fast (cache-friendly) Slower (random access)
View operations Supported without copy May require copy
GPU performance Optimal Degraded
Common cause Default creation Transpose, slicing, permute

⚙️ Practical Example: Strides in Action

Consider a 2D tensor with shape (3, 4):

For reference:

import torch

x = torch.arange(12).reshape(3, 4)
print("Shape:", x.shape)
print("Strides:", x.stride())
print(x)

📤 Output: Shape: torch.Size([3, 4]) Strides: (4, 1) tensor([[ 0, 1, 2, 3], ** [ 4, 5, 6, 7], ** [ 8, 9, 10, 11]])

Now transpose it:

For reference:

y = x.T
print("Shape:", y.shape)
print("Strides:", y.stride())
print(y)

📤 Output: Shape: torch.Size([4, 3]) Strides: (1, 4) tensor([[ 0, 4, 8], ** [ 1, 5, 9], ** [ 2, 6, 10], ** [ 3, 7, 11]])**

Notice that the strides swapped from (4, 1) to (1, 4). The data in memory did not move — only the interpretation changed. This is why transposing is fast but creates a non-contiguous tensor.


🛠️ Why This Matters for Engineers

  • Performance tuning: Operations on contiguous tensors run 2-10x faster on GPUs
  • Memory efficiency: Understanding strides helps avoid unnecessary data copies
  • Debugging: Shape mismatches and stride-related bugs are common in complex models
  • Custom kernels: When writing CUDA or custom operations, you must handle strides explicitly

Simple rule of thumb: If you perform a reshape, transpose, or slice, check if the result is contiguous. If not, and you need maximum performance, call .contiguous() to create a fresh memory layout.


✅ Key Takeaways

  • Shape = logical dimensions of a tensor
  • Strides = number of elements to skip per dimension step
  • Contiguity = whether elements are stored sequentially in memory
  • Non-contiguous tensors are slower but avoid copying data
  • Use .is_contiguous() to check and .contiguous() to fix
  • Most deep learning operations prefer contiguous input for optimal speed