Skip to main content

openvm_cuda_backend/
stacked_pcs.rs

1use std::{ffi::c_void, sync::Arc};
2
3use getset::Getters;
4use itertools::Itertools;
5use openvm_cuda_common::{
6    copy::cuda_memcpy_on, d_buffer::DeviceBuffer, memory_manager::MemTracker, stream::GpuDeviceCtx,
7};
8use openvm_stark_backend::{
9    p3_util::log2_strict_usize,
10    prover::{stacked_pcs::StackedLayout, MatrixDimensions},
11};
12use tracing::instrument;
13
14use crate::{
15    base::{DeviceMatrix, DeviceMatrixView},
16    cuda::{
17        batch_ntt_small::batch_ntt_small,
18        matrix::{batch_expand_pad, batch_expand_pad_wide},
19        ntt::bit_rev,
20    },
21    hash_scheme::GpuMerkleHash,
22    merkle_tree::{MerkleTreeConstructor, MerkleTreeGpu},
23    ntt::batch_ntt,
24    poly::{mle_interpolate_stages, PleMatrix},
25    prelude::F,
26    GpuProverConfig, ProverError, RsCodeMatrixError, StackTracesError,
27};
28
29#[derive(Getters)]
30pub struct StackedPcsDataGpu<F, Digest> {
31    /// Layout of the unstacked collection of matrices within the stacked matrix.
32    #[getset(get = "pub")]
33    pub(crate) layout: StackedLayout,
34    /// The stacked matrix with height `2^{l_skip + n_stack}`.
35    /// This is optionally cached depending on the prover configuration:
36    /// - Caching increases the peak GPU memory but avoids a recomputation during stacked
37    ///   reduction.
38    /// - Not caching means the stacked matrix computation is recomputed during stacked reduction,
39    ///   but lowers the peak GPU memory.
40    #[getset(get = "pub")]
41    pub(crate) matrix: Option<PleMatrix<F>>,
42    /// Merkle tree of the Reed-Solomon codewords of the stacked matrix.
43    /// Depends on `k_whir` parameter.
44    #[getset(get = "pub")]
45    pub(crate) tree: MerkleTreeGpu<F, Digest>,
46}
47
48#[allow(clippy::type_complexity)]
49#[instrument(level = "info", skip_all)]
50pub fn stacked_commit<MH: GpuMerkleHash + MerkleTreeConstructor>(
51    l_skip: usize,
52    n_stack: usize,
53    log_blowup: usize,
54    k_whir: usize,
55    traces: &[&DeviceMatrix<F>],
56    prover_config: GpuProverConfig,
57    device_ctx: &GpuDeviceCtx,
58) -> Result<(MH::Digest, StackedPcsDataGpu<F, MH::Digest>), ProverError> {
59    let mut mem = MemTracker::start("prover.stacked_commit");
60    mem.tracing_info("before stacked_commit");
61    mem.reset_peak();
62    let layout = get_stacked_layout(l_skip, n_stack, traces);
63    tracing::info!(
64        height = layout.height(),
65        width = layout.width(),
66        "stacked_matrix_dimensions"
67    );
68    let opt_stacked_matrix = if prover_config.cache_stacked_matrix {
69        Some(stack_traces(&layout, traces, device_ctx)?)
70    } else {
71        None
72    };
73    let rs_matrix = rs_code_matrix(log_blowup, &layout, traces, &opt_stacked_matrix, device_ctx)?;
74    let tree = MerkleTreeGpu::<F, MH::Digest>::new_with_hash::<MH>(
75        rs_matrix,
76        1 << k_whir,
77        prover_config.cache_rs_code_matrix,
78        device_ctx,
79    )?;
80    let root = tree.root();
81    let data = StackedPcsDataGpu {
82        layout,
83        matrix: opt_stacked_matrix,
84        tree,
85    };
86    mem.emit_metrics();
87    Ok((root, data))
88}
89
90/// The `traces` **must** already be in height-sorted order.
91///
92/// This function is generic in `F` and only relies on CUDA memory operations.
93#[instrument(skip_all)]
94pub fn stacked_matrix(
95    l_skip: usize,
96    n_stack: usize,
97    traces: &[&DeviceMatrix<F>],
98    device_ctx: &GpuDeviceCtx,
99) -> Result<(PleMatrix<F>, StackedLayout), ProverError> {
100    let layout = get_stacked_layout(l_skip, n_stack, traces);
101    let matrix = stack_traces(&layout, traces, device_ctx)?;
102    Ok((matrix, layout))
103}
104
105pub(crate) fn get_stacked_layout(
106    l_skip: usize,
107    n_stack: usize,
108    traces: &[&DeviceMatrix<F>],
109) -> StackedLayout {
110    let sorted_meta = traces
111        .iter()
112        .map(|trace| {
113            // height cannot be zero:
114            let log_height = log2_strict_usize(trace.height());
115            (trace.width(), log_height)
116        })
117        .collect_vec();
118    debug_assert!(sorted_meta.is_sorted_by(|a, b| a.1 >= b.1));
119    StackedLayout::new(l_skip, l_skip + n_stack, sorted_meta).unwrap()
120}
121
122pub(crate) fn stack_traces(
123    layout: &StackedLayout,
124    traces: &[&DeviceMatrix<F>],
125    device_ctx: &GpuDeviceCtx,
126) -> Result<PleMatrix<F>, StackTracesError> {
127    let mem = MemTracker::start("prover.stack_traces");
128    let l_skip = layout.l_skip();
129    let height = layout.height();
130    let width = layout.width();
131    let mut q_evals =
132        DeviceBuffer::<F>::with_capacity_on(width.checked_mul(height).unwrap(), device_ctx);
133    stack_traces_into_expanded(layout, traces, &mut q_evals, height, device_ctx)?;
134    mem.emit_metrics();
135    Ok(PleMatrix::from_evals(
136        l_skip, q_evals, height, width, device_ctx,
137    ))
138}
139
140/// `buffer` should be the buffer to write the stacked traces into.
141/// `buffer` should be a matrix with dimensions `padded_height x width` where `width` is the stacked
142/// width and `padded_height` must be a multiple of the stacked height.
143pub(crate) fn stack_traces_into_expanded(
144    layout: &StackedLayout,
145    traces: &[&DeviceMatrix<F>],
146    buffer: &mut DeviceBuffer<F>,
147    padded_height: usize,
148    device_ctx: &GpuDeviceCtx,
149) -> Result<(), StackTracesError> {
150    let l_skip = layout.l_skip();
151    debug_assert_eq!(padded_height % layout.height(), 0);
152    debug_assert_eq!(buffer.len() % padded_height, 0);
153    debug_assert_eq!(buffer.len() / padded_height, layout.width());
154    buffer
155        .fill_zero_on(device_ctx)
156        .map_err(StackTracesError::FillZero)?;
157    let mut idx = 0;
158    while idx < layout.sorted_cols.len() {
159        let (mat_idx, j, s) = &layout.sorted_cols[idx];
160        let start = s.col_idx * padded_height + s.row_idx;
161        let trace = traces[*mat_idx];
162        let s_len = s.len(l_skip);
163        debug_assert_eq!(trace.height(), 1 << s.log_height());
164        if s.log_height() >= l_skip {
165            debug_assert_eq!(trace.height(), s_len);
166            let mut copy_len = s_len;
167            let mut end = idx + 1;
168            while end < layout.sorted_cols.len() {
169                let (next_mat_idx, next_j, next_s) = &layout.sorted_cols[end];
170                if *next_mat_idx != *mat_idx || next_s.log_height() != s.log_height() {
171                    break;
172                }
173                let expected_j = *j + (end - idx);
174                let next_len = next_s.len(l_skip);
175                let next_start = next_s.col_idx * padded_height + next_s.row_idx;
176                if *next_j != expected_j || next_len != s_len || next_start != start + copy_len {
177                    break;
178                }
179                copy_len += next_len;
180                end += 1;
181            }
182
183            // SAFETY: matrix buffers are allocated correctly with respect to dimensions.
184            // The grouped columns are contiguous in both source and destination buffers.
185            unsafe {
186                let src = trace.buffer().as_ptr().add(*j * s_len);
187                let dst = buffer.as_mut_ptr().add(start);
188                cuda_memcpy_on::<true, true>(
189                    dst as *mut c_void,
190                    src as *const c_void,
191                    copy_len * size_of::<F>(),
192                    device_ctx,
193                )?;
194            }
195            idx = end;
196        } else {
197            let stride = s.stride(l_skip);
198            debug_assert_eq!(stride * trace.height(), s_len);
199            // SAFETY: matrix buffers are allocated correctly
200            // - `q_buf` has enough capacity by definition of stacked `layout`
201            // - we abuse `batch_expand_pad` with `poly_count = trace.height()` to create a strided
202            //   column of length `s_len = stride * trace.height()`
203            unsafe {
204                let src = trace.buffer().as_ptr().add(*j * trace.height());
205                let dst = buffer.as_mut_ptr().add(start);
206                batch_expand_pad_wide(
207                    dst,
208                    src,
209                    trace.height() as u32,
210                    stride as u32,
211                    1,
212                    device_ctx.stream.as_raw(),
213                )
214                .map_err(StackTracesError::BatchExpandPadWide)?;
215            }
216            idx += 1;
217        }
218    }
219    Ok(())
220}
221
222/// Computes the Reed-Solomon codeword of each column vector of `eval_matrix` where the rate is
223/// `2^{-log_blowup}`. The column vectors are treated as evaluations of a prismalinear extension on
224/// a hyperprism.
225///
226/// Uses `stacked_matrix` if available, or else stacks `traces` directly into final codeword matrix
227/// buffer.
228#[instrument(skip_all)]
229pub fn rs_code_matrix(
230    log_blowup: usize,
231    layout: &StackedLayout,
232    traces: &[&DeviceMatrix<F>],
233    stacked_matrix: &Option<PleMatrix<F>>,
234    device_ctx: &GpuDeviceCtx,
235) -> Result<DeviceMatrix<F>, RsCodeMatrixError> {
236    let mem = MemTracker::start_and_reset_peak("prover.rs_code_matrix");
237    let l_skip = layout.l_skip();
238    let height = layout.height();
239    let width = layout.width();
240    debug_assert!(height >= (1 << l_skip));
241    let codeword_height = height.checked_shl(log_blowup as u32).unwrap();
242    let mut codewords = DeviceBuffer::<F>::with_capacity_on(codeword_height * width, device_ctx);
243    // The following kernels together perform MLE interpolation followed by coset NTT for
244    // `width` polys from `height -> codeword_height` size domains.
245    if let Some(stacked_matrix) = stacked_matrix.as_ref() {
246        // SAFETY: `codewords` is allocated for `width` polys of `codeword_height` each, and we
247        // expand from `matrix.mixed` which is `width` polys of `height` each.
248        unsafe {
249            batch_expand_pad(
250                codewords.as_mut_ptr(),
251                stacked_matrix.mixed.as_ptr(),
252                width as u32,
253                codeword_height as u32,
254                height as u32,
255                device_ctx.stream.as_raw(),
256            )
257            .map_err(RsCodeMatrixError::BatchExpandPad)?;
258        }
259    } else {
260        stack_traces_into_expanded(layout, traces, &mut codewords, codeword_height, device_ctx)
261            .map_err(RsCodeMatrixError::StackTraces)?;
262        // Currently codewords has the stacked matrix, batch expanded, in evaluation form on
263        // hyperprism. We convert it to mixed form, i.e., unroll `PleMatrix::from_evals`.
264        // PERF[jpw]: We do some wasted work on the padded zero part. A more specialized kernel
265        // could be written to avoid this.
266        if l_skip > 0 {
267            // For univariate coordinate, perform inverse NTT for each 2^l_skip chunk per column:
268            // (width cols) * (codeword_height / 2^l_skip chunks per col). Use natural ordering.
269            let num_uni_poly = width * (codeword_height >> l_skip);
270            unsafe {
271                batch_ntt_small(
272                    &mut codewords,
273                    l_skip,
274                    num_uni_poly,
275                    true,
276                    device_ctx.stream.as_raw(),
277                )
278                .map_err(RsCodeMatrixError::CustomBatchIntt)?;
279            }
280        }
281    }
282    // Eval-to-coeff RS encoding: Instead of full n-stage MLE evals→coeffs interpolation
283    // (where n = log_height - l_skip), we only need l_skip stages of coeffs→evals
284    // (subset-zeta transform) within each 2^l_skip chunk. This is a major optimization:
285    // e.g. for log_height=17, l_skip=2: 2 stages instead of 15.
286    let log_codeword_height = log2_strict_usize(codeword_height);
287
288    // Apply l_skip stages of coeffs_to_evals within each 2^l_skip chunk.
289    // After iNTT, each chunk holds Z-monomial coefficients. We apply the subset-zeta
290    // transform to convert to hypercube evaluations over the Z-bit variables.
291    if l_skip > 0 {
292        // SAFETY: `codewords` is properly initialized and parameters are valid.
293        // Steps 2^0, 2^1, ..., 2^(l_skip-1) stay within chunk boundaries.
294        unsafe {
295            mle_interpolate_stages(
296                codewords.as_mut_ptr(),
297                width,
298                codeword_height as u32,
299                log_blowup as u32,
300                0,                 // start_log_step
301                l_skip as u32 - 1, // end_log_step (inclusive)
302                false,             // coeffs to evals (NOT eval to coeff)
303                false,             // natural order
304                device_ctx.stream.as_raw(),
305            )
306            .map_err(|error| RsCodeMatrixError::MleInterpolateStage2d { error, step: 1 })?;
307        }
308    }
309
310    // Bit-reverse the entire buffer in-place (required for NTT)
311    unsafe {
312        bit_rev(
313            &codewords,
314            &codewords,
315            log_codeword_height as u32,
316            codeword_height as u32,
317            width as u32,
318            device_ctx.stream.as_raw(),
319        )
320        .map_err(RsCodeMatrixError::BitRev)?;
321    }
322
323    // Compute RS codeword via DFT on the smoothly-embedded domain.
324    batch_ntt(
325        &codewords,
326        log_codeword_height as u32,
327        0u32,
328        width as u32,
329        false, // bit-reversal already done
330        false,
331        device_ctx,
332    );
333    let code_matrix = DeviceMatrix::new(Arc::new(codewords), codeword_height, width);
334    mem.emit_metrics();
335
336    Ok(code_matrix)
337}
338
339impl<F, Digest> StackedPcsDataGpu<F, Digest> {
340    /// Returns a view of the specified unstacked matrix in mixed form.
341    ///
342    /// # Notes
343    /// - `width` must be the width of the unstacked matrix.
344    /// - The unstacked matrix may be strided - this must be handled by the caller.
345    pub fn mixed_view<'a>(
346        &'a self,
347        mat_idx: usize,
348        width: usize,
349    ) -> Option<DeviceMatrixView<'a, F>> {
350        if let Some(matrix) = self.matrix.as_ref() {
351            debug_assert_eq!(self.layout.width_of(mat_idx), width);
352            let s = self
353                .layout
354                .get(mat_idx, 0)
355                .unwrap_or_else(|| panic!("Invalid matrix index: {mat_idx}"));
356            let l_skip = self.layout.l_skip();
357            let lifted_height = s.len(l_skip);
358            let offset = s.col_idx * matrix.height() + s.row_idx;
359            // SAFETY:
360            // - by definition of stacked layout and stacked matrix, `ptr` is valid and allocated
361            //   for `lifted_height * width` elements.
362            unsafe {
363                let ptr = matrix.mixed.as_ptr().add(offset);
364                Some(DeviceMatrixView::from_raw_parts(ptr, lifted_height, width))
365            }
366        } else {
367            None
368        }
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use itertools::Itertools;
375    use openvm_cuda_common::{
376        common::get_device,
377        stream::{CudaStream, GpuDeviceCtx, StreamGuard},
378    };
379    use openvm_stark_backend::{
380        prover::ColMajorMatrix,
381        test_utils::{InteractionsFixture11, TestFixture},
382    };
383    use p3_field::PrimeCharacteristicRing;
384
385    use super::*;
386    use crate::{
387        data_transporter::{transport_matrix_d2h_col_major, transport_matrix_h2d_col_major},
388        prelude::{F, SC},
389    };
390
391    fn test_ctx() -> GpuDeviceCtx {
392        GpuDeviceCtx {
393            device_id: get_device().unwrap() as u32,
394            stream: StreamGuard::new(CudaStream::new_non_blocking().unwrap()),
395        }
396    }
397
398    #[test]
399    fn test_stacked_matrix_manual_0() {
400        let device_ctx = test_ctx();
401        let columns = [vec![1, 2, 3, 4], vec![5, 6], vec![7]]
402            .map(|v| v.into_iter().map(F::from_u32).collect_vec());
403        let mats = columns
404            .into_iter()
405            .map(|c| {
406                transport_matrix_h2d_col_major(&ColMajorMatrix::new(c, 1), &device_ctx).unwrap()
407            })
408            .collect_vec();
409        let mat_refs = mats.iter().collect_vec();
410        let l_skip = 0;
411        let (stacked_mat, _layout) = stacked_matrix(0, 2, &mat_refs, &device_ctx).unwrap();
412        assert_eq!(stacked_mat.height(), 4);
413        assert_eq!(stacked_mat.width(), 2);
414        let stacked_h_mat = transport_matrix_d2h_col_major(
415            &stacked_mat.to_evals(l_skip, &device_ctx).unwrap(),
416            &device_ctx,
417        )
418        .unwrap();
419        assert_eq!(
420            stacked_h_mat.values,
421            [1, 2, 3, 4, 5, 6, 7, 0].map(F::from_u32).to_vec()
422        );
423    }
424
425    #[test]
426    fn test_stacked_matrix_manual_1() {
427        let gpu_ctx = test_ctx();
428        let proving_ctx = TestFixture::<SC>::generate_proving_ctx(&InteractionsFixture11);
429        let [send_trace, rcv_trace] = [0, 1].map(|i| {
430            transport_matrix_h2d_col_major(&proving_ctx.per_trace[i].1.common_main, &gpu_ctx)
431                .unwrap()
432        });
433        let l_skip = 2;
434        let n_stack = 8;
435        let (stacked_mat, _layout) =
436            stacked_matrix(l_skip, n_stack, &[&rcv_trace, &send_trace], &gpu_ctx).unwrap();
437        assert_eq!(stacked_mat.height(), 1 << (l_skip + n_stack));
438        assert_eq!(stacked_mat.width(), 1);
439        let stacked_h_mat = transport_matrix_d2h_col_major(
440            &stacked_mat.to_evals(l_skip, &gpu_ctx).unwrap(),
441            &gpu_ctx,
442        )
443        .unwrap();
444        let mut expected = vec![F::ZERO; 1 << (l_skip + n_stack)];
445        expected[..24].copy_from_slice(
446            &[
447                1, 3, 4, 2, 0, 545, 1, 0, 5, 4, 4, 5, 123, 889, 889, 456, 0, 3, 7, 546, 1, 5, 4,
448                889,
449            ]
450            .map(F::from_u32),
451        );
452        assert_eq!(stacked_h_mat.values, expected);
453    }
454
455    #[test]
456    fn test_stacked_matrix_manual_strided_0() {
457        let device_ctx = test_ctx();
458        let columns = [vec![1, 2, 3, 4], vec![5, 6], vec![7]]
459            .map(|v| v.into_iter().map(F::from_u32).collect_vec());
460        let mats = columns
461            .into_iter()
462            .map(|c| {
463                transport_matrix_h2d_col_major(&ColMajorMatrix::new(c, 1), &device_ctx).unwrap()
464            })
465            .collect_vec();
466        let mat_refs = mats.iter().collect_vec();
467        let l_skip = 2;
468        let (stacked_mat, _layout) = stacked_matrix(l_skip, 0, &mat_refs, &device_ctx).unwrap();
469        assert_eq!(stacked_mat.height(), 4);
470        assert_eq!(stacked_mat.width(), 3);
471        let stacked_h_mat = transport_matrix_d2h_col_major(
472            &stacked_mat.to_evals(l_skip, &device_ctx).unwrap(),
473            &device_ctx,
474        )
475        .unwrap();
476        assert_eq!(
477            stacked_h_mat.values,
478            [1, 2, 3, 4, 5, 0, 6, 0, 7, 0, 0, 0]
479                .map(F::from_u32)
480                .to_vec()
481        );
482    }
483
484    #[test]
485    fn test_stacked_matrix_manual_strided_1() {
486        let device_ctx = test_ctx();
487        let columns = [vec![1, 2, 3, 4], vec![5, 6], vec![7]]
488            .map(|v| v.into_iter().map(F::from_u32).collect_vec());
489        let mats = columns
490            .into_iter()
491            .map(|c| {
492                transport_matrix_h2d_col_major(&ColMajorMatrix::new(c, 1), &device_ctx).unwrap()
493            })
494            .collect_vec();
495        let mat_refs = mats.iter().collect_vec();
496        let l_skip = 3;
497        let (stacked_mat, _layout) = stacked_matrix(l_skip, 0, &mat_refs, &device_ctx).unwrap();
498        assert_eq!(stacked_mat.height(), 8);
499        assert_eq!(stacked_mat.width(), 3);
500        let stacked_h_mat = transport_matrix_d2h_col_major(
501            &stacked_mat.to_evals(l_skip, &device_ctx).unwrap(),
502            &device_ctx,
503        )
504        .unwrap();
505        assert_eq!(
506            stacked_h_mat.values,
507            [
508                [1, 0, 2, 0, 3, 0, 4, 0],
509                [5, 0, 0, 0, 6, 0, 0, 0],
510                [7, 0, 0, 0, 0, 0, 0, 0]
511            ]
512            .into_iter()
513            .flatten()
514            .map(F::from_u32)
515            .collect_vec()
516        );
517    }
518}