☰
PyPTO Pro 算子开发中的 Tiling 切分与 TilingKey/TilingData 参数传递实战
2026/10/14 17:49:58 网站建设 项目流程
  • 人工智能
  • 编译器
  • 模型编译
  • 深度学习
  • 高性能计算
  • CANN
  • Ascend

【免费下载链接】pypto

PyPTO(发音: pai p-t-o):Parallel Tensor/Tile Operation编程范式。

项目地址:https://gitcode.com/cann/pypto
点击查看免费下载

PyPTO(Pro 前端)中,算子 Kernel 需要在 Device 侧被多个 AI Core 并行执行,而每个核的片上 Buffer 容量有限,因此必须在 Host 侧规划好数据切分,并把切分参数传给 Kernel。本文基于仓库中 Tiling 实现指南(Tiling 实现 及其三篇子文档)系统讲解:为什么需要 Tiling、核间/核内切分的两个层次、多核切分的通用写法与 MIX Kernel 的切分方法,以及TilingData与TilingKey两类运行时/编译期参数的声明、传递与校验规则,并结合开源仓库源码印证其底层实现机制。读完后你能够独立完成一个算子的多核切分方案,并正确地把 shape、stride、循环边界等参数从 Host 传入 Kernel。

一、Tiling 概述:两个硬件约束决定了切分的必要性

Tiling(Tiling 实现指南 Tiling概述)指把算子的输入数据划分为基本任务块(Tile),将一组任务块分配给各个核,再为核内处理配置 Buffer 的过程。它是算子开发的第一步:切分方式决定了各核负载是否均衡、片上 Buffer 能否放下、数据搬运是否连续,直接影响算子性能。

Tiling 的必要性来自两个硬件约束:

  • Device 上有多个 AI Core。算子数据若只交给一个核处理,其余核处于空闲,多核算力无法发挥。必须把数据划分成基本任务块,并将一组任务块分配给每个核并行处理,才能把算力用起来。
  • 单个核的片上 Buffer 容量有限。计算只能在片上 Buffer 中进行,而算子的输入输出通常远大于片上容量,无法一次完整装入。基本任务块的大小需要适配片上容量;核内可为已分配的任务划分双缓冲或多个 Buffer 槽位,轮转执行"搬入—计算—搬出",让不同任务的搬运与计算尽量并行。

因此 Tiling 包含两个层次:

层次含义详细文档
核间切分把算子的数据划分为基本任务块,将一组任务块分配给每个 AI Core 并行处理多核Tiling切分
核内切分基于已分配的任务划分核内 Buffer,例如使用双缓冲或多个 Buffer 槽位轮转,让搬运与计算并行Tile创建和操作、Tile计算

AI Core 与执行域

Device 上有多个 AI Core,每个 AI Core 内含 Cube 核(AIC)和 Vector 核(AIV)。在 AIC 与 AIV 为 1:2 的芯片上,一个 AI Core 内有 1 个 Cube 核和 2 个 Vector 核:Cube 核承担矩阵计算,Vector 核承担矢量计算,两者各有自己的片上 Buffer 和指令流水。

Kernel 通过执行域声明代码运行在哪类核上:pypto_pro.language.section_cube()中的代码由 Cube 核执行,pypto_pro.language.section_vector()中的代码由 Vector 核执行。Kernel 包含哪些执行域,决定了它使用哪类核,也决定了核间切分时可用的核数。

片上 Buffer 的层级、容量和数据通路参见 SIMD 抽象硬件架构。

设计 Tiling 时的考虑因素

Tiling 概述文档给出五条设计准则,实际开发中可逐条对照检查:

  • 负载均衡:各个核分到的任务数和计算量应尽量接近,避免个别核成为长尾拖慢整体。
  • 片上容量:单次搬入的 Tile 必须能放进对应的片上 Buffer,并为双缓冲等优化预留空间。
  • 接口约束:Tile 的形状需要满足数据搬运和计算接口在对齐、layout 和分形上的要求。
  • 数据复用:相邻任务如果复用同一块数据,切分顺序应尽量让它们落在同一个核上。
  • 尾块处理:数据量通常不能被 Tile 大小整除,任务数需要向上取整,最后一块用有效形状表达实际范围。

