vLLM Explained

Process groups: how vLLM slices its ranks

AdvancedvLLM conceptsabout 11 min read + 8 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

In one sentence

Each worker arranges all ranks in one grid and cuts it into groups; every collective of the model runs inside one of these groups.

The idea#

Parallel inference splits one model over several GPUs, and the GPUs exchange tensors with collectives such as all-reduce (Parallelism basics). A collective always runs inside a process group: a fixed set of ranks that take part together. Tensor parallelism needs one group, pipeline parallelism another, and expert parallelism a third.

vLLM builds all its groups in the same way, in every worker. It numbers the ranks and puts them into a grid with one dimension per kind of parallelism. Then it reads each group off one line of the grid. This order is the rank layout. TP is the last dimension, so the ranks of one TP group are neighbours, and they normally sit on one node.

Each group becomes a GroupCoordinator. It holds two PyTorch groups for the same ranks: a gloo group for small CPU messages and an NCCL group for GPU tensors. When the group has more than one rank, it also holds a device communicator. On CUDA, this object picks the fastest all-reduce for each tensor: for example the custom all-reduce, symmetric memory or plain NCCL.

An everyday comparison. Think of the seat plan of a large exam hall. Each seat has a row and a column. "Row 3" and "column 5" are two different teams, but each seat belongs to exactly one row and one column. The hall manager does not invent the teams one by one; she reads them off the plan. The comparison stops here: a vLLM rank belongs to one group of each kind at the same time. Some groups (EP) also span several rows and columns.

The picture#

Figure: TP = 2, PP = 2, DP = 2 for an MoE model on 8 GPUs. One grid (panel 1), four kinds of groups read off it (panel 2), and the communicator of each group (panel 3).
Open full size ↗

Read panel 1 as the grid: each orange lane is one DP rank, which is one engine core with its four workers. Panel 2 lists the members of each group; the code moves the group's dimension to the end of the grid and cuts rows. Panel 3 shows that only groups with more than one rank get a device communicator, and only TP groups get the special all-reduce kernels.

In the code#

Step 1: the worker sets up the groups in init_device#

Each worker builds its groups in init_device, before it takes the memory snapshot. The comment says why: the NCCL buffers must exist before vLLM measures the free memory:

            current_platform.check_if_supports_dtype(self.model_config.dtype)

            # Initialize the distributed environment BEFORE taking
            # memory snapshot
            # This ensures NCCL buffers are allocated before we measure
            # available memory
            init_worker_distributed_environment(
                self.vllm_config,
                self.rank,
                self.distributed_init_method,
                self.local_rank,
                current_platform.dist_backend,
            )

init_worker_distributed_environment first prepares the batch-invariant mode and the EPLB environment variables. Then it does four things in order. It sets the custom all-reduce switch from --disable-custom-all-reduce, creates the world group, creates the model-parallel groups, and starts the EC connector (vllm/v1/worker/gpu_worker.py:1678-1720). The rank and the init method come from the multiproc executor: the global rank of a worker is local_world_size × node_rank_within_dp + local_rank (vllm/v1/executor/multiproc_executor.py:179-192). On one node, the init method is a file:// store in the temp folder, unless AITER needs a TCP store (vllm/v1/executor/multiproc_executor.py:139-144).

Step 2: init_distributed_environment makes the world group#

The first call joins all ranks of this engine into PyTorch's default group. With DP > 1 or several nodes, it first widens the world across the data parallel ranks:

    if (
        config is not None
        and config.parallel_config.distributed_executor_backend != "external_launcher"
        and (
            config.parallel_config.nnodes > 1
            or config.parallel_config.data_parallel_size > 1
        )
        and not enable_elastic_ep
    ):
        parallel_config = config.parallel_config
        # adjust to take into account data parallelism
        # offset the rank by the data parallel rank
        rank = parallel_config.data_parallel_rank * world_size + rank
        # adjust the world size to take into account data parallelism
        world_size = parallel_config.world_size_across_dp

        # Use appropriate IP and port based on configuration
        if parallel_config.nnodes > 1:
            ip = parallel_config.master_addr
            port = parallel_config.master_port
  1. Line 1869. The rank is shifted by data_parallel_rank × world_size. So DP rank 1 of a 4-rank engine starts at global rank 4.
  2. Line 1871. The world grows to world_size_across_dp = TP × PP × PCP × DP (vllm/config/parallel.py:626-629).
  3. Lines 1874-1876. With several nodes, the rendezvous goes to --master-addr and --master-port. Otherwise it goes to the DP master address with a fresh port (lines 1878-1920).

