echo-memory / env /loop_utils.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
6e4b62e verified
Raw
History Blame Contribute Delete
16.8 kB
"""
推理工具:供 run_replay_loop_two_chunk 及评估脚本使用。
提供:load_pipeline_and_ckpt、load_prompt_for_video、sample_trajectory_samples_from_dataset。
VWM-style 简化版:使用 DiTBlock_w_Action + MLP_CamPose(block 内 action_mlp),
去除 CameraEncoder / camera_encoder_shallow 等冗余路径。
"""
import os
import re
import sys
import csv
import random
_script_dir = os.path.dirname(os.path.abspath(__file__))
_repo_root = os.path.dirname(os.path.dirname(_script_dir))
if _repo_root not in sys.path:
sys.path.insert(0, _repo_root)
import torch
import torch.nn as nn
from safetensors.torch import load_file as safe_load_file
from diffsynth.pipelines.wan_video_new import WanVideoPipeline, ModelConfig
from diffsynth.models.wan_video_dit import SelfAttention, CrossAttention, GateModule, modulate
from diffsynth.models.memory.block_wise_ssm import BlockWiseStateSpaceMemory
from diffsynth.models.memory.videossm_hybrid import HybridStateSpaceMemory
DEFAULT_NEGATIVE_PROMPT = "oversaturated colors, overexposed, static, blurry details"
# ── MLP_CamPose + DiTBlock_w_Action(与训练侧 train.py 完全一致)──────────
class MLP_CamPose(nn.Module):
def __init__(self, out_dim, pose_dim=12):
super().__init__()
self.proj = nn.Linear(pose_dim, out_dim)
nn.init.zeros_(self.proj.weight)
nn.init.zeros_(self.proj.bias)
def forward(self, x):
return self.proj(x)
class DiTBlock_w_Action(nn.Module):
def __init__(self, has_image_input, dim, num_heads, ffn_dim, eps=1e-6,
add_action_attn=False, action_use_temporal_attention=True,
use_cam_pose=False, use_block_wise_ssm=False, use_videossm_hybrid=False,
videossm_kernel_size=3, videossm_expand=2):
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.ffn_dim = ffn_dim
if add_action_attn:
self.self_attn_with_action = SelfAttention(dim, num_heads, eps)
nn.init.zeros_(self.self_attn_with_action.o.weight)
nn.init.zeros_(self.self_attn_with_action.o.bias)
if use_cam_pose:
self.action_mlp = MLP_CamPose(dim)
else:
self.action_mlp = MLP_CamPose(dim)
self.self_attn = SelfAttention(dim, num_heads, eps)
self.cross_attn = CrossAttention(dim, num_heads, eps, has_image_input=has_image_input)
self.norm1 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
self.norm2 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
self.norm3 = nn.LayerNorm(dim, eps=eps)
self.ffn = nn.Sequential(nn.Linear(dim, ffn_dim), nn.GELU(approximate='tanh'), nn.Linear(ffn_dim, dim))
self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
self.gate = GateModule()
self.action_use_temporal_attention = action_use_temporal_attention
self.use_block_wise_ssm = bool(use_block_wise_ssm)
self.use_videossm_hybrid = bool(use_videossm_hybrid)
if use_block_wise_ssm:
self.block_wise_ssm = BlockWiseStateSpaceMemory(dim)
if use_videossm_hybrid:
self.videossm_hybrid = HybridStateSpaceMemory(
dim, kernel_size=videossm_kernel_size, expand=videossm_expand
)
def forward(self, x, context, t_mod, freqs, actions=None):
has_seq = len(t_mod.shape) == 4
chunk_dim = 2 if has_seq else 1
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
self.modulation.to(dtype=t_mod.dtype, device=t_mod.device) + t_mod).chunk(6, dim=chunk_dim)
if has_seq:
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
shift_msa.squeeze(2), scale_msa.squeeze(2), gate_msa.squeeze(2),
shift_mlp.squeeze(2), scale_mlp.squeeze(2), gate_mlp.squeeze(2),
)
num_frames = None
if actions is not None:
original_x = x
actions = self.action_mlp(actions.to(x.dtype)).to(x.dtype)
bs, num_frames, dim = actions.shape
actions = actions.reshape(bs, num_frames, 1, dim)
x = x.reshape(bs, num_frames, -1, dim)
x = x + actions
if hasattr(self, "self_attn_with_action"):
if not self.action_use_temporal_attention:
x = x.reshape(bs, -1, dim)
x = original_x + self.self_attn_with_action(x, freqs)
else:
from einops import rearrange
x = rearrange(x, "b f p d -> (b p) f d")
attn_out = self.self_attn_with_action(x)
attn_out = rearrange(attn_out, "(b p) f d -> b f p d", b=bs)
x = original_x + attn_out.reshape(bs, -1, dim)
else:
x = x.reshape(bs, -1, dim)
input_x = modulate(self.norm1(x), shift_msa, scale_msa)
x = self.gate(x, gate_msa, self.self_attn(input_x, freqs))
if num_frames is not None:
if hasattr(self, "block_wise_ssm"):
x = self.block_wise_ssm(x, f=num_frames)
if hasattr(self, "videossm_hybrid"):
spatial = x.shape[1] // int(num_frames) if int(num_frames) > 0 else 0
x = self.videossm_hybrid(x, f=num_frames, h=1, w=spatial)
x = x + self.cross_attn(self.norm3(x), context)
input_x = modulate(self.norm2(x), shift_mlp, scale_mlp)
x = self.gate(x, gate_mlp, self.ffn(input_x))
return x
# ── Utility functions ─────────────────────────────────────────────────────
def load_pose_rt(json_file, frame_idx):
"""从数据集 camera json 读取单帧 12 维 RT。"""
from src.model_training.fov_retrieval import load_camera_pose, pose_to_rt
pose = load_camera_pose(json_file, int(frame_idx))
if pose is None:
return None
return pose_to_rt(pose, constrain_to_xy=True)
def get_relative_rt(rt, ref_rt):
"""单帧相对位姿。"""
from src.model_training.fov_retrieval import convert_rt_to_relative
if rt is None or ref_rt is None or len(rt) < 12 or len(ref_rt) < 12:
return None
out = convert_rt_to_relative([rt], ref_rt)
return out[0] if out else None
def load_prompt_for_video(dataset_base, video_name):
"""从 dataset 目录下的 metadata CSV 读取该视频的 prompt。"""
if not dataset_base or not video_name:
return None
vn = str(video_name).replace(".mp4", "").replace(".avi", "").strip()
for name in ("metadata_full.csv", "metadata.csv", "prompts.csv"):
path = os.path.join(dataset_base, name)
if not os.path.isfile(path):
continue
try:
with open(path, "r", encoding="utf-8") as f:
for row in csv.DictReader(f):
if row.get("video_name", "").strip() == vn:
p = row.get("prompt", "").strip()
if p:
return p
except Exception:
pass
return None
def sample_trajectory_samples_from_dataset(dataset_base, num_samples=4, num_frames=81, seed=42):
"""从 dataset 枚举 (video_name, start_frame)。"""
frames_dir = os.path.join(dataset_base, "frames")
if not os.path.isdir(frames_dir):
return []
candidates = []
for vn in sorted(os.listdir(frames_dir)):
vd = os.path.join(frames_dir, vn)
if not os.path.isdir(vd):
continue
try:
names = [f for f in os.listdir(vd) if f.endswith(".png")]
indices = sorted({int(os.path.splitext(n)[0]) for n in names if n[:-4].isdigit()})
if not indices:
continue
max_idx = max(indices)
for start in indices:
if start + num_frames - 1 <= max_idx:
candidates.append((vn, start))
except Exception:
continue
if not candidates:
return []
rng = random.Random(seed)
if len(candidates) <= num_samples:
return candidates
return [candidates[i] for i in rng.sample(range(len(candidates)), num_samples)]
# ── Pipeline loading (VWM-style) ──────────────────────────────────────────
def _build_action_blocks(
pipe,
add_action_attn=False,
action_use_temporal_attention=True,
block_wise_block_ids=None,
videossm_block_ids=None,
):
"""Replace DiT blocks with DiTBlock_w_Action (VWM cam_infer.py style)."""
dit = pipe.dit
old_blocks = dit.blocks
has_image_input = getattr(dit, "has_image_input", False)
dim = dit.dim
num_heads = getattr(dit, "num_heads", None) or getattr(old_blocks[0], "num_heads", None)
ffn_dim = getattr(dit, "ffn_dim", None) or getattr(old_blocks[0], "ffn_dim", None)
eps = getattr(dit, "eps", 1e-6)
block_dtype = next(old_blocks[0].parameters()).dtype
block_device = next(old_blocks[0].parameters()).device
block_wise_block_ids = set(block_wise_block_ids or [])
videossm_block_ids = set(videossm_block_ids or [])
new_blocks = torch.nn.ModuleList()
for block_id, old_block in enumerate(old_blocks):
new_block = DiTBlock_w_Action(
has_image_input=has_image_input,
dim=dim, num_heads=num_heads, ffn_dim=ffn_dim, eps=eps,
add_action_attn=add_action_attn,
action_use_temporal_attention=action_use_temporal_attention,
use_cam_pose=True,
use_block_wise_ssm=block_id in block_wise_block_ids,
use_videossm_hybrid=block_id in videossm_block_ids,
)
new_block = new_block.to(dtype=block_dtype, device=block_device)
for attr in ("self_attn", "cross_attn", "norm1", "norm2", "norm3", "ffn"):
if hasattr(old_block, attr) and hasattr(new_block, attr):
getattr(new_block, attr).load_state_dict(getattr(old_block, attr).state_dict())
if hasattr(old_block, "modulation") and hasattr(new_block, "modulation"):
with torch.no_grad():
new_block.modulation.copy_(old_block.modulation.to(dtype=block_dtype))
new_blocks.append(new_block)
dit.blocks = new_blocks
print(f"[loop_utils] Replaced {len(new_blocks)} blocks with DiTBlock_w_Action (MLP_CamPose)")
if block_wise_block_ids:
print(f"[loop_utils] Loaded Block-wise SSM slots on blocks: {sorted(block_wise_block_ids)[:8]}{'...' if len(block_wise_block_ids) > 8 else ''}")
if videossm_block_ids:
print(f"[loop_utils] Loaded VideoSSM hybrid slots on blocks: {sorted(videossm_block_ids)[:8]}{'...' if len(videossm_block_ids) > 8 else ''}")
def load_pipeline_and_ckpt(
ckpt_path,
dit_path,
text_encoder_path,
vae_path,
device="cuda",
add_action_attn=False,
action_use_temporal_attention=True,
tokenizer_path=None,
# Legacy kwargs accepted but ignored (CameraEncoder removed)
**kwargs,
):
"""Load WanVideoPipeline, replace blocks with DiTBlock_w_Action, load ckpt (strict=False).
VWM-style: no CameraEncoder, no complex inference logic. Action is injected
via MLP_CamPose (nn.Linear(12, dim), zero-init) inside each DiTBlock_w_Action.
"""
print(f"[loop_utils] Loading pipeline (DiT -> {device})")
if not tokenizer_path:
import os as _os
_base = _os.path.dirname(dit_path)
_cand = _os.path.join(_base, "google", "umt5-xxl")
if _os.path.isdir(_cand):
tokenizer_path = _cand
print(f"[loop_utils] Auto-detected tokenizer at {tokenizer_path}")
model_configs = [
ModelConfig(path=dit_path, offload_device=device),
ModelConfig(path=text_encoder_path, offload_device="cpu"),
ModelConfig(path=vae_path, offload_device="cpu"),
]
pipe = WanVideoPipeline.from_pretrained(
torch_dtype=torch.bfloat16,
device=device,
model_configs=model_configs,
tokenizer_config=ModelConfig(path=tokenizer_path) if tokenizer_path else None,
)
ckpt = None
block_wise_block_ids = set()
videossm_block_ids = set()
action_attn_block_ids = set()
if ckpt_path and os.path.isfile(ckpt_path):
ckpt = safe_load_file(ckpt_path)
for key in ckpt.keys():
m = re.match(r"blocks\.(\d+)\.block_wise_ssm\.", key)
if m:
block_wise_block_ids.add(int(m.group(1)))
m = re.match(r"blocks\.(\d+)\.videossm_hybrid\.", key)
if m:
videossm_block_ids.add(int(m.group(1)))
m = re.match(r"blocks\.(\d+)\.self_attn_with_action\.", key)
if m:
action_attn_block_ids.add(int(m.group(1)))
if action_attn_block_ids and not add_action_attn:
add_action_attn = True
print("[loop_utils] Detected action-attention weights in checkpoint; enabling self_attn_with_action")
# Replace blocks with DiTBlock_w_Action, including memory slots implied by ckpt keys.
_build_action_blocks(
pipe,
add_action_attn=add_action_attn,
action_use_temporal_attention=action_use_temporal_attention,
block_wise_block_ids=block_wise_block_ids,
videossm_block_ids=videossm_block_ids,
)
# Load ckpt (strict=False: base model keys match, action_mlp keys are extra)
if ckpt_path and not os.path.isfile(ckpt_path):
print(f"[loop_utils] WARNING: ckpt not found: {ckpt_path} — running with base model weights only!")
if ckpt_path and os.path.isfile(ckpt_path):
if ckpt is None:
ckpt = safe_load_file(ckpt_path)
missing, unexpected = pipe.dit.load_state_dict(ckpt, strict=False)
action_keys = [k for k in ckpt if "action_mlp" in k]
if not missing and not unexpected:
print(f"[loop_utils] Ckpt loaded: {len(ckpt)} keys, perfect match")
else:
print(f"[loop_utils] Ckpt loaded: {len(ckpt)} keys, "
f"missing={len(missing)}, unexpected={len(unexpected)}, "
f"action_mlp_keys={len(action_keys)}")
if missing:
for k in sorted(missing)[:5]:
print(f" missing: {k}")
if unexpected:
for k in sorted(unexpected)[:5]:
print(f" unexpected: {k}")
# Optional: load SpatialGridMemory if present in ckpt
_smsd = {
k.replace("spatial_memory_module.", "", 1): v
for k, v in ckpt.items()
if k.startswith("spatial_memory_module.")
}
if _smsd:
try:
from diffsynth.models.memory.spatial_grid_memory import SpatialGridMemory
except ImportError:
SpatialGridMemory = None
if SpatialGridMemory is not None:
dim = pipe.dit.dim
w = _smsd.get("spatial_to_tokens")
if w is not None:
g2, num_tok = int(w.shape[0]), int(w.shape[1])
gsz = int(round(g2 ** 0.5))
if gsz * gsz != g2:
gsz = 8
sm = SpatialGridMemory(dim, grid_size=gsz, num_tokens=num_tok)
sm.load_state_dict(_smsd, strict=False)
sm = sm.to(dtype=next(pipe.dit.parameters()).dtype, device=next(pipe.dit.parameters()).device)
pipe.spatial_memory_module = sm
pipe.use_spatial_memory_legacy = False
print(f"[loop_utils] Loaded spatial_memory_module (grid={gsz}, tokens={num_tok})")
_srmsd = {
k.replace("spatial_memory_readout_module.", "", 1): v
for k, v in ckpt.items()
if k.startswith("spatial_memory_readout_module.")
}
if _srmsd:
try:
from diffsynth.models.memory.spatial_grid_memory import SpatialCrossAttnReadout
except ImportError:
SpatialCrossAttnReadout = None
if SpatialCrossAttnReadout is not None:
dim = pipe.dit.dim
readout = SpatialCrossAttnReadout(dim=dim, num_heads=8)
readout.load_state_dict(_srmsd, strict=False)
readout = readout.to(dtype=next(pipe.dit.parameters()).dtype, device=next(pipe.dit.parameters()).device)
pipe.spatial_memory_readout_module = readout
print("[loop_utils] Loaded spatial_memory_readout_module")
if getattr(pipe, "enable_vram_management", None):
pipe.enable_vram_management()
return pipe