1use std::rc::Rc;
8
9use gwr_engine::sim_error;
10use gwr_engine::types::{SimError, SimResult};
11use rand::RngExt;
12
13use super::{Operator, Tensor, TensorPartition};
14use crate::processing_element::operators::{
15 HasShape, Shape, TensorView, apply_dim_partitions, partition_across_dimensions,
16};
17use crate::processing_element::{ComputeCapabilities, MachineOp, MachineOpCounts};
18
19const NAME: &str = "Gemm";
20
21fn choose_partition_dims<T: HasShape>(output: &T) -> Vec<usize> {
25 let dims = output.shape().get_dims();
26 let mut candidate_dims = dims
27 .iter()
28 .enumerate()
29 .filter_map(|(dim, size)| (*size > 1).then_some(dim))
30 .collect::<Vec<_>>();
31
32 if candidate_dims.is_empty() {
33 candidate_dims.push(output.num_dims().saturating_sub(1));
34 }
35
36 candidate_dims
37}
38
39const INPUT_A_OFFSET_M: usize = 2;
41const INPUT_A_OFFSET_K: usize = 1;
42const INPUT_B_OFFSET_K: usize = 2;
43const INPUT_B_OFFSET_N: usize = 1;
44const OUTPUT_OFFSET_M: usize = 2;
45const OUTPUT_OFFSET_N: usize = 1;
46
47fn get_inner_dim<T: HasShape>(has_shape: &T, i: usize) -> usize {
49 has_shape.shape().get_dims()[has_shape.num_dims() - i].max(1)
50}
51
52pub fn gemm_rhs_shape<T: HasShape>(input_a: &T) -> Result<Shape, SimError> {
54 let rank = input_a.num_dims();
55 if rank < 2 {
56 return sim_error!("{NAME}: input A must be at least 2D");
57 }
58
59 let mut rhs_dims = input_a.shape().get_dims().clone();
60 rhs_dims[rank - INPUT_B_OFFSET_K] = get_inner_dim(input_a, INPUT_A_OFFSET_K);
61 rhs_dims[rank - INPUT_B_OFFSET_N] = get_inner_dim(input_a, INPUT_A_OFFSET_M);
62 Ok(Shape::new(&rhs_dims))
63}
64
65fn choose_gemm_k(output: &Tensor, rng: &mut impl RngExt, expand_ratio: f64) -> usize {
70 let m = get_inner_dim(output, OUTPUT_OFFSET_M);
71 let n = get_inner_dim(output, OUTPUT_OFFSET_N);
72 let reference = m.max(n);
73 let scaled = ((reference as f64) * expand_ratio).round().max(1.0) as usize;
74 let lower = ((scaled as f64) * 0.75).round().max(1.0) as usize;
75 let upper = ((scaled as f64) * 1.25).round().max(lower as f64) as usize;
76
77 if lower == upper {
78 lower
79 } else {
80 rng.random_range(lower..=upper)
81 }
82}
83
84fn should_add_input_c(rng: &mut impl RngExt, expand_ratio: f64) -> bool {
85 if !expand_ratio.is_finite() || expand_ratio <= 0.0 {
86 false
87 } else if expand_ratio >= 1.0 {
88 true
89 } else {
90 rng.random_bool(expand_ratio)
91 }
92}
93
94fn output_tensor_from_inputs(inputs: &[Option<Tensor>]) -> Result<Tensor, SimError> {
95 let (input_a, input_b) = validate_inputs(inputs)?;
96
97 let output_shape = broadcast_shapes(&input_a.shape, &input_b.shape)?;
98 let output_dtype = if input_a.dtype > input_b.dtype {
99 input_a.dtype
100 } else {
101 input_b.dtype
102 };
103
104 Ok(Tensor {
105 id: None,
106 shape: output_shape,
107 dtype: output_dtype,
108 addr: 0,
109 })
110}
111
112pub fn maybe_add_input_c(
113 inputs: &mut Vec<Option<Tensor>>,
114 expand_ratio: f64,
115 rng: &mut impl RngExt,
116) -> Result<bool, SimError> {
117 if inputs.len() >= 3 || !should_add_input_c(rng, expand_ratio) {
118 return Ok(false);
119 }
120
121 inputs.push(Some(output_tensor_from_inputs(inputs)?));
122 Ok(true)
123}
124
125fn broadcast_shapes(a: &Shape, b: &Shape) -> Result<Shape, SimError> {
126 let rank_a = a.num_dims();
127 let rank_b = b.num_dims();
128 let rank_result = rank_a.max(rank_b);
129
130 if rank_result < 2 {
131 return sim_error!("{NAME}: inputs must be at least 2D ({:?} and {:?})", a, b);
132 }
133
134 let mut result = vec![1; rank_result];
135
136 for (i, result_i) in result.iter_mut().enumerate() {
137 let a_dim = a.get_dim(rank_result, i);
138 let b_dim = b.get_dim(rank_result, i);
139
140 *result_i = if i == (rank_result.saturating_sub(OUTPUT_OFFSET_M)) {
141 a_dim
143 } else if i == (rank_result.saturating_sub(OUTPUT_OFFSET_N)) {
144 b_dim
146 } else if a_dim == b_dim {
147 a_dim
148 } else if a_dim == 1 {
149 b_dim
150 } else if b_dim == 1 {
151 a_dim
152 } else {
153 return sim_error!("{NAME}: cannot broadcast shapes {:?} and {:?}", a, b);
154 };
155 }
156
157 Ok(Shape(result))
158}
159
160fn validate_inputs<T: HasShape>(inputs: &[Option<T>]) -> Result<(&T, &T), SimError> {
161 let input_a = inputs[0]
162 .as_ref()
163 .ok_or(SimError(format!("{NAME}: missing input 0")))?;
164 let input_b = inputs[1]
165 .as_ref()
166 .ok_or(SimError(format!("{NAME}: missing input 1")))?;
167 let shape_a = input_a.shape();
168 let shape_b = input_b.shape();
169 let output_shape = broadcast_shapes(shape_a, shape_b)?;
170
171 let k_a = get_inner_dim(shape_a, INPUT_A_OFFSET_K);
172 let k_b = get_inner_dim(shape_b, INPUT_B_OFFSET_K);
173 if k_a != k_b {
174 return sim_error!("{NAME}: incompatible K in {:?} x {:?}", shape_a, shape_b);
175 }
176
177 if inputs.len() == 2 {
178 } else if inputs.len() == 3
180 && let Some(input_c) = &inputs[2]
181 {
182 let out_m = get_inner_dim(&output_shape, OUTPUT_OFFSET_M);
184 let out_n = get_inner_dim(&output_shape, OUTPUT_OFFSET_N);
185
186 let shape_c = input_c.shape();
187 let c_m = get_inner_dim(&shape_c, OUTPUT_OFFSET_M);
188 let c_n = get_inner_dim(&shape_c, OUTPUT_OFFSET_N);
189 if (c_m != 1 && c_m != out_m) || (c_n != 1 && c_n != out_n) {
190 return sim_error!(
191 "{NAME}: input C incompatible ({:?} x {:?}) + {:?}",
192 shape_a,
193 shape_b,
194 shape_c
195 );
196 }
197 } else {
198 return sim_error!(
199 "{NAME}: {} input tensors found - expected 2 or 3",
200 inputs.len()
201 );
202 }
203
204 Ok((input_a, input_b))
205}
206
207fn validate_outputs<T: HasShape>(outputs: &[Option<T>]) -> Result<&T, SimError> {
208 if outputs.len() != 1 {
209 return sim_error!("{NAME}: {} outputs found - expected 1", outputs.len());
210 }
211 outputs[0]
212 .as_ref()
213 .ok_or(SimError(format!("{NAME}: missing output")))
214}
215
216fn validate_input_outputs<'a, 'b, T: HasShape>(
217 inputs: &'a [Option<T>],
218 outputs: &'b [Option<T>],
219) -> Result<(&'a T, &'a T, &'b T), SimError> {
220 let (input_a, input_b) = validate_inputs(inputs)?;
221 let output = validate_outputs(outputs)?;
222
223 let shape_a = input_a.shape();
224 let shape_b = input_b.shape();
225 let rank_inputs = input_a.num_dims().max(input_b.num_dims());
226 let shape_output = output.shape();
227 let rank_output = output.num_dims();
228
229 if rank_inputs != rank_output {
230 return sim_error!(
231 "{NAME}: incompatible ranks ({:?} x {:?} => {:?}",
232 shape_a,
233 shape_b,
234 shape_output
235 );
236 }
237
238 for i in 0..(rank_inputs - 2) {
241 let dim_a = input_a.get_dim(rank_inputs, i);
242 let dim_b = input_b.get_dim(rank_inputs, i);
243 let dim_in = if dim_a == 1 { dim_b } else { dim_a };
244 let dim_out = output.get_dim(rank_output, i);
245 if dim_in != 1 && dim_out != dim_in {
246 return sim_error!(
247 "{NAME}: Invalid output dimension {i} in {:?} x {:?} => {:?}",
248 shape_a,
249 shape_b,
250 shape_output
251 );
252 }
253 }
254
255 let input_m = get_inner_dim(input_a, INPUT_A_OFFSET_M);
256 let input_n = get_inner_dim(input_b, INPUT_B_OFFSET_N);
257 let output_m = get_inner_dim(output, OUTPUT_OFFSET_M);
258 let output_n = get_inner_dim(output, OUTPUT_OFFSET_N);
259
260 if (input_m != output_m) || (input_n != output_n) {
261 return sim_error!(
262 "{NAME}: Invalid M or N {:?} x {:?} => {:?}",
263 shape_a,
264 shape_b,
265 shape_output
266 );
267 }
268
269 Ok((input_a, input_b, output))
270}
271
272fn gemm_op_counts<T: HasShape>(
273 inputs: &[Option<T>],
274 outputs: &[Option<T>],
275) -> Result<(usize, usize), SimError> {
276 let (input_a_view, input_b_view, output_view) = validate_input_outputs(inputs, outputs)?;
277 let m = get_inner_dim(input_a_view, INPUT_A_OFFSET_M);
278 let k = get_inner_dim(input_a_view, INPUT_A_OFFSET_K);
279 let n = get_inner_dim(input_b_view, INPUT_B_OFFSET_N);
280
281 let num_matmuls = output_view
282 .shape()
283 .get_dims()
284 .iter()
285 .take(output_view.num_dims().saturating_sub(2))
286 .product::<usize>();
287
288 let num_muls = m * n * k * num_matmuls;
289 let num_matmul_adds = m * n * (k - 1) * num_matmuls;
290 let num_c_adds = usize::from(inputs.len() == 3) * output_view.shape().num_elements();
292 let num_adds = num_matmul_adds + num_c_adds;
293 Ok((num_muls, num_adds))
294}
295
296pub struct OperatorGemm {}
297
298impl OperatorGemm {
299 pub fn create_outputs(
300 &self,
301 inputs: &[Option<Tensor>],
302 _expand_ratio: f64,
303 _rng: &mut impl RngExt,
304 ) -> Result<Vec<Option<Tensor>>, gwr_engine::types::SimError> {
305 Ok(vec![Some(output_tensor_from_inputs(inputs)?)])
306 }
307
308 pub fn create_inputs(
309 &self,
310 outputs: &[Option<Tensor>],
311 expand_ratio: f64,
312 rng: &mut impl RngExt,
313 ) -> Result<Vec<Option<Tensor>>, gwr_engine::types::SimError> {
314 let output = validate_outputs(outputs)?;
315
316 let mut input_a_shape = output.shape.clone();
317 let mut input_b_shape = output.shape.clone();
318
319 let k = choose_gemm_k(output, rng, expand_ratio);
320
321 let rank = output.num_dims();
322 input_a_shape.0[rank.saturating_sub(INPUT_A_OFFSET_K)] = k;
323 input_b_shape.0[rank.saturating_sub(INPUT_B_OFFSET_K)] = k;
324
325 let mut inputs = vec![
326 Some(Tensor {
327 id: None,
328 shape: input_a_shape,
329 dtype: output.dtype,
330 addr: 0,
331 }),
332 Some(Tensor {
333 id: None,
334 shape: input_b_shape,
335 dtype: output.dtype,
336 addr: 0,
337 }),
338 ];
339
340 maybe_add_input_c(&mut inputs, expand_ratio, rng)?;
341
342 Ok(inputs)
343 }
344}
345
346impl Operator for OperatorGemm {
347 fn validate_tensors(&self, inputs: &[Option<Tensor>], outputs: &[Option<Tensor>]) -> SimResult {
348 validate_input_outputs(inputs, outputs)?;
349 Ok(())
350 }
351
352 fn compute_delay_ticks(
353 &self,
354 compute_capabilities: &Rc<ComputeCapabilities>,
355 inputs: &[Option<TensorView>],
356 outputs: &[Option<TensorView>],
357 ) -> Result<usize, SimError> {
358 let (num_muls, num_adds) = gemm_op_counts(inputs, outputs)?;
359 Ok(
360 compute_capabilities.cycles_for_ops(num_muls, MachineOp::Mul)?
361 + compute_capabilities.cycles_for_ops(num_adds, MachineOp::Add)?,
362 )
363 }
364
365 fn compute_machine_ops(
366 &self,
367 inputs: &[Option<TensorView>],
368 outputs: &[Option<TensorView>],
369 ) -> Result<MachineOpCounts, SimError> {
370 let (num_muls, num_adds) = gemm_op_counts(inputs, outputs)?;
371 Ok(MachineOpCounts {
372 adds: num_adds,
373 muls: num_muls,
374 ..MachineOpCounts::default()
375 })
376 }
377
378 fn partition_views(
379 &self,
380 input_views: &[Option<TensorView>],
381 output_views: &[Option<TensorView>],
382 num_partitions: usize,
383 ) -> Result<Vec<TensorPartition>, SimError> {
384 let (input_a_view, input_b_view, output_view) =
385 validate_input_outputs(input_views, output_views)?;
386
387 let input_c_view = if input_views.len() > 2 {
388 input_views[2].clone()
389 } else {
390 None
391 };
392
393 let rank = output_view.num_dims();
394 let partition_dims = choose_partition_dims(&output_view);
395 let output_view_dims = output_view.shape().get_dims();
396 let partition_specs =
397 partition_across_dimensions(output_view_dims, &partition_dims, num_partitions);
398 let m_dim = rank.saturating_sub(OUTPUT_OFFSET_M);
399 let n_dim = rank.saturating_sub(OUTPUT_OFFSET_N);
400
401 let mut partitions = Vec::with_capacity(partition_specs.len());
402 for spec in partition_specs {
403 let (output_shape, partition_offsets) = apply_dim_partitions(output_view_dims, &spec);
404 let output_offsets = output_view
405 .offsets()
406 .get_dims()
407 .iter()
408 .zip(partition_offsets.iter())
409 .map(|(base, offset)| base + offset)
410 .collect::<Vec<_>>();
411 let output_view =
412 TensorView::new(output_view.tensor().clone(), &output_shape, &output_offsets);
413
414 let a_spec = spec
415 .iter()
416 .filter(|partition| partition.dim != n_dim)
417 .cloned()
418 .collect::<Vec<_>>();
419 let b_spec = spec
420 .iter()
421 .filter(|partition| partition.dim != m_dim)
422 .cloned()
423 .collect::<Vec<_>>();
424
425 let split_m = spec.iter().any(|partition| partition.dim == m_dim);
426 let split_n = spec.iter().any(|partition| partition.dim == n_dim);
427 let split_outer = spec
428 .iter()
429 .any(|partition| partition.dim != m_dim && partition.dim != n_dim);
430
431 let input_a_view = if split_outer || split_m {
432 TensorView::from_output_partitions_on_view(input_a_view, rank, &a_spec)
433 } else {
434 input_a_view.clone()
435 };
436
437 let input_b_view = if split_outer || split_n {
438 TensorView::from_output_partitions_on_view(input_b_view, rank, &b_spec)
439 } else {
440 input_b_view.clone()
441 };
442
443 let input_c_view = input_c_view
444 .as_ref()
445 .map(|view| TensorView::from_output_partitions_on_view(view, rank, &spec));
446
447 let mut partition_inputs = vec![Some(input_a_view), Some(input_b_view)];
448 if let Some(view) = input_c_view {
449 partition_inputs.push(Some(view));
450 }
451
452 partitions.push(TensorPartition {
453 inputs: partition_inputs,
454 outputs: vec![Some(output_view)],
455 });
456 }
457
458 Ok(partitions)
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use crate::processing_element::operators::dtype::DataType;
466 use crate::processing_element::operators::{Operator, Shape, Tensor, partition_tensors};
467
468 fn tensor(shape: &[usize]) -> Option<Tensor> {
469 Some(Tensor::new(shape, &DataType::Bf16, 0))
470 }
471
472 fn tensor_view(shape: &[usize]) -> Option<TensorView> {
473 let tensor = Tensor::new(shape, &DataType::Bf16, 0);
474 Some(TensorView::new_full(tensor))
475 }
476
477 #[test]
478 fn test_broadcast_shapes() {
479 let a = Shape(vec![1, 1, 4, 5]);
480 let b = Shape(vec![1, 1, 5, 10]);
481 let c = broadcast_shapes(&a, &b).unwrap();
482 assert_eq!(c, Shape(vec![1, 1, 4, 10]));
483
484 let a = Shape(vec![3, 1, 4, 5]);
485 let b = Shape(vec![1, 5, 5, 10]);
486 let c = broadcast_shapes(&a, &b).unwrap();
487 assert_eq!(c, Shape(vec![3, 5, 4, 10]));
488 }
489
490 #[test]
491 fn create_outputs_uses_gemm_m_and_n_before_broadcasting() {
492 let operator = OperatorGemm {};
493 let mut rng = rand::rng();
494
495 let outputs = operator
496 .create_outputs(
497 &[tensor(&[1, 48, 1, 25]), tensor(&[1, 48, 25, 1])],
498 1.0,
499 &mut rng,
500 )
501 .unwrap();
502
503 assert_eq!(
504 outputs[0].as_ref().unwrap().shape(),
505 &Shape::new(&[1, 48, 1, 1])
506 );
507 }
508
509 #[test]
510 fn validate_gemm() {
511 let operator = OperatorGemm {};
512
513 operator
514 .validate_tensors(
515 &[tensor(&[1, 4, 5]), tensor(&[1, 5, 8])],
516 &[tensor(&[1, 4, 8])],
517 )
518 .unwrap();
519
520 operator
521 .validate_tensors(
522 &[tensor(&[3, 2, 10, 5]), tensor(&[5, 12])],
523 &[tensor(&[3, 2, 10, 12])],
524 )
525 .unwrap();
526 }
527
528 #[test]
529 fn invalid_broadcast_1() {
530 let operator = OperatorGemm {};
531
532 let err = operator
533 .validate_tensors(
534 &[tensor(&[2, 4, 5]), tensor(&[3, 5, 8])],
535 &[tensor(&[1, 4, 8])],
536 )
537 .unwrap_err();
538 assert!(format!("{err}").contains("cannot broadcast shapes"));
539 }
540
541 #[test]
542 fn invalid_broadcast_2() {
543 let operator = OperatorGemm {};
544
545 let err = operator
546 .validate_tensors(
547 &[tensor(&[4, 1, 4, 5]), tensor(&[3, 1, 5, 8])],
548 &[tensor(&[1, 1, 4, 8])],
549 )
550 .unwrap_err();
551 assert!(format!("{err}").contains("cannot broadcast shapes"));
552 }
553
554 #[test]
555 fn invalid_k() {
556 let operator = OperatorGemm {};
557
558 let err = operator
559 .validate_tensors(
560 &[tensor(&[9, 2, 4, 5]), tensor(&[9, 2, 4, 8])],
561 &[tensor(&[9, 2, 4, 8])],
562 )
563 .unwrap_err();
564 assert!(format!("{err}").contains("incompatible K"));
565 }
566
567 #[test]
568 fn get_inner_dim_uses_one_based_inner_offsets() {
569 let shape = Shape::new(&[7, 11, 13, 17]);
570
571 assert_eq!(get_inner_dim(&shape, 1), 17);
572 assert_eq!(get_inner_dim(&shape, 2), 13);
573 assert_eq!(get_inner_dim(&shape, 3), 11);
574 assert_eq!(get_inner_dim(&shape, 4), 7);
575 }
576
577 #[test]
578 fn gemm_named_offsets_map_to_expected_dimensions() {
579 let input_a = Shape::new(&[19, 23, 29, 31]);
580 let input_b = Shape::new(&[19, 23, 31, 37]);
581 let output = Shape::new(&[19, 23, 29, 37]);
582
583 assert_eq!(get_inner_dim(&input_a, INPUT_A_OFFSET_M), 29);
584 assert_eq!(get_inner_dim(&input_a, INPUT_A_OFFSET_K), 31);
585 assert_eq!(get_inner_dim(&input_b, INPUT_B_OFFSET_K), 31);
586 assert_eq!(get_inner_dim(&input_b, INPUT_B_OFFSET_N), 37);
587 assert_eq!(get_inner_dim(&output, OUTPUT_OFFSET_M), 29);
588 assert_eq!(get_inner_dim(&output, OUTPUT_OFFSET_N), 37);
589 }
590
591 #[test]
592 fn gemm_rhs_shape_uses_gemm_named_offsets() {
593 let input_a = Shape::new(&[19, 23, 29, 31]);
594 let input_b = gemm_rhs_shape(&input_a).unwrap();
595
596 assert_eq!(input_b, Shape::new(&[19, 23, 31, 29]));
597
598 OperatorGemm {}
599 .validate_tensors(
600 &[tensor(input_a.get_dims()), tensor(input_b.get_dims())],
601 &[tensor(&[19, 23, 29, 29])],
602 )
603 .unwrap();
604 }
605
606 #[test]
607 fn gemm_rhs_shape_rejects_rank_below_two() {
608 let err = gemm_rhs_shape(&Shape::new(&[31])).unwrap_err();
609
610 assert!(format!("{err}").contains("input A must be at least 2D"));
611 }
612
613 #[test]
614 fn delay_ticks_uses_m_k_and_n_from_the_innermost_dimensions() {
615 let operator = OperatorGemm {};
616 let compute_capabilities = Rc::new(ComputeCapabilities {
617 adds_per_tick: 1.0,
618 muls_per_tick: 1.0,
619 compares_per_tick: 100.0,
620 sram_bytes: 1024,
621 });
622 let delay_ticks = operator
623 .compute_delay_ticks(
624 &compute_capabilities,
625 &[tensor_view(&[2, 3, 4, 5]), tensor_view(&[2, 3, 5, 7])],
626 &[tensor_view(&[2, 3, 4, 7])],
627 )
628 .unwrap();
629
630 assert_eq!(delay_ticks, (2 * 3) * ((4 * 5 * 7) + (4 * 4 * 7)));
632 }
633
634 #[test]
635 fn delay_ticks() {
636 let operator = OperatorGemm {};
637 let compute_capabilities = Rc::new(ComputeCapabilities {
638 adds_per_tick: 1.0,
639 muls_per_tick: 1.0,
640 compares_per_tick: 100.0,
641 sram_bytes: 1024,
642 });
643 let delay_ticks = operator
644 .compute_delay_ticks(
645 &compute_capabilities,
646 &[tensor_view(&[4, 5]), tensor_view(&[5, 8])],
647 &[tensor_view(&[4, 8])],
648 )
649 .unwrap();
650 assert_eq!(delay_ticks, 160 + 128);
651
652 let delay_ticks = operator
653 .compute_delay_ticks(
654 &compute_capabilities,
655 &[tensor_view(&[10, 11, 4, 5]), tensor_view(&[5, 8])],
656 &[tensor_view(&[10, 11, 4, 8])],
657 )
658 .unwrap();
659 assert_eq!(delay_ticks, 17600 + 14080);
660 }
661
662 #[test]
663 fn flop_count_adds_multiplies_and_accumulates() {
664 let operator = OperatorGemm {};
665 assert_eq!(
666 operator
667 .compute_flops(
668 &[tensor_view(&[4, 5]), tensor_view(&[5, 8])],
669 &[tensor_view(&[4, 8])],
670 )
671 .unwrap(),
672 (4 * 5 * 8) + (4 * (5 - 1) * 8)
673 );
674 }
675
676 #[test]
677 fn flop_count_includes_optional_c_elementwise_add() {
678 let operator = OperatorGemm {};
679 assert_eq!(
680 operator
681 .compute_flops(
682 &[
683 tensor_view(&[4, 5]),
684 tensor_view(&[5, 8]),
685 tensor_view(&[4, 8]),
686 ],
687 &[tensor_view(&[4, 8])],
688 )
689 .unwrap(),
690 (4 * 5 * 8) + (4 * (5 - 1) * 8) + (4 * 8)
691 );
692 }
693
694 #[test]
695 fn create_inputs_with_expand_ratio_zero_omits_input_c() {
696 let operator = OperatorGemm {};
697 let mut rng = rand::rng();
698
699 let inputs = operator
700 .create_inputs(&[tensor(&[4, 8])], 0.0, &mut rng)
701 .unwrap();
702
703 assert_eq!(inputs.len(), 2);
704 operator
705 .validate_tensors(&inputs, &[tensor(&[4, 8])])
706 .unwrap();
707 }
708
709 #[test]
710 fn create_inputs_with_expand_ratio_one_adds_input_c() {
711 let operator = OperatorGemm {};
712 let mut rng = rand::rng();
713
714 let inputs = operator
715 .create_inputs(&[tensor(&[4, 8])], 1.0, &mut rng)
716 .unwrap();
717
718 assert_eq!(inputs.len(), 3);
719 assert_eq!(inputs[2].as_ref().unwrap().shape(), &Shape::new(&[4, 8]));
720 operator
721 .validate_tensors(&inputs, &[tensor(&[4, 8])])
722 .unwrap();
723 }
724
725 type OffsetsShapes = (&'static [usize], &'static [usize]);
726 type PartitionOffsetsShapes = (OffsetsShapes, OffsetsShapes, OffsetsShapes);
727 fn check_partitions(partitions: &[TensorPartition], expected: &[PartitionOffsetsShapes]) {
728 assert_eq!(partitions.len(), expected.len());
729 for (partition, expected_partition) in partitions.iter().zip(expected.iter()) {
730 let in_a = partition.inputs[0].as_ref().unwrap();
731 let in_b = partition.inputs[1].as_ref().unwrap();
732 let out = partition.outputs[0].as_ref().unwrap();
733
734 assert_eq!(
735 in_a.offsets().get_dims().as_slice(),
736 (expected_partition.0).0
737 );
738 assert_eq!(in_a.shape().get_dims().as_slice(), (expected_partition.0).1);
739 assert_eq!(
740 in_b.offsets().get_dims().as_slice(),
741 (expected_partition.1).0
742 );
743 assert_eq!(in_b.shape().get_dims().as_slice(), (expected_partition.1).1);
744 assert_eq!(
745 out.offsets().get_dims().as_slice(),
746 (expected_partition.2).0
747 );
748 assert_eq!(out.shape().get_dims().as_slice(), (expected_partition.2).1);
749 }
750 }
751
752 #[test]
753 fn partitions_prefer_outer_dims_before_m_or_n() {
754 let operator = OperatorGemm {};
755 let input_tensors = vec![tensor(&[3, 20, 10, 5]), tensor(&[3, 20, 5, 12])];
756 let output_tensors = vec![tensor(&[3, 20, 10, 12])];
757
758 let partitions = partition_tensors(&operator, &input_tensors, &output_tensors, 4).unwrap();
759 assert_eq!(partitions.len(), 6);
760
761 let expected: &[PartitionOffsetsShapes] = &[
762 (
763 (&[0, 0, 0, 0], &[1, 10, 10, 5]),
764 (&[0, 0, 0, 0], &[1, 10, 5, 12]),
765 (&[0, 0, 0, 0], &[1, 10, 10, 12]),
766 ),
767 (
768 (&[0, 10, 0, 0], &[1, 10, 10, 5]),
769 (&[0, 10, 0, 0], &[1, 10, 5, 12]),
770 (&[0, 10, 0, 0], &[1, 10, 10, 12]),
771 ),
772 (
773 (&[1, 0, 0, 0], &[1, 10, 10, 5]),
774 (&[1, 0, 0, 0], &[1, 10, 5, 12]),
775 (&[1, 0, 0, 0], &[1, 10, 10, 12]),
776 ),
777 (
778 (&[1, 10, 0, 0], &[1, 10, 10, 5]),
779 (&[1, 10, 0, 0], &[1, 10, 5, 12]),
780 (&[1, 10, 0, 0], &[1, 10, 10, 12]),
781 ),
782 (
783 (&[2, 0, 0, 0], &[1, 10, 10, 5]),
784 (&[2, 0, 0, 0], &[1, 10, 5, 12]),
785 (&[2, 0, 0, 0], &[1, 10, 10, 12]),
786 ),
787 (
788 (&[2, 10, 0, 0], &[1, 10, 10, 5]),
789 (&[2, 10, 0, 0], &[1, 10, 5, 12]),
790 (&[2, 10, 0, 0], &[1, 10, 10, 12]),
791 ),
792 ];
793 check_partitions(&partitions, expected);
794 }
795
796 #[test]
797 fn partitions_fall_back_to_m_when_no_outer_dims_are_available() {
798 let operator = OperatorGemm {};
799 let inputs = vec![tensor(&[10, 5]), tensor(&[5, 12])];
800 let outputs = vec![tensor(&[10, 12])];
801
802 let partitions = partition_tensors(&operator, &inputs, &outputs, 4).unwrap();
803 assert_eq!(partitions.len(), 4);
804
805 let expected_m_offsets = [0, 3, 6, 8];
806 let expected_m_lengths = [3, 3, 2, 2];
807
808 for (partition_idx, partition) in partitions.iter().enumerate() {
809 let a_view = partition.inputs[0].as_ref().unwrap();
810 let b_view = partition.inputs[1].as_ref().unwrap();
811 let out_view = partition.outputs[0].as_ref().unwrap();
812
813 assert_eq!(
814 a_view.shape().get_dims().as_slice(),
815 &[expected_m_lengths[partition_idx], 5]
816 );
817 assert_eq!(
818 a_view.offsets().get_dims().as_slice(),
819 &[expected_m_offsets[partition_idx], 0]
820 );
821
822 assert_eq!(b_view.shape().get_dims().as_slice(), &[5, 12]);
823 assert_eq!(b_view.offsets().get_dims().as_slice(), &[0, 0]);
824
825 assert_eq!(
826 out_view.shape().get_dims().as_slice(),
827 &[expected_m_lengths[partition_idx], 12]
828 );
829 assert_eq!(
830 out_view.offsets().get_dims().as_slice(),
831 &[expected_m_offsets[partition_idx], 0]
832 );
833 }
834 }
835
836 #[test]
837 fn can_partition_m_and_n_when_needed() {
838 let operator = OperatorGemm {};
839 let inputs = vec![tensor(&[4, 5]), tensor(&[5, 6])];
840 let outputs = vec![tensor(&[4, 6])];
841
842 let partitions = partition_tensors(&operator, &inputs, &outputs, 8).unwrap();
843 assert_eq!(partitions.len(), 8);
844
845 let expected: &[PartitionOffsetsShapes] = &[
846 ((&[0, 0], &[1, 5]), (&[0, 0], &[5, 3]), (&[0, 0], &[1, 3])),
847 ((&[0, 0], &[1, 5]), (&[0, 3], &[5, 3]), (&[0, 3], &[1, 3])),
848 ((&[1, 0], &[1, 5]), (&[0, 0], &[5, 3]), (&[1, 0], &[1, 3])),
849 ((&[1, 0], &[1, 5]), (&[0, 3], &[5, 3]), (&[1, 3], &[1, 3])),
850 ((&[2, 0], &[1, 5]), (&[0, 0], &[5, 3]), (&[2, 0], &[1, 3])),
851 ((&[2, 0], &[1, 5]), (&[0, 3], &[5, 3]), (&[2, 3], &[1, 3])),
852 ((&[3, 0], &[1, 5]), (&[0, 0], &[5, 3]), (&[3, 0], &[1, 3])),
853 ((&[3, 0], &[1, 5]), (&[0, 3], &[5, 3]), (&[3, 3], &[1, 3])),
854 ];
855 check_partitions(&partitions, expected);
856 }
857
858 #[test]
859 fn partitions_preserve_subset_view_offsets() {
860 let operator = OperatorGemm {};
861 let input_a = Tensor::new(&[8, 5], &DataType::Bf16, 0);
862 let input_b = Tensor::new(&[5, 9], &DataType::Bf16, 0);
863 let output = Tensor::new(&[8, 9], &DataType::Bf16, 0);
864
865 let input_views = vec![
866 Some(TensorView::new(input_a, &[4, 5], &[2, 0])),
867 Some(TensorView::new(input_b, &[5, 6], &[0, 3])),
868 ];
869 let output_views = vec![Some(TensorView::new(output, &[4, 6], &[2, 3]))];
870
871 let partitions = operator
872 .partition_views(&input_views, &output_views, 8)
873 .unwrap();
874 assert_eq!(partitions.len(), 8);
875
876 let expected: &[PartitionOffsetsShapes] = &[
877 ((&[2, 0], &[1, 5]), (&[0, 3], &[5, 3]), (&[2, 3], &[1, 3])),
878 ((&[2, 0], &[1, 5]), (&[0, 6], &[5, 3]), (&[2, 6], &[1, 3])),
879 ((&[3, 0], &[1, 5]), (&[0, 3], &[5, 3]), (&[3, 3], &[1, 3])),
880 ((&[3, 0], &[1, 5]), (&[0, 6], &[5, 3]), (&[3, 6], &[1, 3])),
881 ((&[4, 0], &[1, 5]), (&[0, 3], &[5, 3]), (&[4, 3], &[1, 3])),
882 ((&[4, 0], &[1, 5]), (&[0, 6], &[5, 3]), (&[4, 6], &[1, 3])),
883 ((&[5, 0], &[1, 5]), (&[0, 3], &[5, 3]), (&[5, 3], &[1, 3])),
884 ((&[5, 0], &[1, 5]), (&[0, 6], &[5, 3]), (&[5, 6], &[1, 3])),
885 ];
886 check_partitions(&partitions, expected);
887 }
888}