前言

2025-2026年,端侧AI(On-Device AI)无疑是移动与嵌入式领域最热的技术方向。相比云端推理,端侧AI具备低延迟、强隐私、离线可用、零带宽成本等优势,正在从“锦上添花”变成刚需。

而高通凭借其Hexagon NPU + Adreno GPU + Kryo CPU的异构计算架构,以及不断完善的QNN SDK和HuggingFace预优化模型仓库,已经构建起业界最成熟的端侧AI生态之一。

本文将带你从架构原理到代码实战,全面理解高通端侧AI技术栈。

一、为什么端侧AI正在爆发?

1.1 云端推理的瓶颈
维度 云端推理 端侧推理
延迟 50-500ms(含网络) 5-50ms
隐私 数据上传风险 数据不出设备
成本 GPU算力费+带宽费 一次性硬件成本
可用性 依赖网络连接 离线完全可用
带宽 高清视频流传输成本高 本地处理零带宽
1.2 高通的端侧AI布局
  • 骁龙8 Elite:NPU算力达到75 TOPS,支持运行100亿参数级大模型
  • 骁龙X Elite:PC平台45 TOPS NPU,直接对标Apple M4 Neural Engine
  • SA8295P / SA8775P:车载平台,支持多路摄像头AI处理
  • 跃龙系列:IoT/边缘计算专用芯片
  • 机器人系列:RB3,RB5等机器人系列计算专用芯片

二、Hexagon NPU架构深度解析

2.1 Hexagon处理器演进
Hexagon 演进路线:
V65 (SD845) → 基础标量/向量计算
V66 (SD855) → 增强向量扩展
V68 (SD888) → 引入HTA (Hexagon Tensor Accelerator)
V69 (SD8Gen1) → HTA性能翻倍
V73 (SD8Gen2) → micro-NPU + 共享内存
V75 (SD8Gen3) → Hexagon直连内存,INT4支持
2.2 三级计算单元架构

Hexagon NPU并非一个简单的加速器,而是包含三级计算单元的异构处理器:

