Skip to main content

cubecl_tile/space/
share.rs

1//! What the hardware instances hold of a tile's cells once a level has been distributed.
2
3use cubecl::prelude::*;
4
5use crate::{Axis, Carrier, ComputeScope, Count, Coverage, Level, Monoid, Space};
6
7/// What the plane's units each hold of a tile's cells once a units level is distributed.
8#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
9pub enum UnitShare {
10    /// Every unit repeats the same work over the same whole cells.
11    Repeated,
12    /// Each unit carries cells of its own and reduces nothing.
13    Whole,
14    /// Every unit holds a partial of the same cell.
15    Plane,
16    /// Groups of units share one cell each; `unit_bits` are the unit-index bits the reduced axes
17    /// occupy.
18    Group { unit_bits: usize },
19}
20
21impl UnitShare {
22    /// What `level` makes the plane's units to the cells of an operand spanning `spanned`.
23    pub(crate) fn new(level: &Level, spanned: &Space) -> UnitShare {
24        if level.coverage() != Coverage::Distribute(ComputeScope::Unit) {
25            return UnitShare::Repeated;
26        }
27        // A unit of the whole launch holds its tile's cells alone.
28        if level.unit_cube().is_some() {
29            return UnitShare::Whole;
30        }
31        // Innermost first, so `weight` is the axis's stride in the unit index.
32        let (mut weight, mut unit_bits) = (1usize, 0usize);
33        for axis in level.axes().into_iter().rev() {
34            if let Count::Distributed(_) = level.cut(axis).count {
35                match spanned.contains(axis) {
36                    true => continue,
37                    false => return UnitShare::Plane,
38                }
39            }
40            let plane_units = level
41                .cut(axis)
42                .count
43                .stated()
44                .expect("Level::new: a units level carries stated counts");
45            if plane_units == 1 {
46                continue;
47            }
48            assert!(
49                plane_units.is_power_of_two(),
50                "UnitShare: {axis:?} rides {plane_units} units, which is not a power of two, so its \
51                 partials are not a bit range"
52            );
53            if !spanned.contains(axis) {
54                unit_bits |= (plane_units - 1) * weight;
55            }
56            weight *= plane_units;
57        }
58        match unit_bits {
59            0 if weight == 1 => UnitShare::Repeated,
60            0 => UnitShare::Whole,
61            mask if mask == weight - 1 => UnitShare::Plane,
62            unit_bits => UnitShare::Group { unit_bits },
63        }
64    }
65
66    /// This share under `parent`'s.
67    pub(crate) fn under(self, parent: UnitShare) -> UnitShare {
68        match (parent, self) {
69            (UnitShare::Repeated, share) | (share, UnitShare::Repeated) => share,
70            (UnitShare::Whole, share) | (share, UnitShare::Whole) => share,
71            (UnitShare::Group { unit_bits: a }, UnitShare::Group { unit_bits: b }) => {
72                UnitShare::Group { unit_bits: a | b }
73            }
74            _ => {
75                panic!("UnitShare::under: {self:?} under {parent:?}: nothing reduces under a plane")
76            }
77        }
78    }
79
80    /// Whether a cell's partials sit on several units, so a drain has to reduce.
81    pub(crate) fn reduces(self) -> bool {
82        matches!(self, UnitShare::Plane | UnitShare::Group { .. })
83    }
84}
85
86#[cube]
87impl UnitShare {
88    /// Combine the partials of one cell under `monoid`, leaving the total on every holder.
89    pub(crate) fn reduce_of<T: Carrier + CubePrimitive<Scalar: PlaneNumeric>>(
90        value: T,
91        #[comptime] share: UnitShare,
92        #[comptime] monoid: Monoid,
93    ) -> T {
94        match comptime!(share) {
95            UnitShare::Repeated | UnitShare::Whole => value,
96            UnitShare::Plane => match comptime!(monoid) {
97                Monoid::Sum => plane_sum(value),
98                Monoid::Prod => plane_prod(value),
99                Monoid::Max => plane_max(value),
100                Monoid::Min => plane_min(value),
101            },
102            UnitShare::Group { unit_bits } => {
103                let mut total = value;
104                #[unroll]
105                for bit in 0..comptime!(usize::BITS - unit_bits.leading_zeros()) {
106                    if comptime!(unit_bits & (1 << bit) != 0) {
107                        total = monoid
108                            .combine::<T>(total, plane_shuffle_xor(total, comptime!(1u32 << bit)));
109                    }
110                }
111                total
112            }
113        }
114    }
115}
116
117impl UnitShare {
118    /// Combine the partials of one cell under `monoid`, leaving the total on every holder.
119    pub fn reduce<T: Carrier + CubePrimitive<Scalar: PlaneNumeric>>(
120        self,
121        value: T,
122        monoid: Monoid,
123    ) -> T {
124        UnitShare::reduce_of::<T>(value, self, monoid)
125    }
126
127    pub fn __expand_reduce_method<T: Carrier + CubePrimitive<Scalar: PlaneNumeric>>(
128        self,
129        scope: &Scope,
130        value: T::ExpandType,
131        monoid: Monoid,
132    ) -> T::ExpandType {
133        UnitShare::__expand_reduce_of::<T>(scope, value, self, monoid)
134    }
135}
136
137/// What one plane or cube instance holds of a tile's cells, and across which instances the
138/// partials of one cell lie: what a destination that adds has to serialize.
139#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
140pub(crate) enum SplitShare {
141    /// Every cell this instance writes is its own.
142    Whole,
143    /// Several cubes hold partials of the same cell, each writing it whole from its planes.
144    PartialAcrossCubes,
145    /// Several planes of one cube hold partials of the same cell, and write it at once.
146    PartialAcrossPlanes,
147}
148
149impl SplitShare {
150    /// What one instance of an operand spanning `spanned` holds after `level` is distributed over
151    /// `space`. `space` must be the level's whole space, not the operand's projection.
152    pub(crate) fn new(level: &Level, space: &Space, spanned: &Space) -> SplitShare {
153        match level.coverage() {
154            Coverage::Walk | Coverage::Distribute(ComputeScope::Unit) => return SplitShare::Whole,
155            Coverage::Distribute(ComputeScope::Plane)
156            | Coverage::Distribute(ComputeScope::PlaneGroup { .. })
157            | Coverage::Distribute(ComputeScope::Cube) => {}
158        }
159        // A shared flat index over an unspanned axis may cover part of a cell.
160        let split = match level.shared_by() {
161            Some(_) => level.axes().iter().any(|axis| !spanned.contains(*axis)),
162            None => level.axes().into_iter().any(|axis: Axis| {
163                !spanned.contains(axis) && level.instances_along(space, axis) != Some(1)
164            }),
165        };
166        match (split, level.coverage()) {
167            (false, _) => SplitShare::Whole,
168            (true, Coverage::Distribute(ComputeScope::Plane | ComputeScope::PlaneGroup { .. })) => {
169                SplitShare::PartialAcrossPlanes
170            }
171            (true, _) => SplitShare::PartialAcrossCubes,
172        }
173    }
174
175    /// This share under `parent`'s: partial stays partial, and a split across planes, which
176    /// writes a cell from several places at once, outranks one across cubes.
177    pub(crate) fn under(self, parent: SplitShare) -> SplitShare {
178        match (parent, self) {
179            (SplitShare::PartialAcrossPlanes, _) | (_, SplitShare::PartialAcrossPlanes) => {
180                SplitShare::PartialAcrossPlanes
181            }
182            (SplitShare::PartialAcrossCubes, _) | (_, SplitShare::PartialAcrossCubes) => {
183                SplitShare::PartialAcrossCubes
184            }
185            (SplitShare::Whole, SplitShare::Whole) => SplitShare::Whole,
186        }
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use crate::{Levels, Space};
194
195    const M: Axis = Axis(0);
196    const N: Axis = Axis(1);
197    const K: Axis = Axis(2);
198
199    /// A cube-cut contraction is partial to the output.
200    #[test]
201    fn a_cube_cut_contraction_is_partial_to_the_output() {
202        let space = Space::new(&[(M, 4), (N, 4), (K, 8)]);
203        let level = Levels::leaf(&[(K, 4)]).cubes(&[K]).level();
204        assert_eq!(
205            SplitShare::new(&level, &space, &space.subspace(&[M, N])),
206            SplitShare::PartialAcrossCubes
207        );
208        assert_eq!(
209            SplitShare::new(&level, &space, &space.subspace(&[M, K])),
210            SplitShare::Whole
211        );
212    }
213
214    /// Same at plane scope.
215    #[test]
216    fn a_plane_cut_contraction_is_partial_to_the_output() {
217        let space = Space::new(&[(M, 4), (N, 4), (K, 8)]);
218        let level = Levels::leaf(&[(K, 4)]).planes(&[(K, 2)]).level();
219        assert_eq!(
220            SplitShare::new(&level, &space, &space.subspace(&[M, N])),
221            SplitShare::PartialAcrossPlanes
222        );
223    }
224
225    /// A shared grid over the contraction is partial to the output.
226    #[test]
227    fn a_shared_grid_is_partial_to_the_output() {
228        let space = Space::new(&[(M, 8), (N, 8), (K, 8)]);
229        let level = Levels::leaf(&[(M, 4), (N, 4), (K, 8)])
230            .cubes(&[M, N, K])
231            .shared_by(3)
232            .level();
233        assert_eq!(
234            SplitShare::new(&level, &space, &space.subspace(&[M, N])),
235            SplitShare::PartialAcrossCubes
236        );
237        assert_eq!(SplitShare::new(&level, &space, &space), SplitShare::Whole);
238    }
239
240    /// A cube cut of the whole axis is not a split.
241    #[test]
242    fn a_cube_cut_of_the_whole_axis_is_not_a_split() {
243        let space = Space::new(&[(M, 4), (N, 4), (K, 8)]);
244        let level = Levels::leaf(&[(N, 1), (K, 8)]).cubes(&[N, K]).level();
245        assert_eq!(
246            SplitShare::new(&level, &space, &space.subspace(&[M, N])),
247            SplitShare::Whole
248        );
249    }
250
251    /// A cube cut of an output axis stays whole.
252    #[test]
253    fn a_cube_cut_output_axis_stays_whole() {
254        let space = Space::new(&[(M, 4), (N, 8), (K, 4)]);
255        let level = Levels::leaf(&[(N, 4)]).cubes(&[N]).level();
256        assert_eq!(
257            SplitShare::new(&level, &space, &space.subspace(&[M, N])),
258            SplitShare::Whole
259        );
260    }
261
262    /// A units level reduces what the operand does not span.
263    #[test]
264    fn a_units_level_reduces_what_the_operand_does_not_span() {
265        let space = Space::new(&[(M, 4), (N, 8), (K, 32)]);
266        let out = space.subspace(&[M, N]);
267        let plane = Levels::leaf(&[(K, 1)]).units(&[(K, 32)]).level();
268        assert_eq!(UnitShare::new(&plane, &out), UnitShare::Plane);
269        let whole = Levels::leaf(&[(N, 1)]).units(&[(N, 8)]).level();
270        assert_eq!(UnitShare::new(&whole, &out), UnitShare::Whole);
271        let team = Levels::leaf(&[(N, 1), (K, 1)])
272            .units(&[(N, 8), (K, 4)])
273            .level();
274        assert_eq!(
275            UnitShare::new(&team, &out),
276            UnitShare::Group { unit_bits: 3 }
277        );
278        let walk = Levels::leaf(&[(K, 8)]).walk_every(&[K]).level();
279        assert_eq!(UnitShare::new(&walk, &out), UnitShare::Repeated);
280    }
281
282    /// Shares compose level by level.
283    #[test]
284    fn shares_compose_level_by_level() {
285        let group = UnitShare::Group { unit_bits: 3 };
286        assert_eq!(group.under(UnitShare::Repeated), group);
287        assert_eq!(UnitShare::Whole.under(group), group);
288        assert_eq!(
289            UnitShare::Group { unit_bits: 12 }.under(group),
290            UnitShare::Group { unit_bits: 15 }
291        );
292        assert_eq!(
293            UnitShare::Repeated.under(UnitShare::Whole),
294            UnitShare::Whole
295        );
296        assert_eq!(
297            SplitShare::Whole.under(SplitShare::PartialAcrossCubes),
298            SplitShare::PartialAcrossCubes
299        );
300        // A split across planes under one across cubes still has planes writing a cell at once.
301        assert_eq!(
302            SplitShare::PartialAcrossPlanes.under(SplitShare::PartialAcrossCubes),
303            SplitShare::PartialAcrossPlanes
304        );
305        assert_eq!(
306            SplitShare::PartialAcrossCubes.under(SplitShare::PartialAcrossPlanes),
307            SplitShare::PartialAcrossPlanes
308        );
309    }
310}