PyTorch tensors on the GPU
Read these first if they are new to you
- Arrays in NumPy Intermediate
The idea in one paragraph
A PyTorch tensor is an array, like a NumPy array, with one more property: the device it lives on. On the CPU, the computer's main processor, it works much like NumPy. On a GPU, a graphics card, thousands of simple cores apply the same step to thousands of numbers at once. The catch is that the GPU has its own memory. Data must be copied there before the work and copied back after it, and that copy is slow compared with the work. So programs move data to the GPU once and do many steps there.
A tensor is an array that knows which device holds it
PyTorch is a Python library for machine learning. Its array type is the tensor. A tensor has a shape and
a dtype, just like a NumPy array, and most NumPy operations have a PyTorch twin. It
also has a device: cpu, or cuda for an NVIDIA GPU. CUDA is NVIDIA's system for
running general programs on its graphics cards.
The method .to(device) returns a copy of the tensor on that device. .cpu() copies it back,
and .numpy() turns a CPU tensor into a NumPy array. Before you read the output below, predict the two
dtypes on the first line.
tensor.py
import numpy as np
import torch
a = np.array([[0.0, 0.0, 0.0], [2.0, 4.0, 0.0]])
x = torch.from_numpy(a) # from a NumPy array
y = torch.tensor([[1.0, 2.0, 3.0]]) # from a Python list
print(x.dtype, y.dtype)
print(x.shape, x.device)
# Pick "cuda" when PyTorch finds a GPU, and "cpu" otherwise.
device = "cuda" if torch.cuda.is_available() else "cpu"
x = x.float().to(device) # a float32 copy on the GPU, if there is one
z = x * 10 # runs where x lives
print(z.device)
print(z.cpu().numpy()) # copy back to the CPU, then to NumPy
torch.float64 torch.float32
torch.Size([2, 3]) cpu
cpu
[[ 0. 0. 0.]
[20. 40. 0.]]
NumPy makes 64-bit decimals by default, and torch.from_numpy keeps that type. PyTorch makes 32-bit
decimals, float32, by default. Neural networks almost always use float32 or smaller types,
because GPUs handle them much faster. .float() converts to float32.
The second line of output prints the device before the move, so it says cpu on any computer. The
output above came from a computer without a GPU, so z stayed on the CPU too, and the third line says
cpu. On a computer with a GPU, the third line says cuda:0, the first GPU. Every other line
stays the same.
A GPU applies one step to thousands of numbers at once
A CPU has a few powerful cores, the parts that carry out instructions. A desktop CPU typically has from 4 to 24. Each core can follow its own complicated program. A GPU has thousands of simpler cores that work in groups, all doing the same step on different numbers. Adding 1 to a million numbers is exactly that kind of job, and so are most steps of a neural network.
Every tensor in one operation must be on the same device. If x is on the GPU and y is on
the CPU, x + y stops with an error that says all tensors were expected to be on the same device. The fix
is to move one of them with .to(...).
The copy can take longer than the work
Take a million 3D points as float32, and add 1 to each number.
- Size: 1,000,000 points × 3 numbers × 4 bytes = 12,000,000 bytes, or 12 MB.
- Copy to the GPU: a PCIe 4.0 link moves roughly 25 GB per second in practice. 12 MB ÷ 25 GB/s ≈ 0.5 ms.
- Work on the GPU: the step reads 12 MB and writes 12 MB. An NVIDIA A100 GPU's memory is rated at 1,555 GB per second or more, and newer GPUs are faster. 24 MB ÷ 1,500 GB/s ≈ 0.016 ms.
- Copy back: another 0.5 ms.
- Work on the CPU instead, with no copies: on the computer that ran this page's snippets, one CPU core did the same step in about 0.4 ms.
The GPU route costs about 1 ms, almost all of it copying. The CPU does the whole step in less than half that time. For one step, the GPU is not worth it. For a neural network that does thousands of steps on the same data, it is.
size.py
import torch
clouds = torch.zeros(8, 2048, 3) # all zeros: 8 point clouds, 2048 points each
print(clouds.shape, clouds.dtype)
print(clouds.nelement(), "numbers of", clouds.element_size(), "bytes each")
print(clouds.nelement() * clouds.element_size() / 1_000_000, "MB")
torch.Size([8, 2048, 3]) torch.float32
49152 numbers of 4 bytes each
0.196608 MB
The CPU here does 8 numbers per tick. The GPU does all 256 per tick, but each copy takes 40 ticks. At 1 step, which finishes first? Predict, press Run, then drag the slider until the GPU wins.
Link and memory speeds vary several times between machines, and the race uses invented speeds. A PCIe 4.0 link is rated at about 32 GB per second each way and reaches about 25 GB per second in practice. A PCIe 5.0 link doubles both. Copies from ordinary memory, rather than memory set aside for copying, are often slower. CPUs also do some work in parallel: each core can apply one instruction to several numbers. The shape of the trade-off holds: copying is slow, and a GPU pays off when it does many steps per copy.
PyTorch hands GPU work to a queue and returns at once, before the work is done. A Python timer around a GPU step
therefore measures almost nothing. Call torch.cuda.synchronize(), which waits for the queue to empty,
before you read the clock. Copying a result back with .cpu() or .item() also waits.
Check yourself
-
A model's weights are on the GPU. You call it on a tensor
batchmade withtorch.from_numpy. It fails with a device error. What one change fixes it, and what second problem might remain?Answer
Move the input:
batch = batch.to("cuda"). The tensor may also still befloat64, while the weights arefloat32. Usebatch.float().to("cuda"). -
How many megabytes does a
float32tensor of shape(16, 4096, 3)take?Answer
16 × 4096 × 3 = 196,608 numbers, times 4 bytes = 786,432 bytes, or about 0.79 MB.
-
In the race, with 3 steps, how many ticks does each side take, and who wins?
Answer
CPU: 32 × 3 = 96 ticks. GPU: 40 + 3 + 40 = 83 ticks. The GPU wins. At 2 steps the CPU wins, 64 to 82.
Go further
Optional background.
-
The official introduction. This part shows moving a tensor to a GPU; the part before it lists a tensor's shape, dtype and device.
-
Why GPU work runs in the background, and how to time it correctly.
Where this shows up
Units that build on this one
- Automatic gradients in PyTorch Intermediate
- Mixed precision: 16-bit numbers (bf16) Intermediate
- Training on several GPUs at once Advanced