1use 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#[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 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}