<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Deepkumar Patel</title>
    <description>The latest articles on DEV Community by Deepkumar Patel (@deepkpat).</description>
    <link>https://dev.to/deepkpat</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3860532%2Ffa0e27dc-fd61-4373-a4fc-df53ce4dd246.png</url>
      <title>DEV Community: Deepkumar Patel</title>
      <link>https://dev.to/deepkpat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deepkpat"/>
    <language>en</language>
    <item>
      <title>Jacobians, Hessians, JVPs and VJPs: From First Principles to Backpropagation</title>
      <dc:creator>Deepkumar Patel</dc:creator>
      <pubDate>Thu, 29 Oct 2026 08:20:35 +0000</pubDate>
      <link>https://dev.to/deepkpat/jacobians-hessians-jvps-and-vjps-from-first-principles-to-backpropagation-16h6</link>
      <guid>https://dev.to/deepkpat/jacobians-hessians-jvps-and-vjps-from-first-principles-to-backpropagation-16h6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Note&lt;/strong&gt;: Interactive math rendering is currently being updated for this post. In the meantime, you can read the fully formatted version with rendered equations in the PDF in GitHub.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/deepkpat" rel="noopener noreferrer"&gt;
        deepkpat
      &lt;/a&gt; / &lt;a href="https://github.com/deepkpat/BackpropMath" rel="noopener noreferrer"&gt;
        BackpropMath
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Jacobians, Hessians, JVPs and VJPs: From First Principles to Backpropagation&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; someone who knows high-school differentiation and integration, and wants to understand the machinery behind how neural networks learn, without already knowing words like &lt;em&gt;perturbation&lt;/em&gt;, &lt;em&gt;tangent&lt;/em&gt;, or &lt;em&gt;cotangent&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to read it:&lt;/strong&gt; the sections build on each other. Each new idea is introduced only after the ones it needs. If a section feels fast, re-read the worked example right below it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Table of Contents&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/deepkpat/BackpropMath#1-the-big-picture-and-a-roadmap" rel="noopener noreferrer"&gt;The big picture (and a roadmap)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepkpat/BackpropMath#2-rebuilding-the-derivative-what-happens-if-i-nudge-the-input" rel="noopener noreferrer"&gt;Rebuilding the derivative: "what happens if I nudge the input?"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepkpat/BackpropMath#3-functions-with-many-inputs-and-many-outputs" rel="noopener noreferrer"&gt;Functions with many inputs and many outputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepkpat/BackpropMath#4-partial-derivatives-and-the-gradient" rel="noopener noreferrer"&gt;Partial derivatives and the gradient&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepkpat/BackpropMath#5-the-jacobian-the-derivative-of-a-vector-function" rel="noopener noreferrer"&gt;The Jacobian: the derivative of a vector function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepkpat/BackpropMath#6-the-chain-rule-becomes-matrix-multiplication" rel="noopener noreferrer"&gt;The chain rule becomes matrix multiplication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepkpat/BackpropMath#7-the-problem-jacobians-are-too-big" rel="noopener noreferrer"&gt;The problem: Jacobians are too big&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepkpat/BackpropMath#8-jvp-pushing-a-nudge-forward-the-jacobian-vector-product" rel="noopener noreferrer"&gt;JVP: pushing a nudge forward&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepkpat/BackpropMath#9-vjp-pulling-a-sensitivity-backward-the-vector-jacobian-product" rel="noopener noreferrer"&gt;VJP: pulling a sensitivity backward&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepkpat/BackpropMath#10-jvp-vs-vjp-at-a-glance" rel="noopener noreferrer"&gt;JVP vs. VJP at a glance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepkpat/BackpropMath#11-tangents-vs-cotangents-demystified" rel="noopener noreferrer"&gt;Tangents vs. cotangents, demystified&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deepkpat/BackpropMath#12-forward-mode-vs-reverse-mode-which-one-and-why" rel="noopener noreferrer"&gt;Forward mode&lt;/a&gt;…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/deepkpat/BackpropMath" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Jacobians, Hessians, JVPs and VJPs: From First Principles to Backpropagation
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; someone who knows high-school differentiation and integration, and wants to understand the machinery behind how neural networks learn, without already knowing words like &lt;em&gt;perturbation&lt;/em&gt;, &lt;em&gt;tangent&lt;/em&gt;, or &lt;em&gt;cotangent&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to read it:&lt;/strong&gt; the sections build on each other. Each new idea is introduced only after the ones it needs. If a section feels fast, re-read the worked example right below it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The big picture (and a roadmap)&lt;/li&gt;
&lt;li&gt;Rebuilding the derivative: "what happens if I nudge the input?"&lt;/li&gt;
&lt;li&gt;Functions with many inputs and many outputs&lt;/li&gt;
&lt;li&gt;Partial derivatives and the gradient&lt;/li&gt;
&lt;li&gt;The Jacobian: the derivative of a vector function&lt;/li&gt;
&lt;li&gt;The chain rule becomes matrix multiplication&lt;/li&gt;
&lt;li&gt;The problem: Jacobians are too big&lt;/li&gt;
&lt;li&gt;JVP: pushing a nudge forward&lt;/li&gt;
&lt;li&gt;VJP: pulling a sensitivity backward&lt;/li&gt;
&lt;li&gt;JVP vs. VJP at a glance&lt;/li&gt;
&lt;li&gt;Tangents vs. cotangents, demystified&lt;/li&gt;
&lt;li&gt;Forward mode vs. reverse mode: which one, and why&lt;/li&gt;
&lt;li&gt;Backpropagation is just a chain of VJPs&lt;/li&gt;
&lt;li&gt;A complete worked backprop example, by hand&lt;/li&gt;
&lt;li&gt;The Hessian: the derivative of the derivative&lt;/li&gt;
&lt;li&gt;Hessian-vector products: using curvature without building it&lt;/li&gt;
&lt;li&gt;Where all this shows up in AI&lt;/li&gt;
&lt;li&gt;Seeing it in code (JAX and PyTorch)&lt;/li&gt;
&lt;li&gt;Cheat sheet&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. The big picture (and a roadmap)
&lt;/h2&gt;

&lt;p&gt;Training a neural network means adjusting millions or billions of numbers (the &lt;strong&gt;parameters&lt;/strong&gt; or &lt;strong&gt;weights&lt;/strong&gt;) so that a single number, the &lt;strong&gt;loss&lt;/strong&gt;, becomes small. The loss measures how wrong the network currently is.&lt;/p&gt;

&lt;p&gt;To adjust the weights intelligently, we need to know: &lt;strong&gt;"If I nudge this particular weight a tiny bit, how does the loss change?"&lt;/strong&gt; That question is answered by a &lt;em&gt;derivative&lt;/em&gt;. A network has an enormous number of weights, so we need an enormous number of derivatives, computed quickly. That is the whole story of this document.&lt;/p&gt;

&lt;p&gt;Here is the cast of characters, in one sentence each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;One-sentence meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Derivative&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How much the output changes per tiny nudge of the input (one input, one output).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gradient&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;All the derivatives of a single-number output with respect to many inputs, bundled together.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jacobian&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The same idea when there are many outputs too: a matrix of &lt;em&gt;all&lt;/em&gt; the derivatives.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JVP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Jacobian times a vector": what a given nudge of the inputs does to the outputs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VJP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Vector times Jacobian": how sensitive a weighted combination of the outputs is to each input.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backpropagation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A fast way of chaining VJPs from the loss back to the weights.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hessian&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Jacobian of the gradient: how the slope itself changes, i.e. &lt;em&gt;curvature&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HVP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Hessian times a vector": curvature along a direction, computed without building the Hessian.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The path through the document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;derivative  -&amp;gt;  gradient  -&amp;gt;  Jacobian  -&amp;gt;  chain rule
                                  |
                  too big to build explicitly!
                     /                    \
                  JVP                     VJP
            (forward mode)           (reverse mode)
                                          |
                                    backpropagation

        Jacobian of the gradient  -&amp;gt;  Hessian  -&amp;gt;  HVP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Rebuilding the derivative: "what happens if I nudge the input?"
&lt;/h2&gt;

&lt;p&gt;You know that the derivative of $f(x)$ at a point $x$ is the slope of the graph there. Let's re-express this in a way that will generalize cleanly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 The word "perturbation"
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;perturbation&lt;/strong&gt; is just a small nudge or disturbance. If you're at input $x$ and you move to $x + \delta$, where $\delta$ (delta) is a tiny number like $0.001$, you have &lt;em&gt;perturbed&lt;/em&gt; the input by $\delta$.&lt;/p&gt;

