1use cubecl::prelude::*;
4use cubecl::zspace::SmallVec;
5
6use crate::algebra::gcd;
7use crate::{Axis, Extent, Level, Shape};
8
9#[derive(CubeType, CubeLaunch, Clone, Debug)]
11pub struct Space {
12 #[cube(comptime)]
13 pub(crate) shape: Shape,
14 pub(crate) sizes: Sequence<usize>,
15}
16
17impl PartialEq for Space {
19 fn eq(&self, other: &Self) -> bool {
20 self.shape == other.shape
21 }
22}
23impl Eq for Space {}
24impl std::hash::Hash for Space {
25 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
26 self.shape.hash(state);
27 }
28}
29
30impl SpaceExpand {
32 pub(crate) fn comptime(&self) -> Space {
34 Space::from_shape(self.shape.clone())
35 }
36
37 #[allow(clippy::should_implement_trait)]
38 pub fn clone(&self) -> Space {
39 self.comptime()
40 }
41
42 pub fn shape(&self) -> &Shape {
43 &self.shape
44 }
45
46 pub fn rank(&self) -> usize {
47 self.shape.rank()
48 }
49
50 pub fn axis_at(&self, i: usize) -> Axis {
51 self.shape.axis_at(i)
52 }
53
54 pub fn position(&self, axis: Axis) -> usize {
55 self.shape.position(axis)
56 }
57
58 pub fn contains(&self, axis: Axis) -> bool {
59 self.shape.contains(axis)
60 }
61
62 pub fn axes(&self) -> impl Iterator<Item = Axis> + '_ {
63 self.shape.axes()
64 }
65
66 pub fn extent(&self, axis: Axis) -> usize {
67 self.shape.extent(axis)
68 }
69
70 pub fn extent_at(&self, i: usize) -> usize {
71 self.shape.extent_at(i)
72 }
73
74 pub fn is_dynamic(&self, axis: Axis) -> bool {
75 self.shape.is_dynamic(axis)
76 }
77
78 pub fn subspace(&self, axes: &[Axis]) -> Space {
80 Space::from_shape(self.shape.subspace(axes))
81 }
82}
83
84impl Space {
85 pub fn shape(&self) -> &Shape {
86 &self.shape
87 }
88
89 pub(crate) fn narrows_to(&self, other: &Space) -> bool {
91 self.axes()
92 .filter(|&axis| other.contains(axis))
93 .eq(other.axes())
94 && self.axes().last().is_some_and(|axis| other.contains(axis))
95 && self.axes().all(|axis| match other.contains(axis) {
96 true => self.extent(axis) == other.extent(axis),
97 false => self.extent(axis) == 1,
98 })
99 }
100
101 pub fn rank(&self) -> usize {
102 self.shape.rank()
103 }
104
105 pub fn axis_at(&self, i: usize) -> Axis {
106 self.shape.axis_at(i)
107 }
108
109 pub fn position(&self, axis: Axis) -> usize {
110 self.shape.position(axis)
111 }
112
113 pub fn contains(&self, axis: Axis) -> bool {
114 self.shape.contains(axis)
115 }
116
117 pub fn axes(&self) -> impl Iterator<Item = Axis> + '_ {
118 self.shape.axes()
119 }
120
121 pub(crate) fn extent_raw(&self, axis: Axis) -> Extent {
122 self.shape.extent_raw(axis)
123 }
124
125 pub fn extent(&self, axis: Axis) -> usize {
127 self.shape.extent(axis)
128 }
129
130 pub fn extent_at(&self, i: usize) -> usize {
131 self.shape.extent_at(i)
132 }
133
134 pub fn is_dynamic(&self, axis: Axis) -> bool {
135 self.shape.is_dynamic(axis)
136 }
137
138 pub fn subspace(&self, axes: &[Axis]) -> Space {
140 Space::from_shape(self.shape.subspace(axes))
141 }
142}
143
144#[cube]
145impl Space {
146 pub(crate) fn with_sizes(#[comptime] space: Space, sizes: Sequence<usize>) -> Space {
149 Space {
150 shape: comptime!(space.shape.clone()),
151 sizes,
152 }
153 }
154
155 pub fn count(&self, #[comptime] p: usize, #[comptime] edge: usize) -> usize {
157 match comptime!(self.shape.extent_raw(self.shape.axis_at(p))) {
158 Extent::Static(n) => comptime!(n.div_ceil(edge)).runtime(),
159 Extent::Dynamic => (*self.sizes.index(p)).div_ceil(edge),
160 }
161 }
162}
163
164impl Space {
165 pub const MAX_RANK: usize = 6;
167
168 pub fn new(extents: &[(Axis, usize)]) -> Self {
169 let extents: Vec<_> = extents
170 .iter()
171 .map(|&(a, n)| (a, Extent::Static(n)))
172 .collect();
173 Space::from_extents(&extents)
174 }
175
176 pub fn dynamic(axes: &[Axis]) -> Self {
178 let extents: Vec<_> = axes.iter().map(|&a| (a, Extent::Dynamic)).collect();
179 Space::from_extents(&extents)
180 }
181
182 pub(crate) fn from_extents(extents: &[(Axis, Extent)]) -> Self {
184 Space::from_shape(Shape::new(extents))
185 }
186
187 pub(crate) fn from_shape(shape: Shape) -> Self {
189 Space {
190 shape,
191 sizes: Sequence::new(),
192 }
193 }
194
195 pub fn with_dynamic(mut self, axes: &[Axis]) -> Self {
197 for axis in axes {
198 assert!(
199 self.contains(*axis),
200 "Space::with_dynamic: {axis:?} is not an axis of this space"
201 );
202 }
203 let entries: Vec<_> = self
204 .axes()
205 .map(|a| {
206 let extent = if axes.contains(&a) {
207 Extent::Dynamic
208 } else {
209 self.extent_raw(a)
210 };
211 (a, extent)
212 })
213 .collect();
214 self.shape = Shape::new(&entries);
215 self
216 }
217
218 pub fn all_dynamic(self) -> Self {
220 let axes: Vec<_> = self.axes().collect();
221 self.with_dynamic(&axes)
222 }
223
224 pub(crate) fn is_static(&self) -> bool {
226 self.shape.is_static()
227 }
228
229 pub fn extents(&self) -> Vec<(Axis, usize)> {
231 self.axes().map(|axis| (axis, self.extent(axis))).collect()
232 }
233
234 pub(crate) fn leaf(&self, levels: &[Level]) -> Space {
236 levels
237 .iter()
238 .fold(self.clone(), |space, level| level.child(&space))
239 }
240
241 pub fn merge(parts: &[&Space]) -> Space {
244 let mut entries: SmallVec<[(Axis, Extent); Space::MAX_RANK]> = SmallVec::new();
245
246 for part in parts {
247 for axis in part.axes() {
248 let extent = part.extent_raw(axis);
249 match entries.iter_mut().find(|(a, _)| *a == axis) {
250 Some(slot) => slot.1 = slot.1.broadcast(extent),
251 None => entries.push((axis, extent)),
252 }
253 }
254 }
255 Space::from_shape(Shape::new(&entries))
256 }
257
258 pub fn covering(parts: &[&Space]) -> Space {
267 let mut extents: Vec<(Axis, usize)> = Vec::new();
268 for part in parts {
269 for (axis, extent) in part.extents() {
270 match extents.iter_mut().find(|(a, _)| *a == axis) {
271 Some((_, held)) => *held = *held / gcd(*held, extent) * extent,
272 None => extents.push((axis, extent)),
273 }
274 }
275 }
276 Space::new(&extents)
277 }
278
279 pub fn tiles(&self, tile: &Space) -> usize {
287 tile.axes()
288 .map(|axis| {
289 let (whole, part) = (self.extent(axis), tile.extent(axis));
290 assert!(
291 whole.is_multiple_of(part),
292 "Space::tiles: {part} along {axis:?} does not tile {whole}"
293 );
294 whole / part
295 })
296 .product()
297 }
298
299 pub fn difference(&self, output: &Space) -> SmallVec<[Axis; Space::MAX_RANK]> {
301 self.axes().filter(|&axis| !output.contains(axis)).collect()
302 }
303
304 pub(crate) fn contracted_per_step(&self, contracted: &[Axis], width: usize) -> usize {
306 let lined = self.axis_at(self.rank() - 1);
307 let whole_lines = match self.extent_raw(lined) {
308 Extent::Static(extent) => extent.is_multiple_of(width),
309 Extent::Dynamic => true,
310 };
311 let sums_lines = width > 1 && contracted.last() == Some(&lined) && whole_lines;
312 if sums_lines { width } else { 1 }
313 }
314
315 pub fn contracted(operands: &[&Space], output: &Space) -> SmallVec<[Axis; Space::MAX_RANK]> {
317 let merged = Space::merge(operands);
318 let varies = |axis: Axis| merged.extent_raw(axis) != Extent::Static(1);
320 let shared = |axis: Axis| operands.iter().all(|operand| operand.contains(axis));
321 merged
322 .difference(output)
323 .into_iter()
324 .filter(|&axis| varies(axis) || shared(axis))
325 .collect()
326 }
327
328 pub(crate) fn contracted_extent(&self, output: &Space) -> usize {
330 self.difference(output)
331 .iter()
332 .map(|&axis| self.extent(axis))
333 .product()
334 }
335
336 pub(crate) fn contraction_agrees(lhs: &Space, rhs: &Space, output: &Space) -> bool {
338 let joint = Space::contracted(&[lhs, rhs], output);
339 let listed = |operand: &Space| -> SmallVec<[Axis; Space::MAX_RANK]> {
340 operand
341 .difference(output)
342 .into_iter()
343 .filter(|axis| joint.contains(axis))
344 .collect()
345 };
346 listed(lhs) == listed(rhs)
347 }
348
349 pub fn cells(&self) -> usize {
351 self.axes().map(|axis| self.extent(axis)).product()
352 }
353
354 pub fn slices_along(&self, axis: Axis) -> usize {
357 self.axes()
358 .filter(|&other| other != axis)
359 .map(|other| self.extent(other))
360 .product()
361 }
362
363 pub(crate) fn whole_run(&self, axis: Axis, widest: usize) -> usize {
366 let (mut run, mut rest) = (widest, self.extent(axis));
367 while rest != 0 {
368 (run, rest) = (rest, run % rest);
369 }
370 run
371 }
372}
373
374#[cube]
375impl Space {
376 pub(crate) fn runtime_extent_at(&self, #[comptime] i: usize) -> usize {
378 match comptime!(self.shape.extent_raw(self.shape.axis_at(i))) {
379 Extent::Static(n) => comptime!(n).runtime(),
380 Extent::Dynamic => *self.sizes.index(i),
381 }
382 }
383}
384
385#[cfg(test)]
386mod contraction_tests {
387 use crate::*;
388
389 const M: Axis = Axis(0);
390 const N: Axis = Axis(1);
391 const K: Axis = Axis(2);
392 const R: Axis = Axis(3);
393
394 #[test]
398 fn a_step_covers_every_load_and_counts_each() {
399 let values = Space::new(&[(K, 16), (N, 2)]);
400 let scales = Space::new(&[(K, 16), (N, 16)]);
401 let activation = Space::new(&[(K, 8)]);
402 let step = Space::covering(&[&activation, &scales, &values]);
403 assert_eq!(step, Space::new(&[(K, 16), (N, 16)]));
404 assert_eq!(
405 [&activation, &scales, &values].map(|load| step.tiles(load)),
406 [2, 1, 8]
407 );
408 }
409
410 #[test]
413 fn a_wider_scale_deepens_the_step() {
414 let values = Space::new(&[(K, 16), (N, 2)]);
415 let scales = Space::new(&[(K, 32), (N, 16)]);
416 let activation = Space::new(&[(K, 8)]);
417 let step = Space::covering(&[&activation, &scales, &values]);
418 assert_eq!(step, Space::new(&[(K, 32), (N, 16)]));
419 assert_eq!(
420 [&activation, &scales, &values].map(|load| step.tiles(load)),
421 [4, 1, 16]
422 );
423 }
424
425 #[test]
427 fn a_matmul_contracts_its_one_axis() {
428 let lhs = Space::new(&[(M, 8), (K, 4)]);
429 let out = Space::new(&[(M, 8), (N, 8)]);
430 assert_eq!(lhs.contracted_extent(&out), 4);
431 }
432
433 #[test]
435 fn a_convolution_contracts_taps_times_channels() {
436 let lhs = Space::new(&[(M, 8), (R, 3), (K, 4)]);
437 let out = Space::new(&[(M, 8), (N, 8)]);
438 assert_eq!(lhs.contracted_extent(&out), 12);
439 }
440
441 #[test]
443 fn contracting_nothing_is_a_unit_depth() {
444 let lhs = Space::new(&[(M, 8), (N, 8)]);
445 let out = Space::new(&[(M, 8), (N, 8)]);
446 assert_eq!(lhs.contracted_extent(&out), 1);
447 }
448
449 #[test]
451 fn operands_listing_one_contraction_order_agree() {
452 let lhs = Space::new(&[(M, 8), (R, 3), (K, 4)]);
453 let rhs = Space::new(&[(R, 3), (K, 4), (N, 8)]);
454 let out = Space::new(&[(M, 8), (N, 8)]);
455 assert!(Space::contraction_agrees(&lhs, &rhs, &out));
456 }
457
458 #[test]
460 fn a_permuted_contraction_order_disagrees() {
461 let lhs = Space::new(&[(M, 8), (R, 3), (K, 4)]);
462 let rhs = Space::new(&[(K, 4), (R, 3), (N, 8)]);
463 let out = Space::new(&[(M, 8), (N, 8)]);
464 assert!(!Space::contraction_agrees(&lhs, &rhs, &out));
465 }
466}