CANN opbase 框架算子 CopyToNpu:Host 侧数据到 Device 侧的异步拷贝接口详解
2026/9/19 10:42:52 网站建设 项目流程

CANN opbase 框架算子 CopyToNpu:Host 侧数据到 Device 侧的异步拷贝接口详解

【免费下载链接】opbase本项目是CANN算子库的基础框架库,为算子提供公共依赖文件和基础调度能力。项目地址: https://gitcode.com/cann/opbase

导读

CopyToNpu是 CANN opbase 开源算子库中 framework_op 系列框架算子之一,用于在 L2 算子接口的开发过程中,将 Host 侧张量数据以异步方式拷贝到 Device 侧,并把该拷贝任务挂入算子执行器(aclOpExecutor)的任务队列,随算子一起调度执行。本文将以 CopyToNpu.md 为核心,结合仓库中的头文件、实现源码与单元测试,讲解该接口的函数原型、参数语义、返回值、约束限制、内部实现原理及典型调用流程,帮助读者掌握在自定义算子开发中安全、高效地完成 Host→Device 数据搬运的完整方法。

函数原型与接口定位

头文件与命名空间

CopyToNpu的声明位于 framework_op.h:

namespace op { const aclTensor* CopyToNpu(const aclTensor* src, aclOpExecutor* executor); const aclTensor* CopyToNpuSync(const aclTensor* src, aclOpExecutor* executor); aclnnStatus CopyNpuToNpu(const aclTensor* src, const aclTensor* dst, aclOpExecutor* executor); } // namespace op

从源码结构可以看到,framework_op.h中总共声明了三个框架级拷贝算子,构成一个完整的拷贝工具族:

接口数据流向同步/异步返回类型
CopyToNpuHost → Device异步(入队执行)const aclTensor*
CopyToNpuSyncHost → Device同步(阻塞至完成)const aclTensor*
CopyNpuToNpuDevice → Device异步(入队执行)aclnnStatus

其中CopyToNpu是本文的讲解核心,另两个接口可作为对照参考。三者均属于op命名空间,开发者在使用时需要显式引入opdev/framework_op.h头文件。

完整函数原型

const aclTensor *CopyToNpu(const aclTensor *src, aclOpExecutor *executor)

该接口位于src/nnopbase/composite_op/aclnn_engine/z_framework_op.cpp中实现。由于 RTS(Runtime Service)层的内存拷贝任务无法被缓存,实现中特意注释说明在走 memcpy 路径时放弃算子缓存("because rts memcpy cannot be cached, so abandon cache when use memcpy"),这一点是理解该接口行为的关键前提。

参数与返回值语义

参数说明

