Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/guides/async-grpo.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ loss_fn:
use_importance_sampling_correction: true
```

3. **Disable colocated inference** (required for async mode):
3. **Disable colocated inference** (required for async mode with the vLLM backend; the Megatron backend supports colocated async — see `examples/configs/recipes/llm/grpo-nanov3-30BA3B-4n4g-megatron_async_colocated.yaml`):
```yaml
policy:
generation:
Expand Down Expand Up @@ -189,7 +189,7 @@ If no `replay_buffer.pt` file is found in the latest checkpoint directory, train

3. **Resource Allocation**: Ensure sufficient GPU memory for both the training and generation clusters

4. **In-Flight Weight Updates**: Enable `in_flight_weight_updates: true` when using `async_engine: true` for updating the weights of vLLM engine during generation. This prevents stalling training pipeline until longest generation finishes and provides significant performance benefits.
4. **In-Flight Weight Updates**: Enable `in_flight_weight_updates: true` to update engine weights during generation; with vLLM this requires `async_engine: true`, while the Megatron backend is always async-engine. This prevents stalling the training pipeline until the longest generation finishes and provides significant performance benefits.

5. **Recompute KV Cache After Weight Updates**: A user can choose whether to invalidate and recompute KV caches after weight updates by setting the `recompute_kv_cache_after_weight_updates` configuration. This is applicable to async GRPO and independent of in-flight updates.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
defaults: ../../grpo_math_1B.yaml
# Async colocated GRPO with Megatron (Nemotron-3-Nano-30B-A3B): training and
# generation share GPUs/workers; the engine sleeps across training steps and
# serves the shared weights (resharding into a dedicated inference layout when
# one is configured). Off-policy data via the replay buffer.
grpo:
num_prompts_per_step: 2
num_generations_per_prompt: 8
async_grpo:
enabled: true
max_trajectory_age_steps: 4 # weight versions a rollout may span
in_flight_weight_updates: true
# For AREAL-style KV invalidation, add recompute_kv_cache_after_weight_updates: true.
Comment thread
yuki-97 marked this conversation as resolved.
loss_fn:
use_importance_sampling_correction: true # required for off-policy replay data
checkpointing:
enabled: false
checkpoint_dir: results/grpo-nanov3-30BA3B-4n4g-megatron_async_colocated
save_period: 100
policy:
model_name: nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-Base-BF16
tokenizer:
name: nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16
train_global_batch_size: 16
train_micro_batch_size: 1
logprob_batch_size: 1
max_total_sequence_length: 2048
megatron_cfg:
enabled: true
bias_activation_fusion: false
tensor_model_parallel_size: 2
expert_model_parallel_size: 8
sequence_parallel: true
dtensor_cfg:
enabled: false
sequence_packing:
enabled: false
generation:
backend: megatron
logger:
log_dir: logs/grpo-nanov3-30BA3B-4n4g-megatron_async_colocated
wandb_enabled: true
tensorboard_enabled: true
wandb:
project: nemo-rl
name: grpo-nanov3-30BA3B-4n4g-megatron_async_colocated
cluster:
gpus_per_node: 4
num_nodes: 4
segment_size: 2
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defaults: ../../grpo_math_1B.yaml
grpo:
num_prompts_per_step: 2
num_generations_per_prompt: 8
loss_fn:
use_importance_sampling_correction: true # matches the async sibling so the pair isolates the loop
checkpointing:
enabled: false
checkpoint_dir: results/grpo-nanov3-30BA3B-4n4g-megatron_sync_colocated
save_period: 100
policy:
model_name: nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-Base-BF16
tokenizer:
name: nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16
train_global_batch_size: 16
train_micro_batch_size: 1
logprob_batch_size: 1
max_total_sequence_length: 2048
megatron_cfg:
enabled: true
bias_activation_fusion: false
tensor_model_parallel_size: 2
expert_model_parallel_size: 8
sequence_parallel: true
dtensor_cfg:
enabled: false
sequence_packing:
enabled: false
generation:
backend: megatron
logger:
log_dir: logs/grpo-nanov3-30BA3B-4n4g-megatron_sync_colocated
wandb_enabled: true
tensorboard_enabled: true
wandb:
project: nemo-rl
name: grpo-nanov3-30BA3B-4n4g-megatron_sync_colocated
cluster:
gpus_per_node: 4
num_nodes: 4
segment_size: 2
148 changes: 98 additions & 50 deletions nemo_rl/algorithms/grpo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2782,10 +2782,6 @@ def grpo_train(

kv_scales_cache = None # Cache reused for computed kv scales

NEED_REFIT = not (
isinstance(policy_generation, MegatronGeneration)
and master_config.policy["generation"]["colocated"]["enabled"]
)
assert policy_generation is not None

# Check if we need to sync KV cache scales
Expand Down Expand Up @@ -2827,7 +2823,7 @@ def grpo_train(
print("\n🔍 Running initial validation...", flush=True)
memory_tracker.snapshot_start_of_stage("Initial validation", dir())

if NEED_REFIT and POLICY_GENERATION_STALE:
if POLICY_GENERATION_STALE:
refit_policy_generation(
policy,
policy_generation,
Expand Down Expand Up @@ -2949,7 +2945,7 @@ def grpo_train(
flush=True,
)
with timer.time("prepare_for_generation/total"):
if NEED_REFIT and POLICY_GENERATION_STALE:
if POLICY_GENERATION_STALE:
# Compute KV scales if needed for FP8 quantization
if sync_kv_scales and kv_scales_cache is None:
print("▶ Computing KV cache scales...", flush=True)
Expand Down Expand Up @@ -3448,7 +3444,7 @@ def grpo_train(
# Run validation if it's a validation step or last step with val_at_end
if should_run_validation:
memory_tracker.snapshot_start_of_stage("Validation", dir())
if NEED_REFIT and POLICY_GENERATION_STALE:
if POLICY_GENERATION_STALE:
refit_metrics = refit_policy_generation(
policy,
policy_generation,
Expand Down Expand Up @@ -4225,10 +4221,6 @@ def async_grpo_train(
fit_last_save_time=True,
)
timeout.start_iterations()
NEED_REFIT = not (
isinstance(policy_generation, MegatronGeneration)
and master_config.policy["generation"]["colocated"]["enabled"]
)
assert policy_generation is not None

# Training state
Expand All @@ -4244,13 +4236,14 @@ def async_grpo_train(
colocated_inference = master_config.policy["generation"]["colocated"]["enabled"]
stop_at_validation_threshold = master_config.grpo.stop_at_validation_threshold
stop_at_validation_metric = master_config.grpo.stop_at_validation_metric

assert (not colocated_inference) or (
isinstance(policy_generation, MegatronGeneration)
), "Colocated async GRPO is only supported for the Megatron generation backend."

# Initialize advantage estimator
adv_estimator = _create_advantage_estimator(master_config)

assert not colocated_inference, (
"Colocated inference is not supported for async GRPO. Please use non-colocated inference."
)

# Calculate minimum buffer size from training requirements
# In per-prompt buffer mode, one buffer entry is 1 prompt * num_generations_per_prompt
num_prompts_per_step = master_config.grpo.num_prompts_per_step
Expand Down Expand Up @@ -4383,7 +4376,7 @@ def async_grpo_train(
)

print("⏳ Preparing policy generation for training...", flush=True)
if NEED_REFIT and POLICY_GENERATION_STALE:
if POLICY_GENERATION_STALE:
print("🔄 Refitting policy generation with actual model weights...", flush=True)
try:
refit_policy_generation(
Expand Down Expand Up @@ -4439,7 +4432,9 @@ def async_grpo_train(
processor=processor,
)
initial_val_metrics = val_metrics
policy_generation.finish_generation()
# A colocated engine keeps serving between phases (preserves its
# KV/prefix cache); the backend makes that call, not the loop.
policy_generation.finish_generation(release_gpu=False)
logger.log_metrics(val_metrics, step, prefix="validation")
logger.log_metrics(validation_timings, step, prefix="timing/validation")
if master_config.grpo.debug_payload_metrics:
Expand Down Expand Up @@ -4825,6 +4820,14 @@ def async_grpo_train(
)
train_data.to("cpu")

generation_logger_metrics = None
if policy_generation.blocks_training():
Comment thread
yuki-97 marked this conversation as resolved.
print("⏸️ Pausing colocated engine + collector for training...")
with timer.time("exposed_generation"):
ray.get(trajectory_collector.prepare_for_refit.remote())
generation_logger_metrics = policy_generation.get_logger_metrics()
policy_generation.finish_generation(release_gpu=True)

# Training phase (same as sync version)
skip_prev_logprobs, skip_reference_logprobs = (
_resolve_logprob_skip_flags(master_config)
Expand Down Expand Up @@ -4956,9 +4959,48 @@ def async_grpo_train(
timer=timer,
)

is_last_step = step + 1 == master_config.grpo.max_num_steps
should_save_by_step = (
is_last_step
or (step + 1) % master_config.checkpointing["save_period"] == 0
or (ft_save_period is not None and (step + 1) % ft_save_period == 0)
)
# Checked pre-validation so the wake-deferral below can see it.
# A crossing during refit/validation is caught by the lookahead in check_save.
should_save_by_timeout = timeout.check_save()
will_save_checkpoint = master_config.checkpointing["enabled"] and (
should_save_by_step or should_save_by_timeout
)
# An early stop (known only after validation) also saves.
saving_this_step = will_save_checkpoint
# Save-bound colocated steps leave the engine asleep through save with no transfer.
defer_wake_for_save = (
policy_generation.blocks_training()
and will_save_checkpoint
and policy_generation.wake_carries_weight_updates()
)

print("🔄 Synchronizing policy weights to trajectory collector…")
generation_logger_metrics = None
if NEED_REFIT:
if defer_wake_for_save:
# Wake-deferral (checkpoint scheduling, which the backend
# cannot see): the engine is about to be saved, so leave it
# asleep; just drop training-only buffers and version-stamp
# the weights. The post-save block wakes it and resumes
# collection.
print("⏸️ Keeping colocated engine asleep for checkpointing...")
# Seed the category with 0.0 (no refit wake happens on
# save-bound steps) so efficiency summaries, which skip
# missing keys, stay comparable across modes.
with timer.time("idle/refit_bubble"):
pass
with timer.time("offload_before_refit"):
policy.offload_before_refit()
POLICY_GENERATION_STALE = False
weight_version += 1
ray.get(
trajectory_collector.set_weight_version.remote(weight_version)
)
else:
timer.start("idle/refit_bubble")

# Measure pending-generation wait as exposed_generation time
Expand All @@ -4968,7 +5010,8 @@ def async_grpo_train(

# Collect generation logger metrics for performance reporting
# inflight batch sizes and num pending samples are collected from each worker
if policy_generation is not None:
# (colocated collects them before the engine sleeps for training).
if generation_logger_metrics is None:
generation_logger_metrics = (
policy_generation.get_logger_metrics()
)
Expand Down Expand Up @@ -5000,7 +5043,6 @@ def async_grpo_train(

# Validation
val_metrics, validation_timings = None, None
is_last_step = step + 1 == master_config.grpo.max_num_steps
should_run_validation = (
val_period > 0
and (step + 1) >= val_start_at
Expand All @@ -5025,15 +5067,9 @@ def async_grpo_train(
# Run validation if it's a validation step or last step with val_at_end
if should_run_validation:
with timer.time("idle/validation"):
if NEED_REFIT and POLICY_GENERATION_STALE:
refit_metrics = refit_policy_generation(
policy,
policy_generation,
colocated_inference,
)
POLICY_GENERATION_STALE = False
else:
policy_generation.prepare_for_generation()
# No-op on an already-running engine;
# wakes the colocated engine when it stayed asleep for a save-bound step.
policy_generation.prepare_for_generation()
val_metrics, validation_timings = validate(
policy_generation,
val_dataloader,
Expand All @@ -5044,7 +5080,22 @@ def async_grpo_train(
logger=logger,
processor=processor,
)
policy_generation.finish_generation()
# An early stop triggers a save; must note before engine wake/resume.
early_stop_message = _validation_early_stop_message(
val_metrics,
stop_at_validation_threshold,
stop_at_validation_metric,
)
saving_this_step = will_save_checkpoint or (
master_config.checkpointing["enabled"]
and early_stop_message is not None
)
# Save-bound steps need the GPUs for checkpointing,
# so the engine must stand down; otherwise a colocated
# engine keeps serving (backend's call).
policy_generation.finish_generation(
release_gpu=saving_this_step
)
logger.log_metrics(
validation_timings, step + 1, prefix="timing/validation"
)
Expand All @@ -5059,11 +5110,6 @@ def async_grpo_train(
step + 1,
prefix="validation",
)
early_stop_message = _validation_early_stop_message(
val_metrics,
stop_at_validation_threshold,
stop_at_validation_metric,
)
if early_stop_message is not None:
# Exit at the end of this step, after checkpointing.
print(early_stop_message, flush=True)
Expand Down Expand Up @@ -5157,20 +5203,7 @@ def async_grpo_train(
consumed_samples += master_config.grpo.num_prompts_per_step
timeout.mark_iteration()

# +1 because step is 0-indexed
should_save_by_step = (
is_last_step
# Early stop saves the final state like a last step.
or early_stop_message is not None
or (step + 1) % master_config.checkpointing["save_period"] == 0
or (ft_save_period is not None and (step + 1) % ft_save_period == 0)
)
# Check if timeout-based checkpointing is enabled in config.
should_save_by_timeout = timeout.check_save()

if master_config.checkpointing["enabled"] and (
should_save_by_step or should_save_by_timeout
):
if saving_this_step:
grpo_save_state.current_step = step + 1
grpo_save_state.total_valid_tokens = total_valid_tokens
if val_metrics is not None:
Expand Down Expand Up @@ -5266,6 +5299,21 @@ def async_grpo_train(
checkpointer, last_checkpoint_step=step + 1
)

# On save-bound steps, engine stayed asleep after training;
# wake it unless the loop exits right below (last step, timeout, early stop),
# where a wake would only feed the teardown.
# The intervening logging runs with the collector paused either way.
if defer_wake_for_save and not (
is_last_step
or should_save_by_timeout
or early_stop_message is not None
):
# The save onloaded model+optimizer;
# generation windows must start from the offloaded state.
policy.offload_after_refit()
policy_generation.prepare_for_generation()
ray.get(trajectory_collector.resume_after_refit.remote())

# Logging
# Log training data (match sync GRPO logging payload for parity).
# NeMo Gym responses can be very large and expensive to log; when
Expand Down
Loading
Loading