481 lines
18 KiB
Python
481 lines
18 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""
|
|
Numerical-parity reference test for the official Cosmos3 DiT (Cosmos3VFMNetwork).
|
|
|
|
Runs a tiny deterministic forward of the OFFICIAL framework model on CPU / float32
|
|
using a torch SDPA monkey-patch (flash2/flash3/natten are CUDA-only; SDPA works on CPU).
|
|
The test exercises the full forward contract:
|
|
packed_seq -> vfm(packed_seq) -> {last_hidden_state, preds_vision}
|
|
and is used as the "ground truth" side of any FastVideo parity check.
|
|
|
|
Environment requirements
|
|
------------------------
|
|
- cosmos_framework must be installed (editable) in the active interpreter.
|
|
The canonical env is: /home/william5lin/miniconda3/envs/fv-cosmos3/bin/python
|
|
- No transformer_engine / natten / GPU required.
|
|
- PYTHONSAFEPATH=1 is recommended to avoid cwd import shadowing.
|
|
|
|
Run:
|
|
PYTHONSAFEPATH=1 pytest fastvideo/tests/layers/test_cosmos3_reference_forward.py -v
|
|
"""
|
|
|
|
import math
|
|
import sys
|
|
|
|
import pytest
|
|
import torch
|
|
import torch.nn.functional as F
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Skip guard: cosmos_framework may not be installed in the default dev env.
|
|
# ---------------------------------------------------------------------------
|
|
cosmos_framework = pytest.importorskip(
|
|
"cosmos_framework",
|
|
reason="cosmos_framework not installed; run in fv-cosmos3 env.",
|
|
)
|
|
|
|
pytestmark = [pytest.mark.local]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SDPA attention monkey-patch
|
|
# ---------------------------------------------------------------------------
|
|
# The imaginaire attention backend (flash2/flash3) requires CUDA *and*
|
|
# float16/bfloat16. For CPU/float32 parity testing we replace it with a
|
|
# simple SDPA implementation that handles both standard and varlen packed
|
|
# formats (cumulative_seqlen_{Q,KV}).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _sdpa_attention(
|
|
query,
|
|
key,
|
|
value,
|
|
is_causal=False,
|
|
causal_type=None,
|
|
scale=None,
|
|
seqlens_Q=None,
|
|
seqlens_KV=None,
|
|
cumulative_seqlen_Q=None,
|
|
cumulative_seqlen_KV=None,
|
|
max_seqlen_Q=None,
|
|
max_seqlen_KV=None,
|
|
backend=None,
|
|
return_lse=False,
|
|
backend_kwargs=None,
|
|
deterministic=False,
|
|
):
|
|
"""Minimal SDPA wrapper that mirrors the imaginaire attention signature."""
|
|
B, Sq, H, D = query.shape
|
|
Hkv = key.shape[2]
|
|
attn_scale = scale if scale is not None else D**-0.5
|
|
|
|
if cumulative_seqlen_Q is not None:
|
|
# Varlen packed layout: B==1, tokens from different samples are concatenated.
|
|
oq = cumulative_seqlen_Q.cpu().tolist()
|
|
okv = cumulative_seqlen_KV.cpu().tolist()
|
|
outs = []
|
|
for i in range(len(oq) - 1):
|
|
qi = query[0, oq[i] : oq[i + 1]].unsqueeze(0).permute(0, 2, 1, 3) # [1,H,S,D]
|
|
ki = key[0, okv[i] : okv[i + 1]].unsqueeze(0).permute(0, 2, 1, 3)
|
|
vi = value[0, okv[i] : okv[i + 1]].unsqueeze(0).permute(0, 2, 1, 3)
|
|
if Hkv != H:
|
|
ki = ki.repeat_interleave(H // Hkv, dim=1)
|
|
vi = vi.repeat_interleave(H // Hkv, dim=1)
|
|
oi = F.scaled_dot_product_attention(qi, ki, vi, scale=attn_scale, is_causal=is_causal)
|
|
outs.append(oi.permute(0, 2, 1, 3)) # [1,S,H,D]
|
|
out = torch.cat(outs, dim=1) # [1,S_total,H,D]
|
|
else:
|
|
q = query.permute(0, 2, 1, 3)
|
|
k = key.permute(0, 2, 1, 3)
|
|
v = value.permute(0, 2, 1, 3)
|
|
if Hkv != H:
|
|
k = k.repeat_interleave(H // Hkv, dim=1)
|
|
v = v.repeat_interleave(H // Hkv, dim=1)
|
|
out = F.scaled_dot_product_attention(q, k, v, scale=attn_scale, is_causal=is_causal)
|
|
out = out.permute(0, 2, 1, 3) # [B,S,H,D]
|
|
|
|
if return_lse:
|
|
lse = torch.zeros(B, Sq, H, 1, dtype=query.dtype, device=query.device)
|
|
return out, lse
|
|
return out
|
|
|
|
|
|
def _sdpa_merge_attentions(outputs, lse_tensors, torch_compile=False):
|
|
"""Log-sum-exp weighted merge of two attention outputs."""
|
|
if len(outputs) == 1:
|
|
return outputs[0], lse_tensors[0]
|
|
o1, lse1 = outputs[0], lse_tensors[0]
|
|
o2, lse2 = outputs[1], lse_tensors[1]
|
|
m = torch.maximum(lse1, lse2)
|
|
w1 = torch.exp(lse1 - m)
|
|
w2 = torch.exp(lse2 - m)
|
|
ws = w1 + w2
|
|
return (o1 * w1 + o2 * w2) / ws, m + torch.log(ws)
|
|
|
|
|
|
def _apply_sdpa_patches():
|
|
"""Monkey-patch every attention reference in cosmos_framework to use SDPA."""
|
|
import cosmos_framework.model.attention as attn_pkg
|
|
import cosmos_framework.model.attention.frontend as attn_frontend
|
|
import cosmos_framework.model.vfm.mot.attention as vfm_attn
|
|
import cosmos_framework.model.vfm.mot.unified_mot as mot_module
|
|
|
|
attn_frontend.attention = _sdpa_attention
|
|
attn_pkg.attention = _sdpa_attention
|
|
mot_module.imaginaire_attention = _sdpa_attention
|
|
vfm_attn.attention = _sdpa_attention
|
|
|
|
attn_frontend.merge_attentions = _sdpa_merge_attentions
|
|
attn_pkg.merge_attentions = _sdpa_merge_attentions
|
|
vfm_attn.merge_attentions = _sdpa_merge_attentions
|
|
|
|
|
|
# Apply patches at import time (before any model is constructed).
|
|
_apply_sdpa_patches()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _build_tiny_cosmos3(seed: int = 42):
|
|
"""Construct a tiny Cosmos3VFMNetwork on CPU / float32.
|
|
|
|
Architecture:
|
|
hidden_size=16, intermediate_size=32, num_hidden_layers=1,
|
|
num_attention_heads=2, num_key_value_heads=2, head_dim=8,
|
|
vocab_size=64, latent_channel_size=16, latent_patch_size=2,
|
|
max_latent_{h,w,t}=8,8,4
|
|
"""
|
|
from cosmos_framework.model.vfm.mot.cosmos3_vfm_network import (
|
|
Cosmos3VFMNetwork,
|
|
Cosmos3VFMNetworkConfig,
|
|
)
|
|
from cosmos_framework.model.vfm.mot.unified_mot import Qwen3MoTConfig, Qwen3VLTextForCausalLM
|
|
from cosmos_framework.model.vfm.vlm.qwen3_vl.configuration_qwen3_vl import Qwen3VLConfig
|
|
|
|
TINY_TEXT_DICT = dict(
|
|
model_type="qwen3_vl_text",
|
|
vocab_size=64,
|
|
hidden_size=16,
|
|
intermediate_size=32,
|
|
num_hidden_layers=1,
|
|
num_attention_heads=2,
|
|
num_key_value_heads=2,
|
|
head_dim=8,
|
|
rms_norm_eps=1e-6,
|
|
attention_bias=False,
|
|
attention_dropout=0.0,
|
|
)
|
|
mot_cfg = Qwen3MoTConfig(
|
|
config_dict=TINY_TEXT_DICT,
|
|
qk_norm_for_text=True,
|
|
qk_norm_for_diffusion=True,
|
|
include_visual=False,
|
|
)
|
|
tiny_vlm_cfg = Qwen3VLConfig(text_config=TINY_TEXT_DICT)
|
|
vfm_cfg = Cosmos3VFMNetworkConfig(
|
|
vision_gen=True,
|
|
vlm_config=tiny_vlm_cfg,
|
|
latent_patch_size=2,
|
|
latent_downsample_factor=8,
|
|
latent_channel_size=16,
|
|
position_embedding_type="3d_rope",
|
|
max_latent_h=8,
|
|
max_latent_w=8,
|
|
max_latent_t=4,
|
|
)
|
|
torch.manual_seed(seed)
|
|
lm = Qwen3VLTextForCausalLM(config=mot_cfg)
|
|
vfm = Cosmos3VFMNetwork(language_model=lm, config=vfm_cfg)
|
|
vfm.eval()
|
|
return vfm
|
|
|
|
|
|
def _build_tiny_packed_seq(*, n_text: int = 4, seed: int = 7):
|
|
"""Build a minimal PackedSequence: 4 text tokens + 1 vision patch.
|
|
|
|
Vision: C=16, T=1, H=2, W=2 → after patch_size=2: 1*1*1 = 1 patch.
|
|
All vision frames are noisy (timestep=500).
|
|
"""
|
|
from cosmos_framework.data.vfm.sequence_packing import ModalityData, PackedSequence
|
|
|
|
torch.manual_seed(seed)
|
|
vision_tensor = torch.randn(16, 1, 2, 2) # [C=16, T=1, H=2, W=2]
|
|
text_ids = torch.randint(0, 64, (n_text,))
|
|
n_vision = 1 # 1 patch after patchify
|
|
total_len = n_text + n_vision
|
|
|
|
vision_mod = ModalityData(
|
|
sequence_indexes=torch.arange(n_text, total_len, dtype=torch.long),
|
|
timesteps=torch.tensor([500.0]), # one noisy frame
|
|
mse_loss_indexes=torch.arange(n_text, total_len, dtype=torch.long),
|
|
token_shapes=[(1, 1, 1)], # (t_patches, h_patches, w_patches) = (1,1,1)
|
|
tokens=[vision_tensor],
|
|
condition_mask=[torch.zeros(1, dtype=torch.long)], # 0=noisy
|
|
noisy_frame_indexes=[torch.tensor([0])],
|
|
)
|
|
packed_seq = PackedSequence(
|
|
sample_lens=[total_len],
|
|
split_lens=[n_text, n_vision],
|
|
attn_modes=["causal", "full"],
|
|
is_image_batch=True,
|
|
sequence_length=total_len,
|
|
text_ids=text_ids,
|
|
text_indexes=torch.arange(n_text, dtype=torch.long),
|
|
position_ids=torch.arange(total_len),
|
|
vision=vision_mod,
|
|
)
|
|
return packed_seq
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCosmos3ReferenceConfig:
|
|
"""Step 1: verify config construction and field enumeration."""
|
|
|
|
def test_qwen3vl_text_config_fields(self):
|
|
from cosmos_framework.model.vfm.vlm.qwen3_vl.configuration_qwen3_vl import Qwen3VLTextConfig
|
|
|
|
cfg = Qwen3VLTextConfig(
|
|
vocab_size=64,
|
|
hidden_size=16,
|
|
intermediate_size=32,
|
|
num_hidden_layers=1,
|
|
num_attention_heads=2,
|
|
num_key_value_heads=2,
|
|
head_dim=8,
|
|
)
|
|
assert cfg.vocab_size == 64
|
|
assert cfg.hidden_size == 16
|
|
assert cfg.num_attention_heads == 2
|
|
assert cfg.num_key_value_heads == 2
|
|
assert cfg.head_dim == 8
|
|
|
|
def test_cosmos3_vfm_network_config(self):
|
|
from cosmos_framework.model.vfm.mot.cosmos3_vfm_network import Cosmos3VFMNetworkConfig
|
|
from cosmos_framework.model.vfm.vlm.qwen3_vl.configuration_qwen3_vl import Qwen3VLConfig
|
|
|
|
tiny_vlm = Qwen3VLConfig(text_config=dict(vocab_size=64, hidden_size=16))
|
|
cfg = Cosmos3VFMNetworkConfig(
|
|
vision_gen=True,
|
|
vlm_config=tiny_vlm,
|
|
latent_patch_size=2,
|
|
latent_downsample_factor=8,
|
|
latent_channel_size=16,
|
|
position_embedding_type="3d_rope",
|
|
max_latent_h=8,
|
|
max_latent_w=8,
|
|
max_latent_t=4,
|
|
)
|
|
assert cfg.vision_gen is True
|
|
assert cfg.latent_patch_size == 2
|
|
assert cfg.latent_channel_size == 16
|
|
|
|
|
|
class TestCosmos3ReferenceInstantiation:
|
|
"""Step 2: verify parameter key pattern and module tree."""
|
|
|
|
@pytest.fixture(scope="class")
|
|
def tiny_vfm(self):
|
|
return _build_tiny_cosmos3(seed=42)
|
|
|
|
def test_instantiation_succeeds(self, tiny_vfm):
|
|
assert tiny_vfm is not None
|
|
|
|
def test_param_key_pattern_attention(self, tiny_vfm):
|
|
"""Verify understanding and generation attention projections exist."""
|
|
param_keys = {n for n, _ in tiny_vfm.named_parameters()}
|
|
layer = "language_model.model.layers.0.self_attn"
|
|
# Understanding pathway
|
|
for proj in ("q_proj", "k_proj", "v_proj", "o_proj"):
|
|
assert f"{layer}.{proj}.weight" in param_keys, f"Missing {layer}.{proj}.weight"
|
|
# Generation pathway (moe_gen suffix)
|
|
for proj in ("q_proj_moe_gen", "k_proj_moe_gen", "v_proj_moe_gen", "o_proj_moe_gen"):
|
|
assert f"{layer}.{proj}.weight" in param_keys, f"Missing {layer}.{proj}.weight"
|
|
# QK norms
|
|
for norm in ("q_norm", "k_norm", "q_norm_moe_gen", "k_norm_moe_gen"):
|
|
assert f"{layer}.{norm}.weight" in param_keys, f"Missing {layer}.{norm}.weight"
|
|
|
|
def test_param_key_pattern_mlp(self, tiny_vfm):
|
|
param_keys = {n for n, _ in tiny_vfm.named_parameters()}
|
|
for pathway in ("mlp", "mlp_moe_gen"):
|
|
base = f"language_model.model.layers.0.{pathway}"
|
|
for proj in ("gate_proj", "up_proj", "down_proj"):
|
|
assert f"{base}.{proj}.weight" in param_keys
|
|
|
|
def test_param_key_pattern_layernorms(self, tiny_vfm):
|
|
param_keys = {n for n, _ in tiny_vfm.named_parameters()}
|
|
layer = "language_model.model.layers.0"
|
|
for ln in (
|
|
"input_layernorm",
|
|
"input_layernorm_moe_gen",
|
|
"post_attention_layernorm",
|
|
"post_attention_layernorm_moe_gen",
|
|
):
|
|
assert f"{layer}.{ln}.weight" in param_keys
|
|
|
|
def test_param_key_pattern_toplevel(self, tiny_vfm):
|
|
param_keys = {n for n, _ in tiny_vfm.named_parameters()}
|
|
# LM submodules
|
|
assert "language_model.model.embed_tokens.weight" in param_keys
|
|
assert "language_model.model.norm.weight" in param_keys
|
|
assert "language_model.model.norm_moe_gen.weight" in param_keys
|
|
assert "language_model.lm_head.weight" in param_keys
|
|
# VFM vision head
|
|
assert "vae2llm.weight" in param_keys
|
|
assert "vae2llm.bias" in param_keys
|
|
assert "llm2vae.weight" in param_keys
|
|
assert "llm2vae.bias" in param_keys
|
|
# Timestep embedder
|
|
assert "time_embedder.mlp.0.weight" in param_keys
|
|
assert "time_embedder.mlp.2.weight" in param_keys
|
|
|
|
def test_expected_param_count(self, tiny_vfm):
|
|
"""Sanity-check total param count for the tiny model."""
|
|
n_params = sum(p.numel() for p in tiny_vfm.parameters())
|
|
# Rough bound: tiny model should be under 50 K params
|
|
assert n_params < 50_000, f"Unexpected param count: {n_params}"
|
|
|
|
def test_dtype_is_float32(self, tiny_vfm):
|
|
for name, p in tiny_vfm.named_parameters():
|
|
if "inv_freq" in name:
|
|
continue # inv_freq stays float32 always
|
|
assert p.dtype == torch.float32, f"{name} has dtype {p.dtype}"
|
|
|
|
|
|
class TestCosmos3ReferenceForward:
|
|
"""Step 3 + 4: verify forward contract and determinism."""
|
|
|
|
@pytest.fixture(scope="class")
|
|
def tiny_vfm(self):
|
|
return _build_tiny_cosmos3(seed=42)
|
|
|
|
@pytest.fixture(scope="class")
|
|
def packed_seq(self):
|
|
return _build_tiny_packed_seq(n_text=4, seed=7)
|
|
|
|
@pytest.fixture(scope="class")
|
|
def fwd_output(self, tiny_vfm, packed_seq):
|
|
with torch.no_grad():
|
|
return tiny_vfm(packed_seq=packed_seq)
|
|
|
|
def test_forward_returns_dict(self, fwd_output):
|
|
assert isinstance(fwd_output, dict)
|
|
|
|
def test_last_hidden_state_shape(self, fwd_output):
|
|
# 4 text + 1 vision patch = 5 total tokens; hidden_size=16
|
|
lhs = fwd_output["last_hidden_state"]
|
|
assert lhs.shape == torch.Size([5, 16]), f"Got {lhs.shape}"
|
|
|
|
def test_last_hidden_state_finite(self, fwd_output):
|
|
assert torch.isfinite(fwd_output["last_hidden_state"]).all()
|
|
|
|
def test_preds_vision_present(self, fwd_output):
|
|
assert "preds_vision" in fwd_output
|
|
|
|
def test_preds_vision_shape(self, fwd_output):
|
|
# latent_channel=16, T=1, H=2, W=2 → [1, 16, 1, 2, 2]
|
|
pv = fwd_output["preds_vision"][0]
|
|
assert pv.shape == torch.Size([1, 16, 1, 2, 2]), f"Got {pv.shape}"
|
|
|
|
def test_preds_vision_finite(self, fwd_output):
|
|
assert torch.isfinite(fwd_output["preds_vision"][0]).all()
|
|
|
|
def test_forward_deterministic_same_seed(self):
|
|
"""Two models with identical seed should produce identical output."""
|
|
ps = _build_tiny_packed_seq(n_text=4, seed=7)
|
|
vfm1 = _build_tiny_cosmos3(seed=42)
|
|
vfm2 = _build_tiny_cosmos3(seed=42)
|
|
with torch.no_grad():
|
|
out1 = vfm1(packed_seq=ps)
|
|
out2 = vfm2(packed_seq=ps)
|
|
assert torch.allclose(out1["last_hidden_state"], out2["last_hidden_state"])
|
|
assert torch.allclose(out1["preds_vision"][0], out2["preds_vision"][0])
|
|
|
|
def test_forward_repeatable_same_model(self, tiny_vfm, packed_seq):
|
|
"""Same model, same input → identical output on two calls."""
|
|
with torch.no_grad():
|
|
out1 = tiny_vfm(packed_seq=packed_seq)
|
|
out2 = tiny_vfm(packed_seq=packed_seq)
|
|
assert torch.allclose(out1["last_hidden_state"], out2["last_hidden_state"])
|
|
|
|
def test_different_seed_gives_different_output(self):
|
|
"""Different model seeds should give different outputs."""
|
|
ps = _build_tiny_packed_seq(n_text=4, seed=7)
|
|
vfm1 = _build_tiny_cosmos3(seed=42)
|
|
vfm2 = _build_tiny_cosmos3(seed=99)
|
|
with torch.no_grad():
|
|
out1 = vfm1(packed_seq=ps)
|
|
out2 = vfm2(packed_seq=ps)
|
|
# With very high probability random init → different outputs
|
|
assert not torch.allclose(out1["last_hidden_state"], out2["last_hidden_state"])
|
|
|
|
def test_float32_dtype_preserved(self, fwd_output):
|
|
assert fwd_output["last_hidden_state"].dtype == torch.float32
|
|
|
|
def test_cpu_device(self, fwd_output):
|
|
assert fwd_output["last_hidden_state"].device.type == "cpu"
|
|
|
|
|
|
class TestCosmos3ReasonerForward:
|
|
"""Optional: reasoner (und-only) pathway via standard [B,T,H] layout."""
|
|
|
|
def test_reasoner_forward_shape(self):
|
|
"""reasoner_forward runs the und tower only; no PackedSequence needed."""
|
|
vfm = _build_tiny_cosmos3(seed=42)
|
|
lm = vfm.language_model
|
|
input_ids = torch.randint(0, 64, (1, 6))
|
|
with torch.no_grad():
|
|
out = lm.model.reasoner_forward(input_ids=input_ids, cache=None)
|
|
# [B=1, T=6, hidden_size=16]
|
|
assert out.shape == torch.Size([1, 6, 16])
|
|
|
|
def test_reasoner_forward_finite(self):
|
|
vfm = _build_tiny_cosmos3(seed=42)
|
|
lm = vfm.language_model
|
|
input_ids = torch.randint(0, 64, (1, 6))
|
|
with torch.no_grad():
|
|
out = lm.model.reasoner_forward(input_ids=input_ids, cache=None)
|
|
assert torch.isfinite(out).all()
|
|
|
|
def test_reasoner_forward_deterministic(self):
|
|
vfm = _build_tiny_cosmos3(seed=42)
|
|
lm = vfm.language_model
|
|
input_ids = torch.randint(0, 64, (1, 6))
|
|
with torch.no_grad():
|
|
out1 = lm.model.reasoner_forward(input_ids=input_ids, cache=None)
|
|
out2 = lm.model.reasoner_forward(input_ids=input_ids, cache=None)
|
|
assert torch.allclose(out1, out2)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Convenience: print a concise summary when run directly.
|
|
# ---------------------------------------------------------------------------
|
|
if __name__ == "__main__":
|
|
print("Building tiny Cosmos3VFMNetwork...")
|
|
vfm = _build_tiny_cosmos3(seed=42)
|
|
|
|
print("\nParameter keys:")
|
|
for name, p in sorted(vfm.named_parameters()):
|
|
print(f" {name}: {tuple(p.shape)}")
|
|
|
|
print("\nRunning forward...")
|
|
ps = _build_tiny_packed_seq(n_text=4, seed=7)
|
|
with torch.no_grad():
|
|
out = vfm(packed_seq=ps)
|
|
|
|
lhs = out["last_hidden_state"]
|
|
pv = out["preds_vision"][0]
|
|
print(f"\nlast_hidden_state: {lhs.shape}, finite={torch.isfinite(lhs).all().item()}, mean={lhs.mean():.4f}")
|
|
print(f"preds_vision[0]: {pv.shape}, finite={torch.isfinite(pv).all().item()}, mean={pv.mean():.4f}")
|
|
|
|
# Determinism
|
|
with torch.no_grad():
|
|
out2 = vfm(packed_seq=ps)
|
|
print(f"\nDeterministic repeat: {torch.allclose(lhs, out2['last_hidden_state'])}")
|