已关闭
[Bug-Report|缺陷反馈]: 并发时多次分配 DVPP RR 写值设备内存泄露 #808
WuXuefeng创建于  8月11日关闭于  6 天前
WuXuefeng
8月11日 创建

Thanks for sending an issue! Please fill in the following template to help quickly solve your problem.

Describe the current behavior / 问题描述 (Mandatory / 必填)

Stream::GetDvppRRTaskAddr()(src/runtime/core/src/stream/stream.cc:4358)存在双重检查锁缺陷:
锁内缺少二次空检查,并发时多次分配 DVPP RR 写值设备内存。

  void* Stream::GetDvppRRTaskAddr(void)
  {
      if (dvppRRTaskAddr_ != nullptr) {   // 锁外无同步快速路径
          return dvppRRTaskAddr_;
      }
      const std::lock_guard<std::mutex> lock(dvppRRTaskAddrLock_);
      const rtError_t error = device_->Driver_()->DevMemAlloc(&dvppRRTaskAddr_, ...); // 锁内未
      二次检查
      ...
  }

触发路径:rtLaunchDvppTask → LaunchDvppTask → StarsLaunch(sqeHeader.reserved==1 且 DVPP 类 型)→ StarsLaunchDvppRRProcess → GetDvppRRTaskAddr()。多个线程并发向同一 DVPP 流提交任务时,
都会通过锁外空检查,各自执行一次 DevMemAlloc 并覆盖 dvppRRTaskAddr_,造成设备内存泄漏(只保留
最后一次分配)和对 dvppRRTaskAddr_ 的数据竞争。

真机复现

  • 平台:Ascend910(NPU 4),使用仓库构建的 9.1.0 runtime(build/src/runtime + build/src/acl/
    aclrt)加载含缺陷代码的库。

修复建议

在锁内获取锁后再次检查 dvppRRTaskAddr_,仅在仍为空时才分配:

  void* Stream::GetDvppRRTaskAddr(void)
  {
      if (dvppRRTaskAddr_ != nullptr) {
          return dvppRRTaskAddr_;
      }
      const std::lock_guard<std::mutex> lock(dvppRRTaskAddrLock_);
      // 二次检查:避免并发线程重复分配并覆盖,导致设备内存泄漏
      if (dvppRRTaskAddr_ != nullptr) {
          return dvppRRTaskAddr_;
      }
      const rtError_t error =
          device_->Driver_()->DevMemAlloc(&dvppRRTaskAddr_, DVPP_RR_WRITE_VALUE_LEN,
          RT_MEMORY_DEFAULT, device_->Id_());
      ...
  }

Environment / 环境信息 (Mandatory / 必填)

问题触发前提是:该硬件平台支持 DVPP 任务提交(RT_FEATURE_TASK_DVPP),并通过
rtLaunchDvppTask/rtMultipleTaskInfoLaunch 提交 reserved=1 的 DVPP(VPC/JPEG)任务,从而进入
StarsLaunchDvppRRProcess → Stream::GetDvppRRTaskAddr()。

910_B_93
GetDvppRRTaskAddr() 的“锁内缺二次检查”缺陷在以下分支均存在(逐分支核实源码):

  • 8.5.0(src/runtime/feature/src/stream/stream.cc)
  • 9.0.0(src/runtime/feature/src/stream/stream.cc)
  • 9.1.0(src/runtime/core/src/stream/stream.cc)
  • 9.2.0-beta.1

DVPP RR 写值缓冲区并发重复分配问题从 CANN Runtime 8.5.0 起贯穿到当前 master,凡启用 DVPP任务特性(上表 Ascend 平台)且并发向同一 DVPP 流提交任务即可触发。

Steps to reproduce the issue / 重现步骤 (Mandatory / 必填)

  • 测试程序:dvpp_test.cpp——aclrtSetDevice 后创建 DVPP 组、基于组创建流、分配
    cmdlLis 内存,构造 VPC sqe(type=12, reserved=1),8 个线程并发 rtLaunchDvppTask。
    线程(tid 9921/9923/9924)同时进入分配路径,DevMemAlloc 实际执行 3 次;多轮运行统计为2~3 次(竞争窗口所致),而正确行为应恒为 1 次。

dvpp_test.cpp:

#include <atomic>
#include <cstdio>
#include <cstring>
#include <thread>
#include <vector>
#include "acl/acl_rt.h"
#include "runtime/runtime/rt.h"
#include "runtime/runtime/rt_stars.h"
#include "runtime/rt_external_stars.h"
#include "runtime/rt_external_stars_define.h"

static void CheckR(const char* name, rtError_t e){ printf("[%s] ret=0x%x\n", name, e); }

