feat(prover): spill the aux LDE under StorageMode::Disk - #933
Open
MauroToscano wants to merge 1 commit into
Open
feat(prover): spill the aux LDE under StorageMode::Disk#933MauroToscano wants to merge 1 commit into
MauroToscano wants to merge 1 commit into
Conversation
Under `StorageMode::Disk` the aux trace and the aux Merkle tree are both spilled, but the aux LDE itself — `lde_size × aux_cols` ext3 elements at 24 B each, the largest of the three — had no spill path and stayed heap-resident from the aux commit through rounds 2-4. Carry it as a `Table` (the crate's existing mmap-backed row-major container, already used for spilled trace tables) instead of a bare `Vec`, and spill it right after the aux commit. It is write-once at that point and read-only afterwards, so mmap-backing it frees the heap buffer for the whole rounds 2-4 window and lets the OS evict the pages under memory pressure. Behaviour-neutral off `disk-spill` and off Disk mode: the `Table` arm without an mmap backing is the same buffer and the same indexing. Measured with prover/tests/calibration.rs (fib_iterative_372k, 5 runs per arm): peak heap under FORCE_DISK_SPILL=1 goes from 2.615-2.825 GB to 2.290-2.378 GB — non-overlapping ranges, about -403 MB / -14.8%. Ram mode is unchanged.
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.
What
Under
StorageMode::Disk+feature = "disk-spill"the aux stage already spills the aux trace (spill_aux_to_disk) and the aux Merkle tree (spill_tree), but the aux LDE —lde_size × aux_colsext3 elements at 24 B each, the largest of the three by a wide margin — had no spill path. It stayed heap-resident from the aux commit all the way through rounds 2-4, so Disk mode reclaimed almost nothing of the aux stage's actual footprint (tree spilling covers ~32 B/row × 2 against the LDE's 24 B ×aux_colsper row).Mechanism
The aux LDE is write-once at the aux commit and read-only afterwards (constraint evaluation and the DEEP/OOD scan read it sequentially; query openings hit a handful of random rows via
gather_aux_row). That is exactly the shape a write-once mmap wants: spill it once, read it back page-by-page, and let the OS evict pages under pressure.Rather than inventing a second mmap container, this reuses the one the codebase already has:
Table<F>, the row-major field-element buffer withspill_to_disk()/row_major_data()/get()/get_row()/advise_drop_cache()that already backs spilled trace tables.Lde.auxandLDETraceTable::aux_datachange from(Vec<FieldElement<E>>, usize)/Vec<FieldElement<E>>toTable<E>(itswidthcarries the column count).commit_rows_bit_reversed+spill_tree, i.e. at the point where nothing reads it again until rounds 2-4.Table::from_row_majoris added as a non-validating constructor:Table::new'sdebug_assert!(validate_2d_structure(..))clones the entire buffer into aVec<Vec<_>>, which is fine for trace-sized data and prohibitive for LDE-sized data in debug builds.ProvingError::DiskSpill; nounwrap/expecton fallible I/O.Behaviour-neutral off the feature and off Disk mode: an unspilled
Tableis the same buffer with the same row-major indexing.get/get_rowgained#[inline]since they are now on the hot aux read path. The CUDA arms are untouched apart from the mechanical type change (aux_data.is_empty()→aux_data.row_major_data().is_empty(), which is also more correct: a spilled aux LDE is no longer mistaken for a device-only empty buffer).Measured
cargo test --release -p lambda-vm-prover --features disk-spill --test calibration -- --nocapture(fib_iterative_372k), 5 runs per arm. The sampler polls every 10 ms, so single runs are noisy — ranges are given rather than point estimates.origin/main(d898a42)FORCE_DISK_SPILL=1)Disk mode: about −403 MB / −14.8 % peak heap, with non-overlapping ranges across all 10 runs. Ram mode is statistically indistinguishable, as intended — the two ranges overlap almost entirely and the spread is dominated by sampler noise.
peak_bytesis unchanged and still counts the aux LDE for every table, so it remains a conservative over-estimate; the calibration assert only gets more headroom.Scope note: the main LDE
Lde.main/LDETraceTable::main_datais the same shape — write-once incommit_main_trace, read-only afterwards — and would now spill through the identical mechanism. It is also the bigger target onmain, since the Round 1 main commit is a phase-wide barrier and all N main LDEs are live at once. Deliberately left out to keep this diff reviewable; it is a clean follow-up now that the container is in place.Interaction with #897
On
mainthe aux LDEs are k-bounded (k = cores/3co-resident), which is why the win here is a few hundred MB rather than multiple GB. #897 makes allnum_airsof them co-resident on the CPU path (+4.6 GB measured on the real block) — at which point this spill is what makes Disk mode actually reclaim that memory instead of leaving the dominant term on the heap. The code region (aux_stageinternals) is untouched by #897, so this merges cleanly either way and is based onmainso it can land independently.Review order
Part of a 4-PR series around the CPU scheduler change:
fix/pr897-review-comments→ into fix: use CPU-aware prover scheduler #897 — comment/doc alignment + simplifications; review first, merges into fix: use CPU-aware prover scheduler #897.fix/cpu-prover-scheduler) →main— the scheduler fix (+9.8 % peak heap / −12.5 % prove time, measured on the real block).fix/peak-bytes-estimator→main— makesauto_storage::peak_bytesmodel the ~10 AIR kinds it omits, adds a keccak calibration case, wirescalibration.rsinto CI.fix/aux-lde-spill→main(this PR) — reviewed last: its payoff appears mainly post-fix: use CPU-aware prover scheduler #897 (all-N aux residency), and it only engages when the estimator (fixed in PR 3) actually selects Disk. On keccak-heavy workloads the current estimator under-predicts and picksRam, so this spill path stays dormant there untilfix/peak-bytes-estimatorlands (orFORCE_DISK_SPILL=1is set).Validation
FORCE_DISK_SPILL=1 cargo test --release -p lambda-vm-prover --features disk-spill -- disk_spill count_table_lengths— 3 passed, 1 failed:count_table_lengths_matches_nonempty_hint_tracepanics on a missinghint_min.elffixture (added by Feat/hint ecall #876, not built in the local artifacts dir). Verified identical failure on cleanorigin/main, so it is a fixture gap, not a regression.cargo test --release -p stark --features disk-spill disk_spill— 5 passed.cargo test --release -p stark— 217 passed.make lint— all four clippy passes (default,--no-default-features+debug-checks,disk-spill,cuda) clean,cargo fmt --check --allclean.