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}