Skip to content

perf(prover): halve GPU continuation proving time - #863

Open
ColoCarletti wants to merge 38 commits into
mainfrom
gpu-opt-5090
Open

perf(prover): halve GPU continuation proving time #863
ColoCarletti wants to merge 38 commits into
mainfrom
gpu-opt-5090

Conversation

@ColoCarletti

@ColoCarletti ColoCarletti commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Result

ethrex 10tx --continuations on RTX 5090 + 16 cores, interleaved A/B/B/A (12 pairs): main 22.58s → 11.05s = 2.04x (−51.1%, sd 0.88%). Proofs cross-verify in both directions against main's verifier.

What changed

  • Epoch schedule: execution/trace-build pipelined ahead of proving, K concurrent epoch provers, global prove overlapped with the epoch queue, trace builds on a builder pool.
  • Kernels: constraint-interpreter rewrite (dim-split + liveness-reused slots): 3x kernel time, 8-35x less scratch; preprocessed tables committed through the fused GPU pipeline.
  • Device residency (biggest win): the d=2 composition pipeline never crosses PCIe — H stays resident, on-device decompose + slab LDE, parts handle feeds R4 DEEP, FRI folds from the device-resident bit-reversed codeword. ~6 codeword-sized PCIe trips per table per epoch become one drain.
  • Shared caches: domain/twiddles/OOD constants/FRI twiddles/zerofier inverses computed once per domain per process instead of per table per epoch; constraint-program lowering cached by content hash.
  • Fix: intermittent deadlock — parallel batch inversion inside OnceLock initializers could starve against rayon workers parked on the same cell; initializers are now sequential and pre-filled at domain construction.

Validation

Per-change interleaved ABBA + bidirectional cross-verify + full stark cuda suite (212/212) at every step; clippy clean; 20-prove stress run for the deadlock fix; GPU↔CPU parity bit-exact on the constraint kernels.

Notes

  • --epoch-size-log2 22 + LAMBDA_VM_EPOCH_CONCURRENCY=1 gives a further −18% on 32GB/16-core boxes (~27GB RAM per prove) — left at defaults, documented for deployment tuning.

- run_profile.sh: nsys export needs --force-overwrite (nsys stats already
  materializes the sqlite); tolerate runs that produce no timeline JSON
- flamegraphs.sh: fixed off-CPU capture window sized from the on-CPU run
  (SIGINT through sudo is unreliable and produced 0-byte captures);
  find offcputime-bpfcc in /usr/sbin (Debian)
- bench_mode.sh: set the CPU governor via sysfs when cpupower is absent
- setup_machine.sh: Debian-aware perf install (linux-perf); extract
  libnvToolsExt from the cuda-nvtx-12-8 deb into ~/nvtx (CUDA >= 12.9
  removed NVTX v2 from the toolkit) with LAMBDA_VM_NVTX_LIB override
- docs: benchmark/profiling examples use ethrex 5tx/10tx fixtures only
  (team convention: never fibonacci); plan status updated
Adds the pieces needed to use the tooling without reading the scripts:
column-by-column semantics for phase_table.md and phase_busy.md
(including NVML gpu% vs nsys busy% and launch-site attribution), a
reference table of every script with its flags, the environment
variables the tooling understands plus the pre-existing prover knobs
for A/B experiments, and a troubleshooting section (missing NVTX
ranges, silent CPU fallback, empty off-CPU captures, jitter,
concurrent-thread span nesting).
…ee cache

The optimization half of the original campaign commit, without its
profiling layer (this branch keeps gpu-profiling-tooling's toolkit as the
only instrumentation):

- async_dtoh_via/PendingD2H: big D2H copies go through per-worker pinned
  slabs via raw cuMemcpyDtoHAsync + a reusable completion event, instead
  of cudarc's memcpy_dtoh whose pageable path blocks the calling thread
  for all prior stream work (host DtoH blocking 12.4s -> 6.7s on the
  original ethrex A/B).
- GpuLdeBase/GpuLdeExt3 carry a 'ready' event; consumers wait device-side
  (cuStreamWaitEvent) instead of producers host-synchronizing.
