Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1dec574
feat(sc): save native TQ state in checkpoints
macandro96 Aug 2, 2026
03fba29
fix(sc): harden TQ checkpoint lifecycle
macandro96 Aug 2, 2026
1477a15
fix(sc): harden TQ checkpoint I/O
macandro96 Aug 3, 2026
e31f42c
feat(sc): recover replay buffer from native TQ checkpoints
macandro96 Aug 3, 2026
9eb253a
fix(sc): address TQ recovery review feedback
macandro96 Aug 14, 2026
c781d74
fix: lint issues
macandro96 Aug 14, 2026
83eafa3
fix(tests): import DATA_PLANE_CHECKPOINT_SCHEMA_VERSION from data_plane
macandro96 Aug 14, 2026
da8b0a9
fix: lint issues
macandro96 Aug 14, 2026
0b4cecc
fix(tests): update expected save_state and actor_args for new SC fields
macandro96 Aug 14, 2026
608e9bd
fix(tests): wire DataPlaneCheckpointBarrier into _train_pump_controller
macandro96 Aug 15, 2026
98a86fa
chore: merge main into SC TQ recovery
macandro96 Aug 26, 2026
9eda7b2
fix(sc): restore gated sampler checkpoint state
macandro96 Aug 26, 2026
9ff964b
test(sc): align checkpoint tests with TQ recovery
macandro96 Aug 26, 2026
4e5147c
test(sc): declare custom sampler checkpoint capability
macandro96 Aug 26, 2026
a5e9651
fix(sc): address TQ recovery review feedback
macandro96 Aug 26, 2026
1c33a9a
fix: lint issues
macandro96 Aug 26, 2026
d98004e
fix(sc): align recovery tests with native TQ checkpoints
macandro96 Aug 27, 2026
fb53e36
test(sc): add checkpoint recovery race coverage
macandro96 Aug 27, 2026
e34bb7f
feat(sc): recover unfinished rollouts from checkpoints
macandro96 Aug 27, 2026
9557cf5
chore: merge main into unfinished rollout recovery
macandro96 Aug 27, 2026
45246cc
test(sc): stabilize checkpoint recovery coverage
macandro96 Aug 27, 2026
7d1cac5
revert: remove MOPD recipe test environment override
macandro96 Aug 27, 2026
fcf1604
fix(sc): rehydrate unfinished prompts deterministically
macandro96 Aug 27, 2026
cb412ad
refactor(sc): simplify prompt recovery fingerprint
macandro96 Aug 27, 2026
3656fde
fix(sc): stabilize recovered prompt fingerprints
macandro96 Aug 27, 2026
fd1e424
refactor(sc): remove prompt payload fingerprints
macandro96 Aug 27, 2026
6e5aedb
fix(sc): prevent recovery deadlocks and ledger leaks
macandro96 Aug 27, 2026
df31dec
refactor(sc): initialize rollout recovery schema at v1
macandro96 Aug 28, 2026
c3b8624
test(sc): cover unfinished recovery edge cases
macandro96 Aug 28, 2026
bca9a72
test(sc): add rollout recovery copyright header
macandro96 Aug 28, 2026
47dc148
fix(sc): harden rollout recovery checkpointing
macandro96 Aug 28, 2026
2dd8212
test(sc): initialize recovery fixture barrier
macandro96 Aug 28, 2026
54d9e82
fix: lint issues
macandro96 Aug 28, 2026
6c7150b
chore: merge main into SC TQ recovery
macandro96 Aug 28, 2026
2017b60
fix(sc): repair checkpointing CI fixtures
macandro96 Aug 28, 2026
a4a2a18
chore: merge main into SC TQ recovery
macandro96 Aug 29, 2026
ee210a0
chore: merge latest main into SC TQ recovery
macandro96 Aug 29, 2026
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: 4 additions & 0 deletions examples/configs/grpo_math_1B.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,10 @@ data_plane:
storage_capacity: 1000000 # max samples retained per partition
num_storage_units: 2 # storage shards
claim_meta_poll_interval_s: 0.5 # blocking-claim poll cadence
# SingleController only: save native TQ state. Required when trainer
# checkpointing is enabled with a replay-checkpoint-capable sampler;
# supported samplers restore from metadata-only replay indexes.
checkpointing_enabled: false
global_segment_size: 549755813888 # 512 GiB — used when backend == "mooncake_cpu"
local_buffer_size: 68719476736 # 64 GiB — used when backend == "mooncake_cpu"
# observability: # NotRequired
Expand Down
2 changes: 2 additions & 0 deletions examples/configs/grpo_math_1B_megatron_single_controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ logger:

data_plane:
enabled: true
# Required shadow snapshot: a save failure aborts checkpoint finalization.
checkpointing_enabled: true
Comment thread
terrykong marked this conversation as resolved.
Resolved

cluster:
gpus_per_node: 2
Expand Down
446 changes: 296 additions & 150 deletions nemo_rl/algorithms/async_utils/replay_buffer.py

Large diffs are not rendered by default.

