Skip to content

Commit df11ef4

Browse files
committed
Replace ProofOptions::max_ram_bytes with FORCE_DISK_SPILL env var
1 parent 4693226 commit df11ef4

10 files changed

Lines changed: 131 additions & 219 deletions

File tree

crypto/stark/benches/profile_prover.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ fn main() {
2121
fri_number_of_queries: 100,
2222
coset_offset: 3,
2323
grinding_factor: 0,
24-
#[cfg(feature = "disk-spill")]
25-
max_ram_bytes: None,
2624
};
2725

2826
let num_columns = 16;

crypto/stark/benches/prover_benchmark.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ fn benchmark_proof_options() -> ProofOptions {
6161
fri_number_of_queries: 30,
6262
coset_offset: 3,
6363
grinding_factor: 0,
64-
#[cfg(feature = "disk-spill")]
65-
max_ram_bytes: None,
6664
}
6765
}
6866

crypto/stark/src/proof/options.rs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,6 @@ pub struct ProofOptions {
4545
pub fri_number_of_queries: usize,
4646
pub coset_offset: u64,
4747
pub grinding_factor: u8,
48-
/// Optional ceiling on prover RAM usage. When set, the prover spills
49-
/// trace tables and Merkle-tree nodes to mmap if the estimated peak
50-
/// exceeds this cap or system-available RAM (less a safety margin),
51-
/// whichever is smaller. LDE column vectors remain in RAM regardless.
52-
#[cfg(feature = "disk-spill")]
53-
#[serde(default)]
54-
pub max_ram_bytes: Option<u64>,
5548
}
5649

5750
impl ProofOptions {
@@ -63,8 +56,6 @@ impl ProofOptions {
6356
fri_number_of_queries: 3,
6457
coset_offset: 3,
6558
grinding_factor: 1,
66-
#[cfg(feature = "disk-spill")]
67-
max_ram_bytes: None,
6859
}
6960
}
7061
}
@@ -121,8 +112,6 @@ impl GoldilocksCubicProofOptions {
121112
fri_number_of_queries,
122113
coset_offset: 3,
123114
grinding_factor,
124-
#[cfg(feature = "disk-spill")]
125-
max_ram_bytes: None,
126115
})
127116
}
128117
}

crypto/stark/src/tests/prover_tests.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ fn test_domain_constructor() {
3333
fri_number_of_queries: 1,
3434
coset_offset,
3535
grinding_factor,
36-
#[cfg(feature = "disk-spill")]
37-
max_ram_bytes: None,
3836
};
3937

