CANN opbase 基础张量操作接口(L0 层)详解:Cast、Contiguous、Transpose 等 11 个接口的用法与源码原理
【免费下载链接】opbase本项目是CANN算子库的基础框架库,为算子提供公共依赖文件和基础调度能力。项目地址: https://gitcode.com/cann/opbase
导读
在 CANN 算子库框架(opbase)中,Level0(L0)层接口是调用单个 Kernel 的 Host 侧细粒度 API,是开发 aclnn(Level2 层)接口的基础积木。本文以 docs/zh/api/nnopbase/opdev/L0/basic_l0_Interface.md 为核心,系统讲解其收录的Cast、Contiguous、IsNullptr、Pad、ReFormat、Reshape、Slice、TransData、TransDataSpecial、Transpose、ViewCopy共 11 个基础张量操作接口的功能、函数原型、参数约束、调用示例与产品支持情况,并结合仓库源码说明 L0 接口的命名空间、执行器创建、入参打包等底层机制。读完本文,你将能够在自定义 aclnn 算子开发中熟练使用这些 L0 接口完成张量类型转换、内存布局规整、格式切换、切片与转置等基础操作。
L0 接口在 CANN 算子开发体系中的定位
Level0 与 Level2 的层级关系
从 1_opdev_api_introduction.md 的定义可以看出,CANN 的 Host 侧算子开发接口分为两层:
- Level0 层接口(L0):表示调用单 Kernel 的 Host 侧 API,提供细颗粒 API(单 Kernel 下发)和算子 API 开发的基础结构体(如 Tensor 定义等)与公共基础能力(如 workspace 复用、引擎调度等)。上层应用或 L2 层接口可通过 L0 接口的快速组装实现高性能计算。
- Level2 层接口(L2):对 L0 层接口的高层级封装,内部通过调用单个或多个 L0 接口实现更灵活的功能。对外提供
aclnnXxxGetWorkspaceSize与aclnnXxx两段式接口,用户直接调用即可完成算子执行。
L0 接口的返回值类型是 Tensor 类型结构(如aclTensor*、std::tuple<aclTensor*, aclTensor*>、aclTensorList*),最后一个参数固定为aclOpExecutor *executor,类型与名称均不可变。L0 接口统一位于namespace l0op命名空间下,这一命名空间在 include/nnopbase/opdev/op_dfx.h 中有明确声明。
接口命名规范:L0 接口名形如
{op_type}{format}{dtype},例如l0op::AddNd表示 Add 算子输入均按 ND 格式计算;l0op::MatMulNzFp162Fp16表示 MatMul 算子输入输出均按 NZ 格式计算,其中2代表 "To",表示输入输出均为 fp16。
L0 接口的头文件分布
本文涉及的 11 个基础张量操作接口分布在以下头文件中(见 1_opdev_api_introduction.md):
| 接口 | 所属头文件 |
|---|---|
| Cast | aclnn_kernels/cast.h |
| Contiguous、ViewCopy | aclnn_kernels/contiguous.h |
| Pad | aclnn_kernels/pad.h |
| Reshape | aclnn_kernels/reshape.h |
| Slice | aclnn_kernels/slice.h |
| Transpose | aclnn_kernels/transpose.h |
| TransData、TransDataSpecial、ReFormat | aclnn_kernels/transdata.h |
| IsNullptr | aclnn_kernels/op_error_check.h |
11 个基础张量操作接口总览
| 接口名 | 核心功能 | 一句话使用场景 |
|---|---|---|
| Cast | 将输入 tensor 转换为指定数据类型 | bool 转 uint8 后参与整型计算、精度转换 |
| Contiguous | 将非连续 tensor 转换为连续 tensor | L2 接口的非连续输入喂给只支持连续输入的 L0 算子 |
| ViewCopy | 将连续 tensor 搬运到连续或非连续 tensor 上 | 将计算结果写入非连续的输出 tensor |
| Pad | 按 paddings 对各维度填充 0 | 张量补维、边界填充 |
| Reshape | 不改数据、转换 shape | 视图重塑、维度合并 |
| Slice | 从输入 tensor 提取切片 | 分块处理、区域提取 |
| Transpose | 按 perm 重排维度 | 维度交换(如 NHWC 与 NCHW 的互换) |
| TransData | 转换 tensor 的 format | NC1HWC0 与 NCHW 等格式互转 |
| TransDataSpecial | 转换 tensor 的 format(特殊 C0 规则) | 与 TransData 类似但 C0 处理规则不同 |
| ReFormat | 将 viewFormat/originalFormat/storageFormat 统一为目标 format | 显式声明 tensor 的 format 视图 |
| IsNullptr | 判断指针是否为空并打印错误日志 | L0 接口入参的空指针校验 |
数据类型转换:Cast
功能与原型
Cast 将输入 tensor 转换为指定的数据类型:
const aclTensor *Cast(const aclTensor *self, op::DataType dstDtype, aclOpExecutor *executor)参数说明:
| 参数 | 输入/输出 | 说明 |
|---|---|---|
| self | 输入 | 待转换的输入 tensor,数据类型支持 FLOAT16、FLOAT、DOUBLE、BFLOAT16、INT8、UINT8、INT16、UINT16、INT32、UINT32、INT64、UINT64、BOOL、COMPLEX64、COMPLEX128,数据格式支持 ND |
| dstDtype | 输入 | 转换后的目标 dtype,支持的数据类型与 self 相同 |
| executor | 输入 | op 执行器,包含了算子计算流程 |
返回值:类型为 dstDtype 的 tensor。注意BFLOAT16 仅适用于 Atlas A2 与 Atlas A3 训练/推理系列产品。
实战示例:bool 参与整型计算
算子计算中经常遇到"张量为布尔类型但需要进行整型运算"的场景。文档给出的标准写法是先通过 Cast 将 bool 统一转换为 uint8:
// 标准写法,创建OpExecutor auto uniqueExecutor = CREATE_EXECUTOR(); auto selfCasted = self; // 当self为布尔类型时,利用Cast接口转换为uint8类型后可进行整型计算 if (self->GetDataType() == op::DataType::DT_BOOL) { selfCasted = l0op::Cast(self, op::DataType::DT_UINT8, uniqueExecutor.get()); CHECK_RET(selfCasted != nullptr, ACLNN_ERR_PARAM_NULLPTR); }这里的CREATE_EXECUTOR()宏用于创建一个aclOpExecutor的生成工厂对象(UniqueExecutor),其定义见 CREATE_EXECUTOR 宏说明,而CHECK_RET则是基于 IsNullptr 的常见返回校验宏。
连续性与视图转换:Contiguous、ViewCopy、Reshape
L2 级 API 的输入 tensor 可能是非连续的,而 L0 级算子一般只支持连续 tensor 作为输入,因此"连续化"与"视图重排"是 L0 接口使用频率最高的基础能力。
Contiguous:非连续转连续
const aclTensor *Contiguous(const aclTensor *x, aclOpExecutor *executor)- x:待转换的输入 tensor,数据类型和数据格式不限制;输入不要求是连续内存,但要求所表达的数据在 Storage 范围内。
- executor:op 执行器。
返回值:转换成功返回连续 aclTensor,失败返回 nullptr。
约束要点:输入必须是合法 tensor,Shape 和 Stride 所表示的数据必须在 Storage 大小范围内。文档给出的反例:shape=(2, 3), stride=(10, 30), storageSize=8,数据实际空间超过了 Storage 大小 8,该 tensor 非法,Contiguous 返回 nullptr。
// 标准写法,创建OpExecutor auto uniqueExecutor = CREATE_EXECUTOR(); // self如果非连续,需要转换 auto selfContiguous = l0op::Contiguous(self, executor);从 include/nnopbase/opdev/tensor_view_utils.h 提供的IsContiguous等工具接口可见,连续性与 stride/shape 的关系是 opbase 框架的基础概念,Contiguous 正是这类判断与搬运逻辑在 L0 层的封装。
ViewCopy:连续 tensor 搬运到(非)连续输出
与 Contiguous 互为"反向",L2 接口的输出 tensor 可能是非连续的,需要通过 ViewCopy 把计算得到的连续 tensor 搬运到目标输出上:
const aclTensor *ViewCopy(const aclTensor *x, const aclTensor *y, aclOpExecutor *executor)- x:输入 tensor,数据类型和数据格式不限制,必须保证是连续内存数据。
- y:输出 tensor,数据类型和数据格式不限制,但数据类型、ViewShape 和数据格式要求与 x 一致。
// 标准写法,创建OpExecutor auto uniqueExecutor = CREATE_EXECUTOR(); // 如果出参out是非连续Tensor,需要把计算完的连续Tensor转非连续 auto viewCopyResult = l0op::ViewCopy(absResult, out, executor);Reshape:不改数据只改 shape
const aclTensor *Reshape(const aclTensor *x, const op::Shape &shape, aclOpExecutor *executor) const aclTensor *Reshape(const aclTensor *x, const aclIntArray *shape, aclOpExecutor *executor)- x:待转换的输入 tensor,数据类型和数据格式不限制,必须是连续内存数据。
- shape:转换后的目标 shape,支持
aclIntArray*、op::Shape(即 gert::Shape)两种类型。
约束说明:
- Reshape 成功的前提是 x 的 ShapeSize 与目标 shape 的 ShapeSize 相等。例如 A 的 shape 为
(1, 3, 256, 256),则 A 的 ShapeSize =1*3*256*256。 - 当前不支持转换成空 tensor(shape 中包含 0 的空 tensor)。
void Func(const aclTensor *x, const op::Shape &shape, aclOpExecutor *executor) { auto ret = l0op::Reshape(x, shape, executor); return; }数据提取与维度重排:Slice、Transpose、Pad
Slice:按 offset 和 size 提取切片
const aclTensor *Slice(const aclTensor *x, const aclTensor *y, const aclTensor *offset, const aclTensor *size, aclOpExecutor *executor) const aclTensor *Slice(const aclTensor *x, const aclIntArray *offsets, const aclIntArray *size, aclOpExecutor *executor)参数说明:
| 参数 | 输入/输出 | 说明 |
|---|---|---|
| x | 输入 | 输入 tensor,数据类型支持 FLOAT16、FLOAT、BOOL、INT8、UINT8、INT16、UINT16、INT32、UINT32、INT64、BFLOAT16、UINT64,数据格式支持 ND |
| y | 输出 | 切片后的输出 tensor,数据类型与 x 相同 |
| offsets / offset | 输入 | 表示输入 x 在各个维度切片的起始位置,其形状为 x 的维度,支持aclIntArray*与aclTensor*,数据类型支持 INT32、INT64 |
| size | 输入 | 输入 x 的各个维度切片的大小,其形状为 x 的维度,支持aclIntArray*、aclTensor*,数据类型支持 INT32、INT64 |
返回值:类型与输入相同、shape 为 size 的 tensor。BFLOAT16 仅适用于 Atlas A2/A3 训练与推理系列产品。两种调用形态分别对应 host 侧数组与 device 侧 tensor 作为参数:
// 调用l0op::Slice对每一块进行处理 auto sliceRes = l0op::Slice(self, offsetArray, sizeArray, executor); // 调用l0op::Slice对每一块进行处理 auto sliceRes = l0op::Slice(xTensor, yTensor, offsetTensor, sizeTensor, executor);Transpose:按 perm 重排维度
Transpose 不改变 tensor 数据的值,只是把输入 x 的 shape 按指定维度的排列顺序 perm 进行转置输出。提供两个重载:
// 输入和输出为不同地址 const aclTensor *Transpose(const aclTensor *x, const aclTensor *y, const aclTensor *perm, aclOpExecutor *executor) // 输入和输出同一地址(原地) const aclTensor *Transpose(const aclTensor *x, const aclIntArray *perm, aclOpExecutor *executor)参数说明:
| 参数 | 输入/输出 | 说明 |
|---|---|---|
| x | 输入/输出 | 原始输入 tensor,需是连续内存数据;数据类型支持 FLOAT16、FLOAT、INT8、INT16、INT32、INT64、UINT8、UINT16、UINT32、UINT64、BOOL、BFLOAT16,数据格式支持 ND |
| y | 输出 | 转置后输出 tensor,数据类型和数据格式同 x |
| perm | 输入 | 整型数组,代表输入 tensor x 的维度,支持aclIntArray*、aclTensor*类型;最多支持 8 维转置,取值需在[0, x的维度数量-1]范围内,数据类型支持 INT32、INT64 |
约束说明:最多支持 8 维转置(x 和 perm 的 dim 至多为 8),且输入 x 和 perm 的 dim 维度必须一致。
实战示例——结合Contiguous与AllocIntArray的完整流程:
// 标准写法,创建OpExecutor,参数检查 auto uniqueExecutor = CREATE_EXECUTOR(); CHECK_RET(uniqueExecutor.get() != nullptr, ACLNN_ERR_INNER_CREATE_EXECUTOR); // 标准写法,将输入self转换成连续的tensor auto selfContiguous = l0op::Contiguous(self, uniqueExecutor.get()); CHECK_RET(selfContiguous != nullptr, ACLNN_ERR_INNER_NULLPTR); int64_t dims = selfContiguous->GetViewShape().GetDimNum(); int64_t valuePerm[dims] = {0, 2, 1, 3}; // 表示对原始4维的中间2维做转置,即交换1轴和2轴 auto perm = executor->AllocIntArray(valuePerm, dims); selfContiguous = l0op::Transpose(selfContiguous, perm, uniqueExecutor.get());其中executor->AllocIntArray(value, size)由aclOpExecutor提供,用于在 host 侧申请并填充 int64 数组对象,其声明见 include/nnopbase/opdev/op_executor.h;同类接口还包括AllocScalar、AllocTensorList、AllocBoolArray、AllocFloatArray等,详见 op_executor 接口说明。
Pad:按 paddings 各维度填充 0
const aclTensor* Pad(const aclTensor* self, const aclTensor* paddings, aclOpExecutor* executor)- self:待填充的输入 tensor,数据类型支持 FLOAT16、FLOAT、INT16、UINT16、INT32、INT64、BFLOAT16、INT8,数据格式支持 ND。
- paddings:输入 tensor 每个维度被填充的大小,形状为
[self.dim, 2],数据类型支持 INT32、INT64,数据格式支持 ND。
返回被填充了 0 的 tensor。注意BFLOAT16 和 INT8 仅适用于 Atlas A2/A3 训练与推理系列产品:
//调用l0op::Pad对self进行补维 l0op::Pad(self, paddings, executor);数据格式转换:TransData、TransDataSpecial、ReFormat
Ascend 硬件上张量存在多种数据排布格式(如 ND、NCHW、NC1HWC0、NZ 等),L0 层提供了三个 format 相关接口。
TransData:转换到目标 primary format
const aclTensor *TransData(const aclTensor *x, op::Format dstPrimaryFormat, int64_t groups, aclOpExecutor *executor)- x:待转换的 tensor,数据类型支持 FLOAT16、FLOAT32、INT32、UINT32、INT8、UINT8。
- dstPrimaryFormat:目标 format。
- groups:分组参数,用于分组转换时传入,数据类型支持 INT64。
约束说明:当输入 tensor 数据类型为 FLOAT32、INT32、UINT32 时,C0 只能按照 8 处理。
// 将张量格式从NC1HWC0转换成NCHW auto transGradInput = l0op::TransData(gradInputNC1HWC0, op::Format::FORMAT_NCHW, params.groups, executor); CHECK_RET(transGradInput != nullptr, ACLNN_ERR_INNER_NULLPTR);TransDataSpecial:特殊 C0 规则的 format 转换
功能与 TransData 类似,但当输入 tensor 数据类型为 FLOAT32、INT32、UINT32 时,C0 只能按照 16 处理——这是两者最关键的差异,实际选型时需根据目标格式的 C0 值决定:
const aclTensor *TransDataSpecial(const aclTensor *x, op::Format dstPrimaryFormat, int64_t groups, aclOpExecutor *executor)// 标准写法,创建OpExecutor auto uniqueExecutor = CREATE_EXECUTOR(); // 将gradOutputReFormat的format转换为NC1HWC0 auto gradOutputTransData = l0op::TransDataSpecial(gradOutputReFormat, op::Format::FORMAT_NC1HWC0, 0, uniqueExecutor.get());ReFormat:统一三种 format 视图
与 TransData 不同,ReFormat 不做数据重排,而是在指定 format 和输入 x 的维度相同时,将输入数据格式设置为目标 format。具体来说,是把输入 tensor 的viewFormat、originalFormat、storageFormat 统一为指定的 format:
const aclTensor *ReFormat(const aclTensor *x, const op::Format &format, aclOpExecutor *executor=nullptr)- x:需要被转换的 tensor,数据类型支持 FLOAT16、FLOAT32、INT32、UINT32、INT8、UINT8。
- format:目标 format。
约束:输入 tensor 的维度必须与指定 format 的维度相同。
// 将输入reformat成NCHW格式 auto reformatInput = l0op::ReFormat(unsqueezedInput, op::Format::FORMAT_NCHW); CHECK_RET(reformatInput != nullptr, nullptr);关于 viewFormat、originalFormat、storageFormat 的读取与设置接口,可参见 common_types 中的 Get/SetViewFormat、Get/SetOriginalFormat、Get/SetStorageFormat,ReFormat 正是通过统一这三者来实现格式视图的显式声明。
参数校验:IsNullptr
功能与重载
IsNullptr 判断输入的指针是否为空:若为空指针返回 true 并打印错误日志,否则返回 false。它为所有 L0 入参类型都提供了重载:
static inline bool IsNullptr(const aclTensor *tensor, const char *name) static inline bool IsNullptr(const aclTensorList *tensorList, const char *name) static inline bool IsNullptr(const aclScalar *scalar, const char *name) static inline bool IsNullptr(const aclIntArray *intArr, const char *name) static inline bool IsNullptr(const aclBoolArray *boolArr, const char *name) static inline bool IsNullptr(const aclFloatArray *floatArr, const char *name)- name:被检查指针的标识。若被检查指针为空,打印的错误日志中会输出该标识,便于定位是哪个参数为空。
封装为通用校验宏
文档给出的典型用法是将其封装为通用空指针校验宏,配合#param将参数名作为日志标识传入:
#define OP_CHECK_NULL(param, retExpr) \ if (IsNullptr(param, #param)) { \ retExpr; \ }在实际 L0/L2 接口开发中,IsNullptr也是CHECK_RET(param != nullptr, ...)这类返回检查模式的底层支撑,可用于接口入口统一校验,避免对空指针继续下发 Kernel 导致异常。
L0 接口通用开发模式与底层机制
综合前文各示例,一个标准的 L0 接口调用流程通常包含四个固定环节,其底层机制均有源码支撑:
- 创建执行器:通过
CREATE_EXECUTOR()宏创建aclOpExecutor(见 make_op_executor.h 相关宏与 CREATE_EXECUTOR 说明)。aclOpExecutor记录整个 host 侧 API 运行的上下文信息,如 L2 接口执行过程中的计算图、L0 算子 launch 子任务、workspace 地址和大小等。 - 入参规整:对非连续输入先执行
l0op::Contiguous;对需要 host 侧数组入参(如 perm、offsets)的场景,使用executor->AllocIntArray等分配接口(见 op_executor.h)。 - 调用 l0op 接口:L0 接口统一在
namespace l0op下,最后一个参数固定为aclOpExecutor *executor。 - 校验返回值:所有 L0 接口失败时返回
nullptr,成功时返回指向新 tensor 的指针(原地接口除外),必须使用CHECK_RET(... != nullptr, ...)或IsNullptr校验。
此外,include/nnopbase/opdev/op_dfx.h 中还定义了 L0 接口开发必须配套使用的宏:OP_TYPE_REGISTER(kernelName)用于在 L0 接口最开始处注册 L0 算子,L0_DFX(profilingName, ...)用于接口及 L0 接口入参的打印与 profiling 上报。完整的宏使用规范参见 常用宏表。
产品支持情况汇总
根据各接口文档中的产品支持说明,11 个接口在不同硬件平台上的支持情况如下("Ascend 950PR/Ascend 950DT"均不支持;其余以 Atlas 系列为准):
| 接口 | Atlas 训练系列(910) | Atlas 推理系列(310p) | Atlas 200I/500 A2 推理(310b) | Atlas A2 训练/推理(910b) | Atlas A3 训练/推理 |
|---|---|---|---|---|---|
| Cast | 支持 | 支持 | 支持 | 支持 | 支持 |
| Contiguous | 支持 | 支持 | 支持 | 支持 | 支持 |
| ViewCopy | 支持 | 支持 | 支持 | 支持 | 支持 |
| Reshape | 支持 | 支持 | 支持 | 支持 | 支持 |
| Pad | 支持 | 支持 | 不支持 | 支持 | 支持 |
| Slice | 支持 | 支持 | 不支持 | 支持 | 支持 |
| Transpose | 支持 | 支持 | 不支持 | 支持 | 支持 |
| TransData | 支持 | 支持 | 不支持 | 支持 | 支持 |
| TransDataSpecial | 支持 | 支持 | 不支持 | 支持 | 支持 |
| ReFormat | 支持 | 支持 | 不支持 | 支持 | 支持 |
| IsNullptr | 支持 | 支持 | 不支持 | 支持 | 支持 |
从上表可看出:Contiguous、ViewCopy、Reshape、Cast 四个接口在 Atlas 系列全平台支持(实现机制为纯内存/视图操作,不依赖硬件格式特性);而 Pad、Slice、Transpose、TransData 系列与 IsNullptr 在 Atlas 200I/500 A2 推理产品(310b)上暂不支持,开发时需结合目标产品进行能力判断。此外,BFLOAT16数据类型(Cast、Pad、Slice、Transpose 涉及)与INT8(Pad 涉及)仅在 Atlas A2/A3 训练与推理系列产品上可用。
结语
CANN opbase 的 L0 基础张量操作接口是 aclnn 算子开发中复用率最高的"积木":Contiguous/ViewCopy 解决 L2 与 L0 之间的连续性鸿沟,Cast 统一计算前的数据类型,Transpose/Slice/Pad/Reshape 完成视图与区域的变换,TransData/TransDataSpecial/ReFormat 处理硬件格式排布,IsNullptr 则提供统一的入参防线。理解并熟练组合这 11 个接口,是高效编写正确、健壮的 L2 层算子逻辑的基础;结合 basic_l0_Interface.md 中每个接口的产品支持约束与数据类型限制,可以显著降低跨平台算子适配的返工成本。
【免费下载链接】opbase本项目是CANN算子库的基础框架库,为算子提供公共依赖文件和基础调度能力。项目地址: https://gitcode.com/cann/opbase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考