Wire the capture/replay lifecycle into the driven-loop step boundary — the other half of Path A (hand-fused kernels behind the registry + piecewise cudagraphs, no compiler). Models and tests the correctness-critical control logic; replay re-runs the current step thunk (CPU models the lifecycle, not the GPU speedup). - GraphCapturer on the instance (cross-request cache), wired into RuntimeLoopContext.execute and gated by LoopSpec.graph_capture == "breakable_cudagraph". Capture key = (device, arch, loop, shape_sig, resident-weight-versions, graph_key). Eager-break for non-capturable (SDE) and interceptor-overridden steps. Never executes a stored thunk, so interleaved requests can't smear state. - WorkPlan.capturable / graph_key. wan21 sets capturable=not sde and folds compute dtype (shape_sig.dtype) + CFG branch set + expert + scheduler-precision into the key — closing a cross-precision key-collision corruption path a non-fp32 build would otherwise hit on a real GPU (audit finding). - Version-in-key auto-invalidation + real eviction: set_weights_version evicts the synced component's graphs (duck-typed, so card/ imports no runtime), preventing a GPU graph leak across FlowGRPO's per-iteration syncs. - register_kernel gains the workspace_bytes capture-safety contract (declared in the matrix; cuda cells declare real scratch, numpy reference is 0). - 11 tests: capture-once/replay-many, eager-break (SDE + override), capture ≡ pure-eager bit-identical, recapture on shape + weight-version change with eviction, eager-loop gating, accel-backend capture, and capturer unit tests (key discrimination, eager-break, workspace-collision safety net, eviction). Honestly deferred (bite a real GPU, not the CPU tests): the static-buffer refactor of the step body, admission budgeting of capture cost (GRAPH_CAPTURE), and per-card opt-in beyond wan21 — all documented in cudagraph.py + README. 195 tests pass.
137 lines
7.1 KiB
Python
137 lines
7.1 KiB
Python
"""The two tuple-keyed backend registries — the dispatch membrane (design_v3 §17; multi-backend RFC).
|
|
|
|
Everything device/arch-specific resolves through one of two registries, keyed by a tuple:
|
|
|
|
* ``COMPONENTS`` — keyed ``(kind, device, variant)``. Resolves which *weight-bearing* component
|
|
implementation to materialize (the toy numpy module, a torch adapter on a GPU box, …). This is
|
|
the generalization of ``ComponentSpec.factory``: the card's ``factory`` is simply the terminal
|
|
``(kind, "cpu", "numpy")`` rung that every other backend falls back to.
|
|
* ``KERNELS`` — keyed ``(op, device, arch, variant)``. Resolves which *stateless primitive* kernel
|
|
implements an op (the flow-match solver step, an SDE step, a gemm/norm on a real box). Arch is a
|
|
fallback axis (sm100→sm90→…→generic), with the numpy reference as the terminal rung.
|
|
|
|
Two properties this module guarantees (the kernel-colocation answer made concrete):
|
|
|
|
1. **Location is decoupled from dispatch.** A registration carries a key, not a path. Where the .cu
|
|
(or .py) lives on disk — unified ``fastvideo-kernel/`` or co-located with a model — is invisible
|
|
here; both call the same ``register_*``.
|
|
2. **The matrix is enumerable without a GPU and without importing torch.** ``manifest()`` lists
|
|
every declared cell — including ones whose backend is *unavailable* here (e.g. a torch/cuda cell
|
|
on a CPU box, gated by a ``find_spec``-based ``available`` predicate that never imports torch) —
|
|
so the parity/fallback matrix can be dumped on a laptop. ``resolve`` skips unavailable cells;
|
|
``manifest`` lists them.
|
|
|
|
Scope caveat (honest): in this in-tree mini the cells are populated by a fixed backend import
|
|
list (``Platform.ensure_backends_loaded`` imports ``backends/{cpu,accel,torch_cuda}``), not by
|
|
setuptools entry points. So the matrix is complete for the backends in that list, but an
|
|
*out-of-tree* or *model-co-located* backend would not self-enumerate until added to it — full
|
|
entry-point discovery is the real-package mechanism and is deliberately not wired here.
|
|
|
|
This is a pure leaf module: stdlib only. The actual backend registrations live in ``backends/`` and
|
|
are imported lazily (``Platform.ensure_backends_loaded``) to keep this import-cycle-free.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Callable, Iterable
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Op names — the stable identifiers a loop dispatches on (not free functions). #
|
|
# --------------------------------------------------------------------------- #
|
|
FLOW_MATCH_STEP = "flow_match_step" # deterministic flow-match Euler solver step (ODE serve)
|
|
FLOW_SDE_STEP = "flow_sde_step" # FlowGRPO stochastic step + log-prob (RL rollout)
|
|
|
|
|
|
def _always_available() -> bool:
|
|
return True
|
|
|
|
|
|
@dataclass
|
|
class Registration:
|
|
"""One cell in a registry: a key, the implementation, and an availability predicate.
|
|
|
|
``available`` lets a cell be *declared* (so it shows up in ``manifest()``) while being
|
|
*unresolvable* in this environment — e.g. a torch/cuda kernel on a CPU box declares
|
|
``available=lambda: torch.cuda.is_available()`` and is listed-but-skipped here.
|
|
"""
|
|
key: tuple
|
|
fn: Callable[..., Any]
|
|
available: Callable[[], bool] = _always_available
|
|
source: str = ""
|
|
meta: dict[str, Any] = field(default_factory=dict)
|
|
|
|
def is_available(self) -> bool:
|
|
try:
|
|
return bool(self.available())
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
class TupleRegistry:
|
|
"""A registry keyed by an ordered field tuple, with availability-aware resolution."""
|
|
|
|
def __init__(self, name: str, key_fields: tuple[str, ...]):
|
|
self.name = name
|
|
self.key_fields = key_fields
|
|
self._reg: dict[tuple, Registration] = {}
|
|
|
|
def put(self, key: tuple, fn: Callable[..., Any], *,
|
|
available: Callable[[], bool] | None = None, source: str = "",
|
|
meta: dict[str, Any] | None = None) -> None:
|
|
if len(key) != len(self.key_fields):
|
|
raise ValueError(f"{self.name}: key {key} does not match fields {self.key_fields}")
|
|
self._reg[key] = Registration(key=key, fn=fn,
|
|
available=available or _always_available,
|
|
source=source, meta=dict(meta or {}))
|
|
|
|
def lookup(self, key: tuple) -> Registration | None:
|
|
"""Exact lookup, ignoring availability (used by ``manifest``/diagnostics)."""
|
|
return self._reg.get(key)
|
|
|
|
def resolve_first(self, candidate_keys: Iterable[tuple]) -> Registration | None:
|
|
"""First candidate key that is both registered AND available wins (the fallback chain)."""
|
|
for key in candidate_keys:
|
|
reg = self._reg.get(key)
|
|
if reg is not None and reg.is_available():
|
|
return reg
|
|
return None
|
|
|
|
def manifest(self) -> list[dict[str, Any]]:
|
|
"""Every declared cell as a flat dict — enumerable without importing/calling the kernels.
|
|
|
|
This is what makes the parity matrix dumpable on a CPU box: it lists torch/cuda cells too,
|
|
each with ``available=False`` here, so coverage is never silently truncated to 'what imported'.
|
|
"""
|
|
rows: list[dict[str, Any]] = []
|
|
for key, reg in sorted(self._reg.items(), key=lambda kv: tuple(map(str, kv[0]))):
|
|
row = dict(zip(self.key_fields, key))
|
|
row["available"] = reg.is_available()
|
|
row["source"] = reg.source
|
|
row.update(reg.meta) # surface declared metadata (e.g. workspace_bytes)
|
|
rows.append(row)
|
|
return rows
|
|
|
|
|
|
# The two registries (design_v3 §17). Populated by ``backends/`` modules (lazily imported).
|
|
COMPONENTS = TupleRegistry("components", ("kind", "device", "variant"))
|
|
KERNELS = TupleRegistry("kernels", ("op", "device", "arch", "variant"))
|
|
|
|
|
|
def register_component(kind: str, fn: Callable[..., Any], *, device: str, variant: str = "default",
|
|
available: Callable[[], bool] | None = None, source: str = "") -> None:
|
|
"""Register a component builder. ``fn(spec, instance, platform) -> live component``."""
|
|
COMPONENTS.put((kind, device, variant), fn, available=available, source=source)
|
|
|
|
|
|
def register_kernel(op: str, fn: Callable[..., Any], *, device: str, arch: str, variant: str = "default",
|
|
available: Callable[[], bool] | None = None, source: str = "",
|
|
workspace_bytes: int = 0) -> None:
|
|
"""Register a stateless kernel. ``fn(*args, **kwargs)`` — same signature as its numpy reference.
|
|
|
|
``workspace_bytes`` is the kernel's static scratch requirement — the capture-safety contract
|
|
(design_v3 §6.2): a kernel used inside a captured CUDA graph must draw its workspace from the
|
|
pool-provided static buffer, not malloc per call, or replay corrupts. Declared here so the
|
|
requirement is enumerable in the matrix; the numpy reference needs none (0)."""
|
|
KERNELS.put((op, device, arch, variant), fn, available=available, source=source,
|
|
meta={"workspace_bytes": int(workspace_bytes)})
|