Skip to main content

cubecl_tile/launch/binding/
base.rs

1//! The [`Arg`] builder, which turns a launched tensor into an operand.
2
3use core::marker::PhantomData;
4
5use cubecl::zspace::{SmallVec, Tiling};
6
7use cubecl::ir::ElemType;
8use cubecl::prelude::*;
9use cubecl::server::BufferBinding;
10
11use super::{boundaries::Boundaries, derivation::Derivation, labels::Labels};
12use crate::BoundaryPolicy;
13use crate::{
14    Axis, Delivery, Extent, Field, Geometry, Launcher, LineMisfit, Packing, Projection, Refusal,
15    Space, Storage, StoragePartitioning, TileArgLaunch, TileSpec, VectorTile,
16};
17
18/// Typestate marker: the operand's axes are not yet stated.
19pub struct Unlabelled;
20/// Typestate marker: the operand's axes are stated.
21pub struct Labelled;
22
23/// What the builder accumulates.
24pub(crate) struct ArgData<'a> {
25    pub(crate) launch: &'a Launcher,
26    /// The tensor this operand is read from and the element its buffer holds; `None` for a
27    /// destination with no address.
28    pub(crate) binding: Option<(TensorBinding, ElemType)>,
29    pub(crate) geometry: Geometry,
30    pub(crate) axes: &'a [Axis],
31    pub(crate) batches: &'a [Axis],
32    /// The operand's own affine mapping; `None` derives it from the labelled dims.
33    pub(crate) projection: Option<Projection>,
34    pub(crate) width: usize,
35    /// The stated policy; `None` derives one from the overhangs.
36    pub(crate) boundary: Option<BoundaryPolicy>,
37    pub(crate) packing: Packing,
38    /// Who moves the operand into a stage ([`delivery`](Arg::delivery)).
39    pub(crate) delivery: Delivery,
40    /// Whether the labelled dims bind in stride order ([`in_stride_order`](Arg::in_stride_order)).
41    pub(crate) in_stride_order: bool,
42}
43
44/// One operand of a launch, being described; `S` says whether its axes are stated yet.
45pub struct Arg<'a, S> {
46    data: ArgData<'a>,
47    _state: PhantomData<S>,
48}
49
50impl<'a> Arg<'a, Unlabelled> {
51    pub(crate) fn bound(launch: &'a Launcher, binding: TensorBinding, element: ElemType) -> Self {
52        let geometry = Geometry::from(&binding);
53        Self::over(launch, Some((binding, element)), geometry)
54    }
55
56    pub(crate) fn unbound(launch: &'a Launcher, geometry: &Geometry) -> Self {
57        Self::over(launch, None, geometry.clone())
58    }
59
60    fn over(
61        launch: &'a Launcher,
62        binding: Option<(TensorBinding, ElemType)>,
63        geometry: Geometry,
64    ) -> Self {
65        Arg {
66            data: ArgData {
67                launch,
68                binding,
69                geometry,
70                axes: &[],
71                batches: &[],
72                projection: None,
73                width: 1,
74                boundary: None,
75                packing: Packing::Plain,
76                delivery: Delivery::SyncPerUnit,
77                in_stride_order: false,
78            },
79            _state: PhantomData,
80        }
81    }
82
83    /// The axes the operand's trailing buffer dims carry.
84    pub fn axes(mut self, axes: &'a [Axis]) -> Arg<'a, Labelled> {
85        self.data.axes = axes;
86        self.labelled()
87    }
88
89    /// An explicit affine [`Projection`] for a gathered operand. Overruns past the buffer's tail
90    /// are not detected; a gather that overruns must state [`BoundaryPolicy::Every`].
91    pub fn gathered(mut self, projection: Projection) -> Arg<'a, Labelled> {
92        self.data.projection = Some(projection);
93        self.labelled()
94    }
95
96    fn labelled(self) -> Arg<'a, Labelled> {
97        Arg {
98            data: self.data,
99            _state: PhantomData,
100        }
101    }
102}
103
104impl<'a> Arg<'a, Labelled> {
105    /// The batch axes in the output's order, right-aligned to the leading dims (numpy broadcast).
106    pub fn batches(mut self, axes: &'a [Axis]) -> Self {
107        self.data.batches = axes;
108        self
109    }
110
111    /// Bind the labelled dims in the order they step, coarsest first; batch dims stay put.
112    pub fn in_stride_order(mut self) -> Self {
113        self.data.in_stride_order = true;
114        self
115    }
116
117    /// Serve the innermost axis in `width`-wide lines; that axis must be contiguous.
118    pub fn vectorize(mut self, width: usize) -> Self {
119        self.data.width = width;
120        self
121    }
122
123    /// How this operand's edges are checked.
124    pub fn boundary(mut self, policy: BoundaryPolicy) -> Self {
125        self.data.boundary = Some(policy);
126        self
127    }
128
129    /// This operand's values are `field`-wide fields of a stored word.
130    pub fn packed(mut self, field: impl Into<Field>) -> Self {
131        self.data.packing = Packing::Packed {
132            field: field.into(),
133        };
134        self
135    }
136
137    /// Who moves this operand into a stage ([`SyncPerUnit`](Delivery::SyncPerUnit) unless
138    /// stated); the kernel is the same either way.
139    pub fn delivery(mut self, delivery: Delivery) -> Self {
140        self.data.delivery = delivery;
141        self
142    }
143
144    /// The operand, bound, or the [`Refusal`] saying why it cannot be.
145    pub fn build(self) -> Result<Bound, Refusal> {
146        self.realize().map(|(bound, _)| bound)
147    }
148
149    /// [`build`](Self::build) without the tensor argument, for an operand with no address.
150    pub fn build_spec(self) -> Result<Unbound, Refusal> {
151        let (bound, geometry) = self.realize()?;
152        Ok(Unbound {
153            spec: bound.spec,
154            vector_size: bound.vector_size,
155            geometry,
156        })
157    }
158
159    /// The derivation both builds share, looked up where an earlier launch already made it
160    /// ([`Derivation`]): it reads nothing a launch changes but the tensor's address.
161    fn realize(self) -> Result<(Bound, Geometry), Refusal> {
162        let packing = self.data.packing.read_on(self.data.launch.client());
163        let stored = self
164            .data
165            .binding
166            .as_ref()
167            .map(|(b, _)| b.tiling)
168            .unwrap_or_default();
169        let derived = Derivation::view(&self.data, packing, stored)
170            .derived(|| self.derive(packing, stored))?;
171        let tensor = self.data.binding.map(|(mut binding, element)| {
172            // Its tiling restated over the settled geometry's dims.
173            binding.shape = derived.geometry.shape().into();
174            binding.strides = derived.geometry.strides().into();
175            binding.tiling = derived.geometry.tiling();
176            (binding.into_tensor_arg(), element)
177        });
178        let bound = Bound {
179            tensor,
180            vector_size: self.data.width,
181            spec: derived.spec,
182        };
183        Ok((bound, derived.geometry))
184    }
185
186    /// The derivation itself, its packing read on the device as `packing` and its buffer stored as
187    /// `stored`.
188    fn derive(&self, packing: Packing, stored: Tiling) -> Result<Derived, Refusal> {
189        let data = &self.data;
190        let launch = data.launch;
191        let width = data.width;
192        let stated = data.projection.is_some() || stored.is_tiled();
193        let (geometry, axes) = match data.in_stride_order {
194            true if stated => return Err(Refusal::StrideOrderOfAStatedLayout),
195            true => data.geometry.clone().in_stride_order(data.axes),
196            false => (data.geometry.clone(), data.axes.to_vec()),
197        };
198        let (storage, stored_tiles) = self.storage(&geometry, &axes);
199        let labels = match data.projection.clone() {
200            Some(projection) => {
201                Labels::stated(&geometry, launch, projection, &axes, data.batches, stored)?
202            }
203            None => Labels::new(&geometry, &axes, data.batches)?,
204        };
205        let Labels {
206            geometry,
207            projection,
208            addressed,
209        } = labels;
210        projection.validate(width)?;
211        // A stated width is checked here: `stride / width` would truncate silently.
212        // A line runs along the innermost dim's axis; a buffer with no labelled dim has none.
213        let labels = projection.dense_labels();
214        let served = match labels.last() {
215            Some(&axis) => VectorTile::new(&stored_tiles, axis, width)
216                .map_err(LineMisfit::Tile)
217                .and_then(|tile| geometry.serves(tile.extents(), &labels)),
218            None if width == 1 => Ok(()),
219            None => Err(LineMisfit::NoDims),
220        };
221        served.map_err(|why| Refusal::WidthNotServed { width, why })?;
222        let may_overhang;
223        let overhangs = match data.boundary {
224            Some(BoundaryPolicy::AnyExtent(_)) => {
225                may_overhang = launch.may_overhang();
226                &may_overhang
227            }
228            _ => launch.overhangs(),
229        };
230        let boundaries = Boundaries::new(
231            data.boundary,
232            &projection,
233            &addressed,
234            launch.space(),
235            overhangs,
236            width,
237        )?;
238        let spec = TileSpec {
239            projection,
240            boundaries: boundaries.modes,
241            units: launch.cube_dim().num_elems() as usize,
242            packing,
243            storage,
244            stored_tiles,
245            delivery: Delivery::SyncPerUnit,
246            paging: None,
247        }
248        .delivery(data.delivery);
249        Ok(Derived { spec, geometry })
250    }
251
252    /// The coarsest level whose windows the operand's storage tiles address with one stride per
253    /// axis (one contiguous run each), every other window walked through the layout; and the stored
254    /// tiles themselves, finest first ([`TileSpec::stored_tiles`]).
255    fn storage(&self, geometry: &Geometry, axes: &[Axis]) -> (Storage, Vec<(Axis, usize)>) {
256        let launch = self.data.launch;
257        if !(self.data.projection.is_none() && geometry.tiling().is_tiled()) {
258            return (Storage::Strided, Vec::new());
259        }
260        let labels = geometry.labels(axes);
261        let partitioning = StoragePartitioning::new(geometry, &labels);
262        let stored_tiles = partitioning
263            .as_ref()
264            .map_or_else(Vec::new, |storage| storage.tiles().to_vec());
265        let tiles = partitioning
266            .map(|storage| storage.contiguous_tiles(&geometry.extents(&labels)))
267            .unwrap_or_default();
268        // Each level's window along the operand's axes, outermost first: the extent this launch
269        // cuts it to, and whether the kernel hands it down dynamic. A level that cuts an axis gives
270        // it its tile; one that does not passes the window above it down.
271        let (space, kernel) = (launch.space(), launch.partitioning().space());
272        let mut window: LevelWindow = axes
273            .iter()
274            .map(|&axis| (space.extent_raw(axis), kernel.is_dynamic(axis)))
275            .collect();
276        let windows: SmallVec<[LevelWindow; 8]> = launch
277            .partitioning()
278            .levels()
279            .iter()
280            .map(|level| {
281                for (&axis, cut) in axes.iter().zip(window.iter_mut()) {
282                    if let Some(tile) = level.tile(axis) {
283                        *cut = (Extent::Static(tile), false);
284                    }
285                }
286                window.clone()
287            })
288            .collect();
289        // A tile the kernel hands down dynamic is no window a storage tile can be, whatever this
290        // launch's extent makes it.
291        let cuts_to = |window: &[(Extent, bool)], tile: &[(Axis, usize)]| {
292            axes.iter().zip(window).all(|(&axis, &(extent, dynamic))| {
293                let stored = tile
294                    .iter()
295                    .find(|&&(a, _)| a == axis)
296                    .map_or(1, |&(_, e)| e);
297                !dynamic && extent == Extent::Static(stored)
298            })
299        };
300        let level = tiles
301            .iter()
302            .rev()
303            .find_map(|tile| windows.iter().position(|window| cuts_to(window, tile)));
304        (Storage::Tiled(level), stored_tiles)
305    }
306}
307
308/// What [`Arg::realize`] derives from an operand's labels and its launch: the comptime spec and the
309/// geometry the tensor is bound at, stride order settled.
310#[derive(Clone)]
311pub(crate) struct Derived {
312    pub(crate) spec: TileSpec,
313    pub(crate) geometry: Geometry,
314}
315
316/// A bound operand: its tensor argument and the element its buffer holds, comptime [`TileSpec`] and
317/// served width.
318pub struct Bound {
319    tensor: Option<(TensorArg, ElemType)>,
320    /// Served width (values per line); a packed binding is narrower by the packing factor.
321    pub vector_size: usize,
322    pub spec: TileSpec,
323}
324
325impl Bound {
326    /// This operand bound again over `buffers`, another buffer of the same layout: the tensor it
327    /// holds takes the next one and keeps the shape, strides and tiling it was settled at. A launch
328    /// that binds the same operands every step keeps its binding and changes only the addresses.
329    ///
330    /// `None` where `buffers` runs out, or where the operand aliases another input.
331    pub fn rebound(&self, buffers: &mut impl Iterator<Item = BufferBinding>) -> Option<Self> {
332        let tensor = match &self.tensor {
333            Some((tensor, element)) => Some((rebound_tensor(tensor, buffers.next()?)?, *element)),
334            None => None,
335        };
336        Some(Self {
337            tensor,
338            vector_size: self.vector_size,
339            spec: self.spec.clone(),
340        })
341    }
342
343    /// The operand as the kernel's [`TileArg`](crate::TileArg) launch argument.
344    pub fn arg(self) -> TileArgLaunch {
345        let width = self.bound_width();
346        let (tensor, element) = self
347            .tensor
348            .expect("Bound::arg: this operand was built over geometry alone and has no tensor");
349        TileArgLaunch::new(tensor, element, width, self.spec)
350    }
351
352    /// The tensor argument itself; panics for an operand built over geometry alone.
353    pub fn tensor(self) -> TensorArg {
354        self.tensor
355            .expect("Bound::tensor: this operand was built over geometry alone and has no tensor")
356            .0
357    }
358
359    /// The width the binding is declared at, narrower than `vector_size` when packed: a packed
360    /// binding served fewer values a line than a word holds is still read a whole word at a time.
361    pub fn bound_width(&self) -> usize {
362        self.spec.packing.physical(self.vector_size).max(1)
363    }
364}
365
366/// What [`build_spec`](Arg::build_spec) settles for an operand with no tensor to bind.
367#[derive(Clone)]
368pub struct Unbound {
369    pub spec: TileSpec,
370    /// Served width (values per line).
371    pub vector_size: usize,
372    pub geometry: Geometry,
373}
374
375/// One level's window along an operand's axes, in their order: the extent the launch cuts each axis
376/// to, and whether the kernel hands it down dynamic.
377type LevelWindow = SmallVec<[(Extent, bool); Space::MAX_RANK]>;
378
379/// `tensor` over `buffer` instead of its own: the shape, strides and tiling it was settled at kept.
380/// `None` for an alias of another input, which holds no buffer.
381pub(crate) fn rebound_tensor(tensor: &TensorArg, buffer: BufferBinding) -> Option<TensorArg> {
382    match tensor {
383        TensorArg::Handle { handle } => Some(TensorArg::Handle {
384            handle: TensorBinding {
385                handle: buffer,
386                strides: handle.strides.clone(),
387                shape: handle.shape.clone(),
388                tiling: handle.tiling,
389            },
390        }),
391        TensorArg::Alias { .. } => None,
392    }
393}