Skip to main content

rlx_tensor/
lib.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! # rlx-tensor
6//!
7//! Symbolic tensor API for RLX — expression syntax with compiler-backed
8//! execution. Compared to NumPy / ndarray / PyTorch / JAX:
9//!
10//! | | ndarray / NumPy | PyTorch | JAX | **rlx-tensor** |
11//! |-|-----------------|---------|-----|----------------|
12//! | Default execution | eager, host | eager, GPU | trace + XLA | **trace → fuse → backend** |
13//! | Intermediate buffers | always materialized | often kept for autograd | XLA decides | **fusion + memory plan** |
14//! | Device | host only | CUDA-centric | TPU/GPU | **CPU / Metal / CUDA / ROCm / wgpu / MLX / TPU** |
15//! | Variable batch/seq | manual padding | manual | `jax.vmap` / shapes | **first-class `Dim::Dynamic`** |
16//! | Slicing | views (cheap) | views | slice ops | **`s![]` → narrow (fusable)** |
17//!
18//! Symbolic [`Tensor`] handles carry **zero payload bytes** until you compile
19//! and run — but with the `eval` feature you can also build from host data
20//! NumPy-style and read results straight back, the graph staying lazy (so the
21//! compiler fuses + memory-plans the whole expression) until you materialize:
22//!
23//! ```ignore
24//! use rlx_tensor::Tensor; // requires feature = "eval"
25//!
26//! let a = Tensor::from_vec(vec![1.0, 2.0, 3.0], [3]);
27//! let b = Tensor::ones([3]);
28//! let c = (&a + &b).relu();        // graphs auto-merge, nothing runs yet
29//! assert_eq!(c.to_vec(), vec![2.0, 3.0, 4.0]); // auto-selects fastest backend
30//! // c.on(Device::Metal).to_vec()  // or pin an explicit backend
31//! ```
32//!
33//! Reverse-mode autodiff (feature `grad`) returns gradient tensors:
34//!
35//! ```ignore
36//! let loss = (&a * &b).sum([0], false);
37//! let g = loss.grad(&[&a, &b]);     // [∂loss/∂a, ∂loss/∂b]
38//! assert_eq!(g[0].to_vec(), b.to_vec());
39//! ```
40//!
41//! JAX-shaped composable transforms (feature `transforms`) operate on [`Func`]
42//! (a traced function of named inputs) and chain — `vmap(grad(f))`:
43//!
44//! ```ignore
45//! let f = Func::new("f", |s| { let x = s.input("x", shape![3]); (&x * &x).sum([0], false) });
46//! let batched_grad = f.grad(&["x"]).vmap(&["x"], 4);  // Func -> Func -> Func
47//! let compiled = batched_grad.jit();                  // compile once
48//! let out = compiled.run(&[("x", &xs)]);              // run many, no recompile
49//! ```
50//!
51//! Materialization is compile-cached per thread (keyed by graph + constants +
52//! device), so even plain `to_vec` / `run` reuse the compiled artifact.
53//!
54//! ```rust
55//! use rlx_tensor::{ax, graph, rg, s, shape};
56//!
57//! // static shape
58//! let g = graph("mlp", |g| {
59//!     let x = g.input("x", shape![2, 4]);
60//!     let w = g.param("w", shape![4, 3]);
61//!     (&x.matmul(&w)).gelu()
62//! });
63//!
64//! // dynamic batch + window slice (no copy in IR)
65//! let g2 = graph("batched", |g| {
66//!     let x = g.input("x", shape![?, 128]);
67//!     x.slice(s![ax(), rg(0, 64)])
68//! });
69//! # let _ = (g, g2);
70//! ```
71//!
72//! Or, with the `dsl` feature, declare the whole graph in a compact little
73//! language with the `rlx!` macro — `@` is matmul, shapes and outputs are
74//! inferred.
75
76mod array;
77#[cfg(feature = "eval")]
78mod cache;
79mod handle;
80#[cfg(feature = "ndarray")]
81mod interop;
82/// Generic low-precision numerics — the `float_format!` macro + `fXmYeZ`
83/// quantizer for emulated any-precision (nvf4 / f8 / bf8 / …) training.
84pub mod lowp;
85#[cfg(feature = "eval")]
86mod model;
87mod scalar;
88#[cfg(feature = "optim")]
89mod schedule;
90mod scope;
91mod slice;
92mod tensor;
93mod transform;
94
95pub use array::{cat, stack};
96pub use rlx_ir::op::{Activation, MaskKind};
97pub use rlx_ir::{DType, Dim, Graph, NodeId, ScaleLayout, ScaledFormat, Shape};
98/// Optimizers for [`Func::train_step`] (re-exported from `rlx_optim`).
99/// Available with the `optim` feature.
100#[cfg(feature = "optim")]
101pub use rlx_optim::{Adam, AdamW, Lion, Muon, OptItem, Optimizer, Sgd};
102#[cfg(feature = "optim")]
103pub use schedule::LrSchedule;
104pub use scope::{GraphScope, graph, graph_with};
105pub use slice::{SliceAxis, SliceSpec, ax, ix, rg, tail};
106pub use tensor::Tensor;
107pub use transform::Func;
108#[cfg(feature = "eval")]
109pub use transform::Jitted;
110
111#[cfg(feature = "eval")]
112pub use cache::{cache_stats, clear_cache};
113/// A compiled, runnable model with by-name weight binding. Available with the
114/// `eval` feature.
115#[cfg(feature = "eval")]
116pub use model::Model;
117/// Device selector for [`Tensor::on`] / [`Tensor::to_vec_on`] (re-exported
118/// from `rlx_runtime`). Available with the `eval` feature.
119#[cfg(feature = "eval")]
120pub use rlx_runtime::Device;
121/// Backend detection (re-exported from `rlx_runtime`): which devices this build
122/// can use, and the fastest one. `to_vec` / `Func::run` pick automatically.
123#[cfg(feature = "eval")]
124pub use rlx_runtime::{available_devices, fastest_device, fastest_device_for, is_available};
125#[cfg(feature = "eval")]
126pub use tensor::Materialize;
127
128#[doc(hidden)]
129#[macro_export]
130macro_rules! __dim {
131    (?) => {
132        $crate::Dim::Dynamic(0)
133    };
134    (? $sym:literal) => {
135        $crate::Dim::Dynamic($sym)
136    };
137    ($n:expr) => {
138        $crate::Dim::Static($n)
139    };
140}
141
142/// Build a [`Shape`]. Default dtype is `F32`.
143///
144/// Each static dimension may be **any `usize` expression** — a literal, an
145/// identifier, or arithmetic like `w / 2`, `w + heads`, `cfg.width * 2` — so graph
146/// builders can size params/inputs from computed dimensions directly (no need to
147/// hoist every derived size into a `let`). Dynamic dimensions use `?` → symbol 0
148/// (repeat `?` for the same unknown size), `?1` → symbol 1, etc. A leading
149/// `Dtype;` sets the element type (default `F32`).
150///
151/// ```rust
152/// use rlx_tensor::{shape, DType};
153///
154/// let a = shape![2, 4];
155/// let b = shape![F32; ?, 128];
156/// let w = 64usize;
157/// let c = shape![w + 1, w / 2, 3 * w];   // computed dimensions
158/// assert_eq!(c.dtype(), DType::F32);
159/// assert!(c.is_static());
160/// assert!(!b.is_static());
161/// ```
162#[macro_export]
163macro_rules! shape {
164    ($dtype:ident; $($rest:tt)+) => {
165        $crate::Shape::from_dims(&$crate::__shape_dims!([] ; $($rest)+), $crate::DType::$dtype)
166    };
167    ($($rest:tt)+) => {
168        $crate::Shape::from_dims(&$crate::__shape_dims!([] ; $($rest)+), $crate::DType::F32)
169    };
170}
171
172/// Token-tree muncher backing [`shape!`]: accumulates `Dim`s left to right,
173/// dispatching `?`/`?N` to dynamic dims and everything else to a `usize` expression.
174/// (A `tt`-list can't mix the `?` syntax with multi-token expressions, so we munch.)
175#[doc(hidden)]
176#[macro_export]
177macro_rules! __shape_dims {
178    // done (with or without a trailing comma)
179    ([$($acc:tt)*] ; ) => { [ $($acc)* ] };
180    ([$($acc:tt)*] ; ,) => { [ $($acc)* ] };
181    // dynamic `?N`
182    ([$($acc:tt)*] ; ? $sym:literal , $($rest:tt)*) => {
183        $crate::__shape_dims!([$($acc)* $crate::Dim::Dynamic($sym),] ; $($rest)*)
184    };
185    ([$($acc:tt)*] ; ? $sym:literal) => {
186        [ $($acc)* $crate::Dim::Dynamic($sym) ]
187    };
188    // dynamic `?` (symbol 0)
189    ([$($acc:tt)*] ; ? , $($rest:tt)*) => {
190        $crate::__shape_dims!([$($acc)* $crate::Dim::Dynamic(0),] ; $($rest)*)
191    };
192    ([$($acc:tt)*] ; ?) => {
193        [ $($acc)* $crate::Dim::Dynamic(0) ]
194    };
195    // static dimension: any expression, more following
196    ([$($acc:tt)*] ; $e:expr, $($rest:tt)*) => {
197        $crate::__shape_dims!([$($acc)* $crate::Dim::Static($e),] ; $($rest)*)
198    };
199    // static dimension: any expression, last
200    ([$($acc:tt)*] ; $e:expr) => {
201        [ $($acc)* $crate::Dim::Static($e) ]
202    };
203}
204
205#[cfg(feature = "dsl")]
206#[doc(hidden)]
207pub use rlx_macros::{__rlx_build, __rlx_expr};
208
209/// Declare a computation [`Graph`] in a compact, readable little language.
210///
211/// `rlx! { … }` is a thin, versatile front-end over the shape-inferring graph
212/// builder: you write the *math*, and shapes, node wiring, and outputs are
213/// filled in for you. It expands to ordinary [`GraphScope`] / [`Tensor`]
214/// calls (zero runtime cost) and evaluates to an [`rlx_ir::Graph`].
215///
216/// # Statements
217/// | form | meaning |
218/// |------|---------|
219/// | `graph "name";` | *(optional, first)* names the graph |
220/// | `input x: [dims];` | a graph input; the binding is auto-named `"x"` |
221/// | `param w: [dims];` | a trainable parameter, auto-named `"w"` |
222/// | `param w[N]: [dims];` | a parameter *family* `w_0 … w_{N-1}`, indexed `w[i]` |
223/// | `param w @ "ir.name": [dims];` | override the IR/`set_param` name (`{i}` = family index) |
224/// | `const eps = 1e-6 : F32;` | a broadcastable scalar constant |
225/// | `const m = [[1,0],[0,1]] : F32;` | an N-D constant from a nested literal |
226/// | `bind t;` / `bind a, b;` | adopt outer-scope Rust `Tensor`(s) of the same name (a baked constant / codebook param) as in-scope bindings |
227/// | `let h = …;` | bind an intermediate to an expression |
228/// | `fn blk(x, w) { … }` | a reusable subgraph, inlined at each call site (call positionally or named: `blk(w: b, x: a)`) |
229/// | `repeat N { … }` | unroll the body `N` times at macro time (weight-tied stacks) |
230/// | `repeat n { … }` | *runtime* count `n` → a `for _ in 0..n` loop; config-driven depth, rebound bindings are loop-carried |
231/// | `repeat i in 0..N { … }` | unroll with index `i` (drives `w[i]`, distinct-weight stacks) |
232/// | `scan h = h0 for N { … }` | compact `Op::Scan` loop (one body graph, not unrolled) |
233/// | `tap a, b;` | expose intermediates as extra outputs (debugging) |
234/// | `out y;` / `out a, b;` | mark graph outputs (defaults to the last `let`) |
235///
236/// `[dims]` uses the [`shape!`] grammar — literals, **runtime `usize`
237/// expressions** (`[bt, d]`, `[d, ff]` size a graph from config values), `?`
238/// for a dynamic axis, and an optional `DType;` prefix (`[F32; ?, 128]`).
239///
240/// # Expressions
241/// * `a @ b` — matrix multiply (also `matmul(a, b)` / `mm(a, b)`)
242/// * `+ - * / %` `**` — elementwise, with broadcasting and scalar promotion
243///   (`x * 2.0`, `x ** 2`); `**` is power, `%` is `fmod`
244/// * `== != < <= > >=` — elementwise comparison → a `Bool` mask (`x > 0.0`)
245/// * `f(x)` — any no-extra-arg [`Tensor`] method: `gelu(x)`, `sqrt(x)`, …
246/// * `maximum(a,b)` `minimum(a,b)` `pow(a,b)` `atan2(a,b)` `rem(a,b)` — binary
247///   tensor ops (a scalar operand is promoted); `clamp(x, lo, hi)`;
248///   `select(cond, a, b)` (mask select, aka `where_`)
249/// * `linear(x, w, b)` — HF-style `x·Wᵀ + b` (W is `[out,in]`) as one fused
250///   matmul+bias op; a wrapping activation folds in (`gelu(linear(x,w,b))`).
251///   `matmul_t(a, b)` = `a·bᵀ`
252/// * a user `fn`, called `blk(a, w)`, inlines its body here
253/// * `x.method(args)` — the escape hatch: **any** `Tensor` method. A bare
254///   argument naming a binding is validated and auto-borrowed, so
255///   `q.attention(k, v, 8, 64, MaskKind::Causal)` reads naturally (and a typo'd
256///   name is a clear DSL error). Other args are raw Rust; wrap an external
257///   value as `(value)`, or prefix it with `~` (`~eps`, `~num_heads`), to pass
258///   it BY VALUE — the escape a scalar identifier/const needs to reach an
259///   `f32`/`usize`/enum parameter (`h.layer_norm(g, b, ~eps)`,
260///   `q.attention(k, v, ~nh, ~dh, MaskKind::Causal)`) instead of `&f32`.
261///
262/// A definite static matmul shape mismatch (both operands statically 2-D with
263/// incompatible inner dims) is reported at macro-expansion time.
264///
265/// Precedence follows NumPy: `.method(…)` > unary `-` > `**` > (`@` `*` `/` `%`)
266/// > (`+` `-`) > (`== != < <= > >=`), left-associative except right-associative
267/// `**` — so `x @ w * s` is `(x @ w) * s`.
268///
269/// # Example
270/// ```rust
271/// use rlx_tensor::rlx;
272///
273/// let g = rlx! {
274///     graph "mlp";
275///     input x: [?, 784];
276///     param w1: [784, 256];   param b1: [256];
277///     param w2: [256, 10];    param b2: [10];
278///
279///     let h = gelu(x @ w1 + b1);
280///     let y = h @ w2 + b2;
281///     out y;
282/// };
283///
284/// assert_eq!(g.name, "mlp");
285/// assert_eq!(g.outputs.len(), 1);
286/// ```
287///
288/// The macro works identically whether imported from this crate
289/// (`rlx_tensor::rlx!`) or the umbrella (`rlx::rlx!`) — the wrapper resolves
290/// every emitted path through `$crate`.
291///
292/// Requires the `dsl` feature (on for umbrella `rlx` users via `tensor`).
293#[cfg(feature = "dsl")]
294#[macro_export]
295macro_rules! rlx {
296    ( $($body:tt)* ) => {{
297        // Bring the handful of names the generated code references into a
298        // block-local scope via `$crate` (robust across re-exports). The
299        // proc macro below then emits only bare names + method/operator calls.
300        #[allow(unused_imports)]
301        use $crate::{GraphScope, DType, MaskKind, Activation, shape};
302        $crate::__rlx_build! { $($body)* }
303    }};
304}
305
306/// Evaluate a single `rlx!`-grammar expression over in-scope Rust [`Tensor`]
307/// values — the "Rust bridge". Every identifier is a normal Rust variable, so
308/// ordinary `for` loops and config drive the graph structure while the DSL
309/// expresses the per-step math (e.g. `h = rlx_expr!(gelu(linear(h, w, b)))`).
310/// This is what a macro-expanded `rlx! { … }` can't do: runtime-length loops.
311/// Requires the `dsl` feature.
312#[cfg(feature = "dsl")]
313#[macro_export]
314macro_rules! rlx_expr {
315    ( $($body:tt)* ) => {{
316        #[allow(unused_imports)]
317        use $crate::{DType, MaskKind, Activation};
318        $crate::__rlx_expr! { $($body)* }
319    }};
320}