Files
vllm-omni/tests/diffusion/models/auk/test_auk_transformer_parity.py
T

440 lines
16 KiB
Python

#!/usr/bin/env python
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""AuK transformer parity: the correctness oracle for the in-tree port.
Checks that :class:`AuKTransformer` reproduces the upstream ``auk`` reference backbone
on the released ``auk_base`` weights. Both models are built from the same
state dict and fed the same inputs, under the reference's own inference
configuration: fp32 weights with ``torch.autocast(bfloat16)``. That
configuration matters. The reference keeps its rotary frequencies in a buffer,
so casting its weights to bf16 would round both the frequencies and the
integer positions and move the numbers for reasons unrelated to the port.
Cases:
* ``single`` - one conditional forward
* ``cfg`` - the doubled ``cfg_infer`` forward, both branches
* ``masked`` - batch of 2 with padded targets, references and text
* ``rope`` - the local rotary implementation against ``x_transformers``, and
its survival of a cast to bf16
* ``sample`` - an 8-step Euler trajectory against the reference backbone
driven by a transcription of upstream's ODE loop
* ``generator`` - the sampler's two noise paths agreeing
* ``cache`` - the text projection cache and ``clear_cache()``
* ``mask flag`` - ``attn_mask_enabled=False`` reproducing the reference with
its own flag off
This directory deliberately has no ``__init__.py``, unlike its siblings.
pytest's prepend import mode puts the nearest non-package directory on
``sys.path``, and an ``auk`` package here would shadow the upstream ``auk``
distribution this test compares against.
Needs the released checkpoint and a GPU, so it runs locally rather than in
CI. Point ``AUK_CKPT_DIR`` at a directory holding ``auk_base.safetensors`` and
``config.yaml``; the test skips when that is unset or when the upstream
``auk`` package is not importable::
AUK_CKPT_DIR=/path/to/ckpts/AuK CUDA_VISIBLE_DEVICES=6 \
python -m pytest tests/diffusion/models/auk/test_auk_transformer_parity.py -v -s
"""
from __future__ import annotations
import importlib.util
import math
import os
from pathlib import Path
import pytest
import torch
import yaml
from vllm_omni.diffusion.models.auk.auk_transformer import (
AuKTransformer,
Rotary,
_apply_rope,
dit_state_dict,
sample_latents,
)
CKPT_DIR = os.environ.get("AUK_CKPT_DIR")
# Tolerances. The port evaluates rotary embeddings in fp32 and skips the
# reference's redundant uncond text projection, so the two graphs are not
# bit-identical under autocast; they agree well inside bf16 noise.
FORWARD_ATOL = 3e-2
FORWARD_COS = 0.999
SAMPLE_ATOL = 5e-2
SAMPLE_COS = 0.999
ROPE_ATOL = 1e-5
TARGET_FRAMES = 300
REF_FRAMES = 150
TEXT_TOKENS = 40
SHORT_LENS = (220, 90, 25) # target, reference, text for the padded batch row
TIME = 0.37
SAMPLE_STEPS = 8
SWAY_COEF = -1.0
SEED = 1234
DEVICE = "cuda:0"
AUTOCAST_DTYPE = torch.bfloat16
def _skip_reason() -> str | None:
if CKPT_DIR is None:
return "AUK_CKPT_DIR is not set"
if not Path(CKPT_DIR, "auk_base.safetensors").is_file():
return f"no auk_base.safetensors under {CKPT_DIR}"
try:
# Resolving the submodule, not just "auk", also catches the shadow an
# __init__.py in this directory would create.
upstream = importlib.util.find_spec("auk.model")
except ModuleNotFoundError:
upstream = None
if upstream is None:
return "the upstream 'auk' package is not importable or is shadowed by this directory"
if not torch.cuda.is_available():
return "no CUDA device"
return None
SKIP_REASON = _skip_reason()
pytestmark = [
pytest.mark.local_model,
pytest.mark.diffusion,
pytest.mark.gpu,
pytest.mark.cuda,
pytest.mark.skipif(SKIP_REASON is not None, reason=str(SKIP_REASON)),
]
def _arch() -> dict:
"""Read the backbone geometry out of the checkpoint's config."""
config = yaml.safe_load(Path(CKPT_DIR, "config.yaml").read_text())["model"]
return {
"dim": config["arch"]["dim"],
"heads": config["arch"]["heads"],
"dim_head": config["arch"].get("dim_head", 64),
"ff_mult": config["arch"]["ff_mult"],
"text_hidden_dim": config["arch"]["text_hidden_dim"],
"num_layers": config["arch"]["num_layers"],
"num_single_layers": config["arch"]["num_single_layers"],
"latent_dim": config["vae"]["latent_dim"],
}
def _stats(port: torch.Tensor, ref: torch.Tensor) -> tuple[float, float, float]:
"""Return max abs diff, cosine similarity and the reference's RMS."""
a = port.flatten().float()
b = ref.flatten().float()
return (
(a - b).abs().max().item(),
torch.nn.functional.cosine_similarity(a, b, dim=0).item(),
b.pow(2).mean().sqrt().item(),
)
def _assert_close(name: str, port: torch.Tensor, ref: torch.Tensor, atol: float, min_cos: float) -> None:
max_abs, cos, rms = _stats(port, ref)
print(f"[{name}] max|Δ|={max_abs:.4e} cos={cos:.8f} ref_rms={rms:.4f} rel={max_abs / rms:.3e}")
assert max_abs < atol, f"{name}: max|Δ|={max_abs:.4e} >= {atol:.1e} (reference rms {rms:.4f})"
assert cos > min_cos, f"{name}: cos={cos:.8f} <= {min_cos}"
def _autocast():
return torch.autocast("cuda", dtype=AUTOCAST_DTYPE)
@pytest.fixture(scope="module")
def models() -> tuple[torch.nn.Module, AuKTransformer]:
"""The reference and the port, same weights, fp32 on the accelerator."""
from auk.model.flux2_edit import Flux2Edit
from safetensors.torch import load_file
arch = _arch()
device = torch.device(DEVICE)
state = dit_state_dict(load_file(str(Path(CKPT_DIR, "auk_base.safetensors")), device="cpu"))
reference = Flux2Edit(dropout=0.0, attn_backend="torch", attn_mask_enabled=True, **arch)
reference.load_state_dict(state, strict=True)
reference = reference.to(device).eval().requires_grad_(False)
port = AuKTransformer(**arch)
# strict=True is the point of the port: every released tensor lands.
port.load_state_dict(state, strict=True)
port = port.to(device).eval().requires_grad_(False)
del state
return reference, port
@pytest.fixture(scope="module")
def inputs() -> dict[str, torch.Tensor]:
"""Fixed random inputs, as a batch of 2 whose second row is padded."""
device = torch.device(DEVICE)
generator = torch.Generator(device="cpu").manual_seed(SEED)
arch = _arch()
def randn(*shape: int) -> torch.Tensor:
return torch.randn(*shape, generator=generator, dtype=torch.float32).to(device)
def pad_mask(width: int, short: int) -> torch.Tensor:
mask = torch.zeros(2, width, dtype=torch.bool, device=device)
mask[0, :] = True
mask[1, :short] = True
return mask
mask2 = pad_mask(TARGET_FRAMES, SHORT_LENS[0])
ref_mask2 = pad_mask(REF_FRAMES, SHORT_LENS[1])
c_mask2 = pad_mask(TEXT_TOKENS, SHORT_LENS[2])
# Padding rows are all-zero in the text encoder's output, so keep the
# inputs consistent with the masks.
x2 = randn(2, TARGET_FRAMES, arch["latent_dim"]) * mask2.unsqueeze(-1)
ref2 = randn(2, REF_FRAMES, arch["latent_dim"]) * ref_mask2.unsqueeze(-1)
text2 = randn(2, TEXT_TOKENS, arch["text_hidden_dim"]) * c_mask2.unsqueeze(-1)
return {
"x": x2[:1].contiguous(),
"ref": ref2[:1].contiguous(),
"text": text2[:1].contiguous(),
"c_mask": c_mask2[:1].contiguous(),
"x2": x2,
"ref2": ref2,
"text2": text2,
"mask2": mask2,
"ref_mask2": ref_mask2,
"c_mask2": c_mask2,
"time": torch.tensor(TIME, dtype=torch.float32, device=device),
}
def test_single_forward(models, inputs) -> None:
"""One conditional forward: reference against port."""
reference, port = models
kwargs = dict(text=inputs["text"], time=inputs["time"], c_mask=inputs["c_mask"], ref=inputs["ref"])
with torch.no_grad(), _autocast():
want = reference(x=inputs["x"], **kwargs)
got = port(inputs["x"], **kwargs)
assert got.shape == want.shape == (1, TARGET_FRAMES, port.latent_dim)
_assert_close("single", got, want, FORWARD_ATOL, FORWARD_COS)
def test_cfg_forward(models, inputs) -> None:
"""The doubled cfg_infer forward, cond branch first."""
reference, port = models
kwargs = dict(
text=inputs["text"],
time=inputs["time"],
c_mask=inputs["c_mask"],
ref=inputs["ref"],
cfg_infer=True,
)
with torch.no_grad(), _autocast():
want = reference(x=inputs["x"], **kwargs)
got = port(inputs["x"], **kwargs)
assert got.shape == want.shape == (2, TARGET_FRAMES, port.latent_dim)
_assert_close("cfg-cond", got[:1], want[:1], FORWARD_ATOL, FORWARD_COS)
_assert_close("cfg-uncond", got[1:], want[1:], FORWARD_ATOL, FORWARD_COS)
def test_masked_batch_forward(models, inputs) -> None:
"""Batch of 2 with padded targets, references and text.
Only valid target frames are compared. Padded frames are unconstrained:
the reference leaves them to whatever masked attention emits there.
"""
reference, port = models
kwargs = dict(
text=inputs["text2"],
time=inputs["time"],
mask=inputs["mask2"],
c_mask=inputs["c_mask2"],
ref=inputs["ref2"],
ref_mask=inputs["ref_mask2"],
)
with torch.no_grad(), _autocast():
want = reference(x=inputs["x2"], **kwargs)
got = port(inputs["x2"], **kwargs)
assert got.shape == want.shape == (2, TARGET_FRAMES, port.latent_dim)
valid = inputs["mask2"].unsqueeze(-1).expand_as(got)
_assert_close("masked", got[valid], want[valid], FORWARD_ATOL, FORWARD_COS)
def _set_mask_flag(reference, port, enabled: bool) -> None:
"""Flip attn_mask_enabled on both models in place, so no second copy is loaded."""
for module in port.modules():
if hasattr(module, "attn_mask_enabled"):
module.attn_mask_enabled = enabled
for blocks in (reference.transformer_blocks, reference.single_transformer_blocks):
for block in blocks:
block.attn.processor.attn_mask_enabled = enabled
def test_mask_flag_disabled(models, inputs) -> None:
"""``attn_mask_enabled=False`` must track the reference with its own flag off.
With the flag off both models run attention unmasked and still zero the
padded outputs, so the padded batch is compared in full.
"""
reference, port = models
kwargs = dict(
text=inputs["text2"],
time=inputs["time"],
mask=inputs["mask2"],
c_mask=inputs["c_mask2"],
ref=inputs["ref2"],
ref_mask=inputs["ref_mask2"],
)
with torch.no_grad(), _autocast():
masked = port(inputs["x2"], **kwargs)
_set_mask_flag(reference, port, False)
try:
want = reference(x=inputs["x2"], **kwargs)
got = port(inputs["x2"], **kwargs)
finally:
_set_mask_flag(reference, port, True)
_assert_close("mask-flag-off", got, want, FORWARD_ATOL, FORWARD_COS)
# Guard against a vacuous pass: the flag has to actually change something.
assert (got.float() - masked.float()).abs().max().item() > 1e-3
def test_sampler_generator_matches_seed(models, inputs) -> None:
"""A generator seeded with 0 must draw exactly what ``seed=0`` draws."""
_, port = models
common = dict(
gen_frames=TARGET_FRAMES,
nfe=2,
cfg_strength=0.0,
sway_sampling_coef=SWAY_COEF,
text=inputs["text"],
c_mask=inputs["c_mask"],
ref=inputs["ref"],
ref_mask=None,
)
with torch.no_grad(), _autocast():
by_seed = sample_latents(port, seed=0, **common)
by_generator = sample_latents(port, generator=torch.Generator(device=DEVICE).manual_seed(0), **common)
print(f"[generator] max|Δ|={(by_generator.float() - by_seed.float()).abs().max().item():.4e}")
torch.testing.assert_close(by_generator, by_seed, rtol=0, atol=0)
def test_cache_needs_clearing(models, inputs) -> None:
"""The cached text projection is reused until ``clear_cache()``.
``sample_latents`` clears on the way out, so a pipeline that reuses the
module across requests is safe without doing anything. A caller driving
``forward(cache=True)`` itself has to clear when the text changes.
"""
_, port = models
other_text = inputs["text"] * 0.5
kwargs = dict(time=inputs["time"], c_mask=inputs["c_mask"], ref=inputs["ref"], cfg_infer=True)
port.clear_cache()
with torch.no_grad(), _autocast():
want = port(inputs["x"], inputs["text"], **kwargs)
port(inputs["x"], other_text, cache=True, **kwargs)
stale = port(inputs["x"], inputs["text"], cache=True, **kwargs)
port.clear_cache()
fresh = port(inputs["x"], inputs["text"], cache=True, **kwargs)
port.clear_cache()
# The cache is real: the second call reused the first call's projection.
assert (stale.float() - want.float()).abs().max().item() > 1e-3
torch.testing.assert_close(fresh, want, rtol=0, atol=0)
assert port.text_cond is None and port.text_uncond is None
def test_rope_matches_x_transformers() -> None:
"""The local rotary implementation against x_transformers, in fp32."""
from x_transformers.x_transformers import RotaryEmbedding, apply_rotary_pos_emb
device = torch.device(DEVICE)
dim_head, seq_len = 64, TEXT_TOKENS + REF_FRAMES + TARGET_FRAMES
q = torch.randn(2, 24, seq_len, dim_head, dtype=torch.float32, device=device)
freqs = RotaryEmbedding(dim_head).to(device).forward_from_seq_len(seq_len)[0]
want = apply_rotary_pos_emb(q, freqs)
got = _apply_rope(q, Rotary(dim_head).to(device)(seq_len))
max_abs, cos, _ = _stats(got, want)
print(f"[rope] max|Δ|={max_abs:.4e} cos={cos:.10f}")
assert max_abs < ROPE_ATOL, f"rope: max|Δ|={max_abs:.4e} >= {ROPE_ATOL:.1e}"
def test_rope_survives_half_precision_cast() -> None:
"""Casting the module to bf16 must not round the rotary frequencies.
The reference stores them in a buffer, so ``.to(bfloat16)`` rounds both the
frequencies and the integer positions and silently changes every rotation.
The port recomputes them instead.
"""
dim_head, seq_len = 64, TEXT_TOKENS + REF_FRAMES + TARGET_FRAMES
exact = Rotary(dim_head)(seq_len)
got = Rotary(dim_head).to(torch.bfloat16)(seq_len)
assert got.dtype == torch.float32
torch.testing.assert_close(got, exact, rtol=0, atol=0)
def _reference_euler(reference, *, x, text, c_mask, ref, steps, cfg_strength, sway) -> torch.Tensor:
"""Upstream's ODE loop, transcribed.
``CFMEdit.sample`` hands the backbone to ``torchdiffeq.odeint(method=
"euler")``, which on a fixed grid is ``y += (t1 - t0) * f(t0, y)``.
Constructing a real ``CFMEdit`` would drag in the Qwen-Omni text encoder,
so only the backbone here comes from upstream.
"""
t = torch.linspace(0, 1, steps + 1, device=x.device, dtype=torch.float32)
if sway is not None:
t = t + sway * (torch.cos(math.pi / 2 * t) - 1 + t)
shared = dict(text=text, c_mask=c_mask, ref=ref, mask=None, ref_mask=None, cache=True)
for i in range(steps):
if cfg_strength < 1e-5:
v = reference(x=x, time=t[i], **shared)
else:
pred = reference(x=x, time=t[i], cfg_infer=True, **shared)
v_cond, v_uncond = torch.chunk(pred, 2, dim=0)
v = v_cond + (v_cond - v_uncond) * cfg_strength
x = x + (t[i + 1] - t[i]) * v
reference.clear_cache()
return x
@pytest.mark.parametrize("cfg_strength", [0.0, 2.0])
def test_euler_sample(models, inputs, cfg_strength: float) -> None:
"""An 8-step Euler trajectory, with and without guidance."""
reference, port = models
shared = dict(text=inputs["text"], c_mask=inputs["c_mask"], ref=inputs["ref"])
with torch.no_grad(), _autocast():
got = sample_latents(
port,
gen_frames=TARGET_FRAMES,
ref_mask=None,
nfe=SAMPLE_STEPS,
cfg_strength=cfg_strength,
sway_sampling_coef=SWAY_COEF,
seed=SEED,
**shared,
)
# Re-seeding reproduces the noise sample_latents drew for itself.
torch.manual_seed(SEED)
noise = torch.randn(
TARGET_FRAMES, port.latent_dim, device=inputs["ref"].device, dtype=inputs["ref"].dtype
).unsqueeze(0)
want = _reference_euler(
reference, x=noise, steps=SAMPLE_STEPS, cfg_strength=cfg_strength, sway=SWAY_COEF, **shared
)
assert got.shape == want.shape == (1, TARGET_FRAMES, port.latent_dim)
_assert_close(f"sample-cfg{cfg_strength:g}", got, want, SAMPLE_ATOL, SAMPLE_COS)