参数输入/输出说明
src输入需要拷贝到 Device 侧的 Host 侧数据,以aclTensor形式封装;该张量的数据必须位于 Host 内存上(placement 为kOnHost
executor输入L2 接口中一阶段接口声明的算子执行器对象,即aclOpExecutor,拷贝任务将挂入该执行器的任务队列

关于src参数,实现中有明确的放置位置校验(见 z_framework_op.cpp):

OP_CHECK(src->GetPlacement() == op::TensorPlacement::kOnHost, OP_LOGE(ACLNN_ERR_INNER, "Get input param's placement:%d when expect kOnHost.", src->GetPlacement()), return nullptr);

即调用方必须确保src的放置位置是 Host 侧,否则函数会记录错误日志并直接返回nullptr

返回值说明

  • 任务创建成功:返回一个指向拷贝完成后 Device 侧数据的aclTensor(即新分配的dst张量);
  • 任务创建失败:返回nullptr

需要特别注意:CopyToNpu返回的dst张量并非由调用方直接持有 Device 内存,而是由执行器在内部通过AllocTensor分配,拷贝任务真正执行发生在后续调用执行器Run()之时。

约束说明

  • 入参指针不能为空srcexecutor均不能传入空指针;
  • src必须位于 Host 侧src->GetPlacement()必须等于TensorPlacement::kOnHost
  • 目标内存容量校验:实际执行拷贝前,实现会校验dst的字节数不小于src的字节数,否则报ACLNN_ERR_INNER错误(见源码 z_framework_op.cpp)。

内部实现原理

源码实现全景

CopyToNpu的完整实现位于 z_framework_op.cpp,核心流程可分为五个阶段:

  1. 缓存放弃与 DFX 打点:调用L0_DFX(CopyToNpu, src)记录可观测性信息,同时因为 memcpy 任务不可缓存而放弃缓存路径;
  2. Host 侧校验:校验src的 placement 为kOnHost
  3. 目标张量分配:调用executor->AllocTensor(...),依据src的存储形状、原始形状、数据类型、存储格式与原始格式,分配一个同规格的dst张量;
  4. 构建 KernelLauncher:创建CopyToNpuKernelLauncher对象,核心类型标识为op::NO_CALC(纯数据搬运、无需计算核),并注册操作类型CopyToNpuOpTypeId()
  5. 入队:通过executor->AddToKernelLauncherListCopyTask(...)将拷贝任务挂入执行器的任务队列,同时建立输入输出参数关系(OpArgList各一个元素)。
const aclTensor* CopyToNpu(const aclTensor* src, aclOpExecutor* executor) { // because rts memcpy cannot be cached, so abandon cache when use memcpy L0_DFX(CopyToNpu, src) OP_CHECK(src->GetPlacement() == op::TensorPlacement::kOnHost, ...); auto dst = executor->AllocTensor(src->GetStorageShape(), src->GetOriginalShape(), src->GetDataType(), src->GetStorageFormat(), src->GetOriginalFormat()); op::internal::ProfilingInfoId profilingInfoId; auto* launcher = new CopyToNpuKernelLauncher{CopyToNpuOpTypeId(), op::NO_CALC, profilingInfoId, executor, src, dst}; // 构建 srcArg / dstArg 两个 OpArg 并分别包装为长度为 1 的 OpArgList auto ret = executor->AddToKernelLauncherListCopyTask(CopyToNpuOpTypeId(), launcher, srcArgList, dstArgList, emptyArgList); OP_CHECK(ret == ACLNN_SUCCESS, ..., return nullptr); return dst; }

实际拷贝发生在 Launch 阶段

CopyToNpu返回时只完成了"任务创建与入队",真正的内存搬运发生在执行器调用Run()之后,由CopyToNpuKernelLauncher::Launch()完成。该类的定义同样位于 z_framework_op.cpp,关键逻辑如下:

aclnnStatus Launch() override { uint64_t dstNByptes = 0; uint64_t srcNByptes = 0; auto calcRet = CalcTensorNBytes(src_, srcNByptes); CHECK_RET(calcRet == ACLNN_SUCCESS, calcRet); calcRet = CalcTensorNBytes(dst_, dstNByptes); CHECK_RET(calcRet == ACLNN_SUCCESS, calcRet); OP_CHECK(dstNByptes >= srcNByptes, ...); auto ret = aclrtMemcpyAsync(dst_->GetData(), dstNByptes, src_->GetData(), srcNByptes, ACL_MEMCPY_HOST_TO_BUF_TO_DEVICE, executor_->GetStream()); OP_CHECK(ret == ACL_SUCCESS, ..., return ACLNN_ERR_RUNTIME_ERROR); return ACLNN_SUCCESS; }

底层调用了运行时库的aclrtMemcpyAsync,并以ACL_MEMCPY_HOST_TO_BUF_TO_DEVICE作为拷贝类型——该类型意味着数据先经过中间缓冲再落到 Device 内存,同时绑定执行器所在的流(executor_->GetStream()),从而保证拷贝任务与算子任务在同一流上按序执行。

字节数计算的溢出保护

CalcTensorNBytes(z_framework_op.cpp)负责计算张量字节数:取数据类型的大小(TypeSize)与存储形状元素数(GetStorageShape().GetShapeSize())相乘,全程使用ge::MulOverflow做乘法溢出保护,并针对小于 1 字节的数据类型(如 4-bit 类型)做位偏移换算。这保证了超大张量场景下字节数计算的正确性。

调用示例

官方示例

原文档 CopyToNpu.md 给出的最小调用示例为:

// Initialize a tensor on the host side and copy it to dst, which is a tensor on the device side. void Func(aclOpExecutor *executor) { int64_t myArray[10]; auto src = executor->ConvertToTensor(myArray, 10, DT_INT64); auto dst = CopyToNpu(src, executor); }

流程拆解如下:

  1. 在 Host 侧定义一个普通数组myArray[10],即待拷贝的数据源;
  2. 调用执行器的ConvertToTensor,把 Host 数组包装为aclTensor(此时数据仍位于 Host 内存,placement 为kOnHost);
  3. 调用CopyToNpu(src, executor)创建拷贝任务并入队,得到指向 Device 侧数据的dst张量。

结合源码的可运行扩展示例

参考单元测试 test_framework_op.cpp 的写法,一个更完整的"创建执行器 → 构造 Host 张量 → 拷贝 → 运行 → 释放"生命周期如下:

// 基于 opbase 测试框架的完整生命周期示例(对照 UT 用例编写) #include "opdev/framework_op.h" #include "opdev/make_op_executor.h" void CopyHostToDeviceDemo() { // 1. 创建算子执行器(对应 L2 接口一阶段) auto executor = CREATE_EXECUTOR(); // 2. 在 Host 侧构造数据,并包装为 aclTensor std::vector<float> value(10, 1); auto srcArray = executor.get()->AllocFloatArray(value.data(), value.size()); auto srcTensor = executor.get()->ConvertToTensor(srcArray, op::DataType::DT_FLOAT); // 3. 创建 Host -> Device 异步拷贝任务并入队 auto dstTensor = op::CopyToNpu(srcTensor, executor.get()); // 任务创建失败时返回 nullptr if (dstTensor == nullptr) { // 错误处理 return; } // 4. 释放执行器指针并运行,此时才真正执行内存拷贝 aclOpExecutor* executorPtr = nullptr; executor.ReleaseTo(&executorPtr); auto ret = executorPtr->Run(); // ret 应为 ACLNN_SUCCESS // 5. 清理 delete executorPtr; }

单元测试中的验证要点

仓库在 tests/nnopbase/ut/composite_op/test_framework_op.cpp 中提供了CopyToNpu对应的 UT 用例,可验证以下几点行为:

  • 返回值非空EXPECT_NE(dstTensor, nullptr)验证任务创建成功时返回有效张量;
  • 不可重复执行EXPECT_EQ(executorPtr->CheckLauncherRepeatable(), false)验证 memcpy 类拷贝任务不支持 Repeatable 特性(与源码中"放弃缓存"的设计一致);
  • 执行成功EXPECT_EQ(ret, ACLNN_SUCCESS)验证Run()返回成功;
  • 数据一致性:在CopyToNpuSyncTest中,用例构造 75 × 1024 × 256 个 float(共 75 MB 数据)进行同步拷贝,并用memcmp逐字节比对源数据与目标数据完全一致,验证了拷贝正确性。

与 CopyToNpuSync 的差异对比

CopyToNpuSyncCopyToNpu名字相近,但语义差别显著(对照实现见 z_framework_op.cpp 与文档 CopyToNpuSync.md):

对比维度CopyToNpuCopyToNpuSync
任务入队是,进入 executor 任务队列否,立即执行
阻塞行为异步,返回后未完成阻塞,直至拷贝完成
底层调用aclrtMemcpyAsync(异步)aclrtMallocWithCfg+aclrtMemcpy(同步)
内存管理由执行器统一管理调用方负责释放 Device 内存
返回值任务入队成功后返回dst拷贝完成后返回dst

CopyToNpuSync的实现还会先调用executor->AbandonCache(true)主动放弃缓存,并通过ACL_RT_MEM_ATTR_MODULE_ID属性(moduleId = 36,对应 AICPU)使用aclrtMallocWithCfg为数据分配高带宽内存(ACL_MEM_TYPE_HIGH_BAND_WIDTH),随后用aclrtMemcpy同步完成 Host→Device 拷贝。

总结

CopyToNpu是 CANN opbase 为 L2 算子接口开发者提供的最基础的 Host→Device 数据搬运原语:它把"校验 Host 侧放置位置 → 分配目标张量 → 构建拷贝 Launcher → 挂入执行器任务队列"这一整套流程封装为单个函数调用,底层通过aclrtMemcpyAsyncACL_MEMCPY_HOST_TO_BUF_TO_DEVICE在算子流上异步执行。理解它与CopyToNpuSync(同步、不入队、自管内存)、CopyNpuToNpu(Device→Device)的差异,能帮助开发者根据数据生命周期与同步需求选择正确的拷贝接口。相关声明与实现可进一步查阅 framework_op.h、z_framework_op.cpp 及单元测试 test_framework_op.cpp。

【免费下载链接】opbase本项目是CANN算子库的基础框架库,为算子提供公共依赖文件和基础调度能力。项目地址: https://gitcode.com/cann/opbase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询