NCCL M2N Engine¶
The nccl_m2n weight transfer engine uses NCCL M2N to reshard weights between two disjoint meshes of GPUs — the trainer and the inference workers — in a single collective per parameter. Unlike the broadcast-based NCCL engine, the trainer sends its local shard as-is: no full tensor is ever materialized on the training side, whatever its parallelism layout.
When to Use NCCL M2N¶
- The trainer and the inference engine use different parallelism layouts (e.g. trainer FSDP/EP, inference TP) and gathering full tensors on the trainer is expensive
- Training and inference are on separate GPUs, possibly across nodes
- You want weight sync off the critical path of an async RL loop
Requirements¶
This backend depends on the nccl-extensions Python package (and its nccl4py dependency), which vLLM does not install. Install both against a compatible NCCL 2.30.5 or newer runtime. The backend creates its data-plane communicator through nccl4py and passes that communicator directly to nccl.m2n.
The backend imports the runtime lazily, so vLLM is unaffected unless you select it.
How It Works¶
- A
StatelessProcessGroupexchanges metadata and the NCCL unique ID. The trainer ranks and all inference workers then create onenccl.core.Communicator: trainer ranks occupy[0, T)and workers[T, T + N). The communicator is passed directly to each M2N collective. - At the init handshake the trainer ships the full transfer plan: the session-wide destination mesh, then each parameter's name, dtype, shape, source mesh, and source placements. The workers resolve the matching destination placements and publish them over the data-plane communicator; the meshes themselves are declared, not negotiated.
- Each round, both sides issue one
reshardper parameter in the same order. Every rank in the communicator participates in every reshard, so — as with the NCCL engine — the worker must be insideupdate_weightswhile the trainer sends. The trainer engine handles that concurrency itself.
Destination resolution¶
A parameter is resharded directly into the live vLLM parameter when its checkpoint name maps 1:1 onto a model parameter whose shape is either identical to the checkpoint shape (replicated) or differs on exactly one dim by the tensor-parallel factor. That rank then only ever receives its own shard.
Anything else falls back to receiving the full tensor and letting load_weights do the sharding, exactly as the NCCL engine does for every parameter. The fallback covers:
- fused parameters (
qkv_proj,gate_up_proj, MoEw13/w2), whose checkpoint names do not name a model parameter - quantized models, where the weight loader does more than a sharded copy
- pipeline-parallel deployments
Correctness never depends on a parameter resolving. The engine logs how many parameters took each path at init.
Configuration¶
from vllm import LLM
from vllm.config import WeightTransferConfig
llm = LLM(
model="my-model",
weight_transfer_config=WeightTransferConfig(backend="nccl_m2n"),
)
Trainer Side¶
The trainer uses the stateful trainer engine. m2n plans each reshard from the parameter's source layout, which a WeightSource declares on its ParamMeta (owners, mesh_shape, placements), and sends what iter_local() yields: each rank's local shard, never a full_tensor(). ModuleSource covers the common case: it reads each parameter's device mesh and placements.
from vllm.distributed.weight_transfer import (
ModuleSource,
RayVLLMWeightSyncClient,
WeightTransferTrainerFactory,
)
from vllm.distributed.weight_transfer.m2n_trainer import M2NTrainerInitInfo
# Called on every trainer rank; rank 0 is the sender.
engine = WeightTransferTrainerFactory.trainer_init(
init_info=M2NTrainerInitInfo(
master_address=master_address,
master_port=master_port,
world_size=num_trainer_ranks + num_inference_workers,
num_trainer_ranks=num_trainer_ranks,
rank=my_rank,
),
client=RayVLLMWeightSyncClient(llm_handle),
source=ModuleSource(model),
)
# Each round, on every trainer rank:
engine.send_weights()
send_weights() drives the whole round — start_weight_update, update_weights, finish_weight_update. Every rank in the communicator must be inside the same reshard, so update_weights has to be in flight while the trainer sends; the engine runs it on a helper thread so the trainer loop does not have to open-code that.
Trainers with a custom producer (a Megatron export, MoE re-fusing) subclass
WeightSource, declare each layout on ParamMeta and implement iter_local().
A 1-D mesh is described over m2n's 2-D mesh with a size-1 second axis. Each
yielded tensor must match its declared dtype and local shape, and is kept alive
in stream order until its reshard completes.
See examples/rl/rlhf_m2n.py for a runnable FSDP → TP example.
Limitations¶
- vLLM validates tensor-rank and dtype support while agreeing on the complete plan; the installed M2N runtime validates them again for each reshard.
- No pipeline-parallel trainers. Every parameter must be owned by all trainer ranks, in rank order (
ownersunset or(0, ..., T-1)), with a 1-D or 2-D mesh. Partitioned owners, such as one group per pipeline stage, are rejected at init. Partialplacements cannot be sent as shards.ModuleSourcedeclares such a parameter replicated and gathers it initer_local().- Models with known model-level checkpoint preprocessing use the full-tensor fallback for the affected module.
- A reshard cannot be issued from inside a CUDA graph capture (weight updates run outside capture, so this only matters for custom callers).
- vLLM checks every parameter's shard overlap against conservative 16-source and 64-target capacities before committing the plan. The installed M2N runtime remains authoritative and validates each reshard again.
- Each communicator holds a cached staging pool (~256 MiB by default: 4 channels x 64 MiB).