Status: current.
batchlas_tune times every entry of an op's candidates<T>() over the op's grid, appends the results to the ledger benchmarks/results/tuning/ledger/<op>.<device>.jsonl, and writes tuned/<op>.<device>.txt from it through scripts/sweep_to_table.py (the tuning data layout). Candidates are pinned with select::ScopedPin through the public entry point. The output tables are described in tuned/README.md. The selection design is in flat kernel selection §6.
cmake --build build --target batchlas_tune # needs -DBATCHLAS_BUILD_BENCHMARKS=ON
The build produces two binaries:
Tiered runs are the normal workflow: plan, measure, inspect, write tables.
# plan: cells, skips and estimate; no GPU
batchlas_tune potrf --tier preview --dtype float,double --devices 1 --plan
# measure into the ledger
batchlas_tune potrf --tier preview --dtype float,double --devices 1
# what each ledger holds
batchlas_tune --status
# ledger tables into a scratch directory
python3 scripts/sweep_to_table.py --op potrf --device sm_89 --out /tmp/tables
# tiered, several ops, tables in a scratch directory
batchlas_tune potrf,trsm,posv --tier preview --dtype float,double --devices 1 --out /tmp/tables
batchlas_tune all --tier coarse --devices 1,2 --budget 6 --progress-fd 3 3>events.jsonl
# import a schema-1 raw sweep as a deep run
batchlas_tune --import-raw /tmp/raw/trsm.float.sm_120.jsonl
# custom protocol: raw JSONL per dtype; with --ledger also a custom run there and, with --out, tables
batchlas_tune potrf --dtype float,double,cfloat,cdouble --devices 1 \
--raw /tmp/raw --ledger benchmarks/results/tuning/ledger --out tuned
# smoke test
batchlas_tune posv --devices 1 --dtype float --n-list 24,32 --nrhs-list 2 --batches 8192 --uplo L \
--reps 8 --warm 0.5 --raw /tmp/raw
# uplo/diag invariance A/B (raw only; sweep_to_table refuses a ledger that varies a hidden axis)
batchlas_tune trsm --devices 1 --dtype float --grid uplo=L:U --grid diag=N:U --no-refine \
--grid order=16:64:256 --grid q=8:128 --batches 8192 --raw /tmp/ab
# list ops, key names, candidates per dtype and the current kernel hash
batchlas_tune --list
Modes:
A rerun measures only what is missing, stale or below the requested tier. A repeated preview run skips every cell (skip:current) and exits in seconds. After a kernel edit only the cells whose families changed are re-raced (partial:<family>).
| flag | default | meaning |
|---|---|---|
| <op> | required (not for --list, --status, --import-raw) | one op, a comma list or all (every registered spec). Tiered runs take the ops in dependency order ("Op order" below) and skip, with one note, a dtype a spec refuses. Gate and custom runs take one op |
| --tier | none | preview, coarse or deep; custom with --remeasure-keys: exactly the named cells, every candidate, no lattice and no refinement (a rehash sample, kernel hashes) |
| --plan | off | print cells, skips with reasons and the estimate, then exit. No GPU |
| --budget H | none | stop refinement after H hours; the starting lattice always completes. --plan warns if its estimate exceeds H |
| --only-named | off | with --remeasure-keys and a tiered --tier: round 0 is exactly the named cells at the run's tier, no lattice and no refinement (how the cells a rehash sample disagrees on are re-measured at deep) |
| --remeasure-keys FILE | none | lines <op> <dtype> <name=v,...> (the ledger key, in its field order): those cells are measured again in round 0 at the run's tier whatever the ledger holds, refinement midpoints and cells past --max-dim included (not past --cap-gib). Every op and dtype named must be in the run. Other cells the tier plans (stale or missing lattice cells, refinement) are measured too. See "Engine: re-measuring named cells" in tiered tuning |
| --progress-fd N | none | one JSON event per line on fd N |
| --ledger DIR | <repo>/benchmarks/results/tuning/ledger | ledger root, one <op>.<device>.jsonl file per table, every dtype in it. Custom runs record here only when given |
| --allow-dirty | off | measure into a ledger from a --repo tree with uncommitted changes to tracked files. Without it such a run is refused; with it the run is stamped <sha>-dirty and every reader leaves it out of the tables |
| --device-key sm_NN | compute capability of the first --devices GPU | device whose ledger --plan reads |
| --cell-overhead-s | 0.49 | per-child start-up in the estimate |
| --no-worker | off | race every cell in a fresh --cell --mode race child instead of the per-GPU worker |
| --audit-fraction F | preview and coarse 0.02; deep 0.10 | share of worker cells re-raced in a fresh child |
| --status | op x dtype x device matrix: runs, cells, tier mix, stale and partly stale counts, newest run and age, table source, tier mix and date. No GPU | |
| --import-raw F | schema-1 raw sweep into the ledger as a deep run; op, dtype and device come from its meta | |
| --dtype | float (gate with --old-csv: every dtype in the CSV) | comma list; one raw file and one table per dtype |
| --devices | required (not for --plan) | GPU indices in nvidia-smi / PCI order. Inside CUDA_VISIBLE_DEVICES when that is set; UUID lists are refused |
| --raw DIR | <repo>/benchmarks/results/tuning | custom: raw JSONL (Git LFS) |
| --out DIR | none | table directory: after the run, sweep_to_table.py --op <op> --device <dev> regenerates each measured op's table from the ledger (a custom run without --ledger refuses --out) |
| --reps, --warm, --passes | 16, 1.5 s, 2 | custom: §6.3 protocol |
| --remeasure | 0.10 | custom: re-measure a cell once when pass medians differ by more |
| --refine-ratio, --no-refine | 1.1 | custom: §6.2 bisection stops when hi/lo < ratio |
| --no-jit | off | custom: skip the throwaway JIT pass |
| --cap-gib | 4 | skip cells whose matrices exceed this |
| --max-dim | 2048 tiered, off custom | skip:dim: no cell with a matrix dimension (m, n, k, order, q, nrhs, derived rows; never batch) above this; 0 = off |
| --ld-pad | 0 | custom: ld = n + pad (non-natural leading dimension) |
| --n-list, --batches, --nrhs-list, --uplo, --grid key=v1:v2 | the op's choice.hh grid | replace whole grid axes |
| --no-guard, --guard-wait, --util-ceiling | on, 300 s, 5 % | idle guard (see "Guard") |
| --allow-idle-foreign | off | tolerate another user's idle CUDA contexts (see "Guard") |
| --lock-dir | /tmp | per-GPU flock files batchlas_tune_gpu<N>.lock |
| --cell-timeout | 1800 s | a child running longer is killed and counts as a failed child |
| --gate, --old-csv, --parent-bin, --gate-csv, --gate-limit | --gate-limit 1.05 | gate mode |
Parameters and record rules: tiered tuning ("Engine: tiers and the per-cell algorithm"; "Engine: the ledger and table generation"). The driver is tiered_driver.cc and planning is in schedule.cc.
Progress events (--progress-fd), one JSON object per line; every event also carries t, Unix time in seconds:
| event | fields |
|---|---|
| plan | cells, est_s (lattice only), refine_cells, est_refine_s, est_total_s |
| plan_job | one per (op, dtype), before plan: op, dtype, device, cells, measure, mix (12 current, 300 measure), est_s, refine_cells, est_refine_s |
| refine_cap | op, dtype, lattice, refined, cap, dropped |
| cell_start | op, dtype, key fields, gpu |
| cell_done | op, dtype, key fields, ranked (a\|b), tier, round (absent on a single-candidate cell) |
| eliminated | op, dtype, key fields, cand, round |
| audit | op, dtype, key fields, verdict, fresh_ms, warm_ms, fresh |
| worker_restart | op, dtype, key fields, gpu, restarts, fallback |
| ld_audit | op, dtype, key fields, cand, verdict (fail), reason |
| ld_audit_summary | op, dtype, passed, skipped, failed, failures |
| schedule | switches, restarts, switch_restarts |
| done | none |
--devices 1,2,3 runs one child per GPU at a time, shards cells round-robin, and keeps both passes of a cell on one GPU. Each GPU is held under an exclusive flock for the run. Children get CUDA_DEVICE_ORDER=PCI_BUS_ID and CUDA_VISIBLE_DEVICES=<gpu>. Every listed GPU must report the same device key, because one run writes one device's tables.
Note: Running several GPUs at once departs from the one-measuring-process rule in agent guide §10. Use one device on a shared box, or when a verdict hinges on a few percent.
Before each child: no compute process on the GPU and utilization at most --util-ceiling. Otherwise the tuner waits up to --guard-wait and then stops. After each child: a compute process on the GPU discards that child's numbers and the child is retried. Any process with a context counts, and an entry nvidia-smi shows as [N/A] counts as foreign. The tuner warns at start when a listed GPU drives a display, or when another GPU has compute processes. Without nvidia-smi the tuner stops; --no-guard skips the guard.
--allow-idle-foreign tolerates other users' idle contexts while utilization is at most --util-ceiling. Each child remembers their pids, and only a new pid discards its numbers. A busy GPU, or an [N/A] entry, still refuses. The raw files record allow_idle_foreign and tolerated_foreign (gpu:pid(user),..;..) in meta. Each pass and rep record carries the pids the child ran beside.
One file per (op, dtype, device): <raw>/<op>.<dtype>.<device>.jsonl, one flat JSON object per line. Times are milliseconds per call for the whole batch.
| kind | fields |
|---|---|
| meta (first line) | schema, op, dtype, device, device_name, batchlas (8-hex HEAD, -dirty if tracked files differ), kernels (§6.4 hash), kernel_sources (\|-joined), date, keys, candidates (\|-joined, list order), reps, warm_s, passes, tie, remeasure, refine_ratio (0 = off), cap_gib, ld_pad, devices, allow_idle_foreign, tolerated_foreign, argv |
| rep | op, dtype, device, key fields, pass (1-based), attempt (0, or 1 for a re-measure), reverse, gpu, tolerated_foreign, cand, rep, slot (position in the rotated order), ms |
| pass | as rep without rep, slot, ms; plus status (ok, skipped, bad, error), reason, median_ms, residual, info_nonzero, reps |
| retry | op, dtype, key fields, mode, error (a failed child that was run again) |
| cell (last lines) | key fields, round (0 = coarse grid), refined, status (ok, none, skipped = over the cap), reason, final_attempt (-1 when none), ranked (\|-joined) |
| stalled | op, dtype, note (a bracket wider than the ratio because its midpoint has no winner) |
Key fields use the op's key names. Integer values are JSON integers. With --ledger, import_schema1 reads meta and the pass records of each cell's highest attempt that timed anything. A candidate enters the table when it is ok in every pass of that attempt, and the run becomes a custom run of the ledger.
Gate mode compares the old choice with Auto for each cell. Cells come from --old-csv (columns dtype, every key name, old; a previous gate CSV works) or from the grid flags with --parent-bin, a batchlas_tune built from the parent commit. Its choice is read from the coverage reached row. The old choice is compared and pinned verbatim, so it must be a current spelling: a pre-flat-selection native:<algo> or vendor:auto throws as a pin.
One spec per op, registered with BATCHLAS_TUNE_REGISTER and listed in CMakeLists.txt. --list prints the registered ops. Adding one is step 4 of adding an op to kernel selection.
Each <op>_spec.cc implements OpSpec (spec.hh, not select::OpSpec) and provides:
Limits of the hash: posv's list holds only its own kernels, but its cta and blocked times depend on potrf's and trsm's choices (not yet followed). trsm's list covers its native kernels, not the vendor TU. Its uplo and diag are hidden axes fixed at L and N. Its trans=T times ConjTrans for complex scalars, and it has no workspace, so a pin is probed by one untimed run.
gemv, trmm, symm, syrk and syr2k take no workspace either and are probed the same way. Their non-key arguments are fixed: trmm uplo L, trans N, diag N; symm side L, uplo L; syrk uplo L; syr2k uplo L, trans N; gemv's trans=T times ConjTrans for a complex scalar. Inputs carry large finite poison in the triangle an op must not read (A for trmm and symm) or write (C for syrk and syr2k: a changed element is a nonzero info), and trmm's output is re-poisoned before every run. symm, syrk and syr2k are real-only: --dtype cfloat is refused and --list prints -. symm and syrk key on form, which form_of derives from the extents, so their grid() holds only the consistent cells of the lattice (--grid filters it, as gemm's does). trmm's and symm's expand call the public gemm, so their deps list gemm's kernels and gemm.cc, and their tables should be tuned after gemm's.
syev and gesvd check eigen- and singular values against host LAPACKE (<batchlas/verify/reference.hh>; without LAPACKE those arms are bad). gesvd has no batch key: each cell runs at a batch derived from m and n (a power of two in [128, 16384] with m n batch <= 2^24), and its grid drops the cells no call keys (Hermitian non-square, square thin). One vendor refusal lives in a spec because the library would not report it as a refused pin: gesvd's values-only non-square vendor call faults the device (known defect 15) although can_run admits it. spmm's vendor arm needs none: where can_run refuses it (known defect 17), the strict pin refuses it too. spmm tunes the GPU tables only; its cpu tables stay transcribed.
Every spec verifies through batchlas::verify (verification library): inputs from MatrixView fills, the MatrixView checks on the default items, the bound of the check's kind. A spec with one check reports its raw value against verify::bound; a spec with several (getrf, geqrf, orgqr, syev, gesvd) reports the worst value divided by its own bound, against 1.