Source code for hawk.ir.nodes

# Copyright 2026 Alessandro Masat
# SPDX-License-Identifier: Apache-2.0

"""Tensor-typed IR nodes: leaf / op / sink / select.

ONE typed node family, not a class hierarchy per value shape: every node
carries a :class:`~hawk.types.TensorType` ``(shape, dtype, tag)`` as FIELDS.
Nodes are identity objects — structural equality is a KEY the canonical
walk computes (:mod:`hawk.ir.walk`), so a DAG may share a subexpression
freely. Leaves and sinks BIND a slot, keyed on ``(role, name)`` and merged
totally by the walk (:attr:`Node.binding`). Access is structural:
``own(i)`` (default), ``at(expr)`` (:class:`At`), ``scatter(expr)`` (a
wide/accum sink with an index operand).
"""

from __future__ import annotations

from collections.abc import Sequence
from typing import Any

from ..types import Slot, TensorType


[docs] class HawkError(Exception): """The one HAWK refusal type: every structural refusal named by the spec (a total-leaf-merge collision, a mixed sink set, an impossible quantity span, an undeclared reduce op) raises it."""
#: Leaf KINDS. ``constant`` is childless but binds no slot, so it is its #: own class (:class:`Const`), never appearing in ``Walk.leaves``/``arg_spec``. #: ``prior_read`` is a Mutable's launch-start value, read before its first #: store (role ``mutable``), merged with that plane's ``Assign`` SINK on #: ``(role, name)`` by the walk's total merge. LEAF_KINDS = ("vocab_read", "uniform", "table_read", "terminated", "nsamples", "prior_read") #: Roles a LEAF (read side) may carry. ``mutable`` is carried ONLY by a #: ``prior_read`` leaf (:meth:`hawk.trace.value.MutableRef.prior`, read #: internally by the ast front end for a bare read before a plane's first #: store). LEAF_ROLES = ("vec_in", "mat_in", "wide_in", "per_sample", "lookup", "terminated", "uniform", "nsamples", "mutable") #: Roles a SINK (write side) may carry. SINK_ROLES = ("mutable", "out", "wide_out", "accum_out") #: HAWK's OWN canonical role order for ``Walk.arg_spec``, chosen to match #: the previous generator's so artifacts stay comparable. The ONE #: contractual property: ``arg_spec`` order == entry-signature order. ROLE_ORDER = ("mutable", "out", "wide_out", "accum_out", "vec_in", "mat_in", "wide_in", "per_sample", "lookup", "terminated", "uniform", "nsamples") #: The quaternion op kinds; the language spells each as a method of the #: quaternion value. QUAT_OPS = ("quat_mul", "quat_conj", "quat_recip", "quat_rotate", "as_pure", "as_vec3") #: The op KIND an ``@raw_device`` block splices under: opaque author text, #: not a HAWK op, so it has no type or derivative rule (differentiating it #: refuses, naming the kind) and its access class must be DECLARED. RAW_KIND = "raw_device" #: The language a raw block is written in unless it says otherwise; a writer #: refuses a block in a language it cannot write. RAW_LANGUAGE = "aether" #: The op KIND a custom PRIMITIVE records its boundary under, deliberately #: NOT in ``hawk.ir.ops.OP_KINDS`` like :data:`RAW_KIND`: untyped by any #: rule of its own (typed by the inlined forward subgraph) and absent from #: the derivative table (the author supplies that rule). Keeps the #: closed-world table from carrying a per-primitive row. PRIMITIVE_KIND = "primitive" #: The node KIND of the readable LANE INDEX (``own(i)`` as a value): the #: GLOBAL index — ``base + flat`` under a partition, never partition-local #: — so an expression built on it is partition-invariant, like #: ``n_samples``. Lets a 2-D launch domain recover the coordinates the #: flattening folded together. SAMPLE_INDEX_KIND = "sample_index" #: The leaf ROLES an ``at(expr)`` read may target: ``lookup`` (table plane) #: and ``wide_in`` (pinned to the SAME 32-byte ``ScalarHandle``, same access), #: and the per-sample input planes, read at another lane through ``u.at(i)``. #: ``GATHER_ROLES`` are the ones read ONLY through ``at()``. GATHER_ROLES = ("lookup", "wide_in") AT_ROLES = GATHER_ROLES + ("per_sample", "vec_in", "mat_in") #: The tags a factorisation node carries (:mod:`hawk.ir.linalg`): a value so #: tagged is a struct of the factor, its pivots and its status, not a matrix. FACTOR_TAGS = ("lu_factor", "chol_factor", "eigh_factor", "qr_factor") def refuse_factor_arms(what: str, ttype: TensorType) -> None: """Refuse a ``select`` or ``dispatch`` whose arms are factorisations: a branch picks one value of a type, and a factor (with its status) is not one a program passes around; pick between the fields it reads instead.""" if ttype.tag in FACTOR_TAGS: raise HawkError( f"{what}: the arms are {ttype.tag.split('_')[0]} factorisations, which are " "read through lu_solve/cho_solve and the field reads, never selected " "between; select between the results (the solves, the determinant, the " "eigenvalues) instead") #: The compound-quantity wire namespace separator: a bound name #: containing it must be owned by a quantity. NAMESPACE_SEP = "__" #: The kind of the transient node :func:`hawk.sum` (and max/min/mean) mint; #: a store turns it into a :class:`SampleSum` sink, any other use is refused. REDUCE_KIND = "reduce" #: The reductions over samples a cell can hold. CELL_OPS = ("sum", "max", "min") #: The width of the language's row reduction: a fixed pairwise tree over this #: many consecutive samples, the same on every target. ROW_WIDTH = 32 def extent_of(node: Any) -> str: """The extent of ``node``: ``all_samples`` for a reduction over samples, ``shared`` when nothing under it depends on the sample (constants, uniforms, the sample count), else ``per_sample``.""" if isinstance(node, ReduceSamples): return "all_samples" stack, seen = [node], set() while stack: n = stack.pop() if id(n) in seen: continue seen.add(id(n)) if isinstance(n, SampleIndex) or ( isinstance(n, Leaf) and n.role not in ("uniform", "nsamples", "lookup", "wide_in")): return "per_sample" if isinstance(n, At) and not is_param_cell(n): return "per_sample" stack.extend(n.operands) return "shared" class Node: """Base of every IR node: a :class:`~hawk.types.TensorType` plus ordered operands. Identity-hashed on purpose (the canonical walk dedups by a computed key).""" __slots__ = ("ttype", "operands") kind: str = "node" def __init__(self, ttype: TensorType, operands: Sequence[Node] = ()) -> None: if not isinstance(ttype, TensorType): raise HawkError( f"{type(self).__name__}: ttype must be a TensorType, got {ttype!r}" ) self.ttype = ttype self.operands: tuple[Node, ...] = tuple(operands) for o in self.operands: if not isinstance(o, Node): raise HawkError(f"{type(self).__name__}: operand {o!r} is not a Node") if o.kind == REDUCE_KIND and any( getattr(c, "kind", "").startswith("loop") for c in self.operands): raise HawkError( "a cut inside a `for` is not in this release (it would be one " "launch per iteration); hoist hawk.sum(x) out of the " "loop") @property def binding(self) -> Slot | None: """The slot this node binds, or ``None`` if it binds none.""" return None @property def dedup_extra(self) -> Any: """The trailing component of the node's structural dedup key.""" return None def __repr__(self) -> str: # pragma: no cover - diagnostics only return f"<{type(self).__name__} {self.kind} {self.ttype}>" class Leaf(Node): """A read-side leaf, identified by ``(role, name)`` — the ONLY identity a slot is ever keyed on. ``quantity``/``wire_index`` record the compound quantity that MINTED this wire; a consumer never sets them by hand, :meth:`hawk.ir.compound.Quantity.read` does.""" __slots__ = ("kind", "role", "name", "quantity", "wire_index", "sorted") def __init__(self, kind: str, role: str, name: str, ttype: TensorType, *, quantity: Any = None, wire_index: int = 0, sorted: bool = False) -> None: super().__init__(ttype) if kind not in LEAF_KINDS: raise HawkError( f"leaf {name!r}: unknown leaf kind {kind!r}, expected one of " f"{LEAF_KINDS}" ) if role not in LEAF_ROLES: raise HawkError( f"leaf {name!r}: unknown leaf role {role!r}, expected one of " f"{LEAF_ROLES}" ) self.kind = kind self.role = role self.name = name self.quantity = quantity self.wire_index = wire_index #: DECLARED (``Sorted[Index]``): the plane is non-decreasing along the flat lane. #: A fact for :mod:`hawk.ir.lanes` only; never part of identity, digest or emit. self.sorted = sorted @property def binding(self) -> Slot: return Slot(self.role, self.name, self.ttype) class Const(Node): """A literal value: childless, but binds NO slot (the ``constant``).""" __slots__ = ("literal",) kind = "constant" def __init__(self, literal: Any, ttype: TensorType) -> None: super().__init__(ttype) self.literal = literal @property def dedup_extra(self) -> Any: return repr(self.literal) class RoleConst(Const): """A literal a DECLARATION named — an integer STRIDE, carrying its label. Same number a bare :class:`Const` would carry; the label adds PROVENANCE so a contraction recogniser (:mod:`hawk.ir.contraction`) can tell a declared stride from a coincidentally equal literal. It participates in the dedup key (two strides of equal value from different axes differ) and renders like any other integer literal.""" __slots__ = ("label",) def __init__(self, value: int, label: str, ttype: TensorType | None = None) -> None: super().__init__(int(value), ttype or TensorType((), "i32")) self.label = label @property def dedup_extra(self) -> Any: return (repr(self.literal), self.label) class SampleIndex(Node): """The body's own lane index, as an IR VALUE (:data:`SAMPLE_INDEX_KIND`). Childless and binds NO slot: the index the backend's ONE index prologue already binds for every subscript in the body. Exposing it lets a 2-D-domain kernel recover the coordinates a flattened launch folded together, without the author writing the ``/``/``%`` pair by hand. A TERMINAL for both transforms: an index is not a differentiable quantity, so it carries no derivative rule (like :class:`Const`).""" __slots__ = () kind = SAMPLE_INDEX_KIND def __init__(self, ttype: TensorType | None = None) -> None: super().__init__(ttype or TensorType((), "i32")) LANE_POSITION_KIND = "lane_position" class LanePosition(SampleIndex): """The launch position ``t`` of the lane: ``base + flat`` in a plain launch (where it is the sample index) and the slot of the map under an index set, where the sample index is ``map[t]``. A terminal like :class:`SampleIndex`, and read through it everywhere a launch position is a lane index.""" __slots__ = () kind = LANE_POSITION_KIND class Op(Node): """An interior operation of KIND ``kind`` over its operands.""" __slots__ = ("kind", "literal") def __init__(self, kind: str, operands: Sequence[Node], ttype: TensorType, literal: Any = None) -> None: super().__init__(ttype, operands) self.kind = kind self.literal = literal if kind in QUAT_OPS and ttype.shape == (4,) and ttype.tag != "quaternion": raise HawkError( f"op {kind!r} returns a 4-wide value typed {ttype!r}: a " "quaternion-producing op must carry tag='quaternion', because " "the tag participates in type identity and therefore in the walk's " "merge check" ) @property def dedup_extra(self) -> Any: return repr(self.literal) class At(Op): """The ``at(expr)``: an ABSOLUTE-index read of a ``lookup`` plane — the capability says must survive partitioning, and the one form :func:`hawk.ir.access.infer` reads as ``cross_sample_read``.""" __slots__ = () def __init__(self, plane: Leaf, index: Node, ttype: TensorType) -> None: if not isinstance(plane, Leaf) or plane.role not in AT_ROLES: raise HawkError( f"at(): the indexed plane must be a leaf in one of the roles " f"{AT_ROLES}, got {plane!r}" ) super().__init__("at", (plane, index), ttype) @property def plane(self) -> Leaf: return self.operands[0] # type: ignore[return-value] @property def index(self) -> Node: return self.operands[1] #: The label of the index a PARAMETER CELL carries: a ``uniform`` promoted to #: a one-cell ``lookup`` plane (:func:`hawk.diff.param_planes`) is read as #: ``At(plane, RoleConst(0, PARAM_CELL))``. The label tells the access #: classifier it is one broadcast value, not a cross-sample read. PARAM_CELL = "param_cell" def is_param_cell(node: Any) -> bool: """Whether ``node`` is the read of a parameter cell.""" return (isinstance(node, At) and isinstance(node.index, RoleConst) and node.index.label == PARAM_CELL) def param_cell_names(nodes: Any) -> tuple: """The names of the parameter cells read in ``nodes``, in first-seen order.""" out: dict = {} for node in nodes: if is_param_cell(node): out.setdefault(node.plane.name, None) return tuple(out) class Primitive(Op): """A custom primitive's BOUNDARY. Operand 0 is the forward subgraph, traced INLINE; the emitter renders it by rendering that expression, so the artifact contains no call. Operands 1.. are the declared INPUTS — the reason the node exists, since differentiating THROUGH the subgraph is what the supplied ``rules`` replaces (pushed to the inputs, never into operand 0, unless another part of the body shares it). A direction with no supplied rule refuses BY NAME rather than differentiating through silently.""" __slots__ = ("name", "rules", "input_count") def __init__(self, name: str, rules: Any, value: Node, inputs: Sequence[Node] = (), ttype: TensorType | None = None) -> None: inputs = tuple(inputs) super().__init__(PRIMITIVE_KIND, (value, *inputs), ttype if ttype is not None else value.ttype) self.name = name self.rules = rules self.input_count = len(inputs) @property def forward(self) -> Node: """The inlined forward subgraph this boundary stands in front of.""" return self.operands[0] @property def inputs(self) -> tuple: """The primitive's declared inputs, in call order.""" return self.operands[1:] @property def dedup_extra(self) -> Any: # NOT the rules object: its repr carries an address, and the kernel's # digest has to be byte-identical across processes. The NAME is the # identity, and the registry (hawk.ext) refuses two primitives sharing it. return (PRIMITIVE_KIND, self.name) class Select(Node): """``cond ? a: b`` — what the ``ast`` pass rewrites control flow into. A distinct family because it is the ONE op with a control shape.""" __slots__ = () kind = "select" def __init__(self, cond: Node, on_true: Node, on_false: Node, ttype: TensorType) -> None: refuse_factor_arms("select", ttype) super().__init__(ttype, (cond, on_true, on_false)) #: The op KIND of HAWK's finite per-element dispatch, control-shaped like #: :data:`Select.kind`. The ``ast`` pass mints one (policy ``switch``) only #: for an ``if``/``elif`` chain whose branches would otherwise all run; #: otherwise the author picks the POLICY. DISPATCH_KIND = "dispatch" #: Dispatch's lowerings: ``predicated``/``switch`` are per-node text #: choices the renderer makes (:mod:`hawk.emit.render`); ``segmented`` is a #: per-UNIT split the walk never sees. ``auto`` is the one value a #: launching runtime's chooser may resolve (to ``switch`` or #: ``segmented``); a declared policy is never overridden. Until resolved, #: ``auto`` renders as ``switch`` plus a count of the branch each sample #: picks into the group's ``<group>_mix`` plane (the chooser's input). DISPATCH_POLICIES = ("predicated", "switch", "segmented", "auto") #: The policies the renderer lowers as a ``switch`` statement. SWITCH_RENDERED = ("switch", "auto") class Dispatch(Node): """``branches[clamp(kind, 0)]`` — HAWK's finite per-element dispatch. Operand 0 is the SELECTOR (an i32 value, never compile-time); the rest are K >= 2 branches of ONE :class:`~hawk.types.TensorType` (tag included), type-checked on the constructor since no body mints this through :func:`hawk.ir.ops.make`. ``policy`` changes how the BACKEND renders the node, never what it MEANS, so it (with ``K``) is part of :attr:`dedup_extra`.""" __slots__ = ("policy",) kind = DISPATCH_KIND def __init__(self, selector: Node, branches: Sequence[Node], policy: str, ttype: TensorType) -> None: branches = tuple(branches) if len(branches) < 2: raise HawkError( f"dispatch: at least 2 branches are required, got " f"{len(branches)}" ) if policy not in DISPATCH_POLICIES: raise HawkError( f"dispatch: unknown policy {policy!r}; declared are " f"{DISPATCH_POLICIES}" ) if selector.ttype.shape != () or selector.ttype.dtype != "i32": raise HawkError( f"dispatch: kind must be a rank-0 i32 value, got " f"{selector.ttype!r} — a compile-time constant is not a " "traced VALUE and a policy is an authoring decision, not " "something the node infers" ) for b in branches[1:]: if b.ttype != branches[0].ttype: raise HawkError( f"dispatch: the branches disagree — {branches[0].ttype!r} vs " f"{b.ttype!r} (every branch shares ONE TensorType)" ) refuse_factor_arms("dispatch", ttype) super().__init__(ttype, (selector, *branches)) self.policy = policy @property def selector(self) -> Node: """The ``kind`` — the i32 value that PICKS a branch.""" return self.operands[0] @property def branches(self) -> tuple: """The K >= 2 candidate values, in declaration order.""" return self.operands[1:] @property def K(self) -> int: """The branch count — a compile-time fact of the traced call.""" return len(self.operands) - 1 @property def dedup_extra(self) -> Any: return (self.K, self.policy) class Sink(Node): """A write-side node: the walk's ROOT set. Binds a slot on ``(role, name)``, exactly like a leaf, and merges on the same key.""" __slots__ = ("kind", "role", "name") def __init__(self, kind: str, role: str, name: str, value: Node, index: Node | None = None, ttype: TensorType | None = None) -> None: operands = (value,) if index is None else (value, index) super().__init__(ttype if ttype is not None else value.ttype, operands) if role not in SINK_ROLES: raise HawkError( f"sink {name!r}: unknown sink role {role!r}, expected one of " f"{SINK_ROLES}" ) self.kind = kind self.role = role self.name = name @property def value(self) -> Node: return self.operands[0] @property def index(self) -> Node | None: """The ``scatter(expr)`` target lane, or ``None`` for the own column.""" return self.operands[1] if len(self.operands) > 1 else None @property def binding(self) -> Slot: return Slot(self.role, self.name, self.ttype) @property def dedup_extra(self) -> Any: return (self.role, self.name) class Assign(Sink): """``assign(mutable)``: the own-column write of a mutable plane.""" __slots__ = () def __init__(self, name: str, value: Node, ttype: TensorType | None = None) -> None: super().__init__("assign", "mutable", name, value, None, ttype) class WideWrite(Sink): """``wide_write(wide_out)``; an ``index`` makes it a scatter.""" __slots__ = () def __init__(self, name: str, value: Node, index: Node | None = None, ttype: TensorType | None = None) -> None: super().__init__("wide_write", "wide_out", name, value, index, ttype) #: The sum contracts an accumulate commits under (``policy=``): ``auto`` (the #: target's default), ``compensated`` (a companion plane ``<name>_c`` carries the #: correction) and ``ordered`` (bitwise repeatable). POLICIES = ("auto", "compensated", "ordered") #: What a keyed ``hawk.scatter`` takes: the above, ``plain`` (the target's #: atomic add alone, no companion) and ``or`` (``|=`` into an integer plane: #: the target's atomic OR, no sum contract). SCATTER_POLICIES = (*POLICIES, "plain", "or") #: The policy a keyed ``hawk.scatter`` under ``auto`` is traced with: compensated #: on the targets that lower it (:func:`compensates`), the target's own serial #: add on the C target, which lowers no companion. KEYED_AUTO = "keyed_auto" def compensates(policy: str) -> bool: """Whether an accumulate under ``policy`` commits through a companion plane.""" return policy in ("compensated", KEYED_AUTO) #: The suffix of a compensated accumulate plane's companion. COMPANION_SUFFIX = "_c" #: The prefix of a vjp's adjoint plane (``bar_<plane>``). ADJOINT_PREFIX = "bar" class AccumWrite(Sink): """``accum_write(accum_out)`` — the cross-sample accumulate plane; an ``index`` other than the own column is a scatter. ``policy`` is the sum contract (:data:`POLICIES`).""" __slots__ = ("policy",) def __init__(self, name: str, value: Node, index: Node | None = None, ttype: TensorType | None = None, policy: str = "auto") -> None: super().__init__("accum_write", "accum_out", name, value, index, ttype) if policy not in (*SCATTER_POLICIES, KEYED_AUTO): raise HawkError( f"accum_write {name!r}: policy {policy!r} is not one of {SCATTER_POLICIES}") self.policy = policy @property def dedup_extra(self) -> Any: return (self.role, self.name) if self.policy in ("auto", "plain") else ( self.role, self.name, self.policy) def rebuilt(self, value: Node) -> AccumWrite: """The same accumulate over ``value``.""" return AccumWrite(self.name, value, self.index, self.ttype, self.policy) #: The suffix of a ``sample_sum`` plane's compensation companion. SAMPLE_SUM_COMP = "_comp" class ReduceSamples(Node): """``reduce(op, samples)``: the sum, max or min of a rank-0 floating value over every sample (a vector or matrix: one cell per component, row-major). A trace-time node only: storing it to a Mutable makes the :class:`SampleSum` sink(s), and nothing else may read it.""" __slots__ = ("op", "key", "count", "policy", "kept") kind = REDUCE_KIND def __init__(self, op: str, value: Node, *, key: Node | None = None, count: Node | None = None, policy: str = "auto", kept: tuple = ()) -> None: if op not in CELL_OPS: raise HawkError(f"reduce over samples: op {op!r} is not one of {CELL_OPS}") if value.ttype.dtype not in ("f64", "f32") or len(value.ttype.shape) > 2: raise HawkError( f"reduce over samples: an f64/f32 scalar, vector or matrix is " f"reduced, got {value.ttype!r}") if key is not None and op != "sum": raise HawkError(f"reduce over some lane axes: op {op!r} is not built, only sum") if policy not in POLICIES: raise HawkError(f"reduce over samples: policy {policy!r} is not one of {POLICIES}") super().__init__(value.ttype, (value,)) self.op = op #: Over SOME lane axes: the flat index of the kept coordinates (``None``: #: every axis, one cell) and the number of kept coordinates, the #: component stride of a vector or matrix payload. self.key = key self.count = count self.policy = policy #: The kept lane axes ``((label, extent|None), ...)`` of a keyed reduction. self.kept = kept @property def dedup_extra(self) -> Any: return self.op def rebuilt(self, value: Node) -> ReduceSamples: """The same reduction (key, count, policy) over ``value``.""" return ReduceSamples(self.op, value, key=self.key, count=self.count, policy=self.policy, kept=self.kept) def recount(self, count: Node) -> ReduceSamples: """The same keyed reduction over ``count`` kept coordinates.""" return ReduceSamples(self.op, self.operands[0], key=self.key, count=count, policy=self.policy, kept=self.kept) def stored(self, name: str, ttype: TensorType | None = None) -> tuple: """The cell sinks that store this reduction to the plane ``name``: one for a scalar, one per component (planes ``name_0``, ``name_1``, ...) for a vector or matrix.""" value = self.operands[0] shape = value.ttype.shape if self.key is not None: return self._keyed(name) if not shape: return (SampleSum(name, value, ttype, self.op),) scalar = TensorType((), value.ttype.dtype) index = [(r,) if len(shape) == 1 else (r, c) for r in range(shape[0]) for c in range(shape[-1] if len(shape) == 2 else 1)] width = len(index) return tuple(SampleSum(f"{name}_{k}", Op("component", (value,), scalar, ix if len(shape) == 2 else ix[0]), scalar, self.op, cell=name, columns=width) for k, ix in enumerate(index)) def _keyed(self, name: str) -> tuple: """The accumulates of a reduction over some lane axes: entry ``k`` of the payload (row-major) adds into ``name`` at ``key + k * count``, so the plane is ``(*payload, *kept)``, payload first.""" value = self.operands[0] shape = value.ttype.shape scalar = TensorType((), value.ttype.dtype) if not shape: return (AccumWrite(name, value, self.key, scalar, self.policy),) index = [(r,) if len(shape) == 1 else (r, c) for r in range(shape[0]) for c in range(shape[-1] if len(shape) == 2 else 1)] from .ops import make out = [] for k, ix in enumerate(index): entry = Op("component", (value,), scalar, ix if len(shape) == 2 else ix[0]) at = self.key if k == 0 else make("add", (self.key, make( "mul", (Const(k, self.count.ttype), self.count)))) out.append(AccumWrite(name, entry, at, scalar, self.policy)) return tuple(out) class SampleSum(Sink): """``sample_sum(accum_out)``: the sum (or max, or min) of a rank-0 floating ``value`` over each row of :data:`ROW_WIDTH` consecutive samples. The plane ``name`` holds one result per row (``ceil(n/ROW_WIDTH)`` of them); a sum also writes its compensation term to ``name + "_comp"``. A row's result is a fixed pairwise tree over its slots (a slot past ``n`` holds the identity: 0, -inf, +inf), the same on every target.""" __slots__ = ("op", "cell", "columns") def __init__(self, name: str, value: Node, ttype: TensorType | None = None, op: str = "sum", cell: str | None = None, columns: int = 1) -> None: super().__init__("sample_sum", "accum_out", name, value, None, ttype) self.cell = cell or name self.columns = columns if op not in CELL_OPS: raise HawkError(f"sample_sum {name!r}: op {op!r} is not one of {CELL_OPS}") self.op = op if self.ttype.shape or self.ttype.dtype not in ("f64", "f32"): raise HawkError( f"sample_sum {name!r}: sums a rank-0 f64/f32 value, got {self.ttype!r}") @property def dedup_extra(self) -> Any: return (self.role, self.name, self.op, self.cell, self.columns) @property def companion(self) -> Slot: """The compensation plane's slot (a sum only).""" return Slot(self.role, self.name + SAMPLE_SUM_COMP, self.ttype) def rebuilt(self, value: Node) -> SampleSum: """The same sink over ``value``.""" return SampleSum(self.name, value, self.ttype, self.op, self.cell, self.columns) def sample_sums(sinks: Sequence[Node]) -> tuple: """The :class:`SampleSum` sinks of ``sinks``, in order: column ``k`` of the kernel's row of terms is the ``k``-th.""" return tuple(s for s in sinks if isinstance(s, SampleSum)) class TapeWrite(Sink): """``tape[row, :] = (v_0, v_1, ...)`` on the OWN column, if ``on``. The taping twin's store (:func:`hawk.diff.tape`): one dynamic ROW of a per-sample ``(W*w,)`` plane (sample-last, so the memory is ``[W, w, N]``) gets the flattened components of ``values`` in order, predicated on the rank-0 integer ``on`` (the per-launch ``tape_on`` word). Own column only — no other sample's memory is touched, so the access class stays ``sample_local`` (:attr:`index` is ``None``). Operands are ``(row, on, *values)``; :attr:`value` is the first stored value.""" __slots__ = ("width",) def __init__(self, name: str, row: Node, on: Node, values: Sequence[Node], ttype: TensorType) -> None: values = tuple(values) Node.__init__(self, ttype, (row, on, *values)) width = sum(element_count(v.ttype) for v in values) if (len(ttype.shape) != 1 or not values or ttype.shape[0] % width or row.ttype != TensorType((), "i32")): raise HawkError( f"tape_write {name!r}: a {ttype!r} plane takes whole rows of the " f"{width} stored components at an i32 row") self.kind = "tape_write" self.role = "out" self.name = name self.width = width @property def value(self) -> Node: return self.operands[2] @property def index(self) -> Node | None: return None @property def row(self) -> Node: return self.operands[0] @property def on(self) -> Node: return self.operands[1] @property def values(self) -> tuple: return self.operands[2:] @property def dedup_extra(self) -> Any: return (self.role, self.name, self.width) def element_count(ttype: TensorType) -> int: """How many scalars one value of ``ttype`` holds.""" out = 1 for extent in ttype.shape: out *= int(extent) return out #: The reserved ``lookup`` plane a finishing kernel counts its newly finished #: samples into: ONE ``uint32`` cell. eagle's ``eagle.FINISHED_PLANE`` #: names the same plane. FINISHED_PLANE = "finished_count" FINISHED_TTYPE = TensorType((), "uint32") #: The reserved ``lookup`` plane a ``steps="auto"`` kernel reads its trip #: count from (:func:`hawk.steps`): set on the device by the runner, never #: by the host; eagle's ``eagle.FUSED_STEPS_PLANE`` names the same plane. FUSED_STEPS_PLANE = "fused_steps" class Finish(Sink): """``terminated = cond``: the kernel FINISHES its own sample. Set-only and monotone: the mask becomes ``mask | cond``, and a newly finished sample adds one to :data:`FINISHED_PLANE`. It binds the mask's ``(terminated, name)`` slot, merged by the walk with the declared leaf, rendered LAST. ``steps`` is the launch's step count (:func:`hawk.steps`): 1 for authored, ``"auto"`` when read from :data:`FUSED_STEPS_PLANE`. Role ``terminated`` is NOT in :data:`SINK_ROLES`: this is the guard seam's own mask.""" __slots__ = ("steps",) def __init__(self, name: str, value: Node, steps: int = 1) -> None: Node.__init__(self, TensorType((), "bool"), (value,)) if value.ttype != self.ttype: raise HawkError( f"finish {name!r}: the condition is typed {value.ttype!r}; a " "sample finishes on a traced rank-0 bool") self.kind = "finish" self.role = "terminated" self.name = name self.steps = steps if steps == "auto" else int(steps) @property def dedup_extra(self) -> Any: return (self.role, self.name, self.steps)