use crate::support::{HostData, HostDataType, TestInput, skip_unless_plane_holds};
use cubecl::{
bytes::Bytes,
prelude::*,
quant::scheme::{QuantValue, ScaleDtype},
std::tensor::TensorHandle,
zspace::shape,
};
use cubecl_common::{e2m1, e4m3};
use cubecl_tile::Instruction;
use cubecl_tile::kind::Field;
use cubecl_tile::layout::PhysicalAxisMap;
use cubecl_tile::layout::split;
use cubecl_tile::stage::{Prefetch, RowChunks, UnitRead};
use cubecl_tile::*;
use half::f16;
use super::matmul::{require_cmma_8x8x8, require_cmma_8x8x8_f32};
use super::{Form, implied};
use cubecl_tile::launch::Bound;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum Scaled {
Lhs,
Rhs,
}
const M: Axis = Axis(0);
const N: Axis = Axis(1);
const KB: Axis = Axis(2);
const KI: Axis = Axis(3);
const REGISTER_BLOCK: RegisterBlock = RegisterBlock::new(16);
#[cube(launch)]
fn scaled_matmul(
a: &TileArg<'_>,
b: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[comptime] side: Scaled,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let scale = 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);
match comptime!(side) {
Scaled::Lhs => c_r.mma(&a.at(®ion).mul(&scale.at(®ion)), &b.at(®ion)),
Scaled::Rhs => c_r.mma(&a.at(®ion), &b.at(®ion).mul(&scale.at(®ion))),
}
}
}
#[cube(launch)]
fn scaled_matmul_promoted<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[comptime] side: Scaled,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let scale = scale.tile(&space);
let c = c.tile(&space);
let acc = c.accumulator::<E>(
&a,
&b,
comptime!(Instruction::Registers {
config: REGISTER_BLOCK
}),
Semiring::SUM_PROD,
);
for region in space.over(&level) {
let mut acc_r = acc.at(®ion);
match comptime!(side) {
Scaled::Lhs => acc_r.mma(&a.at(®ion).mul(&scale.at(®ion)), &b.at(®ion)),
Scaled::Rhs => acc_r.mma(&a.at(®ion), &b.at(®ion).mul(&scale.at(®ion))),
}
}
acc.drained_into(&c);
}
#[cube(launch)]
#[allow(clippy::too_many_arguments)]
fn factored_matmul<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
blocks: &TileArg<'_>,
global: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[comptime] instruction: Instruction,
#[define(E)] _dtype: ElemType,
) {
let a = a
.tile(&space)
.mul(&blocks.tile(&space))
.mul(&global.tile(&space));
let b = b.tile(&space);
let mut c = c.tile(&space);
let (a, a_factor) = a.factor(c.space(), instruction);
let acc = c.accumulator::<E>(&a, &b, instruction, Semiring::SUM_PROD);
for region in space.over(&level) {
let mut acc_r = acc.at(®ion);
acc_r.mma(&a.at(®ion), &b.at(®ion));
}
c.dequantize_from(&acc.scaled(&a_factor));
}
#[cube(launch)]
fn two_level_scaled_matmul(
a: &TileArg<'_>,
b: &TileArg<'_>,
blocks: &TileArg<'_>,
global: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[comptime] side: Scaled,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let blocks = blocks.tile(&space);
let global = global.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);
match comptime!(side) {
Scaled::Lhs => c_r.mma(
&a.at(®ion)
.mul(&blocks.at(®ion))
.mul(&global.at(®ion)),
&b.at(®ion),
),
Scaled::Rhs => c_r.mma(
&a.at(®ion),
&b.at(®ion)
.mul(&blocks.at(®ion))
.mul(&global.at(®ion)),
),
}
}
}
#[cube(launch)]
#[allow(clippy::too_many_arguments)]
fn scaled_matmul_cmma<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[comptime] side: Scaled,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let scale = scale.tile(&space);
let c = c.tile(&space);
let acc = c.accumulator::<E>(&a, &b, Instruction::Cmma, Semiring::SUM_PROD);
for region in space.over(&level) {
let mut acc_r = acc.at(®ion);
match comptime!(side) {
Scaled::Lhs => acc_r.mma(&a.at(®ion).mul(&scale.at(®ion)), &b.at(®ion)),
Scaled::Rhs => acc_r.mma(&a.at(®ion), &b.at(®ion).mul(&scale.at(®ion))),
}
}
acc.drained_into(&c);
}
#[test]
fn two_levels_multiply_in_order() {
let (rows, cols, block, blocks) = (4, 4, 8, 2);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: 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 (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = TestInput::builder(client.clone(), shape![rows, blocks])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let (g_t, _) = 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, block)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
two_level_scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_t.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_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB],
&[PhysicalAxisMap::of(M), PhysicalAxisMap::of(KB)],
)),
),
TileArgLaunch::new(
g_t.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),
Scaled::Lhs,
);
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| a[m * depth + k] * s[m * blocks + k / block] * g[0] * b[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_decoded_operands_global_scale_leaves_for_the_sum() {
check_factored(
Instruction::Registers {
config: REGISTER_BLOCK,
},
4,
);
}
#[test]
fn a_decoded_operand_keeps_its_global_scale_on_the_load_into_cmma_fragments() {
let client = cubecl::test_device().client();
if !require_cmma_8x8x8_f32(&client) {
return;
}
check_factored(Instruction::Cmma, 8);
}
fn check_factored(instruction: Instruction, size: usize) {
let (rows, cols, block, blocks) = (size, size, 8, 2);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: 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 (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = TestInput::builder(client.clone(), shape![rows, blocks])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let (g_t, _) = 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, block)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
let a_projection = Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
);
factored_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(a_projection.clone()),
),
TileArgLaunch::new(
b_t.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_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(a_projection.scales_per(KB)),
),
TileArgLaunch::new(
g_t.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),
instruction,
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| a[m * depth + k] * s[m * blocks + k / block] * g[0] * b[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_scaled_contraction_multiplies_the_block_scale_in() {
let (rows, cols, block, blocks) = (4, 4, 8, 4);
let (per_region, inside) = (1, block);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: 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 (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = 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, per_region), (KI, inside)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
let a_projection = Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
);
scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(a_projection.clone()),
),
TileArgLaunch::new(
b_t.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_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(a_projection.scales_per(KB)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
Scaled::Lhs,
);
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| a[m * depth + k] * s[m * blocks + k / block] * b[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_cut_finer_than_the_block_reuses_its_scale() {
let (rows, cols, block, blocks) = (4, 4, 8, 4);
let (per_region, inside) = (1, block / 2);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: 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 (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = 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, per_region), (KI, inside)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_t.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_t.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),
Scaled::Lhs,
);
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| a[m * depth + k] * s[m * blocks + k / block] * b[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_scale_over_no_axis_covers_everything() {
let (rows, cols, block, blocks) = (4, 4, 8, 2);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: Vec<f32> = (0..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s = vec![0.5f32];
let (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = TestInput::builder(client.clone(), shape![1])
.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,
);
scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_t.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_t.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),
Scaled::Lhs,
);
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| a[m * depth + k] * s[0] * b[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_cut_coarser_than_the_block_changes_scale_within_a_region() {
let (rows, cols, block, blocks) = (4, 4, 8, 4);
let (per_region, inside) = (2, block);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: 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 (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = 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, per_region), (KI, inside)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_t.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_t.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),
Scaled::Lhs,
);
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| a[m * depth + k] * s[m * blocks + k / block] * b[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 f16_scales_are_read_as_f16() {
let (rows, cols, block, blocks) = (4, 4, 8, 4);
let (per_region, inside) = (1, block);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let scale_dtype = f16::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: 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 (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = TestInput::builder(client.clone(), shape![rows, blocks])
.dtype(scale_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, per_region), (KI, inside)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_t.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_t.binding().into_tensor_arg(),
scale_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),
Scaled::Lhs,
);
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| a[m * depth + k] * s[m * blocks + k / block] * b[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 scales_over_the_columns_scale_the_rhs() {
let (rows, cols, block, blocks) = (4, 4, 8, 4);
let (per_region, inside) = (1, block);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: Vec<f32> = (0..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..blocks * cols).map(|i| (i as f32 + 1.0) / 2.0).collect();
let (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = TestInput::builder(client.clone(), shape![blocks, cols])
.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, per_region), (KI, inside)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_t.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_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(N)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
Scaled::Rhs,
);
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| a[m * depth + k] * b[k * cols + n] * s[(k / block) * 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 an_rhs_scale_survives_a_finer_cut() {
let (rows, cols, block, blocks) = (4, 4, 8, 4);
let (per_region, inside) = (1, block / 2);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: Vec<f32> = (0..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..blocks * cols).map(|i| (i as f32 + 1.0) / 2.0).collect();
let (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = TestInput::builder(client.clone(), shape![blocks, cols])
.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, per_region), (KI, inside)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_t.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_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(N)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
Scaled::Rhs,
);
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| a[m * depth + k] * b[k * cols + n] * s[(k / block) * 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 an_rhs_scale_changes_within_a_coarser_region() {
let (rows, cols, block, blocks) = (4, 4, 8, 4);
let (per_region, inside) = (2, block);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: Vec<f32> = (0..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..blocks * cols).map(|i| (i as f32 + 1.0) / 2.0).collect();
let (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = TestInput::builder(client.clone(), shape![blocks, cols])
.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, per_region), (KI, inside)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_t.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_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(N)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
Scaled::Rhs,
);
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| a[m * depth + k] * b[k * cols + n] * s[(k / block) * 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_promoted_accumulator_takes_the_scaled_contraction() {
let (rows, cols, block, blocks) = (4, 4, 8, 4);
let (per_region, inside) = (1, block);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: 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 (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = 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, per_region), (KI, inside)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
scaled_matmul_promoted::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_t.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_t.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),
Scaled::Lhs,
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| a[m * depth + k] * s[m * blocks + k / block] * b[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 wide_rhs_scaled_matmul_promoted<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[comptime] side: Scaled,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let scale = scale.tile(&space);
let c = c.tile(&space);
let acc = c.accumulator::<E>(
&a,
&b,
comptime!(Instruction::Registers {
config: REGISTER_BLOCK
}),
Semiring::SUM_PROD,
);
for region in space.over(&level) {
let mut acc_r = acc.at(®ion);
match comptime!(side) {
Scaled::Lhs => acc_r.mma(&a.at(®ion).mul(&scale.at(®ion)), &b.at(®ion)),
Scaled::Rhs => acc_r.mma(&a.at(®ion), &b.at(®ion).mul(&scale.at(®ion))),
}
}
acc.drained_into(&c);
}
#[test]
fn rhs_scales_are_served_several_at_a_time() {
let (rows, cols, block, blocks, plane_units) = (2, 4, 8, 4, 4);
let (per_region, inside) = (1, block);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: Vec<f32> = (0..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..blocks * cols).map(|i| (i as f32 + 1.0) / 2.0).collect();
let (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = TestInput::builder(client.clone(), shape![blocks, cols])
.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, per_region), (KI, inside)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
wide_rhs_scaled_matmul_promoted::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_t.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_t.binding().into_tensor_arg(),
dtype,
plane_units,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(N)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
Scaled::Rhs,
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| a[m * depth + k] * b[k * cols + n] * s[(k / block) * 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 wide_lhs_scaled_matmul(
a: &TileArg<'_>,
b: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[comptime] side: Scaled,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let scale = 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(
comptime!(RegisterBlock::new(64).component_fanout()),
Semiring::SUM_PROD,
);
match comptime!(side) {
Scaled::Lhs => c_r.mma(&a.at(®ion).mul(&scale.at(®ion)), &b.at(®ion)),
Scaled::Rhs => c_r
.accumulating(
comptime!(RegisterBlock::new(64).component_fanout()),
Semiring::SUM_PROD,
)
.mma(&a.at(®ion), &b.at(®ion).mul(&scale.at(®ion))),
}
}
}
#[test]
fn lhs_scales_are_served_several_at_a_time() {
let (cols, block, blocks, plane_units) = (4, 4, 4, 4);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: Vec<f32> = (0..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..blocks).map(|i| (i as f32 + 1.0) / 2.0).collect();
let (a_t, _) = TestInput::builder(client.clone(), shape![1, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = TestInput::builder(client.clone(), shape![blocks])
.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), (N, cols), (KB, blocks), (KI, block)]),
Levels::leaf(&[(M, 1), (N, cols), (KB, blocks), (KI, block)])
.walk_every(&[M, N, KB, KI])
.build(),
),
Form::Static,
);
wide_lhs_scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
4,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_t.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_t.binding().into_tensor_arg(),
dtype,
plane_units,
TileSpec::new(Projection::new(&[M, KB], &[PhysicalAxisMap::of(KB)])),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
Scaled::Lhs,
);
let got = HostData::from_tensor_handle(&client, c, HostDataType::F32);
for n in 0..cols {
let want: f32 = (0..depth)
.map(|k| a[k] * s[k / block] * b[k * cols + n])
.sum();
let have = got.get_f32(&[0, n]);
assert!(
(have - want).abs() < 1e-3,
"at {n}: got {have}, want {want}"
);
}
}
#[derive(Clone, Copy)]
enum CmmaCase {
Lhs,
RhsRowMajor,
RhsColMajor,
}
fn check_scaled_cmma(case: CmmaCase) {
let (rows, cols, block, blocks) = (8, 8, 8, 4);
let depth = block * blocks;
let client = cubecl::test_device().client();
if !require_cmma_8x8x8_f32(&client) {
return;
}
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let b: Vec<f32> = (0..depth * cols).map(|i| (i % 7) as f32 - 3.0).collect();
let s: Vec<f32> = (0..blocks * 8).map(|i| (i as f32 + 1.0) / 2.0).collect();
let b_stored: Vec<f32> = match case {
CmmaCase::RhsColMajor => (0..cols * depth)
.map(|i| b[(i % depth) * cols + i / depth])
.collect(),
_ => b.clone(),
};
let b_shape = match case {
CmmaCase::RhsColMajor => shape![cols, depth],
_ => shape![depth, cols],
};
let s_shape = match case {
CmmaCase::Lhs => shape![rows, blocks],
_ => shape![blocks, cols],
};
let (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), b_shape)
.dtype(dtype)
.custom(b_stored)
.generate_with_f32_host_data();
let (s_t, _) = TestInput::builder(client.clone(), s_shape)
.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,
);
let split = PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]);
let b_spec = match case {
CmmaCase::RhsColMajor => TileSpec::new(Projection::new(
&[N, KB, KI],
&[PhysicalAxisMap::of(N), split.clone()],
)),
_ => TileSpec::new(Projection::new(
&[KB, KI, N],
&[split.clone(), PhysicalAxisMap::of(N)],
)),
};
let s_spec = match case {
CmmaCase::Lhs => TileSpec::new(Projection::new(
&[M, KB],
&[PhysicalAxisMap::of(M), PhysicalAxisMap::of(KB)],
)),
_ => TileSpec::new(Projection::new(
&[KB, KI, N],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(N)],
)),
};
let side = match case {
CmmaCase::Lhs => Scaled::Lhs,
_ => Scaled::Rhs,
};
scaled_matmul_cmma::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[PhysicalAxisMap::of(M), split],
)),
),
TileArgLaunch::new(b_t.binding().into_tensor_arg(), dtype, 1, b_spec),
TileArgLaunch::new(s_t.binding().into_tensor_arg(), dtype, 1, s_spec),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
side,
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| {
let scale = match case {
CmmaCase::Lhs => s[m * blocks + k / block],
_ => s[(k / block) * cols + n],
};
a[m * depth + k] * scale * b[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_cmma_accumulator_takes_the_scaled_contraction() {
check_scaled_cmma(CmmaCase::Lhs);
}
#[test]
fn a_cmma_accumulator_takes_rhs_scales() {
check_scaled_cmma(CmmaCase::RhsRowMajor);
}
#[test]
fn a_cmma_accumulator_takes_rhs_scales_col_major() {
check_scaled_cmma(CmmaCase::RhsColMajor);
}
#[cube(launch)]
fn scaled_matmul_cmma_staged<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let scale = scale.tile(&space);
let c = c.tile(&space);
let mut stage = b.stage(comptime!(level.clone()), StageStorage::Strided);
let acc = c.accumulator::<E>(&a, &b, Instruction::Cmma, Semiring::SUM_PROD);
for region in space.over(&level) {
stage.copy_from(&b.at(®ion));
sync_cube();
let mut acc_r = acc.at(®ion);
acc_r.mma(&a.at(®ion), &stage.mul(&scale.at(®ion)));
sync_cube();
}
acc.drained_into(&c);
}
#[test]
fn a_packed_stage_lands_on_the_tensor_cores() {
let (rows, cols, block, blocks) = (8, 8, 8, 4);
let depth = block * blocks;
let field = QuantValue::E2M1;
let factor = 32 / field.size_bits();
let client = cubecl::test_device().client();
if !require_cmma_8x8x8_f32(&client) {
return;
}
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let codes: Vec<u32> = (0..cols * 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 s: Vec<f32> = (0..blocks * cols).map(|i| (i as f32 + 1.0) / 2.0).collect();
let (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let b_t = TensorHandle::new_contiguous(
vec![cols, depth / factor],
client.create(Bytes::from_elems(words)),
u32::elem_type_native(),
);
let (s_t, _) = TestInput::builder(client.clone(), shape![blocks, cols])
.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,
);
let split = PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]);
scaled_matmul_cmma_staged::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[PhysicalAxisMap::of(M), split.clone()],
)),
),
TileArgLaunch::new(
b_t.binding().into_tensor_arg(),
u32::elem_type_native(),
1,
TileSpec::new(Projection::new(
&[N, KB, KI],
&[PhysicalAxisMap::of(N), split],
))
.packed(field),
),
TileArgLaunch::new(
s_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[PhysicalAxisMap::of(KB), PhysicalAxisMap::of(N)],
)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
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| {
let b = e2m1::from_bits(codes[n * depth + k] as u8).to_f32();
a[m * depth + k] * s[(k / block) * cols + n] * b
})
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"at ({m}, {n}): got {have}, want {want}"
);
}
}
}
const NB: Axis = Axis(4);
const NI: Axis = Axis(5);
#[cube(launch)]
#[allow(clippy::too_many_arguments)]
fn chunked_scaled_matmul<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] instruction: Instruction,
#[comptime] chunks: Level,
#[comptime] read: UnitRead,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let scale = scale.tile(&space);
let c = c.tile(&space);
for cube in space {
let a_cube = a.at(&cube);
let b_cube = b.at(&cube);
let scale_cube = scale.at(&cube);
let c_cube = c.at(&cube);
for plane in cube {
let a_plane = a_cube.at(&plane);
let b_plane = b_cube.at(&plane);
let scale_plane = scale_cube.at(&plane);
let c_plane = c_cube.at(&plane);
let mut lines = scale_plane.stage(
comptime!(chunks.clone()),
comptime!(StageStorage::Lines { read }),
);
let sum = c_plane.accumulator::<E>(&a_plane, &b_plane, instruction, Semiring::SUM_PROD);
for chunk in plane {
lines.copy_from(&scale_plane.at(&chunk));
for step in chunk {
for leaf in step {
let mut sum_leaf = sum.at(&leaf);
sum_leaf.mma(&a_plane.at(&leaf), &b_plane.at(&leaf).mul(&lines.at(&leaf)));
}
}
}
sum.drained_into(&c_plane);
}
}
}
fn contiguous_strides(shape: &[usize]) -> Vec<usize> {
let mut strides = vec![1; shape.len()];
for i in (0..shape.len().saturating_sub(1)).rev() {
strides[i] = strides[i + 1] * shape[i + 1];
}
strides
}
#[derive(Clone, Copy, Debug)]
enum TileScales {
F32,
Ue4m3,
}
struct TileOrdered {
rows: usize,
tile: usize,
n_tiles: usize,
k_tiles: usize,
a: Vec<f32>,
words: Vec<u32>,
s: Vec<f32>,
}
impl TileOrdered {
const FIELD: QuantValue = QuantValue::E2M1;
fn new(rows: usize, n_tiles: usize, k_tiles: usize) -> Self {
let tile = 16;
let depth = k_tiles * tile;
let factor = 32 / Self::FIELD.size_bits();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let this = TileOrdered {
rows,
tile,
n_tiles,
k_tiles,
a,
words: Vec::new(),
s: Vec::new(),
};
let mut words = Vec::with_capacity(n_tiles * k_tiles * tile * tile / factor);
for nb in 0..n_tiles {
for kb in 0..k_tiles {
for ni in 0..tile {
for word in 0..tile / factor {
words.push((0..factor).fold(0u32, |acc, j| {
acc | ((this.code(nb, kb, ni, word * factor + j) as u32)
<< (j * Self::FIELD.size_bits()))
}));
}
}
}
}
let s: Vec<f32> = (0..n_tiles * k_tiles * tile)
.map(|i| ((i % 8) + (i / 8) + (i / 64)) as f32 % 8.0 / 2.0 + 0.5)
.collect();
TileOrdered { words, s, ..this }
}
fn depth(&self) -> usize {
self.k_tiles * self.tile
}
fn cols(&self) -> usize {
self.n_tiles * self.tile
}
fn code(&self, nb: usize, kb: usize, ni: usize, ki: usize) -> usize {
(ki + 3 * ni + 5 * kb + 7 * nb) % 16
}
fn scale_at(&self, nb: usize, kb: usize, ni: usize) -> f32 {
self.s[(nb * self.k_tiles + kb) * self.tile + ni]
}
fn want(&self, m: usize, n: usize) -> f32 {
let (nb, ni) = (n / self.tile, n % self.tile);
(0..self.depth())
.map(|k| {
let (kb, ki) = (k / self.tile, k % self.tile);
let b = e2m1::from_bits(self.code(nb, kb, ni, ki) as u8).to_f32();
self.a[m * self.depth() + k] * b * self.scale_at(nb, kb, ni)
})
.sum()
}
fn space(&self) -> Space {
Space::new(&[
(M, self.rows),
(NB, self.n_tiles),
(NI, self.tile),
(KB, self.k_tiles),
(KI, self.tile),
])
}
fn a_op(&self, client: &Client, launcher: &Launcher) -> Bound {
let (a_t, _) = TestInput::builder(client.clone(), shape![self.rows, self.depth()])
.dtype(f32::elem_type_native())
.custom(self.a.clone())
.generate_with_f32_host_data();
launcher
.arg(a_t.binding(), f32::elem_type_native())
.gathered(
Projection::dims()
.dim(M)
.dim(split(&[(KB, self.k_tiles), (KI, self.tile)]))
.build(),
)
.vectorize(32 / Self::FIELD.size_bits())
.build()
.unwrap()
}
fn a_op_blocked(&self, client: &Client, launcher: &Launcher, elem: ElemType) -> Bound {
let (a_t, _) =
TestInput::builder(client.clone(), shape![self.rows, self.k_tiles, self.tile])
.dtype(elem)
.custom(self.a.clone())
.generate_with_f32_host_data();
launcher
.arg(a_t.binding(), elem)
.axes(&[M, KB, KI])
.vectorize(32 / Self::FIELD.size_bits())
.build()
.unwrap()
}
fn b_op(&self, client: &Client, launcher: &Launcher, read: Option<usize>) -> Bound {
let shape = vec![self.n_tiles, self.k_tiles, self.tile, self.tile];
let b_t = TensorHandle::new_contiguous(
shape.clone(),
client.create(Bytes::from_elems(self.words.clone())),
u32::elem_type_native(),
);
let mut binding = b_t.binding();
binding.shape = shape.clone().into();
binding.strides = contiguous_strides(&shape).into();
let b = launcher
.arg(binding, u32::elem_type_native())
.gathered(Projection::dims().dim(NB).dim(KB).dim(NI).dim(KI).build())
.packed(Self::FIELD);
match read {
Some(values) => b.vectorize(values),
None => b,
}
.build()
.unwrap()
}
fn s_op(
&self,
client: &Client,
launcher: &Launcher,
scales: TileScales,
served: usize,
) -> Bound {
let axes = Projection::dims()
.dim(NB)
.dim(KB)
.dim(NI)
.spanning(KI)
.build();
let shape = vec![self.n_tiles, self.k_tiles, self.tile];
match scales {
TileScales::F32 => {
let (s_t, _) = TestInput::builder(
client.clone(),
shape![self.n_tiles, self.k_tiles, self.tile],
)
.dtype(f32::elem_type_native())
.custom(self.s.clone())
.generate_with_f32_host_data();
launcher
.arg(s_t.binding(), f32::elem_type_native())
.gathered(axes)
.vectorize(served)
.build()
.unwrap()
}
TileScales::Ue4m3 => {
let per_word = 4;
let bytes: Vec<u32> = self
.s
.chunks(per_word)
.map(|word| {
word.iter().enumerate().fold(0u32, |acc, (j, &v)| {
acc | ((e4m3::from_f32(v).to_bits() as u32) << (j * 8))
})
})
.collect();
let s_t = TensorHandle::new_contiguous(
shape.clone(),
client.create(Bytes::from_elems(bytes)),
u32::elem_type_native(),
);
let mut binding = s_t.binding();
binding.shape = shape.clone().into();
binding.strides = contiguous_strides(&shape).into();
launcher
.arg(binding, u32::elem_type_native())
.gathered(axes)
.packed(Field::of_scale(ScaleDtype::UE4M3))
.vectorize(served)
.build()
.unwrap()
}
}
}
fn c_op(&self, launcher: &Launcher, c: &TensorHandle) -> Bound {
launcher
.arg(c.clone().binding(), c.dtype)
.gathered(
Projection::dims()
.dim(M)
.dim(split(&[(NB, self.n_tiles), (NI, self.tile)]))
.build(),
)
.build()
.unwrap()
}
fn check(&self, client: &Client, c: TensorHandle, what: &str) {
let got = HostData::from_tensor_handle(client, c, HostDataType::F32);
for m in 0..self.rows {
for n in 0..self.cols() {
let want = self.want(m, n);
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-2 * want.abs().max(1.0),
"{what} at ({m}, {n}): got {have}, want {want}"
);
}
}
}
}
#[derive(Clone, Copy, Debug)]
enum Arm {
Registers,
Landing,
}
fn check_chunked(arm: Arm, scales: TileScales, read: UnitRead) {
let (rows, n_tiles, chunk, chunks) = (8, 2, 32, 2);
let w = TileOrdered::new(rows, n_tiles, chunk * chunks);
let client = cubecl::test_device().client();
if matches!(arm, Arm::Landing) && !require_cmma_8x8x8_f32(&client) {
return;
}
if matches!(arm, Arm::Registers) && skip_unless_plane_holds(&client, (w.tile * 2) as u32) {
return;
}
let dtype = f32::elem_type_native();
let c = TestInput::builder(client.clone(), shape![rows, w.cols()])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let levels = match arm {
Arm::Registers => Levels::leaf(&[(M, rows), (NI, 1), (KB, 1), (KI, w.tile)])
.units(&[(NI, w.tile), (KB, 2)])
.walk(&[(KB, chunk / 2)])
.walk_every(&[KB])
.planes(&[(NB, 1)])
.cubes(&[NB])
.build(),
Arm::Landing => Levels::leaf(&[(M, rows), (NI, 8), (KI, 8)])
.walk(&[(NI, 1)])
.walk(&[(KB, chunk), (KI, 2)])
.walk_every(&[KB])
.planes(&[(NI, w.tile / 8)])
.cubes(&[NB])
.build(),
};
let chunks_level = levels[2].clone();
let instruction = match arm {
Arm::Registers => Instruction::Registers {
config: REGISTER_BLOCK,
},
Arm::Landing => Instruction::Cmma,
};
let launcher = implied(&client, Partitioning::new(w.space(), levels), Form::Static);
let s_op = w.s_op(
&client,
&launcher,
scales,
match arm {
Arm::Registers => w.tile,
Arm::Landing => 8,
},
);
chunked_scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
w.a_op(&client, &launcher).arg(),
w.b_op(&client, &launcher, None).arg(),
s_op.arg(),
w.c_op(&launcher, &c).arg(),
launcher.partitioning_arg(),
instruction,
chunks_level,
read,
dtype,
);
w.check(&client, c, &format!("{arm:?} {scales:?}"));
}
#[test]
fn a_plane_holds_its_scales_in_its_units() {
for read in [UnitRead::Shuffle, UnitRead::PlaneShared] {
check_chunked(Arm::Registers, TileScales::F32, read);
}
}
#[test]
fn a_plane_holds_its_byte_scales_in_its_units() {
for read in [UnitRead::Shuffle, UnitRead::PlaneShared] {
check_chunked(Arm::Registers, TileScales::Ue4m3, read);
}
}
#[test]
fn a_tile_ordered_weight_lands_on_the_tensor_cores() {
for read in [UnitRead::Shuffle, UnitRead::PlaneShared] {
check_chunked(Arm::Landing, TileScales::F32, read);
}
}
#[test]
fn a_tile_ordered_weight_lands_on_the_tensor_cores_under_byte_scales() {
for read in [UnitRead::Shuffle, UnitRead::PlaneShared] {
check_chunked(Arm::Landing, TileScales::Ue4m3, read);
}
}
#[cube(launch)]
#[allow(clippy::too_many_arguments)]
fn partitioned_scaled_matmul<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] chunks: Level,
#[comptime] read: UnitRead,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let scale = scale.tile(&space);
let c = c.tile(&space);
for cube in space {
let a_cube = a.at(&cube);
let b_cube = b.at(&cube);
let scale_cube = scale.at(&cube);
let c_cube = c.at(&cube);
for plane in cube {
let a_plane = a_cube.at(&plane);
let b_plane = b_cube.at(&plane);
let scale_plane = scale_cube.at(&plane);
let c_plane = c_cube.at(&plane);
let mut lines = scale_plane.stage(
comptime!(chunks.clone()),
comptime!(StageStorage::Lines { read }),
);
let sum =
c_plane.accumulator::<E>(&a_plane, &b_plane, Instruction::Cmma, Semiring::SUM_PROD);
for chunk in plane {
lines.copy_from(&scale_plane.at(&chunk));
for step in chunk {
let a_step = a_plane.at(&step);
let b_step = b_plane.at(&step).mul(&lines.at(&step));
for block in step.walk().unrolled() {
for depth in block.walk().unrolled() {
let mut sum_depth = sum.at(&depth);
sum_depth.mma(&a_step.at(&depth), &b_step.at(&depth));
}
}
}
}
sum.drained_into(&c_plane);
}
}
}
fn check_partitioned(scales: TileScales, read: UnitRead) {
let (rows, n_tiles, chunk, chunks, fragment) = (16, 2, 32, 2, 8);
let w = TileOrdered::new(rows, n_tiles, chunk * chunks);
let client = cubecl::test_device().client();
if !require_cmma_8x8x8_f32(&client) {
return;
}
let dtype = f32::elem_type_native();
let c = TestInput::builder(client.clone(), shape![rows, w.cols()])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let levels = Levels::leaf(&[(M, fragment), (NI, fragment), (KI, fragment)])
.walk(&[(M, rows / fragment), (NI, w.tile / fragment)])
.walk(&[(KI, w.tile / fragment)])
.walk(&[(KB, 2)])
.walk(&[(KB, chunk / 2)])
.walk_every(&[KB])
.planes(&[(NI, 1)])
.cubes(&[NB])
.build();
let chunks_level = levels[2].clone();
let launcher = implied(&client, Partitioning::new(w.space(), levels), Form::Static);
let s_op = w.s_op(&client, &launcher, scales, w.tile);
partitioned_scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
w.a_op(&client, &launcher).arg(),
w.b_op(&client, &launcher, None).arg(),
s_op.arg(),
w.c_op(&launcher, &c).arg(),
launcher.partitioning_arg(),
chunks_level,
read,
dtype,
);
w.check(&client, c, &format!("partition {scales:?}"));
}
#[test]
fn a_tile_ordered_weight_lands_on_the_tensor_cores_under_a_partition() {
for read in [UnitRead::Shuffle, UnitRead::PlaneShared] {
check_partitioned(TileScales::F32, read);
}
}
#[test]
fn a_tile_ordered_weight_lands_on_the_tensor_cores_under_a_partition_with_byte_scales() {
for read in [UnitRead::Shuffle, UnitRead::PlaneShared] {
check_partitioned(TileScales::Ue4m3, read);
}
}
#[cube(launch)]
fn stepped_scaled_matmul<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let scale = scale.tile(&space);
let c = c.tile(&space);
for cube in space {
let (a_cube, b_cube) = (a.at(&cube), b.at(&cube));
let (scale_cube, c_cube) = (scale.at(&cube), c.at(&cube));
for plane in cube {
let (a_plane, b_plane) = (a_cube.at(&plane), b_cube.at(&plane));
let (scale_plane, c_plane) = (scale_cube.at(&plane), c_cube.at(&plane));
let sum =
c_plane.accumulator::<E>(&a_plane, &b_plane, Instruction::Cmma, Semiring::SUM_PROD);
for step in plane {
let mut sum_step = sum.at(&step);
sum_step.mma(
&a_plane.at(&step),
&b_plane.at(&step).mul(&scale_plane.at(&step)),
);
}
sum.drained_into(&c_plane);
}
}
}
fn check_stepped(scales: TileScales) {
let (rows, n_tiles, k_tiles, fragment) = (16, 2, 4, 8);
let w = TileOrdered::new(rows, n_tiles, k_tiles);
let client = cubecl::test_device().client();
if !require_cmma_8x8x8_f32(&client) {
return;
}
let dtype = f32::elem_type_native();
let c = TestInput::builder(client.clone(), shape![rows, w.cols()])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let levels = Levels::leaf(&[(M, fragment), (NI, fragment), (KI, fragment)])
.walk(&[(M, rows / fragment), (NI, w.tile / fragment)])
.walk(&[(KI, w.tile / fragment)])
.walk_every(&[KB])
.planes(&[(NI, 1)])
.cubes(&[NB])
.build();
let launcher = implied(&client, Partitioning::new(w.space(), levels), Form::Static);
let s_op = w.s_op(&client, &launcher, scales, w.tile);
stepped_scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
w.a_op(&client, &launcher).arg(),
w.b_op(&client, &launcher, Some(w.tile)).arg(),
s_op.arg(),
w.c_op(&launcher, &c).arg(),
launcher.partitioning_arg(),
dtype,
);
w.check(&client, c, &format!("stepped {scales:?}"));
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum CopiedThrough {
Directly,
ABlockAtATime,
AStage,
}
#[cube(launch)]
fn decoded_tile_order_copy(
b: &TileArg<'_>,
scale: &TileArg<'_>,
out: &TileArg<'_>,
space: Partitioning,
#[comptime] through: CopiedThrough,
) {
let b = b.tile(&space);
let scale = scale.tile(&space);
let out = out.tile(&space);
for cube in space {
let decoded = b.at(&cube).mul(&scale.at(&cube));
let mut out_cube = out.at(&cube);
match comptime!(through) {
CopiedThrough::Directly => out_cube.dequantize_from(&decoded),
CopiedThrough::ABlockAtATime => {
for block in cube.walk() {
let mut out_block = out_cube.at(&block);
out_block.dequantize_from(&decoded.at(&block));
}
}
CopiedThrough::AStage => {
let run = cube.walk();
let mut stages =
Stages::smem_single(&run, &decoded, StageStorage::Strided, 1usize, None);
stages.pipelined(run, |slot, stage| {
let mut out_stage = out_cube.at(stage);
slot.consume(|b_stage| {
out_stage.copy_from(b_stage);
});
});
}
}
}
}
#[test]
fn a_tile_ordered_weight_copies_decoded() {
check_decoded_tile_order_copy(CopiedThrough::Directly);
}
#[test]
fn a_tile_ordered_weight_copies_decoded_a_block_at_a_time() {
check_decoded_tile_order_copy(CopiedThrough::ABlockAtATime);
}
#[test]
fn a_tile_ordered_weight_copies_decoded_through_a_stage() {
check_decoded_tile_order_copy(CopiedThrough::AStage);
}
fn check_decoded_tile_order_copy(through: CopiedThrough) {
let w = TileOrdered::new(1, 2, 4);
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let space = Space::new(&[(NB, w.n_tiles), (KB, w.k_tiles), (NI, w.tile), (KI, w.tile)]);
let levels = Levels::leaf(&[(KB, 1), (NI, w.tile), (KI, w.tile)])
.walk_every(&[KB])
.cubes(&[NB])
.build();
let launcher = implied(&client, Partitioning::new(space, levels), Form::Static);
let shape = shape![w.n_tiles, w.k_tiles, w.tile, w.tile];
let out = TestInput::builder(client.clone(), shape)
.dtype(dtype)
.zeros()
.generate_without_host_data();
let s_op = w.s_op(&client, &launcher, TileScales::Ue4m3, w.tile);
decoded_tile_order_copy::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
w.b_op(&client, &launcher, Some(w.tile)).arg(),
s_op.arg(),
launcher
.arg(out.clone().binding(), dtype)
.axes(&[NB, KB, NI, KI])
.vectorize(w.tile)
.build()
.unwrap()
.arg(),
launcher.partitioning_arg(),
through,
);
let got = HostData::from_tensor_handle(&client, out, HostDataType::F32);
for nb in 0..w.n_tiles {
for kb in 0..w.k_tiles {
for ni in 0..w.tile {
for ki in 0..w.tile {
let want = e2m1::from_bits(w.code(nb, kb, ni, ki) as u8).to_f32()
* w.scale_at(nb, kb, ni);
let have = got.get_f32(&[nb, kb, ni, ki]);
assert_eq!(
have, want,
"{through:?} decoded at ({nb}, {kb}, {ni}, {ki})"
);
}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum Unpacked {
ByEachPlane,
ByTheFill,
}
#[cube(launch)]
#[allow(clippy::too_many_arguments)]
fn staged_stepped_scaled_matmul<A: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] unpacked: Unpacked,
#[comptime] storage: StageStorage,
#[comptime] decoded: ElemType,
#[define(A)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let scale = scale.tile(&space);
let c = c.tile(&space);
for cube in space {
let (a_cube, b_cube) = (a.at(&cube), b.at(&cube));
let (scale_cube, c_cube) = (scale.at(&cube), c.at(&cube));
let sum = c_cube.accumulator::<A>(&a_cube, &b_cube, Instruction::Cmma, Semiring::SUM_PROD);
let run = cube.walk();
let staged = match comptime!(unpacked) {
Unpacked::ByEachPlane => b_cube.clone(),
Unpacked::ByTheFill => b_cube.mul(&scale_cube),
};
let mut stages = Stages::smem(
&run,
&a_cube,
&staged,
comptime!(storage.clone()),
1usize,
comptime!(Some(decoded)),
);
stages.walk(run, Prefetch::InSlots, |slot, stage| {
let sum_stage = sum.at(stage);
let scale_stage = scale_cube.at(stage);
slot.consume(|a_stage, b_stage| {
for plane in stage {
let mut sum_plane = sum_stage.at(&plane);
let b_plane = match comptime!(unpacked) {
Unpacked::ByEachPlane => b_stage.at(&plane).mul(&scale_stage.at(&plane)),
Unpacked::ByTheFill => b_stage.at(&plane),
};
let b_plane = sum_plane.dequantized(&b_plane, decoded);
sum_plane.mma(&a_stage.at(&plane), &b_plane);
}
});
});
sum.drained_into(&c_cube);
}
}
#[derive(Clone, Copy, Debug)]
struct PlaneGrid {
rows: usize,
columns: usize,
}
impl PlaneGrid {
const ONE: Self = Self {
rows: 1,
columns: 1,
};
}
#[derive(Clone, Copy, Debug)]
enum StagesHeld {
Strided,
InPaddedBlocks,
}
impl StagesHeld {
fn storage(self, fragment: usize) -> StageStorage {
match self {
Self::Strided => StageStorage::Strided,
Self::InPaddedBlocks => StageStorage::Tiled {
block: vec![
(M, fragment),
(NB, 1),
(NI, fragment),
(KB, 1),
(KI, fragment),
],
chunks: RowChunks::Padded,
},
}
}
}
fn check_staged_stepped(
scales: TileScales,
elem: ElemType,
planes: PlaneGrid,
unpacked: Unpacked,
stages: StagesHeld,
) {
let (plane_rows, n_tiles, k_tiles, fragment) = (16, 2, 4, 8);
let rows = plane_rows * planes.rows;
let w = TileOrdered::new(rows, n_tiles, k_tiles);
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
if !require_cmma_8x8x8(&client, elem, dtype) {
return;
}
let c = TestInput::builder(client.clone(), shape![rows, w.cols()])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let levels = Levels::leaf(&[(M, fragment), (NI, fragment), (KI, fragment)])
.walk(&[
(M, plane_rows / fragment),
(NI, w.tile / fragment / planes.columns),
])
.walk(&[(KI, w.tile / fragment)])
.planes(&[(M, planes.rows), (NI, planes.columns)])
.walk_every(&[KB])
.cubes(&[NB])
.build();
let launcher = implied(&client, Partitioning::new(w.space(), levels), Form::Static);
let s_op = w.s_op(&client, &launcher, scales, w.tile / planes.columns);
staged_stepped_scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
w.a_op_blocked(&client, &launcher, elem).arg(),
w.b_op(&client, &launcher, Some(w.tile)).arg(),
s_op.arg(),
w.c_op(&launcher, &c).arg(),
launcher.partitioning_arg(),
unpacked,
stages.storage(fragment),
elem,
dtype,
);
w.check(
&client,
c,
&format!("staged stepped {scales:?} {elem:?} {unpacked:?} {stages:?}"),
);
}
#[test]
fn a_tile_ordered_weight_staged_as_its_words_lands_a_step_at_a_time() {
check_staged_stepped(
TileScales::F32,
f32::elem_type_native(),
PlaneGrid::ONE,
Unpacked::ByEachPlane,
StagesHeld::Strided,
);
}
#[test]
fn a_tile_ordered_weight_staged_as_its_words_lands_a_step_at_a_time_under_byte_scales() {
check_staged_stepped(
TileScales::Ue4m3,
f32::elem_type_native(),
PlaneGrid::ONE,
Unpacked::ByEachPlane,
StagesHeld::Strided,
);
}
#[test]
fn a_tile_ordered_weight_staged_as_its_words_lands_a_step_at_a_time_in_halves() {
check_staged_stepped(
TileScales::Ue4m3,
f16::elem_type_native(),
PlaneGrid::ONE,
Unpacked::ByEachPlane,
StagesHeld::Strided,
);
}
#[test]
fn a_tile_ordered_weight_staged_as_its_words_lands_a_step_at_a_time_for_every_plane_sharing_it() {
check_staged_stepped(
TileScales::Ue4m3,
f16::elem_type_native(),
PlaneGrid {
rows: 2,
columns: 1,
},
Unpacked::ByEachPlane,
StagesHeld::Strided,
);
}
#[test]
fn a_tile_ordered_weight_staged_as_its_words_lands_a_step_at_a_time_a_window_a_plane() {
check_staged_stepped(
TileScales::Ue4m3,
f16::elem_type_native(),
PlaneGrid {
rows: 1,
columns: 2,
},
Unpacked::ByEachPlane,
StagesHeld::Strided,
);
}
#[test]
fn a_tile_ordered_weight_decoded_by_the_stage_fill_contracts_as_its_values() {
check_staged_stepped(
TileScales::Ue4m3,
f16::elem_type_native(),
PlaneGrid::ONE,
Unpacked::ByTheFill,
StagesHeld::Strided,
);
}
#[test]
fn a_tile_ordered_weight_decoded_by_the_stage_fill_serves_every_plane_sharing_it() {
check_staged_stepped(
TileScales::Ue4m3,
f16::elem_type_native(),
PlaneGrid {
rows: 2,
columns: 1,
},
Unpacked::ByTheFill,
StagesHeld::Strided,
);
}
#[test]
fn a_tile_ordered_weight_decoded_by_the_stage_fill_lands_in_padded_blocks() {
check_staged_stepped(
TileScales::Ue4m3,
f16::elem_type_native(),
PlaneGrid {
rows: 2,
columns: 2,
},
Unpacked::ByTheFill,
StagesHeld::InPaddedBlocks,
);
}
#[test]
fn a_tile_ordered_weight_read_a_block_a_load_lands_a_step_at_a_time() {
check_stepped(TileScales::F32);
}
#[test]
fn a_tile_ordered_weight_read_a_block_a_load_lands_a_step_at_a_time_under_byte_scales() {
check_stepped(TileScales::Ue4m3);
}
#[cube(launch)]
fn scaled_copy<E: Numeric>(
a: &TileArg<'_>,
scale: &TileArg<'_>,
out: &TileArg<'_>,
space: Partitioning,
#[comptime] staged: bool,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let scale = scale.tile(&space);
let mut out = out.tile(&space);
if comptime!(staged) {
let mut stage = Tile::shared::<E>(comptime!(space.space().clone()), StageStorage::Strided);
stage.dequantize_from(&a.mul(&scale));
sync_cube();
out.copy_from(&stage);
} else {
out.dequantize_from(&a.mul(&scale));
}
}
fn check_scaled_copy(staged: bool) {
let (rows, block, blocks) = (4, 8, 4);
let depth = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth).map(|i| (i % 5) as f32 - 2.0).collect();
let s: Vec<f32> = (0..rows * blocks).map(|i| (i as f32 + 1.0) / 2.0).collect();
let (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (s_t, _) = TestInput::builder(client.clone(), shape![rows, blocks])
.dtype(dtype)
.custom(s.clone())
.generate_with_f32_host_data();
let out_shape = match staged {
true => shape![rows, blocks, block],
false => shape![rows, depth],
};
let out = TestInput::builder(client.clone(), out_shape)
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, rows), (KB, blocks), (KI, block)]),
Levels::leaf(&[(M, rows), (KB, blocks), (KI, block)]).build(),
),
Form::Static,
);
let projection = Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
);
scaled_copy::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(projection.clone()),
),
TileArgLaunch::new(
s_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(projection.scales_per(KB)),
),
TileArgLaunch::new(
out.clone().binding().into_tensor_arg(),
dtype,
1,
match staged {
true => TileSpec::direct(&[M, KB, KI]),
false => TileSpec::new(projection),
},
),
launcher.partitioning_arg(),
staged,
dtype,
);
let got = HostData::from_tensor_handle(&client, out, HostDataType::F32);
for m in 0..rows {
for k in 0..depth {
let want = a[m * depth + k] * s[m * blocks + k / block];
let have = match staged {
true => got.get_f32(&[m, k / block, k % block]),
false => got.get_f32(&[m, k]),
};
assert!(
(have - want).abs() < 1e-6,
"at ({m}, {k}): got {have}, want {want}"
);
}
}
}
#[test]
fn a_copy_of_scaled_values_decodes_them() {
check_scaled_copy(false);
}
#[test]
fn a_stage_filled_from_scaled_values_holds_them_decoded() {
check_scaled_copy(true);
}
#[cube(launch)]
fn staged_scaled_matmul(
a: &TileArg<'_>,
b: &TileArg<'_>,
scale: &TileArg<'_>,
c: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[comptime] depth: usize,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let scale = scale.tile(&space);
let mut c = c.tile(&space);
c.zero();
let walk = space.over(&level);
let mut stages = Stages::smem(
&walk,
&a.mul(&scale),
&b,
StageStorage::Strided,
depth,
None,
);
stages.pipelined(walk, |slot, region| {
let mut c_r = c
.at(region)
.accumulating(REGISTER_BLOCK, Semiring::SUM_PROD);
slot.consume(|a_s, b_s| {
c_r.mma(a_s, b_s);
});
});
}
fn check_staged_scaled_matmul(depth: usize) {
let (rows, cols, block, blocks) = (4, 4, 8, 4);
let depth_k = block * blocks;
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..rows * depth_k).map(|i| (i % 5) as f32 - 2.0).collect();
let b: Vec<f32> = (0..depth_k * 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 (a_t, _) = TestInput::builder(client.clone(), shape![rows, depth_k])
.dtype(dtype)
.custom(a.clone())
.generate_with_f32_host_data();
let (b_t, _) = TestInput::builder(client.clone(), shape![depth_k, cols])
.dtype(dtype)
.custom(b.clone())
.generate_with_f32_host_data();
let (s_t, _) = 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,
);
let a_projection = Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, block), (KI, 1)]),
],
);
staged_scaled_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(a_projection.clone()),
),
TileArgLaunch::new(
b_t.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_t.binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(a_projection.scales_per(KB)),
),
TileArgLaunch::new(
c.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
depth,
);
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_k)
.map(|k| a[m * depth_k + k] * s[m * blocks + k / block] * b[k * cols + n])
.sum();
let have = got.get_f32(&[m, n]);
assert!(
(have - want).abs() < 1e-3,
"depth {depth}, at ({m}, {n}): got {have}, want {want}"
);
}
}
}
#[test]
fn stages_filled_from_scaled_values_contract_them_decoded() {
check_staged_scaled_matmul(1);
check_staged_scaled_matmul(2);
}