44this service pushes the latest training weights from the CPU bucket cache to the
55woken 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
1314This service is a Ray actor; one instance per pipeline, created by
1415NemoRLFullFinetunePipeline.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"""
2217from __future__ import annotations
2318
2419import logging
20+ import uuid
2521from typing import Any , List , Optional
2622
2723import 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 :
0 commit comments