PyTorch QNNPACK 量化推理内核:算子覆盖、交叉编译构建与框架集成全解
【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch
QNNPACK(Quantized Neural Networks PACKage)是 PyTorch 中面向移动端优化的 8 位量化神经网络推理引擎,它不直接面向研究者,而是为上层深度学习框架提供低性能开销的量化算子原语。本文以仓库中 QNNPACK README 为主体,结合 CMakeLists.txt、pytorch_qnnpack.h 与 PyTorch 顶层构建脚本,完整讲解 QNNPACK 的算子覆盖情况、原生/Android/iOS 三种构建方式,以及它如何被 PyTorch 的量化算子调用起来,帮助读者独立完成 QNNPACK 的构建、交叉编译与集成调试。
一、QNNPACK 的定位:框架之下的量化算子库
README 开宗明义地定义了 QNNPACK 的角色:
QNNPACK (Quantized Neural Networks PACKage) is a mobile-optimized library for low-precision high-performance neural network inference. QNNPACK provides implementation of common neural network operators on quantized 8-bit tensors.
也就是说,QNNPACK 提供的是基于量化 8 位整型张量的常见神经网络算子实现,目标是移动端的低精度、高性能推理。它明确不打算被机器学习研究者直接调用,而是作为低层性能原语,供 PyTorch 这类高层框架调用——README 指出该库目前已集成进 PyTorch。
从源码结构可以印证这一"框架之下"的定位:
- QNNPACK 本体位于 aten/src/ATen/native/quantized/cpu/qnnpack,是一套纯 C/C++(外加汇编)库,对外仅暴露一个 C API 头文件 pytorch_qnnpack.h;
- PyTorch 的量化 CPU 算子(如
qconv、qlinear、qrelu、qtanh等)位于同级的 aten/src/ATen/native/quantized/cpu 目录下,通过#ifdef USE_PYTORCH_QNNPACK条件编译决定是否调用 QNNPACK 的算子创建/执行函数(见 init_qnnpack.cpp 与 QnnpackUtils.h)。
这种设计意味着:PyTorch 的torch.quantized量化模型在 CPU 上执行时,其卷积、全连接、激活等算子底层最终可以落到 QNNPACK 针对 ARM NEON / x86 SSE2 手工向量化的微内核上。
二、算子覆盖范围
README 以勾选清单形式列出了已实现与计划实现的算子,这是评估 QNNPACK 能否承载某一移动端模型的关键依据:
| 算子 | 状态 |
|---|---|
| 2D 卷积(Convolution) | 已实现 |
| 2D 反卷积(Deconvolution) | 已实现 |
| 通道混洗(Channel Shuffle) | 已实现 |
| 全连接(Fully Connected) | 已实现 |
| 局部连接(Locally Connected) | 计划中 |
| 2D 最大池化(Max Pooling) | 已实现 |
| 2D 平均池化(Average Pooling) | 已实现 |
| 全局平均池化(Global Average Pooling) | 已实现 |
| Sigmoid | 已实现 |
| TanH | 已实现 |
| Leaky ReLU | 已实现 |
| Hardsigmoid | 已实现 |
| Hardswish | 已实现 |
| Clamp(若未被其他算子融合,可充当 ReLU / ReLU6) | 已实现 |
| SoftArgMax(即 SoftMax) | 已实现 |
| Group Normalization | 计划中 |
这一清单与源码一一对应。CMakeLists.txt 中PYTORCH_QNNPACK_INIT_SRCS变量精确列出了参与编译的算子源文件:src/convolution.c、src/deconvolution.c、src/channel-shuffle.c、src/fully-connected.c(含src/fully-connected-sparse.c稀疏版本)、src/max-pooling.c、src/average-pooling.c、src/global-average-pooling.c、src/sigmoid.c、src/tanh.c、src/leaky-relu.c、src/hardsigmoid.c、src/hardswish.c、src/clamp.c、src/softargmax.c等,与 README 清单完全吻合。值得注意的是,源码中还额外实现了src/add.c(逐元素加法)与稀疏全连接算子,这属于 README 清单之外的工程实现细节。
三、构建方式一:本机原生编译
README 推荐使用scripts/build-local.sh为本机构建 QNNPACK。该脚本实际位于 scripts/build-local.sh,其完整逻辑值得逐段理解:
mkdir -p build/local CMAKE_ARGS=() # CMake-level configuration CMAKE_ARGS+=("-DCMAKE_BUILD_TYPE=Release") CMAKE_ARGS+=("-DCMAKE_POSITION_INDEPENDENT_CODE=ON") # If Ninja is installed, prefer it to Make if [ -x "$(command -v ninja)" ]; then CMAKE_ARGS+=("-GNinja") fi CMAKE_ARGS+=("-DPYTORCH_QNNPACK_LIBRARY_TYPE=static") CMAKE_ARGS+=("-DPYTORCH_QNNPACK_BUILD_BENCHMARKS=ON") CMAKE_ARGS+=("-DPYTORCH_QNNPACK_BUILD_TESTS=ON") # Use-specified CMake arguments go last to allow overriding defaults CMAKE_ARGS+=($@) cd build/local && cmake ../.. "${CMAKE_ARGS[@]}" # Cross-platform parallel build if [ "$(uname)" == "Darwin" ]; then cmake --build . -- "-j$(sysctl -n hw.ncpu)" else cmake --build . -- "-j$(nproc)" fi其中几个关键 CMake 选项在 CMakeLists.txt 中定义:
PYTORCH_QNNPACK_LIBRARY_TYPE:取值default/static/shared,决定产出默认库、静态库还是动态库(脚本固定为static);PYTORCH_QNNPACK_BUILD_TESTS:默认ON,开启后在test/目录下构建大量 gtest 单元测试(算子级如convolution-test、fully-connected-test,微内核级如q8gemm-test、q8conv-test);PYTORCH_QNNPACK_BUILD_BENCHMARKS:默认ON,构建bench/下基于 Google Benchmark 的微基准程序;- 另外 CMake 中还有一行重要的全局宏:
add_definitions(-DPYTORCH_QNNPACK_RUNTIME_QUANTIZATION=1)(CMakeLists.txt 第 21 行),启用运行时重新量化(runtime requantization)路径,这正是 PyTorch 集成时"每层动态确定量化参数"模式所需的。
脚本最后接受任意额外参数($@)追加到 CMake 参数末尾,用户可借此覆盖默认值,例如单独关闭基准测试。构建完成后可用ctest运行add_test(...)注册的用例(如convolution-test、q8gemm-test,见 CMakeLists.txt 第 460 行起)。
一个工程细节:QNNPACK 构建时会自动通过 Confu 风格脚本下载依赖(cpuinfo、FP16、FXdiv、PSimd、pthreadpool,以及测试/基准所需的 GoogleTest、GoogleBenchmark),对应 cmake/DownloadCpuinfo.cmake 等脚本;若需离线构建,可在 CMake 参数中显式指定CPUINFO_SOURCE_DIR等变量跳过下载(见 CMakeLists.txt 第 77-147 行)。
四、构建方式二:Android 交叉编译
README 说明:交叉编译到 Android 需设置$ANDROID_NDK环境变量(指向 Android NDK 目录),然后按目标 ABI 选择对应脚本:
| ABI | 构建脚本 | 限制 |
|---|---|---|
| armeabi-v7a | scripts/build-android-armv7.sh | 要求 CPU 支持 ARM NEON |
| arm64-v8a | scripts/build-android-arm64.sh | — |
| x86 | scripts/build-android-x86.sh | — |
这些脚本在 aten/src/ATen/native/quantized/cpu/qnnpack/scripts 目录下均真实存在,且每个 ABI 还配有对应的运行时测试脚本(如 test-android-arm64.sh),用于在设备/模拟器上执行构建产物中的单测。
README 特别强调了一条 armeabi-v7a 的注意事项(原文引用):
Onarmeabi-v7a
pytorch_qnnp_initializewill fail withpytorch_qnnp_status_unsupported_hardwareif the mobile CPU does not support ARM NEON. Don't set-DANDROID_ARM_NEON=1for QNNPACK compilation as it can makepytorch_qnnp_initializecrash on CPUs without ARM NEON.
这段话可以直接在源码中得到验证:
- 状态码
pytorch_qnnp_status_unsupported_hardware定义于 pytorch_qnnpack.h 第 31 行,是pytorch_qnnp_initialize()的合法返回之一; - 编译期,CMakeLists.txt 第 273-280 行 依据
CMAKE_SYSTEM_PROCESSOR是否为armv[5-8]/aarch64或 iOS 的armv7/arm64前缀,选择性地把PYTORCH_QNNPACK_ARM_NEON_UKERNELS(NEON C 内核)与PYTORCH_QNNPACK_AARCH32_ASM_UKERNELS/PYTORCH_QNNPACK_AARCH64_ASM_UKERNELS(汇编内核)加入编译目标。因此"是否编译 NEON 内核"由 CMake 的目标处理器判定,而不是由ANDROID_ARM_NEON之类的 NDK 全局开关决定——后者若打开,会给非 QNNPACK 代码注入 NEON 指令却绕过了 QNNPACK 自身的运行时硬件探测,从而在无 NEON 的 CPU 上把"优雅失败"变成"直接崩溃"。这就是 README 警告的实质原因。
五、构建方式三:iOS 交叉编译
README 说明:交叉编译到 iOS 需要先准备 ios-cmake 项目(提供ios.toolchain.cmake工具链文件),设置环境变量$IOS_CMAKE_TOOLCHAIN_FILE指向该文件,再按下表选择脚本:
| 架构 | 构建脚本 | 说明 |
|---|---|---|
| armv7 | scripts/build-ios-armv7.sh | iPhone 3GS/4/4S |
| armv7s | scripts/build-ios-armv7s.sh | iPhone 5 及更新机型 |
| arm64 | scripts/build-ios-arm64.sh | iPhone 5S 及更新机型 |
| arm64e | scripts/build-ios-arm64e.sh | iPhone XS/XR |
| i386 | scripts/build-ios-i386.sh | iPhone 模拟器(32 位) |
| x86_64 | scripts/build-ios-x86_64.sh | iPhone 模拟器(64 位) |
scripts 目录下上述 6 个 iOS 脚本全部存在。iOS 构建还有一个硬约束:QNNPACK 不支持多架构混合构建。CMakeLists.txt 第 44-56 行 在CMAKE_SYSTEM_PROCESSOR未定义且处于 iOS 环境时,会检查IOS_ARCH的数量与取值——多架构直接FATAL_ERROR,取值必须匹配i386|x86_64|armv7.*|arm64.*。同样的平台/架构白名单检查也存在于 PyTorch 顶层构建逻辑 cmake/Dependencies.cmake 第 342-357 行,不支持的IOS_ARCH会触发警告并自动关闭USE_NNPACK/USE_PYTORCH_QNNPACK/USE_XNNPACK三个开关。
六、QNNPACK 如何被 PyTorch 调用:C API 与算子生命周期
QNNPACK 对外暴露的是纯 C 接口(pytorch_qnnpack.h,extern "C"包裹),所有算子遵循统一的创建 → 配置 → 执行 → 销毁四步生命周期:
进程级初始化:
pytorch_qnnp_initialize()(第 42 行)只需调用一次,内部通过 cpuinfo 探测 CPU 特性并选定微内核。PyTorch 侧的封装见 init_qnnpack.cpp:void initQNNPACK() { static enum pytorch_qnnp_status qnnpackStatus = pytorch_qnnp_initialize(); TORCH_CHECK( qnnpackStatus == pytorch_qnnp_status_success, "failed to initialize QNNPACK"); }静态局部变量保证只初始化一次;任何非
success状态(含前述unsupported_hardware)都会抛出"failed to initialize QNNPACK"异常。创建算子:
pytorch_qnnp_create_*系列函数接收形状、量化参数(input_zero_point、kernel_zero_points、output_zero_point、output_min/max)、flags与requantization_scales,返回一个不透明句柄pytorch_qnnp_operator_t。以 2D 卷积为例,pytorch_qnnp_create_convolution2d_nhwc_q8(第 48-70 行) 的签名表明输入采用 NHWC 布局、8 位量化,并支持per_channel(逐通道权重量化)与组卷积(groups)。此外还有 3D 卷积(..._convolution3d_ndhwc_q8)、稀疏全连接(..._fully_connected_sparse_dq_nc_q8,支持 uint8/16/32 三种索引位宽的稀疏矩阵)等变体。配置运行参数:
pytorch_qnnp_setup_*系列传入本批次的具体 batch size、空间尺寸、输入/输出指针与 stride,部分算子(卷积、反卷积、池化)还接受pthreadpool_t线程池参数用于多线程切分。执行与销毁:
pytorch_qnnp_run_operator(op, threadpool)触发实际计算,pytorch_qnnp_delete_operator(op)释放算子资源(对应源码 src/operator-run.c 与 src/operator-delete.c)。
在 PyTorch 顶层构建中,这一套库通过 CMake 选项USE_PYTORCH_QNNPACK接入。cmake/Dependencies.cmake 第 490-513 行 显示:
if(USE_PYTORCH_QNNPACK) if(NOT DEFINED PYTORCH_QNNPACK_SOURCE_DIR) set(PYTORCH_QNNPACK_SOURCE_DIR "${PROJECT_SOURCE_DIR}/aten/src/ATen/native/quantized/cpu/qnnpack" CACHE STRING "QNNPACK source directory") endif() if(NOT TARGET pytorch_qnnpack) set(PYTORCH_QNNPACK_BUILD_TESTS OFF CACHE BOOL "") set(PYTORCH_QNNPACK_BUILD_BENCHMARKS OFF CACHE BOOL "") set(PYTORCH_QNNPACK_LIBRARY_TYPE "static" CACHE STRING "") add_subdirectory("${PYTORCH_QNNPACK_SOURCE_DIR}" "${CONFU_DEPENDENCIES_BINARY_DIR}/pytorch_qnnpack") set_property(TARGET pytorch_qnnpack PROPERTY POSITION_INDEPENDENT_CODE ON) ... endif() list(APPEND Caffe2_DEPENDENCY_LIBS pytorch_qnnpack) endif()即:PyTorch 将内嵌于 ATen 目录的 QNNPACK 以静态库方式编译(关闭其独立测试与基准,避免与主构建重复依赖),开启 PIC 后并入 PyTorch 主库。同时 Dependencies.cmake 第 336 行起 还统一协调了 NNPACK 家族的共享依赖(cpuinfo、FP16、FXdiv、PSimd、pthreadpool),确保 QNNPACK/XNNPACK/NNPACK 使用同一份源码版本,避免版本漂移;MSVC 环境下则自动关闭USE_PYTORCH_QNNPACK(第 381-384 行),这解释了 QNNPACK 实际面向 Android/iOS/Linux/macOS 等平台的现实。
七、微内核架构:按 CPU 指令集动态裁剪
QNNPACK 性能的核心在于"微内核(micro-kernel)"分层。从 CMakeLists.txt 的源文件分组 可以看到清晰的按指令集分层:
- 标量内核(
SCALAR_UKERNELS):如src/u8lut32norm/scalar.c、src/x8lut/scalar.c,任何平台兜底可用; - PSimd 内核(可移植 SIMD,SSE2/NEON 通用 C 实现):如
src/sgemm/6x8-psimd.c; - x86 SSE2 内核:如
src/q8gemm/4x4c2-sse2.c、src/q8conv/4x4c2-sse2.c、src/u8maxpool/16x9p8q-sse2.c; - ARM NEON C 内核:如
src/q8gemm/4x8-neon.c、src/q8dwconv/mp8x25-neon-per-channel.c(per-channel 深度卷积); - AARCH32/AARCH64 汇编内核(
.S文件):如src/q8gemm/8x8-aarch64-neon.S、src/q8conv/8x8-aarch64-neon.S,以及 32 位 ARM 的 FP16 半精度 GEMMsrc/hgemm/8x8-aarch32-neonfp16arith.S。
CMake 依据CMAKE_SYSTEM_PROCESSOR(或 iOS 下的IOS_ARCH)追加对应分组的编译单元,并为各分组附加合适的编译标志:NEON C 内核-O2 -marm -mfpu=neon(armv7)、-O2(arm64),SSE2 内核-O2 -msse2(第 298-319 行);初始化源文件额外使用-Os以压缩体积。这种"编译期按架构裁剪 + 运行时 cpuinfo 探测"的组合,正是它能在低端 ARM 手机上保持高性能、同时在无 NEON 的老设备上安全降级的原因。
从微内核命名(q8gemm、q8conv、q8dwconv、q8avgpool、q8gavgpool、u8maxpool、u8clamp、x8zip等目录)也能读出其算子与数据类型的映射:q8前缀表示量化 8 位有符号(零点位偏移),u8表示无符号 8 位(池化、clamp 不需要零点位),x8表示纯字节搬运(如 channel shuffle 的 zip 操作)。
八、测试与基准:如何验证构建产物
README 虽未展开测试细节,但仓库为每一层都提供了可执行的验证手段,这也是判断一次交叉编译是否正确的实用方法:
- 算子级单元测试:
test/目录下的convolution.cc、deconvolution.cc、fully-connected.cc、max-pooling.cc、softargmax.cc等,CMake 将其注册为convolution-test等 ctest 目标(CMakeLists.txt 第 459-602 行); - 微内核级单元测试:
test/q8gemm.cc、test/q8conv.cc、test/q8dwconv.cc、test/requantization.cc、test/u8maxpool.cc等,覆盖每个向量化内核的数值正确性(第 604-748 行); - 微基准:
bench/下convolution.cc、q8gemm.cc、softargmax.cc等,构建为convolution-bench等 Google Benchmark 可执行文件(第 750-869 行); - Android 设备侧验证:scripts/test-android-arm64.sh、test-android-armv7.sh、test-android-x86.sh 用于把构建产物推送到对应 ABI 的设备上执行测试。
九、许可证与致谢
QNNPACK 采用 BSD 许可证,许可证文本见 aten/src/ATen/native/quantized/cpu/qnnpack/LICENSE(即 README 中引用的LICENSE文件)。README 同时致谢了核心开发者 Marat Dukhan、Yiming Wu、Hao Lu、Bert Maher,以及开发过程中提供建议的 Andrew Tulloch 与 Yangqing Jia。
十、小结
- QNNPACK 是 PyTorch CPU 量化推理路径下的低层 8 位量化算子库,覆盖卷积(2D/3D,含组卷积与反卷积)、全连接(含稀疏变体)、池化、通道混洗与常用激活(Sigmoid/TanH/LeakyReLU/Hardsigmoid/Hardswish/Clamp/SoftMax),Group Normalization 与 Locally Connected 仍处于计划状态;
- 构建统一走 CMake:本机用 scripts/build-local.sh;Android 设
$ANDROID_NDK后按 ABI 选择build-android-{armv7,arm64,x86}.sh,armv7 目标注意不要开启ANDROID_ARM_NEON;iOS 设$IOS_CMAKE_TOOLCHAIN_FILE(ios-cmake 工具链)后按build-ios-{armv7,armv7s,arm64,arm64e,i386,x86_64}.sh选择; - 集成层面,PyTorch 通过
USE_PYTORCH_QNNPACK将 QNNPACK 以静态库并入主库(cmake/Dependencies.cmake),运行时经 initQNNPACK() 完成一次性初始化,各量化算子按"create → setup → run → delete"四步使用其 C API; - 性能根基是指令集分层的微内核体系(标量 / PSimd / SSE2 / NEON C / ARM 汇编),由 CMake 在编译期按目标架构裁剪,配合 cpuinfo 运行时探测,保证从高端 arm64 到无 NEON 的 32 位 ARM 设备都有合理的行为。
【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考