Files
hao-ai-lab-FastVideo/v2/platform/platform.py
T
SolitaryThinker 2f044c02dd [feat] multi-backend dispatch substrate (device/arch/kernel registries)
Add v2/platform/: the (recipe, runtime) backend membrane that lets CPU, GPU,
and other devices coexist behind one dispatch substrate.

- Two tuple-keyed registries: COMPONENTS(kind, device, variant) for
  weight-bearing components and KERNELS(op, device, arch, variant) for
  stateless primitives, each with an availability predicate and an enumerable
  manifest (declared-but-unavailable cells listed without importing torch).
- Platform: detected (device, arch) owning the device + arch fallback chains
  and a per-platform cached KernelTable; detect() is honest (CPU/numpy unless
  torch+CUDA are actually present). Arch fallback is monotonic — only degrades
  to older/portable archs, never a newer binary-incompatible one.
- Seams wired: ModelInstance.component() -> platform.build_component(spec,self)
  with spec.factory as the numpy terminal rung (existing cards untouched); the
  wan21 denoise thunk dispatches solver ops through model.platform.kernels.
- Three backends: cpu (numpy terminal + parity oracle), accel (pure-python
  stand-in proving cross-device resolution, arch fallback, and the oracle),
  torch_cuda (declared-but-unavailable; no faked GPU).
- test_platform.py: 16 tests — detection, terminal rung, arch-fallback walk +
  monotonicity, device precedence, variant fallback, component override +
  per-kind device fallback, the parity oracle (accel == cpu, bit_identical via
  the C1 ladder), and matrix enumeration without importing torch.

Scope is honestly bounded in the README/docstrings: only wan21-denoise routes
through the kernel table and only the dit kind is overridden today (each a
one-line adoption); the torch/CUDA path and a cudagraph workspace-safety
contract are declared/deferred, not implemented. 183 tests pass.
2026-07-04 17:15:34 +00:00

197 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Platform — the detected ``(device, arch)`` that resolves both registries (design_v3 §17).
One ``Platform`` per pool. It owns:
* the **device fallback chain** (e.g. ``accel → cpu``) — so a component/kernel a backend hasn't
implemented falls back to the numpy reference (the terminal rung + parity oracle);
* the **arch fallback chain** per device (e.g. cuda ``sm100→sm90→sm80→ptx→generic``) — so a kernel
built for an older arch still resolves on a newer card;
* ``build_component(spec, instance)`` — the single materialization seam (replaces a bare
``spec.factory`` call); and
* ``kernels`` — a per-platform ``KernelTable`` that resolves an op once and caches the callable.
``Platform.detect()`` is honest: it returns a CPU/numpy platform unless torch+CUDA are actually
present (they are not in this environment). The (device, arch) machinery is fully exercised on the
CPU box by the pure-python ``accel`` stand-in backend — proving the dispatch is generic without
faking a GPU.
"""
from __future__ import annotations
from typing import Any, Iterator
from .registry import COMPONENTS, KERNELS
_BACKENDS_LOADED = False
def ensure_backends_loaded() -> None:
"""Import the backend modules once so they self-register. Lazy → import-cycle-free."""
global _BACKENDS_LOADED
if _BACKENDS_LOADED:
return
_BACKENDS_LOADED = True
# imported for side effects (each module calls register_component/register_kernel at import)
from .backends import accel, cpu, torch_cuda # noqa: F401
# Arch fallback chains (newest → oldest → portable terminal). Resolution walks these in order.
_CUDA_ARCH_FALLBACK = ("sm100", "sm90", "sm80", "ptx", "generic")
_ACCEL_ARCH_FALLBACK = ("sm90", "sm80", "generic")
_PORTABLE_ARCHS = ("ptx", "generic") # vendor-neutral terminals — valid on any device
def _arch_rank(arch: str) -> int:
"""``smXY`` → integer ``XY`` for comparison; portable terminals (ptx/generic/unknown) → -1."""
if arch.startswith("sm") and arch[2:].isdigit():
return int(arch[2:])
return -1
def _suffix_from(chain: tuple[str, ...], arch: str) -> tuple[str, ...]:
"""The arch fallback tail for ``arch``: itself, then ONLY older real archs from ``chain``, then
portable terminals. Never falls back to a *newer* arch — a kernel built for sm90 is binary-
incompatible on an sm70 device, so resolution must only ever degrade to older/more-portable code.
(A newer device, e.g. sm120, may still run older-arch kernels — those are kept.)
"""
rank = _arch_rank(arch)
tail: list[str] = [arch]
for a in chain:
if a in tail:
continue
ar = _arch_rank(a)
if ar < 0: # portable terminal (ptx/generic): always valid
tail.append(a)
elif rank >= 0 and ar < rank: # strictly-older real arch: a safe fallback
tail.append(a)
# else: newer-or-equal real arch → skip (would be binary-incompatible on this device)
return tuple(tail)
class KernelTable:
"""A platform's resolved op→callable map. Resolves each op once (over the fallback chain),
then caches — so the hot loop pays the registry walk at most once per (op, variant)."""
def __init__(self, platform: "Platform"):
self.platform = platform
self._cache: dict[tuple[str, str], Any] = {}
def get(self, op: str, variant: str = "default") -> Any:
ck = (op, variant)
if ck not in self._cache:
reg = self.platform.resolve_kernel(op, variant)
if reg is None:
raise KeyError(
f"no kernel for op={op!r} variant={variant!r} on device chain "
f"{self.platform.device_chain} (and no numpy terminal registered)")
self._cache[ck] = reg.fn
return self._cache[ck]
def resolved_source(self, op: str, variant: str = "default") -> str:
"""Which registered cell an op resolves to — used by parity/diagnostics tests."""
reg = self.platform.resolve_kernel(op, variant)
return "" if reg is None else f"{reg.key}@{reg.source}"
class Platform:
def __init__(self, device: str, arch: str, device_chain: tuple[str, ...],
arch_chains: dict[str, tuple[str, ...]]):
self.device = device
self.arch = arch
self.device_chain = tuple(device_chain)
self.arch_chains = dict(arch_chains)
self._kernels: KernelTable | None = None
# --- the kernel table (lazy, cached) ------------------------------------- #
@property
def kernels(self) -> KernelTable:
if self._kernels is None:
ensure_backends_loaded()
self._kernels = KernelTable(self)
return self._kernels
def arch_chain(self, device: str) -> tuple[str, ...]:
return self.arch_chains.get(device, ("generic",))
# --- candidate-key generators (the fallback chains, in priority order) --- #
def _component_keys(self, kind: str, variant: str) -> Iterator[tuple]:
for dev in self.device_chain:
yield (kind, dev, variant)
if variant != "default":
yield (kind, dev, "default")
def _kernel_keys(self, op: str, variant: str) -> Iterator[tuple]:
for dev in self.device_chain:
for arch in self.arch_chain(dev):
yield (op, dev, arch, variant)
if variant != "default":
yield (op, dev, arch, "default")
# --- the two resolution seams -------------------------------------------- #
def build_component(self, spec: Any, instance: Any, variant: str = "default") -> Any:
"""Materialize a component. Registry first (device chain), then ``spec.factory`` as the
terminal numpy rung — so existing cards (factory-only) are unchanged, while a GPU/accel
backend overrides by registering ``(kind, device, …)``."""
ensure_backends_loaded()
reg = COMPONENTS.resolve_first(self._component_keys(spec.kind, variant))
if reg is not None:
return reg.fn(spec, instance, self)
if getattr(spec, "factory", None) is not None:
return spec.factory(instance) # the (kind, "cpu", "numpy") terminal rung
raise RuntimeError(
f"component kind={spec.kind!r} ({getattr(spec, 'component_id', '?')!r}) has no impl on "
f"device chain {self.device_chain} and no factory terminal")
def resolve_kernel(self, op: str, variant: str = "default"):
ensure_backends_loaded()
return KERNELS.resolve_first(self._kernel_keys(op, variant))
# --- constructors -------------------------------------------------------- #
@classmethod
def cpu(cls) -> "Platform":
return cls("cpu", "numpy", ("cpu",), {"cpu": ("numpy",)})
@classmethod
def accel(cls, arch: str = "sm90") -> "Platform":
"""A pure-python stand-in accelerator (CPU-resident) used to prove cross-device dispatch,
arch fallback, and the parity oracle without a real GPU."""
return cls("accel", arch, ("accel", "cpu"),
{"accel": _suffix_from(_ACCEL_ARCH_FALLBACK, arch), "cpu": ("numpy",)})
@classmethod
def cuda(cls, arch: str = "sm90") -> "Platform":
"""A real CUDA platform (resolves torch adapters + fastvideo-kernel cells). Only usable on a
box where those cells are ``available``; declared here so detection has a target."""
return cls("cuda", arch, ("cuda", "cpu"),
{"cuda": _suffix_from(_CUDA_ARCH_FALLBACK, arch), "cpu": ("numpy",)})
@classmethod
def detect(cls) -> "Platform":
"""Honest detection: CUDA iff torch+CUDA are actually importable, else CPU/numpy."""
ensure_backends_loaded()
import importlib.util
if importlib.util.find_spec("torch") is not None:
try:
import torch # type: ignore
if torch.cuda.is_available():
major, minor = torch.cuda.get_device_capability()
return cls.cuda(f"sm{major}{minor}")
except Exception:
pass
return cls.cpu()
def __repr__(self) -> str:
return f"Platform(device={self.device!r}, arch={self.arch!r}, chain={self.device_chain})"
# --------------------------------------------------------------------------- #
# Matrix dumps — enumerate the full (op/kind × device × arch × variant) grid, #
# including unavailable backends, without importing torch or touching a GPU. #
# --------------------------------------------------------------------------- #
def kernel_matrix() -> list[dict[str, Any]]:
ensure_backends_loaded()
return KERNELS.manifest()
def component_matrix() -> list[dict[str, Any]]:
ensure_backends_loaded()
return COMPONENTS.manifest()