&lt;p&gt;(In some texts, this tiny nudge is called a &lt;strong&gt;tangent&lt;/strong&gt; vector, because it points along a direction you could move. Keep that in mind; we'll come back to it in Section 11.)&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 The derivative as a "local linear approximation"
&lt;/h3&gt;

&lt;p&gt;Take $f(x) = x^2$ at $x = 3$. Then $f(3) = 9$.&lt;/p&gt;

&lt;p&gt;Nudge the input by $\delta = 0.01$:&lt;/p&gt;

&lt;p&gt;$$f(3.01) = 9.0601$$&lt;/p&gt;

&lt;p&gt;The output changed by $0.0601$. Compare to "derivative times nudge": $f'(3) \cdot \delta = 6 \times 0.01 = 0.06$. Very close!&lt;/p&gt;

&lt;p&gt;This is the fundamental statement of what a derivative &lt;em&gt;is&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;$$\boxed{f(x + \delta) \approx f(x) + f'(x)\,\delta \quad \text{for small } \delta}$$&lt;/p&gt;

&lt;p&gt;In words: &lt;strong&gt;near a point, the function behaves like a straight line&lt;/strong&gt;, and the derivative is the slope of that line. The smaller the nudge, the better the approximation.&lt;/p&gt;

&lt;p&gt;The "$\approx$" error shrinks &lt;em&gt;faster&lt;/em&gt; than $\delta$ does (it shrinks like $\delta^2$). That's why we trust this for tiny nudges.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 Why this viewpoint matters
&lt;/h3&gt;

&lt;p&gt;Most people first learn "derivative = slope" or "derivative = limit of a ratio." Those are fine, but for machine learning the most useful reading is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The derivative is a machine that converts an input nudge into an output nudge.&lt;/strong&gt;&lt;br&gt;
Input nudge $\delta$ goes in; output nudge $f'(x)\,\delta$ comes out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hold on to this. Everything below is this same sentence, upgraded to many dimensions.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Functions with many inputs and many outputs
&lt;/h2&gt;

&lt;p&gt;Real functions in AI take lists of numbers (vectors) in and give lists of numbers out. We write:&lt;/p&gt;

&lt;p&gt;$$f : \mathbb{R}^n \to \mathbb{R}^m$$&lt;/p&gt;

&lt;p&gt;which reads "$f$ takes a vector of $n$ real numbers and returns a vector of $m$ real numbers."&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;$n$ (inputs)&lt;/th&gt;
&lt;th&gt;$m$ (outputs)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;High-school $f(x) = x^2$&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loss as a function of weights, $L(w_1, \dots, w_n)$&lt;/td&gt;
&lt;td&gt;$n$ (maybe billions)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A neural network layer $y = Wx + b$&lt;/td&gt;
&lt;td&gt;input size&lt;/td&gt;
&lt;td&gt;output size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Position of a robot arm given joint angles&lt;/td&gt;
&lt;td&gt;number of joints&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The loss case ($m = 1$) is special and important. &lt;strong&gt;The loss is always a single number&lt;/strong&gt;, so that we can say "lower is better."&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Partial derivatives and the gradient
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Partial derivatives
&lt;/h3&gt;

&lt;p&gt;Take a function of two variables: $f(x, y) = x^2 y + y^2$.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;partial derivative&lt;/strong&gt; asks: &lt;em&gt;if I nudge only one input and freeze the others, how does the output change?&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$\dfrac{\partial f}{\partial x}$: treat $y$ as a constant, differentiate in $x$. Result: $2xy$.&lt;/li&gt;
&lt;li&gt;$\dfrac{\partial f}{\partial y}$: treat $x$ as a constant, differentiate in $y$. Result: $x^2 + 2y$.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(The curly $\partial$ simply signals "partial": other variables are held fixed.)&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 The gradient
&lt;/h3&gt;

&lt;p&gt;Stack the partial derivatives into a vector. That's the &lt;strong&gt;gradient&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;$$\nabla f(x,y) = \begin{bmatrix} \partial f/\partial x \ \partial f/\partial y \end{bmatrix} = \begin{bmatrix} 2xy \ x^2 + 2y \end{bmatrix}$$&lt;/p&gt;

&lt;p&gt;At the point $(1, 2)$: $\nabla f = (4,\ 5)$.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 The gradient and nudges (the multi-dimensional version of Section 2)
&lt;/h3&gt;

&lt;p&gt;Nudge both inputs at once by $\delta = (\delta_x, \delta_y)$. Then:&lt;/p&gt;

&lt;p&gt;$$f(\mathbf{x} + \boldsymbol\delta) \approx f(\mathbf{x}) + \nabla f(\mathbf{x})^\top \boldsymbol\delta = f(\mathbf{x}) + \frac{\partial f}{\partial x}\delta_x + \frac{\partial f}{\partial y}\delta_y$$&lt;/p&gt;

&lt;p&gt;Check at $(1,2)$ with $\delta = (0.01, -0.02)$:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exact: $f(1.01, 1.98) = (1.0201)(1.98) + (1.98)^2 = 2.019798 + 3.9204 = 5.940198$. And $f(1,2) = 2 + 4 = 6$. Change $\approx -0.0598$.&lt;/li&gt;
&lt;li&gt;Approximation: $4(0.01) + 5(-0.02) = 0.04 - 0.10 = -0.06$.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why the gradient matters for learning:&lt;/strong&gt; the nudge that &lt;em&gt;increases&lt;/em&gt; $f$ the fastest points in the direction of $\nabla f$. To &lt;em&gt;decrease&lt;/em&gt; a loss, step in the direction of $-\nabla L$. That's &lt;strong&gt;gradient descent&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;$$\mathbf{w} \leftarrow \mathbf{w} - \eta\, \nabla L(\mathbf{w})$$&lt;/p&gt;

&lt;p&gt;where $\eta$ is the &lt;strong&gt;learning rate&lt;/strong&gt; (step size).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; For a function with many inputs and &lt;em&gt;one&lt;/em&gt; output, the derivative is the gradient. Dotting it with a nudge gives the output change.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. The Jacobian: the derivative of a vector function
&lt;/h2&gt;

&lt;p&gt;Now let there be many outputs. Take&lt;/p&gt;

&lt;p&gt;$$f(x_1, x_2) = \begin{bmatrix} f_1 \ f_2 \ f_3 \end{bmatrix} = \begin{bmatrix} x_1 x_2 \ x_1 + x_2^2 \ \sin x_1 \end{bmatrix}$$&lt;/p&gt;

&lt;p&gt;Here $n = 2$ inputs and $m = 3$ outputs. Each output $f_i$ has its own gradient (a row of partial derivatives). Stack those rows and you get the &lt;strong&gt;Jacobian matrix&lt;/strong&gt; $J$:&lt;/p&gt;

&lt;p&gt;$$J = \begin{bmatrix}&lt;br&gt;
\frac{\partial f_1}{\partial x_1} &amp;amp; \frac{\partial f_1}{\partial x_2} \[4pt]&lt;br&gt;
\frac{\partial f_2}{\partial x_1} &amp;amp; \frac{\partial f_2}{\partial x_2} \[4pt]&lt;br&gt;
\frac{\partial f_3}{\partial x_1} &amp;amp; \frac{\partial f_3}{\partial x_2}&lt;br&gt;
\end{bmatrix}&lt;br&gt;
= \begin{bmatrix}&lt;br&gt;
x_2 &amp;amp; x_1 \&lt;br&gt;
1 &amp;amp; 2x_2 \&lt;br&gt;
\cos x_1 &amp;amp; 0&lt;br&gt;
\end{bmatrix}$$&lt;/p&gt;
&lt;h3&gt;
  
  
  5.1 Shape and indexing: memorize this
&lt;/h3&gt;

&lt;p&gt;For $f: \mathbb{R}^n \to \mathbb{R}^m$, the Jacobian is an &lt;strong&gt;$m \times n$&lt;/strong&gt; matrix:&lt;/p&gt;

&lt;p&gt;$$J_{ij} = \frac{\partial f_i}{\partial x_j} \qquad (\text{row } i = \text{which output},\ \text{column } j = \text{which input})$$&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rows&lt;/strong&gt; correspond to &lt;strong&gt;outputs&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Columns&lt;/strong&gt; correspond to &lt;strong&gt;inputs&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Row $i$ is the gradient of output $f_i$ (written as a row).&lt;/li&gt;
&lt;li&gt;Column $j$ tells you how &lt;em&gt;all&lt;/em&gt; outputs respond when you nudge only input $x_j$.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  5.2 The Jacobian is the derivative (same formula as before!)
&lt;/h3&gt;

&lt;p&gt;$$\boxed{f(\mathbf{x} + \boldsymbol\delta) \approx f(\mathbf{x}) + J(\mathbf{x})\,\boldsymbol\delta}$$&lt;/p&gt;

&lt;p&gt;Compare with Section 2: $f(x+\delta) \approx f(x) + f'(x)\delta$. Same sentence! The scalar $f'(x)$ became the matrix $J$. The Jacobian is &lt;strong&gt;the machine that converts an input-nudge vector into an output-nudge vector&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.3 Numeric check
&lt;/h3&gt;

&lt;p&gt;At $\mathbf{x} = (1, 2)$:&lt;/p&gt;

&lt;p&gt;$$J = \begin{bmatrix} 2 &amp;amp; 1 \ 1 &amp;amp; 4 \ \cos 1 &amp;amp; 0 \end{bmatrix} \approx \begin{bmatrix} 2 &amp;amp; 1 \ 1 &amp;amp; 4 \ 0.5403 &amp;amp; 0 \end{bmatrix}$$&lt;/p&gt;

&lt;p&gt;Nudge by $\boldsymbol\delta = (0.01, 0.00)$ (only $x_1$ moves):&lt;/p&gt;

&lt;p&gt;$$J\boldsymbol\delta = (0.02,\ 0.01,\ 0.005403)$$&lt;/p&gt;

&lt;p&gt;True values: $f(1,2) = (2,\ 5,\ 0.841471)$ and $f(1.01, 2) = (2.02,\ 5.01,\ 0.846832)$, so the actual changes are $(0.02,\ 0.01,\ 0.005361)$. The first two match exactly (those outputs are linear in $x_1$); the third ($0.005361$ vs. the predicted $0.005403$) differs slightly because $\sin$ is curved, which is the leftover error that vanishes as $\delta \to 0$.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.4 Special cases (to anchor your intuition)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;$m = 1$ (scalar output): $J$ is a single row, the gradient written horizontally ($1 \times n$).&lt;/li&gt;
&lt;li&gt;$n = m = 1$: $J$ is a $1\times1$ matrix, your old friend $f'(x)$.&lt;/li&gt;
&lt;li&gt;Linear layer $f(\mathbf{x}) = W\mathbf{x} + \mathbf{b}$: $J = W$. Always, everywhere. (A linear map's best linear approximation is itself.)&lt;/li&gt;
&lt;li&gt;Elementwise function like ReLU or $\tanh$ applied to each entry: $J$ is &lt;strong&gt;diagonal&lt;/strong&gt;, since output $i$ only depends on input $i$.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  6. The chain rule becomes matrix multiplication
&lt;/h2&gt;

&lt;p&gt;Neural networks are compositions of functions: $\mathbf{x} \to f \to g \to h \to L$. The chain rule says how to differentiate a composition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High school:&lt;/strong&gt; $\dfrac{d}{dx} g(f(x)) = g'(f(x)) \cdot f'(x)$.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now:&lt;/strong&gt; if $\mathbf{y} = f(\mathbf{x})$ and $\mathbf{z} = g(\mathbf{y})$, then&lt;/p&gt;

&lt;p&gt;$$\boxed{J_{g \circ f}(\mathbf{x}) = J_g(\mathbf{y}) \cdot J_f(\mathbf{x})}$$&lt;/p&gt;

&lt;p&gt;Matrix product, with the &lt;em&gt;later&lt;/em&gt; function's Jacobian on the &lt;strong&gt;left&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it makes sense (in nudge language):&lt;/strong&gt; a nudge $\boldsymbol\delta$ to $\mathbf{x}$ becomes a nudge $J_f \boldsymbol\delta$ to $\mathbf{y}$, which then becomes a nudge $J_g (J_f \boldsymbol\delta)$ to $\mathbf{z}$. Matrix multiplication is associative, so $J_g(J_f\boldsymbol\delta) = (J_g J_f)\boldsymbol\delta$.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shape check:&lt;/strong&gt; $f:\mathbb{R}^n\to\mathbb{R}^p$ has $J_f$ of shape $p\times n$. $g:\mathbb{R}^p\to\mathbb{R}^m$ has $J_g$ of shape $m\times p$. Product: $(m\times p)(p\times n) = m \times n$.&lt;/p&gt;

&lt;p&gt;For a deep network with layers $f_1, f_2, \dots, f_k$:&lt;/p&gt;

&lt;p&gt;$$J = J_{k} \cdot J_{k-1} \cdots J_{2} \cdot J_{1}$$&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A product of many matrices.&lt;/strong&gt; The &lt;em&gt;order in which we multiply them&lt;/em&gt; is where all the interesting computer science hides.&lt;/p&gt;


&lt;h2&gt;
  
  
  7. The problem: Jacobians are too big
&lt;/h2&gt;

&lt;p&gt;Suppose a layer maps 4,096 numbers to 4,096 numbers. Its Jacobian is $4096 \times 4096 \approx 16.8$ million entries. Fine for one layer. But now consider the Jacobian of the &lt;em&gt;loss with respect to all parameters&lt;/em&gt; of a model with $10^9$ parameters. And the Jacobian of an entire network's output (say, 50,000 vocabulary logits) with respect to $10^9$ parameters would have $5\times 10^{13}$ entries. That is hundreds of terabytes. &lt;strong&gt;We can never build the full Jacobian for real models.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But here's the key insight: &lt;strong&gt;we almost never need the Jacobian itself.&lt;/strong&gt; We only need what it &lt;em&gt;does to a vector&lt;/em&gt;. There are exactly two useful things we can do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Multiply $J$ by a vector on the &lt;strong&gt;right&lt;/strong&gt;: $J\mathbf{v}$. (JVP)&lt;/li&gt;
&lt;li&gt;Multiply $J$ by a vector on the &lt;strong&gt;left&lt;/strong&gt;: $\mathbf{u}^\top J$. (VJP)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And remarkably, both can be computed at a cost comparable to evaluating the function once, &lt;strong&gt;without ever forming $J$&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  8. JVP: pushing a nudge forward (the Jacobian-Vector Product)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  8.1 Definition
&lt;/h3&gt;

&lt;p&gt;$$\text{JVP}(f, \mathbf{x}, \mathbf{v}) = J_f(\mathbf{x})\,\mathbf{v}$$&lt;/p&gt;

&lt;p&gt;where $\mathbf{v} \in \mathbb{R}^n$ (same size as the input). The result lives in $\mathbb{R}^m$ (same size as the output).&lt;/p&gt;
&lt;h3&gt;
  
  
  8.2 What it means
&lt;/h3&gt;

&lt;p&gt;By Section 5.2: &lt;strong&gt;the JVP is the output nudge caused by an input nudge of direction $\mathbf{v}$.&lt;/strong&gt; It answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If I move my inputs a tiny bit in the direction $\mathbf{v}$, how do all my outputs move?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Equivalently, it is the &lt;strong&gt;directional derivative&lt;/strong&gt; of $f$ in direction $\mathbf{v}$:&lt;/p&gt;

&lt;p&gt;$$J\mathbf{v} = \lim_{\varepsilon \to 0} \frac{f(\mathbf{x} + \varepsilon\mathbf{v}) - f(\mathbf{x})}{\varepsilon}$$&lt;/p&gt;

&lt;p&gt;This also tells you what the JVP &lt;em&gt;is not&lt;/em&gt;: it isn't a number you could guess without the function; it's what you'd get by (infinitesimally) wiggling the inputs and watching the outputs. In fact, you could estimate it with &lt;strong&gt;finite differences&lt;/strong&gt;: pick a tiny $\varepsilon$ like $10^{-5}$, evaluate $f$ twice, subtract, divide. Automatic differentiation does the same thing &lt;em&gt;exactly&lt;/em&gt; (no tiny-$\varepsilon$ rounding errors).&lt;/p&gt;
&lt;h3&gt;
  
  
  8.3 Example
&lt;/h3&gt;

&lt;p&gt;Using the earlier function at $\mathbf{x} = (1,2)$ with $\mathbf{v} = (1, 0.5)$:&lt;/p&gt;

&lt;p&gt;$$J\mathbf{v} = \begin{bmatrix} 2 &amp;amp; 1 \ 1 &amp;amp; 4 \ 0.5403 &amp;amp; 0 \end{bmatrix}\begin{bmatrix} 1 \ 0.5 \end{bmatrix} = \begin{bmatrix} 2.5 \ 3 \ 0.5403 \end{bmatrix}$$&lt;/p&gt;

&lt;p&gt;Interpretation: moving the input along $(1, 0.5)$ changes output 1 at rate 2.5, output 2 at rate 3, output 3 at rate 0.54.&lt;/p&gt;
&lt;h3&gt;
  
  
  8.4 How it's computed without building $J$ ("forward mode")
&lt;/h3&gt;

&lt;p&gt;Walk through the computation &lt;em&gt;as you evaluate $f$&lt;/em&gt;, carrying along, with each intermediate value, how that value changes under the nudge.&lt;/p&gt;
&lt;h4&gt;
  
  
  Step 0: What do the dots ($\dot x_1$, $\dot a$, ...) mean?
&lt;/h4&gt;

&lt;p&gt;Imagine a &lt;strong&gt;slider&lt;/strong&gt; $t$ that walks you away from the starting point $(1, 2)$ in the direction $\mathbf{v} = (1, 0.5)$:&lt;/p&gt;

&lt;p&gt;$$x_1(t) = 1 + 1\cdot t, \qquad x_2(t) = 2 + 0.5\cdot t$$&lt;/p&gt;

&lt;p&gt;At $t = 0$ you are exactly at $(1, 2)$. As $t$ grows, both inputs move together: $x_1$ at speed $1$, and $x_2$ at speed $0.5$. (The slider $t$ is only a bookkeeping device; it is not part of $f$.)&lt;/p&gt;

&lt;p&gt;A dot means &lt;strong&gt;"derivative with respect to the slider $t$"&lt;/strong&gt;, that is, &lt;em&gt;how fast this quantity changes as you slide along $\mathbf{v}$&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;$$\dot x_1 = \frac{dx_1}{dt} = 1, \qquad \dot x_2 = \frac{dx_2}{dt} = 0.5$$&lt;/p&gt;

&lt;p&gt;So for the inputs, &lt;strong&gt;$\dot x_1$ and $\dot x_2$ are simply the entries of $\mathbf{v}$&lt;/strong&gt;: $\dot x_1 = v_1$ and $\dot x_2 = v_2$. They are the "speeds" of the inputs.&lt;/p&gt;

&lt;p&gt;Now the key point: once the inputs depend on $t$, &lt;strong&gt;every quantity computed from them depends on $t$ too&lt;/strong&gt;. So every intermediate value $a$ gets its own dotted partner $\dot a = da/dt$, which is its &lt;strong&gt;tangent&lt;/strong&gt;: the rate at which $a$ changes along $\mathbf{v}$. Forward mode computes each value and its tangent side by side, using the ordinary derivative rules (sum rule, product rule, chain rule) one operation at a time.&lt;/p&gt;

&lt;p&gt;For example, $f_1 = x_1 x_2$. Both factors change with $t$, so the product rule gives&lt;/p&gt;

&lt;p&gt;$$\dot f_1 = \frac{d}{dt}(x_1 x_2) = \dot x_1\,x_2 + x_1\,\dot x_2 = (1)(2) + (1)(0.5) = 2.5$$&lt;/p&gt;
&lt;h4&gt;
  
  
  The full walkthrough
&lt;/h4&gt;

&lt;p&gt;For our function with $x_1 = 1, x_2 = 2$ and $\mathbf{v} = (1, 0.5)$, so $\dot x_1 = 1$, $\dot x_2 = 0.5$:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Tangent (via ordinary calculus rules)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$f_1 = x_1 x_2$&lt;/td&gt;
&lt;td&gt;$2$&lt;/td&gt;
&lt;td&gt;$\dot x_1 x_2 + x_1 \dot x_2 = 1\cdot 2 + 1\cdot 0.5 = 2.5$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$q = x_2^2$&lt;/td&gt;
&lt;td&gt;$4$&lt;/td&gt;
&lt;td&gt;$2x_2\,\dot x_2 = 4 \cdot 0.5 = 2$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$f_2 = x_1 + q$&lt;/td&gt;
&lt;td&gt;$5$&lt;/td&gt;
&lt;td&gt;$\dot x_1 + \dot q = 1 + 2 = 3$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$f_3 = \sin x_1$&lt;/td&gt;
&lt;td&gt;$0.8415$&lt;/td&gt;
&lt;td&gt;$\cos x_1\,\dot x_1 = 0.5403$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Result: $(\dot f_1, \dot f_2, \dot f_3) = (2.5, 3, 0.5403)$, matching $J\mathbf{v}$.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why must these match $J\mathbf{v}$?&lt;/strong&gt; Along the path $\mathbf{x}(t)$, the chain rule says&lt;/p&gt;

&lt;p&gt;$$\dot f_i = \frac{df_i}{dt} = \frac{\partial f_i}{\partial x_1}\dot x_1 + \frac{\partial f_i}{\partial x_2}\dot x_2 = \sum_j J_{ij}\,v_j = (J\mathbf{v})_i$$&lt;/p&gt;

&lt;p&gt;So the vector of output tangents $(\dot f_1, \dot f_2, \dot f_3)$ &lt;em&gt;is&lt;/em&gt; $J\mathbf{v}$. "The rate of change along the path" and "the output nudge per unit of input nudge" are the same thing: a tiny step $\varepsilon$ along $\mathbf{v}$ changes the outputs by about $\varepsilon\cdot J\mathbf{v}$.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symbol&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$\mathbf{v} = (1, 0.5)$&lt;/td&gt;
&lt;td&gt;the direction you chose to move the inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\dot x_1,\ \dot x_2$&lt;/td&gt;
&lt;td&gt;how fast each input moves along that path (just $v_1, v_2$)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\dot a$ (any intermediate value)&lt;/td&gt;
&lt;td&gt;how fast that value changes along the path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$(\dot f_1, \dot f_2, \dot f_3)$&lt;/td&gt;
&lt;td&gt;the JVP, $J\mathbf{v}$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We never wrote down $J$. We only applied the simple, local derivative rules to each operation, alongside the normal calculation. The extra cost is a small constant factor (about 2 to 3 times the cost of evaluating $f$).&lt;/p&gt;

&lt;p&gt;This is the idea behind &lt;strong&gt;dual numbers&lt;/strong&gt;: numbers of the form $a + \dot a\,\varepsilon$ where $\varepsilon^2 = 0$. Do arithmetic on them as usual and the $\varepsilon$ part automatically carries the derivative.&lt;/p&gt;
&lt;h3&gt;
  
  
  8.5 What one JVP gives you, and what it costs
&lt;/h3&gt;

&lt;p&gt;One JVP gives you &lt;strong&gt;one column's worth&lt;/strong&gt; of information (the combined effect of one input direction). To recover the &lt;em&gt;entire&lt;/em&gt; Jacobian with JVPs you'd need $n$ of them: one for each basis vector $\mathbf{e}_1, \dots, \mathbf{e}_n$ (since $J\mathbf{e}_j$ is just column $j$).&lt;/p&gt;
&lt;h3&gt;
  
  
  8.6 Matrix route vs. forward-mode route: a three-layer example
&lt;/h3&gt;

&lt;p&gt;You may notice that for our small function, "multiply the Jacobian by $\mathbf{v}$" (Section 8.3) and "carry tangents through a table" (Section 8.4) do exactly the same arithmetic. That is true for a tiny function. The difference only becomes visible when the function is a &lt;strong&gt;composition&lt;/strong&gt; of several steps, as a neural network is:&lt;/p&gt;

&lt;p&gt;$$f = f_3\circ f_2\circ f_1,\qquad \mathbf{x}\xrightarrow{f_1}\mathbf{y}\xrightarrow{f_2}\mathbf{h}\xrightarrow{f_3}\mathbf{o}$$&lt;/p&gt;

&lt;p&gt;Let's use a mini network with three layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$f_1$: linear, $\mathbf{y}=W_1\mathbf{x}$ with $W_1=\begin{bmatrix}1&amp;amp;-1\2&amp;amp;1\end{bmatrix}$&lt;/li&gt;
&lt;li&gt;$f_2$: ReLU, $\mathbf{h}=\text{ReLU}(\mathbf{y})$ (applied entry by entry)&lt;/li&gt;
&lt;li&gt;$f_3$: linear, $\mathbf{o}=W_3\mathbf{h}$ with $W_3=\begin{bmatrix}1&amp;amp;0\2&amp;amp;1\0&amp;amp;3\end{bmatrix}$ (this one has 3 outputs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We evaluate at $\mathbf{x}=(1,2)$ and nudge along $\mathbf{v}=(1,\,0.5)$.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forward pass (the values):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;$$\mathbf{y}=W_1\mathbf{x}=(1-2,\ 2+2)=(-1,\ 4),\qquad \mathbf{h}=\text{ReLU}(\mathbf{y})=(0,\ 4),\qquad \mathbf{o}=W_3\mathbf{h}=(0,\ 4,\ 12)$$&lt;/p&gt;
&lt;h4&gt;
  
  
  Route A: the matrix route (build $J$ first)
&lt;/h4&gt;

&lt;p&gt;Write down the Jacobian of each layer:&lt;/p&gt;

&lt;p&gt;$$J_1=W_1=\begin{bmatrix}1&amp;amp;-1\2&amp;amp;1\end{bmatrix},\qquad&lt;br&gt;
J_2=\text{diag}\big([\,y&amp;gt;0\,]\big)=\begin{bmatrix}0&amp;amp;0\0&amp;amp;1\end{bmatrix},\qquad&lt;br&gt;
J_3=W_3=\begin{bmatrix}1&amp;amp;0\2&amp;amp;1\0&amp;amp;3\end{bmatrix}$$&lt;/p&gt;

&lt;p&gt;(The ReLU Jacobian is diagonal: it has a $1$ where the unit was active, $y_i&amp;gt;0$, and a $0$ where it was not.)&lt;/p&gt;

&lt;p&gt;Chain rule: $J=J_3J_2J_1$. Multiply the matrices together first:&lt;/p&gt;

&lt;p&gt;$$J_2J_1=\begin{bmatrix}0&amp;amp;0\2&amp;amp;1\end{bmatrix},\qquad J=J_3(J_2J_1)=\begin{bmatrix}0&amp;amp;0\2&amp;amp;1\6&amp;amp;3\end{bmatrix}$$&lt;/p&gt;

&lt;p&gt;Finally apply it to the nudge:&lt;/p&gt;

&lt;p&gt;$$J\mathbf{v}=\begin{bmatrix}0&amp;amp;0\2&amp;amp;1\6&amp;amp;3\end{bmatrix}\begin{bmatrix}1\0.5\end{bmatrix}=\begin{bmatrix}0\2.5\7.5\end{bmatrix}$$&lt;/p&gt;
&lt;h4&gt;
  
  
  Route B: the forward-mode route (carry the tangent through)
&lt;/h4&gt;

&lt;p&gt;Never build any $J_i$. Start with the tangent $\dot{\mathbf{x}}=\mathbf{v}=(1,0.5)$ and push it through each layer using only that layer's own local rule:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Local rule for the tangent&lt;/th&gt;
&lt;th&gt;Computation&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$f_1$ (linear)&lt;/td&gt;
&lt;td&gt;$\dot{\mathbf{y}}=W_1\dot{\mathbf{x}}$&lt;/td&gt;
&lt;td&gt;$(1\cdot1-1\cdot0.5,\ \ 2\cdot1+1\cdot0.5)$&lt;/td&gt;
&lt;td&gt;$\dot{\mathbf{y}}=(0.5,\ 2.5)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$f_2$ (ReLU)&lt;/td&gt;
&lt;td&gt;$\dot{\mathbf{h}}=[\,y&amp;gt;0\,]\odot\dot{\mathbf{y}}$&lt;/td&gt;
&lt;td&gt;$(0,1)\odot(0.5,\,2.5)$&lt;/td&gt;
&lt;td&gt;$\dot{\mathbf{h}}=(0,\ 2.5)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$f_3$ (linear)&lt;/td&gt;
&lt;td&gt;$\dot{\mathbf{o}}=W_3\dot{\mathbf{h}}$&lt;/td&gt;
&lt;td&gt;$(1\cdot0,\ \ 2\cdot0+1\cdot2.5,\ \ 3\cdot2.5)$&lt;/td&gt;
&lt;td&gt;$\dot{\mathbf{o}}=(0,\ 2.5,\ 7.5)$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same answer: $J\mathbf{v}=(0,\,2.5,\,7.5)$.&lt;/p&gt;
&lt;h4&gt;
  
  
  What is actually different?
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Route A multiplied two matrices together&lt;/strong&gt; ($J_2J_1$, then $J_3$ times that). &lt;strong&gt;Route B only ever multiplied a matrix by a vector&lt;/strong&gt; (or, for ReLU, just a vector by a vector, entry by entry).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route A needed all three Jacobians to exist as matrices.&lt;/strong&gt; In particular it built a diagonal matrix for ReLU. &lt;strong&gt;Route B only used each layer's local rule&lt;/strong&gt;, and for ReLU that is just "zero out the entries that were inactive." The diagonal matrix was never created.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route A needed the Jacobian of the &lt;em&gt;whole&lt;/em&gt; function&lt;/strong&gt; to be assembled before the nudge was used. &lt;strong&gt;Route B pushed the nudge through as the function was being evaluated&lt;/strong&gt;, layer by layer, so it can run right alongside the forward pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An honest note: if in Route A you &lt;em&gt;also&lt;/em&gt; chose to multiply right to left, i.e. $J_3\big(J_2(J_1\mathbf{v})\big)$, you would do the very same arithmetic as Route B. That is precisely the point: &lt;strong&gt;forward mode is "multiply right to left, one vector at a time," with each $J_i$ replaced by its cheap local rule so that it never has to be stored.&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Counting the operations in our example
&lt;/h4&gt;

&lt;p&gt;Both orders give the same answer, but they do not do the same amount of work. Let's count multiplications. Multiplying an $(a\times b)$ matrix by a $(b\times c)$ matrix takes $a\cdot b\cdot c$ multiplications, and a matrix times a vector is the special case $c=1$. Here $J_1$ is $2\times2$, $J_2$ is $2\times2$ (the ReLU diagonal, treated as an ordinary dense matrix), $J_3$ is $3\times2$, and $\mathbf{v}$ has 2 entries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order A: $(J_3J_2J_1)\,\mathbf{v}$&lt;/strong&gt; (multiply the matrices together first)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Shapes&lt;/th&gt;
&lt;th&gt;Multiplications&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$J_2J_1$&lt;/td&gt;
&lt;td&gt;$(2\times2)(2\times2)$&lt;/td&gt;
&lt;td&gt;$2\cdot2\cdot2 = 8$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$J_3(J_2J_1)$&lt;/td&gt;
&lt;td&gt;$(3\times2)(2\times2)$&lt;/td&gt;
&lt;td&gt;$3\cdot2\cdot2 = 12$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$J\mathbf{v}$&lt;/td&gt;
&lt;td&gt;$(3\times2)(2\times1)$&lt;/td&gt;
&lt;td&gt;$3\cdot2\cdot1 = 6$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Order B: $J_3\big(J_2(J_1\mathbf{v})\big)$&lt;/strong&gt; (push the vector through one matrix at a time)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Shapes&lt;/th&gt;
&lt;th&gt;Multiplications&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$J_1\mathbf{v}$&lt;/td&gt;
&lt;td&gt;$(2\times2)(2\times1)$&lt;/td&gt;
&lt;td&gt;$2\cdot2\cdot1 = 4$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$J_2(J_1\mathbf{v})$&lt;/td&gt;
&lt;td&gt;$(2\times2)(2\times1)$&lt;/td&gt;
&lt;td&gt;$2\cdot2\cdot1 = 4$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$J_3(\cdots)$&lt;/td&gt;
&lt;td&gt;$(3\times2)(2\times1)$&lt;/td&gt;
&lt;td&gt;$3\cdot2\cdot1 = 6$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same answer costs 26 multiplications in one order and 14 in the other. If we also use ReLU's local rule (an entry-by-entry multiply instead of a dense $2\times2$ product), the middle step drops from 4 to 2 and the total is 12. This is the actual forward-mode computation from the table in Route B.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the gap exists:&lt;/strong&gt; Order A contains two matrix-by-matrix products, which are the expensive operations. Order B contains only matrix-by-vector products. The final step of Order A, $J\mathbf{v}$, is also matrix-by-vector, which is why the 6 appears in both tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the gap grows.&lt;/strong&gt; Suppose every layer has width $d$ (our example has a 3-output last layer, so its counts differ slightly):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Order A&lt;/th&gt;
&lt;th&gt;Order B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;matrix-by-matrix products&lt;/td&gt;
&lt;td&gt;2, each costing $d^3$&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;matrix-by-vector products&lt;/td&gt;
&lt;td&gt;1, costing $d^2$&lt;/td&gt;
&lt;td&gt;3, each costing $d^2$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$2d^3 + d^2$&lt;/td&gt;
&lt;td&gt;$3d^2$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At $d=2$ these are close (about 20 vs 12), which is why our example makes the two orders look nearly the same. At $d=4096$:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Order A: about $1.4\times10^{11}$ multiplications.&lt;/li&gt;
&lt;li&gt;Order B: about $5\times10^{7}$ multiplications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a gap of roughly 2,700 times, and it keeps growing in proportion to $d$. With more layers, Order A adds another $d^3$ matrix-by-matrix product for every extra layer, while Order B adds only another $d^2$.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;So the reordering saves time, not just memory.&lt;/strong&gt; The saving comes from avoiding matrix-by-matrix products entirely, and that is a separate benefit from never storing the $J_i$ matrices.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  Why it matters at real sizes
&lt;/h4&gt;

&lt;p&gt;Suppose each layer has width $d$ (inputs and outputs of size $d$) and the network has $L$ layers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Route A (build matrices, multiply them)&lt;/th&gt;
&lt;th&gt;Route B (forward-mode tangents)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multiplying two Jacobians&lt;/td&gt;
&lt;td&gt;about $d^3$ operations each time&lt;/td&gt;
&lt;td&gt;never done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Applying a linear layer&lt;/td&gt;
&lt;td&gt;(part of the matrix product)&lt;/td&gt;
&lt;td&gt;$d^2$ (matrix times vector)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Applying an elementwise layer (e.g. ReLU)&lt;/td&gt;
&lt;td&gt;stores a $d\times d$ diagonal matrix&lt;/td&gt;
&lt;td&gt;$d$ (entry-wise multiply)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total time&lt;/td&gt;
&lt;td&gt;about $L\,d^3$&lt;/td&gt;
&lt;td&gt;about $L\,d^2$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra memory&lt;/td&gt;
&lt;td&gt;$d^2$ per Jacobian&lt;/td&gt;
&lt;td&gt;$d$ per tangent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For $d=4096$ the time difference is a factor of about 4000, and each ReLU layer would otherwise cost a 16.8-million-entry matrix to hold something that is really a single list of 4096 zeros and ones.&lt;/p&gt;

&lt;p&gt;So the two routes are the same mathematics, but Route B is the one that can be run on huge networks, and it is the one automatic differentiation uses.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; JVP = "forward propagation of a nudge." Cost ≈ one function evaluation. Gives you $J\mathbf{v}$, an &lt;em&gt;output-sized&lt;/em&gt; vector.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  9. VJP: pulling a sensitivity backward (the Vector-Jacobian Product)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  9.1 Definition
&lt;/h3&gt;

&lt;p&gt;$$\text{VJP}(f, \mathbf{x}, \mathbf{u}) = \mathbf{u}^\top J_f(\mathbf{x})$$&lt;/p&gt;

&lt;p&gt;where $\mathbf{u} \in \mathbb{R}^m$ (same size as the &lt;em&gt;output&lt;/em&gt;). The result is a row vector of length $n$ (same size as the &lt;em&gt;input&lt;/em&gt;). Many libraries return it as a column vector, i.e. $J^\top \mathbf{u}$. These are the same numbers in a different layout.&lt;/p&gt;

&lt;p&gt;Notice the sizes flip compared to the JVP: &lt;strong&gt;the JVP takes an input-sized vector and returns an output-sized one. The VJP takes an output-sized vector and returns an input-sized one.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  9.2 What it means
&lt;/h3&gt;

&lt;p&gt;Think of $\mathbf{u}$ as a set of &lt;strong&gt;weights saying how much you care about each output&lt;/strong&gt;. Define a single scalar "score":&lt;/p&gt;

&lt;p&gt;$$s = \mathbf{u}^\top f(\mathbf{x}) = u_1 f_1 + u_2 f_2 + \dots + u_m f_m$$&lt;/p&gt;

&lt;p&gt;Differentiate $s$ with respect to the inputs. By the chain rule, you get exactly $\mathbf{u}^\top J$. So:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The VJP is the gradient (with respect to the inputs) of the weighted sum of outputs, where $\mathbf{u}$ provides the weights.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It answers: &lt;em&gt;"If I care about the outputs in proportion to $\mathbf{u}$, how sensitive is that to each input?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;How to read the result.&lt;/strong&gt; The VJP is a vector with one entry per input. Entry $j$ means:&lt;/p&gt;

&lt;p&gt;$$\text{nudge } x_j \text{ by a tiny } \varepsilon \;\Longrightarrow\; s \text{ changes by about } \varepsilon\cdot(\mathbf{u}^\top J)_j$$&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;large&lt;/strong&gt; entry means the score is very sensitive to that input.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;negative&lt;/strong&gt; entry means increasing that input &lt;em&gt;decreases&lt;/em&gt; the score.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;zero&lt;/strong&gt; entry means that input has (locally) no influence on the score.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;JVP and VJP ask opposite questions:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;th&gt;You choose&lt;/th&gt;
&lt;th&gt;You get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JVP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"I move the inputs this way. What happens to &lt;em&gt;every output&lt;/em&gt;?"&lt;/td&gt;
&lt;td&gt;a direction $\mathbf{v}$ for the inputs&lt;/td&gt;
&lt;td&gt;the change in each output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VJP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"I care about the outputs this much. Which &lt;em&gt;inputs&lt;/em&gt; are responsible?"&lt;/td&gt;
&lt;td&gt;weights $\mathbf{u}$ for the outputs&lt;/td&gt;
&lt;td&gt;the sensitivity of the score to each input&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note that the JVP does not need a score. It reports what happens to all $m$ outputs for one chosen input direction. The VJP &lt;em&gt;collapses&lt;/em&gt; the $m$ outputs into one score first, and then reports what every input does to that single number. That collapse is what makes it so cheap when many inputs feed one loss.&lt;/p&gt;
&lt;h3&gt;
  
  
  9.3 Example
&lt;/h3&gt;

&lt;p&gt;Same function, $\mathbf{x} = (1,2)$.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With $\mathbf{u} = (1, 0, 0)$: you only care about output 1. $\mathbf{u}^\top J$ = row 1 of $J$ = $(2, 1)$. (That's just $\nabla f_1$.)&lt;/li&gt;
&lt;li&gt;With $\mathbf{u} = (1, 1, 1)$: you care about the sum of outputs. $\mathbf{u}^\top J$ = sum of the rows = $(2+1+0.5403,\ 1+4+0) = (3.5403,\ 5)$.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Check the $\mathbf{u}=(1,1,1)$ case with actual numbers.&lt;/strong&gt; The score is $s = f_1+f_2+f_3$, and at $(1,2)$ it equals $2+5+0.841471 = 7.841471$.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nudge $x_1$ from $1$ to $1.01$: $f = (2.02,\ 5.01,\ 0.846832)$, so $s = 7.876832$. The change is $0.035361$, which is about $3.536\times0.01$. This matches the first VJP entry $3.5403$. ✓&lt;/li&gt;
&lt;li&gt;Nudge $x_2$ from $2$ to $2.01$: $f = (2.01,\ 5.0401,\ 0.841471)$, so $s = 7.891571$. The change is $0.05010$, which is about $5.01\times0.01$. This matches the second VJP entry $5$. ✓&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(The small mismatches are the usual curvature leftovers, which shrink as the nudge shrinks.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notice:&lt;/strong&gt; choosing $\mathbf{u}$ as a basis vector $\mathbf{e}_i$ extracts &lt;em&gt;row $i$&lt;/em&gt; of the Jacobian. So to recover the entire Jacobian with VJPs, you need $m$ of them (one per output).&lt;/p&gt;
&lt;h3&gt;
  
  
  9.4 How it's computed without building $J$ ("reverse mode")
&lt;/h3&gt;

&lt;p&gt;Reverse mode works in two phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Forward pass:&lt;/strong&gt; evaluate $f$ normally, &lt;em&gt;recording&lt;/em&gt; the intermediate values (they'll be needed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backward pass:&lt;/strong&gt; start with $\mathbf{u}$ at the output and flow backward through each operation. At each operation, apply that operation's local "transposed" derivative rule.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each intermediate value $a$, we track its &lt;strong&gt;adjoint&lt;/strong&gt; $\bar a$ (read "a-bar"): &lt;em&gt;how much the final weighted score $s$ changes per unit change in $a$&lt;/em&gt;. That is, $\bar a = \partial s/\partial a$.&lt;/p&gt;

&lt;p&gt;The recipe at a single operation $\mathbf{b} = g(\mathbf{a})$: if you know $\bar{\mathbf{b}}$ (sensitivity of the score to $\mathbf{b}$), then&lt;/p&gt;

&lt;p&gt;$$\bar{\mathbf{a}} = J_g(\mathbf{a})^\top\,\bar{\mathbf{b}}$$&lt;/p&gt;

&lt;p&gt;and if a value is used in multiple places, &lt;strong&gt;its adjoints add up&lt;/strong&gt; (that's the multivariable chain rule: contributions from every path are summed).&lt;/p&gt;

&lt;p&gt;Let's run it on our function with $\mathbf{u} = (1,1,1)$ at $\mathbf{x} = (1,2)$. Start: $\bar f_1 = \bar f_2 = \bar f_3 = 1$.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step (reverse order)&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$f_3 = \sin x_1$&lt;/td&gt;
&lt;td&gt;$\bar x_1 \mathrel{+}= \cos(x_1)\,\bar f_3$&lt;/td&gt;
&lt;td&gt;$\bar x_1 = 0.5403$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$f_2 = x_1 + q$&lt;/td&gt;
&lt;td&gt;$\bar x_1 \mathrel{+}= \bar f_2$, $\ \bar q = \bar f_2$&lt;/td&gt;
&lt;td&gt;$\bar x_1 = 1.5403$, $\bar q = 1$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$q = x_2^2$&lt;/td&gt;
&lt;td&gt;$\bar x_2 \mathrel{+}= 2x_2\,\bar q$&lt;/td&gt;
&lt;td&gt;$\bar x_2 = 4$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$f_1 = x_1 x_2$&lt;/td&gt;
&lt;td&gt;$\bar x_1 \mathrel{+}= x_2 \bar f_1$, $\ \bar x_2 \mathrel{+}= x_1\bar f_1$&lt;/td&gt;
&lt;td&gt;$\bar x_1 = 3.5403$, $\bar x_2 = 5$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Final: $(\bar x_1, \bar x_2) = (3.5403, 5)$, matching $\mathbf{u}^\top J$. Again: no Jacobian was ever built.&lt;/p&gt;
&lt;h3&gt;
  
  
  9.5 What do the bars ($\bar x_1$, $\bar a$, ...) mean?
&lt;/h3&gt;

&lt;p&gt;This is the mirror image of the dots in Section 8.4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dot ($\dot a$)&lt;/strong&gt; answers: &lt;em&gt;"if the inputs move along $\mathbf{v}$, how fast does $a$ change?"&lt;/em&gt; It looks &lt;strong&gt;back toward the inputs&lt;/strong&gt;: it depends on everything that came &lt;em&gt;before&lt;/em&gt; $a$.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bar ($\bar a$)&lt;/strong&gt; answers: &lt;em&gt;"if I wiggle $a$ by a tiny amount, and let everything *after&lt;/em&gt; $a$ react, how much does the final score $s$ change?"* It looks &lt;strong&gt;ahead toward the output&lt;/strong&gt;: it depends on everything that comes &lt;em&gt;after&lt;/em&gt; $a$.&lt;/p&gt;

&lt;p&gt;$$\bar a = \frac{\partial s}{\partial a}$$&lt;/p&gt;

&lt;p&gt;Two consequences follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The starting point is known immediately.&lt;/strong&gt; At the very end, the score is $s=u_1f_1+\dots+u_mf_m$, so $\partial s/\partial f_i = u_i$. That is, $\bar f_i = u_i$. The backward pass starts from the weights you chose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;To get $\bar a$ you need to know everything that happens after $a$&lt;/strong&gt;, which is why the pass has to run from the output back to the input. In forward mode you only need what happened &lt;em&gt;before&lt;/em&gt; a value, so it can run alongside the normal evaluation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why adjoints add up (fan-out).&lt;/strong&gt; In our function, $x_1$ is used in &lt;em&gt;three&lt;/em&gt; places: in $f_1 = x_1x_2$, in $f_2 = x_1 + q$, and in $f_3 = \sin x_1$. Wiggling $x_1$ disturbs all three outputs, and $s$ feels all three disturbances. So the effects add:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path from $x_1$ to $s$&lt;/th&gt;
&lt;th&gt;Contribution to $\bar x_1$&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;through $f_1 = x_1 x_2$&lt;/td&gt;
&lt;td&gt;$x_2\cdot\bar f_1 = 2\cdot1 = 2$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;through $f_2 = x_1 + q$&lt;/td&gt;
&lt;td&gt;$1\cdot\bar f_2 = 1$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;through $f_3 = \sin x_1$&lt;/td&gt;
&lt;td&gt;$\cos(x_1)\cdot\bar f_3 = 0.5403$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$\bar x_1 = 3.5403$ ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Likewise $x_2$ is used in $f_1$ (contribution $x_1\bar f_1 = 1$) and in $q=x_2^2$ (contribution $2x_2\,\bar q = 4$), giving $\bar x_2 = 5$ ✓. That is exactly why the table in 9.4 uses "$\mathrel{+}=$": each use of a value &lt;em&gt;adds&lt;/em&gt; its contribution into that value's adjoint.&lt;/p&gt;
&lt;h3&gt;
  
  
  9.6 Matrix route vs. reverse-mode route: the same three-layer example
&lt;/h3&gt;

&lt;p&gt;Let's reuse the mini network from Section 8.6 so you can compare JVP and VJP on identical ground:&lt;/p&gt;

&lt;p&gt;$$\mathbf{x}\xrightarrow{\ f_1:\ \mathbf{y}=W_1\mathbf{x}\ }\mathbf{y}\xrightarrow{\ f_2:\ \mathbf{h}=\text{ReLU}(\mathbf{y})\ }\mathbf{h}\xrightarrow{\ f_3:\ \mathbf{o}=W_3\mathbf{h}\ }\mathbf{o}$$&lt;/p&gt;

&lt;p&gt;with $W_1=\begin{bmatrix}1&amp;amp;-1\2&amp;amp;1\end{bmatrix}$, $W_3=\begin{bmatrix}1&amp;amp;0\2&amp;amp;1\0&amp;amp;3\end{bmatrix}$, input $\mathbf{x}=(1,2)$, so that $\mathbf{y}=(-1,4)$, $\mathbf{h}=(0,4)$, $\mathbf{o}=(0,4,12)$. We choose $\mathbf{u}=(1,1,1)$, so the score is $s=o_1+o_2+o_3 = 16$.&lt;/p&gt;
&lt;h4&gt;
  
  
  Route A: the matrix route (build $J$ first)
&lt;/h4&gt;

&lt;p&gt;From Section 8.6 we already have the full Jacobian:&lt;/p&gt;

&lt;p&gt;$$J=J_3J_2J_1=\begin{bmatrix}0&amp;amp;0\2&amp;amp;1\6&amp;amp;3\end{bmatrix}$$&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;$$\mathbf{u}^\top J=\begin{bmatrix}1&amp;amp;1&amp;amp;1\end{bmatrix}\begin{bmatrix}0&amp;amp;0\2&amp;amp;1\6&amp;amp;3\end{bmatrix}=(8,\ 4)$$&lt;/p&gt;
&lt;h4&gt;
  
  
  Route B: the reverse-mode route (carry the cotangent backward)
&lt;/h4&gt;

&lt;p&gt;Never build any $J_i$. Start with the cotangent $\bar{\mathbf{o}}=\mathbf{u}=(1,1,1)$ and pull it backward through each layer using only that layer's local rule. Recall from 9.4 that the rule is $\bar{\mathbf{a}}=J_g^\top\bar{\mathbf{b}}$, which for each layer type gives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer (in reverse order)&lt;/th&gt;
&lt;th&gt;Local rule for the cotangent&lt;/th&gt;
&lt;th&gt;Computation&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$f_3$ (linear)&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{h}}=W_3^\top\bar{\mathbf{o}}$&lt;/td&gt;
&lt;td&gt;$(1\cdot1+2\cdot1+0\cdot1,\ \ 0\cdot1+1\cdot1+3\cdot1)$&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{h}}=(3,\ 4)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$f_2$ (ReLU)&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{y}}=[\,y&amp;gt;0\,]\odot\bar{\mathbf{h}}$&lt;/td&gt;
&lt;td&gt;$(0,1)\odot(3,\,4)$&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{y}}=(0,\ 4)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$f_1$ (linear)&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{x}}=W_1^\top\bar{\mathbf{y}}$&lt;/td&gt;
&lt;td&gt;$\begin{bmatrix}1&amp;amp;2\-1&amp;amp;1\end{bmatrix}\begin{bmatrix}0\4\end{bmatrix}$&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{x}}=(8,\ 4)$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same answer: $\mathbf{u}^\top J=(8,4)$.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus: the parameter gradients come out for free.&lt;/strong&gt; At each linear layer, the cotangent that arrives is &lt;em&gt;also&lt;/em&gt; all you need for that layer's weights (Section 13.3: $\bar W=\bar{\mathbf{out}}\,\mathbf{in}^\top$):&lt;/p&gt;

&lt;p&gt;$$\bar W_3=\bar{\mathbf{o}}\,\mathbf{h}^\top=\begin{bmatrix}1\1\1\end{bmatrix}\begin{bmatrix}0&amp;amp;4\end{bmatrix}=\begin{bmatrix}0&amp;amp;4\0&amp;amp;4\0&amp;amp;4\end{bmatrix},\qquad \bar W_1=\bar{\mathbf{y}}\,\mathbf{x}^\top=\begin{bmatrix}0\4\end{bmatrix}\begin{bmatrix}1&amp;amp;2\end{bmatrix}=\begin{bmatrix}0&amp;amp;0\4&amp;amp;8\end{bmatrix}$$&lt;/p&gt;

&lt;p&gt;One backward pass delivered the sensitivity of $s$ to the input &lt;em&gt;and&lt;/em&gt; to every weight. This is what training uses.&lt;/p&gt;
&lt;h4&gt;
  
  
  What is actually different?
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Route A multiplied matrices together&lt;/strong&gt; ($J_2J_1$, then $J_3$ times that). &lt;strong&gt;Route B only multiplied a vector by a matrix&lt;/strong&gt; (or, for ReLU, a vector by a vector, entry by entry).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route A needed every Jacobian to exist as a matrix&lt;/strong&gt;, including a diagonal one for ReLU. &lt;strong&gt;Route B used only local rules.&lt;/strong&gt; For ReLU the local rule is "zero out the entries that were inactive."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direction.&lt;/strong&gt; In Route B the vector meets $J_3$ first, then $J_2$, then $J_1$. That is the &lt;em&gt;opposite&lt;/em&gt; order from the forward pass, and that is why it is called the backward pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route B needs the forward pass's values.&lt;/strong&gt; To apply the ReLU rule we need the mask $[\,y&amp;gt;0\,]$, which comes from $\mathbf{y}=(-1,4)$ computed earlier. Forward mode did not have this requirement, because it computed the mask on the fly and could throw it away. In reverse mode the forward pass must happen &lt;em&gt;first&lt;/em&gt;, and its intermediate values must be kept until the backward pass uses them. This is the memory cost of reverse mode (Section 12).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As in Section 8.6, there is an honest note: if Route A multiplied left to right, $\big((\mathbf{u}^\top J_3)J_2\big)J_1$, it would do exactly the same arithmetic as Route B. Reverse mode is "multiply left to right, one vector at a time," with each $J_i$ replaced by its cheap local rule so it never has to be stored.&lt;/p&gt;
&lt;h4&gt;
  
  
  Counting the operations in our example
&lt;/h4&gt;

&lt;p&gt;A $(1\times a)$ vector times an $(a\times b)$ matrix takes $a\cdot b$ multiplications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order A: $\mathbf{u}^\top(J_3J_2J_1)$&lt;/strong&gt; (multiply the matrices first)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Shapes&lt;/th&gt;
&lt;th&gt;Multiplications&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$J_2J_1$&lt;/td&gt;
&lt;td&gt;$(2\times2)(2\times2)$&lt;/td&gt;
&lt;td&gt;$8$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$J_3(J_2J_1)$&lt;/td&gt;
&lt;td&gt;$(3\times2)(2\times2)$&lt;/td&gt;
&lt;td&gt;$12$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\mathbf{u}^\top J$&lt;/td&gt;
&lt;td&gt;$(1\times3)(3\times2)$&lt;/td&gt;
&lt;td&gt;$6$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Order B: $\big((\mathbf{u}^\top J_3)J_2\big)J_1$&lt;/strong&gt; (push the vector through one matrix at a time)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Shapes&lt;/th&gt;
&lt;th&gt;Multiplications&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$\mathbf{u}^\top J_3$&lt;/td&gt;
&lt;td&gt;$(1\times3)(3\times2)$&lt;/td&gt;
&lt;td&gt;$6$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$(\cdot)\,J_2$&lt;/td&gt;
&lt;td&gt;$(1\times2)(2\times2)$&lt;/td&gt;
&lt;td&gt;$4$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$(\cdot)\,J_1$&lt;/td&gt;
&lt;td&gt;$(1\times2)(2\times2)$&lt;/td&gt;
&lt;td&gt;$4$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(With ReLU's local rule, the middle step becomes an entry-by-entry multiply costing $2$, so the total is $12$.)&lt;/p&gt;

&lt;p&gt;These are the same counts as in the JVP case, for the same reason: the expensive part of Order A is the two matrix-by-matrix products. With every layer of width $d$, Order A costs about $2d^3+d^2$ and Order B about $3d^2$, so the same reasoning as in Section 8.6 applies. At $d=4096$ the gap is again about 2,700 times.&lt;/p&gt;
&lt;h3&gt;
  
  
  9.7 What one VJP gives you, and what it costs
&lt;/h3&gt;

&lt;p&gt;This mirrors Section 8.5. Take the full Jacobian of our three-layer network:&lt;/p&gt;

&lt;p&gt;$$J=\begin{bmatrix}0&amp;amp;0\2&amp;amp;1\6&amp;amp;3\end{bmatrix}\qquad(3\text{ outputs},\ 2\text{ inputs})$$&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A JVP reads one column.&lt;/strong&gt; With $\mathbf{v}=\mathbf{e}_1$ you get $(0,2,6)$, and with $\mathbf{v}=\mathbf{e}_2$ you get $(0,1,3)$. The full $J$ needs &lt;strong&gt;$n=2$&lt;/strong&gt; JVPs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A VJP reads one row.&lt;/strong&gt; With $\mathbf{u}=\mathbf{e}_1$ you get $(0,0)$, with $\mathbf{u}=\mathbf{e}_2$ you get $(2,1)$, and with $\mathbf{u}=\mathbf{e}_3$ you get $(6,3)$. The full $J$ needs &lt;strong&gt;$m=3$&lt;/strong&gt; VJPs.&lt;/p&gt;

&lt;p&gt;Here the two counts are similar, so neither mode wins. Now look at the case that matters for training. A loss has $m=1$ output:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Inputs $n$&lt;/th&gt;
&lt;th&gt;Outputs $m$&lt;/th&gt;
&lt;th&gt;JVPs for the full gradient&lt;/th&gt;
&lt;th&gt;VJPs for the full gradient&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Our small network&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A loss over $10^9$ parameters&lt;/td&gt;
&lt;td&gt;$10^9$&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;$10^9$&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Jacobian of a loss is a single row, and the VJP reads a whole row in one pass. Each backward pass costs a small multiple (about 2 to 3 times) of one forward pass, regardless of how many parameters there are.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Takeaway so far:&lt;/strong&gt; a JVP reads one &lt;em&gt;column&lt;/em&gt; of $J$ per pass, a VJP reads one &lt;em&gt;row&lt;/em&gt; of $J$ per pass. Pick the mode that needs fewer passes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  9.8 The magic property
&lt;/h3&gt;

&lt;p&gt;For a &lt;strong&gt;scalar-output&lt;/strong&gt; function ($m=1$), choose $u = 1$. A &lt;em&gt;single&lt;/em&gt; backward pass gives you the &lt;strong&gt;entire gradient&lt;/strong&gt; with respect to &lt;em&gt;all $n$ inputs&lt;/em&gt;, no matter how huge $n$ is, at a cost of only a few times the forward pass.&lt;/p&gt;

&lt;p&gt;That is the miracle behind training billion-parameter models.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; VJP = "backward propagation of a sensitivity." Cost ≈ a small multiple of one function evaluation. Gives you $\mathbf{u}^\top J$, an &lt;em&gt;input-sized&lt;/em&gt; vector.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  10. JVP vs. VJP at a glance
&lt;/h2&gt;

&lt;p&gt;This section pulls Sections 8 and 9 together. If you only re-read one part of the guide later, make it this one.&lt;/p&gt;
&lt;h3&gt;
  
  
  In one sentence each
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JVP&lt;/strong&gt; ($J\mathbf{v}$): &lt;em&gt;"I nudge the inputs in direction $\mathbf{v}$. How does every output move?"&lt;/em&gt; It pushes a nudge &lt;strong&gt;forward&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VJP&lt;/strong&gt; ($\mathbf{u}^\top J$): &lt;em&gt;"I weight the outputs by $\mathbf{u}$ to form one score. How sensitive is that score to every input?"&lt;/em&gt; It pulls a sensitivity &lt;strong&gt;backward&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Side-by-side comparison
&lt;/h3&gt;

&lt;p&gt;For $f:\mathbb{R}^n\to\mathbb{R}^m$ with Jacobian $J$ of shape $m\times n$:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;JVP&lt;/strong&gt; (forward mode)&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;VJP&lt;/strong&gt; (reverse mode)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Formula&lt;/td&gt;
&lt;td&gt;$J\mathbf{v}$&lt;/td&gt;
&lt;td&gt;$\mathbf{u}^\top J$ (or $J^\top\mathbf{u}$)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You supply&lt;/td&gt;
&lt;td&gt;$\mathbf{v}\in\mathbb{R}^n$ (input-sized)&lt;/td&gt;
&lt;td&gt;$\mathbf{u}\in\mathbb{R}^m$ (output-sized)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You get&lt;/td&gt;
&lt;td&gt;a vector in $\mathbb{R}^m$ (output-sized)&lt;/td&gt;
&lt;td&gt;a vector in $\mathbb{R}^n$ (input-sized)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the input vector means&lt;/td&gt;
&lt;td&gt;a &lt;em&gt;direction&lt;/em&gt; to move the inputs&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;weights&lt;/em&gt; for combining the outputs into a score&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the result means&lt;/td&gt;
&lt;td&gt;change in each output&lt;/td&gt;
&lt;td&gt;sensitivity of the score to each input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Name of the vectors carried&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;tangents&lt;/strong&gt; ($\dot a$)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;cotangents / adjoints&lt;/strong&gt; ($\bar a$)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direction of the computation&lt;/td&gt;
&lt;td&gt;same as the forward pass (input $\to$ output)&lt;/td&gt;
&lt;td&gt;opposite to the forward pass (output $\to$ input)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local rule per layer&lt;/td&gt;
&lt;td&gt;multiply the tangent by $J_i$&lt;/td&gt;
&lt;td&gt;multiply the cotangent by $J_i^\top$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linear layer $W$&lt;/td&gt;
&lt;td&gt;$\dot{\mathbf{out}}=W\,\dot{\mathbf{in}}$&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{in}}=W^\top\bar{\mathbf{out}}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ReLU&lt;/td&gt;
&lt;td&gt;$\dot{\mathbf{h}}=\text{mask}\odot\dot{\mathbf{y}}$&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{y}}=\text{mask}\odot\bar{\mathbf{h}}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One pass reads&lt;/td&gt;
&lt;td&gt;one &lt;strong&gt;column&lt;/strong&gt; of $J$ (with $\mathbf{v}=\mathbf{e}_j$)&lt;/td&gt;
&lt;td&gt;one &lt;strong&gt;row&lt;/strong&gt; of $J$ (with $\mathbf{u}=\mathbf{e}_i$)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passes for the full $J$&lt;/td&gt;
&lt;td&gt;$n$&lt;/td&gt;
&lt;td&gt;$m$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost of one pass&lt;/td&gt;
&lt;td&gt;about 2 to 3 times one evaluation&lt;/td&gt;
&lt;td&gt;about 2 to 3 times one evaluation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Must store forward values?&lt;/td&gt;
&lt;td&gt;no (computed on the fly)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;yes&lt;/strong&gt; (the backward pass needs them)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best when&lt;/td&gt;
&lt;td&gt;few inputs, many outputs ($n\ll m$)&lt;/td&gt;
&lt;td&gt;many inputs, few outputs ($n\gg m$), such as a loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In machine learning&lt;/td&gt;
&lt;td&gt;directional derivatives, curvature products, sensitivity analysis&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;backpropagation&lt;/strong&gt; and every gradient used in training&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  The same network, both ways
&lt;/h3&gt;

&lt;p&gt;Using the three-layer network from Sections 8.6 and 9.6 at $\mathbf{x}=(1,2)$, where $J=\begin{bmatrix}0&amp;amp;0\2&amp;amp;1\6&amp;amp;3\end{bmatrix}$:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forward pass (values):   x=(1,2) --f1--&amp;gt; y=(-1,4) --f2--&amp;gt; h=(0,4) --f3--&amp;gt; o=(0,4,12)

JVP, v=(1, 0.5):         (1, 0.5) --W1--&amp;gt; (0.5, 2.5) --mask--&amp;gt; (0, 2.5) --W3--&amp;gt; (0, 2.5, 7.5)
                          tangents flow WITH the forward pass            result = J v

VJP, u=(1, 1, 1):        (8, 4) &amp;lt;--W1^T-- (0, 4) &amp;lt;--mask-- (3, 4) &amp;lt;--W3^T-- (1, 1, 1)
                          cotangents flow AGAINST the forward pass       result = u^T J
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Input vector&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Reading&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JVP&lt;/td&gt;
&lt;td&gt;$\mathbf{v}=(1,\,0.5)$&lt;/td&gt;
&lt;td&gt;$J\mathbf{v}=(0,\ 2.5,\ 7.5)$&lt;/td&gt;
&lt;td&gt;the outputs move at rates $0,\ 2.5,\ 7.5$ along that nudge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VJP&lt;/td&gt;
&lt;td&gt;$\mathbf{u}=(1,\,1,\,1)$&lt;/td&gt;
&lt;td&gt;$\mathbf{u}^\top J=(8,\ 4)$&lt;/td&gt;
&lt;td&gt;the score $s=o_1+o_2+o_3$ changes by $8\varepsilon$ per $\varepsilon$ nudge of $x_1$ and $4\varepsilon$ per $\varepsilon$ nudge of $x_2$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The link between them
&lt;/h3&gt;

&lt;p&gt;Both answer the same underlying question from opposite ends, &lt;em&gt;"how much does the score change when I nudge the inputs along $\mathbf{v}$?"&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;$$\mathbf{u}^\top(J\mathbf{v}) \;=\; (\mathbf{u}^\top J)\,\mathbf{v}$$&lt;/p&gt;

&lt;p&gt;With our numbers: $(1,1,1)\cdot(0,2.5,7.5)=10$ and $(8,4)\cdot(1,0.5)=10$. ✓ The JVP finds it by pushing one specific nudge forward. The VJP finds the sensitivity to &lt;em&gt;every&lt;/em&gt; input at once, which you can then dot with any nudge you like.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to choose
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Count the sizes.&lt;/strong&gt; Is $n$ (inputs) much larger than $m$ (outputs)? Use the VJP. Is $m$ much larger than $n$? Use the JVP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the output a single number, like a loss?&lt;/strong&gt; Use the VJP. One backward pass gives the gradient with respect to &lt;em&gt;all&lt;/em&gt; inputs and parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you want the effect of a specific nudge on every output?&lt;/strong&gt; Use the JVP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is memory tight, and the function has few inputs?&lt;/strong&gt; Consider the JVP, since it doesn't have to store the forward pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Four things to remember
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same $J$, two directions.&lt;/strong&gt; The JVP multiplies $J$ on the right (input-sized vector in). The VJP multiplies $J$ on the left (output-sized vector in).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neither ever builds $J$.&lt;/strong&gt; Both push a vector through the layers using each layer's local rule, so every step is a cheap vector operation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Column vs. row.&lt;/strong&gt; A JVP reads a column and a VJP reads a row. A loss has one row, so reverse mode wins training.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backpropagation is the VJP,&lt;/strong&gt; applied layer by layer from the loss back to the weights.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  11. Tangents vs. cotangents, demystified
&lt;/h2&gt;

&lt;p&gt;You'll see these words in JAX documentation and papers. They sound scary but map directly onto what we just did.&lt;/p&gt;

&lt;h3&gt;
  
  
  11.1 Tangent vectors (the "nudge" side)
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;tangent vector&lt;/strong&gt; at a point is a &lt;em&gt;direction in which you can move&lt;/em&gt; from that point, i.e. a perturbation of the inputs. In our notation: $\boldsymbol\delta$ or $\mathbf{v}$ (inputs) and $\dot{a}$ (intermediates). Recall from Section 8.4 that $\dot a$ means "how fast $a$ changes as you slide along the direction $\mathbf{v}$", and that for the inputs $\dot x_j$ is just $v_j$. The JVP takes a tangent in and yields a tangent out. This is why JVP is sometimes called "forward-mode" or the "pushforward": &lt;strong&gt;it pushes tangents forward&lt;/strong&gt; through $f$.&lt;/p&gt;

&lt;p&gt;Tangents live in the &lt;strong&gt;same space as the quantity they perturb.&lt;/strong&gt; The tangent of a $d$-dimensional vector is a $d$-dimensional vector.&lt;/p&gt;

&lt;h3&gt;
  
  
  11.2 Cotangent vectors (the "sensitivity" side)
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;cotangent vector&lt;/strong&gt; (or &lt;em&gt;covector&lt;/em&gt;) is a &lt;em&gt;measuring device&lt;/em&gt;: a thing that takes a nudge and returns a single number (a linear function of the nudge). The gradient is the perfect example. Given a nudge $\boldsymbol\delta$, it returns the resulting change in $f$: $\nabla f^\top \boldsymbol\delta$.&lt;/p&gt;

&lt;p&gt;In our notation, $\mathbf{u}$ (a weighting over outputs) and the adjoints $\bar a$ are cotangents. The VJP takes a cotangent over the &lt;em&gt;output&lt;/em&gt; and returns a cotangent over the &lt;em&gt;input&lt;/em&gt;. This is the "pullback": &lt;strong&gt;it pulls cotangents backward&lt;/strong&gt; through $f$.&lt;/p&gt;

&lt;h3&gt;
  
  
  11.3 Why bother with two names if both are just vectors?
&lt;/h3&gt;

&lt;p&gt;In ordinary $\mathbb{R}^n$ they're both lists of $n$ numbers, so the distinction seems pedantic. But they &lt;strong&gt;behave differently&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tangents go &lt;strong&gt;forward&lt;/strong&gt;; multiplied by $J$.&lt;/li&gt;
&lt;li&gt;Cotangents go &lt;strong&gt;backward&lt;/strong&gt;; multiplied by $J^\top$.&lt;/li&gt;
&lt;li&gt;The pairing between them is a number: $\mathbf{u}^\top(J\mathbf{v})$, which is the same as $(\mathbf{u}^\top J)\mathbf{v}$. This single identity is the entire logic behind why both modes agree:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;$$\underbrace{\mathbf{u}^\top}&lt;em&gt;{\text{cotangent}}\ \underbrace{(J\mathbf{v})}&lt;/em&gt;{\text{pushed-forward tangent}} \;=\; \underbrace{(\mathbf{u}^\top J)}&lt;em&gt;{\text{pulled-back cotangent}}\ \underbrace{\mathbf{v}}&lt;/em&gt;{\text{tangent}}$$&lt;/p&gt;

&lt;p&gt;Both sides answer: &lt;em&gt;"how much does my weighted output change when I nudge the input along $\mathbf{v}$?"&lt;/em&gt;, just computed from opposite ends.&lt;/p&gt;

&lt;p&gt;Keeping the two roles separate prevents bugs in advanced settings (e.g. non-Euclidean spaces, or when sizes happen to coincide and you mix them up).&lt;/p&gt;

&lt;h3&gt;
  
  
  11.4 The four objects, with numbers
&lt;/h3&gt;

&lt;p&gt;Take the three-layer network from Sections 8.6 and 9.6 at $\mathbf{x}=(1,2)$, with $J=\begin{bmatrix}0&amp;amp;0\2&amp;amp;1\6&amp;amp;3\end{bmatrix}$. If we choose a nudge $\mathbf{v}$ and a set of output weights $\mathbf{u}$, exactly four vectors appear, and each one has a name:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Lives at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$\mathbf{v}=(1,\ 0.5)$&lt;/td&gt;
&lt;td&gt;your chosen nudge&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;input tangent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$J\mathbf{v}=(0,\ 2.5,\ 7.5)$&lt;/td&gt;
&lt;td&gt;how fast each output changes along $\mathbf{v}$&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;output tangent&lt;/strong&gt; (the pushed-forward $\mathbf{v}$)&lt;/td&gt;
&lt;td&gt;the output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\mathbf{u}=(1,\ 1,\ 1)$&lt;/td&gt;
&lt;td&gt;weights defining the score $s=o_1+o_2+o_3$&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;output cotangent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\mathbf{u}^\top J=(8,\ 4)$&lt;/td&gt;
&lt;td&gt;sensitivity of the score to each input&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;input cotangent&lt;/strong&gt; (the pulled-back $\mathbf{u}$)&lt;/td&gt;
&lt;td&gt;the input&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two operations connect them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;JVP&lt;/strong&gt; carries a tangent across the function, from the input to the output: $\mathbf{v}\ \mapsto\ J\mathbf{v}$.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;VJP&lt;/strong&gt; carries a cotangent back across the function, from the output to the input: $\mathbf{u}\ \mapsto\ \mathbf{u}^\top J$.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pairing a cotangent with a tangent at the same location gives a number.&lt;/strong&gt; A cotangent is a measuring device, and a tangent is the thing it measures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At the &lt;strong&gt;input&lt;/strong&gt;: $(8,\,4)\cdot(1,\,0.5)=10$. The input cotangent measures the input tangent.&lt;/li&gt;
&lt;li&gt;At the &lt;strong&gt;output&lt;/strong&gt;: $(1,\,1,\,1)\cdot(0,\,2.5,\,7.5)=10$. The output cotangent measures the output tangent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both give the same $10$, because both describe the same thing: the rate at which the score $s$ changes as the inputs move along $\mathbf{v}$ (a step of size $\varepsilon$ changes $s$ by about $10\varepsilon$). That is the identity $\mathbf{u}^\top(J\mathbf{v})=(\mathbf{u}^\top J)\mathbf{v}$ from Section 11.3, now with actual numbers. The JVP lets you do the pairing at the output end, and the VJP lets you do it at the input end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on wording.&lt;/strong&gt; People say "the JVP" both for the operation ($J\mathbf{v}$) and for its result, the output tangent. Likewise "the VJP" is used for the operation and for its result, the input cotangent. This guide does the same when the meaning is clear from context.&lt;/p&gt;

&lt;h3&gt;
  
  
  11.5 Quick glossary
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Plain meaning&lt;/th&gt;
&lt;th&gt;Symbol in this doc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Perturbation&lt;/td&gt;
&lt;td&gt;A small nudge to a value&lt;/td&gt;
&lt;td&gt;$\boldsymbol\delta$, $\varepsilon\mathbf{v}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tangent&lt;/td&gt;
&lt;td&gt;A nudge/direction of change (forward mode carries these)&lt;/td&gt;
&lt;td&gt;$\mathbf{v}$, $\dot a$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cotangent / covector&lt;/td&gt;
&lt;td&gt;A weighting that measures nudges (reverse mode carries these)&lt;/td&gt;
&lt;td&gt;$\mathbf{u}$, $\bar a$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pushforward&lt;/td&gt;
&lt;td&gt;Moving a tangent through a function (= JVP)&lt;/td&gt;
&lt;td&gt;$J\mathbf{v}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pullback&lt;/td&gt;
&lt;td&gt;Moving a cotangent back through a function (= VJP)&lt;/td&gt;
&lt;td&gt;$J^\top\mathbf{u}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adjoint&lt;/td&gt;
&lt;td&gt;Another word for the cotangent attached to an intermediate value&lt;/td&gt;
&lt;td&gt;$\bar a$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  12. Forward mode vs. reverse mode: which one, and why
&lt;/h2&gt;

&lt;h3&gt;
  
  
  12.1 The question this section answers
&lt;/h3&gt;

&lt;p&gt;You have a function and want derivatives from it. Both the JVP and the VJP can give them, so &lt;strong&gt;which one should you use?&lt;/strong&gt; The answer depends on the &lt;em&gt;shape&lt;/em&gt; of the Jacobian. Recall that for $f:\mathbb{R}^n\to\mathbb{R}^m$, the Jacobian $J$ is an $m\times n$ matrix (rows are outputs, columns are inputs).&lt;/p&gt;

&lt;h3&gt;
  
  
  12.2 The key fact: one pass reads one slice of $J$
&lt;/h3&gt;

&lt;p&gt;Take the Jacobian of our three-layer network from Sections 8.6 and 9.6 (3 outputs, 2 inputs):&lt;/p&gt;

&lt;p&gt;$$J=\begin{bmatrix}0&amp;amp;0\2&amp;amp;1\6&amp;amp;3\end{bmatrix}$$&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A forward-mode pass (a JVP)&lt;/strong&gt; with $\mathbf{v}=\mathbf{e}_1$ gives the first &lt;strong&gt;column&lt;/strong&gt;, $(0,2,6)$. With $\mathbf{v}=\mathbf{e}_2$ it gives the second column, $(0,1,3)$.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reverse-mode pass (a VJP)&lt;/strong&gt; with $\mathbf{u}=\mathbf{e}_1$ gives the first &lt;strong&gt;row&lt;/strong&gt;, $(0,0)$. With $\mathbf{u}=\mathbf{e}_2$ it gives $(2,1)$, and with $\mathbf{u}=\mathbf{e}_3$ it gives $(6,3)$.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To read the &lt;em&gt;whole&lt;/em&gt; matrix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forward mode needs one pass per column, so &lt;strong&gt;$n$ passes&lt;/strong&gt; (here $n=2$).&lt;/li&gt;
&lt;li&gt;Reverse mode needs one pass per row, so &lt;strong&gt;$m$ passes&lt;/strong&gt; (here $m=3$).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each pass costs about the same (a small multiple of one evaluation of the function), so you want whichever mode needs &lt;strong&gt;fewer passes&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Forward mode (JVP)&lt;/th&gt;
&lt;th&gt;Reverse mode (VJP)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One pass computes&lt;/td&gt;
&lt;td&gt;one &lt;strong&gt;column&lt;/strong&gt; ($J\mathbf{v}$)&lt;/td&gt;
&lt;td&gt;one &lt;strong&gt;row&lt;/strong&gt; ($\mathbf{u}^\top J$)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passes to get the full $J$&lt;/td&gt;
&lt;td&gt;$n$ (one per input)&lt;/td&gt;
&lt;td&gt;$m$ (one per output)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;low (no need to store intermediates)&lt;/td&gt;
&lt;td&gt;high (must store the forward pass's intermediates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best when&lt;/td&gt;
&lt;td&gt;few inputs, many outputs ($n \ll m$)&lt;/td&gt;
&lt;td&gt;many inputs, few outputs ($n \gg m$)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(Here "$n \ll m$" means "far fewer inputs than outputs", and "$n \gg m$" means the opposite.)&lt;/p&gt;

&lt;h3&gt;
  
  
  12.3 The shape of $J$ decides
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shape of $J$&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Forward mode&lt;/th&gt;
&lt;th&gt;Reverse mode&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Wide&lt;/strong&gt; (few inputs, many outputs)&lt;/td&gt;
&lt;td&gt;3 inputs, 1,000 outputs&lt;/td&gt;
&lt;td&gt;3 passes&lt;/td&gt;
&lt;td&gt;1,000 passes&lt;/td&gt;
&lt;td&gt;forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Tall&lt;/strong&gt; (many inputs, few outputs)&lt;/td&gt;
&lt;td&gt;$10^9$ inputs, 1 output&lt;/td&gt;
&lt;td&gt;$10^9$ passes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 pass&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;reverse&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Neural network training&lt;/strong&gt; is the second row. The single output is the loss ($m=1$) and the inputs are the parameters (billions of them, $n\approx10^9$). The Jacobian of the loss is one very long row, and a single VJP reads a whole row. Reverse mode gets the entire gradient in &lt;strong&gt;one&lt;/strong&gt; backward pass, while forward mode would need &lt;strong&gt;billions&lt;/strong&gt; of passes. This is the whole reason backpropagation is reverse mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When forward mode wins:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A function with very few inputs and many outputs, for example a physical simulation with 3 tunable parameters and thousands of outputs. Forward mode needs only 3 passes.&lt;/li&gt;
&lt;li&gt;Computing a single directional derivative $J\mathbf{v}$, which is exactly one JVP.&lt;/li&gt;
&lt;li&gt;Memory-constrained settings where storing the activations (see below) is too costly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  12.4 The memory trade-off
&lt;/h3&gt;

&lt;p&gt;Reverse mode needs fewer passes for a loss, but it has a cost. The backward pass runs &lt;em&gt;after&lt;/em&gt; the forward pass and uses values from it: the ReLU mask, the input to each linear layer (needed for $\bar W=\bar{\mathbf{y}}\,\mathbf{x}^\top$), and so on. So the forward pass's intermediate values (the &lt;strong&gt;activations&lt;/strong&gt;) must be stored until the backward pass uses them.&lt;/p&gt;

&lt;p&gt;Forward mode has no such requirement: it computes each value and its tangent together, then discards the value as it moves on.&lt;/p&gt;

&lt;p&gt;This is why &lt;strong&gt;training uses far more memory than inference&lt;/strong&gt;. It is also why techniques like &lt;strong&gt;gradient checkpointing&lt;/strong&gt; exist: they store only some of the activations and recompute the rest during the backward pass, trading extra computation for lower memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  12.5 The same idea as an order of multiplication
&lt;/h3&gt;

&lt;p&gt;This is the content of Sections 8.6 and 9.6 in a compact form. A network's Jacobian is a product of layer Jacobians, $J = J_k \cdots J_2 J_1$, and the two modes differ in &lt;em&gt;which end they start multiplying from&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forward mode&lt;/strong&gt; multiplies from the &lt;strong&gt;right&lt;/strong&gt;: $J_k\big(\cdots(J_2(J_1\mathbf{v}))\big)$. It pushes a vector through $J_1$, then $J_2$, and so on, in the same direction as the forward pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse mode&lt;/strong&gt; multiplies from the &lt;strong&gt;left&lt;/strong&gt;: $\big(((\mathbf{u}^\top J_k)J_{k-1})\cdots\big)J_1$. It pushes a vector through $J_k$ first, then $J_{k-1}$, and so on, in the opposite direction.&lt;/li&gt;
&lt;li&gt;In both cases &lt;strong&gt;each step is a matrix-by-vector product&lt;/strong&gt;, which is cheap. Multiplying the matrices together first (matrix-by-matrix products) would be far costlier. This is the whole trick: &lt;strong&gt;never multiply two Jacobians; always multiply a vector through them.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  12.6 Summary
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;The shape of $J$ decides the mode. You pay &lt;strong&gt;one pass per column&lt;/strong&gt; in forward mode and &lt;strong&gt;one pass per row&lt;/strong&gt; in reverse mode. A loss has a single row, so reverse mode wins for training, and its price is storing the forward pass.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  13. Backpropagation is just a chain of VJPs
&lt;/h2&gt;

&lt;p&gt;Now we connect everything to neural networks.&lt;/p&gt;

&lt;h3&gt;
  
  
  13.1 Setup
&lt;/h3&gt;

&lt;p&gt;A network with layers computes:&lt;/p&gt;

&lt;p&gt;$$\mathbf{x}&lt;em&gt;0 \xrightarrow{f_1(\cdot;\,\theta_1)} \mathbf{x}_1 \xrightarrow{f_2(\cdot;\,\theta_2)} \mathbf{x}_2 \;\cdots\; \mathbf{x}&lt;/em&gt;{k} \xrightarrow{\ \text{loss}\ } L$$&lt;/p&gt;

&lt;p&gt;where $\mathbf{x}_0$ is the input data, $\theta_i$ are layer $i$'s parameters, and $L$ is a &lt;strong&gt;scalar&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We want $\partial L/\partial \theta_i$ for every layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which argument is "the input"?&lt;/strong&gt; Notice that the network is a function of &lt;em&gt;two&lt;/em&gt; things: the data $\mathbf{x}_0$ and the parameters $\theta=(\theta_1,\dots,\theta_k)$. When we talk about "the input" of a Jacobian or a VJP, we mean &lt;em&gt;whichever argument we choose to differentiate with respect to&lt;/em&gt;, with the other one frozen as a constant. In training, the data and label are fixed and &lt;strong&gt;the variable is $\theta$&lt;/strong&gt;: the function we differentiate is $\theta\mapsto L(\theta)$. Section 13.5 walks through this with a digit classifier.&lt;/p&gt;

&lt;h3&gt;
  
  
  13.2 The backward recursion
&lt;/h3&gt;

&lt;p&gt;Define the cotangent (adjoint) at each activation: $\bar{\mathbf{x}}_i = \partial L/\partial \mathbf{x}_i$.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start: $\bar L = 1$. Then $\bar{\mathbf{x}}&lt;em&gt;k = \nabla&lt;/em&gt;{\mathbf{x}_k} L$ (the loss's own gradient).&lt;/li&gt;
&lt;li&gt;For each layer going backward, compute &lt;strong&gt;two VJPs&lt;/strong&gt; of that layer's function $f_i$, using the incoming cotangent $\bar{\mathbf{x}}_i$:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;$$\bar{\mathbf{x}}&lt;em&gt;{i-1} = \left(\frac{\partial \mathbf{x}_i}{\partial \mathbf{x}&lt;/em&gt;{i-1}}\right)^{!\top} \bar{\mathbf{x}}_i \qquad\quad \bar\theta_i = \left(\frac{\partial \mathbf{x}_i}{\partial \theta_i}\right)^{!\top} \bar{\mathbf{x}}_i$$&lt;/p&gt;

&lt;p&gt;The first VJP passes the signal &lt;em&gt;back to the previous layer&lt;/em&gt;; the second yields the &lt;em&gt;parameter gradient&lt;/em&gt;, which is what the optimizer needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are these derivative terms?&lt;/strong&gt; They are Jacobians, the same kind of matrix as the $J$ we have used throughout. Each layer has &lt;em&gt;two&lt;/em&gt; of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$\dfrac{\partial \mathbf{x}&lt;em&gt;i}{\partial \mathbf{x}&lt;/em&gt;{i-1}}=J_i$ is the Jacobian of layer $i$ with respect to its &lt;strong&gt;input activation&lt;/strong&gt;. If $\mathbf{x}&lt;em&gt;{i-1}$ has $p$ entries and $\mathbf{x}_i$ has $q$ entries, it is a $q\times p$ matrix, with rows indexed by the layer's outputs and columns by its inputs (the convention from Section 5.1). So the first formula, $\bar{\mathbf{x}}&lt;/em&gt;{i-1}=J_i^\top\bar{\mathbf{x}}&lt;em&gt;i$, is exactly the VJP rule $\bar{\mathbf{a}}=J_g^\top\bar{\mathbf{b}}$ from Section 9.4, with $g=f_i$. The shapes work out: $J_i^\top$ is $p\times q$ and $\bar{\mathbf{x}}_i$ has $q$ entries, so the result has $p$ entries, the size of $\mathbf{x}&lt;/em&gt;{i-1}$. For a linear layer $J_i=W$, and for ReLU $J_i$ is a diagonal 0/1 matrix.&lt;/li&gt;
&lt;li&gt;$\dfrac{\partial \mathbf{x}&lt;em&gt;i}{\partial \theta_i}$ is a &lt;strong&gt;different&lt;/strong&gt; Jacobian: that of layer $i$ with respect to its &lt;strong&gt;weights&lt;/strong&gt;. It has shape $q\times(\text{number of weights in layer } i)$. For a linear layer it is large and highly structured, and its VJP simplifies to the outer product $\bar W=\bar{\mathbf{x}}_i\,\mathbf{x}&lt;/em&gt;{i-1}^\top$ from Section 13.3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the backward pass applies the transpose of each of the two Jacobians to the same incoming cotangent $\bar{\mathbf{x}}_i$. As always, the framework never builds either matrix: it uses the local rules of Section 13.3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That is backpropagation.&lt;/strong&gt; Nothing more. The famous "error signal propagating backward" is the cotangent $\bar{\mathbf{x}}_i$, and "propagating" it means one VJP per layer. Each layer only needs to know how to compute &lt;strong&gt;its own local VJP&lt;/strong&gt;; the framework chains them together automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  13.3 Local VJP rules for common layers
&lt;/h3&gt;

&lt;p&gt;These are the building blocks that autodiff libraries implement for every primitive operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linear layer&lt;/strong&gt; $\mathbf{y} = W\mathbf{x} + \mathbf{b}$, with incoming cotangent $\bar{\mathbf{y}}$:&lt;/p&gt;

&lt;p&gt;$$\bar{\mathbf{x}} = W^\top \bar{\mathbf{y}} \qquad \bar W = \bar{\mathbf{y}}\,\mathbf{x}^\top \qquad \bar{\mathbf{b}} = \bar{\mathbf{y}}$$&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why?&lt;/em&gt; $J_{\mathbf{x}} = W$, so the VJP is $W^\top\bar{\mathbf{y}}$. For the weights: entry $W_{ij}$ affects only $y_i$ and does so with multiplier $x_j$, so $\partial L/\partial W_{ij} = \bar y_i\, x_j$. That's the &lt;strong&gt;outer product&lt;/strong&gt; $\bar{\mathbf{y}}\mathbf{x}^\top$. (For a batch of examples, the contributions are summed over the batch.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Elementwise activation&lt;/strong&gt; $\mathbf{y} = \sigma(\mathbf{z})$: the Jacobian is diagonal, $\text{diag}(\sigma'(\mathbf{z}))$, so the VJP is just an elementwise product:&lt;/p&gt;

&lt;p&gt;$$\bar{\mathbf{z}} = \sigma'(\mathbf{z}) \odot \bar{\mathbf{y}}$$&lt;/p&gt;

&lt;p&gt;(Here $\odot$ means multiply entry by entry.) For ReLU, $\sigma'(z) = 1$ if $z&amp;gt;0$ else $0$. So ReLU simply &lt;em&gt;lets the signal pass through where the unit was active, and blocks it elsewhere&lt;/em&gt;. We never build the huge diagonal matrix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sum / copy (fan-out):&lt;/strong&gt; if a value is used in two places, cotangents from both uses &lt;strong&gt;add&lt;/strong&gt;. If you add two values, the cotangent flows unchanged to both. This is how skip connections (as in ResNets) work: the gradient takes a "highway" straight back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Squared-error loss&lt;/strong&gt; $L = \tfrac12|\mathbf{h}-\mathbf{t}|^2$: $\ \bar{\mathbf{h}} = \mathbf{h}-\mathbf{t}$.&lt;/p&gt;

&lt;h3&gt;
  
  
  13.4 Why this is efficient
&lt;/h3&gt;

&lt;p&gt;For a network with layers of width $d$, a naive "build the full Jacobian and multiply" approach costs $O(d^3)$ per layer for matrix-matrix products, and storage is quadratic in size. Backprop does only matrix-&lt;strong&gt;vector&lt;/strong&gt; products ($O(d^2)$) and the total backward cost is about &lt;strong&gt;2× the forward pass&lt;/strong&gt;. That ratio (parameters' gradients for roughly the price of two evaluations) is what makes large-scale training feasible.&lt;/p&gt;

&lt;h3&gt;
  
  
  13.5 A concrete example: an MNIST digit classifier
&lt;/h3&gt;

&lt;p&gt;Let's tie everything together with a real task. The input is a $28\times28$ grayscale image from MNIST, flattened into $\mathbf{x}\in\mathbb{R}^{784}$, and the network outputs 10 scores (one per digit). The question we want to settle: &lt;strong&gt;what exactly is the "input" of the function whose VJP we take?&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: the network has two arguments
&lt;/h4&gt;

&lt;p&gt;Take a small network: $784\to128\to10$ with a ReLU in the middle.&lt;/p&gt;

&lt;p&gt;$$\mathbf{a}=W_1\mathbf{x}+\mathbf{b}_1,\qquad \mathbf{h}=\text{ReLU}(\mathbf{a}),\qquad \mathbf{z}=W_2\mathbf{h}+\mathbf{b}_2$$&lt;/p&gt;

&lt;p&gt;The 10 numbers $\mathbf{z}$ are the &lt;strong&gt;logits&lt;/strong&gt; (raw scores). The function is $\mathbf{z}=N(\theta,\mathbf{x})$, where $\theta=(W_1,\mathbf{b}_1,W_2,\mathbf{b}_2)$ holds all the weights. The number of weights is&lt;/p&gt;

&lt;p&gt;$$\underbrace{784\cdot128}&lt;em&gt;{W_1}+\underbrace{128}&lt;/em&gt;{\mathbf{b}&lt;em&gt;1}+\underbrace{128\cdot10}&lt;/em&gt;{W_2}+\underbrace{10}_{\mathbf{b}_2}=100{,}352+128+1{,}280+10=101{,}770$$&lt;/p&gt;

&lt;p&gt;$N$ depends on both $\theta$ and $\mathbf{x}$. Which one is "the input" for the Jacobian depends on which question we ask. We pick one as the variable, and the other is frozen.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: training freezes the image and varies the weights
&lt;/h4&gt;

&lt;p&gt;In training we have one image $\mathbf{x}$ with a known label $y$, and we ask: &lt;em&gt;"how should I change the weights to make the loss smaller?"&lt;/em&gt; So the function we differentiate is&lt;/p&gt;

&lt;p&gt;$$\theta\ \longmapsto\ L(\theta)=\text{loss}\big(N(\theta,\mathbf{x}),\ y\big)$$&lt;/p&gt;

&lt;p&gt;The image and the label are fixed data. &lt;strong&gt;The input of this function is $\theta$&lt;/strong&gt;, with $n=101{,}770$ entries.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: the loss collapses the 10 outputs into one number
&lt;/h4&gt;

&lt;p&gt;The network has 10 outputs, but the full function ends with a loss layer, for example cross-entropy:&lt;/p&gt;

&lt;p&gt;$$L=-\log\Big(\text{softmax}(\mathbf{z})_y\Big)$$&lt;/p&gt;

&lt;p&gt;This turns 10 scores into &lt;strong&gt;one&lt;/strong&gt; number, so $m=1$. The Jacobian of $L$ with respect to $\theta$ is a single row of $101{,}770$ entries, which is the "tall" case from Section 12. One backward pass (a VJP) gives the whole gradient, while forward mode would need $101{,}770$ passes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 4: where "the weighted sum of the outputs" appears
&lt;/h4&gt;

&lt;p&gt;The backward pass starts at the loss. The loss layer's own VJP produces a cotangent at the logits:&lt;/p&gt;

&lt;p&gt;$$\bar{\mathbf{z}}=\frac{\partial L}{\partial\mathbf{z}}=\text{softmax}(\mathbf{z})-\text{onehot}(y)$$&lt;/p&gt;

&lt;p&gt;That is 10 numbers, and it plays exactly the role of the weight vector $\mathbf{u}$ from Section 9 for the rest of the network. For an image of a "3":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The entry for the correct class (3) is &lt;strong&gt;negative&lt;/strong&gt;: raising that logit lowers the loss.&lt;/li&gt;
&lt;li&gt;The entries for the wrong classes are &lt;strong&gt;positive&lt;/strong&gt;, and larger for classes the network is more confident about: raising those logits would increase the loss.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the network's point of view, with $\theta$ as the variable and the logits as the 10 outputs, the rest of the backward pass computes the VJP&lt;/p&gt;

&lt;p&gt;$$\bar{\mathbf{z}}^\top\frac{\partial\mathbf{z}}{\partial\theta}=\nabla_\theta L$$&lt;/p&gt;

&lt;p&gt;This is the gradient of the weighted sum of the outputs $s=\bar{\mathbf{z}}^\top\mathbf{z}$ (with $\bar{\mathbf{z}}$ held fixed), and at this point it equals the gradient of $L$. You never choose $\mathbf{u}$ by hand: the loss layer's backward step computes it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 5: the layers, one VJP at a time
&lt;/h4&gt;

&lt;p&gt;Starting from $\bar{\mathbf{z}}$ (10 numbers), apply the local rules of Section 13.3, last layer first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Cotangent in&lt;/th&gt;
&lt;th&gt;Computation&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linear 2 ($\mathbf{z}=W_2\mathbf{h}+\mathbf{b}_2$)&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{z}}$&lt;/td&gt;
&lt;td&gt;$\bar W_2=\bar{\mathbf{z}}\,\mathbf{h}^\top$, $\ \bar{\mathbf{b}}_2=\bar{\mathbf{z}}$&lt;/td&gt;
&lt;td&gt;weight gradients&lt;/td&gt;
&lt;td&gt;$10\times128$, $10$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{h}}=W_2^\top\bar{\mathbf{z}}$&lt;/td&gt;
&lt;td&gt;goes back&lt;/td&gt;
&lt;td&gt;$128$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ReLU ($\mathbf{h}=\text{ReLU}(\mathbf{a})$)&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{h}}$&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{a}}=[\mathbf{a}&amp;gt;0]\odot\bar{\mathbf{h}}$&lt;/td&gt;
&lt;td&gt;goes back&lt;/td&gt;
&lt;td&gt;$128$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linear 1 ($\mathbf{a}=W_1\mathbf{x}+\mathbf{b}_1$)&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{a}}$&lt;/td&gt;
&lt;td&gt;$\bar W_1=\bar{\mathbf{a}}\,\mathbf{x}^\top$, $\ \bar{\mathbf{b}}_1=\bar{\mathbf{a}}$&lt;/td&gt;
&lt;td&gt;weight gradients&lt;/td&gt;
&lt;td&gt;$128\times784$, $128$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;$\bar{\mathbf{x}}=W_1^\top\bar{\mathbf{a}}$&lt;/td&gt;
&lt;td&gt;image sensitivity&lt;/td&gt;
&lt;td&gt;$784$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every weight gradient comes out of &lt;strong&gt;one&lt;/strong&gt; backward pass: $1{,}280+10+100{,}352+128=101{,}770$ numbers, exactly the size of $\theta$. Each layer's two VJPs are the two formulas of Section 13.2: one passes the cotangent back to the previous layer, and the other produces that layer's weight gradient. The picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;forward:   x (784 pixels) --&amp;gt; Linear1 --&amp;gt; ReLU --&amp;gt; Linear2 --&amp;gt; z (10 logits) --&amp;gt; loss L
                                                                                 |
