Partial derivatives, gradients, gradcheck
Overview
Section titled “Overview”| Module | M04.1 · build · Python · Pass 2 · 2 to 3 h |
| You build | python/tinyllm/num/gradcheck.py: numerical_grad, gradcheck, and its GradcheckReport |
| Contract | course/contracts/py/tinyllm/num/gradcheck.pyi |
| Tests | course/tests/M04.1/test_gradcheck.py (what they check: section 4) |
| Needs | M01.1 central_diff, one call per coordinate (or --ref-deps) |
| Used by | M04.2 builds vjp_numeric on numerical_grad · later L0.2’s F.gradcheck_all() runs every op of your autograd library through gradcheck, behind {tinyllm} gradcheck --suite all in MS-L0 |
| Milestone | MS-P2 (the Pass 2 gate) |
| Optional depth | OpenStax, Calculus Volume 3 (free), sections 4.3 and 4.6 (partial derivatives, the gradient); Nocedal and Wright, Numerical Optimization, section 8.1 (finite-difference gradients); the PyTorch autograd notes on gradcheck |
Key Takeaways
Section titled “Key Takeaways”- A partial derivative is an ordinary derivative along one coordinate with every other coordinate frozen; the gradient collects one per input element, in the input’s shape (
test_hand_example,test_matrix_inputs_and_several_inputs). - A numerical gradient is one central difference per element, evaluations of for elements, exact on quadratics (
test_exact_on_quadratics). - An element passes when : absolute near zero, relative for large gradients (
test_tolerance_is_atol_plus_rtol). gradcheckmust reject the bugs backward passes really have (a transposed gradient, a factor of 2, one forgotten element) and point at the element furthest past its tolerance (test_rejects_transposed_gradient,test_rejects_one_zeroed_coordinate_and_locates_it,test_worst_element_is_the_most_out_of_tolerance).- It perturbs float64 copies, so the caller’s parameters are untouched and integer or float32 inputs still get exact steps (
test_inputs_are_left_unchanged,test_integer_and_float32_inputs_are_promoted).
How to work this chapter
Section titled “How to work this chapter”ol start M04.1 # stubs python/tinyllm/num/gradcheck.py into your repool tests M04.1 # read the test catalog first: rung R0, you write no tests hereol check M04.1 # exit code is the verdictol check M04.1 --ref-deps # only if your M01.1 is not passing yetol diff M04.1 # after passing: your code against the reference1. Why now
Section titled “1. Why now”L0.1 and L0.2 are next: an autograd engine and a library of about 40 differentiable ops, each with a hand-written backward rule. A backward rule is a claim, “this array is the gradient of the loss with respect to that input”, and claims need a referee. The referee is the definition of the derivative, applied one coordinate at a time: perturb one weight by , rerun the forward pass, divide. Your M01.1 gives the one-coordinate version; this module turns it into the gradient of a scalar function of several arrays and into the check that compares it with an analytic gradient. MS-L0 runs {tinyllm} gradcheck --suite all through your library, so every op you write in Pass 2 passes through this file. (The course’s own tests use a frozen copy, course/tests/_lib/gradcheck.py, so a buggy gradcheck here can fail only this module, never pass a broken op elsewhere: design decision D35.)
2. Principles
Section titled “2. Principles”| Symbol | Meaning | Type / shape |
|---|---|---|
| a scalar function of arrays (a loss) | Callable[..., float] | |
| the -th input array; one element, a multi-index | float64[...] | |
| the partial derivative with respect to that element | float | |
| the gradient with respect to , same shape as | float64[...] | |
the finite-difference step (eps), default | float | |
| , | one element of the analytic and of the numerical gradient | float |
| rtol, atol | relative and absolute tolerances, defaults and | float |
| the array that is 1 at position and 0 elsewhere | same shape as |
2.1 Partial derivatives
Section titled “2.1 Partial derivatives”For a function of two numbers, , freeze and differentiate in : . Freeze and differentiate in : . Formally,
the one-variable derivative of M01.1 along the direction. A partial derivative measures how responds to one input when nothing else moves, which is exactly the question a training step asks of each weight.
2.2 The gradient
Section titled “2.2 The gradient”The gradient lists every partial derivative: . For a function of arrays there is one partial derivative per element, and the gradient with respect to is arranged in ‘s shape, so that a training step is simply . Two facts make the gradient the right object. First, for a small change in the inputs, changes by about , the dot product of the gradient with (a first-order Taylor expansion, M02.1). Second, among all directions of length 1 that dot product is largest along the gradient, so is the direction of steepest descent (M10.1). A gradient is only defined for a scalar ; the derivative of a vector-valued function is a matrix, the Jacobian of M04.2.
2.3 Central differences per coordinate
Section titled “2.3 Central differences per coordinate”For each input and each element :
which is central_diff applied to the one-variable function at , with step . That is how numerical_grad is written: it never re-derives the formula, it calls your M01.1. The error analysis carries over: truncation , rounding about . With and values of size 1, both are near , comfortably under the tolerances below. On a quadratic, with gradient , the third derivative is zero and the numerical gradient is exact up to rounding.
Three details decide whether this is trustworthy. Copies: perturb float64 copies of the inputs, never the caller’s arrays (they are the model’s parameters). Restore: put each element back before moving to the next, or every later partial derivative is taken at the wrong point. Promote: an integer array cannot hold (it rounds back to , and the gradient comes out 0), and float32 moves only in steps of about , so the step actually taken is not . The cost is evaluations of for input elements: fine for tests on small inputs, hopeless for training, which is why backpropagation exists.
2.4 When is a gradient right?
Section titled “2.4 When is a gradient right?”Compare elementwise, with a mixed test:
Near zero the absolute term dominates (: a numerical gradient of is indistinguishable from 0); for large gradients the relative term does (: at the allowance is ). A pure relative test would reject a correct analytic 0 against a numerical ; a pure absolute test would accept a gradient of 100 that is off by 0.5 percent at a loose atol, or reject a correct one at a tight one. A nan in either gradient must fail: every comparison with nan is false, so diff > tol would let it pass; test not (diff <= tol) instead.
The report says ok, the largest absolute error, the largest relative error , and where to look: the input and multi-index of the element whose error is the largest multiple of its own allowance. That is not the largest absolute error: an error of on a gradient of 1000 is within tolerance, an error of on a gradient of is a bug.
3. Worked example by hand
Section titled “3. Worked example by hand”at , with the large step so you can do the arithmetic. The gradient is .
| Element | quotient | analytic | ||
|---|---|---|---|---|
| 4 | ||||
| 4 |
Both are exact even at : is quadratic in and linear in , so the central difference has no truncation error (section 2.3). Note the restore: the row is evaluated at , not at the 0.9 left over from the row before. A backward pass that returned (a factor of 2 lost on ) fails at element 0 with . This is test_hand_example.
4. The interface
Section titled “4. The interface”@dataclassclass GradcheckReport: ok: bool; max_abs_err: float; max_rel_err: float; worst_input: int; worst_index: tupledef numerical_grad(f, inputs: list[ArrayLike], eps: float = 1e-6) -> list[NDArray]: ...def gradcheck(f, inputs: list[ArrayLike], analytic: list[ArrayLike], eps: float = 1e-6, rtol: float = 1e-5, atol: float = 1e-7) -> GradcheckReport: ...numerical_grad returns one float64 array per input, in its shape. gradcheck raises ValueError for mismatched lists or shapes (a bug in the caller, not a numeric disagreement), and reports everything else. f must return a single number.
What the tests check
Section titled “What the tests check”| Test | KIND | Checks | Why it matters downstream |
|---|---|---|---|
test_hand_example | unit, smoke | section 3: at , ok | you and the tests agree on the definition |
test_exact_on_quadratics | property | numerical gradient of equals to | the design’s property test |
test_matrix_inputs_and_several_inputs | golden | of is and | layers have weights and inputs |
test_rejects_transposed_gradient | unit, smoke | a transposed weight gradient is not ok, worst input 0 | the most common backward bug |
test_rejects_gradient_off_by_two | unit | instead of is rejected, worst at the largest element, relative error 0.5 | a lost factor keeps every sign right |
test_rejects_one_zeroed_coordinate_and_locates_it | unit | one zeroed element of the second input is found at | the report says where to look |
test_tolerance_is_atol_plus_rtol | boundary | the mixed test at gradients and 100 | both regimes of section 2.4 |
test_worst_element_is_the_most_out_of_tolerance | unit | the worst element is the one furthest past its allowance | not the largest absolute error |
test_nan_gradient_fails | boundary | a nan analytic gradient is not ok | nan comparisons are always false |
test_inputs_are_left_unchanged | unit | the caller’s arrays come back bit for bit | they are the model’s parameters |
test_integer_and_float32_inputs_are_promoted | boundary | int and float32 inputs give exact gradients; sees float64 | steps survive the dtype |
test_default_eps_and_tolerances | unit | default step and tolerances | what L0.2 relies on |
test_rejects_mismatched_arguments | boundary | wrong counts and shapes raise ValueError | caller bugs are not numeric errors |
test_rejects_non_scalar_f | boundary | a vector-valued raises; a 1-element array is fine | gradients are for scalars |
5. Pitfalls
Section titled “5. Pitfalls”| Pitfall | Symptom | Caught by |
|---|---|---|
1. combining the per-element verdicts wrongly (all instead of any) | a transposed or partly zeroed gradient is reported ok | test_rejects_transposed_gradient, test_rejects_one_zeroed_coordinate_and_locates_it (mutant s05) |
| 2. not restoring an element before the next one | every later partial derivative is taken at a shifted point | test_hand_example (mutant s03) |
| 2b. perturbing the caller’s arrays in place | the model’s weights change during a check | test_inputs_are_left_unchanged (mutant s09) |
| 3. perturbing in the caller’s dtype | gradient 0 for int inputs; two digits for float32 | test_integer_and_float32_inputs_are_promoted (mutant s10) |
4. diff > tol as the failure test | a nan gradient passes | test_nan_gradient_fails (mutant s08) |
| a one-sided difference per coordinate | errors near instead of ; quadratics no longer exact | test_exact_on_quadratics (mutant s01) |
| dividing by (the versus confusion) | every numerical gradient halved | test_hand_example (mutant s02) |
| differentiating or comparing only the first input | the input gradient is never checked | test_matrix_inputs_and_several_inputs (mutant s04), test_rejects_one_zeroed_coordinate_and_locates_it (mutant s06) |
| reporting the largest absolute error as the worst | the report points at a correct large gradient | test_worst_element_is_the_most_out_of_tolerance (mutant s07) |
6. Where it’s used next
Section titled “6. Where it’s used next”| Direction | Module | How it uses this |
|---|---|---|
| Back | M01.1 | numerical_grad calls central_diff(along, x_i, eps) for every element |
| Forward | M04.2 | vjp_numeric(f, x, u) is numerical_grad of the scalar |
| Forward | L0.2 | F.gradcheck_all() checks every op’s backward with your gradcheck, behind {tinyllm} gradcheck --suite all in MS-L0 |
| Forward | M10.1 | gradient descent steps along |
| Forward | L4.1 to L7.9 | from rung R5 your own tests gradcheck every backward you write (DESIGN 5.12) |
L0.2 joins used_by when it is authored (course/DEVIATIONS.md row B31-02).
Going further
Section titled “Going further”| Your piece | Production equivalent | What it adds | Where to look |
|---|---|---|---|
gradcheck | PyTorch torch.autograd.gradcheck and gradgradcheck | complex inputs, sparse and batched layouts, second derivatives, a fast mode that checks random projections instead of every element | torch/autograd/gradcheck.py |
numerical_grad | JAX jax.test_util.check_grads | checks forward and reverse mode up to a chosen order | jax/_src/public_test_util.py |
atol + rtol * abs(n) | numpy.isclose | the same mixed test (and the same asymmetry: relative to the second argument) | numpy/_core/numeric.py |
| element-by-element checks | directional checks | one per random pair costs 2 evaluations instead of ; M04.2 builds the pieces | PyTorch fast_mode=True |