File size: 10,211 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 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | """Model loading, validation cases, camera poses and per-forward timing for the
HY-WorldPlay base.
Run inside the ``hy_worldplay_july20260720`` environment with the upstream clone
on ``sys.path`` (``enter_repo`` does that). Weights come from the
HY-WorldPlay-DEV-Predictor project's HF cache; the validation cases (25 VBench
I2V images cropped to 832x480, 4 camera actions each) come from that project's
``hyworldplay_predictor_vbench_val25_test50`` dataset.
"""
import json
import os
import sys
import time
from types import SimpleNamespace
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
REPO = os.path.join(ROOT, "repos/HY-WorldPlay")
DEV = "/local/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
HF = "/local/zoubin/cz/checkpoints/hy_worldplay/huggingface/hub"
BASE_MODEL = os.path.join(
HF, "models--tencent--HunyuanVideo-1.5/snapshots/9b49404b3f5df2a8f0b31df27a0c7ab872e7b038")
ACTION_CKPT = os.path.join(
HF, "models--tencent--HY-WorldPlay/snapshots/f4c29235647707b571479a69b569e4166f9f5bf8/"
"ar_distilled_action_model/diffusion_pytorch_model.safetensors")
DATASET = os.path.join(DEV, "datasets/hyworldplay_predictor_vbench_val25_test50")
# The four bidirectional camera actions of the validation protocol, for 32 latents
# (125 frames). ``pose_for_latents`` rescales them for shorter search videos.
ACTIONS = (("w_s", "w", "s"), ("a_d", "a", "d"), ("up_down", "up", "down"),
("left_right", "left", "right"))
NUM_STEPS = 4
CHUNK_LATENTS = 4
def enter_repo():
if REPO not in sys.path:
sys.path.insert(0, REPO)
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
# 125-frame videos sit at ~71 GB with the stock pipeline; without expandable
# segments the allocator fragments another ~9 GB and the cache banks do not fit.
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
os.environ.setdefault("HF_HUB_OFFLINE", "1")
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
def init_state():
"""Single-process inference state identical to the DEV project's ``init_state``."""
from hyvideo.commons.infer_state import initialize_infer_state
initialize_infer_state(SimpleNamespace(
sage_blocks_range="0-53", use_sageattn=False, enable_torch_compile=False,
use_fp8_gemm=False, quant_type="fp8-per-block", include_patterns="double_blocks",
use_vae_parallel=False))
def load_pipeline(device=None):
import torch
from hyvideo.pipelines.worldplay_video_pipeline import HunyuanVideo_1_5_Pipeline
init_state()
pipe = HunyuanVideo_1_5_Pipeline.create_pipeline(
pretrained_model_name_or_path=BASE_MODEL,
transformer_version="480p_i2v",
enable_offloading=True, # text encoder / VAE live on CPU between uses
enable_group_offloading=False,
create_sr_pipeline=False,
force_sparse_attn=False,
transformer_dtype=torch.bfloat16,
action_ckpt=ACTION_CKPT,
)
pipe.transformer.eval().requires_grad_(False)
return pipe
def pose_for_latents(action, num_latents):
"""``"w-15,s-16"`` for 32 latents; the first move gets floor((n-1)/2) latents,
the reverse move the rest, so the video still turns around half-way."""
_, fwd, back = action
n = num_latents - 1
return f"{fwd}-{n // 2},{back}-{n - n // 2}"
def load_cases(split="validation"):
"""The 25 validation (or 50 test) cases with image paths on this machine."""
rows = []
with open(os.path.join(DATASET, "cases.jsonl")) as f:
for line in f:
r = json.loads(line)
if r["split"] != split:
continue
r["image_path"] = os.path.join(DATASET, split, "input_images",
f"case_{r['split_case_id']:04d}.jpg")
rows.append(r)
rows.sort(key=lambda r: r["split_case_id"])
return rows
class ForwardTimer:
"""Wraps ``pipe.transformer.forward`` with CUDA events and classifies every call:
text-cache pass, per-chunk context pass (``cache_vision=True``), or a denoising
forward. Only the denoising forwards make up ``denoise_dit_ms``; the context
passes are the analogue of Self-Forcing's KV-refresh pass and are reported
separately, never added in. Also tells the cache controller where it is."""
def __init__(self, transformer, controller=None, num_steps=NUM_STEPS, first_chunk_steps=None):
import torch
self.transformer = transformer
self.controller = controller
self.num_steps = num_steps
# chunk 0 may run a different number of denoising steps than the rest
self.first_chunk_steps = first_chunk_steps or num_steps
self.torch = torch
self.reset()
def reset(self):
self.events = {"denoise": [], "context": [], "text": []}
self.denoise_calls = 0
if self.controller is not None:
self.controller.reset_video()
def __enter__(self):
self._orig = self.transformer.forward
timer = self
def forward(*args, **kw):
torch = timer.torch
if kw.get("ar_txt_inference"):
kind = "text"
elif kw.get("cache_vision"):
kind = "context"
else:
kind = "denoise"
if kind == "denoise" and timer.controller is not None:
if timer.denoise_calls < timer.first_chunk_steps:
block, step = 0, timer.denoise_calls
else:
rest = timer.denoise_calls - timer.first_chunk_steps
block, step = 1 + rest // timer.num_steps, rest % timer.num_steps
if step == 0:
timer.controller.begin_chunk(block)
timer.controller.denoise_step(step)
s = torch.cuda.Event(enable_timing=True)
e = torch.cuda.Event(enable_timing=True)
s.record()
out = timer._orig(*args, **kw)
e.record()
timer.events[kind].append((s, e))
if kind == "denoise":
timer.denoise_calls += 1
if timer.controller is not None:
timer.controller.end_step()
return out
self.transformer.forward = forward
return self
def __exit__(self, *exc):
self.transformer.forward = self._orig
def summary(self):
self.torch.cuda.synchronize()
per = {k: [s.elapsed_time(e) for s, e in v] for k, v in self.events.items()}
return {
"denoise_dit_ms": sum(per["denoise"]),
"per_step_denoise_ms": per["denoise"],
"num_denoise_forwards": len(per["denoise"]),
"context_kv_dit_ms": sum(per["context"]),
"num_context_forwards": len(per["context"]),
"text_cache_ms": sum(per["text"]),
}
class _ChunkTimesteps:
"""Stands in for the pipeline's ``timesteps`` tensor so that chunk 0 iterates a
longer schedule than the following chunks. ``_ar_rollout_inner`` only iterates
it (once per chunk), reads ``.dtype`` and ``len()``; the scheduler is re-pointed
at the matching grid at the start of every chunk (the pipeline itself calls
``set_timesteps(num_inference_steps)`` just before, so this always wins)."""
def __init__(self, scheduler, first_n, n, device, extra):
self.scheduler, self.first_n, self.n, self.device, self.extra = scheduler, first_n, n, device, extra
self.calls = 0
self.dtype = scheduler.timesteps.dtype
def __len__(self):
return self.n
def __iter__(self):
k = self.first_n if self.calls == 0 else self.n
self.calls += 1
self.scheduler.set_timesteps(k, device=self.device, **self.extra)
return iter(self.scheduler.timesteps)
def generate(pipe, case, action, controller=None, num_frames=125, seed=0, decode=True,
num_steps=NUM_STEPS, first_chunk_steps=None):
"""One video. Returns (video [1,C,T,H,W] in [0,1] or None, latents, timing).
``first_chunk_steps`` gives chunk 0 its own step count (the others run ``num_steps``)."""
import torch
from hyvideo.generate import pose_to_input
import hyvideo.pipelines.worldplay_video_pipeline as wp
patched = first_chunk_steps is not None and first_chunk_steps != num_steps
orig_retrieve = wp.retrieve_timesteps
def retrieve_first_chunk(scheduler, n, device=None, timesteps=None, sigmas=None, **kw):
ts, n_out = orig_retrieve(scheduler, n, device, timesteps=timesteps, sigmas=sigmas, **kw)
return _ChunkTimesteps(scheduler, first_chunk_steps, n_out, device, kw), n_out
num_latents = (num_frames - 1) // 4 + 1
assert num_latents % CHUNK_LATENTS == 0
pose = pose_for_latents(action, num_latents)
viewmats, Ks, act = pose_to_input(pose, num_latents)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
if patched:
wp.retrieve_timesteps = retrieve_first_chunk
try:
with ForwardTimer(pipe.transformer, controller, num_steps=num_steps,
first_chunk_steps=first_chunk_steps) as timer, torch.no_grad():
wall0 = time.time()
out = pipe(
enable_sr=False, prompt=case["caption"], aspect_ratio="16:9",
num_inference_steps=num_steps, video_length=num_frames, negative_prompt="",
seed=seed, output_type="pt" if decode else "latent", prompt_rewrite=False,
return_pre_sr_video=True, reference_image=case["image_path"],
viewmats=viewmats.unsqueeze(0), Ks=Ks.unsqueeze(0), action=act.unsqueeze(0),
few_step=True, chunk_latent_frames=CHUNK_LATENTS, model_type="ar",
user_height=480, user_width=832, transformer_resident_ar_rollout=True,
)
wall = time.time() - wall0
finally:
if patched:
wp.retrieve_timesteps = orig_retrieve
timing = timer.summary()
timing["wall_s"] = wall
timing["peak_mem_gib"] = torch.cuda.max_memory_allocated() / 1024 ** 3
video = out.videos if decode else None
latents = None if decode else out.videos
return video, latents, timing
|