Ascend C 高阶数学接口 Trunc 详解:向零取整算子开发与临时空间管理

原创2026-10-15 19:19:241,452 阅读
文章标签:人工智能深度学习高性能计算CANN

Ascend C 高阶数学接口 Trunc 详解:向零取整算子开发与临时空间管理

Trunc 是 asc-devkit 提供的 Ascend C 高阶 API(位于 adv_api/math_compute 模块)中用于按元素执行浮点数向零取整的接口,与 Ceil(向正无穷取整)、Floor(向负无穷取整)、Round(四舍五入)共同构成取整类高阶 API 家族。本文基于 Trunc_interface 系列文档,结合仓库内 接口头文件、核函数侧实现、Host 侧 Tiling 实现 与 单元测试,完整讲解 Trunc 的调用方式、内部实现原理、临时空间获取方法,以及基于 maxLiveNodeCount/extraBuf 的单次计算元素数量推算公式,帮助开发者直接上手完成截断取整算子的 Kernel 与 Tiling 开发。

功能说明:向零取整的数学语义

Trunc 接口按元素对浮点数执行截断操作,即向零取整(丢弃小数部分,不做四舍五入,也不向正/负无穷方向进位)。其计算公式为:

dstTensor_i = Trunc(srcTensor_i)

行为示例:

  • Trunc(3.9) = 3
  • Trunc(-3.9) = -3

即正数直接舍去小数部分,负数同样向零方向截断,这与 C/C++ 标准库中 trunc() 的语义一致。实现层面的目标正如 trunc.h 头文件注释所描述:"Get the interger part of float value, towards zero"(获取浮点值的整数部分,方向为零)。

注意:本接口与"四舍五入"(Round)、"向下取整"(Floor)不同,特别适合对负数的处理要求严格的场景,例如数据归一化、量化前处理、坐标对齐等需要确定性截断的算子。

产品支持情况

根据 Trunc.md 文档的产品支持矩阵,Trunc 接口的支持情况如下:

产品 支持情况
Ascend 950PR&950DT 系列产品 支持
Atlas A3 系列产品 支持
Atlas A2 系列产品 支持
Atlas 200I/500 A2 推理产品 不支持
Atlas 推理系列产品 AI Core 支持
Atlas 推理系列产品 Vector Core 不支持
Atlas 训练系列产品 不支持

从源码角度看,trunc.h 通过 __NPU_ARCH__ 宏(2201、2002、3510、9201、5102、5162、3003、3113)决定是否编译该接口以及选择哪套实现,与上述产品矩阵对应。在不受支持的架构上,接口调用体为空实现,不会被实际执行。

函数原型:四种重载形态

Trunc 是模板函数,共提供 4 个重载,按"临时空间的提供方式 × 参与计算的元素范围"两个维度划分。

通过 sharedTmpBuffer 入参传入临时空间:

  • 源操作数 Tensor 全部/部分参与计算:
template <typename T, bool isReuseSource = false>
__aicore__ inline void Trunc(const LocalTensor<T>& dstTensor, const LocalTensor<T>& srcTensor,
                             const LocalTensor<uint8_t>& sharedTmpBuffer, const uint32_t calCount)
  • 源操作数 Tensor 全部参与计算:
template <typename T, bool isReuseSource = false>
__aicore__ inline void Trunc(const LocalTensor<T>& dstTensor, const LocalTensor<T>& srcTensor,
                             const LocalTensor<uint8_t>& sharedTmpBuffer)

由接口框架申请临时空间:

  • 源操作数 Tensor 全部/部分参与计算:
template <typename T, bool isReuseSource = false>
__aicore__ inline void Trunc(const LocalTensor<T>& dstTensor, const LocalTensor<T>& srcTensor,
                             const uint32_t calCount)
  • 源操作数 Tensor 全部参与计算:
template <typename T, bool isReuseSource = false>
__aicore__ inline void Trunc(const LocalTensor<T>& dstTensor, const LocalTensor<T>& srcTensor)

在 trunc.h 中可以看到,不带 calCount 的两个重载分别等价于以 srcTensor.GetSize() 作为 calCount 的完整调用,即默认处理整个源 Tensor 的元素。