The world group itself is a GroupCoordinator over all ranks, with no device communicator (vllm/distributed/parallel_state.py:1507-1516, vllm/distributed/parallel_state.py:2010-2012).

Step 3: initialize_model_parallel reads the groups off the grid#

    # the layout order is: ExternalDP x DP x PP x PCP x TP
    # ExternalDP is the data parallel group that is not part of the model,
    # every dp rank can generate independently (in verl integration).
    # DP is the data parallel group that is part of the model,
    # all the ranks in the same DP group should generate simultaneously,
    # i.e. the `generate` call in the same DP group should be called together,
    # otherwise it will cause deadlock.
    # to get group_ranks for each dimension, transpose that dimension to the
    # last dimension, then reshape to 2D, then unbind the last dimension
    all_ranks = torch.arange(world_size).reshape(
        -1,
        data_parallel_size,
        pipeline_model_parallel_size,
        prefill_context_model_parallel_size,
        tensor_model_parallel_size,
    )  # noqa

    # Build the tensor model-parallel groups.
    global _TP
    assert _TP is None, "tensor model parallel group is already initialized"
    group_ranks = all_ranks.view(-1, tensor_model_parallel_size).unbind(0)
    group_ranks = [x.tolist() for x in group_ranks]
    if enable_elastic_ep:
        group_ranks = local_all_ranks.view(-1, tensor_model_parallel_size).unbind(0)
        group_ranks = [x.tolist() for x in group_ranks]
    # message queue broadcaster is only used in tensor model parallel group
    _TP = init_model_parallel_group(
        group_ranks,
        get_world_group().local_rank,
        backend,
        use_message_queue_broadcaster=True,
        group_name="tp",
    )
  1. Lines 2148-2156. The comment gives the layout order: ExternalDP × DP × PP × PCP × TP. ExternalDP is for frameworks that run several independent copies of vLLM in one world.
  2. Lines 2157-2163. One tensor of all ranks, shaped (-1, DP, PP, PCP, TP).
  3. Line 2168. TP groups are rows of the last dimension. No transpose is needed.
  4. Lines 2173-2180. The comment says that only the TP group uses a MessageQueue broadcaster. The DCP group also asks for one, but it has one rank by default (vllm/distributed/parallel_state.py:2248-2254).

The other groups use the comment's recipe: move the dimension to the end, reshape, and unbind. PP uses transpose(2, 4) (vllm/distributed/parallel_state.py:2278-2280), DP uses transpose(1, 4) (vllm/distributed/parallel_state.py:2295). The EP group is only built for MoE models, and it spans DP × PCP × TP ranks of one PP stage:

    # Don't create EP group for dense models.
    if config.model_config is None or config.model_config.is_moe:
        group_ranks = (
            all_ranks.transpose(1, 2)
            .reshape(
                -1,
                data_parallel_size
                * prefill_context_model_parallel_size
                * tensor_model_parallel_size,
            )
            .unbind(0)
        )

An EPLB group with the same ranks follows only with --enable-eplb. The comment explains the separate group: EPLB weight transfers must not deadlock with the MoE collectives of the forward pass (vllm/distributed/parallel_state.py:2347-2368). At the end, every rank logs its position, for example "rank 5 in world size 8 is assigned as DP rank 1, …" (vllm/distributed/parallel_state.py:2372-2385).

Step 4: one GroupCoordinator per group#

