Skip to main content

cubecl_tile/tile/accumulator/
base.rs

1//! The plane-resident accumulator an output opens ([`Tile::accumulator`]).
2
3use cubecl::prelude::*;
4
5use crate::ops::matmul::leaf::memory;
6use crate::*;
7
8/// The shape a plane-resident accumulator is opened at: `tiles` per plane, each `m × n`.
9#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
10pub(crate) struct GridShape {
11    pub(crate) tiles: (usize, usize),
12    pub(crate) m: usize,
13    pub(crate) n: usize,
14    pub(crate) k: usize,
15}
16
17impl GridShape {
18    /// The grid an accumulator at `place` contracting `lhs` holds.
19    pub(crate) fn new(place: &Placement, lhs: &Space) -> Self {
20        let levels = place.below();
21        let tiles = partition_shape(&place.space, levels);
22        let leaf = place.space.leaf(levels);
23        let lhs_leaf = lhs.leaf(levels);
24        let axes = MatrixAxes::accumulator(&leaf, &lhs_leaf);
25        // Use the accumulator's own edges: a split column group is one edge.
26        GridShape {
27            tiles,
28            m: axes.rows(&leaf),
29            n: axes.cols(&leaf),
30            k: lhs_leaf.contracted_extent(&place.space),
31        }
32    }
33}
34
35/// How much of a plane's accumulator its scratch holds at once. Serialized names are persisted in
36/// autotune keys and must not change.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
38pub enum Scratch {
39    /// No window: fragments store through their intrinsic, which cannot add; a drain that needs
40    /// one opens [`OneTile`](Scratch::OneTile).
41    None,
42    /// One tile, bounced through shared memory with three cube-wide barriers each.
43    OneTile,
44    /// The plane's whole grid, two barriers for the whole drain.
45    #[serde(rename = "WholePartition")]
46    WholeGrid,
47}
48
49/// The persisted setting's spelling.
50impl core::fmt::Display for Scratch {
51    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52        match self {
53            Scratch::None => write!(f, "none"),
54            Scratch::OneTile => write!(f, "one_tile"),
55            Scratch::WholeGrid => write!(f, "whole_partition"),
56        }
57    }
58}
59
60impl Scratch {
61    /// Slots the window holds for a grid of `tiles`.
62    pub fn slots(self, tiles: usize) -> usize {
63        match self {
64            Scratch::None => 0,
65            Scratch::OneTile => 1,
66            Scratch::WholeGrid => tiles,
67        }
68    }
69
70    /// The slot tile `i` of a grid of `tiles` spills into.
71    pub(crate) fn slot_of(self, i: usize, tiles: usize) -> usize {
72        match self {
73            Scratch::WholeGrid => i,
74            Scratch::None | Scratch::OneTile => {
75                let _ = (i, tiles);
76                0
77            }
78        }
79    }
80
81    /// Whether a fragment goes through shared memory on its way out; forced when partials are
82    /// summed.
83    pub fn bounces(self) -> bool {
84        !matches!(self, Scratch::None)
85    }
86
87    /// This setting raised to bouncing where `bouncing` demands it.
88    pub fn at_least_bouncing(self, bouncing: bool) -> Scratch {
89        match (bouncing, self) {
90            (true, Scratch::None) => Scratch::OneTile,
91            (_, scratch) => scratch,
92        }
93    }
94}
95
96/// The planes `levels` distribute `space` across, a plane group's every plane among them: one
97/// window each, as `PLANE_POS` indexes them. Panics on a runtime-only count.
98///
99/// A plane level spanning the launch deals its tiles to the launch's planes, not to the cube's,
100/// so the cube holds the planes its order groups in one, whatever the tiles.
101pub(crate) fn plane_windows(space: &Space, levels: &[Level]) -> usize {
102    let mut handed = space.clone();
103    let mut planes = 1;
104    for level in levels {
105        if let Some(order) = level.plane_order() {
106            planes *= order.planes_per_cube();
107        } else if let Some(cube) = level.unit_cube() {
108            planes *= cube.planes;
109        } else if let Some(per_instance) = level.coverage().scope().and_then(ComputeScope::planes) {
110            let mut instances = 1;
111            for axis in level.axes() {
112                if level.distributes(axis) {
113                    instances *= level.instances_along(&handed, axis).unwrap_or_else(|| {
114                        panic!(
115                            "Tile::with_scratch: {axis:?} is distributed across the cube's planes \
116                             at a count only the launch knows, so the plane's windows cannot be \
117                             sized"
118                        )
119                    });
120                }
121            }
122            planes *= instances * per_instance;
123        }
124        handed = level.child(&handed);
125    }
126    planes
127}
128
129/// What an output does with the sum contracted into it: open, scratch and drain an accumulator.
130#[cube]
131impl Tile {
132    /// What one plane sums `lhs · rhs` into under `semiring`, in the form `instruction` names,
133    /// at the element `EA`, opened at the semiring's identity.
134    pub fn accumulator<EA: Numeric>(
135        &self,
136        lhs: &Tile,
137        rhs: &Tile,
138        #[comptime] instruction: Instruction,
139        #[comptime] semiring: Semiring,
140    ) -> Tile {
141        match comptime!(instruction) {
142            Instruction::Registers { config } => {
143                register_accumulator::<EA>(self, lhs, rhs, config, semiring)
144            }
145            Instruction::Cmma | Instruction::Mma { .. } | Instruction::Wgmma => {
146                if comptime!(instruction == Instruction::Wgmma) {
147                    comptime!(assert_warpgroups_hold(&self.place));
148                    comptime!(assert_groups_own_their_box(&self.place));
149                }
150                let vector_size = self.vector_size();
151                accumulator_in::<EA>(
152                    self,
153                    lhs,
154                    instruction,
155                    vector_size,
156                    1usize,
157                    comptime!(Accumulation::Contraction(semiring)),
158                )
159            }
160        }
161    }
162
163    /// A register block over this tile's grid for a reduction over `input` under `monoid`, at
164    /// the element `EA`, opened at its identity.
165    pub fn block_reducer<EA: Numeric>(
166        &self,
167        input: &Tile,
168        #[comptime] config: RegisterBlock,
169        #[comptime] monoid: Monoid,
170    ) -> Tile {
171        let vector_size = self.vector_size();
172        accumulator_in::<EA>(
173            self,
174            input,
175            comptime!(Instruction::Registers { config }),
176            vector_size,
177            1usize,
178            comptime!(Accumulation::Reduction(monoid)),
179        )
180    }
181
182    /// This accumulator opened with a per-plane shared-memory scratch for bouncing drains, or
183    /// as it is for [`Scratch::None`]. [`drained_into`](Tile::drained_into) opens the smallest
184    /// one itself where it needs one; stating it chooses how much of the grid it holds, or serves
185    /// a drain region by region.
186    pub fn with_scratch(self, #[comptime] scratch: Scratch) -> Tile {
187        let define!(Served) = self.element;
188        match comptime!(scratch) {
189            Scratch::None => self,
190            _ => match &self.kind {
191                TileKind::PlanePartition(p) => {
192                    let (m, n) = p.at(0usize, 0usize).shape();
193                    let cells = comptime!(m * n);
194                    let tiles = comptime!(p.m_tiles * p.n_tiles);
195                    let slots = comptime!(scratch.slots(tiles));
196                    // Indexed by hardware plane position, not level position: the launch decides
197                    // the cube's shape.
198                    let planes = comptime!(plane_windows(&self.place.space, &self.place.levels));
199                    let plane = PLANE_POS.cast::<usize>() * comptime!(cells * slots);
200                    let shared = Shared::<[Served]>::new_slice(comptime!(cells * slots * planes));
201                    // Each fragment carries its own slot, so drain order doesn't matter.
202                    let mut frags = Sequence::<PlaneTile>::new();
203                    #[unroll]
204                    for i in 0..tiles {
205                        let start = plane + comptime!(cells * scratch.slot_of(i, tiles));
206                        let slot = shared.clone().map(|s| &s[start..start + cells]);
207                        frags.push(p.frags.index(i).clone().with_scratch(slot));
208                    }
209                    Tile {
210                        kind: TileKind::new_PlanePartition(PlanePartition {
211                            frags,
212                            m_tiles: comptime!(p.m_tiles),
213                            n_tiles: comptime!(p.n_tiles),
214                            rows: comptime!(p.rows),
215                            cols: comptime!(p.cols),
216                            scratch: ComptimeOption::new_Some(Raw::<SharedSlice>::of::<Served>(
217                                shared.clone().map(|s| &s[plane..plane + cells]),
218                            )),
219                            held: comptime!(scratch),
220                        }),
221                        factor: self.factor.clone(),
222                        place: comptime!(self.place.clone()),
223                        element: comptime!(self.element),
224                    }
225                }
226                TileKind::Memory(_)
227                | TileKind::PlaneTile(_)
228                | TileKind::TmaGmem(_)
229                | TileKind::Procedural(_)
230                | TileKind::Lines(_) => {
231                    panic!("Tile::with_scratch: a scratch backs a plane-resident accumulator")
232                }
233            },
234        }
235    }
236
237    /// Drain this accumulator into `dest`, cast to its element. `self` and `dest` must be
238    /// indexed by the same region. A cmma grid opened with no scratch opens one where `dest`
239    /// needs its fragments bounced.
240    pub fn drained_into(&self, dest: &Tile) {
241        let define!(Served) = self.element;
242        let define!(Out) = dest.element;
243        // A sum written out is a sum with its scales applied: multiplied by them once, as it
244        // drains. A grid of cmma fragments is scaled, and reaches a destination it cannot store
245        // to directly, through a scratch: where none was opened, the drain opens the smallest.
246        let levels = self.factor_levels();
247        let scaled = comptime!(levels > 0);
248        let unopened = self.scratch_unopened();
249        let in_place = self.casts_in_place_to::<Served, Out>();
250        let bounces = dest.fragments_bounce(in_place);
251        if comptime!(unopened && (scaled || bounces)) {
252            let opened = self.clone().with_scratch(Scratch::OneTile);
253            opened.drained_into(dest);
254        } else if comptime!(scaled) {
255            let mut sum = self.unscaled();
256            sum.scale_by::<f32>(self.factor.whole());
257            sum.drained_into(dest);
258        } else {
259            self.drain_as_opened::<Served, Out>(dest);
260        }
261    }
262}
263
264#[cube]
265impl Tile {
266    /// This plane-resident accumulator with each cell cast to `E` where its units hold it: what
267    /// an attention contracts its probabilities with the values through, the next contraction
268    /// reading the cast accumulator in its registers, without a round trip through shared memory.
269    ///
270    /// Only a manual-mma accumulator is cast in place; any other is cast as it drains.
271    pub fn cast<E: Numeric>(&self) -> Tile {
272        let define!(Served) = self.element;
273        let kind = match &self.kind {
274            TileKind::PlanePartition(partition) => {
275                TileKind::new_PlanePartition(partition.cast::<Served, E>())
276            }
277            TileKind::PlaneTile(tile) => TileKind::new_PlaneTile(tile.cast::<Served, E>()),
278            TileKind::Memory(_)
279            | TileKind::TmaGmem(_)
280            | TileKind::Procedural(_)
281            | TileKind::Lines(_) => {
282                panic!("Tile::cast: only a plane-resident accumulator is cast in its registers")
283            }
284        };
285        Tile {
286            kind,
287            factor: self.factor.clone(),
288            place: comptime!(self.place.clone()),
289            element: elem_type_of::<E>(),
290        }
291    }
292
293    /// Whether this accumulator's fragments cast to `Out` are fragments the device holds
294    /// ([`casts_in_place`]): what decides whether a cmma grid draining into an `Out` destination
295    /// stores whole or bounces.
296    fn casts_in_place_to<A: Numeric, Out: Numeric>(&self) -> comptime_type!(bool) {
297        // Only a cmma fragment has a cast the device may lack. Reading the grid of anything else
298        // would ask the extent of an axis that may be dynamic.
299        match &self.kind {
300            // The fragment's own edges: the placement's space spans every plane's grid.
301            TileKind::PlanePartition(p) => {
302                let cmma = p.is_cmma();
303                if comptime!(cmma) {
304                    casts_in_place::<A, Out>(comptime!(p.rows), comptime!(p.cols))
305                } else {
306                    comptime!(true)
307                }
308            }
309            TileKind::PlaneTile(t) => match t {
310                PlaneTile::Cmma(_) => {
311                    let (_, m, n) = self.fragment_grid();
312                    casts_in_place::<A, Out>(m, n)
313                }
314                PlaneTile::Mma(_) | PlaneTile::Registers(_) | PlaneTile::Wgmma(_) => {
315                    comptime!(true)
316                }
317            },
318            TileKind::Memory(_)
319            | TileKind::TmaGmem(_)
320            | TileKind::Procedural(_)
321            | TileKind::Lines(_) => comptime!(true),
322        }
323    }
324
325    /// Waits for the units that share the scratch this accumulator bounces through: its holder's.
326    /// A plane's windows are its own ([`plane_windows`]), so its plane alone meets them; a plane
327    /// group's fragments span its planes, so the cube does.
328    fn sync_scratch(&self) {
329        match comptime!(self.place.holder()) {
330            ComputeScope::Unit | ComputeScope::Plane => sync_plane(),
331            ComputeScope::PlaneGroup { .. } | ComputeScope::Cube => sync_cube(),
332        }
333    }
334
335    /// Whether this is a grid of cmma fragments opened with no scratch.
336    fn scratch_unopened(&self) -> comptime_type!(bool) {
337        match &self.kind {
338            TileKind::PlanePartition(p) => {
339                let cmma = p.is_cmma();
340                comptime!(cmma && p.held == Scratch::None)
341            }
342            TileKind::Memory(_)
343            | TileKind::PlaneTile(_)
344            | TileKind::TmaGmem(_)
345            | TileKind::Procedural(_)
346            | TileKind::Lines(_) => comptime!(false),
347        }
348    }
349
350    /// [`drained_into`](Tile::drained_into) for the fragments of chunk `turn` alone, of
351    /// `chunks` the fragment grid is dealt into by place: what one writer of a cyclic schedule
352    /// lands in one round, each fragment drained in exactly one of the `chunks` rounds. It reaches
353    /// every barrier a whole drain does, so writers landing different chunks meet at all of them.
354    /// A grid opened with no scratch drains through the smallest, as a whole drain does.
355    pub(crate) fn drained_chunk_into<A: Numeric, Out: Numeric>(
356        &self,
357        dest: &Tile,
358        turn: usize,
359        #[comptime] chunks: usize,
360    ) {
361        let unopened = self.scratch_unopened();
362        let in_place = self.casts_in_place_to::<A, Out>();
363        let bounces = dest.fragments_bounce(in_place);
364        if comptime!(unopened && bounces) {
365            let opened = self.clone().with_scratch(Scratch::OneTile);
366            opened.drained_chunk_into::<A, Out>(dest, turn, chunks);
367        } else {
368            self.drain_chunk_as_opened::<A, Out>(dest, turn, chunks);
369        }
370    }
371
372    /// [`drained_chunk_into`](Tile::drained_chunk_into) through the scratch this grid was opened
373    /// with, where `dest` is one its fragments must bounce to reach.
374    fn drain_chunk_as_opened<A: Numeric, Out: Numeric>(
375        &self,
376        dest: &Tile,
377        turn: usize,
378        #[comptime] chunks: usize,
379    ) {
380        let in_place = self.casts_in_place_to::<A, Out>();
381        let bounces = dest.fragments_bounce(in_place);
382        match &self.kind {
383            TileKind::PlanePartition(p) => match comptime!(DrainPlan::new(p.held, bounces)) {
384                DrainPlan::Straight => drain_chunk_below::<A, Out>(
385                    self,
386                    dest,
387                    comptime!(self.place.below().to_vec()),
388                    0usize,
389                    comptime!(DrainPass::Copy),
390                    0usize,
391                    turn,
392                    chunks,
393                ),
394                DrainPlan::BounceEach => drain_chunk_below::<A, Out>(
395                    self,
396                    dest,
397                    comptime!(self.place.below().to_vec()),
398                    0usize,
399                    comptime!(DrainPass::Bounce),
400                    0usize,
401                    turn,
402                    chunks,
403                ),
404                DrainPlan::BounceTogether => {
405                    self.sync_scratch();
406                    drain_chunk_below::<A, Out>(
407                        self,
408                        dest,
409                        comptime!(self.place.below().to_vec()),
410                        0usize,
411                        comptime!(DrainPass::Spill),
412                        0usize,
413                        turn,
414                        chunks,
415                    );
416                    self.sync_scratch();
417                    drain_chunk_below::<A, Out>(
418                        self,
419                        dest,
420                        comptime!(self.place.below().to_vec()),
421                        0usize,
422                        comptime!(DrainPass::Add),
423                        0usize,
424                        turn,
425                        chunks,
426                    );
427                    self.sync_scratch();
428                }
429            },
430            // One tile: it is the one chunk of its grid. A fragment bouncing into its destination
431            // meets its holder at its barriers whatever the turn, so it bounces through the
432            // predicated pass; anything else stores with no barrier at all.
433            TileKind::PlaneTile(t) => {
434                let cmma = match t {
435                    PlaneTile::Cmma(_) => comptime!(true),
436                    PlaneTile::Mma(_) | PlaneTile::Registers(_) | PlaneTile::Wgmma(_) => {
437                        comptime!(false)
438                    }
439                };
440                let in_place = self.casts_in_place_to::<A, Out>();
441                let bounces = dest.fragments_bounce(in_place);
442                if comptime!(cmma && bounces) {
443                    drain_chunk_leaf::<A, Out>(self, dest, comptime!(DrainPass::Bounce), turn == 0);
444                } else {
445                    drain_chunk_leaf::<A, Out>(self, dest, comptime!(DrainPass::Copy), turn == 0);
446                }
447            }
448            TileKind::Memory(_)
449            | TileKind::TmaGmem(_)
450            | TileKind::Procedural(_)
451            | TileKind::Lines(_) => {
452                panic!(
453                    "Tile::drained_chunk_into: a plane-resident accumulator drains; nothing else does"
454                )
455            }
456        }
457    }
458
459    /// [`drained_into`](Tile::drained_into) through the scratch this grid was opened with,
460    /// where `dest` is one its fragments must bounce to reach.
461    fn drain_as_opened<A: Numeric, Out: Numeric>(&self, dest: &Tile) {
462        let in_place = self.casts_in_place_to::<A, Out>();
463        let bounces = dest.fragments_bounce(in_place);
464        match &self.kind {
465            TileKind::PlanePartition(p) => match comptime!(DrainPlan::new(p.held, bounces)) {
466                DrainPlan::Straight => drain_below::<A, Out>(
467                    self,
468                    dest,
469                    comptime!(self.place.below().to_vec()),
470                    0usize,
471                    comptime!(DrainPass::Copy),
472                ),
473                // One slot shared: each spill and add pair off at its leaf.
474                DrainPlan::BounceEach => drain_below::<A, Out>(
475                    self,
476                    dest,
477                    comptime!(self.place.below().to_vec()),
478                    0usize,
479                    comptime!(DrainPass::Bounce),
480                ),
481                // Own slots: every spill precedes any add, so the descent runs twice.
482                DrainPlan::BounceTogether => {
483                    self.sync_scratch();
484                    drain_below::<A, Out>(
485                        self,
486                        dest,
487                        comptime!(self.place.below().to_vec()),
488                        0usize,
489                        comptime!(DrainPass::Spill),
490                    );
491                    self.sync_scratch();
492                    drain_below::<A, Out>(
493                        self,
494                        dest,
495                        comptime!(self.place.below().to_vec()),
496                        0usize,
497                        comptime!(DrainPass::Add),
498                    );
499                    self.sync_scratch();
500                }
501            },
502            // No grid: the accumulator is one tile and the drain is one store.
503            TileKind::PlaneTile(_) => drain_leaf::<A, Out>(self, dest, comptime!(DrainPass::Copy)),
504            TileKind::Memory(_)
505            | TileKind::TmaGmem(_)
506            | TileKind::Procedural(_)
507            | TileKind::Lines(_) => {
508                panic!("Tile::drained_into: a plane-resident accumulator drains; nothing else does")
509            }
510        }
511    }
512}
513
514/// Panics unless a warpgroup holds each of the output's tiles: the narrowest scope a level below
515/// `place` deals them to, or the one holding `place` where none does. A warpgroup MMA takes all
516/// four of its planes, so a plane, a unit or another group size would issue it short.
517fn assert_warpgroups_hold(place: &Placement) {
518    let holder = place
519        .below()
520        .iter()
521        .filter_map(|level| level.coverage().scope())
522        .min()
523        .unwrap_or_else(|| place.holder());
524    assert!(
525        holder == ComputeScope::WARPGROUP,
526        "Tile::accumulator: a warpgroup MMA's tile is held by {:?}, its four planes together, \
527         and this one's by {holder:?}; deal the tiles to `plane_groups(4, ..)`",
528        ComputeScope::WARPGROUP
529    );
530}
531
532/// Panics where a plane group level splits an axis the output at `place` lacks: each group would
533/// hold a partial of the same cells, and nothing merges plane groups' partials yet.
534fn assert_groups_own_their_box(place: &Placement) {
535    for level in &place.levels {
536        let Coverage::Distribute(ComputeScope::PlaneGroup { .. }) = level.coverage() else {
537            continue;
538        };
539        for axis in level.axes() {
540            let split = !matches!(level.count(axis), Some(Count::Stated(1)));
541            assert!(
542                !(level.distributes(axis) && split && !place.space.contains(axis)),
543                "Tile::accumulator: plane groups split {axis:?}, which the output lacks, so each \
544                 would hold a partial of the same cells, and nothing merges plane groups' \
545                 partials yet; deal each group a box of its own"
546            );
547        }
548    }
549}
550
551/// A register block `out`'s plane sums `lhs · rhs` into, run under `config`.
552#[cube]
553fn register_accumulator<EA: Numeric>(
554    out: &Tile,
555    lhs: &Tile,
556    rhs: &Tile,
557    #[comptime] config: RegisterBlock,
558    #[comptime] semiring: Semiring,
559) -> Tile {
560    let lw = lhs.vector_size();
561    // The block's lines are the rhs's loads, or, where the rhs runs along the contraction, one
562    // column's run of a load summed into a cell.
563    let rhs_load = rhs.vector_tile();
564    let rw = comptime!(rhs_load.run_length());
565    let aw = out.vector_size();
566    let contracted_per_step = comptime!(memory::contracted_per_step(
567        &lhs.place.space,
568        &rhs.place.space,
569        &out.place.space,
570        lw,
571        rw,
572        aw
573    ));
574    // A block outliving the leaf cannot spread lines, so its lines and the sink's agree or its
575    // partials sum.
576    comptime!(assert!(
577        contracted_per_step > 1 || rw == aw,
578        "Tile::accumulator: the block's lines are the rhs's ({rw} wide) and drain into \
579         {aw}-wide cells; a stage served wider than its sink is the memory-backed leaf's \
580         (Tile::accumulating)"
581    ));
582    // A block contracting runs along the contraction sums each into its cell, one value wide, or,
583    // asked for partials, keeps a run's products a lane each in a cell as wide as the run.
584    let width = comptime!(match (contracted_per_step > 1, config.partials) {
585        (true, true) => contracted_per_step,
586        (true, false) => 1,
587        (false, _) => rw,
588    });
589    accumulator_in::<EA>(
590        out,
591        lhs,
592        comptime!(Instruction::Registers { config }),
593        width,
594        contracted_per_step,
595        comptime!(Accumulation::Contraction(semiring)),
596    )
597}
598
599/// The plane-resident grid an accumulator gathers in, in `form`, at its identity.
600#[cube]
601fn accumulator_in<EA: Numeric>(
602    out: &Tile,
603    lhs: &Tile,
604    #[comptime] form: Instruction,
605    #[comptime] vector_size: usize,
606    #[comptime] contracted_per_step: usize,
607    #[comptime] accumulation: Accumulation,
608) -> Tile {
609    comptime!(assert!(
610        matches!(form, Instruction::Registers { .. })
611            || accumulation == Accumulation::Contraction(Semiring::SUM_PROD),
612        "Tile::accumulator: a hardware instruction accumulates under the sum of products alone, \
613         not {accumulation:?}; contract in a register block to sum under another"
614    ));
615    let mut acc = PlanePartition::mirror::<EA>(
616        comptime!(out.place.space.clone()),
617        comptime!(MatrixAxes::accumulator(&out.place.space, &lhs.place.space)),
618        comptime!(form),
619        comptime!(GridShape::new(&out.place, &lhs.place.space)),
620        vector_size,
621        contracted_per_step,
622        accumulation,
623        comptime!(out.place.depth),
624        comptime!(out.place.levels.clone()),
625    );
626    acc.init_identity::<EA>(comptime!(accumulation.monoid()));
627    acc
628}
629
630#[cube]
631impl Tile {
632    /// A plane-resident accumulator over one region of `walk` along `axes`, summing `lhs` times a
633    /// factor in the matrix instruction `instruction` names, opened at zero: the accumulator
634    /// [`Tile::scratch`] would open, without the memory, for a product contracted again before
635    /// anything drains it, as an attention's score.
636    pub fn stage_accumulator<EA: Numeric>(
637        walk: &Walk,
638        #[comptime] axes: Vec<Axis>,
639        lhs: &Tile,
640        #[comptime] instruction: Instruction,
641    ) -> Tile {
642        comptime!(assert!(
643            !matches!(instruction, Instruction::Registers { .. }),
644            "Tile::stage_accumulator: a register block's accumulator is a unit's, opened on the \
645             output it drains into"
646        ));
647        let place = comptime!(Placement::new(
648            walk.level.child(&walk.space).subspace(&axes),
649            walk.depth(),
650            walk.parent.path.root_levels(),
651        ));
652        let accumulation = comptime!(Accumulation::Contraction(Semiring::SUM_PROD));
653        let mut acc = PlanePartition::mirror::<EA>(
654            comptime!(place.space.clone()),
655            comptime!(MatrixAxes::accumulator(&place.space, &lhs.place.space)),
656            instruction,
657            comptime!(GridShape::new(&place, &lhs.place.space)),
658            1usize,
659            1usize,
660            accumulation,
661            comptime!(place.depth),
662            comptime!(place.levels.clone()),
663        );
664        acc.init_identity::<EA>(comptime!(accumulation.monoid()));
665        acc
666    }
667}
668
669/// [`Tile::drained_into`]'s descent over `levels[i..]`, applying `pass` at every leaf.
670#[cube]
671fn drain_below<A: Numeric, O: Numeric>(
672    acc: &Tile,
673    dest: &Tile,
674    #[comptime] levels: Vec<Level>,
675    #[comptime] i: usize,
676    #[comptime] pass: DrainPass,
677) {
678    if comptime!(i == levels.len()) {
679        drain_leaf::<A, O>(acc, dest, pass);
680    } else {
681        let level = comptime!(levels[i].clone());
682        // A walked level's coordinates must be constants: its regions select fragments.
683        if comptime!(level.coverage() == Coverage::Walk) {
684            for region in dest.over(&level).unrolled() {
685                drain_below::<A, O>(
686                    &acc.at(&region),
687                    &dest.at(&region),
688                    comptime!(levels.clone()),
689                    comptime!(i + 1),
690                    pass,
691                );
692            }
693        } else {
694            for region in dest.over(&level) {
695                drain_below::<A, O>(
696                    &acc.at(&region),
697                    &dest.at(&region),
698                    comptime!(levels.clone()),
699                    comptime!(i + 1),
700                    pass,
701                );
702            }
703        }
704    }
705}
706
707/// [`Tile::drained_chunk_into`]'s descent over `levels[i..]`: [`drain_below`]'s, the fragment
708/// grid's coordinates summed along the way into `place`, the chunk a fragment falls in being
709/// `place` modulo `chunks`.
710#[cube]
711#[allow(clippy::needless_range_loop)] // `#[unroll]` requires a range loop.
712fn drain_chunk_below<A: Numeric, O: Numeric>(
713    acc: &Tile,
714    dest: &Tile,
715    #[comptime] levels: Vec<Level>,
716    #[comptime] i: usize,
717    #[comptime] pass: DrainPass,
718    place: usize,
719    turn: usize,
720    #[comptime] chunks: usize,
721) {
722    if comptime!(i == levels.len()) {
723        drain_chunk_leaf::<A, O>(acc, dest, pass, place % chunks == turn);
724    } else {
725        let level = comptime!(levels[i].clone());
726        let axes = comptime!(dest.place.space.axes().collect::<Vec<_>>());
727        // Only the fragment grid, a walked level, places a fragment: every plane holding the same
728        // tile reaches its fragments at the same places, the planes' own level adding nothing.
729        if comptime!(level.coverage() == Coverage::Walk) {
730            for region in dest.over(&level).unrolled() {
731                let mut here = place;
732                #[unroll]
733                for a in 0..comptime!(axes.len()) {
734                    here += region.coord(comptime!(axes[a]));
735                }
736                drain_chunk_below::<A, O>(
737                    &acc.at(&region),
738                    &dest.at(&region),
739                    comptime!(levels.clone()),
740                    comptime!(i + 1),
741                    pass,
742                    here,
743                    turn,
744                    chunks,
745                );
746            }
747        } else {
748            for region in dest.over(&level) {
749                drain_chunk_below::<A, O>(
750                    &acc.at(&region),
751                    &dest.at(&region),
752                    comptime!(levels.clone()),
753                    comptime!(i + 1),
754                    pass,
755                    place,
756                    turn,
757                    chunks,
758                );
759            }
760        }
761    }
762}
763
764/// [`drain_leaf`] for a fragment that lands only where `lands`: every barrier is reached
765/// whatever it says, so planes landing different fragments still meet at the same ones.
766#[cube]
767fn drain_chunk_leaf<A: Numeric, O: Numeric>(
768    acc: &Tile,
769    dest: &Tile,
770    #[comptime] pass: DrainPass,
771    lands: bool,
772) {
773    match comptime!(pass) {
774        DrainPass::Copy => {
775            if lands {
776                let mut window = dest.clone();
777                window.copy_cast_from(acc);
778            }
779        }
780        DrainPass::Spill => {
781            if lands {
782                acc.spill_to_scratch::<A>();
783            }
784        }
785        DrainPass::Add => {
786            if lands {
787                let mut window = dest.clone();
788                window.add_from_scratch::<O, A>(acc);
789            }
790        }
791        DrainPass::Bounce => {
792            let mut window = dest.clone();
793            acc.sync_scratch();
794            if lands {
795                acc.spill_to_scratch::<A>();
796            }
797            acc.sync_scratch();
798            if lands {
799                window.add_from_scratch::<O, A>(acc);
800            }
801            acc.sync_scratch();
802        }
803    }
804}
805
806/// One tile of a drain against the one window it lands in.
807#[cube]
808fn drain_leaf<A: Numeric, O: Numeric>(acc: &Tile, dest: &Tile, #[comptime] pass: DrainPass) {
809    match comptime!(pass) {
810        DrainPass::Copy => {
811            let mut window = dest.clone();
812            window.copy_cast_from(acc);
813        }
814        DrainPass::Spill => acc.spill_to_scratch::<A>(),
815        DrainPass::Add => {
816            let mut window = dest.clone();
817            window.add_from_scratch::<O, A>(acc);
818        }
819        DrainPass::Bounce => {
820            let mut window = dest.clone();
821            acc.sync_scratch();
822            acc.spill_to_scratch::<A>();
823            acc.sync_scratch();
824            window.add_from_scratch::<O, A>(acc);
825            acc.sync_scratch();
826        }
827    }
828}
829
830#[cfg(test)]
831mod tests {
832    use super::*;
833    use crate::space::{CubeOrder, PlaneOrder};
834
835    const M: Axis = Axis(0);
836    const N: Axis = Axis(1);
837    const K: Axis = Axis(2);
838
839    /// A plane's `16 × 16` tile, walked through `K`.
840    fn plane() -> Levels {
841        Levels::leaf(&[(M, 16), (N, 16), (K, 16)]).walk_every(&[K])
842    }
843
844    /// A plane's windows sized over its own box: one for every plane of its cube.
845    fn windows(levels: Vec<Level>) -> usize {
846        let partitioning =
847            Partitioning::new(Space::new(&[(M, 64), (N, 64), (K, 64)]), levels.clone());
848        plane_windows(&partitioning.box_of(ComputeScope::Plane), &levels)
849    }
850
851    /// The planes of the launch, four to a cube, size the four windows a cube level holding four
852    /// planes does: a plane's own box holds one tile of the level, not the cube's four.
853    #[test]
854    fn the_launch_planes_size_a_window_for_every_plane_of_the_cube() {
855        let of_each_cube = plane().planes(&[(M, 2), (N, 2)]).cubes(&[M, N]).build();
856        let of_the_launch = plane()
857            .launch_planes(
858                &[M, N],
859                PlaneOrder::new(&[(M, 2), (N, 2)], CubeOrder::RowMajor),
860            )
861            .build();
862        assert_eq!(windows(of_each_cube), 4);
863        assert_eq!(windows(of_the_launch), 4);
864    }
865
866    /// Blocks of one plane grouped two to a cube: the cube's two planes, not the block's one.
867    #[test]
868    fn blocks_grouped_in_a_cube_size_a_window_for_each_of_its_planes() {
869        let levels = plane()
870            .launch_planes(
871                &[M, N],
872                PlaneOrder::new(&[], CubeOrder::RowMajor).cubes_of(2),
873            )
874            .build();
875        assert_eq!(windows(levels), 2);
876    }
877}