Skip to main content

testing

How to write and run tests and benchmarks in this repo. Use whenever adding, changing or reviewing a test in packages/runntime/test, adding or changing a kernel or a kernel route, fixing a bug, or benchmarking a change.

Source facts

Repository
software-mansion/runntime
Last source activity
October 21, 2026 at 10:12
Detected SKILL.md language
English
Stars
121
Forks
6

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
testing
description
How to write and run tests and benchmarks in this repo. Use whenever adding, changing or reviewing a test in packages/runntime/test, adding or changing a kernel or a kernel route, fixing a bug, or benchmarking a change.
# Testing - Vitest, configured in `packages/runntime/vitest.config.ts`. `pnpm test` runs everything, `pnpm --filter runntime test:unit` only the Node tests, `pnpm --filter runntime test:gpu` only the GPU tests. - Tests live in `packages/runntime/test`, never in `src`, and the test path mirrors the source path: `src/core/weights/convert.ts` is tested in `test/core/weights/convert.test.ts`. Import the source with its `.ts` extension. - `*.test.ts` runs in Node with no GPU. `*.gpu.test.ts` runs in headless Chromium on the machine's real GPU (Vitest browser mode, Playwright). `test/gpu.ts` gives it the device (`gpu()`), uploads freed when the test ends (`upload`), feature skips (`requireFeature`), the float check (`expectClose`), seeded inputs (`randomF32`) and f16 rounding for references (`roundF16`). CI runners have no GPU, so the GPU tests run locally: run them before pushing a change to a kernel or its dispatch. - Write the test once and put the cases in a list that `it.each` runs. Adding a case should be one line. - Expected values come from the math, worked out by hand or from Hugging Face / PyTorch, never from running our code and copying its output. A CPU reference for a kernel is a plain loop written from the formula, not one that reuses the kernel's helpers, or a bug in them passes in both. - A bug fix adds a case that fails without the fix. - Compare floats with a tolerance (`|got - want| <= atol + rtol * |want|`) and let NaN fail. For f16, round the inputs to f16 before computing the reference. - No snapshots of generated WGSL. They break on every refactor and catch nothing. - A test that needs a device feature the machine lacks (`shader-f16`, `subgroups`) skips with a reason. It never passes silently. - A known bug that is not fixed yet is an `it.fails` case with a comment naming the bug. The fix turns it into a plain `it`. ## Kernels - Every kernel has a `*.gpu.test.ts` suite whose path mirrors the kernel's file under `src/core/kernels/`: `kernels/matmul/matmulGemv.ts` is tested in `test/core/kernels/matmul/matmulGemv.gpu.test.ts`. It runs the kernel through its op on a real device and checks the output against golden values from a trusted source (PyTorch, Hugging Face) or, where none fits, a plain CPU loop written from the formula. - The cases cover, where the kernel allows them: very large inputs (buffers past 2^24 elements, near `maxStorageBufferBindingSize`), empty inputs, a dtype other than the declared one, sizes past a kernel's limit (it throws, it does not return garbage), and unusual shapes (1×1, odd sizes, sizes that are not a multiple of the tile or vec4 width). - Cover every route a kernel can take: matmul alone has plain, gemv, gemvVec4, smallm, tiled and tiledVec4, each picked by shape and device features. - A new route in a kernel lands with the cases that cover it fully: every dtype it runs, every option it handles, and shapes on both sides of each threshold that picks it, so the cases show which inputs take the route and which fall back. - A case written for one route says so in a comment: the route it takes and the condition that picks it (`// smallm: m % 4 == 0, m <= 32, subgroups`). When the condition changes, the comment shows which cases to move. - Run the suite after every meaningful change while tuning a kernel. A change to a kernel already on `main` also gets an A/B run against `main`'s output: any difference that was not intended means the suite is missing a case, so add it. ## Zoo - Each model is tested against its PyTorch reference on the same inputs, corner cases included. Where our output differs on purpose, the test says why. ## Performance - A change to kernels, `core` or the zoo is benchmarked on the models it touches, before and after. The pull request description carries the A/B report. Performance stays the same or gets better. - Benchmarks always run on the same machine, and every run is saved to a file, so a slow regression over many PRs shows up. - A change to a kernel gets a kernel A/B against `main`: `pnpm --filter runntime bench:ab kernels [-b <ref>] [-k matmul,reduce]`. It copies the base ref's `src` to `.bench/base`, runs both copies in one headless Chromium page on the same inputs with their rounds interleaved, and prints a table, with a markdown copy for the pull request in `.bench/results/report.md`. A row says `faster` or `slower` only past the noise: both versions' interquartile spread, at least 3%. - The table's last column is the largest difference between A's and B's outputs. Anything but 0 on a kernel the change did not mean to touch is a bug, or a case the GPU tests are missing. - Cases live in `bench/kernels/<kernel>.ab.ts`, one per route the dispatch can pick, with shapes from the zoo models. A new route gets a case. - Close the GPU-heavy apps before a run, and run on power. A row that flips between runs is noise, not a result.
View on GitHub