Skip to main content

cubecl_tile/tile/
index.rs

1//! A table of `u32` entries an operand carries ([`TileArgLaunch::indexed`]), read where its window
2//! is cut: each row's expert, each tile's group, each group's run of rows.
3//!
4//! The table's dims are its key axes, in order: it holds one entry per tile of the first level of
5//! the nest that cuts them, the same entry a kernel would read off `region.coord(key)` itself.
6//! The tile reads that entry once, in [`Tile::at`], at the step that cuts the keys, and places its
7//! window with it ([`Indexing`]); its reads never touch the table again. Where no level cuts the
8//! keys they are one tile, so the table's first entry is the one, read as the tile is served. The
9//! scales it carries move with it wherever they span the axis the entry places, so a routed
10//! operand may be quantized.
11
12use cubecl::prelude::*;
13
14use crate::stage::pipeline::payload::base::StageOperand;
15use crate::*;
16
17/// What an operand's table entry places. The table's own dims are its keys
18/// ([`TileArgLaunch::indexed`]).
19#[derive(Clone, PartialEq, Eq, Hash, Debug)]
20pub enum Indexing {
21    /// The entry is the operand's coordinate along `axis`, which no level may cut: each row's
22    /// expert, keyed by the row; each tile's group, keyed by the row axis a level cuts in tiles.
23    /// The axis is one wide below it, so a contraction sees one slab. An entry past the axis
24    /// names nothing: the tile holds nothing to walk ([`Region::walk_within`]), and a read of it
25    /// outside a walk reads the last coordinate, unless the operand checks the axis with
26    /// [`Boundary::Zero`], where it reads zero.
27    Coordinate(Axis),
28    /// The entry is where the operand's run along `axis` ends, and the one before it along the
29    /// key where it starts (`0` for the first): each group's rows packed end to end, the table
30    /// their cumulative ends. The operand reads zero past the run, and a walk over its window
31    /// along `axis` steps the run alone where the axis is dynamic. A run that ends before it
32    /// starts is empty, and holds nothing to walk. No level at or above the key's may cut `axis`.
33    Run(Axis),
34}
35
36impl Indexing {
37    /// The axis the entry places.
38    pub fn axis(&self) -> Axis {
39        match self {
40            Indexing::Coordinate(axis) | Indexing::Run(axis) => *axis,
41        }
42    }
43
44    /// What an operand of `values` owes an index keyed by `keys`, checked as the table is bound.
45    ///
46    /// # Errors
47    ///
48    /// The [`Refusal`] naming the first rule the statement breaks.
49    pub(crate) fn validate(
50        &self,
51        values: &TileSpec,
52        width: usize,
53        keys: &[Axis],
54    ) -> Result<(), Refusal> {
55        let axis = self.axis();
56        if !values.axes().contains(&axis) {
57            return Err(Refusal::IndexOfAnAxisNotSpanned(axis));
58        }
59        if keys.is_empty() || keys.contains(&axis) {
60            return Err(Refusal::IndexKeyedByItsAxis(axis));
61        }
62        if matches!(self, Indexing::Run(_)) && keys.len() != 1 {
63            return Err(Refusal::RunKeyedByMany(keys.len()));
64        }
65        self.places(values, width)
66    }
67
68    /// What an operand bound `width` values a line owes where an entry moves its window, the
69    /// values' or a scale's spanning the placed axis: a buffer neither storage-tiled nor paged, one
70    /// dim of which is the axis alone, read one value a line where that dim is the innermost, since
71    /// an entry is an element and a line would straddle two; under a run, a buffer over its
72    /// labelled dims, as [`Tile::within`] places.
73    ///
74    /// # Errors
75    ///
76    /// The [`Refusal`] naming the first rule `spec` breaks.
77    pub(crate) fn places(&self, spec: &TileSpec, width: usize) -> Result<(), Refusal> {
78        let projection = &spec.projection;
79        let stated =
80            projection.is_tiled() || !spec.stored_tiles.is_empty() || spec.paging.is_some();
81        let gathered = matches!(self, Indexing::Run(_)) && !projection.is_direct();
82        let dim = placed_dim(projection, self.axis());
83        match dim {
84            _ if stated || gathered => Err(Refusal::IndexOfAStatedLayout),
85            None => Err(Refusal::IndexOfAStatedLayout),
86            Some(dim)
87                if dim + 1 == projection.physical_rank()
88                    && (width > 1 || spec.packing != Packing::Plain) =>
89            {
90                Err(Refusal::IndexAlongALine(self.axis()))
91            }
92            Some(_) => Ok(()),
93        }
94    }
95
96    /// Where in `levels` the table is read: the first level that cuts a key, which must cut them
97    /// all, and below which (for a run, at or above which) no level cuts the placed axis; `None`
98    /// where no level cuts the keys, which are then one tile, read as the tile is served.
99    ///
100    /// # Errors
101    ///
102    /// The [`Refusal`] naming the first rule the nest breaks.
103    pub(crate) fn read_at(
104        &self,
105        keys: &[Axis],
106        levels: &[Level],
107    ) -> Result<Option<usize>, Refusal> {
108        let cuts = |level: &Level, axis: Axis| level.count(axis).is_some();
109        let at = levels
110            .iter()
111            .position(|level| keys.iter().any(|&key| cuts(level, key)));
112        if let Some(at) = at
113            && !keys.iter().all(|&key| cuts(&levels[at], key))
114        {
115            return Err(Refusal::IndexKeysCutApart(keys.to_vec()));
116        }
117        let axis = self.axis();
118        let placed_by_a_level = match (self, at) {
119            (Indexing::Coordinate(_), _) => levels.iter().any(|level| cuts(level, axis)),
120            (Indexing::Run(_), Some(at)) => levels[..=at].iter().any(|level| cuts(level, axis)),
121            (Indexing::Run(_), None) => false,
122        };
123        match placed_by_a_level {
124            true => Err(Refusal::IndexedAxisCut(axis)),
125            false => Ok(at),
126        }
127    }
128}
129
130/// The buffer dim that is `axis` alone, which an entry moves the window along.
131pub(crate) fn placed_dim(projection: &Projection, axis: Axis) -> Option<usize> {
132    Witness::new(projection, axis).map(|witness| witness.dim())
133}
134
135/// A table as a tile reads it: its entries, its stride along each key, and whether its entry is
136/// placed yet. Held by the values' [`Memory`], beside the window it places.
137// Read through its expand alone: `Tile::indexed_at` is written at expand time.
138#[allow(dead_code)]
139#[derive(CubeType, Clone)]
140#[expand(derive(Clone))]
141pub(crate) struct IndexTable {
142    entries: Box<[u32]>,
143    /// The table's stride along each key, in entries.
144    strides: Coords<u32>,
145    #[cube(comptime)]
146    pub(crate) indexing: Indexing,
147    /// The table's dims, in order.
148    #[cube(comptime)]
149    pub(crate) keys: Vec<Axis>,
150    /// Whether the entry is read and the window placed: a placed table is never read again.
151    #[cube(comptime)]
152    pub(crate) placed: bool,
153    /// Whether the entry read names a coordinate of the axis, or a run of at least one; `true`
154    /// until it is read ([`Tile::entry_in_range`]).
155    in_range: bool,
156}
157
158#[cube]
159impl IndexTable {
160    /// `table`, its dims the `keys`, placing as `indexing` says.
161    #[allow(dead_code)] // Reached through its expand, from `TileArg::tile`.
162    pub(crate) fn new(
163        table: &Tensor<u32>,
164        #[comptime] keys: Vec<Axis>,
165        #[comptime] indexing: Indexing,
166    ) -> IndexTable {
167        let mut strides = Coords::<u32>::new();
168        #[unroll]
169        for i in 0..comptime!(keys.len()) {
170            strides.push(table.stride(i) as u32);
171        }
172        IndexTable {
173            entries: unsafe { table.as_slice().as_boxed_unchecked() },
174            strides,
175            indexing,
176            keys,
177            placed: comptime!(false),
178            in_range: true,
179        }
180    }
181
182    /// The run whose end is the entry at `at`, `(start, end)`: its start the entry before it
183    /// along the one key, or `0` for the first.
184    #[allow(dead_code)] // Reached through its expand, from `Tile::indexed_at`.
185    pub(crate) fn run(&self, at: u32) -> (u32, u32) {
186        let stride = self.strides.at(0usize);
187        let previous = self.entry(at.max_with(stride).minus(stride));
188        (select(at == 0u32, 0u32, previous), self.entry(at))
189    }
190
191    /// Where the entry of `step`'s tile of the keys lies in the table.
192    #[allow(dead_code)] // Reached through its expand, from `Tile::indexed_at`.
193    pub(crate) fn position(&self, step: &Step) -> u32 {
194        let mut at = 0u32;
195        #[unroll]
196        for i in 0..comptime!(self.keys.len()) {
197            let key = comptime!(self.keys[i]);
198            at = at.plus(step.coord(key).cast::<u32>().times(self.strides.at(i)));
199        }
200        at
201    }
202
203    /// The entry at `at`, or the table's last past it: the table is device data a launch sized,
204    /// and a read past it would land outside the buffer.
205    #[allow(dead_code)] // Reached through its expand, from `Tile::indexed_at`.
206    pub(crate) fn entry(&self, at: u32) -> u32 {
207        let last = (self.entries.len() as u32).max_with(1u32).minus(1u32);
208        self.entries[at.min_with(last).cast::<usize>()]
209    }
210
211    /// This table with its entry read and placed, `in_range` saying whether the entry named a
212    /// coordinate of the axis, or a run of at least one.
213    #[allow(dead_code)] // Reached through its expand, from `Tile::indexed_at`.
214    pub(crate) fn read(&self, in_range: bool) -> IndexTable {
215        IndexTable {
216            entries: self.entries.clone(),
217            strides: self.strides.clone(),
218            indexing: comptime!(self.indexing.clone()),
219            keys: comptime!(self.keys.clone()),
220            placed: comptime!(true),
221            in_range,
222        }
223    }
224}
225
226/// Whether the run `[from, until)` holds a position.
227#[cube]
228fn nonempty(from: u32, until: u32) -> bool {
229    from < until
230}
231
232/// What a tile with no table to read answers: its window is where it lies.
233#[cube]
234fn no_table() -> bool {
235    true
236}
237
238/// Where the first entry lies: the one a table no level's keys cut is read at.
239#[cube]
240fn first_entry() -> u32 {
241    0u32
242}
243
244impl Tile {
245    /// Whether the entry this tile's table placed it with names a coordinate of the axis it
246    /// places, or a run of at least one row: `false` for an entry past a stack of slabs, as a
247    /// padding row's route or a padding tile's group names. Such a tile holds nothing, so a walk
248    /// within it ([`Region::walk_within`]) takes no step. `true` for a tile with no table, or none
249    /// read yet.
250    pub fn entry_in_range(&self) -> bool {
251        cubecl::unexpanded!()
252    }
253
254    /// This tile with its table read where `step` cuts the table's keys: its window, and the
255    /// windows of the scales it carries that span the axis the entry places, placed by the entry.
256    /// Any other tile, or step, as it is.
257    pub(crate) fn indexed_at(self, _step: &Step) -> Tile {
258        cubecl::unexpanded!()
259    }
260
261    /// This tile with `table` to read where a step cuts its keys.
262    #[allow(dead_code)] // Reached through its expand, from `TileArg::tile`.
263    pub(crate) fn with_index(self, _table: IndexTable) -> Tile {
264        cubecl::unexpanded!()
265    }
266
267    /// This tile with its table read at its first entry: what a table whose keys no level cuts
268    /// holds, the keys being one tile.
269    #[allow(dead_code)] // Reached through its expand, from `TileArg::tile`.
270    pub(crate) fn indexed_whole(self) -> Tile {
271        cubecl::unexpanded!()
272    }
273}
274
275impl TileExpand {
276    pub fn __expand_entry_in_range_method(&self, scope: &Scope) -> NativeExpand<bool> {
277        match &self.kind {
278            TileKindExpand::Memory(MemoryExpand {
279                index: ComptimeOptionExpand::Some(table),
280                ..
281            }) => table.in_range,
282            _ => no_table::expand(scope),
283        }
284    }
285
286    pub(crate) fn __expand_indexed_at_method(self, scope: &Scope, step: &StepExpand) -> TileExpand {
287        let TileKindExpand::Memory(memory) = &self.kind else {
288            return self;
289        };
290        let ComptimeOptionExpand::Some(table) = &memory.index else {
291            return self;
292        };
293        let cuts = |key: &Axis| step.level.count(*key).is_some();
294        if table.placed || !table.keys.iter().any(cuts) {
295            return self;
296        }
297        assert!(
298            table.keys.iter().all(cuts),
299            "{}",
300            Refusal::IndexKeysCutApart(table.keys.clone())
301        );
302        let at = table.__expand_position_method(scope, step);
303        self.read_index(scope, at)
304    }
305
306    pub(crate) fn __expand_indexed_whole_method(self, scope: &Scope) -> TileExpand {
307        let at = first_entry::expand(scope);
308        self.read_index(scope, at)
309    }
310
311    /// This tile placed by the entry at `at` of the table it holds, which is then read.
312    fn read_index(self, scope: &Scope, at: NativeExpand<u32>) -> TileExpand {
313        let TileKindExpand::Memory(MemoryExpand {
314            index: ComptimeOptionExpand::Some(table),
315            ..
316        }) = &self.kind
317        else {
318            unreachable!("only a tile holding a table reads it")
319        };
320        let table = table.clone();
321        let (placed, in_range) = match table.indexing {
322            Indexing::Coordinate(axis) => {
323                let entry = table.__expand_entry_method(scope, at);
324                let in_range = self.__expand_names_coordinate_method(scope, axis, entry);
325                let entry = self.__expand_clamped_method(scope, axis, entry);
326                (self.__expand_placed_at_method(scope, axis, entry), in_range)
327            }
328            Indexing::Run(axis) => {
329                let (start, end) = table.__expand_run_method(scope, at);
330                (
331                    self.__expand_run_within_method(scope, axis, start, end),
332                    nonempty::expand(scope, start, end),
333                )
334            }
335        };
336        let read = table.__expand_read_method(scope, in_range);
337        placed.__expand_with_index_method(scope, read)
338    }
339
340    pub(crate) fn __expand_with_index_method(
341        mut self,
342        _scope: &Scope,
343        table: IndexTableExpand,
344    ) -> TileExpand {
345        match &mut self.kind {
346            TileKindExpand::Memory(memory) => memory.index = ComptimeOptionExpand::Some(table),
347            _ => panic!("{}", Refusal::IndexOfAStatedLayout),
348        }
349        self
350    }
351}
352
353#[cube]
354impl Tile {
355    /// Whether `coord`, an entry naming a coordinate along `axis`, lies within the axis.
356    #[allow(dead_code)] // Reached through its expand, from `Tile::indexed_at`.
357    fn names_coordinate(&self, #[comptime] axis: Axis, coord: u32) -> bool {
358        let memory = self.mem("indexed_at");
359        let p = comptime!(placed_dim(&memory.projection, axis).unwrap());
360        coord < memory.window.bound.at(p)
361    }
362
363    /// `coord`, an entry naming a coordinate along `axis`, clamped to the axis unless the operand
364    /// reads zero past it.
365    #[allow(dead_code)] // Reached through its expand, from `Tile::indexed_at`.
366    fn clamped(&self, #[comptime] axis: Axis, coord: u32) -> u32 {
367        let memory = self.mem("indexed_at");
368        let p = comptime!(placed_dim(&memory.projection, axis).unwrap());
369        let zero =
370            comptime!(memory.window.boundaries.get(p).copied().flatten() == Some(Boundary::Zero));
371        if comptime!(zero) {
372            coord
373        } else {
374            coord.min_with(memory.window.bound.at(p).max_with(1u32).minus(1u32))
375        }
376    }
377
378    /// This tile one wide at `coord` along `axis`, and so are the scales it carries that span it.
379    #[allow(dead_code)] // Reached through its expand, from `Tile::indexed_at`.
380    pub(crate) fn placed_at(&self, #[comptime] axis: Axis, coord: u32) -> Tile {
381        Tile {
382            kind: TileKind::new_Memory(self.mem("placed_at").placed_at(axis, coord)),
383            factor: self.factor.placed_at(axis, coord),
384            place: comptime!(Placement::new(
385                Level::every(&[(axis, 1)]).child(&self.place.space),
386                self.place.depth,
387                self.place.levels.clone()
388            )),
389            element: comptime!(self.element),
390        }
391    }
392
393    /// This tile reading the run `[from, until)` along `axis`, zero past it, and so are the scales
394    /// it carries that span it. An `until` before `from` is an empty run, never a negative one.
395    #[allow(dead_code)] // Reached through its expand, from `Tile::indexed_at`.
396    pub(crate) fn run_within(&self, #[comptime] axis: Axis, from: u32, until: u32) -> Tile {
397        let until = until.max_with(from);
398        Tile {
399            kind: TileKind::new_Memory(self.mem("run_within").within(
400                axis,
401                from.cast::<usize>(),
402                until.cast::<usize>(),
403            )),
404            factor: self.factor.run_within(axis, from, until),
405            place: comptime!(self.place.clone()),
406            element: comptime!(self.element),
407        }
408    }
409}
410
411impl Region {
412    /// The regions of the level below this one, over what `tile` holds: along a dynamic axis an
413    /// index placed a run of `tile` along ([`Indexing::Run`]), the run's tiles alone; along every
414    /// other, this region's box. A tile whose entry names nothing, a coordinate past its axis or
415    /// an empty run, holds nothing, and the walk takes no step. What a cube walks its group's rows
416    /// by, every operand it holds windowed by the same regions, and what a routine walks an
417    /// indexed operand by, so a tile a padding row or a padding tile names does no work.
418    pub fn walk_within(&self, _tile: &Tile) -> Walk {
419        cubecl::unexpanded!()
420    }
421}
422
423impl RegionExpand {
424    pub fn __expand_walk_within_method(&self, scope: &Scope, tile: &TileExpand) -> WalkExpand {
425        let child = self.__expand_child_method(scope);
426        let box_space = SpaceExpand::comptime(&child);
427        let run = tile
428            .run_axis()
429            .filter(|&axis| box_space.contains(axis) && box_space.is_dynamic(axis));
430        let sized = match run {
431            Some(axis) => {
432                let extent = tile.clone().__expand_runtime_extent_method(scope, axis);
433                with_extent::expand(scope, &child, box_space.position(axis), extent)
434            }
435            None => child,
436        };
437        let level = self.path.next();
438        let mut walk = Walk::__expand_of(scope, &sized, level.clone(), self.clone());
439        walk.run = run.filter(|&axis| level.axes() == [axis] && !level.is_reversed(axis));
440        // A walk stepping the run itself is as long as the run, empty for an empty one.
441        match (tile.read_entry_in_range(), walk.run) {
442            (Some(in_range), None) => over_what_is_held::expand(scope, walk, in_range),
443            _ => walk,
444        }
445    }
446}
447
448/// `walk`, a fresh one, over all its regions, or none where the tile walked holds nothing.
449#[cube]
450fn over_what_is_held(walk: Walk, in_range: bool) -> Walk {
451    let steps = select(in_range, walk.total(), 0usize);
452    walk.range(0usize, steps)
453}
454
455/// `space` with the runtime size of its dynamic axis at `p` set to `extent`.
456#[cube]
457fn with_extent(space: &Space, #[comptime] p: usize, extent: usize) -> Space {
458    let mut sizes = Sequence::<usize>::new();
459    #[unroll]
460    for i in 0..comptime!(space.rank()) {
461        if comptime!(i == p) {
462            sizes.push(extent);
463        } else {
464            sizes.push(space.runtime_extent_at(i));
465        }
466    }
467    Space::with_sizes(comptime!(space.clone()), sizes)
468}
469
470impl TileExpand {
471    /// Whether the entry this tile's table placed it with names something, where it holds a
472    /// table it has read.
473    fn read_entry_in_range(&self) -> Option<NativeExpand<bool>> {
474        match &self.kind {
475            TileKindExpand::Memory(MemoryExpand {
476                index: ComptimeOptionExpand::Some(table),
477                ..
478            }) if table.placed => Some(table.in_range),
479            _ => None,
480        }
481    }
482
483    /// The axis an index placed a run of this tile along, where it holds one.
484    pub(crate) fn run_axis(&self) -> Option<Axis> {
485        match &self.kind {
486            TileKindExpand::Memory(MemoryExpand {
487                index: ComptimeOptionExpand::Some(table),
488                ..
489            }) if table.placed => match table.indexing {
490                Indexing::Run(axis) => Some(axis),
491                Indexing::Coordinate(_) => None,
492            },
493            _ => None,
494        }
495    }
496}
497
498impl Tile {
499    /// This tile read where a walk proved it inside its run: a window masked only past the run's
500    /// end ([`Overhang::Run`]) reads unchecked, any other tile as it is. Its scales are read as
501    /// they are.
502    #[allow(dead_code)] // Reached through its expand, from `Payload::inside_runs`.
503    pub(crate) fn inside_run(&self) -> Tile {
504        cubecl::unexpanded!()
505    }
506}
507
508impl TileExpand {
509    pub(crate) fn __expand_inside_run_method(&self, _scope: &Scope) -> TileExpand {
510        let mut tile = self.clone();
511        if let TileKindExpand::Memory(memory) = &mut tile.kind
512            && memory.access.overhang == Overhang::Run
513        {
514            memory.access.overhang = Overhang::Fits;
515        }
516        tile
517    }
518}
519
520#[cube]
521impl Memory {
522    /// The bound a walk reads off dim `p`: the window's, or its run's length where an index placed
523    /// a run along it, so a walk over the window steps the run alone.
524    pub(crate) fn walked_bound(&self, #[comptime] p: usize) -> u32 {
525        let bound = self.window.bound.at(p);
526        let runs = self.runs_along(p);
527        if comptime!(runs) {
528            bound.minus(self.window.origin.at(p).cast::<u32>())
529        } else {
530            bound
531        }
532    }
533}
534
535impl Memory {
536    /// Whether an index placed a run along dim `p` of this window.
537    pub(crate) fn runs_along(&self, _p: usize) -> comptime_type!(bool) {
538        cubecl::unexpanded!()
539    }
540}
541
542impl MemoryExpand {
543    pub(crate) fn __expand_runs_along_method(&self, _scope: &Scope, p: usize) -> bool {
544        match &self.index {
545            ComptimeOptionExpand::Some(table) => {
546                table.placed
547                    && matches!(table.indexing, Indexing::Run(axis)
548                        if placed_dim(&self.projection, axis) == Some(p))
549            }
550            ComptimeOptionExpand::None => false,
551        }
552    }
553}
554
555impl Tile {
556    /// This operand as a slot plans its stage: who moves it, the axes it spans, and the keys of a
557    /// table it has yet to read.
558    pub(crate) fn stage_operand(&self) -> comptime_type!(StageOperand) {
559        cubecl::unexpanded!()
560    }
561}
562
563impl TileExpand {
564    pub(crate) fn __expand_stage_operand_method(&self, scope: &Scope) -> StageOperand {
565        StageOperand {
566            delivery: self.clone().__expand_delivery_method(scope),
567            space: self.place.space.clone(),
568            keys: self.pending_keys(),
569        }
570    }
571
572    /// The keys of a table this tile has yet to read; none where it has none, or has read it.
573    pub(crate) fn pending_keys(&self) -> Vec<Axis> {
574        match &self.kind {
575            TileKindExpand::Memory(MemoryExpand {
576                index: ComptimeOptionExpand::Some(table),
577                ..
578            }) if !table.placed => table.keys.clone(),
579            _ => Vec::new(),
580        }
581    }
582}
583
584impl Tile {
585    /// This tile shaped as one region of `level` holds it, for a stage: one wide along the axis
586    /// its table places where `level` cuts the table's keys, and so are the scales it carries
587    /// that span it. Its window is not moved; the fill of each region places it.
588    pub(crate) fn shaped_for(&self, _level: Level) -> Tile {
589        cubecl::unexpanded!()
590    }
591}
592
593impl TileExpand {
594    pub(crate) fn __expand_shaped_for_method(&self, _scope: &Scope, level: Level) -> TileExpand {
595        let TileKindExpand::Memory(MemoryExpand {
596            index: ComptimeOptionExpand::Some(table),
597            ..
598        }) = &self.kind
599        else {
600            return self.clone();
601        };
602        let Indexing::Coordinate(axis) = table.indexing else {
603            return self.clone();
604        };
605        if table.placed || !table.keys.iter().any(|key| level.count(*key).is_some()) {
606            return self.clone();
607        }
608        self.one_wide(axis)
609    }
610
611    /// This tile's place, and its spanning scales', one wide along `axis`; its window unmoved.
612    fn one_wide(&self, axis: Axis) -> TileExpand {
613        let mut shaped = self.clone();
614        if shaped.place.space.contains(axis) {
615            shaped.place.space = Level::every(&[(axis, 1)]).child(&shaped.place.space);
616        }
617        shaped.factor = self.factor.spanning(axis, |scale| scale.one_wide(axis));
618        shaped
619    }
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625
626    const M: Axis = Axis(0);
627    const N: Axis = Axis(1);
628    const K: Axis = Axis(2);
629    const EXPERT: Axis = Axis(3);
630
631    fn rows(tile: usize) -> Level {
632        Level::every(&[(M, tile)])
633    }
634
635    #[test]
636    fn a_table_is_read_at_the_first_level_cutting_its_keys() {
637        let levels = [Level::every(&[(N, 4)]), rows(2), rows(1)];
638        let read = Indexing::Coordinate(EXPERT).read_at(&[M], &levels);
639        assert_eq!(read, Ok(Some(1)));
640    }
641
642    /// No level cuts the keys, so they are one tile: the table's first entry is the one, read as
643    /// the tile is served, as a grouped product of a single group reads its one run.
644    #[test]
645    fn a_table_no_level_cuts_is_read_as_the_tile_is_served() {
646        let levels = [Level::every(&[(N, 4)])];
647        let read = Indexing::Coordinate(EXPERT).read_at(&[M], &levels);
648        assert_eq!(read, Ok(None));
649        let walked = [Level::every(&[(K, 4)])];
650        assert_eq!(Indexing::Run(K).read_at(&[M], &walked), Ok(None));
651    }
652
653    #[test]
654    fn keys_cut_at_two_levels_are_refused() {
655        let levels = [rows(1), Level::every(&[(N, 1)])];
656        let read = Indexing::Coordinate(EXPERT).read_at(&[M, N], &levels);
657        assert_eq!(read, Err(Refusal::IndexKeysCutApart(vec![M, N])));
658    }
659
660    #[test]
661    fn a_coordinate_a_level_cuts_is_refused() {
662        let levels = [rows(1), Level::every(&[(EXPERT, 1)])];
663        let read = Indexing::Coordinate(EXPERT).read_at(&[M], &levels);
664        assert_eq!(read, Err(Refusal::IndexedAxisCut(EXPERT)));
665    }
666
667    /// A run is placed whole: a level cutting its axis at or above the key's would have cut a
668    /// window the run has not placed yet. Below it, the run is what that level walks.
669    #[test]
670    fn a_run_cut_before_it_is_placed_is_refused() {
671        let group = Level::every(&[(EXPERT, 1)]);
672        let above = [rows(2), group.clone()];
673        let below = [group, rows(2)];
674        let run = Indexing::Run(M);
675        assert_eq!(
676            run.read_at(&[EXPERT], &above),
677            Err(Refusal::IndexedAxisCut(M))
678        );
679        assert_eq!(run.read_at(&[EXPERT], &below), Ok(Some(0)));
680    }
681
682    #[test]
683    fn a_coordinate_keyed_by_itself_is_refused() {
684        let spec = TileSpec::direct(&[EXPERT, K, N]);
685        let refused = Indexing::Coordinate(EXPERT).validate(&spec, 1, &[EXPERT]);
686        assert_eq!(refused, Err(Refusal::IndexKeyedByItsAxis(EXPERT)));
687        let unkeyed = Indexing::Coordinate(EXPERT).validate(&spec, 1, &[]);
688        assert_eq!(unkeyed, Err(Refusal::IndexKeyedByItsAxis(EXPERT)));
689    }
690
691    #[test]
692    fn a_paged_operand_is_refused() {
693        let mut spec = TileSpec::direct(&[EXPERT, K, N]);
694        spec.paging = Some(Paging {
695            sequence: EXPERT,
696            position: K,
697            page_size: 4,
698        });
699        let refused = Indexing::Coordinate(EXPERT).validate(&spec, 1, &[M]);
700        assert_eq!(refused, Err(Refusal::IndexOfAStatedLayout));
701    }
702
703    /// The innermost dim may be placed where the operand reads it one value a line: a run along
704    /// the rows of a transposed operand. Read in wider lines, or packed, a line would straddle two
705    /// entries.
706    #[test]
707    fn the_innermost_dim_is_placed_only_one_value_a_line() {
708        let spec = TileSpec::direct(&[N, K]);
709        let run = Indexing::Run(K);
710        assert_eq!(run.validate(&spec, 1, &[M]), Ok(()));
711        assert_eq!(
712            run.validate(&spec, 4, &[M]),
713            Err(Refusal::IndexAlongALine(K))
714        );
715        let packed =
716            TileSpec::direct(&[N, K]).packed(Field::from(cubecl::quant::scheme::QuantValue::Q8S));
717        assert_eq!(
718            run.validate(&packed, 1, &[M]),
719            Err(Refusal::IndexAlongALine(K))
720        );
721    }
722
723    /// A packed weight blocks its columns over two axes, so its projection is not direct; the
724    /// expert is still one dim of its own, which is all a coordinate moves.
725    #[test]
726    fn a_coordinate_places_a_whole_dim_of_a_gathered_operand_and_a_run_does_not() {
727        let block = Axis(4);
728        let spec = TileSpec::new(Projection::new(
729            &[EXPERT, K, N, block],
730            &[
731                PhysicalAxisMap::of(EXPERT),
732                PhysicalAxisMap::of(K),
733                PhysicalAxisMap::disjoint(&[(N, 4), (block, 1)]),
734            ],
735        ));
736        assert_eq!(
737            Indexing::Coordinate(EXPERT).validate(&spec, 1, &[M]),
738            Ok(())
739        );
740        assert_eq!(
741            Indexing::Run(EXPERT).validate(&spec, 1, &[M]),
742            Err(Refusal::IndexOfAStatedLayout)
743        );
744    }
745}