┌──────────────────────────────────────────────┐
│              Hexagon NPU                                                                                    │
│  ┌─────────────────────────────────────────┐  │
│  │  HTA (Hexagon Tensor Accelerator)       │  │
│  │  - 大规模矩阵运算(Con                    │  │
│  │  - INT8/INT16/FP16 混                   │  │
│  │  - 最高能效比                            │  │
│  └─────────────────────────────────────────┘  │
│  ┌─────────────────────────────────────────┐  │
│  │  HVX (Hexagon Vector eXtension)         │  │
│  │  - 1024-bit SIMD 向量运算                │  │
│  │  - 图像预处理、自定义算子                 │  │
│  │  - 灵活可编程                            │  │
│  └─────────────────────────────────────────┘  │
│  ┌─────────────────────────────────────────┐  │
│  │  Scalar Core                            │  │
│  │  - 控制流、标量计算                      │  │
│  │  - 算子调度协调                          │  │
│  └─────────────────────────────────────────┘  │
└──────────────────────────────────────────────┘

关键设计哲学:HTA负责“跑量”(大块矩阵计算),HVX负责“灵活”(自定义向量操作),Scalar Core负责“调度”(控制逻辑)。三者协同工作,实现了高吞吐与灵活性的平衡。

2.3 内存子系统

从V75开始,Hexagon引入了“直连内存(Direct Memory Access)”架构:

传统架构:
DDR → System Cache → Hexagon Cache → HTA/HVX

V75+架构:
DDR
    ↘
System Cache → Hexagon L2 → HTA (带宽翻倍)
    ↘
HVX

这一改进使得大模型推理时的内存带宽瓶颈得到显著缓解,对于Transformer类模型的Attention计算尤为重要。

三、QNN SDK架构与核心概念

3.1 QNN在高通AI软件栈中的位置
应用层
Android NN API / ONNX Runtime / TFLite
        ↓
中间层
HuggingFace 预优化模型 (qualcomm/xxx)
QAIRT (Qualcomm AI Runtime)
        ↓
底层
QNN SDK (Qualcomm AI Engine Direct)
QNN HTP Backend (→ Hexagon NPU)
QNN GPU Backend (→ Adreno GPU)
QNN CPU Backend (→ Kryo CPU)
QNN DSP Backend (→ Hexagon DSP)
        ↓
硬件层
Hexagon NPU / Adreno GPU / Kryo CPU

QNN是高通统一的底层AI推理接口,它抽象了不同硬件后端的差异,开发者通过同一套API就能将模型部署到NPU、GPU或CPU上运行。

3.2 QNN核心概念
概念 说明
Backend 硬件后端(HTP/GPU/CPU/DSP),决定模型运行在哪个计算单元
Context 推理上下文,包含已编译的模型和运行时状态
Graph 计算图,描述模型的计算拓扑
Tensor 张量,模型的输入/输出数据
Op Package 算子包,支持自定义算子扩展
3.3 模型部署完整流程
                    开发阶段                              部署阶段
┌──────────────────────────────┐    ┌────────────────────────────────┐
│  PyTorch / TF / ONNX 模型    │    │    目标设备(手机/车机/IoT)     │
│         │                    │    │         │                      │
│    模型转换 (qnn-xxx-converter)│    │    加载 Context Binary          │
│         │                    │    │         │                      │
│    量化优化 (可选)             │    │    创建 Graph + 绑定 Tensors     │
│         │                    │    │         │                      │
│    离线编译 (qnn-context-      │    │    执行推理 (qnn_graph_execute)  │
│      binary-generator)       │    │         │                      │
│         │                    │    │    读取输出 Tensor               │
│    Context Binary (.bin)     │──→ │                                │
└──────────────────────────────┘    └────────────────────────────────┘

四、实战:QNN SDK环境搭建与第一个推理程序

4.1 环境准备
#1.下载QNN SDK(需要高通开发者账号)
#访问https://qpm.qualcomm.com/下载Qualcomm AI Engine Direct SDK
#假设解压到/opt/qcom/gairt/

#2.设置环境变量
export QNN_SDK_ROOT=/opt/qcom/gairt/2.28.0
export PATH=$QNN_SDK_ROOT/bin/x86_64-linux-clang:$PATH
export LD_LIBRARY_PATH=$QNN_SDK_ROOT/lib/x86_64-linux-clang:$LD_LIBRARY_PATH

#3.验证安装
qnn-model-lib-generator --help

#4.安装Python依赖(用于模型转换)
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple \
    onnx onnxruntime torch torchvision numpy
4.2 模型转换:PyTorch → ONNX → QNN

以MobileNetV2图像分类为例,完整演示端到端流程。

Step 1: PyTorch → ONNX

import torch
import torchvision.models as models

# 加载预训练 MobileNetV2
model = models.mobilenet_v2(pretrained=True)
model.eval()

# 创建示例输入
dummy_input = torch.randn(1, 3, 224, 224)

# 导出 ONNX
torch.onnx.export(
    model,
    dummy_input,
    "mobilenet_v2.onnx",
    input_names=["input"],
    output_names=["output"],
    dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}},
    opset_version=13
)
print("ONNX模型导出完成: mobilenet_v2.onnx")

Step 2: ONNX → QNN模型

# 使用 qnn-onnx-converter 将 ONNX 转换为 QNN 模型 C++ 代码
qnn-onnx-converter \
    --input_network mobilenet_v2.onnx \
    --output_path mobilenet_v2_qnn.cpp \
    --input_dim input 1,3,224,224

# 编译为模型共享库
qnn-model-lib-generator \
    -c mobilenet_v2_qnn.cpp \
    -b mobilenet_v2_qnn.bin \
    -o mobilenet_v2_libs \
    -t x86_64-linux-clang

