1#[derive(Clone, Debug, PartialEq)]
29pub enum TensorOp {
30 Scale { factor: f64 },
32 Bias { offset: f64 },
34 Relu,
36 Clamp { min: f64, max: f64 },
38 Normalize,
40 MatMul { rows: usize, cols: usize },
43}
44
45#[derive(Clone, Debug, PartialEq)]
47pub enum FusedOp {
48 ScaleBias { scale: f64, bias: f64 },
50 ScaleReluBias { scale: f64, bias: f64 },
52 ClampNormalize { min: f64, max: f64 },
54 Identity,
56 Passthrough(TensorOp),
58}
59
60#[derive(Clone, Debug)]
62pub struct FusionPlan {
63 pub ops: Vec<FusedOp>,
65 pub original_op_count: usize,
67 pub fused_op_count: usize,
69}
70
71impl FusionPlan {
72 pub fn reduction_ratio(&self) -> f64 {
76 if self.original_op_count == 0 {
77 return 0.0;
78 }
79 let reduced = self.original_op_count.saturating_sub(self.fused_op_count);
80 reduced as f64 / self.original_op_count as f64
81 }
82}
83
84#[derive(Clone, Debug, Default)]
86pub struct FusionStats {
87 pub total_fusion_runs: u64,
89 pub total_ops_fused: u64,
91 pub total_ops_reduced: u64,
93}
94
95#[derive(Debug, Default)]
112pub struct TensorOpFusion {
113 stats: FusionStats,
114}
115
116impl TensorOpFusion {
117 pub fn new() -> Self {
119 Self {
120 stats: FusionStats::default(),
121 }
122 }
123
124 pub fn fuse(&mut self, ops: Vec<TensorOp>) -> FusionPlan {
126 let original_op_count = ops.len();
127
128 let fused_ops = if ops.is_empty() {
129 vec![FusedOp::Identity]
130 } else {
131 Self::fuse_sequence(ops)
132 };
133
134 let fused_op_count = fused_ops.len();
135 let reduced = original_op_count.saturating_sub(fused_op_count);
136
137 self.stats.total_fusion_runs += 1;
138 self.stats.total_ops_fused += original_op_count as u64;
139 self.stats.total_ops_reduced += reduced as u64;
140
141 FusionPlan {
142 ops: fused_ops,
143 original_op_count,
144 fused_op_count,
145 }
146 }
147
148 pub fn stats(&self) -> &FusionStats {
150 &self.stats
151 }
152
153 pub fn can_fuse(a: &TensorOp, b: &TensorOp) -> bool {
162 matches!(
163 (a, b),
164 (TensorOp::Scale { .. }, TensorOp::Bias { .. })
165 | (TensorOp::Scale { .. }, TensorOp::Relu)
166 | (TensorOp::Relu, TensorOp::Bias { .. })
167 | (TensorOp::Clamp { .. }, TensorOp::Normalize)
168 )
169 }
170
171 fn fuse_sequence(ops: Vec<TensorOp>) -> Vec<FusedOp> {
177 let mut result: Vec<FusedOp> = Vec::with_capacity(ops.len());
178 let mut cursor = 0usize;
179
180 while cursor < ops.len() {
181 if cursor + 2 < ops.len() {
183 if let Some(fused) =
184 Self::try_fuse_three(&ops[cursor], &ops[cursor + 1], &ops[cursor + 2])
185 {
186 result.push(fused);
187 cursor += 3;
188 continue;
189 }
190 }
191
192 if cursor + 1 < ops.len() {
194 if let Some(fused) = Self::try_fuse_two(&ops[cursor], &ops[cursor + 1]) {
195 result.push(fused);
196 cursor += 2;
197 continue;
198 }
199 }
200
201 result.push(FusedOp::Passthrough(ops[cursor].clone()));
203 cursor += 1;
204 }
205
206 result
207 }
208
209 fn try_fuse_three(a: &TensorOp, b: &TensorOp, c: &TensorOp) -> Option<FusedOp> {
211 match (a, b, c) {
212 (
213 TensorOp::Scale { factor: scale },
214 TensorOp::Relu,
215 TensorOp::Bias { offset: bias },
216 ) => Some(FusedOp::ScaleReluBias {
217 scale: *scale,
218 bias: *bias,
219 }),
220 _ => None,
221 }
222 }
223
224 fn try_fuse_two(a: &TensorOp, b: &TensorOp) -> Option<FusedOp> {
226 match (a, b) {
227 (TensorOp::Scale { factor: scale }, TensorOp::Bias { offset: bias }) => {
228 Some(FusedOp::ScaleBias {
229 scale: *scale,
230 bias: *bias,
231 })
232 }
233 (TensorOp::Clamp { min, max }, TensorOp::Normalize) => Some(FusedOp::ClampNormalize {
234 min: *min,
235 max: *max,
236 }),
237 _ => None,
238 }
239 }
240}
241
242#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
253 fn new_starts_with_zero_stats() {
254 let engine = TensorOpFusion::new();
255 let s = engine.stats();
256 assert_eq!(s.total_fusion_runs, 0);
257 assert_eq!(s.total_ops_fused, 0);
258 assert_eq!(s.total_ops_reduced, 0);
259 }
260
261 #[test]
264 fn fuse_empty_returns_identity() {
265 let mut engine = TensorOpFusion::new();
266 let plan = engine.fuse(vec![]);
267 assert_eq!(plan.ops, vec![FusedOp::Identity]);
268 }
269
270 #[test]
271 fn fuse_empty_original_count_zero() {
272 let mut engine = TensorOpFusion::new();
273 let plan = engine.fuse(vec![]);
274 assert_eq!(plan.original_op_count, 0);
275 }
276
277 #[test]
278 fn reduction_ratio_zero_for_empty() {
279 let mut engine = TensorOpFusion::new();
280 let plan = engine.fuse(vec![]);
281 assert!((plan.reduction_ratio() - 0.0).abs() < f64::EPSILON);
282 }
283
284 #[test]
287 fn fuse_scale_bias_produces_scale_bias() {
288 let mut engine = TensorOpFusion::new();
289 let plan = engine.fuse(vec![
290 TensorOp::Scale { factor: 3.0 },
291 TensorOp::Bias { offset: 1.5 },
292 ]);
293 assert_eq!(plan.ops.len(), 1);
294 assert_eq!(
295 plan.ops[0],
296 FusedOp::ScaleBias {
297 scale: 3.0,
298 bias: 1.5
299 }
300 );
301 }
302
303 #[test]
304 fn scale_bias_correct_values() {
305 let mut engine = TensorOpFusion::new();
306 let plan = engine.fuse(vec![
307 TensorOp::Scale { factor: 0.5 },
308 TensorOp::Bias { offset: -2.0 },
309 ]);
310 match &plan.ops[0] {
311 FusedOp::ScaleBias { scale, bias } => {
312 assert!((scale - 0.5).abs() < f64::EPSILON);
313 assert!((bias - (-2.0)).abs() < f64::EPSILON);
314 }
315 other => panic!("expected ScaleBias, got {:?}", other),
316 }
317 }
318
319 #[test]
322 fn fuse_scale_relu_bias_produces_scale_relu_bias() {
323 let mut engine = TensorOpFusion::new();
324 let plan = engine.fuse(vec![
325 TensorOp::Scale { factor: 2.0 },
326 TensorOp::Relu,
327 TensorOp::Bias { offset: 0.1 },
328 ]);
329 assert_eq!(plan.ops.len(), 1);
330 assert_eq!(
331 plan.ops[0],
332 FusedOp::ScaleReluBias {
333 scale: 2.0,
334 bias: 0.1
335 }
336 );
337 }
338
339 #[test]
340 fn scale_relu_bias_correct_values() {
341 let mut engine = TensorOpFusion::new();
342 let plan = engine.fuse(vec![
343 TensorOp::Scale { factor: -1.0 },
344 TensorOp::Relu,
345 TensorOp::Bias { offset: 4.0 },
346 ]);
347 match &plan.ops[0] {
348 FusedOp::ScaleReluBias { scale, bias } => {
349 assert!((scale - (-1.0)).abs() < f64::EPSILON);
350 assert!((bias - 4.0).abs() < f64::EPSILON);
351 }
352 other => panic!("expected ScaleReluBias, got {:?}", other),
353 }
354 }
355
356 #[test]
359 fn fuse_clamp_normalize_produces_clamp_normalize() {
360 let mut engine = TensorOpFusion::new();
361 let plan = engine.fuse(vec![
362 TensorOp::Clamp {
363 min: -1.0,
364 max: 1.0,
365 },
366 TensorOp::Normalize,
367 ]);
368 assert_eq!(plan.ops.len(), 1);
369 assert_eq!(
370 plan.ops[0],
371 FusedOp::ClampNormalize {
372 min: -1.0,
373 max: 1.0
374 }
375 );
376 }
377
378 #[test]
381 fn fuse_single_scale_is_passthrough() {
382 let mut engine = TensorOpFusion::new();
383 let plan = engine.fuse(vec![TensorOp::Scale { factor: 5.0 }]);
384 assert_eq!(
385 plan.ops,
386 vec![FusedOp::Passthrough(TensorOp::Scale { factor: 5.0 })]
387 );
388 }
389
390 #[test]
391 fn fuse_single_relu_is_passthrough() {
392 let mut engine = TensorOpFusion::new();
393 let plan = engine.fuse(vec![TensorOp::Relu]);
394 assert_eq!(plan.ops, vec![FusedOp::Passthrough(TensorOp::Relu)]);
395 }
396
397 #[test]
398 fn fuse_single_matmul_is_passthrough() {
399 let mut engine = TensorOpFusion::new();
400 let plan = engine.fuse(vec![TensorOp::MatMul { rows: 4, cols: 8 }]);
401 assert_eq!(
402 plan.ops,
403 vec![FusedOp::Passthrough(TensorOp::MatMul { rows: 4, cols: 8 })]
404 );
405 }
406
407 #[test]
410 fn matmul_breaks_fusion_chain() {
411 let mut engine = TensorOpFusion::new();
412 let plan = engine.fuse(vec![
414 TensorOp::Scale { factor: 2.0 },
415 TensorOp::MatMul { rows: 2, cols: 2 },
416 TensorOp::Bias { offset: 1.0 },
417 ]);
418 assert_eq!(plan.ops.len(), 3);
419 assert_eq!(
420 plan.ops[0],
421 FusedOp::Passthrough(TensorOp::Scale { factor: 2.0 })
422 );
423 assert_eq!(
424 plan.ops[1],
425 FusedOp::Passthrough(TensorOp::MatMul { rows: 2, cols: 2 })
426 );
427 assert_eq!(
428 plan.ops[2],
429 FusedOp::Passthrough(TensorOp::Bias { offset: 1.0 })
430 );
431 }
432
433 #[test]
436 fn bias_then_scale_produces_two_passthroughs() {
437 let mut engine = TensorOpFusion::new();
438 let plan = engine.fuse(vec![
439 TensorOp::Bias { offset: 1.0 },
440 TensorOp::Scale { factor: 2.0 },
441 ]);
442 assert_eq!(plan.ops.len(), 2);
443 assert_eq!(
444 plan.ops[0],
445 FusedOp::Passthrough(TensorOp::Bias { offset: 1.0 })
446 );
447 assert_eq!(
448 plan.ops[1],
449 FusedOp::Passthrough(TensorOp::Scale { factor: 2.0 })
450 );
451 }
452
453 #[test]
456 fn reduction_ratio_scale_bias() {
457 let mut engine = TensorOpFusion::new();
458 let plan = engine.fuse(vec![
459 TensorOp::Scale { factor: 1.0 },
460 TensorOp::Bias { offset: 0.0 },
461 ]);
462 let expected = 0.5;
464 assert!((plan.reduction_ratio() - expected).abs() < f64::EPSILON);
465 }
466
467 #[test]
468 fn reduction_ratio_scale_relu_bias() {
469 let mut engine = TensorOpFusion::new();
470 let plan = engine.fuse(vec![
471 TensorOp::Scale { factor: 1.0 },
472 TensorOp::Relu,
473 TensorOp::Bias { offset: 0.0 },
474 ]);
475 let expected = 2.0 / 3.0;
477 assert!((plan.reduction_ratio() - expected).abs() < 1e-10);
478 }
479
480 #[test]
483 fn fused_op_count_equals_ops_len() {
484 let mut engine = TensorOpFusion::new();
485 let plan = engine.fuse(vec![
486 TensorOp::Scale { factor: 1.0 },
487 TensorOp::Bias { offset: 0.0 },
488 TensorOp::Relu,
489 ]);
490 assert_eq!(plan.fused_op_count, plan.ops.len());
491 }
492
493 #[test]
496 fn stats_fusion_runs_increments() {
497 let mut engine = TensorOpFusion::new();
498 engine.fuse(vec![TensorOp::Relu]);
499 engine.fuse(vec![TensorOp::Relu]);
500 assert_eq!(engine.stats().total_fusion_runs, 2);
501 }
502
503 #[test]
504 fn stats_total_ops_fused_accumulates() {
505 let mut engine = TensorOpFusion::new();
506 engine.fuse(vec![TensorOp::Relu, TensorOp::Relu]);
507 engine.fuse(vec![TensorOp::Scale { factor: 1.0 }]);
508 assert_eq!(engine.stats().total_ops_fused, 3);
510 }
511
512 #[test]
513 fn stats_total_ops_reduced_correct() {
514 let mut engine = TensorOpFusion::new();
515 engine.fuse(vec![
517 TensorOp::Scale { factor: 1.0 },
518 TensorOp::Bias { offset: 0.0 },
519 ]);
520 engine.fuse(vec![
522 TensorOp::Scale { factor: 2.0 },
523 TensorOp::Relu,
524 TensorOp::Bias { offset: 0.5 },
525 ]);
526 assert_eq!(engine.stats().total_ops_reduced, 3);
527 }
528
529 #[test]
532 fn can_fuse_scale_bias_true() {
533 assert!(TensorOpFusion::can_fuse(
534 &TensorOp::Scale { factor: 1.0 },
535 &TensorOp::Bias { offset: 0.0 }
536 ));
537 }
538
539 #[test]
540 fn can_fuse_scale_relu_true() {
541 assert!(TensorOpFusion::can_fuse(
542 &TensorOp::Scale { factor: 1.0 },
543 &TensorOp::Relu
544 ));
545 }
546
547 #[test]
548 fn can_fuse_relu_bias_true() {
549 assert!(TensorOpFusion::can_fuse(
550 &TensorOp::Relu,
551 &TensorOp::Bias { offset: 0.0 }
552 ));
553 }
554
555 #[test]
556 fn can_fuse_matmul_anything_false() {
557 assert!(!TensorOpFusion::can_fuse(
558 &TensorOp::MatMul { rows: 2, cols: 2 },
559 &TensorOp::Scale { factor: 1.0 }
560 ));
561 assert!(!TensorOpFusion::can_fuse(
562 &TensorOp::MatMul { rows: 2, cols: 2 },
563 &TensorOp::Bias { offset: 0.0 }
564 ));
565 assert!(!TensorOpFusion::can_fuse(
566 &TensorOp::MatMul { rows: 2, cols: 2 },
567 &TensorOp::Relu
568 ));
569 assert!(!TensorOpFusion::can_fuse(
570 &TensorOp::MatMul { rows: 2, cols: 2 },
571 &TensorOp::Normalize
572 ));
573 }
574
575 #[test]
578 fn mixed_sequence_fuses_correctly() {
579 let mut engine = TensorOpFusion::new();
580 let plan = engine.fuse(vec![
583 TensorOp::Scale { factor: 2.0 },
584 TensorOp::Bias { offset: 1.0 },
585 TensorOp::Clamp { min: 0.0, max: 5.0 },
586 TensorOp::Normalize,
587 TensorOp::Relu,
588 ]);
589 assert_eq!(plan.fused_op_count, 3);
590 assert_eq!(
591 plan.ops[0],
592 FusedOp::ScaleBias {
593 scale: 2.0,
594 bias: 1.0
595 }
596 );
597 assert_eq!(plan.ops[1], FusedOp::ClampNormalize { min: 0.0, max: 5.0 });
598 assert_eq!(plan.ops[2], FusedOp::Passthrough(TensorOp::Relu));
599 }
600}