poulpyFully homomorphic encryption
GitHub ↗Get started →
Menu

Tune the workload you have.

Understand the cost drivers, select candidates, and measure the complete circuit.

Source guide · 7dd13e6Markdown

The short version

Start with a correct portable implementation. Select accelerated candidates for your target hardware, then compare them at the same input range, output accuracy, and security assumptions. The benchmark explorer reports 16-level CKKS bootstrapping measurements on an AMD Ryzen 9 9950X (x86 backends) and an Apple M5 Pro (NEON), with fixed presets and precision checks. Each run names its hardware. These results were measured with Poulpy 0.9.0 and describe that workload and hardware.

The guidance below describes mechanisms and starting points. Record actual measurements using Measure a workload.

Poulpy’s backend APIs are hardware-independent, and a GPU backend is in active development. The build commands and Rayon diagnostics on this page exercise CPU implementations. For device execution, also account for data transfers, synchronization, batching, and device memory in the measurement boundary.

Key material and arithmetic

Key switching and external products read prepared evaluation-key material. Large keys can make memory traffic a substantial part of the cost. Measure prepared-key bytes as well as serialized size: they are different representations.

Coefficient-domain operations process polynomial limbs. At a fixed ciphertext width, larger base2k means fewer limbs, but the backend also changes the cost and storage of each transformed limb. Transform count alone does not determine runtime.

1. Arithmetic family

FFT64 uses a floating-point transform, while NTT4x30 and NTT3x42 use integer residue transforms. The integer families permit wider limbs in their supported configurations. An accelerated NTT backend may pack its residues differently from the portable implementation.

For small gate-level workloads, include FFT64 in the comparison. For leveled numerical work, include NTT4x30, and NTT3x42 if IFMA is available. Compare key preparation and the full circuit, not only an isolated multiplication. See Backends for types and supported hardware.

2. Backend

Compare the portable baseline with an accelerated implementation of the same arithmetic family first. Then compare arithmetic families. This helps separate hardware implementation effects from representation and parameter changes.

A wider instruction set is a candidate optimization, not a guarantee for every CPU or memory-bound workload. For CPU measurements, record the model, actual target flags, and thread count with each result.

3. Compilation options

Use release mode and the build requirements for your selected backend. The following settings apply to the CPU implementations:

BackendCrate featureTarget features
AVX2/FMAenable-avx+avx2,+fma
AVX-512enable-avx512f+avx512f
IFMAenable-ifma+avx512f,+avx512ifma,+avx512vl
NEONenable-neonAArch64 target

-C target-cpu=native selects the build machine’s instructions; binaries built that way may not run on another machine. The backend guide covers scheme integration and Rayon features.

From the pinned checkout, inspect CPU capabilities:

cargo test --locked -p poulpy-cpu-portable capabilities -- --ignored --nocapture

4. base2k

Choose a limb width within the backend’s supported bounds for the operation and shape. The query Module::<BE>::max_base2k(n, products, failure_bits, squaring) estimates the largest radix from the degree, the number of accumulated products, the squaring flag, and the failure target 2^(-failure_bits); see Backends for examples and model assumptions. The query only models DFT-domain products, so also reserve coefficient-word headroom for additions and subtractions before normalization.

At fixed Torus width k, the limb count is ceil(k / base2k). Smaller limbs increase this count; larger limbs change product/noise bounds and may not suit every configuration. Re-evaluate accuracy and parameter assumptions when changing it.

5. dsize

dsize groups several limbs into one gadget digit. Changing it trades digit count, evaluation-key size, auxiliary precision, and evaluation work. Use the key-layout and noise constraints to select valid candidates, then measure them. There is no workload-independent best value established by this guide.

6. Threads

On CPUs, Rayon variants parallelize supported operations internally. Serial CPU backends can instead be used for independent operations scheduled by the application. Compare these strategies for the latency or throughput goal that matters to you.

More workers can stop helping when memory bandwidth, scheduling, or scratch reservations dominate. The useful pool width depends on the backend and parameter shape; it is not necessarily the CPU’s core count.

Setting the pool

let pool = rayon::ThreadPoolBuilder::new().num_threads(8).build()?;
pool.install(|| {
    // Poulpy calls use this pool; eight is an example, not a recommendation.
});

Idle polling

A circuit opens many short parallel regions, and a worker that Rayon has put to sleep takes time to wake. The executor therefore keeps the pool’s workers polling for work for a short window after the last region, 1 ms by default. The cost is processor time: every worker stays busy for up to the window after each burst of work. Change the window or turn polling off with poulpy_cpu_rayon::set_idle_polling:

poulpy_cpu_rayon::set_idle_polling(None); // workers sleep as soon as Rayon lets them
poulpy_cpu_rayon::set_idle_polling(Some(std::time::Duration::from_micros(200)));

The setting is global. A window much longer than a millisecond lets the pollers compete with the thread that drives the circuit.

Measuring your own thread count

The pinned source supplies an ignored diagnostic for vector-matrix products, convolution, inverse transforms, and coefficient arithmetic. On a CPU supporting these flags:

RUSTFLAGS="-C target-cpu=native" cargo test --locked --release -p poulpy-cpu-avx512 \
  --features enable-avx512f,enable-ifma,enable-rayon tuning -- --ignored --nocapture

Run on an idle machine with the ring degree, limb count, and rank relevant to the application. Record repetition spread and any per-kernel worker caps. This diagnostic selects candidates; it does not replace measuring the full circuit, preparation, and memory footprint.

Keep commands, raw results, and the configuration record together so another developer can reproduce the recommendation.