> ## Documentation Index
> Fetch the complete documentation index at: https://dragonwingdocs.qualcomm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 使用 libqcperf 在 Qualcomm Ubuntu 上进行实时 NPU 监控

> 使用 libqcperf 在 Qualcomm Ubuntu 上以编程方式获取实时 NPU 利用率数据，包括通过 FastRPC 获取的 Q6、HVX、HMX 和时钟指标。

<div style={{ marginBottom: "2rem" }}>
  <div
    style={{
fontSize: "0.72rem",
fontWeight: 700,
color: "#31017D",
letterSpacing: "1.5px",
textTransform: "uppercase",
marginBottom: "0.5rem"
}}
  >
    AI / ML
  </div>

  <div style={{ fontSize: "0.85rem", color: "#888", display: "flex", gap: "0.5rem", flexWrap: "wrap", alignItems: "center" }}>
    <a href="https://www.linkedin.com/in/samuel-freund/" target="_blank" rel="noopener noreferrer" style={{ color: "#888", textDecoration: "none" }}>Sam Freund</a>
    <span>·</span>
    <span>2026年6月30日</span>
    <span>·</span>
    <a href="/zh/tutorial" style={{ color: "#31017D", fontWeight: 600, textDecoration: "none" }}>← 所有文章</a>
  </div>
</div>

<hr style={{ border: "none", borderTop: "1px solid #eee", margin: "0 0 2rem" }} />

## 简介

当多个 AI 模型在 Qualcomm 设备上并发运行时，利用率的可见性变得至关重要。对于 CPU 工作负载，`/proc/stat` 提供了负载的直接视图。而对于基于 Hexagon DSP 的 NPU 工作负载，没有等效的标准 Linux 接口来实时展示 Q6、HVX（Hexagon Vector eXtensions）和 HMX（Hexagon Matrix eXtensions）的利用率。

没有直接的遥测数据，推理延迟只是一个间接信号。它可以表明性能发生了变化，但不能说明原因。团队无法可靠地判断加速器是否被启用、DSP 是否已饱和，或者是否还有余量运行更多模型。

