use super::{Form, implied};
use crate::support::{HostData, HostDataType, TestInput, ValidationResult};
use cubecl::{
RuntimeId, bytes::Bytes, prelude::*, quant::scheme::QuantValue, std::tensor::TensorHandle,
zspace::shape,
};
use cubecl_tile::layout::PhysicalAxisMap;
use cubecl_tile::*;
const M: Axis = Axis(0);
const N: Axis = Axis(1);
const KB: Axis = Axis(2);
const KI: Axis = Axis(3);
#[cube(launch)]
#[allow(clippy::too_many_arguments)]
fn quantized_decode(
w: &TileArg<'_>,
x: &TileArg<'_>,
scale: &TileArg<'_>,
out: &TileArg<'_>,
space: Partitioning,
#[comptime] budget: usize,
) {
let w = w.tile(&space);
let x = x.tile(&space);
let scale = scale.tile(&space);
let out = out.tile(&space);
for cube in &out {
let mut out_w = out.at(&cube);
out_w.zero();
}
for cube in space {
let out_cube = out.at(&cube);
let w_cube = w.at(&cube);
let x_cube = x.at(&cube);
let scale_cube = scale.at(&cube);
for plane in cube {
let out_plane = out_cube.at(&plane);
let w_plane = w_cube.at(&plane);
let x_plane = x_cube.at(&plane);
let scale_plane = scale_cube.at(&plane);
for unit in plane {
let mut out_unit = out_plane
.at(&unit)
.accumulating(comptime!(RegisterBlock::new(budget)), Semiring::SUM_PROD);
out_unit.mma(
&w_plane.at(&unit).mul(&scale_plane.at(&unit)),
&x_plane.at(&unit),
);
}
}
}
}
#[cube(launch)]
#[allow(clippy::too_many_arguments)]
fn quantized_decode_promoted<E: Numeric>(
w: &TileArg<'_>,
x: &TileArg<'_>,
scale: &TileArg<'_>,
out: &TileArg<'_>,
space: Partitioning,
#[comptime] budget: usize,
#[define(E)] _dtype: ElemType,
) {
let w = w.tile(&space);
let x = x.tile(&space);
let scale = scale.tile(&space);
let out = out.tile(&space);
let acc = out.accumulator::<E>(
&w,
&x,
comptime!(Instruction::Registers {
config: RegisterBlock::new(budget)
}),
Semiring::SUM_PROD,
);
for cube in space {
let acc_cube = acc.at(&cube);
let w_cube = w.at(&cube);
let x_cube = x.at(&cube);
let scale_cube = scale.at(&cube);
for plane in cube {
let acc_plane = acc_cube.at(&plane);
let w_plane = w_cube.at(&plane);
let x_plane = x_cube.at(&plane);
let scale_plane = scale_cube.at(&plane);
for unit in plane {
let mut acc_unit = acc_plane.at(&unit);
acc_unit.mma(
&w_plane.at(&unit).mul(&scale_plane.at(&unit)),
&x_plane.at(&unit),
);
}
}
}
acc.drained_into(&out);
}
#[test]
fn the_serving_geometry_computes_the_decode_product() {
serving_geometry(false, true);
}
#[test]
fn a_promoted_accumulator_spans_the_whole_decode_walk() {
serving_geometry(true, true);
}
#[test]
fn a_unit_level_that_cuts_nothing_hands_every_unit_the_plane() {
serving_geometry(false, false);
}
fn serving_geometry(promoted: bool, units_cut: bool) {
let field = QuantValue::Q8S;
let bits = field.size_bits();
let factor = 32 / bits;
let device = cubecl::test_device();
if !promoted && device.runtime() == RuntimeId::Wgpu {
ValidationResult::Skipped(
"wgpu: the vectorized decode gemv writes only zeros on CI's software Vulkan"
.to_string(),
)
.enforce();
return;
}
let client = device.client();
let max = client.properties().hardware.max_vector_size;
if factor > max {
ValidationResult::Skipped(format!(
"device vectors cap at {max}, below the {factor}-value word"
))
.enforce();
return;
}
let plane = client.properties().hardware.plane_size_max as usize;
let (block, blocks) = (32, 4);
let (d_in, n) = (block * blocks, 1);
let group_units = block / factor;
if !plane.is_multiple_of(group_units) {
ValidationResult::Skipped(format!(
"a {plane}-unit plane does not split into {group_units}-unit groups"
))
.enforce();
return;
}
let groups = plane / group_units;
let rows_per_unit = 2;
let rows_per_plane = groups * rows_per_unit;
let num_planes = 2;
let rows_per_cube = num_planes * rows_per_plane;
let d_out = rows_per_cube * 2;
let span = 1i32 << bits;
let w: Vec<i32> = (0..d_out * d_in)
.map(|i| -(span / 2) + (i as i32 % span))
.collect();
let mask = (1u32 << bits) - 1;
let words: Vec<u32> = w
.chunks(factor)
.map(|word| {
word.iter()
.enumerate()
.fold(0u32, |acc, (j, &v)| acc | ((v as u32 & mask) << (j * bits)))
})
.collect();
let x: Vec<f32> = (0..d_in).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..d_out * blocks)
.map(|i| (i % 9) as f32 / 4.0 + 0.25)
.collect();
let dtype = f32::elem_type_native();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, d_out), (N, n), (KB, blocks), (KI, block)]),
match units_cut {
true => Levels::leaf(&[(M, rows_per_unit), (KI, factor), (KB, 1)])
.walk_every(&[KB])
.units(&[(M, groups), (KI, group_units)])
.interleaved(KI),
false => Levels::leaf(&[(M, rows_per_plane), (KB, 1)])
.walk_every(&[KB])
.units(&[]),
}
.planes(&[(M, num_planes)])
.cubes(&[M])
.build(),
),
Form::Static,
);
let budget = rows_per_unit * factor;
let w_tensor = TensorHandle::new_contiguous(
vec![d_out, d_in],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let x_shape = if promoted {
shape![d_in, n]
} else {
shape![n, d_in]
};
let (x_tensor, _) = TestInput::builder(client.clone(), x_shape)
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let (s_tensor, _) = TestInput::builder(client.clone(), shape![d_out, blocks])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let out = TestInput::builder(client.clone(), shape![d_out, n])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let w_op = launcher
.arg(w_tensor.binding(), u32::elem_type_native())
.gathered(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
))
.packed(field)
.vectorize(factor)
.build()
.unwrap();
let x_projection = if promoted {
Projection::new(
&[KB, KI, N],
&[
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
PhysicalAxisMap::of(N),
],
)
} else {
Projection::new(
&[N, KB, KI],
&[
PhysicalAxisMap::of(N),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)
};
let x_op = launcher
.arg(x_tensor.binding(), dtype)
.gathered(x_projection)
.vectorize(if promoted { 1 } else { factor })
.build()
.unwrap();
let s_op = launcher
.arg(s_tensor.binding(), dtype)
.axes(&[M, KB])
.build()
.unwrap();
let out_op = launcher
.arg(out.clone().binding(), dtype)
.axes(&[M, N])
.build()
.unwrap();
let (count, dim) = (launcher.cube_count(), launcher.cube_dim());
if promoted {
quantized_decode_promoted::launch(
&client,
count,
dim,
w_op.arg(),
x_op.arg(),
s_op.arg(),
out_op.arg(),
launcher.partitioning_arg(),
budget,
dtype,
);
} else {
quantized_decode::launch(
&client,
count,
dim,
w_op.arg(),
x_op.arg(),
s_op.arg(),
out_op.arg(),
launcher.partitioning_arg(),
budget,
);
}
let got = HostData::from_tensor_handle(&client, out, HostDataType::F32);
for m in 0..d_out {
let want: f32 = (0..d_in)
.map(|k| w[m * d_in + k] as f32 * s[m * blocks + k / block] * x[k])
.sum();
let have = got.get_f32(&[m, 0]);
assert!(
(have - want).abs() < 1e-2,
"at row {m}: got {have}, want {want}"
);
}
}