By default, the constructor makes a PyTorch group for every row, so all ranks take part in every new_group call. Each rank keeps the pair for its own row: an NCCL device_group and a gloo cpu_group (vllm/distributed/parallel_state.py:501-518). Then it adds the optional parts:


        self.use_device_communicator = use_device_communicator
        self.device_communicator = None
        if use_device_communicator and self.world_size > 1:
            device_comm_cls = resolve_obj_by_qualname(
                current_platform.get_device_communicator_cls()
            )
            self.device_communicator = device_comm_cls(
                cpu_group=self.cpu_group,
                device=self.device,
                device_group=self.device_group,
                unique_name=self.unique_name,
                use_all2all=use_all2all,
            )

        from vllm.distributed.device_communicators.shm_broadcast import MessageQueue

        self.mq_broadcaster: MessageQueue | None = None
        if use_message_queue_broadcaster and self.world_size > 1:
            self.mq_broadcaster = MessageQueue.create_from_process_group(
                self.cpu_group, 1 << 22, 6
            )

        # TODO(#35915): Remove is_tpu() check once tpu_inference
        # overrides use_custom_op_collectives() to return True.
        self.use_custom_op_call = (
            current_platform.is_tpu() or current_platform.use_custom_op_collectives()
        )
  1. Lines 549-560. The device communicator class comes from the platform: CudaCommunicator on CUDA and ROCm (vllm/platforms/cuda.py:581-584). A group with one rank gets none.
  2. Lines 564-567. The shared-memory broadcaster has 6 chunks of 4 MiB (1 << 22). broadcast_object uses it when it exists (vllm/distributed/parallel_state.py:855-866).
  3. Lines 571-574. On platforms that opt in, the collectives go through registered custom ops. CUDA opts in (vllm/platforms/cuda.py:707-709).

Step 5: the CUDA device communicator picks an all-reduce#

CudaCommunicator always makes a PyNcclCommunicator for a group with more than one rank (vllm/distributed/device_communicators/cuda_communicator.py:106-113). The faster kernels are only for TP groups:

        # Match the group name exactly so ETP does not enable TP-only backends.
        if unique_name.split(":")[0] != "tp":
            # custom allreduce or torch symm mem can be used only by tp
            use_custom_allreduce = False
            use_torch_symm_mem = False
            use_flashinfer_allreduce = False
            use_flashinfer_pcie_ipc_allreduce = False
            use_aiter_allreduce = False
        else:
            from vllm.distributed.parallel_state import _ENABLE_CUSTOM_ALL_REDUCE

            use_custom_allreduce = _ENABLE_CUSTOM_ALL_REDUCE
            use_torch_symm_mem = envs.VLLM_ALLREDUCE_USE_SYMM_MEM
            # FlashInfer all-reduce does not provide a fixed reduction order.
            use_flashinfer_allreduce = (
                envs.VLLM_ALLREDUCE_USE_FLASHINFER and not envs.VLLM_BATCH_INVARIANT
            )
            use_flashinfer_pcie_ipc_allreduce = (
                envs.VLLM_ALLREDUCE_USE_FLASHINFER_PCIE_IPC
                and not envs.VLLM_BATCH_INVARIANT
            )
            # Neither AITER nor QuickReduce all-reduce has a fixed reduction order.
            use_aiter_allreduce = (
                use_custom_allreduce
                and not envs.VLLM_BATCH_INVARIANT
                and bool(rocm_aiter_ops.is_custom_all_reduce_enabled())
            )

At run time, all_reduce tries the implementations in a fixed order and takes the first that accepts the tensor (vllm/distributed/device_communicators/cuda_communicator.py:362-446). On CUDA the chain has six links. They are NCCL symmetric memory (only with VLLM_USE_NCCL_SYMM_MEM=1), the FlashInfer PCIe all-reduce (opt-in), FlashInfer, the custom all-reduce, PyTorch symmetric memory, and pynccl. The size and dtype checks live in each implementation. The start-up log names the enabled ones for each group: "Using [...] all-reduce backends (in dispatch order) for group 'tp:0'" (vllm/distributed/device_communicators/cuda_communicator.py:353-360). The log puts NCCL symmetric memory after the two FlashInfer entries, but all_reduce tries it first (vllm/distributed/device_communicators/cuda_communicator.py:307-337, vllm/distributed/device_communicators/cuda_communicator.py:372-379).

