Skip to content

Commit ba0e484

Browse files
committed
feat: Unify policy phase transtions and offload methods
Signed-off-by: Saurabh Mishra <sauramishra@nvidia.com>
1 parent accfb63 commit ba0e484

21 files changed

Lines changed: 342 additions & 332 deletions

examples/nemo_gym/run_grpo_nemo_gym.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from wandb import Table
2727

2828
from nemo_rl.algorithms.grpo import (
29-
ColocatablePolicyInterface,
3029
EnvironmentInterface,
3130
GenerationInterface,
3231
Logger,
@@ -71,7 +70,7 @@ def parse_args() -> tuple[argparse.Namespace, list[str]]:
7170

7271
# These types are directly imported from grpo_train since if something about the architecture changes we want to immediately fail.
7372
def collect_trajectories(
74-
policy: ColocatablePolicyInterface,
73+
policy,
7574
policy_generation: GenerationInterface,
7675
val_dataloader: StatefulDataLoader,
7776
tokenizer: TokenizerType,

nemo_rl/algorithms/distillation.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
)
5555
from nemo_rl.models.generation.vllm import VllmConfig, VllmGeneration
5656
from nemo_rl.models.policy import PolicyConfig
57-
from nemo_rl.models.policy.interfaces import ColocatablePolicyInterface
57+
from nemo_rl.models.policy.interfaces import OffloadMode, PolicyTrainerInterface
5858
from nemo_rl.models.policy.lm_policy import Policy
5959
from nemo_rl.utils.checkpoint import CheckpointingConfig, CheckpointManager
6060
from nemo_rl.utils.logger import (
@@ -162,8 +162,8 @@ def setup(
162162
train_dataset: AllTaskProcessedDataset,
163163
val_dataset: Optional[AllTaskProcessedDataset],
164164
) -> tuple[
165-
ColocatablePolicyInterface, # student_policy
166-
ColocatablePolicyInterface, # teacher_policy
165+
PolicyTrainerInterface, # student_policy
166+
PolicyTrainerInterface, # teacher_policy
167167
Optional[GenerationInterface], # student_generation
168168
Optional[WeightSynchronizer], # weight_sync
169169
StatefulDataLoader,
@@ -401,7 +401,7 @@ def setup(
401401
init_optimizer=False,
402402
init_reference_model=False,
403403
)
404-
teacher_policy.offload_after_refit()
404+
teacher_policy.finish_training()
405405

406406
# ==========================
407407
# Student Generation Interface
@@ -499,8 +499,8 @@ def setup(
499499

500500

501501
def distillation_train(
502-
student_policy: ColocatablePolicyInterface,
503-
teacher_policy: ColocatablePolicyInterface,
502+
student_policy: PolicyTrainerInterface,
503+
teacher_policy: PolicyTrainerInterface,
504504
student_generation: Optional[GenerationInterface],
505505
dataloader: StatefulDataLoader,
506506
val_dataloader: Optional[StatefulDataLoader],
@@ -682,7 +682,7 @@ def distillation_train(
682682

683683
print("▶ Preparing for teacher logprob inference...", flush=True)
684684
with timer.time("teacher_logprob_inference_prep"):
685-
teacher_policy.prepare_for_lp_inference()
685+
teacher_policy.finish_training(offload_mode=OffloadMode.EVAL_ONLY)
686686

687687
print("▶ Computing teacher logprobs...", flush=True)
688688
with timer.time("teacher_logprob_inference"):
@@ -696,7 +696,7 @@ def distillation_train(
696696

697697
print("▶ Preparing for training...", flush=True)
698698
with timer.time("training_prep"):
699-
teacher_policy.offload_after_refit()
699+
teacher_policy.finish_training()
700700
student_policy.prepare_for_training() # set model train and reload optim to GPU
701701
if weight_sync is not None:
702702
weight_sync.mark_stale()

nemo_rl/algorithms/grpo.py

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
from nemo_rl.models.generation.sglang import SGLangConfig, SGLangGeneration
7171
from nemo_rl.models.generation.vllm import VllmConfig, VllmGeneration
7272
from nemo_rl.models.policy import PolicyConfig
73-
from nemo_rl.models.policy.interfaces import ColocatablePolicyInterface
73+
from nemo_rl.models.policy.interfaces import OffloadMode, PolicyTrainerInterface
7474
from nemo_rl.models.policy.lm_policy import Policy
7575
from nemo_rl.utils.checkpoint import CheckpointingConfig, CheckpointManager
7676
from nemo_rl.utils.logger import (
@@ -220,7 +220,7 @@ def setup(
220220
val_dataset: Optional[AllTaskProcessedDataset],
221221
processor: Optional[AutoProcessor] = None,
222222
) -> tuple[
223-
ColocatablePolicyInterface,
223+
PolicyTrainerInterface,
224224
Optional[GenerationInterface],
225225
tuple[RayVirtualCluster, RayVirtualCluster],
226226
Optional[WeightSynchronizer],
@@ -1089,7 +1089,7 @@ def _extract_prompt_only_messages(message_logs: list) -> list:
10891089

10901090

10911091
def refit_policy_generation(
1092-
policy: ColocatablePolicyInterface,
1092+
policy: PolicyTrainerInterface,
10931093
policy_generation: GenerationInterface,
10941094
colocated_inference: bool,
10951095
_refit_buffer_size_gb: Optional[int] = None,
@@ -1117,26 +1117,23 @@ def refit_policy_generation(
11171117
DeprecationWarning,
11181118
stacklevel=2,
11191119
)
1120+
from nemo_rl.models.policy.interfaces import OffloadMode
1121+
11201122
if colocated_inference:
1121-
policy.offload_before_refit()
1123+
policy.finish_training(offload_mode=OffloadMode.OPTIMIZER_ONLY)
11221124
policy_generation.prepare_for_generation(tags=["weights"])
11231125

1124-
# Create a context manager that does nothing when timer is None
11251126
timer_context = (
11261127
timer.time("prepare_for_generation/transfer_and_update_weights")
11271128
if timer is not None
11281129
else nullcontext()
11291130
)
11301131
with timer_context:
1131-
# update weights
11321132
update_success = False
11331133
if colocated_inference:
1134-
# get model param keys, which is grouped by size
11351134
if _refit_buffer_size_gb is not None:
11361135
buffer_size_bytes = _refit_buffer_size_gb * (1024**3)
11371136
else:
1138-
# Empirically sets ratio as 30% to maximize efficiency.
1139-
# The remaining 70% is a necessary buffer reserved for the parameter all-gathering across the expert-parallelism dimension.
11401137
memory_ratio = os.getenv("NRL_REFIT_BUFFER_MEMORY_RATIO", "0.3")
11411138
buffer_size_bytes = int(
11421139
policy.get_free_memory_bytes() * float(memory_ratio)
@@ -1146,41 +1143,33 @@ def refit_policy_generation(
11461143
sglang_url_to_gpu_uuids = (
11471144
policy_generation.get_sglang_url_to_gpu_uuids()
11481145
)
1149-
# Stream weights via HTTP
11501146
flush_success = policy_generation.invalidate_kv_cache()
11511147
if not flush_success:
11521148
print("SGLang KV cache invalidation failed before weight update. ")
11531149
futures_train = policy.stream_weights_via_http(
11541150
sglang_url_to_gpu_uuids=sglang_url_to_gpu_uuids,
11551151
)
1156-
# Wait for all workers to complete
11571152
ray.get(futures_train)
11581153
update_success = True
11591154
else:
1160-
# Original ZMQ IPC path for vLLM
11611155
futures_train = policy.stream_weights_via_ipc_zmq(
11621156
buffer_size_bytes=buffer_size_bytes
11631157
)
11641158
futures_inference = policy_generation.update_weights_via_ipc_zmq()
1165-
# wait for all futures to complete
11661159
ray.get(futures_train)
11671160
results = ray.get(futures_inference)
11681161
update_success = all(result for result in results if result is not None)
11691162
else:
1170-
# update weights through nccl
1171-
# SGLang haven't implemented non-colocated inference mode.
11721163
if isinstance(policy_generation, SGLangGeneration):
11731164
raise NotImplementedError(
11741165
"SGLang haven't implemented non-colocated inference mode. "
11751166
)
11761167
futures_train = policy.broadcast_weights_for_collective(kv_scales=kv_scales)
11771168
futures_inference = policy_generation.update_weights_from_collective()
1178-
# wait for all futures to complete
11791169
ray.get(futures_train)
11801170
results = ray.get(futures_inference)
11811171
update_success = all(result for result in results if result is not None)
11821172

1183-
# check if update is successful
11841173
if not update_success:
11851174
error_tag = "cuda-ipc" if colocated_inference else "nccl"
11861175
error_message = (
@@ -1191,7 +1180,7 @@ def refit_policy_generation(
11911180
raise RuntimeError(error_message)
11921181

11931182
if colocated_inference:
1194-
policy.offload_after_refit()
1183+
policy.finish_training(offload_mode=OffloadMode.FULL)
11951184
policy_generation.prepare_for_generation(tags=["kv_cache"])
11961185

11971186

@@ -1308,7 +1297,7 @@ def compute_and_apply_seq_logprob_error_masking(
13081297

13091298

13101299
def grpo_train(
1311-
policy: ColocatablePolicyInterface,
1300+
policy: PolicyTrainerInterface,
13121301
policy_generation: Optional[GenerationInterface],
13131302
wrapped_dataloader: StatefulDataLoader | MultipleDataloaderWrapper,
13141303
val_dataloader: Optional[StatefulDataLoader],
@@ -1460,7 +1449,7 @@ def grpo_train(
14601449
# Compute KV scales if needed for FP8 quantization
14611450
if sync_kv_scales and kv_scales_cache is None:
14621451
print("▶ Computing KV cache scales...", flush=True)
1463-
policy.prepare_for_lp_inference()
1452+
policy.finish_training(offload_mode=OffloadMode.EVAL_ONLY)
14641453
# Align with training data processing to ensure parallel training compatibility
14651454
calib_flat, calib_input_lengths = (
14661455
batched_message_log_to_flat_message(
@@ -1494,7 +1483,7 @@ def grpo_train(
14941483
)
14951484
else:
14961485
if colocated_inference:
1497-
policy.offload_after_refit() # unload optimizer to make space for generation
1486+
policy.finish_training()
14981487
policy_generation.prepare_for_generation()
14991488

15001489
dynamic_sampling_num_gen_batches += 1
@@ -1727,7 +1716,7 @@ def grpo_train(
17271716
memory_tracker.snapshot_start_of_stage("Computing logprobs", dir())
17281717
print("▶ Preparing for logprob inference...", flush=True)
17291718
with timer.time("logprob_inference_prep"):
1730-
policy.prepare_for_lp_inference()
1719+
policy.finish_training(offload_mode=OffloadMode.EVAL_ONLY)
17311720

17321721
print("▶ Computing logprobs...", flush=True)
17331722
with timer.time("policy_and_reference_logprobs"):
@@ -1841,7 +1830,7 @@ def grpo_train(
18411830
)
18421831
else:
18431832
if colocated_inference:
1844-
policy.offload_after_refit() # unload optimizer to make space for generation
1833+
policy.finish_training()
18451834
policy_generation.prepare_for_generation()
18461835
val_metrics, validation_timings = validate(
18471836
policy_generation,
@@ -2350,7 +2339,7 @@ def validate(
23502339

23512340

23522341
def async_grpo_train(
2353-
policy: ColocatablePolicyInterface,
2342+
policy: PolicyTrainerInterface,
23542343
policy_generation: Optional[GenerationInterface],
23552344
dataloader: StatefulDataLoader,
23562345
val_dataloader: Optional[StatefulDataLoader],
@@ -2768,7 +2757,7 @@ def async_grpo_train(
27682757
# Training phase (same as sync version)
27692758
print("▶ Preparing for logprob inference...")
27702759
with timer.time("logprob_inference_prep"):
2771-
policy.prepare_for_lp_inference()
2760+
policy.finish_training(offload_mode=OffloadMode.EVAL_ONLY)
27722761

27732762
print("▶ Computing logprobs...")
27742763
with timer.time("policy_and_reference_logprobs"):
@@ -3048,7 +3037,7 @@ def async_grpo_train(
30483037
os.path.join(checkpoint_path, "train_dataloader.pt"),
30493038
)
30503039
checkpointer.finalize_checkpoint(checkpoint_path)
3051-
policy.offload_after_refit()
3040+
policy.finish_training()
30523041

30533042
# Logging
30543043
# Log training data (match sync GRPO logging payload for parity)

nemo_rl/models/generation/interfaces.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ class GenerationOutputSpec(TypedDict):
220220

221221

222222
class GenerationInterface(ABC):
223-
"""Abstract base class defining the interface for RL policies."""
223+
"""Abstract base class defining the interface for generation backends."""
224224

225225
@abstractmethod
226226
def init_collective(
@@ -237,10 +237,12 @@ def generate(
237237

238238
@abstractmethod
239239
def prepare_for_generation(self, *args: Any, **kwargs: Any) -> bool:
240+
"""Transition to generation phase. Load weights, allocate KV cache, etc."""
240241
pass
241242

242243
@abstractmethod
243244
def finish_generation(self, *args: Any, **kwargs: Any) -> bool:
245+
"""Transition out of generation phase. Free GPU resources for training."""
244246
pass
245247

246248
@property

nemo_rl/models/policy/interfaces.py

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
from abc import ABC, abstractmethod
15+
from enum import Enum
1516
from typing import Any, Optional, TypedDict
1617

17-
import ray
1818
import torch
1919

2020
from nemo_rl.algorithms.loss.interfaces import LossFunction
@@ -23,6 +23,26 @@
2323
from nemo_rl.utils.timer import Timer
2424

2525

26+
class OffloadMode(Enum):
27+
"""Controls how aggressively to offload during finish_training().
28+
29+
EVAL_ONLY: keep model on GPU in eval mode, optionally offload optimizer
30+
based on worker config (offload_optimizer_for_logprob). Used by
31+
algorithm code before logprob / KV-scale inference.
32+
OPTIMIZER_ONLY: offload optimizer, keep model on GPU in its current mode.
33+
Used by colocated weight synchronizers before weight transfer (model
34+
must stay on GPU for CUDA IPC / HTTP streaming).
35+
FULL: offload everything appropriate for the deployment topology. In
36+
colocated mode this offloads model + optimizer; in non-colocated mode
37+
this sets eval mode, keeps model on GPU, and optionally offloads
38+
optimizer.
39+
"""
40+
41+
EVAL_ONLY = "eval_only"
42+
OPTIMIZER_ONLY = "optimizer_only"
43+
FULL = "full"
44+
45+
2646
class LogprobOutputSpec(TypedDict):
2747
"""logprobs: Tensor of log probabilities."""
2848

@@ -48,8 +68,8 @@ class TopkLogitsOutputSpec(TypedDict):
4868
topk_indices: torch.Tensor
4969

5070

51-
class PolicyInterface(ABC):
52-
"""Abstract base class defining the interface for RL policies."""
71+
class PolicyTrainerInterface(ABC):
72+
"""Abstract base class defining the interface for RL policy training."""
5373

5474
@abstractmethod
5575
def get_logprobs(
@@ -148,10 +168,18 @@ def calibrate_qkv_fp8_scales(
148168

149169
@abstractmethod
150170
def prepare_for_training(self, *args: Any, **kwargs: Any) -> None:
171+
"""Transition to training phase. Load model and optimizer to GPU, set train mode."""
151172
pass
152173

153174
@abstractmethod
154175
def finish_training(self, *args: Any, **kwargs: Any) -> None:
176+
"""Transition out of training phase. Free GPU resources.
177+
178+
Accepts an optional ``offload_mode`` kwarg (OffloadMode enum):
179+
- EVAL_ONLY: model on GPU in eval mode (for logprob inference).
180+
- OPTIMIZER_ONLY: offload optimizer only (for weight staging).
181+
- FULL (default): full offload based on deployment topology.
182+
"""
155183
pass
156184

157185
@abstractmethod
@@ -163,49 +191,5 @@ def shutdown(self) -> bool:
163191
pass
164192

165193

166-
class ColocatablePolicyInterface(PolicyInterface):
167-
@abstractmethod
168-
def init_collective(
169-
self, ip: str, port: int, world_size: int, *, train_world_size: int
170-
) -> list[ray.ObjectRef]:
171-
pass
172-
173-
@abstractmethod
174-
def offload_before_refit(self) -> None:
175-
pass
176-
177-
@abstractmethod
178-
def offload_after_refit(self) -> None:
179-
pass
180-
181-
@abstractmethod
182-
def prepare_refit_info(self) -> Optional[dict[str, Any]]:
183-
pass
184-
185-
@abstractmethod
186-
def stream_weights_via_ipc_zmq(
187-
self, *args: Any, **kwargs: Any
188-
) -> list[ray.ObjectRef]:
189-
pass
190-
191-
def stream_weights_via_http(
192-
self, sglang_url_to_gpu_uuids: dict[str, list[str]]
193-
) -> list[ray.ObjectRef]:
194-
"""Stream model weights to SGLang servers via HTTP API.
195-
196-
Args:
197-
sglang_url_to_gpu_uuids: Dict mapping SGLang server URL to list of GPU UUIDs it uses
198-
"""
199-
raise NotImplementedError(
200-
"stream_weights_via_http is not implemented for this policy worker"
201-
)
202-
203-
@abstractmethod
204-
def broadcast_weights_for_collective(
205-
self, kv_scales: Optional[dict[str, float]] = None
206-
) -> list[ray.ObjectRef]:
207-
pass
208-
209-
@abstractmethod
210-
def prepare_for_lp_inference(self) -> None:
211-
pass
194+
# Backward compatibility
195+
PolicyInterface = PolicyTrainerInterface

0 commit comments

Comments
 (0)