1use cubecl::prelude::*;
13
14use crate::stage::pipeline::payload::base::StageOperand;
15use crate::*;
16
17#[derive(Clone, PartialEq, Eq, Hash, Debug)]
20pub enum Indexing {
21 Coordinate(Axis),
28 Run(Axis),
34}
35
36impl Indexing {
37 pub fn axis(&self) -> Axis {
39 match self {
40 Indexing::Coordinate(axis) | Indexing::Run(axis) => *axis,
41 }
42 }
43
44 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 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 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
130pub(crate) fn placed_dim(projection: &Projection, axis: Axis) -> Option<usize> {
132 Witness::new(projection, axis).map(|witness| witness.dim())
133}
134
135#[allow(dead_code)]
139#[derive(CubeType, Clone)]
140#[expand(derive(Clone))]
141pub(crate) struct IndexTable {
142 entries: Box<[u32]>,
143 strides: Coords<u32>,
145 #[cube(comptime)]
146 pub(crate) indexing: Indexing,
147 #[cube(comptime)]
149 pub(crate) keys: Vec<Axis>,
150 #[cube(comptime)]
152 pub(crate) placed: bool,
153 in_range: bool,
156}
157
158#[cube]
159impl IndexTable {
160 #[allow(dead_code)] 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 #[allow(dead_code)] 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 #[allow(dead_code)] 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 #[allow(dead_code)] 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 #[allow(dead_code)] 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#[cube]
228fn nonempty(from: u32, until: u32) -> bool {
229 from < until
230}
231
232#[cube]
234fn no_table() -> bool {
235 true
236}
237
238#[cube]
240fn first_entry() -> u32 {
241 0u32
242}
243
244impl Tile {
245 pub fn entry_in_range(&self) -> bool {
251 cubecl::unexpanded!()
252 }
253
254 pub(crate) fn indexed_at(self, _step: &Step) -> Tile {
258 cubecl::unexpanded!()
259 }
260
261 #[allow(dead_code)] pub(crate) fn with_index(self, _table: IndexTable) -> Tile {
264 cubecl::unexpanded!()
265 }
266
267 #[allow(dead_code)] 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 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 #[allow(dead_code)] 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 #[allow(dead_code)] 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 #[allow(dead_code)] 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 #[allow(dead_code)] 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 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 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#[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#[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 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 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 #[allow(dead_code)] 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 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 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 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 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 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 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 #[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 #[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 #[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 #[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}