Skip to main content

cubecl_tile/tile/kind/procedural/
kind.rs

1use cubecl::frontend::IntoExpand;
2use cubecl::ir::Scope;
3use cubecl::prelude::{barrier::Barrier, *};
4use cubecl::unexpanded;
5use cubecl_std::tensor::{ViewOperations, ViewOperationsExpand, layout::CoordsDyn};
6
7use crate::*;
8
9use super::{ErasedRecipe, FactorReads, RecipeCoords, Separable, SeparableExpand};
10
11/// Runtime state of a procedural source. `origin` tracks regions selected by `Tile::at`. Its
12/// recipe computes at its own element; a read names the element it wants.
13#[derive(CubeType, Clone)]
14#[expand(derive(Clone))]
15pub struct Procedural {
16    origin: Coords<u32>,
17    /// The source's static extent in the parent's coordinates; dynamic axes hold `u32::MAX`.
18    bound: Coords<u32>,
19    /// Whether any level can select a partial tile.
20    #[cube(comptime)]
21    pub(crate) bounds_check: bool,
22    /// The statically overhanging axes.
23    #[cube(comptime)]
24    bounded_axes: Vec<Axis>,
25    /// Requested factor normalization, consumed by a separable contraction.
26    #[cube(comptime)]
27    pub(crate) normalization: Option<Normalization>,
28    recipe: ErasedRecipe,
29    #[cube(comptime)]
30    pub(crate) space: Space,
31}
32
33#[cube]
34impl Procedural {
35    #[allow(dead_code)] // Reached through its expand, from `new` and `separable`.
36    pub(crate) fn erased(#[comptime] space: Space, recipe: ErasedRecipe) -> Self {
37        let mut origin = Coords::<u32>::new();
38        let mut bound = Coords::<u32>::new();
39        #[unroll]
40        for p in 0..comptime!(space.rank()) {
41            origin.push(0u32.runtime());
42            let axis = comptime!(space.axis_at(p));
43            let extent = comptime!(if space.is_dynamic(axis) {
44                u32::MAX.runtime()
45            } else {
46                space.runtime_extent_at(p) as u32
47            });
48            bound.push(extent);
49        }
50        let bounded_axes = comptime!(Vec::new());
51        Procedural {
52            origin,
53            bound,
54            bounds_check: comptime!(false),
55            bounded_axes,
56            normalization: None,
57            recipe,
58            space,
59        }
60    }
61
62    pub(crate) fn at(&self, step: &Step, #[comptime] space: Space) -> Self {
63        let mut origin = Coords::<u32>::new();
64        #[unroll]
65        for p in 0..comptime!(space.rank()) {
66            let axis = comptime!(space.axis_at(p));
67            match comptime!(step.level.tile(axis)) {
68                Some(tile) => {
69                    let tile = comptime!(tile as u32);
70                    origin.push(self.origin.at(p) + step.coord(axis).cast::<u32>() * tile);
71                }
72                None => origin.push(self.origin.at(p)),
73            }
74        }
75        let bounded_axes = comptime!({
76            let mut axes = self.bounded_axes.clone();
77            for axis in space.axes() {
78                if !space.is_dynamic(axis)
79                    && step.level.overhangs(&space, axis)
80                    && !axes.contains(&axis)
81                {
82                    axes.push(axis);
83                }
84            }
85            axes
86        });
87        Procedural {
88            origin,
89            bound: self.bound.clone(),
90            bounds_check: comptime!(!bounded_axes.is_empty()),
91            bounded_axes,
92            normalization: comptime!(self.normalization.clone()),
93            recipe: self.recipe.clone(),
94            space: comptime!(step.level.child(&space)),
95        }
96    }
97
98    pub(crate) fn evaluate<T: Numeric>(&self, pos: &Coords<u32>, #[comptime] space: Space) -> T {
99        let absolute = RecipeCoords::new(&self.origin, pos, space);
100        self.recipe.evaluate::<T>(&absolute)
101    }
102
103    /// This recipe's value at position `position` of slice `slice` of a tile over `space` along
104    /// its innermost axis, windowed to the same region: the slice unravelled over every other axis
105    /// of `space`, the position along the innermost, and an axis `space` does not span at zero.
106    pub(crate) fn value_at<T: Numeric>(
107        &self,
108        slice: u32,
109        position: u32,
110        #[comptime] space: Space,
111    ) -> T {
112        // Where each of this recipe's axes reads its coordinate from: the position, a coordinate of
113        // the slice, or nowhere.
114        let (slice_extents, sources) = comptime!({
115            let rank = space.rank();
116            let extents: Vec<usize> = (0..rank - 1).map(|p| space.extent_at(p)).collect();
117            let sources: Vec<Option<usize>> = self
118                .space
119                .axes()
120                .map(|axis| match space.contains(axis) {
121                    true => Some(space.position(axis)),
122                    false => None,
123                })
124                .collect();
125            (extents, sources)
126        });
127        let position_at = comptime!(space.rank() - 1);
128        let slice_coords = Coords::<u32>::constant(slice_extents).unravel(slice);
129        let mut coords = Coords::<u32>::new();
130        // `#[unroll]` needs a range loop.
131        #[allow(clippy::needless_range_loop)]
132        #[unroll]
133        for p in 0..sources.len() {
134            match sources[p] {
135                Some(at) if at == position_at => coords.push(position),
136                Some(at) => coords.push(slice_coords.at(at)),
137                None => coords.push(0u32.runtime()),
138            }
139        }
140        self.evaluate::<T>(&coords, comptime!(self.space.clone()))
141    }
142
143    pub(crate) fn factorization(&self) -> comptime_type!(Option<usize>) {
144        self.recipe.factorization()
145    }
146
147    #[allow(dead_code)] // Reached through its expand, from [`Tile::factor_dependencies`].
148    pub(crate) fn factor_reads(
149        &self,
150        #[comptime] factor: usize,
151        #[comptime] axis: Axis,
152    ) -> comptime_type!(bool) {
153        self.recipe.factor_reads(factor, axis)
154    }
155
156    pub(crate) fn read_factor_at<T: Numeric>(
157        &self,
158        pos: &CoordsDyn,
159        #[comptime] factor: usize,
160        #[comptime] space: Space,
161    ) -> T {
162        let mut coords = Coords::<u32>::new();
163        #[unroll]
164        for p in 0..comptime!(space.rank()) {
165            coords.push(pos[p]);
166        }
167        let absolute = RecipeCoords::new(&self.origin, &coords, space);
168        self.recipe.factor::<T>(&absolute, factor)
169    }
170
171    /// Whether `pos` remains inside the original procedural box along `axis`.
172    pub(crate) fn axis_in_bounds(&self, pos: &CoordsDyn, #[comptime] axis: Axis) -> bool {
173        if comptime!(self.bounded_axes.contains(&axis) && self.space.contains(axis)) {
174            let p = comptime!(self.space.position(axis));
175            self.origin.at(p) + pos[p] < self.bound.at(p)
176        } else {
177            true.runtime()
178        }
179    }
180
181    /// Evaluate with the static partial-tile mask; dynamic axes are unmasked.
182    pub(crate) fn read_masked<T: Numeric>(&self, pos: &Coords<u32>, #[comptime] space: Space) -> T {
183        if comptime!(self.bounds_check) && !self.is_in_bounds(pos) {
184            T::from_int(0)
185        } else {
186            self.evaluate::<T>(pos, space)
187        }
188    }
189
190    #[allow(dead_code)] // Reached through its expand, from the `ViewOperationsExpand` impl below.
191    pub(crate) fn read_at<T: Numeric>(&self, pos: &CoordsDyn, #[comptime] space: Space) -> T {
192        let mut coords = Coords::<u32>::new();
193        #[unroll]
194        for p in 0..comptime!(space.rank()) {
195            coords.push(pos[p]);
196        }
197        self.evaluate::<T>(&coords, space)
198    }
199
200    fn is_in_bounds(&self, pos: &Coords<u32>) -> bool {
201        let mut in_bounds = true;
202        #[unroll]
203        for p in 0..comptime!(self.space.rank()) {
204            in_bounds = in_bounds && self.origin.at(p) + pos.at(p) < self.bound.at(p);
205        }
206        in_bounds
207    }
208}
209
210impl Vectorized for Procedural {}
211
212impl ProceduralExpand {
213    pub(crate) fn factor_count(&self, scope: &Scope) -> Option<usize> {
214        self.recipe.__expand_factorization_method(scope)
215    }
216}
217
218impl VectorizedExpand for ProceduralExpand {
219    fn __expand_vector_size_method(&self, _scope: &Scope) -> VectorSize {
220        1
221    }
222}
223
224impl<T: Numeric, W: Size> ViewOperations<Vector<T, W>, CoordsDyn> for Procedural {}
225
226impl<T: Numeric, W: Size> ViewOperationsExpand<Vector<T, W>, CoordsDyn> for ProceduralExpand {
227    fn __expand_read_method(
228        &self,
229        scope: &Scope,
230        pos: <CoordsDyn as CubeType>::ExpandType,
231    ) -> NativeExpand<Vector<T, W>> {
232        assert_eq!(
233            W::__expand_value(scope),
234            1,
235            "Procedural: a procedural read is scalar; a vectorized read would broadcast \
236             the first unit's value instead of ramping the innermost coordinate"
237        );
238        let value = self
239            .clone()
240            .__expand_read_at_method::<T>(scope, &pos, self.space.clone());
241        Vector::<T, W>::__expand_cast_from(scope, value)
242    }
243
244    fn __expand_read_checked_method(
245        &self,
246        scope: &Scope,
247        pos: <CoordsDyn as CubeType>::ExpandType,
248    ) -> NativeExpand<Vector<T, W>> {
249        let valid =
250            <Self as ViewOperationsExpand<Vector<T, W>, CoordsDyn>>::__expand_is_in_bounds_method(
251                self,
252                scope,
253                pos.clone(),
254            );
255        let value = self.__expand_read_method(scope, pos);
256        let zero = Vector::<T, W>::__expand_cast_from(scope, 0.into());
257        select::expand::<Vector<T, W>>(scope, valid, value, zero)
258    }
259
260    fn __expand_read_masked_method(
261        &self,
262        scope: &Scope,
263        pos: <CoordsDyn as CubeType>::ExpandType,
264        mask_value: NativeExpand<Vector<T, W>>,
265    ) -> NativeExpand<Vector<T, W>> {
266        let valid =
267            <Self as ViewOperationsExpand<Vector<T, W>, CoordsDyn>>::__expand_is_in_bounds_method(
268                self,
269                scope,
270                pos.clone(),
271            );
272        let value = self.__expand_read_method(scope, pos);
273        select::expand::<Vector<T, W>>(scope, valid, value, mask_value)
274    }
275
276    fn __expand_read_unchecked_method(
277        &self,
278        scope: &Scope,
279        pos: <CoordsDyn as CubeType>::ExpandType,
280    ) -> NativeExpand<Vector<T, W>> {
281        self.__expand_read_method(scope, pos)
282    }
283
284    fn __expand_as_linear_slice_method(
285        &self,
286        _scope: &Scope,
287        _pos: <CoordsDyn as CubeType>::ExpandType,
288        _end: <CoordsDyn as CubeType>::ExpandType,
289    ) -> &SliceExpand<Vector<T, W>> {
290        panic!("Procedural: procedural sources have no backing slice")
291    }
292
293    fn __expand_shape_method(&self, scope: &Scope) -> <CoordsDyn as CubeType>::ExpandType {
294        CoordsDyn::__expand_new(scope)
295    }
296
297    fn __expand_is_in_bounds_method(
298        &self,
299        scope: &Scope,
300        pos: <CoordsDyn as CubeType>::ExpandType,
301    ) -> NativeExpand<bool> {
302        let mut in_bounds: NativeExpand<bool> = true.into();
303        for p in 0..comptime!(self.space.rank()) {
304            let index = p.into_expand(scope);
305            let origin = self.origin.__expand_at_method(scope, index);
306            let bound = self.bound.__expand_at_method(scope, index);
307            let pos = pos.clone();
308            let coord = pos.__expand_index_method(scope, index);
309            let absolute = origin.__expand_add_method(scope, *coord);
310            let axis_in_bounds = absolute.__expand_lt_method(scope, &bound);
311            in_bounds = in_bounds.__expand_and_method(scope, axis_in_bounds);
312        }
313        in_bounds
314    }
315
316    fn __expand_tensor_map_load_method(
317        &self,
318        _scope: &Scope,
319        _barrier: &NativeExpand<Barrier>,
320        _shared_memory: &mut SliceExpand<Vector<T, W>>,
321        _pos: <CoordsDyn as CubeType>::ExpandType,
322    ) {
323        panic!("Procedural: procedural sources cannot issue TMA loads")
324    }
325}
326
327#[cube]
328impl Procedural {
329    /// This source as a tile, placed alone.
330    pub fn tile<T: Numeric>(self) -> Tile {
331        let space = comptime!(self.space.clone());
332        Tile::new::<T>(
333            TileKind::new_Procedural(self),
334            comptime!(Placement::alone(space)),
335        )
336    }
337
338    /// This source as a tile at the root of `partitioning`, windowed by its regions like an
339    /// operand bound to it: its space must be the partitioning's, or some of its axes.
340    pub fn tile_in<T: Numeric>(self, partitioning: &Partitioning) -> Tile {
341        let space = comptime!(self.space.clone());
342        let levels = comptime!(partitioning.levels().to_vec());
343        Tile::new::<T>(
344            TileKind::new_Procedural(self),
345            comptime!(Placement::root(space, levels)),
346        )
347    }
348}
349
350impl Procedural {
351    /// A memory-free source over `space`, evaluated from `recipe`, which computes at `T`, where it
352    /// is read.
353    pub fn new<T: Numeric, R: Recipe<T> + 'static>(_space: Space, _recipe: R) -> Self {
354        unexpanded!()
355    }
356
357    pub fn __expand_new<T: Numeric, R: Recipe<T> + 'static>(
358        scope: &Scope,
359        space: Space,
360        recipe: R::ExpandType,
361    ) -> ProceduralExpand {
362        Self::__expand_erased(
363            scope,
364            space,
365            ErasedRecipe::__expand_new::<T, R>(scope, recipe),
366        )
367    }
368
369    /// [`new`](Self::new) keeping the recipe's factorization, one factor per contracted axis.
370    pub fn separable<T: Numeric, R: Separable<T> + 'static>(_space: Space, _recipe: R) -> Self
371    where
372        R::ExpandType: FactorReads,
373    {
374        unexpanded!()
375    }
376
377    pub fn __expand_separable<T: Numeric, R: Separable<T> + 'static>(
378        scope: &Scope,
379        space: Space,
380        recipe: R::ExpandType,
381    ) -> ProceduralExpand
382    where
383        R::ExpandType: SeparableExpand<T> + FactorReads,
384    {
385        Self::__expand_erased(
386            scope,
387            space,
388            ErasedRecipe::__expand_new_separable::<T, R>(scope, recipe),
389        )
390    }
391
392    /// Normalize each factor's taps where the gather contraction evaluates them.
393    /// The recipe must state a factorization.
394    pub fn normalized(self, _taps: TapSupport, _guard: DivGuard) -> Self {
395        unexpanded!()
396    }
397}
398
399impl ProceduralExpand {
400    pub fn __expand_normalized_method(
401        mut self,
402        scope: &Scope,
403        taps: TapSupport,
404        guard: DivGuard,
405    ) -> ProceduralExpand {
406        assert!(
407            self.factor_count(scope).is_some(),
408            "Procedural::normalized: the recipe states no separable factorization"
409        );
410        self.normalization = Some(Normalization::new(taps, guard, self.space.clone()));
411        self
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use cubecl::ir::{ExpandValue, Scope};
418
419    use crate::tile::kind::procedural::constant::{Constant, ConstantExpand};
420    use crate::*;
421
422    fn test_scope() -> Scope {
423        Scope::root(cubecl::ir::settings::KernelSettings::new(
424            cubecl::ir::settings::Dim3::new_single(),
425            cubecl::ir::settings::ExecutionMode::Checked,
426            cubecl::ir::AddressType::U32,
427        ))
428    }
429
430    #[test]
431    #[should_panic(expected = "the recipe states no separable factorization")]
432    fn normalized_rejects_an_opaque_recipe() {
433        let scope = test_scope();
434        let source = Procedural::__expand_new::<f32, Constant<f32>>(
435            &scope,
436            Space::new(&[(Axis(0), 4)]),
437            ConstantExpand::<f32> {
438                value: ExpandValue::constant(
439                    0u64.into(),
440                    cubecl::ir::ElemType::Float(cubecl::ir::FloatKind::F32),
441                )
442                .into(),
443            },
444        );
445        source.__expand_normalized_method(&scope, TapSupport::Whole, DivGuard::default());
446    }
447}