Key ideas
- A deep learning framework gives you tensors on CPU or GPU, automatic differentiation, ready-made layers, losses and optimizers, data loading, and ways to save or export models. PyTorch, TensorFlow and JAX are frameworks; Keras is a high-level API that runs on top of them (
model.compile(...), thenmodel.fit(x, y, epochs=5)). GPUs and TPUs help because training is dominated by large tensor operations that run in parallel; they make training faster, not more accurate. ONNX is a common file format for trained models, so other frameworks and runtimes can run them. - Eager vs compiled: PyTorch executes each operation as Python reaches it and records the autograd graph on the fly, rebuilding it from scratch every iteration, so ordinary Python control flow (and printing any intermediate tensor) just works1. Compiling, e.g. with
jax.jit, hands the whole function to a compiler (XLA) that fuses operations and skips the per-operation Python dispatch, which is usually much faster2. - Tracing:
jax.jitruns the Python function once with tracers that carry only shapes and dtypes. A Pythonifon a traced value therefore raises an error (usejax.lax.condor a static argument); Python side effects such asprintorlist.appendhappen only during tracing; and the compiled code is cached, so calling again with arrays of the same shapes and dtypes reuses it, while a new shape or dtype (or a new value of a static argument) triggers a new trace2.torch.jit.trace, now deprecated in favour oftorch.compileandtorch.export, records only the branch taken by the example input and replays it on every input. - Random numbers in JAX: there is no hidden global generator; every
jax.randomfunction is a pure function of an explicit key, so the same key always gives the same numbers. Derive new keys withkey, sub = jax.random.split(key)and never reuse a key unless you want identical draws3. - Autograd rules in PyTorch1:
- Leaf tensors with
requires_grad=Trueaccumulate gradients into.grad, so calloptimizer.zero_grad()every step. - Under
torch.no_grad()nothing is recorded: use it for manual parameter updates1 and for validation or inference, where it saves memory and time4. Callingbackward()on a result computed there raises aRuntimeError, and so does updating a leaf that requires grad in place outside it (w -= lr * w.grad). x.detach()shares the values but is cut from the graph, so autograd treats it as a constant.- To freeze parameters, set
requires_grad=Falseon them1.
- Leaf tensors with
- Training loop:
zero_grad()→ forward → loss →backward()→optimizer.step()→scheduler.step(). - Modes:
model.train()andmodel.eval()switch dropout and batch normalization between training and inference behaviour.eval()is not a gradient mode: it neither stops graph recording nor freezes weights1; converselyno_grad()does not switch dropout off. - Parameters vs buffers: BatchNorm's and are parameters;
running_mean,running_varandnum_batches_trackedare buffers, stored in thestate_dictbut not returned byparameters(). Saving thestate_dictis the recommended way; pickling the whole model stores only a reference to the file that defines its class, so moving or refactoring that code breaks loading. Callmodel.eval()before inference5. - GPUs run asynchronously: Python queues GPU work and moves on; anything that needs a GPU value on the CPU (
print(t),t.item(),t.cpu()) forces a synchronization, so a per-step.item()stalls the pipeline4. With the defaultnum_workers=0theDataLoaderloads batches in the main process while the GPU waits;num_workers=k > 0uses worker processes that prepare batches in parallel with training4. - Data-parallel training (DDP), usually one process per GPU: the replicas start identical (state broadcast from rank 0); gradients are grouped into buckets whose all-reduce starts as soon as they are ready, overlapping with the rest of the backward pass; when
backward()returns, every process holds the same averaged gradient and takes the same optimizer step on its own replica6. The effective batch is the per-GPU batch times the number of GPUs. - Memory during training: weights, gradients, optimizer state (two extra tensors per parameter for Adam) and stored activations, which grow with the batch size. Remedies: smaller micro-batches with gradient accumulation, mixed precision, and activation checkpointing, which keeps only some activations and recomputes the rest during the backward pass, trading compute for memory4.
- Mixed precision and loss scaling: in float16, gradients below about flush to zero and their updates are lost. Multiplying the loss by a factor multiplies every gradient by , which lifts them into range; the gradients are divided by again before the optimizer step, so the learning rate is unchanged. An that is too large makes gradients overflow (float16's maximum is 65504), and the scaler then lowers . PyTorch's float16 recipe pairs autocasting with a
GradScaler, while its bfloat16 mixed precision (e.g. on CPU) uses autocasting alone7; bfloat16 keeps float32's 8 exponent bits and hence its range.
Formulas
Gradient accumulation over micro-batches equals one large batch only if each micro-batch's mean loss is divided by :
Loss scaling: , so the unscaled step is .
Training memory with Adam in float32: about bytes per parameter (weights, gradients, two moments) plus activations.
Worked example
, data , loss = mean of , whose gradient is .
- Full batch: , gradient .
- Two micro-batches and with
backward()on each mean loss: plus , so.gradholds , twice too much. Dividing each micro-batch loss by gives . - Memory: 10 million float32 parameters trained with Adam need bytes MB before activations.
In code
import torch
w = torch.tensor(2.0, requires_grad=True)
x = torch.tensor([1.0, 3.0, 2.0, 4.0])
for chunk in x.split(2): # two micro-batches of 2
(((w * chunk) ** 2).mean() / 2).backward() # divide by the number of micro-batches
print(w.grad) # tensor(30.) same as one batch of 4
with torch.no_grad():
w -= 0.01 * w.grad # manual SGD step, not recorded
w.grad.zero_()
y = w.detach() ** 2 + 3 * w # detached term is a constant
y.backward()
print(w.grad) # tensor(3.)
Common pitfalls
.gradaccumulates acrossbackward()calls; forgettingzero_grad()adds old gradients to new ones.model.eval()is nottorch.no_grad(): one changes layer behaviour, the other stops graph recording; inference usually needs both. To freeze layers setrequires_grad=False;.eval()freezes nothing.- A traced or compiled function keeps the control flow seen at trace time; data-dependent
ifstatements need framework control-flow primitives, and Python side effects insidejax.jitsilently stop once the compiled version is cached. - Calling a
jax.randomfunction twice with the same key does not give new numbers; split the key. - Summing micro-batch mean losses without dividing by their number multiplies the gradient by that number.
- Loss scaling fights underflow of small float16 gradients, not overflow, and it does not raise the effective learning rate, because the gradients are unscaled before the step.
- The same seed and code can still drift apart on a GPU:
torch.manual_seedseeds CPU and CUDA alike, but some kernels add with atomic operations in a varying order, floating-point addition is not associative, and cuDNN's benchmark mode may pick a different convolution algorithm in each run8. - A
state_dictholds tensors only; you need the model class to rebuild the network beforeload_state_dict.
References
- PyTorch documentation: Autograd mechanics — How autograd encodes the history; Locally disabling gradient computation (Setting requires_grad, No-grad Mode, Evaluation Mode (nn.Module.eval()))
- JAX documentation: Just-in-time compilation — What compilation buys you; What the compiled function captures; Why can't we just jit everything?; JIT and caching
- JAX documentation: Pseudorandom numbers — Why not a global generator?; Keys are values; Deriving new keys
- PyTorch tutorials: Performance Tuning Guide — Enable asynchronous data loading and augmentation; Disable gradient calculation for validation or inference; Checkpoint intermediate buffers; Avoid unnecessary CPU-GPU synchronization
- PyTorch tutorial: Saving and Loading Models — What is a state_dict?; Save/Load state_dict (Recommended); Save/Load Entire Model
- PyTorch documentation: Distributed Data Parallel — Internal Design
- PyTorch documentation: Automatic Mixed Precision package (torch.amp) — Introduction; Gradient Scaling
- PyTorch documentation: Reproducibility — PyTorch random number generator; CUDA convolution benchmarking; Avoiding nondeterministic algorithms (CUDA Scaled Dot Product Attention)