/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 | | } |