backward:  grad W1,b1 &amp;lt;-- ReLU mask &amp;lt;-- grad W2,b2 &amp;lt;-- z_bar = softmax(z) - onehot(y)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then gradient descent updates every weight: $\theta\leftarrow\theta-\eta\,\bar\theta$.&lt;/p&gt;

&lt;p&gt;Notice the last row of the table: the pass also computes $\bar{\mathbf{x}}$, the sensitivity of the loss to each of the 784 pixels. During training this is simply thrown away.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 6: the other choice, freezing the weights
&lt;/h4&gt;

&lt;p&gt;If we freeze the trained weights and make the &lt;strong&gt;image&lt;/strong&gt; the variable, the function is $\mathbf{x}\mapsto L(\mathbf{x})$, and the same backward pass gives $\bar{\mathbf{x}}=\nabla_{\mathbf{x}}L$, the exact quantity thrown away in training. It is used for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adversarial examples:&lt;/strong&gt; nudge the pixels in the direction that increases the loss, so the network misclassifies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saliency maps:&lt;/strong&gt; which pixels is the prediction most sensitive to?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is the same machinery with a different choice of variable.&lt;/p&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;View&lt;/th&gt;
&lt;th&gt;Variable (the "input" of $f$)&lt;/th&gt;
&lt;th&gt;Frozen&lt;/th&gt;
&lt;th&gt;$n$&lt;/th&gt;
&lt;th&gt;$m$&lt;/th&gt;
&lt;th&gt;What the VJP gives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Training&lt;/td&gt;
&lt;td&gt;weights $\theta$&lt;/td&gt;
&lt;td&gt;image, label&lt;/td&gt;
&lt;td&gt;$101{,}770$&lt;/td&gt;
&lt;td&gt;1 (the loss)&lt;/td&gt;
&lt;td&gt;$\nabla_\theta L$, used to update the weights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adversarial / saliency&lt;/td&gt;
&lt;td&gt;pixels $\mathbf{x}$&lt;/td&gt;
&lt;td&gt;weights, label&lt;/td&gt;
&lt;td&gt;$784$&lt;/td&gt;
&lt;td&gt;1 (the loss)&lt;/td&gt;
&lt;td&gt;$\nabla_{\mathbf{x}}L$, the sensitivity of the loss to each pixel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both views have a single output, the loss, so both need just one backward pass. The theory never changes: &lt;strong&gt;you choose which argument to differentiate with respect to, and the VJP gives the sensitivity of the score to that argument.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In practice we train on batches of images and average the loss. The weights are shared across the batch, so each weight's gradient is the sum (or average) of the per-image gradients, as noted in Section 13.3.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. A complete worked backprop example, by hand
&lt;/h2&gt;

