Root-caused "HunyuanVideo-1.5 int8 is slower than dense" with a per-forward profiler (scripts/hunyuan_int8_profile.py, dynamo-reset, back-to-back on a clean B200): int8 compiles cleanly (0 recompiles, 0 graph breaks, steady 268.3 ms/forward) and is only ~7% slower than dense + regional compile (250.5 ms/forward), not the 38% a contended-GPU bench run suggested. int8 is also less accurate (LPIPS 0.085 vs dense+compile 0.037). So for a family where fp8 is denied (Hunyuan black-frames on per-row fp8), int8 is a MEMORY lever, not a speed win, yet the auto-quant default quantised it even when the dense DiT already fit resident. Fix: is_int8_memory_fallback(target, family) is True only when AUTO quant lands on int8 as a denied/black-frame fallback on a data-center, fp8-capable GPU (fp8 would be the arch pick but is denied for the family). The video loader now skips the auto-quant and runs dense+compile when that holds AND the bf16 memory plan already fits resident (offload_policy == none), so there is no new OOM risk. Scoped tightly: only an AUTO request (explicit int8/fp8 honored), only int8-fallback families (Wan / LTX resolve to fp8 -> keep quantising), only data-center fp8-capable parts (consumer GPUs and pre-Ada, where int8 is a genuine accelerator, keep int8), and only when dense provably fits; a memory-constrained plan still quantises. Result: Hunyuan on a resident-fit B200 now runs faster AND more accurate, quantising only when memory is the constraint. Also resets dynamo per config in the video bench (so compiled graphs cannot leak across configs in one process) and adds the per-forward profiler used for the diagnosis.
155 lines
5.8 KiB
Python
155 lines
5.8 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Why is HunyuanVideo-1.5 int8 DiT slower than dense+compile? Per-DiT-forward timing + dynamo
|
|
recompile/graph-break counting for dense vs int8, both under the loader's regional block compile.
|
|
|
|
Signal:
|
|
- steady-state per-forward time uniformly higher on int8 -> inherent int8 quant/dequant overhead
|
|
(memory lever, not speed) -> memory-gate the auto-quant.
|
|
- erratic slow forwards / high recompile count / eager fallback -> a fixable compile inefficiency
|
|
(int8 tensor subclass breaks dynamic-shape compile) -> fix the compile path.
|
|
|
|
Run: CUDA_VISIBLE_DEVICES=3 python scripts/hunyuan_int8_profile.py --modes dense,int8
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
os.environ.setdefault("BITSANDBYTES_NOWELCOME", "1")
|
|
os.environ.setdefault("HF_HOME", "/mnt/disks/unslothai/ubuntu/workspace_81/BACKUP_05/temp/hf_cache")
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
for _p in (str(_REPO_ROOT / "studio" / "backend"), str(_REPO_ROOT / "scripts")):
|
|
if _p not in sys.path:
|
|
sys.path.insert(0, _p)
|
|
|
|
from video_speedmem_bench import _build_pipe, _apply_levers, _target, PROMPT # noqa: E402
|
|
|
|
_REPO = "hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v"
|
|
|
|
|
|
def _dynamo_counts():
|
|
"""(recompiles, graph_breaks, unique_compiles) from dynamo counters, best-effort."""
|
|
try:
|
|
from torch._dynamo.utils import counters
|
|
rc = sum(v for k, v in counters.get("recompiles", {}).items()) if "recompiles" in counters else 0
|
|
gb = sum(counters.get("graph_break", {}).values())
|
|
# total frames compiled
|
|
stats = counters.get("stats", {})
|
|
uc = stats.get("unique_graphs", 0)
|
|
return rc, gb, uc
|
|
except Exception:
|
|
return -1, -1, -1
|
|
|
|
|
|
def _profile_mode(mode: str, *, steps: int, width: int, height: int, num_frames: int, guidance: float,
|
|
seed: int):
|
|
import torch
|
|
from core.inference.video_families import detect_video_family
|
|
from core.inference.diffusion_cache import maybe_toggle_step_cache # noqa: F401
|
|
|
|
# fresh dynamo state per mode
|
|
try:
|
|
torch._dynamo.reset()
|
|
from torch._dynamo.utils import counters
|
|
counters.clear()
|
|
except Exception:
|
|
pass
|
|
|
|
fam_obj = detect_video_family(_REPO)
|
|
gvg = bool(getattr(fam_obj, "guidance_via_guider", False))
|
|
default_steps = getattr(fam_obj, "default_steps", 50)
|
|
|
|
pipe = _build_pipe(_REPO, False)
|
|
cfg = dict(te="none", vae="none", dit=("int8" if mode == "int8" else "none"),
|
|
speed="default", attn="native", cache="off")
|
|
engaged = _apply_levers(pipe, cfg, fam_name="hunyuanvideo-1.5", fam_obj=fam_obj,
|
|
force_fp32_vae=False, default_steps=default_steps)
|
|
print(f"[{mode}] dit_scheme={engaged['dit'] or 'dense'} speed={engaged.get('_effective_speed')}",
|
|
flush=True)
|
|
|
|
# per-forward GPU timing via cuda events on the transformer.
|
|
fwd_ms: list[float] = []
|
|
starts: list = []
|
|
|
|
def _pre(mod, args, kwargs):
|
|
ev = torch.cuda.Event(enable_timing=True)
|
|
ev.record()
|
|
starts.append(ev)
|
|
return None
|
|
|
|
def _post(mod, args, output):
|
|
end = torch.cuda.Event(enable_timing=True)
|
|
end.record()
|
|
torch.cuda.synchronize()
|
|
if starts:
|
|
fwd_ms.append(starts[-1].elapsed_time(end))
|
|
|
|
h1 = pipe.transformer.register_forward_pre_hook(_pre, with_kwargs=True)
|
|
h2 = pipe.transformer.register_forward_hook(_post)
|
|
|
|
def _gen(tag):
|
|
fwd_ms.clear()
|
|
starts.clear()
|
|
g = torch.Generator(device="cuda").manual_seed(seed)
|
|
kwargs = dict(prompt=PROMPT, width=width, height=height, num_frames=num_frames,
|
|
num_inference_steps=steps, generator=g)
|
|
if gvg:
|
|
guider = getattr(pipe, "guider", None)
|
|
if guider is not None and hasattr(guider, "guidance_scale"):
|
|
guider.guidance_scale = guidance
|
|
else:
|
|
kwargs["guidance_scale"] = guidance
|
|
torch.cuda.synchronize()
|
|
t0 = time.perf_counter()
|
|
pipe(**kwargs)
|
|
torch.cuda.synchronize()
|
|
total = time.perf_counter() - t0
|
|
rc, gb, uc = _dynamo_counts()
|
|
n = len(fwd_ms)
|
|
srt = sorted(fwd_ms)
|
|
med = srt[n // 2] if n else 0.0
|
|
p10 = srt[max(0, n // 10)] if n else 0.0
|
|
p90 = srt[min(n - 1, (9 * n) // 10)] if n else 0.0
|
|
slow = sum(1 for x in fwd_ms if x > 2.0 * med) if med else 0
|
|
print(f"[{mode}:{tag}] total={total:.2f}s n_fwd={n} med={med:.1f}ms "
|
|
f"p10={p10:.1f} p90={p90:.1f} max={max(fwd_ms) if fwd_ms else 0:.1f} "
|
|
f"slow(>2x med)={slow} | recompiles={rc} graph_breaks={gb} unique_graphs={uc}",
|
|
flush=True)
|
|
return total
|
|
|
|
_gen("warmup") # pays compile
|
|
_gen("timed")
|
|
_gen("timed2")
|
|
|
|
h1.remove(); h2.remove()
|
|
del pipe
|
|
import gc
|
|
gc.collect()
|
|
torch.cuda.empty_cache()
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--modes", default="dense,int8")
|
|
ap.add_argument("--steps", type=int, default=30)
|
|
ap.add_argument("--num-frames", type=int, default=25)
|
|
ap.add_argument("--width", type=int, default=512)
|
|
ap.add_argument("--height", type=int, default=320)
|
|
ap.add_argument("--guidance", type=float, default=6.0)
|
|
ap.add_argument("--seed", type=int, default=42)
|
|
args = ap.parse_args(argv)
|
|
for mode in [m.strip() for m in args.modes.split(",") if m.strip()]:
|
|
_profile_mode(mode, steps=args.steps, width=args.width, height=args.height,
|
|
num_frames=args.num_frames, guidance=args.guidance, seed=args.seed)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|