Skip to main content

nuvec_core/
lib.rs

1//! Lazy array graph, primitives, dtypes, streams and evaluation: the core of nuvec.
2//!
3//! Operations on [`Array`]s record a graph and return immediately. Nothing is computed
4//! until an array is evaluated, explicitly or by reading it back:
5//!
6//! ```
7//! use nuvec_core::{Result, array, ops};
8//!
9//! # fn main() -> Result<()> {
10//! let a = array![1.0, 2.0, 3.0];
11//! let b = ops::full([3], 10.0);
12//! let c = (&a + &b) * &a; // lazy: nothing computed yet
13//! assert!(!c.is_evaluated());
14//! assert_eq!(c.to_vec::<f32>()?, vec![11.0, 24.0, 39.0]);
15//! # Ok(())
16//! # }
17//! ```
18//!
19//! Operations are methods (`x.exp().sum(-1)`), with free mirrors of MLX's names in [`ops`].
20//! They never return `Result`: an invalid call gives a *failed array* carrying the op, the
21//! caller's source location and the reason ([`Error::Op`]); arrays computed from it fail
22//! with the same error, which surfaces at [`Array::check`], [`Array::eval`], [`Array::item`]
23//! or [`Array::to_vec`]:
24//!
25//! ```
26//! use nuvec_core::{OpErrorKind, array};
27//!
28//! let x = array![[1.0, 2.0, 3.0]];
29//! let y = x.reshape([2, -1]).exp(); // 3 elements cannot be [2, -1]
30//! let err = y.check().unwrap_err();
31//! assert!(matches!(err.kind(), Some(OpErrorKind::InvalidShape(_))));
32//! assert_eq!(err.location().unwrap().file(), file!());
33//! ```
34//!
35//! Binary operations promote types (MLX's table) and broadcast shapes (NumPy rules); views
36//! (`reshape`, `transpose`, basic indexing with [`Array::i`], `broadcast_to`, `expand_dims`,
37//! `squeeze`) share data.
38//!
39//! Reductions (`sum`, `mean`, `max`, `argmax`, …) work over any axes and layouts;
40//! `matmul` broadcasts batch dimensions and uses a SIMD GEMM on the CPU.
41//!
42//! Arrays live in unified memory. Operations on the GPU stream run as Metal kernels; anything
43//! without a GPU kernel (for example float64) runs on the CPU over the same memory:
44//!
45//! ```
46//! use nuvec_core::{Device, array, with_stream};
47//!
48//! # fn main() -> nuvec_core::Result<()> {
49//! let x = array![1.0, 2.0, 3.0];
50//! // A Metal kernel on a Mac; an `UnsupportedDevice` error elsewhere.
51//! let y = with_stream(Device::gpu(0), || &x * &x);
52//! # if y.eval().is_ok() { assert_eq!(y.to_vec::<f32>()?, vec![1.0, 4.0, 9.0]); }
53//! # Ok(())
54//! # }
55//! ```
56//!
57//! Each thread's default stream is the GPU's when Metal is available, else the CPU's
58//! ([`default_device`]); [`with_stream`] changes it for a scope, [`set_default_stream`] for
59//! the calling thread.
60//!
61//! Threads may evaluate arrays that share work concurrently (each node runs once). A
62//! transform traces on the calling thread: evaluating its tracers on other threads
63//! (rayon, say) while it runs is unsupported.
64//!
65//! **Stability:** no promise when this crate is depended on directly; depend on `nuvec`,
66//! whose re-exports are semver-tracked.
67//!
68//! **Status:** pre-alpha.
69
70// The only `unsafe` code is the GPU dispatch helpers in `gpu.rs` (nuvec-gpu's dispatch is
71// `unsafe`: core vouches for its own kernels staying in bounds).
72#![deny(unsafe_code)]
73
74mod array;
75mod axes;
76mod batching;
77mod buffer;
78mod cast;
79mod conv;
80mod creation;
81mod custom;
82mod derivatives;
83mod device;
84mod display;
85pub mod distributed;
86mod dtype;
87mod error;
88mod eval;
89pub mod fast;
90mod file;
91mod fp8;
92// Parts of it are only used by the Metal backend.
93#[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
94mod fused;
95mod gpu;
96pub mod graph;
97mod index;
98mod indexing;
99// Parts of it are only used by the Metal backend.
100#[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
101mod kernel;
102mod kernels;
103pub mod ops;
104mod primitive;
105mod quantized;
106pub mod random;
107mod scalar;
108mod shape;
109mod sorting;
110pub mod speculative;
111mod transforms;
112pub mod tree;
113mod view;
114
115#[doc(hidden)]
116pub use creation::__private;
117
118pub use array::Array;
119pub use axes::{Axes, Axis, IntoShape, ScanOptions, VarOptions};
120pub use custom::{
121    CustomFunction, CustomFunctionFlat, checkpoint, checkpoint_flat, custom_function,
122    custom_function_flat,
123};
124pub use device::{
125    Device, DeviceKind, Stream, default_device, default_stream, set_default_stream, with_stream,
126};
127pub use dtype::{Category, Complex64, DType, Element, at_least_float, bf16, f16, promote_types};
128pub use error::{Error, OpErrorKind, Result};
129pub use eval::{Scheduled, async_eval, eval, schedule};
130pub use file::{MappedFile, MappedRanges};
131pub use index::{ArrayIndex, At, Ellipsis, NewAxis, Step, Stepped};
132pub use quantized::{GatherQmm, Quant, QuantMode, Quantized};
133pub use scalar::{Scalar, ScalarOperand, ScalarOrArray};
134pub use transforms::{
135    GradOutput, InAxes, InAxis, IntoResult, OutAxes, Vmap, grad, grad_flat, jvp, jvp_flat,
136    value_and_grad, value_and_grad_flat, vjp, vjp_flat, vmap, vmap_flat,
137};
138#[doc(hidden)]
139pub use tree::__derive;
140pub use tree::{
141    LeafKind, Leaves, LoadMode, Param, Static, Tree, TreeDef, Visit, VisitMut, Visitor, VisitorMut,
142};
143
144// Graph-level items, public in `graph`, used across the crate by their short paths.
145pub(crate) use graph::SliceArg;
146pub(crate) use graph::UnaryOp;
147
148/// The everyday names in one import: `use nuvec_core::prelude::*;`. Operations are methods
149/// of [`Array`]; the free functions (MLX's names) are in [`ops`], best imported as a module
150/// (`use nuvec_core::ops as mx;`, then `mx::zeros([2, 3], DType::F32)`).
151pub mod prelude {
152    pub use crate::{Array, DType, Ellipsis, Error, NewAxis, Result, Step, array, with_stream};
153}
154
155/// Products with at most this many rows give each row the bits of the same row's
156/// one-row product: dense matrix products in float32, float16 and bfloat16 always, and
157/// quantized and gathered (mixture-of-experts) products evaluated under
158/// [`with_row_exact`]. See the `nuvec` crate's "Determinism" section.
159pub const ROW_INDEPENDENT_ROWS: usize = 8;
160
161thread_local! {
162    /// Whether products evaluated on this thread keep rows independent ([`with_row_exact`]).
163    static ROW_EXACT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
164}
165
166/// Runs `f` with row-exact products on this thread: quantized and gathered products of up
167/// to [`ROW_INDEPENDENT_ROWS`] rows (tokens, for gathers) evaluated or built inside give
168/// each row the bits of its one-row product, through one matrix-vector product per row
169/// (or nuvec's few-row kernels with the same arithmetic). Outside it they route as MLX
170/// does, whose multi-vector kernels are faster from 2 rows on but give a row other bits
171/// than its one-row product. Speculative verification runs in this mode, and so
172/// can batched serving that must equal each request run alone.
173pub fn with_row_exact<R>(f: impl FnOnce() -> R) -> R {
174    struct Restore(bool);
175    impl Drop for Restore {
176        fn drop(&mut self) {
177            ROW_EXACT.with(|r| r.set(self.0));
178        }
179    }
180    let _restore = Restore(ROW_EXACT.with(|r| r.replace(true)));
181    f()
182}
183
184/// Whether products evaluated or built on this thread are row-exact
185/// ([`with_row_exact`]).
186pub fn row_exact() -> bool {
187    ROW_EXACT.with(std::cell::Cell::get)
188}
189
190thread_local! {
191    /// Whether products evaluated on this thread may take the Neural Accelerator kernels
192    /// when they are active.
193    pub(crate) static NAX_ROUTE: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
194}
195
196/// Lets (`true`, the default) or keeps (`false`) products evaluated on this thread from the
197/// Neural Accelerator kernels, so a test compares them with the existing kernels; returns
198/// the previous setting. A test diagnostic, not a stable API: `NUVEC_NAX` is the switch.
199#[doc(hidden)]
200pub fn __nax_route(on: bool) -> bool {
201    NAX_ROUTE.with(|r| r.replace(on))
202}
203
204/// The GPU dispatches this process has encoded, so a test counts what an evaluation
205/// encodes. A test diagnostic, not a stable API: other threads' evaluations count too.
206#[doc(hidden)]
207pub fn __gpu_dispatches() -> u64 {
208    gpu::DISPATCHES.load(std::sync::atomic::Ordering::Relaxed)
209}
210
211/// The version of this crate.
212pub const VERSION: &str = env!("CARGO_PKG_VERSION");