Skip to main content

cubecl_tile/space/partition/
table.rs

1//! The table a [`Partitioning`] prints as: one row a level, leaf up.
2
3use std::fmt::{self, Display, Formatter};
4
5use crate::{Axis, ComputeScope, Coverage, Level, Partitioning, Space, Spread};
6
7/// The leaf row's glyph.
8const LEAF: char = '◦';
9const MARGIN: &str = "  ";
10const LEVEL: &str = "  ";
11const TIMES: &str = " × ";
12const GAP: &str = "    ";
13
14/// A [`Partitioning`] with a name for each of its axes; unnamed axes print their index.
15pub struct LevelTable<'a> {
16    partitioning: &'a Partitioning,
17    labels: &'a [(Axis, &'a str)],
18}
19
20impl<'a> LevelTable<'a> {
21    pub(crate) fn new(partitioning: &'a Partitioning, labels: &'a [(Axis, &'a str)]) -> Self {
22        LevelTable {
23            partitioning,
24            labels,
25        }
26    }
27
28    fn label(&self, axis: Axis) -> String {
29        match self.labels.iter().find(|&&(named, _)| named == axis) {
30            Some(&(_, name)) => name.to_string(),
31            None => format!("a{}", axis.0),
32        }
33    }
34
35    /// Every row of the table, leaf up.
36    fn rows(&self) -> Vec<Row> {
37        let axes: Vec<Axis> = self.partitioning.space().axes().collect();
38        let mut space = self.partitioning.space().clone();
39        let mut rows: Vec<Row> = self
40            .partitioning
41            .levels()
42            .iter()
43            .map(|level| {
44                let row = Row::of(level, &space, &axes);
45                space = level.child(&space);
46                row
47            })
48            .collect();
49        rows.reverse();
50        rows.insert(0, Row::leaf(&space, &axes));
51        rows
52    }
53}
54
55impl Display for LevelTable<'_> {
56    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
57        let axes: Vec<Axis> = self.partitioning.space().axes().collect();
58        let header: Vec<String> = axes.iter().map(|&axis| self.label(axis)).collect();
59        let rows = self.rows();
60
61        let counts = Block::new("count", &header, rows.iter().map(|row| &row.counts));
62        let tiles = Block::new("tile", &header, rows.iter().map(|row| &row.tile));
63        let coverage_wide = rows
64            .iter()
65            .map(|row| row.coverage.chars().count())
66            .max()
67            .unwrap_or(0)
68            + GAP.chars().count();
69        let indent = format!("{MARGIN} {LEVEL}{:coverage_wide$}", "");
70
71        writeln!(
72            f,
73            "{indent}{}{}{GAP}{}{}",
74            counts.indent(),
75            counts.line(&header),
76            tiles.indent(),
77            tiles.line(&header)
78        )?;
79        writeln!(f)?;
80        for row in &rows {
81            writeln!(
82                f,
83                "{MARGIN}{}{LEVEL}{:coverage_wide$}{}{}{GAP}{}{}",
84                row.glyph,
85                row.coverage,
86                counts.indent(),
87                counts.line(&row.counts),
88                tiles.indent(),
89                tiles.line(&row.tile)
90            )?;
91        }
92        writeln!(f)?;
93        write!(f, "{indent}{}{GAP}{}", counts.rule(), tiles.rule())
94    }
95}
96
97/// Prints the table with no axis named; see [`Partitioning::table`].
98impl Display for Partitioning {
99    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
100        self.table(&[]).fmt(f)
101    }
102}
103
104/// One line of the table.
105struct Row {
106    glyph: char,
107    coverage: String,
108    counts: Vec<String>,
109    tile: Vec<String>,
110}
111
112impl Row {
113    fn of(level: &Level, space: &Space, axes: &[Axis]) -> Row {
114        Row {
115            glyph: Row::glyph(level.coverage()),
116            coverage: Row::coverage(level, space),
117            counts: axes
118                .iter()
119                .map(|&axis| Row::count(level, space, axis))
120                .collect(),
121            tile: axes.iter().map(|&axis| Row::extent(space, axis)).collect(),
122        }
123    }
124
125    /// The leaf row: every count is a one.
126    fn leaf(space: &Space, axes: &[Axis]) -> Row {
127        Row {
128            glyph: LEAF,
129            coverage: String::new(),
130            counts: axes.iter().map(|_| "·".to_string()).collect(),
131            tile: axes.iter().map(|&axis| Row::extent(space, axis)).collect(),
132        }
133    }
134
135    /// The glyph a level's coverage prints as.
136    fn glyph(coverage: Coverage) -> char {
137        match coverage {
138            Coverage::Distribute(ComputeScope::Cube) => '▣',
139            Coverage::Distribute(ComputeScope::Plane) => '▤',
140            Coverage::Distribute(ComputeScope::PlaneGroup { .. }) => '▥',
141            Coverage::Distribute(ComputeScope::Unit) => '▪',
142            Coverage::Walk => '↻',
143        }
144    }
145
146    /// What a level covers, in words.
147    fn coverage(level: &Level, space: &Space) -> String {
148        let counts: Vec<Option<usize>> = level
149            .axes()
150            .iter()
151            .map(|&axis| level.tiles_const(space, axis))
152            .collect();
153        let total = counts
154            .iter()
155            .try_fold(1usize, |acc, count| count.map(|n| acc * n));
156        let many = match total {
157            Some(n) => n.to_string(),
158            None => "?".to_string(),
159        };
160        let interleaved = level
161            .axes()
162            .iter()
163            .any(|&axis| level.spread(axis) == Some(Spread::Interleaved));
164        let mut note = match level.coverage() {
165            Coverage::Distribute(ComputeScope::Cube) => match level.shared_by() {
166                Some(cubes) => format!("{cubes} cubes sharing {many} boxes"),
167                None => format!("{many} cubes"),
168            },
169            Coverage::Distribute(ComputeScope::Plane) => match level.shared_by() {
170                Some(planes) => format!("{planes} planes sharing {many} boxes"),
171                None => format!("{many} planes a cube"),
172            },
173            Coverage::Distribute(ComputeScope::PlaneGroup { planes }) => match level.shared_by() {
174                Some(groups) => format!("{groups} groups of {planes} planes sharing {many} boxes"),
175                None => format!("{many} groups of {planes} planes a cube"),
176            },
177            Coverage::Distribute(ComputeScope::Unit) => format!("{many} units"),
178            Coverage::Walk => format!("{many} steps"),
179        };
180        if interleaved {
181            note += " interleaved";
182        }
183        match level.fillers() {
184            0 => {}
185            1 => note += ", 1 filling plane",
186            n => note += &format!(", {n} filling planes"),
187        }
188        note
189    }
190
191    /// How many tiles a level takes along one axis, as printed.
192    fn count(level: &Level, space: &Space, axis: Axis) -> String {
193        match level.count(axis) {
194            None => "·".to_string(),
195            Some(_) => match level.tiles_const(space, axis) {
196                Some(tiles) => tiles.to_string(),
197                None => "?".to_string(),
198            },
199        }
200    }
201
202    /// `axis`'s extent, or the mark a dynamic one prints as.
203    fn extent(space: &Space, axis: Axis) -> String {
204        match space.is_dynamic(axis) {
205            true => "?".to_string(),
206            false => space.extent(axis).to_string(),
207        }
208    }
209}
210
211/// One named block of columns, the counts or the tiles: every cell right-aligned in its column,
212/// `×` between, a rule naming the block beneath.
213struct Block<'n> {
214    name: &'n str,
215    /// The widest cell of each column, the header's own label included.
216    widths: Vec<usize>,
217}
218
219impl<'n> Block<'n> {
220    fn new<'c>(
221        name: &'n str,
222        header: &[String],
223        cells: impl Iterator<Item = &'c Vec<String>>,
224    ) -> Self {
225        let widths = cells.fold(
226            header.iter().map(|label| label.chars().count()).collect(),
227            |widest: Vec<usize>, row| {
228                widest
229                    .iter()
230                    .zip(row)
231                    .map(|(&widest, cell)| widest.max(cell.chars().count()))
232                    .collect()
233            },
234        );
235        Block { name, widths }
236    }
237
238    /// One line's cells of this block.
239    fn line(&self, cells: &[String]) -> String {
240        cells
241            .iter()
242            .zip(&self.widths)
243            .map(|(cell, &width)| format!("{cell:>width$}"))
244            .collect::<Vec<_>>()
245            .join(TIMES)
246    }
247
248    /// The rule naming this block.
249    fn rule(&self) -> String {
250        format!(
251            "└─ {} {}┘",
252            self.name,
253            "─".repeat(self.wide() - self.ruled() + 1)
254        )
255    }
256
257    /// The indent before a line's cells, so they end where the rule does.
258    fn indent(&self) -> String {
259        " ".repeat(self.wide() - self.spanned())
260    }
261
262    /// How wide this block prints: its cells, or its rule where that is wider.
263    fn wide(&self) -> usize {
264        self.spanned().max(self.ruled())
265    }
266
267    /// How wide this block's cells print, separators included.
268    fn spanned(&self) -> usize {
269        self.widths.iter().sum::<usize>()
270            + TIMES.chars().count() * (self.widths.len().saturating_sub(1))
271    }
272
273    /// The narrowest this block can print and still carry its rule.
274    fn ruled(&self) -> usize {
275        self.name.chars().count() + 6
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use crate::{Levels, Space};
283
284    const B: Axis = Axis(0);
285    const M: Axis = Axis(1);
286    const N: Axis = Axis(2);
287    const K: Axis = Axis(3);
288
289    /// A staged matmul's five levels.
290    fn staged() -> Partitioning {
291        Partitioning::new(
292            Space::new(&[(B, 4), (M, 512), (N, 1024), (K, 4096)]),
293            Levels::leaf(&[(M, 16), (N, 16), (K, 16)])
294                .walk(&[(M, 2), (N, 2)])
295                .walk(&[(K, 2)])
296                .planes(&[(M, 2), (N, 2)])
297                .walk_every(&[K])
298                .cubes(&[M, N])
299                .batches(&[B])
300                .build(),
301        )
302    }
303
304    #[test]
305    fn a_row_is_the_row_below_it_times_its_count() {
306        let labels = [(B, "b"), (M, "m"), (N, "n"), (K, "k")];
307
308        assert_eq!(
309            staged().table(&labels).to_string(),
310            [
311                "                        b × m ×  n ×   k    b ×   m ×    n ×    k",
312                "",
313                "  ◦                     · × · ×  · ×   ·    1 ×  16 ×   16 ×   16",
314                "  ↻  4 steps            · × 2 ×  2 ×   ·    1 ×  32 ×   32 ×   16",
315                "  ↻  2 steps            · × · ×  · ×   2    1 ×  32 ×   32 ×   32",
316                "  ▤  4 planes a cube    · × 2 ×  2 ×   ·    1 ×  64 ×   64 ×   32",
317                "  ↻  128 steps          · × · ×  · × 128    1 ×  64 ×   64 × 4096",
318                "  ▣  512 cubes          4 × 8 × 16 ×   ·    4 × 512 × 1024 × 4096",
319                "",
320                "                        └─ count ──────┘    └─ tile ────────────┘",
321            ]
322            .join("\n")
323        );
324    }
325
326    /// An unlabelled axis prints its index.
327    #[test]
328    fn an_unlabelled_axis_prints_its_index() {
329        let table = staged().to_string();
330
331        assert!(table.contains("a0 × a1 × a2 ×  a3"), "{table}");
332    }
333
334    /// A dynamic count prints as a question.
335    #[test]
336    fn a_dynamic_axis_prints_a_question() {
337        let partitioning = Partitioning::new(
338            Space::new(&[(M, 512), (K, 4096)]).with_dynamic(&[K]),
339            Levels::leaf(&[(M, 64), (K, 32)])
340                .walk_every(&[K])
341                .cubes(&[M])
342                .build(),
343        );
344        let table = partitioning.table(&[(M, "m"), (K, "k")]).to_string();
345
346        assert!(table.lines().any(|line| line.contains("× ?")), "{table}");
347    }
348
349    /// A shared level still prints as the cube grid.
350    #[test]
351    fn a_shared_level_still_prints_as_the_cube_grid() {
352        let partitioning = Partitioning::new(
353            Space::new(&[(M, 512), (N, 1024)]),
354            Levels::leaf(&[(M, 64), (N, 64)])
355                .cubes(&[M, N])
356                .shared_by(8)
357                .build(),
358        );
359
360        assert!(partitioning.to_string().contains('▣'));
361    }
362
363    /// An overhanging axis counts its partial tile.
364    #[test]
365    fn an_overhanging_axis_counts_its_partial_tile() {
366        let partitioning = Partitioning::new(
367            Space::new(&[(M, 500), (K, 4096)]),
368            Levels::leaf(&[(M, 128), (K, 32)])
369                .walk_every(&[K])
370                .cubes(&[M])
371                .build(),
372        );
373
374        assert_eq!(
375            partitioning.table(&[(M, "m"), (K, "k")]).to_string(),
376            [
377                "                      m ×   k      m ×    k",
378                "",
379                "  ◦                   · ×   ·    128 ×   32",
380                "  ↻  128 steps        · × 128    128 × 4096",
381                "  ▣  4 cubes          4 ×   ·    500 × 4096",
382                "",
383                "                  └─ count ─┘    └─ tile ─┘",
384            ]
385            .join("\n")
386        );
387    }
388}