use crate::{
add,
arch::word::{SignedWord, Word},
memory::{self, Memory},
Sign::{self, *},
};
use alloc::alloc::Layout;
use core::mem;
pub mod crt;
pub mod pack;
pub mod transform;
use crate::arch::ntt::{
B_PACK_CANDIDATES, B_PACK_MIN, CRT_INV_IJ, K, MAX_LOG_N, MODULI, OMEGA_MAX, P0, P1, P2,
};
use crate::mul::ntt::crt::{garner_combine, CrtAccum};
use num_modular::Reducer;
pub const THRESHOLD_NTT: usize = 4_000;
pub fn select_params(la_words: usize, lb_words: usize) -> (u32, usize, usize) {
let word_bits = Word::BITS;
let la_bits = la_words as u64 * word_bits as u64;
let lb_bits = lb_words as u64 * word_bits as u64;
let prod_2 = (MODULI[0] as u128) * (MODULI[1] as u128);
for &b_pack in B_PACK_CANDIDATES {
let coeffs_a = (la_bits + b_pack as u64 - 1) / b_pack as u64;
let coeffs_b = (lb_bits + b_pack as u64 - 1) / b_pack as u64;
let total_coeffs = (coeffs_a + coeffs_b - 1) as usize;
let n = total_coeffs.next_power_of_two().max(2);
if (n.trailing_zeros()) > MAX_LOG_N {
continue;
}
let coeff_max = (1u128 << b_pack) - 1;
let max_coeff = coeff_max
.checked_mul(coeff_max)
.and_then(|sq| (n as u128 / 2).checked_mul(sq));
let k_eff = match max_coeff {
Some(mc) if mc < prod_2 => 2,
_ => K,
};
return (b_pack, n, k_eff);
}
unreachable!(
"b_pack = {} always passes the headroom check",
B_PACK_CANDIDATES.last().unwrap()
)
}
pub fn bit_len(words: &[Word]) -> u64 {
let leading_zeros = words.iter().rev().take_while(|&&w| w == 0).count();
let used = words.len() - leading_zeros;
if used == 0 {
return 0;
}
let hi_word = words[used - 1];
let hi_bits = Word::BITS - hi_word.leading_zeros();
(used as u64 - 1) * Word::BITS as u64 + hi_bits as u64
}
pub fn coeff_count(bit_len: u64, b_pack: u32) -> usize {
((bit_len + b_pack as u64 - 1) / b_pack as u64) as usize
}
pub fn memory_requirement_up_to(total_len: usize, _smaller_len: usize) -> Layout {
use crate::arch::ntt::Lane;
let word_bits = Word::BITS;
let max_coeffs =
(total_len as u64 * word_bits as u64 + B_PACK_MIN as u64 - 1) / B_PACK_MIN as u64;
let n_max = ((max_coeffs + 1) as usize).next_power_of_two().max(2);
let lanes = 2 * n_max;
let residues = K * n_max;
let twiddles = n_max;
let product = total_len;
let lane_bytes = mem::size_of::<Lane>();
let word_bytes = mem::size_of::<Word>();
let lanes_words = lanes * lane_bytes / word_bytes;
let residues_words = residues * lane_bytes / word_bytes;
let twiddles_words = twiddles * lane_bytes / word_bytes;
let total_words = product + lanes_words + residues_words + twiddles_words;
memory::array_layout::<Word>(total_words)
}
#[must_use]
#[inline]
pub fn add_signed_mul_same_len(
c: &mut [Word],
sign: Sign,
a: &[Word],
b: &[Word],
memory: &mut Memory,
) -> SignedWord {
let n = a.len();
debug_assert!(b.len() == n && c.len() == 2 * n);
add_signed_mul_conv(c, sign, a, b, memory)
}
#[must_use]
#[inline]
pub fn add_signed_mul(
c: &mut [Word],
sign: Sign,
a: &[Word],
b: &[Word],
memory: &mut Memory,
) -> SignedWord {
debug_assert!(a.len() >= b.len() && c.len() == a.len() + b.len());
if a.len() > 2 * b.len() {
return add_signed_mul_chunked(c, sign, a, b, memory);
}
add_signed_mul_conv(c, sign, a, b, memory)
}
fn add_signed_mul_chunked(
c: &mut [Word],
sign: Sign,
a: &[Word],
b: &[Word],
memory: &mut Memory,
) -> SignedWord {
use crate::arch::ntt::Lane;
use crate::mul::helpers::add_signed_mul_split_into_chunks;
let lb = b.len();
let chunk_len = lb * 2;
let (b_pack, nn_chunk, k_eff) = select_params(chunk_len, lb);
let b_hat_len = k_eff * nn_chunk;
let twiddle_len = k_eff * (nn_chunk / 2);
let (b_hat, mut mem) = memory.allocate_slice_fill::<Lane>(b_hat_len, 0);
let (fwd_tw_cache, mut mem) = mem.allocate_slice_fill::<Lane>(twiddle_len, 0);
let (inv_tw_cache, mut mem) = mem.allocate_slice_fill::<Lane>(twiddle_len, 0);
let geom = NttGeometry {
nn: nn_chunk,
b_pack,
k_eff,
output_coeffs: 0, };
prepare_b_hat_and_twiddles(b_hat, fwd_tw_cache, inv_tw_cache, b, &geom, &mut mem);
let lb_bits = bit_len(b);
let coeffs_b = coeff_count(lb_bits, b_pack);
add_signed_mul_split_into_chunks(
c,
sign,
a,
b,
chunk_len,
&mut mem,
|c_slice, sign, a_chunk, b, mem| {
let a_bits = bit_len(a_chunk);
if a_bits == 0 {
return 0;
}
let coeffs_a = coeff_count(a_bits, b_pack);
let output_coeffs = coeffs_a + coeffs_b - 1;
let out_words = a_chunk.len() + b.len();
let geom = NttGeometry {
nn: nn_chunk,
b_pack,
k_eff,
output_coeffs,
};
run_ntt_pipeline(
a_chunk,
b_hat,
fwd_tw_cache,
inv_tw_cache,
&geom,
out_words,
c_slice,
sign,
mem,
)
},
)
}
#[allow(clippy::too_many_arguments)]
fn run_ntt_pipeline(
a: &[Word],
b_hat: &[crate::arch::ntt::Lane],
fwd_tw_cache: &[crate::arch::ntt::Lane],
inv_tw_cache: &[crate::arch::ntt::Lane],
geom: &NttGeometry,
out_words: usize,
c_out: &mut [Word],
sign: Sign,
mem: &mut Memory,
) -> SignedWord {
use crate::arch::ntt::Lane;
let nn = geom.nn;
let k_eff = geom.k_eff;
let (prod, mut m) = mem.allocate_slice_fill::<Word>(out_words, 0);
let (residues, mut m) = m.allocate_slice_fill::<Lane>(k_eff * nn, 0);
let (a_lane, mut m) = m.allocate_slice_fill::<Lane>(nn, 0);
let (b_lane, mut m) = m.allocate_slice_fill::<Lane>(nn, 0);
let (fwd_twiddles, mut m) = m.allocate_slice_fill::<Lane>(nn / 2, 0);
let (inv_twiddles, _) = m.allocate_slice_fill::<Lane>(nn / 2, 0);
let mut ctx = TransformCtx {
a_lane,
b_lane,
fwd_twiddles,
inv_twiddles,
geom: NttGeometry { ..*geom },
};
for pi in 0..k_eff {
let tw_off = pi * (nn / 2);
ctx.fwd_twiddles
.copy_from_slice(&fwd_tw_cache[tw_off..tw_off + nn / 2]);
ctx.inv_twiddles
.copy_from_slice(&inv_tw_cache[tw_off..tw_off + nn / 2]);
let b_hat_slice = &b_hat[pi * nn..(pi + 1) * nn];
match pi {
0 => process_prime(a, b_hat_slice, &mut ctx, residues, pi, &P0),
1 => process_prime(a, b_hat_slice, &mut ctx, residues, pi, &P1),
2 => process_prime(a, b_hat_slice, &mut ctx, residues, pi, &P2),
_ => unreachable!(),
}
}
do_crt::<crate::arch::word::TripleWord>(prod, residues, &ctx.geom, &MODULI, &CRT_INV_IJ);
match sign {
Positive => add::add_signed_in_place(&mut c_out[..out_words], Positive, &prod[..out_words]),
Negative => add::add_signed_in_place(&mut c_out[..out_words], Negative, &prod[..out_words]),
}
}
fn add_signed_mul_conv(
c: &mut [Word],
sign: Sign,
a: &[Word],
b: &[Word],
memory: &mut Memory,
) -> SignedWord {
use crate::arch::ntt::Lane;
let la = a.len();
let lb = b.len();
debug_assert!(la > 0 && lb > 0);
let (b_pack, nn, k_eff) = select_params(la, lb);
let la_bits = bit_len(a);
let lb_bits = bit_len(b);
debug_assert!(la_bits > 0 && lb_bits > 0);
let coeffs_a = coeff_count(la_bits, b_pack);
let coeffs_b = coeff_count(lb_bits, b_pack);
let output_coeffs = coeffs_a + coeffs_b - 1;
let b_hat_len = k_eff * nn;
let twiddle_len = k_eff * (nn / 2);
let (b_hat, mut mem) = memory.allocate_slice_fill::<Lane>(b_hat_len, 0);
let (fwd_tw_cache, mut mem) = mem.allocate_slice_fill::<Lane>(twiddle_len, 0);
let (inv_tw_cache, mut mem) = mem.allocate_slice_fill::<Lane>(twiddle_len, 0);
let geom = NttGeometry {
nn,
b_pack,
k_eff,
output_coeffs,
};
prepare_b_hat_and_twiddles(b_hat, fwd_tw_cache, inv_tw_cache, b, &geom, &mut mem);
run_ntt_pipeline(a, b_hat, fwd_tw_cache, inv_tw_cache, &geom, la + lb, c, sign, &mut mem)
}
pub fn do_crt<A: CrtAccum>(
prod: &mut [Word],
residues: &[A::Lane],
geom: &NttGeometry,
primes: &[A::Lane; K],
crt_inv: &[[A::Lane; K]; K],
) {
let g = geom;
for k in 0..g.output_coeffs {
let mut coeff_residues = [A::Lane::default(); 3];
#[allow(clippy::needless_range_loop)]
for pi in 0..g.k_eff {
coeff_residues[pi] = residues[pi * g.nn + k];
}
let crt_val = garner_combine::<A>(&coeff_residues[..g.k_eff], crt_inv, primes);
let mut crt_buf = [Word::default(); 6];
let crt_n = crt_val.write_words(&mut crt_buf);
add_shifted_to_prod(prod, &crt_buf[..crt_n as usize], crt_n, k, g.b_pack);
}
}
pub struct NttGeometry {
pub nn: usize,
pub b_pack: u32,
pub k_eff: usize,
pub output_coeffs: usize,
}
struct TransformCtx<'a> {
a_lane: &'a mut [crate::arch::ntt::Lane],
b_lane: &'a mut [crate::arch::ntt::Lane],
fwd_twiddles: &'a mut [crate::arch::ntt::Lane],
inv_twiddles: &'a mut [crate::arch::ntt::Lane],
geom: NttGeometry,
}
fn transform_b_forward<R: Reducer<crate::arch::ntt::Lane>>(
b_lane: &mut [crate::arch::ntt::Lane],
b: &[Word],
nn: usize,
b_pack: u32,
fwd_twiddles: &[crate::arch::ntt::Lane],
r: &R,
) {
pack::pack(b_lane, b, b_pack, nn);
for c in b_lane[..nn].iter_mut() {
*c = r.transform(*c);
}
transform::bit_reverse(&mut b_lane[..nn]);
transform::forward(&mut b_lane[..nn], fwd_twiddles, r);
}
fn prepare_b_hat_and_twiddles(
b_hat: &mut [crate::arch::ntt::Lane],
fwd_tw_cache: &mut [crate::arch::ntt::Lane],
inv_tw_cache: &mut [crate::arch::ntt::Lane],
b: &[Word],
geom: &NttGeometry,
mem: &mut Memory,
) {
use crate::arch::ntt::Lane;
let nn = geom.nn;
let b_pack = geom.b_pack;
let k_eff = geom.k_eff;
for (pi, &omega) in OMEGA_MAX.iter().enumerate().take(k_eff) {
let (b_lane, mut rest) = mem.allocate_slice_fill::<Lane>(nn, 0);
let (fwd_tw, mut rest) = rest.allocate_slice_fill::<Lane>(nn / 2, 0);
let (inv_tw, _) = rest.allocate_slice_fill::<Lane>(nn / 2, 0);
match pi {
0 => {
transform::precompute_twiddles(fwd_tw, nn, omega, false, &P0);
transform::precompute_twiddles(inv_tw, nn, omega, true, &P0);
transform_b_forward(b_lane, b, nn, b_pack, fwd_tw, &P0);
}
1 => {
transform::precompute_twiddles(fwd_tw, nn, omega, false, &P1);
transform::precompute_twiddles(inv_tw, nn, omega, true, &P1);
transform_b_forward(b_lane, b, nn, b_pack, fwd_tw, &P1);
}
2 => {
transform::precompute_twiddles(fwd_tw, nn, omega, false, &P2);
transform::precompute_twiddles(inv_tw, nn, omega, true, &P2);
transform_b_forward(b_lane, b, nn, b_pack, fwd_tw, &P2);
}
_ => unreachable!(),
}
let b_off = pi * nn;
let tw_off = pi * (nn / 2);
b_hat[b_off..b_off + nn].copy_from_slice(b_lane);
fwd_tw_cache[tw_off..tw_off + nn / 2].copy_from_slice(fwd_tw);
inv_tw_cache[tw_off..tw_off + nn / 2].copy_from_slice(inv_tw);
}
}
fn process_prime<R: Reducer<crate::arch::ntt::Lane>>(
a: &[Word],
b_hat_slice: &[crate::arch::ntt::Lane],
ctx: &mut TransformCtx<'_>,
residues: &mut [crate::arch::ntt::Lane],
pi: usize,
r: &R,
) {
let nn = ctx.geom.nn;
let b_pack = ctx.geom.b_pack;
pack::pack(ctx.a_lane, a, b_pack, nn);
for c in ctx.a_lane[..nn].iter_mut() {
*c = r.transform(*c);
}
transform::bit_reverse(&mut ctx.a_lane[..nn]);
transform::forward(&mut ctx.a_lane[..nn], ctx.fwd_twiddles, r);
ctx.b_lane[..nn].copy_from_slice(b_hat_slice);
transform::pointwise_mul(&mut ctx.a_lane[..nn], &ctx.b_lane[..nn], r);
transform::inverse(&mut ctx.a_lane[..nn], ctx.inv_twiddles, r);
for c in ctx.a_lane[..nn].iter_mut() {
*c = r.residue(*c);
}
let offset = pi * nn;
residues[offset..offset + nn].copy_from_slice(&ctx.a_lane[..nn]);
}
pub fn add_shifted_to_prod(prod: &mut [Word], words: &[Word], count: u32, k: usize, b_pack: u32) {
let shift_bits = (k as u32).wrapping_mul(b_pack);
let word_bits = Word::BITS;
let start_idx = (shift_bits / word_bits) as usize;
let bit_shift = shift_bits % word_bits;
let mut carry: Word = 0;
for (vi, &word) in words.iter().enumerate().take(count as usize) {
let limb = word.wrapping_add(carry);
let idx = start_idx + vi;
if idx >= prod.len() {
return;
}
if bit_shift == 0 {
let (r, c) = prod[idx].overflowing_add(limb);
prod[idx] = r;
carry = Word::from(c);
} else {
let val = (limb as u128) << bit_shift;
let lo = val as Word;
let hi = (val >> word_bits) as Word;
let (r, c1) = prod[idx].overflowing_add(lo);
prod[idx] = r;
carry = Word::from(c1).wrapping_add(hi);
if idx + 1 < prod.len() && carry != 0 {
let (r2, c2) = prod[idx + 1].overflowing_add(carry);
prod[idx + 1] = r2;
carry = Word::from(c2);
}
}
}
let mut idx = start_idx + count as usize;
while carry != 0 && idx < prod.len() {
let (r, c) = prod[idx].overflowing_add(carry);
prod[idx] = r;
carry = Word::from(c);
idx += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[test]
fn test_select_params_small() {
let (b_pack, n, k_eff) = select_params(10, 10);
assert!(b_pack >= 32);
assert!(n >= 2 && n.is_power_of_two());
assert!((2..=K).contains(&k_eff));
}
#[test]
fn test_select_params_large() {
let (b_pack, n, _k_eff) = select_params(THRESHOLD_NTT, THRESHOLD_NTT);
assert!(b_pack >= 32);
assert!(n.is_power_of_two());
let coeffs_a =
(THRESHOLD_NTT * Word::BITS as usize + b_pack as usize - 1) / b_pack as usize;
let coeffs_b = coeffs_a;
let min_n = (coeffs_a + coeffs_b).next_power_of_two().max(2);
assert!(n >= min_n, "n={n} < min_n={min_n}");
}
#[test]
fn test_bit_len() {
assert_eq!(bit_len(&[]), 0);
assert_eq!(bit_len(&[0]), 0);
assert_eq!(bit_len(&[1]), 1);
}
#[test]
fn test_ntt_sign_negative() {
let a: Vec<Word> = (0..30).map(|i| (i as Word + 1) * 100).collect();
let b: Vec<Word> = (0..30).map(|i| (i as Word + 1) * 200).collect();
let mut c = vec![0u64 as Word; a.len() + b.len()];
let layout = memory_requirement_up_to(c.len(), b.len());
let mut alloc = crate::memory::MemoryAllocation::new(layout);
let mut memory = alloc.memory();
let _ = add_signed_mul_conv(&mut c, Positive, &a, &b, &mut memory);
let layout2 = memory_requirement_up_to(c.len(), b.len());
let mut alloc2 = crate::memory::MemoryAllocation::new(layout2);
let mut memory2 = alloc2.memory();
let _ = add_signed_mul_conv(&mut c, Negative, &a, &b, &mut memory2);
assert!(c.iter().all(|&w| w == 0));
}
fn schoolbook_mul(a: &[Word], b: &[Word]) -> Vec<Word> {
let mut c = vec![0u64 as Word; a.len() + b.len()];
for (i, &ai) in a.iter().enumerate() {
let mut carry: u128 = 0;
for (j, &bj) in b.iter().enumerate() {
let idx = i + j;
let prod = (ai as u128) * (bj as u128) + (c[idx] as u128) + carry;
c[idx] = prod as Word;
carry = prod >> Word::BITS;
}
let mut k = i + b.len();
while carry != 0 {
let sum = (c[k] as u128) + carry;
c[k] = sum as Word;
carry = sum >> Word::BITS;
k += 1;
}
}
c
}
fn run_ntt_vs_schoolbook(la: usize, lb: usize) {
let a: Vec<Word> = (0..la)
.map(|i| (i as Word + 1).wrapping_mul(0x9E3779B97F4A7C15u64 as Word))
.collect();
let b: Vec<Word> = (0..lb)
.map(|i| (i as Word + 1).wrapping_mul(0xC6A4A7935BD1E995u64 as Word))
.collect();
let expected = schoolbook_mul(&a, &b);
let mut c = vec![0u64 as Word; a.len() + b.len()];
let layout = memory_requirement_up_to(c.len(), b.len());
let mut alloc = crate::memory::MemoryAllocation::new(layout);
let mut memory = alloc.memory();
let carry = add_signed_mul_conv(&mut c, Positive, &a, &b, &mut memory);
assert_eq!(carry, 0, "carry should be 0");
assert_eq!(&c[..], &expected[..], "NTT mismatch: la={la}, lb={lb}");
}
#[test]
fn test_ntt_vs_schoolbook_equal() {
for &len in &[20, 30, 50, 64, 100, 128] {
run_ntt_vs_schoolbook(len, len);
}
}
#[test]
fn test_ntt_vs_schoolbook_unequal() {
for &(la, lb) in &[(30, 20), (50, 30), (100, 50), (128, 64), (100, 20)] {
run_ntt_vs_schoolbook(la, lb);
}
}
#[test]
fn test_ntt_vs_schoolbook_asymmetric() {
for &(la, lb) in &[(200, 30), (150, 20)] {
run_ntt_vs_schoolbook(la, lb);
}
}
#[test]
fn test_ntt_all_ones() {
for &len in &[20, 50] {
let a = vec![Word::MAX; len];
let b = vec![Word::MAX; len];
let expected = schoolbook_mul(&a, &b);
let mut c = vec![0u64 as Word; a.len() + b.len()];
let layout = memory_requirement_up_to(c.len(), b.len());
let mut alloc = crate::memory::MemoryAllocation::new(layout);
let mut memory = alloc.memory();
add_signed_mul_conv(&mut c, Positive, &a, &b, &mut memory);
assert_eq!(&c[..], &expected[..], "all-ones mismatch len={len}");
}
}
#[test]
fn test_ntt_high_low_zero_limbs() {
let mut a = vec![0u64 as Word; 80];
let mut b = vec![0u64 as Word; 80];
for i in 20..60 {
a[i] = (i as Word + 1).wrapping_mul(0xDEADBEEF);
b[i] = (i as Word + 1).wrapping_mul(0xCAFEBABE);
}
let expected = schoolbook_mul(&a, &b);
let mut c = vec![0u64 as Word; a.len() + b.len()];
let layout = memory_requirement_up_to(c.len(), b.len());
let mut alloc = crate::memory::MemoryAllocation::new(layout);
let mut memory = alloc.memory();
add_signed_mul_conv(&mut c, Positive, &a, &b, &mut memory);
assert_eq!(&c[..], &expected[..], "sparse operand mismatch");
}
}