main
py 248 lines 7.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import threading
5 import time
6
7 import pytest
8
9 from vllm.config.multimodal import MultiModalConfig
10 from vllm.multimodal.gpu_ipc_memory import (
11 MultiModalGPUMemoryPool,
12 get_mm_gpu_ipc_pool,
13 maybe_init_mm_gpu_ipc_pool,
14 reserve_mm_ipc_gpu_memory,
15 set_mm_gpu_ipc_pool,
16 )
17 from vllm.multimodal.video_decoders import PYNVVIDEOCODEC_VIDEO_BACKEND
18 from vllm.multimodal.video_decoders.pynvvideocodec import (
19 PYNVVIDEOCODEC_CUDA_CONTEXT_BYTES,
20 PYNVVIDEOCODEC_DECODER_GPU_MEMORY_BYTES,
21 )
22 from vllm.utils.mem_constants import GiB_bytes
23
24
25 def _mm_config(
26 *,
27 mm_ipc_gpu_memory_gb: float = 0,
28 video_backend: str | None = None,
29 hw_decoders: int | None = None,
30 ) -> MultiModalConfig:
31 video_kwargs: dict[str, object] = (
32 {} if video_backend is None else {"video_backend": video_backend}
33 )
34 if hw_decoders is not None:
35 video_kwargs["hw_decoders"] = hw_decoders
36
37 return MultiModalConfig(
38 mm_ipc_gpu_memory_gb=mm_ipc_gpu_memory_gb,
39 media_io_kwargs={"video": video_kwargs} if video_kwargs else {},
40 )
41
42
43 def _pynvvideocodec_decoder_budget(
44 api_process_count: int = 1,
45 hw_decoders: int = 2,
46 ) -> int:
47 return api_process_count * (
48 PYNVVIDEOCODEC_DECODER_GPU_MEMORY_BYTES * hw_decoders
49 + PYNVVIDEOCODEC_CUDA_CONTEXT_BYTES
50 )
51
52
53 def test_acquire_release_accounting():
54 pool = MultiModalGPUMemoryPool(total_bytes=100)
55 assert pool.available_bytes == 100
56
57 lease = pool.acquire(40)
58 assert pool.available_bytes == 60
59
60 lease.release()
61 assert pool.available_bytes == 100
62
63
64 def test_acquire_too_large_raises():
65 pool = MultiModalGPUMemoryPool(total_bytes=100)
66 with pytest.raises(ValueError):
67 pool.acquire(101)
68 # Nothing should have been reserved.
69 assert pool.available_bytes == 100
70
71
72 def test_negative_acquire_raises():
73 pool = MultiModalGPUMemoryPool(total_bytes=100)
74 with pytest.raises(ValueError):
75 pool.acquire(-1)
76
77
78 def test_double_release_is_noop():
79 pool = MultiModalGPUMemoryPool(total_bytes=100)
80 lease = pool.acquire(50)
81 lease.release()
82 assert pool.available_bytes == 100
83 # Releasing again must not inflate the pool past its capacity.
84 lease.release()
85 assert pool.available_bytes == 100
86
87
88 def test_context_manager_releases_on_exception():
89 pool = MultiModalGPUMemoryPool(total_bytes=100)
90 with pytest.raises(RuntimeError), pool.acquire(50):
91 assert pool.available_bytes == 50
92 raise RuntimeError("boom")
93 assert pool.available_bytes == 100
94
95
96 def test_acquire_blocks_until_release():
97 pool = MultiModalGPUMemoryPool(total_bytes=100)
98 first = pool.acquire(80)
99
100 acquired = threading.Event()
101
102 def waiter():
103 # Needs 50 bytes but only 20 are free; must block until `first`
104 # is released.
105 with pool.acquire(50):
106 acquired.set()
107
108 t = threading.Thread(target=waiter)
109 t.start()
110
111 # The waiter cannot proceed yet.
112 assert not acquired.wait(timeout=0.2)
113
114 # Releasing the first lease frees enough budget to unblock the waiter.
115 first.release()
116 assert acquired.wait(timeout=2.0)
117 t.join(timeout=2.0)
118 assert not t.is_alive()
119 assert pool.available_bytes == 100
120
121
122 def test_concurrent_acquires_serialize():
123 pool = MultiModalGPUMemoryPool(total_bytes=100)
124 # Each task needs 60 bytes, so only one can hold the budget at a time.
125 in_section = []
126 max_concurrent = 0
127 lock = threading.Lock()
128
129 def task():
130 nonlocal max_concurrent
131 with pool.acquire(60):
132 with lock:
133 in_section.append(1)
134 max_concurrent = max(max_concurrent, len(in_section))
135 time.sleep(0.05)
136 with lock:
137 in_section.pop()
138
139 threads = [threading.Thread(target=task) for _ in range(4)]
140 for t in threads:
141 t.start()
142 for t in threads:
143 t.join(timeout=5.0)
144 assert not t.is_alive()
145
146 assert max_concurrent == 1
147 assert pool.available_bytes == 100
148
149
150 def test_zero_total_bytes_rejected():
151 with pytest.raises(ValueError):
152 MultiModalGPUMemoryPool(total_bytes=0)
153
154
155 def test_global_pool_accessor():
156 try:
157 assert maybe_init_mm_gpu_ipc_pool(0) is None
158 assert get_mm_gpu_ipc_pool() is None
159
160 pool = maybe_init_mm_gpu_ipc_pool(2)
161 assert pool is not None
162 assert get_mm_gpu_ipc_pool() is pool
163 assert pool.total_bytes == 2 * GiB_bytes
164 finally:
165 set_mm_gpu_ipc_pool(None)
166
167
168 def test_global_pool_splits_budget_across_api_processes():
169 try:
170 pool = maybe_init_mm_gpu_ipc_pool(2, api_process_count=4)
171 assert pool is not None
172 assert get_mm_gpu_ipc_pool() is pool
173 assert pool.total_bytes == GiB_bytes // 2
174 finally:
175 set_mm_gpu_ipc_pool(None)
176
177
178 def test_global_pool_rejects_invalid_api_process_count():
179 with pytest.raises(ValueError):
180 maybe_init_mm_gpu_ipc_pool(2, api_process_count=0)
181
182
183 @pytest.mark.parametrize("video_backend", [None, "opencv"])
184 def test_reserve_mm_ipc_gpu_memory_raw_frame_budget_only(
185 monkeypatch: pytest.MonkeyPatch,
186 video_backend: str | None,
187 ):
188 monkeypatch.setenv("VLLM_VIDEO_LOADER_BACKEND", "opencv")
189 mm_config = _mm_config(
190 mm_ipc_gpu_memory_gb=0.25,
191 video_backend=video_backend,
192 )
193
194 assert reserve_mm_ipc_gpu_memory(GiB_bytes, mm_config) == int(0.75 * GiB_bytes)
195
196
197 def test_reserve_mm_ipc_gpu_memory_includes_pynvvideocodec_decoder_budget(
198 monkeypatch: pytest.MonkeyPatch,
199 ):
200 monkeypatch.setenv("VLLM_VIDEO_LOADER_BACKEND", "opencv")
201 mm_config = _mm_config(
202 mm_ipc_gpu_memory_gb=0.25,
203 video_backend=PYNVVIDEOCODEC_VIDEO_BACKEND,
204 )
205 available_bytes = 4 * GiB_bytes
206
207 assert reserve_mm_ipc_gpu_memory(available_bytes, mm_config) == (
208 available_bytes - int(0.25 * GiB_bytes) - _pynvvideocodec_decoder_budget()
209 )
210
211
212 def test_reserve_mm_ipc_gpu_memory_uses_env_video_backend(
213 monkeypatch: pytest.MonkeyPatch,
214 ):
215 monkeypatch.setenv("VLLM_VIDEO_LOADER_BACKEND", PYNVVIDEOCODEC_VIDEO_BACKEND)
216 available_bytes = 4 * GiB_bytes
217
218 assert reserve_mm_ipc_gpu_memory(available_bytes, _mm_config()) == (
219 available_bytes - _pynvvideocodec_decoder_budget()
220 )
221
222
223 def test_reserve_mm_ipc_gpu_memory_scales_decoder_budget_by_api_servers(
224 monkeypatch: pytest.MonkeyPatch,
225 ):
226 monkeypatch.setenv("VLLM_VIDEO_LOADER_BACKEND", PYNVVIDEOCODEC_VIDEO_BACKEND)
227 available_bytes = 8 * GiB_bytes
228
229 assert reserve_mm_ipc_gpu_memory(
230 available_bytes,
231 _mm_config(),
232 api_process_count=3,
233 ) == available_bytes - _pynvvideocodec_decoder_budget(api_process_count=3)
234
235
236 def test_reserve_mm_ipc_gpu_memory_uses_configured_hw_decoders(
237 monkeypatch: pytest.MonkeyPatch,
238 ):
239 monkeypatch.setenv("VLLM_VIDEO_LOADER_BACKEND", "opencv")
240 available_bytes = 4 * GiB_bytes
241 mm_config = _mm_config(
242 video_backend=PYNVVIDEOCODEC_VIDEO_BACKEND,
243 hw_decoders=3,
244 )
245
246 assert reserve_mm_ipc_gpu_memory(available_bytes, mm_config) == (
247 available_bytes - _pynvvideocodec_decoder_budget(hw_decoders=3)
248 )