PyTorch GPU: CUDA and MPS

Running PyTorch on a GPU can make training 10-50 times faster than a CPU, because neural network math is massively parallel. PyTorch supports NVIDIA GPUs through CUDA, AMD GPUs through ROCm (which also uses the "cuda" device name), and Apple Silicon through MPS.

Picking a Device

import torch

if torch.cuda.is_available():
    device = torch.device("cuda")
elif torch.backends.mps.is_available():
    device = torch.device("mps")
else:
    device = torch.device("cpu")
print(device)
cuda   (sample — mps on a Mac, cpu otherwise)

Recent PyTorch versions also offer torch.accelerator.current_accelerator(), which returns whichever accelerator is present, but the explicit version above works everywhere and is easy to read.

Moving Models and Data

Both the model and every batch must be on the same device. .to(device) moves a model in place, but returns a new tensor for tensors, so assign the result.

from torch import nn

model = nn.Linear(10, 2).to(device)
x = torch.randn(4, 10).to(device)
print(model(x).device)

# create directly on the device
z = torch.zeros(3, device=device)
cuda:0   (sample)

The Device Mismatch Error

The most common GPU error looks like this:

x_cpu = torch.randn(4, 10)
model(x_cpu)
RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!

Fix it by moving inputs with .to(device) inside your training loop. To bring results back for NumPy or plotting, use tensor.cpu().

Checking GPU Memory

if device.type == "cuda":
    print(torch.cuda.get_device_name(0))
    print(round(torch.cuda.memory_allocated() / 1e6, 1), "MB allocated")
NVIDIA GeForce RTX 4070
0.1 MB allocated
(sample output)

If you see "CUDA out of memory", reduce the batch size, use mixed precision, or free tensors you no longer need. nvidia-smi in a terminal shows overall GPU usage.

Mixed Precision Training

Automatic mixed precision runs most operations in 16-bit, which is faster and uses less memory, while keeping sensitive operations in 32-bit. bfloat16 is the simplest on modern GPUs:

opt = torch.optim.AdamW(model.parameters(), lr=1e-3)
xb, yb = torch.randn(32, 10, device=device), torch.randint(0, 2, (32,), device=device)

with torch.autocast(device_type=device.type, dtype=torch.bfloat16):
    loss = nn.functional.cross_entropy(model(xb), yb)
opt.zero_grad()
loss.backward()
opt.step()
print(round(loss.item(), 4))
0.7231   (sample output)

With float16 instead of bfloat16, also use torch.amp.GradScaler to prevent tiny gradients from underflowing to zero.

Speed Tips

  • Use DataLoader(pin_memory=True) and .to(device, non_blocking=True) on CUDA.
  • Avoid calling .item() or .cpu() every step; they force the GPU to synchronize.
  • Try model = torch.compile(model) for extra speed on supported setups.
  • Not every operation is implemented on MPS yet; setting PYTORCH_ENABLE_MPS_FALLBACK=1 runs missing ones on the CPU.

Frequently asked questions

How do I use the GPU in PyTorch?

Create device = torch.device("cuda") (or "mps" on a Mac), move the model with model.to(device), and move every batch with xb.to(device) inside the loop.

Does PyTorch work on Apple M-series Macs?

Yes. The standard macOS build includes the MPS backend, which uses the Apple GPU. Most common layers are supported.

How do I fix CUDA out of memory in PyTorch?

Lower the batch size, enable mixed precision, use gradient accumulation, and make sure you are not keeping references to old tensors such as un-detached losses.