- Events are pre-created at backend init plus a reusable pool: a mid-prove
  cuEventCreate convoys the driver lock (~30ms/call measured under load).
- Precomputed-column Merkle trees are cached process-wide keyed by their
  commitment root, so preprocessed tables (DECODE/BITWISE/range) stop
  rebuilding identical trees on every prove; only the multiplicity
  columns are recommitted.
Producer thread executes and builds epoch i+1's traces while epoch i
proves; K epoch provers (LAMBDA_VM_EPOCH_CONCURRENCY, default 3) consume
prepared epochs concurrently — epoch proofs are mutually independent
(label-domain-separated transcripts), results re-ordered by index so
proof bytes match the sequential schedule. The DECODE commitment is
computed once per continuation prove instead of per epoch.

Same as the original campaign commit minus its epoch-timeline
instrumentation (this branch keeps the profiling toolkit's spans as the
only instrumentation; they are re-homed onto this pipelined flow at the
end of the series).
…e slots

The constraint interp/composition kernels evaluated every IR node as ext3
and kept one global-memory scratch slot per node, so scratch size and
traffic scaled with program length (KECCAK_RND/ECSM/ECDAS at full thread
count needed 26-39 GB, failing the alloc and silently falling back to CPU
via result.ok()).

Lowering (constraint_ir/device.rs) now assigns dim-split slots:
- Base-dim nodes compute in the base field (1 mul vs 9 for ext3) and
  live in u64 slots (8B vs 24B); mixed base*ext ops use mul_base /
  componentwise shortcuts that are bit-identical to the full ext op on
  the embedded operand (SUB components keep the literal sub(0, x) form,
  which is NOT bitwise neg on non-canonical limbs).
- Slots are liveness-reused (linear scan, freed at last use, roots
  pinned), so per-thread scratch is the max-live-set, not the node
  count: 8-35x smaller across the 26 tables (CPU 14.4KB -> 1.3KB,
  ECDAS 596KB -> 17KB per thread). Scratch allocs drop the memset.
- Row-invariant leaves (constants, RAP challenges, alpha powers, table
  offset) are propagated into operand encodings (kind<<29|payload) and
  never touch scratch; they only materialize when a root needs them.

The CPU walker eval_device_program mirrors the new walk and stays the
pre-GPU parity oracle; the 26-table differential vs the production
folder and the on-GPU parity tests (synthetic + all real programs) pass
bit-for-bit. ir_stats_dump (ignored) prints per-table node/slot stats
to size scratch when tuning.

Measured on RTX 5090 (nsys, ethrex): constraint_composition_kernel
814ms -> 267ms (-67%) over the same 29 launches; ethrex 10tx
continuations ABBA 15.16s -> 14.77s.
Preprocessed tables (DECODE/BITWISE: precomputed + multiplicity column
split) skipped the fused GPU commit entirely — commit_main_trace only
tried the GPU when precomputed.is_none() — so they paid the CPU row-major
LDE plus two CPU subset Merkle trees (~2.2s thread-time of R1 'Main
commit Merkle CPU' on ethrex).

- keccak256_leaves_base_row_major_row_pair_range: column-range variant
  of the row-pair leaf kernel, byte-identical to the CPU
  commit_rows_bit_reversed_subset layout.
- coset_lde_row_major_split_trees: one row-major GPU LDE of all columns
  plus the two subset trees built on device; both node buffers download
  to host and rebuild full host trees via from_precomputed_nodes, so
  the preprocessed opening path, the process-wide precomputed-tree
  cache and disk-spill work unchanged. The shared expansion stage is
  factored into expand_row_major_on_stream (same code path as the
  existing fused commit).
- The table now gets a GpuLdeBase handle (column-major LDE + trace
  snapshot, no device tree), so its rounds 2-4 (composition, DEEP,
  barycentric) run on GPU too. Preprocessed openings short-circuit to
  the host trees via is_preprocessed, as before.
- REGISTER stays on CPU (LDE below the dispatch threshold).

Parity: split_tree_tests pins roots and opening paths against the CPU
subset commits on device; cross-binary verification of full ethrex
bundles passes both ways.

Measured on RTX 5090: ethrex 10tx continuations interleaved 3-way
14.77s -> 14.23s (cumulative -6.1% vs the pre-kernel baseline).
prove_global consumes only execution artifacts — the per-epoch cell
boundaries built by the producer, the ELF and the genesis pages — never
an epoch proof, yet it ran serially after every epoch prove finished
(~0.9s of pure tail on ethrex 10tx).

The producer now publishes each epoch's boundary (an Arc share of the
one already flowing to the epoch provers — no data copy) on a dedicated
channel, in epoch order. A scoped thread drains that channel until the
producer hangs up (last epoch prepared) and proves the global memory
argument while the tail epochs are still proving. On an epoch failure
first_err still wins and the global result is discarded; proof bytes
and bundle content are unchanged — only the schedule moves.

