Skip to main content

ruda_test_utils/test_tensor/
host_data.rs

1use ruda_kernel::dsl as kernel_dsl;
2use ruda_kernel::dsl::RudaElement;
3use ruda_test_runtime::TestRuntime;
4use ruda_kernel::dsl::client::ComputeClient;
5use ruda_kernel::dsl::prelude::RudaPrimitive;
6use ruda_kernel::library::tensor::TensorHandle;
7use ruda_kernel::dsl::zspace::Shape;
8use ruda_kernel::dsl::zspace::Strides;
9
10use crate::test_tensor::{cast::copy_casted, strides::physical_extent};
11
12#[derive(Debug, Clone)]
13pub struct HostData {
14    pub data: HostDataVec,
15    pub shape: Shape,
16    pub strides: Strides,
17}
18
19#[derive(Eq, PartialEq, PartialOrd, Clone, Copy, Debug)]
20pub enum HostDataType {
21    F32,
22    I32,
23    Bool,
24}
25
26#[derive(Clone, Debug)]
27pub enum HostDataVec {
28    F32(Vec<f32>),
29    I32(Vec<i32>),
30    Bool(Vec<bool>),
31}
32
33impl HostDataVec {
34    pub fn get_f32(&self, i: usize) -> f32 {
35        match self {
36            HostDataVec::F32(items) => items[i],
37            _ => panic!("Can't get as f32"),
38        }
39    }
40
41    pub fn get_bool(&self, i: usize) -> bool {
42        match self {
43            HostDataVec::Bool(items) => items[i],
44            _ => panic!("Can't get as bool"),
45        }
46    }
47
48    pub fn get_i32(&self, i: usize) -> i32 {
49        match self {
50            HostDataVec::I32(items) => items[i],
51            _ => panic!("Can't get as i32"),
52        }
53    }
54
55    pub fn try_get_f32(&self, i: usize) -> Option<f32> {
56        match self {
57            HostDataVec::F32(items) => items.get(i).copied(),
58            _ => None,
59        }
60    }
61
62    pub fn try_get_i32(&self, i: usize) -> Option<i32> {
63        match self {
64            HostDataVec::I32(items) => items.get(i).copied(),
65            _ => None,
66        }
67    }
68
69    pub fn try_get_bool(&self, i: usize) -> Option<bool> {
70        match self {
71            HostDataVec::Bool(items) => items.get(i).copied(),
72            _ => None,
73        }
74    }
75}
76
77impl HostData {
78    pub fn from_tensor_handle(
79        client: &ComputeClient<TestRuntime>,
80        mut tensor_handle: TensorHandle<TestRuntime>,
81        host_data_type: HostDataType,
82    ) -> Self {
83        let shape = tensor_handle.shape().clone();
84        let strides = tensor_handle.strides().clone();
85
86        // Reshape to a flat 1D view of the full physical buffer so the read
87        // covers every offset the jumpy strides might reach. Without this, a
88        // shape like [256,256] with strides [512,1] would only read the
89        // shape.product() (65536) elements that `copy_casted`'s contiguous
90        // rewrite walks, and HostData.get_f32 would then index out-of-bounds
91        // when the logical walk crosses the padding.
92        let physical_len = physical_extent(&shape, &strides);
93        tensor_handle.metadata.shape = Shape::from(vec![physical_len]);
94        tensor_handle.metadata.strides = Strides::new(&[1]);
95
96        let data = match host_data_type {
97            HostDataType::F32 => {
98                let handle = copy_casted(
99                    client,
100                    tensor_handle,
101                    f32::as_type_native_unchecked().storage_type(),
102                );
103                let data = f32::from_bytes(
104                    &client.read_one_unchecked_tensor(handle.into_copy_descriptor()),
105                )
106                .to_owned();
107
108                HostDataVec::F32(data)
109            }
110            HostDataType::I32 => {
111                let handle = copy_casted(
112                    client,
113                    tensor_handle,
114                    i32::as_type_native_unchecked().storage_type(),
115                );
116                let data = i32::from_bytes(
117                    &client.read_one_unchecked_tensor(handle.into_copy_descriptor()),
118                )
119                .to_owned();
120
121                HostDataVec::I32(data)
122            }
123            HostDataType::Bool => {
124                let handle = copy_casted(
125                    client,
126                    tensor_handle,
127                    u32::as_type_native_unchecked().storage_type(),
128                );
129                let data = u32::from_bytes(
130                    &client.read_one_unchecked_tensor(handle.into_copy_descriptor()),
131                )
132                .to_owned();
133
134                HostDataVec::Bool(data.iter().map(|&x| x > 0).collect())
135            }
136        };
137
138        Self {
139            data,
140            shape,
141            strides,
142        }
143    }
144
145    pub fn get_f32(&self, index: &[usize]) -> f32 {
146        self.data.get_f32(self.strided_index(index))
147    }
148
149    pub fn get_bool(&self, index: &[usize]) -> bool {
150        self.data.get_bool(self.strided_index(index))
151    }
152
153    pub fn get_i32(&self, index: &[usize]) -> i32 {
154        self.data.get_i32(self.strided_index(index))
155    }
156
157    /// Like [`get_f32`], but returns `None` if the underlying data isn't `F32`
158    /// (or the index is out of bounds), instead of panicking.
159    pub fn try_get_f32(&self, index: &[usize]) -> Option<f32> {
160        self.data.try_get_f32(self.strided_index(index))
161    }
162
163    pub fn try_get_i32(&self, index: &[usize]) -> Option<i32> {
164        self.data.try_get_i32(self.strided_index(index))
165    }
166
167    pub fn try_get_bool(&self, index: &[usize]) -> Option<bool> {
168        self.data.try_get_bool(self.strided_index(index))
169    }
170
171    /// Iterate every logical index in row-major order, yielding the index vector.
172    ///
173    /// Useful when callers want to walk a non-contiguous tensor without
174    /// re-implementing the rank recursion themselves.
175    pub fn iter_indices(&self) -> impl Iterator<Item = Vec<usize>> + '_ {
176        IndexIter::new(self.shape.as_slice().to_vec())
177    }
178
179    /// Iterate `(index, f32 value)` pairs in row-major order.
180    /// Panics if the underlying data isn't `F32`.
181    pub fn iter_indexed_f32(&self) -> impl Iterator<Item = (Vec<usize>, f32)> + '_ {
182        self.iter_indices().map(move |idx| {
183            let v = self.get_f32(&idx);
184            (idx, v)
185        })
186    }
187
188    /// Iterate `(index, i32 value)` pairs in row-major order.
189    /// Panics if the underlying data isn't `I32`.
190    pub fn iter_indexed_i32(&self) -> impl Iterator<Item = (Vec<usize>, i32)> + '_ {
191        self.iter_indices().map(move |idx| {
192            let v = self.get_i32(&idx);
193            (idx, v)
194        })
195    }
196
197    /// Iterate `(index, bool value)` pairs in row-major order.
198    /// Panics if the underlying data isn't `Bool`.
199    pub fn iter_indexed_bool(&self) -> impl Iterator<Item = (Vec<usize>, bool)> + '_ {
200        self.iter_indices().map(move |idx| {
201            let v = self.get_bool(&idx);
202            (idx, v)
203        })
204    }
205
206    fn strided_index(&self, index: &[usize]) -> usize {
207        let mut i = 0usize;
208        for (d, idx) in index.iter().enumerate() {
209            i += idx * self.strides[d];
210        }
211        i
212    }
213
214    /// Render the tensor as one or more 2-D tables.
215    ///
216    /// - rank 1: a single row.
217    /// - rank 2: a table.
218    /// - rank ≥ 3: one labeled table per combination of leading-dim indices
219    ///   (the last two dims are always the row/col axes).
220    pub fn pretty_print(&self) -> String {
221        self.pretty_print_filtered(None)
222    }
223
224    /// Like [`pretty_print`], but only prints slices whose leading-dim indices
225    /// match the filter. Wildcards (`DimFilter::Any`) iterate every value.
226    ///
227    /// `filter` accepts both `Vec<std::ops::Range<usize>>` and the canonical
228    /// `TensorFilter` (the `RUDA_TEST_MODE` `M-K` syntax).
229    pub fn pretty_print_slice<I>(&self, filter: I) -> String
230    where
231        I: IntoIterator,
232        I::Item: Into<crate::DimFilter>,
233    {
234        let f: crate::TensorFilter = filter.into_iter().map(Into::into).collect();
235        assert_eq!(
236            f.len(),
237            self.shape.rank(),
238            "pretty_print_slice: filter rank ({}) must match tensor rank ({})",
239            f.len(),
240            self.shape.rank(),
241        );
242        self.pretty_print_filtered(Some(f))
243    }
244
245    fn pretty_print_filtered(&self, filter: Option<crate::TensorFilter>) -> String {
246        let rank = self.shape.rank();
247        match rank {
248            0 => String::new(),
249            1 => {
250                // Single-row table; the only filter entry filters the col axis.
251                let col_filter = filter.as_ref().and_then(|f| f.first());
252                let cols = axis_indices(col_filter, self.shape[0]);
253                let rows = vec![0usize];
254                pretty_print_table(&rows, &cols, |_row_label, col_label| {
255                    self.cell_string(self.strided_index(&[col_label]))
256                })
257            }
258            2 => {
259                // Last two filter entries (here filter[0], filter[1]) drive
260                // row and col selection respectively.
261                let row_filter = filter.as_ref().and_then(|f| f.first());
262                let col_filter = filter.as_ref().and_then(|f| f.get(1));
263                let rows = axis_indices(row_filter, self.shape[0]);
264                let cols = axis_indices(col_filter, self.shape[1]);
265                pretty_print_table(&rows, &cols, |row_label, col_label| {
266                    self.cell_string(self.strided_index(&[row_label, col_label]))
267                })
268            }
269            _ => self.print_higher_rank(filter.as_ref()),
270        }
271    }
272
273    fn cell_string(&self, idx: usize) -> String {
274        match &self.data {
275            HostDataVec::I32(_) => self.data.get_i32(idx).to_string(),
276            HostDataVec::F32(_) => format!("{:.3}", self.data.get_f32(idx)),
277            HostDataVec::Bool(_) => self.data.get_bool(idx).to_string(),
278        }
279    }
280
281    fn print_higher_rank(&self, filter: Option<&crate::TensorFilter>) -> String {
282        let rank = self.shape.rank();
283        let leading_dims = rank - 2;
284        let row_dim = self.shape[rank - 2];
285        let col_dim = self.shape[rank - 1];
286
287        // Filter entries for the row and col axes (the last two), if any.
288        let row_filter = filter.and_then(|f| f.get(rank - 2));
289        let col_filter = filter.and_then(|f| f.get(rank - 1));
290        let row_indices = axis_indices(row_filter, row_dim);
291        let col_indices = axis_indices(col_filter, col_dim);
292
293        let mut out = String::new();
294        let mut leading = vec![0usize; leading_dims];
295
296        // Iterate every leading-index combination, lexicographically.
297        loop {
298            let print_this = match filter {
299                None => true,
300                Some(f) => leading_indices_match(&leading, f),
301            };
302
303            if print_this {
304                if !out.is_empty() {
305                    out.push('\n');
306                }
307                out.push_str(&format!("{}:\n", format_leading_label(&leading, rank)));
308
309                let table = pretty_print_table(&row_indices, &col_indices, |row, col| {
310                    let mut full = leading.clone();
311                    full.push(row);
312                    full.push(col);
313                    self.cell_string(self.strided_index(&full))
314                });
315                out.push_str(&table);
316            }
317
318            // Increment the leading-index counter.
319            if !increment_lex(&mut leading, &self.shape.as_slice()[..leading_dims]) {
320                break;
321            }
322        }
323
324        out
325    }
326}
327
328pub fn pretty_print_zip(tensors: &[&HostData]) -> String {
329    assert!(!tensors.is_empty(), "Need at least one tensor");
330
331    let dims = tensors[0].shape.as_slice();
332
333    for t in tensors {
334        assert_eq!(t.shape.as_slice(), dims, "All tensors must have same shape");
335    }
336
337    let rank = tensors[0].shape.rank();
338
339    let cell = |full: &[usize]| -> String {
340        let mut parts = Vec::with_capacity(tensors.len());
341        for t in tensors {
342            let idx = t.strided_index(full);
343            parts.push(t.cell_string(idx));
344        }
345        parts.join("/")
346    };
347
348    match rank {
349        0 => String::new(),
350        1 => {
351            let cols: Vec<usize> = (0..dims[0]).collect();
352            pretty_print_table(&[0], &cols, |_, col| cell(&[col]))
353        }
354        2 => {
355            let rows: Vec<usize> = (0..dims[0]).collect();
356            let cols: Vec<usize> = (0..dims[1]).collect();
357            pretty_print_table(&rows, &cols, |row, col| cell(&[row, col]))
358        }
359        _ => {
360            let leading_dims = rank - 2;
361            let rows: Vec<usize> = (0..dims[rank - 2]).collect();
362            let cols: Vec<usize> = (0..dims[rank - 1]).collect();
363            let mut out = String::new();
364            let mut leading = vec![0usize; leading_dims];
365            loop {
366                if !out.is_empty() {
367                    out.push('\n');
368                }
369                out.push_str(&format!("{}:\n", format_leading_label(&leading, rank)));
370                let table = pretty_print_table(&rows, &cols, |row, col| {
371                    let mut full = leading.clone();
372                    full.push(row);
373                    full.push(col);
374                    cell(&full)
375                });
376                out.push_str(&table);
377
378                if !increment_lex(&mut leading, &dims[..leading_dims]) {
379                    break;
380                }
381            }
382            out
383        }
384    }
385}
386
387/// Match leading indices against the leading slice of a tensor filter. The
388/// trailing two filter entries (covering the row/col axes) are ignored — we
389/// always print the full row × col table for the slices we keep.
390fn leading_indices_match(leading: &[usize], filter: &crate::TensorFilter) -> bool {
391    use crate::DimFilter::*;
392    for (dim, &idx) in leading.iter().enumerate() {
393        let f = filter.get(dim).unwrap_or(&Any);
394        match f {
395            Any => {}
396            Exact(v) => {
397                if idx != *v {
398                    return false;
399                }
400            }
401            Range { start, end } => {
402                if idx < *start || idx > *end {
403                    return false;
404                }
405            }
406        }
407    }
408    true
409}
410
411/// Lexicographic increment over `idx[i] in 0..bounds[i]`. Returns `false` when
412/// the counter has wrapped past the last position (i.e. iteration is done).
413fn increment_lex(idx: &mut [usize], bounds: &[usize]) -> bool {
414    if idx.is_empty() {
415        return false;
416    }
417    for d in (0..idx.len()).rev() {
418        idx[d] += 1;
419        if idx[d] < bounds[d] {
420            return true;
421        }
422        idx[d] = 0;
423    }
424    false
425}
426
427/// Row-major index iterator. Yields every position in a tensor of the given
428/// shape, lexicographically (last dim varies fastest). For a rank-0 tensor
429/// (empty shape) the iterator yields a single empty index vector and stops.
430struct IndexIter {
431    shape: Vec<usize>,
432    next: Option<Vec<usize>>,
433}
434
435impl IndexIter {
436    fn new(shape: Vec<usize>) -> Self {
437        // Empty dim → no indices.
438        let next = if shape.contains(&0) {
439            None
440        } else {
441            Some(vec![0; shape.len()])
442        };
443        Self { shape, next }
444    }
445}
446
447impl Iterator for IndexIter {
448    type Item = Vec<usize>;
449
450    fn next(&mut self) -> Option<Self::Item> {
451        let current = self.next.clone()?;
452
453        // Advance the counter for the next call. `increment_lex` returns
454        // `false` when we've passed the last position; rank-0 also lands
455        // here on the first call.
456        let mut tentative = current.clone();
457        if !increment_lex(&mut tentative, &self.shape) {
458            self.next = None;
459        } else {
460            self.next = Some(tentative);
461        }
462
463        Some(current)
464    }
465}
466
467fn format_leading_label(leading: &[usize], rank: usize) -> String {
468    let mut parts: Vec<String> = leading.iter().map(|i| i.to_string()).collect();
469    // Rows/cols are the last two dims — render as `*` so the label reads
470    // `[i, j, *, *]`.
471    for _ in 0..(rank - leading.len()) {
472        parts.push("*".to_string());
473    }
474    format!("[{}]", parts.join(", "))
475}
476
477/// Resolve which indices along a single dim should be rendered, given a
478/// per-dim filter entry. `None` means "render everything", which is the
479/// default for unfiltered prints.
480fn axis_indices(f: Option<&crate::DimFilter>, dim_size: usize) -> Vec<usize> {
481    use crate::DimFilter::*;
482    match f {
483        None | Some(Any) => (0..dim_size).collect(),
484        Some(Exact(v)) => {
485            if *v < dim_size {
486                vec![*v]
487            } else {
488                Vec::new()
489            }
490        }
491        Some(Range { start, end }) => {
492            if *start >= dim_size {
493                Vec::new()
494            } else {
495                (*start..=(*end).min(dim_size.saturating_sub(1))).collect()
496            }
497        }
498    }
499}
500
501fn pretty_print_table<F>(rows: &[usize], cols: &[usize], mut cell: F) -> String
502where
503    F: FnMut(usize, usize) -> String,
504{
505    let mut max_width = 0;
506
507    for &r in rows {
508        for &c in cols {
509            max_width = max_width.max(cell(r, c).len());
510        }
511    }
512
513    // Also account for the column-label width (so a tensor sliced down to
514    // `[10-12]` renders header `10 11 12` without crowding).
515    let label_width = cols.iter().map(|c| c.to_string().len()).max().unwrap_or(0);
516    max_width = max_width.max(label_width).max(2);
517
518    let row_label_width = rows
519        .iter()
520        .map(|r| r.to_string().len())
521        .max()
522        .unwrap_or(0)
523        .max(3);
524
525    let mut s = String::new();
526
527    // header
528    s.push_str(&format!("{:>width$} |", "", width = row_label_width));
529    for &col in cols {
530        s.push_str(&format!(" {:>width$}", col, width = max_width));
531    }
532    s.push('\n');
533
534    // separator
535    s.push_str(&"-".repeat(row_label_width + 1));
536    s.push('+');
537    for _ in cols {
538        s.push_str(&"-".repeat(max_width + 1));
539    }
540    s.push('\n');
541
542    // rows
543    for &row in rows {
544        s.push_str(&format!("{:>width$} |", row, width = row_label_width));
545
546        for &col in cols {
547            let value = cell(row, col);
548            s.push_str(&format!(" {:>width$}", value, width = max_width));
549        }
550
551        s.push('\n');
552    }
553
554    s
555}