use cubecl::prelude::*;
use cubecl::zspace::SmallVec;
use crate::{Axis, Extent, Level, Shape};
#[derive(CubeType, CubeLaunch, Clone, Debug)]
pub struct Space {
#[cube(comptime)]
pub(crate) shape: Shape,
pub(crate) sizes: Sequence<usize>,
}
impl PartialEq for Space {
fn eq(&self, other: &Self) -> bool {
self.shape == other.shape
}
}
impl Eq for Space {}
impl std::hash::Hash for Space {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.shape.hash(state);
}
}
impl SpaceExpand {
pub(crate) fn comptime(&self) -> Space {
Space::from_shape(self.shape.clone())
}
#[allow(clippy::should_implement_trait)]
pub fn clone(&self) -> Space {
self.comptime()
}
pub fn shape(&self) -> &Shape {
&self.shape
}
pub fn rank(&self) -> usize {
self.shape.rank()
}
pub fn axis_at(&self, i: usize) -> Axis {
self.shape.axis_at(i)
}
pub fn position(&self, axis: Axis) -> usize {
self.shape.position(axis)
}
pub fn contains(&self, axis: Axis) -> bool {
self.shape.contains(axis)
}
pub fn axes(&self) -> impl Iterator<Item = Axis> + '_ {
self.shape.axes()
}
pub fn extent(&self, axis: Axis) -> usize {
self.shape.extent(axis)
}
pub fn extent_at(&self, i: usize) -> usize {
self.shape.extent_at(i)
}
pub fn is_dynamic(&self, axis: Axis) -> bool {
self.shape.is_dynamic(axis)
}
pub fn subspace(&self, axes: &[Axis]) -> Space {
Space::from_shape(self.shape.subspace(axes))
}
}
impl Space {
pub fn shape(&self) -> &Shape {
&self.shape
}
pub(crate) fn narrows_to(&self, other: &Space) -> bool {
self.axes()
.filter(|&axis| other.contains(axis))
.eq(other.axes())
&& self.axes().last().is_some_and(|axis| other.contains(axis))
&& self.axes().all(|axis| match other.contains(axis) {
true => self.extent(axis) == other.extent(axis),
false => self.extent(axis) == 1,
})
}
pub fn rank(&self) -> usize {
self.shape.rank()
}
pub fn axis_at(&self, i: usize) -> Axis {
self.shape.axis_at(i)
}
pub fn position(&self, axis: Axis) -> usize {
self.shape.position(axis)
}
pub fn contains(&self, axis: Axis) -> bool {
self.shape.contains(axis)
}
pub fn axes(&self) -> impl Iterator<Item = Axis> + '_ {
self.shape.axes()
}
pub(crate) fn extent_raw(&self, axis: Axis) -> Extent {
self.shape.extent_raw(axis)
}
pub fn extent(&self, axis: Axis) -> usize {
self.shape.extent(axis)
}
pub fn extent_at(&self, i: usize) -> usize {
self.shape.extent_at(i)
}
pub fn is_dynamic(&self, axis: Axis) -> bool {
self.shape.is_dynamic(axis)
}
pub fn subspace(&self, axes: &[Axis]) -> Space {
Space::from_shape(self.shape.subspace(axes))
}
}
#[cube]
impl Space {
pub(crate) fn with_sizes(#[comptime] space: Space, sizes: Sequence<usize>) -> Space {
Space {
shape: comptime!(space.shape.clone()),
sizes,
}
}
pub fn count(&self, #[comptime] p: usize, #[comptime] edge: usize) -> usize {
match comptime!(self.shape.extent_raw(self.shape.axis_at(p))) {
Extent::Static(n) => comptime!(n.div_ceil(edge)).runtime(),
Extent::Dynamic => (*self.sizes.index(p)).div_ceil(edge),
}
}
}
impl Space {
pub const MAX_RANK: usize = 6;
pub fn new(extents: &[(Axis, usize)]) -> Self {
let extents: Vec<_> = extents
.iter()
.map(|&(a, n)| (a, Extent::Static(n)))
.collect();
Space::from_extents(&extents)
}
pub fn dynamic(axes: &[Axis]) -> Self {
let extents: Vec<_> = axes.iter().map(|&a| (a, Extent::Dynamic)).collect();
Space::from_extents(&extents)
}
pub(crate) fn from_extents(extents: &[(Axis, Extent)]) -> Self {
Space::from_shape(Shape::new(extents))
}
pub(crate) fn from_shape(shape: Shape) -> Self {
Space {
shape,
sizes: Sequence::new(),
}
}
pub fn with_dynamic(mut self, axes: &[Axis]) -> Self {
for axis in axes {
assert!(
self.contains(*axis),
"Space::with_dynamic: {axis:?} is not an axis of this space"
);
}
let entries: Vec<_> = self
.axes()
.map(|a| {
let extent = if axes.contains(&a) {
Extent::Dynamic
} else {
self.extent_raw(a)
};
(a, extent)
})
.collect();
self.shape = Shape::new(&entries);
self
}
pub fn all_dynamic(self) -> Self {
let axes: Vec<_> = self.axes().collect();
self.with_dynamic(&axes)
}
pub(crate) fn is_static(&self) -> bool {
self.shape.is_static()
}
pub fn extents(&self) -> Vec<(Axis, usize)> {
self.axes().map(|axis| (axis, self.extent(axis))).collect()
}
pub(crate) fn leaf(&self, levels: &[Level]) -> Space {
levels
.iter()
.fold(self.clone(), |space, level| level.child(&space))
}
pub fn merge(parts: &[&Space]) -> Space {
let mut entries: SmallVec<[(Axis, Extent); Space::MAX_RANK]> = SmallVec::new();
for part in parts {
for axis in part.axes() {
let extent = part.extent_raw(axis);
match entries.iter_mut().find(|(a, _)| *a == axis) {
Some(slot) => slot.1 = merge_level(slot.1, extent),
None => entries.push((axis, extent)),
}
}
}
Space::from_shape(Shape::new(&entries))
}
pub fn difference(&self, output: &Space) -> SmallVec<[Axis; Space::MAX_RANK]> {
self.axes().filter(|&axis| !output.contains(axis)).collect()
}
pub(crate) fn contracted_per_step(&self, contracted: &[Axis], width: usize) -> usize {
let lined = self.axis_at(self.rank() - 1);
let whole_lines = match self.extent_raw(lined) {
Extent::Static(extent) => extent.is_multiple_of(width),
Extent::Dynamic => true,
};
let folds = width > 1 && contracted.last() == Some(&lined) && whole_lines;
if folds { width } else { 1 }
}
pub fn contracted(operands: &[&Space], output: &Space) -> SmallVec<[Axis; Space::MAX_RANK]> {
let merged = Space::merge(operands);
let varies = |axis: Axis| merged.extent_raw(axis) != Extent::Static(1);
let shared = |axis: Axis| operands.iter().all(|operand| operand.contains(axis));
merged
.difference(output)
.into_iter()
.filter(|&axis| varies(axis) || shared(axis))
.collect()
}
pub(crate) fn contracted_extent(&self, output: &Space) -> usize {
self.difference(output)
.iter()
.map(|&axis| self.extent(axis))
.product()
}
pub(crate) fn contraction_agrees(lhs: &Space, rhs: &Space, output: &Space) -> bool {
let joint = Space::contracted(&[lhs, rhs], output);
let listed = |operand: &Space| -> SmallVec<[Axis; Space::MAX_RANK]> {
operand
.difference(output)
.into_iter()
.filter(|axis| joint.contains(axis))
.collect()
};
listed(lhs) == listed(rhs)
}
pub fn cells(&self) -> usize {
self.axes().map(|axis| self.extent(axis)).product()
}
pub fn slices_along(&self, axis: Axis) -> usize {
self.axes()
.filter(|&other| other != axis)
.map(|other| self.extent(other))
.product()
}
}
#[cube]
impl Space {
pub(crate) fn runtime_extent_at(&self, #[comptime] i: usize) -> usize {
match comptime!(self.shape.extent_raw(self.shape.axis_at(i))) {
Extent::Static(n) => comptime!(n).runtime(),
Extent::Dynamic => *self.sizes.index(i),
}
}
}
fn merge_level(a: Extent, b: Extent) -> Extent {
match (a, b) {
(Extent::Static(1), b) => b,
(a, Extent::Static(1)) => a,
(Extent::Dynamic, _) | (_, Extent::Dynamic) => Extent::Dynamic,
(Extent::Static(a), Extent::Static(b)) if a == b => Extent::Static(a),
_ => panic!("Space::merge: axis appears with conflicting extents"),
}
}
#[cfg(test)]
mod contraction_tests {
use crate::*;
const M: Axis = Axis(0);
const N: Axis = Axis(1);
const K: Axis = Axis(2);
const R: Axis = Axis(3);
#[test]
fn a_matmul_contracts_its_one_axis() {
let lhs = Space::new(&[(M, 8), (K, 4)]);
let out = Space::new(&[(M, 8), (N, 8)]);
assert_eq!(lhs.contracted_extent(&out), 4);
}
#[test]
fn a_convolution_contracts_taps_times_channels() {
let lhs = Space::new(&[(M, 8), (R, 3), (K, 4)]);
let out = Space::new(&[(M, 8), (N, 8)]);
assert_eq!(lhs.contracted_extent(&out), 12);
}
#[test]
fn contracting_nothing_is_a_unit_depth() {
let lhs = Space::new(&[(M, 8), (N, 8)]);
let out = Space::new(&[(M, 8), (N, 8)]);
assert_eq!(lhs.contracted_extent(&out), 1);
}
#[test]
fn operands_listing_one_contraction_order_agree() {
let lhs = Space::new(&[(M, 8), (R, 3), (K, 4)]);
let rhs = Space::new(&[(R, 3), (K, 4), (N, 8)]);
let out = Space::new(&[(M, 8), (N, 8)]);
assert!(Space::contraction_agrees(&lhs, &rhs, &out));
}
#[test]
fn a_permuted_contraction_order_disagrees() {
let lhs = Space::new(&[(M, 8), (R, 3), (K, 4)]);
let rhs = Space::new(&[(K, 4), (R, 3), (N, 8)]);
let out = Space::new(&[(M, 8), (N, 8)]);
assert!(!Space::contraction_agrees(&lhs, &rhs, &out));
}
}