Skip to main content

cubecl_tile/ops/matmul/
base.rs

1//! `c.mm(a, b)` and `c.mma(a, b)` at a final tile, each descending to its leaf through
2//! [`Descent::contract`].
3
4use cubecl::cmma::{MatrixIdent, MatrixLayout};
5use cubecl::prelude::*;
6
7use crate::ops::matmul::leaf::{FragmentRead, window_layouts};
8use crate::*;
9
10#[cube]
11impl Tile {
12    /// `c = a · b`, under the semiring the accumulator was opened with, or a memory window stated
13    /// [`accumulating`](Tile::accumulating). A memory window starts the sum fresh rather than
14    /// reading the cell.
15    pub fn mm(&mut self, lhs: &Tile, rhs: &Tile) {
16        let in_memory = self.is_memory();
17        if comptime!(in_memory) {
18            // A clone holds the same window: reading it reads `self`.
19            let acc = self.clone();
20            let contraction = match &acc.kind {
21                TileKind::Memory(g) => g.stated_contraction(),
22                TileKind::PlaneTile(_)
23                | TileKind::PlanePartition(_)
24                | TileKind::TmaGmem(_)
25                | TileKind::Procedural(_)
26                | TileKind::Lines(_) => {
27                    panic!("Tile::mm: only a memory window states its contraction")
28                }
29            };
30            let semiring = contraction.semiring;
31            let init_from = self.request_init_from(comptime!(InitFrom::Identity));
32            let define!(Served) = self.element;
33            match comptime!(init_from) {
34                InitFrom::Identity => {}
35                InitFrom::Cell => self.init_identity::<Served>(comptime!(semiring.add())),
36            }
37            self.mma(lhs, rhs);
38            self.request_init_from(comptime!(InitFrom::Cell));
39        } else {
40            self.reset();
41            self.mma(lhs, rhs);
42        }
43    }
44
45    /// `operand` dequantized at `element` for this accumulator to contract: the plane-level
46    /// dequantize, called where the kernel wants it instead of left to the leaf, which loads it as
47    /// the multiply would. A cmma grid loads it into its fragments through a landing; one cmma
48    /// fragment into the fragment its leaf builds; a walk of steps into the landing the steps
49    /// read, where the tensor cores read through one. A plain operand is returned as it is: it
50    /// has nothing to decode. So is any operand of a manual-mma sum, whose contraction decodes
51    /// it as it loads it, at its other factor's element ([`Tile::mma`]): loaded here, ahead of
52    /// that factor, its fragments could not hold `k` in the order the other factor's do.
53    pub fn dequantized(&self, operand: &Tile, #[comptime] element: ElemType) -> Tile {
54        let (quantized, packing) = (operand.quantized(), operand.packing());
55        let decodes = comptime!(quantized || packing != Packing::Plain);
56        let mma = self.contracts_through_mma();
57        if comptime!(!decodes || mma) {
58            operand.clone()
59        } else {
60            let out = comptime!(self.place.space.clone());
61            let own = comptime!(operand.place.space.clone());
62            let define!(Target) = element;
63            match &self.kind {
64                TileKind::PlanePartition(partition) => {
65                    let grid = partition.grid();
66                    match comptime!(Descent::of(grid, &self.place, &own, &own)) {
67                        Descent::Here => {
68                            operand.fragment_for::<Target>(&partition.at(0usize, 0usize), out)
69                        }
70                        Descent::Cells(instruction) => PlanePartition::operand::<Target>(
71                            operand,
72                            self,
73                            instruction,
74                            DeepLoad::Steps,
75                        ),
76                        Descent::Steps(instruction) => match comptime!(instruction) {
77                            Instruction::Cmma => {
78                                let side = comptime!(Side::of(&own, &out));
79                                operand.landed::<Target>(side, out)
80                            }
81                            Instruction::Mma { .. }
82                            | Instruction::Registers { .. }
83                            | Instruction::Wgmma => panic!(
84                                "Tile::dequantized: a grid that steps its contraction through \
85                                 manual-mma fragments holds each region's operand only while it \
86                                 contracts; walk the regions and dequantize each one into the \
87                                 accumulator's region"
88                            ),
89                        },
90                    }
91                }
92                TileKind::PlaneTile(fragment) => operand.fragment_for::<Target>(fragment, out),
93                TileKind::Memory(_)
94                | TileKind::TmaGmem(_)
95                | TileKind::Procedural(_)
96                | TileKind::Lines(_) => {
97                    panic!(
98                        "Tile::dequantized: an operand is dequantized for the fragments it meets"
99                    )
100                }
101            }
102        }
103    }
104
105    /// This operand as the fragment the one-fragment `acc`'s leaf loads it into, at `T`.
106    fn fragment_for<T: Numeric>(&self, acc: &PlaneTile, #[comptime] out: Space) -> Tile {
107        let own = comptime!(self.place.space.clone());
108        let side = comptime!(Side::of(&own, &out));
109        let ident = comptime!(match side {
110            Side::Lhs => MatrixIdent::A,
111            Side::Rhs => MatrixIdent::B,
112        });
113        let fragment = match acc {
114            PlaneTile::Cmma(_) => {
115                let read = comptime!(FragmentRead::new(side, &own, &out));
116                let fragment = self.cmma_fragment::<T>(ident, comptime!(read.clone()));
117                PlaneTile::new_Cmma(CmmaData {
118                    fragment: unsafe { fragment.downcast_unchecked::<Held>() },
119                    ident,
120                    layout: comptime!(read.layout),
121                    shape: comptime!((read.m, read.n)),
122                    scratch: ComptimeOption::new_None(),
123                })
124            }
125            PlaneTile::Mma(_) => panic!(
126                "Tile::dequantized: a manual-mma sum decodes its factor as its contraction loads it"
127            ),
128            PlaneTile::Registers(_) | PlaneTile::Wgmma(_) => panic!(
129                "Tile::dequantized: a register block and a warpgroup MMA read their operands where \
130                 they lie"
131            ),
132        };
133        Tile::new::<T>(
134            TileKind::new_PlaneTile(fragment),
135            comptime!(self.place.clone()),
136        )
137    }
138
139    /// `c += a · b`, added to what the accumulator or memory window holds. A packed factor is
140    /// decoded into `f32`, unless the kernel [`dequantized`](Tile::dequantized) it at another
141    /// element first, a manual-mma instruction decodes it at its other factor's element, or the
142    /// instruction reads its codes as stored.
143    pub fn mma(&mut self, lhs: &Tile, rhs: &Tile) {
144        let stored = self.reads_codes_as_stored();
145        let mma = self.contracts_through_mma();
146        let (lhs_read, rhs_read) = (lhs.read_element(), rhs.read_element());
147        let (lhs_packing, rhs_packing) = (lhs.packing(), rhs.packing());
148        // A manual-mma instruction multiplies two factors of one element, so a packed factor
149        // against a plain one decodes at that one's element.
150        let (lhs_read, rhs_read) = comptime!(match (
151            mma,
152            lhs_packing == Packing::Plain,
153            rhs_packing == Packing::Plain
154        ) {
155            (true, false, true) => (rhs_read, rhs_read),
156            (true, true, false) => (lhs_read, lhs_read),
157            _ => (lhs_read, rhs_read),
158        });
159        let define!(Served) = self.element;
160        let define!(Lhs) = comptime!(if stored { lhs.element } else { lhs_read });
161        let define!(Rhs) = comptime!(if stored { rhs.element } else { rhs_read });
162        Descent::contract::<Served, Lhs, Rhs>(self, lhs, rhs)
163    }
164
165    /// Whether this accumulator's fragments contract through the device's manual MMA.
166    fn contracts_through_mma(&self) -> comptime_type!(bool) {
167        match &self.kind {
168            TileKind::PlaneTile(tile) => tile.contracts_through_mma(),
169            TileKind::PlanePartition(partition) => {
170                partition.at(0usize, 0usize).contracts_through_mma()
171            }
172            TileKind::Memory(_)
173            | TileKind::TmaGmem(_)
174            | TileKind::Procedural(_)
175            | TileKind::Lines(_) => comptime!(false),
176        }
177    }
178
179    /// Whether this accumulator's fragments read their factors' packed codes as stored; a memory
180    /// window decodes them.
181    fn reads_codes_as_stored(&self) -> comptime_type!(bool) {
182        match &self.kind {
183            TileKind::PlaneTile(tile) => tile.reads_codes_as_stored(),
184            TileKind::PlanePartition(partition) => {
185                partition.at(0usize, 0usize).reads_codes_as_stored()
186            }
187            TileKind::Memory(_)
188            | TileKind::TmaGmem(_)
189            | TileKind::Procedural(_)
190            | TileKind::Lines(_) => comptime!(false),
191        }
192    }
193
194    /// This plane-resident accumulator back at its semiring's identity, as it was opened.
195    pub fn reset(&mut self) {
196        // A clone holds the same fragments: reading it reads `self`.
197        let acc = self.clone();
198        let semiring = match &acc.kind {
199            TileKind::PlaneTile(t) => t.semiring(),
200            TileKind::PlanePartition(p) => p.at(0usize, 0usize).semiring(),
201            TileKind::Memory(_)
202            | TileKind::TmaGmem(_)
203            | TileKind::Procedural(_)
204            | TileKind::Lines(_) => {
205                panic!(
206                    "Tile::reset: only a plane-resident accumulator has an identity to return to"
207                )
208            }
209        };
210        let define!(Served) = self.element;
211        self.init_identity::<Served>(comptime!(semiring.add()));
212    }
213}
214
215#[cube]
216impl Tile {
217    /// Commit the warpgroup MMAs this accumulator issued since its last commit, and return their
218    /// completion: once it resolves, they are done reading what they read, the stages a walk
219    /// holds for them among it. Only a warpgroup accumulator contracts asynchronously.
220    pub fn commit(&self) -> Pending<()> {
221        let define!(Served) = self.element;
222        match &self.kind {
223            TileKind::PlanePartition(p) => p.fragment().commit::<Served>(),
224            TileKind::PlaneTile(t) => t.commit::<Served>(),
225            TileKind::Memory(_)
226            | TileKind::TmaGmem(_)
227            | TileKind::Procedural(_)
228            | TileKind::Lines(_) => panic!(
229                "Tile::commit: only a plane group's warpgroup accumulator contracts asynchronously"
230            ),
231        }
232    }
233}
234
235#[cube]
236impl Tile {
237    /// This memory window as a contraction's output: each unit's share tiled into `block`, summed
238    /// under `semiring`. A plane-resident accumulator states both when it is opened.
239    pub fn accumulating(
240        &self,
241        #[comptime] block: RegisterBlock,
242        #[comptime] semiring: Semiring,
243    ) -> Tile {
244        match &self.kind {
245            TileKind::Memory(g) => {
246                let mut g = g.clone();
247                g.set_contraction(comptime!(Contraction { block, semiring }));
248                Tile {
249                    kind: TileKind::new_Memory(g),
250                    factor: self.factor.clone(),
251                    place: comptime!(self.place.clone()),
252                    element: comptime!(self.element),
253                }
254            }
255            TileKind::PlaneTile(_)
256            | TileKind::PlanePartition(_)
257            | TileKind::TmaGmem(_)
258            | TileKind::Procedural(_)
259            | TileKind::Lines(_) => panic!(
260                "Tile::accumulating: a memory window states how it is contracted into; a \
261                 plane-resident accumulator states it when opened (Tile::accumulator)"
262            ),
263        }
264    }
265}
266
267#[cube]
268impl PlaneTile {
269    /// Contract this plane tile, each factor times whatever scales it carries.
270    pub(crate) fn mma<E: Numeric, EL: Numeric, ER: Numeric>(
271        &mut self,
272        lhs: &Tile,
273        rhs: &Tile,
274        #[comptime] out: Space,
275    ) {
276        match self {
277            PlaneTile::Cmma(d) => {
278                let (lhs_window, rhs_window) =
279                    comptime!(window_layouts(&lhs.place.space, &rhs.place.space));
280                let lhs_along_m = read_col_major(lhs, lhs_window);
281                let rhs_along_k = read_col_major(rhs, rhs_window);
282                strided_2d(lhs, rhs, comptime!(out.clone()), lhs_along_m, rhs_along_k);
283                d.mma::<E, EL, ER>(lhs, rhs, out)
284            }
285            PlaneTile::Mma(d) => {
286                // The manual-mma instruction reads registers, where a scaled factor cannot land,
287                // unless the device's block-scaled instruction takes the scales beside the values,
288                // or a fragment deeper than its instruction decodes them into its registers as it
289                // loads, a step at a time out of shared memory
290                // ([`steps`](crate::tile::kind::plane::mma::steps)) or in lane runs out of global
291                // memory ([`lane_runs`](crate::tile::kind::plane::mma::lane_runs)), each of which
292                // says why where it cannot.
293                let (m, n, k) = comptime!((d.m, d.n, d.k));
294                let block_scaled = comptime!(d.operands == MmaOperands::BlockScaled);
295                // An operand already in fragments was held to the instruction as it loaded
296                // (`PlanePartition::operand`), and a fragment no longer states its storage.
297                let (lhs_in_memory, rhs_in_memory) = (lhs.is_memory(), rhs.is_memory());
298                if comptime!(block_scaled && lhs_in_memory) {
299                    assert_block_scaled::<EL>(lhs, m, n, k);
300                }
301                if comptime!(block_scaled && rhs_in_memory) {
302                    assert_block_scaled::<ER>(rhs, m, n, k);
303                }
304                let steps = lane_run_steps::<EL>(m, n, k);
305                if comptime!(!block_scaled && steps == 1) {
306                    lhs.refuse_factor("PlaneTile::Mma");
307                    rhs.refuse_factor("PlaneTile::Mma");
308                }
309                flattened_k(lhs, rhs, out);
310                d.mma::<E, EL, ER>(lhs, rhs)
311            }
312            PlaneTile::Wgmma(d) => {
313                lhs.refuse_factor("PlaneTile::Wgmma");
314                rhs.refuse_factor("PlaneTile::Wgmma");
315                d.mma::<E, EL, ER>(lhs, rhs, out)
316            }
317            PlaneTile::Registers(d) => match &lhs.kind {
318                TileKind::PlaneTile(block) => match block {
319                    PlaneTile::Registers(block) => d.mma_block::<E, EL, ER>(block, rhs),
320                    PlaneTile::Cmma(_) | PlaneTile::Mma(_) | PlaneTile::Wgmma(_) => panic!(
321                        "mma: a register block contracts a register block it holds, or memory"
322                    ),
323                },
324                TileKind::PlanePartition(block) => match block.fragment() {
325                    PlaneTile::Registers(block) => d.mma_block::<E, EL, ER>(&block, rhs),
326                    PlaneTile::Cmma(_) | PlaneTile::Mma(_) | PlaneTile::Wgmma(_) => panic!(
327                        "mma: a register block contracts a register block it holds, or memory"
328                    ),
329                },
330                TileKind::Memory(_)
331                | TileKind::TmaGmem(_)
332                | TileKind::Procedural(_)
333                | TileKind::Lines(_) => {
334                    let rhs_along_k = comptime!(
335                        lhs.place
336                            .space
337                            .contains(rhs.place.space.axis_at(rhs.place.space.rank() - 1))
338                    );
339                    // A register block reads its lhs along `k`.
340                    strided_2d(lhs, rhs, comptime!(out.clone()), false, rhs_along_k);
341                    d.mma::<E, EL, ER>(lhs, rhs, out)
342                }
343            },
344        }
345    }
346}
347
348/// Asserts that operands are not gathered and read as one matrix each.
349#[cube]
350fn strided_2d(
351    lhs: &Tile,
352    rhs: &Tile,
353    #[comptime] out: Space,
354    #[comptime] lhs_along_m: bool,
355    #[comptime] rhs_along_k: bool,
356) {
357    let lhs_gathered = lhs.gathered();
358    let rhs_gathered = rhs.gathered();
359    let flat = comptime!({
360        let kc = Space::merge(&[&lhs.place.space, &rhs.place.space]).contracted_extent(&out);
361        let axes = MatrixAxes::accumulator(&out, &lhs.place.space);
362        let cols = axes.cols(&out);
363        let rhs_matrix = if rhs_along_k {
364            MatrixAxes::new(&rhs.place.space, cols, kc)
365        } else {
366            MatrixAxes::new(&rhs.place.space, kc, cols)
367        };
368        let rows = axes.rows(&out);
369        let lhs_matrix = if lhs_along_m {
370            MatrixAxes::new(&lhs.place.space, kc, rows)
371        } else {
372            MatrixAxes::new(&lhs.place.space, rows, kc)
373        };
374        lhs_matrix.is_ok() && rhs_matrix.is_ok()
375    });
376    comptime!(assert!(
377        !lhs_gathered && !rhs_gathered && flat,
378        "mma: a cmma or plane-register fragment reads one `k` edge off a directly addressed \
379         operand; a gather, or a contraction these axes give no edge for, needs the manual-mma \
380         leaf, or an unpromoted Gmem/Smem accumulator, whose software instruction is the \
381         `memory` arm of `Descent::contract`: {:?} · {:?} into {out:?}",
382        lhs.place.space,
383        rhs.place.space
384    ));
385}
386
387/// Whether `factor` is read col-major: a col-major cmma fragment, or a staged window laid out as
388/// `window` says (the lhs along its rows, the rhs along `k`). Only a shared stage can answer
389/// column-major; a gmem factor never does.
390#[cube]
391fn read_col_major(factor: &Tile, #[comptime] window: MatrixLayout) -> comptime_type!(bool) {
392    match &factor.kind {
393        TileKind::PlaneTile(t) => match t {
394            PlaneTile::Cmma(d) => comptime!(d.layout == MatrixLayout::ColMajor),
395            PlaneTile::Mma(_) | PlaneTile::Registers(_) | PlaneTile::Wgmma(_) => comptime!(false),
396        },
397        TileKind::Memory(m) => {
398            comptime!(m.address == AddressSpace::Shared && window == MatrixLayout::ColMajor)
399        }
400        TileKind::PlanePartition(_)
401        | TileKind::TmaGmem(_)
402        | TileKind::Procedural(_)
403        | TileKind::Lines(_) => comptime!(false),
404    }
405}
406
407/// Asserts that operands contract their shared axes in the same order.
408#[cube]
409fn flattened_k(lhs: &Tile, rhs: &Tile, #[comptime] out: Space) {
410    comptime!(assert!(
411        Space::contraction_agrees(&lhs.place.space, &rhs.place.space, &out),
412        "mma: the operands list their contracted axes in different orders ({:?} against {:?}), \
413         so their `k` edges do not line up",
414        lhs.place.space.difference(&out),
415        rhs.place.space.difference(&out)
416    ));
417}