Skip to content

Commit 66232a6

Browse files
committed
fix: Address CodeRabbitAI review feedback on WeightSynchronizer
- Validate refit_buffer_size_gb > 0 in factory and _compute_buffer_size - Safely parse NRL_REFIT_BUFFER_MEMORY_RATIO env var with error handling - Wrap IPC and HTTP sync_weights transfer in try/finally so phase restoration (offload_after_refit, kv_cache rebuild) always runs - Add 7 new tests covering validation and phase-restoration behavior Signed-off-by: Saurabh Mishra <sauramishra@nvidia.com>
1 parent db87da7 commit 66232a6

4 files changed

Lines changed: 146 additions & 45 deletions

File tree

nemo_rl/weight_sync/factory.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ def create_weight_synchronizer(
7474
generation=generation,
7575
)
7676
elif generation_backend in (VLLM_BACKEND, MEGATRON_BACKEND):
77+
if refit_buffer_size_gb is not None and refit_buffer_size_gb <= 0:
78+
raise ValueError("refit_buffer_size_gb must be > 0")
7779
from nemo_rl.weight_sync.ipc_weight_synchronizer import (
7880
IPCWeightSynchronizer,
7981
)

nemo_rl/weight_sync/http_weight_synchronizer.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -61,30 +61,34 @@ def sync_weights(
6161
self._policy.offload_before_refit()
6262
self._generation.prepare_for_generation(tags=["weights"])
6363

64-
timer_context = (
65-
timer.time("prepare_for_generation/transfer_and_update_weights")
66-
if timer is not None
67-
else nullcontext()
68-
)
69-
with timer_context:
70-
sglang_url_to_gpu_uuids = (
71-
self._generation.get_sglang_url_to_gpu_uuids()
64+
sync_succeeded = False
65+
try:
66+
timer_context = (
67+
timer.time("prepare_for_generation/transfer_and_update_weights")
68+
if timer is not None
69+
else nullcontext()
7270
)
73-
74-
flush_success = self._generation.invalidate_kv_cache()
75-
if not flush_success:
76-
print(
77-
"SGLang KV cache invalidation failed before weight update. "
71+
with timer_context:
72+
sglang_url_to_gpu_uuids = (
73+
self._generation.get_sglang_url_to_gpu_uuids()
7874
)
7975

80-
futures_train = self._policy.stream_weights_via_http(
81-
sglang_url_to_gpu_uuids=sglang_url_to_gpu_uuids,
82-
)
83-
ray.get(futures_train)
76+
flush_success = self._generation.invalidate_kv_cache()
77+
if not flush_success:
78+
print(
79+
"SGLang KV cache invalidation failed before weight update. "
80+
)
81+
82+
futures_train = self._policy.stream_weights_via_http(
83+
sglang_url_to_gpu_uuids=sglang_url_to_gpu_uuids,
84+
)
85+
ray.get(futures_train)
86+
sync_succeeded = True
87+
finally:
88+
self._policy.offload_after_refit()
89+
self._generation.prepare_for_generation(tags=["kv_cache"])
8490

85-
self._policy.offload_after_refit()
86-
self._generation.prepare_for_generation(tags=["kv_cache"])
87-
self._stale = False
91+
self._stale = not sync_succeeded
8892

8993
@property
9094
def is_stale(self) -> bool:

nemo_rl/weight_sync/ipc_weight_synchronizer.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -72,34 +72,38 @@ def sync_weights(
7272
self._policy.offload_before_refit()
7373
self._generation.prepare_for_generation(tags=["weights"])
7474

75-
timer_context = (
76-
timer.time("prepare_for_generation/transfer_and_update_weights")
77-
if timer is not None
78-
else nullcontext()
79-
)
80-
with timer_context:
81-
buffer_size_bytes = self._compute_buffer_size()
82-
83-
futures_train = self._policy.stream_weights_via_ipc_zmq(
84-
buffer_size_bytes=buffer_size_bytes
75+
sync_succeeded = False
76+
try:
77+
timer_context = (
78+
timer.time("prepare_for_generation/transfer_and_update_weights")
79+
if timer is not None
80+
else nullcontext()
8581
)
86-
futures_inference = self._generation.update_weights_via_ipc_zmq()
82+
with timer_context:
83+
buffer_size_bytes = self._compute_buffer_size()
8784

88-
ray.get(futures_train)
89-
results = ray.get(futures_inference)
90-
update_success = all(
91-
result for result in results if result is not None
92-
)
85+
futures_train = self._policy.stream_weights_via_ipc_zmq(
86+
buffer_size_bytes=buffer_size_bytes
87+
)
88+
futures_inference = self._generation.update_weights_via_ipc_zmq()
9389

94-
if not update_success:
95-
raise RuntimeError(
96-
"Weight transfer failed during IPC/ZMQ sync. "
97-
"This often indicates an issue with cuda-ipc or the vLLM worker."
90+
ray.get(futures_train)
91+
results = ray.get(futures_inference)
92+
update_success = all(
93+
result for result in results if result is not None
9894
)
9995

100-
self._policy.offload_after_refit()
101-
self._generation.prepare_for_generation(tags=["kv_cache"])
102-
self._stale = False
96+
if not update_success:
97+
raise RuntimeError(
98+
"Weight transfer failed during IPC/ZMQ sync. "
99+
"This often indicates an issue with cuda-ipc or the vLLM worker."
100+
)
101+
sync_succeeded = True
102+
finally:
103+
self._policy.offload_after_refit()
104+
self._generation.prepare_for_generation(tags=["kv_cache"])
105+
106+
self._stale = not sync_succeeded
103107

104108
@property
105109
def is_stale(self) -> bool:
@@ -117,7 +121,19 @@ def shutdown(self) -> None:
117121

118122
def _compute_buffer_size(self) -> int:
119123
if self._refit_buffer_size_gb is not None:
124+
if self._refit_buffer_size_gb <= 0:
125+
raise ValueError("refit_buffer_size_gb must be > 0")
120126
return self._refit_buffer_size_gb * (1024**3)
121127

122-
memory_ratio = os.getenv("NRL_REFIT_BUFFER_MEMORY_RATIO", "0.3")
123-
return int(self._policy.get_free_memory_bytes() * float(memory_ratio))
128+
memory_ratio_raw = os.getenv("NRL_REFIT_BUFFER_MEMORY_RATIO", "0.3")
129+
try:
130+
memory_ratio = float(memory_ratio_raw)
131+
except ValueError as exc:
132+
raise ValueError(
133+
f"NRL_REFIT_BUFFER_MEMORY_RATIO must be a valid float, got {memory_ratio_raw!r}"
134+
) from exc
135+
if memory_ratio <= 0:
136+
raise ValueError(
137+
f"NRL_REFIT_BUFFER_MEMORY_RATIO must be > 0, got {memory_ratio}"
138+
)
139+
return int(self._policy.get_free_memory_bytes() * memory_ratio)

tests/unit/weight_sync/test_weight_synchronizer.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,46 @@ def test_init_communicator(self):
179179
policy.prepare_refit_info.assert_called_once()
180180
gen.prepare_refit_info.assert_called_once()
181181

182+
@patch("nemo_rl.weight_sync.ipc_weight_synchronizer.ray")
183+
def test_phase_restoration_on_transfer_failure(self, mock_ray):
184+
"""offload_after_refit and kv_cache prep run even when transfer raises."""
185+
mock_ray.get.side_effect = RuntimeError("IPC transfer exploded")
186+
policy = _mock_policy()
187+
gen = _mock_generation()
188+
sync = IPCWeightSynchronizer(policy, gen)
189+
190+
with pytest.raises(RuntimeError, match="IPC transfer exploded"):
191+
sync.sync_weights()
192+
193+
policy.offload_after_refit.assert_called_once()
194+
gen.prepare_for_generation.assert_any_call(tags=["kv_cache"])
195+
assert sync.is_stale
196+
197+
def test_negative_buffer_size_raises(self):
198+
policy = _mock_policy()
199+
gen = _mock_generation()
200+
sync = IPCWeightSynchronizer(policy, gen, refit_buffer_size_gb=-1)
201+
with pytest.raises(ValueError, match="refit_buffer_size_gb must be > 0"):
202+
sync._compute_buffer_size()
203+
204+
@patch("nemo_rl.weight_sync.ipc_weight_synchronizer.ray")
205+
def test_invalid_env_ratio_raises(self, mock_ray, monkeypatch):
206+
monkeypatch.setenv("NRL_REFIT_BUFFER_MEMORY_RATIO", "not_a_number")
207+
policy = _mock_policy()
208+
gen = _mock_generation()
209+
sync = IPCWeightSynchronizer(policy, gen)
210+
with pytest.raises(ValueError, match="must be a valid float"):
211+
sync._compute_buffer_size()
212+
213+
@patch("nemo_rl.weight_sync.ipc_weight_synchronizer.ray")
214+
def test_zero_env_ratio_raises(self, mock_ray, monkeypatch):
215+
monkeypatch.setenv("NRL_REFIT_BUFFER_MEMORY_RATIO", "0")
216+
policy = _mock_policy()
217+
gen = _mock_generation()
218+
sync = IPCWeightSynchronizer(policy, gen)
219+
with pytest.raises(ValueError, match="must be > 0"):
220+
sync._compute_buffer_size()
221+
182222

183223
# ---------------------------------------------------------------------------
184224
# HTTPWeightSynchronizer
@@ -237,6 +277,21 @@ def test_init_communicator(self):
237277
policy.prepare_refit_info.assert_called_once()
238278
gen.prepare_refit_info.assert_called_once()
239279

280+
@patch("nemo_rl.weight_sync.http_weight_synchronizer.ray")
281+
def test_phase_restoration_on_transfer_failure(self, mock_ray):
282+
"""offload_after_refit and kv_cache prep run even when transfer raises."""
283+
mock_ray.get.side_effect = RuntimeError("HTTP transfer exploded")
284+
policy = _mock_policy()
285+
gen = _mock_generation()
286+
sync = HTTPWeightSynchronizer(policy, gen)
287+
288+
with pytest.raises(RuntimeError, match="HTTP transfer exploded"):
289+
sync.sync_weights()
290+
291+
policy.offload_after_refit.assert_called_once()
292+
gen.prepare_for_generation.assert_any_call(tags=["kv_cache"])
293+
assert sync.is_stale
294+
240295

241296
# ---------------------------------------------------------------------------
242297
# CollectiveWeightSynchronizer
@@ -399,3 +454,27 @@ def test_unknown_backend_raises(self):
399454
generation_backend="vlllm",
400455
colocated=True,
401456
)
457+
458+
def test_negative_refit_buffer_size_raises(self):
459+
policy = _mock_policy()
460+
gen = _mock_generation()
461+
with pytest.raises(ValueError, match="refit_buffer_size_gb must be > 0"):
462+
create_weight_synchronizer(
463+
policy=policy,
464+
generation=gen,
465+
generation_backend=VLLM_BACKEND,
466+
colocated=True,
467+
refit_buffer_size_gb=-1,
468+
)
469+
470+
def test_zero_refit_buffer_size_raises(self):
471+
policy = _mock_policy()
472+
gen = _mock_generation()
473+
with pytest.raises(ValueError, match="refit_buffer_size_gb must be > 0"):
474+
create_weight_synchronizer(
475+
policy=policy,
476+
generation=gen,
477+
generation_backend=VLLM_BACKEND,
478+
colocated=True,
479+
refit_buffer_size_gb=0,
480+
)

0 commit comments

Comments
 (0)