Skip to content

feat(prover): spill the aux LDE under StorageMode::Disk - #933

Open
MauroToscano wants to merge 1 commit into
mainfrom
fix/aux-lde-spill
Open

feat(prover): spill the aux LDE under StorageMode::Disk#933
MauroToscano wants to merge 1 commit into
mainfrom
fix/aux-lde-spill

Conversation

@MauroToscano

Copy link
Copy Markdown
Contributor

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 LDElde_size × aux_cols ext3 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_cols per 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 with spill_to_disk() / row_major_data() / get() / get_row() / advise_drop_cache() that already backs spilled trace tables.

  • Lde.aux and LDETraceTable::aux_data change from (Vec<FieldElement<E>>, usize) / Vec<FieldElement<E>> to Table<E> (its width carries the column count).
  • The CPU aux-commit path wraps the LDE buffer and spills it immediately after commit_rows_bit_reversed + spill_tree, i.e. at the point where nothing reads it again until rounds 2-4.
  • Table::from_row_major is added as a non-validating constructor: Table::new's debug_assert!(validate_2d_structure(..)) clones the entire buffer into a Vec<Vec<_>>, which is fine for trace-sized data and prohibitive for LDE-sized data in debug builds.
  • I/O errors surface as ProvingError::DiskSpill; no unwrap/expect on fallible I/O.

Behaviour-neutral off the feature and off Disk mode: an unspilled Table is the same buffer with the same row-major indexing. get/get_row gained #[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.

arm origin/main (d898a42) this branch
Disk (FORCE_DISK_SPILL=1) 2.615 – 2.825 GB 2.290 – 2.378 GB
Ram (auto) 2.988 – 4.344 GB 3.940 – 4.337 GB

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_bytes is 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_data is the same shape — write-once in commit_main_trace, read-only afterwards — and would now spill through the identical mechanism. It is also the bigger target on main, 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 main the aux LDEs are k-bounded (k = cores/3 co-resident), which is why the win here is a few hundred MB rather than multiple GB. #897 makes all num_airs of 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_stage internals) is untouched by #897, so this merges cleanly either way and is based on main so it can land independently.

Review order

Part of a 4-PR series around the CPU scheduler change:

  1. 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.
  2. 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).
  3. fix/peak-bytes-estimatormain — makes auto_storage::peak_bytes model the ~10 AIR kinds it omits, adds a keccak calibration case, wires calibration.rs into CI.
  4. fix/aux-lde-spillmain (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 picks Ram, so this spill path stays dormant there until fix/peak-bytes-estimator lands (or FORCE_DISK_SPILL=1 is 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_trace panics on a missing hint_min.elf fixture (added by Feat/hint ecall #876, not built in the local artifacts dir). Verified identical failure on clean origin/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 --all clean.

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.
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.

1 participant