1use core::fmt::{self, Display, Formatter};
4
5use cubecl::zspace::{MetadataError, SmallVec, Tiling};
6
7use crate::{Axis, Geometry, Space};
8
9const PIECES: usize = 12;
11
12type PieceList<T> = SmallVec<[T; PIECES]>;
14
15#[derive(Clone, PartialEq, Eq, Hash, Debug)]
24pub struct StoragePartitioning {
25 tiles: PieceList<(Axis, usize)>,
27 order: SmallVec<[Axis; Space::MAX_RANK]>,
29}
30
31impl StoragePartitioning {
32 pub fn new(geometry: &Geometry, labels: &[Axis]) -> Option<Self> {
35 let rank = geometry.rank();
36 let unlabelled = rank.saturating_sub(labels.len());
37 let tiling = geometry.tiling();
38 let first_stated = match tiling.is_tiled() {
40 true => tiling.logical_rank(rank).unwrap_or(rank),
41 false => rank,
42 };
43 let mut dims: PieceList<(usize, usize, usize)> = geometry
44 .dims()
45 .enumerate()
46 .filter(|&(dim, (extent, stride))| {
47 let piece_of_one = extent == 1 && dim >= first_stated;
48 dim >= unlabelled && (piece_of_one || extent > 1 && stride > 0)
49 })
50 .map(|(dim, (extent, stride))| (dim, extent, stride))
51 .collect();
52 dims.sort_by_key(|&(dim, extent, stride)| (stride, extent > 1, core::cmp::Reverse(dim)));
53 let (stated, rest): (PieceList<_>, PieceList<_>) = dims
54 .iter()
55 .copied()
56 .partition(|&(dim, _, _)| dim >= first_stated);
57 let finest_rest = rest.first().map_or(usize::MAX, |&(_, _, stride)| stride);
58 if stated
59 .iter()
60 .any(|&(_, extent, stride)| extent > 1 && stride > finest_rest)
61 {
62 return None;
63 }
64 Some(Self {
65 tiles: stated
66 .iter()
67 .map(|&(dim, extent, _)| (labels[dim - unlabelled], extent))
68 .collect(),
69 order: rest
70 .iter()
71 .map(|&(dim, _, _)| labels[dim - unlabelled])
72 .collect(),
73 })
74 }
75
76 pub fn tiles(&self) -> &[(Axis, usize)] {
78 &self.tiles
79 }
80
81 pub fn order(&self) -> &[Axis] {
83 &self.order
84 }
85
86 pub fn labels(&self, axes: &[Axis]) -> Vec<Axis> {
88 StoragePartitioning::level_major(axes, &self.fragments(axes))
89 }
90
91 pub(crate) fn level_major(axes: &[Axis], fragments: &[usize]) -> Vec<Axis> {
94 assert_eq!(
95 axes.len(),
96 fragments.len(),
97 "StoragePartitioning::level_major: {} axes but {} piece counts",
98 axes.len(),
99 fragments.len()
100 );
101 let depth = fragments.iter().copied().max().unwrap_or(0);
102 (0..depth)
103 .flat_map(|level| {
104 axes.iter()
105 .zip(fragments)
106 .filter(move |&(_, &pieces)| level < pieces)
107 .map(|(&axis, _)| axis)
108 })
109 .collect()
110 }
111
112 pub fn holds(
115 &self,
116 tile: &[(Axis, usize)],
117 extents: &[(Axis, usize)],
118 ) -> Result<(), TileMisfit> {
119 let mut pieces = self.pieces(extents)?;
120 pieces.reverse();
121 for &(axis, count) in tile {
122 let mut run = 1;
123 while run < count {
124 let Some(mut piece) = pieces.pop() else {
125 return Err(TileMisfit::RunsOut { wanted: axis });
126 };
127 if piece.count == 1 {
128 continue;
129 }
130 if piece.axis != axis {
131 return Err(TileMisfit::Interleaved {
132 wanted: axis,
133 found: piece.axis,
134 });
135 }
136 let missing = count / run;
137 if missing.is_multiple_of(piece.count) {
138 run *= piece.count;
139 continue;
140 }
141 if piece.stated || !piece.count.is_multiple_of(missing) {
142 return Err(TileMisfit::Overshoots {
143 axis,
144 wanted: count,
145 piece: piece.count,
146 });
147 }
148 piece.count /= missing;
149 pieces.push(piece);
150 run = count;
151 }
152 }
153 Ok(())
154 }
155
156 pub(crate) fn contiguous_tiles(
158 &self,
159 extents: &[(Axis, usize)],
160 ) -> PieceList<SmallVec<[(Axis, usize); Space::MAX_RANK]>> {
161 let Ok(pieces) = self.pieces(extents) else {
162 return PieceList::new();
163 };
164 let mut tiles = PieceList::new();
165 let mut tile: SmallVec<[(Axis, usize); Space::MAX_RANK]> = SmallVec::new();
166 for piece in pieces.into_iter().filter(|piece| piece.count > 1) {
167 let continues = tile.last().is_some_and(|&(axis, _)| axis == piece.axis);
168 match tile.iter_mut().find(|(axis, _)| *axis == piece.axis) {
169 Some(_) if !continues => break,
171 Some((_, extent)) => *extent *= piece.count,
172 None => tile.push((piece.axis, piece.count)),
173 }
174 tiles.push(tile.clone());
175 }
176 tiles
177 }
178
179 pub fn physical(&self, extents: &[(Axis, usize)]) -> Result<Geometry, StorageMisfit> {
182 let axes: SmallVec<[Axis; Space::MAX_RANK]> =
183 extents.iter().map(|&(axis, _)| axis).collect();
184 let named = |axis: &Axis| self.order.iter().filter(|&a| a == axis).count() == 1;
185 if self.order.len() != axes.len() || !axes.iter().all(named) {
186 return Err(StorageMisfit::Order {
187 order: self.order.to_vec(),
188 axes: axes.to_vec(),
189 });
190 }
191 let pieces = self.pieces(extents).map_err(|misfit| match misfit {
192 TileMisfit::Unwhole { axis, extent, tile } => {
193 StorageMisfit::PartialTile { axis, extent, tile }
194 }
195 _ => unreachable!("only the rest of an axis is computed against its extent"),
196 })?;
197 let tiling = Tiling::new(&self.fragments(&axes)).map_err(StorageMisfit::Unrecordable)?;
198 let strides: PieceList<usize> = pieces
199 .iter()
200 .scan(1, |run, piece| {
201 let stride = *run;
202 *run *= piece.count;
203 Some(stride)
204 })
205 .collect();
206 let mut taken: PieceList<bool> = SmallVec::from_elem(false, pieces.len());
208 let dims: PieceList<(usize, usize)> = self
209 .labels(&axes)
210 .iter()
211 .map(|&axis| {
212 let p = (0..pieces.len())
213 .rev()
214 .find(|&p| !taken[p] && pieces[p].axis == axis)
215 .expect("an axis has as many pieces as it has dims");
216 taken[p] = true;
217 (pieces[p].count, strides[p])
218 })
219 .collect();
220 Ok(Geometry::new(&dims).with_tiling(tiling))
221 }
222
223 fn fragments(&self, axes: &[Axis]) -> SmallVec<[usize; Space::MAX_RANK]> {
226 axes.iter()
227 .map(|&axis| 1 + self.tiles.iter().filter(|&&(a, _)| a == axis).count())
228 .collect()
229 }
230
231 fn pieces(&self, extents: &[(Axis, usize)]) -> Result<PieceList<Piece>, TileMisfit> {
233 let mut pieces: PieceList<Piece> = self
234 .tiles
235 .iter()
236 .map(|&(axis, count)| Piece {
237 axis,
238 count,
239 stated: true,
240 })
241 .collect();
242 for &axis in &self.order {
243 let extent = extents
244 .iter()
245 .find(|&&(a, _)| a == axis)
246 .map_or(1, |&(_, extent)| extent);
247 let tile: usize = self
248 .tiles
249 .iter()
250 .filter(|&&(a, _)| a == axis)
251 .map(|&(_, count)| count)
252 .product();
253 if !extent.is_multiple_of(tile) {
254 return Err(TileMisfit::Unwhole { axis, extent, tile });
255 }
256 pieces.push(Piece {
257 axis,
258 count: extent / tile,
259 stated: false,
260 });
261 }
262 Ok(pieces)
263 }
264}
265
266#[derive(Clone, Debug)]
268pub struct StorageLevels {
269 tiles: PieceList<(Axis, usize)>,
270}
271
272impl StorageLevels {
273 pub fn new(tile: &[(Axis, usize)]) -> Self {
275 Self {
276 tiles: SmallVec::from_slice(tile),
277 }
278 }
279
280 pub fn tile(mut self, level: &[(Axis, usize)]) -> Self {
282 self.tiles.extend_from_slice(level);
283 self
284 }
285
286 pub fn grid(self, order: &[Axis]) -> StoragePartitioning {
288 StoragePartitioning {
289 tiles: self.tiles,
290 order: SmallVec::from_slice(order),
291 }
292 }
293}
294
295#[derive(Clone, Copy, Debug, PartialEq, Eq)]
297pub enum TileMisfit {
298 Interleaved { wanted: Axis, found: Axis },
300 Overshoots {
302 axis: Axis,
303 wanted: usize,
304 piece: usize,
305 },
306 RunsOut { wanted: Axis },
308 Unwhole {
310 axis: Axis,
311 extent: usize,
312 tile: usize,
313 },
314}
315
316impl Display for TileMisfit {
317 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
318 match self {
319 Self::Interleaved { wanted, found } => write!(
320 f,
321 "a piece along {found:?} sits inside the one wanted along {wanted:?}"
322 ),
323 Self::Overshoots {
324 axis,
325 wanted,
326 piece,
327 } => write!(
328 f,
329 "a piece of {piece} along {axis:?} reaches past the {wanted} wanted, and is not \
330 the rest of the axis, which it would divide"
331 ),
332 Self::RunsOut { wanted } => {
333 write!(f, "nothing is left along {wanted:?} for the piece wanted")
334 }
335 Self::Unwhole { axis, extent, tile } => write!(
336 f,
337 "axis {axis:?} runs {extent}, which is not a whole number of {tile}-wide tiles"
338 ),
339 }
340 }
341}
342
343#[derive(Clone, Debug, PartialEq, Eq)]
345pub enum StorageMisfit {
346 PartialTile {
348 axis: Axis,
349 extent: usize,
350 tile: usize,
351 },
352 Order { order: Vec<Axis>, axes: Vec<Axis> },
354 Unrecordable(MetadataError),
356}
357
358impl Display for StorageMisfit {
359 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
360 match self {
361 Self::PartialTile { axis, extent, tile } => write!(
362 f,
363 "axis {axis:?} runs {extent}, which is not a whole number of {tile}-wide tiles"
364 ),
365 Self::Order { order, axes } => write!(
366 f,
367 "the tiles follow {order:?}, but the tensor's axes are {axes:?}: the order names \
368 each once"
369 ),
370 Self::Unrecordable(why) => {
371 write!(f, "the buffer's metadata cannot record its pieces: {why:?}")
372 }
373 }
374 }
375}
376
377#[derive(Clone, Copy, Debug)]
379struct Piece {
380 axis: Axis,
381 count: usize,
382 stated: bool,
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::*;
390
391 const M: Axis = Axis(0);
392 const N: Axis = Axis(1);
393 const K: Axis = Axis(2);
394
395 #[test]
397 fn a_written_buffer_reads_back_as_its_statement() {
398 let statements = [
399 StorageLevels::new(&[(N, 32), (K, 16)]).grid(&[N, K]),
400 StorageLevels::new(&[(N, 16), (K, 32)]).grid(&[K, N]),
401 StorageLevels::new(&[(N, 4), (N, 8)])
402 .tile(&[(K, 2), (K, 16)])
403 .grid(&[K, N]),
404 StorageLevels::new(&[(N, 4)])
405 .tile(&[(N, 1), (K, 2)])
406 .grid(&[N, K]),
407 ];
408 for storage in statements {
409 let geometry = storage.physical(&[(K, 64), (N, 64)]).unwrap();
410 let read = StoragePartitioning::new(&geometry, &geometry.labels(&[K, N])).unwrap();
411 assert_eq!(read, storage);
412 }
413 }
414
415 #[test]
417 fn an_untiled_buffer_is_its_order_alone() {
418 let rows = Geometry::new(&[(64, 4096), (4096, 1)]);
419 let read = StoragePartitioning::new(&rows, &[K, N]).unwrap();
420 assert!(read.tiles().is_empty());
421 assert_eq!(read.order(), &[N, K]);
422
423 let transposed = Geometry::new(&[(64, 1), (4096, 64)]);
424 let read = StoragePartitioning::new(&transposed, &[K, N]).unwrap();
425 assert_eq!(read.order(), &[K, N]);
426
427 let broadcast = Geometry::new(&[(3, 0), (1, 5), (4096, 1)]);
428 let read = StoragePartitioning::new(&broadcast, &[M, K, N]).unwrap();
429 assert_eq!(read.order(), &[N]);
430 }
431
432 #[test]
433 fn a_stored_read_and_its_whole_tile_are_both_held() {
434 let with_read = StorageLevels::new(&[(N, 4)])
435 .tile(&[(N, 8), (K, 32)])
436 .grid(&[N, K]);
437 let extents = [(K, 64), (N, 64)];
438 assert_eq!(with_read.holds(&[(N, 4)], &extents), Ok(()));
439 assert_eq!(with_read.holds(&[(N, 32), (K, 32)], &extents), Ok(()));
440 }
441
442 #[test]
443 fn a_stated_tile_is_never_split() {
444 let extents = [(K, 64), (N, 64)];
445 let tiles = StorageLevels::new(&[(N, 32), (K, 32)]).grid(&[N, K]);
446 assert_eq!(
447 tiles.holds(&[(N, 4)], &extents),
448 Err(TileMisfit::Overshoots {
449 axis: N,
450 wanted: 4,
451 piece: 32
452 })
453 );
454 let column_first = StorageLevels::new(&[(K, 32), (N, 32)]).grid(&[N, K]);
455 assert_eq!(
456 column_first.holds(&[(N, 32), (K, 32)], &extents),
457 Err(TileMisfit::Interleaved {
458 wanted: N,
459 found: K
460 })
461 );
462 }
463
464 #[test]
465 fn a_read_cuts_itself_out_of_the_rest_of_an_axis() {
466 let rows = StorageLevels::new(&[]).grid(&[N, K]);
467 let extents = [(K, 64), (N, 4096)];
468 assert_eq!(rows.holds(&[(N, 8)], &extents), Ok(()));
469 assert!(rows.holds(&[(N, 3)], &extents).is_err());
470 }
471
472 #[test]
474 fn a_word_and_a_two_dimensional_read_are_whole_tiles() {
475 let values = StorageLevels::new(&[(K, 8)])
476 .tile(&[(K, 2), (N, 2)])
477 .tile(&[(K, 8), (N, 8)])
478 .grid(&[K, N]);
479 let extents = [(K, 256), (N, 64)];
480 assert_eq!(values.holds(&[(K, 8)], &extents), Ok(()));
481 assert_eq!(values.holds(&[(K, 16), (N, 2)], &extents), Ok(()));
482 assert!(values.holds(&[(K, 32)], &extents).is_err());
483 }
484
485 #[test]
486 fn a_k_first_grid_moves_the_strides_not_the_dims() {
487 let storage = StorageLevels::new(&[(N, 16), (K, 32)]).grid(&[K, N]);
488 let geometry = storage.physical(&[(K, 128), (N, 64)]).unwrap();
489 assert_eq!(
491 geometry,
492 Geometry::new(&[(4, 512), (4, 2048), (32, 16), (16, 1)])
493 .with_tiling(cubecl::zspace::Tiling::new(&[2, 2]).unwrap())
494 );
495 }
496
497 #[test]
498 fn a_statement_that_does_not_fit_the_tensor_is_refused_when_written() {
499 let eight_rows = StorageLevels::new(&[(K, 8)]).grid(&[K, N]);
500 assert_eq!(
501 eight_rows.physical(&[(K, 12), (N, 4)]),
502 Err(StorageMisfit::PartialTile {
503 axis: K,
504 extent: 12,
505 tile: 8
506 })
507 );
508 let one_axis = StorageLevels::new(&[(K, 4)]).grid(&[K]);
509 assert!(matches!(
510 one_axis.physical(&[(K, 12), (N, 4)]),
511 Err(StorageMisfit::Order { .. })
512 ));
513 }
514
515 #[test]
517 fn a_piece_of_one_is_a_dim_kept_as_stated() {
518 let with_one = StorageLevels::new(&[(N, 4)])
519 .tile(&[(N, 1), (K, 2)])
520 .grid(&[N, K]);
521 let without = StorageLevels::new(&[(N, 4), (K, 2)]).grid(&[N, K]);
522 assert_ne!(with_one, without);
523 assert_eq!(with_one.labels(&[K, N]), vec![K, N, K, N, N]);
524 assert_eq!(without.labels(&[K, N]), vec![K, N, K, N]);
525 let (read, extents) = ([(N, 4), (K, 2)], [(K, 8), (N, 8)]);
526 assert_eq!(with_one.holds(&read, &extents), Ok(()));
527 assert_eq!(without.holds(&read, &extents), Ok(()));
528 }
529
530 #[test]
534 fn labels_name_the_dims_level_major_coarsest_first() {
535 let untiled = StorageLevels::new(&[]).grid(&[N, K]);
536 assert_eq!(untiled.labels(&[K, N]), vec![K, N]);
537 let one_level = StorageLevels::new(&[(N, 32), (K, 32)]).grid(&[N, K]);
538 assert_eq!(one_level.labels(&[K, N]), vec![K, N, K, N]);
539 let deeper_n = StorageLevels::new(&[(N, 4), (N, 8), (K, 32)]).grid(&[N, K]);
540 assert_eq!(deeper_n.labels(&[K, N]), vec![K, N, K, N, N]);
541 let batched = StorageLevels::new(&[(N, 32), (K, 32)]).grid(&[N, K, M]);
543 assert_eq!(batched.labels(&[M, K, N]), vec![M, K, N, K, N]);
544 }
545
546 fn windows(
548 storage: &StoragePartitioning,
549 extents: &[(Axis, usize)],
550 ) -> Vec<Vec<(Axis, usize)>> {
551 storage
552 .contiguous_tiles(extents)
553 .iter()
554 .map(|tile| tile.to_vec())
555 .collect()
556 }
557
558 #[test]
559 fn a_window_is_one_run_inside_the_tiles_whose_axes_do_not_interleave() {
560 let with_read = StorageLevels::new(&[(N, 4)])
561 .tile(&[(N, 8), (K, 32)])
562 .grid(&[N, K]);
563 assert_eq!(
564 windows(&with_read, &[(K, 64), (N, 64)]),
565 vec![vec![(N, 4)], vec![(N, 32)], vec![(N, 32), (K, 32)]]
566 );
567
568 let down_k = StorageLevels::new(&[(N, 16), (K, 32)]).grid(&[K, N]);
569 assert_eq!(
570 windows(&down_k, &[(K, 128), (N, 64)]),
571 vec![
572 vec![(N, 16)],
573 vec![(N, 16), (K, 32)],
574 vec![(N, 16), (K, 128)]
575 ]
576 );
577
578 let interleaved = StorageLevels::new(&[(N, 16), (K, 2)])
579 .tile(&[(N, 2), (K, 16)])
580 .grid(&[N, K]);
581 assert_eq!(
582 windows(&interleaved, &[(K, 64), (N, 64)]),
583 vec![vec![(N, 16)], vec![(N, 16), (K, 2)]]
584 );
585 }
586}