1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
//! The `Tensor<T>` data structure.
//!
//! `Tensor<T>` is the actual data: an `ObjectMeta` plus a
//! `Vec<T>`. The `Tensor::dense_cpu` constructor builds a dense
//! CPU-stored tensor; the `Tensor::padic` constructor builds a
//! p-adic scalar tensor. The i64 backend is the default; other
//! element types (f32, p-adic digits) are constructed on demand.
//!
use crate::domain::DomainId;
use super::{Dim, ObjectMeta, Representation, Shape};
#[derive(Debug, Clone, PartialEq)]
pub struct Tensor<T> {
pub meta: ObjectMeta,
pub data: Vec<T>,
}
impl<T> Tensor<T> {
/// Build a dense-CPU tensor from a domain, shape, and data vector.
/// This is the existing unchecked constructor: it does not validate
/// that `data.len()` matches the product of static shape dimensions.
/// Use [`Tensor::try_from_vec`] for a fallible variant.
pub fn dense_cpu(domain: DomainId, shape: Shape, data: Vec<T>) -> Self {
Self {
meta: ObjectMeta::tensor(domain, shape, Representation::dense_cpu()),
data,
}
}
/// Build a dense-CPU tensor, validating that the data length matches
/// the product of all `Dim::Static` dimensions. Non-static dimensions
/// (Symbolic, Bounded, DataDependent) cannot be validated, so a shape
/// containing any of those returns an `Error::Shape`.
pub fn try_from_vec(
domain: DomainId,
shape: Shape,
data: Vec<T>,
) -> Result<Self, crate::Error> {
let mut expected: usize = 1;
for dim in shape.dims.iter() {
expected = expected
.checked_mul(dim.value().ok_or_else(|| {
crate::Error::shape(format!(
"try_from_vec requires all-static shape, got dim {dim:?}"
))
})?)
.ok_or_else(|| {
crate::Error::shape(format!(
"shape product overflows usize for shape {shape:?}"
))
})?;
}
let expected = expected;
if data.len() != expected {
return Err(crate::Error::shape(format!(
"data length {} does not match static shape product {expected} for shape {shape:?}",
data.len()
)));
}
Ok(Self::dense_cpu(domain, shape, data))
}
/// Number of elements in the dense data vector.
pub fn len(&self) -> usize {
self.data.len()
}
/// Whether the dense data vector is empty.
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
/// Rank of the tensor (number of dimensions).
pub fn rank(&self) -> usize {
self.meta.shape.rank()
}
/// Borrow the dense data as a slice.
pub fn as_slice(&self) -> &[T] {
&self.data
}
}
impl<T: Clone> Tensor<T> {
/// Build a rank-1 vector tensor from a domain, a single dimension, and a
/// data vector. The data vector is moved into the tensor.
pub fn vector(domain: DomainId, dim: usize, data: Vec<T>) -> Self {
Self::dense_cpu(domain, Shape::from(vec![dim]), data)
}
/// Build a rank-2 matrix tensor from a domain, two dimensions, and a
/// row-major data vector of length `rows * cols`. Returns an error if the
/// data length does not match.
pub fn matrix(
domain: DomainId,
rows: usize,
cols: usize,
data: Vec<T>,
) -> Result<Self, crate::Error> {
let expected = rows.checked_mul(cols).ok_or_else(|| {
crate::Error::shape(format!(
"matrix dimensions overflow: rows={rows} cols={cols}"
))
})?;
if data.len() != expected {
return Err(crate::Error::shape(format!(
"matrix data length {} does not match rows*cols={expected}",
data.len()
)));
}
Ok(Self::dense_cpu(domain, Shape::from(vec![rows, cols]), data))
}
/// Borrow the rank-2 matrix rows as a slice of row slices. Returns None if
/// the tensor is not rank-2.
pub fn as_matrix_rows(&self) -> Option<&[T]> {
if self.rank() == 2 {
Some(&self.data)
} else {
None
}
}
}
impl<T: std::fmt::Display> std::fmt::Display for Tensor<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// For scalar tensors, just print the single element.
if self.meta.shape.dims.is_empty() {
if let Some(value) = self.data.first() {
return write!(f, "{value}");
}
return write!(f, "<empty scalar>");
}
// For 1-D tensors, print the values comma-separated in brackets.
if self.meta.shape.dims.len() == 1 {
let parts: Vec<String> = self.data.iter().map(|v| v.to_string()).collect();
return write!(f, "[{}]", parts.join(", "));
}
// For rank >= 2 tensors, print a small summary header.
write!(
f,
"Tensor<{}>(shape={}, len={})",
std::any::type_name::<T>(),
self.meta.shape,
self.data.len()
)
}
}
/// Iterate over owned tensor data.
impl<T> IntoIterator for Tensor<T> {
type Item = T;
type IntoIter = std::vec::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter()
}
}
/// Build a rank-1 dense i64 tensor from any iterator of i64 values.
impl FromIterator<i64> for Tensor<i64> {
fn from_iter<I: IntoIterator<Item = i64>>(iter: I) -> Self {
let data: Vec<i64> = iter.into_iter().collect();
let dim = data.len();
Self::vector(crate::domain::DomainId::new("integer"), dim, data)
}
}
impl<T: Clone> Tensor<T> {
/// Build a dense CPU tensor filled with the supplied constant value, with
/// the supplied shape. Total element count is the product of the static
/// dimensions of the shape.
pub fn filled(domain: DomainId, shape: Shape, value: T) -> Self {
// For a scalar (empty dims) the total is 1; otherwise the product
// of the Static dimensions. Non-Static dimensions are skipped, so a
// shape that mixes Static and Symbolic dims uses the Static product.
let total: usize = if shape.dims.is_empty() {
1
} else {
shape
.dims
.iter()
.filter_map(|dim| match dim {
super::Dim::Static(value) => Some(*value),
_ => None,
})
.product()
};
let data: Vec<T> = (0..total).map(|_| value.clone()).collect();
Self::dense_cpu(domain, shape, data)
}
}
impl Tensor<i64> {
/// Build a dense CPU i64 tensor of the supplied shape filled with zeros.
/// Requires that all dimensions are Static; returns an error otherwise.
pub fn zeros(domain: DomainId, shape: Shape) -> Result<Self, crate::Error> {
for dim in &shape.dims {
if !matches!(dim, super::Dim::Static(_)) {
return Err(crate::Error::shape(format!(
"Tensor::zeros requires all-Static dimensions, got {shape:?}"
)));
}
}
let total: usize = if shape.dims.is_empty() {
1
} else {
shape
.dims
.iter()
.filter_map(|dim| match dim {
super::Dim::Static(value) => Some(*value),
_ => None,
})
.product()
};
Ok(Self::dense_cpu(domain, shape, vec![0_i64; total]))
}
}
impl Tensor<i64> {
/// Concatenate two rank-1 dense i64 tensors along their first (and only)
/// axis. Both inputs must be rank-1 dense i64 tensors on the same domain.
/// Returns a new rank-1 tensor with `a.len() + b.len()` elements. The
/// resulting shape is `[a.len() + b.len()]`.
pub fn concat_along_first_axis(a: &Tensor<i64>, b: &Tensor<i64>) -> Result<Self, crate::Error> {
if a.rank() != 1 || b.rank() != 1 {
return Err(crate::Error::shape(format!(
"concat_along_first_axis requires rank-1 inputs, got ranks {} and {}",
a.rank(),
b.rank()
)));
}
if a.meta.domain != b.meta.domain {
return Err(crate::Error::domain(format!(
"concat_along_first_axis requires matching domains, got {:?} and {:?}",
a.meta.domain, b.meta.domain
)));
}
let mut data: Vec<i64> = Vec::with_capacity(a.data.len() + b.data.len());
data.extend_from_slice(&a.data);
data.extend_from_slice(&b.data);
let dim = data.len();
Ok(Self::vector(a.meta.domain.clone(), dim, data))
}
}
impl<T: PartialEq> Tensor<T> {
/// Rank-aware element equality at a multi-dimensional index.
///
/// Returns `true` when:
/// - the linear `index` is in-bounds for `self` and `other`,
/// - the flattened rank of `self` and `other` match,
/// - the per-dimension sizes of `self` and `other` match, and
/// - the element at `index` in `self` equals the element at `index` in
/// `other`.
///
/// Returns `false` for any out-of-bounds index, shape mismatch, or value
/// mismatch. The helper never panics, even when the index is past the
/// end of either tensor.
pub fn element_equals(&self, other: &Tensor<T>, index: usize) -> bool {
// Shape must match element-wise. We compare the flat dimension list
// because we do not want this helper to require the two tensors to
// share the same ObjectMeta.
if self.meta.shape.dims != other.meta.shape.dims {
return false;
}
// Rank must also be the same as the data length when both are dense.
if self.data.is_empty() && other.data.is_empty() {
return index == 0 && self.rank() == other.rank();
}
if self.data.is_empty() || other.data.is_empty() {
return false;
}
let self_len = self.data.len();
let other_len = other.data.len();
if index >= self_len || index >= other_len {
return false;
}
self.data[index] == other.data[index]
}
}
impl<T> Tensor<T> {
/// Borrow the first element of the dense data, or `None` when empty.
pub fn first(&self) -> Option<&T> {
self.data.first()
}
/// Borrow the last element of the dense data, or `None` when empty.
pub fn last(&self) -> Option<&T> {
self.data.last()
}
}
impl Tensor<i64> {
/// Flatten a rank-2 matrix tensor into a rank-1 vector tensor by
/// concatenating the rows in row-major order. Returns an error if the
/// tensor is not rank-2.
pub fn flatten_first_axis(t: &Tensor<i64>) -> Result<Tensor<i64>, crate::Error> {
if t.rank() != 2 {
return Err(crate::Error::verification(format!(
"flatten_first_axis requires rank-2 tensor, got rank={}",
t.rank()
)));
}
let rows = t.meta.shape.first_dim().and_then(Dim::value).unwrap_or(0);
let new_shape = if rows == 0 {
Shape::from(vec![0usize])
} else {
// Total element count comes from data length when shape is dense.
Shape::from(vec![t.data.len()])
};
Ok(Tensor {
meta: ObjectMeta::tensor(
t.meta.domain.clone(),
new_shape,
t.meta.representation.clone(),
),
data: t.data.clone(),
})
}
}
impl Tensor<i64> {
/// Sum of all elements in the dense data, computed with `i64::wrapping_add`
/// so that overflow wraps rather than panicking. An empty tensor returns
/// `0`. The sum is independent of the tensor's shape.
pub fn sum(&self) -> i64 {
self.data.iter().copied().fold(0i64, i64::wrapping_add)
}
/// Remove the first `n` elements from the dense data and return them
/// as a `Vec<T>`. The tensor is shortened by `n` (Vec::drain semantics).
/// Use case: popping MoE padding rows after a forward pass.
///
/// Silently saturates: if `n > self.data.len()`, only `self.data.len()`
/// items are drained (no panic). The `T: Clone` bound exists because
/// `Vec::drain` requires the items to be movable.
pub fn truncate(&mut self, n: usize) -> Vec<i64> {
let drain_n = n.min(self.data.len());
self.data.drain(..drain_n).collect()
}
/// Consume the tensor and return its `(meta, data)` parts. Useful
/// when a downstream consumer needs the raw shape/meta/data triple
/// without going through the Tensor wrapper.
pub fn into_inner(self) -> (crate::object::ObjectMeta, Vec<i64>) {
(self.meta, self.data)
}
/// Reverse the order of elements in the dense data, returning a new
/// tensor with the same shape and domain but reversed element order.
/// An empty tensor returns a copy with empty data.
pub fn reversed(&self) -> Tensor<i64> {
let mut data = self.data.clone();
data.reverse();
Tensor {
meta: self.meta.clone(),
data,
}
}
}
impl Tensor<i64> {
/// Smallest element in the dense data, or `None` when empty.
pub fn min(&self) -> Option<i64> {
self.data.iter().copied().min()
}
/// Largest element in the dense data, or `None` when empty.
pub fn max(&self) -> Option<i64> {
self.data.iter().copied().max()
}
}