把分类程序跑上 J6E 后,工具报告 ResNet18 推理只需要约 0.6 毫秒,但从 JPEG 开始识别一张图片,实际却要接近一秒。
前者主要记录模型推理;后者还包含图片处理、模型加载、内存准备和结果读取。我分开计时,再只改变模型的加载方式,做了三轮完整 JPEG 对照。
结果是:每张图重新加载模型时,轮均值约 977—1010 毫秒;模型保持加载、连续处理图片时,轮均值约 26—34 毫秒。两条路径的输入和原始输出逐字节一致。
这次快起来的原因很简单:连续识别时,不再为每张图片重新做一遍昂贵的初始化。
一、时间花在哪里
官方工具单线程、单核的三轮 1000 帧测试,推理平均时间分别约 0.582、0.584、0.578 毫秒。这里已经加载好模型,也没有从 JPEG 开始处理图片。
而最初的图片程序每处理一张图,就启动一次 C++ 执行器。执行器先加载 HBM、创建模型和 tensor 缓冲,再执行任务,最后释放资源退出。下一张图进来,又重复整套流程。
对于只测一张图的示例,这样写很直观;但把它放进图片循环,模型初始化就变成了每帧成本。后面的 socket 对照里,逐帧重载模式的“通信与 UCP 服务”阶段轮均值约 944—984 毫秒,包含重新加载、缓冲准备、执行和释放。
另行记录的服务首次模型与缓冲初始化约为 1.35—1.83 秒。它发生在接收图片之前,未计入每图耗时;不能把这个冷启动数字当成每一帧的重载成本。
所以我没有先去调线程数,而是把模型生命周期从“每张图片”移到“整个处理会话”。
二、修改的核心:模型留着,任务逐帧处理
修改后的结构是:
任务仍然按帧创建和释放。复用的是模型和 tensor 缓冲,而不是把上一张图片的任务或输出当成下一张的结果。
为了把图片处理与 C++ UCP 执行连接起来,我写了一个私有本地 Unix socket 服务。Python 每次发送 150528 字节 INT8 输入,服务完成推理后返回 4000 字节有效 INT32 输出。SDK 日志写入单独文件,避免混进二进制结果。
三、这次怎样让两个版本可比?
只比较旧命令行程序和新服务,不容易知道快在哪里:进程启动、通信方式和文件读写也会一起变化。因此正式对照中,两个模式都保留同样的 socket 通信,只切换模型生命周期。
使用固定的 30 张 JPEG,每种模式运行三轮。每轮先处理三张预热图片,再计量 30 张;第二轮交换两个模式的执行顺序。服务启动独立记录,输入输出校验和结果落盘放在计时区间之外。
每张图的计时范围是:读文件、JPEG 解码与 RGB 转换、相同缩放裁剪和量化、socket 传输及 UCP 服务、输出读取和 top-5 解码。这里没有包括图片绘制与保存。
轮次 | 每图重载,平均毫秒 | 模型常驻,平均毫秒 |
|---|---|---|
1 | 982.769 | 34.369 |
2 | 977.273 | 26.451 |
3 | 1010.288 | 26.582 |
两种模式共计 180 次计时识别,生成的 INT8 输入与冻结参考一致,取回的 INT32 输出也全部一致。常驻版本没有靠换模型、改图片尺寸或跳过输出解码来换速度。

图1|左图使用对数纵轴。右图展示常驻版本三轮的平均阶段耗时;启动、校验与落盘不计入这些柱子。
三轮平均值合起来,每图重载约 990.1 毫秒,常驻约 29.1 毫秒,约相差 34 倍。这是当前完整图片流程的对照结果,变化主要来自省掉逐帧模型初始化。
四、模型常驻以后,新的瓶颈在哪里?
常驻流程的阶段均值如下。
阶段 | 三轮平均每图毫秒 |
|---|---|
读文件、JPEG 解码与 RGB 转换 | 22.90 |
缩放、裁剪、归一化、量化 | 5.10 |
socket 通信与 UCP 服务 | 0.99 |
输出解码 | 0.13 |
模型推理已经很短,图片处理反而占了大部分时间。这也解释了为什么官方工具中的约 0.6 毫秒,不能直接换算成这个 Python 图片应用的每秒处理张数。
第一轮常驻的读图与解码阶段比后两轮慢,完整流程均值也更高。这个阶段从打开文件计到 RGB 图片生成,包含文件读取与颜色转换,不是解码器内部的纯计算时间。我保留三轮原始数据,没有只选最快的一轮。当前实验没有单独控制全部缓存和系统调度因素,因此这里报告观测到的轮次范围。
五、换个图片库,会不会继续变快?
发现 JPEG 解码占比高后,我补做了一轮对照:保持后面的 PIL 缩放、裁剪、量化和常驻 UCP 服务不变,只把 JPEG 解码从 Pillow 换成 OpenCV,再转换成 RGB/PIL 图片接回原流程。OpenCV 设为单线程。
同样是每个解码器三轮、每轮三张预热和 30 张计时图片。180 次输入与原始输出仍然完全一致,但结果没有变快:
解码路径 | 三轮完整流程均值,毫秒 |
|---|---|
Pillow | 34.110、26.463、26.343 |
OpenCV → RGB/PIL | 44.670、45.147、44.244 |
在这份环境和接法里,OpenCV 路径更慢,所以最终保留 Pillow。这个测试比较的是两条具体解码接入路径,不能据此概括某个图片库普遍更快或更慢。
有了分阶段计时,优化就可以这样推进:先找当前占比最高的环节,改动一处,再同时检查结果和耗时。一次尝试没有改善,也应保留下来,避免以后重复走同一条路。
六、代码和结果怎样复用?
这份服务面向当前 ResNet18 的固定输入输出。读者接入自己的模型时,先核对 tensor 属性和预处理约定,再把模型加载与缓冲分配放到会话初始化,把每帧的数据更新、缓存操作和任务执行留在循环内。
这次把“模型推理快”和“图片应用快”区分开了。第一次实际瓶颈是逐帧加载模型,改完之后瓶颈转到 JPEG 解码。接着沿着同一份阶段表继续推进,不用围绕一个 BPU 时间猜测。
参考资料
- 地平线 OE 3.9.1 SDK:samples/ucp_tutorial/deps_aarch64/ucp/include/hobot/hb_ucp.h 与 hobot/dnn/hb_dnn.h,用于模型加载、缓存内存及任务接口核对。
- 官方 hrt_model_exec 源码:samples/ucp_tutorial/tools/hrt_model_exec/src/util/function_util.cpp 与 include/util/common_util.h,用于核对性能计时范围。
- 本次实验材料:jpeg_resident_server.cpp、resident_session.hpp、两种 JPEG 客户端,以及 jpeg_socket_summary.json、jpeg_decoder_summary.json 和逐图运行记录。