&lt;p&gt;A tiny one-layer network, so you can follow every number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;$$\mathbf{x} = \begin{bmatrix}1\2\end{bmatrix},\quad&lt;br&gt;
W = \begin{bmatrix}1 &amp;amp; -1\ 2 &amp;amp; 0.5\end{bmatrix},\quad&lt;br&gt;
\mathbf{b} = \begin{bmatrix}0\-1\end{bmatrix},\quad&lt;br&gt;
\mathbf{t} = \begin{bmatrix}1\1\end{bmatrix}$$&lt;/p&gt;

&lt;p&gt;$$\mathbf{z} = W\mathbf{x}+\mathbf{b},\qquad \mathbf{h} = \text{ReLU}(\mathbf{z}),\qquad L = \tfrac12|\mathbf{h}-\mathbf{t}|^2$$&lt;/p&gt;

&lt;h3&gt;
  
  
  Forward pass
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;$W\mathbf{x} = (1\cdot1 + (-1)\cdot 2,\ 2\cdot1 + 0.5\cdot2) = (-1,\ 3)$&lt;/li&gt;
&lt;li&gt;$\mathbf{z} = (-1+0,\ 3-1) = (-1,\ 2)$&lt;/li&gt;
&lt;li&gt;$\mathbf{h} = \text{ReLU}(\mathbf{z}) = (0,\ 2)$&lt;/li&gt;
&lt;li&gt;$L = \tfrac12\big[(0-1)^2 + (2-1)^2\big] = \tfrac12(1+1) = 1$&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Backward pass (a chain of VJPs, last layer first)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Loss:&lt;/strong&gt; $\bar{\mathbf{h}} = \mathbf{h}-\mathbf{t} = (-1,\ 1)$.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ReLU:&lt;/strong&gt; the mask is $[z&amp;gt;0] = (0, 1)$. So $\bar{\mathbf{z}} = (0,1)\odot(-1,1) = (0,\ 1)$. (Neuron 1 was "dead" so it gets no gradient.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linear layer:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;$\bar{\mathbf{b}} = \bar{\mathbf{z}} = (0,\ 1)$&lt;/li&gt;
&lt;li&gt;$\bar W = \bar{\mathbf{z}}\,\mathbf{x}^\top = \begin{bmatrix}0\1\end{bmatrix}\begin{bmatrix}1 &amp;amp; 2\end{bmatrix} = \begin{bmatrix}0 &amp;amp; 0\ 1 &amp;amp; 2\end{bmatrix}$&lt;/li&gt;
&lt;li&gt;$\bar{\mathbf{x}} = W^\top\bar{\mathbf{z}} = \begin{bmatrix}1 &amp;amp; 2\ -1 &amp;amp; 0.5\end{bmatrix}\begin{bmatrix}0\1\end{bmatrix} = \begin{bmatrix}2\0.5\end{bmatrix}$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Sanity check with a finite difference
&lt;/h3&gt;

&lt;p&gt;Nudge $W_{21}$ (row 2, column 1) from 2 to $2.001$. Then $z_2 = 2.001\cdot1 + 0.5\cdot2 - 1 = 2.001$, $h_2 = 2.001$, and $L = \tfrac12(1 + 1.001^2) = \tfrac12(1+1.002001) = 1.0010005$. The change is $0.0010005$, so the slope $\approx 1.0005 \approx 1$, matching $\bar W_{21} = 1$.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gradient descent step
&lt;/h3&gt;

&lt;p&gt;With learning rate $\eta = 0.1$: $W \leftarrow W - 0.1\,\bar W = \begin{bmatrix}1 &amp;amp; -1\ 1.9 &amp;amp; 0.3\end{bmatrix}$, $\ \mathbf{b} \leftarrow (0,\ -1.1)$. The new $z_2 = 1.9 + 0.6 - 1.1 = 1.4$, so $h_2 = 1.4$, which is closer to the target 1. The loss dropped. That's learning.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. The Hessian: the derivative of the derivative
&lt;/h2&gt;

&lt;h3&gt;
  
  
  15.1 Motivation
&lt;/h3&gt;

&lt;p&gt;The gradient tells you the slope at a point. But slope alone doesn't tell you &lt;strong&gt;how far&lt;/strong&gt; you can safely step, or &lt;strong&gt;whether you're near a bottom&lt;/strong&gt;. For that you need to know how the slope &lt;em&gt;itself&lt;/em&gt; is changing: &lt;strong&gt;curvature&lt;/strong&gt;. In one dimension that's the second derivative $f''(x)$:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$f'' &amp;gt; 0$: curves upward (a bowl).&lt;/li&gt;
&lt;li&gt;$f'' &amp;lt; 0$: curves downward (a hill).&lt;/li&gt;
&lt;li&gt;$f'' = 0$: no curvature at that point (locally flat), and the second derivative alone cannot tell you what the function does next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At a flat spot of the graph (where $f'=0$), these signs decide whether you are at the bottom of a bowl (a minimum), the top of a hill (a maximum), or neither. The Hessian is the multi-dimensional version of $f''$.&lt;/p&gt;

&lt;h3&gt;
  
  
  15.2 Definition
&lt;/h3&gt;

&lt;p&gt;For a scalar function $f:\mathbb{R}^n\to\mathbb{R}$, the &lt;strong&gt;Hessian&lt;/strong&gt; is the $n\times n$ matrix of all second partial derivatives:&lt;/p&gt;

&lt;p&gt;$$H_{ij} = \frac{\partial^2 f}{\partial x_i\,\partial x_j}$$&lt;/p&gt;

&lt;p&gt;Equivalently and importantly:&lt;/p&gt;

&lt;p&gt;$$\boxed{H = \text{Jacobian of the gradient}}$$&lt;/p&gt;

&lt;p&gt;The gradient $\nabla f:\mathbb{R}^n \to \mathbb{R}^n$ is itself a vector-valued function. Its Jacobian is $n\times n$, and that is the Hessian. This identity is how we'll compute Hessian-related quantities with the JVP/VJP machinery from earlier. In particular, the gradient is itself computed by a VJP (Section 9.8), so the Hessian is "the Jacobian of a VJP." Section 16 uses exactly this fact to get Hessian-vector products without ever building $H$.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symmetry:&lt;/strong&gt; if the second derivatives are continuous (true for the smooth functions in this guide), then $\partial^2 f/\partial x_i\partial x_j = \partial^2 f/\partial x_j\partial x_i$, so $H = H^\top$.&lt;/p&gt;

&lt;h3&gt;
  
  
  15.3 Example
&lt;/h3&gt;

&lt;p&gt;Take $f(x,y) = x^2y + y^2$. Gradient: $(2xy,\ x^2+2y)$. Differentiate each component again:&lt;/p&gt;

&lt;p&gt;$$H = \begin{bmatrix} \partial_x(2xy) &amp;amp; \partial_y(2xy)\ \partial_x(x^2+2y) &amp;amp; \partial_y(x^2+2y)\end{bmatrix}= \begin{bmatrix}2y &amp;amp; 2x\ 2x &amp;amp; 2\end{bmatrix}$$&lt;/p&gt;

&lt;p&gt;At $(1,2)$: $H=\begin{bmatrix}4&amp;amp;2\2&amp;amp;2\end{bmatrix}$. Note the off-diagonals match (symmetric).&lt;/p&gt;

&lt;p&gt;The trace is $6$ and the determinant is $4\cdot2-2\cdot2=4$, so the eigenvalues are $3\pm\sqrt5\approx 5.24$ and $0.76$. Both are positive, so &lt;strong&gt;at $(1,2)$ the function curves upward in every direction&lt;/strong&gt;, much more steeply along one direction than the other (a narrow valley). Note that this is a statement about curvature only: $\nabla f(1,2)=(4,5)\neq\mathbf{0}$, so $(1,2)$ is &lt;em&gt;not&lt;/em&gt; a minimum. We are on the side of the valley, not at the bottom.&lt;/p&gt;

&lt;h3&gt;
  
  
  15.4 What the Hessian tells you: the second-order approximation
&lt;/h3&gt;

&lt;p&gt;Extending Section 2's linear approximation by one more term:&lt;/p&gt;

&lt;p&gt;$$f(\mathbf{x}+\boldsymbol\delta)\approx f(\mathbf{x}) + \nabla f^\top\boldsymbol\delta + \tfrac12\,\boldsymbol\delta^\top H\,\boldsymbol\delta$$&lt;/p&gt;

&lt;p&gt;The last term is the &lt;strong&gt;curvature correction&lt;/strong&gt;. Its sign and size depend on the direction $\boldsymbol\delta$:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$\boldsymbol\delta^\top H\boldsymbol\delta$ is the curvature of $f$ along direction $\boldsymbol\delta$ (for unit-length $\boldsymbol\delta$).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;eigenvalues&lt;/strong&gt; of $H$ are the curvatures along its principal axes (its &lt;strong&gt;eigenvectors&lt;/strong&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Numeric check&lt;/strong&gt; (the same kind of check we did for the gradient in Section 4.3). At $(1,2)$ with $\boldsymbol\delta=(0.1,-0.1)$:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exact: $f(1.1,\,1.9)=1.21\cdot1.9+1.9^2=2.299+3.61=5.909$, and $f(1,2)=6$.&lt;/li&gt;
&lt;li&gt;Linear approximation: $6+\nabla f^\top\boldsymbol\delta=6+(4\cdot0.1+5\cdot(-0.1))=5.9$. The error is $0.009$.&lt;/li&gt;
&lt;li&gt;Curvature term: $\boldsymbol\delta^\top H\boldsymbol\delta=4(0.01)+2\cdot2\cdot(0.1)(-0.1)+2(0.01)=0.02$, so $\tfrac12\boldsymbol\delta^\top H\boldsymbol\delta=0.01$.&lt;/li&gt;
&lt;li&gt;Second-order approximation: $5.9+0.01=5.91$. The error drops to $0.001$.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding the Hessian term made the local model nine times more accurate here.&lt;/p&gt;

&lt;h3&gt;
  
  
  15.5 Classifying a critical point
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;critical point&lt;/strong&gt; is a point where $\nabla f=\mathbf{0}$ (the slope is zero in every direction). There, the linear term vanishes and the Hessian decides the shape:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Eigenvalues of $H$&lt;/th&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;all positive&lt;/td&gt;
&lt;td&gt;bowl&lt;/td&gt;
&lt;td&gt;local &lt;strong&gt;minimum&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;all negative&lt;/td&gt;
&lt;td&gt;dome&lt;/td&gt;
&lt;td&gt;local &lt;strong&gt;maximum&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mixed signs (some positive, some negative)&lt;/td&gt;
&lt;td&gt;saddle&lt;/td&gt;
&lt;td&gt;neither: goes up in some directions and down in others&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;some zero, the rest all $\ge0$ (or all $\le0$)&lt;/td&gt;
&lt;td&gt;flat directions&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;inconclusive&lt;/strong&gt;: the Hessian alone cannot decide&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Examples at the origin (where the gradient is zero in each case):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$f=x^2+3y^2$ has $H=\text{diag}(2,6)$: a minimum.&lt;/li&gt;
&lt;li&gt;$f=x^2-y^2$ has $H=\text{diag}(2,-2)$: a saddle, like a mountain pass.&lt;/li&gt;
&lt;li&gt;$f=x^2+y^4$ and $f=x^2+y^3$ both have $H=\text{diag}(2,0)$ at the origin, yet the first has a minimum there and the second does not (along the $y$ axis, $y^3$ is negative for $y&amp;lt;0$). The Hessian alone cannot distinguish them, which is exactly what "inconclusive" means.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters in AI:&lt;/strong&gt; the Hessians of trained neural networks typically have a huge number of eigenvalues near zero (flat directions) and a small number of large ones. So the inconclusive case is the normal case in deep learning, not an edge case.&lt;/p&gt;

&lt;h3&gt;
  
  
  15.6 Gradient descent on a quadratic: where the learning-rate limit comes from
&lt;/h3&gt;

&lt;p&gt;A quadratic bowl is the simplest landscape with curvature. Take&lt;/p&gt;

&lt;p&gt;$$f(x,y)=\tfrac12\big(x^2+10y^2\big),\qquad \nabla f=(x,\ 10y),\qquad H=\begin{bmatrix}1&amp;amp;0\0&amp;amp;10\end{bmatrix}$$&lt;/p&gt;

&lt;p&gt;The eigenvalues are $\lambda_1=1$ and $\lambda_2=10$: very different curvatures, a narrow valley. Gradient descent updates each coordinate independently:&lt;/p&gt;

&lt;p&gt;$$x\leftarrow x-\eta\cdot x=(1-\eta\cdot1)\,x,\qquad y\leftarrow y-\eta\cdot10y=(1-\eta\cdot10)\,y$$&lt;/p&gt;

&lt;p&gt;In general, along an eigen-direction with eigenvalue $\lambda$, &lt;strong&gt;each step multiplies the error by $(1-\eta\lambda)$&lt;/strong&gt;. The error shrinks only if $|1-\eta\lambda|&amp;lt;1$, i.e. $0&amp;lt;\eta&amp;lt;2/\lambda$. This must hold for &lt;em&gt;every&lt;/em&gt; direction, so&lt;/p&gt;

&lt;p&gt;$$\boxed{\eta&amp;lt;\frac{2}{\lambda_{\max}}}$$&lt;/p&gt;

&lt;p&gt;This is the learning-rate rule quoted in Section 17. Starting from $(1,1)$, 50 steps give:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Learning rate $\eta$&lt;/th&gt;
&lt;th&gt;$x$ factor per step&lt;/th&gt;
&lt;th&gt;$y$ factor per step&lt;/th&gt;
&lt;th&gt;After 50 steps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$0.10$&lt;/td&gt;
&lt;td&gt;$0.9$&lt;/td&gt;
&lt;td&gt;$0$&lt;/td&gt;
&lt;td&gt;$x\approx0.005,\ y=0$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$0.19$&lt;/td&gt;
&lt;td&gt;$0.81$&lt;/td&gt;
&lt;td&gt;$-0.9$&lt;/td&gt;
&lt;td&gt;$x\approx3\times10^{-5},\ y\approx0.005$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$0.21$ (too large)&lt;/td&gt;
&lt;td&gt;$0.79$&lt;/td&gt;
&lt;td&gt;$-1.1$&lt;/td&gt;
&lt;td&gt;$x\approx0$, &lt;strong&gt;$y\approx117$&lt;/strong&gt; (blown up)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two lessons from this table:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The steepest direction limits how big a step is safe.&lt;/strong&gt; At $\eta=0.21$ the $y$ direction ($\lambda=10$) overshoots more each time (factor $-1.1$) and diverges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The flattest direction limits how fast you converge.&lt;/strong&gt; At $\eta=0.1$, $y$ is solved immediately but $x$ (with $\lambda=1$) shrinks by only $0.9$ per step. The ratio $\kappa=\lambda_{\max}/\lambda_{\min}$ (here $10$), called the &lt;strong&gt;condition number&lt;/strong&gt;, measures how bad this mismatch is. The larger $\kappa$ is, the slower gradient descent gets, because the step is held back by the steepest direction while progress is needed in the flattest.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  15.7 Newton's method
&lt;/h3&gt;

&lt;p&gt;Minimize the second-order approximation over $\boldsymbol\delta$: set its gradient to zero, giving $H\boldsymbol\delta = -\nabla f$, i.e.&lt;/p&gt;

&lt;p&gt;$$\boldsymbol\delta = -H^{-1}\nabla f$$&lt;/p&gt;

&lt;p&gt;That's &lt;strong&gt;Newton's method&lt;/strong&gt;: it uses curvature to choose both the direction &lt;em&gt;and&lt;/em&gt; the step length. It divides the gradient by the curvature in each direction, so steep and flat directions are treated alike.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worked example.&lt;/strong&gt; On the valley $f=\tfrac12(x^2+10y^2)$ from $(1,1)$: $\nabla f=(1,10)$ and $H^{-1}=\text{diag}(1,\tfrac1{10})$, so&lt;/p&gt;

&lt;p&gt;$$\boldsymbol\delta=-\big(1\cdot1,\ \tfrac1{10}\cdot10\big)=(-1,\,-1)\quad\Longrightarrow\quad(1,1)+(-1,-1)=(0,0)$$&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One step lands exactly on the minimum.&lt;/strong&gt; In any perfect quadratic bowl, Newton's method does this, whereas plain gradient descent needed dozens of steps in the table above and could not safely use a large learning rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A caveat.&lt;/strong&gt; Setting the gradient of the approximation to zero finds a &lt;em&gt;critical point of the approximation&lt;/em&gt;, which is a minimum only if $H$ is positive definite (all eigenvalues positive). Where $H$ has negative eigenvalues, Newton's method heads toward a saddle or a maximum instead. For non-convex problems like neural networks, practical versions modify or damp $H$ (for example, adding a multiple of the identity) to keep the step sensible.&lt;/p&gt;

&lt;h3&gt;
  
  
  15.8 The scale problem
&lt;/h3&gt;

&lt;p&gt;For $n$ parameters, $H$ has $n^2$ entries and inverting it costs $O(n^3)$. Take the small MNIST network from Section 13.5, with $n=101{,}770$ weights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$H$ has $101{,}770^2\approx1.04\times10^{10}$ entries, about &lt;strong&gt;41 GB&lt;/strong&gt; in 32-bit floats. That is for a &lt;em&gt;tiny&lt;/em&gt; network.&lt;/li&gt;
&lt;li&gt;A model with $n=10^9$ parameters would have a Hessian with $10^{18}$ entries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building and inverting $H$ is impossible at these sizes. Hence the next section: we never need $H$ itself, only what it does to a vector.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; the Hessian is the Jacobian of the gradient. Its eigenvalues are curvatures: they classify critical points, set the largest safe learning rate ($2/\lambda_{\max}$), and determine how fast gradient descent converges ($\kappa=\lambda_{\max}/\lambda_{\min}$). Newton's method uses $H$ to fix all of that in one step on a quadratic, but at a cost that rules it out for large models.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  16. Hessian-vector products: using curvature without building it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  16.1 The key trick
&lt;/h3&gt;

&lt;p&gt;Just like with Jacobians, we rarely need the Hessian itself, only what it does to a vector $\mathbf{v}$:&lt;/p&gt;

&lt;p&gt;$$H\mathbf{v}$$&lt;/p&gt;

&lt;p&gt;Since $H$ is the Jacobian of the gradient function $g(\mathbf{x}) = \nabla f(\mathbf{x})$, we have&lt;/p&gt;

&lt;p&gt;$$H\mathbf{v} = J_g\,\mathbf{v} = \text{JVP of } g \text{ in direction } \mathbf{v} = \frac{d}{d\varepsilon}\nabla f(\mathbf{x}+\varepsilon\mathbf{v})\Big|_{\varepsilon=0}$$&lt;/p&gt;

&lt;p&gt;In words: &lt;strong&gt;nudge the point along $\mathbf{v}$ and see how the gradient changes.&lt;/strong&gt; That rate of change of the gradient is $H\mathbf{v}$.&lt;/p&gt;

&lt;h3&gt;
  
  
  16.2 How it is computed
&lt;/h3&gt;

&lt;p&gt;The gradient $g$ itself is computed by a VJP (backprop). So an HVP is &lt;strong&gt;a JVP applied on top of a VJP&lt;/strong&gt;: "forward-over-reverse" differentiation. The cost is roughly a small constant multiple of one gradient computation (about 2 to 3 times, as a rule of thumb), in memory and time, &lt;strong&gt;without ever forming the $n\times n$ matrix&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In practice you never write $\varepsilon$ explicitly. The JVP machinery of Section 8 computes that derivative exactly, as a by-product of evaluating the gradient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An alternative: reverse-over-reverse.&lt;/strong&gt; Because $H$ is symmetric, $H\mathbf{v}=(\mathbf{v}^\top H)^\top$. And $\mathbf{v}^\top H$ is the gradient of the &lt;em&gt;scalar&lt;/em&gt; function $\mathbf{x}\mapsto\mathbf{v}^\top\nabla f(\mathbf{x})$, which an ordinary backward pass can compute. So one can also get $H\mathbf{v}$ by running backprop twice. Forward-over-reverse is typically cheaper, so it is the usual choice. A code example of both the Hessian and the HVP is in Section 18.1.&lt;/p&gt;

&lt;h3&gt;
  
  
  16.3 Example
&lt;/h3&gt;

&lt;p&gt;Take $f = x^2y + y^2$ at $(1,2)$ with $\mathbf{v}=(1,0)$. The gradient at the nudged point is&lt;/p&gt;

&lt;p&gt;$$\nabla f(\mathbf{x}+\varepsilon\mathbf{v}) = \big(2(1+\varepsilon)(2),\ (1+\varepsilon)^2+4\big) = \big(4+4\varepsilon,\ 5 + 2\varepsilon + \varepsilon^2\big)$$&lt;/p&gt;

&lt;p&gt;Differentiating in $\varepsilon$ at $0$ gives $(4,\ 2)$. And from the full Hessian,&lt;/p&gt;

&lt;p&gt;$$H\mathbf{v}=\begin{bmatrix}4&amp;amp;2\2&amp;amp;2\end{bmatrix}\begin{bmatrix}1\0\end{bmatrix} = (4,2)$$&lt;/p&gt;

&lt;p&gt;The two agree. The first route never needed the matrix $H$.&lt;/p&gt;

&lt;h3&gt;
  
  
  16.4 What HVPs enable
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conjugate gradient&lt;/strong&gt; / Newton-CG: solve $H\boldsymbol\delta=-\nabla f$ iteratively using only HVPs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power iteration / Lanczos:&lt;/strong&gt; estimate the dominant eigenvalue of $H$ (the one largest in magnitude, which for loss landscapes is usually the largest positive one, the &lt;strong&gt;sharpness&lt;/strong&gt;) by repeatedly computing $H\mathbf{v}$.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hutchinson's trick:&lt;/strong&gt; estimate $\text{trace}(H)$ via $\mathbb{E}[\mathbf{z}^\top H\mathbf{z}]$, where $\mathbf{z}$ is a random vector whose entries are independent with mean $0$ and variance $1$ (typically random $\pm1$ entries), using one HVP per sample.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; an HVP is a JVP of the gradient. It gives curvature along a direction at about the cost of a few gradient computations, so second-order information becomes available even when $H$ is far too large to build.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  17. Where all this shows up in AI
&lt;/h2&gt;

&lt;p&gt;The ideas of this guide are not just background theory: each one has a specific job in modern machine learning. The table gives a map, with pointers to the sections that explain each idea. Sections 17.1 and 17.2 add detail on the two items that need it most.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Idea&lt;/th&gt;
&lt;th&gt;Where it's used&lt;/th&gt;
&lt;th&gt;Read more&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gradient (via VJP / backprop)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Training every neural network: SGD, Adam, etc. Each step is "compute $\nabla L$ with one backward pass, then update weights."&lt;/td&gt;
&lt;td&gt;Sections 9, 13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VJP with respect to the inputs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The gradient of any scalar quantity with respect to any argument: adversarial examples (gradient of the loss with respect to the &lt;em&gt;input image&lt;/em&gt;), saliency maps, style transfer, and soft-prompt tuning.&lt;/td&gt;
&lt;td&gt;Section 13.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jacobians of layers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Understanding signal flow: the Jacobians of the layers control whether gradients &lt;strong&gt;explode&lt;/strong&gt; or &lt;strong&gt;vanish&lt;/strong&gt; through depth. This motivates ReLU, residual connections, careful initialization and normalization layers.&lt;/td&gt;
&lt;td&gt;Sections 6, 17.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JVP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Directional derivatives, curvature products, kernel analysis, and some training schemes that avoid backprop.&lt;/td&gt;
&lt;td&gt;Sections 8, 17.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hessian / HVP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Second-order optimizers; learning-rate selection (a stable step size for gradient descent on a quadratic is $&amp;lt; 2/\lambda_{\max}$); &lt;em&gt;measuring&lt;/em&gt; sharpness (the largest Hessian eigenvalue, found by power iteration); saddle-point analysis; model pruning (Optimal Brain Surgeon uses Hessian information); influence functions (which training examples affected a prediction); Laplace approximations for uncertainty.&lt;/td&gt;
&lt;td&gt;Sections 15, 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jacobian determinants&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Normalizing flows use $\det J$ to track how probability density changes under a transformation.&lt;/td&gt;
&lt;td&gt;Section 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Meta-learning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Second-order MAML needs gradients &lt;em&gt;of gradients&lt;/em&gt; (HVPs). (First-order MAML skips them.)&lt;/td&gt;
&lt;td&gt;Section 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Implicit differentiation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Differentiating through optimization problems or fixed points: the VJP is obtained by solving a linear system with the transposed Jacobian of the defining equation.&lt;/td&gt;
&lt;td&gt;Section 9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A note on sharpness: &lt;em&gt;sharpness-aware minimization&lt;/em&gt; (SAM) aims for flat minima, but it uses &lt;strong&gt;only gradients&lt;/strong&gt;. It takes a small step uphill, then computes the gradient at that perturbed point. It involves no Hessian and no HVP. HVPs enter when you want to &lt;em&gt;measure&lt;/em&gt; sharpness, as in the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  17.1 The JVP in more detail
&lt;/h3&gt;

&lt;p&gt;The JVP shows up in several places, and sometimes together with a VJP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Directional derivatives and forward-mode sensitivity.&lt;/strong&gt; "How does everything change if I nudge the inputs this way?" is exactly one JVP (Section 8).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gauss-Newton products.&lt;/strong&gt; The Gauss-Newton matrix approximates the Hessian of a loss through a network as $G=J_\theta^\top H_{\text{out}}J_\theta$, where $J_\theta$ is the Jacobian of the network's outputs with respect to its weights and $H_{\text{out}}$ is the Hessian of the loss with respect to the outputs. A product with a vector is
$$G\mathbf{v}=J_\theta^\top\Big(H_{\text{out}}\big(J_\theta\mathbf{v}\big)\Big),$$
which takes &lt;strong&gt;a JVP first and then a VJP&lt;/strong&gt;, so $G$ is never built. Hessian-free optimization uses these products. (K-FAC approximates the same kind of matrix with small Kronecker-product blocks built from per-layer statistics, and does not compute these products.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neural tangent kernel (NTK) analysis.&lt;/strong&gt; The NTK is built from the Jacobian of the output with respect to the parameters ($J_\theta J_\theta^\top$). JVPs and VJPs compute products with it without forming it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forward gradients.&lt;/strong&gt; Pick a random direction $\mathbf{v}$, compute the JVP $\nabla f^\top\mathbf{v}$ (one forward-mode pass, no stored activations), and use $(\nabla f^\top\mathbf{v})\,\mathbf{v}$ as an estimate of the gradient. It is unbiased but &lt;strong&gt;noisy&lt;/strong&gt;, so it trades memory for a harder optimization problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Differentiating through ODE solvers.&lt;/strong&gt; Forward sensitivity analysis is JVP-style. The &lt;em&gt;adjoint method&lt;/em&gt; used by Neural ODEs is VJP-based and is usually preferred when there are many parameters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  17.2 Why "vanishing gradients" is a Jacobian statement
&lt;/h3&gt;

&lt;p&gt;The backward signal at layer $i$ is&lt;/p&gt;

&lt;p&gt;$$\bar{\mathbf{x}}&lt;em&gt;i = J&lt;/em&gt;{i+1}^\top J_{i+2}^\top\cdots J_k^\top\,\bar{\mathbf{x}}_k$$&lt;/p&gt;

&lt;p&gt;a product of many matrices, where $J_j=\partial\mathbf{x}&lt;em&gt;j/\partial\mathbf{x}&lt;/em&gt;{j-1}$ is layer $j$'s Jacobian (Section 13.2). The size of this product, not of any single factor, decides what happens. The norm of a product is at most the product of the norms, which gives the simplest picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;strong&gt;every&lt;/strong&gt; Jacobian scales every vector by at most $0.5$, then after 50 layers the signal is at most $0.5^{50}\approx9\times10^{-16}$ of the original. It &lt;strong&gt;vanishes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If &lt;strong&gt;every&lt;/strong&gt; Jacobian scales every vector by at least $2$, then it grows by at least $2^{50}\approx1.1\times10^{15}$. It &lt;strong&gt;explodes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are disastrous. Real Jacobians do not scale all vectors equally (their singular values differ), so the actual behavior depends on the singular values of the &lt;em&gt;whole product&lt;/em&gt;. But the rough picture is right: &lt;strong&gt;repeated multiplication by matrices of size far from $1$ is unstable.&lt;/strong&gt; A great deal of deep learning architecture design is about keeping those Jacobian products well-behaved.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; VJPs give the gradients that train models, JVPs and HVPs give directional and curvature information, and the Jacobians of the layers decide whether those gradients arrive intact.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  18. Seeing it in code (JAX and PyTorch)
&lt;/h2&gt;

&lt;p&gt;JAX exposes JVP and VJP as first-class functions with exactly the names used here.&lt;/p&gt;

&lt;h3&gt;
  
  
  18.1 JAX
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jax.numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                      &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])])&lt;/span&gt;

