Skip to main content

rlx_tensor/
array.rs

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