# 生成可直接在 NPU 上运行的 Context Binary
qnn-context-binary-generator \
    --model mobilenet_v2_libs/x86_64-linux-clang/libmobilenet_v2_qnn.so \
    --backend $QNN_SDK_ROOT/lib/x86_64-linux-clang/libQnnHtp.so \
    --output_dir output \
    --binary_file mobilenet_v2_ctx.bin
4.3 C++推理代码实战
#include <iostream>
#include <vector>
#include <fstream>
#include <cstring>

#include "QnnInterface.h"
#include "QnnContext.h"
#include "QnnGraph.h"
#include "QnnTensor.h"
#include "QnnBackend.h"

class QnnInferenceEngine {
public:
    bool initialize(const std::string& backend_path,
                    const std::string& context_binary_path) {
        // 1. 加载后端库
        backend_handle_ = dlopen(backend_path.c_str(), RTLD_NOW | RTLD_GLOBAL);
        if (!backend_handle_) {
            std::cerr << "加载后端失败: " << dlerror() << std::endl;
            return false;
        }
        
        // 2. 获取 QNN 接口
        auto get_providers = (QnnInterface_getProvidersFn_t)
            dlsym(backend_handle_, "QnnInterface_getProviders");
        
        QnnInterface_t** providers = nullptr;
        uint32_t num_providers = 0;
        get_providers(&providers, &num_providers);
        qnn_interface_ = providers[0]->QNN_INTERFACE_VER_NAME;
        
        // 3. 创建后端实例
        qnn_interface_.backendCreate(nullptr, nullptr, &backend_);
        
        // 4. 创建设备(针对HTP后端)
        qnn_interface_.deviceCreate(nullptr, nullptr, &device_);
        
        // 5. 加载ContextBinary
        std::vector<uint8_t> ctx_binary = loadBinaryFile(context_binary_path);
        qnn_interface_.contextCreateFromBinary(
            backend_, device_, nullptr,
            ctx_binary.data(), ctx_binary.size(),
            &context_, nullptr);
        
        // 6. 从Context中获取计算图
        const QnnSystemContext_GraphInfo_t* graphs = nullptr;
        uint32_t num_graphs = 0;
        qnn_interface_.contextGetGraphs(context_, &graphs, &num_graphs);
        
        if (num_graphs > 0) {
            qnn_interface_.graphRetrieve(context_,
                graphs[0].graphName, &graph_);
        }
        
        std::cout << "QNN推理引擎初始化完成" << std::endl;
        return true;
    }
    
    std::vector<float> infer(const std::vector<float>& input_data) {
        // 构建输入Tensor
        Qnn_Tensor_t input_tensor = buildTensor(
            input_data.data(), {1, 3, 224, 224}, QNN_DATATYPE_FLOAT_32);
        
        // 构建输出Tensor
        std::vector<float> output_data(1000); // ImageNet 1000类
        Qnn_Tensor_t output_tensor = buildTensor(
            output_data.data(), {1, 1000}, QNN_DATATYPE_FLOAT_32);
        
        // 执行推理
        Qnn_Tensor_t inputs[] = {input_tensor};
        Qnn_Tensor_t outputs[] = {output_tensor};
        
        auto status = qnn_interface_.graphExecute(
            graph_, inputs, 1, outputs, 1, nullptr, nullptr);
        
        if (status != QNN_SUCCESS) {
            std::cerr << "推理执行失败,错误码:" << status << std::endl;
            return {};
        }
        
        return output_data;
    }
    
    ~QnnInferenceEngine() {
        if (graph_) qnn_interface_.graphFinalize(graph_);
        if (context_) qnn_interface_.contextFree(context_, nullptr);
        if (device_) qnn_interface_.deviceFree(device_);
        if (backend_) qnn_interface_.backendFree(backend_);
        if (backend_handle_) dlclose(backend_handle_);
    }
    
private:
    void* backend_handle_ = nullptr;
    QnnInterface_t qnn_interface_ = {};
    Qnn_BackendHandle_t backend_ = nullptr;
    Qnn_DeviceHandle_t device_ = nullptr;
    Qnn_ContextHandle_t context_ = nullptr;
    Qnn_GraphHandle_t graph_ = nullptr;
    
