分类程序跑出第一张图片时,很容易觉得工作已经完成。但把模型留在内存里连续处理以后,还有另一个问题:任务有没有逐帧释放?发生错误时,已经创建的模型、缓冲和任务怎样收尾?下一张正常图片还能不能继续运行?
前面的性能实验主要比较模型常驻与重新加载。这次我沿用同一个会话实现,增加了一份资源与恢复探针,在同一个进程内循环正常输入、制造分阶段失败、执行清理,再重新加载和运行正常输入。
首轮执行一万次正常推理和八十次故障后恢复,随后用三个新进程各复验五万次推理和八十次恢复。合计 16 万次正常推理的原始输出均与参考一致,320 次指定故障之后都能清理资源、重新打开模型并完成正常推理。
这些推理循环使用三十份固定输入,并非识别了 16 万张不同照片。我要检查的是:同一个进程连续工作时,任务有没有收尾;中途出错后,是否必须把整个程序关掉重来。
一、为什么要在同一个进程里测试?
如果每次错误后退出程序,再启动一个新进程,操作系统会替程序回收很多资源。这样只能说明重新启动可以运行,未必检查到会话实现是否正确释放了自己的资源。
正常执行仍使用固定 ResNet18 HBM、三十份正常 INT8 输入和已经核对过的原始输出参考。每次推理结束都比较 1000 个 INT32 元素对应的 4000 字节有效输出,并在释放任务后检查任务句柄是否已经清空。
环境为J6E MatrixB、OE 3.9.1、UCP 3.15.8 和 HBRT 4.11.11,程序由官方 GNU 12.2 ARM 工具链构建。实验使用前一轮检查过的进程内运行库日志过滤。本环境默认会输出 allocator ERROR;本轮采用日志过滤设置,相关报错的原因仍未查清,因此下面报告的是输出一致性和指定失败路径的恢复结果。
本轮程序处理的是已加载到内存的固定 tensor,不重复 JPEG 解码和图片绘制,检查范围集中在 UCP 会话与失败清理。
二、先把正常连续运行检查清楚
程序一次性读取三十份正常输入和参考输出,打开模型和输入输出缓冲,先预热一百帧,然后循环执行一万帧。
正常帧仍然执行输入缓存同步(clean)、创建任务、提交、等待、输出缓存失效处理(invalidate)、raw 比较,以及释放任务。会话保持加载,任务按帧释放。
每一千帧记录一次 RSS 和文件描述符数量;逐帧时间记录先保存在内存,结束循环后再写 CSV。全部一万帧的 raw 与对应参考一致,帧结束时任务句柄也均已清空。
连续运行观察 | 首轮记录 |
|---|---|
计量帧数 | 10000,另外预热 100 |
正常输入 | 30 份循环使用 |
有效 raw 比较 | 10000/10000 一致 |
帧结束任务句柄 | 10000/10000 已清空 |
十个 RSS 采样点 | 均为 24320 kB |
十个 FD 采样点 | 均为 18 |
RSS 是进程驻留内存,FD 是进程打开的文件描述符数量。本轮日志和结果文件也会占用 FD,计时记录也占用内存,所以这里关注连续采样是否增长,不把这些数值直接解释成模型内存大小。
三、四种错误,分别发生在哪个阶段?
四种失败分别发生在加载模型、核对接口、读取输入和检查结果时,覆盖了不同的资源状态。
故障 | 发生阶段 | 清理前已经有什么 |
|---|---|---|
模型文件不存在 | 模型初始化 | 初始化尚未成功 |
把 YOLO HBM 交给固定分类会话 | 查询输入输出数量 | packed model 已加载,但接口不匹配 |
输入文件只有一个字节 | 模型与缓冲准备后读文件 | 模型、输入输出缓冲已准备 |
给完成的任务故意提供错误 raw 参考 | 等待完成、读取输出后核对 | 模型、缓冲和已完成任务仍在 |
最后一种很重要。正常路径是在 raw 比较之后释放任务;如果比较失败直接抛异常,任务释放语句就不会走到。异常清理必须接住这个已经完成、但还未释放的任务。
故意提供错误参考,只改参考字节,不改模型输入,也不让硬件任务失败。它检查的是“任务已经成功,后面的应用检查失败”时怎样清理。
每次探针还核对异常原因,不能拿任意一个报错都算故障测试成功。例如接口错误必须来自输入输出数量检查,参考错误必须来自 raw 比较。