The epoch timeline confirms the tail is gone: the global prove runs
fully inside the window of the last three in-flight epoch proves.

Measured on RTX 5090: ethrex 10tx continuations ABBA 14.16s -> 13.66s
(-3.5%); cross-binary verification passes both ways. Day cumulative
across the three optimizations: -9.4% (15.16s -> 13.66s).
Every epoch's trace build re-parsed the ELF and regenerated the pristine
DECODE trace (~1M rows) inside the serial producer chain, plus moved a
~900K-entry pc->row map by value per epoch.

DecodeArtifacts (instruction map + pristine DECODE trace + pc->row index)
is a pure function of the ELF: prove_continuation builds it once and every
epoch's build clones the pristine trace (a memcpy) and fills its own
multiplicities; build_traces now borrows the pc->row map. The monolithic
entry point delegates and is unchanged.

Net work removal with identical trace bytes (cross-binary verification
passes). Wall-neutral within noise on a 32-core box; groundwork for
pipelining the epoch trace build out of the producer chain, where
parallel builders would otherwise each redo the ELF parse.
The continuation producer built every epoch's full trace tables inline,
so the serial chain feeding the provers was execute + collect + BUILD per
epoch (~95% of it table generation) — 7.2s of a ~18s wall on a 32-core
box, with the last epochs' proves gated on it.

The epoch trace build is now split at its real sequential boundary:

- Traces::collect_epoch (Phases 1-2): op collection over the advancing
  memory image — stays on the producer, in epoch order.
- Traces::build_from_collected (Phases 3-5): table generation — pure
  epoch-local work, runs on a small builder pool
  (LAMBDA_VM_TRACE_BUILDERS, default 2) between the producer and the
  epoch provers, bounded channels capping peak memory.

The cross-epoch chain no longer touches traces: the boundary derives
from CollectedEpoch::touched_memory_cells (same function, same immutable
memory_state as the build) and the next epoch's register init from
register::fini_from_final_state — a trace-free mirror of the REGISTER
FINI column, pinned by fini_from_final_state_matches_trace. PAGE tables
are the build's only image consumers and continuation mode skips them,
so builders need no image snapshot.

Measured on a 32-core RTX 5090 box (ethrex 10tx continuations): the
producer chain drops 7.2s -> 2.9s and the first three proves start ~1s
earlier, but the wall ties (~18s) — the box is bound by total CPU work,
which this change conserves (proves and the global dilate to absorb the
freed schedule). A K/builders sweep confirms K=3/B=2 stays optimal.
Expected to pay on wider boxes where idle cores can absorb the
parallelism; groundwork for cutting per-epoch CPU work (AIR/capture
caching), which is the binding constraint on narrow boxes.
Constructing an AirWithBuses runs every constraint body through a
MetaBuilder, and the first constraint_program() runs them again for the
IR capture — for ECDAS/ECSM/KECCAK_RND (16-25K IR nodes) that dominates
AIR construction (0.78s per VmAirs::new on ethrex). Continuation epochs
rebuild the full AIR set per epoch and shard tables build one instance
per shard, so the same walks re-ran dozens of times per prove.

build_air now keeps a process-wide prototype cache keyed by (table name,
proof options): the prototype is built and pre-captured once, and every
later request clones it — Clone on AirWithBuses copies the derived meta,
LogUp layout and the captured IR inside the OnceLock, never re-running
the bodies. PAGE stays correct because its page base is part of its
name. with_name/with_preprocessed apply to the caller's clone; the
cached prototype stays pristine.

