Benchmarks

September 1, 2026 · View on GitHub

Running them

# seconds: every algorithm, every path, small shapes, precision + device time
python scripts/bench/kernels/quick.py --dtype fp16

# the full sweep: writes one JSON per family, then draws the figures
python scripts/bench/kernels/run_all.py --out out/bench/kernels

# roll every family's JSON into one verdict table
python scripts/bench/kernels/verdict.py --dir out/bench/kernels

quick.py is the working loop — a kernel that is wrong or slow shows it at 1280x1280 as clearly as at 11008x4096. run_all.py is the expensive run: denominators first (the machine's own DRAM bandwidth and matmul peak, measured on the spot, so a figure drawn later still says what it was scored against), then each family, then the figures. Measure scripts write JSON; plot scripts read that JSON and never re-measure, so a finished run redraws without a GPU.

How the numbers are taken

  • Device time is the primary metric: per-kernel time summed from the profiler, best of three windows of 50 calls. At these sizes the wall clock is mostly Python dispatch, so a wall-based ratio compares launchers rather than kernels. Every figure draws the wall faintly behind the device line; the gap between them is the dispatch cost.
  • Arms are interleaved round-robin, each keeping its best round, so thermal drift cannot land on one arm.
  • L2 is flushed between iterations (256 MiB). Rows whose working set is L2-resident are annotated as such — a bandwidth figure above 100% of DRAM is a cache hit, not a fast kernel.
  • Accuracy is in the same row as the time, in ULP against an fp64 oracle, so no speedup is reported without the precision it was obtained at.
  • The compile arm calls torch._dynamo.reset() per case: without it the sweep hits the recompile limit and silently runs eager, which reads as a suspiciously fast baseline.

Current results

RTX 4090, fp16, geometric mean over each family's shape sweep. dev is device (kernel) time, fb is the fwd+bwd wall, vram is peak fwd+bwd memory (all ratios >1 mean ours is better).

familybackendndev/eagerdev/compilefb/eagerfb/compilevramULPeager ULP
loratriton244.873.101.461.431.189.624.6
loratilelang242.541.621.251.231.139.624.6
lora_bypasstriton41.371.190.910.951.103.85.5
lora_bypasstilelang41.291.130.951.001.073.65.5
lokrtriton162.980.930.730.891.136.16.1
lokrtilelang161.700.530.690.841.086.16.1
lokr_bypasstriton44.262.601.471.581.063.13.1
lokr_bypasstilelang41.380.841.491.611.063.13.1
ofttriton85.705.052.231.821.105.04.0
ofttilelang84.353.862.161.761.108.44.0
oft_bypasstriton37.266.152.151.891.062.42.2
oft_bypasstilelang34.593.891.931.701.064.92.2

Reading the rows that are below 1.0:

  • fb/* below dev/* — the fwd+bwd wall includes the Python dispatch of the whole autograd graph, which the kernels cannot shrink. On the small end, dispatch is the run.
  • lokr vs compile (0.93 / 0.53) — inductor fuses the both-full Kronecker rebuild about as well as a hand-written kernel does; that shape's win is memory, not time.
  • TileLang under Triton throughout — expected on Windows, where TileLang compiles through cl; the ordering on Linux is the other way around for several families.

Figures

run_all.py writes six line panels per family into out/bench/kernels/: forward latency, fwd+bwd latency, bandwidth efficiency against the measured DRAM ceiling, device-time speedup against eager, peak VRAM, and ULP. Bold is device time, faint is wall, crimson dashes are the measured ceilings of the card the run happened on.