Skip to main content

cubecl_cpp/hip/mma/
manual.rs

1use cubecl_core::{
2    self as cubecl,
3    cmma::{MatrixIdent, MatrixType},
4    ir::{
5        ElemType, FloatKind,
6        dialect::matrix::{ColIndexOp, MmaManualOp, RowIndexOp},
7        features::MmaConfig,
8        interfaces::TypedExt,
9        prelude::*,
10    },
11    prelude::*,
12};
13use itertools::Itertools;
14use pliron::{context::Context, r#type::TypedHandle, value::Value};
15
16use crate::{
17    hip::{
18        arch::{AMDArchitecture, AmdWmma},
19        hip_op,
20        mma::{WmmaExecute, amd_wmma, compile_fragment_intrinsic},
21    },
22    shared::{
23        CppValue, SupportedMmaCombinations, SupportedScaledMmaCombinations, lowering::LowerOp,
24        ty::TypeExtCPP,
25    },
26    target::Hip,
27};
28
29#[op_interface_impl]
30impl LowerOp<Hip> for RowIndexOp {
31    fn lower(&self, scope: &Scope) -> Vec<Value> {
32        let matrix = self.matrix_ty(scope.ctx()).deref(scope.ctx());
33        let (ident, m, k) = (matrix.ident, matrix.shape.m as u32, matrix.shape.k as u32);
34        let rdna4 = amd_wmma(scope.ctx()) == AmdWmma::Rdna4;
35        let lane_id = self.lane_id(scope.ctx());
36        let i = self.i(scope.ctx());
37        vec![row_index::expand(scope, lane_id.into(), i.into(), ident, m, k, rdna4).value(scope)]
38    }
39}
40
41#[op_interface_impl]
42impl LowerOp<Hip> for ColIndexOp {
43    fn lower(&self, scope: &Scope) -> Vec<Value> {
44        let matrix = self.matrix_ty(scope.ctx()).deref(scope.ctx());
45        let (ident, k) = (matrix.ident, matrix.shape.k as u32);
46        let rdna4 = amd_wmma(scope.ctx()) == AmdWmma::Rdna4;
47        let lane_id = self.lane_id(scope.ctx());
48        let i = self.i(scope.ctx());
49        vec![col_index::expand(scope, lane_id.into(), i.into(), ident, k, rdna4).value(scope)]
50    }
51}
52
53/// RDNA4 splits a dimension between lanes 0-15 and 16-31, giving each lane a contiguous half of
54/// it. Every RDNA4 fragment is built this way: A and B split `k`, the accumulator splits `m`.
55#[cube]
56fn split_half(lane_id: u32, i: u32, #[comptime] dim: u32) -> u32 {
57    (lane_id / 16) * comptime![dim / 2] + i
58}
59
60#[cube]
61fn row_index(
62    lane_id: u32,
63    i: u32,
64    #[comptime] ident: MatrixIdent,
65    #[comptime] m: u32,
66    #[comptime] k: u32,
67    #[comptime] rdna4: bool,
68) -> u32 {
69    match ident {
70        MatrixIdent::A => lane_id % 16,
71        MatrixIdent::B => {
72            if comptime![rdna4] {
73                split_half(lane_id, i, k)
74            } else {
75                // RDNA3 hands every lane the whole `k` range, duplicated across the lane halves.
76                i
77            }
78        }
79        MatrixIdent::Accumulator => {
80            if comptime![rdna4] {
81                split_half(lane_id, i, m)
82            } else {
83                // 2 * i, offset by 1 if lane_id >= 16
84                i * 2 + (lane_id / 16)
85            }
86        }
87    }
88}
89
90#[cube]
91fn col_index(
92    lane_id: u32,
93    i: u32,
94    #[comptime] ident: MatrixIdent,
95    #[comptime] k: u32,
96    #[comptime] rdna4: bool,
97) -> u32 {
98    match ident {
99        MatrixIdent::A => {
100            if comptime![rdna4] {
101                split_half(lane_id, i, k)
102            } else {
103                i
104            }
105        }
106        MatrixIdent::B => lane_id % 16,
107        MatrixIdent::Accumulator => lane_id % 16,
108    }
109}
110
111hip_op!(MmaManualOp, compile_manual_mma);
112
113pub(super) fn compile_manual_mma(op: &MmaManualOp, ctx: &Context) -> String {
114    let frag_a = op.registers_a(ctx);
115    let frag_b = op.registers_b(ctx);
116    let frag_c = op.registers_c(ctx);
117    let frag_d = op.registers_d(ctx);
118    let shape = op.shape(ctx).0;
119
120    // `registers_a/b/c` are array values, only `registers_d` is a pointer (see `MmaManualOp`).
121    let elem_a = frag_a.scalar_ty(ctx);
122    let elem_c = frag_c.scalar_ty(ctx);
123    let elem_d = frag_d.scalar_ty(ctx).to_cpp(ctx);
124
125    let extension = WmmaExecute::from_manual(shape, elem_a, elem_c);
126
127    let cd_elems = shape.num_elems(MatrixIdent::Accumulator) / 32;
128    let ab_elems = amd_wmma(ctx).frag_ab_elems(shape.k);
129
130    // RDNA3 spreads a 16 bit accumulator over 32 bit lanes, using only the low half of each, so
131    // its elements sit at every other index. RDNA4 packs them densely.
132    let frag_cd_step = match amd_wmma(ctx) {
133        AmdWmma::Rdna3 => 4usize.div_ceil(elem_c.size(ctx)),
134        AmdWmma::Rdna4 => 1,
135    };
136
137    // Need to reconstruct the fragments from an array of vectors to a single vector type.
138    // `float8_t {reinterpret_cast<const float*>(arr->data)[0], ...}`
139    let frag = |val: Value, len: usize| {
140        let elem = val.scalar_ty(ctx).to_cpp(ctx);
141        let ptr = format!("reinterpret_cast<const {elem}*>({}.data)", val.name(ctx));
142        (0..len).map(|i| format!("{ptr}[{i}]")).join(", ")
143    };
144
145    let frag_a = frag(frag_a, ab_elems);
146    let frag_b = frag(frag_b, ab_elems);
147    // C matrix needs to be padded for f16, because it only uses the low bytes. The simplest way is
148    // to just replicate the same f16 in both halves of the register.
149    let frag_c = {
150        let elem = elem_c.to_cpp(ctx);
151        let frag_c = frag_c.name(ctx);
152        let ptr = format!("reinterpret_cast<const {elem}*>({frag_c}.data)");
153        (0..cd_elems)
154            .flat_map(|i| {
155                let ptr = ptr.clone();
156                (0..frag_cd_step).map(move |_| format!("{ptr}[{i}]"))
157            })
158            .join(", ")
159    };
160
161    // Should optimize out
162    let name = extension.fn_name(ctx);
163
164    let mut out = String::from("{{");
165    out.push_str(&format!(
166        "{} frag_d_tmp = {{}};",
167        compile_fragment_intrinsic(ctx, &extension.frag_d)
168    ));
169
170    out.push_str(&format!(
171        "{name}({}{{{frag_a}}}, {}{{{frag_b}}}, {}{{{frag_c}}}, frag_d_tmp);",
172        compile_fragment_intrinsic(ctx, &extension.frag_a),
173        compile_fragment_intrinsic(ctx, &extension.frag_b),
174        compile_fragment_intrinsic(ctx, &extension.frag_c)
175    ));
176
177    let frag_d_ptr = format!("reinterpret_cast<{elem_d}*>({}->data)", frag_d.name(ctx));
178
179    for i in 0..cd_elems {
180        out.push_str(&format!(
181            "{frag_d_ptr}[{i}] = frag_d_tmp[{i} * {frag_cd_step}];"
182        ));
183    }
184
185    out.push_str("}}");
186
187    out
188}
189
190pub fn supported_mma_combinations(arch: &AMDArchitecture) -> SupportedMmaCombinations {
191    // Correctness is wrong.
192    const ENABLED: bool = true;
193
194    if !ENABLED {
195        return Vec::new();
196    }
197
198    // Reference: https://gpuopen.com/learn/wmma_on_rdna3/
199    // Feel free to add more if additional intrinsics are supported for execute
200    let mut result: SupportedMmaCombinations = vec![];
201    if arch.wmma_generation().is_some() {
202        // Types fully supported.
203        let types = vec![
204            (
205                ElemType::Float(FloatKind::F16),
206                ElemType::Float(FloatKind::F32),
207            ),
208            (
209                ElemType::Float(FloatKind::BF16),
210                ElemType::Float(FloatKind::F32),
211            ),
212        ];
213        let combinations = types.into_iter().map(|(ab_elem, cd_elem)| MmaConfig {
214            a_type: ab_elem,
215            b_type: ab_elem,
216            cd_type: cd_elem,
217            m: 16,
218            n: 16,
219            k: 16,
220        });
221        result.extend(combinations);
222    }
223    result
224}
225
226pub fn supported_scaled_mma_combinations(
227    _arch: &AMDArchitecture,
228) -> SupportedScaledMmaCombinations {
229    vec![]
230}
231
232pub fn contiguous_elements_rdna3(
233    ctx: &Context,
234    ident: MatrixIdent,
235    matrix: TypedHandle<MatrixType>,
236) -> usize {
237    contiguous_elements(AmdWmma::Rdna3, ctx, ident, matrix)
238}
239
240pub fn contiguous_elements_rdna4(
241    ctx: &Context,
242    ident: MatrixIdent,
243    matrix: TypedHandle<MatrixType>,
244) -> usize {
245    contiguous_elements(AmdWmma::Rdna4, ctx, ident, matrix)
246}
247
248fn contiguous_elements(
249    wmma: AmdWmma,
250    ctx: &Context,
251    ident: MatrixIdent,
252    matrix: TypedHandle<MatrixType>,
253) -> usize {
254    let matrix = matrix.deref(ctx);
255    // Don't exceed swizzle atom and load width
256    let max_vector_size = 16 / matrix.elem_ty.size(ctx);
257    match ident {
258        // Consecutive elements of a lane's fragment are consecutive `k`, so a lane can load as
259        // many in one go as it holds.
260        MatrixIdent::A | MatrixIdent::B => wmma.frag_ab_elems(matrix.shape.k).min(max_vector_size),
261        MatrixIdent::Accumulator => 1,
262    }
263}