use crate::support::{HostData, HostDataType, TestInput, ValidationResult};
use cubecl::{
bytes::Bytes, features::TypeUsage, ir::FloatKind, ir::types::Fp8Format, prelude::*,
quant::scheme::QuantValue, std::tensor::TensorHandle, zspace::shape,
};
use cubecl_common::{e2m1, e4m3, e5m2};
use crate::tile::uncut;
use cubecl_tile::Instruction;
use cubecl_tile::kind::Field;
use cubecl_tile::layout::PhysicalAxisMap;
use cubecl_tile::*;
use half::f16;
use super::matmul::require_cmma_8x8x8_f32;
use super::{Form, implied};
const M: Axis = Axis(0);
const N: Axis = Axis(1);
const KB: Axis = Axis(2);
const KI: Axis = Axis(3);
const NB: Axis = Axis(4);
const NI: Axis = Axis(5);
const REGISTER_BLOCK: RegisterBlock = RegisterBlock::new(16);
#[cube(launch)]
fn packed_copy(input: &TileArg<'_>, output: &TileArg<'_>, space: Partitioning) {
let input = input.tile(&space);
let mut output = output.tile(&space);
output.copy_from(&input);
}
#[cube(launch)]
fn packed_matmul(
w: &TileArg<'_>,
x: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
) {
let w = w.tile(&space).mul(&scale.tile(&space));
let x = x.tile(&space);
let mut c = c.tile(&space);
c.zero();
for region in space.over(&level) {
let mut c_r = c
.at(®ion)
.accumulating(REGISTER_BLOCK, Semiring::SUM_PROD);
c_r.mma(&w.at(®ion), &x.at(®ion));
}
}
#[cube(launch)]
fn nvfp4_shaped_matmul(
w: &TileArg<'_>,
x: &TileArg<'_>,
blocks: &TileArg<'_>,
global: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
) {
let w = w
.tile(&space)
.mul(&blocks.tile(&space))
.mul(&global.tile(&space));
let x = x.tile(&space);
let mut c = c.tile(&space);
c.zero();
for region in space.over(&level) {
let mut c_r = c
.at(®ion)
.accumulating(REGISTER_BLOCK, Semiring::SUM_PROD);
c_r.mma(&w.at(®ion), &x.at(®ion));
}
}
#[test]
fn nvfp4_shaped_decode() {
let (field, rows, cols, block, blocks) = (QuantValue::E2M1, 4, 4, 16, 2);
let depth = block * blocks;
let factor = 32 / field.size_bits();
let client = cubecl::test_device().client();
let codes: Vec<u32> = (0..rows * depth).map(|i| (i % 16) as u32).collect();
let words: Vec<u32> = codes
.chunks(factor)
.map(|word| {
word.iter()
.enumerate()
.fold(0u32, |acc, (j, &c)| acc | (c << (j * field.size_bits())))
})
.collect();
let x: Vec<f32> = (0..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..rows * blocks).map(|i| (i as f32 + 1.0) / 2.0).collect();
let g = vec![0.25f32];
let dtype = f32::elem_type_native();
let w_tensor = TensorHandle::new_contiguous(
vec![rows, depth],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (x_tensor, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let (s_tensor, _) = TestInput::builder(client.clone(), shape![rows, blocks])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let (g_tensor, _) = TestInput::builder(client.clone(), shape![1])
.dtype(dtype)
.custom(g.clone())
.generate_with_f32_host_data();
let c = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, rows), (N, cols), (KB, blocks), (KI, block)]),
Levels::leaf(&[(M, rows), (N, cols), (KB, 1), (KI, factor)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
nvfp4_shaped_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
))
.packed(field),
),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
PhysicalAxisMap::of(N),
],
)),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB],
&[PhysicalAxisMap::of(M), PhysicalAxisMap::of(KB)],
)),
),
TileArgLaunch::new(
g_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(&[M, KB], &[PhysicalAxisMap::broadcast()])),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| {
let value = e2m1::from_bits(codes[m * depth + k] as u8).to_f32();
value * s[m * blocks + k / block] * g[0] * x[k * cols + n]
})
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[cube(launch)]
fn packed_matmul_rhs(
x: &TileArg<'_>,
w: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
) {
let x = x.tile(&space);
let w = w.tile(&space).mul(&scale.tile(&space));
let mut c = c.tile(&space);
c.zero();
for region in space.over(&level) {
let mut c_r = c
.at(®ion)
.accumulating(REGISTER_BLOCK, Semiring::SUM_PROD);
c_r.mma(&x.at(®ion), &w.at(®ion));
}
}
#[cube(launch)]
fn native_matmul(
w: &TileArg<'_>,
x: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
) {
let w = w.tile(&space);
let x = x.tile(&space);
let w = w.mul(&scale.tile(&space));
let mut c = c.tile(&space);
c.zero();
for region in space.over(&level) {
let mut c_r = c
.at(®ion)
.accumulating(REGISTER_BLOCK, Semiring::SUM_PROD);
c_r.mma(&w.at(®ion), &x.at(®ion));
}
}
#[cube(launch)]
fn packed_one_row_product<E: Numeric>(
x: &TileArg<'_>,
w: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[define(E)] _dtype: ElemType,
) {
let x = x.tile(&space);
let values = w.tile(&space);
let w = values.mul(&scale.tile(&space));
let c = c.tile(&space);
for cube in space {
let x = x.at(&cube);
let values = values.at(&cube);
let w = w.at(&cube);
let c = c.at(&cube);
let acc = c.accumulator::<E>(
&x,
&values,
comptime!(Instruction::Registers {
config: REGISTER_BLOCK
}),
Semiring::SUM_PROD,
);
for step in cube {
let mut acc_s = acc.at(&step);
acc_s.mma(&x.at(&step), &w.at(&step));
}
acc.drained_into(&c);
}
}
#[cube(launch)]
fn packed_matmul_byte_scales(
w: &TileArg<'_>,
x: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
) {
let w = w.tile(&space).mul(&scale.tile(&space));
let x = x.tile(&space);
let mut c = c.tile(&space);
c.zero();
for region in space.over(&level) {
let mut c_r = c
.at(®ion)
.accumulating(REGISTER_BLOCK, Semiring::SUM_PROD);
c_r.mma(&w.at(®ion), &x.at(®ion));
}
}
#[cube(launch)]
fn packed_one_row_product_byte_scales<E: Numeric>(
x: &TileArg<'_>,
w: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[define(E)] _dtype: ElemType,
) {
let x = x.tile(&space);
let values = w.tile(&space);
let w = values.mul(&scale.tile(&space));
let c = c.tile(&space);
for cube in space {
let x = x.at(&cube);
let values = values.at(&cube);
let w = w.at(&cube);
let c = c.at(&cube);
let acc = c.accumulator::<E>(
&x,
&values,
comptime!(Instruction::Registers {
config: REGISTER_BLOCK
}),
Semiring::SUM_PROD,
);
for step in cube {
let mut acc_s = acc.at(&step);
acc_s.mma(&x.at(&step), &w.at(&step));
}
acc.drained_into(&c);
}
}
#[cube(launch)]
#[allow(clippy::too_many_arguments)]
fn packed_cmma_rhs<E: Numeric>(
x: &TileArg<'_>,
w: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[define(E)] _dtype: ElemType,
) {
let x = x.tile(&space);
let w = w.tile(&space).mul(&scale.tile(&space));
let c = c.tile(&space);
let acc = c.accumulator::<E>(&x, &w, Instruction::Cmma, Semiring::SUM_PROD);
for region in space.over(&level).unrolled() {
let mut acc_r = acc.at(®ion);
acc_r.mma(&x.at(®ion), &w.at(®ion));
}
acc.drained_into(&c);
}
#[cube(launch)]
#[allow(clippy::too_many_arguments)]
fn packed_cmma_rhs_dequantized<E: Numeric>(
x: &TileArg<'_>,
w: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[comptime] element: ElemType,
#[define(E)] _dtype: ElemType,
) {
let x = x.tile(&space);
let w = w.tile(&space).mul(&scale.tile(&space));
let c = c.tile(&space);
let acc = c.accumulator::<E>(&x, &w, Instruction::Cmma, Semiring::SUM_PROD);
for region in space.over(&level).unrolled() {
let mut acc_r = acc.at(®ion);
let w_r = acc_r.dequantized(&w.at(®ion), element);
acc_r.mma(&x.at(®ion), &w_r);
}
acc.drained_into(&c);
}
#[test]
fn eight_bit_fields_unpack_on_read() {
let (field, rows, cols) = (QuantValue::Q8S, 8, 8);
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 span = 1i32 << bits;
let values: Vec<i32> = (0..rows * cols)
.map(|i| -(span / 2) + (i as i32 % span))
.collect();
let mask = (1u32 << bits) - 1;
let words: Vec<u32> = values
.chunks(factor)
.map(|word| {
word.iter()
.enumerate()
.fold(0u32, |acc, (j, &v)| acc | ((v as u32 & mask) << (j * bits)))
})
.collect();
let input = TensorHandle::new_contiguous(
vec![rows, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let dtype = f32::elem_type_native();
let output = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let space = Space::new(&[(M, rows), (N, cols)]);
packed_copy::launch(
&client,
CubeCount::new_single(),
CubeDim::new_single(),
TileArgLaunch::new(
input.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::direct(&[M, N]).packed(field),
),
TileArgLaunch::new(
output.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::direct(&[M, N]),
),
uncut(&client, &space, &space).partitioning_arg(),
);
let got = HostData::from_tensor_handle(&client, output, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want = values[m * cols + n] as f32;
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-6,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn four_bit_fields_unpack_on_read() {
let (field, rows, cols) = (QuantValue::Q4S, 8, 32);
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 span = 1i32 << bits;
let values: Vec<i32> = (0..rows * cols)
.map(|i| -(span / 2) + (i as i32 % span))
.collect();
let mask = (1u32 << bits) - 1;
let words: Vec<u32> = values
.chunks(factor)
.map(|word| {
word.iter()
.enumerate()
.fold(0u32, |acc, (j, &v)| acc | ((v as u32 & mask) << (j * bits)))
})
.collect();
let input = TensorHandle::new_contiguous(
vec![rows, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let dtype = f32::elem_type_native();
let output = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let space = Space::new(&[(M, rows), (N, cols)]);
packed_copy::launch(
&client,
CubeCount::new_single(),
CubeDim::new_single(),
TileArgLaunch::new(
input.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::direct(&[M, N]).packed(field),
),
TileArgLaunch::new(
output.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::direct(&[M, N]),
),
uncut(&client, &space, &space).partitioning_arg(),
);
let got = HostData::from_tensor_handle(&client, output, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want = values[m * cols + n] as f32;
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-6,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn fp4_codes_unpack_on_read() {
let client = cubecl::test_device().client();
check_fp4_codes_unpack(&client, Field::from(QuantValue::E2M1));
}
#[test]
fn fp4_codes_unpack_as_the_device_reads_them() {
let client = cubecl::test_device().client();
let field = Field::from(QuantValue::E2M1).read_on(&client);
if field == Field::from(QuantValue::E2M1) {
ValidationResult::Skipped(
"the device reads e2m1 as the plain field, which fp4_codes_unpack_on_read checks"
.to_string(),
)
.enforce();
return;
}
check_fp4_codes_unpack(&client, field);
}
#[test]
fn fp4_codes_unpack_placed() {
let client = cubecl::test_device().client();
check_fp4_codes_unpack(
&client,
Field::Placed {
value: QuantValue::E2M1,
lifted: true,
},
);
}
fn check_fp4_codes_unpack(client: &Client, field: Field) {
let (rows, cols) = (4, 32);
let bits = field.size_bits();
let factor = 32 / bits;
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 codes: Vec<u32> = (0..rows * cols).map(|i| (i % 16) as u32).collect();
let words: Vec<u32> = codes
.chunks(factor)
.map(|word| {
word.iter()
.enumerate()
.fold(0u32, |acc, (j, &c)| acc | (c << (j * bits)))
})
.collect();
let input = TensorHandle::new_contiguous(
vec![rows, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let dtype = f32::elem_type_native();
let output = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let space = Space::new(&[(M, rows), (N, cols)]);
packed_copy::launch(
client,
CubeCount::new_single(),
CubeDim::new_single(),
TileArgLaunch::new(
input.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::direct(&[M, N]).packed(field),
),
TileArgLaunch::new(
output.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::direct(&[M, N]),
),
uncut(client, &space, &space).partitioning_arg(),
);
let got = HostData::from_tensor_handle(client, output, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want = e2m1::from_bits(codes[m * cols + n] as u8).to_f32();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-6,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn two_bit_fields_unpack_on_read() {
let (field, rows, cols) = (QuantValue::Q2S, 4, 32);
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 span = 1i32 << bits;
let values: Vec<i32> = (0..rows * cols)
.map(|i| -(span / 2) + (i as i32 % span))
.collect();
let mask = (1u32 << bits) - 1;
let words: Vec<u32> = values
.chunks(factor)
.map(|word| {
word.iter()
.enumerate()
.fold(0u32, |acc, (j, &v)| acc | ((v as u32 & mask) << (j * bits)))
})
.collect();
let input = TensorHandle::new_contiguous(
vec![rows, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let dtype = f32::elem_type_native();
let output = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let space = Space::new(&[(M, rows), (N, cols)]);
packed_copy::launch(
&client,
CubeCount::new_single(),
CubeDim::new_single(),
TileArgLaunch::new(
input.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::direct(&[M, N]).packed(field),
),
TileArgLaunch::new(
output.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::direct(&[M, N]),
),
uncut(&client, &space, &space).partitioning_arg(),
);
let got = HostData::from_tensor_handle(&client, output, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want = values[m * cols + n] as f32;
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-6,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn a_packed_operand_contracts_against_its_scales() {
let (field, rows, cols, block, blocks) = (QuantValue::Q4S, 4, 4, 8, 4);
let depth = block * blocks;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 span = 1i32 << bits;
let w: Vec<i32> = (0..rows * depth)
.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..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..rows * blocks).map(|i| (i as f32 + 1.0) / 2.0).collect();
let dtype = f32::elem_type_native();
let w_tensor = TensorHandle::new_contiguous(
vec![rows, depth],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (x_tensor, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let (s_tensor, _) = TestInput::builder(client.clone(), shape![rows, blocks])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let c = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, rows), (N, cols), (KB, blocks), (KI, block)]),
Levels::leaf(&[(M, rows), (N, cols), (KB, 1), (KI, factor)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
packed_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
))
.packed(field),
),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
PhysicalAxisMap::of(N),
],
)),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB],
&[PhysicalAxisMap::of(M), PhysicalAxisMap::of(KB)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| w[m * depth + k] as f32 * s[m * blocks + k / block] * x[k * cols + n])
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn eight_bit_fields_contract_against_their_scales() {
let (field, rows, cols, block, blocks) = (QuantValue::Q8S, 4, 4, 8, 4);
let depth = block * blocks;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 span = 1i32 << bits;
let w: Vec<i32> = (0..rows * depth)
.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..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..rows * blocks).map(|i| (i as f32 + 1.0) / 2.0).collect();
let dtype = f32::elem_type_native();
let w_tensor = TensorHandle::new_contiguous(
vec![rows, depth],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (x_tensor, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let (s_tensor, _) = TestInput::builder(client.clone(), shape![rows, blocks])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let c = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, rows), (N, cols), (KB, blocks), (KI, block)]),
Levels::leaf(&[(M, rows), (N, cols), (KB, 1), (KI, factor)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
packed_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
))
.packed(field),
),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
PhysicalAxisMap::of(N),
],
)),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB],
&[PhysicalAxisMap::of(M), PhysicalAxisMap::of(KB)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| w[m * depth + k] as f32 * s[m * blocks + k / block] * x[k * cols + n])
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn a_summing_walk_takes_its_scales_several_at_a_time() {
let (field, rows, cols, block, blocks) = (QuantValue::Q4S, 4, 4, 8, 4);
let depth = block * blocks;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 span = 1i32 << bits;
let w: Vec<i32> = (0..rows * depth)
.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..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..rows * blocks).map(|i| (i as f32 + 1.0) / 2.0).collect();
let dtype = f32::elem_type_native();
let w_tensor = TensorHandle::new_contiguous(
vec![rows, depth],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (x_tensor, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let (s_tensor, _) = TestInput::builder(client.clone(), shape![rows, blocks])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let c = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, rows), (N, cols), (KB, blocks), (KI, block)]),
Levels::leaf(&[(M, rows), (N, cols), (KB, 2), (KI, factor)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
packed_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
))
.packed(field),
),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
PhysicalAxisMap::of(N),
],
)),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
dtype,
2,
TileSpec::new(Projection::new(
&[M, KB],
&[PhysicalAxisMap::of(M), PhysicalAxisMap::of(KB)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| w[m * depth + k] as f32 * s[m * blocks + k / block] * x[k * cols + n])
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn a_packed_rhs_contracts_against_its_scales() {
let (field, rows, block_k, blocks_k) = (QuantValue::Q4S, 4, 8, 4);
let depth = block_k * blocks_k;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 (cols, bn) = (factor * 2, factor);
let blocks_n = cols / bn;
let x: Vec<f32> = (0..rows * depth).map(|i| (i % 7) as f32 - 3.0).collect();
let span = 1i32 << bits;
let w: Vec<i32> = (0..depth * cols)
.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 s: Vec<f32> = (0..blocks_k * blocks_n)
.map(|i| (i as f32 + 1.0) / 2.0)
.collect();
let dtype = f32::elem_type_native();
let (x_tensor, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let w_tensor = TensorHandle::new_contiguous(
vec![depth, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (s_tensor, _) = TestInput::builder(client.clone(), shape![blocks_k, blocks_n])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let c = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[
(M, rows),
(NB, blocks_n),
(NI, bn),
(KB, blocks_k),
(KI, block_k),
]),
Levels::leaf(&[(M, rows), (NB, blocks_n), (NI, bn), (KB, 1), (KI, block_k)])
.walk_every(&[M, NB, NI, KB, KI])
.build(),
),
Form::Static,
);
packed_matmul_rhs::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[KB, KI, NB, NI],
&[
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
))
.packed(field),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, NB],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(NB)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::new(Projection::new(
&[M, NB, NI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
)),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| {
x[m * depth + k] * w[k * cols + n] as f32 * s[(k / block_k) * blocks_n + n / bn]
})
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn an_eight_bit_packed_rhs_contracts_against_its_scales() {
let (field, rows, block_k, blocks_k) = (QuantValue::Q8S, 4, 8, 4);
let depth = block_k * blocks_k;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 (cols, bn) = (factor * 2, factor);
let blocks_n = cols / bn;
let x: Vec<f32> = (0..rows * depth).map(|i| (i % 7) as f32 - 3.0).collect();
let span = 1i32 << bits;
let w: Vec<i32> = (0..depth * cols)
.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 s: Vec<f32> = (0..blocks_k * blocks_n)
.map(|i| (i as f32 + 1.0) / 2.0)
.collect();
let dtype = f32::elem_type_native();
let (x_tensor, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let w_tensor = TensorHandle::new_contiguous(
vec![depth, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (s_tensor, _) = TestInput::builder(client.clone(), shape![blocks_k, blocks_n])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let c = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[
(M, rows),
(NB, blocks_n),
(NI, bn),
(KB, blocks_k),
(KI, block_k),
]),
Levels::leaf(&[(M, rows), (NB, blocks_n), (NI, bn), (KB, 1), (KI, block_k)])
.walk_every(&[M, NB, NI, KB, KI])
.build(),
),
Form::Static,
);
packed_matmul_rhs::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[KB, KI, NB, NI],
&[
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
))
.packed(field),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, NB],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(NB)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::new(Projection::new(
&[M, NB, NI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
)),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| {
x[m * depth + k] * w[k * cols + n] as f32 * s[(k / block_k) * blocks_n + n / bn]
})
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn several_lines_may_share_one_scale() {
let (field, rows, block_k, blocks_k) = (QuantValue::Q8S, 4, 8, 4);
let depth = block_k * blocks_k;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 (cols, bn) = (factor * 2, factor * 2);
let blocks_n = cols / bn;
let x: Vec<f32> = (0..rows * depth).map(|i| (i % 7) as f32 - 3.0).collect();
let span = 1i32 << bits;
let w: Vec<i32> = (0..depth * cols)
.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 s: Vec<f32> = (0..blocks_k * blocks_n)
.map(|i| (i as f32 + 1.0) / 2.0)
.collect();
let dtype = f32::elem_type_native();
let (x_tensor, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let w_tensor = TensorHandle::new_contiguous(
vec![depth, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (s_tensor, _) = TestInput::builder(client.clone(), shape![blocks_k, blocks_n])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let c = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[
(M, rows),
(NB, blocks_n),
(NI, bn),
(KB, blocks_k),
(KI, block_k),
]),
Levels::leaf(&[(M, rows), (NB, blocks_n), (NI, bn), (KB, 1), (KI, block_k)])
.walk_every(&[M, NB, NI, KB, KI])
.build(),
),
Form::Static,
);
packed_matmul_rhs::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[KB, KI, NB, NI],
&[
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
))
.packed(field),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, NB],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(NB)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::new(Projection::new(
&[M, NB, NI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
)),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| {
x[m * depth + k] * w[k * cols + n] as f32 * s[(k / block_k) * blocks_n + n / bn]
})
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn an_i8_operand_contracts_against_its_scales() {
let (rows, cols, block, blocks) = (4, 4, 8, 4);
let depth = block * blocks;
let client = cubecl::test_device().client();
if !i8::supported_uses(&client).contains(TypeUsage::Conversion) {
ValidationResult::Skipped("backend has no native i8".to_string()).enforce();
return;
}
let w: Vec<i32> = (0..rows * depth).map(|i| -128 + (i as i32 % 256)).collect();
let x: Vec<f32> = (0..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..rows * blocks).map(|i| (i as f32 + 1.0) / 2.0).collect();
let dtype = f32::elem_type_native();
let (w_tensor, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(i8::elem_type_native())
.custom(w.iter().map(|&v| v as f32).collect::<Vec<_>>())
.generate_with_f32_host_data();
let (x_tensor, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let (s_tensor, _) = TestInput::builder(client.clone(), shape![rows, blocks])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let c = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, rows), (N, cols), (KB, blocks), (KI, block)]),
Levels::leaf(&[(M, rows), (N, cols), (KB, 1), (KI, block)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
native_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
i8::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
PhysicalAxisMap::of(N),
],
)),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB],
&[PhysicalAxisMap::of(M), PhysicalAxisMap::of(KB)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| w[m * depth + k] as f32 * s[m * blocks + k / block] * x[k * cols + n])
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn a_packed_decode_runs_in_this_spelling() {
let (field, block_k, blocks_k) = (QuantValue::Q4S, 8, 4);
let depth = block_k * blocks_k;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 (cols, bn) = (factor * 2, factor);
let blocks_n = cols / bn;
let x: Vec<f32> = (0..depth).map(|i| (i % 7) as f32 - 3.0).collect();
let span = 1i32 << bits;
let w: Vec<i32> = (0..depth * cols)
.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 s: Vec<f32> = (0..blocks_k * blocks_n)
.map(|i| (i as f32 + 1.0) / 2.0)
.collect();
let dtype = f32::elem_type_native();
let (x_tensor, _) = TestInput::builder(client.clone(), shape![1, depth])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let w_tensor = TensorHandle::new_contiguous(
vec![depth, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (s_tensor, _) = TestInput::builder(client.clone(), shape![blocks_k, blocks_n])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let c = TestInput::builder(client.clone(), shape![1, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[
(M, 1),
(NB, blocks_n),
(NI, bn),
(KB, blocks_k),
(KI, block_k),
]),
Levels::leaf(&[(NB, 1), (KB, 1)])
.walk_every(&[KB])
.cubes(&[NB])
.build(),
),
Form::Static,
);
packed_one_row_product::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[KB, KI, NB, NI],
&[
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
))
.packed(field),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, NB],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(NB)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::new(Projection::new(
&[M, NB, NI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
)),
),
launcher.partitioning_arg(),
dtype,
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| x[k] * w[k * cols + n] as f32 * s[(k / block_k) * blocks_n + n / bn])
.sum();
let have = got.get_f32(&[0, n]);
assert!(
(have - want).abs() < 1e-3,
"at {n}: got {have}, want {want}"
);
}
}
#[test]
fn an_eight_bit_decode_runs_in_this_spelling() {
let (field, block_k, blocks_k) = (QuantValue::Q8S, 8, 4);
let depth = block_k * blocks_k;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 (cols, bn) = (factor * 2, factor);
let blocks_n = cols / bn;
let x: Vec<f32> = (0..depth).map(|i| (i % 7) as f32 - 3.0).collect();
let span = 1i32 << bits;
let w: Vec<i32> = (0..depth * cols)
.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 s: Vec<f32> = (0..blocks_k * blocks_n)
.map(|i| (i as f32 + 1.0) / 2.0)
.collect();
let dtype = f32::elem_type_native();
let (x_tensor, _) = TestInput::builder(client.clone(), shape![1, depth])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let w_tensor = TensorHandle::new_contiguous(
vec![depth, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (s_tensor, _) = TestInput::builder(client.clone(), shape![blocks_k, blocks_n])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let c = TestInput::builder(client.clone(), shape![1, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[
(M, 1),
(NB, blocks_n),
(NI, bn),
(KB, blocks_k),
(KI, block_k),
]),
Levels::leaf(&[(NB, 1), (KB, 1)])
.walk_every(&[KB])
.cubes(&[NB])
.build(),
),
Form::Static,
);
packed_one_row_product::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[KB, KI, NB, NI],
&[
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
))
.packed(field),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, NB],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(NB)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::new(Projection::new(
&[M, NB, NI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
)),
),
launcher.partitioning_arg(),
dtype,
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| x[k] * w[k * cols + n] as f32 * s[(k / block_k) * blocks_n + n / bn])
.sum();
let have = got.get_f32(&[0, n]);
assert!(
(have - want).abs() < 1e-3,
"at {n}: got {have}, want {want}"
);
}
}
#[cube(launch)]
fn packed_one_row_product_unscaled<E: Numeric>(
x: &TileArg<'_>,
w: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[define(E)] _dtype: ElemType,
) {
let x = x.tile(&space);
let w = w.tile(&space);
let c = c.tile(&space);
for cube in space {
let x = x.at(&cube);
let w = w.at(&cube);
let c = c.at(&cube);
let acc = c.accumulator::<E>(
&x,
&w,
comptime!(Instruction::Registers {
config: REGISTER_BLOCK
}),
Semiring::SUM_PROD,
);
for step in cube {
let mut acc_s = acc.at(&step);
acc_s.mma(&x.at(&step), &w.at(&step));
}
acc.drained_into(&c);
}
}
#[test]
fn a_packed_rhs_drains_from_a_promoted_accumulator() {
let (field, block_k, blocks_k) = (QuantValue::Q8S, 8, 4);
let depth = block_k * blocks_k;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 (cols, bn) = (factor * 2, factor);
let x: Vec<f32> = (0..depth).map(|i| (i % 7) as f32 - 3.0).collect();
let span = 1i32 << bits;
let w: Vec<i32> = (0..depth * cols)
.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 dtype = f32::elem_type_native();
let (x_tensor, _) = TestInput::builder(client.clone(), shape![1, depth])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let w_tensor = TensorHandle::new_contiguous(
vec![depth, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let c = TestInput::builder(client.clone(), shape![1, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, 1), (N, cols), (KB, blocks_k), (KI, block_k)]),
Levels::leaf(&[(N, bn), (KB, 1)])
.walk_every(&[KB])
.cubes(&[N])
.build(),
),
Form::Static,
);
packed_one_row_product_unscaled::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
PhysicalAxisMap::of(N),
],
))
.packed(field),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
dtype,
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for n in 0..cols {
let want: f32 = (0..depth).map(|k| x[k] * w[k * cols + n] as f32).sum();
let have = got.get_f32(&[0, n]);
assert!(
(have - want).abs() < 1e-3,
"at col {n}: got {have}, want {want}"
);
}
}
fn words_of_bytes(bytes: &[u8]) -> Vec<u32> {
bytes
.chunks(4)
.map(|word| {
word.iter()
.enumerate()
.fold(0u32, |acc, (j, &b)| acc | ((b as u32) << (j * 8)))
})
.collect()
}
#[test]
fn fp8_fields_unpack_on_read() {
let client = cubecl::test_device().client();
for value in [QuantValue::E4M3, QuantValue::E5M2] {
check_fp8_codes_unpack(&client, value, Field::from(value));
}
}
#[test]
fn fp8_codes_unpack_placed() {
let client = cubecl::test_device().client();
for value in [QuantValue::E4M3, QuantValue::E5M2] {
let placed = Field::Placed {
value,
lifted: true,
};
if Field::from(value).read_on(&client) != placed {
ValidationResult::Skipped(format!(
"{value:?}: this device converts the codes, it never reads them placed"
))
.enforce();
continue;
}
check_fp8_codes_unpack(&client, value, placed);
}
}
#[test]
fn fp8_codes_unpack_as_the_device_reads_them() {
let client = cubecl::test_device().client();
for value in [QuantValue::E4M3, QuantValue::E5M2] {
check_fp8_codes_unpack(&client, value, Field::from(value).read_on(&client));
}
}
fn check_fp8_codes_unpack(client: &Client, value: QuantValue, field: Field) {
let (rows, cols) = (16, 16);
let factor = 32 / field.size_bits();
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 decode = |bits: u8| match value {
QuantValue::E4M3 => e4m3::from_bits(bits).to_f32(),
QuantValue::E5M2 => e5m2::from_bits(bits).to_f32(),
other => panic!("check_fp8_codes_unpack: {other:?} is not an 8-bit float"),
};
let bytes: Vec<u8> = (0..=u8::MAX)
.map(|bits| if decode(bits).is_nan() { 0 } else { bits })
.collect();
let input = TensorHandle::new_contiguous(
vec![rows, cols],
client.create(Bytes::from_elems(words_of_bytes(&bytes))),
u32::elem_type_native(),
);
let dtype = f32::elem_type_native();
let output = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let space = Space::new(&[(M, rows), (N, cols)]);
packed_copy::launch(
client,
CubeCount::new_single(),
CubeDim::new_single(),
TileArgLaunch::new(
input.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::direct(&[M, N]).packed(field),
),
TileArgLaunch::new(
output.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::direct(&[M, N]),
),
uncut(client, &space, &space).partitioning_arg(),
);
let got = HostData::from_tensor_handle(client, output, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want = decode(bytes[m * cols + n]);
let have = got.get_f32(&[m, n]);
assert!(
have.to_bits() == want.to_bits(),
"{field:?} at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn ue8m0_scales_are_read_as_bytes() {
check_ue8m0_scales(8, 4);
}
fn check_ue8m0_scales(block: usize, blocks: usize) {
let (field, rows, cols) = (QuantValue::Q8S, 4, 4);
let depth = block * blocks;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 span = 1i32 << bits;
let w: Vec<i32> = (0..rows * depth)
.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..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let exponents: Vec<i32> = (0..rows * blocks).map(|i| (i % 5) as i32 - 2).collect();
let s: Vec<f32> = exponents.iter().map(|&e| 2f32.powi(e)).collect();
let codes: Vec<u8> = exponents.iter().map(|&e| (e + 127) as u8).collect();
let dtype = f32::elem_type_native();
let w_tensor = TensorHandle::new_contiguous(
vec![rows, depth],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (x_tensor, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let s_tensor = TensorHandle::new_contiguous(
vec![rows, blocks],
client.create(Bytes::from_elems(words_of_bytes(&codes))),
u32::elem_type_native(),
);
let c = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, rows), (N, cols), (KB, blocks), (KI, block)]),
Levels::leaf(&[(M, rows), (N, cols), (KB, 4), (KI, factor)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
let w_projection = Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
);
packed_matmul_byte_scales::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(w_projection.clone()).packed(field),
),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
PhysicalAxisMap::of(N),
],
)),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(w_projection.scales_per(KB)).packed(Fp8Format::UE8M0),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| w[m * depth + k] as f32 * s[m * blocks + k / block] * x[k * cols + n])
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn half_scales_are_read_as_fields_of_a_word() {
check_float_scales(FloatKind::F16, 16, 2);
}
#[test]
fn full_width_scales_fill_the_word_they_are_read_from() {
check_float_scales(FloatKind::F32, 8, 4);
}
fn check_float_scales(kind: FloatKind, block: usize, blocks: usize) {
let (field, rows, cols) = (QuantValue::Q8S, 4, 4);
let depth = block * blocks;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 span = 1i32 << bits;
let w: Vec<i32> = (0..rows * depth)
.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..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..rows * blocks)
.map(|i| 0.25 * (1 + i % 4) as f32)
.collect();
let scale_words = match kind {
FloatKind::F32 => s.iter().map(|v| v.to_bits()).collect(),
_ => s
.chunks(2)
.map(|pair| {
pair.iter().enumerate().fold(0u32, |acc, (j, &v)| {
acc | ((f16::from_f32(v).to_bits() as u32) << (j * 16))
})
})
.collect::<Vec<u32>>(),
};
let dtype = f32::elem_type_native();
let w_tensor = TensorHandle::new_contiguous(
vec![rows, depth],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (x_tensor, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let s_tensor = TensorHandle::new_contiguous(
vec![rows, blocks],
client.create(Bytes::from_elems(scale_words)),
u32::elem_type_native(),
);
let c = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, rows), (N, cols), (KB, blocks), (KI, block)]),
Levels::leaf(&[
(M, rows),
(N, cols),
(KB, Field::of_float(kind).per_word()),
(KI, factor),
])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
let w_projection = Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
);
packed_matmul_byte_scales::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(w_projection.clone()).packed(field),
),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
PhysicalAxisMap::of(N),
],
)),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(w_projection.scales_per(KB)).packed(kind),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| w[m * depth + k] as f32 * s[m * blocks + k / block] * x[k * cols + n])
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn e4m3_scales_reach_the_promoted_block() {
let (field, block_k, blocks_k) = (QuantValue::Q8S, 8, 4);
let depth = block_k * blocks_k;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 (cols, bn) = (factor * 4, factor);
let blocks_n = cols / bn;
let x: Vec<f32> = (0..depth).map(|i| (i % 7) as f32 - 3.0).collect();
let span = 1i32 << bits;
let w: Vec<i32> = (0..depth * cols)
.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 s: Vec<f32> = (0..blocks_k * blocks_n)
.map(|i| (i as f32 + 1.0) / 2.0)
.collect();
let codes: Vec<u8> = s.iter().map(|&v| e4m3::from_f32(v).to_bits()).collect();
let dtype = f32::elem_type_native();
let (x_tensor, _) = TestInput::builder(client.clone(), shape![1, depth])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let w_tensor = TensorHandle::new_contiguous(
vec![depth, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let s_tensor = TensorHandle::new_contiguous(
vec![blocks_k, blocks_n],
client.create(Bytes::from_elems(words_of_bytes(&codes))),
u32::elem_type_native(),
);
let c = TestInput::builder(client.clone(), shape![1, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[
(M, 1),
(NB, blocks_n),
(NI, bn),
(KB, blocks_k),
(KI, block_k),
]),
Levels::leaf(&[(NB, 4), (KB, 1)])
.walk_every(&[KB])
.cubes(&[NB])
.build(),
),
Form::Static,
);
packed_one_row_product_byte_scales::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
x_tensor.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
w_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[KB, KI, NB, NI],
&[
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
))
.packed(field),
),
TileArgLaunch::new(
s_tensor.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[KB, KI, NB],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(NB)],
))
.packed(QuantValue::E4M3),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::new(Projection::new(
&[M, NB, NI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
)),
),
launcher.partitioning_arg(),
dtype,
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| x[k] * w[k * cols + n] as f32 * s[(k / block_k) * blocks_n + n / bn])
.sum();
let have = got.get_f32(&[0, n]);
assert!(
(have - want).abs() < 1e-3,
"at {n}: got {have}, want {want}"
);
}
}
#[test]
fn a_packed_rhs_reaches_the_tensor_cores() {
let (field, rows, block_k, blocks_k) = (QuantValue::Q8S, 8, 8, 4);
let depth = block_k * blocks_k;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_device().client();
if !require_cmma_8x8x8_f32(&client) {
return;
}
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 (cols, bn) = (factor * 4, factor);
let blocks_n = cols / bn;
let x: Vec<f32> = (0..rows * depth).map(|i| (i % 7) as f32 - 3.0).collect();
let span = 1i32 << bits;
let w: Vec<i32> = (0..depth * cols)
.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 s: Vec<f32> = (0..blocks_k * blocks_n)
.map(|i| (i as f32 + 1.0) / 2.0)
.collect();
let scale_words: Vec<u32> = s
.chunks(2)
.map(|pair| {
(f16::from_f32(pair[0]).to_bits() as u32)
| ((f16::from_f32(pair[1]).to_bits() as u32) << 16)
})
.collect();
let dtype = f32::elem_type_native();
let (x_tensor, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(x.clone())
.generate_with_f32_host_data();
let w_tensor = TensorHandle::new_contiguous(
vec![depth, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let s_tensor = TensorHandle::new_contiguous(
vec![blocks_k, blocks_n],
client.create(Bytes::from_elems(scale_words)),
u32::elem_type_native(),
);
let c = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[
(M, rows),
(NB, blocks_n),
(NI, bn),
(KB, blocks_k),
(KI, block_k),
]),
Levels::leaf(&[(M, rows), (NB, 8 / bn), (NI, bn), (KB, 1), (KI, block_k)])
.walk_every(&[M, NB, NI, KB, KI])
.build(),
),
Form::Static,
);
let x_arg = || {
TileArgLaunch::new(
x_tensor.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
],
)),
)
};
let w_arg = || {
TileArgLaunch::new(
w_tensor.clone().binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[KB, KI, NB, NI],
&[
PhysicalAxisMap::disjoint(&[(KB, block_k), (KI, 1)]),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
))
.packed(field),
)
};
let s_arg = || {
TileArgLaunch::new(
s_tensor.clone().binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[KB, KI, NB],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(NB)],
))
.packed(FloatKind::F16),
)
};
let out_arg = |out: &TensorHandle| {
TileArgLaunch::new(
out.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, NB, NI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(NB, bn), (NI, 1)]),
],
)),
)
};
packed_cmma_rhs::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
x_arg(),
w_arg(),
s_arg(),
out_arg(&c),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
dtype,
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| {
x[m * depth + k] * w[k * cols + n] as f32 * s[(k / block_k) * blocks_n + n / bn]
})
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
let dequantized = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
packed_cmma_rhs_dequantized::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
x_arg(),
w_arg(),
s_arg(),
out_arg(&dequantized),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
dtype,
dtype,
);
let explicit = HostData::from_tensor_handle(&client, dequantized, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
assert_eq!(
explicit.get_f32(&[m, n]),
got.get_f32(&[m, n]),
"at ({m}, {n}): the explicit dequantize differs from the leaf's"
);
}
}
}
#[cube(launch)]
fn packed_scaled_copy(
input: &TileArg<'_>,
scales: &TileArg<'_>,
output: &TileArg<'_>,
space: Partitioning,
) {
let input = input.tile(&space);
let scales = scales.tile(&space);
let mut output = output.tile(&space);
output.dequantize_from(&input.mul(&scales));
}
#[test]
fn a_copy_of_scaled_packed_fields_decodes_them() {
let (field, rows, block, blocks) = (QuantValue::Q8S, 8, 16, 2);
let cols = block * blocks;
let bits = field.size_bits();
let factor = 32 / bits;
let client = cubecl::test_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 span = 1i32 << bits;
let values: Vec<i32> = (0..rows * cols)
.map(|i| -(span / 2) + (i as i32 % span))
.collect();
let mask = (1u32 << bits) - 1;
let words: Vec<u32> = values
.chunks(factor)
.map(|word| {
word.iter()
.enumerate()
.fold(0u32, |acc, (j, &v)| acc | ((v as u32 & mask) << (j * bits)))
})
.collect();
let input = TensorHandle::new_contiguous(
vec![rows, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let scales: Vec<f32> = (0..rows * blocks).map(|i| (i as f32 + 1.0) / 4.0).collect();
let (scales_t, _) = TestInput::builder(client.clone(), shape![rows, blocks])
.dtype(f32::elem_type_native())
.custom(scales.clone())
.generate_with_f32_host_data();
let dtype = f32::elem_type_native();
let output = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let space = Space::new(&[(M, rows), (KB, blocks), (KI, block)]);
let projection = Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
);
packed_scaled_copy::launch(
&client,
CubeCount::new_single(),
CubeDim::new_single(),
TileArgLaunch::new(
input.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(projection.clone()).packed(field),
),
TileArgLaunch::new(
scales_t.binding().into_tensor_arg(),
f32::elem_type_native(),
1,
TileSpec::new(projection.scales_per(KB)),
),
TileArgLaunch::new(
output.clone().binding().into_tensor_arg(),
dtype,
factor,
TileSpec::new(projection),
),
uncut(&client, &space, &space).partitioning_arg(),
);
let got = HostData::from_tensor_handle(&client, output, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want = values[m * cols + n] as f32 * scales[m * blocks + n / block];
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-6,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
const TBL: Axis = Axis(6);
#[cube(launch)]
fn looked_up_copy(input: &TileArg<'_>, output: &TileArg<'_>, space: Partitioning) {
let input = input.tile(&space);
let mut output = output.tile(&space);
output.dequantize_from(&input);
}
#[test]
fn a_copy_of_looked_up_fields_decodes_them() {
check_looked_up_copy(8, 4);
}
#[test]
fn a_copy_of_looked_up_fields_narrower_than_a_word_decodes_them() {
check_looked_up_copy(4, 4);
check_looked_up_copy(2, 4);
}
fn check_looked_up_copy(bits: usize, width: usize) {
let (rows, block, blocks) = (8, 16, 2);
let cols = block * blocks;
let factor = 32 / bits;
let entries = 1usize << bits;
let client = cubecl::test_device().client();
let max = client.properties().hardware.max_vector_size;
if width > max {
ValidationResult::Skipped(format!(
"device vectors cap at {max}, below the {width}-wide line"
))
.enforce();
return;
}
let indices: Vec<u32> = (0..rows * cols)
.map(|i| ((i * 37 + 3) % entries) as u32)
.collect();
let words: Vec<u32> = indices
.chunks(factor)
.map(|word| {
word.iter()
.enumerate()
.fold(0u32, |acc, (j, &v)| acc | (v << (j * bits)))
})
.collect();
let input = TensorHandle::new_contiguous(
vec![rows, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let table: Vec<f32> = (0..entries).map(|i| (i as f32 - 127.5) / 3.0).collect();
let (table_t, _) = TestInput::builder(client.clone(), shape![entries])
.dtype(f32::elem_type_native())
.custom(table.clone())
.generate_with_f32_host_data();
let scales: Vec<f32> = (0..rows * blocks).map(|i| (i as f32 + 1.0) / 4.0).collect();
let (scales_t, _) = TestInput::builder(client.clone(), shape![rows, blocks])
.dtype(f32::elem_type_native())
.custom(scales.clone())
.generate_with_f32_host_data();
let dtype = f32::elem_type_native();
let output = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let space = Space::new(&[(M, rows), (KB, blocks), (KI, block)]);
let projection = Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
);
let input = TileArgLaunch::new(
input.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(projection.clone()).packed(Field::Index { bits }),
)
.codebook(TileArgLaunch::new(
table_t.binding().into_tensor_arg(),
f32::elem_type_native(),
1,
TileSpec::direct(&[TBL]),
))
.expect("a plain table of an entry per index")
.scales(TileArgLaunch::new(
scales_t.binding().into_tensor_arg(),
f32::elem_type_native(),
1,
TileSpec::new(projection.scales_per(KB)),
));
looked_up_copy::launch(
&client,
CubeCount::new_single(),
CubeDim::new_single(),
input,
TileArgLaunch::new(
output.clone().binding().into_tensor_arg(),
dtype,
width,
TileSpec::new(projection),
),
uncut(&client, &space, &space).partitioning_arg(),
);
let got = HostData::from_tensor_handle(&client, output, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want = table[indices[m * cols + n] as usize] * scales[m * blocks + n / block];
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-6,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum LineRead {
Masked,
Untested,
}
#[cube(launch)]
fn decoded_lines<N: Size>(
input: &TileArg<'_>,
output: &mut Tensor<Vector<f32, N>>,
space: Partitioning,
#[comptime] lines: usize,
#[comptime] read: LineRead,
) {
let input = input.tile(&space);
let decoded = input.decoded::<f32, N>();
let at = ABSOLUTE_POS;
let (row, line) = (at / lines, at % lines);
if at < output.len() {
output[at] = match comptime!(read) {
LineRead::Masked => decoded.read(row, line),
LineRead::Untested => decoded.read_unchecked(row, line),
};
}
}
#[test]
fn a_decoded_read_of_looked_up_fields_matches_the_copy() {
for (bits, width) in [(8, 4), (4, 4), (2, 4), (4, 2), (16, 4), (8, 8), (4, 16)] {
check_decoded_lines(Field::Index { bits }, width, LineRead::Masked);
}
}
#[test]
fn an_untested_decoded_read_matches_the_masked_one() {
for (bits, width) in [(8, 4), (2, 4), (16, 4)] {
check_decoded_lines(Field::Index { bits }, width, LineRead::Untested);
}
}
#[test]
fn a_decoded_read_of_integer_fields_scales_them() {
check_decoded_lines(QuantValue::Q4S.into(), 2, LineRead::Masked);
}
fn check_decoded_lines(field: Field, width: usize, read: LineRead) {
let (rows, cols) = (4, 64);
let bits = field.size_bits();
let factor = 32 / bits;
let span = 1usize << bits;
let client = cubecl::test_device().client();
let max = client.properties().hardware.max_vector_size;
if width > max {
ValidationResult::Skipped(format!(
"device vectors cap at {max}, below the {width}-wide line"
))
.enforce();
return;
}
let raw: Vec<u32> = (0..rows * cols)
.map(|i| ((i * 37 + 3) % span) as u32)
.collect();
let words: Vec<u32> = raw
.chunks(factor)
.map(|word| {
word.iter()
.enumerate()
.fold(0u32, |acc, (j, &v)| acc | (v << (j * bits)))
})
.collect();
let input = TensorHandle::new_contiguous(
vec![rows, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let table: Vec<f32> = (0..span).map(|i| (i as f32 - 127.5) / 3.0).collect();
let decode = |raw: u32| match field {
Field::Index { .. } => table[raw as usize],
_ => (((raw << (32 - bits)) as i32) >> (32 - bits)) as f32,
};
let scales: Vec<f32> = (0..rows).map(|i| (i as f32 + 1.0) / 4.0).collect();
let (scales_t, _) = TestInput::builder(client.clone(), shape![rows])
.dtype(f32::elem_type_native())
.custom(scales.clone())
.generate_with_f32_host_data();
let lines = rows * cols / width;
let output = TestInput::builder(client.clone(), shape![lines * width])
.dtype(f32::elem_type_native())
.zeros()
.generate_without_host_data();
let space = Space::new(&[(M, rows), (N, cols)]);
let mut input = TileArgLaunch::new(
input.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::direct(&[M, N]).packed(field),
);
if let Field::Index { .. } = field {
let (table_t, _) = TestInput::builder(client.clone(), shape![span])
.dtype(f32::elem_type_native())
.custom(table.clone())
.generate_with_f32_host_data();
input = input
.codebook(TileArgLaunch::new(
table_t.binding().into_tensor_arg(),
f32::elem_type_native(),
1,
TileSpec::direct(&[TBL]),
))
.expect("a plain table of an entry per index");
}
let input = input.scales(TileArgLaunch::new(
scales_t.binding().into_tensor_arg(),
f32::elem_type_native(),
1,
TileSpec::direct(&[M]),
));
decoded_lines::launch(
&client,
CubeCount::new_single(),
CubeDim::new_1d(lines as u32),
width,
input,
output.clone().binding().into_tensor_arg(),
uncut(&client, &space, &space).partitioning_arg(),
cols / width,
read,
);
let got = HostData::from_tensor_handle(&client, output, HostDataType::F32);
for m in 0..rows {
for n in 0..cols {
let want = decode(raw[m * cols + n]) * scales[m];
let have = got.get_f32(&[m * cols + n]);
assert!(
(have - want).abs() < 1e-6,
"{field:?} read {width} a line, at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
#[should_panic(
expected = "TileArgLaunch: a packed operand is bound at the u32 words its buffer \
holds, not"
)]
fn a_packed_operand_is_bound_at_its_words() {
let client = cubecl::test_device().client();
let words = TensorHandle::new_contiguous(
vec![1, 1],
client.create_from_slice(u32::as_bytes(&[0])),
u32::elem_type_native(),
);
TileArgLaunch::new(
words.binding().into_tensor_arg(),
f16::elem_type_native(),
1,
TileSpec::direct(&[M, N]).packed(Field::Quant(QuantValue::E2M1)),
);
}
#[cube(launch)]
fn word_field_staged_for_a_walk(
input: &TileArg<'_>,
output: &TileArg<'_>,
space: Partitioning,
#[comptime] decoded: Option<cubecl::ir::ElemType>,
) {
let input = input.tile(&space);
let output = output.tile(&space);
let walk = space.walk();
let first = walk.region(0);
let source = input.at(&first);
let mut staged = source.stage_for(&walk, StageStorage::Strided, decoded);
staged.copy_from(&source);
sync_cube();
let mut out = output.at(&first);
out.copy_cast_from(&staged);
}
fn staged_for_a_walk(values: &[f32], decoded: Option<cubecl::ir::ElemType>) -> Vec<f32> {
let (rows, cols) = (8, 16);
let client = cubecl::test_device().client();
let words: Vec<u32> = values.iter().map(|value| value.to_bits()).collect();
let input = TensorHandle::new_contiguous(
vec![rows, cols],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let dtype = f32::elem_type_native();
let output = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, rows), (N, cols)]),
Levels::leaf(&[(M, rows), (N, cols)])
.walk_every(&[M])
.build(),
),
Form::Static,
);
word_field_staged_for_a_walk::launch(
&client,
CubeCount::new_single(),
launcher.cube_dim(),
TileArgLaunch::new(
input.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::direct(&[M, N]).packed(Field::Float(FloatKind::F32)),
),
TileArgLaunch::new(
output.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
decoded,
);
let got = HostData::from_tensor_handle(&client, output, HostDataType::F32);
(0..rows * cols)
.map(|i| got.get_f32(&[i / cols, i % cols]))
.collect()
}
#[test]
fn a_word_wide_field_staged_for_a_walk_holds_the_element_its_kernel_names() {
let values: Vec<f32> = (0..8 * 16)
.map(|i| 1.0 + i as f32 * 2f32.powi(-12))
.collect();
let rounded: Vec<f32> = values
.iter()
.map(|&value| f16::from_f32(value).to_f32())
.collect();
assert_ne!(rounded, values, "the values must be finer than f16 keeps");
let f16 = cubecl::ir::ElemType::Float(FloatKind::F16);
assert_eq!(staged_for_a_walk(&values, Some(f16)), rounded);
assert_eq!(staged_for_a_walk(&values, None), values);
}