Skip to content

Commit d674d8a

Browse files
author
Toshi Pahadia
committed
feat(wan2.2): Implement Wan 2.2 joint training pipeline with dynamic expert routing
- Add WanTrainer2_2 joint training pipeline with probabilistic batch routing and Beta timestep sampling (Beta(5,2) for high-noise expert, Beta(2,5) for low-noise expert) - Implement batched conditional evaluation (eval_step_2_2) with exact per-sample timestep routing and zero unrolled loop overhead - Implement dual transformer checkpoint saving & restoration in WanCheckpointer2_2 with support for weights-only resume - Prevent TPU VRAM overhead via branch-isolated gradient computation and optimizer application - Support independent Optax learning rate schedule lengths scaled by boundary_ratio - Add step-dependent PRNG key folding during evaluation passes - Add comprehensive unit test suite with real JIT-compiled train_step and eval_step execution
1 parent 8e3e843 commit d674d8a

8 files changed

Lines changed: 1320 additions & 16 deletions

File tree

src/maxdiffusion/checkpointing/wan_checkpointer_2_2.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import jax
1919
from typing import Optional, Tuple
2020
from ..pipelines.wan.wan_pipeline_2_2 import WanPipeline2_2
21-
from .. import max_logging
21+
from .. import max_logging, max_utils
2222
import orbax.checkpoint as ocp
2323
from maxdiffusion.checkpointing.checkpointing_utils import add_sharding_to_struct, get_cpu_mesh_and_sharding
2424
from maxdiffusion.checkpointing.wan_checkpointer import WanCheckpointer
@@ -27,6 +27,15 @@
2727
class WanCheckpointer2_2(WanCheckpointer[WanPipeline2_2]):
2828
pipeline_class = WanPipeline2_2
2929

