Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Validation against reference implementations

A kit that ports a published model makes a claim: it computes what the original computes. This page describes how bunsen tests that claim. The two validation crates are the worked examples, and their own docs carry the details and the commands: whisper-model-validation and silero-model-validation.

Why unit tests are not enough

bunsen’s Whisper and Silero kits are transliterations, written by reading the reference. Their unit tests check the code against itself, so a misreading that the code and its tests share passes: weights that load scrambled, or a ReLU where the reference uses GELU, leave a unit suite green. Validation pins a kit to something with independent provenance.

A reference that runs inside burn

The reference is the upstream model’s ONNX export, converted to Rust by burn_onnx::ModelGen. Both implementations then run inside burn, on the same backend, so a disagreement is a bunsen bug and not a difference between frameworks. The export has to be the same model as the checkpoint bunsen loads. Whisper’s is a conversion of OpenAI’s base checkpoint, so the two agree only if bunsen’s loader and its forward pass are both right.

The reference lives with the validation crate, or with the bundled-asset crate that supplies it, and never in bunsen. It exists to be disagreed with, and shipping it would put a second implementation of the model on the public surface.

Staged, then composed

There are two kinds of test, because each catches what the other lets through.

Staged tests feed each stage synthetic input and compare it with the same stage of the reference, on identical weights. An error can’t be inherited from upstream, so a failure names the stage that is wrong. A decoder’s logits and its argmax are checked separately: across tens of thousands of classes an elementwise tolerance can pass while the predicted token differs, and the token is what a decoder is judged on.

Composition tests run the whole model on real input and judge the result the way a user would. Whisper transcribes real speech and is scored by word error rate, against a ground-truth transcript and against openai-whisper’s own decode of the same clip. Silero runs a real recording through its streaming context and pins the per-chunk probabilities. Every stage can agree within tolerance while the composition diverges, because a greedy decode turns a small numerical difference into a different word.

Tolerances from measured drift

The binding constraint is the backend, not the implementations. Two correct implementations, several blocks deep, drift apart by as much as the backend’s arithmetic does, and that differs by backend: CUDA’s reduced-precision matmul drifts an order of magnitude further than wgpu. So a tolerance is measured on each backend and set with headroom over the worst. It is still far tighter than the error a real defect causes: a scrambled weight or a wrong activation is off by 100% or more (Tolerance).

Fixtures generated once

Some references are not Rust and won’t be: librosa’s mel filterbanks, openai-whisper’s decodes. bunsen’s tests never call them. A script under tools/ runs the reference once and writes its outputs into a testdata/ directory, and the outputs are committed. The Rust tests read the files. Nothing shells out to Python, and the reference is not a build or test dependency.

Regenerating a fixture is a deliberate manual step, never part of CI. Each fixture script’s docstring pins the tool versions it was run with and says how to set up its environment. The scripts run the reference on the CPU, because a fixture only has to be correct, and a CPU run is deterministic. Where the inputs came from, and under what licence, is recorded in the testdata/ READMEs, such as Whisper’s speech fixtures.

The same pattern pins data. tools/gen_shard_digests.py reads a dataset’s listing at a pinned revision and writes the digest table that a shard set verifies against.

Running validation

The Whisper suite takes three features, and none implies another: download fetches the assets it compares against, gpu-tests compiles the tests that load a model, and a backend feature says what they run on. Without gpu-tests only its fixture-integrity checks run, and that is all of it that CI runs: CI turns on neither download nor gpu-tests for any validation crate. The full suite is run by hand, in release mode, with a backend; the README has the command. Without a backend feature the suite still passes, having measured the CPU (Testing and backends).

Validating a new kit

A kit that claims parity with an upstream implementation gets a crate under crates/validation/. Its reference and the checkpoint it is compared on come through a bundled-asset crate, digest-pinned (Assets, bundling and the network), and the crate tests in both layers: staged on synthetic input, composed on real input with a measured tolerance. The workspace lists the validation crates there are.