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#[derive(CubeType, Clone)]
14#[expand(derive(Clone))]
15pub struct Procedural {
16 origin: Coords<u32>,
17 bound: Coords<u32>,
19 #[cube(comptime)]
21 pub(crate) bounds_check: bool,
22 #[cube(comptime)]
24 bounded_axes: Vec<Axis>,
25 #[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)] 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 pub(crate) fn value_at<T: Numeric>(
107 &self,
108 slice: u32,
109 position: u32,
110 #[comptime] space: Space,
111 ) -> T {
112 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 #[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)] 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 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 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)] 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 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 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 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 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 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}