62 changes: 52 additions & 10 deletions nemo_rl/algorithms/async_utils/staleness_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from typing import (
Annotated,
Callable,
ClassVar,
Literal,
Optional,
Protocol,
Expand Down Expand Up @@ -109,6 +110,11 @@ def is_on_policy(self) -> bool:
"""True when the policy admits zero staleness (sync mode)."""
...

@property
def supports_buffer_checkpoint(self) -> bool:
"""Whether completed buffered groups can be restored safely."""
...

def required_buffer_capacity(self, groups_per_step: int) -> Optional[int]:
"""Buffer-capacity the policy needs, or ``None`` if unconstrained."""
...
Expand Down Expand Up @@ -196,6 +202,10 @@ def should_abort_inflight(
def is_on_policy(self) -> bool:
return self._eviction_window() == 0

@property
def supports_buffer_checkpoint(self) -> bool:
return False

def required_buffer_capacity(self, groups_per_step: int) -> Optional[int]:
return None

Expand Down Expand Up @@ -266,6 +276,11 @@ def __init__(
def _eviction_window(self) -> int:
return self.max_staleness_versions

@property
def supports_buffer_checkpoint(self) -> bool:
# Ungated restored groups are ordinary in-window candidates.
return True

def should_abort_inflight(
self,
*,
Expand Down Expand Up @@ -443,6 +458,7 @@ async def evict(self, *, current_train_weight: int) -> int:


class WindowedSamplerConfig(BaseModel, extra="allow"):
supports_buffer_checkpoint: ClassVar[bool] = True
Comment thread
macandro96 marked this conversation as resolved.
Resolved
name: Literal["windowed"] = "windowed"
# Max weight-version gap a selected group may have from the trainer.
max_staleness_versions: NonNegativeInt = 1
Expand All @@ -451,18 +467,22 @@ class WindowedSamplerConfig(BaseModel, extra="allow"):


class WeightFifoSamplerConfig(BaseModel, extra="allow"):
supports_buffer_checkpoint: ClassVar[bool] = False
name: Literal["weight_fifo"] = "weight_fifo"
# Lookahead + selectable weight window, in trainer versions.
max_staleness_versions: NonNegativeInt = 1


class InOrderSamplerConfig(BaseModel, extra="allow"):
supports_buffer_checkpoint: ClassVar[bool] = False
name: Literal["in_order"] = "in_order"
# How far generation may run ahead of the trainer, in dispatch batches.
max_lookahead_versions: NonNegativeInt = 1


class CustomSamplerConfig(BaseModel, extra="allow"):
# A custom implementation's capability is known only after construction.
supports_buffer_checkpoint: ClassVar[Optional[bool]] = None
name: Literal["custom"] = "custom"
# "module:ClassName" of a PromptGroupSampler defined outside this repo.
# Extra keys are forwarded to the constructor (after ``buffer``).
Expand Down Expand Up @@ -505,20 +525,30 @@ def create_sampler(
buffer: TQReplayBuffer,
cfg: SamplerConfig,
) -> PromptGroupSampler:
"""Build a sampler from its config (or import one by FQN)."""
"""Build a sampler from its config (or import one by FQN).

Args:
buffer: Shared TQReplayBuffer holding the candidate slots.
cfg: Discriminated sampler config selecting the policy.
"""
sampler: PromptGroupSampler
if isinstance(cfg, WindowedSamplerConfig):
return WindowedSampler(
sampler = WindowedSampler(
buffer,
max_staleness_versions=cfg.max_staleness_versions,
sample_freshest_first=cfg.sample_freshest_first,
)
if isinstance(cfg, WeightFifoSamplerConfig):
return WeightFifoSampler(
buffer, max_staleness_versions=cfg.max_staleness_versions
elif isinstance(cfg, WeightFifoSamplerConfig):
sampler = WeightFifoSampler(
buffer,
max_staleness_versions=cfg.max_staleness_versions,
)
if isinstance(cfg, InOrderSamplerConfig):
return InOrderSampler(buffer, max_lookahead_versions=cfg.max_lookahead_versions)
if isinstance(cfg, CustomSamplerConfig):
elif isinstance(cfg, InOrderSamplerConfig):
sampler = InOrderSampler(
buffer,
max_lookahead_versions=cfg.max_lookahead_versions,
)
elif isinstance(cfg, CustomSamplerConfig):
module_name, sep, class_name = cfg.target.partition(":")
if not sep:
raise ValueError(
Expand All @@ -532,5 +562,17 @@ def create_sampler(
f"interface (needs admit/select/evict/should_abort_inflight, "
f"set_dispatch_index, is_on_policy, required_buffer_capacity)"
)
return sampler
raise ValueError(f"unknown sampler config {type(cfg).__name__}")
else:
raise ValueError(f"unknown sampler config {type(cfg).__name__}")

expected_capability = cfg.supports_buffer_checkpoint
Comment thread
macandro96 marked this conversation as resolved.
Resolved
if (
expected_capability is not None
and sampler.supports_buffer_checkpoint != expected_capability
):
raise RuntimeError(
f"{type(cfg).__name__}.supports_buffer_checkpoint="
f"{expected_capability} disagrees with {type(sampler).__name__}."
f"supports_buffer_checkpoint={sampler.supports_buffer_checkpoint}"
)
return sampler
4 changes: 4 additions & 0 deletions nemo_rl/algorithms/grpo.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ class GRPOSaveState:
total_steps: int
total_valid_tokens: int # Track total number of non-padding tokens during training
val_reward: float # May be removed when no validation metrics are available
# SC may advance the policy version independently from the optimizer-step
# counter. None preserves compatibility with checkpoints predating it.
trainer_version: Optional[int] = None
# SingleController only: name of the sampler that wrote the replay buffer,
# used to gate the SC buffer restore. None on checkpoints from the other
# algorithms and from SC runs that predate this field.
Expand All @@ -329,6 +332,7 @@ def _initial_grpo_save_state() -> GRPOSaveState:
total_steps=0,
total_valid_tokens=0,
val_reward=-99999999.0,
trainer_version=None,
sampler_name=None,
)

Expand Down
Loading
Loading