
1. 从开机到第一次 NPU 推理M4C-Hat 与 Pulsar2 环境搭建1.1 为什么要在边缘端折腾 NPU我第一次拿到 M4C-Hat 这块板子的时候心里想的其实很简单不就是一块带 NPU 的开发板吗插上电、装个系统、跑个模型能有多难。结果从开机到真正跑通第一次 NPU 推理中间踩的坑比我预想的多得多。这篇文章就是把我整个环境搭建的过程完整记录下来包括 Pulsar2 工具链的安装、ONNX 模型的准备、量化编译的细节以及最后用 AXEngine 跑通推理的完整链路。先说清楚这套东西是干什么的。M4C-Hat 是一块面向边缘计算场景的加速板卡核心卖点是集成了 NPU神经网络处理单元能够在低功耗条件下完成神经网络推理任务。Pulsar2 是配套的模型编译工具链负责把训练好的模型通常是 ONNX 格式转换成 NPU 能执行的离线模型。AXEngine 则是板端的推理引擎负责加载编译好的模型并执行推理。这套组合解决的核心问题是在边缘设备上跑深度学习模型既要保证推理速度又要控制功耗和成本。适合谁看如果你手上有 M4C-Hat 或者类似的 NPU 开发板想把 PyTorch 训练出来的模型部署上去那这篇内容基本能帮你把整条链路走通。如果你只是好奇 NPU 推理是怎么回事也可以跟着了解一下整个流程。我下面会按照实际操作的顺序来讲从环境准备、工具链安装、模型转换、量化编译到板端部署和推理验证每一步都会说清楚为什么这么做以及我踩过的坑。2. 环境搭建前的整体思路与方案选型2.1 整条链路的架构拆解在动手之前先把整条链路想清楚比上来就装工具要高效得多。这套 NPU 推理的流程本质上是一条“模型转换流水线”训练侧用 PyTorch 或者别的框架训练好模型导出为 ONNX 格式。ONNX 在这里扮演的是中间表示的角色相当于一个通用翻译把不同框架的模型统一成一种格式。编译侧用 Pulsar2 把 ONNX 模型读进来经过图优化、量化、算子映射等步骤编译成 NPU 能执行的离线模型文件。部署侧把编译好的模型放到 M4C-Hat 上用 AXEngine 加载并执行推理输入数据经过预处理后送入模型输出结果再做后处理。为什么要用 ONNX 做中间格式因为 Pulsar2 不直接吃 PyTorch 的模型文件它需要一种框架无关的表示。ONNX 就是这个角色PyTorch 有官方的导出接口社区工具也成熟遇到问题好排查。为什么需要量化NPU 的算力通常是为定点运算优化的浮点运算要么不支持要么效率很低。把 FP32 的模型量化成 INT8不仅能大幅减少模型体积还能显著提升推理速度。代价是精度会有一定损失但通过合理的校准策略大部分场景下精度损失可以控制在可接受范围内。2.2 工具链选型的几个关键考量Pulsar2 的安装方式有几种我建议直接用官方提供的 Docker 镜像或者 conda 环境。原因很简单Pulsar2 依赖的 Python 版本、CUDA 版本、各种编译工具版本都比较挑剔手动装很容易出现版本冲突。用 Docker 的好处是环境隔离不会污染你本机的 Python 环境用 conda 的好处是相对轻量适合不想装 Docker 的场景。我个人的选择是 conda 环境因为我的开发机已经有 CUDA 环境了再套一层 Docker 反而麻烦。但如果你是从零开始Docker 镜像会更省心。AXEngine 是板端运行的不需要在开发机上装。你只需要把编译好的模型文件和推理程序传到板子上就行。板子的系统通常是 Linux具体版本看你的板子出厂配置。2.3 版本匹配的坑这里要特别强调一点Pulsar2 的版本、ONNX 的版本、PyTorch 的版本这三者之间是有兼容性要求的。我一开始用 PyTorch 2.x 导出的 ONNX结果 Pulsar2 报了一堆算子不支持的错误。后来换成 PyTorch 1.12 导出问题就少了很多。所以我的建议是先确定 Pulsar2 的版本然后根据它的文档去反推 ONNX 和 PyTorch 的版本。不要反过来先装最新版的 PyTorch 再去适配 Pulsar2那样你会被版本问题折磨到怀疑人生。3. 开发机环境准备与 Pulsar2 安装实操3.1 基础依赖的安装先说一下我的开发机环境Ubuntu 20.04Python 3.8CUDA 11.6。这个组合是我试下来比较稳的。如果你用的是 Ubuntu 22.04 或者 Python 3.10可能会遇到一些依赖包不兼容的问题建议用 conda 创建一个 Python 3.8 的虚拟环境。创建虚拟环境的命令conda create -n pulsar2 python3.8 conda activate pulsar2然后安装基础依赖pip install onnx1.12.0 pip install onnxruntime1.12.0 pip install numpy1.23.5 pip install opencv-python4.7.0.72这里 onnx 和 onnxruntime 的版本要匹配不然会出现一些奇怪的报错。numpy 的版本也不要太新1.24 以上有些 API 变了会导致 Pulsar2 的某些脚本跑不起来。3.2 Pulsar2 的安装Pulsar2 的安装包通常是一个 whl 文件或者 tar 包具体获取方式看官方文档。安装命令大概是pip install pulsar2-xxx.whl安装完成后验证一下是否装好了pulsar2 --version如果能看到版本号输出说明安装成功。如果报错说找不到命令检查一下是不是虚拟环境没激活或者 pip 安装的路径不在 PATH 里。注意Pulsar2 安装过程中可能会编译一些 C 扩展需要你的系统有 gcc 和 g。如果报错说找不到编译器先装一下 build-essential。3.3 验证 Pulsar2 能否正常工作装完之后别急着跑模型先用一个简单的 ONNX 模型测试一下 Pulsar2 能不能正常读取和编译。可以自己用 PyTorch 导出一个简单的卷积网络import torch import torch.nn as nn class SimpleNet(nn.Module): def __init__(self): super(SimpleNet, self).__init__() self.conv nn.Conv2d(3, 16, 3, padding1) self.relu nn.ReLU() self.pool nn.AdaptiveAvgPool2d(1) self.fc nn.Linear(16, 10) def forward(self, x): x self.conv(x) x self.relu(x) x self.pool(x) x x.view(x.size(0), -1) x self.fc(x) return x model SimpleNet() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, simple_net.onnx, opset_version11)导出之后用 Pulsar2 的命令行工具试着编译一下pulsar2 build --input simple_net.onnx --output simple_net_output --config config.json如果这一步能跑通说明你的 Pulsar2 环境基本没问题。如果报错根据错误信息去排查通常是算子不支持或者版本不匹配的问题。4. ONNX 模型准备与量化编译全流程4.1 从 PyTorch 导出 ONNX 的正确姿势导出 ONNX 看起来简单但有几个细节不注意就会埋坑。第一opset_version 的选择。Pulsar2 对 opset 版本有要求通常建议用 11 或者 13。太新的版本可能有些算子 Pulsar2 还不支持太老的版本又可能缺少一些必要的算子。第二输入输出的动态维度。如果你的模型需要支持动态 batch size 或者动态输入尺寸导出的时候要指定 dynamic_axestorch.onnx.export( model, dummy_input, model.onnx, opset_version11, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}} )但要注意Pulsar2 对动态维度的支持是有限的。如果你的模型不需要动态 batch建议直接固定维度这样编译出来的模型效率更高。第三导出后的模型检查。用 onnxruntime 跑一下导出的模型确认输出和 PyTorch 的输出一致。这一步很重要因为有些算子导出后行为会变不检查的话后面编译出来的模型精度不对你都不知道是哪里出的问题。import onnxruntime as ort import numpy as np sess ort.InferenceSession(model.onnx) input_data np.random.randn(1, 3, 224, 224).astype(np.float32) output sess.run(None, {input: input_data}) print(output[0].shape)4.2 量化策略的选择与校准集准备量化是 NPU 推理的关键步骤。Pulsar2 支持几种量化模式常见的有FP32 不量化精度最高但速度最慢模型体积最大。一般只用于验证模型能否跑通。INT8 量化速度和体积最优但需要校准集来统计激活值的分布精度会有损失。混合量化对敏感层保持 FP16 或 FP32其他层用 INT8。精度和速度的折中方案。我一般先用 FP32 跑通整个流程确认模型能正常编译和推理然后再切换到 INT8 量化对比精度和速度。INT8 量化需要准备校准集。校准集的作用是让量化工具统计每一层激活值的动态范围从而确定量化的 scale 和 zero_point。校准集不需要标注但需要能代表真实数据的分布。通常准备 100 到 500 张图片就够了。校准集的准备要注意几点第一图片的预处理要和推理时一致包括 resize、归一化等。第二校准集的数据分布要尽量覆盖真实场景不要只用一类图片。第三校准集的 batch size 不要太大一般 1 或者 2 就行太大反而会影响统计的准确性。4.3 Pulsar2 编译配置文件的编写Pulsar2 的编译需要一个配置文件通常是 JSON 格式。这个配置文件决定了量化的方式、输入输出的格式、目标硬件的参数等。下面是一个典型的配置示例{ input: { name: input, shape: [1, 3, 224, 224], format: RGB, mean: [0.485, 0.456, 0.406], std: [0.229, 0.224, 0.225] }, output: { name: output, shape: [1, 10] }, quantize: { mode: int8, calibration_dataset: calib_data/, calibration_steps: 100 }, target: { hardware: M4C, npu_cores: 1 } }这里的 mean 和 std 要和训练时的预处理一致不然量化后的精度会掉得很厉害。calibration_steps 是校准的步数一般设成校准集的数量就行。编译命令pulsar2 build --input model.onnx --output model_output --config config.json编译过程可能会花几分钟到十几分钟取决于模型的大小和复杂度。编译完成后输出目录里会有编译好的模型文件通常是 .axmodel 或者类似的格式。4.4 编译结果的验证编译完成后Pulsar2 通常会提供一个模拟器或者验证工具让你在开发机上先验证一下编译后的模型精度。这一步很重要因为如果编译后的模型精度就有问题部署到板子上也是白搭。验证的方法通常是用同样的输入数据分别跑原始 ONNX 模型和编译后的模型对比输出的差异。如果差异在可接受范围内比如分类任务 top-1 差异小于 1%就可以进入部署阶段了。注意如果精度差异很大先检查校准集是否合理再检查配置文件的预处理参数是否正确。很多时候精度问题都是预处理不一致导致的。5. 板端部署与 AXEngine 推理验证5.1 M4C-Hat 的系统准备与连接M4C-Hat 出厂通常已经烧好了系统你需要做的是确认板子能正常启动并且和你的开发机在同一个网络里。板子的默认 IP 和登录方式看官方文档一般是通过串口或者 SSH 连接。连接上板子之后先确认一下系统的基本信息uname -a cat /etc/os-release然后检查一下 NPU 驱动是否正常加载ls /dev/ | grep -i npu如果能看到 NPU 相关的设备节点说明驱动没问题。如果没有可能需要手动加载驱动或者更新固件。5.2 模型文件的传输与目录规划把编译好的模型文件从开发机传到板子上可以用 scpscp -r model_output/ userboard_ip:/home/user/models/板子上的目录规划建议这样/home/user/models/存放编译好的模型文件/home/user/data/存放测试图片或者输入数据/home/user/bin/存放推理程序这样分开放后面调试的时候不会乱。5.3 AXEngine 推理程序的编写AXEngine 的推理程序通常是用 C 写的但也有一些 Python 的封装。如果你只是想快速验证用 Python 版本会更方便。下面是一个典型的推理流程import axengine as axe import numpy as np import cv2 # 加载模型 session axe.InferenceSession(model_output/model.axmodel) # 准备输入 image cv2.imread(data/test.jpg) image cv2.resize(image, (224, 224)) image image.astype(np.float32) / 255.0 image (image - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] image np.transpose(image, (2, 0, 1)) image np.expand_dims(image, axis0) # 执行推理 outputs session.run(None, {input: image}) print(outputs[0])这里要注意的是输入的预处理要和编译时配置文件里的一致。如果编译时配置了 mean 和 std推理时也要做同样的归一化。不然结果会完全不对。5.4 第一次推理的性能观测第一次跑通推理之后别急着庆祝先看一下性能数据。AXEngine 通常会提供推理耗时的接口你可以测一下单次推理的时间import time start time.time() for _ in range(100): outputs session.run(None, {input: image}) end time.time() print(Average inference time: {:.2f} ms.format((end - start) / 100 * 1000))这个数据能帮你判断 NPU 是否真的在加速。如果推理时间和 CPU 跑差不多那可能是 NPU 没被正确调用或者模型没有真正编译成 NPU 能执行的格式。6. 常见问题与排查技巧实录6.1 编译阶段的典型报错与解决问题一算子不支持报错信息通常是Unsupported operator: xxx。这说明 Pulsar2 不支持你模型里的某个算子。解决办法有几种一是换一个等价的算子实现比如用多个支持的算子组合出同样的功能二是把不支持的算子放到 CPU 上执行Pulsar2 通常支持混合执行三是升级 Pulsar2 版本看新版本是否支持。问题二量化精度掉太多如果 INT8 量化后精度掉得厉害先检查校准集。校准集太少或者分布不对都会导致量化参数估计不准。可以试着增加校准集的数量或者换一批更有代表性的数据。另外也可以尝试混合量化对敏感层保持 FP16。问题三编译时间过长大模型编译时间可能会很长这时候可以试着减少校准步数或者先用 FP32 模式编译验证流程再切换到 INT8。6.2 板端推理的常见异常问题一模型加载失败通常是模型文件路径不对或者模型文件损坏。检查一下文件是否完整传输路径是否正确。问题二推理结果全零或者乱码大概率是输入预处理不对。检查一下输入的 shape、数据类型、归一化参数是否和编译时一致。问题三推理速度慢先确认 NPU 是否被调用。有些推理框架默认用 CPU需要显式指定用 NPU。另外检查一下模型是否真的编译成了 NPU 格式有些情况下 Pulsar2 会回退到 CPU 执行。6.3 独家避坑技巧技巧一先用小模型跑通全流程不要一上来就拿 ResNet50 或者 YOLO 这种大模型去跑。先用一个简单的几层卷积网络把整个流程走通确认每个环节都没问题再换大模型。这样出问题的时候排查范围小很多。技巧二保存每个环节的中间结果ONNX 模型、编译后的模型、校准集、推理输入输出这些中间结果都保存下来。后面出问题的时候可以逐个环节对比快速定位问题出在哪。技巧三版本信息记录清楚Pulsar2 版本、ONNX 版本、PyTorch 版本、板子固件版本这些信息都记下来。后面遇到问题去查文档或者问人的时候这些信息能帮你快速找到答案。技巧四校准集不要用训练集校准集和训练集要分开。用训练集做校准量化参数会过拟合到训练集的分布导致在真实数据上精度下降。技巧五推理程序加异常处理板端推理程序一定要加异常处理比如模型加载失败、输入数据格式不对、推理超时等。不然程序崩了你都不知道是哪里出的问题。7. 从跑通到用好后续优化方向7.1 性能调优的几个切入点跑通第一次推理只是开始后面还有很多优化空间。第一个切入点是输入尺寸。NPU 对不同的输入尺寸效率不一样有些尺寸能更好地利用硬件资源。可以试着调整输入尺寸看推理时间的变化。第二个切入点是batch size。虽然边缘端通常 batch size 是 1但有些场景下适当增大 batch size 能提升吞吐量。不过要注意batch size 增大会增加内存占用板子的内存有限别设太大。第三个切入点是模型结构。有些算子在小模型上效率很高在大模型上反而成为瓶颈。可以试着替换一些算子看性能变化。7.2 精度与速度的平衡量化的本质是用精度换速度。不同的应用场景对精度的要求不一样。分类任务可能 top-1 掉 1% 也能接受但检测任务可能 mAP 掉 0.5% 就不行了。所以量化策略要根据具体任务来定。我的经验是先用 INT8 量化跑一遍看精度掉多少。如果掉得太多再尝试混合量化把敏感层挑出来保持 FP16。混合量化的配置通常需要在 Pulsar2 的配置文件里指定哪些层不量化具体写法看文档。7.3 多模型部署的注意事项如果你要在同一块板子上部署多个模型要注意内存管理。每个模型加载都会占用内存模型多了可能会 OOM。解决办法有几种一是按需加载用完就释放二是把多个模型合并成一个共享部分计算三是用模型量化进一步压缩体积。另外多模型部署时要注意 NPU 的调度。如果多个模型同时推理可能会互相抢 NPU 资源。可以用队列的方式串行执行或者根据优先级调度。7.4 实际项目中的经验总结我在实际项目里踩过最大的坑是忽略了预处理的一致性。训练时的预处理是 resize 到 256 再 center crop 到 224但推理时我直接 resize 到 224结果精度掉了 5 个点。后来把预处理改成和训练一致精度就回来了。所以预处理这一步一定要和训练时对齐不能想当然。另一个坑是校准集的选择。我一开始用了一组室内场景的图片做校准但实际部署的场景是室外结果量化后的模型在室外数据上精度很差。后来换了一组室外图片做校准问题就解决了。校准集一定要能代表真实部署场景的数据分布。还有一个坑是板子的散热。NPU 满载运行的时候发热量不小如果散热不好会触发降频推理速度会明显下降。所以如果你的应用需要长时间满载运行一定要考虑散热方案。这个内容后续还可以这样扩展一是尝试不同的量化策略对比精度和速度的 trade-off二是把推理程序封装成服务通过 HTTP 或者 gRPC 对外提供接口三是结合视频流做实时推理测试端到端的延迟。这些方向我后面会陆续整理出来。