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}