Skip to content

Commit c2b3a0d

Browse files
committed
fix: resolve distributed stability issues including LoRA sync, recovery crash, and DeepSpeed empty groups (#435)
1 parent 4bb7d74 commit c2b3a0d

6 files changed

Lines changed: 85 additions & 22 deletions

File tree

roll/configs/data_args.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class DataArguments:
2020
default=None,
2121
metadata={"help": "The number of processes to use for the preprocessing."},
2222
)
23+
cache_path: Optional[str] = field(
24+
default=None,
25+
metadata={"help": "Path to cache preprocessed datasets."},
26+
)
2327
file_name: Optional[Union[List[str], str]] = field(
2428
default=None,
2529
metadata={"help": "The name of file path name for train. Conflicts with `--dataset_name`"},

roll/distributed/scheduler/generate_scheduler.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,12 +473,12 @@ def __init__(
473473

474474
self.dataset = dataset
475475
self.indices = list(range(len(dataset)))
476-
if state is not None and state.get("dataset_iter_count", 0) > 0:
477-
for _ in range(state["dataset_iter_count"]):
478-
self.get_next_dataset_item()
479476
self.dataset_epoch = 0
480477
self.dataset_iter = None
481478
self.dataset_iter_count = 0
479+
if state is not None and state.get("dataset_iter_count", 0) > 0:
480+
for _ in range(state["dataset_iter_count"]):
481+
self.get_next_dataset_item()
482482

483483
self.collect_fn_cls = collect_fn_cls
484484
self.collect_fn_kwargs = collect_fn_kwargs

roll/distributed/scheduler/initialize.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import shlex
23
import subprocess
34
import sys
45
import time
@@ -36,12 +37,19 @@ def start_ray_cluster():
3637
logger.info("Ray cluster already initialized")
3738
return False
3839

40+
temp_dir = os.environ.get("ROLL_RAY_TEMP_DIR")
41+
temp_dir_arg = ""
42+
if temp_dir:
43+
os.makedirs(temp_dir, exist_ok=True)
44+
temp_dir_arg = f" --temp-dir={shlex.quote(temp_dir)}"
45+
3946
if rank == 0:
4047
cmd = f"ray start --head --port={master_port} --node-name={node_name} --dashboard-port={dashboard_port}"
4148
else:
4249
# fix: 处理大规模下可能会出现的head/worker node创建顺序不一致问题
4350
time.sleep(5)
4451
cmd = f"ray start --address={master_addr}:{master_port} --node-name={node_name} --dashboard-port={dashboard_port}"
52+
cmd += temp_dir_arg
4553

4654
logger.info(f"Starting ray cluster: {cmd}")
4755
ret = subprocess.run(cmd, shell=True, capture_output=True)

roll/pipeline/rlvr/rlvr_vlm_pipeline.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import copy
22
import json
3+
import math
34
import os
45
import uuid
56
from functools import partial
@@ -224,22 +225,31 @@ def __init__(self, pipeline_config: RLVRConfig):
224225
dataset = get_vlm_dataset(
225226
self.pipeline_config.actor_train.data_args, encode_function, self.processor, get_eval=False
226227
)
227-
# update domain field, DynamicSamplingScheduler requires
228-
dataset = dataset.map(
229-
partial(update_dataset_domain, self.pipeline_config.tag_2_domain),
230-
num_proc=self.pipeline_config.actor_train.data_args.preprocessing_num_workers,
231-
desc="update_dataset_domain",
232-
load_from_cache_file=False,
233-
)
234-
228+
# Avoid rewriting large multimodal Arrow columns when a run has a single domain.
229+
# Re-mapping PIL image columns can overflow pyarrow's regular array offsets.
230+
domains = list(self.pipeline_config.actor_train.data_args.domain_interleave_probs.keys())
235231
self.domain_datasets: Dict[str, datasets.Dataset] = {}
236-
for domain in self.pipeline_config.actor_train.data_args.domain_interleave_probs.keys():
237-
self.domain_datasets[domain] = dataset.filter(
238-
lambda example, dom: example["domain"] == dom,
232+
if len(domains) == 1:
233+
domain = domains[0]
234+
if "domain" not in dataset.column_names:
235+
dataset = dataset.add_column("domain", [domain] * len(dataset))
236+
self.domain_datasets[domain] = dataset
237+
else:
238+
# update domain field, DynamicSamplingScheduler requires
239+
dataset = dataset.map(
240+
partial(update_dataset_domain, self.pipeline_config.tag_2_domain),
239241
num_proc=self.pipeline_config.actor_train.data_args.preprocessing_num_workers,
240-
fn_kwargs={"dom": domain},
242+
desc="update_dataset_domain",
243+
load_from_cache_file=False,
241244
)
242-
assert len(self.domain_datasets[domain]) > 0, f"domain dataset {domain} has no data"
245+
246+
for domain in domains:
247+
self.domain_datasets[domain] = dataset.filter(
248+
lambda example, dom: example["domain"] == dom,
249+
num_proc=self.pipeline_config.actor_train.data_args.preprocessing_num_workers,
250+
fn_kwargs={"dom": domain},
251+
)
252+
assert len(self.domain_datasets[domain]) > 0, f"domain dataset {domain} has no data"
243253

244254
self.val_dataset = None
245255
if self.pipeline_config.validation and self.pipeline_config.validation.data_args:
@@ -259,6 +269,17 @@ def __init__(self, pipeline_config: RLVRConfig):
259269
kl_horizon=self.pipeline_config.kl_horizon,
260270
)
261271

272+
if self.pipeline_config.max_steps <= 0:
273+
num_train_epochs = self.pipeline_config.actor_train.training_args.num_train_epochs
274+
dataset_size = sum(len(domain_dataset) for domain_dataset in self.domain_datasets.values())
275+
inferred_max_steps = math.ceil(num_train_epochs * dataset_size / self.pipeline_config.rollout_batch_size)
276+
logger.info(
277+
"infer pipeline max_steps from dataset: "
278+
f"num_train_epochs={num_train_epochs}, dataset_size={dataset_size}, "
279+
f"rollout_batch_size={self.pipeline_config.rollout_batch_size}, "
280+
f"max_steps={inferred_max_steps}"
281+
)
282+
self.pipeline_config.max_steps = inferred_max_steps
262283
assert self.pipeline_config.max_steps > 0, "max_steps must be greater than 0"
263284
self.pipeline_config.set_max_steps(max_steps=self.pipeline_config.max_steps)
264285

roll/third_party/deepspeed/model_update.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from dataclasses import asdict
2+
13
import ray
24
import torch.distributed as dist
35
from deepspeed.runtime.zero import GatheredParameters
@@ -29,9 +31,22 @@ def _gather_weights(is_zero3, named_params):
2931
return [(n, p.data) for n, p in named_params]
3032

3133

32-
def gather_deepspeed_weights(model, ds_config, buffer_size):
34+
def _get_deepspeed_named_params(model, ds_config, is_lora=False):
35+
if not is_lora:
36+
return [(name, param) for name, param in model.named_parameters()]
37+
38+
if not ds_config.is_zero3():
39+
return [(name, param) for name, param in get_peft_model_state_dict(model).items()]
40+
41+
adapter_name = "default"
42+
state_dict = model.state_dict()
43+
lora_state_dict = {k: state_dict[k] for k in state_dict if ("lora_" in k and adapter_name in k)}
44+
return [(name.replace(f".{adapter_name}", ""), model.get_parameter(name)) for name in lora_state_dict]
45+
46+
47+
def gather_deepspeed_weights(model, ds_config, buffer_size, is_lora=False):
3348
is_zero3 = ds_config.is_zero3()
34-
named_params = [(name, param) for name, param in model.named_parameters()]
49+
named_params = _get_deepspeed_named_params(model, ds_config, is_lora=is_lora)
3550

3651
waiting_params, waiting_params_size = [], 0
3752
for name, param in named_params:
@@ -150,7 +165,7 @@ def _setup_broadcast_group(self):
150165
def _colocated_model_update(self):
151166
refs = []
152167
for named_weights in gather_deepspeed_weights(
153-
self.model, self.ds_config, buffer_size=self._model_update_buffer_size
168+
self.model, self.ds_config, buffer_size=self._model_update_buffer_size, is_lora=self.is_lora
154169
):
155170
serialized_tensors = serialize_named_weights(
156171
named_weights, infer_strategy=self.infer_worker_config.strategy_args.strategy_name
@@ -167,11 +182,16 @@ def _colocated_model_update(self):
167182
ray.get(refs)
168183
refs = []
169184
if co_infer_rank == 0 and self._co_infer_worker is not None:
170-
refs.append(self._co_infer_worker.update_parameter_in_bucket.remote(infer_parallel_tensors))
185+
refs.append(
186+
self._co_infer_worker.update_parameter_in_bucket.remote(
187+
infer_parallel_tensors, is_lora=self.is_lora
188+
)
189+
)
171190
if self._broadcast_workers:
172191
refs.extend(self._broadcast_to_infer_workers(named_weights))
173192
if refs:
174193
ray.get(refs)
194+
self._add_lora_to_infer_workers()
175195
return {}
176196

177197
def _broadcast_to_infer_workers(self, named_weights) -> list[ray.ObjectRef]:
@@ -183,6 +203,7 @@ def _broadcast_to_infer_workers(self, named_weights) -> list[ray.ObjectRef]:
183203
names=[n for n, _ in named_weights],
184204
dtypes=[w.dtype for _, w in named_weights],
185205
shapes=[w.shape for _, w in named_weights],
206+
is_lora=self.is_lora,
186207
)
187208
for worker in self._broadcast_workers
188209
]
@@ -198,8 +219,17 @@ def _broadcast_to_infer_workers(self, named_weights) -> list[ray.ObjectRef]:
198219
def _separated_model_update(self):
199220
logger.info(f"start broadcast model update {self.model_update_group_name}")
200221
for named_weights in gather_deepspeed_weights(
201-
self.model, self.ds_config, buffer_size=self._model_update_buffer_size
222+
self.model, self.ds_config, buffer_size=self._model_update_buffer_size, is_lora=self.is_lora
202223
):
203224
refs = self._broadcast_to_infer_workers(named_weights)
204225
ray.get(refs)
226+
self._add_lora_to_infer_workers()
205227
return {}
228+
229+
def _add_lora_to_infer_workers(self):
230+
if dist.get_rank() != 0 or not self.is_lora:
231+
return
232+
peft_config = self.model.peft_config.get("default", None)
233+
ray.get(
234+
[worker.add_lora.remote(peft_config=asdict(peft_config)) for worker in self.model_update_infer_workers]
235+
)

roll/utils/deepspeed_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def get_optimizer_grouped_parameters(
3636
"weight_decay": 0.0,
3737
},
3838
]
39-
return optimizer_grouped_parameters
39+
return [group for group in optimizer_grouped_parameters if group["params"]]
4040

4141

4242
def _z3_params_to_fetch(param_list):

0 commit comments

Comments
 (0)