Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
gradcheck
Finite-difference gradient checking for Rust ML frameworks.
By Henos D (@4ktLuffy) · henosd19@gmail.com
PyTorch has torch.autograd.gradcheck.
JAX has check_grads.
Rust has had nothing like it, which is roughly why I wrote this.
Here's the bug that convinced me to package it up properly:
burn#5304 — on burn's CPU backend,
a.transpose().matmul(b)returns wrong numbers for certain shapes. Nothing crashes, nothing warns.cpu -> [2.8, 2.8, -4.5, -4.5, 4.0, 4.0] ndarray/flex/metal -> [0.3, 0.3, 0.6, 0.6, -1.1, -1.1] <- correctburn computes weight gradients as
matmul(transpose(lhs), grad), so this quietly corrupts training. 33 of 512 shape combinations were affected, all sharing a parity condition. The maintainers confirmed it. It had been living in a framework with a large test suite.
Why compare against numbers, not another backend
Test CPU against GPU and you only catch the bugs they don't share. When both are wrong the same way, the diff comes back clean and you walk away happy.
A central difference doesn't have that problem. The reference comes from the definition of a derivative rather than from somebody's implementation:
∂f/∂xᵢ ≈ ( f(x + h·eᵢ) − f(x − h·eᵢ) ) / 2h
There's no code in there that can be broken the same way your autodiff is. The whole crate is that one comparison, plus the discipline of admitting when it proves nothing:
your function f, at input x
│
┌───────────────────┴───────────────────┐
│ │
the framework's gradient a central difference
(its own backward pass) ( f(x+h) − f(x−h) ) / 2h
│ │
via the Backend trait no framework involved,
— burn, or your own — just arithmetic
│ │
└───────────────────┬───────────────────┘
│
compared component by component
│
┌───────────────────────┼───────────────────────┐
│ │ │
Pass Mismatch Unchecked
agrees within the exceeds the tolerance the gradient is too
relative tolerance at any magnitude small to certify —
absence of evidence,
reported as such
The left branch is the only part that touches a framework. That separation is what stops the oracle from inheriting the bug it's meant to catch, and it's why adding a framework means implementing one trait rather than modifying the checker.
Run the negative control first
I have written harnesses that computed nothing at all and cheerfully reported everything as passing. A clean run out of one of those is worse than no run, so the control is an actual function in the API instead of something you're trusted to remember:
;
It hands the checker a deliberately corrupted gradient and panics if the checker doesn't notice, then hands it a correct one and panics if that gets flagged. Both directions, because a checker that complains about everything is just as useless as one that complains about nothing.
Put it in the same test binary as your real checks. If it ever fails, ignore every other result in that run.
Quick start
[]
= "0.1"
use ;
Check more than one shape
The burn bug above showed up in 33 of 512 shape combinations. Pick a single shape and you had roughly a 94% chance of seeing nothing wrong, so sweep instead:
use shape_sweep;
let shapes = vec!;
.assert_all_pass;
Using it with burn
burn already has an adapter, so you don't need to write one. Pick the compute backend with a feature flag:
[]
= { = "0.1", = ["burn-ndarray"] }
burn-ndarray, burn-flex and burn-cpu select which backend burn-tensor compiles
against. The adapter itself is generic over tensor rank: Burn<2> checks rank-2 functions,
Burn<4> checks rank-4 ones like convolution and pooling.
use ;
Run the crate's own burn suite:
The same checks against a different backend is the whole trick — compile twice, compare. A gradient bug that only affects one backend is invisible until you do:
Two things worth knowing before you read a result. burn computes in f32, so keep step at
1e-3 or larger — smaller steps drown in cancellation. And quadratic functions like
t.transpose().matmul(t) want 1e-1; there's a worked measurement in the header of
tests/burn_adapter.rs showing why.
A project you can copy
example-project/ is a complete, standalone crate that depends on
gradcheck over git the way yours would — no path dependency, nothing reaching back into this
repo. Copy the directory, or just run it:
It covers the negative control, a single check, a shape sweep, and one case that finds a real
defect when you point it at burn-cpu.
Adding a framework
Implement [Backend] once: build a tensor, read it back, run the function forward, and hand
back the framework's own gradient. The core never sees framework types. Adapters convert at
the boundary, which is what stops the oracle from quietly depending on the thing it is
supposed to be testing.
Picking a step size
Two errors pull against each other. Truncation error shrinks with h (as O(h²)), while
floating-point cancellation grows. 1e-3 is a sane default for f32 frameworks, 1e-5 for
f64.
If a check fails by a small margin, vary h and look again before you believe it. A real bug
won't care about the step size. A tolerance artifact will move.
Before reporting an f16/bf16 mismatch
Check both sides against f32 first. The fast path is not automatically the wrong one.
I nearly filed a bug against Apple's MLX over three configurations where mx.fast.rope
disagreed with MLX's own reference implementation. Measured against f32, the kernel turned
out to be about 60× more accurate than the reference. The reference was the broken side.
Status
Early, and I'd rather say so than oversell it. The core, the negative control, the shape
sweep and the reporting all work, and they're self-tested against a reference backend built
on forward-mode dual numbers. Dual numbers are a completely different algorithm from central
differences, so when the two agree it actually means something. cargo test runs with no ML
framework installed.
Adapters are where I'd most like help. burn is in, everything else is open.
Author
Henos D — GitHub @4ktLuffy · henosd19@gmail.com
Licence
MIT OR Apache-2.0