1use crate::analyze::KernelPattern;
7
8pub fn output_tensor_names(pattern: &KernelPattern) -> Vec<&str> {
10 match pattern {
11 KernelPattern::MatMul { output, .. }
12 | KernelPattern::QuantizedMatMul { output, .. }
13 | KernelPattern::ElementWise { output, .. }
14 | KernelPattern::Conv2D { output, .. }
15 | KernelPattern::Pool { output, .. }
16 | KernelPattern::Activation { output, .. }
17 | KernelPattern::Reduce { output, .. }
18 | KernelPattern::Transpose { output, .. }
19 | KernelPattern::Reshape { output, .. }
20 | KernelPattern::Normalization { output, .. }
21 | KernelPattern::Concat { output, .. }
22 | KernelPattern::Attention { output, .. }
23 | KernelPattern::Gather { output, .. }
24 | KernelPattern::Scatter { output, .. }
25 | KernelPattern::ElementWiseChain { output, .. } => vec![output.name.as_str()],
26 KernelPattern::Split { outputs, .. } => outputs.iter().map(|t| t.name.as_str()).collect(),
27 KernelPattern::Unknown { .. } => vec![],
28 }
29}
30
31pub fn input_tensor_names(pattern: &KernelPattern) -> Vec<&str> {
33 match pattern {
34 KernelPattern::MatMul { inputs, .. } => inputs.iter().map(|t| t.name.as_str()).collect(),
35 KernelPattern::QuantizedMatMul {
39 input,
40 weight,
41 scale,
42 bias,
43 ..
44 } => [input, weight, scale]
45 .into_iter()
46 .chain(bias.as_ref())
47 .map(|t| t.name.as_str())
48 .collect(),
49 KernelPattern::ElementWise { inputs, .. } => {
50 inputs.iter().map(|t| t.name.as_str()).collect()
51 }
52 KernelPattern::Conv2D { input, weight, .. } => {
53 vec![input.name.as_str(), weight.name.as_str()]
54 }
55 KernelPattern::Pool { input, .. }
56 | KernelPattern::Activation { input, .. }
57 | KernelPattern::Reduce { input, .. }
58 | KernelPattern::Transpose { input, .. }
59 | KernelPattern::Reshape { input, .. }
60 | KernelPattern::Split { input, .. } => vec![input.name.as_str()],
61 KernelPattern::Normalization {
62 input, scale, bias, ..
63 } => vec![input.name.as_str(), scale.name.as_str(), bias.name.as_str()],
64 KernelPattern::Concat { inputs, .. } => inputs.iter().map(|t| t.name.as_str()).collect(),
65 KernelPattern::Attention {
66 query, key, value, ..
67 } => vec![query.name.as_str(), key.name.as_str(), value.name.as_str()],
68 KernelPattern::Gather { data, indices, .. } => {
69 vec![data.name.as_str(), indices.name.as_str()]
70 }
71 KernelPattern::Scatter {
72 data,
73 indices,
74 updates,
75 ..
76 } => vec![
77 data.name.as_str(),
78 indices.name.as_str(),
79 updates.name.as_str(),
80 ],
81 KernelPattern::ElementWiseChain { base, steps, .. } => std::iter::once(base.name.as_str())
84 .chain(steps.iter().filter_map(|s| match &s.operand {
85 crate::analyze::ChainOperand::Tensor(t) => Some(t.name.as_str()),
86 crate::analyze::ChainOperand::Scalar(_) => None,
87 }))
88 .collect(),
89 KernelPattern::Unknown { .. } => vec![],
90 }
91}
92
93pub fn tensors_connect(producer: &KernelPattern, consumer: &KernelPattern) -> bool {
96 let outputs = output_tensor_names(producer);
97 let inputs = input_tensor_names(consumer);
98 outputs.iter().any(|o| inputs.contains(o))
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum FusedActivation {
104 None,
105 Relu,
106 Sigmoid,
107 Tanh,
108}
109
110impl std::fmt::Display for FusedActivation {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.write_str(match self {
113 Self::None => "None",
114 Self::Relu => "Relu",
115 Self::Sigmoid => "Sigmoid",
116 Self::Tanh => "Tanh",
117 })
118 }
119}
120
121fn try_fuse_activation(op: &crate::analyze::ActivationOp) -> Option<FusedActivation> {
124 match op {
125 crate::analyze::ActivationOp::Relu => Some(FusedActivation::Relu),
126 crate::analyze::ActivationOp::Sigmoid => Some(FusedActivation::Sigmoid),
127 crate::analyze::ActivationOp::Tanh => Some(FusedActivation::Tanh),
128 crate::analyze::ActivationOp::Softmax => None,
129 crate::analyze::ActivationOp::Gelu => None,
131 crate::analyze::ActivationOp::Silu => None,
132 crate::analyze::ActivationOp::Mish => None,
133 }
134}
135
136#[derive(Debug, Clone)]
138pub enum FusedPattern {
139 Single(KernelPattern),
141 ConvBatchNorm {
143 conv: KernelPattern,
144 norm: Box<KernelPattern>,
145 },
146 WithActivation {
148 base: Box<FusedPattern>,
149 activation: FusedActivation,
150 activation_pattern: Box<KernelPattern>,
152 },
153 MatMulBias {
155 matmul: KernelPattern,
156 bias_add: Box<KernelPattern>,
157 },
158}
159
160impl std::fmt::Display for FusedPattern {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 match self {
163 Self::Single(p) => write!(f, "{p}"),
164 Self::ConvBatchNorm { conv, .. } => write!(f, "{conv}+BatchNorm"),
165 Self::WithActivation {
166 base, activation, ..
167 } => write!(f, "{base}+{activation}"),
168 Self::MatMulBias { .. } => write!(f, "Gemm"),
169 }
170 }
171}
172
173impl FusedPattern {
174 pub fn primary_pattern(&self) -> &KernelPattern {
176 match self {
177 FusedPattern::Single(p) => p,
178 FusedPattern::ConvBatchNorm { conv, .. } => conv,
179 FusedPattern::WithActivation { base, .. } => base.primary_pattern(),
180 FusedPattern::MatMulBias { matmul, .. } => matmul,
181 }
182 }
183
184 fn output_pattern(&self) -> &KernelPattern {
188 match self {
189 FusedPattern::Single(p) => p,
190 FusedPattern::ConvBatchNorm { norm, .. } => norm,
191 FusedPattern::WithActivation {
192 activation_pattern, ..
193 } => activation_pattern,
194 FusedPattern::MatMulBias { bias_add, .. } => bias_add,
195 }
196 }
197
198 pub fn fused_activation(&self) -> FusedActivation {
200 match self {
201 FusedPattern::WithActivation { activation, .. } => *activation,
202 _ => FusedActivation::None,
203 }
204 }
205}
206
207pub fn fuse_patterns(patterns: Vec<KernelPattern>) -> Vec<(FusedPattern, usize)> {
214 let mut result: Vec<(FusedPattern, usize)> = Vec::new();
215 let mut iter = patterns.into_iter().enumerate().peekable();
216
217 while let Some((idx, pattern)) = iter.next() {
218 if matches!(&pattern, KernelPattern::Unknown { .. }) {
220 result.push((FusedPattern::Single(pattern), idx));
221 continue;
222 }
223
224 let fused = match &pattern {
225 KernelPattern::Conv2D { .. } => {
226 if let Some((_, next)) = iter.peek() {
227 if matches!(next, KernelPattern::Normalization { .. })
228 && tensors_connect(&pattern, next)
229 {
230 let (_, norm) = iter.next().unwrap();
231 FusedPattern::ConvBatchNorm {
232 conv: pattern,
233 norm: Box::new(norm),
234 }
235 } else {
236 FusedPattern::Single(pattern)
237 }
238 } else {
239 FusedPattern::Single(pattern)
240 }
241 }
242 KernelPattern::MatMul { .. } => {
243 if let Some((_, next)) = iter.peek() {
244 let is_add = matches!(
245 next,
246 KernelPattern::ElementWise {
247 op: crate::analyze::ElementWiseOp::Add,
248 ..
249 }
250 );
251 if is_add && tensors_connect(&pattern, next) {
252 let (_, bias_add) = iter.next().unwrap();
253 FusedPattern::MatMulBias {
254 matmul: pattern,
255 bias_add: Box::new(bias_add),
256 }
257 } else {
258 FusedPattern::Single(pattern)
259 }
260 } else {
261 FusedPattern::Single(pattern)
262 }
263 }
264 _ => FusedPattern::Single(pattern),
265 };
266
267 let fused = if let Some((_, next)) = iter.peek() {
269 if let KernelPattern::Activation { op, .. } = next {
270 if let Some(fused_act) = try_fuse_activation(op) {
271 if tensors_connect(fused.output_pattern(), next) {
272 let (_, act_pattern) = iter.next().unwrap();
273 FusedPattern::WithActivation {
274 base: Box::new(fused),
275 activation: fused_act,
276 activation_pattern: Box::new(act_pattern),
277 }
278 } else {
279 fused
280 }
281 } else {
282 fused
283 }
284 } else {
285 fused
286 }
287 } else {
288 fused
289 };
290
291 result.push((fused, idx));
292 }
293
294 result
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use crate::analyze::NormType;
301 use crate::analyze::data_type;
302 use crate::analyze::*;
303
304 fn dummy_handle() -> nxpu_ir::Handle<nxpu_ir::GlobalVariable> {
305 let mut arena = nxpu_ir::Arena::new();
306 arena.append(nxpu_ir::GlobalVariable {
307 name: None,
308 space: nxpu_ir::AddressSpace::Uniform,
309 binding: None,
310 ty: {
311 let mut types = nxpu_ir::UniqueArena::new();
312 types.insert(nxpu_ir::Type {
313 name: None,
314 inner: nxpu_ir::TypeInner::Scalar(nxpu_ir::Scalar::F32),
315 })
316 },
317 init: None,
318 layout: None,
319 })
320 }
321
322 fn make_tensor(name: &str, role: TensorRole) -> TensorBinding {
323 TensorBinding {
324 handle: dummy_handle(),
325 name: name.into(),
326 elem_type: data_type::FLOAT,
327 role,
328 }
329 }
330
331 #[test]
332 fn single_pattern_no_fusion() {
333 let patterns = vec![KernelPattern::ElementWise {
334 op: ElementWiseOp::Add,
335 inputs: [
336 make_tensor("a", TensorRole::Input),
337 make_tensor("b", TensorRole::Input),
338 ],
339 output: make_tensor("c", TensorRole::Output),
340 dim_name: "N".into(),
341 }];
342
343 let fused = fuse_patterns(patterns);
344 assert_eq!(fused.len(), 1);
345 let (ref fp, idx) = fused[0];
346 assert!(matches!(fp, FusedPattern::Single(_)));
347 assert_eq!(idx, 0);
348 }
349
350 #[test]
351 fn conv_batchnorm_fusion() {
352 let patterns = vec![
353 KernelPattern::Conv2D {
354 input: make_tensor("x", TensorRole::Input),
355 weight: make_tensor("w", TensorRole::Input),
356 output: make_tensor("conv_out", TensorRole::Output),
357 shape: Conv2DShape {
358 batch: "N".into(),
359 channels_in: "IC".into(),
360 channels_out: "OC".into(),
361 height: "H".into(),
362 width: "W".into(),
363 kernel_h: "KH".into(),
364 kernel_w: "KW".into(),
365 kernel_h_val: 3,
366 kernel_w_val: 3,
367 stride_h: 1,
368 stride_w: 1,
369 pad_h: 0,
370 pad_w: 0,
371 groups: 1,
372 dilation_h: 1,
373 dilation_w: 1,
374 },
375 bias: None,
376 activation: None,
377 },
378 KernelPattern::Normalization {
379 input: make_tensor("conv_out", TensorRole::Input),
380 scale: make_tensor("gamma", TensorRole::Input),
381 bias: make_tensor("beta", TensorRole::Input),
382 output: make_tensor("bn_out", TensorRole::Output),
383 epsilon: 1e-5,
384 norm_type: NormType::Batch,
385 },
386 ];
387
388 let fused = fuse_patterns(patterns);
389 assert_eq!(fused.len(), 1);
390 let (ref fp, idx) = fused[0];
391 assert!(matches!(fp, FusedPattern::ConvBatchNorm { .. }));
392 assert_eq!(idx, 0);
393 }
394
395 #[test]
396 fn add_relu_fusion() {
397 let patterns = vec![
398 KernelPattern::ElementWise {
399 op: ElementWiseOp::Add,
400 inputs: [
401 make_tensor("a", TensorRole::Input),
402 make_tensor("b", TensorRole::Input),
403 ],
404 output: make_tensor("c", TensorRole::Output),
405 dim_name: "N".into(),
406 },
407 KernelPattern::Activation {
408 op: ActivationOp::Relu,
409 input: make_tensor("c", TensorRole::Input),
410 output: make_tensor("d", TensorRole::Output),
411 dim_name: "N".into(),
412 },
413 ];
414
415 let fused = fuse_patterns(patterns);
416 assert_eq!(fused.len(), 1);
417 let (ref fp, idx) = fused[0];
418 assert!(matches!(
419 fp,
420 FusedPattern::WithActivation {
421 activation: FusedActivation::Relu,
422 ..
423 }
424 ));
425 assert_eq!(idx, 0);
426 }
427
428 #[test]
429 fn conv_bn_relu_fusion() {
430 let patterns = vec![
431 KernelPattern::Conv2D {
432 input: make_tensor("x", TensorRole::Input),
433 weight: make_tensor("w", TensorRole::Input),
434 output: make_tensor("conv_out", TensorRole::Output),
435 shape: Conv2DShape {
436 batch: "N".into(),
437 channels_in: "IC".into(),
438 channels_out: "OC".into(),
439 height: "H".into(),
440 width: "W".into(),
441 kernel_h: "KH".into(),
442 kernel_w: "KW".into(),
443 kernel_h_val: 3,
444 kernel_w_val: 3,
445 stride_h: 1,
446 stride_w: 1,
447 pad_h: 0,
448 pad_w: 0,
449 groups: 1,
450 dilation_h: 1,
451 dilation_w: 1,
452 },
453 bias: None,
454 activation: None,
455 },
456 KernelPattern::Normalization {
457 input: make_tensor("conv_out", TensorRole::Input),
458 scale: make_tensor("gamma", TensorRole::Input),
459 bias: make_tensor("beta", TensorRole::Input),
460 output: make_tensor("bn_out", TensorRole::Output),
461 epsilon: 1e-5,
462 norm_type: NormType::Batch,
463 },
464 KernelPattern::Activation {
465 op: ActivationOp::Relu,
466 input: make_tensor("bn_out", TensorRole::Input),
467 output: make_tensor("relu_out", TensorRole::Output),
468 dim_name: "N".into(),
469 },
470 ];
471
472 let fused = fuse_patterns(patterns);
473 assert_eq!(fused.len(), 1);
474 let (ref fp, idx) = fused[0];
475 assert_eq!(idx, 0);
476 match fp {
477 FusedPattern::WithActivation {
478 base,
479 activation: FusedActivation::Relu,
480 ..
481 } => {
482 assert!(matches!(**base, FusedPattern::ConvBatchNorm { .. }));
483 }
484 other => panic!("expected WithActivation(ConvBatchNorm, Relu), got {other:?}"),
485 }
486 }
487
488 #[test]
489 fn tanh_activation_now_fused() {
490 let patterns = vec![
491 KernelPattern::ElementWise {
492 op: ElementWiseOp::Add,
493 inputs: [
494 make_tensor("a", TensorRole::Input),
495 make_tensor("b", TensorRole::Input),
496 ],
497 output: make_tensor("c", TensorRole::Output),
498 dim_name: "N".into(),
499 },
500 KernelPattern::Activation {
501 op: ActivationOp::Tanh,
502 input: make_tensor("c", TensorRole::Input),
503 output: make_tensor("d", TensorRole::Output),
504 dim_name: "N".into(),
505 },
506 ];
507
508 let fused = fuse_patterns(patterns);
509 assert_eq!(fused.len(), 1);
511 assert!(matches!(
512 &fused[0].0,
513 FusedPattern::WithActivation {
514 activation: FusedActivation::Tanh,
515 ..
516 }
517 ));
518 }
519
520 #[test]
521 fn no_fusion_for_softmax_activation() {
522 let patterns = vec![
523 KernelPattern::ElementWise {
524 op: ElementWiseOp::Add,
525 inputs: [
526 make_tensor("a", TensorRole::Input),
527 make_tensor("b", TensorRole::Input),
528 ],
529 output: make_tensor("c", TensorRole::Output),
530 dim_name: "N".into(),
531 },
532 KernelPattern::Activation {
533 op: ActivationOp::Softmax,
534 input: make_tensor("c", TensorRole::Input),
535 output: make_tensor("d", TensorRole::Output),
536 dim_name: "N".into(),
537 },
538 ];
539
540 let fused = fuse_patterns(patterns);
541 assert_eq!(fused.len(), 2);
543 assert_eq!(fused[0].1, 0);
544 assert_eq!(fused[1].1, 1);
545 }
546
547 #[test]
548 fn matmul_add_fusion_to_gemm() {
549 let patterns = vec![
550 KernelPattern::MatMul {
551 inputs: [
552 make_tensor("A", TensorRole::Input),
553 make_tensor("B", TensorRole::Input),
554 ],
555 output: make_tensor("mm_out", TensorRole::Output),
556 shape: MatMulShape {
557 m: "M".into(),
558 n: "N".into(),
559 k: "K".into(),
560 },
561 },
562 KernelPattern::ElementWise {
563 op: ElementWiseOp::Add,
564 inputs: [
565 make_tensor("mm_out", TensorRole::Input),
566 make_tensor("bias", TensorRole::Input),
567 ],
568 output: make_tensor("out", TensorRole::Output),
569 dim_name: "N".into(),
570 },
571 ];
572
573 let fused = fuse_patterns(patterns);
574 assert_eq!(fused.len(), 1);
575 assert!(matches!(&fused[0].0, FusedPattern::MatMulBias { .. }));
576 }
577
578 #[test]
579 fn add_sigmoid_fusion() {
580 let patterns = vec![
581 KernelPattern::ElementWise {
582 op: ElementWiseOp::Add,
583 inputs: [
584 make_tensor("a", TensorRole::Input),
585 make_tensor("b", TensorRole::Input),
586 ],
587 output: make_tensor("c", TensorRole::Output),
588 dim_name: "N".into(),
589 },
590 KernelPattern::Activation {
591 op: ActivationOp::Sigmoid,
592 input: make_tensor("c", TensorRole::Input),
593 output: make_tensor("d", TensorRole::Output),
594 dim_name: "N".into(),
595 },
596 ];
597
598 let fused = fuse_patterns(patterns);
599 assert_eq!(fused.len(), 1);
600 assert!(matches!(
601 &fused[0].0,
602 FusedPattern::WithActivation {
603 activation: FusedActivation::Sigmoid,
604 ..
605 }
606 ));
607 }
608
609 #[test]
610 fn tensors_connect_matching_names() {
611 let producer = KernelPattern::ElementWise {
612 op: ElementWiseOp::Add,
613 inputs: [
614 make_tensor("a", TensorRole::Input),
615 make_tensor("b", TensorRole::Input),
616 ],
617 output: make_tensor("c", TensorRole::Output),
618 dim_name: "N".into(),
619 };
620 let consumer = KernelPattern::Activation {
621 op: ActivationOp::Relu,
622 input: make_tensor("c", TensorRole::Input),
623 output: make_tensor("d", TensorRole::Output),
624 dim_name: "N".into(),
625 };
626 assert!(tensors_connect(&producer, &consumer));
627 }
628
629 #[test]
630 fn tensors_connect_mismatched_names() {
631 let producer = KernelPattern::ElementWise {
632 op: ElementWiseOp::Add,
633 inputs: [
634 make_tensor("a", TensorRole::Input),
635 make_tensor("b", TensorRole::Input),
636 ],
637 output: make_tensor("c", TensorRole::Output),
638 dim_name: "N".into(),
639 };
640 let consumer = KernelPattern::Activation {
641 op: ActivationOp::Relu,
642 input: make_tensor("x", TensorRole::Input),
643 output: make_tensor("y", TensorRole::Output),
644 dim_name: "N".into(),
645 };
646 assert!(!tensors_connect(&producer, &consumer));
647 }
648
649 #[test]
650 fn no_fusion_mismatched_tensor_names() {
651 let patterns = vec![
653 KernelPattern::ElementWise {
654 op: ElementWiseOp::Add,
655 inputs: [
656 make_tensor("a", TensorRole::Input),
657 make_tensor("b", TensorRole::Input),
658 ],
659 output: make_tensor("c", TensorRole::Output),
660 dim_name: "N".into(),
661 },
662 KernelPattern::Activation {
663 op: ActivationOp::Relu,
664 input: make_tensor("x", TensorRole::Input),
665 output: make_tensor("y", TensorRole::Output),
666 dim_name: "N".into(),
667 },
668 ];
669
670 let fused = fuse_patterns(patterns);
671 assert_eq!(fused.len(), 2);
672 }
673
674 #[test]
677 fn display_fused_pattern_single() {
678 let pattern = FusedPattern::Single(KernelPattern::ElementWise {
679 op: ElementWiseOp::Add,
680 inputs: [
681 make_tensor("a", TensorRole::Input),
682 make_tensor("b", TensorRole::Input),
683 ],
684 output: make_tensor("c", TensorRole::Output),
685 dim_name: "N".into(),
686 });
687 let s = format!("{pattern}");
688 assert_ne!(s.len(), 0);
689 }
690
691 #[test]
692 fn display_fused_pattern_conv_batchnorm() {
693 let pattern = FusedPattern::ConvBatchNorm {
694 conv: KernelPattern::Conv2D {
695 input: make_tensor("x", TensorRole::Input),
696 weight: make_tensor("w", TensorRole::Input),
697 output: make_tensor("conv_out", TensorRole::Output),
698 shape: Conv2DShape {
699 batch: "N".into(),
700 channels_in: "IC".into(),
701 channels_out: "OC".into(),
702 height: "H".into(),
703 width: "W".into(),
704 kernel_h: "KH".into(),
705 kernel_w: "KW".into(),
706 kernel_h_val: 3,
707 kernel_w_val: 3,
708 stride_h: 1,
709 stride_w: 1,
710 pad_h: 0,
711 pad_w: 0,
712 groups: 1,
713 dilation_h: 1,
714 dilation_w: 1,
715 },
716 bias: None,
717 activation: None,
718 },
719 norm: Box::new(KernelPattern::Normalization {
720 input: make_tensor("conv_out", TensorRole::Input),
721 scale: make_tensor("gamma", TensorRole::Input),
722 bias: make_tensor("beta", TensorRole::Input),
723 output: make_tensor("bn_out", TensorRole::Output),
724 epsilon: 1e-5,
725 norm_type: crate::NormType::Batch,
726 }),
727 };
728 let s = format!("{pattern}");
729 assert!(s.contains("BatchNorm"), "got: {s}");
730 }
731
732 #[test]
733 fn display_fused_pattern_matmul_bias() {
734 let pattern = FusedPattern::MatMulBias {
735 matmul: KernelPattern::MatMul {
736 inputs: [
737 make_tensor("A", TensorRole::Input),
738 make_tensor("B", TensorRole::Input),
739 ],
740 output: make_tensor("mm_out", TensorRole::Output),
741 shape: MatMulShape {
742 m: "M".into(),
743 n: "N".into(),
744 k: "K".into(),
745 },
746 },
747 bias_add: Box::new(KernelPattern::ElementWise {
748 op: ElementWiseOp::Add,
749 inputs: [
750 make_tensor("mm_out", TensorRole::Input),
751 make_tensor("bias", TensorRole::Input),
752 ],
753 output: make_tensor("out", TensorRole::Output),
754 dim_name: "N".into(),
755 }),
756 };
757 let s = format!("{pattern}");
758 assert_eq!(s, "Gemm");
759 }
760
761 #[test]
762 fn display_fused_pattern_with_activation() {
763 let pattern = FusedPattern::WithActivation {
764 base: Box::new(FusedPattern::Single(KernelPattern::ElementWise {
765 op: ElementWiseOp::Add,
766 inputs: [
767 make_tensor("a", TensorRole::Input),
768 make_tensor("b", TensorRole::Input),
769 ],
770 output: make_tensor("c", TensorRole::Output),
771 dim_name: "N".into(),
772 })),
773 activation: FusedActivation::Relu,
774 activation_pattern: Box::new(KernelPattern::Activation {
775 op: ActivationOp::Relu,
776 input: make_tensor("c", TensorRole::Input),
777 output: make_tensor("d", TensorRole::Output),
778 dim_name: "N".into(),
779 }),
780 };
781 let s = format!("{pattern}");
782 assert!(s.contains("Relu"), "got: {s}");
783 }
784
785 #[test]
788 fn primary_pattern_for_all_variants() {
789 let add = KernelPattern::ElementWise {
790 op: ElementWiseOp::Add,
791 inputs: [
792 make_tensor("a", TensorRole::Input),
793 make_tensor("b", TensorRole::Input),
794 ],
795 output: make_tensor("c", TensorRole::Output),
796 dim_name: "N".into(),
797 };
798
799 let single = FusedPattern::Single(add.clone());
800 assert!(matches!(
801 single.primary_pattern(),
802 KernelPattern::ElementWise { .. }
803 ));
804
805 let conv_bn = FusedPattern::ConvBatchNorm {
806 conv: KernelPattern::Conv2D {
807 input: make_tensor("x", TensorRole::Input),
808 weight: make_tensor("w", TensorRole::Input),
809 output: make_tensor("conv_out", TensorRole::Output),
810 shape: Conv2DShape {
811 batch: "N".into(),
812 channels_in: "IC".into(),
813 channels_out: "OC".into(),
814 height: "H".into(),
815 width: "W".into(),
816 kernel_h: "KH".into(),
817 kernel_w: "KW".into(),
818 kernel_h_val: 3,
819 kernel_w_val: 3,
820 stride_h: 1,
821 stride_w: 1,
822 pad_h: 0,
823 pad_w: 0,
824 groups: 1,
825 dilation_h: 1,
826 dilation_w: 1,
827 },
828 bias: None,
829 activation: None,
830 },
831 norm: Box::new(KernelPattern::Normalization {
832 input: make_tensor("conv_out", TensorRole::Input),
833 scale: make_tensor("gamma", TensorRole::Input),
834 bias: make_tensor("beta", TensorRole::Input),
835 output: make_tensor("bn_out", TensorRole::Output),
836 epsilon: 1e-5,
837 norm_type: crate::NormType::Batch,
838 }),
839 };
840 assert!(matches!(
841 conv_bn.primary_pattern(),
842 KernelPattern::Conv2D { .. }
843 ));
844
845 let matmul_bias = FusedPattern::MatMulBias {
846 matmul: KernelPattern::MatMul {
847 inputs: [
848 make_tensor("A", TensorRole::Input),
849 make_tensor("B", TensorRole::Input),
850 ],
851 output: make_tensor("mm_out", TensorRole::Output),
852 shape: MatMulShape {
853 m: "M".into(),
854 n: "N".into(),
855 k: "K".into(),
856 },
857 },
858 bias_add: Box::new(add.clone()),
859 };
860 assert!(matches!(
861 matmul_bias.primary_pattern(),
862 KernelPattern::MatMul { .. }
863 ));
864
865 let with_act = FusedPattern::WithActivation {
866 base: Box::new(FusedPattern::Single(add.clone())),
867 activation: FusedActivation::Sigmoid,
868 activation_pattern: Box::new(KernelPattern::Activation {
869 op: ActivationOp::Sigmoid,
870 input: make_tensor("c", TensorRole::Input),
871 output: make_tensor("d", TensorRole::Output),
872 dim_name: "N".into(),
873 }),
874 };
875 assert!(matches!(
876 with_act.primary_pattern(),
877 KernelPattern::ElementWise { .. }
878 ));
879 }
880
881 #[test]
882 fn fused_activation_returns_correct_values() {
883 let add = KernelPattern::ElementWise {
884 op: ElementWiseOp::Add,
885 inputs: [
886 make_tensor("a", TensorRole::Input),
887 make_tensor("b", TensorRole::Input),
888 ],
889 output: make_tensor("c", TensorRole::Output),
890 dim_name: "N".into(),
891 };
892
893 let single = FusedPattern::Single(add.clone());
894 assert_eq!(single.fused_activation(), FusedActivation::None);
895
896 let with_relu = FusedPattern::WithActivation {
897 base: Box::new(FusedPattern::Single(add.clone())),
898 activation: FusedActivation::Relu,
899 activation_pattern: Box::new(KernelPattern::Activation {
900 op: ActivationOp::Relu,
901 input: make_tensor("c", TensorRole::Input),
902 output: make_tensor("d", TensorRole::Output),
903 dim_name: "N".into(),
904 }),
905 };
906 assert_eq!(with_relu.fused_activation(), FusedActivation::Relu);
907 }
908
909 #[test]
910 fn fused_activation_display() {
911 assert_eq!(format!("{}", FusedActivation::None), "None");
912 assert_eq!(format!("{}", FusedActivation::Relu), "Relu");
913 assert_eq!(format!("{}", FusedActivation::Sigmoid), "Sigmoid");
914 assert_eq!(format!("{}", FusedActivation::Tanh), "Tanh");
915 }
916
917 #[test]
920 fn output_tensor_names_for_split() {
921 let pattern = KernelPattern::Split {
922 input: make_tensor("x", TensorRole::Input),
923 outputs: vec![
924 make_tensor("o1", TensorRole::Output),
925 make_tensor("o2", TensorRole::Output),
926 ],
927 axis: 1,
928 };
929 let names = output_tensor_names(&pattern);
930 assert_eq!(names, vec!["o1", "o2"]);
931 }
932
933 #[test]
934 fn output_tensor_names_for_unknown() {
935 let pattern = KernelPattern::Unknown {
936 reason: "test".into(),
937 };
938 let names = output_tensor_names(&pattern);
939 assert_eq!(names.len(), 0);
940 }
941
942 #[test]
943 fn input_tensor_names_for_unknown() {
944 let pattern = KernelPattern::Unknown {
945 reason: "test".into(),
946 };
947 let names = input_tensor_names(&pattern);
948 assert_eq!(names.len(), 0);
949 }
950
951 #[test]
952 fn input_tensor_names_for_concat() {
953 let pattern = KernelPattern::Concat {
954 inputs: vec![
955 make_tensor("a", TensorRole::Input),
956 make_tensor("b", TensorRole::Input),
957 ],
958 output: make_tensor("c", TensorRole::Output),
959 axis: 0,
960 };
961 let names = input_tensor_names(&pattern);
962 assert_eq!(names, vec!["a", "b"]);
963 }
964
965 #[test]
966 fn input_tensor_names_for_attention() {
967 let pattern = KernelPattern::Attention {
968 query: make_tensor("q", TensorRole::Input),
969 key: make_tensor("k", TensorRole::Input),
970 value: make_tensor("v", TensorRole::Input),
971 output: make_tensor("o", TensorRole::Output),
972 d_k: "D".into(),
973 seq_len: "S".into(),
974 num_heads: 1,
975 num_kv_heads: 1,
976 causal: false,
977 };
978 let names = input_tensor_names(&pattern);
979 assert_eq!(names, vec!["q", "k", "v"]);
980 }
981
982 #[test]
983 fn unknown_pattern_passes_through_fuse() {
984 let patterns = vec![
985 KernelPattern::Unknown {
986 reason: "test".into(),
987 },
988 KernelPattern::ElementWise {
989 op: ElementWiseOp::Add,
990 inputs: [
991 make_tensor("a", TensorRole::Input),
992 make_tensor("b", TensorRole::Input),
993 ],
994 output: make_tensor("c", TensorRole::Output),
995 dim_name: "N".into(),
996 },
997 ];
998
999 let fused = fuse_patterns(patterns);
1000 assert_eq!(fused.len(), 2);
1001 assert!(matches!(
1002 &fused[0].0,
1003 FusedPattern::Single(KernelPattern::Unknown { .. })
1004 ));
1005 assert!(matches!(
1006 &fused[1].0,
1007 FusedPattern::Single(KernelPattern::ElementWise { .. })
1008 ));
1009 }
1010
1011 #[test]
1012 fn matmul_add_relu_fusion() {
1013 let patterns = vec![
1015 KernelPattern::MatMul {
1016 inputs: [
1017 make_tensor("A", TensorRole::Input),
1018 make_tensor("B", TensorRole::Input),
1019 ],
1020 output: make_tensor("mm_out", TensorRole::Output),
1021 shape: MatMulShape {
1022 m: "M".into(),
1023 n: "N".into(),
1024 k: "K".into(),
1025 },
1026 },
1027 KernelPattern::ElementWise {
1028 op: ElementWiseOp::Add,
1029 inputs: [
1030 make_tensor("mm_out", TensorRole::Input),
1031 make_tensor("bias", TensorRole::Input),
1032 ],
1033 output: make_tensor("gemm_out", TensorRole::Output),
1034 dim_name: "N".into(),
1035 },
1036 KernelPattern::Activation {
1037 op: ActivationOp::Relu,
1038 input: make_tensor("gemm_out", TensorRole::Input),
1039 output: make_tensor("relu_out", TensorRole::Output),
1040 dim_name: "N".into(),
1041 },
1042 ];
1043
1044 let fused = fuse_patterns(patterns);
1045 assert_eq!(fused.len(), 1);
1046 match &fused[0].0 {
1047 FusedPattern::WithActivation {
1048 base,
1049 activation: FusedActivation::Relu,
1050 ..
1051 } => {
1052 assert!(matches!(**base, FusedPattern::MatMulBias { .. }));
1053 }
1054 other => panic!("expected WithActivation(MatMulBias, Relu), got {other:?}"),
1055 }
1056 }
1057
1058 #[test]
1059 fn conv2d_at_end_of_list_no_fusion() {
1060 let patterns = vec![KernelPattern::Conv2D {
1062 input: make_tensor("x", TensorRole::Input),
1063 weight: make_tensor("w", TensorRole::Input),
1064 output: make_tensor("conv_out", TensorRole::Output),
1065 shape: Conv2DShape {
1066 batch: "N".into(),
1067 channels_in: "IC".into(),
1068 channels_out: "OC".into(),
1069 height: "H".into(),
1070 width: "W".into(),
1071 kernel_h: "KH".into(),
1072 kernel_w: "KW".into(),
1073 kernel_h_val: 3,
1074 kernel_w_val: 3,
1075 stride_h: 1,
1076 stride_w: 1,
1077 pad_h: 0,
1078 pad_w: 0,
1079 groups: 1,
1080 dilation_h: 1,
1081 dilation_w: 1,
1082 },
1083 bias: None,
1084 activation: None,
1085 }];
1086
1087 let fused = fuse_patterns(patterns);
1088 assert_eq!(fused.len(), 1);
1089 assert!(matches!(
1090 &fused[0].0,
1091 FusedPattern::Single(KernelPattern::Conv2D { .. })
1092 ));
1093 }
1094
1095 #[test]
1096 fn matmul_at_end_of_list_no_fusion() {
1097 let patterns = vec![KernelPattern::MatMul {
1099 inputs: [
1100 make_tensor("A", TensorRole::Input),
1101 make_tensor("B", TensorRole::Input),
1102 ],
1103 output: make_tensor("mm_out", TensorRole::Output),
1104 shape: MatMulShape {
1105 m: "M".into(),
1106 n: "N".into(),
1107 k: "K".into(),
1108 },
1109 }];
1110
1111 let fused = fuse_patterns(patterns);
1112 assert_eq!(fused.len(), 1);
1113 assert!(matches!(
1114 &fused[0].0,
1115 FusedPattern::Single(KernelPattern::MatMul { .. })
1116 ));
1117 }
1118
1119 #[test]
1120 fn conv2d_with_non_normalization_next() {
1121 let patterns = vec![
1123 KernelPattern::Conv2D {
1124 input: make_tensor("x", TensorRole::Input),
1125 weight: make_tensor("w", TensorRole::Input),
1126 output: make_tensor("conv_out", TensorRole::Output),
1127 shape: Conv2DShape {
1128 batch: "N".into(),
1129 channels_in: "IC".into(),
1130 channels_out: "OC".into(),
1131 height: "H".into(),
1132 width: "W".into(),
1133 kernel_h: "KH".into(),
1134 kernel_w: "KW".into(),
1135 kernel_h_val: 3,
1136 kernel_w_val: 3,
1137 stride_h: 1,
1138 stride_w: 1,
1139 pad_h: 0,
1140 pad_w: 0,
1141 groups: 1,
1142 dilation_h: 1,
1143 dilation_w: 1,
1144 },
1145 bias: None,
1146 activation: None,
1147 },
1148 KernelPattern::ElementWise {
1149 op: ElementWiseOp::Add,
1150 inputs: [
1151 make_tensor("conv_out", TensorRole::Input),
1152 make_tensor("bias", TensorRole::Input),
1153 ],
1154 output: make_tensor("add_out", TensorRole::Output),
1155 dim_name: "N".into(),
1156 },
1157 ];
1158
1159 let fused = fuse_patterns(patterns);
1160 assert_eq!(fused.len(), 2);
1161 assert!(matches!(
1162 &fused[0].0,
1163 FusedPattern::Single(KernelPattern::Conv2D { .. })
1164 ));
1165 assert!(matches!(
1166 &fused[1].0,
1167 FusedPattern::Single(KernelPattern::ElementWise { .. })
1168 ));
1169 }
1170
1171 #[test]
1172 fn matmul_with_non_add_elementwise() {
1173 let patterns = vec![
1175 KernelPattern::MatMul {
1176 inputs: [
1177 make_tensor("A", TensorRole::Input),
1178 make_tensor("B", TensorRole::Input),
1179 ],
1180 output: make_tensor("mm_out", TensorRole::Output),
1181 shape: MatMulShape {
1182 m: "M".into(),
1183 n: "N".into(),
1184 k: "K".into(),
1185 },
1186 },
1187 KernelPattern::ElementWise {
1188 op: ElementWiseOp::Sub,
1189 inputs: [
1190 make_tensor("mm_out", TensorRole::Input),
1191 make_tensor("other", TensorRole::Input),
1192 ],
1193 output: make_tensor("sub_out", TensorRole::Output),
1194 dim_name: "N".into(),
1195 },
1196 ];
1197
1198 let fused = fuse_patterns(patterns);
1199 assert_eq!(fused.len(), 2);
1200 }
1201
1202 #[test]
1203 fn output_tensor_names_for_gather() {
1204 let pattern = KernelPattern::Gather {
1205 data: make_tensor("data", TensorRole::Input),
1206 indices: make_tensor("idx", TensorRole::Input),
1207 output: make_tensor("out", TensorRole::Output),
1208 axis: 0,
1209 };
1210 let names = output_tensor_names(&pattern);
1211 assert_eq!(names, vec!["out"]);
1212 }
1213
1214 #[test]
1215 fn input_tensor_names_for_gather() {
1216 let pattern = KernelPattern::Gather {
1217 data: make_tensor("data", TensorRole::Input),
1218 indices: make_tensor("idx", TensorRole::Input),
1219 output: make_tensor("out", TensorRole::Output),
1220 axis: 0,
1221 };
1222 let names = input_tensor_names(&pattern);
1223 assert_eq!(names, vec!["data", "idx"]);
1224 }
1225
1226 #[test]
1227 fn output_tensor_names_for_scatter() {
1228 let pattern = KernelPattern::Scatter {
1229 data: make_tensor("data", TensorRole::Input),
1230 indices: make_tensor("idx", TensorRole::Input),
1231 updates: make_tensor("upd", TensorRole::Input),
1232 output: make_tensor("out", TensorRole::Output),
1233 axis: 0,
1234 };
1235 let names = output_tensor_names(&pattern);
1236 assert_eq!(names, vec!["out"]);
1237 }
1238
1239 #[test]
1240 fn input_tensor_names_for_scatter() {
1241 let pattern = KernelPattern::Scatter {
1242 data: make_tensor("data", TensorRole::Input),
1243 indices: make_tensor("idx", TensorRole::Input),
1244 updates: make_tensor("upd", TensorRole::Input),
1245 output: make_tensor("out", TensorRole::Output),
1246 axis: 0,
1247 };
1248 let names = input_tensor_names(&pattern);
1249 assert_eq!(names, vec!["data", "idx", "upd"]);
1250 }
1251
1252 #[test]
1253 fn no_fusion_for_gelu_activation() {
1254 let patterns = vec![
1255 KernelPattern::ElementWise {
1256 op: ElementWiseOp::Add,
1257 inputs: [
1258 make_tensor("a", TensorRole::Input),
1259 make_tensor("b", TensorRole::Input),
1260 ],
1261 output: make_tensor("c", TensorRole::Output),
1262 dim_name: "N".into(),
1263 },
1264 KernelPattern::Activation {
1265 op: ActivationOp::Gelu,
1266 input: make_tensor("c", TensorRole::Input),
1267 output: make_tensor("d", TensorRole::Output),
1268 dim_name: "N".into(),
1269 },
1270 ];
1271
1272 let fused = fuse_patterns(patterns);
1273 assert_eq!(fused.len(), 2);
1275 }
1276
1277 #[test]
1278 fn no_fusion_for_silu_activation() {
1279 let patterns = vec![
1280 KernelPattern::ElementWise {
1281 op: ElementWiseOp::Add,
1282 inputs: [
1283 make_tensor("a", TensorRole::Input),
1284 make_tensor("b", TensorRole::Input),
1285 ],
1286 output: make_tensor("c", TensorRole::Output),
1287 dim_name: "N".into(),
1288 },
1289 KernelPattern::Activation {
1290 op: ActivationOp::Silu,
1291 input: make_tensor("c", TensorRole::Input),
1292 output: make_tensor("d", TensorRole::Output),
1293 dim_name: "N".into(),
1294 },
1295 ];
1296
1297 let fused = fuse_patterns(patterns);
1298 assert_eq!(fused.len(), 2);
1299 }
1300
1301 #[test]
1302 fn no_fusion_for_mish_activation() {
1303 let patterns = vec![
1304 KernelPattern::ElementWise {
1305 op: ElementWiseOp::Add,
1306 inputs: [
1307 make_tensor("a", TensorRole::Input),
1308 make_tensor("b", TensorRole::Input),
1309 ],
1310 output: make_tensor("c", TensorRole::Output),
1311 dim_name: "N".into(),
1312 },
1313 KernelPattern::Activation {
1314 op: ActivationOp::Mish,
1315 input: make_tensor("c", TensorRole::Input),
1316 output: make_tensor("d", TensorRole::Output),
1317 dim_name: "N".into(),
1318 },
1319 ];
1320
1321 let fused = fuse_patterns(patterns);
1322 assert_eq!(fused.len(), 2);
1323 }
1324}