1use aes::Aes128;
28use aes::cipher::{Array, BlockCipherEncrypt, KeyInit};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
33pub enum RngBackend {
34 #[default]
36 Philox,
37 Ort,
39 Bnns,
41 Zero,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
48pub struct RngOptions {
49 pub seed: u64,
51 pub backend: RngBackend,
52}
53
54impl Default for RngOptions {
55 fn default() -> Self {
56 Self {
57 seed: 42,
58 backend: RngBackend::Philox,
59 }
60 }
61}
62
63impl RngOptions {
64 pub const fn new(seed: u64, backend: RngBackend) -> Self {
65 Self { seed, backend }
66 }
67
68 pub fn philox(seed: u64) -> Self {
69 Self {
70 seed,
71 backend: RngBackend::Philox,
72 }
73 }
74
75 pub fn ort(seed: u64) -> Self {
76 Self {
77 seed,
78 backend: RngBackend::Ort,
79 }
80 }
81
82 pub fn bnns(seed: u64) -> Self {
83 Self {
84 seed,
85 backend: RngBackend::Bnns,
86 }
87 }
88
89 pub fn zero() -> Self {
90 Self {
91 seed: 0,
92 backend: RngBackend::Zero,
93 }
94 }
95}
96
97pub fn combine_seed(global: u64, key: u64) -> u64 {
99 global.wrapping_add(key.wrapping_mul(0x9E37_79B9_7F4A_7C15))
100}
101
102pub fn ort_engine_seed(global: u64, key: u64, op_seed: Option<f32>) -> u32 {
104 if let Some(s) = op_seed {
105 s as u32
106 } else {
107 global.wrapping_add(key) as u32
108 }
109}
110
111pub fn fill_normal_like(
113 out: &mut [f32],
114 mean: f32,
115 scale: f32,
116 opts: RngOptions,
117 key: u64,
118 op_seed: Option<f32>,
119) {
120 match opts.backend {
121 RngBackend::Zero => out.fill(0.0),
122 RngBackend::Philox => {
123 let mut rng = Philox4x32::new(combine_seed(opts.seed, key));
124 for v in out.iter_mut() {
125 *v = mean + scale * rng.normal();
126 }
127 }
128 RngBackend::Ort => {
129 let mut eng = MinstdRand0::new(ort_engine_seed(opts.seed, key, op_seed));
130 let mut dist = StdNormalDist::new(mean, scale);
131 for v in out.iter_mut() {
132 *v = dist.sample(&mut eng);
133 }
134 }
135 RngBackend::Bnns => {
136 let _ = op_seed;
137 let mut rng = BnnsAesCtr128::new(combine_seed(opts.seed, key));
138 rng.fill_normal(out, mean, scale);
139 }
140 }
141}
142
143pub fn fill_uniform_like(
145 out: &mut [f32],
146 low: f32,
147 high: f32,
148 opts: RngOptions,
149 key: u64,
150 op_seed: Option<f32>,
151) {
152 match opts.backend {
153 RngBackend::Zero => out.fill(0.0),
154 RngBackend::Philox => {
155 let mut rng = Philox4x32::new(combine_seed(opts.seed, key));
156 for v in out.iter_mut() {
157 *v = rng.uniform(low, high);
158 }
159 }
160 RngBackend::Ort => {
161 let mut eng = MinstdRand0::new(ort_engine_seed(opts.seed, key, op_seed));
162 for v in out.iter_mut() {
163 *v = low + (high - low) * eng.unit_f32();
164 }
165 }
166 RngBackend::Bnns => {
167 let _ = op_seed;
168 let mut rng = BnnsAesCtr128::new(combine_seed(opts.seed, key));
169 rng.fill_uniform(out, low, high);
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy)]
177pub struct Philox4x32 {
178 seed: [u32; 2],
179 counter: [u32; 4],
180 buffer: [u32; 4],
182 cursor: u8,
183}
184
185impl Philox4x32 {
186 pub const fn new(seed: u64) -> Self {
187 let lo = (seed & 0xFFFF_FFFF) as u32;
188 let hi = (seed >> 32) as u32;
189 Self {
190 seed: [lo, hi],
191 counter: [0, 0, 0, 0],
192 buffer: [0; 4],
193 cursor: 4, }
195 }
196
197 fn round(state: &mut [u32; 4], key: [u32; 2]) {
198 const M0: u64 = 0xD256_1A75;
199 const M1: u64 = 0xCD9E_8D57;
200 let p0 = (state[0] as u64) * M0;
201 let p1 = (state[2] as u64) * M1;
202 let hi0 = (p0 >> 32) as u32;
203 let lo0 = p0 as u32;
204 let hi1 = (p1 >> 32) as u32;
205 let lo1 = p1 as u32;
206 state[0] = hi1 ^ state[1] ^ key[0];
207 state[1] = lo1;
208 state[2] = hi0 ^ state[3] ^ key[1];
209 state[3] = lo0;
210 }
211
212 fn fill_buffer(&mut self) {
213 let mut state = self.counter;
214 let mut key = self.seed;
215 for _ in 0..10 {
216 Self::round(&mut state, key);
217 key[0] = key[0].wrapping_add(0x9E37_79B9);
219 key[1] = key[1].wrapping_add(0xBB67_AE85);
220 }
221 self.buffer = state;
222 self.cursor = 0;
223
224 let (c0, of0) = self.counter[0].overflowing_add(1);
226 self.counter[0] = c0;
227 if of0 {
228 let (c1, of1) = self.counter[1].overflowing_add(1);
229 self.counter[1] = c1;
230 if of1 {
231 let (c2, of2) = self.counter[2].overflowing_add(1);
232 self.counter[2] = c2;
233 if of2 {
234 self.counter[3] = self.counter[3].wrapping_add(1);
235 }
236 }
237 }
238 }
239
240 pub fn next_u32(&mut self) -> u32 {
241 if self.cursor >= 4 {
242 self.fill_buffer();
243 }
244 let v = self.buffer[self.cursor as usize];
245 self.cursor += 1;
246 v
247 }
248
249 pub fn next_f32(&mut self) -> f32 {
252 let bits = self.next_u32() >> 8;
253 bits as f32 / (1u32 << 24) as f32
254 }
255
256 pub fn uniform(&mut self, lo: f32, hi: f32) -> f32 {
258 lo + self.next_f32() * (hi - lo)
259 }
260
261 pub fn normal(&mut self) -> f32 {
265 let u1 = self.next_f32().max(f32::MIN_POSITIVE);
266 let u2 = self.next_f32();
267 let r = (-2.0 * u1.ln()).sqrt();
268 let theta = 2.0 * std::f32::consts::PI * u2;
269 r * theta.cos()
270 }
271
272 pub fn fill_uniform(&mut self, out: &mut [f32]) {
275 for v in out {
276 *v = self.next_f32();
277 }
278 }
279
280 pub fn fill_normal(&mut self, out: &mut [f32]) {
282 for v in out {
283 *v = self.normal();
284 }
285 }
286
287 pub fn gumbel01(&mut self) -> f32 {
292 gumbel01_from_uniform(self.next_f32())
293 }
294
295 pub fn gumbel_max_argmax(&mut self, logits: &[f32], scale: f32) -> usize {
301 gumbel_max_argmax_with(logits, scale, || self.gumbel01())
302 }
303}
304
305pub struct BnnsAesCtr128 {
313 cipher: Aes128,
314 counter: u128,
315 block: [u8; 16],
316 cursor: u8,
317 normal_spare: Option<f32>,
319}
320
321impl BnnsAesCtr128 {
322 pub fn new(seed: u64) -> Self {
325 let seed = seed.to_le_bytes();
326 let mut key = [0u8; 16];
327 key[..8].copy_from_slice(&seed);
328 key[8..].copy_from_slice(&seed);
329 Self {
330 cipher: Aes128::new(&Array::from(key)),
331 counter: 0,
332 block: [0; 16],
333 cursor: 16,
334 normal_spare: None,
335 }
336 }
337
338 pub fn from_entropy() -> Self {
345 Self::new(bnns_entropy_seed())
346 }
347
348 pub fn try_from_fresh_generator_state(state: &[u8]) -> Option<Self> {
355 let seed = bnns_seed_from_generator_state(state)?;
356 if !bnns_generator_state_is_fresh(state) {
357 return None;
358 }
359 Some(Self::new(seed))
360 }
361
362 fn fill_block(&mut self) {
363 let mut block = Array::from(self.counter.to_be_bytes());
364 self.cipher.encrypt_block(&mut block);
365 self.block.copy_from_slice(&block);
366 self.counter = self.counter.wrapping_add(1);
367 self.cursor = 0;
368 }
369
370 pub fn next_u32(&mut self) -> u32 {
372 if self.cursor >= 16 {
373 self.fill_block();
374 }
375 let start = self.cursor as usize;
376 self.cursor += 4;
377 u32::from_le_bytes(
378 self.block[start..start + 4]
379 .try_into()
380 .expect("four-byte AES word"),
381 )
382 }
383
384 pub fn next_f32(&mut self) -> f32 {
390 bnns_uniform_f32_from_u32(self.next_u32())
391 }
392
393 pub fn uniform(&mut self, lo: f32, hi: f32) -> f32 {
395 self.next_f32().mul_add(hi - lo, lo)
396 }
397
398 pub fn fill_uniform(&mut self, out: &mut [f32], lo: f32, hi: f32) {
400 for value in out {
401 *value = self.uniform(lo, hi);
402 }
403 }
404
405 pub fn gumbel01(&mut self) -> f32 {
407 gumbel01_from_uniform(self.next_f32())
408 }
409
410 pub fn gumbel(&mut self, alpha: f32, beta: f32) -> f32 {
412 bnns_gumbel_from_uniform(self.next_f32(), alpha, beta)
413 }
414
415 pub fn fill_gumbel01(&mut self, out: &mut [f32]) {
417 for value in out {
418 *value = self.gumbel01();
419 }
420 }
421
422 pub fn fill_gumbel(&mut self, out: &mut [f32], alpha: f32, beta: f32) {
424 for value in out {
425 *value = self.gumbel(alpha, beta);
426 }
427 }
428
429 pub fn normal(&mut self) -> f32 {
435 if let Some(spare) = self.normal_spare.take() {
436 return spare;
437 }
438 let u1 = self.next_f32().clamp(f32::MIN_POSITIVE, 1.0 - f32::EPSILON);
439 let u2 = self.next_f32().clamp(0.0, 1.0 - f32::EPSILON);
440 let r = (-2.0 * u1.ln()).sqrt();
441 let theta = 2.0 * std::f32::consts::PI * u2;
442 self.normal_spare = Some(r * theta.sin());
443 r * theta.cos()
444 }
445
446 pub fn fill_normal(&mut self, out: &mut [f32], mean: f32, stddev: f32) {
448 self.normal_spare = None;
449 for value in out {
450 *value = self.normal().mul_add(stddev, mean);
451 }
452 self.normal_spare = None;
453 }
454
455 pub fn uniform_int(&mut self, lo: i64, hi: i64) -> i64 {
461 bnns_uniform_int_from_u32(self.next_u32(), lo, hi)
462 }
463
464 pub fn fill_uniform_int(&mut self, out: &mut [i64], lo: i64, hi: i64) {
466 for value in out {
467 *value = self.uniform_int(lo, hi);
468 }
469 }
470
471 pub fn fill_categorical(
476 &mut self,
477 out: &mut [f32],
478 probabilities: &[f32],
479 log_probabilities: bool,
480 ) {
481 let probs = bnns_normalize_probs(probabilities, log_probabilities);
482 for value in out {
483 *value = bnns_categorical_from_uniform(self.next_f32(), &probs) as f32;
484 }
485 }
486
487 pub fn gumbel_max_argmax(&mut self, logits: &[f32], scale: f32) -> usize {
489 gumbel_max_argmax_with(logits, scale, || self.gumbel01())
490 }
491
492 pub fn bnns_gumbel_max_argmax(&mut self, logits: &[f32], alpha: f32, beta: f32) -> usize {
494 gumbel_max_argmax_with(logits, 1.0, || self.gumbel(alpha, beta))
495 }
496}
497
498#[cfg(target_os = "macos")]
504pub struct NativeBnnsGumbelMax {
505 _guard: std::sync::MutexGuard<'static, ()>,
506}
507
508#[cfg(target_os = "macos")]
509static NATIVE_BNNS_GUMBEL_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
510
511#[cfg(target_os = "macos")]
512impl NativeBnnsGumbelMax {
513 pub fn new(seed: u64) -> Option<Self> {
515 #[link(name = "Accelerate", kind = "framework")]
516 unsafe extern "C" {
517 fn BNNSInitGumbel(seed: u64);
518 }
519
520 let guard = NATIVE_BNNS_GUMBEL_LOCK.lock().ok()?;
521 unsafe { BNNSInitGumbel(seed) };
524 Some(Self { _guard: guard })
525 }
526
527 pub fn argmax(&mut self, logits: &[f32], alpha: f32, beta: f32) -> Option<usize> {
529 native_bnns_gumbel_max_argmax_impl(logits, alpha, beta)
530 }
531}
532
533#[cfg(not(target_os = "macos"))]
534pub struct NativeBnnsGumbelMax;
535
536#[cfg(not(target_os = "macos"))]
537impl NativeBnnsGumbelMax {
538 pub fn new(_seed: u64) -> Option<Self> {
539 None
540 }
541
542 pub fn argmax(&mut self, _logits: &[f32], _alpha: f32, _beta: f32) -> Option<usize> {
543 None
544 }
545}
546
547#[cfg(target_os = "macos")]
548fn native_bnns_gumbel_max_argmax_impl(logits: &[f32], alpha: f32, beta: f32) -> Option<usize> {
549 #[repr(C, align(64))]
550 #[derive(Clone, Copy)]
551 struct AlignedF32Block([f32; 16]);
552
553 #[repr(C)]
554 struct BnnsNdArrayDescriptor {
555 flags: u32,
556 layout: u32,
557 size: [usize; 8],
558 stride: [usize; 8],
559 data: *mut core::ffi::c_void,
560 data_type: u32,
561 table_data: *mut core::ffi::c_void,
562 table_data_type: u32,
563 data_scale: f32,
564 data_bias: f32,
565 }
566
567 #[repr(C)]
568 struct BnnsActivation {
569 function: u32,
570 alpha: f32,
571 beta: f32,
572 iscale: i32,
573 ioffset: i32,
574 ishift: i32,
575 iscale_per_channel: *const i32,
576 ioffset_per_channel: *const i32,
577 ishift_per_channel: *const i32,
578 }
579
580 #[repr(C)]
581 struct BnnsLayerParametersActivation {
582 i_desc: BnnsNdArrayDescriptor,
583 o_desc: BnnsNdArrayDescriptor,
584 activation: BnnsActivation,
585 axis_flags: u32,
586 }
587
588 #[link(name = "Accelerate", kind = "framework")]
589 unsafe extern "C" {
590 fn BNNSDirectApplyActivationBatch(
591 layer_params: *const BnnsLayerParametersActivation,
592 filter_params: *const core::ffi::c_void,
593 batch_size: usize,
594 in_stride: usize,
595 out_stride: usize,
596 ) -> i32;
597 }
598
599 const BNNS_LAYOUT_VECTOR: u32 = 0x1_0000;
600 const BNNS_FLOAT32: u32 = 0x1_0000 | 32;
601 const BNNS_ACTIVATION_GUMBEL_MAX: u32 = 14;
602
603 fn desc(data: *mut f32, n: usize) -> BnnsNdArrayDescriptor {
604 BnnsNdArrayDescriptor {
605 flags: 0,
606 layout: BNNS_LAYOUT_VECTOR,
607 size: [n, 0, 0, 0, 0, 0, 0, 0],
608 stride: [0; 8],
609 data: data.cast(),
610 data_type: BNNS_FLOAT32,
611 table_data: core::ptr::null_mut(),
612 table_data_type: 0,
613 data_scale: 1.0,
614 data_bias: 0.0,
615 }
616 }
617
618 if logits.is_empty() {
619 return None;
620 }
621 let mut input = vec![AlignedF32Block([0.0; 16]); logits.len().div_ceil(16)];
624 for (i, &value) in logits.iter().enumerate() {
625 input[i / 16].0[i % 16] = value;
626 }
627 let mut output = AlignedF32Block([0.0; 16]);
628 let params = BnnsLayerParametersActivation {
629 i_desc: desc(input.as_mut_ptr().cast(), logits.len()),
630 o_desc: desc(output.0.as_mut_ptr(), 1),
631 activation: BnnsActivation {
632 function: BNNS_ACTIVATION_GUMBEL_MAX,
633 alpha,
634 beta,
635 iscale: 0,
636 ioffset: 0,
637 ishift: 0,
638 iscale_per_channel: core::ptr::null(),
639 ioffset_per_channel: core::ptr::null(),
640 ishift_per_channel: core::ptr::null(),
641 },
642 axis_flags: 0,
643 };
644 let rc =
647 unsafe { BNNSDirectApplyActivationBatch(¶ms, core::ptr::null(), 1, logits.len(), 1) };
648 let output = output.0[0];
649 if rc != 0 || !output.is_finite() || output < 0.0 {
650 return None;
651 }
652 let index = output as usize;
653 (index < logits.len() && output == index as f32).then_some(index)
654}
655
656pub const BNNS_AES_CTR_STATE_SIZE: usize = 40;
658
659pub fn bnns_seed_from_generator_state(state: &[u8]) -> Option<u64> {
665 if state.len() < 8 {
666 return None;
667 }
668 Some(u64::from_le_bytes(state[..8].try_into().ok()?))
669}
670
671pub fn bnns_generator_state_is_fresh(state: &[u8]) -> bool {
674 state.len() >= BNNS_AES_CTR_STATE_SIZE
675 && state[16..BNNS_AES_CTR_STATE_SIZE].iter().all(|&b| b == 0)
676}
677
678pub fn bnns_entropy_seed() -> u64 {
683 let mut bytes = [0u8; 8];
684 fill_os_entropy(&mut bytes);
685 u64::from_le_bytes(bytes)
686}
687
688#[cfg(unix)]
689fn fill_os_entropy(out: &mut [u8]) {
690 #[link(name = "c")]
692 unsafe extern "C" {
693 fn getentropy(buf: *mut core::ffi::c_void, buflen: usize) -> i32;
694 }
695 let rc = unsafe { getentropy(out.as_mut_ptr().cast(), out.len()) };
696 if rc == 0 {
697 return;
698 }
699 let t = std::time::SystemTime::now()
701 .duration_since(std::time::UNIX_EPOCH)
702 .map(|d| d.as_nanos() as u64)
703 .unwrap_or(0x0C0F_FEE0_0D15_EA5E);
704 out.copy_from_slice(&t.to_le_bytes()[..out.len().min(8)]);
705}
706
707#[cfg(not(unix))]
708fn fill_os_entropy(out: &mut [u8]) {
709 let t = std::time::SystemTime::now()
710 .duration_since(std::time::UNIX_EPOCH)
711 .map(|d| d.as_nanos() as u64)
712 .unwrap_or(0x0C0F_FEE0_0D15_EA5E);
713 let b = t.to_le_bytes();
714 for (i, slot) in out.iter_mut().enumerate() {
715 *slot = b[i % 8];
716 }
717}
718
719pub fn bnns_uniform_f32_from_u32(word: u32) -> f32 {
724 (word as f32) * (1.0 / 4_294_967_296.0)
725}
726
727pub fn bnns_uniform_f32_from_bytes(bytes: [u8; 4]) -> f32 {
729 bnns_uniform_f32_from_u32(u32::from_le_bytes(bytes))
730}
731
732pub fn bnns_gumbel01_from_bytes(bytes: [u8; 4]) -> f32 {
735 gumbel01_from_uniform(bnns_uniform_f32_from_bytes(bytes))
736}
737
738pub fn bnns_uniform_int_from_u32(word: u32, lo: i64, hi: i64) -> i64 {
742 if lo >= hi {
743 return lo;
744 }
745 let span = (hi - lo) as u64;
746 let offset = ((word as u64).wrapping_mul(span)) >> 32;
747 lo.wrapping_add(offset as i64)
748}
749
750pub fn bnns_gumbel_from_uniform(u: f32, alpha: f32, beta: f32) -> f32 {
754 let u = if beta == 0.0 {
755 u.clamp(GUMBEL_UNIFORM_EPS, 1.0 - GUMBEL_UNIFORM_EPS)
756 } else {
757 u
758 };
759 let inner = alpha.mul_add(u, beta).max(f32::MIN_POSITIVE);
760 -((-inner.ln()) + beta).ln()
761}
762
763pub fn bnns_categorical_from_uniform(u: f32, probabilities: &[f32]) -> usize {
765 if probabilities.is_empty() {
766 return 0;
767 }
768 let mut cum = 0.0f32;
769 for (i, &p) in probabilities.iter().enumerate() {
770 cum += p;
771 if u < cum {
772 return i;
773 }
774 }
775 probabilities.len() - 1
776}
777
778fn bnns_normalize_probs(probabilities: &[f32], log_probabilities: bool) -> Vec<f32> {
779 if probabilities.is_empty() {
780 return Vec::new();
781 }
782 if log_probabilities {
783 let max = probabilities
784 .iter()
785 .copied()
786 .fold(f32::NEG_INFINITY, f32::max);
787 let mut exps: Vec<f32> = probabilities.iter().map(|&p| (p - max).exp()).collect();
788 let sum: f32 = exps.iter().sum();
789 if sum > 0.0 {
790 for p in &mut exps {
791 *p /= sum;
792 }
793 }
794 exps
795 } else {
796 let sum: f32 = probabilities.iter().sum();
797 if sum > 0.0 {
798 probabilities.iter().map(|&p| p / sum).collect()
799 } else {
800 probabilities.to_vec()
801 }
802 }
803}
804
805pub const GUMBEL_UNIFORM_EPS: f32 = 1e-6;
807
808pub fn gumbel01_from_uniform(u: f32) -> f32 {
810 let u = u.clamp(GUMBEL_UNIFORM_EPS, 1.0 - GUMBEL_UNIFORM_EPS);
811 -(-u.ln()).ln()
812}
813
814pub fn gumbel_max_argmax_with<F>(logits: &[f32], scale: f32, mut gumbel: F) -> usize
816where
817 F: FnMut() -> f32,
818{
819 if logits.is_empty() {
820 return 0;
821 }
822 let scale = scale.max(1e-8);
823 let mut best = 0usize;
824 let mut best_v = f32::NEG_INFINITY;
825 for (i, &logit) in logits.iter().enumerate() {
826 let v = logit + scale * gumbel();
827 if v > best_v {
828 best_v = v;
829 best = i;
830 }
831 }
832 best
833}
834
835pub fn argmax_f32(logits: &[f32]) -> usize {
837 logits
838 .iter()
839 .enumerate()
840 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
841 .map(|(i, _)| i)
842 .unwrap_or(0)
843}
844
845#[derive(Debug, Clone, Copy)]
847struct MinstdRand0 {
848 state: u32,
849}
850
851impl MinstdRand0 {
852 const A: u32 = 48_271;
853 const M: u32 = 2_147_483_647;
854
855 fn new(seed: u32) -> Self {
856 Self {
857 state: seed % Self::M,
858 }
859 }
860
861 fn next_u32(&mut self) -> u32 {
862 self.state = ((self.state as u64 * Self::A as u64) % Self::M as u64) as u32;
863 self.state
864 }
865
866 fn unit_f32(&mut self) -> f32 {
868 self.next_u32() as f32 / (Self::M - 1) as f32
869 }
870}
871
872#[derive(Debug, Clone, Copy)]
874struct StdNormalDist {
875 mean: f32,
876 scale: f32,
877 spare: f32,
878 has_spare: bool,
879}
880
881impl StdNormalDist {
882 fn new(mean: f32, scale: f32) -> Self {
883 Self {
884 mean,
885 scale,
886 spare: 0.0,
887 has_spare: false,
888 }
889 }
890
891 fn sample(&mut self, eng: &mut MinstdRand0) -> f32 {
892 if self.has_spare {
893 self.has_spare = false;
894 return self.spare;
895 }
896 loop {
897 let u1 = 2.0 * eng.unit_f32() - 1.0;
898 let u2 = 2.0 * eng.unit_f32() - 1.0;
899 let s = u1 * u1 + u2 * u2;
900 if s >= 1.0 || s == 0.0 {
901 continue;
902 }
903 let factor = (-2.0 * s.ln() / s).sqrt();
904 self.spare = u2 * factor * self.scale + self.mean;
905 self.has_spare = true;
906 return u1 * factor * self.scale + self.mean;
907 }
908 }
909}
910
911#[cfg(test)]
912mod tests {
913 use super::*;
914
915 #[test]
916 fn same_seed_same_sequence() {
917 let mut a = Philox4x32::new(0x1234_5678);
918 let mut b = Philox4x32::new(0x1234_5678);
919 for _ in 0..256 {
920 assert_eq!(a.next_u32(), b.next_u32());
921 }
922 }
923
924 #[test]
925 fn different_seed_different_sequence() {
926 let mut a = Philox4x32::new(1);
927 let mut b = Philox4x32::new(2);
928 let mut diffs = 0usize;
929 for _ in 0..16 {
930 if a.next_u32() != b.next_u32() {
931 diffs += 1;
932 }
933 }
934 assert!(
935 diffs >= 14,
936 "two distinct seeds should disagree on >=14/16 samples"
937 );
938 }
939
940 #[test]
941 fn next_f32_in_unit_interval() {
942 let mut r = Philox4x32::new(42);
943 for _ in 0..1000 {
944 let v = r.next_f32();
945 assert!((0.0..1.0).contains(&v), "{v} not in [0, 1)");
946 }
947 }
948
949 #[test]
950 fn fill_uniform_is_deterministic() {
951 let mut r1 = Philox4x32::new(7);
952 let mut r2 = Philox4x32::new(7);
953 let mut a = vec![0f32; 64];
954 let mut b = vec![0f32; 64];
955 r1.fill_uniform(&mut a);
956 r2.fill_uniform(&mut b);
957 assert_eq!(a, b);
958 }
959
960 #[test]
961 fn normal_mean_is_near_zero() {
962 let mut r = Philox4x32::new(123);
963 let n = 10_000;
964 let mut sum = 0f32;
965 for _ in 0..n {
966 sum += r.normal();
967 }
968 let mean = sum / n as f32;
969 assert!(mean.abs() < 0.1, "mean {mean} too far from 0");
970 }
971
972 #[test]
973 fn zero_backend_fills_zeros() {
974 let mut out = vec![1.0; 8];
975 fill_normal_like(&mut out, 0.0, 1.0, RngOptions::zero(), 0xABC, None);
976 assert!(out.iter().all(|&v| v == 0.0));
977 }
978
979 #[test]
980 fn philox_backend_is_deterministic() {
981 let opts = RngOptions::philox(99);
982 let mut a = vec![0f32; 32];
983 let mut b = vec![0f32; 32];
984 fill_normal_like(&mut a, 0.0, 0.5, opts, 123, None);
985 fill_normal_like(&mut b, 0.0, 0.5, opts, 123, None);
986 assert_eq!(a, b);
987 }
988
989 #[test]
990 fn ort_backend_is_deterministic() {
991 let opts = RngOptions::ort(7);
992 let mut a = vec![0f32; 64];
993 let mut b = vec![0f32; 64];
994 fill_normal_like(&mut a, 0.1, 2.0, opts, 555, None);
995 fill_normal_like(&mut b, 0.1, 2.0, opts, 555, None);
996 assert_eq!(a, b);
997 }
998
999 #[test]
1000 fn backends_disagree() {
1001 let mut philox = vec![0f32; 16];
1002 let mut ort = vec![0f32; 16];
1003 fill_normal_like(&mut philox, 0.0, 1.0, RngOptions::philox(42), 1, None);
1004 fill_normal_like(&mut ort, 0.0, 1.0, RngOptions::ort(42), 1, None);
1005 assert_ne!(philox, ort);
1006 }
1007
1008 #[test]
1009 fn gumbel_max_is_deterministic() {
1010 let logits = [1.0f32, 5.0, 2.0, 3.0];
1011 let mut a = Philox4x32::new(7);
1012 let mut b = Philox4x32::new(7);
1013 assert_eq!(
1014 a.gumbel_max_argmax(&logits, 0.01),
1015 b.gumbel_max_argmax(&logits, 0.01)
1016 );
1017 }
1018
1019 #[test]
1020 fn gumbel_max_zero_scale_is_argmax() {
1021 let logits = [1.0f32, 9.0, 2.0];
1022 assert_eq!(argmax_f32(&logits), 1);
1024 let mut rng = Philox4x32::new(1);
1025 assert_eq!(rng.gumbel_max_argmax(&logits, 1e-8), 1);
1027 }
1028
1029 #[test]
1030 fn gumbel01_from_uniform_matches_icdf() {
1031 let g = gumbel01_from_uniform(0.5);
1032 let expected = -((-0.5f32.ln()).ln());
1033 assert!((g - expected).abs() < 1e-6);
1034 }
1035
1036 #[test]
1037 fn bnns_byte_mapping_uses_little_endian_full_word() {
1038 assert_eq!(
1039 bnns_uniform_f32_from_bytes([0x38, 0xc1, 0x16, 0xcf]).to_bits(),
1040 0x3f4f_16c1
1041 );
1042 assert_eq!(bnns_uniform_f32_from_u32(u32::MAX), 1.0);
1043 }
1044
1045 #[test]
1046 fn bnns_gumbel_uses_private_uniform_mapping() {
1047 let bytes = [0x38, 0xc1, 0x16, 0xcf];
1048 assert_eq!(
1049 bnns_gumbel01_from_bytes(bytes),
1050 gumbel01_from_uniform(f32::from_bits(0x3f4f_16c1))
1051 );
1052 }
1053
1054 #[test]
1055 fn bnns_batch_fill_matches_scalar_stream_across_block_edges() {
1056 let lengths = [1usize, 3, 4, 5, 7, 8, 9, 15, 16, 17, 63, 64, 65, 257, 1024];
1058 let seeds = [0u64, 1, 42, 0x1234_5678_9abc_def0];
1059 let ranges = [(0.0, 1.0), (-2.0, 3.0), (0.5, 0.5), (-1e3, 1e3)];
1060
1061 for &seed in &seeds {
1062 for &n in &lengths {
1063 for &(lo, hi) in &ranges {
1064 let mut scalar = BnnsAesCtr128::new(seed);
1065 let mut batch = BnnsAesCtr128::new(seed);
1066 let mut from_scalar = vec![0.0; n];
1067 let mut from_batch = vec![0.0; n];
1068 for v in &mut from_scalar {
1069 *v = scalar.uniform(lo, hi);
1070 }
1071 batch.fill_uniform(&mut from_batch, lo, hi);
1072 assert_eq!(
1073 from_batch, from_scalar,
1074 "seed={seed} n={n} range=[{lo},{hi}]"
1075 );
1076 }
1077 }
1078 }
1079 }
1080
1081 #[test]
1082 fn bnns_batch_gumbel_matches_uniform_then_icdf() {
1083 let n = 4096;
1084 let seed = 0xA5A5_5A5A_C3C3_3C3C;
1085 let mut u_rng = BnnsAesCtr128::new(seed);
1086 let mut g_rng = BnnsAesCtr128::new(seed);
1087 let mut uniforms = vec![0.0; n];
1088 let mut gumbels = vec![0.0; n];
1089 u_rng.fill_uniform(&mut uniforms, 0.0, 1.0);
1090 g_rng.fill_gumbel01(&mut gumbels);
1091 for (i, (&u, &g)) in uniforms.iter().zip(gumbels.iter()).enumerate() {
1092 assert_eq!(g, gumbel01_from_uniform(u), "gumbel mismatch at sample {i}");
1093 }
1094 }
1095
1096 #[test]
1097 fn bnns_batch_is_deterministic_and_seed_sensitive() {
1098 let n = 2048;
1099 let mut a = vec![0.0; n];
1100 let mut b = vec![0.0; n];
1101 let mut c = vec![0.0; n];
1102 BnnsAesCtr128::new(99).fill_uniform(&mut a, -1.0, 2.0);
1103 BnnsAesCtr128::new(99).fill_uniform(&mut b, -1.0, 2.0);
1104 BnnsAesCtr128::new(100).fill_uniform(&mut c, -1.0, 2.0);
1105 assert_eq!(a, b);
1106 assert_ne!(a, c);
1107 let diffs = a.iter().zip(c.iter()).filter(|(x, y)| x != y).count();
1109 assert!(diffs > n * 9 / 10, "only {diffs}/{n} samples differed");
1110 }
1111
1112 #[test]
1113 fn bnns_captured_apple_vectors_seed_42() {
1114 let unit = [
1116 0x3f4f_16c1,
1117 0x3f17_8337,
1118 0x3ef1_8cae,
1119 0x3f71_4573,
1120 0x3f73_349f,
1121 0x3f24_dd54,
1122 0x3cdb_6bcf,
1123 0x3d7c_eb6a,
1124 ];
1125 let ranged = [
1126 0x4002_dc71,
1127 0x3f75_9013,
1128 0x3eb7_bf66,
1129 0x402d_96d0,
1130 0x4030_01c7,
1131 0x3f9c_2952,
1132 0xbfee_db94,
1133 0xbfd8_7b37,
1134 ];
1135 let mut unit_out = [0.0; 8];
1136 let mut ranged_out = [0.0; 8];
1137 BnnsAesCtr128::new(42).fill_uniform(&mut unit_out, 0.0, 1.0);
1138 BnnsAesCtr128::new(42).fill_uniform(&mut ranged_out, -2.0, 3.0);
1139 assert_eq!(unit_out.map(f32::to_bits), unit);
1140 assert_eq!(ranged_out.map(f32::to_bits), ranged);
1141 }
1142
1143 #[test]
1144 fn bnns_from_entropy_is_seed_sensitive() {
1145 let mut a = [0.0; 32];
1146 let mut b = [0.0; 32];
1147 BnnsAesCtr128::from_entropy().fill_uniform(&mut a, 0.0, 1.0);
1148 BnnsAesCtr128::from_entropy().fill_uniform(&mut b, 0.0, 1.0);
1149 assert_ne!(a, b);
1151 }
1152
1153 #[test]
1154 fn bnns_seed_from_fresh_state_roundtrip() {
1155 let seed: u64 = 0x0123_4567_89ab_cdef;
1156 let mut state = [0u8; BNNS_AES_CTR_STATE_SIZE];
1157 let le = seed.to_le_bytes();
1158 state[..8].copy_from_slice(&le);
1159 state[8..16].copy_from_slice(&le);
1160 assert!(bnns_generator_state_is_fresh(&state));
1161 assert_eq!(bnns_seed_from_generator_state(&state), Some(seed));
1162 let mut from_state = [0.0; 8];
1163 let mut from_seed = [0.0; 8];
1164 BnnsAesCtr128::try_from_fresh_generator_state(&state)
1165 .unwrap()
1166 .fill_uniform(&mut from_state, 0.0, 1.0);
1167 BnnsAesCtr128::new(seed).fill_uniform(&mut from_seed, 0.0, 1.0);
1168 assert_eq!(from_state, from_seed);
1169 state[32] = 4;
1171 assert!(!bnns_generator_state_is_fresh(&state));
1172 assert!(BnnsAesCtr128::try_from_fresh_generator_state(&state).is_none());
1173 }
1174
1175 #[test]
1176 fn bnns_backend_fill_helpers_are_deterministic() {
1177 let opts = RngOptions::bnns(99);
1178 let mut a = vec![0f32; 64];
1179 let mut b = vec![0f32; 64];
1180 fill_uniform_like(&mut a, -1.0, 2.0, opts, 7, None);
1181 fill_uniform_like(&mut b, -1.0, 2.0, opts, 7, None);
1182 assert_eq!(a, b);
1183 fill_normal_like(&mut a, 0.5, 2.0, opts, 7, None);
1184 fill_normal_like(&mut b, 0.5, 2.0, opts, 7, None);
1185 assert_eq!(a, b);
1186 assert_ne!(a, vec![0.0; 64]);
1187 }
1188
1189 #[test]
1190 fn bnns_normal_reuses_pair_within_fill() {
1191 let mut paired = [0.0; 4];
1192 let mut singles = [0.0; 4];
1193 BnnsAesCtr128::new(42).fill_normal(&mut paired, 0.0, 1.0);
1194 let mut rng = BnnsAesCtr128::new(42);
1195 for v in &mut singles {
1196 *v = rng.normal();
1197 }
1198 assert_eq!(paired, singles);
1201 }
1202
1203 #[test]
1204 fn bnns_gumbel_alpha_beta_matches_documented_formula() {
1205 let u = 0.5f32;
1206 let g = bnns_gumbel_from_uniform(u, 2.0, 0.1);
1207 let expected = -((-(2.0 * u + 0.1).ln() + 0.1).ln());
1208 assert!((g - expected).abs() < 1e-6);
1209 assert_eq!(
1211 bnns_gumbel_from_uniform(u, 1.0, 0.0),
1212 gumbel01_from_uniform(u)
1213 );
1214 }
1215
1216 #[test]
1217 fn bnns_categorical_matches_inverse_cdf_on_uniforms() {
1218 let probs = [0.1f32, 0.2, 0.3, 0.4];
1219 let n = 256;
1220 let mut uniforms = vec![0.0; n];
1221 let mut cats = vec![0.0; n];
1222 BnnsAesCtr128::new(42).fill_uniform(&mut uniforms, 0.0, 1.0);
1223 BnnsAesCtr128::new(42).fill_categorical(&mut cats, &probs, false);
1224 for (i, (&u, &c)) in uniforms.iter().zip(cats.iter()).enumerate() {
1225 assert_eq!(
1226 c as usize,
1227 bnns_categorical_from_uniform(u, &probs),
1228 "categorical mismatch at {i}"
1229 );
1230 }
1231 let mut log_cats = vec![0.0; n];
1232 let log_probs: Vec<f32> = probs.iter().map(|p| p.ln()).collect();
1233 BnnsAesCtr128::new(42).fill_categorical(&mut log_cats, &log_probs, true);
1234 assert_eq!(cats, log_cats);
1235 }
1236
1237 #[test]
1238 fn bnns_uniform_int_stays_in_half_open_range() {
1239 let mut out = [0i64; 4096];
1240 BnnsAesCtr128::new(42).fill_uniform_int(&mut out, -3, 10);
1241 assert!(out.iter().all(|&v| (-3..10).contains(&v)));
1242 assert_eq!(bnns_uniform_int_from_u32(0, 0, 1), 0);
1243 assert_eq!(bnns_uniform_int_from_u32(u32::MAX, 0, 1), 0);
1244 assert_ne!(
1245 {
1246 let mut a = [0i64; 64];
1247 BnnsAesCtr128::new(1).fill_uniform_int(&mut a, 0, 100);
1248 a
1249 },
1250 {
1251 let mut b = [0i64; 64];
1252 BnnsAesCtr128::new(2).fill_uniform_int(&mut b, 0, 100);
1253 b
1254 }
1255 );
1256 }
1257
1258 #[cfg(target_os = "macos")]
1259 #[test]
1260 fn native_bnns_gumbel_replays_seeded_stream() {
1261 let logits = [0.0f32; 256];
1262 let draw = || {
1263 let mut sampler = NativeBnnsGumbelMax::new(16_807).unwrap();
1264 (0..8)
1265 .map(|_| sampler.argmax(&logits, 1.0, 0.01).unwrap())
1266 .collect::<Vec<_>>()
1267 };
1268 assert_eq!(draw(), draw());
1269 }
1270
1271 #[cfg(target_os = "macos")]
1272 mod apple_bnns_parity {
1273 use super::*;
1274
1275 #[repr(C)]
1276 struct BnnsNdArrayDescriptor {
1277 flags: u32,
1278 layout: u32,
1279 size: [usize; 8],
1280 stride: [usize; 8],
1281 data: *mut core::ffi::c_void,
1282 data_type: u32,
1283 table_data: *mut core::ffi::c_void,
1284 table_data_type: u32,
1285 data_scale: f32,
1286 data_bias: f32,
1287 }
1288
1289 #[link(name = "Accelerate", kind = "framework")]
1290 unsafe extern "C" {
1291 fn BNNSCreateRandomGenerator(
1292 method: u32,
1293 filter_params: *const core::ffi::c_void,
1294 ) -> *mut core::ffi::c_void;
1295 fn BNNSCreateRandomGeneratorWithSeed(
1296 method: u32,
1297 seed: u64,
1298 filter_params: *const core::ffi::c_void,
1299 ) -> *mut core::ffi::c_void;
1300 fn BNNSDestroyRandomGenerator(generator: *mut core::ffi::c_void);
1301 fn BNNSRandomGeneratorStateSize(generator: *mut core::ffi::c_void) -> usize;
1302 fn BNNSRandomGeneratorGetState(
1303 generator: *mut core::ffi::c_void,
1304 size: usize,
1305 state: *mut u8,
1306 ) -> i32;
1307 fn BNNSRandomFillUniformFloat(
1308 generator: *mut core::ffi::c_void,
1309 desc: *mut BnnsNdArrayDescriptor,
1310 a: f32,
1311 b: f32,
1312 ) -> i32;
1313 fn BNNSRandomFillNormalFloat(
1314 generator: *mut core::ffi::c_void,
1315 desc: *mut BnnsNdArrayDescriptor,
1316 mean: f32,
1317 stddev: f32,
1318 ) -> i32;
1319 fn BNNSRandomFillCategoricalFloat(
1320 generator: *mut core::ffi::c_void,
1321 desc: *const BnnsNdArrayDescriptor,
1322 probabilities: *const BnnsNdArrayDescriptor,
1323 log_probabilities: bool,
1324 ) -> i32;
1325 }
1326
1327 const BNNS_METHOD_AES_CTR: u32 = 0;
1328 const BNNS_LAYOUT_VECTOR: u32 = 0x1_0000;
1329 const BNNS_FLOAT32: u32 = 0x1_0000 | 32;
1330
1331 fn apple_desc(data: *mut f32, n: usize) -> BnnsNdArrayDescriptor {
1332 BnnsNdArrayDescriptor {
1333 flags: 0,
1334 layout: BNNS_LAYOUT_VECTOR,
1335 size: [n, 0, 0, 0, 0, 0, 0, 0],
1336 stride: [0; 8],
1337 data: data.cast(),
1338 data_type: BNNS_FLOAT32,
1339 table_data: core::ptr::null_mut(),
1340 table_data_type: 0,
1341 data_scale: 1.0,
1342 data_bias: 0.0,
1343 }
1344 }
1345
1346 fn apple_fill_uniform(seed: u64, out: &mut [f32], lo: f32, hi: f32) {
1347 unsafe {
1348 let generator =
1349 BNNSCreateRandomGeneratorWithSeed(BNNS_METHOD_AES_CTR, seed, core::ptr::null());
1350 assert!(
1351 !generator.is_null(),
1352 "BNNSCreateRandomGeneratorWithSeed failed"
1353 );
1354 let mut desc = apple_desc(out.as_mut_ptr(), out.len());
1355 let rc = BNNSRandomFillUniformFloat(generator, &mut desc, lo, hi);
1356 BNNSDestroyRandomGenerator(generator);
1357 assert_eq!(rc, 0, "BNNSRandomFillUniformFloat failed");
1358 }
1359 }
1360
1361 fn apple_fill_normal(seed: u64, out: &mut [f32], mean: f32, stddev: f32) {
1362 unsafe {
1363 let generator =
1364 BNNSCreateRandomGeneratorWithSeed(BNNS_METHOD_AES_CTR, seed, core::ptr::null());
1365 assert!(!generator.is_null());
1366 let mut desc = apple_desc(out.as_mut_ptr(), out.len());
1367 let rc = BNNSRandomFillNormalFloat(generator, &mut desc, mean, stddev);
1368 BNNSDestroyRandomGenerator(generator);
1369 assert_eq!(rc, 0, "BNNSRandomFillNormalFloat failed");
1370 }
1371 }
1372
1373 fn apple_fill_categorical(
1374 seed: u64,
1375 out: &mut [f32],
1376 probabilities: &[f32],
1377 log_probabilities: bool,
1378 ) {
1379 unsafe {
1380 let generator =
1381 BNNSCreateRandomGeneratorWithSeed(BNNS_METHOD_AES_CTR, seed, core::ptr::null());
1382 assert!(!generator.is_null());
1383 let mut probs = probabilities.to_vec();
1384 let mut odesc = apple_desc(out.as_mut_ptr(), out.len());
1385 let mut pdesc = apple_desc(probs.as_mut_ptr(), probs.len());
1386 let rc =
1387 BNNSRandomFillCategoricalFloat(generator, &odesc, &pdesc, log_probabilities);
1388 let _ = (&mut odesc, &mut pdesc);
1389 BNNSDestroyRandomGenerator(generator);
1390 assert_eq!(rc, 0, "BNNSRandomFillCategoricalFloat failed");
1391 }
1392 }
1393
1394 fn max_ulps(a: &[f32], b: &[f32]) -> u32 {
1395 a.iter()
1396 .zip(b.iter())
1397 .map(|(&x, &y)| x.to_bits().abs_diff(y.to_bits()))
1398 .max()
1399 .unwrap_or(0)
1400 }
1401
1402 #[test]
1403 fn batch_matches_apple_bnns_across_seeds_sizes_ranges() {
1404 let seeds = [
1405 0u64,
1406 1,
1407 42,
1408 7,
1409 0xffff_ffff_ffff_ffff,
1410 0x1234_5678_9abc_def0,
1411 0xa5a5_5a5a_c3c3_3c3c,
1412 ];
1413 let lengths = [1usize, 3, 4, 5, 16, 17, 64, 65, 256, 257, 1024, 4096];
1414 let ranges = [(0.0, 1.0), (-2.0, 3.0), (0.25, 0.75), (-1e2, 1e2)];
1415
1416 for &seed in &seeds {
1417 for &n in &lengths {
1418 for &(lo, hi) in &ranges {
1419 let mut apple = vec![0.0; n];
1420 let mut ours = vec![0.0; n];
1421 apple_fill_uniform(seed, &mut apple, lo, hi);
1422 BnnsAesCtr128::new(seed).fill_uniform(&mut ours, lo, hi);
1423 assert_eq!(
1424 ours.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
1425 apple.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
1426 "BNNS parity failed seed={seed} n={n} range=[{lo},{hi}]"
1427 );
1428 }
1429 }
1430 }
1431 }
1432
1433 #[test]
1434 fn batch_gumbel_from_apple_uniforms() {
1435 let n = 8192;
1436 let seed = 12345u64;
1437 let mut apple_u = vec![0.0; n];
1438 apple_fill_uniform(seed, &mut apple_u, 0.0, 1.0);
1439
1440 let mut ours_g = vec![0.0; n];
1441 BnnsAesCtr128::new(seed).fill_gumbel01(&mut ours_g);
1442
1443 for (i, (&u, &g)) in apple_u.iter().zip(ours_g.iter()).enumerate() {
1444 assert_eq!(
1445 g.to_bits(),
1446 gumbel01_from_uniform(u).to_bits(),
1447 "Gumbel from Apple uniform mismatch at {i}"
1448 );
1449 }
1450 }
1451
1452 #[test]
1453 fn batch_normal_matches_apple_box_muller_pairing() {
1454 for seed in [42u64, 7, 0xdead_beef] {
1458 for &n in &[1usize, 2, 3, 4] {
1459 let mut apple = vec![0.0; n];
1460 let mut ours = vec![0.0; n];
1461 apple_fill_normal(seed, &mut apple, 0.0, 1.0);
1462 BnnsAesCtr128::new(seed).fill_normal(&mut ours, 0.0, 1.0);
1463 assert!(
1464 max_ulps(&ours, &apple) <= 16,
1465 "short normal ULP seed={seed} n={n}"
1466 );
1467 }
1468 let mut apple = vec![0.0; 64];
1469 let mut ours = vec![0.0; 64];
1470 apple_fill_normal(seed, &mut apple, 0.0, 1.0);
1471 BnnsAesCtr128::new(seed).fill_normal(&mut ours, 0.0, 1.0);
1472 assert!(
1473 max_ulps(&ours[..4], &apple[..4]) <= 16,
1474 "leading-pair ULP seed={seed}"
1475 );
1476 }
1477 }
1478
1479 #[test]
1480 fn batch_categorical_matches_apple_bit_exact() {
1481 let probs = [0.1f32, 0.2, 0.3, 0.4];
1482 for seed in [42u64, 7, 12345] {
1483 for &n in &[1usize, 8, 16, 64, 256, 1024] {
1484 let mut apple = vec![0.0; n];
1485 let mut ours = vec![0.0; n];
1486 apple_fill_categorical(seed, &mut apple, &probs, false);
1487 BnnsAesCtr128::new(seed).fill_categorical(&mut ours, &probs, false);
1488 assert_eq!(ours, apple, "categorical seed={seed} n={n}");
1489
1490 let log_probs: Vec<f32> = probs.iter().map(|p| p.ln()).collect();
1491 let mut apple_log = vec![0.0; n];
1492 let mut ours_log = vec![0.0; n];
1493 apple_fill_categorical(seed, &mut apple_log, &log_probs, true);
1494 BnnsAesCtr128::new(seed).fill_categorical(&mut ours_log, &log_probs, true);
1495 assert_eq!(ours_log, apple_log, "log-categorical seed={seed} n={n}");
1496 }
1497 }
1498 }
1499
1500 #[test]
1501 fn entropy_generator_state_replays_via_seed() {
1502 unsafe {
1503 let generator = BNNSCreateRandomGenerator(BNNS_METHOD_AES_CTR, core::ptr::null());
1504 assert!(!generator.is_null());
1505 let n = BNNSRandomGeneratorStateSize(generator);
1506 assert_eq!(n, BNNS_AES_CTR_STATE_SIZE);
1507 let mut state = vec![0u8; n];
1508 assert_eq!(
1509 BNNSRandomGeneratorGetState(generator, n, state.as_mut_ptr()),
1510 0
1511 );
1512 assert!(bnns_generator_state_is_fresh(&state));
1513 let seed = bnns_seed_from_generator_state(&state).unwrap();
1514
1515 let mut apple = [0.0f32; 16];
1516 let mut desc = BnnsNdArrayDescriptor {
1517 flags: 0,
1518 layout: BNNS_LAYOUT_VECTOR,
1519 size: [apple.len(), 0, 0, 0, 0, 0, 0, 0],
1520 stride: [0; 8],
1521 data: apple.as_mut_ptr().cast(),
1522 data_type: BNNS_FLOAT32,
1523 table_data: core::ptr::null_mut(),
1524 table_data_type: 0,
1525 data_scale: 1.0,
1526 data_bias: 0.0,
1527 };
1528 assert_eq!(
1529 BNNSRandomFillUniformFloat(generator, &mut desc, 0.0, 1.0),
1530 0
1531 );
1532 BNNSDestroyRandomGenerator(generator);
1533
1534 let mut ours = [0.0f32; 16];
1535 BnnsAesCtr128::try_from_fresh_generator_state(&state)
1536 .unwrap()
1537 .fill_uniform(&mut ours, 0.0, 1.0);
1538 assert_eq!(ours.map(f32::to_bits), apple.map(f32::to_bits));
1539
1540 let mut with_seed = [0.0f32; 16];
1542 apple_fill_uniform(seed, &mut with_seed, 0.0, 1.0);
1543 assert_eq!(with_seed.map(f32::to_bits), apple.map(f32::to_bits));
1544 }
1545 }
1546 }
1547}