use std::iter::zip;
use std::mem::transmute;
use std::simd::Simd;
use bytemuck::Zeroable;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use tracing::{span, Level};
use super::fft::{ifft, rfft, CACHED_FFT_LOG_SIZE, MIN_FFT_LOG_SIZE};
use super::m31::{PackedBaseField, LOG_N_LANES, N_LANES};
use super::qm31::PackedSecureField;
use super::SimdBackend;
use crate::core::circle::{CirclePoint, Coset, M31_CIRCLE_LOG_ORDER};
use crate::core::fields::m31::BaseField;
use crate::core::fields::qm31::SecureField;
use crate::core::fields::{Field, FieldExpOps};
use crate::core::poly::circle::{CanonicCoset, CircleDomain};
use crate::core::poly::utils::{domain_line_twiddles_from_tree, fold};
use crate::core::utils::bit_reverse_index;
use crate::prover::backend::cpu::circle::slow_precompute_twiddles;
use crate::prover::backend::simd::column::BaseColumn;
use crate::prover::backend::simd::m31::PackedM31;
use crate::prover::backend::{Col, Column, CpuBackend};
use crate::prover::poly::circle::{CircleEvaluation, CirclePoly, PolyOps};
use crate::prover::poly::twiddles::TwiddleTree;
use crate::prover::poly::BitReversedOrder;
impl SimdBackend {
fn twiddle_at<F: Field>(mappings: &[F], mut index: usize) -> F {
debug_assert!(
(1 << mappings.len()) as usize >= index,
"Index out of bounds. mappings log len = {}, index = {index}",
mappings.len().ilog2()
);
let mut product = F::one();
for num in mappings.iter() {
if index & 1 == 1 {
product *= *num;
}
index >>= 1;
if index == 0 {
break;
}
}
product
}
fn generate_evaluation_mappings<F: Field>(point: CirclePoint<F>, log_size: u32) -> Vec<F> {
let mut mappings = vec![point.y, point.x];
let mut x = point.x;
for _ in 2..log_size {
x = CirclePoint::double_x(x);
mappings.push(x);
}
if log_size > CACHED_FFT_LOG_SIZE {
mappings.reverse();
let n = mappings.len();
let n0 = (n - LOG_N_LANES as usize) / 2;
let n1 = (n - LOG_N_LANES as usize).div_ceil(2);
let (ab, c) = mappings.split_at_mut(n1);
let (a, _b) = ab.split_at_mut(n0);
a.swap_with_slice(&mut c[0..n0]);
mappings.reverse();
}
mappings
}
fn twiddle_steps<F: Field + FieldExpOps>(mappings: &[F]) -> Vec<F> {
let mut denominators: Vec<F> = vec![mappings[0]];
for i in 1..mappings.len() {
denominators.push(denominators[i - 1] * mappings[i]);
}
let denom_inverses = F::batch_inverse(&denominators);
let mut steps = vec![mappings[0]];
mappings
.iter()
.skip(1)
.zip(denom_inverses.iter())
.for_each(|(m, d)| {
steps.push(*m * *d);
});
steps.push(F::one());
steps
}
fn advance_twiddle<F: Field>(twiddle: F, steps: &[F], curr_idx: usize) -> F {
twiddle * steps[curr_idx.trailing_ones() as usize]
}
}
impl PolyOps for SimdBackend {
type Twiddles = Vec<u32>;
fn interpolate(
eval: CircleEvaluation<Self, BaseField, BitReversedOrder>,
twiddles: &TwiddleTree<Self>,
) -> CirclePoly<Self> {
let _span = span!(Level::TRACE, "", class = "iFFT").entered();
let log_size = eval.values.length.ilog2();
if log_size < MIN_FFT_LOG_SIZE {
let cpu_poly = eval.to_cpu().interpolate();
return CirclePoly::new(cpu_poly.coeffs.into_iter().collect());
}
let mut values = eval.values;
let twiddles = domain_line_twiddles_from_tree(eval.domain, &twiddles.itwiddles);
unsafe {
ifft::ifft(
transmute::<*mut PackedBaseField, *mut u32>(values.data.as_mut_ptr()),
&twiddles,
log_size as usize,
);
}
let inv = PackedBaseField::broadcast(BaseField::from(eval.domain.size()).inverse());
values.data.iter_mut().for_each(|x| *x *= inv);
CirclePoly::new(values)
}
fn eval_at_point(poly: &CirclePoly<Self>, point: CirclePoint<SecureField>) -> SecureField {
if poly.log_size() <= 8 {
return slow_eval_at_point(poly, point);
}
let mappings = Self::generate_evaluation_mappings(point, poly.log_size());
let (map_low, map_high) = mappings.split_at(4);
let twiddle_lows =
PackedSecureField::from_array(std::array::from_fn(|i| Self::twiddle_at(map_low, i)));
let (map_mid, map_high) = map_high.split_at(4);
let twiddle_mids =
PackedSecureField::from_array(std::array::from_fn(|i| Self::twiddle_at(map_mid, i)));
let twiddle_steps = Self::twiddle_steps(map_high);
let compute_chunk_sum = |coeff_chunk: &[PackedBaseField],
twiddle_mids: PackedSecureField,
offset: usize| {
let mut sum = PackedSecureField::zeroed();
let mut twiddle_high = Self::twiddle_at(&mappings, offset * N_LANES);
for (i, coeff_chunk) in coeff_chunk.array_chunks::<N_LANES>().enumerate() {
let high_twiddle_factors =
(PackedSecureField::broadcast(twiddle_high) * twiddle_mids).to_array();
for (&packed_coeffs, mid_twiddle) in zip(coeff_chunk, high_twiddle_factors) {
sum += PackedSecureField::broadcast(mid_twiddle) * packed_coeffs;
}
twiddle_high = Self::advance_twiddle(twiddle_high, &twiddle_steps, offset + i);
}
sum
};
#[cfg(not(feature = "parallel"))]
let sum = compute_chunk_sum(&poly.coeffs.data, twiddle_mids, 0);
#[cfg(feature = "parallel")]
let sum: PackedSecureField = {
const CHUNK_SIZE: usize = 1 << 10;
let chunks = poly.coeffs.data.par_chunks(CHUNK_SIZE).enumerate();
chunks
.into_par_iter()
.map(|(i, chunk)| compute_chunk_sum(chunk, twiddle_mids, i * CHUNK_SIZE))
.sum()
};
(sum * twiddle_lows).pointwise_sum()
}
fn extend(poly: &CirclePoly<Self>, log_size: u32) -> CirclePoly<Self> {
poly.evaluate(CanonicCoset::new(log_size).circle_domain())
.interpolate()
}
fn evaluate(
poly: &CirclePoly<Self>,
domain: CircleDomain,
twiddles: &TwiddleTree<Self>,
) -> CircleEvaluation<Self, BaseField, BitReversedOrder> {
let _span = span!(Level::TRACE, "", class = "rFFT").entered();
let log_size = domain.log_size();
let fft_log_size = poly.log_size();
assert!(
log_size >= fft_log_size,
"Can only evaluate on larger domains"
);
if fft_log_size < MIN_FFT_LOG_SIZE {
let cpu_poly: CirclePoly<CpuBackend> = CirclePoly::new(poly.coeffs.to_cpu());
let cpu_eval = cpu_poly.evaluate(domain);
return CircleEvaluation::new(
cpu_eval.domain,
Col::<SimdBackend, BaseField>::from_iter(cpu_eval.values),
);
}
let twiddles = domain_line_twiddles_from_tree(domain, &twiddles.twiddles);
let log_subdomains = log_size - fft_log_size;
let mut values = Vec::with_capacity(domain.size() >> LOG_N_LANES);
#[allow(clippy::uninit_vec)]
unsafe {
values.set_len(domain.size() >> LOG_N_LANES)
};
for i in 0..(1 << log_subdomains) {
let subdomain_twiddles = (0..(fft_log_size - 1))
.map(|layer_i| {
&twiddles[layer_i as usize]
[i << (fft_log_size - 2 - layer_i)..(i + 1) << (fft_log_size - 2 - layer_i)]
})
.collect::<Vec<_>>();
unsafe {
rfft::fft(
transmute::<*const PackedBaseField, *const u32>(poly.coeffs.data.as_ptr()),
transmute::<*mut PackedBaseField, *mut u32>(
values[i << (fft_log_size - LOG_N_LANES)
..(i + 1) << (fft_log_size - LOG_N_LANES)]
.as_mut_ptr(),
),
&subdomain_twiddles,
fft_log_size as usize,
);
}
}
CircleEvaluation::new(
domain,
BaseColumn {
data: values,
length: domain.size(),
},
)
}
fn precompute_twiddles(mut coset: Coset) -> TwiddleTree<Self> {
let _span = span!(Level::TRACE, "", class = "PrecomputeTwiddles").entered();
let root_coset = coset;
if root_coset.size() < N_LANES {
return compute_small_coset_twiddles(root_coset);
}
let mut twiddles = Vec::with_capacity(coset.size() / N_LANES);
while coset.log_size() > LOG_N_LANES {
compute_coset_twiddles(coset, &mut twiddles);
coset = coset.double();
}
let remaining_twiddles = slow_precompute_twiddles(coset);
twiddles.push(PackedM31::from_array(
remaining_twiddles.try_into().unwrap(),
));
let itwiddles = PackedBaseField::batch_inverse(&twiddles);
let dbl_twiddles = twiddles
.into_iter()
.flat_map(|x| (x.into_simd() * Simd::splat(2)).to_array())
.collect();
let dbl_itwiddles = itwiddles
.into_iter()
.flat_map(|x| (x.into_simd() * Simd::splat(2)).to_array())
.collect();
TwiddleTree {
root_coset,
twiddles: dbl_twiddles,
itwiddles: dbl_itwiddles,
}
}
}
fn compute_small_coset_twiddles(coset: Coset) -> TwiddleTree<SimdBackend> {
let twiddles = slow_precompute_twiddles(coset);
let dbl_twiddles = twiddles.iter().map(|x| x.0 * 2).collect();
let dbl_itwiddles = twiddles.iter().map(|x| x.inverse().0 * 2).collect();
TwiddleTree {
root_coset: coset,
twiddles: dbl_twiddles,
itwiddles: dbl_itwiddles,
}
}
fn compute_coset_twiddles(coset: Coset, twiddles: &mut Vec<PackedM31>) {
let log_size = coset.log_size() - 1;
assert!(log_size >= LOG_N_LANES);
let initial_points = std::array::from_fn(|i| coset.at(bit_reverse_index(i, log_size)));
let mut current = CirclePoint {
x: PackedM31::from_array(initial_points.each_ref().map(|p| p.x)),
y: PackedM31::from_array(initial_points.each_ref().map(|p| p.y)),
};
let mut steps = [CirclePoint::zero(); (M31_CIRCLE_LOG_ORDER - LOG_N_LANES) as usize];
for i in 0..(log_size - LOG_N_LANES) {
let prev_mul = bit_reverse_index((1 << i) - 1, log_size - LOG_N_LANES);
let new_mul = bit_reverse_index(1 << i, log_size - LOG_N_LANES);
let step = coset.step.mul(new_mul as u128) - coset.step.mul(prev_mul as u128);
steps[i as usize] = step;
}
for i in 0u32..1 << (log_size - LOG_N_LANES) {
let x = current.x;
let step_index = i.trailing_ones() as usize;
let step = CirclePoint {
x: PackedM31::broadcast(steps[step_index].x),
y: PackedM31::broadcast(steps[step_index].y),
};
current = current + step;
twiddles.push(x);
}
}
fn slow_eval_at_point(
poly: &CirclePoly<SimdBackend>,
point: CirclePoint<SecureField>,
) -> SecureField {
let mut mappings = vec![point.y, point.x];
let mut x = point.x;
for _ in 2..poly.log_size() {
x = CirclePoint::double_x(x);
mappings.push(x);
}
mappings.reverse();
if poly.log_size() > CACHED_FFT_LOG_SIZE {
let n = mappings.len();
let n0 = (n - LOG_N_LANES as usize) / 2;
let n1 = (n - LOG_N_LANES as usize).div_ceil(2);
let (ab, c) = mappings.split_at_mut(n1);
let (a, _b) = ab.split_at_mut(n0);
a.swap_with_slice(&mut c[0..n0]);
}
fold(poly.coeffs.as_slice(), &mappings)
}
#[cfg(test)]
mod tests {
use itertools::Itertools;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use crate::core::circle::CirclePoint;
use crate::core::fields::m31::BaseField;
use crate::core::poly::circle::CanonicCoset;
use crate::prover::backend::simd::circle::slow_eval_at_point;
use crate::prover::backend::simd::fft::{CACHED_FFT_LOG_SIZE, MIN_FFT_LOG_SIZE};
use crate::prover::backend::simd::SimdBackend;
use crate::prover::backend::{Column, CpuBackend};
use crate::prover::poly::circle::{CircleEvaluation, CirclePoly, PolyOps};
use crate::prover::poly::{BitReversedOrder, NaturalOrder};
#[test]
fn test_interpolate_and_eval() {
for log_size in MIN_FFT_LOG_SIZE..CACHED_FFT_LOG_SIZE + 4 {
let domain = CanonicCoset::new(log_size).circle_domain();
let evaluation = CircleEvaluation::<SimdBackend, BaseField, BitReversedOrder>::new(
domain,
(0..1 << log_size).map(BaseField::from).collect(),
);
let poly = evaluation.clone().interpolate();
let evaluation2 = poly.evaluate(domain);
assert_eq!(evaluation.values.to_cpu(), evaluation2.values.to_cpu());
}
}
#[test]
fn test_eval_extension() {
for log_size in MIN_FFT_LOG_SIZE..CACHED_FFT_LOG_SIZE + 2 {
let domain = CanonicCoset::new(log_size).circle_domain();
let domain_ext = CanonicCoset::new(log_size + 2).circle_domain();
let evaluation = CircleEvaluation::<SimdBackend, BaseField, BitReversedOrder>::new(
domain,
(0..1 << log_size).map(BaseField::from).collect(),
);
let poly = evaluation.clone().interpolate();
let evaluation2 = poly.evaluate(domain_ext);
assert_eq!(
poly.extend(log_size + 2).coeffs.to_cpu(),
evaluation2.interpolate().coeffs.to_cpu()
);
}
}
#[test]
fn test_eval_at_point() {
for log_size in MIN_FFT_LOG_SIZE + 1..CACHED_FFT_LOG_SIZE + 4 {
let domain = CanonicCoset::new(log_size).circle_domain();
let evaluation = CircleEvaluation::<SimdBackend, BaseField, NaturalOrder>::new(
domain,
(0..1 << log_size).map(BaseField::from).collect(),
);
let poly = evaluation.bit_reverse().interpolate();
for i in [0, 1, 3, 1 << (log_size - 1), 1 << (log_size - 2)] {
let p = domain.at(i);
let eval = poly.eval_at_point(p.into_ef());
assert_eq!(
eval,
BaseField::from(i).into(),
"log_size={log_size}, i={i}"
);
}
}
}
#[test]
fn test_circle_poly_extend() {
for log_size in MIN_FFT_LOG_SIZE..CACHED_FFT_LOG_SIZE + 2 {
let poly =
CirclePoly::<SimdBackend>::new((0..1 << log_size).map(BaseField::from).collect());
let eval0 = poly.evaluate(CanonicCoset::new(log_size + 2).circle_domain());
let eval1 = poly
.extend(log_size + 2)
.evaluate(CanonicCoset::new(log_size + 2).circle_domain());
assert_eq!(eval0.values.to_cpu(), eval1.values.to_cpu());
}
}
#[test]
fn test_eval_securefield() {
let mut rng = SmallRng::seed_from_u64(0);
for log_size in MIN_FFT_LOG_SIZE..CACHED_FFT_LOG_SIZE + 2 {
let domain = CanonicCoset::new(log_size).circle_domain();
let evaluation = CircleEvaluation::<SimdBackend, BaseField, NaturalOrder>::new(
domain,
(0..1 << log_size).map(BaseField::from).collect(),
);
let poly = evaluation.bit_reverse().interpolate();
let x = rng.gen();
let y = rng.gen();
let p = CirclePoint { x, y };
let eval = PolyOps::eval_at_point(&poly, p);
assert_eq!(eval, slow_eval_at_point(&poly, p), "log_size = {log_size}");
}
}
#[test]
fn test_optimized_precompute_twiddles() {
let coset = CanonicCoset::new(10).half_coset();
let twiddles = SimdBackend::precompute_twiddles(coset);
let expected_twiddles = CpuBackend::precompute_twiddles(coset);
assert_eq!(
twiddles.twiddles,
expected_twiddles
.twiddles
.iter()
.map(|x| x.0 * 2)
.collect_vec()
);
}
}