HunyuanVideo-1.5's DiT runs a joint [video; text] self-attention and, on every block and step, builds a dense [B,1,N,N] boolean mask so the video never attends to the padded text. A dense bool attn_mask disables every fused SDPA kernel (flash rejects it; cuDNN and memory-efficient fall back), so the attention runs the slow math-style path: at the production shape (121 frames, 480p, N about 50k) one attention call is ~421ms with the mask vs ~19ms with attn_mask=None. The text is ~99.5% padding (a t2v prompt fills ~9 of ~1985 slots), so nearly all of that cost is spent masking padding. install_hunyuan_attention_trim installs an eager forward pre-hook that drops the all-zero image stream (t2v) and trims the mllm/byt5 text streams to their globally-valid columns, plus a null-mask attention processor that runs attn_mask=None once no partially-padded column remains (the batch-1 / per-guidance-branch case) and otherwise delegates to the stock dense-mask processor. The model already zeroes and masks the padded text and discards its attention output (only the video split feeds proj_out), so removing it is exact for the video; the only numeric change is the SDPA kernel (masked fallback to fused). Measured on a B200: 23.3s to 1.3s per DiT forward at 121 frames (~18x with regional compile, 0 graph breaks); per-forward cosine 0.99998 vs stock; equal distance to an fp32 reference (LPIPS fp32-vs-stock 0.292, fp32-vs-trim 0.307), so it is not less accurate than the current bf16 default. Wired auto-on for HunyuanVideo-1.5 in the video loader, before the attention backend set so the requested kernel pins onto the new processors; a no-op for every other family and reversible (stock dense-mask path on any anomaly). Adds hermetic tests and the diagnostic/validation scripts.
155 lines
5.9 KiB
Python
155 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
"""Validate the HunyuanVideo-1.5 padded-text attention trim: accuracy (stock vs trimmed forward
|
|
output on the SAME real inputs), per-forward speed, and torch.compile compatibility -- all at the
|
|
real production shape (default 121 frames / 480p).
|
|
|
|
Run: CUDA_VISIBLE_DEVICES=3 python scripts/hunyuan_trim_validate.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
os.environ.setdefault("BITSANDBYTES_NOWELCOME", "1")
|
|
|
|
import torch
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(_REPO_ROOT / "studio" / "backend"))
|
|
|
|
|
|
def _import_diffusers():
|
|
import diffusers.utils.import_utils as iu
|
|
|
|
iu._bitsandbytes_available = False
|
|
import diffusers
|
|
|
|
return diffusers
|
|
|
|
|
|
CAP: dict = {}
|
|
|
|
|
|
class _Stop(Exception):
|
|
pass
|
|
|
|
|
|
def _capture_hook(module, args, kwargs):
|
|
CAP["kwargs"] = {k: v for k, v in kwargs.items()}
|
|
CAP["args"] = args
|
|
raise _Stop
|
|
|
|
|
|
def _forward(transformer, no_grad=True):
|
|
ctx = torch.no_grad() if no_grad else torch.enable_grad()
|
|
with ctx:
|
|
out = transformer(*CAP["args"], **CAP["kwargs"])
|
|
return out[0] if isinstance(out, tuple) else out.sample
|
|
|
|
|
|
def _median_ms(fn, iters=8, warmup=2):
|
|
for _ in range(warmup):
|
|
fn()
|
|
torch.cuda.synchronize()
|
|
ts = []
|
|
for _ in range(iters):
|
|
torch.cuda.synchronize()
|
|
t0 = time.perf_counter()
|
|
fn()
|
|
torch.cuda.synchronize()
|
|
ts.append((time.perf_counter() - t0) * 1e3)
|
|
ts.sort()
|
|
return ts[len(ts) // 2]
|
|
|
|
|
|
def _compare(a, b):
|
|
a = a.float().flatten()
|
|
b = b.float().flatten()
|
|
cos = torch.nn.functional.cosine_similarity(a, b, dim=0).item()
|
|
max_abs = (a - b).abs().max().item()
|
|
denom = a.abs().max().item() or 1.0
|
|
return cos, max_abs, max_abs / denom
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--repo", default="hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v")
|
|
ap.add_argument("--frames", type=int, default=121)
|
|
ap.add_argument("--width", type=int, default=832)
|
|
ap.add_argument("--height", type=int, default=480)
|
|
ap.add_argument("--compile", action="store_true", help="also test regional compile of blocks")
|
|
args = ap.parse_args()
|
|
|
|
diffusers = _import_diffusers()
|
|
from core.inference.diffusion_attention import install_hunyuan_attention_trim
|
|
from core.inference.video_families import detect_video_family
|
|
|
|
dev = "cuda:0"
|
|
print(f"loading {args.repo} ...", flush=True)
|
|
pipe = diffusers.DiffusionPipeline.from_pretrained(args.repo, torch_dtype=torch.bfloat16).to(dev)
|
|
fam = detect_video_family(args.repo) or detect_video_family("hunyuanvideo-1.5")
|
|
print(f"family: {getattr(fam, 'name', None)} / transformer_class={getattr(fam, 'transformer_class', None)}", flush=True)
|
|
|
|
h = pipe.transformer.register_forward_pre_hook(_capture_hook, with_kwargs=True)
|
|
print("capturing one real forward input ...", flush=True)
|
|
try:
|
|
pipe(prompt="a cat playing piano", num_frames=args.frames,
|
|
width=args.width, height=args.height, num_inference_steps=1)
|
|
except _Stop:
|
|
pass
|
|
h.remove()
|
|
|
|
k = CAP["kwargs"]
|
|
ehs = k.get("encoder_hidden_states")
|
|
m = k.get("encoder_attention_mask")
|
|
ie = k.get("image_embeds")
|
|
print(f"\ncaptured: encoder_hidden_states={tuple(ehs.shape)} mask_valid={m.bool().sum(1).tolist()}"
|
|
f" image_embeds={tuple(ie.shape) if ie is not None else None}"
|
|
f" image_all_zero={bool(torch.all(ie==0).item()) if ie is not None else None}", flush=True)
|
|
|
|
# ---- STOCK forward (reference) + timing ----
|
|
transformer = pipe.transformer
|
|
out_stock = _forward(transformer).detach().clone()
|
|
t_stock = _median_ms(lambda: _forward(transformer))
|
|
print(f"\nSTOCK forward: {t_stock:8.2f} ms out={tuple(out_stock.shape)}", flush=True)
|
|
|
|
# ---- install trim, re-run same inputs ----
|
|
engaged = install_hunyuan_attention_trim(pipe, fam, logger=None)
|
|
print(f"install_hunyuan_attention_trim engaged = {engaged}", flush=True)
|
|
out_trim = _forward(transformer).detach().clone()
|
|
t_trim = _median_ms(lambda: _forward(transformer))
|
|
|
|
cos, max_abs, rel = _compare(out_stock, out_trim)
|
|
print(f"TRIM forward: {t_trim:8.2f} ms ({t_stock/t_trim:.2f}x faster)", flush=True)
|
|
print(f"\nACCURACY stock-vs-trim: cosine={cos:.8f} max_abs={max_abs:.4e} rel_max={rel:.4e}", flush=True)
|
|
finite = bool(torch.isfinite(out_trim).all().item())
|
|
print(f"trim output finite: {finite}", flush=True)
|
|
|
|
if args.compile:
|
|
print("\ncompiling blocks (compile_repeated_blocks, mode=default, dynamic=True) ...", flush=True)
|
|
try:
|
|
for _a in ("recompile_limit", "cache_size_limit"):
|
|
if hasattr(torch._dynamo.config, _a):
|
|
setattr(torch._dynamo.config, _a, 64)
|
|
transformer.compile_repeated_blocks(fullgraph=False, dynamic=True)
|
|
out_c = _forward(transformer).detach().clone() # triggers compile
|
|
t_c = _median_ms(lambda: _forward(transformer), iters=5, warmup=1)
|
|
cos_c, ma_c, rel_c = _compare(out_stock, out_c)
|
|
cnt = torch._dynamo.utils.counters
|
|
print(f"TRIM+COMPILE forward: {t_c:8.2f} ms ({t_stock/t_c:.2f}x vs stock)", flush=True)
|
|
print(f" accuracy vs stock: cosine={cos_c:.8f} max_abs={ma_c:.4e}", flush=True)
|
|
print(f" dynamo recompiles={sum(cnt['recompiles'].values()) if 'recompiles' in cnt else '?'}"
|
|
f" graph_breaks={sum(cnt['graph_break'].values()) if 'graph_break' in cnt else 0}", flush=True)
|
|
except Exception as exc: # noqa: BLE001
|
|
import traceback
|
|
print(f"COMPILE FAILED: {type(exc).__name__}: {exc}", flush=True)
|
|
traceback.print_exc()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|