Signed-off-by: Yueqian Lin <linyueqian@outlook.com> Co-authored-by: Sy03 <1370724210@qq.com>
440 lines
16 KiB
Python
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)
|