Wall-neutral within noise on the 32-core box (the removed work is a few
core-seconds against a ~580 core-second prove); cross-binary
verification passes both ways. Also cuts AIR construction out of the
monolithic path and the test suites.
…flow

The toolkit's continuation instrumentation assumed the sequential epoch
loop. With the producer/builder/prover pipeline the stages run on
different threads, so the spans move to where the work actually happens:

- prove_continuation_total root span + timeline reset at entry, drained
  at the end exactly like the monolithic path (stdout tree +
  LAMBDA_VM_TIMELINE_JSON for phase_table.py).
- epoch_execute / epoch_collect on the producer, epoch_trace_build on
  the builder pool, epoch_prove on the prove workers — each prove/build/
  collect also opens an NVTX range with per-epoch identity
  (epoch_*[i=N]) for Nsight timelines.
- Spans close BEFORE blocking channel sends, so backpressure waits are
  never booked as work.
- prove_global span on the overlapped global-prove thread.
@ColoCarletti

Copy link
Copy Markdown
Collaborator Author

/bench-gpu

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

GPU Benchmark (ABBA) — b4fa972b11 vs main (14 pairs)

RTX 5090 · Vast.ai datacenter @ $0.8423148148148147/hr · prover/cuda · ethrex 100tx continuations · drift-free A/B/B/A

=== ABBA paired result  (improvement: - = PR faster) ===
  pairs: 14   mean A (PR): 119.402s   mean B (base): 225.240s

  [parametric] paired-t   mean -46.99%   sd 0.44%   se 0.12%
               95% CI: [-47.24%, -46.73%]   (t df=13 = 2.16)
  [robust]     median -46.89%   Wilcoxon W+=0 W-=105  p(exact)=0.0001  (z=-3.26)

  --- server stability (this run; compare across servers) ---
  run-to-run jitter:    A CV 0.92%   B CV 0.29%        (lower = steadier)
  within-session drift: +1.07% over the run, 1st->2nd half +0.49%
    (jitter -> Tier-1 cached gate floor; drift -> whether the cached baseline can be trusted)

  VERDICT: REAL IMPROVEMENT - PR faster by ~46.99% (t-CI and Wilcoxon agree)

  raw pairs: /tmp/abba_run/pairs.csv

- = PR faster. Trust the verdict when paired-t and Wilcoxon agree.

ColoCarletti and others added 9 commits July 27, 2026 10:24
Domain and LdeTwiddles are now shared across epochs and concurrent epoch
provers via a process-wide cache keyed by (field, trace_length, blowup,
coset_offset). The OOD barycentric constants, FRI inverse twiddles, and
the d=2 decomposition inverses hang off them as lazy per-domain values
instead of being rebuilt (each an LDE-size-order batch inversion or
clone) per table per epoch.
Each boundary constraint paid its own LDE-size batch inversion even when
sharing the step with its neighbours, and the vectors are identical for
every table and epoch on the same domain. The inverted vector now lives
in the shared domain, keyed by step, and constraints hold an Arc to it.
Upload each distinct column once (GpuBaseVec, cached keyed by its host
Arc — storing the Arc pins the allocation so the key can never alias)
and D2D-copy into each dispatch's flat buffer, instead of re-uploading
tens of MB per table per epoch over PCIe.
The composition evaluations stay resident after the fused kernel; a
pointwise kernel decomposes them into the H0/H1 slabs, the batched slab
LDE extends both halves with no H2D, and the parts handle feeds R4 DEEP.
One drain of the final evaluations (still read by the commit tree and
the query openings) replaces four codeword-sized PCIe trips per table
per epoch. Falls back to downloading H and running the host decompose
on any device failure.
The fully-resident DEEP arm keeps its output on device, bit-reverses it
into FRI order with a permutation kernel, and hands the buffer to the
FRI fold state as its working codeword — removing the download /
CPU-bit-reverse / re-upload round trip. The commit loop is shared
between the host and device entries and restores the transcript on any
mid-loop failure so the CPU path reruns cleanly.
The shared domain caches ran the parallel batch inversion inside their
OnceLock initializers. A rayon worker that starts such an initialization
farms chunks to the pool while sibling workers block on the same cell;
with every worker parked the chunks never run and the prove deadlocks
(observed as a full-process futex stall). Initializers now use the
sequential inversion, and domain construction pre-fills every lazy cell
from the setup thread so pool workers never run — or wait on — an
initializer mid-prove.
@ColoCarletti ColoCarletti changed the title Gpu opt 5090 perf(prover): halve GPU continuation proving time Jul 27, 2026

