PyTorch Cheatsheet
Inference
Use this PyTorch reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Basic Inference Pattern
model.eval() # disable Dropout, use BN running stats with torch.inference_mode(): # no grad graph built output = model(input_tensor)
Use
torch.inference_mode()overtorch.no_grad()for pure inference — it is faster and enforces stricter guarantees (no views can be used in autograd contexts).
Preparing Inputs
import torch from torchvision import transforms # Single image (PIL → tensor → batch) transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) img_tensor = transform(pil_image).unsqueeze(0) # (1, 3, 224, 224) # Batch of numpy arrays x = torch.from_numpy(np_array).float() x = x.to(device)
Single-Sample vs. Batch Inference
model.eval() with torch.inference_mode(): # Single sample — add batch dim single = input_tensor.unsqueeze(0).to(device) # (1, C, H, W) out = model(single) out = out.squeeze(0) # remove batch dim # Batch for x in loader: x = x.to(device) out = model(x)
Output Interpretation
# Multi-class classification logits = model(x) # (N, C) probs = torch.softmax(logits, dim=1) # (N, C) pred_class = logits.argmax(dim=1) # (N,) top5_probs, top5_idx = torch.topk(probs, k=5, dim=1) # Binary classification logit = model(x) # (N, 1) or (N,) prob = torch.sigmoid(logit) pred = (prob > 0.5).long() # Regression value = model(x) # (N, 1) or (N, out_dim) # Segmentation / dense output mask_logits = model(x) # (N, num_classes, H, W) mask = mask_logits.argmax(dim=1) # (N, H, W) # Object detection — unpack model-specific output dict detections = model(images) # list of dicts: boxes, labels, scores
Moving Results Back to CPU/Python
with torch.inference_mode(): out = model(x.to(device)) # To numpy / Python arr = out.cpu().numpy() # stays float32 scalar = out.item() # single-element tensor → Python float python_list = out.tolist() # With class labels class_idx = out.argmax(dim=1).item() class_name = idx_to_class[class_idx]
Inference DataLoader
from torch.utils.data import DataLoader, TensorDataset # Build dataset and loader test_dataset = MyDataset(test_data) test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=4, pin_memory=True) model.eval() all_preds = [] all_probs = [] with torch.inference_mode(): for x in test_loader: x = x.to(device, non_blocking=True) logits = model(x) probs = torch.softmax(logits, dim=1) preds = logits.argmax(dim=1) all_preds.append(preds.cpu()) all_probs.append(probs.cpu()) all_preds = torch.cat(all_preds) # (N,) all_probs = torch.cat(all_probs) # (N, C)
Mixed-Precision Inference
from torch.amp import autocast model.eval() with torch.inference_mode(): with autocast('cuda', dtype=torch.float16): output = model(x.to(device))
AMP at inference speeds up throughput and reduces VRAM by ~2x compared to float32. For bfloat16 use
dtype=torch.bfloat16.
TorchScript Inference
# Load a traced or scripted model (no class definition needed) scripted = torch.jit.load('model_scripted.pt', map_location=device) scripted.eval() with torch.inference_mode(): output = scripted(input_tensor.to(device))
ONNX Runtime Inference
import onnxruntime as ort import numpy as np session = ort.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) input_name = session.get_inputs()[0].name output_name = session.get_outputs()[0].name # Input must be numpy array x_np = x.numpy() result = session.run([output_name], {input_name: x_np})[0] output = torch.from_numpy(result)
torch.compile for Inference
model.eval() compiled = torch.compile(model, mode='reduce-overhead') # good for small batches compiled = torch.compile(model, mode='max-autotune') # best throughput, slow warmup with torch.inference_mode(): output = compiled(x)
First call triggers compilation (slow). Subsequent calls use the compiled kernel. Best with fixed shapes.
Batch Size Tuning for Throughput
import time model.eval() results = {} for batch_size in [1, 8, 16, 32, 64, 128]: x = torch.randn(batch_size, 3, 224, 224, device=device) # warmup with torch.inference_mode(): for _ in range(5): model(x) torch.cuda.synchronize() start = time.perf_counter() with torch.inference_mode(): for _ in range(50): model(x) torch.cuda.synchronize() elapsed = time.perf_counter() - start throughput = batch_size * 50 / elapsed results[batch_size] = throughput print(f'bs={batch_size:3d} {throughput:.0f} samples/sec')
Quantization (TorchAO — current API)
torchao is the PyTorch-native quantization stack (pip install torchao). One in-place call, composes with torch.compile:
from torchao.quantization import ( quantize_, Int8WeightOnlyConfig, Int8DynamicActivationInt8WeightConfig, Int4WeightOnlyConfig, ) # Weight-only int8 — safe general speedup, ~2x less VRAM quantize_(model, Int8WeightOnlyConfig()) # Dynamic int8 activations + int8 weights — Linear-heavy models quantize_(model, Int8DynamicActivationInt8WeightConfig()) # Weight-only int4 (grouped) — LLM inference quantize_(model, Int4WeightOnlyConfig(group_size=128)) model = torch.compile(model, mode='max-autotune') # pair with compile with torch.inference_mode(): output = model(x)
Graph-mode post-training quantization for exported models lives in the PT2E workflow (
prepare_pt2e/convert_pt2eon atorch.export-ed graph); quantizers ship with torchao.
Quantization (Legacy Eager Mode — deprecated)
Eager-mode quantization under torch.ao.quantization (the old torch.quantization name is a deprecated alias) is superseded by torchao/PT2E. Only for maintaining existing CPU deployments:
import torch.ao.quantization as tq # Post-training dynamic (weights only) — LSTM/Transformer on CPU quantized = tq.quantize_dynamic(model, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8) # Post-training static (activations calibrated) model.qconfig = tq.get_default_qconfig('x86') # or 'qnnpack' for ARM/mobile tq.prepare(model, inplace=True) with torch.inference_mode(): for x, _ in calibration_loader: # calibration pass model(x) tq.convert(model, inplace=True) # Quantization-aware training (QAT) model.qconfig = tq.get_default_qat_qconfig('x86') tq.prepare_qat(model, inplace=True) # ... train normally ... tq.convert(model.eval(), inplace=True)
Pruning
import torch.nn.utils.prune as prune # Unstructured pruning — zero out lowest-magnitude weights prune.l1_unstructured(model.fc1, name='weight', amount=0.3) # prune 30% # Structured pruning — remove entire filters/channels prune.ln_structured(model.conv1, name='weight', amount=0.2, n=2, dim=0) # Make pruning permanent (remove mask, keep zeroed weights) prune.remove(model.fc1, 'weight') # Global pruning across layers prune.global_unstructured( [(model.fc1, 'weight'), (model.fc2, 'weight')], pruning_method=prune.L1Unstructured, amount=0.2, ) # Check sparsity total = model.fc1.weight.numel() zero = float(torch.sum(model.fc1.weight == 0)) print(f'Sparsity: {100 * zero / total:.1f}%')
Benchmarking with torch.utils.benchmark
from torch.utils.benchmark import Timer t = Timer( stmt='model(x)', setup='', globals={'model': model, 'x': x}, num_threads=1, ) result = t.timeit(100) print(result) # Includes: mean, median, IQR — handles CUDA sync automatically
Common Gotchas
Forgetting
model.eval()causes Dropout to randomly zero outputs and BatchNorm to use batch statistics — both corrupt predictions silently.
torch.no_grad()still builds the grad function objects (just does not compute gradients).torch.inference_mode()skips that entirely and is faster.
After
torch.compile, the first few calls may be slow due to just-in-time compilation. Profile only after warmup iterations.
model.half()(float16) can causeNaNfor some operations with very small or very large values. Preferautocastwhich keeps sensitive ops in float32 automatically.
For deployment in production, prefer exporting to TorchScript or ONNX — they do not require the Python class definition and are portable across runtimes.