Skip to main content

poulpy_hal/layouts/
module.rs

1use std::{marker::PhantomData, ptr::NonNull};
2
3use crate::layouts::{Data, Location, MatZnx, ScalarZnx, VecZnx, checked_product, vec_znx_alloc_zeroed};
4use crate::{
5    GALOISGENERATOR,
6    api::{ModuleLogN, ModuleN},
7};
8
9/// Core trait that every backend (CPU, GPU, FPGA, ...) must implement.
10///
11/// Defines the word types used for the coefficient domain (`ZnxWord`),
12/// DFT-domain (`DftWord`) and extended-precision (`BigWord`)
13/// representations, as well as the opaque `Handle` type that holds
14/// backend-specific precomputed state (e.g. FFT twiddle factors).
15///
16/// # Safety
17///
18/// [`destroy`](Backend::destroy) is called during [`Module`] drop and must
19/// correctly deallocate the handle without double-free.
20#[allow(clippy::missing_safety_doc)]
21pub trait Backend: Sized + Sync + Send + PartialEq + Eq {
22    /// Whether this backend's transform-domain arithmetic is exact within its
23    /// documented operand bounds.
24    ///
25    /// This is an arithmetic property of the backend implementation, not of
26    /// the [`DftWord`](crate::layouts::DftWord) byte-layout marker.
27    const DFT_IS_EXACT: bool = false;
28
29    /// Task executor selected by this backend.
30    type TaskExecutor: crate::execution::TaskExecutor;
31    /// Word type for coefficient-domain (small) polynomial representations.
32    type ZnxWord: crate::layouts::ZnxWord;
33    /// Word type for extended-precision (big) polynomial representations.
34    type BigWord: crate::layouts::BigWord;
35    /// Word type for DFT-domain (prepared) polynomial representations.
36    type DftWord: crate::layouts::DftWord;
37    /// Owned backend storage for layouts and scratch.
38    ///
39    /// This buffer may be host-resident or device-resident. It is intentionally
40    /// no longer required to expose direct host byte slices.
41    type OwnedBuf: Data + Send + Sync;
42    /// Shared borrowed view into backend-owned storage.
43    type BufRef<'a>: Data + Sync
44    where
45        Self: 'a;
46    /// Mutable borrowed view into backend-owned storage.
47    type BufMut<'a>: Data + Send
48    where
49        Self: 'a;
50    /// Opaque backend handle type (e.g. precomputed FFT twiddle factors).
51    type Handle: 'static;
52    /// Residency of this backend's buffers — [`Host`](crate::layouts::Host)
53    /// or [`Device`](crate::layouts::Device).
54    type Location: Location;
55    /// Allocates a backend-owned byte buffer of `len` bytes.
56    fn alloc_bytes(len: usize) -> Self::OwnedBuf;
57    /// Allocates a zero-initialized backend-owned byte buffer of `len` bytes.
58    ///
59    /// Backends may override this with a device-native implementation
60    /// (e.g. `cudaMemset`-backed allocation). The default implementation
61    /// falls back to allocating first and then zero-filling through the
62    /// existing host upload path.
63    fn alloc_zeroed_bytes(len: usize) -> Self::OwnedBuf {
64        let mut buf = Self::alloc_bytes(len);
65        let zeros = vec![0u8; len];
66        Self::copy_from_host(&mut buf, &zeros);
67        buf
68    }
69    /// Uploads or copies host bytes into backend-owned storage.
70    fn from_host_bytes(bytes: &[u8]) -> Self::OwnedBuf;
71    /// Wraps/Uploads a host-owned byte buffer into backend-owned storage.
72    ///
73    /// Backends may override this for a zero-copy fast path when the input is
74    /// already in a compatible host representation.
75    fn from_bytes(bytes: Vec<u8>) -> Self::OwnedBuf;
76    /// Copies the contents of a backend-owned buffer into a fresh host `Vec<u8>`.
77    ///
78    /// For host backends this is typically a simple clone of the underlying
79    /// storage; for device backends it performs a device-to-host download.
80    fn to_host_bytes(buf: &Self::OwnedBuf) -> Vec<u8>;
81    /// Copies the contents of a backend-owned buffer into a host byte slice.
82    ///
83    /// `dst.len()` must equal the byte length of `buf`.
84    fn copy_to_host(buf: &Self::OwnedBuf, dst: &mut [u8]);
85    /// Copies a host byte slice into a backend-owned buffer.
86    ///
87    /// `src.len()` must equal the byte length of `buf`.
88    fn copy_from_host(buf: &mut Self::OwnedBuf, src: &[u8]);
89    /// Copies a backend-native borrowed view into a host byte slice.
90    ///
91    /// Unlike [`Self::copy_to_host`], this accepts a view carved from an
92    /// arena. Device backends should implement it with a device-to-host copy
93    /// from the view's native address.
94    fn copy_view_to_host(buf: &Self::BufRef<'_>, dst: &mut [u8]);
95    /// Copies a host byte slice into a backend-native mutable borrowed view.
96    ///
97    /// Unlike [`Self::copy_from_host`], this accepts a view carved from an
98    /// arena. Device backends should implement it with a host-to-device copy
99    /// to the view's native address.
100    fn copy_host_to_view(buf: &mut Self::BufMut<'_>, src: &[u8]);
101    /// Returns the number of bytes stored in a backend-owned buffer.
102    fn len_bytes(buf: &Self::OwnedBuf) -> usize;
103    /// Returns the number of bytes spanned by a shared borrowed view.
104    ///
105    /// Views are the unit a transfer addresses, so their extent has to be
106    /// legible without an owned buffer in hand.
107    fn len_bytes_ref(buf: &Self::BufRef<'_>) -> usize;
108    /// Returns the number of bytes spanned by a mutable borrowed view.
109    fn len_bytes_mut(buf: &Self::BufMut<'_>) -> usize;
110    /// Borrows a shared backend-native view over an owned buffer.
111    fn view(buf: &Self::OwnedBuf) -> Self::BufRef<'_>;
112    /// Reborrows an existing shared backend-native view.
113    fn view_ref<'a, 'b>(buf: &'a Self::BufRef<'b>) -> Self::BufRef<'a>
114    where
115        Self: 'b;
116    /// Reborrows a mutable backend-native view as a shared backend-native view.
117    fn view_ref_mut<'a, 'b>(buf: &'a Self::BufMut<'b>) -> Self::BufRef<'a>
118    where
119        Self: 'b;
120    /// Reborrows an existing mutable backend-native view.
121    fn view_mut_ref<'a, 'b>(buf: &'a mut Self::BufMut<'b>) -> Self::BufMut<'a>
122    where
123        Self: 'b;
124    /// Borrows a mutable backend-native view over an owned buffer.
125    fn view_mut(buf: &mut Self::OwnedBuf) -> Self::BufMut<'_>;
126    /// Borrows a shared sub-region of an owned buffer.
127    fn region(buf: &Self::OwnedBuf, offset: usize, len: usize) -> Self::BufRef<'_>;
128    /// Borrows a mutable sub-region of an owned buffer.
129    fn region_mut(buf: &mut Self::OwnedBuf, offset: usize, len: usize) -> Self::BufMut<'_>;
130    /// Reborrows a shared sub-region of an existing shared backend-native view.
131    fn region_ref<'a, 'b>(buf: &'a Self::BufRef<'b>, offset: usize, len: usize) -> Self::BufRef<'a>
132    where
133        Self: 'b;
134    /// Reborrows a shared sub-region of an existing mutable backend-native view.
135    fn region_ref_mut<'a, 'b>(buf: &'a Self::BufMut<'b>, offset: usize, len: usize) -> Self::BufRef<'a>
136    where
137        Self: 'b;
138    /// Reborrows a mutable sub-region of an existing mutable backend-native view.
139    fn region_mut_ref<'a, 'b>(buf: &'a mut Self::BufMut<'b>, offset: usize, len: usize) -> Self::BufMut<'a>
140    where
141        Self: 'b;
142    /// Bytes size of `ZnxWord`.
143    fn size_of_znx_word() -> usize {
144        size_of::<Self::ZnxWord>()
145    }
146    /// Bytes size of `BigWord`.
147    fn size_of_big_word() -> usize {
148        size_of::<Self::BigWord>()
149    }
150    /// Bytes size of `DftWord`.
151    fn size_of_dft_word() -> usize {
152        size_of::<Self::DftWord>()
153    }
154
155    /// Required alignment (in bytes) for scratch-arena carved regions.
156    ///
157    /// Default to 64 (one CPU cache line). Device backends should override this
158    /// to match their native memory alignment requirement (e.g. 128 for CUDA,
159    /// 256 for ROCm). `ScratchArena::align_up` uses this constant so that
160    /// carved regions satisfy both alignment and SIMD requirements.
161    const SCRATCH_ALIGN: usize = 64;
162
163    /// Byte size of a [`crate::layouts::VecZnx`] buffer.
164    fn bytes_of_vec_znx(n: usize, cols: usize, size: usize) -> usize {
165        checked_product(&[n, cols, size, Self::size_of_znx_word()], "VecZnx byte size")
166    }
167    /// Byte size of a [`crate::layouts::ScalarZnx`] buffer.
168    fn bytes_of_scalar_znx(n: usize, cols: usize) -> usize {
169        checked_product(&[n, cols, Self::size_of_znx_word()], "ScalarZnx byte size")
170    }
171    /// Byte size of a [`crate::layouts::MatZnx`] buffer.
172    fn bytes_of_mat_znx(n: usize, rows: usize, cols_in: usize, cols_out: usize, size: usize) -> usize {
173        checked_product(
174            &[rows, cols_in, Self::bytes_of_vec_znx(n, cols_out, size)],
175            "MatZnx byte size",
176        )
177    }
178    /// Byte size of a [`crate::layouts::VecZnxDft`] buffer.
179    fn bytes_of_vec_znx_dft(n: usize, cols: usize, size: usize) -> usize {
180        checked_product(&[n, cols, size, Self::size_of_dft_word()], "VecZnxDft byte size")
181    }
182    /// Byte size of a [`crate::layouts::VecZnxBig`] buffer.
183    fn bytes_of_vec_znx_big(n: usize, cols: usize, size: usize) -> usize {
184        checked_product(&[n, cols, size, Self::size_of_big_word()], "VecZnxBig byte size")
185    }
186    /// Byte size of a [`crate::layouts::SvpPPol`] buffer.
187    fn bytes_of_svp_ppol(n: usize, cols: usize) -> usize {
188        checked_product(&[n, cols, Self::size_of_dft_word()], "SvpPPol byte size")
189    }
190    /// Byte size of a [`crate::layouts::VmpPMat`] buffer.
191    fn bytes_of_vmp_pmat(n: usize, rows: usize, cols_in: usize, cols_out: usize, size: usize) -> usize {
192        checked_product(
193            &[n, rows, cols_in, cols_out, size, Self::size_of_dft_word()],
194            "VmpPMat byte size",
195        )
196    }
197    /// Byte size of a [`crate::layouts::CnvPVecL`] buffer.
198    fn bytes_of_cnv_pvec_left(n: usize, cols: usize, size: usize) -> usize {
199        checked_product(&[n, cols, size, Self::size_of_dft_word()], "CnvPVecL byte size")
200    }
201    /// Byte size of a [`crate::layouts::CnvPVecR`] buffer.
202    fn bytes_of_cnv_pvec_right(n: usize, cols: usize, size: usize) -> usize {
203        checked_product(&[n, cols, size, Self::size_of_dft_word()], "CnvPVecR byte size")
204    }
205    /// Deallocates a backend handle.
206    ///
207    /// # Safety
208    ///
209    /// `handle` must be a valid, non-dangling pointer that was previously
210    /// returned by the backend's allocation routine. Must not be called
211    /// more than once on the same handle.
212    unsafe fn destroy(handle: NonNull<Self::Handle>);
213}
214
215/// Primary entry point for all polynomial operations over `Z[X]/(X^N + 1)`.
216///
217/// A `Module` pairs a maximum ring degree `N` (always a power of two) with a
218/// backend-specific handle that holds any required precomputed state. All
219/// [`api`](crate::api) trait methods are dispatched through this type.
220/// Existing fixed-ring operations use the maximum degree; dimension-aware
221/// operations may select any supported power-of-two degree from the handle.
222///
223/// The module **owns** its handle; dropping the `Module` calls
224/// [`Backend::destroy`].
225#[repr(C)]
226pub struct Module<B: Backend> {
227    ptr: NonNull<B::Handle>,
228    n: u64,
229    _marker: PhantomData<B>,
230}
231
232unsafe impl<B: Backend> Sync for Module<B> {}
233unsafe impl<B: Backend> Send for Module<B> {}
234
235impl<B: Backend> Module<B> {
236    /// Creates a backend module for ring degree `N`.
237    #[inline]
238    pub fn new(n: u64) -> Self
239    where
240        Self: crate::api::ModuleNew<B>,
241    {
242        crate::api::ModuleNew::new(n)
243    }
244
245    /// Creates a module from a [`NonNull`] backend handle.
246    ///
247    /// # Safety
248    ///
249    /// `ptr` must point to a valid, fully initialized backend handle whose
250    /// lifetime is transferred to this `Module` (it will be destroyed on drop).
251    #[allow(clippy::missing_safety_doc)]
252    #[inline]
253    pub unsafe fn from_nonnull(ptr: NonNull<B::Handle>, n: u64) -> Self {
254        assert!(n.is_power_of_two(), "n must be a power of two, got {n}");
255        Self {
256            ptr,
257            n,
258            _marker: PhantomData,
259        }
260    }
261
262    /// Construct from a raw pointer managed elsewhere.
263    /// SAFETY: `ptr` must be non-null and remain valid for the lifetime of this Module.
264    #[inline]
265    #[allow(clippy::missing_safety_doc)]
266    pub unsafe fn from_raw_parts(ptr: *mut B::Handle, n: u64) -> Self {
267        assert!(n.is_power_of_two(), "n must be a power of two, got {n}");
268        Self {
269            ptr: NonNull::new(ptr).expect("null module ptr"),
270            n,
271            _marker: PhantomData,
272        }
273    }
274
275    /// Returns the raw pointer to the backend handle.
276    #[allow(clippy::missing_safety_doc)]
277    #[inline]
278    pub unsafe fn ptr(&self) -> *mut <B as Backend>::Handle {
279        self.ptr.as_ptr()
280    }
281
282    /// Returns the maximum supported ring degree `N`.
283    #[inline]
284    pub fn n(&self) -> usize {
285        self.n as usize
286    }
287
288    /// Explicit alias for [`Self::n`] when treating the module as a
289    /// multi-ring execution context.
290    #[inline]
291    pub fn max_n(&self) -> usize {
292        self.n()
293    }
294
295    /// Allocates a zero-initialized backend-owned [`ScalarZnx`].
296    #[inline]
297    pub fn scalar_znx_alloc(&self, cols: usize) -> ScalarZnx<B::OwnedBuf, B::ZnxWord> {
298        let n = self.n();
299        let len = B::bytes_of_scalar_znx(n, cols);
300        let bytes = B::alloc_zeroed_bytes(len);
301        ScalarZnx::from_data(bytes, n, cols)
302    }
303
304    /// Allocates a zero-initialized backend-owned [`VecZnx`].
305    #[inline]
306    pub fn vec_znx_alloc(&self, cols: usize, size: usize) -> VecZnx<B::OwnedBuf, B::ZnxWord> {
307        vec_znx_alloc_zeroed::<B>(self.n(), cols, size)
308    }
309
310    /// Returns the byte size of a [`VecZnx`] with this module's ring degree.
311    #[inline]
312    pub fn bytes_of_vec_znx(&self, cols: usize, size: usize) -> usize {
313        self.bytes_of_vec_znx_n(self.n(), cols, size)
314    }
315
316    /// Returns the byte size of a [`VecZnx`] with an explicit coefficient degree.
317    #[inline]
318    pub fn bytes_of_vec_znx_n(&self, n: usize, cols: usize, size: usize) -> usize {
319        B::bytes_of_vec_znx(n, cols, size)
320    }
321
322    /// Allocates a zero-initialized backend-owned [`MatZnx`].
323    #[inline]
324    pub fn mat_znx_alloc(&self, rows: usize, cols_in: usize, cols_out: usize, size: usize) -> MatZnx<B::OwnedBuf, B::ZnxWord> {
325        let n = self.n();
326        let len = B::bytes_of_mat_znx(n, rows, cols_in, cols_out, size);
327        let bytes = B::alloc_zeroed_bytes(len);
328        MatZnx::from_data(bytes, n, rows, cols_in, cols_out, size)
329    }
330
331    /// Returns the raw pointer to the backend handle.
332    #[inline]
333    pub fn as_mut_ptr(&self) -> *mut B::Handle {
334        self.ptr.as_ptr()
335    }
336
337    /// Returns `log2(N)`.
338    #[inline]
339    pub fn log_n(&self) -> usize {
340        (usize::BITS - (self.n() - 1).leading_zeros()) as _
341    }
342
343    /// Reinterprets this `Module<B>` as a `Module<Other>` sharing the same
344    /// backend `Handle` type.
345    ///
346    /// This is a zero-cost view used to forward API calls to a compatible
347    /// source backend without rebuilding the handle.
348    #[inline]
349    pub fn reinterpret<Other>(&self) -> &Module<Other>
350    where
351        Other: Backend<Handle = B::Handle>,
352    {
353        // Safety: Module is #[repr(C)] and only contains an optional NonNull<Handle>,
354        // a u64, and a ZST PhantomData. When `Handle` matches, the layout is identical.
355        unsafe { &*(self as *const Self as *const Module<Other>) }
356    }
357
358    /// Mutable version of [`Module::reinterpret`].
359    #[inline]
360    pub fn reinterpret_mut<Other>(&mut self) -> &mut Module<Other>
361    where
362        Other: Backend<Handle = B::Handle>,
363    {
364        // Safety: see Module::reinterpret.
365        unsafe { &mut *(self as *mut Self as *mut Module<Other>) }
366    }
367}
368
369/// Returns the cyclotomic order `2N` for the ring `Z[X]/(X^N + 1)`.
370pub trait CyclotomicOrder
371where
372    Self: ModuleN,
373{
374    /// Returns `2N`, the order of the cyclotomic polynomial `X^N + 1`.
375    fn cyclotomic_order(&self) -> i64 {
376        (self.n() << 1) as _
377    }
378}
379
380impl<BE: Backend> ModuleLogN for Module<BE> where Self: ModuleN {}
381
382impl<BE: Backend> CyclotomicOrder for Module<BE> where Self: ModuleN {}
383
384/// Computes [`GALOISGENERATOR`]`^|generator| * sign(generator) mod cyclotomic_order`.
385///
386/// Returns `1` when `generator == 0`.
387///
388/// # Panics (debug)
389///
390/// Debug-asserts that `cyclotomic_order` is a positive power of two.
391#[inline(always)]
392pub fn galois_element(generator: i64, cyclotomic_order: i64) -> i64 {
393    debug_assert!(
394        cyclotomic_order > 0 && (cyclotomic_order as u64).is_power_of_two(),
395        "cyclotomic_order must be a power of two, got {cyclotomic_order}"
396    );
397
398    if generator == 0 {
399        return 1;
400    }
401
402    let g_exp: u64 = mod_exp_u64(GALOISGENERATOR, generator.unsigned_abs() as usize) & (cyclotomic_order - 1) as u64;
403    g_exp as i64 * generator.signum()
404}
405
406/// Maps a set of slot rotations to the distinct Galois elements whose
407/// automorphism keys realize them: drops the identity (`0`) rotation, applies
408/// [`galois_element`], and returns the result sorted and de-duplicated.
409///
410/// Shared by the linear-transformation / DFT layers, which all need "the Galois
411/// keys required by these rotations" and would otherwise each re-spell the
412/// filter/map/sort/dedup.
413pub fn galois_elements_from_rotations(rotations: impl IntoIterator<Item = i64>, cyclotomic_order: i64) -> Vec<i64> {
414    let mut gal_els: Vec<i64> = rotations
415        .into_iter()
416        .filter(|&rot| rot != 0)
417        .map(|rot| galois_element(rot, cyclotomic_order))
418        .collect();
419    gal_els.sort_unstable();
420    gal_els.dedup();
421    gal_els
422}
423
424/// Galois group operations on the cyclotomic ring `Z[X]/(X^N + 1)`.
425///
426/// The Galois group `(Z/2NZ)*` acts on polynomials via the automorphisms
427/// `X -> X^k` for odd `k`. This trait provides methods to compute
428/// Galois elements and their inverses from a signed generator exponent.
429pub trait GaloisElement
430where
431    Self: CyclotomicOrder,
432{
433    /// Returns [`GALOISGENERATOR`]`^|generator| * sign(generator) mod 2N`.
434    fn galois_element(&self, generator: i64) -> i64 {
435        galois_element(generator, self.cyclotomic_order())
436    }
437
438    /// Returns the inverse of `gal_el` in the Galois group `(Z/2NZ)*`.
439    ///
440    /// # Panics
441    ///
442    /// Panics if `gal_el == 0`.
443    fn galois_element_inv(&self, gal_el: i64) -> i64 {
444        if gal_el == 0 {
445            panic!("cannot invert 0")
446        }
447
448        let g_exp: u64 =
449            mod_exp_u64(gal_el.unsigned_abs(), (self.cyclotomic_order() - 1) as usize) & (self.cyclotomic_order() - 1) as u64;
450        g_exp as i64 * gal_el.signum()
451    }
452}
453
454impl<BE: Backend> GaloisElement for Module<BE> where Self: CyclotomicOrder {}
455
456impl<B: Backend> Drop for Module<B> {
457    fn drop(&mut self) {
458        unsafe { B::destroy(self.ptr) }
459    }
460}
461
462/// Computes `x^e mod 2^64` using square-and-multiply with wrapping arithmetic.
463pub fn mod_exp_u64(x: u64, e: usize) -> u64 {
464    let mut y: u64 = 1;
465    let mut x_pow: u64 = x;
466    let mut exp = e;
467    while exp > 0 {
468        if exp & 1 == 1 {
469            y = y.wrapping_mul(x_pow);
470        }
471        x_pow = x_pow.wrapping_mul(x_pow);
472        exp >>= 1;
473    }
474    y
475}