use cubecl::prelude::*;
use core::fmt::{self, Display, Formatter};
use cubecl::zspace::Tiling;
use crate::{Axis, StoragePartitioning, TileMisfit};
use crate::Coords;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Geometry {
shape: Vec<usize>,
strides: Vec<usize>,
tiling: Tiling,
}
impl Geometry {
pub fn new(dims: &[(usize, usize)]) -> Self {
Self {
shape: dims.iter().map(|&(extent, _)| extent).collect(),
strides: dims.iter().map(|&(_, stride)| stride).collect(),
tiling: Tiling::UNTILED,
}
}
pub(crate) fn with_tiling(mut self, tiling: Tiling) -> Self {
self.tiling = tiling;
self
}
pub fn tiling(&self) -> Tiling {
self.tiling
}
pub fn labels(&self, axes: &[Axis]) -> Vec<Axis> {
if !self.tiling.is_tiled() {
return axes.to_vec();
}
let logical_rank = self
.tiling
.logical_rank(self.rank())
.unwrap_or_else(|e| panic!("Geometry::labels: {e:?}"));
assert!(
logical_rank >= axes.len(),
"Geometry::labels: the buffer stands for {logical_rank} dims but {} axes name them",
axes.len()
);
let fragments = self.tiling.fragments(logical_rank);
let (batches, block) = fragments.split_at(logical_rank - axes.len());
assert!(
batches.iter().all(|&n| n == 1),
"Geometry::labels: batch dims are stored one dim each, so {batches:?} pieces over \
them cannot be named"
);
StoragePartitioning::level_major(axes, block)
}
pub fn shape(&self) -> &[usize] {
&self.shape
}
pub fn strides(&self) -> &[usize] {
&self.strides
}
pub fn rank(&self) -> usize {
self.shape.len()
}
pub fn in_stride_order(&self, labels: &[Axis]) -> (Geometry, Vec<Axis>) {
let batch_dims = self.rank() - labels.len();
let mut trailing: Vec<(usize, (usize, usize))> =
self.dims().enumerate().skip(batch_dims).collect();
trailing.sort_by_key(|&(_, (_, stride))| core::cmp::Reverse(stride));
let dims: Vec<(usize, usize)> = self
.dims()
.take(batch_dims)
.chain(trailing.iter().map(|&(_, dim)| dim))
.collect();
let ordered = trailing
.iter()
.map(|&(dim, _)| labels[dim - batch_dims])
.collect();
(Geometry::new(&dims), ordered)
}
pub fn dims(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
self.shape.iter().copied().zip(self.strides.iter().copied())
}
pub fn serves(&self, tile: &[(Axis, usize)], labels: &[Axis]) -> Result<(), LineMisfit> {
let values: usize = tile.iter().map(|&(_, count)| count).product();
if values == 1 {
return Ok(());
}
if self.rank() == 0 {
return Err(LineMisfit::NoDims);
}
let storage = StoragePartitioning::new(self, labels).ok_or(LineMisfit::NotPartitioned)?;
storage
.holds(tile, &self.extents(labels))
.map_err(LineMisfit::Tile)?;
let mut by_stride: Vec<(usize, (usize, usize))> = self
.dims()
.enumerate()
.filter(|&(_, (extent, stride))| extent > 1 && stride > 0)
.collect();
by_stride.sort_by_key(|&(dim, (_, stride))| (stride, core::cmp::Reverse(dim)));
let mut inside = Vec::new();
let mut run = 1;
for &(dim, (extent, stride)) in &by_stride {
if run >= values {
break;
}
if stride != run {
return Err(match run {
1 => LineMisfit::InnermostStrided(stride),
_ => LineMisfit::Gap(stride),
});
}
inside.push(dim);
run *= extent;
}
match self
.dims()
.enumerate()
.find(|&(dim, (_, stride))| !inside.contains(&dim) && !stride.is_multiple_of(values))
{
Some((_, (_, stride))) => Err(LineMisfit::StrideInsideLine(stride)),
None => Ok(()),
}
}
pub(crate) fn extents(&self, labels: &[Axis]) -> Vec<(Axis, usize)> {
let unlabelled = self.rank().saturating_sub(labels.len());
let mut extents: Vec<(Axis, usize)> = Vec::new();
for (dim, extent) in self.shape.iter().copied().enumerate().skip(unlabelled) {
let axis = labels[dim - unlabelled];
match extents.iter_mut().find(|(a, _)| *a == axis) {
Some((_, product)) => *product *= extent,
None => extents.push((axis, extent)),
}
}
extents
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineMisfit {
InnermostStrided(usize),
Gap(usize),
Tile(TileMisfit),
StrideInsideLine(usize),
NotPartitioned,
NoDims,
}
impl Display for LineMisfit {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::InnermostStrided(stride) => write!(
f,
"its finest dim steps by {stride} rather than 1, so consecutive values are not \
one run"
),
Self::Gap(stride) => write!(
f,
"a dim inside the tile steps by {stride}, leaving a gap in its run"
),
Self::Tile(why) => write!(f, "its storage does not hold the tile: {why}"),
Self::StrideInsideLine(stride) => write!(
f,
"its stride {stride} is not a whole number of tiles, so a coarser step lands \
inside one"
),
Self::NotPartitioned => write!(
f,
"a stated piece sits coarser than the rest of an axis, so its order is no \
partitioning"
),
Self::NoDims => write!(f, "it has no dims, so only a single value fits"),
}
}
}
impl From<&TensorBinding> for Geometry {
fn from(binding: &TensorBinding) -> Self {
Self {
shape: binding.shape.to_vec(),
strides: binding.strides.to_vec(),
tiling: binding.tiling,
}
}
}
#[derive(CubeType, Clone)]
#[expand(derive(Clone))]
pub struct RuntimeGeometry {
pub(crate) shape: Coords<u32>,
pub(crate) strides: Coords<u32>,
}
#[cube]
impl RuntimeGeometry {
#[allow(clippy::new_without_default)]
pub fn new() -> RuntimeGeometry {
RuntimeGeometry {
shape: Coords::<u32>::new(),
strides: Coords::<u32>::new(),
}
}
pub fn of_tensor<E: CubePrimitive>(
tensor: &Tensor<E>,
#[comptime] rank: usize,
) -> RuntimeGeometry {
let mut geometry = RuntimeGeometry::new();
#[unroll]
for i in 0..rank {
geometry.push(tensor.shape(i) as u32, tensor.stride(i) as u32);
}
geometry
}
pub fn push(&mut self, extent: u32, stride: u32) {
self.shape.push(extent);
self.strides.push(stride);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::StorageLevels;
const M: Axis = Axis(0);
const N: Axis = Axis(1);
const K: Axis = Axis(2);
#[test]
fn stride_order_reorders_the_dims_and_their_labels() {
let (dims, labels) = Geometry::new(&[(4096, 1), (6144, 4096)]).in_stride_order(&[K, N]);
assert_eq!(labels, vec![N, K]);
assert_eq!(dims, Geometry::new(&[(6144, 4096), (4096, 1)]));
let (dims, labels) = Geometry::new(&[(4096, 6144), (6144, 1)]).in_stride_order(&[K, N]);
assert_eq!(labels, vec![K, N]);
assert_eq!(dims, Geometry::new(&[(4096, 6144), (6144, 1)]));
let (dims, labels) =
Geometry::new(&[(8, 0), (4096, 1), (6144, 4096)]).in_stride_order(&[K, N]);
assert_eq!(labels, vec![N, K]);
assert_eq!(dims, Geometry::new(&[(8, 0), (6144, 4096), (4096, 1)]));
}
#[test]
fn a_line_is_served_where_the_line_rule_served_it() {
let geometries: Vec<(Geometry, Vec<Axis>)> = vec![
(Geometry::new(&[(64, 128), (128, 1)]), vec![M, N]),
(Geometry::new(&[(64, 136), (128, 1)]), vec![M, N]),
(Geometry::new(&[(64, 130), (130, 1)]), vec![M, N]),
(Geometry::new(&[(64, 1), (128, 64)]), vec![M, N]),
(Geometry::new(&[(3, 0), (64, 128), (128, 1)]), vec![M, N]),
(Geometry::new(&[(1, 5), (64, 128), (128, 1)]), vec![M, N]),
(Geometry::new(&[(2, 4096), (32, 1)]), vec![K, N]),
(
Geometry::new(&[(4, 2048), (4, 512), (16, 32), (32, 1)]),
vec![K, N, K, N],
),
(Geometry::new(&[(6, 1)]), vec![N]),
];
for (geometry, labels) in &geometries {
let line = *labels.last().unwrap();
for width in [1, 2, 3, 4, 8, 16, 32] {
assert_eq!(
geometry.serves(&[(line, width)], labels).is_ok(),
served_before(geometry, width),
"{geometry:?} at {width}"
);
}
}
}
#[test]
fn a_tiled_buffer_serves_the_reads_it_was_stored_for() {
let storage = StorageLevels::new(&[(N, 4)])
.tile(&[(N, 8), (K, 32)])
.grid(&[N, K]);
let geometry = storage.physical(&[(K, 64), (N, 64)]).unwrap();
let labels = geometry.labels(&[K, N]);
assert_eq!(geometry.serves(&[(N, 4)], &labels), Ok(()));
assert_eq!(geometry.serves(&[(N, 32), (K, 32)], &labels), Ok(()));
assert!(geometry.serves(&[(N, 2)], &labels).is_err());
assert!(geometry.serves(&[(N, 8)], &labels).is_err());
}
fn served_before(geometry: &Geometry, width: usize) -> bool {
if width == 1 {
return true;
}
let Some(last) = geometry.rank().checked_sub(1) else {
return false;
};
geometry.strides()[last] == 1
&& geometry.shape()[last].is_multiple_of(width)
&& geometry.strides()[..last]
.iter()
.all(|s| s.is_multiple_of(width))
}
}