nuvec_core/random.rs
1//! Random numbers from explicit keys or a per-thread key sequence, bit for bit as in MLX.
2//!
3//! A key is a uint32 array of shape `[2]`. [`bits`] hashes counters with threefry2x32
4//! using MLX's layout, and [`uniform`] and [`normal`] are composed from operations
5//! exactly as in MLX's `random.cpp`. For the same seed, bits and uniform samples equal MLX's.
6//! Normal samples agree within a few ulps: `erfinv` needs a logarithm, whose last bits
7//! differ between implementations.
8//!
9//! Without a key, functions draw the next key from this thread's sequence. Each thread's
10//! sequence starts from a key derived from the process seed (a time seed until [`seed`]
11//! is called) and the thread's ordinal, the order in which threads first draw:
12//!
13//! - ordinal 0 starts from `key(process_seed)`, so a single-threaded program seeded with
14//! `seed(s)` draws exactly MLX's values for `s`;
15//! - ordinal `n > 0` starts from `key(splitmix64(process_seed ^ n))`.
16//!
17//! So threads that never called [`seed`] draw distinct streams. [`seed`] sets the process
18//! seed, restarts the calling thread's sequence as ordinal 0 and makes every other thread
19//! derive its sequence again (with the next ordinals, in the order they draw next); a
20//! thread that called [`seed`] itself keeps its own sequence until it calls [`seed`] again.
21//! Draws are reproducible across runs when threads start drawing in a fixed order; for
22//! exact reproducibility of worker threads whatever their order, pass explicit keys
23//! ([`key`], [`split`], [`split_n`]).
24//!
25//! Like every operation, these return arrays: invalid arguments give a failed array that
26//! carries the error and the caller's location (see [`crate::Array::check`]).
27
28pub(crate) mod imp;
29
30use std::panic::Location;
31
32use crate::axes::{Axis, IntoShape};
33use crate::{Array, DType, Error};
34
35pub use imp::{key, seed};
36
37/// The next key of this thread's sequence (`split` the current key; keep the first half),
38/// the key that functions called without one use. Useful to pass the sequence's state into
39/// a compiled function, where keys drawn inside would be frozen at the first trace.
40#[track_caller]
41pub fn next_key() -> Array {
42 Array::sticky("next_key", Location::caller(), &[], imp::next_key)
43}
44
45/// The current key of this thread's sequence, without advancing it. An internal hook for
46/// `compile_mut` and `checkpoint`, which thread the sequence through traces and
47/// recomputations; not part of the stable API.
48#[doc(hidden)]
49pub fn sequence_key() -> Array {
50 imp::sequence_key()
51}
52
53/// Replaces the current key of this thread's sequence (see [`sequence_key`]). Internal.
54#[doc(hidden)]
55pub fn set_sequence_key(key: Array) {
56 imp::set_sequence_key(key);
57}
58
59/// Random bits of `shape`, `width` bytes per element (1, 2 or 4 → uint8, uint16, uint32).
60#[track_caller]
61pub fn bits<'a>(shape: impl IntoShape, width: i32, key: impl Into<Option<&'a Array>>) -> Array {
62 let key = key.into();
63 let spec = shape.shape_spec();
64 let dtype = match width {
65 1 => DType::U8,
66 2 => DType::U16,
67 _ => DType::U32,
68 };
69 Array::constructor("bits", Location::caller(), dtype, &spec, |shape| {
70 let width = usize::try_from(width)
71 .map_err(|_| Error::InvalidArgument(format!("bits: invalid width {width}")))?;
72 imp::bits(shape, width, key)
73 })
74}
75
76/// `num` new keys from `key`, as an array of shape `[num, 2]`.
77#[track_caller]
78pub fn split(key: &Array, num: usize) -> Array {
79 Array::sticky("split", Location::caller(), &[key], || imp::split(key, num))
80}
81
82/// `N` new keys from `key`, for destructuring: `let [k1, k2] = random::split_n::<2>(&key);`.
83///
84/// ```
85/// use nuvec_core::random;
86///
87/// let [a, b] = random::split_n::<2>(&random::key(0));
88/// assert_eq!((a.shape(), b.shape()), (&[2][..], &[2][..]));
89/// ```
90#[track_caller]
91pub fn split_n<const N: usize>(key: &Array) -> [Array; N] {
92 Array::sticky_n("split", Location::caller(), &[key], || {
93 let all = imp::split(key, N)?;
94 let mut out = Vec::with_capacity(N);
95 for i in 0..N {
96 let row = crate::ops::imp::slice(
97 &all,
98 &[
99 crate::SliceArg::new(Some(i as isize), Some(i as isize + 1), 1),
100 crate::SliceArg::full(),
101 ],
102 )?;
103 out.push(crate::ops::imp::reshape(&row, &[2])?);
104 }
105 Ok(out
106 .try_into()
107 .unwrap_or_else(|_| unreachable!("N keys were split")))
108 })
109}
110
111/// Uniform samples in `[low, high)` of `shape` and floating-point `dtype` (float16,
112/// bfloat16 or float32), as MLX: 32 random bits divided by `u32::MAX`, clamped below 1.
113///
114/// ```
115/// use nuvec_core::{DType, random};
116///
117/// let u = random::uniform(0.0, 1.0, [2, 3], DType::F32, Some(&random::key(1)));
118/// assert_eq!(u.shape(), [2, 3]);
119/// assert!(u.to_vec::<f32>().unwrap().iter().all(|&x| (0.0..1.0).contains(&x)));
120/// ```
121#[track_caller]
122pub fn uniform<'a>(
123 low: f64,
124 high: f64,
125 shape: impl IntoShape,
126 dtype: DType,
127 key: impl Into<Option<&'a Array>>,
128) -> Array {
129 let key = key.into();
130 let spec = shape.shape_spec();
131 Array::constructor("uniform", Location::caller(), dtype, &spec, |shape| {
132 imp::uniform(low, high, shape, dtype, key)
133 })
134}
135
136/// Normal samples (mean `loc`, standard deviation `scale`; MLX's defaults are 0 and 1) of
137/// `shape` and floating-point `dtype`, as MLX: `√2 · erfinv(u)` for `u` uniform in `(-1, 1)`.
138#[track_caller]
139pub fn normal<'a>(
140 shape: impl IntoShape,
141 dtype: DType,
142 loc: f64,
143 scale: f64,
144 key: impl Into<Option<&'a Array>>,
145) -> Array {
146 let key = key.into();
147 let spec = shape.shape_spec();
148 Array::constructor("normal", Location::caller(), dtype, &spec, |shape| {
149 imp::normal(shape, dtype, Some(loc), Some(scale), key)
150 })
151}
152
153/// Gumbel samples of `shape` and floating-point `dtype`, as MLX: `−log(−log(u))` for `u`
154/// uniform in `[0, 1)`.
155#[track_caller]
156pub fn gumbel<'a>(shape: impl IntoShape, dtype: DType, key: impl Into<Option<&'a Array>>) -> Array {
157 let key = key.into();
158 let spec = shape.shape_spec();
159 Array::constructor("gumbel", Location::caller(), dtype, &spec, |shape| {
160 imp::gumbel(shape, dtype, key)
161 })
162}
163
164/// Indices (uint32) drawn from the categorical distributions whose unnormalised log
165/// probabilities run along one `axis` of `logits` (`..`: the flattened array), as MLX: the
166/// argmax of `logits` plus Gumbel noise. The result has `logits`' shape without `axis`.
167///
168/// ```
169/// use nuvec_core::{array, random};
170///
171/// // One certain outcome per row.
172/// let logits = array![[0.0, -1e9, -1e9], [-1e9, -1e9, 0.0]];
173/// let draws = random::categorical(&logits, -1, Some(&random::key(7)));
174/// assert_eq!(draws.to_vec::<u32>().unwrap(), [0, 2]);
175/// ```
176#[track_caller]
177pub fn categorical<'a>(
178 logits: &Array,
179 axis: impl Axis,
180 key: impl Into<Option<&'a Array>>,
181) -> Array {
182 let key = key.into();
183 let spec = axis.axis_spec();
184 let mut ins = vec![logits];
185 ins.extend(key);
186 Array::sticky(
187 "categorical",
188 Location::caller(),
189 &ins,
190 || match crate::axes::one_axis(spec, logits.ndim())? {
191 Some(axis) => imp::categorical(logits, axis as isize, key),
192 None => imp::categorical(&crate::ops::imp::reshape(logits, &[-1])?, 0, key),
193 },
194 )
195}
196
197/// Integers in `[low, high)` of `shape` and integer `dtype`, as MLX: uniform floats in
198/// `[low, high)` rounded down.
199#[track_caller]
200pub fn randint<'a>(
201 low: i64,
202 high: i64,
203 shape: impl IntoShape,
204 dtype: DType,
205 key: impl Into<Option<&'a Array>>,
206) -> Array {
207 let key = key.into();
208 let spec = shape.shape_spec();
209 Array::constructor("randint", Location::caller(), dtype, &spec, |shape| {
210 imp::randint(low, high, shape, dtype, key)
211 })
212}
213
214/// Booleans of `shape` that are true with probability `p`, as MLX: `uniform < p`.
215#[track_caller]
216pub fn bernoulli<'a>(p: f64, shape: impl IntoShape, key: impl Into<Option<&'a Array>>) -> Array {
217 let key = key.into();
218 let spec = shape.shape_spec();
219 Array::constructor(
220 "bernoulli",
221 Location::caller(),
222 DType::Bool,
223 &spec,
224 |shape| imp::bernoulli(p, shape, key),
225 )
226}