from .config import select_case_rows, case_output_id from .checkpoint import build_dit, load_complete_state_dict, stage2_options from .inference_args import parse_args import json import os import time import numpy as np import pandas as pd import torch from PIL import Image from .utils import ModelConfig from .pipelines.wan_video_Nplayers import WanVideoNPlayersPipeline from .data.video import save_video from multics.models.wan_video_dit import prepare_hier_local_hub_state_dict_for_load from multics.data.dataset import ( PrecomputedLatentDataset, consecutive_precomputed_start_rows, ) from multics.data.raw_inputs import ( encode_raw_first_frames, is_raw_metadata, prepare_raw_inputs, ) from multics.data.utils import tensor_bchw_to_pil_list, pad_pil_list_to_length def _timed(section: str): """Context manager: print load-stage timing (flush for tee/log).""" class _Ctx: def __enter__(self): print(f"[timing] >> {section}", flush=True) self._t0 = time.perf_counter() return self def __exit__(self, exc_type, exc, tb): dt = time.perf_counter() - self._t0 if exc_type is None: print(f"[timing] << {section} {dt:.2f}s", flush=True) else: print( f"[timing] !! {section} failed after {dt:.2f}s: {exc}", flush=True ) return False return _Ctx() def _load_dit_checkpoint_state(ckpt_path: str): """Load a DiT checkpoint and fail fast when any saved weight is non-finite.""" partial_ckpt = False if os.path.isdir(ckpt_path): index_path = os.path.join(ckpt_path, "pytorch_model.bin.index.json") if not os.path.exists(index_path): raise FileNotFoundError( f"Checkpoint directory has no pytorch_model.bin.index.json: {ckpt_path}" ) with open(index_path, "r") as f: index = json.load(f) shards = sorted(set(index.get("weight_map", {}).values())) if not shards: raise RuntimeError(f"No shards listed in checkpoint index: {index_path}") state_dict = {} for shard in shards: shard_path = os.path.join(ckpt_path, shard) with _timed(f"torch.load DiT ckpt shard {shard}"): shard_state = torch.load( shard_path, map_location="cpu", weights_only=True ) if not isinstance(shard_state, dict): raise RuntimeError( f"Checkpoint shard is not a state_dict: {shard_path}" ) state_dict.update(shard_state) partial_ckpt = True else: with _timed(f"torch.load DiT ckpt {os.path.basename(ckpt_path)}"): state_dict = torch.load(ckpt_path, map_location="cpu", weights_only=True) if ( isinstance(state_dict, dict) and "state_dict" in state_dict and isinstance(state_dict["state_dict"], dict) ): state_dict = state_dict["state_dict"] if not isinstance(state_dict, dict): raise RuntimeError(f"DiT checkpoint is not a state_dict: {ckpt_path}") if any((k.startswith("pipe.dit.") for k in state_dict.keys())): stripped = {} for key, value in state_dict.items(): if key.startswith("pipe.dit."): stripped[key[len("pipe.dit.") :]] = value state_dict = stripped partial_ckpt = True bad = [] for key, value in state_dict.items(): if not torch.is_tensor(value) or not ( value.is_floating_point() or value.is_complex() ): continue try: finite = bool(torch.isfinite(value).all().item()) except RuntimeError as exc: bad.append( f"{key}: shape={tuple(value.shape)} dtype={value.dtype} check_failed={exc}" ) continue if not finite: bad.append(f"{key}: shape={tuple(value.shape)} dtype={value.dtype}") if len(bad) >= 12: break if bad: raise RuntimeError( f"DiT checkpoint contains non-finite tensor(s); refusing to run inference because this will generate NaN/black video.\nckpt_path={ckpt_path}\n" + "\n".join((f" - {item}" for item in bad)) ) return (state_dict, partial_ckpt) def _patch_model_manager_load_timing(): """Log each Wan base weight file load inside from_pretrained.""" from multics.models import model_manager as mm if getattr(mm.ModelManager.load_model, "_infer_timing_patched", False): return _orig = mm.ModelManager.load_model def _load_model_timed( self, file_path, model_names=None, device=None, torch_dtype=None ): if isinstance(file_path, list): label = f"ModelManager.load_model [{len(file_path)} shards]" else: label = f"ModelManager.load_model {os.path.basename(str(file_path))}" with _timed(label): return _orig( self, file_path, model_names=model_names, device=device, torch_dtype=torch_dtype, ) _load_model_timed._infer_timing_patched = True mm.ModelManager.load_model = _load_model_timed NEGATIVE_PROMPT = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" def _batch_output_names(batch_idx: int, args) -> list: """Filenames written for one dataloader row. batch_idx is the CSV row index.""" names = [] if not args.no_save_separate_players: for pidx in range(args.num_players): names.append( f"video{batch_idx}_{_player_file_tag(pidx, args.num_players)}.mp4" ) if not args.no_save_grid: names.append(f"video{batch_idx}_grid.mp4") return names def _list_mp4_basenames(output_dir, s3_uri=None): return ( {name for name in os.listdir(output_dir) if name.endswith(".mp4")} if os.path.isdir(output_dir) else set() ) def _existing_batch_indices( n_planned: int, args, existing: set, output_ids=None ) -> set: done = set() for batch_idx in range(n_planned): expected = _batch_output_names( output_ids[batch_idx] if output_ids is not None else batch_idx, args ) if expected and all((name in existing for name in expected)): done.add(batch_idx) return done def _infer_rank_world(): rank = int(os.environ.get("RANK", "0")) world_size = int(os.environ.get("WORLD_SIZE", "1")) if world_size < 1: raise ValueError(f"WORLD_SIZE must be >= 1, got {world_size}") if rank < 0 or rank >= world_size: raise ValueError(f"RANK={rank} out of range for WORLD_SIZE={world_size}") return (rank, world_size) def _infer_shard_rows( n_planned: int, skip_batches: set, rank: int, world_size: int ) -> list: """CSV row ids this rank should generate. Names stay video{row}_*.mp4.""" return [ i for i in range(n_planned) if i % world_size == rank and i not in skip_batches ] def _infer_row_bounds(n_planned: int) -> tuple[int, int]: """Optional half-open row range for splitting one result repo across jobs.""" start = int(os.environ.get("INFER_ROW_START", "0")) end = int(os.environ.get("INFER_ROW_END", str(n_planned))) if start < 0 or end < start or end > n_planned: raise ValueError( f"invalid inference row range [{start}, {end}) for n_planned={n_planned}" ) return (start, end) TRAIN_HEIGHT = 352 TRAIN_WIDTH = 640 TILE_SIZE = (34, 34) TILE_STRIDE = (18, 16) def _resolve_metadata_path(dataset_path, metadata_file_name): if os.path.isabs(metadata_file_name): return metadata_file_name return os.path.join(dataset_path, metadata_file_name) def _precomputed_video_latents(batch) -> torch.Tensor: """[P,48,T_lat,h,w] from PrecomputedLatentDataset batch (batch_size=1).""" if "latents" in batch: if batch["latents"].shape[0] != 1: raise ValueError( "precomputed inference currently expects DataLoader batch_size=1" ) return batch["latents"].squeeze(0) return torch.cat([batch["latents1"], batch["latents2"]], dim=0) def _decode_latents_to_video( pipe, latents_b1: torch.Tensor, tiler_kwargs: dict ) -> torch.Tensor: """[1, 48, T_lat, h, w] -> [1, 3, T_rgb, H, W] for GT grid / parity.""" pipe.load_models_to_device(["vae"]) return pipe.decode_video( latents_b1.to(dtype=pipe.torch_dtype, device=pipe.device), **tiler_kwargs ) def _decode_latents_to_videos( pipe, latents_p: torch.Tensor, tiler_kwargs: dict ) -> torch.Tensor: """[P,48,T_lat,h,w] -> [P,3,T_rgb,H,W] for GT grids.""" pipe.load_models_to_device(["vae"]) return pipe.decode_video( latents_p.to(dtype=pipe.torch_dtype, device=pipe.device), **tiler_kwargs ) def _as_player_videos(outputs): """Normalize pipeline return to list length P; P=2 may be returned as a tuple for compatibility.""" if isinstance(outputs, tuple): return list(outputs) return outputs def _batch_prompt_strings(batch, num_players: int): """Return per-player prompt strings from a collated precomputed batch.""" if "prompt" not in batch: return None prompt = batch["prompt"] if isinstance(prompt, (list, tuple)): out = [ p[0] if isinstance(p, (list, tuple)) and len(p) > 0 else p for p in prompt ] else: out = [prompt] out = [str(p).strip() for p in out] if len(out) == 1 and num_players > 1: out = out * num_players if len(out) != num_players: raise ValueError( f"prompt count {len(out)} must match num_players={num_players}" ) return out def _prompt_context_from_batch(pipe, args, batch, prompt_strings): """Build T5 context [P,S,4096] or [1,S,4096] for full-sequence inference.""" prompt_emb = batch.get("prompt_emb", {}) context = prompt_emb.get("context") if isinstance(prompt_emb, dict) else None if context is not None: if context.dim() == 4: if context.shape[0] != 1: raise ValueError( f"prompt context batch must be 1, got {tuple(context.shape)}" ) context = context[0] return context.to(dtype=pipe.torch_dtype, device=pipe.device) pipe.load_models_to_device(["text_encoder"]) if prompt_strings is not None and any(prompt_strings): return pipe.encode_prompt(prompt_strings, positive=True)["context"].to( dtype=pipe.torch_dtype, device=pipe.device ) return pipe.encode_prompt("", positive=True)["context"].to( dtype=pipe.torch_dtype, device=pipe.device ) @torch.no_grad() def _hier_full_sequence_inference( pipe, args, batch, video_latents: torch.Tensor, mouse: torch.Tensor, keyboard: torch.Tensor, camera_pose: torch.Tensor, agent_vertex_ids, ): """Hierarchical inference with a full-sequence block-causal forward, no KV cache. Key tensors: video_latents: [P,C,T,h,w], clean GT latents used only for frame-0 anchor. latents: [P,C,T,h,w], denoising state. timestep: [P,T], frame 0 pinned to t=0; other frames use current sigma. group_labels: [1,T,P] from dataloader, passed to WanModel.forward. active_group: [1,T,N] from dataloader, passed to WanModel.forward. """ if "group_labels" not in batch or "active_group" not in batch: raise KeyError( "hier_hub full-sequence inference requires group_labels and active_group" ) if video_latents is None: raise ValueError( "hier_hub full-sequence inference requires --precomputed_latents" ) video_latents = video_latents.to(dtype=pipe.torch_dtype, device=pipe.device) P, C, T, h, w = video_latents.shape if T == 1: T = int(batch["group_labels"].shape[1]) if P != args.num_players: raise ValueError( f"video_latents P={P} must match --num_players={args.num_players}" ) group_labels = batch["group_labels"].to(device=pipe.device, dtype=torch.long) active_group = batch["active_group"].to(device=pipe.device, dtype=torch.bool) membership = batch.get("membership") global_only = batch.get("global_only") if membership is not None: membership = membership.to(device=pipe.device, dtype=torch.bool) if global_only is None: raise KeyError( "hier_group_format=membership requires batch key global_only" ) global_only = global_only.to(device=pipe.device, dtype=torch.bool) if group_labels.shape[1] != T or group_labels.shape[2] != P: raise ValueError( f"group_labels must be [1,T={T},P={P}], got {tuple(group_labels.shape)}" ) if active_group.shape[1] != T or active_group.shape[2] != args.num_groups: raise ValueError( f"active_group must be [1,T={T},N={args.num_groups}], got {tuple(active_group.shape)}" ) mouse = mouse.to(dtype=pipe.torch_dtype, device=pipe.device) keyboard = keyboard.to(dtype=pipe.torch_dtype, device=pipe.device) if camera_pose is not None: camera_pose = camera_pose.to(dtype=pipe.torch_dtype, device=pipe.device) pipe.scheduler.set_timesteps( args.num_inference_steps, denoising_strength=1.0, shift=5.0 ) noise = pipe.generate_noise( (P, C, T, h, w), seed=args.seed, device="cpu", dtype=torch.float32 ) latents = noise.to(dtype=pipe.torch_dtype, device=pipe.device) latents[:, :, 0:1, ...] = video_latents[:, :, 0:1, ...].clone() prompt_strings = _batch_prompt_strings(batch, args.num_players) prompt_context = _prompt_context_from_batch(pipe, args, batch, prompt_strings) has_prompt = bool(prompt_strings is not None and any(prompt_strings)) has_prompt_emb = "prompt_emb" in batch and batch["prompt_emb"] is not None cfg_scale = args.cfg_scale if has_prompt else 1.0 negative_context = None if cfg_scale != 1.0: pipe.load_models_to_device(["text_encoder"]) negative_context = pipe.encode_prompt([NEGATIVE_PROMPT] * P, positive=False)[ "context" ].to(dtype=pipe.torch_dtype, device=pipe.device) print( f"[full_sequence] P={P} latent_T={T} cfg_scale={cfg_scale} prompt={('caption/emb' if has_prompt or has_prompt_emb else 'blank')}", flush=True, ) pipe.load_models_to_device(["dit"]) for progress_id, step_t in enumerate(pipe.scheduler.timesteps): timestep = torch.full((P, T), step_t, dtype=torch.float32, device=pipe.device) timestep[:, 0] = 0.0 noise_pred_pos = pipe.dit( latents, timestep=timestep, context=prompt_context, mouse=mouse, keyboard=keyboard, loss_type="diffusion_forcing", agent_vertex_ids=agent_vertex_ids, camera_pose=camera_pose, group_labels=group_labels, active_group=active_group, membership=membership, global_only=global_only, ) if cfg_scale != 1.0: noise_pred_neg = pipe.dit( latents, timestep=timestep, context=negative_context, mouse=mouse, keyboard=keyboard, loss_type="diffusion_forcing", agent_vertex_ids=agent_vertex_ids, camera_pose=camera_pose, group_labels=group_labels, active_group=active_group, membership=membership, global_only=global_only, ) noise_pred = noise_pred_neg + cfg_scale * (noise_pred_pos - noise_pred_neg) else: noise_pred = noise_pred_pos latents = pipe.scheduler.step( noise_pred, pipe.scheduler.timesteps[progress_id], latents ) latents[:, :, 0:1, ...] = video_latents[:, :, 0:1, ...].clone() player_videos = pipe._player_videos_from_latents( latents, {"tiled": True, "tile_size": TILE_SIZE, "tile_stride": TILE_STRIDE} ) pipe.load_models_to_device([]) return _as_player_videos(pipe._return_player_videos(player_videos)) def _player_file_tag(player_idx: int, num_players: int) -> str: if num_players == 2: return "playerA" if player_idx == 0 else "playerB" return f"player{player_idx + 1:02d}" def _build_player_rows_grid_video(rows, t_grid: int, border: int = 1): """Build a generic rows x P grid. Tensor/PIL convention at caller: rows: List[R][P][T] of PIL frames. Current N-player inference uses R=2: row 0 = generated players [P,T] row 1 = GT players [P,T] """ if not rows or not rows[0]: raise ValueError("_build_player_rows_grid_video: empty rows") num_rows = len(rows) num_players = len(rows[0]) for ridx, row in enumerate(rows): if len(row) != num_players: raise ValueError( f"_build_player_rows_grid_video: row {ridx} has {len(row)} players, expected {num_players}" ) for pidx, frames in enumerate(row): if len(frames) < t_grid: raise ValueError( f"_build_player_rows_grid_video: row={ridx} player={pidx} has {len(frames)} frames, expected >= {t_grid}" ) w0, h0 = rows[0][0][0].size total_w = num_players * w0 + (num_players - 1) * border total_h = num_rows * h0 + (num_rows - 1) * border grid_frames = [] for tidx in range(t_grid): canvas = Image.new("RGB", (total_w, total_h), color=(0, 0, 0)) for ridx, row in enumerate(rows): for pidx, frames in enumerate(row): x = pidx * (w0 + border) y = ridx * (h0 + border) canvas.paste(frames[tidx], (x, y)) grid_frames.append(canvas) return grid_frames def main(): _t_main = time.perf_counter() print("[timing] main() start", flush=True) with _timed("parse_args"): args = parse_args() if args.num_players < 2: raise ValueError(f"--num_players must be >= 2, got {args.num_players}") agent_vertex_ids = args.agent_vertex_ids if agent_vertex_ids is None: agent_vertex_ids = list(range(args.num_players)) elif len(agent_vertex_ids) != args.num_players: raise ValueError( f"--agent_vertex_ids length ({len(agent_vertex_ids)}) must match --num_players ({args.num_players})" ) args.loss_type = "diffusion_forcing" with _timed("read metadata csv"): meta_path = _resolve_metadata_path(args.dataset_path, args.metadata_file_name) metadata_table = pd.read_csv(meta_path) n_rows = len(metadata_table) if args.precomputed_consecutive_rows < 1: raise ValueError("--precomputed_consecutive_rows must be >= 1") precomputed_start_rows = None precomputed_start_rows = consecutive_precomputed_start_rows( metadata_table, args.precomputed_consecutive_rows, args.num_players ) available_rows = len(precomputed_start_rows) if available_rows < 1: raise ValueError( f"metadata has no valid window for precomputed_consecutive_rows={args.precomputed_consecutive_rows} (source rows={n_rows})" ) n_planned = ( available_rows if args.max_batches is None else min(available_rows, args.max_batches) ) sample_metadata = metadata_table.iloc[precomputed_start_rows].reset_index(drop=True) output_ids = [case_output_id(metadata_table, row) for row in precomputed_start_rows] sample_metadata["case_id"] = output_ids rank, world_size = _infer_rank_world() row_start, row_end = _infer_row_bounds(n_planned) eligible = select_case_rows(args, sample_metadata, list(range(row_start, row_end))) selected = set(eligible) skip_batches = set() if os.environ.get("FORCE_INFER", "0") != "1": existing = _list_mp4_basenames(args.output_dir, None) skip_batches = _existing_batch_indices(n_planned, args, existing, output_ids) remaining = n_planned - len(skip_batches) print( f"[skip] {len(skip_batches)}/{n_planned} batches already have mp4 (local); remaining={remaining}; FORCE_INFER=1 to overwrite", flush=True, ) if skip_batches and remaining == 0: print("[skip] all planned videos exist; nothing to generate", flush=True) return if skip_batches: print(f"[skip] done batches={sorted(skip_batches)}", flush=True) my_rows = [ i for i in _infer_shard_rows(n_planned, skip_batches, rank, world_size) if i in selected ] print( f"[shard] rank={rank}/{world_size} n_planned={n_planned} range=[{row_start},{row_end}) skip={len(skip_batches)} my_rows={my_rows}", flush=True, ) if os.environ.get("INFER_SHARD_DRY", "0") == "1": print("[shard] INFER_SHARD_DRY=1, exit before model load", flush=True) return if not my_rows: print("[shard] nothing left on this rank; skip model load", flush=True) return raw_inputs = is_raw_metadata(metadata_table) if raw_inputs: if args.precomputed_consecutive_rows != 1: raise ValueError("Raw-input CSVs require precomputed_consecutive_rows=1") with _timed("prepare actions, cameras, and grouping from tick logs"): meta_path = prepare_raw_inputs( metadata_table, args.dataset_path, my_rows, args.num_players, args.target_rgb_frames, args.prepared_inputs_dir, ) print(f"[infer] prepared raw inputs: {meta_path}", flush=True) with _timed("dataset init"): dataset = PrecomputedLatentDataset( args.dataset_path, meta_path, steps_per_epoch=available_rows, 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, debug_no_retry=os.environ.get("INFER_SKIP_BAD_ROWS") != "1", 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, num_frame_per_block=args.num_frame_per_block, consecutive_rows=args.precomputed_consecutive_rows, grouping_sample_indices=my_rows, 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), allow_first_frame_latents=raw_inputs, ) local_rank = int(os.environ.get("LOCAL_RANK", "0")) if torch.cuda.is_available(): torch.cuda.set_device(local_rank) wan_root = args.wan_model_root.rstrip("/") print(f"[timing] wan_model_root={wan_root}", flush=True) print(f"[timing] ckpt_path={args.ckpt_path}", flush=True) _patch_model_manager_load_timing() with _timed("WanVideoNPlayersPipeline.from_pretrained (T5+VAE+tokenizer)"): pipe = WanVideoNPlayersPipeline.from_pretrained( torch_dtype=torch.bfloat16, device="cuda", tokenizer_config=ModelConfig( path=f"{wan_root}/Wan2.1-T2V-1.3B/google/umt5-xxl" ), model_configs=[ ModelConfig( path=f"{wan_root}/Wan2.1-T2V-1.3B/models_t5_umt5-xxl-enc-bf16.pth", offload_device="cpu", ), ModelConfig( path=f"{wan_root}/Wan2.2-TI2V-5B/Wan2.2_VAE.pth", offload_device="cpu", ), ], ) pipe.num_players = int(args.num_players) 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 divide 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}" ) 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}" ) infer_kernel = os.environ.get("WAN_HIER_INFER_KERNEL", "sdpa").strip().lower() if infer_kernel not in ("sdpa", "packed_varlen", "paged_kv"): raise ValueError( "--hier_no_local_hub supports WAN_HIER_INFER_KERNEL=sdpa, packed_varlen, or paged_kv; got " + infer_kernel ) elif args.num_local_hub_tokens <= 0: raise ValueError("ordinary hier_hub requires num_local_hub_tokens>0") # Set pipeline runtime options before attaching the DiT so its parameters # are configured exactly once, before loading the complete checkpoint. pipe.enable_stage2(**stage2_options(args)) with _timed("construct Multics DiT without base weights"): pipe.dit = build_dit(args) state_dict, _ = _load_dit_checkpoint_state(args.ckpt_path) if not args.hier_no_local_hub: state_dict, migration = prepare_hier_local_hub_state_dict_for_load( state_dict, target_mode=args.hier_local_hub_mode, target_num_groups=pipe.dit.num_groups, target_num_local_hub_tokens=pipe.dit.num_local_hub_tokens, model_dim=pipe.dit.dim, ) if migration is not None: print( f"[hier-local-hub] inference migration: {migration['source_mode']} {migration['source_shape']} -> {migration['target_mode']} {migration['target_shape']}", flush=True, ) if args.hier_no_local_hub: if "local_hub_embed" in state_dict: raise RuntimeError( "No-local inference refuses to migrate an old hierarchical checkpoint. Warm-start it through training with --hier_no_local_hub, save a new checkpoint, then infer from that checkpoint." ) global_seed = state_dict.get("global_hub_embed") expected = tuple(pipe.dit.global_hub_embed.shape) if not torch.is_tensor(global_seed) or tuple(global_seed.shape) != expected: raise RuntimeError( f"No-local checkpoint has an incompatible global_hub_embed: expected {expected}, got {(None if global_seed is None else tuple(global_seed.shape))}" ) with _timed("load complete Multics DiT checkpoint"): load_complete_state_dict(pipe.dit, state_dict) del state_dict with _timed("pipe.to(cuda) + bfloat16"): pipe.to("cuda") pipe.to(dtype=torch.bfloat16) if raw_inputs: with _timed("encode first frames with the VAE"): encode_raw_first_frames(pipe, meta_path, my_rows, args.num_players) os.makedirs(args.output_dir, exist_ok=True) if args.precomputed_consecutive_rows > 1: manifest = { "metadata_file": meta_path, "target_rgb_frames": int(args.target_rgb_frames), "consecutive_rows": int(args.precomputed_consecutive_rows), "samples": [ { "sample_index": sample_index, "case_id": output_ids[sample_index], "output_index": output_ids[sample_index], "source_rows": list( range( int(precomputed_start_rows[sample_index]), int(precomputed_start_rows[sample_index]) + args.precomputed_consecutive_rows, ) ), } for sample_index in range(n_planned) ], } with open( os.path.join(args.output_dir, "consecutive_rows_manifest.json"), "w", encoding="utf-8", ) as file: json.dump(manifest, file, indent=2, ensure_ascii=False) print( f"[timing] model load total {time.perf_counter() - _t_main:.2f}s", flush=True, ) loader = torch.utils.data.DataLoader( torch.utils.data.Subset(dataset, my_rows), shuffle=False, batch_size=1, num_workers=args.dataloader_num_workers, ) tiler_kwargs = {"tiled": True, "tile_size": TILE_SIZE, "tile_stride": TILE_STRIDE} target_text = "" _t_first_batch = time.perf_counter() for local_i, batch in enumerate(loader): batch_idx = output_ids[my_rows[local_i]] if local_i == 0: src = "precomputed latent/action" print( f"[timing] first dataloader batch {time.perf_counter() - _t_first_batch:.2f}s ({src})", flush=True, ) video_latents = None gt_all = None keyboard = batch["keyboard"].squeeze(0)[:, :-1, :] mouse = batch["mouse"].squeeze(0)[:, :-1, :] video_latents = _precomputed_video_latents(batch).to( dtype=pipe.torch_dtype, device=pipe.device ) if video_latents.shape[0] != args.num_players: raise ValueError( f"batch video_latents P={video_latents.shape[0]} does not match --num_players={args.num_players}" ) # First-frame-only latents (raw inputs) have no ground-truth clip. if video_latents.shape[2] > 1: gt_all = _decode_latents_to_videos(pipe, video_latents, tiler_kwargs) video1 = gt_all[0:1] video2 = gt_all[1:2] else: video1 = video2 = None camera_pose = None if args.use_camera_pose: if "camera_pose" not in batch: raise KeyError( "--use_camera_pose 需要 batch 含 camera_pose(CSV camera_pose_path* / camera12_path*)" ) camera_pose = ( batch["camera_pose"] .squeeze(0) .to(dtype=pipe.torch_dtype, device=pipe.device) ) prompt = target_text negative_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 = target_text causal_cfg_scale = args.cfg_scale if has_caption_prompt else 1.0 print( f"[infer] prompt={('caption' if has_caption_prompt else 'blank')} cfg_scale={causal_cfg_scale}", flush=True, ) if args.hier_infer_mode == "full_sequence": gen_players = _hier_full_sequence_inference( pipe, args, batch, video_latents, mouse, keyboard, camera_pose, agent_vertex_ids, ) else: if "group_labels" not in batch or "active_group" not in batch: raise KeyError( "hier_hub cached inference requires group_labels and active_group" ) gen_players = _as_player_videos( pipe.causal_inference( prompt=prompt, negative_prompt=negative_prompt, video1=video1, video2=video2, mouse=mouse, keyboard=keyboard, num_frames=args.causal_num_frames, height=TRAIN_HEIGHT, width=TRAIN_WIDTH, cfg_scale=causal_cfg_scale, num_inference_steps=args.num_inference_steps, seed=args.seed, tiled=True, tile_size=TILE_SIZE, tile_stride=TILE_STRIDE, loss_type="diffusion_forcing", return_both_players=True, agent_vertex_ids=agent_vertex_ids, video_latents=video_latents, camera_pose=camera_pose, overlap_path=None, group_labels=batch["group_labels"], active_group=batch["active_group"], membership=batch.get("membership"), global_only=batch.get("global_only"), ) ) if len(gen_players) != args.num_players: raise RuntimeError( f"Generated {len(gen_players)} player videos, expected P={args.num_players}" ) fps = args.grid_fps if not args.no_save_separate_players: for pidx, gen_p in enumerate(gen_players): save_video( gen_p, os.path.join( args.output_dir, f"video{batch_idx}_{_player_file_tag(pidx, args.num_players)}.mp4", ), fps=fps, quality=5, ) if not args.no_save_grid: T_gen = len(gen_players[0]) for pidx, gen_p in enumerate(gen_players): if len(gen_p) != T_gen: raise RuntimeError( f"Player outputs length mismatch: player0={T_gen} vs player{pidx}={len(gen_p)}" ) grid_rows = [gen_players] if gt_all is not None: grid_rows.append( [ tensor_bchw_to_pil_list(gt_all[pidx]) for pidx in range(args.num_players) ] ) T_grid = max(len(frames) for row in grid_rows for frames in row) grid_frames = _build_player_rows_grid_video( [ [pad_pil_list_to_length(frames, T_grid) for frames in row] for row in grid_rows ], T_grid, ) save_video( grid_frames, os.path.join(args.output_dir, f"video{batch_idx}_grid.mp4"), fps=fps, quality=5, )