博客算法工具链推理一毫秒,识别一秒?

推理一毫秒,识别一秒?

Sam Hui19小时前
6
0

把分类程序跑上 J6E 后,工具报告 ResNet18 推理只需要约 0.6 毫秒,但从 JPEG 开始识别一张图片,实际却要接近一秒。

前者主要记录模型推理;后者还包含图片处理、模型加载、内存准备和结果读取。我分开计时,再只改变模型的加载方式,做了三轮完整 JPEG 对照。

结果是:每张图重新加载模型时,轮均值约 977—1010 毫秒;模型保持加载、连续处理图片时,轮均值约 26—34 毫秒。两条路径的输入和原始输出逐字节一致。

这次快起来的原因很简单:连续识别时,不再为每张图片重新做一遍昂贵的初始化。

一、时间花在哪里

实验使用 J6E MatrixB,系统为 Debian 12,运行库为 OE 3.9.1 对应的 UCP 3.15.8、HBRT 4.11.11。模型沿用固定 ResNet18 HBM,输入为 INT8 [1,3,224,224],输出为 INT32 [1,1000]。

官方工具单线程、单核的三轮 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 日志写入单独文件,避免混进二进制结果。

服务有两个模式:reload 每个请求重新加载模型,reuse 在启动时加载一次。两种模式使用同一个服务实现,Python 侧也使用同一条 JPEG 处理流程。

三、这次怎样让两个版本可比?

只比较旧命令行程序和新服务,不容易知道快在哪里:进程启动、通信方式和文件读写也会一起变化。因此正式对照中,两个模式都保留同样的 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 输出也全部一致。常驻版本没有靠换模型、改图片尺寸或跳过输出解码来换速度。

alt text
图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。这个测试比较的是两条具体解码接入路径,不能据此概括某个图片库普遍更快或更慢。

有了分阶段计时,优化就可以这样推进:先找当前占比最高的环节,改动一处,再同时检查结果和耗时。一次尝试没有改善,也应保留下来,避免以后重复走同一条路。

六、代码和结果怎样复用?

材料中保留了 jpeg_resident_server.cpp、固定模型契约的会话实现、两种 JPEG 客户端以及 ARM 构建命令。target_jpeg_socket.sh 对应生命周期对照,target_jpeg_decoder.sh 对应解码器对照;逐图结果和服务端请求时间分别保存在 JSON 与 CSV 中。

这份服务面向当前 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 和逐图运行记录。
算法工具链
社区征文征程6
评论0
0/600