Ascend C 高阶数学接口 Ceil 用法与临时空间管理深度解析
Ascend C 高阶数学接口 Ceil 用法与临时空间管理深度解析
本文聚焦 CANN asc-devkit 仓库中 SIMD 高阶 API 数学库的 Ceil 接口族,完整讲解 Ceil 向上取整算子的功能语义、两种临时空间传入方式、Host 侧 Tiling 配套接口(GetCeilMaxMinTmpSize 与 GetCeilTmpBufferFactorSize)的推导原理,并结合仓库源码(ceil.h、ceil_common_impl.h、ceil_tiling_impl.cpp)与测试用例给出可直接复用的实战方案。读完本文,你将能够独立完成 Ceil 算子的 Kernel 侧实现、Host 侧临时空间规划与单次最大计算规模的推算。
一、接口族总览与产品支持情况
Ceil 接口族位于高阶 API 数学计算目录 adv_api/math_compute/Ceil_interface,由三个接口组成:
| 接口 | 所属侧 | 职责 |
|---|---|---|
| Ceil | 核函数(Kernel)侧 | 执行向上取整计算 |
| GetCeilMaxMinTmpSize | Host 侧 Tiling | 获取预留/申请临时空间的最大、最小范围 |
| GetCeilTmpBufferFactorSize | Host 侧 Tiling | 获取 maxLiveNodeCount 与 extraBuf,用于推算单次最大计算元素数量 |
从产品支持情况看,Ceil 属于分产品差异化支持的接口:
- Ascend 950PR & 950DT 系列产品:支持
- Atlas A3 系列产品:支持
- Atlas A2 系列产品:支持
- Atlas 200I/500 A2 推理产品:不支持
- Atlas 推理系列产品 AI Core:支持;Vector Core:不支持
- Atlas 训练系列产品:不支持
从源码结构看,这一差异化正是通过编译期架构宏实现的:公共头文件 ceil.h 根据 __NPU_ARCH__ 宏选择实现文件——架构 2002(对应 A2)与 2201 引入 ceil_common_impl.h,而 3510、9201、5102、5162、3003、3113 等架构引入 ceil_3510_impl.h,其余架构下接口体为空。此外,ceil.h 明确声明:Ceil 不能在 --enable-simt 编译标志下使用(编译期直接报错),这与文档中"Vector Core 不支持"的产品约束一致。
使用前提:本文所有接口均属于高阶 API(adv_api)。按 通用说明和约束 的说明,核函数(Kernel)侧包含
kernel_operator.h即可调用;若在 Host 侧调用配套 Tiling 接口,需要链接libtiling_api.a。
二、Ceil 功能语义:向正无穷取整
Ceil 获取大于或等于 x 的最小的整数值,即向正无穷方向取整。其数学表达为:
Ceil(x) = min{ n ∈ Z | n ≥ x }
典型示例:
Ceil(3.9) = 4.0Ceil(-3.9) = -3.0(注意负数向正无穷取整,结果比原数大)
该语义在 ceil.h 的接口注释中得到印证,其与 PyTorch 的 torch.ceil 语义一致。
三、函数原型与临时空间两种传入方式
由于内部实现涉及复杂数学计算,需要额外的临时空间存储中间变量。Ceil 提供两个重载:
方式一:通过 sharedTmpBuffer 入参传入临时空间
template <typename T, bool isReuseSource = false>
__aicore__ inline void Ceil(const LocalTensor<T>& dstTensor, const LocalTensor<T>& srcTensor,
const LocalTensor<uint8_t>& sharedTmpBuffer, const uint32_t calCount)
方式二:接口框架申请临时空间
template <typename T, bool isReuseSource = false>
__aicore__ inline void Ceil(const LocalTensor<T>& dstTensor, const LocalTensor<T>& srcTensor,
const uint32_t calCount)
两种方式的取舍要点:
| 方式 | 内存管理责任方 | 优点 | 注意点 |
|---|---|---|---|
| sharedTmpBuffer 入参 | 开发者 | 内存可复用,不反复申请释放,灵活性与内存利用率更高 | 需要为 Tensor 申请空间 |
| 接口框架申请 | 接口框架(栈上临时空间) | 开发者无需申请 | 仍需预留空间,需知晓 BufferSize 大小 |
从实现看,第二种方式在 ceil_common_impl.h 中通过 PopStackBuffer<uint8_t, TPosition::LCM> 从栈上弹出临时空间,再转调第一种实现;若栈上申请失败,会通过 ASCENDC_ASSERT 报出 "PopStackBuffer Error!"。
无论哪种方式,临时空间大小 BufferSize 均需通过 Host 侧 GetCeilMaxMinTmpSize 获取预留范围。
四、参数说明
表 1 模板参数
| 参数名 | 描述 |
|---|---|
| T | 操作数的数据类型,支持 half、float |
| isReuseSource | 是否允许修改源操作数。该参数当前预留,传入默认值 false 即可 |
表 2 接口参数
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| dstTensor | 输出 | 目的操作数。类型为 LocalTensor,支持的 TPosition 为 VECIN/VECCALC/VECOUT |
| srcTensor | 输入 | 源操作数。类型为 LocalTensor,支持的 TPosition 为 VECIN/VECCALC/VECOUT;数据类型需与目的操作数保持一致 |
| sharedTmpBuffer | 输入 | 临时缓存。类型为 LocalTensor,支持的 TPosition 为 VECIN/VECCALC/VECOUT,用于 Ceil 内部复杂计算时存储中间变量 |
| calCount | 输入 | 参与计算的元素个数 |
返回值:无。
关于 TPosition 与物理内存的映射:按 通用说明和约束 的说明,VECIN、VECCALC、VECOUT 均映射到 Unified Buffer(UB)。也就是说,dstTensor、srcTensor、sharedTmpBuffer 三者都存放在 UB 上,这正是地址重叠约束(见第六节)存在的原因。
从源码可进一步确认参数校验逻辑:在 ceil_3510_impl.h 中,实现通过 static_assert(SupportType<T, float, half>()) 在编译期限定数据类型,并通过 CheckTensorPosition 校验 dstTensor/srcTensor 的 TPosition 必须为 VECIN/VECOUT/VECCALC,通过 CheckCalCount 校验 calCount 不超过输入输出 Tensor 的实际容量。
五、底层实现原理(源码级)
5.1 架构 2002/2201(A2/A3 等):多阶段转换实现
对 half 类型,由于硬件没有直接的取整指令,需要多次转换。核心处理函数位于 ceil_common_impl.h:
constexpr uint32_t CEIL_HALF_CALC_PROCEDURE = 2;
__aicore__ inline void CeilProcess(
const LocalTensor<half>& dstTensor, const LocalTensor<half>& srcTensor,
const LocalTensor<uint8_t>& tmpTensor)
{
const LocalTensor<float> floatTmpTensor = tmpTensor.ReinterpretCast<float>();
// half 无直接取整指令,需多次转换:
// 1) half -> float
Cast<float, half, false>(floatTmpTensor, srcTensor, RoundMode::CAST_NONE, ...);
PipeBarrier<PIPE_V>();
// 2) float 上执行 Ceil 取整
CeilProcess(floatTmpTensor, floatTmpTensor, tmpTensor);
// 3) float -> half 回写
Cast<half, float, false>(dstTensor, floatTmpTensor, RoundMode::CAST_NONE, ...);
PipeBarrier<PIPE_V>();
}
由此可以解释文档中的关键设计:
- half 的临时空间需求是 float 的 2 倍(源码中
CEIL_HALF_CALC_PROCEDURE = 2):float 中间变量 + half 转换缓冲区; - 临时空间在 ceil_common_impl.h 中被用于分块计算:先按临时空间容量计算
splitCount(并按ONE_BLK_SIZE对齐),再以loopCount = calCount / splitCount循环整块计算、calcTail处理尾部,配合SetVectorMask设置向量掩码。这说明临时空间大小直接影响循环分块粒度,进而影响性能。
5.2 架构 3510 系列:寄存器截断指令直接取整
在 ceil_3510_impl.h 中,取整通过寄存器指令直接完成:
__simd_vf__ inline void CeilCompute(__ubuf__ T* dstUb, __ubuf__ T* srcUb,
uint32_t calCount, const uint16_t repeatTimes)
{
constexpr uint32_t repeatElm = GetVecLen() / sizeof(T);
for (uint16_t i = 0; i < repeatTimes; ++i) {
maskReg = Reg::UpdateMask<T>(calCount);
Reg::LoadAlign<T>(srcReg, srcUb + i * repeatElm);
Reg::Truncate<T, RoundMode::CAST_CEIL>(dstReg, srcReg, maskReg); // CAST_CEIL 即向上取整
Reg::StoreAlign<T>(dstUb + i * repeatElm, dstReg, maskReg);
}
}
可以推断:在支持该指令的架构上,Ceil 通过 RoundMode::CAST_CEIL 的 Truncate 指令完成取整,因此该架构下无需临时空间参与计算(sharedTmpBuffer 仅做位置校验后即被忽略,见 ceil_3510_impl.h)。这也解释了为什么文档要求通过 GetCeilMaxMinTmpSize 动态获取临时空间范围——不同架构对临时空间的需求完全不同,硬编码大小无法跨架构移植。
六、约束说明
Ceil 接口的使用约束如下:
- Atlas 推理系列 AI Core 输入范围限制:输入数据限制在
[-2147483647.0, 2147483647.0]范围内; - 不支持源操作数与目的操作数地址重叠(dstTensor 与 srcTensor 不得指向同一块 UB 空间);
- 不支持 sharedTmpBuffer 与源操作数、目的操作数地址重叠;
- 地址对齐要求:操作数地址对齐遵循 通用地址对齐约束,即 UB 上的操作数需满足 32Byte 对齐;若接口自身有更明确的对齐说明,以接口说明为准。
注意:地址重叠约束与 通用地址重叠约束 中"基础 API 允许源目的完全重叠"的规则不同——高阶 API 有特殊约束时,以本接口说明为准。
七、调用示例与运行结果
Kernel 侧最小调用示例(临时空间入参方式):
// dstLocal: 存放计算结果的Tensor
// srcLocal: 参与计算的输入Tensor
// sharedTmpBuffer: 临时缓存,内部复杂计算时存储中间变量
// 输入tensor长度为1024,算子输入的数据类型为half,实际计算个数为512
AscendC::Ceil(dstLocal, srcLocal, sharedTmpBuffer, 512);
结果示例如下:
输入数据(srcLocal): [-1.1 0.0 0.9 1.9 2.9 3.9 4.9 5.9]
输出数据(dstLocal): [-1.0 0.0 1.0 2.0 3.0 4.0 5.0 6.0]
仓库测试用例 test_operator_ceil.cpp 覆盖了两种调用模式(COUNT 对应接口框架申请临时空间、COUNT_TMPBUF 对应 sharedTmpBuffer 入参)以及 float/half 两种数据类型,其中显式传入临时空间的用例使用 max(ONE_REPEAT_BYTE_SIZE * sizeof(float), 2 * dataSize * sizeof(float)) 计算临时空间大小,与 Host 侧 GetCeilMaxMinTmpSize 的返回规律一致。
八、Host 侧配套:GetCeilMaxMinTmpSize 获取临时空间范围
8.1 功能说明与函数原型
核函数(Kernel)侧 Ceil 计算需要预留/申请临时空间,本接口在 Host 侧获取最大和最小临时空间大小,开发者基于此范围选择合适的空间大小作为 Tiling 参数传递到 Kernel 侧:
void GetCeilMaxMinTmpSize(const AscendC::TensorShape& srcShape, const uint32_t typeSize,
const bool isReuseSource, uint32_t& maxValue, uint32_t& minValue)
使用要点:
- 预留/申请的临时空间大小不能小于最小临时空间大小,否则功能可能不正确;
- 在最小~最大范围内,临时空间越大,Kernel 侧计算性能会有一定程度的优化提升(对应 5.1 节分块循环:更大空间意味着更少的循环分块次数);
maxValue仅为参考值,可能大于 UB 剩余空间,此时开发者需要根据 UB 剩余空间大小选取合适的临时空间大小;- 最大/最小空间大小为 0 表示计算不需要临时空间(如 3510 系列架构)。
8.2 参数说明
| 参数名 | 输入/输出 | 功能 |
|---|---|---|
| srcShape | 输入 | 输入的 shape 信息,参数类型为 AscendC::TensorShape |
| typeSize | 输入 | 输入数据类型大小,单位字节。如 half 传入 2 |
| isReuseSource | 输入 | 是否复用源操作数输入的空间,与 Ceil 接口保持一致 |
| maxValue | 输出 | 完成计算所需最大临时空间大小;超出该值的空间不会被使用 |
| minValue | 输出 | 完成计算所需最小临时空间大小;预留空间不能小于该值 |
8.3 源码级推算公式
Host 侧实现位于 ceil_tiling_impl.cpp:
constexpr uint32_t CEIL_HALF_CALC_FAC = 2; // half 计算过程系数
constexpr uint32_t CEIL_FLOAT_CALC_FAC = 1; // float 计算过程系数
constexpr uint32_t CEIL_ONE_REPEAT_BYTE_SIZE = 256; // 单次 repeat 的字节大小
// maxValue = 计算过程系数 * max(输入总字节数, 256)
// minValue = 256 * 计算过程系数
即:
- minValue = 256 × calcFac(calcFac:float 为 1,half 为 2)
- maxValue = calcFac × max(inputSize × typeSize, 256)
该公式与测试用例 test_tiling.cpp 的断言完全一致:
- float、shape {128,128}:
minValue == 256,maxValue == 128*128*4 - half、shape {128,128}:
minValue == 512,maxValue == 128*128*2*2 - half、shape {512}:
maxValue == 512*2*2,minValue == 512
8.4 调用示例
// 输入shape信息为1024;算子输入的数据类型为half;不允许修改源操作数
std::vector<int64_t> shape_vec = {1024};
AscendC::TensorShape shape(shape_vec);
uint32_t maxValue = 0;
uint32_t minValue = 0;
AscendC::GetCeilMaxMinTmpSize(shape, 2, false, maxValue, minValue);
九、Host 侧配套:GetCeilTmpBufferFactorSize 推算单次最大计算量
9.1 功能说明与函数原型
在固定空间大小的前提下,通过 maxLiveNodeCount 与 extraBuf 可以反推算子单次最大计算元素数量:
void GetCeilTmpBufferFactorSize(const uint32_t typeSize,
uint32_t& maxLiveNodeCount, uint32_t& extraBuf)
| 参数名 | 输入/输出 | 功能 |
|---|---|---|
| typeSize | 输入 | 输入数据类型大小,单位字节。如 half 传入 2 |
| maxLiveNodeCount | 输出 | 最大存活节点数,表示临时空间是单次计算数据量所占空间的多少倍 |
| extraBuf | 输出 | 使用的额外临时空间大小,单位字节 |
9.2 单算子场景的推算公式
为 Ceil 预留 currBuff 大小的空间,利用本接口得到 maxLiveNodeCount、extraBuf,可推导单次最大计算元素数量:
currentShapeSize = (currBuff - extraBuf) / maxLiveNodeCount / typeSize
9.3 多算子场景的推算公式
算子实现需要调用两个 Kernel 侧 API(KernelIntf1、KernelIntf2)时,分别用两个 GetXxxTmpBufferFactorSize 接口(Xxx 为各自的高阶 API)得到两组输出值,再取最小值:
currentShapeSize1 = (currBuff - extraBuf1) / maxLiveNodeCount1 / typeSize
currentShapeSize2 = (currBuff - extraBuf2) / maxLiveNodeCount2 / typeSize
currentShapeSize = min(currentShapeSize1, currentShapeSize2)
使用注意:
currBuff表示接口计算可用的空间,需要去除用户输入输出等空间;maxLiveNodeCount可能为 0,计算时需判断该值非 0,避免除零错误;- 约束:当反推出的
currentShapeSize * typeSize < 256B时,currentShapeSize按256B / typeSize的值向上取整。
9.4 源码级推算公式
实现同样位于 ceil_tiling_impl.cpp:
void GetCeilTmpBufferFactorSize(const uint32_t typeSize, uint32_t& maxLiveNodeCount, uint32_t& extraBuf)
{
extraBuf = 0;
maxLiveNodeCount = (typeSize == sizeof(float)) ? CEIL_FLOAT_CALC_FAC : CEIL_HALF_CALC_FAC;
}
即 Ceil 的 extraBuf 恒为 0,maxLiveNodeCount 在 float 时为 1、half 时为 2。这与测试用例 test_tiling.cpp 的断言(float:maxLiveNodeCount == 1、extraBuf == 0;half:maxLiveNodeCount == 2、extraBuf == 0)完全一致。
9.5 调用示例
uint32_t maxLiveNodeCount = 0;
uint32_t extraBuf = 0;
AscendC::GetCeilTmpBufferFactorSize(typeSize, maxLiveNodeCount, extraBuf);
十、完整配套流程:从 Host Tiling 到 Kernel 计算的实战闭环
将上述接口串成完整的 Ceil 算子开发闭环(完整样例参考 更多样例):
第 1 步(Host 侧):在 Tiling 函数中根据输入 shape 调用 GetCeilMaxMinTmpSize 获取 [minValue, maxValue] 范围;结合 platform_ascendc::PlatformAscendC 的 GetCoreMemSize(CoreMemType::UB, tailSize) 获取 UB 剩余空间,取 tmpSize = tailSize >= maxValue ? maxValue : tailSize,写入 TilingData:
auto shape_input = context->GetInputTensor(0)->GetOriginShape();
std::vector<int64_t> srcDims = {shape_input.GetDim(0), shape_input.GetDim(1)};
AscendC::TensorShape shape(srcDims);
uint32_t minValue = 0, maxValue = 0;
AscendC::GetCeilMaxMinTmpSize(shape, typeSize, false, maxValue, minValue);
auto tmpSize = tailSize >= maxValue ? maxValue : tailSize; // 根据UB剩余空间裁剪
第 2 步(Kernel 侧):读取 TilingData 中的 tmpBufferSize,通过 pipe.InitBuffer(tmpBuf, tmpBufferSize) 分配临时空间,取出 LocalTensor<uint8_t> sharedTmpBuffer = tmpBuf.Get<uint8_t>() 后调用 Ceil:
AscendC::LocalTensor<uint8_t> sharedTmpBuffer = tmpBuf.Get<uint8_t>();
AscendC::Ceil<srcType, false>(dstLocal, srcLocal, sharedTmpBuffer, calCount);
若在固定空间下需要最大化单次计算规模(例如多个高阶数学 API 串联),则改用第 9 节的 GetCeilTmpBufferFactorSize 推算 srcCurSize,并在 Kernel 侧按 for (offset = 0; offset < srcSize; offset += srcCurSize) 分片循环调用(详见 更多样例 样例三)。
十一、常见问题与排查建议
- 预留空间小于 minValue:计算结果可能错误或行为未定义。请通过 GetCeilMaxMinTmpSize 重新获取 minValue,并确保 InitBuffer 大小不小于该值。
- 地址重叠:Ceil 不支持 dstTensor 与 srcTensor 重叠,也不支持 sharedTmpBuffer 与二者重叠。若出现异常结果,先检查三个 Tensor 是否指向同一段 UB 空间。
- 除零错误:使用 GetCeilTmpBufferFactorSize 反推 currentShapeSize 时,务必先判断 maxLiveNodeCount 是否为 0。
- 架构不支持:Atlas 训练系列、Atlas 200I/500 A2 推理产品不支持 Ceil;Atlas 推理系列 Vector Core 不支持(且
--enable-simt编译标志下头文件会直接报错)。编译前请确认目标产品支持情况。 - 对齐问题:UB 上的操作数需满足 32Byte 对齐,地址对齐要求参见 通用地址对齐约束。