metakernel

The measuring instrument for the kernel-design loop, packaged as a kernel. An agent iterating on CUDA candidates is good at writing code and bad at trusting its own measurements; it needs three things it cannot reliably improvise: trustworthy timing, adversarial correctness comparison, and measured facts about the device it is targeting. metakernel ships those as loadable ops, so the generate, build, measure loop has a fixed, verified instrument at its center while the candidate code churns. It runs anywhere get_kernel runs, including headless CI runners where Nsight and CUPTI are unavailable or unprivileged, which is exactly where agent loops live.

Usage

from kernels import get_kernel
mk = get_kernel("phanerozoic/metakernel", version=1, trust_remote_code=True)

dossier = mk.probe_device()                          # measured device facts
report  = mk.bench(lambda: cand.op(x), iters=200,    # throttle-rejected medians
                   bytes=B, flops=F, dossier=dossier)
verdict = mk.compare(ref_op, cand_op, gen=make_inputs)   # pass/fail with ULP evidence
table   = mk.sweep(factory, {"BLOCK": [64, 128, 256]})   # config search

Every op returns plain dicts and tensors, never prose, so the agent parses results instead of scraping stdout.

API

Symbol Purpose
probe_device(quick=False) empirical device dossier: device-memory bandwidth per access width plus read-only, write-only, and random-gather; L2 and shared-memory bandwidth; host-transfer bandwidth (pinned/pageable H2D/D2H, D2D); pointer-chase latencies at shared memory, L1, L2, and DRAM; FMA f32/f64 pipe rates; per-dtype sustained MMA throughput (fp16/bf16/tf32/int8/fp8/2:4-sparse fp16); an atomic contention curve swept over distinct-address counts; launch overhead; grid-barrier cost; the occupancy API table AND a timed occupancy-knee table; clock behavior under sustained load
bench(fn, iters, bytes, flops, dossier, dtype, graph, clock_stride) cudaEvent bracketing with warmup; median with an order-statistic 95% CI plus p05/p95; rejection of samples whose SM clock sagged below 97% of the best observed (clock_stride=1 probes around every sample); achieved GB/s and TFLOP/s from declared traffic; roofline placement against the dossier's measured peaks; graph=True captures fn into a CUDA graph and times replays, stripping dispatch for kernels in the microsecond range, falling back to eager with the reason reported when fn is uncapturable
compare(ref, cand, gen, order_shuffle, atol, rtol, zero_atol) max/mean ULP, log2 ULP histogram, first divergent index, NaN/Inf census over single- or multi-output float or integer ops; with an order shuffle every element is judged against its own POINTWISE reduction-order band, with the fraction of elements above band reported; zero_atol judges near-zero elements by absolute difference (ULP explodes across the denormal range); the verdict carries worst_elements (index, ref, cand, ULP) so a failure is debuggable from the report alone
order_shuffles.last_dim() / .matmul_k() built-in shuffles for the common contractual-noise axes
fuzz_shapes(op, ref, make, dtypes, isolate) the ugly envelope for uni- or multi-argument ops across dtypes: 0-d, 1-d, 2-d, 3-d and zero-size shapes, non-tile-multiple sizes, transposed and strided views, storage-offset-misaligned slices, and stride-0 expansion past 2^31 logical elements to catch int32 indexing (VRAM-guarded); every case carries a repro recipe (shape, strides, storage offset, dtype); in-process runs health-check the CUDA context after exceptions, and isolate=True runs each group in a spawned subprocess so a context-killing candidate costs one group and the sweep continues
sweep(factory, param_grid, check, budget_s, race) launch-configuration search, one bench row per point, fastest first; check records a correctness gate per row so a fast wrong config cannot win; budget_s caps the sweep with skipped points visible as not_run; race=True runs successive halving, full budget only on the fastest quartile, pruned points visible
alloc_stamps(n, blocks) / read_stamps(stamps, labels) / stamped_demo() clock64 phase stamps around agent-marked phases (MK_STAMP and per-block MK_STAMP_BLOCK in metakernel_cuda/stamps.cuh); 2-D buffers decode to per-phase min/median/max across blocks; stamped_demo runs a shipped instrumented kernel end to end, the convention certified on device and a template to copy
dossier_drift(current, pinned, rel_tol) / pinned_dossiers() compare a fresh dossier against the release-pinned one for this architecture (_pinned.py ships a full dossier per certified device); per-entry relative drift, host-dependent entries skipped by default
mma_tile_check(trials) random small-integer m16n16k16 tiles through the same wmma fragment path as the timing kernels, compared exactly against the reference GEMM; pins M, N, K, and operand mapping
measure_clock() effective SM clock in GHz under an FMA spin, the primitive behind every throttle judgment
ulp_diff(a, b) elementwise ULP distance for f16/bf16/f32/f64 (integers: absolute difference); both-NaN counts 0, one-sided NaN counts 2^62
ops.mk_* the raw probe kernels (triads, read/write/gather, smem streaming, chases, FMA and MMA loops incl. sparse, atomic hammers, spinner, empty, cooperative barrier, stamped demo)

