use super::{Form, implied};
use crate::support::{HostData, HostDataType, TestInput, ValidationResult};
use cubecl::{
client::Client,
features::AtomicUsage,
ir::{ElemType, FloatKind, Type},
prelude::*,
std::tensor::TensorHandle,
zspace::shape,
};
use cubecl_tile::kind::Boundary;
use cubecl_tile::launch::AccumulateArg;
use cubecl_tile::launch::AccumulateArgLaunch;
use cubecl_tile::launch::BoundaryPolicy;
use cubecl_tile::*;
const ROW: Axis = Axis(0);
const COL: Axis = Axis(1);
const ROWS: usize = 12;
const COLS: usize = 10;
const TILE_ROWS: usize = 3;
const TILE_COLS: usize = 5;
const REGIONS: usize = (ROWS / TILE_ROWS) * (COLS / TILE_COLS);
#[cube(launch)]
fn copy_run(
src: &TileArg<'_>,
dst: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
#[comptime] cubes: usize,
) {
let src = src.tile(&space);
let dst = dst.tile(&space);
let walk = dst.over(&level);
let total = walk.total();
let pos = CUBE_POS_X as usize;
let start = pos * total / cubes;
let end = (pos + 1) * total / cubes;
for region in walk.range(start, end - start) {
dst.at(®ion).copy_from(&src.at(®ion));
}
}
#[cube(launch)]
fn copy_one_run(
src: &TileArg<'_>,
dst: &TileArg<'_>,
#[comptime] start: usize,
#[comptime] steps: usize,
space: Partitioning,
#[comptime] level: Level,
) {
let src = src.tile(&space);
let dst = dst.tile(&space);
let walk = dst.over(&level);
for region in walk.range(comptime!(start).runtime(), comptime!(steps).runtime()) {
dst.at(®ion).copy_from(&src.at(®ion));
}
}
struct Harness {
client: Client,
dtype: ElemType,
launcher: Launcher,
}
impl Harness {
fn new() -> Self {
Self {
client: cubecl::test_device().client(),
dtype: f32::elem_type_native(),
launcher: implied(
&cubecl::test_device().client(),
Partitioning::new(
Space::new(&[(ROW, ROWS), (COL, COLS)]),
Levels::leaf(&[(ROW, TILE_ROWS), (COL, TILE_COLS)])
.walk_every(&[ROW, COL])
.build(),
),
Form::Static,
),
}
}
fn source(&self) -> (TensorHandle, HostData) {
TestInput::builder(self.client.clone(), shape![ROWS, COLS])
.dtype(self.dtype)
.arange()
.generate_with_f32_host_data()
}
fn destination(&self) -> TensorHandle {
TestInput::builder(self.client.clone(), shape![ROWS, COLS])
.dtype(self.dtype)
.zeros()
.generate_without_host_data()
}
fn read(&self, output: TensorHandle) -> HostData {
HostData::from_tensor_handle(&self.client, output, HostDataType::F32)
}
fn tile_args(&self, src: &TensorHandle, dst: &TensorHandle) -> (TileArgLaunch, TileArgLaunch) {
(
TileArgLaunch::new(
src.clone().binding().into_tensor_arg(),
self.dtype,
1,
TileSpec::direct(&[ROW, COL]),
),
TileArgLaunch::new(
dst.clone().binding().into_tensor_arg(),
self.dtype,
1,
TileSpec::direct(&[ROW, COL]),
),
)
}
}
fn runs_cover_the_grid(cubes: usize) {
let h = Harness::new();
let (src, want) = h.source();
let dst = h.destination();
let (src_arg, dst_arg) = h.tile_args(&src, &dst);
copy_run::launch(
&h.client,
CubeCount::Static(cubes as u32, 1, 1),
h.launcher.cube_dim(),
src_arg,
dst_arg,
h.launcher.partitioning_arg(),
h.launcher.partitioning().level(0),
cubes,
);
let got = h.read(dst);
for row in 0..ROWS {
for col in 0..COLS {
assert_eq!(
got.get_f32(&[row, col]),
want.get_f32(&[row, col]),
"{cubes} cubes: cell ({row}, {col}) was not copied exactly once"
);
}
}
}
#[test]
fn runs_dividing_the_grid_cover_it_once() {
runs_cover_the_grid(4);
}
#[test]
fn runs_that_do_not_divide_the_grid_still_cover_it_once() {
runs_cover_the_grid(3);
runs_cover_the_grid(REGIONS + 2);
}
#[test]
fn a_run_starting_late_copies_the_regions_it_was_given() {
let h = Harness::new();
let (src, want) = h.source();
let dst = h.destination();
let (src_arg, dst_arg) = h.tile_args(&src, &dst);
let (start, steps) = (3usize, 2usize);
copy_one_run::launch(
&h.client,
CubeCount::Static(1, 1, 1),
h.launcher.cube_dim(),
src_arg,
dst_arg,
start,
steps,
h.launcher.partitioning_arg(),
h.launcher.partitioning().level(0),
);
let got = h.read(dst);
let cols_per_band = COLS / TILE_COLS;
for row in 0..ROWS {
for col in 0..COLS {
let region = (row / TILE_ROWS) * cols_per_band + col / TILE_COLS;
let copied = region >= start && region < start + steps;
let expect = match copied {
true => want.get_f32(&[row, col]),
false => 0.0,
};
assert_eq!(
got.get_f32(&[row, col]),
expect,
"cell ({row}, {col}) in region {region}"
);
}
}
}
const MM: Axis = Axis(2);
const NN: Axis = Axis(3);
const KK: Axis = Axis(4);
const BB: Axis = Axis(5);
const REGISTER_BLOCK: RegisterBlock = RegisterBlock::new(16);
#[cube]
fn contract(acc: &Tile, a: &Tile, b: &Tile, region: &Region) {
let mut acc_cell = acc.at(region);
acc_cell.mma(&a.at(region), &b.at(region));
}
#[cube(launch)]
fn stream_matmul<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
out: &AccumulateArg<'_, E>,
space: Partitioning,
#[comptime] outer: Level,
#[comptime] inner: Level,
#[comptime] leaf: Option<Level>,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let c = out.tile(&space);
let portion = space.over(&outer).portion(comptime!(inner.clone()));
for i in 0..portion.touched() {
let region = portion.region(i);
let (from, steps) = portion.steps(i);
let c_region = c.at(®ion);
let a_region = a.at(®ion);
let b_region = b.at(®ion);
let acc = c_region.accumulator::<E>(
&a_region,
&b_region,
comptime!(Instruction::Registers {
config: REGISTER_BLOCK
}),
Semiring::SUM_PROD,
);
for cell in region.over(&inner).range(from, steps) {
match comptime!(leaf.clone()) {
Some(leaf) => {
for step in cell.over(&leaf) {
contract(&acc, &a_region, &b_region, &step);
}
}
None => contract(&acc, &a_region, &b_region, &cell),
}
}
acc.drained_into(&c_region);
}
}
#[cube(launch)]
fn stream_matmul_staged_rhs<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
out: &AccumulateArg<'_, E>,
space: Partitioning,
#[comptime] outer: Level,
#[comptime] inner: Level,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let c = out.tile(&space);
let portion = space.over(&outer).portion(comptime!(inner.clone()));
for i in 0..portion.touched() {
let region = portion.region(i);
let (from, steps) = portion.steps(i);
let c_region = c.at(®ion);
let a_region = a.at(®ion);
let b_region = b.at(®ion);
let acc = c_region.accumulator::<E>(
&a_region,
&b_region,
comptime!(Instruction::Registers {
config: REGISTER_BLOCK
}),
Semiring::SUM_PROD,
);
let cells = region.over(&inner).range(from, steps);
let mut stages =
Stages::smem_single(&cells, &b_region, StageStorage::Strided, 1usize, None);
stages.pipelined(cells, |slot, cell| {
let mut acc_cell = acc.at(cell);
let a_cell = a_region.at(cell);
slot.consume(|b_s| {
acc_cell.mma(&a_cell, b_s);
});
});
acc.drained_into(&c_region);
}
}
const TILE_M: usize = 4;
const TILE_N: usize = 4;
const BLOCK_K: usize = 4;
#[derive(Clone, Copy)]
enum RhsStage {
InPlace,
Smem,
}
fn run_shared_index(m: usize, n: usize, k: usize, runs: usize, rhs: RhsStage) -> HostData {
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let a: Vec<f32> = (0..m * k).map(|i| (i % 7) as f32 - 3.0).collect();
let b: Vec<f32> = (0..k * n).map(|i| (i % 5) as f32 - 2.0).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![m, n])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(MM, m), (NN, n), (KK, k)]),
Levels::leaf(&[(MM, TILE_M), (NN, TILE_N), (KK, BLOCK_K)])
.walk(&[(MM, 1), (NN, 1), (KK, k / BLOCK_K)])
.cubes(&[MM, NN, KK])
.shared_by(runs)
.build(),
),
Form::Static,
);
match rhs {
RhsStage::InPlace => stream_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[MM, KK]),
),
TileArgLaunch::new(
b_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[KK, NN]),
),
AccumulateArgLaunch::new(
out.clone().binding().into_tensor_arg(),
1,
TileSpec::direct(&[MM, NN]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
launcher.partitioning().level(1),
None,
dtype,
),
RhsStage::Smem => stream_matmul_staged_rhs::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[MM, KK]),
),
TileArgLaunch::new(
b_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[KK, NN]),
),
AccumulateArgLaunch::new(
out.clone().binding().into_tensor_arg(),
1,
TileSpec::direct(&[MM, NN]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
launcher.partitioning().level(1),
dtype,
),
}
HostData::from_tensor_handle(&client, out, HostDataType::F32)
}
fn reference(m: usize, n: usize, k: usize) -> Vec<f32> {
let a = |i: usize, p: usize| ((i * k + p) % 7) as f32 - 3.0;
let b = |p: usize, j: usize| ((p * n + j) % 5) as f32 - 2.0;
let mut out = vec![0.0; m * n];
for i in 0..m {
for j in 0..n {
out[i * n + j] = (0..k).map(|p| a(i, p) * b(p, j)).sum();
}
}
out
}
fn shared_index_agrees_with_the_whole(cubes: usize) {
let (m, n, k) = (8usize, 8usize, 16usize);
let got = run_shared_index(m, n, k, cubes, RhsStage::InPlace);
let want = reference(m, n, k);
for i in 0..m {
for j in 0..n {
let have = got.get_f32(&[i, j]);
let want = want[i * n + j];
assert!(
(have - want).abs() < 1e-3,
"{cubes} runs: at ({i}, {j}): got {have}, want {want}"
);
}
}
}
#[test]
fn a_stream_of_one_run_is_the_whole_contraction() {
if !adds_atomically() {
return;
}
shared_index_agrees_with_the_whole(1);
}
#[test]
fn runs_that_end_on_a_tile_do_the_split_a_cut_would() {
if !adds_atomically() {
return;
}
shared_index_agrees_with_the_whole(4);
shared_index_agrees_with_the_whole(8);
}
#[test]
fn runs_that_straddle_a_tile_boundary_still_sum_to_the_whole() {
if !adds_atomically() {
return;
}
shared_index_agrees_with_the_whole(3);
shared_index_agrees_with_the_whole(5);
shared_index_agrees_with_the_whole(7);
shared_index_agrees_with_the_whole(24);
}
fn adds_atomically() -> bool {
let client = cubecl::test_device().client();
let adds = client
.properties()
.atomic_type_usage(Type::atomic(ElemType::Float(FloatKind::F32)))
.contains(AtomicUsage::Add);
if !adds {
ValidationResult::Skipped("device has no f32 atomic add".to_string()).enforce();
}
adds
}
#[test]
fn an_operand_stages_under_a_share_as_it_does_under_a_walk() {
if !adds_atomically() {
return;
}
let (m, n, k) = (8usize, 8usize, 16usize);
let want = reference(m, n, k);
for runs in [1usize, 3, 5] {
let got = run_shared_index(m, n, k, runs, RhsStage::Smem);
for i in 0..m {
for j in 0..n {
let have = got.get_f32(&[i, j]);
let want = want[i * n + j];
assert!(
(have - want).abs() < 1e-3,
"{runs} shares, rhs staged: at ({i}, {j}): got {have}, want {want}"
);
}
}
}
}
#[test]
fn cubes_take_shares_while_the_units_cut_k_between_them() {
let client = cubecl::test_device().client();
if !adds_atomically() {
return;
}
let dtype = f32::elem_type_native();
let plane_size = client.properties().hardware.plane_size_max as usize;
let (m, n, k) = (8usize, 8usize, 2 * plane_size);
let want = reference(m, n, k);
for runs in [1usize, 3, 5] {
let a: Vec<f32> = (0..m * k).map(|i| (i % 7) as f32 - 3.0).collect();
let b: Vec<f32> = (0..k * n).map(|i| (i % 5) as f32 - 2.0).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![m, n])
.dtype(dtype)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(MM, m), (NN, n), (KK, k)]),
Levels::leaf(&[(MM, TILE_M), (NN, TILE_N), (KK, 1)])
.walk(&[(KK, k / plane_size)])
.units(&[(KK, plane_size)])
.cubes(&[MM, NN, KK])
.shared_by(runs)
.build(),
),
Form::Static,
);
stream_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[MM, KK]),
),
TileArgLaunch::new(
b_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[KK, NN]),
),
AccumulateArgLaunch::new(
out.clone().binding().into_tensor_arg(),
1,
TileSpec::direct(&[MM, NN]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
launcher.partitioning().level(1),
Some(launcher.partitioning().level(2)),
dtype,
);
let got = HostData::from_tensor_handle(&client, out, HostDataType::F32);
for i in 0..m {
for j in 0..n {
let have = got.get_f32(&[i, j]);
let want = want[i * n + j];
assert!(
(have - want).abs() < 1e-2,
"{runs} shares over cubes, K over {plane_size} plane_size: at ({i}, {j}): got {have}, \
want {want}"
);
}
}
}
}
#[cube(launch)]
fn merged_stream_matmul(
a: &TileArg<'_>,
b: &TileArg<'_>,
slots: &TileArg<'_>,
out: &TileArg<'_>,
arrivals: &[Atomic<u32>],
space: Partitioning,
#[comptime] steps: Level,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let mut out = out.tile(&space);
let portion = space.walk().portion(comptime!(steps.clone()));
for i in 0..portion.touched() {
let region = portion.region(i);
let (from, count) = portion.steps(i);
let merge = slots.last_arrival::<Const<1>>(&space, &portion, i, arrivals);
let (a_region, b_region) = (a.at(®ion), b.at(®ion));
let acc = merge.tile().accumulator::<f32>(
&a_region,
&b_region,
comptime!(Instruction::Registers {
config: REGISTER_BLOCK
}),
Semiring::SUM_PROD,
);
for cell in region.over(&steps).range(from, count) {
contract(&acc, &a_region, &b_region, &cell);
}
merge.hand_on(&acc, &mut out);
}
}
#[derive(Clone, Copy, Debug)]
struct Product {
batches: usize,
m: usize,
n: usize,
k: usize,
}
impl Product {
fn single(m: usize, n: usize, k: usize) -> Self {
Product {
batches: 1,
m,
n,
k,
}
}
fn integers(&self) -> (Vec<f32>, Vec<f32>) {
let a = (0..self.batches * self.m * self.k)
.map(|i| (i % 7) as f32 - 3.0)
.collect();
let b = (0..self.k * self.n).map(|i| (i % 5) as f32 - 2.0).collect();
(a, b)
}
}
fn run_merged_stream<O: Float + CubeElement>(
product: Product,
(a, b): (Vec<f32>, Vec<f32>),
cubes: usize,
launches: usize,
) -> (Vec<HostData>, Vec<u32>) {
let Product { batches, m, n, k } = product;
let client = cubecl::test_device().client();
let (f32_ty, out_ty) = (f32::elem_type_native(), O::elem_type_native());
let a = TestInput::builder(client.clone(), shape![batches, m, k])
.dtype(f32_ty)
.custom(a)
.generate_without_host_data();
let b = TestInput::builder(client.clone(), shape![k, n])
.dtype(f32_ty)
.custom(b)
.generate_without_host_data();
let out = TestInput::builder(client.clone(), shape![batches, m, n])
.dtype(out_ty)
.zeros()
.generate_without_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(BB, batches), (MM, m), (NN, n), (KK, k)]),
Levels::leaf(&[(MM, TILE_M), (NN, TILE_N), (KK, BLOCK_K)])
.walk(&[(MM, 1), (NN, 1), (KK, k / BLOCK_K)])
.cubes(&[MM, NN, KK])
.shared_by(cubes)
.batches(&[BB])
.build(),
),
Form::Static,
);
let slots = launcher.partitioning().arrival_slots(&[BB, MM, NN]);
let held = vec![1.0e6; slots.num_elements()];
let slots = TestInput::builder(client.clone(), slots)
.dtype(f32_ty)
.custom(held)
.generate_without_host_data();
let counters = launcher.partitioning().arrival_counters();
let arrivals = client.create_from_slice(u32::as_bytes(&vec![0u32; counters]));
let spec =
|axes: &[Axis]| TileSpec::direct(axes).boundary(BoundaryPolicy::Every(Boundary::Zero));
let arg = |handle: &TensorHandle, axes: &[Axis]| {
TileArgLaunch::new(
handle.clone().binding().into_tensor_arg(),
handle.dtype,
1,
spec(axes),
)
};
let mut outputs = Vec::with_capacity(launches);
for _ in 0..launches {
merged_stream_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
arg(&a, &[BB, MM, KK]),
arg(&b, &[KK, NN]),
arg(&slots, &[BB, MM, NN]),
arg(&out, &[BB, MM, NN]),
unsafe { BufferArg::from_raw_parts(arrivals.clone(), counters) },
launcher.partitioning_arg(),
launcher.partitioning().level(1),
);
outputs.push(HostData::from_tensor_handle(
&client,
out.clone(),
HostDataType::F32,
));
}
let arrivals = u32::from_bytes(&client.read_one_unchecked(arrivals)).to_vec();
(outputs, arrivals)
}
fn merges_on_arrival(client: &Client) -> bool {
let hands_off = client.properties().features.device_memory_scope;
let counts = client
.properties()
.atomic_type_usage(Type::atomic(ElemType::UInt(cubecl::ir::UIntKind::U32)))
.contains(AtomicUsage::Add);
if !(hands_off && counts) {
ValidationResult::Skipped(
"device has no device-scope storage sync or no u32 atomic add".to_string(),
)
.enforce();
}
hands_off && counts
}
fn assert_merged(got: &HostData, product: Product, what: &str) {
let Product { batches, m, n, k } = product;
let (a, b) = product.integers();
for batch in 0..batches {
for i in 0..m {
for j in 0..n {
let want: f32 = (0..k)
.map(|p| a[(batch * m + i) * k + p] * b[p * n + j])
.sum();
assert_eq!(
got.get_f32(&[batch, i, j]),
want,
"{what}: ({batch}, {i}, {j})"
);
}
}
}
}
#[test]
fn the_last_arrival_merges_every_tile_of_a_shared_contraction() {
let client = cubecl::test_device().client();
if !merges_on_arrival(&client) {
return;
}
let product = Product::single(12, 16, 20);
for cubes in [1usize, 2, 5, 7, 11, 12, 13, 25, 59, 60, 61, 97] {
let (got, arrivals) = run_merged_stream::<half::f16>(product, product.integers(), cubes, 1);
assert_merged(&got[0], product, &format!("{cubes} cubes"));
assert!(
arrivals.iter().all(|&a| a == 0),
"{cubes} cubes: {arrivals:?}"
);
}
}
#[test]
fn the_last_arrival_merges_tiles_that_overhang_the_output() {
let client = cubecl::test_device().client();
if !merges_on_arrival(&client) {
return;
}
let product = Product::single(10, 14, 20);
for cubes in [3usize, 8, 23] {
let (got, arrivals) = run_merged_stream::<half::f16>(product, product.integers(), cubes, 1);
assert_merged(&got[0], product, &format!("{cubes} cubes"));
assert!(
arrivals.iter().all(|&a| a == 0),
"{cubes} cubes: {arrivals:?}"
);
}
}
#[test]
fn the_last_arrival_merges_the_tiles_of_every_batch() {
let client = cubecl::test_device().client();
if !merges_on_arrival(&client) {
return;
}
let product = Product {
batches: 3,
m: 8,
n: 12,
k: 20,
};
for cubes in [4usize, 7, 18, 31] {
let (got, arrivals) = run_merged_stream::<half::f16>(product, product.integers(), cubes, 1);
assert_merged(&got[0], product, &format!("{cubes} cubes"));
assert!(
arrivals.iter().all(|&a| a == 0),
"{cubes} cubes: {arrivals:?}"
);
}
}
#[test]
fn the_last_arrival_merges_again_on_the_counters_it_left() {
let client = cubecl::test_device().client();
if !merges_on_arrival(&client) {
return;
}
let product = Product::single(12, 16, 20);
let (got, arrivals) = run_merged_stream::<half::f16>(product, product.integers(), 7, 3);
assert_merged(&got[2], product, "a third launch");
assert!(arrivals.iter().all(|&a| a == 0), "{arrivals:?}");
}
#[test]
fn the_last_arrival_sums_the_same_bits_every_launch() {
let client = cubecl::test_device().client();
if !merges_on_arrival(&client) {
return;
}
let product = Product::single(12, 16, 20);
let Product { m, n, k, .. } = product;
let a = (0..m * k).map(|i| (i as f32 * 0.37).sin() * 3.1).collect();
let b = (0..k * n).map(|i| (i as f32 * 0.71).cos() / 1.7).collect();
let (got, _) = run_merged_stream::<f32>(product, (a, b), 29, 8);
let bits = |out: &HostData| -> Vec<u32> {
(0..m * n)
.map(|i| out.get_f32(&[0, i / n, i % n]).to_bits())
.collect()
};
let first = bits(&got[0]);
for (launch, out) in got.iter().enumerate().skip(1) {
assert_eq!(bits(out), first, "launch {launch} differs from the first");
}
}