Step 6: the collectives that the model calls#

Model code does not use the groups directly. It calls helpers such as tensor_model_parallel_all_reduce, which forward to the TP group (vllm/distributed/communication_op.py:12-21). The group then calls the registered op:

    def all_reduce(self, input_: torch.Tensor) -> torch.Tensor:
        """User-facing all-reduce function before we actually call the
        all-reduce operation.

        We need this because Dynamo does not support passing an arbitrary
        object (`self` in this case) to a custom op. We need to pass the
         group name as a string, and then look up the group coordinator from
         the group name, dispatch the all-reduce operation to the group
         coordinator.

        In addition, PyTorch custom ops do not support mutation or returning
        a new tensor in the same op. So we always make the all-reduce operation
        out-of-place.
        """
        # Bypass the function if we are using only 1 GPU.
        if self.world_size == 1:
            return input_

        if self.use_custom_op_call:
            return torch.ops.vllm.all_reduce(input_, group_name=self.unique_name)
        else:
            return self._all_reduce_out_place(input_)

    def _all_reduce_out_place(self, input_: torch.Tensor) -> torch.Tensor:
        if self.device_communicator is None:
            raise ValueError("No device communicator found")
        return self.device_communicator.all_reduce(input_)
  1. Lines 740-741. With one rank, the call returns the input. No kernel runs.
  2. Lines 743-744. torch.ops.vllm.all_reduce carries only the group name, a string. The docstring explains why: Dynamo cannot pass the Python object into a custom op (lines 726-737). The op looks the group up by name and calls the device communicator (vllm/distributed/parallel_state.py:199-204, vllm/distributed/parallel_state.py:399-403).
  3. Lines 748-751. The op is out-of-place: it returns a new tensor.

all_gather and reduce_scatter work the same way (vllm/distributed/parallel_state.py:753-798). Pipeline stages use a different pair: isend_tensor_dict and irecv_tensor_dict on the PP group. The worker sends the IntermediateTensors without waiting, and the next stage receives them at the start of its step (vllm/v1/worker/gpu_worker.py:1336-1347, vllm/v1/worker/gpu_worker.py:1371-1378). broadcast_tensor_dict also exists, as a TP-group helper (vllm/distributed/communication_op.py:38-43); no code of Model Runner V2 calls it (derived from git grep).

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

Worked example: TP = 2 × PP = 2 × DP = 2 on 8 GPUs

vllm serve <an MoE model> -tp 2 -pp 2 -dp 2      # one node, 8 GPUs

Each engine core has world_size = TP × PP = 4 workers. Engine core d gets GPUs 4d to 4d + 3 (vllm/v1/engine/utils.py:441-445). The global rank of a worker is 4 × dp_rank + local rank (Step 2). The grid is arange(8).reshape(1, 2, 2, 1, 2), so rank = 4·dp + 2·pp + tp.

Rank (GPU)dppptpTP groupPP groupDP groupEP group
0000[0, 1][0, 2][0, 4][0, 1, 4, 5]
1001[0, 1][1, 3][1, 5][0, 1, 4, 5]
2010[2, 3][0, 2][2, 6][2, 3, 6, 7]
3011[2, 3][1, 3][3, 7][2, 3, 6, 7]
4100[4, 5][4, 6][0, 4][0, 1, 4, 5]
5101[4, 5][5, 7][1, 5][0, 1, 4, 5]
6110[6, 7][4, 6][2, 6][2, 3, 6, 7]
7111[6, 7][5, 7][3, 7][2, 3, 6, 7]

