Skip to content

perf(prover): default the cuda table scheduler to K = num_airs - #911

Open
MauroToscano wants to merge 4 commits into
mainfrom
perf/table-parallelism-num-airs
Open

perf(prover): default the cuda table scheduler to K = num_airs#911
MauroToscano wants to merge 4 commits into
mainfrom
perf/table-parallelism-num-airs

Conversation

@MauroToscano

@MauroToscano MauroToscano commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The cuda arm of table_parallelism() scales K by available_parallelism()
(cores * 2 / 3). Measured over 881 runs on two RTX 5090 boxes, that is the wrong
shape — not the wrong constant.

All eight core-count curves fit T(K) = S + max(Tmax, W/K) within run-to-run noise,
and the work K divides (W ≈ 5.3–8.0 s) is invariant to host core count over an 8×
range, to CPU model, and to rayon pool width. The decisive one: cutting
RAYON_NUM_THREADS 32 → 4 leaves W alone and merely doubles S, with the best K still
num_airs at every pool width. available_parallelism() sizes precisely that rayon
pool, so it is the wrong quantity to scale K by — K is not a thread count, each
table's work runs on the one global pool.

Worst case against the best measured K, over four core counts on both boxes:

rule worst case
cores/3 +30.2%
cores*2/3 (what this replaces) +13.0%
constant 12 +7.0%
num_airs +1.6%

cores*2/3 fails where predicted: low core counts, K=2 at 4 cores (+13.0%), K=5 at 8
cores (+8.1%). No K below num_airs is significantly faster than num_airs anywhere
on either box (every such cell p ≥ 0.38 at n=8).

Taking the ceiling rather than solving for an optimum is right in both regimes of the
fit: if W/num_airs > Tmax more K strictly helps; if W/num_airs < Tmax the extra
drivers are floor-limited and cost nothing — the one staging slab is held 56% of wall
at K=31 and wall time still improves.

The old doc comment's mechanism ("in-flight tables mostly sit in GPU waits") is not
what happens — mean GPU utilisation never exceeded ~38% at any K — so it is rewritten
rather than re-tuned. What is meant to bound concurrency is memory admission rather
than a count: that is what VramGate is for.

auto_storage is held bounded

table_parallelism now takes num_airs and clamps to it, replacing the .min(num_airs)
the call site applied. auto_storage::decide keeps a bounded figure through the new
storage_estimate_parallelism(): peak_bytes sums the transient bytes of the top-k
tables, so an unbounded k there sums every table — measured +27% at 128 PAGE tables,
+44% at 512 — and would spill proofs to disk that fit in RAM. Its value is unchanged,
so no storage decision moves. Two tests pin both halves.

The CPU arm is untouched

cores / 3 stays. The sweep ran only on cuda builds, where the parallelized work is
device-bound; on a CPU-only build every table is pure host work and none of this
evidence transfers.

Evidence

The full sweep record — both rounds' write-ups, every result CSV and the re-runnable
harnesses, so every number above is checkable — lives in a gist to keep this diff
reviewable: https://gist.github.com/MauroToscano/54cdbf03da9a69517ae30c964efc2827
(filenames are the original scripts/profiling/table-parallelism-sweep/ paths
flattened with __; 00_INDEX.md maps the layout).