二、多核 Tiling 切分:任务编号与连续分核

基本写法

以矩阵乘为例,输出被划分为m_tiles * n_tiles个 Tile,每个 Tile 的计算相互独立。Tile 数按向上取整计算,保证最后不足一个 Tile 的数据也被覆盖:

m_tiles = (m + tile_m - 1) // tile_m n_tiles = (n + tile_n - 1) // tile_n total_tiles = m_tiles * n_tiles

把这些 Tile 按行优先统一编号成total_tiles个任务,每个核负责其中连续的一段,再由任务号换算回二维下标:

import pypto_pro.language as pl core_id = pl.get_block_idx() // pl.get_subblock_num() core_num = pl.get_block_num() tiles_per_core = (total_tiles + core_num - 1) // core_num start = core_id * tiles_per_core end = pl.min(start + tiles_per_core, total_tiles) for task_idx in pl.range(start, end, 1): tile_m_idx = task_idx // n_tiles tile_n_idx = task_idx % n_tiles # 计算第 (tile_m_idx, tile_n_idx) 个输出Tile ...

这里有几个关键点(来自 多核Tiling切分):

  • pypto_pro.language.get_block_num()(API 文档)返回本次实际生效的核数,计算每个核的任务范围时应使用它的返回值,而不是 Host 侧请求的block_dim。block_dim的默认值、调用形式和核数限制参见 Kernel核函数。
  • core_id由pypto_pro.language.get_block_idx()除以pypto_pro.language.get_subblock_num()得到。纯 Vector 和纯 Cube Kernel 中get_subblock_num()返回 1,该除法不改变结果;MIX Kernel 中 Vector 侧的get_block_idx()是 Vector 核的全局索引,除以每个 AI Core 内的 Vector 核数后,与 Cube 侧得到同一个 AI Core 编号,两侧才能按同一套切分处理同一批数据。
  • 这种分配不要求任务数被核数整除:tiles_per_core向上取整,末尾的核用pypto_pro.language.min()把上界限制在total_tiles,剩余任务自然落在最后一个进入循环的核上,不需要为"除不尽"单独编写分支。任务总数少于核数时,编号超出范围的核得到空区间,不会进入循环。
  • 算子的执行时间取决于任务最多的那个核,为ceil(total_tiles / core_num)个任务的耗时。

上面的编号方式是一种通用选择。算子完全可以按自身的数据复用和依赖关系定义别的切分方式,例如输出 Tile 之间存在跨迭代累加时,被累加的那一维必须完整留在同一个核内。

MIX Kernel 的切分

Device 上有多个 AI Core,每个 AI Core 内含 Cube 核(AIC)和 Vector 核(AIV);在 AIC 与 AIV 为 1:2 的芯片上,一个 AI Core 内有 1 个 Cube 核和 2 个 Vector 核。同时包含 Cube 和 Vector 执行域 的 Kernel 称为MIX Kernel。

MIX Kernel 以 AI Core 为启动单位,核间切分也只在 AI Core 这一层做一次:get_block_num()在两侧都返回 AI Core 数,core_id在两侧都取get_block_idx() // get_subblock_num()(AIC 上get_subblock_num()返回 1,AIV 上返回 2,两侧因此得到同一个 AI Core 编号)。上一节的任务划分直接沿用,Cube 和 Vector 在同一个任务循环里按同一个task_idx推进:

import pypto_pro.language as pl core_id = pl.get_block_idx() // pl.get_subblock_num() core_num = pl.get_block_num() tiles_per_core = (total_tiles + core_num - 1) // core_num start = core_id * tiles_per_core end = pl.min(start + tiles_per_core, total_tiles) for task_idx in pl.range(start, end, 1): with pl.section_cube(): # 计算本任务的整块输出 ... with pl.section_vector(): # 处理Cube刚算出的这块数据 ...