临时空间机制:两种方式的取舍

由于 Trunc 的内部实现涉及精度转换,需要额外临时空间存储计算过程中的中间变量。临时空间支持两种方式:

方式一:接口框架申请临时空间

开发者无需自行申请,但必须预先预留足够的临时空间大小。从实现看,trunc_common_impl.h 中不带 sharedTmpBuffer 参数的重载通过 PopStackBuffer<uint8_t, TPosition::LCM> 从 LCM(Local Memory)栈上弹出临时 buffer,并通过 ASCENDC_ASSERT 校验栈分配成功;使用完后随栈生命周期释放。

方式二:通过 sharedTmpBuffer 入参传入

开发者自行管理临时空间的申请、复用与释放。接口内部以该 Tensor 作为临时空间,不再申请额外内存。其优势在于:内存不反复申请释放,接口调用完成后该部分内存可被复用,灵活性更高、内存利用率更高。

无论采用哪种方式,临时空间大小 BufferSize 的获取都通过 Host 侧 Tiling 接口 GetTruncMaxMinTmpSize 完成(详见下文"Host 侧辅助接口"一节)。

参数说明

模板参数

参数名 描述
T 操作数的数据类型,支持 half、float
isReuseSource 是否允许修改源操作数。该参数预留,传入默认值 false 即可

接口参数

参数名 输入/输出 描述
dstTensor 输出 目的操作数。类型为 LocalTensor,支持的 TPosition 为 VECIN/VECCALC/VECOUT
srcTensor 输入 源操作数。类型为 LocalTensor,支持的 TPosition 为 VECIN/VECCALC/VECOUT
sharedTmpBuffer 输入 临时缓存。类型为 LocalTensor,支持的 TPosition 为 VECIN/VECCALC/VECOUT。用于 Trunc 内部复杂计算时存储中间变量,由开发者提供。临时空间大小 BufferSize 的获取方式参考 GetTruncMaxMinTmpSize
calCount 输入 参与计算的元素个数

返回值:无。

约束说明

  • 针对 Atlas 推理系列产品 AI Core,输入数据限制在 [-2147483647.0, 2147483647.0] 范围内(该限制与内部经 int32 中间类型转换的实现相关)。
  • 支持源操作数与目的操作数地址重叠(srcTensor 与 dstTensor 可指向同一片内存)。
  • 不支持 sharedTmpBuffer 与源操作数、目的操作数地址重叠。
  • 操作数地址对齐要求参见 通用地址对齐约束(源码注释也明确指出 src/dst Tensor 必须 32B 对齐,见 trunc.h)。

源码剖析:内部实现原理

Trunc 核函数侧实现按产品架构分为两套:一套是 trunc_common_impl.h(适配 __NPU_ARCH__ == 2002/2201,即 Atlas A2/A3 系列),另一套是 trunc_3510_impl.h(适配其他支持架构,如 Ascend 950 系列)。

half 数据类型的双阶段转换流程

对 half 输入,TruncCompute 的流程为:

  1. 将 sharedTmpBuffer 重解释为 LocalTensor<float>,先把源 half 数据 Cast<float, half> 提升为 float(注释明确说明目的是 "Cast src from half to float type for getting more precise results",即保证更高计算精度);
  2. 通过 TruncCastForTrunc 完成向零截断,得到 float 形式的整数部分;
  3. 将结果 Cast<half, float> 转回 half 写入 dstTensor;
  4. 每步 Cast 之间以 PipeBarrier<PIPE_V> 做 Vector 流水线屏障,保证依赖顺序。

截断指令的架构差异

TruncCastForTrunc 的实现随架构而异:

  • trunc_v200_impl.h(对应 2002):硬件不支持 float->float 的 CAST_TRUNC,因此先 Cast<int32_t, float> 使用 RoundMode::CAST_TRUNC 将浮点数按截断模式转成 int32(丢弃小数部分),再 Cast<float, int32_t> 转回 float。这也解释了为何需要临时空间存放 int32 中间量。
  • trunc_v220_impl.h(对应 2201):硬件直接支持 Cast<float, float> 加 RoundMode::CAST_TRUNC,一步完成截断。
  • trunc_3510_impl.h:直接调用基础接口 Truncate<T, RoundMode::CAST_TRUNC>,并通过 static_assert(SupportType<T, half, float>) 在编译期约束数据类型。

