use cubecl::prelude::*;
use cubecl_std::tensor::WriteOnly;
use crate::*;
#[derive(CubeType, Clone)]
#[expand(derive(Clone))]
pub(crate) struct Memory {
#[cube(comptime)]
pub(crate) address: AddressSpace,
pub(crate) store: Store,
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) codebook: Codebook,
pub(crate) index: ComptimeOption<IndexTable>,
}
#[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 {
Buffer(Raw<Slice>),
WriteCall(RawCall<WriteOnly>),
ReadCall(RawCall<ReadOnly>),
}
#[derive(CubeType, Clone)]
#[expand(derive(Clone))]
pub(crate) struct Store {
pub(crate) backing: Backing,
#[cube(comptime)]
pub(crate) vector_size: usize,
#[cube(comptime)]
pub(crate) packing: Packing,
#[cube(comptime)]
pub(crate) stored_tiles: Vec<(Axis, usize)>,
}
#[cube]
impl Memory {
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 Store {
pub(crate) fn untiled(
backing: Backing,
#[comptime] vector_size: usize,
#[comptime] packing: Packing,
) -> Store {
Store {
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<T: Numeric>(&self) -> &Box<[T]> {
match &self.backing {
Backing::Buffer(buffer) => buffer.at::<T>(),
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) => buffer.free(),
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<T: Numeric>(&mut self) -> &mut Box<[T]> {
match &mut self.backing {
Backing::Buffer(buffer) => buffer.at_mut::<T>(),
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,
pub fillers: usize,
}
impl FillUnits {
pub(crate) fn cube(count: usize) -> Self {
FillUnits {
scope: ComputeScope::Cube,
count,
fillers: 0,
}
}
pub(crate) fn filling_planes(fillers: usize) -> Self {
FillUnits {
scope: ComputeScope::Cube,
count: 0,
fillers,
}
}
pub(crate) fn held_by(self, holder: ComputeScope) -> Self {
match holder < self.scope {
true => FillUnits {
scope: holder,
count: 0,
fillers: 0,
},
false => self,
}
}
}
#[cube]
impl FillUnits {
pub(crate) fn worker(#[comptime] fill: FillUnits) -> usize {
comptime!(assert!(
fill.scope != ComputeScope::Unit,
"FillUnits::worker: a cooperative fill is shared by a cube or a plane, never one unit"
));
if comptime!(fill.fillers > 0) {
(UNIT_POS - (CUBE_DIM - comptime!(fill.fillers as u32) * CUBE_DIM_X)) as usize
} else {
ComputeScope::unit(comptime!(fill.scope))
}
}
pub(crate) fn workers(#[comptime] fill: FillUnits) -> usize {
if comptime!(fill.fillers > 0) {
return comptime!(fill.fillers) * CUBE_DIM_X as usize;
}
match comptime!(fill.scope) {
ComputeScope::Cube => CUBE_DIM as usize,
ComputeScope::Plane => CUBE_DIM_X as usize,
ComputeScope::PlaneGroup { planes } => planes * CUBE_DIM_X as usize,
ComputeScope::Unit => comptime!(panic!(
"FillUnits::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,
Run,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Boundary {
Zero,
Clamp,
}
impl Overhang {
pub(crate) fn masks(&self) -> bool {
matches!(self, Overhang::Masked | Overhang::Run)
}
}
#[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,
}
impl WindowStorage {
pub(crate) fn below(self, depth: usize, level: &Level, space: &Space) -> WindowStorage {
match self {
WindowStorage::Contiguous
| WindowStorage::Stored(Storage::Strided | Storage::Tiled(None)) => self,
WindowStorage::Stored(Storage::Tiled(Some(tiled_at))) => {
assert!(
depth <= tiled_at,
"Memory::at: this window is above its storage tile, the tile of level \
{tiled_at}, yet the descent is at depth {depth}; the storage tile's level was \
skipped past"
);
if depth < tiled_at {
return self;
}
for axis in space.axes() {
assert!(
matches!(level.extent_in(space, axis), Extent::Static(_)),
"Memory::at: level {tiled_at} hands {axis:?} down dynamic, so its tile is \
no storage tile"
);
}
WindowStorage::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");
}
}
}