在 1:2 的芯片上,一个 AI Core 内有 2 个 Vector 核,两者要分摊同一个任务里 Cube 产出的这份数据,因此每个 Vector 核处理的数据量是 Cube 的一半。这一步用pypto_pro.language.get_subblock_idx()在某一维上把数据切成两半,属于核内切分,不参与核间任务的分配:

import pypto_pro.language as pl sub_id = pl.get_subblock_idx() first_half = (rows + 1) // 2 half_rows = first_half if sub_id == 1: half_rows = rows - first_half for task_idx in pl.range(start, end, 1): with pl.section_cube(): # 计算本任务的rows行输出 ... with pl.section_vector(): if half_rows > 0: # 起始行 sub_id * first_half # 行数 half_rows ...

切分的维度由算子自身决定,行、列或 batch 方向都可以,前提是两个 Vector 核的工作互不重叠且合起来覆盖 Cube 的全部输出。

负载均衡与切分粒度

上面的分配方式保证了各核Tile 块数接近,但没有保证各核计算量接近。每个 Tile 块的计算量相近时,两者等价;计算量差异较大时(例如因果掩码下不同行块的有效长度不同),需要额外调整:

  • 调整 Tile 块的编号顺序,使计算量大的 Tile 块分散到不同的核,而不是集中在少数核上。
  • 拆分计算量过大的 Tile 块,减少单个 Tile 块造成的尾部等待。

切分粒度的选择需要同时考虑两端:

  • Tile 块总数要足以覆盖核数。Tile 块数少于核数时部分核不会进入循环;Tile 块数接近核数时,零头会明显放大为负载差异。
  • 每核分配的 Tile 块不宜过多。在数据规模一定时,每核处理过多 Tile 块通常意味着使用核数偏少,串行迭代过长,难以充分发挥多核算力。
  • 单个 Tile 块不宜过小。Tile 块越小,循环控制、数据搬运启动和同步的开销占比越高,且计算指令占比越小,算力不能充分发挥。

设计切分方式时重点检查三点:Tile 块总数是否足以覆盖计划使用的核;各核分到的 Tile 块数和计算量是否接近,单核是否承担了过多 Tile 块;切分后的 Tile 形状是否满足数据搬运和计算接口的约束。

尾块处理

尾块场景需要向上取整计算 Tile 数以覆盖全部任务,并根据数据搬运和计算接口的对齐要求选择 Tile 尺寸;有效形状、填充和计算方法参考 Tile 计算中的尾块处理。

三、Tiling 参数定义与传递:TilingKey 与 TilingData

切分需要的参数(每个核处理多少任务、每个 Tile 多大、循环多少次等)通常依赖运行时的 shape,无法在编写 Kernel 时写死,需要在 Host 侧计算后传给 Kernel。这些参数由 TilingKey 和 TilingData 共同承担(Tiling参数定义与传递):

类型生效阶段承担的部分
TilingKeyKernel 编译时选择本次使用哪一套实现,例如对齐与非对齐、不同的 Tile 模板或数据排布。候选数量有限,每个取值组合生成一个专用 Kernel 实例
TilingDataKernel 运行时携带这一套实现所需的具体数值,例如 shape、stride、循环边界和缩放系数。数值变化时复用同一份已编译的 Kernel

即 TilingKey 决定走哪个分支,TilingData 提供该分支运行所需的数据。

TilingData:dataclass 声明与运行时传值

TilingData 使用 Pythondataclass声明。框架根据字段标注生成设备侧结构体,并在启动 Kernel 时序列化字段值。一个 Kernel 最多只能声明一个 TilingData 参数,且该参数必须位于形参列表末尾。

from __future__ import annotations from dataclasses import dataclass @dataclass class AddTiling: rows: int cols: int scale: float strides: int[2]

字段类型。TilingData 的字段支持标量int、float、bool,以及对应的定长数组int[N]、float[N]、bool[N]。其中,int对应DT_INT64,float对应DT_FP32,bool对应DT_BOOL。

