Skip to main content

cubecl_tile/launch/
base.rs

1//! One kernel launch: the [`Launcher`].
2
3use std::sync::Arc;
4
5use cubecl::ir::{ElemType, HardwareProperties, OpaqueType};
6use cubecl::prelude::*;
7
8use crate::{
9    Arg, Axis, Geometry, Partitioning, PartitioningLaunch, Refusal, Space, SpaceLaunch, Unlabelled,
10};
11
12/// How many cubes of how many units the launch runs: stated, or read off the levels.
13#[derive(Clone, Debug)]
14pub enum Grid {
15    Stated {
16        cube_count: CubeCount,
17        cube_dim: CubeDim,
18    },
19    FromLevels,
20}
21
22/// Whether a device of `hardware` spawns `cube_count` cubes of `cube_dim` units, or the
23/// [`Refusal`] saying why not.
24///
25/// Three caps, and a launch clears all of them: the cube's unit product, the cube's per-axis
26/// shape, and the grid's per-axis extent. Checking only the product passes a cube legal in size
27/// and illegal in shape: a CPU runtime reports its core count on every axis, and a cube of many
28/// planes overruns `max_cube_dim.1` long before the product notices.
29///
30/// A zero axis is no launch rather than a small grid: cubecl's CUDA server returns before
31/// dispatching one, so the outputs keep what they held and the call is timed as though it ran.
32/// A dynamic count lives on the device, and the kernel reading one owns its bound.
33pub fn spawnable(
34    hardware: &HardwareProperties,
35    cube_dim: CubeDim,
36    cube_count: &CubeCount,
37) -> Result<(), Refusal> {
38    let units = cube_dim.num_elems();
39    if units > hardware.max_units_per_cube {
40        return Err(Refusal::CubePastDevice {
41            units,
42            most: hardware.max_units_per_cube,
43        });
44    }
45    if units == 0 {
46        return Err(Refusal::NoUnits);
47    }
48    let (most_x, most_y, most_z) = hardware.max_cube_dim;
49    if cube_dim.x > most_x || cube_dim.y > most_y || cube_dim.z > most_z {
50        return Err(Refusal::CubeShapePastDevice {
51            shape: (cube_dim.x, cube_dim.y, cube_dim.z),
52            most: hardware.max_cube_dim,
53        });
54    }
55    if let CubeCount::Static(x, y, z) = *cube_count {
56        if x == 0 || y == 0 || z == 0 {
57            return Err(Refusal::NoCubes);
58        }
59        let (most_x, most_y, most_z) = hardware.max_cube_count;
60        if x > most_x || y > most_y || z > most_z {
61            return Err(Refusal::GridPastDevice {
62                count: (x, y, z),
63                most: hardware.max_cube_count,
64            });
65        }
66    }
67    Ok(())
68}
69
70/// One launch: a kernel-form partitioning, the concrete space with real extents, and the grid.
71/// What it settled is shared, so a launch kept and run again clones it for the price of a count.
72#[derive(Clone)]
73pub struct Launcher {
74    client: Client,
75    partitioning: Arc<Partitioning>,
76    concrete: Arc<Space>,
77    cube_count: CubeCount,
78    cube_dim: CubeDim,
79    /// The axes some tile reaches past the end of over the concrete space, worked out once: every
80    /// operand the launch binds asks for them.
81    overhanging: Arc<[Axis]>,
82    /// The leaf over the concrete space.
83    leaf: Arc<Space>,
84}
85
86impl Launcher {
87    /// `partitioning` over `concrete`'s real extents, on `grid`, or the [`Refusal`] saying why the
88    /// device cannot run it.
89    pub fn new(
90        client: &Client,
91        partitioning: Partitioning,
92        concrete: &Space,
93        grid: Grid,
94    ) -> Result<Self, Refusal> {
95        // The kernel-form space may state no extents.
96        let over_concrete = Partitioning::new(concrete.clone(), partitioning.levels().to_vec());
97        let (cube_count, cube_dim) = match grid {
98            Grid::Stated {
99                cube_count,
100                cube_dim,
101            } => (cube_count, cube_dim),
102            Grid::FromLevels => {
103                let plane_size = client.properties().hardware.plane_size_max;
104                let plane_units = partitioning.units();
105                if plane_units != 1 && plane_units != plane_size {
106                    return Err(Refusal::GridNotReadOffLevels {
107                        units: plane_units,
108                        plane: plane_size,
109                    });
110                }
111                (
112                    over_concrete.cube_count(),
113                    over_concrete.cube_dim(plane_size),
114                )
115            }
116        };
117        spawnable(&client.properties().hardware, cube_dim, &cube_count)?;
118        let fillers = partitioning.fillers();
119        // Checked on the host: a panic at expansion is unseen and the launch returns zeros.
120        let barriers = client
121            .properties()
122            .features
123            .types
124            .opaque
125            .contains(&OpaqueType::Barrier);
126        if fillers > 0 && !barriers {
127            return Err(Refusal::FillersWithoutBarriers { fillers });
128        }
129        Ok(Launcher {
130            client: client.clone(),
131            partitioning: Arc::new(partitioning),
132            concrete: Arc::new(concrete.clone()),
133            cube_count,
134            cube_dim,
135            overhanging: over_concrete.overhanging().into(),
136            leaf: Arc::new(over_concrete.leaf()),
137        })
138    }
139
140    /// This launch, each cube holding `bytes` of shared memory, or the [`Refusal`] saying the device
141    /// gives one less: what the device gives a cube, less what it reserves for cube-scoped matrices.
142    pub fn holding(self, bytes: usize) -> Result<Self, Refusal> {
143        let hardware = &self.client.properties().hardware;
144        let most = hardware
145            .max_shared_memory_size
146            .saturating_sub(hardware.cube_mma_reserved_shared_memory);
147        match bytes > most {
148            true => Err(Refusal::SharedMemoryPastDevice { bytes, most }),
149            false => Ok(self),
150        }
151    }
152
153    pub fn client(&self) -> &Client {
154        &self.client
155    }
156
157    pub fn cube_count(&self) -> CubeCount {
158        self.cube_count.clone()
159    }
160
161    pub fn cube_dim(&self) -> CubeDim {
162        self.cube_dim
163    }
164
165    /// The concrete space: this launch's real extents.
166    pub fn space(&self) -> &Space {
167        &self.concrete
168    }
169
170    /// The partitioning this launch states, its space in kernel form.
171    pub fn partitioning(&self) -> &Partitioning {
172        &self.partitioning
173    }
174
175    /// The kernel's partitioning argument, dynamic extents sized by this launch.
176    pub fn partitioning_arg(&self) -> PartitioningLaunch {
177        PartitioningLaunch::new(self.space_arg(), self.partitioning.levels().to_vec())
178    }
179
180    /// The kernel-form space as a kernel argument.
181    fn space_arg(&self) -> SpaceLaunch {
182        let kernel = self.partitioning.space();
183        let mut sizes = SequenceArg::new();
184        if !kernel.is_static() {
185            for axis in kernel.axes() {
186                sizes.push(self.concrete.extent(axis));
187            }
188        }
189        SpaceLaunch::new(kernel.shape().clone(), sizes)
190    }
191
192    /// The axes some tile reaches past the end of, whose accesses are masked.
193    pub(crate) fn overhangs(&self) -> &[Axis] {
194        &self.overhanging
195    }
196
197    /// The axes some tile reaches past the end of at some extent: this launch's
198    /// [`overhangs`](Self::overhangs), and every axis the kernel-form space leaves dynamic that a
199    /// level cuts in tiles wider than one or launches in an order that can overrun it. A tile of
200    /// one divides every extent.
201    pub(crate) fn may_overhang(&self) -> Vec<Axis> {
202        let kernel = self.partitioning.space();
203        let levels = self.partitioning.levels();
204        let mut axes = self.overhanging.to_vec();
205        for axis in kernel.axes().filter(|&axis| kernel.is_dynamic(axis)) {
206            let cut_short = levels.iter().any(|level| {
207                level
208                    .tile(axis)
209                    .is_some_and(|tile| tile > 1 || level.launch_order().is_some())
210            });
211            if cut_short && !axes.contains(&axis) {
212                axes.push(axis);
213            }
214        }
215        axes
216    }
217
218    /// The leaf edge along `axis`: the stated tile, or the whole extent where none was.
219    fn leaf_edge(&self, axis: Axis) -> usize {
220        self.leaf
221            .extents()
222            .iter()
223            .find(|&&(a, _)| a == axis)
224            .map(|&(_, edge)| edge)
225            .unwrap_or_else(|| self.concrete.extent(axis))
226    }
227
228    /// Bind `binding`, whose buffer holds `element`, as an operand of this launch: for a
229    /// [`packed`](Arg::packed) one, the words its fields are stored in.
230    pub fn arg(&self, binding: TensorBinding, element: ElemType) -> Arg<'_, Unlabelled> {
231        Arg::bound(self, binding, element)
232    }
233
234    /// [`arg`](Self::arg) over a stated geometry, for an operand with no tensor; end it with
235    /// [`build_spec`](Arg::build_spec).
236    pub fn unbound(&self, geometry: &Geometry) -> Arg<'_, Unlabelled> {
237        Arg::unbound(self, geometry)
238    }
239
240    /// The widest line every operand can be served in along `axis`, which must label each
241    /// operand's innermost dim.
242    pub fn vector_size(
243        &self,
244        axis: Axis,
245        operands: &[(&Geometry, &[Axis])],
246        type_size: usize,
247    ) -> usize {
248        for (_, axes) in operands {
249            assert_eq!(
250                axes.last(),
251                Some(&axis),
252                "Launcher::vector_size: axis {axis:?} must label each operand's innermost dim"
253            );
254        }
255        // A masked access counts in lines and would clip wrongly, so overhangs serve scalar.
256        let overhangs = self.overhangs();
257        let masked = operands
258            .iter()
259            .any(|(_, axes)| axes.iter().any(|a| overhangs.contains(a)));
260        if masked {
261            return 1;
262        }
263        let leaf = self.leaf_edge(axis);
264        self.client
265            .io_optimized_vector_sizes(type_size)
266            .filter(|&v| {
267                leaf.is_multiple_of(v)
268                    && operands
269                        .iter()
270                        .all(|(g, axes)| g.serves(&[(axis, v)], axes).is_ok())
271            })
272            .max()
273            .unwrap_or(1)
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    /// A grid with a zero axis is refused, not launched: cubecl's CUDA server returns without
282    /// dispatching one, so the launch would answer with an uncomputed buffer. Every axis, since
283    /// the count is read off whichever axis a level put there.
284    #[test]
285    fn a_grid_with_a_zero_axis_is_refused() {
286        let hardware = HardwareProperties {
287            load_width: 128,
288            vector_register_count: None,
289            plane_size_min: 32,
290            plane_size_max: 32,
291            max_bindings: 30,
292            max_shared_memory_size: 32 * 1024,
293            max_cube_count: (u32::MAX, u32::MAX, u32::MAX),
294            max_units_per_cube: 1024,
295            max_cube_dim: (1024, 1024, 1024),
296            num_streaming_multiprocessors: None,
297            num_cpu_cores: None,
298            last_level_cache_size: None,
299            num_tensor_cores: None,
300            min_tensor_cores_dim: None,
301            max_vector_size: 4,
302            cube_mma_reserved_shared_memory: 0,
303        };
304        let cube_dim = CubeDim::new_2d(32, 1);
305        for count in [
306            CubeCount::Static(0, 8, 1),
307            CubeCount::Static(64, 0, 1),
308            CubeCount::Static(64, 8, 0),
309        ] {
310            assert_eq!(
311                spawnable(&hardware, cube_dim, &count),
312                Err(Refusal::NoCubes),
313                "{count:?}"
314            );
315        }
316        assert_eq!(
317            spawnable(&hardware, cube_dim, &CubeCount::Static(64, 8, 1)),
318            Ok(())
319        );
320    }
321}