main
py 196 lines 6.84 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import ctypes
5 from concurrent.futures import ThreadPoolExecutor
6
7 import pytest
8 import torch
9
10 from vllm.platforms import current_platform
11
12
13 def check_cuda_context():
14 """Check CUDA driver context status."""
15 try:
16 cuda = ctypes.CDLL("libcuda.so")
17 device = ctypes.c_int()
18 result = cuda.cuCtxGetDevice(ctypes.byref(device))
19 return (True, device.value) if result == 0 else (False, None)
20 except Exception:
21 return False, None
22
23
24 def run_cuda_test_in_thread(device_input, expected_device_id):
25 """Run CUDA context test in separate thread for isolation."""
26 try:
27 # New thread should have no CUDA context initially
28 valid_before, device_before = check_cuda_context()
29 if valid_before:
30 return (
31 False,
32 "CUDA context should not exist in new thread, "
33 f"got device {device_before}",
34 )
35
36 # Test setting CUDA context
37 current_platform.set_device(device_input)
38
39 # Verify context is created correctly
40 valid_after, device_id = check_cuda_context()
41 if not valid_after:
42 return False, "CUDA context should be valid after set_cuda_context"
43 if device_id != expected_device_id:
44 return False, f"Expected device {expected_device_id}, got {device_id}"
45
46 return True, "Success"
47 except Exception as e:
48 return False, f"Exception in thread: {str(e)}"
49
50
51 class TestSetCudaContext:
52 """Test suite for the set_cuda_context function."""
53
54 @pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA not available")
55 @pytest.mark.parametrize(
56 argnames="device_input,expected_device_id",
57 argvalues=[
58 (0, 0),
59 (torch.device("cuda:0"), 0),
60 ("cuda:0", 0),
61 ],
62 ids=["int", "torch_device", "string"],
63 )
64 def test_set_cuda_context_parametrized(self, device_input, expected_device_id):
65 """Test setting CUDA context in isolated threads."""
66 with ThreadPoolExecutor(max_workers=1) as executor:
67 future = executor.submit(
68 run_cuda_test_in_thread, device_input, expected_device_id
69 )
70 success, message = future.result(timeout=30)
71 assert success, message
72
73 @pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA not available")
74 def test_set_cuda_context_invalid_device_type(self):
75 """Test error handling for invalid device type."""
76 with pytest.raises(ValueError, match="Expected a cuda device"):
77 current_platform.set_device(torch.device("cpu"))
78
79
80 def test_get_device_capability_uses_visible_device_ordinal(monkeypatch):
81 import vllm.platforms.interface as platform_interface
82 from vllm.platforms.cuda import NvmlCudaPlatform, pynvml
83
84 seen_indices: list[int] = []
85
86 def record_handle(index: int) -> str:
87 seen_indices.append(index)
88 return f"handle-{index}"
89
90 monkeypatch.setattr(platform_interface, "_assigned_physical_gpu_ids", [1])
91 monkeypatch.setenv(NvmlCudaPlatform.device_control_env_var, "0,1")
92 monkeypatch.setattr(
93 NvmlCudaPlatform,
94 "device_control_id_to_physical_device_id",
95 classmethod(lambda _cls, device_id: int(device_id)),
96 )
97 monkeypatch.setattr(pynvml, "nvmlInit", lambda: None)
98 monkeypatch.setattr(pynvml, "nvmlShutdown", lambda: None)
99 monkeypatch.setattr(
100 pynvml,
101 "nvmlDeviceGetHandleByIndex",
102 record_handle,
103 )
104 monkeypatch.setattr(
105 pynvml,
106 "nvmlDeviceGetCudaComputeCapability",
107 lambda _handle: (9, 0),
108 )
109 NvmlCudaPlatform.get_device_capability.cache_clear()
110
111 capability = NvmlCudaPlatform.get_device_capability(device_id=1)
112
113 assert capability is not None
114 assert capability.to_int() == 90
115 assert seen_indices == [1]
116
117
118 def _stub_nvml(monkeypatch) -> dict[str, int]:
119 """Stub NVML to report SM 9.0 and count init/shutdown pairs.
120
121 Pins `NvmlCudaPlatform` rather than the `CudaPlatform` alias: that alias is
122 `NonNvmlCudaPlatform` where NVML is unavailable, and that class reads
123 `torch.cuda` instead, which needs a real device.
124 """
125 from vllm.platforms.cuda import NvmlCudaPlatform, pynvml
126
127 calls = {"init": 0, "shutdown": 0}
128
129 monkeypatch.setattr(
130 pynvml, "nvmlInit", lambda: calls.__setitem__("init", calls["init"] + 1)
131 )
132 monkeypatch.setattr(
133 pynvml,
134 "nvmlShutdown",
135 lambda: calls.__setitem__("shutdown", calls["shutdown"] + 1),
136 )
137 # Pin the visible-device mapping so the test does not depend on whatever
138 # CUDA_VISIBLE_DEVICES happens to be set to in the environment.
139 monkeypatch.setenv(NvmlCudaPlatform.device_control_env_var, "0")
140 monkeypatch.setattr(
141 NvmlCudaPlatform,
142 "device_control_id_to_physical_device_id",
143 classmethod(lambda _cls, device_id: int(device_id)),
144 )
145 monkeypatch.setattr(
146 pynvml, "nvmlDeviceGetHandleByIndex", lambda index: f"handle-{index}"
147 )
148 monkeypatch.setattr(
149 pynvml, "nvmlDeviceGetCudaComputeCapability", lambda _handle: (9, 0)
150 )
151 NvmlCudaPlatform.get_device_capability.cache_clear()
152 return calls
153
154
155 def test_has_device_capability_does_not_reinit_nvml(monkeypatch):
156 """Repeated capability checks must not re-enter an NVML context.
157
158 `has_device_capability` only reads the cached `get_device_capability`, which
159 carries its own NVML context. Wrapping it in `with_nvml_context` as well
160 cost an nvmlInit()/nvmlShutdown() pair per call, and
161 `triton_reshape_and_cache_flash` calls it per attention layer per step for
162 fp8 and bfloat16 KV caches (issue #50381).
163 """
164 from vllm.platforms.cuda import NvmlCudaPlatform
165
166 calls = _stub_nvml(monkeypatch)
167 try:
168 assert NvmlCudaPlatform.has_device_capability(80)
169 for _ in range(20):
170 NvmlCudaPlatform.has_device_capability(80)
171 NvmlCudaPlatform.has_device_capability(89)
172 NvmlCudaPlatform.has_device_capability((9, 0))
173
174 assert calls["init"] == 1
175 assert calls["shutdown"] == 1
176 finally:
177 NvmlCudaPlatform.get_device_capability.cache_clear()
178
179
180 def test_has_device_capability_comparisons(monkeypatch):
181 """Dropping the redundant NVML context must not change the answers."""
182 from vllm.platforms.cuda import NvmlCudaPlatform
183
184 _stub_nvml(monkeypatch)
185 try:
186 assert NvmlCudaPlatform.has_device_capability(80)
187 assert NvmlCudaPlatform.has_device_capability(90)
188 assert NvmlCudaPlatform.has_device_capability((9, 0))
189 assert not NvmlCudaPlatform.has_device_capability(100)
190 assert not NvmlCudaPlatform.has_device_capability((10, 0))
191 finally:
192 NvmlCudaPlatform.get_device_capability.cache_clear()
193
194
195 if __name__ == "__main__":
196 pytest.main([__file__, "-v"])