Skip to main content

openvm_stark_backend/utils/
mod.rs

1use p3_field::Field;
2use tracing::instrument;
3
4use crate::air_builders::debug::USE_DEBUG_BUILDER;
5
6mod batch_inverse;
7pub use batch_inverse::*;
8
9// Copied from valida-util
10/// Calculates and returns the multiplicative inverses of each field element, with zero
11/// values remaining unchanged.
12#[instrument(name = "batch_multiplicative_inverse", level = "info", skip_all)]
13pub fn batch_multiplicative_inverse_allowing_zero<F: Field>(values: Vec<F>) -> Vec<F> {
14    // Check if values are zero, and construct a new vector with only nonzero values
15    let mut nonzero_values = Vec::with_capacity(values.len());
16    let mut indices = Vec::with_capacity(values.len());
17    for (i, value) in values.iter().cloned().enumerate() {
18        if value.is_zero() {
19            continue;
20        }
21        nonzero_values.push(value);
22        indices.push(i);
23    }
24
25    // Compute the multiplicative inverse of nonzero values
26    let inverse_nonzero_values = p3_field::batch_multiplicative_inverse(&nonzero_values);
27
28    // Reconstruct the original vector
29    let mut result = values.clone();
30    for (i, index) in indices.into_iter().enumerate() {
31        result[index] = inverse_nonzero_values[i];
32    }
33
34    result
35}
36
37/// This utility function will parallelize an operation that is to be
38/// performed over a mutable slice.
39///
40/// Assumes that slice length is a multiple of `chunk_size` and parallelization preserves the chunks
41/// so each slice in a thread is still multiple of `chunk_size`.
42///
43/// The closure `f` takes `(thread_slice, idx)` where `thread_slice` is a sub-slice starting at
44/// `v[idx]`.
45// Copied and modified from https://github.com/axiom-crypto/halo2/blob/4e584896b62c981ec7c7dced4a9ca95b82306550/halo2_proofs/src/arithmetic.rs#L157
46pub fn parallelize_chunks<T, F>(v: &mut [T], chunk_size: usize, f: F)
47where
48    T: Send,
49    F: Fn(&mut [T], usize) + Send + Sync + Clone,
50{
51    debug_assert_eq!(v.len() % chunk_size, 0);
52    #[cfg(not(feature = "parallel"))]
53    {
54        f(v, 0)
55    }
56    // Algorithm rationale:
57    //
58    // Using the stdlib `chunks_mut` will lead to severe load imbalance.
59    // From https://github.com/rust-lang/rust/blob/e94bda3/library/core/src/slice/iter.rs#L1607-L1637
60    // if the division is not exact, the last chunk will be the remainder.
61    //
62    // Dividing 40 items on 12 threads will lead to a chunk size of 40/12 = 3,
63    // There will be a 13 chunks of size 3 and 1 of size 1 distributed on 12 threads.
64    // This leads to 1 thread working on 6 iterations, 1 on 4 iterations and 10 on 3 iterations,
65    // a load imbalance of 2x.
66    //
67    // Instead we can divide work into chunks of size
68    // 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3 = 4*4 + 3*8 = 40
69    //
70    // This would lead to a 6/4 = 1.5x speedup compared to naive chunks_mut
71    //
72    // See also OpenMP spec (page 60)
73    // http://www.openmp.org/mp-documents/openmp-4.5.pdf
74    // "When no chunk_size is specified, the iteration space is divided into chunks
75    // that are approximately equal in size, and at most one chunk is distributed to
76    // each thread. The size of the chunks is unspecified in this case."
77    // This implies chunks are the same size ±1
78    #[cfg(feature = "parallel")]
79    {
80        let f = &f;
81        let total_iters = v.len() / chunk_size;
82        let num_threads = rayon::current_num_threads();
83
84        let lo_slice_size = (total_iters / num_threads) * chunk_size;
85        let hi_slice_size = lo_slice_size + chunk_size;
86        let cutoff_thread_idx = total_iters % num_threads;
87        let split_pos = cutoff_thread_idx * hi_slice_size;
88        let (v_hi, v_lo) = v.split_at_mut(split_pos);
89
90        rayon::scope(|scope| {
91            // Skip special-case: number of iterations is cleanly divided by number of threads.
92            if cutoff_thread_idx != 0 {
93                for (chunk_id, chunk) in v_hi.chunks_exact_mut(hi_slice_size).enumerate() {
94                    let offset = chunk_id * hi_slice_size;
95                    scope.spawn(move |_| f(chunk, offset));
96                }
97            }
98            // Skip special-case: less iterations than number of threads.
99            if lo_slice_size != 0 {
100                for (chunk_id, chunk) in v_lo.chunks_exact_mut(lo_slice_size).enumerate() {
101                    let offset = split_pos + (chunk_id * lo_slice_size);
102                    scope.spawn(move |_| f(chunk, offset));
103                }
104            }
105        });
106    }
107}
108
109/// Disables the debug builder so there are not debug assert panics.
110/// Commonly used in negative tests to prevent panics.
111pub fn disable_debug_builder() {
112    USE_DEBUG_BUILDER.with(|debug| {
113        *debug.lock().unwrap() = false;
114    });
115}
116
117#[macro_export]
118#[cfg(feature = "parallel")]
119macro_rules! parizip {
120    ( $first:expr $( , $rest:expr )* $(,)* ) => {
121        {
122            use rayon::iter::*;
123            ( $first $( , $rest)* ).into_par_iter()
124        }
125    };
126}
127#[macro_export]
128#[cfg(not(feature = "parallel"))]
129macro_rules! parizip {
130    ( $first:expr $( , $rest:expr )* $(,)* ) => {
131        itertools::izip!( $first $( , $rest)* )
132    };
133}