Skip to content

[Training][Rollout] Add direct NCCL delta weight updates - #392

Draft
ShuoleiWang wants to merge 1 commit into
vllm-project:mainfrom
ShuoleiWang:feat/direct-dwu-pr1
Draft

[Training][Rollout] Add direct NCCL delta weight updates#392
ShuoleiWang wants to merge 1 commit into
vllm-project:mainfrom
ShuoleiWang:feat/direct-dwu-pr1

Conversation

@ShuoleiWang

Copy link
Copy Markdown
Contributor

Summary

This PR adds an initial direct delta-weight-update (DWU) path from a non-colocated Megatron trainer to VIME-launched vLLM rollout engines.

Instead of broadcasting the full actor checkpoint after every training step, the new path:

  • sends a mandatory dense BF16 seed for the first committed version;
  • sends absolute BF16 replacement values with flattened int32 checkpoint indices for later sparse updates;
  • transports metadata through Ray/HTTP and tensor payloads through NCCL;
  • applies updates through vLLM's native model.load_weights() mappings; and
  • publishes the new weight version only after the complete update succeeds.

The PR also consolidates the control plane used by the existing full-weight NCCL and CUDA IPC paths into a shared WeightUpdateCoordinator, and adds argument validation, English and Chinese documentation, and protocol/lifecycle tests.

No vLLM source files are modified by this PR.

Motivation

Weight synchronization is on the repeated critical path in disaggregated RL training. A full sync always transfers the complete model, even when only a small fraction of BF16 values changed after the optimizer step.

In the measured Qwen3-30B-A3B run below, a sparse step changed 4.50% of the checkpoint elements. The direct delta path produced an 8.24 GB logical NCCL tensor payload instead of the 61.06 GB dense BF16 payload, a reduction of 86.5%. A no-change step produced no tensor payload.

The implementation deliberately keeps checkpoint-to-runtime mapping inside vLLM. VIME produces patches in Hugging Face checkpoint coordinates, while the model's native loader remains responsible for tensor-parallel slicing, packed QKV and gate/up projections, and model-specific name mapping.

Design

Megatron TP/EP parameters
        |
        | gather + canonical HF export
        v
CheckpointDeltaSource
        |
        | v1: dense BF16 seed
        | vN: checkpoint-local positions + absolute BF16 values
        v
Ray/HTTP metadata + NCCL tensor payloads
        |
        v
VimeDeltaNCCLWeightTransferEngine
        |
        v
CheckpointWeightPatch
        |
        v
native model.load_weights()

Source-side encoding

CheckpointDeltaSource stores one committed CPU snapshot for each deterministic HF export bucket.

For every update it:

  1. validates the base and target versions;
  2. flattens the current HF bucket and validates its tensor layout;
  3. emits a dense seed when no committed snapshot exists;
  4. otherwise performs a BF16 bitwise comparison against the committed snapshot;
  5. emits changed checkpoint-local positions and absolute replacement values;
  6. emits a final manifest after all data chunks; and
  7. advances the snapshot only from the coordinator's commit phase.

An update with no changed values sends only the final manifest and advances the logical version without moving tensor data.

Receiver-side application

The VIME-owned vLLM weight-transfer backend validates the session's base and target versions, sequence order, encoding, and final-manifest lifecycle. For data chunks, it drains the NCCL payload before consulting worker-local version/sequence state so a stale worker does not strand healthy peers in the same collective.

Received metadata and tensors are converted into CheckpointWeightPatch objects. Dense seeds use vLLM's layerwise reload lifecycle. Sparse patches modify initialized runtime storage without rerunning dense post-load processing.

Patch payloads contain absolute values rather than arithmetic deltas. The versioned session protocol still rejects replay, mixed versions, and out-of-order chunks; a failed session is not retried in place.

Shared update lifecycle

WeightUpdateCoordinator provides one control plane for the existing full paths and the new delta path:

pause generation
    -> flush caches
    -> start update session
    -> transfer target weights
    -> optional draft transfer
    -> finish update session
    -> commit source state
    -> publish committed version
    -> resume generation

Intermediate chunks carry a candidate version but do not publish it. The trainer-side version and rollout-engine version marker advance only after the complete transfer and source commit succeed. Rank-zero control-plane failures are broadcast to the other trainer ranks; transport callbacks remain collective operations and are required to surface failures collectively.

GPU writes are not transactionally rollback-safe. Recovery is fail-stop: do not serve or retry an affected session in place; restart the task/affected rollout fleet and establish a new dense seed.

