Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ jobs:
python3 .github/scripts/coverage_obligations.py \
--receipt test/gpu-proof.json
python3 .github/scripts/shard_scope_closure.py
python3 -m pytest test/test_bench_common.py -q
git check-ignore --quiet bench/solver_ladder_20990101_000000.txt
python3 -m pytest test/test_bench_common.py test/test_tune_isolation.py -q

compile-smoke:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions bench/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ __pycache__/
# Raw timing captures written by bench/tune.py / the harnesses (large, per-run).
# The curated *_RESULTS.md summaries and regenerated tables ARE tracked.
mega_sweep_*.txt
solver_ladder_*.txt
reduced_sweep_*.txt
blas2_sweep_*.txt
rect_sweep_*.txt
Expand Down
15 changes: 8 additions & 7 deletions bench/JETSON.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ One capture per box (Orin AGX / Orin NX / Orin Nano — all `sm_87`). Each run
produces a single `bench/jetson_<host>_<ts>.tar.gz` containing the timings
plus a full device/JetPack provenance bundle; send those back for ingestion
(figure columns + the `ideal_sm87` ladder table, spliced off-box via
`tune.py --from-ladder --from-nvt-valid --sm 870` when MathDx is available;
native-only captures need no confirmation companion).
`tune.py --from-ladder --from-solver-ladder --sm 870`). The fresh-input
solver companion is required with or without MathDx so destructive native and
vendor implementations are measured under the same input policy.

## 0. Pre-flight: get on the latest JetPack the box supports

Expand Down Expand Up @@ -114,9 +115,9 @@ methodology). The script:
CPUs, `nvpmodel -q`, `jetson_clocks --show`, a `cudaGetDeviceProperties`
probe (SM count is what separates AGX/NX/Nano), and idle `tegrastats`;
2. builds everything for `sm_87`;
3. runs the timed legs serially — the 3-tier SIMT ladder
(`tune.py --allow-no-mathdx`; MathDx does not ship for Tegra, which is
itself a paper datum), the hostblas + single-call-latency + fusion
3. runs the timed legs serially — the native ladder plus its fresh-input
solver companion (`tune.py --allow-no-mathdx` falls back cleanly when a
copied MathDx tree is unavailable), the hostblas + single-call-latency + fusion
harnesses (host cuBLAS/cuSOLVER are on JetPack), and the robotics
micro-op sweep — with `tegrastats` logging alongside for energy/solve;
4. tars captures + provenance into `bench/jetson_<host>_<ts>.tar.gz`.
Expand All @@ -125,8 +126,8 @@ methodology). The script:

| Capture | Feeds |
|---|---|
| `mega_sweep_*.txt` | `ideal_sm87` ladder table. Full MathDx replay: `python bench/tune.py --sm 870 --legs ladder --from-ladder <mega> --from-nvt-valid <nvt>`; a native-only capture needs only `--from-ladder <mega> --allow-no-mathdx`. |
| `nvt_valid_*.txt` | Required valid-input veto companion when the ladder selects NVIDIA thread; pass it with `--from-nvt-valid` during off-box regeneration. |
| `mega_sweep_*.txt` | Non-destructive rows for the `ideal_sm87` ladder table. Replay with `python bench/tune.py --sm 870 --legs ladder --from-ladder <mega> --from-solver-ladder <solver>`. If a solver capture was interrupted, omit `--from-solver-ladder` to preserve this file and recapture only its fresh-input companion. |
| `solver_ladder_*.txt` | Required symmetric fresh-input POTRF/TRSV/POSV companion. It records every native/vendor execution plan and raw paired-round sample; pass it with `--from-solver-ladder` during off-box regeneration. |
| `paper_hostblas_*.txt` | Jetson columns for the hostblas + latency figures |
| `paper_fusion_*.txt` | Jetson fusion curves |
| `robotics_sweep_*.txt` | Jetson robotics tier panels |
Expand Down
7 changes: 6 additions & 1 deletion bench/RESULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ labor (2026-08-11 consolidation):
is current only when its source digest, environment, timing date, and proximate
signed correctness receipt are recorded together.

## ladder (mega sweep — native thread/warp/block plus NVIDIA block/thread)
## ladder (archived 2026-08-30 method; superseded for the next retune)

The counts below describe the currently shipped historical tables. New tuning
runs replace the asymmetric NVIDIA-thread confirmation with the unified
fresh-input solver ladder documented in `TUNING.md`; update this section only
after the paired captures are accepted.

