Skip to content

Commit 51285c3

Browse files
committed
fix: implement sync_selected_workers + fix wake_up_partial call
nemo_rl_model_update_service.py: - Implement sync_selected_workers() using megatron_policy_worker's selective_sync_active_cache (cpu_serialize transport, no topology analysis). - Add _get_policy_workers() helper: resolves training worker actors from policy.src_cluster.workers, .workers, list, or single handle. nemo_rl_pipeline.py: - Change wake_up_partial(ranks) → wake_up_partial(ranks, skip_activate=True) so woken ranks stay off routing table until weight sync finishes (Step 5).
1 parent 90dd642 commit 51285c3

2 files changed

Lines changed: 130 additions & 53 deletions

File tree

rlix/pipeline/nemo_rl_model_update_service.py

Lines changed: 128 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,20 @@
44
this service pushes the latest training weights from the CPU bucket cache to the
55
woken inference workers.
66
7-
Transport paths (mirroring NeMo RL's existing transports):
8-
- CUDA IPC — sender and receiver share the same physical GPU (overlap shards).
9-
Zero-copy; only correct path when two ranks are on the same GPU.
10-
- NCCL bcast — receiver is on a different GPU. Uses NeMo RL's packed_broadcast
11-
producer/consumer pattern (model_update.py collective group).
7+
Transport paths:
8+
- cpu_serialize — CPU uint8 bucket DMA-copied to each receiver GPU.
9+
Default; works across all GPU topologies.
10+
- cuda_ipc — Zero-copy CUDA IPC handle; only when sender and receiver
11+
share the same physical GPU (colocated overlap shards).
12+
- NCCL bcast — Broadcast via StatelessProcessGroup; cross-GPU non-colocated.
1213
1314
This service is a Ray actor; one instance per pipeline, created by
1415
NemoRLFullFinetunePipeline.initialize_pipeline().
15-
16-
NOTE (Feature 4 dependency):
17-
sync_selected_workers currently raises NotImplementedError until the CPU
18-
bucket cache (Feature 4) and selective transport routing (Feature 4/6) are
19-
implemented in the NeMo RL repo. The interface is complete so F5/F6 wiring
20-
compiles and can be tested end-to-end once F4 lands.
2116
"""
2217
from __future__ import annotations
2318

2419
import logging
20+
import uuid
2521
from typing import Any, List, Optional
2622

2723
import ray
@@ -36,16 +32,14 @@ class NemoRLModelUpdateService:
3632
Holds references to the Megatron training policy and the vLLM generation
3733
interface. sync_selected_workers is called in two scenarios:
3834
- expand path: DP ranks that just woke up (scheduler-driven expand).
39-
- active refresh path: DP ranks currently serving requests (partial-overlap
40-
ranks that did not shrink during training and will not pass through expand).
41-
In both cases, untargeted shards are not contacted and continue generation.
35+
- active refresh path: DP ranks currently serving requests.
4236
4337
Args:
4438
pipeline_id: Unique identifier for this pipeline.
45-
policy: NeMo RL ColocatablePolicyInterface (Megatron backend).
46-
Must expose build_cpu_bucket_cache / cache_ready_step
47-
once Feature 4 is implemented.
48-
policy_generation: NeMo RL VllmGeneration instance owning the vLLM workers.
39+
policy: NeMo RL policy object. Must expose worker actors that
40+
implement selective_sync_active_cache (MegatronPolicyWorkerImpl).
41+
Supported patterns: .src_cluster.workers, .workers, list, single actor.
42+
policy_generation: VllmGeneration Ray actor handle.
4943
"""
5044

5145
def __init__(
@@ -61,57 +55,140 @@ def __init__(
6155
self._policy = policy
6256
self._policy_generation = policy_generation
6357

64-
logger.info(
65-
"[NemoRLModelUpdateService] init pipeline_id=%s", pipeline_id
66-
)
58+
logger.info("[NemoRLModelUpdateService] init pipeline_id=%s", pipeline_id)
6759

6860
def sync_selected_workers(
6961
self,
7062
tgt_dp_ranks: List[int],
7163
verify: bool = False,
7264
) -> None:
73-
"""Push latest training weights to the specified inference DP shards.
74-
75-
High-level flow (once Feature 4 is implemented):
76-
1. Assert CPU bucket cache is ready (_cache_ready_step >= 0).
77-
2. Determine transport per target device:
78-
- Same physical GPU as cache owner → CUDA IPC (zero-copy).
79-
- Different GPU → NCCL broadcast.
80-
3. For each bucket in the CPU cache:
81-
a. Stage CPU → GPU (sender side, controlled staging buffer).
82-
b. Send via IPC handle (colocated) or NCCL broadcast (remote).
83-
c. Receiver calls model_runner.model.load_weights() to apply.
84-
d. Release staging buffer before next bucket.
85-
4. Optionally verify weights via checksum comparison.
86-
87-
Non-targeted shards (non-overlap GPUs) are NOT contacted; they continue
88-
generation without pause.
65+
"""Push active CPU bucket cache to the specified inference DP shards.
66+
67+
Flow:
68+
1. Get inference receiver surface from VllmGeneration.
69+
2. Build comm plan (cpu_serialize, no NCCL topology analysis).
70+
3. Call selective_sync_active_cache on ALL training workers;
71+
only the cache owner (pp0/dp0/tp0) does actual transport.
72+
4. Finalize post-load hooks on inference workers.
73+
5. Optionally verify weight checksums.
8974
9075
Args:
91-
tgt_dp_ranks: DP ranks to push weights to. Two callers:
92-
- expand path: ranks that just woke up, not yet routing.
93-
- active refresh path: ranks currently serving requests;
94-
implementation must synchronize CUDA streams after
95-
load_weights() to avoid mid-inference weight switching.
96-
verify: When True, run post-sync weight verification checksums.
97-
98-
Raises:
99-
NotImplementedError: Until Feature 4 (CPU bucket cache) is implemented.
76+
tgt_dp_ranks: Inference DP ranks to update.
77+
verify: When True, run post-sync checksum verification.
10078
"""
10179
if not tgt_dp_ranks:
10280
raise ValueError("tgt_dp_ranks must be non-empty")
10381

10482
logger.info(
105-
"[NemoRLModelUpdateService] sync_selected_workers "
83+
"[NemoRLModelUpdateService] sync_selected_workers start "
10684
"pipeline_id=%s tgt_dp_ranks=%s",
10785
self._pipeline_id,
10886
tgt_dp_ranks,
10987
)
11088

111-
raise NotImplementedError(
112-
"NeMo RL selective base-weight sync requires the Feature 4 sender "
113-
"implementation (CPU bucket cache transport). Refusing to mark stale "
114-
"inference workers as synced."
89+
# --- Step 1: inference receiver surface ---
90+
# VllmGeneration is a Ray actor; get_model_update_receiver returns a
91+
# SimpleNamespace(workers, rank2worker, worker_config).
92+
receiver = ray.get(self._policy_generation.get_model_update_receiver.remote())
93+
num_gpus_per_worker: int = int(receiver.worker_config.num_gpus_per_worker)
94+
device_mapping: List[int] = list(receiver.worker_config.device_mapping or [])
95+
dp_size: int = len(receiver.rank2worker)
96+
97+
# Build tgt_workers as a list indexed by dp_rank (required by
98+
# selective_sync_active_cache: tgt_workers[dp_rank] → leader actor).
99+
tgt_workers_indexed = [receiver.rank2worker[r] for r in range(dp_size)]
100+
101+
# --- Step 2: comm plan (cpu_serialize — no NCCL group needed) ---
102+
sync_id = f"{self._pipeline_id}_{uuid.uuid4().hex[:8]}"
103+
comm_plan = {
104+
sync_id: {
105+
"group_name": sync_id,
106+
"master_addr": "127.0.0.1",
107+
"master_port": 0, # unused for cpu_serialize
108+
"tgt_devices": [], # unused for cpu_serialize
109+
"ipc_targets": [
110+
{
111+
"dp_rank": dp_rank,
112+
"local_ranks": list(range(num_gpus_per_worker)),
113+
}
114+
for dp_rank in tgt_dp_ranks
115+
],
116+
"broadcast_local_ranks_by_dp_rank": {}, # no NCCL
117+
}
118+
}
119+
120+
# --- Step 3: run selective sync on all training workers ---
121+
# selective_sync_active_cache is a no-op on non-owner ranks.
122+
policy_workers = self._get_policy_workers()
123+
sync_refs = [
124+
w.selective_sync_active_cache.remote(
125+
sync_id=sync_id,
126+
comm_plan=comm_plan,
127+
tgt_dp_ranks=tgt_dp_ranks,
128+
tgt_workers=tgt_workers_indexed,
129+
tgt_device_mapping=device_mapping or list(range(dp_size)),
130+
tgt_num_gpus_per_worker=num_gpus_per_worker,
131+
model_update_transport="cpu_serialize",
132+
)
133+
for w in policy_workers
134+
]
135+
results = ray.get(sync_refs)
136+
137+
# --- Step 4: finalize post-load hooks on all inference workers ---
138+
# VllmGeneration.finalize_weight_update() is a pass-through that calls
139+
# process_weights_after_loading on all workers (idempotent).
140+
ray.get(self._policy_generation.finalize_weight_update.remote())
141+
142+
# --- Step 5: optional weight verification ---
143+
if verify:
144+
weight_stats: Optional[dict] = None
145+
for r in results:
146+
if isinstance(r, dict) and "weight_stats" in r:
147+
weight_stats = r["weight_stats"]
148+
break
149+
if weight_stats:
150+
ray.get(self._policy_generation.verify_model.remote(weight_stats))
151+
152+
logger.info(
153+
"[NemoRLModelUpdateService] sync_selected_workers done "
154+
"pipeline_id=%s tgt_dp_ranks=%s",
155+
self._pipeline_id,
156+
tgt_dp_ranks,
157+
)
158+
159+
def _get_policy_workers(self) -> List[Any]:
160+
"""Resolve list of training worker Ray actor handles from self._policy.
161+
162+
Tries common NeMo RL policy API patterns in priority order:
163+
1. policy.src_cluster.workers (NeMo RL ClusterSpec pattern)
164+
2. policy.workers (direct cluster with .workers list)
165+
3. policy itself is a list/tuple of Ray actor handles
166+
4. policy is a single Ray actor handle
167+
"""
168+
# Pattern 1: policy.src_cluster.workers
169+
src_cluster = getattr(self._policy, "src_cluster", None)
170+
if src_cluster is not None:
171+
workers = getattr(src_cluster, "workers", None)
172+
if workers:
173+
return list(workers)
174+
175+
# Pattern 2: policy.workers
176+
workers = getattr(self._policy, "workers", None)
177+
if workers:
178+
return list(workers)
179+
180+
# Pattern 3: policy is a list/tuple of actor handles
181+
if isinstance(self._policy, (list, tuple)) and self._policy:
182+
return list(self._policy)
183+
184+
# Pattern 4: single actor handle
185+
if self._policy is not None:
186+
return [self._policy]
187+
188+
raise RuntimeError(
189+
f"[NemoRLModelUpdateService] Cannot resolve training workers from policy "
190+
f"(type={type(self._policy).__name__}). Policy must expose "
191+
".src_cluster.workers, .workers, or be a list/single Ray actor handle."
115192
)
116193

117194
def __repr__(self) -> str:

rlix/pipeline/nemo_rl_pipeline.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,8 +427,8 @@ def _expand_workers(self, *, dp_ranks_to_add: List[int]) -> None:
427427
self._policy_generation.mark_dp_ranks_inactive(ranks)
428428

429429
# Step 2: Wake sleeping workers (training already offloaded — no OOM risk).
430-
# F2: VllmGeneration.wake_up_partial(dp_ranks)
431-
self._policy_generation.wake_up_partial(ranks)
430+
# skip_activate=True: keep ranks off routing until weight sync finishes (Step 5).
431+
self._policy_generation.wake_up_partial(ranks, skip_activate=True)
432432
self._pre_activation_ranks.update(ranks)
433433

434434
# Steps 3-5: atomic block.

0 commit comments

Comments
 (0)