#![allow(dead_code)]
use cubecl::prelude::{Numeric, Size};
use cubecl::{
bytes::Bytes,
client::Client,
prelude::TensorArg,
prelude::TensorBinding,
quant::scheme::{QuantScheme, QuantValue},
zspace::Shape,
};
use cubecl::{
frontend::Scalar,
std::tensor::{
TensorHandle,
layout::tiled_view::{TileSpec, TiledViewLaunch, TiledViewLayout},
},
};
use cubek_tile::{Axis, Projection, Space, TileArgLaunch, TileSpec as CubekTileSpec, kind::Field};
use crate::{TestInput, TestInputBuilder};
pub struct TileInput {
handle: TensorHandle,
space: Space,
levels: usize,
}
impl TileInput {
pub fn builder(client: &Client, space: Space) -> TileInputBuilder {
TileInputBuilder {
client: client.clone(),
space,
levels: None,
}
}
pub fn view(&self) -> TiledViewLaunch {
TiledViewLaunch::new_tensor::<TiledViewLayout>(
self.handle.clone().binding().into_tensor_arg(),
TileSpec {
start_axis: 0,
num_tiled: self.space.rank(),
levels: self.levels,
},
)
}
pub fn view_vectorized(&self, vector_size: usize) -> TiledViewLaunch {
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();
let lined = TensorHandle::new(
self.handle.handle.clone(),
new_shape,
new_strides,
self.handle.dtype,
);
TiledViewLaunch::new_tensor::<TiledViewLayout>(
lined.binding().into_tensor_arg(),
TileSpec {
start_axis: 0,
num_tiled: self.space.rank(),
levels: self.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<E: Numeric, V: Size>(&self) -> TileArgLaunch<'static, E, V> {
TileArgLaunch::new(self.tensor_arg(1), self.spec())
}
pub fn spec(&self) -> CubekTileSpec {
let axes: Vec<_> = (0..self.space.rank())
.map(|i| self.space.axis_at(i))
.collect();
let levels = self.handle.shape().len() / self.space.rank() - 1;
CubekTileSpec::new(Projection::tiled(&axes, &axes.repeat(levels + 1)))
}
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 {
handle: fill(builder).generate_without_host_data(),
space: self.space,
levels: 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::stubs::quant::pack_q_values(&q, &self.scheme);
let handle = self.client.create(Bytes::from_elems(words));
let block = crate::stubs::quant::block_dims(&self.scheme, &shape);
let grid = crate::quant_layout::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 {
handle: TensorHandle::new_contiguous(shape, handle, u32::elem_type_native()),
space: self.space,
levels: 0,
},
scales,
scheme: self.scheme,
table: None,
q,
scale_values,
table_values: Vec::new(),
}
}
pub fn lookup_arange(self, table: &[f32]) -> QuantizedTileInput {
assert!(
matches!(self.scheme.mode, cubecl::quant::scheme::QuantMode::Lookup),
"lookup_arange: the scheme's mode is {:?}, not Lookup",
self.scheme.mode
);
let span = 1usize << self.scheme.size_bits_value();
assert_eq!(
table.len(),
span,
"lookup_arange: a {}-bit scheme indexes {span} entries",
self.scheme.size_bits_value()
);
let rank = self.space.rank();
let shape: Vec<usize> = (0..rank)
.map(|i| self.space.extent(self.space.axis_at(i)))
.collect();
let q: Vec<i32> = (0..shape.iter().product())
.map(|i| (span - 1 - (i % span)) as i32)
.collect();
let words = crate::stubs::quant::pack_q_values(&q, &self.scheme);
let handle = self.client.create(Bytes::from_elems(words));
let block = crate::stubs::quant::block_dims(&self.scheme, &shape);
let grid = crate::quant_layout::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.clone(), Shape::from(grid))
.custom(scale_values.clone())
.generate_without_host_data();
let table_handle = TestInput::builder(self.client, Shape::from(vec![span]))
.custom(table.to_vec())
.generate_without_host_data();
QuantizedTileInput {
tile: TileInput {
handle: TensorHandle::new_contiguous(shape, handle, u32::elem_type_native()),
space: self.space,
levels: 0,
},
scales,
scheme: self.scheme,
table: Some(table_handle),
q,
scale_values,
table_values: table.to_vec(),
}
}
}
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,
table: Option<TensorHandle>,
pub q: Vec<i32>,
pub scale_values: Vec<f32>,
pub table_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<V: Size>(&self, spec: CubekTileSpec) -> TileArgLaunch<'static, u32, V> {
TileArgLaunch::new(self.tile.tensor_arg(1), spec.packed(self.field()))
}
pub fn scales_arg<V: Size>(&self, spec: CubekTileSpec) -> TileArgLaunch<'static, f32, V> {
TileArgLaunch::new(self.scales_binding().into_tensor_arg(), spec)
}
pub fn table_arg<V: Size>(&self, axis: Axis) -> TileArgLaunch<'static, f32, V> {
let table = self
.table
.as_ref()
.expect("QuantizedTileInput::table_arg: this scheme indexes no table");
TileArgLaunch::new(
table.clone().binding().into_tensor_arg(),
CubekTileSpec::direct(&[axis]),
)
}
}