Status: current · RTX 4090 (sm_89), CUDA 13.2
The tuning harness grid-searches tuning parameters by running the benchmark executables in build/benchmarks/. It writes a JSON profile (meta: backend, type, build dir; results: per-bench best and top-K), and generate_tuning_header.py turns the profile into constants for include/batchlas/tuning_params.hh. The numbers below are sm_89 measurements.
From the repository root, after building the benchmarks:
| File | Purpose |
|---|---|
| spaces/default.json | Benches that feed a constant in tuning_params.hh. Run this to retune. |
| spaces/unwired.json | Parameters that matter but have no constant. The generator ignores them; the run produces a profile for a human to read. |
A bench belongs in default.json only if the generator reads it. Today those are stedc, sytrd_blocked, ormqr_blocked, syev, gesvd, sb2st and sy2sb. A bench outside that list is measured and then silently dropped. steqr was in that state until it was removed.
| Key | Meaning |
|---|---|
| arg_spec | Positional benchmark arguments |
| cases | Problem sizes, each with fixed args and a case-local tune grid |
| pre_tune | Optional phases { "params": {...}, "cases": [...] } run before the main search. Winners are injected as fixed params into every case and recorded in the profile. |
| env | Optional {param: ENV_VAR}. Passed as environment variables instead of positional args; must not appear in arg_spec. Merged onto the parent environment and part of the measurement cache key. |
| row | Substring selecting the benchmark row to read. Required when an executable registers several benchmarks; the harness refuses to guess. |
Rules:
For each (bench, parameter) it sweeps that parameter with everything else fixed and reports how far the metric moves. Read the profile, not a log: env-only parameters never appear in the benchmark's CSV columns.
The 2026-08-07 pass removed the latrd_lower_panel bench (its only parameter moved the metric by at most 0.38 %) and trimmed values that never won. The default space went from 609 to 265 invocations.
An ordinary constant can also lose at the consumer. The stedc bench picked wg_multiplier 2–4 and threads_per_root 4–8. Adopting those cost syev 2.7 % at n = 256. Measured through syev with everything else pinned, 8/8 wins nearly everywhere (7.6 % at n = 64; noise at n ≥ 512), so both ship as 8.
The stedc bench measures the merge in isolation; syev pays for the whole tridiagonal solve. A parameter whose owning bench is not its dominant consumer must be confirmed at the consumer. The isolated sensitivity (0.56 % median for wg_multiplier) understated the real cost by an order of magnitude.
The header the library compiles is the checked-in include/batchlas/tuning_params.hh. There is no generated copy.
Port only the inline constexpr values you intend to change, by hand. Do not overwrite the file:
Read results[].per_case_best for the per-n winners the bucketed header uses. Read results[].best for the single set best averaged across cases; it is a much smaller search when the per-case grids barely overlap.
Every accessor reads an environment variable first:
Variables: BATCHLAS_TUNE_{ORMQR_BLOCK_SIZE, SYTRD_BLOCK_SIZE, LATRD_WG_HINT, STEDC_RECURSION_THRESHOLD, STEDC_MERGE_VARIANT, STEDC_THREADS_PER_ROOT, STEDC_WG_MULTIPLIER}. Each overrides all size buckets, so test one n at a time. Do not change one mid-process: the same accessor feeds *_buffer_size() and the matching solve.
Build the five benches the default space needs. BATCHLAS_ENABLE_TUNING is not required.
There is no CMake target that writes the header. Drive the scripts directly.
Measured 2026-08-06, CUDA/float:
| Block size | ormqr kernel, n = 1024 | syev n = 1024 | gesvd_blocked n = 512 |
|---|---|---|---|
| 16 (shipped) | 536 µs | 899 µs | 940 µs |
| 48/56 (tuned) | 238 µs (2.16× faster) | 905 µs (inert) | 1045 µs (11 % slower) |
Three mechanisms explain it. None is visible from ormqr_blocked_benchmark.
| Stage | nb = 16 | nb = 48 |
|---|---|---|
| gesvd.gebrd | 229.7 ms | 259.2 ms (+12.8 %) |
| gesvd.apply_left_backtransform | 18.35 ms | 14.87 ms (−19 %) |
| gesvd.apply_right_backtransform | 18.25 ms | 15.12 ms (−17 %) |
The uses have opposite gradients, and gebrd is 6.3× the larger term. Aliasing pins the steep knob at the flat knob's optimum.
| nb = 16 | nb = 56 | |
|---|---|---|
| BATCHLAS_SY2SB_ORMQR_NB=off | 1094.9 µs | 905.4 µs (1.21×) |
| default (gate on) | 898.6 µs | 905.0 µs (inert) |
What to do:
The header is bucket-first; there is no single global block-size constant.
| Constant family | Accessor |
|---|---|
| ORMQR_BLOCK_SIZE_<BUCKET> | ormqr_block_size_for_n(n) |
| SYTRD_BLOCK_SIZE_<BUCKET> | sytrd_block_size_for_n(n) |
| LATRD_LOWER_PANEL_WG_HINT_<BUCKET> | latrd_lower_panel_wg_hint_for_n(n) |
| SYTRD_FUSE_PANEL_UPDATE_<BUCKET> | sytrd_fuse_panel_update_for_n(n) |
| STEDC_{RECURSION_THRESHOLD, MERGE_VARIANT, THREADS_PER_ROOT, WG_MULTIPLIER}_<BUCKET> | stedc_*_for_n(n) |
<BUCKET> is TINY, SMALL, MEDIUM, LARGE or XLARGE:
| Bucket | n |
|---|---|
| TINY | ≤ 64 |
| SMALL | 65–128 |
| MEDIUM | 129–256 |
| LARGE | 257–512 |
| XLARGE | > 512 |
Bucketed values come from each case winner (per_case_best). Missing ranges use the configured fallback values.
STEDC tuning cases start at n = 64. Leaf sizes n ≤ 32 are not tuned separately. The default space pre-tunes recursion_threshold at n = 64 only, then reuses it for the merge-variant and workgroup sweep. At runtime the threshold is clamped to the local subproblem size at each level.
syev tunes nb, the LATRD lower-panel wg hint and the fused panel update as one tuple, against end-to-end eigensolver time.
Shares on CUDA/float, RTX 4090 (from BATCHLAS_KERNEL_TRACE=1 and BATCHLAS_GESVD_PROFILE=1):
| Kernel | Share | Tuned by |
|---|---|---|
| syev_two_stage.sb2st_hh | 52.9 % of syev at n = 1024 | sb2st bench; the heuristic already wins, so the constants stay 0 |
| gesvd.gebrd | 79–95 % of gesvd | gesvd bench |
| ormqr_blocked.larft + pack_v_panel | about 20 % of syev at n = 1024 | ormqr_blocked, plus sy2sb for the width that shadows it |
| Two-stage band width kd | sets the stage-1/chase balance | nothing (hand table); in unwired.json |
| Chase blocking factor | hardcoded 32 | nothing; in unwired.json |
| steqr CTA knobs | leaf of every stedc merge | nothing; in unwired.json |
To promote a parameter out of unwired.json, in order: