nxpu_backend_intel/
lib.rs1use nxpu_analysis::analyze;
8use nxpu_backend_core::{
9 Backend, BackendError, BackendOptions, BackendOutput, Diagnostic, DiagnosticLevel,
10 OutputContent, OutputFile, Precision, validate_patterns,
11};
12use nxpu_backend_onnx::OnnxBackend;
13use nxpu_ir::Module;
14
15mod openvino_ir;
16mod support;
17
18use support::IntelNpuSupport;
19
20#[derive(Debug)]
22pub struct IntelBackend;
23
24impl Backend for IntelBackend {
25 fn name(&self) -> &str {
26 "Intel NPU"
27 }
28
29 fn targets(&self) -> &[&str] {
30 &["intel-npu", "openvino"]
31 }
32
33 fn preferred_precision(&self) -> Precision {
34 Precision::F16
35 }
36
37 fn compile(
38 &self,
39 module: &Module,
40 opts: &BackendOptions,
41 ) -> Result<BackendOutput, BackendError> {
42 let mut patterns = Vec::new();
44 let mut op_names = Vec::new();
45 for (i, ep) in module.entry_points.iter().enumerate() {
46 match analyze::classify_entry_point(module, i) {
47 Ok(pattern) => {
48 op_names.extend(analyze::pattern_op_names(&pattern));
49 patterns.push(pattern);
50 }
51 Err(e) => {
52 return Err(BackendError::Unsupported(format!(
53 "entry point '{}': {e}",
54 ep.name
55 )));
56 }
57 }
58 }
59
60 let precision = resolve_precision(opts, self.preferred_precision());
62 let op_refs: Vec<&str> = op_names.iter().map(|s| s.as_str()).collect();
63 let mut diagnostics = validate_patterns(&IntelNpuSupport, &op_refs, precision);
64
65 let ir_xml = openvino_ir::build_ir_xml(&patterns, "nxpu_model");
67 let mut files = vec![
68 OutputFile {
69 name: "model.xml".into(),
70 content: OutputContent::Text(ir_xml),
71 },
72 OutputFile {
73 name: "model.bin".into(),
74 content: OutputContent::Binary(vec![]),
75 },
76 ];
77
78 match OnnxBackend.compile(module, opts) {
80 Ok(onnx_output) => {
81 files.extend(onnx_output.files);
82 diagnostics.extend(onnx_output.diagnostics);
83 }
84 Err(e) => {
85 diagnostics.push(Diagnostic {
86 level: DiagnosticLevel::Warning,
87 message: format!("ONNX fallback emission failed: {e}"),
88 });
89 }
90 }
91
92 diagnostics.push(Diagnostic {
93 level: DiagnosticLevel::Info,
94 message: "Load in OpenVINO: ov::Core::read_model(\"model.xml\")".into(),
95 });
96 diagnostics.push(Diagnostic {
97 level: DiagnosticLevel::Info,
98 message: "Alternative: ov::Core::read_model(\"output.onnx\")".into(),
99 });
100
101 Ok(BackendOutput { files, diagnostics })
102 }
103}
104
105fn resolve_precision(opts: &BackendOptions, preferred: Precision) -> Precision {
106 match opts.precision {
107 nxpu_backend_core::PrecisionPolicy::Explicit(p) => p,
108 nxpu_backend_core::PrecisionPolicy::Auto => preferred,
109 nxpu_backend_core::PrecisionPolicy::Keep => Precision::F32,
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use nxpu_backend_core::{BackendOptions, OutputContent};
117
118 #[test]
119 fn backend_metadata() {
120 let backend = IntelBackend;
121 assert_eq!(backend.name(), "Intel NPU");
122 assert!(backend.targets().contains(&"intel-npu"));
123 assert!(backend.targets().contains(&"openvino"));
124 assert_eq!(backend.preferred_precision(), Precision::F16);
125 }
126
127 #[test]
128 fn compile_matmul_emits_xml_and_onnx() {
129 let source = std::fs::read_to_string(concat!(
130 env!("CARGO_MANIFEST_DIR"),
131 "/../../examples/matmul.wgsl"
132 ))
133 .unwrap();
134 let module = nxpu_parser::parse(&source).unwrap();
135
136 let output = IntelBackend
137 .compile(&module, &BackendOptions::default())
138 .unwrap();
139
140 let names: Vec<&str> = output.files.iter().map(|f| f.name.as_str()).collect();
142 assert!(names.contains(&"model.xml"));
143 assert!(names.contains(&"model.bin"));
144 assert!(names.contains(&"output.onnx"));
145
146 let xml_file = output.files.iter().find(|f| f.name == "model.xml").unwrap();
148 let xml = match &xml_file.content {
149 OutputContent::Text(t) => t,
150 _ => panic!("expected text"),
151 };
152 assert!(xml.contains("<net"));
153 assert!(xml.contains("version=\"11\""));
154 assert!(xml.contains("type=\"MatMul\""));
155
156 let bin_file = output.files.iter().find(|f| f.name == "model.bin").unwrap();
158 assert!(matches!(&bin_file.content, OutputContent::Binary(b) if b.is_empty()));
159 }
160
161 #[test]
162 fn compile_vecadd_emits_ir() {
163 let source = std::fs::read_to_string(concat!(
164 env!("CARGO_MANIFEST_DIR"),
165 "/../../examples/vecadd.wgsl"
166 ))
167 .unwrap();
168 let module = nxpu_parser::parse(&source).unwrap();
169
170 let output = IntelBackend
171 .compile(&module, &BackendOptions::default())
172 .unwrap();
173 let xml_file = output.files.iter().find(|f| f.name == "model.xml").unwrap();
174 let xml = match &xml_file.content {
175 OutputContent::Text(t) => t,
176 _ => panic!("expected text"),
177 };
178 assert!(xml.contains("type=\"Add\""));
179 }
180
181 #[test]
182 fn diagnostics_include_openvino_hint() {
183 let source = std::fs::read_to_string(concat!(
184 env!("CARGO_MANIFEST_DIR"),
185 "/../../examples/matmul.wgsl"
186 ))
187 .unwrap();
188 let module = nxpu_parser::parse(&source).unwrap();
189
190 let output = IntelBackend
191 .compile(&module, &BackendOptions::default())
192 .unwrap();
193 let messages: Vec<&str> = output
194 .diagnostics
195 .iter()
196 .map(|d| d.message.as_str())
197 .collect();
198 assert!(messages.iter().any(|m| m.contains("read_model")));
199 }
200
201 fn load_and_compile(example: &str, opts: &BackendOptions) -> BackendOutput {
202 let source = std::fs::read_to_string(format!(
203 "{}/../../examples/{example}.wgsl",
204 env!("CARGO_MANIFEST_DIR")
205 ))
206 .unwrap();
207 let module = nxpu_parser::parse(&source).unwrap();
208 IntelBackend.compile(&module, opts).unwrap()
209 }
210
211 #[test]
212 fn compile_conv2d_emits_convolution() {
213 let output = load_and_compile("conv2d", &BackendOptions::default());
214 let xml_file = output.files.iter().find(|f| f.name == "model.xml").unwrap();
215 let xml = match &xml_file.content {
216 OutputContent::Text(t) => t,
217 _ => panic!("expected text"),
218 };
219 assert!(xml.contains("type=\"Convolution\""));
220 }
221
222 #[test]
223 fn compile_relu_emits_relu_layer() {
224 let output = load_and_compile("relu", &BackendOptions::default());
225 let xml_file = output.files.iter().find(|f| f.name == "model.xml").unwrap();
226 let xml = match &xml_file.content {
227 OutputContent::Text(t) => t,
228 _ => panic!("expected text"),
229 };
230 assert!(xml.contains("type=\"ReLU\""));
231 }
232
233 #[test]
234 fn compile_attention() {
235 let output = load_and_compile("attention", &BackendOptions::default());
236 assert_ne!(output.files.len(), 0);
237 }
238
239 #[test]
240 fn compile_maxpool() {
241 let output = load_and_compile("maxpool", &BackendOptions::default());
242 assert_ne!(output.files.len(), 0);
243 }
244
245 #[test]
246 fn compile_reduce_sum() {
247 let output = load_and_compile("reduce_sum", &BackendOptions::default());
248 assert_ne!(output.files.len(), 0);
249 }
250
251 #[test]
252 fn compile_batchnorm() {
253 let output = load_and_compile("batchnorm", &BackendOptions::default());
254 assert_ne!(output.files.len(), 0);
255 }
256
257 #[test]
258 fn resolve_precision_explicit_and_keep() {
259 use nxpu_backend_core::PrecisionPolicy;
260
261 let opts_explicit = BackendOptions {
263 precision: PrecisionPolicy::Explicit(Precision::F16),
264 ..Default::default()
265 };
266 assert_eq!(
267 resolve_precision(&opts_explicit, Precision::Int8),
268 Precision::F16
269 );
270
271 let opts_keep = BackendOptions {
273 precision: PrecisionPolicy::Keep,
274 ..Default::default()
275 };
276 assert_eq!(
277 resolve_precision(&opts_keep, Precision::Int8),
278 Precision::F32
279 );
280 }
281}