&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# --- JVP: push a tangent forward -----------------------------------
&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;                       &lt;span class="c1"&gt;# input-sized tangent
&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;                  &lt;span class="c1"&gt;# jv = J @ v
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JVP:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                               &lt;span class="c1"&gt;# [2.5, 3.0, 0.5403]
&lt;/span&gt;
&lt;span class="c1"&gt;# --- VJP: pull a cotangent backward --------------------------------
&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vjp_fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vjp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;                  &lt;span class="c1"&gt;# output-sized cotangent
&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vj&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;vjp_fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                               &lt;span class="c1"&gt;# vj = J.T @ u
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VJP:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                               &lt;span class="c1"&gt;# [3.5403, 5.0]
&lt;/span&gt;
&lt;span class="c1"&gt;# --- Full Jacobian (built from many JVPs or VJPs) ------------------
&lt;/span&gt;&lt;span class="n"&gt;J_fwd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jacfwd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# n JVPs (good when few inputs)
&lt;/span&gt;&lt;span class="n"&gt;J_rev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jacrev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# m VJPs (good when few outputs)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;J_fwd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;# 3x2 matrix; J_rev is identical
&lt;/span&gt;
&lt;span class="c1"&gt;# --- Gradient of a scalar = VJP with u = 1 -------------------------
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                        &lt;span class="c1"&gt;# [4., 5.]
&lt;/span&gt;
&lt;span class="c1"&gt;# --- Hessian, and Hessian-vector product ---------------------------
&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hessian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                        &lt;span class="c1"&gt;# [[4, 2], [2, 2]]
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# forward-over-reverse: JVP of the gradient function
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fun&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;      &lt;span class="c1"&gt;# [4., 2.]  (== H @ v)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  18.2 PyTorch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;torch.autograd.functional&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jvp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vjp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jacobian&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hessian&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                        &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])])&lt;/span&gt;

