The first real-weights Cosmos3-Nano T2V run produced an all-black video.
Root cause: the checkpoint's diffusers-style scheduler_config.json sets
use_karras_sigmas=true, and FastVideo's vendored UniPC checks karras
*before* use_flow_sigmas, so it built diffusion (beta) sigmas instead of
flow-matching sigmas -> scheduler.step diverged to NaN latents -> black
frames. The DiT/CFG velocity itself was clean.
The framework samples with FlowUniPCMultistepScheduler (pure flow:
shift + num_train_timesteps only). FastVideo's vendored UniPC is the same
algorithm, so the fix is config, not a port:
- Coerce the loaded scheduler to the flow setup in
Cosmos3OmniDiffusersPipeline.initialize_pipeline (kill karras/exp/beta
sigmas, force use_flow_sigmas + flow_prediction + final_sigmas_type=zero)
and rebuild the runtime scheduler from it.
- Use FastVideo's native UniPC (not diffusers) in the pipeline and tests,
honoring the "no diffusers at runtime" constraint.
Parity:
- Add test_cosmos3_scheduler_parity.py: FastVideo vendored UniPC (flow
config) vs the framework FlowUniPCMultistepScheduler. timesteps bit-exact,
sigmas within fp32 epsilon (~1e-8), full multi-step trajectory < ~1e-6
across shift in {10 (video), 3 (t2i)} and steps in {4,10,35}.
- Fix test_cosmos3_denoise_cfg_parity to use the framework scheduler as the
oracle (it previously compared diffusers-vs-diffusers, so the scheduler
was never actually checked against the framework).
Integration fixes to run end-to-end on real weights:
- registry: map checkpoint DiT class Cosmos3OmniTransformer ->
Cosmos3VFMTransformer.
- component_loader: route the "text_tokenizer" module to TokenizerLoader;
filter scheduler config to the class's __init__ params (robust to
diffusers schema drift like shift_terminal/sigma_min/sigma_max).
- DiT: materialize_non_persistent_buffers (recompute rotary_emb.inv_freq
after meta-device FSDP load) + cast vision latents / timestep embeds to
the compute dtype (no-ops in the fp32 parity tests).
- sequence_packing: move packed tensors to the model device in to_dit_kwargs.
- config: drop the unused identity text-preprocess (Cosmos3 has no text
encoder; the DiT tokenizes in the denoise stage).
Adds examples/inference/basic/basic_cosmos3_new_api.py (new VideoGenerator
API). Verified: 1280x704, 29 frames, 35 steps on B200 -> coherent video
matching the prompt (no NaNs, real pixel variance). Full cosmos3 parity
suite green.
215 lines
7.8 KiB
Python
215 lines
7.8 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Shared fixtures for the Cosmos3 native-pipeline local tests.
|
|
|
|
These fixtures build the FastVideo-native Cosmos3 pipeline
|
|
(``fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline.Cosmos3OmniDiffusersPipeline``)
|
|
via ``__new__`` and wire it with tiny stub components so the runtime call graph
|
|
(sequential CFG, condition-frame masking, mode dispatch) can be exercised on CPU
|
|
without real weights or ``cosmos_framework``.
|
|
|
|
The stub transformer implements the native DiT's packed-input contract
|
|
(``{"preds_vision": [[1, C, T, H, W], ...]}``) and records, per call, the first
|
|
``text_ids`` token so tests can assert the cond/uncond pass order.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import pytest
|
|
import torch
|
|
from fastvideo.models.schedulers.scheduling_unipc_multistep import (
|
|
UniPCMultistepScheduler,
|
|
)
|
|
from torch import nn
|
|
|
|
_LATENT_CHANNEL = 16
|
|
_LATENT_PATCH_SIZE = 2
|
|
_SPATIAL_FACTOR = 8
|
|
_TEMPORAL_FACTOR = 4
|
|
|
|
|
|
def pytest_configure(config: pytest.Config) -> None:
|
|
"""Register the ``local`` marker used by sibling test files."""
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"local: marker for local-only parity/scaffold tests (skipped in CI)",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stub transformer: records cond/uncond call order; bounded preds_vision.
|
|
# ---------------------------------------------------------------------------
|
|
class StubCosmos3Transformer(nn.Module):
|
|
"""Records each forward's first ``text_ids`` token + returns preds_vision.
|
|
|
|
``preds_vision`` is keyed by the first text token (so the conditional and
|
|
unconditional passes return different velocities) and is zero on
|
|
conditioning frames, matching the real DiT's unpatchify output.
|
|
"""
|
|
|
|
def __init__(self, latent_channel: int = _LATENT_CHANNEL) -> None:
|
|
super().__init__()
|
|
self.latent_channel = latent_channel
|
|
self.embed_tokens = nn.Embedding(64, 8)
|
|
self.calls: list[dict[str, Any]] = []
|
|
|
|
def forward(self, **kwargs: Any) -> dict[str, Any]:
|
|
token_ids = kwargs["text_ids"]
|
|
token = int(token_ids.reshape(-1)[0].item()) if token_ids.numel() else 0
|
|
self.calls.append({"token": token, "kwargs": dict(kwargs)})
|
|
scale = 0.01 * (1.0 + (token % 7))
|
|
preds: list[torch.Tensor] = []
|
|
for latent, _shape, nfi in zip(kwargs["vision_tokens"], kwargs["vision_token_shapes"],
|
|
kwargs["vision_noisy_frame_indexes"]):
|
|
lat = latent.squeeze(0) if latent.dim() == 5 else latent # [C, T, H, W]
|
|
out = torch.zeros_like(lat)
|
|
if nfi.numel() > 0:
|
|
out[:, nfi] = scale * torch.tanh(lat[:, nfi])
|
|
preds.append(out.unsqueeze(0))
|
|
return {"preds_vision": preds}
|
|
|
|
|
|
class _StubLatentDist:
|
|
|
|
def __init__(self, latents: torch.Tensor) -> None:
|
|
self._latents = latents
|
|
|
|
def mode(self) -> torch.Tensor:
|
|
return self._latents
|
|
|
|
|
|
class StubCosmos3VAE:
|
|
"""Deterministic VAE shaped by the Wan scale factors."""
|
|
|
|
def __init__(self, z_dim: int = _LATENT_CHANNEL) -> None:
|
|
self.config = SimpleNamespace(
|
|
z_dim=z_dim,
|
|
scale_factor_temporal=_TEMPORAL_FACTOR,
|
|
scale_factor_spatial=_SPATIAL_FACTOR,
|
|
latents_mean=[0.0] * z_dim,
|
|
latents_std=[1.0] * z_dim,
|
|
)
|
|
|
|
def encode(self, video: torch.Tensor):
|
|
b, _c, t, h, w = video.shape
|
|
lt = (t - 1) // self.config.scale_factor_temporal + 1
|
|
lh = h // self.config.scale_factor_spatial
|
|
lw = w // self.config.scale_factor_spatial
|
|
return _StubLatentDist(torch.ones(b, self.config.z_dim, lt, lh, lw, dtype=video.dtype, device=video.device))
|
|
|
|
def decode(self, z: torch.Tensor):
|
|
b, _c, lt, lh, lw = z.shape
|
|
t = (lt - 1) * self.config.scale_factor_temporal + 1
|
|
h = lh * self.config.scale_factor_spatial
|
|
w = lw * self.config.scale_factor_spatial
|
|
sig = torch.nan_to_num(torch.tanh(z[:, :1, :1, :1, :1])).reshape(b, 1, 1, 1, 1)
|
|
return torch.clamp(torch.zeros(b, 3, t, h, w, dtype=z.dtype, device=z.device) + sig, -1.0, 1.0)
|
|
|
|
|
|
class StubQwen2Tokenizer:
|
|
"""Qwen2-shaped chat tokenizer stub (special tokens + chat template)."""
|
|
|
|
eos_token_id = 62
|
|
_SPECIAL = {"<|vision_start|>": 60, "<|vision_end|>": 61}
|
|
|
|
def convert_tokens_to_ids(self, token: str) -> int:
|
|
return self._SPECIAL[token]
|
|
|
|
def apply_chat_template(self, conversations, *, tokenize=True, add_generation_prompt=True, add_vision_id=False):
|
|
user = next((c["content"] for c in conversations if c["role"] == "user"), "")
|
|
n = max(1, min(8, len(user) % 8 + 1))
|
|
return [10 + (i % 40) for i in range(n)]
|
|
|
|
|
|
def make_scheduler(flow_shift: float = 10.0) -> UniPCMultistepScheduler:
|
|
return UniPCMultistepScheduler(
|
|
num_train_timesteps=1000,
|
|
solver_order=2,
|
|
prediction_type="flow_prediction",
|
|
use_flow_sigmas=True,
|
|
flow_shift=flow_shift,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pipeline factory — builds the native pipeline via __new__ + stub modules.
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.fixture
|
|
def make_cosmos3_pipeline():
|
|
"""Return a factory building the native Cosmos3 pipeline wired with stubs."""
|
|
|
|
def _make(**overrides: Any):
|
|
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import ( # noqa: F401
|
|
Cosmos3OmniDiffusersPipeline, )
|
|
|
|
pipe = Cosmos3OmniDiffusersPipeline.__new__(Cosmos3OmniDiffusersPipeline)
|
|
scheduler = make_scheduler()
|
|
pipe.modules = {
|
|
"transformer": StubCosmos3Transformer(),
|
|
"vae": StubCosmos3VAE(),
|
|
"scheduler": scheduler,
|
|
"text_tokenizer": StubQwen2Tokenizer(),
|
|
}
|
|
pipe.scheduler = scheduler
|
|
pipe._base_scheduler_config = scheduler.config
|
|
pipe._current_flow_shift = float(scheduler.config.flow_shift)
|
|
pipe._engine_init_flow_shift = 10.0
|
|
for key, value in overrides.items():
|
|
setattr(pipe, key, value)
|
|
return pipe
|
|
|
|
return _make
|
|
|
|
|
|
@pytest.fixture
|
|
def make_cosmos3_stage():
|
|
"""Return a factory building a ``Cosmos3DenoisingStage`` bound to a pipeline."""
|
|
|
|
def _make(pipeline):
|
|
from fastvideo.pipelines.stages.cosmos3_stages import Cosmos3DenoisingStage
|
|
|
|
return Cosmos3DenoisingStage(
|
|
transformer=pipeline.modules["transformer"],
|
|
scheduler=pipeline.modules["scheduler"],
|
|
vae=pipeline.modules["vae"],
|
|
tokenizer=pipeline.modules["text_tokenizer"],
|
|
pipeline=pipeline,
|
|
)
|
|
|
|
return _make
|
|
|
|
|
|
def make_forward_batch(*, num_frames: int, height: int, width: int, image: Any = None, **overrides: Any):
|
|
"""Build a tiny ``ForwardBatch`` for the Cosmos3 stage."""
|
|
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
|
|
|
|
values: dict[str, Any] = dict(
|
|
data_type="video",
|
|
prompt="a calm ocean at sunrise",
|
|
negative_prompt="",
|
|
height=height,
|
|
width=width,
|
|
num_frames=num_frames,
|
|
fps=24,
|
|
num_inference_steps=2,
|
|
guidance_scale=6.0,
|
|
generator=torch.Generator("cpu").manual_seed(0),
|
|
preprocessed_image=image,
|
|
)
|
|
values.update(overrides)
|
|
return ForwardBatch(**values)
|
|
|
|
|
|
def make_fastvideo_args():
|
|
"""Build minimal ``fastvideo_args`` (only ``pipeline_config`` is read)."""
|
|
from fastvideo.configs.pipelines.cosmos3 import Cosmos3Config
|
|
|
|
cfg = Cosmos3Config()
|
|
arch = cfg.dit_config.arch_config
|
|
arch.latent_channel = _LATENT_CHANNEL
|
|
arch.latent_patch_size = _LATENT_PATCH_SIZE
|
|
arch.temporal_compression_factor = _TEMPORAL_FACTOR
|
|
arch.enable_fps_modulation = False
|
|
return SimpleNamespace(pipeline_config=cfg)
|