Files
hao-ai-lab-FastVideo/scripts/verify_anyflow_fastvideo_parity.py
T

397 lines
16 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""FastVideo↔AnyFlow numerical parity verification.
Two checks, both on a single H200:
(A) Forward parity — load nvidia/AnyFlow-Wan2.1-T2V-1.3B-Diffusers into
FastVideo's WanTransformer3DModel (with r_embedder enabled +
param_names_mapping handling the delta_embedder rename), forward
it on identical inputs against AnyFlow's reference loader, and
compare. Expectation: rel-mean diff < 10% in bf16 (bf16 kernel noise).
(B) Any-step end-to-end sampling — run the new
FlowMapEulerDiscreteScheduler through 4 Euler-flow steps on the
same loaded weights, confirm the final latent is finite and
well-scaled.
Configure local checkout paths via env vars (defaults assume a sibling
layout next to this repo):
ANYFLOW_LOCAL — path to ``nvidia/AnyFlow-Wan2.1-T2V-1.3B-Diffusers``
(default ``./anyflow-1.3b``)
ANYFLOW_REF — path to the NVlabs/AnyFlow reference repo, used to
import its loader (default ``./anyflow-ref``)
Run via::
PYTHONPATH=$PWD python scripts/verify_anyflow_fastvideo_parity.py
"""
from __future__ import annotations
import os
import re
import sys
import time
from pathlib import Path
import torch
from safetensors.torch import load_file
ANYFLOW_LOCAL = Path(os.environ.get("ANYFLOW_LOCAL", "./anyflow-1.3b")).expanduser()
ANYFLOW_REF = Path(os.environ.get("ANYFLOW_REF", "./anyflow-ref")).expanduser()
sys.path.insert(0, str(ANYFLOW_REF))
SEED = 1234
DEVICE = torch.device("cuda")
DTYPE = torch.bfloat16
def banner(msg: str) -> None:
print("\n" + "=" * 80)
print(msg)
print("=" * 80)
# ---------------------------------------------------------------------------
# Distributed bootstrap (single-rank). Required by WanTransformer3DModel
# which calls get_sp_world_size() and uses ReplicatedLinear.
# ---------------------------------------------------------------------------
def init_single_rank() -> None:
os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
os.environ.setdefault("MASTER_PORT", "29551")
os.environ.setdefault("WORLD_SIZE", "1")
os.environ.setdefault("RANK", "0")
os.environ.setdefault("LOCAL_RANK", "0")
from fastvideo.distributed import (
init_distributed_environment,
initialize_model_parallel,
)
init_distributed_environment(world_size=1, rank=0, local_rank=0, backend="nccl")
initialize_model_parallel(
tensor_model_parallel_size=1,
sequence_model_parallel_size=1,
data_parallel_size=1,
)
print(" single-rank distributed environment + TP/SP/DP groups initialized")
# ---------------------------------------------------------------------------
# Translate AnyFlow HF safetensor keys onto FastVideo's WanTransformer3DModel
# internal layout, applying the regex from WanVideoArchConfig.param_names_mapping.
# ---------------------------------------------------------------------------
def translate_keys(
raw: dict[str, torch.Tensor],
*,
mapping: dict[str, str],
) -> dict[str, torch.Tensor]:
out: dict[str, torch.Tensor] = {}
for k, v in raw.items():
new_k = k
for pat, repl in mapping.items():
new_k = re.sub(pat, repl, new_k)
out[new_k] = v
return out
# ---------------------------------------------------------------------------
# Build FastVideo WanTransformer3DModel with AnyFlow weights loaded.
# ---------------------------------------------------------------------------
def build_fastvideo_transformer():
banner("(1) Build FastVideo WanTransformer3DModel + load AnyFlow weights")
from fastvideo.configs.models.dits import WanVideoConfig
from fastvideo.models.dits.wanvideo import WanTransformer3DModel
cfg = WanVideoConfig()
arch = cfg.arch_config
# AnyFlow Wan2.1-T2V-1.3B arch dims.
arch.num_attention_heads = 12
arch.attention_head_dim = 128
arch.num_layers = 30
arch.ffn_dim = 8960
# AnyFlow dual-timestep.
arch.r_embedder = True
arch.r_embedder_fusion = "gated"
arch.r_embedder_gate_value = 0.25
arch.r_embedder_deltatime_type = "r"
arch.__post_init__()
# WanTransformer3DModel takes (config, hf_config). hf_config is
# diffusers-style — we provide a minimal dict; only fields the model
# actually reads matter.
hf_config: dict = {}
t0 = time.time()
model = WanTransformer3DModel(config=cfg, hf_config=hf_config)
model = model.to(DEVICE, dtype=DTYPE).eval()
print(f" Wan transformer built in {time.time() - t0:.1f}s; "
f"params: {sum(p.numel() for p in model.parameters())/1e9:.2f}B")
# Load AnyFlow checkpoint.
af_path = ANYFLOW_LOCAL / "transformer" / "diffusion_pytorch_model.safetensors"
af_raw = load_file(str(af_path), device="cpu")
print(f" AnyFlow checkpoint: {len(af_raw)} tensors")
translated = translate_keys(af_raw, mapping=arch.param_names_mapping)
info = model.load_state_dict(translated, strict=False)
miss, unex = info.missing_keys, info.unexpected_keys
print(f" missing_keys : {len(miss)} (first 5: {miss[:5]})")
print(f" unexpected_keys : {len(unex)} (first 5: {unex[:5]})")
return model, len(miss), len(unex)
# ---------------------------------------------------------------------------
# Build AnyFlow reference net.
# ---------------------------------------------------------------------------
def build_anyflow_reference():
banner("(2) Build AnyFlow reference loader")
from far.models import build_model
af_net = build_model("FAR_Wan_Transformer3DModel").from_pretrained(
str(ANYFLOW_LOCAL),
subfolder="transformer",
chunk_partition=None,
full_chunk_limit=0,
compressed_patch_size=[1, 4, 4],
).to(DEVICE, dtype=DTYPE).eval()
print(f" AnyFlow {type(af_net).__name__} ready")
return af_net
# ---------------------------------------------------------------------------
# Forward parity test.
# ---------------------------------------------------------------------------
def forward_compare(fv_model, af_net):
banner("(3) Forward output comparison")
B, C, F, H, W = 1, 16, 21, 60, 104
SEQ, DIM = 32, 4096
g = torch.Generator(device=DEVICE).manual_seed(SEED)
x = torch.randn(B, C, F, H, W, device=DEVICE, dtype=DTYPE, generator=g)
enc = torch.randn(B, SEQ, DIM, device=DEVICE, dtype=DTYPE, generator=g)
# AnyFlow expects per-frame (t, r) [B, F]; FastVideo T2V expects a
# single scalar per sample [B] (timestep.dim()==2 is reserved for
# Wan2.2 ti2v's per-token timestep schedule). For shared-t T2V the two
# are semantically equivalent.
t_per_frame = torch.full((B, F), 500.0, device=DEVICE, dtype=DTYPE)
r_per_frame = torch.full((B, F), 200.0, device=DEVICE, dtype=DTYPE)
t_per_sample = torch.full((B, ), 500.0, device=DEVICE, dtype=DTYPE)
r_per_sample = torch.full((B, ), 200.0, device=DEVICE, dtype=DTYPE)
with torch.no_grad():
# AnyFlow native: takes [B, F, C, H, W] + [B, F] (t, r).
x_af = x.permute(0, 2, 1, 3, 4).contiguous()
af_out = af_net(
x_af,
timestep=t_per_frame,
r_timestep=r_per_frame,
encoder_hidden_states=enc,
return_dict=False,
is_causal=False,
)[0]
if af_out.shape[1] != C and af_out.shape[2] == C:
af_out = af_out.permute(0, 2, 1, 3, 4).contiguous()
# FastVideo: [B, C, F, H, W] + [B] (t, r). Must wrap in
# set_forward_context so the attention layer can pick up the
# current timestep / attn_metadata.
from fastvideo.forward_context import set_forward_context
with set_forward_context(
current_timestep=t_per_sample,
attn_metadata=None,
):
fv_out = fv_model(
hidden_states=x,
encoder_hidden_states=enc,
timestep=t_per_sample,
r_timestep=r_per_sample,
)
print(f" AnyFlow out: shape={tuple(af_out.shape)} dtype={af_out.dtype}")
print(f" FastVideo : shape={tuple(fv_out.shape)} dtype={fv_out.dtype}")
if af_out.shape != fv_out.shape:
print(" ❌ shape mismatch")
return False
diff = (af_out.float() - fv_out.float()).abs()
ref = af_out.float().abs().mean().item() + 1e-12
print(f" max abs diff : {diff.max().item():.3e}")
print(f" mean abs diff: {diff.mean().item():.3e}")
print(f" rel mean diff: {diff.mean().item() / ref:.3e}")
ok = diff.mean().item() / ref < 0.10
print(f" >>> {'PASS' if ok else 'FAIL'} (target rel diff < 10%, bf16 noise)")
return ok
# ---------------------------------------------------------------------------
# Any-step sampling smoke via FlowMapEulerDiscreteScheduler.
# ---------------------------------------------------------------------------
def sample_anystep(fv_model):
banner("(4) Any-step 4-step Euler-flow sampling")
from fastvideo.models.schedulers.scheduling_flow_map_euler_discrete import (
FlowMapEulerDiscreteScheduler, )
scheduler = FlowMapEulerDiscreteScheduler(num_train_timesteps=1000, shift=5.0)
scheduler.set_timesteps(num_inference_steps=4, device=DEVICE)
timesteps = scheduler.timesteps.to(dtype=DTYPE)
B, C, F, H, W = 1, 16, 21, 60, 104
SEQ, DIM = 32, 4096
g = torch.Generator(device=DEVICE).manual_seed(SEED)
x = torch.randn(B, C, F, H, W, device=DEVICE, dtype=DTYPE, generator=g)
enc = torch.randn(B, SEQ, DIM, device=DEVICE, dtype=DTYPE, generator=g)
from fastvideo.forward_context import set_forward_context
t0 = time.time()
with torch.no_grad():
for t_cur, t_next in zip(timesteps[:-1], timesteps[1:]):
t_in = t_cur.expand(B).to(DTYPE)
r_in = t_next.expand(B).to(DTYPE)
with set_forward_context(current_timestep=t_in, attn_metadata=None):
v = fv_model(
hidden_states=x,
encoder_hidden_states=enc,
timestep=t_in,
r_timestep=r_in,
)
x = scheduler.step(
v,
sample=x,
timestep=t_cur.repeat(B),
r_timestep=t_next.repeat(B),
)
elapsed = time.time() - t0
xf = x.float()
print(f" elapsed: {elapsed:.1f}s")
print(f" final latent: mean={xf.mean().item():+.3f} std={xf.std().item():.3f} "
f"range=[{xf.min().item():+.2f}, {xf.max().item():+.2f}] "
f"finite={torch.isfinite(xf).all().item()}")
ok = torch.isfinite(xf).all().item() and 0.01 < xf.std().item() < 30
print(f" >>> {'PASS' if ok else 'FAIL'}")
return ok
NUM_TRAIN_TIMESTEPS = 1000
EPSILON = 5.0 # AnyFlow paper default
@torch.no_grad()
def training_step_compare(fv_model, af_net) -> bool:
"""Inline replica of AnyFlow's train_bidirection central-difference loss
on both code paths with identical synthetic (real, noise, t, r) inputs.
Compares scalar loss + intermediate flow_pred / target tensors.
"""
banner("(5) Training-step loss comparison (central-difference target)")
from fastvideo.forward_context import set_forward_context
B, C, F, H, W = 1, 16, 21, 60, 104
SEQ, DIM = 32, 4096
g = torch.Generator(device=DEVICE).manual_seed(SEED)
real = torch.randn(B, C, F, H, W, device=DEVICE, dtype=DTYPE, generator=g)
enc = torch.randn(B, SEQ, DIM, device=DEVICE, dtype=DTYPE, generator=g)
noise = torch.randn_like(real)
t_abs_pf = torch.full((B, F), 500.0, device=DEVICE, dtype=DTYPE)
r_abs_pf = torch.full((B, F), 200.0, device=DEVICE, dtype=DTYPE)
t_abs_ps = torch.full((B, ), 500.0, device=DEVICE, dtype=DTYPE)
r_abs_ps = torch.full((B, ), 200.0, device=DEVICE, dtype=DTYPE)
# AnyFlow inline replica: uses [B, F, C, H, W] layout.
real_btchw = real.permute(0, 2, 1, 3, 4).contiguous()
noise_btchw = noise.permute(0, 2, 1, 3, 4).contiguous()
t_norm_pf = (t_abs_pf / NUM_TRAIN_TIMESTEPS).view(B, F, 1, 1, 1).to(DTYPE)
noisy_btchw = t_norm_pf * noise_btchw + (1 - t_norm_pf) * real_btchw
def u_func_af(x_in, t_in, r_in):
return af_net(x_in,
timestep=t_in,
r_timestep=r_in,
encoder_hidden_states=enc,
return_dict=False,
is_causal=False)[0]
v_pred = noise_btchw - real_btchw
eps = EPSILON
F_plus = u_func_af(noisy_btchw + v_pred * (eps / NUM_TRAIN_TIMESTEPS), t_abs_pf + eps, r_abs_pf)
F_minus = u_func_af(noisy_btchw - v_pred * (eps / NUM_TRAIN_TIMESTEPS), t_abs_pf - eps, r_abs_pf)
dF_dt_af = (F_plus - F_minus) / (2 * eps)
target_af = ((noise_btchw - real_btchw) - (t_abs_pf - r_abs_pf).view(B, F, 1, 1, 1) * dF_dt_af)
flow_af = u_func_af(noisy_btchw, t_abs_pf, r_abs_pf)
loss_af = (flow_af.float() - target_af.float()).pow(2).reshape(B, -1).mean(-1)
# FastVideo inline replica: uses [B, C, F, H, W] layout + [B] t/r.
real_bcfhw = real
noise_bcfhw = noise
t_norm_ps = (t_abs_ps / NUM_TRAIN_TIMESTEPS).view(B, 1, 1, 1, 1).to(DTYPE)
noisy_bcfhw = t_norm_ps * noise_bcfhw + (1 - t_norm_ps) * real_bcfhw
def u_func_fv(x_in, t_in, r_in):
with set_forward_context(current_timestep=t_in, attn_metadata=None):
return fv_model(
hidden_states=x_in,
encoder_hidden_states=enc,
timestep=t_in,
r_timestep=r_in,
)
v_pred_fv = noise_bcfhw - real_bcfhw
F_plus_fv = u_func_fv(noisy_bcfhw + v_pred_fv * (eps / NUM_TRAIN_TIMESTEPS), t_abs_ps + eps, r_abs_ps)
F_minus_fv = u_func_fv(noisy_bcfhw - v_pred_fv * (eps / NUM_TRAIN_TIMESTEPS), t_abs_ps - eps, r_abs_ps)
dF_dt_fv = (F_plus_fv - F_minus_fv) / (2 * eps)
target_fv = ((noise_bcfhw - real_bcfhw) - (t_abs_ps - r_abs_ps).view(B, 1, 1, 1, 1) * dF_dt_fv)
flow_fv = u_func_fv(noisy_bcfhw, t_abs_ps, r_abs_ps)
loss_fv = (flow_fv.float() - target_fv.float()).pow(2).reshape(B, -1).mean(-1)
# Compare. Align AnyFlow's [B, F, C, H, W] → [B, C, F, H, W].
flow_af_aligned = flow_af.permute(0, 2, 1, 3, 4)
target_af_aligned = target_af.permute(0, 2, 1, 3, 4)
flow_diff = (flow_fv.float() - flow_af_aligned.float()).abs()
target_diff = (target_fv.float() - target_af_aligned.float()).abs()
af_loss_v = loss_af.mean().item()
fv_loss_v = loss_fv.mean().item()
abs_diff = abs(af_loss_v - fv_loss_v)
rel_diff = abs_diff / abs(af_loss_v + 1e-12)
print(f" AnyFlow loss : {af_loss_v:.6f}")
print(f" FastVideo loss: {fv_loss_v:.6f}")
print(f" abs diff : {abs_diff:.3e}")
print(f" rel diff : {rel_diff:.3e}")
print(f" flow_pred : max abs {flow_diff.max().item():.3e} "
f"mean {flow_diff.mean().item():.3e}")
print(f" target : max abs {target_diff.max().item():.3e} "
f"mean {target_diff.mean().item():.3e}")
ok = rel_diff < 0.20
print(f" >>> {'PASS' if ok else 'FAIL'} (target rel loss diff < 20%)")
return ok
def main() -> None:
torch.manual_seed(SEED)
torch.cuda.manual_seed_all(SEED)
init_single_rank()
fv_model, n_miss, n_unex = build_fastvideo_transformer()
af_net = build_anyflow_reference()
forward_ok = forward_compare(fv_model, af_net)
sample_ok = sample_anystep(fv_model)
train_ok = training_step_compare(fv_model, af_net)
banner(f"SUMMARY: missing_keys={n_miss} unexpected_keys={n_unex} "
f"forward_parity={forward_ok} sample_smoke={sample_ok} "
f"training_parity={train_ok}")
sys.exit(0 if (forward_ok and sample_ok and train_ok) else 1)
if __name__ == "__main__":
main()