@MauroToscano MauroToscano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ AI-generated review. Produced by an automated agent and posted from this account — not written by hand. Findings below are leads to verify, not confirmed conclusions.

This batch adds an overlapped epoch pipeline for continuations, CUDA LDE/commit work, and a profiling toolkit. The test shape is right — the dim-split slot-file interpreter is differentially tested against the CPU model, the fused preprocessed-commit path has a CPU parity check, and fini_from_final_state pins the register fini to the trace — and the ABBA numbers back the perf claim. Two blocking issues, plus cleanup.

prover/src/continuation.rs:1366 — the new epoch pipeline deadlocks instead of returning Err on any downstream failure. rx and build_rx are Mutex<Receiver> locals declared outside std::thread::scope (1130-1131), so they stay alive for the whole scope and a send can never observe disconnection; tx.send(...).is_err() (1237) and build_tx.send(Ok(job)).is_err() (1366) are therefore dead branches, contradicting their own "the downstream side hung up, stop quietly" comments.

The chain: a worker's prove_epoch Err (1174-1177, reachable from Prover::multi_prove) or a builder error forwarded as tx.send(Err(e)) (1241-1244) sets first_err; every worker then exits at 1148-1150 and every builder at 1196-1198 after at most one more message. Nothing drains build_rx after that, and the producer never consults first_err — its only exits are is_final and the dead send check — so once the ~3 jobs of drain slack are consumed it blocks forever in build_tx.send, producer.join() (1442) never returns, the scope cannot exit, and the overlapped global thread stays wedged in boundary_rx.recv() (1399) because boundary_tx was moved into the stuck producer. Net: a failure with ~3+ epochs still pending turns a clean Err into a permanent hang of the main continuation entry point.

Scope limits: producer-originated errors (e.g. HaltInNonFinalEpoch from Traces::collect_epoch) shut down fine because the producer drops build_tx; a failure on or near the final epoch also returns normally; a single panicking prover with workers >= 2 still completes.

Fix: check first_err at the top of the producer loop and return early (draining in the workers alone unblocks the scope but makes the producer execute the whole remaining program first), and have workers/builders keep draining to close rather than returning on the error path. This path has zero coverage — the only Err-expecting continuation test returns before the pipeline is built, and trace_builder_tests.rs:1009 calls Traces::from_image_and_logs directly — so a negative test is needed: cfg(test) fault injection in prove_epoch or build_from_collected, enough epochs to exceed the slack, asserting prove_continuation returns Err, wrapped in a timeout so a regression fails instead of wedging CI.

crypto/math-cuda/src/device.rs:632PendingD2H holds the staging slot's MutexGuard but has no Drop, so every ? between enqueue and wait releases the slot with cuMemcpyDtoHAsync still writing into the pinned slab. Three sites do exactly that: coset_lde_row_major_inner (pending at 592-602, then launch_row_to_col_major(...)? at 606 which allocates, plus take_event()?/record(..)? at 609-610, wait only at 616), coset_lde_row_major_split_trees (759 → 764 → 770), and coset_lde_batch_base_into_with_merkle_tree_inner (1541, held across d2h_bytes_via_pinned_hashes(...)? at 1543, whose nested ensure_capacity can fail on pinned-host OOM).

