Skip to main content

cubecl_tile/layout/projection/
base.rs

1//! An operand's logical axes mapped onto its buffer's physical axes.
2
3use crate::Addressed;
4use cubecl::zspace::{SmallVec, Tiling};
5
6use crate::{
7    Axis, Composition, DimsBuilder, Divisor, Geometry, Offset, PhysicalAxisMap, Refusal, Space,
8};
9
10/// An operand's logical axes mapped onto its buffer's physical axes.
11#[derive(Clone, PartialEq, Eq, Hash, Debug)]
12pub struct Projection {
13    /// One entry per physical axis, in buffer order.
14    physical: SmallVec<[PhysicalAxisMap; Space::MAX_RANK]>,
15    /// The logical axes, in tile order; the last is the vectorized axis.
16    axes: SmallVec<[Axis; Space::MAX_RANK]>,
17}
18
19impl Projection {
20    /// One logical axis per physical axis at coefficient `1`, in buffer order.
21    pub fn direct(axes: &[Axis]) -> Self {
22        Projection {
23            physical: axes.iter().map(|&a| PhysicalAxisMap::of(a)).collect(),
24            axes: SmallVec::from_slice(axes),
25        }
26    }
27
28    /// [`direct`](Projection::direct) over a space's own axes.
29    pub(crate) fn direct_over(space: &crate::Space) -> Self {
30        Projection::direct(&space.axes().collect::<Vec<_>>())
31    }
32
33    /// [`direct`](Projection::direct) storage-tiled: `labels` name each buffer dim by the axis it
34    /// is a piece of, level-major.
35    pub fn tiled(axes: &[Axis], labels: &[Axis]) -> Self {
36        let physical: Vec<PhysicalAxisMap> =
37            labels.iter().copied().map(PhysicalAxisMap::of).collect();
38        Projection::new(axes, &physical)
39    }
40
41    /// The same operand with each axis's storage fragments merged back into one coordinate.
42    pub(crate) fn untiled(&self) -> Projection {
43        let carried = self.carried_groups();
44        let physical: Vec<PhysicalAxisMap> = carried
45            .iter()
46            .map(|&pa| self.physical[pa].clone())
47            .collect();
48        Projection::new(&self.axes, &physical)
49    }
50
51    /// How many coordinates address this operand: its physical axes with tiled fragments merged.
52    pub fn coordinate_rank(&self) -> usize {
53        self.carried_groups().len()
54    }
55
56    /// The same buffer addressed by physical position: physical axis `p` relabeled `Axis(p)`.
57    pub(crate) fn positional(&self) -> Projection {
58        let carried = self.carried_groups();
59        let axes: Vec<Axis> = (0..carried.len()).map(|p| Axis(p as u8)).collect();
60        let physical: Vec<PhysicalAxisMap> = self
61            .physical
62            .iter()
63            .enumerate()
64            .map(|(pa, map)| {
65                let at = match map.addressed() {
66                    Addressed::Broadcast => carried.iter().position(|&q| q == pa),
67                    Addressed::By(axis) => carried
68                        .iter()
69                        .position(|&q| self.physical[q].addressed() == Addressed::By(axis)),
70                }
71                .expect("collected above");
72                PhysicalAxisMap::of(axes[at])
73            })
74            .collect();
75        Projection::new(&axes, &physical)
76    }
77
78    /// The first physical axis of each coordinate group, in buffer order.
79    fn carried_groups(&self) -> Vec<usize> {
80        assert!(
81            self.is_invertible() || !self.is_tiled(),
82            "Projection: an affine map cannot also be storage-tiled; its physical axes do not \
83             group into coordinates"
84        );
85        let mut carried: Vec<usize> = Vec::new();
86        for (pa, map) in self.physical.iter().enumerate() {
87            match map.addressed() {
88                // Still a buffer axis, so it is its own group.
89                Addressed::Broadcast => carried.push(pa),
90                Addressed::By(axis) => {
91                    if !carried
92                        .iter()
93                        .any(|&q| self.physical[q].addressed() == Addressed::By(axis))
94                    {
95                        carried.push(pa);
96                    }
97                }
98            }
99        }
100        carried
101    }
102
103    /// How many fragments each logical axis is split across; panics past what a [`Tiling`]
104    /// records.
105    pub fn tiling(&self) -> Tiling {
106        let fragments: Vec<usize> = self.axes.iter().map(|&axis| self.fragments(axis)).collect();
107        Tiling::new(&fragments)
108            .unwrap_or_else(|e| panic!("Projection::tiling: {fragments:?} fragments: {e:?}"))
109    }
110
111    /// Whether some axis is split across several physical fragments.
112    pub(crate) fn is_tiled(&self) -> bool {
113        self.axes.iter().any(|&axis| self.fragments(axis) > 1)
114    }
115
116    /// How many physical axes `axis` is split across. A broadcast axis is spread over none, which
117    /// is not a tiling of it: one fragment, the same answer an untiled axis gives.
118    fn fragments(&self, axis: Axis) -> usize {
119        match self.addresses(axis) {
120            false => 1,
121            true => self.carriers(axis).len(),
122        }
123    }
124
125    /// How physical axis `pa` carries `axis`: the positions of its finer carriers, whose extents a
126    /// coordinate is divided by first, and the one whose extent bounds `pa`'s coordinate (`None`
127    /// for the outermost, which keeps the whole quotient).
128    pub(crate) fn carried_by(
129        &self,
130        pa: usize,
131        axis: Axis,
132    ) -> (SmallVec<[usize; Space::MAX_RANK]>, Option<usize>) {
133        let carriers = self.carriers(axis);
134        assert!(
135            carriers.contains(&pa),
136            "Projection::carried_by: physical axis {pa} does not carry {axis:?} (carried by \
137             {carriers:?})"
138        );
139        let finer = carriers.iter().copied().filter(|&q| q > pa).collect();
140        (finer, (carriers[0] != pa).then_some(pa))
141    }
142
143    /// Whether this operand addresses `axis`; a spanned but unaddressed axis is a broadcast.
144    pub(crate) fn addresses(&self, axis: Axis) -> bool {
145        self.physical.iter().any(|m| m.addresses(axis))
146    }
147
148    /// The physical axes carrying `axis`, in buffer order. Panics if there are none.
149    pub(crate) fn carriers(&self, axis: Axis) -> SmallVec<[usize; Space::MAX_RANK]> {
150        let carriers: SmallVec<[usize; Space::MAX_RANK]> = (0..self.physical.len())
151            .filter(|&q| self.physical[q].terms().iter().any(|t| t.axis == axis))
152            .collect();
153        assert!(
154            !carriers.is_empty(),
155            "Projection::carriers: {axis:?} addresses no physical axis of this operand"
156        );
157        carriers
158    }
159
160    /// This operand's scales' projection, one scale per `block`.
161    /// `block` must partition its dim ([`disjoint`](PhysicalAxisMap::disjoint)); panics otherwise.
162    pub fn scales_per(&self, block: Axis) -> Projection {
163        let pa = self
164            .physical
165            .iter()
166            .position(|map| map.terms().iter().any(|term| term.axis == block))
167            .unwrap_or_else(|| {
168                panic!("Projection::scales_per: {block:?} addresses no dim of this operand")
169            });
170        let map = &self.physical[pa];
171        let terms = map.terms();
172        let at = terms.iter().position(|term| term.axis == block).unwrap();
173        assert!(
174            map.composition() == Composition::Disjoint && at + 1 < terms.len(),
175            "Projection::scales_per: {block:?} must partition its dim with an axis inside it \
176             (PhysicalAxisMap::disjoint(&[({block:?}, block), (inside, 1)]))"
177        );
178        let radix = terms[at].scale.get();
179        let kept: Vec<(Axis, usize)> = terms[..=at]
180            .iter()
181            .map(|term| (term.axis, term.scale.get() / radix))
182            .collect();
183        let omitted: Vec<Axis> = terms[at + 1..].iter().map(|term| term.axis).collect();
184        let mut physical = self.physical.clone();
185        physical[pa] = match kept.as_slice() {
186            [(axis, 1)] => PhysicalAxisMap::of(*axis),
187            _ => PhysicalAxisMap::disjoint(&kept),
188        };
189        Projection {
190            physical,
191            axes: self
192                .axes
193                .iter()
194                .copied()
195                .filter(|axis| !omitted.contains(axis))
196                .collect(),
197        }
198    }
199
200    /// `axes` in the tile's logical order, `physical` one per physical axis in buffer order.
201    pub fn new(axes: &[Axis], physical: &[PhysicalAxisMap]) -> Self {
202        Projection::of_maps(axes, physical.iter().cloned().collect())
203    }
204
205    /// [`new`](Projection::new) over maps the caller built for it, taken as they are.
206    pub(crate) fn of_maps(
207        axes: &[Axis],
208        physical: SmallVec<[PhysicalAxisMap; Space::MAX_RANK]>,
209    ) -> Self {
210        Projection {
211            physical,
212            axes: SmallVec::from_slice(axes),
213        }
214    }
215
216    /// Whether this is the [`direct`](Projection::direct) mapping.
217    pub(crate) fn is_direct(&self) -> bool {
218        self.physical.len() == self.axes.len()
219            && self
220                .physical
221                .iter()
222                .zip(self.axes.iter())
223                .all(|(map, &axis)| map.is_identity(axis))
224    }
225
226    /// The logical axes carried by the trailing physical axes, up to the innermost broadcast.
227    pub(crate) fn dense_labels(&self) -> Vec<Axis> {
228        let mut labels: Vec<Axis> = self
229            .physical
230            .iter()
231            .rev()
232            .map_while(|map| match map.addressed() {
233                Addressed::By(axis) => Some(axis),
234                Addressed::Broadcast => None,
235            })
236            .collect();
237        labels.reverse();
238        labels
239    }
240
241    pub fn physical_rank(&self) -> usize {
242        self.physical.len()
243    }
244
245    pub(crate) fn logical_rank(&self) -> usize {
246        self.axes.len()
247    }
248
249    pub(crate) fn logical_axes(&self) -> &[Axis] {
250        &self.axes
251    }
252
253    /// `axis`'s index in the logical order, which is the order a coordinate comes in.
254    pub fn position(&self, axis: Axis) -> usize {
255        self.axes
256            .iter()
257            .position(|&a| a == axis)
258            .expect("Projection::position: axis not spanned by this operand")
259    }
260
261    pub(crate) fn physical_axis(&self, pa: usize) -> &PhysicalAxisMap {
262        &self.physical[pa]
263    }
264
265    /// `axis`'s coefficient along physical axis `pa`, `0` when it does not address it.
266    pub fn scale(&self, pa: usize, axis: Axis) -> usize {
267        self.physical[pa].scale(axis)
268    }
269
270    /// The offset along physical axis `pa`.
271    pub fn offset(&self, pa: usize) -> Offset {
272        self.physical[pa].offset()
273    }
274
275    /// Physical axis `pa`'s divisor, [`Static(1)`](Divisor::Static) for integer mappings.
276    pub(crate) fn divisor(&self, pa: usize) -> Divisor {
277        self.physical[pa].divisor()
278    }
279
280    /// Whether any physical axis is [rational](PhysicalAxisMap::is_rational).
281    pub(crate) fn is_rational(&self) -> bool {
282        self.physical.iter().any(|m| m.is_rational())
283    }
284
285    /// Whether any physical axis has a negative or dynamic offset.
286    pub(crate) fn may_underflow(&self) -> bool {
287        self.physical.iter().any(|m| match m.offset() {
288            Offset::Static(o) => o < 0,
289            Offset::Dynamic => true,
290        })
291    }
292
293    /// How many elements of physical axis `pa` a region covers: `1 + Σ (extent - 1) * scale`.
294    /// Rational and dynamic axes report the widest span their bounds admit.
295    pub(crate) fn span(&self, pa: usize, extent_of: impl Fn(Axis) -> usize) -> usize {
296        let map = &self.physical[pa];
297        let field: usize = map
298            .terms()
299            .iter()
300            .map(|t| (extent_of(t.axis) - 1) * t.scale.bound())
301            .sum();
302        1 + field.div_ceil(map.divisor().bound())
303    }
304
305    /// `Disjoint` if every physical axis is partitioned by its axes, else `Overlapping`.
306    pub(crate) fn composition(&self) -> Composition {
307        match self
308            .physical
309            .iter()
310            .all(|m| m.composition() == Composition::Disjoint && m.divisor().is_unit())
311        {
312            true => Composition::Disjoint,
313            false => Composition::Overlapping,
314        }
315    }
316
317    /// Panics if a disjoint axis's coefficients are not the products of its finer extents.
318    pub(crate) fn validate_composition(&self, extent_of: impl Fn(Axis) -> usize) {
319        for (pa, map) in self.physical.iter().enumerate() {
320            let radices = map.claimed_radices();
321            if radices.is_empty() {
322                continue;
323            }
324            assert!(
325                map.offset() == Offset::Static(0),
326                "Projection: physical axis {pa} claims to be partitioned by its axes, but carries \
327                 the offset {:?}; a partition starts at 0",
328                map.offset()
329            );
330            // Only extents below the coarsest are read, keeping a `Dynamic` top axis out.
331            let mut expected = 1;
332            for (i, (axis, coefficient)) in radices.iter().enumerate().rev() {
333                assert!(
334                    *coefficient == expected,
335                    "Projection: physical axis {pa} claims its axes partition it, so {axis:?} \
336                     must step by {expected} (the extents below it); it steps by {coefficient}"
337                );
338                if i == 0 {
339                    break;
340                }
341                expected *= extent_of(*axis);
342            }
343        }
344    }
345
346    pub(crate) fn is_invertible(&self) -> bool {
347        self.physical.iter().all(|m| m.identity_axis().is_some())
348    }
349
350    /// Why a gathered projection cannot be served at `vector_size`, if it cannot.
351    pub(crate) fn validate(&self, vector_size: usize) -> Result<(), Refusal> {
352        if self.is_invertible() {
353            return Ok(());
354        }
355        if self.physical.is_empty() || self.axes.is_empty() {
356            return Err(Refusal::NoCoordinate);
357        }
358        // The innermost physical axis is addressed in lines, so it must step by one element.
359        if vector_size > 1 {
360            let innermost = self.axes[self.axes.len() - 1];
361            let last = &self.physical[self.physical.len() - 1];
362            let steps_by_lines = match last.composition() {
363                Composition::Disjoint => last.terms().last().map(|t| t.axis) == Some(innermost),
364                Composition::Overlapping => last.is_identity(innermost),
365            };
366            if !steps_by_lines {
367                return Err(Refusal::GatherInnermostNotInLines);
368            }
369        }
370        for &axis in self.axes.iter() {
371            // A gathered operand must be untiled: each addressed axis maps to one physical axis.
372            let count = self.physical.iter().filter(|m| m.addresses(axis)).count();
373            if count > 1 {
374                return Err(Refusal::GatherAxisAddressedTwice(axis));
375            }
376        }
377        Ok(())
378    }
379
380    /// Start writing a projection one buffer dim at a time, in buffer order ([`DimsBuilder`]).
381    pub fn dims() -> DimsBuilder {
382        DimsBuilder::new()
383    }
384}
385
386// Queries against a buffer's runtime `Geometry`.
387impl Projection {
388    /// Whether the buffer's dims can be addressed without two positions landing on one cell.
389    pub fn is_addressable(&self, geometry: &Geometry) -> bool {
390        if self.composition() == Composition::Overlapping {
391            return true;
392        }
393        geometry.addressing_dims_by_stride().windows(2).all(|pair| {
394            let (_, (_, stride)) = pair[0];
395            let (_, (finer_extent, finer_stride)) = pair[1];
396            stride >= finer_extent * finer_stride
397        })
398    }
399}
400
401// Indices into the runtime coefficient (unsigned) and offset (signed) arrays: physical axis
402// major, term order within, each axis's divisor last.
403impl Projection {
404    /// Term `t` of physical axis `pa` in the runtime coefficient array, `None` if static.
405    pub(crate) fn dynamic_scale_index(&self, pa: usize, t: usize) -> Option<usize> {
406        if !self.physical_axis(pa).terms()[t].scale.is_dynamic() {
407            return None;
408        }
409        let within = self.physical_axis(pa).terms()[..t]
410            .iter()
411            .filter(|term| term.scale.is_dynamic())
412            .count();
413        Some(self.coefficient_base(pa) + within)
414    }
415
416    /// Physical axis `pa`'s divisor in the runtime coefficient array, `None` if static.
417    pub(crate) fn dynamic_divisor_index(&self, pa: usize) -> Option<usize> {
418        if !self.physical_axis(pa).divisor().is_dynamic() {
419            return None;
420        }
421        Some(self.coefficient_base(pa) + self.physical_axis(pa).dynamic_scale_count())
422    }
423
424    /// Where physical axis `pa`'s entries start in the runtime coefficient array.
425    fn coefficient_base(&self, pa: usize) -> usize {
426        (0..pa)
427            .map(|i| self.physical_axis(i))
428            .map(|m| m.dynamic_scale_count() + m.divisor().is_dynamic() as usize)
429            .sum()
430    }
431
432    /// The length of the runtime coefficient array.
433    pub(crate) fn dynamic_coefficient_count(&self) -> usize {
434        self.coefficient_base(self.physical_rank())
435    }
436
437    /// Physical axis `pa`'s offset in the runtime offset array, `None` if static.
438    pub(crate) fn dynamic_offset_index(&self, pa: usize) -> Option<usize> {
439        if !self.physical_axis(pa).offset().is_dynamic() {
440            return None;
441        }
442        Some(
443            (0..pa)
444                .map(|i| self.physical_axis(i))
445                .filter(|m| m.offset().is_dynamic())
446                .count(),
447        )
448    }
449
450    /// The length of the runtime offset array.
451    pub(crate) fn dynamic_offset_count(&self) -> usize {
452        self.physical
453            .iter()
454            .filter(|m| m.offset().is_dynamic())
455            .count()
456    }
457
458    /// Whether any coefficient is only known at runtime.
459    pub(crate) fn has_dynamic_scales(&self) -> bool {
460        self.physical.iter().any(|m| m.has_dynamic_scale())
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use crate::Scale;
468
469    /// Block scales omit the in-block axis and count the split dim in blocks.
470    #[test]
471    fn scales_per_block_omit_the_axis_inside_it() {
472        let values = Projection::new(
473            &[M, KB, KI],
474            &[
475                PhysicalAxisMap::of(M),
476                PhysicalAxisMap::disjoint(&[(KB, 32), (KI, 1)]),
477            ],
478        );
479        let scales = values.scales_per(KB);
480        assert_eq!(scales.logical_axes(), &[M, KB]);
481        assert_eq!(scales.physical_rank(), 2);
482        assert!(scales.physical[1].is_identity(KB));
483    }
484
485    /// Scales per a middle axis keep the coarser axis, rescaled to count blocks.
486    #[test]
487    fn scales_per_a_middle_axis_keep_what_is_above_it() {
488        let values = Projection::new(
489            &[M, KO, KB, KI],
490            &[
491                PhysicalAxisMap::of(M),
492                PhysicalAxisMap::disjoint(&[(KO, 64), (KB, 32), (KI, 1)]),
493            ],
494        );
495        let per_block = values.scales_per(KB);
496        assert_eq!(per_block.logical_axes(), &[M, KO, KB]);
497        assert_eq!(per_block.scale(1, KO), 2);
498        assert_eq!(per_block.scale(1, KB), 1);
499        let per_outer = values.scales_per(KO);
500        assert_eq!(per_outer.logical_axes(), &[M, KO]);
501        assert!(per_outer.physical[1].is_identity(KO));
502    }
503
504    #[test]
505    #[should_panic(expected = "must partition its dim with an axis inside it")]
506    fn scales_per_an_unsplit_axis_are_refused() {
507        Projection::direct(&[M, KB]).scales_per(KB);
508    }
509
510    const A: Axis = Axis(0);
511    const B: Axis = Axis(1);
512    const R: Axis = Axis(2);
513
514    const M: Axis = Axis(3);
515    const KB: Axis = Axis(4);
516    const KI: Axis = Axis(5);
517    const KO: Axis = Axis(6);
518
519    #[test]
520    fn direct_is_direct() {
521        let p = Projection::direct(&[A, B]);
522        assert!(p.is_direct());
523        assert_eq!(p.physical_rank(), 2);
524        assert_eq!(p.logical_axes(), &[A, B]);
525    }
526
527    #[test]
528    fn direct_span_is_the_edge() {
529        let p = Projection::direct(&[A, B]);
530        assert_eq!(p.span(0, |_| 8), 8);
531    }
532
533    #[test]
534    fn affine_span_is_the_receptive_field() {
535        let p = Projection::new(
536            &[A, R, B],
537            &[
538                PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
539                PhysicalAxisMap::of(B),
540            ],
541        );
542        assert!(!p.is_direct());
543        assert_eq!(p.scale(0, A), 2);
544        assert_eq!(p.scale(0, R), 3);
545        assert_eq!(p.scale(0, B), 0);
546        // 1 + (4-1)*2 + (3-1)*3 = 13
547        assert_eq!(p.span(0, |a| if a == A { 4 } else { 3 }), 13);
548        let q = Projection::new(
549            &[A, R, B],
550            &[
551                PhysicalAxisMap::affine(&[(A, 1), (R, 1)]),
552                PhysicalAxisMap::of(B),
553            ],
554        );
555        assert_eq!(q.span(0, |a| if a == A { 4 } else { 1 }), 4);
556    }
557
558    #[test]
559    fn innermost_axis_rides_no_coarser_physical_axis() {
560        let refused = Projection::new(
561            &[A, B],
562            &[
563                PhysicalAxisMap::affine(&[(A, 1), (B, 2)]),
564                PhysicalAxisMap::of(B),
565            ],
566        )
567        .validate(4);
568        assert_eq!(refused, Err(Refusal::GatherAxisAddressedTwice(B)));
569    }
570
571    #[test]
572    fn innermost_must_be_identity() {
573        let refused = Projection::new(
574            &[A, R],
575            &[PhysicalAxisMap::of(A), PhysicalAxisMap::affine(&[(R, 2)])],
576        )
577        .validate(4);
578        assert_eq!(refused, Err(Refusal::GatherInnermostNotInLines));
579    }
580
581    #[test]
582    fn innermost_may_gather_when_scalar() {
583        Projection::new(
584            &[A, R],
585            &[PhysicalAxisMap::of(A), PhysicalAxisMap::affine(&[(R, 2)])],
586        )
587        .validate(1)
588        .unwrap();
589    }
590
591    #[test]
592    fn a_gather_still_rejects_tiled_storage_when_scalar() {
593        let refused = Projection::new(
594            &[A, R, B],
595            &[
596                PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
597                PhysicalAxisMap::of(B),
598                PhysicalAxisMap::of(B),
599            ],
600        )
601        .validate(1);
602        assert_eq!(refused, Err(Refusal::GatherAxisAddressedTwice(B)));
603    }
604
605    #[test]
606    fn a_gather_rejects_tiled_storage() {
607        let refused = Projection::new(
608            &[A, R, B],
609            &[
610                PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
611                PhysicalAxisMap::of(B),
612                PhysicalAxisMap::of(B),
613            ],
614        )
615        .validate(4);
616        assert_eq!(refused, Err(Refusal::GatherAxisAddressedTwice(B)));
617    }
618
619    #[test]
620    fn an_axis_addressing_nothing_is_a_broadcast() {
621        let p = Projection::new(
622            &[A, R, B],
623            &[PhysicalAxisMap::affine(&[(A, 2)]), PhysicalAxisMap::of(B)],
624        );
625        p.validate(4).unwrap();
626        assert!(!p.addresses(R));
627        assert!(p.addresses(A) && p.addresses(B));
628    }
629
630    #[test]
631    fn tiled_is_not_a_gather() {
632        let p = Projection::tiled(&[A, B], &[A, B, A, B]);
633        assert!(!p.is_direct());
634        assert!(p.is_tiled());
635        assert!(p.is_invertible());
636        p.validate(4).unwrap();
637    }
638
639    #[test]
640    fn a_gather_is_not_tiled() {
641        let p = Projection::new(
642            &[A, R, B],
643            &[
644                PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
645                PhysicalAxisMap::of(B),
646            ],
647        );
648        assert!(!p.is_tiled());
649        p.validate(4).unwrap();
650    }
651
652    #[test]
653    fn a_tiled_spec_matches_its_buffer() {
654        use crate::TileSpec;
655
656        const BATCH: Axis = Axis(3);
657        let spec = TileSpec::new(Projection::new(
658            &[BATCH, A, B],
659            &[BATCH, A, B, A, B].map(PhysicalAxisMap::of),
660        ));
661
662        assert_eq!(spec.projection.physical_rank(), 5);
663        let [p0, p1, p2] = [Axis(0), Axis(1), Axis(2)];
664        assert_eq!(
665            spec.projection.positional(),
666            Projection::tiled(&[p0, p1, p2], &[p0, p1, p2, p1, p2])
667        );
668        assert_eq!(
669            Projection::tiled(&[BATCH, A, B], &[BATCH, A, B, A, B]),
670            spec.projection
671        );
672        assert_eq!(spec.projection.tiling(), Tiling::new(&[1, 2, 2]).unwrap());
673    }
674
675    #[test]
676    fn positional_of_a_plain_operand_is_the_identity() {
677        assert_eq!(
678            Projection::direct(&[B, A]).positional(),
679            Projection::direct(&[Axis(0), Axis(1)])
680        );
681    }
682
683    #[test]
684    fn positional_of_a_gather_drops_the_affine_terms() {
685        let p = Projection::new(
686            &[A, R, B],
687            &[
688                PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
689                PhysicalAxisMap::of(B),
690            ],
691        );
692        assert_eq!(p.positional(), Projection::direct(&[Axis(0), Axis(1)]));
693    }
694
695    #[test]
696    fn a_ragged_tiling_orders_level_major() {
697        let p = Projection::tiled(&[A, B], &[A, B, A, B, A]);
698        assert_eq!(p.physical_rank(), 5);
699        // `[A0, B0, A1, B1, A2]`.
700        assert_eq!(p.carriers(A).as_slice(), &[0, 2, 4]);
701        assert_eq!(p.carriers(B).as_slice(), &[1, 3]);
702        assert_eq!(p.carried_by(0, A), (SmallVec::from_slice(&[2, 4]), None));
703        assert_eq!(p.carried_by(2, A), (SmallVec::from_slice(&[4]), Some(2)));
704        assert_eq!(p.carried_by(4, A), (SmallVec::new(), Some(4)));
705        assert_eq!(p.carried_by(1, B), (SmallVec::from_slice(&[3]), None));
706        assert!(p.is_invertible());
707        p.validate(4).unwrap();
708    }
709
710    /// Weighing each carrier's coordinate by the extents finer than it gives back the coordinate.
711    #[test]
712    fn carried_coordinates_weigh_back_to_the_coordinate() {
713        let shape = [3usize, 8];
714        let p = Projection::new(&[A], &[PhysicalAxisMap::of(A), PhysicalAxisMap::of(A)]);
715        let block = |pa| {
716            p.carried_by(pa, A)
717                .0
718                .iter()
719                .map(|&q| shape[q])
720                .product::<usize>()
721        };
722
723        for coord in [0usize, 1, 7, 8, 9, 19, 23] {
724            let physical = |pa| match p.carried_by(pa, A).1 {
725                Some(m) => (coord / block(pa)) % shape[m],
726                None => coord / block(pa),
727            };
728            assert_eq!(physical(0) * block(0) + physical(1) * block(1), coord);
729        }
730    }
731
732    /// `tiling` reads back the piece counts of the dims `tiled` was given.
733    #[test]
734    fn tiling_reads_back_the_counts_tiled_was_built_from() {
735        for fragments in [[1, 1, 1], [3, 3, 3], [1, 2, 2], [3, 1, 2]] {
736            let axes: Vec<Axis> = (0..3).map(|p| Axis(p as u8)).collect();
737            let labels = crate::StoragePartitioning::level_major(&axes, &fragments);
738            let p = Projection::tiled(&axes, &labels);
739            assert_eq!(p.tiling(), Tiling::new(&fragments).unwrap());
740            assert_eq!(p.physical_rank(), fragments.iter().sum::<usize>());
741            assert_eq!(p.is_tiled(), fragments.iter().any(|&n| n > 1));
742        }
743    }
744
745    #[test]
746    fn is_invertible_false_for_a_stencil_map() {
747        let p = Projection::new(
748            &[A, R, B],
749            &[
750                PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
751                PhysicalAxisMap::of(B),
752            ],
753        );
754        assert!(!p.is_invertible());
755        assert!(Projection::direct(&[A, B]).is_invertible());
756
757        let with_offset = Projection::new(
758            &[A, B],
759            &[
760                PhysicalAxisMap::affine(&[(A, 1)]).shifted(-1),
761                PhysicalAxisMap::of(B),
762            ],
763        );
764        assert!(!with_offset.is_invertible());
765        assert_eq!(with_offset.offset(0), Offset::Static(-1));
766        assert_eq!(with_offset.offset(1), Offset::Static(0));
767    }
768
769    #[test]
770    fn may_underflow_tracks_negative_offsets_only() {
771        let padded = Projection::new(
772            &[A, R, B],
773            &[
774                PhysicalAxisMap::affine(&[(A, 2), (R, 1)]).shifted(-1),
775                PhysicalAxisMap::of(B),
776            ],
777        );
778        assert!(padded.may_underflow());
779
780        let shifted = Projection::new(
781            &[A, R, B],
782            &[
783                PhysicalAxisMap::affine(&[(A, 2), (R, 1)]).shifted(1),
784                PhysicalAxisMap::of(B),
785            ],
786        );
787        assert!(!shifted.may_underflow());
788        assert!(!Projection::direct(&[A, B]).may_underflow());
789
790        let dynamic_offset = Projection::new(
791            &[A, R, B],
792            &[
793                PhysicalAxisMap::affine(&[(A, 2), (R, 1)]).shifted(Offset::Dynamic),
794                PhysicalAxisMap::of(B),
795            ],
796        );
797        assert!(dynamic_offset.may_underflow());
798    }
799
800    /// Physical axis major, term order within; offsets indexed apart from coefficients.
801    #[test]
802    fn dynamic_terms_index_in_projection_order() {
803        let p = Projection::new(
804            &[A, R, B],
805            &[
806                PhysicalAxisMap::scaled(&[
807                    (A, Scale::Dynamic { max: 2 }),
808                    (R, Scale::Dynamic { max: 2 }),
809                ]),
810                PhysicalAxisMap::of(B),
811            ],
812        );
813        assert_eq!(p.dynamic_coefficient_count(), 2);
814        assert_eq!(p.dynamic_offset_count(), 0);
815        assert_eq!(p.dynamic_scale_index(0, 0), Some(0));
816        assert_eq!(p.dynamic_scale_index(0, 1), Some(1));
817        assert_eq!(p.dynamic_offset_index(0), None);
818        assert_eq!(p.dynamic_scale_index(1, 0), None);
819
820        let mixed = Projection::new(
821            &[A, R, B],
822            &[
823                PhysicalAxisMap::scaled(&[(A, Scale::Static(2)), (R, Scale::Dynamic { max: 2 })]),
824                PhysicalAxisMap::of(B),
825            ],
826        );
827        assert_eq!(mixed.dynamic_coefficient_count(), 1);
828        assert_eq!(mixed.dynamic_scale_index(0, 0), None);
829        assert_eq!(mixed.dynamic_scale_index(0, 1), Some(0));
830        assert_eq!(mixed.dynamic_offset_index(0), None);
831
832        let with_dynamic_offset = Projection::new(
833            &[A, R, B],
834            &[
835                PhysicalAxisMap::scaled(&[(A, Scale::Dynamic { max: 2 }), (R, Scale::Static(1))])
836                    .shifted(Offset::Dynamic),
837                PhysicalAxisMap::of(B),
838            ],
839        );
840        assert_eq!(with_dynamic_offset.dynamic_coefficient_count(), 1);
841        assert_eq!(with_dynamic_offset.dynamic_offset_count(), 1);
842        assert_eq!(with_dynamic_offset.dynamic_scale_index(0, 0), Some(0));
843        assert_eq!(with_dynamic_offset.dynamic_scale_index(0, 1), None);
844        assert_eq!(with_dynamic_offset.dynamic_offset_index(0), Some(0));
845        assert_eq!(with_dynamic_offset.dynamic_offset_index(1), None);
846
847        let two_offsets = Projection::new(
848            &[A, R, B],
849            &[
850                PhysicalAxisMap::affine(&[(A, 2), (R, 1)]).shifted(Offset::Dynamic),
851                PhysicalAxisMap::scaled(&[(B, Scale::Dynamic { max: 2 })]).shifted(Offset::Dynamic),
852            ],
853        );
854        assert_eq!(two_offsets.dynamic_offset_index(0), Some(0));
855        assert_eq!(two_offsets.dynamic_offset_index(1), Some(1));
856        assert_eq!(two_offsets.dynamic_scale_index(1, 0), Some(0));
857    }
858
859    #[test]
860    fn a_dynamic_coefficient_is_not_invertible() {
861        let p = Projection::new(
862            &[A, B],
863            &[
864                PhysicalAxisMap::scaled(&[(A, Scale::Dynamic { max: 2 })]),
865                PhysicalAxisMap::of(B),
866            ],
867        );
868        assert!(!p.is_invertible());
869        p.validate(4).unwrap();
870    }
871
872    #[test]
873    fn rational_projection_properties() {
874        let p = Projection::new(
875            &[A, R, B],
876            &[
877                PhysicalAxisMap::affine(&[(A, 100), (R, 133)])
878                    .shifted(-50)
879                    .over(133),
880                PhysicalAxisMap::of(B),
881            ],
882        );
883        assert!(p.is_rational());
884        assert_eq!(p.divisor(0), Divisor::Static(133));
885        assert_eq!(p.divisor(1), Divisor::Static(1));
886        assert_eq!(p.span(1, |_| 8), 8);
887    }
888
889    #[test]
890    fn rational_axis_conservative_span() {
891        let p = Projection::new(
892            &[A, R, B],
893            &[
894                PhysicalAxisMap::affine(&[(A, 100), (R, 133)])
895                    .shifted(-50)
896                    .over(133),
897                PhysicalAxisMap::of(B),
898            ],
899        );
900        // field = 300, span = 1 + ⌈300 / 133⌉ = 4
901        assert_eq!(p.span(0, |a| if a == A { 4 } else { 1 }), 4);
902        // field = 433, span = 1 + ⌈433 / 133⌉ = 5
903        assert_eq!(p.span(0, |a| if a == A { 4 } else { 2 }), 5);
904    }
905
906    #[test]
907    fn a_dynamic_divisor_spans_against_its_min() {
908        let bounded = |min| {
909            Projection::new(
910                &[A, R, B],
911                &[
912                    PhysicalAxisMap::affine(&[(A, 100), (R, 133)])
913                        .shifted(-50)
914                        .over(Divisor::Dynamic { min }),
915                    PhysicalAxisMap::of(B),
916                ],
917            )
918        };
919        // field = (4-1)*100 + (1-1)*133 = 300, so span = 1 + ⌈300 / min⌉.
920        let extents = |a| if a == A { 4 } else { 1 };
921        assert_eq!(bounded(133).span(0, extents), 4);
922        assert_eq!(bounded(100).span(0, extents), 4);
923        assert_eq!(bounded(50).span(0, extents), 7);
924        let exact = |d| {
925            Projection::new(
926                &[A, R, B],
927                &[
928                    PhysicalAxisMap::affine(&[(A, 100), (R, 133)])
929                        .shifted(-50)
930                        .over(d),
931                    PhysicalAxisMap::of(B),
932                ],
933            )
934            .span(0, extents)
935        };
936        for d in 50..160 {
937            assert!(exact(d) <= bounded(50).span(0, extents));
938        }
939    }
940
941    #[test]
942    fn a_dynamic_coefficient_spans_against_its_max() {
943        let p = Projection::new(
944            &[A, R, B],
945            &[
946                PhysicalAxisMap::scaled(&[(A, Scale::Dynamic { max: 3 }), (R, Scale::Static(1))]),
947                PhysicalAxisMap::of(B),
948            ],
949        );
950        // field = (4-1)*3 + (2-1)*1 = 10, so span = 11.
951        assert_eq!(p.span(0, |a| if a == A { 4 } else { 2 }), 11);
952        let at_max = Projection::new(
953            &[A, R, B],
954            &[
955                PhysicalAxisMap::affine(&[(A, 3), (R, 1)]),
956                PhysicalAxisMap::of(B),
957            ],
958        );
959        assert_eq!(at_max.span(0, |a| if a == A { 4 } else { 2 }), 11);
960    }
961
962    #[test]
963    fn dynamic_divisor_indexing() {
964        let p = Projection::new(
965            &[A, R, B],
966            &[
967                PhysicalAxisMap::scaled(&[(A, Scale::Dynamic { max: 2 }), (R, Scale::Static(1))])
968                    .over(Divisor::Dynamic { min: 4 }),
969                PhysicalAxisMap::scaled(&[(B, Scale::Dynamic { max: 2 })]),
970            ],
971        );
972        assert_eq!(p.dynamic_coefficient_count(), 3);
973        assert_eq!(p.dynamic_scale_index(0, 0), Some(0));
974        assert_eq!(p.dynamic_scale_index(0, 1), None);
975        assert_eq!(p.dynamic_divisor_index(0), Some(1));
976        assert_eq!(p.dynamic_scale_index(1, 0), Some(2));
977    }
978
979    #[test]
980    fn a_partition_of_higher_logical_rank_is_still_dense() {
981        let blocked = Projection::new(
982            &[A, B, R],
983            &[
984                PhysicalAxisMap::of(A),
985                PhysicalAxisMap::disjoint(&[(B, 32), (R, 1)]),
986            ],
987        );
988        assert_eq!(blocked.composition(), Composition::Disjoint);
989        assert!(!blocked.is_direct());
990
991        let gathered = Projection::new(
992            &[A, B, R],
993            &[
994                PhysicalAxisMap::of(A),
995                PhysicalAxisMap::affine(&[(B, 32), (R, 1)]),
996            ],
997        );
998        assert_eq!(gathered.composition(), Composition::Overlapping);
999    }
1000
1001    #[test]
1002    fn a_partition_whose_radices_match_the_extents_is_accepted() {
1003        let extents = |axis| match axis {
1004            x if x == B => 4,
1005            x if x == R => 32,
1006            _ => 8,
1007        };
1008        Projection::new(
1009            &[A, B, R],
1010            &[
1011                PhysicalAxisMap::of(A),
1012                PhysicalAxisMap::disjoint(&[(B, 32), (R, 1)]),
1013            ],
1014        )
1015        .validate_composition(extents);
1016    }
1017
1018    #[test]
1019    #[should_panic(expected = "must step by 32")]
1020    fn a_partition_whose_radices_contradict_the_extents_is_refused() {
1021        let extents = |axis| match axis {
1022            x if x == B => 4,
1023            x if x == R => 32,
1024            _ => 8,
1025        };
1026        Projection::new(
1027            &[A, B, R],
1028            &[
1029                PhysicalAxisMap::of(A),
1030                PhysicalAxisMap::disjoint(&[(B, 16), (R, 1)]),
1031            ],
1032        )
1033        .validate_composition(extents);
1034    }
1035}
1036
1037/// The questions a launch asks once it holds the buffer's real extents and strides.
1038#[cfg(test)]
1039mod geometry_tests {
1040    use super::*;
1041
1042    const K: Axis = Axis(0);
1043    const N: Axis = Axis(1);
1044    const B: Axis = Axis(2);
1045    const H: Axis = Axis(3);
1046    const S: Axis = Axis(4);
1047    const D: Axis = Axis(5);
1048    const M: Axis = Axis(6);
1049    const C: Axis = Axis(7);
1050
1051    fn geometry(dims: &[(usize, usize)]) -> Geometry {
1052        Geometry::new(dims)
1053    }
1054
1055    #[test]
1056    fn padding_is_addressable() {
1057        let p = Projection::direct(&[K, N]);
1058        let padded = geometry(&[(4096, 11136), (11008, 1)]);
1059
1060        assert!(p.is_addressable(&padded));
1061    }
1062
1063    #[test]
1064    fn a_narrowed_row_aliases() {
1065        let p = Projection::direct(&[K, N]);
1066        let narrowed = geometry(&[(4096, 4096), (11008, 1)]);
1067
1068        assert!(!p.is_addressable(&narrowed));
1069    }
1070
1071    #[test]
1072    fn an_in_place_cache_is_addressable_padded_or_permuted() {
1073        let p = Projection::direct(&[B, H, S, D]);
1074
1075        let padded = geometry(&[(2, 8 * 4096 * 136), (8, 4096 * 136), (4096, 136), (128, 1)]);
1076        assert!(p.is_addressable(&padded));
1077
1078        let permuted = geometry(&[(2, 8 * 4096 * 128), (8, 128), (4096, 8 * 128), (128, 1)]);
1079        assert!(p.is_addressable(&permuted));
1080    }
1081
1082    #[test]
1083    fn a_window_is_exempt_from_the_aliasing_check() {
1084        let p = Projection::dims()
1085            .dim(B)
1086            .dim(crate::layout::projection::dims_builder::stencil(&[(M, 2), (K, 1)]).pad(1))
1087            .dim(C)
1088            .build();
1089        let g = geometry(&[(8, 64 * 32), (64, 32), (32, 1)]);
1090
1091        assert_eq!(p.composition(), Composition::Overlapping);
1092        assert!(p.is_addressable(&g));
1093    }
1094}