分块循环计算(Split Loop)

带 sharedTmpBuffer 的实现(trunc_common_impl.h)按临时空间大小将大张量切分为多轮计算:

  • 先以 tmpBufferSize / sizeof(T) 得到可容纳的元素数,half 类型还需再除以 2(因为中间 float 转换占用额外空间)并按 ONE_BLK_SIZE(单次 block 元素数)向下对齐;
  • 用 CheckTmpBufferSize 校验临时空间是否过小;
  • 通过 SetVectorMask 设置逐轮掩码,loopCount = calCount / splitCount 轮循环调用 TruncCompute,尾部 calcTail 单独设置掩码处理。

可见,临时空间越大,单轮处理的元素越多,循环次数越少,这也是文档中"临时空间增大可带来一定程度性能提升"的源码依据。

调用示例与运行结果

Kernel 侧最小调用方式如下:

// dstLocal: 存放计算结果的Tensor
// srcLocal: 参与计算的输入Tensor
AscendC::Trunc<srcType, false>(dstLocal, srcLocal);

结果示例:

输入数据(srcLocal): [9.33 -8.07 -6.38 8.45 5.83 6.46 4.18 1.93]
输出数据(dstLocal): [9.   -8.   -6.   8.   5.   6.   4.   1.  ]

完整的可编译算子样例可参考 ceil 算子样例(该样例目录的 README 明确说明 Ceil/Floor/Round/Sign/Trunc 系列接口实现方式一致,替换接口名即可复用工程)。样例支持在 NPU 运行、CPU 调试(-DCMAKE_ASC_RUN_MODE=cpu)与 NPU 仿真(-DCMAKE_ASC_RUN_MODE=sim)三种模式下编译执行。

仓库中的单元测试 test_operator_trunc.cpp 则覆盖了 4 种调用形态 × 2 种数据类型(float/half)× 256 元素规模的组合,即 NORMAL_MODE(无 calCount、框架申请空间)、CAL_MODE(带 calCount)、BUF_MODE(传入 sharedTmpBuffer)、BUF_CAL_MODE(两者同时指定),可作为开发者自测的参照模板。

Host 侧辅助接口一:GetTruncMaxMinTmpSize

Trunc 的 Tiling 开发需要 Host 侧在 Tiling 阶段确定传给 Kernel 的临时空间大小。GetTruncMaxMinTmpSize 用于获取预留/申请临时空间的最大与最小取值,开发者在该范围内结合 UB(Unified Buffer)剩余空间选取合适值作为 Tiling 参数传递到核函数侧。

函数原型

void GetTruncMaxMinTmpSize(const AscendC::TensorShape& srcShape, const uint32_t typeSize,
                           const bool isReuseSource, uint32_t& maxValue, uint32_t& minValue)

参数说明

参数名 输入/输出 功能
srcShape 输入 输入的 shape 信息,参数类型为 AscendC::TensorShape
typeSize 输入 输入的数据类型大小,单位为字节。例如输入为 half,此处传 2
isReuseSource 输入 是否复用源操作数输入的空间,与 Trunc 接口的模板参数保持一致
maxValue 输出 Trunc 接口完成计算所需的最大临时空间大小,超出该值的空间不会被接口使用。在最小~最大范围内,临时空间越大 Kernel 侧计算性能越优。最大空间为 0 表示计算不需要临时空间。注意 maxValue 仅为参考值,可能大于 UB 剩余空间,此时需根据 UB 剩余空间选取实际值
minValue 输出 Trunc 接口完成计算所需的最小临时空间大小。为保证功能正确,预留/申请的临时空间不能小于该值。最小空间为 0 表示计算不需要临时空间

源码级取值规则

从 trunc_tiling_impl.cpp 的实现可以确认其具体计算逻辑:

  • 最小空间:minValue = 256B × calcPro,其中 calcPro 对 float 为 1、对 half 为 2(TRUNC_HALF_CALC_FAC),即 half 最小 512B、float 最小 256B;
  • 最大空间:maxValue = calcPro × max(inputSize × typeSize, 256B),其中 inputSize 取自 srcShape.GetShapeSize(),即临时空间与输入数据量成正比,但至少为一次重复计算所需的 256B;
  • isReuseSource 参数当前实现中不参与计算((void)isReuseSource),仅作接口预留。

