# Copyright 2026 Alessandro Masat
# SPDX-License-Identifier: Apache-2.0
"""``hawk.diff.param_planes(kernel)`` — a kernel whose ``uniform`` parameters
are read from one-cell parameter planes.
A ``uniform`` is a by-value argument: changing it between two launches of a
recorded graph means re-capturing the launch. The same value held in a
one-cell ``lookup`` plane is changed with a memcpy, and the capture stays
valid. This is the same promotion the time step gets in
:func:`hawk.diff.tape`: one traced body, no second compile of the same text.
The plane keeps the uniform's own name (``k`` stays ``k``), so ``rebind(k=...)``
reads naturally. The sidecar lists the promoted planes under ``param_planes``;
a launching runtime binds each to a one-element array.
:func:`hawk.diff.vjp` of the promoted kernel gives the gradient with respect
to each floating-point promoted parameter as one sum per 32 samples
(``bar_<name>`` plus its compensation ``bar_<name>_comp``, ``ceil(n/32)``
values each, :class:`hawk.ir.nodes.SampleSum`), never a scatter into the cell,
so the gradient is the same bits on every launch. The plain kernel is never
changed.
A uniform read inside a ``for`` is promoted the same way: the loop's body reads
it as a loop-invariant boundary value, so the cell is read once before the loop.
"""
from __future__ import annotations
from typing import Any
from ..ir import HawkError, Leaf
from ..ir.nodes import PARAM_CELL, At, RoleConst, SampleSum, is_param_cell
from ..ir.walk import substitute
from ..types import TensorType
from .tape import TAPE_ON, _reachable, _resink
#: The promoted kernel's name suffix, and the one a per-cell kernel adds to it.
SUFFIX = "_params"
CELLS_SUFFIX = "_cells"
#: The per-sample ``i32`` plane that holds each sample's parameter cell.
CELL_PLANE = "param_cell"
_INDEX = TensorType((), "i32")
def param_cell(name: str, ttype: TensorType, per_cell: bool = False) -> At:
"""The read of parameter ``name``: cell 0 of a one-cell ``lookup`` plane,
or, with ``per_cell``, the cell the sample's ``param_cell`` plane names."""
plane = Leaf("table_read", "lookup", name, ttype)
if per_cell:
return At(plane, Leaf("vocab_read", "per_sample", CELL_PLANE, _INDEX), ttype)
return At(plane, RoleConst(0, PARAM_CELL, _INDEX), ttype)
[docs]
def param_planes(kernel: Any, names: Any = None, per_cell: bool = False) -> Any:
"""``kernel`` with its ``uniform`` parameters read from one-cell planes.
``names`` selects which uniforms (default: every one the body reads, the
taping flag aside); an unknown name, or a plane of that name that is not
the uniform, is refused. The result is named ``<name>_params``. A default
automatic kernel is promoted through its single step and rebuilt.
With ``per_cell=True`` a sample reads cell ``param_cell[i]`` of each plane
(an ``i32`` per-sample plane) instead of cell 0, so one launch can run many
parameter values; the result is named ``<name>_params_cells``, and ``vjp``
gives the gradient of each cell, summed over the samples that read it. A
kernel with reductions of its own, or a ``jump=`` plan, is refused."""
from ..trace.kernel import Kernel
from ..trace.steps import default_steps
from ..trace.steps import default_step
step = default_step(kernel)
step = getattr(step, "one_step", None) or step
nodes = _reachable(step.sinks)
leaves = [n for n in nodes if isinstance(n, Leaf) and n.role == "uniform"
and n.name != TAPE_ON]
have = {n.name for n in leaves}
wanted = sorted(have) if names is None else list(names)
unknown = [n for n in wanted if n not in have]
if unknown:
raise HawkError(
f"hawk.diff.param_planes({step.name!r}): {unknown} are not uniform "
f"parameters this kernel reads; its uniforms are {sorted(have)}")
clash = [n for role, n in step.walk.arg_spec if n in wanted and role != "uniform"]
if clash:
raise HawkError(
f"hawk.diff.param_planes({step.name!r}): a plane named {clash} "
"already exists; the parameter plane takes the uniform's own name, "
"so rename one of them")
if not wanted:
raise HawkError(
f"hawk.diff.param_planes({step.name!r}): the kernel reads no uniform "
"parameter, so there is nothing to promote")
if per_cell:
if any(isinstance(n, SampleSum) for n in nodes):
raise HawkError(
f"hawk.diff.param_planes({step.name!r}, per_cell=True): the kernel "
"already reduces over samples into cells of its own, and a "
"parameter cell index would be a second use of the word. Promote "
"its parameters with per_cell=False, or move the reduction out")
if step.jump is not None:
raise HawkError(
f"hawk.diff.param_planes({step.name!r}, per_cell=True): the kernel "
"has a `jump=` plan, whose switching comparisons are written per "
"launch, not per parameter cell. Promote with per_cell=False")
if CELL_PLANE in {n for _, n in step.walk.arg_spec}:
raise HawkError(
f"hawk.diff.param_planes({step.name!r}, per_cell=True): a plane "
f"named {CELL_PLANE!r} already exists; the cell index takes that "
"name, so rename the plane")
cells = {n: param_cell(n, next(l.ttype for l in leaves if l.name == n), per_cell)
for n in wanted}
mapping = {id(l): cells[l.name] for l in leaves if l.name in cells}
sinks = swap(step.sinks, mapping)
out = Kernel(step.name + SUFFIX + (CELLS_SUFFIX if per_cell else ""), sinks,
step.planes, kind=step.kind)
out.time, out.time_step = step.time, step.time_step
out.jump = step.jump
return default_steps(out) if step is not kernel else out
def demote(sinks: Any) -> tuple:
"""``sinks`` with every parameter cell read as a per-sample plane of the
same name — the form whose reverse yields a per-sample gradient."""
nodes = _reachable(sinks)
mapping = {id(n): Leaf("vocab_read", "per_sample", n.plane.name, n.ttype,
sorted=n.plane.sorted)
for n in nodes if is_param_cell(n)}
return tuple(sinks) if not mapping else swap(sinks, mapping)
def promote(sinks: Any, names: Any) -> tuple:
"""``sinks`` with every per-sample read of a ``names`` plane turned back
into a parameter-cell read."""
nodes = _reachable(sinks)
mapping = {id(n): param_cell(n.name, n.ttype) for n in nodes
if isinstance(n, Leaf) and n.role == "per_sample" and n.name in names}
return swap(sinks, mapping)
def swap(sinks: Any, mapping: dict) -> tuple:
"""``sinks`` with the nodes in ``mapping`` (by ``id``) replaced."""
if mapping and any(not hasattr(s, "value") for s in sinks):
raise HawkError(
"a kernel that reads a parameter cell cannot also be differentiated "
"through a `for` that commits a gradient per iteration (a plane "
"read at a runtime index inside the loop): the per-sample "
"accumulate that feeds the per-32-sample sum of the parameter is "
"not built. Differentiate the kernel with the parameter passed as a "
"plain `Param` (not through param_planes), or read the plane outside "
"the loop")
values = substitute(tuple(s.value for s in sinks), mapping)
return tuple(_resink(s, v) for s, v in zip(sinks, values))