Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/trueno/src/backends/gpu/tensor_view.rs
Line
Count
Source
1
//! TensorView - GPU Memory Layout Abstraction
2
//!
3
//! Provides a view into GPU buffer memory with shape, stride, and layout information.
4
//! Enables zero-copy slicing and transposition operations.
5
//!
6
//! # cuda-tile-behavior.md References
7
//!
8
//! - Section 3.2: Two-Level Memory Hierarchy
9
//! - Falsification tests #31-40: TensorView correctness
10
//!
11
//! # Academic Foundation
12
//!
13
//! Based on Halide (PLDI 2013): Schedule/algorithm separation improves portability.
14
15
use std::marker::PhantomData;
16
use std::ops::Range;
17
18
/// Memory layout for tensor storage
19
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20
pub enum MemoryLayout {
21
    /// Row-major (C-style): last dimension varies fastest
22
    #[default]
23
    RowMajor,
24
    /// Column-major (Fortran-style): first dimension varies fastest
25
    ColumnMajor,
26
    /// Tiled layout for GPU shared memory optimization
27
    Tiled {
28
        /// Tile dimensions
29
        tile_size: [usize; 2],
30
    },
31
}
32
33
/// A view into a contiguous memory region with shape and stride information.
34
///
35
/// TensorView does not own the data - it provides a structured view over
36
/// existing memory, enabling zero-copy operations like slicing and transposition.
37
///
38
/// # Type Parameters
39
///
40
/// * `T` - Element type (typically f32 for GPU compute)
41
///
42
/// # cuda-tile-behavior.md References
43
///
44
/// - Falsification test #31: TensorView preserves data integrity
45
/// - Falsification test #32: Slicing produces correct views
46
/// - Falsification test #33: Transpose swaps dimensions correctly
47
#[derive(Debug)]
48
pub struct TensorView<T> {
49
    /// Shape of the tensor (up to 4 dimensions: N, C, H, W)
50
    shape: [usize; 4],
51
    /// Strides for each dimension (in elements, not bytes)
52
    strides: [usize; 4],
53
    /// Offset from the start of the buffer (in elements)
54
    offset: usize,
55
    /// Memory layout hint for optimization
56
    layout: MemoryLayout,
57
    /// Number of active dimensions (1-4)
58
    ndim: usize,
59
    /// Phantom data for type safety
60
    _marker: PhantomData<T>,
61
}
62
63
impl<T> TensorView<T> {
64
    /// Create a new TensorView with the given shape.
65
    ///
66
    /// Strides are computed automatically based on row-major layout.
67
    ///
68
    /// # Arguments
69
    ///
70
    /// * `shape` - Shape of the tensor (unused dimensions should be 1)
71
    ///
72
    /// # Examples
73
    ///
74
    /// ```ignore
75
    /// let view = TensorView::<f32>::new([2, 3, 4, 1]); // 2x3x4 tensor
76
    /// assert_eq!(view.numel(), 24);
77
    /// ```
78
0
    pub fn new(shape: [usize; 4]) -> Self {
79
0
        let ndim = Self::compute_ndim(&shape);
80
0
        let strides = Self::compute_row_major_strides(&shape);
81
0
        Self {
82
0
            shape,
83
0
            strides,
84
0
            offset: 0,
85
0
            layout: MemoryLayout::RowMajor,
86
0
            ndim,
87
0
            _marker: PhantomData,
88
0
        }
89
0
    }
90
91
    /// Create a TensorView with explicit strides.
92
    ///
93
    /// # Arguments
94
    ///
95
    /// * `shape` - Shape of the tensor
96
    /// * `strides` - Strides for each dimension (in elements)
97
0
    pub fn with_strides(shape: [usize; 4], strides: [usize; 4]) -> Self {
98
0
        let ndim = Self::compute_ndim(&shape);
99
0
        Self {
100
0
            shape,
101
0
            strides,
102
0
            offset: 0,
103
0
            layout: MemoryLayout::RowMajor,
104
0
            ndim,
105
0
            _marker: PhantomData,
106
0
        }
107
0
    }
108
109
    /// Create a 1D TensorView.
110
0
    pub fn new_1d(len: usize) -> Self {
111
0
        Self::new([len, 1, 1, 1])
112
0
    }
113
114
    /// Create a 2D TensorView (matrix).
115
0
    pub fn new_2d(rows: usize, cols: usize) -> Self {
116
0
        Self::new([rows, cols, 1, 1])
117
0
    }
118
119
    /// Create a 3D TensorView.
120
0
    pub fn new_3d(d0: usize, d1: usize, d2: usize) -> Self {
121
0
        Self::new([d0, d1, d2, 1])
122
0
    }
123
124
    /// Create a 4D TensorView.
125
0
    pub fn new_4d(d0: usize, d1: usize, d2: usize, d3: usize) -> Self {
126
0
        Self::new([d0, d1, d2, d3])
127
0
    }
128
129
    /// Get the shape of the tensor.
130
0
    pub fn shape(&self) -> &[usize; 4] {
131
0
        &self.shape
132
0
    }
133
134
    /// Get the strides of the tensor.
135
0
    pub fn strides(&self) -> &[usize; 4] {
136
0
        &self.strides
137
0
    }
138
139
    /// Get the offset from the start of the buffer.
140
0
    pub fn offset(&self) -> usize {
141
0
        self.offset
142
0
    }
143
144
    /// Get the memory layout.
145
0
    pub fn layout(&self) -> MemoryLayout {
146
0
        self.layout
147
0
    }
148
149
    /// Get the number of active dimensions.
150
0
    pub fn ndim(&self) -> usize {
151
0
        self.ndim
152
0
    }
153
154
    /// Get the total number of elements.
155
0
    pub fn numel(&self) -> usize {
156
0
        self.shape.iter().product()
157
0
    }
158
159
    /// Check if the tensor is contiguous in memory.
160
    ///
161
    /// A tensor is contiguous if elements are stored without gaps
162
    /// in row-major order.
163
0
    pub fn is_contiguous(&self) -> bool {
164
0
        let expected_strides = Self::compute_row_major_strides(&self.shape);
165
0
        self.strides == expected_strides
166
0
    }
167
168
    /// Check if the tensor is empty (has zero elements).
169
0
    pub fn is_empty(&self) -> bool {
170
0
        self.numel() == 0
171
0
    }
172
173
    /// Get dimension size at the given index.
174
    ///
175
    /// # Panics
176
    ///
177
    /// Panics if `dim >= 4`.
178
0
    pub fn dim(&self, dim: usize) -> usize {
179
0
        self.shape[dim]
180
0
    }
181
182
    /// Get stride at the given dimension.
183
    ///
184
    /// # Panics
185
    ///
186
    /// Panics if `dim >= 4`.
187
0
    pub fn stride(&self, dim: usize) -> usize {
188
0
        self.strides[dim]
189
0
    }
190
191
    /// Create a slice of this tensor along the first dimension.
192
    ///
193
    /// # Arguments
194
    ///
195
    /// * `range` - Range of indices to include
196
    ///
197
    /// # Returns
198
    ///
199
    /// A new TensorView representing the slice.
200
    ///
201
    /// # cuda-tile-behavior.md References
202
    ///
203
    /// - Falsification test #32: Slicing produces correct views
204
0
    pub fn slice(&self, range: Range<usize>) -> Self {
205
0
        assert!(range.end <= self.shape[0], "Slice range out of bounds");
206
0
        let new_offset = self.offset + range.start * self.strides[0];
207
0
        let mut new_shape = self.shape;
208
0
        new_shape[0] = range.end - range.start;
209
210
0
        Self {
211
0
            shape: new_shape,
212
0
            strides: self.strides,
213
0
            offset: new_offset,
214
0
            layout: self.layout,
215
0
            ndim: self.ndim,
216
0
            _marker: PhantomData,
217
0
        }
218
0
    }
219
220
    /// Create a slice along a specific dimension.
221
    ///
222
    /// # Arguments
223
    ///
224
    /// * `dim` - Dimension to slice along
225
    /// * `range` - Range of indices to include
226
0
    pub fn slice_dim(&self, dim: usize, range: Range<usize>) -> Self {
227
0
        assert!(dim < 4, "Dimension out of bounds");
228
0
        assert!(range.end <= self.shape[dim], "Slice range out of bounds");
229
230
0
        let new_offset = self.offset + range.start * self.strides[dim];
231
0
        let mut new_shape = self.shape;
232
0
        new_shape[dim] = range.end - range.start;
233
234
0
        Self {
235
0
            shape: new_shape,
236
0
            strides: self.strides,
237
0
            offset: new_offset,
238
0
            layout: self.layout,
239
0
            ndim: self.ndim,
240
0
            _marker: PhantomData,
241
0
        }
242
0
    }
243
244
    /// Transpose the tensor by swapping two dimensions.
245
    ///
246
    /// # Arguments
247
    ///
248
    /// * `dim0` - First dimension to swap
249
    /// * `dim1` - Second dimension to swap
250
    ///
251
    /// # Returns
252
    ///
253
    /// A new TensorView with swapped dimensions.
254
    ///
255
    /// # cuda-tile-behavior.md References
256
    ///
257
    /// - Falsification test #33: Transpose swaps dimensions correctly
258
0
    pub fn transpose(&self, dim0: usize, dim1: usize) -> Self {
259
0
        assert!(dim0 < 4 && dim1 < 4, "Dimension out of bounds");
260
261
0
        let mut new_shape = self.shape;
262
0
        let mut new_strides = self.strides;
263
0
        new_shape.swap(dim0, dim1);
264
0
        new_strides.swap(dim0, dim1);
265
266
0
        Self {
267
0
            shape: new_shape,
268
0
            strides: new_strides,
269
0
            offset: self.offset,
270
0
            layout: self.layout,
271
0
            ndim: self.ndim,
272
0
            _marker: PhantomData,
273
0
        }
274
0
    }
275
276
    /// Reshape the tensor to a new shape.
277
    ///
278
    /// # Arguments
279
    ///
280
    /// * `new_shape` - New shape (must have same number of elements)
281
    ///
282
    /// # Returns
283
    ///
284
    /// A new TensorView with the new shape, or None if reshape is invalid.
285
0
    pub fn reshape(&self, new_shape: [usize; 4]) -> Option<Self> {
286
0
        let new_numel: usize = new_shape.iter().product();
287
0
        if new_numel != self.numel() {
288
0
            return None;
289
0
        }
290
291
        // Reshape requires contiguous memory
292
0
        if !self.is_contiguous() {
293
0
            return None;
294
0
        }
295
296
0
        Some(Self::new(new_shape))
297
0
    }
298
299
    /// Squeeze dimensions of size 1.
300
    ///
301
    /// Returns a view with all size-1 dimensions removed.
302
0
    pub fn squeeze(&self) -> Self {
303
0
        let mut new_shape = [1usize; 4];
304
0
        let mut new_strides = [1usize; 4];
305
0
        let mut new_ndim = 0;
306
307
0
        for i in 0..4 {
308
0
            if self.shape[i] > 1 {
309
0
                new_shape[new_ndim] = self.shape[i];
310
0
                new_strides[new_ndim] = self.strides[i];
311
0
                new_ndim += 1;
312
0
            }
313
        }
314
315
        // If all dimensions were 1, keep at least one
316
0
        if new_ndim == 0 {
317
0
            new_ndim = 1;
318
0
        }
319
320
0
        Self {
321
0
            shape: new_shape,
322
0
            strides: new_strides,
323
0
            offset: self.offset,
324
0
            layout: self.layout,
325
0
            ndim: new_ndim,
326
0
            _marker: PhantomData,
327
0
        }
328
0
    }
329
330
    /// Unsqueeze: add a dimension of size 1 at the specified position.
331
    ///
332
    /// # Arguments
333
    ///
334
    /// * `dim` - Position to insert the new dimension
335
0
    pub fn unsqueeze(&self, dim: usize) -> Option<Self> {
336
0
        if dim > self.ndim || self.ndim >= 4 {
337
0
            return None;
338
0
        }
339
340
0
        let mut new_shape = [1usize; 4];
341
0
        let mut new_strides = [1usize; 4];
342
343
        // Copy dimensions before the insertion point
344
        // Using manual loop since we're copying from two separate arrays to two separate arrays
345
        #[allow(clippy::manual_memcpy)]
346
0
        for i in 0..dim {
347
0
            new_shape[i] = self.shape[i];
348
0
            new_strides[i] = self.strides[i];
349
0
        }
350
351
        // Insert the new dimension
352
0
        new_shape[dim] = 1;
353
0
        new_strides[dim] = if dim < self.ndim {
354
0
            self.strides[dim] * self.shape[dim]
355
        } else {
356
0
            1
357
        };
358
359
        // Copy remaining dimensions (offset by 1 for insertion)
360
        #[allow(clippy::manual_memcpy)]
361
0
        for i in dim..self.ndim {
362
0
            new_shape[i + 1] = self.shape[i];
363
0
            new_strides[i + 1] = self.strides[i];
364
0
        }
365
366
0
        Some(Self {
367
0
            shape: new_shape,
368
0
            strides: new_strides,
369
0
            offset: self.offset,
370
0
            layout: self.layout,
371
0
            ndim: self.ndim + 1,
372
0
            _marker: PhantomData,
373
0
        })
374
0
    }
375
376
    /// Set the memory layout hint.
377
0
    pub fn with_layout(mut self, layout: MemoryLayout) -> Self {
378
0
        self.layout = layout;
379
0
        self
380
0
    }
381
382
    /// Compute linear index from multi-dimensional indices.
383
    ///
384
    /// # Arguments
385
    ///
386
    /// * `indices` - Array of indices for each dimension
387
    ///
388
    /// # Returns
389
    ///
390
    /// Linear offset into the underlying buffer.
391
0
    pub fn linear_index(&self, indices: [usize; 4]) -> usize {
392
0
        self.offset
393
0
            + indices[0] * self.strides[0]
394
0
            + indices[1] * self.strides[1]
395
0
            + indices[2] * self.strides[2]
396
0
            + indices[3] * self.strides[3]
397
0
    }
398
399
    /// Compute row-major strides for a given shape.
400
0
    fn compute_row_major_strides(shape: &[usize; 4]) -> [usize; 4] {
401
0
        let mut strides = [1usize; 4];
402
        // Strides: s[i] = product of shape[i+1..4]
403
0
        strides[3] = 1;
404
0
        strides[2] = shape[3];
405
0
        strides[1] = shape[3] * shape[2];
406
0
        strides[0] = shape[3] * shape[2] * shape[1];
407
0
        strides
408
0
    }
409
410
    /// Compute the number of active dimensions.
411
0
    fn compute_ndim(shape: &[usize; 4]) -> usize {
412
        // Count from the end: find last dimension > 1
413
0
        for i in (0..4).rev() {
414
0
            if shape[i] > 1 {
415
0
                return i + 1;
416
0
            }
417
        }
418
0
        1 // At least 1 dimension
419
0
    }
420
}
421
422
impl<T> Clone for TensorView<T> {
423
0
    fn clone(&self) -> Self {
424
0
        Self {
425
0
            shape: self.shape,
426
0
            strides: self.strides,
427
0
            offset: self.offset,
428
0
            layout: self.layout,
429
0
            ndim: self.ndim,
430
0
            _marker: PhantomData,
431
0
        }
432
0
    }
433
}
434
435
impl<T> Default for TensorView<T> {
436
0
    fn default() -> Self {
437
0
        Self::new([1, 1, 1, 1])
438
0
    }
439
}
440
441
#[cfg(test)]
442
mod tests {
443
    use super::*;
444
445
    // cuda-tile-behavior.md: Falsification test #31
446
    #[test]
447
    fn test_tensor_view_creation() {
448
        let view = TensorView::<f32>::new([2, 3, 4, 5]);
449
        assert_eq!(view.shape(), &[2, 3, 4, 5]);
450
        assert_eq!(view.numel(), 120);
451
        assert_eq!(view.ndim(), 4);
452
        assert!(view.is_contiguous());
453
    }
454
455
    #[test]
456
    fn test_tensor_view_1d() {
457
        let view = TensorView::<f32>::new_1d(100);
458
        assert_eq!(view.shape(), &[100, 1, 1, 1]);
459
        assert_eq!(view.numel(), 100);
460
        assert_eq!(view.ndim(), 1);
461
    }
462
463
    #[test]
464
    fn test_tensor_view_2d() {
465
        let view = TensorView::<f32>::new_2d(10, 20);
466
        assert_eq!(view.shape(), &[10, 20, 1, 1]);
467
        assert_eq!(view.numel(), 200);
468
        assert_eq!(view.ndim(), 2);
469
    }
470
471
    #[test]
472
    fn test_tensor_view_strides() {
473
        let view = TensorView::<f32>::new([2, 3, 4, 5]);
474
        // Row-major: strides[i] = product of shape[i+1..]
475
        assert_eq!(view.strides(), &[60, 20, 5, 1]);
476
    }
477
478
    // cuda-tile-behavior.md: Falsification test #32
479
    #[test]
480
    fn test_tensor_view_slice() {
481
        let view = TensorView::<f32>::new([10, 20, 1, 1]);
482
        let sliced = view.slice(2..7);
483
484
        assert_eq!(sliced.shape(), &[5, 20, 1, 1]);
485
        assert_eq!(sliced.offset(), 40); // 2 * 20
486
        assert_eq!(sliced.numel(), 100);
487
    }
488
489
    #[test]
490
    fn test_tensor_view_slice_dim() {
491
        let view = TensorView::<f32>::new([10, 20, 30, 1]);
492
        let sliced = view.slice_dim(1, 5..15);
493
494
        assert_eq!(sliced.shape(), &[10, 10, 30, 1]);
495
        assert_eq!(sliced.offset(), 5 * 30); // offset by 5 in dim 1
496
    }
497
498
    // cuda-tile-behavior.md: Falsification test #33
499
    #[test]
500
    fn test_tensor_view_transpose() {
501
        let view = TensorView::<f32>::new([2, 3, 1, 1]);
502
        let transposed = view.transpose(0, 1);
503
504
        assert_eq!(transposed.shape(), &[3, 2, 1, 1]);
505
        assert_eq!(transposed.strides(), &[1, 3, 1, 1]); // Swapped strides
506
        assert!(!transposed.is_contiguous()); // Non-contiguous after transpose
507
    }
508
509
    #[test]
510
    fn test_tensor_view_reshape() {
511
        let view = TensorView::<f32>::new([2, 3, 4, 1]);
512
        let reshaped = view.reshape([6, 4, 1, 1]).unwrap();
513
514
        assert_eq!(reshaped.shape(), &[6, 4, 1, 1]);
515
        assert_eq!(reshaped.numel(), 24);
516
    }
517
518
    #[test]
519
    fn test_tensor_view_reshape_invalid() {
520
        let view = TensorView::<f32>::new([2, 3, 4, 1]);
521
        let result = view.reshape([5, 5, 1, 1]); // 25 != 24
522
        assert!(result.is_none());
523
    }
524
525
    #[test]
526
    fn test_tensor_view_squeeze() {
527
        let view = TensorView::<f32>::new([1, 3, 1, 4]);
528
        let squeezed = view.squeeze();
529
530
        assert_eq!(squeezed.shape()[0], 3);
531
        assert_eq!(squeezed.shape()[1], 4);
532
        assert_eq!(squeezed.ndim(), 2);
533
    }
534
535
    #[test]
536
    fn test_tensor_view_unsqueeze() {
537
        let view = TensorView::<f32>::new_2d(3, 4);
538
        let unsqueezed = view.unsqueeze(0).unwrap();
539
540
        assert_eq!(unsqueezed.shape(), &[1, 3, 4, 1]);
541
        assert_eq!(unsqueezed.ndim(), 3);
542
    }
543
544
    #[test]
545
    fn test_tensor_view_linear_index() {
546
        let view = TensorView::<f32>::new([2, 3, 4, 1]);
547
548
        // First element
549
        assert_eq!(view.linear_index([0, 0, 0, 0]), 0);
550
551
        // Element at [1, 0, 0, 0]
552
        assert_eq!(view.linear_index([1, 0, 0, 0]), 12); // 1 * 12
553
554
        // Element at [0, 1, 0, 0]
555
        assert_eq!(view.linear_index([0, 1, 0, 0]), 4); // 1 * 4
556
557
        // Element at [1, 2, 3, 0]
558
        assert_eq!(view.linear_index([1, 2, 3, 0]), 12 + 8 + 3); // 23
559
    }
560
561
    #[test]
562
    fn test_tensor_view_is_empty() {
563
        let empty = TensorView::<f32>::new([0, 1, 1, 1]);
564
        assert!(empty.is_empty());
565
566
        let non_empty = TensorView::<f32>::new([1, 1, 1, 1]);
567
        assert!(!non_empty.is_empty());
568
    }
569
570
    #[test]
571
    fn test_tensor_view_with_strides() {
572
        let view = TensorView::<f32>::with_strides([2, 3, 1, 1], [6, 2, 1, 1]);
573
        assert_eq!(view.strides(), &[6, 2, 1, 1]);
574
        assert!(!view.is_contiguous()); // Custom strides
575
    }
576
577
    #[test]
578
    fn test_tensor_view_default() {
579
        let view = TensorView::<f32>::default();
580
        assert_eq!(view.numel(), 1);
581
        assert_eq!(view.ndim(), 1);
582
    }
583
584
    #[test]
585
    fn test_memory_layout() {
586
        let view = TensorView::<f32>::new([4, 4, 1, 1])
587
            .with_layout(MemoryLayout::Tiled { tile_size: [2, 2] });
588
589
        assert!(matches!(
590
            view.layout(),
591
            MemoryLayout::Tiled { tile_size: [2, 2] }
592
        ));
593
    }
594
595
    #[test]
596
    fn test_tensor_view_clone() {
597
        let view = TensorView::<f32>::new([2, 3, 4, 5]);
598
        let cloned = view.clone();
599
600
        assert_eq!(view.shape(), cloned.shape());
601
        assert_eq!(view.strides(), cloned.strides());
602
    }
603
604
    // cuda-tile-behavior.md: Falsification test #34 - Dimension accessors
605
    #[test]
606
    fn test_tensor_view_dim_accessors() {
607
        let view = TensorView::<f32>::new([2, 3, 4, 5]);
608
609
        assert_eq!(view.dim(0), 2);
610
        assert_eq!(view.dim(1), 3);
611
        assert_eq!(view.dim(2), 4);
612
        assert_eq!(view.dim(3), 5);
613
614
        assert_eq!(view.stride(0), 60);
615
        assert_eq!(view.stride(1), 20);
616
        assert_eq!(view.stride(2), 5);
617
        assert_eq!(view.stride(3), 1);
618
    }
619
620
    // cuda-tile-behavior.md: Falsification test #35 - Contiguity detection
621
    #[test]
622
    fn test_contiguity_after_operations() {
623
        let view = TensorView::<f32>::new([4, 4, 1, 1]);
624
        assert!(view.is_contiguous());
625
626
        // Slice preserves contiguity
627
        let sliced = view.slice(1..3);
628
        assert!(sliced.is_contiguous());
629
630
        // Transpose breaks contiguity
631
        let transposed = view.transpose(0, 1);
632
        assert!(!transposed.is_contiguous());
633
    }
634
}