use cubecl::{
prelude::*,
std::tensor::{ErasedTensor, WriteOnly},
};
use crate::*;
#[derive(CubeType, Clone)]
#[expand(derive(Clone))]
pub(crate) struct Memory<T: Numeric> {
#[cube(comptime)]
pub(crate) address: AddressSpace,
pub(crate) store: Store<T>,
pub(crate) layout: BufferLayout,
pub(crate) window: Window,
#[cube(comptime)]
pub(crate) projection: Projection,
pub(crate) map: RuntimeMap,
pub(crate) offsets: Coords<i32>,
pub(crate) window_start: u32,
#[cube(comptime)]
pub(crate) access: Access,
#[cube(comptime)]
pub(crate) unit_share: UnitShare,
#[cube(comptime)]
pub(crate) split_share: SplitShare,
#[cube(comptime)]
pub(crate) init_from: InitFrom,
#[cube(comptime)]
pub(crate) contraction: Option<Contraction>,
pub(crate) source_window: ComptimeOption<SourceWindow>,
pub(crate) factor: Factor,
pub(crate) codebook: Codebook,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) struct Contraction {
pub(crate) block: RegisterBlock,
pub(crate) semiring: Semiring,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) enum AddressSpace {
Global,
Shared,
}
#[derive(CubeType, Clone)]
#[expand(derive(Clone))]
#[allow(dead_code)] pub(crate) enum Backing<T: Numeric> {
Buffer(Box<[T]>),
WriteCall(ErasedTensor<T, WriteOnly>),
ReadCall(ErasedTensor<T, ReadOnly>),
}
#[derive(CubeType, Clone)]
#[expand(derive(Clone))]
pub(crate) struct Store<T: Numeric> {
pub(crate) backing: Backing<T>,
#[cube(comptime)]
pub(crate) vector_size: usize,
#[cube(comptime)]
pub(crate) packing: Packing,
#[cube(comptime)]
pub(crate) stored_tiles: Vec<(Axis, usize)>,
}
#[cube]
impl<T: Numeric> Memory<T> {
pub(crate) fn vector_tile(&self, #[comptime] space: &Space) -> comptime_type!(VectorTile) {
let width = comptime!(self.store.vector_size);
comptime!(
VectorTile::new(
&self.store.stored_tiles,
space.axis_at(space.rank() - 1),
width
)
.unwrap_or_else(|why| panic!("Memory: {width} values a load: {why}"))
)
}
pub(crate) fn stated_contraction(&self) -> comptime_type!(Contraction) {
comptime!(self.contraction.unwrap_or_else(|| panic!(
"Tile::mma: a memory window contracts under the register block and semiring it is \
stated with; state them with Tile::accumulating(block, semiring)"
)))
}
}
#[cube]
impl<T: Numeric> Store<T> {
pub(crate) fn untiled(
backing: Backing<T>,
#[comptime] vector_size: usize,
#[comptime] packing: Packing,
) -> Store<T> {
Store::<T> {
backing,
vector_size,
packing,
stored_tiles: comptime!(Vec::new()),
}
}
pub(crate) fn addressed(&self) -> comptime_type!(bool) {
match &self.backing {
Backing::Buffer(_) | Backing::ReadCall(_) => comptime!(true),
Backing::WriteCall(_) => comptime!(false),
}
}
#[allow(clippy::borrowed_box)]
pub(crate) fn buffer(&self) -> &Box<[T]> {
match &self.backing {
Backing::Buffer(buffer) => buffer,
Backing::WriteCall(_) => panic!(
"Store::buffer: this tile's backing is written through a call, which has no \
address, it can only be written through its layout"
),
Backing::ReadCall(_) => panic!(
"Store::buffer: this tile's backing is read through a call, which has no \
address, it can only be read through its layout (Memory::read_view), so the \
slice-shaped paths (a dense run, a re-typed quant storage, a tensor-map load) \
are closed to it"
),
}
}
pub(crate) fn free(&self) {
match &self.backing {
Backing::Buffer(buffer) => free_shared(buffer),
Backing::WriteCall(_) | Backing::ReadCall(_) => {}
}
}
pub(crate) fn has_address(&self) -> comptime_type!(bool) {
match &self.backing {
Backing::Buffer(_) => comptime!(true),
Backing::WriteCall(_) | Backing::ReadCall(_) => comptime!(false),
}
}
#[allow(clippy::borrowed_box)]
pub(crate) fn buffer_mut(&mut self) -> &mut Box<[T]> {
match &mut self.backing {
Backing::Buffer(buffer) => buffer,
Backing::WriteCall(_) => panic!(
"Store::buffer_mut: this tile's backing is written through a call, which has no \
address, it can only be written through its layout"
),
Backing::ReadCall(_) => panic!(
"Store::buffer_mut: this tile's backing is read through a call, which is \
read-only"
),
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) struct Access {
pub whole: bool,
pub overhang: Overhang,
pub write: Write,
pub fill: FillUnits,
pub storage: WindowStorage,
pub delivery: Delivery,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) struct FillUnits {
pub scope: ComputeScope,
pub count: usize,
}
impl FillUnits {
pub(crate) fn cube(count: usize) -> Self {
FillUnits {
scope: ComputeScope::Cube,
count,
}
}
}
#[cube]
pub(crate) fn fill_worker(#[comptime] fill: FillUnits) -> usize {
match comptime!(fill.scope) {
ComputeScope::Cube => UNIT_POS as usize,
ComputeScope::Plane => UNIT_POS_X as usize,
ComputeScope::Unit => comptime!(panic!(
"fill_worker: a cooperative fill is shared by a cube or a plane, never one unit"
)),
}
}
#[cube]
pub(crate) fn fill_workers(#[comptime] fill: FillUnits) -> usize {
match comptime!(fill.scope) {
ComputeScope::Cube => CUBE_DIM as usize,
ComputeScope::Plane => CUBE_DIM_X as usize,
ComputeScope::Unit => comptime!(panic!(
"fill_workers: a cooperative fill is shared by a cube or a plane, never one unit"
)),
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Write {
Replace,
Accumulate,
Exclusive(Schedule),
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Schedule {
Sequential,
Cyclic,
}
impl Write {
pub(crate) fn admits(self, split: SplitShare, monoid: Monoid, site: &str) {
match (self, monoid) {
(Write::Accumulate | Write::Exclusive(_), Monoid::Prod | Monoid::Max | Monoid::Min) => {
panic!(
"{site}: this destination adds ({self:?}), atomically or relayed, so a \
{monoid:?} accumulation into it would come out summed. Drain it into a \
replacing destination, combining the partials first where they are split."
)
}
(Write::Accumulate | Write::Exclusive(_), Monoid::Sum) | (Write::Replace, _) => {}
}
match (split, self) {
(SplitShare::Whole, _)
| (
SplitShare::PartialAcrossCubes | SplitShare::PartialAcrossPlanes,
Write::Accumulate,
)
| (SplitShare::PartialAcrossCubes, Write::Exclusive(Schedule::Sequential))
| (SplitShare::PartialAcrossPlanes, Write::Exclusive(Schedule::Cyclic)) => {}
(SplitShare::PartialAcrossCubes, Write::Exclusive(Schedule::Cyclic)) => panic!(
"{site}: this accumulator's cells are split across cubes and its destination \
takes its writers in cyclic rounds, which a cube barrier orders: it orders the \
planes of one cube, never cubes. Take the cubes' turns through a `Relay`, or \
combine them atomically."
),
(SplitShare::PartialAcrossPlanes, Write::Exclusive(Schedule::Sequential)) => panic!(
"{site}: this accumulator's cells are split across the planes of one cube and its \
destination is relayed, a plain read and write taken one cube at a time; the planes \
would write the same line at once and lose each other's partial. \
Drain into an atomic destination (bind it as an `AccumulateArg`), or combine \
the planes' partials in the cube before its turn at the relay, atomically or in \
cyclic rounds (`smem_cyclic_accumulation`)."
),
(SplitShare::PartialAcrossCubes | SplitShare::PartialAcrossPlanes, Write::Replace) => {
panic!(
"{site}: this accumulator's cells are split across planes or cubes and its \
destination replaces rather than accumulates, so every partial but one would be \
lost. \
A contracted axis distributed across planes or cubes gives each instance a \
slice of the contraction, and none of them holds a whole cell. \
Drain into an accumulating destination (bind it as an `AccumulateArg`), \
distribute the contraction across the plane's units instead \
(`distribute(units(n), ..)`, combined in the plane's registers), or give the \
output an axis of its own for the split."
)
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) enum Overhang {
Never,
Fits,
Masked,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Boundary {
Zero,
Clamp,
}
impl Overhang {
pub(crate) fn masks(&self) -> bool {
matches!(self, Overhang::Masked)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) enum Guard {
Checked,
Proved,
}
impl Guard {
pub(crate) fn checks(self) -> bool {
matches!(self, Guard::Checked)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Storage {
Strided,
Tiled(Option<usize>),
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) enum WindowStorage {
Stored(Storage),
Contiguous,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_relay_takes_a_split_across_cubes() {
Write::Exclusive(Schedule::Sequential).admits(
SplitShare::PartialAcrossCubes,
Monoid::Sum,
"test",
);
Write::Accumulate.admits(SplitShare::PartialAcrossCubes, Monoid::Sum, "test");
Write::Accumulate.admits(SplitShare::PartialAcrossPlanes, Monoid::Sum, "test");
}
#[test]
#[should_panic(expected = "split across the planes of one cube")]
fn a_relay_refuses_a_split_across_planes() {
Write::Exclusive(Schedule::Sequential).admits(
SplitShare::PartialAcrossPlanes,
Monoid::Sum,
"test",
);
}
#[test]
#[should_panic(expected = "would come out summed")]
fn an_atomic_destination_refuses_a_max() {
Write::Accumulate.admits(SplitShare::Whole, Monoid::Max, "test");
}
#[test]
#[should_panic(expected = "would come out summed")]
fn a_relay_refuses_a_max() {
Write::Exclusive(Schedule::Sequential).admits(
SplitShare::PartialAcrossCubes,
Monoid::Max,
"test",
);
}
#[test]
fn a_replacing_destination_takes_any_monoid() {
for monoid in [Monoid::Sum, Monoid::Prod, Monoid::Max, Monoid::Min] {
Write::Replace.admits(SplitShare::Whole, monoid, "test");
}
}
}
#[allow(clippy::borrowed_box)]
#[cube]
fn free_shared<T: Numeric>(buffer: &Box<[T]>) {
intrinsic!(|scope| {
let list = buffer.__extract_list(scope);
scope.register(&cubecl::ir::dialect::general::FreeOp::new(
scope.ctx_mut(),
list,
));
})
}