use cubecl::{
bytes::Bytes,
client::Client,
prelude::TensorArg,
prelude::TensorBinding,
quant::scheme::{QuantScheme, QuantValue},
zspace::{Shape, Tiling},
};
use cubecl::{frontend::Scalar, std::tensor::TensorHandle};
use cubecl_tile::{
Axis, Launcher, Space, TileArgLaunch, TileSpec,
kind::Field,
launch::{Arg, Labelled},
};
use crate::support::{TestInput, TestInputBuilder};
pub struct TileInput {
handle: TensorHandle,
space: Space,
axes: Vec<Axis>,
levels: usize,
}
impl TileInput {
pub fn builder(client: &Client, space: Space) -> TileInputBuilder {
TileInputBuilder {
client: client.clone(),
space,
levels: None,
}
}
fn new(handle: TensorHandle, space: Space, levels: usize) -> Self {
let axes = (0..space.rank()).map(|i| space.axis_at(i)).collect();
TileInput {
handle,
space,
axes,
levels,
}
}
pub fn tensor_arg(&self, vector_size: usize) -> TensorArg {
if vector_size <= 1 {
return self.handle.clone().binding().into_tensor_arg();
}
let shape = self.handle.shape();
let strides = self.handle.strides();
let inner = shape.len() - 1;
let new_shape: Vec<usize> = shape
.iter()
.enumerate()
.map(|(i, &s)| if i == inner { s / vector_size } else { s })
.collect();
let new_strides: Vec<usize> = strides
.iter()
.enumerate()
.map(|(i, &s)| if i == inner { s } else { s / vector_size })
.collect();
TensorHandle::new(
self.handle.handle.clone(),
new_shape,
new_strides,
self.handle.dtype,
)
.binding()
.into_tensor_arg()
}
pub fn arg(&self, launcher: &Launcher, width: usize) -> TileArgLaunch {
self.bind(launcher)
.vectorize(width)
.build()
.unwrap_or_else(|refusal| panic!("TileInput::arg: {refusal}"))
.arg()
}
pub fn bind<'a>(&'a self, launcher: &'a Launcher) -> Arg<'a, Labelled> {
let mut binding = self.handle.clone().binding();
binding.tiling = self.tiling();
launcher.arg(binding, self.handle.dtype).axes(&self.axes)
}
fn tiling(&self) -> Tiling {
match self.levels {
0 => Tiling::UNTILED,
levels => Tiling::new(&vec![levels + 1; self.space.rank()])
.expect("TileInput: a tiling the buffer's dims hold"),
}
}
pub fn space(&self) -> Space {
self.space.clone()
}
pub fn handle(&self) -> TensorHandle {
self.handle.clone()
}
}
enum TileLevel {
Split(Vec<usize>),
Tile(Vec<usize>),
}
pub struct TileInputBuilder {
client: Client,
space: Space,
levels: Option<Vec<TileLevel>>,
}
impl TileInputBuilder {
pub fn split(mut self, counts: &[usize]) -> Self {
self.levels
.get_or_insert_with(Vec::new)
.push(TileLevel::Split(counts.to_vec()));
self
}
pub fn tile(mut self, edges: &[usize]) -> Self {
self.levels
.get_or_insert_with(Vec::new)
.push(TileLevel::Tile(edges.to_vec()));
self
}
pub fn untiled(mut self) -> Self {
self.levels = Some(Vec::new());
self
}
pub fn arange(self) -> TileInput {
self.build(TestInputBuilder::arange)
}
pub fn zeros(self) -> TileInput {
self.build(TestInputBuilder::zeros)
}
pub fn uniform(self, seed: u64, lo: f32, hi: f32) -> TileInput {
self.build(|b: TestInputBuilder| b.uniform(seed, lo, hi))
}
pub fn packed(self, scheme: &QuantScheme) -> QuantizedTileInputBuilder {
let levels = self
.levels
.expect("TileInput: set .untiled() before .packed");
assert!(
levels.is_empty(),
"TileInput::packed: a packed input is untiled"
);
QuantizedTileInputBuilder {
client: self.client,
space: self.space,
scheme: *scheme,
}
}
fn build(self, fill: impl FnOnce(TestInputBuilder) -> TestInput) -> TileInput {
let levels = self
.levels
.expect("TileInput: set .split/.tile(...) or .untiled() before a finalizer");
let rank = self.space.rank();
let mut current: Vec<usize> = (0..rank)
.map(|i| self.space.extent(self.space.axis_at(i)))
.collect();
let mut blocks: Vec<Vec<usize>> = Vec::with_capacity(levels.len() + 1);
for level in &levels {
let (values, is_split) = match level {
TileLevel::Split(values) => (values, true),
TileLevel::Tile(values) => (values, false),
};
assert_eq!(
values.len(),
rank,
"TileInput: a tile level needs one value per axis (rank {rank})"
);
let block: Vec<usize> = (0..rank)
.map(|i| {
let (edge, value) = (current[i], values[i]);
assert!(
value != 0 && edge % value == 0,
"TileInput: tile value {value} does not divide the current edge {edge} on axis {i}"
);
let (dim, next) = if is_split {
(value, edge / value)
} else {
(edge / value, value)
};
current[i] = next;
dim
})
.collect();
blocks.push(block);
}
blocks.push(current);
let mut dims = Vec::with_capacity(blocks.len() * rank);
for block in &blocks {
dims.extend_from_slice(block);
}
let builder = TestInput::builder(self.client, Shape::from(dims));
TileInput::new(
fill(builder).generate_without_host_data(),
self.space,
levels.len(),
)
}
}
pub struct QuantizedTileInputBuilder {
client: Client,
space: Space,
scheme: QuantScheme,
}
impl QuantizedTileInputBuilder {
pub fn arange(self) -> QuantizedTileInput {
assert!(
integer_valued(self.scheme.value),
"QuantizedTileInputBuilder::arange: {:?} is a minifloat, whose fields are codes rather \
than magnitudes; state the codes with `pack_q_values` and fold the format's grid into \
the test's own reference",
self.scheme.value
);
let rank = self.space.rank();
let shape: Vec<usize> = (0..rank)
.map(|i| self.space.extent(self.space.axis_at(i)))
.collect();
let (lo, hi) = self.scheme.value.range();
let (lo, hi) = (lo as i32, hi as i32);
let span = hi - lo + 1;
let q: Vec<i32> = (0..shape.iter().product())
.map(|i| lo + (i as i32 % span))
.collect();
let words = crate::support::stubs::quant::pack_q_values(&q, &self.scheme);
let handle = self.client.create(Bytes::from_elems(words));
let block = crate::support::stubs::quant::block_dims(&self.scheme, &shape);
let grid = crate::support::stubs::quant::scales_grid(&shape, &block);
let scale_values: Vec<f32> = (0..grid.iter().product())
.map(|g| 0.05 * (g + 1) as f32)
.collect();
let scales = TestInput::builder(self.client, Shape::from(grid))
.custom(scale_values.clone())
.generate_without_host_data();
QuantizedTileInput {
tile: TileInput::new(
TensorHandle::new_contiguous(shape, handle, u32::elem_type_native()),
self.space,
0,
),
scales,
scheme: self.scheme,
q,
scale_values,
}
}
}
fn integer_valued(value: QuantValue) -> bool {
match value {
QuantValue::Q8F
| QuantValue::Q8S
| QuantValue::Q4F
| QuantValue::Q4S
| QuantValue::Q2F
| QuantValue::Q2S => true,
QuantValue::E5M2 | QuantValue::E4M3 | QuantValue::E2M1 => false,
}
}
pub struct QuantizedTileInput {
pub tile: TileInput,
scales: TensorHandle,
scheme: QuantScheme,
pub q: Vec<i32>,
pub scale_values: Vec<f32>,
}
impl QuantizedTileInput {
pub fn scales_binding(&self) -> TensorBinding {
self.scales.clone().binding()
}
pub fn field(&self) -> Field {
match self.scheme.mode {
cubecl::quant::scheme::QuantMode::Lookup => Field::Index {
bits: self.scheme.size_bits_value(),
},
_ => Field::Quant(self.scheme.value),
}
}
pub fn values_arg(&self, width: usize, spec: TileSpec) -> TileArgLaunch {
TileArgLaunch::new(
self.tile.tensor_arg(1),
self.tile.handle.dtype,
width,
spec.packed(self.field()),
)
}
pub fn scales_arg(&self, width: usize, spec: TileSpec) -> TileArgLaunch {
TileArgLaunch::new(
self.scales_binding().into_tensor_arg(),
self.scales.dtype,
width,
spec,
)
}
}