main
py 727 lines 26.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 # Copyright 2023 The vLLM team.
5 # Adapted from
6 # https://github.com/NVIDIA/Megatron-LM/blob/main/megatron/core/tensor_parallel/utils.py
7 # Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
8 import dataclasses
9 import functools
10 import os
11 import pickle
12 import socket
13 import sys
14 import time
15 import uuid
16 from collections import deque
17 from collections.abc import Sequence
18 from datetime import timedelta
19 from typing import Any
20
21 import torch
22 from torch.distributed import ProcessGroup, Store, TCPStore
23 from torch.distributed.distributed_c10d import (
24 Backend,
25 PrefixStore,
26 _get_default_timeout,
27 _unregister_process_group,
28 )
29 from torch.distributed.rendezvous import rendezvous
30
31 import vllm.envs as envs
32 from vllm.logger import init_logger
33 from vllm.utils.network_utils import get_tcp_uri
34 from vllm.utils.system_utils import suppress_stdout
35
36 logger = init_logger(__name__)
37
38 # We prefer to use os.sched_yield as it results in tighter polling loops,
39 # measured to be around 3e-7 seconds. However on earlier versions of Python
40 # os.sched_yield() does not release the GIL, so we fall back to time.sleep(0)
41 USE_SCHED_YIELD = (sys.version_info[:3] >= (3, 11, 1)) or (
42 sys.version_info[:2] == (3, 10) and sys.version_info[2] >= 8
43 )
44
45
46 def sched_yield():
47 if USE_SCHED_YIELD:
48 os.sched_yield()
49 else:
50 time.sleep(0)
51
52
53 def ensure_divisibility(numerator, denominator):
54 """Ensure that numerator is divisible by the denominator."""
55 assert numerator % denominator == 0, "{} is not divisible by {}".format(
56 numerator, denominator
57 )
58
59
60 def divide(numerator, denominator):
61 """Ensure that numerator is divisible by the denominator and return
62 the division value."""
63 ensure_divisibility(numerator, denominator)
64 return numerator // denominator
65
66
67 def verify_group_size_divides_partition(
68 input_size_per_partition: int,
69 group_size: int,
70 layer_name: str | None = None,
71 extra_suggestion: str = "",
72 ) -> None:
73 """Validate that a TP-sharded layer holds a whole number of quant groups."""
74 if input_size_per_partition % group_size == 0:
75 return
76 location = f" for layer '{layer_name}'" if layer_name else ""
77 raise ValueError(
78 f"Weight {input_size_per_partition=}{location} is not divisible by "
79 f"{group_size=}. This happens when tensor_parallel_size splits the layer input "
80 "into shards that are not a whole number of quant groups. Consider reducing "
81 f"tensor_parallel_size{extra_suggestion}."
82 )
83
84
85 def is_weak_contiguous(inp: torch.Tensor) -> bool:
86 """Check that *inp* occupies a single contiguous block of memory.
87
88 Unlike ``torch.Tensor.is_contiguous()``, this also accepts tensors
89 whose strides are not strictly C-contiguous (e.g. column-major) as
90 long as the underlying storage from the tensor's offset onward is
91 exactly ``numel * element_size`` bytes.
92 """
93 return inp.is_contiguous() or (
94 inp.storage().nbytes() - inp.storage_offset() * inp.element_size()
95 == inp.numel() * inp.element_size()
96 )
97
98
99 def split_tensor_along_last_dim(
100 tensor: torch.Tensor,
101 num_partitions: int,
102 contiguous_split_chunks: bool = False,
103 ) -> Sequence[torch.Tensor]:
104 """Split a tensor along its last dimension.
105
106 Arguments:
107 tensor: input tensor.
108 num_partitions: number of partitions to split the tensor
109 contiguous_split_chunks: If True, make each chunk contiguous
110 in memory.
111
112 Returns:
113 A list of Tensors
114
115 """
116 # Get the size and dimension.
117 last_dim = tensor.dim() - 1
118 last_dim_size = divide(tensor.size()[last_dim], num_partitions)
119 # Split.
120 tensor_list = torch.split(tensor, last_dim_size, dim=last_dim)
121 # NOTE: torch.split does not create contiguous tensors by default.
122 if contiguous_split_chunks:
123 return tuple(chunk.contiguous() for chunk in tensor_list)
124
125 return tensor_list
126
127
128 def get_pp_indices(
129 num_hidden_layers: int, pp_rank: int, pp_size: int
130 ) -> tuple[int, int]:
131 """Try to evenly distribute layers across partitions.
132
133 If the number of layers is not divisible by the number of partitions,
134 the remaining layers are evenly distributed across all but the last
135 partition. The last partition is excluded because it often contains an
136 additional norm layer and we are attempting to balance compute.
137
138 If `pp_size > 2` and the number of remaining layers is
139 `0 < x <= pp_size - 2` then the remaining layers are evenly distributed
140 across the middle partitions. The first and last partitions are excluded
141 because they contain the input and output embeddings respectively and we
142 are attempting to reduce maximum memory consumption across partitions.
143 """
144 partition_list_str = envs.VLLM_PP_LAYER_PARTITION
145 if partition_list_str is not None:
146 try:
147 partitions = [int(layer) for layer in partition_list_str.split(",")]
148 except ValueError as err:
149 raise ValueError(
150 "Invalid partition string: {}".format(partition_list_str)
151 ) from err
152 if len(partitions) != pp_size:
153 raise ValueError(f"{len(partitions)=} does not match {pp_size=}.")
154 if sum(partitions) != num_hidden_layers:
155 raise ValueError(f"{sum(partitions)=} does not match {num_hidden_layers=}.")
156 else:
157 layers_per_partition = num_hidden_layers // pp_size
158 partitions = [layers_per_partition for _ in range(pp_size)]
159
160 if remaining_layers := num_hidden_layers % pp_size:
161 for i in range(2, remaining_layers + 2):
162 partitions[-i] += 1
163 logger.info(
164 "Hidden layers were unevenly partitioned: [%s]. "
165 "This can be manually overridden using the "
166 "VLLM_PP_LAYER_PARTITION environment variable",
167 ",".join(str(p) for p in partitions),
168 )
169
170 start_layer = sum(partitions[:pp_rank])
171 end_layer = start_layer + partitions[pp_rank]
172
173 return (start_layer, end_layer)
174
175
176 def create_tcp_store(
177 host: str,
178 port: int,
179 listen_socket: socket.socket | None = None,
180 **kwargs: Any,
181 ) -> TCPStore:
182 """Create a TCPStore, optionally taking ownership of ``listen_socket``."""
183 if listen_socket is None:
184 return TCPStore(host_name=host, port=port, **kwargs)
185
186 listen_fd = listen_socket.detach()
187 try:
188 return TCPStore(
189 host_name=host,
190 port=port,
191 master_listen_fd=listen_fd,
192 **kwargs,
193 )
194 except Exception:
195 socket.close(listen_fd)
196 raise
197
198
199 @dataclasses.dataclass
200 class StatelessProcessGroup:
201 """A dataclass to hold a metadata store, and the rank, world_size of the
202 group. Only use it to communicate metadata between processes.
203 For data-plane communication, create NCCL-related objects.
204 """
205
206 rank: int
207 world_size: int
208 store: torch._C._distributed_c10d.Store
209
210 data_expiration_seconds: int = 3600 # 1 hour
211
212 # dst rank -> counter
213 send_dst_counter: dict[int, int] = dataclasses.field(default_factory=dict)
214 # src rank -> counter
215 recv_src_counter: dict[int, int] = dataclasses.field(default_factory=dict)
216 broadcast_send_counter: int = 0
217 broadcast_recv_src_counter: dict[int, int] = dataclasses.field(default_factory=dict)
218
219 # A deque to store the data entries, with key and timestamp.
220 entries: deque[tuple[str, float]] = dataclasses.field(default_factory=deque)
221
222 def __post_init__(self):
223 assert self.rank < self.world_size
224 self.send_dst_counter = {i: 0 for i in range(self.world_size)}
225 self.recv_src_counter = {i: 0 for i in range(self.world_size)}
226 self.broadcast_recv_src_counter = {i: 0 for i in range(self.world_size)}
227
228 def send_obj(self, obj: Any, dst: int):
229 """Send an object to a destination rank."""
230 self.expire_data()
231 key = f"send_to/{dst}/{self.send_dst_counter[dst]}"
232 self.store.set(key, pickle.dumps(obj))
233 self.send_dst_counter[dst] += 1
234 self.entries.append((key, time.time()))
235
236 def expire_data(self):
237 """Expire data that is older than `data_expiration_seconds` seconds."""
238 while self.entries:
239 # check the oldest entry
240 key, timestamp = self.entries[0]
241 if time.time() - timestamp > self.data_expiration_seconds:
242 self.store.delete_key(key)
243 self.entries.popleft()
244 else:
245 break
246
247 def recv_obj(self, src: int) -> Any:
248 """Receive an object from a source rank."""
249 obj = pickle.loads(
250 self.store.get(f"send_to/{self.rank}/{self.recv_src_counter[src]}")
251 )
252 self.recv_src_counter[src] += 1
253 return obj
254
255 def broadcast_obj(self, obj: Any | None, src: int) -> Any:
256 """Broadcast an object from a source rank to all other ranks.
257 It does not clean up after all ranks have received the object.
258 Use it for limited times, e.g., for initialization.
259 """
260 if self.rank == src:
261 self.expire_data()
262 key = f"broadcast_from/{src}/{self.broadcast_send_counter}"
263 self.store.set(key, pickle.dumps(obj))
264 self.broadcast_send_counter += 1
265 self.entries.append((key, time.time()))
266 return obj
267 else:
268 key = f"broadcast_from/{src}/{self.broadcast_recv_src_counter[src]}"
269 recv_obj = pickle.loads(self.store.get(key))
270 self.broadcast_recv_src_counter[src] += 1
271 return recv_obj
272
273 def all_gather_obj(self, obj: Any) -> list[Any]:
274 """All gather an object from all ranks."""
275 gathered_objs = []
276 for i in range(self.world_size):
277 if i == self.rank:
278 gathered_objs.append(obj)
279 self.broadcast_obj(obj, src=self.rank)
280 else:
281 recv_obj = self.broadcast_obj(None, src=i)
282 gathered_objs.append(recv_obj)
283 return gathered_objs
284
285 def broadcast(self, tensor: torch.Tensor, src: int) -> torch.Tensor:
286 """Broadcast a tensor from source rank to all other ranks."""
287 if self.rank == src:
288 tensor_bytes = pickle.dumps(tensor)
289 self.expire_data()
290 key = f"broadcast_tensor/{src}/{self.broadcast_send_counter}"
291 self.store.set(key, tensor_bytes)
292 self.broadcast_send_counter += 1
293 self.entries.append((key, time.time()))
294 return tensor
295 else:
296 key = f"broadcast_tensor/{src}/{self.broadcast_recv_src_counter[src]}"
297 tensor = pickle.loads(self.store.get(key))
298 self.broadcast_recv_src_counter[src] += 1
299 return tensor
300
301 def send(self, tensor: torch.Tensor, dst: int):
302 """Send a tensor to a destination rank."""
303 self.expire_data()
304 key = f"send_tensor/{dst}/{self.send_dst_counter[dst]}"
305 self.store.set(key, pickle.dumps(tensor))
306 self.send_dst_counter[dst] += 1
307 self.entries.append((key, time.time()))
308
309 def recv(self, tensor: torch.Tensor, src: int) -> torch.Tensor:
310 """Receive a tensor from a source rank."""
311 key = f"send_tensor/{self.rank}/{self.recv_src_counter[src]}"
312 received = pickle.loads(self.store.get(key))
313 self.recv_src_counter[src] += 1
314 tensor.copy_(received)
315 return tensor
316
317 def all_reduce(
318 self, tensor: torch.Tensor, op=torch.distributed.ReduceOp.SUM
319 ) -> torch.Tensor:
320 """All-reduce a tensor across all ranks."""
321 tensors = self.all_gather_obj(tensor)
322 result = tensors[0].clone()
323 for t in tensors[1:]:
324 if op == torch.distributed.ReduceOp.SUM:
325 result.add_(t)
326 elif op == torch.distributed.ReduceOp.PRODUCT:
327 result.mul_(t)
328 elif op == torch.distributed.ReduceOp.MAX:
329 result = torch.maximum(result, t)
330 elif op == torch.distributed.ReduceOp.MIN:
331 result = torch.minimum(result, t)
332 return result
333
334 def barrier(self, timeout: float = 30.0):
335 """A robust barrier to synchronize all ranks.
336
337 Uses a multi-phase approach to ensure all processes reach the barrier
338 before proceeding:
339
340 1. Each process signals it has reached the barrier
341
342 2. Each process signals that it has confirmed the arrival of all other
343 ranks.
344
345 3. Rank 0 waits for all other ranks to signal their departure to ensure
346 that all ranks have departed the barrier first.
347
348 Args:
349 timeout: Maximum time in seconds to wait for each phase (in seconds)
350
351
352 Raises:
353 RuntimeError: If coordination fails or times out
354
355 """
356 # Generate a barrier ID that is globally unique
357 try:
358 if self.rank == 0:
359 barrier_id = f"barrier_{uuid.uuid4()}"
360 self.broadcast_obj(barrier_id, src=0)
361 else:
362 barrier_id = self.broadcast_obj(None, src=0)
363 except Exception as e:
364 raise RuntimeError("Failed to broadcast barrier_id") from e
365
366 # Phase 1: Signal arrival at barrier
367 # Wait for all processes to arrive
368 # We need all ranks to confirm the arrival of all other ranks.
369 # This is the key synchronization point.
370 arrival_key = f"arrival_{barrier_id}_{self.rank}"
371 try:
372 self.store.set(arrival_key, b"1")
373 except Exception as e:
374 raise RuntimeError("Failed to signal barrier arrival") from e
375
376 start_time = time.time()
377 processes_arrived: set[int] = set()
378
379 while len(processes_arrived) < self.world_size:
380 # Check for timeout
381 cur_time = time.time()
382 if cur_time - start_time > timeout:
383 raise RuntimeError(f"Barrier timed out after {timeout:.2f} seconds")
384
385 # Check for each process
386 for i in range(self.world_size):
387 if i in processes_arrived:
388 continue
389
390 key = f"arrival_{barrier_id}_{i}"
391 try:
392 # Try to get the key - if it exists, we'll get a value
393 # If it doesn't exist, it will throw an exception
394 self.store.get(key)
395 processes_arrived.add(i)
396 except KeyError:
397 # Key doesn't exist yet
398 pass
399 except Exception as check_e:
400 logger.debug("Error checking key existence: %s", check_e)
401 sched_yield()
402
403 # Short sleep to avoid tight polling
404 if len(processes_arrived) < self.world_size:
405 sched_yield()
406
407 # Phase 2: Signal departure from barrier
408 # We only care to block at this stage in rank 0, which runs the
409 # server side of the TCPStore. We want to make sure that all
410 # clients have departed the barrier before rank 0 in case the
411 # next thing after the barrier is a shutdown, including tearing
412 # down the TCPStore. Other ranks can exit the barrier immediately
413 # after signaling their departure.
414 departure_key = f"departure_{barrier_id}_{self.rank}"
415 try:
416 self.store.set(departure_key, b"1")
417 except Exception as e:
418 raise RuntimeError("Failed to signal barrier departure") from e
419
420 if self.rank != 0:
421 return
422
423 # Make rank 0 wait for all processes to signal departure
424 start_time = time.time()
425 processes_departed: set[int] = set()
426
427 while len(processes_departed) < self.world_size:
428 # Check for timeout
429 if time.time() - start_time > timeout:
430 raise RuntimeError(
431 f"Barrier departure timed out after {timeout:.2f} seconds"
432 )
433
434 # Check for each process
435 for i in range(self.world_size):
436 if i in processes_departed:
437 continue
438
439 key = f"departure_{barrier_id}_{i}"
440 try:
441 # Try to get the key - if it exists, we'll get a value
442 # If it doesn't exist, it will throw an exception
443 self.store.get(key)
444 processes_departed.add(i)
445 except KeyError:
446 # Key doesn't exist yet
447 pass
448 except Exception as check_e:
449 logger.debug("Error checking key existence: %s", check_e)
450 sched_yield()
451
452 # Short sleep to avoid tight polling
453 if len(processes_departed) < self.world_size:
454 sched_yield()
455
456 # Clean up keys to avoid leaking memory in the store
457 for i in range(self.world_size):
458 try:
459 self.store.delete_key(f"arrival_{barrier_id}_{i}")
460 except Exception:
461 logger.debug("Error deleting key: %s", f"arrival_{barrier_id}_{i}")
462
463 try:
464 self.store.delete_key(f"departure_{barrier_id}_{i}")
465 except Exception:
466 logger.debug("Error deleting key: %s", f"departure_{barrier_id}_{i}")
467
468 @staticmethod
469 def create(
470 host: str,
471 port: int,
472 rank: int,
473 world_size: int,
474 data_expiration_seconds: int = 3600,
475 store_timeout: int = 300,
476 listen_socket: socket.socket | None = None,
477 ) -> "StatelessProcessGroup":
478 """A replacement for `torch.distributed.init_process_group` that does not
479 pollute the global state.
480
481 If we have process A and process B called `torch.distributed.init_process_group`
482 to form a group, and then we want to form another group with process A, B, C,
483 D, it is not possible in PyTorch, because process A and process B have already
484 formed a group, and process C and process D cannot join that group. This
485 function is a workaround for this issue.
486
487 `torch.distributed.init_process_group` is a global call, while this function
488 is a stateless call. It will return a `StatelessProcessGroup` object that can be
489 used for exchanging metadata. With this function, process A and process B
490 can call `StatelessProcessGroup.create` to form a group, and then process A, B,
491 C, and D can call `StatelessProcessGroup.create` to form another group.
492 """ # noqa
493 launch_server = rank == 0
494 if launch_server and listen_socket is None:
495 listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
496 listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
497 listen_socket.bind((host, port))
498 listen_socket.listen()
499 store = create_tcp_store(
500 host,
501 port,
502 listen_socket=listen_socket,
503 world_size=world_size,
504 is_master=launch_server,
505 timeout=timedelta(seconds=store_timeout),
506 use_libuv=False, # for now: github.com/pytorch/pytorch/pull/150215
507 )
508
509 return StatelessProcessGroup(
510 rank=rank,
511 world_size=world_size,
512 store=store,
513 data_expiration_seconds=data_expiration_seconds,
514 )
515
516
517 @functools.lru_cache(maxsize=1)
518 def get_cached_tcp_store_client(host: str, port: int) -> TCPStore:
519 """Return a cached TCPStore client.
520
521 Cached so that every call with the same ``(host, port)`` reuses the
522 same connection. A new ``(host, port)`` evicts the old entry.
523 """
524 return TCPStore(host, port, is_master=False, wait_for_workers=False)
525
526
527 def get_cpu_distributed_timeout_or_none() -> timedelta | None:
528 from vllm.config import get_current_vllm_config_or_none
529
530 vllm_config = get_current_vllm_config_or_none()
531 if vllm_config is None:
532 return None
533 return vllm_config.parallel_config.cpu_distributed_timeout
534
535
536 def get_distributed_timeout_or_none() -> timedelta | None:
537 from vllm.config import get_current_vllm_config_or_none
538
539 vllm_config = get_current_vllm_config_or_none()
540 if vllm_config is None:
541 return None
542 timeout_seconds = vllm_config.parallel_config.distributed_timeout_seconds
543 return timedelta(seconds=timeout_seconds) if timeout_seconds is not None else None
544
545
546 def init_gloo_process_group(
547 prefix_store: PrefixStore,
548 group_rank: int,
549 group_size: int,
550 timeout: timedelta,
551 ) -> ProcessGroup:
552 """Stateless init ProcessGroup with gloo backend compatible with
553 different torch versions.
554 """
555 with suppress_stdout():
556 pg = ProcessGroup(
557 prefix_store,
558 group_rank,
559 group_size,
560 )
561 from torch.distributed.distributed_c10d import ProcessGroupGloo
562
563 backend_class = ProcessGroupGloo(
564 prefix_store, group_rank, group_size, timeout=timeout
565 )
566 backend_type = ProcessGroup.BackendType.GLOO
567 device = torch.device("cpu")
568 pg._set_default_backend(backend_type)
569 backend_class._set_sequence_number_for_group()
570
571 pg._register_backend(device, backend_type, backend_class)
572 return pg
573
574
575 def stateless_init_torch_distributed_process_group(
576 host: str,
577 port: int,
578 rank: int,
579 world_size: int,
580 backend: str,
581 group_name: str | None = None,
582 return_store: bool = False,
583 listen_socket: socket.socket | None = None,
584 timeout: timedelta | None = None,
585 ) -> ProcessGroup | tuple[ProcessGroup, Store]:
586 """A replacement for `torch.distributed.init_process_group` that does not
587 pollute the global state. The created ProcessGroup object can be used for
588 some operations such as `allreduce`, because it does not depend on the
589 global rank. However, some operations such as `broadcast` cannot be used
590 because it depends on the global rank.
591
592 # TODO: ask for help from PyTorch team if we need the `broadcast` operation.
593
594 This function is useful when we are not sure about the total number of
595 processes in the process group. For example, we may have process
596 1, 2, ..., 8 who want to communicate, and process 9 might be the same
597 process as process 1, or it might be a different process; process 10
598 might be the same process as process 5, or it might be a different process.
599 In this case, how can we reliably form a communication channel within
600 process 9 and 10, without affecting the communication channel within
601 process 1, 2, ..., 8?
602
603 One possible solution is to figure out if process 9 and 10 are the same
604 as process 1 and 5 beforehand, and then form a communication channel
605 based on the information, adjusting the ranks and world_size etc. However,
606 figuring out the information is not always easy, and it will interfere
607 with the main communication channel.
608
609 Our solution is to always form a communication channel with process 1, 2,
610 ..., 8, and then use this function to form another communication channel
611 with process 9 and 10. This way, regardless of whether process 9 and 10
612 are the same as process 1 and 5, the main communication channel is
613 always formed with process 1, 2, ..., 8, and the additional communication
614 channel is formed with process 9 and 10.
615
616 When *listen_socket* is provided, the rendezvous step
617 is skipped and a ``TCPStore`` server is created directly using the
618 pre-bound socket. This is useful for eliminating TOCTOU races
619 between port allocation and binding.
620
621 If *timeout* is None, use PyTorch's default for the backend.
622 """
623 init_method = get_tcp_uri(host, port)
624 backend = Backend(backend) # it is basically string
625 if timeout is None:
626 timeout = _get_default_timeout(backend)
627
628 if listen_socket is not None:
629 store = create_tcp_store(
630 host,
631 port,
632 listen_socket=listen_socket,
633 world_size=world_size,
634 is_master=True,
635 timeout=timeout,
636 multi_tenant=True,
637 )
638 else:
639 store, rank, world_size = next(
640 rendezvous(init_method, rank, world_size, timeout=timeout)
641 )
642 store.set_timeout(timeout)
643
644 group_rank = rank
645 group_size = world_size
646
647 # Use a PrefixStore to avoid accidental overrides of keys used by
648 # different systems (e.g. RPC) in case the store is multi-tenant.
649 prefix_store = PrefixStore(init_method, store)
650
651 if backend == "gloo":
652 pg = init_gloo_process_group(
653 prefix_store=prefix_store,
654 group_rank=group_rank,
655 group_size=group_size,
656 timeout=timeout,
657 )
658 else:
659 from vllm.platforms import current_platform
660
661 pg = current_platform.stateless_init_device_torch_dist_pg(
662 backend=backend,
663 prefix_store=prefix_store,
664 group_rank=group_rank,
665 group_size=group_size,
666 timeout=timeout,
667 )
668
669 if group_name is not None:
670 from torch._C._distributed_c10d import _register_process_group
671
672 pg._set_group_name(group_name)
673 _register_process_group(group_name, pg)
674
675 if return_store:
676 return pg, store
677 else:
678 return pg
679
680
681 def stateless_destroy_torch_distributed_process_group(pg: ProcessGroup) -> None:
682 """Destroy ProcessGroup returned by
683 stateless_init_torch_distributed_process_group().
684 """
685 pg.shutdown()
686 _unregister_process_group(pg.group_name)
687
688
689 def get_worker_rank_suffix(global_rank: int | None = None) -> str:
690 """Generate a descriptive rank suffix for worker identification.
691
692 Returns a string like 'dp0_pp0_tp0_dcp0_ep0_rank0' including all
693 parallel dimensions: DP, PP, TP, DCP, EP.
694
695 Args:
696 global_rank: Optional global rank to append. If not provided,
697 only parallel dimension ranks are included.
698
699 Returns:
700 A string suffix identifying the worker's position in the
701 distributed topology.
702
703 """
704 from vllm.distributed.parallel_state import (
705 get_dcp_group,
706 get_dp_group,
707 get_ep_group,
708 get_pp_group,
709 get_tp_group,
710 )
711
712 try:
713 dp_rank = get_dp_group().rank_in_group
714 pp_rank = get_pp_group().rank_in_group
715 tp_rank = get_tp_group().rank_in_group
716 dcp_rank = get_dcp_group().rank_in_group
717 ep_rank = get_ep_group().rank_in_group
718
719 suffix = f"dp{dp_rank}_pp{pp_rank}_tp{tp_rank}_dcp{dcp_rank}_ep{ep_rank}"
720 if global_rank is not None:
721 suffix = f"{suffix}_rank{global_rank}"
722 return suffix
723 except Exception:
724 # Fallback if parallel state not initialized
725 if global_rank is not None:
726 return f"rank{global_rank}"
727 return ""