1use std::collections::{BTreeSet, HashMap, HashSet};
68
69use onnx_runtime_ir::{Attribute, DataType, Graph, Node, NodeId, ValueId, WeightRef};
70
71use crate::error::Result;
72use crate::pass::{OptimizationPass, PassContext};
73
74pub const CONTRIB_DOMAIN: &str = "com.microsoft";
83
84const SQRT_2: f32 = std::f32::consts::SQRT_2;
86const FRAC_1_SQRT_2: f32 = std::f32::consts::FRAC_1_SQRT_2;
88
89fn approx(a: f32, expected: f32) -> bool {
95 (a - expected).abs() <= 1e-6 * expected.abs().max(1.0)
96}
97
98type FusedNodeSpec = (Vec<Option<ValueId>>, HashMap<String, Attribute>);
100
101pub struct PatternMatch {
103 pub nodes: Vec<NodeId>,
105 pub external_inputs: Vec<ValueId>,
109 pub output: ValueId,
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
116pub enum RewriteKind {
117 Structural,
120 LayerNorm,
124 Attention,
129 Gelu,
136}
137
138#[derive(Clone, Debug)]
140pub struct FusionPattern {
141 name: String,
142 ops: Vec<String>,
143 replacement: String,
144 #[cfg(test)]
145 replacement_domain: String,
146 kind: RewriteKind,
147}
148
149impl FusionPattern {
150 pub fn new(name: &str, ops: &[&str], replacement: &str) -> Self {
154 assert!(!ops.is_empty(), "fusion pattern must have at least one op");
155 Self {
156 name: name.to_string(),
157 ops: ops.iter().map(|s| s.to_string()).collect(),
158 replacement: replacement.to_string(),
159 #[cfg(test)]
160 replacement_domain: CONTRIB_DOMAIN.to_string(),
161 kind: RewriteKind::Structural,
162 }
163 }
164
165 pub fn layernorm() -> Self {
170 Self {
171 name: "LayerNorm".to_string(),
172 ops: [
173 "ReduceMean", "Sub", "Pow", "ReduceMean", "Add", "Sqrt", "Div", "Mul", "Add",
174 ]
175 .iter()
176 .map(|s| s.to_string())
177 .collect(),
178 replacement: "LayerNormalization".to_string(),
179 #[cfg(test)]
180 replacement_domain: CONTRIB_DOMAIN.to_string(),
181 kind: RewriteKind::LayerNorm,
182 }
183 }
184
185 pub fn kind(&self) -> RewriteKind {
187 self.kind
188 }
189
190 pub fn attention() -> Self {
195 Self {
196 name: "Attention".to_string(),
197 ops: ["Softmax"].iter().map(|s| s.to_string()).collect(),
200 replacement: "FusedAttention".to_string(),
201 #[cfg(test)]
202 replacement_domain: CONTRIB_DOMAIN.to_string(),
203 kind: RewriteKind::Attention,
204 }
205 }
206
207 pub fn gelu() -> Self {
212 Self {
213 name: "Gelu".to_string(),
214 ops: ["Erf"].iter().map(|s| s.to_string()).collect(),
217 replacement: "Gelu".to_string(),
218 #[cfg(test)]
219 replacement_domain: CONTRIB_DOMAIN.to_string(),
220 kind: RewriteKind::Gelu,
221 }
222 }
223
224 #[cfg(test)]
225 fn with_replacement_domain(mut self, domain: &str) -> Self {
226 self.replacement_domain = domain.to_string();
227 self
228 }
229
230 pub fn pattern_name(&self) -> &str {
232 &self.name
233 }
234
235 pub fn find_match(&self, graph: &Graph) -> Option<PatternMatch> {
244 for start in graph.nodes.keys() {
245 if let Some(m) = self.try_match_at(graph, start) {
246 return Some(m);
247 }
248 }
249 None
250 }
251
252 fn try_match_at(&self, graph: &Graph, start: NodeId) -> Option<PatternMatch> {
253 match self.kind {
254 RewriteKind::LayerNorm => self.try_match_layernorm(graph, start),
255 RewriteKind::Attention => self.try_match_attention(graph, start),
256 RewriteKind::Gelu => self.try_match_gelu(graph, start),
257 RewriteKind::Structural => self.try_match_from(graph, start),
258 }
259 }
260
261 fn affected_candidate_starts(&self, graph: &Graph, matched: &PatternMatch) -> Vec<NodeId> {
268 let max_depth = match self.kind {
269 RewriteKind::LayerNorm => 10,
270 RewriteKind::Attention => 6,
271 RewriteKind::Gelu => 5,
272 RewriteKind::Structural => self.ops.len(),
273 };
274 let mut affected = HashSet::new();
275 let mut frontier: Vec<(NodeId, usize)> = matched
276 .external_inputs
277 .iter()
278 .filter_map(|&value| graph.value(value).producer)
279 .map(|producer| (producer, 0))
280 .collect();
281
282 while let Some((node_id, depth)) = frontier.pop() {
283 if !affected.insert(node_id) || depth >= max_depth.saturating_sub(1) {
284 continue;
285 }
286 frontier.extend(
287 graph
288 .node(node_id)
289 .input_values()
290 .filter_map(|value| graph.value(value).producer)
291 .map(|producer| (producer, depth + 1)),
292 );
293 }
294 affected.into_iter().collect()
295 }
296
297 fn op_matches(node: &Node, op: &str) -> bool {
299 node.op_type == op && matches!(node.domain.as_str(), "" | "ai.onnx")
300 }
301
302 fn find_consumer(graph: &Graph, value: ValueId, op: &str) -> Option<NodeId> {
304 graph
305 .consumers(value)
306 .into_iter()
307 .find(|&c| Self::op_matches(graph.node(c), op))
308 }
309
310 fn try_match_layernorm(&self, graph: &Graph, start: NodeId) -> Option<PatternMatch> {
329 let mean_rm = graph.try_node(start)?;
330 if !Self::op_matches(mean_rm, "ReduceMean") || mean_rm.outputs.len() != 1 {
331 return None;
332 }
333 let mean = mean_rm.outputs[0];
334
335 let subs: Vec<NodeId> = graph
338 .consumers(mean)
339 .into_iter()
340 .filter(|&c| {
341 let n = graph.node(c);
342 Self::op_matches(n, "Sub") && n.input_values().any(|v| v == mean)
343 })
344 .collect();
345
346 for &sub_pow in &subs {
348 let sp = graph.node(sub_pow);
349 if sp.outputs.len() != 1 {
350 continue;
351 }
352 let diff_pow = sp.outputs[0];
353 let Some(pow) = Self::find_consumer(graph, diff_pow, "Pow") else {
355 continue;
356 };
357 let sq = graph.node(pow).outputs[0];
358 let Some(var_rm) = Self::find_consumer(graph, sq, "ReduceMean") else {
359 continue;
360 };
361 let var = graph.node(var_rm).outputs[0];
362 let Some(add_eps) = Self::find_consumer(graph, var, "Add") else {
363 continue;
364 };
365 let vare = graph.node(add_eps).outputs[0];
366 let Some(sqrt) = Self::find_consumer(graph, vare, "Sqrt") else {
367 continue;
368 };
369 let std = graph.node(sqrt).outputs[0];
370 let Some(div) = Self::find_consumer(graph, std, "Div") else {
372 continue;
373 };
374 let dn = graph.node(div);
375 let Some(num) = dn.input_values().find(|&v| v != std) else {
378 continue;
379 };
380 let Some(&sub_div) = subs.iter().find(|&&s| graph.node(s).outputs[0] == num) else {
381 continue;
382 };
383 let norm = dn.outputs[0];
384 let Some(mul) = Self::find_consumer(graph, norm, "Mul") else {
385 continue;
386 };
387 let scaled = graph.node(mul).outputs[0];
388 let Some(final_add) = Self::find_consumer(graph, scaled, "Add") else {
389 continue;
390 };
391
392 let mut nodes = vec![
394 start, sub_pow, pow, var_rm, add_eps, sqrt, div, mul, final_add,
395 ];
396 if sub_div != sub_pow {
397 nodes.push(sub_div);
398 }
399 let matched_set: HashSet<NodeId> = nodes.iter().copied().collect();
400 if matched_set.len() != nodes.len() {
402 continue;
403 }
404
405 let escapes = nodes.iter().any(|&nid| {
408 nid != final_add
409 && graph.node(nid).outputs.iter().any(|&out| {
410 graph.outputs.contains(&out)
411 || graph
412 .consumers(out)
413 .into_iter()
414 .any(|consumer| !matched_set.contains(&consumer))
415 })
416 });
417 if escapes {
418 continue;
419 }
420
421 let fa = graph.node(final_add);
424 if fa.outputs.len() != 1 {
425 continue;
426 }
427 let output = fa.outputs[0];
428 let survives = graph.outputs.contains(&output)
429 || graph
430 .consumers(output)
431 .into_iter()
432 .any(|consumer| !matched_set.contains(&consumer));
433 if !survives {
434 continue;
435 }
436
437 let produced: HashSet<ValueId> = nodes
439 .iter()
440 .flat_map(|&n| graph.node(n).outputs.iter().copied())
441 .collect();
442 let mut external = Vec::new();
443 let mut seen = HashSet::new();
444 for &nid in &nodes {
445 for iv in graph.node(nid).input_values() {
446 if produced.contains(&iv) {
447 continue;
448 }
449 if seen.insert(iv) {
450 external.push(iv);
451 }
452 }
453 }
454
455 let matched = PatternMatch {
456 nodes,
457 external_inputs: external,
458 output,
459 };
460
461 if self.layernorm_spec(graph, &matched).is_none() {
463 continue;
464 }
465 return Some(matched);
466 }
467 None
468 }
469
470 fn try_match_attention(&self, graph: &Graph, start: NodeId) -> Option<PatternMatch> {
479 let p = self.try_parse_attention(graph, start)?;
480 Some(PatternMatch {
481 nodes: p.nodes,
482 external_inputs: p.external_inputs,
483 output: p.output,
484 })
485 }
486
487 fn try_match_gelu(&self, graph: &Graph, start: NodeId) -> Option<PatternMatch> {
490 let p = self.try_parse_gelu(graph, start)?;
491 Some(PatternMatch {
492 nodes: p.nodes,
493 external_inputs: p.external_inputs,
494 output: p.output,
495 })
496 }
497
498 fn try_parse_attention(&self, graph: &Graph, sm_start: NodeId) -> Option<AttnParts> {
518 let sm = graph.try_node(sm_start)?;
520 if !Self::op_matches(sm, "Softmax") || sm.inputs.len() != 1 || sm.outputs.len() != 1 {
521 return None;
522 }
523 let sm_in = sm.inputs[0]?;
524 let sm_out = sm.outputs[0];
525 let rank = graph.value(sm_in).shape.len();
526 if rank == 0 {
527 return None;
528 }
529 let axis = sm.attr("axis").and_then(Attribute::as_int)?;
533 let axis = if axis < 0 { axis + rank as i64 } else { axis };
534 if axis != rank as i64 - 1 {
535 return None;
536 }
537
538 let out_mm = graph
542 .consumers(sm_out)
543 .into_iter()
544 .find(|&c| {
545 let n = graph.node(c);
546 Self::op_matches(n, "MatMul") && n.inputs.first() == Some(&Some(sm_out))
547 })?;
548 let out_mm_node = graph.node(out_mm);
549 if out_mm_node.inputs.len() != 2 || out_mm_node.outputs.len() != 1 {
550 return None;
551 }
552 let v = out_mm_node.inputs[1]?;
553 let output = out_mm_node.outputs[0];
554
555 let sm_in_prod = graph.value(sm_in).producer?;
558 let prod = graph.node(sm_in_prod);
559 let (scale_out, mask, mask_add) =
560 if Self::op_matches(prod, "Add") && prod.inputs.len() == 2 {
561 let a = prod.inputs[0]?;
562 let b = prod.inputs[1]?;
563 let a_scale = graph
568 .value(a)
569 .producer
570 .is_some_and(|p| Self::parse_scale(graph, p).is_some());
571 let b_scale = graph
572 .value(b)
573 .producer
574 .is_some_and(|p| Self::parse_scale(graph, p).is_some());
575 match (a_scale, b_scale) {
576 (true, false) => (a, Some(b), Some(sm_in_prod)),
577 (false, true) => (b, Some(a), Some(sm_in_prod)),
578 _ => return None,
579 }
580 } else {
581 (sm_in, None, None)
582 };
583
584 let scale_node_id = graph.value(scale_out).producer?;
587 let scale_node = graph.node(scale_node_id);
588 if scale_node.outputs.len() != 1 || scale_node.outputs[0] != scale_out {
589 return None;
590 }
591 let (scores_out, scale) = Self::parse_scale(graph, scale_node_id)?;
592
593 let score_mm_id = graph.value(scores_out).producer?;
596 let score_mm = graph.node(score_mm_id);
597 if !Self::op_matches(score_mm, "MatMul")
598 || score_mm.inputs.len() != 2
599 || score_mm.outputs.len() != 1
600 || score_mm.outputs[0] != scores_out
601 {
602 return None;
603 }
604 let q = score_mm.inputs[0]?;
605 let k_side = score_mm.inputs[1]?;
606
607 let (k, k_transposed, transpose_node) = Self::attention_k(graph, k_side, score_mm_id);
611
612 let mut nodes = vec![sm_start, score_mm_id, scale_node_id, out_mm];
615 if let Some(ma) = mask_add {
616 nodes.push(ma);
617 }
618 if let Some(t) = transpose_node {
619 nodes.push(t);
620 }
621 let matched_set: HashSet<NodeId> = nodes.iter().copied().collect();
622 if matched_set.len() != nodes.len() {
623 return None;
624 }
625
626 let escapes = nodes.iter().any(|&nid| {
630 nid != out_mm
631 && graph.node(nid).outputs.iter().any(|&o| {
632 graph.outputs.contains(&o)
633 || graph
634 .consumers(o)
635 .into_iter()
636 .any(|consumer| !matched_set.contains(&consumer))
637 })
638 });
639 if escapes {
640 return None;
641 }
642
643 let survives = graph.outputs.contains(&output)
645 || graph
646 .consumers(output)
647 .into_iter()
648 .any(|consumer| !matched_set.contains(&consumer));
649 if !survives {
650 return None;
651 }
652
653 let mut external = vec![q, k, v];
655 if let Some(m) = mask {
656 external.push(m);
657 }
658
659 Some(AttnParts {
660 nodes,
661 q,
662 k,
663 v,
664 mask,
665 scale,
666 k_transposed,
667 output,
668 external_inputs: external,
669 })
670 }
671
672 fn parse_scale(graph: &Graph, node_id: NodeId) -> Option<(ValueId, f32)> {
679 let n = graph.node(node_id);
680 if n.inputs.len() != 2 || n.outputs.len() != 1 {
681 return None;
682 }
683 let (scores_out, scale) = if Self::op_matches(n, "Div") {
684 let num = n.inputs[0]?;
685 let den = n.inputs[1]?;
686 let c = read_scalar_const_f32(graph, den)?;
687 if c == 0.0 {
688 return None;
689 }
690 (num, 1.0 / c)
691 } else if Self::op_matches(n, "Mul") {
692 let x = n.inputs[0]?;
693 let y = n.inputs[1]?;
694 match (
695 read_scalar_const_f32(graph, x),
696 read_scalar_const_f32(graph, y),
697 ) {
698 (None, Some(c)) => (x, c),
699 (Some(c), None) => (y, c),
700 _ => return None,
702 }
703 } else {
704 return None;
705 };
706 let prod = graph.value(scores_out).producer?;
708 if !Self::op_matches(graph.node(prod), "MatMul") {
709 return None;
710 }
711 Some((scores_out, scale))
712 }
713
714 fn attention_k(
722 graph: &Graph,
723 k_side: ValueId,
724 score_mm_id: NodeId,
725 ) -> (ValueId, bool, Option<NodeId>) {
726 if let Some(t_id) = graph.value(k_side).producer {
727 let t = graph.node(t_id);
728 if Self::op_matches(t, "Transpose")
729 && t.inputs.len() == 1
730 && t.outputs.len() == 1
731 && t.outputs[0] == k_side
732 && graph.consumers(k_side) == [score_mm_id]
733 && let Some(perm) = t.attr("perm").and_then(Attribute::as_ints)
734 && is_last2_swap_perm(perm)
735 && let Some(kin) = t.inputs[0]
736 {
737 return (kin, false, Some(t_id));
738 }
739 }
740 (k_side, true, None)
741 }
742
743 fn attention_spec(&self, graph: &Graph, m: &PatternMatch) -> Option<FusedNodeSpec> {
749 let start = *m.nodes.first()?;
750 let p = self.try_parse_attention(graph, start)?;
751 if p.nodes != m.nodes {
752 return None;
753 }
754 let mut inputs: Vec<Option<ValueId>> = vec![Some(p.q), Some(p.k), Some(p.v)];
755 if let Some(mask) = p.mask {
756 inputs.push(Some(mask));
757 }
758 let mut attributes = HashMap::new();
759 attributes.insert("scale".to_string(), Attribute::Float(p.scale));
760 attributes.insert(
761 "k_transposed".to_string(),
762 Attribute::Int(if p.k_transposed { 1 } else { 0 }),
763 );
764 Some((inputs, attributes))
765 }
766
767 fn try_parse_gelu(&self, graph: &Graph, erf_start: NodeId) -> Option<GeluParts> {
791 let erf = graph.try_node(erf_start)?;
793 if !Self::op_matches(erf, "Erf") || erf.inputs.len() != 1 || erf.outputs.len() != 1 {
794 return None;
795 }
796 let erf_in = erf.inputs[0]?;
797 let erf_out = erf.outputs[0];
798
799 let inner_id = graph.value(erf_in).producer?;
801 let inner = graph.node(inner_id);
802 if inner.outputs.first() != Some(&erf_in) {
803 return None;
804 }
805 let x = Self::parse_scaled(graph, inner, &[("Div", SQRT_2), ("Mul", FRAC_1_SQRT_2)])?;
806
807 let add1_id = Self::find_consumer(graph, erf_out, "Add")?;
809 let add1 = graph.node(add1_id);
810 if add1.inputs.len() != 2 || add1.outputs.len() != 1 {
811 return None;
812 }
813 let one = add1.input_values().find(|&v| v != erf_out)?;
814 if !approx(read_scalar_const_f32(graph, one)?, 1.0) {
815 return None;
816 }
817 let add1_out = add1.outputs[0];
818
819 let outer_id = Self::find_consumer(graph, add1_out, "Mul")?;
821 let outer = graph.node(outer_id);
822 if outer.inputs.len() != 2 || outer.outputs.len() != 1 {
823 return None;
824 }
825 let half = outer.input_values().find(|&v| v != add1_out)?;
826 let output = outer.outputs[0];
827
828 let half_id = graph.value(half).producer?;
832 let half_node = graph.node(half_id);
833 if half_node.outputs.first() != Some(&half) {
834 return None;
835 }
836 let x2 = Self::parse_scaled(graph, half_node, &[("Mul", 0.5), ("Div", 2.0)])?;
837 if x2 != x {
838 return None;
839 }
840
841 let nodes = vec![erf_start, inner_id, add1_id, outer_id, half_id];
843 let matched_set: HashSet<NodeId> = nodes.iter().copied().collect();
844 if matched_set.len() != nodes.len() {
845 return None;
846 }
847
848 let escapes = nodes.iter().any(|&nid| {
852 nid != outer_id
853 && graph.node(nid).outputs.iter().any(|&o| {
854 graph.outputs.contains(&o)
855 || graph
856 .consumers(o)
857 .into_iter()
858 .any(|consumer| !matched_set.contains(&consumer))
859 })
860 });
861 if escapes {
862 return None;
863 }
864
865 let survives = graph.outputs.contains(&output)
867 || graph
868 .consumers(output)
869 .into_iter()
870 .any(|consumer| !matched_set.contains(&consumer));
871 if !survives {
872 return None;
873 }
874
875 Some(GeluParts {
876 nodes,
877 x,
878 output,
879 external_inputs: vec![x],
880 })
881 }
882
883 fn parse_scaled(graph: &Graph, node: &Node, forms: &[(&str, f32)]) -> Option<ValueId> {
890 if node.inputs.len() != 2 || node.outputs.len() != 1 {
891 return None;
892 }
893 let a = node.inputs[0]?;
894 let b = node.inputs[1]?;
895 for &(op, k) in forms {
896 if !Self::op_matches(node, op) {
897 continue;
898 }
899 if read_scalar_const_f32(graph, b).is_some_and(|c| approx(c, k)) {
903 return Some(a);
904 }
905 if op == "Mul" && read_scalar_const_f32(graph, a).is_some_and(|c| approx(c, k)) {
906 return Some(b);
907 }
908 }
909 None
910 }
911
912 fn gelu_spec(&self, graph: &Graph, m: &PatternMatch) -> Option<FusedNodeSpec> {
917 let start = *m.nodes.first()?;
918 let p = self.try_parse_gelu(graph, start)?;
919 if p.nodes != m.nodes {
920 return None;
921 }
922 Some((vec![Some(p.x)], HashMap::new()))
923 }
924
925 fn try_match_from(&self, graph: &Graph, start: NodeId) -> Option<PatternMatch> {
927 let start_node = graph.try_node(start)?;
928 if !Self::op_matches(start_node, &self.ops[0]) {
929 return None;
930 }
931
932 let mut chain = vec![start];
933 let mut chain_set: HashSet<NodeId> = HashSet::from([start]);
934
935 for op in &self.ops[1..] {
936 let prev = *chain.last().unwrap();
937 let mut succ_ids = graph.successors(prev);
940 succ_ids.sort_by_key(|n| n.0);
941 let next = succ_ids.into_iter().find(|&s| {
942 !chain_set.contains(&s) && Self::op_matches(graph.node(s), op)
943 })?;
944 chain.push(next);
945 chain_set.insert(next);
946 }
947
948 for &nid in &chain[..chain.len() - 1] {
951 for &out in &graph.node(nid).outputs {
952 if graph.outputs.contains(&out) {
953 return None;
954 }
955 if graph
956 .consumers(out)
957 .into_iter()
958 .any(|consumer| !chain_set.contains(&consumer))
959 {
960 return None;
961 }
962 }
963 }
964
965 let last = *chain.last().unwrap();
967 let last_node = graph.node(last);
968 if last_node.outputs.len() != 1 {
969 return None;
970 }
971 let output = last_node.outputs[0];
972
973 let survives = graph.outputs.contains(&output)
976 || graph
977 .consumers(output)
978 .into_iter()
979 .any(|consumer| !chain_set.contains(&consumer));
980 if !survives {
981 return None;
982 }
983
984 let produced: HashSet<ValueId> = chain
986 .iter()
987 .flat_map(|&n| graph.node(n).outputs.iter().copied())
988 .collect();
989 let mut external = Vec::new();
990 let mut seen = HashSet::new();
991 for &nid in &chain {
992 for iv in graph.node(nid).input_values() {
993 if produced.contains(&iv) {
994 continue;
995 }
996 if seen.insert(iv) {
997 external.push(iv);
998 }
999 }
1000 }
1001
1002 let matched = PatternMatch {
1003 nodes: chain,
1004 external_inputs: external,
1005 output,
1006 };
1007
1008 if !self.match_is_fusable(graph, &matched) {
1013 return None;
1014 }
1015
1016 Some(matched)
1017 }
1018
1019 fn match_is_fusable(&self, graph: &Graph, m: &PatternMatch) -> bool {
1023 match self.kind {
1024 RewriteKind::LayerNorm => self.layernorm_spec(graph, m).is_some(),
1025 RewriteKind::Attention => self.attention_spec(graph, m).is_some(),
1026 RewriteKind::Gelu => self.gelu_spec(graph, m).is_some(),
1027 RewriteKind::Structural => {
1028 if self.replacement == "FusedMatMulBias" || self.replacement == "FusedGemm" {
1033 self.matmul_bias_broadcast_ok(graph, m)
1034 } else {
1035 true
1036 }
1037 }
1038 }
1039 }
1040
1041 fn matmul_bias_broadcast_ok(&self, graph: &Graph, m: &PatternMatch) -> bool {
1058 let (Some(&matmul), Some(&add)) = (m.nodes.first(), m.nodes.get(1)) else {
1062 return false;
1063 };
1064 let mm_out = graph.node(matmul).outputs[0];
1065 let Some(bias) = graph.node(add).input_values().find(|&v| v != mm_out) else {
1066 return false;
1067 };
1068 let mm_shape = &graph.value(mm_out).shape;
1069 let bias_shape = &graph.value(bias).shape;
1070
1071 if bias_shape.len() > mm_shape.len() {
1073 return false;
1074 }
1075 let offset = mm_shape.len() - bias_shape.len();
1079 for (i, &bdim) in bias_shape.iter().enumerate() {
1080 let mdim = mm_shape[offset + i];
1081 if bdim == mdim {
1082 continue;
1083 }
1084 if bdim.as_static() == Some(1) {
1085 continue;
1086 }
1087 return false;
1088 }
1089 true
1090 }
1091
1092 pub fn apply_fusion(&self, graph: &mut Graph, m: &PatternMatch) -> Result<()> {
1095 self.apply_fusion_returning_id(graph, m).map(|_| ())
1096 }
1097
1098 fn apply_fusion_returning_id(&self, graph: &mut Graph, m: &PatternMatch) -> Result<NodeId> {
1099 let output = m.output;
1100
1101 let (inputs, attributes) = match self.kind {
1104 RewriteKind::Structural => (
1105 m.external_inputs.iter().map(|&v| Some(v)).collect(),
1106 HashMap::new(),
1107 ),
1108 RewriteKind::LayerNorm => self
1109 .layernorm_spec(graph, m)
1110 .ok_or_else(|| crate::error::OptimizerError::Fusion(self.name.clone()))?,
1111 RewriteKind::Attention => self
1112 .attention_spec(graph, m)
1113 .ok_or_else(|| crate::error::OptimizerError::Fusion(self.name.clone()))?,
1114 RewriteKind::Gelu => self
1115 .gelu_spec(graph, m)
1116 .ok_or_else(|| crate::error::OptimizerError::Fusion(self.name.clone()))?,
1117 };
1118
1119 for &nid in m.nodes.iter().rev() {
1123 graph.remove_node(nid);
1124 }
1125
1126 if graph.try_value(output).is_none() {
1127 return Err(crate::error::OptimizerError::Fusion(self.name.clone()));
1128 }
1129
1130 let mut fused = Node::new(NodeId(0), self.replacement.clone(), inputs, vec![output]);
1131 fused.attributes = attributes;
1132 #[cfg(not(test))]
1135 {
1136 fused.domain = CONTRIB_DOMAIN.to_string();
1137 }
1138 #[cfg(test)]
1139 {
1140 fused.domain = self.replacement_domain.clone();
1141 }
1142 Ok(graph.insert_node(fused))
1143 }
1144
1145 fn layernorm_spec(&self, graph: &Graph, m: &PatternMatch) -> Option<FusedNodeSpec> {
1170 let nodes = &m.nodes;
1171 if nodes.len() != 9 && nodes.len() != 10 {
1172 return None;
1173 }
1174 let rm1 = graph.node(nodes[0]);
1175 let sub_pow = graph.node(nodes[1]);
1176 let pow = graph.node(nodes[2]);
1177 let rm2 = graph.node(nodes[3]);
1178 let add_eps = graph.node(nodes[4]);
1179 let div = graph.node(nodes[6]);
1180 let mul = graph.node(nodes[7]);
1181 let final_add = graph.node(nodes[8]);
1182 let sub_div = if nodes.len() == 10 {
1185 graph.node(nodes[9])
1186 } else {
1187 sub_pow
1188 };
1189
1190 let mean = rm1.outputs[0];
1191 let diff_pow = sub_pow.outputs[0];
1192 let diff_div = sub_div.outputs[0];
1193 let var = rm2.outputs[0];
1194 let norm = div.outputs[0];
1195 let scaled = mul.outputs[0];
1196
1197 if !sub_pow.input_values().any(|v| v == mean)
1201 || !sub_div.input_values().any(|v| v == mean)
1202 || !pow.input_values().any(|v| v == diff_pow)
1203 || !div.input_values().any(|v| v == diff_div)
1204 || !mul.input_values().any(|v| v == norm)
1205 || !final_add.input_values().any(|v| v == scaled)
1206 {
1207 return None;
1208 }
1209
1210 let x = sub_pow.input_values().find(|&v| v != mean)?;
1214 if !sub_div.input_values().any(|v| v == x) {
1215 return None;
1216 }
1217
1218 let subtracts_x_minus_mean = |sub: &Node| -> bool {
1225 matches!(sub.inputs.as_slice(), [Some(a), Some(b)] if *a == x && *b == mean)
1226 };
1227 if !subtracts_x_minus_mean(sub_pow) || !subtracts_x_minus_mean(sub_div) {
1228 return None;
1229 }
1230 let scale = mul.input_values().find(|&v| v != norm)?;
1231 let bias = final_add.input_values().find(|&v| v != scaled)?;
1232
1233 let eps_val = add_eps.input_values().find(|&v| v != var)?;
1235 let epsilon = read_scalar_f32(graph, eps_val)?;
1236
1237 let axes = rm1.attr("axes").and_then(Attribute::as_ints)?;
1241 let [axis] = axes else {
1242 return None;
1243 };
1244
1245 let mut attributes = HashMap::new();
1246 attributes.insert("axis".to_string(), Attribute::Int(*axis));
1247 attributes.insert("epsilon".to_string(), Attribute::Float(epsilon));
1248
1249 Some((vec![Some(x), Some(scale), Some(bias)], attributes))
1250 }
1251}
1252
1253fn read_scalar_f32(graph: &Graph, value: ValueId) -> Option<f32> {
1256 match graph.initializers.get(&value)? {
1257 WeightRef::Inline(t) if t.dtype == DataType::Float32 && t.data.len() >= 4 => {
1258 Some(f32::from_le_bytes(t.data[0..4].try_into().ok()?))
1259 }
1260 _ => None,
1261 }
1262}
1263
1264#[derive(Clone, Debug)]
1267struct AttnParts {
1268 nodes: Vec<NodeId>,
1272 q: ValueId,
1273 k: ValueId,
1274 v: ValueId,
1275 mask: Option<ValueId>,
1276 scale: f32,
1277 k_transposed: bool,
1278 output: ValueId,
1279 external_inputs: Vec<ValueId>,
1280}
1281
1282#[derive(Clone, Debug)]
1285struct GeluParts {
1286 nodes: Vec<NodeId>,
1289 x: ValueId,
1291 output: ValueId,
1293 external_inputs: Vec<ValueId>,
1294}
1295fn read_scalar_const_f32(graph: &Graph, value: ValueId) -> Option<f32> {
1299 match graph.initializers.get(&value)? {
1300 WeightRef::Inline(t) if t.dtype == DataType::Float32 => {
1301 let numel: usize = t.dims.iter().product();
1302 if numel != 1 || t.data.len() < 4 {
1303 return None;
1304 }
1305 Some(f32::from_le_bytes(t.data[0..4].try_into().ok()?))
1306 }
1307 _ => None,
1308 }
1309}
1310
1311fn is_last2_swap_perm(perm: &[i64]) -> bool {
1316 let r = perm.len();
1317 if r < 2 {
1318 return false;
1319 }
1320 for (i, &p) in perm.iter().enumerate().take(r - 2) {
1321 if p != i as i64 {
1322 return false;
1323 }
1324 }
1325 perm[r - 2] == (r - 1) as i64 && perm[r - 1] == (r - 2) as i64
1326}
1327
1328pub fn default_fusion_patterns() -> Vec<FusionPattern> {
1333 vec![
1334 FusionPattern::attention(),
1338 FusionPattern::new("MatMul+Bias+Relu", &["MatMul", "Add", "Relu"], "FusedGemm"),
1339 FusionPattern::layernorm(),
1340 FusionPattern::gelu(),
1341 FusionPattern::new("MatMul+Bias", &["MatMul", "Add"], "FusedMatMulBias"),
1342 ]
1343}
1344
1345#[derive(Clone, Debug)]
1347pub struct OpFusion {
1348 patterns: Vec<FusionPattern>,
1349}
1350
1351#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1352enum ScanCandidateSource {
1353 Initial,
1354 Revisit,
1355}
1356
1357impl Default for OpFusion {
1358 fn default() -> Self {
1359 Self::new()
1360 }
1361}
1362
1363impl OpFusion {
1364 pub fn new() -> Self {
1366 Self {
1367 patterns: default_fusion_patterns(),
1368 }
1369 }
1370
1371 pub fn with_patterns(patterns: Vec<FusionPattern>) -> Self {
1373 Self { patterns }
1374 }
1375
1376 fn run_resumable(
1377 &self,
1378 graph: &mut Graph,
1379 mut observe_fusion: impl FnMut(
1380 &str,
1381 ScanCandidateSource,
1382 NodeId,
1383 &[NodeId],
1384 &[NodeId],
1385 NodeId,
1386 ),
1387 ) -> Result<()> {
1388 for pattern in &self.patterns {
1389 let candidates: Vec<u32> = graph.nodes.keys().map(|id| id.0).collect();
1390 let mut cursor = 0;
1391 let mut revisits = BTreeSet::new();
1392 loop {
1393 let initial = candidates.get(cursor).copied();
1394 let revisit = revisits.first().copied();
1395 let (raw_id, source) = match (initial, revisit) {
1396 (None, None) => break,
1397 (Some(id), None) => {
1398 cursor += 1;
1399 (id, ScanCandidateSource::Initial)
1400 }
1401 (None, Some(_)) => (
1402 revisits.pop_first().unwrap(),
1403 ScanCandidateSource::Revisit,
1404 ),
1405 (Some(id), Some(revisit)) if id <= revisit => {
1406 cursor += 1;
1407 if id == revisit {
1408 revisits.pop_first();
1409 }
1410 (id, ScanCandidateSource::Initial)
1411 }
1412 (Some(_), Some(_)) => (
1413 revisits.pop_first().unwrap(),
1414 ScanCandidateSource::Revisit,
1415 ),
1416 };
1417 let start = NodeId(raw_id);
1418 let Some(matched) = pattern.try_match_at(graph, start) else {
1419 continue;
1420 };
1421
1422 let affected = pattern.affected_candidate_starts(graph, &matched);
1423 let fused_id = pattern.apply_fusion_returning_id(graph, &matched)?;
1424 observe_fusion(
1425 pattern.pattern_name(),
1426 source,
1427 start,
1428 &matched.nodes,
1429 &affected,
1430 fused_id,
1431 );
1432
1433 revisits.insert(fused_id.0);
1437 for candidate in affected {
1438 if graph.try_node(candidate).is_some() {
1439 revisits.insert(candidate.0);
1440 }
1441 }
1442 }
1443 }
1444 Ok(())
1445 }
1446
1447 #[cfg(test)]
1448 fn run_with_fusion_observer(
1449 &self,
1450 graph: &mut Graph,
1451 observe_fusion: impl FnMut(
1452 &str,
1453 ScanCandidateSource,
1454 NodeId,
1455 &[NodeId],
1456 &[NodeId],
1457 NodeId,
1458 ),
1459 ) -> Result<()> {
1460 self.run_resumable(graph, observe_fusion)
1461 }
1462}
1463
1464impl OptimizationPass for OpFusion {
1465 fn name(&self) -> &str {
1466 "OpFusion"
1467 }
1468
1469 fn run(&self, graph: &mut Graph, _ctx: &PassContext) -> Result<()> {
1470 self.run_resumable(graph, |_, _, _, _, _, _| {})
1471 }
1472}
1473
1474#[cfg(test)]
1475mod tests {
1476 use super::*;
1477 use onnx_runtime_ir::{DataType, Node, NodeId, TensorData, static_shape};
1478
1479 fn val(g: &mut Graph, name: &str) -> ValueId {
1480 g.create_named_value(name, DataType::Float32, static_shape([4]))
1481 }
1482
1483 fn matmul_add_graph() -> Graph {
1486 let mut g = Graph::new();
1487 g.opset_imports.insert(String::new(), 17);
1488 let a = val(&mut g, "a");
1489 let w = val(&mut g, "w");
1490 let bias = val(&mut g, "bias");
1491 g.add_input(a);
1492 g.add_input(w);
1493 g.add_input(bias);
1494
1495 let m = val(&mut g, "m");
1496 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(a), Some(w)], vec![m]));
1497 let out = val(&mut g, "out");
1498 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(m), Some(bias)], vec![out]));
1499 g.add_output(out);
1500 g
1501 }
1502
1503 #[test]
1504 fn fuses_matmul_add() {
1505 let mut g = matmul_add_graph();
1506 assert_eq!(g.num_nodes(), 2);
1507 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
1508 assert_eq!(g.num_nodes(), 1);
1509 let fused = g.nodes.values().next().unwrap();
1510 assert_eq!(fused.op_type, "FusedMatMulBias");
1511 assert_eq!(fused.domain, CONTRIB_DOMAIN);
1512 assert_eq!(fused.inputs.len(), 3);
1514 assert!(g.validate().is_ok());
1515 assert_eq!(g.outputs.len(), 1);
1517 assert_eq!(fused.outputs, g.outputs);
1518 }
1519
1520 #[test]
1521 fn fuses_matmul_add_relu_before_matmul_add() {
1522 let mut g = Graph::new();
1523 g.opset_imports.insert(String::new(), 17);
1524 let a = val(&mut g, "a");
1525 let w = val(&mut g, "w");
1526 let bias = val(&mut g, "bias");
1527 g.add_input(a);
1528 g.add_input(w);
1529 g.add_input(bias);
1530 let m = val(&mut g, "m");
1531 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(a), Some(w)], vec![m]));
1532 let s = val(&mut g, "s");
1533 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(m), Some(bias)], vec![s]));
1534 let out = val(&mut g, "out");
1535 g.insert_node(Node::new(NodeId(0), "Relu", vec![Some(s)], vec![out]));
1536 g.add_output(out);
1537
1538 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
1539 assert_eq!(g.num_nodes(), 1);
1540 let fused = g.nodes.values().next().unwrap();
1541 assert_eq!(fused.op_type, "FusedGemm");
1542 assert_eq!(fused.domain, CONTRIB_DOMAIN);
1543 assert!(g.validate().is_ok());
1544 }
1545
1546 #[test]
1547 fn does_not_fuse_when_intermediate_has_second_consumer() {
1548 let mut g = matmul_add_graph();
1550 let m = g
1552 .values
1553 .iter()
1554 .find(|(_, v)| v.name.as_deref() == Some("m"))
1555 .map(|(id, _)| id)
1556 .unwrap();
1557 let side = val(&mut g, "side");
1558 g.insert_node(Node::new(NodeId(0), "Relu", vec![Some(m)], vec![side]));
1559 g.add_output(side);
1560
1561 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
1562 assert!(
1564 g.nodes.values().any(|n| n.op_type == "MatMul"),
1565 "MatMul must remain — its output has a second consumer"
1566 );
1567 assert!(g.nodes.values().all(|n| n.op_type != "FusedMatMulBias"));
1568 assert!(g.validate().is_ok());
1569 }
1570
1571 #[test]
1572 fn no_match_returns_none() {
1573 let mut g = Graph::new();
1574 g.opset_imports.insert(String::new(), 17);
1575 let a = val(&mut g, "a");
1576 g.add_input(a);
1577 let out = val(&mut g, "out");
1578 g.insert_node(Node::new(NodeId(0), "Relu", vec![Some(a)], vec![out]));
1579 g.add_output(out);
1580 let p = FusionPattern::new("MatMul+Bias", &["MatMul", "Add"], "FusedMatMulBias");
1581 assert!(p.find_match(&g).is_none());
1582 }
1583
1584 fn layernorm_graph() -> Graph {
1591 const EPS: f32 = 1e-12;
1592 let mut g = Graph::new();
1593 g.opset_imports.insert(String::new(), 17);
1594 let x = val(&mut g, "x");
1595 let two = val(&mut g, "two");
1596 let eps = val(&mut g, "eps");
1597 let scale = val(&mut g, "scale");
1598 let bias = val(&mut g, "bias");
1599 g.add_input(x);
1600 g.add_input(two);
1601 g.set_initializer(
1602 eps,
1603 WeightRef::Inline(TensorData::from_raw(
1604 DataType::Float32,
1605 vec![],
1606 EPS.to_le_bytes().to_vec(),
1607 )),
1608 );
1609 g.add_input(scale);
1610 g.add_input(bias);
1611
1612 let reduce_mean = |g: &mut Graph, input: ValueId, out: ValueId| {
1613 let mut n = Node::new(NodeId(0), "ReduceMean", vec![Some(input)], vec![out]);
1614 n.attributes.insert("axes".into(), Attribute::Ints(vec![-1]));
1615 n.attributes.insert("keepdims".into(), Attribute::Int(1));
1616 g.insert_node(n);
1617 };
1618
1619 let mean = val(&mut g, "mean");
1620 reduce_mean(&mut g, x, mean);
1621 let diff = val(&mut g, "diff");
1622 g.insert_node(Node::new(NodeId(0), "Sub", vec![Some(x), Some(mean)], vec![diff]));
1623 let sq = val(&mut g, "sq");
1624 g.insert_node(Node::new(NodeId(0), "Pow", vec![Some(diff), Some(two)], vec![sq]));
1625 let var = val(&mut g, "var");
1626 reduce_mean(&mut g, sq, var);
1627 let vare = val(&mut g, "vare");
1628 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(var), Some(eps)], vec![vare]));
1629 let std = val(&mut g, "std");
1630 g.insert_node(Node::new(NodeId(0), "Sqrt", vec![Some(vare)], vec![std]));
1631 let norm = val(&mut g, "norm");
1632 g.insert_node(Node::new(NodeId(0), "Div", vec![Some(diff), Some(std)], vec![norm]));
1633 let scaled = val(&mut g, "scaled");
1634 g.insert_node(Node::new(
1635 NodeId(0),
1636 "Mul",
1637 vec![Some(norm), Some(scale)],
1638 vec![scaled],
1639 ));
1640 let out = val(&mut g, "out");
1641 g.insert_node(Node::new(
1642 NodeId(0),
1643 "Add",
1644 vec![Some(scaled), Some(bias)],
1645 vec![out],
1646 ));
1647 g.add_output(out);
1648 g
1649 }
1650
1651 #[test]
1652 fn fuses_layernorm_chain() {
1653 let mut g = layernorm_graph();
1654 assert_eq!(g.num_nodes(), 9);
1655 assert!(g.validate().is_ok());
1656
1657 let vid = |name: &str| {
1659 g.values
1660 .iter()
1661 .find(|(_, v)| v.name.as_deref() == Some(name))
1662 .map(|(id, _)| id)
1663 .unwrap()
1664 };
1665 let x = vid("x");
1666 let scale = vid("scale");
1667 let bias = vid("bias");
1668
1669 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
1670
1671 assert_eq!(g.num_nodes(), 1, "9-op chain collapses to one node");
1672 let fused = g.nodes.values().next().unwrap();
1673 assert_eq!(fused.op_type, "LayerNormalization");
1674 assert_eq!(fused.domain, CONTRIB_DOMAIN);
1675 assert_eq!(fused.inputs, vec![Some(x), Some(scale), Some(bias)]);
1678 assert_eq!(
1680 fused.attr("axis").and_then(Attribute::as_int),
1681 Some(-1),
1682 "axis extracted from ReduceMean axes"
1683 );
1684 let eps = fused
1685 .attr("epsilon")
1686 .and_then(Attribute::as_float)
1687 .expect("epsilon attribute present");
1688 assert!(
1689 (eps - 1e-12).abs() < 1e-18,
1690 "epsilon extracted from the var+eps constant, got {eps}"
1691 );
1692 assert_eq!(fused.outputs, g.outputs);
1693 assert!(g.validate().is_ok());
1694 }
1695
1696 #[test]
1697 fn layernorm_count_bookkeeping() {
1698 let mut g = layernorm_graph();
1699 let ln_before = g.nodes.values().filter(|n| n.op_type == "LayerNormalization").count();
1700 let rm_before = g.nodes.values().filter(|n| n.op_type == "ReduceMean").count();
1701 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
1702 let ln_after = g.nodes.values().filter(|n| n.op_type == "LayerNormalization").count();
1703 let rm_after = g.nodes.values().filter(|n| n.op_type == "ReduceMean").count();
1704 assert_eq!(ln_before, 0);
1705 assert_eq!(ln_after, 1);
1706 assert_eq!(rm_before, 2);
1707 assert_eq!(rm_after, 0);
1708 }
1709
1710 fn layernorm_split_graph(reverse_num_sub: bool) -> Graph {
1718 const EPS: f32 = 1e-12;
1719 let mut g = Graph::new();
1720 g.opset_imports.insert(String::new(), 17);
1721 let x = val(&mut g, "x");
1722 let two = val(&mut g, "two");
1723 let eps = val(&mut g, "eps");
1724 let scale = val(&mut g, "scale");
1725 let bias = val(&mut g, "bias");
1726 g.add_input(x);
1727 g.add_input(two);
1728 g.set_initializer(
1729 eps,
1730 WeightRef::Inline(TensorData::from_raw(
1731 DataType::Float32,
1732 vec![],
1733 EPS.to_le_bytes().to_vec(),
1734 )),
1735 );
1736 g.add_input(scale);
1737 g.add_input(bias);
1738
1739 let reduce_mean = |g: &mut Graph, input: ValueId, out: ValueId| {
1740 let mut n = Node::new(NodeId(0), "ReduceMean", vec![Some(input)], vec![out]);
1741 n.attributes.insert("axes".into(), Attribute::Ints(vec![-1]));
1742 n.attributes.insert("keepdims".into(), Attribute::Int(1));
1743 g.insert_node(n);
1744 };
1745
1746 let mean = val(&mut g, "mean");
1747 reduce_mean(&mut g, x, mean);
1748 let diff_pow = val(&mut g, "diff_pow");
1750 g.insert_node(Node::new(
1751 NodeId(0),
1752 "Sub",
1753 vec![Some(x), Some(mean)],
1754 vec![diff_pow],
1755 ));
1756 let diff_div = val(&mut g, "diff_div");
1759 let num_inputs = if reverse_num_sub {
1760 vec![Some(mean), Some(x)]
1761 } else {
1762 vec![Some(x), Some(mean)]
1763 };
1764 g.insert_node(Node::new(NodeId(0), "Sub", num_inputs, vec![diff_div]));
1765
1766 let sq = val(&mut g, "sq");
1767 g.insert_node(Node::new(
1768 NodeId(0),
1769 "Pow",
1770 vec![Some(diff_pow), Some(two)],
1771 vec![sq],
1772 ));
1773 let var = val(&mut g, "var");
1774 reduce_mean(&mut g, sq, var);
1775 let vare = val(&mut g, "vare");
1776 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(var), Some(eps)], vec![vare]));
1777 let std = val(&mut g, "std");
1778 g.insert_node(Node::new(NodeId(0), "Sqrt", vec![Some(vare)], vec![std]));
1779 let norm = val(&mut g, "norm");
1780 g.insert_node(Node::new(
1781 NodeId(0),
1782 "Div",
1783 vec![Some(diff_div), Some(std)],
1784 vec![norm],
1785 ));
1786 let scaled = val(&mut g, "scaled");
1787 g.insert_node(Node::new(
1788 NodeId(0),
1789 "Mul",
1790 vec![Some(norm), Some(scale)],
1791 vec![scaled],
1792 ));
1793 let out = val(&mut g, "out");
1794 g.insert_node(Node::new(
1795 NodeId(0),
1796 "Add",
1797 vec![Some(scaled), Some(bias)],
1798 vec![out],
1799 ));
1800 g.add_output(out);
1801 g
1802 }
1803
1804 #[test]
1805 fn fuses_layernorm_split_chain() {
1806 let mut g = layernorm_split_graph(false);
1809 assert_eq!(g.num_nodes(), 10, "split-diff shape has two distinct Subs");
1810 assert!(g.validate().is_ok());
1811
1812 let vid = |name: &str| {
1813 g.values
1814 .iter()
1815 .find(|(_, v)| v.name.as_deref() == Some(name))
1816 .map(|(id, _)| id)
1817 .unwrap()
1818 };
1819 let x = vid("x");
1820 let scale = vid("scale");
1821 let bias = vid("bias");
1822
1823 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
1824
1825 assert_eq!(g.num_nodes(), 1, "10-op split chain collapses to one node");
1826 let fused = g.nodes.values().next().unwrap();
1827 assert_eq!(fused.op_type, "LayerNormalization");
1828 assert_eq!(fused.domain, CONTRIB_DOMAIN);
1829 assert_eq!(fused.inputs, vec![Some(x), Some(scale), Some(bias)]);
1831 assert_eq!(
1832 fused.attr("axis").and_then(Attribute::as_int),
1833 Some(-1),
1834 "axis extracted from ReduceMean axes"
1835 );
1836 let eps = fused
1837 .attr("epsilon")
1838 .and_then(Attribute::as_float)
1839 .expect("epsilon attribute present");
1840 assert!(
1841 (eps - 1e-12).abs() < 1e-18,
1842 "epsilon extracted from the var+eps constant, got {eps}"
1843 );
1844 assert_eq!(fused.outputs, g.outputs);
1845 assert!(g.validate().is_ok());
1846 }
1847
1848 #[test]
1849 fn declines_layernorm_when_numerator_sub_reversed() {
1850 let mut g = layernorm_split_graph(true);
1855 assert_eq!(g.num_nodes(), 10);
1856 assert!(g.validate().is_ok());
1857
1858 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
1859
1860 assert!(
1861 g.nodes.values().all(|n| n.op_type != "LayerNormalization"),
1862 "reversed Sub(mean, x) must NOT fuse — sign-flip over-match"
1863 );
1864 assert_eq!(g.num_nodes(), 10, "all 10 ops remain (declined)");
1865 assert_eq!(
1866 g.nodes.values().filter(|n| n.op_type == "Sub").count(),
1867 2,
1868 "both centering Subs preserved"
1869 );
1870 assert!(g.validate().is_ok());
1871 }
1872
1873 #[test]
1874 fn does_not_fuse_partial_layernorm() {
1875 let mut g = layernorm_graph();
1877 let p = FusionPattern::layernorm();
1880 let diff = g
1883 .values
1884 .iter()
1885 .find(|(_, v)| v.name.as_deref() == Some("diff"))
1886 .map(|(id, _)| id)
1887 .unwrap();
1888 let side = val(&mut g, "side");
1889 g.insert_node(Node::new(NodeId(0), "Neg", vec![Some(diff)], vec![side]));
1890 g.add_output(side);
1891 assert!(
1892 p.find_match(&g).is_none(),
1893 "external consumer on `diff` blocks the fusion"
1894 );
1895 }
1896
1897 #[test]
1898 fn fuses_two_independent_matmul_adds() {
1899 let mut g = Graph::new();
1900 g.opset_imports.insert(String::new(), 17);
1901 for i in 0..2 {
1902 let a = val(&mut g, &format!("a{i}"));
1903 let w = val(&mut g, &format!("w{i}"));
1904 let bias = val(&mut g, &format!("bias{i}"));
1905 g.add_input(a);
1906 g.add_input(w);
1907 g.add_input(bias);
1908 let m = val(&mut g, &format!("m{i}"));
1909 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(a), Some(w)], vec![m]));
1910 let out = val(&mut g, &format!("out{i}"));
1911 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(m), Some(bias)], vec![out]));
1912 g.add_output(out);
1913 }
1914 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
1915 assert_eq!(g.num_nodes(), 2);
1916 assert!(g.nodes.values().all(|n| n.op_type == "FusedMatMulBias"));
1917 assert!(g.validate().is_ok());
1918 }
1919
1920 #[test]
1921 fn find_match_reports_correct_shape() {
1922 let g = matmul_add_graph();
1923 let p = FusionPattern::new("MatMul+Bias", &["MatMul", "Add"], "FusedMatMulBias");
1924 let m = p.find_match(&g).expect("should match");
1925 assert_eq!(m.nodes.len(), 2);
1926 assert_eq!(m.external_inputs.len(), 3);
1927 assert_eq!(p.pattern_name(), "MatMul+Bias");
1928 }
1929
1930 #[test]
1931 fn declines_layernorm_when_axes_is_input() {
1932 let mut g = layernorm_graph();
1937 let mean = g
1938 .values
1939 .iter()
1940 .find(|(_, v)| v.name.as_deref() == Some("mean"))
1941 .map(|(id, _)| id)
1942 .unwrap();
1943 let rm1 = g.value(mean).producer.unwrap();
1944 g.node_mut(rm1).attributes.remove("axes");
1946 let axes_in = g.create_named_value("axes_in", DataType::Int64, static_shape([1]));
1947 g.set_initializer(
1948 axes_in,
1949 WeightRef::Inline(TensorData::from_raw(
1950 DataType::Int64,
1951 vec![1],
1952 (-1i64).to_le_bytes().to_vec(),
1953 )),
1954 );
1955 let input_index = g.node(rm1).inputs.len();
1956 g.node_mut(rm1).inputs.push(None);
1957 g.replace_input(rm1, input_index, Some(axes_in));
1958 assert!(g.validate().is_ok());
1959
1960 assert_eq!(g.num_nodes(), 9);
1961 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
1962 assert_eq!(
1963 g.num_nodes(),
1964 9,
1965 "axes-as-input LayerNorm must NOT fuse — all original ops kept"
1966 );
1967 assert!(
1968 g.nodes.values().all(|n| n.op_type != "LayerNormalization"),
1969 "no fused LayerNormalization must be emitted"
1970 );
1971 assert_eq!(
1972 g.nodes.values().filter(|n| n.op_type == "ReduceMean").count(),
1973 2,
1974 "both ReduceMean ops remain"
1975 );
1976 assert!(g.validate().is_ok());
1977 }
1978
1979 #[test]
1980 fn declines_layernorm_when_epsilon_not_constant() {
1981 let mut g = layernorm_graph();
1985 let eps = g
1986 .values
1987 .iter()
1988 .find(|(_, v)| v.name.as_deref() == Some("eps"))
1989 .map(|(id, _)| id)
1990 .unwrap();
1991 g.initializers.remove(&eps);
1993 g.add_input(eps);
1994 assert!(g.validate().is_ok());
1995
1996 assert_eq!(g.num_nodes(), 9);
1997 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
1998 assert_eq!(
1999 g.num_nodes(),
2000 9,
2001 "non-constant epsilon LayerNorm must NOT fuse"
2002 );
2003 assert!(g.nodes.values().all(|n| n.op_type != "LayerNormalization"));
2004 assert!(g.validate().is_ok());
2005 }
2006
2007 #[test]
2008 fn declines_matmul_add_when_bias_expands() {
2009 let mut g = Graph::new();
2014 g.opset_imports.insert(String::new(), 17);
2015 let a = g.create_named_value("a", DataType::Float32, static_shape([4, 4]));
2016 let w = g.create_named_value("w", DataType::Float32, static_shape([4]));
2017 let bias = g.create_named_value("bias", DataType::Float32, static_shape([2, 4]));
2018 g.add_input(a);
2019 g.add_input(w);
2020 g.add_input(bias);
2021 let m = g.create_named_value("m", DataType::Float32, static_shape([4]));
2022 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(a), Some(w)], vec![m]));
2023 let out = g.create_named_value("out", DataType::Float32, static_shape([2, 4]));
2024 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(m), Some(bias)], vec![out]));
2025 g.add_output(out);
2026
2027 assert_eq!(g.num_nodes(), 2);
2028 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2029 assert_eq!(g.num_nodes(), 2, "expanding bias must NOT fuse");
2030 assert!(g.nodes.values().any(|n| n.op_type == "MatMul"));
2031 assert!(g.nodes.values().any(|n| n.op_type == "Add"));
2032 assert!(g.nodes.values().all(|n| n.op_type != "FusedMatMulBias"));
2033 assert!(g.validate().is_ok());
2034 }
2035
2036 #[test]
2037 fn fuses_matmul_add_with_trailing_broadcast_bias() {
2038 let mut g = Graph::new();
2041 g.opset_imports.insert(String::new(), 17);
2042 let a = g.create_named_value("a", DataType::Float32, static_shape([3, 4]));
2043 let w = g.create_named_value("w", DataType::Float32, static_shape([4, 4]));
2044 let bias = g.create_named_value("bias", DataType::Float32, static_shape([1, 4]));
2045 g.add_input(a);
2046 g.add_input(w);
2047 g.add_input(bias);
2048 let m = g.create_named_value("m", DataType::Float32, static_shape([3, 4]));
2049 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(a), Some(w)], vec![m]));
2050 let out = g.create_named_value("out", DataType::Float32, static_shape([3, 4]));
2051 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(m), Some(bias)], vec![out]));
2052 g.add_output(out);
2053
2054 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2055 assert_eq!(g.num_nodes(), 1, "trailing-broadcast bias must fuse");
2056 assert_eq!(
2057 g.nodes.values().next().unwrap().op_type,
2058 "FusedMatMulBias"
2059 );
2060 assert!(g.validate().is_ok());
2061 }
2062
2063 #[test]
2064 fn declines_matmul_add_when_shape_unknown() {
2065 let mut g = Graph::new();
2068 g.opset_imports.insert(String::new(), 17);
2069 let a = g.create_named_value("a", DataType::Float32, Vec::new());
2070 let w = g.create_named_value("w", DataType::Float32, Vec::new());
2071 let bias = g.create_named_value("bias", DataType::Float32, static_shape([4]));
2072 g.add_input(a);
2073 g.add_input(w);
2074 g.add_input(bias);
2075 let m = g.create_named_value("m", DataType::Float32, Vec::new());
2077 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(a), Some(w)], vec![m]));
2078 let out = g.create_named_value("out", DataType::Float32, static_shape([4]));
2079 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(m), Some(bias)], vec![out]));
2080 g.add_output(out);
2081
2082 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2083 assert_eq!(g.num_nodes(), 2, "unknown matmul shape must NOT fuse");
2084 assert!(g.nodes.values().all(|n| n.op_type != "FusedMatMulBias"));
2085 assert!(g.validate().is_ok());
2086 }
2087
2088 #[test]
2089 fn declines_fused_gemm_when_bias_expands() {
2090 let mut g = Graph::new();
2096 g.opset_imports.insert(String::new(), 17);
2097 let a = g.create_named_value("a", DataType::Float32, static_shape([4, 4]));
2098 let w = g.create_named_value("w", DataType::Float32, static_shape([4]));
2099 let bias = g.create_named_value("bias", DataType::Float32, static_shape([2, 4]));
2100 g.add_input(a);
2101 g.add_input(w);
2102 g.add_input(bias);
2103 let m = g.create_named_value("m", DataType::Float32, static_shape([4]));
2104 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(a), Some(w)], vec![m]));
2105 let biased = g.create_named_value("biased", DataType::Float32, static_shape([2, 4]));
2106 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(m), Some(bias)], vec![biased]));
2107 let out = g.create_named_value("out", DataType::Float32, static_shape([2, 4]));
2108 g.insert_node(Node::new(NodeId(0), "Relu", vec![Some(biased)], vec![out]));
2109 g.add_output(out);
2110
2111 assert_eq!(g.num_nodes(), 3);
2112 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2113 assert_eq!(g.num_nodes(), 3, "expanding bias must NOT fuse to FusedGemm");
2114 assert!(g.nodes.values().any(|n| n.op_type == "MatMul"));
2115 assert!(g.nodes.values().any(|n| n.op_type == "Add"));
2116 assert!(g.nodes.values().any(|n| n.op_type == "Relu"));
2117 assert!(g.nodes.values().all(|n| n.op_type != "FusedGemm"));
2118 assert!(g.validate().is_ok());
2119 }
2120
2121 fn scalar_init(g: &mut Graph, name: &str, v: f32) -> ValueId {
2125 let vid = g.create_named_value(name, DataType::Float32, Vec::new());
2126 g.set_initializer(
2127 vid,
2128 WeightRef::Inline(TensorData::from_raw(
2129 DataType::Float32,
2130 vec![],
2131 v.to_le_bytes().to_vec(),
2132 )),
2133 );
2134 vid
2135 }
2136
2137 fn fval(g: &mut Graph, name: &str, dims: &[usize]) -> ValueId {
2138 g.create_named_value(name, DataType::Float32, static_shape(dims.iter().copied()))
2139 }
2140
2141 fn value_id(g: &Graph, name: &str) -> ValueId {
2143 g.values
2144 .iter()
2145 .find(|(_, v)| v.name.as_deref() == Some(name))
2146 .map(|(id, _)| id)
2147 .unwrap_or_else(|| panic!("no value named {name}"))
2148 }
2149
2150 fn sdpa_graph(masked: bool, axis: i64) -> Graph {
2155 let mut g = Graph::new();
2156 g.opset_imports.insert(String::new(), 12);
2157 let q = fval(&mut g, "Q", &[1, 2, 3, 4]);
2158 let kt = fval(&mut g, "K", &[1, 2, 4, 3]); let v = fval(&mut g, "V", &[1, 2, 3, 4]);
2160 g.add_input(q);
2161 g.add_input(kt);
2162 g.add_input(v);
2163 let c = scalar_init(&mut g, "scale_c", 2.0);
2164
2165 let scores = fval(&mut g, "scores", &[1, 2, 3, 3]);
2166 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(q), Some(kt)], vec![scores]));
2167 let scaled = fval(&mut g, "scaled", &[1, 2, 3, 3]);
2168 g.insert_node(Node::new(NodeId(0), "Div", vec![Some(scores), Some(c)], vec![scaled]));
2169
2170 let sm_in = if masked {
2171 let mask = fval(&mut g, "mask", &[1, 1, 3, 3]);
2172 g.add_input(mask);
2173 let masked_v = fval(&mut g, "masked", &[1, 2, 3, 3]);
2174 g.insert_node(Node::new(
2175 NodeId(0),
2176 "Add",
2177 vec![Some(scaled), Some(mask)],
2178 vec![masked_v],
2179 ));
2180 masked_v
2181 } else {
2182 scaled
2183 };
2184
2185 let probs = fval(&mut g, "probs", &[1, 2, 3, 3]);
2186 let mut sm = Node::new(NodeId(0), "Softmax", vec![Some(sm_in)], vec![probs]);
2187 sm.attributes.insert("axis".into(), Attribute::Int(axis));
2188 g.insert_node(sm);
2189 let out = fval(&mut g, "out", &[1, 2, 3, 4]);
2190 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(probs), Some(v)], vec![out]));
2191 g.add_output(out);
2192 g
2193 }
2194
2195 fn fused_attention_node(g: &Graph) -> Option<&Node> {
2196 g.nodes.values().find(|n| n.op_type == "FusedAttention")
2197 }
2198
2199 #[test]
2200 fn fuses_sdpa_unmasked_pretransposed_k() {
2201 let mut g = sdpa_graph(false, 3);
2202 let q = value_id(&g, "Q");
2203 let k = value_id(&g, "K");
2204 let v = value_id(&g, "V");
2205 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2206
2207 assert_eq!(
2209 g.nodes.values().filter(|n| n.op_type == "FusedAttention").count(),
2210 1
2211 );
2212 assert!(g.nodes.values().all(|n| n.op_type != "Softmax"));
2213 assert!(g.nodes.values().all(|n| n.op_type != "MatMul"));
2214 assert!(g.nodes.values().all(|n| n.op_type != "Div"));
2215
2216 let fa = fused_attention_node(&g).unwrap();
2217 assert_eq!(fa.domain, CONTRIB_DOMAIN);
2218 assert_eq!(fa.inputs, vec![Some(q), Some(k), Some(v)]);
2219 assert_eq!(fa.attr("scale").and_then(Attribute::as_float), Some(0.5));
2221 assert_eq!(fa.attr("k_transposed").and_then(Attribute::as_int), Some(1));
2222 assert!(g.validate().is_ok());
2223 }
2224
2225 #[test]
2226 fn fuses_sdpa_masked() {
2227 let mut g = sdpa_graph(true, 3);
2228 let (q, k, v, mask) = (
2229 value_id(&g, "Q"),
2230 value_id(&g, "K"),
2231 value_id(&g, "V"),
2232 value_id(&g, "mask"),
2233 );
2234 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2235
2236 assert_eq!(
2237 g.nodes.values().filter(|n| n.op_type == "FusedAttention").count(),
2238 1
2239 );
2240 assert!(g.nodes.values().all(|n| n.op_type != "Softmax"));
2241 assert!(g.nodes.values().all(|n| n.op_type != "Add"));
2242 let fa = fused_attention_node(&g).unwrap();
2243 assert_eq!(fa.inputs, vec![Some(q), Some(k), Some(v), Some(mask)]);
2245 assert_eq!(fa.attr("k_transposed").and_then(Attribute::as_int), Some(1));
2246 assert!(g.validate().is_ok());
2247 }
2248
2249 #[test]
2250 fn fuses_sdpa_absorbing_clean_transpose_sets_k_transposed_0() {
2251 let mut g = Graph::new();
2256 g.opset_imports.insert(String::new(), 12);
2257 let q = fval(&mut g, "Q", &[1, 2, 3, 4]);
2258 let k = fval(&mut g, "K", &[1, 2, 3, 4]); let v = fval(&mut g, "V", &[1, 2, 3, 4]);
2260 g.add_input(q);
2261 g.add_input(k);
2262 g.add_input(v);
2263 let c = scalar_init(&mut g, "scale_c", 4.0);
2264
2265 let kt = fval(&mut g, "Kt", &[1, 2, 4, 3]);
2266 let mut tr = Node::new(NodeId(0), "Transpose", vec![Some(k)], vec![kt]);
2267 tr.attributes.insert("perm".into(), Attribute::Ints(vec![0, 1, 3, 2]));
2268 g.insert_node(tr);
2269 let scores = fval(&mut g, "scores", &[1, 2, 3, 3]);
2270 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(q), Some(kt)], vec![scores]));
2271 let scaled = fval(&mut g, "scaled", &[1, 2, 3, 3]);
2272 g.insert_node(Node::new(NodeId(0), "Div", vec![Some(scores), Some(c)], vec![scaled]));
2273 let probs = fval(&mut g, "probs", &[1, 2, 3, 3]);
2274 let mut sm = Node::new(NodeId(0), "Softmax", vec![Some(scaled)], vec![probs]);
2275 sm.attributes.insert("axis".into(), Attribute::Int(-1));
2276 g.insert_node(sm);
2277 let out = fval(&mut g, "out", &[1, 2, 3, 4]);
2278 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(probs), Some(v)], vec![out]));
2279 g.add_output(out);
2280
2281 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2282 assert!(g.nodes.values().all(|n| n.op_type != "Transpose"), "clean Kᵀ Transpose absorbed");
2283 let fa = fused_attention_node(&g).unwrap();
2284 assert_eq!(fa.inputs, vec![Some(q), Some(k), Some(v)], "K input is the natural (un-transposed) K");
2285 assert_eq!(fa.attr("k_transposed").and_then(Attribute::as_int), Some(0));
2286 assert_eq!(fa.attr("scale").and_then(Attribute::as_float), Some(0.25));
2288 assert!(g.validate().is_ok());
2289 }
2290
2291 #[test]
2292 fn declines_sdpa_when_softmax_axis_not_last() {
2293 let mut g = sdpa_graph(false, 1);
2295 let before = g.num_nodes();
2296 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2297 assert!(g.nodes.values().all(|n| n.op_type != "FusedAttention"));
2298 assert!(g.nodes.values().any(|n| n.op_type == "Softmax"));
2299 assert_eq!(g.num_nodes(), before, "no fusion when axis is not last");
2300 }
2301
2302 #[test]
2303 fn declines_sdpa_when_scale_is_not_scalar_constant() {
2304 let mut g = Graph::new();
2307 g.opset_imports.insert(String::new(), 12);
2308 let q = fval(&mut g, "Q", &[1, 2, 3, 4]);
2309 let kt = fval(&mut g, "K", &[1, 2, 4, 3]);
2310 let v = fval(&mut g, "V", &[1, 2, 3, 4]);
2311 let c = fval(&mut g, "scale_c", &[]); g.add_input(q);
2313 g.add_input(kt);
2314 g.add_input(v);
2315 g.add_input(c);
2316 let scores = fval(&mut g, "scores", &[1, 2, 3, 3]);
2317 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(q), Some(kt)], vec![scores]));
2318 let scaled = fval(&mut g, "scaled", &[1, 2, 3, 3]);
2319 g.insert_node(Node::new(NodeId(0), "Div", vec![Some(scores), Some(c)], vec![scaled]));
2320 let probs = fval(&mut g, "probs", &[1, 2, 3, 3]);
2321 let mut sm = Node::new(NodeId(0), "Softmax", vec![Some(scaled)], vec![probs]);
2322 sm.attributes.insert("axis".into(), Attribute::Int(3));
2323 g.insert_node(sm);
2324 let out = fval(&mut g, "out", &[1, 2, 3, 4]);
2325 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(probs), Some(v)], vec![out]));
2326 g.add_output(out);
2327
2328 let before = g.num_nodes();
2329 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2330 assert!(g.nodes.values().all(|n| n.op_type != "FusedAttention"));
2331 assert_eq!(g.num_nodes(), before, "non-constant scale must NOT fuse");
2332 }
2333
2334 #[test]
2335 fn declines_sdpa_when_softmax_is_right_operand_of_output_matmul() {
2336 let mut g = Graph::new();
2339 g.opset_imports.insert(String::new(), 12);
2340 let q = fval(&mut g, "Q", &[1, 2, 3, 4]);
2341 let kt = fval(&mut g, "K", &[1, 2, 4, 3]);
2342 let v = fval(&mut g, "V", &[1, 2, 3, 3]);
2343 g.add_input(q);
2344 g.add_input(kt);
2345 g.add_input(v);
2346 let c = scalar_init(&mut g, "scale_c", 2.0);
2347 let scores = fval(&mut g, "scores", &[1, 2, 3, 3]);
2348 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(q), Some(kt)], vec![scores]));
2349 let scaled = fval(&mut g, "scaled", &[1, 2, 3, 3]);
2350 g.insert_node(Node::new(NodeId(0), "Div", vec![Some(scores), Some(c)], vec![scaled]));
2351 let probs = fval(&mut g, "probs", &[1, 2, 3, 3]);
2352 let mut sm = Node::new(NodeId(0), "Softmax", vec![Some(scaled)], vec![probs]);
2353 sm.attributes.insert("axis".into(), Attribute::Int(3));
2354 g.insert_node(sm);
2355 let out = fval(&mut g, "out", &[1, 2, 3, 3]);
2356 g.insert_node(Node::new(NodeId(0), "MatMul", vec![Some(v), Some(probs)], vec![out]));
2358 g.add_output(out);
2359
2360 let before = g.num_nodes();
2361 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2362 assert!(g.nodes.values().all(|n| n.op_type != "FusedAttention"));
2363 assert!(g.nodes.values().any(|n| n.op_type == "Softmax"));
2364 assert_eq!(g.num_nodes(), before);
2365 }
2366
2367 fn gelu_graph(inner_div_sqrt2: bool, half_mul: bool) -> Graph {
2372 let mut g = Graph::new();
2373 g.opset_imports.insert(String::new(), 17);
2374 let x = val(&mut g, "x");
2375 g.add_input(x);
2376
2377 let half = val(&mut g, "half");
2379 if half_mul {
2380 let c = scalar_init(&mut g, "c_half", 0.5);
2381 g.insert_node(Node::new(NodeId(0), "Mul", vec![Some(x), Some(c)], vec![half]));
2382 } else {
2383 let c = scalar_init(&mut g, "c_two", 2.0);
2384 g.insert_node(Node::new(NodeId(0), "Div", vec![Some(x), Some(c)], vec![half]));
2385 }
2386
2387 let scaled = val(&mut g, "scaled");
2389 if inner_div_sqrt2 {
2390 let c = scalar_init(&mut g, "c_sqrt2", std::f32::consts::SQRT_2);
2391 g.insert_node(Node::new(NodeId(0), "Div", vec![Some(x), Some(c)], vec![scaled]));
2392 } else {
2393 let c = scalar_init(&mut g, "c_isqrt2", std::f32::consts::FRAC_1_SQRT_2);
2394 g.insert_node(Node::new(NodeId(0), "Mul", vec![Some(x), Some(c)], vec![scaled]));
2395 }
2396
2397 let e = val(&mut g, "e");
2398 g.insert_node(Node::new(NodeId(0), "Erf", vec![Some(scaled)], vec![e]));
2399 let one = scalar_init(&mut g, "c_one", 1.0);
2400 let a = val(&mut g, "a");
2401 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(e), Some(one)], vec![a]));
2402 let out = val(&mut g, "out");
2403 g.insert_node(Node::new(NodeId(0), "Mul", vec![Some(half), Some(a)], vec![out]));
2404 g.add_output(out);
2405 g
2406 }
2407
2408 #[test]
2409 fn fuses_gelu_div_sqrt2() {
2410 let mut g = gelu_graph(true, true);
2411 assert_eq!(g.num_nodes(), 5);
2412 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2413 let gelu: Vec<_> = g.nodes.values().filter(|n| n.op_type == "Gelu").collect();
2414 assert_eq!(gelu.len(), 1, "the Erf decomposition must fuse to one Gelu");
2415 let fused = gelu[0];
2416 assert_eq!(fused.domain, CONTRIB_DOMAIN);
2417 assert_eq!(fused.inputs.len(), 1, "Gelu takes the single input x");
2418 assert!(fused.attributes.is_empty(), "exact Gelu has no attributes");
2419 let x = g.values.iter().find(|(_, v)| v.name.as_deref() == Some("x")).map(|(id, _)| id).unwrap();
2421 assert_eq!(fused.inputs[0], Some(x));
2422 assert_eq!(fused.outputs, g.outputs);
2423 assert!(g.nodes.values().all(|n| n.op_type != "Erf"));
2424 assert!(g.validate().is_ok());
2425 }
2426
2427 #[test]
2428 fn fuses_gelu_mul_reciprocal_and_div_two() {
2429 let mut g = gelu_graph(false, false);
2431 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2432 assert_eq!(
2433 g.nodes.values().filter(|n| n.op_type == "Gelu").count(),
2434 1,
2435 "the reciprocal/half-divisor encoding must also fuse"
2436 );
2437 assert!(g.validate().is_ok());
2438 }
2439
2440 #[test]
2441 fn declines_gelu_wrong_inner_constant() {
2442 let mut g = Graph::new();
2444 g.opset_imports.insert(String::new(), 17);
2445 let x = val(&mut g, "x");
2446 g.add_input(x);
2447 let half = val(&mut g, "half");
2448 let ch = scalar_init(&mut g, "c_half", 0.5);
2449 g.insert_node(Node::new(NodeId(0), "Mul", vec![Some(x), Some(ch)], vec![half]));
2450 let scaled = val(&mut g, "scaled");
2451 let cbad = scalar_init(&mut g, "c_bad", 2.0);
2452 g.insert_node(Node::new(NodeId(0), "Div", vec![Some(x), Some(cbad)], vec![scaled]));
2453 let e = val(&mut g, "e");
2454 g.insert_node(Node::new(NodeId(0), "Erf", vec![Some(scaled)], vec![e]));
2455 let one = scalar_init(&mut g, "c_one", 1.0);
2456 let a = val(&mut g, "a");
2457 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(e), Some(one)], vec![a]));
2458 let out = val(&mut g, "out");
2459 g.insert_node(Node::new(NodeId(0), "Mul", vec![Some(half), Some(a)], vec![out]));
2460 g.add_output(out);
2461
2462 let before = g.num_nodes();
2463 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2464 assert!(g.nodes.values().all(|n| n.op_type != "Gelu"));
2465 assert!(g.nodes.values().any(|n| n.op_type == "Erf"));
2466 assert_eq!(g.num_nodes(), before);
2467 }
2468
2469 #[test]
2470 fn declines_gelu_wrong_half_constant() {
2471 let mut g = gelu_graph(true, true);
2473 let ch = g.values.iter().find(|(_, v)| v.name.as_deref() == Some("c_half")).map(|(id, _)| id).unwrap();
2475 g.set_initializer(
2476 ch,
2477 WeightRef::Inline(TensorData::from_raw(DataType::Float32, vec![], 0.4f32.to_le_bytes().to_vec())),
2478 );
2479 let before = g.num_nodes();
2480 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2481 assert!(g.nodes.values().all(|n| n.op_type != "Gelu"));
2482 assert_eq!(g.num_nodes(), before);
2483 }
2484
2485 #[test]
2486 fn declines_gelu_when_half_uses_different_x() {
2487 let mut g = Graph::new();
2490 g.opset_imports.insert(String::new(), 17);
2491 let x = val(&mut g, "x");
2492 let y = val(&mut g, "y");
2493 g.add_input(x);
2494 g.add_input(y);
2495 let half = val(&mut g, "half");
2496 let ch = scalar_init(&mut g, "c_half", 0.5);
2497 g.insert_node(Node::new(NodeId(0), "Mul", vec![Some(y), Some(ch)], vec![half]));
2499 let scaled = val(&mut g, "scaled");
2500 let cs = scalar_init(&mut g, "c_sqrt2", std::f32::consts::SQRT_2);
2501 g.insert_node(Node::new(NodeId(0), "Div", vec![Some(x), Some(cs)], vec![scaled]));
2502 let e = val(&mut g, "e");
2503 g.insert_node(Node::new(NodeId(0), "Erf", vec![Some(scaled)], vec![e]));
2504 let one = scalar_init(&mut g, "c_one", 1.0);
2505 let a = val(&mut g, "a");
2506 g.insert_node(Node::new(NodeId(0), "Add", vec![Some(e), Some(one)], vec![a]));
2507 let out = val(&mut g, "out");
2508 g.insert_node(Node::new(NodeId(0), "Mul", vec![Some(half), Some(a)], vec![out]));
2509 g.add_output(out);
2510
2511 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2512 assert!(g.nodes.values().all(|n| n.op_type != "Gelu"));
2513 assert!(g.nodes.values().any(|n| n.op_type == "Erf"));
2514 }
2515
2516 #[test]
2517 fn declines_gelu_when_interior_escapes() {
2518 let mut g = gelu_graph(true, true);
2521 let e = g.values.iter().find(|(_, v)| v.name.as_deref() == Some("e")).map(|(id, _)| id).unwrap();
2522 let side = val(&mut g, "side");
2523 g.insert_node(Node::new(NodeId(0), "Erf", vec![Some(e)], vec![side]));
2524 g.add_output(side);
2525
2526 OpFusion::new().run(&mut g, &PassContext::new()).unwrap();
2527 assert!(
2528 g.nodes.values().all(|n| n.op_type != "Gelu"),
2529 "must not fuse when an interior value escapes"
2530 );
2531 }
2532
2533 fn run_restart_reference(patterns: &[FusionPattern], graph: &mut Graph) {
2534 for pattern in patterns {
2535 while let Some(matched) = pattern.find_match(graph) {
2536 pattern.apply_fusion(graph, &matched).unwrap();
2537 }
2538 }
2539 }
2540
2541 fn serialized_graph_bytes(mut graph: Graph) -> Vec<u8> {
2542 use std::fmt::Write;
2543
2544 let mut snapshot = String::new();
2545 writeln!(&mut snapshot, "inputs={:?}", graph.inputs).unwrap();
2546 writeln!(&mut snapshot, "outputs={:?}", graph.outputs).unwrap();
2547
2548 let mut initializers: Vec<_> = graph.initializers.iter().collect();
2549 initializers.sort_by_key(|(id, _)| id.0);
2550 writeln!(&mut snapshot, "initializers={initializers:?}").unwrap();
2551 let mut constraints: Vec<_> = graph.symbol_constraints.iter().collect();
2552 constraints.sort_by_key(|(id, _)| id.0);
2553 writeln!(&mut snapshot, "constraints={constraints:?}").unwrap();
2554 let mut opsets: Vec<_> = graph.opset_imports.iter().collect();
2555 opsets.sort_by_key(|(domain, _)| *domain);
2556 writeln!(&mut snapshot, "opsets={opsets:?}").unwrap();
2557 let mut subgraphs: Vec<_> = graph.subgraphs.iter().collect();
2558 subgraphs.sort_by_key(|((id, name), _)| (id.0, name.as_str()));
2559 writeln!(&mut snapshot, "subgraphs={subgraphs:?}").unwrap();
2560
2561 for (id, node) in graph.nodes.iter() {
2562 let mut attributes: Vec<_> = node.attributes.iter().collect();
2563 attributes.sort_by_key(|(name, _)| *name);
2564 writeln!(
2565 &mut snapshot,
2566 "node={id:?}|{:?}|{:?}|{:?}|{:?}|{:?}|{:?}|{attributes:?}|{:?}|{:?}|{:?}",
2567 node.id,
2568 node.name,
2569 node.op_type,
2570 node.domain,
2571 node.inputs,
2572 node.outputs,
2573 node.doc_string,
2574 node.device,
2575 node.exec_order,
2576 )
2577 .unwrap();
2578 }
2579 for (id, value) in graph.values.iter() {
2580 writeln!(&mut snapshot, "value={id:?}|{value:?}").unwrap();
2581 }
2582 writeln!(
2583 &mut snapshot,
2584 "topological_order={:?}",
2585 graph.topological_order().unwrap()
2586 )
2587 .unwrap();
2588
2589 for _ in 0..128 {
2593 let node =
2594 graph.insert_node(Node::new(NodeId(0), "ArenaProbe", Vec::new(), Vec::new()));
2595 let value = graph.create_value(DataType::Float32, static_shape([1]));
2596 writeln!(&mut snapshot, "probe={node:?}|{value:?}").unwrap();
2597 }
2598 snapshot.into_bytes()
2599 }
2600
2601 fn assert_fusion_graphs_byte_identical(actual: Graph, expected: Graph, trial: usize) {
2602 assert_eq!(
2603 serialized_graph_bytes(actual),
2604 serialized_graph_bytes(expected),
2605 "restart and resumable fixpoints differ byte-for-byte on trial {trial}"
2606 );
2607 }
2608
2609 struct FusionTestRng(u64);
2610
2611 impl FusionTestRng {
2612 fn next(&mut self) -> u64 {
2613 self.0 ^= self.0 << 13;
2614 self.0 ^= self.0 >> 7;
2615 self.0 ^= self.0 << 17;
2616 self.0
2617 }
2618
2619 fn usize(&mut self, upper: usize) -> usize {
2620 (self.next() as usize) % upper
2621 }
2622
2623 fn coin(&mut self) -> bool {
2624 self.next() & 1 == 0
2625 }
2626
2627 fn shuffle<T>(&mut self, values: &mut [T]) {
2628 for i in (1..values.len()).rev() {
2629 values.swap(i, self.usize(i + 1));
2630 }
2631 }
2632 }
2633
2634 struct DifferentialGraphBuilder {
2635 graph: Graph,
2636 pending: Vec<Node>,
2637 next_name: usize,
2638 }
2639
2640 impl DifferentialGraphBuilder {
2641 fn new() -> Self {
2642 let mut graph = Graph::new();
2643 graph.opset_imports.insert(String::new(), 17);
2644 Self {
2645 graph,
2646 pending: Vec::new(),
2647 next_name: 0,
2648 }
2649 }
2650
2651 fn value(&mut self, prefix: &str, dims: &[usize]) -> ValueId {
2652 let name = format!("{prefix}_{}", self.next_name);
2653 self.next_name += 1;
2654 self.graph.create_named_value(
2655 name,
2656 DataType::Float32,
2657 static_shape(dims.iter().copied()),
2658 )
2659 }
2660
2661 fn input(&mut self, prefix: &str, dims: &[usize]) -> ValueId {
2662 let value = self.value(prefix, dims);
2663 self.graph.add_input(value);
2664 value
2665 }
2666
2667 fn scalar(&mut self, prefix: &str, value: f32) -> ValueId {
2668 let id = self.value(prefix, &[]);
2669 self.graph.set_initializer(
2670 id,
2671 WeightRef::Inline(TensorData::from_raw(
2672 DataType::Float32,
2673 vec![],
2674 value.to_le_bytes().to_vec(),
2675 )),
2676 );
2677 id
2678 }
2679
2680 fn node(
2681 &mut self,
2682 name: impl Into<String>,
2683 op: &str,
2684 inputs: Vec<ValueId>,
2685 output: ValueId,
2686 ) -> &mut Node {
2687 let mut node = Node::new(
2688 NodeId(0),
2689 op,
2690 inputs.into_iter().map(Some).collect(),
2691 vec![output],
2692 );
2693 node.name = name.into();
2694 self.pending.push(node);
2695 self.pending.last_mut().unwrap()
2696 }
2697
2698 fn output(&mut self, value: ValueId) {
2699 self.graph.add_output(value);
2700 }
2701
2702 fn add_matmul_bias(&mut self, rng: &mut FusionTestRng, relu: bool, overlap: bool) {
2703 let a = self.input("mm_a", &[4]);
2704 let w0 = self.input("mm_w0", &[4]);
2705 let m0 = self.value("mm_m0", &[4]);
2706 self.node("mm_left", "MatMul", vec![a, w0], m0);
2707
2708 let bias = if overlap {
2709 let w1 = self.input("mm_w1", &[4]);
2710 let m1 = self.value("mm_m1", &[4]);
2711 self.node("mm_right", "MatMul", vec![a, w1], m1);
2712 m1
2713 } else {
2714 self.input("mm_bias", &[4])
2715 };
2716
2717 let add = self.value("mm_add", &[4]);
2718 let add_inputs = if rng.coin() {
2719 vec![m0, bias]
2720 } else {
2721 vec![bias, m0]
2722 };
2723 self.node("mm_add_node", "Add", add_inputs, add);
2724 if relu {
2725 let output = self.value("mm_relu", &[4]);
2726 self.node("mm_relu_node", "Relu", vec![add], output);
2727 self.output(output);
2728 } else {
2729 self.output(add);
2730 }
2731 }
2732
2733 fn add_layernorm(&mut self, split_diff: bool) {
2734 let x = self.input("ln_x", &[4]);
2735 let two = self.input("ln_two", &[4]);
2736 let eps = self.scalar("ln_eps", 1e-12);
2737 let scale = self.input("ln_scale", &[4]);
2738 let bias = self.input("ln_bias", &[4]);
2739
2740 let mean = self.value("ln_mean", &[4]);
2741 let rm1 = self.node("ln_mean_node", "ReduceMean", vec![x], mean);
2742 rm1.attributes
2743 .insert("axes".into(), Attribute::Ints(vec![-1]));
2744 rm1.attributes.insert("keepdims".into(), Attribute::Int(1));
2745
2746 let diff_pow = self.value("ln_diff_pow", &[4]);
2747 self.node("ln_sub_pow", "Sub", vec![x, mean], diff_pow);
2748 let diff_div = if split_diff {
2749 let value = self.value("ln_diff_div", &[4]);
2750 self.node("ln_sub_div", "Sub", vec![x, mean], value);
2751 value
2752 } else {
2753 diff_pow
2754 };
2755 let sq = self.value("ln_sq", &[4]);
2756 self.node("ln_pow", "Pow", vec![diff_pow, two], sq);
2757 let var = self.value("ln_var", &[4]);
2758 let rm2 = self.node("ln_var_node", "ReduceMean", vec![sq], var);
2759 rm2.attributes
2760 .insert("axes".into(), Attribute::Ints(vec![-1]));
2761 rm2.attributes.insert("keepdims".into(), Attribute::Int(1));
2762 let vare = self.value("ln_vare", &[4]);
2763 self.node("ln_add_eps", "Add", vec![var, eps], vare);
2764 let std = self.value("ln_std", &[4]);
2765 self.node("ln_sqrt", "Sqrt", vec![vare], std);
2766 let norm = self.value("ln_norm", &[4]);
2767 self.node("ln_div", "Div", vec![diff_div, std], norm);
2768 let scaled = self.value("ln_scaled", &[4]);
2769 self.node("ln_mul", "Mul", vec![norm, scale], scaled);
2770 let output = self.value("ln_output", &[4]);
2771 self.node("ln_add_bias", "Add", vec![scaled, bias], output);
2772 self.output(output);
2773 }
2774
2775 fn add_gelu(&mut self, rng: &mut FusionTestRng) {
2776 let x = self.input("gelu_x", &[4]);
2777 let half = self.value("gelu_half", &[4]);
2778 if rng.coin() {
2779 let c = self.scalar("gelu_half_c", 0.5);
2780 let inputs = if rng.coin() { vec![x, c] } else { vec![c, x] };
2781 self.node("gelu_half_node", "Mul", inputs, half);
2782 } else {
2783 let c = self.scalar("gelu_two_c", 2.0);
2784 self.node("gelu_half_node", "Div", vec![x, c], half);
2785 }
2786
2787 let scaled = self.value("gelu_scaled", &[4]);
2788 if rng.coin() {
2789 let c = self.scalar("gelu_sqrt2", std::f32::consts::SQRT_2);
2790 self.node("gelu_inner", "Div", vec![x, c], scaled);
2791 } else {
2792 let c = self.scalar("gelu_isqrt2", std::f32::consts::FRAC_1_SQRT_2);
2793 let inputs = if rng.coin() { vec![x, c] } else { vec![c, x] };
2794 self.node("gelu_inner", "Mul", inputs, scaled);
2795 }
2796 let erf = self.value("gelu_erf", &[4]);
2797 self.node("gelu_erf_node", "Erf", vec![scaled], erf);
2798 let one = self.scalar("gelu_one", 1.0);
2799 let plus_one = self.value("gelu_plus_one", &[4]);
2800 let inputs = if rng.coin() {
2801 vec![erf, one]
2802 } else {
2803 vec![one, erf]
2804 };
2805 self.node("gelu_add", "Add", inputs, plus_one);
2806 let output = self.value("gelu_output", &[4]);
2807 let inputs = if rng.coin() {
2808 vec![half, plus_one]
2809 } else {
2810 vec![plus_one, half]
2811 };
2812 self.node("gelu_outer", "Mul", inputs, output);
2813 self.output(output);
2814 }
2815
2816 fn add_attention(&mut self, rng: &mut FusionTestRng) {
2817 let q = self.input("attn_q", &[1, 2, 3, 4]);
2818 let k = self.input("attn_k", &[1, 2, 3, 4]);
2819 let v = self.input("attn_v", &[1, 2, 3, 4]);
2820 let k_side = if rng.coin() {
2821 let kt = self.value("attn_kt", &[1, 2, 4, 3]);
2822 let transpose = self.node("attn_transpose", "Transpose", vec![k], kt);
2823 transpose
2824 .attributes
2825 .insert("perm".into(), Attribute::Ints(vec![0, 1, 3, 2]));
2826 kt
2827 } else {
2828 k
2829 };
2830 let scores = self.value("attn_scores", &[1, 2, 3, 3]);
2831 self.node("attn_score_mm", "MatMul", vec![q, k_side], scores);
2832 let scale_const = if rng.coin() {
2833 self.scalar("attn_divisor", 2.0)
2834 } else {
2835 self.scalar("attn_multiplier", 0.5)
2836 };
2837 let scaled = self.value("attn_scaled", &[1, 2, 3, 3]);
2838 if self
2839 .graph
2840 .value(scale_const)
2841 .name
2842 .as_deref()
2843 .unwrap()
2844 .contains("divisor")
2845 {
2846 self.node("attn_scale", "Div", vec![scores, scale_const], scaled);
2847 } else {
2848 let inputs = if rng.coin() {
2849 vec![scores, scale_const]
2850 } else {
2851 vec![scale_const, scores]
2852 };
2853 self.node("attn_scale", "Mul", inputs, scaled);
2854 }
2855 let softmax_input = if rng.coin() {
2856 let mask = self.input("attn_mask", &[1, 1, 3, 3]);
2857 let masked = self.value("attn_masked", &[1, 2, 3, 3]);
2858 let inputs = if rng.coin() {
2859 vec![scaled, mask]
2860 } else {
2861 vec![mask, scaled]
2862 };
2863 self.node("attn_mask_add", "Add", inputs, masked);
2864 masked
2865 } else {
2866 scaled
2867 };
2868 let probs = self.value("attn_probs", &[1, 2, 3, 3]);
2869 let softmax = self.node("attn_softmax", "Softmax", vec![softmax_input], probs);
2870 softmax.attributes.insert(
2871 "axis".into(),
2872 Attribute::Int(if rng.coin() { -1 } else { 3 }),
2873 );
2874 let output = self.value("attn_output", &[1, 2, 3, 4]);
2875 self.node("attn_output_mm", "MatMul", vec![probs, v], output);
2876 self.output(output);
2877 }
2878
2879 fn add_resumable_chain(&mut self, rng: &mut FusionTestRng) {
2880 let input = self.input("chain_input", &[4]);
2881 let first = self.value("chain_start", &[4]);
2882 self.node("chain_0", "ChainStart", vec![input], first);
2883 let mut value = first;
2884 for index in 1..(4 + rng.usize(6)) {
2885 let output = self.value("chain_link", &[4]);
2886 self.node(format!("chain_{index}"), "ChainLink", vec![value], output);
2887 value = output;
2888 }
2889 self.output(value);
2890 }
2891
2892 fn add_noise(&mut self, rng: &mut FusionTestRng) {
2893 for index in 0..rng.usize(8) {
2894 let input = self.input("noise_input", &[4]);
2895 let output = self.value("noise_output", &[4]);
2896 let op = ["Abs", "Neg", "Identity", "Tanh"][rng.usize(4)];
2897 self.node(format!("noise_{index}"), op, vec![input], output);
2898 self.output(output);
2899 }
2900 }
2901
2902 fn finish(mut self, rng: &mut FusionTestRng) -> Graph {
2903 let mut slots = Vec::with_capacity(self.pending.len());
2907 for _ in 0..self.pending.len() {
2908 slots.push(self.graph.insert_node(Node::new(
2909 NodeId(0),
2910 "IdSeed",
2911 Vec::new(),
2912 Vec::new(),
2913 )));
2914 }
2915 rng.shuffle(&mut slots);
2916 for id in slots {
2917 self.graph.remove_node(id);
2918 }
2919 rng.shuffle(&mut self.pending);
2920 for node in self.pending {
2921 self.graph.insert_node(node);
2922 }
2923 self.graph
2924 }
2925 }
2926
2927 fn randomized_fusion_graph(rng: &mut FusionTestRng) -> Graph {
2928 let mut builder = DifferentialGraphBuilder::new();
2929 builder.add_attention(rng);
2933 builder.add_matmul_bias(rng, true, true);
2934 builder.add_layernorm(rng.coin());
2935 builder.add_gelu(rng);
2936 builder.add_matmul_bias(rng, false, true);
2937
2938 for _ in 0..rng.usize(4) {
2940 match rng.usize(5) {
2941 0 => builder.add_attention(rng),
2942 1 => {
2943 let overlap = rng.coin();
2944 builder.add_matmul_bias(rng, true, overlap);
2945 }
2946 2 => builder.add_layernorm(rng.coin()),
2947 3 => builder.add_gelu(rng),
2948 _ => {
2949 let overlap = rng.coin();
2950 builder.add_matmul_bias(rng, false, overlap);
2951 }
2952 }
2953 }
2954 builder.add_resumable_chain(rng);
2955 builder.add_noise(rng);
2956 builder.finish(rng)
2957 }
2958
2959 fn differential_patterns() -> Vec<FusionPattern> {
2960 let mut patterns = default_fusion_patterns();
2961 patterns.push(
2966 FusionPattern::new("ResumableChain", &["ChainStart", "ChainLink"], "ChainStart")
2967 .with_replacement_domain(""),
2968 );
2969 patterns
2970 }
2971
2972 struct AffectedRevisitCase {
2973 graph: Graph,
2974 lower_start: NodeId,
2975 later_start: NodeId,
2976 first_middle: NodeId,
2977 first_tail: NodeId,
2978 final_tail: NodeId,
2979 }
2980
2981 fn affected_revisit_case(seed: u64) -> AffectedRevisitCase {
2982 let mut rng = FusionTestRng(seed ^ 0xbb67_ae85_84ca_a73b);
2983 let noise_count = 3 + rng.usize(6);
2984 let slot_count = 5 + noise_count;
2985 let later_start = NodeId((slot_count - 1) as u32);
2986 let mut lower_ids: Vec<u32> = (0..later_start.0).collect();
2987 rng.shuffle(&mut lower_ids);
2988 let lower_start = NodeId(lower_ids[0]);
2989 let first_middle = NodeId(lower_ids[1]);
2990 let first_tail = NodeId(lower_ids[2]);
2991 let final_tail = NodeId(lower_ids[3]);
2992
2993 let mut graph = Graph::new();
2994 graph.opset_imports.insert(String::new(), 17);
2995 let lower_input = graph.create_named_value(
2996 format!("lower_input_{seed}"),
2997 DataType::Float32,
2998 static_shape([4]),
2999 );
3000 let later_input = graph.create_named_value(
3001 format!("later_input_{seed}"),
3002 DataType::Float32,
3003 static_shape([4]),
3004 );
3005 graph.add_input(lower_input);
3006 graph.add_input(later_input);
3007 let lower_value = graph.create_named_value(
3008 format!("lower_value_{seed}"),
3009 DataType::Float32,
3010 static_shape([4]),
3011 );
3012 let middle_value = graph.create_named_value(
3013 format!("middle_value_{seed}"),
3014 DataType::Float32,
3015 static_shape([4]),
3016 );
3017 let first_output = graph.create_named_value(
3018 format!("first_output_{seed}"),
3019 DataType::Float32,
3020 static_shape([4]),
3021 );
3022 let first_tail_output = graph.create_named_value(
3023 format!("first_tail_output_{seed}"),
3024 DataType::Float32,
3025 static_shape([4]),
3026 );
3027 let final_output = graph.create_named_value(
3028 format!("final_output_{seed}"),
3029 DataType::Float32,
3030 static_shape([4]),
3031 );
3032 graph.add_output(final_output);
3033
3034 let mut placements = vec![
3035 (
3036 lower_start,
3037 Node::new(
3038 NodeId(0),
3039 "AdversaryStart",
3040 vec![Some(lower_input)],
3041 vec![lower_value],
3042 ),
3043 ),
3044 (
3045 later_start,
3046 Node::new(
3047 NodeId(0),
3048 "AdversaryStart",
3049 vec![Some(later_input)],
3050 vec![middle_value],
3051 ),
3052 ),
3053 (
3054 first_middle,
3055 Node::new(
3056 NodeId(0),
3057 "AdversaryMiddle",
3058 vec![Some(middle_value)],
3059 vec![first_output],
3060 ),
3061 ),
3062 (
3063 first_tail,
3064 Node::new(
3065 NodeId(0),
3066 "AdversaryTail",
3067 vec![Some(first_output), Some(lower_value)],
3068 vec![first_tail_output],
3069 ),
3070 ),
3071 (
3072 final_tail,
3073 Node::new(
3074 NodeId(0),
3075 "AdversaryTail",
3076 vec![Some(first_tail_output)],
3077 vec![final_output],
3078 ),
3079 ),
3080 ];
3081 let core_ids = HashSet::from([
3082 lower_start,
3083 later_start,
3084 first_middle,
3085 first_tail,
3086 final_tail,
3087 ]);
3088 let mut noise_index = 0;
3089 for raw_id in 0..slot_count as u32 {
3090 let id = NodeId(raw_id);
3091 if core_ids.contains(&id) {
3092 continue;
3093 }
3094 let input = graph.create_named_value(
3095 format!("noise_input_{seed}_{noise_index}"),
3096 DataType::Float32,
3097 static_shape([4]),
3098 );
3099 let output = graph.create_named_value(
3100 format!("noise_output_{seed}_{noise_index}"),
3101 DataType::Float32,
3102 static_shape([4]),
3103 );
3104 graph.add_input(input);
3105 graph.add_output(output);
3106 placements.push((
3107 id,
3108 Node::new(
3109 NodeId(0),
3110 ["Abs", "Neg", "Identity", "Tanh"][rng.usize(4)],
3111 vec![Some(input)],
3112 vec![output],
3113 ),
3114 ));
3115 noise_index += 1;
3116 }
3117
3118 rng.shuffle(&mut placements);
3119 for _ in 0..slot_count {
3120 graph.insert_node(Node::new(NodeId(0), "IdSeed", Vec::new(), Vec::new()));
3121 }
3122 for &(target, _) in placements.iter().rev() {
3123 graph.remove_node(target);
3124 }
3125 for (expected, node) in placements {
3126 assert_eq!(graph.insert_node(node), expected);
3127 }
3128
3129 AffectedRevisitCase {
3130 graph,
3131 lower_start,
3132 later_start,
3133 first_middle,
3134 first_tail,
3135 final_tail,
3136 }
3137 }
3138
3139 #[test]
3140 fn affected_candidate_starts_revisits_newly_eligible_lower_ids() {
3141 const TRIALS: usize = 5_000;
3142 let pattern = FusionPattern::new(
3143 "AffectedBehindCursor",
3144 &["AdversaryStart", "AdversaryMiddle", "AdversaryTail"],
3145 "AdversaryMiddle",
3146 )
3147 .with_replacement_domain("");
3148 let patterns = vec![pattern.clone()];
3149 let mut reclaimable_low_slot_trials = 0;
3150 let mut affected_scheduled = 0;
3151 let mut affected_revisit_hits = 0;
3152
3153 for trial in 0..TRIALS {
3154 let case = affected_revisit_case(trial as u64);
3155 assert!(case.graph.validate().is_ok(), "invalid trial {trial}");
3156 assert!(case.lower_start.0 < case.later_start.0);
3157 assert!(case.first_middle.0 < case.later_start.0);
3158 assert!(case.first_tail.0 < case.later_start.0);
3159 assert!(pattern.try_match_at(&case.graph, case.lower_start).is_none());
3160 let first_match = pattern
3161 .try_match_at(&case.graph, case.later_start)
3162 .expect("later start must be the first eligible match");
3163 assert_eq!(
3164 first_match.nodes,
3165 vec![case.later_start, case.first_middle, case.first_tail]
3166 );
3167
3168 let mut reclaim_probe = case.graph.clone();
3171 let first_fused = pattern
3172 .apply_fusion_returning_id(&mut reclaim_probe, &first_match)
3173 .unwrap();
3174 assert_eq!(first_fused, case.later_start);
3175 let probe_id = reclaim_probe.insert_node(Node::new(
3176 NodeId(0),
3177 "ReclaimProbe",
3178 Vec::new(),
3179 Vec::new(),
3180 ));
3181 assert_eq!(probe_id, case.first_middle);
3182 reclaimable_low_slot_trials += 1;
3183
3184 let mut reference = case.graph.clone();
3185 run_restart_reference(&patterns, &mut reference);
3186 let mut actual = case.graph;
3187 let mut lower_was_scheduled = false;
3188 let mut trial_hits = 0;
3189 OpFusion::with_patterns(patterns.clone())
3190 .run_with_fusion_observer(
3191 &mut actual,
3192 |name, source, start, matched, affected, fused_id| {
3193 if name != "AffectedBehindCursor" {
3194 return;
3195 }
3196 assert_eq!(
3197 fused_id, matched[0],
3198 "replacement must reuse the just-freed match-start slot"
3199 );
3200 if start == case.later_start {
3201 assert_eq!(source, ScanCandidateSource::Initial);
3202 assert_eq!(
3203 matched,
3204 &[case.later_start, case.first_middle, case.first_tail]
3205 );
3206 assert!(affected.contains(&case.lower_start));
3207 assert_ne!(fused_id, case.lower_start);
3208 lower_was_scheduled = true;
3209 affected_scheduled += 1;
3210 } else if start == case.lower_start {
3211 assert!(lower_was_scheduled);
3212 assert_eq!(source, ScanCandidateSource::Revisit);
3213 assert_eq!(
3214 matched,
3215 &[case.lower_start, case.later_start, case.final_tail]
3216 );
3217 trial_hits += 1;
3218 affected_revisit_hits += 1;
3219 }
3220 },
3221 )
3222 .unwrap();
3223
3224 assert_eq!(trial_hits, 1, "affected revisit not hit on trial {trial}");
3225 assert!(actual.validate().is_ok(), "invalid result on trial {trial}");
3226 assert_fusion_graphs_byte_identical(actual, reference, trial);
3227 }
3228
3229 assert_eq!(reclaimable_low_slot_trials, TRIALS);
3230 assert_eq!(affected_scheduled, TRIALS);
3231 assert_eq!(affected_revisit_hits, TRIALS);
3232 eprintln!(
3233 "affected behind-cursor revisit hits: {affected_revisit_hits}/{TRIALS} \
3234 ({}%); reclaimable lower slots present: {reclaimable_low_slot_trials}/{TRIALS}",
3235 affected_revisit_hits * 100 / TRIALS
3236 );
3237 }
3238
3239 fn assert_overlapping_structural_candidates(graph: &Graph, patterns: &[FusionPattern]) {
3240 let mut saw_gemm_overlap = false;
3241 let mut saw_bias_overlap = false;
3242 for (add_id, add) in graph.nodes.iter().filter(|(_, node)| node.op_type == "Add") {
3243 let starts: Vec<_> = add
3244 .input_values()
3245 .filter_map(|value| graph.value(value).producer)
3246 .filter(|&producer| graph.node(producer).op_type == "MatMul")
3247 .collect();
3248 if starts.len() != 2 {
3249 continue;
3250 }
3251 let has_relu = graph
3252 .successors(add_id)
3253 .iter()
3254 .any(|&successor| graph.node(successor).op_type == "Relu");
3255 let pattern = if has_relu { &patterns[1] } else { &patterns[4] };
3256 assert!(
3257 starts
3258 .iter()
3259 .all(|&start| pattern.try_match_at(graph, start).is_some()),
3260 "both MatMul starts must be eligible for the shared structural tail"
3261 );
3262 saw_gemm_overlap |= has_relu;
3263 saw_bias_overlap |= !has_relu;
3264 }
3265 assert!(saw_gemm_overlap, "missing shared MatMul+Add+Relu candidates");
3266 assert!(saw_bias_overlap, "missing shared MatMul+Add candidates");
3267 }
3268
3269 #[test]
3270 fn resumable_scan_matches_restart_reference_on_randomized_graphs() {
3271 const TRIALS: usize = 5_000;
3272 const REGISTERED_PATTERNS: usize = 5;
3273 let mut rng = FusionTestRng(0x6a09_e667_f3bc_c909);
3274 let patterns = differential_patterns();
3275 assert_eq!(default_fusion_patterns().len(), REGISTERED_PATTERNS);
3276
3277 let mut saw_non_topological_ids = false;
3278 for trial in 0..TRIALS {
3279 let mut graph = randomized_fusion_graph(&mut rng);
3280 assert!(
3281 graph.validate().is_ok(),
3282 "invalid input graph on trial {trial}"
3283 );
3284
3285 for pattern in &patterns[..REGISTERED_PATTERNS] {
3286 assert!(
3287 pattern.find_match(&graph).is_some(),
3288 "{} was not exercised on trial {trial}",
3289 pattern.pattern_name()
3290 );
3291 }
3292 assert_overlapping_structural_candidates(&graph, &patterns);
3293 assert!(
3294 graph
3295 .nodes
3296 .values()
3297 .filter(|node| node.op_type == "ChainLink")
3298 .count()
3299 >= 3,
3300 "chained replacement adversary must require multiple revisits"
3301 );
3302 saw_non_topological_ids |=
3303 graph.topological_order().unwrap() != graph.nodes.keys().collect::<Vec<_>>();
3304
3305 let mut reference = graph.clone();
3306 run_restart_reference(&patterns, &mut reference);
3307 OpFusion::with_patterns(patterns.clone())
3308 .run(&mut graph, &PassContext::new())
3309 .unwrap();
3310
3311 assert!(
3312 graph.validate().is_ok(),
3313 "invalid result graph on trial {trial}"
3314 );
3315 assert!(
3316 graph.nodes.values().all(|node| node.op_type != "ChainLink"),
3317 "resumable chain did not reach its fixpoint on trial {trial}"
3318 );
3319 assert_fusion_graphs_byte_identical(graph, reference, trial);
3320 }
3321 assert!(
3322 saw_non_topological_ids,
3323 "randomized insertion must decouple NodeId from topological order"
3324 );
3325 }
3326}