GPU Lab / interactive course
GPU mapSource ↗

EXTRA PRACTICE / AFTER CHAPTER 5

Reading a tensor

Follow a warp’s addresses from tensor elements to memory sectors.

Before you start: Complete chapter 5: How do threads read those values? first, then go straight to the memory access controls. The explanations below recap storage and strides if you need them.

START WITH A PYTORCH TENSOR

Where does a tensor keep its numbers?

PyTorch calls the memory holding a tensor’s numbers its storage. A tensor on the CPU normally uses system RAM; a CUDA tensor (on an NVIDIA GPU) normally uses GPU memory, also called VRAM. Here, storage means memory, not a saved file on an SSD or disk.

We often draw a tensor as a grid. In a contiguous, row-major tensor, its values occupy neighbouring positions in memory: all the values of the first row, then the second row, and so on. Each number in this example is FP32 (32-bit floating point), occupying four bytes.

One tensor: a grid and its memory layout

Drawn as 2 rows × 4 columns

The same 8 values in memory order

First 4 positions: row 0 · Next 4: row 1. Small labels are byte offsets from the start of storage, not RAM addresses.

These are two pictures of the same eight values, not two copies of them.

The shape tells PyTorch how to interpret those values as a grid: here it is (2, 4). The strides tell it how many element positions to move when an index changes. Strides (4, 1) mean that moving down one row advances four elements (16 bytes), while moving right one column advances one element (4 bytes).

Now imagine a batch of 32 examples with eight features each. We can make a larger tensor whose values are easy to follow:

Python · requires PyTorch · runs on CPU
import torch

x = torch.arange(32 * 8, dtype=torch.float32).reshape(32, 8)
print(x.shape, x.stride())   # shape (32, 8), strides (8, 1)

This tensor has shape (32, 8): 32 rows and eight columns. Its strides (8, 1) mean a move down one row advances 8 elements (32 bytes), and a move right one column advances 1 element (4 bytes).

With arange, the values happen to match their element offsets (positions counted from the beginning of storage). This is a convenient coincidence, not a general property of tensors. Byte offsets are those positions multiplied by four; actual memory addresses are different and are not shown.

Example: move right one column

Tensor view: x[row, column]

01234567891011121314151617181920212223

… 32 rows in total · 8 columns across →

Storage order (row-major)

00 B14 B28 B312 B416 B520 B624 B728 B832 B936 B

0 → 1 · +1 element = +4 bytes

x[0, 0] → x[0, 1]: column index +1. Storage offset 0 → 1: +1 element × 4 bytes = +4 bytes.

Example: move down one row

Tensor view: x[row, column]

01234567891011121314151617181920212223

… 32 rows in total · 8 columns across →

Storage order (row-major)

00 B14 B28 B312 B416 B520 B624 B728 B832 B936 B

0 → 8 · +8 elements = +32 bytes

x[0, 0] → x[1, 0]: row index +1. Storage offset 0 → 8: +8 elements × 4 bytes = +32 bytes.

Both slices below are views: they share x’s storage. Reading the first row visits neighbouring elements. Reading the first column visits every eighth element. contiguous() copies this strided column into packed storage; that copy also takes work and memory.

Compare: x[:, 0] (view) versus x[:, 0].contiguous() (copy)

x: original storage (first 4 rows)

012345678910111213141516171819202122232425262728293031

… 32 rows in total · 8 columns across →

Column view: x[:, 0] · stride 8 · shares x's storage

0value 0 at slot 0next: +8 elements
8value 8 at slot 8next: +8 elements
16value 16 at slot 16next: +8 elements
24value 24 at slot 24

Values 0, 8, 16, 24 … point into x's existing storage at offsets 0, 8, 16, 24 … No copy is made; the slice is a view with stride 8.

Packed copy: x[:, 0].contiguous() · stride 1 · new storage

0value 0 at slot 0next: +1 element
8value 8 at slot 1next: +1 element
16value 16 at slot 2next: +1 element
24value 24 at slot 3