&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# JVP and VJP
&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;vjp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Full Jacobian and Hessian (small problems only!)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;jacobian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hessian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# The everyday case: backprop IS a VJP with u = 1 on a scalar loss
&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;requires_grad&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;L&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backward&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;           &lt;span class="c1"&gt;# runs the backward pass (chain of VJPs)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# tensor([4., 5.])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you call &lt;code&gt;L.backward()&lt;/code&gt;, PyTorch is running exactly the reverse-mode procedure of Sections 9 and 13, starting from the cotangent $\bar L=1$.&lt;/p&gt;

&lt;h3&gt;
  
  
  18.3 A habit worth building: check against finite differences
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;numeric_jvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# central difference
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare &lt;code&gt;numeric_jvp(f, x, v)&lt;/code&gt; with the JVP from the framework (and, for a gradient, check &lt;code&gt;g @ v&lt;/code&gt; against it). If they disagree by more than the tolerances below, you have a bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Precision matters.&lt;/strong&gt; JAX and PyTorch use 32-bit floats by default, which carry about 7 digits. The numerator of a finite difference subtracts two nearly equal numbers, so the rounding error is roughly $10^{-7}/\texttt{eps}$. Making &lt;code&gt;eps&lt;/code&gt; too small makes the check &lt;em&gt;worse&lt;/em&gt;. In our example (the function from 18.1):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;&lt;code&gt;eps&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Max relative error (correct code)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;float32&lt;/td&gt;
&lt;td&gt;$10^{-4}$&lt;/td&gt;
&lt;td&gt;about $6\times10^{-4}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;float32&lt;/td&gt;
&lt;td&gt;$10^{-3}$&lt;/td&gt;
&lt;td&gt;about $3\times10^{-5}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;float64&lt;/td&gt;
&lt;td&gt;$10^{-4}$&lt;/td&gt;
&lt;td&gt;about $2\times10^{-9}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;float64&lt;/td&gt;
&lt;td&gt;$10^{-5}$&lt;/td&gt;
&lt;td&gt;about $2\times10^{-11}$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rules of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;float32&lt;/strong&gt;, use &lt;code&gt;eps&lt;/code&gt; around $10^{-3}$ and treat a relative error above about $10^{-3}$ as suspicious.&lt;/li&gt;
&lt;li&gt;For a tighter check, switch to &lt;strong&gt;float64&lt;/strong&gt; (in JAX: &lt;code&gt;jax.config.update("jax_enable_x64", True)&lt;/code&gt;; in PyTorch: create tensors with &lt;code&gt;dtype=torch.float64&lt;/code&gt;), use &lt;code&gt;eps&lt;/code&gt; around $10^{-5}$, and treat a relative error above about $10^{-6}$ as suspicious.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  19. Cheat sheet
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Shapes&lt;/strong&gt; (for $f:\mathbb{R}^n\to\mathbb{R}^m$):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$J$&lt;/td&gt;
&lt;td&gt;$m\times n$&lt;/td&gt;
&lt;td&gt;$J_{ij}=\partial f_i/\partial x_j$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JVP: $J\mathbf{v}$&lt;/td&gt;
&lt;td&gt;$\mathbf{v}\in\mathbb{R}^n \Rightarrow \mathbb{R}^m$&lt;/td&gt;
&lt;td&gt;output change per input nudge $\mathbf{v}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VJP: $\mathbf{u}^\top J$ (or $J^\top\mathbf{u}$)&lt;/td&gt;
&lt;td&gt;$\mathbf{u}\in\mathbb{R}^m \Rightarrow \mathbb{R}^n$&lt;/td&gt;
&lt;td&gt;input sensitivity of $\mathbf{u}^\top f$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradient of scalar $f$&lt;/td&gt;
&lt;td&gt;$n$&lt;/td&gt;
&lt;td&gt;VJP with $u=1$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hessian of scalar $f$&lt;/td&gt;
&lt;td&gt;$n\times n$&lt;/td&gt;
&lt;td&gt;$J$ of $\nabla f$, symmetric&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HVP: $H\mathbf{v}$&lt;/td&gt;
&lt;td&gt;$n$&lt;/td&gt;
&lt;td&gt;JVP of $\nabla f$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The four objects&lt;/strong&gt; (Section 11.4):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Lives at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$\mathbf{v}$&lt;/td&gt;
&lt;td&gt;input tangent&lt;/td&gt;
&lt;td&gt;input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$J\mathbf{v}$&lt;/td&gt;
&lt;td&gt;output tangent&lt;/td&gt;
&lt;td&gt;output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\mathbf{u}$&lt;/td&gt;
&lt;td&gt;output cotangent&lt;/td&gt;
&lt;td&gt;output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\mathbf{u}^\top J$&lt;/td&gt;
&lt;td&gt;input cotangent&lt;/td&gt;
&lt;td&gt;input&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rules of thumb:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chain rule: $J_{g\circ f}=J_g J_f$ (later function on the left).&lt;/li&gt;
&lt;li&gt;Forward mode: carries &lt;strong&gt;tangents&lt;/strong&gt; forward (multiplying by $J$, right to left), reads one column of $J$ per pass, cheap when inputs are few.&lt;/li&gt;
&lt;li&gt;Reverse mode: carries &lt;strong&gt;cotangents&lt;/strong&gt; backward (multiplying by $J^\top$, left to right), reads one row of $J$ per pass, cheap when outputs are few. Needs stored activations.&lt;/li&gt;
&lt;li&gt;Never multiply two Jacobians together and never build $J$ or $H$ for large models; always push a vector through.&lt;/li&gt;
&lt;li&gt;Backprop = reverse mode on a scalar loss = a chain of local VJPs.&lt;/li&gt;
&lt;li&gt;Linear layer: $\bar{\mathbf{x}}=W^\top\bar{\mathbf{y}}$, $\ \bar W=\bar{\mathbf{y}}\,\mathbf{x}^\top$, $\ \bar{\mathbf{b}}=\bar{\mathbf{y}}$.&lt;/li&gt;
&lt;li&gt;Elementwise activation: $\bar{\mathbf{z}}=\sigma'(\mathbf{z})\odot\bar{\mathbf{y}}$.&lt;/li&gt;
&lt;li&gt;If a value is used multiple times, its cotangents &lt;strong&gt;add&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;"The input" is whichever argument you choose to differentiate with respect to. In training it is the weights $\theta$, with the data frozen (Section 13.5).&lt;/li&gt;
&lt;li&gt;$H\mathbf{v}$ = JVP of the gradient function (forward-over-reverse).&lt;/li&gt;
&lt;li&gt;Duality identity: $\mathbf{u}^\top(J\mathbf{v})=(\mathbf{u}^\top J)\mathbf{v}$.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Curvature&lt;/strong&gt; (Sections 15 and 16):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Second-order approximation: $f(\mathbf{x}+\boldsymbol\delta)\approx f(\mathbf{x})+\nabla f^\top\boldsymbol\delta+\tfrac12\boldsymbol\delta^\top H\boldsymbol\delta$.&lt;/li&gt;
&lt;li&gt;At a critical point ($\nabla f=\mathbf{0}$): all eigenvalues of $H$ positive means a minimum, all negative a maximum, mixed signs a saddle, and zero eigenvalues are inconclusive.&lt;/li&gt;
&lt;li&gt;Gradient descent on a quadratic is stable only if $\eta&amp;lt;2/\lambda_{\max}$. Its speed is limited by the condition number $\kappa=\lambda_{\max}/\lambda_{\min}$.&lt;/li&gt;
&lt;li&gt;Newton's step is $\boldsymbol\delta=-H^{-1}\nabla f$. It reaches the minimum of a quadratic in one step, but it is a descent step only if $H$ is positive definite.&lt;/li&gt;
&lt;li&gt;$H$ has $n^2$ entries, so for large models use HVPs, never the matrix itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Checking your work&lt;/strong&gt; (Section 18.3): compare against a central finite difference. In float32 use $\texttt{eps}\approx10^{-3}$ and expect agreement to about $10^{-3}$; in float64 use $\texttt{eps}\approx10^{-5}$ and expect about $10^{-6}$.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you remember only one thing:&lt;/em&gt; &lt;strong&gt;a derivative is a machine that turns input nudges into output nudges. A JVP runs that machine forward on a nudge; a VJP runs its transpose backward on a sensitivity; backpropagation is a chain of VJPs; and the Hessian is what you get when you differentiate the derivative itself.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>learning</category>
      <category>mathematics</category>
    </item>
    <item>
      <title>Two-Phase Commit in the Real World: Why It Blocks, How Spanner Fixed It, and What to Do When Things Break</title>
      <dc:creator>Deepkumar Patel</dc:creator>
      <pubDate>Sun, 11 Oct 2026 16:46:23 +0000</pubDate>
      <link>https://dev.to/deepkpat/two-phase-commit-in-the-real-world-why-it-blocks-how-spanner-fixed-it-and-what-to-do-when-things-5fb4</link>
      <guid>https://dev.to/deepkpat/two-phase-commit-in-the-real-world-why-it-blocks-how-spanner-fixed-it-and-what-to-do-when-things-5fb4</guid>
      <description>&lt;p&gt;Imagine you’re building an e-commerce checkout. A customer clicks “Buy.” Two things need to happen: their balance has to go down, and the item’s stock has to go down. If the money moves but the stock doesn’t, you’ve charged someone for nothing. If the stock moves but the money doesn’t, you’ve promised someone a product you can’t deliver.&lt;/p&gt;

