Skip to main content

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}