use crate::api::{copy_device_to_host_vec, copy_host_vec_to_device};
use crate::error::{tensor_error_result, Error};
use crate::tile_kernel::UnwrapPartition;
use anyhow::Result;
use cuda_async::device_buffer::{DeviceAllocation, DeviceBuffer, DevicePointer};
use cuda_async::device_operation;
use cuda_async::device_operation::{value, DeviceOp, IntoDeviceOp, Value};
use cuda_core::sys::CUdeviceptr;
use cuda_core::{DType, DTypeId};
use std::fmt::Debug;
use std::marker::PhantomData;
use std::mem::{align_of, size_of, MaybeUninit};
use std::ops::Index;
use std::sync::Arc;
pub struct Partition<T> {
pub(crate) object: T,
pub partition_shape: Vec<usize>,
pub partition_strides: Vec<usize>,
pub(crate) prefix_coverage: bool,
}
impl<T> Partition<T> {
pub fn prefix(mut self) -> Self {
self.prefix_coverage = true;
self
}
pub fn unpartition(self) -> T {
self.object
}
pub fn map<const RANK: usize>(
self,
map_shape: [usize; RANK],
num_tile_blocks: u32,
) -> MappedLaunchPartition<Self> {
assert!(
!self.prefix_coverage,
"a partial-coverage (prefix) partition cannot be mapped: a mapped \
schedule covers its full index space by construction"
);
MappedLaunchPartition {
partition: self,
map_shape: map_shape.to_vec(),
num_tile_blocks,
}
}
}
pub const OWNED: usize = 0;
pub struct MappedLaunchPartition<P> {
pub(crate) partition: P,
pub(crate) map_shape: Vec<usize>,
pub(crate) num_tile_blocks: u32,
}
impl<P> MappedLaunchPartition<P> {
fn validate(
&self,
partition_grid: (u32, u32, u32),
num_tile_blocks: u32,
) -> Result<(u32, u32, u32), Error> {
let map_rank = self.map_shape.len();
if map_rank == 0 || map_rank > 3 {
return tensor_error_result(
"mapped partitions require a rank-1 through rank-3 map shape.",
);
}
if self.map_shape.iter().all(|&dim| dim == OWNED) {
return tensor_error_result(
"mapped partition requires at least one streamed (non-OWNED) map axis.",
);
}
let grid_axes = [partition_grid.0, partition_grid.1, partition_grid.2];
if grid_axes.iter().take(map_rank).any(|&axis| axis == 0) {
return tensor_error_result(
"mapped partition requires a non-empty logical partition grid.",
);
}
if grid_axes.iter().skip(map_rank).any(|&axis| axis != 1) {
return tensor_error_result(
"mapped partition map rank must match the logical partition grid rank.",
);
}
let streamed_tiles = grid_axes
.iter()
.zip(self.map_shape.iter())
.filter(|&(_, &map_dim)| map_dim != OWNED)
.map(|(&axis, _)| axis)
.try_fold(1u32, |total, axis| total.checked_mul(axis))
.ok_or_else(|| {
crate::error::tensor_error("mapped partition logical grid is too large")
})?;
if num_tile_blocks == 0 {
return tensor_error_result("mapped partition requires num_tile_blocks > 0.");
}
if num_tile_blocks > streamed_tiles {
return tensor_error_result(
"mapped partition num_tile_blocks cannot exceed the streamed logical tile count.",
);
}
Ok((num_tile_blocks, 1, 1))
}
}
impl<T: DType> Partition<Tensor<T>> {
pub fn num_bytes(&self) -> usize {
self.object.size() * size_of::<T>()
}
pub fn num_mb(&self) -> usize {
self.num_bytes() / 10usize.pow(6)
}
pub fn num_gb(&self) -> usize {
self.num_bytes() / 10usize.pow(9)
}
pub fn dtype(&self) -> DTypeId {
T::DTYPE
}
pub fn dtype_str(&self) -> &'static str {
T::DTYPE.as_str()
}
pub fn grid(&self) -> Result<(u32, u32, u32), Error> {
if !self.object.shape.iter().all(|&x| x > 0) {
return tensor_error_result("Shape dimensions must be positive.");
}
let shape: Vec<u32> = self.object.shape.iter().map(|&x| x as u32).collect();
let partition_shape: Vec<u32> = self.partition_shape.iter().map(|&x| x as u32).collect();
let rank = shape.len();
match rank {
1 => Ok((u32::div_ceil(shape[0], partition_shape[0]), 1, 1)),
2 => Ok((
u32::div_ceil(shape[0], partition_shape[0]),
u32::div_ceil(shape[1], partition_shape[1]),
1,
)),
3 => Ok((
u32::div_ceil(shape[0], partition_shape[0]),
u32::div_ceil(shape[1], partition_shape[1]),
u32::div_ceil(shape[2], partition_shape[2]),
)),
_ => tensor_error_result("Mutable tensor must be at most rank 3."),
}
}
}
impl<T> From<Partition<T>> for Arc<T> {
fn from(val: Partition<T>) -> Self {
Arc::new(val.unpartition())
}
}
pub trait IntoPartition {
fn partition<const RANK: usize>(self, partition_shape: [usize; RANK]) -> Partition<Self>
where
Self: Sized;
fn partition_prefix<const RANK: usize>(self, partition_shape: [usize; RANK]) -> Partition<Self>
where
Self: Sized,
{
self.partition(partition_shape).prefix()
}
}
pub trait IntoPartitionArc {
fn partition<const RANK: usize>(
self: Arc<Self>,
partition_shape: [usize; RANK],
) -> Partition<Self>
where
Self: Sized;
}
pub use cutile_compiler::specialization::{compute_spec, SpecializationBits};
#[derive(Debug)]
pub(crate) enum Storage {
Device(DeviceBuffer),
Meta { len_bytes: usize, device_id: usize },
}
impl Storage {
const META_SPEC_PTR: u64 = 16;
fn len_bytes(&self) -> usize {
match self {
Storage::Device(b) => b.len_bytes(),
Storage::Meta { len_bytes, .. } => *len_bytes,
}
}
fn device_id(&self) -> usize {
match self {
Storage::Device(b) => b.device_id(),
Storage::Meta { device_id, .. } => *device_id,
}
}
fn cu_deviceptr(&self) -> CUdeviceptr {
match self {
Storage::Device(b) => b.cu_deviceptr(),
Storage::Meta { .. } => panic!(
"cutile: read of a meta tensor's device pointer. Meta tensors \
(api::meta) carry only shape/stride metadata for warmup via \
`.compile()`; they have no device memory and cannot be launched \
or copied. Use a real tensor (api::zeros/ones/...) on `.sync()` \
/ `.await` paths."
),
}
}
fn spec_ptr(&self) -> CUdeviceptr {
match self {
Storage::Device(b) => b.cu_deviceptr(),
Storage::Meta { .. } => Storage::META_SPEC_PTR,
}
}
}
#[derive(Debug)]
pub struct Tensor<T: DType> {
pub(crate) storage: Arc<Storage>,
pub(crate) shape: Vec<i32>,
pub(crate) strides: Vec<i32>,
pub(crate) spec: SpecializationBits,
_dtype: PhantomData<T>,
}
fn contiguous_strides(shape: &[i32]) -> Vec<i32> {
let mut stride = 1;
let mut strides = Vec::with_capacity(shape.len());
for dim in shape.iter().rev() {
strides.push(stride);
stride *= *dim;
}
strides.reverse();
strides
}
fn checked_num_elements(shape: &[usize]) -> Result<usize, Error> {
shape.iter().try_fold(1usize, |acc, dim| {
acc.checked_mul(*dim)
.ok_or_else(|| crate::error::tensor_error("Tensor shape overflowed usize."))
})
}
fn checked_num_bytes<T>(shape: &[usize]) -> Result<usize, Error> {
checked_num_elements(shape)?
.checked_mul(size_of::<T>())
.ok_or_else(|| crate::error::tensor_error("Tensor byte size overflowed usize."))
}
fn checked_num_elements_i32(shape: &[i32]) -> Result<usize, Error> {
shape.iter().try_fold(1usize, |acc, dim| {
let dim = usize::try_from(*dim)
.map_err(|_| crate::error::tensor_error("Tensor shape contains negative dimension."))?;
acc.checked_mul(dim)
.ok_or_else(|| crate::error::tensor_error("Tensor shape overflowed usize."))
})
}
fn checked_num_bytes_i32<T>(shape: &[i32]) -> Result<usize, Error> {
checked_num_elements_i32(shape)?
.checked_mul(size_of::<T>())
.ok_or_else(|| crate::error::tensor_error("Tensor byte size overflowed usize."))
}
fn addressable_bytes<T>(shape: &[i32], strides: &[i32]) -> usize {
assert_eq!(
shape.len(),
strides.len(),
"Tensor shape/stride rank mismatch."
);
if shape.iter().any(|&d| d <= 0) {
return 0;
}
let mut max_elem_offset: usize = 0;
for (&dim, &stride) in shape.iter().zip(strides) {
assert!(
stride >= 0,
"from_foreign requires non-negative strides; use borrow_raw_parts for negative-stride views."
);
max_elem_offset = (dim as usize - 1)
.checked_mul(stride as usize)
.and_then(|off| max_elem_offset.checked_add(off))
.expect("Tensor addressable extent overflowed usize.");
}
max_elem_offset
.checked_add(1)
.and_then(|elems| elems.checked_mul(size_of::<T>()))
.expect("Tensor addressable extent overflowed usize.")
}
impl<T: DType> Tensor<T> {
fn assert_valid_metadata(shape: &[i32], strides: &[i32], storage_num_bytes: usize) {
assert_eq!(
shape.len(),
strides.len(),
"Tensor shape/stride rank mismatch."
);
let num_bytes = checked_num_bytes_i32::<T>(shape)
.expect("Tensor shape contains invalid dimensions or overflows.");
assert_eq!(
num_bytes, storage_num_bytes,
"Tensor logical byte size must match storage byte size."
);
}
pub(crate) fn from_device_buffer(
device_buffer: DeviceBuffer,
shape: Vec<i32>,
strides: Vec<i32>,
) -> Self {
Self::assert_valid_metadata(&shape, &strides, device_buffer.len_bytes());
let storage = Arc::new(Storage::Device(device_buffer));
let spec = compute_spec(
storage.cu_deviceptr(),
&shape,
&strides,
size_of::<T>() as i32,
);
Self {
storage,
shape,
strides,
spec,
_dtype: PhantomData,
}
}
pub unsafe fn from_raw_parts(
dptr: CUdeviceptr,
len_bytes: usize,
device_id: usize,
shape: Vec<i32>,
strides: Vec<i32>,
) -> Self {
Self::assert_valid_metadata(&shape, &strides, len_bytes);
Self::from_device_buffer(
DeviceBuffer::from_raw_parts(dptr, len_bytes, device_id),
shape,
strides,
)
}
pub unsafe fn borrow_raw_parts(
dptr: CUdeviceptr,
device_id: usize,
shape: Vec<i32>,
strides: Vec<i32>,
) -> Self {
let len_bytes = checked_num_bytes_i32::<T>(&shape)
.expect("Tensor shape contains invalid dimensions or overflows.");
Self::from_device_buffer(
DeviceBuffer::borrowed_from_raw_parts(dptr, len_bytes, device_id),
shape,
strides,
)
}
pub unsafe fn from_foreign(
owner: Arc<dyn DeviceAllocation>,
shape: Vec<i32>,
strides: Vec<i32>,
) -> Self {
let len_bytes = checked_num_bytes_i32::<T>(&shape)
.expect("Tensor shape contains invalid dimensions or overflows.");
let extent_bytes = addressable_bytes::<T>(&shape, &strides);
assert!(
owner.len_bytes() >= extent_bytes,
"foreign allocation ({} bytes) is smaller than the tensor's addressable extent \
({extent_bytes} bytes for the given shape/strides)",
owner.len_bytes(),
);
Self::from_device_buffer(DeviceBuffer::foreign(owner, len_bytes), shape, strides)
}
pub(crate) fn from_meta(shape: Vec<i32>, device_id: usize) -> Self {
let strides = contiguous_strides(&shape);
let len_bytes = checked_num_bytes_i32::<T>(&shape)
.expect("Tensor shape contains invalid dimensions or overflows.");
Self::assert_valid_metadata(&shape, &strides, len_bytes);
let spec = compute_spec(
Storage::META_SPEC_PTR,
&shape,
&strides,
size_of::<T>() as i32,
);
Self {
storage: Arc::new(Storage::Meta {
len_bytes,
device_id,
}),
shape,
strides,
spec,
_dtype: PhantomData,
}
}
fn storage_num_bytes(&self) -> usize {
self.storage.len_bytes()
}
fn num_elements(&self) -> usize {
checked_num_elements_i32(&self.shape)
.expect("Tensor shape contains invalid dimensions or overflows.")
}
fn typed_num_bytes(&self) -> usize {
checked_num_bytes_i32::<T>(&self.shape)
.expect("Tensor shape contains invalid dimensions or overflows.")
}
fn validate_view_shape(&self, shape: &[usize]) -> Result<(), Error> {
if !self.is_contiguous() {
return tensor_error_result("Zero-copy tensor views require contiguous storage.");
}
let target_num_bytes = checked_num_bytes::<T>(shape)?;
if target_num_bytes != self.typed_num_bytes() {
return tensor_error_result("View shape must preserve tensor size.");
}
Ok(())
}
fn validate_reinterpret_shape<U: DType>(&self, shape: &[usize]) -> Result<(), Error> {
if !self.is_contiguous() {
return tensor_error_result("Zero-copy reinterpret requires contiguous storage.");
}
let target_num_bytes = checked_num_bytes::<U>(shape)?;
if target_num_bytes != self.typed_num_bytes() {
return tensor_error_result("Reinterpret shape must preserve total byte size.");
}
let alignment = align_of::<U>() as u64;
if alignment > 1 && self.storage.spec_ptr() % alignment != 0 {
return tensor_error_result(
"Tensor storage alignment is incompatible with reinterpret target type.",
);
}
Ok(())
}
fn assert_unique_storage(&self) {
assert!(
Arc::strong_count(&self.storage) == 1,
"Cannot create mutable partition from shared tensor storage."
);
}
pub fn uninitialized(len: usize) -> impl DeviceOp<Output = MaybeUninit<Self>> {
assert!(len > 0, "Non-zero length required.");
device_operation::with_context(move |ctx| {
let num_bytes = len * size_of::<T>();
value(MaybeUninit::new(unsafe {
Self::from_raw_parts(
ctx.alloc_async(num_bytes),
num_bytes,
ctx.get_device_id(),
vec![len as i32],
vec![1],
)
}))
})
}
pub fn dtype(&self) -> DTypeId {
T::DTYPE
}
pub(crate) fn cu_deviceptr(&self) -> CUdeviceptr {
self.storage.cu_deviceptr()
}
pub fn device_id(&self) -> usize {
self.storage.device_id()
}
pub fn device_pointer(&self) -> DevicePointer<T> {
unsafe { DevicePointer::from_cu_deviceptr(self.cu_deviceptr()) }
}
pub fn shape(&self) -> &[i32] {
&self.shape
}
pub fn strides(&self) -> &[i32] {
&self.strides
}
pub fn spec(&self) -> &SpecializationBits {
&self.spec
}
pub fn size(&self) -> usize {
debug_assert_eq!(self.typed_num_bytes(), self.storage_num_bytes());
self.num_elements()
}
pub fn dup(&self) -> impl DeviceOp<Output = Self> {
crate::api::dup(self)
}
pub fn num_bytes(&self) -> usize {
self.typed_num_bytes()
}
pub fn is_contiguous(&self) -> bool {
self.strides == contiguous_strides(&self.shape)
}
pub unsafe fn into_shared_alias(&self) -> Arc<Self> {
Arc::new(Self {
storage: self.storage.clone(),
shape: self.shape.clone(),
strides: self.strides.clone(),
spec: self.spec.clone(),
_dtype: PhantomData,
})
}
pub(crate) fn reshape_unchecked(mut self, shape: &[usize]) -> Self {
let shape: Vec<i32> = shape.iter().map(|&x| x as i32).collect();
self.strides = contiguous_strides(&shape);
self.spec = compute_spec(
self.storage.spec_ptr(),
&shape,
&self.strides,
size_of::<T>() as i32,
);
self.shape = shape;
self
}
pub(crate) fn reshape_shared(self: &Arc<Self>, shape: &[usize]) -> Result<Arc<Self>, Error> {
self.validate_view_shape(shape)?;
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.storage.spec_ptr(),
&new_shape,
&new_strides,
size_of::<T>() as i32,
);
Ok(Arc::new(Self {
storage: self.storage.clone(),
strides: new_strides,
shape: new_shape,
spec,
_dtype: PhantomData,
}))
}
pub fn reinterpret<U: DType>(
self: &Arc<Self>,
shape: &[usize],
) -> Result<Arc<Tensor<U>>, Error> {
self.validate_reinterpret_shape::<U>(shape)?;
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.storage.spec_ptr(),
&new_shape,
&new_strides,
size_of::<U>() as i32,
);
Ok(Arc::new(Tensor::<U> {
storage: self.storage.clone(),
strides: new_strides,
shape: new_shape,
spec,
_dtype: PhantomData,
}))
}
}
pub trait ToHostVec<T: Send> {
fn to_host_vec(self) -> impl DeviceOp<Output = Vec<T>>;
}
impl<T: DType> ToHostVec<T> for Tensor<T> {
fn to_host_vec(self) -> impl DeviceOp<Output = Vec<T>> {
let arc_self = Arc::new(self);
copy_device_to_host_vec(&arc_self)
}
}
impl<T: DType> ToHostVec<T> for Arc<Tensor<T>> {
fn to_host_vec(self) -> impl DeviceOp<Output = Vec<T>> {
copy_device_to_host_vec(&self)
}
}
impl<T: DType> ToHostVec<T> for &Arc<Tensor<T>> {
fn to_host_vec(self) -> impl DeviceOp<Output = Vec<T>> {
copy_device_to_host_vec(self)
}
}
pub trait Reshape {
type Output;
fn reshape(self, shape: &[usize]) -> Result<Self::Output, Error>;
}
impl<T: DType> Reshape for Tensor<T> {
type Output = Tensor<T>;
fn reshape(self, shape: &[usize]) -> Result<Tensor<T>, Error> {
let current_elems: i32 = self.shape.iter().product();
let new_elems: i32 = shape.iter().map(|&x| x as i32).product();
if new_elems != current_elems {
return tensor_error_result("reshape: new shape must preserve element count.");
}
Ok(self.reshape_unchecked(shape))
}
}
impl<'a, T: DType> Reshape for &'a Arc<Tensor<T>> {
type Output = Arc<Tensor<T>>;
fn reshape(self, shape: &[usize]) -> Result<Arc<Tensor<T>>, Error> {
self.reshape_shared(shape)
}
}
pub struct TensorView<'a, T: DType> {
base: &'a Tensor<T>,
offset_bytes: usize,
shape: Vec<i32>,
strides: Vec<i32>,
spec: SpecializationBits,
}
impl<'a, T: DType> TensorView<'a, T> {
pub fn shape(&self) -> &[i32] {
&self.shape
}
pub fn strides(&self) -> &[i32] {
&self.strides
}
pub fn spec(&self) -> &SpecializationBits {
&self.spec
}
pub fn size(&self) -> usize {
self.shape.iter().map(|&x| x as usize).product()
}
pub fn view(&self, shape: &[usize]) -> Result<TensorView<'_, T>, Error> {
if self.strides != contiguous_strides(&self.shape) {
return tensor_error_result("view: cannot reshape a non-contiguous view.");
}
let current_elems: i32 = self.shape.iter().product();
let new_elems: i32 = shape.iter().map(|&x| x as i32).product();
if new_elems != current_elems {
return tensor_error_result("view: new shape must preserve element count.");
}
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(),
&new_shape,
&new_strides,
size_of::<T>() as i32,
);
Ok(TensorView {
base: self.base,
offset_bytes: self.offset_bytes,
shape: new_shape,
strides: new_strides,
spec,
})
}
pub fn slice(&self, ranges: &[std::ops::Range<usize>]) -> Result<TensorView<'_, T>, Error> {
if ranges.len() > self.shape.len() {
return tensor_error_result("slice: more ranges than axes.");
}
let mut offset_elems: usize = 0;
let mut new_shape = self.shape.clone();
for (axis, range) in ranges.iter().enumerate() {
let dim = self.shape[axis] as usize;
if range.start > range.end || range.end > dim {
return tensor_error_result("slice: range out of bounds.");
}
offset_elems += range.start * self.strides[axis] as usize;
new_shape[axis] = (range.end - range.start) as i32;
}
let new_strides = self.strides.clone();
let spec = compute_spec(
self.base.storage.spec_ptr()
+ (self.offset_bytes + offset_elems * size_of::<T>()) as u64,
&new_shape,
&new_strides,
size_of::<T>() as i32,
);
Ok(TensorView {
base: self.base,
offset_bytes: self.offset_bytes + offset_elems * size_of::<T>(),
shape: new_shape,
strides: new_strides,
spec,
})
}
}
impl<T: DType> Tensor<T> {
pub fn view(&self, shape: &[usize]) -> Result<TensorView<'_, T>, Error> {
let current_elems: i32 = self.shape.iter().product();
let new_elems: i32 = shape.iter().map(|&x| x as i32).product();
if new_elems != current_elems {
return tensor_error_result("view: new shape must preserve element count.");
}
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.storage.spec_ptr(),
&new_shape,
&new_strides,
size_of::<T>() as i32,
);
Ok(TensorView {
base: self,
offset_bytes: 0,
shape: new_shape,
strides: new_strides,
spec,
})
}
pub fn slice(&self, ranges: &[std::ops::Range<usize>]) -> Result<TensorView<'_, T>, Error> {
if ranges.len() > self.shape.len() {
return tensor_error_result("slice: more ranges than axes.");
}
let mut offset_elems: usize = 0;
let mut new_shape = self.shape.clone();
for (axis, range) in ranges.iter().enumerate() {
let dim = self.shape[axis] as usize;
if range.start > range.end || range.end > dim {
return tensor_error_result("slice: range out of bounds.");
}
offset_elems += range.start * self.strides[axis] as usize;
new_shape[axis] = (range.end - range.start) as i32;
}
let new_strides = self.strides.clone();
let spec = compute_spec(
self.storage.spec_ptr() + (offset_elems * size_of::<T>()) as u64,
&new_shape,
&new_strides,
size_of::<T>() as i32,
);
Ok(TensorView {
base: self,
offset_bytes: offset_elems * size_of::<T>(),
shape: new_shape,
strides: new_strides,
spec,
})
}
}
impl<T: DType> IntoPartitionArc for Tensor<T> {
fn partition<const RANK: usize>(
self: Arc<Tensor<T>>,
partition_shape: [usize; RANK],
) -> Partition<Tensor<T>> {
let partition_shape = partition_shape.to_vec();
let partition_strides: Vec<usize> = self.strides.iter().map(|&s| s as usize).collect();
let tensor = Arc::try_unwrap(self).expect("Failed to convert Arc to Partition.");
tensor.assert_unique_storage();
Partition::<Tensor<T>> {
object: tensor,
partition_shape,
partition_strides,
prefix_coverage: false,
}
}
}
impl<T: DType> IntoPartition for Tensor<T> {
fn partition<const RANK: usize>(self, partition_shape: [usize; RANK]) -> Partition<Tensor<T>> {
let partition_shape = partition_shape.to_vec();
let partition_strides: Vec<usize> = self.strides.iter().map(|&s| s as usize).collect();
self.assert_unique_storage();
Partition::<Tensor<T>> {
object: self,
partition_shape,
partition_strides,
prefix_coverage: false,
}
}
}
pub trait PartitionMut<'a, T: DType> {
fn partition<const RANK: usize>(
self,
partition_shape: [usize; RANK],
) -> Partition<&'a mut Tensor<T>>;
fn partition_prefix<const RANK: usize>(
self,
partition_shape: [usize; RANK],
) -> Partition<&'a mut Tensor<T>>
where
Self: Sized,
{
self.partition(partition_shape).prefix()
}
}
impl<'a, T: DType> PartitionMut<'a, T> for &'a mut Tensor<T> {
fn partition<const RANK: usize>(
self,
partition_shape: [usize; RANK],
) -> Partition<&'a mut Tensor<T>> {
let partition_shape = partition_shape.to_vec();
let partition_strides: Vec<usize> = self.strides.iter().map(|&s| s as usize).collect();
Partition {
object: self,
partition_shape,
partition_strides,
prefix_coverage: false,
}
}
}
impl<'a, T: DType> Partition<&'a mut Tensor<T>> {
pub fn dtype_str(&self) -> &'static str {
T::DTYPE.as_str()
}
pub fn grid(&self) -> Result<(u32, u32, u32), Error> {
if !self.object.shape.iter().all(|&x| x > 0) {
return tensor_error_result("Shape dimensions must be positive.");
}
let shape: Vec<u32> = self.object.shape.iter().map(|&x| x as u32).collect();
let partition_shape: Vec<u32> = self.partition_shape.iter().map(|&x| x as u32).collect();
let rank = shape.len();
match rank {
1 => Ok((u32::div_ceil(shape[0], partition_shape[0]), 1, 1)),
2 => Ok((
u32::div_ceil(shape[0], partition_shape[0]),
u32::div_ceil(shape[1], partition_shape[1]),
1,
)),
3 => Ok((
u32::div_ceil(shape[0], partition_shape[0]),
u32::div_ceil(shape[1], partition_shape[1]),
u32::div_ceil(shape[2], partition_shape[2]),
)),
_ => tensor_error_result("Mutable tensor must be at most rank 3."),
}
}
}
impl<'a, T: DType + Sync> IntoDeviceOp<Partition<&'a mut Tensor<T>>>
for Partition<&'a mut Tensor<T>>
{
type Op = Value<Partition<&'a mut Tensor<T>>>;
fn into_op(self) -> Value<Partition<&'a mut Tensor<T>>> {
value(self)
}
}
pub trait TryPartition<T: DType> {
fn try_partition<const RANK: usize>(
self,
partition_shape: [usize; RANK],
) -> Result<Partition<Tensor<T>>, Error>;
}
impl<T: DType> TryPartition<T> for Arc<Tensor<T>> {
fn try_partition<const RANK: usize>(
self,
partition_shape: [usize; RANK],
) -> Result<Partition<Tensor<T>>, Error> {
let tensor = Arc::try_unwrap(self).map_err(|_| {
crate::error::tensor_error("try_partition: Arc<Tensor> has multiple owners")
})?;
Ok(tensor.partition(partition_shape))
}
}
pub trait Unpartition<T: DType> {
fn unpartition(self) -> impl DeviceOp<Output = Tensor<T>>;
}
impl<T: DType, DI: DeviceOp<Output = Partition<Tensor<T>>>> Unpartition<T> for DI {
fn unpartition(self) -> impl DeviceOp<Output = Tensor<T>> {
UnwrapPartition { op: self }
}
}
#[derive(Clone, Debug)]
pub struct DeviceVec<T> {
_ty: PhantomData<T>,
host_vec: Vec<Arc<T>>,
device_vec: Arc<Tensor<i64>>,
}
impl<T: DType> DeviceVec<Tensor<T>> {
pub fn from(v: Vec<Tensor<T>>) -> DeviceVec<Tensor<T>> {
let i64vec: Arc<Vec<i64>> = v
.iter()
.map(|x| x.cu_deviceptr() as i64)
.collect::<Vec<_>>()
.into();
let device_vec: Arc<Tensor<i64>> = copy_host_vec_to_device(&i64vec)
.sync()
.expect("Failed to execute device operation.")
.reshape_unchecked(&[v.len()])
.into();
let host_vec: Vec<Arc<Tensor<T>>> = v.into_iter().map(Arc::new).collect::<Vec<_>>();
DeviceVec {
_ty: PhantomData,
host_vec,
device_vec,
}
}
pub fn is_empty(&self) -> bool {
self.host_vec.len() == 0
}
pub fn len(&self) -> usize {
self.host_vec.len()
}
pub unsafe fn inner(&self) -> &Arc<Tensor<i64>> {
&self.device_vec
}
}
impl<T: DType> From<Vec<Tensor<T>>> for DeviceVec<Tensor<T>> {
fn from(v: Vec<Tensor<T>>) -> Self {
DeviceVec::from(v)
}
}
impl<T: DType> Index<usize> for DeviceVec<Tensor<T>> {
type Output = Arc<Tensor<T>>;
fn index(&self, index: usize) -> &Self::Output {
&self.host_vec[index]
}
}
pub struct DeviceVecIntoIter<Item> {
items: DeviceVec<Item>,
}
impl<T: DType> Iterator for DeviceVecIntoIter<Tensor<T>> {
type Item = Tensor<T>;
fn next(&mut self) -> Option<Self::Item> {
if !self.items.is_empty() {
let x = self.items.host_vec.remove(0);
let x = Arc::try_unwrap(x).expect("Unable to perform into_iter from non-unique Arc.");
Some(x)
} else {
None
}
}
}
impl<T: DType> IntoIterator for DeviceVec<Tensor<T>> {
type Item = Tensor<T>;
type IntoIter = DeviceVecIntoIter<Tensor<T>>;
fn into_iter(self) -> Self::IntoIter {
DeviceVecIntoIter { items: self }
}
}
impl<T: DType> IntoDeviceOp<Partition<Tensor<T>>> for Partition<Tensor<T>> {
type Op = Value<Partition<Tensor<T>>>;
fn into_op(self) -> Value<Partition<Tensor<T>>> {
value(self)
}
}
impl<T: DType> IntoDeviceOp<MappedLaunchPartition<Partition<Tensor<T>>>>
for MappedLaunchPartition<Partition<Tensor<T>>>
{
type Op = Value<MappedLaunchPartition<Partition<Tensor<T>>>>;
fn into_op(self) -> Value<MappedLaunchPartition<Partition<Tensor<T>>>> {
value(self)
}
}
impl<'a, T: DType + Sync> IntoDeviceOp<MappedLaunchPartition<Partition<&'a mut Tensor<T>>>>
for MappedLaunchPartition<Partition<&'a mut Tensor<T>>>
{
type Op = Value<MappedLaunchPartition<Partition<&'a mut Tensor<T>>>>;
fn into_op(self) -> Value<MappedLaunchPartition<Partition<&'a mut Tensor<T>>>> {
value(self)
}
}
impl<T: DType> IntoDeviceOp<Tensor<T>> for Tensor<T> {
type Op = Value<Tensor<T>>;
fn into_op(self) -> Value<Tensor<T>> {
value(self)
}
}
impl<'a, T: DType + Sync> IntoDeviceOp<&'a Tensor<T>> for &'a Tensor<T> {
type Op = Value<&'a Tensor<T>>;
fn into_op(self) -> Value<&'a Tensor<T>> {
value(self)
}
}
use cuda_async::launch::AsyncKernelLaunch;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridBound {
Exact((u32, u32, u32)),
AtMost((u32, u32, u32)),
}
pub trait KernelOutputStored<T: DType>: Send {
fn push_kernel_args(&self, launcher: &mut AsyncKernelLaunch);
fn grid(&self) -> Result<(u32, u32, u32), Error>;
fn grid_bound(&self) -> Result<GridBound, Error> {
Ok(GridBound::Exact(self.grid()?))
}
fn map_shape_as_i32(&self) -> Option<Vec<i32>> {
None
}
fn dtype_str(&self) -> &'static str;
fn partition_shape_as_i32(&self) -> Vec<i32>;
fn strides_hint(&self) -> Vec<i32>;
fn spec(&self) -> &SpecializationBits;
fn shape_as_i32(&self) -> Vec<i32>;
}
pub trait KernelOutput<T: DType>: Send + Sized {
type Stored: KernelOutputStored<T>;
type Returned: Send;
fn prepare(self) -> Self::Stored;
fn recover(stored: Self::Stored) -> Self::Returned;
}
impl<T: DType> KernelOutputStored<T> for Partition<Tensor<T>> {
fn grid_bound(&self) -> Result<GridBound, Error> {
let grid = KernelOutputStored::grid(self)?;
Ok(if self.prefix_coverage {
GridBound::AtMost(grid)
} else {
GridBound::Exact(grid)
})
}
fn push_kernel_args(&self, launcher: &mut AsyncKernelLaunch) {
unsafe {
launcher.push_device_ptr(self.object.cu_deviceptr());
}
for dim in self.object.shape.iter() {
launcher.push_arg(*dim);
}
for stride in self.object.strides.iter() {
launcher.push_arg(*stride);
}
for dim in self.partition_shape.iter() {
launcher.push_arg(*dim as i32);
}
for stride in self.partition_strides.iter() {
launcher.push_arg(*stride as i32);
}
}
fn grid(&self) -> Result<(u32, u32, u32), Error> {
let shape: Vec<u32> = self.shape_as_i32().iter().map(|&x| x as u32).collect();
let pshape: Vec<u32> = self
.partition_shape_as_i32()
.iter()
.map(|&x| x as u32)
.collect();
match shape.len() {
1 => Ok((u32::div_ceil(shape[0], pshape[0]), 1, 1)),
2 => Ok((
u32::div_ceil(shape[0], pshape[0]),
u32::div_ceil(shape[1], pshape[1]),
1,
)),
3 => Ok((
u32::div_ceil(shape[0], pshape[0]),
u32::div_ceil(shape[1], pshape[1]),
u32::div_ceil(shape[2], pshape[2]),
)),
_ => tensor_error_result("Mutable tensor must be at most rank 3."),
}
}
fn dtype_str(&self) -> &'static str {
T::DTYPE.as_str()
}
fn partition_shape_as_i32(&self) -> Vec<i32> {
self.partition_shape.iter().map(|&x| x as i32).collect()
}
fn strides_hint(&self) -> Vec<i32> {
self.object
.spec
.stride_one
.iter()
.map(|&is_one| if is_one { 1 } else { -1 })
.collect()
}
fn spec(&self) -> &SpecializationBits {
&self.object.spec
}
fn shape_as_i32(&self) -> Vec<i32> {
self.object.shape.clone()
}
}
impl<'a, T: DType> KernelOutputStored<T> for Partition<&'a mut Tensor<T>> {
fn grid_bound(&self) -> Result<GridBound, Error> {
let grid = KernelOutputStored::grid(self)?;
Ok(if self.prefix_coverage {
GridBound::AtMost(grid)
} else {
GridBound::Exact(grid)
})
}
fn push_kernel_args(&self, launcher: &mut AsyncKernelLaunch) {
unsafe {
launcher.push_device_ptr(self.object.cu_deviceptr());
}
for dim in self.object.shape.iter() {
launcher.push_arg(*dim);
}
for stride in self.object.strides.iter() {
launcher.push_arg(*stride);
}
for dim in self.partition_shape.iter() {
launcher.push_arg(*dim as i32);
}
for stride in self.partition_strides.iter() {
launcher.push_arg(*stride as i32);
}
}
fn grid(&self) -> Result<(u32, u32, u32), Error> {
let shape: Vec<u32> = self.shape_as_i32().iter().map(|&x| x as u32).collect();
let pshape: Vec<u32> = self
.partition_shape_as_i32()
.iter()
.map(|&x| x as u32)
.collect();
match shape.len() {
1 => Ok((u32::div_ceil(shape[0], pshape[0]), 1, 1)),
2 => Ok((
u32::div_ceil(shape[0], pshape[0]),
u32::div_ceil(shape[1], pshape[1]),
1,
)),
3 => Ok((
u32::div_ceil(shape[0], pshape[0]),
u32::div_ceil(shape[1], pshape[1]),
u32::div_ceil(shape[2], pshape[2]),
)),
_ => tensor_error_result("Mutable tensor must be at most rank 3."),
}
}
fn dtype_str(&self) -> &'static str {
T::DTYPE.as_str()
}
fn partition_shape_as_i32(&self) -> Vec<i32> {
self.partition_shape.iter().map(|&x| x as i32).collect()
}
fn strides_hint(&self) -> Vec<i32> {
self.object
.spec
.stride_one
.iter()
.map(|&is_one| if is_one { 1 } else { -1 })
.collect()
}
fn spec(&self) -> &SpecializationBits {
&self.object.spec
}
fn shape_as_i32(&self) -> Vec<i32> {
self.object.shape.clone()
}
}
impl<T: DType> KernelOutputStored<T> for MappedLaunchPartition<Partition<Tensor<T>>> {
fn push_kernel_args(&self, launcher: &mut AsyncKernelLaunch) {
self.partition.push_kernel_args(launcher);
}
fn grid(&self) -> Result<(u32, u32, u32), Error> {
self.validate(self.partition.grid()?, self.num_tile_blocks)
}
fn map_shape_as_i32(&self) -> Option<Vec<i32>> {
Some(self.map_shape.iter().map(|&dim| dim as i32).collect())
}
fn dtype_str(&self) -> &'static str {
self.partition.dtype_str()
}
fn partition_shape_as_i32(&self) -> Vec<i32> {
self.partition.partition_shape_as_i32()
}
fn strides_hint(&self) -> Vec<i32> {
self.partition.strides_hint()
}
fn spec(&self) -> &SpecializationBits {
self.partition.spec()
}
fn shape_as_i32(&self) -> Vec<i32> {
self.partition.shape_as_i32()
}
}
impl<'a, T: DType> KernelOutputStored<T> for MappedLaunchPartition<Partition<&'a mut Tensor<T>>> {
fn push_kernel_args(&self, launcher: &mut AsyncKernelLaunch) {
self.partition.push_kernel_args(launcher);
}
fn grid(&self) -> Result<(u32, u32, u32), Error> {
self.validate(self.partition.grid()?, self.num_tile_blocks)
}
fn map_shape_as_i32(&self) -> Option<Vec<i32>> {
Some(self.map_shape.iter().map(|&dim| dim as i32).collect())
}
fn dtype_str(&self) -> &'static str {
self.partition.dtype_str()
}
fn partition_shape_as_i32(&self) -> Vec<i32> {
self.partition.partition_shape_as_i32()
}
fn strides_hint(&self) -> Vec<i32> {
self.partition.strides_hint()
}
fn spec(&self) -> &SpecializationBits {
self.partition.spec()
}
fn shape_as_i32(&self) -> Vec<i32> {
self.partition.shape_as_i32()
}
}
impl<T: DType> KernelOutput<T> for Partition<Tensor<T>> {
type Stored = Partition<Tensor<T>>;
type Returned = Partition<Tensor<T>>;
fn prepare(self) -> Self::Stored {
self
}
fn recover(stored: Self::Stored) -> Self::Returned {
stored
}
}
impl<'a, T: DType> KernelOutput<T> for Partition<&'a mut Tensor<T>> {
type Stored = Partition<&'a mut Tensor<T>>;
type Returned = Partition<&'a mut Tensor<T>>;
fn prepare(self) -> Self::Stored {
self
}
fn recover(stored: Self::Stored) -> Self::Returned {
stored
}
}
impl<T: DType> KernelOutput<T> for MappedLaunchPartition<Partition<Tensor<T>>> {
type Stored = MappedLaunchPartition<Partition<Tensor<T>>>;
type Returned = Partition<Tensor<T>>;
fn prepare(self) -> Self::Stored {
self
}
fn recover(stored: Self::Stored) -> Self::Returned {
stored.partition
}
}
impl<'a, T: DType> KernelOutput<T> for MappedLaunchPartition<Partition<&'a mut Tensor<T>>> {
type Stored = MappedLaunchPartition<Partition<&'a mut Tensor<T>>>;
type Returned = Partition<&'a mut Tensor<T>>;
fn prepare(self) -> Self::Stored {
self
}
fn recover(stored: Self::Stored) -> Self::Returned {
stored.partition
}
}
pub trait KernelInputStored: Send {
fn push_kernel_args(&self, launcher: &mut AsyncKernelLaunch);
fn shape(&self) -> &[i32];
fn strides(&self) -> &[i32];
fn spec(&self) -> &SpecializationBits;
fn dtype_str(&self) -> &'static str;
}
pub trait KernelInput<T: DType>: Send + Sized {
type Stored: KernelInputStored;
type Returned: Send;
fn prepare(self) -> Self::Stored;
fn recover(stored: Self::Stored) -> Self::Returned;
}
impl<T: DType> KernelInputStored for Arc<Tensor<T>> {
fn push_kernel_args(&self, launcher: &mut AsyncKernelLaunch) {
unsafe {
launcher.push_device_ptr(self.cu_deviceptr());
}
for dim in self.shape.iter() {
launcher.push_arg(*dim);
}
for stride in self.strides.iter() {
launcher.push_arg(*stride);
}
}
fn shape(&self) -> &[i32] {
Tensor::shape(self)
}
fn strides(&self) -> &[i32] {
Tensor::strides(self)
}
fn spec(&self) -> &SpecializationBits {
Tensor::spec(self)
}
fn dtype_str(&self) -> &'static str {
T::DTYPE.as_str()
}
}
impl<'a, T: DType + Sync> KernelInputStored for &'a Tensor<T> {
fn push_kernel_args(&self, launcher: &mut AsyncKernelLaunch) {
unsafe {
launcher.push_device_ptr(self.cu_deviceptr());
}
for dim in self.shape.iter() {
launcher.push_arg(*dim);
}
for stride in self.strides.iter() {
launcher.push_arg(*stride);
}
}
fn shape(&self) -> &[i32] {
Tensor::shape(self)
}
fn strides(&self) -> &[i32] {
Tensor::strides(self)
}
fn spec(&self) -> &SpecializationBits {
Tensor::spec(self)
}
fn dtype_str(&self) -> &'static str {
T::DTYPE.as_str()
}
}
impl<T: DType> KernelInput<T> for Tensor<T> {
type Stored = Arc<Tensor<T>>;
type Returned = Tensor<T>;
fn prepare(self) -> Arc<Tensor<T>> {
Arc::new(self)
}
fn recover(stored: Arc<Tensor<T>>) -> Tensor<T> {
Arc::try_unwrap(stored).expect("KernelInput::recover: Arc has multiple owners")
}
}
impl<T: DType> KernelInput<T> for Arc<Tensor<T>> {
type Stored = Arc<Tensor<T>>;
type Returned = Arc<Tensor<T>>;
fn prepare(self) -> Arc<Tensor<T>> {
self
}
fn recover(stored: Arc<Tensor<T>>) -> Arc<Tensor<T>> {
stored
}
}
impl<'a, T: DType + Sync> KernelInput<T> for &'a Tensor<T> {
type Stored = &'a Tensor<T>;
type Returned = &'a Tensor<T>;
fn prepare(self) -> &'a Tensor<T> {
self
}
fn recover(stored: &'a Tensor<T>) -> &'a Tensor<T> {
stored
}
}
impl<'a, T: DType + Sync> KernelInputStored for &'a TensorView<'a, T> {
fn push_kernel_args(&self, launcher: &mut AsyncKernelLaunch) {
unsafe {
launcher.push_device_ptr(self.base.cu_deviceptr() + self.offset_bytes as u64);
}
for dim in self.shape.iter() {
launcher.push_arg(*dim);
}
for stride in self.strides.iter() {
launcher.push_arg(*stride);
}
}
fn shape(&self) -> &[i32] {
&self.shape
}
fn strides(&self) -> &[i32] {
&self.strides
}
fn spec(&self) -> &SpecializationBits {
TensorView::spec(self)
}
fn dtype_str(&self) -> &'static str {
T::DTYPE.as_str()
}
}
impl<'a, T: DType + Sync> KernelInput<T> for &'a TensorView<'a, T> {
type Stored = &'a TensorView<'a, T>;
type Returned = &'a TensorView<'a, T>;
fn prepare(self) -> Self::Stored {
self
}
fn recover(stored: Self::Stored) -> Self::Returned {
stored
}
}
impl<'a, T: DType + Sync> IntoDeviceOp<&'a TensorView<'a, T>> for &'a TensorView<'a, T> {
type Op = Value<&'a TensorView<'a, T>>;
fn into_op(self) -> Value<&'a TensorView<'a, T>> {
value(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct FakeAlloc {
len_bytes: usize,
}
unsafe impl DeviceAllocation for FakeAlloc {
fn device_ptr(&self) -> CUdeviceptr {
16 }
fn len_bytes(&self) -> usize {
self.len_bytes
}
fn device_id(&self) -> usize {
0
}
}
#[test]
fn addressable_bytes_accounts_for_strides() {
assert_eq!(addressable_bytes::<f32>(&[4], &[1]), 4 * 4);
assert_eq!(addressable_bytes::<f32>(&[4], &[10]), 31 * 4);
assert_eq!(addressable_bytes::<f32>(&[0], &[1]), 0);
}
#[test]
fn from_foreign_accepts_allocation_covering_strided_extent() {
let owner: Arc<dyn DeviceAllocation> = Arc::new(FakeAlloc { len_bytes: 124 });
let _t = unsafe { Tensor::<f32>::from_foreign(owner, vec![4], vec![10]) };
}
#[test]
#[should_panic(expected = "addressable extent")]
fn from_foreign_rejects_strides_overrunning_allocation() {
let owner: Arc<dyn DeviceAllocation> = Arc::new(FakeAlloc { len_bytes: 16 });
let _t = unsafe { Tensor::<f32>::from_foreign(owner, vec![4], vec![10]) };
}
#[test]
#[should_panic(expected = "addressable extent overflowed")]
fn from_foreign_rejects_extent_overflow_instead_of_wrapping() {
let owner: Arc<dyn DeviceAllocation> = Arc::new(FakeAlloc { len_bytes: 16 });
let _t = unsafe {
Tensor::<f32>::from_foreign(owner, vec![i32::MAX, i32::MAX], vec![i32::MAX, i32::MAX])
};
}
#[test]
fn swizzle_accepts_tile_block_count() {
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![4, 1],
num_tile_blocks: 3,
};
let launch_grid = partition.validate((2, 3, 1), 3).unwrap();
assert_eq!(launch_grid, (3, 1, 1));
}
#[test]
fn swizzle_rejects_tile_block_count_larger_than_logical_grid() {
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![4, 1],
num_tile_blocks: 7,
};
let err = partition.validate((2, 3, 1), 7).unwrap_err();
assert!(err
.to_string()
.contains("num_tile_blocks cannot exceed the streamed logical tile count"));
}
#[test]
fn owned_axis_excluded_from_streamed_tile_count() {
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![1, OWNED],
num_tile_blocks: 2,
};
let launch_grid = partition.validate((2, 3, 1), 2).unwrap();
assert_eq!(launch_grid, (2, 1, 1));
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![1, OWNED],
num_tile_blocks: 3,
};
let err = partition.validate((2, 3, 1), 3).unwrap_err();
assert!(err
.to_string()
.contains("num_tile_blocks cannot exceed the streamed logical tile count"));
}
#[test]
fn owned_axis_rejects_all_owned_map() {
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![OWNED, OWNED],
num_tile_blocks: 1,
};
let err = partition.validate((2, 3, 1), 1).unwrap_err();
assert!(err
.to_string()
.contains("at least one streamed (non-OWNED) map axis"));
}
#[test]
fn owned_axis_accepts_leading_owned() {
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![OWNED, 1],
num_tile_blocks: 3,
};
let launch_grid = partition.validate((2, 3, 1), 3).unwrap();
assert_eq!(launch_grid, (3, 1, 1));
}
#[test]
fn swizzle_rejects_zero_tile_blocks() {
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![4, 1],
num_tile_blocks: 0,
};
let err = partition.validate((2, 3, 1), 0).unwrap_err();
assert!(err.to_string().contains("num_tile_blocks > 0"));
}
#[test]
fn swizzle_rejects_grid_rank_above_map_rank() {
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![4, 1],
num_tile_blocks: 3,
};
let err = partition.validate((2, 3, 4), 3).unwrap_err();
assert!(err
.to_string()
.contains("map rank must match the logical partition grid rank"));
}
#[test]
fn swizzle_accepts_rank1_map() {
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![1],
num_tile_blocks: 4,
};
let launch_grid = partition.validate((8, 1, 1), 4).unwrap();
assert_eq!(launch_grid, (4, 1, 1));
}
#[test]
fn swizzle_accepts_rank3_map() {
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![1, 4, 1],
num_tile_blocks: 6,
};
let launch_grid = partition.validate((2, 3, 4), 6).unwrap();
assert_eq!(launch_grid, (6, 1, 1));
}
#[test]
fn swizzle_rejects_rank1_map_with_2d_grid() {
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![1],
num_tile_blocks: 2,
};
let err = partition.validate((2, 3, 1), 2).unwrap_err();
assert!(err
.to_string()
.contains("map rank must match the logical partition grid rank"));
}
#[test]
fn swizzle_rejects_map_rank_above_3() {
let partition = MappedLaunchPartition {
partition: (),
map_shape: vec![1, 1, 1, 1],
num_tile_blocks: 2,
};
let err = partition.validate((2, 3, 4), 2).unwrap_err();
assert!(err.to_string().contains("rank-1 through rank-3 map shape"));
}
}