Warmup Guide — PyTorch
Zero-to-expert primer for Phase 03: tensors and autograd, the training loop and its failure modes, CNNs through ResNet, transfer learning's regimes, and a working mental model of distributed training.
Table of Contents
- Chapter 1: Tensors — NumPy with Two Superpowers
- Chapter 2: Autograd — The Tape Behind the Curtain
- Chapter 3: Modules, Losses, Optimizers — The Object Model
- Chapter 4: The Training Loop and Its Bug Bestiary
- Chapter 5: Data Loading — The Underrated Bottleneck
- Chapter 6: Convolutions to ResNet
- Chapter 7: Transfer Learning — The Default Strategy
- Chapter 8: Distributed Training in One Chapter
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: Tensors — NumPy with Two Superpowers
A torch.Tensor is Phase 00's ndarray (same buffer/shape/strides/dtype model, same
broadcasting rule, same views-vs-copies semantics) plus: device placement
(.to("cuda") — and the rule that all operands of an op must share a device; the
Expected all tensors to be on the same device error is your introduction) and
gradient tracking (Ch. 2).
The transfer costs that shape code structure: host↔device copies are expensive and
asynchronous-able (pin_memory + non_blocking=True in loaders); .item()/.cpu()
synchronize — calling them per step in the loop serializes the GPU (the bug that
the model-accuracy track's profiling phase hunts with traces; here, just don't write
it). Image layout is (N, C, H, W) in PyTorch — the transpose from OpenCV/NumPy's
HWC is a permanent border-crossing ritual (permute(2, 0, 1)), and contiguous()
after permute when an op demands it.
Chapter 2: Autograd — The Tape Behind the Curtain
Every operation on a requires_grad tensor records a node in a dynamic computation
graph; loss.backward() walks it in reverse applying the chain rule (Phase 00 Ch. 5,
automated). The working rules:
- Gradients accumulate into
.grad— henceoptimizer.zero_grad()every step (forgetting it = silently summed gradients = effective LR drift; bug bestiary #1). - The graph is rebuilt every forward (define-by-run) — Python control flow just
works; the price is per-op overhead (what
torch.compilelater removes — the model-accuracy track's Phase 04 is the deep dive; here, know it exists). with torch.no_grad():for inference/eval — skips graph construction (memory and speed);.detach()cuts a tensor out of the graph (the stop-gradient tool — you saw its load-bearing uses in DINO and DPO if you've toured other tracks).- Leaf vs non-leaf: only leaves (parameters, inputs you created) get
.gradby default; intermediate gradients needretain_grad()— relevant when debugging. - Verify understanding once: implement a custom
torch.autograd.Functionwith explicit forward/backward and gradcheck it (the lab does) — after that, autograd is bookkeeping you trust.
Chapter 3: Modules, Losses, Optimizers — The Object Model
nn.Module: parameters (registered automatically by attribute assignment) +forward(). Composition is nesting;model.parameters()walks the tree — understanding registration explains the classic silent bug (modules stored in a plain Python list don't register — usenn.ModuleList).train()vseval()mode: flips Dropout and BatchNorm behavior. Forgettingmodel.eval()at validation (ortrain()after) is bug bestiary #2 — symptoms: noisy val metrics or frozen-stats training.- Losses:
CrossEntropyLosstakes logits, not probabilities (it fuses log-softmax + NLL for numerical stability — bestiary #3 is applying softmax first); class weights live here for imbalance. - Optimizers: SGD+momentum (the CNN classic — pairs with stepped/cosine LR decay) and AdamW (the adaptive default; the LLM track's Phase 05 derives it fully). LR schedulers step per epoch or per iteration — know which yours expects. The LR is the single most important hyperparameter; everything else is second-order.
Chapter 4: The Training Loop and Its Bug Bestiary
The canonical loop you'll write hundreds of times — and its known failure modes, each of which the lab makes you commit deliberately:
for x, y in loader:
x, y = x.to(dev, non_blocking=True), y.to(dev, non_blocking=True)
optimizer.zero_grad()
loss = criterion(model(x), y)
loss.backward()
optimizer.step()
| Bug | Symptom |
|---|---|
missing zero_grad | loss decreases then oscillates; effective LR wrong |
missing model.eval() / no_grad at val | val noisy, OOM at eval, slow eval |
| softmax before CrossEntropyLoss | trains, but worse — doubly-squashed gradients |
accumulating loss (not loss.item()) into a list | memory leak — the whole graph retained per step |
| labels/logits shape or dtype mismatch | sometimes an error; sometimes silent broadcasting garbage (Phase 00 Ch. 3's warning, realized) |
| transforms with stale normalization stats | accuracy mysteriously capped |
And the two pre-flight rituals from the shared curriculum discipline: overfit one batch to ~zero loss before any long run (wiring test), and fixed-seed determinism so changes are attributable.
Chapter 5: Data Loading — The Underrated Bottleneck
Dataset (__getitem__/__len__ — Phase 00's dunder protocol, productized) +
DataLoader (batching, shuffling, parallel workers). The performance model:
workers decode/augment on CPU while the GPU trains — if the GPU waits (low
utilization, jagged step times), raise num_workers, use pin_memory=True, move
heavy augmentation to GPU (or pre-resize the dataset on disk — JPEG decode of
full-resolution photos is the classic hidden cost). Augmentation policy lives here
too: train-time randomness (flips, crops, color jitter) is regularization;
val/test transforms are deterministic (resize + center-crop + normalize) — train
transforms on val data is bestiary #7. Normalization uses the pretraining dataset's
stats when fine-tuning (ImageNet means/stds) — a contract with the checkpoint, same
species as the LLM track's chat-template contract.
Chapter 6: Convolutions to ResNet
- Convolution layers are Phase 01 Ch. 3 with learned kernels: local connectivity + weight sharing = translation equivariance and parameter economy (a 3×3×C_in×C_out kernel vs a dense layer over pixels). Output size arithmetic ($\lfloor (n + 2p - k)/s \rfloor + 1$) and receptive field growth with depth are the two calculations to be fluent in.
- BatchNorm: normalize each channel over the batch, then learnable scale/shift — stabilizes and accelerates CNN training; its batch-statistics dependence is also its curse (small batches → noisy stats; train/eval mode split; the running-stats machinery). Conv-BN-ReLU is the CNN unit (and BN folds into conv at inference — the model-accuracy track's FX lab does it mechanically).
- The ResNet insight: deeper plain nets trained worse (not just overfit — higher training error: a degradation problem). Residual blocks ($x + F(x)$) make identity the default and let layers learn corrections — gradient highways (the same idea you've now met as LSTM cell states and transformer residuals; deep learning has one good trick and it keeps wearing new clothes). Bottleneck blocks (1×1 reduce → 3×3 → 1×1 expand) buy depth cheaply; stride-2 stages halve resolution while doubling channels.
- Building ResNet-18 from scratch (the lab) and matching torchvision's parameter count exactly is the rite of passage — the count check catches every wiring mistake.
Chapter 7: Transfer Learning — The Default Strategy
Training from scratch is the exception; the default is starting from ImageNet-pretrained (or CLIP-class) weights:
- Why it works: early conv layers learn universal features (edges, textures — Gabor-like filters emerge regardless of task); only later layers are task-specific. Pretraining is, in effect, a better initialization plus a feature library.
- The regimes, by data size and domain gap:
- Linear probe (freeze all, train a new head): tiny data, similar domain.
- Partial fine-tune (unfreeze last block(s) + head): the workhorse middle.
- Full fine-tune at low LR (often with discriminative LRs — smaller for early layers): enough data, or large domain gap.
- Larger domain gap (medical, satellite, industrial) shifts you toward later regimes — early features transfer less.
- The mechanics that matter: replace the classifier head to your class count; match the pretraining normalization stats; consider BN-stats behavior when freezing (frozen-but-train-mode BN is a subtle gotcha); start with 10–100× lower LR than scratch training. The lab runs regimes 1–3 on the same dataset — the comparison table is the deliverable.
Chapter 8: Distributed Training in One Chapter
The working mental model (the LLM track's Phase 10 carries the full depth):
- DDP (the 90% case): N processes, each a GPU, each a model replica and a data
shard; gradients all-reduced (averaged) each step so replicas stay identical.
Launch via
torchrun; wrap withDistributedDataParallel; useDistributedSampler(andset_epochfor reshuffling — the classic omission). Effective batch = per-GPU batch × N: adjust LR accordingly (linear-scaling heuristic + warmup). - The practical rules: rank-0-only logging/checkpointing;
barrier()around shared filesystem moments; seeds offset per rank for augmentation diversity; and measurement before scaling — a dataloader-bound job scales GPUs into idleness (Ch. 5 first!). - Beyond DDP — when the model doesn't fit — lives sharding (FSDP/ZeRO) and tensor/pipeline parallelism: pointers in the LLM track's Phase 10 WARMUP; for CV-scale models, DDP is almost always the whole story.
Lab Walkthrough Guidance
Order: labs 01→05 as numbered.
- Lab 01 (tensors/autograd): port a Phase 00 NumPy exercise to tensors; build and
gradcheck a custom autograd Function; demonstrate the
.gradaccumulation behavior explicitly. - Lab 02 (training loop): write the loop bare (no Lightning-style wrappers); commit each bestiary bug deliberately and document its symptom; finish with the overfit-one-batch ritual and a clean CIFAR-10 run with curves.
- Lab 03 (ResNet from scratch): build ResNet-18; assert parameter-count parity with torchvision; train on CIFAR-10 (with the stem adaptation for 32×32) and compare against your plain-CNN baseline — the residual delta, personally measured.
- Lab 04 (transfer learning): the three regimes on a small fine-grained dataset; the comparison table + learning curves; note where each regime's curve says bias vs variance (Phase 02's lens, reused).
- Lab 05 (distributed): convert lab 02's script to DDP via torchrun on 2 processes (GPU or even CPU-Gloo to learn the mechanics); verify same-loss-as-single-process at matched effective batch; measure scaling efficiency and name what limits it.
Success Criteria
You are ready for Phase 04 when you can, from memory:
- Explain device semantics, the
.item()sync trap, and NCHW vs HWC. - Describe graph construction/backward, why
zero_grad, and whatdetachdoes. - Recite five bestiary bugs with symptoms.
- Do conv output-size and receptive-field arithmetic; explain BN's train/eval split.
- Tell the ResNet degradation story and connect residuals to the other gradient highways you know.
- Choose a transfer regime from data size × domain gap and list the three mechanical musts.
- Sketch DDP's replicate-shard-allreduce loop and the LR/batch adjustment.
Interview Q&A
Q: Training loss decreases but validation accuracy is stuck at chance. What's your
checklist?
Wiring first: labels shuffled relative to inputs in the val loader (or a transform
mismatch — train augmentations applied to val); model.eval() missing (BN/Dropout
chaos); metric computed on logits vs probabilities with a wrong argmax axis;
normalization stats mismatched between train and val. The tell is chance-level —
that's a broken pipe, not underfitting; bisect by evaluating on the training set
(if that's also chance under the val code path, the eval code is the bug).
Q: Why does CrossEntropyLoss take logits instead of probabilities?
Numerical stability: it computes log-softmax internally via the log-sum-exp trick;
feeding probabilities means an external softmax → log of small numbers → precision
loss, and gradients through two softmaxes are doubly squashed. It also fuses two
kernels. The deeper point: API contracts encode numerics — same reason
binary_cross_entropy_with_logits exists and is preferred.
Q: You move from 1 GPU to 4 with DDP and accuracy drops slightly. Why might that be
correct behavior?
Effective batch ×4 changes the optimization trajectory: fewer steps per epoch, less
gradient noise (which regularizes), and the LR heuristic (linear scaling) is an
approximation — some recipes need warmup retuning. Also check DistributedSampler
without set_epoch (same shard order every epoch) and per-rank seed handling. The
point being probed: DDP is mathematically not identical to the single-GPU run at
the same nominal hyperparameters — it's identical to a 4×-batch run.
References
- PyTorch tutorials: 60-minute blitz + autograd mechanics notes
- He et al., Deep Residual Learning for Image Recognition (2015) — arXiv:1512.03385
- Ioffe & Szegedy, Batch Normalization (2015) — arXiv:1502.03167
- Goyal et al., Accurate, Large Minibatch SGD (2017) — arXiv:1706.02677 — linear scaling + warmup
- Yosinski et al., How transferable are features in deep neural networks? (2014) — arXiv:1411.1792
- PyTorch DDP design notes
- Karpathy, A Recipe for Training Neural Networks — the bug-bestiary philosophy, canonically
- The model-accuracy track's Phase 01 WARMUP — autograd/dispatcher internals when you want the next level down