Repository navigation
feat(vLLM): support direct vLLM TP rollout engine and vLLM native IPC weight sync - #480
Merged
leviking98z-rgb merged 3 commits intoSep 19, 2026
Conversation
cboss6
requested review from
CjhHa1,
Ideny42,
Jayce-Ping,
Zcchill,
celve,
haonan3,
leviking98z-rgb and
zzhuoxin1508
as code owners
September 17, 2026 08:11
2 tasks
leviking98z-rgb
approved these changes
Sep 19, 2026
2 tasks done
CjhHa1
pushed a commit
to CjhHa1/UniRL
that referenced
this pull request
Sep 27, 2026
Tencent-Hunyuan#480 added VLLMRolloutEngine and a vLLM-native path inside IPCWeightSync. Declare the vllm family, accept either IPC method set, and gate the native path's enable_fsdp_offload requirement at recipe time.
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
This PR adds a direct, text-only vLLM rollout engine and a full-weight
synchronization path for colocated FSDP training and vLLM tensor-parallel
inference.
The PR has two first-class deliverables:
spawned vLLM 0.27 runtime, TP placement, text generation, sampled-token
log-probabilities, lifecycle management, and partial sleep/wake.
path built on vLLM's native packed CUDA IPC Weight Transfer Engine.
The implementation is intentionally built around vLLM 0.27's native Weight
Transfer Engine (WTE) instead of introducing another UniRL-owned CUDA IPC
protocol. UniRL supplies the framework-specific scheduling, lazy canonical
weight export, topology validation, rank consensus, publication receipts, and
fail-stop policy. vLLM continues to own:
load_weights()implementation.This work was extracted from
UniRL PR #430.
PR #430 covers the broader Qwen3-MoE train-inference parity experiment; this PR
isolates the reusable direct-vLLM rollout and weight-sync infrastructure.
The final PR does not modify the existing SGLang, TensorWeightSync,
BucketedWeightSender, or vLLM-Omni legacy IPC behavior.
Why these capabilities are delivered together
origin/mainhas vLLM-Omni integrations, but it does not have a directtext-only
VLLMRolloutEngine. Adding only a trainer-side WTE adapter wouldtherefore leave no direct vLLM runtime in UniRL to receive native updates.
Conversely, adding a rollout engine without a post-optimizer weight publication
path would only support inference from the initial checkpoint, not iterative RL
training.
In colocated RL training, the Actor and rollout engine time-share the same
physical GPUs:
vLLM before the next rollout.
Copying an entire model through CPU memory or checkpoint files introduces:
Sending FSDP local shards directly is also incorrect. FSDP and vLLM do not
necessarily shard a parameter in the same way:
The correct boundary is therefore a lazy stream of complete canonical
HuggingFace-format tensors. vLLM's native loader then performs the correct TP
slice and model-specific fusion.
Supported configuration
The currently validated scope is deliberately narrow:
qwen3_moe;The reference recipe is:
Part I: Direct vLLM Rollout Engine
Purpose and boundary
VLLMRolloutEngineimplements UniRL'sBaseRolloutEnginecontract for directtext-only vLLM inference. It is distinct from
VLLMOmniRolloutEngine:vllm.LLM;orchestrator;
implementations.
The direct engine provides:
VLLMEngineConfig;Rollout architecture
flowchart LR subgraph UniRL["UniRL worker process"] Manager["RolloutManager / ARTrainer"] Engine["VLLMRolloutEngine"] Adapter["TextLMAdapter"] State["VLLMConnectionState<br/>IDLE / INFLIGHT / BROKEN"] Pipe["multiprocessing.Pipe"] Manager -->|"generate(sample)"| Engine Engine --> Adapter Engine --> State Engine --> Pipe end subgraph Child["Spawned vLLM runtime"] Runtime["engine_process_main"] LLM["vllm.LLM"] Executor["vLLM multiprocess TP executor"] TP0["TP worker 0"] TPn["TP workers 1..N"] Pipe --> Runtime Runtime --> LLM --> Executor Executor --> TP0 Executor --> TPn end TP0 -->|"plain token IDs, text, log-probs"| Runtime Runtime -->|"versioned response"| Pipe Engine -->|"build_response()"| Adapter Adapter -->|"version-stamped Sample"| ManagerTP placement and process isolation
The new engine opts into UniRL's rollout-TP placement contract through
_accepts_rollout_tp_kwargs = True.Handletherefore:config.tp_size;CUDA_VISIBLE_DEVICEStoken;Only UniRL TP rank 0 creates the child process and communicates with
vllm.LLM; sibling UniRL ranks still participate in FSDP and weight-synccollectives.
flowchart TD Config["VLLMEngineConfig.tp_size = 4"] Handle["UniRL Handle TP layout"] Devices["ordered physical devices<br/>GPU0, GPU1, GPU2, GPU3"] Rank0["UniRL rollout TP rank 0"] Other["UniRL rollout TP ranks 1..3"] Child["one spawned vLLM runtime"] VTP["vLLM TP4 executor"] Config --> Handle --> Devices Devices --> Rank0 --> Child --> VTP Handle --> Other Other -->|"no child process; distributed no-op methods"| OtherSpawning isolates vLLM's global runtime state, CUDA environment, multiprocessing
configuration, and EngineCore process tree from the long-lived UniRL Worker.
Generation path
sequenceDiagram participant Trainer as ARTrainer participant Engine as VLLMRolloutEngine participant Adapter as TextLMAdapter participant Runtime as spawned runtime participant LLM as vllm.LLM Trainer->>Engine: generate(Sample) Engine->>Adapter: build_inputs() Adapter-->>Engine: prompt IDs + sampling payloads Engine->>Engine: truncate to max_prompt_length Engine->>Runtime: Pipe generate request Runtime->>LLM: generate(prompts, SamplingParams) LLM-->>Runtime: RequestOutput list Runtime->>Runtime: extract selected-token processed log-probs Runtime-->>Engine: plain serializable outputs Engine->>Adapter: build_response() Engine->>Engine: stamp committed model version Engine-->>Trainer: generated SampleThe Pipe transports request metadata and generated outputs. It is not the
weight data plane.
Runtime control protocol
Every request carries:
Every response echoes the same identity and carries:
ok;VLLMRolloutEnginepermits only one in-flight request on the synchronous Pipe.The connection state is:
stateDiagram-v2 [*] --> IDLE IDLE --> INFLIGHT: send request INFLIGHT --> IDLE: validated response INFLIGHT --> BROKEN: timeout / EOF / protocol error INFLIGHT --> BROKEN: sleep, wake, or weight-update failure BROKEN --> [*]: terminate process treeAn invalid response ID, command, protocol version, result type, timeout, or
broken Pipe marks the runtime unusable and triggers process-tree cleanup.
Sleep and wake lifecycle
The direct rollout supports vLLM sleep levels and tagged wake-up:
the first rollout;
wake_up(tags=["weights"])restores only the storage required for weightpublication;
wake_up()restores KV-cache resources before generation.flowchart LR Initial["checkpoint-loaded runtime"] Sleep1["bootstrap sleep level 1<br/>preserve weights"] First["first rollout"] Sleep2["steady-state sleep level 2<br/>discard stale weights"] WeightWake["wake weights"] Reload["native WTE reload"] KVWake["wake KV cache"] Next["next rollout"] Initial --> Sleep1 --> First --> Sleep2 --> WeightWake --> Reload --> KVWake --> Next Next --> Sleep2Native WTE control surface
The rollout engine exposes a narrow adapter around vLLM's native lifecycle:
init_native_weight_transfer();start_native_weight_update();update_native_weights();finish_native_weight_update();release_native_ipc();poison().These methods do not implement tensor loading. They validate publication state,
forward control to the spawned runtime, validate the child response, and mark
the connection broken after an unsafe failure.
Rollout-engine files
unirl/rollout/engine/vllm/__init__.pyunirl/rollout/engine/vllm/config.pyunirl/rollout/engine/vllm/engine.pyunirl/rollout/engine/vllm/runtime.pyunirl/rollout/engine/vllm/worker_extension.pyunirl/distributed/group/handle.pyunirl/rollout/engine/base.pyPart II: Native WTE Weight Synchronization
Design principles
Reuse community-owned mechanisms
The data plane is based on vLLM's native:
WeightSource;VLLMWeightSyncClient;IPCTrainerWeightTransferEngine;IPCWeightTransferEngine;load_weights()lifecycle.The design also follows the central idea used by Verl's colocated vLLM sync:
rwith vLLM TP rankron the same GPU;plane;
Keep UniRL policy outside vLLM
UniRL remains responsible for:
Fail closed
The update mutates resident vLLM storage during layerwise loading; it is not an
atomic weight swap and cannot roll back a partially loaded model. Safety is
therefore provided by fail-stop publication:
staging.
High-level architecture
flowchart LR subgraph Trainer["UniRL trainer process group"] AR["ARTrainer"] FSDP["FSDP Actor ranks"] Sync["IPCWeightSync"] Source["VLLMNativeWeightSource"] NativeTrainer["VLLMNativeIPCTrainerEngine"] Client["VLLMNativeWeightSyncClient"] AR -->|"selective offload + sync()"| Sync FSDP -->|"lazy canonical iterator"| Source Sync --> Source Sync --> NativeTrainer Source --> NativeTrainer NativeTrainer --> Client end subgraph Control["Spawned-runtime control plane"] Rollout["VLLMRolloutEngine"] Pipe["versioned synchronous Pipe protocol"] Runtime["engine_process_main"] Client -->|"init / start / update / finish / release"| Rollout Rollout --> Pipe --> Runtime end subgraph VLLM["vLLM 0.27 native weight-transfer stack"] LLM["vllm.LLM"] WorkerWTE["IPCWeightTransferEngine on TP workers"] Consumer["packed IPC consumer"] Loader["layerwise reload + model.load_weights()"] Resident["TP-local resident weights"] Runtime -->|"LLM.init/update/finish_weight_*"| LLM LLM --> WorkerWTE --> Consumer --> Loader --> Resident end NativeTrainer -.->|"merged UUID-to-handle metadata"| WorkerWTE FSDP ==>|"same-GPU CUDA IPC payload"| ConsumerRank and data-plane topology
Every Actor rank materializes the same canonical tensor stream because
DTensor.full_tensor()is a collective. Each rank copies its materializedtensor into a reusable packed buffer on its own GPU. vLLM's trainer engine
all-gathers the IPC handles and rank 0 sends a merged UUID-to-handle map through
the control plane.
Each vLLM TP worker selects the handle matching its physical CUDA UUID.
flowchart TB subgraph FSDP["FSDP4 collective domain"] A0["Actor rank 0<br/>GPU UUID 0"] A1["Actor rank 1<br/>GPU UUID 1"] A2["Actor rank 2<br/>GPU UUID 2"] A3["Actor rank 3<br/>GPU UUID 3"] end Gather["DTensor.full_tensor()<br/>FSDP NCCL all-gather"] A0 --> Gather A1 --> Gather A2 --> Gather A3 --> Gather Gather --> A0 Gather --> A1 Gather --> A2 Gather --> A3 subgraph IPC["Same-GPU CUDA IPC; no Actor-to-vLLM NCCL"] A0 ==>|"UUID 0 handle"| T0["vLLM TP rank 0"] A1 ==>|"UUID 1 handle"| T1["vLLM TP rank 1"] A2 ==>|"UUID 2 handle"| T2["vLLM TP rank 2"] A3 ==>|"UUID 3 handle"| T3["vLLM TP rank 3"] end subgraph Load["vLLM-native TP loading"] T0 --> L0["load_weights: TP slice/fusion"] T1 --> L1["load_weights: TP slice/fusion"] T2 --> L2["load_weights: TP slice/fusion"] T3 --> L3["load_weights: TP slice/fusion"] endLarge-scale cross-GPU communication occurs only in the FSDP
full_tensor()collective. The Actor-to-vLLM boundary is same-GPU CUDA IPC.Engine selection and legacy compatibility
IPCWeightSyncremains the public full-weight IPC handler. It selects itsimplementation from the concrete rollout engine:
flowchart TD Init["IPCWeightSync.__init__"] Name["_receiver_component_name()"] Resolve["_resolve_weight_sync_engine()"] Native["VLLM_NATIVE_WTE"] Legacy["LEGACY_ZMQ"] Init --> Name --> Resolve Resolve -->|"component_name() == vllm"| Native Resolve -->|"all existing non-direct-vLLM cases"| Legacy Native --> NativeSync["_sync_with_vllm_native_engine()"] Legacy --> LegacySync["_sync_with_legacy_zmq()<br/>existing main behavior"]Only the direct
VLLMRolloutEngineselects the native WTE path. ExistingvLLM-Omni and other legacy users continue through the original bucketed ZMQ
implementation. The following files are unchanged from
origin/main:unirl/distributed/weight_sync/transfer/bucketed_transfer.py;unirl/distributed/weight_sync/transfer/sgl_compat.py;unirl/distributed/weight_sync/full/tensor.py;unirl/rollout/engine/vllm_omni/worker/ipc_receive_mixin.py.IPCWeightSyncclass designClass relationships
classDiagram direction LR class FullWeightSync { +_iter_full_tensors() +_iter_buckets() } class IPCWeightSync { -_weight_sync_engine -_gpu_memory_headroom -_control_group -_next_model_version +uses_gpu_streaming() +setup() +sync() +cleanup() +shutdown() -_receiver_component_name() -_resolve_weight_sync_engine() -_dist_ready() -_global_rank -_world_size -_all_gather() -_broadcast_from_rank_zero() -_consensus() -_target_topology() -_run_preflight() -_shape_metadata() -_expected_tensor_metadata() -_poison_rollout() -_sync_with_vllm_native_engine() -_sync_with_legacy_zmq() -_release_resources() } class VLLMNativeWeightSource { +metadata() +__iter__() +actual_metadata } class VLLMNativeIPCTrainerEngine { +initialize() +send_weights() -_send_planned_packed() -_plan_chunks() -_validate_tensor() } class VLLMNativeWeightSyncClient { +init_weight_transfer_engine() +start_weight_update() +update_weights() +finish_weight_update() +release_ipc_imports() } class IPCTrainerWeightTransferEngine { +_all_gather_and_merge_handles() +_do_send() } class VLLMRolloutEngine { +init_native_weight_transfer() +start_native_weight_update() +update_native_weights() +finish_native_weight_update() +release_native_ipc() +poison() } FullWeightSync <|-- IPCWeightSync IPCWeightSync --> VLLMNativeWeightSource IPCWeightSync --> VLLMNativeIPCTrainerEngine IPCWeightSync --> VLLMNativeWeightSyncClient IPCTrainerWeightTransferEngine <|-- VLLMNativeIPCTrainerEngine VLLMNativeWeightSyncClient --> VLLMRolloutEngineMember-function responsibilities
Selection and setup
__init__()next model version.
_receiver_component_name()tensor_weight_sync_targetwhen therollout is a wrapper;
component_name()accessor rather than reading a privateclass field across modules.
_resolve_weight_sync_engine()vllmtoVLLM_NATIVE_WTE;LEGACY_ZMQfor existing non-direct-vLLM behavior.uses_gpu_streaming()ARTrainer;IPCWeightSync, so Tensor/NCCL/LoRA sync paths do notreceive a new RPC or state.
setup()Remotesetup;rollout, so discarded inference weights are reconstructed by sync.
Rank control and failure consensus
_dist_ready()_global_rankand_world_size_all_gather()_broadcast_from_rank_zero()ranks.
_consensus(error, phase)rank has already failed.
Topology and memory preflight
_target_topology()VLLMRolloutEngine;_run_preflight()FSDP/TP world sizes, and exact Actor/worker physical UUID ordering;
materialization.
The memory check is:
The three buffer-sized terms conservatively cover overlapping full-tensor,
trainer packed-buffer, and receiver/loading storage.
largest_layer_sizecovers deferred layerwise processing.
gpu_memory_headroom_mbis an additionalsafety margin for allocator fragmentation, CUDA/NCCL state, and temporary
kernel workspaces. It is a preflight threshold, not a reservation.
Canonical export plan
_shape_metadata()_expected_tensor_metadata()gate_up_projanddown_projexpert tensors;projection key;
models.
The expected plan is intentionally independent from the actual lazy iterator.
If the exporter consistently omits one expert on every rank, comparing Actor
ranks only would not detect the bug; comparing against the independent plan
does.
Publication
sync()_sync_with_vllm_native_engine()VLLMNativeWeightSource,VLLMNativeWeightSyncClient, andVLLMNativeIPCTrainerEngine;finalizes the update, releases IPC imports, validates receipts, and advances
the model version;
_sync_with_legacy_zmq()origin/mainpath for existing rollout engines;_poison_rollout()cleanup(),shutdown(),_release_resources()Internal method dependency graph
flowchart TD Construct["__init__()"] Receiver["_receiver_component_name()"] Resolve["_resolve_weight_sync_engine()"] Policy["uses_gpu_streaming()"] Setup["setup()"] Sync["sync()"] Construct --> Resolve Resolve --> Receiver Construct --> Policy Setup --> Dist["_dist_ready()"] Setup --> Sleep["rollout.set_weight_sync_sleep_level()"] Sync -->|"native"| Native["_sync_with_vllm_native_engine()"] Sync -->|"legacy"| Legacy["_sync_with_legacy_zmq()"] Native --> Expected["_expected_tensor_metadata()"] Expected --> Shape["_shape_metadata()"] Expected --> Manifest["structural_manifest()"] Native --> Target["_target_topology()"] Target --> Consensus["_consensus()"] Target --> Broadcast["_broadcast_from_rank_zero()"] Native --> Preflight["_run_preflight()"] Preflight --> Gather["_all_gather()"] Preflight --> Consensus Native --> Source["VLLMNativeWeightSource"] Native --> Client["VLLMNativeWeightSyncClient"] Native --> Engine["VLLMNativeIPCTrainerEngine"] Engine --> Consensus Native --> Validate["validate_publication_result()"] Native -->|"failure"| Poison["_poison_rollout()"] Cleanup["cleanup() / shutdown()"] --> Release["_release_resources()"]vLLM-native adapter layer
VLLMNativeWeightSourceVLLMNativeWeightSourceimplements vLLM'sWeightSourcecontract:metadata()returns ordered name/dtype/global-shape information withoutmaterializing FSDP full tensors;
__iter__()invokesFullWeightSync._iter_full_tensors()lazily;actual_metadatarecords the exact stream consumed by WTE for post-transfermanifest verification.
The source is re-iterable through an iterator factory, as required by vLLM.
VLLMNativeWeightSyncClientThe native vLLM client protocol assumes four control operations:
VLLMNativeWeightSyncClientmaps those operations onto the spawnedVLLMRolloutEngine.Raw PyTorch IPC reducer objects cannot cross vLLM EngineCore's msgspec channel.
The client therefore follows vLLM's trusted local IPC-client pattern:
ipc_handlesfrom the ordinary update payload;Tensor payload bytes never pass through Pipe, pickle, base64, or CPU memory.
VLLMNativeIPCTrainerEngineThis class subclasses vLLM's native
IPCTrainerWeightTransferEngine. It reusesthe upstream:
_do_send()update format;UniRL overrides packed iteration for one reason: vLLM 0.27's generic packed
producer obtains the next tensor from the iterator before deciding to flush the
current buffer. With FSDP, obtaining the next tensor performs another
full_tensor()collective and can temporarily keep two complete tensors alive.VLLMNativeIPCTrainerEngine._plan_chunks()uses cheap metadata first. Theruntime then materializes exactly the tensors belonging to one planned chunk,
copies them into the reusable buffer, sends the chunk, waits for rank consensus,
and only then consumes the next chunk.
flowchart LR Meta["WeightSource.metadata()<br/>no full_tensor"] --> Plan["_plan_chunks()"] Plan --> C1["chunk 1 names/shapes"] Plan --> C2["chunk 2 names/shapes"] Plan --> CN["chunk N names/shapes"] C1 --> M1["materialize only chunk 1"] M1 --> B["reusable packed CUDA buffer"] B --> S1["_do_send() + consensus"] S1 --> M2["materialize only chunk 2"] M2 --> B B --> S2["_do_send() + consensus"] S2 --> More["..."]This is a bounded wrapper around vLLM native WTE, not a replacement data plane.
End-to-end synchronization sequence
sequenceDiagram autonumber participant Trainer as ARTrainer participant Backend as FSDPBackend participant Sync as IPCWeightSync participant Source as VLLMNativeWeightSource participant WTE as VLLMNativeIPCTrainerEngine participant Client as VLLMNativeWeightSyncClient participant Rollout as VLLMRolloutEngine participant Runtime as spawned vLLM runtime participant LLM as vllm.LLM native WTE participant TP as vLLM TP workers Trainer->>Backend: offload optimizer; clear consumed gradients Trainer->>Rollout: wake_up(tags=["weights"]) Trainer->>Sync: sync() Sync->>Sync: _expected_tensor_metadata() Sync->>Sync: structural_manifest(expected) Sync->>Rollout: read TP fanout and worker CUDA UUIDs Sync->>Sync: _run_preflight() Note over Sync: Verify single node, FSDP4 == TP4,<br/>rank-aligned UUIDs, buffer size, free memory Sync->>Source: construct metadata + lazy iterator factory Sync->>Client: bind rollout, header, cache policy Sync->>WTE: construct packed native trainer engine WTE->>Client: init_weight_transfer_engine(packed=true) Client->>Rollout: init_native_weight_transfer() Rollout->>Runtime: Pipe init_native_weight_transfer Runtime->>LLM: init_weight_transfer_engine() WTE->>Client: start_weight_update() Client->>Rollout: start_native_weight_update(header) Rollout->>Runtime: Pipe start_native_weight_update Runtime->>LLM: start_weight_update() loop each metadata-planned chunk on every Actor rank WTE->>Source: consume next planned tensors Source->>Backend: _iter_full_tensors() Backend->>Backend: DTensor.full_tensor() Source-->>WTE: canonical CUDA tensors WTE->>WTE: validate order/shape/dtype WTE->>WTE: copy into reusable packed CUDA buffer WTE->>WTE: upstream all_gather_and_merge_handles() WTE->>Client: update_weights(merged UUID handles) Client->>Rollout: update_native_weights(encoded handle metadata) Rollout->>Runtime: Pipe update_native_weights Runtime->>LLM: update_weights() LLM->>TP: native IPC consumer selects same-GPU handle TP->>TP: layerwise load_weights / TP slice / fusion WTE->>Sync: _consensus(materialize and transfer) end WTE->>Sync: verify actual Actor manifests Sync->>Sync: all ranks must equal expected manifest WTE->>Client: finish_weight_update(model_version) Client->>Rollout: finish_native_weight_update() Rollout->>Runtime: Pipe finish_native_weight_update Runtime->>LLM: finish_weight_update() Runtime->>TP: collect publication receipts TP-->>Runtime: rank, world size, UUID, manifest, resident count Runtime->>Runtime: validate receipts Runtime->>LLM: update_weight_version() Runtime->>LLM: reset_prefix_cache() Runtime-->>Rollout: committed result Rollout->>Rollout: validate untrusted child result Rollout-->>Client: committed result WTE-->>Sync: finish consensus Sync->>Client: release_ipc_imports() Client->>Rollout: release_native_ipc() Rollout->>Runtime: Pipe release_native_ipc Runtime->>TP: ipc_collect() Sync->>Sync: validate_publication_result() Sync-->>Trainer: sync complete Trainer->>Backend: offload Actor model shards Trainer->>Rollout: wake_up() for KV cache Note over Trainer,TP: Next rollout observes the committed model versionTrainer memory schedule
ARTrainerqueriesIPCWeightSync.uses_gpu_streaming()once after constructingthe remote sync object. Existing sync implementations do not implement this
method and therefore keep their original path.
For direct vLLM native WTE:
flowchart LR Step["optimizer step complete"] OptOffload["offload optimizer<br/>clear gradients"] ActorResident["keep FSDP local parameter shards on GPU"] WeightWake["wake vLLM weights only"] Sync["native lazy IPC sync"] ActorOffload["offload Actor model"] KVWake["wake vLLM KV cache"] Generate["next rollout"] Step --> OptOffload --> ActorResident --> WeightWake --> Sync Sync --> ActorOffload --> KVWake --> GenerateThe Actor parameter shards must remain resident until synchronization finishes,
because each lazy
full_tensor()is an FSDP collective over those shards.Optimizer state and consumed gradients are not needed by export and are removed
first to create GPU headroom.
Native GPU-streaming sync requires:
enable_fsdp_offload=true;wake_up()supports weight-only tags;rollout_anchor_devicemode.Publication identity and validation
Each publication header binds:
The structural manifest hashes ordered:
It deliberately does not hash tensor payload bytes, avoiding a GPU-to-CPU copy
solely for checksums.
Validation occurs at multiple boundaries:
ParamMeta.manifest.
expected fingerprint, TP rank, and CUDA UUID.
boundary.
IPCWeightSyncbroadcasts and validates the final result on every Actorrank.
Publication state and failure behavior
stateDiagram-v2 [*] --> Idle Idle --> Initialized: init native WTE Initialized --> Started: start_weight_update Started --> Loading: first packed chunk Loading --> Loading: next chunk + rank consensus Loading --> Finalizing: source manifest verified Finalizing --> Committed: TP receipts + version + cache reset Committed --> Released: IPC imports collected Released --> Idle Initialized --> Poisoned: init/start failure Started --> Poisoned: materialization/transfer failure Loading --> Poisoned: schema/receiver/consensus failure Finalizing --> Poisoned: receipt/finalize/cache failure Committed --> Poisoned: release/final validation failure Poisoned --> BrokenRuntime BrokenRuntime --> [*]There is no fallback transition from
Poisonedto legacy ZMQ. A failed nativepublication may have already mutated some resident storage, so reusing that
runtime would be unsafe.
Rank-consensus failure gate
FSDP materialization requires every Actor rank to enter collectives in exactly
the same order. A receiver failure on one rank must therefore be reported
before another rank advances to the next tensor.
flowchart TD Materialize["materialize planned chunk on all Actor ranks"] LocalResult["capture rank-local exception or success"] Gloo["Gloo all_gather_object"] AnyFailure{"any rank failed?"} Raise["raise the same phase/rank error everywhere"] Transfer["send merged IPC handles"] TransferConsensus["Gloo transfer consensus"] Next["consume next planned chunk"] Materialize --> LocalResult --> Gloo --> AnyFailure AnyFailure -->|"yes"| Raise AnyFailure -->|"no"| Transfer --> TransferConsensus TransferConsensus -->|"all succeeded"| Next TransferConsensus -->|"any failed"| RaiseThe control group is Gloo rather than the FSDP NCCL group so error propagation
does not depend on entering the next weight-materialization collective.
Relationship to vLLM
This PR does not duplicate vLLM worker-side weight loading.
Reused directly from vLLM 0.27:
WeightTransferConfig(backend="ipc");IPCTrainerWeightTransferEngine;start_weight_update()/update_weights()/finish_weight_update();load_weights();UniRL's
VLLMNativeIPCTrainerEngineis a bounded trainer-side wrapper. Itscustomization is limited to:
References:
Relationship to Verl
The shared design with Verl's colocated vLLM synchronization is:
rand vLLM TP rankrexchange same-GPU IPC handles;UniRL-specific policy includes:
Verl supports a broader set of update modes. This PR intentionally fails closed
outside its validated direct-vLLM Qwen3-MoE full-weight configuration.
File-level implementation map
Part I: direct rollout files
unirl/rollout/engine/vllm/__init__.pyunirl/rollout/engine/vllm/config.pyunirl/rollout/engine/vllm/engine.pyweight-update client methods, connection poisoning, and process-tree
cleanup.
unirl/rollout/engine/vllm/runtime.pyvllm.LLM, enables native IPC WTE, and serves the synchronous Pipeprotocol.
unirl/rollout/engine/vllm/worker_extension.pyimport cleanup.
Part II: native weight-sync files
unirl/distributed/weight_sync/transfer/vllm_native_engine.pyunirl/distributed/weight_sync/transfer/vllm_native_protocol.pyunirl/distributed/weight_sync/full/ipc.pyIntegration and reference files
examples/ar/qwen3_moe_grpo_30b_a3b_gsm8k_fsdp_vllm_tp4.yamlunirl/distributed/group/handle.pyvisible-device map already used for TP rollout engines.
unirl/rollout/engine/base.pycomponent_name()while retaining_component_nameas the internalclass field.
unirl/train/backend/base_backend.pypreserving the old no-argument behavior.
unirl/trainer/ar.pysync behavior when
uses_gpu_streaming()is absent or false.Compatibility and non-goals
Preserved behavior
TensorWeightSyncandNCCLWeightSyncare unchanged.IPCWeightSyncremains the public configuration target.Non-goals
Validation
Completed checks:
DirectVLLM*, standaloneNativeIPCWeightSync,_direct_send_large_weight, andsend_bucketpaths;vLLM-Omni receiver files match
origin/main.A two-rollout Qwen3-30B-A3B FSDP4/vLLM-TP4 run of the native WTE path completed
successfully with exit code 0 before the final naming and dead-code-only cleanup.
A final post-squash TP4 rerun is pending GPU availability.
Reviewer focus
Please focus review on:
the next FSDP collective;
load_weights();at the correct process boundaries;
partially updated weights;
and failure paths;
validated hardware.
Related Issue
Test Plan
Compatibility / Risk
Reviewer Notes
Checklist