> ## Documentation Index
> Fetch the complete documentation index at: https://dragonwingdocs-staging.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>Jun 30, 2026</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 毫秒)控制后台线程通过 FastRPC 调用 CDSP 读取原始硬件计数器的频率。较低的值提供更精细的时间分辨率,但会增加 FastRPC 开销。NPU 后端支持 1、5、10、50、100 和 200 毫秒。

**流式传输率**(1000 毫秒)控制后台线程触发数据回调的频率。每次回调交付包含自上次交付以来收集的所有样本 — 在 100 毫秒采样 / 1000 毫秒流式传输下为十个样本。回调以扁平的 `metric_response` 数组接收它们;上面的示例使用位掩码只提取每个指标最近的样本。

支持的流式传输率为 100 毫秒到 1000 毫秒,以 100 毫秒为步长。

### 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) — 在 LiteRT-LM 旁边运行 `npu_monitor`,观察预填充期间 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