How each column is computed (derived from the lines in Step 3):

  1. TP: view(-1, 2) cuts the flat order 0…7 into pairs.
  2. PP: transpose(2, 4) swaps the PP and TP axes. The rows of length 2 are then (dp, tp) fixed, pp varying: [0, 2], [1, 3], [4, 6], [5, 7].
  3. DP: transpose(1, 4) swaps DP and TP. The rows are (tp, pp) fixed, dp varying: [0, 4], [2, 6], [1, 5], [3, 7].
  4. EP: transpose(1, 2) puts PP before DP. Then each row of DP × PCP × TP = 4 ranks is one PP stage: [0, 1, 4, 5] and [2, 3, 6, 7].
  5. PCP and DCP: both sizes are 1, so there are eight groups of one rank each. They have no device communicator.

The same command with a dense model. Each engine core is reconfigured to DP = 1 (Step 2). So each engine core has its own world arange(4).reshape(1, 1, 2, 1, 2). It has TP groups [0, 1] and [2, 3], PP groups [0, 2] and [1, 3], DP groups of one rank, and no EP group. Rank numbers start at 0 in both engines, and the two engines have separate NCCL communicators.

Deep dive

Which groups exist, and which get a communicator

The table is for initialize_model_parallel on CUDA with default flags. "Size" is the number of ranks in one group.

Group (name)SizeExistsDevice communicatorGetter
world (world)all ranksalwaysnone (use_device_communicator=False)get_world_group (vllm/distributed/parallel_state.py:1497-1499)
TP (tp)TPalwaysif TP > 1; with custom all-reduce, symmetric memory, FlashInfer; plus a MessageQueue broadcasterget_tp_group (vllm/distributed/parallel_state.py:1593-1595)
ETP (etp)TP unless an Engram config changes itsame object as TP by defaultas TPvllm/distributed/parallel_state.py:2183-2200
DCP (dcp)1 by defaultalwaysonly with --decode-context-parallel-size > 1vllm/distributed/parallel_state.py:2237-2254
PCP (pcp)1 by defaultalwaysonly with PCP > 1vllm/distributed/parallel_state.py:2256-2272
PP (pp)PPalwaysif PP > 1 (pynccl only)vllm/distributed/parallel_state.py:2274-2291
DP (dp)DP (1 for a dense model)alwaysif DP > 1 (MoE)vllm/distributed/parallel_state.py:2293-2309
EP (ep)DP × PCP × TPMoE models onlyif size > 1; an all2all manager when use_all2allvllm/distributed/parallel_state.py:2311-2345
EPLB (eplb)as EPMoE and --enable-eplbif size > 1vllm/distributed/parallel_state.py:2347-2368
inner DP world (inner_dp_world)TP × PP × PCPonly when one DP replica spans nodes and DP > 1; with DP = 1 the name points to the world groupnone; MessageQueue broadcastervllm/distributed/parallel_state.py:2022-2039

The group names get a counter suffix, so the first TP group object of a process is tp:0 (vllm/distributed/parallel_state.py:145-158). The CUDA communicator checks the prefix before the colon, so only groups named exactly tp get the TP-only kernels (vllm/distributed/device_communicators/cuda_communicator.py:56-57). use_all2all is true when DP > 1, for sequence-parallel MoE, or for EP with PCP (vllm/config/parallel.py:787-793); the EP communicator then makes the manager for --all2all-backend (vllm/distributed/device_communicators/cuda_communicator.py:202-283).

Facts for a dense -tp 2 server (derived): each worker builds exactly one device communicator, for its TP group. All other groups have one rank.

Deep dive

The all-reduce chain on CUDA: conditions and switches

