feat(rollout): add TP/EP/PP parallel strategies on SGLang rollout side#191
Draft
NancyFyong wants to merge 13 commits into
Draft
feat(rollout): add TP/EP/PP parallel strategies on SGLang rollout side#191NancyFyong wants to merge 13 commits into
NancyFyong wants to merge 13 commits into
Conversation
Add rollout-end TP/EP/PP parallelism in colocate mode (sleep/wake
alternation between train and rollout on the same GPU), mirroring
the approach used in verl and slime.
Core changes:
- group/handle.py: extend rank layout from (dp, sp) to (dp, pp, tp)
with EP support. Inject per-rank tp_rank/tp_size/tp_device_ids and
pp_rank/pp_size/ep_rank/ep_size into SGLangRolloutEngine init kwargs.
Add tp_size/pp_size/ep_size/tp_zero_workers properties.
- group/placement.py: propagate tp/pp/ep sizes through HandleRef markers.
- rollout/engine/sglang/config.py: add pp_size/ep_size/dp_size/
enable_expert_parallel to SGLangEngineConfig; thread server_args
runtime_overrides for base_gpu_id/gpu_id_step.
- rollout/engine/sglang/engine.py: tp_rank>0 workers become no-op shells
(no SGLang boot, no port, no weight_sync). tp_rank==0 with tp_size>1
overrides CUDA_VISIBLE_DEVICES to the full TP group and sets
base_gpu_id=0, gpu_id_step=1. All rollout verbs guarded by _is_tp_zero.
- weight_sync/full/nccl.py: rank_offset = i*tp + 1; PP>1 raises
NotImplementedError (fail-closed, future work).
- weight_sync/full/tensor.py: tp_rank==0 ships [payload]*tp_size;
tp_rank>0 drives all-gather but does not push.
- trainer/async_ar.py, trainer/diffusion.py: driver targets
tp_zero_workers; pass tp_size/pp_size through.
Tests (scripts/tests/, pytest):
- 60 unit tests covering rank layout math, server_intent, engine shell
behavior, weight sync (NCCL + Tensor), EP/PP (incl. PP fail-closed).
- 2 GPU-gated E2E recipes (skipped without GPUs).
- E2E recipes verified end-to-end on tiny-random/qwen3.5-moe:
* examples/ar/qwen3_moe_tiny_sglang_tp2.yaml (2 GPUs, tp=2)
* examples/ar/qwen3_moe_tiny_sglang_ep8.yaml (8 GPUs, tp=2 ep=2,
4 engines x 2 GPU)
Design doc: docs/rollout_tp_ep_pp_design.md
Note: PP>1 weight sync per-stage rank_offset mapping is fail-closed
(NotImplementedError) and left as future work.
Reorganize the rollout TP/EP/PP tests into a rollout/ subdirectory with _on_cpu / _gpu name suffixes (mirroring verl-omni/tests), so the pure-Python and GPU-gated tiers are distinguishable at a glance and share one conftest. - Extract the duplicated transport fakes (Ray handle, FSDP backend, NCCL sync builder) into conftest so each module imports them instead of redefining. - Centralize the SGLang E2E force-exit into conftest.sglang_e2e_teardown with a docstring explaining the root cause (in-process SGLang SIGQUIT) and the long-term fix (Ray-actor isolation); replace the per-test _RESULT globals with a local passed flag. - Delete the placeholder assert 0 == 0 shell test (its contract is covered by the E2E _is_tp_zero assertions) and drop unused imports / dead locals. Verified: 57 passed, 6 deselected (GPU-gated) in 8.66s.
Satisfy the Lint CI (pre-commit run --all-files): ruff import-sort/format, remove unused imports (torch/Any) and the unused ep local, fix a mis-indented dict in handle.py, and trim trailing whitespace in the design doc.
- LocalLoraWeightSync/RemoteLoraWeightSync: extract on all ranks, push only from tp_rank==0 / rank-0; SGLang fans the adapter to its TP workers. - IPCWeightSync: per-stage receiver thread + sender pump covered by CPU tests. - _is_sglang_rollout_role: switch from string name match to the _accepts_rollout_tp_kwargs class attribute so per-rank TP kwargs injection survives rename/subclassing and never fires on sibling (weight sync / reward / algorithm) roles. - SGLangRolloutEngine: document the DP_SCATTER collect filter contract and record ep_rank/ep_size for completeness (EP is sharded inside SGLang). - Add CPU unit tests for NCCL/Tensor/IPC/LoRA tp_rank guards and the rank-zero broadcast vs non-rank-zero drain behavior.
Three fixes for SGLang rollout engine TP>1 in colocate (same-actor) mode:
1. engine.py: stop overriding CUDA_VISIBLE_DEVICES for TP>1; pass
base_gpu_id via runtime_overrides so SGLang locates its GPUs without
breaking the train side's single-GPU view.
2. backends/{native,http}.py boot(): temporarily set
CUDA_VISIBLE_DEVICES to all TP GPUs before spawning the SGLang
server subprocess, then restore the parent's Ray-assigned view so
FSDP training is unaffected.
3. backends/{native,http}.py set_lora(): use plain pickle.dumps instead
of ForkingPickler (MultiprocessingSerializer.serialize) to avoid
resource_sharer fd handles that race across TP>1 scheduler
subprocesses. SafeUnpickler (SGLang's deserializer) handles plain
pickle CPU tensors correctly.
Verified: Qwen3.5-35B-A3B TP=8 EP=8 LoRA GRPO on 8×H200, baseline eval
generates 960 requests at ~20k token/s with CUDA graph enabled.
890e64c to
2a365dc
Compare
- Replace CUDA_VISIBLE_DEVICES assertions with _tp_size/_backend checks (TP>1 fix moved CVD handling into backend boot, not engine __init__) - Fix test_tp4_ep4_e2e fixture from tp8_gate to tp4_gate (4 GPUs needed) - Remove unused devices parameter from _boot_and_generate
…/UniRL into feat/rollout-tp-ep-pp
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.
Summary
Add tensor-parallel (TP) distribution on the SGLang rollout engine in colocate mode, with EP sharding inside SGLang and PP rank layout reserved for future work. Larger rollout models (e.g. Qwen3.5-MoE 35B) need >1 GPU per engine; before this change UniRL could only do
dp_size = world_size, one GPU per engine.The train side stays flat (FSDP). Each SGLang engine now spans
tp_sizeGPUs:tp_rank==0owns the server,tp_rank>0workers are no-op shells that participate in training but hold no SGLang process. EP is forwarded toServerArgs; PP>1 populatesRankInfobut weight sync is fail-closed (NotImplementedError).Key changes:
Rank layout (
handle.py):_build_rank_infoslays out(dp, pp, tp)with TP rank fastest.Handle.__init__injectstp_rank/tp_size/tp_device_ids/pp_rank/pp_size/ep_sizeinto the engine constructor.sp_size>1combined withtp/pp>1is rejected;tp=pp=ep=1reproduces the flat layout.SGLang engine (
sglang/{config,engine}.py):tp_rank>0constructs as a no-op shell (no server, no ports). All rollout verbs early-return ontp_rank>0.tp_rank==0withtp_size>1passesbase_gpu_idviaruntime_overridesinstead of overridingCUDA_VISIBLE_DEVICESglobally.Weight sync — all four handlers guard on
tp_rank:NCCLWeightSync:rank_offset = i*tp + 1,world_size = num_engines*tp + 1; PP>1 raises.TensorWeightSync: ships[payload]*tp_sizeontp_rank==0;tp_rank>0drains the FSDP all-gather in lockstep.IPCWeightSync: receiver started once per stage;tp_rank>0shells drop at the collect filter.LocalLoraWeightSync/RemoteLoraWeightSync: extract on all ranks, push only fromtp_rank==0/rank-0.TP>1 colocate crash fix (
sglang/backends/{native,http}.py): SGLang Engine spawn temporarily exposes all TP GPUs viaCUDA_VISIBLE_DEVICESthen restores Ray's CVD. LoRA tensor serialization switched fromForkingPicklerto plain pickle to avoid fd handle races across TP subprocesses.Related Issue
N/A
Test Plan
SKIP=no-commit-to-branch pre-commit run --all-files --show-diff-on-failurepytest scripts/tests/rollout/ -k on_cpu— CPU unit tests (rank layout, shell construction, weight sync tp_rank guards)python scripts/check_recipe_targets.py—_target_paths resolvetiny-random/qwen3.5-moe):tp=2: boot + generate + sleep/wake — EXIT=0tp=2weight-sync round-trip — tokens returned post-synctp=2 ep=2(4 engines × 2 GPU): full GRPO loop —reward/losslogged, EXIT=0num_heads=2(cannot dividetp_size=4)Compatibility / Risk
SGLangEngineConfiggainspp_size/ep_size/dp_size/enable_expert_parallel, all default to1/None.tp_size=1path is asserted identical to pre-change layout.NCCLWeightSync.connectnow takestp_size;pp_size>1raises. All handlers'tp_size=1path is unchanged.tp_rank>0workers must construct without booting SGLang or reserving ports. A regression would spawn duplicate multi-GPU servers and OOM. Covered bytest_tp_engine_shell_on_cpu.pyand E2E boot test.Reviewer Notes
feat/veomni-backend-ep-pr) is training-backend EP (VeOmni FSDP2), distinct from this PR's rollout-side (SGLang) EP.handle.py(rank layout + kwargs injection) →weight_sync/*.sync(tp_rank guards) →sglang/engine.py(tp_rank>0 no-op shell)Checklist