Slots are per-rayon-worker (529-536), so the next user is usually the same worker on a later call — on a different stream, since next_stream round-robins, so nothing orders them — and slot 0 is shared by every non-rayon thread. That next user either grows the slot (ensure_capacity cuMemFreeHosts the old pointer at 63-69, and the in-flight DMA writes into a freed mapping) or repacks the slab for its own H2D (as_mut_slice + copy_from_slice, lde.rs:1170-1180) and the stale DMA silently corrupts that upload. These Errs are meant to be survivable — gpu_lde.rs falls back to CPU on Err at 467-471, 1042, 1114, 1656, and the crate ships a test-cuda-faults injector — so a recoverable OOM leaves the process running with a live DMA into an unowned slab. ? does not poison the mutex; only the panic path is accidentally safe. Fix: impl Drop for PendingD2H doing a best-effort self.staging.sync_event() (ignoring the error) before the guard releases, so the invariant holds by construction. That also covers the lower-risk holds in deep.rs:249, fri.rs:223, logup.rs:205 and constraint_interp.rs:140, where the intervening ? happens to be a sync/memcpy_dtoh that drains the copy on success.

prover/src/continuation.rs:1120 / :1433LAMBDA_VM_EPOCH_CONCURRENCY defaults to 3 at 1120, but the sizing comment at 1433 says "the default is a conservative 2". Both lines are new in this PR, so one is stale on arrival, and it is not only a comment: main proves epochs strictly sequentially, while the new pipeline holds up to ~7 epoch-sized working sets concurrently per the comment at 1103-1104 (1 collected + 2 building + 1 queued + 3 proving), on CPU-only builds too. Nothing sizes this from available memory — unwrap_or(3) is unconditional — and the per-prove VRAM admission control does not coordinate across concurrent proves: crypto/stark/src/prover.rs:2548-2552 hands each prove backend().vram_budget_bytes(), which is 80% of total device memory (crypto/math-cuda/src/device.rs:252-275), so all three plan plan_table_chunks against the whole card. 24 GB cards look safe (the measured ~15 GB peak is already the 3-worker config; the standalone ~9 GB per-epoch peak does not stack on top of it), but ≤16 GB cards, larger EPOCH_SIZE_LOG2, and CPU host RAM are exposed. Pick a number and justify it in one place — default 2 with 3 opt-in, or derive it from the detected budget — and delete whichever comment loses.

scripts/profiling/README.md:149 — four doc sites promise math-cuda entry-point NVTX ranges that do not exist. Nothing under crypto/math-cuda/src/ references nvtx except lib.rs:18 and nvtx.rs itself; the ~40 crate::nvtx::Range::fmt(...) guards present at a48f5d4 (e.g. lde.rs lde_tree_base[n=.. m=.. bf=..], barycentric.rs bary_base[..]) were dropped when 530f423 rewrote those files. They are still claimed at README.md:149 ("every math-cuda public entry point pushes a range with its shape"), crypto/math-cuda/Cargo.toml:40, crypto/stark/Cargo.toml:59, and crypto/math-cuda/src/nvtx.rs:2. The toolkit is not broken — instruments spans mirrored via instruments.rs:89 and the LAMBDA_VM_NSYS_CAPTURE_SPAN gating at 92/112 both work, so the README's "verify ranges appear" check passes — but nsys_phase_busy.py:318's "## math-cuda entry points (innermost NVTX range)" table computes fine_key = base_name(chain[-1]), which now resolves to the innermost instruments span, so that section is mislabeled and mostly duplicates the coarse table above it. Either restore the ranges behind the nvtx feature (zero-cost when disabled) or fix all four doc sites and drop that section from the report.

