Skip to content

[Single Controller / Async RL] cleanup tracking issue #2625

Description

@yuki-97

Tracking issue for loose ends in the Single Controller / Async RL / Streaming.
Items will be checked off or split into separate issues as they land.

Known cleanup items

Drop after new implementation aligned with old implementation.

  • Async RL
    • Drop nemo_rl/algorithms/async_utils/trajectory_collector.py .
    • Drop old implementation in nemo_rl/algorithms/async_utils/replay_buffer.py.
  • Streaming

Known missing items

✅ Resolved
  • Epoch semantics in the Single Controller train loop: _train_pump was bounded only by max_train_steps, with no max_num_epochs/current_epoch notion of full dataset passes. — ✅ Fixed in feat(algorithms): SingleController streaming train_pump (split-API consumer) #2700 (feat(sc): epoch-bounded rollout passes (max_num_epochs parity), commit 4c2bf5b6c): SC now has max_num_epochs + a _current_epoch counter with epoch-bounded rollout dispatch, surfaced in run()/ping().
  • Add a retry policy to the Single Controller train loop. _train_pump's per-cycle error path (single_controller.py L437-L573) aborts the worker step and re-raises on any mid-cycle failure (microbatch OOM, NaN-guard trip, prepare_logprobs / begin / finish failure), so a transient error kills the run — there is no retry. Flagged by the TODO(sc): retry policy is a follow-up at L441. (Flagged during feat(algorithms): SingleController streaming train_pump (split-API consumer) #2700 review.) — Not planned.
  • Support custom samplers by extracting the TQReplayBuffer implementation. — ✅ Done in feat(sc): train path — StalenessSampler + _train_pump rewrite #3220: PromptGroupSampler protocol + WindowedSampler / WeightFifoSampler / InOrderSampler built-ins in nemo_rl/algorithms/async_utils/staleness_sampler.py, plus CustomSamplerConfig(target="module:ClassName") wired through create_sampler and the AsyncRLConfig.sampler discriminated union.
  • Honor recompute_kv_cache_after_weight_updates in async mode. — ✅ Done in feat(sc): setup + entrypoint #3266: SC's _sync_weights (nemo_rl/algorithms/single_controller.py:615) calls self._gen.invalidate_kv_cache() after every weight sync when AsyncRLConfig.recompute_kv_cache_after_weight_updates=True.
  • Decouple the SC logprob refresh from the advantage config. — ✅ Done in feat(sc): setup + entrypoint #3266: SC's _policy_logprobs_required / _reference_logprobs_required predicates (single_controller.py:104-110) are loss-driven (force_on_policy_ratio, seq_logprob_error_threshold, skip_reference_policy_logprobs_calculation), replacing the old advantage-config gating.
  • Router replay (R3) not supported/validated on the Single Controller (async + TransferQueue) path. — ✅ Done in feat(sc): support router replay with TQ #3378: routed-expert indices are threaded through the async rollout → TQReplayBuffer → training payload, with fail-loud validation when router_replay.enabled=true and routes are missing; adds SC + R3 recipe grpo-qwen3-30ba3b-10n8g-megatron-cp2-r3-async-single-controller.{yaml,sh} + a nightly correctness test. Closes [Single Controller] Router replay (R3) not supported/validated on the async + TransferQueue path #3327.
  • Support multi-reward (GDPO) in AsyncRolloutImpl. Multi-reward aggregation — needed for the GDPO recipe — was dropped when the async rollout impl was introduced in feat: add AsyncRolloutManager for native async per-prompt rollouts #2566; wire it back through the async path for GDPO parity with grpo.

  • Support the greedy option in RolloutManager. Dropped during the streaming rewrite in refactor: unify per-prompt rollout to RolloutManager #2567; needed for greedy validation/eval sampling before the sync path can be fully retired.

  • Fix env observation chat templating at the env interface level. Templating currently leaks across the env boundary and each backend rolls its own; move it into the env interface so observations arrive already templated. Flagged in feat: add AsyncRolloutManager for native async per-prompt rollouts #2566, context feat: Add Tau bench environment #2479.

  • Generation-level backpressure in the Single Controller — cap in-flight generations, not prompt groups. Today _rollout_pump backpressures per prompt group (max_inflight_prompts + the _buffer_capacity semaphore), so a group with one slow generation holds its buffer slot for the whole group → long-tail under-utilization (can't admit the next group even with spare generation capacity). Doing it properly needs the rollout/generation-worker rewrite that streams each generation into the DataPlane as it completes — today generate_and_push pushes a whole group atomically, so SC never sees individual generations finish and a generation-level semaphore has nothing to release against mid-group. Track with that rewrite; until then max_inflight_prompts stays as a conservative bound (under-utilizes on long tails but never over-subscribes DataPlane memory). (Discussed in feat(algorithms): SingleController streaming train_pump (split-API consumer) #2700, thread feat(algorithms): SingleController streaming train_pump (split-API consumer) #2700 (comment), Known Missing Feature in feat(sc): rollout path — TQReplayBuffer, rollout_manager, _rollout_pump #3219)

  • Add an over_sampling_ratio (>1) knob to bound rollout over-sampling. The rollout pipeline can over-sample beyond what the trainer will consume; a configurable ratio lets recipes cap wasted generations while keeping enough headroom for slow tails. (Known Missing Feature in feat(sc): rollout path — TQReplayBuffer, rollout_manager, _rollout_pump #3219)

  • Support multiple mini-steps inside a single RL step. The SC train loop today does exactly one optimizer.step per RL step; PPO-style multi-mini-step updates (iterating minibatches over the same batch of rollouts) need to be plumbed through the train pump. (Known Missing Feature in feat(sc): train path — StalenessSampler + _train_pump rewrite #3220)

  • Add a drain gate to refit. Refit today does not gate in-flight rollouts to fully drain before swapping weights, so straggler generations can still be produced against the pre-refit weights. Needed for correctness on tight staleness bounds. (Known Missing Feature in feat(sc): setup + entrypoint #3266)

  • Support (or clearly reject) non-colocated Megatron generation in the Single Controller. setup.py asserts backend != "megatron" on the non-colocated cluster split, whereas legacy grpo.py supports it via a dedicated inference policy (init_megatron_generation non-colocated branch builds MegatronGeneration(cluster=inference_cluster, ...)). All SC recipes use vLLM generation so this is latent; either mirror grpo's non-colocated Megatron-generation path, or keep the fail-loud guard with a message that names the generation backend rather than reading like it's about Megatron training. (Flagged during feat(sc): setup + entrypoint #3266 review)

  • FP8 KV-cache scale sync not implemented in the Single Controller. grpo's train loop, when the vLLM backend reports requires_kv_scale_sync (FP8 KV cache), computes FP8 QKV scales via policy.calibrate_qkv_fp8_scales(...) and forwards them through sync_weights(..., kv_scales=...) (feature). SC's _sync_weights calls sync_weights() with no kv_scales and never calls calibrate_qkv_fp8_scales, so an FP8-KV-cache rollout under SC keeps stale quantization scales after weight updates (degraded generation, cf. fix: forward calibrated KV cache scales on colocated ZMQ IPC refit path #3226). The synchronizer interfaces already accept kv_scales; only the driver-side compute+forward is missing. (Flagged during feat(sc): setup + entrypoint #3266 review)

  • SC NeMo-Gym non-vLLM guard is unreachable dead code, placed after the expensive build. The if generation_config["backend"] != "vllm": raise NotImplementedError in setup_single_controller (setup.py:380) can't fire through the real entrypoint: sglang+gym trips _should_use_nemo_gym's should_expose_http_server assert and megatron+gym trips _build_generation's ValueError first; the guard only sits after _build_generation/_build_trainer (multi-minute cluster+model build). test_nemo_gym_rejects_non_vllm_backend reaches it only by mocking both earlier gates. Hoist the check to right after use_nemo_gym = _should_use_nemo_gym(...) (setup.py:327) so megatron+gym fails fast with a clear message before any build. (Flagged during feat(sc): NeMo-Gym path #3267 review.)

  • Tighten the Single Controller NeMo-Gym functional-test signal once SC validation is wired. tests/functional/grpo_async_gym_single_controller.sh currently checks only max(data["train/reward"]) > 0, which is a liveness bound; replace it with validation accuracy or a trending mean-reward threshold comparable to grpo_async_gym.sh after validation metrics are available. (Flagged during feat(sc): NeMo-Gym path #3267 review.)

  • Overlap Single Controller NeMo-Gym spinup with deferred vLLM model loading. SC currently completes _build_generation/finish_generation before spinup_nemo_gym_actor, serializing setup; mirror grpo.py by reserving server URLs with defer_model_load=True and running Gym spinup alongside vLLM weight loading (and policy initialization for non-colocated execution). Tracked by the TODO in single_controller_utils/setup.py. (Flagged during feat(sc): NeMo-Gym path #3267 review.)


Above: Jul 28 version.

Add anything else here as it comes up.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions