CANN ops-math FloorDiv 算子深度解析向下取整除法的 NPU 实现与 aclnn 调用指南【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-mathFloorDiv向下取整除法是 CANN ops-math 数学算子库中的基础二元算子完成out floor(self / other)计算。本文以 math/floor_div/README.md 为骨架结合算子源码、Tiling 实现、配置与测试用例系统讲解 FloorDiv 的功能语义、产品支持矩阵、aclnnFloorDivide 系列接口的参数规范与错误码、两段式调用流程并深入到 op_api、op_host、op_kernel 三层源码剖析其实现原理。读完本文你将能够在 Ascend NPU 上正确、高效地通过 aclnn 接口调用 FloorDiv并理解其内部数据类型推导、Broadcast 与 Kernel 计算细节。一、功能说明与数学语义FloorDiv 算子的功能是完成除法计算对结果向下取整计算公式为$$ out_i floor(\frac{self_i}{other_i}) $$其中self、other为两个输入 Tensorout为输出 Tensor。需要特别注意的是向下取整与向零取整的差异对于负数结果例如-7 / 2 -3.5向下取整得到-4而 C 语言中整数除法向零取整得到-3。从 floor_div_dag.h 中可以看到整数 Kernel 通过判断两操作数符号是否相异signs_differ来修正商当符号相异且余数不为 0 时商减 1从而严格保证 floor 语义。从算子定义看FloorDiv 在 Graph 层注册的算子名称为FloorDiv输入为x1、x2输出为y见 floor_div_def.cppaclnn 层的self/other/out与之一一对应。二、产品支持情况根据 math/floor_div/README.mdFloorDiv 算子在不同产品的支持情况如下产品是否支持Ascend 950PR/Ascend 950DT√Atlas A3 训练系列产品/Atlas A3 推理系列产品√Atlas A2 训练系列产品/Atlas A2 推理系列产品√Atlas 200I/500 A2 推理产品×Atlas 推理系列产品√Atlas 训练系列产品√其中Atlas 训练系列产品不支持 BFLOAT16 数据类型。对于aclnnFloorDivides标量版本Atlas 推理系列产品同样不支持详见 aclnnFloorDividesaclnnInplaceFloorDivides.md。三、算子接口全景四种 aclnn 接口FloorDiv 在 aclnn 层提供四组接口其头文件声明位于 aclnn_floor_divide.h接口特点适用场景aclnnFloorDivideTensor ÷ Tensor需新建输出张量两个输入均为 Tensor 的常规计算aclnnInplaceFloorDivideTensor ÷ Tensor结果直接写回 selfRef无需保留原输入、希望省内存aclnnFloorDividesTensor ÷ Scalar需新建输出张量除数是一个标量如除以 2.0aclnnInplaceFloorDividesTensor ÷ Scalar结果直接写回 selfRef标量除数 原地更新非 Inplace 接口与 Inplace 接口实现相同功能区别仅在于是否新建输出张量aclnnFloorDivide/aclnnFloorDivides需要调用方新建out张量存放结果aclnnInplaceFloorDivide/aclnnInplaceFloorDivides无需输出张量直接在输入selfRef张量的内存中覆盖写入计算结果。每个算子均采用 CANN 通用的两段式接口模式先调用GetWorkspaceSize接口完成入参校验、计算所需 workspace 大小并生成包含算子计算流程的执行器executor再调用执行接口真正下发计算任务。3.1 函数原型Tensor-Tensor 版本aclnnStatus aclnnFloorDivideGetWorkspaceSize( const aclTensor* self, const aclTensor* other, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor) aclnnStatus aclnnFloorDivide( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream) aclnnStatus aclnnInplaceFloorDivideGetWorkspaceSize( aclTensor* selfRef, const aclTensor* other, uint64_t* workspaceSize, aclOpExecutor** executor) aclnnStatus aclnnInplaceFloorDivide( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)3.2 函数原型Tensor-Scalar 版本aclnnStatus aclnnFloorDividesGetWorkspaceSize( const aclTensor* self, const aclScalar* other, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor) aclnnStatus aclnnFloorDivides( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream) aclnnStatus aclnnInplaceFloorDividesGetWorkspaceSize( aclTensor* selfRef, const aclScalar* other, uint64_t* workspaceSize, aclOpExecutor** executor) aclnnStatus aclnnInplaceFloorDivides( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)四、第一段接口参数详解aclnnFloorDivideGetWorkspaceSize以 Tensor 版本为例第一段接口参数说明如下参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续 TensorselfaclTensor*输入公式中的输入 self数据类型需与 other 满足互推导关系shape 需与 other 满足 broadcast 关系FLOAT、FLOAT16、DOUBLE、INT32、INT64、INT16、INT8、UINT8、BOOLND不超过 8 维√otheraclTensor*输入公式中的输入 other数据类型需与 self 满足互推导关系shape 需与 self 满足 broadcast 关系FLOAT、FLOAT16、DOUBLE、INT32、INT64、INT16、INT8、UINT8、BOOLND不超过 8 维√outaclTensor*输出公式中的输出 out数据类型需是 self 与 other 推导之后可转换的类型shape 需是 broadcast 之后的 shapeFLOAT、FLOAT16、DOUBLE、INT32、INT64、INT16、INT8、UINT8、COMPLEX64、COMPLEX128ND不超过 8 维√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小-----executoraclOpExecutor**输出返回 op 执行器包含算子计算流程-----补充说明与 README 中算子级参数表一致BFLOAT16 扩展Atlas A2 训练系列产品/Atlas A2 推理系列产品、Atlas A3 训练系列产品/Atlas A3 推理系列产品、Ascend 950PR/Ascend 950DT 上self、other、out额外支持 BFLOAT16aclnnFloorDivides在 Ascend 950PR/Ascend 950DT 上还要求数据类型满足 Tensor-Scalar 互推导关系。数据格式仅支持 ND 格式且 self、other、out 的格式需要一致参见数据格式说明。非连续 Tensor三个 Tensor 参数均支持非连续输入参见非连续 Tensor 说明op_api 内部会先做Contiguous归一化处理。维度限制shape 不超过 8 维超过则报参数非法错误。对于aclnnFloorDivides的标量版本other为aclScalar*无 shape/format/非连续概念仅要求与self满足数据类型推导规则self的维度同样不超过 8 维。Inplace 版本的第一段接口aclnnInplaceFloorDivideGetWorkspaceSize参数为selfRef输入/输出、other、workspaceSize、executorselfRef同时充当输入与输出数据类型、shape、format 约束与self相同。五、返回值与错误码所有第一段接口均返回aclnnStatus状态码具体语义参见 aclnn 返回码。第一段接口在入参校验阶段发现非法输入时报错Tensor-Tensor 版本的典型错误场景如下返回值错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 self、other 或 out 是空指针ACLNN_ERR_PARAM_INVALID161002self 和 other 的数据类型和数据格式不在支持的范围之内ACLNN_ERR_PARAM_INVALID161002self 和 other 不满足数据类型推导规则ACLNN_ERR_PARAM_INVALID161002推导出的数据类型无法转换为指定输出 out 的类型ACLNN_ERR_PARAM_INVALID161002self 和 other 的 shape 无法做 broadcastACLNN_ERR_PARAM_INVALID161002self 和 other 的维度大于 8Inplace 版本aclnnInplaceFloorDivideGetWorkspaceSize的错误场景为selfRef或other为空指针161001数据类型/格式不在支持范围、不满足推导规则、shape 无法 broadcast、维度大于 8161002。Scalar 版本aclnnFloorDividesGetWorkspaceSize则额外包含推导出的数据类型无法转换为指定输出 out 的类型这一错误场景且维度检查只针对self。第二段执行接口如aclnnFloorDivide的入参为参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口获取executor输入op 执行器包含了算子计算流程stream输入指定执行任务的 Stream六、完整调用示例aclnnFloorDivide 与 aclnnInplaceFloorDivide以下示例代码来自 test_aclnn_floor_divide.cpp仓库中另有 test_aclnn_floor_divides.cpp 演示 Scalar 版本完整展示了从资源初始化、Tensor 构造、两段式调用到结果回拷与资源释放的整个流程。编译与执行环境搭建请参考编译与运行样例。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_floor_divide.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); aclFinalize(); return ret); ret aclrtCreateStream(stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtCreateStream failed. ERROR: %d\n, ret); aclrtResetDevice(deviceId); aclFinalize(); 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 otherShape {4, 2}; std::vectorint64_t outShape {4, 2}; void* selfDeviceAddr nullptr; void* otherDeviceAddr nullptr; void* outDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* other nullptr; aclTensor* out nullptr; std::vectorfloat selfHostData {0, 1, 2, 3, 4, 5, 6, 7}; std::vectorfloat otherHostData {1, 1, 1, 2, 2, 2, 3, 3}; std::vectorfloat outHostData(8, 0); // 创建self aclTensor ret CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建other aclTensor ret CreateAclTensor(otherHostData, otherShape, otherDeviceAddr, aclDataType::ACL_FLOAT, other); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建out aclTensor ret CreateAclTensor(outHostData, outShape, outDeviceAddr, aclDataType::ACL_FLOAT, out); CHECK_RET(ret ACL_SUCCESS, return ret); // 3. 调用CANN算子库API需要修改为具体的API名称 uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnFloorDivide第一段接口 ret aclnnFloorDivideGetWorkspaceSize(self, other, out, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnFloorDivideGetWorkspaceSize 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); } // 调用aclnnFloorDivide第二段接口 ret aclnnFloorDivide(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnFloorDivide failed. ERROR: %d\n, ret); return ret); uint64_t inplaceWorkspaceSize 0; aclOpExecutor* inplaceExecutor; // 调用aclnnInplaceFloorDivide第一段接口 ret aclnnInplaceFloorDivideGetWorkspaceSize(self, other, inplaceWorkspaceSize, inplaceExecutor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnInplaceFloorDivideGetWorkspaceSize failed. ERROR: %d\n, ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 void* inplaceWorkspaceAddr nullptr; if (inplaceWorkspaceSize 0) { ret aclrtMalloc(inplaceWorkspaceAddr, inplaceWorkspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(allocate workspace failed. ERROR: %d\n, ret); return ret); } // 调用aclnnInplaceFloorDivide第二段接口 ret aclnnInplaceFloorDivide(inplaceWorkspaceAddr, inplaceWorkspaceSize, inplaceExecutor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnInplaceFloorDivide 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::vectorfloat 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]); } auto inplaceSize GetShapeSize(selfShape); std::vectorfloat inplaceResultData(inplaceSize, 0); ret aclrtMemcpy(inplaceResultData.data(), inplaceResultData.size() * sizeof(inplaceResultData[0]), selfDeviceAddr, inplaceSize * sizeof(float), 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 inplaceSize; i) { LOG_PRINT(inplaceResult[%ld] is: %f\n, i, inplaceResultData[i]); } // 6. 释放aclTensor和aclScalar需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyTensor(other); aclDestroyTensor(out); // 7. 释放device资源需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(otherDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }上述样例中self {0,1,2,3,4,5,6,7}、other {1,1,1,2,2,2,3,3}计算结果为out {0,1,2,1,2,2,2,2}Inplace 版本执行后self内存中的值会被原地覆盖为同样的结果。Scalar 版本aclnnFloorDivides的核心差异在于other使用aclCreateScalar(otherValue, aclDataType::ACL_FLOAT)创建aclScalar*且输出 shape 必须与self完全一致标量除不产生 broadcast 扩展释放资源时使用aclDestroyScalar。七、源码级实现原理7.1 op_api 层统一的数据类型推导与执行流FloorDiv 的 aclnn 实现位于 aclnn_floor_divide.cpp核心执行函数CalcFloorDivide的调用链如下入参校验CheckParams依次执行空指针检查、dtype 支持列表检查、类型推导检查CheckPromoteType、shape 与 broadcast 检查CheckShape空 Tensor 短路self-IsEmpty() || other-IsEmpty()时直接返回成功不执行计算执行图构建Contiguous非连续 Tensor 归一化→Cast输入提升到 promoteType→l0op::FloorDiv核心除算子→Cast结果转换到 out 的数据类型→ViewCopy写入输出张量。其中 dtype 支持列表按 NPU 架构区分aclnn_floor_divide.cppASCEND910_DTYPE_SUPPORT_LIST包含 FLOAT16、FLOAT、INT64、INT32、INT16、INT8、UINT8、DOUBLE、BOOLASCEND910B_DTYPE_SUPPORT_LIST在此基础上额外包含 BF16与 README 中A2/A3/950 额外支持 BFLOAT16的说明一致。从该实现可以推断aclnn 层先通过PromoteType推导出统一的中间类型再对输入做 Cast 提升因此 README 中列出的 DOUBLE、INT16、BOOL 等类型如与另一输入推导后落入支持列表也可以正常计算。Scalar 版本还实现了InferFloorDivTensorScalarDtype对 Tensor-Scalar 组合做专门推导并在 950 等架构上将 FLOAT16/BFLOAT16 提升为 FLOAT 参与数学运算以提高精度。7.2 op_host 层算子定义、shape 推导与 Tiling算子定义floor_div_def.cpp 通过OpDef注册FloorDiv输入x1/x2与输出y的数据类型支持 BF16、FLOAT16、FLOAT、INT32、UINT8、INT8、INT64格式均为 ND并为ascend950、ascend350两个架构添加 AICore 配置支持动态编译、动态 shape、动态 rank关闭格式动态。shape 推导floor_div_infershape.cpp 直接复用Ops::Base::InferShape4Broadcast即输出 shape 由两个输入按广播规则推导得出。Tilingfloor_div_tiling_arch35.cpp 首先要求x1、x2数据类型一致然后按 dtype 分发到不同的BroadcastBaseTiling模板INT64/INT32 走整数版、FLOAT16/BF16 走先 Cast 到 float 再计算的浮点版、FLOAT 直接走 float 版、UINT8/INT8 走对应整数版其余 dtype 报错。Tiling 过程还通过GetCoreMemSize(CoreMemType::UB)获取片上 UB 大小并在PostTiling中预留DCACHE_SIZE32KB后将剩余空间设置为算子可用 Local Memory。7.3 op_kernel 层DAG 化 Kernel 与 floor 语义实现Kernel 采用 DAG有向无环图方式描述计算流水定义见 floor_div_dag.h按数据类型拆分为多套 OpDag浮点无 CastFLOATCopyInBrc → DivHighPrecision → Truncate(floor) → CopyOut浮点有 CastFLOAT16/BF16先Cast到 FLOAT 做高精度除法floor 后再Cast回原类型DIV_CAST_MODE_RINT避免低精度浮点直接除法放大误差INT32/INT64使用 SIMT 向量核函数FloorDivInt_1通过signs_differ判断符号差异并按余数修正商严格实现向下取整INT8/UINT8先拓宽到 INT16/UINT16 做除法避免 8 位整型中间溢出再做饱和转换回原类型SAT_POS 60控制饱和模式。Kernel 入口 floor_div_apt.cpp 根据模板参数DTYPE_X1在编译期选择对应 OpDag并通过BroadcastSch调度执行。各 dtype 对应的二进制产物映射关系由 floor_div_binary.json 描述如FloorDiv_FLOAT32、FloorDiv_INT64等shape: [-2]表示支持动态 shape。八、约束与精度说明确定性计算aclnnFloorDivide、aclnnInplaceFloorDivide、aclnnFloorDivides、aclnnInplaceFloorDivides默认均为确定性实现多次调用结果一致。低精度误差在 Atlas A2 训练系列产品/Atlas A2 推理系列产品、Atlas A3 训练系列产品/Atlas A3 推理系列产品上因 FLOAT16/BFLOAT16 精度有限、无法表示所有小数向下取整时存在一定误差。若对精度敏感可以选择更高精度的数据类型如 FLOAT32参与计算——这与 op_api 层FLOAT16/BF16 先提升到 FLOAT 再计算的实现相呼应而 950 架构上的标量版本甚至直接采用该提升策略保证精度。九、测试与验证仓库为 FloorDiv 提供了多层次的测试用例可用于验证实现正确性test_aclnn_floor_divide.cppaclnn API 层单元测试覆盖 Tensor 与 Scalar 两套接口test_floor_div_infershape.cppshape 推导广播单测test_floor_div_tiling_arch35.cppTiling 单测atk_aclnnFloorDivide.json 与 ttk_kernel_floor_div_st.csv系统级ST测试用例描述覆盖各 dtype 组合与典型 shape。十、总结FloorDiv 是 ops-math 中实现简单但细节严谨的算子数学语义上严格遵循向下取整而非向零取整实现上通过 op_api 层的数据类型推导 op_host 层的动态 Tiling op_kernel 层的 DAG 流水三层协作覆盖了从 FLOAT/BFLOAT16 到 INT8/INT64 的广泛数据类型与 ND 格式的非连续 Tensor 输入。无论你是在做框架适配、模型迁移还是算子二次开发掌握aclnnFloorDivide/aclnnFloorDivides及其 Inplace 版本的两段式调用方式并理解其类型推导与 floor 语义细节都能帮助你在 Ascend NPU 上写出正确、高效的向下取整除法计算代码。【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考