p3_field/batch_inverse.rs
1use alloc::vec::Vec;
2
3use p3_maybe_rayon::prelude::*;
4use tracing::instrument;
5
6use crate::field::Field;
7use crate::{
8 ExtensionField, FieldArray, PackedFieldExtension, PackedValue, PrimeCharacteristicRing,
9};
10
11/// Compute the multiplicative inverse of every element in a slice via Montgomery's trick.
12///
13/// Replaces `n` field inversions with one inversion plus `~3n` multiplications:
14/// - forward pass: build prefix products of the inputs,
15/// - one inversion of the full product,
16/// - reverse pass: derive each individual inverse from the prefix products.
17///
18/// The forward pass is a long dependency chain. It is parallelised on two axes:
19/// - 4-lane packed arrays — four independent chains run side by side,
20/// - 1024-element chunks — dispatched across Rayon workers.
21///
22/// Lengths not a multiple of 4 finish with a scalar pass on the trailing 1..=3 elements.
23///
24/// # Panics
25///
26/// Panics if any input is zero.
27#[instrument(level = "debug", skip_all)]
28#[must_use]
29pub fn batch_multiplicative_inverse<F: Field>(x: &[F]) -> Vec<F> {
30 // 1024-element chunks per Rayon task.
31 //
32 // Why 1024:
33 // - amortizes the one field inversion per chunk over many multiplies,
34 // - leaves enough chunks for work-stealing on long slices.
35 const CHUNK_SIZE: usize = 1024;
36
37 // 4-lane packing.
38 //
39 // Why 4:
40 // - smallest packed-field width on every backend,
41 // - wider lanes risk register spills in the per-lane dependency chains.
42 const WIDTH: usize = 4;
43
44 // Pre-allocate the output: each Rayon task writes a disjoint sub-slice.
45 let mut result = F::zero_vec(x.len());
46
47 // One item is a whole chunk of Montgomery steps, not a chunk of reads.
48 //
49 // Per element the three multiplies and the amortized inversion dominate the move.
50 //
51 // So the chunk is priced by the time it takes, in multiples of what it would cost to read.
52 //
53 // A wider field is charged more because its multiply costs more, not because it moves more.
54 // Measured on Zen 5 with `-C target-cpu=native`, per element, as a multiple of one read:
55 //
56 // BabyBear 4 B 2.3 ns 5.8 reads
57 // BabyBear quartic 16 B 8.4 ns 5.2
58 // BabyBear quintic 20 B 15.9 ns 7.9
59 // Goldilocks 8 B 1.5 ns 1.8
60 // Goldilocks quadratic 16 B 8.1 ns 5.1
61 // Ghash128 16 B 6.7 ns 4.2
62 //
63 // Five reads slightly overcharges Goldilocks and can split it one size early.
64 // It stays within a factor of 1.6 of the other measured rows.
65 // A build without the wide carryless multiply puts the binary field at 44 reads instead,
66 // so the residual error is an undercharge, which only ever leaves a loop whole.
67 x.par_chunks(CHUNK_SIZE)
68 .zip(result.par_chunks_mut(CHUNK_SIZE))
69 .with_min_task_bytes(5 * CHUNK_SIZE * size_of::<F>())
70 .for_each(|(x_chunk, result_chunk)| {
71 // Phase 1 — split the chunk:
72 // - packed: 4-aligned prefix viewed as 4-lane arrays,
73 // - tail: 0..=3 trailing scalars (m = n - n%4).
74 //
75 // x_chunk: [ x_0 .. x_{m-1} | x_m .. x_{n-1} ]
76 // └──── packed ────┘└──── tail ────┘
77 let (x_packed, x_tail) = FieldArray::<F, WIDTH>::pack_slice_with_suffix(x_chunk);
78 let (result_packed, result_tail) =
79 FieldArray::<F, WIDTH>::pack_slice_with_suffix_mut(result_chunk);
80
81 // Phase 2 — packed pass: 4 independent Montgomery chains, one per lane.
82 //
83 // Final inversion lands on a 4-lane array → one scalar inversion per chunk.
84 batch_multiplicative_inverse_general(x_packed, result_packed, |y| y.inverse());
85
86 // Phase 3 — tail pass: 0..=3 leftover scalars.
87 //
88 // Empty when n % 4 == 0; this call then returns immediately.
89 batch_multiplicative_inverse_general(x_tail, result_tail, |y| y.inverse());
90 });
91
92 result
93}
94
95/// A simple single-threaded implementation of Montgomery's trick. Since not all `PrimeCharacteristicRing`s
96/// support inversion, this takes a custom inversion function.
97///
98/// Unlike [`batch_multiplicative_inverse`], this writes into a caller-provided buffer,
99/// avoiding heap allocation. This makes it suitable for small, fixed-size inputs
100/// such as packed field lanes.
101#[inline]
102pub fn batch_multiplicative_inverse_general<F, Inv>(x: &[F], result: &mut [F], inv: Inv)
103where
104 F: PrimeCharacteristicRing + Copy,
105 Inv: Fn(F) -> F,
106{
107 let n = x.len();
108 assert_eq!(result.len(), n);
109 if n == 0 {
110 return;
111 }
112
113 result[0] = F::ONE;
114 for i in 1..n {
115 result[i] = result[i - 1] * x[i - 1];
116 }
117
118 let product = result[n - 1] * x[n - 1];
119 let mut inv = inv(product);
120
121 for i in (0..n).rev() {
122 result[i] *= inv;
123 inv *= x[i];
124 }
125}
126
127/// Per-lane inverse of a packed extension via Montgomery's trick. Allocation-free.
128///
129/// Dispatches on `F::Packing::WIDTH` to a const-generic body that materializes the `W`
130/// lanes via [`PackedFieldExtension::extract`], runs [`batch_multiplicative_inverse_general`]
131/// over a stack-sized `[EF; W]` buffer, and rebuilds the packed extension via
132/// [`PackedFieldExtension::from_ext_fn`]. After monomorphization the match folds to
133/// the single live arm.
134///
135/// All `PackedField` backends in this workspace use `WIDTH ∈ {1, 2, 4, 8, 16}`; the
136/// fallback arm panics if a future backend introduces a different width.
137#[inline]
138pub fn invert_packed_extension<F, EF>(packed: EF::ExtensionPacking) -> EF::ExtensionPacking
139where
140 F: Field,
141 EF: ExtensionField<F>,
142{
143 match F::Packing::WIDTH {
144 1 => invert_packed_extension_const::<F, EF, 1>(packed),
145 2 => invert_packed_extension_const::<F, EF, 2>(packed),
146 4 => invert_packed_extension_const::<F, EF, 4>(packed),
147 8 => invert_packed_extension_const::<F, EF, 8>(packed),
148 16 => invert_packed_extension_const::<F, EF, 16>(packed),
149 w => panic!("unsupported PackedField WIDTH = {w}"),
150 }
151}
152
153#[inline]
154fn invert_packed_extension_const<F, EF, const W: usize>(
155 packed: EF::ExtensionPacking,
156) -> EF::ExtensionPacking
157where
158 F: Field,
159 EF: ExtensionField<F>,
160{
161 let lanes: [EF; W] = core::array::from_fn(|i| packed.extract(i));
162 let mut invs = [EF::ZERO; W];
163 batch_multiplicative_inverse_general(&lanes, &mut invs, |x| x.inverse());
164 EF::ExtensionPacking::from_ext_fn(|i| invs[i])
165}