Skip to main content

cubecl_tile/stage/pipeline/
schedule.rs

1//! The schedules that drive a walk over [`Stages`]: fill each slot ahead of the region that
2//! reads it.
3
4use cubecl::frontend::branch::{if_else_expand, if_expand};
5use cubecl::ir::Scope;
6use cubecl::prelude::*;
7use cubecl::unexpanded;
8
9use super::payload::base::{Payload, PayloadExpand};
10use crate::*;
11
12/// How stages fill their slots from their own sources, at expand level.
13pub trait StagesFill {
14    /// Whether any operand's window is fixed across the walk.
15    fn has_fixed(&self, scope: &Scope) -> bool;
16    /// Fill slot `slot`'s fixed operands from `region`'s window.
17    fn fill_fixed(&mut self, scope: &Scope, slot: usize, region: &RegionExpand);
18    /// Fill slot `slot`'s streamed operands from `region`'s window.
19    fn fill_streamed(&mut self, scope: &Scope, slot: usize, region: &RegionExpand);
20    /// [`fill_streamed`](StagesFill::fill_streamed) for a region the walk proves inside the runs
21    /// its operands hold ([`Payload::inside_runs`]).
22    fn fill_inside_run(&mut self, scope: &Scope, slot: usize, region: &RegionExpand);
23    /// Publish slot `slot`'s last fill where no later fill will (see [`Slot::publish`]).
24    fn publish(&mut self, scope: &Scope, slot: usize);
25}
26
27impl<T: CubeType> Stages<T> {
28    /// Walk `walk`'s regions, filling slots from the stages' sources and `compute` consuming each
29    /// through [`consume`](Slot::consume).
30    pub fn pipelined<F>(&mut self, _walk: Walk, _compute: F)
31    where
32        F: FnMut(&mut Slot<T>, &Region),
33    {
34        unexpanded!()
35    }
36}
37
38impl<T: CubeType> StagesExpand<T>
39where
40    StagesExpand<T>: StagesFill,
41{
42    pub fn __expand_pipelined_method<F>(&mut self, scope: &Scope, walk: WalkExpand, compute: F)
43    where
44        F: FnMut(&Scope, &mut SlotExpand<T>, &RegionExpand),
45    {
46        shared::run(
47            scope,
48            walk,
49            self,
50            |scope, stages, slot, region| stages.fill_streamed(scope, slot, region),
51            compute,
52        )
53    }
54}
55
56/// Where a walk's next stage waits while the previous one is contracted.
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
58pub enum Prefetch {
59    /// In its own slot, filled before the previous contraction.
60    InSlots,
61    /// In registers, loaded before the contraction and stored after; bounded by
62    /// [`fits`](Prefetch::fits).
63    InRegisters,
64}
65
66impl Prefetch {
67    /// Scalars one unit holds when `units` prefetch `elements` values read `width` at a time.
68    pub fn scalars(elements: usize, units: usize, width: usize) -> usize {
69        UnitLines::new(elements.div_ceil(width), units).scalars(width)
70    }
71
72    /// Whether a register prefetch of `scalars` a unit, over both operands, is accepted.
73    pub fn fits(scalars: usize) -> bool {
74        scalars <= MOST_FETCHED_SCALARS
75    }
76}
77
78impl Stages<OperandPair> {
79    /// Walk `walk`'s stages with the schedule `prefetch` selects.
80    pub fn walk<F>(&mut self, _walk: Walk, _prefetch: Prefetch, _compute: F)
81    where
82        F: FnMut(&mut Slot<OperandPair>, &Region),
83    {
84        unexpanded!()
85    }
86}
87
88impl StagesExpand<OperandPair> {
89    pub fn __expand_walk_method<F>(
90        &mut self,
91        scope: &Scope,
92        walk: WalkExpand,
93        prefetch: Prefetch,
94        compute: F,
95    ) where
96        F: FnMut(&Scope, &mut SlotExpand<OperandPair>, &RegionExpand),
97    {
98        match prefetch {
99            Prefetch::InSlots => self.__expand_pipelined_method(scope, walk, compute),
100            Prefetch::InRegisters => self.__expand_prefetched_method(scope, walk, compute),
101        }
102    }
103}
104
105impl Stages<OperandPair> {
106    /// [`pipelined`](Stages::pipelined) with the next region's loads held in registers across the
107    /// current contraction.
108    ///
109    /// Requires depth 1 or 2, every unit copying straight from a plain operand, and loads within
110    /// [`Prefetch::fits`]; otherwise refused at expansion. Fixed operands, and operands whose
111    /// stages keep their scales beside them, fall back to [`pipelined`](Stages::pipelined).
112    pub fn prefetched<F>(&mut self, _walk: Walk, _compute: F)
113    where
114        F: FnMut(&mut Slot<OperandPair>, &Region),
115    {
116        unexpanded!()
117    }
118}
119
120impl StagesExpand<OperandPair> {
121    pub fn __expand_prefetched_method<F>(&mut self, scope: &Scope, walk: WalkExpand, compute: F)
122    where
123        F: FnMut(&Scope, &mut SlotExpand<OperandPair>, &RegionExpand),
124    {
125        // Registers hold the values alone, so scales a stage keeps beside them are brought by the
126        // slots.
127        let scales_beside = self.sources.lhs.__expand_scales_beside_method(scope)
128            || self.sources.rhs.__expand_scales_beside_method(scope);
129        if self.has_fixed(scope) || scales_beside {
130            return self.__expand_pipelined_method(scope, walk, compute);
131        }
132        self.__expand_assert_copied_by_every_unit_method(scope);
133        assert!(
134            self.depth <= 2,
135            "Stages::prefetched: {} slots hold one region ahead, so the slots past a second \
136             would sit idle; build them with a depth of 1 or 2",
137            self.depth
138        );
139        let total = walk.__expand_total_method(scope);
140        self.prime(scope, &walk, &total);
141        self.prefetching_laps(scope, walk, total, compute);
142    }
143
144    /// Fill and publish the first region before any contraction.
145    fn prime(&mut self, scope: &Scope, walk: &WalkExpand, total: &NativeExpand<usize>) {
146        let any = 0usize.into_expand(scope).__expand_lt_method(scope, total);
147        if_expand(scope, any, |scope| {
148            let first = walk.__expand_region_method(scope, FIRST_SLOT.into_expand(scope));
149            self.fill_streamed(scope, FIRST_SLOT, &first);
150            self.publish(scope, FIRST_SLOT);
151        });
152    }
153
154    /// The laps whose every region has a next one to fetch, run with no branch around the fetch,
155    /// so what it computes that no lap changes (each unit's place in the stage) leaves the loop.
156    /// How many ran.
157    fn steady_prefetching_laps<Lhs: Numeric, Rhs: Numeric, F>(
158        &mut self,
159        scope: &Scope,
160        walk: &WalkExpand,
161        total: NativeExpand<usize>,
162        lhs: &mut <Array<Vector<Lhs, super::base::FL>> as CubeType>::ExpandType,
163        rhs: &mut <Array<Vector<Rhs, super::base::FR>> as CubeType>::ExpandType,
164        compute: &mut F,
165    ) -> NativeExpand<usize>
166    where
167        F: FnMut(&Scope, &mut SlotExpand<OperandPair>, &RegionExpand),
168    {
169        // The laps whose every region has a next one to fetch: no branch around the fetch, so what
170        // it computes that no lap changes (each unit's place in the stage) leaves the loop. Over a
171        // run ([`Walk::run`]) they leave its last region to the last laps and fetch unchecked.
172        let depth = self.depth;
173        let (held_back, sources) = match walk.run {
174            None => (0, self.sources.clone()),
175            Some(_) => (1, self.sources.__expand_inside_runs_method(scope)),
176        };
177        let steady = steady_laps(scope, total, 1 + held_back, depth);
178        let mut steady_body = |scope: &Scope, lap: NativeExpand<usize>| {
179            for j in 0..depth {
180                let target = (j + 1) % depth;
181                let region_idx = lap
182                    .__expand_times_method(scope, depth.into_expand(scope))
183                    .__expand_plus_method(scope, j.into_expand(scope));
184                let next = region_idx.__expand_plus_method(scope, 1usize.into_expand(scope));
185                let upcoming = walk.__expand_region_method(scope, next);
186                self.__expand_fetch_method(scope, target, &sources, &upcoming, lhs, rhs);
187                let region = walk.__expand_region_method(scope, region_idx);
188                let slot = self.__expand_slot_mut_method(scope, j);
189                compute(scope, slot, &region);
190                // One slot: every unit has read it before any overwrites it.
191                if depth == 1 {
192                    self.publish(scope, FIRST_SLOT);
193                }
194                self.__expand_store_method(scope, target, lhs, rhs);
195                self.publish(scope, target);
196            }
197        };
198        run_laps(
199            scope,
200            0usize.into_expand(scope),
201            steady,
202            walk.unroll,
203            &mut steady_body,
204        );
205        steady
206    }
207
208    /// Each lap fetches the next region into registers, contracts, then stores into the freed slot.
209    fn prefetching_laps<F>(
210        &mut self,
211        scope: &Scope,
212        walk: WalkExpand,
213        total: NativeExpand<usize>,
214        mut compute: F,
215    ) where
216        F: FnMut(&Scope, &mut SlotExpand<OperandPair>, &RegionExpand),
217    {
218        let depth = self.depth;
219        // The registers a fetch holds a region in are at the elements the sources hold.
220        define_scalar!(Lhs);
221        define_scalar!(Rhs);
222        let _lhs = scope.define_type::<Lhs>(self.sources.lhs.element);
223        let _rhs = scope.define_type::<Rhs>(self.sources.rhs.element);
224        let (mut lhs, mut rhs) = self.__expand_fetch_buffers_method::<Lhs, Rhs>(scope);
225        let steady =
226            self.steady_prefetching_laps(scope, &walk, total, &mut lhs, &mut rhs, &mut compute);
227        // The last laps, where the walk runs out under them.
228        let mut body = |scope: &Scope, lap: NativeExpand<usize>| {
229            for j in 0..depth {
230                let target = (j + 1) % depth;
231                let region_idx = lap
232                    .__expand_times_method(scope, depth.into_expand(scope))
233                    .__expand_plus_method(scope, j.into_expand(scope));
234                let next = region_idx.__expand_plus_method(scope, 1usize.into_expand(scope));
235                let in_walk = region_idx.__expand_lt_method(scope, &total);
236                if_expand(scope, in_walk, |scope| {
237                    let prefetching = next.__expand_lt_method(scope, &total);
238                    if_expand(scope, prefetching, |scope| {
239                        let upcoming = walk.__expand_region_method(scope, next);
240                        let sources = self.sources.clone();
241                        self.__expand_fetch_method(
242                            scope, target, &sources, &upcoming, &mut lhs, &mut rhs,
243                        );
244                    });
245                    let region = walk.__expand_region_method(scope, region_idx);
246                    let slot = self.__expand_slot_mut_method(scope, j);
247                    compute(scope, slot, &region);
248                    if_expand(scope, prefetching, |scope| {
249                        // One slot: every unit must finish reading before any overwrites it.
250                        if depth == 1 {
251                            self.publish(scope, FIRST_SLOT);
252                        }
253                        self.__expand_store_method(scope, target, &lhs, &rhs);
254                        self.publish(scope, target);
255                    });
256                });
257            }
258        };
259        // One lap is left whenever the walk holds a region (`ceil(t / d) - floor((t - 1) / d)`),
260        // none when it holds nothing, and the body guards every region it names: it runs once,
261        // straight, rather than as a loop around a copy of the contraction. The region a run's
262        // steady laps leave may make it two.
263        match walk.run {
264            None => body(scope, steady),
265            Some(_) => {
266                let laps = total
267                    .__expand_plus_method(scope, (depth - 1).into_expand(scope))
268                    .__expand_divided_by_method(scope, depth.into_expand(scope));
269                run_laps(scope, steady, laps, walk.unroll, &mut body);
270            }
271        }
272    }
273}
274
275/// Laps of `depth` regions each, out of a walk of `total`, in which every region has the one
276/// `ahead` of it to fill: `(total - ahead) / depth`, none where the walk is shorter than `ahead`.
277/// What a schedule runs with no branch around its fills before the laps where the walk runs out.
278fn steady_laps(
279    scope: &Scope,
280    total: NativeExpand<usize>,
281    ahead: usize,
282    depth: usize,
283) -> NativeExpand<usize> {
284    total
285        .__expand_max_with_method(scope, ahead.into_expand(scope))
286        .__expand_minus_method(scope, ahead.into_expand(scope))
287        .__expand_divided_by_method(scope, depth.into_expand(scope))
288}
289
290/// `body` on every lap in `from..to`, unrolled where the walk says.
291fn run_laps(
292    scope: &Scope,
293    from: NativeExpand<usize>,
294    to: NativeExpand<usize>,
295    unroll: bool,
296    body: &mut dyn FnMut(&Scope, NativeExpand<usize>),
297) {
298    let range = RangeExpand::new(from, to);
299    if unroll {
300        range.expand_unroll(scope, body);
301    } else {
302        range.expand(scope, body);
303    }
304}
305
306impl<P: Payload<P> + CubeType<ExpandType: Clone>> StagesFill for StagesExpand<P> {
307    fn has_fixed(&self, scope: &Scope) -> bool {
308        self.slots
309            .__expand_index_method(scope, FIRST_SLOT.into_expand(scope))
310            .__expand_has_fixed_method(scope)
311    }
312
313    fn fill_fixed(&mut self, scope: &Scope, slot: usize, region: &RegionExpand) {
314        let sources = self.sources.clone();
315        self.__expand_slot_mut_method(scope, slot)
316            .__expand_refill_method(scope, &sources, region, Refill::Once);
317    }
318
319    fn fill_streamed(&mut self, scope: &Scope, slot: usize, region: &RegionExpand) {
320        let sources = self.sources.clone();
321        self.__expand_slot_mut_method(scope, slot)
322            .__expand_refill_method(scope, &sources, region, Refill::EveryRegion);
323    }
324
325    fn fill_inside_run(&mut self, scope: &Scope, slot: usize, region: &RegionExpand) {
326        let sources = self.sources.__expand_inside_runs_method(scope);
327        self.__expand_slot_mut_method(scope, slot)
328            .__expand_refill_method(scope, &sources, region, Refill::EveryRegion);
329    }
330
331    fn publish(&mut self, scope: &Scope, slot: usize) {
332        self.__expand_slot_mut_method(scope, slot)
333            .__expand_publish_method(scope);
334    }
335}
336
337/// The shared schedule: prologue, laps prefetching one region ahead, drain publishing.
338mod shared {
339    use super::*;
340
341    pub(super) fn run<T: CubeType, Fill, F>(
342        scope: &Scope,
343        walk: WalkExpand,
344        stages: &mut StagesExpand<T>,
345        mut fill: Fill,
346        mut compute: F,
347    ) where
348        StagesExpand<T>: StagesFill,
349        Fill: FnMut(&Scope, &mut StagesExpand<T>, usize, &RegionExpand),
350        F: FnMut(&Scope, &mut SlotExpand<T>, &RegionExpand),
351    {
352        let depth = stages.depth;
353        let unroll = walk.unroll;
354        let total = walk.__expand_total_method(scope);
355
356        if stages.has_fixed(scope) {
357            let first = walk.__expand_region_method(scope, FIRST_SLOT.into_expand(scope));
358            for slot in 0..depth {
359                stages.fill_fixed(scope, slot, &first);
360            }
361        }
362
363        for slot in 0..depth - 1 {
364            let index = slot.into_expand(scope);
365            let cond = index.__expand_lt_method(scope, &total);
366            if_expand(scope, cond, |scope| {
367                let region = walk.__expand_region_method(scope, slot.into_expand(scope));
368                fill(scope, stages, slot, &region);
369            });
370        }
371
372        let laps = total
373            .__expand_plus_method(scope, (depth - 1).into_expand(scope))
374            .__expand_divided_by_method(scope, depth.into_expand(scope));
375        // Over a run ([`Walk::run`]) the steady laps leave its last region to the last laps and fill
376        // unchecked.
377        let steady = match walk.run {
378            None => steady_ahead_laps(scope, &walk, stages, total, 0, &mut fill, &mut compute),
379            Some(_) => steady_ahead_laps(
380                scope,
381                &walk,
382                stages,
383                total,
384                1,
385                &mut |scope, stages: &mut StagesExpand<T>, slot, region: &RegionExpand| {
386                    stages.fill_inside_run(scope, slot, region)
387                },
388                &mut compute,
389            ),
390        };
391        let mut body = |scope: &Scope, lap: NativeExpand<usize>| {
392            for j in 0..depth {
393                let region_idx = lap
394                    .__expand_times_method(scope, depth.into_expand(scope))
395                    .__expand_plus_method(scope, j.into_expand(scope));
396                if depth == 1 {
397                    let region = walk.__expand_region_method(scope, region_idx);
398                    fill(scope, stages, FIRST_SLOT, &region);
399                    stages.publish(scope, FIRST_SLOT);
400                    let slot = stages.__expand_slot_mut_method(scope, FIRST_SLOT);
401                    compute(scope, slot, &region);
402                } else {
403                    let ahead =
404                        region_idx.__expand_plus_method(scope, (depth - 1).into_expand(scope));
405                    let prefetching = ahead.__expand_lt_method(scope, &total);
406                    let in_walk = region_idx.__expand_lt_method(scope, &total);
407                    // Compute is emitted once, after the fill branch, keeping its accumulator live.
408                    if_else_expand(scope, prefetching, |scope| {
409                        let prefetch = walk.__expand_region_method(scope, ahead);
410                        fill(scope, stages, (j + depth - 1) % depth, &prefetch);
411                    })
412                    .or_else(scope, |scope| {
413                        // Draining: no fill follows, so this consume publishes.
414                        if_expand(scope, in_walk, |scope| {
415                            stages.publish(scope, j);
416                        });
417                    });
418                    if_expand(scope, in_walk, |scope| {
419                        let region = walk.__expand_region_method(scope, region_idx);
420                        let slot = stages.__expand_slot_mut_method(scope, j);
421                        compute(scope, slot, &region);
422                    });
423                }
424            }
425        };
426        run_laps(scope, steady, laps, unroll, &mut body);
427    }
428
429    /// The laps whose every region has the one `depth - 1` ahead to fill, `held_back` regions
430    /// short of the walk's end, run with no branch around the fill, so what it computes that no
431    /// lap changes (each unit's place in the stage) leaves the loop; the last laps keep the
432    /// branches and the regions held back. One slot fills the region it contracts, so it has
433    /// steady laps only where a region is held back. How many ran.
434    fn steady_ahead_laps<T: CubeType, Fill, F>(
435        scope: &Scope,
436        walk: &WalkExpand,
437        stages: &mut StagesExpand<T>,
438        total: NativeExpand<usize>,
439        held_back: usize,
440        fill: &mut Fill,
441        compute: &mut F,
442    ) -> NativeExpand<usize>
443    where
444        StagesExpand<T>: StagesFill,
445        Fill: FnMut(&Scope, &mut StagesExpand<T>, usize, &RegionExpand),
446        F: FnMut(&Scope, &mut SlotExpand<T>, &RegionExpand),
447    {
448        let depth = stages.depth;
449        let ahead = depth - 1 + held_back;
450        if ahead == 0 {
451            return 0usize.into_expand(scope);
452        }
453        let steady = steady_laps(scope, total, ahead, depth);
454        let mut steady_body = |scope: &Scope, lap: NativeExpand<usize>| {
455            for j in 0..depth {
456                let region_idx = lap
457                    .__expand_times_method(scope, depth.into_expand(scope))
458                    .__expand_plus_method(scope, j.into_expand(scope));
459                let ahead = region_idx.__expand_plus_method(scope, (depth - 1).into_expand(scope));
460                let prefetch = walk.__expand_region_method(scope, ahead);
461                fill(scope, stages, (j + depth - 1) % depth, &prefetch);
462                // One slot: the fill is the region contracted next, published before it is read.
463                if depth == 1 {
464                    stages.publish(scope, FIRST_SLOT);
465                }
466                let region = walk.__expand_region_method(scope, region_idx);
467                let slot = stages.__expand_slot_mut_method(scope, j);
468                compute(scope, slot, &region);
469            }
470        };
471        run_laps(
472            scope,
473            0usize.into_expand(scope),
474            steady,
475            walk.unroll,
476            &mut steady_body,
477        );
478        steady
479    }
480}