    std::vector<uint8_t> loadBinaryFile(const std::string& path) {
        std::ifstream file(path, std::ios::binary | std::ios::ate);
        auto size = file.tellg();
        file.seekg(0);
        std::vector<uint8_t> data(size);
        file.read(reinterpret_cast<char*>(data.data()), size);
        return data;
    }
    
    Qnn_Tensor_t buildTensor(const void* data,
                             std::vector<uint32_t> dims,
                             Qnn_DataType_t dtype) {
        Qnn_Tensor_t tensor = QNN_TENSOR_INIT;
        tensor.v2.dataType = dtype;
        tensor.v2.dimensions = dims.data();
        tensor.v2.rank = dims.size();
        tensor.v2.clientBuf.data = const_cast<void*>(data);
        tensor.v2.clientBuf.dataSize = 1;
        for (auto d : dims) tensor.v2.clientBuf.dataSize *= d;
        tensor.v2.clientBuf.dataSize *= sizeof(float);
        return tensor;
    }
};

int main() {
    QnnInferenceEngine engine;
    
    // 初始化(使用 HTP 后端 + Context Binary)
    engine.initialize(
        "/opt/qcom/qiart/2.28.0/lib/aarch64-android/libQnnHtp.so",
        "output/mobilenet_v2_ctx.bin"
    );
    
    // 准备输入数据(实际项目中从图片读取并预处理)
    std::vector<float> input(1 * 3 * 224 * 224, 0.5f);
    
    // 执行推理
    auto output = engine.infer(input);
    
    // 找到最大概率的类别
    int max_idx = 0;
    float max_val = output[0];
    for (int i = 1; i < output.size(); i++) {
        if (output[i] > max_val) {
            max_val = output[i];
            max_idx = i;
        }
    }
    
    std::cout << "预测类别: " << max_idx
              << ", 置信度: " << max_val << std::endl;
    
    return 0;
}
4.4 编译与运行
# 交叉编译(Android aarch64 目标)
$QNN_SDK_ROOT/bin/x86_64-linux-clang/clang++ \
    -std=c++17 \
    -I$QNN_SDK_ROOT/include \
    -L$QNN_SDK_ROOT/lib/aarch64-android \
    -lQnnHtp -lQnnSystem \
    -o qnn_inference main.cpp

# 推送到设备运行
adb push qnn_inference /data/local/tmp/
adb push output/mobilenet_v2_ctx.bin /data/local/tmp/
adb shell "cd /data/local/tmp && ./qnn_inference"

五、HuggingFace Qualcomm预优化模型:开箱即用

5.1 从HuggingFace下载并使用预优化模型
from huggingface_hub import hf_hub_download, snapshot_download
import numpy as np
import onnxruntime as ort

# ==== 方法1:下载单个模型文件 ====
model_path = hf_hub_download(
    repo_id="qualcomm/MobileNet-v2-Quantized",
    filename="MobileNet-v2-Quantized.onnx"
)
print(f"模型已下载到:{model_path}")

# ==== 方法2:下载整个模型仓库 ====
repo_dir = snapshot_download(
    repo_id="qualcomm/MobileNet-v2-Quantized",
    allow_patterns=["*.onnx", "*.dlc"]
)
print(f"模型仓库已下载到:{repo_dir}")

# ==== 使用 ONNX Runtime 推理验证 ====
session = ort.InferenceSession(model_path)
input_name = session.get_inputs()[0].name
input_shape = session.get_inputs()[0].shape
print(f"模型输入:{input_name}, shape: {input_shape}")

sample_input = np.random.randn(*input_shape).astype(np.float32)
output = session.run(None, {input_name: sample_input})
print(f"推理输出 shape: {output[0].shape}")
print(f"预测类别:{np.argmax(output[0])}")

