Extensions¶
This page describes forms that only some specs use. Each section covers one case, and each section can be read on its own. The fields most specs need are in Spec fields.
1. Optional inputs, nullable outputs and omittable parameters¶
Whether a tensor exists is expressed with present. FusedMoESharedExpert is an example:
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]"}
- An optional input is declared with
optional: true. If several tensors must be passed together or omitted together, they share one discriminant, writtenoptional: "<expression>", asshared_w_downdoes above. - An output that may be
Noneis declared withnullable: "<expression>". In the example above, the op returnsshared_outputwhenshared_w_gate_upis passed, and that output isNoneotherwise. - The expressions of
optionalandnullablemay consist only of boolean quantities with finitely many values:Booland enum parameters, ADT tags and fields with finitely many values, andpresent(...). - Optional inputs come after required inputs, and
forwardreceives them in declaration order with a default ofNone. Omitting an optional input has the same effect as passingNoneexplicitly. - The kind of an
int | Noneparameter isMaybe[Int]:present(v)says whether it is given, andv.valueis its value.v.valuemay appear only in branches wherepresent(v)is true. - An index is used only on the branches where it appears. In the example above, when
shared_w_gate_upis not passed,Sis not used, and the corresponding workload row does not giveS. - A workload row lists the optional tensors it passes in
some.somelists only tensors declaredoptional: true; a tensor whoseoptionalis an expression follows the value of that expression and is not written insome. For example, passing the shared expert is writtensome: [shared_w_gate_up], andshared_w_downis passed along with it. - For an
implementedop, each optional tensor must be passed by at least one row and omitted by at least one row.
2. Shapes that vary with parameters: type family¶
When a parameter determines the rank of a tensor or the order of its axes, the shape is described with a type family. A type family is defined in the spec's 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]"} # arguments map to Mat's t, R, C in order
- A tensor's
shapeis written<Family>[argument, ...], and the arguments map toparamsin declaration order. - The subject of
matchmust be a discriminant with finitely many values:Bool, an enum, an ADT,present(...), or a tuple of these. When matching a tuple,whenis written as a list, for examplewhen: [true, false]. casesmust have no gaps and no overlaps over all values the spec accepts.- When several tensors apply the same type family, they always take the same branch.
- References between type families must not form a cycle, and the validator reports an error if a type family is not used by any shape.
A refinement that reads only discriminants is called a domain restriction. A domain restriction is checked before a type family branch is selected, and values it excludes need no corresponding case. Clamp is an example:
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) reads only discriminants and excludes the case where neither min nor max is passed, so the three cases already cover every combination Clamp accepts. Whether a refinement reads only discriminants is decided by all the names that appear in it, independent of the order in which the operands are written.
3. Parameters with fields: ADT¶
When a parameter has a finite number of forms and each form carries its own fields, the parameter's type is described with an ADT. ADTs are defined in spec/types.yaml and shared by several specs:
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}
- Each constructor corresponds to a Python class, given by
python. The object'skindattribute is the constructor name, each field is an attribute of the same name, and the value of an enum field is the attribute's.value. - An ADT value is written
{constructor: {field: value}}, and workload rows use the same form. invariantis an optional refinement on a constructor, checked both at instantiation and at construction.- A type family can pattern-match on constructors:
{masked: _}matches any masked value, and{contiguous: {metadata_kind: per_row}}also constrains a field with finitely many values. A field specific to a constructor can be read only in a branch that matched that constructor, for examplelayout.max_min the masked branch. - ADTs are sealed: constructors and fields are fixed where the ADT is defined. Adding a constructor to a shared ADT is done by editing its definition directly; a spec that does not accept the new constructor can exclude it with a domain restriction, without changing its type families.
4. Computed quantities: let and primitive¶
When a shape or formula needs a quantity computed from indices, the quantity is defined as a let. MaxPool2d is an example:
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]"}
- The value of a
letis computed from the signature: aletthat can be evaluated at construction is computed at construction, and the others are computed on every call. Workload rows do not giveletentries. - A
letcan refer to otherletentries, assHrefers tokH, but dependencies betweenletentries must not form a cycle.
A primitive is a built-in function callable in expressions, such as broadcast, reduced, per_axis and ceil_div.
- Primitives, generators and the predicates in
requiresare each a fixed set. The full list is intileops.manifest.primitives, and the docstring of each member describes what it computes. - Members used by only one family carry a family prefix, such as
pool.outandmoe.capacity. - When an argument is outside its domain, the primitive raises an error that points to the declaration that called it.
- Adding a member requires changing
tileops.manifest.primitives, with tests. - Every primitive that takes an axis argument handles axes by the same rule: for a zero-dimensional tensor, both
0and-1denote the single scalar axis; otherwise, the axis ranges over[-rank, rank).
5. Construction-time tensors, memory layout and device¶
- A tensor passed at construction is declared in
paramswithdtypeandshape, and can declareoptional: true. For example,rescale_factorsin LongRoPE:
- A tensor that must be contiguous in memory declares
contiguous: true, for example the inputs and outputs of the MoE staged ops. A tensor without this declaration can have any strides. - A tensor that must be on the CPU declares
device: cpu, for examplecu_seqlens_cpuinGDNFwdOp. - An op without call-time tensor inputs declares a
deviceparameter, for example Alibi. How the call device is determined is in Calls and validation § 4.
6. Writes to arguments: effect¶
An op without effect declarations only reads its inputs and allocates new tensors for its outputs. If an op writes to an argument, an effect is declared on the corresponding tensor in the signature. Effects determine the read and write counts of the roofline, and the operator schema generated for an op with a compile boundary.
Table 1 Effect declarations
| No. | Declaration | Meaning | Example |
|---|---|---|---|
| 1 | buffer: out on an output |
forward gets a parameter out after all inputs. When the caller passes out, the op writes the result into it and returns it; otherwise the op allocates a new tensor. out has the same shape and dtype as that output |
output of MoEGroupedGemm |
| 2 | mutated: true on an input |
the op may write this input, and its contents before the call take part in the computation | |
| 3 | mutated: true and write_only: true on an input |
a result buffer that must be passed: the op overwrites it, and the result depends only on the other inputs; if the op returns None, outputs is empty |
output of FusedMoEExperts |
| 4 | mutated: "<discriminant expression>" on an input |
the op writes this input only when the expression is true | mutated: inplace of activation functions |
| 5 | alias: <input name> on an output |
when that input is written, this output is that input object itself | alias: input of activation functions |
The inplace parameter of activation functions is an example:
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 declarations must also satisfy the following rules, and the validator rejects specs that violate them:
write_only: truecan be used only together withmutated: true;aliasmust point to an input that is written;- an output that declares
aliascannot also declarebuffer; - at most one output declares
buffer: out.
For each effect branch, the validator checks that the operator schema, the aliasing and the roofline read and write counts agree.
7. Metadata tensors: generator and requires¶
Varlen, paged and similar ops use metadata tensors such as cu_seqlens and block_table. The type of a metadata tensor is written in the signature, and its values are generated at instantiation by a generator written in the tensor's values field. MeanPooling is an example:
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)"}
The rules for generators:
- The arguments of a generator are given by the workload row, for example the list of lengths
seq_lens. A name of kindSeq[Int]inforallcan be used only as a generator argument. ASeq[Int]in a workload row can be written as a list of integers or as a call to a value primitive that returns a list, for exampleseq_lens: "repeat(512, 64)". - At instantiation, the generator's result is unified with the declared shape, which solves the other indices in the shape, so they are not given in the row. In the example above,
NSandNCare solved from the generatedoffsetsandindicesrespectively. In an actual call, these indices are likewise solved from the input tensors. - A generator is either deterministic or uses private random numbers derived from the workload seed, so the same row generates the same values every time.
- The rank of each generator result is fixed, or determined by its shape arguments.
- A generated tensor declares an integer dtype (
int32orint64). When an argument is outside its domain, or the result is outside the range of the declared dtype, the generator raises an error. - A generator argument can be a primitive that returns a list, for example
as_tensor(balanced_sizes(M, G))in GroupedGemm.
The rules for requires:
requireslists predicates that constrain the contents of a metadata tensor. For example,prefix_offsets(S)requires the tensor's first element to be 0, the elements to be non-decreasing, and the last element to beS. The contents of the constrained tensor are the predicate's implicit first argument.- Each predicate reads the constrained tensor at a fixed rank; predicates that give elementwise bounds (such as
in_range) can apply to tensors of any rank. - A predicate argument can be another metadata tensor, so that one predicate constrains the relation between two tensors. For example,
batch_offsetsin GroupedGemm declaresrequires: ["exclusive_prefix_of(batch_sizes)"], which requires it to be the exclusive prefix sum ofbatch_sizes. On every branch where the constrained tensor exists, the tensor used as the argument must also exist. - The validator checks
requiresat instantiation against the generated values. In an actual call, the caller guarantees these constraints and the op does not check tensor contents; the validator therefore also checks that the predicate is well defined on every branch where the constrained tensor exists. - A tensor that declares
requiresmust also declarevalues.
8. dtype combinations and packed dtypes¶
When several DType indices allow only specific combinations, the spec lists every allowed combination in dtype_combos. Paged GQA is an example:
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}
- Each row is a mapping from index to dtype. All rows have the same keys, the keys may include dtype parameters, and the rows are all distinct.
- The dtype values of a call must equal one of the rows exactly.
- Every column of
dtype_combosmust be used on every branch the spec accepts.
Packed dtypes such as fp4 and int4 are stored in a carrier dtype such as uint8, and the spec is written in terms of the carrier:
dtypeis the carrier dtype, andshapeis the carrier shape as PyTorch sees it, for examplepacked_weight: "[N, K // 2]"in GemmW4A16;- the logical dtype is given by a dtype parameter, or fixed in the spec;
- the roofline counts bytes by the carrier.
9. Scalar parameters whose value range depends on dtype¶
The valid values of some scalar parameters depend on the dtype at call time, for example alpha in Add. Such constraints are written as refinements with the primitives category and representable:
params:
alpha: {type: int | float, default: 1, kw_only: true}
shape_rules:
- "category(alpha) == 'int' or category(alpha) == category(T)"
- "representable(alpha, T)"
The generated call checks apply the same rule to TileOPs in-tree implementations and to implementations provided by a target.
10. Composite ops: composition¶
A composite op uses composition to record the sub-ops its in-tree implementation may hold, and the positions of its own kernels. FusedMoESharedExpert is an example:
composition:
kind: composite
stages:
- {name: route_select, op: FusedTopKFwdOp}
- {name: routed_experts, op: FusedMoEExpertsFwdOp}
- {name: shared_expert, op: SharedExpertMLPFwdOp, optional: true}
- Each stage refers either to an op in the manifest (
op) or to a key in the op's ownkernel_types(kernel). - A sub-op that is not held on every call is written as a stage with
optional: true; whether it is held is decided by code. - When and how many times sub-ops are constructed, how they are scheduled, and how forward executes are all decided by code.
- The manifest does not prescribe how the roofline of a composite op relates to the rooflines of its stages.
- For an
implementedop, the validator checks in order that theopstages agree with the class'sdelegate_typesand thekernelstages agree with the class'skernel_types. stagescannot be empty, stage names are all distinct, andoptionalis a boolean.