Skip to main content

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}