字段声明需满足以下要求:

  • 类中至少包含一个字段,每个字段只能使用上述类型。
  • 定长数组的长度N必须直接写成 1~2048 的整数值;运行时传入的列表长度必须与声明一致。
  • 使用int[N]、float[N]或bool[N]时,需要启用from __future__ import annotations。
  • 字段可以设置dataclass默认值;数组默认值使用dataclasses.field(default_factory=...)。
  • 字段顺序决定设备侧结构体的字段顺序、偏移和对齐,不能在 Host 侧和设备侧使用不同顺序解释同一份数据。

Host 侧传入。Host 侧构造 TilingData 实例,并将其放在 Kernel 启动实参列表末尾(下面的add_kernel是一个接收AddTiling的 Kernel,定义见下文):

tiling = AddTiling( rows=513, cols=511, scale=1.0, strides=[511, 1], ) block_dim = 8 add_kernelNone, block_dim

使用默认值时,未显式传入的字段采用dataclass默认值:

from dataclasses import dataclass, field @dataclass class CopyTiling: length: int scale: float = 1.0 offsets: int[4] = field(default_factory=lambda: [0, 0, 0, 0])

带默认值的字段必须位于无默认值字段之后。

Kernel 中读取。在 Kernel 函数签名中声明 TilingData 参数后,即可通过字段名读取运行时值:

import pypto_pro.language as pl @pl.jit(auto_mutex=True) def add_kernel( x: pl.Ptr[pl.DT_FP16], y: pl.Ptr[pl.DT_FP16], z: pl.Ptr[pl.DT_FP16], tiling: AddTiling, ): strides = [tiling.strides[0], tiling.strides[1]] tensor_x = pl.make_tensor(x, [tiling.rows, tiling.cols], strides) tensor_y = pl.make_tensor(y, [tiling.rows, tiling.cols], strides) tensor_z = pl.make_tensor(z, [tiling.rows, tiling.cols], strides) tile_rows = (tiling.rows + 127) // 128 tile_cols = (tiling.cols + 127) // 128 for tile_idx in pl.range( pl.get_block_idx(), tile_rows * tile_cols, pl.get_block_num(), ): ...

TilingData 字段是运行时值,可以用于:

  • pypto_pro.language.make_tensor的 shape 和 stride。
  • pypto_pro.language.range的循环边界。
  • Tile 数量、偏移和有效形状等标量计算。
  • 运行时if条件和算术表达式。

TilingData 字段不能用于要求编译期 Python 常量的参数,例如TileType的静态shape。这类有限候选项应通过 TilingKey 选择。

源码级实现:字段如何变成设备侧结构体

从源码结构看,TilingData 的校验与序列化逻辑集中在 python/pypto_pro/language/typing/_tiling.py:

  • is_tiling_class()判定一个类是否为合法 Tiling 类:必须是带至少一个字段的dataclass,且所有字段标注都能被_parse_field_annotation()解析为标量类型或T[N]定长数组。其中N必须是正整数字面量,并通过check_in_range(size, 1, _MAX_ARRAY_SIZE, ...)限制在 1~2048(_MAX_ARRAY_SIZE = 2048),与文档描述的约束一一对应。
  • _PYTHON_TYPE_TO_DTYPE明确给出int → DataType.INT64、float → DataType.FP32、bool → DataType.BOOL的映射,即文档中"int对应 DT_INT64"的出处。
  • get_tiling_tuple_type()把各字段降级为 IR 的TupleType(定长数组T[N]降级为 N 个标量组成的元组),用于 Kernel 参数类型检查。
  • get_tiling_ctype_struct()依据字段顺序生成ctypes.Structure,其原生对齐规则与 CCE codegen 输出的 Cstruct Tiling保持一致,从而保证 Host 侧tiling_instance_to_bytes()序列化出的字节布局与设备侧结构体的sizeof、字段偏移、对齐完全相同——这就是"字段顺序决定设备侧结构体布局"这句话的底层依据。
  • 额外的防御性检查:_check_duplicate_annotations()通过解析类源码 AST 检测重复字段名(Python 运行时的__annotations__会去重、看不到重复项);tiling_instance_to_bytes()会在传入的列表长度与T[N]声明不一致时抛出InvalidType。

单测 python/tests/ut/pypto_pro/language/tiling_class/test_array_type.py 覆盖了int[4]、float[2]、bool[3]的合法解析以及str[4]、int[0]等非法标注的拒绝行为,可作为字段类型约束的验证参考。