调用示例

// 输入shape信息为1024; 算子输入的数据类型为half; 不允许修改源操作数
std::vector<int64_t> shape_vec = {1024};
AscendC::TensorShape shape(shape_vec);
uint32_t maxValue = 0;
uint32_t minValue = 0;
AscendC::GetTruncMaxMinTmpSize(shape, 2, false, maxValue, minValue);

Host 侧辅助接口二:GetTruncTmpBufferFactorSize

该接口用于获取 maxLiveNodeCount 和 extraBuf 两个因子。maxLiveNodeCount 表示临时空间是"单次计算数据量所占空间"的倍数;extraBuf 表示使用的额外临时空间大小(单位为字节)。在固定可用空间大小的前提下,通过这两个因子可以反推算子单次最大计算元素数量,用于 Tiling 切分。

函数原型

void GetTruncTmpBufferFactorSize(const uint32_t typeSize, uint32_t& maxLiveNodeCount, uint32_t& extraBuf)

参数说明

参数名 输入/输出 功能
typeSize 输入 输入的数据类型大小,单位为字节。例如输入为 half,此处传 2
maxLiveNodeCount 输出 最大存活节点数,表示临时空间是单次计算数据量所占空间的多少倍
extraBuf 输出 使用的额外临时空间大小,单位为字节

源码级取值规则

trunc_tiling_impl.cpp 中实现为:extraBuf = 0,maxLiveNodeCount 对 float 为 1、对 half 为 2——这正对应 half 需要中间 float 空间(数据量放大 2 倍)的内部实现事实。

单次最大计算元素数量推算公式

场景一:仅调用 Trunc 一个高阶接口

为算子预留 currBuff 大小的空间后,用本接口取得 maxLiveNodeCount、extraBuf,单次最大计算元素数量为:

currentShapeSize = (currBuff - extraBuf) / maxLiveNodeCount / typeSize

场景二:同时调用两个高阶接口(KernelIntf1、KernelIntf2)

分别用两个 GetXxxTmpBufferFactorSize 接口得到两组因子,分别计算后再取较小值:

currentShapeSize1 = (currBuff - extraBuf1) / maxLiveNodeCount1 / typeSize
currentShapeSize2 = (currBuff - extraBuf2) / maxLiveNodeCount2 / typeSize
currentShapeSize = min(currentShapeSize1, currentShapeSize2)

注意事项:

  • 上文 currBuff 表示接口计算可用的空间,需去除输入、输出等已占用空间;
  • 接口获取的 maxLiveNodeCount 可能为 0,计算前必须判断其非 0,避免除零错误;
  • 当反推出的 currentShapeSize × typeSize < 256B 时,currentShapeSize 需按 256B / typeSize 的值向上取整(即单次计算至少覆盖一个 256B 的数据块)。

调用示例

uint32_t maxLiveNodeCount = 0;
uint32_t extraBuf = 0;
AscendC::GetTruncTmpBufferFactorSize(typeSize, maxLiveNodeCount, extraBuf);

完整 Tiling 开发流程建议

综合上述接口,Trunc 算子的典型开发链路为:

  1. Host 侧 Tiling:调用 GetTruncMaxMinTmpSize 获取临时空间上下限,结合 UB 剩余空间确定 BufferSize 并作为 Tiling 参数传入 Kernel;若需精确切分单次计算量,再调用 GetTruncTmpBufferFactorSize 按上述公式推算 currentShapeSize;
  2. Kernel 侧计算:按文档 Trunc.md 的 4 种原型选择调用形态——内存紧张、追求复用选择 sharedTmpBuffer 方式,简化开发选择框架申请方式;
  3. 验证:参照 test_operator_trunc.cpp 覆盖 4 种调用形态与两种数据类型,或直接改写 ceil 算子样例 的工程进行端到端验证。

更多高阶 API 的通用 Tiling 示例可参考 更多样例。

相关文档与资源

登录后查看全文
asc-devkit