You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Merge fix/autograd-topo-sort: correct silent wrong-answer bugs in autograd and quantization
backward() had no topological sort, so any reused tensor received half its
gradient; quantize_int8 clamped the zero_point and destroyed all-positive
tensors. Both returned plausible wrong numbers rather than raising. Adds the
topological sort as taught material plus a regression test, and syncs the
published listing.
Copy file name to clipboardExpand all lines: tinytorch/quarto/modules/06_autograd.qmd
+43-16Lines changed: 43 additions & 16 deletions
Original file line number
Diff line number
Diff line change
@@ -538,12 +538,12 @@ Each operation knows only its own derivative; the chain rule does the connecting
538
538
539
539
### Backward Pass Implementation
540
540
541
-
The backward pass walks the computation graph in reverse, computing gradients for every tensor it visits. Your `backward()` method does this as a recursive tree walk — short enough to read in one sitting, but enough to support arbitrarily deep networks:
541
+
The backward pass walks the computation graph in reverse, computing gradients for every tensor it visits. The order matters: a tensor can feed more than one operation, and its true gradient is the sum of what every consumer sends back, so it must not propagate until all of them have contributed. Your `backward()` method therefore sorts the graph topologically first, then makes a single pass over it:
542
542
543
543
The code in @lst-06-autograd-tensor-backward makes this concrete.
# Step 3: release the graph once, after the whole walk
594
+
ifnot retain_graph:
595
+
for tensor in topo_order:
596
+
tensor._grad_fn =None
570
597
```
571
598
572
-
: **Listing 6.3 — `Tensor.backward()`seeds the output gradient, accumulates into `.grad`, and recurses through the `_grad_fn` chain.** {#lst-06-autograd-tensor-backward}
599
+
: **Listing 6.3 — `Tensor.backward()`sorts the graph topologically, then makes one pass that accumulates into each `.grad` and hands every parent its share.** {#lst-06-autograd-tensor-backward}
573
600
574
-
For a 100-layer network, `loss.backward()`triggers 100 recursive calls — one per layer — flowing gradients from output to input. The traversal is recursive Python; the math inside each `apply()` is vectorized NumPy. That split is why the system stays both readable and fast.
601
+
For a 100-layer network, `loss.backward()`sorts 100 tensors and then visits each exactly once, flowing gradients from output to input. Sorting first is what makes that "exactly once" possible: descending into a tensor's parents the moment you reach it would re-walk any shared subtree once per consumer, which is exponential on a graph with residual connections. The traversal is Python; the math inside each `apply()` is vectorized NumPy. That split is why the system stays both readable and fast.
575
602
576
603
The `gradient` argument deserves a closer look. For scalar losses (the typical case) you call `loss.backward()` with no arguments and the method seeds the gradient to 1.0 — because `∂loss/∂loss = 1`. For non-scalar outputs you must pass the upstream gradient explicitly; there is no canonical scalar to seed from, and silently picking one would hide bugs.
0 commit comments