int main()
{
   aclError ae = aclInit(nullptr);
   printf("[aclInit] ret=%d\n", ae);
   ae = aclrtSetDevice(0);
   printf("[aclrtSetDevice] ret=%d\n", ae);
   if (ae != ACL_SUCCESS) return 1;

   rtContext_t cur = nullptr;
   rtError_t e = rtCtxGetCurrent(&cur);
   CheckR("rtCtxGetCurrent", e);
   printf("current ctx=%p\n", cur);

   rtDvppGrp_t grp = nullptr;
   e = rtDvppGroupCreate(&grp, 0);
   CheckR("rtDvppGroupCreate", e);

   rtStream_t stm = nullptr;
   e = rtStreamCreateByGrp(&stm, 0, 0, grp);
   CheckR("rtStreamCreateByGrp", e);

   void* cmdList = nullptr;
   e = rtMalloc(&cmdList, 4096, RT_MEMORY_DEFAULT, 0);
   CheckR("rtMalloc-cmdList", e);
   printf("cmdList=%p\n", cmdList);

   rtStarsCommonSqe_t sqe;
   std::memset(&sqe, 0, sizeof(sqe));
   sqe.sqeHeader.type = 12;          /* VPC */
   sqe.sqeHeader.reserved = 1;       /* trigger DVPP RR write-value tasks */
   sqe.sqeHeader.wrCqe = 1;
   const uint64_t cmd = reinterpret_cast<uint64_t>(cmdList);
   sqe.commandCustom[12] = static_cast<uint32_t>(cmd & 0xFFFFFFFFULL);
   sqe.commandCustom[13] = static_cast<uint32_t>(cmd >> 32);

   constexpr int kThreads = 8;
   std::atomic<int> ok{0};
   std::atomic<uint32_t> ready{0};
   std::atomic<bool> go{false};
   std::vector<std::thread> ts;
   ts.reserve(kThreads);
   for (int i = 0; i < kThreads; ++i) {
       ts.emplace_back([&]() {
           rtCtxSetCurrent(cur);   /* bind current context to this thread */
           ready.fetch_add(1, std::memory_order_release);
           while (!go.load(std::memory_order_acquire)) std::this_thread::yield();
           const rtError_t r = rtLaunchDvppTask(&sqe, sizeof(sqe), stm, nullptr);
           if (r == RT_ERROR_NONE) ok.fetch_add(1);
           printf("  launch ret=0x%x\n", r);
       });
   }
   while (ready.load(std::memory_order_acquire) < kThreads) std::this_thread::yield();
   go.store(true, std::memory_order_release);
   for (auto& t : ts) t.join();
   printf("launch OK count=%d/%d\n", ok.load(), kThreads);

   e = rtStreamSynchronize(stm);
   CheckR("rtStreamSynchronize", e);

   if (cmdList) rtFree(cmdList);
   if (grp) rtDvppGroupDestory(grp);
   if (stm) rtStreamDestroy(stm);
   aclrtResetDevice(0);
   aclFinalize();
   printf("done\n");
   return 0;
}

Describe the expected behavior / 预期结果 (Mandatory / 必填)

测试程序:dvpp_test.cpp——aclrtSetDevice 后创建 DVPP 组、基于组创建流、分配
cmdlLis 内存,构造 VPC sqe(type=12, reserved=1),8 个线程并发 rtLaunchDvppTask。
线程(tid 9921/9923/9924)同时进入分配路径,DevMemAlloc 实际执行 3 次;多轮运行统计为2~3 次(竞争窗口所致),而正确行为应恒为 1 次。

运行结果

  • 平台:Ascend910B(soc Ascend910_9362),加载仓库构建的 9.1.0 runtime。
  • aclInit / aclrtSetDevice 成功,DVPP 组、DVPP 流、cmdList 内存全部创建成功。
  • 8 个线程并发 rtLaunchDvppTask 全部成功(launch OK count=8/8)。

关键日志(复现 bug 证据)

同一 stream_id=47 下,Stream::GetDvppRRTaskAddr()(stream.cc:4375)被多个线程同时进入分配路
径,执行了多次 DevMemAlloc:

[INFO] RUNTIME(13799,repro4):...[stream.cc:4375] 13807 GetDvppRRTaskAddr: stream_id=47
[INFO] RUNTIME(13799,repro4):...[stream.cc:4375] 13813 GetDvppRRTaskAddr: stream_id=47

运行统计:GetDvppRRTaskAddr 分配次数 = 2(线程 13807、13813 同时通过锁外空检查各自分
配);正确行为应为 1 次。多轮运行该值在 2~3 之间波动,符合竞争窗口特征。

Special notes for this issue/备注 (Optional / 选填)

likedislike
WWuXuefeng
8月11日 关联了pull request:fix: Stream::GetDvppRRTaskAddr双重检查锁补充锁内二次检查(#808)
ykl999
ykl999成员
8月11日 评论:

/assign @xiao_cun

likedislike
CANN-robotCANN-robot成员
8月11日 将 xiao_cun 设为负责人
ykl999
ykl999成员
8月11日 评论:

感谢指出问题,我们会尽快分析并且fix相关问题

likedislike
xiangyang0909xiangyang0909成员
8月11日 将 xiangyang0909 设为负责人
xiangyang0909
xiangyang0909成员
8月11日 评论:

感谢提供这么完整的复现和日志。我们按你给出的调用链核对了当前 master 以及相关维护分支,初步分析,确认这个问题成立。

GetDvppRRTaskAddr() 目前只对分配过程加了锁。首次并发调用时,多个线程可能都先在锁外看到地址为空,之后再依次进入临界区重复分配。后一次分配会覆
盖前一次地址,而 Stream 销毁时只能释放最后保存的地址,因此会造成设备内存泄漏。

这个问题我们会按缺陷处理,修复进展后续在 issue 里同步。

likedislike
xiangyang0909xiangyang0909成员
24 天前 将 xwxf 设为负责人
xiangyang0909xiangyang0909成员
24 天前 移除了负责人 xiangyang0909
xiangyang0909xiangyang0909成员
24 天前 移除了负责人 xiao_cun
WWuXuefeng
9 天前 关联了pull request:fix: Stream::GetDvppRRTaskAddr彻底消除并发重复分配与数据竞争(#808)
ykl999
ykl999成员
6 天前 评论:

问题已提pr自修复,issue关闭处理

likedislike
ykl999ykl999成员
6 天前 issue状态由 进行中 改变为 已解决
ykl999ykl999成员
6 天前 关闭了 issue