Ascend C 高阶数学接口 Ceil 用法与临时空间管理深度解析

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

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.0
  • Ceil(-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 接口的使用约束如下:

  1. Atlas 推理系列 AI Core 输入范围限制:输入数据限制在 [-2147483647.0, 2147483647.0] 范围内;
  2. 不支持源操作数与目的操作数地址重叠(dstTensor 与 srcTensor 不得指向同一块 UB 空间);
  3. 不支持 sharedTmpBuffer 与源操作数、目的操作数地址重叠;
  4. 地址对齐要求:操作数地址对齐遵循 通用地址对齐约束,即 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) 分片循环调用(详见 更多样例 样例三)。

十一、常见问题与排查建议

  1. 预留空间小于 minValue:计算结果可能错误或行为未定义。请通过 GetCeilMaxMinTmpSize 重新获取 minValue,并确保 InitBuffer 大小不小于该值。
  2. 地址重叠:Ceil 不支持 dstTensor 与 srcTensor 重叠,也不支持 sharedTmpBuffer 与二者重叠。若出现异常结果,先检查三个 Tensor 是否指向同一段 UB 空间。
  3. 除零错误:使用 GetCeilTmpBufferFactorSize 反推 currentShapeSize 时,务必先判断 maxLiveNodeCount 是否为 0。
  4. 架构不支持:Atlas 训练系列、Atlas 200I/500 A2 推理产品不支持 Ceil;Atlas 推理系列 Vector Core 不支持(且 --enable-simt 编译标志下头文件会直接报错)。编译前请确认目标产品支持情况。
  5. 对齐问题:UB 上的操作数需满足 32Byte 对齐,地址对齐要求参见 通用地址对齐约束。
登录后查看全文
asc-devkit