Skip to main content

rlx_tensor/
array.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//! Data-bearing constructors — the NumPy-style entry points.
6//!
7//! Each returns a [`Tensor`] backed by its own freshly-rooted graph with the
8//! host data embedded as an [`rlx_ir::Op::Constant`]. Combining tensors from
9//! different constructors merges their graphs transparently (see
10//! [`crate::handle::GraphHandle::adopt`]), so callers never touch a graph
11//! scope:
12//!
13//! ```rust
14//! use rlx_tensor::Tensor;
15//!
16//! let a = Tensor::from_vec(vec![1.0, 2.0, 3.0], [3]);
17//! let b = Tensor::ones([3]);
18//! let c = (&a + &b).relu();
19//! assert_eq!(c.rank(), 1);
20//! ```
21
22use rlx_ir::{DType, Graph, GraphExt, NodeId, Op, Shape};
23
24use crate::Tensor;
25use crate::handle::GraphHandle;
26
27/// Concatenate tensors along an existing `axis` (NumPy `concatenate`). Inputs
28/// from different graphs are merged transparently.
29pub fn cat(tensors: &[&Tensor], axis: usize) -> Tensor {
30    assert!(!tensors.is_empty(), "cat: need at least one tensor");
31    let root = tensors[0];
32    let ids: Vec<NodeId> = tensors.iter().map(|t| root.adopt(t)).collect();
33    let id = root.handle.with_graph(|g| g.concat_(ids, axis));
34    Tensor::new(root.handle.clone(), id)
35}
36
37/// Stack tensors along a new `axis` (NumPy `stack`): each gains a size-1 axis
38/// at `axis`, then they are concatenated there.
39pub fn stack(tensors: &[&Tensor], axis: usize) -> Tensor {
40    assert!(!tensors.is_empty(), "stack: need at least one tensor");
41    let expanded: Vec<Tensor> = tensors.iter().map(|t| t.unsqueeze(axis)).collect();
42    let refs: Vec<&Tensor> = expanded.iter().collect();
43    cat(&refs, axis)
44}
45
46impl Tensor {
47    /// Constant tensor from row-major host data (F32). The product of `shape`
48    /// must equal `data.len()`.
49    pub fn from_vec(data: impl Into<Vec<f32>>, shape: impl AsRef<[usize]>) -> Self {
50        let data = data.into();
51        let dims = shape.as_ref();
52        let shape = Shape::new(dims, DType::F32);
53        let n = shape
54            .num_elements()
55            .expect("from_vec: shape must be static");
56        assert_eq!(
57            n,
58            data.len(),
59            "from_vec: shape {dims:?} expects {n} elements, got {}",
60            data.len()
61        );
62        Self::constant_f32(&data, shape)
63    }
64
65    /// All-zeros constant of the given shape (F32).
66    pub fn zeros(shape: impl AsRef<[usize]>) -> Self {
67        Self::full(shape, 0.0)
68    }
69
70    /// All-ones constant of the given shape (F32).
71    pub fn ones(shape: impl AsRef<[usize]>) -> Self {
72        Self::full(shape, 1.0)
73    }
74
75    /// Constant of the given shape filled with `value` (F32).
76    pub fn full(shape: impl AsRef<[usize]>, value: f32) -> Self {
77        let dims = shape.as_ref();
78        let shape = Shape::new(dims, DType::F32);
79        let n = shape.num_elements().expect("full: shape must be static");
80        Self::constant_f32(&vec![value; n], shape)
81    }
82
83    /// `n × n` identity matrix (F32).
84    pub fn eye(n: usize) -> Self {
85        let mut data = vec![0.0f32; n * n];
86        for i in 0..n {
87            data[i * n + i] = 1.0;
88        }
89        Self::constant_f32(&data, Shape::new(&[n, n], DType::F32))
90    }
91
92    /// 1-D constant `[0, 1, …, end)` (F32).
93    pub fn arange(end: i64) -> Self {
94        Self::arange_step(0, end, 1)
95    }
96
97    /// 1-D constant `[start, start+step, …)` up to (excluding) `end` (F32).
98    pub fn arange_step(start: i64, end: i64, step: i64) -> Self {
99        assert!(step != 0, "arange: step must be non-zero");
100        let mut v = Vec::new();
101        let mut x = start;
102        if step > 0 {
103            while x < end {
104                v.push(x as f32);
105                x += step;
106            }
107        } else {
108            while x > end {
109                v.push(x as f32);
110                x += step;
111            }
112        }
113        let len = v.len();
114        Self::constant_f32(&v, Shape::new(&[len], DType::F32))
115    }
116
117    /// `F64` (double-precision) constant from host data.
118    pub fn from_f64(data: impl AsRef<[f64]>, shape: impl AsRef<[usize]>) -> Self {
119        let data = data.as_ref();
120        let shape = Shape::new(shape.as_ref(), DType::F64);
121        let n = shape
122            .num_elements()
123            .expect("from_f64: shape must be static");
124        assert_eq!(
125            n,
126            data.len(),
127            "from_f64: shape expects {n} elements, got {}",
128            data.len()
129        );
130        let bytes = data.iter().flat_map(|v| v.to_le_bytes()).collect();
131        Self::constant_raw(bytes, shape)
132    }
133
134    /// `I64` constant from host data (general N-D).
135    pub fn from_i64(data: impl AsRef<[i64]>, shape: impl AsRef<[usize]>) -> Self {
136        let data = data.as_ref();
137        let shape = Shape::new(shape.as_ref(), DType::I64);
138        let n = shape
139            .num_elements()
140            .expect("from_i64: shape must be static");
141        assert_eq!(
142            n,
143            data.len(),
144            "from_i64: shape expects {n} elements, got {}",
145            data.len()
146        );
147        let bytes = data.iter().flat_map(|v| v.to_le_bytes()).collect();
148        Self::constant_raw(bytes, shape)
149    }
150
151    /// 1-D `I64` index vector for [`gather`](Tensor::gather) /
152    /// [`index_select`](Tensor::index_select).
153    pub fn index_vec(data: impl AsRef<[i64]>) -> Self {
154        let data = data.as_ref();
155        Self::from_i64(data, [data.len()])
156    }
157
158    /// Build a fresh-graph F32 constant node from host data.
159    fn constant_f32(data: &[f32], shape: Shape) -> Self {
160        let bytes = data.iter().flat_map(|x| x.to_le_bytes()).collect();
161        Self::constant_raw(bytes, shape)
162    }
163
164    /// Uniform random in `[0, 1)` (F32), reproducible from `seed`.
165    pub fn rand(shape: impl AsRef<[usize]>, seed: u64) -> Self {
166        Self::rand_range(shape, seed, 0.0, 1.0)
167    }
168
169    /// Uniform random in `[lo, hi)` (F32), reproducible from `seed`.
170    pub fn rand_range(shape: impl AsRef<[usize]>, seed: u64, lo: f32, hi: f32) -> Self {
171        let shape = Shape::new(shape.as_ref(), DType::F32);
172        let n = shape
173            .num_elements()
174            .expect("rand_range: shape must be static");
175        let mut s = seed_state(seed);
176        let data: Vec<f32> = (0..n).map(|_| lo + (hi - lo) * next_unit(&mut s)).collect();
177        Self::constant_f32(&data, shape)
178    }
179
180    /// Standard-normal random (mean 0, std 1), reproducible from `seed`.
181    pub fn randn(shape: impl AsRef<[usize]>, seed: u64) -> Self {
182        Self::randn_std(shape, seed, 0.0, 1.0)
183    }
184
185    /// Normal random with the given `mean`/`std`, reproducible from `seed`.
186    /// Useful for weight init, e.g. `randn_std(shape, seed, 0.0, (2.0/fan_in).sqrt())`.
187    pub fn randn_std(shape: impl AsRef<[usize]>, seed: u64, mean: f32, std: f32) -> Self {
188        let shape = Shape::new(shape.as_ref(), DType::F32);
189        let n = shape
190            .num_elements()
191            .expect("randn_std: shape must be static");
192        let mut s = seed_state(seed);
193        let data: Vec<f32> = (0..n).map(|_| mean + std * next_normal(&mut s)).collect();
194        Self::constant_f32(&data, shape)
195    }
196
197    /// Build a fresh-graph constant node from raw little-endian bytes.
198    fn constant_raw(bytes: Vec<u8>, shape: Shape) -> Self {
199        let mut graph = Graph::new("rlx");
200        let id = graph.add_node(Op::Constant { data: bytes }, vec![], shape);
201        Tensor::new(GraphHandle::new(graph), id)
202    }
203}
204
205/// SplitMix64 — a tiny, dependency-free PRNG so random constructors are
206/// reproducible (host-generated, baked as constants) without a `rand` dep.
207fn seed_state(seed: u64) -> u64 {
208    // Avoid the all-zero fixed point.
209    seed ^ 0x9E37_79B9_7F4A_7C15
210}
211
212fn next_u64(s: &mut u64) -> u64 {
213    *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
214    let mut z = *s;
215    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
216    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
217    z ^ (z >> 31)
218}
219
220/// Uniform `f32` in `[0, 1)` from 24 random mantissa bits.
221fn next_unit(s: &mut u64) -> f32 {
222    (next_u64(s) >> 40) as f32 / (1u64 << 24) as f32
223}
224
225/// Standard normal via Box–Muller.
226fn next_normal(s: &mut u64) -> f32 {
227    let u1 = next_unit(s).max(f32::MIN_POSITIVE);
228    let u2 = next_unit(s);
229    (-2.0 * u1.ln()).sqrt() * (std::f32::consts::TAU * u2).cos()
230}