Skip to content

Commit a482ec2

Browse files
authored
test(recursion): memory-bounded execute/prove smoke tests; wire CI (#750)
* test(recursion): memory-bounded execute/prove smoke tests; wire CI Stream the execute-only path via Executor::resume() and read the committed marker straight off Memory (Executor::finish), dropping the run()+Traces build that OOM'd. Prove the outer recursion guest memory-bounded via prove_and_verify_continuation (2^20-cycle epochs) instead of a monolithic ~125 GB prove. Un-ignore the three execute-only tests (now sub-minute); the full-prove tests stay #[ignore]d (>60s) and run in the comprehensive CI job. CI: build the recursion guest ELFs (cache + conditional setup-rust + make compile-recursion-elfs) in both the per-PR and comprehensive prover jobs, and add test(test_recursion_prove) to the comprehensive filter. * cargo fmt * a bit of brevity * less verbose * re-ignore slow tests, fix cache keys * test(recursion): CI runs execute-only tier; manual prove uses smaller epochs The prove tier's continuation bundle retains every epoch's STARK proof in RAM, so it OOMs CI runners. Narrow the CI selector to test_recursion_execute (streaming, bounded) and leave the prove tier manual-only with a 2^16 epoch to cap the per-epoch trace+LDE spike.
1 parent 5edcc6c commit a482ec2

2 files changed

Lines changed: 103 additions & 69 deletions

File tree

.github/workflows/pr_main.yaml

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,12 +370,30 @@ jobs:
370370
with:
371371
name: prover-tests
372372

373+
- name: Cache compiled recursion guest ELF artifacts
374+
id: cache-recursion-elfs
375+
uses: actions/cache@v4
376+
with:
377+
path: executor/program_artifacts/recursion
378+
key: recursion-elf-artifacts-${{ hashFiles('bench_vs/lambda/**', 'prover/src/**', 'prover/Cargo.toml', 'crypto/**/src/**', 'crypto/**/Cargo.toml', 'executor/src/**', 'executor/Cargo.toml', 'syscalls/**', 'executor/programs/riscv64im-lambda-vm-elf.json', 'Makefile') }}
379+
restore-keys: |
380+
recursion-elf-artifacts-
381+
382+
- name: Setup Rust Environment (recursion ELFs)
383+
if: steps.cache-recursion-elfs.outputs.cache-hit != 'true' && steps.cache-rust-elfs.outputs.cache-hit == 'true'
384+
uses: ./.github/actions/setup-rust
385+
386+
- name: Compile recursion guest ELFs
387+
if: steps.cache-recursion-elfs.outputs.cache-hit != 'true'
388+
run: |
389+
make compile-recursion-elfs
390+
373391
- name: Run comprehensive prover tests
374392
run: |
375393
cargo nextest run \
376394
--archive-file prover-tests.tar.zst \
377395
--test-threads=1 \
378-
-E 'test(test_prove_elfs_all_instructions_64_full)' \
396+
-E 'test(test_prove_elfs_all_instructions_64_full) | test(test_recursion_execute)' \
379397
--run-ignored ignored-only
380398
381399
# Seed ELF caches on refs/heads/main so merge-queue runs can restore them.
@@ -424,3 +442,20 @@ jobs:
424442
- name: Compile Rust programs to ELF
425443
if: steps.cache-rust-elfs.outputs.cache-hit != 'true'
426444
run: make compile-programs-rust
445+
446+
- name: Cache compiled recursion guest ELF artifacts
447+
id: cache-recursion-elfs
448+
uses: actions/cache@v4
449+
with:
450+
path: executor/program_artifacts/recursion
451+
key: recursion-elf-artifacts-${{ hashFiles('bench_vs/lambda/**', 'prover/src/**', 'prover/Cargo.toml', 'crypto/**/src/**', 'crypto/**/Cargo.toml', 'executor/src/**', 'executor/Cargo.toml', 'syscalls/**', 'executor/programs/riscv64im-lambda-vm-elf.json', 'Makefile') }}
452+
restore-keys: |
453+
recursion-elf-artifacts-
454+
455+
- name: Setup Rust Environment (recursion ELFs)
456+
if: steps.cache-recursion-elfs.outputs.cache-hit != 'true' && steps.cache-rust-elfs.outputs.cache-hit == 'true'
457+
uses: ./.github/actions/setup-rust
458+
459+
- name: Compile recursion guest ELFs
460+
if: steps.cache-recursion-elfs.outputs.cache-hit != 'true'
461+
run: make compile-recursion-elfs

prover/src/tests/recursion_smoke_test.rs

Lines changed: 67 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,13 @@
44
//! 1. Proves an inner program on the host.
55
//! 2. Serializes `(VmProof, inner_elf, opts)` with postcard.
66
//! 3. Hands that as private input to the recursion guest.
7-
//! 4. Either **proves** the recursion guest's execution and verifies the outer
8-
//! proof (`OuterMode::Prove`), or merely **executes** the guest in-VM and
9-
//! reads the committed marker off the trace (`OuterMode::ExecuteOnly`) — a
10-
//! cheaper tier that skips the LDE/FRI that dominate the full pipeline.
7+
//! 4. Either **proves** the recursion guest's execution (memory-bounded via
8+
//! continuations) and verifies the outer proof (`OuterMode::Prove`), or
9+
//! merely **executes** the guest in-VM and reads the committed marker
10+
//! straight off the executor's memory (`OuterMode::ExecuteOnly`) — a cheaper
11+
//! tier that skips the LDE/FRI that dominate the full pipeline.
1112
//!
12-
//! The guest ELFs are built by `make compile-recursion-elfs` (which the
13-
//! `test-prover-all` make target depends on) and read from
14-
//! `executor/program_artifacts/recursion/`, like every other program test.
15-
//!
16-
//! Tests are `#[ignore]`d because the outer proof runs the full STARK verifier
17-
//! inside the VM (minutes per run, large memory footprint).
13+
//! The guest ELFs are assumed built by `make compile-recursion-elfs`.
1814
1915
use std::path::PathBuf;
2016

@@ -83,64 +79,77 @@ fn prove_inner_and_encode_blob(
8379
/// whether we also prove the guest's own execution.
8480
#[derive(Clone, Copy, Debug)]
8581
enum OuterMode {
86-
/// Execute the guest in-VM and read the committed marker off the trace.
87-
/// Skips the LDE blowup + FRI commit that dominate the full pipeline's
88-
/// footprint, so it needs materially less RAM than `Prove`.
89-
///
90-
/// "Less" is not "little": `Executor::run` retains a per-instruction log
91-
/// and `Traces` materializes the full execution trace, so verifying even a
92-
/// 1-query inner proof still needs tens of GB — it OOMs on a 36 GB box.
82+
/// Execute the guest in-VM and read the committed marker straight off the
83+
/// executor's memory. Streams logs via `Executor::resume()` and never
84+
/// builds a `Traces`, so footprint stays bounded to the VM's touched
85+
/// memory + instruction cache. Skips the LDE/FRI of the full pipeline entirely.
9386
ExecuteOnly,
94-
/// Prove the guest's execution and verify the outer proof on the host. The
95-
/// full STARK verifier inside the VM — minutes per run, ~125 GB.
87+
/// Prove the guest's execution via continuations, then verify the outer
88+
/// proof on the host. `prove_and_verify_continuation` retains every epoch's
89+
/// STARK proof in the bundle before verifying, so peak RAM grows with epoch
90+
/// count. Heavy — excluded from CI, run manually. A future verify-one-and-
91+
/// discard API extension would make this memory-friendlier.
9692
Prove,
9793
}
9894

9995
/// Execute the recursion guest in-VM on `blob` and return the bytes it
10096
/// committed (the success marker the in-VM verifier emits).
97+
///
98+
/// Streams execution via `Executor::resume()`. The committed marker is
99+
/// read directly off the executor's memory. This avoids OOMs.
101100
fn execute_outer_and_commit(label: &str, recursion_elf_bytes: &[u8], blob: &[u8]) -> Vec<u8> {
102101
use executor::elf::Elf;
103102
use executor::vm::execution::Executor;
104103

105-
eprintln!("[{label}] executing outer (recursion guest, in-VM verify) ...");
104+
eprintln!("[{label}] executing outer (recursion guest, in-VM verify, streaming) ...");
106105
let program = Elf::load(recursion_elf_bytes).expect("load recursion elf");
107-
let result = Executor::new(&program, blob.to_vec())
108-
.expect("executor new")
109-
.run()
110-
.expect("recursion guest execution failed (verify panicked in-VM?)");
111-
112-
let traces = crate::tables::trace_builder::Traces::from_elf_and_logs(
113-
&program,
114-
&result.logs,
115-
&crate::MaxRowsConfig::default(),
116-
blob,
117-
#[cfg(feature = "disk-spill")]
118-
stark::storage_mode::StorageMode::Ram,
119-
)
120-
.expect("trace build");
106+
let mut executor = Executor::new(&program, blob.to_vec()).expect("executor new");
107+
108+
// Drain chunks to completion without retaining logs or building a trace.
109+
while executor
110+
.resume()
111+
.expect("recursion guest execution failed (verify panicked in-VM?)")
112+
.is_some()
113+
{}
114+
115+
let committed = executor
116+
.finish()
117+
.expect("read committed output after execution")
118+
.memory_values;
121119

122120
eprintln!(
123121
"[{label}] committed {} bytes: {:?} (as str: {:?})",
124-
traces.public_output_bytes.len(),
125-
traces.public_output_bytes,
126-
String::from_utf8_lossy(&traces.public_output_bytes),
122+
committed.len(),
123+
committed,
124+
String::from_utf8_lossy(&committed),
127125
);
128-
traces.public_output_bytes
126+
committed
129127
}
130128

131-
/// Prove the recursion guest's execution on `blob`, verify the outer proof on
132-
/// the host, and return the bytes the guest committed.
133-
fn prove_outer_and_commit(label: &str, recursion_elf_bytes: &[u8], blob: &[u8]) -> Vec<u8> {
134-
eprintln!("[{label}] proving outer (recursion guest) ...");
135-
let outer_proof =
136-
crate::prove_with_inputs(recursion_elf_bytes, blob).expect("outer prove should succeed");
137-
eprintln!("[{label}] outer proof generated");
129+
/// Epoch size for the outer prove: 2^16 ≈ 65K cycles. Small so one epoch's
130+
/// trace+LDE stays under the ~16GiB CI runners.
131+
const OUTER_EPOCH_SIZE_LOG2: u32 = 16;
138132

139-
assert!(
140-
crate::verify(&outer_proof, recursion_elf_bytes).expect("outer verify errored"),
141-
"outer proof must verify on host"
133+
/// Prove the recursion guest's execution on `blob` memory-bounded via
134+
/// continuations and verify the bundle on the host, returning the bytes the
135+
/// guest committed.
136+
fn prove_outer_and_commit(label: &str, recursion_elf_bytes: &[u8], blob: &[u8]) -> Vec<u8> {
137+
let opts =
138+
crate::GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid");
139+
eprintln!(
140+
"[{label}] proving outer (recursion guest) via continuations \
141+
(epoch=2^{OUTER_EPOCH_SIZE_LOG2} cycles) ..."
142142
);
143-
outer_proof.public_output
143+
let committed = crate::continuation::prove_and_verify_continuation(
144+
recursion_elf_bytes,
145+
blob,
146+
OUTER_EPOCH_SIZE_LOG2,
147+
&opts,
148+
)
149+
.expect("outer continuation prove/verify errored")
150+
.expect("outer continuation proof must verify on host");
151+
eprintln!("[{label}] outer continuation proof generated and verified");
152+
committed
144153
}
145154

146155
/// Core pipeline: prove an inner program with the given options, hand the
@@ -214,10 +223,7 @@ fn run_recursion_pipeline(
214223

215224
/// Reproduce the recursion guest's EXACT path on the host — decode the postcard
216225
/// blob into `(VmProof, Vec<u8>, ProofOptions)` and call `verify_with_options`.
217-
/// The cheapest regression guard in this file: no VM execution, just the
218-
/// encode/decode contract plus a host verify, so it catches drift in the proof
219-
/// format or the blob layout in seconds. Unlike the guest, a failure here
220-
/// surfaces the actual error instead of an infinite abort loop.
226+
/// Cheap regression guard.
221227
#[test]
222228
#[ignore = "needs prebuilt guest ELF (make compile-recursion-elfs)"]
223229
fn test_recursion_blob_decodes_and_verifies_on_host() {
@@ -249,15 +255,11 @@ fn test_recursion_blob_decodes_and_verifies_on_host() {
249255
}
250256

251257
// === Execute-only tier ========================================================
252-
// Mirrors the proving tests below, but stops at `OuterMode::ExecuteOnly`: the
253-
// guest runs in-VM and we read the committed marker off the trace, skipping the
254-
// outer STARK prove. Needs tens of GB (execution trace), not the ~125 GB the
255-
// full outer prove wants — but still OOMs on a 36 GB box.
256258

257259
/// Execute-only mirror of `test_recursion_prove_empty`: verify a `blowup=8`
258260
/// proof of the empty program in-VM.
259261
#[test]
260-
#[ignore = "needs prebuilt recursion guest ELF + tens of GB RAM (execution trace)"]
262+
#[ignore = "slow: runs the in-VM STARK verifier (minutes on CI)"]
261263
fn test_recursion_execute_empty() {
262264
let root = workspace_root();
263265
let empty_elf_bytes = read_guest_elf(&root, "empty");
@@ -272,7 +274,7 @@ fn test_recursion_execute_empty() {
272274
/// Execute-only mirror of `test_recursion_prove_1query`: smallest possible
273275
/// inner proof (blowup=2, 1 query) → least guest work.
274276
#[test]
275-
#[ignore = "needs prebuilt recursion guest ELF + tens of GB RAM (execution trace)"]
277+
#[ignore = "slow: runs the in-VM STARK verifier (minutes on CI)"]
276278
fn test_recursion_execute_1query() {
277279
let root = workspace_root();
278280
let empty_elf_bytes = read_guest_elf(&root, "empty");
@@ -288,7 +290,7 @@ fn test_recursion_execute_1query() {
288290
/// Execute-only mirror of `test_recursion_prove`: verify a `blowup=8` proof of
289291
/// fibonacci(10) in-VM.
290292
#[test]
291-
#[ignore = "needs prebuilt recursion guest ELF + tens of GB RAM (execution trace)"]
293+
#[ignore = "slow: runs the in-VM STARK verifier (minutes on CI)"]
292294
fn test_recursion_execute() {
293295
let root = workspace_root();
294296
let fib_elf_bytes = read_guest_elf(&root, "fibonacci");
@@ -307,10 +309,9 @@ fn test_recursion_execute() {
307309
// === Full-prove tier ==========================================================
308310

309311
/// Inner program: empty (halt immediately). Useful for measuring the
310-
/// lambda-vm verifier's intrinsic recursion overhead — i.e. what it costs
311-
/// to verify the smallest possible lambda-vm proof, with no inner workload.
312+
/// verifier's intrinsic recursion overhead.
312313
#[test]
313-
#[ignore = "slow: runs the full STARK verifier inside the VM"]
314+
#[ignore = "slow: memory-bounded continuation prove of the verifier-in-VM"]
314315
fn test_recursion_prove_empty() {
315316
let root = workspace_root();
316317
let empty_elf_bytes = read_guest_elf(&root, "empty");
@@ -323,11 +324,9 @@ fn test_recursion_prove_empty() {
323324
}
324325

325326
/// Inner program: empty, but with the absolute-minimum FRI parameters
326-
/// (blowup=2, **fri_number_of_queries=1**). This is a "can the pipeline even
327-
/// run end-to-end on a 125 GB box" experiment — security is intentionally
328-
/// terrible. Use only for capacity probing.
327+
/// (blowup=2, **fri_number_of_queries=1**). For quick profiling only.
329328
#[test]
330-
#[ignore = "slow: runs the full STARK verifier inside the VM"]
329+
#[ignore = "slow: memory-bounded continuation prove of the verifier-in-VM"]
331330
fn test_recursion_prove_1query() {
332331
let root = workspace_root();
333332
let empty_elf_bytes = read_guest_elf(&root, "empty");
@@ -343,7 +342,7 @@ fn test_recursion_prove_1query() {
343342

344343
/// Inner program: fibonacci(10).
345344
#[test]
346-
#[ignore = "slow: runs the full STARK verifier inside the VM"]
345+
#[ignore = "slow: memory-bounded continuation prove of the verifier-in-VM"]
347346
fn test_recursion_prove() {
348347
let root = workspace_root();
349348
let fib_elf_bytes = read_guest_elf(&root, "fibonacci");

0 commit comments

Comments
 (0)