feat(video): step caching for HunyuanVideo-1.5 (MagCache auto, FBCache registry) + int8 trim fix

HunyuanVideo-1.5 loads previously logged 'fbcache unavailable (Model class
HunyuanVideo15TransformerBlock not registered)': diffusers 0.39 ships FBCache
block metadata for HunyuanVideo 1.0 but not 1.5, although the 1.5 DiT is fully
cache-shaped (CacheMixin, homogeneous residual-additive dual-stream blocks,
cache_context per guidance branch). Register the missing metadata at engage
time (deferring to a native registration when a future diffusers ships one).

Measured on a B200 (720p t2v, 1280x720, 33 frames, seed 42), FBCache is fast
but not shippable for this family: 1.44x at 30 steps / 2.41x at 50 steps, at
LPIPS 0.43-0.54 vs the same uncached stack with a +5..8 luma drift (no skip
cap or error budget, so the trajectory derails into a different clip). MagCache
(same registry metadata, also dispatched via enable_cache) is bounded by
design and lands the win: 1.49x end-to-end at 50 steps at LPIPS 0.147 with the
same composition, 1.21x at LPIPS 0.071 on the 480p model. Ship magcache as the
per-family AUTO cache mode for hunyuanvideo-1.5 / -720p with 50-step
calibrated per-family mag_ratios (cond/uncond curves agree within 0.014,
30 vs 50-step calibration within 0.027 after interpolation); every other
family keeps fbcache, and explicit fbcache/magcache requests are honored.
The auto toggle re-engages magcache on a step-count change so the ratio curve
is re-interpolated over the actual schedule.

Two production bugs fixed along the way:
- diffusers' HookRegistry caches its child-registry list, so enabling a cache
  AFTER any uncached generation (the auto off-to-on toggle) left the new block
  hooks without a context ('No context is set' on the first cached forward).
  Invalidate the stale cache after every enable_cache.
- An explicit int8 DiT request crashed under the padded-text trim: torchao's
  int8 dynamic path returns a zero-token (M=0) input unprojected (t2v byt5 /
  image streams -> cond-type add shape crash) and torch._int_mm requires
  M > 16 (an empty negative prompt trims to ~6 tokens -> TokenRefiner crash).
  Add per-family int8 excludes for the text-stream linears (context_embedder*,
  image_embedder, add_q/k/v_proj, to_add_out, ff_context); they run at tens of
  tokens vs the ~32k video stream, so the exclusion costs nothing measurable.

Bench: trim lever key (trim_off / eager_trim isolation configs), int8_cudnn +
shipped_nocache rows, --cache-threshold, warmup timing, per-config frame
persistence for offline LPIPS rescoring, and the loader's per-family auto
cache mode mirrored. Full 720p matrix recorded: reference 481.6s ->
trim+cudnn+compile 35.4s -> shipped default with TE/VAE quant + magcache
24.9s (19.4x, peak VRAM 89.4 -> 81.8 GB), int8 latency-neutral (dense auto
policy confirmed), compile 1.56x per step, trim 13.5x per step at production
shapes.

Validated end to end through the real VideoBackend: load resolves
transformer_cache=magcache with trim + compile + cudnn, generation
re-interpolates 50 -> 30 steps, auto-disengages below 20 steps, re-engages
after an uncached generation, unload restores globals. Hermetic tests cover
the registration, the child-cache invalidation, magcache engage/threshold/
no-curve/no-steps paths, auto-mode routing, toggle re-interpolation, and the
family int8 excludes.
This commit is contained in:
Daniel Han 2026-07-10 10:47:03 +00:00
commit c998183cc2
6 changed files with 637 additions and 50 deletions

View file