Effect on existing full-weight modes

This PR also changes the lifecycle implementation used by existing full-weight NCCL and CUDA IPC synchronization:

  • version publication is deferred until the complete update succeeds;
  • the distributed and tensor adapters share the same pause/commit/resume order;
  • candidate versions carried by intermediate RPCs are no longer treated as committed versions; and
  • set_weight_version publishes the server-side tag when available. For non-delta modes, an older vLLM that returns 404/405 falls back to the local marker and logs one warning.

The non-colocated full NCCL path was exercised by the H20 baseline below. Colocated CUDA IPC behavior is covered by the CPU adapter/lifecycle tests but was not included in this H20 A/B.

Scope

Validated in this PR:

  • Megatron training backend;
  • non-colocated, VIME-launched rollout engines;
  • NCCL weight transport;
  • BF16, non-quantized rollout weights;
  • Megatron PP=1 and VPP=1;
  • vLLM PP=1 and DP=1, with TP supported;
  • version 0 startup followed by a mandatory dense seed; and
  • Qwen3-30B-A3B with a TP4 vLLM rollout (no rollout-side EP filtering).

Rejected by argument validation:

  • colocated direct delta;
  • disk delta transport;
  • quantized rollout weights;
  • speculative decoding or MTP draft synchronization;
  • rollout offload;
  • fully asynchronous rollout; and
  • fault-tolerant rollout-worker replacement.

Not claimed by the current validation evidence: multimodal models, rollout-side expert parallelism/EP filtering, and multi-model deployments with multiple independently configured rollout models.

The source remains wire-sparse rather than end-to-end sparse: every sync still performs a full canonical-HF export and compares against a CPU snapshot. The receiver may also construct full checkpoint-shaped staging tensors while applying a sparse patch.

vLLM dependency

This PR requires a vLLM build containing the checkpoint-coordinate patch API introduced by:

vllm-project/vllm#50723

The H20 testbed used vLLM 0.26. Its checkpoint_weight_patch.py implementation was verified byte-identical to the file at vLLM commit:

fd07acd5b596c11f949fa71b5f0ee926b9e6bf17

The patch and layerwise-reload APIs are checked when the Direct-DWU engine is constructed. Default full-weight mode does not import the delta receiver and does not require the checkpoint patch API, although it does use the shared coordinator introduced by this PR.

Direct-DWU also requires the vLLM server's /update_weight_version endpoint to publish the committed version. Unlike the patch and reload helpers, this capability is exercised at the final publish step; a missing endpoint is fatal for delta mode. Non-delta modes retain the 404/405 fallback described above.

H20 validation

The A/B used Qwen3-30B-A3B in BF16 on two nodes:

  • trainer: 8 x H20, Megatron TP4 / EP8 / ETP1;
  • rollout: 4 x H20, vLLM TP4; and
  • weight transport: NCCL over eth0, with IB disabled.

The timings below use the outer Timer update_weights wall time. Payload sizes are logical tensor payloads, not NIC counters; they exclude HTTP metadata and NCCL protocol overhead.

The easiest way to read the result is that the full path always moves the whole model, while the delta path still inspects the whole model but moves only the changed values.

First sync (one-time setup)

  • Full sent 61.06 GB and took 23.2 s.
  • Delta also sent 61.06 GB, but additionally created its CPU snapshot, and took 44.8 s.

Later sync with no BF16 weight change

  • Full would still send 61.06 GB. Its three post-initial measurements averaged 20.47 s (20.3, 20.6, and 20.5 s).
  • Delta sent no tensor payload and took 8.6 s.

Later sync with 4.50% of BF16 elements changed

  • Full would still send 61.06 GB, with the same 20.47 s post-initial reference.
  • Delta sent 8.24 GB of positions and replacement values and took 19.4 s.

The main result is therefore straightforward: for the measured sparse update, the tensor payload fell by 86.5% (61.06 GB to 8.24 GB), but end-to-end sync time only fell by about 5% (20.47 s to 19.4 s). The producer still performs a full HF export and diff before sending the sparse payload. The 8.6 s no-change result also shows substantial fixed work even when tensor payload is zero; this experiment did not profile the individual components separately.

Full and delta were separate runs, not paired measurements of the same optimizer step. Their training trajectories diverged because vLLM sampling is not deterministic across independent runs, so training-curve agreement is not used as a correctness claim.

