TileFoundry Spec — semantic-analysis (per-Op semantic derivation)¶
This spec owns TileFoundry's concrete static analysis services — the semantic contracts that derive types, access relations, and shard layouts over the IR. Each service is a registry-backed derived visitor: the common registration and dispatch mechanism is owned by visitor-registry; this file owns each service's requirements, handler shape, required context, and the semantic rules it enforces. The access relation's own carrier is owned by visitor-registry §4.1; this file links to it rather than redefining it.
1. Type propagation¶
Type inference is registered per Op through
@register_typeinfer(<OpClass>) and enforces its constraints via ctx.error(...)
(visitor-registry §4). A handler receives the op and a
typeinfer context, derives the output Type, and reports violations through
ctx.error. A hir.Function call composes these per-op rules under
elaboration (hir §1.1): the callee body is
reconstructed and each of its nodes re-derives through the same per-op
rules under the call's actual argument types, so a relation-derived rule
never needs its own function-boundary case.
- constraints:
- A function boundary MUST NOT complete or reject a legal
Partial. - A function return MAY carry a
Partial(reduction)in aTensorType, and a tuple return MAY carry it in any nested tensor field. - Call elaboration MUST preserve the
ShardLayoutmesh-axis position and reduction on the actual value; completion remains the responsibility of an explicitReshardor allreduce.
1.1 Relation-derived type behavior¶
An op's typeinfer MAY derive the output type from the Op's access relations (visitor-registry §4.1) rather than from a hand-written rule. Every boundary states an access map from the Op's own iteration space to that tensor's index space, and what the Op walks is the union of those domains. The relations carry no tensor shape: the output shape is typeinfer-side data, derived from the op's shape rule or from the extents the output relation reaches.
Within the relation:
- A domain dim that appears in an input access map but not in the output access map is a reduction dim (it is eliminated in the output).
- A tensor axis whose access maps to a constant (rather than a domain dim) is a broadcast axis.
- A symbolic size is an isl parameter of the domain; the relation's rank is fixed and is read from the input types.
The shard consequences of these structural facts (how Split /
Broadcast / Partial propagate, and the reduction effect) are
defined in §3.2.
1.2 Domain construction and output shape derivation¶
Every boundary's image rank is held to the Type of the value it describes by the relation service (visitor-registry §4.1).
2. Access relation analysis¶
The access relation is the boundary model shared by relation-derived type
behavior, shard propagation, dependence and movement: per boundary, an affine
access map from the Op's own iteration space to a tensor's index space. Its
carrier AccessRelations and the registry that produces it are both defined in
visitor-registry §4.1.
The rule reads only the access maps' affine structure (which domain dim each
axis uses), never the domain bounds, so it is size-agnostic and identical for
static and dynamic shapes.
3. Shard propagation¶
3.1 Logical shape to layout domain¶
TensorType.shapeis the logical shape.layouthas its own domain shape.- The current interpretation is canonical regroup: linearize first along the logical shape's row-major order, then reinterpret along the layout domain's row-major order.
3.2 Relation-driven shard propagation¶
When an op's output ShardLayout is derived from a forward access
relation (§1.1), the
output ShardAttrs are determined from the input shards and the
relation's access maps by a single rule, shared across ops.
def partial_reductions_by_axis(layout: object) -> tuple[str | None, ...]: ...
def derive_output_shard_layout(
input_types: tuple,
relation,
output_shape: tuple,
*,
partial_reduction_dims: frozenset[int] = frozenset(),
complete_reduction_dims: frozenset[int] = frozenset(),
fresh_strides: bool = False,
): ...
- constraints:
derive_output_shard_layoutreads only input types, forward-relation maps, the requested output shape, and the explicit reduction/stride controls. It MUST NOT read relation bounds or an already-derived output type.partial_reductions_by_axisreturns one entry per mesh axis: the carried reduction name forPartial,Nonefor any other attr, and an empty tuple for a non-sharded layout.- With no real input sharding it returns
None. Inputs with real sharding on different meshes MUST fail. partial_reduction_dimsturns a reduced split intoPartial("sum");complete_reduction_dimsturns it intoBroadcast. The sets MUST NOT overlap.fresh_strides=Truerequests fresh canonical strides; otherwise a compatible propagated physical layout is preserved when possible.ShardLayout.attrsis indexed by mesh axis. APartial(reduction)is a value state at that exact index, not an unordered collection of reductions and not a layout position. Every propagation decision MUST retain that index and its reduction independently of every other mesh axis.- A reduction dim (a domain dim absent from the output access map) carries
one of two effects declared by the op/relation:
partialmeans the per-shard result still needs a cross-shard reduction;completemeans the reduction is already complete within each shard. - Propagation applies per input mesh axis:
Split(k)maps layout axiskto the input's logical tensor axis and then to a domain dim through the input access map.- If that domain dim appears in the output access map, the output carries
Spliton the output layout axis to which the domain dim maps. - If that domain dim is reduced, the output mesh axis becomes
Partial(reduction)for apartialeffect orBroadcastfor acompleteeffect. The resultingPartialis a value state on that mesh axis and carries no layout axis. - A
Partial(reduction)input propagates on the same mesh axis only when the op's math is proven to commute withreduction:op(reduction(x0..xn)) == reduction(op(x0)..op(xn)). This service MUST NOT make that mathematical judgment; each op's typeinfer rule owns it. On each mesh axis, two Partial inputs MUST be compatible with that op's rule. States on different mesh axes MUST NOT be compared as an unordered set. Only an explicit reduction/allreduce resolvesPartialtoBroadcast. - A
Broadcastinput axis contributes noSplit. - Two inputs binding the same domain dim to incompatible mesh axes is an error.
- A
PartialMUST NOT be silently eliminated or carried through a non-commuting ordinary op; only an explicitReshard/allreduce fromPartialtoBroadcastcompletes it. - An ordinary multi-input op MUST inspect every tensor input for a
Partial. When its result type cannot represent a secondary input's axis-preserving state, typeinfer MUST reject that input and name theReshardremedy. - A fully-
BroadcastinputShardLayoutis replicated: it carries no real sharding, contributes noSplit/Partial, and does not pin a mesh. When no input carries real sharding, the output carries none. - Only a zero-offset, unit-coefficient access of one domain dim is a
projection for shared ownership propagation. An input
Splitthat accesses any other form, including a nonzero affine translation, or an output-surviving dim reachable only through such an access, MUST fail closed. The shared service reads only the maps' affine structure, never the domain bounds, so it has no owner-boundary alignment proof for a translated access. Split(axis)indexes an output layout axis, not a logical tensor axis. A reduction-inducedPartialattaches to no layout axis; it remains a value state on the mesh axis that was reduced.
3.3 Output storage and mesh/layout compatibility¶
- constraints:
- A symmetric multi-input op (
Binary,MatMul,Concat,Stack) resolves output storage by anchoring on the concrete residency among its operands (types §2); the rule is independent of operand order. - An unmaterialized operand (
storage=umat), including host shape metadata, abstains and does not constrain the output. - One concrete operand storage is the anchor; the output takes that storage.
- Several concrete operands that agree on a storage produce that storage.
- Several concrete operands that disagree on storage cause typeinfer to
ctx.error, unless the op defines its own destination/mixed-storage resolution. There is no operand-order tie-break. - If all operands are unmaterialized, the output is unmaterialized (
umat).
This resolution uses no memory-level lattice; output residency is a function
of the concrete anchor(s) alone. (The rmem < smem < gmem hierarchy is
a Reshard-direction notion and is unrelated to output-storage anchoring.)
A tensor value's mesh / layout is carried by its TensorType.layout
(ShardLayout.mesh names the mesh instance) — that type is the source of
truth, and the IR places no scope-based restriction on values from
different meshes coexisting. Each op's registered typeinfer owns the
operand layout / mesh compatibility it requires and its result layout;
there is no uniform cross-op rule imposed from outside typeinfer.
Reshard is the explicit op that changes a value's layout / mesh.
For example, IndexSelect owns whether an indexed access is a foldable
one-element slice or a materializing data-dependent selection.