Este tutorial describe el NTT industrial actual de ntt.H. En esta fase el
objetivo es el nucleo exacto de la transformada y su primera capa de algebra
formal: planes reutilizables, aritmetica Montgomery en el butterfly, soporte
de tamano arbitrario via Bluestein cuando el modulo admite las raices
necesarias, lotes, paralelismo explicito con ThreadPool, reconstruccion
exacta multi-primo via CRT, operaciones de polinomios modulo p y
multiplicacion de enteros grandes por bloques de base fija, incluyendo
convolucion negaciclica.
La Number Theoretic Transform es el analogo modular de la FFT. En vez de
trabajar con numeros complejos, trabaja con residuos modulo un primo p:
- Dominio FFT:
std::complex<Real> - Dominio NTT:
uint64_tmodulop - Resultado FFT: aproximado (redondeo de punto flotante)
- Resultado NTT: exacto modulo
p
Para una transformada de tamano N = 2^k, el modulo debe satisfacer
N | (p - 1) para que exista una raiz primitiva N-esima de la unidad en
Z / p Z.
En ese caso, la transformada directa e inversa toman la misma forma algebraica que la DFT clasica, pero sobre un campo finito:
F[k] = sum_{j=0}^{N-1} f[j] * w^(j*k) mod p
f[j] = N^(-1) * sum_{k=0}^{N-1} F[k] * w^(-j*k) mod p
donde w es una raiz primitiva N-esima de la unidad y N^(-1) es el
inverso modular de N.
| Aspecto | FFT | NTT |
|---|---|---|
| Dominio | std::complex<Real> |
uint64_t mod p |
| Twiddles | exp(-2 pi i / N) |
g^((p - 1) / N) mod p |
| Precision | Aproximada | Exacta modulo p |
| Restriccion de tamano | Cualquier N con Bluestein |
Requiere que existan las raices en Z/pZ |
| Caso de uso principal | DSP, espectros, filtros | Productos exactos modulares, CRT, algebra formal |
| Overflow / rango | Error numerico acumulado | Coeficientes acotados por p o por el rango CRT |
El header esta organizado por capas:
NTT<MOD, ROOT>: core secuencial y paralelo de transformada/moduloNTT<MOD, ROOT>::Plan: cache reutilizable para bit-reversal, twiddles o datos de Bluesteinmodular_arithmetic.H:MontgomeryCtx,mont_mul()ymont_reduce()para el hot loop del butterfly- dispatch SIMD: AVX2/NEON en tiempo de ejecucion sobre el camino potencia-de-dos
NTTExact: paquete de tres primos con reconstruccion CRT en__uint128_t- capa algebraica: series formales, evaluacion multipunto, interpolacion, bigint y convolucion negaciclica
La superficie publica de esta fase ofrece:
NTT<MOD, ROOT>::transform()ytransformed()NTT<MOD, ROOT>::multiply()ymultiply_inplace()NTT<MOD, ROOT>::transform_batch()NTT<MOD, ROOT>::ptransform()ypmultiply()NTT<MOD, ROOT>::Planpara tamanos repetidosNTT<MOD, ROOT>::simd_backend_name(),avx2_dispatch_available()yneon_dispatch_available()NTTExact::multiply()yNTTExact::pmultiply()para productos exactos en__uint128_tNTT<MOD, ROOT>::poly_eval(),poly_inverse(),poly_divmod()NTT<MOD, ROOT>::poly_log(),poly_exp(),poly_sqrt()ypoly_power()NTT<MOD, ROOT>::multipoint_eval()einterpolate()NTT<MOD, ROOT>::bigint_multiply()ypbigint_multiply()para enteros grandes representados como digitos en baseBNTT<MOD, ROOT>::negacyclic_multiply()para productos modulox^N + 1
El alcance actual es deliberadamente estricto:
- las potencias de dos usan el kernel directo de Cooley-Tukey
- los tamanos no potencia de dos usan Bluestein cuando
2 * NdivideMOD - 1 - el tamano interno de convolucion de Bluestein debe seguir cabiendo en la mayor potencia de dos soportada por el modulo
- todos los coeficientes se reducen modulo
MOD - el SIMD actual acelera el butterfly potencia-de-dos agrupando sumas/restas en AVX2 o NEON y deja la multiplicacion Montgomery por lane
- las rutas paralelas con
ThreadPoolsiguen prefiriendo el kernel escalar para evitar mezclar paralelismo por etapas con dispatch SIMD NTTExactusa tres primos fijos y solo acepta entradas que una cota conservadora puede garantizar dentro del rango CRT- el algebra formal trabaja sobre series truncadas modulo
x^ny exige las precondiciones clasicas de invertibilidad, logaritmo, exponencial y raiz cuadrada en anillos formales - la API bigint recibe digitos little-endian (
a[0]menos significativo), normaliza ceros altos y devuelve{0}cuando el producto es cero - la convolucion negaciclica exige
a.size() == b.size() == N,Npotencia de dos y una raiz primitiva de orden2Nbajo el modulo actual
# include <ntt.H>
using DefaultNTT = Aleph::NTT<>;
Aleph::Array<uint64_t> a = {1, 2, 3};
Aleph::Array<uint64_t> b = {4, 5};
auto c = DefaultNTT::multiply(a, b);
// c = {4, 13, 22, 15} modulo 998244353Para transformar directamente:
Aleph::Array<uint64_t> values = {3, 1, 4, 1, 5, 9, 2, 6};
DefaultNTT::transform(values, false); // NTT directa
DefaultNTT::transform(values, true); // NTT inversaDespues de la inversa, values vuelve a los coeficientes originales modulo
MOD.
Si vas a transformar muchas veces el mismo tamano, usa un plan:
using DefaultNTT = Aleph::NTT<>;
DefaultNTT::Plan plan(7);
Aleph::Array<uint64_t> x = ...; // size 7
Aleph::Array<uint64_t> y = ...; // size 7
plan.transform(x, false);
plan.transform(y, false);Para tamanos potencia de dos, Plan precomputa:
- permutacion bit-reversal
- twiddles forward en forma Montgomery
- twiddles inverse en forma Montgomery
N^{-1} mod MODpara normalizar la inversa
Para tamanos no potencia de dos soportados, Plan cambia a Bluestein y
precomputa:
- factores chirp
- kernels de convolucion ya transformados
- un plan interno potencia de dos para la convolucion
La transformada ejecuta una multiplicacion modular por butterfly. En esta
fase esas multiplicaciones usan MontgomeryCtx, mont_mul() y mont_reduce()
de modular_arithmetic.H.
La consecuencia practica es:
- las sumas y restas siguen siendo modulares ordinarias
- las multiplicaciones evitan reduccion generica basada en division dentro del hot loop
La API publica sigue trabajando con residuos normales, pero el nucleo corre en forma Montgomery internamente.
Cuando muchas entradas comparten el mismo tamano, las APIs batch evitan reconstruir el plan y hacen explicita la intencion:
Aleph::NTT<>::Plan plan(8);
Aleph::Array<Aleph::Array<uint64_t>> batch = {
{1, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 0, 0, 0, 0, 0, 0},
{1, 1, 1, 1, 0, 0, 0, 0}
};
plan.transform_batch(batch, false);Todos los items del lote deben coincidir exactamente con el tamano del plan.
Las variantes paralelas usan ThreadPool:
# include <thread_pool.H>
Aleph::ThreadPool pool(4);
Aleph::NTT<>::Plan plan(1 << 12);
Aleph::Array<uint64_t> values = ...;
plan.ptransform(pool, values, false);
auto product = Aleph::NTT<>::pmultiply(pool, a, b);La estrategia actual es:
- una sola transformada: paralelizar el trabajo dentro de las etapas
- un lote: paralelizar a traves de los items cuando conviene
Asi se evita oversubscription por anidamiento.
El backend SIMD activo puede consultarse en tiempo de ejecucion:
using DefaultNTT = Aleph::NTT<>;
const char * backend = DefaultNTT::simd_backend_name();
bool has_avx2 = DefaultNTT::avx2_dispatch_available();
bool has_neon = DefaultNTT::neon_dispatch_available();Politica actual:
ALEPH_NTT_SIMD=auto|scalar|avx2|neonautoselecciona el mejor backend disponible- el SIMD solo se aplica al camino potencia-de-dos
- Bluestein y las variantes multihilo siguen usando el kernel escalar
Cuando los coeficientes reales del producto pueden exceder un solo primo,
NTTExact ejecuta tres NTT independientes y reconstruye cada coeficiente en
__uint128_t:
Aleph::Array<uint64_t> a = {
1000000000000ULL,
Aleph::NTT<>::mod + 77ULL,
1234567890123ULL
};
Aleph::Array<uint64_t> b = {
777777777777ULL,
42ULL,
Aleph::NTT<>::mod + 11ULL
};
Aleph::Array<__uint128_t> exact = Aleph::NTTExact::multiply(a, b);Contrato importante:
- la reconstruccion usa los primos
998244353,469762049y1004535809 - el resultado es exacto solo si cada coeficiente verdadero es
< p1 * p2 * p3 - la implementacion verifica una cota suficiente conservadora y lanza
std::invalid_argumentcuando no puede garantizar exactitud - la variante paralela
NTTExact::pmultiply(pool, a, b)paraleliza primero los tres primos y luego la reconstruccion CRT
El header expone dos utilidades practicas:
using DefaultNTT = Aleph::NTT<>;
auto max_n = DefaultNTT::max_transform_size();
bool ok = DefaultNTT::supports_size(1024);
auto w = DefaultNTT::primitive_root_of_unity(1024);Para el modulo por defecto 998244353:
- el maximo tamano directo potencia de dos es
2^23 - tamanos como
7,14,17y119tambien estan soportados - tamanos como
3,10o13no lo estan porque las raices necesarias no existen bajo ese modulo
Sobre el mismo nucleo NTT, ntt.H ahora expone primitivas clasicas de algebra
de polinomios modulo MOD.
using DefaultNTT = Aleph::NTT<>;
Aleph::Array<uint64_t> f = {9, 7, 5, 3};
uint64_t value = DefaultNTT::poly_eval(f, 9);
auto qr = DefaultNTT::poly_divmod(f, Aleph::Array<uint64_t>({1, 1, 1}));
Aleph::Array<uint64_t> q = qr.first;
Aleph::Array<uint64_t> r = qr.second;poly_divmod() devuelve cociente y residuo normalizados. Si el divisor es el
polinomio cero, la implementacion lanza std::invalid_argument.
Las operaciones siguientes trabajan truncadas modulo x^n:
Aleph::Array<uint64_t> unit = {1, 3, 4, 0, 2, 1};
auto inv = DefaultNTT::poly_inverse(unit, 6);
auto lg = DefaultNTT::poly_log(unit, 6);
auto ex = DefaultNTT::poly_exp(Aleph::Array<uint64_t>({0, 5, 7, 0}), 6);
auto pw = DefaultNTT::poly_power(unit, 3, 6);Contrato algebraico:
poly_inverse(f, n)exigef(0) != 0 mod MODpoly_log(f, n)exigef(0) = 1poly_exp(f, n)exigef(0) = 0poly_power(f, k, n)usa la descomposicion formal habitual con desplazamiento por ceros iniciales y cubre tambienk = 0
Internamente:
- la inversa usa Newton doubling
poly_log()usaf' / fseguido de integral formalpoly_exp()itera cong *= (1 - log(g) + f)poly_power()reduce alog/expdespues de separar el termino lider
Aleph::Array<uint64_t> square = {4, 12, 25, 24, 16};
auto root = DefaultNTT::poly_sqrt(square, 5);poly_sqrt() soporta dos casos:
- termino constante no nulo con raiz modular, resuelto con Tonelli-Shanks y Newton
- polinomios con ceros iniciales en grado par, donde la raiz se reduce
recursivamente tras factorizar
x^(2m)
Si la primera potencia no nula aparece en grado impar o el termino constante no
es un residuo cuadratico modulo MOD, la operacion lanza
std::invalid_argument.
Aleph::Array<uint64_t> points = {2, 4, 8, 16};
Aleph::Array<uint64_t> values = DefaultNTT::multipoint_eval(f, points);
Aleph::Array<uint64_t> recovered = DefaultNTT::interpolate(points, values);La implementacion usa un arbol de subproductos:
multipoint_eval()reduce por residuos sucesivos contra cada nodointerpolate()evalua la derivada del producto total en los puntos y combina recursivamente los subpolinomios
multipoint_eval() acepta puntos repetidos. interpolate() si exige puntos
distintos modulo MOD; si hay duplicados, lanza std::invalid_argument.
La misma infraestructura exacta sirve para multiplicar enteros no negativos
representados como digitos en base B.
using DefaultNTT = Aleph::NTT<>;
Aleph::Array<uint64_t> a = {0, 9, 8, 7, 6}; // 67890 en base 10
Aleph::Array<uint64_t> b = {0, 1, 2, 3, 4}; // 43210 en base 10
auto product = DefaultNTT::bigint_multiply<10>(a, b);Contrato de representacion:
a[0]yb[0]son los digitos menos significativos- cada digito debe cumplir
0 <= digit < Base - la salida queda normalizada sin ceros altos, salvo el cero que se representa
como
{0} - la base por defecto es
2^15, util para empaquetar bloques densos enuint64_t
Seleccion de backend exacto:
- si
min(a.size(), b.size()) * (Base - 1)^2 < MODy el tamano de convolucion cabe enNTT<MOD, ROOT>, se usa un solo primo - en caso contrario, la API cae a
NTTExacty reconstruye coeficientes exactos antes de propagar carries pbigint_multiply()paraleliza la convolucion; la propagacion de carry sigue siendo un barrido lineal
Para enteros decimales grandes, Base = 10 es comodo para pruebas y demos.
Para cargas reales, la base por defecto reduce el numero total de digitos.
ntt.H tambien expone multiplicacion modulo (x^N + 1), util para
criptografia basada en reticulos y para kernels estilo Schonhage-Strassen.
using DefaultNTT = Aleph::NTT<>;
Aleph::Array<uint64_t> a = {1, 1, 1, 1};
Aleph::Array<uint64_t> b = {1, 1, 0, 0};
auto c = DefaultNTT::negacyclic_multiply(a, b);
// c = {0, 2, 2, 2} modulo 998244353Contrato:
a.size() == b.size() == NNdebe ser potencia de dos- debe existir una raiz primitiva de orden
2N, asi que en la practica se requiere2N <= max_transform_size()para modulos potencia-de-dos-friendly
La implementacion usa el esquema clasico:
- twist por una raiz
2N-esimazeta - NTT directa de tamano
Nsobrea[i] * zeta^iyb[i] * zeta^i - producto punto a punto
- NTT inversa y untwist por
zeta^{-i}
Este tutorial cubre solo lo implementado hoy. Las siguientes capacidades quedan preparadas para fases posteriores, pero no forman parte del header actual:
- seleccion automatica de primos
- reconstruccion exacta mas alla del rango de tres primos
- aceptacion de todos los casos validos posibles: la cota actual es conservadora y puede rechazar algunas entradas que aun cabrian en CRT
Consulta Examples/ntt_example.cc para un recorrido ejecutable compacto de:
- multiplicacion de polinomios
- backend SIMD activo
- multiplicacion exacta con CRT
- algebra formal de polinomios
- multiplicacion de enteros grandes en base 10
- convolucion negaciclica
- planes reutilizables
- lotes
- multiplicacion paralela
math_nt_example.cc sigue mostrando el toolbox numerico mas amplio alrededor
de la NTT.
| Necesidad | API |
|---|---|
| Transformada directa/inversa | NTT<>::transform(a, invert) |
| Producto modular | NTT<>::multiply(a, b) |
| Plan reutilizable | NTT<>::Plan plan(n) |
| Paralelismo explicito | NTT<>::ptransform(pool, a, invert), pmultiply() |
| Lotes | plan.transform_batch(batch, invert) |
| Backend SIMD | simd_backend_name(), avx2_dispatch_available(), neon_dispatch_available() |
| CRT exacto | NTTExact::multiply(a, b), NTTExact::pmultiply(pool, a, b) |
| Algebra formal | poly_inverse, poly_divmod, poly_log, poly_exp, poly_sqrt, poly_power |
| Evaluacion/interpolacion | poly_eval, multipoint_eval, interpolate |
| Big integers | bigint_multiply<Base>(), pbigint_multiply<Base>() |
Modulo x^N + 1 |
negacyclic_multiply(a, b) |
| Capacidad del modulo | max_transform_size(), supports_size(n), primitive_root_of_unity(n) |