Correctness was checked separately with the real checkpoint patch implementation on vLLM TP4. Sparse and native dense loading produced bit-identical runtime tensors for 14 representative checkpoint tensors spanning fused MoE experts, router weights, packed QKV, embeddings, norms, and lm_head; reverting the perturbations also restored the two greedy test outputs exactly.

The delta run successfully committed the dense seed, no-change update, and nonzero sparse update and completed all 3/3 update-and-rollout cycles without protocol errors. It later failed in the unrelated final asynchronous actor-checkpoint save. The full baseline disabled final checkpoint saving outside the measured update boundary and completed successfully. Testbed-only changes also relaxed the local NumPy-version guard and pinned NCCL/Gloo to eth0; they did not modify the weight-sync implementation.

CPU validation

Focused suites:

pytest tests/utils/test_direct_dwu_roundtrip.py \
       tests/utils/test_weight_update_coordinator.py \
       tests/utils/test_update_weight_from_distributed.py \
       tests/utils/test_update_weight_from_tensor.py \
       tests/utils/test_vllm_engine.py \
       tests/test_megatron_argument_validation.py

The main round-trip suite feeds the real source's update_info() and wire_tensors() into the real receiver against a fake model. It covers dense seed, nonzero sparse update, no-change update, source abort, failed-session poisoning, version/sequence errors, layout drift, NaN rejection, and final manifest behavior.

Latest full tests/utils comparison in the same local environment:

branch:      157 passed, 2 skipped, 6 failed
origin/main: the same 6 failures reproduced in the same environment
ruff:        passed on the changed Python files

The six failures were not branch-specific; no additional failure was introduced relative to origin/main in that environment.

Usage

--update-weight-mode delta \
--update-weight-transport nccl

The trainer must start from update version 0, and VIME must launch and own the non-colocated vLLM rollout engines. See examples/delta_weight_sync/README.md for the runtime requirements and verification checklist.

Follow-up

The current producer gathers and exports the full HF checkpoint before computing the delta. A follow-up shard-local producer can diff Megatron shards in parallel and gather only changed positions and values, reducing the producer-side contribution to the fixed overhead visible in the no-change run.

AI assistance disclosure

This PR includes code developed with assistance from Claude and Codex. The human submitter reviewed the changes, ran the validation described above, and remains responsible for this PR.

Signed-off-by: Shuolei Wang <shuoleiwang123@gmail.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a direct delta-weight-update (DWU) MVP for non-colocated Megatron trainers and VIME-launched vLLM rollout engines, utilizing direct NCCL transport instead of the previously planned disk-based transport. It introduces a CheckpointDeltaSource to compute and send sparse weight updates, a VimeDeltaNCCLWeightTransferEngine to receive and apply these patches on the vLLM side, and a WeightUpdateCoordinator to manage the control plane sequence across engines. The review comments correctly identify a critical runtime issue in vime/backends/vllm_utils/checkpoint_delta.py where PyTorch Stream objects are passed to PyNcclCommunicator.broadcast instead of raw CUDA stream pointers, which will cause a TypeError on GPU.

Comment on lines +169 to +173
self.model_update_group.broadcast(
positions,
src=0,
stream=torch.cuda.current_stream(),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In vLLM, the PyNcclCommunicator.broadcast method expects a raw CUDA stream pointer (an integer) rather than a PyTorch Stream object. Passing torch.cuda.current_stream() directly will result in a TypeError at runtime on GPU.

Use torch.cuda.current_stream().cuda_stream to pass the raw stream pointer.

Suggested change
self.model_update_group.broadcast(
positions,
src=0,
stream=torch.cuda.current_stream(),
)
self.model_update_group.broadcast(
positions,
src=0,
stream=torch.cuda.current_stream().cuda_stream,
)

Comment on lines +180 to +184
self.model_update_group.broadcast(
values,
src=0,
stream=torch.cuda.current_stream(),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In vLLM, the PyNcclCommunicator.broadcast method expects a raw CUDA stream pointer (an integer) rather than a PyTorch Stream object. Passing torch.cuda.current_stream() directly will result in a TypeError at runtime on GPU.

Use torch.cuda.current_stream().cuda_stream to pass the raw stream pointer.

Suggested change
self.model_update_group.broadcast(
values,
src=0,
stream=torch.cuda.current_stream(),
)
self.model_update_group.broadcast(
values,
src=0,
stream=torch.cuda.current_stream().cuda_stream,
)

@read-the-docs-community

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant