poulpyFully homomorphic encryption
GitHub ↗Get started →
Menu

Performance

Backends.

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

Source guide · 7dd13e6Markdown

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 backend, 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 residue lanes (u64 on the portable backend; the NEON backend runs a native 32-bit NTT) 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.

SubfamilyPortableAVX2 / FMAAVX-512NEON
FFT64FFT64PortableFFT64Avx, FFT64AvxRayonFFT64Avx512, FFT64Avx512RayonFFT64Neon, FFT64NeonRayon
NTT4x30NTT4x30PortableNTT4x30Avx, NTT4x30AvxRayonNTT4x30Avx512, NTT4x30Avx512RayonNTT4x30Neon, NTT4x30NeonRayon
NTT3x42nonenoneNTT3x42Ifma, NTT3x42IfmaRayonnone

The *Portable types live in poulpy-cpu-portable, are written in plain scalar Rust, and run on every CPU. 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.

Every backend type takes a ring parameter that defaults to the standard ring Z[X]/(X^N + 1). Each one also has a conjugate-invariant alias, named with CI after the subfamily (FFT64CIPortable, NTT4x30CIAvx512, NTT3x42CIIfma, FFT64CINeonRayon, and so on), which CKKS uses for N real slots.

BackendCrateFeatureRequired target features
FFT64Portablepoulpy-cpu-portablenonenone
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
NTT4x30Portablepoulpy-cpu-portablenonenone
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. Each CPU backend crate wires the scheme layers behind enable-ckks and enable-bin-fhe; poulpy-cpu-portable also has enable-core and enable-mhe.

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_portable::FFT64Portable;

let module: Module<FFT64Portable> = 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_portable::NTT4x30Portable;

let module = Module::<NTT4x30Portable>::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_portable::FFT64Portable as BackendImpl;

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

Choosing a subfamily

The backend bounds the limb size base2k you can use. Query the bound with Module::<BE>::max_base2k(n, products, failure_bits, squaring), which needs no module instance. products counts the polynomial products accumulated into one output, and failure_bits requests an estimated whole-polynomial failure probability of at most 2^(-failure_bits). Set squaring = false for independent products and true if any term is a square. For a sum of 32 products, Module::<NTT4x30Portable>::max_base2k(1 << 16, 32, 128, false) returns Some(54); the same query gives Some(19) for FFT64Portable and Some(57) for NTT3x42Ifma. Some(0) means no positive radix fits, and None means the backend has no applicable model. These are estimates for independent products of centered-uniform inputs, not guarantees for arbitrary inputs; see Failure estimates for base-2^K arithmetic for the models. A larger base2k represents the same precision in fewer limbs, but also reserve coefficient-word headroom for additions before normalization and respect the circuit’s noise budget.

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.

CKKS encoding

CKKS encoding and decoding return the same bytes on every backend and platform, at each scalar precision (f32, f64, or Quad), in both rings. The plaintexts, the decoded slots, and the setup constants of bootstrapping all match. A backend that implements CKKSEncodingImpl must follow the arithmetic definition in the upstream backend guide; the encoding_determinism tests of the CKKS backend suites check it.