
CANN ops-math 算子 aclnnCast 使用指南两段式接口调用与 NPU 数据类型转换实践【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math导读aclnnCast是 CANN ops-math 数学算子库项目根目录为Cast数据类型转换算子提供的 ACLNNAscendCL NN高层 API用于将输入 tensor 转换为指定的目标 dtype是网络模型中 Float→Half、Float32→Int8 等量化/精度转换场景的常用基础算子。本文以仓库内算子文档 experimental/math/cast/docs/aclnnCast.md 为主体结合 experimental/math/cast 目录下的 op_api、op_host、op_kernel 源码系统讲解其产品支持情况、两段式接口调用流程、参数约束、错误码语义与完整可编译示例帮助开发者在 Atlas A2 系列产品上快速完成 dtype 转换算子的接入与调优。产品支持情况aclnnCast当前支持的产品如下表所示产品是否支持Atlas A2 训练系列产品 / Atlas A2 推理系列产品√需要注意的是虽然 API 层面声明了多种数据类型但Atlas A2 训练系列产品 / Atlas A2 推理系列产品即 Ascend 910B 系列并不支持全部声明类型具体不支持的类型见后文「约束说明」章节。这一点与算子注册代码相印证在 cast_def.cpp 中通过this-AICore().AddConfig(ascend910b)仅注册了ascend910b硬件配置。功能说明Cast 算子提供将 tensor 从源数据类型转换为目标数据类型的能力。其 API 功能定义与底层算子规格在仓库中可以一一对应API 语义见 aclnn_cast.h将输入tensor转换为指定的dtype类型算子规格见 README.md 中的算子规格描述算子类型OpType为Cast输入为 tensorx输出为 tensorout属性为dstTypeint64核函数名为cast底层定义见 cast_def.cpp算子注册了x输入、y输出与dst_typeREQUIRED、Int属性数据格式均为 ND。从 API 层看aclnnCast支持的数据类型覆盖 FLOAT16、FLOAT、DOUBLE、INT8、UINT8、INT16、UINT16、INT32、UINT32、INT64、UINT64、BOOL、COMPLEX32、COMPLEX64、COMPLEX128、BFLOAT16、HIFLOAT8、FLOAT8_E5M2、FLOAT8_E4M3FN、FLOAT4_E2M1、FLOAT4_E1M2 等类型而底层 kernel 在 cast.cpp 与 cast.h 中针对 AscendC 指令集可实现的具体组合做了精细化实现详见「内核实现」章节API 的宽类型声明与硬件实际实现能力之间的差异正是「约束说明」章节大量限制条款的来源。两段式接口调用流程aclnnCast属于 CANN 的标准两段式接口设计必须先调用aclnnCastGetWorkspaceSize获取计算所需 workspace 大小以及包含了算子计算流程的执行器executor再调用aclnnCast执行计算。第一段接口aclnnCastGetWorkspaceSizeaclnnStatus aclnnCastGetWorkspaceSize( const aclTensor *self, const aclDataType dtype, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor)其参数说明如下表参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensorself输入待进行cast计算的入参Device侧的aclTensor-FLOAT16、FLOAT、DOUBLE、INT8、UINT8、INT16、UINT16、INT32、UINT32、INT64、UINT64、BOOL、COMPLEX32、COMPLEX64、COMPLEX128、BFLOAT16、HIFLOAT8、FLOAT8_E5M2、FLOAT8_E4M3FN、FLOAT4_E2M1、FLOAT4_E1M2ND0-8√dtype属性输入tensor要转换的目标dtype-const aclDataType---out输出待进行cast计算的出参Device侧的aclTensorshape与self相同FLOAT16、FLOAT、DOUBLE、INT8、UINT8、INT16、UINT16、INT32、UINT32、INT64、UINT64、BOOL、COMPLEX32、COMPLEX64、COMPLEX128、BFLOAT16、HIFLOAT8、FLOAT8_E5M2、FLOAT8_E4M3FN、FLOAT4_E2M1、FLOAT4_E1M2、INT4暂不支持非连续TensorND0-8√workspaceSize输出返回需要在Device侧申请的workspace大小-----executor输出返回op执行器包含了算子计算流程-----Atlas A2 训练系列产品/Atlas A2 推理系列产品不支持 COMPLEX32、HIFLOAT8、FLOAT8_E5M2、FLOAT8_E4M3FN、FLOAT4_E2M1、FLOAT4_E1M2、INT4。从源码实现看第一段接口完成了三类工作对应 aclnn_cast.cpp创建执行器并完成入参校验CheckParams依次执行空指针检查CheckNotNull、数据类型与格式合法性检查CheckDtypeValid、shape 合法性及输入输出 shape 一致性检查CheckShape任一步失败即返回对应错误码空 tensor 短路处理self-IsEmpty()为真时直接返回workspaceSize 0无需实际计算编排底层计算图l0op::Contiguous将输入转成连续 tensor →l0op::Cast完成实际类型转换 →l0op::ViewCopy将结果写回可能非连续的输出 tensor最后通过uniqueExecutor-GetWorkspaceSize()汇总所需 workspace 大小并释放执行器给用户。第二段接口aclnnCastaclnnStatus aclnnCast( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)其参数说明如下表参数名输入/输出描述workspace输入在Device侧申请的workspace内存地址workspaceSize输入在Device侧申请的workspace大小由第一段接口aclnnCastGetWorkspaceSize获取executor输入op执行器包含了算子计算流程stream输入指定执行任务的Stream第二段接口的实现非常简洁在 aclnn_cast.cpp 中aclnnCast直接调用框架能力CommonOpExecutorRun(workspace, workspaceSize, executor, stream)完成异步计算下发无需用户感知内部细节。整体调用关系可用下图概括self dtype \ / Contiguous(workspace_0) \ / Cast(workspace_1) | ViewCopy | result返回值与错误码两个接口均返回aclnnStatus状态码具体可参见 aclnn返回码。第一段接口aclnnCastGetWorkspaceSize会完成入参校验出现以下场景时报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的tensor或out是空指针ACLNN_ERR_PARAM_INVALID可对应以下 5 种场景161002self的数据类型和数据格式不在支持的范围之内self的数据格式与out的数据格式不同self的shape与out的shape不同参数dtype不在输出支持的数据格式范围之内out的数据类型为INT4时self为非连续TensorACLNN_ERR_INNER_TILING_ERROR561002out的数据类型为INT4时self的shape尾轴为奇数这些校验逻辑在 aclnn_cast.cpp 中均有对应实现例如CheckDtypeValid会根据当前 NPU 架构NpuArch::DAV_2201对应 Ascend 910B 系列、NpuArch::DAV_3510对应 Ascend 950选择不同的 dtype 支持列表并校验dtype参数CheckShape则通过OP_CHECK_MAX_DIM上限 8 维与OP_CHECK_SHAPE_NOT_EQUAL完成 shape 校验。约束说明使用aclnnCast前需重点确认以下约束确定性计算aclnnCast默认确定性实现即相同输入保证相同输出相关内容可参考 确定性计算浮点转整型输入数据中存在nan时nan将被转换为0非连续输入限制输入数据类型为 BOOL、COMPLEX32、COMPLEX64、COMPLEX128、FLOAT4_E2M1、FLOAT4_E1M2 时不支持输入为非连续 tensor非连续 tensor 的概念详见 非连续TensorAtlas A2 训练系列产品/Atlas A2 推理系列产品数据类型从 int32 转换为 int8 时只能保证输入数据在(-2048, 1920)范围内精度无误差数据类型从 float64/complex64/complex128 转换为 uint8 时只能保证输入数据为非负数时精度无误差。这些精度限制与 kernel 中窄化转换的实现方式直接相关。从 cast.h 可以看到涉及 int8/uint8 输出的转换如half→int8、float→int8、int32→int8、int64→int8普遍采用Duplicate填 255 →And取低 8 位 →Adds(128)→And→Adds(-128)的位运算包装方案因此数值范围越界时存在截断误差属于实现层面的固有特性。完整调用示例仓库在 examples/test_aclnn_cast.cpp 中提供了与文档示例一致的完整可编译样例文档正文示例即出自该文件具体编译与执行流程请参考编译与运行样例。以下代码演示了将{4, 2}的 FLOAT 数据转换为 DOUBLE 数据的完整流程#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_cast.h #define CHECK_RET(cond, return_expr) \ do { \ if (!(cond)) { \ return_expr; \ } \ } while (0) #define LOG_PRINT(message, ...) \ do { \ printf(message, ##__VA_ARGS__); \ } while (0) int64_t GetShapeSize(const std::vectorint64_t shape) { int64_t shapeSize 1; for (auto i : shape) { shapeSize * i; } return shapeSize; } int Init(int32_t deviceId, aclrtStream* stream) { // 固定写法初始化 auto ret aclInit(nullptr); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclInit failed. ERROR: %d\n, ret); return ret); ret aclrtSetDevice(deviceId); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtSetDevice failed. ERROR: %d\n, ret); return ret); ret aclrtCreateStream(stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtCreateStream failed. ERROR: %d\n, ret); return ret); return 0; } template typename T int CreateAclTensor(const std::vectorT hostData, const std::vectorint64_t shape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { auto size GetShapeSize(shape) * sizeof(T); // 调用aclrtMalloc申请device侧内存 auto ret aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtMalloc failed. ERROR: %d\n, ret); return ret); // 调用aclrtMemcpy将host侧数据拷贝到device侧内存上 ret aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtMemcpy failed. ERROR: %d\n, ret); return ret); // 计算连续tensor的strides std::vectorint64_t strides(shape.size(), 1); for (int64_t i shape.size() - 2; i 0; i--) { strides[i] shape[i 1] * strides[i 1]; } // 调用aclCreateTensor接口创建aclTensor *tensor aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } int main() { // 1. 固定写法device/stream初始化参考acl API文档 // 根据自己的实际device填写deviceId int32_t deviceId 0; aclrtStream stream; auto ret Init(deviceId, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(Init acl failed. ERROR: %d\n, ret); return ret); // 2. 构造输入与输出需要根据API的接口自定义构造 std::vectorint64_t selfShape {4, 2}; std::vectorint64_t outShape {4, 2}; void* selfDeviceAddr nullptr; void* outDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* out nullptr; std::vectorfloat selfHostData {0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1}; std::vectordouble outHostData {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; // 创建self aclTensor ret CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建out aclTensor ret CreateAclTensor(outHostData, outShape, outDeviceAddr, aclDataType::ACL_DOUBLE, out); CHECK_RET(ret ACL_SUCCESS, return ret); // 3. 调用CANN算子库API需要修改为具体的API名称 uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnCastGetWorkspaceSize第一段接口 ret aclnnCastGetWorkspaceSize(self, aclDataType::ACL_DOUBLE, out, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnCastGetWorkspaceSize failed. ERROR: %d\n, ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 void* workspaceAddr nullptr; if (workspaceSize 0) { ret aclrtMalloc(workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(allocate workspace failed. ERROR: %d\n, ret); return ret); } // 调用aclnnCast第二段接口 ret aclnnCast(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnCast failed. ERROR: %d\n, ret); return ret); // 4. 固定写法同步等待任务执行结束 ret aclrtSynchronizeStream(stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtSynchronizeStream failed. ERROR: %d\n, ret); return ret); // 5. 获取输出的值将device侧内存上的结果拷贝至host侧需要根据具体API的接口定义修改 auto size GetShapeSize(outShape); std::vectordouble resultData(size, 0); ret aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, size * sizeof(resultData[0]), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(copy result from device to host failed. ERROR: %d\n, ret); return ret); for (int64_t i 0; i size; i) { LOG_PRINT(result[%ld] is: %f\n, i, resultData[i]); } // 6. 释放aclTensor需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyTensor(out); // 7. 释放device 资源 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例整体流程可归纳为七步初始化aclInit / aclrtSetDevice / aclrtCreateStream→构造输入输出aclrtMalloc 申请显存、aclrtMemcpy 搬运数据、aclCreateTensor 创建 aclTensor→两段式调用GetWorkspaceSize 后按需申请 workspace 再执行 aclnnCast→同步等待aclrtSynchronizeStream→结果回拷ACL_MEMCPY_DEVICE_TO_HOST→释放 aclTensoraclDestroyTensor→释放资源aclrtFree / aclrtDestroyStream / aclrtResetDevice / aclFinalize。从源码看实现原理Tiling 策略按数据类型组合分派Cast 算子通过 Tiling 阶段为每一组「输入类型 × 输出类型」生成不同的调度参数。在 cast_tiling.cpp 中三张映射表共同决定了计算分片方式UbDataNumMapInit按「输入字节数 × 双缓冲 输出字节数 × 双缓冲 临时 buffer 字节数」预先计算每种类型组合在 UBUnified Buffer中占用单位TilingKeyMapInit将每种类型组合映射到 1~8 的 tiling key用于选择 kernel 实现分支MinDataTypeLengthMapInit记录输入与输出类型中的最小字节长度用于估算块内可处理的数据量。随后CastTilingFunc结合 UB 大小、核数coreNum、总数据量计算出smallCoreDataNum/bigCoreDataNum大小核任务量、tileDataNum每块数据量、finalBigTileNum/finalSmallTileNumtile 数量、tailDataNum等参数并通过context-SetBlockDim(coreNum)与context-SetTilingKey(tilingKey)下发给 kernel。这也解释了为什么aclnnCastGetWorkspaceSize能提前计算出 workspace 大小——整个计算图Contiguous Cast ViewCopy的中间 buffer 需求在构图阶段就已确定。Kernel 实现按临时 buffer 需求分类的 8 类内核从 cast.cpp 的cast核函数入口可以看到kernel 按TILING_KEY_IS(1)到TILING_KEY_IS(8)分派到 cast.h 中的 8 类实现分类依据是完成转换所需的临时 buffer 数量与位宽tiling key内核类典型类型组合转换要点1KernelCast0TBufhalf→float/int32/int16/bool、float→half、int32→float/int64/int16、int8/uint8/bool→half、int64→float/int32、bf16→float、int16→float/half无临时变量直接Cast浮点转整型默认CAST_TRUNC截断int64→float 使用CAST_ROUNDhalf/float→bool 先AbsMins(1)再CAST_CEIL2KernelCast1TBuf4Bhalf→bf16、int32→bf16/half、int64→half/bf16/int16、bf16→half、int16→int32/int641 个 4 字节临时变量多经 float 中转bf16 目标使用CAST_RINT3KernelCast2TBuf2Bhalf→int8/uint8、float→bool、int32→bool、int16→int8/uint82 个 2 字节临时变量int8 输出使用「与 255 ±128」位运算包装4KernelCast3TBuf2Bfloat→int8/uint8、int32→int8/uint83 个 2 字节临时变量int32 先按CAST_NONE转 int16 再做位运算包装5KernelCast1TBuf2Bint8/uint8/bool→float/int32/int16、int8→bool1 个 2 字节临时变量先经 half 中转6KernelCast1TBuf2B1TBuf4Bint8/uint8/bool→int64/bf16、int64/bf16→bool1 个 2 字节 1 个 4 字节临时变量多级中转7KernelCast3TBuf2B1TBuf4Bint64→int8/uint8、bf16→int8/uint83 个 2 字节 1 个 4 字节临时变量int64 先转 int32 再包装8KernelCastTQueBindbool→int8/uint8、int8→uint8、uint8→int88bit 类型直接 TQueBind 搬运无向量计算所有内核类共享BaseKernelCast的流水模式CopyInGlobal→UB 双缓冲入队→Compute向量 Cast 指令计算→CopyOutUB→Global 出队并针对大核/小核数量与尾块tail block做了负载均衡处理。辅助阅读上下文文档若希望进一步理解本文涉及的概念可配合阅读 两段式接口、aclnn返回码、编译与运行样例、确定性计算、非连续Tensor 以及算子快速调用说明 quick_op_invocationREADME.md 中亦引用了该调用方式。小结aclnnCast是 ops-math 中覆盖数据类型最广的基础算子之一其「两段式接口 workspace 预计算 按类型组合分派内核」的设计既保证了调用方可以在构图阶段精确掌握资源需求也通过 8 类内核的精细实现覆盖了从 8bit 布尔到 128bit 复数以及 Atlas A2 之外的扩展类型的丰富转换组合。开发者在实际使用时只需遵循「先 GetWorkspaceSize 校验并构图、再申请 workspace、最后 aclnnCast 下发」的标准流程并特别注意 Atlas A2 系列在窄化转换int32→int8、float64→uint8 等上的精度范围限制与非连续 tensor 支持限制即可稳定、正确地完成 NPU 上的数据类型转换。【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考