main
py 471 lines 17.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import multiprocessing
5 import random
6 import time
7 import traceback
8 import unittest
9 from multiprocessing import Lock
10
11 import torch
12
13 # Assuming these are imported from your module
14 from vllm.distributed.device_communicators.shm_object_storage import (
15 MsgpackSerde,
16 SingleWriterShmObjectStorage,
17 SingleWriterShmRingBuffer,
18 )
19 from vllm.multimodal.inputs import (
20 MultiModalFieldElem,
21 MultiModalKwargsItem,
22 MultiModalSharedField,
23 )
24
25
26 def _dummy_elem(size: int):
27 return MultiModalFieldElem(
28 data=torch.empty((size,), dtype=torch.int8),
29 field=MultiModalSharedField(batch_size=1),
30 )
31
32
33 def _dummy_item(size_by_key: dict[str, int]):
34 return MultiModalKwargsItem(
35 {key: _dummy_elem(size) for key, size in size_by_key.items()}
36 )
37
38
39 class TestSingleWriterShmObjectStorage(unittest.TestCase):
40 def setUp(self):
41 """Set up test fixtures before each test method."""
42 ring_buffer = SingleWriterShmRingBuffer(
43 data_buffer_size=1024 * 100,
44 create=True, # 10 MB buffer
45 )
46 self.storage = SingleWriterShmObjectStorage(
47 max_object_size=1024 * 10, # 10KB max object
48 n_readers=2,
49 ring_buffer=ring_buffer,
50 serde_class=MsgpackSerde,
51 reader_lock=Lock(),
52 )
53
54 def tearDown(self):
55 """Clean up after each test."""
56 if self.storage:
57 self.storage.close()
58
59 def test_minimal_put_get_cycle(self):
60 """Test basic put and get operations."""
61 key = "test_key"
62 value = _dummy_item({"field1": 10, "field2": 20})
63
64 # Put operation
65 address, monotonic_id = self.storage.put(key, value)
66
67 # Verify key is in index
68 self.assertIn(key, self.storage.key_index)
69 self.assertEqual(self.storage.key_index[key], (address, monotonic_id))
70 self.assertEqual(self.storage.id_index[monotonic_id], key)
71
72 # Get operation
73 result = self.storage.get(address, monotonic_id)
74
75 # Verify result
76 self.assertEqual(result, value)
77
78 def test_put_same_key_twice(self):
79 """Test behavior when putting the same key multiple times."""
80 key = "duplicate_key"
81 value1 = "first value"
82 value2 = "second value"
83
84 # First put
85 address1, id1 = self.storage.put(key, value1)
86 retrieved1 = self.storage.get(address1, id1)
87 self.assertEqual(retrieved1, value1)
88
89 # should raise an error on second put
90 with self.assertRaises(ValueError) as context:
91 self.storage.put(key, value2)
92
93 self.assertIn("already exists in the storage", str(context.exception))
94
95 def test_large_object_rejection(self):
96 """Test that objects exceeding max_object_size are rejected."""
97 # Create an object larger than max_object_size
98 large_data = "x" * (self.storage.max_object_size + 100)
99
100 with self.assertRaises(ValueError) as context:
101 self.storage.put("large_key", large_data)
102
103 self.assertIn("exceeds max object size", str(context.exception))
104
105 def test_buffer_overflow_and_cleanup(self):
106 """Test behavior when buffer fills up and needs cleanup."""
107 # Fill up the buffer with many small objects
108 stored_items = []
109
110 try:
111 for i in range(1000): # Try to store many items
112 key = f"item_{i}"
113 value = f"data_{i}" * 100 # Make it reasonably sized
114 address, monotonic_id = self.storage.put(key, value)
115 stored_items.append((key, value, address, monotonic_id))
116 except MemoryError:
117 print(f"Buffer filled after {len(stored_items)} items")
118
119 # Verify that some items are still accessible
120 accessible_count = 0
121 for key, original_value, address, monotonic_id in stored_items:
122 for i in range(self.storage.n_readers):
123 retrieved = self.storage.get(address, monotonic_id)
124 if retrieved == original_value:
125 accessible_count += 1
126
127 self.assertEqual(accessible_count, len(stored_items))
128
129 try:
130 for i in range(len(stored_items), 1000): # Try to store many items
131 key = f"item_{i}"
132 value = f"data_{i}" * 100 # Make it reasonably sized
133 address, monotonic_id = self.storage.put(key, value)
134 stored_items.append((key, value, address, monotonic_id))
135 except MemoryError:
136 print(f"Buffer filled after {len(stored_items)} items")
137
138 # Verify that some items are still accessibles
139 for key, original_value, address, monotonic_id in stored_items:
140 try:
141 for i in range(self.storage.n_readers):
142 retrieved = self.storage.get(address, monotonic_id)
143 if retrieved == original_value:
144 accessible_count += 1
145 except ValueError as e:
146 print(f"Error retrieving {key}: {e}")
147
148 # some items from the first batch may still be accessible
149 self.assertGreaterEqual(accessible_count, len(stored_items))
150
151 def test_blocking_unread_object(self):
152 """Test behavior when buffer fills up and needs cleanup."""
153 # Fill up the buffer with many small objects
154 stored_items = []
155
156 try:
157 for i in range(1000): # Try to store many items
158 key = f"item_{i}"
159 value = f"data_{i}" * 100 # Make it reasonably sized
160 address, monotonic_id = self.storage.put(key, value)
161 stored_items.append((key, value, address, monotonic_id))
162 except MemoryError:
163 print(f"Buffer filled after {len(stored_items)} items")
164
165 # read all items except the first one
166 # to simulate a blocking situation
167 accessible_count = 0
168 for key, original_value, address, monotonic_id in stored_items[1:]:
169 for i in range(self.storage.n_readers):
170 retrieved = self.storage.get(address, monotonic_id)
171 if retrieved == original_value:
172 accessible_count += 1
173
174 self.assertEqual(accessible_count, len(stored_items) - 1)
175
176 try:
177 key = f"item_{len(stored_items)}"
178 value = f"data_{len(stored_items)}" * 100
179 address, monotonic_id = self.storage.put(key, value)
180 except MemoryError:
181 print(f"Buffer filled after {len(stored_items)} items")
182
183 # read the first item
184 for i in range(self.storage.n_readers):
185 key, original_value, address, monotonic_id = stored_items[0]
186 retrieved = self.storage.get(address, monotonic_id)
187 self.assertEqual(retrieved, original_value)
188
189 try:
190 for i in range(len(stored_items), 1000): # Try to store many items
191 key = f"item_{i}"
192 value = f"data_{i}" * 100 # Make it reasonably sized
193 address, monotonic_id = self.storage.put(key, value)
194 stored_items.append((key, value, address, monotonic_id))
195 except MemoryError:
196 print(f"Buffer filled after {len(stored_items)} items")
197
198 # some items from the first batch may still be accessible
199 self.assertGreaterEqual(len(stored_items), accessible_count + 10)
200
201 def test_invalid_get_operations(self):
202 """Test various invalid get operations."""
203 # Test with non-existent address
204 with self.assertRaises(ValueError): # Could be various exceptions
205 self.storage.get(99999, 1)
206
207 # Store something first
208 address, monotonic_id = self.storage.put("test", "value")
209
210 # Test with wrong monotonic_id
211 with self.assertRaises(ValueError) as context:
212 self.storage.get(address, monotonic_id + 100)
213
214 self.assertIn("has been modified or is invalid", str(context.exception))
215
216 def test_clear_storage(self):
217 """Test clearing the storage."""
218 # Store some items
219 for i in range(5):
220 self.storage.put(f"item_{i}", f"value_{i}")
221
222 # Clear the storage
223 self.storage.clear()
224
225 # Verify that all indices are empty
226 self.assertEqual(len(self.storage.key_index), 0)
227 self.assertEqual(len(self.storage.id_index), 0)
228 self.assertEqual(len(self.storage.ring_buffer.metadata), 0)
229
230 # Verify that new items can be added after clearing. IDs keep
231 # increasing so handles issued before the clear cannot alias new data.
232 address, monotonic_id = self.storage.put("new_item", "new_value")
233 self.assertIn("new_item", self.storage.key_index)
234 self.assertEqual((address, monotonic_id), (0, 5))
235
236 def _open_reader(self) -> SingleWriterShmObjectStorage:
237 reader = SingleWriterShmObjectStorage.create_from_handle(self.storage.handle())
238 self.addCleanup(reader.close)
239 return reader
240
241 def test_reader_get_and_touch_each_require_valid_signature(self):
242 address, monotonic_id = self.storage.put("key", "value")
243 signature = self.storage.get_signature("key")
244 reader = self._open_reader()
245
246 reader.touch("key", address, monotonic_id, signature)
247 self.assertEqual(reader.get(address, monotonic_id, signature, "key"), "value")
248
249 flipped = [signature[0] ^ 1, *signature[1:]]
250 for bad in (None, flipped, [signature], signature[:-1], [256] * 32):
251 with self.assertRaisesRegex(ValueError, "SHM handle signature"):
252 reader.get(address, monotonic_id, bad, "key")
253 with self.assertRaisesRegex(ValueError, "SHM handle signature"):
254 reader.touch("key", address, monotonic_id, bad)
255
256 def test_signature_is_bound_to_key_and_address(self):
257 address, monotonic_id = self.storage.put("key", "value")
258 signature = self.storage.get_signature("key")
259 reader = self._open_reader()
260
261 with self.assertRaisesRegex(ValueError, "SHM handle signature"):
262 reader.get(address, monotonic_id, signature, "other_key")
263 with self.assertRaisesRegex(ValueError, "SHM handle signature"):
264 reader.get(address + 1, monotonic_id, signature, "key")
265
266 def test_writer_rejects_handle_no_longer_in_key_index(self):
267 address, monotonic_id = self.storage.put("key", "value")
268 signature = self.storage.get_signature("key")
269 self.storage.verify_signature("key", address, monotonic_id, signature)
270
271 self.storage.clear()
272
273 with self.assertRaisesRegex(ValueError, "SHM handle signature"):
274 self.storage.verify_signature("key", address, monotonic_id, signature)
275
276 def test_handle_issued_before_clear_reads_own_data_until_reused(self):
277 address, monotonic_id = self.storage.put("key", "value")
278 signature = self.storage.get_signature("key")
279 reader = self._open_reader()
280
281 self.storage.clear()
282
283 # Requests still draining after a clear keep reading their own data.
284 self.assertEqual(reader.get(address, monotonic_id, signature, "key"), "value")
285
286 self.storage.put("key", "new_value")
287
288 with self.assertRaisesRegex(ValueError, "modified or is invalid"):
289 reader.get(address, monotonic_id, signature, "key")
290 with self.assertRaisesRegex(ValueError, "modified or is invalid"):
291 reader.touch("key", address, monotonic_id, signature)
292
293
294 class TestShmObjectStorageReferenceCounting(unittest.TestCase):
295 """One writer and four readers, as with tensor-parallel workers."""
296
297 def setUp(self):
298 ring_buffer = SingleWriterShmRingBuffer(
299 data_buffer_size=1024 * 100, create=True
300 )
301 self.writer = SingleWriterShmObjectStorage(
302 max_object_size=1024 * 10,
303 n_readers=4,
304 ring_buffer=ring_buffer,
305 serde_class=MsgpackSerde,
306 )
307 self.addCleanup(self.writer.close)
308 handle = self.writer.handle()
309 handle.reader_lock = Lock()
310 self.readers = [
311 SingleWriterShmObjectStorage.create_from_handle(handle) for _ in range(4)
312 ]
313 for reader in self.readers:
314 self.addCleanup(reader.close)
315
316 def _reference(self, key: str) -> tuple[int, int, list[int], str]:
317 """Take one writer reference to the key, as one request item does."""
318 if self.writer.is_cached(key):
319 address, monotonic_id = self.writer.get_cached(key)
320 else:
321 address, monotonic_id = self.writer.put(key, "value")
322 return address, monotonic_id, self.writer.get_signature(key), key
323
324 @staticmethod
325 def _touch(reader: SingleWriterShmObjectStorage, reference) -> None:
326 address, monotonic_id, signature, key = reference
327 reader.touch(key, address, monotonic_id, signature)
328
329 def _is_evictable(self, key: str) -> bool:
330 self.writer.free_unused()
331 return not self.writer.is_cached(key)
332
333 def test_item_referenced_again_before_every_reader_fetched_it(self):
334 """A second request must not leave the item unevictable forever."""
335 first = self._reference("image")
336 self.writer.touch("image")
337 second = self._reference("image")
338 self.writer.release_touches()
339
340 # Every reader starts on the first request; the ones that finish it
341 # go on to the second while the others are still fetching.
342 for reader in self.readers:
343 self._touch(reader, first)
344 for reader in self.readers:
345 reader.get(*first)
346 self._touch(reader, second)
347 reader.get(*second)
348
349 self.assertTrue(self._is_evictable("image"))
350
351 def test_new_item_twice_in_one_request_is_not_evicted_early(self):
352 """The item must stay until the last reader has fetched it."""
353 self.writer.touch("image")
354 references = [self._reference("image"), self._reference("image")]
355 self.writer.release_touches()
356
357 for reader in self.readers[:-1]:
358 for reference in references:
359 self._touch(reader, reference)
360 for reference in references:
361 reader.get(*reference)
362 for reference in references:
363 self._touch(self.readers[-1], reference)
364 self.assertFalse(self._is_evictable("image"))
365
366 for reference in references:
367 self.readers[-1].get(*reference)
368 self.assertTrue(self._is_evictable("image"))
369
370 def test_touch_protects_item_until_released(self):
371 reference = self._reference("image")
372 for reader in self.readers:
373 reader.get(*reference)
374
375 self.writer.touch("image")
376 self.assertFalse(self._is_evictable("image"))
377 self.writer.release_touches()
378 self.assertTrue(self._is_evictable("image"))
379
380
381 # Reader process function
382 def reader_process(process_id, storage_handle, items_to_read):
383 """Reader process that connects to existing shared memory and reads data."""
384 reader_storage = SingleWriterShmObjectStorage.create_from_handle(storage_handle)
385
386 print(f"Reader {process_id} started")
387
388 errors = []
389
390 for key, original_value, address, monotonic_id, signature in items_to_read:
391 time.sleep(random.random() / 100)
392 try:
393 # Read data from shared memory
394 retrieved_value = reader_storage.get(address, monotonic_id, signature, key)
395
396 # Verify data integrity
397 assert retrieved_value == original_value
398 print(f"Reader {process_id} retrieved {key}: {retrieved_value}")
399 except Exception as e:
400 errors.append((key, str(e), type(e).__name__))
401
402
403 def run_multiprocess_example():
404 """Run a minimal working example with real shared memory."""
405 print("=== Minimal Object Storage Example ===")
406
407 try:
408 # Create storage instance
409 ring_buffer = SingleWriterShmRingBuffer(
410 data_buffer_size=1024 * 100,
411 create=True, # 10 MB buffer
412 )
413 storage = SingleWriterShmObjectStorage(
414 max_object_size=1024,
415 n_readers=3,
416 ring_buffer=ring_buffer,
417 serde_class=MsgpackSerde,
418 reader_lock=Lock(),
419 )
420
421 print(f"Created storage (writer: {storage.is_writer})")
422
423 # Test basic data types
424 test_data = [
425 ("user_data", {"name": "Alice", "age": 30, "scores": [95, 87, 92]}),
426 ("simple_string", "Hello, World!"),
427 ("number", 42),
428 ("list_data", [1, 2, 3, "four", 5.0]),
429 ]
430
431 stored_items = []
432
433 # Store all data
434 for key, value in test_data:
435 print(f"Storing {key}: {value}")
436 address, monotonic_id = storage.put(key, value)
437 signature = storage.get_signature(key)
438 stored_items.append((key, value, address, monotonic_id, signature))
439 print(f" -> Stored at address {address}, ID {monotonic_id}")
440
441 print("\n--- Retrieving Data ---")
442 processes = []
443 handle = storage.handle()
444 # initialize lock for reader processes
445 handle.reader_lock = Lock()
446 for i in range(storage.n_readers):
447 p = multiprocessing.Process(
448 target=reader_process, args=(i, handle, stored_items)
449 )
450 processes.append(p)
451 p.start()
452
453 for p in processes:
454 p.join(timeout=10)
455 if p.is_alive():
456 p.terminate()
457 p.join()
458
459 except Exception as e:
460 print(f"Error in minimal example: {e}")
461 traceback.print_exc()
462
463
464 if __name__ == "__main__":
465 # Run the minimal example first
466 run_multiprocess_example()
467 print("\n" + "=" * 50 + "\n")
468
469 # Run the test suite
470 print("Running comprehensive test suite...")
471 unittest.main(verbosity=2, exit=False)