TilingKey:编译期常量与专用 Kernel 实例

TilingKey 描述一组有限的编译期配置。同一份 Kernel 源码可以针对不同 Key 生成多个专用实例,并在启动时选择其中一个。Key 字段在 Kernel 中是编译期常量,对应的条件分支可以在编译阶段确定。

声明 TilingKey。TilingKey 使用普通 Python 类声明,类属性使用TilingKeyField定义候选值:

from pypto_pro.runtime.tilingkey import TilingKeyField class AddKey: UseScale = TilingKeyField(bits=1, values=[0, 1]) BlockM = TilingKeyField(bits=2, values=[64, 128]) def is_valid(self, key): use_scale, block_m = key return use_scale == 0 or block_m == 128

字段按照类中的定义顺序收集。该顺序决定is_valid()参数元组的顺序和 64-bit Key 中各字段的位置。各字段候选值的下标打包为 64-bit Key,是为了与离线二进制编译场景使用的uint64_tTilingKey 配合。

TilingKeyField需满足以下约束:

项目约束
bits必须大于 0
values候选值只能是整数,不能使用字符串、浮点数或bool;集合必须非空且值互不重复
编码容量候选值数量不能超过2**bits
总位宽所有字段的bits之和不能超过 64
is_valid可选;用于拒绝不支持的字段组合

字段编码保存候选值在values中的下标,而不是候选值本身。例如:

class BlockKey: BlockM = TilingKeyField(bits=2, values=[32, 64, 128])

BlockM=32、64、128分别编码为0、1、2。Host 侧启动 Kernel 时仍传入实际值,不需要计算候选下标。

将 TilingKey 绑定到 Kernel。通过@pypto_pro.language.jit(tiling_key=...)绑定 TilingKey。字段名可以直接在 Kernel 函数体中使用,无需加入形参列表:

import pypto_pro.language as pl @pl.jit(auto_mutex=True, tiling_key=AddKey) def add_kernel( x: pl.Ptr[pl.DT_FP16], y: pl.Ptr[pl.DT_FP16], z: pl.Ptr[pl.DT_FP16], tiling: AddTiling, ): tile_type = pl.TileType( shape=[BlockM, 128], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec, ) if UseScale == 1: # 此分支在编译时确定 ... else: ...

TilingKey 字段名不能与 Kernel 形参或模块级普通变量重名。字段应只描述会影响代码生成的有限模式,不要为任意运行时 shape 分别定义 Key。

选择 TilingKey 并启动 Kernel。Key 字典位于启动参数中的stream和block_dim之后:

key = {"UseScale": 1, "BlockM": 128} add_kernelNone, block_dim, key

Key 字典必须满足以下要求:

  • 包含 Schema 中的全部字段,且不能包含额外字段。
  • 每个值都属于对应字段的values。
  • 字段组合能够通过is_valid()校验。
  • 传入字典中的值是实际候选值,不是编码后的下标。

同时使用 datatype 特化时,datatype 字典位于 TilingKey 之后:

add_kernelNone, block_dim, key, datatype

二进制交付时,TilingKey Schema 用于生成字段声明和合法的 Key 组合,具体流程参考 离线二进制编译。

源码级实现:64-bit 打包与启动时校验

上述约束在 python/pypto_pro/runtime/tilingkey.py 的TilingKeySchema中逐条落地:

  • _validate_and_assign_offsets()(第 156~179 行)依次检查:bits > 0;values非空且元素为int(显式排除bool,即isinstance(v, bool)会被拒绝)且互不重复;候选值数量不超过1 << bits;所有字段位宽之和不超过_MAX_TOTAL_BITS = 64。同时按定义顺序为每个字段分配位偏移offset。
  • pack()(第 130~136 行)实现"候选值下标打包为 64-bit Key":packed |= value_index << field.offset,与文档描述一致。模块 docstring 还说明该编码方式与 AscendC 模板 Tiling Key 的位选择语义对齐。
  • validate_concrete()(第 138~154 行)对应启动时对 Key 字典的校验:必须是 dict、键集合与 Schema 字段完全一致、每个值都在候选集合内、is_valid返回真,否则分别抛出InvalidType/InvalidArgument/InvalidVal。
  • enumerate_valid()对全部字段组合做笛卡尔积并用is_valid过滤,服务于离线二进制编译场景下"枚举合法 Key 组合、逐一生成专用实例"的流程。

