poulpyFully homomorphic encryption
GitHub Get started
Menu

Performance

Backends.

Poulpy separates scheme code from polynomial arithmetic through interchangeable FFT and NTT backends.

For developers selecting hardware backends · Source guide · 2394fa5

Poulpy decouples scheme code from polynomial arithmetic through the hardware abstraction layer (poulpy-hal). A backend performs the low-level primitives behind that layer, such as the Fourier transform and matrix-vector products. The backend type is the generic parameter B of Module<B>; its implementation controls the underlying hardware and storage. User code stays generic over B and the backend is chosen at compile time.

There are two main arithmetic families: FFT and NTT backends, each with their subfamilies. They differ in the format of the DFT used to make polynomial multiplication in Z[X]/(X^N + 1) efficient: FFT backends use a complex floating-point DFT, while NTT backends use a number-theoretic transform over integers. All backends are interchangeable behind the HAL, so the same scheme code runs on any of them.

Hardware targets

Poulpy’s scheme APIs are hardware-independent. Backends control memory layouts, kernels, and scheduling behind those APIs, with the freedom to override operations at any layer.

CPU implementations include the portable reference, x86 acceleration, and ARM NEON. A GPU backend is in active development. The Architecture page explains the common extension points and reference contract for hardware backends.

The concrete types and build commands below cover the CPU implementations in the pinned source guide.

Currently available subfamilies

Poulpy currently ships one FFT subfamily, FFT64, and two NTT subfamilies, NTT4x30 and NTT3x42.

FFT64

FFT64 uses a 64-bit floating-point FFT for polynomial multiplication. Coefficients live in the DFT domain as f64 and in the large-integer domain as i64. The transform is approximate because it relies on IEEE 754 rounding. It is a useful candidate for small gate-level workloads; compare it at your actual parameters. Examples can select different families.

NTT4x30

NTT4x30 uses an exact integer NTT for polynomial multiplication. It uses the Chinese Remainder Theorem over four roughly 30-bit primes (Primes30), giving a modulus Q near 2^120. Coefficients live in the NTT domain as four u64 lanes and reconstruct to i128 in the large-integer domain. Its modular arithmetic is exact within the supported reconstruction bounds. Compare its wider limbs and transform cost at the ring dimensions you need.

NTT3x42

NTT3x42 is also an exact integer NTT. It uses three roughly 42-bit primes (Primes42) chosen for AVX-512 IFMA52 hardware, giving a modulus Q near 2^126. It reconstructs to i128 like NTT4x30 but reaches slightly higher precision per transform. It exists only as an IFMA-accelerated backend, because it relies on IFMA multiply-add to keep its matrix-vector products within 64 bits.

Available backend types

This table lists the CPU types documented at the guide’s source revision. See Hardware targets for the broader backend scope and GPU development status.

SubfamilyReferenceAVX2 / FMAAVX-512NEON
FFT64FFT64RefFFT64Avx, FFT64AvxRayonFFT64Avx512, FFT64Avx512RayonFFT64Neon, FFT64NeonRayon
NTT4x30NTT4x30RefNTT4x30Avx, NTT4x30AvxRayonNTT4x30Avx512, NTT4x30Avx512RayonNTT4x30Neon, NTT4x30NeonRayon
NTT3x42nonenoneNTT3x42Ifma, NTT3x42IfmaRayonnone

The *Ref types live in poulpy-cpu-ref and are portable across every CPU. They prioritize correctness and validation, not performance; use an accelerated backend for performance-sensitive workloads. The *Avx types live in poulpy-cpu-avx. The *Avx512 and NTT3x42Ifma types live in poulpy-cpu-avx512. The *Neon types live in poulpy-cpu-arm and target AArch64 (Apple Silicon, Neoverse). The *Rayon types use the same arithmetic subfamily and storage formats as their serial counterparts but schedule supported operations over the active Rayon thread pool.

