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