Seyed Masoud Hosseini · Overview · Study log · Weekly summaries · Ideas · Search · Transcript · RSS feed
Deep Learning Systems · Lecture 9 of 25 · 55:49
Lecture 8: Neural Network Library Implementation
Study guide
What this lecture covers
This is a hands-on implementation lecture that builds the modular components described in the previous lecture (modules, loss functions, optimizers) directly in needle. It first addresses a subtle but important bug: naive weight updates inside an autograd framework silently accumulate the entire computational graph across iterations, which wastes memory. It then walks through implementing Parameter, Module, a loss function, and an SGD optimizer, and closes with numerically stable softmax and a brief look at tuple-valued operators.
After watching, you can explain why weight updates must use detached tensors, implement a minimal nn.Module with recursive parameter collection, wire together a module, loss function, and optimizer into a training loop, and explain why naive softmax computation overflows for large inputs.
Key ideas
- In-place graph accumulation is a memory bug: writing
w = w - lr * gradinside a loop, without detaching, keeps extendingw's computational graph every iteration, retaining references to all previous computations and causing memory to grow linearly with iteration count. .data/.detach(): returns a new tensor sharing the same underlyingcached_databut with emptyinputsandopfields, so it is disconnected from the computational graph; needle also automatically produces detached tensors in eager mode when none of an operation's inputs require gradient.- In-place weight mutation: assigning through
w.data = ...updates a parameter's content without breaking the reference other parts of the model (such as a module's parameter list) hold to that same tensor object. - Numerical stability of softmax: because floating-point numbers have limited precision (a sign bit, mantissa, and exponent), naively computing
exp(x) / sum(exp(x))overflows for large inputs; subtracting the max value fromxbefore exponentiating keeps the computation stable without changing the mathematical result. Parameter: a subclass ofTensorwith no special behavior beyond marking a tensor as a trainable parameter, which letsModule.parameters()distinguish parameters from other tensors.Module.parameters(): recursively walks a module's__dict__, collecting anyParameter, recursing into dictionaries and lists, and recursing into any nestedModule's ownparameters()call, giving automatic parameter discovery for arbitrarily composed modules.- Loss as a module: implementing a loss (such as a simple squared-difference L2 loss) as a module lets it plug into the same forward/backward flow as any other layer.
- Optimizer interface: holds a list of parameters, exposes
reset_grad()(clears each parameter's.grad) andstep()(applies an update rule using detached data), and can keep auxiliary per-parameter state such as momentum for more advanced update rules. - Modularity payoff: swapping the hypothesis (model), the loss function, or the optimizer in the training loop each requires changing only one line, in contrast to hand-deriving gradients and writing update rules from scratch.
- Tuple-valued operators: needle's autograd mechanism extends to operators that return multiple tensors as a tuple (such as
FusedAddScalars), with gradients still propagating correctly through indexing into the tuple.
Walkthrough
The graph-accumulation bug in weight updates (2:05)
Starting from a plain weight tensor w with requires_grad=True, the lecture shows that a loop like w = w + (-lr * grad) keeps extending w's op and inputs fields every iteration, since each new w still refers back to all previous computations. Inspecting w.op, w.inputs, and nested .inputs.inputs confirms the growing chain. This is identified as a common source of out-of-memory errors when writing training loops in imperative autograd frameworks like PyTorch.
Fixing it with detached tensors (7:15)
The lecture explains that computational graphs are only needed to support reverse-mode automatic differentiation, which weight update rules themselves don't need. Calling w.data (a shortcut for w.detach(), which calls Tensor.make_const(self.realize_cached_data())) returns a new tensor sharing the same underlying cached data but with op set to None and empty inputs, so updates written as new_w = w.data - lr * grad.data no longer accumulate history. In-place mutation via w.data = ... is shown as the preferred approach, since it updates content without breaking references other structures hold to w.
Numerically stable softmax (15:22)
A naive softmax implementation (exp(x) / sum(exp(x))) works for small inputs like [0, 0, 1] but returns NaN for larger inputs like [100, 100, 101], because floating-point numbers cannot represent the resulting huge exponentials. Subtracting the maximum value from x before exponentiating is shown to leave the mathematical result unchanged (since exp(x_i - c) normalizes the same way for any constant c) while keeping all values well within representable range; the same principle applies to logsoftmax and logsumexp.
Building Parameter and Module (22:28)
Parameter is defined as a plain subclass of Tensor used only as a marker. Module.parameters() is implemented via a recursive helper, _get_params, that checks whether a value is a Parameter (return it), a dictionary (recurse over its items), or a Module (call its own .parameters()), pulling from self.__dict__. A ScaleAdd module (x * self.s + self.b, with self.s and self.b as parameters) is built and shown to return both parameters when .parameters() is called, and to compute forward outputs correctly when applied to a tensor.
Composing modules recursively (34:08)
A MultiPathScaleAdd module combining two ScaleAdd sub-modules along parallel paths (self.path0(x) + self.path1(x)) demonstrates that .parameters() automatically collects all four parameters across both sub-modules without any extra code, which is the same mechanism used to build residual modules in the homework.
Loss function, backward pass, and manual verification (37:11)
An L2Loss module computes the squared difference between a prediction and a label. Running the multi-path model on an input, computing the loss, and calling .backward() produces a gradient that the lecture verifies by hand (chaining the derivative of the squared loss through the scale-add path), confirming the gradient value matches the automatic computation.
Implementing SGD as an optimizer (41:18)
An Optimizer base holds a list of parameters and exposes reset_grad() and step(). A minimal SGD subclass stores the parameter list and learning rate, and its step() applies w.data = w.data - lr * w.grad.data for each parameter, relying on detached data to avoid graph accumulation. The lecture notes that more advanced optimizers, such as SGD with momentum, need to track additional per-parameter state (for example, a momentum tensor initialized to zero for each parameter).
End-to-end training loop (45:29)
A full loop is assembled: create input and label tensors, instantiate the multi-path model, the L2 loss, and the SGD optimizer, then for each epoch call opt.reset_grad(), run the model forward, compute the loss, call loss.backward(), and call opt.step(). The loss is shown decreasing over epochs, and the lecture emphasizes that swapping the model, loss function, or optimizer each requires changing only a single line of this loop.
Initialization variance and tensor tuples (brief) (49:52)
The lecture briefly revisits deriving the initialization variance needed to keep output variance close to input variance through a ReLU layer, matching the Kaiming-style derivation from the optimization lecture, and notes the same approach extends to uniform-distribution initialization. It closes with a short look at TensorTuple, needle's mechanism for operators (like FusedAddScalars) that return multiple tensors as a tuple, showing that automatic differentiation still works correctly when indexing into tuple-valued outputs.
Before you watch
- Complete or be familiar with the automatic differentiation homework, since this lecture assumes a working
Tensor/autograd implementation and reuses itsdetachandrealize_cached_datamechanics. - Watch the previous lecture on neural network abstractions, since this lecture implements the
nn.Module, loss function, and optimizer concepts it introduced.
Check your understanding
- Why does writing
w = w - lr * gradin a loop without detaching cause memory usage to grow with each iteration? - What does
.data(or.detach()) return, and why does using it in a weight update avoid the graph-accumulation problem? - Why does naive softmax computation produce
NaNfor large input values, and why does subtracting the maximum value fix this without changing the result? - How does
Module.parameters()find every trainable parameter in a nested module likeMultiPathScaleAddwithout being told the module's structure in advance? - What are the two core responsibilities of an
Optimizer, and why might a more advanced optimizer need to store additional state beyond the parameter list?
Vocabulary
- subtle (adjective)
- Hard to notice at first, but important.
There is a subtle bug in how weights are updated. - naive (adjective)
- Simple in a way that ignores an important problem.
A naive weight update accumulates the whole graph. - detach (verb)
- To remove a tensor's connection to its past computation history.
We detach the weight before updating it. - in-place (adjective)
- Changing data directly in memory instead of creating a new copy.
In-place mutation updates the weight's content directly. - mutation (noun)
- A change made to existing data.
The lecture shows in-place mutation of a parameter's data. - linearly (adverb)
- Growing at a steady, constant rate as something increases.
Memory use grows linearly with iteration count. - reference (noun)
- A link that points to where an object is stored in memory.
Other parts of the model hold a reference to the same tensor. - floating-point (noun)
- A computer number format used to store numbers with decimals.
Floating-point numbers have limited precision. - precision (noun)
- How exactly a number can be represented without rounding error.
Limited precision causes large numbers to overflow. - overflow (verb)
- To become too large for a computer to represent correctly.
exp(100) can overflow in floating-point arithmetic. - numerically stable (adjective)
- Describes a calculation that avoids extreme values and rounding errors.
Subtracting the max keeps softmax numerically stable. - subclass (noun)
- A class in code that builds on and extends a more general class.
Parameter is a subclass of Tensor. - marker (noun)
- Something that labels or identifies an item without changing it.
Parameter acts only as a marker on a tensor. - recursively (adverb)
- By repeating the same process on smaller parts inside a structure.
parameters() recursively collects values from nested modules. - nested (adjective)
- Placed inside another structure of the same kind.
A nested module's own parameters are collected too. - plug into (phrasal verb)
- To connect smoothly into an existing system.
A loss module plugs into the same forward/backward flow. - auxiliary (adjective)
- Extra, providing additional support to something main.
The optimizer can keep auxiliary state like momentum. - swap (verb)
- To replace one thing with another of the same kind.
You can swap the loss function with one line of code. - tuple (noun)
- A fixed group of values treated as a single item.
Some operators return a tuple of tensors. - index into (phrasal verb)
- To access one specific item inside a collection using its position.
Gradients still work when you index into the tuple. - hands-on (adjective)
- Involving direct practice rather than only theory.
This is a hands-on implementation lecture.
Chapters
- 0:00 <Untitled Chapter 1>
- 15:32 Numerical Stability
- 17:46 Compute the Soft Max
- 21:46 Design a Neural Network Library
- 22:20 Important Components of of the Neural Network
- 24:10 Module Class
- 29:25 Call Function
- 29:59 Forward Function
- 34:39 Constructor Class
- 37:36 Define Loss Functions
- 42:35 Reset Gradient
- 43:02 Step Function
- 46:09 Create an Optimizer
- 46:19 Initial Import
- 49:02 States in Sgd
From the YouTube description
This lecture provides a code review of neural network module and optimizer implementations
Sign up for the course for free at https://dlsyscourse.org.
← Lecture 7: Neural Network Abstractions · Lecture 9: Normalization and Regularization →
