Semantics #
Denotational semantics for NN.IR.Graph.
This file defines an evaluator for the current IR fragment:
- it evaluates nodes in SSA/topological order,
- each node applies the corresponding spec-layer tensor operation to its parents,
- parameter payloads for
const,linear, andconvare supplied by an explicitPayload.
Partiality #
The evaluator returns Except String. It fails on malformed graphs (Graph.checkWellFormed),
on nodes whose parents or declared outShape violate the shape rules, on missing or mismatched
payloads (const flat length, linear dimensions, conv geometry, batchNormEval channels,
layernorm normalized suffix), and on exactly one data-dependent condition: .log rejects an input
tensor containing an entry <= 0 (or NaN), because the spec logarithm is undefined there. Every
other operation is total on well-shaped inputs; reduceMean and layernorm divide by extents that
the shape rules already require to be positive, and mseLoss divides by the totalized
TorchLean.Tensor.meanDenominator rather than by the raw element count.
denoteAll runs the structural check but not Graph.checkShapes; each node instead checks its
parents' shapes locally and normalizeNodeOutput compares the computed shape with the declared
one. NN.IR.ShapeSoundness proves that on a graph accepted by checkShapes this final comparison
never fails, so the two validation routes agree.
Softmax and layer norm:
softmax axisnormalizes independently along the zero-based tensor dimension named byaxis. The canonical specification handles every in-bounds dimension and preserves the tensor shape. This matches the meaning oftorch.softmax(x, dim=axis)in PyTorch.layernorm axismatches PyTorch'sF.layer_norm(x, normalized_shape=x.shape[axis:])convention:axisselects the start of the normalized suffix. We implement this by reshaping the tensor into a 2D view(seqLen, embedDim), applying the spec 2D LayerNorm (Spec.layerNorm), then reshaping back.
How this relates to PyTorch:
Graph.nodesis analogous to a topologically-sorted IR like FX/TorchScript.Payloadis analogous to “parameters / buffers / constants” that live outside the pure graph structure.- The evaluator is a pure, denotational model of running the graph. It is designed for clarity and for connecting to proofs and verification passes (not for performance).
References / related systems:
- PyTorch FX: https://pytorch.org/docs/stable/fx.html
- TorchScript: https://pytorch.org/docs/stable/jit.html
- ONNX (graph + initializers): https://onnx.ai/
Except helpers #
The evaluator returns Except String, so almost every proof about it has to unfold a throw at
some point. That one rfl fact lives here, at the definition of the semantics, instead of being
restated by each consumer: NN.IR.ShapeSoundness and the runtime correctness proofs in
NN.Runtime.Autograd.IRExec both simp with it.
Permutation lowering #
Compute a sequence of adjacent swaps that realizes a target permutation.
This is used to implement .permute by repeatedly applying swapAdjacentAtDepth, which is already
available in the spec tensor library. If the permutation is ill-formed, this returns an error
explaining what went wrong.
Instances For
Permute a shape-tagged tensor according to perm.
This checks that perm is a valid permutation for the input shape (using Shape.permute?), then
lowers it to a sequence of adjacent swaps and applies them to the tensor.
Instances For
Evaluation helpers #
The evaluator itself (evalAt / denoteAll) is a fold over nodes. These helpers keep the fold
readable:
expectShapechecks a value's stored shape against the node's declaredoutShape.evalConst,evalLinear, andevalConvfetch and apply external payloads keyed by node id.
Check that a shape-erased tensor has the expected shape and recover its statically typed tensor.
Instances For
Evaluate MSE loss on two shape-erased tensors after checking that their stored shapes agree.
Instances For
MSE on two shape-erased tensors of the same shape unfolds to the shaped formula.
The IR hides tensor shapes, so the loss first has to re-discover that its two operands agree before it can subtract them. This equation says nothing else happens on the way.
Transport a Tensor α (dim n scalar) across an equality n = n' (helper for payload casts).
Instances For
Apply one affine map independently at every index of an arbitrary leading shape.
Instances For
Multiply matrices independently at every index of a shared leading shape.
Instances For
Evaluate a const node from the external payload.
Constants are stored “flat” (1D) for convenience, so we check the flattened length matches
Spec.Shape.size s and then unflatten to the requested shape.
Instances For
Evaluate a linear node from the external payload.
The final input axis must match inDim; every leading axis is preserved while the same affine map
$y=Wx+b$ is applied independently.
Instances For
Evaluate arbitrary-rank max pooling over the spatial suffix of a shape-erased tensor.
Instances For
Evaluate arbitrary-rank average pooling over the spatial suffix of a shape-erased tensor.
Instances For
Evaluate an arbitrary-rank convolution independently over every leading index.
Instances For
Evaluate fixed-statistics BatchNorm along an arbitrary channel axis.
Instances For
Layer normalization of a matrix with explicit scale, bias, and epsilon.
Instances For
Layer normalization with the historical unit affine transform and default epsilon.
Instances For
Affine data for a LayerNorm matrix view after validating its normalized suffix.
- gamma : TorchLean.Tensor α [embedDim]
- beta : TorchLean.Tensor α [embedDim]
- epsilon : α
Instances For
Resolve optional LayerNorm payload data into the vector shape consumed by the matrix semantics.
An absent payload means the standard unit scale, zero bias, and default normalization epsilon. Learned parameters are accepted only when their declared suffix is exactly the suffix normalized by the IR node.
Instances For
Decode a dynamic concat parent as a tensor with an existential leading dimension and the requested tail shape. This is the checked boundary shared by concat evaluation and its proofs.
Instances For
Fold leading-axis concat over dynamic values that already share the same tail shape.
Instances For
Evaluate a concat node from already evaluated parent values.
The IR concat operation accepts any valid axis. The tensor primitive concatenates along axis 0,
so the evaluator implements the generic case by moving the requested axis to the front, folding
Tensor.concatAxisSpec .scalar over the permuted parents, and moving the result back.
Instances For
Normalize a node result to the node's declared shape, rejecting inconsistent implementations.
Instances For
A result whose shape matches the node declaration passes normalization unchanged.
The same statement with the node written out as a literal record.
Both spellings show up in proofs depending on whether the node came from a builder or was written inline, and simp will not see through the record projection on its own.
Read an already-evaluated parent, reporting the node and available prefix on failure.
Instances For
Evaluate a known node from its already computed parent values, without the final declared-shape normalization.
This is the operator dispatch of the evaluator: each OpKind branch validates its parents,
applies the spec-layer operation, and returns a shape-erased value whose shape is computed by the
shared shape rules (NN.IR.OpContracts). evalNode wraps it with normalizeNodeOutput.
NN.IR.ShapeSoundness proves that the raw value already has the shape inferred by
Infer.nodeOutShape. The definition is a simp lemma so proofs about evalNode reduce the
selected branch exactly as before the split.
Instances For
Evaluate a known node from its already computed parent values.
Keeping operator dispatch (evalNodeRaw) separate from graph lookup lets local correctness proofs
reduce only the selected OpKind branch. The caller remains responsible for the graph's
topological invariant. The result is normalized to the node's declared outShape; on graphs
accepted by Graph.checkShapes that normalization is provably the identity.
Instances For
Evaluate node i after checking the graph's id discipline and retrieving the corresponding node.
denoteAll checks the full graph structure before repeatedly calling this one-step evaluator.
Instances For
Evaluate nodes i, i+1, ... given already computed prefix values vals.
This is written as a structurally recursive function so it is easy to reason about in proofs (evaluation is “a simple loop over node ids”).
Instances For
Evaluate a graph to a table of node values.
This returns an array vals of length g.size where vals[i] is the value of node i.
We do a structural well-formedness check once up front (ids/arity/topology). For lowering-produced
graphs, the boolean Graph.wellFormed check is a fast path; if it fails we fall back to the
exception-producing Graph.checkWellFormed so callers get a readable error message.
The evaluator always returns either:
.ok vals(all nodes evaluated successfully), or.error msgdescribing the first failure (malformed IR, missing payload, a local shape error, or a.logof a nonpositive entry).
Graph.checkShapes is not run here: the per-node checks reject the same ill-shaped graphs, and the
existing lowering-correctness proofs unfold denoteAll with only the structural check in place.
NN.IR.ShapeSoundness.denoteAllRaw_eq_denoteAll shows that on a checkShapes-accepted graph the
per-node declared-shape normalization is redundant.
Instances For
Scoped notation #
Scoped notation for evaluating a graph to all node values.
Use with:
open scoped IR
g⟦payload, input⟧
Instances For
ASCII alternative to g⟦payload, input⟧.
Instances For
Evaluate the graph and return the value at outputId.