multimodalart's picture
multimodalart HF Staff
prep stage
1811aab verified
Raw History Blame Contribute Delete
16.3 kB
"""Single-GPU Ref2VA inference with a fixed first frame and Retake34 continuation."""
from __future__ import annotations
import gc
import os
from pathlib import Path
import time
from typing import Any
import uuid
from .cache import cache_paths, validate_cache
from .config import RunConfig
from .constants import (
AUDIO_CONDITION_CLEAN,
AUDIO_LATENT_SHAPE,
AUDIO_PREFIX_LATENTS,
AUDIO_SHIFT,
FPS,
OVERLAP_AUDIO_SAMPLES,
OVERLAP_FRAMES,
RECOVERY_SCHEMA,
REFERENCE_LATENT_SHAPE,
REFERENCE_RUNTIME_SHAPE,
RESULT_SCHEMA,
STRIDE_FRAMES,
TARGET_HEIGHT,
TARGET_WIDTH,
VIDEO_LATENT_SHAPE,
VIDEO_PREFIX_LATENTS,
VIDEO_SHIFT,
VISUAL_CONDITION_CLEAN,
WINDOW_AUDIO_SAMPLES,
WINDOW_FRAMES,
)
from .io import atomic_json
from .runtime import build_runtime_batch, strict_merge_lora
def _require_one_gpu(torch: Any) -> Any:
if not torch.cuda.is_available() or torch.cuda.device_count() != 1:
raise RuntimeError("generation requires exactly one visible CUDA GPU")
torch.cuda.set_device(0)
return torch.device("cuda:0")
def _unique_key(handle: Any, *, exact: str | None = None, prefix: str | None = None) -> str:
keys = list(handle.keys())
selected = [key for key in keys if key == exact] if exact else [key for key in keys if key.startswith(str(prefix))]
if len(selected) != 1:
raise ValueError(f"expected one tensor key, got {keys}")
return selected[0]
class Ref2VAEngine:
def __init__(self, config: RunConfig) -> None:
import torch
from musubi_tuner.minimax_h3.audio_vae import load_audio_vae
from musubi_tuner.minimax_h3.model import load_h3_transformer
from musubi_tuner.minimax_h3.video_vae import load_video_vae
from musubi_tuner.modules.custom_offloading_utils import BlockSwapConfig
self.config = config
self.torch = torch
self.device = _require_one_gpu(torch)
transformer = load_h3_transformer(
config.models.dit,
device="cpu",
dtype=torch.bfloat16,
attn_mode=config.attention_mode,
split_attn=False,
disable_mmap=False,
)
self.lora_gate = strict_merge_lora(
transformer, config.models.lora, config.expected_lora_modules
)
swap = BlockSwapConfig(device=self.device, supports_backward=False, use_pinned_memory=True)
transformer.enable_block_swap(config.blocks_to_swap, swap)
transformer.move_to_device_except_swap_blocks(self.device)
transformer.prepare_block_swap_before_forward()
transformer.switch_block_swap_for_inference()
transformer.eval().requires_grad_(False)
self.transformer = transformer
self.video_vae = load_video_vae(
config.models.video_vae, device=self.device, dtype=torch.float16, disable_mmap=False
).eval().requires_grad_(False)
self.audio_vae = load_audio_vae(
config.models.audio_vae, device=self.device, dtype=torch.float32, disable_mmap=False
).eval().requires_grad_(False)
def _load_window_cache(self, index: int):
import torch
from safetensors import safe_open
paths = cache_paths(self.config, index)
with safe_open(str(paths["anchor"]), framework="pt", device="cpu") as handle:
anchor = handle.get_tensor(_unique_key(handle, prefix="latents_ref_"))
with safe_open(str(paths["reference"]), framework="pt", device="cpu") as handle:
reference = handle.get_tensor(_unique_key(handle, prefix="latents_ref_"))
with safe_open(str(paths["qwen"]), framework="pt", device="cpu") as handle:
hidden = handle.get_tensor(_unique_key(handle, exact="varlen_mmh3_hidden_states_bfloat16"))
tags = handle.get_tensor(_unique_key(handle, exact="varlen_mmh3_token_tags_int64"))
if tuple(reference.shape) != REFERENCE_LATENT_SHAPE or reference.dtype != torch.float32:
raise ValueError(f"window {index} DUV reference cache shape/dtype changed")
reference = torch.nn.functional.pad(reference.unsqueeze(0), (0, 1, 0, 0), value=0.0).contiguous()
if tuple(reference.shape) != REFERENCE_RUNTIME_SHAPE:
raise ValueError(f"window {index} padded DUV reference shape changed")
if anchor.ndim != 4 or tuple(anchor.shape[:2]) != (24, 1) or anchor.dtype != torch.float32:
raise ValueError("anchor cache shape/dtype changed")
if hidden.ndim != 2 or hidden.shape[1] != 5120 or hidden.dtype != torch.bfloat16:
raise ValueError(f"window {index} Qwen hidden states changed")
if tags.shape != (hidden.shape[0],) or tags.dtype != torch.int64:
raise ValueError(f"window {index} Qwen token tags changed")
if not all(torch.isfinite(value).all().item() for value in (anchor, reference, hidden)):
raise ValueError(f"window {index} cache contains non-finite values")
return anchor.unsqueeze(0), reference, hidden.unsqueeze(0), tags.unsqueeze(0)
def _load_input_video(self):
import torch
from safetensors import safe_open
path = cache_paths(self.config, 0)["input_video"]
with safe_open(str(path), framework="pt", device="cpu") as handle:
latent = handle.get_tensor(_unique_key(handle, exact="input_video_latents"))
if tuple(latent.shape) != VIDEO_LATENT_SHAPE[1:] or latent.dtype != torch.float32:
raise ValueError("fixed-first-frame latent shape/dtype changed")
if torch.count_nonzero(latent[:, 1:]).item() != 0:
raise ValueError("fixed-first-frame latent must contain data only at temporal index 0")
latent = latent.unsqueeze(0).to(self.device)
mask = torch.ones_like(latent, dtype=torch.bool)
mask[:, :, 0] = False
return latent, mask
def _recovery_path(self, index: int) -> Path:
return self.config.cache_dir / "recovery" / f"window_{index:03d}.safetensors"
def _load_recovery(self, index: int):
import torch
from safetensors import safe_open
path = self._recovery_path(index)
with safe_open(str(path), framework="pt", device="cpu") as handle:
metadata = handle.metadata() or {}
if metadata.get("schema") != RECOVERY_SCHEMA or metadata.get("window_index") != str(index):
raise ValueError(f"recovery identity changed: {path}")
video = handle.get_tensor("video")
audio = handle.get_tensor("audio")
if tuple(video.shape) != VIDEO_LATENT_SHAPE[1:] or tuple(audio.shape) != AUDIO_LATENT_SHAPE[1:]:
raise ValueError(f"recovery shape changed: {path}")
if video.dtype != torch.float32 or audio.dtype != torch.float32:
raise ValueError(f"recovery dtype changed: {path}")
if not torch.isfinite(video).all().item() or not torch.isfinite(audio).all().item():
raise ValueError(f"recovery contains non-finite values: {path}")
return video, audio
def _retake_inputs(self, previous: tuple[Any, Any]):
torch = self.torch
previous_video, previous_audio = previous
torch.cuda.empty_cache()
with torch.inference_mode():
decoded_video = self.video_vae.decode(
previous_video.unsqueeze(0).to(self.device, torch.float16)
)[:, :, :WINDOW_FRAMES]
if tuple(decoded_video.shape) != (1, 3, WINDOW_FRAMES, TARGET_HEIGHT, TARGET_WIDTH):
raise RuntimeError("Retake34 decoded video shape changed")
tail = decoded_video[:, :, -OVERLAP_FRAMES:]
padded = torch.cat((tail, tail[:, :, -1:].repeat(1, 1, STRIDE_FRAMES, 1, 1)), dim=2)
clean_video = self.video_vae.encode(padded.to(torch.float16)).float()
decoded_audio = self.audio_vae.decode(
previous_audio.unsqueeze(0).to(self.device, torch.float32)
)[:, :, :WINDOW_AUDIO_SAMPLES]
if tuple(decoded_audio.shape) != (1, 2, WINDOW_AUDIO_SAMPLES):
raise RuntimeError("Retake34 decoded audio shape changed")
prefix = decoded_audio[:, :, -OVERLAP_AUDIO_SAMPLES:]
prefix_audio = self.audio_vae.encode(prefix.to(torch.float32)).float()
if prefix_audio.shape[-1] != AUDIO_PREFIX_LATENTS:
raise RuntimeError("Retake34 audio prefix shape changed")
clean_audio = torch.cat(
(prefix_audio, prefix_audio[..., -1:].repeat(1, 1, 1, AUDIO_LATENT_SHAPE[-1] - AUDIO_PREFIX_LATENTS)),
dim=-1,
)
if tuple(clean_video.shape) != VIDEO_LATENT_SHAPE or tuple(clean_audio.shape) != AUDIO_LATENT_SHAPE:
raise RuntimeError("Retake34 clean latent shape changed")
video_mask = torch.ones_like(clean_video, dtype=torch.bool)
video_mask[:, :, :VIDEO_PREFIX_LATENTS] = False
audio_mask = torch.ones_like(clean_audio, dtype=torch.bool)
audio_mask[..., :AUDIO_PREFIX_LATENTS] = False
return clean_video, video_mask, clean_audio, audio_mask
def sample_window(self, index: int) -> Path:
import torch
from musubi_tuner.minimax_h3.sampling import initialize_target_latents, sample_joint_av
from safetensors.torch import save_file
output = self._recovery_path(index)
if output.exists():
self._load_recovery(index)
return output
anchor, reference, hidden, tags = self._load_window_cache(index)
runtime = build_runtime_batch(anchor, reference, hidden, tags)
window_seed = self.config.seed + index
initial_video, initial_audio = initialize_target_latents(
video_shape=VIDEO_LATENT_SHAPE,
audio_shape=AUDIO_LATENT_SHAPE,
seed=window_seed,
device=self.device,
video_dtype=torch.float32,
audio_dtype=torch.float32,
)
ref = reference.to(self.device, torch.float32)
ref_generator = torch.Generator(device="cpu").manual_seed(self.config.seed + 1000 + index)
ref_noise = torch.randn(tuple(ref.shape), generator=ref_generator, dtype=torch.float32).to(self.device)
augmented_ref = VISUAL_CONDITION_CLEAN * ref + (1.0 - VISUAL_CONDITION_CLEAN) * ref_noise
anchor_ref = anchor.to(self.device, torch.float32)
anchor_generator = torch.Generator(device="cpu").manual_seed(self.config.seed + 3000 + index)
anchor_noise = torch.randn(tuple(anchor_ref.shape), generator=anchor_generator, dtype=torch.float32).to(self.device)
augmented_anchor = VISUAL_CONDITION_CLEAN * anchor_ref + (1.0 - VISUAL_CONDITION_CLEAN) * anchor_noise
retake = self._load_input_video() + (None, None) if index == 0 else self._retake_inputs(self._load_recovery(index - 1))
with torch.inference_mode():
sampled = sample_joint_av(
self.transformer,
layout=runtime.layout,
text_hidden_states=runtime.text_hidden_states.to(self.device, torch.bfloat16),
text_token_tags=runtime.text_token_tags.to(self.device),
initial_video=initial_video,
initial_audio=initial_audio,
steps=self.config.steps,
video_shift=VIDEO_SHIFT,
audio_shift=AUDIO_SHIFT,
visual_condition_latents=(augmented_anchor, augmented_ref),
audio_condition_latents=(),
visual_condition_clean=VISUAL_CONDITION_CLEAN,
audio_condition_clean=AUDIO_CONDITION_CLEAN,
input_video_latents=retake[0],
video_denoise_mask=retake[1],
input_audio_latents=retake[2],
audio_denoise_mask=retake[3],
)
video = sampled.video[0].float().cpu().contiguous()
audio = sampled.audio[0].float().cpu().contiguous()
if not torch.isfinite(video).all().item() or not torch.isfinite(audio).all().item():
raise RuntimeError(f"window {index} produced non-finite latents")
output.parent.mkdir(parents=True, exist_ok=True)
temporary = output.with_name(f".{output.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
try:
save_file(
{"video": video, "audio": audio},
str(temporary),
metadata={
"schema": RECOVERY_SCHEMA,
"window_index": str(index),
"seed": str(window_seed),
"steps": str(self.config.steps),
},
)
os.replace(temporary, output)
finally:
temporary.unlink(missing_ok=True)
del sampled, video, audio, anchor, reference, hidden, tags, runtime
del initial_video, initial_audio, ref, ref_noise, augmented_ref
del anchor_ref, anchor_noise, augmented_anchor, retake
gc.collect()
torch.cuda.empty_cache()
return output
def write_video(self) -> dict[str, Any]:
import av
import torch
output = self.config.output
if output.exists():
raise FileExistsError(f"refusing to overwrite output: {output}")
output.parent.mkdir(parents=True, exist_ok=True)
temporary = output.with_name(f".{output.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp.mp4")
frame_count = 0
try:
with av.open(str(temporary), mode="w") as container:
stream = container.add_stream("libx264", rate=FPS)
stream.width = TARGET_WIDTH
stream.height = TARGET_HEIGHT
stream.pix_fmt = "yuv420p"
for index in range(len(self.config.windows)):
video, _audio = self._load_recovery(index)
torch.cuda.empty_cache()
with torch.inference_mode():
decoded = self.video_vae.decode(
video.unsqueeze(0).to(self.device, torch.float16)
)[0, :, :WINDOW_FRAMES].cpu()
pixels = (
((decoded.float().clamp(-1, 1) + 1.0) * 127.5)
.round()
.to(torch.uint8)
.permute(1, 2, 3, 0)
.contiguous()
)
if index:
pixels = pixels[OVERLAP_FRAMES:]
for value in pixels:
frame = av.VideoFrame.from_ndarray(value.numpy(), format="rgb24")
for packet in stream.encode(frame):
container.mux(packet)
frame_count += 1
del video, decoded, pixels
gc.collect()
torch.cuda.empty_cache()
for packet in stream.encode():
container.mux(packet)
if frame_count != self.config.output_frames:
raise RuntimeError(f"stitched frame count mismatch: {frame_count}")
os.replace(temporary, output)
finally:
temporary.unlink(missing_ok=True)
receipt = {
"schema": RESULT_SCHEMA,
"solver": "explicit_euler",
"steps": self.config.steps,
"seed": self.config.seed,
"window_frames": WINDOW_FRAMES,
"overlap_frames": OVERLAP_FRAMES,
"output_frames": frame_count,
"fps": FPS,
"resolution": [TARGET_WIDTH, TARGET_HEIGHT],
"audio": False,
"output": str(output),
"bytes": output.stat().st_size,
"lora_gate": self.lora_gate,
}
atomic_json(output.with_suffix(output.suffix + ".json"), receipt)
return receipt
def generate(config: RunConfig) -> dict[str, Any]:
validate_cache(config)
started = time.monotonic()
engine = Ref2VAEngine(config)
for index in range(len(config.windows)):
engine.sample_window(index)
result = engine.write_video()
result["elapsed_seconds"] = time.monotonic() - started
return result