#![allow(non_snake_case)]
use crate::support::{HostData, HostDataType, TestInput, ValidationResult};
use cubecl::{
CubeCount, CubeDim,
features::AtomicUsage,
ir::{ElemType, FloatKind, Type},
prelude::*,
zspace::shape,
};
use super::{Form, implied};
use cubecl_tile::launch::AccumulateArg;
use cubecl_tile::launch::AccumulateArgLaunch;
use cubecl_tile::layout::PhysicalAxisMap;
use cubecl_tile::*;
const M: Axis = Axis(0);
const N: Axis = Axis(1);
const KB: Axis = Axis(2);
const KI: Axis = Axis(3);
const REGISTER_BLOCK: RegisterBlock = RegisterBlock::new(16);
#[cube(launch)]
fn split_partials(
a: &TileArg<'_>,
b: &TileArg<'_>,
partials: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let partials = partials.tile(&space);
for region in space.over(&level) {
let mut partials_cube = partials
.at(®ion)
.accumulating(REGISTER_BLOCK, Semiring::SUM_PROD);
partials_cube.mm(&a.at(®ion), &b.at(®ion));
}
}
#[cube(launch)]
fn reduce_splits(
partials: &TileArg<'_>,
out: &TileArg<'_>,
space: Partitioning,
#[comptime] level: Level,
) {
let partials = partials.tile(&space);
let out = out.tile(&space);
for region in space.over(&level) {
let mut out_cube = out.at(®ion);
out_cube.reduce_axis(&partials.at(®ion), Monoid::Sum);
}
}
fn run_split_contraction(m: usize, n: usize, k: usize, splits: usize) -> (HostData, HostData) {
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
assert!(k.is_multiple_of(splits), "the test shapes divide evenly");
let inside = k / splits;
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 partials = TestInput::builder(client.clone(), shape![splits, m, n])
.dtype(dtype)
.uniform(4242, 10., 100.)
.generate_without_host_data();
let out = TestInput::builder(client.clone(), shape![m, n])
.dtype(dtype)
.uniform(4243, 10., 100.)
.generate_without_host_data();
let split_space = implied(
&client,
Partitioning::new(
Space::new(&[(M, m), (N, n), (KB, splits), (KI, inside)]),
Levels::leaf(&[(KB, 1)]).cubes(&[KB]).build(),
),
Form::Static,
);
split_partials::launch(
&client,
split_space.cube_count(),
split_space.cube_dim(),
TileArgLaunch::new(
a_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[M, KB, KI],
&[
PhysicalAxisMap::of(M),
PhysicalAxisMap::disjoint(&[(KB, inside), (KI, 1)]),
],
)),
),
TileArgLaunch::new(
b_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::new(Projection::new(
&[KB, KI, N],
&[
PhysicalAxisMap::disjoint(&[(KB, inside), (KI, 1)]),
PhysicalAxisMap::of(N),
],
)),
),
TileArgLaunch::new(
partials.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[KB, M, N]),
),
split_space.partitioning_arg(),
split_space.partitioning().level(0),
);
let reduce_space = implied(
&client,
Partitioning::new(
Space::new(&[(M, m), (N, n), (KB, splits)]),
Levels::leaf(&[(M, 1)]).cubes(&[M]).build(),
),
Form::Static,
);
reduce_splits::launch(
&client,
reduce_space.cube_count(),
reduce_space.cube_dim(),
TileArgLaunch::new(
partials.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[KB, M, N]),
),
TileArgLaunch::new(
out.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
reduce_space.partitioning_arg(),
reduce_space.partitioning().level(0),
);
(
HostData::from_tensor_handle(&client, partials, HostDataType::F32),
HostData::from_tensor_handle(&client, out, HostDataType::F32),
)
}
fn reference(m: usize, n: usize, k: usize, rows: std::ops::Range<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;
(0..m * n)
.map(|idx| {
let (i, j) = (idx / n, idx % n);
rows.clone().map(|p| a(i, p) * b(p, j)).sum()
})
.collect()
}
#[test]
fn a_split_contraction_sums_back_to_the_whole() {
let (m, n, k, splits) = (4usize, 4usize, 16usize, 4usize);
let inside = k / splits;
let (partials, out) = run_split_contraction(m, n, k, splits);
for s in 0..splits {
let want = reference(m, n, k, s * inside..(s + 1) * inside);
for i in 0..m {
for j in 0..n {
let have = partials.get_f32(&[s, i, j]);
let want = want[i * n + j];
assert!(
(have - want).abs() < 1e-3,
"split {s} at ({i}, {j}): got {have}, want {want}"
);
}
}
}
let want = reference(m, n, k, 0..k);
for i in 0..m {
for j in 0..n {
let have = out.get_f32(&[i, j]);
let want = want[i * n + j];
assert!(
(have - want).abs() < 1e-3,
"total at ({i}, {j}): got {have}, want {want}"
);
}
}
}
#[test]
fn a_split_of_one_is_the_whole_contraction() {
let (m, n, k) = (4usize, 4usize, 16usize);
let (_, out) = run_split_contraction(m, n, k, 1);
let want = reference(m, n, k, 0..k);
for i in 0..m {
for j in 0..n {
let have = out.get_f32(&[i, j]);
let want = want[i * n + j];
assert!(
(have - want).abs() < 1e-3,
"at ({i}, {j}): got {have}, want {want}"
);
}
}
}
#[cube(launch)]
fn atomic_add_probe(out: &mut Tensor<Atomic<f32>>) {
if UNIT_POS == 0 {
out[0].fetch_add(1.0);
}
}
#[test]
fn the_device_adds_floats_atomically_across_cubes() {
let client = cubecl::test_device().client();
if !client
.properties()
.atomic_type_usage(Type::atomic(ElemType::Float(FloatKind::F32)))
.contains(AtomicUsage::Add)
{
ValidationResult::Skipped("device has no f32 atomic add".to_string()).enforce();
return;
}
let cubes = 64u32;
let out = TestInput::builder(client.clone(), shape![1])
.dtype(f32::elem_type_native())
.zeros()
.generate_without_host_data();
atomic_add_probe::launch(
&client,
CubeCount::new_1d(cubes),
CubeDim::new_single(),
out.clone().binding().into_tensor_arg(),
);
let got = HostData::from_tensor_handle(&client, out, HostDataType::F32);
assert_eq!(got.get_f32(&[0]), cubes as f32);
}
const K: Axis = Axis(4);
#[cube(launch)]
fn atomic_split_matmul<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
out: &AccumulateArg<'_, E>,
space: Partitioning,
#[comptime] level: Level,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let c = out.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_region = acc.at(®ion);
acc_region.mma(&a.at(®ion), &b.at(®ion));
}
acc.drained_into(&c);
}
#[cube(launch)]
fn atomic_split_matmul_by_unit<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
out: &AccumulateArg<'_, E>,
space: Partitioning,
#[comptime] cubes: Level,
#[comptime] plane_units: Level,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let c = out.tile(&space);
let acc = c.accumulator::<E>(
&a,
&b,
comptime!(Instruction::Registers {
config: REGISTER_BLOCK
}),
Semiring::SUM_PROD,
);
for cube in space.over(&cubes) {
for unit in cube.over(&plane_units) {
let mut acc_unit = acc.at(&unit);
acc_unit.mma(&a.at(&unit), &b.at(&unit));
}
}
acc.drained_into(&c);
}
fn run_atomic_split_contraction(m: usize, n: usize, k: usize, splits: usize) -> 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(&[(M, m), (N, n), (K, k)]),
Levels::leaf(&[(K, k / splits)]).cubes(&[K]).build(),
),
Form::Static,
);
atomic_split_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, K]),
),
TileArgLaunch::new(
b_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[K, N]),
),
AccumulateArgLaunch::new(
out.clone().binding().into_tensor_arg(),
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
dtype,
);
HostData::from_tensor_handle(&client, out, HostDataType::F32)
}
#[test]
fn an_atomic_drain_sums_the_slices_back_together() {
let client = cubecl::test_device().client();
if !client
.properties()
.atomic_type_usage(Type::atomic(ElemType::Float(FloatKind::F32)))
.contains(AtomicUsage::Add)
{
ValidationResult::Skipped("device has no f32 atomic add".to_string()).enforce();
return;
}
let (m, n, k, splits) = (4usize, 4usize, 16usize, 4usize);
let got = run_atomic_split_contraction(m, n, k, splits);
let want = reference(m, n, k, 0..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,
"at ({i}, {j}): got {have}, want {want}"
);
}
}
}
#[test]
fn the_atomic_drain_agrees_with_the_workspace() {
let client = cubecl::test_device().client();
if !client
.properties()
.atomic_type_usage(Type::atomic(ElemType::Float(FloatKind::F32)))
.contains(AtomicUsage::Add)
{
ValidationResult::Skipped("device has no f32 atomic add".to_string()).enforce();
return;
}
let (m, n, k, splits) = (4usize, 4usize, 16usize, 4usize);
let (_, workspace) = run_split_contraction(m, n, k, splits);
let atomic = run_atomic_split_contraction(m, n, k, splits);
for i in 0..m {
for j in 0..n {
let w = workspace.get_f32(&[i, j]);
let a = atomic.get_f32(&[i, j]);
assert!(
(w - a).abs() < 1e-3,
"at ({i}, {j}): workspace {w}, atomic {a}"
);
}
}
}
#[test]
fn an_atomic_drain_with_units_of_their_own() {
let client = cubecl::test_device().client();
if !client
.properties()
.atomic_type_usage(Type::atomic(ElemType::Float(FloatKind::F32)))
.contains(AtomicUsage::Add)
{
ValidationResult::Skipped("device has no f32 atomic add".to_string()).enforce();
return;
}
let plane_size = client.properties().hardware.plane_size_max as usize;
let dtype = f32::elem_type_native();
let (m, k, splits, per_unit) = (4usize, 16usize, 4usize, 2usize);
let n = plane_size * per_unit;
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(&[(M, m), (N, n), (K, k)]),
Levels::leaf(&[(N, per_unit), (K, k / splits)])
.units(&[(N, plane_size)])
.cubes(&[K])
.build(),
),
Form::Static,
);
atomic_split_matmul_by_unit::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, K]),
),
TileArgLaunch::new(
b_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[K, N]),
),
AccumulateArgLaunch::new(
out.clone().binding().into_tensor_arg(),
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
launcher.partitioning().level(1),
dtype,
);
let got = HostData::from_tensor_handle(&client, out, HostDataType::F32);
let want = reference(m, n, k, 0..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,
"at ({i}, {j}): got {have}, want {want}"
);
}
}
}
#[test]
fn an_atomic_drain_sums_across_planes() {
let client = cubecl::test_device().client();
if !client
.properties()
.atomic_type_usage(Type::atomic(ElemType::Float(FloatKind::F32)))
.contains(AtomicUsage::Add)
{
ValidationResult::Skipped("device has no f32 atomic add".to_string()).enforce();
return;
}
let dtype = f32::elem_type_native();
let (m, n, k, num_planes) = (4usize, 4usize, 16usize, 4usize);
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(&[(M, m), (N, n), (K, k)]),
Levels::leaf(&[(K, k / num_planes)])
.planes(&[(K, num_planes)])
.build(),
),
Form::Static,
);
atomic_split_matmul::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, K]),
),
TileArgLaunch::new(
b_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[K, N]),
),
AccumulateArgLaunch::new(
out.clone().binding().into_tensor_arg(),
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
dtype,
);
let got = HostData::from_tensor_handle(&client, out, HostDataType::F32);
let want = reference(m, n, k, 0..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,
"at ({i}, {j}): got {have}, want {want}"
);
}
}
}
#[cube(launch)]
fn atomic_split_matmul_in_place<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
out: &AccumulateArg<'_, E>,
space: Partitioning,
#[comptime] level: Level,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let c = out.tile(&space);
for region in space.over(&level) {
let mut c_region = c
.at(®ion)
.accumulating(REGISTER_BLOCK, Semiring::SUM_PROD);
c_region.mm(&a.at(®ion), &b.at(®ion));
}
}
#[test]
fn a_summing_output_contracts_in_place() {
let client = cubecl::test_device().client();
if !client
.properties()
.atomic_type_usage(Type::atomic(ElemType::Float(FloatKind::F32)))
.contains(AtomicUsage::Add)
{
ValidationResult::Skipped("device has no f32 atomic add".to_string()).enforce();
return;
}
let dtype = f32::elem_type_native();
let (m, n, k, splits) = (4usize, 4usize, 16usize, 4usize);
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(&[(M, m), (N, n), (K, k)]),
Levels::leaf(&[(K, k / splits)]).cubes(&[K]).build(),
),
Form::Static,
);
atomic_split_matmul_in_place::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, K]),
),
TileArgLaunch::new(
b_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[K, N]),
),
AccumulateArgLaunch::new(
out.clone().binding().into_tensor_arg(),
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
launcher.partitioning().level(0),
dtype,
);
let got = HostData::from_tensor_handle(&client, out, HostDataType::F32);
let want = reference(m, n, k, 0..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,
"at ({i}, {j}): got {have}, want {want}"
);
}
}
}
#[cube(launch)]
fn atomic_split_cmma<E: Numeric>(
a: &TileArg<'_>,
b: &TileArg<'_>,
out: &AccumulateArg<'_, E>,
space: Partitioning,
#[define(E)] _dtype: ElemType,
) {
let a = a.tile(&space);
let b = b.tile(&space);
let c = out.tile(&space);
for cube in space {
let a_cube = a.at(&cube);
let b_cube = b.at(&cube);
let c_cube = c.at(&cube);
let acc = c_cube
.accumulator::<E>(&a_cube, &b_cube, Instruction::Cmma, Semiring::SUM_PROD)
.with_scratch(Scratch::OneTile);
let walk = cube.walk();
let mut stages = Stages::smem(
&walk,
&a_cube,
&b_cube,
comptime!(StageStorage::Strided),
1usize,
None,
);
stages.pipelined(walk, |slot, stage| {
let mut acc_s = acc.at(stage);
slot.consume(|a_s, b_s| {
acc_s.mma(a_s, b_s);
});
});
acc.drained_into(&c_cube);
}
}
fn sums_fragments(client: &cubecl::client::Client) -> bool {
let f32_ty = f32::elem_type_native();
let cmma = client.properties().features.matmul.cmma.iter().any(|cfg| {
cfg.a_type == f32_ty
&& cfg.b_type == f32_ty
&& cfg.cd_type == f32_ty
&& cfg.m == 8
&& cfg.n == 8
&& cfg.k == 8
});
let adds = client
.properties()
.atomic_type_usage(Type::atomic(ElemType::Float(FloatKind::F32)))
.contains(AtomicUsage::Add);
if !(cmma && adds) {
ValidationResult::Skipped(
"device has no 8x8x8 f32 cmma fragment or no f32 atomic add".to_string(),
)
.enforce();
}
cmma && adds
}
fn run_atomic_split_cmma(k: usize, splits: usize) -> HostData {
let client = cubecl::test_device().client();
let dtype = f32::elem_type_native();
let (m, n, edge) = (8usize, 8usize, 8usize);
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(&[(M, m), (N, n), (K, k)]),
Levels::leaf(&[(M, m), (N, n), (K, edge)])
.walk_every(&[K])
.cubes(&[M, N, K])
.across(K, splits)
.build(),
),
Form::Static,
);
atomic_split_cmma::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
a_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, K]),
),
TileArgLaunch::new(
b_handle.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[K, N]),
),
AccumulateArgLaunch::new(
out.clone().binding().into_tensor_arg(),
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
dtype,
);
HostData::from_tensor_handle(&client, out, HostDataType::F32)
}
#[test]
fn a_fragment_sums_into_the_output_through_the_scratch() {
let client = cubecl::test_device().client();
if !sums_fragments(&client) {
return;
}
let (m, n, k) = (8usize, 8usize, 64usize);
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;
for splits in [1usize, 2, 4, 8] {
let got = run_atomic_split_cmma(k, splits);
for i in 0..m {
for j in 0..n {
let want: f32 = (0..k).map(|p| a(i, p) * b(p, j)).sum();
assert_eq!(got.get_f32(&[i, j]), want, "{splits} splits: ({i}, {j})");
}
}
}
}
fn adds_atomically(client: &cubecl::client::Client) -> bool {
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
}
#[cube(launch)]
fn copy_into_summing<E: Numeric>(
src: &TileArg<'_>,
out: &AccumulateArg<'_, E>,
space: Partitioning,
#[define(E)] _dtype: ElemType,
) {
let src = src.tile(&space);
let mut out = out.tile(&space);
out.copy_from(&src);
}
#[test]
fn a_copy_into_a_summing_output_adds() {
let client = cubecl::test_device().client();
if !adds_atomically(&client) {
return;
}
let dtype = f32::elem_type_native();
let (rows, cols) = (4usize, 8usize);
let values: Vec<f32> = (0..rows * cols).map(|i| (i % 5) as f32 + 1.0).collect();
let (src, _) = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.custom(values.clone())
.generate_with_f32_host_data();
let seed = 10.0f32;
let (out, _) = TestInput::builder(client.clone(), shape![rows, cols])
.dtype(dtype)
.custom(vec![seed; rows * cols])
.generate_with_f32_host_data();
let launcher = implied(
&client,
Partitioning::new(
Space::new(&[(M, rows), (N, cols)]),
Levels::leaf(&[(M, rows), (N, cols)]).cubes(&[M, N]).build(),
),
Form::Static,
);
copy_into_summing::launch(
&client,
launcher.cube_count(),
launcher.cube_dim(),
TileArgLaunch::new(
src.clone().binding().into_tensor_arg(),
dtype,
1,
TileSpec::direct(&[M, N]),
),
AccumulateArgLaunch::new(
out.clone().binding().into_tensor_arg(),
1,
TileSpec::direct(&[M, N]),
),
launcher.partitioning_arg(),
dtype,
);
let got = HostData::from_tensor_handle(&client, out, HostDataType::F32);
for r in 0..rows {
for c in 0..cols {
assert_eq!(
got.get_f32(&[r, c]),
values[r * cols + c] + seed,
"at ({r}, {c}): the copy replaced the seed instead of adding to it"
);
}
}
}