Qualcomm Linux · AI Hub · QNN · NPU
A complete prototype path for live monocular depth on Dragonwing IQ-8275: Ultralytics YOLO depth → AI Hub W8A16 quantization → QNN DLC → generated QNN context binary → persistent native C++ QNN runner → Python/OpenCV live camera UI.
This tutorial shows how to run a live USB-camera monocular depth demo on the Hexagon NPU of a Dragonwing IQ-8275 EVK using a model compiled by Qualcomm AI Hub and executed through a native C++ QNN app. The final demo is intentionally split into two parts:
That split keeps the NPU path realistic and persistent while still making the live demo easy to modify.
Target used for this prototype: Dragonwing IQ-8275 EVK running Ubuntu 24.04 on
aarch64, QCS8275/QCS8300-class platform, Hexagon V75, USB camera, and an attached display. The same pattern applies to other Dragonwing targets, but the AI Hub target, QAIRT version, and generated context binary must match your hardware/runtime.What you will build
By the end, the live path looks like this:Reference benchmark from the prototype
All measurements below usedimgsz=320.
Prerequisites
Hardware
- Dragonwing IQ-8275 EVK with HTP/NPU support. If your board is not set up yet, follow Set up the IQ-8275 EVK on Ubuntu.
- USB camera
- Display attached to the board for the live OpenCV window
- Network connectivity for package/model downloads and AI Hub job submission
Software on the board
Install the runtime, development headers, and Python packages:Step 1: Export YOLO depth to ONNX
Download/load the Ultralytics YOLO depth model and export a fixed320×320 ONNX model.
Clean duplicate ONNX metadata if necessary
Some exporters can place a graph output in bothgraph.output and graph.value_info. AI Hub can reject that with a duplicate-name error. This small sanitizer removes any duplicated value_info entries.
Step 2: Capture calibration images
The model input is NHWC float32 RGB, normalized to[0, 1], with Ultralytics-style square letterboxing to 320×320.
Create make_aihub_calib.py:
Step 3: Quantize with AI Hub
This example uses W8A16 quantization: 8-bit weights and 16-bit activations. Createsubmit_aihub_quant.py:
Quantized ONNX graphs often contain integer tensors internally. In this prototype, the public output remained
FLOAT [1,1,320,320]. The internal tensor before dequantization was uint16, followed by DequantizeLinear to the public float output. Check your own graph before assuming output type or layout.Step 4: Compile the quantized model to QNN DLC
In AI Hub, compile the quantized model for a target that matches your board’s SoC/NPU generation and select the Qualcomm AI Runtime / QNN DLC target runtime. Download the compiled DLC to the board:qnn-net-run before writing a native app:
Step 5: Generate a QNN context binary
The downloaded DLC may contain topology, parameters, and weights, not a prebuilt HTP context cache. A native app can compose that, but the standard fast path is to generate a QNN context binary once and load it directly.Step 6: Build a persistent native QNN runner
The native runner does three things:- Dynamically loads QNN providers from
/usr/lib. - Loads the generated context binary with
QnnContext_createFromBinary(). - Reuses the graph and tensors for repeated
QnnGraph_execute()calls.
qnn_app/Makefile:
qnn_dlc_runner.cpp source from the companion files page. These are the key implementation requirements:
- Include QNN headers from
/usr/include/QNN. - Load
libQnnHtp.sowithdlopen(). - Load
QnnInterface_getProvidersand choose a provider that exposesQNN_API_VERSION_MAJOR. - Call the QNN backend/device/context APIs in the same sequence used by QNN sample apps.
- Load the generated context binary, not the original DLC, using
QnnContext_createFromBinary(). - Use the graph/tensor metadata from the context or the known model contract:
- graph:
graph_ymndtmzg - input:
images, shape[1,320,320,3], float32,1,228,800bytes - output:
output_0, shape[1,1,320,320], float32,409,600bytes
- graph:
- In server mode, copy each new input buffer into the registered input tensor, call
QnnGraph_execute(), and write the output tensor to disk.
qnn-net-run output to verify correctness:
qnn-net-run exactly or within normal floating-point tolerance. In the prototype, max_abs_diff was 0.0.
Step 7: Add the live Python camera app
The live Python process owns the camera and display. The native C++ process owns the QNN context and graph. The Python app should:- Start
qnn_dlc_runner --server. - Wait for
READY. - Open the USB camera with OpenCV.
- For each frame:
- letterbox to
320×320, BGR → RGB, float32[0,1], NHWC batch - write
input.raw - send
RUN input.raw output.rawto the native process - read
output.rawas float32[1,1,320,320] - crop away the letterbox padding and resize depth back to camera resolution
- colorize depth and show
RGB | DEPTH | OVERLAY
- letterbox to
0 is not correct:
Output contract and dtype checks
For this prototype:
Even though W8A16 quantization uses integer tensors internally, the compiled QNN runtime output was float32. Check this for every model. A common pattern is:
output0_q may be 16-bit but the public model/runtime output is float32.
Troubleshooting
qnn-platform-validator fails
The NPU backend is not ready. Check that the correct board image, firmware, FastRPC devices, and QNN packages are installed before debugging the model.
qnn-context-binary-generator succeeds but the app cannot load the context
Regenerate the context on the same board/runtime that will run the app. Context binaries are not portable across arbitrary QAIRT versions and targets.
The native app exits before printing READY
In server mode, make sure the initial --input path exists and has the correct byte size. The prototype runner validates the input path during startup, even though later frames are supplied through RUN commands.
Create a dummy input before launching the server:
Camera opens but display fails
Run from a terminal attached to the board’s graphical session, not a headless SSH shell. If using SSH for logs, keep the OpenCV display on the board’s monitor.Depth colors look unstable
Use more representative calibration frames and recapture in the actual lighting/camera setup. For live demos, 100–300 frames is a better starting point than 32 frames.Why not run qnn-net-run per frame?
qnn-net-run is excellent for validation, but it is a command-line test tool. If you spawn it per frame, most of your time goes into process startup, context setup, and teardown.
For a live app, keep the QNN context alive:
qnn-net-run path measured around 48 ms amortized in the prototype.

