1use std::collections::HashMap;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub enum BackendKind {
14 Cpu,
16 Gpu,
18 Remote,
20 Simulated,
22}
23
24#[derive(Clone, Debug, PartialEq)]
30pub enum DispatchOp {
31 MatMul { m: usize, n: usize, k: usize },
33 ElementWise {
35 op_name: String,
36 element_count: usize,
37 },
38 Reduction { op_name: String, dims: Vec<usize> },
40 Convolution { kernel_size: usize, channels: usize },
42}
43
44#[derive(Clone, Debug)]
50pub struct BackendRegistration {
51 pub kind: BackendKind,
53 pub priority: u32,
55 pub supported_ops: Vec<String>,
58 pub is_available: bool,
60}
61
62impl BackendRegistration {
63 pub fn supports_op(&self, op: &DispatchOp) -> bool {
72 match op {
73 DispatchOp::MatMul { .. } => match self.kind {
74 BackendKind::Cpu | BackendKind::Simulated => true,
75 BackendKind::Gpu | BackendKind::Remote => {
76 self.supported_ops.iter().any(|s| s == "matmul")
77 }
78 },
79 DispatchOp::ElementWise { op_name, .. } => {
80 self.supported_ops.iter().any(|s| s == op_name)
81 }
82 DispatchOp::Reduction { op_name, .. } => {
83 self.supported_ops.iter().any(|s| s == op_name)
84 }
85 DispatchOp::Convolution { .. } => self.supported_ops.iter().any(|s| s == "conv"),
86 }
87 }
88}
89
90#[derive(Clone, Debug, PartialEq)]
96pub struct DispatchResult {
97 pub op: DispatchOp,
99 pub selected_backend: BackendKind,
101 pub fallback_used: bool,
104}
105
106#[derive(Clone, Debug)]
112pub struct BackendStats {
113 pub backend: BackendKind,
115 pub total_dispatched: u64,
117 pub total_fallback_selected: u64,
120}
121
122impl BackendStats {
123 fn new(backend: BackendKind) -> Self {
124 Self {
125 backend,
126 total_dispatched: 0,
127 total_fallback_selected: 0,
128 }
129 }
130}
131
132#[derive(Clone, Debug, Default)]
138pub struct DispatcherStats {
139 pub total_dispatched: u64,
141 pub total_fallbacks: u64,
143 pub total_failed: u64,
145}
146
147pub struct TensorOpDispatcher {
165 pub backends: Vec<BackendRegistration>,
167 pub backend_stats: HashMap<BackendKind, BackendStats>,
169 pub dispatcher_stats: DispatcherStats,
171}
172
173impl TensorOpDispatcher {
174 pub fn new() -> Self {
176 Self {
177 backends: Vec::new(),
178 backend_stats: HashMap::new(),
179 dispatcher_stats: DispatcherStats::default(),
180 }
181 }
182
183 pub fn register_backend(&mut self, registration: BackendRegistration) {
187 self.backend_stats
188 .entry(registration.kind)
189 .or_insert_with(|| BackendStats::new(registration.kind));
190 self.backends.push(registration);
191 self.backends.sort_by_key(|b| std::cmp::Reverse(b.priority));
193 }
194
195 pub fn dispatch(&mut self, op: DispatchOp) -> Option<DispatchResult> {
201 let mut found_unavailable_capable = false;
204 let mut selected: Option<BackendKind> = None;
205
206 for backend in &self.backends {
207 if !backend.supports_op(&op) {
208 continue;
209 }
210 if !backend.is_available {
211 found_unavailable_capable = true;
213 continue;
214 }
215 selected = Some(backend.kind);
217 break;
218 }
219
220 match selected {
221 None => {
222 self.dispatcher_stats.total_failed += 1;
223 None
224 }
225 Some(kind) => {
226 let fallback_used = found_unavailable_capable;
227
228 self.dispatcher_stats.total_dispatched += 1;
230 if fallback_used {
231 self.dispatcher_stats.total_fallbacks += 1;
232 }
233
234 if let Some(stats) = self.backend_stats.get_mut(&kind) {
236 stats.total_dispatched += 1;
237 if fallback_used {
238 stats.total_fallback_selected += 1;
239 }
240 }
241
242 Some(DispatchResult {
243 op,
244 selected_backend: kind,
245 fallback_used,
246 })
247 }
248 }
249 }
250
251 pub fn set_backend_available(&mut self, kind: BackendKind, available: bool) {
254 for backend in &mut self.backends {
255 if backend.kind == kind {
256 backend.is_available = available;
257 }
258 }
259 }
260
261 pub fn stats(&self) -> &DispatcherStats {
263 &self.dispatcher_stats
264 }
265
266 pub fn backend_stats(&self, kind: BackendKind) -> Option<&BackendStats> {
269 self.backend_stats.get(&kind)
270 }
271}
272
273impl Default for TensorOpDispatcher {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279#[cfg(test)]
284mod tests {
285 use super::*;
286
287 fn cpu_backend(priority: u32) -> BackendRegistration {
292 BackendRegistration {
293 kind: BackendKind::Cpu,
294 priority,
295 supported_ops: vec![
296 "add".to_string(),
297 "mul".to_string(),
298 "sum".to_string(),
299 "mean".to_string(),
300 "conv".to_string(),
301 "relu".to_string(),
302 ],
303 is_available: true,
304 }
305 }
306
307 fn gpu_backend(priority: u32) -> BackendRegistration {
308 BackendRegistration {
309 kind: BackendKind::Gpu,
310 priority,
311 supported_ops: vec![
312 "matmul".to_string(),
313 "add".to_string(),
314 "mul".to_string(),
315 "sum".to_string(),
316 "conv".to_string(),
317 "relu".to_string(),
318 ],
319 is_available: true,
320 }
321 }
322
323 fn remote_backend(priority: u32) -> BackendRegistration {
324 BackendRegistration {
325 kind: BackendKind::Remote,
326 priority,
327 supported_ops: vec!["matmul".to_string(), "sum".to_string()],
328 is_available: true,
329 }
330 }
331
332 fn sim_backend(priority: u32) -> BackendRegistration {
333 BackendRegistration {
334 kind: BackendKind::Simulated,
335 priority,
336 supported_ops: vec![
337 "add".to_string(),
338 "mul".to_string(),
339 "sum".to_string(),
340 "conv".to_string(),
341 ],
342 is_available: true,
343 }
344 }
345
346 fn matmul_op() -> DispatchOp {
347 DispatchOp::MatMul { m: 4, n: 4, k: 4 }
348 }
349
350 fn ew_op(name: &str) -> DispatchOp {
351 DispatchOp::ElementWise {
352 op_name: name.to_string(),
353 element_count: 1024,
354 }
355 }
356
357 fn red_op(name: &str) -> DispatchOp {
358 DispatchOp::Reduction {
359 op_name: name.to_string(),
360 dims: vec![0, 1],
361 }
362 }
363
364 fn conv_op() -> DispatchOp {
365 DispatchOp::Convolution {
366 kernel_size: 3,
367 channels: 64,
368 }
369 }
370
371 #[test]
376 fn test_register_maintains_priority_order_ascending_insert() {
377 let mut d = TensorOpDispatcher::new();
378 d.register_backend(cpu_backend(10));
379 d.register_backend(gpu_backend(50));
380 d.register_backend(remote_backend(30));
381
382 assert_eq!(d.backends[0].priority, 50);
383 assert_eq!(d.backends[1].priority, 30);
384 assert_eq!(d.backends[2].priority, 10);
385 }
386
387 #[test]
388 fn test_register_maintains_priority_order_descending_insert() {
389 let mut d = TensorOpDispatcher::new();
390 d.register_backend(gpu_backend(100));
391 d.register_backend(cpu_backend(10));
392
393 assert_eq!(d.backends[0].kind, BackendKind::Gpu);
394 assert_eq!(d.backends[1].kind, BackendKind::Cpu);
395 }
396
397 #[test]
398 fn test_register_creates_stats_entry() {
399 let mut d = TensorOpDispatcher::new();
400 d.register_backend(cpu_backend(10));
401 assert!(d.backend_stats(BackendKind::Cpu).is_some());
402 }
403
404 #[test]
405 fn test_register_same_kind_twice_does_not_duplicate_stats_entry() {
406 let mut d = TensorOpDispatcher::new();
407 d.register_backend(cpu_backend(10));
408 d.register_backend(cpu_backend(5)); assert_eq!(
411 d.backend_stats(BackendKind::Cpu)
412 .expect("test: should succeed")
413 .total_dispatched,
414 0
415 );
416 assert_eq!(d.backends.len(), 2);
418 }
419
420 #[test]
425 fn test_dispatch_selects_highest_priority() {
426 let mut d = TensorOpDispatcher::new();
427 d.register_backend(cpu_backend(10));
428 d.register_backend(gpu_backend(50)); let result = d.dispatch(matmul_op()).expect("should dispatch");
431 assert_eq!(result.selected_backend, BackendKind::Gpu);
432 assert!(!result.fallback_used);
433 }
434
435 #[test]
436 fn test_dispatch_selects_only_capable_backend() {
437 let mut d = TensorOpDispatcher::new();
438 d.register_backend(BackendRegistration {
440 kind: BackendKind::Gpu,
441 priority: 100,
442 supported_ops: vec!["matmul".to_string()],
443 is_available: true,
444 });
445 d.register_backend(cpu_backend(10));
446
447 let result = d.dispatch(ew_op("add")).expect("should dispatch");
448 assert_eq!(result.selected_backend, BackendKind::Cpu);
449 assert!(!result.fallback_used);
452 }
453
454 #[test]
459 fn test_dispatch_fallback_when_primary_unavailable() {
460 let mut d = TensorOpDispatcher::new();
461 let mut gpu = gpu_backend(100);
462 gpu.is_available = false;
463 d.register_backend(gpu);
464 d.register_backend(cpu_backend(10));
465
466 let result = d.dispatch(matmul_op()).expect("should dispatch");
467 assert_eq!(result.selected_backend, BackendKind::Cpu);
468 assert!(result.fallback_used);
469 }
470
471 #[test]
476 fn test_fallback_used_false_when_primary_available() {
477 let mut d = TensorOpDispatcher::new();
478 d.register_backend(gpu_backend(100));
479 d.register_backend(cpu_backend(10));
480
481 let result = d.dispatch(matmul_op()).expect("should dispatch");
482 assert!(!result.fallback_used);
483 }
484
485 #[test]
486 fn test_fallback_used_true_when_skipping_unavailable_capable() {
487 let mut d = TensorOpDispatcher::new();
488 let mut gpu = gpu_backend(100);
489 gpu.is_available = false;
490 d.register_backend(gpu);
491 d.register_backend(cpu_backend(10));
492
493 let result = d.dispatch(matmul_op()).expect("should dispatch");
494 assert!(result.fallback_used);
495 }
496
497 #[test]
498 fn test_fallback_used_false_when_incapable_backend_skipped() {
499 let mut d = TensorOpDispatcher::new();
502 d.register_backend(BackendRegistration {
503 kind: BackendKind::Gpu,
504 priority: 200,
505 supported_ops: vec!["matmul".to_string()],
506 is_available: true,
507 });
508 d.register_backend(cpu_backend(10));
509
510 let result = d.dispatch(ew_op("add")).expect("should dispatch");
511 assert_eq!(result.selected_backend, BackendKind::Cpu);
512 assert!(!result.fallback_used);
513 }
514
515 #[test]
520 fn test_dispatch_none_when_no_backends() {
521 let mut d = TensorOpDispatcher::new();
522 assert!(d.dispatch(matmul_op()).is_none());
523 }
524
525 #[test]
526 fn test_dispatch_none_when_all_unavailable() {
527 let mut d = TensorOpDispatcher::new();
528 let mut gpu = gpu_backend(100);
529 gpu.is_available = false;
530 let mut cpu = cpu_backend(10);
531 cpu.is_available = false;
532 d.register_backend(gpu);
533 d.register_backend(cpu);
534
535 assert!(d.dispatch(matmul_op()).is_none());
536 }
537
538 #[test]
539 fn test_dispatch_none_when_op_unsupported_by_all() {
540 let mut d = TensorOpDispatcher::new();
541 d.register_backend(BackendRegistration {
542 kind: BackendKind::Gpu,
543 priority: 50,
544 supported_ops: vec!["matmul".to_string()],
545 is_available: true,
546 });
547 assert!(d.dispatch(ew_op("sigmoid")).is_none());
549 }
550
551 #[test]
556 fn test_set_backend_available_disables() {
557 let mut d = TensorOpDispatcher::new();
558 d.register_backend(gpu_backend(100));
559 d.register_backend(cpu_backend(10));
560
561 d.set_backend_available(BackendKind::Gpu, false);
562 let result = d.dispatch(matmul_op()).expect("CPU should be fallback");
563 assert_eq!(result.selected_backend, BackendKind::Cpu);
564 assert!(result.fallback_used);
565 }
566
567 #[test]
568 fn test_set_backend_available_re_enables() {
569 let mut d = TensorOpDispatcher::new();
570 let mut gpu = gpu_backend(100);
571 gpu.is_available = false;
572 d.register_backend(gpu);
573 d.register_backend(cpu_backend(10));
574
575 let r1 = d.dispatch(matmul_op()).expect("should dispatch");
577 assert_eq!(r1.selected_backend, BackendKind::Cpu);
578
579 d.set_backend_available(BackendKind::Gpu, true);
581 let r2 = d.dispatch(matmul_op()).expect("should dispatch");
582 assert_eq!(r2.selected_backend, BackendKind::Gpu);
583 assert!(!r2.fallback_used);
584 }
585
586 #[test]
591 fn test_matmul_always_supported_on_cpu() {
592 let reg = cpu_backend(10);
593 let minimal_cpu = BackendRegistration {
595 kind: BackendKind::Cpu,
596 priority: 10,
597 supported_ops: vec![],
598 is_available: true,
599 };
600 assert!(minimal_cpu.supports_op(&matmul_op()));
601 assert!(reg.supports_op(&matmul_op()));
602 }
603
604 #[test]
605 fn test_matmul_always_supported_on_simulated() {
606 let minimal_sim = BackendRegistration {
607 kind: BackendKind::Simulated,
608 priority: 1,
609 supported_ops: vec![],
610 is_available: true,
611 };
612 assert!(minimal_sim.supports_op(&matmul_op()));
613 }
614
615 #[test]
616 fn test_matmul_requires_capability_on_gpu() {
617 let gpu_no_mm = BackendRegistration {
618 kind: BackendKind::Gpu,
619 priority: 10,
620 supported_ops: vec!["add".to_string()],
621 is_available: true,
622 };
623 assert!(!gpu_no_mm.supports_op(&matmul_op()));
624
625 let gpu_with_mm = BackendRegistration {
626 kind: BackendKind::Gpu,
627 priority: 10,
628 supported_ops: vec!["matmul".to_string()],
629 is_available: true,
630 };
631 assert!(gpu_with_mm.supports_op(&matmul_op()));
632 }
633
634 #[test]
635 fn test_matmul_requires_capability_on_remote() {
636 let remote_no_mm = BackendRegistration {
637 kind: BackendKind::Remote,
638 priority: 10,
639 supported_ops: vec!["sum".to_string()],
640 is_available: true,
641 };
642 assert!(!remote_no_mm.supports_op(&matmul_op()));
643 }
644
645 #[test]
650 fn test_elementwise_matches_op_name() {
651 let cpu = cpu_backend(10); assert!(cpu.supports_op(&ew_op("add")));
653 assert!(cpu.supports_op(&ew_op("mul")));
654 assert!(cpu.supports_op(&ew_op("relu")));
655 assert!(!cpu.supports_op(&ew_op("sigmoid")));
656 }
657
658 #[test]
659 fn test_dispatch_elementwise_correct_backend() {
660 let mut d = TensorOpDispatcher::new();
661 d.register_backend(BackendRegistration {
663 kind: BackendKind::Gpu,
664 priority: 100,
665 supported_ops: vec!["matmul".to_string(), "mul".to_string()],
666 is_available: true,
667 });
668 d.register_backend(cpu_backend(10)); let r = d.dispatch(ew_op("relu")).expect("should dispatch");
671 assert_eq!(r.selected_backend, BackendKind::Cpu);
672 assert!(!r.fallback_used);
674 }
675
676 #[test]
681 fn test_convolution_matches_conv_keyword() {
682 let backend = BackendRegistration {
683 kind: BackendKind::Gpu,
684 priority: 10,
685 supported_ops: vec!["conv".to_string(), "matmul".to_string()],
686 is_available: true,
687 };
688 assert!(backend.supports_op(&conv_op()));
689 }
690
691 #[test]
692 fn test_convolution_fails_without_conv_keyword() {
693 let backend = BackendRegistration {
694 kind: BackendKind::Gpu,
695 priority: 10,
696 supported_ops: vec!["matmul".to_string()],
697 is_available: true,
698 };
699 assert!(!backend.supports_op(&conv_op()));
700 }
701
702 #[test]
703 fn test_dispatch_convolution_selects_correct_backend() {
704 let mut d = TensorOpDispatcher::new();
705 d.register_backend(BackendRegistration {
706 kind: BackendKind::Remote,
707 priority: 200,
708 supported_ops: vec!["matmul".to_string()], is_available: true,
710 });
711 d.register_backend(gpu_backend(100)); let r = d.dispatch(conv_op()).expect("GPU should handle conv");
714 assert_eq!(r.selected_backend, BackendKind::Gpu);
715 assert!(!r.fallback_used);
716 }
717
718 #[test]
723 fn test_reduction_matches_op_name() {
724 let cpu = cpu_backend(10);
725 assert!(cpu.supports_op(&red_op("sum")));
726 assert!(cpu.supports_op(&red_op("mean")));
727 assert!(!cpu.supports_op(&red_op("prod")));
728 }
729
730 #[test]
735 fn test_dispatcher_stats_total_dispatched() {
736 let mut d = TensorOpDispatcher::new();
737 d.register_backend(cpu_backend(10));
738 d.dispatch(matmul_op());
739 d.dispatch(matmul_op());
740 assert_eq!(d.stats().total_dispatched, 2);
741 }
742
743 #[test]
744 fn test_dispatcher_stats_total_failed() {
745 let mut d = TensorOpDispatcher::new();
746 d.dispatch(matmul_op()); d.dispatch(ew_op("nonexistent"));
748 assert_eq!(d.stats().total_failed, 2);
749 }
750
751 #[test]
752 fn test_dispatcher_stats_total_fallbacks() {
753 let mut d = TensorOpDispatcher::new();
754 let mut gpu = gpu_backend(100);
755 gpu.is_available = false;
756 d.register_backend(gpu);
757 d.register_backend(cpu_backend(10));
758
759 d.dispatch(matmul_op()); d.dispatch(matmul_op()); assert_eq!(d.stats().total_fallbacks, 2);
762 }
763
764 #[test]
765 fn test_dispatcher_stats_mixed_outcomes() {
766 let mut d = TensorOpDispatcher::new();
767 d.register_backend(gpu_backend(100));
768 d.register_backend(cpu_backend(10));
769
770 d.dispatch(matmul_op()); d.set_backend_available(BackendKind::Gpu, false);
772 d.dispatch(matmul_op()); d.dispatch(ew_op("sigmoid")); assert_eq!(d.stats().total_dispatched, 2);
776 assert_eq!(d.stats().total_fallbacks, 1);
777 assert_eq!(d.stats().total_failed, 1);
778 }
779
780 #[test]
785 fn test_backend_stats_dispatched_count() {
786 let mut d = TensorOpDispatcher::new();
787 d.register_backend(cpu_backend(10));
788
789 d.dispatch(matmul_op());
790 d.dispatch(ew_op("add"));
791 let stats = d.backend_stats(BackendKind::Cpu).expect("stats exist");
792 assert_eq!(stats.total_dispatched, 2);
793 }
794
795 #[test]
796 fn test_backend_stats_fallback_count() {
797 let mut d = TensorOpDispatcher::new();
798 let mut gpu = gpu_backend(100);
799 gpu.is_available = false;
800 d.register_backend(gpu);
801 d.register_backend(cpu_backend(10));
802
803 d.dispatch(matmul_op()); d.dispatch(matmul_op()); let cpu_stats = d.backend_stats(BackendKind::Cpu).expect("CPU stats");
807 assert_eq!(cpu_stats.total_dispatched, 2);
808 assert_eq!(cpu_stats.total_fallback_selected, 2);
809
810 let gpu_stats = d.backend_stats(BackendKind::Gpu).expect("GPU stats");
812 assert_eq!(gpu_stats.total_dispatched, 0);
813 }
814
815 #[test]
816 fn test_backend_stats_none_for_unregistered() {
817 let d = TensorOpDispatcher::new();
818 assert!(d.backend_stats(BackendKind::Remote).is_none());
819 }
820
821 #[test]
822 fn test_backend_stats_independent_per_backend() {
823 let mut d = TensorOpDispatcher::new();
824 d.register_backend(gpu_backend(100));
825 d.register_backend(cpu_backend(10));
826
827 d.dispatch(matmul_op());
829 d.set_backend_available(BackendKind::Gpu, false);
831 d.dispatch(matmul_op());
832
833 let gpu_stats = d
834 .backend_stats(BackendKind::Gpu)
835 .expect("test: should succeed");
836 assert_eq!(gpu_stats.total_dispatched, 1);
837 assert_eq!(gpu_stats.total_fallback_selected, 0);
838
839 let cpu_stats = d
840 .backend_stats(BackendKind::Cpu)
841 .expect("test: should succeed");
842 assert_eq!(cpu_stats.total_dispatched, 1);
843 assert_eq!(cpu_stats.total_fallback_selected, 1);
844 }
845
846 #[test]
851 fn test_simulated_backend_matmul_always_supported() {
852 let mut d = TensorOpDispatcher::new();
853 d.register_backend(sim_backend(5));
854
855 let r = d
856 .dispatch(matmul_op())
857 .expect("simulated should handle matmul");
858 assert_eq!(r.selected_backend, BackendKind::Simulated);
859 assert!(!r.fallback_used);
860 }
861
862 #[test]
863 fn test_simulated_backend_elementwise_by_op_name() {
864 let mut d = TensorOpDispatcher::new();
865 d.register_backend(sim_backend(5)); let r = d
868 .dispatch(ew_op("add"))
869 .expect("simulated should handle add");
870 assert_eq!(r.selected_backend, BackendKind::Simulated);
871
872 assert!(d.dispatch(ew_op("relu")).is_none()); }
874
875 #[test]
880 fn test_dispatch_result_carries_op() {
881 let mut d = TensorOpDispatcher::new();
882 d.register_backend(cpu_backend(10));
883
884 let op = DispatchOp::MatMul { m: 8, n: 16, k: 32 };
885 let r = d.dispatch(op.clone()).expect("should dispatch");
886 assert_eq!(r.op, op);
887 }
888
889 #[test]
894 fn test_multiple_registrations_same_kind_priority_wins() {
895 let mut d = TensorOpDispatcher::new();
896 d.register_backend(cpu_backend(20)); d.register_backend(cpu_backend(10)); let r = d.dispatch(matmul_op()).expect("should dispatch");
901 assert_eq!(r.selected_backend, BackendKind::Cpu);
902 assert!(!r.fallback_used);
903 assert_eq!(
905 d.backend_stats(BackendKind::Cpu)
906 .expect("test: should succeed")
907 .total_dispatched,
908 1
909 );
910 }
911}