ATVOSS DefaultKernelSchedule:kernel 层默认调度策略的完整实现与使用指南
【免费下载链接】atvossATVOSS(Ascend C Templates for Vector Operator Subroutines)是一套基于Ascend C开发的Vector算子库,致力于为昇腾硬件上的Vector类融合算子提供极简、高效、高性能、高拓展的编程方式。项目地址: https://gitcode.com/cann/atvoss
导读
DefaultKernelSchedule是 ATVOSS(Ascend C Templates for Vector Operator Subroutines)中 kernel 层默认的 schedule 调度策略。它以完全继承BaseKernelSchedule能力的方式,依据DefaultKernelConfig中的切分参数,在昇腾 AI Core 上完成「多核任务划分 → 各核 GM 数据定位 → 下发 block 层执行」的完整调度逻辑。阅读本文后,你将掌握DefaultKernelSchedule的模板参数含义、tiling 配置字段的计算规则、kernel 层Run的执行链路,并能直接在KernelBuilder中正确装配该调度策略编写可运行的 Vector 融合算子。
一、功能定位:kernel 层调度策略在 ATVOSS 分层架构中的角色
ATVOSS 将算子实现划分为 device 层、kernel 层与 block 层。DefaultKernelSchedule属于 kernel 层的调度类(Schedule),其职责与分层协作关系如下:
- kernel 层:负责根据总元素数量计算 tiling 信息(核数、每核基本块数、尾块元素数等),再依据当前核的
blockIdx确定本核需要处理的 GM 数据区间,把数据交给 block 层计算; - block 层:kernel 层调度的"被包含对象"(
BlockOp),在单核内完成 UB 缓冲管理、基本块循环与表达式求值; - device 层:通过 DeviceAdapter 完成 tiling 计算与 kernel 启动(
LaunchKernelWithDataTuple)。
DefaultKernelSchedule是BaseKernelSchedule的默认实现,二者在 include/elewise/kernel/schedule.h 中定义。从源码结构看,DefaultKernelSchedule是一个空继承类,未覆写任何成员,其全部调度能力(tiling 计算、参数准备、GM 偏移计算)均来自基类BaseKernelSchedule,这保证了默认策略与用户自定义策略拥有完全一致的接口契约。
二、类模板原型与参数说明
DefaultKernelSchedule的完整声明如下(位于 include/elewise/kernel/schedule.h#L214-L215):
template <typename BlockOp, const auto& Policy, typename ScheduleCfg> class DefaultKernelSchedule : public BaseKernelSchedule<BlockOp, Policy, ScheduleCfg> {};| 参数名称 | 参数类型 | 输入/输出 | 数据类型 | 参数说明 | 默认值 |
|---|---|---|---|---|---|
| BlockOp | 模板参数 | 输入 | NA | block 层对象类型,与 kernel 层是被包含关系 | NA |
| Policy | 模板参数 | 输入 | NA | kernel 层的用户静态策略类型 | NA |
| ScheduleCfg | 模板参数 | 输入 | NA | kernel 层调度配置类型 | NA |
三个模板参数的取值通常为:
BlockOp:由Atvoss::Ele::BlockBuilder<Compute, ArchTag>生成的 block 层对象类型;Policy:Atvoss::Ele::DefaultKernelPolicy类型(含segmentPolicy成员,目前仅支持UniformSegment均匀切分),定义于 include/elewise/kernel/builder.h#L24-L33;ScheduleCfg:Atvoss::Ele::DefaultKernelConfig类型,即 kernel 层 tiling 配置结构体。
返回值说明
| 返回值数据类型 | 返回值说明 |
|---|---|
| DefaultKernelSchedule | 返回默认的 kernel 层 schedule 调度策略对象 |
约束说明
原文档标注为 NA。结合源码实现可补充一条隐式约束:ScheduleCfg必须提供blockNum / unitNumPerCore / moreUnitCoreNum / tailNum / unitNum这些成员字段(即满足DefaultKernelConfig的字段布局),否则基类中的 tiling 计算与核内元素计数逻辑将无法编译通过。
三、调度配置的数据基础:DefaultKernelConfig 字段解析
DefaultKernelSchedule的调度逻辑围绕 DefaultKernelConfig 展开,该结构体定义于 include/elewise/kernel/builder.h#L16-L22:
struct DefaultKernelConfig { // Kernel layer tiling information uint32_t blockNum = 1; // Number of cores started uint64_t unitNumPerCore = 0; // Average number of unit processed per core uint64_t moreUnitCoreNum = 0; // Number of cores that need to process an additional full unit uint64_t tailNum = 0; // Number of tail elements to be processed by the last core uint64_t unitNum = 1; // Number of elements per unit block };各字段含义与默认值如下:
| 成员名称 | 成员类型 | 成员说明 | 默认值 |
|---|---|---|---|
| blockNum | uint32_t | 启用的核的数量 | 1 |
| unitNumPerCore | uint64_t | 平均每个核处理的基本块个数 | 0 |
| moreUnitCoreNum | uint64_t | 核均分后,需要处理额外多出来的基本块的核的数量 | 0 |
| tailNum | uint64_t | 最后一个核要处理的尾块元素数量 | 0 |
| unitNum | uint64_t | 基本块的元素数量 | 1 |
其中unitNum在调度执行时会被覆写为ACTUAL_N_ASSIGN(见下文),它代表单个基本块实际分配的元素数,是 tiling 计算的最小粒度。
四、调度原理深度解析:从源码看 tiling 计算与核内执行
4.1 编译期常量推导
BaseKernelSchedule在编译期根据 block 层的TileShape推导出一组调度常量(include/elewise/kernel/schedule.h#L35-L49):
static constexpr uint64_t TILE_SHAPE_SIZE = TileShape::size::value; static constexpr uint64_t BASIC_BLOCK = BlockOp::ScheduleClz::BASIC_BLOCK; static constexpr uint64_t ALIGN_TILE_SHAPE_SIZE = 2; static constexpr uint64_t ACTUAL_N_ASSIGN = TILE_SHAPE_SIZE == 1 ? 32 : TileShape::template get_type<TILE_SHAPE_SIZE - 1>::value; static constexpr uint64_t BASIC_CORE_ELE_NUM = (BASIC_BLOCK + ACTUAL_N_ASSIGN - 1) / ACTUAL_N_ASSIGN * ACTUAL_N_ASSIGN;BASIC_BLOCK:block 层单个 Tile 的元素个数,由 block 层 policy 推导;ACTUAL_N_ASSIGN:基本块实际分配元素数——一维 Tile 形状下取 32,多维取最后一维的元素数;BASIC_CORE_ELE_NUM:单个核最少能处理的对齐后元素数,即BASIC_BLOCK向上对齐到ACTUAL_N_ASSIGN的整数倍。
4.2 MakeScheduleConfig:host 侧 tiling 计算
MakeScheduleConfig是 kernel 层 schedule 的 host 侧静态接口,输入用户参数列表,输出ScheduleCfg(即DefaultKernelConfig)配置(include/elewise/kernel/schedule.h#L57-L93)。计算流程:
- 提取形状并校验:从 arguments 的第一个输入中读取
shape_vector(),累乘得到totalEleNum;若形状为空或总元素数为 0,打印[ERROR]: [Atvoss][Kernel] Shape info error并返回false; - 小数据量直接单核处理:若
totalEleNum <= BASIC_CORE_ELE_NUM,则blockNum = 1、tailNum = totalEleNum,其余字段置 0,直接返回true; - 多核均分:
basicCoreUnitNum = BASIC_CORE_ELE_NUM / ACTUAL_N_ASSIGN:单核可处理的对齐基本块数;totalUnitCnt = totalEleNum / ACTUAL_N_ASSIGN:总基本块数;blockNum = ceil(totalUnitCnt / basicCoreUnitNum),且不超过ArchTag::CORE_NUM(由 common/arch.h 提供的架构核数上限);unitNumPerCore = totalUnitCnt / blockNum(每核平均基本块数);moreUnitCoreNum = totalUnitCnt % blockNum(多分到一个基本块的核数);tailNum = totalEleNum % ACTUAL_N_ASSIGN(最后一个核负责的尾元素数)。
该接口在整个调用链中的位置可见于 device_adapter.h#L128-L140:CalcParam依次调用KernelOp::ScheduleClz::MakeScheduleConfig生成 kernel 层配置,再调用BlockOp::ScheduleClz::MakeScheduleConfig生成 block 层配置。
4.3 Run:device 侧核内调度执行
Run是 kernel 层在 AI Core 上执行的入口(include/elewise/kernel/schedule.h#L117-L130),完成三件事:
- 计算本核元素总数:
CalCurCoreEleCnt依据AscendC::GetBlockIdx()判断当前核归属——若blockIdx < moreUnitCoreNum则多分一个基本块;若blockIdx == blockNum - 1则追加tailNum尾元素; - 准备参数:
PrepareParams根据编译期推导出的Params列表逐个构造参数,其中 Tensor 参数通过CalGMOffset计算本核在 GM 中的起始偏移(blockIdx * unitNumPerCore * unitNum + moreUnitCoreNum * unitNum的均匀划分公式),将 GM 地址指针按数据类型大小平移后传入;标量参数则直接透传或解引用; - 下发 block 层:组装
configBlock(含totalElemCnt)与参数元组,构造BlockOp并调用blockOp.Run(...),由 BaseBlockSchedule::Run 继续按BASIC_BLOCK切分wholeLoop与tileCnt循环求值。
值得注意,Run定义在#if !defined(__ATVOSS_HOST_ONLY__)分支内,即仅编译进 device 侧代码;host 侧仅暴露MakeScheduleConfig静态接口用于 tiling 计算,这正是 ATVOSS 将 host/device 代码通过宏隔离的典型模式。
五、在 KernelBuilder 中装配 DefaultKernelSchedule
DefaultKernelSchedule是 KernelBuilder 的第四个模板参数(调度类型)的默认值,定义于 include/elewise/kernel/builder.h#L39-L48:
template < typename BlockOp, const auto& Policy = defaultKernelPolicy, typename ScheduleCfg = DefaultKernelConfig, template <typename, const auto&, typename> class Schedule = DefaultKernelSchedule> class KernelBuilder { ... };因此常规写法Atvoss::Ele::KernelBuilder<BlockOp, kernelPolicy>内部即隐式使用DefaultKernelSchedule+DefaultKernelConfig。当用户需要自定义调度时,可在第四个模板参数处替换为自己的 Schedule 类(同样须继承自BaseKernelSchedule)。
六、完整使用示例
以下示例实现out = in1 + in2 - in3(两个 Tensor 输入减一个标量输入),完整演示如何将DefaultKernelSchedule显式装配进KernelBuilder:
template <typename InputDtype, typename OutputDtype> struct AddSubConfig { struct AddSubCompute { template <template <typename> class Tensor> __host_aicore__ constexpr auto Compute() const { auto in1 = Atvoss::PlaceHolder<1, Tensor<InputDtype>, Atvoss::ParamUsage::IN>(); auto in2 = Atvoss::PlaceHolder<2, Tensor<InputDtype>, Atvoss::ParamUsage::IN>(); auto in3 = Atvoss::PlaceHolder<3, InputDtype, Atvoss::ParamUsage::IN>(); auto out = Atvoss::PlaceHolder<4, Tensor<OutputDtype>, Atvoss::ParamUsage::OUT>(); return (out = in1 + in2 - in3); }; }; static constexpr Atvoss::Ele::DefaultKernelPolicy kernelPolicy{Atvoss::Ele::DefaultSegmentPolicy::UniformSegment}; using ArchTag = Atvoss::Arch::DAV_3510; using BlockOp = Atvoss::Ele::BlockBuilder<AddSubCompute, ArchTag>; using KernelOp = Atvoss::Ele::KernelBuilder< BlockOp, kernelPolicy, Atvoss::Ele::DefaultKernelConfig, // 🔥🔥🔥 使用示例 🔥🔥🔥 Atvoss::Ele::DefaultKernelSchedule // 🔥🔥🔥 使用示例 🔥🔥🔥 >; using DeviceOp = Atvoss::DeviceAdapter<KernelOp>; }; template <typename InputDtype, typename OutputDtype> static void Run() { /* ACL init and stream create */ ... Atvoss::Tensor<InputDtype> in1(deviceIn1, {{3, 4, 0, 0, 0, 0, 0, 0}}, 2); Atvoss::Tensor<InputDtype> in2(deviceIn2, {{3, 4, 0, 0, 0, 0, 0, 0}}, 2); InputDtype in3 = 5.0; Atvoss::Tensor<OutputDtype> out(deviceOut, {{3, 4, 0, 0, 0, 0, 0, 0}}, 2); auto arguments = Atvoss::ArgumentsBuilder{}.inputOutput(in1, in2, in3, out).attr("dim", 5).build(); using DeviceOp = typename AddSubConfig<InputDtype, OutputDtype>::DeviceOp; DeviceOp deviceOp; deviceOp.Run(arguments, stream); } int main(int argc, char const* argv[]) { Run<float, float>(); return 0; }代码要点:
kernelPolicy采用DefaultSegmentPolicy::UniformSegment(均匀切分,当前唯一支持的切分方式);BlockBuilder生成 block 层对象,KernelBuilder以BlockOp + kernelPolicy + DefaultKernelConfig + DefaultKernelSchedule组装 kernel 层;DeviceAdapter负责在 host 侧驱动MakeScheduleConfig生成 tiling,再以opParam.kernelParam.blockNum作为启动核数调用LaunchKernelWithDataTuple拉起 kernel;- 运行期每个核通过
AscendC::GetBlockIdx()在Run内确定自己的数据区间,实现多核并行。
该模式与仓库内 examples/muls/muls.cpp 等官方示例的编写方式一致,可作为实际算子开发的参照。
七、约束、局限与总结
- 切分策略约束:
DefaultKernelPolicy目前仅支持UniformSegment均匀切分;若需按非均匀方式(如按 shape 维度特化切分)划分数据,需要自定义继承自BaseKernelSchedule的调度类; - 核数上限:
blockNum被ArchTag::CORE_NUM截断,超大 shape 下不会超过目标架构可用核数; - 调度粒度:kernel 层以「基本块(unit)」为最小调度单元,block 层再按
BASIC_BLOCK细分循环,两级切分共同决定了数据在核间与核内的分布; - 接口契约:无论默认还是自定义调度,都必须实现 host 侧
MakeScheduleConfig与 device 侧Run两个接口,且ScheduleCfg需与KernelBuilder传入的配置类型一致。
DefaultKernelSchedule以极小的代码量(单行继承声明)复用了BaseKernelSchedule完整的多核 tiling 与数据分发逻辑,是 ATVOSS「极简编程」理念在调度层的集中体现。理解它的 tiling 计算规则与核内执行链路,是进一步自定义 kernel 层调度、编写高性能 Vector 融合算子的基础。
相关文档导航
- BaseKernelSchedule::MakeScheduleConfig:tiling 配置生成接口详解
- BaseKernelSchedule::Run:核内调度执行接口详解
- DefaultKernelConfig:调度配置结构体
- DefaultKernelPolicy:kernel 层静态策略
- KernelBuilder:kernel 层对象构建类
- DefaultBlockSchedule:block 层默认调度策略
- DeviceAdapter:device 层适配与 kernel 启动
【免费下载链接】atvossATVOSS(Ascend C Templates for Vector Operator Subroutines)是一套基于Ascend C开发的Vector算子库,致力于为昇腾硬件上的Vector类融合算子提供极简、高效、高性能、高拓展的编程方式。项目地址: https://gitcode.com/cann/atvoss
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考