use cubecl_core::{CubeElement, prelude::Runtime};
use crate::tensor::{TensorHandle, copy_into, into_contiguous_packed};
fn contiguous_strides(shape: &[usize]) -> Vec<usize> {
let mut strides = vec![1usize; shape.len()];
let mut current = 1usize;
for d in (0..shape.len()).rev() {
strides[d] = current;
current *= shape[d];
}
strides
}
fn pack_along(
unpacked: &[u32],
shape: &[usize],
pack_dim: usize,
packing: usize,
bits: u32,
) -> Vec<u32> {
let rank = shape.len();
let mut storage_shape = shape.to_vec();
storage_shape[pack_dim] = storage_shape[pack_dim].div_ceil(packing);
let storage_strides = contiguous_strides(&storage_shape);
let storage_len: usize = storage_shape.iter().product();
let mask = (1u32 << bits) - 1;
let mut out = vec![0u32; storage_len];
for (q, &value) in unpacked.iter().enumerate() {
let mut coords = vec![0usize; rank];
let mut remainder = q;
for d in (0..rank).rev() {
coords[d] = remainder % shape[d];
remainder /= shape[d];
}
let slot = (coords[pack_dim] % packing) as u32;
let mut offset = 0usize;
for d in 0..rank {
let coord = if d == pack_dim {
coords[d] / packing
} else {
coords[d]
};
offset += coord * storage_strides[d];
}
out[offset] |= (value & mask) << (slot * bits);
}
out
}
fn run_repack_case<R: Runtime>(
device: &R::Device,
shape: &[usize],
in_pack_dim: usize,
packing: usize,
bits: u32,
) {
let client = R::client(device);
let dtype = u32::cube_type();
let rank = shape.len();
let num_unpacked: usize = shape.iter().product();
let unpacked: Vec<u32> = (0..num_unpacked).map(|q| ((q % 15) + 1) as u32).collect();
let input_storage = pack_along(&unpacked, shape, in_pack_dim, packing, bits);
let mut in_storage_shape = shape.to_vec();
in_storage_shape[in_pack_dim] = in_storage_shape[in_pack_dim].div_ceil(packing);
let packed_dim = rank - 1 - in_pack_dim;
let expected = pack_along(&unpacked, shape, rank - 1, packing, bits);
let input_handle = client.create_from_slice(u32::as_bytes(&input_storage));
let input = TensorHandle::<R>::new_contiguous(in_storage_shape, input_handle, dtype);
let output =
into_contiguous_packed(&client, input.binding(), packed_dim, shape, packing, dtype);
let bytes = client.read_one_unchecked_tensor(output.handle.clone().copy_descriptor(
output.shape().clone(),
output.strides().clone(),
size_of::<u32>(),
));
let actual = u32::from_bytes(&bytes);
assert!(
actual.iter().any(|&v| v != 0),
"repacked output is all zeros: the write loop ran zero vectors per thread (shape {shape:?})",
);
assert_eq!(
actual,
&expected[..],
"repacked PackedU32 tensor does not match the CPU reference (shape {shape:?}, in_pack_dim {in_pack_dim})",
);
}
pub fn test_into_contiguous_packed_repack<R: Runtime>(device: &R::Device) {
run_repack_case::<R>(device, &[1, 8, 16], 1, 8, 4);
}
pub fn test_into_contiguous_packed_vector_size_one<R: Runtime>(device: &R::Device) {
run_repack_case::<R>(device, &[1, 8, 8], 1, 8, 4);
}
pub fn test_into_contiguous_packed_multi_vector<R: Runtime>(device: &R::Device) {
run_repack_case::<R>(device, &[4096, 256], 0, 8, 4);
}
pub fn test_into_contiguous_packed_halving<R: Runtime>(device: &R::Device) {
run_repack_case::<R>(device, &[8192, 32], 0, 8, 4);
}
pub fn test_into_contiguous_rank_mismatch<R: Runtime>(device: &R::Device) {
let client = R::client(device);
let dtype = f32::cube_type();
let shape = vec![1usize, 2, 4, 1];
let strides = vec![8usize, 1, 2, 2];
let num_elems = 8;
let data: Vec<f32> = (0..num_elems).map(|i| i as f32 + 1.0).collect();
let input = TensorHandle::<R>::new(
client.create_from_slice(f32::as_bytes(&data)),
shape.clone(),
strides.clone(),
dtype,
);
let out_shape = vec![1usize, 2, 4];
let output = TensorHandle::<R>::new_contiguous(
out_shape.clone(),
client.empty(num_elems * size_of::<f32>()),
dtype,
);
copy_into(&client, input.binding(), output.clone().binding(), dtype);
let bytes = client.read_one_unchecked_tensor(output.handle.clone().copy_descriptor(
output.shape().clone(),
output.strides().clone(),
size_of::<f32>(),
));
let actual = f32::from_bytes(&bytes);
let in_strides = contiguous_strides(&shape);
let expected: Vec<f32> = (0..num_elems)
.map(|q| {
let src: usize = (0..shape.len())
.map(|d| (q / in_strides[d]) % shape[d] * strides[d])
.sum();
data[src]
})
.collect();
assert_eq!(
actual,
&expected[..],
"rank-mismatched copy ({shape:?} strides {strides:?} -> {out_shape:?})",
);
}
fn run_permuted_case<R: Runtime, E: CubeElement + From<u8> + PartialEq>(
device: &R::Device,
base_shape: &[usize],
perm: &[usize],
) {
let client = R::client(device);
let dtype = E::cube_type();
let base_strides = contiguous_strides(base_shape);
let num_elems: usize = base_shape.iter().product();
let shape: Vec<usize> = perm.iter().map(|&p| base_shape[p]).collect();
let strides: Vec<usize> = perm.iter().map(|&p| base_strides[p]).collect();
let data: Vec<E> = (0..num_elems)
.map(|i| E::from((i % 251 + 1) as u8))
.collect();
let input = TensorHandle::<R>::new(
client.create_from_slice(E::as_bytes(&data)),
shape.clone(),
strides.clone(),
dtype,
);
let output = TensorHandle::<R>::new_contiguous(
shape.clone(),
client.empty(num_elems * size_of::<E>()),
dtype,
);
copy_into(&client, input.binding(), output.clone().binding(), dtype);
let bytes = client.read_one_unchecked_tensor(output.handle.clone().copy_descriptor(
output.shape().clone(),
output.strides().clone(),
size_of::<E>(),
));
let actual = E::from_bytes(&bytes);
let out_strides = contiguous_strides(&shape);
let expected: Vec<E> = (0..num_elems)
.map(|q| {
let src: usize = (0..shape.len())
.map(|d| (q / out_strides[d]) % shape[d] * strides[d])
.sum();
data[src]
})
.collect();
assert_eq!(
actual,
&expected[..],
"permuted copy mismatch (base shape {base_shape:?}, perm {perm:?}, \
view shape {shape:?}, view strides {strides:?})",
);
}
pub fn test_into_contiguous_permuted_unaligned_axis<R: Runtime>(device: &R::Device) {
run_permuted_case::<R, u8>(device, &[2, 2, 3], &[2, 1, 0]);
}
pub fn test_into_contiguous_permuted_sweep<R: Runtime>(device: &R::Device) {
let shapes: &[&[usize]] = &[
&[2, 3],
&[3, 2],
&[4, 6],
&[6, 4],
&[2, 2, 3],
&[3, 2, 2],
&[2, 3, 4],
&[4, 3, 2],
&[8, 3, 4],
&[3, 8, 5],
&[16, 5],
&[5, 16],
&[8, 8],
&[16, 16],
&[32, 2, 3],
&[2, 3, 4, 5],
];
for shape in shapes {
for perm in permutations(shape.len()) {
run_permuted_case::<R, u8>(device, shape, &perm);
run_permuted_case::<R, u32>(device, shape, &perm);
}
}
}
fn permutations(rank: usize) -> Vec<Vec<usize>> {
let mut out = Vec::new();
let mut current = Vec::with_capacity(rank);
fn recurse(rank: usize, current: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
if current.len() == rank {
out.push(current.clone());
return;
}
for axis in 0..rank {
if !current.contains(&axis) {
current.push(axis);
recurse(rank, current, out);
current.pop();
}
}
}
recurse(rank, &mut current, &mut out);
out
}