Return schemas

Every op returns plain data with a fixed schema; nothing needs parsing from prose.

probe_device() returns a dict of entries keyed device, clock, dram_bw, l2_bw, smem_bw, transfer_bw, latency, fma_f32, fma_f64, mma, atomics_f32, launch_overhead, grid_barrier, occupancy_blocks_per_sm, occupancy_knees_fma_tflops. Each measurement entry is {value, units, method, clock_ghz, confidence, note?}; value is a scalar or a flat dict of scalars; mma additionally carries verified_k_lanes: {kind: {executed_k_per_iter, vs_nominal}}; device records ecc, driver, and power_limit_w (None when nvidia-smi is unavailable).

bench() returns {median_ms, sustained_ms, median_ci95_ms: [lo, hi], p05_ms, p95_ms, min_ms, mean_ms, samples, rejected_throttled, graph_captured, clock_ghz: {best, worst}} plus graph_fallback_reason? when capture failed, achieved_gbs?/achieved_tflops?/ arithmetic_intensity? when traffic was declared, and roofline?: {bound, ridge_flops_per_byte, pct_of_roof, peak_dtype} when a dossier was supplied. median_ms is over throttle-clean windows; sustained_ms is over all samples, so power-limited parts are characterized by both.

compare() returns {pass, criterion, worst_trial, max_abs, max_rel, trials: [...], ulp_hist_log2}; each trial is {max_ulp, mean_ulp, first_divergent_index, nan_ref, nan_cand, inf_ref, inf_cand, order_band_ulp, band_score, frac_above_band, worst_elements: [{output, index, ref, cand, ulp}]}.

fuzz_shapes() returns {cases: [...], failed, total, context_lost}; each case is {shape, variant, dtype, status, contiguous?, max_ulp?, repro?: {shape, strides, storage_offset, dtype}, error?, stderr_tail?, logical_elems?, exitcode?} with status in ok | mismatch | exception | context_killed | context_lost | crashed_isolated | skipped_vram | skipped_empty.

sweep() returns a list of rows, each the grid point's params plus {median_ms, rejected_throttled, check?, achieved_gbs?, achieved_tflops?, status?, error?} with status in pruned_race | not_run for points not fully run. read_stamps() returns {phases: [{phase, ticks..., us...}], clock_ghz, blocks_reporting?, blocks_total?}. dossier_drift() returns {entries: [{path, current, pinned, rel}], worst_rel, over_tol, rel_tol, compared, pass}.

How it works

Each dossier entry is produced by a dedicated probe kernel rather than a spec sheet: streaming triads at 4/8/16-byte access widths plus read-only, write-only, and random-gather kernels for bandwidth (one-pass launches for DRAM, in-kernel repeated passes over an L2-resident working set for L2), single-lane Sattolo-cycle pointer chases for latency (a 16 KB ring for L1, a 2 MB TLB-resident ring for L2, and a chain long enough to evict itself, two L2 capacities of touched lines, for DRAM), register-resident FMA chains for the f32 and f64 pipes, register-fragment mma_sync loops per dtype for tensor-core rates (fp8 via inline PTX m16n8k32 on sm89+), an atomic hammer swept over distinct-address counts for the contention curve, empty-launch trains for overhead, and a cooperative-groups kernel for grid-barrier cost. The occupancy knee is timed, not just queried: a single dependent FMA chain per thread runs at forced (threads, dynamic smem) points, and because its issue rate is proportional to resident threads until the pipe saturates, the throughput cliff marks where occupancy stops covering latency.

