Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cutile-rs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
and release tags are namespaced `cutile-rs/vX.Y.Z`. Crate names and versions
are unchanged.

### Fixed

- `TensorView::view` keeps the slice offset when it recomputes the
specialization, so a view over a slice that starts off a 16-byte boundary
no longer advertises the allocation's alignment. Before, the JIT emitted
vectorized loads for such views and launches failed with CUDA error 716
(misaligned address).

## [0.4.0] - 2026-09-25

Debug builds support cuda-gdb and Nsight. DGX Spark is now supported.
Expand Down
2 changes: 1 addition & 1 deletion cutile-rs/cutile/src/tensor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1479,7 +1479,7 @@ impl<'a, T: DType> TensorView<'a, T> {
let new_shape: Vec<i32> = shape.iter().map(|&x| x as i32).collect();
let new_strides = contiguous_strides(&new_shape);
let spec = compute_spec(
self.base.storage.spec_ptr(),
self.base.storage.spec_ptr() + self.offset_bytes as u64,
&new_shape,
&new_strides,
size_of::<T>() as i32,
Expand Down
50 changes: 50 additions & 0 deletions cutile-rs/cutile/tests/tensor_views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ mod tensor_views_module {
let tile: Tile<f32, { [2, 2] }> = load_tile_like(input, output);
output.store(tile);
}

/// Copies a 32x32 tile. Tiles this wide get vectorized loads when the
/// specialization claims a 16-byte-aligned base, so a wrong
/// `base_ptr_div` on an offset view faults instead of silently working.
#[cutile::entry()]
fn copy_32x32(output: &mut Tensor<f32, { [32, 32] }>, input: &Tensor<f32, { [-1, -1] }>) {
let tile: Tile<f32, { [32, 32] }> = load_tile_like(input, output);
output.store(tile);
}
}

#[test]
Expand Down Expand Up @@ -267,6 +276,47 @@ fn slice_chained() {
});
}

#[test]
fn view_of_offset_slice_keeps_measured_alignment() {
// Slicing one f32 in moves the data pointer 4 bytes off the 16-aligned
// allocation. Re-viewing that slice must keep the slice's alignment, not
// revert to the allocation's.
common::with_test_stack(|| {
let tensor = api::arange::<f32>(64).sync().expect("Failed.");
let sliced = tensor.slice(&[1..33]).expect("Failed.");
let viewed = sliced.view(&[4, 8]).expect("Failed.");

assert_eq!(sliced.spec().base_ptr_div.divisor, 4);
assert_eq!(
viewed.spec().base_ptr_div,
sliced.spec().base_ptr_div,
"view of an offset slice must keep the slice's base_ptr_div"
);
});
}

#[test]
fn view_of_offset_slice_launches_with_wide_tiles() {
// arange(128*128 + 16), slice one element in, view as [128, 128], copy
// with 32x32 tiles. The view's data pointer is 4 bytes past a 16-byte
// boundary, so a spec that claims 16-byte alignment makes the JIT emit
// vectorized loads and the launch fails with a misaligned address.
common::with_test_stack(|| {
let n = 128 * 128;
let tensor = api::arange::<f32>(n + 16).sync().expect("Failed.");
let sliced = tensor.slice(&[1..1 + n]).expect("Failed.");
let viewed = sliced.view(&[128, 128]).expect("Failed.");

let output = api::zeros::<f32>(&[128, 128]).sync().expect("Failed.");
let (result, _) = tensor_views_module::copy_32x32(output.partition([32, 32]), &viewed)
.sync()
.expect("copy of a view over an offset slice failed");
let host: Vec<f32> = result.unpartition().to_host_vec().sync().expect("Failed.");
let want: Vec<f32> = (1..1 + n).map(|v| v as f32).collect();
assert_eq!(host, want);
});
}

#[test]
#[allow(clippy::reversed_empty_ranges)] // `5..3` is the rejected input under test
fn slice_out_of_bounds_rejected() {
Expand Down