Skip to main content

openvm_cuda_backend/
whir.rs

1use std::{iter::zip, sync::Arc};
2
3use itertools::Itertools;
4use openvm_cuda_common::{
5    copy::{MemCopyD2H, MemCopyH2D},
6    d_buffer::DeviceBuffer,
7    memory_manager::MemTracker,
8    stream::GpuDeviceCtx,
9};
10use openvm_stark_backend::{
11    proof::{MerkleProof, WhirProof},
12    prover::MatrixDimensions,
13    SystemParams,
14};
15use p3_field::{BasedVectorSpace, PrimeCharacteristicRing, TwoAdicField};
16use p3_util::log2_strict_usize;
17use tracing::instrument;
18
19use crate::{
20    base::DeviceMatrix,
21    cuda::{
22        batch_ntt_small::batch_ntt_small,
23        matrix::{batch_expand_pad, split_ext_to_base_col_major_matrix},
24        mle_interpolate::mle_interpolate_stage_ext,
25        poly::{eval_poly_ext_at_point_from_base, transpose_fp_to_fpext_vec},
26        whir::{
27            _whir_sumcheck_coeff_moments_required_temp_buffer_size, w_moments_accumulate,
28            whir_algebraic_batch_traces, whir_fold_coeffs_and_moments,
29            whir_sumcheck_coeff_moments_round,
30        },
31    },
32    hash_scheme::GpuHashScheme,
33    merkle_tree::{MerkleProofQueryDigest, MerkleTreeConstructor, MerkleTreeGpu},
34    ntt::batch_ntt,
35    poly::evals_eq_hypercube,
36    prelude::{D_EF, EF, F},
37    sponge::GpuFiatShamirTranscript,
38    stacked_pcs::rs_code_matrix,
39    stacked_reduction::StackedPcsData2,
40    WhirProverError,
41};
42
43#[repr(C)]
44pub(crate) struct BatchingTracePacket {
45    /// Pointer to trace device buffer
46    ptr: *const F,
47    /// Trace height
48    height: u32,
49    /// Trace width
50    width: u32,
51    /// Row start in the stacked matrix
52    stacked_row_start: u32,
53    /// Index in mu powers
54    mu_idx: u32,
55}
56
57#[instrument(
58    name = "prover.openings.whir",
59    level = "info",
60    skip_all,
61    fields(phase = "prover")
62)]
63pub fn prove_whir_opening_gpu<HS, TS>(
64    params: &SystemParams,
65    transcript: &mut TS,
66    mut stacked_per_commit: Vec<StackedPcsData2<HS::Digest>>,
67    u: &[EF],
68    device_ctx: &GpuDeviceCtx,
69) -> Result<WhirProof<HS::SC>, WhirProverError>
70where
71    HS: GpuHashScheme,
72    TS: GpuFiatShamirTranscript<HS::SC>,
73    HS::MerkleHash: MerkleTreeConstructor,
74    HS::Digest: MerkleProofQueryDigest,
75{
76    let mem = MemTracker::start("prover.prove_whir_opening");
77    let l_skip = params.l_skip;
78    let log_blowup = params.log_blowup;
79    let k_whir = params.k_whir();
80    let whir_params = params.whir();
81    let log_final_poly_len = params.log_final_poly_len();
82
83    let height = stacked_per_commit[0].layout().height();
84    debug_assert!(stacked_per_commit
85        .iter()
86        .all(|d| d.layout().height() == height));
87    let mut m = log2_strict_usize(height);
88    assert_eq!(m, u.len());
89    debug_assert!(m >= l_skip);
90
91    // Proof-of-work grinding before μ batching challenge.
92    // This amplifies soundness of the initial batching step.
93    let mu_pow_witness = transcript
94        .grind_gpu(whir_params.mu_pow_bits, device_ctx)
95        .map_err(WhirProverError::MuGrind)?;
96    // Sample randomness for algebraic batching.
97    // We batch the codewords for \hat{q}_j together _before_ applying WHIR.
98    let mu = transcript.sample_ext();
99    let num_commits = stacked_per_commit.len();
100
101    // The coefficient table of `\hat{f}` in the current WHIR round (MLE coefficient form).
102    let mut f_ple_evals = DeviceBuffer::<F>::with_capacity_on(height * D_EF, device_ctx);
103    // We algebraically batch all matrices together so we only need to interpolate one column vector
104    {
105        let mut packets = Vec::new();
106        let mut total_stacked_width = 0u32;
107        for stacked in &stacked_per_commit {
108            let layout = stacked.layout();
109            for (trace, &idx) in zip(&stacked.traces, &layout.mat_starts) {
110                let (_, _, s) = layout.sorted_cols[idx];
111                let packet = BatchingTracePacket {
112                    ptr: trace.buffer().as_ptr(),
113                    height: trace.height() as u32,
114                    width: trace.width() as u32,
115                    stacked_row_start: s.row_idx as u32,
116                    mu_idx: total_stacked_width + s.col_idx as u32,
117                };
118                packets.push(packet);
119            }
120            total_stacked_width += layout.width() as u32;
121        }
122        let mu_powers = mu.powers().take(total_stacked_width as usize).collect_vec();
123        let d_mu_powers = mu_powers.to_device_on(device_ctx)?;
124        let d_packets = packets.to_device_on(device_ctx)?;
125        // SAFETY:
126        // - `mu_powers` has length `total_width`.
127        // - `f_ple_evals` has capacity `height * D_EF` (stacked height in base coordinates).
128        // - `d_packets` contain valid pointers and stacked row indices by construction.
129        unsafe {
130            whir_algebraic_batch_traces(
131                &mut f_ple_evals,
132                &d_packets,
133                &d_mu_powers,
134                1 << l_skip,
135                device_ctx.stream.as_raw(),
136            )
137            .map_err(WhirProverError::AlgebraicBatch)?;
138        }
139        for stacked in &mut stacked_per_commit {
140            // drop traces to free device memory if RS codewords matrix exists
141            if stacked.inner.tree.backing_matrix.is_some() {
142                stacked.traces.clear();
143            }
144        }
145    } // common_main_pcs_data.matrix has now been freed
146
147    // Compute \hat{f} coefficients:
148    //
149    // Step 1: iDFT per chunk of size 2^l_skip. After this, each chunk holds univariate
150    // coefficients c_z(x) of f(Z, x) for a fixed boolean assignment x in H_{m - l_skip}.
151    unsafe {
152        let num_poly = f_ple_evals.len() >> l_skip;
153        batch_ntt_small(
154            &mut f_ple_evals,
155            l_skip,
156            num_poly,
157            true,
158            device_ctx.stream.as_raw(),
159        )
160        .map_err(WhirProverError::CustomBatchIntt)?;
161    }
162    let mut f_coeffs = DeviceBuffer::<EF>::with_capacity_on(height, device_ctx);
163    // SAFETY: `f_ple_evals` is constructed with length `height * D_EF`.
164    unsafe {
165        transpose_fp_to_fpext_vec(&mut f_coeffs, &f_ple_evals, device_ctx.stream.as_raw())
166            .map_err(WhirProverError::Transpose)?;
167    }
168    drop(f_ple_evals);
169    // Step 2: Within-chunk zeta (stages 0..l_skip). Applies the subset-zeta transform
170    // over the Z-index bits, converting univariate coefficients (root-of-unity basis)
171    // into hypercube evaluations over H_{l_skip}. Together with step 1, this computes
172    // eval_to_coeff_rs_message, i.e. the MLE coefficient table of \hat{f}.
173    for i in 0..l_skip {
174        let step = 1u32 << i;
175        // SAFETY: `f_coeffs` has length `2^m` with `m >= l_skip`.
176        unsafe {
177            mle_interpolate_stage_ext(&mut f_coeffs, step, false, device_ctx.stream.as_raw())
178                .map_err(|error| WhirProverError::MleInterpolate { error, step })?;
179        }
180    }
181
182    debug_assert_eq!((m - log_final_poly_len) % k_whir, 0);
183    let num_whir_rounds = (m - log_final_poly_len) / k_whir;
184    assert!(num_whir_rounds > 0);
185
186    // We maintain moments of \hat{w}:
187    // M[T] = sum_{x superset T} \hat{w}(x).
188    // For initial \hat{w} = mobius_eq(u, -), these moments are exactly eq(u, -).
189    let mut w_moments = DeviceBuffer::<EF>::with_capacity_on(1 << m, device_ctx);
190    unsafe {
191        evals_eq_hypercube(&mut w_moments, u, device_ctx).map_err(WhirProverError::EvalEq)?;
192    }
193
194    let mut whir_sumcheck_polys: Vec<[EF; 2]> = vec![];
195    let mut codeword_commits = vec![];
196    let mut ood_values = vec![];
197    // per commitment, per whir query, per column
198    let mut initial_round_opened_rows: Vec<Vec<Vec<Vec<F>>>> = vec![vec![]; num_commits];
199    let mut initial_round_merkle_proofs: Vec<Vec<MerkleProof<HS::Digest>>> = vec![];
200    let mut codeword_opened_values: Vec<Vec<Vec<EF>>> = vec![];
201    let mut codeword_merkle_proofs: Vec<Vec<MerkleProof<HS::Digest>>> = vec![];
202    let mut folding_pow_witnesses = vec![];
203    let mut query_phase_pow_witnesses = vec![];
204    let mut rs_tree = None;
205    let mut log_rs_domain_size = m + log_blowup;
206    let mut final_poly = None;
207
208    let mut d_s_evals = DeviceBuffer::<EF>::with_capacity_on(2, device_ctx);
209    let mut d_sumcheck_tmp = DeviceBuffer::<EF>::new();
210
211    mem.tracing_info("before_whir_rounds");
212    // We will drop `stacked_per_commit` and hence `common_main_pcs_data` after whir round 0.
213    for (whir_round, round_params) in whir_params.rounds.iter().enumerate() {
214        let is_last_round = whir_round == num_whir_rounds - 1;
215        // Run k_whir rounds of sumcheck on `sum_{x in H_m} \hat{w}(\hat{f}(x), x)`
216        for round in 0..k_whir {
217            // Do not use f_coeffs.len() because it might have extra capacity.
218            let f_height = 1 << (m - round);
219            debug_assert!(
220                f_coeffs.len() >= f_height,
221                "f_coeffs has length {}, expected 2^{} for m={m}, round={round}",
222                f_coeffs.len(),
223                m - round
224            );
225            debug_assert!(w_moments.len() >= f_height);
226            let output_height = f_height / 2;
227            let tmp_buffer_capacity =
228                unsafe { _whir_sumcheck_coeff_moments_required_temp_buffer_size(f_height as u32) };
229            if d_sumcheck_tmp.len() < tmp_buffer_capacity as usize {
230                d_sumcheck_tmp =
231                    DeviceBuffer::<EF>::with_capacity_on(tmp_buffer_capacity as usize, device_ctx);
232            }
233            let mut new_f_coeffs = DeviceBuffer::<EF>::with_capacity_on(output_height, device_ctx);
234            let mut new_w_moments = DeviceBuffer::<EF>::with_capacity_on(output_height, device_ctx);
235            // SAFETY:
236            // - `d_s_evals` has length 2
237            // - `d_sumcheck_tmp` has at least required scratch length
238            unsafe {
239                whir_sumcheck_coeff_moments_round(
240                    &f_coeffs,
241                    &w_moments,
242                    &mut d_s_evals,
243                    &mut d_sumcheck_tmp,
244                    f_height as u32,
245                    device_ctx.stream.as_raw(),
246                )
247                .map_err(|error| WhirProverError::SumcheckMleRound {
248                    error,
249                    whir_round,
250                    round,
251                })?;
252            }
253            let s_evals = d_s_evals.to_host_on(device_ctx)?;
254            for &eval in &s_evals {
255                transcript.observe_ext(eval);
256            }
257            whir_sumcheck_polys.push(s_evals.try_into().unwrap());
258
259            folding_pow_witnesses.push(
260                transcript
261                    .grind_gpu(whir_params.folding_pow_bits, device_ctx)
262                    .map_err(WhirProverError::FoldingGrind)?,
263            );
264            let alpha = transcript.sample_ext();
265
266            // Fold `f` and `w` in coefficient/moment form with respect to `alpha`.
267            // SAFETY:
268            // - input buffers have length `f_height`.
269            // - output buffers have length `f_height / 2`.
270            unsafe {
271                whir_fold_coeffs_and_moments(
272                    &f_coeffs,
273                    &w_moments,
274                    &mut new_f_coeffs,
275                    &mut new_w_moments,
276                    alpha,
277                    f_height as u32,
278                    device_ctx.stream.as_raw(),
279                )
280                .map_err(|error| WhirProverError::FoldMle {
281                    error,
282                    whir_round,
283                    round,
284                })?;
285            }
286            f_coeffs = new_f_coeffs;
287            w_moments = new_w_moments;
288        }
289        // Define g^ = f^(alpha, \cdot) and send matrix commit of RS(g^)
290        // `f_coeffs` is the coefficient form of f^(alpha, \cdot).
291        let f_height = 1 << (m - k_whir);
292        debug_assert!(f_coeffs.len() >= f_height);
293        debug_assert_eq!(size_of::<EF>() / size_of::<F>(), D_EF);
294        let mut g_coeffs = DeviceBuffer::<F>::with_capacity_on(f_height * D_EF, device_ctx);
295        // SAFETY: we allocated `f_coeffs.len() * D_EF` space for `g_coeffs` to do a 1-to-D_EF
296        // (1-to-4) split
297        unsafe {
298            split_ext_to_base_col_major_matrix(
299                &mut g_coeffs,
300                &f_coeffs,
301                f_height as u64,
302                f_height as u32,
303                device_ctx.stream.as_raw(),
304            )
305            .map_err(|error| WhirProverError::SplitExtPoly { error, whir_round })?;
306        }
307        let (g_tree, z_0) = if !is_last_round {
308            let codeword_height = 1 << (log_rs_domain_size - 1);
309            // `g: \mathcal{L}^{(2)} \to \mathbb F`
310            let g_rs = DeviceBuffer::<F>::with_capacity_on(D_EF * codeword_height, device_ctx);
311            // SAFETY:
312            // - g_coeffs is a single EF polynomial, treated as 4 F-polynomials of height
313            //   2^{m-k_whir}
314            // - We resize each F-poly to RS domain size 2^{log_rs_domain_size - 1}, which is
315            //   equivalent to resizing the EF-polynomial
316            unsafe {
317                batch_expand_pad(
318                    g_rs.as_mut_ptr(),
319                    g_coeffs.as_ptr(),
320                    D_EF as u32,
321                    codeword_height as u32,
322                    f_height as u32,
323                    device_ctx.stream.as_raw(),
324                )
325                .map_err(|error| WhirProverError::BatchExpandPad { error, whir_round })?;
326
327                batch_ntt(
328                    &g_rs,
329                    (log_rs_domain_size - 1) as u32,
330                    0u32,
331                    D_EF as u32,
332                    true,
333                    false,
334                    device_ctx,
335                );
336            }
337
338            let g_tree = MerkleTreeGpu::<F, HS::Digest>::new_with_hash::<HS::MerkleHash>(
339                DeviceMatrix::new(Arc::new(g_rs), codeword_height, D_EF),
340                1 << k_whir,
341                true,
342                device_ctx,
343            )
344            .map_err(WhirProverError::MerkleTree)?;
345            let g_commit = g_tree.root();
346            transcript.observe_commit(g_commit);
347            codeword_commits.push(g_commit);
348
349            let z_0 = transcript.sample_ext();
350            // SAFETY:
351            // - `g_coeffs` is coefficient form of `\hat{g}`, which is degree `2^{m-k_whir}`.
352            // - `g_coeffs` is F-column major matrix.
353            let g_opened_value = unsafe {
354                eval_poly_ext_at_point_from_base(&g_coeffs, 1 << (m - k_whir), z_0, device_ctx)
355                    .map_err(|error| WhirProverError::EvalPolyAtPoint { error, whir_round })?
356            };
357            transcript.observe_ext(g_opened_value);
358            ood_values.push(g_opened_value);
359
360            (Some(g_tree), Some(z_0))
361        } else {
362            // Observe the final poly
363            debug_assert_eq!(log_final_poly_len, m - k_whir);
364            let final_poly_len = 1 << log_final_poly_len;
365            let base_coeffs = g_coeffs.to_host_on(device_ctx)?;
366            debug_assert_eq!(base_coeffs.len(), D_EF * final_poly_len);
367            let mut coeffs = Vec::with_capacity(final_poly_len);
368            for i in 0..final_poly_len {
369                let coeff = EF::from_basis_coefficients_fn(|j| base_coeffs[j * final_poly_len + i]);
370                transcript.observe_ext(coeff);
371                coeffs.push(coeff);
372            }
373            final_poly = Some(coeffs);
374            (None, None)
375        };
376
377        // omega is generator of RS domain `\mathcal{L}^{(2^k)}`
378        let omega = F::two_adic_generator(log_rs_domain_size - k_whir);
379        let num_queries = round_params.num_queries;
380        let mut query_indices = Vec::with_capacity(num_queries);
381        query_phase_pow_witnesses.push(
382            transcript
383                .grind_gpu(whir_params.query_phase_pow_bits, device_ctx)
384                .map_err(WhirProverError::QueryPhaseGrind)?,
385        );
386        // Sample query indices first
387        for _ in 0..num_queries {
388            // This is the index of the leaf in the Merkle tree
389            let index = transcript.sample_bits(log_rs_domain_size - k_whir);
390            query_indices.push(index as usize);
391        }
392        if !is_last_round {
393            codeword_opened_values.push(vec![]);
394            codeword_merkle_proofs.push(vec![]);
395        }
396        if whir_round == 0 {
397            // Vector to hold owned copies of backing matrices that are regenerated in the case they
398            // were not cached
399            let mut backing_mats_owned = vec![None; stacked_per_commit.len()];
400            let mut backing_matrices = Vec::with_capacity(stacked_per_commit.len());
401            let mut trees = Vec::with_capacity(stacked_per_commit.len());
402            for (d, backing_mat_owned) in zip(&mut stacked_per_commit, &mut backing_mats_owned) {
403                trees.push(&d.inner.tree);
404                if let Some(matrix) = d.inner.tree.backing_matrix.as_ref() {
405                    backing_matrices.push(matrix);
406                } else {
407                    let layout = d.layout();
408                    let traces = d.traces.iter().collect_vec();
409                    debug_assert!(!traces.is_empty());
410                    let backing_matrix =
411                        rs_code_matrix(log_blowup, layout, &traces, &d.inner.matrix, device_ctx)
412                            .map_err(WhirProverError::RsCodeMatrix)?;
413                    d.traces.clear();
414                    *backing_mat_owned = Some(backing_matrix);
415                    backing_matrices.push(backing_mat_owned.as_ref().unwrap());
416                }
417            }
418            // Get merkle proofs for in-domain samples necessary to evaluate Fold(f, \vec
419            // \alpha)(z_i)
420            initial_round_merkle_proofs =
421                <MerkleTreeGpu<F, HS::Digest>>::batch_query_merkle_proofs(
422                    trees.as_slice(),
423                    &query_indices,
424                    device_ctx,
425                )
426                .map_err(WhirProverError::MerkleTree)?;
427
428            let query_stride = trees[0].query_stride();
429            debug_assert!(
430                trees.iter().all(|tree| tree.query_stride() == query_stride),
431                "Merkle trees don't have same layer size"
432            );
433            let num_rows_per_query = trees[0].rows_per_query;
434            debug_assert!(
435                trees
436                    .iter()
437                    .all(|tree| tree.rows_per_query == num_rows_per_query),
438                "Merkle trees don't have same rows_per_query"
439            );
440
441            initial_round_opened_rows = MerkleTreeGpu::<F, HS::Digest>::batch_open_rows(
442                &backing_matrices,
443                &query_indices,
444                query_stride,
445                num_rows_per_query,
446                device_ctx,
447            )
448            .map_err(WhirProverError::MerkleTree)?
449            .into_iter()
450            .map(|rows_per_commit| {
451                rows_per_commit
452                    .into_iter()
453                    .map(|rows| {
454                        let width = rows.len() / num_rows_per_query;
455                        rows.chunks_exact(width).map(|row| row.to_vec()).collect()
456                    })
457                    .collect()
458            })
459            .collect();
460            debug_assert_eq!(
461                Arc::strong_count(&stacked_per_commit[0].inner),
462                1,
463                "common_main_pcs_data should be owned"
464            );
465            stacked_per_commit.clear(); // this drops common_main_pcs_data
466            mem.tracing_info("after_initial_whir_round");
467        } else {
468            let tree: &MerkleTreeGpu<F, HS::Digest> = rs_tree.as_ref().unwrap();
469            codeword_merkle_proofs[whir_round - 1] =
470                <MerkleTreeGpu<F, HS::Digest>>::batch_query_merkle_proofs(
471                    &[tree],
472                    &query_indices,
473                    device_ctx,
474                )
475                .map_err(WhirProverError::MerkleTree)?
476                .pop()
477                .expect("exactly 1 tree");
478            codeword_opened_values[whir_round - 1] =
479                MerkleTreeGpu::<F, HS::Digest>::batch_open_rows(
480                    &[tree.backing_matrix.as_ref().unwrap()],
481                    &query_indices,
482                    tree.query_stride(),
483                    tree.rows_per_query,
484                    device_ctx,
485                )
486                .map_err(WhirProverError::MerkleTree)?
487                .pop()
488                .unwrap()
489                .into_iter()
490                .map(EF::reconstitute_from_base)
491                .collect();
492        }
493        rs_tree = g_tree;
494
495        // We still sample on the last round to match the verifier, who uses a
496        // final gamma to unify some logic. But we do not need to update
497        // `w_moments`.
498        let gamma = transcript.sample_ext();
499
500        if !is_last_round {
501            // Update moments of \hat{w}:
502            // M(T) += gamma * z0^T + sum_i gamma^{i+1} * z_i^T.
503            let log_height = (m - k_whir) as u32;
504            let z0 = z_0.unwrap();
505            let z_points = query_indices
506                .iter()
507                .map(|&index| omega.exp_u64(index as u64))
508                .collect_vec();
509
510            let mut z0_pows2 = Vec::with_capacity(log_height as usize);
511            let mut z0_pow = z0;
512            for _ in 0..log_height {
513                z0_pows2.push(z0_pow);
514                z0_pow = z0_pow.square();
515            }
516
517            let mut z_pows2 = Vec::with_capacity(num_queries * log_height as usize);
518            for z in &z_points {
519                let mut z_pow = *z;
520                for _ in 0..log_height {
521                    z_pows2.push(z_pow);
522                    z_pow = z_pow.square();
523                }
524            }
525
526            let d_z0_pows2 = z0_pows2.to_device_on(device_ctx)?;
527            let d_z_pows2 = z_pows2.to_device_on(device_ctx)?;
528            unsafe {
529                w_moments_accumulate(
530                    &mut w_moments,
531                    &d_z0_pows2,
532                    &d_z_pows2,
533                    gamma,
534                    num_queries as u32,
535                    log_height,
536                    device_ctx.stream.as_raw(),
537                )
538                .map_err(|error| WhirProverError::WMomentsAccumulate { error, whir_round })?;
539            }
540        }
541
542        m -= k_whir;
543        log_rs_domain_size -= 1;
544    }
545
546    mem.emit_metrics();
547    Ok(WhirProof {
548        mu_pow_witness,
549        whir_sumcheck_polys,
550        codeword_commits,
551        ood_values,
552        folding_pow_witnesses,
553        query_phase_pow_witnesses,
554        initial_round_opened_rows,
555        initial_round_merkle_proofs,
556        codeword_opened_values,
557        codeword_merkle_proofs,
558        final_poly: final_poly.unwrap(),
559    })
560}
561
562#[cfg(test)]
563mod tests {
564    use std::sync::Arc;
565
566    use itertools::Itertools;
567    use openvm_stark_backend::{
568        keygen::types::MultiStarkProvingKey,
569        prover::{
570            stacked_pcs::stacked_commit, CpuColMajorBackend, DeviceDataTransporter, ProvingContext,
571        },
572        test_utils::{CachedFixture11, FibFixture, TestFixture},
573        verifier::whir::{verify_whir, VerifyWhirError},
574        StarkEngine, StarkProtocolConfig, SystemParams, WhirConfig, WhirParams,
575        WhirProximityStrategy,
576    };
577    use openvm_stark_sdk::{
578        config::{
579            baby_bear_poseidon2::{
580                default_duplex_sponge, BabyBearPoseidon2RefEngine, DuplexSponge,
581            },
582            log_up_params::log_up_security_params_baby_bear_100_bits,
583        },
584        utils::setup_tracing_with_log_level,
585    };
586    use rand::{rngs::StdRng, SeedableRng};
587    use test_case::test_case;
588    use tracing::Level;
589
590    use crate::{
591        prelude::SC, sponge::DuplexSpongeGpu, stacked_reduction::StackedPcsData2,
592        whir::prove_whir_opening_gpu, BabyBearPoseidon2GpuEngine, GpuBackend,
593    };
594
595    /// GPU-specific WHIR test runner. Uses `prove_whir_opening_gpu` for the
596    /// proving step and the shared CPU verifier for verification.
597    fn run_whir_test_gpu(
598        params: SystemParams,
599        pk: MultiStarkProvingKey<SC>,
600        ctx: ProvingContext<CpuColMajorBackend<SC>>,
601    ) -> Result<(), VerifyWhirError> {
602        let engine = BabyBearPoseidon2GpuEngine::new(params.clone());
603        let device = engine.device();
604        let mut rng = StdRng::seed_from_u64(0);
605        let (z_prism, z_cube) = openvm_backend_tests::generate_random_z(&params, &mut rng);
606
607        let common_main_traces = ctx
608            .common_main_traces()
609            .map(|(_, trace)| trace)
610            .collect_vec();
611        // Use CPU stacked_commit to isolate GPU testing to WHIR prover
612        let (common_main_commit, common_main_pcs_data) = stacked_commit(
613            engine.config().hasher(),
614            params.l_skip,
615            params.n_stack,
616            params.log_blowup,
617            params.k_whir(),
618            &common_main_traces,
619        )
620        .unwrap();
621        let d_common_main_traces = common_main_traces
622            .iter()
623            .map(|t| {
624                <_ as DeviceDataTransporter<SC, GpuBackend>>::transport_matrix_to_device(device, t)
625            })
626            .collect_vec();
627        let d_common_main_pcs_data =
628            <_ as DeviceDataTransporter<SC, GpuBackend>>::transport_pcs_data_to_device(
629                device,
630                &common_main_pcs_data,
631            );
632
633        let mut stacking_openings = vec![openvm_backend_tests::stacking_openings_for_matrix(
634            &params,
635            &z_prism,
636            &common_main_pcs_data.matrix,
637        )];
638        let mut commits = vec![common_main_commit];
639        let mut stacked_per_commit = vec![StackedPcsData2 {
640            inner: Arc::new(d_common_main_pcs_data),
641            traces: d_common_main_traces,
642        }];
643        for (air_id, air_ctx) in ctx.per_trace {
644            for data in pk.per_air[air_id]
645                .preprocessed_data
646                .iter()
647                .chain(air_ctx.cached_mains.iter().map(|cd| &cd.data))
648            {
649                let trace =
650                    <_ as DeviceDataTransporter<SC, GpuBackend>>::transport_matrix_to_device(
651                        device,
652                        &data.mat_view(0).to_matrix(),
653                    );
654                commits.push(data.commit().unwrap());
655                stacking_openings.push(openvm_backend_tests::stacking_openings_for_matrix(
656                    &params,
657                    &z_prism,
658                    &data.matrix,
659                ));
660                let d_data =
661                    <_ as DeviceDataTransporter<SC, GpuBackend>>::transport_pcs_data_to_device(
662                        device, data,
663                    );
664                stacked_per_commit.push(StackedPcsData2 {
665                    inner: Arc::new(d_data),
666                    traces: vec![trace],
667                });
668            }
669        }
670
671        let mut prover_sponge = DuplexSpongeGpu::default();
672
673        let proof = prove_whir_opening_gpu::<crate::DefaultHashScheme, _>(
674            &params,
675            &mut prover_sponge,
676            stacked_per_commit,
677            &z_cube,
678            &device.device_ctx,
679        )
680        .unwrap();
681
682        let mut verifier_sponge = default_duplex_sponge();
683        verify_whir(
684            &mut verifier_sponge,
685            engine.config(),
686            &proof,
687            &stacking_openings,
688            &commits,
689            &z_cube,
690        )
691    }
692
693    fn run_whir_fixture_test_gpu<F>(
694        params: SystemParams,
695        engine: &BabyBearPoseidon2RefEngine<DuplexSponge>,
696        fx: F,
697    ) -> Result<(), VerifyWhirError>
698    where
699        F: TestFixture<SC>,
700    {
701        let (pk, _vk) = fx.keygen(engine);
702        let ctx = fx.generate_proving_ctx().into_sorted();
703        run_whir_test_gpu(params, pk, ctx)
704    }
705
706    fn whir_test_params(k_whir: usize, log_final_poly_len: usize) -> WhirParams {
707        WhirParams {
708            k: k_whir,
709            log_final_poly_len,
710            query_phase_pow_bits: 2,
711            folding_pow_bits: 1,
712            mu_pow_bits: 3,
713            proximity: WhirProximityStrategy::UniqueDecoding,
714        }
715    }
716
717    fn whir_test_system_params(
718        n_stack: usize,
719        log_blowup: usize,
720        k_whir: usize,
721        log_final_poly_len: usize,
722    ) -> SystemParams {
723        let l_skip = 2;
724        let w_stack = 8;
725        let whir = WhirConfig::new(
726            log_blowup,
727            l_skip + n_stack,
728            whir_test_params(k_whir, log_final_poly_len),
729            10,
730        );
731        SystemParams {
732            l_skip: 2,
733            n_stack,
734            w_stack,
735            log_blowup,
736            whir,
737            logup: log_up_security_params_baby_bear_100_bits(0.0),
738            max_constraint_degree: 3,
739        }
740    }
741
742    #[test_case(0, 1, 1, 0)]
743    #[test_case(2, 1, 1, 2)]
744    #[test_case(2, 1, 2, 0)]
745    #[test_case(2, 1, 3, 1)]
746    #[test_case(2, 1, 4, 0)]
747    #[test_case(2, 2, 4, 0)]
748    fn test_whir_single_fib_gpu(
749        n_stack: usize,
750        log_blowup: usize,
751        k_whir: usize,
752        log_final_poly_len: usize,
753    ) -> Result<(), VerifyWhirError> {
754        setup_tracing_with_log_level(Level::DEBUG);
755
756        let params = whir_test_system_params(n_stack, log_blowup, k_whir, log_final_poly_len);
757        let engine = BabyBearPoseidon2RefEngine::<DuplexSponge>::new(params.clone());
758        let height = 1 << params.log_stacked_height();
759
760        run_whir_fixture_test_gpu(params, &engine, FibFixture::new(0, 1, height))
761    }
762
763    #[test_case(2, 1, 1, 2)]
764    #[test_case(2, 1, 2, 0)]
765    #[test_case(2, 1, 3, 1)]
766    #[test_case(2, 1, 4, 0)]
767    #[test_case(2, 2, 4, 0)]
768    fn test_whir_cached_gpu(
769        n_stack: usize,
770        log_blowup: usize,
771        k_whir: usize,
772        log_final_poly_len: usize,
773    ) -> Result<(), VerifyWhirError> {
774        setup_tracing_with_log_level(Level::DEBUG);
775
776        let params = whir_test_system_params(n_stack, log_blowup, k_whir, log_final_poly_len);
777        let engine = BabyBearPoseidon2RefEngine::<DuplexSponge>::new(params.clone());
778
779        run_whir_fixture_test_gpu(
780            params,
781            &engine,
782            CachedFixture11::new(engine.config().clone()),
783        )
784    }
785}