Ascend C 高阶数学接口 Trunc 详解:向零取整算子开发与临时空间管理
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) = 3Trunc(-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 的流程为:
- 将
sharedTmpBuffer重解释为LocalTensor<float>,先把源half数据Cast<float, half>提升为float(注释明确说明目的是 "Cast src from half to float type for getting more precise results",即保证更高计算精度); - 通过
TruncCastForTrunc完成向零截断,得到float形式的整数部分; - 将结果
Cast<half, float>转回half写入dstTensor; - 每步 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 算子的典型开发链路为:
- Host 侧 Tiling:调用 GetTruncMaxMinTmpSize 获取临时空间上下限,结合 UB 剩余空间确定 BufferSize 并作为 Tiling 参数传入 Kernel;若需精确切分单次计算量,再调用 GetTruncTmpBufferFactorSize 按上述公式推算
currentShapeSize; - Kernel 侧计算:按文档 Trunc.md 的 4 种原型选择调用形态——内存紧张、追求复用选择 sharedTmpBuffer 方式,简化开发选择框架申请方式;
- 验证:参照 test_operator_trunc.cpp 覆盖 4 种调用形态与两种数据类型,或直接改写 ceil 算子样例 的工程进行端到端验证。
更多高阶 API 的通用 Tiling 示例可参考 更多样例。
相关文档与资源
- 接口文档入口:Trunc_interface
- 核函数侧接口实现:trunc.h、trunc_common_impl.h
- Host 侧 Tiling 接口声明与实现:trunc_tiling.h、trunc_tiling_impl.cpp
- 单元测试:test_operator_trunc.cpp
- 可复用算子样例:ceil 样例目录
- 数据类型定义:TensorShape、LocalTensor