PyTorch Cheatsheet
GPU and Device
Use this PyTorch reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Device Detection
import torch # Check availability torch.cuda.is_available() # True if CUDA GPU is accessible torch.backends.mps.is_available() # True on Apple Silicon (Metal) torch.cuda.device_count() # number of GPUs torch.cuda.current_device() # index of current GPU torch.cuda.get_device_name(0) # GPU name string # Universal device selection device = ( 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu' ) device = torch.device(device)
Moving Data to Device
# Tensors t = t.to(device) t = t.to('cuda') t = t.to('cuda:0') # specific GPU index t = t.cuda() # shortcut t = t.cpu() # With options t = t.to(device, dtype=torch.float16, non_blocking=True) # Models model = model.to(device) model = model.cuda() # Rule: always move model first, then data inside the loop model = model.to(device) for x, y in loader: x = x.to(device, non_blocking=True) # non_blocking with pin_memory y = y.to(device, non_blocking=True)
Pinned Memory (host-side)
# Pinned memory enables faster CPU→GPU transfers via DMA t = torch.zeros(1000, 1000).pin_memory() t = t.to(device, non_blocking=True) # async transfer # In DataLoader loader = DataLoader(dataset, batch_size=32, pin_memory=True, num_workers=4) # combines pin_memory + non_blocking
CUDA Device Context
# Explicitly select device torch.cuda.set_device(1) # Context manager — all operations in this block use cuda:1 with torch.cuda.device(1): model = MyModel().cuda() out = model(x.cuda()) # Create tensor on specific device t = torch.empty(3, 3, device='cuda:1') t = torch.empty(3, 3, device=torch.device('cuda', 1))
Memory Management
# Query torch.cuda.memory_allocated() # bytes in use (current tensors) torch.cuda.max_memory_allocated() # peak usage since last reset torch.cuda.memory_reserved() # bytes reserved by the caching allocator torch.cuda.max_memory_reserved() # peak reserved # Human-readable summary print(torch.cuda.memory_summary(device)) # Free unused cached memory (does not free in-use allocations) torch.cuda.empty_cache() # Reset peak stats torch.cuda.reset_peak_memory_stats() # Global memory total, reserved, free = torch.cuda.mem_get_info() # bytes
Reducing Memory Usage
# 1. Use mixed precision from torch.amp import autocast with autocast('cuda', dtype=torch.float16): output = model(x) # 2. Gradient checkpointing from torch.utils.checkpoint import checkpoint out = checkpoint(segment, x) # 3. Delete intermediates and empty cache del intermediate_tensor torch.cuda.empty_cache() # 4. Accumulate gradients over smaller batches # 5. Use in-place operations where safe # 6. Reduce batch size or input resolution
CUDA Streams
# Streams allow overlapping compute with memory transfers s1 = torch.cuda.Stream() s2 = torch.cuda.Stream() with torch.cuda.stream(s1): y1 = model_a(x1) with torch.cuda.stream(s2): y2 = model_b(x2) torch.cuda.synchronize() # wait for all streams on current device # Current stream current = torch.cuda.current_stream() current.synchronize() # CUDA events (for fine-grained timing) start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) start.record() out = model(x) end.record() torch.cuda.synchronize() elapsed_ms = start.elapsed_time(end)
Timing GPU Operations
# Always synchronize before timing! torch.cuda.synchronize() import time t0 = time.perf_counter() for _ in range(100): out = model(x) torch.cuda.synchronize() elapsed = (time.perf_counter() - t0) / 100 # CUDA event timing (more accurate) start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) start.record() out = model(x) end.record() torch.cuda.synchronize() print(start.elapsed_time(end), 'ms')
Mixed Precision
from torch.amp import autocast, GradScaler scaler = GradScaler('cuda') for x, y in loader: x, y = x.to(device), y.to(device) optimizer.zero_grad(set_to_none=True) with autocast('cuda', dtype=torch.float16): output = model(x) loss = criterion(output, y) scaler.scale(loss).backward() scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) scaler.step(optimizer) scaler.update()
On Ampere+ GPUs (A100, H100, RTX 3090+), prefer
bfloat16overfloat16— same exponent range asfloat32, so no loss scaling is needed: noGradScalerrequired.
with autocast('cuda', dtype=torch.bfloat16): output = model(x) loss = criterion(output, y) loss.backward() optimizer.step()
cuDNN Settings
# Benchmark mode: tries multiple algorithms and picks the fastest # Good for: fixed input sizes, production training torch.backends.cudnn.benchmark = True # Deterministic mode: always use the same algorithm # Good for: reproducibility torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False # turn off benchmark when deterministic # Disable cuDNN entirely (rarely needed) torch.backends.cudnn.enabled = False
CUDA Graphs (PyTorch >= 1.10)
# Capture and replay a static computation graph # Eliminates Python overhead — best for fixed-shape batch training # Warmup s = torch.cuda.Stream() s.wait_stream(torch.cuda.current_stream()) with torch.cuda.stream(s): for _ in range(3): optimizer.zero_grad(set_to_none=True) y_pred = model(x) loss = criterion(y_pred, y) loss.backward() optimizer.step() torch.cuda.current_stream().wait_stream(s) # Capture g = torch.cuda.CUDAGraph() optimizer.zero_grad(set_to_none=True) with torch.cuda.graph(g): static_output = model(static_x) static_loss = criterion(static_output, static_y) static_loss.backward() # Replay (copy data into static buffers, replay graph) static_x.copy_(real_x) static_y.copy_(real_y) g.replay() optimizer.step()
Apple Silicon (MPS)
device = torch.device('mps') model = model.to(device) x = x.to(device) # Check MPS availability torch.backends.mps.is_available() torch.backends.mps.is_built() # Fallback for unsupported ops import os os.environ['PYTORCH_ENABLE_MPS_FALLBACK'] = '1'
MPS does not support all PyTorch ops. Set
PYTORCH_ENABLE_MPS_FALLBACK=1to silently fall back to CPU for unsupported operations.
Multi-GPU: Summary
| Approach | When to use |
|---|---|
nn.DataParallel | Quick single-node prototyping, 2–4 GPUs |
DistributedDataParallel | Production, best performance, any scale |
FullyShardedDataParallel | Very large models (billions of params) |
torch.compile | Single GPU, reduces Python overhead, Inductor backend |
# DDP launch (from command line) # torchrun --nproc_per_node=4 --nnodes=1 train.py # Inside train.py import torch.distributed as dist dist.init_process_group(backend='nccl') # nccl for GPU, gloo for CPU local_rank = int(os.environ['LOCAL_RANK']) torch.cuda.set_device(local_rank) model = DDP(model.to(local_rank), device_ids=[local_rank]) dist.destroy_process_group()
Gotchas
Moving a model to GPU after creating an optimizer means the optimizer still holds references to CPU parameters. Always
.to(device)before creating the optimizer.
Calling
torch.cuda.empty_cache()does not reduce GPU memory usage shown bynvidia-smi— it only returns cached (but unused) memory to the allocator pool.
CPU and GPU operations execute asynchronously. Timing must include
torch.cuda.synchronize(), otherwise you measure only kernel launch time.
Mixing
device='cuda'anddevice='cpu'tensors in an operation raises aRuntimeError. Always ensure all inputs to an operation are on the same device.