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 LoopRegion)
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
DispatchRegistry container of
visitor-registry §3 but is not one of
that spec's module-level instances).
eval_registry: DispatchRegistry[type[Op]]
def register_eval(op_cls: type[Op]):
"""Return the evaluator registration decorator for an Op class."""
...
Instruction-backed HIR scheduling uses a parallel registry:
schedule_eval_registry: DispatchRegistry[type[Op]]
def register_schedule_eval(op_cls: type[Op]): ...
eval_registry answers how to execute an HIR Op. The schedule registry
instead answers which SSA value tf.schedule produces when it selects a TIR
instruction class. Those are distinct from the in-place effects of executing
the TIR instruction itself, so TIR schedule-value handlers MUST NOT be
registered in eval_registry. The ScheduleOp evaluator performs the second
dispatch and refuses an instruction with no schedule-value handler.
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, LoopRegion,
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_LoopRegion(self, region: LoopRegion, 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). Emptymaterializes deterministic zeros of its declared shape and dtype for reference evaluation.- 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. LoopRegion¶
A LoopRegion (hir §1.2) is a loop over its iteration
domain with an isolated entry block of params and args.
EvaluatorVisitor.visit_LoopRegion(region) implements the loop. All args
are evaluated once in the enclosing scope. With k = len(yield_values):
- The first iteration binds
params[:k]toargs[:k]; later iterations bind them to the previous iteration'syield_values. - Every iteration binds invariant
params[k:]to the savedargs[k:]values andinduction_varto the current index (a rank-0 tensor). Its memo contains only these entry bindings; body Calls execute in that iteration. - The result is the final carried value or a
TupleValueof multiple carries. - A no-carry loop (
k == 0) yields the finalbodyvalue; it may capture values.
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.