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}