use crate::layout::{Fragments, assemble, fragments_of};
use crate::matmul_axes::{KB, KI, blocked};
use cubecl::quant::scheme::{QuantScheme, QuantStore, QuantValue, ScaleDtype};
use cubecl::tile::Axis;
use cubecl::tile::layout::{StorageLevels, StoragePartitioning};
use cubecl::zspace::metadata::Metadata;
use cubecl::zspace::{Shape, Strides};
pub const BLOCK: usize = 16;
pub fn values_axes(across: Axis) -> [Axis; 3] {
[blocked(across).1, KB, KI]
}
const VALUES_PER_WORD: usize = 8;
pub fn scheme() -> QuantScheme {
QuantScheme::default()
.with_value(QuantValue::E2M1)
.per_block([BLOCK as u8, 1], ScaleDtype::UE4M3)
.with_store(QuantStore::PackedU32(1))
.per_tensor(ScaleDtype::F32)
}
pub fn values_storage(
across: Axis,
tile: (usize, usize),
words_per_load: usize,
) -> StoragePartitioning {
let (tile_k, tile_lines) = tile;
let lines = blocked(across).1;
let pieces = [
(KI, BLOCK / VALUES_PER_WORD),
(KB, tile_k / BLOCK),
(lines, tile_lines),
];
let (read, rest) = split_at_load(&pieces, words_per_load);
let levels = match (read.is_empty(), rest.is_empty()) {
(false, false) => vec![read, rest],
_ => vec![pieces.to_vec()],
};
levels
.iter()
.fold(
StorageLevels::new(&[(KI, VALUES_PER_WORD)]),
|storage, level| storage.tile(level),
)
.grid(&[KI, KB, lines])
}
type Pieces = Vec<(Axis, usize)>;
fn split_at_load(pieces: &[(Axis, usize)], words: usize) -> (Pieces, Pieces) {
let (mut read, mut rest) = (Vec::new(), Vec::new());
let mut taken = 1;
for &(axis, count) in pieces {
let fits = (words / taken).min(count);
match rest.is_empty() && fits > 1 && count.is_multiple_of(fits) {
true => {
read.push((axis, fits));
taken *= fits;
if fits < count {
rest.push((axis, count / fits));
}
}
false => rest.push((axis, count)),
}
}
(read, rest)
}
pub fn matrix_values(kernel: &Metadata) -> Metadata {
let [lines, kb, ki] = fragments_of::<3>(kernel)
.unwrap_or_else(|rank| panic!("tile order states values over three dims, not {rank}"));
assemble(&[&[kb.as_slice(), ki.as_slice()].concat(), &lines])
}
pub fn values_in_rows(matrix: &Metadata) -> Metadata {
let [k, n] = fragments_of::<2>(matrix)
.unwrap_or_else(|rank| panic!("a matrix states its values over two dims, not {rank}"));
let pieces = |fragments: Fragments| {
let (rest, finer) = fragments.split_first().expect("an axis has a rest");
core::iter::once(*rest)
.chain(finer.iter().copied().filter(|&(extent, _)| extent > 1))
.collect::<Vec<_>>()
};
assemble(&[&pieces(n), &pieces(k)])
}
pub fn kernel_values_read(
k: usize,
(across, lines): (Axis, usize),
tile: (usize, usize),
words_per_load: usize,
) -> Metadata {
let extents = [(blocked(across).1, lines), (KB, k / BLOCK), (KI, BLOCK)];
let stored = values_storage(across, tile, words_per_load)
.physical(&extents)
.expect("tile order's extents close whole tiles");
Metadata::new(
Shape::from(stored.shape().to_vec()),
Strides::from(stored.strides().to_vec()),
)
.with_tiling(stored.tiling())
.expect("cubek states its own tiling")
}
pub fn words_in_tiles(k: usize, lines: usize, tile: (usize, usize)) -> Metadata {
let (tile_k, tile_lines) = tile;
let words_per_line = tile_k / VALUES_PER_WORD;
let tile_words = words_per_line * tile_lines;
let k_tiles = k / tile_k;
Metadata::new(
Shape::from(vec![
lines / tile_lines,
k_tiles,
tile_lines,
words_per_line,
]),
Strides::from(vec![k_tiles * tile_words, tile_words, words_per_line, 1]),
)
}
pub fn rows_read(
k: usize,
across: (Axis, usize),
tile: (usize, usize),
words_per_load: usize,
) -> Metadata {
values_in_rows(&matrix_values(&kernel_values_read(
k,
across,
tile,
words_per_load,
)))
}
pub fn kernel_values(matrix: &Metadata) -> Option<Metadata> {
let [k, n] = matrix_fragments(matrix)?;
let (kb, ki) = k.split_at(block_split(&k)?);
Some(assemble(&[&n, kb, ki]))
}
pub fn tile(matrix: &Metadata) -> Option<(usize, usize)> {
let [k, n] = matrix_fragments(matrix)?;
block_split(&k)?;
let inside = |dim: &[(usize, usize)]| dim.iter().skip(1).map(|&(extent, _)| extent).product();
Some((inside(&k), inside(&n)))
}
fn matrix_fragments(matrix: &Metadata) -> Option<[Fragments; 2]> {
if !matrix.is_tiled() {
return None;
}
fragments_of::<2>(matrix).ok()
}
fn block_split(k: &[(usize, usize)]) -> Option<usize> {
let mut split = k.len();
let mut inside = 1;
while inside < BLOCK && split > 0 {
split -= 1;
inside *= k[split].0;
}
if inside != BLOCK {
return None;
}
while split > 0 && k[split - 1].0 == 1 {
split -= 1;
}
Some(split)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::{self, fragments};
use crate::matmul_axes::N;
fn offset(meta: &Metadata, coords: &[usize]) -> usize {
fragments(meta)
.iter()
.zip(coords)
.map(|(fragments, &coord)| {
let mut finer: usize = fragments.iter().map(|&(extent, _)| extent).product();
fragments
.iter()
.map(|&(extent, stride)| {
finer /= extent;
coord / finer % extent * stride
})
.sum::<usize>()
})
.sum()
}
const LOADS: [usize; 5] = [1, 2, 4, 8, 64];
const TILES: [(usize, usize); 3] = [(128, 8), (64, 8), (64, 1)];
#[test]
fn the_rhs_and_the_kernel_state_one_memory() {
let (k, n) = (128, 48);
for (tile, words_per_load) in TILES.iter().flat_map(|&tile| LOADS.map(|w| (tile, w))) {
let kernel = kernel_values_read(k, (N, n), tile, words_per_load);
let weight = matrix_values(&kernel);
assert_eq!(weight.logical_shape().unwrap().as_slice(), &[k, n]);
assert_eq!(
layout::tile(&weight),
tile,
"{tile:?}, {words_per_load} words"
);
let mut seen = vec![false; k * n];
for ki in 0..k {
for ni in 0..n {
let at = offset(&weight, &[ki, ni]);
let block = [ni, ki / BLOCK, ki % BLOCK];
assert_eq!(at, offset(&kernel, &block), "value ({ki}, {ni})");
assert!(
!std::mem::replace(&mut seen[at], true),
"two values at {at}"
);
}
}
}
}
#[test]
fn the_kernel_reads_back_what_the_rhs_states() {
let (k, n) = (128, 48);
for (tile, words_per_load) in TILES.iter().flat_map(|&tile| LOADS.map(|w| (tile, w))) {
let kernel = kernel_values_read(k, (N, n), tile, words_per_load);
let restated =
kernel_values(&matrix_values(&kernel)).expect("a tile-order rhs closes its blocks");
assert_eq!(restated, kernel, "{tile:?}, {words_per_load} words a load");
}
}
#[test]
fn every_load_width_stores_the_same_bytes() {
let (k, n) = (128, 48);
let address = |tile, words_per_load| {
let kernel = kernel_values_read(k, (N, n), tile, words_per_load);
(0..n)
.flat_map(|ni| (0..k).map(move |ki| (ni, ki)))
.map(|(ni, ki)| offset(&kernel, &[ni, ki / BLOCK, ki % BLOCK]))
.collect::<Vec<_>>()
};
for (tile, words_per_load) in TILES.iter().flat_map(|&tile| LOADS.map(|w| (tile, w))) {
assert_eq!(
address(tile, words_per_load),
address(tile, 1),
"{tile:?}, {words_per_load} words a load"
);
}
}
#[test]
fn a_descriptor_reads_the_words_where_the_tiles_hold_them() {
let (k, n) = (384, 24);
let tile = (128, 8);
let tiles = words_in_tiles(k, n, tile);
let kernel = kernel_values_read(k, (N, n), tile, 4);
for ni in 0..n {
for w in 0..k / VALUES_PER_WORD {
let (tile_k, tile_n) = tile;
let per_column = tile_k / VALUES_PER_WORD;
let coords = [ni / tile_n, w / per_column, ni % tile_n, w % per_column];
let at: usize = coords
.iter()
.zip(tiles.strides().iter())
.map(|(c, s)| c * s)
.sum();
let ki = w * VALUES_PER_WORD;
let value = offset(&kernel, &[ni, ki / BLOCK, ki % BLOCK]);
assert_eq!(at * VALUES_PER_WORD, value, "column {ni}, word {w}");
}
}
}
#[test]
fn the_rows_of_k_state_the_same_memory() {
let (k, n) = (128, 48);
let kernel = kernel_values_read(k, (N, n), (128, 8), 4);
let weight = matrix_values(&kernel);
let rows = values_in_rows(&weight);
assert_eq!(layout::tile(&rows), (8, 128));
for ki in 0..k {
for ni in 0..n {
assert_eq!(offset(&rows, &[ni, ki]), offset(&weight, &[ki, ni]));
}
}
}
#[test]
fn an_rhs_in_rows_has_no_kernel_statement() {
let rows = Metadata::new(Shape::new([64, 48]), vec![48, 1]);
assert!(kernel_values(&rows).is_none());
assert!(tile(&rows).is_none());
}
#[test]
fn an_rhs_states_the_tile_it_was_stored_in() {
let (k, n) = (128, 48);
for (stored, words_per_load) in TILES.iter().flat_map(|&tile| LOADS.map(|w| (tile, w))) {
let weight = matrix_values(&kernel_values_read(k, (N, n), stored, words_per_load));
assert_eq!(tile(&weight), Some(stored), "{words_per_load} words a load");
}
}
}