Skip to main content

p3_field/
vectorized.rs

1//! Lockstep evaluation over multiple packed vectors, trading register pressure
2//! for instruction-level parallelism in latency-bound field arithmetic.
3//!
4//! Inspired by stwo's `Vectorized` type:
5//! <https://github.com/starkware-libs/stwo/blob/cca98119f/crates/stwo/src/prover/backend/simd/very_packed_m31.rs>
6
7use core::array;
8use core::iter::{Product, Sum};
9use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign};
10
11use crate::{
12    Algebra, BasedVectorSpace, ExtensionField, Field, PackedFieldExtension, PackedValue,
13    PrimeCharacteristicRing,
14};
15
16/// `N` packed base-field vectors operated on in lockstep.
17///
18/// A single packed vector often cannot saturate the CPU's multiplier pipes: a
19/// dependency chain of packed multiplications leaves most issue slots idle
20/// (e.g. NEON's modular multiply has ~10 cycles of latency at ~1.25 cycles of
21/// throughput per vector). Widening the *data type* rather than the loop turns
22/// every ring operation into `N` independent instructions back to back, giving
23/// the out-of-order core `N` interleaved dependency chains without changing the
24/// shape of the expression being evaluated.
25///
26/// Lane `i` of the logical vector lives in `self.0[i / W].as_slice()[i % W]`
27/// where `W = F::Packing::WIDTH`, i.e. components hold consecutive blocks of
28/// lanes.
29#[derive(Clone, Copy, Debug)]
30#[repr(transparent)]
31#[must_use]
32pub struct Vectorized<F: Field, const N: usize>(pub [F::Packing; N]);
33
34/// `N` packed extension-field vectors operated on in lockstep.
35///
36/// The extension-field counterpart of [`Vectorized`]: lane `i` corresponds to
37/// lane `i` of a `Vectorized<F, N>` operand, so the two types can be mixed in
38/// the same expression (`VectorizedExt * Vectorized`, etc.).
39#[derive(Clone, Copy, Debug)]
40#[repr(transparent)]
41#[must_use]
42pub struct VectorizedExt<F: Field, EF: ExtensionField<F>, const N: usize>(
43    pub [EF::ExtensionPacking; N],
44);
45
46impl<F: Field, const N: usize> Vectorized<F, N> {
47    /// Map a function over the `N` packed components.
48    #[inline]
49    fn map(self, f: impl FnMut(F::Packing) -> F::Packing) -> Self {
50        Self(self.0.map(f))
51    }
52
53    /// Combine two values component-wise.
54    #[inline]
55    fn zip_with<T: Copy>(
56        self,
57        rhs: &[T; N],
58        mut f: impl FnMut(F::Packing, T) -> F::Packing,
59    ) -> Self {
60        Self(array::from_fn(|i| f(self.0[i], rhs[i])))
61    }
62}
63
64impl<F: Field, EF: ExtensionField<F>, const N: usize> VectorizedExt<F, EF, N> {
65    /// Map a function over the `N` packed components.
66    #[inline]
67    fn map(self, f: impl FnMut(EF::ExtensionPacking) -> EF::ExtensionPacking) -> Self {
68        Self(self.0.map(f))
69    }
70
71    /// Combine two values component-wise.
72    #[inline]
73    fn zip_with<T: Copy>(
74        self,
75        rhs: &[T; N],
76        mut f: impl FnMut(EF::ExtensionPacking, T) -> EF::ExtensionPacking,
77    ) -> Self {
78        Self(array::from_fn(|i| f(self.0[i], rhs[i])))
79    }
80
81    /// Build from basis coefficients, where coefficient `d` is the vectorized
82    /// base-field value `coefficients[d]`.
83    ///
84    /// This is the vectorized analogue of
85    /// [`BasedVectorSpace::from_basis_coefficients_fn`]; it takes a slice
86    /// rather than a closure so each coefficient is computed once and shared
87    /// by all `N` components.
88    #[inline]
89    pub fn from_vectorized_basis_coefficients(coefficients: &[Vectorized<F, N>]) -> Self {
90        debug_assert_eq!(coefficients.len(), EF::DIMENSION);
91        Self(array::from_fn(|i| {
92            EF::ExtensionPacking::from_basis_coefficients_fn(|d| coefficients[d].0[i])
93        }))
94    }
95
96    /// Extract the extension-field element at logical lane `i`.
97    ///
98    /// Lanes `0..F::Packing::WIDTH` come from component `0`, the next
99    /// `F::Packing::WIDTH` from component `1`, and so on.
100    #[inline]
101    pub fn extract(&self, i: usize) -> EF {
102        let width = F::Packing::WIDTH;
103        self.0[i / width].extract(i % width)
104    }
105}
106
107impl<F: Field, const N: usize> Default for Vectorized<F, N> {
108    #[inline]
109    fn default() -> Self {
110        Self::ZERO
111    }
112}
113
114impl<F: Field, EF: ExtensionField<F>, const N: usize> Default for VectorizedExt<F, EF, N> {
115    #[inline]
116    fn default() -> Self {
117        Self::ZERO
118    }
119}
120
121impl<F: Field, const N: usize> From<F> for Vectorized<F, N> {
122    #[inline]
123    fn from(value: F) -> Self {
124        Self([F::Packing::from(value); N])
125    }
126}
127
128impl<F: Field, EF: ExtensionField<F>, const N: usize> From<EF> for VectorizedExt<F, EF, N> {
129    #[inline]
130    fn from(value: EF) -> Self {
131        Self([EF::ExtensionPacking::from(value); N])
132    }
133}
134
135impl<F: Field, EF: ExtensionField<F>, const N: usize> From<Vectorized<F, N>>
136    for VectorizedExt<F, EF, N>
137{
138    #[inline]
139    fn from(value: Vectorized<F, N>) -> Self {
140        Self(value.0.map(EF::ExtensionPacking::from))
141    }
142}
143
144macro_rules! impl_binary_ops {
145    ($ty:ty, $($bound:tt)*) => {
146        impl<$($bound)*> Add for $ty {
147            type Output = Self;
148            #[inline]
149            fn add(self, rhs: Self) -> Self {
150                self.zip_with(&rhs.0, |x, y| x + y)
151            }
152        }
153
154        impl<$($bound)*> Sub for $ty {
155            type Output = Self;
156            #[inline]
157            fn sub(self, rhs: Self) -> Self {
158                self.zip_with(&rhs.0, |x, y| x - y)
159            }
160        }
161
162        impl<$($bound)*> Mul for $ty {
163            type Output = Self;
164            #[inline]
165            fn mul(self, rhs: Self) -> Self {
166                self.zip_with(&rhs.0, |x, y| x * y)
167            }
168        }
169
170        impl<$($bound)*> Neg for $ty {
171            type Output = Self;
172            #[inline]
173            fn neg(self) -> Self {
174                self.map(|x| -x)
175            }
176        }
177
178        impl<$($bound)*> AddAssign for $ty {
179            #[inline]
180            fn add_assign(&mut self, rhs: Self) {
181                *self = *self + rhs;
182            }
183        }
184
185        impl<$($bound)*> SubAssign for $ty {
186            #[inline]
187            fn sub_assign(&mut self, rhs: Self) {
188                *self = *self - rhs;
189            }
190        }
191
192        impl<$($bound)*> MulAssign for $ty {
193            #[inline]
194            fn mul_assign(&mut self, rhs: Self) {
195                *self = *self * rhs;
196            }
197        }
198
199        impl<$($bound)*> Sum for $ty {
200            #[inline]
201            fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
202                iter.fold(Self::ZERO, |acc, x| acc + x)
203            }
204        }
205
206        impl<$($bound)*> Product for $ty {
207            #[inline]
208            fn product<I: Iterator<Item = Self>>(iter: I) -> Self {
209                iter.fold(Self::ONE, |acc, x| acc * x)
210            }
211        }
212    };
213}
214
215macro_rules! impl_scalar_ops {
216    ($ty:ty, $scalar:ty, $($bound:tt)*) => {
217        impl<$($bound)*> Add<$scalar> for $ty {
218            type Output = Self;
219            #[inline]
220            fn add(self, rhs: $scalar) -> Self {
221                self.map(|x| x + rhs)
222            }
223        }
224
225        impl<$($bound)*> Sub<$scalar> for $ty {
226            type Output = Self;
227            #[inline]
228            fn sub(self, rhs: $scalar) -> Self {
229                self.map(|x| x - rhs)
230            }
231        }
232
233        impl<$($bound)*> Mul<$scalar> for $ty {
234            type Output = Self;
235            #[inline]
236            fn mul(self, rhs: $scalar) -> Self {
237                self.map(|x| x * rhs)
238            }
239        }
240
241        impl<$($bound)*> AddAssign<$scalar> for $ty {
242            #[inline]
243            fn add_assign(&mut self, rhs: $scalar) {
244                *self = *self + rhs;
245            }
246        }
247
248        impl<$($bound)*> SubAssign<$scalar> for $ty {
249            #[inline]
250            fn sub_assign(&mut self, rhs: $scalar) {
251                *self = *self - rhs;
252            }
253        }
254
255        impl<$($bound)*> MulAssign<$scalar> for $ty {
256            #[inline]
257            fn mul_assign(&mut self, rhs: $scalar) {
258                *self = *self * rhs;
259            }
260        }
261    };
262}
263
264impl_binary_ops!(Vectorized<F, N>, F: Field, const N: usize);
265impl_scalar_ops!(Vectorized<F, N>, F, F: Field, const N: usize);
266impl_binary_ops!(VectorizedExt<F, EF, N>, F: Field, EF: ExtensionField<F>, const N: usize);
267impl_scalar_ops!(VectorizedExt<F, EF, N>, EF, F: Field, EF: ExtensionField<F>, const N: usize);
268
269impl<F: Field, EF: ExtensionField<F>, const N: usize> Add<Vectorized<F, N>>
270    for VectorizedExt<F, EF, N>
271{
272    type Output = Self;
273    #[inline]
274    fn add(self, rhs: Vectorized<F, N>) -> Self {
275        self.zip_with(&rhs.0, |x, y| x + y)
276    }
277}
278
279impl<F: Field, EF: ExtensionField<F>, const N: usize> Sub<Vectorized<F, N>>
280    for VectorizedExt<F, EF, N>
281{
282    type Output = Self;
283    #[inline]
284    fn sub(self, rhs: Vectorized<F, N>) -> Self {
285        self.zip_with(&rhs.0, |x, y| x - y)
286    }
287}
288
289impl<F: Field, EF: ExtensionField<F>, const N: usize> Mul<Vectorized<F, N>>
290    for VectorizedExt<F, EF, N>
291{
292    type Output = Self;
293    #[inline]
294    fn mul(self, rhs: Vectorized<F, N>) -> Self {
295        self.zip_with(&rhs.0, |x, y| x * y)
296    }
297}
298
299impl<F: Field, EF: ExtensionField<F>, const N: usize> AddAssign<Vectorized<F, N>>
300    for VectorizedExt<F, EF, N>
301{
302    #[inline]
303    fn add_assign(&mut self, rhs: Vectorized<F, N>) {
304        *self = *self + rhs;
305    }
306}
307
308impl<F: Field, EF: ExtensionField<F>, const N: usize> SubAssign<Vectorized<F, N>>
309    for VectorizedExt<F, EF, N>
310{
311    #[inline]
312    fn sub_assign(&mut self, rhs: Vectorized<F, N>) {
313        *self = *self - rhs;
314    }
315}
316
317impl<F: Field, EF: ExtensionField<F>, const N: usize> MulAssign<Vectorized<F, N>>
318    for VectorizedExt<F, EF, N>
319{
320    #[inline]
321    fn mul_assign(&mut self, rhs: Vectorized<F, N>) {
322        *self = *self * rhs;
323    }
324}
325
326impl<F: Field, const N: usize> PrimeCharacteristicRing for Vectorized<F, N> {
327    type PrimeSubfield = F::PrimeSubfield;
328
329    const ZERO: Self = Self([F::Packing::ZERO; N]);
330    const ONE: Self = Self([F::Packing::ONE; N]);
331    const TWO: Self = Self([F::Packing::TWO; N]);
332    const NEG_ONE: Self = Self([F::Packing::NEG_ONE; N]);
333
334    #[inline]
335    fn from_prime_subfield(f: Self::PrimeSubfield) -> Self {
336        F::from_prime_subfield(f).into()
337    }
338
339    #[inline]
340    fn double(&self) -> Self {
341        self.map(|x| x.double())
342    }
343
344    #[inline]
345    fn halve(&self) -> Self {
346        self.map(|x| x.halve())
347    }
348
349    #[inline]
350    fn square(&self) -> Self {
351        self.map(|x| x.square())
352    }
353
354    #[inline]
355    fn cube(&self) -> Self {
356        self.map(|x| x.cube())
357    }
358
359    #[inline]
360    fn exp_const_u64<const POWER: u64>(&self) -> Self {
361        self.map(|x| x.exp_const_u64::<POWER>())
362    }
363
364    #[inline]
365    fn mul_2exp_u64(&self, exp: u64) -> Self {
366        self.map(|x| x.mul_2exp_u64(exp))
367    }
368
369    #[inline]
370    fn dot_product<const M: usize>(u: &[Self; M], v: &[Self; M]) -> Self {
371        Self(array::from_fn(|i| {
372            F::Packing::dot_product::<M>(
373                &array::from_fn::<_, M, _>(|j| u[j].0[i]),
374                &array::from_fn::<_, M, _>(|j| v[j].0[i]),
375            )
376        }))
377    }
378
379    #[inline]
380    fn sum_array<const M: usize>(input: &[Self]) -> Self {
381        assert_eq!(input.len(), M);
382        Self(array::from_fn(|i| {
383            F::Packing::sum_array::<M>(&array::from_fn::<_, M, _>(|j| input[j].0[i]))
384        }))
385    }
386}
387
388impl<F: Field, EF: ExtensionField<F>, const N: usize> PrimeCharacteristicRing
389    for VectorizedExt<F, EF, N>
390{
391    type PrimeSubfield = <EF::ExtensionPacking as PrimeCharacteristicRing>::PrimeSubfield;
392
393    const ZERO: Self = Self([EF::ExtensionPacking::ZERO; N]);
394    const ONE: Self = Self([EF::ExtensionPacking::ONE; N]);
395    const TWO: Self = Self([EF::ExtensionPacking::TWO; N]);
396    const NEG_ONE: Self = Self([EF::ExtensionPacking::NEG_ONE; N]);
397
398    #[inline]
399    fn from_prime_subfield(f: Self::PrimeSubfield) -> Self {
400        Self([EF::ExtensionPacking::from_prime_subfield(f); N])
401    }
402
403    #[inline]
404    fn double(&self) -> Self {
405        self.map(|x| x.double())
406    }
407
408    #[inline]
409    fn halve(&self) -> Self {
410        self.map(|x| x.halve())
411    }
412
413    #[inline]
414    fn square(&self) -> Self {
415        self.map(|x| x.square())
416    }
417
418    #[inline]
419    fn cube(&self) -> Self {
420        self.map(|x| x.cube())
421    }
422
423    #[inline]
424    fn exp_const_u64<const POWER: u64>(&self) -> Self {
425        self.map(|x| x.exp_const_u64::<POWER>())
426    }
427
428    #[inline]
429    fn mul_2exp_u64(&self, exp: u64) -> Self {
430        self.map(|x| x.mul_2exp_u64(exp))
431    }
432
433    #[inline]
434    fn dot_product<const M: usize>(u: &[Self; M], v: &[Self; M]) -> Self {
435        Self(array::from_fn(|i| {
436            EF::ExtensionPacking::dot_product::<M>(
437                &array::from_fn::<_, M, _>(|j| u[j].0[i]),
438                &array::from_fn::<_, M, _>(|j| v[j].0[i]),
439            )
440        }))
441    }
442
443    #[inline]
444    fn sum_array<const M: usize>(input: &[Self]) -> Self {
445        assert_eq!(input.len(), M);
446        Self(array::from_fn(|i| {
447            EF::ExtensionPacking::sum_array::<M>(&array::from_fn::<_, M, _>(|j| input[j].0[i]))
448        }))
449    }
450}
451
452impl<F: Field, const N: usize> Algebra<F> for Vectorized<F, N> {
453    const BATCHED_LC_CHUNK: usize = <F::Packing as Algebra<F>>::BATCHED_LC_CHUNK;
454
455    #[inline]
456    fn mixed_dot_product<const M: usize>(a: &[Self; M], f: &[F; M]) -> Self {
457        Self(array::from_fn(|i| {
458            F::Packing::mixed_dot_product(&array::from_fn(|j| a[j].0[i]), f)
459        }))
460    }
461}
462
463impl<F: Field, EF: ExtensionField<F>, const N: usize> Algebra<EF> for VectorizedExt<F, EF, N> {
464    const BATCHED_LC_CHUNK: usize = <EF::ExtensionPacking as Algebra<EF>>::BATCHED_LC_CHUNK;
465
466    #[inline]
467    fn mixed_dot_product<const M: usize>(a: &[Self; M], f: &[EF; M]) -> Self {
468        Self(array::from_fn(|i| {
469            EF::ExtensionPacking::mixed_dot_product(&array::from_fn(|j| a[j].0[i]), f)
470        }))
471    }
472}
473
474impl<F: Field, EF: ExtensionField<F>, const N: usize> Algebra<Vectorized<F, N>>
475    for VectorizedExt<F, EF, N>
476{
477}
478
479// SAFETY: `Vectorized<F, N>` is `repr(transparent)` over `[F::Packing; N]` and
480// `F::Packing: PackedField` guarantees that `F::Packing` can be cast to/from
481// `[F; F::Packing::WIDTH]` without UB. Hence `Vectorized<F, N>` can be cast
482// to/from `[F; F::Packing::WIDTH * N]`. `from_slice`/`from_slice_mut` additionally
483// rely on `align_of::<F::Packing>() <= align_of::<F>()`, the same invariant
484// `PackedValue::pack_slice` asserts, to cast a `&[F]` pointer to `&Self` without
485// under-aligning the read.
486unsafe impl<F: Field, const N: usize> PackedValue for Vectorized<F, N> {
487    type Value = F;
488
489    const WIDTH: usize = F::Packing::WIDTH * N;
490
491    #[inline]
492    fn from_fn<Fn>(mut f: Fn) -> Self
493    where
494        Fn: FnMut(usize) -> Self::Value,
495    {
496        Self(array::from_fn(|i| {
497            F::Packing::from_fn(|j| f(i * F::Packing::WIDTH + j))
498        }))
499    }
500
501    #[inline]
502    fn from_slice(slice: &[Self::Value]) -> &Self {
503        assert_eq!(slice.len(), Self::WIDTH);
504        unsafe { &*slice.as_ptr().cast() }
505    }
506
507    #[inline]
508    fn from_slice_mut(slice: &mut [Self::Value]) -> &mut Self {
509        assert_eq!(slice.len(), Self::WIDTH);
510        unsafe { &mut *slice.as_mut_ptr().cast() }
511    }
512
513    #[inline]
514    fn as_slice(&self) -> &[Self::Value] {
515        unsafe { core::slice::from_raw_parts(core::ptr::from_ref(self).cast(), Self::WIDTH) }
516    }
517
518    #[inline]
519    fn as_slice_mut(&mut self) -> &mut [Self::Value] {
520        unsafe { core::slice::from_raw_parts_mut(core::ptr::from_mut(self).cast(), Self::WIDTH) }
521    }
522}