本指南将带你从源码构建 [`libqcperf`](https://github.com/qualcomm/libqcperf)，并编写一个最小化的 C 程序，将实时 NPU 指标流式传输到你自己的应用中。

## 为什么现有途径不够用

官方的 Qualcomm Profiler 不适用于许多开放的工作流程，因为它需要 NDA 访问权限。

Hexagon SDK 中的 SysmonApp 可以通过 FastRPC 查询 CDSP 利用率，但它是离线流程：捕获为二进制 `.bin`，传输到主机，然后后处理为 HTML 或 CSV。这适用于一次性性能分析，而不是应用代码中持续的设备端遥测。

Hexagon QuRT PMU 计数器是另一个选择，但它们需要 DSP 侧的插桩，并需要用 Hexagon 工具链产物进行部署。当目标是从标准 Linux 进程进行应用层监控时，这个门槛太高了。

## 你将做什么

1. 确认设备上存在 FastRPC。
2. 克隆并构建带 NPU 后端的 `libqcperf`。
3. 使用 `libqcperf` API 编写并构建一个最小化的 C 程序。
4. 运行它，观察实时的 Q6、HVX 和 HMX 指标流式输出到 stdout。

## 前提条件

`libqcperf` 通过 FastRPC 与 CDSP 通信。在下面的任何步骤生效之前，设备需要启用其 Qualcomm 外设并具备 FastRPC 用户态。还需要安装 DSP 服务的头文件。

请先按照 IQ8 设备页面完成设置，然后再回到这里：

* [Dragonwing IQ8 首次设置](/zh/Ubuntu/devices/iq8275-evk/setup)
* [安装所需的软件包](/zh/Ubuntu/devices/iq8275-evk/Install_required_software_packages)

重启后，确认 FastRPC 存在：

```bash theme={null}
ls /dev/fastrpc-cdsp                 # must exist
ldconfig -p | grep cdsprpc           # libcdsprpc.so[.1] present
```

如果 `/dev/fastrpc-cdsp` 不存在，说明内核缺少 FastRPC 支持。这是 BSP 或镜像问题，无法在用户态修复。

你需要运行以下命令将用户添加到 fastrpc 组，然后注销并重新登录。

```bash theme={null}
sudo usermod -aG fastrpc $USER
```

你还需要标准构建工具以及 DSP 头文件：

```bash theme={null}
sudo apt-get install -y git cmake build-essential qcom-dspservices-headers-dev
```

## 构建 libqcperf

所有工作都位于 `~/libqcperf-build` 中。每个代码块都以自己的 `cd` 开头，因此你可以将任何代码块粘贴到新终端中，而无需记住当前处于哪个目录。

### 克隆仓库

```bash theme={null}
mkdir -p ~/libqcperf-build
cd ~/libqcperf-build
git clone https://github.com/qualcomm/libqcperf.git
```

### 配置和构建

NPU 后端默认关闭。需要显式启用它。此构建直接面向主机设备（原生 aarch64），因此不需要交叉编译工具链：

```bash theme={null}
cd ~/libqcperf-build
cmake -S libqcperf/qcperf -B build \
    -DCMAKE_BUILD_TYPE=Release \
    -DProjectVersion="0.1.0.0" \
    -DBACKENDS="NPU"
cmake --build build --parallel
```

构建会生成 C 示例要链接的静态库归档：

```text theme={null}
build/libqcperfCore.a
build/libQcPerfDspNpuBackend.a
build/libQcPerfQCv.a
build/libQcPerfQMutex.a
build/libQcPerfQSleep.a
build/libQcPerfQThread.a
build/libQcPerfQTime.a
build/libQcPerfqlist.a
build/libQcPerfQcomDsp.so
```

## 编写 C 集成

对于应用层集成——将 NPU 遥测直接嵌入推理循环、将指标与延迟测量关联，或触发自适应行为——请直接使用 `libqcperf` API。

完整的生命周期是九个步骤。下面是一个最小但完整的程序，将全部四项 NPU 指标流式输出到 stdout。

### 程序

创建源文件：

```bash theme={null}
mkdir -p ~/libqcperf-build/example
```

```c theme={null}
/* npu_monitor.c — minimal libqcperf NPU integration example */
#define _POSIX_C_SOURCE 200809L

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "qcperf.h"
#include "qcperf_common.h"

/* ── Shared state ─────────────────────────────────────────────────────────── */

static volatile sig_atomic_t g_running = 1;

/* Deep copy of backend info so the callback can resolve metric names. */
static struct QcPerfBackendInfo *g_info = NULL;

/* ── Signal handler ───────────────────────────────────────────────────────── */

static void on_signal(int sig) {
    (void)sig;
    g_running = 0;
}

/* ── Data callback ────────────────────────────────────────────────────────── */

/*
 * Called by the libqcperf background thread once per streaming interval
 * (1000 ms in this example).  data->metric_response holds all samples
 * collected during that window; we print only the most recent value for
 * each metric_id.
 */
static enum QcPerfReturnCode on_data(struct QcPerfData *data) {
    uint32_t idx = 0;
    uint64_t written = 0;

    if (NULL == data) {
        return QC_PERF_RETURN_CODE_FAILED;
    }

    printf("--- NPU snapshot ---\n");

    for (idx = data->metric_response_len; idx > 0; idx--) {
        uint16_t mid = data->metric_response[idx - 1].metric_id;
        uint64_t bit = (mid < 64) ? ((uint64_t)1 << mid) : 0;

        if (0 == bit || 0 != (written & bit)) {
            continue;   /* skip: out of range or already printed */
        }
        written |= bit;

        /* Resolve the metric name from the deep-copied backend info. */
        const char *name = NULL;
        if (NULL != g_info && NULL != g_info->capabilities_list) {
            uint8_t cap = data->capabilityId;
            if (cap < g_info->capabilities_list_length) {
                struct QcPerfCapabilityInfo *ci = &g_info->capabilities_list[cap];
                for (uint8_t m = 0; m < ci->metric_ids_list_len; m++) {
                    if (ci->metric_ids_list[m].metric_id == mid) {
                        name = ci->metric_ids_list[m].metric_name;
                        break;
                    }
                }
            }
        }

        if (NULL == name) {
            printf("  metric_%u: ", (unsigned)mid);
        } else {
            printf("  %-20s ", name);
        }

        struct QcPerfGenericType *v = &data->metric_response[idx - 1].metric_value;
        switch (v->data_type) {
        case QC_PERF_DATA_TYPE_DOUBLE:  printf("%.2f\n",  v->double_value);                    break;
        case QC_PERF_DATA_TYPE_UINT64:  printf("%llu\n",  (unsigned long long)v->uint64_value); break;
        case QC_PERF_DATA_TYPE_INT64:   printf("%lld\n",  (long long)v->int64_value);           break;
        case QC_PERF_DATA_TYPE_BOOL:    printf("%s\n",    v->bool_value ? "true" : "false");    break;
        default:                        printf("(unknown type)\n");                              break;
        }
    }

    return QC_PERF_RETURN_CODE_SUCCESS;
}

/* ── Message callback ─────────────────────────────────────────────────────── */

static enum QcPerfReturnCode on_message(struct QcPerfMessage *msg) {
    if (NULL == msg || NULL == msg->message) {
        return QC_PERF_RETURN_CODE_FAILED;
    }
    if (msg->message_level != QC_PERF_MESSAGE_LEVEL_DEBUG) {
        fprintf(stderr, "[backend] %s\n", msg->message);
    }
    return QC_PERF_RETURN_CODE_SUCCESS;
}

/* ── main ─────────────────────────────────────────────────────────────────── */

int main(void) {
    enum QcPerfReturnCode rc = QC_PERF_RETURN_CODE_FAILED;
    struct QcPerfBackendInfo *info = NULL;
    struct QcPerfRequest *req = NULL;
    int exit_code = 0;

    /* Install signal handlers for clean shutdown. */
    struct sigaction sa = {0};
    sa.sa_handler = on_signal;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGINT,  &sa, NULL);

    /* Step 1: Initialise the library. */
    rc = qcperf_init();
    if (QC_PERF_RETURN_CODE_SUCCESS != rc) {
        fprintf(stderr, "qcperf_init failed (%d)\n", (int)rc);
        return 1;
    }

    /* Step 2: Connect to the NPU backend, registering the message callback. */
    rc = qcperf_connect_backend(QC_PERF_BACKEND_DSP_NPU, &on_message);
    if (QC_PERF_RETURN_CODE_SUCCESS != rc) {
        fprintf(stderr, "qcperf_connect_backend failed (%d)\n", (int)rc);
        exit_code = 1;
        goto deinit;
    }

    /* Step 3: Query capabilities and deep-copy for use in the callback. */
    info = (struct QcPerfBackendInfo *)calloc(1, sizeof(struct QcPerfBackendInfo));
    if (NULL == info) {
        fprintf(stderr, "calloc failed\n");
        exit_code = 1;
        goto disconnect;
    }

    rc = qcperf_get_capabilities_info(QC_PERF_BACKEND_DSP_NPU, info);
    if (QC_PERF_RETURN_CODE_SUCCESS != rc) {
        fprintf(stderr, "qcperf_get_capabilities_info failed (%d)\n", (int)rc);
        exit_code = 1;
        goto disconnect;
    }

    /*
     * Deep-copy into g_info so the data callback (called from a background
     * thread) can safely look up metric names without touching the stack-local
     * `info` pointer.
     */
    g_info = (struct QcPerfBackendInfo *)calloc(1, sizeof(struct QcPerfBackendInfo));
    if (NULL != g_info) {
        g_info->backend_id = info->backend_id;
        g_info->capabilities_list_length = info->capabilities_list_length;
        g_info->capabilities_list = (struct QcPerfCapabilityInfo *)calloc(
            info->capabilities_list_length, sizeof(struct QcPerfCapabilityInfo));
        if (NULL != g_info->capabilities_list) {
            for (uint8_t c = 0; c < info->capabilities_list_length; c++) {
                g_info->capabilities_list[c] = info->capabilities_list[c];
                uint8_t mlen = info->capabilities_list[c].metric_ids_list_len;
                g_info->capabilities_list[c].metric_ids_list =
                    (struct QcPerfMetricInfo *)calloc(mlen, sizeof(struct QcPerfMetricInfo));
                if (NULL != g_info->capabilities_list[c].metric_ids_list) {
                    for (uint8_t m = 0; m < mlen; m++) {
                        g_info->capabilities_list[c].metric_ids_list[m] =
                            info->capabilities_list[c].metric_ids_list[m];
                    }
                }
            }
        }
    }

    /* Step 4: Register the data callback. */
    rc = qcperf_set_data_callback(QC_PERF_BACKEND_DSP_NPU, &on_data);
    if (QC_PERF_RETURN_CODE_SUCCESS != rc) {
        fprintf(stderr, "qcperf_set_data_callback failed (%d)\n", (int)rc);
        exit_code = 1;
        goto disconnect;
    }

    /* Step 5: Build the request and start monitoring. */
    req = (struct QcPerfRequest *)calloc(1, sizeof(struct QcPerfRequest));
    if (NULL == req) {
        fprintf(stderr, "calloc failed\n");
        exit_code = 1;
        goto disconnect;
    }

    req->capability_id  = info->capabilities_list[0].capability_id;
    req->sampling_rate  = 100;   /* poll CDSP every 100 ms */
    req->streaming_rate = 1000;  /* deliver callback every 1000 ms */

    rc = qcperf_start(QC_PERF_BACKEND_DSP_NPU, req);
    if (QC_PERF_RETURN_CODE_SUCCESS != rc) {
        fprintf(stderr, "qcperf_start failed (%d)\n", (int)rc);
        exit_code = 1;
        goto disconnect;
    }

    fprintf(stderr, "Streaming NPU metrics — press Ctrl-C to stop\n");

    /* Step 6: Run until signalled. */
    while (0 != g_running) {
        sleep(1);
    }

    /* Step 7: Stop monitoring. */
    qcperf_stop(QC_PERF_BACKEND_DSP_NPU, req);
    free(req);
    req = NULL;

disconnect:
    /* Step 8: Disconnect the backend. */
    free(req);
    req = NULL;
    qcperf_disconnect_backend(QC_PERF_BACKEND_DSP_NPU);

deinit:
    /* Step 9: Deinitialise the library. */
    qcperf_deinit();

    /* Free caller-owned memory. */
    if (NULL != info) {
        free(info);
    }
    if (NULL != g_info) {
        if (NULL != g_info->capabilities_list) {
            for (uint8_t c = 0; c < g_info->capabilities_list_length; c++) {
                free(g_info->capabilities_list[c].metric_ids_list);
            }
            free(g_info->capabilities_list);
        }
        free(g_info);
    }

    return exit_code;
}
```

将其保存为 `~/libqcperf-build/example/npu_monitor.c`。

### 构建示例

该示例链接前面构建产生的同一批静态库归档：

```bash theme={null}
cd ~/libqcperf-build
gcc -std=c11 \
    -I libqcperf/qcperf/core/inc \
    -I libqcperf/qcperf/backends/inc \
    -I build/include \
    example/npu_monitor.c \
    build/libqcperfCore.a \
    build/libQcPerfDspNpuBackend.a \
    build/libQcPerfQCv.a \
    build/libQcPerfQMutex.a \
    build/libQcPerfQSleep.a \
    build/libQcPerfQThread.a \
    build/libQcPerfQTime.a \
    build/libQcPerfqlist.a \
    -L build -lQcPerfQcomDsp \
    -lcdsprpc \
    -lpthread \
    -o example/npu_monitor
```

### 运行

```bash theme={null}
cd ~/libqcperf-build
export LD_LIBRARY_PATH=build
./example/npu_monitor
```

预期输出（模型运行时每秒一个数据块）：

```text theme={null}
Streaming NPU metrics — press Ctrl-C to stop
--- NPU snapshot ---
  Q6 Utilization      42.50
  Q6 Clock            614400.00
  HVX Utilization     12.30
  HMX Utilization     8.70
--- NPU snapshot ---
  Q6 Utilization      67.10
  Q6 Clock            729600.00
  HVX Utilization     31.80
  HMX Utilization     55.20
```

按 `Ctrl-C` 停止。库在收到 `SIGINT` 时会干净地关闭。

## 深入原理

### 采样率与流传输率

这两个参数相互独立，用途也不同。

**采样率**（上例中为 100 ms）控制后台线程通过 FastRPC 调用 CDSP 读取原始硬件计数器的频率。较低的值提供更精细的时间分辨率，但会增加 FastRPC 开销。NPU 后端支持 1、5、10、50、100 和 200 ms。

**流传输率**（1000 ms）控制后台线程触发数据回调的频率。每次回调传递自上次传递以来收集的所有样本——在 100 ms 采样 / 1000 ms 流传输时为 10 个样本。回调以扁平的 `metric_response` 数组接收它们；上面的示例使用位掩码只提取每个指标的最新样本。

支持的流传输率为 100 ms 到 1000 ms，步长 100 ms。

### FastRPC 路径

`libqcperf` 不打开内核驱动，也不读取 sysfs 文件。它通过 FastRPC 调用 `sysmonquery_get_profdata`——这与 llama.cpp 和 LiteRT-LM 用于将计算分派到 CDSP 的处理器间 RPC 机制相同。该调用穿过内核 FastRPC 桥（`/dev/fastrpc-cdsp`），并直接从 DSP 固件返回包含四个硬件计数器值的结构体。

运行时依赖是 `libcdsprpc.so`。这个共享库作为 FastRPC 用户态的一部分，已经存在于 Qualcomm Ubuntu 镜像中。如果它缺失，动态链接器会在到达 `main` 之前就无法启动进程。

### 后台线程

`qcperf_start` 会派生一个名为 `qcperf_dsp_npu_thread` 的后台线程。在监控会话期间，该线程拥有 FastRPC 会话。你的数据回调是从这个线程调用的，而不是调用 `qcperf_start` 的那个线程。保持回调足够快；任何阻塞性工作都应交给队列处理。

## 解读指标

实时遥测将 NPU 从黑盒变成可观测的子系统。

| 指标              | 单位           | 含义                                                                          |
| --------------- | ------------ | --------------------------------------------------------------------------- |
| Q6 Utilization  | 占最大 Q6 时钟的 % | 标量 DSP 压力。高值意味着 Q6 核心繁忙——要么在运行非向量化代码，要么在管理 HVX/HMX 分派开销。                    |
| Q6 Clock        | KHz          | DCVS 选择的实际 CDSP 时钟频率。将其与利用率关联：614 MHz 下 80% 的利用率与 1 GHz 下 80% 的利用率是截然不同的情况。 |
| HVX Utilization | 占最大 Q6 时钟的 % | Hexagon Vector eXtensions 的使用程度。HVX 处理 128 字节的 SIMD 操作——卷积、激活、逐元素操作。        |
| HMX Utilization | 占最大 Q6 时钟的 % | Hexagon Matrix eXtensions 的使用程度。HMX 是用于量化线性层的专用矩阵乘加速器。                      |

几个值得了解的模式：

**量化推理期间 HMX 偏低**是最常见的意外。如果你预期量化模型运行在 NPU 上，但 HMX 利用率接近零，说明工作负载没有走预期的加速器路径。常见原因：模型编译时未启用 HMX 算子、QNN 上下文二进制文件版本与设备上的运行时不匹配，或者模型回退到了 CPU。

**HVX 高、HMX 低**表明模型在向量化运行但未使用矩阵加速——这是 FP16 或非量化路径的典型特征，或者模型使用了适合 HVX 的算子（池化、归一化），但没有 INT8/INT4 矩阵乘法。

**负载下 Q6 时钟逐级上升**说明 DCVS 工作正常。如果利用率高时时钟不上升，请检查是否有功耗配置限制了 CDSP 频率。

**推理运行时所有指标接近零**通常意味着工作负载在 CPU 上执行，而不是 DSP。用 `htop` 确认，并检查模型的后端配置。

## 故障排查

| 症状                                                                       | 可能原因                     | 解决办法                                                                                                     |
| ------------------------------------------------------------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------- |
| `error while loading shared libraries: libcdsprpc.so`                    | 未安装 `libcdsprpc.so`      | 按照 [IQ8 软件设置](/zh/Ubuntu/devices/iq8275-evk/Install_required_software_packages)指南操作；该库随 FastRPC 用户态软件包提供 |
| `qcperf_connect_backend` 返回 `QC_PERF_RETURN_CODE_FAILED` 且日志中有 `[ERROR]` | CDSP 不可访问                | 确认 `/dev/fastrpc-cdsp` 存在；如不存在，说明内核或固件未启用 FastRPC                                                        |
| 所有指标值均为 `0.00`                                                           | 没有活跃的 DSP 工作负载           | 计数器与硬件精确对应；零表示 CDSP 空闲。启动一个 NPU 推理工作负载                                                                   |
| 构建失败：未设置 `QCPERF_ENABLED_QCOM_LINUX_NPU`                                 | CMake 未检测到 aarch64 Linux | 确认你是在 `linux-aarch64` 上或为其构建；NPU 后端仅限该平台                                                                 |

## 后续步骤

有了实时 NPU 遥测，下一步自然是观察真实模型的运行：

* [使用 LiteRT-LM 在 IQ8 NPU 上运行 Gemma-4 E2B](/zh/tutorials/gemma-litert-lm-on-iq8) — 让 `npu_monitor` 与 LiteRT-LM 一起运行，观察预填充期间 HMX 利用率攀升
* [在 Dragonwing 上使用 llama.cpp 运行 LLM](/zh/Ubuntu/ai-workflows/llama-cpp) — 将 Q6 时钟阶跃与 llama.cpp 的 token 吞吐量关联
* [libqcperf API 参考](/zh/Ubuntu/tools/libqcperf) — 所有后端、返回码和结构体字段的完整文档
* [libqcperf on GitHub](https://github.com/qualcomm/libqcperf) — 源码、问题跟踪器以及用于添加新后端的 DEVELOPMENT-GUIDE
