Jacobians, multivariable chain rule, numeric JVP/VJP
Overview
Section titled “Overview”| Module | M04.2 · build · Python · Pass 2 · 2 to 3 h |
| You build | python/tinyllm/num/jacobian.py: jacobian, jvp_numeric (), vjp_numeric () |
| Contract | course/contracts/py/tinyllm/num/jacobian.pyi |
| Tests | course/tests/M04.2/test_jacobian.py (what they check: section 4) |
| Needs | M04.1 numerical_grad, on which vjp_numeric is built (or --ref-deps). Reading: M03.1 (matrices and their product) |
| Used by | M08.1 checks dual-number JVPs against jvp_numeric · M08.2 and M08.3 check reverse-mode VJPs against vjp_numeric and jacobian · later L3.1 checks manual backpropagation through time |
| Milestone | MS-P2 (the Pass 2 gate) |
| Optional depth | OpenStax, Calculus Volume 3 (free), section 4.5 (the chain rule for several variables); Baydin et al., “Automatic Differentiation in Machine Learning: a Survey” (2018), sections 3.1 and 3.2 (forward and reverse mode) |
Key Takeaways
Section titled “Key Takeaways”- For the Jacobian is the matrix of partial derivatives, : row is the gradient of output , column is how every output moves with input (
test_hand_example). - The chain rule in several variables is a matrix product, outer function on the left: (
test_chain_rule_is_a_matrix_product). - Autodiff never forms . Forward mode pushes a direction through, ; reverse mode pulls a cotangent back, , which is the gradient of the scalar (
test_hand_example_products). - The two products are consistent: for every and (
test_jvp_and_vjp_agree). - The JVP step must scale with : , so the input moves by whatever the size of (
test_jvp_scales_its_step_to_v).
How to work this chapter
Section titled “How to work this chapter”ol start M04.2 # stubs python/tinyllm/num/jacobian.py into your repool tests M04.2 # read the test catalog first: rung R0, you write no tests hereol check M04.2 # exit code is the verdictol check M04.2 --ref-deps # only if your M04.1 is not passing yetol diff M04.2 # after passing: your code against the reference1. Why now
Section titled “1. Why now”M04.1 checks the gradient of a scalar loss. But your autodiff engine never handles the loss as one formula: it handles a chain of layers, each mapping arrays to arrays, and its backward pass multiplies through them one at a time. The objects being multiplied are Jacobians, or rather products with Jacobians, because the matrices themselves are far too large to form (a layer from 4096 to 4096 numbers has a Jacobian of 16 million entries per token). The next three modules build automatic differentiation three ways: dual numbers (M08.1, forward mode), a scalar Value graph (M08.2, reverse mode), and closed-form VJPs of matrix expressions (M08.3). Each needs an independent numeric oracle for and . This module defines the Jacobian and the chain rule from partial derivatives and builds those oracles.
2. Principles
Section titled “2. Principles”| Symbol | Meaning | Type / shape |
|---|---|---|
| a function from numbers to numbers | Callable[[NDArray], ArrayLike] | |
| the input; any shape, flattened row-major to numbers | float64[...] | |
| output of (row-major order of ) | float | |
| the Jacobian, | float64[m, n] | |
| a tangent: a direction to move the input | shape of | |
| a cotangent: a weighting of the outputs | shape of | |
| the Jacobian-vector product (JVP) | shape of | |
| the vector-Jacobian product (VJP) | shape of | |
| , | finite-difference steps | float |
2.1 The Jacobian
Section titled “2.1 The Jacobian”Each output is a scalar function of , so it has a gradient (M04.1). Stack those gradients as rows and you get the Jacobian:
It is the best linear approximation of near : for small , the several-variable version of the tangent line. Column is how the whole output moves per unit move of input , which is also how to compute it numerically:
evaluations of , dividing by the step actually taken (M01.1). Three examples recur. A linear map has everywhere. An elementwise map like has a diagonal Jacobian, : output depends only on input . Softmax, , couples everything: , so , symmetric with columns summing to 0 (the probabilities always add to 1). M08.3 derives that one by hand. When or is a matrix, flatten both in row-major order (M03.1): the derivative with respect to of a input is column .
2.2 The chain rule
Section titled “2.2 The chain rule”If and , then a small change in moves by about , which moves by about . So
a product of a and an matrix, the outer function’s Jacobian on the left, evaluated at the inner function’s output. Entry by entry this is : add up every path from to . The one-variable chain rule is the case. A deep network is a long composition, and its Jacobian is a long product.
2.3 Products without the matrix
Section titled “2.3 Products without the matrix”Forming costs forward passes and memory. Two products avoid it:
- JVP, : the derivative of at , the rate at which the output moves when the input moves in direction . One central difference computes it: . Forward-mode autodiff (
M08.1) computes it exactly by carrying a tangent alongside every value. Along a chain, : push through one layer at a time. - VJP, : the gradient of the scalar , since .
vjp_numericcomputes it withM04.1’snumerical_grad. Reverse-mode autodiff (backpropagation,M08.2,L0.1) computes it exactly by pulling back one layer at a time: . With and a scalar loss, the VJP is the gradient; that is why training uses reverse mode.
Both products describe the same , so for every and , : one number computed two ways, which test_jvp_and_vjp_agree checks at random , , . Mixing them up (returning for a VJP) gives the right shape whenever is square and the wrong numbers unless it is symmetric.
Scale the JVP step to . multiplies , so the input moves by . A fixed with moves by 1 and measures a secant over a whole unit. Choosing keeps the largest coordinate’s move at ; then to rounding, and returns 0 without computing .
3. Worked example by hand
Section titled “3. Worked example by hand”at . Partial derivatives:
With : , the first column: moving moves the outputs at rates 4 and 5. With : , the first row: the gradient of the first output. is not symmetric, so the two differ, and a transposed Jacobian swaps them. Check the identity with and : , ; , . These are test_hand_example and test_hand_example_products.
4. The interface
Section titled “4. The interface”def jacobian(f, x: ArrayLike, eps: float = 1e-6) -> NDArray: ... # [f(x).size, x.size]def jvp_numeric(f, x: ArrayLike, v: ArrayLike) -> NDArray: ... # J v, shape f(x).shapedef vjp_numeric(f, x: ArrayLike, u: ArrayLike) -> NDArray: ... # u^T J, shape x.shapeAll arithmetic is float64 on a private copy of x. jacobian raises ValueError for a bad eps or when f’s output shape changes; the products raise it when v does not have x’s shape or u does not have f(x)’s.
What the tests check
Section titled “What the tests check”| Test | KIND | Checks | Why it matters downstream |
|---|---|---|---|
test_hand_example | unit, smoke | section 3’s | you and the tests agree on rows and columns |
test_hand_example_products | unit | and | forward and reverse mode are different products |
test_shapes_flatten_row_major | unit | of shape gives of shape in row-major columns; product shapes; shape errors | backward passes return gradients in the input’s shape |
test_linear_map_jacobian_is_the_matrix | golden | M03.1’s matmul is every layer’s first step | |
test_softmax_jacobian_closed_form | golden | , columns sum to 0 | M08.3 derives this VJP by hand |
test_elementwise_jacobian_is_diagonal | golden | ; its VJP is elementwise | activation backward passes (M01.3) |
test_chain_rule_is_a_matrix_product | differential | the rule backpropagation applies | |
test_jvp_and_vjp_agree | property, smoke | , , and at random points | the design’s property test; forward and reverse oracles |
test_jvp_scales_its_step_to_v | boundary | , , | tangents of any size |
test_inputs_are_left_unchanged_and_float64 | unit | the caller’s is untouched; sees float64 even for int | parameters are not perturbed in place |
test_rejects_bad_eps_and_changing_shapes | boundary | eps = 0 and an whose output shape changes raise | no ragged Jacobians |
5. Pitfalls
Section titled “5. Pitfalls”| Pitfall | Symptom | Caught by |
|---|---|---|
| 1. the Jacobian transposed (columns stacked as rows) | : right shape for square , wrong numbers, and a chain rule product in the wrong order | test_hand_example, test_chain_rule_is_a_matrix_product (mutant s01) |
| 2. perturbing the caller’s and not restoring it | the input array changes during a check | test_inputs_are_left_unchanged_and_float64 (mutant s08) |
| 2b. leaving coordinate at | every later column is taken at a shifted point | test_hand_example (mutant s09) |
| 3. a fixed JVP step whatever the size of | is a secant over a unit step | test_jvp_scales_its_step_to_v (mutant m04) |
| 4. a VJP that computes instead of | the column where the row was wanted | test_hand_example_products (mutant s04) |
| one-sided differences for the columns | errors near ; softmax’s closed form missed | test_softmax_jacobian_closed_form (mutant s02) |
| dividing the JVP by instead of | every JVP doubled | test_hand_example_products (mutant s03) |
| flattening through a copy | the perturbations never reach : | test_shapes_flatten_row_major (mutant s05) |
| the column difference reversed | test_softmax_jacobian_closed_form (mutant s06) | |
| a VJP that ignores | the gradient of for every | test_jvp_and_vjp_agree (mutant s07) |
6. Where it’s used next
Section titled “6. Where it’s used next”| Direction | Module | How it uses this |
|---|---|---|
| Back | M04.1 | vjp_numeric is numerical_grad(lambda z: sum(u * f(z)), [x]) |
| Back | M03.1 | matrices, row-major layout, and the product (reading) |
| Forward | M08.1 | dual numbers compute exactly; its tests compare them with jvp_numeric |
| Forward | M08.2 | the scalar Value graph computes by backpropagation, checked against vjp_numeric |
| Forward | M08.3 | closed-form VJPs of matmul, softmax, LayerNorm, RMSNorm, and cross-entropy, checked against jacobian and vjp_numeric |
| Forward | L3.1 | manual backpropagation through time, a chain of Jacobians over time steps |
Going further
Section titled “Going further”| Your piece | Production equivalent | What it adds | Where to look |
|---|---|---|---|
jvp_numeric, vjp_numeric | JAX jax.jvp, jax.vjp | exact products by forward and reverse mode, composable with jit and vmap | jax/_src/api.py |
jacobian | PyTorch torch.autograd.functional.jacobian (vectorize=True), JAX jacfwd and jacrev | the full Jacobian from JVPs or VJPs, batched; pick forward when , reverse when | torch/autograd/functional.py |
| the chain rule | reverse-mode autodiff (backpropagation) | one VJP per layer, in reverse order, storing the forward values it needs | your L0.1; Baydin et al. (2018) |
PyTorch gradcheck(fast_mode=True) | checks a whole backward with one random and instead of entries | torch/autograd/gradcheck.py |