Training on a GPU

PyTorch · Practical · 10 min

GPUs make deep learning 10-100x faster, but only if data and model actually live there.

To train on a GPU, put both the model and the data on it:

  • model.to(device) moves all parameters and buffers in place. Do this before creating the optimizer.
  • Each batch: xb = xb.to(device, non_blocking=True). With pin_memory=True in the DataLoader, the copy can overlap with computation.

Write the device choice once and keep the rest device-agnostic. CUDA covers NVIDIA (and AMD via ROCm builds, which also use the "cuda" name); "mps" covers Apple silicon. Recent releases also offer torch.accelerator.current_accelerator(), which picks whichever accelerator is present.

GPU work is asynchronous: Python queues kernels and continues. The CPU only waits when it needs a result, for example .item(), .cpu(), print(tensor) or an if on a tensor's value. Each of these is a synchronisation; too many in the inner loop starve the GPU. When timing GPU code, call torch.cuda.synchronize() before reading the clock.

Check memory with torch.cuda.memory_allocated() and nvidia-smi. If you run out of memory, reduce the batch size first, then look at mixed precision and gradient accumulation.

Common hiccups: tensors created inside forward with torch.zeros(...) land on the CPU; create them with device=x.device or torch.zeros_like.

Example

import torch
from torch import nn

device = torch.device(
    "cuda" if torch.cuda.is_available()
    else "mps" if torch.backends.mps.is_available()
    else "cpu")

model = nn.Linear(10, 2).to(device)               # move parameters first
opt = torch.optim.SGD(model.parameters(), lr=0.1)

x = torch.randn(64, 10).to(device, non_blocking=True)
mask = torch.ones(64, 1, device=x.device)          # create new tensors on the same device

loss = (model(x) * mask).pow(2).mean()
opt.zero_grad()
loss.backward()
opt.step()
print(loss.device.type, round(loss.item(), 3))     # .item() synchronises and copies to CPU

if device.type == "cuda":
    torch.cuda.synchronize()                       # before timing anything
    print(torch.cuda.memory_allocated() // 2**20, "MiB allocated")

Remember

  • Model and data must both be on the device
  • GPU calls are async; .item(), .cpu() and printing synchronise
  • Create tensors inside forward with device=x.device

Watch out for

  • Moving the inputs to the GPU but forgetting the targets, which fails inside the loss.

Check yourself

  1. Why can timing a CUDA forward pass with time.time() before and after give a misleadingly small number?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pytorch.org/docs/stable/notes/cuda.html