Every number carries the SM clock it was measured under. The clock primitive is clock64 ticks over wall time from an FMA spinner, so throttled samples are labeled instead of silently poisoning the median: bench probes the clock every clock_stride samples and rejects samples from windows that sagged, reporting the rejection count next to the result.

The comparison ops treat the reference as a distribution, not a point: when the caller supplies an order_shuffle that permutes contractual-noise dimensions (for a matmul, permuting K in both operands changes accumulation order but not the mathematical value), the permuted references bound the legitimate numerical band. The band is applied pointwise: each element of the candidate is scored as ulp / (own_band + 2), so a candidate that is uniformly one percent wrong cannot hide behind one legitimately order-sensitive near-zero element, and a genuinely reordered reduction passes. ULP distances come from the monotone integer mapping of the float bit patterns, computed per element on device.

Certification

The test suite asserts measurement invariants that must hold on any correctly functioning device and runs against the published build variants on multiple architectures. L2 bandwidth exceeds device-memory bandwidth and random gather costs more than streaming; dependent-load latency orders shared memory below L2 below DRAM with L1 between the cache levels; tensor-core rates beat the FMA pipe and hold the structural ratios of the silicon (2:4-sparse at 1.5-2.4x dense fp16, fp16 at 1.5-2.6x tf32, int8 at 1.5-4.8x fp16); a random-operand tile through the MMA fragment path reproduces the reference GEMM exactly; a large cuBLAS GEMM lands within [0.30, 1.05] of the dossier fp16 rate; the f64 pipe lands well below f32, and fp8 reports a number exactly when the architecture has it; the atomic curve rises monotonically with distinct addresses and spans at least 4x end to end; the timed occupancy knee shows a real cliff when dynamic smem starves residency; graph-captured timing does not exceed eager timing on a dispatch-bound train; ulp_diff is exact on constructed cases (nextafter = 1 ULP, -0.0 = +0.0, both-NaN = 0); compare passes a reordered reduction and a K-permuted matmul against their own pointwise order bands and fails corrupted candidates through both routes; fuzz_shapes catches a contiguity-cheating op, exercises multi-argument ops across dtypes, and reports the 2^31 overflow case as run or as skipped_vram, never silently; sweep keeps not-run and failed points visible; the chase probe repeats within 20%.

Measured

RTX 6000 Ada (sm89, 142 SMs, 96 MB L2), single tenant, ECC enabled, driver 595.97, 300 W power limit. Values are the release-pinned dossier (_pinned.py); dossier_drift compares fresh measurements against it.

entry value
clock under sustained FMA load 2.71 GHz max, 0.3% sag over 24 windows
device-memory bandwidth triad 638-705 GB/s (4/8/16 B); read-only 771; write-only 767; random 16 B gather 218
L2 / shared-memory bandwidth 9,325 GB/s (72 MB resident set) / 21,250 GB/s aggregate
host transfers H2D 24.0 pinned / 16.6 pageable; D2H 26.2 / 15.6; D2D 331 GB/s (one direction counted)
dependent-load latency smem 30.0 cy; L1 45.8; L2 292; DRAM 649 (0.37 ns/cy)
FMA pipes f32 90.9 TFLOP/s; f64 1.28 TFLOP/s
MMA sustained (accumulator-verified) fp16 376, bf16 366, tf32 177, int8 770 (TOP/s), 2:4-sparse fp16 713 dense-equivalent TFLOP/s; fp8 194 as a flagged lower bound (see below)
atomics fp32 curve 0.72 Gop/s at 1 address up to 484 Gop/s at one per thread; smem same-address 12.9
launch overhead 5.6 us queued, 14.1 us round trip (includes torch dispatch)
grid barrier 0.54 us per grid.sync (142 blocks x 256)
occupancy knee (timed) dependent-FMA at t128: 79.9 to 44.6 TFLOP/s as dyn smem forces 12 to 2 blocks/SM; t256 81.7 at 0 KB