The same values 0, 8, 16, 24 … are copied into new storage at offsets 0, 1, 2, 3 … The copy has stride 1. .contiguous() on an already-contiguous tensor may return the same tensor without copying.

More settings: explore other starting positions

One index step, two views

Dashed border = start. Solid filled border = destination. The underlined storage cells lie between them.

Tensor view: 32 rows × 8 columns

Showing rows 0–3; each cell shows x[row, column].

012345678910111213141516171819202122232425262728293031

← 8 columns across · 32 rows down ↓

The same values in storage order

A window starting at element 0. Neighbouring boxes are neighbouring stored elements.

00 B14 B28 B312 B416 B520 B624 B728 B832 B

0 → 8
+8 elements = +32 bytes

Top number: element offset and value in this arange example. Bottom: byte offset from x’s storage base.

x[0, 0] → x[1, 0]: increase the row index by one. Storage offset 0 → 8; 8 elements × 4 bytes = 32 bytes.

Strides count elements. Each FP32 element occupies four bytes, so the row step is eight × four bytes and the column step is one × four bytes. This figure shows x’s original contiguous storage.

A view keeps the original offsets; a packed copy changes them

x: original storage

012345678910111213141516171819202122232425262728293031

First four rows of x. Highlighted values belong to the selected slice.

x[:, 0]

View points into x’s same storage

0slot 0next: +8
8slot 8next: +8
16slot 16next: +8
24slot 24

First four reads shown; 32 values in the complete slice.

Storage is shared: no values are copied to make this view. The resulting slice has stride 8.

Full runnable example
Python · requires PyTorch · runs on CPU
import torch

x = torch.arange(32 * 8, dtype=torch.float32).reshape(32, 8)
row = x[0, :]       # features of example 0: 0, 1, ..., 7
column = x[:, 0]   # feature 0 of each example: 0, 8, ..., 248

print(x.shape, x.stride())   # shape [32, 8], strides (8, 1)
print(row.stride())          # (1,)
print(column.stride())       # (8,)

packed_column = column.contiguous()
print(packed_column.stride())  # (1,)

The buttons below map one participating lane to each selected value. The row has eight values, so its experiment uses eight active lanes. The column has 32 values and uses the full warp. Both start at offset zero.

This CPU example needs no GPU. The same shape and stride reasoning applies to the corresponding CUDA tensor. PyTorch can choose its own kernels, thread mapping and copies; slicing alone does not launch the particular load modelled here.

Same values. Different addresses.

Interactive draft

Imagine reading one feature from each row of a tensor. The GPU can give a different row to each participating thread.

New concepts

Kernel
A function launched to run on a GPU. Many threads execute it.
Thread
One execution of the kernel, with its own thread identity and work.
Warp
A group of 32 threads that an NVIDIA GPU schedules together.
Lane
A thread’s position within its warp, numbered 0 through 31.
One warp · 32 lanes

Each square represents one lane (one thread). Eight per row is just the drawing layout, not a separate hardware group. Some lanes may be inactive for an instruction.

Each of the 32 threads in a warp can read a different row. When every lane participates, the warp requests 32 values together. Each value here is a 32-bit floating-point number (FP32) occupying four bytes.

The GPU can combine nearby memory requests. Our teaching model groups addresses into aligned 32-byte sectors: bytes 0–31, then 32–63, and so on. Combining nearby requests into fewer memory transactions is called coalescing.

Model comparison: what a warp reads

This model counts distinct aligned 32-byte sectors touched by one warp's FP32 load. It is not measured GPU timing. Each FP32 value is 4 bytes; each sector holds 8 values (32 bytes).

Row read: 32 lanes, 32 adjacent values

Sector 0
L0L1L2L3L4L5L6L7
Sector 1
L8L9L10L11L12L13L14L15
Sector 2
L16L17L18L19L20L21L22L23
Sector 3
L24L25L26L27L28L29L30L31

4 sectors · 128 bytes requested · 128 bytes covered · 100% utilisation

Column read: 32 lanes, every 8th value