图1|故障参考使程序在 raw 核对处抛异常;此时任务已经完成,异常路径仍需释放任务,再释放缓冲与模型。
四、清理以后,立即恢复一次正常推理
每个故障之后,程序调用同一会话的清理函数,再检查应用保存的资源句柄是否清空。随后重新打开正确分类模型,执行一个正常输入,核对 raw,关闭会话。这个过程不更换 PID。
上面展示的是实验结构。实际 C++ 实现还记录每次异常信息、清理前是否仍持有任务,以及恢复后的 raw 核对结果。
下表是首轮四类故障各二十次的记录;后续三个进程分别重复相同的八十次故障与恢复,合计三百二十次。
故障 | 次数 | 符合预期的失败 | 正常推理恢复 | 清理前仍持有完成任务 |
|---|---|---|---|---|
缺失模型 | 20 | 20 | 20 | 0 |
模型接口不匹配 | 20 | 20 | 20 | 0 |
输入截短 | 20 | 20 | 20 | 0 |
完成任务后参考核对失败 | 20 | 20 | 20 | 20 |
合计 | 80 | 80 | 80 | 20 |
那二十个任务并没有靠进程退出释放:探针先检查到句柄仍在,清理成功后继续在同一进程执行正常推理。这是本轮比“报错后重新启动成功”多检查的一步。
五、内存没回到最初值,应该怎样解释?

图2|第一段按每1000帧采样;第二段在故障清理并完成正常恢复后采样。RSS、FD包含探针自己的数据结构与结果文件。
这 128 kB 值得保留,但仅凭 RSS 曲线还无法判断来自哪里:运行库缓存、分配器、动态库和探针自身都可能影响它。后段平台说明本轮没有持续逐次上涨,进一步定位泄漏仍需更细的分配记录和更长时间观察。
为避免只有一次观察,我随后用三个新的进程各补跑五万帧和八十次失败恢复。三轮共十五万帧,raw 逐帧一致;共二百四十次恢复也全部完成。
复验轮次 | 常驻帧数 | 故障后成功恢复 | 常驻阶段 RSS 范围,kB | 恢复后 RSS 范围,kB |
|---|---|---|---|---|
1 | 50000 | 80/80 | 24320—24320 | 26240—26368 |
2 | 50000 | 80/80 | 24320—24320 | 26240—26368 |
3 | 50000 | 80/80 | 24320—24384 | 26304—26432 |
三轮常驻阶段 FD 均为 18,恢复后均为 15;各轮最后二十个恢复采样点的 RSS 保持平台。第三轮常驻 RSS 增加了 64 kB,这个变化也保留在表中。扩大帧数还会增加探针自己的计时记录,不能直接用不同配置的 RSS 总量比较模型内存。

图3|三轮分别使用新的 PID;同一轮内故障与恢复保持同一 PID。曲线包含探针自身,不作为运行库内部泄漏归因。
合并首轮与三个独立进程的复验,本次计量了 16 万正常帧和 320 次故障后恢复,不含预热与小规模试跑。正常 raw 均与各自固定参考一致;四条指定失败路径能够清理并恢复,采样的 FD 没有逐次增长,RSS 在各轮观察末段保持平台。这些结果适合作为工程回归的一部分。
六、这份探针怎样复用?
这条命令依赖本轮完整工程、三十份固定输入与参考,以及对应运行库,不是下载单个源码即可执行的独立入口。第三个模型参数用来触发“分类会话拿到检测接口”的错误。
迁移到自己的程序时,可以先照着四个阶段列出资源状态:模型尚未打开、模型已打开、缓冲已分配、任务已完成。每种失败后都执行一次正常推理,并同时检查结果和句柄。相比直接照抄次数,这份阶段表更能帮助找到遗漏的释放路径。本轮覆盖的是四类应用侧失败及任务完成后的清理,尚未测试驱动故障、执行中超时或强制取消。
这轮工作让我把“跑通”再向实际工程推进一步:不仅看正常结果,还看失败发生时已经持有什么、怎样释放,以及释放后能不能继续工作。以后修改任务顺序、缓存操作或资源管理,这份探针可以和图片回归工具一起保留。
参考资料
- OE 3.9.1 模型推理应用开发指导,以及对应 UCP 任务、缓存内存与模型接口。
- 本轮 resident_session.hpp、recovery_probe.cpp、首轮一万帧及八十次恢复,以及三个新进程各五万帧的复验记录。
