Stop setting CUDA_VISIBLE_DEVICES internally in vLLM, add device_ids arg (#45026)
Signed-off-by: Tyler Michael Smith <tlrmchlsmth@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Codex <codex@openai.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: kourosh hakhamaneshi <kouroshHakha@users.noreply.github.com>
Tyler Michael Smith committed
Jun 20, 2026 at 16:38 UTC
ebfbcfe46aa895d428933244908bae08b1ca6397
24 files changed
+722
-185
csrc/libtorch_stable/attention/mla/sm100_cutlass_mla_kernel.cu
+8
-3
@@ -268,9 +268,14 @@ int64_t sm100_cutlass_mla_get_workspace_size(int64_t max_seq_len, int64_t num_ba
268
using TileShapeD = typename MlaSm100Type::TileShapeD;
269
arguments.problem_shape =
270
cute::make_tuple(TileShapeH{}, static_cast<int>(max_seq_len), TileShapeD{}, static_cast<int>(num_batches));
271
- // Assumes device 0 when getting sm_count.
272
- arguments.hw_info.sm_count =
273
- sm_count <= 0 ? cutlass::KernelHardwareInfo::query_device_multiprocessor_count(/*device_id=*/0) : sm_count;
271
+ if (sm_count <= 0) {
272
+ int current_device = 0;
273
+ cudaGetDevice(¤t_device);
274
+ arguments.hw_info.sm_count =
275
+ cutlass::KernelHardwareInfo::query_device_multiprocessor_count(current_device);
276
+ } else {
277
+ arguments.hw_info.sm_count = sm_count;
278
+ }
279
arguments.split_kv = static_cast<int>(num_kv_splits);
280
MlaSm100Type::Fmha::set_split_kv(arguments);
281
tests/engine/test_arg_utils.py
+193
@@ -649,3 +649,196 @@ def test_cloud_storage_tokenizer_skips_get_model_path(monkeypatch):
649
args = EngineArgs(model="s3://bucket/model", tokenizer="s3://bucket/tokenizer")
650
assert args.model == "s3://bucket/model"
651
assert args.tokenizer == "s3://bucket/tokenizer"
652
+
653
+
654
+class TestDeviceIds:
655
+ def test_device_ids_with_cvd_out_of_range(self, monkeypatch):
656
+ """--device-ids index beyond the CVD set raises ValueError."""
657
+ from vllm.platforms import current_platform
658
+
659
+ key = current_platform.device_control_env_var
660
+ monkeypatch.setenv(key, "4,5")
661
+ args = EngineArgs(model="m", device_ids=[0, 2])
662
+ with pytest.raises(ValueError, match="out of range"):
663
+ args._resolve_device_ids()
664
+
665
+ def test_device_ids_with_cvd_resolve_to_physical_ids(self, monkeypatch):
666
+ """--device-ids are CVD-local indices resolved to physical ids."""
667
+ from vllm.platforms import current_platform
668
+
669
+ key = current_platform.device_control_env_var
670
+ monkeypatch.setenv(key, "4,5")
671
+ args = EngineArgs(model="m", device_ids=[0, 1])
672
+ assert args._resolve_device_ids() == [4, 5]
673
+
674
+ def test_device_ids_with_uuid_cvd_resolve_to_physical_ids(self, monkeypatch):
675
+ """--device-ids support UUID CVD values resolved by the platform."""
676
+ from vllm.platforms import current_platform
677
+
678
+ key = current_platform.device_control_env_var
679
+ monkeypatch.setenv(key, "GPU-abcd1234,GPU-ef567890")
680
+ monkeypatch.setattr(
681
+ type(current_platform),
682
+ "device_control_id_to_physical_device_id",
683
+ classmethod(
684
+ lambda cls, device_id: {"GPU-abcd1234": 4, "GPU-ef567890": 5}[device_id]
685
+ ),
686
+ )
687
+
688
+ args = EngineArgs(model="m", device_ids=[0, 1])
689
+ assert args._resolve_device_ids() == [4, 5]
690
+
691
+ def test_device_ids_with_uuid_args_resolve_to_physical_ids(self, monkeypatch):
692
+ """UUID --device-ids are resolved to physical IDs immediately."""
693
+ from vllm.platforms import current_platform
694
+
695
+ monkeypatch.setattr(
696
+ type(current_platform),
697
+ "device_control_id_to_physical_device_id",
698
+ classmethod(lambda cls, device_id: {"GPU-abcd1234": 4}[device_id]),
699
+ )
700
+
701
+ args = EngineArgs(model="m", device_ids=["GPU-abcd1234"])
702
+ assert args._resolve_device_ids() == [4]
703
+
704
+ def test_device_ids_reject_mixed_integer_and_uuid_args(self):
705
+ """--device-ids must not mix CVD indices and UUIDs."""
706
+ args = EngineArgs(model="m", device_ids=[0, "GPU-abcd1234"])
707
+ with pytest.raises(ValueError, match="must not mix"):
708
+ args._resolve_device_ids()
709
+
710
+ def test_no_device_ids(self):
711
+ """No --device-ids returns None."""
712
+ args = EngineArgs(model="m")
713
+ assert args._resolve_device_ids() is None
714
+
715
+ def test_cli_parsing(self):
716
+ """--device-ids parses comma-separated string from CLI."""
717
+ parser = FlexibleArgumentParser()
718
+ EngineArgs.add_cli_args(parser)
719
+ parsed = parser.parse_args(["--model", "m", "--device-ids", "0,2,4"])
720
+ assert parsed.device_ids == [0, 2, 4]
721
+
722
+ def test_cli_parsing_uuid(self):
723
+ """--device-ids parses comma-separated UUID strings from CLI."""
724
+ parser = FlexibleArgumentParser()
725
+ EngineArgs.add_cli_args(parser)
726
+ parsed = parser.parse_args(
727
+ ["--model", "m", "--device-ids", "GPU-abcd1234,GPU-ef567890"]
728
+ )
729
+ assert parsed.device_ids == ["GPU-abcd1234", "GPU-ef567890"]
730
+
731
+ def test_assigned_physical_gpu_ids_are_physical_with_cvd(self, monkeypatch):
732
+ """assigned_physical_gpu_ids are already physical and not composed with CVD."""
733
+ import vllm.platforms.interface as platform_interface
734
+ from vllm.platforms import current_platform
735
+
736
+ monkeypatch.setattr(platform_interface, "_assigned_physical_gpu_ids", [4, 5])
737
+ monkeypatch.setenv(current_platform.device_control_env_var, "4,5")
738
+
739
+ assert current_platform.device_id_to_physical_device_id(0) == 4
740
+ assert current_platform.device_id_to_physical_device_id(1) == 5
741
+ assert current_platform.logical_device_id_to_visible_device_id(0) == 0
742
+ assert current_platform.logical_device_id_to_visible_device_id(1) == 1
743
+
744
+ def test_assigned_physical_gpu_ids_map_to_visible_uuid_cvd(self, monkeypatch):
745
+ """Physical IDs map back to visible ordinals when CVD uses UUIDs."""
746
+ import vllm.platforms.interface as platform_interface
747
+ from vllm.platforms import current_platform
748
+
749
+ monkeypatch.setattr(platform_interface, "_assigned_physical_gpu_ids", [5])
750
+ monkeypatch.setenv(
751
+ current_platform.device_control_env_var,
752
+ "GPU-abcd1234,GPU-ef567890",
753
+ )
754
+ monkeypatch.setattr(
755
+ type(current_platform),
756
+ "device_control_id_to_physical_device_id",
757
+ classmethod(
758
+ lambda cls, device_id: {"GPU-abcd1234": 4, "GPU-ef567890": 5}[device_id]
759
+ ),
760
+ )
761
+
762
+ assert current_platform.logical_device_id_to_visible_device_id(0) == 1
763
+
764
+ def test_device_ids_reject_duplicates(self):
765
+ """--device-ids must not contain duplicate entries."""
766
+ args = EngineArgs(model="m", device_ids=[2, 2])
767
+ with pytest.raises(ValueError, match="duplicates"):
768
+ args._resolve_device_ids()
769
+
770
+ def test_cli_parsing_strips_whitespace(self):
771
+ """--device-ids tolerates whitespace around commas."""
772
+ parser = FlexibleArgumentParser()
773
+ EngineArgs.add_cli_args(parser)
774
+ parsed = parser.parse_args(["--model", "m", "--device-ids", "0, 2, 4"])
775
+ assert parsed.device_ids == [0, 2, 4]
776
+
777
+ def test_visible_ordinal_to_physical_ignores_assigned_ids(self, monkeypatch):
778
+ """visible_device_id_to_physical_device_id maps torch device ordinals,
779
+ independent of the logical-to-physical mapping.
780
+
781
+ Regression test: CustomAllreduce passes device.index (a visible
782
+ ordinal) and must not index into assigned_physical_gpu_ids, which
783
+ raised IndexError for non-identity --device-ids like [2, 3].
784
+ """
785
+ import vllm.platforms.interface as platform_interface
786
+ from vllm.platforms import current_platform
787
+
788
+ monkeypatch.setattr(platform_interface, "_assigned_physical_gpu_ids", [2, 3])
789
+ monkeypatch.delenv(current_platform.device_control_env_var, raising=False)
790
+
791
+ # CVD unset: visible ordinal == physical ID, even beyond the
792
+ # assigned list's length.
793
+ assert current_platform.visible_device_id_to_physical_device_id(2) == 2
794
+ assert current_platform.visible_device_id_to_physical_device_id(3) == 3
795
+
796
+ monkeypatch.setenv(current_platform.device_control_env_var, "4,5")
797
+ assert current_platform.visible_device_id_to_physical_device_id(1) == 5
798
+ with pytest.raises(IndexError, match="out of range"):
799
+ current_platform.visible_device_id_to_physical_device_id(2)
800
+
801
+
802
+class TestDpDeviceIdSharding:
803
+ def test_dp_supervisor_device_ids_stay_env_relative(self):
804
+ """Regression test: the DP supervisor must pass env-relative indices,
805
+ not physical IDs, because each child re-resolves --device-ids
806
+ against its inherited device-control env var."""
807
+ import argparse
808
+
809
+ from vllm.entrypoints.openai.dp_supervisor import _build_device_ids
810
+
811
+ args = argparse.Namespace(
812
+ tensor_parallel_size=2, pipeline_parallel_size=1, device_ids=None
813
+ )
814
+ assert _build_device_ids(args, local_rank=0) == [0, 1]
815
+ assert _build_device_ids(args, local_rank=1) == [2, 3]
816
+
817
+ def test_dp_supervisor_shards_user_device_ids(self):
818
+ """User-provided --device-ids are sharded across DP children."""
819
+ import argparse
820
+
821
+ from vllm.entrypoints.openai.dp_supervisor import _build_device_ids
822
+
823
+ args = argparse.Namespace(
824
+ tensor_parallel_size=2, pipeline_parallel_size=1, device_ids=[4, 5, 6, 7]
825
+ )
826
+ assert _build_device_ids(args, local_rank=0) == [4, 5]
827
+ assert _build_device_ids(args, local_rank=1) == [6, 7]
828
+ with pytest.raises(ValueError, match="needs devices"):
829
+ _build_device_ids(args, local_rank=2)
830
+
831
+ def test_dp_rank_shards_user_assigned_gpu_ids(self):
832
+ """get_physical_gpu_ids_for_local_dp_rank slices the user-provided
833
+ --device-ids list instead of recomputing from the env var."""
834
+ from vllm.platforms import current_platform
835
+ from vllm.v1.engine.utils import get_physical_gpu_ids_for_local_dp_rank
836
+
837
+ evar = current_platform.device_control_env_var
838
+ assert get_physical_gpu_ids_for_local_dp_rank(
839
+ evar, local_dp_rank=1, world_size=2, user_assigned_gpu_ids=[4, 5, 6, 7]
840
+ ) == [6, 7]
841
+ with pytest.raises(ValueError, match="needs devices"):
842
+ get_physical_gpu_ids_for_local_dp_rank(
843
+ evar, local_dp_rank=2, world_size=2, user_assigned_gpu_ids=[4, 5, 6, 7]
844
+ )
tests/entrypoints/openai/test_dp_supervisor.py
+2
-2
@@ -364,7 +364,7 @@ class MockVLLMServer:
364
await self._serve_task
365
366
367
-def launch_mock_vllm(child_args: argparse.Namespace, env_updates: dict[str, str]):
367
+def launch_mock_vllm(child_args: argparse.Namespace):
368
logger.info("Launching mock vLLM on port %s", child_args.port)
369
mock_vllm = MockVLLMServer(
370
port=child_args.port,
@@ -375,7 +375,7 @@ def launch_mock_vllm(child_args: argparse.Namespace, env_updates: dict[str, str]
375
376
377
def launch_mock_vllm_with_drain(
378
- child_args: argparse.Namespace, env_updates: dict[str, str]
378
+ child_args: argparse.Namespace,
379
):
380
logger.info("Launching mock vLLM with 15s drain on port %s", child_args.port)
381
mock_vllm = MockVLLMServer(
vllm/config/parallel.py
+9
@@ -302,6 +302,14 @@ class ParallelConfig:
302
Each entry must use `numactl --physcpubind` CPU-list syntax, for example
303
`"0-3"` or `"0,2,4-7"`.
304
"""
305
+ assigned_physical_gpu_ids: list[int] | None = None
306
+ """Mapping from vLLM-local logical GPU IDs to physical GPU IDs.
307
+
308
+ For example, ``[2, 3]`` means logical GPU 0 maps to physical GPU 2,
309
+ and logical GPU 1 maps to physical GPU 3. Physical IDs are used only
310
+ at platform/topology boundaries such as NVML, NIC affinity, P2P
311
+ checks, and final CUDA device selection when needed. When None,
312
+ logical IDs map to visible device IDs in order."""
313
314
distributed_timeout_seconds: int | None = None
315
"""Timeout in seconds for distributed operations (e.g., init_process_group).
@@ -772,6 +780,7 @@ class ParallelConfig:
780
"numa_bind",
781
"numa_bind_nodes",
782
"numa_bind_cpus",
783
+ "assigned_physical_gpu_ids",
784
}
785
786
from vllm.config.utils import get_hash_factors, hash_factors
vllm/distributed/device_communicators/all2all.py
+8
-1
@@ -704,7 +704,14 @@ class FlashInferNVLinkOneSidedManager(All2AllManagerBase):
704
self.num_experts = num_experts
705
706
self.cleanup()
707
- gpus_per_node = torch.accelerator.device_count()
707
+ from vllm.platforms.interface import get_assigned_physical_gpu_ids
708
+
709
+ assigned_physical_gpu_ids = get_assigned_physical_gpu_ids()
710
+ gpus_per_node = (
711
+ len(assigned_physical_gpu_ids)
712
+ if assigned_physical_gpu_ids is not None
713
+ else torch.accelerator.device_count()
714
+ )
715
logger.debug(
716
"Making One-sided NVLink mapping: rank=%d, world size=%d",
717
self.rank,
vllm/distributed/device_communicators/all_reduce_utils.py
+26
-7
@@ -320,13 +320,21 @@ def gpu_p2p_access_check(src: int, tgt: int) -> bool:
320
321
is_distributed = dist.is_initialized()
322
323
- num_dev = current_platform.device_count()
324
- cuda_visible_devices = envs.CUDA_VISIBLE_DEVICES
325
- if cuda_visible_devices is None:
326
- cuda_visible_devices = ",".join(str(i) for i in range(num_dev))
323
+ from vllm.platforms.interface import get_assigned_physical_gpu_ids
324
+
325
+ assigned_physical_gpu_ids = get_assigned_physical_gpu_ids()
326
+ if assigned_physical_gpu_ids is not None:
327
+ # Key by the ordered list: the cache stores directed local-index
328
+ # pairs, so permutations of the same set are distinct mappings.
329
+ cache_key = ",".join(str(i) for i in assigned_physical_gpu_ids)
330
+ num_dev = len(assigned_physical_gpu_ids)
331
+ else:
332
+ num_dev = current_platform.device_count()
333
+ cuda_visible_devices = envs.CUDA_VISIBLE_DEVICES
334
+ cache_key = cuda_visible_devices or ",".join(str(i) for i in range(num_dev))
335
336
path = os.path.join(
329
- envs.VLLM_CACHE_ROOT, f"gpu_p2p_access_cache_for_{cuda_visible_devices}.json"
337
+ envs.VLLM_CACHE_ROOT, f"gpu_p2p_access_cache_for_{cache_key}.json"
338
)
339
os.makedirs(os.path.dirname(path), exist_ok=True)
340
from vllm.distributed.parallel_state import get_world_group
@@ -338,7 +346,15 @@ def gpu_p2p_access_check(src: int, tgt: int) -> bool:
346
# enter this block to calculate the cache
347
logger.info("generating GPU P2P access cache in %s", path)
348
cache: dict[str, bool] = {}
341
- ids = list(range(num_dev))
349
+ # The probe subprocesses inherit this process's device-control env
350
+ # var, so they must be given visible ordinals, not physical IDs.
351
+ if assigned_physical_gpu_ids is not None:
352
+ ids = [
353
+ current_platform.logical_device_id_to_visible_device_id(local)
354
+ for local in range(num_dev)
355
+ ]
356
+ else:
357
+ ids = list(range(num_dev))
358
# batch of all pairs of GPUs
359
batch_src, batch_tgt = zip(*list(product(ids, ids)))
360
# NOTE: we use `subprocess` rather than `multiprocessing` here
@@ -368,8 +384,11 @@ def gpu_p2p_access_check(src: int, tgt: int) -> bool:
384
) from e
385
with open(output_file.name, "rb") as f:
386
result = pickle.load(f)
387
+ # Cache entries must be keyed by local indices (0..N-1) because
388
+ # gpu_p2p_access_check() is called with local ranks.
389
+ id_to_local = {device_id: local for local, device_id in enumerate(ids)}
390
for _i, _j, r in zip(batch_src, batch_tgt, result):
372
- cache[f"{_i}->{_j}"] = r
391
+ cache[f"{id_to_local[_i]}->{id_to_local[_j]}"] = r
392
with open(path, "w") as f:
393
json.dump(cache, f, indent=4)
394
if is_distributed:
vllm/distributed/device_communicators/custom_all_reduce.py
+10
-8
@@ -34,7 +34,12 @@ def _can_p2p(rank: int, world_size: int) -> bool:
34
continue
35
if envs.VLLM_SKIP_P2P_CHECK:
36
logger.debug("Skipping P2P check and trusting the driver's P2P report.")
37
- return torch.cuda.can_device_access_peer(rank, i)
37
+ # can_device_access_peer takes visible device ordinals, while
38
+ # rank and i are logical local IDs.
39
+ return torch.cuda.can_device_access_peer(
40
+ current_platform.logical_device_id_to_visible_device_id(rank),
41
+ current_platform.logical_device_id_to_visible_device_id(i),
42
+ )
43
if not gpu_p2p_access_check(rank, i):
44
return False
45
return True
@@ -126,13 +131,10 @@ class CustomAllreduce:
131
CUSTOM_ALL_REDUCE_MAX_SIZES[device_capability_str][world_size],
132
max_size,
133
)
129
- cuda_visible_devices = envs.CUDA_VISIBLE_DEVICES
130
- if cuda_visible_devices:
131
- device_ids = list(map(int, cuda_visible_devices.split(",")))
132
- else:
133
- device_ids = list(range(current_platform.device_count()))
134
-
135
- physical_device_id = device_ids[device.index]
134
+ # device.index is a visible ordinal, not a logical local ID.
135
+ physical_device_id = current_platform.visible_device_id_to_physical_device_id(
136
+ device.index
137
+ )
138
tensor = torch.tensor([physical_device_id], dtype=torch.int, device="cpu")
139
gather_list = [
140
torch.tensor([0], dtype=torch.int, device="cpu") for _ in range(world_size)
vllm/distributed/device_communicators/quick_all_reduce.py
+4
-6
@@ -129,12 +129,10 @@ class QuickAllReduce:
129
assert isinstance(device, torch.device)
130
self.device = device
131
132
- cuda_visible_devices = envs.CUDA_VISIBLE_DEVICES
133
- if cuda_visible_devices:
134
- device_ids = list(map(int, cuda_visible_devices.split(",")))
135
- else:
136
- device_ids = list(range(current_platform.device_count()))
137
- physical_device_id = device_ids[device.index]
132
+ # device.index is a visible ordinal, not a logical local ID.
133
+ physical_device_id = current_platform.visible_device_id_to_physical_device_id(
134
+ device.index
135
+ )
136
tensor = torch.tensor([physical_device_id], dtype=torch.int, device="cpu")
137
gather_list = [
138
torch.tensor([0], dtype=torch.int, device="cpu")
vllm/distributed/device_communicators/shm_broadcast.py
+7
-1
@@ -840,7 +840,13 @@ class MessageQueue:
840
The MessageQueue instance for the calling process,
841
and a list of handles (only non-empty for the reader process).
842
"""
843
- local_size = current_platform.device_count()
843
+ from vllm.platforms.interface import get_assigned_physical_gpu_ids
844
+
845
+ assigned_physical_gpu_ids = get_assigned_physical_gpu_ids()
846
+ if assigned_physical_gpu_ids is not None:
847
+ local_size = len(assigned_physical_gpu_ids)
848
+ else:
849
+ local_size = current_platform.device_count()
850
rank = dist.get_rank()
851
same_node = rank // local_size == reader_rank // local_size
852
buffer_io = MessageQueue(
vllm/distributed/kv_transfer/kv_connector/v1/lmcache_integration/vllm_v1_adapter.py
+5
-4
@@ -482,10 +482,11 @@ def _init_lmcache_engine(
482
)
483
484
# Change current device.
485
- num_gpus = torch.accelerator.device_count()
486
- local_rank = parallel_config.rank % num_gpus
487
- torch.accelerator.set_device_index(local_rank)
488
- device = torch.device(f"cuda:{local_rank}")
485
+ from vllm.distributed.parallel_state import get_world_group
486
+
487
+ device_index = get_world_group().device_index
488
+ torch.accelerator.set_device_index(device_index)
489
+ device = torch.device(f"cuda:{device_index}")
490
metadata = LMCacheEngineMetadata(
491
model_config.model,
492
parallel_config.world_size,
vllm/distributed/parallel_state.py
+24
-4
@@ -392,6 +392,14 @@ class GroupCoordinator:
392
393
self.rank = torch.distributed.get_rank()
394
self.local_rank = local_rank
395
+ self.device_index: int
396
+ if _WORLD is not None:
397
+ self.device_index = _WORLD.device_index
398
+ else:
399
+ assert local_rank >= 0, (
400
+ "local_rank must be provided when creating the world group"
401
+ )
402
+ self.device_index = local_rank
403
404
self_device_group = None
405
self_cpu_group = None
@@ -442,11 +450,18 @@ class GroupCoordinator:
450
from vllm.platforms import current_platform
451
452
if current_platform.is_cuda_alike():
445
- self.device = torch.device(f"cuda:{local_rank}")
453
+ visible_device_index = (
454
+ current_platform.logical_device_id_to_visible_device_id(
455
+ self.device_index
456
+ )
457
+ )
458
+ self.device = torch.device(f"cuda:{visible_device_index}")
459
elif current_platform.is_xpu():
447
- self.device = torch.device(f"xpu:{local_rank}")
460
+ self.device = torch.device(f"xpu:{self.device_index}")
461
elif current_platform.is_out_of_tree():
449
- self.device = torch.device(f"{current_platform.device_name}:{local_rank}")
462
+ self.device = torch.device(
463
+ f"{current_platform.device_name}:{self.device_index}"
464
+ )
465
else:
466
self.device = torch.device("cpu")
467
@@ -1438,7 +1453,12 @@ def _init_process_group_for_split_group(
1453
"""
1454
if torch.accelerator.is_available() and backend != "gloo":
1455
init_backend = "cpu:gloo,cuda:nccl"
1441
- device_id: torch.device | None = torch.device(f"cuda:{local_rank}")
1456
+ from vllm.platforms import current_platform
1457
+
1458
+ visible_device_index = current_platform.logical_device_id_to_visible_device_id(
1459
+ local_rank
1460
+ )
1461
+ device_id: torch.device | None = torch.device(f"cuda:{visible_device_index}")
1462
else:
1463
init_backend = "gloo"
1464
device_id = None
vllm/distributed/stateless_coordinator.py
+19
-3
@@ -86,6 +86,15 @@ class StatelessGroupCoordinator(GroupCoordinator):
86
87
self.rank = global_rank
88
self.local_rank = local_rank
89
+ from vllm.distributed.parallel_state import _WORLD
90
+
91
+ if _WORLD is not None:
92
+ self.device_index = _WORLD.device_index
93
+ else:
94
+ assert local_rank >= 0, (
95
+ "local_rank must be provided when creating the world group"
96
+ )
97
+ self.device_index = local_rank
98
99
self_device_group = None
100
self_cpu_group = None
@@ -152,11 +161,18 @@ class StatelessGroupCoordinator(GroupCoordinator):
161
self.tcp_store_group = self_tcp_store_group
162
163
if current_platform.is_cuda_alike():
155
- self.device = torch.device(f"cuda:{local_rank}")
164
+ visible_device_index = (
165
+ current_platform.logical_device_id_to_visible_device_id(
166
+ self.device_index
167
+ )
168
+ )
169
+ self.device = torch.device(f"cuda:{visible_device_index}")
170
elif current_platform.is_xpu():
157
- self.device = torch.device(f"xpu:{local_rank}")
171
+ self.device = torch.device(f"xpu:{self.device_index}")
172
elif current_platform.is_out_of_tree():
159
- self.device = torch.device(f"{current_platform.device_name}:{local_rank}")
173
+ self.device = torch.device(
174
+ f"{current_platform.device_name}:{self.device_index}"
175
+ )
176
else:
177
self.device = torch.device("cpu")
178
vllm/engine/arg_utils.py
+58
@@ -6,6 +6,7 @@ import copy
6
import dataclasses
7
import functools
8
import json
9
+import os
10
import sys
11
from collections.abc import Callable
12
from dataclasses import MISSING, asdict, dataclass, fields, is_dataclass
@@ -465,6 +466,7 @@ class EngineArgs:
466
numa_bind: bool = ParallelConfig.numa_bind
467
numa_bind_nodes: list[int] | None = ParallelConfig.numa_bind_nodes
468
numa_bind_cpus: list[str] | None = ParallelConfig.numa_bind_cpus
469
+ device_ids: list[int | str] | None = None
470
tensor_parallel_size: int = ParallelConfig.tensor_parallel_size
471
prefill_context_parallel_size: int = ParallelConfig.prefill_context_parallel_size
472
decode_context_parallel_size: int = ParallelConfig.decode_context_parallel_size
@@ -979,6 +981,20 @@ class EngineArgs:
981
parallel_group.add_argument(
982
"--numa-bind-cpus", **parallel_kwargs["numa_bind_cpus"]
983
)
984
+ parallel_group.add_argument(
985
+ "--device-ids",
986
+ type=lambda s: [
987
+ int(device_id) if device_id.isdigit() else device_id
988
+ for device_id in (part.strip() for part in s.split(","))
989
+ ],
990
+ default=None,
991
+ help="Comma-separated physical GPU device IDs or UUIDs to use "
992
+ '(e.g. --device-ids "2,3,5,7"). Avoids setting '
993
+ "CUDA_VISIBLE_DEVICES, preserving full GPU topology "
994
+ "visibility for GPU-NIC affinity and DeepGEMM. "
995
+ "Note: has no effect with Ray executors; use Ray "
996
+ "placement groups for GPU selection instead.",
997
+ )
998
parallel_group.add_argument(
999
"--tensor-parallel-size", "-tp", **parallel_kwargs["tensor_parallel_size"]
1000
)
@@ -1716,6 +1732,47 @@ class EngineArgs:
1732
)
1733
return SpeculativeConfig(**self.speculative_config)
1734
1735
+ def _resolve_device_ids(self) -> list[int] | None:
1736
+ if not self.device_ids:
1737
+ return None
1738
+ if self.distributed_executor_backend == "ray":
1739
+ logger.warning(
1740
+ "--device-ids has no effect when using the Ray executor. "
1741
+ "Use Ray placement groups for GPU selection instead."
1742
+ )
1743
+ ids = self.device_ids
1744
+ if len(set(ids)) != len(ids):
1745
+ raise ValueError(f"--device-ids must not contain duplicates: {ids}")
1746
+ if all(isinstance(i, str) for i in ids):
1747
+ return [
1748
+ current_platform.device_control_id_to_physical_device_id(i)
1749
+ for i in cast(list[str], ids)
1750
+ ]
1751
+ if any(isinstance(i, str) for i in ids):
1752
+ raise ValueError("--device-ids must not mix integer IDs and UUIDs")
1753
+ int_ids = cast(list[int], ids)
1754
+ # Compose with CUDA_VISIBLE_DEVICES: if CVD is set, treat
1755
+ # --device-ids values as indices into the CVD-visible set.
1756
+ cvd = getattr(
1757
+ envs,
1758
+ current_platform.device_control_env_var,
1759
+ os.environ.get(current_platform.device_control_env_var),
1760
+ )
1761
+ if cvd:
1762
+ cvd_ids = [
1763
+ current_platform.device_control_id_to_physical_device_id(x)
1764
+ for x in cvd.split(",")
1765
+ ]
1766
+ for i in int_ids:
1767
+ if i >= len(cvd_ids):
1768
+ raise ValueError(
1769
+ f"--device-ids index {i} is out of range for "
1770
+ f"{current_platform.device_control_env_var}"
1771
+ f"={cvd} ({len(cvd_ids)} devices visible)"
1772
+ )
1773
+ return [cvd_ids[i] for i in int_ids]
1774
+ return int_ids
1775
+
1776
def create_diffusion_config(self) -> DiffusionConfig | None:
1777
if self.diffusion_config is None:
1778
return None
@@ -2029,6 +2086,7 @@ class EngineArgs:
2086
cp_kv_cache_interleave_size=self.cp_kv_cache_interleave_size,
2087
_api_process_count=self._api_process_count,
2088
_api_process_rank=self._api_process_rank,
2089
+ assigned_physical_gpu_ids=self._resolve_device_ids(),
2090
numa_bind=self.numa_bind,
2091
numa_bind_nodes=self.numa_bind_nodes,
2092
numa_bind_cpus=self.numa_bind_cpus,
vllm/entrypoints/openai/dp_supervisor.py
+19
-18
@@ -23,12 +23,10 @@ import uvloop
23
from fastapi import FastAPI, Response
24
25
from vllm.logger import init_logger
26
-from vllm.platforms import current_platform
26
from vllm.utils.system_utils import (
27
decorate_logs,
28
kill_process_tree,
29
set_process_title,
31
- update_environment_variables,
30
)
31
32
logger = init_logger(__name__)
@@ -127,22 +125,29 @@ def _build_vllm_dp_server_args(
125
child_args.data_parallel_multi_port_external_lb = False
126
child_args.data_parallel_supervisor_port = None
127
child_args.api_server_count = 1
128
+ child_args.device_ids = _build_device_ids(args, local_rank)
129
return child_args
130
131
133
-def _build_vllm_dp_server_env(
134
- args: argparse.Namespace, local_rank: int
135
-) -> dict[str, str]:
136
- # set visible devices for the child process
132
+def _build_device_ids(args: argparse.Namespace, local_rank: int) -> list[int | str]:
133
+ """Build the --device-ids value for a DP child process.
134
+
135
+ The child resolves these against its own inherited device-control env
136
+ var (e.g. CUDA_VISIBLE_DEVICES), so integer IDs must stay env-relative
137
+ here rather than being translated to physical IDs.
138
+ """
139
devices_per_rank = args.tensor_parallel_size * args.pipeline_parallel_size
140
start = local_rank * devices_per_rank
141
stop = start + devices_per_rank
140
- device_env = current_platform.device_control_env_var
141
- visible_devices = ",".join(
142
- str(current_platform.device_id_to_physical_device_id(idx))
143
- for idx in range(start, stop)
144
- )
145
- return {device_env: visible_devices}
142
+ device_ids = getattr(args, "device_ids", None)
143
+ if device_ids is not None:
144
+ if stop > len(device_ids):
145
+ raise ValueError(
146
+ f"--device-ids has {len(device_ids)} entries, but DP rank "
147
+ f"{local_rank} needs devices [{start}, {stop})"
148
+ )
149
+ return device_ids[start:stop]
150
+ return list(range(start, stop))
151
152
153
def _child_base_url(args: argparse.Namespace, port: int) -> str:
@@ -228,9 +233,7 @@ def _build_dp_supervisor_app(supervisor: DPSupervisor) -> FastAPI:
233
return app
234
235
231
-def _run_vllm_dp_server(
232
- child_args: argparse.Namespace, env_updates: dict[str, str]
233
-) -> None:
236
+def _run_vllm_dp_server(child_args: argparse.Namespace) -> None:
237
"""
238
Entrypoint function for the vLLM DP Server.
239
"""
@@ -241,7 +244,6 @@ def _run_vllm_dp_server(
244
os.setpgrp()
245
246
name = f"APIServer_DP{child_args.data_parallel_rank}"
244
- update_environment_variables(env_updates)
247
set_process_title(name)
248
decorate_logs(name)
249
uvloop.run(run_server(child_args))
@@ -345,11 +347,10 @@ class DPSupervisor:
347
context = multiprocessing.get_context("spawn")
348
for local_rank in range(self.args.data_parallel_size_local):
349
child_args = _build_vllm_dp_server_args(self.args, local_rank)
348
- child_env = _build_vllm_dp_server_env(self.args, local_rank)
350
process = context.Process(
351
target=_run_vllm_dp_server,
352
name=f"APIServer_DPRank_{child_args.data_parallel_rank}",
352
- args=(child_args, child_env),
353
+ args=(child_args,),
354
)
355
process.start()
356
self._processes.append(process)
vllm/platforms/cuda.py
+9
@@ -685,6 +685,15 @@ class CudaPlatformBase(Platform):
685
# all the related functions work on real physical device ids.
686
# the major benefit of using NVML is that it will not initialize CUDA
687
class NvmlCudaPlatform(CudaPlatformBase):
688
+ @classmethod
689
+ @with_nvml_context
690
+ def device_control_id_to_physical_device_id(cls, device_id: str) -> int:
691
+ try:
692
+ return int(device_id)
693
+ except ValueError:
694
+ handle = pynvml.nvmlDeviceGetHandleByUUID(device_id)
695
+ return pynvml.nvmlDeviceGetIndex(handle)
696
+
697
@classmethod
698
@cache
699
@with_nvml_context
vllm/platforms/interface.py
+102
-1
@@ -30,6 +30,33 @@ else:
30
31
logger = init_logger(__name__)
32
33
+_assigned_physical_gpu_ids: list[int] | None = None
34
+
35
+
36
+def set_assigned_physical_gpu_ids(ids: list[int]) -> None:
37
+ """Set the physical GPU IDs assigned to this worker process.
38
+ Called during worker init so that device_id_to_physical_device_id()
39
+ can map local_rank to the correct physical device without relying
40
+ on CUDA_VISIBLE_DEVICES.
41
+
42
+ Idempotent: a second call with the same value is a no-op.
43
+ Raises RuntimeError if called again with a different value.
44
+
45
+ This is expected to run during single-threaded worker initialization."""
46
+ global _assigned_physical_gpu_ids
47
+ if _assigned_physical_gpu_ids is not None:
48
+ if _assigned_physical_gpu_ids != ids:
49
+ raise RuntimeError(
50
+ f"set_assigned_physical_gpu_ids called with conflicting values: "
51
+ f"existing={_assigned_physical_gpu_ids}, new={ids}"
52
+ )
53
+ return
54
+ _assigned_physical_gpu_ids = ids
55
+
56
+
57
+def get_assigned_physical_gpu_ids() -> list[int] | None:
58
+ return _assigned_physical_gpu_ids
59
+
60
61
@functools.cache
62
def in_wsl() -> bool:
@@ -233,8 +260,34 @@ class Platform:
260
"""
261
import vllm.kernels # noqa: F401
262
263
+ @classmethod
264
+ def device_control_id_to_physical_device_id(cls, device_id: str) -> int:
265
+ """Map one device-control env entry to an integer physical device ID."""
266
+ try:
267
+ return int(device_id)
268
+ except ValueError as e:
269
+ raise ValueError(
270
+ f"Non-integer device ID {device_id!r} is not supported by "
271
+ f"{cls.device_name}."
272
+ ) from e
273
+
274
@classmethod
275
def device_id_to_physical_device_id(cls, device_id: int):
276
+ """Map a vLLM-local logical device ID to a physical device ID.
277
+
278
+ The input is a logical local ID (e.g. a local rank), NOT a visible
279
+ device ordinal; for the latter use
280
+ visible_device_id_to_physical_device_id(). The two coincide only
281
+ when no logical-to-physical mapping is in effect.
282
+ """
283
+ if _assigned_physical_gpu_ids is not None:
284
+ if device_id >= len(_assigned_physical_gpu_ids):
285
+ raise IndexError(
286
+ f"device_id {device_id} is out of range for "
287
+ f"assigned_physical_gpu_ids {_assigned_physical_gpu_ids} "
288
+ f"({len(_assigned_physical_gpu_ids)} devices assigned)"
289
+ )
290
+ return _assigned_physical_gpu_ids[device_id]
291
# Treat empty device control env var as unset. This is a valid
292
# configuration in Ray setups where the engine is launched in
293
# a CPU-only placement group located on a GPU node.
@@ -244,10 +297,58 @@ class Platform:
297
):
298
device_ids = os.environ[cls.device_control_env_var].split(",")
299
physical_device_id = device_ids[device_id]
247
- return int(physical_device_id)
300
+ return cls.device_control_id_to_physical_device_id(physical_device_id)
301
else:
302
return device_id
303
304
+ @classmethod
305
+ def logical_device_id_to_visible_device_id(cls, device_id: int) -> int:
306
+ """Map a vLLM-local logical device ID to the current process's
307
+ visible accelerator ordinal.
308
+
309
+ vLLM internals use logical local IDs. Physical IDs are used only
310
+ at platform/topology boundaries. This helper performs the final
311
+ translation needed by APIs such as ``torch.device("cuda:N")``.
312
+ """
313
+ physical_device_id = cls.device_id_to_physical_device_id(device_id)
314
+ device_control_env = os.environ.get(cls.device_control_env_var, "")
315
+ if not device_control_env:
316
+ return physical_device_id
317
+
318
+ visible_physical_device_ids = [
319
+ cls.device_control_id_to_physical_device_id(physical_id)
320
+ for physical_id in device_control_env.split(",")
321
+ ]
322
+ if physical_device_id not in visible_physical_device_ids:
323
+ raise RuntimeError(
324
+ f"Physical device {physical_device_id} for logical device "
325
+ f"{device_id} is not visible in {cls.device_control_env_var}="
326
+ f"{device_control_env}"
327
+ )
328
+ return visible_physical_device_ids.index(physical_device_id)
329
+
330
+ @classmethod
331
+ def visible_device_id_to_physical_device_id(cls, device_id: int) -> int:
332
+ """Map a visible accelerator ordinal (e.g. ``torch.device.index``)
333
+ to a physical device ID.
334
+
335
+ This is the inverse of the env-var translation performed by
336
+ logical_device_id_to_visible_device_id() and is independent of any
337
+ logical-to-physical mapping set via set_assigned_physical_gpu_ids().
338
+ """
339
+ device_control_env = os.environ.get(cls.device_control_env_var, "")
340
+ if not device_control_env:
341
+ return device_id
342
+ visible_device_ids = device_control_env.split(",")
343
+ if device_id >= len(visible_device_ids):
344
+ raise IndexError(
345
+ f"visible device ordinal {device_id} is out of range for "
346
+ f"{cls.device_control_env_var}={device_control_env}"
347
+ )
348
+ return cls.device_control_id_to_physical_device_id(
349
+ visible_device_ids[device_id]
350
+ )
351
+
352
@classmethod
353
def import_kernels(cls) -> None:
354
"""Import any platform-specific C kernels."""
vllm/v1/engine/core.py
+16
-9
@@ -74,7 +74,7 @@ from vllm.v1.engine.utils import (
74
EngineHandshakeMetadata,
75
EngineZmqAddresses,
76
SignalCallback,
77
- get_device_indices,
77
+ get_physical_gpu_ids_for_local_dp_rank,
78
)
79
from vllm.v1.executor import Executor
80
from vllm.v1.kv_cache_interface import KVCacheConfig, get_kv_cache_spec_kind
@@ -2175,23 +2175,30 @@ class EngineCoreActorMixin:
2175
pass
2176
else:
2177
device_control_env_var = current_platform.device_control_env_var
2178
- self._set_cuda_visible_devices(
2178
+ self._set_assigned_physical_gpu_ids(
2179
vllm_config, local_dp_rank, device_control_env_var
2180
)
2181
2182
- def _set_cuda_visible_devices(
2183
- self, vllm_config: VllmConfig, local_dp_rank: int, device_control_env_var: str
2182
+ def _set_assigned_physical_gpu_ids(
2183
+ self,
2184
+ vllm_config: VllmConfig,
2185
+ local_dp_rank: int,
2186
+ device_control_env_var: str,
2187
):
2188
world_size = vllm_config.parallel_config.world_size
2186
- # Set CUDA_VISIBLE_DEVICES or equivalent.
2189
try:
2188
- value = get_device_indices(
2189
- device_control_env_var, local_dp_rank, world_size
2190
+ physical_gpu_ids = get_physical_gpu_ids_for_local_dp_rank(
2191
+ device_control_env_var,
2192
+ local_dp_rank,
2193
+ world_size,
2194
+ user_assigned_gpu_ids=(
2195
+ vllm_config.parallel_config.assigned_physical_gpu_ids
2196
+ ),
2197
)
2191
- os.environ[device_control_env_var] = value
2198
+ vllm_config.parallel_config.assigned_physical_gpu_ids = physical_gpu_ids
2199
except IndexError as e:
2200
raise Exception(
2194
- f"Error setting {device_control_env_var}: "
2201
+ f"Error computing assigned_physical_gpu_ids: "
2202
f"local range: [{local_dp_rank * world_size}, "
2203
f"{(local_dp_rank + 1) * world_size}) "
2204
f'base value: "{os.getenv(device_control_env_var)}"'
vllm/v1/engine/utils.py
+66
-43
@@ -12,7 +12,6 @@ from multiprocessing import Process, connection
12
from multiprocessing.process import BaseProcess
13
from multiprocessing.queues import Queue
14
from typing import TYPE_CHECKING, cast
15
-from unittest.mock import patch
15
16
import msgspec
17
import zmq
@@ -175,38 +174,38 @@ class CoreEngineProcManager:
174
self.manager_stopped = threading.Event()
175
self.failed_proc_name: str | None = None
176
177
+ # All ranks share this config object: capture the user-provided
178
+ # --device-ids list before the per-rank shard overwrites it. Mutating
179
+ # the config before each proc.start() works because the spawn method
180
+ # pickles process args at start() time, sequentially per rank.
181
+ user_assigned_gpu_ids = vllm_config.parallel_config.assigned_physical_gpu_ids
182
try:
183
for proc, local_dp_rank in zip(self.processes, local_dp_ranks):
180
- # Adjust device control in DP for platforms that cannot rely
181
- # on torch.accelerator.set_device_index(), and for Ray launchers.
182
- device_control_context: contextlib.AbstractContextManager[None] = (
183
- contextlib.nullcontext()
184
- )
184
+ # Populate the logical-to-physical GPU mapping in DP for
185
+ # platforms that cannot rely on
186
+ # torch.accelerator.set_device_index(), and for Ray.
187
needs_device_env_isolation = not (
188
current_platform.is_cuda_alike() or current_platform.is_xpu()
189
)
190
if is_dp and (
191
needs_device_env_isolation or vllm_config.parallel_config.use_ray
192
):
191
- device_control_context = set_device_control_env_var(
192
- vllm_config, local_dp_rank
193
+ set_assigned_physical_gpu_ids_for_dp_rank(
194
+ vllm_config, local_dp_rank, user_assigned_gpu_ids
195
)
196
195
- with (
196
- device_control_context,
197
- numa_utils.configure_subprocess(
198
- # EngineCore itself does not have a TP/PP-local rank.
199
- # When DP is enabled, set_device_control_env_var()
200
- # narrows visible devices to this DP shard first, so
201
- # local_rank=0 means "the first local GPU in this
202
- # shard". The actual TP/PP worker processes spawned by
203
- # the executor are bound separately with their own
204
- # local_rank values.
205
- vllm_config,
206
- local_rank=0,
207
- dp_local_rank=local_dp_rank,
208
- process_kind="EngineCore",
209
- ),
197
+ with numa_utils.configure_subprocess(
198
+ # EngineCore itself does not have a TP/PP-local rank.
199
+ # When DP is enabled, set_assigned_physical_gpu_ids_for_dp_rank()
200
+ # populates the logical-to-physical mapping for this DP
201
+ # shard, so local_rank=0 means "the first local GPU in
202
+ # this shard". The actual TP/PP worker processes spawned
203
+ # by the executor are bound separately with their own
204
+ # local_rank values.
205
+ vllm_config,
206
+ local_rank=0,
207
+ dp_local_rank=local_dp_rank,
208
+ process_kind="EngineCore",
209
):
210
proc.start()
211
finally:
@@ -281,55 +280,79 @@ class SignalCallback:
280
self._event.set()
281
282
284
-@contextlib.contextmanager
285
-def set_device_control_env_var(
286
- vllm_config: VllmConfig, local_dp_rank: int
287
-) -> Iterator[None]:
283
+def set_assigned_physical_gpu_ids_for_dp_rank(
284
+ vllm_config: VllmConfig,
285
+ local_dp_rank: int,
286
+ user_assigned_gpu_ids: list[int] | None = None,
287
+) -> None:
288
"""
289
- Temporarily set CUDA_VISIBLE_DEVICES or equivalent
290
- for engine subprocess.
289
+ Populate assigned_physical_gpu_ids on the config for the given DP rank.
290
+
291
+ user_assigned_gpu_ids is the full (un-sharded) --device-ids list, if the
292
+ user provided one; this DP rank's shard is sliced from it. It is passed
293
+ explicitly rather than read from the config because callers may reuse
294
+ one config object across DP ranks, overwriting the field each time.
295
"""
296
world_size = vllm_config.parallel_config.world_size
297
local_world_size = vllm_config.parallel_config.local_world_size
298
evar = current_platform.device_control_env_var
299
296
- value = get_device_indices(evar, local_dp_rank, world_size, local_world_size)
297
- with patch.dict(os.environ, values=((evar, value),)):
298
- yield
300
+ physical_gpu_ids = get_physical_gpu_ids_for_local_dp_rank(
301
+ evar,
302
+ local_dp_rank,
303
+ world_size,
304
+ local_world_size,
305
+ user_assigned_gpu_ids=user_assigned_gpu_ids,
306
+ )
307
+ vllm_config.parallel_config.assigned_physical_gpu_ids = physical_gpu_ids
308
309
301
-def get_device_indices(
310
+def get_physical_gpu_ids_for_local_dp_rank(
311
device_control_env_var: str,
312
local_dp_rank: int,
313
world_size: int,
314
local_world_size: int | None = None,
306
-):
315
+ user_assigned_gpu_ids: list[int] | None = None,
316
+) -> list[int]:
317
"""
308
- Returns a comma-separated string of device indices for the specified
318
+ Returns list of physical GPU IDs for the specified
319
data parallel rank.
320
321
For example, if world_size=2 and local_dp_rank=1, and there are 4 devices,
312
- this will select devices 2 and 3 for local_dp_rank=1.
322
+ this will return [2, 3] for local_dp_rank=1.
323
+
324
+ If user_assigned_gpu_ids is provided (e.g. from --device-ids), this DP
325
+ rank's shard is sliced from it instead of being derived from the
326
+ device-control env var.
327
"""
328
if local_world_size is None:
329
local_world_size = world_size
330
+ if user_assigned_gpu_ids is not None:
331
+ start = local_dp_rank * world_size
332
+ stop = start + local_world_size
333
+ if stop > len(user_assigned_gpu_ids):
334
+ raise ValueError(
335
+ f"--device-ids provides {len(user_assigned_gpu_ids)} devices, "
336
+ f"but DP rank {local_dp_rank} needs devices [{start}, {stop})"
337
+ )
338
+ return user_assigned_gpu_ids[start:stop]
339
try:
317
- value = ",".join(
318
- str(current_platform.device_id_to_physical_device_id(i))
340
+ return [
341
+ current_platform.device_id_to_physical_device_id(i)
342
for i in range(
343
local_dp_rank * world_size,
344
local_dp_rank * world_size + local_world_size,
345
)
323
- )
346
+ ]
347
except IndexError as e:
348
raise Exception(
326
- f"Error setting {device_control_env_var}: "
349
+ f"Error computing device indices for "
350
+ f"{device_control_env_var}: "
351
f"local range: [{local_dp_rank * world_size}, "
352
f"{(local_dp_rank + 1) * world_size}) "
353
"base value: "
354
f'"{os.getenv(device_control_env_var)}"'
355
) from e
332
- return value
356
357
358
def _apply_dp_identity_suffix(dp_vllm_config, dp_rank: int) -> None:
@@ -453,11 +476,11 @@ class CoreEngineActorManager:
476
# https://github.com/ray-project/ray/blob/master/python/ray/_private/accelerators/intel_gpu.py#L56 # noqa: E501
477
if current_platform.is_xpu():
478
device_evar = current_platform.device_control_env_var
456
- device_indices = get_device_indices(
479
+ physical_gpu_ids = get_physical_gpu_ids_for_local_dp_rank(
480
device_evar, local_index, world_size
481
)
482
actor_env_vars = self.env_vars_dict.copy()
460
- actor_env_vars[device_evar] = device_indices
483
+ actor_env_vars[device_evar] = ",".join(str(d) for d in physical_gpu_ids)
484
runtime_env = RuntimeEnv(env_vars=actor_env_vars)
485
486
actor = (
vllm/v1/executor/multiproc_executor.py
+10
@@ -826,6 +826,16 @@ class WorkerProc:
826
signal.signal(signal.SIGTERM, signal_handler)
827
signal.signal(signal.SIGINT, signal_handler)
828
829
+ # Publish the logical-to-physical mapping early so topology helpers
830
+ # work before init_device (needed by set_worker_net_device below).
831
+ assigned_physical_gpu_ids = kwargs[
832
+ "vllm_config"
833
+ ].parallel_config.assigned_physical_gpu_ids
834
+ if assigned_physical_gpu_ids is not None:
835
+ from vllm.platforms.interface import set_assigned_physical_gpu_ids
836
+
837
+ set_assigned_physical_gpu_ids(assigned_physical_gpu_ids)
838
+
839
# Set net device env vars for the worker if VLLM_GPU_NIC_PCIE_MAPPING is set
840
set_worker_net_device(kwargs.get("local_rank", 0), kwargs["vllm_config"])
841
vllm/v1/executor/ray_executor.py
+22
-31
@@ -258,30 +258,35 @@ class RayDistributedExecutor(Executor):
258
}
259
self.collective_rpc("adjust_rank", args=(rerank_mapping,))
260
261
- # Get the set of GPU IDs used on each node.
262
- worker_node_and_gpu_ids = []
261
+ # Get the set of physical GPU IDs used on each node.
262
+ worker_node_and_physical_gpu_ids = []
263
for worker in [self.driver_dummy_worker] + self.workers:
264
if worker is None:
265
# driver_dummy_worker can be None when using ray spmd worker.
266
continue
267
- worker_node_and_gpu_ids.append(
268
- ray.get(worker.get_node_and_gpu_ids.remote()) # type: ignore[attr-defined]
267
+ worker_node_and_physical_gpu_ids.append(
268
+ ray.get(worker.get_node_and_physical_gpu_ids.remote()) # type: ignore[attr-defined]
269
)
270
271
node_workers = defaultdict(list) # node id -> list of worker ranks
272
- node_gpus = defaultdict(list) # node id -> list of gpu ids
272
+ node_physical_gpu_ids = defaultdict(list) # node id -> physical GPU IDs
273
274
- for i, (node_id, gpu_ids) in enumerate(worker_node_and_gpu_ids):
274
+ for i, (node_id, physical_gpu_ids) in enumerate(
275
+ worker_node_and_physical_gpu_ids
276
+ ):
277
node_workers[node_id].append(i)
276
- # `gpu_ids` can be a list of strings or integers.
278
+ # `physical_gpu_ids` can be a list of strings or integers.
279
# convert them to integers for consistency.
278
- # NOTE: gpu_ids can be larger than 9 (e.g. 16 GPUs),
280
+ # NOTE: physical GPU IDs can be larger than 9 (e.g. 16 GPUs),
281
# string sorting is not sufficient.
282
# see https://github.com/vllm-project/vllm/issues/5590
281
- gpu_ids = [int(x) for x in gpu_ids]
282
- node_gpus[node_id].extend(gpu_ids)
283
- for node_id, gpu_ids in node_gpus.items():
284
- node_gpus[node_id] = sorted(gpu_ids)
283
+ physical_gpu_ids = [
284
+ current_platform.device_control_id_to_physical_device_id(str(x))
285
+ for x in physical_gpu_ids
286
+ ]
287
+ node_physical_gpu_ids[node_id].extend(physical_gpu_ids)
288
+ for node_id, physical_gpu_ids in node_physical_gpu_ids.items():
289
+ node_physical_gpu_ids[node_id] = sorted(physical_gpu_ids)
290
291
all_ips = set(worker_ips + [driver_ip])
292
n_ips = len(all_ips)
@@ -297,23 +302,8 @@ class RayDistributedExecutor(Executor):
302
" each node."
303
)
304
300
- # Set environment variables for the driver and workers.
301
- # We set CUDA_VISIBLE_DEVICES to ALL GPUs on the node for each worker.
302
- # This is needed because:
303
- # 1. Ray's compiled DAG needs to find the allocated GPU in
304
- # CUDA_VISIBLE_DEVICES.
305
- # 2. vLLM's communication layer (NCCL, CustomAllreduce) needs to see
306
- # all GPUs for P2P checks and communication setup. Though if it was
307
- # just this reason, we could have also just kept the visible devices
308
- # unset.
309
- # Each worker will use local_rank to index into the visible devices.
310
- all_args_to_update_environment_variables = [
311
- {
312
- current_platform.device_control_env_var: ",".join(
313
- map(str, node_gpus[node_id])
314
- ),
315
- }
316
- for (node_id, _) in worker_node_and_gpu_ids
305
+ all_args_to_update_environment_variables: list[dict[str, str]] = [
306
+ {} for _ in worker_node_and_physical_gpu_ids
307
]
308
309
# Environment variables to copy from driver to workers
@@ -336,7 +326,7 @@ class RayDistributedExecutor(Executor):
326
"update_environment_variables", args=(self._get_env_vars_to_be_updated(),)
327
)
328
339
- if len(node_gpus) == 1:
329
+ if len(node_physical_gpu_ids) == 1:
330
# in single node case, we don't need to get the IP address.
331
# the loopback address is sufficient
332
# NOTE: a node may have several IP addresses, one for each
@@ -352,10 +342,11 @@ class RayDistributedExecutor(Executor):
342
343
# Initialize the actual workers inside worker wrapper.
344
all_kwargs = []
355
- for rank, (node_id, _) in enumerate(worker_node_and_gpu_ids):
345
+ for rank, (node_id, _) in enumerate(worker_node_and_physical_gpu_ids):
346
local_rank = node_workers[node_id].index(rank)
347
kwargs = dict(
348
vllm_config=self.vllm_config,
349
+ assigned_physical_gpu_ids=sorted(node_physical_gpu_ids[node_id]),
350
local_rank=local_rank,
351
rank=rank,
352
distributed_init_method=distributed_init_method,
vllm/v1/executor/ray_executor_v2.py
+56
-31
@@ -79,24 +79,25 @@ class RayWorkerProc(WorkerProc):
79
1. __init__: lightweight setup, stores init args (no device/model init)
80
2. initialize_worker: called after GPU IDs are discovered, completes
81
the full WorkerProc initialization with the correct local_rank and
82
- CUDA_VISIBLE_DEVICES.
82
+ logical-to-physical GPU mapping.
83
84
- CUDA_VISIBLE_DEVICES setup flow:
84
+ GPU assignment flow:
85
86
1. RayExecutorV2 enables RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES so Ray does
87
not set CUDA_VISIBLE_DEVICES on RayWorkerProc actors at creation time.
88
2. Each actor is scheduled with a placement group and bundle index; Ray resolves
89
the physical GPU ID for that bundle at placement time.
90
- 3. After placement, the worker discovers that GPU ID and sets
91
- CUDA_VISIBLE_DEVICES before finishing WorkerProc initialization.
90
+ 3. After placement, the executor discovers each worker's GPU ID and passes the
91
+ node's logical-to-physical mapping (assigned_physical_gpu_ids) to
92
+ initialize_worker(); CUDA_VISIBLE_DEVICES is never modified.
93
93
- There is no workaround for this unset-and-reset sequence when the placement group
94
- is externally managed: scheduling must complete before CUDA_VISIBLE_DEVICES can
95
- match the GPU tied to the worker's bundle.
94
+ Scheduling must complete before the mapping is known when the placement
95
+ group is externally managed: only then is the GPU tied to the worker's
96
+ bundle resolved.
97
98
This sequence allows multiple vLLM instances to coexist on the same node:
99
each instance is unaware which physical devices others hold, and the
99
- externally managed placement group avoids CUDA_VISIBLE_DEVICES conflicts
100
+ externally managed placement group avoids device assignment conflicts
101
by binding workers to specific placement group bundles.
102
"""
103
@@ -120,28 +121,33 @@ class RayWorkerProc(WorkerProc):
121
is_driver_worker=is_driver_worker,
122
)
123
123
- def get_node_and_gpu_ids(self) -> tuple[str, list[int]]:
124
- """Return (node_id, gpu_ids) assigned to this actor by Ray."""
124
+ def get_node_and_physical_gpu_ids(self) -> tuple[str, list[int]]:
125
+ """Return (node_id, physical_gpu_ids) assigned to this actor by Ray."""
126
node_id = ray.get_runtime_context().get_node_id()
127
device_key = current_platform.ray_device_key
128
if not device_key:
129
raise RuntimeError(
130
f"current platform {current_platform.device_name} does not support ray."
131
)
131
- gpu_ids = ray.get_runtime_context().get_accelerator_ids()[device_key]
132
- return node_id, [int(x) for x in gpu_ids]
132
+ physical_gpu_ids = ray.get_runtime_context().get_accelerator_ids()[device_key]
133
+ return node_id, [
134
+ current_platform.device_control_id_to_physical_device_id(str(x))
135
+ for x in physical_gpu_ids
136
+ ]
137
138
def initialize_worker(
139
self,
140
local_rank: int,
141
env_vars: dict[str, str],
142
driver_env_vars: dict[str, str] | None = None,
143
+ assigned_physical_gpu_ids: list[int] | None = None,
144
) -> None:
145
"""Complete initialization after GPU assignment is known.
146
147
*driver_env_vars* are applied with ``setdefault`` — they fill
148
in missing vars but never overwrite node-local values.
144
- *env_vars* (e.g. CUDA_VISIBLE_DEVICES) always overwrite.
149
+ *env_vars* always overwrite.
150
+ *assigned_physical_gpu_ids* maps local_rank to physical CUDA device ID.
151
"""
152
if driver_env_vars:
153
for key, value in driver_env_vars.items():
@@ -149,6 +155,13 @@ class RayWorkerProc(WorkerProc):
155
for key, value in env_vars.items():
156
os.environ[key] = value
157
158
+ if assigned_physical_gpu_ids is not None:
159
+ vllm_config = self._init_kwargs["vllm_config"]
160
+ assert isinstance(vllm_config, VllmConfig)
161
+ vllm_config.parallel_config.assigned_physical_gpu_ids = (
162
+ assigned_physical_gpu_ids
163
+ )
164
+
165
self.local_rank = local_rank
166
super().__init__(
167
local_rank=local_rank,
@@ -365,36 +378,48 @@ class RayExecutorV2(MultiprocExecutor):
378
)
379
self.ray_worker_handles.append(handle)
380
368
- # Step 6: Discover GPU IDs assigned to each worker via Ray runtime context.
369
- worker_node_and_gpu_ids = ray.get(
370
- [h.actor.get_node_and_gpu_ids.remote() for h in self.ray_worker_handles]
381
+ # Step 6: Discover physical GPU IDs assigned to each worker via Ray
382
+ # runtime context.
383
+ worker_node_and_physical_gpu_ids = ray.get(
384
+ [
385
+ h.actor.get_node_and_physical_gpu_ids.remote()
386
+ for h in self.ray_worker_handles
387
+ ]
388
)
389
390
node_workers: dict[str, list[int]] = defaultdict(list)
374
- node_gpus: dict[str, list[int]] = defaultdict(list)
375
- for i, (node_id, gpu_ids) in enumerate(worker_node_and_gpu_ids):
391
+ node_physical_gpu_ids: dict[str, list[int]] = defaultdict(list)
392
+ for i, (node_id, physical_gpu_ids) in enumerate(
393
+ worker_node_and_physical_gpu_ids
394
+ ):
395
node_workers[node_id].append(i)
377
- node_gpus[node_id].extend(gpu_ids)
378
- for node_id, gpu_ids in node_gpus.items():
379
- node_gpus[node_id] = sorted(gpu_ids)
396
+ node_physical_gpu_ids[node_id].extend(physical_gpu_ids)
397
+ for node_id, physical_gpu_ids in node_physical_gpu_ids.items():
398
+ node_physical_gpu_ids[node_id] = sorted(physical_gpu_ids)
399
381
- # Step 7: Initialize workers with correct local_rank and
382
- # CUDA_VISIBLE_DEVICES. Each worker sees all GPUs assigned to
383
- # this executor on its node; local_rank indexes into that set.
400
+ # Step 7: Initialize workers with local logical ranks and the
401
+ # logical-to-physical GPU mapping discovered from Ray placement.
402
init_worker_refs = []
385
- for i, (node_id, _) in enumerate(worker_node_and_gpu_ids):
403
+ for i, (node_id, _) in enumerate(worker_node_and_physical_gpu_ids):
404
local_rank = node_workers[node_id].index(i)
387
- worker_env_vars = {
388
- current_platform.device_control_env_var: ",".join(
389
- map(str, node_gpus[node_id])
390
- ),
391
- }
405
+ assigned_physical_gpu_ids = sorted(node_physical_gpu_ids[node_id])
406
+ worker_env_vars: dict[str, str] = {}
407
self.ray_worker_handles[i].local_rank = local_rank
408
init_worker_refs.append(
409
self.ray_worker_handles[i].actor.initialize_worker.remote(
395
- local_rank, worker_env_vars, self.driver_env_vars
410
+ local_rank,
411
+ worker_env_vars,
412
+ self.driver_env_vars,
413
+ assigned_physical_gpu_ids=assigned_physical_gpu_ids,
414
)
415
)
416
+ # Also set on the executor-side config for consistency. The mapping
417
+ # is per-node, so only do this when all workers share one node.
418
+ if len(node_physical_gpu_ids) == 1:
419
+ node_id_0 = worker_node_and_physical_gpu_ids[0][0]
420
+ self.vllm_config.parallel_config.assigned_physical_gpu_ids = sorted(
421
+ node_physical_gpu_ids[node_id_0]
422
+ )
423
ray.get(init_worker_refs)
424
425
# Step 8: Collect response MQ handles
vllm/v1/executor/ray_utils.py
+5
-3
@@ -93,7 +93,7 @@ try:
93
def get_node_ip(self) -> str:
94
return get_ip()
95
96
- def get_node_and_gpu_ids(self) -> tuple[str, list[int]]:
96
+ def get_node_and_physical_gpu_ids(self) -> tuple[str, list[int]]:
97
node_id = ray.get_runtime_context().get_node_id()
98
device_key = vllm.platforms.current_platform.ray_device_key
99
if not device_key:
@@ -101,8 +101,10 @@ try:
101
"current platform %s does not support ray.",
102
vllm.platforms.current_platform.device_name,
103
)
104
- gpu_ids = ray.get_runtime_context().get_accelerator_ids()[device_key]
105
- return node_id, gpu_ids
104
+ physical_gpu_ids = ray.get_runtime_context().get_accelerator_ids()[
105
+ device_key
106
+ ]
107
+ return node_id, physical_gpu_ids
108
109
def setup_device_if_necessary(self):
110
# TODO(swang): This is needed right now because Ray CG executes
vllm/v1/worker/gpu_worker.py
+38
-10
@@ -270,19 +270,47 @@ class Worker(WorkerBase):
270
271
# DP_LOCAL_RANK * TP_PP_WORLD_SIZE + TP_LOCAL_RANK
272
self.local_rank += dp_local_rank * tp_pp_world_size
273
- assert self.local_rank < torch.accelerator.device_count(), (
274
- f"DP adjusted local rank {self.local_rank} is out of bounds. "
275
- )
276
- visible_device_count = (
277
- torch.accelerator.device_count() if torch.cuda.is_available() else 0
273
+
274
+ # Publish the logical-to-physical mapping for topology queries
275
+ # such as NIC affinity and P2P checks.
276
+ assigned_physical_gpu_ids = parallel_config.assigned_physical_gpu_ids
277
+ if assigned_physical_gpu_ids is not None:
278
+ from vllm.platforms.interface import set_assigned_physical_gpu_ids
279
+
280
+ set_assigned_physical_gpu_ids(assigned_physical_gpu_ids)
281
+ assert self.local_rank < len(assigned_physical_gpu_ids), (
282
+ f"local_rank {self.local_rank} is out of bounds for "
283
+ f"assigned_physical_gpu_ids {assigned_physical_gpu_ids}"
284
)
279
- assert self.parallel_config.local_world_size <= visible_device_count, (
280
- f"local_world_size ({self.parallel_config.local_world_size}) must "
281
- f"be less than or equal to the number of visible devices "
282
- f"({visible_device_count})."
285
+ # NOTE(patch pr45026): local_world_size is derived from
286
+ # parallel_config.nnodes, which is only set for the "mp"
287
+ # multi-node backend. With the "ray"/"external_launcher"
288
+ # backends nnodes stays 1, so local_world_size collapses to
289
+ # the full world_size and this check wrongly fires on
290
+ # cross-node deployments. assigned_physical_gpu_ids is already
291
+ # per-node and the local_rank bound above fully validates the
292
+ # mapping for these backends, so skip the check for them.
293
+ if parallel_config.distributed_executor_backend not in (
294
+ "ray",
295
+ "external_launcher",
296
+ ):
297
+ assert self.parallel_config.local_world_size <= len(
298
+ assigned_physical_gpu_ids
299
+ ), (
300
+ f"local_world_size ({self.parallel_config.local_world_size})"
301
+ " exceeds assigned_physical_gpu_ids count "
302
+ f"({len(assigned_physical_gpu_ids)})"
303
+ )
304
+ else:
305
+ assert self.local_rank < torch.accelerator.device_count(), (
306
+ f"DP adjusted local rank {self.local_rank} is out of "
307
+ f"bounds for {torch.accelerator.device_count()} devices."
308
)
309
285
- self.device = torch.device(f"cuda:{self.local_rank}")
310
+ visible_device_index = (
311
+ current_platform.logical_device_id_to_visible_device_id(self.local_rank)
312
+ )
313
+ self.device = torch.device(f"cuda:{visible_device_index}")
314
torch.accelerator.set_device_index(self.device)
315
316
current_platform.check_if_supports_dtype(self.model_config.dtype)
vllm/v1/worker/worker_base.py
+6
@@ -286,6 +286,12 @@ class WorkerWrapperBase:
286
extended_calls,
287
)
288
289
+ assigned_physical_gpu_ids = kwargs.pop("assigned_physical_gpu_ids", None)
290
+ if assigned_physical_gpu_ids is not None:
291
+ vllm_config.parallel_config.assigned_physical_gpu_ids = (
292
+ assigned_physical_gpu_ids
293
+ )
294
+
295
shared_worker_lock = kwargs.pop("shared_worker_lock", None)
296
if shared_worker_lock is None:
297
msg = (