S0
L0
S1
L1
S2
L2
S3
L3
S4
L4
S5
L5
S6
L6
S7
L7
S8
L8
S9
L9
S10
L10
S11
L11
S12
L12
S13
L13
S14
L14
S15
L15
S16
L16
S17
L17
S18
L18
S19
L19
S20
L20
S21
L21
S22
L22
S23
L23
S24
L24
S25
L25
S26
L26
S27
L27
S28
L28
S29
L29
S30
L30
S31
L31

32 sectors · 128 bytes requested · 1,024 bytes covered · 12.5% utilisation

Both reads request the same 128 bytes of useful data, but the column read touches 32 sectors instead of 4. More sectors can mean more memory traffic; this is a model count, not a measured slowdown. Note: the row pattern x[0, :] has only 8 values (8 active lanes), while the column x[:, 0] has 32. Comparing their sector totals is not a same-work comparison.

The comparison above shows the key result: reading 32 adjacent values touches 4 sectors; reading every 8th value touches 32 sectors, even though both request the same 128 bytes of useful data. Use the experiment below to explore how stride, offset, and active lanes change the sector count.

EXPERIMENT · ONE WARP · ONE LOADAddress model
Try a pattern

The warp

Choose a lane to follow its address

What this model tells you

We count distinct aligned 32-byte sectors touched by one warp’s global-memory load, with four-byte aligned FP32 values and a sector-aligned base pointer. This matches the coalescing accounting described for devices of compute capability 6.0 or newer in NVIDIA’s CUDA Best Practices Guide.

These counts are not DRAM transactions or elapsed time. Caches, reuse between warps, stores, instruction issue, and memory-controller behaviour are outside this model. A sector diagram does not show execution order. More sectors can increase memory traffic; it does not imply a proportional slowdown.

Read NVIDIA’s coalescing guide ↗

CONNECT THE MODEL TO CODE

A small CUDA kernel.

CUDA lets you write the program each GPU thread runs. This kernel copies selected input values into consecutive output positions. Change stride or offset above and the input indexing below changes with it.

Threads are also grouped into blocks. Here, threadIdx.x is a thread’s index in its block; blockIdx.x identifies the block; blockDim.x is its thread count. Together they calculate i, the output index.

The simulator shows the input load for the first warp of the first block. Its active-lane control corresponds to a bound n smaller than a warp. The output store is not counted.

gather.cu · illustrative source

TRY IT YOURSELF

Make a prediction you can explain.

  1. Choose Tensor column. Think of a row-major tensor with eight features per row. The warp reads the same feature from successive rows. Predict the sector count, then check it.
  2. Switch to Contiguous. Now it reads neighbouring features. Compare requested bytes and sector coverage. Which changed?
  3. Choose Shift by one. Explain what happens at the two ends of the address range.
  4. Set 8 active lanes, stride 1, and offset 0. Predict what changes if you move the offset to 1.
Explain the result in your own words, then compare

Each sector holds eight FP32 values. Neighbouring lanes can use different values in the same sector. With a stride of eight elements, each lane reaches a different sector. Shifting a consecutive range by one element can make it cross an extra sector boundary.

In an AI workload, changing how threads map onto tensor dimensions can change this access pattern. A transpose or a different tensor layout may help one operation and add costs elsewhere. Count the work across the whole operation before choosing a layout.

References

Use these official references to check the concepts and code on this page.

These browser exercises do not run an installed PyTorch or CUDA toolkit. For optional local execution, choose documentation matching your installed tools. Links open online in a new tab; the questions and answer comparisons below work offline.

Use the documentation

Use the references to explain each answer, then apply it to this page’s example. No installation is needed.

Install GPU Lab

Give GPU Lab its own icon and window. All lessons and simulators work offline once the course is saved.

Use your browser’s install option:

If your browser has no install option, try Chrome or Edge.

App status

Version

Checking for updates

Offline access

Not saved for offline use

Save all lessons, browser simulators, diagrams and example files. Desktop execution needs separate PyTorch or CUDA/GPGPU-Sim tools; their installations are not part of this offline download. External links need internet access.