use alloc::vec::Vec;
use core::marker::PhantomData;
use p3_binary_field::TowerLevel;
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::butterfly::ButterflyField;
use crate::domain::{domain_point, domain_point_steps};
use crate::staging::{Dispatch, StagedRuns, for_each_staged_tile};
use crate::traits::AdditiveNtt;
#[derive(Clone, Debug, Default)]
pub struct LchNtt<F> {
_marker: PhantomData<F>,
}
pub(crate) const BUTTERFLY_GRAIN: usize = 1 << 10;
const DEEP_TILE_BYTES: usize = 128 * 1024;
const STAGING_TILE_BYTES: usize = 64 * 1024;
const MIN_FUSED_STAGES: usize = 2;
const STAGED_WORKERS: usize = 16;
const CACHE_LINE_BYTES: usize = 64;
impl<F: ButterflyField> AdditiveNtt<F> for LchNtt<F> {
fn shifted_ntt_batch(&self, mat: RowMajorMatrix<F>, shift: F) -> RowMajorMatrix<F> {
transform::<F, false>(mat, shift)
}
fn shifted_intt_batch(&self, mat: RowMajorMatrix<F>, shift: F) -> RowMajorMatrix<F> {
transform::<F, true>(mat, shift)
}
fn ntt_batch_padded(
&self,
mut mat: RowMajorMatrix<F>,
log_inv_rate: usize,
) -> RowMajorMatrix<F> {
let width = mat.width();
let log_n = log2_strict_usize(mat.height());
assert!(log_inv_rate <= log_n, "padding exceeds matrix height");
assert!(log_n <= 1 << F::LOG_BITS, "domain exceeds field dimension");
transform_cosets::<F>(&mut mat.values, width, log_n - log_inv_rate);
mat
}
}
struct Twiddles<F> {
bases: Vec<F>,
steps: Vec<F>,
}
impl<F: TowerLevel> Twiddles<F> {
fn new(log_n: usize, shift: F) -> Self {
let mut base = shift;
let bases = (0..log_n)
.map(|_| {
let current = base;
base = base.square() + base;
current
})
.collect();
let steps = domain_point_steps::<F>(log_n.saturating_sub(1));
Self { bases, steps }
}
#[inline]
fn at(&self, stage: usize, block: usize) -> F {
self.bases[stage] + domain_point::<F>(block << 1)
}
#[inline]
fn step(&self, index: usize) -> F {
self.steps[index.trailing_zeros() as usize]
}
}
fn stage_pass<F: ButterflyField, const INVERSE: bool>(
values: &mut [F],
width: usize,
j: usize,
twiddles: &Twiddles<F>,
) {
let half = (1 << j) * width;
let per_task = (BUTTERFLY_GRAIN / half).max(1);
let task_len = per_task * (half << 1);
let task = |(task, group): (usize, &mut [F])| {
let first = task * per_task;
let mut t = twiddles.at(j, first);
for (i, block) in group.chunks_mut(half << 1).enumerate() {
if i != 0 {
t += twiddles.step(first + i);
}
let (lo, hi) = block.split_at_mut(half);
let butterfly = |lo: &mut [F], hi: &mut [F]| {
F::butterfly::<INVERSE>(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));
}
}
};
if values.len() > task_len {
values.par_chunks_mut(task_len).enumerate().for_each(task);
} else {
values.chunks_mut(task_len).enumerate().for_each(task);
}
}
fn tile_stages<F: ButterflyField, const INVERSE: bool>(
tile: &mut [F],
row_len: usize,
log_rows: usize,
stage_base: usize,
index: usize,
twiddles: &Twiddles<F>,
) {
for step in 0..log_rows {
let jj = if INVERSE { step } else { log_rows - 1 - step };
let half = (1 << jj) * row_len;
let first = index << (log_rows - 1 - jj);
let mut t = twiddles.at(stage_base + jj, first);
for (i, block) in tile.chunks_mut(half << 1).enumerate() {
if i != 0 {
t += twiddles.step(first + i);
}
let (lo, hi) = block.split_at_mut(half);
F::butterfly::<INVERSE>(lo, hi, t);
}
}
}
fn deep_tiles<F: ButterflyField, const INVERSE: bool>(
values: &mut [F],
width: usize,
log_rows: usize,
twiddles: &Twiddles<F>,
) {
values
.par_chunks_mut((1 << log_rows) * width)
.enumerate()
.for_each(|(index, tile)| {
tile_stages::<F, INVERSE>(tile, width, log_rows, 0, index, twiddles);
});
}
const fn staged_runs(width: usize, top: usize, depth: usize, log_slab: usize) -> StagedRuns {
StagedRuns::new((1 << log_slab) * width, top + 1 - depth - log_slab, depth)
}
fn fused_group<F: ButterflyField, const INVERSE: bool>(
values: &mut [F],
width: usize,
log_n: usize,
top: usize,
depth: usize,
log_slab: usize,
twiddles: &Twiddles<F>,
) {
debug_assert!(depth >= 1 && log_slab + depth <= top + 1);
debug_assert!(top < log_n);
let row_len = (1 << log_slab) * width;
let tasks = values.len() / (row_len << depth);
let min_len = (tasks / (4 * current_num_threads())).max(1);
for_each_staged_tile(
values,
staged_runs(width, top, depth, log_slab),
Dispatch::Parallel { min_len },
|tile, block| {
tile_stages::<F, INVERSE>(tile, row_len, depth, top + 1 - depth, block, twiddles);
},
);
}
struct Schedule {
log_tile_rows: usize,
log_staged_rows: usize,
log_slab_rows: usize,
}
impl Schedule {
fn new<F: TowerLevel>(width: usize, log_n: usize) -> Self {
Self::for_workers::<F>(width, log_n, current_num_threads())
}
fn for_workers<F: TowerLevel>(width: usize, log_n: usize, workers: usize) -> Self {
let row_bytes = core::mem::size_of::<F>() * width;
let capacity = log2_floor_usize((DEEP_TILE_BYTES / row_bytes).max(1));
let log_tile_rows = capacity.min(log_n.saturating_sub(log2_ceil_usize(workers)));
let log_slab_rows =
log2_ceil_usize(CACHE_LINE_BYTES.div_ceil(row_bytes)).min(log_tile_rows);
let slab_bytes = row_bytes << log_slab_rows;
let log_staged_rows = if workers >= STAGED_WORKERS {
log2_floor_usize((STAGING_TILE_BYTES / slab_bytes).max(1))
} else {
0
};
Self {
log_tile_rows,
log_staged_rows,
log_slab_rows,
}
}
}
fn group_pass<F: ButterflyField, const INVERSE: bool>(
values: &mut [F],
width: usize,
log_n: usize,
low: usize,
high: usize,
log_slab: usize,
twiddles: &Twiddles<F>,
) {
if high - low == 1 {
stage_pass::<F, INVERSE>(values, width, low, twiddles);
} else {
fused_group::<F, INVERSE>(
values,
width,
log_n,
high - 1,
high - low,
log_slab,
twiddles,
);
}
}
fn run<F: ButterflyField, const INVERSE: bool>(
values: &mut [F],
width: usize,
log_n: usize,
top: usize,
twiddles: &Twiddles<F>,
schedule: &Schedule,
) {
let tile = schedule.log_tile_rows.min(top);
let slab = schedule.log_slab_rows.min(tile);
let group = if schedule.log_staged_rows >= MIN_FUSED_STAGES {
schedule.log_staged_rows
} else {
1
};
let groups = (top - tile).div_ceil(group);
let bounds = |g: usize| {
let low = tile + g * group;
(low, (low + group).min(top))
};
if INVERSE {
if tile > 0 {
deep_tiles::<F, INVERSE>(values, width, tile, twiddles);
}
for g in 0..groups {
let (low, high) = bounds(g);
group_pass::<F, INVERSE>(values, width, log_n, low, high, slab, twiddles);
}
} else {
for g in (0..groups).rev() {
let (low, high) = bounds(g);
group_pass::<F, INVERSE>(values, width, log_n, low, high, slab, twiddles);
}
if tile > 0 {
deep_tiles::<F, INVERSE>(values, width, tile, twiddles);
}
}
}
pub(crate) fn transform_stages<F: ButterflyField, const INVERSE: bool>(
values: &mut [F],
width: usize,
top: usize,
shift: F,
) {
let log_n = log2_strict_usize(values.len() / width);
assert!(log_n <= 1 << F::LOG_BITS, "domain exceeds field dimension");
assert!(top <= log_n, "stage count exceeds the domain dimension");
let twiddles = Twiddles::new(log_n, shift);
if core::mem::size_of_val(values) <= DEEP_TILE_BYTES {
for step in 0..top {
let j = if INVERSE { step } else { top - 1 - step };
stage_pass::<F, INVERSE>(values, width, j, &twiddles);
}
return;
}
let schedule = Schedule::new::<F>(width, log_n);
run::<F, INVERSE>(values, width, log_n, top, &twiddles, &schedule);
}
pub(crate) fn transform_cosets<F: ButterflyField>(
values: &mut [F],
width: usize,
log_message: usize,
) {
let len = width << log_message;
let (first, rest) = values.split_at_mut(len);
rest.par_chunks_mut(len).enumerate().for_each(|(c, coset)| {
coset.copy_from_slice(first);
let shift = domain_point::<F>((c + 1) << log_message);
transform_stages::<F, false>(coset, width, log_message, shift);
});
transform_stages::<F, false>(first, width, log_message, F::ZERO);
}
fn transform<F: ButterflyField, const INVERSE: bool>(
mut mat: RowMajorMatrix<F>,
shift: F,
) -> RowMajorMatrix<F> {
let width = mat.width();
let log_n = log2_strict_usize(mat.height());
transform_stages::<F, INVERSE>(&mut mat.values, width, log_n, shift);
mat
}
#[cfg(test)]
mod tests {
use alloc::{format, vec};
use p3_binary_field::{
BinaryField8, BinaryField16, BinaryField32, BinaryField64, BinaryField128, Ghash128,
TowerLevel,
};
use p3_field::PrimeCharacteristicRing;
use p3_matrix::Matrix;
use p3_matrix::dense::RowMajorMatrix;
use p3_util::log2_strict_usize;
use proptest::prelude::*;
use super::{
ButterflyField, DEEP_TILE_BYTES, LchNtt, MIN_FUSED_STAGES, STAGED_WORKERS, Schedule,
Twiddles, run, stage_pass, staged_runs,
};
use crate::domain::{domain_point, subspace_polynomial};
use crate::naive::NaiveAdditiveNtt;
use crate::traits::AdditiveNtt;
const WIDTHS: [usize; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 16, 64];
const BOUNDARY_WIDTHS: [usize; 5] = [1, 2, 3, 16, 64];
const LOG_HEIGHTS: core::ops::RangeInclusive<usize> = 0..=10;
const HEAVY_LOG_HEIGHTS: core::ops::RangeInclusive<usize> = 0..=14;
const SHIFTS: [u64; 2] = [0, 0x5555_1234_9abc_def0];
fn sample<F: ButterflyField>(bits: u64) -> F {
F::from_le_byte_iter(bits.to_le_bytes().into_iter().cycle())
}
fn matrix<F: ButterflyField>(log_n: usize, width: usize, seed: u64) -> RowMajorMatrix<F> {
RowMajorMatrix::new(
(0..(width << log_n))
.map(|i| {
sample::<F>(
seed.wrapping_mul(0x9e37_79b9_7f4a_7c15)
.wrapping_add(i as u64),
)
})
.collect(),
width,
)
}
fn twiddle_walk_ntt<F: ButterflyField>(
mut mat: RowMajorMatrix<F>,
shift: F,
) -> RowMajorMatrix<F> {
let width = mat.width();
let log_n = log2_strict_usize(mat.height());
for j in (0..log_n).rev() {
let half = (1 << j) * width;
let base = subspace_polynomial::<F>(j, shift);
for (blk, block) in mat.values.chunks_mut(half << 1).enumerate() {
let t = base + domain_point::<F>(blk << 1);
let (lo, hi) = block.split_at_mut(half);
for (u, v) in lo.iter_mut().zip(hi) {
*u += t * *v;
*v += *u;
}
}
}
mat
}
fn twiddle_walk_intt<F: ButterflyField>(
mut mat: RowMajorMatrix<F>,
shift: F,
) -> RowMajorMatrix<F> {
let width = mat.width();
let log_n = log2_strict_usize(mat.height());
for j in 0..log_n {
let half = (1 << j) * width;
let base = subspace_polynomial::<F>(j, shift);
for (blk, block) in mat.values.chunks_mut(half << 1).enumerate() {
let t = base + domain_point::<F>(blk << 1);
let (lo, hi) = block.split_at_mut(half);
for (u, v) in lo.iter_mut().zip(hi) {
*v += *u;
*u += t * *v;
}
}
}
mat
}
fn stage_by_stage<F: ButterflyField, const INVERSE: bool>(
mat: &mut RowMajorMatrix<F>,
twiddles: &Twiddles<F>,
) {
let width = mat.width();
let log_n = log2_strict_usize(mat.height());
stages_one_at_a_time::<F, INVERSE>(&mut mat.values, width, log_n, twiddles);
}
fn stages_one_at_a_time<F: ButterflyField, const INVERSE: bool>(
values: &mut [F],
width: usize,
top: usize,
twiddles: &Twiddles<F>,
) {
for step in 0..top {
let j = if INVERSE { step } else { top - 1 - step };
stage_pass::<F, INVERSE>(values, width, j, twiddles);
}
}
fn check_every_stage_range_agrees<F: ButterflyField>(
log_n: usize,
width: usize,
schedule: &Schedule,
) {
let coeffs = matrix::<F>(log_n, width, 3);
for shift_bits in SHIFTS {
let shift = sample::<F>(shift_bits);
let twiddles = Twiddles::new(log_n, shift);
for top in 0..=log_n {
let label = format!("log_n={log_n} width={width} shift={shift_bits:#x} top={top}");
let mut expected = coeffs.clone();
stages_one_at_a_time::<F, false>(&mut expected.values, width, top, &twiddles);
let mut blocked = coeffs.clone();
run::<F, false>(&mut blocked.values, width, log_n, top, &twiddles, schedule);
assert_eq!(blocked, expected, "forward {label}");
run::<F, true>(&mut blocked.values, width, log_n, top, &twiddles, schedule);
assert_eq!(blocked, coeffs, "round trip {label}");
let mut expected = coeffs.clone();
stages_one_at_a_time::<F, true>(&mut expected.values, width, top, &twiddles);
let mut blocked = coeffs.clone();
run::<F, true>(&mut blocked.values, width, log_n, top, &twiddles, schedule);
assert_eq!(blocked, expected, "inverse {label}");
}
}
}
fn check_schedules_agree<F: ButterflyField>(log_n: usize, width: usize, schedule: &Schedule) {
let coeffs = matrix::<F>(log_n, width, 3);
for shift_bits in SHIFTS {
let shift = sample::<F>(shift_bits);
let twiddles = Twiddles::new(log_n, shift);
let label = format!("log_n={log_n} width={width} shift={shift_bits:#x}");
let mut expected = coeffs.clone();
stage_by_stage::<F, false>(&mut expected, &twiddles);
let mut blocked = coeffs.clone();
run::<F, false>(
&mut blocked.values,
width,
log_n,
log_n,
&twiddles,
schedule,
);
assert_eq!(blocked, expected, "forward {label}");
let mut expected = coeffs.clone();
stage_by_stage::<F, true>(&mut expected, &twiddles);
let mut blocked = coeffs.clone();
run::<F, true>(
&mut blocked.values,
width,
log_n,
log_n,
&twiddles,
schedule,
);
assert_eq!(blocked, expected, "inverse {label}");
}
}
fn check_matches_naive<F: ButterflyField>(log_n: usize, width: usize, seed: u64, shift: u64) {
let coeffs = matrix::<F>(log_n, width, seed);
let shift = sample::<F>(shift);
let fast = LchNtt::<F>::default().shifted_ntt_batch(coeffs.clone(), shift);
let slow = NaiveAdditiveNtt::<F>::default().shifted_ntt_batch(coeffs, shift);
assert_eq!(fast, slow);
}
fn check_the_twiddle_walk<F: ButterflyField>(log_n: usize, shift_bits: u64) {
let shift = sample::<F>(shift_bits);
let twiddles = Twiddles::new(log_n, shift);
for stage in 0..log_n {
let blocks = 1usize << (log_n - 1 - stage);
for start in [0, 1, 3, 63, blocks.saturating_sub(5)] {
if start >= blocks {
continue;
}
let mut t = twiddles.at(stage, start);
for block in start..blocks.min(start + 5) {
let expected =
subspace_polynomial::<F>(stage, shift) + domain_point::<F>(block << 1);
assert_eq!(t, expected, "stage={stage} block={block}");
if block + 1 < blocks {
t += twiddles.step(block + 1);
}
}
}
}
}
#[test]
fn incremental_twiddles_match_independent_domain_points() {
for shift_bits in SHIFTS {
check_the_twiddle_walk::<BinaryField32>(16, shift_bits);
check_the_twiddle_walk::<BinaryField64>(16, shift_bits);
check_the_twiddle_walk::<BinaryField128>(16, shift_bits);
check_the_twiddle_walk::<Ghash128>(16, shift_bits);
}
}
fn check_walk_agrees<F: ButterflyField>(log_n: usize, width: usize, shift_bits: u64) {
let coeffs = matrix::<F>(log_n, width, 41);
let shift = sample::<F>(shift_bits);
let ntt = LchNtt::<F>::default();
let label = format!("log_n={log_n} width={width} shift={shift_bits:#x}");
assert_eq!(
ntt.shifted_ntt_batch(coeffs.clone(), shift),
twiddle_walk_ntt::<F>(coeffs.clone(), shift),
"ntt {label}"
);
assert_eq!(
ntt.shifted_intt_batch(coeffs.clone(), shift),
twiddle_walk_intt::<F>(coeffs, shift),
"intt {label}"
);
}
fn check_the_blocked_walk_agrees<F: ButterflyField>(
log_n: usize,
width: usize,
shift_bits: u64,
workers: &[usize],
) {
let coeffs = matrix::<F>(log_n, width, 41);
let shift = sample::<F>(shift_bits);
let twiddles = Twiddles::new(log_n, shift);
let walked = twiddle_walk_ntt::<F>(coeffs.clone(), shift);
for &workers in workers {
let schedule = Schedule::for_workers::<F>(width, log_n, workers);
let label =
format!("log_n={log_n} width={width} shift={shift_bits:#x} workers={workers}");
let mut blocked = coeffs.clone();
run::<F, false>(
&mut blocked.values,
width,
log_n,
log_n,
&twiddles,
&schedule,
);
assert_eq!(blocked, walked, "ntt {label}");
let mut blocked = walked.clone();
run::<F, true>(
&mut blocked.values,
width,
log_n,
log_n,
&twiddles,
&schedule,
);
assert_eq!(blocked, coeffs, "intt {label}");
}
}
fn check_oracle_agrees<F: ButterflyField>(log_n: usize, width: usize, shift_bits: u64) {
let coeffs = matrix::<F>(log_n, width, 53);
let shift = sample::<F>(shift_bits);
let fast = LchNtt::<F>::default();
let slow = NaiveAdditiveNtt::<F>::default();
let label = format!("log_n={log_n} width={width} shift={shift_bits:#x}");
assert_eq!(
fast.shifted_ntt_batch(coeffs.clone(), shift),
slow.shifted_ntt_batch(coeffs.clone(), shift),
"ntt {label}"
);
assert_eq!(
fast.shifted_intt_batch(coeffs.clone(), shift),
slow.shifted_intt_batch(coeffs, shift),
"intt {label}"
);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn ghash_packing_matches_naive_and_round_trips(
log_n in 0usize..=7,
width in 1usize..=9,
seed: u64,
shift: u64,
) {
check_matches_naive::<Ghash128>(log_n, width, seed, shift);
let coeffs = matrix::<Ghash128>(log_n, width, seed);
let shift = sample::<Ghash128>(shift);
let ntt = LchNtt::<Ghash128>::default();
let transformed = ntt.shifted_ntt_batch(coeffs.clone(), shift);
prop_assert_eq!(ntt.shifted_intt_batch(transformed, shift), coeffs);
}
#[test]
fn lch_matches_naive_at_8_bits(
log_n in 0usize..=8,
width in 1usize..=5,
seed in any::<u64>(),
shift in any::<u64>(),
) {
check_matches_naive::<BinaryField8>(log_n, width, seed, shift);
}
#[test]
fn lch_matches_naive_at_16_bits(
log_n in 0usize..=8,
width in 1usize..=5,
seed in any::<u64>(),
shift in any::<u64>(),
) {
check_matches_naive::<BinaryField16>(log_n, width, seed, shift);
}
#[test]
fn lch_matches_naive_at_32_bits(
log_n in 0usize..=8,
width in 1usize..=5,
seed in any::<u64>(),
shift in any::<u64>(),
) {
check_matches_naive::<BinaryField32>(log_n, width, seed, shift);
}
#[test]
fn lch_matches_naive_at_64_bits(
log_n in 0usize..=8,
width in 1usize..=5,
seed in any::<u64>(),
shift in any::<u64>(),
) {
check_matches_naive::<BinaryField64>(log_n, width, seed, shift);
}
#[test]
fn lch_matches_naive_at_128_bits(
log_n in 0usize..=8,
width in 1usize..=5,
seed in any::<u64>(),
shift in any::<u64>(),
) {
check_matches_naive::<BinaryField128>(log_n, width, seed, shift);
}
#[test]
fn lch_round_trips(
log_n in 0usize..=10,
width in 1usize..=3,
seed in any::<u64>(),
shift in any::<u64>(),
) {
let coeffs = matrix::<BinaryField16>(log_n, width, seed);
let shift = sample::<BinaryField16>(shift);
let ntt = LchNtt::<BinaryField16>::default();
let evals = ntt.shifted_ntt_batch(coeffs.clone(), shift);
prop_assert_eq!(ntt.shifted_intt_batch(evals, shift), coeffs);
}
#[test]
fn lch_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::<BinaryField16>(log_n, width, seed);
let shift = sample::<BinaryField16>(shift);
let lde = LchNtt::<BinaryField16>::default()
.shifted_lde_batch(coeffs.clone(), added, shift);
let naive = NaiveAdditiveNtt::<BinaryField16>::default()
.shifted_lde_batch(coeffs.clone(), added, shift);
prop_assert_eq!(&lde, &naive);
prop_assert_eq!(&lde.values[..coeffs.values.len()], &coeffs.values[..]);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(8))]
#[test]
fn lch_round_trips_past_the_blocking_threshold(
log_n in 11usize..=14,
width in 1usize..=4,
seed in any::<u64>(),
shift in any::<u64>(),
) {
let coeffs = matrix::<BinaryField32>(log_n, width, seed);
let ntt = LchNtt::<BinaryField32>::default();
let shift32 = sample::<BinaryField32>(shift);
let evals = ntt.shifted_ntt_batch(coeffs.clone(), shift32);
prop_assert_eq!(ntt.shifted_intt_batch(evals, shift32), coeffs);
let coeffs = matrix::<Ghash128>(log_n, width, seed);
let ntt = LchNtt::<Ghash128>::default();
let shift128 = sample::<Ghash128>(shift);
let evals = ntt.shifted_ntt_batch(coeffs.clone(), shift128);
prop_assert_eq!(ntt.shifted_intt_batch(evals, shift128), coeffs);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn random_padded_shapes_match_the_full_transform(
log_message in 0usize..=7,
width in 1usize..=5,
log_inv_rate in 0usize..=3,
seed in any::<u64>(),
) {
let message = matrix::<BinaryField128>(log_message, width, seed);
let mut padded = message.values;
padded.resize(padded.len() << log_inv_rate, BinaryField128::ZERO);
let padded = RowMajorMatrix::new(padded, width);
let ntt = LchNtt::<BinaryField128>::default();
let expected = ntt.ntt_batch(padded.clone());
prop_assert_eq!(ntt.ntt_batch_padded(padded, log_inv_rate), expected);
}
}
#[test]
fn lch_matches_a_twiddle_walk_across_several_tasks() {
const LOG_N: usize = 12;
let ntt = LchNtt::<BinaryField128>::default();
for width in [1usize, 3] {
for shift_bits in [0u64, 0x1234_5678_9abc_def0] {
let coeffs = matrix::<BinaryField128>(LOG_N, width, 5);
let shift = sample::<BinaryField128>(shift_bits);
let walked = twiddle_walk_ntt::<BinaryField128>(coeffs.clone(), shift);
assert_eq!(
ntt.shifted_ntt_batch(coeffs.clone(), shift),
walked,
"ntt width={width} shift={shift_bits:#x}"
);
assert_eq!(
ntt.shifted_intt_batch(walked, shift),
coeffs,
"intt width={width} shift={shift_bits:#x}"
);
}
}
}
#[test]
fn blocked_schedule_matches_the_plain_one() {
let shape = |tile: usize, staged: usize, slab: usize| Schedule {
log_tile_rows: tile,
log_staged_rows: staged,
log_slab_rows: slab,
};
for width in WIDTHS {
for log_n in [2usize, 3, 4, 5, 6, 7, 8] {
let schedule = shape(3, 2, 0);
check_schedules_agree::<BinaryField32>(log_n, width, &schedule);
check_schedules_agree::<BinaryField64>(log_n, width, &schedule);
check_schedules_agree::<BinaryField128>(log_n, width, &schedule);
check_schedules_agree::<Ghash128>(log_n, width, &schedule);
}
check_schedules_agree::<BinaryField128>(7, width, &shape(2, 4, 0));
check_schedules_agree::<BinaryField128>(8, width, &shape(2, 4, 0));
check_schedules_agree::<BinaryField128>(7, width, &shape(2, 1, 0));
check_schedules_agree::<BinaryField128>(6, width, &shape(0, 3, 0));
check_schedules_agree::<BinaryField32>(8, width, &shape(4, 3, 2));
check_schedules_agree::<BinaryField32>(9, width, &shape(3, 2, 1));
}
}
#[test]
fn production_schedule_matches_the_plain_one() {
for workers in [1usize, 32] {
for (log_n, width) in [(14usize, 3usize), (15, 3), (13, 16)] {
let schedule = Schedule::for_workers::<BinaryField32>(width, log_n, workers);
assert!(schedule.log_tile_rows < log_n, "no group above the tile");
check_schedules_agree::<BinaryField32>(log_n, width, &schedule);
}
for (log_n, width) in [(11usize, 16usize), (12, 16), (13, 5)] {
let schedule = Schedule::for_workers::<BinaryField128>(width, log_n, workers);
assert!(schedule.log_tile_rows < log_n, "no group above the tile");
check_schedules_agree::<BinaryField128>(log_n, width, &schedule);
check_schedules_agree::<Ghash128>(log_n, width, &schedule);
}
}
}
#[test]
fn a_partial_stage_range_matches_the_plain_passes() {
let shape = |tile: usize, staged: usize, slab: usize| Schedule {
log_tile_rows: tile,
log_staged_rows: staged,
log_slab_rows: slab,
};
for width in [1usize, 3, 16] {
check_every_stage_range_agrees::<BinaryField128>(8, width, &shape(3, 2, 0));
check_every_stage_range_agrees::<BinaryField128>(7, width, &shape(2, 4, 0));
check_every_stage_range_agrees::<BinaryField128>(6, width, &shape(0, 3, 0));
check_every_stage_range_agrees::<BinaryField32>(8, width, &shape(4, 3, 2));
}
for workers in [1usize, 32] {
let schedule = Schedule::for_workers::<BinaryField128>(16, 11, workers);
assert!(schedule.log_tile_rows < 11, "no group above the tile");
check_every_stage_range_agrees::<BinaryField128>(11, 16, &schedule);
}
}
fn check_the_padded_transform_agrees<F: ButterflyField>(
log_message: usize,
width: usize,
log_inv_rate: usize,
) {
let message = matrix::<F>(log_message, width, 59);
let mut padded = message.values;
padded.resize(padded.len() << log_inv_rate, F::ZERO);
let padded = RowMajorMatrix::new(padded, width);
let ntt = LchNtt::<F>::default();
let expected = ntt.ntt_batch(padded.clone());
assert_eq!(
ntt.ntt_batch_padded(padded, log_inv_rate),
expected,
"log_message={log_message} width={width} rate={log_inv_rate}"
);
}
#[test]
fn the_padded_transform_matches_the_full_one() {
for width in WIDTHS {
for log_message in 0..=5 {
for log_inv_rate in 0..=3 {
check_the_padded_transform_agrees::<BinaryField32>(
log_message,
width,
log_inv_rate,
);
check_the_padded_transform_agrees::<BinaryField128>(
log_message,
width,
log_inv_rate,
);
check_the_padded_transform_agrees::<Ghash128>(log_message, width, log_inv_rate);
}
}
}
}
#[test]
fn the_padded_transform_matches_the_reference_oracle() {
for width in [1usize, 3] {
for log_message in 0..=4 {
for log_inv_rate in 0..=2 {
let message = matrix::<BinaryField64>(log_message, width, 61);
let mut padded = message.values;
padded.resize(padded.len() << log_inv_rate, BinaryField64::ZERO);
let padded = RowMajorMatrix::new(padded, width);
let expected =
NaiveAdditiveNtt::<BinaryField64>::default().ntt_batch(padded.clone());
let actual =
LchNtt::<BinaryField64>::default().ntt_batch_padded(padded, log_inv_rate);
let label = format!("log_message={log_message} width={width}");
assert_eq!(actual, expected, "{label} rate={log_inv_rate}");
}
}
}
}
#[test]
#[should_panic = "padding exceeds matrix height"]
fn the_padded_transform_rejects_padding_past_the_height() {
let mat = matrix::<BinaryField32>(2, 4, 0);
let _ = LchNtt::<BinaryField32>::default().ntt_batch_padded(mat, 3);
}
#[test]
#[should_panic = "domain exceeds field dimension"]
fn the_padded_transform_rejects_a_domain_past_the_field_dimension() {
let mat = matrix::<BinaryField8>(9, 1, 0);
let _ = LchNtt::<BinaryField8>::default().ntt_batch_padded(mat, 1);
}
#[test]
fn the_contiguous_tile_leaves_one_for_every_worker() {
for workers in [1usize, 2, 3, 4, 14, 16, 32, 4096] {
for log_n in 0..=24usize {
for width in [1usize, 3, 16, 64, 1 << 14] {
let tile =
Schedule::for_workers::<BinaryField32>(width, log_n, workers).log_tile_rows;
assert!(tile <= log_n, "a tile taller than the matrix");
let tiles = 1usize << (log_n - tile);
assert!(
tiles >= workers.min(1 << log_n),
"workers={workers} log_n={log_n} width={width}: {tiles} tiles"
);
let row_bytes = core::mem::size_of::<BinaryField32>() * width;
let deeper_fits = (row_bytes << (tile + 1)) <= DEEP_TILE_BYTES;
assert!(
tile == log_n || !deeper_fits || tiles / 2 < workers,
"workers={workers} log_n={log_n} width={width}: tile {tile} is short"
);
}
}
}
assert_eq!(
Schedule::for_workers::<BinaryField32>(1, 16, 1).log_tile_rows,
15
);
assert_eq!(
Schedule::for_workers::<BinaryField32>(1, 16, 32).log_tile_rows,
11
);
}
#[test]
fn staging_waits_for_enough_workers() {
for workers in [1usize, STAGED_WORKERS - 1, STAGED_WORKERS] {
let schedule = Schedule::for_workers::<Ghash128>(16, 20, workers);
assert_eq!(
schedule.log_tile_rows, 9,
"a 256-byte row fills the tile budget at 2^9 rows"
);
assert_eq!(
schedule.log_staged_rows >= MIN_FUSED_STAGES,
workers >= STAGED_WORKERS,
"workers={workers}"
);
}
}
#[test]
fn a_staging_group_stages_every_row_exactly_once() {
let mut shapes = 0;
for log_n in 1..=10usize {
for top in 0..log_n {
for depth in 1..=top + 1 {
for log_slab in 0..=top + 1 - depth {
shapes += 1;
let map = staged_runs(1, top, depth, log_slab);
let mut seen = vec![false; 1 << log_n];
for task in 0..1usize << (log_n - depth - log_slab) {
for k in 0..1usize << depth {
let first = map.run_index(task, k) << log_slab;
for row in first..first + (1 << log_slab) {
let hit =
seen.get_mut(row).expect("the walk leaves the matrix");
assert!(!*hit, "row {row} staged twice");
*hit = true;
}
for s in 0..depth {
assert_eq!(
first >> (top - s + 1),
(map.block(task) << s) + (k >> (depth - s)),
"log_n={log_n} top={top} depth={depth} \
slab={log_slab} task={task} k={k} s={s}"
);
}
}
}
assert!(seen.iter().all(|&hit| hit), "a row was left unstaged");
}
}
}
}
assert_eq!(shapes, 715, "the model stopped covering every shape");
}
fn sweep_against_the_walk<F: ButterflyField>(heights: core::ops::RangeInclusive<usize>) {
for width in WIDTHS {
for log_n in heights.clone() {
for shift_bits in SHIFTS {
check_walk_agrees::<F>(log_n, width, shift_bits);
}
}
}
}
#[test]
fn the_schedule_matches_an_independent_walk_at_32_bits() {
sweep_against_the_walk::<BinaryField32>(LOG_HEIGHTS);
}
#[test]
fn the_schedule_matches_an_independent_walk_at_64_bits() {
sweep_against_the_walk::<BinaryField64>(LOG_HEIGHTS);
}
#[test]
fn the_schedule_matches_an_independent_walk_at_128_bits() {
sweep_against_the_walk::<BinaryField128>(LOG_HEIGHTS);
}
#[test]
fn the_schedule_matches_an_independent_walk_in_the_ghash_basis() {
sweep_against_the_walk::<Ghash128>(LOG_HEIGHTS);
}
#[test]
#[ignore = "serial oracle over every width and level up to 2^14; run from heavy CI"]
fn the_schedule_matches_an_independent_walk_at_every_blocked_shape() {
sweep_against_the_walk::<BinaryField32>(HEAVY_LOG_HEIGHTS);
sweep_against_the_walk::<BinaryField64>(HEAVY_LOG_HEIGHTS);
sweep_against_the_walk::<BinaryField128>(HEAVY_LOG_HEIGHTS);
sweep_against_the_walk::<Ghash128>(HEAVY_LOG_HEIGHTS);
}
fn sweep_across_the_tile_boundaries<F: ButterflyField>() {
for width in BOUNDARY_WIDTHS {
let tile = Schedule::for_workers::<F>(width, usize::BITS as usize, 1).log_tile_rows;
for log_n in [tile, tile + 1, tile + 2] {
for shift_bits in SHIFTS {
check_walk_agrees::<F>(log_n, width, shift_bits);
}
}
for shift_bits in SHIFTS {
check_the_blocked_walk_agrees::<F>(tile + 2, width, shift_bits, &[1, 32]);
}
}
}
#[test]
fn every_tile_boundary_is_crossed_at_32_bits() {
sweep_across_the_tile_boundaries::<BinaryField32>();
}
#[test]
fn every_tile_boundary_is_crossed_at_64_bits() {
sweep_across_the_tile_boundaries::<BinaryField64>();
}
#[test]
fn every_tile_boundary_is_crossed_at_128_bits() {
sweep_across_the_tile_boundaries::<BinaryField128>();
}
#[test]
fn every_tile_boundary_is_crossed_in_the_ghash_basis() {
sweep_across_the_tile_boundaries::<Ghash128>();
}
fn check_a_staged_run_of_rows<F: ButterflyField>(log_n: usize) {
let alone = Schedule::for_workers::<F>(1, log_n, 1);
assert_eq!(alone.log_tile_rows + MIN_FUSED_STAGES, log_n);
assert!(alone.log_slab_rows > 0, "the staged row is a single row");
let shared = Schedule::for_workers::<F>(1, log_n, STAGED_WORKERS);
assert!(
shared.log_staged_rows >= MIN_FUSED_STAGES,
"nothing is staged"
);
assert!(shared.log_slab_rows > 0, "the staged row is a single row");
}
#[test]
fn narrow_rows_are_staged_in_runs() {
check_a_staged_run_of_rows::<BinaryField32>(17);
check_a_staged_run_of_rows::<BinaryField64>(16);
check_a_staged_run_of_rows::<BinaryField128>(15);
check_a_staged_run_of_rows::<Ghash128>(15);
}
#[test]
fn the_transform_matches_the_reference_oracle_at_every_width() {
for width in WIDTHS {
for log_n in 0..=6 {
for shift_bits in SHIFTS {
check_oracle_agrees::<BinaryField32>(log_n, width, shift_bits);
check_oracle_agrees::<BinaryField64>(log_n, width, shift_bits);
check_oracle_agrees::<BinaryField128>(log_n, width, shift_bits);
check_oracle_agrees::<Ghash128>(log_n, width, shift_bits);
}
}
}
}
#[test]
fn a_row_wider_than_the_budgets_degrades_to_plain_passes() {
const WIDTH: usize = 16384;
let schedule = Schedule::for_workers::<BinaryField128>(WIDTH, 4, 32);
assert_eq!(schedule.log_tile_rows, 0);
assert_eq!(schedule.log_slab_rows, 0);
assert_eq!(schedule.log_staged_rows, 0);
check_schedules_agree::<BinaryField128>(2, WIDTH, &schedule);
}
#[test]
#[should_panic]
fn shifted_ntt_batch_rejects_l_past_the_bit_width() {
let coeffs = matrix::<BinaryField8>((1 << BinaryField8::LOG_BITS) + 1, 1, 0);
let _ = LchNtt::<BinaryField8>::default().ntt_batch(coeffs);
}
#[test]
#[should_panic]
fn shifted_ntt_batch_rejects_a_non_power_of_two_height() {
let coeffs = RowMajorMatrix::new(vec![sample::<BinaryField8>(0); 3], 1);
let _ = LchNtt::<BinaryField8>::default().ntt_batch(coeffs);
}
#[test]
fn lch_is_linear() {
let ntt = LchNtt::<BinaryField16>::default();
let a = matrix::<BinaryField16>(5, 2, 1);
let b = matrix::<BinaryField16>(5, 2, 2);
let sum = RowMajorMatrix::new(
a.values
.iter()
.zip(&b.values)
.map(|(x, y)| *x + *y)
.collect(),
2,
);
let lhs = ntt.ntt_batch(sum);
let rhs_a = ntt.ntt_batch(a);
let rhs_b = ntt.ntt_batch(b);
for (i, v) in lhs.values.iter().enumerate() {
assert_eq!(*v, rhs_a.values[i] + rhs_b.values[i]);
}
}
#[test]
fn levels_agree_after_embedding() {
const LOG_N: usize = 6;
let coeffs32 = matrix::<BinaryField32>(LOG_N, 1, 7);
let coeffs128 = RowMajorMatrix::new(
coeffs32
.values
.iter()
.map(|v| BinaryField128::from_repr(u128::from(v.to_repr())))
.collect(),
1,
);
let small = LchNtt::<BinaryField32>::default().ntt_batch(coeffs32);
let large = LchNtt::<BinaryField128>::default().ntt_batch(coeffs128);
for (s, l) in small.values.iter().zip(&large.values) {
assert_eq!(u128::from(s.to_repr()), l.to_repr());
}
}
#[test]
fn ghash_packing_matches_scalar_across_tasks() {
for width in [3, 5] {
for shift in [Ghash128::ZERO, sample::<Ghash128>(17)] {
let coeffs = matrix::<Ghash128>(11, width, 29);
let ntt = LchNtt::<Ghash128>::default();
let actual = ntt.shifted_ntt_batch(coeffs.clone(), shift);
assert_eq!(actual, twiddle_walk_ntt(coeffs.clone(), shift));
assert_eq!(ntt.shifted_intt_batch(actual, shift), coeffs);
}
}
}
#[test]
fn the_two_representations_of_the_widest_level_transform_alike() {
const LOG_N: usize = 7;
const WIDTH: usize = 3;
let tower_coeffs = matrix::<BinaryField128>(LOG_N, WIDTH, 11);
let ghash_coeffs = RowMajorMatrix::new(
tower_coeffs
.values
.iter()
.copied()
.map(Ghash128::from)
.collect(),
WIDTH,
);
let shift = sample::<BinaryField128>(0x0123_4567_89ab_cdef);
let tower = LchNtt::<BinaryField128>::default().shifted_ntt_batch(tower_coeffs, shift);
let ghash =
LchNtt::<Ghash128>::default().shifted_ntt_batch(ghash_coeffs, Ghash128::from(shift));
for (t, g) in tower.values.iter().zip(&ghash.values) {
assert_eq!(Ghash128::from(*t), *g);
}
}
}