OrderImplementationBuilt whenUsed for a tensor when
1NCCL symmetric memory (all_reduce_symmetric_with_copy)VLLM_USE_NCCL_SYMM_MEM=1 (default 0, vllm/envs.py:2088-2090)FlashInfer does not take it, and the size rule of NCCL_SYMM_MEM_ALL_REDUCE_CONFIG allows it: world size 4 or more; at 4 and 8 only small and large tensors, not the middle range (vllm/distributed/device_communicators/all_reduce_utils.py:109-154)
2FlashInfer PCIe IPCTP group and VLLM_ALLREDUCE_USE_FLASHINFER_PCIE_IPC=1 (default 0, vllm/envs.py:1965-1967)should_use(input)
3FlashInfer all-reduceTP group, VLLM_ALLREDUCE_USE_FLASHINFER=1 (default 1, vllm/envs.py:1959-1961), FlashInfer installed, and a size entry for this world size and GPU: only compute capability 9.0 and 10.x have entries, for example 64 MiB at TP = 2 (vllm/distributed/device_communicators/flashinfer_all_reduce.py:400-449, vllm/config/compilation.py:197-209, vllm/compilation/passes/fusion/allreduce_rms_fusion.py:104-127)2-D, contiguous, FP16/BF16/FP32, and not larger than its workspace (vllm/distributed/device_communicators/flashinfer_all_reduce.py:469-494)
4custom all-reduce (CustomAllreduce)TP group, not --disable-custom-all-reduce, world size 2, 4, 6, 8 or 16, P2P works, and NVLink when more than 2 GPUs (vllm/distributed/device_communicators/custom_all_reduce.py:214-291)world size 8 or less, FP16/BF16/FP32, a multiple of 16 bytes, smaller than max_size. That is 8 MiB by default. When symmetric memory is active, a per-GPU table can only lower it: the code takes the smaller value. For example, it stays 8 MiB at TP = 2 on compute capability 9.0 and becomes 2 MiB at TP = 2 on 10.0 (vllm/distributed/device_communicators/custom_all_reduce.py:499-513, vllm/distributed/device_communicators/custom_all_reduce.py:240-254, vllm/distributed/device_communicators/custom_all_reduce.py:141, vllm/distributed/device_communicators/all_reduce_utils.py:31-43)
5PyTorch symmetric memoryTP group and VLLM_ALLREDUCE_USE_SYMM_MEM=1 (default 1, vllm/envs.py:1955-1957)should_use_symm_mem(input)
6pyncclevery group with more than one rankeverything else

The chain is the order of the if blocks in vllm/distributed/device_communicators/cuda_communicator.py:362-446. ROCm adds QuickReduce and AITER into the same chain. Which implementation wins for a given tensor depends on the installed libraries and the GPU topology. The code gives no single answer, so read the start-up log line of Step 5.

Deep dive

Global ranks across nodes

A worker's global rank is built in two steps (derived from the cited lines):

  1. The executor of one DP rank numbers its workers across the nodes of that replica: rank = local_world_size × node_rank_within_dp + local_rank (vllm/v1/executor/multiproc_executor.py:179-192). local_world_size is world_size // nnodes_within_dp (vllm/config/parallel.py:838-840).
  2. If the world spans DP ranks or nodes, init_distributed_environment adds data_parallel_rank × world_size (Step 2).

So global = dp_rank × (TP·PP·PCP) + node_rank_within_dp × local_world_size + local_rank.

Example. -tp 8 -pp 2 --nnodes 2 with DP = 1: world_size = 16, local_world_size = 8. Local rank 3 on node 1 has global rank 8 × 1 + 3 = 11. In the grid (1, 1, 2, 1, 8), rank 11 is PP stage 1, TP rank 3. All TP groups stay inside one node; the PP groups [i, i + 8] cross the nodes (derived). The executor's output rank is 16 − 8 = 8, the first TP rank of the last stage (vllm/v1/executor/multiproc_executor.py:549-563).

Deep dive

Environment variables and NCCL settings

