Skip to main content

nxpu_backend_qualcomm/
lib.rs

1//! Qualcomm Hexagon NPU backend for NxPU.
2//!
3//! Delegates compilation to the ONNX backend with Qualcomm QNN SDK-specific
4//! validation and Hexagon NPU compilation 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::HexagonNpuSupport;
17
18/// Qualcomm Hexagon NPU backend with QNN SDK hints.
19#[derive(Debug)]
20pub struct QualcommBackend;
21
22impl Backend for QualcommBackend {
23    fn name(&self) -> &str {
24        "Qualcomm Hexagon NPU"
25    }
26
27    fn targets(&self) -> &[&str] {
28        &["qualcomm", "hexagon-npu"]
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(&HexagonNpuSupport, &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 convert for QNN: qnn-onnx-converter --input_network output.onnx".into(),
63        });
64        diagnostics.push(Diagnostic {
65            level: DiagnosticLevel::Info,
66            message: "To compile for HTP: qnn-net-run --model model.so --backend libQnnHtp.so"
67                .into(),
68        });
69        diagnostics.push(Diagnostic {
70            level: DiagnosticLevel::Info,
71            message:
72                "For Int8 quantization: qnn-onnx-converter --input_network output.onnx --input_list calibration.txt"
73                    .into(),
74        });
75
76        output.diagnostics = diagnostics;
77        Ok(output)
78    }
79}
80
81fn resolve_precision(opts: &BackendOptions, preferred: Precision) -> Precision {
82    match opts.precision {
83        PrecisionPolicy::Explicit(p) => p,
84        PrecisionPolicy::Auto => preferred,
85        PrecisionPolicy::Keep => Precision::F32,
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use nxpu_backend_core::{BackendOptions, OutputContent};
93
94    #[test]
95    fn backend_metadata() {
96        let backend = QualcommBackend;
97        assert_eq!(backend.name(), "Qualcomm Hexagon NPU");
98        assert!(backend.targets().contains(&"qualcomm"));
99        assert!(backend.targets().contains(&"hexagon-npu"));
100        assert_eq!(backend.preferred_precision(), Precision::Int8);
101    }
102
103    #[test]
104    fn compile_matmul_with_qnn_hints() {
105        let source = std::fs::read_to_string(concat!(
106            env!("CARGO_MANIFEST_DIR"),
107            "/../../examples/matmul.wgsl"
108        ))
109        .unwrap();
110        let module = nxpu_parser::parse(&source).unwrap();
111
112        let output = QualcommBackend
113            .compile(&module, &BackendOptions::default())
114            .unwrap();
115        assert_eq!(output.files.len(), 1);
116        assert_eq!(output.files[0].name, "output.onnx");
117        assert!(matches!(output.files[0].content, OutputContent::Binary(_)));
118
119        let messages: Vec<&str> = output
120            .diagnostics
121            .iter()
122            .map(|d| d.message.as_str())
123            .collect();
124        assert!(messages.iter().any(|m| m.contains("qnn-onnx-converter")));
125    }
126
127    fn load_and_compile(example: &str, opts: &BackendOptions) -> BackendOutput {
128        let source = std::fs::read_to_string(format!(
129            "{}/../../examples/{example}.wgsl",
130            env!("CARGO_MANIFEST_DIR")
131        ))
132        .unwrap();
133        let module = nxpu_parser::parse(&source).unwrap();
134        QualcommBackend.compile(&module, opts).unwrap()
135    }
136
137    #[test]
138    fn compile_conv2d() {
139        let output = load_and_compile("conv2d", &BackendOptions::default());
140        assert_ne!(output.files.len(), 0);
141        for file in &output.files {
142            assert_ne!(file.content.len(), 0);
143        }
144    }
145
146    #[test]
147    fn compile_relu() {
148        let output = load_and_compile("relu", &BackendOptions::default());
149        assert_ne!(output.files.len(), 0);
150        for file in &output.files {
151            assert_ne!(file.content.len(), 0);
152        }
153    }
154
155    #[test]
156    fn compile_attention() {
157        let output = load_and_compile("attention", &BackendOptions::default());
158        assert_ne!(output.files.len(), 0);
159        for file in &output.files {
160            assert_ne!(file.content.len(), 0);
161        }
162    }
163
164    #[test]
165    fn resolve_precision_explicit_and_keep() {
166        let explicit_opts = BackendOptions {
167            precision: PrecisionPolicy::Explicit(Precision::F16),
168            ..BackendOptions::default()
169        };
170        assert_eq!(
171            resolve_precision(&explicit_opts, Precision::Int8),
172            Precision::F16
173        );
174
175        let keep_opts = BackendOptions {
176            precision: PrecisionPolicy::Keep,
177            ..BackendOptions::default()
178        };
179        assert_eq!(
180            resolve_precision(&keep_opts, Precision::Int8),
181            Precision::F32
182        );
183    }
184
185    #[test]
186    fn all_qnn_diagnostics() {
187        let output = load_and_compile("matmul", &BackendOptions::default());
188        let messages: Vec<&str> = output
189            .diagnostics
190            .iter()
191            .map(|d| d.message.as_str())
192            .collect();
193        assert!(messages.iter().any(|m| m.contains("qnn-onnx-converter")));
194        assert!(messages.iter().any(|m| m.contains("qnn-net-run")));
195        assert!(messages.iter().any(|m| m.contains("Int8 quantization")));
196    }
197}