Skip to main content
AI / ML
Sam Freund·Jun 30, 2026·← All posts

Introduction

When multiple AI models run concurrently on a Qualcomm device, utilization visibility becomes critical. On CPU workloads, /proc/stat offers a direct view of load. On Hexagon DSP-backed NPU workloads, there is no equivalent standard Linux interface that exposes Q6, HVX (Hexagon Vector eXtensions), and HMX (Hexagon Matrix eXtensions) utilization in real time. Without direct telemetry, inference latency is only an indirect signal. It can show that performance changed, but not why. Teams cannot reliably tell whether the accelerators are engaged, whether the DSP is saturated, or whether there is headroom for additional models. This guide walks through building libqcperf from source and writing a minimal C program that streams live NPU metrics into your own application.

Why Existing Paths Fall Short

The official Qualcomm Profiler is not suitable for many open workflows because it requires NDA access. SysmonApp in the Hexagon SDK can query CDSP utilization over FastRPC, but it is an offline flow: capture to a binary .bin, transfer to a host, then post-process into HTML or CSV. This works for one-time profiling, not continuous on-device telemetry in application code. Hexagon QuRT PMU counters are another option, but they require DSP-side instrumentation and deployment with Hexagon toolchain artifacts. That is a high barrier when the goal is application-layer monitoring from standard Linux processes.

What You Will Do

  1. Confirm FastRPC is present on the device.
  2. Clone and build libqcperf with the NPU backend.
  3. Write and build a minimal C program using the libqcperf API.
  4. Run it and observe live Q6, HVX, and HMX metrics streaming to stdout.

Prerequisites

libqcperf communicates with the CDSP over FastRPC. Before anything below works, the device needs its Qualcomm peripherals enabled and the FastRPC userland present. It’s also necessary to install the headers for the DSP services. Set this up first by following the IQ8 device pages, then come back here: After the reboot, confirm FastRPC is present:
If /dev/fastrpc-cdsp does not exist, the kernel lacks FastRPC support. That is a BSP or image problem, not something you can fix in userland. You’ll need to add your user to the fastrpc group by running the command below, then log out and log back in.
You also need standard build tools along with the DSP headers:

Build libqcperf

All work lives in ~/libqcperf-build. Every code block starts with its own cd, so you can paste any block into a fresh terminal without tracking which directory you are in.

Clone the repository

Configure and build

The NPU backend is off by default. Enable it explicitly. This build targets the host device directly (native aarch64), so no cross-compile toolchain is needed:
The build produces the static library archives the C example links against:

Write a C Integration

For application-layer integration — embedding NPU telemetry directly in your inference loop, correlating metrics with latency measurements, or triggering adaptive behavior — use the libqcperf API directly. The full lifecycle is nine steps. Here is a minimal but complete program that streams all four NPU metrics to stdout.

The program

Create the source file:
Save this as ~/libqcperf-build/example/npu_monitor.c.

Build the example

The example links against the same static library archive produced by the earlier build:

Run it

Expected output (one block per second while a model is running):
Press Ctrl-C to stop. The library shuts down cleanly on SIGINT.

Under the Hood

Sampling rate vs. streaming rate

These two parameters are independent and serve different purposes. The sampling rate (100 ms in the examples above) controls how often the background thread calls into the CDSP over FastRPC to read raw hardware counters. Lower values give finer time resolution but increase FastRPC overhead. The NPU backend supports 1, 5, 10, 50, 100, and 200 ms. The streaming rate (1000 ms) controls how often the background thread fires your data callback. Each callback delivery includes all samples collected since the last delivery — ten samples at 100 ms sampling / 1000 ms streaming. The callback receives them as a flat metric_response array; the example above uses a bitmask to extract only the most recent sample per metric. The supported streaming rates are 100 ms through 1000 ms in 100 ms steps.

The FastRPC path

libqcperf does not open a kernel driver or read a sysfs file. It calls sysmonquery_get_profdata over FastRPC — the same inter-processor RPC mechanism that llama.cpp and LiteRT-LM use to dispatch compute to the CDSP. The call crosses the kernel FastRPC bridge (/dev/fastrpc-cdsp) and returns a struct with the four hardware counter values directly from the DSP firmware. The runtime dependency is libcdsprpc.so. This shared library is already present on Qualcomm Ubuntu images as part of the FastRPC userland. If it is absent, the dynamic linker will fail to start the process before main is reached.

The background thread

qcperf_start spawns a single background thread named qcperf_dsp_npu_thread. This thread owns the FastRPC session for the duration of the monitoring session. Your data callback is called from this thread, not from the thread that called qcperf_start. Keep the callback fast; any blocking work should be handed off to a queue.

Interpreting the Metrics

Live telemetry turns the NPU from a black box into an observable subsystem. A few patterns worth knowing: Low HMX during quantized inference is the most common surprise. If you expect a quantized model to be running on the NPU but HMX utilization is near zero, the workload is not taking the intended accelerator path. Common causes: the model was not compiled with HMX ops enabled, the QNN context binary version does not match the on-device runtime, or the model is falling back to CPU. HVX high, HMX low suggests the model is running vectorized but not matrix-accelerated — typical of FP16 or non-quantized paths, or of models that use HVX-friendly ops (pooling, normalization) but not INT8/INT4 matmuls. Q6 clock stepping up under load is DCVS working correctly. If the clock does not step up when utilization is high, check whether a power profile is capping the CDSP frequency. All metrics near zero while inference is running usually means the workload is executing on the CPU, not the DSP. Confirm with htop and check your model’s backend configuration.

Troubleshooting

Next Steps

With live NPU telemetry in place, the natural next step is to watch a real model run: