benchviz measures BatchLAS against the vendor library the build links (cuSOLVER/cuBLAS on CUDA, rocSOLVER/rocBLAS on ROCm) for every LAPACK op with a vendor route. It writes vector figures and a local dashboard that updates while a campaign runs.
Requirements: Python 3 with matplotlib, numpy and pandas; a TeX installation with latex and dvipng for Computer Modern text. benchviz compiles nothing; build the harnesses first.
benchviz runs the harness binaries it finds, searched in this order:
run --build-dir <dir> overrides the search. The measured code is the code last compiled into that directory, not the checkout's HEAD.
info, the serve banner and the dashboard header show the build's estimated source commit (~sha), its build time, and a warning when it is behind (source changed after the build, or main has commits it lacks). Each campaign records its builds in campaign.json under provenance.builds; every result row records its binary path. Older campaigns show "not recorded". info --build-dir <dir> (repeatable) describes other trees.
Each provenance.builds[] entry records how its binaries were compiled:
| Field | Source |
|---|---|
| sycl_impl | BATCHLAS_SYCL_IMPL_RESOLVED in CMakeCache.txt (DPCPP or ACPP); trees configured before it existed are inferred from -fsycl-targets / --acpp-targets (sycl_impl_inferred) |
| sycl_impl_version | acpp: the AdaptiveCpp version: line of acpp --acpp-version; DPC++: release and intel/llvm commit from clang++ --version |
| compiler, compiler_id, compiler_version | CMAKE_CXX_COMPILER, CMake's compiler id (Clang for both), the --version head |
| build_type | CMAKE_BUILD_TYPE |
| fp_model, fp_contract, fp_flags | the last -ffp-model= / -ffp-contract= and every result-changing fp flag in batchlas_sycl_obj's flags.make |
| fp_contract_effective | the flag, else the driver default at -O2+: on for DPC++, fast for acpp |
| sycl_targets | -fsycl-targets= or --acpp-targets= |
| acpp_env (acpp only) | the ACPP_* variables its runs got, which came from the environment, and whether the JIT database is per campaign |
| acpp_coarse_grained_events (acpp only) | whether src/ or include/ uses the queue property; it is not an environment knob |
Warnings (in info, run logs and warnings):
acpp runtime knobs. Every process run from an acpp tree gets ACPP_ADAPTIVITY_LEVEL=1, ACPP_RT_SCHEDULER=direct and ACPP_APPDB_DIR=<campaign>/acpp-appdb (per-campaign JIT and adaptivity database), unless the environment already sets them; any ACPP_* variable in the environment wins and is recorded. Each acpp row records sycl_impl, its acpp_env, and acpp_jit: true when the process printed acpp's "new binaries being JIT-compiled" warning. The harness warm-up keeps a first-launch JIT out of the timed iterations. For throwaway passes that fill the JIT cache before the measured campaign (design page section 7), export one ACPP_APPDB_DIR for all of them, since resuming a campaign skips the cells it has.
export writes a static, read-only copy with downscaled figures:
--fragment omits the page skeleton for hosts that supply their own <html>.
Campaigns live in benchviz_runs/<name>/ (git-ignored).
| File | Contents |
|---|---|
| results.jsonl | Append-only, one row per (cell, arm) |
| campaign.json | Request and provenance: GPU, driver, builds measured, benchviz checkout SHA and dirty flag |
| figures/<op>/{speedup_n,throughput_n,heatmap}.{pdf,png} | Per-op figures |
| figures/_summary/summary_<precision>.{pdf,png} | Cross-op summaries |
A comparison has kind: compare in campaign.json and no results.jsonl.
| Op | Harness | Vendor arm | BatchLAS arm |
|---|---|---|---|
| potrf, getrf, getrs, geqrf, orgqr | factor_bench (verified residuals) | --arms=vendor | --arms=native |
| gesv, posv | factor_bench | sub-ops pinned vendor | sub-ops pinned native |
| ormqr | ormqr_benchmark | BATCHLAS_ORMQR_ROUTE=vendor | =native |
| syev (with vectors) | syev_benchmark | BATCHLAS_SYEV_ROUTE=vendor | =native, tuned nb and fuse |
| gesvd (n ≤ 32) | gesvd_vendor_benchmark | direct gesvdjBatched call | gesvd_cta |
| gemm, gemv, trsm, spmm | <op>_benchmark | BATCHLAS_<OP>_ROUTE=vendor (cuBLAS / cuSPARSE) | =native |
| trmm, syrk, syr2k (float) | <op>_benchmark | =vendor | =triangular |
| stedc, steqr, sytrd, sytrd_cta, sy2sb, sb2st | own harnesses | none | the harness's direct call |
Left out:
Each process sets BATCHLAS_COVERAGE_OUT. Each row records the route that ran (route) and every sub-op route (subroutes). A pin is not trusted:
| Figure | Content |
|---|---|
| speedup_n | Vendor time / BatchLAS time against n at the saturated batch. One series per precision (S/D/C/Z) with a 2σ band; red dashed parity line at 1×. Log2 y-axis only when the range exceeds 8×. |
| throughput_n | GFLOP/s against n (LAWN 41 counts, complex = 4× real), or matrices/s for syev and gesvd. BatchLAS (○) vs vendor (★), one panel per precision, 2σ bands. |
| heatmap | Speedup over the n × batch grid, viridis with log2 steps. A red boundary follows the 1× crossing. Empty cells were not measured. |
| summary_<t> | Op × n speedup table at saturation, value annotated in each cell. |
| speedup_2d | Rectangular ops only: speedup over both shape axes, annotated, same colour scale and 1× boundary as heatmap. Hatched cells are undefined shapes. |
| throughput_2d | Rectangular ops only: GFLOP/s over the same axes, BatchLAS and vendor side by side on a shared log scale. |
An op with no vendor arm gets throughput_n with BatchLAS alone, and heatmap shows its throughput over n × batch. It has no speedup figures and is left out of summary_<t>, except in a log comparison.
syev's stages are ops in the dashboard group "Eigensolver stages". No vendor library ships them batched, so each has one BatchLAS arm.
| Op | Harness | What it runs | Arguments |
|---|---|---|---|
| stedc | stedc_benchmark | divide and conquer, with vectors | threshold 0, merge and driver Auto (the tuning tables syev uses) |
| steqr | steqr_benchmark | QR iteration, with vectors; n ≤ 32 is steqr_cta, above it steqr_wg | 50 sweeps, interleaved working vectors; Wilkinson shift pinned |
| sytrd | sytrd_blocked_benchmark | blocked reduction to tridiagonal (syev_blocked stage 1) | nb as syev_blocked chooses it, including the complex override at 256 < n ≤ 512 |
| sytrd_cta | sytrd_cta_benchmark | one-CTA reduction, n ≤ 32 | defaults |
| sy2sb | sytrd_sy2sb_benchmark | two-stage stage 1, dense to band | kd = 32, as choose_two_stage_kd |
| sb2st | sytrd_sb2st_benchmark | two-stage stage 2, bulge chase | kd = 32 |
A source is a campaign name (looked up in --root, then every checkout; an ambiguous name lists the candidate paths), a campaign directory, or a results.jsonl. The output goes to benchviz_runs/<name>/ (default cmp-<baseline>-vs-<candidate>). compare stores no rows and re-reads both logs on each load, so it fills in while a campaign runs.
For figures you will quote, measure both campaigns back to back on the same card with the same grid.
ab takes exactly two --build-dir (baseline first) and the grid flags of run. Every cell runs on both builds back to back on one card; the build that goes first alternates from cell to cell. Rows land in <campaign>-dpcpp and <campaign>-acpp (-base / -new when both builds are one implementation), and <campaign> is their comparison, paired with --exact. When the two logs differ in implementation, figures and reports name the arms DPC++ and AdaptiveCpp, adding the commit only when the commits differ (which is also a warning). The vendor control then includes each implementation's vendor interop path, not only the machine state. Compare implementations at saturation only, from Release trees.
Ops with a second shape dimension also get a 2-D sweep (their Plane in ops.py). Each shape runs once at its saturated batch: the largest power of two that fits the memory budget for that shape.
| Op | y axis | x axis | Constraint |
|---|---|---|---|
| geqrf, orgqr | rows m | columns n | n ≤ m |
| ormqr | order of Q, m | reflectors n | n ≤ m |
| getrs, gesv, posv | order n | right-hand sides (1, 4, 16, 64, 256) | |
| gemm | output order m = n | inner dimension k | |
| gemv | rows m | columns n | |
| trsm | triangle order n | right-hand sides q | |
| syrk, syr2k | order n | rank k | |
| spmm | rows n | right-hand sides (4 … 128) |
The square point of each plane is the square sweep's cell. Left square: potrf and getrf (factor_bench takes m = n only), syev, trmm, and gesvd.
Figures follow style.py, the source of truth for the house style:
A grid is a preset plus overrides. The dashboard's run panel edits the same fields and previews the n × batch cells; the CLI takes them as flags.
| Field | Flag | Meaning |
|---|---|---|
| orders | --orders | n values. 16,32,64 lists them, 4:512 doubles from 4 to 512, 8:128:8 steps by 8. Blank uses each op's ladder. Out-of-range values are dropped. |
| batch mode | --batch-mode | ladder (default), list, or saturated (one batch per n: the largest that fits the memory budget) |
| ladder | --batch-min --batch-max --batch-step | batch-min × step^k, up to min(batch-max, memory cap) |
| list | --batches | explicit batch list, same syntax as orders |
| reps | --reps | timed repetitions per arm-cell |
| memory | --mem-gib | per-arm device-memory budget; caps batch per (op, n) |
| rectangular | --rect / --no-rect | sweep the rectangular shape planes, one saturated batch per shape |
| Preset | Grid |
|---|---|
| smoke | every 3rd order, batch ×16 from 256 |
| saturation | every order, saturated batch only (speedup vs n, throughput) |
| quick | every order, batch ×4 from 64 |
| full | every order, batch ×2 from 32 |
On an RTX 4090 an arm-cell takes about 3–8 s, mostly process start, warm-up and the exclusive-GPU check.
serve also hosts /tuning, the selection tuner's dashboard (switch pages in the header). It shows what the tuning ledgers hold and what a table written from them would change, and starts and watches tools/tune/batchlas_tune runs. Files: docs/design/tuning-data.md (one ledger benchmarks/results/tuning/ledger/<op>.<device>.jsonl and one table tuned/<op>.<device>.txt per op and device). Design: docs/design/tiered-tuning.md, sub-project 3.
| View | Shows |
|---|---|
| Status | op × precision for one device. Per op: its kernel families, ! where a family's hash v2 differs from the table header's family_kernels (hover: both hashes and the ledger keys stale in it per precision), and the retune warning sweep_to_table.py prints. Per precision: the tier mix of the best records (deep, coarse, preview, custom, converted, transcribed), refined share, stale and partly stale cells, audit mismatches, age, the table's rows of that precision, and behind when the table header does not name the ledger's newest run |
| Op (click a status cell) | the kernel families with table and current hashes and a verdict (current, retune, regenerate). The winner map over two keys with the others fixed: colour is the winning family, paler is a closer runner-up, a mark gives the tier. Click a cell for every record and candidate with its interval. "Change vs table" colours the predicted speedup instead, and the table below lists every changed pick. The lede says whether the table's rows equal the ledger's as --check judges them, and how many rows a table written now would hold |
| Runs | per-GPU current cell, per-job progress against the plan, eliminations, audits, worker restarts, rate and ETA, the feed and the log |
The read-only views need no GPU and no build. They read each ledger with scripts/sweep_to_table.py (read_ledger: one precision's lines, rehash lines applied, -dirty runs left out with a warning; best_records, freshness, ledger_rows, parse_table), the code that writes the tables, and judge freshness against the kernel sources in this checkout (re-hashed every 2 s). The first view loads every ledger, about 13 s for the 39 files on threadripper02; later views reload only a changed file. A ledger that is still an LFS pointer shows as unreadable.
A run needs a build with the batchlas_tune target. The tab uses the first build whose batchlas_tune --help lists every flag it passes (--tier, --plan, --progress-fd, --ledger, --device-key, --remeasure-keys, --allow-dirty); otherwise the drawer says which build is missing or too old, and planning and starting are refused. The drawer offers the tiers preview, coarse and deep, plus custom, which measures exactly the cells listed under "Cells to measure again" (--remeasure-keys, also usable with the other tiers). A checkout with uncommitted changes to tracked files can be planned but not measured unless "Allow a dirty tree" is ticked; such a run never reaches tuned/. With no GPU visible, the plan reads the shown device's ledger (--device-key). One tuning run at a time. Stop sends SIGTERM to the run's process group. Every finished cell is already in the ledger, so "Run again" resumes: the tuner skips what the ledger holds at the run's tier or better. Runs live in benchviz_runs/_tuning/<run>/ (with remeasure-keys.txt when cells were listed). A run started with benchviz tune from a terminal is watched the same way, and keeps going if the server stops.
factor_bench selects its backend at compile time. Run a ROCm build with --backend rocm; it compares against rocSOLVER/rocBLAS. gpu_guard.sh is NVIDIA-only, so on ROCm the device is pinned with ROCR_VISIBLE_DEVICES and exclusivity is not checked. gesvd has no ROCm arm, because gesvd_vendor_benchmark calls cuSOLVER directly.