fix(continuation): prove the global memory argument AFTER the epochs, not beside them - #981
Open
MauroToscano wants to merge 1 commit into
Open
fix(continuation): prove the global memory argument AFTER the epochs, not beside them#981MauroToscano wants to merge 1 commit into
MauroToscano wants to merge 1 commit into
Conversation
… not beside them The global prove ran on its own scoped thread so it would overlap the tail epochs. It is a second `stark::prover::multi_prove`, and every `multi_prove` builds its OWN `VramGate` from the whole device budget (80% of total device memory). Two of them running at once therefore put 2x the card's admission budget on one GPU with nothing summing the two, and each additionally holds a round-1 working set no gate counts at all: a table's LDE, trace snapshot and Merkle tree stay device-resident from its commit until its own rounds task ends, so at the R1 barrier the card holds `sum_t n_t * (40*m_t + 128)` bytes at blowup 4 while the gate's `used` has fallen back to zero. Measured on an RTX 5090 (31.40 GiB driver-visible): the overlap window opens about `builders + 2` epochs before the end -- the producer cannot run further ahead than the bounded build/prove channels allow, and the global prove blocks until the boundary channel closes -- and inside that window the card ran out with tables from BOTH proves aborting in the same instant. The abort set is the evidence: `main 9 / aux 1` is `l2g_global_air`, which exists only in `prove_global` (the epoch-local `l2g_memory_air` carries the range-check interactions and is 6 buses wide, never 1 aux column), while BITWISE and LOAD exist only in an epoch prove. Serialising costs this proof's own wall time, which the overlap used to hide. It changes no proof bytes: the global proof consumes only execution artifacts -- boundaries, ELF, genesis pages -- never an epoch proof, so the schedule was always free to choose. The boundary channel is unbounded, so the producer still never blocks on it; it is now drained once, after the epoch scope joins.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(continuation): prove the global memory argument AFTER the epochs, not beside them
pt/serialise-global@111ca721→per-table-gpu. One file,prover/src/continuation.rs,+43 / −55. One signed commit.
make lintexit 0 over all three feature sets;make fmtano-op.
Measured, one RTX 5090 (31.40 GiB driver-visible), 2^21 / blowup 4,
TABLE_PARALLELISM=4,10 Hz VRAM sampling, census 525,932,288 cells identical in every arm:
LAMBDA_VM_VRAM_BUDGET_MB=16000The 7.3 GiB is this change's. The budget did nothing to the peak — that is what the first row is
for, and it is why this lands on its own rather than behind a scheduler rewrite.
What changes
The global prove ran on its own scoped thread so it would overlap the tail epochs. That
scope.spawnis deleted.boundary_rxnow stays outside the epoch scope and is drained once withtry_iter()after the scope joins and after the existingfirst_errcheck;prove_globalrunsinline there. The
global_resultmutex and theGlobalResultalias go with it — the errorpropagates with a plain
?instead of through a slot, and the "an epoch already failed, skip theglobal prove" guard is subsumed by the
first_errearly return that was already there. Thecomment at the channel declaration, which asserted the overlap, is rewritten so the two ends of
the mechanism cannot drift apart again.
★ This is a revert of a schedule optimisation, not a redesign. The safety argument is the
overlap's own comment:
Both halves still hold, in the other direction. Nothing about the argument moved: the same
boundaries, the same
touched_page_bases, the sameglobal_transcript, the samemulti_provecall with the same inputs in the same order.Two mechanical notes. The boundary channel is unbounded, so the producer never blocked on it
before and does not now — the epoch pipeline's capacity-1 build and prove channels remain the only
backpressure. And the boundaries were always
Arcshares of the ones the epoch prover holds, sobuffering them in the channel instead of in a thread-local
Veccosts nothing: same allocations,same lifetimes, one fewer copy of the pointer.
The mechanism
Every
multi_provebuilds its ownVramGate::new(vram_budget)(crypto/stark/src/prover.rs,the R1 pre-pass), and
vram_budgetisdetect_vram_budget_bytes— 80 % of total device memory.An epoch prove and the global prove are two
multi_provecalls. Overlapped, they putwith nothing summing the two. Neither gate can see the other, and neither is wrong about anything
it was asked.
⚠ And each additionally holds a working set no gate counts at all. The R1 main commit's handle
keeps the column-major LDE, the trace-domain snapshot and the full Merkle node buffer resident
(
math-cuda/src/lde.rs::coset_lde_row_major_with_merkle_tree_keep—tree: Some(tree),retain_trace_col_majorhard-coded true) from the commit until that table's own rounds task ends.Round 1 is a hard barrier — every root must be in the transcript before the LogUp challenges are
sampled — so at the R1/R2 transition the card holds
while the gate's
usedhas fallen back to zero. Measured at this posture: 11.86 GiB for anepoch prove, 6.0 GiB for the global prove. Two proves, two barriers, two budgets, one card.
The abort set identified both proves, which is why this is a mechanism and not a story. Eight
threads aborted in the same instant through
gpu_lde::refuse_host_recovery, and their shapespartition cleanly:
main 9 / aux 1×6l2g_global_airprove_globalmain 21 / aux 5@ LDE 2^22main 18 / aux 3@ LDE 2^21local_to_global::cols::NUM_COLUMNSis 9 andbus_interactionsis 2 interactions, soaux = ⌈2/2⌉ = 1. The epoch-locall2g_memory_aircarriesmemory_bus_interactions(2) plusrange_check_interactions(1 +|RANGE_CHECKED_HALFWORDS|+ 1 = 4) = six, so aux 3, never 1.Nothing else in the registry is 9 wide. BITWISE and LOAD are absent from
prove_global's tableset entirely, which is
l2g_global_air× epochs plusglobal_memory_air× touched pages.The overlapped run's VRAM trace says the same thing in one shape: a single 31 s hump spanning 58
tables in one monotone ladder — one prove's worth of tables laddering down on top of another
prove's tooth.
⇒ The window opens about
builders + 2epochs before the end, and never earlier. The producercannot run further ahead than the capacity-1 build channel, two builders and the capacity-1 prove
channel allow, and the global thread blocks on
boundary_rxuntilboundary_txdrops with theproducer. That is why the same configuration was a coin flip: it aborted twice and succeeded once,
because whether the hump lands on a tooth's peak is a race whose width varies run to run.
The measurement
Base 19 epochs in 251.8 s, PROVED AND VERIFIED, total 459.72 s.
Seven quantities were pre-registered before the run, with falsifiers.
prove_continuation< 31,500k=4survives★ P1 and P4 came in low for one reason, and it was an error of mine that two mistakes had been
hiding. The prediction priced the co-resident set as "fill the byte budget", but
VramGate::acquireblocks; it does not skip ahead —run_admittedworkers pull indices inheaviest-first walk order and queue behind each other. At 15.625 GiB the two heaviest 2^21 tables
are
est9.80 + 9.56 = 19.36 GiB, so the second blocks and one heavy table proves at atime. That is a 34 % over-estimate of the incremental set (9.84 predicted against 6.54 actual),
and it was cancelling a 15 % under-estimate of the barrier (10.3 predicted against the 11.86
the tooth implies). Net +1.90 GiB, which is exactly the gap. The two errors agreeing is the
failure mode this campaign has on the record, and the count matching was what concealed it: fix
both, and do not quote a tooth match as corroboration of either input.
★ P5 is reconciled rather than falsified, and the honest verdict is "not detectable". The
quantity was mis-named — the added work lands after the epoch loop, so it cannot appear in a
timer around the epoch loop. In the total it appears as −15 s, not +31. The overlapped 31 s was
a lock-queue artefact:
gpu_lde::r2_serialize_guardis a process-widestatic Mutexserialisingevery table's R2 submission window across proves, not just across tables, and the two proves
were also sharing one rayon pool. Alone, the global prove ran ~10 s — 3.1× faster — and the
epoch tail got the lock back. ⇒ The cost of serialising is not 31 s of margin bought with wall
time; removing the contention paid for the serialisation.
Two independent readings of the global prove's working set, which is the number the whole account
turns on:
They agree to 288 MiB (3 %) — the increment the overlap adds to a tooth is the global
prove's entire working set, which is what "no gate subtracts it" predicts and nothing else does.
The caveat, stated rather than buried
⚠ Serialising can convert a shared overflow into a solo one. Alone, the global prove's tooth is
floor + B_global + Σ live increments, andB_globalgrows with the touched-page countPat2^18 × 288 B = 0.0703 GiBperglobal_memory_air. At the default budget it admits ~17 tablesconcurrently, and:
⇒ Run this with
LAMBDA_VM_VRAM_BUDGET_MB=16000until the gate is process-wide and itsscheduler budget is decoupled from
admit's per-table ceiling. At 16000 the same tooth is21,904 MiB at
P = 0and 29,104 atP = 100, and the constant still clears the largest singledispatch (≈6.7 GiB for a 2^21 main commit) with room, so no transient is declined — which
matters, because a declined transient on a device-only table is
refuse_host_recovery, theabort this PR exists to remove.
⚠
Pfor this block is unmeasured. It isContinuationProof::touched_page_bases.len(), or thecount of
main 4 / aux 1entries in the global prove's[prover] table walk R1line. Read itbefore running at the default budget.
What this does not fix
B_epochis invisible to the gate whether or not theglobal prove overlaps, and at this posture it is 11.86 GiB of the 24,536 MiB peak. The durable
form is one process-wide
VramGate, seeded with the barrier and charging tasks only theirincrement, with its budget decoupled from
admit's ceiling. This PR is what makes thatgate's budget satisfiable: it turns
B_epoch + B_globalintomax(B_epoch, B_global), whichat 2^21 is 15.8 GiB of headroom against 6.3.
B_epoch(2^22) ≈ 23.7 GiBagainst 27.7 GiB of usable working set, with a single MEMWincremental at 11.3 GiB — no
k, no budget and no schedule fits that. It needs the barrierlevers (free the trace snapshot at R1 exit, −8·C_main; download the main Merkle tree after the
root, −128·R_tot; together −29…39 %) and a row cap on the tables
MaxRowsConfigdoes notcover.
here and is measured separately.
Gate — MEASURED
The ninth is
continuation::tests::test_prover_panic_mid_pipeline_returns_err, and it is the oneexception this gate reserved in advance. Every other name matches the eight below, byte for byte.
⚠ Stated precisely, because the looser version would overclaim in this PR's favour: that red neither
implicates nor exonerates this change. The bound it failed is a 300 s whole-call timeout —
to-panic + shutdown > 300— which has two summands and isolates neither. This change deletes ascope.spawnon the shutdown path, so it could only ever move the shutdown half, and a whole-callbound does not measure that half. ⇒ The failing summand is unknown, and this PR is not gated on it.
That the failure is not this change's is established separately: it appears on branches without this
commit — #975 read the same ninth name at
fdd2e0d1, and passed on a re-run at the same sha, so it isintermittent under suite load. Its diagnosis is in flight on a separate branch and does not belong here.
ⓘ For the record on what a split bound shows: an instrumented run measured
total 490.19 s = to-panic 489.10 s + shutdown 1.09 s— a healthy 1.09 s shutdown inside a run this300 s bound would have failed. That is why the summand matters and why the count alone does not settle it.
Gate — as pre-registered
Pre-registered before the run as a name set, not a count — a count is invariant under both a
regression and a substitution, which is how three runs of #975 read
1090 / 9 / 46twice with adifferent failure the second time., not a count — a count is invariant under both a
regression and a substitution, which is how three runs of #975 read
1090 / 9 / 46twice with adifferent failure the second time.
The eight, byte-identical across #973, #974 and #975 (seven are the RPX pin's, one —
the_closure…—pre-dates the pin; the pin registered ten and the format deletion removed the two batched census tests):
Extracted mechanically, never read off the screen:
⚠ A ninth red is acceptable in exactly one case: if it is
continuation::tests::test_prover_panic_mid_pipeline_returns_err. That test bounds an honestprove at 300 s, which binds under load rather than measuring the wedge it was written for; the
mechanism is named and the fix is separate, on
pt/panic-timer. Any other ninth name is aregression in this change and blocks it — the point of a set is that a substitution cannot pass.
starkandmath-cudaare untouched by a change confined toprover/src/continuation.rs, sotheir counts must be unmoved.