30+
def _create_optimizer(self, model, config, learning_rate, scale_factor: float = 1.0):
31+
total_steps = max(1, int(config.max_train_steps * scale_factor))
32+
schedule_steps = max(1, int(config.learning_rate_schedule_steps * scale_factor))
33+
learning_rate_scheduler = max_utils.create_learning_rate_schedule(
34+
learning_rate, schedule_steps, config.warmup_steps_fraction, total_steps
35+
)
36+
tx = max_utils.create_optimizer(config, learning_rate_scheduler)
37+
return tx, learning_rate_scheduler
38+
3039
def load_wan_configs_from_orbax(self, step: Optional[int]) -> Tuple[Optional[dict], Optional[int]]:
3140
if step is None:
3241
step = self.checkpoint_manager.latest_step()
@@ -81,11 +90,20 @@ def load_wan_configs_from_orbax(self, step: Optional[int]) -> Tuple[Optional[dic
8190
return restored_checkpoint, step
8291

8392
def _extract_opt_state(self, restored_checkpoint):
84-
if "opt_state" in restored_checkpoint.low_noise_transformer_state.keys():
85-
return restored_checkpoint.low_noise_transformer_state["opt_state"]
86-
elif "opt_state" in restored_checkpoint.high_noise_transformer_state.keys():
87-
return restored_checkpoint.high_noise_transformer_state["opt_state"]
88-
return None
93+
low_state = getattr(restored_checkpoint, "low_noise_transformer_state", {})
94+
high_state = getattr(restored_checkpoint, "high_noise_transformer_state", {})
95+
low_opt = low_state.get("opt_state") if isinstance(low_state, dict) else getattr(low_state, "opt_state", None)
96+
high_opt = high_state.get("opt_state") if isinstance(high_state, dict) else getattr(high_state, "opt_state", None)
97+
low_step = low_state.get("step") if isinstance(low_state, dict) else getattr(low_state, "step", None)
98+
high_step = high_state.get("step") if isinstance(high_state, dict) else getattr(high_state, "step", None)
99+
if low_opt is None and high_opt is None:
100+
return None
101+
return {
102+
"low_noise_transformer": low_opt,
103+
"high_noise_transformer": high_opt,
104+
"low_noise_step": low_step,
105+
"high_noise_step": high_step,
106+
}
89107

90108
def save_checkpoint(self, train_step, pipeline: WanPipeline2_2, train_states: dict):
91109
"""Saves the training state and model configurations."""

src/maxdiffusion/configs/base_wan_27b.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,9 @@ num_inference_steps: 40
409409
fps: 16
410410
save_final_checkpoint: False
411411

412+
# Location to download pretrained weights
413+
checkpoint_save_location: "/tmp"
414+
412415
# SDXL Lightning parameters
413416
lightning_from_pt: True
414417
# Empty or "ByteDance/SDXL-Lightning" to enable lightning.

src/maxdiffusion/pyconfig.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
)
4444

4545
_ALLOWED_MODEL_NAMES = {WAN2_1, WAN2_2, LTX2_VIDEO, LTX2_3, Z_IMAGE}
46-
_ALLOWED_TRAINING_MODEL_NAMES = {WAN2_1}
46+
_ALLOWED_TRAINING_MODEL_NAMES = {WAN2_1, WAN2_2}
4747

4848

4949
def _validate_model_name(model_name: str | None):
@@ -283,12 +283,13 @@ def user_init(raw_keys):
283283

284284
# Orbax doesn't save the tokenizer params, instead it loads them from the pretrained_model_name_or_path
285285
raw_keys["tokenizer_model_name_or_path"] = raw_keys["pretrained_model_name_or_path"]
286+
ckpt_save_loc = raw_keys.get("checkpoint_save_location", "/tmp")
286287
if "gs://" in raw_keys["pretrained_model_name_or_path"]:
287-
raw_keys["pretrained_model_name_or_path"] = max_utils.download_blobs(raw_keys["pretrained_model_name_or_path"], "/tmp")
288+
raw_keys["pretrained_model_name_or_path"] = max_utils.download_blobs(raw_keys["pretrained_model_name_or_path"], ckpt_save_loc)
288289
if "gs://" in raw_keys["unet_checkpoint"]:
289-
raw_keys["unet_checkpoint"] = max_utils.download_blobs(raw_keys["unet_checkpoint"], "/tmp")
290+
raw_keys["unet_checkpoint"] = max_utils.download_blobs(raw_keys["unet_checkpoint"], ckpt_save_loc)
290291
if "gs://" in raw_keys["tokenizer_model_name_or_path"]:
291-
raw_keys["tokenizer_model_name_or_path"] = max_utils.download_blobs(raw_keys["tokenizer_model_name_or_path"], "/tmp")
292+
raw_keys["tokenizer_model_name_or_path"] = max_utils.download_blobs(raw_keys["tokenizer_model_name_or_path"], ckpt_save_loc)
292293
if "gs://" in raw_keys["dataset_name"]:
293294
raw_keys["dataset_name"] = max_utils.download_blobs(raw_keys["dataset_name"], raw_keys["dataset_save_location"])
294295
raw_keys["dataset_save_location"] = raw_keys["dataset_name"]

src/maxdiffusion/tests/wan/wan_checkpointer_test.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,8 @@ def test_load_checkpoint_with_optimizer_in_low_noise(self, mock_from_checkpoint,
387387
)
388388
self.assertEqual(pipeline, mock_pipeline_instance)
389389
self.assertIsNotNone(opt_state)
390-
self.assertEqual(opt_state["learning_rate"], 0.001)
390+
self.assertEqual(opt_state["low_noise_transformer"]["learning_rate"], 0.001)
391+
self.assertIsNone(opt_state["high_noise_transformer"])
391392
self.assertEqual(step, 1)
392393

393394
@patch("maxdiffusion.checkpointing.wan_checkpointer.create_orbax_checkpoint_manager")
@@ -429,7 +430,8 @@ def test_load_checkpoint_with_optimizer_in_high_noise(self, mock_from_checkpoint
429430
)
430431
self.assertEqual(pipeline, mock_pipeline_instance)
431432
self.assertIsNotNone(opt_state)
432-
self.assertEqual(opt_state["learning_rate"], 0.002)
433+
self.assertIsNone(opt_state["low_noise_transformer"])
434+
self.assertEqual(opt_state["high_noise_transformer"]["learning_rate"], 0.002)
433435
self.assertEqual(step, 1)
434436

435437

@@ -758,9 +760,10 @@ def test_load_checkpoint_both_optimizers_present(self, mock_from_checkpoint, moc
758760
checkpointer = WanCheckpointer2_2(config=self.config)
759761
pipeline, opt_state, step = checkpointer.load_checkpoint(step=1)
760762

761-
# Should prioritize low_noise_transformer's optimizer state
763+
# Should preserve both low_noise_transformer and high_noise_transformer optimizer states
762764
self.assertIsNotNone(opt_state)
763-
self.assertEqual(opt_state["learning_rate"], 0.001)
765+
self.assertEqual(opt_state["low_noise_transformer"]["learning_rate"], 0.001)
766+
self.assertEqual(opt_state["high_noise_transformer"]["learning_rate"], 0.002)
764767

765768

766769
if __name__ == "__main__":

0 commit comments

Comments
 (0)