From f307a0ea9aa3708197c9e10c37473b7c3375fbff Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 26 Aug 2026 16:30:26 -0300 Subject: [PATCH] test(gpu): make test-cuda-d1 actually exercise DECODE `test-cuda-d1` set LAMBDA_VM_GPU_LDE_THRESHOLD=64 on the premise that 64 is "the exact LDE size of fib_iterative_1M's DECODE ROM". It is 32, and the reasoning behind the number was wrong too: DECODE's rows come from the ELF's executable words, not from cycles. fib_iterative_1M is 13 executable words (one 52-byte executable PT_LOAD; the variants differ only in the `li a0, ` immediate, so fib_iterative_16M is 13 too). 13 + 1 CPU-padding entry = 14 -> next_power_of_two() = 16 rows -> blowup 2 -> DECODE LDE 32. At threshold 64 that is below the gate, so DECODE failed the R1 split-tree commit, had no gpu_main() handle, and evaluate_dev declined - DECODE never reached the d=1 path at all. The counter could therefore only be fed by KECCAK_RC, the only other num_parts==1 table (a d=1 table is one with a single bus interaction), whose fixed NUM_ROWS=32 gives LDE 64 and passes `64 < 64` by one unit. So the target, the test name, the module docs and the assert message all named the one d=1 table guaranteed not to be exercised. No threshold fixes this with a fib fixture: DECODE (32) sits below KECCAK_RC (64), so <=32 engages both and 33..=64 engages only KECCAK_RC. Switch to all_instructions_64 - 66 executable words -> 128 rows -> DECODE LDE 256 - at threshold 128, where DECODE engages with 2x margin and KECCAK_RC declines, so a nonzero gpu_comp_h_slabs_calls() uniquely attributes to DECODE. 128 is also higher than the previous 64, so strictly fewer tables land on the GPU-committed path: it narrows rather than widens the R4 gather_proofs_dev abort site that gpu_lde.rs warns about for lowered thresholds. Tighten the test's own guard while here. `thr > 0 && thr < 1<<14` passed vacuously for any wrong value - including the 64 that caused this - so pin the window to (KECCAK_RC_LDE, DECODE_LDE] against named constants instead. --- Makefile | 30 +++++++++++++----- prover/tests/cuda_d1_path.rs | 59 +++++++++++++++++++++++------------- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index db12d3722..8392bd826 100644 --- a/Makefile +++ b/Makefile @@ -587,15 +587,29 @@ test-cuda-integration: --test cuda_path_integration -- --ignored --nocapture --test-threads=1 # num_parts==1 (DECODE) device DEEP/FRI coverage (requires NVIDIA GPU + nvcc). -# The steady-state cuda_path_integration fixtures never cross the default LDE -# threshold (1<<14) for a num_parts==1 table, so lower it here so DECODE engages -# the d=1 device path end to end. 64 = the exact LDE size of fib_iterative_1M's -# DECODE ROM: high enough to engage only DECODE (not the tiny tables whose GPU -# NTT degenerates below ~16), low enough that DECODE crosses it. Its own binary + -# a process-wide env because gpu_lde_threshold() caches the value on first read -# (OnceLock), so it must be set before any prove in the process. +# No fixture crosses the default LDE threshold (1<<14) for a num_parts==1 table, +# so lower it here until DECODE engages the d=1 device path end to end. +# +# Threshold and fixture are one choice, because there are exactly two d=1 tables +# (a d=1 table is one with a single bus interaction): DECODE, whose rows come from +# the guest's instruction count, and KECCAK_RC, fixed at NUM_ROWS=32 => LDE 64. +# DECODE's ROM is derived from the ELF, NOT from cycles, so the whole +# fib_iterative_* family is 13 executable words (the variants differ only in the +# `li a0, ` immediate) => 16 rows => LDE 32. That sits BELOW KECCAK_RC's 64, +# so with a fib fixture no threshold isolates DECODE: <=32 engages both and +# 33..=64 engages only KECCAK_RC. +# +# all_instructions_64 is 66 executable words => 128 rows => DECODE LDE 256. At 128, +# DECODE engages with 2x margin and KECCAK_RC (64) declines, so a nonzero +# gpu_comp_h_slabs_calls() uniquely attributes to DECODE. 128 is also ABOVE the +# PR's original 64, so it sends strictly fewer tables onto the GPU-committed path +# and narrows -- rather than widens -- the R4 gather_proofs_dev abort site that +# crypto/stark/src/gpu_lde.rs warns about for lowered thresholds. +# +# Its own binary + a process-wide env because gpu_lde_threshold() caches the value +# on first read (OnceLock), so it must be set before any prove in the process. test-cuda-d1: - LAMBDA_VM_GPU_LDE_THRESHOLD=64 $(GPU_TEST_TIMEOUT) cargo test -p lambda-vm-prover \ + LAMBDA_VM_GPU_LDE_THRESHOLD=128 $(GPU_TEST_TIMEOUT) cargo test -p lambda-vm-prover \ --release --features cuda \ --test cuda_d1_path -- --ignored --nocapture --test-threads=1 diff --git a/prover/tests/cuda_d1_path.rs b/prover/tests/cuda_d1_path.rs index f3eaae958..da449ee47 100644 --- a/prover/tests/cuda_d1_path.rs +++ b/prover/tests/cuda_d1_path.rs @@ -1,13 +1,19 @@ //! End-to-end coverage for the num_parts==1 (DECODE) device DEEP/FRI path. //! -//! The steady-state fixtures in `cuda_path_integration.rs` only exercise the -//! degree-2 composition path: every asm fixture is a `fib_iterative_*` whose -//! DECODE ROM sits below the default GPU LDE threshold, so no num_parts==1 table -//! ever engages the device path there. This binary lowers -//! `LAMBDA_VM_GPU_LDE_THRESHOLD` (via `make test-cuda-d1`) so DECODE crosses it -//! and the whole d=1 wiring — de-interleave -> R2 commit -> R3 OOD -> R4 DEEP -> -//! FRI -> openings — runs end to end, validated by the release query-0 -//! composition canary and the final verify. +//! No fixture crosses the default GPU LDE threshold for a num_parts==1 table, so +//! this binary lowers `LAMBDA_VM_GPU_LDE_THRESHOLD` (via `make test-cuda-d1`) +//! until DECODE engages and the whole d=1 wiring — de-interleave -> R2 commit -> +//! R3 OOD -> R4 DEEP -> FRI -> openings — runs end to end, validated by the +//! release query-0 composition canary and the final verify. +//! +//! Fixture and threshold are one choice. There are exactly two d=1 tables (a d=1 +//! table is one with a single bus interaction): DECODE, sized from the guest's +//! instruction count, and KECCAK_RC, fixed at `NUM_ROWS = 32` => LDE 64. DECODE's +//! ROM comes from the ELF and not from cycles, so every `fib_iterative_*` variant +//! is 13 executable words => 16 rows => LDE 32 — below KECCAK_RC's 64, which means +//! no threshold isolates DECODE with a fib fixture. `all_instructions_64` is 66 +//! executable words => 128 rows => LDE 256, so at threshold 128 DECODE engages and +//! KECCAK_RC declines: a nonzero counter uniquely attributes to DECODE. //! //! Its own binary (not another test in `cuda_path_integration.rs`) on purpose: //! `gpu_lde_threshold()` caches the env in a `OnceLock` on first read, so the @@ -22,28 +28,38 @@ use lambda_vm_prover::test_utils::asm_elf_bytes; use lambda_vm_prover::{prove, verify}; use stark::gpu_lde::{gpu_comp_h_slabs_calls, reset_all_gpu_call_counters}; -/// With the LDE threshold lowered so the DECODE (num_parts==1) table engages, -/// the device de-interleave path (`gpu_comp_h_slabs_calls`) must fire and the -/// proof — whose DECODE DEEP/FRI now ran on device — must still verify. Guards a -/// silent CPU fallback (counter == 0) and a bad-layout regression (fires but the -/// proof fails verification); the in-prove release query-0 canary guards the +/// The fixture whose DECODE ROM crosses the lowered threshold: 66 executable +/// words -> 128 rows -> LDE 256. +const FIXTURE: &str = "all_instructions_64"; +/// DECODE's LDE for [`FIXTURE`], and the LDE of the only other d=1 table. The +/// threshold must fall between them so the counter attributes to DECODE alone. +const DECODE_LDE: usize = 256; +const KECCAK_RC_LDE: usize = 64; + +/// With the LDE threshold lowered so the DECODE (num_parts==1) table engages, the +/// device de-interleave path (`gpu_comp_h_slabs_calls`) must fire and the proof — +/// whose DECODE DEEP/FRI now ran on device — must still verify. Guards a silent +/// CPU fallback (counter == 0) and a bad-layout regression (fires but the proof +/// fails verification); the in-prove release query-0 canary guards the /// composition-row gather on top. #[test] #[ignore = "requires GPU + a lowered LAMBDA_VM_GPU_LDE_THRESHOLD; run via `make test-cuda-d1`"] fn gpu_num_parts_1_decode_path_fires_and_verifies() { - // Meaningful only with the threshold lowered (the make target sets it). Fail - // with a pointed message rather than a confusing "path did not fire". + // Pin the window rather than just "below the default": a threshold anywhere + // outside (KECCAK_RC_LDE, DECODE_LDE] silently measures the wrong table (or no + // table), which is exactly the failure this constant pair exists to prevent. let thr: usize = std::env::var("LAMBDA_VM_GPU_LDE_THRESHOLD") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(0); assert!( - thr > 0 && thr < 1 << 14, - "run via `make test-cuda-d1`: needs LAMBDA_VM_GPU_LDE_THRESHOLD set below the \ - default (1<<14) so the DECODE (num_parts==1) table engages the device path; got {thr}" + thr > KECCAK_RC_LDE && thr <= DECODE_LDE, + "run via `make test-cuda-d1`: LAMBDA_VM_GPU_LDE_THRESHOLD must land in \ + ({KECCAK_RC_LDE}, {DECODE_LDE}] so {FIXTURE}'s DECODE (LDE {DECODE_LDE}) engages the \ + device path while KECCAK_RC (LDE {KECCAK_RC_LDE}) declines; got {thr}" ); - let elf = asm_elf_bytes("fib_iterative_1M"); + let elf = asm_elf_bytes(FIXTURE); // Warm-up amortises PTX load + pool warm-up so the measured prove reflects // steady state (mirrors cuda_path_integration.rs). let _ = prove(&elf).expect("warm-up prove"); @@ -53,8 +69,9 @@ fn gpu_num_parts_1_decode_path_fires_and_verifies() { assert!( gpu_comp_h_slabs_calls() > 0, - "num_parts==1 device de-interleave path did not fire: no DECODE-shaped table \ - crossed the lowered LDE threshold, so the d=1 DEEP/FRI wiring was not exercised" + "num_parts==1 device de-interleave path did not fire: DECODE (the only d=1 table \ + above the threshold for {FIXTURE}) did not take it, so the d=1 DEEP/FRI wiring \ + was not exercised" ); assert!( verify(&proof, &elf).expect("verify"),