1use crate::tensor::{Metadata, Shape, Strides};
2
3const ROW_TILE: usize = 32;
6
7#[derive(Debug, PartialEq, Eq, Clone, Copy)]
9pub enum MatmulTransformAction {
10 Keep,
12 MergeBatches {
16 rows: usize,
18 },
19}
20
21impl MatmulTransformAction {
22 pub fn apply(&self, meta: &mut Metadata) {
28 let rows = match self {
29 MatmulTransformAction::Keep => return,
30 MatmulTransformAction::MergeBatches { rows } => *rows,
31 };
32
33 let rank = meta.rank();
34 let stride_rows = merged_row_stride(meta.shape(), meta.strides())
35 .expect("The action requires batch-contiguous rows");
36
37 for i in 0..rank - 2 {
38 meta.shape_mut()[i] = 1;
39 meta.strides_mut()[i] = rows * stride_rows;
40 }
41 meta.shape_mut()[rank - 2] = rows;
42 meta.strides_mut()[rank - 2] = stride_rows;
43 }
44}
45
46#[derive(Debug, PartialEq, Eq, Clone, Copy)]
49pub struct MatmulTransformAnalysis {
50 batches: usize,
52 rows: usize,
54 cols: usize,
56 mergeable: bool,
60}
61
62impl MatmulTransformAnalysis {
63 pub fn from_shapes(lhs: &Shape, rhs: &Shape) -> Self {
69 Self::new(lhs, rhs, true)
70 }
71
72 pub fn from_metadata(lhs: &Metadata, rhs: &Metadata, out: &Metadata) -> Self {
75 let mergeable = merged_row_stride(lhs.shape(), lhs.strides()).is_some()
76 && merged_row_stride(out.shape(), out.strides()).is_some();
77
78 Self::new(lhs.shape(), rhs.shape(), mergeable)
79 }
80
81 fn new(lhs: &Shape, rhs: &Shape, rows_contiguous: bool) -> Self {
82 let rank = lhs.num_dims();
83 let rank_rhs = rhs.num_dims();
84
85 let batches = lhs[..rank - 2].iter().product();
86 let rows = lhs[rank - 2];
87 let cols = rhs[rank_rhs - 1];
88
89 let rhs_shared = rhs[..rank_rhs - 2].iter().all(|&dim| dim == 1);
91
92 Self {
93 batches,
94 rows,
95 cols,
96 mergeable: rhs_shared && rows_contiguous,
97 }
98 }
99}
100
101#[derive(Debug, Default, Clone, Copy)]
104pub enum MatmulTransformPolicy {
105 #[default]
110 BetterTiling,
111 Never,
113}
114
115impl MatmulTransformPolicy {
116 pub fn action(&self, analysis: &MatmulTransformAnalysis) -> MatmulTransformAction {
118 match self {
119 MatmulTransformPolicy::Never => MatmulTransformAction::Keep,
120 MatmulTransformPolicy::BetterTiling => {
121 if !analysis.mergeable || analysis.batches == 1 {
122 return MatmulTransformAction::Keep;
123 }
124
125 if analysis.rows.is_multiple_of(ROW_TILE) {
127 return MatmulTransformAction::Keep;
128 }
129
130 let rows = analysis.batches * analysis.rows;
131
132 if !squarer(rows, analysis.rows, analysis.cols) {
134 return MatmulTransformAction::Keep;
135 }
136
137 MatmulTransformAction::MergeBatches { rows }
138 }
139 }
140 }
141}
142
143fn squarer(rows_new: usize, rows: usize, cols: usize) -> bool {
146 rows_new.min(cols) * rows.max(cols) > rows.min(cols) * rows_new.max(cols)
149}
150
151fn merged_row_stride(shape: &Shape, strides: &Strides) -> Option<usize> {
156 let rank = shape.num_dims();
157 let mut chained: Option<(usize, usize)> = None;
158
159 for i in (0..rank - 1).rev() {
160 if shape[i] == 1 {
161 continue;
162 }
163 match chained {
164 None => chained = Some((strides[i], strides[i] * shape[i])),
165 Some((row_stride, expected)) => {
166 if strides[i] != expected {
167 return None;
168 }
169 chained = Some((row_stride, strides[i] * shape[i]));
170 }
171 }
172 }
173
174 match chained {
175 Some((row_stride, _)) => Some(row_stride),
176 None => Some(shape[rank - 1] * strides[rank - 1]),
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::shape;
185
186 fn analysis(lhs: &[usize], rhs: &[usize]) -> MatmulTransformAnalysis {
187 MatmulTransformAnalysis::from_shapes(&Shape::from(lhs), &Shape::from(rhs))
188 }
189
190 fn action(analysis: &MatmulTransformAnalysis) -> MatmulTransformAction {
191 MatmulTransformPolicy::default().action(analysis)
192 }
193
194 #[test]
195 fn merges_batched_vec_mat() {
196 let analysis = analysis(&[16, 1, 4096], &[4096, 14336]);
198
199 assert_eq!(
200 action(&analysis),
201 MatmulTransformAction::MergeBatches { rows: 16 }
202 );
203 }
204
205 #[test]
206 fn keeps_row_tiled_batches() {
207 let analysis = analysis(&[8, 512, 4096], &[4096, 14336]);
210
211 assert_eq!(action(&analysis), MatmulTransformAction::Keep);
212 }
213
214 #[test]
215 fn keeps_single_batch() {
216 let analysis = analysis(&[1, 1, 4096], &[4096, 14336]);
217
218 assert_eq!(action(&analysis), MatmulTransformAction::Keep);
219 }
220
221 #[test]
222 fn keeps_matrix() {
223 let analysis = analysis(&[16, 4096], &[4096, 14336]);
224
225 assert_eq!(action(&analysis), MatmulTransformAction::Keep);
226 }
227
228 #[test]
229 fn keeps_batched_rhs() {
230 let analysis = analysis(&[8, 1, 64], &[8, 64, 32]);
232
233 assert_eq!(action(&analysis), MatmulTransformAction::Keep);
234 }
235
236 #[test]
237 fn merges_with_broadcast_rhs_rank() {
238 let analysis = analysis(&[8, 1, 64], &[1, 64, 32]);
239
240 assert_eq!(
241 action(&analysis),
242 MatmulTransformAction::MergeBatches { rows: 8 }
243 );
244 }
245
246 #[test]
247 fn keeps_when_merge_leaves_square() {
248 let analysis = analysis(&[4, 1000, 64], &[64, 64]);
251
252 assert_eq!(action(&analysis), MatmulTransformAction::Keep);
253 }
254
255 #[test]
256 fn merges_multiple_batch_dims() {
257 let analysis = analysis(&[2, 8, 1, 4096], &[4096, 14336]);
258
259 assert_eq!(
260 action(&analysis),
261 MatmulTransformAction::MergeBatches { rows: 16 }
262 );
263 }
264
265 #[test]
266 fn never_policy_keeps() {
267 let analysis = analysis(&[16, 1, 4096], &[4096, 14336]);
268
269 assert_eq!(
270 MatmulTransformPolicy::Never.action(&analysis),
271 MatmulTransformAction::Keep
272 );
273 }
274
275 #[test]
276 fn metadata_merges_pitched_rows() {
277 let lhs = Metadata::new(shape![16, 1, 4096], crate::strides![8192, 4096, 1]);
280 let rhs = Metadata::new(shape![1, 4096, 14336], crate::strides![0, 14336, 1]);
281 let out = Metadata::new(shape![16, 1, 14336], crate::strides![14336, 14336, 1]);
282
283 let analysis = MatmulTransformAnalysis::from_metadata(&lhs, &rhs, &out);
284
285 assert_eq!(
286 action(&analysis),
287 MatmulTransformAction::MergeBatches { rows: 16 }
288 );
289 }
290
291 #[test]
292 fn metadata_keeps_batch_holes() {
293 let lhs = Metadata::new(shape![4, 3, 8], crate::strides![48, 8, 1]);
296 let rhs = Metadata::new(shape![1, 8, 32], crate::strides![0, 32, 1]);
297 let out = Metadata::new(shape![4, 3, 32], crate::strides![96, 32, 1]);
298
299 let analysis = MatmulTransformAnalysis::from_metadata(&lhs, &rhs, &out);
300
301 assert_eq!(action(&analysis), MatmulTransformAction::Keep);
302 }
303
304 #[test]
305 fn metadata_merges_contiguous() {
306 let lhs = Metadata::new(shape![16, 1, 4096], crate::strides![4096, 4096, 1]);
307 let rhs = Metadata::new(shape![1, 4096, 14336], crate::strides![0, 14336, 1]);
308 let out = Metadata::new(shape![16, 1, 14336], crate::strides![14336, 14336, 1]);
309
310 let analysis = MatmulTransformAnalysis::from_metadata(&lhs, &rhs, &out);
311
312 assert_eq!(
313 action(&analysis),
314 MatmulTransformAction::MergeBatches { rows: 16 }
315 );
316 }
317
318 #[test]
319 fn metadata_ignores_unit_dim_strides() {
320 let lhs = Metadata::new(shape![16, 1, 4096], crate::strides![4096, 12345, 1]);
322 let rhs = Metadata::new(shape![1, 4096, 14336], crate::strides![0, 14336, 1]);
323 let out = Metadata::new(shape![16, 1, 14336], crate::strides![14336, 99999, 1]);
324
325 let analysis = MatmulTransformAnalysis::from_metadata(&lhs, &rhs, &out);
326
327 assert_eq!(
328 action(&analysis),
329 MatmulTransformAction::MergeBatches { rows: 16 }
330 );
331 }
332
333 #[test]
334 fn apply_folds_batches_in_place() {
335 let mut lhs = Metadata::new(shape![16, 1, 4096], crate::strides![4096, 4096, 1]);
336
337 MatmulTransformAction::MergeBatches { rows: 16 }.apply(&mut lhs);
338
339 assert_eq!(lhs.shape(), &shape![1, 16, 4096]);
340 assert_eq!(lhs.strides()[1], 4096);
341 assert_eq!(lhs.strides()[2], 1);
342 }
343
344 #[test]
345 fn apply_keep_is_noop() {
346 let mut lhs = Metadata::new(shape![16, 1, 4096], crate::strides![4096, 4096, 1]);
347
348 MatmulTransformAction::Keep.apply(&mut lhs);
349
350 assert_eq!(lhs.shape(), &shape![16, 1, 4096]);
351 }
352}