Skip to main content

usv - Unladen Swallow Velocity

GPU micro-benchmarking for AMD and NVIDIA, in the spirit of asv but geared for GPU kernels.

usv follows the familiar triton.testing.do_bench / torch.utils.benchmark style - time a plain callable - and adds optional accuracy features that matter for GPU micro-benchmarks:

  • Interleaved scheduling - time a group of callables together and collect samples round-robin, so time-correlated noise (thermal ramp, DVFS, neighboring jobs) spreads across every benchmark instead of biasing one contiguous block.
  • Cache flushing - zero an L2-sized scratch buffer before each sample to measure cold-cache kernel cost.
  • Rotating buffers - cycle inputs through a ring so back-to-back launches see different memory, reducing cache-residency bias without a full flush.
  • Event-based GPU timing - per-call cuda.Event timing (works on NVIDIA and AMD/ROCm via PyTorch), with a CPU wall-clock fallback for development off-GPU.

Install

pip install -e .           # runtime (numpy + torch)
pip install -e ".[dev]"    # + test tooling

Quick start

Time a single kernel:

import torch
from usv import do_bench, rotating

bufs = [torch.randn(1024, 1024, device="cuda") for _ in range(8)]
nxt = rotating(bufs)                       # rotating input buffers

m = do_bench(lambda: nxt() @ nxt(), flops=2 * 1024**3)
print(m)                                   # 0.1234 ms +/- 0.0021 (median+/-std, n=100)  17.4 TFLOP/s
print(m.median, m.tflops)

Interleaving

A single do_bench(fn) can't interleave - it only sees one kernel. To interleave, hand all the callables to do_bench_many at once; it collects samples round-robin (one per callable per round, in a reshuffled order), so slow drift lands on one sample of each benchmark rather than a contiguous block of one:

from usv import do_bench_many, format_table

fns = {f"matmul[N={N}]": make_matmul(N) for N in (512, 1024, 2048)}
results = do_bench_many(fns, iters=100, interleave=True)   # dict[name -> Measurement]
print(format_table(results.values()))

Interleaving is opt-in. By default (interleave=False) each callable is timed to completion in turn (equivalent to calling do_bench in a loop).

Writing benchmarks

A benchmark is just a callable of no arguments that launches the work to time. Close over any state (tensors, streams) you set up beforehand:

def make_matmul(N, nbuf=8):
    bufs = [torch.randn(N, N, device="cuda") for _ in range(nbuf)]
    nxt = rotating(bufs)
    return lambda: nxt() @ nxt()

API

do_bench(fn, ...) -> Measurement

Argument Default Meaning
fn - Callable of no args to time.
warmup 50 Untimed calls before timing (a first prime call is always discarded).
iters 100 Number of timed samples.
inner 1 Calls per timed sample, or "auto" to fill target_window_s.
target_window_s 1e-3 Target window duration for inner="auto".
min_warmup_time None If set (s), warm up until this much kernel time elapses (floor on warmup).
min_iters_time None If set (s), sample until this much kernel time elapses (floor on iters).
cache_flush False Zero an L2-sized buffer before each sample.
flush_mb None Flush buffer size in MB; None sizes it from the device L2 cache.
lock_clocks False Pin GPU clocks to the device max for the run (see fixed_clocks for a specific MHz).
timer "auto" auto | torch | wall, or a GPUTimer.
name "" Label for printing.
flops / bytes None Per-call work -> TFLOP/s / GB/s columns.

do_bench_many(fns, ...) -> dict[str, Measurement]

Same knobs as do_bench, plus:

Argument Default Meaning
fns - {name: callable} to time together.
interleave False Collect samples round-robin across fns.
flops / bytes None {name: value} maps for throughput columns.

Measurement

Holds the raw per-call samples (a numpy array, already divided by inner) and exposes median, mean, std, min, max, n, quantile(q), and - when flops/bytes are set - tflops / gbps.

Stable clocks

A big source of run-to-run variance in GPU micro-benchmarks is usually not the kernel - it's the GPU changing its clock (DVFS / boost / thermal throttle) between runs. Interleaving (above) spreads that noise across benchmarks, but for reproducible absolute numbers you want to pin the clocks to a fixed frequency before measuring:

Triton does this in an opt-in helper, triton.testing.set_gpu_clock(ref_sm_clock, ref_mem_clock), which hard-codes a reference clock. usv provides usv.fixed_clocks() - a cross-vendor context manager that locks to the device's max clock (NVIDIA locks SM + memory clocks, AMD forces performance level HIGH) and restores them on exit:

from usv import do_bench, fixed_clocks

with fixed_clocks():   # needs privileges (usually sudo)
    m = do_bench(lambda: x @ x)

Because it changes global GPU state and usually needs sudo, it is never run automatically - use it only when you need reproducible absolute numbers.

For a single measurement you can pass it inline instead: do_bench(..., lock_clocks=True) does the same lock around that one call; use the fixed_clocks() context manager to lock once around a loop or sweep. (Locking to the absolute max can still throttle under sustained load; a slightly sub-max clock - set manually via the commands above - is often more reproducible.)

License

MIT

Metadata

Release files for usv 2026.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for usv 2026.1
File Size Uploaded Provenance
usv-2026.1.tar.gz 18.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for usv 2026.1
File Interpreter ABI Platform
usv-2026.1-py3-none-any.whl Python 3 none any Details

Total release size: 33.8 kB

Release files / usv-2026.1.tar.gz

Download URL usv-2026.1.tar.gz
Size 18.5 kB
Tags Source
SHA-256 checksum
How to use checksums
3026f7ca25269d8a1524e531c8c868d92ee27c716632b3127685928f16b5cf5b
BLAKE2b-256 checksum
How to use checksums
5197e5ec0fd4b0673320cbb99c12d5868285bf3b14b24a33a192e6e7dfd4ed67
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.0

Release files / usv-2026.1-py3-none-any.whl

Download URL usv-2026.1-py3-none-any.whl
Size 15.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cb0ec06fd9c82b6bc1720a7bf0cc91a483a2e66b0732603002ea92ac12c5fef0
BLAKE2b-256 checksum
How to use checksums
9795e03c64c3dcf5bdb77c746a421ae1c44b4afc1d14606efbc79bf9d7c91a88
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.0

Security for usv 2026.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

2026.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page