Atlas 300V:(5)VDEC 通道内存泄漏实测报告
Atlas 300V VDEC 通道内存泄漏实测报告
一句话结论:在 Atlas 300V (Ascend 310P)上,「每轮新建 VDEC 通道 → 解码 → 销毁」会稳定泄漏约 97 MB / 次设备内存,且
aclvdecDestroyChannel无条件永久卡死;跨 CANN 8.5.0 / 8.5.2 / 9.0.1、驱动 25.5.2 / 26.1.1、固件 7.8 / 9.0、内核 5.15 / 5.10 五组组合全部逐项复现。升级软件栈解决不了这个问题,唯一可行路线是「不销毁 + 通道复用」或「进程级回收」。
0. 摘要(先看这里)
| 项目 | 实测结果 |
|---|---|
| 症状一 | 每次「新建通道 → 解码 60 帧 → 关流」净泄漏 +97.19 ~ +97.34 MB(B 帧源),无 B 帧源 +84.63 ~ +85.15 MB,线性累加、不回落 |
| 症状二 | aclvdecDestroyChannel 永不返回,即使零帧新建后立即销毁;线程停在内核驱动模块的 CQ 等待上,S 态、可 SIGKILL |
| 泄漏主体 | 每轮新建通道本身,不是尾帧、不是 ring 槽、不是 Stream(同通道复用 12 轮仅 +0.04 MB/轮) |
| 单卡寿命 | 21.5 GB 可用设备内存 ÷ 97 MB ≈ 206 次开合;1000 次开合外推约 97 GB |
| 版本矩阵 | 5 组 / 4 个软件维度 / 2 个内核主线全部复现,斜率极差 ≤0.52 MB/轮(噪声量级) |
| 结论 | 平台级固有行为,非版本回归;不能称为「某个 CANN 版本的 bug」 |
| 推荐方案 | ① 进程级 VDEC 服务 + 通道池(根治)② 不销毁通道 + 安全释放 + 开合预算(过渡)③ worker 进程池定期回收(兜底) |
1. 背景与术语
在昇腾 CANN 的媒体处理(DVPP)体系中,VDEC 是硬件视频解码模块,应用层通过 aclvdec* 系列接口使用:
acldvppCreateChannelDesc → aclvdecCreateChannel 创建解码通道(同步接口,内部创建 2 个 Stream 并订阅回调)
aclvdecSendFrame / aclrtProcessReport 送帧 + 回调取帧
aclvdecDestroyChannel → acldvppDestroyChannelDesc 销毁通道(同步接口,需等待已发送帧与回调完成)
一个「通道」是解码资源的载体。很多视频分析服务在多路流接入、断线重连、按任务切片的场景下,会自然写出「每路流 / 每次重连 / 每个任务 建一次通道、用完销毁」的生命周期。本文的问题正出在这条路径上。
涉及三层软件:CANN(用户态运行库与头文件)、HDK 驱动(内核模块)、设备固件(NVRAM,不随虚拟机快照回滚)。三者可独立更换,这也是本次矩阵测试能拆出四个维度的原因。
测试硬件:Atlas 300V (Ascend 310P),NPU DDR 21525 MB,单卡以 PCIe 直通方式独占给 KVM 虚拟机。测试素材:2560×1440 / 25 fps / H.264(含 B 帧)及其 -bf 0 重编码的无 B 帧版本。
2. 现象
2.1 症状一:内存按轮次线性上涨,不回落
用探针在同一进程内循环 12 次「新建通道 → 送 60 帧 → 关流」,每轮用 aclrtGetMemInfo 记录设备空闲内存:
cycle free_mb used_mb delta_mb path dev_frames read fallback
0 19982.8 1542.3 +144.07 vdec 60 60 0 ← 第 0 轮含初始化摊销,不计入斜率
1 19878.6 1646.4 +104.16 vdec 60 60 0
2 19774.9 1750.1 +103.70 vdec 60 60 0
3 19671.2 1853.9 +103.75 vdec 60 60 0
...
10 18980.8 2544.2 +104.41 vdec 60 60 0
11 18913.2 2611.8 +67.63 vdec 60 60 0
# 每轮平均净泄漏 = +97.23 MB(第 1..11 轮,11 个样本)
# 外推:1000 次开合 ⇒ 97233.0 MB
斜率是直线,没有随内存水位升高而加速,也没有在某一轮收敛。换成无 B 帧素材是 +85.12 MB/轮;换成软件解码(不建 VDEC 通道)对照是 −0.22 MB/轮——说明增长确实来自 VDEC 路径。
按起始可用约 20 100 MB 计算,约 206 次开合就会把 21.5 GB 设备内存吃满。对长驻视频服务来说,这不是「慢一点」的问题,而是必然 OOM。
2.2 症状二:销毁通道永久卡死
aclvdecDestroyChannel 不返回。取证方式是把每个变体放进 fork 出的子进程,父进程超时后读取 /proc/<pid>/task/*/{stack,wchan,stat} 再 SIGKILL:
[parent] A3 超过 25s 未返回 ⇒ 判定卡死,开始取证
[parent] tid=19183 state=S wchan=logic_ioctl_cq_wait
[parent] [<0>] logic_ioctl_cq_wait+0x7b1/0x1c60 [drv_devdrv_host]
[parent] [<0>] devdrv_ioctl+0x169/0x5d0 [drv_devdrv_host]
[parent] [<0>] tsdrv_comm_ioctl+0xc8/0x1d0 [drv_devdrv_host]
[parent] [<0>] drv_ascend_intf_ioctl+0x80/0x2d0 [drv_davinci_intf_host]
关键细节:
- S 态(可中断睡眠),不是 D 态:SIGKILL 能回收进程,设备内存随之归零,不需要重启机器;但反过来,这意味着等它自己超时返回是不可能的。
- 多线层栈里同时出现
hdcdrv_recv_peek_wait(host-device 通道等待)、futex_wait、nanosleep,主线程与另一线程都停在logic_ioctl_cq_wait。 - 零帧新建后立即销毁(变体 A3)同样卡死,因此「还有未解完的帧 / 回调没处理完」不是必要条件。
2.3 最小复现
// 伪代码:足以复现两个症状的最小循环
aclInit(nullptr);
aclrtSetDevice(0);
for (int cycle = 0; cycle < 12; ++cycle) {
auto *desc = acldvppCreateChannelDesc();
// 设置 codec / width / height / refFrameNum / 回调 / threadId
aclvdecCreateChannel(desc); // 不注册回调与 threadId 会直接返回 100000
aclrtCreateStream(&stream);
aclrtSubscribeReport(hostThreadId, stream);
for (int i = 0; i < 60; ++i) aclvdecSendFrame(...); // 送 60 个 Annex-B AU
// 等待回调 done == 60
aclvdecDestroyChannel(desc); // ← 永久卡死;注释掉它则每轮泄漏 ~97 MB
acldvppDestroyChannelDesc(desc);
aclrtGetMemInfo(ACL_HBM_MEM, &free, &total); // 记录
}
对应探针命令(探针为自写的 4 个独立小程序,互不共享状态):
# 斜率
ATLAS300V_ENABLE_VDEC=1 ./probe_vdec_leak --cycles 12 --frames 60
# 卡死取证(timeout 必须放在 sudo 内层,见 §8)
timeout -k 10 90 sudo env LD_LIBRARY_PATH="$LD_LIBRARY_PATH" \
./probe_vdec_destroy_hang --variant A3 --timeout 25
3. 根因定位(第一轮排查)
3.1 拆开 97 MB:泄漏主体是「新建通道」
用「不喂帧、只建通道」的零风险探针做分项累加:
| 实验 | 配置 | 每轮净增量 | 推论 |
|---|---|---|---|
| C1 | 只建通道,ring 4 / ref 8 | +48.35 MB | 通道 + 4 槽的成本 |
| C2 | 只建通道,ring 2 / ref 8 | +32.14 MB | 与 C1 相差 16.2 ÷ 2 ⇒ 每槽 ≈8.1 MB |
| C3 | 只建通道,ring 4 / ref 2 | +48.43 MB | 与 C1 相同 ⇒ RefFrameNum 不是决定因素 |
| C5 | 只创建 / 销毁 Stream ×12 | +0.04 MB | Stream 与订阅本身不泄漏 |
| 复用 | 同一通道连续喂 12 轮 | +0.04 MB/轮(无 B 帧 +0.01) | 泄漏主体就是「新建通道」 |
据此可以给出 97 MB 的构成:通道固定 ≈16 MB + 解码期内部池 ≈47 MB + 在途槽 4×8.1 ≈32 MB。这个数量级也与官方给出的单路 VDEC 内存参考值一致(1080p H.265 / ref8 约 58 MB,4K 最大参考约 182 MB)——即「每轮留下接近一个通道规模的内存块」是数量级合理、但不符合「销毁应归还」预期的行为。
复用实验还有一个重要发现:通道与创建线程强绑定。对已建通道调用
aclvdecSetChannelDescThreadId会失败,换线程后回调不再投递。所以「通道池」不能设计成「多个任务线程各取一个通道」,见 §6.1。
3.2 销毁路径:哪些步骤安全,哪一步必挂
| 变体 | 动作(在途 ≥1 帧) | 结果 |
|---|---|---|
| A1 | 只销通道,不 free 任何缓冲 | 卡死 |
| A3 | 零帧:建完立刻销通道 | 卡死,logic_ioctl_cq_wait+0x7b1 |
| A6 | A1 成功后再 free 全部在途槽 | 卡死(卡在 A1) |
| A2 | 只 acldvppFree 一个在途槽的 out_dev | 0 ms,rc=0 |
| A4 | 只销毁在途槽的 host 侧 desc | 0 ms,rc=0 |
| A5 | 只退订 + 销毁 Stream | 1 ms,rc=0 |
| A7/A8 | 换辅助线程销通道 | 8 ms 返回 500001(invalid param) |
| A14/A15/A16 | 清回调 / 不建 Stream / 先销 Stream 再销通道 | 全部卡死 |
结论:卡死与在途帧数、Stream、回调注册、调用顺序、调用线程都无关,是「销毁通道」这条 ioctl 本身在本平台不可完成。而 acldvppFree、desc 释放、退订并销毁 Stream 都是安全的——这条结论直接决定了 §6.2 的过渡方案能做到什么程度。
3.3 几个「想当然」的解释已被实测否证
| 猜测 | 否证证据 |
|---|---|
| 尾帧没冲刷干净,导致 destroy 一直等 | aclvdecSendSkippedFrame 连发 16 次,done 递增但 ring 在途恒为 4/8 不降;对 IPPP 顶到 0 槽后销毁仍然卡死 |
| 设备内存不足 → AI CPU 超时 → 回调不触发 | A3 零帧卡死时设备仅占用 1850 / 21525 MB,距耗尽差一个数量级;全程无 aicpu timeout / 0x28 日志 |
| 参考帧数配置太大 | ref 2 与 ref 8 的每轮增量相同(±0.08 MB) |
| 跨 Context / 跨线程销毁导致资源不归还 | A7/A8 跨线程直接返回 500001;A14/A15/A16 改变顺序仍卡死 |
| 进程退出后设备残留 | SIGKILL 后 npu-smi 显示无残留进程,内存归零 |
3.4 一次真实的代码修复:有效,但不够
把析构逻辑改成「只释放可证明安全的部分」:
- 有界泵回收已完成帧;
- 释放已归还的槽(
acldvppFree、pic/stream desc)——A2/A4 证明安全; aclrtUnSubscribeReport+aclrtDestroyStream——A5 证明安全;- 不调用
aclvdecDestroyChannel,把channel_置空以切断迟到回调入口。
顺带修掉了 4 个确定性缺陷(ring 扩容回滚漏回收、空输出指针、槽位匹配失败落到 0 号槽、RefFrameNum 硬编码)。结果:
| 素材 | 修复前 | 修复后 | 变化 |
|---|---|---|---|
| 含 B 帧 | +130.01 MB/轮 | +97.25 MB/轮 | −25% |
| 无 B 帧(IPPP) | +94.83 MB/轮 | +85.03 MB/轮 | −10% |
功能回归正常(150 帧视频跑通,解码均值 4.91 ms,零推理失败、零重连)。但预算只从约 154 次提升到约 206 次,依然不能进长驻服务。 这也是为什么要做下面这轮版本矩阵测试:得先确认「是不是某个软件版本坏了」。
4. 五组版本矩阵测试(本文重点)
4.1 实验设计
目标:判断这两个症状是不是某个软件层的版本缺陷,以及是否存在「destroy 正常 + 单次开合泄漏可接受」的可长驻组合。
- 单变量:每格只改一个维度(CANN / 驱动+固件 / OS+内核),其余全部冻结,包括代码、探针、素材。
- 串行执行:只有一张卡且独占直通,无法并行对照;每格全量重编译、跑完整套探针。
- 固件单向升高:设备固件写在卡上 NVRAM,不随虚拟机快照回滚,且旧驱动不认新固件。因此把「CANN 降级」排在「固件升级」之前,避免不可逆的回退组合。
- 判据先定死,再看数据:
| 判据 | 定义 |
|---|---|
| D1(挂死) | destroy_hang 退出码:0 = 全部变体正常返回 = 通过;2 = 超时但可杀 = 失败;3 = D 态不可杀 = 最严重失败 |
| D2(泄漏) | aclrtGetMemInfo 空闲内存增量,第 0 轮不计入斜率。≤5 MB/轮 = 根治(与复用基线同量级);≤20 MB/轮 = 可用(F0/1000,支撑 1000 次开合);>20 MB/轮 = 失败 |
4.2 五格矩阵
| 格 | OS / 内核 | CANN | 驱动 | 固件 | 主要作用 |
|---|---|---|---|---|---|
| A(基线) | Ubuntu 20.04.5 / 5.15 | 8.5.2 | 25.5.2 | 7.8.0.7.220 | 基线复现 + 内存水位观测 |
| D | 同上 | 8.5.0 | 25.5.2 | 7.8 | 检验「8.5.1/8.5.2 补丁回归」 |
| C | 同上 | 9.0.1 | 26.1.1 | 9.0.0.9.220 | 检验「CANN 大版本跃迁」 |
| B | 同上 | 8.5.2 | 26.1.1 | 9.0.0.9.220 | 检验「驱动 / 固件」 |
| G | openEuler 22.03 SP4 / 5.10 | 8.5.2 | 26.1.1 | 9.0.0.9.220 | 检验「OS / 内核主线」 |
G 格是一台全新安装的虚拟机(kickstart 无人值守装机),从驱动开始重装整套软件栈——排除了「原系统残留状态」这一干扰。
4.3 结果:五格几乎逐项重合
泄漏斜率(MB/轮)
| 格 | B 帧源 | 无 B 帧源 | chopen 分项 | stream_only 分项 |
|---|---|---|---|---|
| A | +97.23 | +85.12 | +51.48 | −0.17 |
| D | +97.26 | +84.71 | +51.42 | −0.16 |
| C | +97.34 | +85.15 | +52.02 | −0.16 |
| B | +97.19 | +84.63 | +51.23 | −0.17 |
| G | +97.19 | +85.11 | +51.42 | −0.15 |
| 极差 | 0.15 | 0.52 | 0.79 | 0.02 |
挂死行为
| 观测 | A / D / C / B(5.15) | G(5.10) |
|---|---|---|
destroy_hang A3 | exit 2 | exit 2 |
| 变体矩阵 | A1 / A3 / A6 卡死,A2 / A4 / A5 正常 | 完全同构 |
chopen_close | 60 s 超时,无任何 cycle 输出 | 同 |
| skipflush 16 次后 ring 在途 | 4/8 不降 | 4/8 不降 |
| 挂死内核栈 | logic_ioctl_cq_wait+0x7b1 [drv_devdrv_host] | logic_cq_wait_event+0x2a2 → logic_wait_cq_report+0x158 → logic_ioctl_cq_wait+0x11c [drv_devdrv_host] |
| 线程状态 | S 态,SIGKILL 可回收 | S 态,SIGKILL 可回收 |
最强的一条证据:跨 25.5.2 与 26.1.1 两个大版本驱动,挂死点在同一函数、同一偏移(+0x7b1)命中;换成 5.10 内核后函数链依然一致,只是因内核编译差异导致内联展开与偏移不同。这不是某版本引入的回归,而是两条发布线、两个内核主线上长期一致的行为。
4.4 有效性控制(这部分决定了结论能不能信)
跨格比较最怕「测了个寂寞」,所以每格都强制校验:
probe_vdec_leak的path列必须全为vdec;出现vdec->ffmpeg_fallback该次作废;- 回调完成帧数 == 发送帧数(
dev_frames=60、read=60、fallback=0),防空跑假阴性; - 素材三件套 md5 与基线逐字节一致(源文件、Annex-B 裸流、无 B 帧重编码);
- 三元组自证:CANN 软链指向、
npu-smi驱动 / 固件版本、内核版本逐项打印并核对; npu-smihealth=OK、单卡在位;全量重编译后ctest全绿(基线格与驱动升级格实测 16/16);- 第 0 轮不计入斜率;每格 11 个样本取均值。
G 格的两个测试程序因发行版依赖差异(gtest 1.8.1 无
GTEST_SKIP、spdlog 使用系统 fmt)编不过,ctest11/16 失败——但它们只是单元测试程序,四个 VDEC 探针二进制均正常构建并执行,不影响上述任何指标。这一点在证据包里有单独说明。
5. 结论
5.1 五个假设的判决
| ID | 假设 | 判决 | 依据 |
|---|---|---|---|
| H1 | 驱动层缺陷(特定版本) | 排除 | 25.5.2 与 26.1.1 同函数同偏移命中 |
| H2 | CANN 层缺陷 / 回调链断裂 | 排除 | 8.5.0 / 8.5.2 / 9.0.1 三条发布线无差异 |
| H3 | 8.5.x 补丁回归 | 排除 | D 格(8.5.0)与基线逐项等价 |
| H4 | 资源耗尽 → AI CPU 超时 | 排除 | 零帧即卡且设备仅占用 1850 / 21525 MB |
| H5 | OS 内核 / 直通环境特有 | 排除 | 全新 openEuler 5.10 + 全新装机,全部复现 |
5.2 该怎么表述这个结论
可以确认的是:在这张 310P 上,aclvdec* 通道销毁路径的 CQ 等待条件不被满足,且每次新建通道留下的约 97 MB 不会通过任何已测试的销毁手段归还;该行为不随 CANN、驱动、固件、内核版本变化。
需要克制的措辞:
- 现有证据不足以区分「驱动实现」「固件微码」「硬件逻辑」三者中哪一个是最终责任点,因此更准确的说法是平台级固有行为,而不是「某某版本的 bug」或「硬件缺陷」。
- 结论仅在本文的硬件型号、接口路径与调用方式下成立,不能外推到其它 Ascend 型号、其它解码接口或新版 SDK。
- 官方故障手册里「VDEC 解码异常导致进程卡死(
0x28 aicpu timeout)」是另一条触发路径(设备内存不足),本案已明确排除。
5.3 与公开资料的对照
检索公开渠道,未找到与「310P + 每轮建毁固定涨 83–130 MB + 线性累加」完全一致的报告,也没有找到官方缺陷编号或针对该现象的修复说明。最接近的公开记录有三类:
- 反复建毁 VDEC 通道后 event 资源耗尽(207007)(Gitee
Ascend/pytorch#I6WUNN); - 通道异常后
aclvdecDestroyChannel阻塞,官方回复承认该接口有同步等待(GiteeAscend/samples#I4TTM0); - 解码器释放后显存残留 237 MB、重建后继续增长(Gitee
ffmpeg-ascend#I9S7C4)。
官方文档的两个要点值得注意:
aclvdecCreateChannel明确写着「同一个通道可以重复使用,销毁后不再可用」;DVPP 总体说明也建议「根据实际场景管理通道,反复创建与销毁会影响业务性能」——这与本文实测的规避方向一致;aclvdecDestroyChannel「会等待已发送帧解码完成且用户回调函数处理完成后再销毁」——这能解释「有在途帧时不返回」,但解释不了零帧即卡,公开资料里也没有对零帧场景的说明。
换句话说:官方推荐的做法本来就是复用,而本文用五组数据说明了「不复用」在这张卡上的代价有多大。
6. 参考资料
官方文档
aclvdecCreateChannel(同一个通道可以重复使用):https://www.hiascend.com/document/detail/zh/canncommercial/800/apiref/appdevgapi/aclcppdevg_03_0330.htmlaclvdecDestroyChannel(等待已发送帧与回调完成):https://www.hiascend.com/document/detail/zh/canncommercial/800/apiref/appdevgapi/aclcppdevg_03_0331.html- DVPP 总体说明「关于通道的要求」:https://www.hiascend.com/doc_center/source/zh/canncommercial/80RC2/apiref/appdevgapi/aclpythondevg_01_0220.html
- VDEC 性能指标 / 每路内存消耗公式:https://www.hiascend.com/doc_center/source/zh/canncommercial/80RC2/apiref/appdevgapi/aclpythondevg_01_0338.html
- 310 → 310P 迁移指引(按默认参考帧数申请帧存):https://www.hiascend.com/doc_center/source/zh/canncommercial/60RC1/inferapplicationdev/aclcppdevg/aclcppdevg_000184.html
- VDEC 解码异常导致进程卡死(
0x28 aicpu timeout):https://www.hiascend.com/document/detail/zh/canncommercial/82RC1/maintenref/troubleshooting/troubleshooting_0181.html - 内存未释放导致内存泄漏(通用排查):https://www.hiascend.com/document/detail/zh/canncommercial/850/maintenref/troubleshooting/troubleshooting_0110.html
社区记录(部分相似,非同一问题)
- Gitee
Ascend/samples#I4TTM0:16 路解码、通道异常后aclvdecDestroyChannel阻塞:https://gitee.com/ascend/samples/issues/I4TTM0 - Gitee
Ascend/pytorch#I6WUNN:反复建毁 200 余次后 207007(event 资源不足):https://gitee.com/ascend/pytorch/issues/I6WUNN - Gitee
chou-shun/ffmpeg-ascend#I9S7C4:解码器释放后 237 MB 显存残留:https://gitee.com/chou-shun/ffmpeg-ascend/issues/I9S7C4 - 博客园《DVPP 问题汇总》(310P Context / 通道描述符释放经验):https://www.cnblogs.com/betterwgo/p/18398010
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐


所有评论(0)