#![allow(non_snake_case)]
use crate::support::{HostData, HostDataType, TestInput, ValidationResult};
use cubecl::{
features::AtomicUsage,
ir::{ElemType, FloatKind, Type},
prelude::*,
zspace::shape,
};
use cubecl_tile::kind::{Boundary, Write};
use cubecl_tile::launch::{BoundaryPolicy, Buffered, Destination, DestinationLaunch, Grid};
use cubecl_tile::space::{CubeOrder, PlaneOrder};
use cubecl_tile::*;
use super::{Form, kernel_form};
const M: Axis = Axis(0);
const N: Axis = Axis(1);
const K: Axis = Axis(2);
const B: Axis = Axis(3);
const BLOCK: [usize; 3] = [2, 2, 4];
const UNITS: usize = 2;
#[cube(launch)]
fn launch_plane_matmul<O: Destination>(
a: &TileArg<'_>,
b: &TileArg<'_>,
c: &O::Arg<f32>,
space: Partitioning,
) {
let (a, b) = (a.tile(&space), b.tile(&space));
let c = O::tile::<f32, Const<1>>(c, &space);
for plane in &space {
plane_product(&plane, &a, &b, &c);
}
}
#[cube(launch)]
fn cube_plane_matmul<O: Destination>(
a: &TileArg<'_>,
b: &TileArg<'_>,
c: &O::Arg<f32>,
space: Partitioning,
) {
let (a, b) = (a.tile(&space), b.tile(&space));
let c = O::tile::<f32, Const<1>>(c, &space);
for cube in &space {
for plane in cube {
plane_product(&plane, &a, &b, &c);
}
}
}
#[cube(launch)]
fn count_walks(walks: &mut Tensor<f32>, space: Partitioning) {
let mut taken = 0.0f32;
for _plane in &space {
taken += 1.0;
}
if UNIT_POS_X == 0 {
let cube = CUBE_POS_X + CUBE_POS_Y * CUBE_COUNT_X;
walks[(cube * CUBE_DIM_Y + UNIT_POS_Y) as usize] = taken;
}
}
#[cube]
fn plane_product(plane: &Region, a: &Tile, b: &Tile, c: &Tile) {
for unit in plane {
let (a_unit, b_unit, c_unit) = (a.at(&unit), b.at(&unit), c.at(&unit));
let sum = c_unit.accumulator::<f32>(
&a_unit,
&b_unit,
comptime!(Instruction::Registers {
config: RegisterBlock::new(BLOCK[0] * BLOCK[1])
}),
Semiring::SUM_PROD,
);
for step in &unit {
let mut sum_step = sum.at(&step);
sum_step.mma(&a_unit.at(&step), &b_unit.at(&step));
}
sum.drained_into(&c_unit);
}
}
#[derive(Clone, Debug)]
enum Planes {
OfEachCube,
OfTheLaunch(PlaneOrder),
}
impl Planes {
fn levels(&self, reversed: &[Axis]) -> Vec<Level> {
let [rows, columns, depth] = BLOCK;
let units = Levels::leaf(&[(M, rows), (N, columns), (K, depth)])
.walk_every(&[K])
.units(&[(N, UNITS)]);
let outermost = match self {
Planes::OfEachCube => units.planes(&[(M, 2), (N, 2)]).cubes(&[M, N]).batches(&[B]),
Planes::OfTheLaunch(order) => units.launch_planes(&[M, N, B], order.clone()),
};
reversed
.iter()
.fold(outermost, |levels, &axis| levels.reversed(axis))
.build()
}
}
#[derive(Clone, Copy, Debug)]
struct Problem {
m: usize,
n: usize,
k: usize,
batches: usize,
}
impl Problem {
const fn new(m: usize, n: usize, k: usize) -> Self {
Self {
m,
n,
k,
batches: 1,
}
}
fn batched(self, batches: usize) -> Self {
Self { batches, ..self }
}
}
fn lhs(i: usize, p: usize, k: usize) -> f32 {
((i * k + p) % 7) as f32 - 3.0
}
fn rhs(p: usize, j: usize, n: usize) -> f32 {
((p * n + j) % 5) as f32 - 2.0
}
fn adds_atomically(client: &cubecl::client::Client) -> bool {
let usage = client
.properties()
.atomic_type_usage(Type::atomic(ElemType::Float(FloatKind::F32)));
let adds = usage.contains(AtomicUsage::Add);
if !adds {
ValidationResult::Skipped("device has no f32 atomic add".to_string()).enforce();
}
adds
}
#[derive(Clone, Copy, Debug)]
struct Dealt<'a> {
reversed: &'a [Axis],
form: Form<'a>,
}
impl Dealt<'_> {
const IN_ORDER: Dealt<'static> = Dealt {
reversed: &[],
form: Form::Static,
};
}
pub(super) fn holds_a_cube(client: &cubecl::client::Client, partitioning: &Partitioning) -> bool {
let units = partitioning.cube_dim(UNITS as u32).num_elems();
let most = client.properties().hardware.max_units_per_cube;
let holds = units <= most;
if !holds {
ValidationResult::Skipped(format!(
"a cube of {units} units, past the {most} this device holds"
))
.enforce();
}
holds
}
fn run(problem: Problem, planes: &Planes, dealt: Dealt<'_>) -> Option<HostData> {
let Problem { m, n, k, batches } = problem;
let client = cubecl::test_device().client();
if !adds_atomically(&client) {
return None;
}
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..m * k).map(|i| lhs(i / k, i % k, k)).collect();
let b: Vec<f32> = (0..k * n).map(|i| rhs(i / n, i % n, n)).collect();
let (a_handle, _) = TestInput::builder(client.clone(), shape![m, k])
.dtype(dtype)
.custom(a)
.generate_with_f32_host_data();
let (b_handle, _) = TestInput::builder(client.clone(), shape![k, n])
.dtype(dtype)
.custom(b)
.generate_with_f32_host_data();
let out = TestInput::builder(client.clone(), shape![batches, m, n])
.dtype(dtype)
.custom(vec![0.0; batches * m * n])
.generate_without_host_data();
let partitioning = Partitioning::new(
Space::new(&[(B, batches), (M, m), (N, n), (K, k)]),
planes.levels(dealt.reversed),
);
if !holds_a_cube(&client, &partitioning) {
return None;
}
let grid = Grid::Stated {
cube_count: partitioning.cube_count(),
cube_dim: partitioning.cube_dim(UNITS as u32),
};
let space = partitioning.space().clone();
let kernel = kernel_form(&partitioning, dealt.form);
let launcher = Launcher::new(&client, kernel, &space, grid).unwrap();
let policy = BoundaryPolicy::Every(Boundary::Zero);
let bound = launcher
.arg(out.clone().binding(), dtype)
.axes(&[B, M, N])
.build()
.unwrap();
let a_arg = TileArgLaunch::new(
a_handle.binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, K]).boundary(policy),
);
let b_arg = TileArgLaunch::new(
b_handle.binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[K, N]).boundary(policy),
);
let c_arg = Buffered::arg((bound, Write::Accumulate));
match planes {
Planes::OfTheLaunch(_) => launch_plane_matmul::launch::<Buffered>(
&client,
launcher.cube_count(),
launcher.cube_dim(),
a_arg,
b_arg,
c_arg,
launcher.partitioning_arg(),
),
Planes::OfEachCube => cube_plane_matmul::launch::<Buffered>(
&client,
launcher.cube_count(),
launcher.cube_dim(),
a_arg,
b_arg,
c_arg,
launcher.partitioning_arg(),
),
}
Some(HostData::from_tensor_handle(
&client,
out,
HostDataType::F32,
))
}
fn assert_every_tile_once(got: &HostData, problem: Problem, what: &str) {
let Problem { m, n, k, batches } = problem;
for batch in 0..batches {
for i in 0..m {
for j in 0..n {
let want: f32 = (0..k).map(|p| lhs(i, p, k) * rhs(p, j, n)).sum();
assert_eq!(
got.get_f32(&[batch, i, j]),
want,
"{what}: at ({batch}, {i}, {j})"
);
}
}
}
}
const PROBLEMS: [Problem; 3] = [
Problem::new(8, 16, 8),
Problem::new(7, 29, 11),
Problem::new(13, 5, 6),
];
fn assert_takes_every_tile_once(planes: Planes, dealt: Dealt<'_>, what: &str) {
for problem in PROBLEMS {
if let Some(got) = run(problem, &planes, dealt) {
assert_every_tile_once(&got, problem, what);
}
}
}
#[test]
fn a_cube_level_holding_planes_takes_every_tile_once() {
assert_takes_every_tile_once(Planes::OfEachCube, Dealt::IN_ORDER, "cubes holding planes");
}
#[test]
fn one_block_a_cube_takes_every_tile_once() {
let order = PlaneOrder::new(&[(M, 2), (N, 2)], CubeOrder::RowMajor);
assert_takes_every_tile_once(
Planes::OfTheLaunch(order),
Dealt::IN_ORDER,
"one block a cube",
);
}
#[test]
fn planes_past_the_last_tile_take_none() {
let order = PlaneOrder::new(&[], CubeOrder::RowMajor).cubes_of(3);
assert_takes_every_tile_once(
Planes::OfTheLaunch(order),
Dealt::IN_ORDER,
"three planes a cube",
);
}
#[test]
fn a_block_spans_several_cubes() {
let order = PlaneOrder::new(&[(N, 4)], CubeOrder::RowMajor).cubes_of(2);
assert_takes_every_tile_once(
Planes::OfTheLaunch(order),
Dealt::IN_ORDER,
"a block across two cubes",
);
}
#[test]
fn swizzled_blocks_take_every_tile_once() {
for block_order in [CubeOrder::SwizzleRow(2), CubeOrder::SwizzleCol(3)] {
let order = PlaneOrder::new(&[(M, 2), (N, 1)], block_order);
assert_takes_every_tile_once(
Planes::OfTheLaunch(order),
Dealt::IN_ORDER,
&format!("{block_order:?}"),
);
}
}
#[test]
fn the_batches_ride_outermost() {
for order in [
PlaneOrder::new(&[(M, 2), (N, 2)], CubeOrder::RowMajor),
PlaneOrder::new(&[], CubeOrder::RowMajor).cubes_of(3),
PlaneOrder::new(&[(M, 2), (N, 1)], CubeOrder::SwizzleRow(2)),
] {
let planes = Planes::OfTheLaunch(order);
for problem in PROBLEMS.map(|problem| problem.batched(3)) {
if let Some(got) = run(problem, &planes, Dealt::IN_ORDER) {
assert_every_tile_once(&got, problem, &format!("three batches, {planes:?}"));
}
}
}
}
const REVERSALS: [&[Axis]; 4] = [&[M], &[N], &[M, N], &[M, N, B]];
fn assert_reversed_takes_every_tile_once(planes: Planes) {
for reversed in REVERSALS {
for form in [Form::Static, Form::Dynamic] {
let dealt = Dealt { reversed, form };
for problem in PROBLEMS.map(|problem| problem.batched(3)) {
if let Some(got) = run(problem, &planes, dealt) {
assert_every_tile_once(&got, problem, &format!("{planes:?}, {dealt:?}"));
}
}
}
}
}
#[test]
fn reversed_cubes_take_every_tile_once() {
assert_reversed_takes_every_tile_once(Planes::OfEachCube);
}
#[test]
fn reversed_launch_planes_take_every_tile_once() {
for order in [
PlaneOrder::new(&[(M, 2), (N, 2)], CubeOrder::RowMajor),
PlaneOrder::new(&[(M, 2), (N, 1)], CubeOrder::SwizzleRow(2)),
PlaneOrder::new(&[], CubeOrder::RowMajor).cubes_of(3),
] {
assert_reversed_takes_every_tile_once(Planes::OfTheLaunch(order));
}
}
#[test]
fn the_launch_holds_every_block() {
let partitioning = |order| {
Partitioning::new(
Space::new(&[(B, 1), (M, 7), (N, 29), (K, 11)]),
Levels::leaf(&[(M, 2), (N, 2), (K, 4)])
.walk_every(&[K])
.units(&[(N, UNITS)])
.launch_planes(&[M, N, B], order)
.build(),
)
};
let cubes = |partitioning: &Partitioning| match partitioning.cube_count() {
CubeCount::Static(x, y, z) => (x, y, z),
CubeCount::Dynamic(_) => panic!("a static space spawns a static grid"),
};
let one_block_a_cube = partitioning(PlaneOrder::new(&[(M, 2), (N, 2)], CubeOrder::RowMajor));
assert_eq!(cubes(&one_block_a_cube), (8, 1, 1));
assert_eq!(one_block_a_cube.planes_per_cube(), 4);
assert_eq!(one_block_a_cube.cube_dim(2), CubeDim::new_2d(2, 4));
let three_a_cube = partitioning(PlaneOrder::new(&[], CubeOrder::RowMajor).cubes_of(3));
assert_eq!(cubes(&three_a_cube), (11, 1, 1));
}
fn orders() -> [PlaneOrder; 4] {
[
PlaneOrder::new(&[(M, 2), (N, 2)], CubeOrder::RowMajor),
PlaneOrder::new(&[], CubeOrder::RowMajor).cubes_of(3),
PlaneOrder::new(&[(N, 4)], CubeOrder::RowMajor).cubes_of(2),
PlaneOrder::new(&[(M, 2), (N, 1)], CubeOrder::SwizzleCol(3)),
]
}
#[test]
fn every_plane_of_the_launch_walks_one_tile() {
let client = cubecl::test_device().client();
for order in orders() {
for problem in PROBLEMS.map(|problem| problem.batched(2)) {
let Problem { m, n, k, batches } = problem;
let partitioning = Partitioning::new(
Space::new(&[(B, batches), (M, m), (N, n), (K, k)]),
Planes::OfTheLaunch(order.clone()).levels(&[]),
);
if !holds_a_cube(&client, &partitioning) {
continue;
}
let cube_count = partitioning.cube_count();
let CubeCount::Static(x, y, z) = cube_count else {
panic!("a static space spawns a static grid");
};
let planes = (x * y * z * partitioning.planes_per_cube()) as usize;
let walks = TestInput::builder(client.clone(), shape![planes])
.dtype(f32::elem_type_native())
.custom(vec![0.0; planes])
.generate_without_host_data();
let grid = Grid::Stated {
cube_count,
cube_dim: partitioning.cube_dim(UNITS as u32),
};
let space = partitioning.space().clone();
let launcher = Launcher::new(&client, partitioning, &space, grid).unwrap();
count_walks::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
walks.clone().binding().into_tensor_arg(),
launcher.partitioning_arg(),
);
let got = HostData::from_tensor_handle(&client, walks, HostDataType::F32);
for plane in 0..planes {
assert_eq!(
got.get_f32(&[plane]),
1.0,
"{order:?} on {problem:?}: plane {plane} of {planes}"
);
}
}
}
}
#[test]
fn the_binding_masks_where_the_planes_run_past_the_tiles() {
let masked = |order: PlaneOrder, (m, n): (usize, usize)| {
let partitioning = Partitioning::new(
Space::new(&[(B, 1), (M, m), (N, n), (K, 8)]),
Planes::OfTheLaunch(order).levels(&[]),
);
[M, N, B].map(|axis| partitioning.overhangs(axis))
};
let blocks = || PlaneOrder::new(&[(M, 2), (N, 2)], CubeOrder::RowMajor);
assert_eq!(masked(blocks(), (8, 16)), [false, false, false]);
assert_eq!(masked(blocks(), (8, 20)), [false, true, false]);
let three_a_cube = PlaneOrder::new(&[], CubeOrder::RowMajor).cubes_of(3);
assert_eq!(masked(three_a_cube, (8, 16)), [false, false, true]);
let three_a_cube = PlaneOrder::new(&[], CubeOrder::RowMajor).cubes_of(3);
assert_eq!(masked(three_a_cube, (6, 20)), [false, false, false]);
}
#[test]
fn a_static_axis_is_answered_beside_a_dynamic_batch() {
let partitioning = |n| {
Partitioning::new(
Space::new(&[(B, 1), (M, 8), (N, n), (K, 8)]).with_dynamic(&[B]),
Planes::OfTheLaunch(PlaneOrder::new(&[(M, 2), (N, 2)], CubeOrder::RowMajor))
.levels(&[]),
)
};
assert!(!partitioning(16).overhangs(M));
assert!(!partitioning(16).overhangs(N));
assert!(partitioning(20).overhangs(N));
}