4038
let domain = Domain::new(
@@ -126,8 +124,6 @@ fn barycentric_trace_eval_matches_horner_trace_eval() {
126124
fri_number_of_queries: 1,
127125
coset_offset,
128126
grinding_factor: 0,
129-
#[cfg(feature = "disk-spill")]
130-
max_ram_bytes: None,
131127
};
132128

133129
let air = simple_fibonacci::FibonacciAIR::<GoldilocksField>::new(&proof_options);
@@ -199,8 +195,6 @@ fn test_decompose_and_extend_d2_matches_original() {
199195
fri_number_of_queries: 1,
200196
coset_offset: 3,
201197
grinding_factor: 0,
202-
#[cfg(feature = "disk-spill")]
203-
max_ram_bytes: None,
204198
};
205199

206200
// We need an AIR with composition_poly_degree_bound = 2 * trace_length.
@@ -261,16 +255,12 @@ fn test_multi_prove_mixed_coset_offsets() {
261255
fri_number_of_queries: 3,
262256
coset_offset: 3,
263257
grinding_factor: 1,
264-
#[cfg(feature = "disk-spill")]
265-
max_ram_bytes: None,
266258
};
267259
let proof_options_7 = ProofOptions {
268260
blowup_factor: 2,
269261
fri_number_of_queries: 3,
270262
coset_offset: 7,
271263
grinding_factor: 1,
272-
#[cfg(feature = "disk-spill")]
273-
max_ram_bytes: None,
274264
};
275265

276266
// Both AIRs have the same trace length and blowup, but different coset offsets.
@@ -335,8 +325,6 @@ fn test_multi_prove_dedups_shared_domain_params() {
335325
fri_number_of_queries: 3,
336326
coset_offset: 3,
337327
grinding_factor: 1,
338-
#[cfg(feature = "disk-spill")]
339-
max_ram_bytes: None,
340328
};
341329

342330
let mut trace_1 = simple_fibonacci::fibonacci_trace([Felt::from(1), Felt::from(1)], 8);
@@ -427,8 +415,6 @@ fn test_deep_poly_direct_2n_matches_interpolate_fft_extend() {
427415
fri_number_of_queries: 1,
428416
coset_offset: 3,
429417
grinding_factor: 0,
430-
#[cfg(feature = "disk-spill")]
431-
max_ram_bytes: None,
432418
};
433419

434420
let air = QuadraticAIR::<GoldilocksField>::new(&proof_options);

prover/src/auto_storage.rs

Lines changed: 101 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
//! Automatic `StorageMode` selection from an analytical peak-RAM estimate.
2+
//!
3+
//! `FORCE_DISK_SPILL` env var forces `StorageMode::Disk` regardless of the
4+
//! estimate.
25
36
use crate::tables::bitwise::{
47
NUM_ROWS as BITWISE_ROWS, bus_interactions as bitwise_buses, cols::NUM_COLUMNS as BITWISE_COLS,
@@ -27,6 +30,7 @@ use crate::tables::register::{
2730
};
2831
use crate::tables::shift::{bus_interactions as shift_buses, cols::NUM_COLUMNS as SHIFT_COLS};
2932
use crate::tables::trace_builder::TableLengths;
33+
use stark::prover::table_parallelism;
3034
use stark::storage_mode::StorageMode;
3135
use sysinfo::System;
3236

@@ -38,8 +42,8 @@ const MEMORY_CELL_BYTES: u64 = 32;
3842
const INSTRUCTION_MAP_BYTES_PER_ROW: u64 = 32;
3943

4044
/// 9/10 budget headroom for OS, other processes, and allocator slack.
41-
pub(crate) const SAFETY_FRACTION_NUM: u64 = 9;
42-
pub(crate) const SAFETY_FRACTION_DEN: u64 = 10;
45+
const SAFETY_FRACTION_NUM: u64 = 9;
46+
const SAFETY_FRACTION_DEN: u64 = 10;
4347

4448
/// `(rows, main_cols, aux_cols, num_main_merkle_trees)` for a single table.
4549
type TableSpec = (u64, u64, u64, u64);
@@ -209,13 +213,22 @@ fn table_specs(lengths: &TableLengths) -> Vec<TableSpec> {
209213
specs
210214
}
211215

216+
/// Estimates heap from `lengths` and `blowup_factor`. Picks `Disk` if the
217+
/// estimate is greater than available RAM, else `Ram`. `FORCE_DISK_SPILL` env
218+
/// var forces `Disk`.
219+
pub fn decide(lengths: &TableLengths, blowup_factor: u8) -> StorageMode {
220+
if std::env::var("FORCE_DISK_SPILL").is_ok() {
221+
log::info!("storage_mode: Disk (forced via FORCE_DISK_SPILL)");
222+
return StorageMode::Disk;
223+
}
224+
let estimated = peak_bytes(lengths, blowup_factor, table_parallelism());
225+
let mode = select_storage_mode(estimated, available_ram_bytes());
226+
log::info!("estimated_peak_bytes: {estimated}, storage_mode: {mode:?}");
227+
mode
228+
}
229+
212230
/// Peak RAM estimate in bytes for a proof whose trace shape matches `lengths`.
213-
///
214-
/// `blowup_factor` is `ProofOptions::blowup_factor`. `table_parallelism` is the
215-
/// `k` used by `multi_prove_with_mode` to chunk rounds 2-4; pass
216-
/// `stark::prover::table_parallelism()` so the worst-case-chunk transient term
217-
/// matches the runtime.
218-
pub fn peak_bytes(lengths: &TableLengths, blowup_factor: u8, table_parallelism: usize) -> u64 {
231+
fn peak_bytes(lengths: &TableLengths, blowup_factor: u8, table_parallelism: usize) -> u64 {
219232
let blowup = blowup_factor as u64;
220233
let k = table_parallelism.max(1);
221234
let specs = table_specs(lengths);
@@ -268,53 +281,23 @@ pub fn peak_bytes(lengths: &TableLengths, blowup_factor: u8, table_parallelism:
268281
.saturating_add(state_total)
269282
}
270283

271-
/// User cap ∩ OS available, or None if both are unknown.
272-
fn effective_budget(available: Option<u64>, cap: Option<u64>) -> Option<u64> {
273-
match (cap, available) {
274-
(Some(c), Some(a)) => Some(c.min(a)),
275-
(Some(c), None) => Some(c),
276-
(None, a) => a,
277-
}
278-
}
279-
280-
/// Disk if `estimated` exceeds 90% of the effective budget, else Ram.
281-
/// Defaults to Disk when budget is unknown (sysinfo failure + no cap).
282-
pub fn select_storage_mode(
283-
estimated: u64,
284-
available: Option<u64>,
285-
cap: Option<u64>,
286-
) -> StorageMode {
287-
let Some(budget) = effective_budget(available, cap) else {
288-
log::warn!(
289-
"Auto disk-spill: sysinfo could not read system memory and no cap set, \
290-
defaulting to Disk."
291-
);
284+
/// `Disk` if `estimated` exceeds `available` minus a safety margin, else
285+
/// `Ram`. Defaults to `Disk` when `available` is `None`.
286+
fn select_storage_mode(estimated: u64, available: Option<u64>) -> StorageMode {
287+
let Some(available) = available else {
288+
log::warn!("Auto disk-spill: sysinfo could not read system memory, defaulting to Disk.");
292289
return StorageMode::Disk;
293290
};
294-
let threshold = budget.saturating_mul(SAFETY_FRACTION_NUM) / SAFETY_FRACTION_DEN;
295-
291+
let threshold = available.saturating_mul(SAFETY_FRACTION_NUM) / SAFETY_FRACTION_DEN;
296292
if estimated > threshold {
297293
StorageMode::Disk
298294
} else {
299-
// `cap.is_none()` plus an `effective_budget` that returned `Some` means
300-
// `available` must be `Some` (see `effective_budget`).
301-
if cap.is_none() && estimated.saturating_mul(4) >= available.unwrap().saturating_mul(3) {
302-
log::warn!(
303-
"Auto disk-spill picked Ram with estimated_peak={estimated} bytes near \
304-
available={available:?}. Set max_ram_bytes to bound the budget to a \
305-
cgroup limit if running in a container."
306-
);
307-
}
308295
StorageMode::Ram
309296
}
310297
}
311298

312-
/// OS-available RAM, or None if sysinfo can't read it (e.g. stripped containers).
313-
/// Returns `Some(0)` on near-OOM so callers force Disk rather than fall back to Ram.
314-
///
315-
/// Reads host `/proc/meminfo`, not cgroup limits — set `max_ram_bytes` in
316-
/// containerized environments to bound the budget to the container's limit.
317-
pub fn available_ram_bytes() -> Option<u64> {
299+
/// OS-available RAM, or `None` if sysinfo can't read it.
300+
fn available_ram_bytes() -> Option<u64> {
318301
let mut sys = System::new();
319302
sys.refresh_memory();
320303
// total_memory == 0 means sysinfo can't read; otherwise available is real.
@@ -400,56 +383,93 @@ mod tests {
400383
#[test]
401384
fn select_ram_when_estimate_below_threshold() {
402385
// 10 GB estimated, 32 GB available → threshold 28.8 GB → Ram.
403-
let mode = select_storage_mode(10 * GB, Some(32 * GB), None);
386+
let mode = select_storage_mode(10 * GB, Some(32 * GB));
404387
assert_eq!(mode, StorageMode::Ram);
405388
}
406389

407390
#[test]
408391
fn select_disk_when_estimate_exceeds_threshold() {
409392
// 30 GB estimated, 32 GB available → threshold 28.8 GB → Disk.
410-
let mode = select_storage_mode(30 * GB, Some(32 * GB), None);
393+
let mode = select_storage_mode(30 * GB, Some(32 * GB));
411394
assert_eq!(mode, StorageMode::Disk);
412395
}
413396

414397
#[test]
415-
fn cap_forces_disk_when_smaller_than_available() {
416-
// 10 GB estimated, 64 GB available (would be Ram), but cap=4 GB
417-
// → threshold = 4 × 0.9 = 3.6 GB → Disk.
418-
let mode = select_storage_mode(10 * GB, Some(64 * GB), Some(4 * GB));
398+
fn unknown_available_defaults_to_disk() {
399+
let mode = select_storage_mode(peak_bytes(&empty_lengths(), 2, ALL_TABLES), None);
419400
assert_eq!(mode, StorageMode::Disk);
420401
}
421402

422-
#[test]
423-
fn cap_ignored_when_larger_than_available() {
424-
// available=8 GB dominates a cap of 64 GB.
425-
// threshold = 8 × 0.9 = 7.2 GB, estimate 10 GB → Disk.
426-
let mode = select_storage_mode(10 * GB, Some(8 * GB), Some(64 * GB));
427-
assert_eq!(mode, StorageMode::Disk);
428-
}
403+
/// Asserts predicted [`peak_bytes`] does not underestimate jemalloc-measured
404+
/// heap during a proof.
405+
mod calibration {
406+
use super::*;
407+
use crate::tables::MaxRowsConfig;
408+
use crate::tables::trace_builder::count_table_lengths;
409+
use crate::test_utils::{asm_elf_bytes, run_asm_elf};
410+
use stark::proof::options::GoldilocksCubicProofOptions;
411+
use std::sync::Arc;
412+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
413+
use std::thread;
414+
use std::time::Duration;
415+
use tikv_jemalloc_ctl::{epoch, stats};
416+
417+
#[global_allocator]
418+
static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
419+
420+
fn allocated_bytes() -> usize {
421+
epoch::advance().ok();
422+
stats::allocated::read().unwrap_or(0)
423+
}
429424

430-
#[test]
431-
fn tiny_cap_always_forces_disk() {
432-
let mode = select_storage_mode(
433-
peak_bytes(&empty_lengths(), 2, ALL_TABLES),
434-
Some(64 * GB),
435-
Some(1_000_000),
436-
);
437-
assert_eq!(mode, StorageMode::Disk);
438-
}
425+
#[test]
426+
fn peak_bytes_does_not_underestimate_measured_heap() {
427+
let (elf, logs, _) = run_asm_elf("fib_iterative_372k");
428+
let elf_bytes = asm_elf_bytes("fib_iterative_372k");
439429

440-
#[test]
441-
fn unknown_available_with_no_cap_defaults_to_disk() {
442-
// sysinfo failed and no cap was set. Default to Disk: sysinfo fails
443-
// in stripped-down containers where Ram would OOM. Pass max_ram_bytes
444-
// to opt out on a known-sized machine.
445-
let mode = select_storage_mode(peak_bytes(&empty_lengths(), 2, ALL_TABLES), None, None);
446-
assert_eq!(mode, StorageMode::Disk);
447-
}
430+
let max_rows = MaxRowsConfig::default();
431+
let lengths = count_table_lengths(&elf, &logs, &max_rows, &[])
432+
.expect("count_table_lengths succeeds");
448433

449-
#[test]
450-
fn unknown_available_with_cap_uses_cap_as_budget() {
451-
// OS can't report; cap is the whole budget.
452-
let mode = select_storage_mode(10 * GB, None, Some(4 * GB));
453-
assert_eq!(mode, StorageMode::Disk);
434+
let opts = GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is valid");
435+
let predicted = peak_bytes(&lengths, opts.blowup_factor, table_parallelism()) as usize;
436+
437+
drop(logs);
438+
439+
let baseline = allocated_bytes();
440+
let peak = Arc::new(AtomicUsize::new(baseline));
441+
let stop = Arc::new(AtomicBool::new(false));
442+
443+
let sampler = {
444+
let peak = Arc::clone(&peak);
445+
let stop = Arc::clone(&stop);
446+
thread::spawn(move || {
447+
while !stop.load(Ordering::Relaxed) {
448+
peak.fetch_max(allocated_bytes(), Ordering::Relaxed);
449+
thread::sleep(Duration::from_millis(10));
450+
}
451+
})
452+
};
453+
454+
let _proof = crate::prove_with_options_and_inputs(&elf_bytes, &[], &opts, &max_rows)
455+
.expect("proof succeeds");
456+
457+
stop.store(true, Ordering::Relaxed);
458+
sampler.join().expect("sampler joins");
459+
460+
let measured = peak.load(Ordering::Relaxed).saturating_sub(baseline);
461+
462+
eprintln!(
463+
"peak_bytes calibration: predicted={predicted} bytes, measured_heap={measured} bytes, ratio={:.2}",
464+
predicted as f64 / measured as f64
465+
);
466+
467+
let safety_num = SAFETY_FRACTION_NUM as usize;
468+
let safety_den = SAFETY_FRACTION_DEN as usize;
469+
assert!(
470+
predicted.saturating_mul(safety_den) >= measured.saturating_mul(safety_num),
471+
"peak_bytes underestimates measured heap: predicted={predicted}, measured={measured}"
472+
);
473+
}
454474
}
455475
}

prover/src/lib.rs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ use crypto::fiat_shamir::is_transcript::IsTranscript;
2929
use executor::elf::Elf;
3030
use executor::vm::execution::Executor;
3131
use math::field::element::FieldElement;
32-
#[cfg(feature = "disk-spill")]
33-
use stark::prover::table_parallelism;
3432
use stark::prover::{IsStarkProver, Prover};
3533
#[cfg(feature = "disk-spill")]
3634
use stark::storage_mode::StorageMode;
@@ -584,23 +582,10 @@ pub fn prove_with_options_and_inputs(
584582
#[cfg(feature = "instruments")]
585583
let phase_start = std::time::Instant::now();
586584

587-
// Pick storage mode from analytical heap estimate.
588585
#[cfg(feature = "disk-spill")]
589586
let storage_mode = {
590587
let lengths = count_table_lengths(&program, &result.logs, max_rows, private_inputs)?;
591-
592-
let available = auto_storage::available_ram_bytes();
593-
let estimated_peak =
594-
auto_storage::peak_bytes(&lengths, proof_options.blowup_factor, table_parallelism());
595-
let mode = auto_storage::select_storage_mode(
596-
estimated_peak,
597-
available,
598-
proof_options.max_ram_bytes,
599-
);
600-
601-
log::info!("predicted_peak_bytes: {estimated_peak}, storage_mode: {mode:?}");
602-
603-
mode
588+
auto_storage::decide(&lengths, proof_options.blowup_factor)
604589
};
605590

606591
let mut traces = Traces::from_elf_and_logs(

0 commit comments

Comments
 (0)