MMA flops derive from the accumulator read-back with every fragment element observed, never the declared loop shape; verified_k_lanes reports executed against nominal lanes, 100.0% for fp16, bf16, tf32, int8, and sparse. mma_tile_check reproduces the reference GEMM exactly through the same fragment path. The fp8 probe feeds 25% of its nominal K-lanes (vs_nominal 0.25); its entry is an executed-work lower bound at low confidence.

A 1 GiB device copy measures 635 GB/s of combined read+write traffic. An fp16 8192x8192x8192 torch.mm measures 118 TFLOP/s at the median and 183 TFLOP/s at the best sample, all 64 samples rejected as throttled (SM clock 0.81-2.65 GHz at the 300 W limit).

GeForce RTX 3070 Ti Laptop (sm86, 46 SMs, 4 MB L2), measured from the published build variants (torch 2.13, cu126):

entry value
clock under sustained FMA load 1.785 GHz, 0.0% sag
device-memory bandwidth triad 419-422 GB/s; read-only 437; write-only 436; random 16 B gather 105
L2 / shared-memory bandwidth 3,332 GB/s / 4,747 GB/s aggregate
host transfers H2D 11.8 pinned / 10.3 pageable; D2H 10.8 / 9.2; D2D 209 GB/s (one direction counted)
dependent-load latency smem 29.0 cy; L1 43.9; L2 234; DRAM 503 (0.57 ns/cy)
FMA pipes f32 19.5 TFLOP/s; f64 0.27 TFLOP/s
MMA sustained (accumulator-verified) fp16 42.0, bf16 42.0, tf32 21.0, int8 168 (TOP/s); 2:4-sparse fp16 83.9 dense-equivalent; fp8 unsupported on sm86
atomics fp32 curve 0.56 Gop/s at 1 address up to 244 Gop/s at one per thread; smem same-address 2.8
launch overhead 2.2 us queued, 5.2 us round trip (includes torch dispatch)
grid barrier 0.69 us per grid.sync
occupancy knee (timed) dependent-FMA at t128: 17.7 to 9.7 TFLOP/s as dyn smem forces occupancy down; t256 18.2 at 0 KB

Limits

No hardware counters: everything is inferred from timing, clocks, and the occupancy API, and timing-differential inference can misattribute (a bank-conflict signature and an ILP stall can look alike), so dossier entries carry a confidence field. Single device, single tenant: the probes measure whatever the GPU is doing, so a contended device surfaces as rejected samples and sagged clocks rather than an error, and a high rejection count indicates the measurement should be repeated on an idle device. launch_overhead includes torch op dispatch, the overhead a torch-launched candidate pays. The MMA numbers are dense-tile register rates with no memory traffic, an upper bound real kernels reach only when staging hides behind compute. In-process fuzz_shapes cannot survive a candidate that corrupts the CUDA context (the report marks how far it got); isolate=True survives by spawning each group, at the cost of a torch startup per child and the requirement that op/ref/make be picklable. A failed CUDA-graph capture can leave the device RNG wedged for the whole process on some torch versions; bench reports the condition in graph_fallback_reason (timing itself uses no RNG), and capturability of untrusted candidates should be probed in a subprocess. Sparse MMA is counted at dense-equivalent flops, the vendor convention. The fp8 probe feeds 25% of its nominal K-lanes, so its entry is an executed-work lower bound, not the device's fp8 ceiling. Device memory is GDDR6 on both certified cards. Published variants are Linux x86_64; on Windows load_local.py JIT-builds the same source and exposes the identical API.

The position in the loop is the point: kernel-builder compiles candidates, kernels loads them, metakernel measures and judges them, and the agent closes the loop conditioned on measured facts. It is the one component that stays fixed while everything it measures is disposable.

License

Apache-2.0.

