Skip to content

TileFoundry Spec — evaluator (HIR reference interpreter)

The evaluator executes a HIR Function's SSA-DAG on a tensor backend 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>Evaluator</b><br/>ExprVisitor[Value]"]
    registry["<b>eval_registry</b><br/>register_eval(Op)"]
    handler["per-op handler<br/>(EvalContext → 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(
    fn_or_call: "Function | Call",
    *inputs: "torch.Tensor",
    backend: str = "torch",
) -> "torch.Tensor | tuple[torch.Tensor, ...]":
    ...

evaluate binds inputs to the entry Function's parameters in order, walks the body, and returns the logical tensor for a single output or a tuple for a TupleType result. backend selects the tensor engine; "torch" is the defined backend.

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.

class Value: ...    # base of every evaluated value
  • constraints: none — abstract base; concrete values are TensorValue / TupleValue

TensorValue

class TensorValue:
    data: torch.Tensor    # the logical tensor value
    type: TensorType      # the HIR type of this value (carries layout)
  • constraints:
  • data holds the value in its logical shape — the shape of type (types §2), not a layout-domain shape.
  • type.layout, when a ShardLayout or Layout, drives the layout-domain projection of §6.
  • A scalar value is a rank-0 data with a rank-0 TensorType.

TupleValue

class TupleValue:
    elements: tuple[Value, ...]    # the per-field `Value`s
  • constraints:
  • elements are the per-field Values; tuple_get_item projects 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 backend tensor; it is cast to the parameter TensorType's dtype.
  • Weights and activations are bound identically — a weight is an ordinary Function parameter, not a distinct constant carrier.
  • Each DimVar (types §3) appearing in a parameter's TensorType.shape is bound from the corresponding axis of the input tensor's concrete shape. A later occurrence of the same DimVar MUST agree with the first binding.

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]): ...   # decorator

A handler receives an EvalContext and returns a Value:

class EvalContext:
    args: tuple[Value, ...]    # already-evaluated operands in `Call.args` order (TensorValue / TupleValue)
    op: Op                     # the op instance; attributes read as fields (e.g. `ctx.op.kind`, `ctx.op.index`)
    result_type: Type          # the `Call`'s result type
    device: torch.device       # backend device; a handler materialising a new tensor (e.g. `Zeros`) creates it there

def handler(ctx: EvalContext) -> Value: ...    # a registered per-op value handler

A Call whose op class has no registered handler raises an error that names the op class. Backend dtype promotion follows the backend's own rules; a handler MUST NOT depend on type inference having run.

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 Evaluator(ExprVisitor[Value]):             # memoized on id(expr): a shared sub-DAG is evaluated once
    def visit(self, expr: Expr) -> Value: ...    # dispatch by expr kind: Var / Constant / Tuple / Call(Op) / Call(Function) / GridRegionExpr
  • A Var resolves to its binding in the current environment; a Constant (core-ir §2) materialises to a backend tensor of its TensorType (a scalar becomes a rank-0 tensor).
  • A Call whose target is an Op evaluates its operands, then dispatches through eval_registry (§3).
  • A Call whose target is a Function (hir §1.1) binds the evaluated arguments to the callee's parameters in a fresh environment and evaluates the callee body — the same value semantics a call site has under type inference.

5. GridRegionExpr

A GridRegionExpr (hir §1.2) is a loop over its iteration domain whose carry chain starts from init_args:

def eval_grid(region: GridRegionExpr) -> Value: ...    # loop over the iteration domain; carry chain starts from init_args
  • The first iteration binds each carried_args phi to the matching init_args value; each later iteration binds it to the previous iteration's yield_values.
  • induction_var is bound to the current index (a rank-0 tensor) for every iteration.
  • The result is the final carried value (single carry) or a TupleValue of them (multi-carry), matching the node's type.
  • A no-carry loop (init_args / carried_args / yield_values all empty) yields the final body value.

6. Layout domain

Evaluation models a single mesh participant and operates on logical values:

  • An axis-bearing op (Reduce, rms_norm, …) addresses its axis / axes in the operand's logical TensorType.shape, regardless of type.layout. A computation that must group a logical axis differently (e.g. per-head normalisation) is expressed by a logical Reshape (hir §1.3) to the target logical shape before the op; the op's axis then indexes that reshaped logical shape. Reshard only changes distribution / layout and never changes which values an op reduces or indexes.
  • as_layout_view(value: TensorValue) -> torch.Tensor reshapes data from its logical shape to the element organisation of type.layout.shape (for a ShardLayout, its layout.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 for the single modelled participant.