-
Notifications
You must be signed in to change notification settings - Fork 420
Expand file tree
/
Copy pathworker_dispatch.py
More file actions
326 lines (262 loc) · 13.2 KB
/
Copy pathworker_dispatch.py
File metadata and controls
326 lines (262 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
"""
WorkerDispatch: Manages all actor groups with automatic offload/onload.
Automatically handles GPU placement:
- Tracks which model is currently on GPU
- If colocation is enabled, offloads other models when one is requested
The trainer interacts with the worker dispatch if all models are always on GPU.
"""
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
import ray
from omegaconf import DictConfig
from skyrl_train.distributed.dispatch import concatenate_outputs_after_mesh_dispatch
from skyrl_train.inference_engines.inference_engine_client import InferenceEngineClient
from skyrl_train.training_batch import TrainingInputBatch, TrainingOutputBatch
from skyrl_train.workers.worker import PPORayActorGroup
@dataclass
class GPUState:
"""Tracks what's on GPU for a model."""
model_on_gpu: bool = False
optimizer_on_gpu: bool = False
class WorkerDispatch:
"""
Unified dispatch layer that manages all actor groups (policy, critic, ref).
Handles automatic offload/onload when colocate_all=True.
"""
def __init__(
self,
cfg: DictConfig,
policy_actor_group: PPORayActorGroup,
critic_actor_group: Optional[PPORayActorGroup] = None,
ref_actor_group: Optional[PPORayActorGroup] = None,
inference_engine_client: Optional[InferenceEngineClient] = None,
):
self.cfg = cfg
self.colocate_all = cfg.trainer.placement.colocate_all
self.colocate_policy_ref = cfg.trainer.placement.colocate_policy_ref
# Inference engine client for weight sync (optional)
self._inference_engine_client = inference_engine_client
# Actor groups by name.
# TODO: Remove these role-specific identifiers. We will move to using model IDs and add support for generic models beyond these.
self._actor_groups: Dict[str, PPORayActorGroup] = {"policy": policy_actor_group}
if critic_actor_group is not None:
self._actor_groups["critic"] = critic_actor_group
if ref_actor_group is not None:
self._actor_groups["ref"] = ref_actor_group
# GPU state tracking (only matters when colocated)
self._gpu_state: Dict[str, GPUState] = {name: GPUState() for name in self._actor_groups.keys()}
def get_lcm_dp_size(self) -> int:
"""Get LCM of all models' dp_size."""
import math
dp_size = self._actor_groups["policy"].actor_infos[0].rank.dp_size
if "critic" in self._actor_groups:
dp_size = math.lcm(dp_size, self._actor_groups["critic"].actor_infos[0].rank.dp_size)
if "ref" in self._actor_groups:
dp_size = math.lcm(dp_size, self._actor_groups["ref"].actor_infos[0].rank.dp_size)
return dp_size
def _should_manage_offload(self, model: str) -> bool:
"""Check if we need to manage offload for this model."""
if self.colocate_all:
return True
if self.colocate_policy_ref and model in ("policy", "ref"):
return True
return False
def _get_colocation_group(self, model: str) -> List[str]:
"""Get which models share GPU with the given model."""
if self.colocate_all:
return list(self._actor_groups.keys())
elif self.colocate_policy_ref and model in ("policy", "ref"):
return [m for m in ["policy", "ref"] if m in self._actor_groups]
return [model]
def _ensure_on_gpu(self, model: str, need_optimizer: bool = True, need_model: bool = True) -> None:
"""Ensure model is on GPU, offloading others in same colocation group if needed."""
if not self._should_manage_offload(model):
return
if model not in self._actor_groups:
return
group = self._get_colocation_group(model)
# Offload others in the same colocation group
for other in group:
if other != model and other in self._actor_groups:
state = self._gpu_state[other]
if state.model_on_gpu or state.optimizer_on_gpu:
self._actor_groups[other].offload_to_cpu()
self._gpu_state[other] = GPUState()
# Backload requested model
state = self._gpu_state[model]
needs_backload = (need_model and not state.model_on_gpu) or (need_optimizer and not state.optimizer_on_gpu)
if needs_backload:
self._actor_groups[model].backload_to_gpu(
backload_optimizer=need_optimizer,
backload_model=need_model,
)
if need_model:
self._gpu_state[model].model_on_gpu = True
if need_optimizer:
self._gpu_state[model].optimizer_on_gpu = True
def _offload(self, model: str, offload_optimizer: bool = True, offload_model: bool = True) -> None:
"""Offload model to CPU."""
if not self._should_manage_offload(model):
return
if model not in self._actor_groups:
return
self._actor_groups[model].offload_to_cpu(
offload_optimizer=offload_optimizer,
offload_model=offload_model,
)
if offload_model:
self._gpu_state[model].model_on_gpu = False
if offload_optimizer:
self._gpu_state[model].optimizer_on_gpu = False
def mark_all_offloaded(self) -> None:
"""Mark all models as offloaded (call after build_models when colocate_all)."""
for model in self._actor_groups:
self._gpu_state[model] = GPUState()
def forward(self, model: str, data: TrainingInputBatch) -> TrainingOutputBatch:
"""Run inference forward pass. Only loads model (not optimizer)."""
self._ensure_on_gpu(model, need_optimizer=False, need_model=True)
refs = self._actor_groups[model].async_run_ray_method("mesh", "forward", data=data)
results = ray.get(refs)
output = concatenate_outputs_after_mesh_dispatch(self._actor_groups[model].actor_infos, results)
return output
# === Training ===
def forward_backward(self, model: str, data: TrainingInputBatch) -> Dict[str, float]:
"""Run forward/backward pass. Needs model + optimizer."""
self._ensure_on_gpu(model, need_optimizer=True, need_model=True)
refs = self._actor_groups[model].async_run_ray_method("mesh", "forward_backward", data)
statuses = ray.get(refs)
self._save_memory_snapshot(model, "forward_backward")
return statuses[0]
def optim_step(self, model: str) -> Optional[float]:
"""Run optimizer step. Model should already be on GPU from forward_backward."""
refs = self._actor_groups[model].async_run_ray_method("pass_through", "optim_step")
grad_norms = ray.get(refs)
self._save_memory_snapshot(model, "optim_step")
return grad_norms[0]
# TODO(tgriggs): Remove this when Megatron supports forward_backward and optim_step.
def ppo_train(self, model: str, data: TrainingInputBatch) -> Dict[str, float]:
"""Run full PPO training loop (for Megatron)."""
self._ensure_on_gpu(model, need_optimizer=True, need_model=True)
refs = self._actor_groups[model].async_run_ray_method("mesh", "ppo_train", data)
statuses = ray.get(refs)
return statuses[0].metadata["train_status"]
def _save_memory_snapshot(self, model: str, tag: str) -> None:
"""Save memory snapshot on workers."""
ray.get(
self._actor_groups[model].async_run_ray_method("pass_through", "save_memory_snapshot", tag=f"{model}_{tag}")
)
def save_checkpoint(self, model: str, ckpt_dir: str, tokenizer=None) -> None:
"""Save checkpoint for model."""
self._ensure_on_gpu(model, need_optimizer=True, need_model=True)
ray.get(
self._actor_groups[model].async_run_ray_method(
"pass_through", "save_checkpoint", ckpt_dir=ckpt_dir, tokenizer=tokenizer
)
)
def load_checkpoint(
self,
model: str,
ckpt_dir: str,
load_optimizer_states: bool = True,
load_lr_scheduler_states: bool = True,
) -> None:
"""Load checkpoint for model."""
self._ensure_on_gpu(model, need_optimizer=load_optimizer_states, need_model=True)
ray.get(
self._actor_groups[model].async_run_ray_method(
"pass_through",
"load_checkpoint",
ckpt_dir=ckpt_dir,
load_optimizer_states=load_optimizer_states,
load_lr_scheduler_states=load_lr_scheduler_states,
)
)
def save_hf_model(self, model: str, export_dir: str, tokenizer) -> None:
"""Save model in HuggingFace format."""
self._ensure_on_gpu(model, need_optimizer=False, need_model=True)
ray.get(self._actor_groups[model].async_run_ray_method("pass_through", "save_hf_model", export_dir, tokenizer))
def init_model(self, model: str, model_path: str, num_training_steps: Optional[int] = None) -> None:
"""Initialize model from path. Offloads others in colocation group first."""
# Offload others in colocation group before init
if self._should_manage_offload(model):
group = self._get_colocation_group(model)
for other in group:
if other != model and other in self._actor_groups:
state = self._gpu_state[other]
if state.model_on_gpu or state.optimizer_on_gpu:
self._actor_groups[other].offload_to_cpu()
self._gpu_state[other] = GPUState()
kwargs = {"model_path": model_path}
if num_training_steps is not None:
kwargs["num_training_steps"] = num_training_steps
ray.get(self._actor_groups[model].async_init_model(**kwargs))
# After init, model is on GPU
self._gpu_state[model].model_on_gpu = True
self._gpu_state[model].optimizer_on_gpu = model != "ref" # ref has no optimizer
def init_weight_sync_state(self, inference_engine_client) -> None:
"""Initialize weight sync state for policy model."""
ray.get(
self._actor_groups["policy"].async_run_ray_method(
"pass_through", "init_weight_sync_state", inference_engine_client
)
)
def broadcast_to_inference_engines(self, inference_engine_client) -> None:
"""Broadcast policy weights to inference engines."""
ray.get(
self._actor_groups["policy"].async_run_ray_method(
"pass_through", "broadcast_to_inference_engines", inference_engine_client
)
)
def prepare_for_weight_sync(self) -> None:
"""Prepare for weight sync: ensure policy model is on GPU, offload optimizer."""
if not self.colocate_all:
return
# Ensure policy model is on GPU (will offload others in colocation group)
self._ensure_on_gpu("policy", need_optimizer=False, need_model=True)
# Offload optimizer if it's on GPU
if self._gpu_state["policy"].optimizer_on_gpu:
self._offload("policy", offload_optimizer=True, offload_model=False)
def finish_weight_sync(self) -> None:
"""Finish weight sync: offload model."""
if not self.colocate_all:
return
self._offload("policy", offload_optimizer=False, offload_model=True)
async def save_weights_for_sampler(self) -> None:
"""
Tinker API method to prepare updated parameters for sampling.
Syncs weights to inference engine for sampling.
"""
if self._inference_engine_client is None:
raise RuntimeError(
"Cannot save_weights_for_sampler: no inference_engine_client configured. "
"Pass inference_engine_client to WorkerDispatch constructor or call set_inference_engine_client()."
)
# Sync weights to inference engine
self.prepare_for_weight_sync()
if self.colocate_all:
await self._inference_engine_client.wake_up(tags=["weights"])
self.broadcast_to_inference_engines(self._inference_engine_client)
self.finish_weight_sync()
if self.colocate_all:
await self._inference_engine_client.wake_up(tags=["kv_cache"])
def set_inference_engine_client(self, inference_engine_client: InferenceEngineClient) -> None:
"""Set the inference engine client for weight sync.
This can be called after construction if the client isn't available at init time.
"""
self._inference_engine_client = inference_engine_client
def empty_cache(self, model: Optional[str] = None) -> None:
"""Empty GPU cache for model(s)."""
if model is not None:
ray.get(self._actor_groups[model].async_run_ray_method("pass_through", "empty_cache"))
else:
refs = []
for group in self._actor_groups.values():
refs.extend(group.async_run_ray_method("pass_through", "empty_cache"))
ray.get(refs)
def get_node_ids(self) -> List[str]:
"""Get unique node IDs from all actor groups."""
all_node_ids = []
for group in self._actor_groups.values():
node_ids = ray.get(group.async_run_ray_method("pass_through", "get_ray_node_id"))
all_node_ids.extend(node_ids)
return list(set(all_node_ids))