The shipped tables were regenerated 2026-08-30 after adding cuSOLVERDx's
thread-per-problem LAPACK interface as a fifth contender. The sm_120 table uses
Expand Down
26 changes: 12 additions & 14 deletions bench/TUNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,18 @@ The ladder-grammar harnesses share one measurement core
busy GPU and invalidates a leg if a foreign compute PID appears after start.
Correctness stays a separate gate; a timing capture does not promote peer
agreement into a numerical oracle.
- **Mutation invariant**: the main ladder's reps run with no restore in the timed region, so
in-place ops re-factor their own output from rep 2 on. This is
suitable for branch-free steady-throughput characterization, but it is not
sufficient evidence for a solver default. Every main-ladder
`nvidia_thread` winner is therefore remeasured by `bench_nvt_valid.cu`
against native thread/warp/block using a bounded ring of independent valid
systems (one per timed launch; initialization untimed). It must clear the
same 5% margin or `tune.py` vetoes it to the valid-input native winner.
Missing confirmation fails regeneration, and the confirmation leg never
promotes a vendor path. The capture records each contender's three-trial
spread; if the observed min-to-max intervals overlap the 5% decision
boundary, regeneration fails closed instead of treating the nominal minima
as a verdict. Never use the main loop alone for a `CHECK`-gated,
pivoted, or other data-dependent op.
- **Fresh-input solver invariant**: the main ladder's back-to-back launches are
authoritative only for non-destructive operations. POTRF, TRSV, and POSV
overwrite inputs, so `bench_solver_ladder.cu` replaces those rows with a
symmetric measurement of every supported native block/warp/thread and
NVIDIA block/thread launch plan. Each timed launch consumes a separate valid
system from a bounded ring; initialization is untimed. Plans are randomized
within nine paired rounds, and the capture retains every raw sample plus the
exact launch shape. Any contender may win under the ordinary 5% dependency
and ±2% SIMT tie rules—there is no special NVIDIA-thread veto. Missing solver
cells fail regeneration. Correctness remains a separate signed-receipt gate.
Never use the destructive main-ladder loop alone for a `CHECK`-gated,
pivoted, or other data-dependent operation.
- **Decisions absorb residual noise**: the 5% dependency margin, the ±2% SIMT
tie band, and the `noise_floor` override (sub-granularity cells refuse to
resolve a margin) all live in `tune_pick.py` — measured 4× on Jetson Orin
Expand Down
22 changes: 20 additions & 2 deletions bench/autotune.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
Requires MATHDX_ROOT set (cuBLASDx headers needed for the cuBLASDx leg).
"""

from __future__ import annotations

import argparse
import concurrent.futures
import datetime
Expand Down Expand Up @@ -814,12 +816,21 @@ def emit_results_md(md_path: pathlib.Path,

# ─── main ───────────────────────────────────────────────────────────────────

def emit_defaults_table(sweep_path, out_path, sms, margin=0.05):
def emit_defaults_table(sweep_path, solver_path, out_path, sms, margin=0.05):
"""Parse a bench_mega_sweep run (mega_sweep_*.txt) and emit a per-host override
header for glass-defaults.cuh (native plus NVIDIA block/thread ladder). Uses the NPROB=8192
throughput regime for f32 + f64."""
text = pathlib.Path(sweep_path).read_text()
cells = tp.parse_mega_sweep(text, nprob=8192)
solver_cells = tp.parse_solver_ladder(
pathlib.Path(solver_path).read_text(), nprob=8192)
expected = {key for key in cells if key[1] in {"potrf", "trsv", "posv"}}
missing = sorted(expected - set(solver_cells))
if missing:
sys.exit(f"Fresh-input solver capture is incomplete: {missing[:6]}")
for key in expected:
cells[key] = {impl: row["ns"]
for impl, row in solver_cells[key].items()}
winners = {} # (native_only, dtype, op) -> {N: backend}
for (dtype, op, N), measured in cells.items():
for native_only in (False, True):
Expand Down Expand Up @@ -851,6 +862,7 @@ def emit_op(op, dtype, native_only):
# GLASS_DEFAULTS_TABLE_LOCAL hook), so this fragment must NOT re-open the namespace.
lines = [
f"// Generated by bench/autotune.py --emit-defaults from {pathlib.Path(sweep_path).name}",
f"// Fresh-input solver rows: {pathlib.Path(solver_path).name}",
f"// (sm {sms}, NPROB=8192 throughput regime). Do not edit by hand.",
"// Included by glass-defaults.cuh inside namespace glass::defaults — no namespace wrapper.",
"#define GLASS_DEFAULTS_HAVE_LOCAL",
Expand Down Expand Up @@ -928,15 +940,21 @@ def main():
p.add_argument("--emit-defaults", metavar="SWEEP_TXT", default=None,
help="Parse a bench_mega_sweep run (mega_sweep_*.txt) and emit a per-host "
"glass-defaults.cuh override header (native and NVIDIA ladder), then exit.")
p.add_argument("--solver-ladder", metavar="SOLVER_TXT", default=None,
help="Fresh-input solver_ladder_*.txt companion required by "
"--emit-defaults")
args = p.parse_args()

sms = detect_sm() if args.sm == "AUTO" else int(args.sm)
hostname = platform.node().replace(" ", "_")

if args.emit_defaults:
if not args.solver_ladder:
p.error("--emit-defaults requires --solver-ladder")
out = args.out or str(TUNING_DIR / f"{hostname}_defaults.cuh")
TUNING_DIR.mkdir(parents=True, exist_ok=True)
emit_defaults_table(args.emit_defaults, out, sms, args.margin)
emit_defaults_table(args.emit_defaults, args.solver_ladder,
out, sms, args.margin)
return

requested_apis = [a.strip() for a in args.apis.split(",") if a.strip()]
Expand Down
Loading