VariableDefaultRead atEffect
VLLM_HOST_IP"" (vllm/envs.py:723)vllm/utils/network_utils.py:52-63The IP of this node for vLLM's own sockets. If empty, vLLM asks the OS for the interface that routes to 8.8.8.8. Set it per node on multi-node hosts with several interfaces (comment at vllm/envs.py:719-722). It is not the API server address.
VLLM_PORTunset (vllm/envs.py:517-545)vllm/utils/network_utils.py:207-240First port for internal ports; later ports count up from it. Not the HTTP port.
VLLM_LOOPBACK_IP""vllm/utils/network_utils.py:104-107Loopback address for single-node rendezvous.
VLLM_DISTRIBUTED_USE_SPLIT_GROUP0 (vllm/envs.py:933-935)vllm/distributed/parallel_state.py:480-484Build the groups with PyTorch split_group instead of new_group.
VLLM_ALLREDUCE_USE_SYMM_MEM, VLLM_ALLREDUCE_USE_FLASHINFER, VLLM_USE_NCCL_SYMM_MEM1, 1, 0see the all-reduce deep diveSwitch implementations in the all-reduce chain.
VLLM_SKIP_P2P_CHECKread as true when unset (vllm/envs.py:1207)vllm/distributed/device_communicators/custom_all_reduce.py:90Trust the driver's P2P report instead of a test; the type stub says False (vllm/envs.py:128), the reader defaults to "1".
VLLM_NCCL_SO_PATHunsetvllm/utils/nccl.py:21-28The NCCL library that pynccl loads.

NCCL variables that vLLM sets itself. Only in special cases:

Deep dive

CUDA graph capture and the groups

Collectives inside a captured CUDA graph need care, because the custom all-reduce must register the buffers that the graph uses. graph_capture() opens one capture stream and enters the capture context of the TP, PP and DP groups (vllm/distributed/parallel_state.py:1693-1726). For each group, the context enters the custom all-reduce capture, and the FlashInfer PCIe and AITER captures when they exist (vllm/distributed/parallel_state.py:673-723). With the sleep-mode cuMem graph pool, the communicators do not register graph buffers; they copy instead (vllm/distributed/device_communicators/cuda_communicator.py:123-125).

What can go wrong#

Why it matters. Every multi-GPU feature of vLLM runs on these groups. When you know the rank layout, you can predict which GPUs talk in each collective, and which of them must sit on one node.

Check yourself#

  1. 1
    In which order does vLLM lay out the ranks, and which dimension is the fastest?
    easy
    Show answer

    ExternalDP × DP × PP × PCP × TP, with TP last. So the ranks of a TP group are neighbours (vllm/distributed/parallel_state.py:2148-2163).

  2. 2
    Which group gets a device communicator?
    easy
    Show answer

    Every group with more than one rank, except the world group, which is built with use_device_communicator=False (vllm/distributed/parallel_state.py:549-560, vllm/distributed/parallel_state.py:1507-1516).

  3. 3
    Why does GroupCoordinator.all_reduce call torch.ops.vllm.all_reduce with a group name instead of calling the communicator directly?
    medium
    Show answer

    Dynamo cannot pass the Python group object into a custom op, so the op takes the group's unique name and looks it up. The custom op also stays one opaque node in the compiled graph. The docstring says this (vllm/distributed/parallel_state.py:725-746).

  4. 4
    An MoE model runs with -tp 2 -pp 2 -dp 2 on 8 GPUs. Which ranks are in the DP group and in the EP group of rank 5?
    hard
    Show answer

    Rank 5 is dp 1, pp 0, tp 1 (5 = 4·1 + 2·0 + 1). DP group: transpose(1, 4) keeps tp 1, pp 0 and varies dp: [1, 5]. EP group: the DP × PCP × TP ranks of PP stage 0: [0, 1, 4, 5] (vllm/distributed/parallel_state.py:2295, vllm/distributed/parallel_state.py:2313-2324).

  5. 5
    You serve a dense model with -tp 2 -dp 2. Do the two engine cores share one NCCL world? Why?
    hard
    Show answer

    No. A dense model's engine core calls reconfigure_for_independent_dp_rank(), which sets the DP size to 1 (vllm/v1/engine/core.py:1534-1543, vllm/config/parallel.py:1192-1201). So init_distributed_environment skips the DP widening (vllm/distributed/parallel_state.py:1857-1871), and each engine has its own world of two ranks.