File size: 8,128 Bytes
b34c6c3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | """Route ``HunyuanVideo_1_5_DiffusionTransformer.forward_vision`` through a cache method.
The preamble (patch embed, time/action modulation vector, RoPE tables, per-token
cameras) and the tail (``final_layer``, ``unpatchify``) are reproduced from the
upstream method; only the 54-block loop is handed to the controller, and only on
denoising forwards -- context passes (``cache_vision=True``) run untouched.
"""
import types
import torch
from einops import rearrange, repeat
from hyvideo.commons.parallel_states import get_parallel_state
from .methods import StepCtx
DEFAULT_SCHEDULE = "FxxF"
def parse_schedule(schedule, num_steps):
# 'R' spells out "reuse" in the naive-cache baselines (FRFF / FRRF / FRRR);
# it is the same thing as 'x': a step the method may serve from cache.
s = schedule.strip().upper().replace("?", "X").replace("R", "X")
if len(s) != num_steps or set(s) - {"F", "X"}:
raise ValueError(f"schedule {schedule!r} must be {num_steps} chars of F/x")
if s[0] != "F":
raise ValueError(f"schedule {schedule!r}: step 0 must be F")
return tuple(i for i, c in enumerate(s) if c == "F")
def schedule_string(forced, num_steps):
return "".join("F" if i in forced else "x" for i in range(num_steps))
class CacheController:
def __init__(self, method, num_steps=4, forced_steps=(0, -1), first_chunk_forced_steps=None):
self.method = method
self.num_steps = num_steps
self.forced = {s % num_steps for s in forced_steps}
self.schedule = schedule_string(self.forced, num_steps)
cacheable = [s for s in range(num_steps) if s not in self.forced]
self.last_cacheable_step = max(cacheable) if cacheable else -1
# Chunk 0 may get its own schedule (usually all-full, possibly with more
# steps than the other chunks); it is not reduced mod num_steps.
self.first_chunk_forced = (None if first_chunk_forced_steps is None
else set(first_chunk_forced_steps))
self.first_chunk_schedule = None
if self.first_chunk_forced is not None:
n0 = max(num_steps, max(self.first_chunk_forced, default=-1) + 1)
self.first_chunk_schedule = schedule_string(self.first_chunk_forced, n0)
c0 = [s for s in range(n0) if s not in self.first_chunk_forced]
self.first_chunk_last_cacheable = max(c0) if c0 else -1
self.active = False
self.block_idx = -1
self.step_idx = -1
self.records = []
def reset_video(self):
self.method.reset_video()
self.records = []
def begin_chunk(self, block_idx):
self.block_idx = block_idx
self.method.begin_chunk(block_idx)
def denoise_step(self, step_idx):
self.step_idx = step_idx
self.active = True
def end_step(self):
self.active = False
def forced_now(self):
if self.first_chunk_forced is not None and self.block_idx == 0:
return self.first_chunk_forced
return self.forced
def run(self, ctx_kwargs):
first = self.first_chunk_forced is not None and self.block_idx == 0
ctx = StepCtx(block_idx=self.block_idx, step_idx=self.step_idx,
forced_full=self.step_idx in self.forced_now(),
last_cacheable_step=(self.first_chunk_last_cacheable if first
else self.last_cacheable_step), **ctx_kwargs)
img, frac = self.method.forward(ctx)
self.records.append({"block": self.block_idx, "step": self.step_idx,
"compute_fraction": float(frac)})
return img
def summary(self):
d = self.records
if not d:
return {}
compute = sum(r["compute_fraction"] for r in d)
middle = [r for r in d if r["step"] not in (self.first_chunk_forced if (
self.first_chunk_forced is not None and r["block"] == 0) else self.forced)]
# A token-wise method is only doing token-wise work while its cacheable
# steps are *partial*. Steps that select nothing (or everything) are
# behaviourally a whole-step skip (or a full step), so record how the
# budget is spread, not just how large it is.
active = [r["compute_fraction"] for r in middle
if 1e-9 < r["compute_fraction"] < 1 - 1e-9]
n_mid = len(middle) or 1
return {"denoise_forwards": len(d), "compute_equivalent_forwards": compute,
"middle_steps": len(middle),
"middle_compute_equivalent": sum(r["compute_fraction"] for r in middle),
"active_step_ratio": len(active) / n_mid,
"empty_step_ratio": sum(r["compute_fraction"] <= 1e-9 for r in middle) / n_mid,
"full_step_ratio": sum(r["compute_fraction"] >= 1 - 1e-9 for r in middle) / n_mid,
"mean_selected_fraction_active": (sum(active) / len(active)) if active else 0.0,
"flops_speedup_estimate": len(d) / compute if compute else float("inf")}
def _cached_forward_vision(self, hidden_states, timestep, timestep_r=None, freqs_cos=None,
freqs_sin=None, return_dict=False, mask_type="t2v",
extra_kwargs=None, action=None, viewmats=None, Ks=None,
kv_cache=None, cache_vision=False, rope_temporal_size=4,
start_rope_start_idx=0):
ctrl = getattr(self, "_cache_ctrl", None)
if ctrl is None or not ctrl.active or cache_vision:
return self._orig_forward_vision(
hidden_states=hidden_states, timestep=timestep, timestep_r=timestep_r,
freqs_cos=freqs_cos, freqs_sin=freqs_sin, return_dict=return_dict,
mask_type=mask_type, extra_kwargs=extra_kwargs, action=action,
viewmats=viewmats, Ks=Ks, kv_cache=kv_cache, cache_vision=cache_vision,
rope_temporal_size=rope_temporal_size, start_rope_start_idx=start_rope_start_idx)
assert not get_parallel_state().sp_enabled, "cache methods are single-GPU"
# -- preamble, verbatim from forward_vision -------------------------------------
img = x = hidden_states
t = timestep
bs, _, ot, oh, ow = x.shape
tt, th, tw = ot // self.patch_size[0], oh // self.patch_size[1], ow // self.patch_size[2]
self.attn_param["thw"] = [tt, th, tw]
rope_temporal_size = rope_temporal_size // self.patch_size[0]
if freqs_cos is None and freqs_sin is None:
freqs_cos, freqs_sin = self.get_rotary_pos_embed((rope_temporal_size, th, tw))
per_latent_size = th * tw
start_index = start_rope_start_idx * per_latent_size
end_index = (start_rope_start_idx + tt) * per_latent_size
freqs_cos = freqs_cos[start_index:end_index, ...]
freqs_sin = freqs_sin[start_index:end_index, ...]
img = self.img_in(img)
action = action.reshape(-1)
t = t.reshape(-1)
vec = self.time_in(t)
vec = vec + self.action_in(action)
vec = repeat(vec, "(B T) C->B (T H W) C", B=img.shape[0], H=th, W=tw)
viewmats = repeat(viewmats, "B T M N->B (T H W) M N", H=th, W=tw)
Ks = repeat(Ks, "B T M N->B (T H W) M N", H=th, W=tw)
vec = rearrange(vec, "B S C->(B S) C")
# get_rotary_pos_embed builds the tables on CPU and upstream moves them inside
# apply_rotary_emb; the selective path indexes them with a CUDA index first.
freqs_cis = (freqs_cos.to(img.device), freqs_sin.to(img.device))
img = ctrl.run(dict(model=self, img=img, vec=vec, freqs_cis=freqs_cis,
viewmats=viewmats, Ks=Ks, kv_cache=kv_cache, grid=(tt, th, tw)))
img = self.final_layer(img, vec)
img = self.unpatchify(img, tt, th, tw)
assert return_dict is False
return (img, None)
def install(transformer, controller):
if not hasattr(transformer, "_orig_forward_vision"):
transformer._orig_forward_vision = transformer.forward_vision
transformer.forward_vision = types.MethodType(_cached_forward_vision, transformer)
transformer._cache_ctrl = controller
return transformer
|