&lt;p&gt;In a single database, you’d wrap both updates in one transaction and move on. But the moment those two pieces of data live on different machines, which is common in microservice architectures, that guarantee disappears. You need a way for multiple machines to agree that a transaction either happened everywhere or nowhere.&lt;/p&gt;

&lt;p&gt;The classic answer is &lt;strong&gt;Two-Phase Commit (2PC)&lt;/strong&gt;. It works, it’s been around since the late 1970s, and it’s still used in plenty of real systems. It also has a famous flaw: it can freeze. This post walks through how 2PC works, why it blocks, how Google Spanner solved that problem, and how you can run a working 2PC demo yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The Problem Is Atomicity Across Machines
&lt;/h2&gt;

&lt;p&gt;Atomicity means a transaction is all-or-nothing. A single Postgres instance handles this for you. Once you split data across services, you have to coordinate.&lt;/p&gt;

&lt;p&gt;Our running example is a purchase that touches two independent pieces of state:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The money shard holds &lt;code&gt;users(id, name, balance)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The inventory shard holds &lt;code&gt;products(id, name, price, quantity)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both have to change together or not at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: How Two-Phase Commit Works
&lt;/h2&gt;

&lt;p&gt;2PC uses two roles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Coordinator&lt;/strong&gt; drives the transaction and makes the final decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Participants&lt;/strong&gt; hold the data and can each vote on whether the transaction can proceed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The protocol runs in two phases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb9jq4rqpyu1oa83hvhux.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb9jq4rqpyu1oa83hvhux.png" alt="Two Phase Commit Sequence" width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: Prepare (voting)&lt;/strong&gt;. The coordinator asks each participant, “Can you commit this if I tell you to?” Each participant checks its constraints (enough balance, enough stock), takes locks, and writes its intent to durable storage. Then it votes YES or NO. A YES is a promise: “I will commit if asked, even if I crash and restart.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: Commit or Abort&lt;/strong&gt;. If every vote is YES, the coordinator sends COMMIT to everyone. If any vote is NO, it sends ABORT. The decision goes to all participants.&lt;/p&gt;

&lt;p&gt;The rule is simple: all YES means commit everywhere, and any NO means abort everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: The Architectural Trap: Why 2PC Blocks
&lt;/h2&gt;

&lt;p&gt;Here’s where things get uncomfortable.&lt;/p&gt;

&lt;p&gt;Suppose both participants vote YES. They’ve locked their rows and are waiting. Then the coordinator crashes right after collecting the votes, before it sends the COMMIT message to Participant B.&lt;/p&gt;

&lt;p&gt;Participant B is now in a state called &lt;strong&gt;PREPARED&lt;/strong&gt;. It has promised to commit, so it can’t abort on its own. Other participants might have already committed, so it can’t safely guess. It also can’t commit, because it doesn’t know whether the coordinator decided COMMIT or ABORT before it died.&lt;/p&gt;

&lt;p&gt;So Participant B waits. It keeps its exclusive locks on the product row, and every other purchase touching that product queues up behind it.&lt;/p&gt;

&lt;p&gt;This is the cost of blocking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Locks held hostage&lt;/strong&gt;. A single dead coordinator can freeze rows across the system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thread saturation&lt;/strong&gt;. Requests pile up waiting on those locks, and connection pools fill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cascading latency&lt;/strong&gt;. Services that depend on the stuck rows slow down, then time out, then fail in turn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recovery usually needs a human to look at the coordinator’s logs and push the decision through by hand. In the middle of an incident, that’s a bad place to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 4: A Quick Detour: Consensus in 60 Seconds
&lt;/h2&gt;

&lt;p&gt;Before we fix 2PC, we need a building block: &lt;strong&gt;consensus&lt;/strong&gt;. You don’t need a PhD to understand the core idea.&lt;/p&gt;

&lt;p&gt;The problem with the coordinator is that it’s a single machine. If it dies, its memory dies with it. The fix is to stop trusting one machine and instead replace it with a &lt;strong&gt;group&lt;/strong&gt; of machines that agree on what happened, using an algorithm like &lt;strong&gt;Paxos&lt;/strong&gt; or &lt;strong&gt;Raft&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A few terms are enough to follow the rest of this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leader and followers&lt;/strong&gt;. One node in the group is the leader. It accepts writes and proposes decisions. The followers copy those decisions so they have the same history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The log&lt;/strong&gt;. This is an append-only sequence of state changes. Every node that has the same log has the same state. The log is the source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quorum (N/2 + 1)&lt;/strong&gt;. A decision is official only when a majority of nodes have recorded it. Quorums stop split-brain problems: if the network splits, only one side can hold a majority, so only one side can make decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With those pieces, you can replace a fragile single machine with a cluster that survives individual failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 5: How Google Spanner Fixed It
&lt;/h2&gt;

&lt;p&gt;Spanner’s key idea is to make both the coordinator and the participants &lt;strong&gt;consensus groups&lt;/strong&gt; instead of single machines.&lt;/p&gt;

&lt;p&gt;The shift looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Old model&lt;/strong&gt;: One coordinator node. One participant node per shard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spanner model&lt;/strong&gt;: Each coordinator and each participant is a Paxos group with a leader and replicas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s what changes in practice. When the coordinator’s leader decides to commit, it writes that decision to its replicated log before telling any participant. The commit decision is now durable across a majority of replicas.&lt;/p&gt;

&lt;p&gt;If the coordinator’s leader crashes mid-commit, the group elects a new leader. The new leader reads the log, sees the decision (or sees that the transaction was prepared but undecided), and finishes the job. No human has to dig through logs, and no one has to guess.&lt;/p&gt;

&lt;p&gt;That’s why Spanner doesn’t block the way naive 2PC does. A single machine failure no longer strands a transaction. The protocol’s state survives because it lives in a replicated log, not in one process’s memory.&lt;/p&gt;

&lt;p&gt;The tradeoff is cost. Every decision now requires a round of consensus, which adds latency. Spanner accepts that cost because the alternative is a system that freezes when a machine fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 6: Real-World Edge Cases and Gotchas
&lt;/h2&gt;

&lt;p&gt;Even with consensus, there are things to watch for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Participant failures&lt;/strong&gt;. What if a participant’s leader crashes while its rows are locked in the PREPARED state? Its replicas hold the prepared record. A new leader takes over, reads that record, and asks the coordinator’s replicated log for the outcome. The lock is released once the decision is known.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network partitions&lt;/strong&gt;. Quorums handle splits. If the coordinator group loses contact with part of the network, the side without a majority can’t make decisions. It waits rather than guessing. This is the right behavior, but it means some transactions will wait during a partition instead of finishing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The performance tax&lt;/strong&gt;. 2PC adds network round trips. Every write transaction pays for both phases plus consensus. Spanner has a shortcut for the most common workload: read-only transactions don’t need 2PC at all. They read a consistent snapshot at a chosen timestamp. Spanner’s TrueTime API provides tightly bounded clock uncertainty, which lets it pick timestamps that are safe across machines without coordinating. For read-heavy traffic, that removes most of the overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: The Tradeoffs at a Glance
&lt;/h2&gt;