Nits:

  • ~59 statements across crypto/math-cuda are wrapped in braces that bind nothing and scope no borrow (let out = { stream.clone_dtoh(&out_dev)? };, { stream.synchronize()?; }, multi-line let (a, b, c) = { ( ..?, ..?, ..? ) };) where main has the plain form (barycentric.rs:48, :75-77); densest in barycentric.rs (22), merkle.rs (10), lde.rs (~9 inert plus 5 real borrow scopes), logup.rs (8), inverse.rs (5), ntt.rs and fri.rs (3), deep.rs (2), constraint_interp.rs (1) — semantically identical, but they read as if a guard were deleted, and inverse.rs:71, logup.rs:202, deep.rs:246, deep.rs:399 still say "the labelled sync" with no label present; unwrap them (keeping lde.rs's five) or fill the holes with the nvtx::Range guards this PR already ships.
  • scripts/profiling/__pycache__/{nsys_phase_busy,nvml_sampler,phase_table,timeline_to_perfetto}.cpython-313.pyc are checked in (~35 KB, first tracked .pyc files in the repo) with no __pycache__/*.pyc rule in any .gitignore — timestamp-invalidated so no stale-bytecode risk, but they persist in history and show as modified for any 3.13 dev importing these modules; git rm -r --cached scripts/profiling/__pycache__ plus a root .gitignore entry.

…ts senders

The prove/build channel receivers live in the outer scope, so a worker
that returned on error left the bounded senders parked in send() with no
consumer — any mid-run proving error hung prove_continuation forever
instead of surfacing. Workers now drain-and-discard until the channels
disconnect, the producer stops executing epochs once an error is
recorded, and the global-prove thread skips its (whole-prove-sized) run
when the bundle can no longer be assembled.
- PendingD2H now synchronizes on drop: an error between enqueue and wait
  no longer releases the pinned slab to reuse/free while the DMA is in
  flight.
- domain_and_twiddles re-checks the cache under the insert lock so a
  build race can't pin a duplicate instance's columns in the
  pointer-keyed device caches.
- Hard-assert b_z_inv column length at the D2D copy (a short column left
  uninitialized VRAM in the kernel's window), mirror the batched-LDE
  input asserts in the split-trees entry, gate mismatched FRI twiddles
  to the CPU path, and pin the ext3 tower in the shared FRI drive.
- Refresh the event-tracking safety note to the wait_ready_on contract.
A builder-injected fault (keyed by a magic private input, so it is
stateless and inert for every real caller and for concurrent tests)
fails epoch 3 of a ~9-epoch prove — enough pending work past the
bounded channels' slack that a shutdown regression wedges instead of
returning. The test runs the prove under a timeout so that regression
fails CI rather than hanging it.
ColoCarletti and others added 4 commits July 27, 2026 18:35
- The per-entry-point NVTX shape ranges were dropped when the math-cuda
  pipelines were rewritten; four doc sites still promised them and the
  nsys report mislabeled its innermost-range table. Align them with
  what the nvtx feature actually emits (mirrored instruments spans).
- Untrack scripts/profiling/__pycache__ and ignore Python bytecode.
- Remove ~86 brace wrappers in math-cuda left inert by the async-DMA
  refactor (kept the ones that scope real borrows) and reword three
  comments that referenced a deleted sync label.
@ColoCarletti

Copy link
Copy Markdown
Collaborator Author

/bench-gpu

@ColoCarletti

Copy link
Copy Markdown
Collaborator Author

/bench-gpu

The per-epoch report filtered `epoch[i=N]` ranges, but the pipelined
continuation flow emits `epoch_collect[i=N]`, `epoch_trace_build[i=N]`
and `epoch_prove[i=N]` — the section silently never printed. Group by
stage instead (prove first: a gap between prove instances now means the
pipeline stopped feeding the prover, not missing overlap), and update the
README names to match.
set_staging_size_hints and its plumbing (per-slot Arc hint, ensure_capacity
branch) were dead since the pre-sizing experiment was measured as a regression
on the 5090 — remove them so it can't be silently re-enabled. Update comments
that drifted from the code (preprocessed tables keep a device handle, boundary
zerofiers are D2D from the resident cache, register chaining binds
fini_from_final_state, the continuations capture span is epoch_prove), point
the profiling examples at the fixture generator (the .bin files are not
checked in), gitignore reports/, and untrack the working notes.
…dump

An Err between enqueueing a DMA that reads a pinned-staging slab and its
sync used to release the slot mutex with the copy in flight (the next locker
can realloc the slab mid-DMA): async_dtoh_via now drains the stream when the
event record fails, and the batched-LDE upload window holds a drain-on-error
guard. try_fri_commit_gpu gains the inv_twiddles length gate its from_dev
sibling had (degrade to CPU instead of panicking on a wiring bug), with a
debug_assert on the CPU path. ir_stats_dump was never declared in
tests/mod.rs (its documented command matched nothing), and capture_env.sh
reported every tree as dirty.
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