use super::layout::{Major, WgmmaTileLayout};
use crate::frontend::pending::{GroupToken, PendingExpand};
use crate::{self as cubecl, prelude::*, unexpanded};
use core::marker::PhantomData;
use cubecl_ir::{
dialect::matrix::{
WgmmaDescriptorOp, WgmmaFenceOp, WgmmaFenceOperandOp, WgmmaOp, warpgroup_elems_per_unit,
},
features::WgmmaElems,
types::{MatrixShape, pending::AsyncGroup},
};
use cubecl_macros::{comptime_type, cube, intrinsic};
use pliron::builtin::attributes::TypeAttr;
use alloc::format;
pub use cubecl_ir::dialect::matrix::{WARPGROUP_M, WARPGROUP_UNITS};
const ACCUMULATOR_RUN: u32 = 2;
const FRAGMENT_RUN_BYTES: usize = 4;
#[derive(CubeType, Clone, Copy)]
pub struct MatrixDescriptor<E: Scalar> {
#[allow(unused)]
descriptor: u64,
#[allow(unused)]
#[cube(comptime)]
layout: WgmmaTileLayout,
#[cube(comptime)]
_elem: PhantomData<E>,
}
#[cube]
impl<E: Scalar> MatrixDescriptor<E> {
#[allow(unused_variables)]
pub fn new<L: CubePrimitive<Scalar = E>>(
tile: &[L],
#[comptime] layout: WgmmaTileLayout,
) -> Self {
intrinsic!(|scope| {
if let Err(err) = layout.validate(E::elem_type(scope).size()) {
scope.push_error(format!("a warpgroup MMA can't read the tile: {err}"));
}
let ptr = unsafe { *tile.__expand_as_ptr_method(scope) }.value(scope);
let op = WgmmaDescriptorOp::new(
scope.ctx_mut(),
ptr,
layout.leading_byte_offset(),
layout.stride_byte_offset(),
layout.swizzle,
);
MatrixDescriptorExpand {
descriptor: scope.register_with_result(&op).into(),
layout,
_elem: PhantomData,
}
})
}
pub fn at(&self, row: usize, k: usize) -> Self {
let elem_size = E::size();
let offset = comptime![self.layout].byte_offset(row, k, elem_size);
MatrixDescriptor::<E> {
descriptor: self.descriptor + (offset as u64 >> 4),
layout: comptime![self.layout],
_elem: PhantomData,
}
}
pub fn layout(&self) -> comptime_type!(WgmmaTileLayout) {
intrinsic!(|_| self.layout)
}
}
#[derive(CubeType, Clone)]
pub struct Accumulator<CD: Numeric> {
registers: Array<CD>,
#[allow(unused)]
#[cube(comptime)]
n: usize,
}
#[cube]
impl<CD: Numeric> Accumulator<CD> {
pub fn new(#[comptime] n: usize) -> Self {
let len = comptime![warpgroup_elems_per_unit(n)];
let mut registers = Array::new(len);
#[unroll]
for i in 0..len {
registers[i] = CD::from_int(0);
}
Accumulator::<CD> { registers, n }
}
pub fn n(&self) -> comptime_type!(usize) {
intrinsic!(|_| self.n)
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> comptime_type!(usize) {
intrinsic!(|_| warpgroup_elems_per_unit(self.n))
}
pub fn get(&self, nth: usize) -> CD {
self.registers[nth]
}
pub fn position_of_nth(&self, unit: u32, nth: u32) -> (u32, u32) {
position_of_nth(unit, nth, ACCUMULATOR_RUN)
}
pub fn start(self) -> Pending<Accumulator<CD>> {
intrinsic!(|scope| {
fence_operand(scope, &self.registers);
scope.register(&WgmmaFenceOp::new(scope.ctx_mut()));
PendingExpand::ready(scope, AsyncGroup::Warpgroup, self)
})
}
}
impl<CD: Numeric> Clone for AccumulatorExpand<CD> {
fn clone(&self) -> Self {
Self {
registers: self.registers,
n: self.n,
}
}
}
impl<CD: Numeric> PendingValue for Accumulator<CD> {
fn wait_for(_pending: Pending<Self>) -> Self {
unexpanded!()
}
fn __expand_wait_for(scope: &Scope, pending: PendingExpand<Self>) -> AccumulatorExpand<CD> {
let token = GroupToken::commit(scope, AsyncGroup::Warpgroup);
token.__expand_wait_method(scope);
fence_operand(scope, &pending.value.registers);
pending.value
}
}
#[cube]
impl<CD: Numeric> Pending<Accumulator<CD>> {
#[allow(unused_variables)]
pub fn execute<A: Scalar, B: Scalar>(
&mut self,
a: &MatrixDescriptor<A>,
b: &MatrixDescriptor<B>,
) {
intrinsic!(|scope| {
let a_layout = a.layout;
let a = a.descriptor.read_value(scope);
issue::<A, B, CD>(scope, &self.value, a, Some(a_layout), b)
})
}
#[allow(unused_variables)]
pub fn execute_registers<A: Scalar, B: Scalar>(
&mut self,
a: &Fragment<A>,
b: &MatrixDescriptor<B>,
) {
intrinsic!(|scope| {
fence_operand(scope, &a.registers);
scope.register(&WgmmaFenceOp::new(scope.ctx_mut()));
let a = a.registers.read_value(scope);
issue::<A, B, CD>(scope, &self.value, a, None, b)
})
}
pub fn reset(&mut self) {
intrinsic!(|scope| {
let token = GroupToken::commit(scope, AsyncGroup::Warpgroup);
token.__expand_wait_method(scope);
fence_operand(scope, &self.value.registers);
let len = warpgroup_elems_per_unit(self.value.n);
zero_registers::expand::<CD>(scope, &mut self.value.registers, len);
fence_operand(scope, &self.value.registers);
scope.register(&WgmmaFenceOp::new(scope.ctx_mut()));
})
}
pub fn commit(&mut self) -> Pending<()> {
intrinsic!(|scope| PendingExpand::commit(scope, AsyncGroup::Warpgroup, ()))
}
}
#[cube]
fn zero_registers<CD: Numeric>(registers: &mut Array<CD>, #[comptime] len: usize) {
#[unroll]
for i in 0..len {
registers[i] = CD::from_int(0);
}
}
fn fence_operand<E: CubePrimitive>(scope: &Scope, registers: &NativeExpand<Array<E>>) {
let registers = registers.__extract_list(scope);
scope.register(&WgmmaFenceOperandOp::new(scope.ctx_mut(), registers))
}
fn issue<A: Scalar, B: Scalar, CD: Numeric>(
scope: &Scope,
acc: &AccumulatorExpand<CD>,
a: cubecl_ir::pliron::value::Value,
a_layout: Option<WgmmaTileLayout>,
b: &MatrixDescriptorExpand<B>,
) {
let elems = WgmmaElems {
a: A::elem_type(scope),
b: B::elem_type(scope),
cd: CD::elem_type(scope),
};
let shape = MatrixShape {
m: WARPGROUP_M,
n: acc.n,
k: WgmmaTileLayout::k_step(elems.a.size()),
};
check_supported(scope, elems, shape);
if let Some(a_layout) = a_layout {
check_covers(scope, "A", a_layout, shape.m, shape.k);
}
check_covers(scope, "B", b.layout, shape.n, shape.k);
let a_major = a_layout.map_or(Major::K, |layout| layout.major);
let b_major = b.layout.major;
let b = b.descriptor.read_value(scope);
let registers = acc.registers.__extract_list(scope);
let a_ty = TypeAttr::new(A::__expand_as_type(scope));
let b_ty = TypeAttr::new(B::__expand_as_type(scope));
scope.register(&WgmmaOp::new(
scope.ctx_mut(),
a,
b,
registers,
a_ty,
b_ty,
shape,
a_major,
b_major,
));
}
fn check_covers(scope: &Scope, operand: &str, layout: WgmmaTileLayout, rows: usize, k: usize) {
if layout.rows < rows || layout.k < k {
scope.push_error(format!(
"a warpgroup MMA reads {rows} rows and {k} of K of {operand}, but its tile is {} x {}",
layout.rows, layout.k
));
}
}
fn check_supported(scope: &Scope, elems: WgmmaElems, shape: MatrixShape) {
let Some(props) = scope.state().device_properties.clone() else {
return;
};
let wgmma = &props.features.matmul.wgmma;
if !wgmma.iter().any(|config| config.matches(elems, shape)) {
let WgmmaElems { a, b, cd } = elems;
let MatrixShape { m, n, k } = shape;
scope.push_error(format!(
"the device doesn't support a {m}x{n}x{k} warpgroup MMA of {a:?} x {b:?} into \
{cd:?}; supported configurations: {wgmma:?}"
));
}
}
#[derive(CubeType)]
pub struct Fragment<A: Scalar> {
registers: Array<A>,
}
#[cube]
impl<A: Scalar> Fragment<A> {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
intrinsic!(|scope| {
let len = fragment_elems(A::elem_type(scope).size());
FragmentExpand {
registers: Array::__expand_new(scope, len),
}
})
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> comptime_type!(usize) {
intrinsic!(|scope| fragment_elems(A::elem_type(scope).size()))
}
pub fn get(&self, nth: usize) -> A {
self.registers[nth]
}
pub fn set(&mut self, nth: usize, value: A) {
self.registers[nth] = value;
}
pub fn position_of_nth(&self, unit: u32, nth: u32) -> (u32, u32) {
intrinsic!(|scope| {
let run = (FRAGMENT_RUN_BYTES / A::elem_type(scope).size()) as u32;
position_of_nth::expand(scope, unit, nth, run)
})
}
}
fn fragment_elems(elem_size: usize) -> usize {
warpgroup_elems_per_unit(WgmmaTileLayout::k_step(elem_size))
}
#[cube]
fn position_of_nth(unit: u32, nth: u32, #[comptime] run: u32) -> (u32, u32) {
let plane = unit / 32;
let lane = unit % 32;
let chunk = nth / run;
let row = plane * 16 + lane / 4 + (chunk % 2) * 8;
let col = (chunk / 2) * (run * 4) + (lane % 4) * run + nth % run;
(row, col)
}