Downloads last month
-
apache-2.0
Supported hardwares new
CUDA
8.08.68.99.010.012.0
GPU
B300
288GB
NVIDIA SXM
B200
192GB
NVIDIA SXM
H200
141GB
NVIDIA SXM
H100
80GB
GPU
H800
80GB
GPU
H20
96GB
GPU
L40s
48GB
GPU
L40
48GB
GPU
L20
48GB
GPU
L4
24GB
DGX Spark
GB10
128GB
GPU
RTX PRO 6000 WS
96GB
GPU
RTX PRO 6000 Max-Q
96GB
GPU
RTX PRO 5000
48GB
GPU
RTX PRO 4500 WS
32GB
GPU
RTX PRO 4000
24GB
GPU
RTX PRO 4000 SFF
24GB
GPU
RTX PRO 2000
16GB
GPU
RTX 6000 Ada
48GB
GPU
RTX 5880 Ada
48GB
RTX
RTX 5000 Ada
32GB
GPU
RTX 4500 Ada
24GB
RTX
RTX 4000 Ada
20GB
RTX
RTX 4000 SFF Ada
20GB
GPU
RTX 3500 Ada Mobile
12GB
GPU
RTX 2000 Ada
16GB
GPU
RTX A6000
48GB
GPU
RTX A5000
8GB
GPU
RTX A5000 Max-Q
16GB
GPU
RTX A5000 Mobile
16GB
GPU
RTX A4000
16GB
GPU
RTX A4000 Max-Q
8GB
GPU
RTX A4000 Mobile
8GB
GPU
RTX A3000 Mobile
6GB
GPU
RTX A2000
6GB
GPU
RTX A2000 Embedded
4GB
GPU
RTX A2000 Max-Q
4GB
GPU
RTX A2000 Mobile
4GB
GPU
A800
40GB
GPU
A100
80GB
GPU
A40
48GB
GPU
A30
24GB
GPU
A10
24GB
GPU
A2
16GB
RTX
RTX 5090
32GB
RTX
RTX 5090 D
32GB
RTX
RTX 5090 Mobile
24GB
RTX
RTX 5080
16GB
RTX
RTX 5080 Mobile
16GB
RTX
RTX 5070
12GB
RTX
RTX 5070 Mobile
8GB
RTX
RTX 5070 Ti
16GB
RTX
RTX 5070 Ti Mobile
12GB
RTX
RTX 5060 Ti
16GB
RTX
RTX 5060
8GB
RTX
RTX 5060 Mobile
8GB
RTX
RTX 5050
8GB
RTX
RTX 5050 Mobile
8GB
RTX
RTX 4090
24GB
RTX
RTX 4090D
24GB
RTX
RTX 4090 Mobile
16GB
RTX
RTX 4080 SUPER
16GB
RTX
RTX 4080
16GB
RTX
RTX 4080 Mobile
12GB
RTX
RTX 4070
12GB
RTX
RTX 4070 Mobile
8GB
RTX
RTX 4070 Ti
12GB
RTX
RTX 4070 Super
12GB
RTX
RTX 4070 Ti Super
16GB
RTX
RTX 4060
8GB
RTX
RTX 4060 Ti
8GB
RTX
RTX 4090 Laptop
16GB
RTX
RTX 4080 Laptop
12GB
RTX
RTX 4070 Laptop
8GB
RTX
RTX 4060 Laptop
8GB
RTX
RTX 4050 Laptop
6GB
RTX
RTX 3090
24GB
RTX
RTX 3090 Ti
24GB
RTX
RTX 3080
12GB
RTX
RTX 3080 Ti
12GB
RTX
RTX 3080 Mobile
16GB
RTX
RTX 3070
8GB
RTX
RTX 3070 Ti
8GB
RTX
RTX 3070 Ti Mobile
8GB
RTX
RTX 3060 Ti
8GB
RTX
RTX 3060
12GB
RTX
RTX 3060 Mobile
6GB
RTX
RTX 3050 Mobile
4GB
GPU
RTX 2050 Mobile
4GB
Jetson
Jetson AGX Orin 64GB
64GB
Jetson
Jetson AGX Orin 32GB
32GB
Jetson
Jetson Orin NX 16GB
16GB
Jetson
Jetson Orin NX 8GB
8GB
Jetson
Jetson Orin Nano 8GB
8GB
Jetson
Jetson Orin Nano 4GB
4GB
OS
linux
Arch
x86_64
Kernel Builder
19aaa64