Skip to main content

cubecl_tile/tile/kind/memory/
base.rs

1//! What a [`Memory`] is: its [`Backing`], its [`Store`], and how it may be touched ([`Access`]).
2
3use cubecl::prelude::*;
4
5use cubecl_std::tensor::WriteOnly;
6
7use crate::*;
8
9/// A lifetime-erased buffer, its fixed `layout`, and the window this tile looks at. The buffer
10/// holds its element as data; what reads or writes it names the element ([`Store::buffer`]).
11#[derive(CubeType, Clone)]
12#[expand(derive(Clone))]
13pub(crate) struct Memory {
14    /// Which memory the bytes sit in.
15    #[cube(comptime)]
16    pub(crate) address: AddressSpace,
17    /// What the bytes are and mean.
18    pub(crate) store: Store,
19    /// How a logical coordinate becomes a buffer offset; fixed at construction.
20    pub(crate) layout: BufferLayout,
21    /// The region of the physical buffer this tile covers; narrowed by [`at`](Tile::at).
22    pub(crate) window: Window,
23    /// How the tile's logical axes address the buffer's physical ones; fixed at construction.
24    #[cube(comptime)]
25    pub(crate) projection: Projection,
26    /// The runtime half of [`projection`](Self::projection): its coefficients and window phase.
27    pub(crate) map: RuntimeMap,
28    /// The runtime constant terms of the projection, one signed value per dynamic-offset axis.
29    pub(crate) offsets: Coords<i32>,
30    /// The window origin's line offset, accumulated across [`at`](Tile::at)s.
31    pub(crate) window_start: u32,
32    /// How this store may be touched; decided at construction.
33    #[cube(comptime)]
34    pub(crate) access: Access,
35    /// What the plane's units are to these cells, stamped across [`at`](Tile::at)s.
36    #[cube(comptime)]
37    pub(crate) unit_share: UnitShare,
38    /// What one instance holds of these cells; read by accumulators only.
39    #[cube(comptime)]
40    pub(crate) split_share: SplitShare,
41    /// What the accumulation being lowered starts from ([`InitFrom`]).
42    #[cube(comptime)]
43    pub(crate) init_from: InitFrom,
44    /// How a contraction into this window runs ([`Tile::accumulating`](crate::Tile::accumulating));
45    /// `None` until stated.
46    #[cube(comptime)]
47    pub(crate) contraction: Option<Contraction>,
48    /// Where this tile's cells sit in the buffer they were filled from; `Some` only for a gathered
49    /// stage.
50    pub(crate) source_window: ComptimeOption<SourceWindow>,
51    /// The table these values index ([`Tile::lookup`](crate::Tile::lookup)); empty when none.
52    pub(crate) codebook: Codebook,
53    /// The table that places this window ([`Indexing`]); `None` for an operand read where its
54    /// windows are cut.
55    pub(crate) index: ComptimeOption<IndexTable>,
56}
57
58/// How a contraction into a memory window runs: the register block a unit's share is tiled into,
59/// and the semiring it sums under.
60#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
61pub(crate) struct Contraction {
62    pub(crate) block: RegisterBlock,
63    pub(crate) semiring: Semiring,
64}
65
66/// Which memory a [`Memory`] tile's buffer sits in.
67#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
68pub(crate) enum AddressSpace {
69    Global,
70    Shared,
71}
72
73/// What backs a [`Memory`]'s values: an addressable buffer or an erased call.
74/// Address-shaped operations on a call are a comptime panic.
75#[derive(CubeType, Clone)]
76#[expand(derive(Clone))]
77#[allow(dead_code)] // Built through the derived `new_*` expand constructors.
78pub(crate) enum Backing {
79    /// Bytes this kernel addresses directly, grouped into lines at `vector_size`.
80    Buffer(Raw<Slice>),
81    /// A write-only destination that is not memory (an erased tensor).
82    WriteCall(RawCall<WriteOnly>),
83    /// A read-only producer that is not memory.
84    ReadCall(RawCall<ReadOnly>),
85}
86
87/// What a [`Memory`]'s values are: their backing, line width and packing.
88#[derive(CubeType, Clone)]
89#[expand(derive(Clone))]
90pub(crate) struct Store {
91    /// What backs the values.
92    pub(crate) backing: Backing,
93    /// Physical line size of the destination, `1` when unvectorized.
94    #[cube(comptime)]
95    pub(crate) vector_size: usize,
96    /// How the buffer's values sit in it, from [`TileSpec::packed`].
97    #[cube(comptime)]
98    pub(crate) packing: Packing,
99    /// How the buffer is stored: its stated storage tiles, finest first
100    /// ([`TileSpec::stored_tiles`]); empty for a plain buffer and every stage. What one load of it
101    /// covers follows from these and the width ([`Tile::vector_tile`]).
102    #[cube(comptime)]
103    pub(crate) stored_tiles: Vec<(Axis, usize)>,
104}
105
106#[cube]
107impl Memory {
108    /// What one vector load of this memory covers over a window spanning `space`: the stored
109    /// tiles that hold [`vector_size`](Store::vector_size) values, a run along the innermost axis
110    /// where none are stated ([`VectorTile::new`]).
111    pub(crate) fn vector_tile(&self, #[comptime] space: &Space) -> comptime_type!(VectorTile) {
112        let width = comptime!(self.store.vector_size);
113        comptime!(
114            VectorTile::new(
115                &self.store.stored_tiles,
116                space.axis_at(space.rank() - 1),
117                width
118            )
119            .unwrap_or_else(|why| panic!("Memory: {width} values a load: {why}"))
120        )
121    }
122
123    /// How a contraction into this window runs, as stated with
124    /// [`Tile::accumulating`](crate::Tile::accumulating). Refuses a window never stated.
125    pub(crate) fn stated_contraction(&self) -> comptime_type!(Contraction) {
126        comptime!(self.contraction.unwrap_or_else(|| panic!(
127            "Tile::mma: a memory window contracts under the register block and semiring it is \
128             stated with; state them with Tile::accumulating(block, semiring)"
129        )))
130    }
131}
132
133#[cube]
134impl Store {
135    /// A store whose buffer is laid down in rows, stored in no tiles: every stage.
136    pub(crate) fn untiled(
137        backing: Backing,
138        #[comptime] vector_size: usize,
139        #[comptime] packing: Packing,
140    ) -> Store {
141        Store {
142            backing,
143            vector_size,
144            packing,
145            stored_tiles: comptime!(Vec::new()),
146        }
147    }
148
149    /// Whether these values sit at an address, which a slice-shaped write such as a fragment's
150    /// store intrinsic needs; a write call has none.
151    pub(crate) fn addressed(&self) -> comptime_type!(bool) {
152        match &self.backing {
153            Backing::Buffer(_) | Backing::ReadCall(_) => comptime!(true),
154            Backing::WriteCall(_) => comptime!(false),
155        }
156    }
157
158    /// The bytes, for a destination that has an address.
159    // `Box<[T]>` is cubecl's owned-slice handle, not a Rust box; `&[T]` is a different kernel type.
160    #[allow(clippy::borrowed_box)]
161    pub(crate) fn buffer<T: Numeric>(&self) -> &Box<[T]> {
162        match &self.backing {
163            Backing::Buffer(buffer) => buffer.at::<T>(),
164            Backing::WriteCall(_) => panic!(
165                "Store::buffer: this tile's backing is written through a call, which has no \
166                 address, it can only be written through its layout"
167            ),
168            Backing::ReadCall(_) => panic!(
169                "Store::buffer: this tile's backing is read through a call, which has no \
170                 address, it can only be read through its layout (Memory::read_view), so the \
171                 slice-shaped paths (a dense run, a re-typed quant storage, a tensor-map load) \
172                 are closed to it"
173            ),
174        }
175    }
176
177    /// Free the shared memory a buffer backing is held in, for what is declared after it to take;
178    /// a call holds none.
179    pub(crate) fn free(&self) {
180        match &self.backing {
181            Backing::Buffer(buffer) => buffer.free(),
182            Backing::WriteCall(_) | Backing::ReadCall(_) => {}
183        }
184    }
185
186    /// Whether the values have an address: a buffer, rather than a call.
187    pub(crate) fn has_address(&self) -> comptime_type!(bool) {
188        match &self.backing {
189            Backing::Buffer(_) => comptime!(true),
190            Backing::WriteCall(_) | Backing::ReadCall(_) => comptime!(false),
191        }
192    }
193
194    /// The mutable twin of [`buffer`](Self::buffer).
195    #[allow(clippy::borrowed_box)]
196    pub(crate) fn buffer_mut<T: Numeric>(&mut self) -> &mut Box<[T]> {
197        match &mut self.backing {
198            Backing::Buffer(buffer) => buffer.at_mut::<T>(),
199            Backing::WriteCall(_) => panic!(
200                "Store::buffer_mut: this tile's backing is written through a call, which has no \
201                 address, it can only be written through its layout"
202            ),
203            Backing::ReadCall(_) => panic!(
204                "Store::buffer_mut: this tile's backing is read through a call, which is \
205                 read-only"
206            ),
207        }
208    }
209}
210
211/// How a [`Memory`] may be touched; plain comptime data.
212#[derive(Clone, PartialEq, Eq, Hash, Debug)]
213pub(crate) struct Access {
214    /// Whether the window still covers the whole buffer.
215    pub whole: bool,
216    pub overhang: Overhang,
217    /// What a write here does to the cell it lands on.
218    pub write: Write,
219    /// The units that share a cooperative fill of this window.
220    pub fill: FillUnits,
221    /// What the storage tiles are to this window.
222    pub storage: WindowStorage,
223    /// Who moves this tile's lines into a stage filled from it. Stated by the operand's spec and
224    /// carried down its windows; a stage copied onward is copied by its units.
225    pub delivery: Delivery,
226}
227
228/// The units that share a cooperative fill of a window: every unit of the cube, the units of the
229/// planes the cube sets aside to fill its stages, or the units of one plane, for a stage that
230/// plane owns and fills alone ([`Stages::smem`](crate::Stages::smem)).
231#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
232pub(crate) struct FillUnits {
233    /// Whose units they are: the cube's, or one plane's.
234    pub scope: ComputeScope,
235    /// How many there are, `0` when unknown: a stage filled from this tile emits its fill
236    /// straight-line when it knows. The launch's cube size, or one plane's width.
237    pub count: usize,
238    /// The planes at the end of the cube set aside to fill its stages, whose units alone share
239    /// the fill of a cube's stage; `0` where every unit of the cube does.
240    pub fillers: usize,
241}
242
243impl FillUnits {
244    /// Every unit of the cube, `count` of them (`0` when unknown).
245    pub(crate) fn cube(count: usize) -> Self {
246        FillUnits {
247            scope: ComputeScope::Cube,
248            count,
249            fillers: 0,
250        }
251    }
252
253    /// The units of the `fillers` planes at the end of the cube set aside to fill its stages
254    /// ([`Level::fillers`](crate::Level::fillers)), how many unknown: the launch states the
255    /// cube's units, not the plane's width.
256    pub(crate) fn filling_planes(fillers: usize) -> Self {
257        FillUnits {
258            scope: ComputeScope::Cube,
259            count: 0,
260            fillers,
261        }
262    }
263
264    /// These units narrowed to `holder`'s where it has fewer: a plane's window of a buffer the
265    /// cube fills is the plane's to fill.
266    pub(crate) fn held_by(self, holder: ComputeScope) -> Self {
267        match holder < self.scope {
268            true => FillUnits {
269                scope: holder,
270                count: 0,
271                fillers: 0,
272            },
273            false => self,
274        }
275    }
276}
277
278#[cube]
279impl FillUnits {
280    /// This unit's position among the units `fill` names, which a cooperative fill takes its
281    /// lines at: its position in the cube, among the filling planes' units, or in its plane.
282    ///
283    /// A plane is the launch's `x` ([`Partitioning::cube_dim`]), so a unit's position in its plane
284    /// is `UNIT_POS_X` and the plane's width `CUBE_DIM_X`; the filling planes are the cube's last.
285    pub(crate) fn worker(#[comptime] fill: FillUnits) -> usize {
286        comptime!(assert!(
287            fill.scope != ComputeScope::Unit,
288            "FillUnits::worker: a cooperative fill is shared by a cube or a plane, never one unit"
289        ));
290        if comptime!(fill.fillers > 0) {
291            (UNIT_POS - (CUBE_DIM - comptime!(fill.fillers as u32) * CUBE_DIM_X)) as usize
292        } else {
293            ComputeScope::unit(comptime!(fill.scope))
294        }
295    }
296
297    /// How many units `fill` names at runtime: the cube's, the filling planes', or one plane's
298    /// ([`worker`](FillUnits::worker)).
299    pub(crate) fn workers(#[comptime] fill: FillUnits) -> usize {
300        if comptime!(fill.fillers > 0) {
301            return comptime!(fill.fillers) * CUBE_DIM_X as usize;
302        }
303        match comptime!(fill.scope) {
304            ComputeScope::Cube => CUBE_DIM as usize,
305            ComputeScope::Plane => CUBE_DIM_X as usize,
306            ComputeScope::PlaneGroup { planes } => planes * CUBE_DIM_X as usize,
307            ComputeScope::Unit => comptime!(panic!(
308                "FillUnits::workers: a cooperative fill is shared by a cube or a plane, never \
309                 one unit"
310            )),
311        }
312    }
313}
314
315/// What a write to a store does to the cell it lands on; stated by the binding operand. `Replace`
316/// is the writer's own cell, `Accumulate` adds atomically so writers need not know of each other,
317/// `Exclusive` adds with a plain read and write, one writer at a cell at a time in the order its
318/// [`Schedule`] sets.
319#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
320pub enum Write {
321    /// Replaces the cell.
322    Replace,
323    /// Adds into the cell, atomically: the writers' partials land in any order.
324    Accumulate,
325    /// Replaces the cell on its first writer's turn and adds into it, reading it and writing the
326    /// sum back a line at a time, on every later one. One writer reaches a cell at a time, in the
327    /// order the schedule sets, so the sum is the same bits from run to run.
328    Exclusive(Schedule),
329}
330
331/// The order a destination's [`Exclusive`](Write::Exclusive) writers take their turns at a cell in.
332#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
333pub enum Schedule {
334    /// One writer at a time over the whole box, in the order of their runs, handed on through a
335    /// counter: the cubes of a split taking turns at a [`Relay`](crate::launch::Relay)'s carry.
336    Sequential,
337    /// Every writer every round, each on its own chunk of the box: in round `r`, writer `w` takes
338    /// chunk `(w + r) mod writers`, a cube barrier between rounds. The planes of one cube meeting
339    /// in shared memory ([`SmemCyclicAccumulation`](crate::kind::SmemCyclicAccumulation)), where a
340    /// barrier orders them.
341    Cyclic,
342}
343
344impl Write {
345    /// Refuse an accumulation `split` leaves in pieces unless this write adds them, a relay whose
346    /// partials the planes of one cube write at once, which no turn across cubes serializes, and
347    /// a `monoid` other than a sum into a destination that adds.
348    pub(crate) fn admits(self, split: SplitShare, monoid: Monoid, site: &str) {
349        match (self, monoid) {
350            (Write::Accumulate | Write::Exclusive(_), Monoid::Prod | Monoid::Max | Monoid::Min) => {
351                panic!(
352                    "{site}: this destination adds ({self:?}), atomically or relayed, so a \
353                 {monoid:?} accumulation into it would come out summed. Drain it into a \
354                 replacing destination, combining the partials first where they are split."
355                )
356            }
357            (Write::Accumulate | Write::Exclusive(_), Monoid::Sum) | (Write::Replace, _) => {}
358        }
359        match (split, self) {
360            (SplitShare::Whole, _)
361            | (
362                SplitShare::PartialAcrossCubes | SplitShare::PartialAcrossPlanes,
363                Write::Accumulate,
364            )
365            | (SplitShare::PartialAcrossCubes, Write::Exclusive(Schedule::Sequential))
366            | (SplitShare::PartialAcrossPlanes, Write::Exclusive(Schedule::Cyclic)) => {}
367            (SplitShare::PartialAcrossCubes, Write::Exclusive(Schedule::Cyclic)) => panic!(
368                "{site}: this accumulator's cells are split across cubes and its destination \
369                 takes its writers in cyclic rounds, which a cube barrier orders: it orders the \
370                 planes of one cube, never cubes. Take the cubes' turns through a `Relay`, or \
371                 combine them atomically."
372            ),
373            (SplitShare::PartialAcrossPlanes, Write::Exclusive(Schedule::Sequential)) => panic!(
374                "{site}: this accumulator's cells are split across the planes of one cube and its \
375                 destination is relayed, a plain read and write taken one cube at a time; the planes \
376                 would write the same line at once and lose each other's partial. \
377                 Drain into an atomic destination (bind it as an `AccumulateArg`), or combine \
378                 the planes' partials in the cube before its turn at the relay, atomically or in \
379                 cyclic rounds (`smem_cyclic_accumulation`)."
380            ),
381            (SplitShare::PartialAcrossCubes | SplitShare::PartialAcrossPlanes, Write::Replace) => {
382                panic!(
383                    "{site}: this accumulator's cells are split across planes or cubes and its \
384                 destination replaces rather than accumulates, so every partial but one would be \
385                 lost. \
386                 A contracted axis distributed across planes or cubes gives each instance a \
387                 slice of the contraction, and none of them holds a whole cell. \
388                 Drain into an accumulating destination (bind it as an `AccumulateArg`), \
389                 distribute the contraction across the plane's units instead \
390                 (`distribute(units(n), ..)`, combined in the plane's registers), or give the \
391                 output an axis of its own for the split."
392                )
393            }
394        }
395    }
396}
397
398/// How a store relates to the window overhanging its valid data (past [`Window`]'s `bound`).
399#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
400pub(crate) enum Overhang {
401    /// Impossible: the buffer is allocated to exactly the tile (smem).
402    Never,
403    /// Excluded at launch: every shape divides its tiling (unchecked gmem).
404    Fits,
405    /// Reads/writes past `bound` are masked per the window's [`Boundary`].
406    Masked,
407    /// Masked past the end of a run ([`Memory::within`]) and nowhere else: a stage the walk proves
408    /// inside the run reads it as [`Fits`](Overhang::Fits).
409    Run,
410}
411
412/// Boundary handling mode for out-of-bounds reads/writes, carried by `Window`.
413#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
414pub enum Boundary {
415    /// Out-of-bounds reads return zero; writes are skipped.
416    Zero,
417    /// Out-of-bounds reads/writes clamp to the edge cell.
418    Clamp,
419}
420
421impl Overhang {
422    /// The flag a [`Masked`] is built with.
423    pub(crate) fn masks(&self) -> bool {
424        matches!(self, Overhang::Masked | Overhang::Run)
425    }
426}
427
428/// Whether a read still proves its own bounds, stated by the reader.
429#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
430pub(crate) enum Guard {
431    /// Mask the overhang and apply the window's [`Boundary`] on every access.
432    Checked,
433    /// The reader proved its whole box is in bounds; reading past it is out of bounds, not masked.
434    Proved,
435}
436
437impl Guard {
438    /// Whether this guard still costs a test per access.
439    pub(crate) fn checks(self) -> bool {
440        matches!(self, Guard::Checked)
441    }
442}
443
444/// How an operand's buffer is stored: what a launched spec states.
445#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
446pub enum Storage {
447    /// Untiled storage: every window lies inside the one storage tile.
448    Strided,
449    /// Storage-tiled, and the window may span several tiles.
450    /// The level is the one whose tile the storage tile is; `None` if no level's.
451    Tiled(Option<usize>),
452}
453
454/// What a storage tile is to the window an operand is read through.
455#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
456pub(crate) enum WindowStorage {
457    /// As the buffer is stored.
458    Stored(Storage),
459    /// Storage-tiled and inside one storage tile: one contiguous run from its origin.
460    Contiguous,
461}
462
463impl WindowStorage {
464    /// The storage tiling one level down: through the storage tile's own level, inside one tile.
465    pub(crate) fn below(self, depth: usize, level: &Level, space: &Space) -> WindowStorage {
466        match self {
467            WindowStorage::Contiguous
468            | WindowStorage::Stored(Storage::Strided | Storage::Tiled(None)) => self,
469            WindowStorage::Stored(Storage::Tiled(Some(tiled_at))) => {
470                assert!(
471                    depth <= tiled_at,
472                    "Memory::at: this window is above its storage tile, the tile of level \
473                     {tiled_at}, yet the descent is at depth {depth}; the storage tile's level was \
474                     skipped past"
475                );
476                if depth < tiled_at {
477                    return self;
478                }
479                for axis in space.axes() {
480                    assert!(
481                        matches!(level.extent_in(space, axis), Extent::Static(_)),
482                        "Memory::at: level {tiled_at} hands {axis:?} down dynamic, so its tile is \
483                         no storage tile"
484                    );
485                }
486                WindowStorage::Contiguous
487            }
488        }
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495
496    /// A relay takes one cube at a time: the cubes of a split meet it in turns, and an atomic
497    /// destination takes a split at either scope.
498    #[test]
499    fn a_relay_takes_a_split_across_cubes() {
500        Write::Exclusive(Schedule::Sequential).admits(
501            SplitShare::PartialAcrossCubes,
502            Monoid::Sum,
503            "test",
504        );
505        Write::Accumulate.admits(SplitShare::PartialAcrossCubes, Monoid::Sum, "test");
506        Write::Accumulate.admits(SplitShare::PartialAcrossPlanes, Monoid::Sum, "test");
507    }
508
509    /// The planes of one cube write a cell at once, which no turn across cubes serializes.
510    #[test]
511    #[should_panic(expected = "split across the planes of one cube")]
512    fn a_relay_refuses_a_split_across_planes() {
513        Write::Exclusive(Schedule::Sequential).admits(
514            SplitShare::PartialAcrossPlanes,
515            Monoid::Sum,
516            "test",
517        );
518    }
519
520    /// A destination that adds sums whatever it is handed, so it takes only a sum.
521    #[test]
522    #[should_panic(expected = "would come out summed")]
523    fn an_atomic_destination_refuses_a_max() {
524        Write::Accumulate.admits(SplitShare::Whole, Monoid::Max, "test");
525    }
526
527    #[test]
528    #[should_panic(expected = "would come out summed")]
529    fn a_relay_refuses_a_max() {
530        Write::Exclusive(Schedule::Sequential).admits(
531            SplitShare::PartialAcrossCubes,
532            Monoid::Max,
533            "test",
534        );
535    }
536
537    /// A replacing destination takes any monoid: each cell is its writer's own.
538    #[test]
539    fn a_replacing_destination_takes_any_monoid() {
540        for monoid in [Monoid::Sum, Monoid::Prod, Monoid::Max, Monoid::Min] {
541            Write::Replace.admits(SplitShare::Whole, monoid, "test");
542        }
543    }
544}