Buckets:
| """LoRA-finetune Wan2.1-VACE-1.3B on our ``vace/window_*`` bundles via DiffSynth-Studio. | |
| **Trainer verification (read from code, not the README).** DiffSynth-Studio | |
| (``third_party/diffsynth``, pinned commit -- see ``third_party/PINNED_COMMITS.txt``) | |
| genuinely drives the VACE control branches during training: | |
| ``examples/wanvideo/model_training/lora/Wan2.1-VACE-1.3B.sh`` sets | |
| ``--lora_base_model vace --extra_inputs vace_video,vace_reference_image``; | |
| ``WanTrainingModule.__init__`` (this same directory's ``train.py``) resolves | |
| ``lora_base_model="vace"`` against ``pipe.vace`` | |
| (``diffsynth/pipelines/wan_video.py:159-163``, a real | |
| ``VaceWanModel`` instance -- ``diffsynth/models/wan_video_vace.py``), and the | |
| forward pass's ``WanVideoUnit_VACE.process`` (``wan_video.py:652-707``) builds | |
| a real ``vace_context`` tensor from ``vace_video``/``vace_video_mask``/ | |
| ``vace_reference_image`` that is fed through ``VaceWanModel.forward``'s own | |
| ``vace_blocks``/``vace_patch_embedding`` and injected into the base DiT at | |
| every layer in ``VaceWanModel``'s ``vace_layers`` (``wan_video.py:1525-1572``). | |
| This is not the base T2V/I2V path with the VACE weights merely present but | |
| unused. | |
| **flash_attn is optional here** (unlike ``third_party/wan2.1``'s native | |
| inference CLI, which hard-asserts without it -- | |
| ``scripts/setup_wan_env.sh``): ``diffsynth/core/attention/attention.py``'s | |
| ``initialize_attention_priority`` genuinely falls back to | |
| ``torch.nn.functional.scaled_dot_product_attention`` when | |
| ``flash_attn`` isn't importable (``ATTENTION_IMPLEMENTATION = "torch"``), | |
| so training still runs, just slower, if the wheel install in | |
| ``scripts/setup_wan_train_env.sh`` fails on some future host. | |
| This module imports ``diffsynth``/``accelerate``/``peft`` **lazily**, inside | |
| :func:`main`/:func:`build_training_module`, not at module scope -- so | |
| ``import fpgm.training.train`` succeeds in the plain ``fpgm`` conda env | |
| (no GPU, no ``diffsynth`` installed), matching this package's "lazy heavy | |
| imports" convention. Only :mod:`fpgm.training.dataset`/``manifest``/``augment`` | |
| are meant to be imported from that env; this module is meant to run inside | |
| the ``wan-train`` env created by ``scripts/setup_wan_train_env.sh``. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| import time | |
| from pathlib import Path | |
| from fpgm.training.dataset import build_dataset_from_manifest | |
| from fpgm.training.types import AugmentConfig, BundleAssemblyConfig, GateFilterConfig | |
| from fpgm.utils.logging import get_logger | |
| logger = get_logger(__name__) | |
| #: The DiffSynth-Studio checkout this module was written and pinned against | |
| #: -- see third_party/PINNED_COMMITS.txt. Only used to locate the example | |
| #: script we reuse WanTrainingModule from (that script is not part of the | |
| #: installed `diffsynth` package, see _load_diffsynth_train_module). | |
| _REPO_ROOT = Path(__file__).resolve().parents[3] | |
| _DIFFSYNTH_DIR = _REPO_ROOT / "third_party" / "diffsynth" | |
| _DIFFSYNTH_TRAIN_SCRIPT_DIR = _DIFFSYNTH_DIR / "examples" / "wanvideo" / "model_training" | |
| def _load_diffsynth_train_module(): | |
| """Import ``examples/wanvideo/model_training/train.py`` for its | |
| ``WanTrainingModule`` class. | |
| That file is an example script, not part of the installable ``diffsynth`` | |
| package (``pip install -e third_party/diffsynth`` does not put it on | |
| ``sys.path``), so it's loaded by path. Only the module-level class/function | |
| definitions are used -- its own ``if __name__ == "__main__":`` launcher | |
| block never runs because we import it, not execute it as a script. | |
| """ | |
| if str(_DIFFSYNTH_TRAIN_SCRIPT_DIR) not in sys.path: | |
| sys.path.insert(0, str(_DIFFSYNTH_TRAIN_SCRIPT_DIR)) | |
| import train as diffsynth_train # noqa: PLC0415 (intentionally lazy, see module docstring) | |
| return diffsynth_train | |
| def _default_model_paths(ckpt_dir: Path) -> list[str]: | |
| """The 3 local checkpoint files DiffSynth's ``ModelConfig(path=...)`` needs. | |
| Points directly at the already-downloaded snapshot | |
| (see the owning plan's "Already established" section) -- never triggers | |
| a modelscope/HuggingFace download, unlike ``--model_id_with_origin_paths``. | |
| """ | |
| return [ | |
| str(ckpt_dir / "diffusion_pytorch_model.safetensors"), | |
| str(ckpt_dir / "models_t5_umt5-xxl-enc-bf16.pth"), | |
| str(ckpt_dir / "Wan2.1_VAE.pth"), | |
| ] | |
| def build_parser() -> argparse.ArgumentParser: | |
| p = argparse.ArgumentParser(description=__doc__) | |
| p.add_argument("--dataset-kind", choices=("bundle", "appearance"), default="bundle", | |
| help="'bundle' = the datagen vace/window_* exports (geometry-buffer " | |
| "controls, gate-filtered). 'appearance' = the (control, target) " | |
| "pairs from scripts/appearance_control.py, whose only job is to " | |
| "teach the model what a Franka looks like. The two carry different " | |
| "supervision and are selected, not merged, so a run's log says " | |
| "which one it trained on.") | |
| p.add_argument("--dataset-root", type=Path, default=_REPO_ROOT / "outputs" / "datagen", | |
| help="bundle: root scanned for */*/vace/window_*/sample.json. " | |
| "appearance: root holding <uuid>__<serial>/{control,target}.mp4.") | |
| p.add_argument("--window-stride", type=int, default=None, | |
| help="appearance only: window start step in frames (default: no overlap). " | |
| "A smaller stride multiplies sample count from the same clips, at the " | |
| "cost of the extra windows being highly correlated.") | |
| p.add_argument("--reference", choices=("target_frame0", "window_frame0"), | |
| default="target_frame0", | |
| help="appearance only: what VACE gets as its reference image. See " | |
| "AppearancePairDataset.__init__ for why neither is silently correct.") | |
| p.add_argument("--ckpt-dir", type=Path, | |
| default=Path("/home/quang/.cache/huggingface/hub/models--Wan-AI--Wan2.1-VACE-1.3B/" | |
| "snapshots/574e6a744642ce3bee319afc31496b88bde8aac4"), | |
| help="Local Wan2.1-VACE-1.3B snapshot dir (never re-downloaded).") | |
| p.add_argument("--output-path", type=Path, required=True, | |
| help="Where to save LoRA checkpoints.") | |
| p.add_argument("--learning-rate", type=float, default=1e-4) | |
| p.add_argument("--lora-rank", type=int, default=32) | |
| p.add_argument("--lora-target-modules", type=str, default="q,k,v,o,ffn.0,ffn.2") | |
| p.add_argument("--num-epochs", type=int, default=5) | |
| p.add_argument("--max-steps", type=int, default=None, | |
| help="Stop after this many optimizer steps regardless of --num-epochs " | |
| "(the smoke train's mechanism for '30 steps', not a full run).") | |
| p.add_argument("--save-steps", type=int, default=None) | |
| p.add_argument("--gradient-accumulation-steps", type=int, default=1) | |
| p.add_argument("--use-gradient-checkpointing", action="store_true", default=True) | |
| p.add_argument("--no-gradient-checkpointing", dest="use_gradient_checkpointing", | |
| action="store_false") | |
| p.add_argument("--use-gradient-checkpointing-offload", action="store_true", default=False) | |
| p.add_argument("--max-num-frames", type=int, default=None, | |
| help="Truncate every window to its largest 4k+1 <= this (VRAM-vs-frame-count " | |
| "measurement knob, see fpgm.training.dataset.WindowBundleDataset).") | |
| # Gate filter (fpgm.training.types.GateFilterConfig) | |
| p.add_argument("--allow-pose-gap-overlap", action="store_true", default=False, | |
| help="Include windows with sample.json overlaps_pose_gap=True. Only intended " | |
| "for the mechanics-only smoke train -- see GateFilterConfig's docstring.") | |
| p.add_argument("--ignored-gate-keys", type=str, default="", | |
| help="Comma-separated gate keys to tolerate failing, e.g. for a smoke train.") | |
| # Bundle assembly (fpgm.training.types.BundleAssemblyConfig) | |
| p.add_argument("--include-fg-mask", action="store_true", default=False, | |
| help="Also pass fg_mask.mp4 as vace_video_mask. Off by default -- see " | |
| "BundleAssemblyConfig's docstring (measured worse in the project's own " | |
| "Wan-VACE ablation, outputs/wan_ablation/comparison.md).") | |
| # Augmentation (fpgm.training.types.AugmentConfig) | |
| p.add_argument("--no-augment", action="store_true", default=False, | |
| help="Disable every augmentation (pose noise, mask dilate/erode, channel " | |
| "dropout) -- e.g. for the VRAM measurement, where they're irrelevant.") | |
| p.add_argument("--no-pose-noise", action="store_true", default=False) | |
| p.add_argument("--no-mask-dilate-erode", action="store_true", default=False) | |
| p.add_argument("--no-channel-dropout", action="store_true", default=False) | |
| p.add_argument("--measure-vram-only", action="store_true", default=False, | |
| help="Run exactly 1 forward+backward step, report peak VRAM to " | |
| "--report-json, and exit before any optimizer step or checkpoint save.") | |
| p.add_argument("--report-json", type=Path, default=None, | |
| help="Where to write the {steps, steps_per_sec, peak_vram_gb, ...} report.") | |
| return p | |
| def _build_configs( | |
| args: argparse.Namespace, | |
| ) -> tuple[GateFilterConfig, BundleAssemblyConfig, AugmentConfig | None]: | |
| gate_filter = GateFilterConfig( | |
| allow_pose_gap_overlap=args.allow_pose_gap_overlap, | |
| ignored_gate_keys=tuple(k for k in args.ignored_gate_keys.split(",") if k), | |
| ) | |
| assembly_cfg = BundleAssemblyConfig(include_fg_mask=args.include_fg_mask) | |
| if args.no_augment: | |
| augment_cfg = None | |
| else: | |
| augment_cfg = AugmentConfig( | |
| enable_pose_noise=not args.no_pose_noise, | |
| enable_mask_dilate_erode=not args.no_mask_dilate_erode, | |
| enable_channel_dropout=not args.no_channel_dropout, | |
| ) | |
| return gate_filter, assembly_cfg, augment_cfg | |
| def build_training_module(args: argparse.Namespace, device: str): | |
| """Construct DiffSynth's ``WanTrainingModule`` pointed at the local checkpoint. | |
| Kept as its own function (rather than inlined in :func:`main`) so | |
| ``scripts/train_wan_vace_lora.sh``'s VRAM-measurement mode can build the | |
| module once and reuse it across multiple frame-count trials without | |
| reloading the ~1.3B-parameter DiT + T5 + VAE from disk each time. | |
| """ | |
| diffsynth_train = _load_diffsynth_train_module() | |
| return diffsynth_train.WanTrainingModule( | |
| model_paths=json.dumps(_default_model_paths(args.ckpt_dir)), | |
| tokenizer_path=str(args.ckpt_dir / "google" / "umt5-xxl"), | |
| lora_base_model="vace", | |
| lora_target_modules=args.lora_target_modules, | |
| lora_rank=args.lora_rank, | |
| use_gradient_checkpointing=args.use_gradient_checkpointing, | |
| use_gradient_checkpointing_offload=args.use_gradient_checkpointing_offload, | |
| extra_inputs="vace_video,vace_reference_image" | |
| + (",vace_video_mask" if args.include_fg_mask else ""), | |
| remove_prefix_in_ckpt="pipe.vace.", | |
| device=device, | |
| ) | |
| def main(argv: list[str] | None = None) -> dict: | |
| args = build_parser().parse_args(argv) | |
| import accelerate | |
| import torch | |
| if args.dataset_kind == "appearance": | |
| from fpgm.training.appearance_dataset import ( | |
| AppearancePairDataset, discover_windows, | |
| ) | |
| windows = discover_windows(args.dataset_root, stride=args.window_stride) | |
| dataset = AppearancePairDataset(windows, reference=args.reference) | |
| logger.info( | |
| "appearance dataset: %d window(s) from %d clip(s), reference=%s", | |
| len(dataset), len({w.pair_dir for w in windows}), args.reference, | |
| ) | |
| if args.max_num_frames is not None: | |
| # Not silently ignored: the appearance windows are a fixed 81 frames | |
| # by construction, and truncating them is a bundle-path knob. | |
| raise SystemExit("--max-num-frames applies to --dataset-kind bundle only") | |
| else: | |
| gate_filter, assembly_cfg, augment_cfg = _build_configs(args) | |
| dataset = build_dataset_from_manifest( | |
| args.dataset_root, gate_filter, assembly_cfg, augment_cfg, | |
| ) | |
| # Frame-count truncation is a dataset-level knob applied after the | |
| # gate-filtered manifest is built (see WindowBundleDataset.__init__). | |
| dataset.max_num_frames = args.max_num_frames | |
| logger.info("training dataset: %d window(s) after gate filter", len(dataset)) | |
| accelerator = accelerate.Accelerator( | |
| gradient_accumulation_steps=args.gradient_accumulation_steps | |
| ) | |
| # Load the ~1.3B-param DiT/T5/VAE directly onto the training device (no | |
| # CPU-offload path in this minimal driver -- see the VRAM measurement | |
| # job for whether that's needed on this contended host). | |
| model = build_training_module(args, device=accelerator.device) | |
| from diffsynth.diffusion.logger import ModelLogger | |
| args.output_path.mkdir(parents=True, exist_ok=True) | |
| model_logger = ModelLogger(str(args.output_path), remove_prefix_in_ckpt="pipe.vace.") | |
| optimizer = torch.optim.AdamW(model.trainable_modules(), lr=args.learning_rate) | |
| scheduler = torch.optim.lr_scheduler.ConstantLR(optimizer) | |
| dataloader = torch.utils.data.DataLoader( | |
| dataset, shuffle=True, collate_fn=lambda x: x[0], num_workers=0 | |
| ) | |
| model.to(device=accelerator.device) | |
| model, optimizer, dataloader, scheduler = accelerator.prepare( | |
| model, optimizer, dataloader, scheduler | |
| ) | |
| if torch.cuda.is_available(): | |
| torch.cuda.reset_peak_memory_stats(accelerator.device) | |
| # DiffSynth's own WanTrainingModule.__init__ forcibly re-enables gradient | |
| # checkpointing (with a UserWarning) if asked to disable it -- "to | |
| # prevent out-of-memory errors" is its own stated reason. So the | |
| # *requested* value and the *effective* one can differ; record both | |
| # rather than let the report silently claim gradient checkpointing was | |
| # off when DiffSynth overrode that. accelerator.prepare() may wrap | |
| # `model` (e.g. DDP) but does not change this attribute's value. | |
| unwrapped = accelerator.unwrap_model(model) | |
| effective_gc = getattr(unwrapped, "use_gradient_checkpointing", args.use_gradient_checkpointing) | |
| report = { | |
| "dataset_kind": args.dataset_kind, | |
| "max_num_frames": args.max_num_frames, | |
| "requested_gradient_checkpointing": args.use_gradient_checkpointing, | |
| "effective_gradient_checkpointing": effective_gc, | |
| "n_windows": len(dataset), | |
| "steps": 0, | |
| } | |
| step_times: list[float] = [] | |
| global_step = 0 | |
| stop = False | |
| t_prev = time.monotonic() | |
| for _epoch in range(args.num_epochs): | |
| if stop: | |
| break | |
| for data in dataloader: | |
| with accelerator.accumulate(model): | |
| loss = model(data) | |
| accelerator.backward(loss) | |
| optimizer.step() | |
| scheduler.step() | |
| optimizer.zero_grad() | |
| t_now = time.monotonic() | |
| step_times.append(t_now - t_prev) | |
| t_prev = t_now | |
| global_step += 1 | |
| logger.info( | |
| "step %d window=%s loss=%.4f dt=%.2fs", | |
| global_step, data.get("_window_name"), float(loss.detach().item()), step_times[-1], | |
| ) | |
| model_logger.on_step_end(accelerator, model, args.save_steps, loss=loss) | |
| if args.measure_vram_only: | |
| stop = True | |
| break | |
| if args.max_steps is not None and global_step >= args.max_steps: | |
| stop = True | |
| break | |
| if not args.measure_vram_only and not stop and args.save_steps is None: | |
| model_logger.on_epoch_end(accelerator, model, _epoch) | |
| if not args.measure_vram_only: | |
| model_logger.on_training_end(accelerator, model, args.save_steps) | |
| report["steps"] = global_step | |
| mean_step_seconds = sum(step_times) / len(step_times) if step_times else None | |
| report["mean_step_seconds"] = mean_step_seconds | |
| report["steps_per_sec"] = (1.0 / mean_step_seconds) if mean_step_seconds else None | |
| if torch.cuda.is_available(): | |
| report["peak_vram_allocated_gb"] = torch.cuda.max_memory_allocated(accelerator.device) / 1e9 | |
| report["peak_vram_reserved_gb"] = torch.cuda.max_memory_reserved(accelerator.device) / 1e9 | |
| if args.report_json is not None: | |
| args.report_json.parent.mkdir(parents=True, exist_ok=True) | |
| args.report_json.write_text(json.dumps(report, indent=2)) | |
| logger.info("training report: %s", json.dumps(report, indent=2)) | |
| return report | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 17 kB
- Xet hash:
- 2e432baacded0e09f9b8a3fb84a6c85f7e02cc047494ed7c9bbed64e7a61f291
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.