feat(sglang): engine fault tolerance for rollouts - #3187
Open
xiuhu17 wants to merge 8 commits into
Open
Conversation
This was referenced Jul 14, 2026
xiuhu17
force-pushed
the
zhw/sglang_fault_tolerance
branch
from
July 14, 2026 02:29
c20a694 to
3f62683
Compare
xiuhu17
marked this pull request as ready for review
July 14, 2026 05:05
6 tasks
Kh4L
added a commit
to Kh4L/NemoRL
that referenced
this pull request
Jul 25, 2026
Rebased onto the SGLang refit stack (NVIDIA-NeMo#3187 -> NVIDIA-NeMo#3190 -> NVIDIA-NeMo#3188 -> NVIDIA-NeMo#3189). The stack now owns the collective-skip and the real Megatron->SGLang refit, so the earlier NCCL-skip / NRL_SGLANG_SKIP_REFIT / set_rollout_num_gpus_per_engine scaffolding is dropped. This keeps only the async-GRPO replay-path enablement: - lift the async_grpo_train backend gate to include sglang - expose_http_server + _spinup_nemo_gym wiring for the sglang branch - SGLangGeneration.cfg parity property + rollouts.py sglang context_length - picklability (__getstate__/__setstate__) dropping the aiohttp client, async loop, and (new on the stack) health monitor for the collector actor Signed-off-by: Serge Panev <spanev@nvidia.com>
yuki-97
force-pushed
the
zhw/sglang_fault_tolerance
branch
from
July 27, 2026 05:12
3f62683 to
6760b97
Compare
yuki-97
reviewed
Jul 27, 2026
| self.engine_gpu_offsets, | ||
| ) | ||
|
|
||
| def clear_updatable_num_new_engines(self): |
Contributor
There was a problem hiding this comment.
where will this function be called?
Contributor
Author
There was a problem hiding this comment.
| def check_weights(self, action: str): | ||
| return self._make_request("weights_checker", {"action": action}) | ||
|
|
||
| def _simulate_crash(self): |
Contributor
There was a problem hiding this comment.
where will this function be called?
Contributor
Author
There was a problem hiding this comment.
It is for testing purpose and used in test: test_crashed_engine_is_detected_recovered_and_generation_survives
| ray.get(init_handles) | ||
|
|
||
| # Serializes weight refits against engine recovery across processes. | ||
| self.rollout_engine_lock = Lock.options(num_cpus=1, num_gpus=0).remote() |
Contributor
There was a problem hiding this comment.
looks it only be used in fault tolerance case, so shall we also init with None and do real init under if sglang_cfg["sglang_cfg"].get("use_fault_tolerance"): like self._health_monitor?
Kh4L
added a commit
to Kh4L/NemoRL
that referenced
this pull request
Jul 27, 2026
Rebased onto the SGLang refit stack (NVIDIA-NeMo#3187 -> NVIDIA-NeMo#3190 -> NVIDIA-NeMo#3188 -> NVIDIA-NeMo#3189). The stack now owns the collective-skip and the real Megatron->SGLang refit, so the earlier NCCL-skip / NRL_SGLANG_SKIP_REFIT / set_rollout_num_gpus_per_engine scaffolding is dropped. This keeps only the async-GRPO replay-path enablement: - lift the async_grpo_train backend gate to include sglang - expose_http_server + _spinup_nemo_gym wiring for the sglang branch - SGLangGeneration.cfg parity property + rollouts.py sglang context_length - picklability (__getstate__/__setstate__) dropping the aiohttp client, async loop, and (new on the stack) health monitor for the collector actor Signed-off-by: Serge Panev <spanev@nvidia.com>
Kh4L
added a commit
to Kh4L/NemoRL
that referenced
this pull request
Jul 28, 2026
Rebased onto the SGLang refit stack (NVIDIA-NeMo#3187 -> NVIDIA-NeMo#3190 -> NVIDIA-NeMo#3188 -> NVIDIA-NeMo#3189). The stack now owns the collective-skip and the real Megatron->SGLang refit, so the earlier NCCL-skip / NRL_SGLANG_SKIP_REFIT / set_rollout_num_gpus_per_engine scaffolding is dropped. This keeps only the async-GRPO replay-path enablement: - lift the async_grpo_train backend gate to include sglang - expose_http_server + _spinup_nemo_gym wiring for the sglang branch - SGLangGeneration.cfg parity property + rollouts.py sglang context_length - picklability (__getstate__/__setstate__) dropping the aiohttp client, async loop, and (new on the stack) health monitor for the collector actor Signed-off-by: Serge Panev <spanev@nvidia.com>
Kh4L
added a commit
to Kh4L/NemoRL
that referenced
this pull request
Jul 28, 2026
Rebased onto the SGLang refit stack (NVIDIA-NeMo#3187 -> NVIDIA-NeMo#3190 -> NVIDIA-NeMo#3188 -> NVIDIA-NeMo#3189). The stack now owns the collective-skip and the real Megatron->SGLang refit, so the earlier NCCL-skip / NRL_SGLANG_SKIP_REFIT / set_rollout_num_gpus_per_engine scaffolding is dropped. This keeps only the async-GRPO replay-path enablement: - lift the async_grpo_train backend gate to include sglang - expose_http_server + _spinup_nemo_gym wiring for the sglang branch - SGLangGeneration.cfg parity property + rollouts.py sglang context_length - picklability (__getstate__/__setstate__) dropping the aiohttp client, async loop, and (new on the stack) health monitor for the collector actor Signed-off-by: Serge Panev <spanev@nvidia.com>
xiuhu17
force-pushed
the
zhw/sglang_fault_tolerance
branch
3 times, most recently
from
August 1, 2026 22:32
72b52c2 to
fafd4d4
Compare
|
@xiuhu17 could you fix the DCO issue? thanks |
…atch The SGLang refit needs an engine-side finalize after the last weight bucket lands: quantized weights carry derived state (swizzled blockscales aliased over their own storage, `g1/g2_alphas` products) that a plain `load_weights` leaves stale, and re-running the finalize per bucket is not idempotent. No tagged sglang release exposes a hook for it. sgl-project/sglang's `sglang-miles` branch does, as `begin_weight_update` / `end_weight_update` routed through `weight_updater` + `model_runner`. Pin that branch instead of PyPI 0.5.12.post1, and drop the one on-disk patch it makes redundant: - `_patch_sglang_custom_all_reduce_v2_tms_cudagraph` backported the "skip custom all-reduce v2 graph capture under torch_memory_saver" fix. The branch has it natively via `_graph_mode_allowed`. Worse than redundant: `set_cuda_graph_capture` no longer exists there, so the patch's anchor lookup raised and aborted the whole patch chain. - `_patch_sglang_file_replacements`, its only caller now gone. Kept: `_patch_sglang_safe_unpickler` (the branch's allowlist still has no `nemo_rl.` prefix, and the policy venv cannot import sglang to reuse its `tensor_bucket`), the imbalance-check env override, and the two megatron `hook_mode` patches. The branch is based on v0.5.16, which costs a port: - `sglang-kernel` 0.4.2.post2 -> 0.4.5, per the branch's own pyproject. - `disable_piecewise_cuda_graph` was replaced by a per-phase enum. All three shipped sglang configs set it and it is passed straight into `ServerArgs(**kwargs)`, so on v0.5.16 every run would die with `TypeError: unexpected keyword argument` at sglang_worker.py:91, before `launch_server` is spawned. Translated to `cuda_graph_backend_prefill: breakable` -- the non-piecewise graph mode -- to preserve why the flag existed (piecewise prefill hit an illegal memory access on torch 2.10). - `cuda_graph_bs` / `cuda_graph_max_bs` split per phase. No config set them, so they were dormant rather than broken; renamed to the `_decode`/`_prefill` pairs. All 39 kwargs `sglang_worker` passes resolve against the pinned rev, and the vendored `FlattenedTensorBucket` / `FlattenedTensorMetadata` are functionally identical to the branch's, so the IPC payload is unchanged. `uv.lock` is deliberately not hand-edited: a git-sourced sglang changes the package entry, its transitive deps and its hashes, and a plausible-but-wrong lock is worse than an obviously stale one. Run `uv lock` in the project container. Whether `breakable` is the right translation (v0.5.16 may have fixed the crash, in which case the key can go) needs one smoke test. Signed-off-by: zhihaow6 <zhihaow6@illinois.edu>
xiuhu17
force-pushed
the
zhw/sglang_fault_tolerance
branch
from
August 4, 2026 01:26
fafd4d4 to
348b555
Compare
Replace the HTTP weight-streaming refit with two first-class SGLang refit paths driven from grpo via _refit_sglang_dispatch: - colocated (weight_transfer_mode: ipc): Ray CUDA-IPC buckets via send_hf_buckets_via_ipc_actor_impl + SGLangColocatedWeightSynchronizer, generalized Gloo gather topology (connect_colocate_topology) supporting FSDP and Megatron layouts - disaggregate (weight_transfer_mode: broadcast): trainer-rank-0 NCCL weight-update group (side-by-side init_process_group, connect/ disconnect_rollout_engines_from_distributed) broadcasting AutoBridge- restored HF buckets; engines join via init_weights_update_group - MegatronSGLangHfWeightIterator: AutoBridge export walk bucketed by post-transformation size - engine-side worker endpoints (update_weights_from_distributed, pause/continue_generation, post_process_weights, weight versioning) - NCCL_CUMEM_ENABLE=0 alignment between trainer and sglang scheduler - drop stream_weights_via_http / set_rollout_num_gpus_per_engine Quantization-related parameters (target_precision, sglang_quantization_cfg) are inert bf16 plumbing here; the mxfp8 implementation lands in a follow-up PR. Signed-off-by: zhihaow6 <zhihaow6@illinois.edu>
`_refit_sglang_dispatch` and `SGLangColocatedWeightSynchronizer._refit_colocated`
run in the driver process but imported `megatron_policy_worker` /
`dtensor_policy_worker_v2` to reach `refit_sglang_{colocated,distributed}`.
Those modules import `megatron.bridge` and `nemo_automodel` at module scope,
and the driver environment is synced without the `mcore` / `automodel` extras,
which is why worker imports otherwise go through `PY_EXECUTABLES`. Every SGLang
refit therefore failed on its first attempt with
`ModuleNotFoundError: No module named 'megatron'`, reproduced on a two node run.
Move the four drivers to `nemo_rl/weight_sync/`, next to the synchronizer that
calls them, and repoint both dispatch sites. The function bodies and names are
unchanged, and the dispatch keeps its lazy `as _backend` import, so each call
site changes only the module it imports from. The drivers call `policy` and
`policy_generation` facade methods plus `ray`, and already imported
`nemo_rl.models.policy.utils` lazily, so they need no extra.
The test parses the sources instead of importing them, so it also fails in an
environment that provides a backend, such as the Megatron unit test shards.
Signed-off-by: Serge Panev <spanev@nvidia.com>
When the trainer's default process group is eager-initialized with a bound device id (device_id= passed to torch.distributed.init_process_group), torch's _new_process_group_helper silently sets Options.split_from to the default group's NCCL communicator, and the first collective on the side weight-update group then issues ncclCommSplit. The split is collective over the parent (trainer) communicator, but only trainer rank 0 is a member of the refit group, so rank 0 blocks forever in the split bootstrap all-gather (commGetSplitInfo) while the remaining trainer ranks hit the watchdog. Clear default_pg.bound_device_id for the duration of the _new_process_group_helper call (restored in finally) so the helper's split predicate sees a lazily-initialized default group and never selects the split path. Current main initializes the Megatron default PG without device_id, so this is latent hardening: any trainer base that binds the default group would otherwise trip it silently. Verified: torch 2.10/2.11 have the identical split predicate and a pure field-assignment setter; gloo interop + split-predicate regression tests pass locally. Co-authored-by: Serge Panev <3193578+Kh4L@users.noreply.github.com> Signed-off-by: zhihaow6 <zhihaow6@illinois.edu> (cherry picked from commit e27d3f1)
…nizer The SGLang refit had two entry points: `grpo._refit_sglang_dispatch`, which the live GRPO/PPO/distillation loops used, and `SGLangColocatedWeightSynchronizer`, which nothing constructed. The offload and prepare/restore phases were duplicated across both, plus a third copy in `refit_policy_generation` itself. Collapse them onto the synchronizer: - `sglang_weight_synchronizer` keeps only the GPU phase transitions and delegates the refit itself (recover, connect, pause, KV invalidation, bucket transfer, post-process, continue) to the backend driver modules. `SGLangColocatedWeightSynchronizer` offloads the policy around the transfer; the new `SGLangDisaggregatedWeightSynchronizer` does not — the trainer keeps its own GPUs, and `prepare_for_training` onloads unconditionally on both the Megatron and DTensor workers, so there is no offload state to restore. `prepare_for_generation` still runs on both paths: `SGLangGeneration` gates it on `needs_offload` internally. - `create_weight_synchronizer` now returns the disaggregated synchronizer for non-colocated SGLang instead of raising. SGLang establishes its own weight-update group on the first refit, so it needs neither cluster handle. - `refit_policy_generation` hands SGLang to the factory and returns; the remaining branches are vLLM IPC/NCCL and the Megatron reshard. This covers Megatron colocated + disaggregated and DTensor colocated; DTensor disaggregated still raises from `dtensor_sglang_refit`. Also drop `http_weight_synchronizer`. It was dead (never constructed) and broken: `policy.stream_weights_via_http` was removed in 7b0f0bd along with `stream_weights_via_http_impl`. `weight_update_actor` now streams over the production Ray CUDA-IPC path (`connect_colocate_topology` + `send_hf_buckets_via_ipc_actor_impl`) rather than the deleted HTTP impl, so the real weight-update tests exercise the transport they claim to. The two `excluded_unit_tests.sh` deselects for the `test_refit_policy_generation_sglang_*` tests were stale — those tests went away in 7b0f0bd. Signed-off-by: zhihaow6 <zhihaow6@illinois.edu>
The sglang extra tracks the sglang-miles branch, which requires transformers==5.12.1, kernels>=0.14.1 and flashinfer-python 0.6.14. Raising those forced three follow-on changes: - flashinfer-cubin/flashinfer-jit-cache lose their pins: cubin has no 0.6.14 release and jit-cache is only published on the flashinfer index, so both are left to resolve transitively. - llguidance drops its upper bound and xgrammar becomes a floor. vllm needs llguidance>=1.3 and sglang-miles needs >=1.7.6; the two are mutually exclusive extras, so no single pin serves both. xgrammar keeps the 0.1.33 floor for GHSA-7rgv-gqhr-fxg3 while allowing sglang's 0.2.1. - the outlines override is dropped, since it only existed to pull in Python 3.13 support that the current resolution already has. The deepseek_v3 tokenizer-blocklist patch asserted transformers < 5.12 on the assumption that the upstream fix would land there. It has not: MODELS_WITH_INCORRECT_HUB_TOKENIZER_CLASS and TOKENIZER_MAPPING_NAMES both still carry deepseek_v3 in 5.12.1, so the patch stays load-bearing and the assert moves to < 5.13. Signed-off-by: zhihaow6 <zhihaow6@illinois.edu>
The existing sglang recipes start at 1n8g, which needs a full node. These two run the same Qwen2.5-Math-1.5B GRPO workload on four GPUs, keeping every sglang setting identical to the 2n8g reference (tp_size 1, mem_fraction_static 0.6, ipc weight transfer, breakable prefill graph) so the only variable between them is the training backend. Measured on 4xB200: megatron averages 20.6s/step over steps 2-30, inside the 25s ceiling the suite already asserts for the larger sglang recipes. Signed-off-by: zhihaow6 <zhihaow6@illinois.edu>
A hung or dead SGLang engine currently wedges the whole run: the refit's `ray.get` on its actor never returns, and the router keeps sending `/generate` to it. Add `RolloutHealthMonitor`, a daemon thread that polls `/health_generate` on every node-0 engine and, on failure, kills and restarts the actor. The refit picks the survivors up through the engine registry this PR's base already exposes (`get_updatable_engines_and_lock`), so a recovered engine is reconnected on the next weight update. The monitor must not run while the engines are offloaded: `/health_generate` always executes a real one-token generation (the `SGLANG_ENABLE_HEALTH_ENDPOINT_GENERATION=false` bypass only covers `/health`), so probing a released engine reports a false failure and kills a live actor. It is therefore wired to the generation lifecycle — resumed by `prepare_for_generation` once the KV cache is back, paused by `finish_generation` and for the duration of a refit. `pause()` blocks on an in-flight probe rather than only setting a flag, so a probe cannot overlap `release_memory_occupation`. `_kill_engine` bounds its graceful-shutdown `ray.get` and kills the actor even when that shutdown fails, which is the exact case the monitor exists to handle. Signed-off-by: zhihaow6 <zhihaow6@illinois.edu>
xiuhu17
force-pushed
the
zhw/sglang_fault_tolerance
branch
from
August 10, 2026 05:31
348b555 to
f563fa9
Compare
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
RolloutHealthMonitor: a daemon thread that health-checks each SGLang engine and restarts hung or dead actors during rollout, gated bypolicy.generation.sglang_cfg.use_fault_tolerance(off by default).fault_tolerance.py: monitor lifecycle (start/stop/pause/resume),health_generateprobing, engine kill/restart bookkeepingSGLangGeneration: engine recovery (_recover,recover_updatable_engines,get_updatable_engines_and_lock),num_new_enginestracking, monitor wiringray_utils.Lock: cooperative Ray lock serializing weight refits against engine recoverySGLangGenerationWorker._simulate_crash: test-only crash injectionFt design is adapted from miles
Stack
Stacked on #2997 (reserved-band port allocation) — review only the last commit (
feat(sglang): engine fault tolerance for rollouts); earlier commits belong to #2997. Follow-ups: #3190 (megatron refit) → #3188 (mxfp8) → #3189 (comm-split fix).Split out of #3091.