multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
f7dc16a verified
Raw History Blame Contribute Delete
94.4 kB
from .data.dataset import (
PrecomputedLatentDataset,
_default_blank_prompt_emb_path,
_prompt_emb_to_cpu,
_save_prompt_emb_atomic,
)
import copy
import glob
import os
import re
import time
import torch, os
import lightning as pl
from .pipelines.wan_video_Nplayers import WanVideoNPlayersPipeline
from .models.model_manager import ModelManager
from multics.pipelines.wan_video_Nplayers import model_fn_wan_video_causal
import random
import json
import torch.nn as nn
import torch.nn.functional as F
import glob
from lightning.pytorch.loggers import TensorBoardLogger
from multics.models.wan_video_dit import (
prepare_hier_local_hub_state_dict_for_load,
migrate_hier_no_local_hub_state_dict,
)
_REPO_DIR = os.path.dirname(os.path.abspath(__file__))
_BLANK_PROMPT_EMB_FILENAME = "blank_string_prompt_emb.pth"
_CAMERA_PLAYER_PART = re.compile("(?:^|/)player=(\\d+)(?:/|$)")
_GENERATED_HISTORY_NEGATIVE_PROMPT = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
def get_corrupt_noise_sigma(
model_input,
batch_size,
corrupt_ratio=1 / 3,
num_frames=None,
is_frame_independent=False,
):
"""DF-style σ ~ U(0, corrupt_ratio) for (1-σ)*x + σ*noise. Broadcast to model_input.ndim."""
if is_frame_independent:
noise_sigma_shape = (batch_size, 1, num_frames)
else:
noise_sigma_shape = (batch_size,)
noise_sigma = (
torch.rand(
size=noise_sigma_shape, device=model_input.device, dtype=model_input.dtype
)
* corrupt_ratio
)
while len(noise_sigma.shape) < model_input.ndim:
noise_sigma = noise_sigma.unsqueeze(-1)
return noise_sigma
def _attach_hier_group_dit_kwargs(dit_kwargs: dict, batch: dict, device) -> None:
"""Copy grouping tensors from the dataloader batch into DiT kwargs.
labels mode: group_labels [1,T,P], active_group [1,T,N]
membership mode also: membership [1,T,N,P], global_only [1,T,P]
"""
if "group_labels" not in batch or "active_group" not in batch:
raise KeyError(
"hier_hub requires dataset batch keys group_labels and active_group"
)
dit_kwargs["group_labels"] = batch["group_labels"].to(
device=device, dtype=torch.long
)
dit_kwargs["active_group"] = batch["active_group"].to(
device=device, dtype=torch.bool
)
if "membership" in batch:
if "global_only" not in batch:
raise KeyError("hier_group_format=membership batch requires global_only")
dit_kwargs["membership"] = batch["membership"].to(
device=device, dtype=torch.bool
)
dit_kwargs["global_only"] = batch["global_only"].to(
device=device, dtype=torch.bool
)
class LightningModelForTrain(pl.LightningModule):
def __init__(
self,
text_encoder_path,
vae_path,
image_encoder_path,
dit_path,
learning_rate=1e-05,
use_gradient_checkpointing=True,
use_gradient_checkpointing_offload=False,
resume_ckpt_path=None,
output_path="./",
num_players=2,
vae_latent_channels=48,
perf_log_every_n_steps=100,
perf_timing=True,
target_rgb_frames=81,
first_frame_dropout_p=0.0,
first_frame_noise_p=0.0,
first_frame_noise_corrupt_ratio=1 / 3,
first_frame_noise_min_seg=1,
ckpt_local_dir=None,
run_version=None,
blank_prompt_emb_path: str = None,
text_prompt_dropout_p: float = 0.0,
use_simplex_rope: bool = True,
simplex_pool_size: int = 4,
simplex_gamma: float = 1.0,
simplex_train_permute: bool = True,
simplex_train_sample_vertex_ids: bool = False,
training_stage: str = "stage2",
loss_type: str = "diffusion_forcing",
attention_mode: str = "hier_hub",
attn_impl: str = "sdpa",
num_hub_tokens: int = 8,
local_attn_size: int = 24,
local_attn_size_after: int = None,
local_attn_switch_step: int = None,
train_step_offset: int = 0,
num_frame_per_block: int = 7,
num_groups: int = 1,
num_local_hub_tokens: int = 0,
num_global_hub_tokens: int = None,
hier_no_local_hub: bool = False,
hier_local_hub_mode: str = "shared",
hier_direct_schedule: str = "none",
hier_direct_frac: float = 0.0,
hier_direct_layers=None,
unfreeze_self_attn: bool = False,
clean_prefix_prob: float = 0.0,
clean_prefix_context_noise: int = 0,
generated_history_prob: float = 0.0,
df_per_frame_noise: bool = False,
causal_forcing_training: bool = False,
use_camera_pose: bool = False,
camera_dim: int = 12,
camera_dropout_p: float = 0.1,
val_infer: bool = False,
val_every_n_steps: int = 500,
val_num_samples: int = 4,
val_num_inference_steps: int = 50,
val_causal_num_frames: int = None,
val_output_dir: str = "outputs/validation",
val_grid_fps: int = 15,
):
super().__init__()
if (
training_stage != "stage2"
or loss_type != "diffusion_forcing"
or attention_mode != "hier_hub"
or (not use_simplex_rope)
):
raise ValueError(
"Multics supports hierarchical attention with diffusion forcing and simplex RoPE"
)
model_manager = ModelManager(torch_dtype=torch.bfloat16, device="cpu")
if "*" in dit_path:
dit_files = sorted(glob.glob(dit_path))
model_manager.load_model(dit_files)
elif os.path.isfile(dit_path):
model_manager.load_models([dit_path])
else:
dit_path = dit_path.split(",")
model_manager.load_models([dit_path])
model_path = [text_encoder_path, vae_path]
if image_encoder_path is not None:
model_path.append(image_encoder_path)
model_manager.load_models(model_path)
self.pipe = WanVideoNPlayersPipeline.from_model_manager(model_manager)
self.pipe.scheduler.set_timesteps(1000, training=True)
self.pipe.num_players = num_players
self.use_simplex_rope = bool(True)
self.simplex_pool_size = int(simplex_pool_size)
self.simplex_gamma = float(simplex_gamma)
self.simplex_train_permute = bool(simplex_train_permute)
self.simplex_train_sample_vertex_ids = bool(simplex_train_sample_vertex_ids)
self.training_stage = str("stage2")
self.loss_type = str("diffusion_forcing")
self.attention_mode = str("hier_hub")
self.attn_impl = str(attn_impl)
self.num_hub_tokens = int(num_hub_tokens)
self.local_attn_size_initial = int(local_attn_size)
self.local_attn_size_after = (
None if local_attn_size_after is None else int(local_attn_size_after)
)
self.local_attn_switch_step = (
None if local_attn_switch_step is None else int(local_attn_switch_step)
)
self.train_step_offset = int(train_step_offset)
if (self.local_attn_size_after is None) != (
self.local_attn_switch_step is None
):
raise ValueError(
"local_attn_size_after and local_attn_switch_step must be set together"
)
if self.local_attn_switch_step is not None and self.local_attn_switch_step < 0:
raise ValueError("local_attn_switch_step must be >= 0")
if self.train_step_offset < 0:
raise ValueError("train_step_offset must be >= 0")
self._last_logged_local_attn_size = None
self.num_frame_per_block = int(num_frame_per_block)
for name, value in (
("local_attn_size", self.local_attn_size_initial),
("local_attn_size_after", self.local_attn_size_after),
):
if value is not None and value != -1 and (value < self.num_frame_per_block):
raise ValueError(
f"{name} must be -1 or >= num_frame_per_block; got {value} and {self.num_frame_per_block}"
)
self.local_attn_size = self._local_attn_size_for_effective_step(
self.train_step_offset
)
self.num_groups = int(num_groups)
self.num_local_hub_tokens = int(num_local_hub_tokens)
self.num_global_hub_tokens = (
None if num_global_hub_tokens is None else int(num_global_hub_tokens)
)
self.hier_no_local_hub = bool(hier_no_local_hub)
self.hier_local_hub_mode = str(hier_local_hub_mode)
self.hier_direct_schedule = str(hier_direct_schedule)
self.hier_direct_frac = float(hier_direct_frac)
self.hier_direct_layers = hier_direct_layers
self.unfreeze_self_attn = bool(unfreeze_self_attn)
self.target_rgb_frames = int(target_rgb_frames)
self.val_infer = bool(val_infer)
self.val_every_n_steps = int(val_every_n_steps)
self.val_num_samples = int(val_num_samples)
self.val_num_inference_steps = int(val_num_inference_steps)
self.val_causal_num_frames = int(val_causal_num_frames or target_rgb_frames)
self.val_output_dir = val_output_dir
self.val_grid_fps = int(val_grid_fps)
self.val_agent_vertex_ids = list(range(int(num_players)))
self._manual_val_loader = None
self._last_val_step = -1
self.clean_prefix_prob = float(clean_prefix_prob)
self.clean_prefix_context_noise = int(clean_prefix_context_noise)
self.generated_history_prob = float(generated_history_prob)
if not 0.0 <= self.generated_history_prob <= 1.0:
raise ValueError(
f"generated_history_prob must be in [0,1], got {self.generated_history_prob}"
)
if self.generated_history_prob > 0.0:
if causal_forcing_training:
raise ValueError(
"Generated history uses the parallel target path and is incompatible with --causal_forcing_training"
)
print(
f"[generated-history] enabled prob={self.generated_history_prob} blocks=1..3 steps={self.val_num_inference_steps} cfg=5",
flush=True,
)
self._generated_history_negative_context_cpu = None
self.df_per_frame_noise = bool(df_per_frame_noise)
self.causal_forcing_training = bool(causal_forcing_training)
self.use_camera_pose = bool(use_camera_pose)
self.camera_dim = int(camera_dim)
self.camera_dropout_p = float(camera_dropout_p)
self.pipe.dit.keyboard_embedding = nn.Linear(23, 3072)
nn.init.normal_(self.pipe.dit.keyboard_embedding.weight, mean=0, std=0.05)
nn.init.zeros_(self.pipe.dit.keyboard_embedding.bias)
dim = self.pipe.dit.blocks[0].self_attn.q.weight.shape[0]
for block in self.pipe.dit.blocks:
block.action_mlp = nn.Sequential(
nn.LayerNorm(dim + 24, eps=1e-05, elementwise_affine=True),
nn.Linear(dim + 24, dim + 24),
nn.GELU(),
nn.Linear(dim + 24, dim),
nn.LayerNorm(dim, eps=1e-05, elementwise_affine=True),
)
block.action_temporal_attn = copy.deepcopy(block.self_attn)
block.action_cross_attn = copy.deepcopy(block.cross_attn)
nn.init.zeros_(block.action_temporal_attn.o.weight)
nn.init.zeros_(block.action_temporal_attn.o.bias)
nn.init.zeros_(block.action_cross_attn.o.weight)
nn.init.zeros_(block.action_cross_attn.o.bias)
self.pipe.dit.enable_simplex_rope(
pool_size=self.simplex_pool_size, gamma=self.simplex_gamma
)
latent_t = (self.target_rgb_frames - 1) // 4 + 1
if latent_t % self.num_frame_per_block != 0:
raise ValueError(
f"Requires latent_T % num_frame_per_block == 0; got latent_T={latent_t}, n={self.num_frame_per_block}"
)
stage2_attention_mode = "hier_hub"
if self.simplex_train_permute:
raise ValueError(
"hier_hub requires --no_simplex_train_permute so group_labels stay aligned"
)
self.pipe.enable_stage2(
num_hub_tokens=self.num_hub_tokens,
local_attn_size=self.local_attn_size,
num_frame_per_block=self.num_frame_per_block,
attention_mode="hier_hub",
num_groups=self.num_groups,
num_local_hub_tokens=self.num_local_hub_tokens,
num_global_hub_tokens=self.num_global_hub_tokens,
hier_direct_schedule=self.hier_direct_schedule,
hier_direct_frac=self.hier_direct_frac,
hier_direct_layers=self.hier_direct_layers,
attn_impl=self.attn_impl,
hier_no_local_hub=self.hier_no_local_hub,
hier_local_hub_mode=self.hier_local_hub_mode,
)
self.loss_type = "diffusion_forcing"
self.attention_mode = "hier_hub"
self.unfreeze_self_attn = True
if self.use_camera_pose:
self.pipe.dit.enable_camera_pose_alignment(
cam_dim=self.camera_dim, dropout_p=self.camera_dropout_p
)
print(
f"[camera-pose] enabled bias-free Q/K-only camera conditioning (dim={self.camera_dim}, dropout_p={self.camera_dropout_p})"
)
assert 0.0 <= float(first_frame_dropout_p) <= 1.0, first_frame_dropout_p
self.first_frame_dropout_p = float(first_frame_dropout_p)
assert 0.0 <= float(first_frame_noise_p) <= 1.0, first_frame_noise_p
assert 0.0 <= float(first_frame_noise_corrupt_ratio) <= 1.0, (
first_frame_noise_corrupt_ratio
)
self.first_frame_noise_p = float(first_frame_noise_p)
self.first_frame_noise_corrupt_ratio = float(first_frame_noise_corrupt_ratio)
self.first_frame_noise_min_seg = int(first_frame_noise_min_seg)
if resume_ckpt_path is not None:
state_dict = torch.load(resume_ckpt_path, map_location="cpu")
if not self.hier_no_local_hub:
state_dict, migration = prepare_hier_local_hub_state_dict_for_load(
state_dict,
target_mode=self.hier_local_hub_mode,
target_num_groups=self.pipe.dit.num_groups,
target_num_local_hub_tokens=self.pipe.dit.num_local_hub_tokens,
model_dim=self.pipe.dit.dim,
)
if migration is not None:
print(
f"[hier-local-hub] training warm-start migration: {migration['source_mode']} {migration['source_shape']} -> {migration['target_mode']} {migration['target_shape']}; optimizer state is not migrated",
flush=True,
)
old_hub = state_dict.get("hub_embed")
new_hub = self.pipe.dit.hub_embed
if (
torch.is_tensor(old_hub)
and torch.is_tensor(new_hub)
and (old_hub.ndim == 2)
and (tuple(old_hub.shape) != tuple(new_hub.shape))
):
resized = new_hub.detach().cpu().clone()
n = min(old_hub.shape[0], resized.shape[0])
resized[:n].copy_(old_hub[:n].to(dtype=resized.dtype))
state_dict["hub_embed"] = resized
print(
f"[checkpoint] resized hub_embed {tuple(old_hub.shape)} -> {tuple(resized.shape)}; copied {n} slots",
flush=True,
)
if self.hier_no_local_hub:
state_dict, migration = migrate_hier_no_local_hub_state_dict(
state_dict,
target_num_global_hub_tokens=self.pipe.dit.num_global_hub_tokens,
model_dim=self.pipe.dit.dim,
)
if migration is not None:
print(
f"[hier-no-local] training warm-start migration: {migration['source']} {migration['source_shape']} -> global_hub_embed {migration['target_shape']} (replaced old global {migration['replaced_global_shape']}); optimizer state is not migrated",
flush=True,
)
missing, unexpected = self.pipe.dit.load_state_dict(
state_dict, strict=False
)
if missing:
_hub_related = [k for k in missing if k.endswith("hub_embed")]
_cam_keys = (".cam_q.", ".cam_k.")
_cam_related = [k for k in missing if any((s in k for s in _cam_keys))]
_other_missing = [
k
for k in missing
if k not in _hub_related and k not in _cam_related
]
if _other_missing:
raise RuntimeError(
f"Unexpected missing keys when loading DiT: {_other_missing[:10]} ..."
)
if _hub_related:
print(
f"[checkpoint] {len(_hub_related)} hub_embed param(s) absent in ckpt (zeros init, will train from seed)."
)
if _cam_related:
print(
f"[camera-pose] {len(_cam_related)} camera adapter params absent in ckpt (zeros init)."
)
if unexpected:
_ff_keys = (".k_ff", ".v_ff", ".norm_k_ff", ".ff_gate")
_ff_related = [k for k in unexpected if any((s in k for s in _ff_keys))]
_handled_unexpected = list(_ff_related)
_old_hub = [
k
for k in unexpected
if k == "hub_embed" or k.endswith(".hub_embed")
]
if _old_hub:
print(
f"[checkpoint] ignoring {len(_old_hub)} legacy per-block hub_embed key(s) in ckpt."
)
_handled_unexpected += _old_hub
_legacy_cam_bias = [
k
for k in unexpected
if k.endswith(".cam_q.bias") or k.endswith(".cam_k.bias")
]
if _legacy_cam_bias:
print(
f"[camera-pose] dropping {len(_legacy_cam_bias)} legacy camera projection bias key(s) for the bias-free adapter."
)
_disabled_cam = [
k
for k in unexpected
if not self.use_camera_pose
and (".cam_q." in k or ".cam_k." in k)
and (k not in _legacy_cam_bias)
]
if _disabled_cam:
print(
f"[camera-pose] ignoring {len(_disabled_cam)} camera adapter key(s) because camera conditioning is disabled."
)
_handled_unexpected += _legacy_cam_bias + _disabled_cam
if _ff_related:
print(
f"[WARN] DiT ckpt 中检测到 {len(_ff_related)} 个 first-frame cross-attn 分支参数 (k_ff/v_ff/norm_k_ff/ff_gate),当前架构已回退到 latent-overwrite 方案,这些参数会被忽略。如不需要可重新存一份干净的 ckpt。示例: {_ff_related[:3]}"
)
_other_unexpected = [
k for k in unexpected if k not in _handled_unexpected
]
if _other_unexpected:
print(
f"[WARN] unexpected keys in DiT ckpt: {_other_unexpected[:10]} ..."
)
if "hub_embed" in state_dict and (
"local_hub_embed" not in state_dict
or "global_hub_embed" not in state_dict
):
old_hub = state_dict["hub_embed"]
if (
torch.is_tensor(old_hub)
and old_hub.dim() == 2
and (old_hub.shape[1] == self.pipe.dit.dim)
):
with torch.no_grad():
kl = int(self.pipe.dit.num_local_hub_tokens)
kg = int(self.pipe.dit.num_global_hub_tokens)
if kl > 0:
n = min(kl, old_hub.shape[0])
local_target = self.pipe.dit.local_hub_embed
local_values = old_hub[:n].to(local_target)
if local_target.dim() == 2:
local_target[:n].copy_(local_values)
elif local_target.dim() == 3:
local_target[:, :n].copy_(
local_values.unsqueeze(0).expand(
local_target.shape[0], -1, -1
)
)
else:
raise RuntimeError(
f"local_hub_embed must be rank 2 or 3, got {tuple(local_target.shape)}"
)
if kg > 0:
n = min(kg, old_hub.shape[0])
self.pipe.dit.global_hub_embed[-n:].copy_(
old_hub[-n:].to(self.pipe.dit.global_hub_embed)
)
print(
f"[hier_hub] initialized local/global hub seeds from central hub_embed {tuple(old_hub.shape)} -> local={(None if self.pipe.dit.local_hub_embed is None else tuple(self.pipe.dit.local_hub_embed.shape))} global={tuple(self.pipe.dit.global_hub_embed.shape)}"
)
self.freeze_parameters()
trainable_keywords = [
"keyboard_embedding",
"action_mlp",
"action_temporal_attn",
"action_cross_attn",
]
if self.unfreeze_self_attn:
trainable_keywords.extend(["self_attn", "hub_embed"])
if self.use_camera_pose:
trainable_keywords.extend(["cam_q", "cam_k"])
for name, module in self.pipe.denoising_model().named_modules():
if any((keyword in name for keyword in trainable_keywords)):
print(f"Trainable: {name}")
for param in module.parameters():
param.requires_grad = True
if name.endswith(".cross_attn"):
print(f"Trainable (text cross-attention): {name}")
for param in module.parameters():
param.requires_grad = True
if self.unfreeze_self_attn:
_dit = self.pipe.denoising_model()
if hasattr(_dit, "hub_embed") and isinstance(_dit.hub_embed, nn.Parameter):
_dit.hub_embed.requires_grad_(True)
print(
f"Trainable (parameter, explicit): model-level hub_embed {tuple(_dit.hub_embed.shape)}"
)
for _name in ("local_hub_embed", "global_hub_embed"):
_p = getattr(_dit, _name, None)
if isinstance(_p, nn.Parameter):
_p.requires_grad_(True)
print(f"Trainable (parameter, explicit): {_name} {tuple(_p.shape)}")
os.makedirs(output_path, exist_ok=True)
self.output_path = output_path
self.ckpt_local_dir = ckpt_local_dir
self.run_version = run_version
self._prompt_encode_cache: dict = {}
self._blank_prompt_ctx: torch.Tensor | None = None
self._blank_prompt_emb_path = (
blank_prompt_emb_path or _default_blank_prompt_emb_path()
)
self.text_prompt_dropout_p = float(text_prompt_dropout_p)
if not 0.0 <= self.text_prompt_dropout_p <= 1.0:
raise ValueError(
f"--text_prompt_dropout_p must be in [0,1], got {self.text_prompt_dropout_p}"
)
if self.text_prompt_dropout_p > 0:
print(
f"[text-cfg] text_prompt_dropout_p={self.text_prompt_dropout_p}",
flush=True,
)
trainable_params = 0
seen_params = set()
for name, module in self.pipe.denoising_model().named_modules():
for param in module.parameters():
if param.requires_grad and param not in seen_params:
trainable_params += param.numel()
seen_params.add(param)
print(f"Total number of trainable parameters: {trainable_params}")
self.learning_rate = learning_rate
self.use_gradient_checkpointing = use_gradient_checkpointing
self.use_gradient_checkpointing_offload = use_gradient_checkpointing_offload
tiled = True
tile_size = (34, 34)
tile_stride = (18, 16)
self.tiler_kwargs = {
"tiled": tiled,
"tile_size": tile_size,
"tile_stride": tile_stride,
}
self._perf_timing_enabled = bool(perf_timing)
self.perf_log_every_n_steps = max(1, int(perf_log_every_n_steps))
self._iter_start_time = None
self._prev_batch_end_time = None
self._current_data_wait_time = 0.0
self._backward_start_time = None
self._last_backward_time = 0.0
self._last_optimizer_time = 0.0
self._latest_step_part_times = {"encode_target": 0.0, "dit_forward_loss": 0.0}
self._perf_window = {
"count": 0,
"iter_wall": 0.0,
"data_wait": 0.0,
"compute": 0.0,
"backward": 0.0,
"optimizer": 0.0,
"encode_target": 0.0,
"dit_forward_loss": 0.0,
}
def _synced_time(self):
if self._perf_timing_enabled and torch.cuda.is_available():
try:
torch.cuda.synchronize(self.device)
except Exception:
torch.cuda.synchronize()
return time.perf_counter()
@staticmethod
def _classify_bottleneck(avg_data_wait, avg_compute, avg_backward):
total_iter = avg_data_wait + avg_compute
if total_iter <= 0:
return "unknown"
data_wait_ratio = avg_data_wait / total_iter
compute_ratio = avg_compute / total_iter
backward_ratio = avg_backward / max(avg_compute, 1e-08)
if data_wait_ratio >= 0.4 and data_wait_ratio >= compute_ratio:
return "data"
if compute_ratio >= 0.6 and backward_ratio >= 0.55:
return "communication"
if compute_ratio >= 0.6:
return "compute"
if backward_ratio >= 0.65:
return "communication"
if data_wait_ratio >= 0.35:
return "data"
return "compute"
def on_train_batch_start(self, batch, batch_idx):
self._maybe_update_local_attn_size()
if not self._perf_timing_enabled:
return
now = self._synced_time()
if self._prev_batch_end_time is None:
self._current_data_wait_time = 0.0
else:
self._current_data_wait_time = max(0.0, now - self._prev_batch_end_time)
self._iter_start_time = now
self._backward_start_time = None
self._last_backward_time = 0.0
self._last_optimizer_time = 0.0
def _local_attn_size_for_effective_step(self, effective_step: int) -> int:
if (
self.local_attn_switch_step is not None
and int(effective_step) >= self.local_attn_switch_step
):
return int(self.local_attn_size_after)
return int(self.local_attn_size_initial)
def _maybe_update_local_attn_size(self):
"""Apply the attention window selected for the next optimizer update."""
if self.local_attn_switch_step is None:
return
local_step = int(self.global_step)
effective_step = self.train_step_offset + local_step
target = self._local_attn_size_for_effective_step(effective_step)
if int(self.local_attn_size) != target:
self.pipe.set_stage2_local_attn_size(target, reset_training_cache=True)
self.local_attn_size = target
values = {
int(self.local_attn_size),
int(self.pipe.local_attn_size),
int(self.pipe.dit.local_attn_size),
*(int(block.local_attn_size) for block in self.pipe.dit.blocks),
*(int(block.self_attn.local_attn_size) for block in self.pipe.dit.blocks),
}
if values != {target}:
raise RuntimeError(
f"local-attn schedule propagation failed: expected {target}, observed {sorted(values)}"
)
if self._last_logged_local_attn_size != target:
if int(getattr(self, "global_rank", 0)) == 0:
print(
f"[local-attn-schedule] local_step={local_step} effective_step={effective_step} local_attn_size={target}",
flush=True,
)
self._last_logged_local_attn_size = target
def on_before_backward(self, loss):
if not self._perf_timing_enabled:
return
self._backward_start_time = self._synced_time()
def on_after_backward(self):
if not self._perf_timing_enabled:
return
if self._backward_start_time is None:
self._last_backward_time = 0.0
return
self._last_backward_time = max(
0.0, self._synced_time() - self._backward_start_time
)
def optimizer_step(self, *args, **kwargs):
if not self._perf_timing_enabled:
return super().optimizer_step(*args, **kwargs)
start_time = self._synced_time()
out = super().optimizer_step(*args, **kwargs)
self._last_optimizer_time = max(0.0, self._synced_time() - start_time)
return out
def on_train_batch_end(self, outputs, batch, batch_idx):
end_time = self._synced_time() if self._perf_timing_enabled else None
self._maybe_run_manual_validation()
if not self._perf_timing_enabled:
return
compute_time = (
0.0
if self._iter_start_time is None
else max(0.0, end_time - self._iter_start_time)
)
data_wait_time = max(0.0, self._current_data_wait_time)
iter_wall_time = data_wait_time + compute_time
self._prev_batch_end_time = end_time
self._perf_window["count"] += 1
self._perf_window["iter_wall"] += iter_wall_time
self._perf_window["data_wait"] += data_wait_time
self._perf_window["compute"] += compute_time
self._perf_window["backward"] += self._last_backward_time
self._perf_window["optimizer"] += self._last_optimizer_time
for key in ("encode_target", "dit_forward_loss"):
self._perf_window[key] += self._latest_step_part_times.get(key, 0.0)
if self._perf_window["count"] < self.perf_log_every_n_steps:
return
c = float(self._perf_window["count"])
avg_iter_wall = self._perf_window["iter_wall"] / c
avg_data_wait = self._perf_window["data_wait"] / c
avg_compute = self._perf_window["compute"] / c
avg_backward = self._perf_window["backward"] / c
avg_optimizer = self._perf_window["optimizer"] / c
avg_encode_target = self._perf_window["encode_target"] / c
avg_dit_forward_loss = self._perf_window["dit_forward_loss"] / c
total_iter = max(avg_iter_wall, 1e-08)
data_wait_ratio = avg_data_wait / total_iter
compute_ratio = avg_compute / total_iter
backward_ratio = avg_backward / max(avg_compute, 1e-08)
bottleneck = self._classify_bottleneck(avg_data_wait, avg_compute, avg_backward)
if self.trainer is not None and self.trainer.is_global_zero:
print(
f"[Perf][step={int(self.global_step)}][N={int(c)}] iter={avg_iter_wall:.4f}s data_wait={avg_data_wait:.4f}s({data_wait_ratio:.1%}) compute={avg_compute:.4f}s({compute_ratio:.1%}) backward={avg_backward:.4f}s(backward/compute={backward_ratio:.1%}) optimizer={avg_optimizer:.4f}s encode_target={avg_encode_target:.4f}s dit_forward_loss={avg_dit_forward_loss:.4f}s bottleneck={bottleneck}"
)
self.log(
"perf/iter_wall_s",
avg_iter_wall,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/data_wait_s",
avg_data_wait,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/compute_s",
avg_compute,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/backward_s",
avg_backward,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/optimizer_s",
avg_optimizer,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/encode_target_s",
avg_encode_target,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/dit_forward_loss_s",
avg_dit_forward_loss,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/data_wait_ratio",
data_wait_ratio,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/compute_ratio",
compute_ratio,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/backward_in_compute_ratio",
backward_ratio,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/bottleneck_data",
1.0 if bottleneck == "data" else 0.0,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/bottleneck_compute",
1.0 if bottleneck == "compute" else 0.0,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
self.log(
"perf/bottleneck_communication",
1.0 if bottleneck == "communication" else 0.0,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=True,
)
for key in self._perf_window.keys():
self._perf_window[key] = 0.0 if key != "count" else 0
def freeze_parameters(self):
self.pipe.requires_grad_(False)
self.pipe.eval()
self.pipe.denoising_model().train()
def _encode_prompt_with_cache(self, text: str) -> torch.Tensor:
"""将文本编码为 T5 context tensor (seq_len, 4096),结果缓存在 CPU。
text 为空字符串时优先读取 repo-local blank cache;缺失则 encode 后写回。
返回值为 CPU tensor;调用侧负责 .to(device)。
"""
text = text.strip() if text else ""
if not text:
if self._blank_prompt_ctx is None:
if os.path.isfile(self._blank_prompt_emb_path):
blank = torch.load(
self._blank_prompt_emb_path,
map_location="cpu",
weights_only=True,
)
else:
with torch.no_grad():
blank = self.pipe.encode_prompt("", positive=True)
blank = _prompt_emb_to_cpu(blank)
_save_prompt_emb_atomic(blank, self._blank_prompt_emb_path)
print(
f"[text-cfg] encoded blank prompt and cached to {self._blank_prompt_emb_path}",
flush=True,
)
ctx = blank["context"]
if ctx.ndim == 3:
ctx = ctx[0]
self._blank_prompt_ctx = ctx.cpu()
return self._blank_prompt_ctx
if text not in self._prompt_encode_cache:
with torch.no_grad():
emb = self.pipe.encode_prompt(text)
ctx = emb["context"]
if ctx.ndim == 3:
ctx = ctx[0]
self._prompt_encode_cache[text] = ctx.cpu()
return self._prompt_encode_cache[text]
def _get_context_for_seg(self, batch: dict, seg_idx: int) -> torch.Tensor:
"""返回 segment seg_idx 的 DiT context tensor。
- use_text_prompt=True:返回 (2, seq_len, 4096),legacy 2-player 文本路径;空字符串 fallback 到 blank。
- use_text_prompt=False:返回 dataset 提供的 context,去掉 DataLoader batch 维;
通常为 per-player (P, seq_len, 4096),没有 CSV prompt_emb_path 时 P 个 player 共用 blank。
注意:返回的是 T5 原始 embedding(4096 dim),DiT 内部 text_embedding MLP 负责投影到 3072。
"""
_dtype = self.pipe.torch_dtype
_dev = self.device
prompt_emb = batch.get("prompt_emb") or {}
context = prompt_emb.get("context") if isinstance(prompt_emb, dict) else None
if context is None:
return self._blank_context_for_players(
self.pipe.num_players, dtype=_dtype, device=_dev
)
return context[0].to(dtype=_dtype, device=_dev)
def _blank_context_for_players(
self, num_players: int, *, dtype, device
) -> torch.Tensor:
blank = self._encode_prompt_with_cache("").to(dtype=dtype, device=device)
if blank.ndim != 2:
raise ValueError(
f"blank prompt context must be [S,D], got {tuple(blank.shape)}"
)
return blank.unsqueeze(0).expand(int(num_players), -1, -1).contiguous()
def _blank_context_like(self, context: torch.Tensor) -> torch.Tensor:
blank = self._blank_context_for_players(
int(context.shape[0]), dtype=context.dtype, device=context.device
)
if tuple(blank.shape[1:]) != tuple(context.shape[1:]):
raise ValueError(
f"blank prompt context shape {tuple(blank.shape)} does not match training context {tuple(context.shape)}"
)
return blank
def _maybe_drop_text_prompt_context(
self, context: torch.Tensor
) -> tuple[torch.Tensor, bool]:
p = self.text_prompt_dropout_p
if p <= 0.0 or not self.training:
return (context, False)
if bool(torch.rand((), device=context.device) < p):
return (self._blank_context_like(context), True)
return (context, False)
@staticmethod
def _encode_latents(pipe, video, tiler_kwargs):
"""VAE encode 可能返回 tensor 或 list/tuple,统一为 tensor。"""
z = pipe.encode_video(video, **tiler_kwargs)
if isinstance(z, (list, tuple)):
z = z[0]
return z
def _should_corrupt_first_frame_anchor(self, seg_idx: int) -> bool:
if self.first_frame_noise_p <= 0.0:
return False
if seg_idx < self.first_frame_noise_min_seg:
return False
return bool(torch.rand(1, device=self.device).item() < self.first_frame_noise_p)
def _corrupt_origin_frame0(self, origin_latents: torch.Tensor) -> torch.Tensor:
"""(1-σ)*z + σ*ε on latent frame 0; σ ~ U(0, corrupt_ratio). Aligns with flow_match.add_noise."""
out = origin_latents.clone()
z0 = out[:, :, 0:1]
eps = torch.randn_like(z0)
sigma = get_corrupt_noise_sigma(
z0,
batch_size=z0.shape[0],
corrupt_ratio=self.first_frame_noise_corrupt_ratio,
num_frames=1,
is_frame_independent=False,
)
out[:, :, 0:1] = (1.0 - sigma) * z0 + sigma * eps
return out
def _prepare_simplex_agents(self, latents, keyboard, mouse):
"""Select simplex vertices, then optionally permute aligned player streams."""
num_players = latents.shape[0]
pool = self.simplex_pool_size
if pool < num_players:
raise ValueError(f"simplex_pool_size={pool} < num_players={num_players}")
if self.simplex_train_sample_vertex_ids:
vertex_ids = random.sample(range(pool), num_players)
else:
vertex_ids = list(range(num_players))
if self.simplex_train_permute:
perm = torch.randperm(num_players).tolist()
vertex_ids = [vertex_ids[i] for i in perm]
latents = latents[perm]
keyboard = keyboard[perm]
mouse = mouse[perm]
return (latents, keyboard, mouse, vertex_ids)
def _apply_first_frame_conditioning(
self,
latents: torch.Tensor,
noise: torch.Tensor,
timestep: torch.Tensor,
*,
seg_idx: int = 0,
allow_drop: bool = True,
):
"""Build noisy_latents with optional DF-style corrupt on anchor frame 0."""
origin_latents = latents.clone().detach()
if timestep.ndim >= 1 and timestep.numel() > 1:
t_bf = timestep.reshape(latents.shape[0], latents.shape[2])
sigma, _ = self.pipe.scheduler._sigma_from_timestep(
t_bf, device=latents.device, dtype=latents.dtype
)
sigma = sigma.view(latents.shape[0], 1, latents.shape[2], 1, 1)
noisy_latents = (1.0 - sigma) * latents + sigma * noise
else:
noisy_latents = self.pipe.scheduler.add_noise(latents, noise, timestep)
drop_first_frame = (
allow_drop
and self.first_frame_dropout_p > 0.0
and bool(torch.rand(1).item() < self.first_frame_dropout_p)
)
corrupt_anchor = False
if not drop_first_frame:
corrupt_anchor = self._should_corrupt_first_frame_anchor(seg_idx)
if corrupt_anchor:
origin_latents = self._corrupt_origin_frame0(origin_latents)
noisy_latents[:, :, 0:1, ...] = origin_latents[:, :, 0:1, ...]
return (noisy_latents, drop_first_frame, corrupt_anchor)
def _stage2_causal_training_step(
self,
latents: torch.Tensor,
keyboard: torch.Tensor,
mouse: torch.Tensor,
prompt_emb: dict,
agent_vertex_ids,
image_emb: dict,
batch: dict,
) -> torch.Tensor:
"""Causal-Forcing teacher-forcing training for Stage 2.
Uses a SINGLE parallel forward (WanModel.forward, same as standard training),
NOT the KV-cache causal path. Using KV cache during the supervised block's
forward breaks gradient flow to Wk, Wv, and hub_embed via in-place cache
writes (cache tensors are requires_grad=False, so the autograd graph is severed
at the slice-assignment boundary).
Algorithm:
- Sample supervised block k ∈ [0, num_blocks-1].
- Context blocks 0..k-1: timestep = 0 → sigma = 0 → noisy = clean latents.
- Supervised block k: timestep = random t → noisy = latents + noise.
- Frame 0: always clean (I2V anchor), timestep = 0.
- Single WanModel.forward over the full sequence (block-causal mask handles
attention restriction). Loss computed only on supervised block k.
This gives full gradient flow to all trainable parameters (Wq, Wk, Wv, Wo,
hub_embed, action layers) and matches inference semantics: block k is denoised
while attending to clean context from earlier blocks.
"""
B, C, f_latent, H, W = latents.shape
n = self.num_frame_per_block
num_blocks = f_latent // n
device = latents.device
k = int(torch.randint(0, num_blocks, (1,)).item())
t_idx = torch.randint(0, self.pipe.scheduler.num_train_timesteps, (1,))
t_val = self.pipe.scheduler.timesteps[t_idx].to(
dtype=self.pipe.torch_dtype, device=device
)
timestep = torch.zeros(
[B, f_latent], dtype=self.pipe.torch_dtype, device=device
)
timestep[:, k * n : (k + 1) * n] = t_val
timestep[:, 0] = 0.0
noise = torch.randn_like(latents)
sigma, _ = self.pipe.scheduler._sigma_from_timestep(
timestep, device=device, dtype=latents.dtype
)
sigma_view = sigma.view(B, 1, f_latent, 1, 1)
noisy_latents = (1.0 - sigma_view) * latents + sigma_view * noise
noisy_latents[:, :, 0:1] = latents[:, :, 0:1]
df_supervise_mask = torch.zeros(f_latent, dtype=torch.bool, device=device)
df_supervise_mask[k * n : (k + 1) * n] = True
df_supervise_mask[0] = False
training_target = self.pipe.scheduler.training_target(latents, noise, timestep)
extra_input = self.pipe.prepare_extra_input(latents)
dit_kwargs = dict(
mouse=mouse,
keyboard=keyboard,
loss_type="diffusion_forcing",
**prompt_emb,
**extra_input,
**image_emb,
use_gradient_checkpointing=self.use_gradient_checkpointing,
use_gradient_checkpointing_offload=self.use_gradient_checkpointing_offload,
)
if agent_vertex_ids is not None:
dit_kwargs["agent_vertex_ids"] = agent_vertex_ids
_attach_hier_group_dit_kwargs(dit_kwargs, batch, self.device)
if self.use_camera_pose:
if "camera_pose" not in batch:
raise KeyError(
"--use_camera_pose 需要 dataset 返回 camera_pose(CSV camera_pose_path* / camera12_path*)"
)
dit_kwargs["camera_pose"] = batch["camera_pose"][0].to(
device=self.device, dtype=self.pipe.torch_dtype
)
noise_pred = self.pipe.denoising_model()(
noisy_latents, timestep=timestep, **dit_kwargs
)
sel = df_supervise_mask
loss = F.mse_loss(
noise_pred[:, :, sel, ...].float(), training_target[:, :, sel, ...].float()
)
loss_weight = self.pipe.scheduler.training_weight(timestep[:, sel])
loss = loss * loss_weight
if not torch.isfinite(loss.detach()).all():
raise RuntimeError(
f"Non-finite training loss at step={int(self.global_step)} loss={loss.detach().float().cpu()}"
)
return loss
def _select_generated_history_plan(self, num_blocks: int):
"""Return a rank-synchronous (target k, history m, start g0) plan or None.
The decision uses only global_step, so every DDP rank takes the same branch and
uses the same block counts. It does not consume the training RNG.
"""
if self.generated_history_prob <= 0.0 or num_blocks < 2:
return None
step = int(self.global_step)
mix_hash = step * 1103515245 + 12345 & 4294967295
if mix_hash / float(1 << 32) >= self.generated_history_prob:
return None
plan_hash = (step + 1) * 2654435761 + 1013904223 & 4294967295
k = 1 + plan_hash % (num_blocks - 1)
max_m = min(3, k)
m = 1 + (plan_hash >> 16) % max_m
return (int(k), int(m), int(k - m))
@torch.inference_mode()
def _generated_history_negative_context(self, num_players: int, *, dtype, device):
"""Encode the deployment negative prompt once and keep its cache on CPU."""
if self._generated_history_negative_context_cpu is None:
self.pipe.text_encoder.to(device)
encoded = self.pipe.encode_prompt(
[_GENERATED_HISTORY_NEGATIVE_PROMPT] * int(num_players), positive=False
)["context"]
self._generated_history_negative_context_cpu = encoded.detach().cpu()
self.pipe.text_encoder.to("cpu")
if torch.cuda.is_available():
torch.cuda.empty_cache()
context = self._generated_history_negative_context_cpu
if int(context.shape[0]) != int(num_players):
raise ValueError(
f"negative prompt cache P={context.shape[0]} does not match P={num_players}"
)
return context.to(device=device, dtype=dtype)
@torch.inference_mode()
def _rollout_generated_history_online(
self,
*,
latents,
keyboard,
mouse,
positive_context,
agent_vertex_ids,
batch,
target_block,
history_blocks,
batch_idx,
):
"""Generate the most recent 1--3 history blocks with the real cached CFG path.
Input latents are [P,C,T,H,W]. Blocks [0,g0) are committed as clean GT,
blocks [g0,k) are generated with the deployment sampler, and the returned
[P,C,m*n,H,W] tensor is detached. The student target is intentionally not
evaluated here; it stays on the parallel differentiable training path.
"""
P, C, T, H, W = latents.shape
n = int(self.num_frame_per_block)
k = int(target_block)
m = int(history_blocks)
g0 = k - m
if not (1 <= k < T // n and 1 <= m <= min(3, k)):
raise ValueError(f"invalid generated-history plan T={T} n={n} k={k} m={m}")
rollout_scheduler = copy.deepcopy(self.pipe.scheduler)
rollout_scheduler.set_timesteps(
self.val_num_inference_steps, denoising_strength=1.0, shift=5.0
)
negative_context = self._generated_history_negative_context(
P, dtype=latents.dtype, device=latents.device
)
group_labels = batch["group_labels"][0].to(
device=latents.device, dtype=torch.long
)
active_group = batch["active_group"][0].to(
device=latents.device, dtype=torch.bool
)
membership = batch.get("membership")
global_only = batch.get("global_only")
if membership is not None:
if global_only is None:
raise KeyError(
"membership generated-history rollout requires global_only"
)
membership = membership[0].to(device=latents.device, dtype=torch.bool)
global_only = global_only[0].to(device=latents.device, dtype=torch.bool)
camera_pose = None
if self.use_camera_pose:
camera_pose = batch["camera_pose"][0].to(
device=latents.device, dtype=latents.dtype
)
def group_kwargs(lo, hi):
out = {
"group_labels": group_labels[lo:hi],
"active_group": active_group[lo:hi],
}
if membership is not None:
out.update(
membership=membership,
global_only=global_only,
active_group=active_group,
)
return out
old_kv, old_cross = (self.pipe.kv_cache1, self.pipe.crossattn_cache)
def new_cache_pair():
self.pipe.kv_cache1, self.pipe.crossattn_cache = (None, None)
self.pipe.init_kv_cache_for_training(batch_size=P, device=latents.device)
return (self.pipe.kv_cache1, self.pipe.crossattn_cache)
kv_pos, cross_pos = new_cache_pair()
kv_neg, cross_neg = new_cache_pair()
if kv_pos is kv_neg or cross_pos is cross_neg:
raise AssertionError("positive and negative CFG caches must be independent")
token_stride = int(self.pipe.frame_seq_length) * P
common = dict(
mouse=mouse,
keyboard=keyboard,
loss_type="diffusion_forcing",
agent_vertex_ids=agent_vertex_ids,
camera_pose_full=camera_pose,
)
pos_common = dict(
common, context=positive_context, kv_cache=kv_pos, crossattn_cache=cross_pos
)
neg_common = dict(
common, context=negative_context, kv_cache=kv_neg, crossattn_cache=cross_neg
)
module_states = [
(module, module.training) for module in self.pipe.dit.modules()
]
self.pipe.dit.eval()
generated = []
started = time.perf_counter()
try:
warm_t = torch.zeros((P, 1), device=latents.device, dtype=torch.int64)
for branch in (pos_common, neg_common):
model_fn_wan_video_causal(
self.pipe.dit,
latents[:, :, :1],
timestep=warm_t,
current_start=0,
cache_start=0,
**group_kwargs(0, 1),
**branch,
)
for block_idx in range(g0):
lo, hi = (block_idx * n, (block_idx + 1) * n)
context_timestep = torch.zeros(
(P, n), device=latents.device, dtype=torch.int64
)
for branch in (pos_common, neg_common):
model_fn_wan_video_causal(
self.pipe.dit,
latents[:, :, lo:hi],
timestep=context_timestep,
current_start=lo * token_stride,
cache_start=lo * token_stride,
**group_kwargs(lo, hi),
**branch,
)
generator = torch.Generator(device=latents.device)
seed = (
12648430
+ 1000003 * int(self.global_rank)
+ 9176 * int(self.global_step)
+ int(batch_idx)
& 9223372036854775807
)
generator.manual_seed(seed)
for block_idx in range(g0, k):
lo, hi = (block_idx * n, (block_idx + 1) * n)
noisy = torch.randn(
(P, C, n, H, W),
generator=generator,
device=latents.device,
dtype=latents.dtype,
)
if block_idx == 0:
noisy[:, :, :1] = latents[:, :, :1]
for current_timestep in rollout_scheduler.timesteps:
timestep = torch.full(
(P, n),
current_timestep,
device=latents.device,
dtype=torch.int64,
)
if block_idx == 0:
timestep[:, 0] = 0
noisy[:, :, :1] = latents[:, :, :1]
pred_pos = model_fn_wan_video_causal(
self.pipe.dit,
noisy,
timestep=timestep,
current_start=lo * token_stride,
cache_start=lo * token_stride,
**group_kwargs(lo, hi),
**pos_common,
)
pred_neg = model_fn_wan_video_causal(
self.pipe.dit,
noisy,
timestep=timestep,
current_start=lo * token_stride,
cache_start=lo * token_stride,
**group_kwargs(lo, hi),
**neg_common,
)
denoised_pred = pred_neg + 5.0 * (pred_pos - pred_neg)
noisy = rollout_scheduler.step(
denoised_pred, current_timestep, noisy
)
if block_idx == 0:
noisy[:, :, :1] = latents[:, :, :1]
generated.append(noisy.detach().clone())
context_timestep = torch.zeros(
(P, n), device=latents.device, dtype=torch.int64
)
for branch in (pos_common, neg_common):
model_fn_wan_video_causal(
self.pipe.dit,
noisy,
timestep=context_timestep,
current_start=lo * token_stride,
cache_start=lo * token_stride,
**group_kwargs(lo, hi),
**branch,
)
finally:
for module, state in module_states:
module.training = state
self.pipe.kv_cache1, self.pipe.crossattn_cache = (old_kv, old_cross)
history = torch.cat(generated, dim=2).detach()
expected_shape = (P, C, m * n, H, W)
if tuple(history.shape) != expected_shape:
raise AssertionError(
f"generated history shape={tuple(history.shape)}, expected={expected_shape}"
)
if history.requires_grad or history.grad_fn is not None:
raise AssertionError("generated history must be detached")
if not torch.isfinite(history).all():
raise RuntimeError("generated history contains non-finite values")
if int(self.global_rank) == 0:
print(
f"[generated-history] step={int(self.global_step)} k={k} m={m} g0={g0} sampler_steps={self.val_num_inference_steps} cfg=5 rollout_s={time.perf_counter() - started:.3f} shape={tuple(history.shape)}",
flush=True,
)
return history
def training_step(self, batch, batch_idx):
self.pipe.device = self.device
perf = self._perf_timing_enabled
step_part_times = {"encode_target": 0.0, "dit_forward_loss": 0.0}
if perf:
t_encode_target_start = self._synced_time()
if "latents" in batch:
if batch["latents"].shape[0] != 1:
raise ValueError(
"N-player precomputed training currently expects DataLoader batch_size=1"
)
latents = (
batch["latents"]
.squeeze(0)
.to(dtype=self.pipe.torch_dtype, device=self.pipe.device)
)
if latents.shape[0] != self.pipe.num_players:
raise ValueError(
f"latents P={latents.shape[0]} does not match num_players={self.pipe.num_players}"
)
elif "latents1" in batch:
latents1 = batch["latents1"].to(
dtype=self.pipe.torch_dtype, device=self.pipe.device
)
latents2 = batch["latents2"].to(
dtype=self.pipe.torch_dtype, device=self.pipe.device
)
latents = torch.cat((latents1, latents2), dim=0)
else:
video1 = batch["video1"]
video1 = video1.to(dtype=self.pipe.torch_dtype, device=self.pipe.device)
latents1 = self._encode_latents(self.pipe, video1, self.tiler_kwargs)
latents1 = latents1.to(dtype=self.pipe.torch_dtype, device=self.pipe.device)
video2 = batch["video2"]
video2 = video2.to(dtype=self.pipe.torch_dtype, device=self.pipe.device)
latents2 = self._encode_latents(self.pipe, video2, self.tiler_kwargs)
latents2 = latents2.to(dtype=self.pipe.torch_dtype, device=self.pipe.device)
latents = torch.cat((latents1, latents2), dim=0)
if perf:
step_part_times["encode_target"] = max(
0.0, self._synced_time() - t_encode_target_start
)
image_emb = {}
keyboard = batch["keyboard"].squeeze(0)[:, :-1, :]
mouse = batch["mouse"].squeeze(0)[:, :-1, :]
keyboard = keyboard.to(device=self.pipe.device)
mouse = mouse.to(device=self.pipe.device)
if keyboard.shape[0] != latents.shape[0] or mouse.shape[0] != latents.shape[0]:
raise ValueError(
f"Player dim mismatch: latents P={latents.shape[0]}, keyboard P={keyboard.shape[0]}, mouse P={mouse.shape[0]}"
)
latents, keyboard, mouse, agent_vertex_ids = self._prepare_simplex_agents(
latents, keyboard, mouse
)
generated_history_plan = None
if self.generated_history_prob > 0.0:
if latents.shape[2] % self.num_frame_per_block != 0:
raise ValueError(
f"generated-history latent T={latents.shape[2]} is not divisible by block size {self.num_frame_per_block}"
)
generated_history_plan = self._select_generated_history_plan(
int(latents.shape[2]) // self.num_frame_per_block
)
prompt_emb = dict(batch.get("prompt_emb", {}))
prompt_context = self._get_context_for_seg(batch, seg_idx=0)
if generated_history_plan is None:
prompt_context, text_prompt_dropped = self._maybe_drop_text_prompt_context(
prompt_context
)
else:
text_prompt_dropped = False
prompt_emb["context"] = prompt_context
self.log(
"train/text_prompt_dropped",
1.0 if text_prompt_dropped else 0.0,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=False,
)
if perf:
t_dit_forward_loss_start = self._synced_time()
if self.causal_forcing_training:
loss = self._stage2_causal_training_step(
latents=latents,
keyboard=keyboard,
mouse=mouse,
prompt_emb=prompt_emb,
agent_vertex_ids=agent_vertex_ids,
image_emb=image_emb,
batch=batch,
)
loss_weight = torch.tensor(1.0)
self.log("train_loss", loss, prog_bar=True)
self.log(
"train/is_recon_warmup",
0.0,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=False,
)
self._latest_step_part_times = step_part_times
return loss
noise = torch.randn_like(latents)
f_latent = latents.shape[2]
df_supervise_mask = None
clean_prefix_block = None
n_block = self.num_frame_per_block
num_blocks = f_latent // n_block
if self.df_per_frame_noise:
timestep_per_frame = torch.randint(
0, self.pipe.scheduler.num_train_timesteps, (1, f_latent)
)
else:
timestep_id = torch.randint(
0, self.pipe.scheduler.num_train_timesteps, (1, num_blocks)
)
timestep_per_frame = timestep_id.repeat_interleave(n_block, dim=1)
if timestep_per_frame.shape[1] < f_latent:
pad = timestep_id[:, -1:].expand(
-1, f_latent - timestep_per_frame.shape[1]
)
timestep_per_frame = torch.cat([timestep_per_frame, pad], dim=1)
elif timestep_per_frame.shape[1] > f_latent:
timestep_per_frame = timestep_per_frame[:, :f_latent]
t_idx = self.pipe.scheduler.timesteps[timestep_per_frame[0]].to(
dtype=self.pipe.torch_dtype, device=self.pipe.device
)
timestep = t_idx.unsqueeze(0).expand(latents.shape[0], -1).clone()
if generated_history_plan is not None:
k, m, g0 = generated_history_plan
prefix_end = k * n_block
timestep[:, :prefix_end] = 0.0
cur_lo = k * n_block
cur_hi = min((k + 1) * n_block, f_latent)
df_supervise_mask = torch.zeros(
f_latent, dtype=torch.bool, device=timestep.device
)
df_supervise_mask[cur_lo:cur_hi] = True
generated_history = self._rollout_generated_history_online(
latents=latents,
keyboard=keyboard,
mouse=mouse,
positive_context=prompt_emb["context"],
agent_vertex_ids=agent_vertex_ids,
batch=batch,
target_block=k,
history_blocks=m,
batch_idx=batch_idx,
)
mixed_latents = latents.clone()
mixed_latents[:, :, g0 * n_block : k * n_block] = generated_history
latents = mixed_latents
elif (
self.clean_prefix_prob > 0.0
and num_blocks >= 2
and bool(torch.rand(1).item() < self.clean_prefix_prob)
):
k = int(torch.randint(1, num_blocks, (1,)).item())
clean_prefix_block = k
prefix_end = k * n_block
timestep[:, :prefix_end] = float(self.clean_prefix_context_noise)
cur_lo = k * n_block
cur_hi = min((k + 1) * n_block, f_latent)
df_supervise_mask = torch.zeros(
f_latent, dtype=torch.bool, device=timestep.device
)
df_supervise_mask[cur_lo:cur_hi] = True
timestep[:, 0] = 0.0
extra_input = self.pipe.prepare_extra_input(latents)
noisy_latents, drop_first_frame, corrupt_anchor = (
self._apply_first_frame_conditioning(
latents,
noise,
timestep,
seg_idx=0,
allow_drop=df_supervise_mask is None,
)
)
training_target = self.pipe.scheduler.training_target(latents, noise, timestep)
dit_kwargs = dict(
mouse=mouse,
keyboard=keyboard,
loss_type="diffusion_forcing",
**prompt_emb,
**extra_input,
**image_emb,
use_gradient_checkpointing=self.use_gradient_checkpointing,
use_gradient_checkpointing_offload=self.use_gradient_checkpointing_offload,
)
if agent_vertex_ids is not None:
dit_kwargs["agent_vertex_ids"] = agent_vertex_ids
_attach_hier_group_dit_kwargs(dit_kwargs, batch, self.device)
if self.use_camera_pose:
if "camera_pose" not in batch:
raise KeyError(
"--use_camera_pose 需要 dataset 返回 camera_pose(CSV camera_pose_path* / camera12_path*)"
)
dit_kwargs["camera_pose"] = batch["camera_pose"][0].to(
device=self.device, dtype=self.pipe.torch_dtype
)
noise_pred = self.pipe.denoising_model()(
noisy_latents, timestep=timestep, **dit_kwargs
)
if df_supervise_mask is not None:
sel = df_supervise_mask
loss = torch.nn.functional.mse_loss(
noise_pred[:, :, sel, ...].float(),
training_target[:, :, sel, ...].float(),
)
loss_weight = self.pipe.scheduler.training_weight(timestep[:, sel])
elif drop_first_frame:
loss = torch.nn.functional.mse_loss(
noise_pred.float(), training_target.float()
)
loss_weight = self.pipe.scheduler.training_weight(timestep)
else:
loss = torch.nn.functional.mse_loss(
noise_pred[:, :, 1:, ...].float(),
training_target[:, :, 1:, ...].float(),
)
loss_weight = self.pipe.scheduler.training_weight(timestep[:, 1:])
loss = loss * loss_weight
if not torch.isfinite(loss.detach()).all():
raise RuntimeError(
f"Non-finite training loss at step={int(self.global_step)} batch_idx={int(batch_idx)} loss={loss.detach().float().cpu()}"
)
if perf:
step_part_times["dit_forward_loss"] = max(
0.0, self._synced_time() - t_dit_forward_loss_start
)
self._latest_step_part_times = step_part_times
self.log("train_loss", loss, prog_bar=True)
self.log(
"train/is_recon_warmup",
0.0,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=False,
)
self.log(
"train/first_frame_dropped",
1.0 if drop_first_frame else 0.0,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=False,
)
self.log(
"train/first_frame_corrupted",
1.0 if corrupt_anchor else 0.0,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=False,
)
self.log(
"train/clean_prefix",
1.0 if clean_prefix_block is not None else 0.0,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=False,
)
self.log(
"train/generated_history",
1.0 if generated_history_plan is not None else 0.0,
on_step=True,
prog_bar=False,
logger=True,
sync_dist=False,
)
return loss
@torch.no_grad()
def _maybe_run_manual_validation(self):
"""方案二:按 global_step 触发验证,与 epoch 边界解耦。
在 on_train_batch_end 里每步调用。所有 rank 依据同一 global_step 判定是否验证
(DDP 下 global_step 同步,判定一致),只有 rank0 真正跑生成/记录(无集合通信),
其余 rank 到末尾 barrier 等待,避免跑太远 / logger 竞争。
"""
if not self.val_infer or self._manual_val_loader is None:
return
gs = int(self.global_step)
if gs <= 0 or gs % self.val_every_n_steps != 0:
return
if getattr(self, "_last_val_step", -1) == gs:
return
self._last_val_step = gs
was_training = self.training
self.eval()
try:
if self.trainer.is_global_zero:
try:
with torch.no_grad():
for i, batch in enumerate(self._manual_val_loader):
self._run_val_sample(batch, i)
except Exception as e:
print(
f"[val] manual validation failed @ step {gs}: {e}", flush=True
)
finally:
if was_training:
self.train()
self.pipe.scheduler.set_timesteps(1000, training=True)
if (
getattr(self.trainer, "world_size", 1) > 1
and torch.distributed.is_initialized()
):
torch.distributed.barrier()
def _run_val_sample(self, batch, batch_idx):
"""训练集前 N 条的 causal_inference 可视化:只在 rank0 跑,生成/GT 视频记到 logger。
依赖 PrecomputedLatentDataset 的 batch 结构(batch_size=1):
latents [1,P,48,T_lat,h,w]、keyboard [1,P,T,23]、mouse [1,P,T,2]。
KV cache 与 δ-mem state 在 causal_inference 内部逐次 reset,故每条样本独立。
"""
if "latents" not in batch and "latents1" not in batch:
return
from .data.video import save_video
from multics.data.utils import tensor_bchw_to_pil_list, pad_pil_list_to_length
from PIL import Image as _PILImage
gs = int(self.global_step)
_dtype, _dev = (self.pipe.torch_dtype, self.device)
if "latents" in batch:
video_latents = batch["latents"].squeeze(0).to(dtype=_dtype, device=_dev)
else:
video_latents = torch.cat([batch["latents1"], batch["latents2"]], dim=0).to(
dtype=_dtype, device=_dev
)
P = int(video_latents.shape[0])
keyboard = batch["keyboard"].squeeze(0)[:, :-1, :].to(_dev)
mouse = batch["mouse"].squeeze(0)[:, :-1, :].to(_dev)
camera_pose = None
if self.use_camera_pose:
if "camera_pose" not in batch:
raise KeyError("--use_camera_pose validation 需要 camera_pose")
camera_pose = batch["camera_pose"].squeeze(0).to(dtype=_dtype, device=_dev)
self.pipe.load_models_to_device(["vae"])
gt_all = self.pipe.decode_video(video_latents, **self.tiler_kwargs)
gt_a = gt_all[0:1]
gt_b = gt_all[1:2]
prompt = ""
negative_prompt = ""
has_caption_prompt = False
if "prompt" in batch:
prompt = batch["prompt"]
if isinstance(prompt, (list, tuple)):
prompt = [
p[0] if isinstance(p, (list, tuple)) and len(p) > 0 else p
for p in prompt
]
else:
prompt = [prompt]
prompt = [str(p).strip() for p in prompt]
has_caption_prompt = any(prompt)
if has_caption_prompt:
negative_prompt = [
"色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
] * len(prompt)
else:
prompt = ""
val_cfg_scale = 5.0 if has_caption_prompt else 1.0
if batch_idx == 0:
print(
f"[val] prompt={('caption1/caption2' if has_caption_prompt else 'blank')} cfg_scale={val_cfg_scale}",
flush=True,
)
try:
hier_infer_kwargs = {}
_attach_hier_group_dit_kwargs(hier_infer_kwargs, batch, self.device)
gen_players = self.pipe.causal_inference(
prompt=prompt,
negative_prompt=negative_prompt,
video1=gt_a,
video2=gt_b,
mouse=mouse,
keyboard=keyboard,
num_frames=self.val_causal_num_frames,
height=gt_a.shape[-2],
width=gt_a.shape[-1],
cfg_scale=val_cfg_scale,
num_inference_steps=self.val_num_inference_steps,
seed=0,
tiled=self.tiler_kwargs["tiled"],
tile_size=self.tiler_kwargs["tile_size"],
tile_stride=self.tiler_kwargs["tile_stride"],
loss_type="diffusion_forcing",
return_both_players=True,
agent_vertex_ids=self.val_agent_vertex_ids,
video_latents=video_latents,
camera_pose=camera_pose,
overlap_path=None,
**hier_infer_kwargs,
)
finally:
self.pipe.scheduler.set_timesteps(1000, training=True)
if isinstance(gen_players, tuple):
gen_players = list(gen_players)
if len(gen_players) != P:
raise RuntimeError(
f"validation generated {len(gen_players)} players, expected P={P}"
)
gen_a, gen_b = (gen_players[0], gen_players[1])
gt_a_pils = tensor_bchw_to_pil_list(gt_a)
gt_b_pils = tensor_bchw_to_pil_list(gt_b)
T = max(len(gen_a), len(gen_b), len(gt_a_pils), len(gt_b_pils))
ga = pad_pil_list_to_length(gen_a, T)
gb = pad_pil_list_to_length(gen_b, T)
qa = pad_pil_list_to_length(gt_a_pils, T)
qb = pad_pil_list_to_length(gt_b_pils, T)
w0, h0 = ga[0].size
_cell = lambda im: im if im.size == (w0, h0) else im.resize((w0, h0))
bw = 2
total_w, total_h = (2 * w0 + bw, 2 * h0 + bw)
grid = []
for t in range(T):
canvas = _PILImage.new("RGB", (total_w, total_h), (0, 0, 0))
canvas.paste(_cell(ga[t]), (0, 0))
canvas.paste(_cell(gb[t]), (w0 + bw, 0))
canvas.paste(_cell(qa[t]), (0, h0 + bw))
canvas.paste(_cell(qb[t]), (w0 + bw, h0 + bw))
grid.append(canvas)
os.makedirs(self.val_output_dir, exist_ok=True)
grid_path = os.path.join(self.val_output_dir, f"step{gs}_s{batch_idx}_grid.mp4")
save_video(grid, grid_path, fps=self.val_grid_fps, quality=5)
def configure_optimizers(self):
params = [
p for p in self.pipe.denoising_model().parameters() if p.requires_grad
]
optimizer = torch.optim.AdamW(params, lr=self.learning_rate)
return optimizer
def _resolve_local_version_dir(self):
"""Resolve a local run directory for logs and checkpoints."""
try:
ld = self.trainer.logger.log_dir
except Exception:
ld = None
if ld:
_out = os.path.abspath(self.output_path)
if os.path.abspath(ld).startswith(_out + os.sep):
return ld
ver = self.run_version or "version_0"
return os.path.join(self.output_path, ver)
def on_train_start(self):
"""Record trainable parameter names in the local run directory."""
if not self.trainer.is_global_zero:
return
local_version_dir = self._resolve_local_version_dir()
if not local_version_dir:
print(
"[on_train_start] cannot resolve local version dir; skip train_module.txt dump."
)
return
os.makedirs(local_version_dir, exist_ok=True)
file_path = os.path.join(local_version_dir, "train_module.txt")
with open(file_path, "w", encoding="utf-8") as f:
for name, param in self.pipe.dit.named_parameters():
f.write(f"name: {name} grad: {param.requires_grad}\n")
print(f"[on_train_start] wrote {file_path}")
def _assert_dit_state_finite_before_save(self):
bad = []
for name, param in self.pipe.denoising_model().named_parameters():
if not param.is_floating_point() and (not param.is_complex()):
continue
try:
finite = bool(torch.isfinite(param.detach()).all().item())
except RuntimeError as exc:
bad.append(
f"{name}: shape={tuple(param.shape)} dtype={param.dtype} check_failed={exc}"
)
continue
if not finite:
bad.append(f"{name}: shape={tuple(param.shape)} dtype={param.dtype}")
if len(bad) >= 12:
break
if bad:
raise RuntimeError(
"DiT contains non-finite parameter(s) at checkpoint save time; refusing to save a checkpoint that will produce NaN/black videos.\n"
+ "\n".join((f" - {item}" for item in bad))
)
def on_save_checkpoint(self, checkpoint):
checkpoint.clear()
if not self.trainer.is_global_zero:
return
checkpoint_dir = self.trainer.checkpoint_callback.dirpath
if self.ckpt_local_dir:
save_dir = self.ckpt_local_dir
else:
save_dir = os.path.join(self._resolve_local_version_dir(), "checkpoints")
os.makedirs(save_dir, exist_ok=True)
print(f"Checkpoint save_dir: {save_dir}")
current_step = self.global_step
print(f"Current step: {current_step}")
saved_files = []
self._assert_dit_state_finite_before_save()
fname = f"step{current_step}.ckpt"
torch.save(
self.pipe.denoising_model().state_dict(), os.path.join(save_dir, fname)
)
saved_files.append(fname)
def _trainer_cluster_kwargs():
"""Match Lightning num_nodes/devices to torchrun (WORLD_SIZE / LOCAL_WORLD_SIZE)."""
ws = os.environ.get("WORLD_SIZE")
lws = os.environ.get("LOCAL_WORLD_SIZE")
if ws is not None and lws is not None:
try:
ws_i, lws_i = (int(ws), int(lws))
if lws_i > 0 and ws_i >= lws_i and (ws_i % lws_i == 0):
return {"num_nodes": ws_i // lws_i, "devices": lws_i}
except ValueError:
pass
return {"num_nodes": 1, "devices": "auto"}
def _read_int_hparam(path: str, key: str, default: int = 0) -> int:
if not path or not os.path.isfile(path):
return int(default)
with open(path, encoding="utf-8") as f:
for raw in f:
field, sep, value = raw.strip().partition(":")
if not sep or field.strip() != key:
continue
value = value.strip()
try:
parsed = json.loads(value)
except json.JSONDecodeError:
parsed = value.strip("'\"")
if parsed in (None, ""):
return int(default)
return int(parsed)
return int(default)
def _resolve_train_step_offset(
resume_ckpt_path: str, explicit_offset, *, schedule_enabled: bool
) -> int:
"""Resolve cumulative completed updates for weight-only resume checkpoints."""
if explicit_offset is not None:
offset = int(explicit_offset)
if offset < 0:
raise ValueError(f"train_step_offset must be >= 0, got {offset}")
print(f"[train-step] using explicit train_step_offset={offset}", flush=True)
return offset
if not resume_ckpt_path:
return 0
source_path = os.environ.get("WAN_RESUME_CKPT_SOURCE_PATH") or resume_ckpt_path
match = re.fullmatch("step(\\d+)\\.ckpt", os.path.basename(source_path))
if match is None:
message = f"cannot infer train_step_offset: resume checkpoint basename must be stepN.ckpt, got {source_path!r}"
if schedule_enabled:
raise ValueError(message + "; pass --train_step_offset explicitly")
print(f"[train-step][warn] {message}; using 0", flush=True)
return 0
checkpoint_step = int(match.group(1))
parent_hparams = os.path.join(
os.path.dirname(os.path.dirname(source_path)), "hparams.yaml"
)
parent_offset = _read_int_hparam(parent_hparams, "train_step_offset", default=0)
offset = parent_offset + checkpoint_step
print(
f"[train-step] auto-resolved parent_offset={parent_offset} checkpoint_step={checkpoint_step} train_step_offset={offset} source={source_path}",
flush=True,
)
return offset
def _write_hparams_yaml(path, args):
"""Write Lightning-style hparams.yaml next to checkpoints (TB logger does not)."""
payload = {k: "" if v is None else str(v) for k, v in vars(args).items()}
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
for key, value in payload.items():
f.write(f"{key}: {json.dumps(value, ensure_ascii=False)}\n")
def _compute_run_version(output_path):
"""Choose the next local lightning_logs/version_N directory."""
base = os.path.join(output_path, "lightning_logs")
max_ver = -1
if os.path.isdir(base):
for name in os.listdir(base):
m = re.match("version_(\\d+)$", name)
if m:
max_ver = max(max_ver, int(m.group(1)))
return f"version_{max_ver + 1}"
def train(args):
schedule_enabled = (
args.local_attn_size_after is not None
or args.local_attn_switch_step is not None
)
if (args.local_attn_size_after is None) != (args.local_attn_switch_step is None):
raise ValueError(
"--local_attn_size_after and --local_attn_switch_step must be set together"
)
if args.local_attn_switch_step is not None and args.local_attn_switch_step < 0:
raise ValueError("--local_attn_switch_step must be >= 0")
args.train_step_offset = _resolve_train_step_offset(
args.resume_ckpt_path, args.train_step_offset, schedule_enabled=schedule_enabled
)
latent_t = (args.target_rgb_frames - 1) // 4 + 1
if latent_t % args.num_frame_per_block != 0:
raise ValueError(
f"latent_T={latent_t} must be divisible by num_frame_per_block={args.num_frame_per_block}"
)
if args.local_attn_size != -1 and args.local_attn_size < args.num_frame_per_block:
raise ValueError(
f"local_attn_size must be -1 or >= num_frame_per_block; got local={args.local_attn_size}, n={args.num_frame_per_block}"
)
if (
args.local_attn_size_after is not None
and args.local_attn_size_after != -1
and (args.local_attn_size_after < args.num_frame_per_block)
):
raise ValueError(
f"local_attn_size_after must be -1 or >= num_frame_per_block; got local={args.local_attn_size_after}, n={args.num_frame_per_block}"
)
if args.simplex_train_permute:
raise ValueError("hier_hub requires --no_simplex_train_permute")
kg = args.num_global_hub_tokens
if kg is None:
kg = args.num_hub_tokens - args.num_groups * args.num_local_hub_tokens
if args.num_groups * args.num_local_hub_tokens + kg != args.num_hub_tokens:
raise ValueError(
f"hier_hub requires num_groups*num_local_hub_tokens + num_global_hub_tokens == num_hub_tokens; got {args.num_groups}*{args.num_local_hub_tokens}+{kg} != {args.num_hub_tokens}"
)
if args.hier_no_local_hub:
if args.hier_local_hub_mode != "shared":
raise ValueError(
"--hier_no_local_hub requires --hier_local_hub_mode shared"
)
if args.num_groups <= 0 or args.num_local_hub_tokens != 0 or kg <= 0:
raise ValueError(
f"--hier_no_local_hub requires num_groups>0, K_L=0, and K_G>0; got N={args.num_groups}, K_L={args.num_local_hub_tokens}, K_G={kg}"
)
if args.attn_impl == "hier_tiled_ste":
raise ValueError("--hier_no_local_hub does not support hier_tiled_ste")
elif args.num_local_hub_tokens <= 0:
raise ValueError("ordinary hier_hub requires num_local_hub_tokens>0")
print(
"[train] 使用预压 latent 数据集(PrecomputedLatentDataset),跳过在线视频/VAE 编码。"
)
dataset = PrecomputedLatentDataset(
args.dataset_path,
args.metadata_file_name
if os.path.isabs(args.metadata_file_name)
else os.path.join(args.dataset_path, args.metadata_file_name),
steps_per_epoch=args.steps_per_epoch,
num_players=args.num_players,
target_rgb_frames=args.target_rgb_frames,
blank_prompt_emb_path=args.blank_prompt_emb_path,
vae_latent_channels=args.vae_latent_channels,
num_frame_per_block=args.num_frame_per_block,
local_attn_size=args.local_attn_size,
use_camera_pose=args.use_camera_pose,
use_group_labels=True,
num_groups=args.num_groups,
group_path_column=args.group_path_column,
group_label_player_ids=args.group_label_player_ids,
hier_group_mode=args.hier_group_mode,
hier_group_seed=args.hier_group_seed,
hier_random_preserve_sizes=bool(args.hier_random_preserve_sizes),
hier_group_format=args.hier_group_format,
grouping_cache_dir=args.grouping_cache_dir,
grouping_model_profile=(args.num_local_hub_tokens, args.num_global_hub_tokens,
args.hier_direct_frac, args.num_frame_per_block),
)
dataloader = torch.utils.data.DataLoader(
dataset, shuffle=True, batch_size=1, num_workers=args.dataloader_num_workers
)
val_loader = None
if args.val_infer:
from torch.utils.data import Subset
n_val = min(args.val_num_samples, len(dataset))
val_loader = torch.utils.data.DataLoader(
Subset(dataset, list(range(n_val))),
shuffle=False,
batch_size=1,
num_workers=2,
)
print(
f"[val] inline validation enabled: {n_val} samples, every {args.val_every_n_steps} steps."
)
run_version = args.run_version or _compute_run_version(args.output_path)
print(
f"[run] version = {run_version}"
+ (" (来自 --run_version)" if args.run_version else " (扫描 max+1)")
)
model = LightningModelForTrain(
vae_path=args.vae_path,
text_encoder_path=args.text_encoder_path,
image_encoder_path=args.image_encoder_path,
dit_path=args.dit_path,
learning_rate=args.learning_rate,
use_gradient_checkpointing=args.use_gradient_checkpointing,
use_gradient_checkpointing_offload=args.use_gradient_checkpointing_offload,
resume_ckpt_path=args.resume_ckpt_path,
output_path=os.path.join(args.output_path, "lightning_logs"),
num_players=args.num_players,
vae_latent_channels=args.vae_latent_channels,
perf_log_every_n_steps=args.perf_log_every_n_steps,
perf_timing=not args.no_perf_timing,
target_rgb_frames=args.target_rgb_frames,
first_frame_dropout_p=args.first_frame_dropout_p,
first_frame_noise_p=args.first_frame_noise_p,
first_frame_noise_corrupt_ratio=args.first_frame_noise_corrupt_ratio,
first_frame_noise_min_seg=args.first_frame_noise_min_seg,
ckpt_local_dir=args.ckpt_local_dir,
run_version=run_version,
blank_prompt_emb_path=args.blank_prompt_emb_path,
text_prompt_dropout_p=args.text_prompt_dropout_p,
use_simplex_rope=True,
simplex_pool_size=args.simplex_pool_size,
simplex_gamma=args.simplex_gamma,
simplex_train_permute=args.simplex_train_permute,
simplex_train_sample_vertex_ids=args.simplex_train_sample_vertex_ids,
training_stage="stage2",
loss_type="diffusion_forcing",
attention_mode="hier_hub",
attn_impl=args.attn_impl,
num_hub_tokens=args.num_hub_tokens,
local_attn_size=args.local_attn_size,
local_attn_size_after=args.local_attn_size_after,
local_attn_switch_step=args.local_attn_switch_step,
train_step_offset=args.train_step_offset,
num_frame_per_block=args.num_frame_per_block,
num_groups=args.num_groups,
num_local_hub_tokens=args.num_local_hub_tokens,
num_global_hub_tokens=args.num_global_hub_tokens,
hier_no_local_hub=args.hier_no_local_hub,
hier_local_hub_mode=args.hier_local_hub_mode,
hier_direct_schedule=args.hier_direct_schedule,
hier_direct_frac=args.hier_direct_frac,
hier_direct_layers=args.hier_direct_layers,
unfreeze_self_attn=args.unfreeze_self_attn or True,
clean_prefix_prob=args.clean_prefix_prob,
clean_prefix_context_noise=args.clean_prefix_context_noise,
generated_history_prob=args.generated_history_prob,
df_per_frame_noise=args.df_per_frame_noise,
causal_forcing_training=args.causal_forcing_training,
use_camera_pose=args.use_camera_pose,
camera_dim=args.camera_dim,
camera_dropout_p=args.camera_dropout_p,
val_infer=args.val_infer,
val_every_n_steps=args.val_every_n_steps,
val_num_samples=args.val_num_samples,
val_num_inference_steps=args.val_num_inference_steps,
val_causal_num_frames=args.val_causal_num_frames,
val_output_dir=args.val_output_dir,
val_grid_fps=args.val_grid_fps,
)
model._manual_val_loader = val_loader
if args.use_swanlab:
from swanlab.integration.pytorch_lightning import SwanLabLogger
swanlab_config = {"UPPERFRAMEWORK": "DiffSynth-Studio"}
swanlab_config.update(vars(args))
swanlab_logger = SwanLabLogger(
project="wan",
name=f"wan_{run_version}",
config=swanlab_config,
mode=args.swanlab_mode,
logdir=os.path.join(args.output_path, "swanlog"),
)
logger = [swanlab_logger]
else:
logger = TensorBoardLogger(
save_dir=args.output_path, name="lightning_logs", version=run_version
)
logger.log_hyperparams({k: str(v) for k, v in vars(args).items()})
checkpoint_callback = pl.pytorch.callbacks.ModelCheckpoint(
every_n_train_steps=1000, save_top_k=2, monitor="step", mode="max"
)
_val_trainer_kwargs = {}
if args.val_infer:
_val_trainer_kwargs = dict(use_distributed_sampler=False)
trainer = pl.Trainer(
max_epochs=args.max_epochs,
max_steps=args.max_steps,
accelerator="gpu",
**_trainer_cluster_kwargs(),
precision="bf16",
strategy=args.training_strategy,
default_root_dir=args.output_path,
accumulate_grad_batches=args.accumulate_grad_batches,
gradient_clip_val=args.gradient_clip_val
if args.gradient_clip_val > 0
else None,
callbacks=[checkpoint_callback],
logger=logger,
**_val_trainer_kwargs,
)
if trainer.global_rank == 0:
_primary_logger = logger[0] if isinstance(logger, (list, tuple)) else logger
_log_dir = getattr(_primary_logger, "log_dir", None) or args.output_path
checkpoints_dir = os.path.join(_log_dir, "checkpoints")
os.makedirs(checkpoints_dir, exist_ok=True)
print(f"Manually created checkpoint directory for rank 0: {checkpoints_dir}")
version_dir = os.path.join(args.output_path, "lightning_logs", run_version)
hparams_path = os.path.join(version_dir, "hparams.yaml")
_write_hparams_yaml(hparams_path, args)
print(f"[hparams] wrote {hparams_path}")
trainer.fit(model, dataloader)