Skip to main content

poulpy_hal/
lib.rs

1//! # poulpy-hal
2//!
3//! A trait-based Hardware Abstraction Layer (HAL) for lattice-based polynomial
4//! arithmetic over the cyclotomic ring `Z[X]/(X^N + 1)`.
5//!
6//! This crate provides backend-agnostic data layouts and a trait-based API for
7//! polynomial operations commonly used in lattice-based cryptography (LWE/Module-LWE
8//! ciphertexts, key-switching matrices, external products, etc.). It is designed
9//! so that cryptographic schemes can be written once against the [`api`] traits and
10//! then executed on any backend (CPU with AVX2/AVX-512, GPU, FPGA, ...) that
11//! implements the [`oep`] (Open Extension Point) traits.
12//!
13//! ## Core Concepts
14//!
15//! **Ring:** All polynomials live in `Z[X]/(X^N + 1)` where `N` is a power of
16//! two (the *ring degree*). A [`layouts::Module`] encapsulates `N` together with
17//! an optional backend-specific handle (e.g. precomputed FFT twiddle factors).
18//!
19//! **Limbed representation (base-2^k):** Large coefficients are decomposed into
20//! a vector of `size` limbs, each carrying at most `base2k` bits. This is the
21//! *bivariate* view `Z[X, Y]` with `Y = 2^{-k}`, central to gadget
22//! decomposition and normalization.
23//!
24//! **Layout types** ([`layouts`]):
25//! - [`layouts::ScalarZnx`] -- single polynomial of integer coefficients (word type `W`, default `i64`).
26//! - [`layouts::VecZnx`] -- vector of `cols` polynomials, each with `size` limbs.
27//! - [`layouts::MatZnx`] -- matrix of polynomials (`rows x cols_in`, each entry a [`layouts::VecZnx`] of `cols_out` polynomials).
28//! - [`layouts::VecZnxBig`] -- vector of polynomials with large-coefficient accumulator words (keyed by the backend's declared [`layouts::BigWord`]).
29//! - [`layouts::VecZnxDft`] -- vector of polynomials in DFT/NTT domain (keyed by the backend's declared [`layouts::DftWord`]).
30//! - [`layouts::SvpPPol`] -- prepared scalar polynomial for scalar-vector products.
31//! - [`layouts::VmpPMat`] -- prepared matrix for vector-matrix products.
32//! - [`layouts::CnvPVecL`], [`layouts::CnvPVecR`] -- prepared left/right operands for bivariate convolution.
33//! - [`layouts::ScratchArena`], [`layouts::ScratchOwned`] -- aligned scratch memory for temporary workspace.
34//!
35//! All layout types are generic over a data container `D` (owned `Vec<u8>`, borrowed
36//! `&[u8]` / `&mut [u8]`), enabling zero-copy views and arena-style allocation via
37//! [`layouts::ScratchArena`], and over a word type `W` naming the byte-layout
38//! convention of their coefficient domain (see [`layouts::ZnxWord`],
39//! [`layouts::BigWord`], [`layouts::DftWord`]).
40//!
41//! ## Architecture
42//!
43//! The crate is organized into a four-layer stack:
44//!
45//! 1. **[`api`]** -- Safe, user-facing trait definitions (e.g. [`api::VecZnxAddIntoBackend`],
46//!    [`api::VmpApplyDftToDft`]). Scheme authors program against these.
47//! 2. **[`oep`]** -- Unsafe extension-point layer of per-family backend traits.
48//!    Backend crates implement only the families they own and may reuse helper
49//!    macros or defaults where convenient.
50//! 3. **[`delegates`]** -- Blanket `impl` glue that connects each [`api`] trait to
51//!    the corresponding backend family method on [`layouts::Module`].
52//! 4. **Reference implementations** live in the `poulpy-cpu-ref` crate, which provides
53//!    the portable default backend used by tests and benchmarks.
54//!
55//! ## Testing and Benchmarking
56//!
57//! The [`test_suite`] module provides fully generic, backend-parametric test
58//! functions. Backend crates instantiate these via the
59//! [`backend_test_suite!`](crate::backend_test_suite) and
60//! [`cross_backend_test_suite!`](crate::cross_backend_test_suite) macros to
61//! validate correctness against the reference implementation in
62//! [`poulpy-cpu-ref`](https://docs.rs/poulpy-cpu-ref).
63//!
64//! Analogous Criterion-based benchmark harnesses live in the separate
65//! [`poulpy-bench`](https://docs.rs/poulpy-bench) crate.
66//!
67//! ## Safety Contract
68//!
69//! All [`oep`] extension points are `unsafe` to implement. Implementors must uphold the
70//! contract documented in [`doc::backend_safety`], covering memory domains,
71//! alignment, scratch lifetime, synchronization, aliasing, and numerical
72//! exactness.
73//!
74//! ## Non-Goals
75//!
76//! - This crate does **not** provide a complete cryptographic scheme. It is a
77//!   low-level arithmetic layer consumed by higher-level crates such as
78//!   `poulpy-core` and `poulpy-bin-fhe`.
79//! - It does **not** perform constant-time enforcement. Side-channel resistance
80//!   is the responsibility of the backend and the caller.
81//!
82//! ## Compatibility
83//!
84//! - Requires **nightly** Rust (uses `trait_alias` and `associated_type_defaults`).
85//! - All memory allocations are aligned to [`DEFAULTALIGN`] (64 bytes).
86//! - Types matching the API of **spqlios-arithmetic**.
87
88#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals, dead_code, improper_ctypes)]
89#![deny(rustdoc::broken_intra_doc_links)]
90#![cfg_attr(docsrs, feature(doc_cfg))]
91#![feature(associated_type_defaults)]
92#![feature(trait_alias)]
93
94/// Safe, user-facing trait definitions for polynomial arithmetic operations.
95///
96/// Scheme authors program against these traits; the actual computation is
97/// dispatched to a backend via the [`oep`] extension points.
98pub mod api;
99
100/// Criterion-based benchmark harnesses, generic over any backend.
101/// Blanket implementations connecting [`api`] traits to [`oep`] traits on
102/// [`layouts::Module`].
103///
104/// This module contains no user-facing logic; it exists solely to wire
105/// the safe API layer to the unsafe backend implementations.
106pub mod delegates;
107
108/// Backend-agnostic data layout types for polynomials, vectors, matrices,
109/// and prepared (DFT-domain) representations.
110///
111/// All types are generic over a data container `D` (`Vec<u8>`, `&[u8]`,
112/// `&mut [u8]`) enabling owned, borrowed, and scratch-backed usage.
113pub mod layouts;
114
115/// Open Extension Points: the `unsafe` backend extension layer of per-family
116/// backend traits.
117///
118/// Backend crates implement only the families they own and may delegate to
119/// helper defaults provided by a backend crate (for example `poulpy-cpu-ref`). See
120/// [`doc::backend_safety`] for the safety contract.
121pub mod oep;
122
123/// Portable scalar kernels over `[i64]`, shared by every backend.
124pub mod reference;
125
126/// Deterministic pseudorandom number generation based on ChaCha8.
127pub mod source;
128
129/// Fully generic, backend-parametric test functions.
130///
131/// Backend crates instantiate these via the [`backend_test_suite!`] and
132/// [`cross_backend_test_suite!`] macros.
133pub mod test_suite;
134
135/// Embedded safety contract documentation for backend implementors.
136pub mod doc {
137    /// Safety contract that all [`crate::oep`] trait implementations must uphold.
138    ///
139    /// Covers memory domains, alignment, scratch lifetime, synchronization,
140    /// aliasing, and numerical exactness requirements.
141    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/backend_safety_contract.md"))]
142    pub mod backend_safety {
143        pub const _PLACEHOLDER: () = ();
144    }
145}
146
147/// Default generator of the Galois group `(Z/2NZ)*` for the cyclotomic ring
148/// `Z[X]/(X^N + 1)`.
149///
150/// Used to compute Galois automorphisms `X -> X^{5^k}` and their inverses.
151pub const GALOISGENERATOR: u64 = 5;
152
153/// Default memory alignment in bytes for all allocated buffers.
154///
155/// Set to 64 bytes to match the cache-line size of modern x86 processors
156/// and the alignment required by AVX-512 instructions.
157pub const DEFAULTALIGN: usize = 64;
158
159fn is_aligned_custom<T>(ptr: *const T, align: usize) -> bool {
160    (ptr as usize).is_multiple_of(align)
161}
162
163/// Returns `true` if `ptr` is aligned to [`DEFAULTALIGN`] bytes.
164pub fn is_aligned<T>(ptr: *const T) -> bool {
165    is_aligned_custom(ptr, DEFAULTALIGN)
166}
167
168/// Panics if `ptr` is not aligned to [`DEFAULTALIGN`] bytes.
169///
170/// # Panics
171///
172/// Panics with a descriptive message when the pointer does not satisfy the
173/// default alignment requirement.
174pub fn assert_alignment<T>(ptr: *const T) {
175    assert!(
176        is_aligned(ptr),
177        "invalid alignment: ensure passed bytes have been allocated with [alloc_aligned_u8] or [alloc_aligned]"
178    )
179}
180
181/// Deprecated spelling variant. Use [`assert_alignment`] instead.
182#[inline]
183pub fn assert_alignement<T>(ptr: *const T) {
184    assert_alignment(ptr)
185}
186
187/// Reinterprets a `&[T]` as a `&[V]`.
188///
189/// # Safety (via assertions)
190/// - `V` must not be zero-sized.
191/// - The pointer must be aligned for `V`.
192/// - The total byte length must be a multiple of `size_of::<V>()`.
193pub fn cast<T, V>(data: &[T]) -> &[V] {
194    assert!(size_of::<V>() > 0, "cast: target type V must not be zero-sized");
195    let byte_len: usize = std::mem::size_of_val(data);
196    assert!(
197        byte_len.is_multiple_of(size_of::<V>()),
198        "cast: byte length {} is not a multiple of target size {}",
199        byte_len,
200        size_of::<V>()
201    );
202    let ptr: *const V = data.as_ptr() as *const V;
203    assert!(
204        ptr.align_offset(align_of::<V>()) == 0,
205        "cast: pointer {:p} is not aligned to {} bytes",
206        ptr,
207        align_of::<V>()
208    );
209    let len: usize = byte_len / size_of::<V>();
210    unsafe { std::slice::from_raw_parts(ptr, len) }
211}
212
213/// Reinterprets a `&mut [T]` as a `&mut [V]`.
214///
215/// # Safety (via assertions)
216/// - `V` must not be zero-sized.
217/// - The pointer must be aligned for `V`.
218/// - The total byte length must be a multiple of `size_of::<V>()`.
219pub fn cast_mut<T, V>(data: &mut [T]) -> &mut [V] {
220    assert!(size_of::<V>() > 0, "cast_mut: target type V must not be zero-sized");
221    let byte_len: usize = std::mem::size_of_val(data);
222    assert!(
223        byte_len.is_multiple_of(size_of::<V>()),
224        "cast_mut: byte length {} is not a multiple of target size {}",
225        byte_len,
226        size_of::<V>()
227    );
228    let ptr: *mut V = data.as_mut_ptr() as *mut V;
229    assert!(
230        ptr.align_offset(align_of::<V>()) == 0,
231        "cast_mut: pointer {:p} is not aligned to {} bytes",
232        ptr,
233        align_of::<V>()
234    );
235    let len: usize = byte_len / size_of::<V>();
236    unsafe { std::slice::from_raw_parts_mut(ptr, len) }
237}
238
239/// Minimum allocation size for which the aligned allocator advises
240/// transparent huge pages. Overridable via `POULPY_HUGEPAGE_MIN_BYTES`.
241const HUGEPAGE_ADVISE_THRESHOLD: usize = 2 * 1024 * 1024;
242
243#[cfg(target_os = "linux")]
244fn hugepage_min_bytes() -> usize {
245    use once_cell::sync::Lazy;
246    static THRESH: Lazy<usize> = Lazy::new(|| {
247        std::env::var("POULPY_HUGEPAGE_MIN_BYTES")
248            .ok()
249            .and_then(|v| v.trim().parse().ok())
250            .unwrap_or(HUGEPAGE_ADVISE_THRESHOLD)
251    });
252    *THRESH
253}
254
255/// `madvise(MADV_HUGEPAGE)` on a freshly-allocated range. Skipped if the
256/// pointer is not page-aligned (non-mmap'd heap arenas) or the size is
257/// below the threshold. Failure is silently ignored — advisory only.
258#[cfg(target_os = "linux")]
259fn advise_hugepage(ptr: *mut u8, size: usize) {
260    if size < hugepage_min_bytes() {
261        return;
262    }
263    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
264    if page_size <= 0 {
265        return;
266    }
267    let page_size = page_size as usize;
268    if !(ptr as usize).is_multiple_of(page_size) {
269        return;
270    }
271    let len = size - (size % page_size);
272    if len == 0 {
273        return;
274    }
275    let _ = unsafe { libc::madvise(ptr.cast(), len, libc::MADV_HUGEPAGE) };
276}
277
278#[cfg(not(target_os = "linux"))]
279#[inline(always)]
280fn advise_hugepage(_ptr: *mut u8, _size: usize) {}
281
282/// Allocates a block of bytes with a custom alignment.
283/// Alignment must be a power of two and size a multiple of the alignment.
284/// Allocated memory is initialized to zero.
285///
286/// Large allocations are advised for transparent huge pages via
287/// [`advise_hugepage`] on Linux before the zero-fill.
288///
289/// # Known issue (CRITICAL-2)
290/// The returned `Vec<u8>` was allocated with custom alignment via `std::alloc::alloc`,
291/// but `Vec::drop` will call `std::alloc::dealloc` with `align_of::<u8>() = 1`.
292/// This is technically UB per the `GlobalAlloc` contract (mismatched layout).
293/// In practice it works on all major allocators (glibc, jemalloc, mimalloc) because
294/// they ignore the alignment parameter during deallocation. A proper fix requires
295/// replacing `Vec<u8>` with a custom `AlignedBuf` type that tracks the layout.
296fn alloc_aligned_custom_u8(size: usize, align: usize) -> Vec<u8> {
297    assert!(align.is_power_of_two(), "Alignment must be a power of two but is {align}");
298    assert_eq!(
299        (size * size_of::<u8>()) % align,
300        0,
301        "size={size} must be a multiple of align={align}"
302    );
303    unsafe {
304        let layout: std::alloc::Layout = std::alloc::Layout::from_size_align(size, align).expect("Invalid alignment");
305        let ptr: *mut u8 = std::alloc::alloc(layout);
306        if ptr.is_null() {
307            panic!("Memory allocation failed");
308        }
309        assert!(
310            is_aligned_custom(ptr, align),
311            "Memory allocation at {ptr:p} is not aligned to {align} bytes"
312        );
313        // Advise before write_bytes so the zero-fill faults materialise
314        // 2 MB pages directly rather than relying on khugepaged promotion.
315        advise_hugepage(ptr, size);
316        std::ptr::write_bytes(ptr, 0, size);
317        Vec::from_raw_parts(ptr, size, size)
318    }
319}
320
321/// Allocates a zero-initialized `Vec<T>` with custom alignment.
322///
323/// The total byte size (`size * size_of::<T>()`) must be a multiple of `align`,
324/// and `align` must be a power of two.
325///
326/// # Panics
327///
328/// - If `T` is zero-sized.
329/// - If `align` is not a power of two.
330/// - If `size * size_of::<T>()` is not a multiple of `align`.
331pub fn alloc_aligned_custom<T>(size: usize, align: usize) -> Vec<T> {
332    assert!(size_of::<T>() > 0, "alloc_aligned_custom: zero-sized types are not supported");
333    assert!(align.is_power_of_two(), "Alignment must be a power of two but is {align}");
334
335    assert_eq!(
336        (size * size_of::<T>()) % align,
337        0,
338        "size*size_of::<T>()={} must be a multiple of align={align}",
339        size * size_of::<T>(),
340    );
341
342    let mut vec_u8: Vec<u8> = alloc_aligned_custom_u8(size_of::<T>() * size, align);
343    let ptr: *mut T = vec_u8.as_mut_ptr() as *mut T;
344    let len: usize = vec_u8.len() / size_of::<T>();
345    let cap: usize = vec_u8.capacity() / size_of::<T>();
346    std::mem::forget(vec_u8);
347    unsafe { Vec::from_raw_parts(ptr, len, cap) }
348}
349
350/// Allocates a zero-initialized `Vec<T>` aligned to [`DEFAULTALIGN`] bytes.
351///
352/// The allocation is padded so that the total byte size is a multiple of
353/// [`DEFAULTALIGN`]. This is the primary allocation entry point for all
354/// layout types in the crate.
355///
356/// # Panics
357///
358/// Panics if `T` is zero-sized.
359pub fn alloc_aligned<T>(size: usize) -> Vec<T> {
360    alloc_aligned_custom::<T>(
361        (size * size_of::<T>()).next_multiple_of(DEFAULTALIGN) / size_of::<T>(),
362        DEFAULTALIGN,
363    )
364}