use cubecl::{prelude::*, std::tensor::layout::CoordsDyn};
use crate::{
Axis, Coords, Extent, Space, TileMisfit,
algebra::{Integer, IntegerExpand, comptime_only},
};
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct VectorTile {
extents: Vec<(Axis, usize)>,
}
impl VectorTile {
pub fn new(
stored: &[(Axis, usize)],
innermost: Axis,
values: usize,
) -> Result<Self, TileMisfit> {
if stored.is_empty() || values == 1 {
return Ok(Self::run(innermost, values));
}
let mut extents: Vec<(Axis, usize)> = Vec::new();
let mut held = 1;
for &(axis, count) in stored.iter().filter(|&&(_, count)| count > 1) {
if held == values {
break;
}
if !(values / held).is_multiple_of(count) {
return Err(TileMisfit::Overshoots {
axis,
wanted: values / held,
piece: count,
});
}
held *= count;
match extents.last().copied() {
Some((last, extent)) if last == axis => {
*extents.last_mut().expect("just read") = (axis, extent * count);
}
Some((last, _)) if extents.iter().any(|&(a, _)| a == axis) => {
return Err(TileMisfit::Interleaved {
wanted: axis,
found: last,
});
}
_ => extents.push((axis, count)),
}
}
if held != values {
return Err(TileMisfit::RunsOut {
wanted: extents.last().map_or(innermost, |&(axis, _)| axis),
});
}
Ok(Self { extents })
}
pub fn run(axis: Axis, values: usize) -> Self {
Self {
extents: vec![(axis, values)],
}
}
pub fn extents(&self) -> &[(Axis, usize)] {
&self.extents
}
pub(crate) fn run_length(&self) -> usize {
self.extents.first().map_or(1, |&(_, run)| run)
}
pub fn values(&self) -> usize {
self.extents.iter().map(|&(_, extent)| extent).product()
}
pub(crate) fn along_one_axis(&self, reader: &str) -> (Axis, usize) {
match self.extents.as_slice() {
&[one] => one,
extents => panic!(
"{reader} places a load along one axis, and this operand loads {extents:?}: \
read it with a narrower vector, or through a path that places a tile"
),
}
}
pub(crate) fn extent_along(&self, axis: Axis) -> usize {
self.extents
.iter()
.find(|&&(a, _)| a == axis)
.map_or(1, |&(_, extent)| extent)
}
pub(crate) fn step_along(&self, axis: Axis) -> usize {
let mut step = 1;
for &(a, extent) in &self.extents {
if a == axis {
return step;
}
step *= extent;
}
0
}
pub(crate) fn offset_along(&self, position: usize, axis: Axis) -> usize {
let mut rest = position;
for &(a, extent) in &self.extents {
if a == axis {
return rest % extent;
}
rest /= extent;
}
0
}
pub(crate) fn loads_in(&self, axis: Axis, edge: usize, extent: Extent) -> usize {
let w = self.extent_along(axis);
assert!(
edge.is_multiple_of(w) || matches!(extent, Extent::Static(x) if x == edge),
"Memory::at: the edge {edge} along {axis:?} is neither a whole number of loads {w} \
wide there nor the axis's whole extent ({extent:?}), so a step would start mid-load"
);
edge.div_ceil(w)
}
pub(crate) fn counts(&self, space: &Space) -> Vec<usize> {
space
.axes()
.map(|axis| space.extent(axis).div_ceil(self.extent_along(axis)))
.collect()
}
pub(crate) fn parts(
&self,
stored: &[(Axis, usize)],
labels: &[Axis],
rank: usize,
) -> Vec<usize> {
let mut parts = vec![1; rank];
if stored.is_empty() || self.values() == 1 {
if let Some(last) = parts.last_mut() {
*last = self.values();
}
return parts;
}
let unlabelled = rank - labels.len();
let mut taken = 1;
let mut from_end: Vec<(Axis, usize)> = Vec::new();
for &(axis, count) in stored {
let nth = match from_end.iter_mut().find(|(a, _)| *a == axis) {
Some((_, seen)) => {
*seen += 1;
*seen
}
None => {
from_end.push((axis, 1));
1
}
};
if count == 1 {
continue;
}
if taken == self.values() {
break;
}
let dim = (0..labels.len())
.rev()
.filter(|&d| labels[d] == axis)
.nth(nth - 1)
.expect("a stated tile is a dim of the buffer");
parts[unlabelled + dim] = count;
taken *= count;
}
parts
}
}
#[cube]
impl VectorTile {
fn count_of(#[comptime] space: &Space, #[comptime] tile: &VectorTile) -> u32 {
comptime!(tile.counts(space).iter().product::<usize>() as u32).runtime()
}
fn start_of(
line: u32,
#[comptime] space: &Space,
#[comptime] tile: &VectorTile,
) -> Coords<u32> {
let rank = comptime!(space.rank());
let digits = Coords::constant(comptime!(tile.counts(space))).unravel(line);
let mut coords = Coords::<u32>::new();
#[unroll]
for p in 0..rank {
let extent = comptime!(tile.extent_along(space.axis_at(p)) as u32);
coords.push(digits.at(p).times(extent));
}
coords
}
fn index_of(
coords: &Coords<u32>,
#[comptime] space: &Space,
#[comptime] tile: &VectorTile,
) -> CoordsDyn {
let rank = comptime!(space.rank());
let mut index = CoordsDyn::new();
#[unroll]
for p in 0..rank {
let extent = comptime!(tile.extent_along(space.axis_at(p)) as u32);
index.push(coords.at(p).divided_by(extent));
}
index
}
}
impl VectorTile {
pub fn count(&self, space: &Space) -> u32 {
VectorTile::count_of(space, self)
}
pub fn start(&self, line: u32, space: &Space) -> Coords<u32> {
VectorTile::start_of(line, space, self)
}
pub fn index(&self, coords: &Coords<u32>, space: &Space) -> CoordsDyn {
VectorTile::index_of(coords, space, self)
}
pub fn __expand_count_method(&self, scope: &Scope, space: &Space) -> NativeExpand<u32> {
VectorTile::__expand_count_of(scope, space, self)
}
pub fn __expand_start_method(
&self,
scope: &Scope,
line: NativeExpand<u32>,
space: &Space,
) -> <Coords<u32> as CubeType>::ExpandType {
VectorTile::__expand_start_of(scope, line, space, self)
}
pub fn __expand_index_method(
&self,
scope: &Scope,
coords: &<Coords<u32> as CubeType>::ExpandType,
space: &Space,
) -> <CoordsDyn as CubeType>::ExpandType {
VectorTile::__expand_index_of(scope, coords, space, self)
}
}
comptime_only!(VectorTile);
#[cfg(test)]
mod tests {
use super::*;
const K: Axis = Axis(0);
const N: Axis = Axis(1);
#[test]
fn a_plain_buffer_loads_along_its_innermost_axis() {
assert_eq!(VectorTile::new(&[], N, 4).unwrap().extents(), &[(N, 4)]);
assert_eq!(VectorTile::new(&[], N, 1).unwrap().extents(), &[(N, 1)]);
}
#[test]
fn a_stored_buffer_loads_its_finest_tiles() {
let stored = [(K, 8), (K, 2), (N, 2), (K, 32), (N, 8)];
assert_eq!(
VectorTile::new(&stored, N, 32).unwrap().extents(),
&[(K, 16), (N, 2)]
);
assert_eq!(
VectorTile::new(&stored, N, 16).unwrap().extents(),
&[(K, 16)]
);
assert_eq!(VectorTile::new(&stored, N, 8).unwrap().extents(), &[(K, 8)]);
assert_eq!(VectorTile::new(&stored, N, 32).unwrap().values(), 32);
}
#[test]
fn a_load_that_cuts_a_stored_tile_is_refused() {
let stored = [(K, 8), (N, 2)];
assert_eq!(
VectorTile::new(&stored, N, 4),
Err(TileMisfit::Overshoots {
axis: K,
wanted: 4,
piece: 8
})
);
assert!(VectorTile::new(&stored, N, 24).is_err());
assert!(VectorTile::new(&stored, N, 32).is_err());
}
#[test]
fn a_load_that_returns_to_an_axis_is_refused() {
assert_eq!(
VectorTile::new(&[(K, 8), (N, 2), (K, 2)], N, 32),
Err(TileMisfit::Interleaved {
wanted: K,
found: N
})
);
}
#[test]
fn a_position_in_a_load_is_an_offset_along_each_axis() {
let load = VectorTile::new(&[(K, 8), (K, 2), (N, 2)], N, 32).unwrap();
assert_eq!(load.offset_along(20, K), 4);
assert_eq!(load.offset_along(20, N), 1);
assert_eq!(VectorTile::run(N, 4).offset_along(3, N), 3);
assert_eq!(VectorTile::run(N, 4).offset_along(3, K), 0);
}
#[test]
fn a_stored_tile_of_one_is_skipped() {
let stored = [(N, 4), (N, 1), (K, 2)];
assert_eq!(
VectorTile::new(&stored, N, 8).unwrap().extents(),
&[(N, 4), (K, 2)]
);
}
#[test]
#[should_panic(expected = "the fragment load places a load along one axis")]
fn a_rectangle_is_refused_by_a_one_axis_reader() {
let tile = VectorTile::new(&[(K, 8), (N, 2)], N, 16).unwrap();
tile.along_one_axis("the fragment load");
}
#[test]
fn a_load_covers_the_innermost_dim_or_the_stored_tiles_it_takes() {
let run = VectorTile::run(N, 4);
assert_eq!(run.parts(&[], &[K, N], 2), vec![1, 4]);
const M: Axis = Axis(2);
let stored = [(K, 8), (K, 2), (M, 2), (K, 4), (M, 4)];
let labels = [M, K, M, K, M, K, K];
let load = VectorTile::new(&stored, K, 32).unwrap();
assert_eq!(load.extents(), &[(K, 16), (M, 2)]);
assert_eq!(load.parts(&stored, &labels, 7), vec![1, 1, 1, 1, 2, 2, 8]);
assert_eq!(
load.parts(&stored, &labels, 8),
vec![1, 1, 1, 1, 1, 2, 2, 8]
);
}
}