单测 python/tests/ut/pypto_pro/runtime/tiling_key/test_validation.py 给出了完整的校验行为示例:{"OpType": 99}(候选值之外)、{}(缺字段)、{"OpType": 0, "ExtraField": 1}(多字段)、[0](非 dict)、以及被is_valid拒绝的组合都会在kernel[None, 1, key]阶段抛出异常。

系统级用例 python/tests/st/pypto_pro/frontend/matmul/test_matmul_tilingkey.py 则演示了 TilingKey 的典型价值:Dtype(0: FP16 / 1: BF16)和TileBlock(0: 64 / 1: 128)两个 1-bit 字段,让同一份 matmul 源码在TileType(shape=...)处按 Key 常量折叠出 4 个专用实例,Kernel 体内的if Dtype == 0: io_dtype = pl.DT_FP16 ...分支在编译时确定。测试通过matmul_tilingkeyNone, 1, {"Dtype": dtype_key, "TileBlock": tile_key}启动并与torch.matmul对照数值,覆盖了全部 4 个 Key 组合。

TilingData 与 TilingKey 配合使用

同一个 Kernel 可以使用 TilingKey 选择编译期实现,再通过 TilingData 接收本次调用的运行时参数:

key = {"UseScale": 1, "BlockM": 128} tiling = AddTiling(rows=m, cols=n, scale=0.5, strides=[n, 1]) add_kernelNone, block_dim, key

选择参数传输方式时,可以按以下规则判断:

参数特点传输方式
每次调用都可能变化,且不要求重新生成代码TilingData
候选数量有限,并影响代码分支、Tile 模板或排布TilingKey
只决定启动的核数block_dim,参考 blockDim 的含义与设置

其中block_dim表示请求启动的逻辑核数:Vector Kernel 中是逻辑 Vector(AIV)核数,Cube Kernel 中是逻辑 Cube(AIC)核数,MIX Kernel 中是逻辑执行组数(1:2 芯片上每组含 2 个 Vector 核和 1 个 Cube 核);实际生效数量一律以get_block_num()为准(见 Kernel核函数)。

使用限制与建议

  • 不要把任意 shape 值放入 TilingKey,否则候选组合会快速增加,并产生大量专用 Kernel。
  • 不要把影响静态 Tile 类型的参数放入 TilingData,这类参数在 Kernel 运行时才可用。
  • 修改 TilingData 字段顺序或类型会改变设备侧结构体布局,Host 侧与 Kernel 必须使用同一定义。
  • 数组字段长度必须与声明完全一致;运行时可变长度数据应拆成长度字段和固定容量数组,或通过 Tensor 传递。
  • 同时使用 TilingKey、datatype 特化和 TilingData 时,分别检查启动方括号参数与函数调用参数的顺序。

四、延伸阅读

  • 多核Tiling切分:核间切分的具体写法,包括任务编号与连续分核、MIX Kernel 的切分和负载均衡。
  • Tiling参数定义与传递:TilingData 和 TilingKey 的声明、传递与配合使用。
  • Tile创建和操作:Tile 的规格描述、片上地址绑定和有效形状设置。
  • SIMD抽象硬件架构:AIC 与 AIV 的计算单元、片上存储层级和数据通路。
  • 实现与测试:TilingKey Schema、Tiling 类类型与序列化、TilingKey 校验单测、matmul TilingKey 系统测试、Tiling 数组字段单测。
  • 人工智能
  • 编译器
  • 模型编译
  • 深度学习
  • 高性能计算
  • CANN
  • Ascend

【免费下载链接】pypto

PyPTO(发音: pai p-t-o):Parallel Tensor/Tile Operation编程范式。

项目地址:https://gitcode.com/cann/pypto
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询