1#![warn(missing_docs)]
2use std::fmt::{self, Debug};
9
10use nxpu_ir::Module;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum Precision {
15 F32,
17 F16,
19 BF16,
21 Int8,
23}
24
25impl fmt::Display for Precision {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 f.write_str(match self {
28 Self::F32 => "F32",
29 Self::F16 => "F16",
30 Self::BF16 => "BF16",
31 Self::Int8 => "I8",
32 })
33 }
34}
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum PrecisionPolicy {
39 Keep,
41 Explicit(Precision),
43 #[default]
45 Auto,
46}
47
48impl fmt::Display for PrecisionPolicy {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 Self::Keep => f.write_str("Keep"),
52 Self::Explicit(p) => write!(f, "Explicit({p})"),
53 Self::Auto => f.write_str("Auto"),
54 }
55 }
56}
57
58pub trait Backend: Debug + Send + Sync {
60 fn name(&self) -> &str;
62
63 fn targets(&self) -> &[&str];
65
66 fn compile(
68 &self,
69 module: &Module,
70 opts: &BackendOptions,
71 ) -> Result<BackendOutput, BackendError>;
72
73 fn preferred_precision(&self) -> Precision {
75 Precision::F32
76 }
77}
78
79#[derive(Clone, Debug, Default)]
89pub struct BackendOptions {
90 pub opt_level: u8,
92 pub symbolic_extent: Option<u32>,
104 pub precision: PrecisionPolicy,
110 pub memory_plan: Option<MemoryPlan>,
116 pub quantization_params: Vec<QuantParam>,
122 pub per_channel_params: Vec<PerChannelParam>,
128 pub tiling_plans: Vec<TilingPlanInfo>,
133 pub vectorization_hints: Vec<VectorizationHintInfo>,
138 pub constant_tensors: Vec<ConstantTensor>,
153}
154
155#[derive(Clone, Debug)]
157pub struct ConstantTensor {
158 pub name: String,
160 pub data: Vec<u8>,
162}
163
164#[derive(Clone, Debug)]
166pub struct QuantParam {
167 pub name: String,
169 pub scale: f32,
171 pub zero_point: i32,
173}
174
175#[derive(Clone, Debug)]
177pub struct PerChannelParam {
178 pub name: String,
180 pub scales: Vec<f32>,
182 pub zero_points: Vec<i32>,
184 pub channel_axis: u32,
186}
187
188#[derive(Clone, Debug)]
190pub struct TilingPlanInfo {
191 pub op_name: String,
193 pub tiles: Vec<(String, u32)>,
195 pub reuse_factor: f64,
197}
198
199#[derive(Clone, Debug)]
201pub struct VectorizationHintInfo {
202 pub op_name: String,
204 pub dim_name: String,
206 pub lanes: u32,
208 pub is_reduction: bool,
210 pub is_contiguous: bool,
212}
213
214impl fmt::Display for BackendOptions {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 let mem = if self.memory_plan.is_some() {
217 ", memory_plan: yes"
218 } else {
219 ""
220 };
221 let quant = if self.quantization_params.is_empty() {
222 String::new()
223 } else {
224 format!(", quant_params: {}", self.quantization_params.len())
225 };
226 let per_ch = if self.per_channel_params.is_empty() {
227 String::new()
228 } else {
229 format!(", per_channel: {}", self.per_channel_params.len())
230 };
231 write!(
232 f,
233 "BackendOptions {{ opt_level: {}, precision: {}{}{}{} }}",
234 self.opt_level, self.precision, mem, quant, per_ch
235 )
236 }
237}
238
239#[derive(Clone, Debug)]
241pub struct BackendOutput {
242 pub files: Vec<OutputFile>,
244 pub diagnostics: Vec<Diagnostic>,
246}
247
248impl fmt::Display for BackendOutput {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 write!(
251 f,
252 "{} file(s), {} diagnostic(s)",
253 self.files.len(),
254 self.diagnostics.len()
255 )
256 }
257}
258
259#[derive(Clone, Debug)]
261pub struct OutputFile {
262 pub name: String,
264 pub content: OutputContent,
266}
267
268impl fmt::Display for OutputFile {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 f.write_str(&self.name)
271 }
272}
273
274#[derive(Clone, Debug)]
276pub enum OutputContent {
277 Text(String),
279 Binary(Vec<u8>),
281}
282
283impl OutputContent {
284 pub fn len(&self) -> usize {
293 match self {
294 Self::Text(s) => s.len(),
295 Self::Binary(b) => b.len(),
296 }
297 }
298
299 pub fn is_empty(&self) -> bool {
301 self.len() == 0
302 }
303}
304
305impl fmt::Display for OutputContent {
306 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307 match self {
308 Self::Text(s) => write!(f, "Text({} chars)", s.len()),
309 Self::Binary(b) => write!(f, "Binary({} bytes)", b.len()),
310 }
311 }
312}
313
314#[derive(Clone, Debug)]
316pub struct Diagnostic {
317 pub level: DiagnosticLevel,
319 pub message: String,
321}
322
323impl fmt::Display for Diagnostic {
324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325 write!(f, "[{}] {}", self.level, self.message)
326 }
327}
328
329#[derive(Clone, Copy, Debug, PartialEq, Eq)]
331pub enum DiagnosticLevel {
332 Warning,
334 Info,
336}
337
338impl fmt::Display for DiagnosticLevel {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 f.write_str(match self {
341 Self::Warning => "Warning",
342 Self::Info => "Info",
343 })
344 }
345}
346
347#[derive(Clone, Copy, Debug, PartialEq, Eq)]
349pub enum PerformanceTier {
350 Native,
352 Emulated,
354 Unsupported,
356}
357
358impl fmt::Display for PerformanceTier {
359 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360 f.write_str(match self {
361 Self::Native => "native",
362 Self::Emulated => "emulated",
363 Self::Unsupported => "unsupported",
364 })
365 }
366}
367
368pub trait OperatorSupport {
373 fn op_support(&self, op_name: &str, precision: Precision) -> PerformanceTier;
375
376 fn hardware_name(&self) -> &str;
378
379 fn native_ops(&self) -> &[&str];
381
382 fn emulated_ops(&self) -> &[&str];
384}
385
386pub fn validate_patterns(
390 support: &dyn OperatorSupport,
391 patterns: &[&str],
392 precision: Precision,
393) -> Vec<Diagnostic> {
394 let mut diagnostics = Vec::new();
395 for &op in patterns {
396 match support.op_support(op, precision) {
397 PerformanceTier::Native => {}
398 PerformanceTier::Emulated => {
399 diagnostics.push(Diagnostic {
400 level: DiagnosticLevel::Warning,
401 message: format!(
402 "{}: '{}' at {} will be emulated (may be slower)",
403 support.hardware_name(),
404 op,
405 precision,
406 ),
407 });
408 }
409 PerformanceTier::Unsupported => {
410 diagnostics.push(Diagnostic {
411 level: DiagnosticLevel::Warning,
412 message: format!(
413 "{}: '{}' at {} is unsupported",
414 support.hardware_name(),
415 op,
416 precision,
417 ),
418 });
419 }
420 }
421 }
422 diagnostics
423}
424
425#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
431pub struct TensorId(pub usize);
432
433impl fmt::Display for TensorId {
434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435 write!(f, "tensor_{}", self.0)
436 }
437}
438
439#[derive(Clone, Debug, PartialEq, Eq)]
441pub struct BufferAllocation {
442 pub tensor_id: TensorId,
444 pub offset: usize,
446 pub size_bytes: usize,
448}
449
450#[derive(Clone, Debug, Default, PartialEq, Eq)]
452pub struct MemoryPlan {
453 pub allocations: Vec<BufferAllocation>,
455 pub peak_bytes: usize,
457}
458
459impl fmt::Display for MemoryPlan {
460 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461 writeln!(f, "Memory Plan:")?;
462 writeln!(f, " Peak memory: {} bytes", self.peak_bytes)?;
463 writeln!(f, " Buffers: {}", self.allocations.len())?;
464
465 let total: usize = self.allocations.iter().map(|a| a.size_bytes).sum();
466 if total > 0 {
467 let ratio = 1.0 - (self.peak_bytes as f64 / total as f64);
468 writeln!(f, " Total tensor sizes: {total} bytes")?;
469 writeln!(f, " Reuse savings: {:.1}%", ratio * 100.0)?;
470 }
471
472 for alloc in &self.allocations {
473 writeln!(
474 f,
475 " {} -> offset: {}, size: {} bytes",
476 alloc.tensor_id, alloc.offset, alloc.size_bytes
477 )?;
478 }
479 Ok(())
480 }
481}
482
483#[derive(Debug, thiserror::Error)]
485pub enum BackendError {
486 #[error("unsupported: {0}")]
488 Unsupported(String),
489 #[error("{0}")]
491 Other(String),
492}
493
494pub struct BackendRegistry {
496 backends: Vec<Box<dyn Backend>>,
497}
498
499impl Default for BackendRegistry {
500 fn default() -> Self {
501 Self::new()
502 }
503}
504
505impl BackendRegistry {
506 pub fn new() -> Self {
508 Self {
509 backends: Vec::new(),
510 }
511 }
512
513 pub fn with_builtins() -> Self {
515 let mut reg = Self::new();
516 reg.register(Box::new(IrDumpBackend));
517 reg
518 }
519
520 pub fn register(&mut self, backend: Box<dyn Backend>) {
522 self.backends.push(backend);
523 }
524
525 pub fn find(&self, target: &str) -> Option<&dyn Backend> {
527 self.backends
528 .iter()
529 .find(|b| b.targets().contains(&target))
530 .map(|b| &**b)
531 }
532
533 pub fn list_targets(&self) -> Vec<&str> {
535 self.backends
536 .iter()
537 .flat_map(|b| b.targets().iter().copied())
538 .collect()
539 }
540}
541
542#[derive(Debug)]
544pub struct IrDumpBackend;
545
546impl Backend for IrDumpBackend {
547 fn name(&self) -> &str {
548 "IR Dump"
549 }
550
551 fn targets(&self) -> &[&str] {
552 &["ir-dump", "ir"]
553 }
554
555 fn compile(
556 &self,
557 module: &Module,
558 _opts: &BackendOptions,
559 ) -> Result<BackendOutput, BackendError> {
560 let text = nxpu_ir::dump_module(module);
561 Ok(BackendOutput {
562 files: vec![OutputFile {
563 name: "module.ir".into(),
564 content: OutputContent::Text(text),
565 }],
566 diagnostics: vec![],
567 })
568 }
569}
570
571#[cfg(test)]
572mod output_content_tests {
573 use super::*;
574
575 #[test]
576 fn text_length_is_utf8_bytes() {
577 assert_eq!(OutputContent::Text("hello".into()).len(), 5);
579 assert_eq!(OutputContent::Text("日本語".into()).len(), 9);
580 }
581
582 #[test]
583 fn binary_length_is_bytes() {
584 assert_eq!(OutputContent::Binary(vec![0u8; 7]).len(), 7);
585 }
586
587 #[test]
588 fn both_variants_report_empty() {
589 assert!(OutputContent::Text(String::new()).is_empty());
590 assert!(OutputContent::Binary(Vec::new()).is_empty());
591 assert!(!OutputContent::Binary(vec![1]).is_empty());
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598
599 #[test]
600 fn ir_dump_backend_targets() {
601 let backend = IrDumpBackend;
602 assert_eq!(backend.name(), "IR Dump");
603 assert!(backend.targets().contains(&"ir-dump"));
604 assert!(backend.targets().contains(&"ir"));
605 }
606
607 #[test]
608 fn ir_dump_backend_compile() {
609 let module = Module::default();
610 let opts = BackendOptions::default();
611 let output = IrDumpBackend.compile(&module, &opts).unwrap();
612 assert_eq!(output.files.len(), 1);
613 assert_eq!(output.files[0].name, "module.ir");
614 match &output.files[0].content {
615 OutputContent::Text(text) => assert!(text.contains("Types:")),
616 _ => panic!("expected text output"),
617 }
618 }
619
620 #[test]
621 fn registry_find_builtin() {
622 let reg = BackendRegistry::with_builtins();
623 assert!(reg.find("ir-dump").is_some());
624 assert!(reg.find("ir").is_some());
625 assert!(reg.find("nonexistent").is_none());
626 }
627
628 #[test]
629 fn registry_list_targets() {
630 let reg = BackendRegistry::with_builtins();
631 let targets = reg.list_targets();
632 assert!(targets.contains(&"ir-dump"));
633 assert!(targets.contains(&"ir"));
634 }
635
636 #[test]
637 fn registry_custom_backend() {
638 #[derive(Debug)]
639 struct TestBackend;
640 impl Backend for TestBackend {
641 fn name(&self) -> &str {
642 "test"
643 }
644 fn targets(&self) -> &[&str] {
645 &["test-target"]
646 }
647 fn compile(
648 &self,
649 _module: &Module,
650 _opts: &BackendOptions,
651 ) -> Result<BackendOutput, BackendError> {
652 Ok(BackendOutput {
653 files: vec![],
654 diagnostics: vec![],
655 })
656 }
657 }
658
659 let mut reg = BackendRegistry::new();
660 reg.register(Box::new(TestBackend));
661 assert!(reg.find("test-target").is_some());
662 }
663
664 #[test]
665 fn display_precision_all_variants() {
666 assert_eq!(format!("{}", Precision::F32), "F32");
667 assert_eq!(format!("{}", Precision::F16), "F16");
668 assert_eq!(format!("{}", Precision::BF16), "BF16");
669 assert_eq!(format!("{}", Precision::Int8), "I8");
670 }
671
672 #[test]
673 fn display_precision_policy_all_variants() {
674 assert_eq!(format!("{}", PrecisionPolicy::Keep), "Keep");
675 assert_eq!(format!("{}", PrecisionPolicy::Auto), "Auto");
676 assert_eq!(
677 format!("{}", PrecisionPolicy::Explicit(Precision::F16)),
678 "Explicit(F16)"
679 );
680 }
681
682 #[test]
683 fn display_backend_options() {
684 let opts = BackendOptions {
685 opt_level: 2,
686 precision: PrecisionPolicy::Explicit(Precision::Int8),
687 ..Default::default()
688 };
689 let s = format!("{opts}");
690 assert!(s.contains("opt_level: 2"));
691 assert!(s.contains("Explicit(I8)"));
692 }
693
694 #[test]
695 fn display_backend_output() {
696 let output = BackendOutput {
697 files: vec![
698 OutputFile {
699 name: "a.bin".into(),
700 content: OutputContent::Binary(vec![1, 2, 3]),
701 },
702 OutputFile {
703 name: "b.txt".into(),
704 content: OutputContent::Text("hello".into()),
705 },
706 ],
707 diagnostics: vec![Diagnostic {
708 level: DiagnosticLevel::Info,
709 message: "done".into(),
710 }],
711 };
712 assert_eq!(format!("{output}"), "2 file(s), 1 diagnostic(s)");
713 }
714
715 #[test]
716 fn display_output_file() {
717 let f = OutputFile {
718 name: "model.onnx".into(),
719 content: OutputContent::Binary(vec![]),
720 };
721 assert_eq!(format!("{f}"), "model.onnx");
722 }
723
724 #[test]
725 fn display_output_content_all_variants() {
726 assert_eq!(
727 format!("{}", OutputContent::Text("abc".into())),
728 "Text(3 chars)"
729 );
730 assert_eq!(
731 format!("{}", OutputContent::Binary(vec![0; 100])),
732 "Binary(100 bytes)"
733 );
734 }
735
736 #[test]
737 fn display_diagnostic_and_level() {
738 let warn = Diagnostic {
739 level: DiagnosticLevel::Warning,
740 message: "deprecated op".into(),
741 };
742 assert_eq!(format!("{warn}"), "[Warning] deprecated op");
743
744 let info = Diagnostic {
745 level: DiagnosticLevel::Info,
746 message: "classified as Add".into(),
747 };
748 assert_eq!(format!("{info}"), "[Info] classified as Add");
749 }
750
751 #[test]
752 fn display_diagnostic_level_all_variants() {
753 assert_eq!(format!("{}", DiagnosticLevel::Warning), "Warning");
754 assert_eq!(format!("{}", DiagnosticLevel::Info), "Info");
755 }
756
757 #[test]
758 fn registry_empty_list_targets() {
759 let reg = BackendRegistry::new();
760 assert_eq!(reg.list_targets().len(), 0);
761 }
762
763 #[test]
764 fn registry_default_is_empty() {
765 let reg = BackendRegistry::default();
766 assert_eq!(reg.list_targets().len(), 0);
767 }
768
769 #[test]
770 fn preferred_precision_default() {
771 let backend = IrDumpBackend;
772 assert_eq!(backend.preferred_precision(), Precision::F32);
773 }
774
775 #[test]
776 fn display_performance_tier() {
777 assert_eq!(format!("{}", PerformanceTier::Native), "native");
778 assert_eq!(format!("{}", PerformanceTier::Emulated), "emulated");
779 assert_eq!(format!("{}", PerformanceTier::Unsupported), "unsupported");
780 }
781
782 #[test]
783 fn validate_patterns_generates_diagnostics() {
784 struct TestSupport;
785 impl OperatorSupport for TestSupport {
786 fn op_support(&self, op_name: &str, _precision: Precision) -> PerformanceTier {
787 match op_name {
788 "MatMul" => PerformanceTier::Native,
789 "Conv" => PerformanceTier::Emulated,
790 _ => PerformanceTier::Unsupported,
791 }
792 }
793 fn hardware_name(&self) -> &str {
794 "TestNPU"
795 }
796 fn native_ops(&self) -> &[&str] {
797 &["MatMul"]
798 }
799 fn emulated_ops(&self) -> &[&str] {
800 &["Conv"]
801 }
802 }
803
804 let diags = validate_patterns(&TestSupport, &["MatMul", "Conv", "Softmax"], Precision::F16);
805 assert_eq!(diags.len(), 2);
806 assert!(diags[0].message.contains("emulated"));
807 assert!(diags[1].message.contains("unsupported"));
808 }
809
810 #[test]
811 fn backend_error_display() {
812 let e1 = BackendError::Unsupported("int64 tensors".into());
813 assert_eq!(format!("{e1}"), "unsupported: int64 tensors");
814
815 let e2 = BackendError::Other("internal failure".into());
816 assert_eq!(format!("{e2}"), "internal failure");
817 }
818
819 #[test]
822 fn tensor_id_display() {
823 assert_eq!(format!("{}", TensorId(0)), "tensor_0");
824 assert_eq!(format!("{}", TensorId(42)), "tensor_42");
825 }
826
827 #[test]
828 fn memory_plan_display() {
829 let plan = MemoryPlan {
830 allocations: vec![
831 BufferAllocation {
832 tensor_id: TensorId(0),
833 offset: 0,
834 size_bytes: 1024,
835 },
836 BufferAllocation {
837 tensor_id: TensorId(1),
838 offset: 0,
839 size_bytes: 512,
840 },
841 ],
842 peak_bytes: 1024,
843 };
844 let s = format!("{plan}");
845 assert!(s.contains("Peak memory: 1024 bytes"));
846 assert!(s.contains("Buffers: 2"));
847 assert!(s.contains("Reuse savings:"));
848 assert!(s.contains("tensor_0"));
849 assert!(s.contains("tensor_1"));
850 }
851
852 #[test]
853 fn memory_plan_default() {
854 let plan = MemoryPlan::default();
855 assert_eq!(plan.allocations.len(), 0);
856 assert_eq!(plan.peak_bytes, 0);
857 }
858
859 #[test]
860 fn display_backend_options_with_memory_plan() {
861 let opts = BackendOptions {
862 opt_level: 1,
863 precision: PrecisionPolicy::Auto,
864 memory_plan: Some(MemoryPlan {
865 allocations: vec![],
866 peak_bytes: 0,
867 }),
868 ..Default::default()
869 };
870 let s = format!("{opts}");
871 assert!(s.contains("memory_plan: yes"));
872 }
873
874 #[test]
875 fn display_backend_options_without_memory_plan() {
876 let opts = BackendOptions {
877 opt_level: 1,
878 precision: PrecisionPolicy::Auto,
879 memory_plan: None,
880 ..Default::default()
881 };
882 let s = format!("{opts}");
883 assert!(!s.contains("memory_plan"));
884 }
885
886 #[test]
887 fn per_channel_param_display() {
888 let opts = BackendOptions {
889 per_channel_params: vec![PerChannelParam {
890 name: "conv_weight".into(),
891 scales: vec![0.1, 0.2, 0.3],
892 zero_points: vec![0, 0, 0],
893 channel_axis: 0,
894 }],
895 ..Default::default()
896 };
897 let s = format!("{opts}");
898 assert!(s.contains("per_channel: 1"));
899 }
900
901 #[test]
902 fn per_channel_param_default_empty() {
903 let opts = BackendOptions::default();
904 assert_eq!(opts.per_channel_params.len(), 0);
905 let s = format!("{opts}");
906 assert!(!s.contains("per_channel"));
907 }
908}