BackendCrateFeatureRequired target features
FFT64Refpoulpy-cpu-refnonenone
FFT64Avxpoulpy-cpu-avxenable-avx+avx2,+fma
FFT64AvxRayonpoulpy-cpu-avxenable-rayon+avx2,+fma
FFT64Avx512poulpy-cpu-avx512enable-avx512f+avx512f
FFT64Avx512Rayonpoulpy-cpu-avx512enable-rayon+avx512f
FFT64Neonpoulpy-cpu-armenable-neonnone
FFT64NeonRayonpoulpy-cpu-armenable-rayonnone
NTT4x30Refpoulpy-cpu-refnonenone
NTT4x30Avxpoulpy-cpu-avxenable-avx+avx2,+fma
NTT4x30AvxRayonpoulpy-cpu-avxenable-rayon+avx2,+fma
NTT4x30Avx512poulpy-cpu-avx512enable-avx512f+avx512f
NTT4x30Avx512Rayonpoulpy-cpu-avx512enable-rayon+avx512f
NTT4x30Neonpoulpy-cpu-armenable-neonnone
NTT4x30NeonRayonpoulpy-cpu-armenable-rayonnone
NTT3x42Ifmapoulpy-cpu-avx512enable-ifma+avx512f,+avx512ifma,+avx512vl
NTT3x42IfmaRayonpoulpy-cpu-avx512enable-ifma, enable-rayon+avx512f,+avx512ifma,+avx512vl

The AVX and AVX-512 backends check the required CPU features at runtime in Module::new and panic if they are missing. They also require the matching target-feature flags at compile time. The *Neon backends need no target-feature flags and build only on aarch64. In poulpy-cpu-avx, enable-rayon implies enable-avx. In poulpy-cpu-avx512, enable-rayon implies enable-avx512f, but it must be combined with enable-ifma to expose NTT3x42IfmaRayon.

How to use a backend

A backend is selected by naming its type when you build the Module.

use poulpy_hal::layouts::Module;
use poulpy_cpu_ref::FFT64Ref;

let module: Module<FFT64Ref> = Module::new(1 << 10);

The backend type is the selection point. Check Cargo features, target flags, parameter bounds, and prepared layouts when changing it.

use poulpy_cpu_ref::NTT4x30Ref;

let module = Module::<NTT4x30Ref>::new(1 << 10);

Some examples select an enabled backend with cfg; this is compile-time selection, not a runtime performance comparison.

#[cfg(all(feature = "enable-avx", target_arch = "x86_64"))]
use poulpy_cpu_avx::FFT64Avx as BackendImpl;
#[cfg(not(all(feature = "enable-avx", target_arch = "x86_64")))]
use poulpy_cpu_ref::FFT64Ref as BackendImpl;

let module = Module::<BackendImpl>::new(n as u64);

Choosing a subfamily

The backend fixes the maximum limb size base2k you can use. FFT64 allows up to 19 bits per limb, while NTT4x30 allows up to 52. A larger base2k represents the same precision in fewer limbs, at the cost of more expensive elementary operations.

Use FFT64 for gate-level and TFHE-style work, especially at small ring dimensions: there the limb count is already low, so the wider NTT limbs cannot pay for their extra transforms.

For leveled schemes, include NTT3x42 when IFMA is available, and compare NTT4x30 with FFT64 for your circuit. Wider limbs reduce coefficient passes, while transform costs and prepared-key traffic differ. The site does not publish measured evidence for a universal ranking. Use Performance and Measure a workload to compare candidates.

Within a chosen subfamily, compare the accelerated backends supported by your hardware and build configuration. Choose a *Rayon variant when one operation should use several CPU cores, especially for large dimensions or batches. Choose its serial counterpart when the application already parallelizes independent operations or when the workload is too small to repay scheduling overhead. Rayon variants fall back to serial execution when the active pool has one thread or the work is below their internal parallelization threshold.