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}