lip-forcing / scripts /inference /_loader.py
multimodalart's picture
multimodalart HF Staff
Initial Lip Forcing 14B streaming demo
9368ee7 verified
Raw
History Blame Contribute Delete
7.03 kB
"""Shared student-model loader for the inference scripts.
``inference_segmentwise.py`` and ``inference_streaming.py`` both import
``load_diffusion_model`` from here so the 3-source assembly (base Wan 2.1 T2V +
OmniAvatar-LS adapter + Self-Forcing student) and the post-load LoRA merge live
in exactly one place.
This module intentionally keeps only light top-level deps (``os``, ``torch``);
the heavy model import is lazy (inside the function), so tools that merely need
the loader (e.g. ``scripts/export_merged_checkpoint.py``) don't pull in the
video/audio stack.
"""
import os
import torch
def load_diffusion_model(args, device, dtype):
"""Load the CausalOmniAvatarWan student model.
Constructs the model, loads base Wan + OmniAvatar weights (if paths given),
then overlays the Self-Forcing trained checkpoint on top.
Returns:
CausalOmniAvatarWan model in eval mode on *device*.
"""
from lipforcing.networks.OmniAvatar.network_causal import CausalOmniAvatarWan
# 14B SF LoRA: construct with merge_lora=False so PEFT is injected and
# the model exposes lora_A/lora_B keys that the trainable-filtered SF
# state_dict expects. After load_state_dict the LoRA values are
# populated; we then merge them into base for inference speed (unless
# --no_merge_lora_post_load is passed).
#
# For 1.3B, merge_lora=True folds the OmniAvatar V2V LoRA into base at
# construction, so the SF checkpoint is expected to have plain
# (already-merged) keys.
constructor_merge_lora = (args.model_size == "1.3B")
model = CausalOmniAvatarWan(
model_size=args.model_size,
in_dim=65,
mode="v2v",
use_audio=True,
audio_hidden_size=32,
chunk_size=args.chunk_size,
total_num_frames=21,
base_model_paths=args.base_model_paths,
omniavatar_ckpt_path=args.omniavatar_ckpt_path,
merge_lora=constructor_merge_lora,
lora_rank=128,
lora_alpha=64,
net_pred_type="flow",
schedule_type="rf",
mask_all_frames=True,
dtype=args.dtype,
local_attn_size=args.local_attn_size,
sink_size=args.sink_size,
use_dynamic_rope=args.use_dynamic_rope,
)
# Load Self-Forcing checkpoint on top
# Supports: regular .pt/.pth, FSDP distcp directory, or .pth + adjacent distcp dir
print(f"Loading SF checkpoint from {args.ckpt_path} ...")
state_dict = None
# Check for FSDP distributed checkpoint: look for .net_model/ directory
ckpt_stem = args.ckpt_path.replace(".pth", "")
fsdp_net_dir = ckpt_stem + ".net_model"
if os.path.isdir(fsdp_net_dir):
# FSDP2 distributed checkpoint — load via torch.distributed.checkpoint
print(f" Loading FSDP distributed checkpoint from {fsdp_net_dir} ...")
from torch.distributed.checkpoint import FileSystemReader
from torch.distributed.checkpoint.state_dict_loader import load as dcp_load
reader = FileSystemReader(fsdp_net_dir)
md = reader.read_metadata()
state_dict = {}
for key, meta in md.state_dict_metadata.items():
if hasattr(meta, "size"):
state_dict[key] = torch.empty(meta.size)
dcp_load(state_dict, storage_reader=reader, no_dist=True)
print(f" Loaded {len(state_dict)} tensors from FSDP distcp")
elif os.path.isdir(args.ckpt_path):
# Direct distcp directory path
from torch.distributed.checkpoint import FileSystemReader
from torch.distributed.checkpoint.state_dict_loader import load as dcp_load
reader = FileSystemReader(args.ckpt_path)
md = reader.read_metadata()
state_dict = {}
for key, meta in md.state_dict_metadata.items():
if hasattr(meta, "size"):
state_dict[key] = torch.empty(meta.size)
dcp_load(state_dict, storage_reader=reader, no_dist=True)
print(f" Loaded {len(state_dict)} tensors from distcp directory")
else:
# Regular .pt/.pth checkpoint
ckpt = torch.load(args.ckpt_path, map_location="cpu", weights_only=False)
if isinstance(ckpt, dict):
if "model" in ckpt and isinstance(ckpt["model"], dict) and "net" in ckpt["model"]:
state_dict = ckpt["model"]["net"]
elif "net" in ckpt:
state_dict = ckpt["net"]
else:
state_dict = ckpt
else:
state_dict = ckpt
# Keys in checkpoint use plain names (e.g. "patch_embedding.weight")
# Model wraps everything under _core, so keys are "_core.xxx"
# Try loading directly first, if too many missing, try adding _core prefix
missing, unexpected = model.load_state_dict(state_dict, strict=False)
if len(missing) > len(state_dict) * 0.5 and not any(k.startswith("_core.") for k in state_dict):
# Try with _core prefix
prefixed_sd = {"_core." + k: v for k, v in state_dict.items()}
missing2, unexpected2 = model.load_state_dict(prefixed_sd, strict=False)
if len(missing2) < len(missing):
missing, unexpected = missing2, unexpected2
print(" Applied _core. prefix for key matching")
print(f" SF checkpoint: {len(state_dict)} params, {len(missing)} missing, {len(unexpected)} unexpected")
# Merge LoRA into base post-load (14B path only; the 1.3B path constructs
# with merge_lora=True so there's nothing to merge here).
#
# Uses PEFT's per-layer merge() which fuses LoRA A/B into the wrapped
# base_layer.weight in-place. After merge, LoraLinear.forward dispatches
# to base_layer.forward and the LoRA path is a no-op — same numeric
# result as not having LoRA at all. We keep the LoraLinear wrappers in
# place (no unload) because that's a structural change and not necessary
# for correctness; only the merge_count is reported.
if args.model_size == "14B" and args.merge_lora_post_load:
print(" Merging LoRA into base for inference speed...")
from peft.tuners.lora import LoraLayer
merge_count = 0
for name, module in model.named_modules():
if isinstance(module, LoraLayer):
module.merge()
merge_count += 1
if merge_count > 0:
print(f" Merged {merge_count} LoRA layers.")
elif args.base_model_paths is None and args.omniavatar_ckpt_path is None:
# Consolidated single-file checkpoint: no base/adapter supplied and
# the .pth already carries fully-merged plain weights, so there are
# no PEFT layers to merge. This is the expected published-weights path.
print(" Checkpoint is already consolidated (merged); nothing to merge.")
else:
print(" WARN: no LoraLayer instances found; model has no PEFT "
"adapters to merge (was --model_size set correctly?).")
model = model.to(device=device, dtype=dtype)
model.eval()
return model