multimodalart's picture
multimodalart HF Staff
SCoPE camera-controlled I2V demo
e793773 verified
Raw
History Blame Contribute Delete
8.95 kB
"""Public RDPO high-noise fine-tuning entry for SCoPE.
Training reproduces the released recipe: only the high-noise expert is
optimized, timesteps are sampled from ``[0.9, 1.0)``, and the data mixture is a
``ConcatDataset`` of the four native loaders in :mod:`scope.data`. The run is
described by a single YAML config (see configs/train_rdpo_high_only.yaml).
"""
from __future__ import annotations
import argparse
from pathlib import Path
from typing import Any
import pytorch_lightning as pl
import torch
import yaml
from pytorch_lightning.callbacks import ModelCheckpoint
from torch.utils.data import DataLoader
from torch.utils.data.dataloader import default_collate
from scope.config import SCOPE_MODEL_ID, InferenceConfig
from scope.data import build_training_dataset
from scope.patch import enable_scope_grad
from scope.weights import load_pipeline, resolve_model_dir
_TRAINABLE_KEYWORDS = ["plucker_pe", "self_attn", "norm3", "ffn"]
# PIL first frames cannot be default-collated, so they are kept as a list.
_LIST_KEYS = ("first_frame_pil",)
def load_config(path: Path) -> dict[str, Any]:
config = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
if not isinstance(config, dict):
raise ValueError(f"Training config must be a mapping: {path}")
return config
def _collate(batch: list[dict[str, Any]]) -> dict[str, Any]:
output: dict[str, Any] = {}
for key in batch[0]:
values = [sample[key] for sample in batch]
output[key] = values if key in _LIST_KEYS else default_collate(values)
return output
class SCoPEFineTuner(pl.LightningModule):
"""Fine-tune the released SCoPE model in the RDPO high-noise regime."""
def __init__(
self,
model_path: str,
learning_rate: float,
weight_decay: float,
height: int,
width: int,
num_frames: int,
) -> None:
super().__init__()
self.save_hyperparameters()
self.pipe = None
def setup(self, stage: str | None = None) -> None:
if self.pipe is not None:
return
inference_config = InferenceConfig(
height=self.hparams.height,
width=self.hparams.width,
num_frames=self.hparams.num_frames,
)
model_dir = resolve_model_dir(self.hparams.model_path)
self.pipe = load_pipeline(model_dir, inference_config)
self.pipe.i2v_vae_condition_mode = "official_zero"
enable_scope_grad(self.pipe, _TRAINABLE_KEYWORDS, expert="high_noise_model")
object.__setattr__(self, "dit", self.pipe.dit)
object.__setattr__(self, "dit2", self.pipe.dit2)
self.pipe.scheduler.set_timesteps(
self.pipe.scheduler.num_train_timesteps,
training=True,
)
def training_step(self, batch: dict[str, Any], batch_index: int) -> torch.Tensor:
del batch_index
if len(batch["first_frame_pil"]) != 1:
raise ValueError("SCoPE A14B training currently requires batch_size=1 per GPU")
self.pipe.device = self.device
self.pipe.load_models_to_device(["vae"])
video = batch["video"].to(device=self.device, dtype=self.pipe.torch_dtype)
with torch.inference_mode():
latents = self.pipe.vae.single_encode(video, self.device)
latents = latents.to(device=self.device, dtype=self.pipe.torch_dtype).detach()
self.pipe.load_models_to_device(["text_encoder"])
with torch.inference_mode():
context = self.pipe.prompter.encode_prompt(
batch["caption"], positive=True, device=self.device
)
conditioning = self.pipe.build_i2v_conditioning(
input_image=batch["first_frame_pil"][0],
num_frames=self.hparams.num_frames,
height=self.hparams.height,
width=self.hparams.width,
)
if conditioning is None:
raise RuntimeError("The selected model does not expose Wan2.2 I2V conditioning")
camera = {
"pose": batch["pose"].to(device=self.device, dtype=self.pipe.torch_dtype),
"x_fov": batch["x_fov"].to(device=self.device, dtype=self.pipe.torch_dtype),
"xi": batch["xi"].to(device=self.device, dtype=self.pipe.torch_dtype),
}
self.pipe.load_models_to_device(["dit", "dit2"])
loss = self.pipe.training_loss(
input_latents=latents,
noise=torch.randn_like(latents),
context=context,
height=self.hparams.height,
width=self.hparams.width,
camera_control_panshot=camera,
y=conditioning,
first_frame_latents=conditioning[:, 4:, 0:1].clone(),
min_timestep_boundary=0.9,
max_timestep_boundary=1.0,
switch_DiT_boundary=0.9,
use_gradient_checkpointing=True,
use_gradient_checkpointing_offload=False,
)
self.log("train_loss", loss, on_step=True, on_epoch=True, prog_bar=True, sync_dist=True)
return loss
def configure_optimizers(self) -> torch.optim.Optimizer:
parameters = [
parameter for parameter in self.pipe.dit.parameters() if parameter.requires_grad
]
if not parameters:
raise RuntimeError("No trainable parameters found in the high-noise expert")
return torch.optim.AdamW(
parameters,
lr=self.hparams.learning_rate,
weight_decay=self.hparams.weight_decay,
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--config",
type=Path,
default=Path("configs/train_rdpo_high_only.yaml"),
help="Training YAML (see configs/train_rdpo_high_only.yaml).",
)
parser.add_argument("--model_path", default=None, help="Override config model_path.")
parser.add_argument("--output_dir", type=Path, default=None, help="Override config output_dir.")
parser.add_argument("--num_gpus", type=int, default=None, help="Override trainer.num_gpus.")
parser.add_argument("--max_steps", type=int, default=None, help="Override trainer.max_steps.")
parser.add_argument("--resume_from_checkpoint", type=Path, default=None)
return parser
def build_strategy(num_gpus: int) -> "str | Any":
if num_gpus <= 1:
return "auto"
from pytorch_lightning.strategies import FSDPStrategy
from torch.distributed.fsdp import ShardingStrategy
from diffsynth.models.wan_video_dit import DiTBlock
return FSDPStrategy(
sharding_strategy=ShardingStrategy.FULL_SHARD,
auto_wrap_policy={DiTBlock},
state_dict_type="sharded",
use_orig_params=True,
)
def main() -> None:
args = build_parser().parse_args()
config = load_config(args.config)
data_config = config["data"]
trainer_config = config.get("trainer", {})
optimizer_config = config.get("optimizer", {})
model_path = args.model_path or config.get("model_path", SCOPE_MODEL_ID)
output_dir = Path(args.output_dir or config.get("output_dir", "outputs/training"))
num_gpus = args.num_gpus or int(trainer_config.get("num_gpus", 8))
max_steps = args.max_steps or int(trainer_config.get("max_steps", 10_000))
pl.seed_everything(int(config.get("seed", 42)), workers=True)
dataset = build_training_dataset(data_config)
num_workers = int(data_config.get("num_workers", 4))
loader = DataLoader(
dataset,
batch_size=1,
shuffle=True,
num_workers=num_workers,
collate_fn=_collate,
pin_memory=True,
persistent_workers=num_workers > 0,
)
checkpoint = ModelCheckpoint(
dirpath=output_dir / "checkpoints",
filename="scope-{step:06d}",
every_n_train_steps=int(trainer_config.get("save_every_n_steps", 1_000)),
save_top_k=-1,
save_last=True,
save_on_train_epoch_end=False,
)
model = SCoPEFineTuner(
model_path=model_path,
learning_rate=float(optimizer_config.get("learning_rate", 2e-5)),
weight_decay=float(optimizer_config.get("weight_decay", 1e-2)),
height=int(data_config.get("height", 480)),
width=int(data_config.get("width", 832)),
num_frames=int(data_config.get("num_frames", 81)),
)
trainer = pl.Trainer(
accelerator="gpu",
devices=num_gpus,
strategy=build_strategy(num_gpus),
precision=str(trainer_config.get("precision", "bf16-mixed")),
max_steps=max_steps,
gradient_clip_val=float(trainer_config.get("gradient_clip_val", 1.0)),
default_root_dir=output_dir,
callbacks=[checkpoint],
log_every_n_steps=int(trainer_config.get("log_every_n_steps", 10)),
)
trainer.fit(model, train_dataloaders=loader, ckpt_path=args.resume_from_checkpoint)
if __name__ == "__main__":
main()