扩展写法¶
本页介绍只有部分 spec 才会用到的写法。每一节对应一种情况,各节可以独立阅读。大多数 spec 所需的字段见写一个 spec。
1. 可选输入、可空输出与可缺省参数¶
张量是否存在通过 present 表达。以 FusedMoESharedExpert 为例:
inputs:
correction_bias: {dtype: float32, shape: "[E]", optional: true}
shared_w_gate_up: {dtype: D, shape: "[2 * S, H]", optional: true}
shared_w_down: {dtype: D, shape: "[H, S]", optional: "present(shared_w_gate_up)"}
outputs:
shared_output: {dtype: D, shape: "[T, H]", nullable: "present(shared_w_gate_up)"}
routed_output: {dtype: D, shape: "[T, H]"}
- 可选输入使用
optional: true声明。如果几个张量必须同时传入或同时不传,它们共用同一个 discriminant,写作optional: "<表达式>",例如上例中的shared_w_down。 - 可能返回
None的输出使用nullable: "<表达式>"声明。在上例中,传入shared_w_gate_up时 op 返回shared_output,否则该输出为None。 optional与nullable的表达式只能由取值有限的布尔量组成,包括Bool与枚举参数、ADT 的 tag 与取值有限的字段,以及present(...)。- 可选输入排在必选输入之后,
forward按声明顺序接收它们,默认值为None。省略一个可选输入与显式传入None效果相同。 int | None参数的 kind 是Maybe[Int]:present(v)表示它是否给出,v.value是它的值。v.value只能出现在present(v)为 true 的分支中。- 一个 index 只在出现它的分支上被用到。在上例中,不传
shared_w_gate_up时S没有被用到,对应的 workload 行不需要给出S。 - workload 行通过
some列出传入的可选张量。some只列出声明为optional: true的张量;optional为表达式的张量随表达式的取值确定,不写在some中。例如传入共享专家时写some: [shared_w_gate_up],shared_w_down随之传入。 - 对于
implemented的 op,每个可选张量都要至少有一条行传入它,并至少有一条行不传它。
2. 随参数变化的形状:type family¶
当参数决定张量的秩或轴的顺序时,形状使用 type family 描述。type family 定义在 spec 的 signature.types 中:
signature:
types:
Mat:
params: {t: Bool, R: Dim, C: Dim}
match: t
cases:
- {when: false, is: "[R, C]"}
- {when: true, is: "[C, R]"}
inputs:
a: {dtype: T, shape: "Mat[trans_a, M, K]"} # 实参依次对应 Mat 的 t、R、C
- 张量的
shape写作<Family>[实参, ...],实参按params的声明顺序对应。 match的对象必须是取值有限的 discriminant,即Bool、枚举、ADT、present(...),或由它们组成的元组。对元组做 match 时,when写成列表,例如when: [true, false]。cases在 spec 接受的所有取值上必须既无遗漏也无重叠。- 多个张量应用同一个 type family 时,它们总是取同一个分支。
- type family 之间的引用不能成环;如果某个 type family 没有被任何形状使用,validator 会报错。
只读取 discriminant 的 refinement 称为定义域限制。定义域限制在选择 type family 分支之前检查,被它排除的取值不需要对应的 case。以 Clamp 为例:
types:
ClampOut:
params: {A: Shape, L: Shape, U: Shape, pl: Bool, pu: Bool}
match: [pl, pu]
cases:
- {when: [true, true], is: "[*broadcast(A, L, U)]"}
- {when: [true, false], is: "[*broadcast(A, L)]"}
- {when: [false, true], is: "[*broadcast(A, U)]"}
outputs:
output: {dtype: T, shape: "ClampOut[A, L, U, present(min), present(max)]"}
shape_rules:
- "present(min) or present(max)"
present(min) or present(max) 只读取 discriminant,它排除了 min 与 max 都不传的情况,因此三个 case 已经覆盖了 Clamp 接受的所有组合。判断一条 refinement 是否只读取 discriminant 时,考察的是它出现的所有名字,与操作数的书写顺序无关。
3. 带字段的参数:ADT¶
当一个参数有有限的几种形态,且每种形态带有各自的字段时,参数的类型使用 ADT 描述。ADT 定义在 spec/types.yaml 中,供多个 spec 共用:
adts:
MGroupedLayout:
sum:
contiguous:
python: tileops.ops.moe.contracts.ContiguousLayoutSpec
fields:
packing: {type: "'tight' | 'aligned'", python: tileops.ops.moe.contracts.ContiguousPacking}
metadata_kind: {type: "'physical_psum' | 'per_row'", python: tileops.ops.moe.contracts.ContiguousMetadata}
alignment: Dim
invariant: "(packing == 'tight') == (alignment == 1) and alignment >= 1"
masked:
python: tileops.ops.moe.contracts.MaskedLayoutSpec
fields: {max_m: Dim}
- 每个 constructor 对应一个 Python 类,由
python指定。对象的kind属性是 constructor 名,各字段是同名属性,枚举字段的取值是属性的.value。 - ADT 值写作
{constructor: {字段: 值}},workload 行中也使用这种写法。 invariant是 constructor 上可选的 refinement,在实例化与构造时都会检查。- type family 可以按 constructor 做 pattern matching:
{masked: _}匹配任意 masked 值,{contiguous: {metadata_kind: per_row}}同时约束了一个取值有限的字段。constructor 特有的字段只能在匹配了该 constructor 的分支中读取,例如 masked 分支中的layout.max_m。 - ADT 是 sealed 的,constructor 与字段在定义处确定。为共用的 ADT 增加 constructor 时直接修改其定义;不接受新 constructor 的 spec 可以用一条定义域限制排除它,无需修改 type family。
4. 计算得到的量:let 与 primitive¶
形状或公式中需要用到由 index 计算得到的量时,这个量定义为 let。以 MaxPool2d 为例:
let:
kH: "per_axis(kernel_size, 0, 2)"
sH: "per_axis(stride, 0, 2, fallback=kH)"
H_out: "pool.out(H_in, kH, sH, pH, dH, ceil_mode)"
outputs:
output: {dtype: T, shape: "[N, C, H_out, W_out]"}
let的值由签名计算:能在构造时求值的在构造时计算,其余的在每次调用时计算。workload 行中不需要给出let。- 一个
let可以引用其他let,例如sH引用了kH,但let之间的依赖不能成环。
primitive 是表达式中可以调用的内建函数,例如 broadcast、reduced、per_axis、ceil_div。
- primitive、generator 与
requires中的谓词都是固定的集合,完整列表在tileops.manifest.primitives中,每个成员的 docstring 说明了它的计算内容。 - 只供某一个 family 使用的成员带有 family 前缀,例如
pool.out、moe.capacity。 - 参数超出定义域时,primitive 会报错,错误信息指向调用它的那条声明。
- 新增成员需要修改
tileops.manifest.primitives,并附带测试。 - 所有接受轴参数的 primitive 都按同一规则处理轴:对零维张量,
0与-1都表示唯一的标量轴;其他情况下,轴的取值范围是[-rank, rank)。
5. 构造期张量、内存布局与设备¶
- 在构造时传入的张量声明在
params中,带有dtype与shape,并可以声明optional: true。例如 LongRoPE 的rescale_factors:
- 要求内存连续的张量声明
contiguous: true,例如 MoE staged 系列 op 的输入与输出。没有这项声明的张量可以有任意 stride。 - 必须位于 CPU 上的张量声明
device: cpu,例如GDNFwdOp的cu_seqlens_cpu。 - 没有调用期张量输入的 op 声明
device参数,例如 Alibi。调用设备的确定方式见调用与校验 4。
6. 对参数的写入:effect¶
没有 effect 声明的 op 只读取输入,并为输出分配新的张量。如果 op 会写入参数,需要在签名的相应张量上声明 effect。effect 决定了 roofline 的读写计数,以及有编译边界的 op 所生成的 operator schema。
表 1 effect 声明
| No. | 声明 | 含义 | 例子 |
|---|---|---|---|
| 1 | 输出上的 buffer: out |
forward 在所有输入之后增加参数 out。调用方传入 out 时,op 将结果写入并返回它;未传入时,op 分配新张量。out 与该输出的形状和 dtype 相同 |
MoEGroupedGemm 的 output |
| 2 | 输入上的 mutated: true |
op 可能写入这个输入,它在调用前的内容参与计算 | |
| 3 | 输入上的 mutated: true 与 write_only: true |
必须传入的结果缓冲:op 覆盖写入,结果只取决于其他输入;如果 op 返回 None,outputs 为空 |
FusedMoEExperts 的 output |
| 4 | 输入上的 mutated: "<discriminant 表达式>" |
仅当表达式为 true 时,op 才写入这个输入 | 激活函数的 mutated: inplace |
| 5 | 输出上的 alias: <输入名> |
该输入被写入时,这个输出就是该输入对象本身 | 激活函数的 alias: input |
以激活函数的 inplace 为例:
params:
inplace: {type: bool, default: false, kw_only: true}
inputs:
input: {dtype: T, shape: "[*S]", mutated: inplace}
outputs:
output: {dtype: T, shape: "[*S]", alias: input}
effect 声明还需满足以下规则,validator 会拒绝违反它们的 spec:
write_only: true只能与mutated: true同时使用;alias指向的必须是一个会被写入的输入;- 声明了
alias的输出不能再声明buffer; - 至多一个输出声明
buffer: out。
validator 对每个 effect 分支检查 operator schema、别名关系与 roofline 的读写计数是否一致。
7. metadata 张量:generator 与 requires¶
varlen、paged 等 op 使用 metadata 张量,例如 cu_seqlens、block_table。metadata 张量的类型写在签名中,取值则由 generator 在实例化时生成,generator 写在张量的 values 字段中。以 MeanPooling 为例:
forall: {B: Dim, S: Dim, H: Dim, D: Dim, NS: Dim, NC: Dim, T: "DType[...]", seq_lens: "Seq[Int]"}
inputs:
offsets: {dtype: int32, optional: true, shape: "[NS + 1]",
values: "prefix_sum(seq_lens)", requires: ["prefix_offsets(S)"]}
indices: {dtype: int32, optional: "present(offsets)", shape: "[NC, 2]",
values: "chunk_indices(seq_lens, chunk_size)"}
generator 的规则如下:
- generator 的实参由 workload 行给出,例如长度列表
seq_lens。forall中 kind 为Seq[Int]的名字只能用作 generator 的实参。workload 行中的Seq[Int]可以写成整数列表,也可以写成返回列表的值 primitive 调用,例如seq_lens: "repeat(512, 64)"。 - 实例化时,generator 的结果与声明的形状做 unification,形状中的其他 index 由此求得,不需要在行中给出。上例中的
NS与NC分别由offsets与indices的生成结果求得。实际调用时,这些 index 同样由输入张量求得。 - generator 或者是确定性的,或者使用由 workload 种子派生的私有随机数,因此同一条行每次生成的取值都相同。
- 每个 generator 结果的秩是固定的,或者由其形状参数决定。
- 生成的张量声明整数 dtype(
int32或int64);参数超出定义域,或结果超出所声明 dtype 的范围时,generator 会报错。 - generator 的实参可以是返回列表的 primitive,例如 GroupedGemm 的
as_tensor(balanced_sizes(M, G))。
requires 的规则如下:
requires列出约束 metadata 张量内容的谓词。例如prefix_offsets(S)要求张量首项为 0、单调不减、末项为S。被约束张量的内容是谓词隐含的第一个实参。- 每个谓词按固定的秩读取被约束的张量;逐元素给出上下界的谓词(如
in_range)可以作用于任意秩的张量。 - 谓词的实参可以是另一个 metadata 张量,这样一条谓词就能约束两个张量之间的关系,例如 GroupedGemm 的
batch_offsets声明requires: ["exclusive_prefix_of(batch_sizes)"],要求它是batch_sizes的 exclusive 前缀和。在被约束的张量存在的每个分支上,作为实参的那个张量也必须存在。 - validator 在实例化时用生成的取值检查
requires。在实际调用中,这些约束由调用方保证,op 不检查张量的内容;因此 validator 还会检查,在被约束的张量存在的每个分支上,谓词都是良定义的。 - 声明了
requires的张量也必须声明values。
8. dtype 组合与打包 dtype¶
当多个 DType index 只允许特定组合时,spec 用 dtype_combos 列出所有允许的组合。以 paged GQA 为例:
forall: {..., T: "DType[float16 | bfloat16 | float8_e4m3fn]", KV: "DType[float16 | bfloat16 | float8_e4m3fn]"}
dtype_combos:
- {T: float16, KV: float16}
- {T: bfloat16, KV: bfloat16}
- {T: float16, KV: float8_e4m3fn}
- {T: bfloat16, KV: float8_e4m3fn}
- {T: float8_e4m3fn, KV: float8_e4m3fn}
- 每一行是一个从 index 到 dtype 的映射。各行的键相同,键中可以包含 dtype 参数;各行互不相同。
- 一次调用的 dtype 取值必须与其中某一行完全相同。
dtype_combos的每一列在 spec 接受的每个分支上都必须被用到。
fp4、int4 等打包 dtype 存放在 uint8 等载体 dtype 中,spec 按载体书写:
dtype写载体 dtype,shape写 PyTorch 所见的载体形状,例如 GemmW4A16 的packed_weight: "[N, K // 2]";- 逻辑 dtype 由 dtype 参数给出,或在 spec 中固定;
- roofline 按载体计算字节数。
9. 取值范围随 dtype 变化的标量参数¶
有些标量参数的合法取值取决于调用时的 dtype,例如 Add 的 alpha。这类约束使用 primitive category 与 representable 写成 refinement:
params:
alpha: {type: int | float, default: 1, kw_only: true}
shape_rules:
- "category(alpha) == 'int' or category(alpha) == category(T)"
- "representable(alpha, T)"
生成的调用检查对 TileOPs in-tree 实现与 target 提供的实现应用同一条规则。
10. 复合 op:composition¶
复合 op 用 composition 记录其 in-tree 实现可能持有的子 op,以及它自身 kernel 的位置。以 FusedMoESharedExpert 为例:
composition:
kind: composite
stages:
- {name: route_select, op: FusedTopKFwdOp}
- {name: routed_experts, op: FusedMoEExpertsFwdOp}
- {name: shared_expert, op: SharedExpertMLPFwdOp, optional: true}
- 每个 stage 或者引用一个 manifest 中的 op(
op),或者引用 op 自身kernel_types中的一个键(kernel)。 - 并非每次调用都会持有的子 op,写成
optional: true的 stage;是否持有由代码决定。 - 子 op 的构造时机与次数、调度方式以及 forward 的执行,都由代码决定。
- manifest 不规定复合 op 的 roofline 与其各 stage 的 roofline 之间的关系。
- 对于
implemented的 op,validator 按顺序核对:opstage 与类的delegate_types一致,kernelstage 与类的kernel_types一致。 stages不能为空,stage 名互不相同,optional为布尔值。