Skip to main content

cubecl_tile/launch/arg/
tensor.rs

1//! An operand as a single launch argument: its values, a buffer or a TMA tensor map, its comptime
2//! [`TileSpec`], the element it serves them at, the runtime half of its projection, and what its
3//! values are read through: the table that places them, the table they index and the scales they
4//! are under, each a [`TileArg`] of its own.
5
6use std::marker::PhantomData;
7use std::sync::Arc;
8
9use cubecl::frontend::{AsMutExpand, AsRefExpand, CubeDebug, ExpandTypeClone, IntoExpand, IntoMut};
10use cubecl::frontend::{buffer_len::expand_buffer_length_native, slice};
11use cubecl::ir::pliron::value::Value;
12use cubecl::ir::{ElemType, Scope, types::VectorType};
13use cubecl::prelude::*;
14use cubecl::unexpanded;
15use cubecl_std::tensor::{
16    layout::{CoordsDyn, VirtualLayout, VirtualLayoutExpand},
17    view::launch::{VirtualViewLayoutCompilationArg, VirtualViewLayoutLaunch},
18};
19
20use crate::launch::binding::rebound_tensor;
21use crate::tile::packing::base::Packing;
22use crate::*;
23use cubecl::server::BufferBinding;
24
25/// An operand as a single launch argument: its values, a buffer or a TMA tensor map, its comptime
26/// [`TileSpec`], the element its buffer holds, and the scales they are read under.
27///
28/// The element is data the argument carries, stated where the operand is bound
29/// ([`TileArgLaunch::new`]), not a type of the kernel's: what the buffer holds, `width` of them a
30/// line. A packed buffer holds `u32` words, its spec's packing naming the fields in them; a reader
31/// decodes them into the element it asks for ([`Tile::dequantized`]), or `f32` where it asks for
32/// none.
33///
34/// Scales are levels innermost first, so each covers more than the one inside it: NVFP4's values
35/// carry their block scales, which carry the per-tensor scale. A plain level holding `u32` is words
36/// of block scales, which only the block-scaled instruction reads; any other, a packed one
37/// included, is scales read as numbers.
38///
39/// Values stored as indices ([`Field::Index`]) read through the table they index, their codebook
40/// ([`TileArgLaunch::codebook`]); a gather's runtime coefficients and offsets
41/// ([`TileArgLaunch::runtime_map`]) ride the argument too, as does the table that places their
42/// windows ([`TileArgLaunch::indexed`]), a routed product's experts or a pool's pages. A kernel
43/// serves every operand the same way, with [`tile`](TileArg::tile).
44pub struct TileArg<'a> {
45    pub spec: TileSpec,
46    /// The element the buffer holds.
47    pub element: ElemType,
48    _values: PhantomData<&'a ()>,
49}
50
51impl TileArg<'_> {
52    /// Serve the operand as a [`Tile`] over the kernel's one `partitioning`, under its scales.
53    pub fn tile(&self, _partitioning: &Partitioning) -> Tile {
54        unexpanded!()
55    }
56
57    /// The buffer as lines of `V` of `E`, which must be the element it holds and the width it is
58    /// bound at: what a write that adds into a line reads and writes it as. A launch that bound
59    /// the buffer at another element is refused as the kernel expands.
60    pub fn lines<E: Numeric, V: Size>(&self) -> &Tensor<Vector<E, V>> {
61        unexpanded!()
62    }
63}
64
65/// A buffer of values, served at `E`, its projection's runtime half `coefficients` and `offsets`.
66#[cube]
67fn buffer_tile<E: Numeric>(
68    tensor: &Tensor<E>,
69    partitioning: &Partitioning,
70    coefficients: Coords<u32>,
71    offsets: Coords<i32>,
72    #[comptime] spec: TileSpec,
73) -> Tile {
74    let space = comptime!(partitioning.clone());
75    GlobalOperand::gathered(
76        tensor,
77        comptime!(space.space().clone()),
78        spec,
79        coefficients,
80        offsets,
81    )
82    .tile::<E>(comptime!(space.levels().to_vec()))
83}
84
85/// A codebook's table, `entries` long along its one axis, outside the kernel's space.
86#[cube]
87fn table_tile<E: Numeric>(
88    tensor: &Tensor<E>,
89    #[comptime] entries: usize,
90    #[comptime] spec: TileSpec,
91) -> Tile {
92    let space = comptime!(Space::new(&[(spec.axes()[0], entries)]));
93    GlobalOperand::tensor(tensor, comptime!(space.clone()), spec).tile::<E>(comptime!(Vec::new()))
94}
95
96/// [`buffer_tile`] over a pool of pages, read as the sequences `table` names.
97#[cube]
98fn paged_tile<E: Numeric>(
99    tensor: &Tensor<E>,
100    table: &Tensor<u32>,
101    partitioning: &Partitioning,
102    #[comptime] spec: TileSpec,
103) -> Tile {
104    let space = comptime!(partitioning.clone());
105    GlobalOperand::paged(tensor, table, comptime!(space.space().clone()), spec)
106        .tile::<E>(comptime!(space.levels().to_vec()))
107}
108
109/// A tensor map, served as a TMA-filled tile.
110#[cube]
111fn tensor_map_tile<E: Numeric>(
112    map: TensorMap<E, Tiled>,
113    layout: VirtualLayout<CoordsDyn, Sequence<i32>>,
114    partitioning: &Partitioning,
115    #[comptime] spec: TileSpec,
116) -> Tile {
117    let space = comptime!(partitioning.clone());
118    let own = comptime!(space.space().subspace(spec.axes()));
119    let data = TmaData::from_tensor_map(
120        map,
121        layout,
122        comptime!(own.rank()),
123        comptime!(spec.units),
124        comptime!(spec.packing),
125    );
126    Tile::new::<E>(
127        TileKind::new_TmaGmem(data),
128        comptime!(Placement::root(own.clone(), space.levels().to_vec())),
129    )
130}
131
132/// The device side of a [`TileArg`].
133#[derive(Clone)]
134pub struct TileArgExpand<'a> {
135    values: ValuesExpand<'a>,
136    pub spec: TileSpec,
137    /// The element the buffer holds.
138    pub element: ElemType,
139    /// The scales the values are read under: none, or a coarser operand carrying its own.
140    scale: Option<Box<TileArgExpand<'static>>>,
141    /// The table that places the values' windows, and what its entry places ([`Indexing`]).
142    index: Option<(Box<TileArgExpand<'static>>, Indexing)>,
143    /// The table the values index, where they are stored as indices.
144    codebook: Option<Box<TileArgExpand<'static>>>,
145    /// The projection's dynamic coefficients and divisors, then its dynamic offsets.
146    runtime_map: (CoordsExpand<u32>, CoordsExpand<i32>),
147}
148
149/// The device side of the values: a buffer, kept also as it was declared for
150/// [`lines`](TileArg::lines), or a tensor map and the layout landing a position in it.
151#[derive(Clone)]
152enum ValuesExpand<'a> {
153    Tensor {
154        /// The values, kept at [`Held`] and named at their element where a tile reads them.
155        tensor: &'a TensorExpand<Held>,
156        buffer: Value,
157        meta: TensorMetaExpand,
158        width: usize,
159    },
160    TensorMap {
161        map: Value,
162        layout: VirtualLayoutExpand<CoordsDyn, Sequence<i32>>,
163    },
164}
165
166impl<'a> TileArgExpand<'a> {
167    pub fn __expand_tile_method(
168        &self,
169        scope: &Scope,
170        partitioning: &PartitioningExpand,
171    ) -> TileExpand {
172        let values = self.values_tile(scope, partitioning);
173        let values = self.looking_up(scope, values);
174        let values = self.indexing(scope, values, partitioning);
175        self.scaling(scope, values, partitioning)
176    }
177
178    pub fn __expand_lines_method<E: Numeric, V: Size>(
179        &self,
180        scope: &Scope,
181    ) -> &'a TensorExpand<Vector<E, V>> {
182        let ValuesExpand::Tensor {
183            buffer,
184            meta,
185            width: bound,
186            ..
187        } = &self.values
188        else {
189            panic!("TileArg::lines: a tensor map has no lines to add into")
190        };
191        let named = E::elem_type(scope);
192        assert_eq!(
193            named, self.element,
194            "TileArg::lines: the buffer is bound at {}, not {named}",
195            self.element
196        );
197        let width = V::__expand_value(scope);
198        assert_eq!(
199            width, *bound,
200            "TileArg::lines: the buffer is bound {bound} wide, not {width}"
201        );
202        let len = expand_buffer_length_native(scope, *buffer);
203        let lines = slice::from_raw_parts::<Vector<E, V>>(
204            scope,
205            *buffer,
206            0usize.into_expand(scope),
207            len.into(),
208        );
209        scope.create_kernel_ref(TensorExpand::__expand_from_parts(*meta, lines))
210    }
211
212    /// The values alone, under none of their scales: read through their table where they are a
213    /// pool of pages, where they lie otherwise.
214    fn values_tile(&self, scope: &Scope, partitioning: &PartitioningExpand) -> TileExpand {
215        let _held = scope.define_type::<Held>(self.element);
216        match &self.values {
217            ValuesExpand::Tensor { tensor, meta, .. } => {
218                let tensor = scope.create_kernel_ref(TensorExpand::<Held>::__expand_from_parts(
219                    *meta,
220                    tensor.expand.into(),
221                ));
222                if let (Some(_), Some((table, _))) = (&self.spec.paging, &self.index) {
223                    let ValuesExpand::Tensor {
224                        tensor: pages,
225                        meta,
226                        ..
227                    } = &table.values
228                    else {
229                        unreachable!("TileArgLaunch::indexed binds its table as a buffer")
230                    };
231                    let pages = scope.create_kernel_ref(TensorExpand::<u32>::__expand_from_parts(
232                        *meta,
233                        pages.expand.into(),
234                    ));
235                    return paged_tile::expand::<Held>(
236                        scope,
237                        tensor,
238                        pages,
239                        partitioning,
240                        self.spec.clone(),
241                    );
242                }
243                let (coefficients, offsets) = self.runtime_map.clone();
244                buffer_tile::expand::<Held>(
245                    scope,
246                    tensor,
247                    partitioning,
248                    coefficients,
249                    offsets,
250                    self.spec.clone(),
251                )
252            }
253            ValuesExpand::TensorMap { map, layout } => tensor_map_tile::expand::<Held>(
254                scope,
255                (*map).into(),
256                layout.clone(),
257                partitioning,
258                self.spec.clone(),
259            ),
260        }
261    }
262
263    /// `values` read through the table they index, where they are stored as indices.
264    fn looking_up(&self, scope: &Scope, values: TileExpand) -> TileExpand {
265        let Some(table) = &self.codebook else {
266            return values;
267        };
268        let ValuesExpand::Tensor { tensor, meta, .. } = &table.values else {
269            unreachable!("TileArgLaunch::codebook binds a buffer")
270        };
271        let Packing::Packed {
272            field: Field::Index { bits },
273        } = self.spec.packing
274        else {
275            unreachable!("TileArgLaunch::codebook binds values stored as indices")
276        };
277        let _held = scope.define_type::<Held>(table.element);
278        let entries = scope.create_kernel_ref(TensorExpand::<Held>::__expand_from_parts(
279            *meta,
280            tensor.expand.into(),
281        ));
282        let table = table_tile::expand::<Held>(scope, entries, 1 << bits, table.spec.clone());
283        values.__expand_lookup_method(scope, &table)
284    }
285
286    /// `values` carrying the table that places their windows, where the operand has one: read at
287    /// the first level of `partitioning` that cuts the table's keys ([`Indexing::read_at`]), or
288    /// now, at its first entry, where none does. A pool of pages reads its table as each window
289    /// is cut and each line is offset ([`values_tile`](Self::values_tile)), so holds none here.
290    fn indexing(
291        &self,
292        scope: &Scope,
293        values: TileExpand,
294        partitioning: &PartitioningExpand,
295    ) -> TileExpand {
296        let Some((table, indexing)) = &self.index else {
297            return values;
298        };
299        if self.spec.paging.is_some() {
300            return values;
301        }
302        let keys = table.spec.axes().to_vec();
303        let read_at = indexing
304            .read_at(&keys, &partitioning.levels)
305            .unwrap_or_else(|refusal| panic!("{refusal}"));
306        let ValuesExpand::Tensor { tensor, meta, .. } = &table.values else {
307            panic!("{}", Refusal::IndexTableNotU32)
308        };
309        let entries = scope.create_kernel_ref(TensorExpand::<u32>::__expand_from_parts(
310            *meta,
311            tensor.expand.into(),
312        ));
313        let table = IndexTable::__expand_new(scope, entries, keys, indexing.clone());
314        let values = values.__expand_with_index_method(scope, table);
315        match read_at {
316            Some(_) => values,
317            None => values.__expand_indexed_whole_method(scope),
318        }
319    }
320
321    /// `values` under these scales, a tile carrying its own scales in turn. A tensor map's values
322    /// land in a stage as they lie, so the scales on them are words of block scales, which a stage
323    /// keeps beside them; a scale above the words leaves the operand for the sum (`Tile::factor`)
324    /// before the stage is filled, and staging checks what is left.
325    fn scaling(
326        &self,
327        scope: &Scope,
328        values: TileExpand,
329        partitioning: &PartitioningExpand,
330    ) -> TileExpand {
331        let Some(level) = &self.scale else {
332            return values;
333        };
334        let words =
335            level.element == u32::elem_type_native() && level.spec.packing == Packing::Plain;
336        assert!(
337            words || !matches!(self.values, ValuesExpand::TensorMap { .. }),
338            "TileArg: a tensor map lands its values as they lie, so it rides only scales a stage \
339             keeps beside them: words of block scales"
340        );
341        values.__expand_mul_method(scope, &level.__expand_tile_method(scope, partitioning))
342    }
343}
344
345/// The host side of a [`TileArg`]: its values, its spec, the element its buffer holds and its
346/// scales. The spec is shared: a rebound operand and the compilation argument it registers as
347/// read the one its launch was settled with.
348pub struct TileArgLaunch {
349    values: ValuesLaunch,
350    spec: Arc<TileSpec>,
351    element: ElemType,
352    scale: Option<Box<TileArgLaunch>>,
353    index: Option<(Box<TileArgLaunch>, Indexing)>,
354    codebook: Option<Box<TileArgLaunch>>,
355    runtime_map: (Vec<u32>, Vec<i32>),
356}
357
358/// The host side of the values: a tensor and the width it is bound at, or a tensor map, boxed for
359/// being several times a tensor's size, and its layout.
360enum ValuesLaunch {
361    Tensor {
362        tensor: TensorArg,
363        width: usize,
364    },
365    TensorMap {
366        map: Box<TensorMapArg<Tiled>>,
367        layout: VirtualViewLayoutLaunch<CoordsDyn, Sequence<i32>, TensorMapArg<Tiled>>,
368    },
369}
370
371impl TileArgLaunch {
372    /// `tensor`, holding `element`, bound `width` wide, as `spec` reads it, under no scales.
373    pub fn new(tensor: TensorArg, element: ElemType, width: usize, spec: TileSpec) -> Self {
374        Self::of(ValuesLaunch::Tensor { tensor, width }, element, spec)
375    }
376
377    /// A tensor map landing through `layout`, holding the element its descriptor moves, as `spec`
378    /// reads it, under no scales.
379    pub(crate) fn of_tensor_map(
380        map: TensorMapArg<Tiled>,
381        layout: VirtualViewLayoutLaunch<CoordsDyn, Sequence<i32>, TensorMapArg<Tiled>>,
382        spec: TileSpec,
383    ) -> Self {
384        let element = map.metadata.elem_ty;
385        let map = Box::new(map);
386        Self::of(ValuesLaunch::TensorMap { map, layout }, element, spec)
387    }
388
389    fn of(values: ValuesLaunch, element: ElemType, spec: TileSpec) -> Self {
390        assert!(
391            spec.packing == Packing::Plain || element == u32::elem_type_native(),
392            "TileArgLaunch: a packed operand is bound at the u32 words its buffer holds, not \
393             {element:?}"
394        );
395        Self {
396            values,
397            spec: Arc::new(spec),
398            element,
399            scale: None,
400            index: None,
401            codebook: None,
402            runtime_map: (Vec::new(), Vec::new()),
403        }
404    }
405
406    /// This operand bound again over other buffers of the same layout: each tensor it holds, in the
407    /// order it holds them (its values, then its scales', then its index table's, then its
408    /// codebook's), takes the next
409    /// of `buffers` and keeps the shape, strides and tiling it was settled at. A launch that binds
410    /// the same operands every step keeps its binding and changes only the addresses.
411    ///
412    /// `None` for a tensor map, whose descriptor holds its buffer, and where `buffers` runs out.
413    pub fn rebound(&self, buffers: &mut impl Iterator<Item = BufferBinding>) -> Option<Self> {
414        let values = match &self.values {
415            ValuesLaunch::Tensor { tensor, width } => ValuesLaunch::Tensor {
416                tensor: rebound_tensor(tensor, buffers.next()?)?,
417                width: *width,
418            },
419            ValuesLaunch::TensorMap { .. } => return None,
420        };
421        let scale = match &self.scale {
422            Some(scale) => Some(Box::new(scale.rebound(buffers)?)),
423            None => None,
424        };
425        let index = match &self.index {
426            Some((table, indexing)) => Some((Box::new(table.rebound(buffers)?), indexing.clone())),
427            None => None,
428        };
429        let codebook = match &self.codebook {
430            Some(table) => Some(Box::new(table.rebound(buffers)?)),
431            None => None,
432        };
433        Some(Self {
434            values,
435            spec: self.spec.clone(),
436            element: self.element,
437            scale,
438            index,
439            codebook,
440            runtime_map: self.runtime_map.clone(),
441        })
442    }
443
444    /// These values, stored as indices ([`Field::Index`]), read as the entries of `table` they
445    /// name: a plain one-dim buffer of at least an entry per index, read one value a line.
446    ///
447    /// # Errors
448    ///
449    /// The [`Refusal`] for values not stored as indices, or a table that is not a plain one-dim
450    /// buffer holding an entry for every index.
451    pub fn codebook(mut self, table: TileArgLaunch) -> Result<Self, Refusal> {
452        let Packing::Packed {
453            field: Field::Index { bits },
454        } = self.spec.packing
455        else {
456            return Err(Refusal::CodebookOfValuesNotIndices);
457        };
458        let ValuesLaunch::Tensor { tensor, width } = &table.values else {
459            return Err(Refusal::CodebookTableNotPlain);
460        };
461        let plain = *width == 1
462            && tensor.shape().len() == 1
463            && table.spec.axes().len() == 1
464            && table.spec.packing == Packing::Plain
465            && table.scale.is_none()
466            && table.index.is_none()
467            && table.codebook.is_none();
468        if !plain {
469            return Err(Refusal::CodebookTableNotPlain);
470        }
471        let entries = tensor.shape()[0];
472        if entries < 1 << bits {
473            return Err(Refusal::CodebookTableShort { entries, bits });
474        }
475        self.codebook = Some(Box::new(table));
476        Ok(self)
477    }
478
479    /// These values read through their projection's runtime half: a value for each of its
480    /// dynamic coefficients and divisors (`Scale::Dynamic`, `Divisor::Dynamic`), physical axis
481    /// major, each axis's divisor last, and for each of its dynamic offsets (`Offset::Dynamic`).
482    /// Only how many there are is checked, as the kernel expands.
483    pub fn runtime_map(mut self, coefficients: Vec<u32>, offsets: Vec<i32>) -> Self {
484        self.runtime_map = (coefficients, offsets);
485        self
486    }
487
488    /// These values under `scale`, outside the scales they carry already: words of block scales
489    /// where `scale` is a plain level holding `u32`, scales read as numbers otherwise.
490    pub fn scales(mut self, scale: TileArgLaunch) -> Self {
491        self.scale = Some(Box::new(match self.scale {
492            None => scale,
493            Some(inner) => inner.scales(scale),
494        }));
495        self
496    }
497
498    /// These values with their windows placed by `table`, whose dims are its keys, as `indexing`
499    /// says ([`Indexing`]). The scales they carry move with them wherever they span the placed
500    /// axis; state them first, so they are checked here.
501    ///
502    /// A table keyed by the axis it places and one more, `[sequence, position]` placing the
503    /// sequence, reads these values as a pool of pages: the dim the sequence labels holds pages,
504    /// the one the position labels a page's positions, and sequence `b`'s position `s` lies in page
505    /// `table[b, s / page_size]`, `page_size` the pool's extent along the position. The operand's
506    /// extents along the two axes are the table's, checked on every read. The table is read as
507    /// each window is cut, a window holding one page or two reading it once. The scales of a pool
508    /// spanning either axis are a pool of their own, placed by the same table before they are
509    /// stated.
510    ///
511    /// # Errors
512    ///
513    /// The [`Refusal`] for a table that is not plain `u32` entries over one dim per key, or for
514    /// values, or a scale level spanning the placed axis, whose windows an entry cannot place.
515    pub fn indexed(self, table: TileArgLaunch, indexing: Indexing) -> Result<Self, Refusal> {
516        self.placed_by(table, indexing, false)
517    }
518
519    /// [`indexed`](Self::indexed), the table a pool of pages is read through stated to cover the
520    /// launch's extents along both its axes, so a read the launch keeps in bounds is in bounds
521    /// through the table: the two axes are checked only where the launch overhangs, as a buffer
522    /// read where it lies is. A table that stops short reads its clamped entries there: other
523    /// positions of the pool, never past it.
524    ///
525    /// # Errors
526    ///
527    /// As [`indexed`](Self::indexed).
528    pub fn indexed_covering(
529        self,
530        table: TileArgLaunch,
531        indexing: Indexing,
532    ) -> Result<Self, Refusal> {
533        self.placed_by(table, indexing, true)
534    }
535
536    /// [`indexed`](Self::indexed), a pool's table stated to cover the launch where `covering`.
537    fn placed_by(
538        mut self,
539        table: TileArgLaunch,
540        indexing: Indexing,
541        covering: bool,
542    ) -> Result<Self, Refusal> {
543        let ValuesLaunch::Tensor { tensor, width } = &table.values else {
544            return Err(Refusal::IndexTableNotU32);
545        };
546        let plain_u32 = table.element == u32::elem_type_native()
547            && *width == 1
548            && table.spec.packing == Packing::Plain
549            && table.scale.is_none()
550            && table.index.is_none();
551        if !plain_u32 {
552            return Err(Refusal::IndexTableNotU32);
553        }
554        let keys = table.spec.axes();
555        let rank = tensor.shape().len();
556        if rank != keys.len() {
557            return Err(Refusal::IndexTableRank {
558                keys: keys.len(),
559                rank,
560            });
561        }
562        if let (Indexing::Coordinate(sequence), &[first, position]) = (&indexing, keys)
563            && first == *sequence
564        {
565            self.spec = Arc::new(self.paged_spec(*sequence, position, covering)?);
566            let mut level = self.scale.as_deref();
567            while let Some(scale) = level {
568                let spans = |axis| scale.spec.axes().contains(&axis);
569                if (spans(first) || spans(position)) && scale.spec.paging.is_none() {
570                    return Err(Refusal::ScalesOfAPoolUnpaged);
571                }
572                level = scale.scale.as_deref();
573            }
574            self.index = Some((Box::new(table), indexing));
575            return Ok(self);
576        }
577        indexing.validate(&self.spec, self.indexed_width()?, keys)?;
578        let mut level = self.scale.as_deref();
579        while let Some(scale) = level {
580            if scale.spec.axes().contains(&indexing.axis()) {
581                indexing.places(&scale.spec, scale.indexed_width()?)?;
582            }
583            level = scale.scale.as_deref();
584        }
585        self.index = Some((Box::new(table), indexing));
586        Ok(self)
587    }
588
589    /// This operand's spec read as a pool of pages along `sequence` and `position`: its paging, and
590    /// unless the table `covering` the launch, the two axes checked on every read, their extents
591    /// being the table's.
592    fn paged_spec(
593        &self,
594        sequence: Axis,
595        position: Axis,
596        covering: bool,
597    ) -> Result<TileSpec, Refusal> {
598        let ValuesLaunch::Tensor { tensor, .. } = &self.values else {
599            return Err(Refusal::PagingMovedByRuns);
600        };
601        let mut spec = TileSpec::clone(&self.spec);
602        let paging = Paging::of_pool(
603            sequence,
604            position,
605            tensor.shape(),
606            &spec.projection,
607            &spec.stored_tiles,
608            spec.delivery,
609        )?;
610        if !covering {
611            let boundary = spec.boundaries.iter().flatten().next().copied();
612            if spec.boundaries.is_empty() {
613                spec.boundaries = (0..spec.projection.coordinate_rank())
614                    .map(|_| None)
615                    .collect();
616            }
617            let (sequence_dim, position_dim) = paging.dims(&spec.projection)?;
618            for dim in [sequence_dim, position_dim] {
619                spec.boundaries[dim] = Some(boundary.unwrap_or(Boundary::Zero));
620            }
621        }
622        spec.paging = Some(paging);
623        Ok(spec)
624    }
625
626    /// The values a line of this buffer holds, where an entry may move its window: a tensor map
627    /// lands boxes where they lie, and is never indexed.
628    fn indexed_width(&self) -> Result<usize, Refusal> {
629        match &self.values {
630            ValuesLaunch::Tensor { width, .. } => Ok(*width),
631            ValuesLaunch::TensorMap { .. } => Err(Refusal::IndexOfAStatedLayout),
632        }
633    }
634}
635
636/// What a [`TileArg`] compiles to: its values', its spec, its element and its scales'.
637#[derive(Clone, PartialEq, Eq, Hash, Debug)]
638pub struct TileArgCompilationArg {
639    values: ValuesCompilationArg,
640    spec: Arc<TileSpec>,
641    element: ElemType,
642    scale: Option<Box<TileArgCompilationArg>>,
643    index: Option<(Box<TileArgCompilationArg>, Indexing)>,
644    codebook: Option<Box<TileArgCompilationArg>>,
645    /// How many dynamic coefficients and offsets the runtime map holds.
646    runtime_map: (usize, usize),
647}
648
649/// What the values compile to: a buffer's and the width it is bound at, or a tensor map's layout.
650#[derive(Clone, PartialEq, Eq, Hash, Debug)]
651enum ValuesCompilationArg {
652    Tensor {
653        tensor: TensorCompilationArg,
654        width: usize,
655    },
656    TensorMap(VirtualViewLayoutCompilationArg<CoordsDyn, Sequence<i32>>),
657}
658
659impl<'a> CubeType for TileArg<'a> {
660    type ExpandType = TileArgExpand<'a>;
661}
662
663impl LaunchArg for TileArg<'static> {
664    type RuntimeArg = TileArgLaunch;
665    type CompilationArg = TileArgCompilationArg;
666
667    fn register(arg: Self::RuntimeArg, launcher: &mut KernelLauncher) -> Self::CompilationArg {
668        let TileArgLaunch {
669            values,
670            spec,
671            element,
672            scale,
673            index,
674            codebook,
675            runtime_map: (coefficients, offsets),
676        } = arg;
677        let values = match values {
678            ValuesLaunch::Tensor { tensor, width } => {
679                let line_size = element.size() * width;
680                let meta = TensorMetaLaunch::new(tensor.size() / width, tensor.shape().len());
681                let buffer = match &tensor {
682                    TensorArg::Handle { .. } => BufferCompilationArg { inplace: None },
683                    TensorArg::Alias { input_pos, .. } => BufferCompilationArg {
684                        inplace: Some(*input_pos),
685                    },
686                };
687                launcher.register_tensor(tensor, line_size);
688                let meta = TensorMeta::register(meta, launcher);
689                ValuesCompilationArg::Tensor {
690                    tensor: TensorCompilationArg { meta, buffer },
691                    width,
692                }
693            }
694            ValuesLaunch::TensorMap { map, layout } => {
695                let layout = layout.register(&map, Type::new(element), launcher);
696                launcher.register_tensor_map(*map, element.size());
697                ValuesCompilationArg::TensorMap(layout)
698            }
699        };
700        let scale = scale.map(|level| Box::new(TileArg::register(*level, launcher)));
701        let index = index
702            .map(|(table, indexing)| (Box::new(TileArg::register(*table, launcher)), indexing));
703        let codebook = codebook.map(|table| Box::new(TileArg::register(*table, launcher)));
704        for coefficient in &coefficients {
705            coefficient.register(launcher);
706        }
707        for offset in &offsets {
708            offset.register(launcher);
709        }
710        TileArgCompilationArg {
711            values,
712            spec,
713            element,
714            scale,
715            index,
716            codebook,
717            runtime_map: (coefficients.len(), offsets.len()),
718        }
719    }
720
721    fn expand(arg: &Self::CompilationArg, builder: &mut KernelBuilder) -> TileArgExpand<'static> {
722        let values = match &arg.values {
723            ValuesCompilationArg::Tensor { tensor, width } => {
724                let buffer = match tensor.buffer.inplace {
725                    Some(id) => builder.inplace(id),
726                    None => {
727                        let scope = &builder.scope;
728                        let held = arg.element.to_type(scope.ctx());
729                        let line = match *width {
730                            1 => held,
731                            width => VectorType::get(scope.ctx(), held, width).into(),
732                        };
733                        builder.tensor(line)
734                    }
735                };
736                let meta = TensorMeta::expand(&tensor.meta, builder);
737                let scope = &builder.scope;
738                let _held = scope.define_type::<Held>(arg.element);
739                let len = expand_buffer_length_native(scope, buffer);
740                let values = slice::from_raw_parts::<Held>(
741                    scope,
742                    buffer,
743                    0usize.into_expand(scope),
744                    len.into(),
745                );
746                ValuesExpand::Tensor {
747                    tensor: scope
748                        .create_kernel_ref(TensorExpand::__expand_from_parts(meta, values)),
749                    buffer,
750                    meta,
751                    width: *width,
752                }
753            }
754            // The map and its layout kept apart: a tile builds the view where a box moves, at the
755            // element the map serves.
756            ValuesCompilationArg::TensorMap(layout) => ValuesExpand::TensorMap {
757                layout: layout.expand(Type::new(arg.element), builder),
758                map: builder.tensor_map(),
759            },
760        };
761        let scale = arg
762            .scale
763            .as_ref()
764            .map(|level| Box::new(TileArg::expand(level, builder)));
765        let index = arg
766            .index
767            .as_ref()
768            .map(|(table, indexing)| (Box::new(TileArg::expand(table, builder)), indexing.clone()));
769        let codebook = arg
770            .codebook
771            .as_ref()
772            .map(|table| Box::new(TileArg::expand(table, builder)));
773        let (coefficients, offsets) = arg.runtime_map;
774        let mut runtime_map = (
775            Coords::<u32>::__expand_new(&builder.scope),
776            Coords::<i32>::__expand_new(&builder.scope),
777        );
778        for _ in 0..coefficients {
779            let coefficient = u32::expand_scalar(builder);
780            runtime_map
781                .0
782                .__expand_push_method(&builder.scope, coefficient);
783        }
784        for _ in 0..offsets {
785            let offset = i32::expand_scalar(builder);
786            runtime_map.1.__expand_push_method(&builder.scope, offset);
787        }
788        TileArgExpand {
789            values,
790            spec: TileSpec::clone(&arg.spec),
791            element: arg.element,
792            scale,
793            index,
794            codebook,
795            runtime_map,
796        }
797    }
798}
799
800impl IntoExpand for TileArgExpand<'_> {
801    type Expand = Self;
802    fn into_expand(self, _: &Scope) -> Self {
803        self
804    }
805}
806
807impl ExpandTypeClone for TileArgExpand<'_> {
808    fn clone_unchecked(&self) -> Self {
809        self.clone()
810    }
811}
812
813impl IntoMut for TileArgExpand<'_> {
814    fn into_mut(self, _: &Scope) -> Self {
815        self
816    }
817}
818
819impl CubeDebug for TileArgExpand<'_> {}
820
821impl AsRefExpand for TileArgExpand<'_> {
822    fn __expand_ref_method(&self, _: &Scope) -> &Self {
823        self
824    }
825}
826
827impl AsMutExpand for TileArgExpand<'_> {
828    fn __expand_ref_mut_method(&mut self, _: &Scope) -> &mut Self {
829        self
830    }
831}
832
833#[cfg(test)]
834mod tests {
835    use cubecl::ir::FloatKind;
836
837    use super::*;
838
839    /// A one-axis buffer of `f32`, expanded as a kernel argument outside any launch.
840    fn expanded_f32_buffer(builder: &mut KernelBuilder) -> TileArgExpand<'static> {
841        let mut launcher = KernelLauncher::new(builder_settings());
842        let meta = TensorMeta::register(TensorMetaLaunch::new(4, 1), &mut launcher);
843        let arg = TileArgCompilationArg {
844            values: ValuesCompilationArg::Tensor {
845                tensor: TensorCompilationArg {
846                    meta,
847                    buffer: BufferCompilationArg { inplace: None },
848                },
849                width: 1,
850            },
851            spec: Arc::new(TileSpec::direct(&[Axis(0)])),
852            element: ElemType::Float(FloatKind::F32),
853            scale: None,
854            index: None,
855            codebook: None,
856            runtime_map: (0, 0),
857        };
858        TileArg::expand(&arg, builder)
859    }
860
861    fn builder_settings() -> KernelSettings {
862        KernelSettings::new(
863            cubecl::ir::settings::Dim3::new_single(),
864            cubecl::ir::settings::ExecutionMode::Checked,
865            cubecl::ir::AddressType::U32,
866        )
867    }
868
869    #[test]
870    #[should_panic(expected = "TileArg::lines: the buffer is bound at f32, not i32")]
871    fn lines_refuse_a_buffer_bound_at_another_element() {
872        let mut builder = KernelBuilder::new(builder_settings());
873        let arg = expanded_f32_buffer(&mut builder);
874        arg.__expand_lines_method::<i32, Const<1>>(&builder.scope);
875    }
876
877    /// A `[4]` `u32` table keyed by `keys`, expanded as a kernel argument outside any launch.
878    fn expanded_table(builder: &mut KernelBuilder, keys: &[Axis]) -> TileArgExpand<'static> {
879        let mut launcher = KernelLauncher::new(builder_settings());
880        let meta = TensorMeta::register(TensorMetaLaunch::new(4, keys.len()), &mut launcher);
881        let arg = TileArgCompilationArg {
882            values: ValuesCompilationArg::Tensor {
883                tensor: TensorCompilationArg {
884                    meta,
885                    buffer: BufferCompilationArg { inplace: None },
886                },
887                width: 1,
888            },
889            spec: Arc::new(TileSpec::direct(keys)),
890            element: u32::elem_type_native(),
891            scale: None,
892            index: None,
893            codebook: None,
894            runtime_map: (0, 0),
895        };
896        TileArg::expand(&arg, builder)
897    }
898
899    /// The one-axis `f32` buffer as a run keyed by `keys`, served on a bare scope under `levels`
900    /// over `space`.
901    fn serve_indexed(keys: &[Axis], space: &[(Axis, usize)], levels: Vec<Level>) {
902        let mut builder = KernelBuilder::new(builder_settings());
903        let mut values = expanded_f32_buffer(&mut builder);
904        let table = expanded_table(&mut builder, keys);
905        values.index = Some((Box::new(table), Indexing::Run(Axis(0))));
906        let scope = &builder.scope;
907        let partitioning = PartitioningExpand {
908            space: SpaceExpand {
909                shape: Space::new(space).shape().clone(),
910                sizes: Sequence::<usize>::__expand_new(scope),
911            },
912            levels,
913        };
914        values.__expand_tile_method(scope, &partitioning);
915    }
916
917    /// The table is read at one step, the one that cuts its keys; keys cut at two levels name no
918    /// such step, and the operand is refused as it is served.
919    #[test]
920    #[should_panic(
921        expected = "the first level cutting one of [Axis(1), Axis(2)] does not cut them all"
922    )]
923    fn keys_cut_at_two_levels_are_refused_as_the_tile_expands() {
924        serve_indexed(
925            &[Axis(1), Axis(2)],
926            &[(Axis(0), 4), (Axis(1), 2), (Axis(2), 2)],
927            vec![Level::every(&[(Axis(1), 1)]), Level::every(&[(Axis(2), 1)])],
928        );
929    }
930
931    /// No level cuts the key, so it is one tile, and the run is read at the table's first entry as
932    /// the operand is served: a grouped product of a single group.
933    #[test]
934    fn a_table_no_level_cuts_is_read_as_the_tile_expands() {
935        serve_indexed(
936            &[Axis(1)],
937            &[(Axis(0), 4), (Axis(1), 1)],
938            vec![Level::every(&[(Axis(0), 2)])],
939        );
940    }
941
942    #[test]
943    fn lines_read_a_buffer_at_the_element_it_is_bound_at() {
944        let mut builder = KernelBuilder::new(builder_settings());
945        let arg = expanded_f32_buffer(&mut builder);
946        arg.__expand_lines_method::<f32, Const<1>>(&builder.scope);
947    }
948}