Skip to main content

nxpu_backend_ceva/
lib.rs

1//! CEVA NeuPro NPU backend for NxPU.
2//!
3//! Delegates compilation to the ONNX backend with CEVA NeuPro-specific
4//! validation and CDNN compiler hints.
5
6use nxpu_analysis::analyze;
7use nxpu_backend_core::{
8    Backend, BackendError, BackendOptions, BackendOutput, Diagnostic, DiagnosticLevel, Precision,
9    PrecisionPolicy, validate_patterns,
10};
11use nxpu_backend_onnx::OnnxBackend;
12use nxpu_ir::Module;
13
14mod support;
15
16use support::CevaNeuProSupport;
17
18/// CEVA NeuPro NPU backend with CDNN compiler hints.
19#[derive(Debug)]
20pub struct CevaBackend;
21
22impl Backend for CevaBackend {
23    fn name(&self) -> &str {
24        "CEVA NeuPro NPU"
25    }
26
27    fn targets(&self) -> &[&str] {
28        &["ceva", "neupro"]
29    }
30
31    fn preferred_precision(&self) -> Precision {
32        Precision::Int8
33    }
34
35    fn compile(
36        &self,
37        module: &Module,
38        opts: &BackendOptions,
39    ) -> Result<BackendOutput, BackendError> {
40        let mut op_names = Vec::new();
41        for (i, ep) in module.entry_points.iter().enumerate() {
42            match analyze::classify_entry_point(module, i) {
43                Ok(pattern) => op_names.extend(analyze::pattern_op_names(&pattern)),
44                Err(e) => {
45                    return Err(BackendError::Unsupported(format!(
46                        "entry point '{}': {e}",
47                        ep.name
48                    )));
49                }
50            }
51        }
52
53        let precision = resolve_precision(opts, self.preferred_precision());
54        let op_refs: Vec<&str> = op_names.iter().map(|s| s.as_str()).collect();
55        let mut diagnostics = validate_patterns(&CevaNeuProSupport, &op_refs, precision);
56
57        let mut output = OnnxBackend.compile(module, opts)?;
58        diagnostics.extend(output.diagnostics);
59
60        diagnostics.push(Diagnostic {
61            level: DiagnosticLevel::Info,
62            message: "To compile for NeuPro: import output.onnx in CDNN compiler".into(),
63        });
64        diagnostics.push(Diagnostic {
65            level: DiagnosticLevel::Info,
66            message: "CDNN command: cdnn_cli --model output.onnx --target neupro-s".into(),
67        });
68        diagnostics.push(Diagnostic {
69            level: DiagnosticLevel::Info,
70            message:
71                "Note: CEVA NeuPro has a limited operator set. Unsupported ops will fall back to CPU."
72                    .into(),
73        });
74
75        output.diagnostics = diagnostics;
76        Ok(output)
77    }
78}
79
80fn resolve_precision(opts: &BackendOptions, preferred: Precision) -> Precision {
81    match opts.precision {
82        PrecisionPolicy::Explicit(p) => p,
83        PrecisionPolicy::Auto => preferred,
84        PrecisionPolicy::Keep => Precision::F32,
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use nxpu_backend_core::{BackendOptions, OutputContent};
92
93    #[test]
94    fn backend_metadata() {
95        let backend = CevaBackend;
96        assert_eq!(backend.name(), "CEVA NeuPro NPU");
97        assert!(backend.targets().contains(&"ceva"));
98        assert!(backend.targets().contains(&"neupro"));
99        assert_eq!(backend.preferred_precision(), Precision::Int8);
100    }
101
102    #[test]
103    fn compile_matmul_with_cdnn_hints() {
104        let source = std::fs::read_to_string(concat!(
105            env!("CARGO_MANIFEST_DIR"),
106            "/../../examples/matmul.wgsl"
107        ))
108        .unwrap();
109        let module = nxpu_parser::parse(&source).unwrap();
110
111        let output = CevaBackend
112            .compile(&module, &BackendOptions::default())
113            .unwrap();
114        assert_eq!(output.files.len(), 1);
115        assert_eq!(output.files[0].name, "output.onnx");
116        assert!(matches!(output.files[0].content, OutputContent::Binary(_)));
117
118        let messages: Vec<&str> = output
119            .diagnostics
120            .iter()
121            .map(|d| d.message.as_str())
122            .collect();
123        assert!(messages.iter().any(|m| m.contains("CDNN")));
124    }
125
126    #[test]
127    fn compile_matmul_validates_support() {
128        let source = std::fs::read_to_string(concat!(
129            env!("CARGO_MANIFEST_DIR"),
130            "/../../examples/matmul.wgsl"
131        ))
132        .unwrap();
133        let module = nxpu_parser::parse(&source).unwrap();
134
135        let output = CevaBackend
136            .compile(&module, &BackendOptions::default())
137            .unwrap();
138
139        // MatMul is emulated on CEVA, so we should get a warning
140        let has_emulated_warning = output
141            .diagnostics
142            .iter()
143            .any(|d| d.message.contains("emulated"));
144        assert!(has_emulated_warning);
145    }
146
147    fn load_and_compile(example: &str, opts: &BackendOptions) -> BackendOutput {
148        let source = std::fs::read_to_string(format!(
149            "{}/../../examples/{example}.wgsl",
150            env!("CARGO_MANIFEST_DIR")
151        ))
152        .unwrap();
153        let module = nxpu_parser::parse(&source).unwrap();
154        CevaBackend.compile(&module, opts).unwrap()
155    }
156
157    #[test]
158    fn compile_conv2d() {
159        let output = load_and_compile("conv2d", &BackendOptions::default());
160        assert_ne!(output.files.len(), 0);
161        for file in &output.files {
162            assert_ne!(file.content.len(), 0);
163        }
164    }
165
166    #[test]
167    fn compile_relu() {
168        let output = load_and_compile("relu", &BackendOptions::default());
169        assert_ne!(output.files.len(), 0);
170        for file in &output.files {
171            assert_ne!(file.content.len(), 0);
172        }
173    }
174
175    #[test]
176    fn compile_attention() {
177        let output = load_and_compile("attention", &BackendOptions::default());
178        assert_ne!(output.files.len(), 0);
179        for file in &output.files {
180            assert_ne!(file.content.len(), 0);
181        }
182    }
183
184    #[test]
185    fn resolve_precision_explicit_and_keep() {
186        let explicit_opts = BackendOptions {
187            precision: PrecisionPolicy::Explicit(Precision::F16),
188            ..BackendOptions::default()
189        };
190        assert_eq!(
191            resolve_precision(&explicit_opts, Precision::Int8),
192            Precision::F16
193        );
194
195        let keep_opts = BackendOptions {
196            precision: PrecisionPolicy::Keep,
197            ..BackendOptions::default()
198        };
199        assert_eq!(
200            resolve_precision(&keep_opts, Precision::Int8),
201            Precision::F32
202        );
203    }
204}