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中总共声明了三个框架级拷贝算子,构成一个完整的拷贝工具族:
| 接口 | 数据流向 | 同步/异步 | 返回类型 |
|---|---|---|---|
CopyToNpu | Host → Device | 异步(入队执行) | const aclTensor* |
CopyToNpuSync | Host → Device | 同步(阻塞至完成) | const aclTensor* |
CopyNpuToNpu | Device → 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()之时。
约束说明
- 入参指针不能为空:
src与executor均不能传入空指针; src必须位于 Host 侧:src->GetPlacement()必须等于TensorPlacement::kOnHost;- 目标内存容量校验:实际执行拷贝前,实现会校验
dst的字节数不小于src的字节数,否则报ACLNN_ERR_INNER错误(见源码 z_framework_op.cpp)。
内部实现原理
源码实现全景
CopyToNpu的完整实现位于 z_framework_op.cpp,核心流程可分为五个阶段:
- 缓存放弃与 DFX 打点:调用
L0_DFX(CopyToNpu, src)记录可观测性信息,同时因为 memcpy 任务不可缓存而放弃缓存路径; - Host 侧校验:校验
src的 placement 为kOnHost; - 目标张量分配:调用
executor->AllocTensor(...),依据src的存储形状、原始形状、数据类型、存储格式与原始格式,分配一个同规格的dst张量; - 构建 KernelLauncher:创建
CopyToNpuKernelLauncher对象,核心类型标识为op::NO_CALC(纯数据搬运、无需计算核),并注册操作类型CopyToNpuOpTypeId(); - 入队:通过
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); }流程拆解如下:
- 在 Host 侧定义一个普通数组
myArray[10],即待拷贝的数据源; - 调用执行器的
ConvertToTensor,把 Host 数组包装为aclTensor(此时数据仍位于 Host 内存,placement 为kOnHost); - 调用
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 的差异对比
CopyToNpuSync与CopyToNpu名字相近,但语义差别显著(对照实现见 z_framework_op.cpp 与文档 CopyToNpuSync.md):
| 对比维度 | CopyToNpu | CopyToNpuSync |
|---|---|---|
| 任务入队 | 是,进入 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 → 挂入执行器任务队列"这一整套流程封装为单个函数调用,底层通过aclrtMemcpyAsync与ACL_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),仅供参考