QNN 执行提供程序
ONNX Runtime 的 QNN 执行提供程序可在高通芯片组上实现硬件加速执行。它使用高通 AI Engine Direct SDK (QNN SDK) 从 ONNX 模型构建 QNN 图,该图可通过受支持的加速器后端库执行。ONNX Runtime QNN 执行提供程序可用于配备高通骁龙 SOC 的 Android 和 Windows 设备。
目录
- 安装先决条件(仅限从源代码构建)
- 构建(Android 和 Windows)
- 预构建包(仅限 Windows)
- 高通 AI Hub
- 配置选项
- 支持的 ONNX 算子
- 使用 QNN EP 的 HTP 后端运行模型 (Python)
- QNN 上下文二进制缓存功能
- QNN EP 权重共享
- 用法
- 错误处理
- 在 QNN EP 中添加新算子支持
- 混合精度支持
- LoRAv2 支持
安装先决条件(仅限从源代码构建)
如果您从源代码构建 QNN 执行提供程序,应首先从 https://qpm.qualcomm.com/#/main/tools/details/Qualcomm_AI_Runtime_SDK 下载高通 AI Engine Direct SDK (QNN SDK)。
QNN 版本要求
ONNX Runtime QNN 执行提供程序已使用 QNN 2.22.x 和高通 SC8280、SM8350、骁龙 X SOC 在 Android 和 Windows 上构建并测试通过。
构建(Android 和 Windows)
有关构建说明,请参阅构建页面。
预构建包(仅限 Windows)
注意:从 1.18.0 版本开始,您无需单独下载和安装 QNN SDK。所需的 QNN 依赖库已包含在 ONNX Runtime 包中。
- NuGet 包
- Microsoft.ML.OnnxRuntime.QNN 的每夜构建包源可在此处找到
- Python 包
- 要求
- Windows ARM64(用于在配备高通 NPU 的本地设备上进行推理)
- Windows X64(用于量化模型。请参阅生成量化模型)
- Python 3.11.x
- Numpy 1.25.2 或 >= 1.26.4
- 安装:
pip install onnxruntime-qnn
- 安装每夜构建包
python -m pip install --pre --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/ORT-Nightly/pypi/simple onnxruntime-qnn
- 要求
高通 AI Hub
高通 AI Hub 可用于在高通托管的设备上优化和运行模型。ONNX Runtime QNN 执行提供程序是高通 AI Hub 中受支持的运行时。
配置选项
QNN 执行提供程序支持多种配置选项。这些提供程序选项以键值字符串对的形式指定。
EP 提供程序选项
"backend_type" | 描述 |
---|---|
‘cpu’ | 启用 CPU 后端。对于集成测试很有用。CPU 后端是 QNN 算子的参考实现。 |
‘gpu’ | 启用 GPU 后端。 |
‘htp’ | 启用 HTP 后端。将计算卸载到 NPU。默认值。 |
‘saver’ | 启用 Saver 后端。 |
"backend_path" | 描述 |
---|---|
‘libQnnCpu.so’ 或 ‘QnnCpu.dll’ | 启用 CPU 后端。请参阅 backend_type ‘cpu’。 |
‘libQnnHtp.so’ 或 ‘QnnHtp.dll’ | 启用 HTP 后端。请参阅 backend_type ‘htp’。 |
注意: backend_path
是 backend_type
的替代方案。两者中最多只能指定一个。backend_path
需要平台特定的路径(例如 libQnnCpu.so
对比 QnnCpu.dll
),但也允许指定任意路径。
"profiling_level" | 描述 |
---|---|
‘off’ | 默认值。 |
‘basic’ | |
‘detailed’ |
"profiling_file_path" | 描述 |
---|---|
‘your_qnn_profile_path.csv’ | 指定 CSV 文件路径以导出 QNN 分析事件 |
有关分析的更多信息,请参阅分析工具
除了在编译时设置 profiling_level 外,还可以在运行时通过 ETW (Windows) 动态启用分析。有关更多详细信息,请参阅追踪
"rpc_control_latency" | 描述 |
---|---|
微秒(字符串) | 允许客户端设置 RPC 控制延迟(单位为微秒) |
"vtcm_mb" | 描述 |
---|---|
大小(MB,字符串) | QNN VTCM 大小(MB),默认为 0(未设置) |
"htp_performance_mode" | 描述 |
---|---|
‘burst’ | |
‘balanced’ | |
‘default’ | 默认值。 |
‘high_performance’ | |
‘high_power_saver’ | |
‘low_balanced’ | |
‘low_power_saver’ | |
‘power_saver’ | |
‘sustained_high_performance’ |
"qnn_saver_path" | 描述 |
---|---|
文件路径至 ‘QnnSaver.dll’ 或 ‘libQnnSaver.so’ | QNN Saver 后端库的文件路径。将 QNN API 调用转储到磁盘以供回放/调试。 |
"qnn_context_priority" | 描述 |
---|---|
‘low’ | |
‘normal’ | 默认值。 |
‘normal_high’ | |
‘high’ |
"htp_graph_finalization_optimization_mode" | 描述 |
---|---|
‘0’ | 默认值。 |
‘1’ | 准备时间更快,图不够优化。 |
‘2’ | 准备时间更长,图更优化。 |
‘3’ | 准备时间最长,图最可能更优化。 |
"soc_model" | 描述 |
---|---|
型号(字符串) | SoC 型号。有效值请参考QNN SDK 文档。默认为“0”(未知)。 |
"htp_arch" | 描述 |
---|---|
硬件架构 | HTP 架构编号。有效值请参考QNN SDK 文档。默认(无) |
"device_id" | 描述 |
---|---|
设备 ID(字符串) | 设置 htp_arch 时使用的设备 ID。默认为“0”(表示单个设备)。 |
"enable_htp_fp16_precision" | 描述 示例 |
---|---|
‘0’ | 禁用。如果是 fp32 模型,则使用 fp32 精度进行推理。 |
‘1’ | 默认值。启用 float32 模型使用 fp16 精度进行推理。 |
"offload_graph_io_quantization" | 描述 |
---|---|
‘0’ | 禁用。QNN EP 将处理图 I/O 的量化和反量化。 |
‘1’ | 默认值。启用。将图 I/O 的量化和反量化卸载到 CPU EP。 |
"enable_htp_shared_memory_allocator" | 描述 |
---|---|
‘0’ | 默认值。禁用。 |
‘1’ | 启用 QNN HTP 共享内存分配器。需要提供 libcdsprpc.so/dll。代码示例 |
运行选项
"qnn.lora_config" | 描述 |
---|---|
配置文件路径 | LoRAv2 配置文件路径。配置格式将在LoRAv2 支持中说明。 |
支持的 ONNX 算子
算子 | 注意事项 |
---|---|
ai.onnx:Abs | |
ai.onnx:Add | |
ai.onnx:And | |
ai.onnx:ArgMax | |
ai.onnx:ArgMin | |
ai.onnx:Asin | |
ai.onnx:Atan | |
ai.onnx:AveragePool | |
ai.onnx:BatchNormalization | 从 1.18.0 开始支持 fp16 |
ai.onnx:Cast | |
ai.onnx:Clip | 从 1.18.0 开始支持 fp16 |
ai.onnx:Concat | |
ai.onnx:Conv | 从 1.18.0 开始支持 3d |
ai.onnx:ConvTranspose | 从 1.18.0 开始支持 3d |
ai.onnx:Cos | |
ai.onnx:DepthToSpace | |
ai.onnx:DequantizeLinear | |
ai.onnx:Div | |
ai.onnx:Elu | |
ai.onnx:Equal | |
ai.onnx:Exp | |
ai.onnx:Expand | |
ai.onnx:Flatten | |
ai.onnx:Floor | |
ai.onnx:Gather | 仅支持正索引 |
ai.onnx:Gelu | |
ai.onnx:Gemm | |
ai.onnx:GlobalAveragePool | |
ai.onnx:Greater | |
ai.onnx:GreaterOrEqual | |
ai.onnx:GridSample | |
ai.onnx:HardSwish | |
ai.onnx:InstanceNormalization | |
ai.onnx:LRN | |
ai.onnx:LayerNormalization | |
ai.onnx:LeakyRelu | |
ai.onnx:Less | |
ai.onnx:LessOrEqual | |
ai.onnx:Log | |
ai.onnx:LogSoftmax | |
ai.onnx:LpNormalization | p == 2 |
ai.onnx:MatMul | HTP 后端支持的输入数据类型:(uint8, uint8), (uint8, uint16), (uint16, uint8) |
ai.onnx:Max | |
ai.onnx:MaxPool | |
ai.onnx:Min | |
ai.onnx:Mul | |
ai.onnx:Neg | |
ai.onnx:Not | |
ai.onnx:Or | |
ai.onnx:Prelu | 从 1.18.0 开始支持 fp16, int32 |
ai.onnx:Pad | |
ai.onnx:Pow | |
ai.onnx:QuantizeLinear | |
ai.onnx:ReduceMax | |
ai.onnx:ReduceMean | |
ai.onnx:ReduceMin | |
ai.onnx:ReduceProd | |
ai.onnx:ReduceSum | |
ai.onnx:Relu | |
ai.onnx:Resize | |
ai.onnx:Round | |
ai.onnx:Sigmoid | |
ai.onnx:Sign | |
ai.onnx:Sin | |
ai.onnx:Slice | |
ai.onnx:Softmax | |
ai.onnx:SpaceToDepth | |
ai.onnx:Split | |
ai.onnx:Sqrt | |
ai.onnx:Squeeze | |
ai.onnx:Sub | |
ai.onnx:Tanh | |
ai.onnx:Tile | |
ai.onnx:TopK | |
ai.onnx:Transpose | |
ai.onnx:Unsqueeze | |
ai.onnx:Where | |
com.microsoft:DequantizeLinear | 提供 16 位整数反量化支持 |
com.microsoft:Gelu | |
com.microsoft:QuantizeLinear | 提供 16 位整数量化支持 |
支持的数据类型因算子和 QNN 后端而异。有关更多信息,请参阅QNN SDK 文档。
使用 QNN EP 的 HTP 后端运行模型 (Python)
QNN HTP 后端仅支持量化模型。具有 32 位浮点激活和权重的模型必须首先被量化为使用较低的整数精度(例如,8 位或 16 位整数)。
本节提供了量化模型以及随后使用 Python API 在 QNN EP 的 HTP 后端上运行量化模型的说明。有关量化概念的更广泛概述,请参阅量化页面。
模型要求
QNN EP 不支持具有动态形状的模型(例如,动态批大小)。动态形状必须固定为特定值。有关更多信息,请参阅固定动态输入形状的文档。
此外,QNN EP 支持 ONNX 算子的子集(例如,不支持 Loops 和 Ifs)。请参阅支持的 ONNX 算子列表。
生成量化模型(仅限 x64)
ONNX Runtime Python 包通过导入 onnxruntime.quantization
提供了量化 ONNX 模型的工具。由于在 ARM64 上安装 onnx
包存在问题,量化工具目前仅在 x86_64 上受支持。因此,建议要么使用 x64 机器进行模型量化,要么在 Windows ARM64 机器上使用单独的 x64 Python 安装。
安装 ONNX Runtime x64 Python 包。(请注意,必须使用 x64 包进行模型量化,使用 arm64 包进行推理和利用 HTP/NPU)
python -m pip install onnxruntime-qnn
QNN EP 的量化需要使用校准输入数据。使用能够代表典型模型输入的校准数据集对于生成准确的量化模型至关重要。
以下代码片段定义了一个示例 DataReader
类,用于生成随机 float32 输入数据。请注意,使用随机输入数据很可能会产生不准确的量化模型。有关如何创建从磁盘图像文件提供输入的 CalibrationDataReader
的示例,请参阅Resnet 数据读取器的实现。
# data_reader.py
import numpy as np
import onnxruntime
from onnxruntime.quantization import CalibrationDataReader
class DataReader(CalibrationDataReader):
def __init__(self, model_path: str):
self.enum_data = None
# Use inference session to get input shape.
session = onnxruntime.InferenceSession(model_path, providers=['CPUExecutionProvider'])
inputs = session.get_inputs()
self.data_list = []
# Generate 10 random float32 inputs
# TODO: Load valid calibration input data for your model
for _ in range(10):
input_data = {inp.name : np.random.random(inp.shape).astype(np.float32) for inp in inputs}
self.data_list.append(input_data)
self.datasize = len(self.data_list)
def get_next(self):
if self.enum_data is None:
self.enum_data = iter(
self.data_list
)
return next(self.enum_data, None)
def rewind(self):
self.enum_data = None
以下代码片段预处理原始模型,然后将预处理的模型量化为使用 uint16
激活和 uint8
权重。虽然量化工具公开了 uint8
、int8
、uint16
和 int16
量化数据类型,但 QNN 算子通常支持 uint8
和 uint16
数据类型。有关每个 QNN 算子对数据类型的要求,请参阅QNN SDK 算子文档。
# quantize_model.py
import data_reader
import numpy as np
import onnx
from onnxruntime.quantization import QuantType, quantize
from onnxruntime.quantization.execution_providers.qnn import get_qnn_qdq_config, qnn_preprocess_model
if __name__ == "__main__":
input_model_path = "model.onnx" # TODO: Replace with your actual model
output_model_path = "model.qdq.onnx" # Name of final quantized model
my_data_reader = data_reader.DataReader(input_model_path)
# Pre-process the original float32 model.
preproc_model_path = "model.preproc.onnx"
model_changed = qnn_preprocess_model(input_model_path, preproc_model_path)
model_to_quantize = preproc_model_path if model_changed else input_model_path
# Generate a suitable quantization configuration for this model.
# Note that we're choosing to use uint16 activations and uint8 weights.
qnn_config = get_qnn_qdq_config(model_to_quantize,
my_data_reader,
activation_type=QuantType.QUInt16, # uint16 activations
weight_type=QuantType.QUInt8) # uint8 weights
# Quantize the model.
quantize(model_to_quantize, output_model_path, qnn_config)
运行 python quantize_model.py
将生成一个名为 model.qdq.onnx
的量化模型,该模型可通过 ONNX Runtime 的 QNN EP 在 Windows ARM64 设备上运行。
有关量化工具用法的更多信息,请参阅以下页面
- mobilenet 在 CPU EP 上的量化示例
- quantization/execution_providers/qnn/preprocess.py
- quantization/execution_providers/qnn/quant_config.py
在 Windows ARM64 上运行量化模型 (onnxruntime-qnn 版本 >= 1.18.0)
安装用于 QNN EP 的 ONNX Runtime ARM64 Python 包(需要 Python 3.11.x 和 Numpy 1.25.2 或 >= 1.26.4)
python -m pip install onnxruntime-qnn
以下 Python 代码片段创建了一个带有 QNN EP 的 ONNX Runtime 会话,并在 HTP 后端上运行量化模型 model.qdq.onnx
。
# run_qdq_model.py
import onnxruntime
import numpy as np
options = onnxruntime.SessionOptions()
# (Optional) Enable configuration that raises an exception if the model can't be
# run entirely on the QNN HTP backend.
options.add_session_config_entry("session.disable_cpu_ep_fallback", "1")
# Create an ONNX Runtime session.
# TODO: Provide the path to your ONNX model
session = onnxruntime.InferenceSession("model.qdq.onnx",
sess_options=options,
providers=["QNNExecutionProvider"],
provider_options=[{"backend_path": "QnnHtp.dll"}]) # Provide path to Htp dll in QNN SDK
# Run the model with your input.
# TODO: Use numpy to load your actual input from a file or generate random input.
input0 = np.ones((1,3,224,224), dtype=np.float32)
result = session.run(None, {"input": input0})
# Print output.
print(result)
运行 python run_qdq_model.py
将在 QNN HTP 后端上执行量化的 model.qdq.onnx
模型。
注意,会话已选择性配置为,如果整个模型无法在 QNN HTP 后端上执行,则抛出异常。这对于验证量化模型是否被 QNN EP 完全支持很有用。可用的会话配置包括
- session.disable_cpu_ep_fallback:禁用将不受支持的算子回退到 CPU EP。
- ep.context_enable:启用 QNN 上下文缓存功能,以便导出模型的缓存版本,从而减少会话创建时间。
以上代码片段仅指定了 backend_path
提供程序选项。有关所有可用 QNN EP 提供程序选项的列表,请参阅配置选项部分。
QNN 上下文二进制缓存功能
模型转换、编译、最终化后会有一个 QNN 上下文,其中包含 QNN 图。QNN 可以将上下文序列化为二进制文件,以便用户可以直接使用它进行进一步推理(无需 QDQ 模型),从而降低模型加载成本。QNN 执行提供程序支持多种会话选项来配置此功能。
转储 QNN 上下文二进制文件
- 创建会话选项,将“ep.context_enable”设置为“1”以启用 QNN 上下文转储。键“ep.context_enable”在 onnxruntime_session_options_config_keys.h 中定义为 kOrtSessionOptionEpContextEnable。
- 使用步骤 1 中创建的会话选项创建带有 QDQ 模型的会话,并使用 HTP 后端。一旦会话创建/初始化完成,将创建一个带有 QNN 上下文二进制文件的 Onnx 模型。无需运行会话。QNN 上下文二进制文件的生成可以在具有 HTP 的高通设备上使用 Arm64 构建完成。也可以在 x64 机器上使用 x64 构建完成(但无法运行,因为没有 HTP 设备)。
生成的带有 QNN 上下文二进制文件的 Onnx 模型可以部署到生产/真实设备上运行推理。此 Onnx 模型被 QNN 执行提供程序视为普通模型。推理代码与在 HTP 后端上使用 QDQ 模型进行推理的代码相同。
#include "onnxruntime_session_options_config_keys.h"
// C++
Ort::SessionOptions so;
so.AddConfigEntry(kOrtSessionOptionEpContextEnable, "1");
// C
const OrtApi* g_ort = OrtGetApiBase()->GetApi(ORT_API_VERSION);
OrtSessionOptions* session_options;
CheckStatus(g_ort, g_ort->CreateSessionOptions(&session_options));
g_ort->AddSessionConfigEntry(session_options, kOrtSessionOptionEpContextEnable, "1");
# Python
import onnxruntime
options = onnxruntime.SessionOptions()
options.add_session_config_entry("ep.context_enable", "1")
配置上下文二进制文件路径
如果用户未指定路径,生成的带有 QNN 上下文二进制文件的 Onnx 模型默认为 [input_QDQ_model_name]_ctx.onnx。用户可以使用键“ep.context_file_path”在会话选项中设置路径。下面的代码示例
// C++
so.AddConfigEntry(kOrtSessionOptionEpContextFilePath, "./model_a_ctx.onnx");
// C
g_ort->AddSessionConfigEntry(session_options, kOrtSessionOptionEpContextFilePath, "./model_a_ctx.onnx");
# Python
options.add_session_config_entry("ep.context_file_path", "./model_a_ctx.onnx")
启用嵌入模式
QNN 上下文二进制内容默认不嵌入到生成的 Onnx 模型中。会单独生成一个 bin 文件。文件名类似 [input_model_file_name]QNN[hash_id].bin。此名称由 Ort 提供并在生成的 Onnx 模型中进行跟踪。如果对 bin 文件进行任何更改,将会导致问题。此 bin 文件需要与生成的 Onnx 文件放在一起。用户可以通过将“ep.context_embed_mode”设置为“1”来启用此功能。在这种情况下,上下文二进制内容将嵌入到 Onnx 模型内部。
// C++
so.AddConfigEntry(kOrtSessionOptionEpContextEmbedMode, "1");
// C
g_ort->AddSessionConfigEntry(session_options, kOrtSessionOptionEpContextEmbedMode, "1");
# Python
options.add_session_config_entry("ep.context_embed_mode", "1")
QNN EP 权重共享
注意:QNN EP 需要 Linux x86_64 或 Windows x86_64 平台。
此外,如果用户使用 QNN 工具链(qnn-context-binary-generator
)创建带有权重共享的 QNN 上下文二进制文件(qnn_ctx.bin
),他们可以使用脚本从上下文中生成封装的 Onnx 模型:gen_qnn_ctx_onnx_model.py。该脚本创建多个 model_x_ctx.onnx
文件,每个文件包含一个引用共享的 qnn_ctx.bin
文件的 EPContext
节点。每个 EPContext
节点指定一个唯一的节点名称,引用 QNN 上下文中的不同 QNN 图。
用法
C++
C API 详情请参阅此处。
Ort::Env env = Ort::Env{ORT_LOGGING_LEVEL_ERROR, "Default"};
std::unordered_map<std::string, std::string> qnn_options;
qnn_options["backend_path"] = "QnnHtp.dll";
Ort::SessionOptions session_options;
session_options.AppendExecutionProvider("QNN", qnn_options);
Ort::Session session(env, model_path, session_options);
Python
import onnxruntime as ort
# Create a session with QNN EP using HTP (NPU) backend.
sess = ort.InferenceSession(model_path, providers=['QNNExecutionProvider'], provider_options=[{'backend_path':'QnnHtp.dll'}])`
推理示例
使用 QNN Execution Provider 和 QNN CPU & HTP 后端在 CPP 中进行 Mobilenetv2 图像分类
错误处理
HTP 子系统重启 - SSR
QNN EP 针对 QNN HTP SSR 问题返回 StatusCode::ENGINE_ERROR。如果在会话运行时检测到此错误,上层框架/应用程序应重新创建 ONNX Runtime 会话。
在 QNN EP 中添加新算子支持
要在 EP 中启用新算子支持,需要关注的方面
- QDQ 脚本是否支持此算子?代码示例
- ONNX Runtime QDQ 节点单元是否支持此算子?代码示例
- 是否是布局敏感算子?
- 是否已在 LayoutTransformer 中注册?代码示例
- NHWC 算子模式是否已注册?示例错误消息
::operator ()] 模型 face_det_qdq 加载失败:致命错误:com.ms.internal.nhwc:BatchNormalization(9) 不是已注册的函数/算子 [示例 PR](https://github.com/microsoft/onnxruntime/pull/15278)
启用新算子的示例 PR
混合精度支持
下图展示了一个混合精度模型的示例。
混合精度 QDQ 模型由具有不同激活/权重量化数据类型的区域组成。区域之间的边界使用 DQ 到 Q 序列在激活量化数据类型之间进行转换(例如,uint8 到 uint16)。
指定不同量化数据类型区域的能力有助于探索精度和延迟之间的权衡。更高的整数精度可能会提高精度,但会牺牲延迟,因此有选择地将某些区域提升到更高的精度有助于在关键指标上实现理想的平衡。
下图显示了一个模型,其中某个区域已从默认的 8 位激活类型提升到 16 位。
此模型量化为 uint8 精度,但张量“Op4_out”量化为 16 位。这可以通过指定以下初始张量量化覆盖来实现
# Op4_out could be an inaccurate tensor that should be upgraded to 16bit
initial_overrides = {"Op4_out": [{"quant_type": QuantType.QUInt16}]}
qnn_config = get_qnn_qdq_config(
float_model_path,
data_reader,
activation_type=QuantType.QUInt8,
weight_type=QuantType.QUInt8,
init_overrides=initial_overrides, # These initial overrides will be "fixed"
)
以上代码片段生成以下“固定”覆盖(通过 qnn_config.extra_options[“TensorQuantOverrides”] 获取)
overrides = {
“Op2_out”: [{“quant_type”: QUInt8, “convert”: {“quant_type”: QUInt16, “recv_nodes”: {“Op4”}}}],
“Op3_out”: [{“quant_type”: QUInt8, “convert”: {“quant_type”: QUInt16, “recv_nodes”: {“Op5”}}}],
“Op4_out”: [{“quant_type”: QUInt16}],
“Op5_out”: [{“quant_type”: QUInt16, “convert”: {“quant_type”: QUInt8, “recv_nodes”: {“Op6”}}}]
}
覆盖后,模型工作原理如下
- Op2 的输出被 Op4、Op7 和 Op8 消费。Op4 消费转换后的 u16 类型,而 Op7 和 Op8 消费原始的 u8 类型。
- Op3 的输出从 u8 转换为 u16。Op5 消费转换后的 u16 类型。
- Op4 的输出仅为 u16(未转换)。
- Op5 的输出从 u16 转换为 u8。Op6 消费 u8 类型。
LoRAv2 支持
目前,仅支持带有 EPContext 节点的预编译模型。参考示例脚本 gen_qnn_ctx_onnx_model.py。使用 QNN SDK 应用 LoRAv2 模型后,将生成一个主 QNN 上下文二进制文件和几个适配器二进制部分。我们在推理时使用 LoRAv2 配置并将其放入 RunOptions 中。
- LoRAv2 配置的格式
- 图名称:QNN 预构建上下文二进制文件中的 QNN 图。
- 适配器二进制部分路径:由 qnn-context-binary-generator 生成的二进制部分 ```