CANN ops-math 算子实战:aclnnFmodTensor 与 aclnnInplaceFmodTensor 张量取余接口全解析
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
导读
aclnnFmodTensor/aclnnInplaceFmodTensor是 CANN ops-math 仓库中 Mod(Fmod,截断取余)算子在 aclnn(Ascend CANN 轻量算子接口)层的张量形态对外接口,用于对self与可广播的other执行out = self - other * trunc(self / other)逐元素取余。本文以 关联文档 为核心骨架,结合 op_api 实现、tiling 实现 与 UT 用例 等仓库源码,完整讲解两段式接口的调用流程、数据类型与产品约束、A2/A3 平台的 INT16 增强与大商数值稳定性算法,并给出可直接编译运行的完整示例,帮助读者在 NPU 上正确、高效地完成张量取余计算。
功能说明与数学语义
Mod 算子返回self除以other的余数,采用截断取余(trunc-mod,结果符号跟随被除数)语义:
$$ out_{i} = self_{i} - other \times trunc(self_{i} / other) $$
核心要点(见 关联文档 与 README 功能说明):
other需要能广播(broadcast)到self,广播后逐元素计算;out的 shape 必须与self完全一致(余数形状跟随被除数);self、other、out均支持 ND 格式,维度不超过 8 维。
接口原型:两段式(GetWorkspaceSize + Execute)异步调用
aclnnFmodTensor与aclnnInplaceFmodTensor均遵循 aclnn 两级接口约定:先调用GetWorkspaceSize完成参数校验与图构建,查询所需 workspace 大小并得到executor;再调用执行接口在指定stream上异步下发算子。两个接口的原型如下(见 关联文档 与 接口头文件):
aclnnStatus aclnnFmodTensorGetWorkspaceSize( const aclTensor* self, const aclTensor* other, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor); aclnnStatus aclnnFmodTensor( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream); aclnnStatus aclnnInplaceFmodTensorGetWorkspaceSize( aclTensor* selfRef, const aclTensor* other, uint64_t* workspaceSize, aclOpExecutor** executor); aclnnStatus aclnnInplaceFmodTensor( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream);两者区别仅在于:
- aclnnFmodTensor:非原地版本,
self、other、out三个张量独立传入,结果写入out; - aclnnInplaceFmodTensor:原地版本,只有
selfRef(同时充当被除数与输出)和other,结果直接写回selfRef。从 源码 可以看到,原地版本内部通过auto out = const_cast<aclTensor*>(selfRef);将out指向selfRef,并复用CheckParamsInplaceTensorTensor做参数校验后,与普通版本共享同一条ExecFmodTensorGetWorkspaceSize执行链路。
参数说明
| 参数名 | 输入/输出/属性 | 描述 | 数据类型 | 数据格式 |
|---|---|---|---|---|
| self | 输入 | 待进行 mod 计算的入参,即公式中的 self_i(被除数) | BFLOAT16、FLOAT16、FLOAT32、INT32、INT16* | ND |
| other | 输入 | 待进行 mod 计算的入参,即公式中的 other(除数) | BFLOAT16、FLOAT16、FLOAT32、INT32、INT16* | ND |
| out | 输出 | 待进行 mod 计算的出参,即公式中的 out_i(余数) | BFLOAT16、FLOAT16、FLOAT32、INT32、INT16* | ND |
*INT16 同数据类型计算,以及self/other分别为 INT16 与 BFLOAT16/FLOAT16/FLOAT32 的混合数据类型计算,仅由 Atlas A2 训练系列产品/Atlas A2 推理系列产品、Atlas A3 训练系列产品/Atlas A3 推理系列产品的 AICore 支持;其余产品上该增强不适用,BFLOAT16/FLOAT16/FLOAT32/INT32 的既有支持不受影响。
约束与数据类型支持
形状约束
self、other、out支持 ND,维度不超过 8 维(源码中通过CheckTensorDimSize校验,见 aclnn_fmod_tensor.cpp);other必须可广播到self,且广播结果 shape 必须等于selfshape;outshape 必须等于selfshape。这两条校验实现在 CheckBroadcastShape:先计算广播 shape,再与self->GetViewShape()、out->GetViewShape()逐一比对,不满足即返回ACLNN_ERR_PARAM_INVALID。
数据类型与产品差异化
aclnn 层支持 DOUBLE、BFLOAT16、FLOAT16、FLOAT32、INT32、INT64、INT8、UINT8、INT16 的类型推导;AICore kernel 直接覆盖 BFLOAT16、FLOAT16、FLOAT32、INT32,其余类型(DOUBLE/INT64/INT8/UINT8)走 AICPU fallback。其中:
- INT16 同数据类型计算,以及INT16 与 BFLOAT16/FLOAT16/FLOAT32 的混合数据类型计算,是 Atlas A2 / Atlas A3 平台(AICore)的专属增强;
- 其余产品上的 INT16 增强不适用,但已有的 BFLOAT16/FLOAT16/FLOAT32/INT32 同数据类型计算与 DOUBLE/INT64/INT8/UINT8 的 AICPU 回退行为保持不变。
从 op_api 源码 可以看到不同 NPU 架构的 dtype 支持列表是分架构维护的:ASCEND910B_DTYPE_DTYPE_SUPPORT_LIST(对应DAV_2201及 RegBase,即 Atlas A2/A3 平台)在原有 DOUBLE/BF16/FP16/FP32/INT32/INT64/INT8/UINT8 基础上新增了 INT16(源码注释明确标注 "int16 同 dtype lane 的 L2 门控 (A2)");而ASCEND910_DTYPE_DTYPE_SUPPORT_LIST(DAV_2002)与ASCEND310P_DTYPE_DTYPE_SUPPORT_LIST(DAV_1001)均不含 INT16。
精度说明:大商场景数值稳定性增强
针对self/other商值较大(大 |self/other|)的场景,Atlas A2/A3 上的 AICore 计算路径引入了数值稳定性增强算法,相比朴素截断取余(trunc-mod)实现,降低了大商场景下的精度损失风险。该增强与 INT16/混合数据类型能力一并限定于 Atlas A2/A3,其余产品的既有算法与精度行为不变(见 README 精度说明)。
其工程实现在 mod_tiling.cpp 中:
- 定义了自适应路由阈值
FMOD_NAIVE_THRESH_DEFAULT = 256.0f,当 |商| 超过该阈值时路由到增强算法(源码注释为 "AlgoA 大商精度路"); - 该阈值可由环境变量
FMOD_NAIVE_THRESH覆盖,供真机精度 sweep 使用(生产环境默认不设置);FmodNaiveThresh 对非法输入(无法解析、溢出、非有限值、非正值)会安全回退到默认阈值; - FP32/FP16/INT16/INT32 各 dtype 按 kernel 缓冲区的实际占用分别使用不同的 UB 切分因子(
UB_DIVIDER_FP32=69、UB_DIVIDER_FP16=65、UB_DIVIDER_INT16=45、UB_DIVIDER_INT32=69),其中 INT16 同 dtype 走整数域 naive 路径、不分配 A1..A5 工作块,因此每元素占用更低;连续派发(isInput2Scalar || isInput2SameShape)的同 dtype FP32/FP16/BF16 场景还会走 kernel 精简核(USE_LEAN_CONTIG),UB 切分因子进一步下调到 48,以获得更宽的 tile。
产品支持情况
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR/Ascend 950DT | √ |
| Atlas A3 训练系列产品/Atlas A3 推理系列产品 | √ |
| Atlas A2 训练系列产品/Atlas A2 推理系列产品 | √ |
| Atlas 200I/500 A2 推理产品 | × |
| Atlas 推理系列产品 | √ |
| Atlas 训练系列产品 | √ |
(来源:README 产品支持情况)
完整调用示例:两段式接口实战
仓库在 examples/test_aclnn_fmod_tensor.cpp 提供了可直接参考的完整示例。整个调用流程分为初始化设备、构造 aclTensor、两段式执行、读取结果、释放资源五个阶段,核心执行逻辑如下:
int Compute(aclrtStream stream, aclTensor* self, aclTensor* other, aclTensor* out) { uint64_t workspaceSize = 0; aclOpExecutor* executor; // 第一阶段:查询 workspace 大小并构建执行器 auto ret = aclnnFmodTensorGetWorkspaceSize(self, other, out, &workspaceSize, &executor); if (ret != ACL_SUCCESS) { LOG_PRINT("aclnnFmodTensorGetWorkspaceSize failed. ERROR: %d\n", ret); return ret; } void* workspaceAddr = nullptr; if (workspaceSize > 0) { ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); if (ret != ACL_SUCCESS) { LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret; } } // 第二阶段:在指定 stream 上异步执行 ret = aclnnFmodTensor(workspaceAddr, workspaceSize, executor, stream); if (ret != ACL_SUCCESS) { LOG_PRINT("aclnnFmodTensor failed. ERROR: %d\n", ret); return ret; } ret = aclrtSynchronizeStream(stream); if (ret != ACL_SUCCESS) { LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret; } if (workspaceSize > 0) { aclrtFree(workspaceAddr); } return 0; }示例主函数中构造了{4, 4}的 FLOAT 类型输入:
std::vector<int64_t> selfShape = {4, 4}; std::vector<int64_t> otherShape = {4, 4}; std::vector<int64_t> outShape = {4, 4}; std::vector<float> selfData = {5.5, -11.51, 36.23, 7, -10, -8, -15, -7, 10, 8, 15, 7, -10, -8, -15, -7}; std::vector<float> otherData = {2, 3, -24.1, 2, 3, 5, 4, 2, -3, -5, -4, -2, -3, -5, -4, -2};使用要点总结:
- 初始化:先
aclInit(nullptr)、aclrtSetDevice(deviceId)、aclrtCreateStream(&stream); - 张量构造:通过
aclrtMalloc+aclrtMemcpy(HOST_TO_DEVICE)准备设备侧数据,再用aclCreateTensor创建 ND 格式、带连续 strides 的aclTensor; - workspace 分配:仅当
workspaceSize > 0时才需要aclrtMalloc分配 workspace,执行完毕记得aclrtFree; - 异步语义:
aclnnFmodTensor为异步下发,必须aclrtSynchronizeStream(stream)后再读取结果; - 结果回读:
aclrtMemcpy(DEVICE_TO_HOST)将结果拷回 host 打印; - 资源释放:依次
aclDestroyTensor、aclrtFree、aclrtDestroyStream、aclrtResetDevice、aclFinalize。
源码级实现剖析:从 aclnn 到 AICore 的完整链路
op_api 层:参数校验与 dtype 归一化
aclnn_fmod_tensor.cpp 是 aclnn 接口的核心实现,非原地入口aclnnFmodTensorGetWorkspaceSize(L694-L702)依次完成:
- 空指针检查:
CheckNotNullTensorTensor校验self/other/out非空; - 类型推导检查:
CheckPromoteType计算self与other的 promote 类型,要求其能 cast 成out,且属于当前 NPU 架构的 dtype 支持列表;当out为 INT16 时,要求 promote 类型必须是{FLOAT32, FLOAT16, BFLOAT16, INT16}之一(源码 L234-L273); - 广播与形状检查:
CheckBroadcastShape校验广播合法性及outshape 与self一致; - 维度检查:
CheckTensorDimSize限制不超过 8 维。
随后ExecFmodTensorGetWorkspaceSize(L567-L598)按计算 dtype 选择执行路径:
- AICore 路径:当 promote 类型或计算类型属于
{BF16, FP16, FP32, INT32, INT16}时(IsAiCoreComputeDtype,L145-L150),对self/other先Contiguous归一,再以计算 dtype 做 Mod,最后把结果 cast 到out的 dtype 并ViewCopy落盘。这一设计保证窄输出(尤其是out=int16)不会改变运算精度域——先按 promote 类型算,最后才收窄; - 混合 dtype 的 FP32 桥接:
CastWithFp32Bridge(L169-L188)针对INT16 ↔ BF16之间的互转做了特殊处理——DAV_2201 架构没有 int16 与 bf16 之间的直接 Cast lane,因此统一经 FP32 中转(int16/bf16 -> fp32是精确的,最终 Cast 的收窄与直接转换等价); - 通用路径(AICPU fallback):对于 DOUBLE/INT64/INT8/UINT8 等非 AICore dtype,
InitializeTensor先将张量转连续、0 维转 1 维、cast 到 promote 类型,BroadcastTensor广播到outshape 后交给底层l0op::Mod完成。
host 层:shape 推导与 tiling
- shape 推导:mod_infershape.cpp 中
InferShape4Mod校验other能右对齐广播到self(CanBroadcastOtherToSelf逐维判断otherDim == 1 || otherDim == selfDim),然后令输出 shape 直接继承selfshape(*yShape = *xShape),印证了"余数形状跟随被除数"的语义; - tiling:mod_tiling.cpp 的
ModTilingForGe读取 x1/x2/y 三个 dtype 并映射为编译期 tiling key(MOD_TPL_FP32/FP16/BF16/INT32/INT16),按每核最小 1024 元素、UB 容量与 dtype 对应的ubDivider计算needCoreNum、perCoreDataCount等切分参数;此外还会对通用广播场景尝试"融合广播"tiling(SetFusedBroadcastTiling),将右对齐后恰好为 OUTER(行)广播或 INNER(列)广播的二维折叠形状命中为融合路径,降低广播场景的访存开销;最终SetBlockDim(tilingData->needCoreNum)并设置 32MB 的 workspace(WORK_SPACE_SIZE); - 算子定义:mod_def.cpp 注册
Mod算子的 x1/x2/y 输入输出,dtype 三元组为{BF16, FP16, FP32, INT32, INT16}(kernel 只暴露同 dtype 原型,跨 dtype 由 aclnn 层先 promote + cast),并声明AICore().AddConfig("ascend910b")与AICore().AddConfig("ascend910_93")——即该 AICore kernel 仅在 Atlas A2/A3 平台(DAV_2201)注册,这与文档"INT16 增强仅 A2/A3"的产品边界完全一致。
kernel 层:同 dtype 分发与 AlgoA 数值稳定性
mod_dispatch_impl.h 展示了ModKernelDispatchSameDtype的五个同 dtype 分发 lane:
INT32 -> Mod<int>;FP16 -> Mod<half>;FP32 -> Mod<float>;BF16 -> Mod<bfloat16_t>(在__NPU_ARCH__ == 3003上不编译);INT16 -> Mod<int16_t>(受MOD_ENH_ARCH22宏保护,即 A2/A3 增强)。
结合 op_kernel 目录 下的mod_compute_impl.h、mod_algoa_impl.h、mod_bcast_impl.h、mod_flat_impl.h、mod_leancontig_impl.h、mod_int32_impl.h等实现文件,可以推断 kernel 层按输入几何形态(标量/同形/广播/扁平/连续精简)与大商判定分别走不同的计算内核,其中mod_algoa_impl.h即大商数值稳定性增强算法(AlgoA)的实现载体。
测试与验证
仓库为 Tensor 形态接口提供了完整的 UT 覆盖:
- test_aclnn_fmod_tensor.cpp:覆盖
float_same_shape、fp16_broadcast({2,3,5}对{1,3,1}广播)、invalid_broadcast(非法广播返回ACLNN_ERR_PARAM_INVALID)、int16_same_dtype(INT16 同 dtype 新 lane)、mixed_int16_fp32_promote_to_fp32(INT16 self × FP32 other 归一化到 FP32 计算)、mixed_int16_fp32_to_int16(FP32 计算后 cast 回 INT16 输出)等关键场景; - test_aclnn_inplace_fmod_tensor.cpp:覆盖原地版本的
float_same_shape与invalid_broadcast; - host 侧另有 test_mod_infershape.cpp 与 test_mod_tiling.cpp 验证 shape 推导与 tiling 切分。
这些用例从三个维度印证了文档约束:广播合法性会被拒绝、FP32 计算 + 收窄输出的混合 dtype 语义正确、INT16 增强仅在 A2/A3 dtype 支持列表内放行。
常见问题与使用建议
- 返回
ACLNN_ERR_PARAM_INVALID:优先检查other是否可广播到self、outshape 是否严格等于selfshape、维度是否超过 8 维、promote 类型是否在架构 dtype 支持列表内; - INT16 报不支持:INT16 同/混合 dtype 计算仅限 Atlas A2/A3 训练/推理系列产品(AICore),其他产品请改用 INT32 等既有支持类型;
- 原地版本别忘自引用:
aclnnInplaceFmodTensor没有独立的out,结果覆盖selfRef,调用前应确认原始self数据不再需要; - workspace 不要无条件分配:遵循示例代码,仅当
workspaceSize > 0时再aclrtMalloc,避免不必要的显存开销; - 大商精度敏感场景:Atlas A2/A3 上 AICore 已内置 AlgoA 大商数值稳定性增强(默认阈值 256,可用
FMOD_NAIVE_THRESH环境变量覆盖以做真机 sweep),建议在精度敏感应用中使用该路径;非 A2/A3 平台仍为朴素 trunc-mod 语义,精度行为保持不变。
延伸阅读
- Tensor 形态配套接口:aclnnFmodScalar & aclnnInplaceFmodScalar(
self为张量、other为标量的取余接口); - Mod 算子总览(参数表、贡献说明、调用方式汇总):experimental/math/mod/README.md;
- 完整示例:examples/test_aclnn_fmod_tensor.cpp。
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考