Skip to content

Commit 89dfa83

Browse files
authored
[Train] Support DP in training (#1193)
1 parent bf75223 commit 89dfa83

7 files changed

Lines changed: 258 additions & 10 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
model:
2+
name: qwen_image
3+
pretrained_model_name_or_path: /path/to/Qwen/Qwen-Image-2512
4+
max_sequence_length: 1024
5+
running_dtype: bf16
6+
7+
distributed:
8+
backend: nccl
9+
timeout_minutes: 60
10+
# Data parallel training uses torch.nn.parallel.DistributedDataParallel internally.
11+
dp:
12+
enabled: true
13+
broadcast_buffers: false
14+
find_unused_parameters: false
15+
static_graph: false
16+
gradient_as_bucket_view: false
17+
18+
data:
19+
train:
20+
name: image_dataset
21+
num_workers: 8
22+
prompt_dropout_rate: 0.1
23+
target_area: 1048576 # 1024 * 1024
24+
shuffle: true
25+
# examples: https://github.com/ModelTC/LightX2V_train_data_examples
26+
data_path:
27+
- /path/to/LightX2V_train_data_examples/dataset_v1/train.jsonl
28+
val:
29+
name: image_dataset
30+
num_workers: 8
31+
shuffle: false
32+
data_path:
33+
- /path/to/LightX2V_train_data_examples/dataset_v1/val.jsonl
34+
35+
scheduler:
36+
num_train_timesteps: 1000
37+
timestep_distribution: logitnormal
38+
logitnormal_mean: 0.0
39+
logitnormal_std: 1.0
40+
min_t: 0.001
41+
max_t: 1.0
42+
time_shift_settings:
43+
do_time_shift: true
44+
shift_type: exponential
45+
# shift function: "linear" => mu/(mu+(1/t-1)^p), "exponential" => exp(mu)/(exp(mu)+(1/t-1)^p)
46+
time_shift_power: 1.0
47+
dynamic_shift: true
48+
patch_size: [2, 2] # [H, W]
49+
# https://github.com/huggingface/diffusers/blob/v0.38.0/src/diffusers/pipelines/qwenimage/pipeline_qwenimage.py#L59
50+
shift_x1: 256
51+
shift_x2: 4096
52+
shift_y1: 0.5
53+
shift_y2: 1.15
54+
55+
training:
56+
method: flow
57+
train_type: lora
58+
max_train_iters: 1000
59+
gradient_accumulation_iters: 1
60+
gradient_checkpointing: true
61+
max_grad_norm: 1.0
62+
lr_scheduler: constant
63+
lr_warmup_iters: 10
64+
save_every_iters: 100
65+
save_total_limit: 10
66+
lora:
67+
rank: 16
68+
alpha: 16
69+
target_modules:
70+
- to_k
71+
- to_q
72+
- to_v
73+
- to_out.0
74+
optimizer:
75+
learning_rate: 0.0001
76+
adam_beta1: 0.9
77+
adam_beta2: 0.999
78+
weight_decay: 0.01
79+
adam_epsilon: 0.00000001
80+
output_dir: ./output_train/qwen_image_lora_dp
81+
82+
inference:
83+
method: image_infer
84+
negative_prompt: " "
85+
default_width: 1024
86+
default_height: 1024
87+
num_inference_steps: 50
88+
enable_cfg: true
89+
cfg_guidance_scale: 4.0
90+
seed: 42
91+
output_dir: ./output_infer/qwen_image_lora_dp
92+
infer_every_iters: ${training.save_every_iters}
93+
94+
logging:
95+
rank_zero_only: true
96+
train_log_every_iters: 10
97+
infer_log_every_steps: 10
98+
99+
resume:
100+
auto_resume: true
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import torch
2+
from loguru import logger
3+
from torch.nn.parallel import DistributedDataParallel
4+
5+
from lightx2v_train.runtime.distributed import is_distributed
6+
7+
8+
class LightX2VDistributedDataParallel(DistributedDataParallel):
9+
"""DDP wrapper that keeps the denoiser usable as the original transformer.
10+
11+
LightX2V replaces ``model.transformer`` with this wrapper when data
12+
parallelism is enabled. The extra forwarding below lets existing model,
13+
LoRA, checkpoint, and Wan causal-mask code keep calling attributes and
14+
methods on ``model.transformer`` without having to special-case
15+
``DistributedDataParallel.module`` everywhere.
16+
"""
17+
18+
@property
19+
def __class__(self):
20+
# Preserve class-based checks such as isinstance(transformer, CausalWanModel)
21+
# after the transformer is wrapped by DDP.
22+
return self.module.__class__
23+
24+
def __getattr__(self, name):
25+
try:
26+
return super().__getattr__(name)
27+
except AttributeError as error:
28+
try:
29+
module = super().__getattr__("module")
30+
except AttributeError:
31+
raise error
32+
# Expose attributes/methods from the wrapped denoiser directly.
33+
return getattr(module, name)
34+
35+
def __setattr__(self, name, value):
36+
modules = self.__dict__.get("_modules")
37+
wrapped = modules.get("module") if modules is not None else None
38+
if name == "block_mask" and wrapped is not None:
39+
# Causal Wan stores the attention block mask on the transformer used
40+
# by forward(), so write this field through to the wrapped module.
41+
setattr(wrapped, name, value)
42+
return
43+
super().__setattr__(name, value)
44+
45+
def state_dict(self, *args, **kwargs):
46+
# Save plain denoiser keys instead of DDP's "module."-prefixed keys.
47+
return self.module.state_dict(*args, **kwargs)
48+
49+
def load_state_dict(self, *args, **kwargs):
50+
# Match the plain state_dict() format used for non-DDP checkpoints.
51+
return self.module.load_state_dict(*args, **kwargs)
52+
53+
54+
def ddp_config(config):
55+
distributed_config = config.get("distributed", {})
56+
config_value = distributed_config.get("dp", {})
57+
return config_value or {}
58+
59+
60+
def ddp_enabled(config):
61+
return is_distributed() and ddp_config(config).get("enabled", False)
62+
63+
64+
def is_ddp_module(module):
65+
return isinstance(module, DistributedDataParallel)
66+
67+
68+
def unwrap_ddp_module(module):
69+
while is_ddp_module(module):
70+
module = module.module
71+
return module
72+
73+
74+
def set_ddp_gradient_sync(module, enabled):
75+
if is_ddp_module(module):
76+
module.require_backward_grad_sync = enabled
77+
78+
79+
def _ddp_kwargs(config):
80+
config = ddp_config(config)
81+
kwargs = {
82+
"broadcast_buffers": config.get("broadcast_buffers", False),
83+
"find_unused_parameters": config.get("find_unused_parameters", False),
84+
"static_graph": config.get("static_graph", False),
85+
"gradient_as_bucket_view": config.get("gradient_as_bucket_view", False),
86+
}
87+
if torch.cuda.is_available():
88+
kwargs["device_ids"] = [torch.cuda.current_device()]
89+
kwargs["output_device"] = torch.cuda.current_device()
90+
return kwargs
91+
92+
93+
def apply_ddp(model, config):
94+
if not ddp_enabled(config) or is_ddp_module(model.denoiser_module()):
95+
return model
96+
97+
denoiser = model.denoiser_module()
98+
99+
if not any(param.requires_grad for param in denoiser.parameters()):
100+
logger.info("DP(DDP) skipped for {} because the denoiser has no trainable parameters.", model.__class__.__name__)
101+
return model
102+
103+
ddp_kwargs = _ddp_kwargs(config)
104+
wrapped = LightX2VDistributedDataParallel(denoiser, **ddp_kwargs)
105+
if getattr(model, "transformer", None) is not denoiser:
106+
raise RuntimeError(f"{model.__class__.__name__} must store its trainable denoiser in self.transformer to use DP(DDP).")
107+
model.transformer = wrapped
108+
logger.info(
109+
"DP(DDP) transformer wrapped: broadcast_buffers={} find_unused_parameters={} static_graph={}",
110+
ddp_kwargs["broadcast_buffers"],
111+
ddp_kwargs["find_unused_parameters"],
112+
ddp_kwargs["static_graph"],
113+
)
114+
return model
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from loguru import logger
2+
3+
from lightx2v_train.runtime.ddp import apply_ddp, ddp_enabled, set_ddp_gradient_sync
4+
from lightx2v_train.runtime.distributed import is_distributed
5+
from lightx2v_train.runtime.fsdp import apply_fsdp2, fsdp2_enabled
6+
7+
8+
def apply_parallel(model, config):
9+
"""Apply the configured distributed parallel strategy exactly once."""
10+
11+
if not is_distributed():
12+
return model
13+
14+
if ddp_enabled(config):
15+
return apply_ddp(model, config)
16+
17+
if fsdp2_enabled(config):
18+
return apply_fsdp2(model, config)
19+
20+
logger.warning("Distributed training is initialized, but neither DP(DDP) nor FSDP2 is enabled. The model will run without distributed wrapping.")
21+
return model
22+
23+
24+
def set_parallel_gradient_sync(model, enabled):
25+
model.set_fsdp2_gradient_sync(enabled)
26+
set_ddp_gradient_sync(model.denoiser_module(), enabled)

lightx2v_train/lightx2v_train/trainers/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from lightx2v_train.infer import build_inferencer
1111
from lightx2v_train.runtime.checkpoint import find_latest_checkpoint, parse_checkpoint_iteration, prune_checkpoints
1212
from lightx2v_train.runtime.distributed import barrier, get_world_size, is_main_process
13-
from lightx2v_train.runtime.fsdp import apply_fsdp2
13+
from lightx2v_train.runtime.parallel import apply_parallel, set_parallel_gradient_sync
1414
from lightx2v_train.schedulers.flow_matching import RectifiedFlowMatchingScheduler
1515
from lightx2v_train.utils.utils import get_running_dtype
1616

@@ -120,7 +120,7 @@ def _build_lr_scheduler(self, optimizer, num_training_steps=None, num_warmup_ste
120120
def setup(self, resume_ckpt_path=None):
121121
self._setup_trainable_model(self.model)
122122

123-
apply_fsdp2(self.model, self.config)
123+
apply_parallel(self.model, self.config)
124124

125125
if self.gradient_checkpointing:
126126
self.model.enable_gradient_checkpointing()
@@ -222,7 +222,7 @@ def _resolve_resume(self):
222222
return ckpt_path, current_iter
223223

224224
def _set_gradient_sync(self, enabled):
225-
self.model.set_fsdp2_gradient_sync(enabled)
225+
set_parallel_gradient_sync(self.model, enabled)
226226

227227
def run_inference(self, current_iter):
228228
base_output_dir = self.infer_config.get("output_dir", "./output_infer")

lightx2v_train/lightx2v_train/trainers/dmd.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from lightx2v_train.model_zoo import build_model
1414
from lightx2v_train.runtime.checkpoint import prune_checkpoints
1515
from lightx2v_train.runtime.distributed import barrier, get_world_size, is_distributed, is_main_process, reduce_mean
16-
from lightx2v_train.runtime.fsdp import apply_fsdp2
16+
from lightx2v_train.runtime.parallel import apply_parallel, set_parallel_gradient_sync
1717
from lightx2v_train.schedulers import DMDFlowMatchingScheduler
1818
from lightx2v_train.schedulers.flow_matching import CausalForcingFlowMatchScheduler
1919
from lightx2v_train.utils.registry import TRAINER_REGISTER
@@ -70,7 +70,7 @@ def setup(self, resume_ckpt_path=None):
7070
self.fake_model = build_model(fake_model_config)
7171
self.fake_model.load_components(transformer_only=True, reference_model=self.model)
7272
self._setup_trainable_model(self.fake_model)
73-
apply_fsdp2(self.fake_model, self.config)
73+
apply_parallel(self.fake_model, self.config)
7474
if self.gradient_checkpointing:
7575
self.fake_model.enable_gradient_checkpointing()
7676

@@ -81,7 +81,7 @@ def setup(self, resume_ckpt_path=None):
8181
self.teacher_model.load_components(transformer_only=True, reference_model=self.model)
8282
self.teacher_model.transformer.requires_grad_(False)
8383
self.teacher_model.transformer.eval()
84-
apply_fsdp2(self.teacher_model, self.config)
84+
apply_parallel(self.teacher_model, self.config)
8585
self.teacher_model.transformer.eval()
8686

8787
self.fake_trainable_params = list(self.fake_model.trainable_parameters())
@@ -443,10 +443,10 @@ def train(self):
443443
logger.info("[train] finished iter={}/{}", current_iter, max_train_iters)
444444

445445
def _set_student_gradient_sync(self, enabled):
446-
self.model.set_fsdp2_gradient_sync(enabled)
446+
set_parallel_gradient_sync(self.model, enabled)
447447

448448
def _set_fake_gradient_sync(self, enabled):
449-
self.fake_model.set_fsdp2_gradient_sync(enabled)
449+
set_parallel_gradient_sync(self.fake_model, enabled)
450450

451451
def _set_gradient_sync(self, enabled):
452452
self._set_student_gradient_sync(enabled)

lightx2v_train/lightx2v_train/trainers/dopsd.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from lightx2v_train.infer.dopsd_trajectory_viz import save_student_teacher_trajectory_grid
1717
from lightx2v_train.runtime.checkpoint import find_latest_checkpoint, parse_checkpoint_iteration, prune_checkpoints
1818
from lightx2v_train.runtime.distributed import barrier, get_rank, get_world_size, is_distributed, is_main_process, reduce_mean
19-
from lightx2v_train.runtime.fsdp import apply_fsdp2
19+
from lightx2v_train.runtime.parallel import apply_parallel
2020
from lightx2v_train.utils.registry import TRAINER_REGISTER
2121
from lightx2v_train.utils.utils import get_running_dtype
2222

@@ -102,7 +102,7 @@ def setup(self, resume_ckpt_path=None):
102102
)
103103
self.model.set_dual_lora_trainable(self.student_adapter, self.teacher_adapter)
104104

105-
apply_fsdp2(self.model, self.config)
105+
apply_parallel(self.model, self.config)
106106

107107
if self.gradient_checkpointing:
108108
self.model.enable_gradient_checkpointing()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
3+
export CUDA_VISIBLE_DEVICES=0,1
4+
5+
torchrun \
6+
--standalone \
7+
--nproc_per_node=2 \
8+
train.py --config configs/train/flow/qwen_image_lora_dp.yaml

0 commit comments

Comments
 (0)