use alloc::vec::Vec;
use p3_binary_field::poly_basis::{LOW_STAGES, LowStageTwiddles};
use p3_binary_field::{BinaryField128, TowerLevel, poly_basis};
use p3_field::PrimeCharacteristicRing;
use p3_matrix::Matrix;
use p3_matrix::dense::RowMajorMatrix;
use p3_maybe_rayon::prelude::*;
use p3_util::{log2_ceil_usize, log2_floor_usize, log2_strict_usize};
use crate::domain::domain_point;
use crate::lch::BUTTERFLY_GRAIN;
use crate::staging::{
Dispatch, StagedRuns, for_each_staged_tile, for_each_staged_tile_into_cosets,
};
use crate::traits::AdditiveNtt;
#[derive(Clone, Debug, Default)]
pub struct PolyBasisNtt {
tower: crate::tower::TowerNtt,
}
struct Twiddles {
basis: [u128; usize::BITS as usize],
deltas: [u128; usize::BITS as usize],
shifts: [u128; usize::BITS as usize],
}
impl Twiddles {
fn new(log_n: usize, shift: BinaryField128) -> Self {
let mut result = Self {
basis: [0; usize::BITS as usize],
deltas: [0; usize::BITS as usize],
shifts: [0; usize::BITS as usize],
};
let mut delta = 0;
let mut base = poly_basis::from_tower(shift);
for j in 0..log_n {
result.basis[j] = poly_basis::from_tower(BinaryField128::cantor_basis(j + 1));
delta ^= result.basis[j];
result.deltas[j] = delta;
result.shifts[j] = base;
base = poly_basis::square(base) ^ base;
}
result
}
const fn at(&self, stage: usize, mut block: usize) -> u128 {
let mut t = self.shifts[stage];
while block != 0 {
t ^= self.basis[block.trailing_zeros() as usize];
block &= block - 1;
}
t
}
}
#[allow(clippy::missing_const_for_fn)]
fn use_parallel(elements: usize) -> bool {
let threads = p3_maybe_rayon::prelude::current_num_threads();
threads > 1 && elements >= 2 * BUTTERFLY_GRAIN * threads
}
fn for_chunks(
values: &mut [u128],
chunk_len: usize,
stages: usize,
operation: impl Fn((usize, &mut [u128])) + Send + Sync,
) {
if values.len() > chunk_len && use_parallel(values.len().saturating_mul(stages.max(1))) {
values
.par_chunks_mut(chunk_len)
.enumerate()
.for_each(operation);
} else {
values.chunks_mut(chunk_len).enumerate().for_each(operation);
}
}
const COSET_COPY_GRAIN: usize = 1 << 16;
fn copy_coset(dst: &mut [u128], src: &[u128]) {
if dst.len() > COSET_COPY_GRAIN && use_parallel(dst.len()) {
dst.par_chunks_mut(COSET_COPY_GRAIN)
.zip(src.par_chunks(COSET_COPY_GRAIN))
.for_each(|(d, s)| d.copy_from_slice(s));
} else {
dst.copy_from_slice(src);
}
}
type Conversion = fn(&mut [u128]);
const INTO_POLY: Conversion = poly_basis::from_tower_slice;
const INTO_TOWER: Conversion = poly_basis::to_tower_slice;
fn convert(values: &mut [u128], conversion: Conversion) {
if use_parallel(values.len().saturating_mul(4)) {
values.par_chunks_mut(BUTTERFLY_GRAIN).for_each(conversion);
} else {
conversion(values);
}
}
fn convert_tile(tile: &mut [u128], conversion: Conversion) {
conversion(tile);
}
#[derive(Copy, Clone, Debug)]
struct Fold {
entry: bool,
exit: bool,
}
impl Fold {
const BOTH: Self = Self {
entry: true,
exit: true,
};
const ENTRY: Self = Self {
entry: true,
exit: false,
};
const EXIT: Self = Self {
entry: false,
exit: true,
};
}
fn stage(values: &mut [u128], half: usize, j: usize, twiddles: &Twiddles, inverse: bool) {
if !use_parallel(values.len()) {
local_stage(values, half, j, twiddles, inverse, 0);
return;
}
let blocks_per_chunk = (BUTTERFLY_GRAIN / (half << 1)).max(1);
values
.par_chunks_mut((half << 1) * blocks_per_chunk)
.enumerate()
.for_each(|(chunk_index, chunk)| {
let first = chunk_index * blocks_per_chunk;
let mut t = twiddles.at(j, first);
for (index, block) in chunk.chunks_mut(half << 1).enumerate() {
let (lo, hi) = block.split_at_mut(half);
let butterfly = |lo: &mut [u128], hi: &mut [u128]| {
if inverse {
poly_basis::butterfly_inverse(lo, hi, t);
} else {
poly_basis::butterfly_forward(lo, hi, t);
}
};
if half <= BUTTERFLY_GRAIN {
butterfly(lo, hi);
} else {
lo.par_chunks_mut(BUTTERFLY_GRAIN)
.zip(hi.par_chunks_mut(BUTTERFLY_GRAIN))
.for_each(|(lo, hi)| butterfly(lo, hi));
}
t ^= twiddles.deltas[(first + index).trailing_ones() as usize];
}
});
}
const TILE_BYTES: usize = 32 * 1024;
const STAGING_BYTES: usize = 64 * 1024;
const DEEP_TILE_BYTES: usize = 256 * 1024;
const SHARED_CACHE_BYTES: usize = 128 * 1024 * 1024;
const _: () = assert!(SHARED_CACHE_BYTES / DEEP_TILE_BYTES >= 256);
const STAGED_LINE_BYTES: usize = 64;
const STAGED_RUN_BYTES: usize = 1024;
const STAGED_GATHER_DOUBLINGS: usize = 3;
const STAGED_WORKERS: usize = 4;
#[derive(Copy, Clone, Debug)]
struct Plan {
width: usize,
log_n: usize,
local: usize,
log_block: usize,
depth: usize,
}
impl Plan {
const fn leftover_stages(above: usize, depth: usize) -> usize {
if depth < 2 {
above
} else if above % depth == 1 {
1
} else {
0
}
}
fn groups(above: usize, depth: usize) -> impl Iterator<Item = usize> {
let leftover = Self::leftover_stages(above, depth);
let mut remaining = above;
core::iter::from_fn(move || {
(remaining > leftover).then(|| {
let take = depth.min(remaining - leftover);
remaining -= take;
take
})
})
}
fn traversals(above: usize, depth: usize) -> usize {
Self::groups(above, depth).count() + Self::leftover_stages(above, depth)
}
fn budgets(row: usize, log_n: usize) -> (usize, usize) {
if log_n > log2_floor_usize((SHARED_CACHE_BYTES / row).max(1)) {
(DEEP_TILE_BYTES, DEEP_TILE_BYTES)
} else {
(TILE_BYTES, STAGING_BYTES)
}
}
fn new(width: usize, log_n: usize) -> Self {
Self::for_workers(width, log_n, current_num_threads())
}
fn for_workers(width: usize, log_n: usize, workers: usize) -> Self {
let row = core::mem::size_of::<u128>() * width;
let (tile, staging) = Self::budgets(row, log_n);
let local = log2_floor_usize((tile / row).max(1)).min(log_n);
let above = log_n - local;
let depth_at = |log_block: usize| log2_floor_usize((staging / (row << log_block)).max(1));
let floor = log2_ceil_usize(STAGED_LINE_BYTES.div_ceil(row));
let target = log2_ceil_usize(STAGED_RUN_BYTES.div_ceil(row)).max(floor);
let budget = Self::traversals(above, depth_at(floor));
let gathers = Self::groups(above, depth_at(floor)).count();
let log_block = (floor..=target)
.rev()
.find(|&log_block| {
let depth = depth_at(log_block);
let paid = log_block >= floor + STAGED_GATHER_DOUBLINGS;
Self::traversals(above, depth) <= budget
&& (paid || Self::groups(above, depth).count() <= gathers)
})
.unwrap_or(floor);
Self {
width,
log_n,
local,
log_block,
depth: if row >= STAGED_LINE_BYTES || workers >= STAGED_WORKERS {
depth_at(log_block)
} else {
0
},
}
}
const fn leftover(&self) -> usize {
Self::leftover_stages(self.log_n - self.local, self.depth)
}
fn group_sizes(&self) -> impl Iterator<Item = usize> {
Self::groups(self.log_n - self.local, self.depth)
}
}
fn local_stage(
values: &mut [u128],
half: usize,
j: usize,
twiddles: &Twiddles,
inverse: bool,
first: usize,
) {
let mut t = twiddles.at(j, first);
for (index, block) in values.chunks_mut(half << 1).enumerate() {
let (lo, hi) = block.split_at_mut(half);
if inverse {
poly_basis::butterfly_inverse(lo, hi, t);
} else {
poly_basis::butterfly_forward(lo, hi, t);
}
t ^= twiddles.deltas[(first + index).trailing_ones() as usize];
}
}
fn tile_stages(
tile: &mut [u128],
row: usize,
depth: usize,
top: usize,
twiddles: &Twiddles,
inverse: bool,
block: usize,
) {
for k in 0..depth {
let s = if inverse { depth - 1 - k } else { k };
local_stage(
tile,
(1 << (depth - 1 - s)) * row,
top - 1 - s,
twiddles,
inverse,
block << s,
);
}
}
fn local_stages(values: &mut [u128], plan: Plan, twiddles: &Twiddles, inverse: bool, fold: Fold) {
let Plan {
width,
log_n,
local,
..
} = plan;
let tile_len = (1 << local) * width;
let low = (!inverse && width == 1 && local >= LOW_STAGES)
.then(|| LowStageTwiddles::new(&twiddles.shifts, &twiddles.basis[..log_n]));
for_chunks(values, tile_len, local, |(index, tile)| {
if fold.entry {
convert_tile(tile, INTO_POLY);
}
if let Some(low) = &low {
let above = local - LOW_STAGES;
tile_stages(tile, 1 << LOW_STAGES, above, local, twiddles, false, index);
low.forward(tile, index << above);
} else {
tile_stages(tile, width, local, local, twiddles, inverse, index);
}
if fold.exit {
convert_tile(tile, INTO_TOWER);
}
});
}
fn fused_stages(
values: &mut [u128],
plan: Plan,
top: usize,
depth: usize,
twiddles: &Twiddles,
inverse: bool,
convert_basis: bool,
) {
let (runs, run, dispatch) = staged_group(plan, top, depth, values.len());
for_each_staged_tile(values, runs, dispatch, |tile, block| {
if convert_basis && !inverse {
convert_tile(tile, INTO_POLY);
}
tile_stages(tile, run, depth, top, twiddles, inverse, block);
if convert_basis && inverse {
convert_tile(tile, INTO_TOWER);
}
});
}
fn staged_group(
plan: Plan,
top: usize,
depth: usize,
elements: usize,
) -> (StagedRuns, usize, Dispatch) {
let log_block = plan.log_block.min(top - depth);
let run = plan.width << log_block;
let dispatch = if use_parallel(elements.saturating_mul(depth)) {
Dispatch::Parallel { min_len: 1 }
} else {
Dispatch::Serial
};
let runs = StagedRuns::new(run, top - log_block - depth, depth);
(runs, run, dispatch)
}
fn first_group_into_cosets(
values: &mut [u128],
message_len: usize,
plan: Plan,
depth: usize,
twiddles: &[Twiddles],
) {
let top = plan.log_n;
let (runs, run, dispatch) = staged_group(plan, top, depth, values.len());
for_each_staged_tile_into_cosets(
values,
message_len,
runs,
dispatch,
|tile| convert_tile(tile, INTO_POLY),
|tile, block, coset| tile_stages(tile, run, depth, top, &twiddles[coset], false, block),
);
}
fn padded_sharing_first_group(values: &mut [u128], plan: Plan, depth: usize, log_inv_rate: usize) {
let log_message = plan.log_n;
let len = values.len() >> log_inv_rate;
let twiddles: Vec<Twiddles> = (0..1 << log_inv_rate)
.map(|c| Twiddles::new(log_message, domain_point(c << log_message)))
.collect();
first_group_into_cosets(values, len, plan, depth, &twiddles);
for_chunks(values, len, log_message, |(c, coset)| {
forward_below(coset, plan, &twiddles[c], 1, Fold::EXIT);
});
}
fn forward(values: &mut [u128], plan: Plan, shift: BinaryField128, fold: Fold) {
forward_below(values, plan, &Twiddles::new(plan.log_n, shift), 0, fold);
}
fn forward_below(values: &mut [u128], plan: Plan, twiddles: &Twiddles, done: usize, fold: Fold) {
let Plan {
width,
log_n,
local,
..
} = plan;
debug_assert!(
done == 0 || !fold.entry,
"the entry conversion rides on the first group"
);
let mut entry = fold.entry;
let mut top = log_n;
for (group, take) in plan.group_sizes().enumerate() {
if group >= done {
fused_stages(values, plan, top, take, twiddles, false, entry);
entry = false;
}
top -= take;
}
if entry && top > local {
convert(values, INTO_POLY);
entry = false;
}
for j in (local..top).rev() {
stage(values, (1 << j) * width, j, twiddles, false);
}
local_stages(
values,
plan,
twiddles,
false,
Fold {
entry,
exit: fold.exit,
},
);
}
fn inverse(values: &mut [u128], plan: Plan, shift: BinaryField128, fold: Fold) {
let Plan {
width,
log_n,
local,
..
} = plan;
let twiddles = Twiddles::new(log_n, shift);
let leftover = plan.leftover();
let tile_exit = fold.exit && local == log_n;
let mut exit = fold.exit && !tile_exit;
local_stages(
values,
plan,
&twiddles,
true,
Fold {
entry: fold.entry,
exit: tile_exit,
},
);
for j in local..local + leftover {
stage(values, (1 << j) * width, j, &twiddles, true);
}
let mut sizes = [0; usize::BITS as usize];
let mut count = 0;
for take in plan.group_sizes() {
sizes[count] = take;
count += 1;
}
let mut base = local + leftover;
for (index, take) in sizes[..count].iter().rev().enumerate() {
base += take;
let last = index + 1 == count;
fused_stages(values, plan, base, *take, &twiddles, true, exit && last);
exit &= !last;
}
if exit {
convert(values, INTO_TOWER);
}
}
impl AdditiveNtt<BinaryField128> for PolyBasisNtt {
fn shifted_ntt_batch(
&self,
mut mat: RowMajorMatrix<BinaryField128>,
shift: BinaryField128,
) -> RowMajorMatrix<BinaryField128> {
if !poly_basis::HAS_HARDWARE_CLMUL {
return self.tower.shifted_ntt_batch(mat, shift);
}
let width = mat.width();
let log_n = log2_strict_usize(mat.height());
let mut values: Vec<u128> = core::mem::take(&mut mat.values)
.into_iter()
.map(BinaryField128::to_repr)
.collect();
forward(&mut values, Plan::new(width, log_n), shift, Fold::BOTH);
mat.values = values.into_iter().map(BinaryField128::from_repr).collect();
mat
}
fn ntt_batch_padded(
&self,
mut mat: RowMajorMatrix<BinaryField128>,
log_inv_rate: usize,
) -> RowMajorMatrix<BinaryField128> {
let log_n = log2_strict_usize(mat.height());
assert!(log_inv_rate <= log_n, "padding exceeds matrix height");
if log_inv_rate == 0 || !poly_basis::HAS_HARDWARE_CLMUL {
return self.ntt_batch(mat);
}
let width = mat.width;
let log_message = log_n - log_inv_rate;
let len = mat.values.len() >> log_inv_rate;
let mut values: Vec<u128> = core::mem::take(&mut mat.values)
.into_iter()
.map(BinaryField128::to_repr)
.collect();
let plan = Plan::new(width, log_message);
let large = len >= 2 * BUTTERFLY_GRAIN * p3_maybe_rayon::prelude::current_num_threads();
if let Some(depth) = plan.group_sizes().next().filter(|_| large) {
padded_sharing_first_group(&mut values, plan, depth, log_inv_rate);
mat.values = values.into_iter().map(BinaryField128::from_repr).collect();
return mat;
}
let (message, tail) = values.split_at_mut(len);
convert(message, INTO_POLY);
if large {
for_chunks(tail, len, log_message, |(c, chunk)| {
copy_coset(chunk, message);
forward(
chunk,
plan,
domain_point((c + 1) << log_message),
Fold::EXIT,
);
});
forward(message, plan, BinaryField128::ZERO, Fold::EXIT);
} else {
for_chunks(tail, len, 1, |(_, chunk)| chunk.copy_from_slice(message));
for_chunks(&mut values, len, log_message, |(c, chunk)| {
forward(chunk, plan, domain_point(c << log_message), Fold::EXIT);
});
}
mat.values = values.into_iter().map(BinaryField128::from_repr).collect();
mat
}
fn shifted_lde_batch(
&self,
mut mat: RowMajorMatrix<BinaryField128>,
added_bits: usize,
shift: BinaryField128,
) -> RowMajorMatrix<BinaryField128> {
if !poly_basis::HAS_HARDWARE_CLMUL {
return self.tower.shifted_lde_batch(mat, added_bits, shift);
}
let log_n = log2_strict_usize(mat.height());
if added_bits == 0 {
return mat;
}
let width = mat.width;
let len = mat.values.len();
let padded_len = u32::try_from(added_bits)
.ok()
.and_then(|bits| len.checked_shl(bits))
.filter(|&padded| padded >> added_bits == len)
.expect("extended codeword length overflows usize");
let mut coeffs: Vec<u128> = core::mem::take(&mut mat.values)
.into_iter()
.map(BinaryField128::to_repr)
.collect();
let mut values = alloc::vec![0u128; padded_len];
values[..len].copy_from_slice(&coeffs);
let plan = Plan::new(width, log_n);
inverse(&mut coeffs, plan, shift, Fold::ENTRY);
for_chunks(&mut values[len..], len, log_n, |(c, chunk)| {
chunk.copy_from_slice(&coeffs);
forward(
chunk,
plan,
shift + domain_point::<BinaryField128>((c + 1) << log_n),
Fold::EXIT,
);
});
mat.values = values.into_iter().map(BinaryField128::from_repr).collect();
mat
}
fn shifted_intt_batch(
&self,
mut mat: RowMajorMatrix<BinaryField128>,
shift: BinaryField128,
) -> RowMajorMatrix<BinaryField128> {
if !poly_basis::HAS_HARDWARE_CLMUL {
return self.tower.shifted_intt_batch(mat, shift);
}
let width = mat.width();
let log_n = log2_strict_usize(mat.height());
let mut values: Vec<u128> = core::mem::take(&mut mat.values)
.into_iter()
.map(BinaryField128::to_repr)
.collect();
inverse(&mut values, Plan::new(width, log_n), shift, Fold::BOTH);
mat.values = values.into_iter().map(BinaryField128::from_repr).collect();
mat
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use p3_binary_field::{BinaryField128, TowerLevel, poly_basis};
use p3_field::PrimeCharacteristicRing;
use p3_matrix::dense::RowMajorMatrix;
use p3_util::log2_floor_usize;
use proptest::prelude::*;
use super::{Fold, Plan, PolyBasisNtt, STAGED_RUN_BYTES, STAGED_WORKERS};
use crate::domain::{domain_point, subspace_polynomial};
use crate::lch::LchNtt;
use crate::naive::NaiveAdditiveNtt;
use crate::traits::AdditiveNtt;
const CUTS: [(usize, usize, usize); 6] = [
(2, 0, 3),
(2, 2, 3),
(2, 0, 0),
(0, 0, 3),
(0, 3, 3),
(1, 1, 1),
];
const DEEP_CUTS: [(usize, usize, usize, usize); 4] =
[(0, 0, 3, 8), (0, 1, 3, 9), (1, 2, 2, 8), (0, 0, 2, 8)];
const WIDTHS: [usize; 5] = [1, 2, 3, 16, 64];
const PRODUCTION_CUTS: [(usize, usize, (usize, usize, usize)); 11] = [
(1, 10, (10, 6, 6)),
(1, 14, (11, 6, 6)),
(1, 20, (11, 3, 9)),
(2, 13, (10, 5, 6)),
(3, 12, (9, 5, 5)),
(4, 14, (9, 4, 6)),
(16, 10, (7, 2, 6)),
(16, 14, (7, 1, 7)),
(64, 10, (5, 0, 6)),
(512, 8, (2, 0, 3)),
(1024, 7, (1, 0, 2)),
];
const PLAN_ONLY_CUTS: [(usize, usize, (usize, usize, usize)); 8] = [
(48, 18, (8, 1, 7)),
(16, 16, (7, 0, 8)),
(8, 18, (8, 3, 6)),
(4, 20, (9, 4, 6)),
(1, 22, (11, 6, 6)),
(16, 18, (7, 2, 6)),
(16, 20, (10, 0, 10)),
(1, 25, (14, 3, 11)),
];
const DEEP_BUDGET_GROUPS: [(usize, usize, usize); 3] = [(48, 1, 7), (16, 0, 10), (1, 3, 11)];
const DEEP_BUDGET_TILES: [(usize, usize); 3] = [(1, 14), (16, 10), (48, 8)];
const ORACLE_CUTS: [(usize, usize, usize, usize); 4] =
[(2, 0, 3, 6), (2, 2, 3, 6), (1, 1, 2, 5), (0, 0, 3, 6)];
const ORACLE_WIDTHS: [usize; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 16, 64];
const MAX_LOG_N: usize = 14;
fn max_log_n(width: usize) -> usize {
if core::mem::size_of::<u128>() * width >= STAGED_RUN_BYTES {
10
} else {
MAX_LOG_N
}
}
const REFERENCE_LOG_N: usize = 6;
const NONE: Fold = Fold {
entry: false,
exit: false,
};
fn test_shift() -> BinaryField128 {
BinaryField128::from_repr((1 << 127) | 7919)
}
fn coefficients(log_n: usize, width: usize) -> Vec<u128> {
(0..(width << log_n))
.map(|i| {
let low = (i as u64)
.wrapping_mul(0x9e37_79b9_7f4a_7c15)
.wrapping_add(0x5555_5555_5555_5555);
let high = low.rotate_left(17).wrapping_mul(0xc2b2_ae3d_27d4_eb4f);
(u128::from(high) << 64) | u128::from(low)
})
.collect()
}
fn per_stage_schedule(
values: &mut [u128],
width: usize,
log_n: usize,
shift: BinaryField128,
inverse: bool,
) {
let twiddles = super::Twiddles::new(log_n, shift);
for k in 0..log_n {
let j = if inverse { k } else { log_n - 1 - k };
let half = (1 << j) * width;
for (block, rows) in values.chunks_mut(half << 1).enumerate() {
let t = twiddles.at(j, block);
let (lo, hi) = rows.split_at_mut(half);
if inverse {
poly_basis::butterfly_inverse(lo, hi, t);
} else {
poly_basis::butterfly_forward(lo, hi, t);
}
}
}
}
fn scheduled(values: &mut [u128], plan: Plan, inverse: bool, fold: Fold) {
if inverse {
super::inverse(values, plan, test_shift(), fold);
} else {
super::forward(values, plan, test_shift(), fold);
}
}
fn matrix(log_n: usize, width: usize, seed: u64) -> RowMajorMatrix<BinaryField128> {
RowMajorMatrix::new(
(0..(width << log_n))
.map(|i| {
let bits = seed
.wrapping_mul(0x9e37_79b9_7f4a_7c15)
.wrapping_add(i as u64);
BinaryField128::from_le_byte_iter(bits.to_le_bytes().into_iter().cycle())
})
.collect(),
width,
)
}
fn cut_plans() -> Vec<Plan> {
let mut plans = Vec::new();
for (local, log_block, depth) in CUTS {
for log_n in [
local,
local + 1,
local + depth,
local + depth + 1,
2 * local + depth,
] {
for width in WIDTHS {
plans.push(Plan {
width,
log_n,
local: local.min(log_n),
log_block,
depth,
});
}
}
}
for (local, log_block, depth, log_n) in DEEP_CUTS {
for width in WIDTHS {
plans.push(Plan {
width,
log_n,
local,
log_block,
depth,
});
}
}
plans
}
#[test]
fn every_cut_of_the_stage_sequence_matches_the_per_stage_schedule() {
for plan in cut_plans() {
let Plan { width, log_n, .. } = plan;
for inverse in [false, true] {
let mut expected = coefficients(log_n, width);
let mut actual = expected.clone();
per_stage_schedule(&mut expected, width, log_n, test_shift(), inverse);
scheduled(&mut actual, plan, inverse, NONE);
assert_eq!(actual, expected, "{plan:?} inverse={inverse}");
}
}
for (width, log_n, _) in PRODUCTION_CUTS {
let plan = Plan::for_workers(width, log_n, 32);
for inverse in [false, true] {
let mut expected = coefficients(log_n, width);
let mut actual = expected.clone();
per_stage_schedule(&mut expected, width, log_n, test_shift(), inverse);
scheduled(&mut actual, plan, inverse, NONE);
assert_eq!(actual, expected, "{plan:?} inverse={inverse}");
}
}
}
#[test]
fn a_deep_budget_cut_matches_the_per_stage_schedule() {
let matches_per_stage = |plan: Plan| {
let Plan { width, log_n, .. } = plan;
for inverse in [false, true] {
let mut expected = coefficients(log_n, width);
let mut actual = expected.clone();
per_stage_schedule(&mut expected, width, log_n, test_shift(), inverse);
scheduled(&mut actual, plan, inverse, NONE);
assert_eq!(actual, expected, "{plan:?} inverse={inverse}");
}
};
for (width, log_block, depth) in DEEP_BUDGET_GROUPS {
let local = log_block + 1;
for extra in [0usize, 1, 2] {
matches_per_stage(Plan {
width,
log_n: local + depth + extra,
local,
log_block,
depth,
});
}
}
for (width, local) in DEEP_BUDGET_TILES {
for extra in [1usize, 2, 3] {
matches_per_stage(Plan {
width,
log_n: local + extra,
local,
log_block: 0,
depth: 2,
});
}
}
}
#[test]
fn every_width_and_height_matches_the_per_stage_schedule() {
for width in ORACLE_WIDTHS {
for log_n in 0..=max_log_n(width) {
for workers in [1, 32] {
let plan = Plan::for_workers(width, log_n, workers);
for shift in [BinaryField128::ZERO, test_shift()] {
for inverse in [false, true] {
let mut expected = coefficients(log_n, width);
let mut actual = expected.clone();
per_stage_schedule(&mut expected, width, log_n, shift, inverse);
if inverse {
super::inverse(&mut actual, plan, shift, NONE);
} else {
super::forward(&mut actual, plan, shift, NONE);
}
assert_eq!(
actual, expected,
"{plan:?} shift={shift:?} inverse={inverse}"
);
}
}
}
}
}
}
fn passes_above_the_tile(above: usize, depth: usize) -> usize {
let leftover = if depth < 2 {
above
} else {
usize::from(above % depth == 1)
};
let mut passes = 0;
let mut remaining = above;
while remaining > leftover {
remaining -= depth.min(remaining - leftover);
passes += 1;
}
passes + remaining
}
#[test]
fn the_plan_gathers_the_longest_run_its_budgets_allow() {
for (width, log_n, cut) in PRODUCTION_CUTS.into_iter().chain(PLAN_ONLY_CUTS) {
let plan = Plan::for_workers(width, log_n, 32);
assert_eq!((plan.local, plan.log_block, plan.depth), cut, "{plan:?}");
}
for width in [4usize, 5, 8, 16, 64, 512, 1024] {
for log_n in 0..=24 {
let plan = Plan::for_workers(width, log_n, 32);
let row = core::mem::size_of::<u128>() * width;
let one_row = log2_floor_usize((Plan::budgets(row, log_n).1 / row).max(1));
let above = log_n - plan.local;
assert!(
passes_above_the_tile(above, plan.depth)
<= passes_above_the_tile(above, one_row),
"{plan:?}"
);
}
}
}
#[test]
fn a_staged_cut_matches_the_reference_transform() {
let naive = NaiveAdditiveNtt::<BinaryField128>::default();
for (local, log_block, depth, log_n) in ORACLE_CUTS {
for width in [1usize, 3, 16] {
let plan = Plan {
width,
log_n,
local,
log_block,
depth,
};
assert!(
plan.group_sizes().next().is_some(),
"{plan:?} gathers no tile"
);
for shift in [BinaryField128::ZERO, test_shift()] {
let coeffs = matrix(log_n, width, 3);
let evals = naive.shifted_ntt_batch(coeffs.clone(), shift);
let reprs = |mat: &RowMajorMatrix<BinaryField128>| -> Vec<u128> {
mat.values
.iter()
.copied()
.map(BinaryField128::to_repr)
.collect()
};
let mut actual = reprs(&coeffs);
super::forward(&mut actual, plan, shift, Fold::BOTH);
assert_eq!(actual, reprs(&evals), "ntt {plan:?} shift={shift:?}");
let mut actual = reprs(&evals);
super::inverse(&mut actual, plan, shift, Fold::BOTH);
assert_eq!(actual, reprs(&coeffs), "intt {plan:?} shift={shift:?}");
}
}
}
}
#[test]
fn a_row_that_covers_a_line_is_gathered_whatever_the_worker_count() {
for width in [4usize, 16, 64, 512] {
for log_n in [10, 14, 20] {
let alone = Plan::for_workers(width, log_n, 1);
let shared = Plan::for_workers(width, log_n, 32);
assert_eq!(alone.depth, shared.depth, "{alone:?}");
assert_eq!(alone.log_block, shared.log_block, "{alone:?}");
}
}
}
#[test]
fn the_worker_count_decides_whether_a_narrow_row_is_gathered() {
let alone = Plan::for_workers(1, 20, 1);
assert_eq!(alone.depth, 0, "{alone:?}");
assert_eq!(alone.leftover(), alone.log_n - alone.local, "{alone:?}");
let shared = Plan::for_workers(1, 20, STAGED_WORKERS);
assert!(shared.depth >= 2, "{shared:?}");
assert_eq!(shared.leftover(), 0, "{shared:?}");
assert_eq!(alone.local, shared.local);
assert_eq!(alone.log_block, shared.log_block);
}
#[test]
fn every_width_matches_the_reference_transform() {
let poly = PolyBasisNtt::default();
let naive = NaiveAdditiveNtt::<BinaryField128>::default();
for width in ORACLE_WIDTHS {
for log_n in 0..=REFERENCE_LOG_N {
for shift in [BinaryField128::ZERO, test_shift()] {
let coeffs = matrix(log_n, width, 3);
let evals = naive.shifted_ntt_batch(coeffs.clone(), shift);
assert_eq!(
poly.shifted_ntt_batch(coeffs.clone(), shift),
evals,
"ntt width={width} log_n={log_n} shift={shift:?}"
);
assert_eq!(
poly.shifted_intt_batch(evals, shift),
coeffs,
"intt width={width} log_n={log_n} shift={shift:?}"
);
}
}
}
}
#[test]
fn padded_transform_matches_the_tower_at_every_width() {
let poly = PolyBasisNtt::default();
let tower = LchNtt::<BinaryField128>::default();
for width in ORACLE_WIDTHS {
for log_message in [0, 1, 5, 9] {
for log_inv_rate in 0..=3 {
let mut mat = matrix(log_message, width, 23);
mat.values
.resize(mat.values.len() << log_inv_rate, BinaryField128::ZERO);
let expected = tower.ntt_batch(mat.clone());
assert_eq!(
poly.ntt_batch_padded(mat, log_inv_rate),
expected,
"width={width} log_message={log_message} rate={log_inv_rate}"
);
}
}
}
}
#[test]
fn folded_conversions_match_standalone_conversion_passes() {
for plan in cut_plans() {
let Plan { width, log_n, .. } = plan;
for inverse in [false, true] {
let input = coefficients(log_n, width);
let mut expected = input.clone();
super::convert(&mut expected, super::INTO_POLY);
scheduled(&mut expected, plan, inverse, NONE);
let mut actual = input.clone();
scheduled(&mut actual, plan, inverse, Fold::ENTRY);
assert_eq!(actual, expected, "entry {plan:?} inverse={inverse}");
super::convert(&mut expected, super::INTO_TOWER);
let mut actual = input.clone();
scheduled(&mut actual, plan, inverse, Fold::BOTH);
assert_eq!(actual, expected, "both {plan:?} inverse={inverse}");
let mut expected = input.clone();
scheduled(&mut expected, plan, inverse, NONE);
super::convert(&mut expected, super::INTO_TOWER);
let mut actual = input;
scheduled(&mut actual, plan, inverse, Fold::EXIT);
assert_eq!(actual, expected, "exit {plan:?} inverse={inverse}");
}
}
}
#[test]
fn a_first_group_shared_by_every_coset_matches_each_coset_alone() {
for plan in cut_plans() {
let Plan { width, log_n, .. } = plan;
let Some(depth) = plan.group_sizes().next() else {
continue;
};
let message = coefficients(log_n, width);
let len = message.len();
for log_cosets in 0..=2 {
let twiddles: Vec<_> = (0..1 << log_cosets)
.map(|c| super::Twiddles::new(log_n, domain_point(c << log_n)))
.collect();
let mut actual = message.clone();
actual.resize(len << log_cosets, 0);
super::first_group_into_cosets(&mut actual, len, plan, depth, &twiddles);
for (c, coset) in actual.chunks_mut(len).enumerate() {
super::forward_below(coset, plan, &twiddles[c], 1, Fold::EXIT);
}
for (c, coset) in actual.chunks(len).enumerate() {
let mut expected = message.clone();
super::forward(&mut expected, plan, domain_point(c << log_n), Fold::BOTH);
assert_eq!(coset, &expected[..], "{plan:?} coset={c}");
}
}
}
}
#[test]
fn padded_transform_matches_the_tower_where_cosets_share_their_first_group() {
let tower = LchNtt::<BinaryField128>::default();
for (width, log_message) in [(4, 14), (16, 12), (1, 17)] {
let plan = Plan::for_workers(width, log_message, STAGED_WORKERS);
let depth = plan
.group_sizes()
.next()
.expect("every shape here stages a first group");
for log_inv_rate in 1..=2 {
let mut mat = matrix(log_message, width, 29);
mat.values
.resize(mat.values.len() << log_inv_rate, BinaryField128::ZERO);
let expected = tower.ntt_batch(mat.clone());
let mut values: Vec<u128> = mat.values.iter().map(|v| v.to_repr()).collect();
super::padded_sharing_first_group(&mut values, plan, depth, log_inv_rate);
let actual: Vec<_> = values.into_iter().map(BinaryField128::from_repr).collect();
assert_eq!(
actual, expected.values,
"width={width} log_message={log_message} rate={log_inv_rate}"
);
}
}
}
#[test]
fn padded_transform_matches_naive_at_wide_widths() {
for width in [1, 4, 16, 64] {
for added in [0, 1, 2, 3] {
let mut mat = matrix(4, width, 13);
mat.values
.resize(mat.values.len() << added, BinaryField128::ZERO);
let expected = NaiveAdditiveNtt::default().ntt_batch(mat.clone());
assert_eq!(
PolyBasisNtt::default().ntt_batch_padded(mat, added),
expected
);
}
}
}
#[test]
fn shifted_lde_matches_naive_at_wide_widths() {
for width in [1, 4, 16, 64] {
for added in [0, 1, 2, 3] {
let mat = matrix(4, width, 17);
let shift = BinaryField128::from_repr(1 << 127);
let expected =
NaiveAdditiveNtt::default().shifted_lde_batch(mat.clone(), added, shift);
let actual = PolyBasisNtt::default().shifted_lde_batch(mat.clone(), added, shift);
assert_eq!(actual, expected);
assert_eq!(&actual.values[..mat.values.len()], &mat.values);
}
}
}
#[test]
#[should_panic = "extended codeword length overflows usize"]
fn lde_rejects_length_overflow() {
let _ = PolyBasisNtt::default().lde_batch(matrix(1, 1, 0), usize::BITS as usize - 1);
}
#[test]
fn incremental_twiddles_match_independent_domain_points() {
let shift = BinaryField128::from_repr((1 << 127) | 123);
let twiddles = super::Twiddles::new(usize::BITS as usize - 1, shift);
for stage in [0, 1, 7, 15, 31]
.into_iter()
.filter(|&stage| stage < usize::BITS as usize - 1)
{
for start in [0, 1, 63, 127, (1usize << (usize::BITS - 3)) - 3] {
let mut t = twiddles.at(stage, start);
for block in start..start + 9 {
let expected = subspace_polynomial(stage, shift)
+ domain_point::<BinaryField128>(block << 1);
assert_eq!(t, poly_basis::from_tower(expected));
t ^= twiddles.deltas[block.trailing_ones() as usize];
}
}
}
}
#[test]
fn transforms_cross_cache_boundaries_in_natural_order() {
for width in [1, 3, 16, 64] {
let mat = matrix(12, width, 29);
let shift = BinaryField128::from_repr((1 << 127) | 7919);
let expected = crate::LchNtt::default().shifted_ntt_batch(mat.clone(), shift);
let actual = PolyBasisNtt::default().shifted_ntt_batch(mat.clone(), shift);
assert_eq!(actual, expected);
assert_eq!(
PolyBasisNtt::default().shifted_intt_batch(actual, shift),
mat
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn poly_basis_matches_naive(
log_n in 0usize..=8,
width in 1usize..=8,
seed in any::<u64>(),
shift in any::<u64>(),
) {
let coeffs = matrix(log_n, width, seed);
let shift = BinaryField128::from_le_byte_iter(
shift.to_le_bytes().into_iter().cycle(),
);
let fast = PolyBasisNtt::default().shifted_ntt_batch(coeffs.clone(), shift);
let slow = NaiveAdditiveNtt::<BinaryField128>::default()
.shifted_ntt_batch(coeffs, shift);
prop_assert_eq!(fast, slow);
}
#[test]
fn poly_basis_round_trips(
log_n in 0usize..=8,
width in 1usize..=8,
seed in any::<u64>(),
shift in any::<u64>(),
) {
let coeffs = matrix(log_n, width, seed);
let shift = BinaryField128::from_le_byte_iter(
shift.to_le_bytes().into_iter().cycle(),
);
let ntt = PolyBasisNtt::default();
let evals = ntt.shifted_ntt_batch(coeffs.clone(), shift);
prop_assert_eq!(ntt.shifted_intt_batch(evals, shift), coeffs);
}
#[test]
fn poly_basis_lde_matches_naive(
log_n in 0usize..=6,
added in 0usize..=3,
width in 1usize..=3,
seed in any::<u64>(),
shift in any::<u64>(),
) {
let coeffs = matrix(log_n, width, seed);
let shift = BinaryField128::from_le_byte_iter(
shift.to_le_bytes().into_iter().cycle(),
);
let lde = PolyBasisNtt::default().shifted_lde_batch(coeffs.clone(), added, shift);
let naive = NaiveAdditiveNtt::<BinaryField128>::default()
.shifted_lde_batch(coeffs.clone(), added, shift);
prop_assert_eq!(&lde, &naive);
prop_assert_eq!(&lde.values[..coeffs.values.len()], &coeffs.values[..]);
}
}
#[test]
fn poly_basis_matches_the_tower_across_several_tasks() {
let poly = PolyBasisNtt::default();
let tower = LchNtt::<BinaryField128>::default();
for width in ORACLE_WIDTHS {
let heights: &[usize] = if width <= 4 {
&[7, 10, MAX_LOG_N]
} else {
&[7, 10]
};
for &log_n in heights {
for shift_bits in [0u64, 0x1234_5678_9abc_def0] {
let coeffs = matrix(log_n, width, 5);
let shift = BinaryField128::from_le_byte_iter(
shift_bits.to_le_bytes().into_iter().cycle(),
);
let evals = tower.shifted_ntt_batch(coeffs.clone(), shift);
assert_eq!(
poly.shifted_ntt_batch(coeffs.clone(), shift),
evals,
"ntt width={width} log_n={log_n} shift={shift_bits:#x}"
);
assert_eq!(
poly.shifted_intt_batch(evals, shift),
coeffs,
"intt width={width} log_n={log_n} shift={shift_bits:#x}"
);
}
}
}
}
#[test]
#[should_panic]
fn shifted_ntt_batch_rejects_a_non_power_of_two_height() {
let coeffs = RowMajorMatrix::new(matrix(0, 1, 0).values.repeat(3), 1);
let _ = PolyBasisNtt::default().ntt_batch(coeffs);
}
}