Skip to main content

nxpu_backend_intel/
lib.rs

1//! Intel NPU backend for NxPU.
2//!
3//! Emits OpenVINO IR v11 format (`model.xml` + `model.bin`) alongside the
4//! standard ONNX fallback. Validates operator patterns against the Intel NPU
5//! support matrix and classifies entry points for vendor-aware diagnostics.
6
7use 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/// Intel NPU backend with OpenVINO IR emission.
21#[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        // 1. Classify entry points and collect op names for validation.
43        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        // 2. Validate against Intel NPU support matrix.
61        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        // 3. Emit OpenVINO IR XML.
66        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        // 4. Also emit ONNX as fallback.
79        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        // Should have model.xml, model.bin, and output.onnx
141        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        // Verify XML contains expected elements
147        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        // model.bin should be empty binary placeholder
157        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        // Explicit(F16) with preferred Int8 => F16
262        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        // Keep with preferred Int8 => F32
272        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}