# ==== 使用 qai_hub_models 库加载模型 ====
import qai_hub_models.models.mobilenet_v2 as mobilenet_v2
model = mobilenet_v2.MobileNetV2.from_pretrained()
print("qai_hub_models 模型加载完成")
5.2 HuggingFace上的高通预优化模型
类别 模型 骁龙8Gen3推理延迟
图像分类 MobileNetV2 ~1.2 ms
目标检测 YOLOv8-N ~4.5 ms
语义分割 DeepLabV3-MobileNet ~8.3 ms
超分辨率 Real-ESRGAN ~18 ms
人脸检测 MediaPipe Face ~2.1 ms
大语言模型 Llama 2 7B (INT4) ~25 tokens/s
文生图 Stable Diffusion 1.5 ~8s/张

六、不同后端的选择策略

【图片:后端选择策略图,原图坐标44,214,954,486】

特性 HTP (NPU) GPU (Adreno) CPU (Kryo)
典型加速比 5-20x 2-8x 1x (基准)
精度支持 INT4/INT8/INT16/FP16 FP16/FP32 FP32
功耗 最低 中等 最高
算子覆盖 ~85% 常用算子 ~95% 100%
适用场景 CNN/Transformer推理 通用计算+渲染 调试/回退

实践建议:优先选HTP后端;遇到不支持的算子,用--fallback_to_cpu--fallback_to_gpu让不支持的算子回退到其他后端执行。

七、常见踩坑与调试技巧

7.1 模型转换失败
# 问题:Unsupported ONNX op: XXX
# 解决:检查QNN支持的算子列表,或用ONNX simplifier优化
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple onnxsim
onnxsim mobilenet_v2.onnx mobilenet_v2_sim.onnx

# 问题:Dynamic shape not supported
# 解决:导出ONNX时固定batch_size,去掉dynamic_axes
7.2 精度偏差排查
import numpy as np

# 对比QNN输出与PyTorch原始输出
pytorch_output = np.load("pytorch_output.npy")
qnn_output = np.load("qnn_output.npy")

# 计算余弦相似度
cos_sim = np.dot(pytorch_output.flatten(), qnn_output.flatten()) / (
    np.linalg.norm(pytorch_output) * np.linalg.norm(qnn_output))
print(f"余弦相似度: {cos_sim:.6f}")  # > 0.99 通常可接受

# 计算SNR (Signal-to-Noise Ratio)
noise = pytorch_output - qnn_output
snr = 10 * np.log10(np.sum(pytorch_output**2) / np.sum(noise**2))
print(f"SNR: {snr:.2f} dB")  # > 30dB 通常OK
7.3 性能Profiling
# 使用 qnn-net-run 进行基准测试
qnn-net-run \
    --model mobilenet_v2_libs/aarch64-android/libmobilenet_v2_qnn.so \
    --backend $QNN_SDK_ROOT/lib/aarch64-android/libQnnHtp.so \
    --input_list input_list.txt \
    --perf_profile burst \
    --profiling_level detailed \
    --output_dir profiling_output

# 分析耗时热点
cat profiling_output/qnn-profiling-data.log | grep "Execute" | sort -k4 -rn | head -20

八、总结与后续

本文从宏观到微观,梳理了高通端侧AI技术栈的全貌:

  1. 硬件层:Hexagon NPU的HTA + HVX + Scalar三级架构
  2. SDK层:QNN统一接口 + 多后端支持
  3. 工具层:模型转换 → 量化 → 编译 → 部署的完整工作流
  4. 模型仓库:HuggingFace qualcomm预优化模型的开箱即用能力

参考资源

下一篇预告:我们将进入实战环节——在骁龙平台部署YOLOv8目标检测模型,包含快速路径(HuggingFace预优化模型)和深度路径(QNN SDK手动转换与调优),并完整演示Android端集成与自定义数据集训练。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