Skip to main content

winter_prover/matrix/
row_matrix.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2//
3// This source code is licensed under the MIT license found in the
4// LICENSE file in the root directory of this source tree.
5
6use alloc::vec::Vec;
7
8use air::PartitionOptions;
9use crypto::{ElementHasher, VectorCommitment};
10use math::{fft, FieldElement, StarkField};
11#[cfg(feature = "concurrent")]
12use utils::iterators::*;
13use utils::{batch_iter_mut, flatten_vector_elements, uninit_vector};
14
15use super::{ColMatrix, Segment};
16use crate::StarkDomain;
17
18// ROW-MAJOR MATRIX
19// ================================================================================================
20
21/// A two-dimensional matrix of field elements arranged in row-major order.
22///
23/// The matrix is represented as a single vector of base field elements for the field defined by E
24/// type parameter. The first `row_width` base field elements represent the first row of the matrix,
25/// the next `row_width` base field elements represent the second row, and so on.
26///
27/// When rows are returned via the [RowMatrix::row()] method, base field elements are grouped
28/// together as appropriate to form elements in E.
29///
30/// In some cases, rows may be padded with extra elements. The number of elements which are
31/// accessible via the [RowMatrix::row()] method is specified by the `elements_per_row` member.
32#[derive(Clone, Debug)]
33pub struct RowMatrix<E: FieldElement> {
34    /// Field elements stored in the matrix.
35    data: Vec<E::BaseField>,
36    /// Total number of base field elements stored in a single row.
37    row_width: usize,
38    /// Number of field elements in a single row accessible via the [RowMatrix::row()] method. This
39    /// must be equal to or smaller than `row_width`.
40    elements_per_row: usize,
41}
42
43impl<E: FieldElement> RowMatrix<E> {
44    // CONSTRUCTORS
45    // --------------------------------------------------------------------------------------------
46
47    /// Returns a new [RowMatrix] constructed by evaluating the provided polynomials over the
48    /// domain defined by the specified blowup factor.
49    ///
50    /// The provided `polys` matrix is assumed to contain polynomials in coefficient form (one
51    /// polynomial per column). Columns in the returned matrix will contain evaluations of the
52    /// corresponding polynomials over the domain defined by polynomial size (i.e., number of rows
53    /// in the `polys` matrix) and the `blowup_factor`.
54    ///
55    /// To improve performance, polynomials are evaluated in batches specified by the `N` type
56    /// parameter. Minimum batch size is 1.
57    pub fn evaluate_polys<const N: usize>(polys: &ColMatrix<E>, blowup_factor: usize) -> Self {
58        assert!(N > 0, "batch size N must be greater than zero");
59
60        // pre-compute offsets for each row
61        let poly_size = polys.num_rows();
62        let offsets =
63            get_evaluation_offsets::<E>(poly_size, blowup_factor, E::BaseField::GENERATOR);
64
65        // compute twiddles for polynomial evaluation
66        let twiddles = fft::get_twiddles::<E::BaseField>(polys.num_rows());
67
68        // build matrix segments by evaluating all polynomials
69        let segments = build_segments::<E, N>(polys, &twiddles, &offsets);
70
71        // transpose data in individual segments into a single row-major matrix
72        Self::from_segments(segments, polys.num_base_cols())
73    }
74
75    /// Returns a new [RowMatrix] constructed by evaluating the provided polynomials over the
76    /// specified [StarkDomain].
77    ///
78    /// The provided `polys` matrix is assumed to contain polynomials in coefficient form (one
79    /// polynomial per column). Columns in the returned matrix will contain evaluations of the
80    /// corresponding polynomials over the LDE domain defined by the provided [StarkDomain].
81    ///
82    /// To improve performance, polynomials are evaluated in batches specified by the `N` type
83    /// parameter. Minimum batch size is 1.
84    pub fn evaluate_polys_over<const N: usize>(
85        polys: &ColMatrix<E>,
86        domain: &StarkDomain<E::BaseField>,
87    ) -> Self {
88        assert!(N > 0, "batch size N must be greater than zero");
89
90        // pre-compute offsets for each row
91        let poly_size = polys.num_rows();
92        let offsets =
93            get_evaluation_offsets::<E>(poly_size, domain.trace_to_lde_blowup(), domain.offset());
94
95        // build matrix segments by evaluating all polynomials
96        let segments = build_segments::<E, N>(polys, domain.trace_twiddles(), &offsets);
97
98        // transpose data in individual segments into a single row-major matrix
99        Self::from_segments(segments, polys.num_base_cols())
100    }
101
102    /// Returns a new [RowMatrix] instantiated from the specified matrix segments.
103    ///
104    /// `elements_per_row` specifies how many base field elements are considered to form a single
105    /// row in the matrix.
106    ///
107    /// # Panics
108    /// Panics if
109    /// - `segments` is an empty vector.
110    /// - `elements_per_row` is greater than the row width implied by the number of segments and `N`
111    ///   type parameter.
112    pub fn from_segments<const N: usize>(
113        segments: Vec<Segment<E::BaseField, N>>,
114        elements_per_row: usize,
115    ) -> Self {
116        assert!(N > 0, "batch size N must be greater than zero");
117        assert!(!segments.is_empty(), "a list of segments cannot be empty");
118
119        // compute the size of each row
120        let row_width = segments.len() * N;
121        assert!(
122            elements_per_row <= row_width,
123            "elements per row cannot exceed {row_width}, but was {elements_per_row}"
124        );
125
126        // transpose the segments into a single vector of arrays
127        let result = transpose(segments);
128
129        // flatten the result to be a simple vector of elements and return
130        RowMatrix {
131            data: flatten_vector_elements(result),
132            row_width,
133            elements_per_row,
134        }
135    }
136
137    // PUBLIC ACCESSORS
138    // --------------------------------------------------------------------------------------------
139
140    /// Returns the number of columns in this matrix.
141    pub fn num_cols(&self) -> usize {
142        self.elements_per_row / E::EXTENSION_DEGREE
143    }
144
145    /// Returns the number of rows in this matrix.
146    pub fn num_rows(&self) -> usize {
147        self.data.len() / self.row_width
148    }
149
150    /// Returns the element located at the specified column and row indexes in this matrix.
151    ///
152    /// # Panics
153    /// Panics if either `col_idx` or `row_idx` are out of bounds for this matrix.
154    pub fn get(&self, col_idx: usize, row_idx: usize) -> E {
155        self.row(row_idx)[col_idx]
156    }
157
158    /// Returns a reference to a row at the specified index in this matrix.
159    ///
160    /// # Panics
161    /// Panics if the specified row index is out of bounds.
162    pub fn row(&self, row_idx: usize) -> &[E] {
163        assert!(row_idx < self.num_rows());
164        let start = row_idx * self.row_width;
165        E::slice_from_base_elements(&self.data[start..start + self.elements_per_row])
166    }
167
168    /// Returns the data in this matrix as a slice of field elements.
169    pub fn data(&self) -> &[E::BaseField] {
170        &self.data
171    }
172
173    // COMMITMENTS
174    // --------------------------------------------------------------------------------------------
175
176    /// Returns a commitment to this matrix.
177    ///
178    /// The commitment is built as follows:
179    /// * Each row of the matrix is hashed into a single digest of the specified hash function. The
180    ///   result is a vector of digests of length equal to the number of matrix rows.
181    /// * A vector commitment is computed for the resulting vector using the specified vector
182    ///   commitment scheme.
183    /// * The resulting vector commitment is returned as the commitment to the entire matrix.
184    pub fn commit_to_rows<H, V>(&self, partition_options: PartitionOptions) -> V
185    where
186        H: ElementHasher<BaseField = E::BaseField>,
187        V: VectorCommitment<H>,
188    {
189        // allocate vector to store row hashes
190        let mut row_hashes = unsafe { uninit_vector::<H::Digest>(self.num_rows()) };
191        let partition_size = partition_options.partition_size::<E>(self.num_cols());
192
193        if partition_size == self.num_cols() {
194            // iterate though matrix rows, hashing each row
195            batch_iter_mut!(
196                &mut row_hashes,
197                128, // min batch size
198                |batch: &mut [H::Digest], batch_offset: usize| {
199                    for (i, row_hash) in batch.iter_mut().enumerate() {
200                        *row_hash = H::hash_elements(self.row(batch_offset + i));
201                    }
202                }
203            );
204        } else {
205            let num_partitions = partition_options.num_partitions::<E>(self.num_cols());
206
207            // iterate though matrix rows, hashing each row
208            batch_iter_mut!(
209                &mut row_hashes,
210                128, // min batch size
211                |batch: &mut [H::Digest], batch_offset: usize| {
212                    let mut buffer = vec![H::Digest::default(); num_partitions];
213                    for (i, row_hash) in batch.iter_mut().enumerate() {
214                        self.row(batch_offset + i)
215                            .chunks(partition_size)
216                            .zip(buffer.iter_mut())
217                            .for_each(|(chunk, buf)| {
218                                *buf = H::hash_elements(chunk);
219                            });
220                        *row_hash = H::merge_many(&buffer);
221                    }
222                }
223            );
224        }
225
226        // build the vector commitment to the hashed rows
227        V::new(row_hashes).expect("failed to construct trace vector commitment")
228    }
229}
230
231// HELPER FUNCTIONS
232// ================================================================================================
233
234/// Returns a vector of offsets for an evaluation defined by the specified polynomial size, blowup
235/// factor and domain offset.
236///
237/// When `concurrent` feature is enabled, offsets are computed in multiple threads.
238pub fn get_evaluation_offsets<E: FieldElement>(
239    poly_size: usize,
240    blowup_factor: usize,
241    domain_offset: E::BaseField,
242) -> Vec<E::BaseField> {
243    let domain_size = poly_size * blowup_factor;
244    let g = E::BaseField::get_root_of_unity(domain_size.ilog2());
245
246    // allocate memory to hold the offsets
247    let mut offsets = unsafe { uninit_vector(domain_size) };
248
249    // define a closure to compute offsets for a given chunk of the result; the number of chunks
250    // is defined by the blowup factor. for example, for blowup factor = 2, the number of chunks
251    // will be 2, for blowup factor = 8, the number of chunks will be 8 etc.
252    let compute_offsets = |(chunk_idx, chunk): (usize, &mut [E::BaseField])| {
253        let idx = fft::permute_index(blowup_factor, chunk_idx) as u64;
254        let offset = g.exp_vartime(idx.into()) * domain_offset;
255        let mut factor = E::BaseField::ONE;
256        for res in chunk.iter_mut() {
257            *res = factor;
258            factor *= offset;
259        }
260    };
261
262    // compute offsets for each chunk using either parallel or regular iterators
263
264    #[cfg(not(feature = "concurrent"))]
265    offsets.chunks_mut(poly_size).enumerate().for_each(compute_offsets);
266
267    #[cfg(feature = "concurrent")]
268    offsets.par_chunks_mut(poly_size).enumerate().for_each(compute_offsets);
269
270    offsets
271}
272
273/// Returns matrix segments constructed by evaluating polynomials in the specified matrix over the
274/// domain defined by twiddles and offsets.
275pub fn build_segments<E: FieldElement, const N: usize>(
276    polys: &ColMatrix<E>,
277    twiddles: &[E::BaseField],
278    offsets: &[E::BaseField],
279) -> Vec<Segment<E::BaseField, N>> {
280    assert!(N > 0, "batch size N must be greater than zero");
281    debug_assert_eq!(polys.num_rows(), twiddles.len() * 2);
282    debug_assert_eq!(offsets.len() % polys.num_rows(), 0);
283
284    let num_segments = if polys.num_base_cols().is_multiple_of(N) {
285        polys.num_base_cols() / N
286    } else {
287        polys.num_base_cols() / N + 1
288    };
289
290    (0..num_segments)
291        .map(|i| Segment::new(polys, i * N, offsets, twiddles))
292        .collect()
293}
294
295/// Transposes a vector of segments into a single vector of fixed-size arrays.
296///
297/// When `concurrent` feature is enabled, transposition is performed in multiple threads.
298fn transpose<B: StarkField, const N: usize>(mut segments: Vec<Segment<B, N>>) -> Vec<[B; N]> {
299    let num_rows = segments[0].num_rows();
300    let num_segs = segments.len();
301    let result_len = num_rows * num_segs;
302
303    // if there is only one segment, there is nothing to transpose as it is already in row
304    // major form
305    if segments.len() == 1 {
306        return segments.remove(0).into_data();
307    }
308
309    // allocate memory to hold the transposed result;
310    // TODO: investigate transposing in-place
311    let mut result = unsafe { uninit_vector::<[B; N]>(result_len) };
312
313    // determine number of batches in which transposition will be preformed; if `concurrent`
314    // feature is not enabled, the number of batches will always be 1
315    let num_batches = get_num_batches(result_len);
316    let rows_per_batch = num_rows / num_batches;
317
318    // define a closure for transposing a given batch
319    let transpose_batch = |(batch_idx, batch): (usize, &mut [[B; N]])| {
320        let row_offset = batch_idx * rows_per_batch;
321        for i in 0..rows_per_batch {
322            let row_idx = i + row_offset;
323            for j in 0..num_segs {
324                let v = &segments[j][row_idx];
325                batch[i * num_segs + j].copy_from_slice(v);
326            }
327        }
328    };
329
330    // call the closure either once (for single-threaded transposition) or in a parallel
331    // iterator (for multi-threaded transposition)
332
333    #[cfg(not(feature = "concurrent"))]
334    transpose_batch((0, &mut result));
335
336    #[cfg(feature = "concurrent")]
337    result
338        .par_chunks_mut(result_len / num_batches)
339        .enumerate()
340        .for_each(transpose_batch);
341
342    result
343}
344
345#[cfg(not(feature = "concurrent"))]
346fn get_num_batches(_input_size: usize) -> usize {
347    1
348}
349
350#[cfg(feature = "concurrent")]
351fn get_num_batches(input_size: usize) -> usize {
352    if input_size < 1024 {
353        return 1;
354    }
355    utils::rayon::current_num_threads().next_power_of_two() * 2
356}