[feat] cosmos3: native video pipeline + framework denoise parity

This commit is contained in:
SolitaryThinker
2026-07-20 12:19:45 -07:00
parent 8931f1b5d7
commit 7ae57e6fbd
10 changed files with 1810 additions and 1022 deletions
+20 -15
View File
@@ -1,16 +1,19 @@
# SPDX-License-Identifier: Apache-2.0
"""Cosmos3 pipeline configuration.
Port reference: vllm-omni PR #3454 (https://github.com/vllm-project/vllm-omni/pull/3454)
HEAD ``8536f5b1421f``. Cosmos3 is structurally different from Cosmos 2.5:
Reference of record: the official ``cosmos-framework`` / ``nvidia/Cosmos3-Nano``
checkpoint (``model_index.json``). Cosmos3 is structurally different from
Cosmos 2.5:
- Dual-stack DiT (UND + GEN) lives entirely inside ``Cosmos3VFMTransformer``.
- No separate text encoder — ``Cosmos3LanguageModel`` is inside the DiT, so
``text_encoder_configs`` is the empty tuple. The tokenizer is loaded by the
pipeline's ``__init__`` from the checkpoint's ``text_tokenizer/`` subfolder;
that is an instance-level concern, not a config-level concern.
- VAE is ``DistributedAutoencoderKLWan`` (same as Cosmos 2.5), so the
``Cosmos25VAEConfig`` is reused as-is.
- Dual-pathway (UND + GEN) DiT lives entirely inside ``Cosmos3VFMTransformer``
(``Cosmos3VideoConfig``).
- No separate text encoder — the Qwen3-VL-text backbone is inside the DiT, so
``text_encoder_configs`` is the empty tuple. The Qwen2 tokenizer is loaded as
the ``text_tokenizer`` checkpoint module by the component loader.
- VAE is Wan2.2 ``AutoencoderKLWan`` (z_dim=48, scale_factor_spatial=16),
configured by ``Cosmos3VAEConfig`` (the checkpoint's exact latents_mean/std).
- Scheduler is diffusers ``UniPCMultistepScheduler`` (flow_prediction,
use_flow_sigmas), loaded from the checkpoint.
- T2I default ``flow_shift`` is 3.0 (set per-request by ``_set_flow_shift``);
T2V/I2V use the engine-init default of 1.0 baked into this config.
"""
@@ -22,7 +25,7 @@ import torch
from fastvideo.configs.models import DiTConfig, EncoderConfig, VAEConfig
from fastvideo.configs.models.dits.cosmos3 import (Cosmos3ArchConfig, Cosmos3VideoConfig)
from fastvideo.configs.models.encoders import BaseEncoderOutput
from fastvideo.configs.models.vaes import Cosmos25VAEConfig # Cosmos3 reuses the Wan-based VAE
from fastvideo.configs.models.vaes import Cosmos3VAEConfig # Wan2.2 AutoencoderKLWan
from fastvideo.configs.pipelines.base import PipelineConfig
@@ -34,14 +37,14 @@ def _identity_preprocess_text(prompt: str) -> str:
class Cosmos3Config(PipelineConfig):
"""Configuration for the Cosmos3 video generation pipeline (T2V/I2V/T2I).
Tier A scope: architectural skeleton. Real weights from ``nvidia/Cosmos3-Nano``
are not yet published; this config is shaped to be activatable when they
land without further config changes.
Wires the framework-parity-verified Cosmos3 components: the native
``Cosmos3VideoConfig`` DiT, the Wan2.2 ``Cosmos3VAEConfig`` VAE, the Qwen2
tokenizer (loaded as ``text_tokenizer``), and the UniPC scheduler.
"""
dit_config: DiTConfig = field(default_factory=lambda: Cosmos3VideoConfig(arch_config=Cosmos3ArchConfig()))
vae_config: VAEConfig = field(default_factory=Cosmos25VAEConfig)
vae_config: VAEConfig = field(default_factory=Cosmos3VAEConfig)
# No separate text encoder: Cosmos3LanguageModel lives inside the DiT.
text_encoder_configs: tuple[EncoderConfig, ...] = field(default_factory=tuple)
@@ -55,7 +58,9 @@ class Cosmos3Config(PipelineConfig):
text_encoder_precisions: tuple[str, ...] = field(default_factory=tuple)
embedded_cfg_scale: float = 0.0
flow_shift: float = 1.0 # T2V/I2V engine-init; T2I uses 3.0 set per-request
# T2V/I2V engine-init flow_shift (framework text2video/image2video default);
# T2I overrides to 3.0 per request via Cosmos3DenoisingStage._set_flow_shift.
flow_shift: float = 10.0
vae_tiling: bool = False
vae_sp: bool = False
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,85 @@
# SPDX-License-Identifier: Apache-2.0
"""Cosmos3 (Cosmos3-Nano) inference presets.
Defaults track the official ``cosmos-framework`` ``sample_args`` for the video
paths (``text2video`` / ``image2video``: guidance=6.0, num_steps=35, shift=10.0,
fps=24, num_frames=189) and ``text2image`` (guidance=4.0, num_steps=50,
shift=3.0). The default resolution is 16:9 at a VAE-aligned 704x1280 (spatial
compression 16 -> 44x80 latent grid).
"""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Cosmos3 sequential-CFG UniPC denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
# Framework video negative prompt (Cosmos quality prompt).
_COSMOS3_NEGATIVE_PROMPT = (
"The video captures a series of frames showing ugly scenes, static with no motion, motion blur, "
"over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, "
"underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, "
"jerky movements, low frame rate, artifacting, color banding, unnatural transitions, outdated special effects, "
"fake elements, unconvincing visuals, poorly edited content, jump cuts, visual noise, and flickering. "
"Overall, the video is of poor quality.")
COSMOS3_NANO = InferencePreset(
name="cosmos3_nano",
version=1,
model_family="cosmos3",
description="Cosmos3-Nano text-to-video",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 704,
"width": 1280,
"num_frames": 189,
"fps": 24,
"guidance_scale": 6.0,
"num_inference_steps": 35,
"negative_prompt": _COSMOS3_NEGATIVE_PROMPT,
},
)
COSMOS3_NANO_I2V = InferencePreset(
name="cosmos3_nano_i2v",
version=1,
model_family="cosmos3",
description="Cosmos3-Nano image-to-video",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 704,
"width": 1280,
"num_frames": 189,
"fps": 24,
"guidance_scale": 6.0,
"num_inference_steps": 35,
"negative_prompt": _COSMOS3_NEGATIVE_PROMPT,
},
)
COSMOS3_NANO_T2I = InferencePreset(
name="cosmos3_nano_t2i",
version=1,
model_family="cosmos3",
description="Cosmos3-Nano text-to-image",
workload_type="t2i",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 1024,
"width": 1024,
"num_frames": 1,
"fps": 24,
"guidance_scale": 4.0,
"num_inference_steps": 50,
"negative_prompt": "",
},
)
ALL_PRESETS = (COSMOS3_NANO, COSMOS3_NANO_I2V, COSMOS3_NANO_T2I)
@@ -0,0 +1,230 @@
# SPDX-License-Identifier: Apache-2.0
"""Cosmos3 video denoising stage.
The Cosmos3 video path is monolithic by design: each CFG pass repacks the whole
text+vision sequence (the conditional pass carries prompt tokens, the
unconditional pass carries negative-prompt tokens), so the standard
encode/condition/denoise/decode stage split does not apply. This single stage
owns the full flow, delegating the framework-parity-tested math to
``fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline``:
1. resolve mode (T2I / I2V / T2V) + per-mode defaults, set ``flow_shift``;
2. tokenize the prompt + negative prompt with the Qwen2 chat template;
3. VAE-encode the conditioning frame(s) for I2V / T2I (kept clean), build the
initial noise (clean condition frames + pure noise elsewhere);
4. run the UniPC denoise loop with sequential CFG
(``Cosmos3DenoiseEngine.denoise``);
5. VAE-decode + ``(1 + x) / 2`` clamp to [0, 1].
This mirrors the framework ``Cosmos3OmniDiffusersPipeline.__call__``.
"""
from __future__ import annotations
import weakref
from typing import Any
import torch
from diffusers.utils.torch_utils import randn_tensor
from tqdm import tqdm
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.logger import init_logger
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import (
COSMOS3_VIDEO_NEGATIVE_PROMPT,
Cosmos3DenoiseEngine,
Cosmos3VisionSpec,
_VaeNorm,
cosmos3_special_tokens,
cosmos3_tokenize_caption,
cosmos3_vae_decode,
cosmos3_vae_encode,
)
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.pipelines.stages.base import PipelineStage
logger = init_logger(__name__)
class Cosmos3DenoisingStage(PipelineStage):
"""Full Cosmos3 video denoise: tokenize + encode + denoise + decode."""
def __init__(self, *, transformer, scheduler, vae, tokenizer, pipeline=None) -> None:
self.transformer = transformer
self.scheduler = scheduler
self.vae = vae
self.tokenizer = tokenizer
self.pipeline = weakref.ref(pipeline) if pipeline is not None else None
# ------------------------------------------------------------------
# Geometry helpers
# ------------------------------------------------------------------
@staticmethod
def _latent_frames(num_frames: int, temporal_factor: int) -> int:
return (int(num_frames) - 1) // int(temporal_factor) + 1
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
pipeline_config = fastvideo_args.pipeline_config
arch = pipeline_config.dit_config.arch_config
device = self.transformer.embed_tokens.weight.device
dtype = self.transformer.embed_tokens.weight.dtype
num_frames = int(batch.num_frames) if batch.num_frames is not None else 1
height = int(batch.height)
width = int(batch.width)
fps = float(batch.fps) if batch.fps is not None else float(arch.base_fps)
guidance = float(batch.guidance_scale)
is_t2i = num_frames == 1 and batch.preprocessed_image is None and batch.pil_image is None
is_i2v = (batch.preprocessed_image is not None or batch.pil_image is not None) and not is_t2i
# Per-mode flow_shift, set on the owning pipeline (rebuilds scheduler).
pipe = self.pipeline() if self.pipeline is not None else None
engine_shift = float(getattr(pipe, "_engine_init_flow_shift", pipeline_config.flow_shift or 1.0))
flow_shift = 3.0 if is_t2i else engine_shift
if pipe is not None and hasattr(pipe, "_set_flow_shift"):
pipe._set_flow_shift(flow_shift)
scheduler = pipe.scheduler
else:
scheduler = self.scheduler
# ---- Tokenize prompt + negative prompt ----
prompt = batch.prompt if isinstance(batch.prompt, str) else (batch.prompt[0] if batch.prompt else "")
negative_prompt = batch.negative_prompt
if negative_prompt is None:
negative_prompt = "" if is_t2i else COSMOS3_VIDEO_NEGATIVE_PROMPT
if isinstance(negative_prompt, list):
negative_prompt = negative_prompt[0] if negative_prompt else ""
special_tokens = cosmos3_special_tokens(self.tokenizer)
is_video = not is_t2i
cond_ids = cosmos3_tokenize_caption(self.tokenizer, prompt, is_video=is_video, use_system_prompt=False)
uncond_ids = cosmos3_tokenize_caption(self.tokenizer,
negative_prompt,
is_video=is_video,
use_system_prompt=False)
# ---- VAE normalization constants + geometry ----
norm = _VaeNorm.from_vae(self.vae, dtype)
temporal_factor = int(arch.temporal_compression_factor)
spatial_factor = int(self.vae.config.scale_factor_spatial)
latent_t = self._latent_frames(num_frames, temporal_factor)
latent_h = height // spatial_factor
latent_w = width // spatial_factor
latent_channel = int(arch.latent_channel)
latent_shape = (latent_channel, latent_t, latent_h, latent_w)
generator = batch.generator
if isinstance(generator, list):
generator = generator[0] if generator else None
# ---- Conditioning latent (I2V / T2I) + condition mask ----
condition_frame_indexes: list[int] = []
clean_latent: torch.Tensor | None = None
if is_i2v or (is_t2i and (batch.preprocessed_image is not None or batch.pil_image is not None)):
image = batch.preprocessed_image if batch.preprocessed_image is not None else batch.pil_image
cond_pixels = self._image_to_video_tensor(image, num_frames, height, width, device, dtype)
clean_latent = cosmos3_vae_encode(self.vae, cond_pixels, norm).squeeze(0).float() # [C, T, H, W]
condition_frame_indexes = [0]
# ---- Initial noise (clean condition frames + pure noise elsewhere) ----
pure_noise = randn_tensor(latent_shape, generator=generator, device=device, dtype=dtype).float()
if clean_latent is not None:
cond_mask = torch.zeros((latent_t, 1, 1), device=device, dtype=pure_noise.dtype)
for idx in condition_frame_indexes:
if 0 <= idx < latent_t:
cond_mask[idx, 0, 0] = 1.0
clean = clean_latent.to(device=device, dtype=pure_noise.dtype)
init_latent = cond_mask * clean + (1.0 - cond_mask) * pure_noise
else:
init_latent = pure_noise
spec = Cosmos3VisionSpec(
shape=latent_shape,
condition_frame_indexes=condition_frame_indexes,
clean_latent=clean_latent,
)
# ---- Scheduler timesteps ----
scheduler.set_timesteps(int(batch.num_inference_steps), device=device)
timesteps = scheduler.timesteps
engine = Cosmos3DenoiseEngine(
transformer=self.transformer,
scheduler=scheduler,
special_tokens=special_tokens,
latent_patch_size=int(arch.latent_patch_size),
temporal_modality_margin=int(arch.temporal_modality_margin),
reset_spatial_ids=bool(arch.unified_3d_mrope_reset_spatial_ids),
enable_fps_modulation=bool(arch.enable_fps_modulation),
base_fps=float(arch.base_fps),
temporal_compression_factor=temporal_factor,
include_end_of_generation_token=False,
)
flat_latent = init_latent.reshape(-1)
fps_per_item = [fps] if bool(arch.enable_fps_modulation) else None
final_flat = engine.denoise(
flat_latent=flat_latent,
timesteps=timesteps,
guidance=guidance,
specs=[spec],
cond_token_ids=cond_ids,
uncond_token_ids=uncond_ids,
fps_per_item=fps_per_item,
progress_bar=lambda it: tqdm(it, desc="Cosmos3 denoising"),
)
# ---- Decode: [C, T, H, W] -> pixels [B, 3, T, H, W] in [0, 1] ----
result_latent = final_flat.reshape(latent_shape).unsqueeze(0).to(device=device, dtype=dtype)
decoded = cosmos3_vae_decode(self.vae, result_latent, norm) # [B, 3, T, H, W] in [-1, 1]
video = ((1.0 + decoded) / 2.0).clamp(0.0, 1.0)
batch.latents = result_latent
batch.output = video
return batch
# ------------------------------------------------------------------
# Image preprocessing
# ------------------------------------------------------------------
@staticmethod
def _image_to_video_tensor(
image: Any,
num_frames: int,
height: int,
width: int,
device: torch.device,
dtype: torch.dtype,
) -> torch.Tensor:
"""Coerce a conditioning image into a ``[1, 3, T, H, W]`` pixel tensor in [-1, 1].
The first frame holds the (already preprocessed) conditioning image and
the remaining frames are zero-filled; only frame 0 is kept clean by the
condition mask, so the rest of the clip is denoised from noise.
"""
import torchvision.transforms.functional as TF
if isinstance(image, torch.Tensor):
img = image.float()
# Accept [B,3,T,H,W], [3,T,H,W], [3,H,W], or [B,3,H,W].
if img.dim() == 5:
img = img[0]
if img.dim() == 4 and img.shape[1] == 1:
img = img[:, 0] # [3, H, W]
elif img.dim() == 4:
img = img[:, 0] # take first frame -> [3, H, W]
# Normalize to [-1, 1] if it looks like [0, 1] or [0, 255].
if img.max() > 1.5:
img = img / 127.5 - 1.0
elif hasattr(image, "convert"): # PIL.Image
import numpy as np
arr = np.array(image.convert("RGB"))
img = torch.from_numpy(arr).permute(2, 0, 1).float() / 127.5 - 1.0
else:
raise TypeError(f"Unsupported conditioning image type: {type(image)}")
if img.shape[-2:] != (height, width):
img = TF.resize(img.unsqueeze(0), [height, width]).squeeze(0)
video = torch.zeros((1, 3, num_frames, height, width), device=device, dtype=dtype)
video[0, :, 0] = img.to(device=device, dtype=dtype)
return video
+3
View File
@@ -1268,6 +1268,8 @@ def _register_presets() -> None:
from fastvideo.api.presets import register_preset
from fastvideo.pipelines.basic.cosmos.presets import (
ALL_PRESETS as COSMOS_PRESETS, )
from fastvideo.pipelines.basic.cosmos3.presets import (
ALL_PRESETS as COSMOS3_PRESETS, )
from fastvideo.pipelines.basic.dreamx_world.presets import (
ALL_PRESETS as DREAMX_WORLD_PRESETS, )
from fastvideo.pipelines.basic.gamecraft.presets import (
@@ -1311,6 +1313,7 @@ def _register_presets() -> None:
all_preset_groups = (
COSMOS_PRESETS,
COSMOS3_PRESETS,
DREAMX_WORLD_PRESETS,
FLUX2_PRESETS,
GAMECRAFT_PRESETS,
+144 -230
View File
@@ -1,37 +1,34 @@
# SPDX-License-Identifier: Apache-2.0
"""Shared fixtures for Cosmos3 local parity tests (Phase 2a Tier A).
"""Shared fixtures for the Cosmos3 native-pipeline local tests.
Ported from the vllm-omni reference suite so FastVideo's Tier A parity
coverage can land before NVIDIA publishes the real Cosmos3 weights.
The fixtures here are intentionally self-contained: they do NOT import
``vllm_omni`` and do NOT import any FastVideo Cosmos3 modules (those
do not exist yet; they are introduced in Phase 2b).
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``.
Reference:
- ``tests/diffusion/models/cosmos3/conftest.py`` lines 1-176 from
``vllm-omni`` HEAD ``8536f5b1421f78c7df06af6d96fa195c1ceb6384``.
The stubs reproduce the same name + protocol as the upstream fixtures
so test code can be ported with minimal renaming once FastVideo's
Cosmos3 pipeline class lands and a ``make_cosmos3_pipeline`` fixture
can be re-pointed at the real class.
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
import sys
import types
from types import SimpleNamespace
from typing import Any
import pytest
import torch
from diffusers 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 ``local`` marker so ``pytestmark = [pytest.mark.local]``
in sibling test files does not trigger ``PytestUnknownMarkWarning``.
"""
"""Register the ``local`` marker used by sibling test files."""
config.addinivalue_line(
"markers",
"local: marker for local-only parity/scaffold tests (skipped in CI)",
@@ -39,46 +36,39 @@ def pytest_configure(config: pytest.Config) -> None:
# ---------------------------------------------------------------------------
# Stub scheduler — mirrors vllm-omni ``StubScheduler``.
# Stub transformer: records cond/uncond call order; bounded preds_vision.
# ---------------------------------------------------------------------------
class StubScheduler:
"""Minimal UniPC-shaped scheduler stub used by pipeline call-graph tests.
class StubCosmos3Transformer(nn.Module):
"""Records each forward's first ``text_ids`` token + returns preds_vision.
Mirrors ``tests/diffusion/models/cosmos3/conftest.py:16-30`` from
the vllm-omni reference: records ``set_timesteps`` and ``step`` calls,
advances ``latents`` by ``+noise_pred``, and exposes a ``config``
namespace carrying ``num_train_timesteps`` and ``flow_shift``.
``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,
timesteps: list[int] | None = None,
*,
flow_shift: float = 1.0,
) -> None:
self.timesteps = torch.tensor(timesteps or [9, 3], dtype=torch.int64)
self.config = SimpleNamespace(num_train_timesteps=1000, flow_shift=flow_shift)
self.set_timesteps_calls: list[tuple[int, torch.device]] = []
self.step_calls: list[tuple[torch.Tensor, torch.Tensor, torch.Tensor]] = []
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 set_timesteps(self, num_steps: int, device: torch.device) -> None:
self.set_timesteps_calls.append((num_steps, device))
self.timesteps = torch.arange(num_steps, 0, -1, dtype=torch.int64, device=device)
def step(
self,
noise_pred: torch.Tensor,
timestep: torch.Tensor,
latents: torch.Tensor,
**kwargs: Any,
):
del kwargs
self.step_calls.append((noise_pred.clone(), timestep.clone(), latents.clone()))
return (latents + noise_pred,)
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 _ModeLatentDist:
"""Stub for diffusers ``DiagonalGaussianDistribution.mode``."""
class _StubLatentDist:
def __init__(self, latents: torch.Tensor) -> None:
self._latents = latents
@@ -88,211 +78,135 @@ class _ModeLatentDist:
class StubCosmos3VAE:
"""VAE stub returning deterministic latents shaped by VAE scale factors.
"""Deterministic VAE shaped by the Wan scale factors."""
Mirrors ``tests/diffusion/models/cosmos3/conftest.py:41-70``.
"""
dtype = torch.float32
def __init__(self, z_dim: int = 2, *, temporal: int = 4, spatial: int = 8) -> None:
def __init__(self, z_dim: int = _LATENT_CHANNEL) -> None:
self.config = SimpleNamespace(
z_dim=z_dim,
scale_factor_temporal=temporal,
scale_factor_spatial=spatial,
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):
latent_frames = (video.shape[2] - 1) // self.config.scale_factor_temporal + 1
latent_height = video.shape[-2] // self.config.scale_factor_spatial
latent_width = video.shape[-1] // self.config.scale_factor_spatial
latents = torch.ones(
video.shape[0],
self.config.z_dim,
latent_frames,
latent_height,
latent_width,
dtype=video.dtype,
device=video.device,
)
return SimpleNamespace(latent_dist=_ModeLatentDist(latents))
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, latents: torch.Tensor, return_dict: bool = False):
del return_dict
return (latents,)
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 StubCosmos3Transformer(nn.Module):
"""Transformer stub that records per-call inputs and emits deterministic
output tensors keyed by the first ``text_ids`` token.
class StubQwen2Tokenizer:
"""Qwen2-shaped chat tokenizer stub (special tokens + chat template)."""
Mirrors ``tests/diffusion/models/cosmos3/conftest.py:73-114``.
eos_token_id = 62
_SPECIAL = {"<|vision_start|>": 60, "<|vision_end|>": 61}
The stub also exposes ``cached_kv`` / ``cached_freqs_gen`` so pipeline
tests can assert that:
* ``reset_cache()`` is called before each diffusion loop;
* UND/cond and uncond branches each populate their own cache exactly
once and reuse it on subsequent timesteps.
"""
def convert_tokens_to_ids(self, token: str) -> int:
return self._SPECIAL[token]
def __init__(self, *, latent_channel_size: int = 2) -> None:
super().__init__()
self.latent_channel_size = latent_channel_size
self.cached_kv: Any | None = None
self.cached_freqs_gen: Any | None = None
self.calls: list[dict[str, Any]] = []
self.reset_calls = 0
def reset_cache(self) -> None:
self.reset_calls += 1
self.cached_kv = None
self.cached_freqs_gen = None
def forward(
self,
*,
hidden_states: torch.Tensor,
timestep: torch.Tensor,
text_ids: torch.Tensor,
text_mask: torch.Tensor,
**kwargs: Any,
) -> torch.Tensor:
token = int(text_ids.reshape(-1)[0].item()) if text_ids.numel() else 0
self.calls.append(
{
"token": token,
"timestep": timestep.clone(),
"text_mask": text_mask.clone(),
"cache_before": self.cached_kv,
"kwargs": dict(kwargs),
}
)
if self.cached_kv is None:
marker = torch.tensor([token], dtype=torch.float32)
self.cached_kv = [(marker, marker + 100)]
self.cached_freqs_gen = (marker + 200, marker + 300)
return torch.full_like(hidden_states, float(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 passthrough_progress_bar(iterable):
return iterable
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,
)
# ---------------------------------------------------------------------------
# Tiny config — mirrors test_cosmos3_transformer.py:15-29
# ---------------------------------------------------------------------------
def _tiny_cosmos3_config(**overrides: Any) -> dict:
"""Minimal Cosmos3 transformer config sufficient for shape-only construction.
Mirrors ``tests/diffusion/models/cosmos3/test_cosmos3_transformer.py:15-29``.
"""
config: dict = {
"hidden_size": 8,
"num_hidden_layers": 0,
"num_attention_heads": 2,
"num_key_value_heads": 2,
"head_dim": 4,
"intermediate_size": 16,
"vocab_size": 32,
"latent_patch_size": 1,
"latent_channel": 2,
"rope_scaling": {"mrope_section": [1, 1, 0]},
}
config.update(overrides)
return config
# ---------------------------------------------------------------------------
# Guardrail no-op stub — mirrors conftest.py:121-129
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def fake_cosmos3_guardrails(monkeypatch: pytest.MonkeyPatch):
"""Install a no-op replacement for the Cosmos3 guardrails module.
The vllm-omni reference imports guardrails eagerly; FastVideo's port
may either skip guardrails or use a different module path. Either
way, an autouse stub avoids accidental network-dependent imports
during scaffold-test collection.
"""
module = types.ModuleType("vllm_omni.diffusion.models.cosmos3.guardrails")
module.is_guardrails_enabled = lambda od_config, sampling_params=None: False # type: ignore[attr-defined]
module.ensure_initialized = lambda od_config: None # type: ignore[attr-defined]
module.check_text_safety = lambda text: None # type: ignore[attr-defined]
module.check_video_safety = lambda video: video # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, module.__name__, module)
return module
# ---------------------------------------------------------------------------
# Pipeline factory — kept as a scaffold; activate in Phase 2b when the
# FastVideo Cosmos3 pipeline class exists.
# Pipeline factory — builds the native pipeline via __new__ + stub modules.
# ---------------------------------------------------------------------------
@pytest.fixture
def make_cosmos3_pipeline():
"""Factory that returns a FastVideo Cosmos3 pipeline pre-wired with stubs.
"""Return a factory building the native Cosmos3 pipeline wired with stubs."""
In Phase 2a (Tier A, no real weights), tests that consume this factory
should ``pytest.skip`` if the FastVideo Cosmos3 pipeline class does not
yet exist. In Phase 2b we replace the placeholder construction with
``object.__new__(<FastVideoCosmos3Pipeline>)`` + ``nn.Module.__init__``
and re-point the stubs onto the real attribute names.
def _make(**overrides: Any):
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import ( # noqa: F401
Cosmos3OmniDiffusersPipeline, )
Mirrors the upstream layout at
``tests/diffusion/models/cosmos3/conftest.py:132-157``.
"""
def _make():
try:
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import ( # type: ignore
Cosmos3OmniDiffusersPipeline,
)
except ImportError:
pytest.skip(
"FastVideo Cosmos3 pipeline class not yet implemented "
"(Phase 2b will provide fastvideo.pipelines.basic.cosmos3)."
)
pipeline = object.__new__(Cosmos3OmniDiffusersPipeline)
nn.Module.__init__(pipeline)
pipeline.od_config = SimpleNamespace()
pipeline.device = torch.device("cpu")
pipeline.dtype = torch.float32
pipeline.transformer = StubCosmos3Transformer(latent_channel_size=2)
pipeline.vae = StubCosmos3VAE(z_dim=2)
pipeline.vae_scale_factor_temporal = 4
pipeline.vae_scale_factor_spatial = 8
pipeline.scheduler = StubScheduler([9, 3], flow_shift=1.0)
pipeline._base_scheduler_config = pipeline.scheduler.config
pipeline._engine_init_flow_shift = 1.0
pipeline._current_flow_shift = 1.0
pipeline._guidance_scale = None
pipeline._num_timesteps = None
pipeline.progress_bar = passthrough_progress_bar
return pipeline
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
def make_sampling_params(**overrides: Any) -> SimpleNamespace:
"""Build a SamplingParams-like namespace with Cosmos3's expected fields.
@pytest.fixture
def make_cosmos3_stage():
"""Return a factory building a ``Cosmos3DenoisingStage`` bound to a pipeline."""
Mirrors ``tests/diffusion/models/cosmos3/conftest.py:160-176``.
"""
values = {
"height": None,
"width": None,
"num_frames": None,
"num_inference_steps": None,
"guidance_scale": None,
"generator": None,
"seed": 123,
"num_outputs_per_prompt": 1,
"frame_rate": None,
"resolved_frame_rate": None,
"max_sequence_length": None,
"extra_args": {},
}
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 SimpleNamespace(**values)
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)
@@ -0,0 +1,351 @@
# SPDX-License-Identifier: Apache-2.0
"""Numerical-parity test: FastVideo Cosmos3 denoise/CFG glue vs the framework.
The DiT forward and the sequence-packing are already framework-parity-verified
(``test_cosmos3_dit_parity*`` / ``test_cosmos3_packing_parity``). This test pins
the remaining glue that the native pipeline adds — the SEQUENTIAL classifier-free
guidance velocity and one UniPC scheduler step — against the framework math
(``diffusers_cosmos3.pipeline.Cosmos3OmniDiffusersPipeline.get_cfg_velocity`` /
``__call__``):
* for one denoise step, replicate the framework's ``get_cfg_velocity`` exactly
on top of the OFFICIAL ``Cosmos3VFMNetwork`` forward (oracle): a conditional
pass (prompt tokens) and an unconditional pass (negative-prompt tokens),
each masking the prediction on conditioning frames
(``pred * (1 - condition_mask)``), then ``v = uncond + g*(cond - uncond)``;
* run FastVideo's :func:`cosmos3_get_cfg_velocity` with the native DiT (the
framework weights copied in) + the native packer, and assert the velocity
matches the oracle;
* take one ``UniPCMultistepScheduler.step`` on each (the actual checkpoint
scheduler) and assert the stepped latent matches;
* drive :meth:`Cosmos3DenoiseEngine.denoise` for >= 2 steps and assert it
equals the manual framework step-by-step loop.
CPU / float32, via the reference SDPA monkey-patch. The official model is the
parity ORACLE.
Run:
cd <worktree> && <fv-cosmos3 python> -m pytest \
tests/local_tests/cosmos3/test_cosmos3_denoise_cfg_parity.py -q
"""
from __future__ import annotations
import pytest
import torch
# The official framework provides the parity oracle.
cosmos_framework = pytest.importorskip(
"cosmos_framework",
reason="cosmos_framework not installed; run in fv-cosmos3 env.",
)
from diffusers import UniPCMultistepScheduler # noqa: E402
from .test_cosmos3_dit_parity import _copy_weights # noqa: E402
from .test_cosmos3_dit_parity_mrope import ( # noqa: E402
_LATENT_CHANNEL,
_LATENT_PATCH_SIZE,
_RESET_SPATIAL_IDS,
_TCF,
_TEMPORAL_MODALITY_MARGIN,
_build_tiny_cosmos3_mrope,
_build_tiny_fastvideo_dit_mrope,
)
from .test_cosmos3_reference_forward import _apply_sdpa_patches # noqa: E402
pytestmark = [pytest.mark.local]
_apply_sdpa_patches()
# Tiny special tokens (< tiny vocab_size=64), video path appends eos + sog.
_SPECIAL_TOKENS = {
"start_of_generation": 60,
"end_of_generation": 61,
"eos_token_id": 62,
}
def _make_scheduler() -> UniPCMultistepScheduler:
"""UniPC scheduler matching the Cosmos3 checkpoint flow config."""
return UniPCMultistepScheduler(
num_train_timesteps=1000,
solver_order=2,
prediction_type="flow_prediction",
use_flow_sigmas=True,
flow_shift=10.0,
)
# ---------------------------------------------------------------------------
# Framework-oracle CFG velocity (replicates pipeline.get_cfg_velocity math).
# ---------------------------------------------------------------------------
def _framework_pack(*, text_ids, vision_latent, cond_frames, timestep):
from cosmos_framework.data.vfm.sequence_packing import (
GenerationDataClean,
SequencePlan,
pack_input_sequence,
)
# vision_latent is [1, C, T, H, W]; temporal dim is axis 2.
gen_data_clean = GenerationDataClean(
batch_size=1,
is_image_batch=(vision_latent.shape[2] == 1),
x0_tokens_vision=[vision_latent],
fps_vision=None,
num_vision_items_per_sample=[1],
)
plans = [SequencePlan(has_text=True, has_vision=True, condition_frame_indexes_vision=list(cond_frames))]
return pack_input_sequence(
sequence_plans=plans,
input_text_indexes=[list(text_ids)],
gen_data_clean=gen_data_clean,
input_timesteps=torch.tensor([timestep], dtype=torch.float32),
special_tokens=_SPECIAL_TOKENS,
latent_patch_size=_LATENT_PATCH_SIZE,
include_end_of_generation_token=False,
position_embedding_type="unified_3d_mrope",
unified_3d_mrope_reset_spatial_ids=_RESET_SPATIAL_IDS,
unified_3d_mrope_temporal_modality_margin=_TEMPORAL_MODALITY_MARGIN,
enable_fps_modulation=False,
base_fps=24.0,
temporal_compression_factor=_TCF,
)
def _framework_inputs(ps):
"""Framework PackedSequence -> framework Cosmos3VFMNetwork forward kwargs."""
return dict(packed_seq=ps)
def _framework_cfg_velocity(
*,
vfm,
flat_latent: torch.Tensor,
timestep: torch.Tensor,
guidance: float,
vision_shape: tuple[int, int, int, int],
cond_frames: list[int],
cond_ids: list[int],
uncond_ids: list[int],
) -> torch.Tensor:
"""Replicate the framework ``get_cfg_velocity`` on the oracle model.
Single vision item; sequential cond then uncond pass; mask condition
frames; ``v = uncond + g*(cond - uncond)``.
"""
timestep_value = float(timestep.reshape(()).item())
vision_latent = flat_latent.reshape(vision_shape) # [C, T, H, W]
def _run(text_ids: list[int]) -> torch.Tensor:
ps = _framework_pack(
text_ids=text_ids,
# The framework packer expects a 5D [1, C, T, H, W] latent.
vision_latent=vision_latent.unsqueeze(0),
cond_frames=cond_frames,
timestep=timestep_value,
)
out = vfm(**_framework_inputs(ps))
preds = out.get("preds_vision")
cond_mask = ps.vision.condition_mask[0] # [T] or [T,1,1]
if preds is None:
return torch.zeros_like(flat_latent)
pred = preds[0].squeeze(0) # [C, T, H, W]
keep = (1.0 - cond_mask.reshape(-1, 1, 1)).to(dtype=pred.dtype, device=pred.device)
velocity = pred * keep if keep.sum() > 0 else torch.zeros_like(pred)
return velocity.reshape(-1)
cond_v = _run(cond_ids)
uncond_v = _run(uncond_ids)
return uncond_v + guidance * (cond_v - uncond_v)
# ---------------------------------------------------------------------------
# Cases: T2V (no cond), I2V (cond frame 0), single-frame T2I.
# ---------------------------------------------------------------------------
_CASES = [
pytest.param(2, 4, 4, 6, [], id="t2v_2x2x2"),
pytest.param(3, 8, 4, 5, [0], id="i2v_3x4x2_cond0"),
pytest.param(1, 8, 8, 4, [], id="t2i_1x4x4"),
]
def _build_models(num_layers: int = 2, seed_model: int = 42):
vfm = _build_tiny_cosmos3_mrope(seed=seed_model, num_layers=num_layers)
dit = _build_tiny_fastvideo_dit_mrope(num_layers=num_layers)
_copy_weights(vfm, dit)
return vfm, dit
def _fastvideo_velocity(dit, *, flat_latent, timestep, guidance, vision_shape, cond_frames, cond_ids, uncond_ids):
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import (
Cosmos3VisionSpec,
cosmos3_get_cfg_velocity,
)
spec = Cosmos3VisionSpec(shape=vision_shape, condition_frame_indexes=list(cond_frames))
return cosmos3_get_cfg_velocity(
transformer=dit,
flat_latent=flat_latent,
timestep=timestep,
guidance=guidance,
specs=[spec],
cond_token_ids=cond_ids,
uncond_token_ids=uncond_ids,
special_tokens=_SPECIAL_TOKENS,
latent_patch_size=_LATENT_PATCH_SIZE,
temporal_modality_margin=_TEMPORAL_MODALITY_MARGIN,
reset_spatial_ids=_RESET_SPATIAL_IDS,
enable_fps_modulation=False,
base_fps=24.0,
temporal_compression_factor=_TCF,
)
class TestCosmos3DenoiseCFGParity:
@pytest.mark.parametrize(("grid_t", "latent_h", "latent_w", "n_text", "cond"), _CASES)
def test_cfg_velocity_matches_framework(self, grid_t, latent_h, latent_w, n_text, cond):
vfm, dit = _build_models()
torch.manual_seed(0)
cond_ids = torch.randint(0, 60, (n_text,)).tolist()
uncond_ids = torch.randint(0, 60, (max(1, n_text - 1),)).tolist()
vision_shape = (_LATENT_CHANNEL, grid_t, latent_h, latent_w)
flat_latent = torch.randn(int(torch.tensor(vision_shape).prod()))
timestep = torch.tensor([[500.0]]) # framework expects [1,1]; we reshape to scalar
guidance = 6.0
fw_v = _framework_cfg_velocity(
vfm=vfm,
flat_latent=flat_latent,
timestep=timestep,
guidance=guidance,
vision_shape=vision_shape,
cond_frames=cond,
cond_ids=cond_ids,
uncond_ids=uncond_ids,
)
fv_v = _fastvideo_velocity(
dit,
flat_latent=flat_latent,
timestep=timestep,
guidance=guidance,
vision_shape=vision_shape,
cond_frames=cond,
cond_ids=cond_ids,
uncond_ids=uncond_ids,
)
assert fw_v.shape == fv_v.shape, f"shape: fw={fw_v.shape} fv={fv_v.shape}"
max_abs = (fw_v - fv_v).abs().max().item()
print(f"\n[cfg_velocity {grid_t}x{latent_h}x{latent_w} cond={cond}] max abs diff = {max_abs:.3e}")
torch.testing.assert_close(fv_v, fw_v, atol=1e-4, rtol=1e-3)
def test_one_unipc_step_matches_framework(self):
"""CFG velocity + one UniPC step: FastVideo == framework math."""
vfm, dit = _build_models()
grid_t, latent_h, latent_w = 2, 4, 4
vision_shape = (_LATENT_CHANNEL, grid_t, latent_h, latent_w)
torch.manual_seed(3)
cond_ids = torch.randint(0, 60, (5,)).tolist()
uncond_ids = torch.randint(0, 60, (4,)).tolist()
flat_latent = torch.randn(int(torch.tensor(vision_shape).prod()))
guidance = 6.0
fw_sched = _make_scheduler()
fv_sched = _make_scheduler()
fw_sched.set_timesteps(4, device=torch.device("cpu"))
fv_sched.set_timesteps(4, device=torch.device("cpu"))
t = fw_sched.timesteps[0]
fw_v = _framework_cfg_velocity(
vfm=vfm,
flat_latent=flat_latent,
timestep=t.reshape(1, 1),
guidance=guidance,
vision_shape=vision_shape,
cond_frames=[],
cond_ids=cond_ids,
uncond_ids=uncond_ids,
)
fw_stepped = fw_sched.step(model_output=fw_v, timestep=t, sample=flat_latent.unsqueeze(0),
return_dict=False)[0].squeeze(0)
fv_v = _fastvideo_velocity(
dit,
flat_latent=flat_latent,
timestep=t.reshape(1),
guidance=guidance,
vision_shape=vision_shape,
cond_frames=[],
cond_ids=cond_ids,
uncond_ids=uncond_ids,
)
fv_stepped = fv_sched.step(model_output=fv_v, timestep=t, sample=flat_latent.unsqueeze(0),
return_dict=False)[0].squeeze(0)
max_abs = (fw_stepped - fv_stepped).abs().max().item()
print(f"\n[unipc_step] max abs diff = {max_abs:.3e}")
torch.testing.assert_close(fv_stepped, fw_stepped, atol=1e-4, rtol=1e-3)
def test_full_denoise_loop_matches_framework(self):
"""Cosmos3DenoiseEngine.denoise (>= 2 steps) == framework step-by-step."""
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import (
Cosmos3DenoiseEngine,
Cosmos3VisionSpec,
)
vfm, dit = _build_models()
grid_t, latent_h, latent_w = 2, 4, 4
vision_shape = (_LATENT_CHANNEL, grid_t, latent_h, latent_w)
torch.manual_seed(5)
cond_ids = torch.randint(0, 60, (5,)).tolist()
uncond_ids = torch.randint(0, 60, (4,)).tolist()
flat_latent = torch.randn(int(torch.tensor(vision_shape).prod()))
guidance = 6.0
num_steps = 3
# Manual framework loop (oracle).
fw_sched = _make_scheduler()
fw_sched.set_timesteps(num_steps, device=torch.device("cpu"))
fw_latent = flat_latent.clone()
for t in fw_sched.timesteps:
v = _framework_cfg_velocity(
vfm=vfm,
flat_latent=fw_latent,
timestep=t.reshape(1, 1),
guidance=guidance,
vision_shape=vision_shape,
cond_frames=[],
cond_ids=cond_ids,
uncond_ids=uncond_ids,
)
fw_latent = fw_sched.step(model_output=v, timestep=t, sample=fw_latent.unsqueeze(0),
return_dict=False)[0].squeeze(0)
# FastVideo engine loop.
fv_sched = _make_scheduler()
fv_sched.set_timesteps(num_steps, device=torch.device("cpu"))
engine = Cosmos3DenoiseEngine(
transformer=dit,
scheduler=fv_sched,
special_tokens=_SPECIAL_TOKENS,
latent_patch_size=_LATENT_PATCH_SIZE,
temporal_modality_margin=_TEMPORAL_MODALITY_MARGIN,
reset_spatial_ids=_RESET_SPATIAL_IDS,
enable_fps_modulation=False,
base_fps=24.0,
temporal_compression_factor=_TCF,
)
spec = Cosmos3VisionSpec(shape=vision_shape, condition_frame_indexes=[])
fv_latent = engine.denoise(
flat_latent=flat_latent.clone(),
timesteps=fv_sched.timesteps,
guidance=guidance,
specs=[spec],
cond_token_ids=cond_ids,
uncond_token_ids=uncond_ids,
)
assert fv_latent.shape == fw_latent.shape
max_abs = (fw_latent - fv_latent).abs().max().item()
print(f"\n[full_denoise {num_steps} steps] final latent max abs diff = {max_abs:.3e}")
torch.testing.assert_close(fv_latent, fw_latent, atol=1e-4, rtol=1e-3)
@@ -1,199 +1,198 @@
# SPDX-License-Identifier: Apache-2.0
"""Cosmos3 pipeline call-graph parity (Tier A scaffold).
"""Cosmos3 native-pipeline call-graph contract (Tier A, no real weights).
Reference:
* ``vllm_omni/diffusion/models/cosmos3/pipeline_cosmos3.py:883-1033`` —
``Cosmos3OmniDiffusersPipeline.diffuse``: 3-mode CFG denoising loop
with UND-cache management.
* ``vllm_omni/diffusion/models/cosmos3/pipeline_cosmos3.py:1037-1067`` —
``forward``: parses the request, selects T2I/T2V/I2V mode, dispatches
defaults.
* Reference test invariants at
``tests/diffusion/models/cosmos3/test_cosmos3_pipeline.py:126-156,196-251``.
Pins the runtime call graph of the FastVideo-native Cosmos3 pipeline against the
framework math, using the stub components from ``conftest.py`` (no real weights,
no ``cosmos_framework``). The native pipeline replaced the vllm-omni-derived
``diffuse``/``forward(req)``/``reset_cache`` skeleton with a stage-based
``Cosmos3DenoisingStage`` + ``Cosmos3DenoiseEngine`` doing SEQUENTIAL CFG.
The Tier A scaffold uses the stubs from ``conftest.py`` (StubScheduler,
StubCosmos3VAE, StubCosmos3Transformer) so the call-graph can be tested
without a real DiT or VAE. The 4 invariants under test:
Invariants under test:
1. ``diffuse(...)`` calls ``transformer.reset_cache()`` exactly once
before iterating timesteps;
2. With ``do_cfg=True`` and ``guidance_interval=None``, each timestep
invokes the transformer twice — once with ``cond_ids`` and once with
``uncond_ids`` — and reuses the cached UND K/V from step 1 onward
(asserted via ``StubCosmos3Transformer.calls`` order);
3. I2V mode: ``velocity_mask`` zeros frame-0 noise predictions, and
``image_latent`` is re-injected into frame 0 after each scheduler step;
4. ``forward`` selects T2I vs T2V mode from ``prompt["modalities"]``
and applies the per-mode default ``flow_shift`` / ``num_frames``.
1. SEQUENTIAL CFG order — per UniPC step, the transformer is called twice,
conditional (prompt tokens) then unconditional (negative-prompt tokens), in
that order; over N steps the call order is ``[cond, uncond] * N``.
2. CFG combination — the per-step velocity equals
``uncond + guidance * (cond - uncond)`` (verified against the stub's known
per-token output) and one UniPC step advances the latent accordingly.
3. I2V conditioning — a conditioning image is VAE-encoded and frame 0 is kept
clean: its velocity is zeroed (condition mask) so the decoded clip's
frame-0 latent equals the clean conditioning latent.
4. Mode dispatch — the stage routes T2V (num_frames>1, flow_shift=10.0,
``is_video`` tokenization) vs T2I (num_frames==1, flow_shift=3.0, image
tokenization), applying the per-mode ``flow_shift``.
"""
from __future__ import annotations
import pytest
import torch
from .conftest import make_fastvideo_args, make_forward_batch
pytestmark = [pytest.mark.local]
def _ids(value: int) -> torch.Tensor:
return torch.tensor([[value]], dtype=torch.long)
_LATENT_CHANNEL = 16
_LATENT_PATCH_SIZE = 2
_TEMPORAL_FACTOR = 4
_COND_TOKEN = 2
_UNCOND_TOKEN = 1
def _mask() -> torch.Tensor:
return torch.ones(1, 1, dtype=torch.long)
def _engine(pipeline, scheduler):
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import Cosmos3DenoiseEngine
def test_diffuse_resets_cache_and_calls_cfg_in_order(make_cosmos3_pipeline) -> None:
"""Asserts the 3-mode CFG call graph: ``reset_cache`` first, then
interleaved ``cond / uncond`` transformer calls with cache reuse.
With timesteps ``[900, 100]`` and ``guidance_scale=3.0``, the upstream
reference (test_cosmos3_pipeline.py:126-142) asserts the call order is
``[2, 1, 2]`` — i.e. step 0 calls cond (token=2) then uncond (token=1),
step 1 reuses the cached UND K/V and only calls cond again (token=2).
"""
pipeline = make_cosmos3_pipeline()
latents = torch.zeros(1, 2, 1, 1, 1)
result = pipeline.diffuse(
latents=latents,
timesteps=torch.tensor([900, 100]),
cond_ids=_ids(2),
cond_mask=_mask(),
uncond_ids=_ids(1),
uncond_mask=_mask(),
guidance_scale=3.0,
shared_kwargs={"video_shape": (1, 1, 1), "fps": 24.0},
guidance_interval=(500.0, 1000.0),
return Cosmos3DenoiseEngine(
transformer=pipeline.modules["transformer"],
scheduler=scheduler,
special_tokens={"start_of_generation": 60, "end_of_generation": 61, "eos_token_id": 62},
latent_patch_size=_LATENT_PATCH_SIZE,
temporal_modality_margin=15_000,
reset_spatial_ids=True,
enable_fps_modulation=False,
base_fps=24.0,
temporal_compression_factor=_TEMPORAL_FACTOR,
)
assert pipeline.transformer.reset_calls == 1
assert [call["token"] for call in pipeline.transformer.calls] == [2, 1, 2]
torch.testing.assert_close(result, torch.full_like(latents, 6.0))
def test_diffuse_i2v_velocity_mask_zeros_frame_zero(make_cosmos3_pipeline) -> None:
"""Asserts I2V velocity-mask + image-latent re-injection contract.
def test_sequential_cfg_calls_cond_then_uncond_each_step(make_cosmos3_pipeline) -> None:
"""Each UniPC step calls the transformer cond-then-uncond, in order."""
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import Cosmos3VisionSpec
Cross-check: pipeline_cosmos3.py:937-951. The velocity_mask zeroes
noise predictions on conditioning frames before stepping, and
``image_latent`` is overwritten into frame 0 of the output latents
after each scheduler step.
"""
pipeline = make_cosmos3_pipeline()
result = pipeline.diffuse(
latents=torch.zeros(1, 2, 2, 1, 1),
timesteps=torch.tensor([7]),
cond_ids=_ids(2),
cond_mask=_mask(),
uncond_ids=_ids(1),
uncond_mask=_mask(),
guidance_scale=1.0,
shared_kwargs={"video_shape": (2, 1, 1), "fps": 24.0},
velocity_mask=torch.tensor([[[[[0.0]], [[1.0]]]]]),
image_latent=torch.full((1, 2, 1, 1, 1), 7.0),
scheduler = pipeline.modules["scheduler"]
scheduler.set_timesteps(2, device=torch.device("cpu"))
engine = _engine(pipeline, scheduler)
shape = (_LATENT_CHANNEL, 2, 2, 2)
flat = torch.randn(int(torch.tensor(shape).prod()))
spec = Cosmos3VisionSpec(shape=shape, condition_frame_indexes=[])
engine.denoise(
flat_latent=flat,
timesteps=scheduler.timesteps,
guidance=6.0,
specs=[spec],
cond_token_ids=[_COND_TOKEN, 5, 6],
uncond_token_ids=[_UNCOND_TOKEN, 7],
)
torch.testing.assert_close(result[:, :, 0:1], torch.full((1, 2, 1, 1, 1), 7.0))
tokens = [c["token"] for c in pipeline.modules["transformer"].calls]
# 2 steps -> 4 calls: cond, uncond, cond, uncond.
assert tokens == [_COND_TOKEN, _UNCOND_TOKEN, _COND_TOKEN, _UNCOND_TOKEN]
@pytest.mark.parametrize(
("modalities", "expected_is_t2i", "expected_default_flow_shift", "expected_default_frames"),
[
(["image"], True, 3.0, 1),
(["video"], False, 1.0, 189),
],
)
def test_forward_mode_dispatch_t2i_vs_t2v(
make_cosmos3_pipeline,
modalities: list[str],
expected_is_t2i: bool,
expected_default_flow_shift: float,
expected_default_frames: int,
) -> None:
"""Asserts forward() routes to T2I vs T2V mode based on prompt modalities,
and applies per-mode defaults.
def test_cfg_velocity_combination_formula(make_cosmos3_pipeline) -> None:
"""The per-step velocity equals ``uncond + g*(cond - uncond)``.
Cross-check: pipeline_cosmos3.py:1069-1093. T2I defaults:
``num_frames=1``, ``flow_shift=3.0``, ``num_inference_steps=50``,
``guidance_interval=[400, 1000]``. T2V defaults: ``num_frames=189``,
``flow_shift=engine_init`` (1.0 here), ``num_inference_steps=35``,
no guidance_interval.
The stub returns ``scale(token) * tanh(latent)`` on noisy frames, so the
expected velocity is a closed form we can check exactly.
"""
pipeline = make_cosmos3_pipeline()
from types import SimpleNamespace
captured: dict[str, object] = {"flow_shifts": [], "format_calls": []}
def fake_format(prompt, negative_prompt, num_frames, frame_rate, height, width, *args, **kwargs):
captured["format_calls"].append(
{
"is_t2i": kwargs.get("is_t2i"),
"num_frames": num_frames,
}
)
return _ids(2), _mask(), _ids(1), _mask()
pipeline._format_and_tokenize_prompts = fake_format
pipeline._prepare_latents = lambda *a, **kw: torch.zeros(1, 2, 1, 1, 1)
pipeline._set_flow_shift = lambda target: captured["flow_shifts"].append(target)
pipeline._set_scheduler_timesteps = lambda steps: setattr(
pipeline.scheduler, "timesteps", torch.tensor([7])
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import (
Cosmos3VisionSpec,
cosmos3_get_cfg_velocity,
)
pipeline.diffuse = lambda **kw: kw["latents"]
pipeline._decode_latents = lambda latents: latents
output = pipeline.forward(
SimpleNamespace(
prompts=[{"prompt": "test", "modalities": modalities}],
sampling_params=SimpleNamespace(
height=None,
width=None,
num_frames=None,
num_inference_steps=None,
guidance_scale=None,
generator=None,
seed=123,
num_outputs_per_prompt=1,
frame_rate=None,
resolved_frame_rate=None,
max_sequence_length=None,
extra_args={},
),
)
)
assert captured["format_calls"][-1]["is_t2i"] is expected_is_t2i
assert captured["format_calls"][-1]["num_frames"] == expected_default_frames
assert captured["flow_shifts"] == [expected_default_flow_shift]
expected_output_key = "image" if expected_is_t2i else "video"
assert expected_output_key in output.output
def test_forward_rejects_both_image_and_video_modalities(make_cosmos3_pipeline) -> None:
"""Asserts ``_is_t2i_request`` raises ValueError when a prompt
requests both image and video modalities simultaneously.
Cross-check: pipeline_cosmos3.py:490-496.
"""
pipeline = make_cosmos3_pipeline()
transformer = pipeline.modules["transformer"]
shape = (_LATENT_CHANNEL, 2, 2, 2)
flat = torch.randn(int(torch.tensor(shape).prod()))
guidance = 6.0
from types import SimpleNamespace
v = cosmos3_get_cfg_velocity(
transformer=transformer,
flat_latent=flat,
timestep=torch.tensor([500.0]),
guidance=guidance,
specs=[Cosmos3VisionSpec(shape=shape, condition_frame_indexes=[])],
cond_token_ids=[_COND_TOKEN, 5, 6],
uncond_token_ids=[_UNCOND_TOKEN, 7],
special_tokens={"start_of_generation": 60, "end_of_generation": 61, "eos_token_id": 62},
latent_patch_size=_LATENT_PATCH_SIZE,
temporal_modality_margin=15_000,
reset_spatial_ids=True,
enable_fps_modulation=False,
base_fps=24.0,
temporal_compression_factor=_TEMPORAL_FACTOR,
)
with pytest.raises(ValueError, match="both image and video"):
pipeline.forward(
SimpleNamespace(
prompts=[{"prompt": "x", "modalities": ["image", "video"]}],
sampling_params=SimpleNamespace(
height=None,
width=None,
num_frames=None,
num_inference_steps=None,
guidance_scale=None,
generator=None,
seed=123,
num_outputs_per_prompt=1,
frame_rate=None,
resolved_frame_rate=None,
max_sequence_length=None,
extra_args={},
),
)
)
lat = flat.reshape(shape)
scale_cond = 0.01 * (1.0 + (_COND_TOKEN % 7))
scale_uncond = 0.01 * (1.0 + (_UNCOND_TOKEN % 7))
cond_v = (scale_cond * torch.tanh(lat)).reshape(-1)
uncond_v = (scale_uncond * torch.tanh(lat)).reshape(-1)
expected = uncond_v + guidance * (cond_v - uncond_v)
torch.testing.assert_close(v, expected, atol=1e-6, rtol=1e-5)
def test_i2v_keeps_condition_frame_clean(make_cosmos3_pipeline) -> None:
"""I2V: frame-0 velocity is zeroed so the conditioning frame stays clean."""
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import (
Cosmos3VisionSpec,
cosmos3_get_cfg_velocity,
)
pipeline = make_cosmos3_pipeline()
transformer = pipeline.modules["transformer"]
shape = (_LATENT_CHANNEL, 3, 2, 2) # 3 latent frames, frame 0 conditioned
flat = torch.randn(int(torch.tensor(shape).prod()))
v = cosmos3_get_cfg_velocity(
transformer=transformer,
flat_latent=flat,
timestep=torch.tensor([500.0]),
guidance=6.0,
specs=[Cosmos3VisionSpec(shape=shape, condition_frame_indexes=[0])],
cond_token_ids=[_COND_TOKEN, 5, 6],
uncond_token_ids=[_UNCOND_TOKEN, 7],
special_tokens={"start_of_generation": 60, "end_of_generation": 61, "eos_token_id": 62},
latent_patch_size=_LATENT_PATCH_SIZE,
temporal_modality_margin=15_000,
reset_spatial_ids=True,
enable_fps_modulation=False,
base_fps=24.0,
temporal_compression_factor=_TEMPORAL_FACTOR,
)
v_grid = v.reshape(shape) # [C, T, H, W]
# Condition frame 0 velocity must be exactly zero; noisy frames non-zero.
assert torch.count_nonzero(v_grid[:, 0]) == 0
assert torch.count_nonzero(v_grid[:, 1:]) > 0
def test_stage_mode_dispatch_t2v(make_cosmos3_pipeline, make_cosmos3_stage) -> None:
"""T2V (num_frames>1): flow_shift=engine-init (10.0), video tokenization."""
pipeline = make_cosmos3_pipeline()
stage = make_cosmos3_stage(pipeline)
args = make_fastvideo_args()
batch = make_forward_batch(num_frames=5, height=16, width=16)
out = stage.forward(batch, args)
assert float(pipeline.scheduler.config.flow_shift) == 10.0
assert out.output is not None and out.output.dim() == 5
# T2V latent: (5-1)//4 + 1 = 2 frames; 16/8 = 2 latent h/w.
assert tuple(out.latents.shape) == (1, _LATENT_CHANNEL, 2, 2, 2)
def test_stage_mode_dispatch_t2i(make_cosmos3_pipeline, make_cosmos3_stage) -> None:
"""T2I (num_frames==1): flow_shift=3.0; single-frame latent."""
pipeline = make_cosmos3_pipeline()
stage = make_cosmos3_stage(pipeline)
args = make_fastvideo_args()
batch = make_forward_batch(num_frames=1, height=16, width=16, guidance_scale=4.0)
out = stage.forward(batch, args)
assert float(pipeline.scheduler.config.flow_shift) == 3.0
assert tuple(out.latents.shape) == (1, _LATENT_CHANNEL, 1, 2, 2)
def test_stage_i2v_encodes_conditioning_image(make_cosmos3_pipeline, make_cosmos3_stage) -> None:
"""I2V stage: a conditioning image is accepted and decoded to a finite clip."""
pipeline = make_cosmos3_pipeline()
stage = make_cosmos3_stage(pipeline)
args = make_fastvideo_args()
image = torch.zeros(3, 16, 16) # [-1, 1] conditioning frame
batch = make_forward_batch(num_frames=5, height=16, width=16, image=image)
out = stage.forward(batch, args)
# I2V uses the same engine-init flow_shift as T2V (10.0).
assert float(pipeline.scheduler.config.flow_shift) == 10.0
assert out.output is not None and out.output.dim() == 5
assert torch.isfinite(out.output).all()
@@ -0,0 +1,310 @@
# SPDX-License-Identifier: Apache-2.0
"""Cosmos3 pipeline smoke test (CPU / float32, tiny components).
Exercises the native pipeline's runtime path end-to-end on tiny stub-or-real
components, with NO real weights and NO cosmos_framework dependency:
* a stub transformer implementing the native DiT's packed-input ->
``{"preds_vision": [...]}`` contract with bounded, deterministic output
(the REAL DiT forward / unpatchify is exhaustively bit-identical-tested in
``test_cosmos3_dit_parity*`` and ``test_cosmos3_denoise_cfg_parity``; an
untrained real DiT emits unbounded velocities that overflow UniPC, so the
smoke uses a stub to keep finiteness deterministic);
* a stub VAE exposing the ``AutoencoderKLWan`` surface
(``config.latents_mean/std/scale_factor_spatial``, ``encode().mode()``,
``decode()``) used by the encode/normalize + decode/denormalize bridges;
* a stub Qwen2-shaped tokenizer (chat template + special tokens).
Two paths are covered:
1. ``Cosmos3DenoiseEngine.denoise`` for >= 2 UniPC steps over a tiny T2V
latent, asserting a finite final latent of the right shape, plus the VAE
decode + ``(1 + x)/2`` clamp producing a finite ``[B, 3, T, H, W]`` video;
2. the real ``Cosmos3DenoisingStage.forward`` (full tokenize -> noise ->
denoise -> decode wiring) driven through a ``__new__``-built pipeline +
``ForwardBatch``, for both T2V and I2V.
Run:
cd <worktree> && <fv-cosmos3 python> -m pytest \
tests/local_tests/cosmos3/test_cosmos3_pipeline_smoke.py -q
"""
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
import pytest
import torch
from diffusers import UniPCMultistepScheduler
pytestmark = [pytest.mark.local]
_LATENT_CHANNEL = 16
_LATENT_PATCH_SIZE = 2
_SPATIAL_FACTOR = 8
_TEMPORAL_FACTOR = 4
# ---------------------------------------------------------------------------
# Stub transformer implementing the native DiT packed-input contract.
# ---------------------------------------------------------------------------
class StubCosmos3Transformer(torch.nn.Module):
"""Bounded stand-in for ``Cosmos3VFMTransformer.forward``.
Consumes the same packed kwargs (``vision_token_shapes`` /
``vision_noisy_frame_indexes`` / ``vision_tokens`` / ``text_ids``) and
returns ``{"preds_vision": [[1, C, T, H, W], ...]}`` with predictions only on
noisy frames (zeros on conditioning frames), matching the real DiT's
``_unpatchify_and_unpack`` output structure. The prediction is a small
``tanh`` of the input latent, scaled by the first text id so the cond and
uncond passes differ (exercising the CFG combination).
"""
def __init__(self, latent_channel: int = _LATENT_CHANNEL) -> None:
super().__init__()
self.latent_channel = latent_channel
# A real attribute the stage reads for device/dtype.
self.embed_tokens = torch.nn.Embedding(64, 8)
def forward(self, **kwargs: Any) -> dict[str, Any]:
token_ids = kwargs["text_ids"]
token = float(token_ids.reshape(-1)[0].item()) if token_ids.numel() else 1.0
scale = 0.01 * (1.0 + (token % 7))
token_shapes = kwargs["vision_token_shapes"]
noisy = kwargs["vision_noisy_frame_indexes"]
tokens = kwargs["vision_tokens"]
preds: list[torch.Tensor] = []
for latent, (t, _h, _w), nfi in zip(tokens, token_shapes, noisy):
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)) # [1, C, T, H, W]
return {"preds_vision": preds}
# ---------------------------------------------------------------------------
# Stub VAE matching the AutoencoderKLWan surface used by the bridges.
# ---------------------------------------------------------------------------
class _StubLatentDist:
def __init__(self, latents: torch.Tensor) -> None:
self._latents = latents
def mode(self) -> torch.Tensor:
return self._latents
class StubCosmos3VAE:
"""Minimal VAE: deterministic encode/decode shaped by 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
latents = torch.ones(b, self.config.z_dim, lt, lh, lw, dtype=video.dtype, device=video.device)
return _StubLatentDist(latents)
def decode(self, z: torch.Tensor):
# Upsample latents back to pixel dims; clamp like AutoencoderKLWan.
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
# Bounded function of z so the output reflects (and stays finite with)
# the latent: tanh maps any finite z to [-1, 1]; nan_to_num guards
# against non-finite latents from an untrained denoise.
z_signal = torch.nan_to_num(torch.tanh(z[:, :1, :1, :1, :1]))
out = torch.zeros(b, 3, t, h, w, dtype=z.dtype, device=z.device) + z_signal.reshape(b, 1, 1, 1, 1)
return torch.clamp(out, -1.0, 1.0)
# ---------------------------------------------------------------------------
# Stub Qwen2-shaped tokenizer (chat template + special tokens).
# ---------------------------------------------------------------------------
class StubQwen2Tokenizer:
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):
# Deterministic small token ids from the user message length.
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() -> UniPCMultistepScheduler:
return UniPCMultistepScheduler(
num_train_timesteps=1000,
solver_order=2,
prediction_type="flow_prediction",
use_flow_sigmas=True,
flow_shift=10.0,
)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestCosmos3PipelineSmoke:
def test_denoise_engine_and_decode_finite(self):
"""Engine.denoise (>= 2 steps) + VAE decode -> finite output, right shape."""
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import (
Cosmos3DenoiseEngine,
Cosmos3VisionSpec,
_VaeNorm,
cosmos3_vae_decode,
)
dit = StubCosmos3Transformer()
vae = StubCosmos3VAE()
scheduler = _make_scheduler()
scheduler.set_timesteps(2, device=torch.device("cpu"))
latent_shape = (_LATENT_CHANNEL, 2, 2, 2) # [C, T, H, W]
torch.manual_seed(1)
flat = torch.randn(int(torch.tensor(latent_shape).prod()))
engine = Cosmos3DenoiseEngine(
transformer=dit,
scheduler=scheduler,
special_tokens={"start_of_generation": 60, "end_of_generation": 61, "eos_token_id": 62},
latent_patch_size=_LATENT_PATCH_SIZE,
temporal_modality_margin=15_000,
reset_spatial_ids=True,
enable_fps_modulation=False,
base_fps=24.0,
temporal_compression_factor=_TEMPORAL_FACTOR,
)
spec = Cosmos3VisionSpec(shape=latent_shape, condition_frame_indexes=[])
out_flat = engine.denoise(
flat_latent=flat,
timesteps=scheduler.timesteps,
guidance=6.0,
specs=[spec],
cond_token_ids=[10, 11, 12],
uncond_token_ids=[13, 14],
)
assert out_flat.shape == flat.shape
assert torch.isfinite(out_flat).all()
norm = _VaeNorm.from_vae(vae, torch.float32)
result_latent = out_flat.reshape(latent_shape).unsqueeze(0)
decoded = cosmos3_vae_decode(vae, result_latent, norm)
video = ((1.0 + decoded) / 2.0).clamp(0.0, 1.0)
assert video.dim() == 5 and video.shape[1] == 3
assert torch.isfinite(video).all()
assert float(video.min()) >= 0.0 and float(video.max()) <= 1.0
def _make_pipeline(self):
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import 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
return pipe
def _make_args(self):
from fastvideo.configs.pipelines.cosmos3 import Cosmos3Config
cfg = Cosmos3Config()
# Shrink the DiT arch config to the tiny smoke geometry.
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)
def _make_batch(self, *, num_frames, height, width, image=None):
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
return ForwardBatch(
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,
)
def test_stage_forward_t2v_finite(self):
"""The real Cosmos3DenoisingStage.forward runs T2V end-to-end."""
from fastvideo.pipelines.stages.cosmos3_stages import Cosmos3DenoisingStage
pipe = self._make_pipeline()
args = self._make_args()
stage = Cosmos3DenoisingStage(
transformer=pipe.modules["transformer"],
scheduler=pipe.modules["scheduler"],
vae=pipe.modules["vae"],
tokenizer=pipe.modules["text_tokenizer"],
pipeline=pipe,
)
# 5 frames -> latent_t = (5-1)//4 + 1 = 2; 16x16 px -> 2x2 latent.
batch = self._make_batch(num_frames=5, height=16, width=16)
out = stage.forward(batch, args)
assert out.output is not None
assert out.output.dim() == 5 and out.output.shape[1] == 3
assert torch.isfinite(out.output).all()
assert torch.isfinite(out.latents).all()
def test_stage_forward_i2v_keeps_condition_frame(self):
"""I2V: a conditioning image is VAE-encoded and frame 0 stays clean."""
from fastvideo.pipelines.stages.cosmos3_stages import Cosmos3DenoisingStage
pipe = self._make_pipeline()
args = self._make_args()
stage = Cosmos3DenoisingStage(
transformer=pipe.modules["transformer"],
scheduler=pipe.modules["scheduler"],
vae=pipe.modules["vae"],
tokenizer=pipe.modules["text_tokenizer"],
pipeline=pipe,
)
# Conditioning image as a [3, H, W] tensor in [-1, 1].
image = torch.zeros(3, 16, 16)
batch = self._make_batch(num_frames=5, height=16, width=16, image=image)
out = stage.forward(batch, args)
assert out.output is not None
assert out.output.dim() == 5 and out.output.shape[1] == 3
assert torch.isfinite(out.output).all()
def test_tokenize_caption_special_tokens(self):
"""Pipeline.tokenize_caption uses the Qwen2 chat template + ids."""
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import cosmos3_special_tokens
pipe = self._make_pipeline()
ids = pipe.tokenize_caption("hello world", is_video=True, use_system_prompt=False)
assert isinstance(ids, list) and len(ids) > 0
special = cosmos3_special_tokens(pipe.get_module("text_tokenizer"))
assert special == {"start_of_generation": 60, "end_of_generation": 61, "eos_token_id": 62}
@@ -1,91 +1,83 @@
# SPDX-License-Identifier: Apache-2.0
"""Cosmos3 prompt tokenization parity — chat template + special tokens (Tier A).
"""Cosmos3 prompt tokenization contract — chat template + special tokens.
Reference: ``vllm_omni/diffusion/models/cosmos3/pipeline_cosmos3.py``
lines 562-606 (``Cosmos3OmniDiffusersPipeline._tokenize_prompt``).
The native pipeline tokenizes via the module-level helpers
``cosmos3_special_tokens`` / ``cosmos3_tokenize_caption``
(``fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline``), which wrap a Qwen2
chat-template tokenizer:
The Cosmos3 tokenizer is a Qwen2 chat-template tokenizer with two
appended special tokens:
* special tokens: ``start_of_generation=<|vision_start|>``,
``end_of_generation=<|vision_end|>``, ``eos_token_id=tokenizer.eos_token_id``
(the framework ``llm_special_tokens``);
* ``tokenize_caption`` applies the chat template with
``add_generation_prompt=True`` / ``add_vision_id=False`` and an optional
image/video system prompt.
* ``eos_token_id`` == 151645
* ``<|vision_start|>`` == 151652
The reference pipeline:
1. Wraps the prompt in a ``role=user`` conversation (optionally with a
``role=system`` prefix carrying ``COSMOS3_SYSTEM_PROMPT``);
2. Applies the chat template with ``add_generation_prompt=True``;
3. Truncates to ``max_sequence_length``;
4. Appends ``eos`` and ``<|vision_start|>``;
5. Right-pads with ``pad_token_id`` and produces a matching
attention mask of ``[1]*seq_len + [0]*pad_len``.
This Tier A scaffold validates the two special-token IDs and the
right-pad shape contract. Full byte-for-byte parity against an
official Cosmos3 tokenizer requires Phase 2b weights.
These contract checks run against the conftest Qwen2-shaped stub tokenizer (no
real weights). The byte-for-byte real-token-id check (``eos == 151645``,
``<|vision_start|> == 151652``) needs the real ``nvidia/Cosmos3-Nano``
``text_tokenizer`` and is skipped cleanly when it is unavailable.
"""
from __future__ import annotations
import pytest
from .conftest import StubQwen2Tokenizer
pytestmark = [pytest.mark.local]
def test_cosmos3_special_token_ids() -> None:
"""Asserts the two Cosmos3 special-token IDs the pipeline depends on.
def test_native_special_tokens_resolution() -> None:
"""``cosmos3_special_tokens`` resolves the three generation special tokens."""
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import cosmos3_special_tokens
Specifically ``eos_token_id == 151645`` and
``convert_tokens_to_ids('<|vision_start|>') == 151652``. These IDs
are pinned by the Qwen2 base tokenizer and are appended in
``Cosmos3OmniDiffusersPipeline._tokenize_prompt`` at
pipeline_cosmos3.py:596-597.
special = cosmos3_special_tokens(StubQwen2Tokenizer())
assert set(special) == {"start_of_generation", "end_of_generation", "eos_token_id"}
assert special["start_of_generation"] == StubQwen2Tokenizer().convert_tokens_to_ids("<|vision_start|>")
assert special["end_of_generation"] == StubQwen2Tokenizer().convert_tokens_to_ids("<|vision_end|>")
assert special["eos_token_id"] == StubQwen2Tokenizer.eos_token_id
def test_native_tokenize_caption_uses_chat_template() -> None:
"""``cosmos3_tokenize_caption`` returns a non-empty token-id list.
The video / image system-prompt variants tokenize independently (the chat
template prepends a role=system turn when ``use_system_prompt`` is set), and
the result is always a plain list of ints.
"""
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import cosmos3_tokenize_caption
tok = StubQwen2Tokenizer()
ids_video = cosmos3_tokenize_caption(tok, "a robot dances", is_video=True, use_system_prompt=False)
ids_image = cosmos3_tokenize_caption(tok, "a robot", is_video=False, use_system_prompt=True)
assert isinstance(ids_video, list) and all(isinstance(i, int) for i in ids_video) and ids_video
assert isinstance(ids_image, list) and ids_image
def test_cosmos3_special_token_ids_real_weights() -> None:
"""Byte-for-byte Qwen2 special-token ids (needs the real text_tokenizer).
Asserts ``eos_token_id == 151645`` and
``convert_tokens_to_ids('<|vision_start|>') == 151652`` on the real
``nvidia/Cosmos3-Nano`` Qwen2 tokenizer. Skipped cleanly when the real
tokenizer is not loadable in this environment.
"""
try:
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import ( # type: ignore
Cosmos3OmniDiffusersPipeline,
)
from transformers import AutoTokenizer
except ImportError:
pytest.skip("FastVideo Cosmos3 pipeline not yet implemented (Phase 2b)")
pytest.skip("transformers not available")
pipeline = Cosmos3OmniDiffusersPipeline.__new__(Cosmos3OmniDiffusersPipeline)
tokenizer = getattr(pipeline, "tokenizer", None)
if tokenizer is None:
pytest.skip("Cosmos3 tokenizer instance not yet wired on the pipeline")
import os
candidate_paths = [
os.path.join(os.environ.get("COSMOS3_WEIGHTS_DIR", ""), "text_tokenizer"),
"official_weights/cosmos3/text_tokenizer",
]
tok_path = next((p for p in candidate_paths if p and os.path.isdir(p)), None)
if tok_path is None:
pytest.skip("real nvidia/Cosmos3-Nano text_tokenizer not available "
"(set COSMOS3_WEIGHTS_DIR or provide official_weights/cosmos3)")
tokenizer = AutoTokenizer.from_pretrained(tok_path)
assert tokenizer.eos_token_id == 151645
assert tokenizer.convert_tokens_to_ids("<|vision_start|>") == 151652
def test_cosmos3_chat_template_shape_contract() -> None:
"""Asserts the right-pad shape contract of ``_tokenize_prompt``.
For ``max_sequence_length = N``, the returned ``input_ids`` and
``attention_mask`` must each be ``[1, N]``, with the first
``seq_len`` mask entries equal to 1 and the remainder equal to 0.
Once Phase 2b lands, this test should:
* call ``_tokenize_prompt`` directly with a small max_seq_length;
* assert ``input_ids.shape == (1, max_seq_length)``;
* assert ``attention_mask.sum().item() == seq_len`` and that the
last two non-pad tokens are ``[eos, vision_start]``.
"""
try:
from fastvideo.pipelines.basic.cosmos3.cosmos3_pipeline import ( # type: ignore
Cosmos3OmniDiffusersPipeline,
)
except ImportError:
pytest.skip("FastVideo Cosmos3 pipeline not yet implemented (Phase 2b)")
pipeline = Cosmos3OmniDiffusersPipeline.__new__(Cosmos3OmniDiffusersPipeline)
if not hasattr(pipeline, "_tokenize_prompt"):
pytest.skip("_tokenize_prompt not yet wired on the FastVideo pipeline")
input_ids, attention_mask = pipeline._tokenize_prompt(
"A robot.", max_sequence_length=32, use_system_prompt=False
)
assert tuple(input_ids.shape) == (1, 32)
assert tuple(attention_mask.shape) == (1, 32)
seq_len = int(attention_mask.sum().item())
assert attention_mask[0, :seq_len].sum().item() == seq_len
assert attention_mask[0, seq_len:].sum().item() == 0
assert int(input_ids[0, seq_len - 2].item()) == 151645
assert int(input_ids[0, seq_len - 1].item()) == 151652