Files
2026-07-13 12:31:40 +08:00

313 lines
12 KiB
Python

# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES
#
# Licensed under the Apache License, Version 2.0 (the "License").
# You may not use this file except in compliance with the License.
# To view a copy of this license, visit http://www.apache.org/licenses/LICENSE-2.0
#
# No warranties are given. The work is provided "AS IS", without warranty of any kind, express or implied.
#
# SPDX-License-Identifier: Apache-2.0
"""Small helpers for release inference examples."""
from __future__ import annotations
import os
from collections.abc import Mapping
from pathlib import Path
from typing import Sequence
import torch
from einops import rearrange
from torchvision.io import write_video
from utils.nvfp4_checkpoint import (
clean_fsdp_state_dict_keys,
drop_fouroversix_master_weights,
is_nvfp4_state_dict,
is_te_nvfp4_checkpoint,
quantize_model_for_fouroversix_nvfp4,
quantize_model_for_transformer_engine_nvfp4,
unwrap_generator_state_dict,
)
def _torch_load(path: str):
try:
return torch.load(path, map_location="cpu", weights_only=False)
except TypeError:
return torch.load(path, map_location="cpu")
def load_generator_checkpoint(generator, checkpoint_path: str, *, use_ema: bool = False, strict: bool | None = None):
"""Load a LongLive generator checkpoint into ``generator``."""
checkpoint = _torch_load(checkpoint_path)
state_dict = unwrap_generator_state_dict(checkpoint, use_ema=use_ema)
if use_ema:
state_dict = clean_fsdp_state_dict_keys(state_dict)
if strict is None:
strict = not use_ema
return generator.load_state_dict(state_dict, strict=strict)
def _load_lora_state_dict(lora_ckpt_path: str) -> Mapping[str, torch.Tensor]:
"""Load a LoRA checkpoint, unwrapping ``generator_lora`` when present."""
checkpoint = _torch_load(lora_ckpt_path)
if isinstance(checkpoint, Mapping) and "generator_lora" in checkpoint:
return checkpoint["generator_lora"]
return checkpoint
def apply_and_merge_lora(
pipeline,
config,
*,
device: torch.device | str | None = None,
dtype: torch.dtype = torch.bfloat16,
verbose: bool = False,
):
"""Wrap ``pipeline.generator.model`` with a LoRA adapter, load weights, and merge.
The merged module ends up structurally identical to the original generator
(``nn.Linear`` layers carrying the base + LoRA delta), which is what NVFP4
quantization needs as its starting point.
Returns ``True`` when LoRA was applied and merged, ``False`` when the config
did not request a LoRA adapter.
"""
adapter_cfg = getattr(config, "adapter", None)
lora_ckpt = getattr(config, "lora_ckpt", None)
if adapter_cfg is None or not lora_ckpt:
return False
import peft
from utils.lora_utils import configure_lora_for_model
if device is not None:
pipeline.generator.to(device=torch.device(device), dtype=dtype)
else:
pipeline.generator.to(dtype=dtype)
if verbose:
print(f"[LoRA] Wrapping generator with adapter config: {adapter_cfg}")
pipeline.generator.model = configure_lora_for_model(
pipeline.generator.model,
model_name="generator",
lora_config=adapter_cfg,
is_main_process=verbose,
)
if verbose:
print(f"[LoRA] Loading LoRA weights from: {lora_ckpt}")
lora_state = _load_lora_state_dict(lora_ckpt)
peft.set_peft_model_state_dict(pipeline.generator.model, lora_state) # type: ignore[arg-type]
if verbose:
print("[LoRA] Merging LoRA delta into base weights (merge_and_unload)...")
pipeline.generator.model = pipeline.generator.model.merge_and_unload(safe_merge=True)
pipeline.generator.model.eval().requires_grad_(False)
pipeline.is_lora_enabled = False
pipeline.is_lora_merged = True
return True
def place_vae_for_streaming(pipeline, config) -> torch.device | None:
"""Move ``pipeline.vae`` to ``config.vae_device`` for streaming-pipeline decode.
Only acts when both ``streaming_vae`` and ``vae_device`` are set; otherwise
leaves the VAE on whatever device the rest of the pipeline already uses.
Mirrors the relocation done in ``inference.py`` so that quick-start scripts
can opt in to the streaming-pipeline VAE simply by enabling those config
fields.
"""
if not bool(getattr(config, "streaming_vae", False)):
return None
vae_device_str = getattr(config, "vae_device", None)
if not vae_device_str:
return None
vae_device = torch.device(vae_device_str)
pipeline.vae.to(device="cpu")
pipeline.vae.to(device=vae_device)
if hasattr(pipeline.vae, "mean"):
pipeline.vae.mean = pipeline.vae.mean.to(device=vae_device)
pipeline.vae.std = pipeline.vae.std.to(device=vae_device)
return vae_device
def setup_nvfp4_pipeline(
pipeline,
config,
device: torch.device | str,
*,
verbose: bool = False,
):
"""Configure ``pipeline`` for NVFP4 inference from a generator checkpoint.
Handles both supported NVFP4 backends:
* ``model_quant_use_transformer_engine=True`` -> a BF16 generator checkpoint
that gets wrapped with TransformerEngine NVFP4 modules and materialized
after moving to ``device``.
* ``model_quant_use_transformer_engine=False`` -> either a BF16 generator
checkpoint that gets quantized with FourOverSix at load time, or a
pre-materialized FourOverSix NVFP4 state dict loaded directly into the
already-quantized architecture.
Optional LoRA support (BF16 base only): when ``config.adapter`` and
``config.lora_ckpt`` are both set, the LoRA adapter is loaded on the BF16
base generator, merged via ``merge_and_unload``, and the resulting weights
are then quantized — so the same yaml can swap between TE and FourOverSix
backends without pre-merging the LoRA checkpoint.
For materialized FourOverSix checkpoints LoRA cannot be applied (the master
weights have already been quantized away); ``lora_ckpt``/``adapter`` are
ignored in that case with a printed warning.
"""
if not bool(getattr(config, "model_quant", False)):
raise ValueError("setup_nvfp4_pipeline requires model_quant=true in the config.")
generator_ckpt = getattr(config, "generator_ckpt", None)
if not generator_ckpt:
raise ValueError("checkpoints.generator_ckpt is required for NVFP4 inference.")
use_te = bool(getattr(config, "model_quant_use_transformer_engine", False))
device = torch.device(device)
use_ema = bool(getattr(config, "use_ema", False))
checkpoint = _torch_load(generator_ckpt)
state_dict = unwrap_generator_state_dict(checkpoint, use_ema=use_ema)
if use_ema:
state_dict = clean_fsdp_state_dict_keys(state_dict)
if is_te_nvfp4_checkpoint(checkpoint):
raise ValueError(
"Detected a TransformerEngine module state_dict export (no longer supported). "
"Re-export with `--backend transformer_engine` (merged BF16) or `--backend fouroversix`."
)
is_prequantized = is_nvfp4_state_dict(state_dict)
has_lora_request = bool(getattr(config, "adapter", None)) and bool(getattr(config, "lora_ckpt", None))
pipeline.is_lora_enabled = False
pipeline.is_lora_merged = False
if is_prequantized:
if has_lora_request and verbose:
print(
"[NVFP4] generator_ckpt is a materialized FourOverSix NVFP4 checkpoint; "
"ignoring lora_ckpt/adapter because the master weights are already quantized. "
"Use a BF16 base checkpoint if you need to load a LoRA on top."
)
if use_te:
raise ValueError(
"generator_ckpt is a materialized NVFP4 (FourOverSix) checkpoint; set "
"model_quant_use_transformer_engine: false."
)
pipeline.generator.model, _ = quantize_model_for_fouroversix_nvfp4(
pipeline.generator.model,
config=config,
keep_master_weights=False,
verbose=verbose,
)
drop_fouroversix_master_weights(pipeline.generator.model)
pipeline.generator.load_state_dict(state_dict, strict=True)
pipeline.text_encoder.to(dtype=torch.bfloat16)
pipeline.vae.to(dtype=torch.bfloat16)
else:
load_strict = not use_ema
pipeline.generator.load_state_dict(state_dict, strict=load_strict)
if has_lora_request:
# Apply + merge LoRA on the BF16 base before quantization. Move the
# generator to CUDA first so the TE wrapper (which requires CUDA
# modules) can later replace the merged Linear layers in-place.
apply_and_merge_lora(
pipeline,
config,
device=device,
dtype=torch.bfloat16,
verbose=verbose,
)
if use_te:
pipeline.generator.model, _ = quantize_model_for_transformer_engine_nvfp4(
pipeline.generator.model,
config=config,
keep_master_weights=False,
verbose=verbose,
)
te_fallback = bool(getattr(config, "model_quant_te_fallback_to_fouroversix", False))
if te_fallback:
from utils.quant import _materialize_mixed_quantized_weights_for_inference as materialize_fn
else:
from utils.quant import _materialize_transformer_engine_weights_for_inference as materialize_fn
else:
pipeline.generator.model, _ = quantize_model_for_fouroversix_nvfp4(
pipeline.generator.model,
config=config,
keep_master_weights=False,
verbose=verbose,
)
from utils.quant import _materialize_quantized_weights_for_inference as materialize_fn
pipeline.to(dtype=torch.bfloat16)
materialize_fn(pipeline.generator.model, target_device=device)
pipeline.generator.to(device=device)
pipeline.text_encoder.to(device=device)
pipeline.vae.to(device=device)
place_vae_for_streaming(pipeline, config)
return pipeline
def prepare_single_prompt_inputs(
config,
prompt: str,
device: torch.device | str,
*,
dtype: torch.dtype = torch.bfloat16,
batch_size: int = 1,
generator: torch.Generator | None = None,
):
"""Create the per-block prompt list and latent noise for one text prompt."""
num_frames = int(getattr(config, "num_output_frames", config.image_or_video_shape[1]))
frames_per_block = int(getattr(config, "num_frame_per_block", 1))
if num_frames % frames_per_block != 0:
raise ValueError(f"num_frames={num_frames} must be divisible by num_frame_per_block={frames_per_block}")
latent_shape = list(config.image_or_video_shape[2:])
if len(latent_shape) != 3:
raise ValueError(f"Expected latent shape [C, H, W], got {latent_shape}")
num_blocks = num_frames // frames_per_block
prompts = [[prompt] * num_blocks for _ in range(batch_size)]
noise = torch.randn(
[batch_size, num_frames, *latent_shape],
device=device,
dtype=dtype,
generator=generator,
)
return noise, prompts
def video_to_uint8(video: torch.Tensor) -> torch.Tensor:
"""Convert a generated video tensor from [T, C, H, W] or [1, T, C, H, W] to uint8 THWC."""
if video.ndim == 5:
if video.shape[0] != 1:
raise ValueError("video_to_uint8 expects a single sample when a batch dimension is present.")
video = video[0]
if video.ndim != 4:
raise ValueError(f"Expected video tensor with 4 dims, got shape={tuple(video.shape)}")
if video.shape[1] in (1, 3):
video = rearrange(video, "t c h w -> t h w c")
return (255.0 * video.cpu()).clamp(0, 255).to(torch.uint8)
def save_video(video: torch.Tensor, output_path: str | os.PathLike, *, fps: int = 24) -> None:
"""Save a generated LongLive video tensor as an mp4 file."""
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
write_video(str(output_path), video_to_uint8(video), fps=fps)