Unit 10 · AI research · Lesson 2 of 9
PyTorch and autograd fluency
Step 1: Learn the ideas
- Tensors: dtype, device, strides, and views versus copies
- Broadcasting rules and shape debugging
- The autograd graph: leaves, grad_fn and gradient accumulation
- Saved tensors, activation memory and retain_graph
- In-place operations and the version counter
- no_grad, inference_mode, detach and eval mode
- Custom autograd Functions and double backward (awareness)
- Gradient checking with finite differences and gradcheck
- Seeding, deterministic algorithms and data-loader workers
Why it matters
In research code, wrong gradients rarely crash; they quietly train a worse model. A broadcast that turns a per-example loss into a matrix, a tensor modified in place after it was saved, or an evaluation loop that keeps building graphs can each cost days. Interviewers for research-engineering and research-ML roles therefore ask you to predict shapes, explain what backward() does, and implement or check a gradient. The relevant PyTorch notes are short; this topic is about turning them into reflexes.
Core ideas
Tensors and views. A tensor is storage plus shape, strides and an offset. transpose, view, slicing and expand return views that share storage; clone and contiguous copy. Many surprises, such as an in-place write that changes another tensor or view() failing after a transpose, come from forgetting which operations alias.
Broadcasting. Shapes are aligned from the right; each pair of dimensions must be equal, or one of them must be 1 or missing. Predictions of shape (N, 1) minus targets of shape (N,) silently produce an (N, N) result whose mean is a meaningless loss. Assert shapes at module boundaries and name dimensions in comments.
The graph. When an operation involves a tensor with requires_grad set, autograd records a node (visible as grad_fn) and saves whatever the backward formula needs. Leaves are tensors you created, usually parameters; by default only leaves receive .grad, and gradients accumulate across backward calls, which is why training loops zero them and why gradient accumulation over micro-batches works. backward() walks the graph in reverse applying vector-Jacobian products and frees saved tensors unless retain_graph is set. Saved activations are the main source of training memory.
In-place operations. Every tensor has a version counter that in-place operations increment. If a tensor saved for backward changed before backward runs, autograd raises an error instead of using stale values. Writing through .data bypasses that check and can produce wrong gradients with no error at all.
Turning tracking off. torch.no_grad() stops recording, for evaluation and for parameter updates; inference_mode() is stricter and faster, but its outputs cannot later join autograd; detach() returns a view cut from the graph; model.eval() changes layer behaviour (dropout, batch-norm statistics) and does not disable gradients. Mixing these up causes "validation uses dropout" and "evaluation leaks memory" bugs.
Custom gradients. Subclass torch.autograd.Function, compute the output in forward, save what backward needs with save_for_backward, and return one gradient per input from backward (None for non-differentiable inputs). Do this only when built-in operations cannot express the computation or when you want a cheaper or more stable backward, for example recomputing an intermediate instead of storing it.
Gradient checking. Compare analytic gradients with central differences, (f(x + h) − f(x − h)) / 2h. In float32, rounding error swamps the comparison, so check in float64 on small inputs; gradcheck automates this by comparing full Jacobians. Choose test points away from kinks such as ReLU at zero.
x = torch.randn(4, 3, dtype=torch.float64, requires_grad=True)
torch.autograd.gradcheck(StableLogSumExp.apply, (x,))
Determinism. Seed Python, NumPy and torch; seed data-loader workers; disable cuDNN autotuning; and request deterministic algorithms, which raises an error for operations without a deterministic implementation. Results can still differ across PyTorch versions, hardware, and CPU versus GPU, and some fused attention backward passes are nondeterministic by default. Determinism is a debugging aid; statistical claims still need several seeds.
Common misconceptions
- "model.eval() turns off gradients." It only switches layer modes; wrap evaluation in no_grad or inference_mode as well.
- "Each backward call overwrites .grad." Gradients accumulate; forgetting to zero them silently changes the effective update.
- "If gradcheck passes in float32, the backward is right." Float32 finite differences are too noisy to be informative; check in float64, at several points, away from kinks.
- "Seeding makes runs identical everywhere." Seeds fix random streams, but nondeterministic kernels, other hardware or library versions, and data-loader ordering can still change results.
- "Editing weights through .data or detach() is harmless." Both hide the operation from autograd; changing values that were saved for backward this way can corrupt gradients without any error.
How to approach it
Build a small autograd engine first, then extend it to tensors so that broadcasting forces you to sum gradients over expanded dimensions. Reimplement two operations (log-sum-exp, softmax cross-entropy) as custom Functions and gradcheck them. In interviews, narrate shapes aloud and say where the graph is created and freed; senior candidates are expected to discuss activation memory, determinism costs and how they would test numerics in reduced precision.
Suggested order
- 1Read the PyTorch broadcasting semantics note and the tensor views page; write ten shape-prediction exercises for yourself and check them in a REPL.
- 2Read the PyTorch "Autograd mechanics" note end to end, focusing on saved tensors, locally disabling gradient computation, in-place operations and the version counter.
- 3Read micrograd’s engine.py and trace backward() by hand for a two-layer expression, then start the tensor autograd project.
- 4Read the "Extending torch.autograd" part of the Extending PyTorch note; implement a numerically stable log-sum-exp as a custom Function and verify it with gradcheck in float64.
- 5Read the PyTorch reproducibility note and the FAQ entries on memory growth and data-loader seeding; make a small training script repeat exactly on one machine and record which settings were needed.
- 6Finish the custom-Function project and, for extra depth, the CS231n assignments that derive and numerically check gradients by hand.
Step 2: Check off what you can do
Tap each one you can genuinely do or explain. Each earns 10 XP.
Step 3: Go deeper
PyTorch: Autograd mechanicsStart here
PyTorch documentationAuthoritative description of graph recording, saved tensors, in-place checks and no-grad and inference modes.
Open(opens in a new tab)Link responded, 5 Oct 2026PyTorch documentation: developer notes and reference pagesStart here
PyTorch documentationSection: Extending PyTorch: Extending torch.autograd
How to write custom Functions, save state for backward and validate gradients with gradcheck.
Open(opens in a new tab)Link responded, 5 Oct 2026PyTorch documentation: developer notes and reference pages
PyTorch documentationSection: Broadcasting semantics
Exact broadcasting rules, including in-place restrictions and backward-compatibility caveats.
Open(opens in a new tab)Link responded, 5 Oct 2026PyTorch documentation: developer notes and reference pages
PyTorch documentationSection: Reproducibility
Seeding, cuDNN benchmarking, deterministic algorithms and which backends remain nondeterministic.
Open(opens in a new tab)Link responded, 5 Oct 2026micrograd
GitHub (Andrej Karpathy)A tiny reverse-mode engine whose code you can read in one sitting before moving to tensors.
Open(opens in a new tab)Link responded, 5 Oct 2026
Step 4: Practice
Write shape assertions before writing math. Every gradient you implement gets a float64 gradcheck on random inputs plus one case small enough to compute by hand. Keep a short log of the bugs you hit (in-place error, accumulation, broadcast, detached graph) with the one-line fix; that log becomes concrete interview material.
- Tensor autograd engine with broadcasting-aware gradientsOpen(opens in a new tab)Not attemptedSelf-directed project (micrograd as the scalar reference)
- Custom autograd Functions verified with gradcheckOpen(opens in a new tab)Not attemptedSelf-directed project (PyTorch documentation)
- Stanford CS231n assignments (backprop, optimizers, normalization, CNNs)Open(opens in a new tab)Not attemptedStanford CS231n
Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).
Step 5: Test yourself
Answer each one out loud or on paper before you finish the lesson.
- Question 1What shape does a (32, 1) tensor minus a (32,) tensor produce, and why?
- Question 2Which tensors receive .grad after backward(), and how do you obtain the gradient of an intermediate tensor?
- Question 3Why does a second backward() through the same graph fail unless retain_graph is set?
- Question 4When is writing a custom backward worth the risk?
- Question 5Why use central differences and float64 for gradient checks?
- Question 6Which settings does the PyTorch reproducibility note recommend, and what do they cost?
You’re done when you can
- Predict the output shapes of ten broadcasting expressions with no mistakes.
- Explain without notes why modifying a saved tensor in place raises an error and why writing through .data hides the problem.
- Implement a custom autograd Function that passes float64 gradcheck within two attempts.
- Make a small training run repeat exactly on one machine and list the settings that were required.
- Explain the difference between no_grad, inference_mode, detach and eval() in under two minutes.
Finish the lesson
Unlocks next: Implementing attention from scratch


