nxpu_backend_arm_ethos/
lib.rs1use std::process::Command;
12
13use nxpu_analysis::analyze;
14use nxpu_backend_core::{
15 Backend, BackendError, BackendOptions, BackendOutput, Diagnostic, DiagnosticLevel,
16 OutputContent, OutputFile, Precision, PrecisionPolicy, validate_patterns,
17};
18use nxpu_backend_tflite::TfLiteBackend;
19use nxpu_ir::Module;
20
21pub mod support;
22
23use support::EthosU55Support;
24
25#[derive(Debug)]
34pub struct ArmEthosBackend;
35
36fn vela_available() -> bool {
38 Command::new("vela")
39 .arg("--version")
40 .output()
41 .is_ok_and(|o| o.status.success())
42}
43
44fn run_vela(tflite_bytes: &[u8]) -> Result<Vec<u8>, BackendError> {
49 let temp_dir = std::env::temp_dir().join(format!("nxpu-vela-{}", std::process::id()));
50 std::fs::create_dir_all(&temp_dir)
51 .map_err(|e| BackendError::Other(format!("failed to create temp dir: {e}")))?;
52
53 let result = (|| {
55 let input_name = "input";
56 let input_path = temp_dir.join(format!("{input_name}.tflite"));
57 std::fs::write(&input_path, tflite_bytes)
58 .map_err(|e| BackendError::Other(format!("failed to write temp tflite: {e}")))?;
59
60 let output = Command::new("vela")
61 .arg(&input_path)
62 .arg("--output-dir")
63 .arg(&temp_dir)
64 .output()
65 .map_err(|e| BackendError::Other(format!("failed to run vela: {e}")))?;
66
67 if !output.status.success() {
68 let stderr = String::from_utf8_lossy(&output.stderr);
69 return Err(BackendError::Other(format!("vela failed: {stderr}")));
70 }
71
72 let vela_output = temp_dir.join(format!("{input_name}_vela.tflite"));
74 let optimized = std::fs::read(&vela_output)
75 .map_err(|e| BackendError::Other(format!("failed to read vela output: {e}")))?;
76
77 Ok(optimized)
78 })();
79
80 let _ = std::fs::remove_dir_all(&temp_dir);
82
83 result
84}
85
86impl Backend for ArmEthosBackend {
87 fn name(&self) -> &str {
88 "Arm Ethos NPU"
89 }
90
91 fn targets(&self) -> &[&str] {
92 &["arm-ethos", "ethos-u"]
93 }
94
95 fn preferred_precision(&self) -> Precision {
96 Precision::Int8
97 }
98
99 fn compile(
100 &self,
101 module: &Module,
102 opts: &BackendOptions,
103 ) -> Result<BackendOutput, BackendError> {
104 let mut op_names = Vec::new();
106 for (i, ep) in module.entry_points.iter().enumerate() {
107 match analyze::classify_entry_point(module, i) {
108 Ok(pattern) => {
109 op_names.extend(analyze::pattern_op_names(&pattern));
110 }
111 Err(e) => {
112 return Err(BackendError::Unsupported(format!(
113 "entry point '{}': {e}",
114 ep.name
115 )));
116 }
117 }
118 }
119
120 let precision = resolve_precision(opts, self.preferred_precision());
121 let op_refs: Vec<&str> = op_names.iter().map(|s| s.as_str()).collect();
122 let mut diagnostics = validate_patterns(&EthosU55Support, &op_refs, precision);
123
124 let tflite_output = TfLiteBackend.compile(module, opts)?;
126 diagnostics.extend(tflite_output.diagnostics);
127
128 let mut files = Vec::new();
129
130 let has_vela = vela_available();
132
133 for file in &tflite_output.files {
134 let tflite_bytes = match &file.content {
135 OutputContent::Binary(b) => b,
136 OutputContent::Text(_) => {
137 files.push(file.clone());
138 continue;
139 }
140 };
141
142 if has_vela {
144 match run_vela(tflite_bytes) {
145 Ok(optimized) => {
146 diagnostics.push(Diagnostic {
147 level: DiagnosticLevel::Info,
148 message: format!(
149 "Vela compilation successful ({} -> {} bytes)",
150 tflite_bytes.len(),
151 optimized.len()
152 ),
153 });
154 files.push(OutputFile {
155 name: file.name.replace(".tflite", "_vela.tflite"),
156 content: OutputContent::Binary(optimized),
157 });
158 files.push(file.clone());
160 }
161 Err(e) => {
162 diagnostics.push(Diagnostic {
163 level: DiagnosticLevel::Warning,
164 message: format!(
165 "Vela compilation failed, emitting unoptimized TFLite: {e}"
166 ),
167 });
168 files.push(file.clone());
169 }
170 }
171 } else {
172 diagnostics.push(Diagnostic {
174 level: DiagnosticLevel::Info,
175 message: "vela not found on PATH; emitting unoptimized TFLite. \
176 Install: pip install ethos-u-vela"
177 .into(),
178 });
179 diagnostics.push(Diagnostic {
180 level: DiagnosticLevel::Info,
181 message: format!(
182 "To optimize for Ethos-U55: vela {} --accelerator-config ethos-u55-128",
183 file.name
184 ),
185 });
186 diagnostics.push(Diagnostic {
187 level: DiagnosticLevel::Info,
188 message: format!(
189 "To optimize for Ethos-U65: vela {} --accelerator-config ethos-u65-512",
190 file.name
191 ),
192 });
193 files.push(file.clone());
194 }
195 }
196
197 diagnostics.push(Diagnostic {
199 level: DiagnosticLevel::Info,
200 message: "For Int8 quantization: use TFLite post-training quantization with \
201 representative dataset"
202 .into(),
203 });
204
205 Ok(BackendOutput { files, diagnostics })
206 }
207}
208
209fn resolve_precision(opts: &BackendOptions, preferred: Precision) -> Precision {
210 match opts.precision {
211 PrecisionPolicy::Explicit(p) => p,
212 PrecisionPolicy::Auto => preferred,
213 PrecisionPolicy::Keep => Precision::F32,
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use nxpu_backend_core::BackendOptions;
221
222 #[test]
223 fn backend_metadata() {
224 let backend = ArmEthosBackend;
225 assert_eq!(backend.name(), "Arm Ethos NPU");
226 assert!(backend.targets().contains(&"arm-ethos"));
227 assert!(backend.targets().contains(&"ethos-u"));
228 assert_eq!(backend.preferred_precision(), Precision::Int8);
229 }
230
231 #[test]
232 fn compile_produces_tflite_with_validation() {
233 let source = std::fs::read_to_string(concat!(
234 env!("CARGO_MANIFEST_DIR"),
235 "/../../examples/matmul.wgsl"
236 ))
237 .unwrap();
238 let module = nxpu_parser::parse(&source).unwrap();
239
240 let output = ArmEthosBackend
241 .compile(&module, &BackendOptions::default())
242 .unwrap();
243
244 assert_ne!(output.files.len(), 0);
246
247 let has_tflite = output.files.iter().any(|f| f.name.ends_with(".tflite"));
249 assert!(has_tflite);
250
251 assert_ne!(output.diagnostics.len(), 0);
253 let messages: Vec<&str> = output
254 .diagnostics
255 .iter()
256 .map(|d| d.message.as_str())
257 .collect();
258 assert!(
259 messages
260 .iter()
261 .any(|m| m.contains("ethos-u") || m.contains("Ethos"))
262 );
263 }
264
265 #[test]
266 fn vela_availability_check() {
267 let _available = vela_available();
270 }
271
272 fn load_and_compile(example: &str, opts: &BackendOptions) -> BackendOutput {
273 let source = std::fs::read_to_string(format!(
274 "{}/../../examples/{example}.wgsl",
275 env!("CARGO_MANIFEST_DIR")
276 ))
277 .unwrap();
278 let module = nxpu_parser::parse(&source).unwrap();
279 ArmEthosBackend.compile(&module, opts).unwrap()
280 }
281
282 #[test]
283 fn compile_conv2d() {
284 let output = load_and_compile("conv2d", &BackendOptions::default());
285 assert_ne!(output.files.len(), 0);
286 let has_tflite = output.files.iter().any(|f| f.name.ends_with(".tflite"));
287 assert!(has_tflite);
288 }
289
290 #[test]
291 fn compile_relu() {
292 let output = load_and_compile("relu", &BackendOptions::default());
293 assert_ne!(output.files.len(), 0);
294 }
295
296 #[test]
297 fn compile_attention() {
298 let output = load_and_compile("attention", &BackendOptions::default());
299 assert_ne!(output.files.len(), 0);
300 }
301
302 #[test]
303 fn resolve_precision_explicit_and_keep() {
304 let explicit_opts = BackendOptions {
305 precision: PrecisionPolicy::Explicit(Precision::F16),
306 ..BackendOptions::default()
307 };
308 assert_eq!(
309 resolve_precision(&explicit_opts, Precision::Int8),
310 Precision::F16
311 );
312
313 let keep_opts = BackendOptions {
314 precision: PrecisionPolicy::Keep,
315 ..BackendOptions::default()
316 };
317 assert_eq!(
318 resolve_precision(&keep_opts, Precision::Int8),
319 Precision::F32
320 );
321 }
322}