# Backends.

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

- Canonical page: https://www.poulpy.dev/docs/backends/
- Poulpy version: 0.9.0
- Source revision: [`7dd13e6aa9808982f5355fb6acb70ca434de4e35`](https://github.com/poulpy-fhe/poulpy/tree/7dd13e6aa9808982f5355fb6acb70ca434de4e35)
- Documentation reviewed: 2026-10-08
- Audience: For developers selecting hardware backends

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](https://www.poulpy.dev/architecture/#backends) 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](https://www.poulpy.dev/docs/versions/).

## 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](#hardware-targets) for the broader backend scope and GPU development status.

| Subfamily | Portable | AVX2 / FMA | AVX-512 | NEON |
|-----------|-----------|------------|---------|------|
| FFT64  | `FFT64Portable` | `FFT64Avx`, `FFT64AvxRayon` | `FFT64Avx512`, `FFT64Avx512Rayon` | `FFT64Neon`, `FFT64NeonRayon` |
| NTT4x30 | `NTT4x30Portable` | `NTT4x30Avx`, `NTT4x30AvxRayon` | `NTT4x30Avx512`, `NTT4x30Avx512Rayon` | `NTT4x30Neon`, `NTT4x30NeonRayon` |
| NTT3x42 | none | none | `NTT3x42Ifma`, `NTT3x42IfmaRayon` | none |

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.

| Backend | Crate | Feature | Required target features |
|---------|-------|---------|--------------------------|
| `FFT64Portable` | `poulpy-cpu-portable` | none | none |
| `FFT64Avx` | `poulpy-cpu-avx` | `enable-avx` | `+avx2,+fma` |
| `FFT64AvxRayon` | `poulpy-cpu-avx` | `enable-rayon` | `+avx2,+fma` |
| `FFT64Avx512` | `poulpy-cpu-avx512` | `enable-avx512f` | `+avx512f` |
| `FFT64Avx512Rayon` | `poulpy-cpu-avx512` | `enable-rayon` | `+avx512f` |
| `FFT64Neon` | `poulpy-cpu-arm` | `enable-neon` | none |
| `FFT64NeonRayon` | `poulpy-cpu-arm` | `enable-rayon` | none |
| `NTT4x30Portable` | `poulpy-cpu-portable` | none | none |
| `NTT4x30Avx` | `poulpy-cpu-avx` | `enable-avx` | `+avx2,+fma` |
| `NTT4x30AvxRayon` | `poulpy-cpu-avx` | `enable-rayon` | `+avx2,+fma` |
| `NTT4x30Avx512` | `poulpy-cpu-avx512` | `enable-avx512f` | `+avx512f` |
| `NTT4x30Avx512Rayon` | `poulpy-cpu-avx512` | `enable-rayon` | `+avx512f` |
| `NTT4x30Neon` | `poulpy-cpu-arm` | `enable-neon` | none |
| `NTT4x30NeonRayon` | `poulpy-cpu-arm` | `enable-rayon` | none |
| `NTT3x42Ifma` | `poulpy-cpu-avx512` | `enable-ifma` | `+avx512f,+avx512ifma,+avx512vl` |
| `NTT3x42IfmaRayon` | `poulpy-cpu-avx512` | `enable-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`.

## Correctness oracle and validation

`poulpy-cpu-oracle` provides `FFT64Oracle` and `NTT4x30Oracle`, with their conjugate-invariant aliases `FFT64CIOracle` and `NTT4x30CIOracle`. They implement only the required HAL operations, with plain loops, independently generated tables and no fast paths. The crate is not published: the backend crates use it as a path-only development dependency, and its types make no promise of buffer compatibility with production backends.

Every other backend is validated by parity tests against an attested backend, and attestation is transitive. In this release the oracle attests `poulpy-cpu-portable`; the portable backend attests the AVX2 and NEON backends; AVX2 attests AVX-512, which attests `NTT3x42Ifma`; and each `*Rayon` type is checked against its serial counterpart. A new backend can be checked against any attested one. See [Attestation is transitive](https://www.poulpy.dev/architecture/#verification).

## How to use a backend

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

```rust
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.

```rust
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.

```rust
#[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](https://github.com/poulpy-fhe/poulpy/blob/7dd13e6aa9808982f5355fb6acb70ca434de4e35/docs/base2k-failure-probability.md) 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](https://www.poulpy.dev/docs/performance/) and [Measure a workload](https://www.poulpy.dev/docs/measurement/) 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](https://github.com/poulpy-fhe/poulpy/blob/7dd13e6aa9808982f5355fb6acb70ca434de4e35/docs/backends.md#ckks-encoding); the `encoding_determinism` tests of the CKKS backend suites check it.
