poulpy/poulpy-schemes/src/bin_fhe/bdd_arithmetic/eval.rs
poulpy-schemes/src/bin_fhe/bdd_arithmetic/eval.rsRaw
1use core::panic;
2use std::thread;
3
4use itertools::Itertools;
5use poulpy_core::{
6 GLWECopy, GLWEExternalProductInternal, GLWENormalize, GLWESub, ScratchTakeCore,
7 layouts::{
8 GGSWInfos, GGSWPrepared, GLWE, GLWEInfos, GLWELayout, GLWEToMut, GLWEToRef, LWEInfos, prepared::GGSWPreparedToRef,
9 },
10};
11use poulpy_hal::{
12 api::{
13 ScratchAvailable, ScratchTakeBasic, VecZnxBigAddSmall, VecZnxBigAddSmallInplace, VecZnxBigBytesOf, VecZnxBigNormalize,
14 VecZnxBigNormalizeTmpBytes, VecZnxBigSubSmallA, VecZnxDftBytesOf,
15 },
16 layouts::{Backend, DataMut, Module, Scratch, VecZnxBig, ZnxInfos, ZnxZero},
17};
18
19use crate::bin_fhe::bdd_arithmetic::GetGGSWBit;
20
21pub trait BitCircuitInfo: Sync {
22 fn info(&self) -> (&[Node], usize);
23}
24
25pub trait GetBitCircuitInfo: Sync {
26 fn input_size(&self) -> usize;
27 fn output_size(&self) -> usize;
28 fn get_circuit(&self, bit: usize) -> (&[Node], usize);
29}
30
31pub struct BitCircuit<const N: usize> {
32 pub nodes: [Node; N],
33 pub max_inter_state: usize,
34}
35
36pub trait BitCircuitFamily {
37 const INPUT_BITS: usize;
38 const OUTPUT_BITS: usize;
39}
40
41pub struct Circuit<C: BitCircuitInfo, const N: usize>(pub [C; N]);
42
43impl<C, const N: usize> GetBitCircuitInfo for Circuit<C, N>
44where
45 C: BitCircuitInfo + BitCircuitFamily,
46{
47 fn input_size(&self) -> usize {
48 C::INPUT_BITS
49 }
50 fn output_size(&self) -> usize {
51 C::OUTPUT_BITS
52 }
53 fn get_circuit(&self, bit: usize) -> (&[Node], usize) {
54 self.0[bit].info()
55 }
56}
57
58pub trait ExecuteBDDCircuit<BE: Backend> {
59 fn execute_bdd_circuit_tmp_bytes<R, G>(&self, res_infos: &R, state_size: usize, ggsw_infos: &G) -> usize
60 where
61 R: GLWEInfos,
62 G: GGSWInfos;
63
64 fn execute_bdd_circuit<C, G, O>(&self, out: &mut [GLWE<O>], inputs: &G, circuit: &C, scratch: &mut Scratch<BE>)
65 where
66 G: GetGGSWBit<BE> + BitSize,
67 C: GetBitCircuitInfo,
68 O: DataMut,
69 {
70 self.execute_bdd_circuit_multi_thread(1, out, inputs, circuit, scratch);
71 }
72
73 fn execute_bdd_circuit_multi_thread<C, G, O>(
74 &self,
75 threads: usize,
76 out: &mut [GLWE<O>],
77 inputs: &G,
78 circuit: &C,
79 scratch: &mut Scratch<BE>,
80 ) where
81 G: GetGGSWBit<BE> + BitSize,
82 C: GetBitCircuitInfo,
83 O: DataMut;
84}
85
86pub trait BitSize {
87 fn bit_size(&self) -> usize;
88}
89
90impl<BE: Backend> ExecuteBDDCircuit<BE> for Module<BE>
91where
92 Self: Cmux<BE> + GLWECopy,
93 Scratch<BE>: ScratchTakeCore<BE>,
94{
95 fn execute_bdd_circuit_tmp_bytes<R, G>(&self, res_infos: &R, state_size: usize, ggsw_infos: &G) -> usize
96 where
97 R: GLWEInfos,
98 G: GGSWInfos,
99 {
100 2 * state_size * GLWE::bytes_of_from_infos(res_infos) + self.cmux_tmp_bytes(res_infos, res_infos, ggsw_infos)
101 }
102
103 fn execute_bdd_circuit_multi_thread<C, G, O>(
104 &self,
105 threads: usize,
106 out: &mut [GLWE<O>],
107 inputs: &G,
108 circuit: &C,
109 scratch: &mut Scratch<BE>,
110 ) where
111 G: GetGGSWBit<BE> + BitSize,
112 C: GetBitCircuitInfo,
113 O: DataMut,
114 {
115 #[cfg(debug_assertions)]
116 {
117 assert!(
118 inputs.bit_size() >= circuit.input_size(),
119 "inputs.bit_size(): {} < circuit.input_size():{}",
120 inputs.bit_size(),
121 circuit.input_size()
122 );
123 assert!(
124 out.len() >= circuit.output_size(),
125 "out.len(): {} < circuit.output_size(): {}",
126 out.len(),
127 circuit.output_size()
128 );
129 }
130
131 let mut max_state_size = 0;
132 for i in 0..circuit.output_size() {
133 let (_, state_size) = circuit.get_circuit(i);
134 max_state_size = max_state_size.max(state_size)
135 }
136
137 let scratch_thread_size: usize = self.execute_bdd_circuit_tmp_bytes(&out[0], max_state_size, &inputs.get_bit(0));
138
139 assert!(
140 scratch.available() >= threads * scratch_thread_size,
141 "scratch.available(): {} < threads:{threads} * scratch_thread_size: {scratch_thread_size}",
142 scratch.available()
143 );
144
145 let (mut scratches, _) = scratch.split_mut(threads, scratch_thread_size);
146
147 let chunk_size: usize = circuit.output_size().div_ceil(threads);
148
149 thread::scope(|scope| {
150 for (thread_idx, (scratch_thread, out_chunk)) in scratches
151 .iter_mut()
152 .zip(out[..circuit.output_size()].chunks_mut(chunk_size))
153 .enumerate()
154 {
155 // Capture chunk + thread scratch by move
156 scope.spawn(move || {
157 for (idx, out_i) in out_chunk.iter_mut().enumerate() {
158 let (nodes, state_size) = circuit.get_circuit(thread_idx * chunk_size + idx);
159
160 if state_size == 0 {
161 out_i.data_mut().zero();
162 } else {
163 eval_level(self, out_i, inputs, nodes, state_size, *scratch_thread);
164 }
165 }
166 });
167 }
168 });
169
170 for out_i in out.iter_mut().skip(circuit.output_size()) {
171 out_i.data_mut().zero();
172 }
173 }
174}
175
176fn eval_level<M, R, G, BE: Backend>(
177 module: &M,
178 res: &mut R,
179 inputs: &G,
180 nodes: &[Node],
181 state_size: usize,
182 scratch: &mut Scratch<BE>,
183) where
184 M: Cmux<BE> + GLWECopy,
185 R: GLWEToMut,
186 G: GetGGSWBit<BE> + BitSize,
187 Scratch<BE>: ScratchTakeCore<BE>,
188{
189 assert!(nodes.len().is_multiple_of(state_size));
190 let res: &mut GLWE<&mut [u8]> = &mut res.to_mut();
191
192 let (mut level, scratch_1) = scratch.take_glwe_slice(state_size * 2, res);
193
194 level.iter_mut().for_each(|ct| ct.data_mut().zero());
195
196 // TODO: implement API on GLWE
197 level[1]
198 .data_mut()
199 .encode_coeff_i64(res.base2k().into(), 0, 2, 0, 1);
200
201 let mut level_ref: Vec<&mut GLWE<&mut [u8]>> = level.iter_mut().collect_vec();
202 let (mut prev_level, mut next_level) = level_ref.split_at_mut(state_size);
203
204 let (all_but_last, last) = nodes.split_at(nodes.len() - state_size);
205
206 for nodes_lvl in all_but_last.chunks_exact(state_size) {
207 for (j, node) in nodes_lvl.iter().enumerate() {
208 match node {
209 Node::Cmux(in_idx, hi_idx, lo_idx) => {
210 module.cmux(
211 next_level[j],
212 prev_level[*hi_idx],
213 prev_level[*lo_idx],
214 &inputs.get_bit(*in_idx),
215 scratch_1,
216 );
217 }
218 Node::Copy => module.glwe_copy(next_level[j], prev_level[j]), /* Update BDD circuits to order Cmux -> Copy -> None so that mem swap can be used */
219 Node::None => {}
220 }
221 }
222
223 (prev_level, next_level) = (next_level, prev_level);
224 }
225
226 // Last chunck of max_inter_state Nodes is always structured as
227 // [CMUX, NONE, NONE, ..., NONE]
228 match &last[0] {
229 Node::Cmux(in_idx, hi_idx, lo_idx) => {
230 module.cmux(
231 res,
232 prev_level[*hi_idx],
233 prev_level[*lo_idx],
234 &inputs.get_bit(*in_idx),
235 scratch_1,
236 );
237 }
238 _ => {
239 panic!("invalid last node, should be CMUX")
240 }
241 }
242}
243
244impl<const N: usize> BitCircuit<N> {
245 pub const fn new(nodes: [Node; N], max_inter_state: usize) -> Self {
246 Self {
247 nodes,
248 max_inter_state,
249 }
250 }
251}
252impl<const N: usize> BitCircuitInfo for BitCircuit<N> {
253 fn info(&self) -> (&[Node], usize) {
254 (self.nodes.as_ref(), self.max_inter_state)
255 }
256}
257
258#[derive(Debug)]
259pub enum Node {
260 Cmux(usize, usize, usize),
261 Copy,
262 None,
263}
264
265impl<BE: Backend> Cswap<BE> for Module<BE> where
266 Self: Sized
267 + GLWEExternalProductInternal<BE>
268 + GLWESub
269 + VecZnxBigAddSmallInplace<BE>
270 + GLWENormalize<BE>
271 + VecZnxDftBytesOf
272 + VecZnxBigNormalize<BE>
273 + VecZnxBigNormalizeTmpBytes
274 + GLWENormalize<BE>
275 + VecZnxBigAddSmall<BE>
276 + VecZnxBigSubSmallA<BE>
277 + VecZnxBigBytesOf
278{
279}
280
281pub trait Cswap<BE: Backend>
282where
283 Self: Sized
284 + GLWEExternalProductInternal<BE>
285 + GLWESub
286 + GLWECopy
287 + VecZnxBigAddSmallInplace<BE>
288 + GLWENormalize<BE>
289 + VecZnxDftBytesOf
290 + VecZnxBigNormalize<BE>
291 + VecZnxBigNormalizeTmpBytes
292 + GLWENormalize<BE>
293 + VecZnxBigAddSmall<BE>
294 + VecZnxBigSubSmallA<BE>
295 + VecZnxBigBytesOf,
296{
297 fn cswap_tmp_bytes<R, A, S>(&self, res_a_infos: &R, res_b_infos: &A, s_infos: &S) -> usize
298 where
299 R: GLWEInfos,
300 A: GLWEInfos,
301 S: GGSWInfos,
302 {
303 let res_dft: usize = self.bytes_of_vec_znx_dft((s_infos.rank() + 1).into(), s_infos.size());
304 let mut tot = res_dft
305 + (self.glwe_external_product_internal_tmp_bytes(res_a_infos, res_b_infos, s_infos)
306 + GLWE::bytes_of_from_infos(&GLWELayout {
307 n: s_infos.n(),
308 base2k: s_infos.base2k(),
309 k: res_a_infos.k().max(res_b_infos.k()),
310 rank: s_infos.rank(),
311 }))
312 .max(self.vec_znx_big_normalize_tmp_bytes());
313
314 if res_a_infos.base2k() != s_infos.base2k() {
315 tot += GLWE::bytes_of_from_infos(&GLWELayout {
316 n: res_a_infos.n(),
317 base2k: s_infos.base2k(),
318 k: res_a_infos.k(),
319 rank: res_a_infos.rank(),
320 });
321
322 tot += GLWE::bytes_of_from_infos(&GLWELayout {
323 n: res_b_infos.n(),
324 base2k: s_infos.base2k(),
325 k: res_b_infos.k(),
326 rank: res_b_infos.rank(),
327 });
328 }
329
330 tot += self.bytes_of_vec_znx_big(1, s_infos.size());
331
332 tot
333 }
334
335 fn cswap<A, B, S>(&self, res_a: &mut A, res_b: &mut B, s: &S, scratch: &mut Scratch<BE>)
336 where
337 A: GLWEToMut,
338 B: GLWEToMut,
339 S: GGSWPreparedToRef<BE> + GGSWInfos,
340 Scratch<BE>: ScratchTakeCore<BE>,
341 {
342 let res_a: &mut GLWE<&mut [u8]> = &mut res_a.to_mut();
343 let res_b: &mut GLWE<&mut [u8]> = &mut res_b.to_mut();
344 let s: &GGSWPrepared<&[u8], BE> = &s.to_ref();
345 assert_eq!(res_a.base2k(), res_b.base2k());
346
347 let res_base2k: usize = res_a.base2k().as_usize();
348 let s_base2k: usize = s.base2k().as_usize();
349
350 if res_base2k == s_base2k {
351 let res_big: VecZnxBig<&mut [u8], BE>;
352 let (res_dft, scratch_1) = scratch.take_vec_znx_dft(self, (s.rank() + 1).into(), s.size()); // Todo optimise
353 {
354 // Temporary value storing a - b
355 let tmp_c_infos: GLWELayout = GLWELayout {
356 n: s.n(),
357 base2k: s.base2k(),
358 k: res_a.k().max(res_b.k()),
359 rank: s.rank(),
360 };
361 let (mut tmp_c, scratch_2) = scratch_1.take_glwe(&tmp_c_infos);
362 self.glwe_sub(&mut tmp_c, res_b, res_a);
363 res_big = self.glwe_external_product_internal(res_dft, &tmp_c, s, scratch_2);
364 }
365
366 // Single column res_big to store temporary value before normalization
367 let (mut res_big_tmp, scratch_2) = scratch_1.take_vec_znx_big::<_, BE>(self, 1, res_big.size());
368
369 // res_a = (b-a) * bit + a
370 for j in 0..(res_a.rank() + 1).into() {
371 self.vec_znx_big_add_small(&mut res_big_tmp, 0, &res_big, j, res_a.data(), j);
372 self.vec_znx_big_normalize(
373 res_base2k,
374 res_a.data_mut(),
375 j,
376 s_base2k,
377 &res_big_tmp,
378 0,
379 scratch_2,
380 );
381 }
382
383 // res_b = a - (a - b) * bit = (b - a) * bit + a
384 for j in 0..(res_b.rank() + 1).into() {
385 self.vec_znx_big_sub_small_a(&mut res_big_tmp, 0, res_b.data(), j, &res_big, j);
386 self.vec_znx_big_normalize(
387 res_base2k,
388 res_b.data_mut(),
389 j,
390 s_base2k,
391 &res_big_tmp,
392 0,
393 scratch_2,
394 );
395 }
396 } else {
397 let (mut tmp_a, scratch_1) = scratch.take_glwe(&GLWELayout {
398 n: res_a.n(),
399 base2k: s.base2k(),
400 k: res_a.k(),
401 rank: res_a.rank(),
402 });
403
404 let (mut tmp_b, scratch_2) = scratch_1.take_glwe(&GLWELayout {
405 n: res_b.n(),
406 base2k: s.base2k(),
407 k: res_b.k(),
408 rank: res_b.rank(),
409 });
410
411 self.glwe_normalize(&mut tmp_a, res_a, scratch_2);
412 self.glwe_normalize(&mut tmp_b, res_b, scratch_2);
413
414 let res_big: VecZnxBig<&mut [u8], BE>;
415 let (res_dft, scratch_3) = scratch_2.take_vec_znx_dft(self, (s.rank() + 1).into(), s.size()); // Todo optimise
416 {
417 // Temporary value storing a - b
418 let tmp_c_infos: GLWELayout = GLWELayout {
419 n: s.n(),
420 base2k: s.base2k(),
421 k: res_a.k().max(res_b.k()),
422 rank: s.rank(),
423 };
424 let (mut tmp_c, scratch_4) = scratch_3.take_glwe(&tmp_c_infos);
425 self.glwe_sub(&mut tmp_c, res_b, res_a);
426 res_big = self.glwe_external_product_internal(res_dft, &tmp_c, s, scratch_4);
427 }
428
429 // Single column res_big to store temporary value before normalization
430 let (mut res_big_tmp, scratch_4) = scratch_3.take_vec_znx_big::<_, BE>(self, 1, res_big.size());
431
432 // res_a = (b-a) * bit + a
433 for j in 0..(res_a.rank() + 1).into() {
434 self.vec_znx_big_add_small(&mut res_big_tmp, 0, &res_big, j, tmp_a.data(), j);
435 self.vec_znx_big_normalize(
436 res_base2k,
437 res_a.data_mut(),
438 j,
439 s_base2k,
440 &res_big_tmp,
441 0,
442 scratch_4,
443 );
444 }
445
446 // res_b = a - (a - b) * bit = (b - a) * bit + a
447 for j in 0..(res_b.rank() + 1).into() {
448 self.vec_znx_big_sub_small_a(&mut res_big_tmp, 0, tmp_b.data(), j, &res_big, j);
449 self.vec_znx_big_normalize(
450 res_base2k,
451 res_b.data_mut(),
452 j,
453 s_base2k,
454 &res_big_tmp,
455 0,
456 scratch_4,
457 );
458 }
459 }
460 }
461}
462
463pub trait Cmux<BE: Backend>
464where
465 Self: Sized
466 + GLWEExternalProductInternal<BE>
467 + GLWESub
468 + VecZnxBigAddSmallInplace<BE>
469 + GLWENormalize<BE>
470 + VecZnxDftBytesOf
471 + VecZnxBigNormalize<BE>
472 + VecZnxBigNormalizeTmpBytes,
473{
474 fn cmux_tmp_bytes<R, A, B>(&self, res_infos: &R, a_infos: &A, selector_infos: &B) -> usize
475 where
476 R: GLWEInfos,
477 A: GLWEInfos,
478 B: GGSWInfos,
479 {
480 let res_dft: usize = self.bytes_of_vec_znx_dft((selector_infos.rank() + 1).into(), selector_infos.size());
481 res_dft
482 + self
483 .glwe_external_product_internal_tmp_bytes(res_infos, a_infos, selector_infos)
484 .max(self.vec_znx_big_normalize_tmp_bytes())
485 }
486
487 // res = (t - f) * s + f
488 fn cmux<R, T, F, S>(&self, res: &mut R, t: &T, f: &F, s: &S, scratch: &mut Scratch<BE>)
489 where
490 R: GLWEToMut,
491 T: GLWEToRef,
492 F: GLWEToRef,
493 S: GGSWPreparedToRef<BE> + GGSWInfos,
494 Scratch<BE>: ScratchTakeCore<BE>,
495 {
496 let res: &mut GLWE<&mut [u8]> = &mut res.to_mut();
497 let s: &GGSWPrepared<&[u8], BE> = &s.to_ref();
498 let f: GLWE<&[u8]> = f.to_ref();
499
500 let res_base2k: usize = res.base2k().into();
501 let ggsw_base2k: usize = s.base2k().into();
502
503 self.glwe_sub(res, t, &f);
504 let (res_dft, scratch_1) = scratch.take_vec_znx_dft(self, (res.rank() + 1).into(), s.size()); // Todo optimise
505 let mut res_big: VecZnxBig<&mut [u8], BE> = self.glwe_external_product_internal(res_dft, res, s, scratch_1);
506 for j in 0..(res.rank() + 1).into() {
507 self.vec_znx_big_add_small_inplace(&mut res_big, j, f.data(), j);
508 self.vec_znx_big_normalize(
509 res_base2k,
510 res.data_mut(),
511 j,
512 ggsw_base2k,
513 &res_big,
514 j,
515 scratch_1,
516 );
517 }
518 }
519
520 // res = (a - res) * s + res
521 fn cmux_inplace_neg<R, A, S>(&self, res: &mut R, a: &A, s: &S, scratch: &mut Scratch<BE>)
522 where
523 R: GLWEToMut,
524 A: GLWEToRef,
525 S: GGSWPreparedToRef<BE> + GGSWInfos,
526 Scratch<BE>: ScratchTakeCore<BE>,
527 {
528 let res: &mut GLWE<&mut [u8]> = &mut res.to_mut();
529 let s: &GGSWPrepared<&[u8], BE> = &s.to_ref();
530 let a: &GLWE<&[u8]> = &a.to_ref();
531
532 assert_eq!(res.base2k(), a.base2k());
533
534 let res_base2k: usize = res.base2k().into();
535 let ggsw_base2k: usize = s.base2k().into();
536 let (mut tmp, scratch_1) = scratch.take_glwe(&GLWELayout {
537 n: s.n(),
538 base2k: res.base2k(),
539 k: res.k().max(a.k()),
540 rank: res.rank(),
541 });
542 self.glwe_sub(&mut tmp, a, res);
543 let (res_dft, scratch_2) = scratch_1.take_vec_znx_dft(self, (res.rank() + 1).into(), s.size()); // Todo optimise
544 let mut res_big: VecZnxBig<&mut [u8], BE> = self.glwe_external_product_internal(res_dft, &tmp, s, scratch_2);
545 for j in 0..(res.rank() + 1).into() {
546 self.vec_znx_big_add_small_inplace(&mut res_big, j, res.data(), j);
547 self.vec_znx_big_normalize(
548 res_base2k,
549 res.data_mut(),
550 j,
551 ggsw_base2k,
552 &res_big,
553 j,
554 scratch_2,
555 );
556 }
557 }
558
559 // res = (res - a) * s + a
560 fn cmux_inplace<R, A, S>(&self, res: &mut R, a: &A, s: &S, scratch: &mut Scratch<BE>)
561 where
562 R: GLWEToMut,
563 A: GLWEToRef,
564 S: GGSWPreparedToRef<BE> + GGSWInfos,
565 Scratch<BE>: ScratchTakeCore<BE>,
566 {
567 let res: &mut GLWE<&mut [u8]> = &mut res.to_mut();
568 let s: &GGSWPrepared<&[u8], BE> = &s.to_ref();
569 let a: GLWE<&[u8]> = a.to_ref();
570 let res_base2k: usize = res.base2k().into();
571 let ggsw_base2k: usize = s.base2k().into();
572 self.glwe_sub_inplace(res, &a);
573 let (res_dft, scratch_1) = scratch.take_vec_znx_dft(self, (res.rank() + 1).into(), s.size()); // Todo optimise
574 let mut res_big: VecZnxBig<&mut [u8], BE> = self.glwe_external_product_internal(res_dft, res, s, scratch_1);
575 for j in 0..(res.rank() + 1).into() {
576 self.vec_znx_big_add_small_inplace(&mut res_big, j, a.data(), j);
577 self.vec_znx_big_normalize(
578 res_base2k,
579 res.data_mut(),
580 j,
581 ggsw_base2k,
582 &res_big,
583 j,
584 scratch_1,
585 );
586 }
587 }
588}
589
590impl<BE: Backend> Cmux<BE> for Module<BE>
591where
592 Self: Sized
593 + GLWEExternalProductInternal<BE>
594 + GLWESub
595 + VecZnxBigAddSmallInplace<BE>
596 + GLWENormalize<BE>
597 + VecZnxDftBytesOf
598 + VecZnxBigNormalize<BE>
599 + VecZnxBigNormalizeTmpBytes,
600 Scratch<BE>: ScratchTakeCore<BE>,
601{
602}