TileFoundry Spec — evaluator (HIR reference interpreter)¶
The evaluator executes a HIR Function's SSA-DAG on torch tensors
and returns concrete values. It is a codegen-independent reference
oracle for parser output, type inference, and op value semantics; it
does not lower to TIR or invoke codegen / runtime.
flowchart TB
evaluate["<b>evaluate()</b><br/>entry"]
Evaluator["<b>EvaluatorVisitor</b><br/>ExprVisitor[Value]"]
registry["<b>eval_registry</b><br/>register_eval(Op)"]
handler["per-op handler<br/>(EvaluateContext → Value)"]
Value["<b>Value</b>"]
TensorValue["<b>TensorValue</b><br/>(data, type)"]
TupleValue["<b>TupleValue</b><br/>(elements)"]
evaluate --> Evaluator
Evaluator -. "Call(target=Op)" .-> registry
registry --> handler
handler -. returns .-> Value
Evaluator -. produces .-> Value
Value --> TensorValue
Value --> TupleValue
def evaluate(
target: "Function | LoadedModule",
*inputs: "torch.Tensor",
) -> "torch.Tensor | tuple[torch.Tensor, ...]":
...
evaluate binds inputs to the selected Function's parameters in
order, walks the body, and returns the logical tensor for a single
output or a tuple for a TupleType result. When passed a LoadedModule,
it binds only activation inputs; declared constants are loaded lazily from
that reading at first use. A loaded module runs its declared entry; callers
select another function or child on the LoadedModule before calling
evaluate.
The evaluator selects nothing on the caller's behalf: no tensor engine, and no
device. It computes where its inputs already are, so what torch supports is what
it supports. A run whose inputs carry no tensor leaves the device to torch's own
default.
1. Value¶
The values that flow through evaluation form a small hierarchy: a
single-output node produces a TensorValue; a multi-output node (a
Tuple, a TupleType Call, or a multi-carry GridRegionExpr)
produces a TupleValue.
- constraints: none — abstract base; concrete values are
TensorValue/TupleValue
TensorValue¶
class TensorValue(Value):
"""Pair a logical tensor value with its HIR type.
Attributes:
data: attribute; Logical tensor value.
type: attribute; HIR tensor type, including layout.
"""
data: torch.Tensor
type: TensorType
- constraints:
dataholds the value in its logical shape — the shape oftype(types §2), not a layout-domain shape.type.layout, when aShardLayoutorLayout, drives the layout-domain projection of §6.- A scalar value is a rank-0
datawith a rank-0TensorType.
TupleValue¶
class TupleValue(Value):
"""Aggregate evaluated values.
Attributes:
elements: attribute; Values in field order.
"""
elements: tuple[Value, ...]
- constraints:
elementsare the per-fieldValues;tuple_get_itemprojects one by static index.
2. Parameters and inputs¶
evaluate binds each entry-Function parameter Var to the
corresponding positional input:
- An input MUST be convertible to a torch tensor; it is cast to the
parameter
TensorType's dtype. - Weights and activations are bound identically — a weight is an
ordinary
Functionparameter, not a distinct constant carrier. -
Each
DimVar(types §3) appearing in a parameter'sTensorType.shapeis bound from the corresponding axis of the input tensor's concrete shape. A later occurrence of the sameDimVarMUST agree with the first binding. -
constraints:
- A
FunctionorCallentry takes no resource context and requires one input per declared parameter. ALoadedModuleentry takes activation inputs only and supplies eachConstTensorlazily by name. A loaded module MUST have anentry; otherwise the evaluator refuses the call as having no default step. Another function or child is selected as aLoadedModulevalue before evaluation; orchestration methods are host Python and are not evaluator targets. - the run happens where the inputs already are. Inputs on more than one device MUST be refused, naming which input is where. For a loaded reading, a weight somewhere other than the inputs MUST be refused at its first use, naming the weight. Evaluation moves neither kind of tensor implicitly, and it never picks a device the caller did not express.
3. register_eval and the eval context¶
Each op's value semantics are a handler registered against the op
class. The registry is local to the evaluator (it reuses the
AnalysisRegistry container of
visitor-registry §2 but is not one of that
spec's module-level instances).
eval_registry: AnalysisRegistry[type[Op]]
def register_eval(op_cls: type[Op]):
"""Return the evaluator registration decorator for an Op class."""
...
A handler receives an EvaluateContext and returns a Value:
class EvaluateContext:
"""Carry one recursive evaluation and registered evaluator invocation.
Attributes:
op: attribute; Operation instance.
args: attribute; Evaluated operands in Call-argument order.
result_type: attribute; Call result type.
loaded_module: attribute; Runtime module reading, when one is active.
device: attribute; Where the inputs are, or None to leave it to torch.
dim_bindings: attribute; concrete values for symbolic ShapeDims.
"""
op: Any = None
args: tuple[Any, ...] = ()
result_type: Any = None
loaded_module: Any | None = None
device: str | None = None
dim_bindings: Mapping[str, int] = field(default_factory=dict)
def for_op(self, op: Any, args: tuple[Any, ...], result_type: Any) -> EvaluateContext: ...
def handler(ctx: EvaluateContext) -> Value:
"""Evaluate one registered Op invocation."""
...
A Call whose op class has no registered handler raises an error that
names the op class. Dtype promotion follows torch's own
rules; a handler MUST NOT depend on type inference having run.
Every failed Op dispatch, including a missing handler and an exception from
the handler, MUST surface as EvalError prefixed by describe_expr(call).
When parser provenance is available, that prefix identifies the authored
physical source location, binding, and op. Function-call, GridRegionExpr,
and Var failures retain their own contracts.
4. Node evaluation¶
Evaluation is an ExprVisitor[Value]
(visitor-mutator §1) memoized on id(expr), so
a shared sub-DAG (hir §1.1) is evaluated once:
class EvaluatorVisitor(ExprVisitor):
"""Evaluate expressions with identity-based memoization."""
def visit(self, expr: Expr, ctx: EvaluateContext) -> Value: ...
def visit_GridRegionExpr(self, region: GridRegionExpr, ctx: EvaluateContext) -> Value: ...
- A
Varresolves to its binding in the current environment; aConstant(core-ir §2) materialises to a torch tensor of itsTensorType(a scalar becomes a rank-0 tensor). - A
Callwhosetargetis anOpevaluates its operands, then dispatches througheval_registry(§3). EvaluateContextcarries evaluated operands and concretedim_bindingsfor call-invariantShapeDimattributes. Expr-valued runtime data MUST be a Call operand; handlers MUST NOT re-enter the evaluator through an attribute.Sliceconsumes its evaluatedstartstuple and resolvessizes/stridesthroughdim_bindings. A window exceeding a runtime axis MUST raiseEvalErrorrather than return a value whose data disagrees with its full-window type.- A dim-arithmetic
Callreached as an operand — aSlicestart moved off a loop's induction variable is one — folds the values its operands evaluated to. It performs the arithmeticresolve_dimperforms in a shape position; only the leaves differ, being operands rather thanDimVarsizes. - A
Callwhosetargetis aFunction(hir §1.1) binds the evaluated arguments to the callee's parameters in a fresh environment and evaluates the calleebody— the same value semantics a call site has under type inference. Each evaluated argument's HIR type MUST be compatible with its parameter annotation under the Function-boundary rules in HIR; an incompatible value raisesEvalErrorbefore the callee body is evaluated.
5. GridRegionExpr¶
A GridRegionExpr (hir §1.2) is a loop over its iteration
domain whose carry chain starts from init_args:
EvaluatorVisitor.visit_GridRegionExpr(region) implements the loop; there is no
separate eval_grid function.
- The first iteration binds each
carried_argsphi to the matchinginit_argsvalue; each later iteration binds it to the previous iteration'syield_values. induction_varis bound to the current index (a rank-0 tensor) for every iteration.- The result is the final carried value (single carry) or a
TupleValueof them (multi-carry), matching the node'stype. - A no-carry loop (
init_args/carried_args/yield_valuesall empty) yields the finalbodyvalue.
6. Layout domain¶
Evaluation models a single mesh participant and operates on logical values:
- An axis-bearing op (
Reduce,rms_norm, …) addresses itsaxis/axesin the operand's logicalTensorType.shape, regardless oftype.layout. A computation that must group a logical axis differently (e.g. per-head normalisation) is expressed by a logicalReshape(hir §1.3) to the target logical shape before the op; the op's axis then indexes that reshaped logical shape.Reshardonly changes distribution / layout and never changes which values an op reduces or indexes. as_layout_view(value: TensorValue) -> torch.Tensorreshapesdatafrom its logical shape to the element organisation oftype.layout.shape(for aShardLayout, itslayout.shape) under default-contiguous ordering;from_layout_view(data, type)is the inverse. These are provided for an op explicitly defined to compute in the layout domain; no op in the current set is layout-domain, so none of them call these helpers.Reshard(hir §1.3) preserves the logical value and MAY reshape it into the target layout's shape; it performs no cross-participant data movement.Local(hir §1.3) returns its operand's value only when its inputShardLayouthas noSplitattribute. For a split axis it MUST raiseEvalErrorsaying that the evaluator models one mesh participant and linking to this section, until the mesh evaluator models that path. An unmodelled path MUST identify itself rather than leaking a torch exception.