main
py 219 lines 7.39 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import numpy as np
4 import pytest
5 import torch
6
7 from vllm.utils.torch_utils import (
8 OMP_NUM_THREADS_SET_BY_VLLM,
9 async_tensor_h2d,
10 available_cpu_count,
11 common_broadcastable_dtype,
12 current_stream,
13 get_kv_cache_torch_dtype,
14 is_lossless_cast,
15 is_quantized_kv_cache,
16 set_default_torch_dtype,
17 set_torch_threads_for_runtime,
18 startup_omp_num_threads,
19 )
20
21
22 def test_nvfp4_4over6_cache_dtype() -> None:
23 from vllm.config.cache import CacheConfig
24 from vllm.v1.kv_cache_interface import KVQuantMode, get_kv_quant_mode
25
26 cache_config = CacheConfig(cache_dtype="nvfp4_4over6")
27
28 assert cache_config.cache_dtype == "nvfp4_4over6"
29 assert get_kv_cache_torch_dtype(cache_config.cache_dtype) == torch.uint8
30 assert is_quantized_kv_cache(cache_config.cache_dtype)
31 assert get_kv_quant_mode(cache_config.cache_dtype) == KVQuantMode.NVFP4
32
33
34 def test_set_default_torch_dtype_restores_dtype_after_exception() -> None:
35 original_dtype = torch.get_default_dtype()
36
37 with (
38 pytest.raises(RuntimeError, match="expected failure"),
39 set_default_torch_dtype(torch.bfloat16),
40 ):
41 assert torch.get_default_dtype() == torch.bfloat16
42 raise RuntimeError("expected failure")
43
44 assert torch.get_default_dtype() == original_dtype
45
46
47 @pytest.mark.parametrize(
48 ("src_dtype", "tgt_dtype", "expected_result"),
49 [
50 # Different precision_levels
51 (torch.bool, torch.int8, True),
52 (torch.bool, torch.float16, True),
53 (torch.bool, torch.complex32, True),
54 (torch.int64, torch.bool, False),
55 (torch.int64, torch.float16, True),
56 (torch.int64, torch.complex32, True),
57 (torch.float64, torch.bool, False),
58 (torch.float64, torch.int8, False),
59 (torch.float64, torch.complex32, True),
60 (torch.complex128, torch.bool, False),
61 (torch.complex128, torch.int8, False),
62 (torch.complex128, torch.float16, False),
63 # precision_level=0
64 (torch.bool, torch.bool, True),
65 # precision_level=1
66 (torch.int8, torch.int16, True),
67 (torch.int16, torch.int8, False),
68 (torch.uint8, torch.int8, False),
69 (torch.int8, torch.uint8, False),
70 # precision_level=2
71 (torch.float16, torch.float32, True),
72 (torch.float32, torch.float16, False),
73 (torch.bfloat16, torch.float32, True),
74 (torch.float32, torch.bfloat16, False),
75 # precision_level=3
76 (torch.complex32, torch.complex64, True),
77 (torch.complex64, torch.complex32, False),
78 ],
79 )
80 def test_is_lossless_cast(src_dtype, tgt_dtype, expected_result):
81 assert is_lossless_cast(src_dtype, tgt_dtype) == expected_result
82
83
84 @pytest.mark.parametrize(
85 ("dtypes", "expected_result"),
86 [
87 ([torch.bool], torch.bool),
88 ([torch.bool, torch.int8], torch.int8),
89 ([torch.bool, torch.int8, torch.float16], torch.float16),
90 ([torch.bool, torch.int8, torch.float16, torch.complex32], torch.complex32), # noqa: E501
91 ],
92 )
93 def test_common_broadcastable_dtype(dtypes, expected_result):
94 assert common_broadcastable_dtype(dtypes) == expected_result
95
96
97 def _test_stream_thread(main_expected_stream: torch.cuda.Stream):
98 import threading
99
100 child_stream = torch.cuda.Stream()
101 thread_stream_ready = threading.Event()
102 thread_can_exit = threading.Event()
103
104 def child_thread_func():
105 with torch.cuda.stream(child_stream):
106 thread_stream_ready.set()
107 thread_can_exit.wait(timeout=10)
108
109 child_thread = threading.Thread(target=child_thread_func)
110 child_thread.start()
111
112 try:
113 assert thread_stream_ready.wait(timeout=5), (
114 "Child thread failed to enter stream context in time"
115 )
116
117 main_current_stream = current_stream()
118
119 assert main_current_stream != child_stream, (
120 "Main thread's current_stream was contaminated by child thread"
121 )
122 assert main_current_stream == main_expected_stream, (
123 f"Main thread's stream changed unexpectedly. "
124 f"Expected {main_expected_stream}, got {main_current_stream}"
125 )
126
127 thread_can_exit.set()
128
129 finally:
130 child_thread.join(timeout=5)
131 if child_thread.is_alive():
132 pytest.fail("Child thread failed to exit properly")
133
134
135 def test_current_stream_multithread():
136 if not torch.cuda.is_available():
137 pytest.skip("CUDA not available")
138
139 main_dedicated_stream = current_stream()
140
141 assert main_dedicated_stream.cuda_stream != 0, (
142 "ROCm/CUDA should create a dedicated stream, not use default stream (0x0)"
143 )
144
145 main_stream_again = current_stream()
146 assert main_stream_again == main_dedicated_stream, (
147 "Multiple calls to current_stream should return the same dedicated stream"
148 )
149
150 _test_stream_thread(main_dedicated_stream)
151
152
153 @pytest.fixture
154 def restore_torch_threads(monkeypatch: pytest.MonkeyPatch):
155 monkeypatch.delenv("OMP_NUM_THREADS", raising=False)
156 original = torch.get_num_threads()
157 yield
158 torch.set_num_threads(original)
159
160
161 def test_startup_omp_num_threads_divides_between_local_workers():
162 """Workers share the node's usable CPUs rather than each taking them all."""
163 available = available_cpu_count()
164 if available < 4:
165 pytest.skip("needs at least 4 usable CPUs")
166 assert startup_omp_num_threads(1) == available
167 assert startup_omp_num_threads(2) == available // 2
168 # Never zero, however many workers share the node.
169 assert startup_omp_num_threads(available * 4) == 1
170
171
172 def test_set_torch_threads_for_runtime(restore_torch_threads):
173 torch.set_num_threads(max(2, available_cpu_count()))
174 set_torch_threads_for_runtime()
175 assert torch.get_num_threads() == 1
176
177
178 def test_runtime_threads_respect_user_omp_num_threads(
179 restore_torch_threads, monkeypatch: pytest.MonkeyPatch
180 ):
181 """An externally-set OMP_NUM_THREADS is the user's choice; leave it alone."""
182 monkeypatch.setenv("OMP_NUM_THREADS", "3")
183 torch.set_num_threads(3)
184 set_torch_threads_for_runtime()
185 assert torch.get_num_threads() == 3
186
187
188 def test_runtime_threads_override_vllm_set_omp_num_threads(
189 restore_torch_threads, monkeypatch: pytest.MonkeyPatch
190 ):
191 """The value vLLM picked for worker startup is dropped once serving starts."""
192 monkeypatch.setenv("OMP_NUM_THREADS", "3")
193 monkeypatch.setenv(OMP_NUM_THREADS_SET_BY_VLLM, "1")
194 torch.set_num_threads(3)
195 set_torch_threads_for_runtime()
196 assert torch.get_num_threads() == 1
197
198
199 @pytest.mark.parametrize(
200 "device", ["cpu", "cuda"] if torch.cuda.is_available() else ["cpu"]
201 )
202 def test_async_tensor_h2d_staging(device):
203 """Staging must preserve values while normalizing dtype and layout."""
204 # Non-contiguous source with a dtype conversion: would previously sync.
205 src = torch.arange(12, dtype=torch.int64).reshape(3, 4).T
206 result = async_tensor_h2d(src, device=device, dtype=torch.int32)
207 assert result.dtype == torch.int32
208 assert result.is_contiguous()
209 assert torch.equal(result.cpu(), src.to(torch.int32))
210
211 # numpy and list sources still work.
212 assert torch.equal(
213 async_tensor_h2d(np.arange(4, dtype=np.int64), device=device),
214 torch.arange(4, device=device),
215 )
216 assert torch.equal(
217 async_tensor_h2d([1, 2, 3], device=device, dtype=torch.int32),
218 torch.tensor([1, 2, 3], dtype=torch.int32, device=device),
219 )