@ -268,6 +268,29 @@ _CONFIGS: dict[str, dict[str, Any]] = {
"ditint8_nocache": dict(
te = "none", vae = "none", dit = "int8", speed = "default", attn = "native", cache = "off"
),
# Hunyuan padded-text trim isolation: "cudnn" above is the same stack WITH the trim
# (it auto-engages under any active speed tier), so trim_off isolates its win. The
# trim key defaults True everywhere else; only this row forces it off.
"trim_off": dict(
te = "none", vae = "none", dit = "none", speed = "default", attn = "auto", cache = "off",
trim = False,
),
# Compile isolation at matched attention/trim: eager tier (channels_last + cudnn
# benchmark, NO compile) vs the "cudnn" row (default tier = regional compile).
"eager_trim": dict(
te = "none", vae = "none", dit = "none", speed = "eager", attn = "auto", cache = "off"
),
# int8 DiT baseline at the production attention stack (cudnn + trim), cache off,
# directly comparable to the "cudnn" dense row.
"int8_cudnn": dict(
te = "none", vae = "none", dit = "int8", speed = "default", attn = "auto", cache = "off"
),
# The full companion-quant stack WITHOUT step caching: te/vae auto + dit auto (dense
# -fit skip on Hunyuan) + compile + cudnn + trim. The stacked-best candidate default
# when the family's auto cache policy stays off.
"shipped_nocache": dict(
te = "auto", vae = "auto", dit = "auto", speed = "default", attn = "auto", cache = "off"
),
}
@ -335,6 +358,7 @@ def _apply_levers(
fam_obj,
force_fp32_vae: bool,
default_steps: int,
cache_threshold: Optional[float] = None,
logger = None,
) -> dict:
"""Apply the configured levers with the loader's own argument values, in the loader's order:
@ -355,7 +379,7 @@ def _apply_levers(
)
from core.inference.diffusion_cache import (
apply_step_cache,
TC_FBCACHE,
auto_cache_mode,
FBCACHE_MIN_STEPS,
)
@ -436,14 +460,18 @@ def _apply_levers(
# Step cache FIRST (compile keys fullgraph off an active cache); per expert.
cache_active = False
if cfg["cache"] == "auto":
cache_request = TC_FBCACHE if default_steps >= FBCACHE_MIN_STEPS else None
# Per-family auto mode, exactly like the loader (video.py): MagCache for the
# HunyuanVideo-1.5 families, FBCache elsewhere.
cache_request = auto_cache_mode(fam_name) if default_steps >= FBCACHE_MIN_STEPS else None
if cache_request is not None:
for v in views:
engaged["cache"] = apply_step_cache(
v,
mode = cache_request,
threshold = None,
threshold = cache_threshold,
quant_active = dit_quant_active,
family = fam_name,
steps = default_steps,
logger = logger,
)
cache_active = engaged["cache"] not in (None, "off")
@ -453,7 +481,7 @@ def _apply_levers(
# text tokens so the fused SDPA kernel runs (~18x/DiT-forward, cosine ~1.0). A speed lever, so
# gated on an active tier like the loader; no-op for every non-Hunyuan family.
trim_engaged = False
if speed_active:
if speed_active and cfg.get("trim", True):
for v in views:
trim_engaged = install_hunyuan_attention_trim(v, fam_obj, logger = logger) or trim_engaged
engaged["attn_trim"] = trim_engaged
@ -493,13 +521,15 @@ def _timed_video(
dit_quant_active,
default_steps,
guidance_via_guider = False,
cache_threshold = None,
family = None,
logger = None,
):
"""One clip generation. Re-checks FBCache per generation (maybe_toggle_step_cache) exactly
like the loader, then times total + per-step. Returns (output, total_s, [per_step_ms])."""
"""One clip generation. Re-checks the step cache per generation (maybe_toggle_step_cache)
exactly like the loader, then times total + per-step. Returns (output, total_s, [per_step_ms])."""
import torch
from core.inference.diffusion_cache import maybe_toggle_step_cache, FBCACHE_MIN_STEPS
from core.inference.diffusion_cache import auto_cache_mode, maybe_toggle_step_cache
if cache_mode == "auto":
# Toggle on EVERY expert view, exactly like the loader's per-view recheck
@ -512,7 +542,13 @@ def _timed_video(
for v in views:
try:
maybe_toggle_step_cache(
v, steps = steps, quant_active = dit_quant_active, threshold = None, logger = logger
v,
steps = steps,
quant_active = dit_quant_active,
threshold = cache_threshold,
mode = auto_cache_mode(family),
family = family,
logger = logger,
)
except Exception:
pass
@ -592,6 +628,7 @@ def _run_config(
seed: int,
iters: int,
out: Path,
cache_threshold: Optional[float] = None,
logger = None,
):
import numpy as np
@ -627,6 +664,7 @@ def _run_config(
fam_obj = fam_obj,
force_fp32_vae = force_fp32,
default_steps = default_steps,
cache_threshold = cache_threshold,
logger = logger,
)
pipe = pipe.to("cuda")
@ -638,6 +676,7 @@ def _run_config(
cache_mode = cfg["cache"]
# warmup (pays the one-time compile / autotune)
warmup_t0 = time.perf_counter()
_timed_video(
pipe,
steps = steps,
@ -650,8 +689,11 @@ def _run_config(
dit_quant_active = dit_active,
default_steps = default_steps,
guidance_via_guider = gvg,
cache_threshold = cache_threshold,
family = family,
logger = logger,
)
warmup_s = time.perf_counter() - warmup_t0
_reset_peak()
dts, steps_ms = [], []
last_out = None
@ -668,6 +710,8 @@ def _run_config(
dit_quant_active = dit_active,
default_steps = default_steps,
guidance_via_guider = gvg,
cache_threshold = cache_threshold,
family = family,
logger = logger,
)
dts.append(dt)
@ -700,6 +744,14 @@ def _run_config(
)
except Exception:
pass
# Persist EVERY config's frames too, so a row generated before the reference exists
# (parallel per-GPU processes) can be LPIPS-rescored offline instead of publishing null.
if arrs:
try:
import numpy as _np
_np.savez_compressed(out / f"frames_{family}_{name}.npz", *arrs)
except Exception:
pass
row = {
"config": name,
@ -713,9 +765,13 @@ def _run_config(
"cache": engaged["cache"] or "off",
"effective_speed": engaged.get("_effective_speed"),
"speed_optims": engaged["speed_optims"],
"attn_trim": engaged.get("attn_trim", False),
"cache_threshold": cache_threshold,
"cache_marker": getattr(getattr(pipe, "transformer", None), "_unsloth_step_cache", None),
"load_peak_gb": round(load_peak, 2),
"weights_gb": round(weights_gb, 2),
"gen_peak_gb": round(gen_peak, 2),
"warmup_s": round(warmup_s, 3),
"gen_latency_s": round(_median(dts), 3),
"per_step_ms": round(_median(steps_ms), 1),
"n_frames": len(arrs),
@ -739,6 +795,12 @@ def main(argv = None) -> int:
ap.add_argument("--seed", type = int, default = 42)
ap.add_argument("--iters", type = int, default = 3)
ap.add_argument("--out", default = "outputs/video_speedmem")
ap.add_argument(
"--cache-threshold",
type = float,
default = None,
help = "FBCache residual-diff threshold override (None -> the production default)",
)
args = ap.parse_args(argv)
import logging
@ -793,6 +855,7 @@ def main(argv = None) -> int:
seed = args.seed,
iters = args.iters,
out = out,
cache_threshold = args.cache_threshold,
logger = logger,
)
if n == "reference":