Skip to main content

cubecl_tile/tile/kind/memory/
global.rs

1//! A launched memory operand ([`GlobalOperand`]) and the [`Memory`] tile it becomes.
2
3use cubecl::prelude::*;
4
5use cubecl_std::tensor::{ErasedTensor, WriteOnly};
6
7use crate::*;
8
9/// A launched memory operand: a buffer, a fused sink or a fused producer, with its geometry.
10#[derive(CubeType)]
11pub struct GlobalOperand {
12    pub(crate) backing: Backing,
13    /// The buffer's physical extents and strides in scalars, one per physical axis.
14    pub geometry: RuntimeGeometry,
15    /// The binding's own line width, before any packing factor.
16    #[cube(comptime)]
17    pub bound_width: usize,
18    /// The kernel's space.
19    #[cube(comptime)]
20    pub space: Space,
21    #[cube(comptime)]
22    pub spec: TileSpec,
23    /// What a write does to the cell it lands on.
24    #[cube(comptime)]
25    pub write: Write,
26    /// One per [`Scale::Dynamic`](crate::Scale) term and one per
27    /// [`Divisor::Dynamic`](crate::Divisor) axis, by physical axis, divisor last.
28    pub coefficients: Coords<u32>,
29    /// One signed value per [`Offset::Dynamic`](crate::Offset) axis.
30    pub offsets: Coords<i32>,
31    /// The table a pool of pages is read as sequences through, where the spec states
32    /// [`paging`](TileSpec::paging).
33    pub(crate) paging: ComptimeOption<PageTable>,
34}
35
36#[cube]
37impl GlobalOperand {
38    /// A launched tensor, `space` projected onto the `spec` axes; shape and strides in scalars.
39    pub fn tensor<T: Numeric, E: CubePrimitive<Scalar = T>>(
40        tensor: &Tensor<E>,
41        #[comptime] space: Space,
42        #[comptime] spec: TileSpec,
43    ) -> GlobalOperand {
44        GlobalOperand::of_tensor::<T, E>(
45            tensor,
46            space,
47            spec,
48            Coords::<u32>::new(),
49            Coords::<i32>::new(),
50            ComptimeOption::new_None(),
51        )
52    }
53
54    /// [`tensor`](GlobalOperand::tensor) over a pool of pages, read as the sequences `table` names
55    /// ([`Paging`]): the spec states which axes the table resolves, the tensor is the pool as it
56    /// lies, and the operand's extent along the two axes is the table's.
57    pub fn paged<T: Numeric, E: CubePrimitive<Scalar = T>>(
58        tensor: &Tensor<E>,
59        table: &Tensor<u32>,
60        #[comptime] space: Space,
61        #[comptime] spec: TileSpec,
62    ) -> GlobalOperand {
63        let paging = comptime!(spec.paging.unwrap_or_else(|| panic!(
64            "GlobalOperand::paged: the spec states no paging for the table to resolve"
65        )));
66        let projection = comptime!(spec.projection.clone());
67        let (sequence_dim, _) = comptime!(
68            paging
69                .dims(&projection)
70                .unwrap_or_else(|refusal| panic!("GlobalOperand::paged: {refusal}"))
71        );
72        let table = PageTable::new(
73            table,
74            tensor.shape(sequence_dim) as u32,
75            paging,
76            projection,
77            tensor.vector_size(),
78        );
79        GlobalOperand::of_tensor::<T, E>(
80            tensor,
81            space,
82            spec,
83            Coords::<u32>::new(),
84            Coords::<i32>::new(),
85            ComptimeOption::new_Some(table),
86        )
87    }
88
89    /// [`tensor`](GlobalOperand::tensor) for a gather with a runtime affine map.
90    /// `coefficients` and `offsets` follow the field order; only their lengths are checked.
91    pub fn gathered<T: Numeric, E: CubePrimitive<Scalar = T>>(
92        tensor: &Tensor<E>,
93        #[comptime] space: Space,
94        #[comptime] spec: TileSpec,
95        coefficients: Coords<u32>,
96        offsets: Coords<i32>,
97    ) -> GlobalOperand {
98        GlobalOperand::of_tensor::<T, E>(
99            tensor,
100            space,
101            spec,
102            coefficients,
103            offsets,
104            ComptimeOption::new_None(),
105        )
106    }
107
108    /// The shared body: `E` is the binding element, `T` the served scalar, `paging` the table a
109    /// pool of pages is read through, which sets the operand's extent along its two axes.
110    fn of_tensor<T: Numeric, E: CubePrimitive>(
111        tensor: &Tensor<E>,
112        #[comptime] space: Space,
113        #[comptime] spec: TileSpec,
114        coefficients: Coords<u32>,
115        offsets: Coords<i32>,
116        paging: ComptimeOption<PageTable>,
117    ) -> GlobalOperand {
118        let rank = comptime!(spec.projection.physical_rank());
119        // Held at what the binding holds: `T`, or the `u32` words a packed one packs it in.
120        let backing = match comptime!(spec.packing) {
121            Packing::Plain => Backing::new_Buffer(Raw::<Slice>::of::<T>(unsafe {
122                tensor
123                    .as_slice()
124                    .downcast_unchecked::<T>()
125                    .as_boxed_unchecked()
126            })),
127            Packing::Packed { .. } => Backing::new_Buffer(Raw::<Slice>::of::<u32>(unsafe {
128                tensor
129                    .as_slice()
130                    .downcast_unchecked::<u32>()
131                    .as_boxed_unchecked()
132            })),
133        };
134        let pool = RuntimeGeometry::of_tensor::<E>(tensor, rank);
135        #[comptime]
136        let geometry = match &paging {
137            ComptimeOption::Some(table) => table.sequences_over(&pool),
138            ComptimeOption::None => pool,
139        };
140        GlobalOperand {
141            backing,
142            geometry,
143            bound_width: tensor.vector_size(),
144            space,
145            spec,
146            write: comptime!(Write::Replace),
147            coefficients,
148            offsets,
149            paging,
150        }
151    }
152
153    /// An operand whose values are handed to `sink` instead of stored, under the stated geometry.
154    /// Under [`Accumulate`](Write::Accumulate) the buffer must hold zero; under
155    /// [`Exclusive`](Write::Exclusive), anything: the first turn replaces it.
156    pub fn sink<T: Numeric>(
157        sink: ErasedTensor<T, WriteOnly>,
158        geometry: RuntimeGeometry,
159        #[comptime] vector_size: usize,
160        #[comptime] space: Space,
161        #[comptime] spec: TileSpec,
162        #[comptime] write: Write,
163    ) -> GlobalOperand {
164        comptime!(assert!(
165            spec.packing == Packing::Plain,
166            "GlobalOperand::sink: a sink is written at the width it states, so its spec may not \
167             state a packing ({:?}) on top of it",
168            spec.packing
169        ));
170        GlobalOperand {
171            backing: Backing::new_WriteCall(RawCall::of::<T>(sink)),
172            geometry,
173            bound_width: vector_size,
174            space,
175            spec,
176            write,
177            coefficients: Coords::<u32>::new(),
178            offsets: Coords::<i32>::new(),
179            paging: ComptimeOption::new_None(),
180        }
181    }
182
183    /// An operand whose values come from `source` instead of memory, under the stated geometry.
184    pub fn source<T: Numeric>(
185        source: ErasedTensor<T, ReadOnly>,
186        geometry: RuntimeGeometry,
187        #[comptime] vector_size: usize,
188        #[comptime] space: Space,
189        #[comptime] spec: TileSpec,
190    ) -> GlobalOperand {
191        comptime!(assert!(
192            spec.packing == Packing::Plain,
193            "GlobalOperand::source: a source is read at the width it states, so its spec may not \
194             state a packing ({:?}) on top of it",
195            spec.packing
196        ));
197        GlobalOperand {
198            backing: Backing::new_ReadCall(RawCall::of::<T>(source)),
199            geometry,
200            bound_width: vector_size,
201            space,
202            spec,
203            write: comptime!(Write::Replace),
204            coefficients: Coords::<u32>::new(),
205            offsets: Coords::<i32>::new(),
206            paging: ComptimeOption::new_None(),
207        }
208    }
209
210    /// This operand as a [`Tile`] at the top of `levels`.
211    pub fn tile<T: Numeric>(self, #[comptime] levels: Vec<Level>) -> Tile {
212        let place = comptime!(Placement::root(
213            self.space.subspace(self.spec.axes()),
214            levels
215        ));
216        Tile::new::<T>(TileKind::new_Memory(Memory::global(self)), place)
217    }
218}
219
220#[cube]
221impl Memory {
222    /// The memory tile a launched operand becomes, its top window boxed over the physical axes.
223    // The unrolled loop over the buffer's dims indexes a compile-time list by the dim.
224    #[allow(clippy::needless_range_loop)]
225    pub(crate) fn global(operand: GlobalOperand) -> Memory {
226        let backing = operand.backing;
227        let geometry = operand.geometry;
228        let paging = operand.paging;
229        let coefficients = operand.coefficients;
230        let offsets = operand.offsets;
231        let bound_width = comptime!(operand.bound_width);
232        let space = comptime!(operand.space.clone());
233        let spec = comptime!(operand.spec.clone());
234        let write = comptime!(operand.write);
235        let split_share = comptime!(SplitShare::Whole);
236        let space = comptime!(space.subspace(spec.axes()));
237        let projection = comptime!(spec.projection.clone());
238        let coords = comptime!(projection.untiled());
239        let packing = comptime!(spec.packing);
240        let vector_size = comptime!(packing.served(bound_width));
241        let dims_given = geometry.shape.len();
242        let coefficients_given = coefficients.len();
243        let offsets_given = offsets.len();
244        comptime!(assert!(
245            dims_given == projection.physical_rank(),
246            "GlobalOperand: the projection addresses {} physical dims but {dims_given} were given",
247            projection.physical_rank()
248        ));
249        comptime!(assert!(
250            coefficients_given == coords.dynamic_coefficient_count(),
251            "GlobalOperand: the projection has {} Dynamic coefficients and divisors but \
252             {coefficients_given} were given",
253            coords.dynamic_coefficient_count()
254        ));
255        comptime!(assert!(
256            offsets_given == coords.dynamic_offset_count(),
257            "GlobalOperand: the projection has {} Dynamic offsets but {offsets_given} were given",
258            coords.dynamic_offset_count()
259        ));
260        comptime!(check_operand(&space, &spec, vector_size));
261        comptime!(assert!(
262            paging.is_some() == spec.paging.is_some(),
263            "GlobalOperand: a spec stating paging is read through its table \
264             (GlobalOperand::paged), and only such a spec is"
265        ));
266        // Gathered buffers have fewer physical axes than logical; storage-tiled ones have more.
267        let rank = comptime!(projection.physical_rank());
268        // The buffer read in loads: each dim's extent over what one load covers of it, every
269        // stride counted in loads. A plain buffer's innermost dim counts lines one line apart and
270        // the coarser strides divide by the width; a stored tile a load takes whole is one.
271        let load = comptime!(
272            VectorTile::new(
273                &spec.stored_tiles,
274                space.axis_at(space.rank() - 1),
275                vector_size
276            )
277            .unwrap_or_else(|why| panic!("GlobalOperand: {vector_size} values a load: {why}"))
278        );
279        let parts = comptime!(load.parts(&spec.stored_tiles, &projection.dense_labels(), rank));
280        // A stated tile's extent is the statement's, not read off the buffer: the reads over it
281        // divide by a constant.
282        let stated = comptime!(VectorTile::stated_extents(
283            &spec.stored_tiles,
284            &projection.dense_labels(),
285            rank
286        ));
287        let mut physical_shape = Coords::<u32>::new();
288        let mut physical_strides = Coords::<u32>::new();
289        #[unroll]
290        for i in 0..rank {
291            let part = comptime!(parts[i] as u32);
292            if comptime!(stated[i].is_some()) {
293                physical_shape.push(comptime!(stated[i].unwrap() as u32 / part));
294            } else {
295                physical_shape.push(geometry.shape.at(i) / part);
296            }
297            physical_strides.push(geometry.strides.at(i) * part / comptime!(vector_size as u32));
298        }
299        let gmem_projection = comptime!(projection.positional());
300        // The product of the physical shape, so tiled (padded) operands get the logical extent.
301        let bound = logical_extent(comptime!(gmem_projection.clone()), &physical_shape);
302        let (origin, extent, map) = top_window(
303            comptime!(space.clone()),
304            &bound,
305            &offsets,
306            coefficients,
307            comptime!(load.clone()),
308            comptime!(coords.clone()),
309        );
310        Memory {
311            address: comptime!(AddressSpace::Global),
312            store: Store {
313                backing,
314                vector_size: comptime!(vector_size),
315                packing: comptime!(packing),
316                stored_tiles: comptime!(spec.stored_tiles.clone()),
317            },
318            layout: BufferLayout {
319                physical_shape,
320                physical_strides,
321                projection: gmem_projection,
322                rows: RowArrangement::InOrder,
323                paging,
324            },
325            window: Window::new(
326                origin,
327                extent,
328                bound,
329                comptime!(coords.may_underflow()),
330                comptime!(spec.boundaries.clone()),
331            ),
332            source_window: ComptimeOption::new_None(),
333            projection: comptime!(coords),
334            map,
335            offsets,
336            window_start: 0u32,
337            access: comptime!(Access {
338                whole: true,
339                overhang: if spec.is_checked() {
340                    Overhang::Masked
341                } else {
342                    Overhang::Fits
343                },
344                write,
345                fill: FillUnits::cube(spec.units),
346                storage: WindowStorage::Stored(spec.storage),
347                delivery: spec.delivery,
348            }),
349            unit_share: comptime!(UnitShare::Repeated),
350            split_share,
351            init_from: comptime!(InitFrom::Cell),
352            contraction: comptime!(None),
353            codebook: Codebook::none(),
354            index: ComptimeOption::new_None(),
355        }
356    }
357}
358
359/// The contract an operand's statement owes, checked on the host.
360fn check_operand(space: &Space, spec: &TileSpec, vector_size: usize) {
361    let projection = &spec.projection;
362    // At expansion no caller takes a refusal; a spec stated by hand, not bound, arrives here
363    // unchecked.
364    if let Err(refusal) = projection.validate(vector_size) {
365        panic!("{refusal}");
366    }
367    projection.validate_composition(|axis| space.extent(axis));
368    if let Some(paging) = &spec.paging {
369        let checked = paging.validate(projection, &spec.stored_tiles, spec.delivery);
370        if let Err(refusal) = checked {
371            panic!("GlobalOperand: {refusal}");
372        }
373    }
374    let coord_rank = projection.coordinate_rank();
375    assert!(
376        spec.boundaries.is_empty() || spec.boundaries.len() == coord_rank,
377        "GlobalOperand: boundaries rank ({}) does not match coordinate rank ({coord_rank})",
378        spec.boundaries.len()
379    );
380    assert!(
381        vector_size == 1 || spec.boundaries.last().copied().flatten() != Some(Boundary::Clamp),
382        "GlobalOperand: Boundary::Clamp cannot clamp the vectorized innermost axis (served at \
383         {vector_size})"
384    );
385}
386
387/// [`full_window`] for the top gmem tile over the physical axes; `Dynamic` axes read `bound`.
388#[cube]
389fn top_window(
390    #[comptime] space: Space,
391    bound: &Coords<u32>,
392    offsets: &Coords<i32>,
393    coefficients: Coords<u32>,
394    #[comptime] load: VectorTile,
395    #[comptime] projection: Projection,
396) -> (Coords<i32>, Coords<u32>, RuntimeMap) {
397    let mut origin = Coords::<i32>::new();
398    let mut extent = Coords::<u32>::new();
399    let mut residues = Coords::<u32>::new();
400    let rank = comptime!(projection.physical_rank());
401
402    #[unroll]
403    for pa in 0..rank {
404        let size = if comptime!(projection.is_direct()) {
405            origin.push(0);
406            residues.push(0u32);
407            let axis = comptime!(space.axis_at(pa));
408            match comptime!(space.extent_raw(axis)) {
409                Extent::Static(e) => (comptime!(e / load.extent_along(axis)) as u32).runtime(),
410                Extent::Dynamic => bound.at(pa),
411            }
412        } else {
413            let (start, phase) =
414                gathered_origin(comptime!(projection.clone()), offsets, &coefficients, pa);
415            origin.push(start);
416            residues.push(phase);
417            bound.at(pa)
418        };
419        extent.push(size);
420    }
421
422    (
423        origin,
424        extent,
425        RuntimeMap {
426            coefficients,
427            residues,
428        },
429    )
430}
431
432/// Where a gathered physical axis's top window starts, and the phase its division left behind.
433#[cube]
434fn gathered_origin(
435    #[comptime] projection: Projection,
436    offsets: &Coords<i32>,
437    coefficients: &Coords<u32>,
438    #[comptime] pa: usize,
439) -> (i32, u32) {
440    let axis_map = comptime!(projection.physical_axis(pa));
441
442    if comptime!(axis_map.origin().is_some()) {
443        (
444            comptime!(axis_map.origin().unwrap() as i32).runtime(),
445            comptime!(axis_map.residue().unwrap() as u32).runtime(),
446        )
447    } else {
448        // Floor division: a negative (padding) offset must not round toward zero.
449        let offset = match comptime!(axis_map.offset()) {
450            Offset::Static(o) => comptime!(o as i32).runtime(),
451            Offset::Dynamic => offsets.at(comptime!(projection.dynamic_offset_index(pa).unwrap())),
452        };
453        if comptime!(!axis_map.is_rational()) {
454            (offset, 0u32)
455        } else {
456            let divisor = match comptime!(axis_map.divisor()) {
457                Divisor::Static(d) => comptime!(d as i32).runtime(),
458                Divisor::Dynamic { .. } => coefficients
459                    .at(comptime!(projection.dynamic_divisor_index(pa).unwrap()))
460                    .cast::<i32>(),
461            };
462            let (start, residue) = floor_div_rem(offset, divisor);
463            (start, residue.cast::<u32>())
464        }
465    }
466}