use crate::matmul_axes::{K, blocked};
use cubecl::quant::scheme::{QuantScheme, QuantStore};
use cubecl::tile::launch::Refusal;
use cubecl::tile::{Axis, Space, kind::Field};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub struct MicroTile([Option<(Axis, usize)>; MicroTile::MAX_AXES]);
impl MicroTile {
const MAX_AXES: usize = 2;
pub fn unit() -> Self {
MicroTile([None; Self::MAX_AXES])
}
pub fn along(axis: Axis, extent: usize) -> Self {
let mut tile = MicroTile::unit();
tile.0[0] = Some((axis, extent));
tile
}
fn entries(&self) -> impl Iterator<Item = (Axis, usize)> + '_ {
self.0.iter().flatten().copied()
}
pub fn extent(&self, axis: Axis) -> usize {
self.entries()
.find(|&(a, _)| a == axis)
.map_or(1, |(_, extent)| extent)
}
pub fn times(mut self, axis: Axis, count: usize) -> Self {
if let Some((_, extent)) = self.0.iter_mut().flatten().find(|(a, _)| *a == axis) {
*extent *= count;
} else if count > 1 {
let free = self
.0
.iter_mut()
.find(|entry| entry.is_none())
.expect("a microtile reaches along at most two axes");
*free = Some((axis, count));
}
self
}
pub fn renamed(mut self, from: Axis, to: Axis) -> Self {
for (axis, _) in self.0.iter_mut().flatten() {
if *axis == from {
*axis = to;
}
}
self
}
pub fn space(&self) -> Space {
Space::new(&self.entries().collect::<Vec<_>>())
}
pub fn values(&self) -> usize {
self.entries().map(|(_, extent)| extent).product()
}
pub fn axis(&self) -> Option<Axis> {
self.entries()
.find(|&(_, extent)| extent > 1)
.map(|(a, _)| a)
}
fn read_off(
self,
along: Axis,
across: Axis,
words: usize,
inside: &MicroTile,
stored: Option<Stored<'_>>,
) -> Option<MicroTile> {
let Some((axes, tiles)) = stored else {
return Some(self);
};
let per_word = self.extent(along);
let axis_of = |tile_axis: Axis, read: &MicroTile| {
[along, across].into_iter().find(|&axis| {
let (groups, within) = blocked(axis);
let block = inside.extent(axis);
Self::finest_axis(axes, axis) == Some(tile_axis)
|| (tile_axis == groups
&& axes.contains(&within)
&& block > 1
&& read.extent(axis) == block)
})
};
let (&(tile_axis, first), above) = tiles.split_first()?;
let word_along = axis_of(tile_axis, &MicroTile::unit()).filter(|_| first == per_word)?;
let mut read = MicroTile::along(word_along, per_word);
let mut taken = 1;
let mut last = word_along;
for &(tile_axis, count) in above.iter().filter(|&&(_, count)| count > 1) {
if taken * count > words {
break;
}
let axis = axis_of(tile_axis, &read)?;
let bound = inside.extent(axis);
if bound > 1 && read.extent(axis) * count > bound {
break;
}
if axis != last && read.extent(axis) > 1 {
return None;
}
read = read.times(axis, count);
(taken, last) = (taken * count, axis);
}
Some(read)
}
fn finest_axis(axes: &[Axis], axis: Axis) -> Option<Axis> {
let (groups, within) = blocked(axis);
[within, groups]
.into_iter()
.find(|split| axes.contains(split))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Operand {
read: MicroTile,
scale: Option<MicroTile>,
scale_read: MicroTile,
}
impl Operand {
pub fn plain() -> Self {
Operand {
read: MicroTile::unit(),
scale: None,
scale_read: MicroTile::unit(),
}
}
pub fn renamed(self, from: Axis, to: Axis) -> Self {
Operand {
read: self.read.renamed(from, to),
scale: self.scale.map(|scale| scale.renamed(from, to)),
scale_read: self.scale_read.renamed(from, to),
}
}
pub fn of_scheme(scheme: &QuantScheme, axis: Axis, axes: [Axis; 2]) -> Option<Self> {
let dims = scheme.block_size()?.to_dim_vec(2);
let position = axes.iter().position(|&a| a == axis)?;
if dims[1 - position] != 1 {
return None;
}
let scale = dims[position] as usize;
let read = scheme.num_quants();
let scales_per_word = Field::of_scale(scheme.block_scale()?.dtype).per_word();
scale.is_multiple_of(read).then(|| Operand {
read: MicroTile::along(axis, read),
scale: Some(MicroTile::along(axis, scale)),
scale_read: MicroTile::along(axis, scales_per_word),
})
}
pub fn axis_of(scheme: &QuantScheme, axes: [Axis; 2]) -> Option<Axis> {
match scheme.store {
QuantStore::PackedU32(0) => Some(axes[1]),
QuantStore::PackedU32(1) => Some(axes[0]),
_ => None,
}
}
pub fn read(&self, axis: Axis) -> usize {
self.read.extent(axis)
}
pub fn scales_per_word(&self) -> usize {
self.scaled_along()
.map_or(1, |axis| self.scale_read.extent(axis))
}
pub fn read_space(&self) -> Space {
self.read.space()
}
pub fn scale_read_space(&self) -> Option<Space> {
let covered = self.scale.as_ref()?;
let mut extents: Vec<(Axis, usize)> = Vec::new();
for (axis, _) in self.scale_read.entries().chain(covered.entries()) {
if extents.iter().all(|&(a, _)| a != axis) {
extents.push((axis, self.scale_read.extent(axis) * covered.extent(axis)));
}
}
Some(Space::new(&extents))
}
pub fn scale_read(&self, axis: Axis) -> usize {
self.scale_read.extent(axis)
}
pub fn read_values(&self) -> usize {
self.read.values()
}
pub fn scale_read_values(&self) -> usize {
self.scale_read.values()
}
pub fn stored(
self,
across: Axis,
words: usize,
values: Option<Stored<'_>>,
scales: Option<Stored<'_>>,
) -> Option<Self> {
let unit = MicroTile::unit();
let inside = self.scale.as_ref().unwrap_or(&unit);
let read = self
.read
.read_off(self.lined(), across, words, inside, values)?;
let scale_read = match self.scaled_along() {
Some(axis) => self
.scale_read
.read_off(axis, across, words, &unit, scales)?,
None => self.scale_read,
};
Some(Operand {
read,
scale_read,
..self
})
}
pub fn scale(&self, axis: Axis) -> Option<usize> {
self.scale.as_ref().map(|tile| tile.extent(axis))
}
pub fn scaled_along(&self) -> Option<Axis> {
self.scale.as_ref().and_then(|tile| tile.axis())
}
pub fn lined(&self) -> Axis {
self.read.axis().unwrap_or(K)
}
pub fn grouping(&self, axis: Axis, extent: usize) -> Option<usize> {
self.scale(axis)
.filter(|&scale| scale > 1 && scale < extent)
}
pub fn cut(&self, axis: Axis, extent: usize, width: usize) -> Vec<(Axis, usize)> {
let (groups, inside) = blocked(axis);
let Some(scale) = self.grouping(axis, extent) else {
return vec![(inside, width)];
};
match (width.is_multiple_of(scale), scale.is_multiple_of(width)) {
(true, _) => vec![(groups, width / scale)],
(false, true) => vec![(groups, 1), (inside, width)],
(false, false) => Vec::new(),
}
}
pub fn span(&self, axis: Axis, extent: usize, width: usize) -> Vec<(Axis, usize)> {
let (groups, inside) = blocked(axis);
match self.grouping(axis, extent) {
Some(scale) if width.is_multiple_of(scale) => {
vec![(groups, width / scale), (inside, scale)]
}
Some(_) => vec![(groups, 1), (inside, width)],
None => vec![(inside, width)],
}
}
pub fn tiles(&self, axis: Axis, extent: usize, tile: usize, span: usize) -> Vec<(Axis, usize)> {
let (groups, inside) = blocked(axis);
let Some(scale) = self.grouping(axis, extent) else {
return vec![(inside, span / tile)];
};
let covered = |width: usize| match width >= scale {
true => (width / scale, scale),
false => (1, width),
};
let (tile_blocks, tile_inside) = covered(tile);
let (span_blocks, span_inside) = covered(span);
[
(groups, span_blocks / tile_blocks),
(inside, span_inside / tile_inside),
]
.into_iter()
.filter(|&(_, count)| count > 1)
.collect()
}
pub fn axes(&self, axis: Axis, extent: usize) -> [(Axis, usize); 2] {
let (groups, inside) = blocked(axis);
let scale = self.grouping(axis, extent).unwrap_or(extent);
assert!(
extent.is_multiple_of(scale),
"{extent} along {axis:?} is not whole scale tiles of {scale}"
);
[(groups, extent / scale), (inside, scale)]
}
pub fn units_distributing(&self, axis: Axis, extent: usize, units: usize) -> usize {
if self.grouping(axis, extent).is_none() {
return units;
}
let [(_, blocks), _] = self.axes(axis, extent);
if !blocks.is_multiple_of(self.scales_per_word()) {
return units;
}
let words = blocks / self.scales_per_word();
(1..=units)
.rev()
.find(|&group| units.is_multiple_of(group) && words.is_multiple_of(group))
.unwrap_or(1)
}
pub fn spread_across(
&self,
axis: Axis,
extent: usize,
units: usize,
) -> Result<Spread, Refusal> {
let (groups, inside) = blocked(axis);
let read = self.read(axis);
let Some(scale) = self.grouping(axis, extent) else {
let per_unit = match extent.is_multiple_of(units) {
true => extent / units,
false => extent,
};
return Ok(Spread {
leaf: vec![(inside, read)],
units: match extent.is_multiple_of(units) {
true => vec![(inside, units)],
false => Vec::new(),
},
interleaved: Vec::new(),
walk: Walked::Below(inside, per_unit / read),
});
};
let [(_, blocks), _] = self.axes(axis, extent);
let word = self.scales_per_word();
if !blocks.is_multiple_of(word * units) {
return Err(Refusal::Declined {
why: "the units do not distribute the scales' words evenly: a turn is a word of \
scales a unit, and the blocks are not whole turns",
});
}
Ok(Spread {
leaf: vec![(groups, word), (inside, scale)],
units: vec![(groups, units)],
interleaved: vec![groups],
walk: Walked::Above(vec![groups]),
})
}
pub fn packed(
lined: Axis,
read: usize,
tiled: Axis,
scale: usize,
scales_per_word: usize,
) -> Self {
Operand {
read: MicroTile::along(lined, read),
scale: Some(MicroTile::along(tiled, scale)),
scale_read: MicroTile::along(tiled, scales_per_word),
}
}
}
pub type Stored<'a> = (&'a [Axis], &'a [(Axis, usize)]);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockScales {
Fields,
Words,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Packing {
pub operand: Operand,
pub scheme: QuantScheme,
}
impl Packing {
pub fn values_bytes(&self, values: usize) -> usize {
values * self.scheme.size_bits_stored() / self.scheme.num_quants() / 8
}
pub fn bytes(&self, values: usize) -> usize {
let scales = match (self.operand.scaled_along(), self.scheme.block_scale()) {
(Some(axis), Some(block)) => {
let covered = self.operand.scale(axis).unwrap_or(1);
(values / covered) * 4 / Field::of_scale(block.dtype).per_word()
}
_ => 0,
};
self.values_bytes(values) + scales
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Spread {
pub leaf: Vec<(Axis, usize)>,
pub units: Vec<(Axis, usize)>,
pub interleaved: Vec<Axis>,
pub walk: Walked,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Walked {
Above(Vec<Axis>),
Below(Axis, usize),
}
#[cfg(test)]
mod tests {
use crate::layout::stored::Operand;
use crate::layout::tile_order;
use crate::matmul_axes::{K, KB, KI, N, NI};
const WORDS: usize = 4;
fn nvfp4() -> Operand {
Operand::packed(K, 8, K, 16, 4)
}
#[test]
fn tile_order_is_read_a_load_of_each() {
let values = tile_order::values_storage(N, (128, 8), WORDS);
let stored = nvfp4()
.stored(
N,
WORDS,
Some((&tile_order::values_axes(N), values.tiles())),
None,
)
.expect("tile order is read");
assert_eq!((stored.read(K), stored.read(N)), (16, 1));
assert_eq!(stored.read_values(), 16);
assert_eq!((stored.scale_read(K), stored.scale_read(N)), (4, 1));
assert_eq!(
stored.scales_per_word(),
4,
"a read of scales covers a word of blocks"
);
assert_eq!(stored.lined(), K);
}
#[test]
fn a_read_takes_whole_tiles_while_they_fit_one_load() {
let one_level = [(KI, 8), (KI, 2), (NI, 16)];
let stored = nvfp4()
.stored(
N,
WORDS,
Some((&tile_order::values_axes(N), &one_level)),
None,
)
.expect("tile order is read");
assert_eq!((stored.read(K), stored.read(N)), (16, 1));
for (columns, read) in [(1, 1), (2, 2), (4, 4), (8, 1)] {
let tiles = [(KI, 8), (NI, columns)];
let stored = nvfp4()
.stored(N, WORDS, Some((&tile_order::values_axes(N), &tiles)), None)
.expect("columns beside a word are read");
assert_eq!((stored.read(K), stored.read(N)), (8, read), "{columns}");
}
}
#[test]
fn a_read_does_not_jump_to_the_next_block_inside_one() {
let half_a_block_then_the_next = [(KI, 8), (KB, 2), (NI, 4)];
assert_eq!(
nvfp4().stored(
N,
WORDS,
Some((&tile_order::values_axes(N), &half_a_block_then_the_next)),
None
),
None
);
}
#[test]
fn a_read_that_is_not_whole_words_in_one_run_each_is_refused() {
let half_word = [(KI, 4), (KI, 4)];
let interleaved = [(KI, 8), (NI, 2), (KI, 2)];
for tiles in [&half_word[..], &interleaved[..]] {
assert_eq!(
nvfp4().stored(N, WORDS, Some((&tile_order::values_axes(N), tiles)), None),
None,
"{tiles:?}"
);
}
}
}