&lt;p&gt;Here's how the three approaches compare:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Naive 2PC&lt;/th&gt;
&lt;th&gt;3PC&lt;/th&gt;
&lt;th&gt;Paxos-Backed 2PC (Spanner)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rounds of messages&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2 plus consensus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocks if coordinator dies&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Mostly no, under strict assumptions&lt;/td&gt;
&lt;td&gt;No (a new leader takes over)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavior during network partitions&lt;/td&gt;
&lt;td&gt;Can block&lt;/td&gt;
&lt;td&gt;Can produce wrong decisions&lt;/td&gt;
&lt;td&gt;Waits safely for quorum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical use&lt;/td&gt;
&lt;td&gt;Single-datacenter systems, careful ops&lt;/td&gt;
&lt;td&gt;Rarely used in practice&lt;/td&gt;
&lt;td&gt;Globally distributed databases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three-phase commit adds an extra round so participants can make progress after a coordinator crash. It works only if the network never partitions, which real networks do. That's why it's mostly a textbook idea.&lt;/p&gt;

&lt;p&gt;If you remember one thing from this post, make it this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Your coordinator's decision is data. Treat it like data: replicate it, persist it, and never let it live only in one process's memory.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;2PC is not a bad protocol. It's a protocol that exposes a hard truth about distributed systems: agreement costs something, and the cheapest version breaks when you need it most. Understanding where it blocks is the first step toward designing systems that don't.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code and crash-injection demos:&lt;/em&gt; &lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/deepkpat" rel="noopener noreferrer"&gt;
        deepkpat
      &lt;/a&gt; / &lt;a href="https://github.com/deepkpat/spanner" rel="noopener noreferrer"&gt;
        spanner
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;2PC Hands-On: E-Commerce Purchase (Go + Postgres)&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Learn &lt;strong&gt;Two-Phase Commit (2PC)&lt;/strong&gt; by running it. A purchase must update two
logical shards atomically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;users(id, name, balance)&lt;/code&gt; — money shard (participant A)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;products(id, name, price, quantity)&lt;/code&gt; — inventory shard (participant B), &lt;code&gt;price&lt;/code&gt; = cost per unit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;POST /purchase&lt;/code&gt; runs a real 2PC coordinator with durable votes
(Postgres &lt;code&gt;PREPARE TRANSACTION&lt;/code&gt; / &lt;code&gt;COMMIT PREPARED&lt;/code&gt; / &lt;code&gt;ROLLBACK PREPARED&lt;/code&gt;)
Structured logs (&lt;code&gt;slog&lt;/code&gt;) narrate every phase so you can watch 2PC happen.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;0. Mental model (60 seconds)&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;Client -&amp;gt; Coordinator -&amp;gt; Participant A (users)      Participant B (products)
              |               |                              |
              |--- BEGIN ----&amp;gt;|--- BEGIN -------------------&amp;gt;|
              |  Phase 1: PREPARE / VOTE                     |
              |  "can you commit if I ask you to?"           |
              |&amp;lt;-- YES/NO ----|                   &amp;lt;-- YES/NO -|
              |  Phase 2: COMMIT or ABORT (one decision, sent to ALL)     |
              |--- COMMIT ----&amp;gt;|--- COMMIT -----------------&amp;gt;|
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Rules: &lt;strong&gt;all YES → COMMIT everywhere; any NO →&lt;/strong&gt;…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/deepkpat/spanner" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>From Mutex to Lock-Free: How a Few Cache-Line Decisions Made a Go Pipeline 4x Faster</title>
      <dc:creator>Deepkumar Patel</dc:creator>
      <pubDate>Sat, 26 Sep 2026 07:03:05 +0000</pubDate>
      <link>https://dev.to/deepkpat/from-mutex-to-lock-free-how-a-few-cache-line-decisions-made-a-go-pipeline-4x-faster-5b11</link>
      <guid>https://dev.to/deepkpat/from-mutex-to-lock-free-how-a-few-cache-line-decisions-made-a-go-pipeline-4x-faster-5b11</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A hands-on tour of six performance-engineering techniques: cache-line padding, atomic semaphores, sharding, SPSC ring buffers, work stealing, and mmap zero-copy I/O. Explained for engineers who want to understand how lock-free systems actually work in modern Go.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Every team I have worked with eventually builds a pipeline like this one: events flow in, workers process them, and results get persisted. It usually looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One shared queue guarded by a mutex.&lt;/li&gt;
&lt;li&gt;A pool of workers popping from that same queue.&lt;/li&gt;
&lt;li&gt;A mechanism to bound in-flight work so a traffic spike does not exhaust memory.&lt;/li&gt;
&lt;li&gt;File-based persistence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That design is correct, simple, and usually the right first version. Once it shows up in a CPU profile, someone has to make it fast. This post serves as the playbook for that day.&lt;/p&gt;

&lt;p&gt;We will build the same pipeline twice in Go:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The naive version&lt;/strong&gt;: mutex queue, channel-based semaphore, and buffered file I/O. This is what most teams ship first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The optimal version&lt;/strong&gt;: N shards, each with a lock-free single-producer/single-consumer ring buffer, a Chase-Lev work-stealing deque, a cache-line-padded atomic semaphore, and an mmap zero-copy store.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here are the results upfront, measured on an 8-core machine processing 4,000,000 events:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pipeline&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Naive: mutex queue + channel sem + buffered I/O&lt;/td&gt;
&lt;td&gt;4.87 s&lt;/td&gt;
&lt;td&gt;~821K events/sec&lt;/td&gt;
&lt;td&gt;1×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimal: sharded lock-free + mmap&lt;/td&gt;
&lt;td&gt;1.21 s&lt;/td&gt;
&lt;td&gt;~3.3M events/sec&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.04×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A standalone micro-demo demonstrates a &lt;strong&gt;3.46×&lt;/strong&gt; performance difference from a single struct layout change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unpadded (both counters on one cache line): 543.5ms
padded   (each counter on its own line):    157.1ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;All concepts here transfer directly to C++, Rust, Java, or any other language running performance-critical workloads.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 0: A 90-Second Go Primer
&lt;/h2&gt;

&lt;p&gt;You do not need extensive Go experience to read this code. Five core constructs cover the mechanics:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c"&gt;// Spawn a lightweight thread ("goroutine")&lt;/span&gt;
&lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;            &lt;span class="c"&gt;// Mutual-exclusion lock&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;    &lt;span class="c"&gt;// Standard atomic integer type &lt;/span&gt;
&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;         &lt;span class="c"&gt;// Typed channel used as a token bucket semaphore&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c"&gt;// Schedule f() to execute when the enclosing function returns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Go's runtime multiplexes goroutines onto OS threads, making goroutine creation cheap. Everything else here is standard systems engineering.&lt;/p&gt;

&lt;p&gt;The domain model uses fixed-size structures:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;        &lt;span class="kt"&gt;uint64&lt;/span&gt;
    &lt;span class="n"&gt;Timestamp&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;ShardKey&lt;/span&gt;  &lt;span class="kt"&gt;uint64&lt;/span&gt;     &lt;span class="c"&gt;// Route key for shard placement&lt;/span&gt;
    &lt;span class="n"&gt;Payload&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;   &lt;span class="c"&gt;// Fixed-size work payload&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ResultRecord&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;        &lt;span class="kt"&gt;uint64&lt;/span&gt;
    &lt;span class="n"&gt;Timestamp&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;Checksum&lt;/span&gt;  &lt;span class="kt"&gt;uint64&lt;/span&gt;     &lt;span class="c"&gt;// Output from simulated work&lt;/span&gt;
    &lt;span class="n"&gt;Worker&lt;/span&gt;    &lt;span class="kt"&gt;int32&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;         &lt;span class="kt"&gt;int32&lt;/span&gt;      &lt;span class="c"&gt;// Explicit padding to keep struct aligned to 8 bytes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 1: The Naive Pipeline and Its Bottlenecks
&lt;/h2&gt;

&lt;p&gt;The naive engine relies on standard library primitives:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;NaiveQueue&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;       &lt;span class="c"&gt;// Workers share a single queue&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt;    &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;           &lt;span class="c"&gt;// Guarded by a mutual exclusion lock&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A single producer goroutine claims a permit from a channel semaphore, increments a pending counter, and enqueues the event:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ChanSemaphore&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ChanSemaphore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;TryAcquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}{}&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ChanSemaphore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If the token bucket is full, &lt;code&gt;TryAcquire&lt;/code&gt; fails and the producer yields CPU time via &lt;code&gt;runtime.Gosched()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Worker goroutines pop events, compute checksums, and write records to disk using &lt;code&gt;encoding/binary&lt;/code&gt; through a &lt;code&gt;bufio.Writer&lt;/code&gt; guarded by a &lt;code&gt;sync.Mutex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While correct and maintainable, this architecture encounters three distinct bottlenecks at high concurrency:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lock contention&lt;/strong&gt;: every enqueue, dequeue, and output write contends for locks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache-line invalidation&lt;/strong&gt;: shared queue pointers and mutex state continually invalidate L1 cache across cores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Syscall overhead&lt;/strong&gt;: writing records through reflection and mutex-guarded buffered I/O triggers frequent OS context shifts.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Part 2: False Sharing and Cache Line Geometry
&lt;/h2&gt;

&lt;p&gt;Modern CPUs do not load memory single bytes at a time. They fetch memory in 64-byte cache lines. When a CPU core updates a byte, its cache coherency protocol (such as MESI) invalidates that entire 64-byte line across all other core caches.&lt;/p&gt;

&lt;p&gt;Coherency operates on cache lines, not individual fields.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;unpaddedCounters&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;   &lt;span class="c"&gt;// Bytes 0 to 7&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;   &lt;span class="c"&gt;// Bytes 8 to 15 (same cache line)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If goroutine 1 continuously increments &lt;code&gt;a&lt;/code&gt; while goroutine 2 increments &lt;code&gt;b&lt;/code&gt;, both cores fight for ownership of the exact same 64-byte line. Every write on core 1 invalidates the line on core 2. This is &lt;strong&gt;false sharing&lt;/strong&gt;: invalidation traffic caused by independent variables occupying shared cache lines.&lt;/p&gt;

&lt;p&gt;We eliminate false sharing by padding struct fields to 64-byte boundaries:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;cacheLineSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;64&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;paddedCounters&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheLineSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;   &lt;span class="c"&gt;// 56 bytes of padding&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheLineSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;   &lt;span class="c"&gt;// 56 bytes of padding&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Running two goroutines across 20,000,000 iterations per counter yields:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== False sharing: two goroutines each hammering their own counter ===
unpadded (both counters on one cache line): 543.538652ms
padded   (each counter on its own line):    157.098564ms
padding speedup: 3.46x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Adding 112 bytes of padding yields a &lt;strong&gt;3.46×&lt;/strong&gt; speedup without altering algorithmic logic.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 3: Sharding
&lt;/h2&gt;

&lt;p&gt;Contention scales with the number of concurrent writers to shared state. Eliminating shared state removes contention structurally.&lt;/p&gt;

&lt;p&gt;Rather than operating a single queue, we split the engine into N independent shards:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2hn0jw27tx1yfj0zrc0u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2hn0jw27tx1yfj0zrc0u.png" alt="Architecture Diagram" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each shard owns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Single-Producer Single-Consumer (SPSC) ring buffer&lt;/strong&gt; for event ingress.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Chase-Lev work-stealing deque&lt;/strong&gt; for dynamic load balancing.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;cache-line-padded atomic semaphore&lt;/strong&gt; for flow control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demux goroutine hashes incoming events using MurmurHash3 to assign shards:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;shard&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShardKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numShards&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Hashing by &lt;code&gt;ShardKey&lt;/code&gt; ensures all events for a given key route to the same shard, preserving per-key processing order.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 4: The SPSC Ring Buffer
&lt;/h2&gt;

&lt;p&gt;A Single-Producer Single-Consumer (SPSC) queue has exact ownership boundaries: one goroutine writes to &lt;code&gt;tail&lt;/code&gt; and one goroutine reads from &lt;code&gt;head&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because ownership is non-overlapping, SPSC queues require zero compare-and-swap (CAS) loops. The producer only updates &lt;code&gt;tail&lt;/code&gt;, while the consumer only updates &lt;code&gt;head&lt;/code&gt;. In Go, we implement this cleanly using generics (&lt;code&gt;[T any]&lt;/code&gt;) and standard atomic types (&lt;code&gt;atomic.Uint64&lt;/code&gt;, &lt;code&gt;atomic.Pointer[T]&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SPSCRingBuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt;  &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pointer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt;

    &lt;span class="n"&gt;_&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheLineSize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
    &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uint64&lt;/span&gt; &lt;span class="c"&gt;// Consumer-owned cursor&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheLineSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
    &lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uint64&lt;/span&gt; &lt;span class="c"&gt;// Producer-owned cursor&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheLineSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewSPSCRingBuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;capacityPow2&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SPSCRingBuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;capacityPow2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;capacityPow2&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacityPow2&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"capacity must be a power of two"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;SPSCRingBuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]{&lt;/span&gt;
        &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pointer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;capacityPow2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;capacityPow2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SPSCRingBuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="n"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="c"&gt;// Buffer full&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Publish item&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SPSCRingBuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="n"&gt;Dequeue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="c"&gt;// Buffer empty&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Consume item&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Design considerations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Power-of-two bitmask indexing&lt;/strong&gt; (&lt;code&gt;tail &amp;amp; r.mask&lt;/code&gt;): replaces expensive integer division instructions with a single bitwise AND.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit cache-line padding&lt;/strong&gt;: prevents consumer updates to &lt;code&gt;head&lt;/code&gt; from invalidating producer access to &lt;code&gt;tail&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic &lt;code&gt;atomic.Pointer[T]&lt;/code&gt; storage&lt;/strong&gt;: provides compile-time type safety and avoids interface allocations or runtime type assertions.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Part 5: Work Stealing via Chase-Lev Deque
&lt;/h2&gt;

&lt;p&gt;Sharding prevents lock contention, but static hashing can cause load imbalances if certain shard keys require more processing. Work stealing addresses this by allowing idle workers to steal work from busy workers.&lt;/p&gt;

&lt;p&gt;We use a Chase-Lev double-ended queue (deque):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The owner pushes and pops work from the &lt;strong&gt;bottom&lt;/strong&gt; (LIFO order), maximizing L1 cache locality for recent tasks.&lt;/li&gt;
&lt;li&gt;Thieves steal work from the &lt;strong&gt;top&lt;/strong&gt; (FIFO order), taking the oldest tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We write the Chase-Lev deque generically using &lt;code&gt;atomic.Int64&lt;/code&gt; and &lt;code&gt;atomic.Pointer[T]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;WSDeque&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheLineSize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt;    &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheLineSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
    &lt;span class="n"&gt;bottom&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheLineSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
    &lt;span class="n"&gt;mask&lt;/span&gt;   &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt;    &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pointer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;WSDeque&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="n"&gt;PushBottom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="c"&gt;// Queue full&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;WSDeque&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="n"&gt;PopBottom&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="c"&gt;// Queue empty&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Single item remaining: race against prospective thieves&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompareAndSwap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="c"&gt;// Thief won the race&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt; &lt;span class="c"&gt;// Uncontended pop&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;WSDeque&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="n"&gt;Steal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="c"&gt;// Queue empty&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompareAndSwap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="c"&gt;// Lost race to owner or another thief&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The worker priority loop follows a set hierarchy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Drain up to &lt;code&gt;drainBatch&lt;/code&gt; (64) events from its local SPSC ring into its Chase-Lev deque.&lt;/li&gt;
&lt;li&gt;Pop locally from its Chase-Lev deque (LIFO).&lt;/li&gt;
&lt;li&gt;Attempt to steal work from a randomly selected victim shard's deque (FIFO).&lt;/li&gt;
&lt;li&gt;Yield or sleep briefly if no work is available globally.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Randomized victim selection distributes thief CAS operations across shards, avoiding centralized steal contention. In our benchmark, &lt;strong&gt;242,218 steals&lt;/strong&gt; occurred across 4M events (~6% rebalanced work).&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 6: Lock-Free Atomic Semaphores
&lt;/h2&gt;

&lt;p&gt;Channel operations involve Go runtime scheduler locks and sleeping goroutine management (&lt;code&gt;sudog&lt;/code&gt;). For microsecond-scale flow control, we replace channel semaphores with a non-blocking padded atomic counter:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;PaddedSemaphore&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheLineSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
    &lt;span class="n"&gt;max&lt;/span&gt;   &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheLineSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewPaddedSemaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PaddedSemaphore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;PaddedSemaphore&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PaddedSemaphore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;TryAcquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="c"&gt;// Permit limit reached&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompareAndSwap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PaddedSemaphore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero runtime scheduler involvement&lt;/strong&gt;: executed strictly in user space via hardware atomic instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-blocking semantics&lt;/strong&gt;: &lt;code&gt;TryAcquire&lt;/code&gt; allows the producer to yield CPU (&lt;code&gt;runtime.Gosched()&lt;/code&gt;) rather than parking the thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache-line isolation&lt;/strong&gt;: padding isolates each semaphore counter onto its own cache line.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Part 7: Persistence using &lt;code&gt;mmap&lt;/code&gt; and &lt;code&gt;unsafe.Slice&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The naive implementation calls binary serialization and buffered I/O under a mutex. The optimal pipeline maps file storage directly into process virtual memory using &lt;code&gt;syscall.Mmap&lt;/code&gt; and wraps it in a typed slice with &lt;code&gt;unsafe.Slice&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Open file, size it, and map into address space&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fd&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PROT_READ&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PROT_WRITE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MAP_SHARED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Zero-copy reinterpret of memory bytes as a ResultRecord slice&lt;/span&gt;
&lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;unsafe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Slice&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ResultRecord&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;unsafe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pointer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])),&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Writing a record requires only an atomic slot reservation and a memory store:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;MMapStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="n"&gt;ResultRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="c"&gt;// Direct memory write; kernel handles disk sync asynchronously&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Because &lt;code&gt;ResultRecord&lt;/code&gt; has fixed size and explicit byte alignment, writing directly to mmap memory avoids serialization and syscalls on the execution hot path.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 8: End-to-End Performance Benchmark
&lt;/h2&gt;

&lt;p&gt;Running both pipelines on an 8-core x86-64 Linux system over 4,000,000 events:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== False sharing: two goroutines each hammering their own counter ===
unpadded (both counters on one cache line): 543.538652ms
padded   (each counter on its own line):    157.098564ms
padding speedup: 3.46x

generating 4000000 synthetic events across up to 997 shard keys, 8 shards, GOMAXPROCS=8

=== Naive pipeline: mutex queue + channel semaphore + buffered file I/O ===
processed=4000000/4000000 duration=4.870354709s throughput=821295 events/sec

=== Optimal pipeline: sharded SPSC rings + work-stealing deques + padded atomic semaphores + mmap ===
processed=4000000/4000000 duration=1.205254565s throughput=3318801 events/sec steals=242218

end-to-end speedup: 4.04x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;strong&gt;4.04×&lt;/strong&gt; speedup results from combining these techniques into a single system design. Eliminating queue mutexes, false sharing, scheduler context switches, and I/O serialization allows all available CPU cores to execute continuously without stall states.&lt;/p&gt;


&lt;h2&gt;
  
  
  Engineering Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cache lines are the unit of hardware sharing&lt;/strong&gt;: isolate hot atomic counters to separate 64-byte boundaries to eliminate false sharing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topologically eliminate contention&lt;/strong&gt;: SPSC rings require no CAS loops because pointer ownership is single-threaded per direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combine sharding with work stealing&lt;/strong&gt;: sharding isolates hot execution paths, while work stealing handles dynamic load imbalance across shards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid runtime scheduler overhead in microsecond loops&lt;/strong&gt;: atomic CAS counters provide non-blocking flow control without parking goroutines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leverage zero-copy memory mapping&lt;/strong&gt;: memory-mapped storage combined with structured byte alignments eliminates serialization overhead on output paths.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Running the Code
&lt;/h2&gt;

&lt;p&gt;The complete benchmark source code and reproduction steps are hosted on GitHub.&lt;/p&gt;

&lt;p&gt;For representative numbers, run the benchmark on a dedicated multi-core system. Single-core environments force thread serialization and mask cache-line invalidation costs.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/deepkpat" rel="noopener noreferrer"&gt;
        deepkpat
      &lt;/a&gt; / &lt;a href="https://github.com/deepkpat/vortex" rel="noopener noreferrer"&gt;
        vortex
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Vortex — A Sharded, Lock-Free Event Pipeline in Go, Built Twice&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A self-contained, single-file (&lt;code&gt;main.go&lt;/code&gt;) demonstration of how a "naive" event-processing
pipeline — mutex queue + channel semaphore + buffered file I/O — becomes a &lt;strong&gt;4× faster&lt;/strong&gt;
"optimal" pipeline using classic performance-engineering techniques. No external
dependencies; the Go standard library only.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run it&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;go run vertex/main.go&lt;/pre&gt;

&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Run on a machine with &lt;strong&gt;≥ 4 cores&lt;/strong&gt; for representative numbers. On a single-core
sandbox the goroutines time-share one core and every effect is understated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it demonstrates&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Naive&lt;/th&gt;
&lt;th&gt;Optimal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Cache-line padding&lt;/td&gt;
&lt;td&gt;counters sharing one line (~3.5× slower)&lt;/td&gt;
&lt;td&gt;each hot field on its own 64-byte line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Semaphore&lt;/td&gt;
&lt;td&gt;buffered channel (&lt;code&gt;ChanSemaphore&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;padded atomic CAS loop (&lt;code&gt;PaddedSemaphore&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Sharding&lt;/td&gt;
&lt;td&gt;one shared queue&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NumCPU&lt;/code&gt; independent shards, hash-routed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Queue&lt;/td&gt;
&lt;td&gt;mutex-guarded slice (&lt;code&gt;NaiveQueue&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;lock-free SPSC ring buffer (&lt;code&gt;SPSCRingBuffer&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Load balancing&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Chase-Lev work-stealing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/deepkpat/vortex" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>go</category>
      <category>performance</category>
      <category>learning</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