Honest limits

  • One GPU model (RTX 5090) and one workload throughout: 6.8M cycles, 4 epochs at
    epoch-size-log2 21, ~31 tables. Production tables are much larger, so memory per
    concurrently-admitted table is larger and the auto_storage interaction is unverified
    at that scale — a big-block check is a merge gate, not something this PR establishes.
  • Low-core legs are cache-confounded against round 1 (this box's taskset -c 0-7 is one
    CCD with 32 MB L3 vs the 3D part's much larger cache).
  • W is inferred from T(1) − T(best), not instrumented per phase.
  • Round 1's headline "knee K≈8" should not be quoted: the curve is S + W/K with no
    saturating resource, so a tolerance-knee is derived and reads 8 on one box and 16 on
    the other at the same 5% tolerance.

Verified

cargo fmt --check clean; cargo clippy --all-targets -p stark -p lambda-vm-prover
clean. auto_storage_tests 8 passed (needs --features disk-spill);
table_parallelism_stays_within_one_and_num_airs passes. The cuda-gated test and the
cuda arm itself cannot compile here — no local CUDA toolchain — so both were verified by
reading plus a non-cuda build.

`table_parallelism()`'s cuda arm scaled K by `available_parallelism()`
(`cores * 2 / 3`). Measured over 881 runs on two RTX 5090 boxes, that is the
wrong shape. All eight core-count curves fit `T(K) = S + max(Tmax, W/K)` within
run-to-run noise, and the work K divides — W ≈ 5.3-8.0 s — is invariant to host
core count over an 8x range, to CPU model, and to rayon pool width: cutting
RAYON_NUM_THREADS 32 -> 4 leaves W alone and merely doubles S, with the best K
still num_airs at every pool width. `available_parallelism()` sizes precisely
that rayon pool, so it is the wrong quantity to scale K by. K is not a thread
count; each table's work runs on the one global pool.

Worst case against the best measured K, over four core counts on both boxes:

  cores/3      +30.2 %
  cores*2/3    +13.0 %   (what this replaces)
  constant 12   +7.0 %
  num_airs      +1.6 %   (both non-zero cells inside noise, p = 0.88 / 0.80)

`cores*2/3` fails where it was predicted to: low core counts, K=2 at 4 cores
(+13.0 %) and K=5 at 8 cores (+8.1 %).

Taking the ceiling rather than solving for an optimum is right in both regimes
of the fit: if W/num_airs > Tmax more K strictly helps, and if W/num_airs < Tmax
the extra drivers are floor-limited and cost nothing — the one staging slab is
held 56 % of wall at K=31 and wall time still improves. The old doc comment's
mechanism ("in-flight tables mostly sit in GPU waits") is not what happens —
mean GPU utilisation never exceeded ~38 % at any K — so it is rewritten rather
than re-tuned. What is meant to bound concurrency is memory admission rather
than a count: that is what VramGate is for, and it never binds at the default
budget.

`table_parallelism` now takes `num_airs` and clamps to it, replacing the
`.min(num_airs)` the call site applied. `auto_storage::decide` keeps a bounded
figure through the new `storage_estimate_parallelism()`: `peak_bytes` sums the
transient bytes of the top-k tables, so an unbounded k there sums every table —
measured +27 % at 128 PAGE tables, +44 % at 512 — and would spill proofs to disk
that fit in RAM. Its value is unchanged, so no storage decision moves.

The CPU arm keeps `cores / 3`. The sweep ran only on cuda builds, where the
parallelized work is device-bound; on a CPU-only build every table is pure host
work and none of this evidence transfers.
MauroToscano added a commit that referenced this pull request Aug 10, 2026
The sweep record moves out of the tree to a gist linked from the PR, so
the doc comment can no longer cite scripts/profiling/table-parallelism-sweep/.
@MauroToscano
MauroToscano force-pushed the perf/table-parallelism-num-airs branch from 4049dcb to 67fde58 Compare August 10, 2026 18:09
The sweep record moves out of the tree to a gist linked from PR #911, and
the full defense of the K = num_airs choice (curve fit, rayon-width legs,
per-cell p-values) lives there and in the PR body. The code site keeps the
conclusion, the mechanism in one sentence, the headline numbers, and the
pointer.
Under the cuda feature the unwrap_or_else closure in table_parallelism
collapses to a plain num_airs, tripping the lint on the Makefile's cuda
clippy pass. Move the cfg split outside the closure: the cuda arm uses
unwrap_or, the CPU arm keeps its lazy host_cores() call.
@MauroToscano
MauroToscano force-pushed the perf/table-parallelism-num-airs branch from 67fde58 to d4c9e05 Compare August 10, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants