[misc] v2: simplify docstrings/comments + drop deleted-design-doc citations
Sweep all 296 v2 modules: simplify verbose docstrings/comments and remove 376 dangling "(design_vN §X)" citations to the now-deleted design docs (v2/README.md is the source of truth). Comment/docstring-only — AST-verified code-identical; the CPU suite holds at 237 passed / 5 pre-existing. Also applies yapf + ruff --fix auto-fixes (import ordering, forward-ref annotation de-quoting under `from __future__ import annotations`; behavior- neutral, suite-confirmed) and adds the legitimate domain terms mot/clen/te to the codespell ignore-list. Remaining ruff (24) + mypy (68 no-untyped-call) findings are pre-existing v2 debt, untouched here.
This commit is contained in:
+3
-2
@@ -223,8 +223,9 @@ follow_imports = "silent"
|
||||
skip = "./data,./wandb,ui/package-lock.json,performance_dashboard/frontend/package-lock.json,*/_vendored/*"
|
||||
# "tread" matches daVinci-MagiHuman's acronym "TReAD" (Token Routing and
|
||||
# Early Drop). codespell lowercases ignore-words entries, so the single
|
||||
# lowercase form silences all case variants.
|
||||
ignore-words-list = "tread,passt"
|
||||
# lowercase form silences all case variants. "mot" = Mixture-of-Transformers
|
||||
# (MoT); "clen" = a Content-Length local; "te" = a text-embeds local.
|
||||
ignore-words-list = "tread,passt,mot,clen,te"
|
||||
|
||||
[tool.ruff]
|
||||
# Allow lines to be as long as 120.
|
||||
|
||||
+30
-6
@@ -1,4 +1,4 @@
|
||||
"""v2 — a scoped, CPU-testable realization of the design_v3 model-native runtime.
|
||||
"""v2 — a scoped, CPU-testable realization of the model-native runtime (see v2/README.md).
|
||||
|
||||
> A model card is a (recipe, runtime) pair with a parity obligation.
|
||||
> The model owns loop semantics; the runtime owns loop lifecycle.
|
||||
@@ -49,11 +49,35 @@ from v2.runtime import AsyncEngine, Engine
|
||||
__version__ = "0.2.0"
|
||||
|
||||
__all__ = [
|
||||
"ModelCard", "ComponentSpec", "LoopSpec", "RecipeSpec", "ParitySpec", "CostModel",
|
||||
"CapabilityMatrix", "ModelInstance", "load_card",
|
||||
"Engine", "AsyncEngine", "Program", "ProgramKind", "ComponentNode", "ModelLoopNode", "when_task", "when_opt",
|
||||
"Request", "Session", "Output", "make_request", "TaskType", "SamplingParams", "DiffusionParams",
|
||||
"LoopKind", "WorkUnitKind", "ConsistencyLevel", "ExecutionProfile", "Capability",
|
||||
"ModelCard",
|
||||
"ComponentSpec",
|
||||
"LoopSpec",
|
||||
"RecipeSpec",
|
||||
"ParitySpec",
|
||||
"CostModel",
|
||||
"CapabilityMatrix",
|
||||
"ModelInstance",
|
||||
"load_card",
|
||||
"Engine",
|
||||
"AsyncEngine",
|
||||
"Program",
|
||||
"ProgramKind",
|
||||
"ComponentNode",
|
||||
"ModelLoopNode",
|
||||
"when_task",
|
||||
"when_opt",
|
||||
"Request",
|
||||
"Session",
|
||||
"Output",
|
||||
"make_request",
|
||||
"TaskType",
|
||||
"SamplingParams",
|
||||
"DiffusionParams",
|
||||
"LoopKind",
|
||||
"WorkUnitKind",
|
||||
"ConsistencyLevel",
|
||||
"ExecutionProfile",
|
||||
"Capability",
|
||||
"VideoGenerator",
|
||||
"__version__",
|
||||
]
|
||||
|
||||
+28
-29
@@ -1,9 +1,8 @@
|
||||
"""Shared vocabulary enums (design_v3 §6.1, §9.2, §9.4).
|
||||
"""Shared vocabulary enums.
|
||||
|
||||
These live in a leaf module so both ``card/`` (which references loop kinds and
|
||||
consistency levels in its specs) and ``loop/``/``runtime/`` can import them
|
||||
without a circular dependency. design_v3 §18 boundary: ``card/`` imports no
|
||||
runtime; this is pure vocabulary.
|
||||
without a circular dependency. ``card/`` imports no runtime; this is pure vocabulary.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -11,21 +10,21 @@ from enum import Enum
|
||||
|
||||
|
||||
class LoopKind(str, Enum):
|
||||
"""The kind of iterative computation a LoopSpec describes (design_v3 §4.3)."""
|
||||
DIFFUSION_DENOISE = "diffusion_denoise" # N solver steps over full clip (Wan, LTX-2)
|
||||
CHUNK_ROLLOUT = "chunk_rollout" # causal chunk loop × inner denoise (self-forcing, world models)
|
||||
AR_DECODE = "ar_decode" # token loop until EOS (reasoner/thinker/talker — phase 2)
|
||||
VAE_TILE = "vae_tile" # tiled VAE encode/decode
|
||||
ENCODER = "encoder" # one-shot encoder (text/vision) — degenerate single-step loop
|
||||
AUDIO_DECODE = "audio_decode" # vocoder / codec decode
|
||||
TRAIN_FORWARD = "train_forward" # grad-enabled forward for a training method
|
||||
"""The kind of iterative computation a LoopSpec describes."""
|
||||
DIFFUSION_DENOISE = "diffusion_denoise" # N solver steps over full clip (Wan, LTX-2)
|
||||
CHUNK_ROLLOUT = "chunk_rollout" # causal chunk loop × inner denoise (self-forcing, world models)
|
||||
AR_DECODE = "ar_decode" # token loop until EOS (reasoner/thinker/talker — phase 2)
|
||||
VAE_TILE = "vae_tile" # tiled VAE encode/decode
|
||||
ENCODER = "encoder" # one-shot encoder (text/vision) — degenerate single-step loop
|
||||
AUDIO_DECODE = "audio_decode" # vocoder / codec decode
|
||||
TRAIN_FORWARD = "train_forward" # grad-enabled forward for a training method
|
||||
|
||||
|
||||
class WorkUnitKind(str, Enum):
|
||||
"""The smallest schedulable action — the scheduler's currency unit (design_v3 §6.1).
|
||||
"""The smallest schedulable action — the scheduler's currency unit.
|
||||
|
||||
Tokens are *one kind*, not the scheduler — the generalization of vLLM's token
|
||||
scheduler that diffusion forces.
|
||||
Tokens are one kind among many, not the scheduler itself: this generalizes
|
||||
vLLM's token scheduler to the work units diffusion needs.
|
||||
"""
|
||||
AR_PREFILL = "ar_prefill"
|
||||
AR_TOKEN = "ar_token"
|
||||
@@ -35,20 +34,20 @@ class WorkUnitKind(str, Enum):
|
||||
ENCODER_CHUNK = "encoder_chunk"
|
||||
VAE_TILE = "vae_tile"
|
||||
AUDIO_CHUNK = "audio_chunk"
|
||||
REWARD_BATCH = "reward_batch" # RL
|
||||
LOGPROB_BATCH = "logprob_batch" # RL (likelihood-based)
|
||||
REWARD_BATCH = "reward_batch" # RL
|
||||
LOGPROB_BATCH = "logprob_batch" # RL (likelihood-based)
|
||||
TRANSFER = "transfer"
|
||||
CACHE_IO = "cache_io"
|
||||
GRAPH_CAPTURE = "graph_capture"
|
||||
|
||||
|
||||
class ConsistencyLevel(str, Enum):
|
||||
"""The consistency ladder (design_v3 §9.2). RL methods declare their required rung."""
|
||||
C0 = "C0" # component parity (VAE/encoder/block/scheduler-step in isolation)
|
||||
C1 = "C1" # loop parity (full denoise trajectory / AR logits, fixed seed)
|
||||
C2 = "C2" # behavioral identity (train-forward ≡ serve-forward on the RL objective's quantity)
|
||||
C3 = "C3" # distribution parity (rollout distribution under allowed nondeterminism)
|
||||
C4 = "C4" # artifact quality (SSIM / reward agreement / human preference)
|
||||
"""The consistency ladder. RL methods declare their required rung."""
|
||||
C0 = "C0" # component parity (VAE/encoder/block/scheduler-step in isolation)
|
||||
C1 = "C1" # loop parity (full denoise trajectory / AR logits, fixed seed)
|
||||
C2 = "C2" # behavioral identity (train-forward ≡ serve-forward on the RL objective's quantity)
|
||||
C3 = "C3" # distribution parity (rollout distribution under allowed nondeterminism)
|
||||
C4 = "C4" # artifact quality (SSIM / reward agreement / human preference)
|
||||
|
||||
@property
|
||||
def rank(self) -> int:
|
||||
@@ -56,25 +55,25 @@ class ConsistencyLevel(str, Enum):
|
||||
|
||||
|
||||
class ExecutionProfile(str, Enum):
|
||||
"""Three forwards, one loop definition — differ only in grad mode + capture (design_v3 §9.4)."""
|
||||
SERVE = "serve" # no-grad, graphed, cached, possibly quantized
|
||||
ROLLOUT = "rollout" # serve profile + behavior capture
|
||||
TRAIN = "train" # grad, checkpointed, FSDP-gathered
|
||||
"""Three forwards, one loop definition — differ only in grad mode + capture."""
|
||||
SERVE = "serve" # no-grad, graphed, cached, possibly quantized
|
||||
ROLLOUT = "rollout" # serve profile + behavior capture
|
||||
TRAIN = "train" # grad, checkpointed, FSDP-gathered
|
||||
|
||||
|
||||
class Capability(str, Enum):
|
||||
"""CapabilityMatrix entries (designv2 model plane)."""
|
||||
"""CapabilityMatrix entries (model plane)."""
|
||||
TEXT_TO_VIDEO = "text_to_video"
|
||||
IMAGE_TO_VIDEO = "image_to_video"
|
||||
VIDEO_TO_VIDEO = "video_to_video"
|
||||
TEXT_TO_IMAGE = "text_to_image"
|
||||
TEXT_TO_VIDEO_SOUND = "text_to_video_sound"
|
||||
AUDIO_TO_VIDEO = "audio_to_video"
|
||||
TEXT_TO_SPEECH = "text_to_speech" # thinker→talker→vocoder (Qwen-Omni): reason + speak
|
||||
TEXT_TO_SPEECH = "text_to_speech" # thinker→talker→vocoder (Qwen-Omni): reason + speak
|
||||
REASONING_TEXT = "reasoning_text"
|
||||
ACTION_CONDITIONING = "action_conditioning"
|
||||
STREAMING_VIDEO_CONTINUATION = "streaming_video_continuation"
|
||||
VAE_ENCODE = "vae_encode"
|
||||
VAE_DECODE = "vae_decode"
|
||||
POLICY_ROLLOUT = "policy_rollout" # can serve as an RL rollout engine
|
||||
POLICY_ROLLOUT = "policy_rollout" # can serve as an RL rollout engine
|
||||
LOGPROB_RECOMPUTE = "logprob_recompute"
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
"""Shared lightweight type aliases for v2.
|
||||
|
||||
The core (card/loop/runtime/cache/parity/program/request/training) is numpy-only
|
||||
and CPU-testable. Tensors are therefore typed structurally as ``TensorLike``:
|
||||
on the CPU test path they are ``numpy.ndarray``; on a GPU box the same code runs
|
||||
with ``torch.Tensor``. The core never imports torch — only model-component
|
||||
*adapters* do, lazily (see ``v2/card/components.py``).
|
||||
and CPU-testable, so tensors are typed structurally as ``TensorLike``: a
|
||||
``numpy.ndarray`` on the CPU test path, a ``torch.Tensor`` on GPU. The core never
|
||||
imports torch — only model-component adapters do, lazily (see
|
||||
``v2/card/components.py``).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
# STUB: re-exports fastvideo until vendored (see memory: v2-vendoring-approach).
|
||||
"""``api`` facade — the typed config dataclasses the VideoGenerator consumes. Re-exported so v2 code
|
||||
imports ``v2.api`` instead of ``fastvideo.api``; a vendored cutover replaces these with v2-native configs."""
|
||||
imports ``v2.api`` instead of ``fastvideo.api``; a vendored cutover will replace these with v2-native configs."""
|
||||
from fastvideo.api import ( # noqa: F401
|
||||
EngineConfig,
|
||||
GenerationRequest,
|
||||
GenerationResult,
|
||||
GeneratorConfig,
|
||||
OffloadConfig,
|
||||
OutputConfig,
|
||||
SamplingConfig,
|
||||
EngineConfig, GenerationRequest, GenerationResult, GeneratorConfig, OffloadConfig, OutputConfig, SamplingConfig,
|
||||
)
|
||||
|
||||
Vendored
+5
-3
@@ -1,9 +1,11 @@
|
||||
"""Cache plane — correct by key, per-class pools (design_v3 §7)."""
|
||||
"""Cache plane — correct by key, per-class pools."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2.cache.classes import FeatureCache, PagedKVCache, ResidualCache, Slab, SlabKVCache, make_pool
|
||||
from v2.cache.keys import CacheKey, CachePolicy, content_hash
|
||||
from v2.cache.manager import CacheManager
|
||||
|
||||
__all__ = ["CacheKey", "CachePolicy", "content_hash", "CacheManager",
|
||||
"FeatureCache", "ResidualCache", "SlabKVCache", "PagedKVCache", "Slab", "make_pool"]
|
||||
__all__ = [
|
||||
"CacheKey", "CachePolicy", "content_hash", "CacheManager", "FeatureCache", "ResidualCache", "SlabKVCache",
|
||||
"PagedKVCache", "Slab", "make_pool"
|
||||
]
|
||||
|
||||
Vendored
+13
-15
@@ -1,10 +1,7 @@
|
||||
"""Per-class cache pools (design_v3 §7.2).
|
||||
"""Per-class cache pools.
|
||||
|
||||
> There is **no single unified block pool**, because a unified pool requires uniform
|
||||
> bytes-per-block and our cache classes differ by 150–500× in natural granularity ...
|
||||
> Each class gets a statically budgeted pool behind one ``CacheHandle``.
|
||||
|
||||
Mini-fastvideo implements four classes (the §7.2 minimal set):
|
||||
No single unified block pool: cache classes differ by 150-500x in natural granularity, so
|
||||
each gets its own statically budgeted pool behind one ``CacheHandle``. Four classes:
|
||||
* ``FeatureCache`` — content-hash keyed, partitioned by adapter+weights (text/vision encoders)
|
||||
* ``ResidualCache`` — cache-dit residuals, scoped per request AND per CFG branch
|
||||
* ``SlabKVCache`` — chunk-KV slabs (self-forcing / world models); training mode disables recycle
|
||||
@@ -15,7 +12,7 @@ KV is the minority case — a pure bidirectional deployment (Wan/LTX T2V) alloca
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from v2.cache.keys import CacheKey, CachePolicy
|
||||
@@ -28,6 +25,7 @@ def _nbytes(value: Any) -> int:
|
||||
|
||||
|
||||
class _Pool:
|
||||
|
||||
def __init__(self, policy: CachePolicy):
|
||||
self.policy = policy
|
||||
self.used_bytes = 0
|
||||
@@ -36,7 +34,7 @@ class _Pool:
|
||||
|
||||
|
||||
class FeatureCache(_Pool):
|
||||
"""content-hash keyed, reference-counted-ish FIFO/LRU, budget-aware (§7.2).
|
||||
"""Content-hash keyed, budget-aware FIFO/LRU.
|
||||
|
||||
Partitioned by ``adapter_versions``/``weights_version`` through the CacheKey, so two
|
||||
workflows sharing a prompt but differing in te-LoRA stack never serve stale embeddings.
|
||||
@@ -44,7 +42,7 @@ class FeatureCache(_Pool):
|
||||
|
||||
def __init__(self, policy: CachePolicy):
|
||||
super().__init__(policy)
|
||||
self._store: "OrderedDict[str, tuple[Any, int, CacheKey]]" = OrderedDict()
|
||||
self._store: OrderedDict[str, tuple[Any, int, CacheKey]] = OrderedDict()
|
||||
|
||||
def get(self, key: CacheKey) -> Any | None:
|
||||
if not self.policy.reuse_across_requests:
|
||||
@@ -52,7 +50,7 @@ class FeatureCache(_Pool):
|
||||
h = key.hash
|
||||
if h in self._store:
|
||||
self.hits += 1
|
||||
self._store.move_to_end(h) # LRU
|
||||
self._store.move_to_end(h) # LRU
|
||||
return self._store[h][0]
|
||||
self.misses += 1
|
||||
return None
|
||||
@@ -80,7 +78,7 @@ class FeatureCache(_Pool):
|
||||
self.used_bytes -= nb
|
||||
|
||||
def invalidate_components(self, components: set[str]) -> None:
|
||||
"""Drop only entries produced by the changed components (design_v3 §7.1 partition-not-flush):
|
||||
"""Drop only entries produced by the changed components (partition, not flush):
|
||||
a transformer-only weight sync must NOT evict text-encoder embeddings."""
|
||||
drop = [h for h, (_v, _nb, k) in self._store.items() if k.component_id in components]
|
||||
for h in drop:
|
||||
@@ -89,7 +87,7 @@ class FeatureCache(_Pool):
|
||||
|
||||
|
||||
class ResidualCache(_Pool):
|
||||
"""cache-dit residual store, scoped per ``LoopState`` AND per CFG branch (§5.1, §11).
|
||||
"""cache-dit residual store, scoped per ``LoopState`` AND per CFG branch.
|
||||
|
||||
Keyed by (namespace, branch, name) where namespace is the request/loop id. This is the
|
||||
structural fix for the module-global residual state that corrupts cache-dit forks under
|
||||
@@ -124,10 +122,10 @@ class Slab:
|
||||
|
||||
|
||||
class SlabKVCache(_Pool):
|
||||
"""Chunk-KV slabs for causal/world-model rollout (§7.2).
|
||||
"""Chunk-KV slabs for causal/world-model rollout.
|
||||
|
||||
``training_mode`` disables mid-rollout recycling and keeps grad-aware index snapshots
|
||||
so activation-checkpoint recompute doesn't double-advance the cache (self-forcing).
|
||||
``training_mode`` disables mid-rollout recycling so activation-checkpoint recompute
|
||||
doesn't double-advance the cache (self-forcing).
|
||||
"""
|
||||
|
||||
def __init__(self, policy: CachePolicy):
|
||||
|
||||
Vendored
+12
-16
@@ -1,12 +1,9 @@
|
||||
"""CacheKey — cache correctness is a contract, not hope (design_v3 §7.1).
|
||||
"""CacheKey — cache correctness is a contract.
|
||||
|
||||
> If a field can change output semantics, it is in the key. Incorrect reuse is worse
|
||||
> than no reuse.
|
||||
|
||||
The serving hazard this kills: a workflow-cloud request that shares a prompt but
|
||||
differs in te-LoRA stack must not serve stale embeddings — so the key is *partitioned*
|
||||
by ``adapter_versions``, not flushed. An RL ``update_weights`` bumps ``weights_version``
|
||||
and invalidates wholesale.
|
||||
If a field can change output semantics, it is in the key (incorrect reuse is worse than no reuse).
|
||||
The serving hazard this kills: a request that shares a prompt but differs in te-LoRA stack must not
|
||||
serve stale embeddings — so the key is *partitioned* by ``adapter_versions``, not flushed. An RL
|
||||
``update_weights`` bumps ``weights_version`` and invalidates wholesale.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -16,17 +13,16 @@ from typing import Any
|
||||
|
||||
|
||||
def content_hash(obj: Any) -> str:
|
||||
"""Stable content hash for feature-cache keys (text/vision embeddings, §7.2).
|
||||
"""Stable content hash for feature-cache keys (text/vision embeddings).
|
||||
|
||||
Lets K rollout samples of one prompt reuse a single text encode (design_v3 §10:
|
||||
24× text-encode reduction).
|
||||
Lets K rollout samples of one prompt reuse a single text encode.
|
||||
"""
|
||||
h = hashlib.sha256()
|
||||
if isinstance(obj, str):
|
||||
h.update(obj.encode("utf-8"))
|
||||
elif isinstance(obj, (bytes, bytearray)):
|
||||
h.update(obj)
|
||||
elif hasattr(obj, "tobytes") and hasattr(obj, "shape"): # numpy / torch tensor
|
||||
elif hasattr(obj, "tobytes") and hasattr(obj, "shape"): # numpy / torch tensor
|
||||
h.update(str(getattr(obj, "shape", "")).encode())
|
||||
h.update(str(getattr(obj, "dtype", "")).encode())
|
||||
try:
|
||||
@@ -44,7 +40,7 @@ class CacheKey:
|
||||
component_id: str
|
||||
loop_id: str | None = None
|
||||
weights_version: str = "v0"
|
||||
adapter_versions: tuple[tuple[str, str], ...] = () # sorted (adapter_id, version) pairs
|
||||
adapter_versions: tuple[tuple[str, str], ...] = () # sorted (adapter_id, version) pairs
|
||||
precision: str = "float32"
|
||||
parallel_plan_hash: str = ""
|
||||
shape_sig: str = ""
|
||||
@@ -75,11 +71,11 @@ class CacheKey:
|
||||
|
||||
@dataclass
|
||||
class CachePolicy:
|
||||
"""Runtime config for one cache class pool (design_v3 §7.2; designv2 cache plane)."""
|
||||
class_name: str # "feature" | "residual" | "slab_kv" | "paged_kv"
|
||||
"""Runtime config for one cache class pool."""
|
||||
class_name: str # "feature" | "residual" | "slab_kv" | "paged_kv"
|
||||
max_bytes: int = 1 << 30
|
||||
block_bytes: int = 1 << 16
|
||||
eviction: str = "lru" # "lru" | "fifo" | "none"
|
||||
eviction: str = "lru" # "lru" | "fifo" | "none"
|
||||
reuse_across_requests: bool = True
|
||||
per_component: dict[str, int] = field(default_factory=dict)
|
||||
training_mode_disables_recycle: bool = False
|
||||
|
||||
Vendored
+22
-11
@@ -1,4 +1,4 @@
|
||||
"""CacheManager — per-class pools with static budgets, behind one handle (design_v3 §7.2).
|
||||
"""CacheManager — per-class pools with static budgets, behind one handle.
|
||||
|
||||
Static partitioning makes cross-class fragmentation impossible (jumbo slab traffic cannot
|
||||
strand text-KV pages and vice versa). Each class gets a budget carved at init from the card's
|
||||
@@ -13,22 +13,27 @@ from v2.cache.keys import CachePolicy
|
||||
|
||||
|
||||
class CacheManager:
|
||||
|
||||
def __init__(self, policies: list[CachePolicy] | None = None):
|
||||
self._pools: dict[str, _Pool] = {}
|
||||
for p in (policies or []):
|
||||
self._pools[p.class_name] = make_pool(p)
|
||||
|
||||
@classmethod
|
||||
def from_card(cls, card) -> "CacheManager":
|
||||
def from_card(cls, card) -> CacheManager:
|
||||
"""Build the per-class pools a card declares (KV pools materialize only if declared)."""
|
||||
policies = []
|
||||
for cc in card.caches.values():
|
||||
policies.append(CachePolicy(
|
||||
class_name=cc.cache_class, max_bytes=cc.max_bytes, block_bytes=cc.block_bytes,
|
||||
eviction=cc.eviction, reuse_across_requests=cc.reuse_across_requests,
|
||||
per_component=dict(cc.per_component),
|
||||
training_mode_disables_recycle=cc.training_mode_disables_recycle,
|
||||
))
|
||||
policies.append(
|
||||
CachePolicy(
|
||||
class_name=cc.cache_class,
|
||||
max_bytes=cc.max_bytes,
|
||||
block_bytes=cc.block_bytes,
|
||||
eviction=cc.eviction,
|
||||
reuse_across_requests=cc.reuse_across_requests,
|
||||
per_component=dict(cc.per_component),
|
||||
training_mode_disables_recycle=cc.training_mode_disables_recycle,
|
||||
))
|
||||
return cls(policies)
|
||||
|
||||
def pool(self, class_name: str) -> _Pool:
|
||||
@@ -45,7 +50,7 @@ class CacheManager:
|
||||
pool.invalidate_weights(version)
|
||||
|
||||
def invalidate_components(self, components) -> None:
|
||||
"""Component-scoped invalidation (design_v3 §7.1): only drop caches for changed components."""
|
||||
"""Component-scoped invalidation: only drop caches for changed components."""
|
||||
comps = set(components)
|
||||
for pool in self._pools.values():
|
||||
if hasattr(pool, "invalidate_components"):
|
||||
@@ -60,5 +65,11 @@ class CacheManager:
|
||||
pool.free_namespace(namespace)
|
||||
|
||||
def stats(self) -> dict[str, Any]:
|
||||
return {name: {"used_bytes": p.used_bytes, "hits": p.hits, "misses": p.misses}
|
||||
for name, p in self._pools.items()}
|
||||
return {
|
||||
name: {
|
||||
"used_bytes": p.used_bytes,
|
||||
"hits": p.hits,
|
||||
"misses": p.misses
|
||||
}
|
||||
for name, p in self._pools.items()
|
||||
}
|
||||
|
||||
+23
-6
@@ -1,4 +1,4 @@
|
||||
"""Model Plane — the center (design_v3 §4). The (recipe, runtime) pair as a typed card."""
|
||||
"""Model Plane — the (recipe, runtime) pair as a typed card."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2._enums import Capability, ConsistencyLevel, ExecutionProfile, LoopKind, WorkUnitKind
|
||||
@@ -22,9 +22,26 @@ from v2.card.specs import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ModelCard", "ComponentSpec", "LoopSpec", "RecipeSpec", "ParitySpec", "ParityTestSpec",
|
||||
"CheckpointManifest", "CapabilityMatrix", "CostModel", "CacheContract", "SamplingDefaults",
|
||||
"ParallelismContract", "PrecisionContract", "DataRef", "CardValidationError",
|
||||
"ModelInstance", "load_card",
|
||||
"Capability", "ConsistencyLevel", "ExecutionProfile", "LoopKind", "WorkUnitKind",
|
||||
"ModelCard",
|
||||
"ComponentSpec",
|
||||
"LoopSpec",
|
||||
"RecipeSpec",
|
||||
"ParitySpec",
|
||||
"ParityTestSpec",
|
||||
"CheckpointManifest",
|
||||
"CapabilityMatrix",
|
||||
"CostModel",
|
||||
"CacheContract",
|
||||
"SamplingDefaults",
|
||||
"ParallelismContract",
|
||||
"PrecisionContract",
|
||||
"DataRef",
|
||||
"CardValidationError",
|
||||
"ModelInstance",
|
||||
"load_card",
|
||||
"Capability",
|
||||
"ConsistencyLevel",
|
||||
"ExecutionProfile",
|
||||
"LoopKind",
|
||||
"WorkUnitKind",
|
||||
]
|
||||
|
||||
+30
-24
@@ -1,13 +1,10 @@
|
||||
"""ModelInstance — a resident, loaded card (design_v3 §4.3).
|
||||
"""ModelInstance — a resident, loaded card: component instances, model state, caches,
|
||||
compiled graphs, and a parallel plan. One request may run several of the card's loops
|
||||
against one ``ModelInstance``.
|
||||
|
||||
> A ``ModelInstance`` is a resident, loaded card: component instances, model state,
|
||||
> caches, compiled graphs, and a parallel plan. **A request may run several of the
|
||||
> card's loops against one ``ModelInstance``.**
|
||||
|
||||
That single sentence is the difference between this design and a stage-only design,
|
||||
and it is what makes omni native: when two loops bind the same component
|
||||
(``shared_weight_components``), ``component()`` returns the *exact same* live object
|
||||
— no duplicated weights, shared live state.
|
||||
This is what makes omni native: when two loops bind the same component
|
||||
(``shared_weight_components``), ``component()`` returns the *exact same* live object —
|
||||
no duplicated weights, shared live state. See v2/README.md.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -21,23 +18,26 @@ class ModelInstance:
|
||||
"""The live, resident form of a ModelCard. Also serves as the ``ModelState``
|
||||
passed to ``Loop.init`` (the loop reads components through it)."""
|
||||
|
||||
def __init__(self, card: ModelCard, parallel_plan: Any = None,
|
||||
cache_manager: Any = None, weights_version: str = "v0",
|
||||
def __init__(self,
|
||||
card: ModelCard,
|
||||
parallel_plan: Any = None,
|
||||
cache_manager: Any = None,
|
||||
weights_version: str = "v0",
|
||||
platform: Any = None):
|
||||
self.card = card
|
||||
self.parallel_plan = parallel_plan
|
||||
self.caches = cache_manager
|
||||
self.weights_version = weights_version
|
||||
# The detected (device, arch). Resolves component/kernel implementations through the two
|
||||
# backend registries; defaults to CPU/numpy. Swapping this to a GPU platform is the whole
|
||||
# "change only the backend, not the loops/policies/training" story (design_v3 §17).
|
||||
# backend registries; defaults to CPU/numpy. Swapping this to a GPU platform changes only
|
||||
# the backend, not the loops/policies/training.
|
||||
self.platform = platform if platform is not None else Platform.cpu()
|
||||
# Piecewise CUDA-graph cache for loops declaring graph_capture="breakable_cudagraph". Lazily
|
||||
# created by the runtime (kept as a plain attr so card/ stays free of any runtime import —
|
||||
# design_v3 §18 boundary); a captured graph is tied to this instance's resident weights.
|
||||
# created by the runtime (kept as a plain attr so card/ stays free of any runtime import);
|
||||
# a captured graph is tied to this instance's resident weights.
|
||||
self.graphs: Any = None
|
||||
self.adapter_versions: dict[str, str] = {}
|
||||
# per-component weight versions (§7.1): a component's version changes only when IT is synced,
|
||||
# Per-component weight versions: a component's version changes only when IT is synced,
|
||||
# so a transformer-only RL sync never invalidates the frozen text-encoder's feature cache.
|
||||
self.component_versions: dict[str, str] = {cid: weights_version for cid in card.components}
|
||||
self._components: dict[str, Any] = {}
|
||||
@@ -71,7 +71,7 @@ class ModelInstance:
|
||||
self._loops[loop_id] = spec.loop_factory()
|
||||
return self._loops[loop_id]
|
||||
|
||||
# --- sleep/wake by component (design_v3 §7.3; CuMem tags = component names) #
|
||||
# --- sleep/wake by component (CuMem tags = component names) --------------- #
|
||||
def sleep(self, component_ids: list[str]) -> None:
|
||||
for cid in component_ids:
|
||||
self._components.pop(cid, None)
|
||||
@@ -81,15 +81,15 @@ class ModelInstance:
|
||||
for cid in component_ids:
|
||||
self._asleep.discard(cid)
|
||||
|
||||
# --- weight sync (design_v3 §10): bump version + invalidate caches -------- #
|
||||
# --- weight sync: bump version + invalidate caches ----------------------- #
|
||||
def version_of(self, component_id: str) -> str:
|
||||
"""The component's own weights version (defaults to the instance version)."""
|
||||
return self.component_versions.get(component_id, self.weights_version)
|
||||
|
||||
def set_weights_version(self, version: str, components: list[str] | None = None) -> None:
|
||||
"""Publish a new weights version. If ``components`` is given, only those components' versions
|
||||
bump and only their caches are invalidated (design_v3 §7.1 partition-not-flush) — so a
|
||||
transformer-only RL weight sync leaves the frozen text-encoder's feature cache intact."""
|
||||
bump and only their caches are invalidated (partition, not flush) — so a transformer-only RL
|
||||
weight sync leaves the frozen text-encoder's feature cache intact."""
|
||||
self.weights_version = version
|
||||
changed = components if components is not None else list(self.card.components.keys())
|
||||
for c in changed:
|
||||
@@ -106,9 +106,13 @@ class ModelInstance:
|
||||
f"resident={sorted(self._components)})")
|
||||
|
||||
|
||||
def load_card(card: ModelCard, parallel_plan: Any = None, cache_manager: Any = None,
|
||||
*, validate: bool = True, platform: Any = None) -> ModelInstance:
|
||||
"""The card-as-factory entrypoint (design_v3 §4.1: card is a runtime factory).
|
||||
def load_card(card: ModelCard,
|
||||
parallel_plan: Any = None,
|
||||
cache_manager: Any = None,
|
||||
*,
|
||||
validate: bool = True,
|
||||
platform: Any = None) -> ModelInstance:
|
||||
"""The card-as-factory entrypoint: the card is a runtime factory.
|
||||
|
||||
``platform`` selects the backend (device, arch); when omitted it is detected (CPU/numpy here,
|
||||
CUDA on a torch+GPU box). The same card loads on any backend — only the resolved component/kernel
|
||||
@@ -116,5 +120,7 @@ def load_card(card: ModelCard, parallel_plan: Any = None, cache_manager: Any = N
|
||||
"""
|
||||
if validate:
|
||||
card.validate()
|
||||
return ModelInstance(card, parallel_plan=parallel_plan, cache_manager=cache_manager,
|
||||
return ModelInstance(card,
|
||||
parallel_plan=parallel_plan,
|
||||
cache_manager=cache_manager,
|
||||
platform=platform if platform is not None else Platform.detect())
|
||||
|
||||
+66
-73
@@ -1,36 +1,36 @@
|
||||
"""ModelCard and its sub-specs — the Model Plane, the center (design_v3 §4).
|
||||
"""ModelCard and its sub-specs — the Model Plane.
|
||||
|
||||
> The atomic unit is the (recipe, runtime) pair, owned by a typed ``ModelCard``.
|
||||
The atomic unit is the (recipe, runtime) pair, owned by a typed ``ModelCard``. The card
|
||||
is both a declarative contract (strict enough to validate before any GPU touches it — see
|
||||
``ModelCard.validate``) and a runtime factory (it instantiates components, binds loops, and
|
||||
resolves caches — see ``card/instance.py``).
|
||||
|
||||
The card is both a **declarative contract** (strict enough to validate before any
|
||||
GPU touches it — see ``ModelCard.validate``) and a **runtime factory** (it knows how
|
||||
to instantiate components, bind loops, and resolve caches — see ``card/instance.py``).
|
||||
|
||||
design_v3 §18 boundary: ``card/`` imports no product/runtime. It depends only on the
|
||||
shared leaf modules (``_enums``, ``_types``) and references ``ParallelPlan`` under
|
||||
``TYPE_CHECKING`` so there is no runtime coupling to ``parallel/``.
|
||||
Boundary: ``card/`` imports no product/runtime. It depends only on the shared leaf modules
|
||||
(``_enums``, ``_types``) and references ``ParallelPlan`` under ``TYPE_CHECKING`` so there is
|
||||
no runtime coupling to ``parallel/``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from collections.abc import Callable
|
||||
|
||||
from v2._enums import Capability, ConsistencyLevel, LoopKind, WorkUnitKind
|
||||
|
||||
if TYPE_CHECKING: # avoid runtime card -> parallel coupling
|
||||
from v2.parallel.plan import ParallelPlan
|
||||
pass
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Cost model — the budget currency, REQUIRED on every LoopSpec (design_v3 §6.1) #
|
||||
# Cost model — the budget currency, REQUIRED on every LoopSpec #
|
||||
# --------------------------------------------------------------------------- #
|
||||
@dataclass
|
||||
class CostModel:
|
||||
"""Predicted GPU-time per WorkUnit at (shape, precision, policy).
|
||||
|
||||
"The budget currency is predicted GPU-time, not counts." One object, two
|
||||
consumers: the scheduler's internal budget and the fleet's routing input
|
||||
(design_v3 §6.1, §14). Calibrated online by the Profiler observer (§11).
|
||||
The budget currency is predicted GPU-time, not counts. One object, two consumers:
|
||||
the scheduler's internal budget and the fleet's routing input. Calibrated online by
|
||||
the Profiler observer.
|
||||
"""
|
||||
kind: WorkUnitKind
|
||||
# synthetic seconds = base + per_unit * work_units(shape) * policy_factor
|
||||
@@ -39,7 +39,7 @@ class CostModel:
|
||||
coefficients: dict[str, float] = field(default_factory=dict)
|
||||
|
||||
def predict(self, work_units: float, policy_factor: float = 1.0) -> float:
|
||||
"""Conservative baseline cost (design_v3 §6.1: admission never optimistic)."""
|
||||
"""Conservative baseline cost: admission is never optimistic."""
|
||||
return (self.base_seconds + self.per_unit_seconds * float(work_units)) * policy_factor
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@ class CostModel:
|
||||
class PrecisionContract:
|
||||
default_dtype: str = "float32"
|
||||
component_overrides: dict[str, str] = field(default_factory=dict)
|
||||
quantization_scheme: str | None = None # "nvfp4" | "int8" | None
|
||||
training_precision: str = "float32" # distinct from serving precision
|
||||
quantization_scheme: str | None = None # "nvfp4" | "int8" | None
|
||||
training_precision: str = "float32" # distinct from serving precision
|
||||
|
||||
def dtype_for(self, component_id: str) -> str:
|
||||
return self.component_overrides.get(component_id, self.default_dtype)
|
||||
@@ -59,39 +59,39 @@ class PrecisionContract:
|
||||
|
||||
@dataclass
|
||||
class ParallelismContract:
|
||||
valid_plans: list = field(default_factory=list) # list[ParallelPlan]
|
||||
default_plan: Any = None # ParallelPlan | None
|
||||
valid_plans: list = field(default_factory=list) # list[ParallelPlan]
|
||||
default_plan: Any = None # ParallelPlan | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheContract:
|
||||
"""A cache class the card declares it needs (design_v3 §7.2; designv2 cache plane)."""
|
||||
cache_class: str # "feature" | "residual" | "slab_kv" | "paged_kv"
|
||||
"""A cache class the card declares it needs."""
|
||||
cache_class: str # "feature" | "residual" | "slab_kv" | "paged_kv"
|
||||
max_bytes: int = 1 << 30
|
||||
block_bytes: int = 1 << 16 # page bytes (paged) / slab granule (slab)
|
||||
eviction: str = "lru" # "lru" | "fifo" | "none"
|
||||
reuse_across_requests: bool = True # paged/feature=True; slab depends on mode
|
||||
block_bytes: int = 1 << 16 # page bytes (paged) / slab granule (slab)
|
||||
eviction: str = "lru" # "lru" | "fifo" | "none"
|
||||
reuse_across_requests: bool = True # paged/feature=True; slab depends on mode
|
||||
per_component: dict[str, int] = field(default_factory=dict)
|
||||
training_mode_disables_recycle: bool = False # chunk-KV training mode (§7.2)
|
||||
training_mode_disables_recycle: bool = False # chunk-KV training mode
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParityTestSpec:
|
||||
"""A named tap + tolerance + ladder level (design_v3 §9.1)."""
|
||||
"""A named tap + tolerance + ladder level."""
|
||||
name: str
|
||||
level: ConsistencyLevel
|
||||
tap: str # named activation tap, e.g. "block.0.out"
|
||||
tap: str # named activation tap, e.g. "block.0.out"
|
||||
rtol: float = 0.0
|
||||
atol: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParitySpec:
|
||||
"""The (recipe, runtime) honesty contract (design_v3 §9). Measured, never assumed."""
|
||||
"""The (recipe, runtime) honesty contract. Measured, never assumed."""
|
||||
consistency_levels: list[ConsistencyLevel] = field(default_factory=lambda: [ConsistencyLevel.C1])
|
||||
tests: list[ParityTestSpec] = field(default_factory=list)
|
||||
tap_tolerances: dict[str, float] = field(default_factory=dict)
|
||||
interleave_required: bool = True # the batch-of-N gate (§9.3) is non-negotiable
|
||||
interleave_required: bool = True # the batch-of-N gate is non-negotiable
|
||||
|
||||
@property
|
||||
def max_level(self) -> ConsistencyLevel:
|
||||
@@ -100,7 +100,7 @@ class ParitySpec:
|
||||
|
||||
@dataclass
|
||||
class DataRef:
|
||||
"""What a recipe trained on, for governance/reproduction (design_v3 §4.2)."""
|
||||
"""What a recipe trained on, for governance/reproduction."""
|
||||
dataset_id: str = ""
|
||||
revision: str = ""
|
||||
description: str = ""
|
||||
@@ -108,41 +108,41 @@ class DataRef:
|
||||
|
||||
@dataclass
|
||||
class RecipeSpec:
|
||||
"""The provenance half of the (recipe, runtime) pair (design_v3 §4.2, §2.1).
|
||||
"""The provenance half of the (recipe, runtime) pair.
|
||||
|
||||
``assumes_loop`` and ``assumes_precision`` are the teeth: a 4-step distilled
|
||||
model whose ``assumes_loop = "ddim_4step"`` cannot be served under a 50-step
|
||||
sampler without a typed mismatch error (enforced in ModelCard.validate).
|
||||
"""
|
||||
method: str = "base" # base | dmd2 | self_forcing | diffusion_nft | attn_qat_nvfp4
|
||||
parents: list[str] = field(default_factory=list) # teacher / base model_ids
|
||||
method: str = "base" # base | dmd2 | self_forcing | diffusion_nft | attn_qat_nvfp4
|
||||
parents: list[str] = field(default_factory=list) # teacher / base model_ids
|
||||
data_contract: DataRef = field(default_factory=DataRef)
|
||||
assumes_loop: str = "" # loop_id this recipe's weights require
|
||||
assumes_loop: str = "" # loop_id this recipe's weights require
|
||||
assumes_precision: str = "float32"
|
||||
consistency_required: ConsistencyLevel = ConsistencyLevel.C1
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentSpec:
|
||||
"""A weight-bearing (or processing) component (design_v3 §4.3; designv2 §1.2).
|
||||
"""A weight-bearing (or processing) component.
|
||||
|
||||
Omni-ready fields ``resident_for`` / ``optional_for`` / ``required_for`` turn the
|
||||
Cosmos3 lazy-sound-VAE problem into a declaration, not an ``if env_var`` inside
|
||||
``forward`` (design_v3 §4.1).
|
||||
``forward``.
|
||||
"""
|
||||
component_id: str
|
||||
kind: str # dit | vae | text_encoder | audio_vae | reasoner_tower | ...
|
||||
load_id: str = "" # "module:Class" for the real (torch) adapter
|
||||
kind: str # dit | vae | text_encoder | audio_vae | reasoner_tower | ...
|
||||
load_id: str = "" # "module:Class" for the real (torch) adapter
|
||||
config_schema: type | None = None
|
||||
io_schema: tuple[type | None, type | None] = (None, None)
|
||||
precision_policy: str | None = None
|
||||
placement_policy: str = "colocated"
|
||||
parallel_constraints: dict[str, Any] = field(default_factory=dict)
|
||||
parity_tests: list[ParityTestSpec] = field(default_factory=list)
|
||||
# omni-ready (design_v3 §4.1; omni-readiness report §4a):
|
||||
# omni-ready:
|
||||
resident_for: list[str] = field(default_factory=list) # loop_ids that keep this resident mid-request
|
||||
optional_for: set[str] = field(default_factory=set) # tasks that don't need it
|
||||
required_for: set[str] = field(default_factory=set) # tasks that require it
|
||||
optional_for: set[str] = field(default_factory=set) # tasks that don't need it
|
||||
required_for: set[str] = field(default_factory=set) # tasks that require it
|
||||
# v2 wiring: a factory producing the live component (toy numpy or torch adapter)
|
||||
factory: Callable[..., Any] | None = None
|
||||
# GPU backend: weights source (HF id or local path) for the real torch adapter resolved from
|
||||
@@ -157,7 +157,7 @@ class ComponentSpec:
|
||||
|
||||
@dataclass
|
||||
class LoopSpec:
|
||||
"""Describes one iterative computation the card can run (design_v3 §4.3).
|
||||
"""Describes one iterative computation the card can run.
|
||||
|
||||
The model owns loop *semantics*; the runtime owns loop *lifecycle*. A single
|
||||
``ModelInstance`` may run several of these against shared components — the MoT
|
||||
@@ -166,16 +166,16 @@ class LoopSpec:
|
||||
loop_id: str
|
||||
kind: LoopKind
|
||||
work_unit_kind: WorkUnitKind
|
||||
step_cost_model: CostModel # REQUIRED (design_v3 §6.1)
|
||||
state_schema: type | None = None # the typed LoopState
|
||||
step_schema: type | None = None # the typed WorkPlan a step emits
|
||||
result_schema: type | None = None # the typed StepResult
|
||||
behavior_schema: type | None = None # what to capture for RL (None if not training-relevant)
|
||||
extension_schema: type | None = None # per-model LoopState extension (Cosmos3PackedSeq, etc.)
|
||||
step_cost_model: CostModel # REQUIRED
|
||||
state_schema: type | None = None # the typed LoopState
|
||||
step_schema: type | None = None # the typed WorkPlan a step emits
|
||||
result_schema: type | None = None # the typed StepResult
|
||||
behavior_schema: type | None = None # what to capture for RL (None if not training-relevant)
|
||||
extension_schema: type | None = None # per-model LoopState extension (Cosmos3PackedSeq, etc.)
|
||||
cache_policy: list[str] = field(default_factory=list) # cache class names this loop draws from
|
||||
valid_parallel_plans: list = field(default_factory=list)
|
||||
graph_capture: str = "eager" # eager | breakable_cudagraph
|
||||
# omni-ready (omni-readiness report §4a):
|
||||
graph_capture: str = "eager" # eager | breakable_cudagraph
|
||||
# omni-ready:
|
||||
shared_weight_components: list[str] = field(default_factory=list)
|
||||
allows_interleaving: bool = True
|
||||
# v2: the Loop implementation factory (built at bind time)
|
||||
@@ -184,12 +184,12 @@ class LoopSpec:
|
||||
|
||||
@dataclass
|
||||
class CheckpointManifest:
|
||||
"""Explicit declared components + key maps — no name-detector guessing (design_v3 §4.1)."""
|
||||
"""Explicit declared components + key maps — no name-detector guessing."""
|
||||
upstream_source: str = ""
|
||||
revision: str = ""
|
||||
component_ownership: dict[str, list[str]] = field(default_factory=dict) # component_id -> file globs
|
||||
key_mappings: dict[str, str] = field(default_factory=dict) # ckpt_key -> component.param
|
||||
required_for: dict[str, set[str]] = field(default_factory=dict) # component_id -> tasks requiring it
|
||||
key_mappings: dict[str, str] = field(default_factory=dict) # ckpt_key -> component.param
|
||||
required_for: dict[str, set[str]] = field(default_factory=dict) # component_id -> tasks requiring it
|
||||
optional_for: dict[str, set[str]] = field(default_factory=dict)
|
||||
conversion_version: str = "v0"
|
||||
|
||||
@@ -202,7 +202,7 @@ class CapabilityMatrix:
|
||||
return cap in self.capabilities
|
||||
|
||||
@classmethod
|
||||
def of(cls, *caps: Capability) -> "CapabilityMatrix":
|
||||
def of(cls, *caps: Capability) -> CapabilityMatrix:
|
||||
return cls(frozenset(caps))
|
||||
|
||||
|
||||
@@ -210,8 +210,7 @@ class CapabilityMatrix:
|
||||
class SamplingDefaults:
|
||||
"""Per-model default generation params (the v2 mirror of fastvideo's per-model ``InferencePreset``
|
||||
defaults). Applied by the entrypoint when the caller didn't specify a value; ``None`` => fall back to
|
||||
the generic default. ``shift``/``sigmas``/MoE boundary already live in the card's loop policies — these
|
||||
are the user-facing knobs surfaced at request time."""
|
||||
the generic default. These are the user-facing knobs surfaced at request time."""
|
||||
num_steps: int | None = None
|
||||
guidance_scale: float | None = None
|
||||
guidance_per_modality: dict[str, float] = field(default_factory=dict) # joint A/V, e.g. {"video":3,"audio":7}
|
||||
@@ -246,28 +245,23 @@ class ModelCard:
|
||||
checkpoint: CheckpointManifest = field(default_factory=CheckpointManifest)
|
||||
sampling_defaults: SamplingDefaults = field(default_factory=SamplingDefaults)
|
||||
|
||||
def validate(self) -> "ModelCard":
|
||||
"""Strict enough to validate before any GPU touches it (design_v3 §4.1).
|
||||
def validate(self) -> ModelCard:
|
||||
"""Strict enough to validate before any GPU touches it.
|
||||
|
||||
Returns self so it can be chained. Raises CardValidationError on any
|
||||
contract violation — the (recipe, runtime) binding is enforced here.
|
||||
"""
|
||||
errs: list[str] = []
|
||||
|
||||
# 1) recipe.assumes_loop must exist (the (recipe, runtime) binding, §2.1/§4.2)
|
||||
# 1) recipe.assumes_loop must exist (the (recipe, runtime) binding)
|
||||
if self.recipe.assumes_loop and self.recipe.assumes_loop not in self.loops:
|
||||
errs.append(
|
||||
f"recipe.assumes_loop={self.recipe.assumes_loop!r} is not a declared loop "
|
||||
f"(have {sorted(self.loops)}) — a recipe cannot assume a loop the card does not run"
|
||||
)
|
||||
errs.append(f"recipe.assumes_loop={self.recipe.assumes_loop!r} is not a declared loop "
|
||||
f"(have {sorted(self.loops)}) — a recipe cannot assume a loop the card does not run")
|
||||
|
||||
# 2) recipe.assumes_precision must be consistent with the precision contract
|
||||
if (self.recipe.assumes_precision
|
||||
and self.recipe.assumes_precision not in (self.precision.default_dtype,
|
||||
*self.precision.component_overrides.values())):
|
||||
errs.append(
|
||||
f"recipe.assumes_precision={self.recipe.assumes_precision!r} not present in precision contract"
|
||||
)
|
||||
if (self.recipe.assumes_precision and self.recipe.assumes_precision
|
||||
not in (self.precision.default_dtype, *self.precision.component_overrides.values())):
|
||||
errs.append(f"recipe.assumes_precision={self.recipe.assumes_precision!r} not present in precision contract")
|
||||
|
||||
# 3) every loop's shared_weight_components must be declared components
|
||||
for lid, loop in self.loops.items():
|
||||
@@ -278,7 +272,7 @@ class ModelCard:
|
||||
if cc not in self.caches:
|
||||
errs.append(f"loop {lid!r} references cache class {cc!r} not in card.caches")
|
||||
if loop.step_cost_model is None:
|
||||
errs.append(f"loop {lid!r} is missing the REQUIRED step_cost_model (design_v3 §6.1)")
|
||||
errs.append(f"loop {lid!r} is missing the REQUIRED step_cost_model")
|
||||
|
||||
# 4) required_for / optional_for tasks must be disjoint per component
|
||||
for cid, comp in self.components.items():
|
||||
@@ -287,8 +281,7 @@ class ModelCard:
|
||||
errs.append(f"component {cid!r} lists tasks {overlap} as both required and optional")
|
||||
|
||||
if errs:
|
||||
raise CardValidationError(
|
||||
f"ModelCard {self.model_id!r} failed validation:\n - " + "\n - ".join(errs))
|
||||
raise CardValidationError(f"ModelCard {self.model_id!r} failed validation:\n - " + "\n - ".join(errs))
|
||||
return self
|
||||
|
||||
def loops_sharing(self, component_id: str) -> list[str]:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Deployment & fleet plane (design_v3 §14) — DeploymentCard, our own LocalFleet, Dynamo adapter.
|
||||
"""Deployment & fleet plane — DeploymentCard, our own LocalFleet, Dynamo adapter.
|
||||
|
||||
The engine exports a DeploymentCard; our LocalFleet routes over it (so we don't rely on Dynamo),
|
||||
and the DynamoWorkerAdapter exports the same card so Dynamo can front us too — one object, two
|
||||
@@ -10,6 +10,7 @@ from v2.deploy.card import DeploymentCard, HealthSchema, SLOSchema, build_deploy
|
||||
from v2.deploy.dynamo import DynamoWorkerAdapter, FakeDynamoRuntime
|
||||
from v2.deploy.fleet import LocalFleet, NoWorkerAvailable, Worker
|
||||
|
||||
__all__ = ["DeploymentCard", "HealthSchema", "SLOSchema", "build_deployment_card",
|
||||
"LocalFleet", "Worker", "NoWorkerAvailable",
|
||||
"DynamoWorkerAdapter", "FakeDynamoRuntime"]
|
||||
__all__ = [
|
||||
"DeploymentCard", "HealthSchema", "SLOSchema", "build_deployment_card", "LocalFleet", "Worker", "NoWorkerAvailable",
|
||||
"DynamoWorkerAdapter", "FakeDynamoRuntime"
|
||||
]
|
||||
|
||||
+22
-15
@@ -1,7 +1,8 @@
|
||||
"""DeploymentCard — what an engine exports to a fleet (design_v3 §14; designv2 Deployment plane).
|
||||
"""DeploymentCard — what an engine exports to a fleet.
|
||||
|
||||
> The engine exports a ``DeploymentCard`` and lets a fleet orchestrator route. ... The cost model is
|
||||
> the SAME §6 object — one object, two consumers (the scheduler's budget and the fleet's routing input).
|
||||
The engine exports a ``DeploymentCard`` and lets a fleet orchestrator route. The cost model is the
|
||||
SAME object the scheduler budgets with — one object, two consumers (the scheduler's budget and the
|
||||
fleet's routing input).
|
||||
|
||||
This is the contract our OWN fleet (``deploy/fleet.py``) consumes AND the Dynamo adapter
|
||||
(``deploy/dynamo.py``) exports — so we are frontable by Dynamo without depending on it.
|
||||
@@ -10,7 +11,6 @@ from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from v2._enums import Capability
|
||||
from v2.card import CostModel
|
||||
@@ -18,14 +18,14 @@ from v2.card import CostModel
|
||||
|
||||
@dataclass
|
||||
class HealthSchema:
|
||||
status: str = "healthy" # "healthy" | "draining" | "unhealthy"
|
||||
status: str = "healthy" # "healthy" | "draining" | "unhealthy"
|
||||
in_flight: int = 0
|
||||
queue_depth: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class SLOSchema:
|
||||
slo_class: str = "standard" # "latency" | "throughput" | "cost"
|
||||
slo_class: str = "standard" # "latency" | "throughput" | "cost"
|
||||
max_concurrent: int = 8
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ class DeploymentCard:
|
||||
engine_id: str
|
||||
model_cards: list[str] = field(default_factory=list)
|
||||
capabilities: frozenset[Capability] = frozenset()
|
||||
role_pools: list = field(default_factory=list) # list[RolePoolSpec]
|
||||
role_pools: list = field(default_factory=list) # list[RolePoolSpec]
|
||||
supported_programs: list[str] = field(default_factory=list)
|
||||
cost_model: CostModel | None = None # the SAME §6 cost model
|
||||
cost_model: CostModel | None = None # the SAME cost model the scheduler uses
|
||||
health: HealthSchema = field(default_factory=HealthSchema)
|
||||
slo: SLOSchema = field(default_factory=SLOSchema)
|
||||
|
||||
@@ -44,10 +44,14 @@ class DeploymentCard:
|
||||
return model_id in self.model_cards
|
||||
|
||||
|
||||
def build_deployment_card(engine_id: str, model_cards: list, *, max_concurrent: int = 8,
|
||||
slo_class: str = "standard", role_pools: list | None = None,
|
||||
def build_deployment_card(engine_id: str,
|
||||
model_cards: list,
|
||||
*,
|
||||
max_concurrent: int = 8,
|
||||
slo_class: str = "standard",
|
||||
role_pools: list | None = None,
|
||||
supported_programs: list[str] | None = None) -> DeploymentCard:
|
||||
"""Export a DeploymentCard from the model cards an engine serves (design_v3 §14).
|
||||
"""Export a DeploymentCard from the model cards an engine serves.
|
||||
|
||||
Picks a representative ``step_cost_model`` so the fleet/Dynamo route on the SAME cost object the
|
||||
scheduler budgets with."""
|
||||
@@ -64,7 +68,10 @@ def build_deployment_card(engine_id: str, model_cards: list, *, max_concurrent:
|
||||
cost_model = dataclasses.replace(lp.step_cost_model,
|
||||
coefficients=dict(lp.step_cost_model.coefficients))
|
||||
break
|
||||
return DeploymentCard(
|
||||
engine_id=engine_id, model_cards=ids, capabilities=frozenset(caps),
|
||||
role_pools=role_pools or [], supported_programs=supported_programs or [],
|
||||
cost_model=cost_model, slo=SLOSchema(slo_class=slo_class, max_concurrent=max_concurrent))
|
||||
return DeploymentCard(engine_id=engine_id,
|
||||
model_cards=ids,
|
||||
capabilities=frozenset(caps),
|
||||
role_pools=role_pools or [],
|
||||
supported_programs=supported_programs or [],
|
||||
cost_model=cost_model,
|
||||
slo=SLOSchema(slo_class=slo_class, max_concurrent=max_concurrent))
|
||||
|
||||
+20
-16
@@ -1,11 +1,11 @@
|
||||
"""Dynamo worker adapter (design_v3 §14; design.md §6.3.5) — Dynamo as an *option*, not a dependency.
|
||||
"""Dynamo worker adapter — Dynamo as an *option*, not a dependency.
|
||||
|
||||
> NVIDIA Dynamo is the named first-class partner for the fleet layer ... the engine's job is to be an
|
||||
> excellent Dynamo citizen: registration, health/drain, cost metrics, affinity events.
|
||||
NVIDIA Dynamo is the named first-class partner for the fleet layer; the engine's job is to be a good
|
||||
Dynamo citizen: registration, health/drain, cost metrics, affinity events.
|
||||
|
||||
This adapter exposes exactly that contract over an AsyncEngine + DeploymentCard, so Dynamo CAN front
|
||||
this engine. Crucially, it consumes the **same** DeploymentCard + cost model as our own LocalFleet —
|
||||
one object, two consumers — so choosing Dynamo vs. our fleet is a deployment decision, not a rewrite.
|
||||
this engine. It consumes the same DeploymentCard + cost model as our own LocalFleet — one object, two
|
||||
consumers — so choosing Dynamo vs. our fleet is a deployment decision, not a rewrite.
|
||||
``FakeDynamoRuntime`` proves the contract is satisfiable end-to-end without importing Dynamo.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -17,7 +17,7 @@ from v2.deploy.card import DeploymentCard
|
||||
|
||||
|
||||
class DynamoWorkerAdapter:
|
||||
"""Implements the Dynamo worker surface (design.md §6.3.5 work items 1–4)."""
|
||||
"""Implements the Dynamo worker surface: registration, metrics, affinity, handle."""
|
||||
|
||||
def __init__(self, engine: Any, card: DeploymentCard, *, worker_type: str = "Aggregated"):
|
||||
self.engine = engine
|
||||
@@ -30,19 +30,23 @@ class DynamoWorkerAdapter:
|
||||
def registration(self) -> dict[str, Any]:
|
||||
self.registered = True
|
||||
return {
|
||||
"engine_id": self.card.engine_id,
|
||||
"engine_id":
|
||||
self.card.engine_id,
|
||||
"model_type": ["Videos", "Images"] + (["Chat"] if any("omni" in m or "cosmos" in m or "bagel" in m
|
||||
for m in self.card.model_cards) else []),
|
||||
"worker_type": self.worker_type,
|
||||
"models": list(self.card.model_cards),
|
||||
"capabilities": sorted(c.value for c in self.card.capabilities),
|
||||
"supported_programs": list(self.card.supported_programs),
|
||||
for m in self.card.model_cards) else []),
|
||||
"worker_type":
|
||||
self.worker_type,
|
||||
"models":
|
||||
list(self.card.model_cards),
|
||||
"capabilities":
|
||||
sorted(c.value for c in self.card.capabilities),
|
||||
"supported_programs":
|
||||
list(self.card.supported_programs),
|
||||
}
|
||||
|
||||
# 2) metrics for routing + the SLA Planner (the SAME §6 cost model)
|
||||
# 2) metrics for routing + the SLA Planner (the SAME cost model the scheduler uses)
|
||||
def metrics(self) -> dict[str, Any]:
|
||||
return {"in_flight": self.engine.in_flight, "queue_depth": self.engine.queue_depth,
|
||||
"draining": self.draining}
|
||||
return {"in_flight": self.engine.in_flight, "queue_depth": self.engine.queue_depth, "draining": self.draining}
|
||||
|
||||
def cost_estimate(self, request: Any) -> float:
|
||||
cm = self.card.cost_model
|
||||
@@ -57,7 +61,7 @@ class DynamoWorkerAdapter:
|
||||
def drain(self) -> None:
|
||||
self.draining = True
|
||||
|
||||
# 3) affinity / cache events (KvCacheEventData shape) — checkpoint/session residency (ask A1)
|
||||
# 3) affinity / cache events (KvCacheEventData shape) — checkpoint/session residency
|
||||
def cache_event(self, kind: str, key: str) -> dict[str, Any]:
|
||||
return {"event": kind, "engine_id": self.card.engine_id, "key": key}
|
||||
|
||||
|
||||
+16
-14
@@ -1,15 +1,15 @@
|
||||
"""LocalFleet — OUR OWN fleet router (design_v3 §14; design.md §6.3.5/6.3.6 fallbacks).
|
||||
"""LocalFleet — OUR OWN fleet router.
|
||||
|
||||
design.md names Dynamo as the first-class fleet partner, but every Dynamo ask (§6.3.6) has a
|
||||
*first-class fallback* — and this is it: a self-contained fleet that does discovery, health/drain,
|
||||
and routing (least-loaded / cost-model / affinity) over multiple engine workers, so we are never
|
||||
*reliant* on Dynamo. The router's cost input is the SAME §6 cost model the scheduler uses (one
|
||||
object, two consumers). Affinity routing is the A1 fallback (sticky-by-key for checkpoint/session
|
||||
residency, "least-loaded + engine redirects").
|
||||
Dynamo is the first-class fleet partner, but every Dynamo ask has a first-class fallback — and this
|
||||
is it: a self-contained fleet that does discovery, health/drain, and routing (least-loaded /
|
||||
cost-model / affinity) over multiple engine workers, so we are never *reliant* on Dynamo. The
|
||||
router's cost input is the SAME cost model the scheduler uses (one object, two consumers). Affinity
|
||||
routing is sticky-by-key for checkpoint/session residency (least-loaded + engine redirects).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, AsyncIterator
|
||||
from typing import Any
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from v2.request.artifacts import Output
|
||||
from v2.deploy.card import DeploymentCard, HealthSchema
|
||||
@@ -40,7 +40,7 @@ class Worker:
|
||||
return self.engine.serves(model_id) or self.card.serves(model_id)
|
||||
|
||||
def cost_estimate(self, request: Any) -> float:
|
||||
"""Predicted GPU-time for this request — the §6 cost model as the fleet's routing input."""
|
||||
"""Predicted GPU-time for this request — the cost model as the fleet's routing input."""
|
||||
cm = self.card.cost_model
|
||||
steps = max(1, int(getattr(request.diffusion, "num_steps", 1) or 1))
|
||||
work = max(1, int(getattr(request.diffusion, "height", 1)) * int(getattr(request.diffusion, "width", 1)))
|
||||
@@ -49,16 +49,18 @@ class Worker:
|
||||
|
||||
def health(self) -> HealthSchema:
|
||||
return HealthSchema(status=("draining" if self.draining else "healthy"),
|
||||
in_flight=self.engine.in_flight, queue_depth=self.engine.queue_depth)
|
||||
in_flight=self.engine.in_flight,
|
||||
queue_depth=self.engine.queue_depth)
|
||||
|
||||
|
||||
class LocalFleet:
|
||||
|
||||
def __init__(self, policy: str = "least_loaded", *, max_affinity: int = 100_000):
|
||||
assert policy in ("least_loaded", "cost", "affinity")
|
||||
self.policy = policy
|
||||
self.max_affinity = max_affinity
|
||||
self.workers: dict[str, Worker] = {}
|
||||
self._affinity: dict[str, str] = {} # affinity key -> worker_id (sticky), FIFO-bounded
|
||||
self._affinity: dict[str, str] = {} # affinity key -> worker_id (sticky), FIFO-bounded
|
||||
|
||||
# --- discovery / health (what Dynamo's registry + planner would do) ------ #
|
||||
def register(self, worker_id: str, engine: Any, card: DeploymentCard) -> Worker:
|
||||
@@ -89,14 +91,14 @@ class LocalFleet:
|
||||
wid = self._affinity.get(key)
|
||||
if wid in self.workers and self.workers[wid] in cands:
|
||||
return self.workers[wid]
|
||||
chosen = min(cands, key=lambda w: w.load) # cold key → least-loaded, then pin
|
||||
if len(self._affinity) >= self.max_affinity: # FIFO-bound the sticky map
|
||||
chosen = min(cands, key=lambda w: w.load) # cold key → least-loaded, then pin
|
||||
if len(self._affinity) >= self.max_affinity: # FIFO-bound the sticky map
|
||||
self._affinity.pop(next(iter(self._affinity)), None)
|
||||
self._affinity[key] = chosen.worker_id
|
||||
return chosen
|
||||
if self.policy == "cost":
|
||||
return min(cands, key=lambda w: w.cost_estimate(request) * (1.0 + w.load))
|
||||
return min(cands, key=lambda w: w.load) # least_loaded (default)
|
||||
return min(cands, key=lambda w: w.load) # least_loaded (default)
|
||||
|
||||
# --- serving (delegates to the chosen worker's engine) ------------------- #
|
||||
async def generate(self, request: Any, *, affinity_key: str | None = None) -> Output:
|
||||
|
||||
+24
-18
@@ -1,4 +1,4 @@
|
||||
"""Worked examples (design_v3 §15), runnable as a demo:
|
||||
"""Worked examples, runnable as a demo:
|
||||
|
||||
python3 -m v2.examples
|
||||
|
||||
@@ -6,8 +6,6 @@ Each prints what it demonstrates. This doubles as living documentation of the AP
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from v2.recipes import build_default_engine, build_omni_engine
|
||||
from v2.recipes.wan21 import build_wan21_card
|
||||
from v2.parity import assert_interleave_parity
|
||||
@@ -16,8 +14,7 @@ from v2.training import build_diffusion_nft
|
||||
|
||||
|
||||
def _t2v(mid, prompt, seed, steps=4, **kw):
|
||||
return make_request(TaskType.T2V, mid, prompt,
|
||||
diffusion=DiffusionParams(num_steps=steps, seed=seed), **kw)
|
||||
return make_request(TaskType.T2V, mid, prompt, diffusion=DiffusionParams(num_steps=steps, seed=seed), **kw)
|
||||
|
||||
|
||||
def example_a_text_to_video(eng) -> None:
|
||||
@@ -36,8 +33,8 @@ def example_b_ltx2_two_stage(eng) -> None:
|
||||
|
||||
def example_c_causal_streaming(eng) -> None:
|
||||
print("\n(c) Causal streaming (Wan-causal): chunk rollout + slab-KV, streamable by chunk")
|
||||
out = eng.run(_t2v("wan-causal-sf-1.3b", "a drone flight over mountains", 3,
|
||||
outputs=OutputSpec(stream={"video": True})))
|
||||
out = eng.run(
|
||||
_t2v("wan-causal-sf-1.3b", "a drone flight over mountains", 3, outputs=OutputSpec(stream={"video": True})))
|
||||
print(f" latents {out.artifacts['latents'].latent.shape} chunks={out.metrics['chunks']:.0f} "
|
||||
f"streamed_chunks={out.metrics.get('stream_chunks', 0)}")
|
||||
|
||||
@@ -63,14 +60,20 @@ def example_d_rl_rollout() -> None:
|
||||
def example_g_omni_mot() -> None:
|
||||
print("\n(g) Omni / MoT (§16): ONE resident instance runs AR + diffusion loops on shared weights")
|
||||
eng = build_omni_engine()
|
||||
o = eng.run(make_request(TaskType.T2V, "cosmos3-vfm", "a phoenix",
|
||||
sampling=SamplingParams(max_tokens=6, seed=1),
|
||||
diffusion=DiffusionParams(num_steps=4, seed=1)))
|
||||
o = eng.run(
|
||||
make_request(TaskType.T2V,
|
||||
"cosmos3-vfm",
|
||||
"a phoenix",
|
||||
sampling=SamplingParams(max_tokens=6, seed=1),
|
||||
diffusion=DiffusionParams(num_steps=4, seed=1)))
|
||||
print(f" Cosmos3 (reason→joint denoise): text={o.artifacts['text'].text} "
|
||||
f"video={o.artifacts['video'].frames.shape}")
|
||||
o2 = eng.run(make_request(TaskType.T2I, "bagel-mot", "a teapot",
|
||||
sampling=SamplingParams(max_tokens=6, seed=2),
|
||||
diffusion=DiffusionParams(num_steps=4, seed=2)))
|
||||
o2 = eng.run(
|
||||
make_request(TaskType.T2I,
|
||||
"bagel-mot",
|
||||
"a teapot",
|
||||
sampling=SamplingParams(max_tokens=6, seed=2),
|
||||
diffusion=DiffusionParams(num_steps=4, seed=2)))
|
||||
print(f" BAGEL (generate_text→generate_image): text={o2.artifacts['text'].text} "
|
||||
f"image={o2.artifacts['image'].tensor.shape}")
|
||||
print(f" scheduler priced BOTH WorkUnit kinds (runtime-visible, not one opaque stage): "
|
||||
@@ -94,8 +97,8 @@ async def _serving_demo() -> None:
|
||||
pools = PoolSet(wan_t2v_disaggregated(), card)
|
||||
pools.warmup()
|
||||
ae.register_disaggregated("wan-disagg", pools, build_wan_t2v_program())
|
||||
out = await ae.generate(make_request(TaskType.T2V, "wan-disagg", "a wave",
|
||||
diffusion=DiffusionParams(num_steps=4, seed=1)))
|
||||
out = await ae.generate(
|
||||
make_request(TaskType.T2V, "wan-disagg", "a wave", diffusion=DiffusionParams(num_steps=4, seed=1)))
|
||||
print(f" disaggregated T2V (enc→den→dec): video={out.artifacts['video'].frames.shape} "
|
||||
f"cross-pool transfers={out.metrics['transfers']:.0f}")
|
||||
|
||||
@@ -107,14 +110,17 @@ async def _serving_demo() -> None:
|
||||
r, w = await asyncio.open_connection(host, port)
|
||||
w.write(f"{method} {path} HTTP/1.1\r\nHost: x\r\nContent-Length: {len(body)}\r\n\r\n".encode() + body)
|
||||
await w.drain()
|
||||
data = await r.read(); w.close()
|
||||
data = await r.read()
|
||||
w.close()
|
||||
return data.decode("utf-8", "replace")
|
||||
|
||||
health = await http("GET", "/health")
|
||||
sse = await http("POST", "/v1/chat/completions",
|
||||
b'{"model":"cosmos3-vfm","messages":[{"role":"user","content":"a comet"}],"stream":true}')
|
||||
n_chunks = sse.count("data: ")
|
||||
print(f" OpenAI server: /health ok={'healthy' in health}; chat SSE streamed {n_chunks} chunks (omni reason→denoise)")
|
||||
print(
|
||||
f" OpenAI server: /health ok={'healthy' in health}; chat SSE streamed {n_chunks} chunks (omni reason→denoise)"
|
||||
)
|
||||
await server.close()
|
||||
|
||||
# our own fleet router + Dynamo adapter (frontable, not relied upon) — same DeploymentCard
|
||||
@@ -136,7 +142,7 @@ def example_h_serving_and_fleet() -> None:
|
||||
|
||||
def main() -> None:
|
||||
print("=" * 78)
|
||||
print("v2 — worked examples (design_v3 §6,§13-16). One runtime, many loops.")
|
||||
print("v2 — worked examples. One runtime, many loops.")
|
||||
print("=" * 78)
|
||||
eng = build_default_engine()
|
||||
print(f"registered (recipe, runtime) cards: {list(eng._registry)}")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Extension plane — observers (read-only) and interceptors (compute-altering) (design_v3 §11)."""
|
||||
"""Extension plane — observers (read-only) and interceptors (compute-altering)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2.extend.base import Interceptor, InterceptorChain, InterceptorConflict, Observer, ObserverBus
|
||||
@@ -6,5 +6,7 @@ from v2.extend.interceptors import ResidualSkipInterceptor
|
||||
from v2.extend.observers import NaNWatch, Profiler
|
||||
from v2.extend.registry import PluginRegistry
|
||||
|
||||
__all__ = ["Observer", "Interceptor", "ObserverBus", "InterceptorChain", "InterceptorConflict",
|
||||
"Profiler", "NaNWatch", "ResidualSkipInterceptor", "PluginRegistry"]
|
||||
__all__ = [
|
||||
"Observer", "Interceptor", "ObserverBus", "InterceptorChain", "InterceptorConflict", "Profiler", "NaNWatch",
|
||||
"ResidualSkipInterceptor", "PluginRegistry"
|
||||
]
|
||||
|
||||
+14
-10
@@ -1,6 +1,6 @@
|
||||
"""Observers and interceptors (design_v3 §11) — the optimization/debug/parity surface.
|
||||
"""Observers and interceptors — the optimization/debug/parity surface.
|
||||
|
||||
They compose with §5 cleanly: the hooks wrap ``ctx.execute(plan)``.
|
||||
They compose with the loop cleanly: the hooks wrap ``ctx.execute(plan)``.
|
||||
* Observers (read-only): cannot mutate state. An unused hook is *literally absent* from
|
||||
the hot path (the bus only iterates when observers are attached).
|
||||
* Interceptors (compute-altering): ``before_step`` may supply a cached prediction (skip);
|
||||
@@ -18,7 +18,9 @@ from typing import Any, Protocol, runtime_checkable
|
||||
|
||||
@runtime_checkable
|
||||
class Observer(Protocol):
|
||||
def observe(self, event: str, **kw) -> None: ...
|
||||
|
||||
def observe(self, event: str, **kw) -> None:
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@@ -27,8 +29,11 @@ class Interceptor(Protocol):
|
||||
distribution_altering: bool
|
||||
graph_safe: bool
|
||||
|
||||
def before_step(self, plan: Any, state: Any) -> Any | None: ... # return override output to skip, or None
|
||||
def after_step(self, plan: Any, state: Any, result: Any) -> None: ...
|
||||
def before_step(self, plan: Any, state: Any) -> Any | None:
|
||||
... # return override output to skip, or None
|
||||
|
||||
def after_step(self, plan: Any, state: Any, result: Any) -> None:
|
||||
...
|
||||
|
||||
|
||||
class ObserverBus:
|
||||
@@ -45,7 +50,7 @@ class ObserverBus:
|
||||
return bool(self._observers)
|
||||
|
||||
def emit(self, event: str, **kw) -> None:
|
||||
if not self._observers: # absent from the hot path when off
|
||||
if not self._observers: # absent from the hot path when off
|
||||
return
|
||||
for obs in self._observers:
|
||||
obs.observe(event, **kw)
|
||||
@@ -56,7 +61,7 @@ class InterceptorConflict(ValueError):
|
||||
|
||||
|
||||
class InterceptorChain:
|
||||
"""Ordered interceptor chain; conflicting interceptors rejected pre-flight (§11)."""
|
||||
"""Ordered interceptor chain; conflicting interceptors rejected pre-flight."""
|
||||
|
||||
def __init__(self, interceptors: list[Interceptor] | None = None, *, exact_mode: bool = False):
|
||||
self._chain = list(interceptors or [])
|
||||
@@ -65,9 +70,8 @@ class InterceptorChain:
|
||||
def _validate(self, exact_mode: bool) -> None:
|
||||
skippers = [i for i in self._chain if getattr(i, "distribution_altering", False)]
|
||||
if len(skippers) > 1:
|
||||
raise InterceptorConflict(
|
||||
f"multiple distribution-altering interceptors {[i.plugin_id for i in skippers]} "
|
||||
"conflict — only one step-skipper allowed (design_v3 §11)")
|
||||
raise InterceptorConflict(f"multiple distribution-altering interceptors {[i.plugin_id for i in skippers]} "
|
||||
"conflict — only one step-skipper allowed")
|
||||
if exact_mode and skippers:
|
||||
raise InterceptorConflict(
|
||||
f"exact-mode rejects distribution_altering interceptors {[i.plugin_id for i in skippers]}")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""cache-dit-style interceptors (design_v3 §11).
|
||||
"""cache-dit-style interceptors.
|
||||
|
||||
``ResidualSkipInterceptor`` is the reference step-skip integration. The load-bearing
|
||||
correctness property: its per-step state lives in ``LoopState.plugin_state[id][branch]``,
|
||||
keyed per request AND per CFG branch — NOT a module global. This is exactly why the
|
||||
interleave gate (§9.3) passes with it on: two interleaved requests have disjoint plugin
|
||||
interleave gate passes with it on: two interleaved requests have disjoint plugin
|
||||
state, so neither smears the other's cached prediction.
|
||||
|
||||
A 4-step distilled card *rejects* this interceptor (capability negotiation) rather than
|
||||
|
||||
+11
-14
@@ -1,32 +1,29 @@
|
||||
"""Built-in read-only observers (design_v3 §11): Profiler, NaNWatch.
|
||||
"""Built-in read-only observers: Profiler, NaNWatch.
|
||||
|
||||
ParityAligner lives in ``parity/`` (it is a first-class package, §18) but also implements
|
||||
the Observer ``observe`` protocol so it attaches to the same bus.
|
||||
ParityAligner lives in its own ``parity/`` package but also implements the Observer ``observe``
|
||||
protocol so it attaches to the same bus.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Profiler:
|
||||
"""Per-step wall/CUDA timing; calibrates the cost model (design_v3 §6.1, §11).
|
||||
"""Per-step wall/CUDA timing that calibrates the cost model.
|
||||
|
||||
Here it accumulates (work_units, actual_seconds) samples and can fit a card's
|
||||
CostModel coefficients — the online calibration the conservative baseline refines.
|
||||
Accumulates (work_units, actual_seconds) samples and fits a card's CostModel
|
||||
coefficients — the online calibration that refines the conservative baseline.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.samples: list[tuple[str, float, float]] = [] # (batch_key_repr, work_units, seconds)
|
||||
self.samples: list[tuple[str, float, float]] = [] # (batch_key_repr, work_units, seconds)
|
||||
|
||||
def observe(self, event: str, **kw) -> None:
|
||||
if event == "step_complete":
|
||||
plan, result = kw.get("plan"), kw.get("result")
|
||||
if plan is not None and result is not None:
|
||||
self.samples.append((repr(plan.shape_sig.batch_key),
|
||||
float(plan.shape_sig.work_units),
|
||||
float(result.actual_seconds)))
|
||||
self.samples.append(
|
||||
(repr(plan.shape_sig.batch_key), float(plan.shape_sig.work_units), float(result.actual_seconds)))
|
||||
|
||||
def calibrate(self, cost_model) -> None:
|
||||
"""Fit base + per_unit seconds from observed samples (least squares)."""
|
||||
@@ -43,10 +40,10 @@ class Profiler:
|
||||
|
||||
|
||||
class NaNWatch:
|
||||
"""First-NaN/Inf localization (design_v3 §11). Request-fatal → SPMD-consistent abort (§6.4)."""
|
||||
"""First-NaN/Inf localization. Request-fatal, triggering an SPMD-consistent abort."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.first: tuple[str, int] | None = None # (tap/output name, step)
|
||||
self.first: tuple[str, int] | None = None # (tap/output name, step)
|
||||
|
||||
def observe(self, event: str, **kw) -> None:
|
||||
if event == "step_complete" and self.first is None:
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
"""Plugin registry + trust boundary (design_v3 §11).
|
||||
"""Plugin registry + trust boundary.
|
||||
|
||||
> Plugins are enabled at deploy scope only (never a per-request ``plugins=[...]`` field
|
||||
> that would wire third-party code selection into the public API); requests only
|
||||
> *parameterize* pre-enabled plugins through validated schemas.
|
||||
Plugins are enabled at deploy scope only (never a per-request ``plugins=[...]`` field that
|
||||
would wire third-party code selection into the public API); requests only *parameterize*
|
||||
pre-enabled plugins through validated schemas.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
from collections.abc import Callable
|
||||
|
||||
from v2.extend.base import InterceptorChain, Observer
|
||||
|
||||
|
||||
class PluginRegistry:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._interceptors: dict[str, Callable[..., Any]] = {}
|
||||
self._observers: dict[str, Callable[..., Observer]] = {}
|
||||
self._enabled: set[str] = set() # deploy-scope enablement
|
||||
self._enabled: set[str] = set() # deploy-scope enablement
|
||||
|
||||
def register_interceptor(self, plugin_id: str, factory: Callable[..., Any]) -> None:
|
||||
self._interceptors[plugin_id] = factory
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# STUB: re-exports fastvideo until vendored (see memory: v2-vendoring-approach).
|
||||
"""``FastVideoArgs`` facade — the args object the component loaders consume. Re-exported so v2 code imports
|
||||
``v2.fastvideo_args``; a vendored cutover slims it to ~250 LOC of inference-only args."""
|
||||
``v2.fastvideo_args``; a vendored cutover slims it to inference-only args."""
|
||||
from fastvideo.fastvideo_args import FastVideoArgs # noqa: F401
|
||||
|
||||
+21
-5
@@ -1,4 +1,4 @@
|
||||
"""Loop plane — driven loops + policies (design_v3 §5)."""
|
||||
"""Loop plane — driven loops + policies."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2._enums import LoopKind, WorkUnitKind
|
||||
@@ -21,8 +21,24 @@ from v2.loop.driver import LoopRunner
|
||||
from v2.loop.sampler import add_noise, build_flow_sigmas, flow_match_euler_step, x0_from_velocity
|
||||
|
||||
__all__ = [
|
||||
"Loop", "LoopContext", "LoopState", "LoopResult", "WorkPlan", "Done", "StepResult",
|
||||
"StepContext", "ShapeSignature", "ResourceRequest", "CachePlan", "CacheOp", "PlacementHint",
|
||||
"LoopRunner", "LoopKind", "WorkUnitKind",
|
||||
"build_flow_sigmas", "flow_match_euler_step", "x0_from_velocity", "add_noise",
|
||||
"Loop",
|
||||
"LoopContext",
|
||||
"LoopState",
|
||||
"LoopResult",
|
||||
"WorkPlan",
|
||||
"Done",
|
||||
"StepResult",
|
||||
"StepContext",
|
||||
"ShapeSignature",
|
||||
"ResourceRequest",
|
||||
"CachePlan",
|
||||
"CacheOp",
|
||||
"PlacementHint",
|
||||
"LoopRunner",
|
||||
"LoopKind",
|
||||
"WorkUnitKind",
|
||||
"build_flow_sigmas",
|
||||
"flow_match_euler_step",
|
||||
"x0_from_velocity",
|
||||
"add_noise",
|
||||
]
|
||||
|
||||
+48
-36
@@ -1,6 +1,4 @@
|
||||
"""The driven-loop contract (design_v3 §5) — the keystone.
|
||||
|
||||
A loop is a **serializable state machine the runtime drives**:
|
||||
"""The driven-loop contract — a serializable state machine the runtime drives:
|
||||
|
||||
state = loop.init(req, model_state, ctx)
|
||||
while True:
|
||||
@@ -11,13 +9,12 @@ A loop is a **serializable state machine the runtime drives**:
|
||||
for chunk in plan.emits: ctx.emit(chunk)
|
||||
return loop.finalize(state)
|
||||
|
||||
Two properties this contract buys (design_v3 §5.1):
|
||||
Two properties this contract buys:
|
||||
* content-adaptive steps are natural — ``next`` reads ``state``, which already folded
|
||||
in the previous ``StepResult`` via ``advance``;
|
||||
* cross-request state safety is *structural* — all per-request mutable state lives in
|
||||
``LoopState`` (incl. ``plugin_state`` per request/CFG-branch), never module globals,
|
||||
so interleaving requests through one ModelInstance cannot smear state. This is what
|
||||
the batch-of-N interleave gate (§9.3) verifies.
|
||||
so interleaving requests through one ModelInstance cannot smear state.
|
||||
|
||||
This module is pure stdlib (tensors typed as ``TensorLike``) so it imports no backend.
|
||||
"""
|
||||
@@ -36,11 +33,11 @@ from v2.request.streams import StreamChunk
|
||||
# --------------------------------------------------------------------------- #
|
||||
@dataclass(frozen=True)
|
||||
class ShapeSignature:
|
||||
"""Batch-compatibility + graph-capture key (design_v3 §6.2)."""
|
||||
"""Batch-compatibility + graph-capture key."""
|
||||
kind: WorkUnitKind
|
||||
dims: tuple[int, ...] = ()
|
||||
dtype: str = "float32"
|
||||
extra: tuple[tuple[str, Any], ...] = () # e.g. (("cfg","classic"),("expert","e0"))
|
||||
extra: tuple[tuple[str, Any], ...] = () # e.g. (("cfg","classic"),("expert","e0"))
|
||||
|
||||
@property
|
||||
def work_units(self) -> int:
|
||||
@@ -51,13 +48,13 @@ class ShapeSignature:
|
||||
|
||||
@property
|
||||
def batch_key(self) -> tuple:
|
||||
"""Two WorkPlans batch together iff their batch_keys are equal (§6.3 BatchScheduler)."""
|
||||
"""Two WorkPlans batch together iff their batch_keys are equal."""
|
||||
return (self.kind, self.dims, self.dtype, self.extra)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResourceRequest:
|
||||
"""Everything admission must reserve (design_v3 §6.2 admission rule)."""
|
||||
"""Everything admission must reserve."""
|
||||
compute_seconds: float = 0.0
|
||||
resident_bytes: int = 0
|
||||
peak_activation_bytes: int = 0
|
||||
@@ -69,7 +66,7 @@ class ResourceRequest:
|
||||
@dataclass
|
||||
class CacheOp:
|
||||
cache_class: str
|
||||
key: Any # CacheKey
|
||||
key: Any # CacheKey
|
||||
nbytes: int = 0
|
||||
|
||||
|
||||
@@ -91,7 +88,7 @@ class PlacementHint:
|
||||
# --------------------------------------------------------------------------- #
|
||||
@dataclass
|
||||
class WorkPlan:
|
||||
"""A typed description of the next step (design_v3 §6.2).
|
||||
"""A typed description of the next step.
|
||||
|
||||
``run`` is the kernel thunk built by ``next()`` but NOT called there (kernel-free
|
||||
planning). The runtime's ``ctx.execute`` calls it — possibly after batching with
|
||||
@@ -105,14 +102,14 @@ class WorkPlan:
|
||||
cache: CachePlan = field(default_factory=CachePlan)
|
||||
placement: PlacementHint = field(default_factory=PlacementHint)
|
||||
emits: list[StreamChunk] = field(default_factory=list)
|
||||
payload: dict[str, Any] = field(default_factory=dict) # inspectable inputs (debug/parity)
|
||||
payload: dict[str, Any] = field(default_factory=dict) # inspectable inputs (debug/parity)
|
||||
# the kernel thunk: run(model_instance, override=None) -> StepResult | dict.
|
||||
# Built by next() but NOT called there (kernel-free planning). ``override`` is an optional
|
||||
# interceptor-supplied forward result (e.g. a cached prediction); the step body still runs
|
||||
# the cheap solver step with it.
|
||||
run: Any = None
|
||||
label: str = ""
|
||||
# --- piecewise CUDA-graph capture (design_v3 §6.2; Path A) ----------------------------------- #
|
||||
# --- piecewise CUDA-graph capture ------------------------------------------------------------ #
|
||||
# ``capturable``: the model's static declaration that this step is safe to capture/replay — i.e.
|
||||
# no host RNG / data-dependent control flow inside the captured region. A stochastic step (the
|
||||
# FlowGRPO SDE rollout) sets this False, forcing the runtime to eager-break it.
|
||||
@@ -121,7 +118,7 @@ class WorkPlan:
|
||||
# the captured graph's *shape of computation*. Part of the capture key so a step with a different
|
||||
# branch set / expert never replays an incompatible graph. Empty ⇒ structure fixed by shape alone.
|
||||
graph_key: tuple = ()
|
||||
# The static-buffer capture form (design_v3 §6.2). A capturable step exposes its deterministic op
|
||||
# The static-buffer capture form. A capturable step exposes its deterministic op
|
||||
# structure as ``graph_fn(model, workspace) -> StepResult`` reading EVERY per-step-varying input
|
||||
# (latent, sigmas, conditioning) from ``workspace`` — never from closure — plus ``graph_inputs``,
|
||||
# the dict of those current values. The runtime allocates address-stable buffers once per key and
|
||||
@@ -136,22 +133,22 @@ class WorkPlan:
|
||||
class Done:
|
||||
"""Sentinel returned by ``next`` when the loop is finished. ``finalize`` produces the
|
||||
actual LoopResult; ``result`` here is optional (kept for loops that want to carry it)."""
|
||||
result: "LoopResult | None" = None
|
||||
result: LoopResult | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class StepResult:
|
||||
output: dict[str, Any] = field(default_factory=dict) # typed per-loop (e.g. {"noise_pred": ...})
|
||||
output: dict[str, Any] = field(default_factory=dict) # typed per-loop (e.g. {"noise_pred": ...})
|
||||
actual_seconds: float = 0.0
|
||||
cache_writes: list[CacheOp] = field(default_factory=list)
|
||||
behavior: Any = None # BehaviorRecord slice (rollout profile)
|
||||
behavior: Any = None # BehaviorRecord slice (rollout profile)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoopResult:
|
||||
outputs: dict[str, Any] = field(default_factory=dict) # final latents / tokens / artifacts
|
||||
outputs: dict[str, Any] = field(default_factory=dict) # final latents / tokens / artifacts
|
||||
metrics: dict[str, float] = field(default_factory=dict)
|
||||
behavior: Any = None # full BehaviorRecord (rollout)
|
||||
behavior: Any = None # full BehaviorRecord (rollout)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -159,23 +156,23 @@ class LoopResult:
|
||||
# --------------------------------------------------------------------------- #
|
||||
@dataclass
|
||||
class LoopState:
|
||||
"""All per-request mutable state lives here (design_v3 §5.1 structural safety)."""
|
||||
"""All per-request mutable state lives here (structural cross-request safety)."""
|
||||
loop_id: str
|
||||
instance_id: str
|
||||
request_id: str
|
||||
profile: ExecutionProfile = ExecutionProfile.SERVE
|
||||
step_idx: int = 0
|
||||
done: bool = False
|
||||
rng: Any = None # seeded numpy Generator (per request)
|
||||
rng: Any = None # seeded numpy Generator (per request)
|
||||
seed: int | None = None
|
||||
# common typed fields (resolved at init)
|
||||
latents: dict[str, TensorLike] = field(default_factory=dict)
|
||||
cond: dict[str, Any] = field(default_factory=dict) # conditioning written by ConditioningInjector
|
||||
cond: dict[str, Any] = field(default_factory=dict) # conditioning written by ConditioningInjector
|
||||
timesteps: list[float] = field(default_factory=list)
|
||||
sigmas: list[float] = field(default_factory=list)
|
||||
# per-model extension (Cosmos3PackedSeq, MatrixGameState, ...) — typed by LoopSpec.extension_schema
|
||||
extension: Any = None
|
||||
# interceptor/policy state, keyed per plugin id AND per CFG branch (design_v3 §5.1, §11)
|
||||
# interceptor/policy state, keyed per plugin id AND per CFG branch
|
||||
plugin_state: dict[str, Any] = field(default_factory=dict)
|
||||
cache_handles: dict[str, Any] = field(default_factory=dict)
|
||||
# capture buffers (rollout): trajectory of per-step records
|
||||
@@ -184,15 +181,15 @@ class LoopState:
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# StepContext — what policies read each step (design_v3 §6.2.3 rule 2) #
|
||||
# StepContext — what policies read each step #
|
||||
# --------------------------------------------------------------------------- #
|
||||
@dataclass
|
||||
class StepContext:
|
||||
step_idx: int
|
||||
timestep: float
|
||||
sigma: float
|
||||
branch: str = "cond" # current guidance branch
|
||||
active_expert_id: str | None = None # set by ExpertRouting; observed by AdaptiveGateCFG
|
||||
branch: str = "cond" # current guidance branch
|
||||
active_expert_id: str | None = None # set by ExpertRouting; observed by AdaptiveGateCFG
|
||||
sampler_coeffs: dict[str, float] = field(default_factory=dict)
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@@ -206,16 +203,31 @@ class LoopContext(Protocol):
|
||||
scheduler; the scheduler never sees the model's math."""
|
||||
profile: ExecutionProfile
|
||||
|
||||
def execute(self, plan: WorkPlan) -> StepResult: ... # THE INVERSION POINT
|
||||
def emit(self, chunk: StreamChunk) -> None: ...
|
||||
def check_cancel(self) -> None: ... # raises request.Cancelled at boundary
|
||||
def observe(self, event: str, **kw) -> None: ... # observer bus hook
|
||||
def execute(self, plan: WorkPlan) -> StepResult:
|
||||
... # THE INVERSION POINT
|
||||
|
||||
def emit(self, chunk: StreamChunk) -> None:
|
||||
...
|
||||
|
||||
def check_cancel(self) -> None:
|
||||
... # raises request.Cancelled at boundary
|
||||
|
||||
def observe(self, event: str, **kw) -> None:
|
||||
... # observer bus hook
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Loop(Protocol):
|
||||
"""The model-owned control flow (design_v3 §5.1). Four methods; ``next`` is kernel-free."""
|
||||
def init(self, req: Any, model: Any, ctx: LoopContext) -> LoopState: ...
|
||||
def next(self, state: LoopState) -> "WorkPlan | Done": ...
|
||||
def advance(self, state: LoopState, result: StepResult) -> LoopState: ...
|
||||
def finalize(self, state: LoopState) -> LoopResult: ...
|
||||
"""The model-owned control flow. Four methods; ``next`` is kernel-free."""
|
||||
|
||||
def init(self, req: Any, model: Any, ctx: LoopContext) -> LoopState:
|
||||
...
|
||||
|
||||
def next(self, state: LoopState) -> WorkPlan | Done:
|
||||
...
|
||||
|
||||
def advance(self, state: LoopState, result: StepResult) -> LoopState:
|
||||
...
|
||||
|
||||
def finalize(self, state: LoopState) -> LoopResult:
|
||||
...
|
||||
|
||||
+4
-4
@@ -1,10 +1,9 @@
|
||||
"""LoopRunner — the only place iteration lives (design_v3 §5.1).
|
||||
"""LoopRunner — the only place iteration lives.
|
||||
|
||||
This is the runtime's driver, factored so the engine can either run it to completion
|
||||
The runtime's driver, factored so the engine can either run it to completion
|
||||
(``run``) or advance it one step at a time (``peek`` + ``step``) to **interleave** the steps
|
||||
of concurrent requests. Per-request state is entirely in ``LoopState``; the runner holds no
|
||||
hidden iteration state beyond a cached pending plan, so interleaving is safe by construction
|
||||
(the property the §9.3 interleave gate verifies).
|
||||
hidden iteration state beyond a cached pending plan, so interleaving is safe by construction.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -14,6 +13,7 @@ from v2.loop.contracts import Done, LoopContext, LoopResult, LoopState, WorkPlan
|
||||
|
||||
|
||||
class LoopRunner:
|
||||
|
||||
def __init__(self, loop: Any, ctx: LoopContext, request: Any, model: Any):
|
||||
self.loop = loop
|
||||
self.ctx = ctx
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Policies — the default step decomposition (design_v3 §5.1, §6.2.3)."""
|
||||
"""Policies — the default step decomposition."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2.loop.policies.base import (
|
||||
@@ -20,7 +20,17 @@ from v2.loop.policies.cfg import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CFGPolicy", "ClassicCFG", "BatchedCFG", "EmbeddedGuidance", "AdaptiveGateCFG", "PerModalityCFG",
|
||||
"FlowShiftPolicy", "PrecisionPolicy", "ExpertRouting", "NoRouting", "BoundaryTimestepRouting",
|
||||
"ConditioningInjector", "PassthroughConditioning",
|
||||
"CFGPolicy",
|
||||
"ClassicCFG",
|
||||
"BatchedCFG",
|
||||
"EmbeddedGuidance",
|
||||
"AdaptiveGateCFG",
|
||||
"PerModalityCFG",
|
||||
"FlowShiftPolicy",
|
||||
"PrecisionPolicy",
|
||||
"ExpertRouting",
|
||||
"NoRouting",
|
||||
"BoundaryTimestepRouting",
|
||||
"ConditioningInjector",
|
||||
"PassthroughConditioning",
|
||||
]
|
||||
|
||||
+20
-15
@@ -1,12 +1,10 @@
|
||||
"""Policy base classes (design_v3 §5.1, §6.2.3) — the *default* step decomposition.
|
||||
"""Policy base classes — the *default* step decomposition.
|
||||
|
||||
> Policies (CFG, expert routing, precision, flow-shift, conditioning) are the default
|
||||
> decomposition that deletes duplication for the families that fit — never an admission
|
||||
> requirement.
|
||||
|
||||
A family whose math is braided ships a custom ``next``/``advance`` and uses these as a
|
||||
library (LTX-2's multi-pass guidance does exactly that). Policy *bindings* resolve at build;
|
||||
policy *state* is per-request in ``LoopState`` (the adaptive-gate cached delta).
|
||||
Policies (CFG, expert routing, precision, flow-shift, conditioning) delete duplication for
|
||||
the families that fit; they are never required. A family whose math is braided ships a custom
|
||||
``next``/``advance`` and uses these as a library (LTX-2's multi-pass guidance does exactly
|
||||
that). Policy *bindings* resolve at build; policy *state* is per-request in ``LoopState``
|
||||
(the adaptive-gate cached delta).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -23,7 +21,7 @@ from v2.loop.sampler import build_flow_sigmas
|
||||
# FlowShiftPolicy — resolution-bucket shift + sigma schedule #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class FlowShiftPolicy:
|
||||
"""config-driven flow-shift lookup (design_v3 §6.2.3; Wan 480p shift=3.0, 720p=5.0)."""
|
||||
"""config-driven flow-shift lookup (e.g. Wan 480p shift=3.0, 720p=5.0)."""
|
||||
|
||||
def __init__(self, shift: float = 3.0, bucket_lookup: dict[int, float] | None = None):
|
||||
self.shift = shift
|
||||
@@ -32,9 +30,12 @@ class FlowShiftPolicy:
|
||||
def shift_for(self, height: int = 0, width: int = 0) -> float:
|
||||
return self.bucket_lookup.get(height * width, self.shift)
|
||||
|
||||
def build_schedule(self, num_steps: int, height: int = 0, width: int = 0,
|
||||
def build_schedule(self,
|
||||
num_steps: int,
|
||||
height: int = 0,
|
||||
width: int = 0,
|
||||
sigmas: list[float] | None = None) -> np.ndarray:
|
||||
if sigmas is not None: # explicit distilled schedule (LTX-2)
|
||||
if sigmas is not None: # explicit distilled schedule (LTX-2)
|
||||
return np.asarray(sigmas, dtype=np.float64)
|
||||
return build_flow_sigmas(num_steps, shift=self.shift_for(height, width))
|
||||
|
||||
@@ -43,7 +44,7 @@ class FlowShiftPolicy:
|
||||
# PrecisionPolicy — autocast / scheduler-step dtype control #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class PrecisionPolicy:
|
||||
"""Replaces ``prefix=='Flux'`` autocast hacks + ``scheduler_step_in_fp32`` (§6.2.3)."""
|
||||
"""Replaces ``prefix=='Flux'`` autocast hacks + ``scheduler_step_in_fp32``."""
|
||||
|
||||
def __init__(self, compute_dtype: str = "float32", scheduler_step_in_fp32: bool = True):
|
||||
self.compute_dtype = compute_dtype
|
||||
@@ -61,8 +62,10 @@ class PrecisionPolicy:
|
||||
# ExpertRouting — Wan2.2 boundary-timestep transformer switch #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ExpertRouting(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def expert_for(self, ctx: StepContext) -> str: ...
|
||||
def expert_for(self, ctx: StepContext) -> str:
|
||||
...
|
||||
|
||||
|
||||
class NoRouting(ExpertRouting):
|
||||
@@ -76,7 +79,7 @@ class NoRouting(ExpertRouting):
|
||||
|
||||
|
||||
class BoundaryTimestepRouting(ExpertRouting):
|
||||
"""Wan2.2 ``boundary_ratio`` switch between two transformers (design_v3 §6.2.3)."""
|
||||
"""Wan2.2 ``boundary_ratio`` switch between two transformers."""
|
||||
|
||||
def __init__(self, high_noise: str, low_noise: str, boundary: float = 0.5):
|
||||
self.high_noise = high_noise
|
||||
@@ -91,8 +94,10 @@ class BoundaryTimestepRouting(ExpertRouting):
|
||||
# ConditioningInjector — writes RequestState.cond; the loop stays agnostic #
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ConditioningInjector(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def inject(self, state: Any, request: Any) -> None: ...
|
||||
def inject(self, state: Any, request: Any) -> None:
|
||||
...
|
||||
|
||||
|
||||
class PassthroughConditioning(ConditioningInjector):
|
||||
|
||||
+12
-14
@@ -1,11 +1,11 @@
|
||||
"""CFGPolicy — one taxonomy over one shared denoise body (design_v3 §5.3).
|
||||
"""CFGPolicy — one taxonomy over one shared denoise body.
|
||||
|
||||
> Batched-vs-two-forward is a *dispatch detail inside one policy*, not a separate mechanism.
|
||||
Batched-vs-two-forward is a dispatch detail inside one policy, not a separate mechanism.
|
||||
|
||||
The step body asks ``branches_this_step(ctx, state)`` which forwards to run, runs them, then
|
||||
calls ``combine(preds, scale, ctx, state)``. Per-request mutable state (the adaptive-gate
|
||||
cached delta) lives in the ``state`` dict, which the step body slices out of
|
||||
``LoopState.plugin_state`` — never a module global (the §5.1 safety property).
|
||||
``LoopState.plugin_state`` — never a module global.
|
||||
|
||||
cfg-parallel is a *parallelism axis* (not a policy); companions are an *orchestrator pattern*
|
||||
(not in the loop). Both compose with any policy here.
|
||||
@@ -13,7 +13,6 @@ cfg-parallel is a *parallelism axis* (not a policy); companions are an *orchestr
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -21,15 +20,15 @@ from v2.loop.contracts import StepContext
|
||||
|
||||
|
||||
class CFGPolicy(ABC):
|
||||
batched: bool = False # dispatch detail: stack branches into one forward
|
||||
branch_vocabulary: list[str] = ["cond", "uncond"] # full declared set (for per-branch plugin state)
|
||||
batched: bool = False # dispatch detail: stack branches into one forward
|
||||
branch_vocabulary: list[str] = ["cond", "uncond"] # full declared set (for per-branch plugin state)
|
||||
|
||||
def branches_this_step(self, ctx: StepContext, state: dict) -> list[str]:
|
||||
return list(self.branch_vocabulary)
|
||||
|
||||
@abstractmethod
|
||||
def combine(self, preds: dict[str, np.ndarray], guidance_scale: float,
|
||||
ctx: StepContext, state: dict) -> np.ndarray: ...
|
||||
def combine(self, preds: dict[str, np.ndarray], guidance_scale: float, ctx: StepContext, state: dict) -> np.ndarray:
|
||||
...
|
||||
|
||||
|
||||
class ClassicCFG(CFGPolicy):
|
||||
@@ -52,7 +51,7 @@ class BatchedCFG(ClassicCFG):
|
||||
class EmbeddedGuidance(CFGPolicy):
|
||||
"""Degenerate single-branch identity-combine (Flux): guidance rides in the forward kwarg.
|
||||
|
||||
This is *not* 'no CFG' — it is kept inside the same abstraction (design_v3 §5.3)."""
|
||||
This is *not* 'no CFG' — it is kept inside the same abstraction."""
|
||||
branch_vocabulary = ["cond"]
|
||||
|
||||
def combine(self, preds, guidance_scale, ctx, state):
|
||||
@@ -60,7 +59,7 @@ class EmbeddedGuidance(CFGPolicy):
|
||||
|
||||
|
||||
class AdaptiveGateCFG(CFGPolicy):
|
||||
"""Cached-delta reuse with expert-switch self-invalidation (design_v3 §5.1 canonical state).
|
||||
"""Cached-delta reuse with expert-switch self-invalidation.
|
||||
|
||||
On reuse steps it runs ONLY the cond branch and reuses the cached delta:
|
||||
``out = cond + (s−1)·delta`` (algebraically identical to ``uncond + s·(cond−uncond)``).
|
||||
@@ -71,20 +70,19 @@ class AdaptiveGateCFG(CFGPolicy):
|
||||
self.interval = max(1, interval)
|
||||
|
||||
def _recompute(self, ctx, state) -> bool:
|
||||
return (ctx.step_idx % self.interval == 0
|
||||
or "delta" not in state
|
||||
return (ctx.step_idx % self.interval == 0 or "delta" not in state
|
||||
or state.get("expert") != ctx.active_expert_id)
|
||||
|
||||
def branches_this_step(self, ctx, state):
|
||||
return ["cond", "uncond"] if self._recompute(ctx, state) else ["cond"]
|
||||
|
||||
def combine(self, preds, guidance_scale, ctx, state):
|
||||
if "uncond" in preds: # recompute path
|
||||
if "uncond" in preds: # recompute path
|
||||
delta = preds["cond"] - preds["uncond"]
|
||||
state["delta"] = delta
|
||||
state["expert"] = ctx.active_expert_id
|
||||
return preds["uncond"] + guidance_scale * delta
|
||||
delta = state["delta"] # reuse path (skipped the uncond forward)
|
||||
delta = state["delta"] # reuse path (skipped the uncond forward)
|
||||
return preds["cond"] + (guidance_scale - 1.0) * delta
|
||||
|
||||
|
||||
|
||||
+9
-9
@@ -1,12 +1,12 @@
|
||||
"""Samplers as a small library (design_v3 §5.1: families use samplers as a library).
|
||||
"""Samplers as a small library; model families compose them.
|
||||
|
||||
Flow-match Euler is shared by the Wan and LTX-2 denoise step bodies. On a GPU box the real
|
||||
UniPC multistep / distilled schedulers are wrapped by the torch component adapters; this
|
||||
numpy form is the CPU-testable, bit-reproducible reference used by the loop tests.
|
||||
Flow-match Euler is shared by the Wan and LTX-2 denoise step bodies. On GPU the real UniPC
|
||||
multistep / distilled schedulers are wrapped by the torch component adapters; this numpy form
|
||||
is the CPU-testable, bit-reproducible reference used by the loop tests.
|
||||
|
||||
Flow-match interpolation: ``x_t = (1-σ_t)·x0 + σ_t·ε``; the model predicts velocity
|
||||
``v = ε - x0``. Then a deterministic Euler step to σ_next is simply ``x_next = x_t + (σ_next-σ_t)·v``
|
||||
(this is exactly LTX-2's ``velocity=(latents-denoised)/σ; latents += velocity·dt``).
|
||||
``v = ε - x0``. A deterministic Euler step to σ_next is ``x_next = x_t + (σ_next-σ_t)·v``
|
||||
(exactly LTX-2's ``velocity=(latents-denoised)/σ; latents += velocity·dt``).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -67,7 +67,7 @@ def flow_sde_step_with_logprob(x_t,
|
||||
noise=None,
|
||||
prev_sample=None,
|
||||
noise_scale: float = 0.7):
|
||||
"""FlowGRPO-style stochastic step + Gaussian log-prob (PromptRL/UniRL §6; FlowGRPO lineage).
|
||||
"""FlowGRPO-style stochastic step + Gaussian log-prob.
|
||||
|
||||
The RL *rollout* sampler (vs the deterministic ODE ``flow_match_euler_step`` used at serve time).
|
||||
Injects noise for exploration and returns the log-prob of the realized sample under the step's
|
||||
@@ -95,8 +95,8 @@ def flow_sde_step_with_logprob(x_t,
|
||||
def flow_sde_ml_velocity(x_t, sample, sigma_t: float, sigma_next: float, *, noise_scale: float = 0.7):
|
||||
"""The velocity whose deterministic SDE mean lands exactly on ``sample`` (the max-likelihood
|
||||
velocity for a realized FlowGRPO sample). Moving the policy toward it, advantage-weighted, is the
|
||||
policy-gradient direction — and is nonzero even at ratio==1, so it is the *correct* FlowGRPO update
|
||||
surrogate (nudging toward the velocity the model already produced would be a no-op). PromptRL §6.
|
||||
policy-gradient direction — and is nonzero even at ratio==1, so it is the correct FlowGRPO update
|
||||
surrogate (nudging toward the velocity the model already produced would be a no-op).
|
||||
"""
|
||||
x_t = np.asarray(x_t, dtype=np.float64)
|
||||
sample = np.asarray(sample, dtype=np.float64)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Memory plane — reservation before admission, sleep/wake by tag (design_v3 §7.3)."""
|
||||
"""Memory plane — reservation before admission, sleep/wake by tag."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2.memory.allocator import MemoryManager, OutOfMemory, Reservation
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"""Tagged memory pools with reservation-before-admission (design_v3 §6.2, §7.3).
|
||||
"""Tagged memory pools with reservation-before-admission.
|
||||
|
||||
> Reservation must be pre-flight. A scheduler that admits a diffusion step and then
|
||||
> discovers it cannot allocate the VAE tile is wrong.
|
||||
Reservation must be pre-flight: a scheduler that admits a diffusion step and then discovers it
|
||||
cannot allocate the VAE tile is wrong.
|
||||
|
||||
Sleep/wake is component-granular (tags are component names) for RL: drop DiT + caches,
|
||||
keep VAE/text-encoder resident (design_v3 §7.3; CuMem-style).
|
||||
keep VAE/text-encoder resident (CuMem-style).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class OutOfMemory(Exception):
|
||||
@@ -28,6 +28,7 @@ class Reservation:
|
||||
|
||||
|
||||
class MemoryManager:
|
||||
|
||||
def __init__(self, total_bytes: int = 1 << 40, per_tag_budget: dict[str, int] | None = None):
|
||||
self.total_bytes = total_bytes
|
||||
self.per_tag_budget = dict(per_tag_budget or {})
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# STUB: re-exports fastvideo until vendored (see memory: v2-vendoring-approach).
|
||||
"""LingBot-World DiT facade.
|
||||
|
||||
The transformer class itself is constructed by the loader from the card's ``load_id``
|
||||
(``fastvideo.models.dits.lingbotworld.model:LingBotWorldTransformer3DModel``), so it is not imported
|
||||
here. What the v2 recipe DOES need from the fastvideo source is the camera/Plucker embedding builder
|
||||
``prepare_camera_embedding`` (poses.npy + intrinsics.npy -> ``c2ws_plucker_emb [B, 6*s^2, F, H, W]``),
|
||||
re-exported so the program's camera node can build the tensor without reaching into the model package.
|
||||
The transformer class is constructed by the loader from the card's ``load_id``, so it is not imported
|
||||
here. This stub only re-exports the camera/Plucker embedding builder ``prepare_camera_embedding``
|
||||
(poses.npy + intrinsics.npy -> ``c2ws_plucker_emb [B, 6*s^2, F, H, W]``) so the program's camera node
|
||||
can build the tensor without reaching into the model package.
|
||||
"""
|
||||
from fastvideo.models.dits.lingbotworld.cam_utils import ( # noqa: F401
|
||||
prepare_camera_embedding, )
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Parallelism plane (design_v3 §8) — named axes → validated mesh, part of the cache key."""
|
||||
"""Parallelism plane — named axes -> validated mesh, part of the cache key."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2.parallel.mesh import FakeDeviceMesh, build_mesh
|
||||
from v2.parallel.plan import AXIS_NAMES, ParallelPlan
|
||||
from v2.parallel.validation import ParallelValidationError, validate_plan
|
||||
|
||||
__all__ = ["ParallelPlan", "AXIS_NAMES", "FakeDeviceMesh", "build_mesh",
|
||||
"validate_plan", "ParallelValidationError"]
|
||||
__all__ = ["ParallelPlan", "AXIS_NAMES", "FakeDeviceMesh", "build_mesh", "validate_plan", "ParallelValidationError"]
|
||||
|
||||
+3
-5
@@ -1,7 +1,4 @@
|
||||
"""FakeDeviceMesh — a ParallelDims-style builder, CPU-testable (design_v3 §8; torchtitan).
|
||||
|
||||
> Declarative, validated, compiled to a PyTorch ``DeviceMesh`` via a ``ParallelDims``-
|
||||
> style builder (product-of-degrees validation, cached submeshes).
|
||||
"""FakeDeviceMesh — a torchtitan-style ParallelDims builder, CPU-testable.
|
||||
|
||||
On a GPU box this compiles to ``torch.distributed.device_mesh.DeviceMesh``. Here it is a
|
||||
pure-Python mesh of rank tuples so topology logic is unit-tested without GPUs.
|
||||
@@ -16,6 +13,7 @@ from v2.parallel.validation import validate_plan
|
||||
|
||||
|
||||
class FakeDeviceMesh:
|
||||
|
||||
def __init__(self, plan: ParallelPlan):
|
||||
self.plan = plan
|
||||
self.order = plan.mesh_order or list(plan.axes.keys())
|
||||
@@ -45,7 +43,7 @@ class FakeDeviceMesh:
|
||||
|
||||
def _coord_to_rank(self, coord: tuple[int, ...]) -> int:
|
||||
rank = 0
|
||||
for c, d in zip(coord, self.shape):
|
||||
for c, d in zip(coord, self.shape, strict=False):
|
||||
rank = rank * d + c
|
||||
return rank
|
||||
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
"""ParallelPlan — parallelism as a model contract (design_v3 §8).
|
||||
"""ParallelPlan — parallelism as a model contract.
|
||||
|
||||
Parallelism is not a launch flag; it affects cache keys, scheduling, transport,
|
||||
capture, and parity, so it lives on the card. Declarative, validated, compiled to a
|
||||
@@ -11,17 +11,17 @@ import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Canonical axis names (design_v3 §8). cfgp is ≤2.
|
||||
# Canonical axis names. cfgp is <=2.
|
||||
AXIS_NAMES = ("dp", "tp", "sp", "cp", "cfgp", "pp_patch", "vae", "ep", "fsdp", "role", "replica")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParallelPlan:
|
||||
axes: dict[str, int] = field(default_factory=dict) # e.g. {"tp": 2, "sp": 4, "cfgp": 2}
|
||||
axes: dict[str, int] = field(default_factory=dict) # e.g. {"tp": 2, "sp": 4, "cfgp": 2}
|
||||
mesh_order: list[str] = field(default_factory=list)
|
||||
placement: str = "colocated"
|
||||
communication: dict[str, str] = field(default_factory=dict)
|
||||
# design_v3 §8 + omni-readiness §4b: applicability conditions travel with axes
|
||||
# applicability conditions travel with axes
|
||||
applicability: dict[str, dict] = field(default_factory=dict)
|
||||
per_axis_communication: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
@@ -36,11 +36,11 @@ class ParallelPlan:
|
||||
|
||||
@property
|
||||
def hash(self) -> str:
|
||||
"""Stable hash — part of the CacheKey (design_v3 §7.1: parallel_plan_hash)."""
|
||||
"""Stable hash — part of the CacheKey (parallel_plan_hash)."""
|
||||
payload = json.dumps({"axes": self.axes, "order": self.mesh_order}, sort_keys=True)
|
||||
return hashlib.sha256(payload.encode()).hexdigest()[:16]
|
||||
|
||||
@classmethod
|
||||
def single(cls) -> "ParallelPlan":
|
||||
def single(cls) -> ParallelPlan:
|
||||
"""The default deployment: one device, all degree-one trivial groups."""
|
||||
return cls(axes={"dp": 1}, mesh_order=["dp"])
|
||||
|
||||
+11
-10
@@ -1,9 +1,8 @@
|
||||
"""Pre-flight parallelism validation (design_v3 §8).
|
||||
"""Pre-flight parallelism validation.
|
||||
|
||||
> Pre-flight or it fails at load, never halfway. Ownership conflicts are build errors
|
||||
> ... Applicability conditions travel with axes: ``pp_patch`` is invalid for causal/AR.
|
||||
|
||||
All checks are CPU-testable on a fake mesh (design.md §6.3.4: CPU-testability).
|
||||
Validate at load or fail, never halfway. Ownership conflicts are build errors, and
|
||||
applicability conditions travel with axes (e.g. ``pp_patch`` is invalid for causal/AR).
|
||||
All checks are CPU-testable on a fake mesh.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -19,16 +18,19 @@ class ParallelValidationError(ValueError):
|
||||
_CAUSAL_KINDS = {LoopKind.CHUNK_ROLLOUT, LoopKind.AR_DECODE}
|
||||
|
||||
|
||||
def validate_plan(plan: ParallelPlan, card=None, *, world_size: int | None = None,
|
||||
def validate_plan(plan: ParallelPlan,
|
||||
card=None,
|
||||
*,
|
||||
world_size: int | None = None,
|
||||
cfg_policy_batched: bool = False) -> ParallelPlan:
|
||||
"""Validate a plan, optionally against a card. Returns the plan or raises.
|
||||
|
||||
Checks (design_v3 §8):
|
||||
Checks:
|
||||
1. axis names are known; degrees >= 1
|
||||
2. cfgp <= 2
|
||||
3. product of degrees matches world_size (when given)
|
||||
4. pp_patch invalid for any causal/AR loop on the card
|
||||
5. ownership conflict: cfgp>1 AND a batched CFGPolicy is rejected (§5.3, §9 build-guard)
|
||||
5. ownership conflict: cfgp>1 AND a batched CFGPolicy is rejected
|
||||
"""
|
||||
errs: list[str] = []
|
||||
|
||||
@@ -45,8 +47,7 @@ def validate_plan(plan: ParallelPlan, card=None, *, world_size: int | None = Non
|
||||
errs.append(f"product of degrees {plan.world_size()} != world_size {world_size}")
|
||||
|
||||
if plan.degree("cfgp") > 1 and cfg_policy_batched:
|
||||
errs.append("ownership conflict: a request owns a BatchedCFG policy OR a cfgp group, never both "
|
||||
"(design_v3 §5.3 / §9 build-guard)")
|
||||
errs.append("ownership conflict: a request owns a BatchedCFG policy OR a cfgp group, never both")
|
||||
|
||||
if card is not None and plan.degree("pp_patch") > 1:
|
||||
causal = [lid for lid, lp in card.loops.items() if lp.kind in _CAUSAL_KINDS]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Parity plane — a first-class package, not a test folder (design_v3 §9, §18).
|
||||
"""Parity plane — a first-class package, not a test folder.
|
||||
|
||||
It is how the (recipe, runtime) pair is kept honest: parity is measured by ParityAligner,
|
||||
the consistency ladder is typed, and the interleave gate is non-negotiable.
|
||||
@@ -10,5 +10,7 @@ from v2.parity.aligner import ParityAligner
|
||||
from v2.parity.interleave_gate import assert_interleave_parity, compare_outputs
|
||||
from v2.parity.ladder import Divergence, array_diff, bit_identical, within
|
||||
|
||||
__all__ = ["ConsistencyLevel", "ExecutionProfile", "ParityAligner", "Divergence",
|
||||
"array_diff", "bit_identical", "within", "assert_interleave_parity", "compare_outputs"]
|
||||
__all__ = [
|
||||
"ConsistencyLevel", "ExecutionProfile", "ParityAligner", "Divergence", "array_diff", "bit_identical", "within",
|
||||
"assert_interleave_parity", "compare_outputs"
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""ParityAligner — parity is measured, never assumed (design_v3 §9.1, §11).
|
||||
"""ParityAligner — parity is measured, never assumed.
|
||||
|
||||
A read-only observer: *record mode* dumps named taps per step/block from a reference
|
||||
(the official framework, or a pre-change build); *compare mode* replays with fixed seeds
|
||||
and reports the **first** divergence beyond per-tap tolerance. This is the engine behind
|
||||
and reports the first divergence beyond per-tap tolerance. This is the engine behind
|
||||
the "old loop vs new loop, bit-identical" gate and the standing instrument for every
|
||||
port, precision change, and kernel swap.
|
||||
"""
|
||||
@@ -20,7 +20,7 @@ class ParityAligner:
|
||||
self.name = name
|
||||
self.default_atol = default_atol
|
||||
self.default_rtol = default_rtol
|
||||
self.taps: dict[tuple[int, str], Any] = {} # (step, tap_name) -> value
|
||||
self.taps: dict[tuple[int, str], Any] = {} # (step, tap_name) -> value
|
||||
self.tolerances: dict[str, tuple[float, float]] = {} # tap_name -> (atol, rtol)
|
||||
|
||||
def set_tolerance(self, tap_name: str, atol: float = 0.0, rtol: float = 0.0) -> None:
|
||||
@@ -34,19 +34,19 @@ class ParityAligner:
|
||||
if event == "tap":
|
||||
self.record_tap(kw.get("step", 0), kw["name"], kw["value"])
|
||||
|
||||
def first_divergence(self, reference: "ParityAligner",
|
||||
def first_divergence(self,
|
||||
reference: ParityAligner,
|
||||
level: ConsistencyLevel = ConsistencyLevel.C1) -> Divergence | None:
|
||||
"""Compare this (current) run against a reference, reporting the FIRST divergence
|
||||
in step order beyond the per-tap tolerance."""
|
||||
for (step, tap_name) in sorted(reference.taps, key=lambda k: (k[0], k[1])):
|
||||
ref_val = reference.taps[(step, tap_name)]
|
||||
if (step, tap_name) not in self.taps:
|
||||
return Divergence(f"{tap_name}@{step}", level, float("inf"), float("inf"),
|
||||
"tap missing in current run")
|
||||
return Divergence(f"{tap_name}@{step}", level, float("inf"), float("inf"), "tap missing in current run")
|
||||
cur_val = self.taps[(step, tap_name)]
|
||||
atol, rtol = self.tolerances.get(tap_name, (self.default_atol, self.default_rtol))
|
||||
abs_d, rel_d = array_diff(ref_val, cur_val)
|
||||
if not (abs_d <= atol or rel_d <= rtol): # diverged: outside BOTH tolerances
|
||||
if not (abs_d <= atol or rel_d <= rtol): # diverged: outside BOTH tolerances
|
||||
return Divergence(f"{tap_name}@{step}", level, abs_d, rel_d,
|
||||
f"abs={abs_d:.3e} rel={rel_d:.3e} > (atol={atol},rtol={rtol})")
|
||||
return None
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
"""The batch-of-N interleave parity gate (design_v3 §9.3) — non-negotiable.
|
||||
"""The batch-of-N interleave parity gate — required, not optional.
|
||||
|
||||
> Loop inversion's real hazard is cross-request state smearing under interleaving — and a
|
||||
> batch-of-1 parity gate is structurally blind to it. So v3 makes a batch-of-N interleave
|
||||
> parity test a *required* gate: two (or more) concurrent requests, interleaved at step
|
||||
> granularity, must be bit-identical to the same requests run serially.
|
||||
Loop inversion's real hazard is cross-request state smearing under interleaving, which a
|
||||
batch-of-1 parity gate cannot detect. So this gate runs two or more concurrent requests
|
||||
interleaved at step granularity and requires bit-identical output versus running them serially.
|
||||
|
||||
This is the test the whole loop-inversion bet lives or dies on. The gate is pure and
|
||||
duck-typed: it takes any engine exposing ``run_serial`` and ``run_interleaved``.
|
||||
The gate is pure and duck-typed: it takes any engine exposing ``run_serial`` and ``run_interleaved``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from v2.request.artifacts import (
|
||||
AudioArtifact,
|
||||
LatentArtifact,
|
||||
Output,
|
||||
TensorArtifact,
|
||||
TextArtifact,
|
||||
VideoArtifact,
|
||||
)
|
||||
Output, )
|
||||
from v2.parity.ladder import ConsistencyLevel, Divergence, bit_identical
|
||||
|
||||
|
||||
@@ -34,27 +26,32 @@ def compare_outputs(serial: dict[str, Output], interleaved: dict[str, Output]) -
|
||||
"""Bitwise-compare per-request outputs from serial vs interleaved runs."""
|
||||
divs: list[Divergence] = []
|
||||
if set(serial) != set(interleaved):
|
||||
divs.append(Divergence("request_set", ConsistencyLevel.C1, float("inf"), float("inf"),
|
||||
f"serial reqs {set(serial)} != interleaved {set(interleaved)}"))
|
||||
divs.append(
|
||||
Divergence("request_set", ConsistencyLevel.C1, float("inf"), float("inf"),
|
||||
f"serial reqs {set(serial)} != interleaved {set(interleaved)}"))
|
||||
return divs
|
||||
for rid in sorted(serial):
|
||||
so, io = serial[rid], interleaved[rid]
|
||||
if set(so.artifacts) != set(io.artifacts):
|
||||
divs.append(Divergence(f"{rid}:artifacts", ConsistencyLevel.C1, float("inf"), float("inf"),
|
||||
f"artifact names differ: {set(so.artifacts)} vs {set(io.artifacts)}"))
|
||||
divs.append(
|
||||
Divergence(f"{rid}:artifacts", ConsistencyLevel.C1, float("inf"), float("inf"),
|
||||
f"artifact names differ: {set(so.artifacts)} vs {set(io.artifacts)}"))
|
||||
continue
|
||||
for name in sorted(so.artifacts):
|
||||
a, b = _artifact_payload(so.artifacts[name]), _artifact_payload(io.artifacts[name])
|
||||
if a is None and b is None:
|
||||
# both empty is NOT parity — a deferred/aborted request must not pass the gate vacuously
|
||||
divs.append(Divergence(f"{rid}:{name}", ConsistencyLevel.C1, float("inf"), float("inf"),
|
||||
"both serial and interleaved produced EMPTY output for a declared "
|
||||
"artifact (deferred/aborted?) — suspicious, not parity"))
|
||||
divs.append(
|
||||
Divergence(
|
||||
f"{rid}:{name}", ConsistencyLevel.C1, float("inf"), float("inf"),
|
||||
"both serial and interleaved produced EMPTY output for a declared "
|
||||
"artifact (deferred/aborted?) — suspicious, not parity"))
|
||||
continue
|
||||
if not bit_identical(a, b):
|
||||
divs.append(Divergence(f"{rid}:{name}", ConsistencyLevel.C1, float("nan"), float("nan"),
|
||||
"serial vs interleaved output not bit-identical "
|
||||
"(cross-request state smearing!)"))
|
||||
divs.append(
|
||||
Divergence(f"{rid}:{name}", ConsistencyLevel.C1, float("nan"), float("nan"),
|
||||
"serial vs interleaved output not bit-identical "
|
||||
"(cross-request state smearing!)"))
|
||||
return divs
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""The consistency ladder (design_v3 §9.2) + numeric comparison helpers.
|
||||
"""The consistency ladder + numeric comparison helpers.
|
||||
|
||||
C0 component · C1 loop · C2 behavioral · C3 distribution · C4 artifact.
|
||||
|
||||
@@ -18,7 +18,7 @@ from v2._enums import ConsistencyLevel # re-exported for the package
|
||||
|
||||
@dataclass
|
||||
class Divergence:
|
||||
where: str # tap name / artifact path
|
||||
where: str # tap name / artifact path
|
||||
level: ConsistencyLevel
|
||||
max_abs_diff: float
|
||||
max_rel_diff: float
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""The multi-backend dispatch substrate (design_v3 §17).
|
||||
"""The multi-backend dispatch substrate.
|
||||
|
||||
Two tuple-keyed registries (``COMPONENTS``, ``KERNELS``) + a ``Platform`` that detects ``(device,
|
||||
arch)`` and resolves both — with the numpy reference as the terminal fallback rung and parity
|
||||
oracle. This package is the membrane that lets CPU, GPU, and other backends coexist: a model's
|
||||
loops, policies, scheduler, caches, and training code never name a device; they call through the
|
||||
component/kernel seams and the resolved ``Platform`` decides the implementation.
|
||||
arch)`` and resolves both, with the numpy reference as the terminal fallback rung and parity oracle.
|
||||
Lets CPU, GPU, and other backends coexist: a model's loops, policies, scheduler, caches, and
|
||||
training code never name a device; they call through the component/kernel seams and the resolved
|
||||
``Platform`` decides the implementation.
|
||||
|
||||
Importing this package is cheap and cycle-free (registry + platform classes only). The concrete
|
||||
Importing this package is cheap and cycle-free (registry + platform classes only); the concrete
|
||||
backend registrations in ``backends/`` are imported lazily on first ``Platform`` use.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""``accel`` — a pure-python stand-in accelerator backend (design_v3 §17 honesty note).
|
||||
"""``accel`` — a pure-python stand-in accelerator backend.
|
||||
|
||||
There is no GPU in this environment, so to *prove* the dispatch substrate is genuinely
|
||||
device-generic — not a single-backend abstraction with a CPU special case — this backend registers
|
||||
a second device, ``accel``, entirely in CPU-resident Python. It exercises everything a real GPU
|
||||
There is no GPU in this environment, so to prove the dispatch substrate is genuinely
|
||||
device-generic (not a single-backend abstraction with a CPU special case) this backend registers a
|
||||
second device, ``accel``, entirely in CPU-resident Python. It exercises everything a real GPU
|
||||
backend would:
|
||||
|
||||
* a **component** override — ``AccelDiT`` is built via ``COMPONENTS[("dit", "accel", …)]`` instead
|
||||
@@ -11,12 +11,12 @@ backend would:
|
||||
* **kernels at different arch rungs** — ``flow_sde_step`` is registered at ``sm90`` directly, while
|
||||
``flow_match_step`` is registered only at ``generic``, so resolving it on an ``sm90`` accel
|
||||
platform must walk the arch fallback ``sm90 → sm80 → generic`` to find it;
|
||||
* the **parity oracle** — the ODE solver op (``_accel_flow_match_step``) is an *independently
|
||||
written* kernel that nonetheless matches the numpy reference bit-for-bit, so a denoise run on the
|
||||
accel platform equals the cpu run. That independent-impl-vs-reference equality is the oracle a
|
||||
real backend must pass; the stand-in shows the check has teeth. (The SDE op below deliberately
|
||||
*reuses* the reference impl — a backend may legitimately not specialize every op — so the SDE
|
||||
test exercises the dispatch path, not an independent implementation.)
|
||||
* the **parity oracle** — the ODE solver op (``_accel_flow_match_step``) is an independently
|
||||
written kernel that matches the numpy reference bit-for-bit, so a denoise run on the accel
|
||||
platform equals the cpu run. That independent-impl-vs-reference equality is the oracle a real
|
||||
backend must pass. (The SDE op below deliberately reuses the reference impl — a backend may
|
||||
legitimately not specialize every op — so the SDE test exercises the dispatch path, not an
|
||||
independent implementation.)
|
||||
|
||||
On a real box this file is where a torch/cuda backend would register its adapters and fused kernels
|
||||
(unified primitives + model-co-located fusion variants) through the same two functions.
|
||||
@@ -79,8 +79,7 @@ def _build_accel_component(spec: Any, instance: Any, platform: Any) -> AccelComp
|
||||
# Override more than one component kind on accel (proving the seam is kind-generic, not dit-special).
|
||||
# text_encoder is deliberately left UNregistered so the device→cpu fallback path stays demonstrated.
|
||||
for _kind in ("vae", "audio_vae"):
|
||||
register_component(_kind, _build_accel_component, device="accel",
|
||||
source="accel(stand-in):AccelComponent")
|
||||
register_component(_kind, _build_accel_component, device="accel", source="accel(stand-in):AccelComponent")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -93,8 +92,11 @@ def _accel_flow_match_step(x_t, velocity, sigma_t: float, sigma_next: float):
|
||||
|
||||
# flow_match_step lives only at the `generic` arch rung → resolving on sm90 must fall back through
|
||||
# the arch chain (sm90 → sm80 → generic). This proves the arch-fallback walk.
|
||||
register_kernel(FLOW_MATCH_STEP, _accel_flow_match_step,
|
||||
device="accel", arch="generic", source="accel(stand-in):fused_flow_match")
|
||||
register_kernel(FLOW_MATCH_STEP,
|
||||
_accel_flow_match_step,
|
||||
device="accel",
|
||||
arch="generic",
|
||||
source="accel(stand-in):fused_flow_match")
|
||||
|
||||
# flow_sde_step is registered at sm90 directly. This backend does NOT specialize the SDE op — it
|
||||
# reuses the numpy reference, so the accel SDE path is the same function as cpu (the SDE parity test
|
||||
@@ -102,5 +104,8 @@ register_kernel(FLOW_MATCH_STEP, _accel_flow_match_step,
|
||||
# independent oracle). A device that DID specialize SDE would be checked the same way the ODE op is.
|
||||
from v2.loop.sampler import flow_sde_step_with_logprob # noqa: E402
|
||||
|
||||
register_kernel(FLOW_SDE_STEP, flow_sde_step_with_logprob,
|
||||
device="accel", arch="sm90", source="accel(stand-in):sde_logprob")
|
||||
register_kernel(FLOW_SDE_STEP,
|
||||
flow_sde_step_with_logprob,
|
||||
device="accel",
|
||||
arch="sm90",
|
||||
source="accel(stand-in):sde_logprob")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""CPU / numpy backend — the terminal rung and parity oracle (design_v3 §17).
|
||||
"""CPU / numpy backend — the terminal rung and parity oracle.
|
||||
|
||||
This backend registers the numpy reference kernels. It is the bottom of every device fallback
|
||||
chain: any op a richer backend hasn't implemented resolves here, and every other backend's output
|
||||
@@ -16,7 +16,13 @@ from v2.platform.registry import FLOW_MATCH_STEP, FLOW_SDE_STEP, register_kernel
|
||||
# The numpy solver primitives, registered as the terminal (cpu, numpy) kernels. These are the exact
|
||||
# same functions the loops used to call directly — so dispatching through the registry on a CPU box
|
||||
# is bit-for-bit identical to the pre-registry behavior.
|
||||
register_kernel(FLOW_MATCH_STEP, flow_match_euler_step,
|
||||
device="cpu", arch="numpy", source="loop.sampler:flow_match_euler_step")
|
||||
register_kernel(FLOW_SDE_STEP, flow_sde_step_with_logprob,
|
||||
device="cpu", arch="numpy", source="loop.sampler:flow_sde_step_with_logprob")
|
||||
register_kernel(FLOW_MATCH_STEP,
|
||||
flow_match_euler_step,
|
||||
device="cpu",
|
||||
arch="numpy",
|
||||
source="loop.sampler:flow_match_euler_step")
|
||||
register_kernel(FLOW_SDE_STEP,
|
||||
flow_sde_step_with_logprob,
|
||||
device="cpu",
|
||||
arch="numpy",
|
||||
source="loop.sampler:flow_sde_step_with_logprob")
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
"""Real torch component adapters for the GPU backend (design_v3 §17; README "Running the real models").
|
||||
"""Real torch component adapters for the GPU backend. See v2/README.md ("Running the real models").
|
||||
|
||||
ONE cohesive module (was the torch_adapters.py + torch_ltx2.py split): a ``TorchComponent`` base that
|
||||
centralizes the shared mechanics — ``.to(device,dtype).eval()``, the numpy<->torch marshalling at the
|
||||
loop boundary, the ``set_forward_context`` wrap every fastvideo forward needs, and the weight surface
|
||||
(copy_from / blend_from / clone) — plus thin per-model subclasses that carry ONLY the forward semantics
|
||||
(Wan ``sigma*1000`` -> velocity; LTX-2 per-token sigma, x0 -> ``(x_t-x0)/sigma``, joint A/V; VAE
|
||||
normalization; T5 padding; Gemma dual-projection; upsampler; audio decode->vocoder). A single
|
||||
``build_component(spec, instance, platform)`` dispatches by ``spec.kind`` through ``_MAKERS`` (replacing
|
||||
the six ``build_torch_*`` builders + six lazy trampolines).
|
||||
A ``TorchComponent`` base centralizes the shared mechanics — ``.to(device,dtype).eval()``, the
|
||||
numpy<->torch marshalling at the loop boundary, the ``set_forward_context`` wrap every fastvideo
|
||||
forward needs, and the weight surface (copy_from / blend_from / clone). Thin per-model subclasses
|
||||
carry only the forward semantics (Wan ``sigma*1000`` -> velocity; LTX-2 per-token sigma,
|
||||
x0 -> ``(x_t-x0)/sigma``, joint A/V; VAE normalization; T5 padding; Gemma dual-projection; upsampler;
|
||||
audio decode->vocoder). A single ``build_component(spec, instance, platform)`` dispatches by
|
||||
``spec.kind`` through ``_MAKERS``.
|
||||
|
||||
The loops / CFG / scheduler math stay numpy fp32 (lowest-risk bring-up); the boundary marshals
|
||||
numpy<->torch in ONE place (``TorchComponent._t``/``_n``) so a torch-native path is a later swap.
|
||||
Component construction goes through ``load_component`` — the v2-owned loader seam (currently delegates to
|
||||
Component construction goes through ``load_component``, the v2-owned loader seam (currently delegates to
|
||||
fastvideo's component_loader; a per-module vendored cutover replaces it later, no caller changes).
|
||||
|
||||
Imported ONLY lazily — inside ``torch_cuda.py``'s registered builder — so importing the platform package
|
||||
@@ -87,7 +86,7 @@ def _fastvideo_args(spec: Any) -> Any:
|
||||
|
||||
|
||||
def load_component(loader_attr: str, path: str, args):
|
||||
"""v2-owned loader seam (design: ``v2/loader``). Constructs a real component from a checkpoint via the
|
||||
"""v2-owned loader seam (``v2/loader``). Constructs a real component from a checkpoint via the
|
||||
component loaders. ``WanTransformer3DModel`` / ``AutoencoderKLWan`` have NO ``from_pretrained``; the
|
||||
loader reads the checkpoint config, resolves the class (UMT5 vs T5 from config), and loads weights.
|
||||
Currently delegates to fastvideo's loader; a vendored cutover swaps the body, not the callers."""
|
||||
@@ -145,7 +144,7 @@ class TorchComponent:
|
||||
from v2.forward_context import set_forward_context
|
||||
return set_forward_context(current_timestep=current_timestep, attn_metadata=None)
|
||||
|
||||
# weight surface used by serving weight-sync + training (design_v3 §10) ------------------------- #
|
||||
# weight surface used by serving weight-sync + training ----------------------------------------- #
|
||||
def copy_from(self, other) -> None:
|
||||
self.module.load_state_dict(other.module.state_dict())
|
||||
|
||||
|
||||
@@ -8,14 +8,13 @@ The three Cosmos2.5 deltas vs Wan (all GPU-path, all carried HERE so the loop st
|
||||
* ``Cosmos25DiT`` — a flow-match velocity DiT, but the per-step call differs from ``WanDiT`` in three
|
||||
load-bearing ways (see ``fastvideo/pipelines/stages/denoising.py:Cosmos25DenoisingStage``):
|
||||
1. timestep is a PER-FRAME 2D tensor ``[B, T]`` filled with the PLAIN sigma (in [0,1]) — NOT Wan's
|
||||
scalar ``sigma*1000``. (The fastvideo stage feeds ``t*0.001`` of the 0..1000 schedule, i.e. the
|
||||
shifted sigma directly; ``Cosmos25DenoiseLoop`` already hands us that plain sigma.)
|
||||
scalar ``sigma*1000``. ``Cosmos25DenoiseLoop`` already hands us that plain sigma.
|
||||
2. the forward REQUIRES a ``condition_mask[B,1,T,H,W]`` (zeros for t2v) + a ``padding_mask[B,1,H,W]``
|
||||
(ones for t2v) every step — the model concats them internally (16 -> 17 -> 18ch patch_embed),
|
||||
so we feed the RAW 16-channel latent (the OPPOSITE of Wan i2v, where the adapter pre-concats).
|
||||
3. an ``fps`` scalar is required for RoPE temporal scaling (NTK fps-modulation).
|
||||
The model output is the rectified-flow velocity (a bare tensor); the loop integrates it with the
|
||||
shared ``FLOW_MATCH_STEP`` Euler solver and combines branches with the standard ``ClassicCFG``.
|
||||
The output is the rectified-flow velocity (a bare tensor); the loop integrates it with the shared
|
||||
``FLOW_MATCH_STEP`` Euler solver and combines branches with ``ClassicCFG``.
|
||||
* ``Cosmos25WanVAE`` — a Wan-style causal 3D VAE (z=16, 8x/4x), BUT unlike ``AutoencoderKLWan`` its
|
||||
``encode``/``decode`` already apply the (z-mean)/std normalization INTERNALLY (the Cosmos latent
|
||||
contract: encode -> normalized, decode <- normalized). So this is a thin marshalling wrapper (like
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""``cuda`` registration — the real torch/GPU backend (design_v3 §17; README "Running the real models").
|
||||
"""``cuda`` registration — the real torch/GPU backend (see v2/README.md "Running the real models").
|
||||
|
||||
Torch-free at import (stdlib only): gates every cell on ``available=_cuda_available`` (a ``find_spec``
|
||||
check that never imports torch) and registers ONE lazy component builder for all cuda component kinds +
|
||||
@@ -50,17 +50,28 @@ def _flow_sde_cuda(*args, **kwargs):
|
||||
# --- components: one generic builder; torch_backend.build_component dispatches by spec.kind ---------- #
|
||||
_B = "v2.platform.backends.torch_backend"
|
||||
_KIND_SOURCE = {
|
||||
"dit": "WanDiT/LTX2DiT", "vae": "WanVAE/LTX2VAE", "text_encoder": "T5Encoder/Gemma",
|
||||
"image_encoder": "CLIPImageEncoder", "upsampler": "LTX2Upsampler",
|
||||
"audio_vae": "LTX2AudioVAE", "vocoder": "LTX2Vocoder",
|
||||
"dit": "WanDiT/LTX2DiT",
|
||||
"vae": "WanVAE/LTX2VAE",
|
||||
"text_encoder": "T5Encoder/Gemma",
|
||||
"image_encoder": "CLIPImageEncoder",
|
||||
"upsampler": "LTX2Upsampler",
|
||||
"audio_vae": "LTX2AudioVAE",
|
||||
"vocoder": "LTX2Vocoder",
|
||||
}
|
||||
for _kind, _cls in _KIND_SOURCE.items():
|
||||
register_component(_kind, _build_component, device="cuda", available=_cuda_available,
|
||||
source=f"{_B}:{_cls}")
|
||||
register_component(_kind, _build_component, device="cuda", available=_cuda_available, source=f"{_B}:{_cls}")
|
||||
del _kind, _cls
|
||||
|
||||
# --- solver ops: plain torch elementwise (no fused solver kernel in fastvideo-kernel) ---------------- #
|
||||
register_kernel(FLOW_MATCH_STEP, _flow_match_cuda, device="cuda", arch="generic",
|
||||
available=_cuda_available, source="torch elementwise (no fused solver kernel)")
|
||||
register_kernel(FLOW_SDE_STEP, _flow_sde_cuda, device="cuda", arch="generic",
|
||||
available=_cuda_available, source="torch elementwise (no fused solver kernel)")
|
||||
register_kernel(FLOW_MATCH_STEP,
|
||||
_flow_match_cuda,
|
||||
device="cuda",
|
||||
arch="generic",
|
||||
available=_cuda_available,
|
||||
source="torch elementwise (no fused solver kernel)")
|
||||
register_kernel(FLOW_SDE_STEP,
|
||||
_flow_sde_cuda,
|
||||
device="cuda",
|
||||
arch="generic",
|
||||
available=_cuda_available,
|
||||
source="torch elementwise (no fused solver kernel)")
|
||||
|
||||
@@ -128,10 +128,9 @@ class Gen3CVAE(TorchComponent):
|
||||
|
||||
|
||||
class Gen3CT5Encoder(CosmosT5Encoder):
|
||||
"""GEN3C T5-Large (1024-dim): raw last_hidden_state (NaN→0), no Wan zero-pad — identical to Cosmos.
|
||||
Reuses ``CosmosT5Encoder.encode`` verbatim. (BRINGUP: GEN3C pads with ``max_length`` and zeros rows
|
||||
past the attention-mask length, which ``CosmosT5Encoder`` already does — verify the max_length /
|
||||
mask-zeroing matches the converted T5-Large tokenizer on the GPU box.)"""
|
||||
"""GEN3C T5-Large (1024-dim): raw last_hidden_state (NaN→0), no Wan zero-pad — identical to Cosmos,
|
||||
so it reuses ``CosmosT5Encoder.encode`` verbatim. (BRINGUP: verify the max_length / mask-zeroing
|
||||
matches the converted T5-Large tokenizer on the GPU box.)"""
|
||||
|
||||
|
||||
# Re-export so a reader can ``from v2.platform.backends.torch_gen3c import _to_numpy`` if needed.
|
||||
|
||||
@@ -2,37 +2,33 @@
|
||||
so the recipe is self-contained (no edit to the shared ``_make_dit``/``_make_vae``/``_make_text_encoder``
|
||||
dispatch in ``torch_backend.py``). Imported lazily by ``_explicit_adapter`` only on a GPU box.
|
||||
|
||||
Architecture deltas vs Wan that force NEW adapters (all faithful to
|
||||
Architecture deltas vs Wan that force NEW adapters (faithful to
|
||||
``fastvideo/models/dits/hunyuanvideo15.py`` + ``fastvideo/pipelines/stages/denoising.py``):
|
||||
|
||||
* ``HunyuanVideo15DiT`` — the rectified-flow velocity network ``F_θ``. Unlike ``WanDiT`` (single text
|
||||
embed, single image embed), the real ``forward`` takes ``encoder_hidden_states`` as a **LIST of two**
|
||||
tensors ``[qwen_hidden(3584-d), byt5_hidden(1472-d)]`` and ``encoder_hidden_states_image`` as a
|
||||
**LIST of one** image-embed tensor (all-zeros ``[1,729,1152]`` for t2v → the DiT's
|
||||
``torch.all(image_embeds==0)`` auto-selects the t2v token-ordering branch). The loop hands the two
|
||||
text embeds as a tuple in ``text_embed`` and the image embeds in ``context``; this adapter marshals
|
||||
them into the two lists internally and builds the zero image-embed default, so the loop's dit-call
|
||||
signature stays ``dit(latent, text_embed, sigma, context=, cond=)`` (toy-compatible). For i2v the
|
||||
loop passes the 33-channel first-frame conditioning latent (32 VAE + 1 mask) in ``cond``; the adapter
|
||||
concats it onto the noisy latent on the channel dim to reach ``in_channels=65`` BEFORE the forward
|
||||
(mirrors ``denoising.py`` line 393's ``torch.cat([latent, image_latent], dim=1)``). ``timestep`` is
|
||||
``sigma*1000`` (the scheduler's discrete timestep; ``scale_model_input`` is identity for flow-match).
|
||||
The network predicts velocity directly → returned via ``self._n()`` with NO x0→velocity conversion
|
||||
(unlike ``LTX2DiT``). ``use_meanflow`` SR variants pass ``timestep_r`` — out of scope here (t2v/i2v
|
||||
ship with ``use_meanflow=False``), so ``timestep_r=None``.
|
||||
* ``HunyuanVideo15VAE`` — the HunyuanVideo 1.5 causal-3D VAE (z=32, 16× spatial / 4× temporal). Its DiT
|
||||
latent space is normalized by a single **scalar** ``scaling_factor=1.03682`` (NOT Wan's per-channel
|
||||
* ``HunyuanVideo15DiT`` — rectified-flow velocity network. Unlike ``WanDiT`` (single text/image embed),
|
||||
the real ``forward`` takes ``encoder_hidden_states`` as a LIST of two ``[qwen_hidden(3584-d),
|
||||
byt5_hidden(1472-d)]`` and ``encoder_hidden_states_image`` as a LIST of one image-embed (all-zeros
|
||||
``[1,729,1152]`` for t2v -> the DiT's ``torch.all(image_embeds==0)`` selects the t2v token-ordering
|
||||
branch). The loop hands the two text embeds as a tuple in ``text_embed`` and the image embeds in
|
||||
``context``; this adapter marshals them into the two lists and builds the zero image-embed default, so
|
||||
the loop's dit-call signature stays ``dit(latent, text_embed, sigma, context=, cond=)``. For i2v the
|
||||
loop passes the 33-channel first-frame cond latent (32 VAE + 1 mask) in ``cond``; the adapter concats
|
||||
it onto the noisy latent to reach ``in_channels=65`` before the forward. ``timestep`` is ``sigma*1000``
|
||||
(``scale_model_input`` is identity for flow-match). Predicts velocity directly, returned via
|
||||
``self._n()`` with NO x0->velocity conversion. ``timestep_r`` (``use_meanflow`` SR variants) is out of
|
||||
scope here (t2v/i2v ship with ``use_meanflow=False``), so ``timestep_r=None``.
|
||||
* ``HunyuanVideo15VAE`` — HunyuanVideo 1.5 causal-3D VAE (z=32, 16x spatial / 4x temporal). DiT latent
|
||||
space normalized by a single scalar ``scaling_factor=1.03682`` (NOT Wan's per-channel
|
||||
``latents_mean``/``latents_std`` — the ``WanVAE`` adapter would corrupt these latents, BRINGUP risk D).
|
||||
Decode: ``module.decode(latent / scaling_factor)``; encode (i2v first-frame cond): ``z * scaling_factor``.
|
||||
* ``HunyuanVideo15QwenEncoder`` — Qwen2.5-VL primary text encoder. Faithful to
|
||||
``configs/pipelines/hunyuan15.py:qwen_preprocess_text/qwen_postprocess_text``: wraps the prompt in the
|
||||
system+chat template, requests ``output_hidden_states=True``, takes ``hidden_states[-3]`` (3rd-from-last
|
||||
layer, NOT ``last_hidden_state``), and crops the first 108 prompt-template tokens. The generic
|
||||
``T5Encoder.encode`` (last_hidden_state, pad-to-max) is WRONG for Qwen (BRINGUP risk B).
|
||||
* ``HunyuanVideo15ByT5Encoder`` — ByT5/Glyph secondary encoder. ``byt5_preprocess_text`` extracts quoted
|
||||
glyph texts via regex; when the prompt has no quoted text it is ``None`` and the real pipeline emits a
|
||||
zero-length ``[1, 0, 1472]`` embedding (the DiT always concatenates the txt_in_2 output) — this adapter
|
||||
reproduces that (BRINGUP risk C). Output is the raw ``last_hidden_state``.
|
||||
* ``HunyuanVideo15QwenEncoder`` — Qwen2.5-VL primary text encoder. Wraps the prompt in the system+chat
|
||||
template, requests ``output_hidden_states=True``, takes ``hidden_states[-3]`` (3rd-from-last layer, NOT
|
||||
``last_hidden_state``), and crops the first 108 template tokens. The generic ``T5Encoder.encode`` is
|
||||
WRONG for Qwen (BRINGUP risk B).
|
||||
* ``HunyuanVideo15ByT5Encoder`` — ByT5/Glyph secondary encoder. Extracts quoted glyph texts via regex;
|
||||
when the prompt has no quoted text the real pipeline emits a zero-length ``[1, 0, 1472]`` embedding (the
|
||||
DiT always concatenates the txt_in_2 output) — this adapter reproduces that (BRINGUP risk C). Output is
|
||||
the raw ``last_hidden_state``.
|
||||
|
||||
All forwards run inside ``self._ctx()`` (FastVideo attention reads attn_metadata via the forward
|
||||
context). BRINGUP: the DiT's ``sequence_model_parallel_shard``/all-gather is a no-op at sp=1 (the card
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"""Torch solver-step kernels for the GPU backend (design_v3 §17).
|
||||
"""Torch solver-step kernels for the GPU backend.
|
||||
|
||||
GROUND TRUTH (from the fastvideo-kernel audit): there is NO fused flow-match / SDE *solver* kernel.
|
||||
fastvideo-kernel ships only primitives — sparse/tiled attention, RMS/LayerNorm, int8 GEMM/quant.
|
||||
So the GPU solver step is a plain torch elementwise op with the SAME math as ``loop/sampler.py``'s
|
||||
numpy reference (the consistency oracle that the accel stand-in already proves matches bit-for-bit).
|
||||
There is NO fused flow-match / SDE *solver* kernel in fastvideo-kernel — it ships only primitives
|
||||
(sparse/tiled attention, RMS/LayerNorm, int8 GEMM/quant). So the GPU solver step is a plain torch
|
||||
elementwise op with the SAME math as ``loop/sampler.py``'s numpy reference (which it matches bit-for-bit).
|
||||
|
||||
WRITTEN-NOT-RUN: imports torch; loaded only lazily from ``torch_cuda.py``. Under the current numpy
|
||||
loop surface these marshal numpy↔torch at the boundary (a torch-native surface would keep the latent
|
||||
on-device through forward→combine→solver — the perf follow-up, Risk G in GPU_BRINGUP.md).
|
||||
Imports torch; loaded only lazily from ``torch_cuda.py``. Under the current numpy loop surface these
|
||||
marshal numpy<->torch at the boundary (a torch-native surface would keep the latent on-device through
|
||||
forward->combine->solver — a perf follow-up).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -42,16 +41,16 @@ def flow_sde_step(x_t, velocity, sigma_t, sigma_next, *, noise=None, prev_sample
|
||||
v = v.double()
|
||||
s = min(float(sigma_t), 0.9999)
|
||||
dt = float(sigma_next) - float(sigma_t)
|
||||
std = float((s / (1.0 - s)) ** 0.5 * noise_scale)
|
||||
std = float((s / (1.0 - s))**0.5 * noise_scale)
|
||||
denom = 2.0 * max(s, 1e-6)
|
||||
mean = xt * (1.0 + std ** 2 / denom * dt) + v * (1.0 + std ** 2 * (1.0 - s) / denom) * dt
|
||||
eff_std = max(std * max(-dt, 1e-12) ** 0.5, 1e-6)
|
||||
mean = xt * (1.0 + std**2 / denom * dt) + v * (1.0 + std**2 * (1.0 - s) / denom) * dt
|
||||
eff_std = max(std * max(-dt, 1e-12)**0.5, 1e-6)
|
||||
if prev_sample is None:
|
||||
n = _as_torch(noise)[0].double() if noise is not None else torch.zeros_like(xt)
|
||||
prev = mean + eff_std * n
|
||||
else:
|
||||
prev = _as_torch(prev_sample)[0].double()
|
||||
var = eff_std ** 2
|
||||
log_prob = float((-((prev - mean) ** 2) / (2.0 * var)
|
||||
- float(np.log(eff_std)) - 0.5 * float(np.log(2.0 * np.pi))).mean().item())
|
||||
var = eff_std**2
|
||||
log_prob = float(
|
||||
(-((prev - mean)**2) / (2.0 * var) - float(np.log(eff_std)) - 0.5 * float(np.log(2.0 * np.pi))).mean().item())
|
||||
return (_back(prev.float(), was_np), log_prob, _back(mean.float(), was_np), float(eff_std))
|
||||
|
||||
@@ -4,20 +4,18 @@ Imported lazily by ``_explicit_adapter`` only on a GPU box.
|
||||
|
||||
* ``LongCatDiT`` — the flow-match velocity predictor. Mirrors ``WanDiT`` (``timestep = sigma*1000``,
|
||||
scalar ``[B]`` timestep that the DiT internally expands ``[B] -> [B, T_latent]`` for the per-latent-frame
|
||||
AdaLN time embedding) but with the **critical LongCat sign convention**: the fastvideo
|
||||
``LongCatDenoisingStage`` does ``noise_pred = -noise_pred`` *before* the flow-match scheduler step
|
||||
(longcat_denoising.py line 152). The v2 loop integrates ``x + (sigma_next-sigma)*velocity`` with the
|
||||
velocity this adapter returns, so to reproduce that step EXACTLY this adapter returns ``-velocity``.
|
||||
Forgetting the negation silently produces diverging samples (spec blocker #1).
|
||||
AdaLN time embedding) but with the critical LongCat sign convention: the fastvideo
|
||||
``LongCatDenoisingStage`` does ``noise_pred = -noise_pred`` *before* the flow-match scheduler step. The v2
|
||||
loop integrates ``x + (sigma_next-sigma)*velocity`` with the velocity this adapter returns, so to reproduce
|
||||
that step exactly this adapter returns ``-velocity``. Forgetting the negation silently diverges.
|
||||
|
||||
LongCat reuses the existing ``WanVAE`` torch adapter unchanged (same ``AutoencoderKLWan`` mean/std
|
||||
normalization) and the existing ``T5Encoder`` (UMT5, zero-padded to max_length=512 for the CFG concat
|
||||
uniform-seq contract). Both are wired on the card by ``load_id`` alone — no adapter override needed.
|
||||
|
||||
The DiT runs its AdaLN modulation / residual gating + final projection under fp32 and casts the output to
|
||||
float32 (longcat.py line 1121); we keep the module at its native bf16 dtype (``_native_dtype`` in
|
||||
``_make_dit``, matching the fastvideo stage's hardcoded bf16 target) and let ``_n`` marshal the fp32 output
|
||||
back to numpy (spec blocker #6).
|
||||
float32; we keep the module at its native bf16 dtype (``_native_dtype`` in ``_make_dit``, matching the
|
||||
fastvideo stage's hardcoded bf16 target) and let ``_n`` marshal the fp32 output back to numpy.
|
||||
|
||||
BRINGUP: written-not-run on CPU (no torch/weights here). GPU-verify against a real
|
||||
``FastVideo/LongCat-Video-T2V-Diffusers`` checkpoint — confirm the bf16 dtype, the scalar-timestep
|
||||
@@ -40,17 +38,17 @@ class LongCatDiT(TorchComponent):
|
||||
|
||||
The loop hands the raw ``sigma`` (1->0); this adapter forms ``timestep = sigma*1000`` (the Wan/diffusers
|
||||
convention LongCat shares) as a scalar ``[B]`` tensor — the DiT's ``forward`` expands ``[B] -> [B, T_latent]``
|
||||
internally for the AdaLN time embedding (longcat.py line 1075). It returns the **negated** velocity to
|
||||
fold in the fastvideo stage's ``noise_pred = -noise_pred`` (spec blocker #1)."""
|
||||
internally for the AdaLN time embedding. It returns the negated velocity to fold in the fastvideo stage's
|
||||
``noise_pred = -noise_pred``."""
|
||||
|
||||
@torch.no_grad()
|
||||
def __call__(self, latent, text_embed, sigma, context=None, *, cond=None):
|
||||
hs = self._t(latent) # [1, C=16, T, H, W]
|
||||
ehs = self._t(text_embed) # [1, N_text, 4096]
|
||||
b = hs.shape[0]
|
||||
# timestep = sigma*1000 (BRINGUP risk B / spec timestep_convention). Scalar [B]; the DiT expands
|
||||
# [B] -> [B, T_latent] internally for the per-latent-frame AdaLN time embedding. (I2V/VC would build
|
||||
# an explicit [B, T_latent] with cond frames zeroed — deferred, T2V uses the scalar path.)
|
||||
# timestep = sigma*1000 (BRINGUP risk B). Scalar [B]; the DiT expands [B] -> [B, T_latent] internally
|
||||
# for the per-latent-frame AdaLN time embedding. (I2V/VC would build an explicit [B, T_latent] with
|
||||
# cond frames zeroed — deferred, T2V uses the scalar path.)
|
||||
ts = float(sigma) * NUM_TRAIN_TIMESTEPS
|
||||
timestep = torch.full((b, ), ts, device=self.device, dtype=self.dtype)
|
||||
with self._ctx(current_timestep=ts):
|
||||
@@ -58,6 +56,6 @@ class LongCatDiT(TorchComponent):
|
||||
encoder_hidden_states=ehs,
|
||||
timestep=timestep,
|
||||
encoder_attention_mask=None)
|
||||
# CRITICAL (spec blocker #1): negate so the loop's FLOW_MATCH_STEP reproduces the fastvideo stage's
|
||||
# CRITICAL: negate so the loop's FLOW_MATCH_STEP reproduces the fastvideo stage's
|
||||
# negate-then-scheduler-step. The DiT already casts to float32; _n marshals fp32 -> numpy.
|
||||
return self._n(-velocity)
|
||||
|
||||
@@ -64,8 +64,8 @@ class MatrixGame2CausalDiT(TorchComponent):
|
||||
The loop drives the causal block-autoregressive schedule and the DMD epsilon->x0 conversion; this
|
||||
adapter is a single ``_forward_inference`` call with the cache/i2v/action plumbing. It is NOT the toy
|
||||
``dit(latent, text_embed, sigma)`` shape — the loop calls ``MatrixGame2CausalDiT.call`` explicitly with
|
||||
the causal kwargs (the toy backend exercises the SAME loop via its degenerate path; see the recipe
|
||||
loop). All arch-specifics are built INTERNALLY here so the loop stays backend-agnostic.
|
||||
the causal kwargs (the toy backend exercises the same loop via its degenerate path). All arch-specifics
|
||||
are built internally here so the loop stays backend-agnostic.
|
||||
|
||||
KV-CACHE OWNERSHIP: ``forward`` dispatches to ``_forward_inference`` ONLY when ``kv_cache is not None``;
|
||||
otherwise it falls into the train forward (which expects a non-empty text-embed list and uses the
|
||||
|
||||
@@ -4,7 +4,7 @@ so the MG3 recipe is self-contained (no edit to the shared ``_make_dit`` dispatc
|
||||
|
||||
``MatrixGame3DiT`` is the autoregressive world-model DiT (``MatrixGame3WanModel``, 30 layers, dim 3072,
|
||||
in/out_channels=48, patch (1,2,2), ``use_memory=True``, ``sigma_theta=0.8``). It departs from ``WanDiT``
|
||||
in three load-bearing ways (BRINGUP risks 1/2/7 in the spec):
|
||||
in three load-bearing ways:
|
||||
|
||||
1. PER-TOKEN timestep, NOT ``sigma*1000`` scalar. The model's ``timestep`` arg is a flattened
|
||||
``[1, T*(H/2)*(W/2)]`` LongTensor whose value is the raw FlowUniPC scheduler timestep, with the
|
||||
@@ -59,9 +59,9 @@ class MatrixGame3DiT(TorchComponent):
|
||||
|
||||
def _per_token_timestep(self, hs: torch.Tensor, sigma_value: float, cond_frames: int,
|
||||
x_memory: torch.Tensor | None) -> torch.Tensor:
|
||||
"""Build the flattened per-token timestep [1, T*(H/p)*(W/p)] (BRINGUP risk 1). The raw FlowUniPC
|
||||
timestep value fills every token; ``cond_frames`` latent rows are zeroed (pinned, not denoised);
|
||||
an all-zeros memory block is prepended when ``x_memory`` is present."""
|
||||
"""Build the flattened per-token timestep [1, T*(H/p)*(W/p)]. The raw FlowUniPC timestep value fills
|
||||
every token; ``cond_frames`` latent rows are zeroed (pinned, not denoised); an all-zeros memory block
|
||||
is prepended when ``x_memory`` is present."""
|
||||
_b, _c, t, h, w = hs.shape
|
||||
tokens_per_frame = (h // self.patch_h) * (w // self.patch_w)
|
||||
tt = hs.new_full((t, tokens_per_frame), float(sigma_value))
|
||||
|
||||
+60
-45
@@ -1,4 +1,4 @@
|
||||
"""Toy numpy components — the CPU-testable backend (design_v3 §17 honesty note).
|
||||
"""Toy numpy components — the CPU-testable backend.
|
||||
|
||||
There is no GPU/torch/weights in this environment, so the heavy Wan/LTX neural forwards are
|
||||
represented by small, deterministic numpy components. They exercise the *real* loop control
|
||||
@@ -75,17 +75,22 @@ class ToyDiT:
|
||||
self.s_ctx = 0.3
|
||||
|
||||
def _pre_tanh(self, latent: np.ndarray, text_embed, sigma: float, context) -> np.ndarray:
|
||||
mixed = np.tensordot(self.w_x, latent, axes=([1], [0])) # [C,...] channel mix
|
||||
mixed = mixed + self.w_t.reshape((self.C,) + (1,) * (latent.ndim - 1)) * float(sigma)
|
||||
mixed = np.tensordot(self.w_x, latent, axes=([1], [0])) # [C,...] channel mix
|
||||
mixed = mixed + self.w_t.reshape((self.C, ) + (1, ) * (latent.ndim - 1)) * float(sigma)
|
||||
cond = float(np.mean(text_embed)) if text_embed is not None else 0.0
|
||||
mixed = mixed + self.s_text * cond
|
||||
if context is not None:
|
||||
mixed = mixed + self.s_ctx * float(np.mean(context))
|
||||
return mixed
|
||||
|
||||
def __call__(self, latent: np.ndarray, text_embed: np.ndarray | None, sigma: float,
|
||||
context: np.ndarray | None = None, *,
|
||||
audio_latent: np.ndarray | None = None, audio_text: np.ndarray | None = None,
|
||||
def __call__(self,
|
||||
latent: np.ndarray,
|
||||
text_embed: np.ndarray | None,
|
||||
sigma: float,
|
||||
context: np.ndarray | None = None,
|
||||
*,
|
||||
audio_latent: np.ndarray | None = None,
|
||||
audio_text: np.ndarray | None = None,
|
||||
cond: np.ndarray | None = None):
|
||||
# ``cond`` (i2v mask+cond latent) is the real WanDiT's 36ch concat; the toy denoises the noise
|
||||
# channels only (image-conditioning is a GPU-path concern), so it's accepted and ignored here.
|
||||
@@ -101,22 +106,28 @@ class ToyDiT:
|
||||
return video, audio
|
||||
|
||||
# --- minimal trainable surface (so training methods do real optimizer steps) --------- #
|
||||
def clone(self) -> "ToyDiT":
|
||||
def clone(self) -> ToyDiT:
|
||||
c = ToyDiT.__new__(ToyDiT)
|
||||
c.C, c.s_text, c.s_ctx = self.C, self.s_text, self.s_ctx
|
||||
c.w_x, c.w_t = self.w_x.copy(), self.w_t.copy()
|
||||
return c
|
||||
|
||||
def blend_from(self, other: "ToyDiT", decay: float) -> None:
|
||||
"""EMA / decay-blended-old-policy update: self ← decay·self + (1-decay)·other (design_v3 §10)."""
|
||||
def blend_from(self, other: ToyDiT, decay: float) -> None:
|
||||
"""EMA / decay-blended-old-policy update: self ← decay·self + (1-decay)·other."""
|
||||
self.w_x = (decay * self.w_x + (1.0 - decay) * other.w_x).astype("float32")
|
||||
self.w_t = (decay * self.w_t + (1.0 - decay) * other.w_t).astype("float32")
|
||||
|
||||
def copy_from(self, other: "ToyDiT") -> None:
|
||||
def copy_from(self, other: ToyDiT) -> None:
|
||||
self.w_x, self.w_t = other.w_x.copy(), other.w_t.copy()
|
||||
|
||||
def mse_grad_step(self, latent: np.ndarray, text_embed, sigma: float, target: np.ndarray,
|
||||
lr: float, context=None, weight: float = 1.0) -> tuple[float, float]:
|
||||
def mse_grad_step(self,
|
||||
latent: np.ndarray,
|
||||
text_embed,
|
||||
sigma: float,
|
||||
target: np.ndarray,
|
||||
lr: float,
|
||||
context=None,
|
||||
weight: float = 1.0) -> tuple[float, float]:
|
||||
"""One exact SGD step minimizing ``weight·MSE(forward(latent), target)`` w.r.t. ``w_x``.
|
||||
|
||||
Returns (loss, grad_norm). The grad_norm is the #1396-style per-method regression signal.
|
||||
@@ -127,11 +138,11 @@ class ToyDiT:
|
||||
z = self._pre_tanh(latent, text_embed, sigma, context)
|
||||
pred = np.tanh(z)
|
||||
err = pred - target
|
||||
loss = float(weight * np.mean(err ** 2))
|
||||
loss = float(weight * np.mean(err**2))
|
||||
n = err.size
|
||||
g = weight * (2.0 * err / n) * (1.0 - pred ** 2)
|
||||
g = weight * (2.0 * err / n) * (1.0 - pred**2)
|
||||
axes = (list(range(1, g.ndim)), list(range(1, latent.ndim)))
|
||||
grad_wx = np.tensordot(g, latent, axes=axes) # [C_out, C_in]
|
||||
grad_wx = np.tensordot(g, latent, axes=axes) # [C_out, C_in]
|
||||
grad_norm = float(np.linalg.norm(grad_wx))
|
||||
self.w_x = (self.w_x - lr * grad_wx).astype("float32")
|
||||
return loss, grad_norm
|
||||
@@ -152,7 +163,7 @@ class ToyTokenizer:
|
||||
|
||||
class ToyMoTDiT(ToyDiT):
|
||||
"""Mixture-of-Transformers stand-in: ONE resident module that runs BOTH an AR (understanding)
|
||||
pathway and a diffusion (generation) pathway on shared weights (design_v3 §4.2, §16).
|
||||
pathway and a diffusion (generation) pathway on shared weights.
|
||||
|
||||
``ar_forward`` is the und pathway (next-token); ``__call__`` (inherited) is the gen pathway
|
||||
(velocity). Binding both the ar_decode and diffusion_denoise loops to one instance of this
|
||||
@@ -181,7 +192,7 @@ class ToyPromptRefiner:
|
||||
refined prompt). Sampling an action picks an embedding offset that shifts the diffusion
|
||||
conditioning; the realized reward then drives the policy by REINFORCE. This is the *second
|
||||
trainable expert* in the unified-RL stress test: one reward → a token-policy-gradient update
|
||||
here AND a FlowGRPO update on the DiT, under one advantage (design_v3 §10; PromptRL §6).
|
||||
here AND a FlowGRPO update on the DiT, under one advantage (PromptRL §6).
|
||||
|
||||
Real gradient: loss = −A·logπ(a); ∇_logits = −A·(onehot(a) − softmax). So the toy LM actually
|
||||
learns to prefer the reward-favored action — the LM-side analogue of ToyDiT.mse_grad_step.
|
||||
@@ -190,7 +201,7 @@ class ToyPromptRefiner:
|
||||
def __init__(self, n_actions: int = 8, dim: int = TEXT_DIM, seq: int = TEXT_SEQ, seed: int = 0):
|
||||
self.n = int(n_actions)
|
||||
self.dim, self.seq = int(dim), int(seq)
|
||||
self.logits = np.zeros(self.n, dtype="float64") # the trainable policy params
|
||||
self.logits = np.zeros(self.n, dtype="float64") # the trainable policy params
|
||||
rng = np.random.default_rng(seed)
|
||||
# each action ⇒ a fixed conditioning offset (the "content" of that refined prompt)
|
||||
self._emb = (rng.standard_normal((self.n, self.seq, self.dim)) * 0.1).astype("float32")
|
||||
@@ -228,25 +239,25 @@ class ToyPromptRefiner:
|
||||
p = self._probs()
|
||||
onehot = np.zeros(self.n, dtype="float64")
|
||||
onehot[int(action)] = 1.0
|
||||
grad = -float(advantage) * (onehot - p) # ∇_logits (−A·logπ(a))
|
||||
self.logits = self.logits - float(lr) * grad # descent on −A·logπ ⇒ ascent on A·logπ
|
||||
grad = -float(advantage) * (onehot - p) # ∇_logits (−A·logπ(a))
|
||||
self.logits = self.logits - float(lr) * grad # descent on −A·logπ ⇒ ascent on A·logπ
|
||||
return float(np.linalg.norm(grad))
|
||||
|
||||
def kl_to(self, other: "ToyPromptRefiner") -> float:
|
||||
def kl_to(self, other: ToyPromptRefiner) -> float:
|
||||
"""KL(π_self ‖ π_other) over the categorical — the LM-PG reference-KL penalty term."""
|
||||
p, q = self._probs(), other._probs()
|
||||
return float(np.sum(p * (np.log(p + 1e-12) - np.log(q + 1e-12))))
|
||||
|
||||
def clone(self) -> "ToyPromptRefiner":
|
||||
def clone(self) -> ToyPromptRefiner:
|
||||
c = ToyPromptRefiner.__new__(ToyPromptRefiner)
|
||||
c.n, c.dim, c.seq = self.n, self.dim, self.seq
|
||||
c.logits, c._emb = self.logits.copy(), self._emb.copy()
|
||||
return c
|
||||
|
||||
def copy_from(self, other: "ToyPromptRefiner") -> None:
|
||||
def copy_from(self, other: ToyPromptRefiner) -> None:
|
||||
self.logits = other.logits.copy()
|
||||
|
||||
def blend_from(self, other: "ToyPromptRefiner", decay: float) -> None:
|
||||
def blend_from(self, other: ToyPromptRefiner, decay: float) -> None:
|
||||
self.logits = decay * self.logits + (1.0 - decay) * other.logits
|
||||
|
||||
|
||||
@@ -265,7 +276,7 @@ class ToyTalker(ToyMoTDiT):
|
||||
|
||||
def ar_forward(self, tokens) -> int:
|
||||
ctx = sum(int(t) for t in tokens)
|
||||
return int((ctx * 7 + 13 + self.salt) % self.VOCAB) # weight-dependent (salt) + context
|
||||
return int((ctx * 7 + 13 + self.salt) % self.VOCAB) # weight-dependent (salt) + context
|
||||
|
||||
|
||||
class ToyVocoder:
|
||||
@@ -279,7 +290,7 @@ class ToyVocoder:
|
||||
def __init__(self, samples_per_token: int = 16, vocab: int = 256, seed: int = 7):
|
||||
self.spt = int(samples_per_token)
|
||||
rng = np.random.default_rng(seed)
|
||||
self.bank = (rng.standard_normal(vocab) * 0.3).astype("float32") # per-token timbre offset
|
||||
self.bank = (rng.standard_normal(vocab) * 0.3).astype("float32") # per-token timbre offset
|
||||
|
||||
def synthesize(self, token_chunk) -> np.ndarray:
|
||||
"""One chunk of codec tokens → a waveform segment in [-1, 1], length spt·len(chunk)."""
|
||||
@@ -296,11 +307,15 @@ class ToyLoRA:
|
||||
|
||||
Many of these are served over ONE resident base (the adapter is tiny: a rank-r channel delta);
|
||||
a request picks which to apply. Swappable/versioned independently (the cache key's
|
||||
``adapter_versions``). Stand-in for a real LoRA / DoRA / IP-Adapter (design_v3 §9.19)."""
|
||||
``adapter_versions``). Stand-in for a real LoRA / DoRA / IP-Adapter."""
|
||||
kind = "lora"
|
||||
|
||||
def __init__(self, adapter_id: str, channels: int = LATENT_CHANNELS, scale: float = 0.6,
|
||||
rank: int = 2, seed: int = 0):
|
||||
def __init__(self,
|
||||
adapter_id: str,
|
||||
channels: int = LATENT_CHANNELS,
|
||||
scale: float = 0.6,
|
||||
rank: int = 2,
|
||||
seed: int = 0):
|
||||
self.adapter_id = adapter_id
|
||||
self.scale = float(scale)
|
||||
rng = np.random.default_rng(seed)
|
||||
@@ -308,18 +323,18 @@ class ToyLoRA:
|
||||
self.b = (rng.standard_normal((rank, channels)) * 0.4).astype("float32")
|
||||
|
||||
def delta(self, latent, control=None) -> np.ndarray:
|
||||
w = self.a @ self.b # [C, C] low-rank weight delta
|
||||
d = np.tensordot(w, np.asarray(latent, dtype="float32"), axes=([1], [0])) # [C, ...]
|
||||
w = self.a @ self.b # [C, C] low-rank weight delta
|
||||
d = np.tensordot(w, np.asarray(latent, dtype="float32"), axes=([1], [0])) # [C, ...]
|
||||
return (self.scale * np.tanh(d)).astype("float32")
|
||||
|
||||
def update(self, seed: int) -> None: # hot-swap: replace the adapter's weights
|
||||
def update(self, seed: int) -> None: # hot-swap: replace the adapter's weights
|
||||
rng = np.random.default_rng(seed)
|
||||
self.a = (rng.standard_normal(self.a.shape) * 0.4).astype("float32")
|
||||
|
||||
|
||||
class ToyControlNet:
|
||||
"""Toy ControlNet adapter — conditions the velocity on a control signal (pose/depth/edge stand-in).
|
||||
Different control ⇒ different generation; selected per request like a LoRA (design_v3 §9.19)."""
|
||||
Different control ⇒ different generation; selected per request like a LoRA."""
|
||||
kind = "controlnet"
|
||||
|
||||
def __init__(self, adapter_id: str, channels: int = LATENT_CHANNELS, scale: float = 0.8, seed: int = 1):
|
||||
@@ -330,8 +345,8 @@ class ToyControlNet:
|
||||
|
||||
def delta(self, latent, control=None) -> np.ndarray:
|
||||
latent = np.asarray(latent, dtype="float32")
|
||||
c = float(np.mean(control)) if control is not None else 0.0 # the control image's signal
|
||||
shape = (latent.shape[0],) + (1,) * (latent.ndim - 1)
|
||||
c = float(np.mean(control)) if control is not None else 0.0 # the control image's signal
|
||||
shape = (latent.shape[0], ) + (1, ) * (latent.ndim - 1)
|
||||
return (self.scale * c * self.proj.reshape(shape)).astype("float32")
|
||||
|
||||
def update(self, seed: int) -> None:
|
||||
@@ -366,7 +381,7 @@ class ToyDraftModel:
|
||||
are accepted) and diverges otherwise (→ a rejection + a target correction). The accept rate varies
|
||||
*within* a decode, so different rounds accept different lengths (a ragged loop). The demonstration is
|
||||
exact regardless of draft quality: speculative decoding accepts only tokens the target would have
|
||||
produced, so the output equals the target's own greedy decode (design_v3 §2.2)."""
|
||||
produced, so the output equals the target's own greedy decode."""
|
||||
VOCAB = 256
|
||||
EOS = 0
|
||||
|
||||
@@ -376,9 +391,9 @@ class ToyDraftModel:
|
||||
def ar_forward(self, tokens) -> int:
|
||||
target = _spec_target_next(tokens)
|
||||
s, n = sum(int(t) for t in tokens), len(tokens)
|
||||
if ((s * 13 + n * 7) % 10) < int(round(self.agree * 10)): # agree on a varying subset
|
||||
if ((s * 13 + n * 7) % 10) < int(round(self.agree * 10)): # agree on a varying subset
|
||||
return int(target)
|
||||
return int((target + 1) % self.VOCAB) # a cheap wrong guess (rejected)
|
||||
return int((target + 1) % self.VOCAB) # a cheap wrong guess (rejected)
|
||||
|
||||
|
||||
class ToyRewardModel:
|
||||
@@ -387,7 +402,7 @@ class ToyRewardModel:
|
||||
Maps a media latent → a scalar reward in [-1, 1] via a fixed projection over per-channel features.
|
||||
The point isn't the math: it's that the reward is computed by a *model* (served, scheduled as
|
||||
``REWARD_BATCH`` work units, place-able on its own pool), not a numpy heuristic bolted onto the
|
||||
method — the reward plane composing with the serving plane (design_v3 §10)."""
|
||||
method — the reward plane composing with the serving plane."""
|
||||
|
||||
def __init__(self, dim: int = LATENT_CHANNELS, seed: int = 5):
|
||||
rng = np.random.default_rng(seed)
|
||||
@@ -407,7 +422,7 @@ class ToyAudioVAE:
|
||||
Decodes a small ``[C, A, 1, 1]`` audio latent (denoised jointly with the video latent) into a 1-D
|
||||
waveform: channel-collapse via a fixed projection, then upsample along time. Deterministic, so the
|
||||
joint A/V denoise loop stays interleave-safe. The video VAE and this share nothing — two decoders,
|
||||
one synchronized latent pair (design_v3 §15b joint A/V)."""
|
||||
one synchronized latent pair."""
|
||||
|
||||
def __init__(self, samples_per_frame: int = 16, seed: int = 2):
|
||||
self.spf = int(samples_per_frame)
|
||||
@@ -416,12 +431,12 @@ class ToyAudioVAE:
|
||||
|
||||
def decode(self, audio_latent: np.ndarray) -> np.ndarray:
|
||||
a = np.asarray(audio_latent, dtype="float32")
|
||||
flat = a.reshape(a.shape[0], -1) # [C, A·...]
|
||||
flat = a.reshape(a.shape[0], -1) # [C, A·...]
|
||||
# project over the actual channel count (LTX-2.3 audio latent is 8ch; the 2-stage toy is
|
||||
# LATENT_CHANNELS) — np.resize is identity when they already match, so existing T2VS is unchanged.
|
||||
proj = np.resize(self.proj, a.shape[0]).astype("float32")
|
||||
mono = np.tensordot(proj, flat, axes=([0], [0])) # [A·...]
|
||||
return np.tanh(np.repeat(mono, self.spf)).astype("float32") # [A·spf] mono waveform
|
||||
mono = np.tensordot(proj, flat, axes=([0], [0])) # [A·...]
|
||||
return np.tanh(np.repeat(mono, self.spf)).astype("float32") # [A·spf] mono waveform
|
||||
|
||||
|
||||
class ToyVAE:
|
||||
@@ -437,13 +452,13 @@ class ToyVAE:
|
||||
def decode(self, latent: np.ndarray) -> np.ndarray:
|
||||
"""latent [C,T,H,W] -> video [3, T, H*spatial, W*spatial] (deterministic upsample)."""
|
||||
latent = np.asarray(latent, dtype=np.float32)
|
||||
rgb = np.tensordot(self.dec_proj, latent, axes=([1], [0])) # [3,T,H,W]
|
||||
rgb = np.tensordot(self.dec_proj, latent, axes=([1], [0])) # [3,T,H,W]
|
||||
up = np.repeat(np.repeat(rgb, self.spatial, axis=2), self.spatial, axis=3)
|
||||
return np.tanh(up).astype("float32")
|
||||
|
||||
def encode(self, video: np.ndarray) -> np.ndarray:
|
||||
video = np.asarray(video, dtype=np.float32)
|
||||
pooled = video[:self.C] # crude: take C channels
|
||||
pooled = video[:self.C] # crude: take C channels
|
||||
return pooled.astype("float32")
|
||||
|
||||
|
||||
|
||||
+27
-30
@@ -1,4 +1,4 @@
|
||||
"""Platform — the detected ``(device, arch)`` that resolves both registries (design_v3 §17).
|
||||
"""Platform — the detected ``(device, arch)`` that resolves both registries.
|
||||
|
||||
One ``Platform`` per pool. It owns:
|
||||
* the **device fallback chain** (e.g. ``accel → cpu``) — so a component/kernel a backend hasn't
|
||||
@@ -9,14 +9,14 @@ One ``Platform`` per pool. It owns:
|
||||
``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.
|
||||
``Platform.detect()`` returns a CPU/numpy platform unless torch+CUDA are actually present. The
|
||||
(device, arch) machinery is exercised on a CPU box by the pure-python ``accel`` stand-in backend,
|
||||
proving the dispatch is generic without a real GPU.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterator
|
||||
from typing import Any
|
||||
from collections.abc import Iterator
|
||||
|
||||
from v2.platform.registry import COMPONENTS, KERNELS
|
||||
|
||||
@@ -36,7 +36,7 @@ def ensure_backends_loaded() -> None:
|
||||
# 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
|
||||
_PORTABLE_ARCHS = ("ptx", "generic") # vendor-neutral terminals — valid on any device
|
||||
|
||||
|
||||
def _arch_rank(arch: str) -> int:
|
||||
@@ -58,9 +58,7 @@ def _suffix_from(chain: tuple[str, ...], arch: str) -> tuple[str, ...]:
|
||||
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
|
||||
if ar < 0 or rank >= 0 and ar < rank: # portable terminal (ptx/generic): always valid
|
||||
tail.append(a)
|
||||
# else: newer-or-equal real arch → skip (would be binary-incompatible on this device)
|
||||
return tuple(tail)
|
||||
@@ -70,7 +68,7 @@ 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"):
|
||||
def __init__(self, platform: Platform):
|
||||
self.platform = platform
|
||||
self._cache: dict[tuple[str, str], Any] = {}
|
||||
|
||||
@@ -79,9 +77,8 @@ class KernelTable:
|
||||
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)")
|
||||
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]
|
||||
|
||||
@@ -92,8 +89,8 @@ class KernelTable:
|
||||
|
||||
|
||||
class Platform:
|
||||
def __init__(self, device: str, arch: str, device_chain: tuple[str, ...],
|
||||
arch_chains: dict[str, tuple[str, ...]]):
|
||||
|
||||
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)
|
||||
@@ -109,7 +106,7 @@ class Platform:
|
||||
return self._kernels
|
||||
|
||||
def arch_chain(self, device: str) -> tuple[str, ...]:
|
||||
return self.arch_chains.get(device, ("generic",))
|
||||
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]:
|
||||
@@ -135,10 +132,9 @@ class Platform:
|
||||
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")
|
||||
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()
|
||||
@@ -146,25 +142,26 @@ class Platform:
|
||||
|
||||
# --- constructors -------------------------------------------------------- #
|
||||
@classmethod
|
||||
def cpu(cls) -> "Platform":
|
||||
return cls("cpu", "numpy", ("cpu",), {"cpu": ("numpy",)})
|
||||
def cpu(cls) -> Platform:
|
||||
return cls("cpu", "numpy", ("cpu", ), {"cpu": ("numpy", )})
|
||||
|
||||
@classmethod
|
||||
def accel(cls, arch: str = "sm90") -> "Platform":
|
||||
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",)})
|
||||
return cls("accel", arch, ("accel", "cpu"), {
|
||||
"accel": _suffix_from(_ACCEL_ARCH_FALLBACK, arch),
|
||||
"cpu": ("numpy", )
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def cuda(cls, arch: str = "sm90") -> "Platform":
|
||||
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",)})
|
||||
return cls("cuda", arch, ("cuda", "cpu"), {"cuda": _suffix_from(_CUDA_ARCH_FALLBACK, arch), "cpu": ("numpy", )})
|
||||
|
||||
@classmethod
|
||||
def detect(cls) -> "Platform":
|
||||
def detect(cls) -> Platform:
|
||||
"""Honest detection: CUDA iff torch+CUDA are actually importable, else CPU/numpy."""
|
||||
ensure_backends_loaded()
|
||||
import importlib.util
|
||||
|
||||
+41
-20
@@ -1,4 +1,4 @@
|
||||
"""The two tuple-keyed backend registries — the dispatch membrane (design_v3 §17; multi-backend RFC).
|
||||
"""The two tuple-keyed backend registries — the dispatch membrane.
|
||||
|
||||
Everything device/arch-specific resolves through one of two registries, keyed by a tuple:
|
||||
|
||||
@@ -33,13 +33,14 @@ are imported lazily (``Platform.ensure_backends_loaded``) to keep this import-cy
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Iterable
|
||||
from typing import Any
|
||||
from collections.abc import 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)
|
||||
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:
|
||||
@@ -75,14 +76,20 @@ class TupleRegistry:
|
||||
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 = "",
|
||||
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,
|
||||
self._reg[key] = Registration(key=key,
|
||||
fn=fn,
|
||||
available=available or _always_available,
|
||||
source=source, meta=dict(meta or {}))
|
||||
source=source,
|
||||
meta=dict(meta or {}))
|
||||
|
||||
def lookup(self, key: tuple) -> Registration | None:
|
||||
"""Exact lookup, ignoring availability (used by ``manifest``/diagnostics)."""
|
||||
@@ -104,33 +111,47 @@ class TupleRegistry:
|
||||
"""
|
||||
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 = dict(zip(self.key_fields, key, strict=False))
|
||||
row["available"] = reg.is_available()
|
||||
row["source"] = reg.source
|
||||
row.update(reg.meta) # surface declared metadata (e.g. workspace_bytes)
|
||||
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).
|
||||
# The two registries, 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:
|
||||
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 = "",
|
||||
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,
|
||||
``workspace_bytes`` is the kernel's static scratch requirement — the capture-safety contract: 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)})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Program plane — compose a card's loops into a task (design_v3 §13)."""
|
||||
"""Program plane — compose a card's loops into a task."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2.program.specs import (
|
||||
@@ -21,6 +21,7 @@ from v2.program.workflow import (
|
||||
WorkflowStage,
|
||||
)
|
||||
|
||||
__all__ = ["Program", "ProgramKind", "ProgramNode", "ComponentNode", "ModelLoopNode",
|
||||
"Edge", "EdgeKind", "always", "when_task", "when_opt",
|
||||
"Workflow", "WorkflowStage", "WorkflowRegistry", "ParallelWorkflow", "BestOfNWorkflow"]
|
||||
__all__ = [
|
||||
"Program", "ProgramKind", "ProgramNode", "ComponentNode", "ModelLoopNode", "Edge", "EdgeKind", "always",
|
||||
"when_task", "when_opt", "Workflow", "WorkflowStage", "WorkflowRegistry", "ParallelWorkflow", "BestOfNWorkflow"
|
||||
]
|
||||
|
||||
+10
-9
@@ -1,4 +1,4 @@
|
||||
"""Programs compose a card's loops into a task (design_v3 §13).
|
||||
"""Programs compose a card's loops into a task.
|
||||
|
||||
> The card says what loops *exist*, the program says how to *run* them for this request.
|
||||
|
||||
@@ -11,7 +11,8 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
from collections.abc import Callable
|
||||
|
||||
from v2.request.tasks import TaskType
|
||||
|
||||
@@ -24,7 +25,7 @@ class ProgramKind(str, Enum):
|
||||
WORKFLOW = "workflow"
|
||||
|
||||
|
||||
# --- node predicates (replace geometry heuristics; design.md P7) -------------- #
|
||||
# --- node predicates (declarative, replacing geometry heuristics) ------------- #
|
||||
def always(_request: Any) -> bool:
|
||||
return True
|
||||
|
||||
@@ -43,8 +44,8 @@ def when_opt(node_id: str, key: str) -> Callable[[Any], bool]:
|
||||
class ProgramNode:
|
||||
node_id: str
|
||||
when: Callable[[Any], bool] = always
|
||||
reads: tuple[str, ...] = () # input slot names (typed edges in)
|
||||
writes: tuple[str, ...] = () # output slot names (typed edges out)
|
||||
reads: tuple[str, ...] = () # input slot names (typed edges in)
|
||||
writes: tuple[str, ...] = () # output slot names (typed edges out)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -59,7 +60,7 @@ class ComponentNode(ProgramNode):
|
||||
@dataclass
|
||||
class ModelLoopNode(ProgramNode):
|
||||
"""Binds one of the card's loops. The engine drives it via a LoopRunner, one step per tick,
|
||||
so its steps interleave with other requests' steps (design_v3 §5, §6.3)."""
|
||||
so its steps interleave with other requests' steps."""
|
||||
loop_id: str = ""
|
||||
output_slot: str = "latents"
|
||||
|
||||
@@ -76,7 +77,7 @@ class EdgeKind(str, Enum):
|
||||
|
||||
@dataclass
|
||||
class Edge:
|
||||
src: str # slot or node id
|
||||
src: str # slot or node id
|
||||
dst: str
|
||||
kind: EdgeKind = EdgeKind.TENSOR
|
||||
|
||||
@@ -88,13 +89,13 @@ class Program:
|
||||
kind: ProgramKind
|
||||
nodes: list[ProgramNode] = field(default_factory=list)
|
||||
edges: list[Edge] = field(default_factory=list)
|
||||
# artifact name -> slot that holds its value at the end (design_v3 §12 named artifacts)
|
||||
# artifact name -> slot that holds its value at the end
|
||||
output_artifacts: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def active_nodes(self, request: Any) -> list[ProgramNode]:
|
||||
return [n for n in self.nodes if n.when(request)]
|
||||
|
||||
def validate(self) -> "Program":
|
||||
def validate(self) -> Program:
|
||||
produced: set[str] = set()
|
||||
for n in self.nodes:
|
||||
for r in n.reads:
|
||||
|
||||
+12
-11
@@ -1,4 +1,4 @@
|
||||
"""Workflow — multi-MODEL composition above the engine (design_v3 §13; ``ProgramKind.WORKFLOW``).
|
||||
"""Workflow — multi-MODEL composition above the engine (``ProgramKind.WORKFLOW``).
|
||||
|
||||
A ``Program`` composes the loops of ONE resident model instance (the omni/MoT default — every
|
||||
``ModelLoopNode`` resolves on the program's single instance). A ``Workflow`` composes *across model
|
||||
@@ -21,7 +21,8 @@ the program runner is what preserves the interleave-parity guarantee for the sin
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -36,8 +37,8 @@ class WorkflowStage:
|
||||
|
||||
def _engine_serves(engine: Any, model_id: str) -> bool:
|
||||
if hasattr(engine, "serves"):
|
||||
return engine.serves(model_id) # AsyncEngine
|
||||
return model_id in getattr(engine, "_registry", {}) # Engine
|
||||
return engine.serves(model_id) # AsyncEngine
|
||||
return model_id in getattr(engine, "_registry", {}) # Engine
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -46,7 +47,7 @@ class Workflow:
|
||||
the SAME namespace as a card's ``model_id`` (so the engine, server, and fleet route to it the same
|
||||
way), and by convention is dotted/namespaced to avoid colliding with a card id (e.g.
|
||||
``"image_video.t2i_i2v"``). ``requires`` is the set of cards it composes — declared, validated,
|
||||
never inferred (design.md P7)."""
|
||||
never inferred."""
|
||||
workflow_id: str
|
||||
stages: list[WorkflowStage] = field(default_factory=list)
|
||||
|
||||
@@ -62,7 +63,7 @@ class Workflow:
|
||||
out.append(s.model_id)
|
||||
return out
|
||||
|
||||
def validate(self, engine: Any) -> "Workflow":
|
||||
def validate(self, engine: Any) -> Workflow:
|
||||
"""Fail-fast: every required card must already be registered on ``engine`` (no silent skips)."""
|
||||
if not self.stages:
|
||||
raise ValueError(f"workflow {self.workflow_id!r} has no stages")
|
||||
@@ -106,7 +107,7 @@ class ParallelWorkflow(Workflow):
|
||||
"""Fan-out: run every stage on the SAME initial input (conceptually in parallel — the engine can
|
||||
interleave their steps), then merge their artifacts into one Output, namespaced by stage label.
|
||||
The non-linear shape a linear chain can't express: one prompt → N models → a merged result (e.g.
|
||||
variants, or video+audio+upscale in parallel). Design_v3 §13."""
|
||||
variants, or video+audio+upscale in parallel)."""
|
||||
|
||||
def run(self, engine: Any, **initial: Any) -> Any:
|
||||
if not self.stages:
|
||||
@@ -117,13 +118,13 @@ class ParallelWorkflow(Workflow):
|
||||
rid = self.workflow_id
|
||||
for stage in self.stages:
|
||||
label = stage.label or stage.model_id
|
||||
req = stage.make_request(dict(initial)) # each branch sees the ORIGINAL input
|
||||
req = stage.make_request(dict(initial)) # each branch sees the ORIGINAL input
|
||||
if req.model_id != stage.model_id:
|
||||
raise ValueError(f"{self.workflow_id!r} stage {label!r}: model mismatch")
|
||||
out = engine.run(req)
|
||||
rid = out.request_id
|
||||
for name, art in out.artifacts.items():
|
||||
merged[f"{label}:{name}"] = art # namespaced — branches don't collide
|
||||
merged[f"{label}:{name}"] = art # namespaced — branches don't collide
|
||||
for k, v in out.metrics.items():
|
||||
metrics[f"{label}:{k}"] = v
|
||||
return Output(request_id=rid, artifacts=merged, metrics=metrics)
|
||||
@@ -132,11 +133,11 @@ class ParallelWorkflow(Workflow):
|
||||
class BestOfNWorkflow(Workflow):
|
||||
"""Inference-time scaling / rejection sampling: generate N candidates (varying seed), score each with
|
||||
a reward scorer, return the best. A feedback loop across models — generator + reward (e.g. the served
|
||||
REWARD_BATCH card, §9.15) — the other non-linear shape. Design_v3 §10/§13."""
|
||||
REWARD_BATCH card) — the other non-linear shape."""
|
||||
|
||||
def __init__(self, workflow_id, generator_stage, *, scorer, n: int = 4, score_key: str = "latents"):
|
||||
super().__init__(workflow_id, [generator_stage])
|
||||
self.scorer = scorer # any .score(media, prompts) -> {"avg": ...}
|
||||
self.scorer = scorer # any .score(media, prompts) -> {"avg": ...}
|
||||
self.n = int(n)
|
||||
self.score_key = score_key
|
||||
|
||||
|
||||
+40
-21
@@ -1,4 +1,4 @@
|
||||
"""Model definitions — concrete (recipe, runtime) cards (design_v3 §4).
|
||||
"""Model definitions — concrete (recipe, runtime) cards.
|
||||
|
||||
Phase 1: Wan2.1-1.3B (T2V), LTX2.3 (2-stage distilled), Wan-causal (self-forcing student).
|
||||
``build_default_engine`` loads all three onto one engine (one resident instance per card).
|
||||
@@ -29,20 +29,40 @@ from v2.recipes.wan21 import build_wan21_card, build_wan_t2v_program
|
||||
from v2.recipes.wan_causal import build_wan_causal_card, build_wan_causal_program
|
||||
|
||||
__all__ = [
|
||||
"build_wan21_card", "build_wan_t2v_program",
|
||||
"build_ltx2_card", "build_ltx2_program", "build_ltx2_av_program",
|
||||
"build_wan_causal_card", "build_wan_causal_program",
|
||||
"build_cosmos3_card", "build_cosmos3_program",
|
||||
"build_bagel_card", "build_bagel_program",
|
||||
"build_qwen_omni_card", "build_qwen_omni_program",
|
||||
"build_wan21_card",
|
||||
"build_wan_t2v_program",
|
||||
"build_ltx2_card",
|
||||
"build_ltx2_program",
|
||||
"build_ltx2_av_program",
|
||||
"build_wan_causal_card",
|
||||
"build_wan_causal_program",
|
||||
"build_cosmos3_card",
|
||||
"build_cosmos3_program",
|
||||
"build_bagel_card",
|
||||
"build_bagel_program",
|
||||
"build_qwen_omni_card",
|
||||
"build_qwen_omni_program",
|
||||
"build_reward_card",
|
||||
"build_speculative_card", "build_speculative_program",
|
||||
"build_unified_card", "build_unified_program",
|
||||
"build_flux_t2i_card", "build_flux_t2i_program", "build_wan_i2v_card", "build_wan_i2v_program",
|
||||
"build_t2i_then_i2v_workflow", "build_t2i_i2v_extend_workflow", "register_workflows",
|
||||
"build_tiled_card", "build_tiled_program", "build_adaptive_card",
|
||||
"build_adapter_card", "build_adapter_program",
|
||||
"build_default_engine", "build_omni_engine", "build_unified_engine", "build_image_video_engine",
|
||||
"build_speculative_card",
|
||||
"build_speculative_program",
|
||||
"build_unified_card",
|
||||
"build_unified_program",
|
||||
"build_flux_t2i_card",
|
||||
"build_flux_t2i_program",
|
||||
"build_wan_i2v_card",
|
||||
"build_wan_i2v_program",
|
||||
"build_t2i_then_i2v_workflow",
|
||||
"build_t2i_i2v_extend_workflow",
|
||||
"register_workflows",
|
||||
"build_tiled_card",
|
||||
"build_tiled_program",
|
||||
"build_adaptive_card",
|
||||
"build_adapter_card",
|
||||
"build_adapter_program",
|
||||
"build_default_engine",
|
||||
"build_omni_engine",
|
||||
"build_unified_engine",
|
||||
"build_image_video_engine",
|
||||
"build_tiled_engine",
|
||||
]
|
||||
|
||||
@@ -78,7 +98,7 @@ def build_omni_engine(engine: Any = None) -> Any:
|
||||
"""Register the phase-2 omni cards (Cosmos3 + BAGEL/lance) onto one engine.
|
||||
|
||||
Each is ONE resident MoT instance whose ``transformer`` runs BOTH an ar_decode loop and a
|
||||
diffusion_denoise loop (shared weights) — the §16 'true omni/MoT serving' claim made native.
|
||||
diffusion_denoise loop (shared weights) — true omni/MoT serving.
|
||||
"""
|
||||
from v2.cache import CacheManager
|
||||
from v2.card import load_card
|
||||
@@ -95,9 +115,8 @@ def build_omni_engine(engine: Any = None) -> Any:
|
||||
# Adding a custom pipeline is one line here (the cross-model analog of _BUILDERS / _OMNI_BUILDERS;
|
||||
# cf. vllm-omni's pipeline_registry). ``register_workflows`` registers each whose cards are present.
|
||||
_WORKFLOWS: dict[str, tuple] = {
|
||||
"image_video.t2i_i2v": (build_t2i_then_i2v_workflow,
|
||||
[(build_flux_t2i_card, build_flux_t2i_program),
|
||||
(build_wan_i2v_card, build_wan_i2v_program)]),
|
||||
"image_video.t2i_i2v": (build_t2i_then_i2v_workflow, [(build_flux_t2i_card, build_flux_t2i_program),
|
||||
(build_wan_i2v_card, build_wan_i2v_program)]),
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +140,7 @@ def register_workflows(engine: Any, *, only: list[str] | None = None) -> Any:
|
||||
def build_image_video_engine(engine: Any = None) -> Any:
|
||||
"""Register the T2I (``flux-t2i``) and I2V (``wan-i2v``) cards plus the ``image_video.t2i_i2v``
|
||||
workflow on one engine — two *separate* models chained by a cross-model workflow, addressable by
|
||||
its workflow_id like any servable (design_v3 §13)."""
|
||||
its workflow_id like any servable."""
|
||||
from v2.runtime import Engine
|
||||
eng = engine if engine is not None else Engine()
|
||||
return register_workflows(eng, only=["image_video.t2i_i2v"])
|
||||
@@ -130,7 +149,7 @@ def build_image_video_engine(engine: Any = None) -> Any:
|
||||
def build_tiled_engine(engine: Any = None) -> Any:
|
||||
"""Register the tiled-decode card (``wan-tiled``, whose VAE decode emits ``VAE_TILE`` work units)
|
||||
alongside plain Wan2.1 — so an interleaved batch mixes ``VAE_TILE`` and ``DIFFUSION_STEP`` units
|
||||
through one admission/scheduler budget (the §17 heterogeneous-co-scheduling probe)."""
|
||||
through one admission/scheduler budget (the heterogeneous co-scheduling probe)."""
|
||||
from v2.cache import CacheManager
|
||||
from v2.card import load_card
|
||||
from v2.runtime import Engine
|
||||
@@ -146,7 +165,7 @@ def build_tiled_engine(engine: Any = None) -> Any:
|
||||
def build_unified_engine(engine: Any = None) -> Any:
|
||||
"""Register the unified LM+generator card (UniRL/PromptRL): a prompt-refiner ``llm`` expert + a
|
||||
flow ``transformer`` expert on the SAME engine, each driving its own loop. Two separate experts,
|
||||
one request, both trainable under one RL reward (the §10 joint-RL stress test)."""
|
||||
one request, both trainable under one RL reward (the joint-RL stress test)."""
|
||||
from v2.cache import CacheManager
|
||||
from v2.card import load_card
|
||||
from v2.runtime import Engine
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Default negative prompts per model family — copied verbatim from fastvideo's pipeline presets
|
||||
(`fastvideo/pipelines/basic/{wan,ltx2}/presets.py`). These are stable recipe DATA (not model code), so
|
||||
v2 owns its own copy rather than importing private symbols from fastvideo's pipelines package."""
|
||||
(`fastvideo/pipelines/basic/{wan,ltx2}/presets.py`). These are stable recipe DATA, not model code,
|
||||
so v2 keeps its own copy rather than importing private symbols from fastvideo's pipelines package."""
|
||||
from __future__ import annotations
|
||||
|
||||
# Wan family — English (Wan2.1 T2V) and Chinese (Wan2.2 / self-forcing) negative prompts.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Adapter plane: one base + swappable LoRA/ControlNet adapters, selected per request (design_v3 §9.19)."""
|
||||
"""Adapter plane: one base + swappable LoRA/ControlNet adapters, selected per request."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2.recipes.adapters.card import ADAPTERS, build_adapter_card
|
||||
|
||||
+53
-28
@@ -1,4 +1,4 @@
|
||||
"""Adapter-serving card — one base DiT + a set of swappable LoRA/ControlNet adapters (design_v3 §9.19).
|
||||
"""Adapter-serving card — one base DiT + a set of swappable LoRA/ControlNet adapters.
|
||||
|
||||
Many adapters are declared as components over one resident ``transformer``; a request picks which to
|
||||
apply (``DiffusionParams.adapters``). Adapters are versioned independently (the cache key's
|
||||
@@ -24,7 +24,7 @@ from v2.parallel import ParallelPlan
|
||||
from v2.platform.backends.toy import ToyControlNet, ToyDiT, ToyLoRA, ToyTextEncoder, ToyVAE, _seed_from
|
||||
from v2.recipes.adapters.loop import AdapterDenoiseLoop
|
||||
|
||||
ADAPTERS = ("lora_anime", "lora_realistic", "control_pose") # the served adapter library
|
||||
ADAPTERS = ("lora_anime", "lora_realistic", "control_pose") # the served adapter library
|
||||
|
||||
|
||||
def build_adapter_card(model_id: str = "wan-adapters") -> ModelCard:
|
||||
@@ -34,40 +34,65 @@ def build_adapter_card(model_id: str = "wan-adapters") -> ModelCard:
|
||||
precision, expert = PrecisionPolicy(), NoRouting("transformer")
|
||||
|
||||
def loop_factory():
|
||||
return AdapterDenoiseLoop(loop_id="diffusion_denoise", cfg=cfg, flow_shift=flow,
|
||||
precision=precision, expert=expert, cost=cost)
|
||||
return AdapterDenoiseLoop(loop_id="diffusion_denoise",
|
||||
cfg=cfg,
|
||||
flow_shift=flow,
|
||||
precision=precision,
|
||||
expert=expert,
|
||||
cost=cost)
|
||||
|
||||
components = {
|
||||
"text_encoder": ComponentSpec("text_encoder", kind="text_encoder",
|
||||
factory=lambda inst: ToyTextEncoder(), required_for={"t2v"}),
|
||||
"transformer": ComponentSpec("transformer", kind="dit", factory=lambda inst: ToyDiT(seed=seed),
|
||||
resident_for=["diffusion_denoise"], required_for={"t2v"}),
|
||||
"vae": ComponentSpec("vae", kind="vae", factory=lambda inst: ToyVAE(), required_for={"t2v"}),
|
||||
"text_encoder":
|
||||
ComponentSpec("text_encoder", kind="text_encoder", factory=lambda inst: ToyTextEncoder(), required_for={"t2v"}),
|
||||
"transformer":
|
||||
ComponentSpec("transformer",
|
||||
kind="dit",
|
||||
factory=lambda inst: ToyDiT(seed=seed),
|
||||
resident_for=["diffusion_denoise"],
|
||||
required_for={"t2v"}),
|
||||
"vae":
|
||||
ComponentSpec("vae", kind="vae", factory=lambda inst: ToyVAE(), required_for={"t2v"}),
|
||||
# the adapter library — lightweight, optional, applied per request:
|
||||
"lora_anime": ComponentSpec("lora_anime", kind="adapter",
|
||||
factory=lambda inst: ToyLoRA("lora_anime", scale=0.6, seed=seed + 1),
|
||||
optional_for={"t2v"}),
|
||||
"lora_realistic": ComponentSpec("lora_realistic", kind="adapter",
|
||||
factory=lambda inst: ToyLoRA("lora_realistic", scale=0.6, seed=seed + 2),
|
||||
optional_for={"t2v"}),
|
||||
"control_pose": ComponentSpec("control_pose", kind="adapter",
|
||||
factory=lambda inst: ToyControlNet("control_pose", scale=0.8, seed=seed + 3),
|
||||
optional_for={"t2v"}),
|
||||
"lora_anime":
|
||||
ComponentSpec("lora_anime",
|
||||
kind="adapter",
|
||||
factory=lambda inst: ToyLoRA("lora_anime", scale=0.6, seed=seed + 1),
|
||||
optional_for={"t2v"}),
|
||||
"lora_realistic":
|
||||
ComponentSpec("lora_realistic",
|
||||
kind="adapter",
|
||||
factory=lambda inst: ToyLoRA("lora_realistic", scale=0.6, seed=seed + 2),
|
||||
optional_for={"t2v"}),
|
||||
"control_pose":
|
||||
ComponentSpec("control_pose",
|
||||
kind="adapter",
|
||||
factory=lambda inst: ToyControlNet("control_pose", scale=0.8, seed=seed + 3),
|
||||
optional_for={"t2v"}),
|
||||
}
|
||||
loops = {
|
||||
"diffusion_denoise": LoopSpec("diffusion_denoise", kind=LoopKind.DIFFUSION_DENOISE,
|
||||
work_unit_kind=WorkUnitKind.DIFFUSION_STEP, step_cost_model=cost,
|
||||
shared_weight_components=["transformer"], cache_policy=["feature"],
|
||||
loop_factory=loop_factory),
|
||||
"diffusion_denoise":
|
||||
LoopSpec("diffusion_denoise",
|
||||
kind=LoopKind.DIFFUSION_DENOISE,
|
||||
work_unit_kind=WorkUnitKind.DIFFUSION_STEP,
|
||||
step_cost_model=cost,
|
||||
shared_weight_components=["transformer"],
|
||||
cache_policy=["feature"],
|
||||
loop_factory=loop_factory),
|
||||
}
|
||||
return ModelCard(
|
||||
model_id=model_id, family="wan", components=components, loops=loops,
|
||||
model_id=model_id,
|
||||
family="wan",
|
||||
components=components,
|
||||
loops=loops,
|
||||
capabilities=CapabilityMatrix.of(Capability.TEXT_TO_VIDEO, Capability.VAE_DECODE),
|
||||
recipe=RecipeSpec(method="base", assumes_loop="diffusion_denoise",
|
||||
assumes_precision="float32", consistency_required=ConsistencyLevel.C1),
|
||||
recipe=RecipeSpec(method="base",
|
||||
assumes_loop="diffusion_denoise",
|
||||
assumes_precision="float32",
|
||||
consistency_required=ConsistencyLevel.C1),
|
||||
parity=ParitySpec(consistency_levels=[ConsistencyLevel.C1], interleave_required=True),
|
||||
caches={"feature": CacheContract("feature", max_bytes=1 << 24, reuse_across_requests=True)},
|
||||
caches={
|
||||
"feature": CacheContract("feature", max_bytes=1 << 24, reuse_across_requests=True)
|
||||
},
|
||||
precision=PrecisionContract(default_dtype="float32", training_precision="float32"),
|
||||
parallelism=ParallelismContract(valid_plans=[ParallelPlan.single()],
|
||||
default_plan=ParallelPlan.single()),
|
||||
parallelism=ParallelismContract(valid_plans=[ParallelPlan.single()], default_plan=ParallelPlan.single()),
|
||||
).validate()
|
||||
|
||||
+11
-10
@@ -1,10 +1,10 @@
|
||||
"""AdapterDenoiseLoop — per-request LoRA / ControlNet over one resident base (design_v3 §9.19).
|
||||
"""AdapterDenoiseLoop — per-request LoRA / ControlNet over one resident base.
|
||||
|
||||
The adapter plane: a request selects adapters (``DiffusionParams.adapters``); the loop applies each
|
||||
active adapter's velocity delta on top of the base DiT's prediction, then re-integrates. Many adapters
|
||||
are served over ONE resident base; the active set lives in the request/``LoopState`` (never global), so
|
||||
two requests using different adapters interleave without smearing. Isolated ``WanDenoiseLoop`` subclass
|
||||
(empty adapter set ⇒ identical to the base), so nothing else is touched.
|
||||
A request selects adapters (``DiffusionParams.adapters``); the loop applies each active adapter's
|
||||
velocity delta on top of the base DiT's prediction, then re-integrates. Many adapters are served over
|
||||
ONE resident base; the active set lives in the request/``LoopState`` (never global), so two requests
|
||||
using different adapters interleave without smearing. An empty adapter set is identical to the base
|
||||
``WanDenoiseLoop``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -16,10 +16,11 @@ from v2.recipes.wan21.loop import WanDenoiseLoop
|
||||
|
||||
|
||||
class AdapterDenoiseLoop(WanDenoiseLoop):
|
||||
|
||||
def init(self, req, model, ctx):
|
||||
st = super().init(req, model, ctx)
|
||||
st.scratch["adapters"] = tuple(getattr(req.diffusion, "adapters", ()) or ())
|
||||
st.scratch["control"] = ctx.slots.get("control_signal") # ControlNet conditioning (optional)
|
||||
st.scratch["control"] = ctx.slots.get("control_signal") # ControlNet conditioning (optional)
|
||||
return st
|
||||
|
||||
def next(self, st):
|
||||
@@ -34,12 +35,12 @@ class AdapterDenoiseLoop(WanDenoiseLoop):
|
||||
prec = self.precision
|
||||
|
||||
def run(model, override=None):
|
||||
res = base_run(model, override) # base velocity (CFG combine)
|
||||
res = base_run(model, override) # base velocity (CFG combine)
|
||||
v = np.asarray(res.output["noise_pred"], dtype="float32")
|
||||
for aid in adapters: # apply each active adapter's delta
|
||||
for aid in adapters: # apply each active adapter's delta
|
||||
v = v + model.component(aid).delta(x, control)
|
||||
res.output["noise_pred"] = v
|
||||
fm = model.platform.kernels.get(FLOW_MATCH_STEP) # solver dispatched per (device, arch)
|
||||
fm = model.platform.kernels.get(FLOW_MATCH_STEP) # solver dispatched per (device, arch)
|
||||
res.output["latents"] = fm(prec.cast(x), v, sigma_t, sigma_next).astype("float32")
|
||||
return res
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Adaptive-compute denoise: the loop owns cache-dit skip + early-exit (design_v3 §2.2, §9.12)."""
|
||||
"""Adaptive-compute denoise: the loop owns cache-dit skip + early-exit."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2.recipes.adaptive.card import build_adaptive_card
|
||||
|
||||
+43
-21
@@ -1,6 +1,6 @@
|
||||
"""Adaptive-compute card — a Wan-like T2V model whose denoise loop owns content-adaptive control flow
|
||||
(cache-dit skip + early-exit; design_v3 §2.2, §9.12). Reuses the Wan T2V program (loop_id
|
||||
``diffusion_denoise``); only the loop differs."""
|
||||
(cache-dit skip + early-exit). Reuses the Wan T2V program (loop_id ``diffusion_denoise``); only the
|
||||
loop differs."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2._enums import Capability, ConsistencyLevel, LoopKind, WorkUnitKind
|
||||
@@ -22,7 +22,9 @@ from v2.platform.backends.toy import ToyDiT, ToyTextEncoder, ToyVAE, _seed_from
|
||||
from v2.recipes.adaptive.loop import CacheDiTDenoiseLoop
|
||||
|
||||
|
||||
def build_adaptive_card(model_id: str = "wan-adaptive", *, cache_threshold: float = 0.02,
|
||||
def build_adaptive_card(model_id: str = "wan-adaptive",
|
||||
*,
|
||||
cache_threshold: float = 0.02,
|
||||
exit_threshold: float = 0.0) -> ModelCard:
|
||||
seed = _seed_from(model_id)
|
||||
cost = CostModel(kind=WorkUnitKind.DIFFUSION_STEP, base_seconds=1e-4, per_unit_seconds=1e-7)
|
||||
@@ -30,31 +32,51 @@ def build_adaptive_card(model_id: str = "wan-adaptive", *, cache_threshold: floa
|
||||
precision, expert = PrecisionPolicy(), NoRouting("transformer")
|
||||
|
||||
def loop_factory():
|
||||
return CacheDiTDenoiseLoop(loop_id="diffusion_denoise", cfg=cfg, flow_shift=flow,
|
||||
precision=precision, expert=expert, cost=cost,
|
||||
cache_threshold=cache_threshold, exit_threshold=exit_threshold)
|
||||
return CacheDiTDenoiseLoop(loop_id="diffusion_denoise",
|
||||
cfg=cfg,
|
||||
flow_shift=flow,
|
||||
precision=precision,
|
||||
expert=expert,
|
||||
cost=cost,
|
||||
cache_threshold=cache_threshold,
|
||||
exit_threshold=exit_threshold)
|
||||
|
||||
components = {
|
||||
"text_encoder": ComponentSpec("text_encoder", kind="text_encoder",
|
||||
factory=lambda inst: ToyTextEncoder(), required_for={"t2v"}),
|
||||
"transformer": ComponentSpec("transformer", kind="dit", factory=lambda inst: ToyDiT(seed=seed),
|
||||
resident_for=["diffusion_denoise"], required_for={"t2v"}),
|
||||
"vae": ComponentSpec("vae", kind="vae", factory=lambda inst: ToyVAE(), required_for={"t2v"}),
|
||||
"text_encoder":
|
||||
ComponentSpec("text_encoder", kind="text_encoder", factory=lambda inst: ToyTextEncoder(), required_for={"t2v"}),
|
||||
"transformer":
|
||||
ComponentSpec("transformer",
|
||||
kind="dit",
|
||||
factory=lambda inst: ToyDiT(seed=seed),
|
||||
resident_for=["diffusion_denoise"],
|
||||
required_for={"t2v"}),
|
||||
"vae":
|
||||
ComponentSpec("vae", kind="vae", factory=lambda inst: ToyVAE(), required_for={"t2v"}),
|
||||
}
|
||||
loops = {
|
||||
"diffusion_denoise": LoopSpec("diffusion_denoise", kind=LoopKind.DIFFUSION_DENOISE,
|
||||
work_unit_kind=WorkUnitKind.DIFFUSION_STEP, step_cost_model=cost,
|
||||
shared_weight_components=["transformer"], cache_policy=["feature"],
|
||||
loop_factory=loop_factory),
|
||||
"diffusion_denoise":
|
||||
LoopSpec("diffusion_denoise",
|
||||
kind=LoopKind.DIFFUSION_DENOISE,
|
||||
work_unit_kind=WorkUnitKind.DIFFUSION_STEP,
|
||||
step_cost_model=cost,
|
||||
shared_weight_components=["transformer"],
|
||||
cache_policy=["feature"],
|
||||
loop_factory=loop_factory),
|
||||
}
|
||||
return ModelCard(
|
||||
model_id=model_id, family="wan", components=components, loops=loops,
|
||||
model_id=model_id,
|
||||
family="wan",
|
||||
components=components,
|
||||
loops=loops,
|
||||
capabilities=CapabilityMatrix.of(Capability.TEXT_TO_VIDEO, Capability.VAE_DECODE),
|
||||
recipe=RecipeSpec(method="base", assumes_loop="diffusion_denoise",
|
||||
assumes_precision="float32", consistency_required=ConsistencyLevel.C1),
|
||||
recipe=RecipeSpec(method="base",
|
||||
assumes_loop="diffusion_denoise",
|
||||
assumes_precision="float32",
|
||||
consistency_required=ConsistencyLevel.C1),
|
||||
parity=ParitySpec(consistency_levels=[ConsistencyLevel.C1], interleave_required=True),
|
||||
caches={"feature": CacheContract("feature", max_bytes=1 << 24, reuse_across_requests=True)},
|
||||
caches={
|
||||
"feature": CacheContract("feature", max_bytes=1 << 24, reuse_across_requests=True)
|
||||
},
|
||||
precision=PrecisionContract(default_dtype="float32", training_precision="float32"),
|
||||
parallelism=ParallelismContract(valid_plans=[ParallelPlan.single()],
|
||||
default_plan=ParallelPlan.single()),
|
||||
parallelism=ParallelismContract(valid_plans=[ParallelPlan.single()], default_plan=ParallelPlan.single()),
|
||||
).validate()
|
||||
|
||||
+18
-21
@@ -1,16 +1,13 @@
|
||||
"""CacheDiTDenoiseLoop — the loop OWNS content-adaptive control flow (design_v3 §2.2).
|
||||
"""CacheDiTDenoiseLoop — the loop owns content-adaptive control flow.
|
||||
|
||||
The signature payoff of loop inversion: because the *model* owns control flow, content-adaptive
|
||||
decisions are natural. This loop, at each step, decides whether to:
|
||||
Because the loop owns control flow, each step can decide whether to:
|
||||
|
||||
* **skip the expensive forward** and reuse the cached velocity when consecutive predictions barely
|
||||
change (cache-dit / TeaCache / Δ-DiT — a 1.5–2× inference win), or
|
||||
* **early-exit** when the latent has converged.
|
||||
* skip the forward and reuse the cached velocity when consecutive predictions barely change
|
||||
(cache-dit / TeaCache / Δ-DiT), or
|
||||
* early-exit once the latent has converged.
|
||||
|
||||
The result is a *variable, content-dependent* step count — impossible in a ``for t in timesteps``
|
||||
runtime, trivial in a driven loop whose ``next()`` reads ``LoopState``. It is fully isolated from the
|
||||
shared ``WanDenoiseLoop`` (subclass, threshold 0 ⇒ identical behavior), and still interleave-safe:
|
||||
different requests skip different steps, and the parity gate still holds (ragged loops don't smear).
|
||||
This yields a variable, content-dependent step count. Subclass of ``WanDenoiseLoop`` (threshold 0 ⇒
|
||||
identical behavior) and interleave-safe: different requests skip different steps without smearing.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -22,10 +19,11 @@ from v2.recipes.wan21.loop import WanDenoiseLoop
|
||||
|
||||
|
||||
class CacheDiTDenoiseLoop(WanDenoiseLoop):
|
||||
|
||||
def __init__(self, *, cache_threshold: float = 0.0, exit_threshold: float = 0.0, **kw):
|
||||
super().__init__(**kw)
|
||||
self.cache_threshold = cache_threshold # rel velocity change below which we reuse (skip the DiT)
|
||||
self.exit_threshold = exit_threshold # rel latent change below which we stop early
|
||||
self.cache_threshold = cache_threshold # rel velocity change below which we reuse (skip the DiT)
|
||||
self.exit_threshold = exit_threshold # rel latent change below which we stop early
|
||||
|
||||
def init(self, req, model, ctx):
|
||||
st = super().init(req, model, ctx)
|
||||
@@ -34,8 +32,8 @@ class CacheDiTDenoiseLoop(WanDenoiseLoop):
|
||||
|
||||
def next(self, st):
|
||||
if st.scratch.get("exited"):
|
||||
return Done() # converged early — content-adaptive termination
|
||||
plan = super().next(st) # the normal full-forward step plan
|
||||
return Done() # converged early — content-adaptive termination
|
||||
plan = super().next(st) # the normal full-forward step plan
|
||||
last_v = st.scratch.get("last_v")
|
||||
if isinstance(plan, Done) or not self.cache_threshold or not st.scratch.get("skip_next") \
|
||||
or last_v is None:
|
||||
@@ -57,21 +55,20 @@ class CacheDiTDenoiseLoop(WanDenoiseLoop):
|
||||
x_prev = np.asarray(st.latents["video"], dtype="float64")
|
||||
v = np.asarray(result.output["noise_pred"], dtype="float64")
|
||||
prev_v = st.scratch.get("last_v")
|
||||
st = super().advance(st, result) # folds latents, step_idx, (rollout) trajectory
|
||||
st = super().advance(st, result) # folds latents, step_idx, (rollout) trajectory
|
||||
if result.output.get("skipped"):
|
||||
st.scratch["skipped"] += 1
|
||||
st.scratch["skip_next"] = False # don't chain skips — re-check with a full forward
|
||||
st.scratch["skip_next"] = False # don't chain skips — re-check with a full forward
|
||||
elif self.cache_threshold and prev_v is not None:
|
||||
rel = float(np.linalg.norm(v - np.asarray(prev_v, dtype="float64"))
|
||||
/ (np.linalg.norm(prev_v) + 1e-8))
|
||||
st.scratch["skip_next"] = rel < self.cache_threshold # next prediction will barely change
|
||||
rel = float(np.linalg.norm(v - np.asarray(prev_v, dtype="float64")) / (np.linalg.norm(prev_v) + 1e-8))
|
||||
st.scratch["skip_next"] = rel < self.cache_threshold # next prediction will barely change
|
||||
st.scratch["last_v"] = result.output["noise_pred"]
|
||||
half = max(1, (len(st.sigmas) - 1) // 2) # only consider exiting in the low-noise tail
|
||||
half = max(1, (len(st.sigmas) - 1) // 2) # only consider exiting in the low-noise tail
|
||||
if self.exit_threshold and st.step_idx >= half:
|
||||
x_next = np.asarray(st.latents["video"], dtype="float64")
|
||||
dl = float(np.linalg.norm(x_next - x_prev) / (np.linalg.norm(x_prev) + 1e-8))
|
||||
if dl < self.exit_threshold:
|
||||
st.scratch["exited"] = True # latent converged → stop next tick
|
||||
st.scratch["exited"] = True # latent converged → stop next tick
|
||||
return st
|
||||
|
||||
def finalize(self, st):
|
||||
|
||||
+53
-34
@@ -1,13 +1,12 @@
|
||||
"""BAGEL/lance-style MoT ModelCard — the canonical vllm-omni omni model (design_v3 §1, §16, §19).
|
||||
"""BAGEL/lance-style MoT ModelCard — canonical vllm-omni omni model.
|
||||
|
||||
vllm-omni's ``bagel_single_stage``/``lance`` prove one resident MoT instance can run both AR
|
||||
``generate_text`` and diffusion ``generate_image`` on co-resident experts in a single request — BUT
|
||||
they bury that interleaving inside one opaque ``DIFFUSION`` stage the scheduler never sees inside
|
||||
(``max_num_running_reqs=1``). This card expresses the same shared-weight MoT, but makes both loops
|
||||
**runtime-visible, step-scheduled, and batchable**: ``generate_text`` (ar_decode) and
|
||||
``generate_image`` (diffusion_denoise) both bind the one resident ``transformer``, and every AR token
|
||||
and denoise step is a WorkUnit the scheduler can interleave and price. That visibility is the
|
||||
differentiation the §1 thesis names.
|
||||
vllm-omni's ``bagel_single_stage``/``lance`` run one resident MoT instance doing both AR
|
||||
``generate_text`` and diffusion ``generate_image`` on co-resident experts in a single request, but
|
||||
bury the interleaving inside one opaque ``DIFFUSION`` stage the scheduler can't see into
|
||||
(``max_num_running_reqs=1``). This card expresses the same shared-weight MoT with both loops
|
||||
runtime-visible, step-scheduled, and batchable: ``generate_text`` (ar_decode) and ``generate_image``
|
||||
(diffusion_denoise) both bind the one resident ``transformer``, and every AR token and denoise step
|
||||
is a WorkUnit the scheduler can interleave and price.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -42,44 +41,64 @@ def build_bagel_card(model_id: str = "bagel-mot") -> ModelCard:
|
||||
return ARDecodeLoop(loop_id="generate_text", transformer_id="transformer", cost=ar_cost, max_tokens=6)
|
||||
|
||||
def image_factory():
|
||||
return WanDenoiseLoop(loop_id="generate_image", cfg=cfg, flow_shift=flow,
|
||||
precision=precision, expert=expert, cost=dn_cost)
|
||||
return WanDenoiseLoop(loop_id="generate_image",
|
||||
cfg=cfg,
|
||||
flow_shift=flow,
|
||||
precision=precision,
|
||||
expert=expert,
|
||||
cost=dn_cost)
|
||||
|
||||
components = {
|
||||
"tokenizer": ComponentSpec("tokenizer", kind="tokenizer", factory=lambda inst: ToyTokenizer(),
|
||||
required_for={"reason", "t2i"}),
|
||||
"transformer": ComponentSpec(
|
||||
"transformer", kind="dit",
|
||||
"tokenizer":
|
||||
ComponentSpec("tokenizer",
|
||||
kind="tokenizer",
|
||||
factory=lambda inst: ToyTokenizer(),
|
||||
required_for={"reason", "t2i"}),
|
||||
"transformer":
|
||||
ComponentSpec(
|
||||
"transformer",
|
||||
kind="dit",
|
||||
load_id="vllm_omni.diffusion.models.bagel:BagelTransformer",
|
||||
factory=lambda inst: ToyMoTDiT(seed=seed),
|
||||
resident_for=["generate_text", "generate_image"], # one resident copy for BOTH loops
|
||||
resident_for=["generate_text", "generate_image"], # one resident copy for BOTH loops
|
||||
required_for={"reason", "t2i"}),
|
||||
"vae": ComponentSpec("vae", kind="vae", factory=lambda inst: ToyVAE(), required_for={"t2i"}),
|
||||
"vae":
|
||||
ComponentSpec("vae", kind="vae", factory=lambda inst: ToyVAE(), required_for={"t2i"}),
|
||||
}
|
||||
loops = {
|
||||
"generate_text": LoopSpec("generate_text", kind=LoopKind.AR_DECODE,
|
||||
work_unit_kind=WorkUnitKind.AR_TOKEN, step_cost_model=ar_cost,
|
||||
shared_weight_components=["transformer"], cache_policy=["paged_kv"],
|
||||
loop_factory=text_factory),
|
||||
"generate_image": LoopSpec("generate_image", kind=LoopKind.DIFFUSION_DENOISE,
|
||||
work_unit_kind=WorkUnitKind.DIFFUSION_STEP, step_cost_model=dn_cost,
|
||||
shared_weight_components=["transformer"], cache_policy=["feature"],
|
||||
loop_factory=image_factory),
|
||||
"generate_text":
|
||||
LoopSpec("generate_text",
|
||||
kind=LoopKind.AR_DECODE,
|
||||
work_unit_kind=WorkUnitKind.AR_TOKEN,
|
||||
step_cost_model=ar_cost,
|
||||
shared_weight_components=["transformer"],
|
||||
cache_policy=["paged_kv"],
|
||||
loop_factory=text_factory),
|
||||
"generate_image":
|
||||
LoopSpec("generate_image",
|
||||
kind=LoopKind.DIFFUSION_DENOISE,
|
||||
work_unit_kind=WorkUnitKind.DIFFUSION_STEP,
|
||||
step_cost_model=dn_cost,
|
||||
shared_weight_components=["transformer"],
|
||||
cache_policy=["feature"],
|
||||
loop_factory=image_factory),
|
||||
}
|
||||
card = ModelCard(
|
||||
model_id=model_id, family="bagel", components=components, loops=loops,
|
||||
capabilities=CapabilityMatrix.of(Capability.TEXT_TO_IMAGE, Capability.REASONING_TEXT,
|
||||
Capability.VAE_DECODE),
|
||||
recipe=RecipeSpec(method="base", assumes_loop="generate_image",
|
||||
assumes_precision="float32", consistency_required=ConsistencyLevel.C1),
|
||||
model_id=model_id,
|
||||
family="bagel",
|
||||
components=components,
|
||||
loops=loops,
|
||||
capabilities=CapabilityMatrix.of(Capability.TEXT_TO_IMAGE, Capability.REASONING_TEXT, Capability.VAE_DECODE),
|
||||
recipe=RecipeSpec(method="base",
|
||||
assumes_loop="generate_image",
|
||||
assumes_precision="float32",
|
||||
consistency_required=ConsistencyLevel.C1),
|
||||
parity=ParitySpec(consistency_levels=[ConsistencyLevel.C1], interleave_required=True),
|
||||
caches={
|
||||
"feature": CacheContract("feature", max_bytes=1 << 24, reuse_across_requests=True),
|
||||
"paged_kv": CacheContract("paged_kv", max_bytes=1 << 24, block_bytes=1 << 12,
|
||||
reuse_across_requests=False),
|
||||
"paged_kv": CacheContract("paged_kv", max_bytes=1 << 24, block_bytes=1 << 12, reuse_across_requests=False),
|
||||
},
|
||||
precision=PrecisionContract(default_dtype="float32", training_precision="float32"),
|
||||
parallelism=ParallelismContract(valid_plans=[ParallelPlan.single()],
|
||||
default_plan=ParallelPlan.single()),
|
||||
parallelism=ParallelismContract(valid_plans=[ParallelPlan.single()], default_plan=ParallelPlan.single()),
|
||||
)
|
||||
return card.validate()
|
||||
|
||||
+31
-15
@@ -1,9 +1,9 @@
|
||||
"""BAGEL/lance program (design_v3 §16): one resident MoT instance, two runtime-visible loops.
|
||||
"""BAGEL/lance program: one resident MoT instance, two runtime-visible loops.
|
||||
|
||||
tokenize → generate_text (ar_decode) → pack(text→cond) → generate_image (diffusion) → vae_decode
|
||||
|
||||
generate_text and generate_image hit the SAME resident weights; their steps are WorkUnits the
|
||||
scheduler interleaves — the shared-weight MoT made native, not a monolith bypassing the abstraction.
|
||||
scheduler interleaves.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -13,19 +13,35 @@ from v2.recipes.omni import emit_text_node, pack_cond_from_tokens, tokenize_node
|
||||
|
||||
def build_bagel_program() -> Program:
|
||||
return Program(
|
||||
program_id="bagel.mot", kind=ProgramKind.INLINE,
|
||||
program_id="bagel.mot",
|
||||
kind=ProgramKind.INLINE,
|
||||
nodes=[
|
||||
ComponentNode("tokenize", fn=tokenize_node, writes=("prompt_tokens",)),
|
||||
ModelLoopNode("generate_text", loop_id="generate_text", output_slot="gen_text_out",
|
||||
reads=("prompt_tokens",), writes=("gen_text_out",)),
|
||||
ComponentNode("emit_text", fn=emit_text_node("gen_text_out", "text"),
|
||||
reads=("gen_text_out",), writes=("text",)),
|
||||
ComponentNode("pack", fn=pack_cond_from_tokens("gen_text_out"),
|
||||
reads=("gen_text_out",), writes=("text_embeds", "neg_text_embeds")),
|
||||
ModelLoopNode("generate_image", loop_id="generate_image", output_slot="gen_image_out",
|
||||
reads=("text_embeds",), writes=("gen_image_out",)),
|
||||
ComponentNode("vae_decode", fn=vae_decode_node("gen_image_out", "image"),
|
||||
reads=("gen_image_out",), writes=("image",)),
|
||||
ComponentNode("tokenize", fn=tokenize_node, writes=("prompt_tokens", )),
|
||||
ModelLoopNode("generate_text",
|
||||
loop_id="generate_text",
|
||||
output_slot="gen_text_out",
|
||||
reads=("prompt_tokens", ),
|
||||
writes=("gen_text_out", )),
|
||||
ComponentNode("emit_text",
|
||||
fn=emit_text_node("gen_text_out", "text"),
|
||||
reads=("gen_text_out", ),
|
||||
writes=("text", )),
|
||||
ComponentNode("pack",
|
||||
fn=pack_cond_from_tokens("gen_text_out"),
|
||||
reads=("gen_text_out", ),
|
||||
writes=("text_embeds", "neg_text_embeds")),
|
||||
ModelLoopNode("generate_image",
|
||||
loop_id="generate_image",
|
||||
output_slot="gen_image_out",
|
||||
reads=("text_embeds", ),
|
||||
writes=("gen_image_out", )),
|
||||
ComponentNode("vae_decode",
|
||||
fn=vae_decode_node("gen_image_out", "image"),
|
||||
reads=("gen_image_out", ),
|
||||
writes=("image", )),
|
||||
],
|
||||
output_artifacts={"text": "text", "image": "image"},
|
||||
output_artifacts={
|
||||
"text": "text",
|
||||
"image": "image"
|
||||
},
|
||||
).validate()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Shared component-node helpers (text encode with feature cache, etc.).
|
||||
|
||||
The content-hash feature cache (design_v3 §7.2) is the reuse mechanism behind the §10 claim that
|
||||
a K-sample RL group encodes its shared prompt once instead of K times.
|
||||
The content-hash feature cache lets a K-sample RL group encode its shared prompt once instead of
|
||||
K times.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -14,14 +14,15 @@ def cached_text_encode(instance: Any, text: str) -> Any:
|
||||
enc = instance.component("text_encoder")
|
||||
cache = instance.caches
|
||||
if cache is not None and cache.has("feature"):
|
||||
# §7.1: every output-semantic field is in the key — adapter stack + precision partition it,
|
||||
# and the text-encoder's OWN version (not the instance's) so a transformer weight sync
|
||||
# does not invalidate the (frozen) text encoder's embeddings.
|
||||
key = CacheKey(model_id=instance.card.model_id, component_id="text_encoder",
|
||||
# Every output-semantic field is in the key — adapter stack + precision partition it, and the
|
||||
# text-encoder's OWN version (not the instance's) so a transformer weight sync does not
|
||||
# invalidate the (frozen) text encoder's embeddings.
|
||||
key = CacheKey(model_id=instance.card.model_id,
|
||||
component_id="text_encoder",
|
||||
weights_version=instance.version_of("text_encoder"),
|
||||
adapter_versions=CacheKey.adapters(instance.adapter_versions),
|
||||
precision=instance.card.precision.dtype_for("text_encoder"),
|
||||
input_hashes=(("text", content_hash(text)),))
|
||||
input_hashes=(("text", content_hash(text)), ))
|
||||
hit = cache.pool("feature").get(key)
|
||||
if hit is not None:
|
||||
return hit
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Cosmos-Predict2 (Video2World) — EDM-Karras denoiser ported into the v2 substrate.
|
||||
"""Cosmos-Predict2 (Video2World) EDM-Karras denoiser recipe.
|
||||
|
||||
Self-contained recipe package (the bucket-C pattern): card declares its torch adapters via
|
||||
``ComponentSpec.adapter`` (``CosmosDiT``/``CosmosT5Encoder`` in ``v2/platform/backends/torch_cosmos.py``)
|
||||
and a new ``CosmosDenoiseLoop`` (EDM preconditioning folded into flow-match Euler), reusing the Wan VAE
|
||||
adapter + T5 + ``stamp_wan21_checkpoints``. Registered in ``v2/registry.py``.
|
||||
Self-contained recipe: the card declares torch adapters (``CosmosDiT``/``CosmosT5Encoder`` in
|
||||
``v2/platform/backends/torch_cosmos.py``) plus ``CosmosDenoiseLoop`` (EDM preconditioning folded into
|
||||
flow-match Euler), reusing the Wan VAE adapter + T5 + ``stamp_wan21_checkpoints``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"""Cosmos-Predict2-2B-Video2World ModelCard — text→video (the registered preset; video2world
|
||||
"""Cosmos-Predict2-2B-Video2World ModelCard — text->video (the registered preset; video2world
|
||||
conditioning is a later capability the loop already threads).
|
||||
|
||||
Architecture deltas vs Wan (all GPU-path, declared on the card so the recipe is self-contained):
|
||||
* DiT ``fastvideo.models.dits.cosmos:CosmosTransformer3DModel`` — an **EDM denoiser** (NOT flow-match);
|
||||
the adapter (``ComponentSpec.adapter`` -> ``CosmosDiT``) returns the raw network output and
|
||||
``CosmosDenoiseLoop`` does the EDM ``c_in/c_skip/c_out`` → x0 reconstruction + x0-space CFG + the
|
||||
Karras (ρ=7, σ_max=80→σ_min=0.002) schedule.
|
||||
* VAE ``fastvideo.models.vaes.wanvae:AutoencoderKLWan`` — Wan-style (z=16, 8×/4×); reuses the v2
|
||||
``WanVAE`` adapter unchanged (sigma_data=1.0 makes the Cosmos *σ_data factor a no-op).
|
||||
Architecture deltas vs Wan (declared on the card so the recipe is self-contained):
|
||||
* DiT ``fastvideo.models.dits.cosmos:CosmosTransformer3DModel`` — an EDM denoiser (not flow-match); the
|
||||
``CosmosDiT`` adapter returns the raw network output and ``CosmosDenoiseLoop`` does the EDM
|
||||
``c_in/c_skip/c_out`` -> x0 reconstruction + x0-space CFG + the Karras (rho=7, sigma 80->0.002) schedule.
|
||||
* VAE ``fastvideo.models.vaes.wanvae:AutoencoderKLWan`` — Wan-style (z=16, 8x/4x); reuses the v2
|
||||
``WanVAE`` adapter unchanged (sigma_data=1.0 makes the Cosmos sigma_data factor a no-op).
|
||||
* Text ``fastvideo.models.encoders.t5:T5EncoderModel`` (T5-Large, 1024-dim) via ``CosmosT5Encoder``
|
||||
(raw last_hidden_state, no Wan zero-pad).
|
||||
``stamp_wan21_checkpoints`` applies (diffusers transformer/vae/text_encoder subfolder layout).
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"""CosmosDenoiseLoop — EDM-Karras preconditioning folded into a flow-match Euler integrator.
|
||||
|
||||
Cosmos-Predict2 is NOT a plain flow-match model despite the pipeline wrapping a
|
||||
Cosmos-Predict2 is not a plain flow-match model despite the pipeline wrapping a
|
||||
``FlowMatchEulerDiscreteScheduler``. The network is an EDM denoiser ``F_θ`` whose preconditioned output
|
||||
reconstructs ``x0`` (``x0 = c_skip·x + c_out·F_θ(x·c_in)``, ``sigma_data=1``); CFG combines in **x0
|
||||
space**; then ``x0`` is converted to a flow-match velocity ``(x - x0)/σ`` for the deterministic Euler
|
||||
update ``x_next = x + (σ_next - σ)·v``. The σ schedule is Karras (ρ=7, σ_max=80 → σ_min=0.002), NOT the
|
||||
flow-shift linspace, and the latent is initialized at ``randn·σ_max``. Faithful port of
|
||||
reconstructs ``x0`` (``x0 = c_skip·x + c_out·F_θ(x·c_in)``, ``sigma_data=1``); CFG combines in x0 space;
|
||||
then ``x0`` is converted to a flow-match velocity ``(x - x0)/σ`` for the Euler update
|
||||
``x_next = x + (σ_next - σ)·v``. The σ schedule is Karras (ρ=7, σ_max=80 -> σ_min=0.002), not the
|
||||
flow-shift linspace, and the latent starts at ``randn·σ_max``. Port of
|
||||
``fastvideo/pipelines/stages/denoising.py:CosmosDenoisingStage`` (see that file for the exact math).
|
||||
|
||||
video2world (frame_replace) conditioning is threaded (``conditioning_latents``/``cond_indicator`` from
|
||||
slots) but is ``None`` for the registered t2v preset → the gated injection branch is inert; the loop
|
||||
degrades to pure t2v exactly as the fastvideo pipeline does when no image/video is given.
|
||||
slots) but is ``None`` for the registered t2v preset -> the gated injection is inert; the loop degrades
|
||||
to pure t2v exactly as the fastvideo pipeline does when no image/video is given.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Cosmos-Predict2 t2v program: text_encode → diffusion_denoise (EDM) → vae_decode.
|
||||
|
||||
Same inline shape as the Wan t2v program — the EDM specifics live entirely in ``CosmosDenoiseLoop`` and
|
||||
the ``CosmosDiT`` adapter, so the node graph is unchanged. (video2world conditioning would add a
|
||||
VAE-encode node writing ``conditioning_latents``/``cond_indicator`` into slots; the loop already reads
|
||||
them.)
|
||||
Same inline shape as the Wan t2v program — the EDM specifics live in ``CosmosDenoiseLoop`` and the
|
||||
``CosmosDiT`` adapter, so the node graph is unchanged. (video2world conditioning would add a VAE-encode
|
||||
node writing ``conditioning_latents``/``cond_indicator`` into slots; the loop already reads them.)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"""Cosmos-Predict2.5 (text->video) — a flow-match denoiser ported into the v2 substrate.
|
||||
"""Cosmos-Predict2.5 (text->video) flow-match denoiser recipe.
|
||||
|
||||
Self-contained recipe package (the bucket-C pattern): the card declares its torch adapters via
|
||||
``ComponentSpec.adapter`` (``Cosmos25DiT`` / ``Cosmos25WanVAE`` / ``Cosmos25Reason1Encoder`` in
|
||||
``v2/platform/backends/torch_cosmos25.py``) and a new ``Cosmos25DenoiseLoop`` (Wan's flow-shift sigma
|
||||
schedule + ``FLOW_MATCH_STEP`` solver + ``ClassicCFG``, but the PLAIN per-frame sigma timestep ``[B,T]``
|
||||
Cosmos2.5 needs — NOT Wan's ``sigma*1000``), reusing ``stamp_wan21_checkpoints``. Covers both the 2B and
|
||||
14B sizes (the card is size-agnostic — size resolves from the checkpoint config). Registered in
|
||||
``v2/registry.py`` by the orchestrator from the returned metadata.
|
||||
Self-contained recipe: the card declares torch adapters (``Cosmos25DiT`` / ``Cosmos25WanVAE`` /
|
||||
``Cosmos25Reason1Encoder`` in ``v2/platform/backends/torch_cosmos25.py``) plus ``Cosmos25DenoiseLoop``,
|
||||
which reuses Wan's flow-shift sigma schedule + ``FLOW_MATCH_STEP`` + ``ClassicCFG`` but feeds the DiT the
|
||||
plain per-frame sigma timestep ``[B,T]`` Cosmos2.5 needs (not Wan's ``sigma*1000``). Reuses
|
||||
``stamp_wan21_checkpoints``. Size-agnostic (2B/14B resolve from the checkpoint config).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
+31
-45
@@ -1,32 +1,20 @@
|
||||
"""Cosmos25DenoiseLoop — a flow-match Euler denoiser with Cosmos2.5's per-frame sigma timestep.
|
||||
"""Cosmos25DenoiseLoop — flow-match Euler denoiser with Cosmos2.5's per-frame sigma timestep.
|
||||
|
||||
Cosmos-Predict2.5 IS a rectified-flow / flow-match model (unlike Cosmos-Predict2, which is an EDM
|
||||
denoiser). The sigma schedule and the deterministic Euler solver are EXACTLY Wan's
|
||||
(``FlowShiftPolicy.build_schedule`` + ``FLOW_MATCH_STEP``), and CFG is the standard ``ClassicCFG``
|
||||
(``uncond + s*(cond-uncond)``). So this loop is almost identical to ``WanDenoiseLoop``; it forks it for
|
||||
ONE load-bearing reason (blocker (1)):
|
||||
Cosmos-Predict2.5 is a flow-match model (unlike Cosmos-Predict2's EDM denoiser). The sigma schedule and
|
||||
Euler solver are Wan's (``FlowShiftPolicy.build_schedule`` + ``FLOW_MATCH_STEP``, shift=5.0) and CFG is
|
||||
``ClassicCFG``, so this loop nearly matches ``WanDenoiseLoop``. The one fork: Wan feeds the DiT a scalar
|
||||
``timestep = sigma*1000``, but Cosmos2.5 feeds a per-frame 2D ``timestep[B,T]`` of the plain sigma in
|
||||
[0,1]. So we set ``st.timesteps = st.sigmas`` (no *1000) and the ``Cosmos25DiT`` adapter broadcasts the
|
||||
plain sigma to ``[B, T]`` and builds the mandatory zero ``condition_mask`` / ones ``padding_mask`` +
|
||||
``fps`` the forward requires (the model concats the masks itself — we feed the raw 16ch latent). See
|
||||
``v2/platform/backends/torch_cosmos25.py:Cosmos25DiT`` and the source
|
||||
``fastvideo/pipelines/stages/denoising.py:Cosmos25DenoisingStage``.
|
||||
|
||||
* Wan feeds the DiT a SCALAR ``timestep = sigma * num_train_timesteps`` (``sigma*1000``).
|
||||
* Cosmos2.5 feeds a PER-FRAME 2D ``timestep[B,T]`` of the PLAIN sigma in [0,1] — the fastvideo stage
|
||||
computes ``t * 0.001`` of the 0..1000 schedule, which is precisely the shifted sigma. So we set
|
||||
``st.timesteps = st.sigmas`` (NO *1000) and the ``Cosmos25DiT`` adapter broadcasts that plain sigma
|
||||
to ``[B, T]`` and builds the mandatory zero ``condition_mask`` / ones ``padding_mask`` + ``fps`` the
|
||||
forward requires (the model concats the masks itself — we feed the RAW 16ch latent). See
|
||||
``v2/platform/backends/torch_cosmos25.py:Cosmos25DiT`` and the faithful source
|
||||
``fastvideo/pipelines/stages/denoising.py:Cosmos25DenoisingStage``.
|
||||
|
||||
Verification that the math matches the source: the fastvideo ``FlowMatchEulerDiscreteScheduler``
|
||||
(shift=5.0) builds ``sigma' = shift*sigma/(1+(shift-1)*sigma)`` over a 1->0 base + a terminal 0, sets
|
||||
``timesteps = sigma'*1000``, feeds the DiT ``timesteps*0.001 = sigma'``, and steps
|
||||
``prev = sample + (sigma_next - sigma)*velocity`` — i.e. exactly ``build_flow_sigmas(num_steps,
|
||||
shift=5.0)`` + ``FLOW_MATCH_STEP``.
|
||||
|
||||
video2world / image2world (frame_replace) conditioning (cond_indicator GT-x0 clamp + GT-velocity
|
||||
injection, conditional_frame_timestep=0.1 on conditioned frames) is present in fastvideo
|
||||
(``Cosmos25V2WDenoisingStage``) but is NOT in the registered HF presets — this loop scopes to t2v.
|
||||
The hooks are read from ``ctx.slots`` (None for t2v -> inert), mirroring the Wan/Cosmos2 pattern, so a
|
||||
later capability adds the gated injection without changing the t2v path. (BRINGUP: frame-replace needs a
|
||||
request-API extension to carry the conditioning latents + indicator.)
|
||||
video2world / image2world (frame_replace) conditioning exists in fastvideo
|
||||
(``Cosmos25V2WDenoisingStage``) but not in the registered HF presets, so this loop is t2v-only. The hooks
|
||||
are read from ``ctx.slots`` (None for t2v -> inert), mirroring Wan/Cosmos2, so a later capability can add
|
||||
the gated injection without touching the t2v path. (BRINGUP: frame-replace needs a request-API extension
|
||||
to carry the conditioning latents + indicator.)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -48,9 +36,8 @@ from v2.loop.contracts import (
|
||||
from v2.platform import FLOW_MATCH_STEP
|
||||
from v2.recipes.wan21.loop import latent_shape
|
||||
|
||||
# Real Cosmos2.5 Wan-style VAE (Cosmos25WanVAE) compression: z_dim=16, 4x temporal, 8x spatial — the
|
||||
# same geometry as Wan2.1, so ``latent_shape`` (reused from the Wan recipe) yields
|
||||
# (16, (F-1)//4 + 1, H//8, W//8) on the real backend.
|
||||
# Cosmos2.5 Wan-style VAE (Cosmos25WanVAE): z_dim=16, 4x temporal, 8x spatial — same geometry as Wan2.1,
|
||||
# so the reused ``latent_shape`` yields (16, (F-1)//4 + 1, H//8, W//8) on the real backend.
|
||||
COSMOS25_LATENT_CHANNELS = 16
|
||||
COSMOS25_TEMPORAL_RATIO = 4
|
||||
COSMOS25_SPATIAL_RATIO = 8
|
||||
@@ -84,8 +71,7 @@ class Cosmos25DenoiseLoop:
|
||||
def init(self, req, model, ctx) -> LoopState:
|
||||
seed = req.diffusion.seed if req.diffusion.seed is not None else 0
|
||||
rng = np.random.default_rng(seed)
|
||||
# The σ schedule + flow-shift are Wan's verbatim (shift=5.0 for Cosmos2.5): 1 -> 0 over
|
||||
# num_steps+1 points with the flow-shift warp applied.
|
||||
# Wan's flow-shift sigma schedule (shift=5.0): 1 -> 0 over num_steps+1 points, warped.
|
||||
sig = self.flow_shift.build_schedule(req.diffusion.num_steps,
|
||||
req.diffusion.height,
|
||||
req.diffusion.width,
|
||||
@@ -106,16 +92,16 @@ class Cosmos25DenoiseLoop:
|
||||
seed=seed,
|
||||
latents={"video": x},
|
||||
sigmas=[float(s) for s in sig],
|
||||
# BLOCKER (1): the model timestep is the PLAIN sigma (NOT sigma*1000). The
|
||||
# Cosmos25DiT adapter broadcasts it to the per-frame [B, T] form the forward needs.
|
||||
# Model timestep is the plain sigma (not sigma*1000); the Cosmos25DiT adapter
|
||||
# broadcasts it to the per-frame [B, T] form the forward needs.
|
||||
timesteps=[float(s) for s in sig])
|
||||
st.cond["prompt_embeds"] = ctx.slots.get("text_embeds")
|
||||
st.cond["negative_prompt_embeds"] = ctx.slots.get("neg_text_embeds")
|
||||
st.scratch["guidance_scale"] = float(req.diffusion.guidance_scale)
|
||||
st.scratch["stream_video"] = bool(req.outputs.stream.get("video"))
|
||||
st.scratch["fps"] = int(getattr(req.diffusion, "fps", None) or COSMOS25_DEFAULT_FPS)
|
||||
# video2world (frame_replace) conditioning: None for the t2v preset -> the gated injection a later
|
||||
# capability would add is inert (pure t2v, matching the fastvideo pipeline with no image/video).
|
||||
# video2world (frame_replace) conditioning: None for the t2v preset -> any later gated injection
|
||||
# is inert (pure t2v, matching the fastvideo pipeline with no image/video).
|
||||
st.scratch["conditioning_latents"] = ctx.slots.get("conditioning_latents")
|
||||
st.scratch["cond_indicator"] = ctx.slots.get("cond_indicator")
|
||||
st.plugin_state["cfg"] = {}
|
||||
@@ -126,8 +112,8 @@ class Cosmos25DenoiseLoop:
|
||||
if i >= len(st.sigmas) - 1:
|
||||
return Done()
|
||||
sigma_t, sigma_next = st.sigmas[i], st.sigmas[i + 1]
|
||||
# The model timestep here is the PLAIN sigma (st.timesteps[i] == st.sigmas[i]); the adapter turns
|
||||
# it into the per-frame [B, T] timestep + masks + fps internally.
|
||||
# Model timestep is the plain sigma (st.timesteps[i] == st.sigmas[i]); the adapter turns it into
|
||||
# the per-frame [B, T] timestep + masks + fps internally.
|
||||
expert_id = self.expert.expert_for(StepContext(i, st.timesteps[i], sigma_t))
|
||||
sctx = StepContext(step_idx=i, timestep=st.timesteps[i], sigma=sigma_t, active_expert_id=expert_id)
|
||||
cfg_state = st.plugin_state["cfg"]
|
||||
@@ -138,16 +124,16 @@ class Cosmos25DenoiseLoop:
|
||||
cfg, precision = self.cfg, self.precision
|
||||
|
||||
def _velocity(model: Any, x_: Any, sigma_t_: float, pe_: Any, ne_: Any, scale_: float) -> np.ndarray:
|
||||
# The conditioned forward + ClassicCFG combine. The dit-call is toy-compatible:
|
||||
# Conditioned forward + ClassicCFG combine. The dit-call is toy-compatible:
|
||||
# ``dit(latent, text_embed, sigma)`` — the Cosmos2.5-specific timestep/masks/fps are built
|
||||
# INSIDE the Cosmos25DiT adapter (so the CPU ToyDiT, which ignores them, also runs).
|
||||
# inside the Cosmos25DiT adapter (so the CPU ToyDiT, which ignores them, also runs).
|
||||
dit = model.component(expert_id)
|
||||
preds = {b: dit(x_, pe_ if b == "cond" else ne_, sigma_t_) for b in branches}
|
||||
return precision.cast(cfg.combine(preds, scale_, sctx, cfg_state))
|
||||
|
||||
def run(model, override=None):
|
||||
# The EAGER thunk: handles the override (cached-prediction) path. The deterministic Euler
|
||||
# solver dispatches through the platform kernel table (numpy on CPU, the device kernel on GPU).
|
||||
# Eager thunk: handles the override (cached-prediction) path. The Euler solver dispatches
|
||||
# through the platform kernel table (numpy on CPU, the device kernel on GPU).
|
||||
kernels = model.platform.kernels
|
||||
if override is not None and "noise_pred" in override:
|
||||
velocity = precision.cast(np.asarray(override["noise_pred"], dtype="float32"))
|
||||
@@ -160,7 +146,7 @@ class Cosmos25DenoiseLoop:
|
||||
})
|
||||
|
||||
def graph_fn(model, ws):
|
||||
# The CAPTURABLE op-structure: reads EVERY per-step input from the static workspace and writes
|
||||
# Capturable op-structure: reads every per-step input from the static workspace and writes
|
||||
# into the static output buffer, so the captured graph replays correctly with rebound buffers.
|
||||
st_t, st_n = float(ws["sigma_t"]), float(ws["sigma_next"])
|
||||
velocity = _velocity(model, ws["x"], st_t, ws["pe"], ws["ne"], float(ws["scale"]))
|
||||
@@ -190,8 +176,8 @@ class Cosmos25DenoiseLoop:
|
||||
},
|
||||
run=run,
|
||||
label=f"cosmos25.denoise.{i}",
|
||||
# The flow-match Euler step is a clean capturable op (no host RNG / data-dependent branch),
|
||||
# like Wan t2v: the branch-set + expert + scheduler-precision discriminate the captured graph.
|
||||
# Flow-match Euler step is a clean capturable op (no host RNG / data-dependent branch), like
|
||||
# Wan t2v: branch-set + expert + scheduler-precision discriminate the captured graph.
|
||||
capturable=True,
|
||||
graph_key=(tuple(sorted(branches)), expert_id, precision.scheduler_step_in_fp32),
|
||||
graph_fn=graph_fn,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Cosmos-Predict2.5 t2v program: text_encode -> diffusion_denoise (flow-match) -> vae_decode.
|
||||
|
||||
Same inline shape as the Wan / Cosmos2 t2v programs — the Cosmos2.5 specifics (per-frame plain-sigma
|
||||
timestep, the mandatory masks + fps, the Reason1 100352-dim embedding) live entirely in
|
||||
``Cosmos25DenoiseLoop`` and the ``torch_cosmos25`` adapters, so the node graph is unchanged. fps is read
|
||||
by the loop from the request (``req.diffusion.fps``, default 24) into scratch and threaded to the DiT
|
||||
adapter. (video2world/image2world conditioning would add a VAE-encode node writing
|
||||
``conditioning_latents``/``cond_indicator`` into slots; the loop already reads them — BRINGUP.)
|
||||
timestep, the mandatory masks + fps, the Reason1 100352-dim embedding) live in ``Cosmos25DenoiseLoop``
|
||||
and the ``torch_cosmos25`` adapters, so the node graph is unchanged. The loop reads fps from the request
|
||||
(``req.diffusion.fps``, default 24). (video2world/image2world conditioning would add a VAE-encode node
|
||||
writing ``conditioning_latents``/``cond_indicator`` into slots; the loop already reads them — BRINGUP.)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
+60
-38
@@ -1,12 +1,11 @@
|
||||
"""Cosmos3 omni ModelCard (design_v3 §4.1, §15b) — the forcing function for omni-native serving.
|
||||
"""Cosmos3 omni ModelCard — one resident MoT transformer serving both und and generation.
|
||||
|
||||
ONE resident MoT ``transformer`` is bound to BOTH the ``ar_decode`` loop (the reasoner, und pathway)
|
||||
and the ``diffusion_denoise`` loop (the joint generation pathway) — ``shared_weight_components`` on
|
||||
both. A request runs the reasoner (prompt upsampling), packs its tokens into conditioning, then the
|
||||
joint denoise — all on the same resident weights, with every AR token and denoise step a
|
||||
runtime-visible WorkUnit (the differentiation vs vllm-omni's opaque DIFFUSION stage). The diffusion
|
||||
loop is literally ``WanDenoiseLoop`` — the same step body the engine serves for Wan — bound to the
|
||||
MoT module. ``sound_vae`` is declared ``optional_for`` non-t2vs tasks (the lazy-component fix, P8).
|
||||
ONE resident MoT ``transformer`` is bound to BOTH the ``ar_decode`` loop (the reasoner) and the
|
||||
``diffusion_denoise`` loop (joint generation) via ``shared_weight_components``. A request runs the
|
||||
reasoner (prompt upsampling), packs its tokens into conditioning, then the joint denoise — all on the
|
||||
same resident weights, with every AR token and denoise step a runtime-visible WorkUnit. The diffusion
|
||||
loop is ``WanDenoiseLoop`` (the same step body the engine serves for Wan) bound to the MoT module.
|
||||
``sound_vae`` is ``optional_for`` non-t2vs tasks so it loads lazily.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -41,49 +40,72 @@ def build_cosmos3_card(model_id: str = "cosmos3-vfm") -> ModelCard:
|
||||
return ARDecodeLoop(loop_id="ar_decode", transformer_id="transformer", cost=ar_cost, max_tokens=6)
|
||||
|
||||
def dn_factory():
|
||||
return WanDenoiseLoop(loop_id="diffusion_denoise", cfg=cfg, flow_shift=flow,
|
||||
precision=precision, expert=expert, cost=dn_cost)
|
||||
return WanDenoiseLoop(loop_id="diffusion_denoise",
|
||||
cfg=cfg,
|
||||
flow_shift=flow,
|
||||
precision=precision,
|
||||
expert=expert,
|
||||
cost=dn_cost)
|
||||
|
||||
components = {
|
||||
"tokenizer": ComponentSpec("tokenizer", kind="tokenizer",
|
||||
factory=lambda inst: ToyTokenizer(),
|
||||
required_for={"reason", "t2v", "t2vs"}),
|
||||
"transformer": ComponentSpec(
|
||||
"transformer", kind="dit",
|
||||
"tokenizer":
|
||||
ComponentSpec("tokenizer",
|
||||
kind="tokenizer",
|
||||
factory=lambda inst: ToyTokenizer(),
|
||||
required_for={"reason", "t2v", "t2vs"}),
|
||||
"transformer":
|
||||
ComponentSpec(
|
||||
"transformer",
|
||||
kind="dit",
|
||||
load_id="fastvideo.models.dits.cosmos3:Cosmos3VFMTransformer",
|
||||
factory=lambda inst: ToyMoTDiT(seed=seed),
|
||||
resident_for=["ar_decode", "diffusion_denoise"], # never unload mid-request
|
||||
resident_for=["ar_decode", "diffusion_denoise"], # never unload mid-request
|
||||
required_for={"reason", "t2v", "t2vs"}),
|
||||
"vae": ComponentSpec("vae", kind="vae", factory=lambda inst: ToyVAE(),
|
||||
required_for={"t2v", "t2vs"}),
|
||||
"sound_vae": ComponentSpec("sound_vae", kind="audio_vae",
|
||||
load_id="fastvideo.models.audio:Cosmos3SoundVAE",
|
||||
optional_for={"reason", "t2v"}, required_for={"t2vs"}),
|
||||
"vae":
|
||||
ComponentSpec("vae", kind="vae", factory=lambda inst: ToyVAE(), required_for={"t2v", "t2vs"}),
|
||||
"sound_vae":
|
||||
ComponentSpec("sound_vae",
|
||||
kind="audio_vae",
|
||||
load_id="fastvideo.models.audio:Cosmos3SoundVAE",
|
||||
optional_for={"reason", "t2v"},
|
||||
required_for={"t2vs"}),
|
||||
}
|
||||
loops = {
|
||||
"ar_decode": LoopSpec("ar_decode", kind=LoopKind.AR_DECODE, work_unit_kind=WorkUnitKind.AR_TOKEN,
|
||||
step_cost_model=ar_cost, shared_weight_components=["transformer"],
|
||||
cache_policy=["paged_kv"], loop_factory=ar_factory),
|
||||
"diffusion_denoise": LoopSpec("diffusion_denoise", kind=LoopKind.DIFFUSION_DENOISE,
|
||||
work_unit_kind=WorkUnitKind.DIFFUSION_STEP, step_cost_model=dn_cost,
|
||||
shared_weight_components=["transformer"], cache_policy=["feature"],
|
||||
loop_factory=dn_factory),
|
||||
"ar_decode":
|
||||
LoopSpec("ar_decode",
|
||||
kind=LoopKind.AR_DECODE,
|
||||
work_unit_kind=WorkUnitKind.AR_TOKEN,
|
||||
step_cost_model=ar_cost,
|
||||
shared_weight_components=["transformer"],
|
||||
cache_policy=["paged_kv"],
|
||||
loop_factory=ar_factory),
|
||||
"diffusion_denoise":
|
||||
LoopSpec("diffusion_denoise",
|
||||
kind=LoopKind.DIFFUSION_DENOISE,
|
||||
work_unit_kind=WorkUnitKind.DIFFUSION_STEP,
|
||||
step_cost_model=dn_cost,
|
||||
shared_weight_components=["transformer"],
|
||||
cache_policy=["feature"],
|
||||
loop_factory=dn_factory),
|
||||
}
|
||||
card = ModelCard(
|
||||
model_id=model_id, family="cosmos3", components=components, loops=loops,
|
||||
capabilities=CapabilityMatrix.of(
|
||||
Capability.TEXT_TO_VIDEO, Capability.REASONING_TEXT, Capability.TEXT_TO_VIDEO_SOUND,
|
||||
Capability.VAE_DECODE, Capability.POLICY_ROLLOUT),
|
||||
recipe=RecipeSpec(method="base", assumes_loop="diffusion_denoise",
|
||||
assumes_precision="float32", consistency_required=ConsistencyLevel.C1),
|
||||
model_id=model_id,
|
||||
family="cosmos3",
|
||||
components=components,
|
||||
loops=loops,
|
||||
capabilities=CapabilityMatrix.of(Capability.TEXT_TO_VIDEO, Capability.REASONING_TEXT,
|
||||
Capability.TEXT_TO_VIDEO_SOUND, Capability.VAE_DECODE,
|
||||
Capability.POLICY_ROLLOUT),
|
||||
recipe=RecipeSpec(method="base",
|
||||
assumes_loop="diffusion_denoise",
|
||||
assumes_precision="float32",
|
||||
consistency_required=ConsistencyLevel.C1),
|
||||
parity=ParitySpec(consistency_levels=[ConsistencyLevel.C1], interleave_required=True),
|
||||
caches={
|
||||
"feature": CacheContract("feature", max_bytes=1 << 24, reuse_across_requests=True),
|
||||
"paged_kv": CacheContract("paged_kv", max_bytes=1 << 24, block_bytes=1 << 12,
|
||||
reuse_across_requests=False),
|
||||
"paged_kv": CacheContract("paged_kv", max_bytes=1 << 24, block_bytes=1 << 12, reuse_across_requests=False),
|
||||
},
|
||||
precision=PrecisionContract(default_dtype="float32", training_precision="float32"),
|
||||
parallelism=ParallelismContract(valid_plans=[ParallelPlan.single()],
|
||||
default_plan=ParallelPlan.single()),
|
||||
parallelism=ParallelismContract(valid_plans=[ParallelPlan.single()], default_plan=ParallelPlan.single()),
|
||||
)
|
||||
return card.validate()
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""Cosmos3 omni program (design_v3 §15b):
|
||||
"""Cosmos3 omni program:
|
||||
|
||||
tokenize → reason (ar_decode) → pack(tokens→cond) → diffusion_denoise → vae_decode
|
||||
|
||||
The reasoner and the denoiser bind the SAME resident ``transformer``; the scheduler is the mode
|
||||
multiplexer. This is the workload no DAG-of-engines can express (splitting them doubles the weights
|
||||
and severs the shared state).
|
||||
The reasoner and the denoiser bind the SAME resident ``transformer``; the scheduler multiplexes the
|
||||
mode. Splitting them into separate engines would double the weights and sever the shared state.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -14,19 +13,36 @@ from v2.recipes.omni import emit_text_node, pack_cond_from_tokens, tokenize_node
|
||||
|
||||
def build_cosmos3_program() -> Program:
|
||||
return Program(
|
||||
program_id="cosmos3.omni", kind=ProgramKind.INLINE,
|
||||
program_id="cosmos3.omni",
|
||||
kind=ProgramKind.INLINE,
|
||||
nodes=[
|
||||
ComponentNode("tokenize", fn=tokenize_node, writes=("prompt_tokens",)),
|
||||
ModelLoopNode("reason", loop_id="ar_decode", output_slot="reason_out",
|
||||
reads=("prompt_tokens",), writes=("reason_out",)),
|
||||
ComponentNode("emit_text", fn=emit_text_node("reason_out", "reason_text"),
|
||||
reads=("reason_out",), writes=("reason_text",)),
|
||||
ComponentNode("pack", fn=pack_cond_from_tokens("reason_out"),
|
||||
reads=("reason_out",), writes=("text_embeds", "neg_text_embeds")),
|
||||
ModelLoopNode("denoise", loop_id="diffusion_denoise", output_slot="denoise_out",
|
||||
reads=("text_embeds",), writes=("denoise_out",)),
|
||||
ComponentNode("vae_decode", fn=vae_decode_node("denoise_out", "video"),
|
||||
reads=("denoise_out",), writes=("video",)),
|
||||
ComponentNode("tokenize", fn=tokenize_node, writes=("prompt_tokens", )),
|
||||
ModelLoopNode("reason",
|
||||
loop_id="ar_decode",
|
||||
output_slot="reason_out",
|
||||
reads=("prompt_tokens", ),
|
||||
writes=("reason_out", )),
|
||||
ComponentNode("emit_text",
|
||||
fn=emit_text_node("reason_out", "reason_text"),
|
||||
reads=("reason_out", ),
|
||||
writes=("reason_text", )),
|
||||
ComponentNode("pack",
|
||||
fn=pack_cond_from_tokens("reason_out"),
|
||||
reads=("reason_out", ),
|
||||
writes=("text_embeds", "neg_text_embeds")),
|
||||
ModelLoopNode("denoise",
|
||||
loop_id="diffusion_denoise",
|
||||
output_slot="denoise_out",
|
||||
reads=("text_embeds", ),
|
||||
writes=("denoise_out", )),
|
||||
ComponentNode("vae_decode",
|
||||
fn=vae_decode_node("denoise_out", "video"),
|
||||
reads=("denoise_out", ),
|
||||
writes=("video", )),
|
||||
],
|
||||
output_artifacts={"video": "video", "text": "reason_text", "latents": "denoise_out"},
|
||||
output_artifacts={
|
||||
"video": "video",
|
||||
"text": "reason_text",
|
||||
"latents": "denoise_out"
|
||||
},
|
||||
).validate()
|
||||
|
||||
+27
-28
@@ -1,26 +1,25 @@
|
||||
"""FastWan (DMD few-step Wan) ModelCard — self-contained recipe package (bucket-C pattern).
|
||||
"""FastWan (DMD few-step Wan) ModelCard — self-contained recipe package.
|
||||
|
||||
FastWan = Distribution-Matching-Distillation few-step Wan. The architecture is the same
|
||||
``WanTransformer3DModel`` / ``AutoencoderKLWan`` / UMT5 as base Wan — so we REUSE the Wan torch adapters
|
||||
(no ``adapter=`` needed; the torch backend's ``_make_dit`` already builds ``WanTransformer3DModel`` from
|
||||
``load_id``) and the Wan VAE / T5 adapters. The ONLY new piece is the DMD few-step schedule, captured by
|
||||
the in-package ``FastWanDMDLoop`` (predict-x0-then-renoise over a fixed ``dmd_denoising_steps`` list).
|
||||
FastWan = Distribution-Matching-Distillation few-step Wan. Same architecture as base Wan
|
||||
(``WanTransformer3DModel`` / ``AutoencoderKLWan`` / UMT5), so the Wan torch adapters are reused via
|
||||
``load_id`` (no ``adapter=`` needed; the torch backend's ``_make_dit`` already builds the transformer)
|
||||
along with the Wan VAE / T5 adapters. The only new piece is the DMD few-step schedule in
|
||||
``FastWanDMDLoop`` (predict-x0-then-renoise over a fixed ``dmd_denoising_steps`` list).
|
||||
|
||||
Loadable target (this card's primary, GPU-verifiable id):
|
||||
* ``FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers`` — FULL ATTENTION (no VSA kernel needed) +
|
||||
Loadable target (this card's primary id):
|
||||
* ``FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers`` — full attention (no VSA kernel needed),
|
||||
TI2V-5B geometry (z_dim=48 VAE, 16x spatial / 4x temporal), DMD steps [1000, 757, 522] (3-step),
|
||||
flow_shift 5.0 / DMD-scheduler shift 8.0. From ``FastWan2_2_TI2V_5B_Config`` +
|
||||
``FAST_WAN_2_2_TI2V_5B`` preset (height 704, width 1280, 121 frames, fps 24, guidance 5.0).
|
||||
flow_shift 5.0 / DMD-scheduler shift 8.0. From ``FAST_WAN_2_2_TI2V_5B`` preset (704x1280, 121
|
||||
frames, fps 24, guidance 5.0).
|
||||
|
||||
BRINGUP variants (registered to a card here, but NOT loadable in this environment — clearly flagged):
|
||||
* ``FastVideo/FastWan2.2-TI2V-5B-Diffusers`` (the non-FullAttn TI2V-5B) — trained for the **VSA**
|
||||
(Video Sparse Attention) kernel, which is NOT built here (no nvcc). Same card/geometry; load is
|
||||
BRINGUP until the VSA kernel is built + selected.
|
||||
BRINGUP variants (registered here but NOT loadable in this environment):
|
||||
* ``FastVideo/FastWan2.2-TI2V-5B-Diffusers`` (non-FullAttn TI2V-5B) — trained for the VSA (Video
|
||||
Sparse Attention) kernel, not built here (no nvcc). Same card/geometry; load is BRINGUP until VSA
|
||||
is built + selected.
|
||||
* ``FastVideo/FastWan2.1-T2V-1.3B-Diffusers`` and ``FastVideo/FastWan2.1-T2V-14B-480P-Diffusers``
|
||||
(``FastWan2_1_T2V_480P_Config``: Wan2.1 16/8/4 geometry, DMD steps [1000, 757, 522], flow_shift 8.0,
|
||||
448x832x61, fps 16, guidance 3.0) — also VSA-trained, AND the DMD checkpoints carry **gated-attention**
|
||||
params (``to_gate_compress``) the generic Wan loader cannot map -> they require a v2-side non-strict
|
||||
loader option. Both are BRINGUP (VSA kernel build + non-strict load).
|
||||
(Wan2.1 16/8/4 geometry, DMD steps [1000, 757, 522], flow_shift 8.0, 448x832x61, fps 16, guidance
|
||||
3.0) — also VSA-trained, AND the DMD checkpoints carry gated-attention params (``to_gate_compress``)
|
||||
the generic Wan loader cannot map, so they need a v2-side non-strict loader option. Both BRINGUP.
|
||||
|
||||
``stamp_wan21_checkpoints`` applies (diffusers transformer/vae/text_encoder subfolder layout).
|
||||
"""
|
||||
@@ -65,14 +64,14 @@ def _build_fastwan_card(model_id: str,
|
||||
temporal_ratio: int,
|
||||
sampling_defaults: SamplingDefaults,
|
||||
checkpoint_root: str | None = None) -> ModelCard:
|
||||
"""Shared FastWan card builder. The DMD few-step loop is the only delta vs base Wan; the
|
||||
WanTransformer3DModel / AutoencoderKLWan / UMT5 torch adapters are reused via ``load_id`` (no
|
||||
``adapter=``). ``EmbeddedGuidance`` (single-branch) matches DMD's distilled, no-CFG forward.
|
||||
"""Shared FastWan card builder. The DMD few-step loop is the only delta vs base Wan; the Wan torch
|
||||
adapters are reused via ``load_id`` (no ``adapter=``). ``EmbeddedGuidance`` (single-branch) matches
|
||||
DMD's distilled, no-CFG forward.
|
||||
|
||||
No ``FlowShiftPolicy`` is needed: DMD does NOT integrate a continuous flow-shift sigma schedule —
|
||||
the schedule is the fixed ``dmd_denoising_steps`` list, and the DMD re-noise scheduler's shift is
|
||||
fixed at 8.0 inside ``FastWanDMDLoop`` (the pipeline ``flow_shift`` 5.0/8.0 is doc-level metadata
|
||||
on the per-variant builders, not a loop input). The negative prompt rides in ``sampling_defaults``."""
|
||||
No ``FlowShiftPolicy``: DMD does not integrate a continuous flow-shift sigma schedule — the schedule
|
||||
is the fixed ``dmd_denoising_steps`` list, and the re-noise scheduler's shift is fixed at 8.0 inside
|
||||
``FastWanDMDLoop`` (the pipeline ``flow_shift`` 5.0/8.0 is doc metadata on the per-variant builders,
|
||||
not a loop input). The negative prompt rides in ``sampling_defaults``."""
|
||||
seed = _seed_from(model_id)
|
||||
cost = CostModel(kind=WorkUnitKind.DIFFUSION_STEP, base_seconds=1e-4, per_unit_seconds=1e-7)
|
||||
cfg = EmbeddedGuidance() # DMD is distilled single-branch (guidance embedded, NOT CFG)
|
||||
@@ -149,15 +148,15 @@ def _build_fastwan_card(model_id: str,
|
||||
def build_fastwan_ti2v_5b_card(model_id: str = "fastwan2.2-ti2v-5b-fullattn",
|
||||
*,
|
||||
checkpoint_root: str | None = None) -> ModelCard:
|
||||
"""FastWan2.2-TI2V-5B-FullAttn (the loadable target). FullAttn = no VSA kernel needed.
|
||||
"""FastWan2.2-TI2V-5B-FullAttn (the loadable target; FullAttn = no VSA kernel needed).
|
||||
|
||||
Geometry: z_dim=48 VAE, 16x spatial / 4x temporal (Wan2.2-TI2V-5B). DMD steps [1000, 757, 522]
|
||||
(3-step), pipeline flow_shift 5.0 (the DMD re-noise scheduler internally uses shift 8.0). Defaults
|
||||
from ``FAST_WAN_2_2_TI2V_5B``: 704x1280, 121 frames, fps 24, guidance 5.0. ``num_steps=3`` is the
|
||||
DMD schedule length (the preset's misleading 50 is ignored by the DMD stage).
|
||||
DMD schedule length (the preset's 50 is ignored by the DMD stage).
|
||||
|
||||
Serves both ``FastWan2.2-TI2V-5B-FullAttn-Diffusers`` (loadable) and ``FastWan2.2-TI2V-5B-Diffusers``
|
||||
(the non-FullAttn variant -> VSA-trained -> load is BRINGUP; same card/geometry)."""
|
||||
(non-FullAttn variant, VSA-trained, so load is BRINGUP; same card/geometry)."""
|
||||
return _build_fastwan_card(model_id,
|
||||
latent_channels=FASTWAN_TI2V_LATENT_CHANNELS,
|
||||
spatial_ratio=FASTWAN_TI2V_SPATIAL_RATIO,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""FastWanDMDLoop — Distribution-Matching-Distillation few-step denoise for FastWan (design_v3 §5).
|
||||
"""FastWanDMDLoop — Distribution-Matching-Distillation few-step denoise for FastWan.
|
||||
|
||||
FastWan is a DMD-distilled Wan: instead of N flow-match Euler steps it runs a handful (3) of
|
||||
*predict-x0-then-renoise* steps over a fixed list of discrete model timesteps. This is NOT a plain
|
||||
flow-match Euler integrator (``WanDenoiseLoop``), so it ships its own ``next``/``advance`` and uses the
|
||||
shared policies as a library (the design_v3 §5.1 escape hatch for braided math).
|
||||
shared policies as a library.
|
||||
|
||||
Faithful port of ``fastvideo/pipelines/stages/denoising.py:DmdDenoisingStage`` (+ ``pred_noise_to_pred_video``
|
||||
in ``fastvideo/models/utils.py``). The exact math, per discrete model timestep ``t_i`` (e.g. [1000, 757, 522]):
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"""FastWan (DMD) t2v program: text_encode -> diffusion_denoise (DMD few-step) -> vae_decode.
|
||||
|
||||
Same inline node graph as the Wan t2v program — the DMD specifics live entirely in
|
||||
``FastWanDMDLoop`` (the in-package loop), so the program is unchanged. (i2v conditioning for the TI2V
|
||||
variant would add image-encode / first-frame-VAE nodes writing ``i2v_img_embeds`` / ``i2v_cond`` into
|
||||
slots, exactly like ``v2/recipes/wan21/i2v.py``; the loop already reads them. The registered preset is
|
||||
t2v, so the base program covers it.)
|
||||
Same inline node graph as the Wan t2v program — the DMD specifics live entirely in ``FastWanDMDLoop``.
|
||||
The TI2V variant would add image-encode / first-frame-VAE nodes writing ``i2v_img_embeds`` / ``i2v_cond``
|
||||
(see ``v2/recipes/wan21/i2v.py``; the loop already reads them), but the registered preset is t2v.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""FLUX.2 flow-match sigma schedule — the BFL empirical-mu grid (recipe-local; NOT in v2/loop/sampler.py).
|
||||
|
||||
FLUX.2 does NOT use ``FlowShiftPolicy.build_schedule`` (the Wan/LTX resolution-bucket shift). It builds a
|
||||
resolution-dependent ``mu`` from the *packed* image token count and feeds it, together with a custom
|
||||
resolution-dependent ``mu`` from the *packed* image token count and feeds it, with a custom
|
||||
``sigmas = linspace(1, 1/N, N)`` grid, to diffusers' ``FlowMatchEulerDiscreteScheduler.set_timesteps``
|
||||
under ``use_dynamic_shifting=True``. This module reproduces that scheduler's numpy result exactly so the
|
||||
CPU-toy loop is bit-faithful to the GPU pipeline.
|
||||
under ``use_dynamic_shifting=True``. This module reproduces that scheduler's numpy result exactly.
|
||||
|
||||
Faithful to:
|
||||
* ``fastvideo/pipelines/basic/flux_2/flux_2_timestep_preparation.py:compute_empirical_mu`` (the BFL
|
||||
@@ -49,7 +48,7 @@ def flux2_sigmas(num_steps: int, image_seq_len: int) -> np.ndarray:
|
||||
1. base grid ``sigmas = linspace(1.0, 1/N, N)`` (the custom grid the pipeline passes);
|
||||
2. resolution-dependent ``mu`` shift ``time_shift(mu, 1.0, sigmas)`` (use_dynamic_shifting=True);
|
||||
3. append the terminal ``0.0`` (FlowMatchEuler's ``invert_sigmas=False`` tail).
|
||||
The loop integrates these pairwise via the deterministic flow-match Euler step (FLOW_MATCH_STEP).
|
||||
The loop integrates these pairwise via the flow-match Euler step (FLOW_MATCH_STEP).
|
||||
"""
|
||||
n = max(1, int(num_steps))
|
||||
base = np.linspace(1.0, 1.0 / n, n, dtype=np.float64)
|
||||
|
||||
@@ -28,12 +28,12 @@ step math (see ``build_edm_euler_sigmas`` in ``sampler.py``):
|
||||
|
||||
Camera / MoGe-depth / 3D-cache conditioning fills ``condition_video_pose`` / ``conditioning_latents`` /
|
||||
``cond_indicator`` from program slots; ALL are ``None`` for the registered degenerate **t2v** path → the
|
||||
frame-replace branches are inert and the DiT's internal pose concat is zeroed, exactly as the fastvideo
|
||||
frame-replace branches are inert and the DiT's internal pose concat is zeroed, matching how the fastvideo
|
||||
pipeline degrades when no image/camera is given. Faithful port of ``Gen3CDenoisingStage.forward`` +
|
||||
``Gen3CLatentPreparationStage`` (fastvideo/pipelines/stages/gen3c_stages.py).
|
||||
|
||||
The DiT call is ``dit(model_input, text_embed, sigma, cond=bundle)`` — compatible with ``ToyDiT`` (which
|
||||
accepts and ignores ``cond``), so the loop CPU-verifies on the toy backend.
|
||||
The DiT call is ``dit(model_input, text_embed, sigma, cond=bundle)`` — ``ToyDiT`` accepts and ignores
|
||||
``cond``, so the loop CPU-verifies on the toy backend.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"""HunyuanVideo — rectified flow-match video DiT ported into the v2 substrate.
|
||||
|
||||
Self-contained recipe package (the bucket-C pattern): the card declares its torch adapters via
|
||||
``ComponentSpec.adapter`` (``HunyuanVideoDiT`` / ``HunyuanVideoVAE`` / ``HunyuanVideoLlamaEncoder`` /
|
||||
``HunyuanVideoCLIPEncoder`` in ``v2/platform/backends/torch_hunyuan_video.py``) and reuses the canonical
|
||||
``WanDenoiseLoop`` flow-match math via a thin ``HunyuanDenoiseLoop`` (which only threads the CLIP-pooled
|
||||
vector through Wan's ``context=`` channel). The new work vs Wan is the dual text-encoder marshalling
|
||||
(LLaMA per-token sequence + CLIP pooled global vector packed into the DiT's 2-element
|
||||
``encoder_hidden_states``) and the Hunyuan VAE scalar-``scaling_factor`` normalization. Reuses
|
||||
``stamp_wan21_checkpoints``. Registered in ``v2/registry.py`` by the orchestrator.
|
||||
Self-contained recipe package: the card declares its torch adapters via ``ComponentSpec.adapter``
|
||||
(``HunyuanVideoDiT`` / ``HunyuanVideoVAE`` / ``HunyuanVideoLlamaEncoder`` / ``HunyuanVideoCLIPEncoder`` in
|
||||
``v2/platform/backends/torch_hunyuan_video.py``) and reuses the canonical ``WanDenoiseLoop`` flow-match math
|
||||
via a thin ``HunyuanDenoiseLoop`` (which only threads the CLIP-pooled vector through Wan's ``context=``
|
||||
channel). The new work vs Wan: dual text-encoder marshalling (LLaMA per-token sequence + CLIP pooled global
|
||||
vector packed into the DiT's 2-element ``encoder_hidden_states``) and the Hunyuan VAE scalar-``scaling_factor``
|
||||
normalization. Reuses ``stamp_wan21_checkpoints``. Registered in ``v2/registry.py``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ Architecture deltas vs Wan (all declared on the card so the recipe is self-conta
|
||||
|
||||
``stamp_wan21_checkpoints`` applies (diffusers transformer/vae/text_encoder subfolder layout); HunyuanVideo
|
||||
additionally ships ``text_encoder_2/`` (CLIP) + ``tokenizer_2/`` — the stamp helper points each declared
|
||||
component (incl. ``text_encoder_2``) at its subfolder (see the BRINGUP note in ``card.py`` and the CLIP
|
||||
adapter for the ``tokenizer_2`` wiring caveat).
|
||||
component (incl. ``text_encoder_2``) at its subfolder (see the BRINGUP note below and the CLIP adapter for
|
||||
the ``tokenizer_2`` wiring caveat).
|
||||
|
||||
Defaults (fastvideo HUNYUAN_T2V preset): 50 steps, guidance_scale 1.0 (CFG effectively off),
|
||||
720x1280, 125 frames @ 24fps, flow_shift 7.0. A FastHunyuan variant ships shift 17, 6 steps.
|
||||
Defaults (fastvideo HUNYUAN_T2V preset): 50 steps, guidance_scale 1.0 (CFG effectively off), 720x1280,
|
||||
125 frames @ 24fps, flow_shift 7.0. A FastHunyuan variant ships shift 17, 6 steps.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
"""HunyuanDenoiseLoop — a thin subclass of the canonical ``WanDenoiseLoop`` (flow-match Euler).
|
||||
"""HunyuanDenoiseLoop — a thin subclass of ``WanDenoiseLoop`` (flow-match Euler).
|
||||
|
||||
HunyuanVideo is a *rectified flow-match* model: the DiT predicts a velocity in unpatchified
|
||||
HunyuanVideo is a rectified flow-match model: the DiT predicts a velocity in unpatchified
|
||||
``[B, 16, T, H, W]`` latent space, the model timestep is the scalar ``sigma * 1000`` (a per-batch
|
||||
``LongTensor`` consumed by the internal ``time_in`` ``TimestepEmbedder``), and the deterministic Euler
|
||||
update is ``x_next = x + (sigma_next - sigma)*v`` — exactly the math ``WanDenoiseLoop`` already runs. The
|
||||
latent geometry (16 channels, 8x spatial, 4x temporal) is the Wan default too, so we reuse the loop
|
||||
verbatim and only carry a Hunyuan-specific docstring + the dual-text-encoder marshalling delta below.
|
||||
``LongTensor``), and the Euler update is ``x_next = x + (sigma_next - sigma)*v`` — exactly the math
|
||||
``WanDenoiseLoop`` already runs. The latent geometry (16 channels, 8x spatial, 4x temporal) is the Wan
|
||||
default too, so we reuse the loop verbatim and only add the dual-text-encoder marshalling delta below.
|
||||
|
||||
The ONE delta vs Wan is conditioning shape: the Hunyuan DiT consumes TWO text signals —
|
||||
The one delta vs Wan is conditioning shape: the Hunyuan DiT consumes TWO text signals —
|
||||
``encoder_hidden_states = [llama_hidden[B, L, 4096], clip_pooled[B, 768]]`` (a per-token LLaMA sequence
|
||||
plus a single CLIP-pooled global vector). To keep the loop's ``dit(latent, text_embed, sigma)`` call
|
||||
identical on the CPU toy and the GPU backend (the cosmos2 discipline: the adapter assembles
|
||||
arch-specifics INTERNALLY), the program writes the LLaMA sequence into the ``text_embeds`` slot (the
|
||||
primary ``text_embed`` the loop already threads) and the CLIP-pooled vector into ``text_pooled``. This
|
||||
loop reuses Wan's existing ``context=`` channel (Wan uses it for the i2v CLIP-vision embeds) to carry
|
||||
the CLIP-pooled vector to the DiT adapter — so ``WanDiT``-style ``dit(x, text_embed, sigma, context=...)``
|
||||
is unchanged, ``ToyDiT`` (which means over both ``text_embed`` and ``context``) runs on CPU, and the real
|
||||
identical on the CPU toy and GPU backends (the adapter assembles arch-specifics internally), the program
|
||||
writes the LLaMA sequence into the ``text_embeds`` slot and the CLIP-pooled vector into ``text_pooled``.
|
||||
This loop reuses Wan's existing ``context=`` channel (Wan uses it for i2v CLIP-vision embeds) to carry the
|
||||
pooled vector to the adapter — so the ``dit(x, text_embed, sigma, context=...)`` call is unchanged,
|
||||
``ToyDiT`` (which means over both ``text_embed`` and ``context``) runs on CPU, and the real
|
||||
``HunyuanVideoDiT`` adapter packs ``[text_embed, context]`` into the 2-element ``encoder_hidden_states``.
|
||||
|
||||
guidance_scale defaults to 1.0 for base HunyuanVideo → ``ClassicCFG.combine`` collapses to the single
|
||||
``cond`` branch (``uncond + 1*(cond-uncond) == cond``), i.e. CFG is effectively off, matching the
|
||||
fastvideo pipeline. (``embedded_cfg_scale``/``guidance`` is a no-op on the base model whose
|
||||
``guidance_in`` is ``None``; see ``torch_hunyuan_video.HunyuanVideoDiT`` for that BRINGUP note.)
|
||||
``cond`` branch (CFG effectively off), matching the fastvideo pipeline. (``embedded_cfg_scale``/``guidance``
|
||||
is a no-op on the base model whose ``guidance_in`` is ``None``; see the BRINGUP note in
|
||||
``torch_hunyuan_video.HunyuanVideoDiT``.)
|
||||
|
||||
Faithful to ``fastvideo/pipelines/basic/hunyuan/hunyuan_pipeline.py`` (flow-match DenoisingStage) and
|
||||
``fastvideo/models/dits/hunyuanvideo.py:HunyuanVideoTransformer3DModel.forward`` (the list-split of
|
||||
@@ -41,17 +39,16 @@ HUNYUAN_SPATIAL_RATIO = 8
|
||||
class HunyuanDenoiseLoop(WanDenoiseLoop):
|
||||
"""Wan flow-match loop with HunyuanVideo's dual-text-encoder marshalling.
|
||||
|
||||
Reuses ``WanDenoiseLoop.next``/``advance``/``finalize`` unchanged (flow-match Euler, velocity output,
|
||||
timestep ``sigma*1000``, scalar per-batch timestep). Overrides only ``init`` to thread the CLIP-pooled
|
||||
global vector (slot ``text_pooled``) through Wan's existing ``context=`` channel so the DiT adapter can
|
||||
reassemble the 2-element ``encoder_hidden_states`` the Hunyuan forward expects.
|
||||
Reuses ``WanDenoiseLoop.next``/``advance``/``finalize`` unchanged. Overrides only ``init`` to thread the
|
||||
CLIP-pooled global vector (slot ``text_pooled``) through Wan's existing ``context=`` channel so the DiT
|
||||
adapter can reassemble the 2-element ``encoder_hidden_states`` the Hunyuan forward expects.
|
||||
"""
|
||||
|
||||
def init(self, req, model, ctx) -> LoopState:
|
||||
st = super().init(req, model, ctx)
|
||||
# Wan's ``context`` channel (``i2v_img_embeds`` -> dit ``context=``) carries the CLIP-pooled vector
|
||||
# for Hunyuan. None for any caller that did not run the dual-encode node -> the DiT adapter falls
|
||||
# back to a single-tensor encoder_hidden_states (still valid: the Hunyuan forward also accepts a
|
||||
# bare tensor whose token 0 is the pooled vector — but the dual-slot path is the faithful one).
|
||||
# for Hunyuan. None when the dual-encode node did not run -> the DiT adapter falls back to a
|
||||
# single-tensor encoder_hidden_states (still valid: the Hunyuan forward also accepts a bare tensor
|
||||
# whose token 0 is the pooled vector — but the dual-slot path is the faithful one).
|
||||
st.scratch["i2v_img_embeds"] = ctx.slots.get("text_pooled")
|
||||
return st
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"""HunyuanVideo t2v program: hunyuan_text_encode -> diffusion_denoise (flow-match) -> vae_decode.
|
||||
|
||||
Same inline shape as the Wan/cosmos2 t2v programs, with ONE delta: HunyuanVideo has TWO text encoders
|
||||
(a LLaMA per-token sequence + a CLIP-pooled global vector), so the text-encode node writes BOTH a
|
||||
Same inline shape as the Wan/cosmos2 t2v programs, with one delta: HunyuanVideo has TWO text encoders
|
||||
(a LLaMA per-token sequence + a CLIP-pooled global vector), so the text-encode node writes both a
|
||||
``text_embeds`` slot (the LLaMA sequence, the loop's primary ``text_embed``) and a ``text_pooled`` slot
|
||||
(the CLIP vector, threaded to the DiT via the loop's ``context=`` channel). The dual-encode node uses the
|
||||
content-hash feature cache (like ``recipes.common.text_encode_node_fn``) so a repeated prompt encodes once.
|
||||
content-hash feature cache so a repeated prompt encodes once.
|
||||
|
||||
On the CPU toy backend both components are ``ToyTextEncoder`` (single arrays), so ``ToyDiT`` — which means
|
||||
over both ``text_embed`` and ``context`` — runs end-to-end; on the GPU backend the real LLaMA/CLIP adapters
|
||||
produce the sequence + pooled pair the ``HunyuanVideoDiT`` adapter reassembles into the 2-element
|
||||
``encoder_hidden_states`` the Hunyuan forward expects.
|
||||
``encoder_hidden_states``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -23,9 +23,9 @@ def _ensure_text_encoder_2_stamped(instance: Any) -> None:
|
||||
"""The shared ``stamp_wan21_checkpoints`` (called by the entrypoint after ``build_card``) only knows
|
||||
Wan's subfolder superset, which does NOT include ``text_encoder_2`` (HunyuanVideo's CLIP secondary
|
||||
encoder). Stamp it here — right before the component is materialized — from the already-stamped
|
||||
``transformer`` checkpoint's parent (the model root). Idempotent / no-op once set; this keeps the fix
|
||||
inside the recipe (no edit to the shared stamp). Build-time stamping via ``build_*_card(checkpoint_root)``
|
||||
already covers the local-path path; this covers the registry+entrypoint path."""
|
||||
``transformer`` checkpoint's parent (the model root). Idempotent / no-op once set; keeps the fix in the
|
||||
recipe. Build-time stamping via ``build_*_card(checkpoint_root)`` covers the local-path case; this covers
|
||||
the registry+entrypoint case."""
|
||||
import os
|
||||
te2 = instance.card.components.get("text_encoder_2")
|
||||
if te2 is None or te2.checkpoint:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""HunyuanVideo 1.5 (T2V) — rectified-flow video DiT ported into the v2 substrate.
|
||||
|
||||
Self-contained recipe package (the bucket-C pattern): the card declares its torch adapters via
|
||||
``ComponentSpec.adapter`` (``HunyuanVideo15DiT``/``HunyuanVideo15VAE``/``HunyuanVideo15QwenEncoder``/
|
||||
``HunyuanVideo15ByT5Encoder`` in ``v2/platform/backends/torch_hunyuan_video15.py``) and a
|
||||
``HunyuanVideo15DenoiseLoop`` (a thin ``WanDenoiseLoop`` subclass with z=32/16×/4× geometry). The
|
||||
flow-match loop math is unchanged from Wan; the arch deltas (two text embeds, an image-embed list, the
|
||||
33-channel i2v cond concat, the scalar-scaling VAE) live in the adapters. Registered in ``v2/registry.py``
|
||||
(by the orchestrator) for ``hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v``.
|
||||
Self-contained recipe package: the card declares its torch adapters via ``ComponentSpec.adapter``
|
||||
(``HunyuanVideo15DiT``/``HunyuanVideo15VAE``/``HunyuanVideo15QwenEncoder``/``HunyuanVideo15ByT5Encoder`` in
|
||||
``v2/platform/backends/torch_hunyuan_video15.py``) plus a ``HunyuanVideo15DenoiseLoop`` (a thin
|
||||
``WanDenoiseLoop`` subclass with z=32/16×/4× geometry). The flow-match loop math is unchanged from Wan; the
|
||||
arch deltas (two text embeds, an image-embed list, the 33-channel i2v cond concat, the scalar-scaling VAE)
|
||||
live in the adapters. Registered in ``v2/registry.py`` (by the orchestrator) for
|
||||
``hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""HunyuanVideo 1.5 ModelCard — text→video (the registered 480p t2v preset; i2v + SR are later
|
||||
capabilities the loop/adapter thread but the t2v program does not yet exercise on CPU).
|
||||
"""HunyuanVideo 1.5 ModelCard — text→video (the registered 480p t2v preset). i2v + SR are later
|
||||
capabilities the loop/adapter thread but the t2v program does not yet exercise on CPU.
|
||||
|
||||
Architecture deltas vs Wan (all declared on the card so the recipe is self-contained):
|
||||
* DiT ``fastvideo.models.dits.hunyuanvideo15:HunyuanVideo15Transformer3DModel`` — a rectified-flow
|
||||
velocity network (54 dual-stream MMDoubleStream blocks, hidden 2048, in/out 65/32). Its forward takes
|
||||
a LIST of two text embeds (Qwen2.5-VL + ByT5) and a LIST image-embed; the new ``HunyuanVideo15DiT``
|
||||
adapter marshals those, so ``HunyuanVideo15DenoiseLoop`` (a thin ``WanDenoiseLoop`` subclass with
|
||||
z=32/16×/4× geometry) drives it with the unchanged flow-match math.
|
||||
* VAE ``fastvideo.models.vaes.hunyuan15vae`` — causal-3D, z=32, 16× spatial / 4× temporal, normalized
|
||||
by a SCALAR ``scaling_factor=1.03682`` -> the ``HunyuanVideo15VAE`` adapter (NOT Wan's mean/std).
|
||||
* DiT ``HunyuanVideo15Transformer3DModel`` — a rectified-flow velocity network (54 dual-stream
|
||||
MMDoubleStream blocks, hidden 2048, in/out 65/32). Its forward takes a list of two text embeds
|
||||
(Qwen2.5-VL + ByT5) and a list image-embed; the ``HunyuanVideo15DiT`` adapter marshals those, so
|
||||
``HunyuanVideo15DenoiseLoop`` (a thin ``WanDenoiseLoop`` subclass with z=32/16×/4× geometry) drives it
|
||||
with the unchanged flow-match math.
|
||||
* VAE ``hunyuan15vae`` — causal-3D, z=32, 16× spatial / 4× temporal, normalized by a SCALAR
|
||||
``scaling_factor=1.03682`` via the ``HunyuanVideo15VAE`` adapter (not Wan's mean/std).
|
||||
* Text encoders: ``text_encoder`` = Qwen2.5-VL (3584-d, ``hidden_states[-3]`` cropped of 108 template
|
||||
tokens) via ``HunyuanVideo15QwenEncoder``; ``text_encoder_2`` = ByT5/Glyph (1472-d, quoted-glyph text,
|
||||
may be empty) via ``HunyuanVideo15ByT5Encoder``.
|
||||
@@ -60,7 +60,7 @@ _DEFAULT_STEPS = 50
|
||||
|
||||
def _linspace_sigmas(num_steps: int) -> tuple[float, ...]:
|
||||
"""Faithful to ``presets.py:_sigmas`` — ``np.linspace(1,0,n+1)[:-1]`` (drops the terminal 0). The
|
||||
loop's ``build_schedule`` consumes this verbatim when present (FlowShiftPolicy passes it through)."""
|
||||
loop's ``build_schedule`` consumes it verbatim when present (FlowShiftPolicy passes it through)."""
|
||||
return tuple((1.0 - i / num_steps) for i in range(num_steps))
|
||||
|
||||
|
||||
@@ -166,10 +166,10 @@ def build_hunyuan_video15_card(model_id: str = "hunyuan-video-1.5-t2v-480p",
|
||||
card.validate()
|
||||
if checkpoint_root:
|
||||
stamp_wan21_checkpoints(card, checkpoint_root)
|
||||
# stamp_wan21_checkpoints only knows the Wan superset of subfolders; HunyuanVideo 1.5 adds a
|
||||
# second text encoder (ByT5 in ``text_encoder_2/``) that the shared stamp does not cover. Stamp it
|
||||
# here so the recipe stays self-contained (no edit to the shared stamp). The model root is the
|
||||
# parent of any already-stamped subfolder (stamp resolves HF ids -> a local snapshot).
|
||||
# stamp_wan21_checkpoints does not cover HunyuanVideo 1.5's second text encoder (ByT5 in
|
||||
# ``text_encoder_2/``), so stamp it here to keep the recipe self-contained (no edit to the shared
|
||||
# stamp). The model root is the parent of any already-stamped subfolder (stamp resolves HF ids -> a
|
||||
# local snapshot).
|
||||
import os
|
||||
if "text_encoder_2" in card.components and not card.components["text_encoder_2"].checkpoint:
|
||||
model_root = os.path.dirname(os.path.normpath(card.components["transformer"].checkpoint))
|
||||
|
||||
@@ -1,39 +1,36 @@
|
||||
"""HunyuanVideo15DenoiseLoop — the canonical flow-match denoise loop, parameterized for HunyuanVideo 1.5.
|
||||
"""HunyuanVideo15DenoiseLoop — the flow-match denoise loop parameterized for HunyuanVideo 1.5.
|
||||
|
||||
The loop *math* is identical to Wan's (``WanDenoiseLoop``): N deterministic flow-match Euler steps over
|
||||
the full latent, ``ClassicCFG`` combine, ``timestep = sigma*1000``, ``x_next = x + (σ_next-σ)·v``. This is
|
||||
faithful to ``fastvideo/models/schedulers/scheduling_flow_match_euler_discrete.py`` — ``scale_model_input``
|
||||
is the identity for flow-match, ``step`` is ``prev_sample = sample + (sigma_next - sigma)*model_output``,
|
||||
and HunyuanVideo 1.5's DiT predicts velocity directly (no x0 reconstruction). The latent is initialized at
|
||||
``randn·σ[0]`` with ``σ[0]=1.0`` (the preset's ``linspace(1,0,n+1)[:-1]`` schedule).
|
||||
The loop math is identical to ``WanDenoiseLoop``: N deterministic flow-match Euler steps over the full
|
||||
latent, ``ClassicCFG`` combine, ``timestep = sigma*1000``, ``x_next = x + (σ_next-σ)·v``. HunyuanVideo 1.5's
|
||||
DiT predicts velocity directly (no x0 reconstruction); the latent starts at ``randn·σ[0]`` with ``σ[0]=1.0``.
|
||||
|
||||
The ONLY architectural delta lives in the ``HunyuanVideo15DiT`` torch adapter (two text embeds + an
|
||||
image-embed list + the 33-channel i2v cond concat). Because ``WanDenoiseLoop`` already threads
|
||||
``context=`` (image embeds) and ``cond=`` (the conditioning latent) into the adapter, the cleanest port
|
||||
is to subclass it: change only the latent geometry (z=32, 16× spatial, 4× temporal) and the slot names
|
||||
the loop reads for the i2v signal. For t2v those slots are absent (``None``) → the dit-call degenerates
|
||||
to ``dit(latent, text_embed, sigma)`` exactly like Wan t2v, and the toy backend's ``ToyDiT`` runs it.
|
||||
The only architectural delta lives in the ``HunyuanVideo15DiT`` torch adapter (two text embeds + an
|
||||
image-embed list + the 33-channel i2v cond concat). Because ``WanDenoiseLoop`` already threads ``context=``
|
||||
(image embeds) and ``cond=`` (the conditioning latent) into the adapter, the port subclasses it: change only
|
||||
the latent geometry (z=32, 16× spatial, 4× temporal) and the slot names the loop reads for the i2v signal.
|
||||
For t2v those slots are ``None`` → the dit-call degenerates to ``dit(latent, text_embed, sigma)`` like Wan
|
||||
t2v, and the toy backend's ``ToyDiT`` runs it.
|
||||
|
||||
BRINGUP: i2v conditioning (``context``=zero image embeds, ``cond``=33ch first-frame latent) is threaded
|
||||
but the program currently writes neither slot for the registered t2v preset, so the loop is pure t2v on
|
||||
CPU; the i2v VAE-encode node + image-latent slot are documented hooks (see ``program.py``).
|
||||
BRINGUP: i2v conditioning (``context``=zero image embeds, ``cond``=33ch first-frame latent) is threaded but
|
||||
the program writes neither slot for the registered t2v preset, so the loop is pure t2v on CPU; the i2v
|
||||
VAE-encode node + image-latent slot are documented hooks (see ``program.py``).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2.recipes.wan21.loop import WanDenoiseLoop
|
||||
|
||||
# HunyuanVideo 1.5 causal-3D VAE compression (configs/models/vaes/hunyuan15vae.py): z=32, 16× spatial,
|
||||
# 4× temporal. The DiT's noisy-latent in_channels is 32 (out_channels); i2v reaches 65 by concatenating
|
||||
# the 33ch first-frame cond latent inside the adapter, so the loop's latent stays 32ch.
|
||||
# 4× temporal. The DiT's noisy-latent in_channels is 32; i2v reaches 65 by concatenating the 33ch
|
||||
# first-frame cond latent inside the adapter, so the loop's latent stays 32ch.
|
||||
HUNYUAN15_LATENT_CHANNELS = 32
|
||||
HUNYUAN15_SPATIAL_RATIO = 16
|
||||
HUNYUAN15_TEMPORAL_RATIO = 4
|
||||
|
||||
|
||||
class HunyuanVideo15DenoiseLoop(WanDenoiseLoop):
|
||||
"""Wan flow-match loop with HunyuanVideo 1.5 latent geometry. The i2v conditioning the base loop reads
|
||||
from ``ctx.slots`` is keyed the same way (``i2v_cond``/``i2v_img_embeds``), so a future i2v program can
|
||||
write them with no loop change; the t2v program leaves them unset (pure t2v)."""
|
||||
"""Wan flow-match loop with HunyuanVideo 1.5 latent geometry. The i2v slots the base loop reads from
|
||||
``ctx.slots`` are keyed identically (``i2v_cond``/``i2v_img_embeds``), so a future i2v program can write
|
||||
them with no loop change; the t2v program leaves them unset (pure t2v)."""
|
||||
|
||||
def __init__(self,
|
||||
*,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""HunyuanVideo 1.5 t2v program: text_encode → diffusion_denoise → vae_decode.
|
||||
|
||||
Same inline shape as the Wan/Cosmos t2v programs — the arch specifics (two text embeds, the image-embed
|
||||
list, the 33ch i2v concat) live entirely in the ``HunyuanVideo15DiT`` adapter, so the node graph is
|
||||
unchanged except for the text-encode node, which must produce BOTH text embeddings.
|
||||
list, the 33ch i2v concat) live entirely in the ``HunyuanVideo15DiT`` adapter, so the node graph differs
|
||||
only in the text-encode node, which must produce BOTH text embeddings.
|
||||
|
||||
HunyuanVideo 1.5 conditions on two encoders (faithful to ``HunyuanVideo15Pipeline``'s
|
||||
``TextEncodingStage(text_encoders=[text_encoder, text_encoder_2])``):
|
||||
@@ -11,10 +11,10 @@ HunyuanVideo 1.5 conditions on two encoders (faithful to ``HunyuanVideo15Pipelin
|
||||
zero-length embedding when the prompt carries no quoted text.
|
||||
|
||||
The text-encode node packs them as a ``(qwen, byt5)`` tuple in the slot; ``HunyuanVideo15DiT`` unpacks
|
||||
that into the two-element ``encoder_hidden_states`` list. On the CPU **toy** backend there is a single
|
||||
that into the two-element ``encoder_hidden_states`` list. On the CPU toy backend there is a single
|
||||
``text_encoder`` component (``ToyTextEncoder``) and no ``text_encoder_2``: the node then writes the plain
|
||||
toy embedding (a single numpy array), which ``ToyDiT`` consumes directly — so the program CPU-verifies
|
||||
unchanged. The feature cache (design_v3 §7.2) still applies to the Qwen path via ``cached_text_encode``.
|
||||
unchanged. The feature cache still applies to the Qwen path via ``cached_text_encode``.
|
||||
|
||||
BRINGUP (i2v): the i2v path adds a VAE-encode node that writes the 33-channel first-frame conditioning
|
||||
latent into ``i2v_cond`` and zero image embeds into ``i2v_img_embeds`` (the slots the loop already reads);
|
||||
@@ -33,11 +33,10 @@ from v2.recipes.common import cached_text_encode
|
||||
def _ensure_byt5_checkpoint(instance: Any) -> None:
|
||||
"""Stamp ``text_encoder_2``'s weights subfolder if the shared stamp left it empty.
|
||||
|
||||
``VideoGenerator.from_pretrained`` stamps the card via ``stamp_wan21_checkpoints``, whose subfolder
|
||||
superset is Wan-centric and does NOT cover HunyuanVideo 1.5's second text encoder (ByT5 lives in
|
||||
``text_encoder_2/``). Rather than edit the shared stamp, the recipe stamps it here at first use,
|
||||
deriving the diffusers model root from the already-stamped primary ``text_encoder`` subfolder
|
||||
(sibling layout). Idempotent: a no-op once ``checkpoint`` is set (e.g. by the card's own stamp path)."""
|
||||
``stamp_wan21_checkpoints`` (run by ``VideoGenerator.from_pretrained``) is Wan-centric and does NOT cover
|
||||
HunyuanVideo 1.5's second text encoder (ByT5 lives in ``text_encoder_2/``). Rather than edit the shared
|
||||
stamp, derive the model root from the already-stamped primary ``text_encoder`` (sibling layout) and stamp
|
||||
here at first use. Idempotent: a no-op once ``checkpoint`` is set."""
|
||||
import os
|
||||
spec2 = instance.card.components.get("text_encoder_2")
|
||||
if spec2 is None or spec2.checkpoint:
|
||||
@@ -51,16 +50,14 @@ def _text_encode(instance: Any, slots: dict, request: Any, ctx: Any) -> None:
|
||||
"""Encode prompt + negative prompt. When a second text encoder (ByT5) exists and the prompt carries
|
||||
quoted glyph text, pack the two embeds; otherwise write the single Qwen embedding.
|
||||
|
||||
The shared ``WanDenoiseLoop`` does numpy bookkeeping on the conditioning slot (``np.asarray(e).nbytes``
|
||||
for the resident-bytes estimate, and ``np.array(e)`` to allocate the CUDA-graph static workspace). A
|
||||
plain Python tuple of the (qwen[seq,3584], byt5[bseq,1472]) embeds is INHOMOGENEOUS, so np.asarray /
|
||||
np.array raise. For the common t2v case the ByT5 stream is a zero-length ``[0,1472]`` embedding (no
|
||||
quoted text), which the DiT concatenates as 0 tokens — i.e. a no-op. So we pass ONLY the Qwen embedding
|
||||
then; the ``HunyuanVideo15DiT`` adapter rebuilds the empty ByT5 default internally (its ``byt5_e is
|
||||
None`` branch). When glyph text IS present we pack the pair as a numpy OBJECT array (length 2): an
|
||||
object array is a single ndarray, so ``np.asarray(...).nbytes`` and ``np.array(...)`` both succeed
|
||||
(no stacking), and the adapter unpacks it. This keeps the t2v path capturable (single ndarray) while
|
||||
still threading real ByT5 embeddings for glyph prompts (those steps eager-break on the object array)."""
|
||||
Why not a plain tuple: ``WanDenoiseLoop`` does numpy bookkeeping on the conditioning slot
|
||||
(``np.asarray(e).nbytes`` for resident-bytes, ``np.array(e)`` to allocate the CUDA-graph workspace), and
|
||||
a tuple of (qwen[seq,3584], byt5[bseq,1472]) is INHOMOGENEOUS so both raise. For the common t2v case the
|
||||
ByT5 stream is a zero-length ``[0,1472]`` embedding (no quoted text), a 0-token DiT no-op, so we pass ONLY
|
||||
the Qwen embedding and the adapter rebuilds the empty ByT5 default (its ``byt5_e is None`` branch). When
|
||||
glyph text IS present we pack the pair as a numpy OBJECT array (a single ndarray, so the np.asarray /
|
||||
np.array calls succeed without stacking) and the adapter unpacks it. This keeps the t2v path capturable
|
||||
(single ndarray) while still threading real ByT5 embeddings for glyph prompts (which eager-break)."""
|
||||
has_byt5 = "text_encoder_2" in instance.card.components
|
||||
# The dual-encoder packing is a GPU-path concern: the CPU toy backend's ToyDiT takes a single
|
||||
# [seq,dim] float embedding (and its ToyTextEncoder has no glyph/ByT5 stream), so on the toy we pass
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""HunyuanGameCraft (interactive camera/action-conditioned i2v) ported into the v2 substrate.
|
||||
|
||||
Self-contained recipe package (the bucket-C pattern): the card declares its torch adapters via
|
||||
``ComponentSpec.adapter`` (``GameCraftDiT``/``GameCraftVAE``/``GameCraftLlamaEncoder``/
|
||||
``GameCraftClipEncoder`` in ``v2/platform/backends/torch_hunyuangamecraft.py``) and a new
|
||||
``GameCraftDenoiseLoop`` (flow-match Euler + 33ch concat + per-step clean-ref injection). The registered
|
||||
path is the t2v/degenerate denoise; the camera/action (CameraNet Plücker) conditioning is BRINGUP — it
|
||||
needs a request-API camera-input channel v2 lacks today. Registered in ``v2/registry.py`` by the
|
||||
orchestrator from this package's returned metadata.
|
||||
Self-contained recipe package: the card declares its torch adapters via ``ComponentSpec.adapter``
|
||||
(``GameCraftDiT``/``GameCraftVAE``/``GameCraftLlamaEncoder``/``GameCraftClipEncoder`` in
|
||||
``v2/platform/backends/torch_hunyuangamecraft.py``) plus a ``GameCraftDenoiseLoop`` (flow-match Euler +
|
||||
33ch concat + per-step clean-ref injection). The registered path is the t2v/degenerate denoise; the
|
||||
camera/action (CameraNet Plücker) conditioning is BRINGUP — it needs a request-API camera-input channel v2
|
||||
lacks today. Registered in ``v2/registry.py`` by the orchestrator from this package's returned metadata.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
"""HunyuanGameCraft ModelCard — camera/action-conditioned interactive i2v (the registered preset is the
|
||||
t2v/degenerate path; the action/camera conditioning is BRINGUP — see the recipe ``__init__`` docstring).
|
||||
"""HunyuanGameCraft ModelCard — camera/action-conditioned interactive i2v. The registered preset is the
|
||||
t2v/degenerate path; the action/camera conditioning is BRINGUP (see the recipe ``__init__`` docstring).
|
||||
|
||||
Architecture deltas vs Wan (all declared on the card so the recipe is self-contained):
|
||||
* DiT ``fastvideo.models.dits.hunyuangamecraft:HunyuanGameCraftTransformer3DModel`` — a flow-match DiT
|
||||
whose forward takes a **33-channel** input ``cat([latent16|gt_latent16|mask1])`` (assembled by the
|
||||
loop/adapter, NOT the DiT), a **list** of text states ``[LLaMA(4096d), CLIP-pooled(768d)]``, an
|
||||
optional ``camera_states`` (CameraNet Plücker), and ``guidance=None`` (plain ClassicCFG, NO embedded
|
||||
guidance). Adapter -> ``GameCraftDiT``; the new ``GameCraftDenoiseLoop`` does the 33ch concat
|
||||
(via the adapter), per-step clean-ref injection, and flow-match Euler.
|
||||
* VAE ``fastvideo.models.vaes.gamecraftvae:GameCraftVAE`` — SCALAR ``scaling_factor=0.476986``
|
||||
normalization (NOT Wan per-channel stats). Adapter -> ``GameCraftVAE`` (cannot reuse ``WanVAE``).
|
||||
* Text ``fastvideo.models.encoders.llama:LlamaModel`` (LLaVA-LLaMA-3-8B, 4096d -> ``text_states``) and
|
||||
``fastvideo.models.encoders.clip:CLIPTextModel`` (CLIP ViT-L/14, 768d pooled -> ``text_states_2``).
|
||||
Adapters -> ``GameCraftLlamaEncoder`` / ``GameCraftClipEncoder``.
|
||||
The diffusers checkpoint layout has extra ``text_encoder_2/`` + ``tokenizer_2/`` subfolders the Wan stamp
|
||||
lacks, so this file extends ``stamp_wan21_checkpoints`` with a GameCraft-specific stamp.
|
||||
* DiT ``HunyuanGameCraftTransformer3DModel`` — a flow-match DiT whose forward takes a 33-channel input
|
||||
``cat([latent16|gt_latent16|mask1])`` (assembled by the adapter, not the DiT), a list of text states
|
||||
``[LLaMA(4096d), CLIP-pooled(768d)]``, an optional ``camera_states`` (CameraNet Plücker), and
|
||||
``guidance=None`` (plain ClassicCFG, no embedded guidance). Adapter ``GameCraftDiT``; the
|
||||
``GameCraftDenoiseLoop`` does the 33ch concat (via the adapter), per-step clean-ref injection, and
|
||||
flow-match Euler.
|
||||
* VAE ``GameCraftVAE`` — SCALAR ``scaling_factor=0.476986`` normalization (not Wan per-channel stats);
|
||||
cannot reuse ``WanVAE``.
|
||||
* Text ``LlamaModel`` (LLaVA-LLaMA-3-8B, 4096d -> ``text_states``) and ``CLIPTextModel`` (CLIP ViT-L/14,
|
||||
768d pooled -> ``text_states_2``). Adapters ``GameCraftLlamaEncoder`` / ``GameCraftClipEncoder``.
|
||||
The diffusers layout adds ``text_encoder_2/`` + ``tokenizer_2/`` subfolders the Wan stamp lacks, so this
|
||||
file extends ``stamp_wan21_checkpoints`` with a GameCraft-specific stamp.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -159,11 +158,11 @@ _GAMECRAFT_SUBFOLDERS = {
|
||||
|
||||
|
||||
def stamp_gamecraft_checkpoints(card: ModelCard, model_root: str) -> ModelCard:
|
||||
"""GPU deploy-time: point each component's ``ComponentSpec.checkpoint`` at its weights subfolder under
|
||||
``model_root`` (a local diffusers dir or an HF id resolved via ``snapshot_download``). Mirrors
|
||||
``stamp_wan21_checkpoints`` but covers GameCraft's ``text_encoder_2`` (the Wan stamp lacks it). The
|
||||
sibling ``tokenizer/`` (LLaMA) and ``tokenizer_2/`` (CLIP) + ``scheduler/`` subfolders are loaded by
|
||||
the torch adapters from ``dirname(checkpoint)``. Mutates and returns the card. BRINGUP risk A."""
|
||||
"""Point each component's ``ComponentSpec.checkpoint`` at its weights subfolder under ``model_root`` (a
|
||||
local diffusers dir or an HF id resolved via ``snapshot_download``). Like ``stamp_wan21_checkpoints``
|
||||
but also covers GameCraft's ``text_encoder_2`` (the Wan stamp lacks it). The torch adapters load the
|
||||
sibling ``tokenizer/`` (LLaMA), ``tokenizer_2/`` (CLIP), and ``scheduler/`` subfolders from
|
||||
``dirname(checkpoint)``. Mutates and returns the card. BRINGUP risk A."""
|
||||
import os
|
||||
if not os.path.isdir(model_root):
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
"""GameCraftDenoiseLoop — a flow-match Euler denoise loop with GameCraft i2v + camera conditioning.
|
||||
"""GameCraftDenoiseLoop — flow-match Euler denoise with GameCraft i2v + camera conditioning.
|
||||
|
||||
GameCraft (HunyuanGameCraft) is an *interactive* world model: a camera/action-conditioned i2v diffusion
|
||||
DiT. The sampler is plain flow-match (``FlowMatchEulerDiscreteScheduler``, ``flow_shift=5.0``,
|
||||
``timestep = σ·1000``) with standard ClassicCFG (``guidance_scale=6.0``, ``guidance=None`` to the DiT — NO
|
||||
embedded guidance). So the *integrator* is identical to ``WanDenoiseLoop`` (``x_next = x + (σ_next−σ)·v``
|
||||
via ``FLOW_MATCH_STEP``). The deltas this loop carries (faithful to
|
||||
GameCraft (HunyuanGameCraft) is an interactive world model: a camera/action-conditioned i2v diffusion DiT.
|
||||
The sampler is plain flow-match (``FlowMatchEulerDiscreteScheduler``, ``flow_shift=5.0``,
|
||||
``timestep = σ·1000``) with standard ClassicCFG (``guidance_scale=6.0``, ``guidance=None`` to the DiT, no
|
||||
embedded guidance), so the integrator is identical to ``WanDenoiseLoop`` (``x_next = x + (σ_next−σ)·v`` via
|
||||
``FLOW_MATCH_STEP``). The deltas this loop carries (faithful to
|
||||
``fastvideo/pipelines/stages/gamecraft_denoising.py``):
|
||||
|
||||
1. **33-channel input is assembled by the DiT adapter, not here.** The DiT forward wants
|
||||
``cat([latent16 | gt_latent16 | mask1], dim=1)``; the loop hands the bare 16ch latent + a ``cond``
|
||||
pack ``{"gt_latents","mask","camera_states"}`` to the ``GameCraftDiT`` adapter, which concats. The
|
||||
toy ``ToyDiT`` accepts (and ignores) the ``cond`` kwarg, so this loop CPU-verifies unchanged.
|
||||
pack ``{"gt_latents","mask","camera_states"}`` to the adapter, which concats. The toy ``ToyDiT``
|
||||
accepts and ignores the ``cond`` kwarg, so this loop CPU-verifies unchanged.
|
||||
|
||||
2. **Per-step clean-ref injection (frame_replace).** Official GameCraft overwrites the *conditioned*
|
||||
latent frames (``mask>0.5``) with the clean reference latent at EVERY step (not just step 0), so the
|
||||
conditioned frames stay noise-free throughout denoising. ``next()`` mutates the working latent before
|
||||
the forward when a reference latent is present.
|
||||
2. **Per-step clean-ref injection (frame_replace).** Official GameCraft overwrites the conditioned latent
|
||||
frames (``mask>0.5``) with the clean reference latent at every step (not just step 0) to keep them
|
||||
noise-free. ``next()`` mutates the working latent before the forward when a reference latent is present.
|
||||
|
||||
3. **Dual text encoders.** ``encoder_hidden_states = [LLaMA_states(4096d), CLIP_pooled(768d)]``. The
|
||||
loop threads the LLaMA states as the usual ``text_embed`` (cond vs uncond) and the CLIP pooled embed
|
||||
3. **Dual text encoders.** ``encoder_hidden_states = [LLaMA_states(4096d), CLIP_pooled(768d)]``. The loop
|
||||
threads the LLaMA states as the usual ``text_embed`` (cond vs uncond) and the CLIP pooled embed
|
||||
through the adapter's ``context=`` slot per branch, so ClassicCFG combines both correctly.
|
||||
|
||||
4. **BRINGUP — camera/action conditioning.** GameCraft is fundamentally CameraNet (Plücker)
|
||||
conditioned, with the forward special-casing the latent temporal length (18/9/10) for autoregressive
|
||||
multi-chunk generation. v2 has no camera/action request field or Plücker-conversion node yet, so the
|
||||
registered path passes ``camera_states=None`` -> the DiT's ``if camera_states is not None`` branch is
|
||||
skipped (the standard / degenerate denoise). For pure t2v (no reference image) gt_latents+mask are
|
||||
zero -> the 33ch concat reduces to ``[noise | 0 | 0]``, exactly the ``GameCraftDenoisingStage``
|
||||
fallback. Wiring real camera/action + the 18/9/10 length special-casing needs a request-API
|
||||
extension (see ``program.py`` blockers).
|
||||
4. **BRINGUP — camera/action conditioning.** GameCraft is CameraNet (Plücker) conditioned, with the
|
||||
forward special-casing the latent temporal length (18/9/10) for autoregressive multi-chunk generation.
|
||||
v2 has no camera/action request field or Plücker-conversion node yet, so the registered path passes
|
||||
``camera_states=None`` -> the DiT's ``if camera_states is not None`` branch is skipped (the degenerate
|
||||
denoise). For pure t2v (no reference image) gt_latents+mask are zero -> the 33ch concat reduces to
|
||||
``[noise | 0 | 0]``, exactly the ``GameCraftDenoisingStage`` fallback. Wiring real camera/action + the
|
||||
18/9/10 length special-casing needs a request-API extension (see ``program.py`` blockers).
|
||||
|
||||
All per-request state lives in ``LoopState`` (interleave-safe). ``next`` is kernel-free; ``advance`` folds
|
||||
the result + (under ROLLOUT) captures a trajectory slice — same shape as ``WanDenoiseLoop``.
|
||||
the result and, under ROLLOUT, captures a trajectory slice — same shape as ``WanDenoiseLoop``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -115,7 +113,7 @@ class GameCraftDenoiseLoop:
|
||||
st.scratch["guidance_scale"] = float(req.diffusion.guidance_scale)
|
||||
st.scratch["stream_video"] = bool(req.outputs.stream.get("video"))
|
||||
# i2v conditioning slots written by the image-cond program node. All None for pure t2v -> the 33ch
|
||||
# concat degenerates to [noise | 0 | 0] and the ref-injection branch is inert (the fastvideo
|
||||
# concat degenerates to [noise | 0 | 0] and the ref-injection branch is inert (the
|
||||
# GameCraftDenoisingStage fallback). camera_states is a BRINGUP slot (no request plumbing yet).
|
||||
st.scratch["gt_latents"] = ctx.slots.get("gt_latents")
|
||||
st.scratch["conditioning_mask"] = ctx.slots.get("conditioning_mask")
|
||||
@@ -128,9 +126,9 @@ class GameCraftDenoiseLoop:
|
||||
"""Per-step frame_replace: overwrite conditioned frames (mask>0.5) with the clean reference latent.
|
||||
|
||||
Faithful to ``GameCraftDenoisingStage`` (``latents[:,:,t_idx] = ref[:,:,0]`` for each conditioned
|
||||
``t``). Loop latents here are unbatched ``[C,T,h,w]`` (the toy/CPU geometry), so the mask's temporal
|
||||
axis is read at ``mask[0,0,:,0,0]`` when 5D (matching the fastvideo ``[B,1,T,H,W]`` mask), or as a
|
||||
1D ``[T]`` vector otherwise. ``ref`` is the clean ref latent ``[C,1,h,w]`` (or ``[C,T,h,w]``)."""
|
||||
``t``). Loop latents here are unbatched ``[C,T,h,w]`` (toy/CPU geometry), so the mask's temporal axis
|
||||
is read at ``mask[0,0,:,0,0]`` when 5D (the fastvideo ``[B,1,T,H,W]`` mask) or as a 1D ``[T]`` vector
|
||||
otherwise. ``ref`` is the clean ref latent ``[C,1,h,w]`` (or ``[C,T,h,w]``)."""
|
||||
m = np.asarray(mask)
|
||||
if m.ndim == 5: # fastvideo-shaped [B,1,T,H,W]: read the temporal slice
|
||||
frame_mask = m[0, 0, :, 0, 0]
|
||||
@@ -143,10 +141,9 @@ class GameCraftDenoiseLoop:
|
||||
r = np.asarray(ref, dtype="float32")
|
||||
ref_frame = r[:, 0] if r.ndim >= 4 else r # [C,h,w]
|
||||
out = np.array(x, dtype="float32", copy=True)
|
||||
# The conditioned-frame replacement requires the ref frame to match the working latent's per-frame
|
||||
# geometry (it always does on the real VAE geometry). If a backend's encode produced a mismatched
|
||||
# shape (e.g. the crude CPU toy VAE that does not spatially downsample), skip injection rather than
|
||||
# crash — the degenerate denoise still proceeds.
|
||||
# Frame replacement needs the ref frame to match the working latent's per-frame geometry (always
|
||||
# true on the real VAE). If a backend's encode produced a mismatched shape (e.g. the toy VAE that
|
||||
# does not spatially downsample), skip injection rather than crash — the degenerate denoise proceeds.
|
||||
if ref_frame.shape != out[:, 0].shape:
|
||||
return out
|
||||
n_t = min(out.shape[1], int(frame_mask.shape[0]))
|
||||
@@ -180,14 +177,14 @@ class GameCraftDenoiseLoop:
|
||||
scale = st.scratch["guidance_scale"]
|
||||
cfg, precision = self.cfg, self.precision
|
||||
# One ``cond`` pack reused across CFG branches (gt/mask/camera are branch-independent); the adapter
|
||||
# assembles the 33ch concat from it. None entries -> zero gt/mask + no camera inside the adapter.
|
||||
# assembles the 33ch concat from it. None entries -> zero gt/mask and no camera inside the adapter.
|
||||
cond_pack = {"gt_latents": gt, "mask": mask, "camera_states": camera}
|
||||
|
||||
def _velocity(model: Any, x_: np.ndarray, sigma_t_: float, pe_: Any, clip_: Any, ne_: Any, clip_n_: Any,
|
||||
scale_: float) -> np.ndarray:
|
||||
# Conditioned forward + ClassicCFG combine. The CLIP pooled embed rides ``context=`` per branch
|
||||
# so text_states_2 differs cond vs uncond; ``cond`` carries the i2v/camera pack. The toy DiT
|
||||
# accepts+ignores both kwargs, so this is CPU-toy compatible.
|
||||
# accepts and ignores both kwargs, so this is CPU-toy compatible.
|
||||
dit = model.component(expert_id)
|
||||
preds = {
|
||||
b:
|
||||
@@ -230,9 +227,9 @@ class GameCraftDenoiseLoop:
|
||||
},
|
||||
run=run,
|
||||
label=f"gamecraft.denoise.{i}",
|
||||
# i2v ref-injection mutates host state + threads non-workspace conditioning each step -> the
|
||||
# static-buffer capture form would miss it, so this step takes the eager path (like cosmos2's
|
||||
# gated frame-injection). Pure-t2v could capture, but keeping one path is simpler + correct.
|
||||
# i2v ref-injection mutates host state and threads non-workspace conditioning each step, which
|
||||
# static-buffer capture would miss, so this step takes the eager path (like cosmos2's gated
|
||||
# frame-injection). Pure-t2v could capture, but keeping one path is simpler and correct.
|
||||
capturable=False)
|
||||
|
||||
def advance(self, st: LoopState, result: StepResult) -> LoopState:
|
||||
|
||||
@@ -3,26 +3,26 @@
|
||||
|
||||
Same backbone as the Wan/Cosmos t2v programs, with two GameCraft-specific deltas:
|
||||
|
||||
* **Dual text encode.** ``_text_encode_dual`` runs BOTH encoders: the LLaMA encoder (``text_encoder``)
|
||||
writes the DiT's ``text_states`` into ``text_embeds``/``neg_text_embeds`` (the usual slots the loop
|
||||
reads), and the CLIP encoder (``text_encoder_2``) writes the pooled ``text_states_2`` into
|
||||
* **Dual text encode.** ``_text_encode_dual`` runs both encoders: LLaMA (``text_encoder``) writes the
|
||||
DiT's ``text_states`` into ``text_embeds``/``neg_text_embeds`` (the usual slots the loop reads), and
|
||||
CLIP (``text_encoder_2``) writes the pooled ``text_states_2`` into
|
||||
``clip_text_embeds``/``clip_neg_text_embeds`` (threaded to the adapter's ``context=`` per CFG branch).
|
||||
On the CPU toy both are ``ToyTextEncoder`` (cached via the content-hash feature cache).
|
||||
|
||||
* **Image conditioning node (i2v).** ``_image_cond_encode`` mirrors ``GameCraftImageVAEEncodingStage``:
|
||||
VAE-encode the reference image -> scaled ``ref_latent`` (the ``GameCraftVAE`` adapter applies the
|
||||
scalar ``scaling_factor`` already), repeat to ``T_lat`` frames -> ``gt_latents``, build the binary
|
||||
VAE-encode the reference image -> scaled ``ref_latent`` (the ``GameCraftVAE`` adapter already applies
|
||||
the scalar ``scaling_factor``), repeat to ``T_lat`` frames -> ``gt_latents``, build the binary
|
||||
``conditioning_mask`` (latent_frames<=10: only frame 0; else first half), zero ``gt_latents`` on
|
||||
non-conditioned frames, and expose ``ref_latent_for_injection = gt_latents[:, 0:1]`` for the loop's
|
||||
per-step frame_replace. No request image -> a no-op (slots stay unset) -> the loop runs pure t2v
|
||||
(zero gt/mask), exactly the fastvideo fallback.
|
||||
per-step frame_replace. No request image -> no-op (slots stay unset) -> the loop runs pure t2v (zero
|
||||
gt/mask), exactly the fastvideo fallback.
|
||||
|
||||
BRINGUP — the camera/action conditioning node is intentionally omitted: GameCraft's CameraNet needs a
|
||||
Plücker-coordinate ``camera_states`` tensor derived from an action/camera sequence the v2 request API does
|
||||
not carry yet, and the DiT's autoregressive latent-length special-casing (18/9/10) for multi-chunk
|
||||
generation has no request surface. Adding a ``camera_encode`` node that writes the ``camera_states`` slot
|
||||
(which the loop already reads) is the follow-up once the request API is extended. T_lat helper below
|
||||
matches the GameCraft geometry (4x temporal compression).
|
||||
not carry yet, and the DiT's autoregressive latent-length special-casing (18/9/10) has no request surface.
|
||||
Adding a ``camera_encode`` node that writes the ``camera_states`` slot (which the loop already reads) is the
|
||||
follow-up once the request API is extended. The T_lat helper below matches GameCraft's 4x temporal
|
||||
compression.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -38,12 +38,12 @@ GAMECRAFT_SPATIAL_RATIO = 8
|
||||
|
||||
|
||||
def _stamp_text_encoder_2(instance: Any) -> None:
|
||||
"""The shared ``from_config`` stamps the card via ``stamp_wan21_checkpoints``, whose subfolder map
|
||||
knows only ``transformer/vae/text_encoder`` — NOT GameCraft's second text encoder. So ``text_encoder_2``
|
||||
arrives at GPU build time with an empty ``checkpoint`` and ``_require_checkpoint`` fails. Derive the
|
||||
model root from the already-stamped ``text_encoder`` (LLaMA) checkpoint and point ``text_encoder_2`` at
|
||||
the sibling ``text_encoder_2/`` subfolder, here in the recipe's own node (no edit to the shared stamp).
|
||||
No-op once stamped or on the CPU toy (empty LLaMA checkpoint -> the toy factory needs no weights)."""
|
||||
"""``stamp_wan21_checkpoints`` knows only ``transformer/vae/text_encoder``, not GameCraft's second text
|
||||
encoder, so ``text_encoder_2`` reaches GPU build time with an empty ``checkpoint`` and
|
||||
``_require_checkpoint`` fails. Derive the model root from the already-stamped ``text_encoder`` (LLaMA)
|
||||
checkpoint and point ``text_encoder_2`` at the sibling ``text_encoder_2/`` subfolder, here in the
|
||||
recipe's own node (no edit to the shared stamp). No-op once stamped or on the CPU toy (empty LLaMA
|
||||
checkpoint -> the toy factory needs no weights)."""
|
||||
import os
|
||||
comps = instance.card.components
|
||||
spec2 = comps.get("text_encoder_2")
|
||||
@@ -84,10 +84,10 @@ def _image_cond_encode(instance: Any, slots: dict, request: Any, ctx: Any) -> No
|
||||
return # T2V mode: leave the i2v slots unset (loop falls back to zero gt/mask)
|
||||
|
||||
d = request.diffusion
|
||||
# The GameCraftVAE adapter applies the scalar scaling_factor in ``encode`` (matches the fastvideo
|
||||
# stage's ``ref_latents.mul_(scaling_factor)``), so we use the returned latent directly. The real
|
||||
# adapter also runs the resize/center-crop/normalize preprocessing on the PIL image (BRINGUP); the
|
||||
# CPU toy VAE consumes the pixel array directly.
|
||||
# The GameCraftVAE adapter applies the scalar scaling_factor in ``encode`` (matching the fastvideo
|
||||
# stage's ``ref_latents.mul_(scaling_factor)``), so use the returned latent directly. The real adapter
|
||||
# also runs resize/center-crop/normalize on the PIL image (BRINGUP); the toy VAE consumes the pixel
|
||||
# array directly.
|
||||
ref_latent = np.asarray(instance.component("vae").encode(pixels), dtype="float32") # [C,1,h,w] (or [C,h,w])
|
||||
if ref_latent.ndim == 3: # [C, h, w] -> add the singleton temporal axis
|
||||
ref_latent = ref_latent[:, None]
|
||||
|
||||
+15
-15
@@ -1,28 +1,28 @@
|
||||
"""HY-WorldPlay-Bidirectional ModelCard — interactive world model ported into the v2 substrate.
|
||||
"""HY-WorldPlay-Bidirectional ModelCard: interactive world model on the v2 substrate.
|
||||
|
||||
HY-WorldPlay is a chunk-rollout *world model*: it generates video in temporal chunks where each
|
||||
chunk after the first retrieves camera-aligned history "memory" frames as frozen context, and it
|
||||
conditions on THREE text/image streams (Qwen2.5-VL mllm + ByT5 glyph + SigLIP image) plus per-frame
|
||||
camera (viewmats/Ks, injected via ProPE attention) and per-frame action ids. The registered preset
|
||||
here is the **bidirectional t2v / degenerate (no-action, single straight-trajectory pose) path** that
|
||||
CPU-verifies; the full action/camera/memory conditioning is BRINGUP (needs a request-API extension to
|
||||
carry the pose string -> viewmats/Ks/action expansion + the point-cloud memory retrieval). See
|
||||
``loop.py`` and ``torch_hyworld.py`` for the BRINGUP markers.
|
||||
HY-WorldPlay is a chunk-rollout world model: it generates video in temporal chunks where each chunk
|
||||
after the first retrieves camera-aligned history "memory" frames as frozen context, and conditions on
|
||||
three text/image streams (Qwen2.5-VL mllm + ByT5 glyph + SigLIP image) plus per-frame camera
|
||||
(viewmats/Ks, injected via ProPE attention) and per-frame action ids. The registered preset is the
|
||||
bidirectional t2v / degenerate (no-action, single straight-trajectory pose) path that CPU-verifies;
|
||||
the full action/camera/memory conditioning is BRINGUP (needs a request-API extension to carry the pose
|
||||
string -> viewmats/Ks/action expansion + point-cloud memory retrieval). See ``loop.py`` and
|
||||
``torch_hyworld.py`` for the BRINGUP markers.
|
||||
|
||||
Architecture deltas vs Wan/Cosmos (all declared on the card so the recipe is self-contained):
|
||||
* DiT ``fastvideo.models.dits.hyworld.hyworld:HYWorldTransformer3DModel`` (subclasses
|
||||
HunyuanVideo15Transformer3DModel) — a **rectified-flow velocity** predictor whose forward takes a
|
||||
65ch pre-concatenated latent ``[noise(32) | cond(32) + mask(1)]``, a LIST of text embeds
|
||||
``[qwen, byt5]``, a LIST image embed ``[siglip]``, a PER-LATENT-FRAME LongTensor ``timestep`` (+ a
|
||||
HunyuanVideo15Transformer3DModel) — a rectified-flow velocity predictor whose forward takes a
|
||||
65ch pre-concatenated latent ``[noise(32) | cond(32) + mask(1)]``, a list of text embeds
|
||||
``[qwen, byt5]``, a list image embed ``[siglip]``, a per-latent-frame LongTensor ``timestep`` (+ a
|
||||
scalar ``timestep_txt``), and ``viewmats/Ks/action`` per-frame camera/action conditioning. The
|
||||
adapter (``HYWorldDiT``) marshals all of this INTERNALLY so the loop only hands it
|
||||
adapter (``HYWorldDiT``) marshals all of this internally so the loop only hands it
|
||||
``dit(latent, text_embed, sigma)`` (the cosmos2/ToyDiT-compatible signature).
|
||||
* VAE ``fastvideo.models.vaes.hyworldvae:AutoencoderKLHYWorld`` (z=32, 16x spatial, 4x temporal,
|
||||
scaling_factor=1.03682) — encode -> ``.mode() * scaling_factor`` (no shift/mean), decode inverts.
|
||||
* Encoders: Qwen2.5-VL (mllm primary text) + ByT5/T5 (glyph) + SigLIP (image). For the t2v preset the
|
||||
glyph + image streams are zeroed (the DiT detects an all-zero image and masks it).
|
||||
* Sampler: flow-match (``FlowMatchEulerDiscreteScheduler``) but the preset OVERRIDES the schedule with
|
||||
explicit sigmas ``linspace(1.0, 0.0, 51)[:-1]`` (50 steps), flow_shift 5.0, guidance 6.0.
|
||||
* Sampler: flow-match (``FlowMatchEulerDiscreteScheduler``), but the preset overrides the schedule
|
||||
with explicit sigmas ``linspace(1.0, 0.0, 51)[:-1]`` (50 steps), flow_shift 5.0, guidance 6.0.
|
||||
``stamp_wan21_checkpoints`` applies (diffusers transformer/vae/text_encoder/text_encoder_2/image_encoder
|
||||
subfolder layout); the superset stamp helper already covers ``text_encoder_2`` + ``image_encoder``.
|
||||
"""
|
||||
|
||||
@@ -17,15 +17,15 @@ the 65ch latent concat, the 3-stream conditioning list, the per-frame heterogene
|
||||
camera/action tensors INTERNALLY, so this loop only calls the cosmos2/ToyDiT-compatible
|
||||
``dit(latent, text_embed, sigma)`` (with the per-chunk camera context threaded via ``context=``).
|
||||
|
||||
BRINGUP (documented honestly — needs a request-API extension to be real):
|
||||
* The default preset (num_frames=125 -> ~32 latent frames, chunk 16 -> 2 chunks) WILL execute chunk>0.
|
||||
BRINGUP (needs a request-API extension to be real):
|
||||
* The default preset (num_frames=125 -> ~32 latent frames, chunk 16 -> 2 chunks) will execute chunk>0.
|
||||
Camera-aligned memory retrieval, ProPE camera attention, and per-frame action conditioning all need
|
||||
``viewmats``/``Ks``/``action`` carried from a pose string (``pose="w-31"`` -> ``pose_to_input`` +
|
||||
``compute_latent_num``). The v2 request/program has no slot for the pose string yet, so on the CPU
|
||||
toy path (and absent a pose) this loop runs the **degenerate single-chunk t2v path**: it clamps
|
||||
``chunk_num`` to the number of whole chunks but the camera/memory selection is skipped (the adapter
|
||||
builds zero camera/action internally). On a GPU box with a pose-carrying request, the chunk>0 branch
|
||||
(memory retrieval + frozen context) activates — that path is written-not-run here.
|
||||
``compute_latent_num``). The v2 request/program has no slot for the pose string yet, so absent a
|
||||
pose (the CPU toy path) this loop runs the degenerate per-chunk t2v path: it clamps ``chunk_num`` to
|
||||
the number of whole chunks but skips camera/memory selection (the adapter builds zero camera/action
|
||||
internally). On a GPU box with a pose-carrying request the chunk>0 branch (memory retrieval + frozen
|
||||
context) activates; that path is written-not-run here.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Text-to-image → image-to-video: two distinct generation models chained by a cross-model
|
||||
``Workflow`` (design_v3 §13). The realistic FLUX→Wan pipeline — two cards, two weight sets — proving
|
||||
the design composes across model *instances*, not just loops within one (which LTX-2 already covers)."""
|
||||
"""Text-to-image -> image-to-video: two distinct generation models chained by a cross-model
|
||||
``Workflow``. The realistic FLUX->Wan pipeline (two cards, two weight sets) proving the design
|
||||
composes across model instances, not just loops within one (which LTX-2 already covers)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from v2.recipes.image_video.card import build_flux_t2i_card, build_wan_i2v_card
|
||||
@@ -11,5 +11,7 @@ from v2.recipes.image_video.program import (
|
||||
build_wan_i2v_program,
|
||||
)
|
||||
|
||||
__all__ = ["build_flux_t2i_card", "build_wan_i2v_card", "build_flux_t2i_program",
|
||||
"build_wan_i2v_program", "build_t2i_then_i2v_workflow", "build_t2i_i2v_extend_workflow"]
|
||||
__all__ = [
|
||||
"build_flux_t2i_card", "build_wan_i2v_card", "build_flux_t2i_program", "build_wan_i2v_program",
|
||||
"build_t2i_then_i2v_workflow", "build_t2i_i2v_extend_workflow"
|
||||
]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Two single-purpose generation cards for the T2I→I2V workflow (design_v3 §4, §13).
|
||||
"""Two single-purpose generation cards for the T2I->I2V workflow.
|
||||
|
||||
These are deliberately *separate models* — distinct weights, distinct cards — because the realistic
|
||||
"text-to-image followed by image-to-video" pipeline is FLUX→Wan, not one model's two stages (that
|
||||
same-card case is already covered by LTX-2's base→refine program). Chaining them is a ``Workflow``
|
||||
These are deliberately separate models (distinct weights, distinct cards) because the realistic
|
||||
"text-to-image followed by image-to-video" pipeline is FLUX->Wan, not one model's two stages (that
|
||||
same-card case is already covered by LTX-2's base->refine program). Chaining them is a ``Workflow``
|
||||
(``program/workflow.py``), not a single-instance ``Program``.
|
||||
|
||||
Both reuse the *unchanged* ``WanDenoiseLoop`` — the image-conditioning for I2V lives in the I2V
|
||||
*program* (it folds the conditioning image into ``text_embeds``), so no loop change is needed. That
|
||||
is the point: a new pipeline is cards + a program + a workflow, never a new loop or runtime.
|
||||
Both reuse the unchanged ``WanDenoiseLoop``: the I2V image-conditioning lives in the I2V program (it
|
||||
folds the conditioning image into ``text_embeds``), so no loop change is needed. A new pipeline is
|
||||
cards + a program + a workflow, never a new loop or runtime.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -37,45 +37,64 @@ def _diffusion_card(model_id, family, loop_id, caps, tasks, *, shift):
|
||||
precision, expert = PrecisionPolicy(), NoRouting("transformer")
|
||||
|
||||
def loop_factory():
|
||||
return WanDenoiseLoop(loop_id=loop_id, cfg=cfg, flow_shift=flow,
|
||||
precision=precision, expert=expert, cost=cost)
|
||||
return WanDenoiseLoop(loop_id=loop_id, cfg=cfg, flow_shift=flow, precision=precision, expert=expert, cost=cost)
|
||||
|
||||
components = {
|
||||
"text_encoder": ComponentSpec("text_encoder", kind="text_encoder",
|
||||
factory=lambda inst: ToyTextEncoder(), required_for=tasks),
|
||||
"transformer": ComponentSpec("transformer", kind="dit",
|
||||
factory=lambda inst: ToyDiT(seed=seed),
|
||||
resident_for=[loop_id], required_for=tasks),
|
||||
"vae": ComponentSpec("vae", kind="vae", factory=lambda inst: ToyVAE(), required_for=tasks),
|
||||
"text_encoder":
|
||||
ComponentSpec("text_encoder", kind="text_encoder", factory=lambda inst: ToyTextEncoder(), required_for=tasks),
|
||||
"transformer":
|
||||
ComponentSpec("transformer",
|
||||
kind="dit",
|
||||
factory=lambda inst: ToyDiT(seed=seed),
|
||||
resident_for=[loop_id],
|
||||
required_for=tasks),
|
||||
"vae":
|
||||
ComponentSpec("vae", kind="vae", factory=lambda inst: ToyVAE(), required_for=tasks),
|
||||
}
|
||||
loops = {
|
||||
loop_id: LoopSpec(loop_id, kind=LoopKind.DIFFUSION_DENOISE,
|
||||
work_unit_kind=WorkUnitKind.DIFFUSION_STEP, step_cost_model=cost,
|
||||
shared_weight_components=["transformer"], cache_policy=["feature"],
|
||||
loop_factory=loop_factory),
|
||||
loop_id:
|
||||
LoopSpec(loop_id,
|
||||
kind=LoopKind.DIFFUSION_DENOISE,
|
||||
work_unit_kind=WorkUnitKind.DIFFUSION_STEP,
|
||||
step_cost_model=cost,
|
||||
shared_weight_components=["transformer"],
|
||||
cache_policy=["feature"],
|
||||
loop_factory=loop_factory),
|
||||
}
|
||||
return ModelCard(
|
||||
model_id=model_id, family=family, components=components, loops=loops,
|
||||
model_id=model_id,
|
||||
family=family,
|
||||
components=components,
|
||||
loops=loops,
|
||||
capabilities=CapabilityMatrix.of(*caps),
|
||||
recipe=RecipeSpec(method="base", assumes_loop=loop_id,
|
||||
assumes_precision="float32", consistency_required=ConsistencyLevel.C1),
|
||||
recipe=RecipeSpec(method="base",
|
||||
assumes_loop=loop_id,
|
||||
assumes_precision="float32",
|
||||
consistency_required=ConsistencyLevel.C1),
|
||||
parity=ParitySpec(consistency_levels=[ConsistencyLevel.C1], interleave_required=True),
|
||||
caches={"feature": CacheContract("feature", max_bytes=1 << 24, reuse_across_requests=True)},
|
||||
caches={
|
||||
"feature": CacheContract("feature", max_bytes=1 << 24, reuse_across_requests=True)
|
||||
},
|
||||
precision=PrecisionContract(default_dtype="float32", training_precision="float32"),
|
||||
parallelism=ParallelismContract(valid_plans=[ParallelPlan.single()],
|
||||
default_plan=ParallelPlan.single()),
|
||||
parallelism=ParallelismContract(valid_plans=[ParallelPlan.single()], default_plan=ParallelPlan.single()),
|
||||
).validate()
|
||||
|
||||
|
||||
def build_flux_t2i_card(model_id: str = "flux-t2i") -> ModelCard:
|
||||
"""Stage 1 — text → image (a single-frame diffusion). Stand-in for FLUX/SD."""
|
||||
return _diffusion_card(model_id, "flux", "t2i_denoise",
|
||||
return _diffusion_card(model_id,
|
||||
"flux",
|
||||
"t2i_denoise",
|
||||
caps=(Capability.TEXT_TO_IMAGE, Capability.VAE_DECODE),
|
||||
tasks={"t2i"}, shift=3.0)
|
||||
tasks={"t2i"},
|
||||
shift=3.0)
|
||||
|
||||
|
||||
def build_wan_i2v_card(model_id: str = "wan-i2v") -> ModelCard:
|
||||
"""Stage 2 — text + conditioning image → video. Stand-in for Wan-I2V / SVD."""
|
||||
return _diffusion_card(model_id, "wan", "i2v_denoise",
|
||||
return _diffusion_card(model_id,
|
||||
"wan",
|
||||
"i2v_denoise",
|
||||
caps=(Capability.IMAGE_TO_VIDEO, Capability.VAE_DECODE),
|
||||
tasks={"i2v"}, shift=5.0)
|
||||
tasks={"i2v"},
|
||||
shift=5.0)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user