main
py 748 lines 25.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for packed tensor broadcasting functionality.
4
5 Unit tests for packed_nccl_broadcast_producer and packed_nccl_broadcast_consumer.
6 These utilities enable efficient batched tensor transfer over NCCL.
7 """
8
9 import pytest
10 import torch
11
12 from vllm.distributed.weight_transfer.packed_tensor import (
13 pack_tensors,
14 packed_ipc_consumer,
15 packed_ipc_producer,
16 packed_nccl_broadcast_consumer,
17 packed_nccl_broadcast_producer,
18 unpack_tensor,
19 )
20
21
22 class MockCommunicationGroup:
23 """Mock communication group for testing producer broadcast operations."""
24
25 def __init__(self):
26 self.broadcasted_tensors: list[torch.Tensor] = []
27 self.broadcast_count = 0
28 self.device = torch.device("cuda:0")
29
30 def broadcast(self, tensor, src):
31 """Mock broadcast that stores the tensor for later verification."""
32 self.broadcasted_tensors.append(tensor.clone())
33 self.broadcast_count += 1
34
35
36 class MockConsumerCommunicationGroup:
37 """Mock communication group for consumer that returns pre-stored tensors."""
38
39 def __init__(self, tensors_to_return: list[torch.Tensor]):
40 self.tensors_to_return = tensors_to_return
41 self.current_index = 0
42 self.device = torch.device("cuda:0")
43
44 def broadcast(self, tensor, src):
45 """Mock broadcast that fills the tensor with pre-stored data."""
46 if self.current_index < len(self.tensors_to_return):
47 tensor.copy_(self.tensors_to_return[self.current_index])
48 self.current_index += 1
49
50
51 def create_mock_model_params(
52 num_layers: int = 3,
53 dtype: torch.dtype = torch.float32,
54 ) -> list[tuple[str, torch.Tensor]]:
55 """Create mock model parameters for testing."""
56 params = []
57 for i in range(num_layers):
58 params.append((f"layer{i}.weight", torch.randn(10, 20, dtype=dtype)))
59 params.append((f"layer{i}.bias", torch.randn(10, dtype=dtype)))
60 return params
61
62
63 def create_state_dict_info(
64 params: list[tuple[str, torch.Tensor]],
65 ) -> dict[str, tuple[tuple[int, ...], torch.dtype]]:
66 """Create state dict info (name -> (shape, dtype)) from params."""
67 return {name: (tuple(tensor.shape), tensor.dtype) for name, tensor in params}
68
69
70 # --- Unit Tests: packed_nccl_broadcast_producer ---
71
72
73 @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
74 class TestPackedBroadcastProducer:
75 """Test packed_nccl_broadcast_producer function."""
76
77 def test_producer_empty_iterator(self):
78 """Test producer handles empty iterator gracefully."""
79 mock_group = MockCommunicationGroup()
80
81 packed_nccl_broadcast_producer(
82 iterator=iter([]),
83 group=mock_group,
84 src=0,
85 post_iter_func=lambda x: x[1],
86 buffer_size_bytes=1000,
87 )
88
89 # No broadcasts for empty iterator
90 assert mock_group.broadcast_count == 0
91
92
93 # --- Integration Tests: Producer-Consumer Roundtrip ---
94
95
96 @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
97 class TestPackedBroadcastRoundtrip:
98 """Test producer-consumer roundtrip behavior."""
99
100 @pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
101 def test_roundtrip_different_dtypes(self, dtype):
102 """Test roundtrip with different data types."""
103 params = create_mock_model_params(num_layers=2, dtype=dtype)
104 params_cuda = [(name, tensor.cuda()) for name, tensor in params]
105
106 buffer_size = 1000
107 producer_group = MockCommunicationGroup()
108
109 packed_nccl_broadcast_producer(
110 iterator=iter(params_cuda),
111 group=producer_group,
112 src=0,
113 post_iter_func=lambda x: x[1],
114 buffer_size_bytes=buffer_size,
115 )
116
117 consumer_group = MockConsumerCommunicationGroup(
118 producer_group.broadcasted_tensors
119 )
120
121 state_dict_info = create_state_dict_info(params_cuda)
122 unpacked_tensors = {}
123
124 def post_unpack_func(tensor_list):
125 for name, tensor in tensor_list:
126 unpacked_tensors[name] = tensor.clone()
127
128 packed_nccl_broadcast_consumer(
129 iterator=iter(state_dict_info.items()),
130 group=consumer_group,
131 src=0,
132 post_unpack_func=post_unpack_func,
133 buffer_size_bytes=buffer_size,
134 )
135
136 # Verify roundtrip preserves data
137 for name, original_tensor in params_cuda:
138 assert name in unpacked_tensors
139 unpacked = unpacked_tensors[name]
140 assert unpacked.dtype == dtype
141 assert torch.allclose(unpacked, original_tensor, rtol=1e-4, atol=1e-6)
142
143 def test_roundtrip_mixed_dtypes(self):
144 """Test roundtrip with mixed data types."""
145 # Create params with mixed dtypes
146 params = [
147 ("layer1.weight", torch.randn(10, 20, dtype=torch.float32).cuda()),
148 ("layer1.bias", torch.randn(10, dtype=torch.float16).cuda()),
149 ("layer2.weight", torch.randn(20, 30, dtype=torch.bfloat16).cuda()),
150 ]
151
152 buffer_size = 500
153 producer_group = MockCommunicationGroup()
154
155 packed_nccl_broadcast_producer(
156 iterator=iter(params),
157 group=producer_group,
158 src=0,
159 post_iter_func=lambda x: x[1],
160 buffer_size_bytes=buffer_size,
161 )
162
163 consumer_group = MockConsumerCommunicationGroup(
164 producer_group.broadcasted_tensors
165 )
166
167 state_dict_info = create_state_dict_info(params)
168 unpacked_tensors = {}
169
170 def post_unpack_func(tensor_list):
171 for name, tensor in tensor_list:
172 unpacked_tensors[name] = tensor.clone()
173
174 packed_nccl_broadcast_consumer(
175 iterator=iter(state_dict_info.items()),
176 group=consumer_group,
177 src=0,
178 post_unpack_func=post_unpack_func,
179 buffer_size_bytes=buffer_size,
180 )
181
182 # Verify all params roundtrip correctly with correct dtypes
183 for name, original_tensor in params:
184 assert name in unpacked_tensors
185 unpacked = unpacked_tensors[name]
186 assert unpacked.shape == original_tensor.shape
187 assert unpacked.dtype == original_tensor.dtype
188 assert torch.allclose(unpacked, original_tensor, rtol=1e-4, atol=1e-6)
189
190 @pytest.mark.parametrize("target_size", [100, 100000])
191 def test_roundtrip_different_batch_sizes(self, target_size):
192 """Test roundtrip with different target batch sizes."""
193 params = create_mock_model_params(num_layers=5)
194 params_cuda = [(name, tensor.cuda()) for name, tensor in params]
195
196 producer_group = MockCommunicationGroup()
197
198 packed_nccl_broadcast_producer(
199 iterator=iter(params_cuda),
200 group=producer_group,
201 src=0,
202 post_iter_func=lambda x: x[1],
203 buffer_size_bytes=target_size,
204 )
205
206 consumer_group = MockConsumerCommunicationGroup(
207 producer_group.broadcasted_tensors
208 )
209
210 state_dict_info = create_state_dict_info(params_cuda)
211 unpacked_tensors = {}
212
213 def post_unpack_func(tensor_list):
214 for name, tensor in tensor_list:
215 unpacked_tensors[name] = tensor.clone()
216
217 packed_nccl_broadcast_consumer(
218 iterator=iter(state_dict_info.items()),
219 group=consumer_group,
220 src=0,
221 post_unpack_func=post_unpack_func,
222 buffer_size_bytes=target_size,
223 )
224
225 # Verify all params roundtrip correctly
226 assert len(unpacked_tensors) == len(params)
227 for name, original_tensor in params_cuda:
228 assert name in unpacked_tensors
229 assert torch.allclose(
230 unpacked_tensors[name], original_tensor, rtol=1e-5, atol=1e-7
231 )
232
233 def test_roundtrip_non_contiguous_tensors(self):
234 """Test roundtrip with non-contiguous tensors from the trainer."""
235 # Create non-contiguous tensors (simulating trainer outputs)
236 # Transposed tensors are non-contiguous
237 weight1 = torch.randn(20, 10, dtype=torch.float32).cuda().T
238 # Sliced tensors with step are non-contiguous
239 weight2 = torch.randn(40, 30, dtype=torch.float16).cuda()[::2, ::2]
240 # Permuted tensors are non-contiguous
241 weight3 = torch.randn(5, 10, 15, dtype=torch.bfloat16).cuda().permute(2, 0, 1)
242
243 params = [
244 ("layer1.weight", weight1),
245 ("layer2.weight", weight2),
246 ("layer3.weight", weight3),
247 ]
248
249 # Verify tensors are indeed non-contiguous
250 for name, tensor in params:
251 assert not tensor.is_contiguous(), f"{name} should be non-contiguous"
252
253 buffer_size = 500
254 producer_group = MockCommunicationGroup()
255
256 packed_nccl_broadcast_producer(
257 iterator=iter(params),
258 group=producer_group,
259 src=0,
260 post_iter_func=lambda x: x[1],
261 buffer_size_bytes=buffer_size,
262 )
263
264 consumer_group = MockConsumerCommunicationGroup(
265 producer_group.broadcasted_tensors
266 )
267
268 state_dict_info = create_state_dict_info(params)
269 unpacked_tensors = {}
270
271 def post_unpack_func(tensor_list):
272 for name, tensor in tensor_list:
273 unpacked_tensors[name] = tensor.clone()
274
275 packed_nccl_broadcast_consumer(
276 iterator=iter(state_dict_info.items()),
277 group=consumer_group,
278 src=0,
279 post_unpack_func=post_unpack_func,
280 buffer_size_bytes=buffer_size,
281 )
282
283 # Verify all non-contiguous params roundtrip correctly
284 for name, original_tensor in params:
285 assert name in unpacked_tensors
286 unpacked = unpacked_tensors[name]
287 assert unpacked.shape == original_tensor.shape
288 assert unpacked.dtype == original_tensor.dtype
289 assert torch.allclose(unpacked, original_tensor, rtol=1e-4, atol=1e-6)
290
291
292 # --- Unit Tests: unpack_tensor ---
293
294
295 @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
296 class TestUnpackTensor:
297 """Test the shared unpack_tensor function."""
298
299 def test_unpack_produces_independent_copies(self):
300 """Verify unpacked tensors don't share memory with packed buffer."""
301 original = torch.randn(10, dtype=torch.float32).cuda()
302 packed = original.contiguous().view(torch.uint8).view(-1)
303
304 result = unpack_tensor(
305 packed,
306 names=["w"],
307 shapes=[[10]],
308 dtypes=[torch.float32],
309 tensor_sizes=[packed.numel()],
310 )
311
312 # Mutate the packed buffer
313 packed.zero_()
314
315 # Unpacked tensor should be unaffected
316 assert torch.allclose(result[0][1], original)
317
318
319 # --- Unit Tests: pack_tensors ---
320
321
322 @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
323 class TestPackTensors:
324 """Test the shared pack_tensors function."""
325
326 def test_pack_basic(self):
327 """Test packing a few tensors into one buffer."""
328 params = [
329 ("w1", torch.randn(10, 20, dtype=torch.float32).cuda()),
330 ("w2", torch.randn(5, dtype=torch.float16).cuda()),
331 ]
332
333 chunk = pack_tensors(
334 iterator=iter(params),
335 post_iter_func=lambda x: x[1],
336 buffer_size_bytes=10_000_000,
337 )
338
339 assert chunk is not None
340 assert len(chunk.names) == 2
341 assert chunk.names == ["w1", "w2"]
342 assert chunk.shapes == [[10, 20], [5]]
343 assert chunk.dtypes == [torch.float32, torch.float16]
344 assert chunk.packed_tensor.dtype == torch.uint8
345
346 def test_pack_respects_buffer_limit(self):
347 """Test that packing stops when buffer_size_bytes is exceeded."""
348 params = [
349 (f"w{i}", torch.randn(100, 100, dtype=torch.float32).cuda())
350 for i in range(10)
351 ]
352
353 chunk = pack_tensors(
354 iterator=iter(params),
355 post_iter_func=lambda x: x[1],
356 buffer_size_bytes=50_000,
357 )
358
359 assert chunk is not None
360 assert len(chunk.names) < 10
361
362 def test_pack_empty_iterator(self):
363 """Test that an empty iterator returns None."""
364 chunk = pack_tensors(
365 iterator=iter([]),
366 post_iter_func=lambda x: x[1],
367 buffer_size_bytes=1000,
368 )
369 assert chunk is None
370
371 def test_pack_single_tensor_larger_than_buffer_warns(self):
372 """Test that a tensor exceeding buffer_size_bytes emits a warning."""
373 big = torch.randn(1000, 1000, dtype=torch.float32).cuda()
374 params = [("big", big)]
375
376 import warnings
377
378 with warnings.catch_warnings(record=True) as w:
379 warnings.simplefilter("always")
380 chunk = pack_tensors(
381 iterator=iter(params),
382 post_iter_func=lambda x: x[1],
383 buffer_size_bytes=100,
384 )
385 assert chunk is not None
386 assert len(chunk.names) == 1
387 assert any("exceeds buffer_size_bytes" in str(wi.message) for wi in w)
388
389 def test_pack_unpack_roundtrip(self):
390 """Test pack then unpack produces identical tensors."""
391 params = [
392 ("a", torch.randn(8, 16, dtype=torch.float32).cuda()),
393 ("b", torch.randn(4, dtype=torch.float16).cuda()),
394 ("c", torch.randn(3, 5, 7, dtype=torch.bfloat16).cuda()),
395 ]
396
397 chunk = pack_tensors(
398 iterator=iter(params),
399 post_iter_func=lambda x: x[1],
400 buffer_size_bytes=10_000_000,
401 )
402
403 assert chunk is not None
404 result = unpack_tensor(
405 chunk.packed_tensor,
406 chunk.names,
407 chunk.shapes,
408 chunk.dtypes,
409 chunk.tensor_sizes,
410 )
411
412 assert len(result) == len(params)
413 for (orig_name, orig_tensor), (res_name, res_tensor) in zip(params, result):
414 assert orig_name == res_name
415 assert res_tensor.shape == orig_tensor.shape
416 assert res_tensor.dtype == orig_tensor.dtype
417 assert torch.allclose(res_tensor, orig_tensor, rtol=1e-4, atol=1e-6)
418
419 def test_pack_multiple_chunks(self):
420 """Test consuming an iterator across multiple pack_tensors calls."""
421 params = [
422 (f"w{i}", torch.randn(50, 50, dtype=torch.float32).cuda()) for i in range(6)
423 ]
424 it = iter(params)
425
426 all_names = []
427 chunks = []
428 while True:
429 chunk = pack_tensors(it, lambda x: x[1], buffer_size_bytes=12_000)
430 if chunk is None:
431 break
432 chunks.append(chunk)
433 all_names.extend(chunk.names)
434
435 assert len(chunks) > 1
436 assert all_names == [f"w{i}" for i in range(6)]
437
438
439 # --- Unit Tests: packed_ipc_producer ---
440
441
442 @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
443 class TestPackedIpcProducer:
444 """Test the packed_ipc_producer generator."""
445
446 def test_producer_yields_chunks(self):
447 """Test that the producer yields PackedIpcChunk objects."""
448 params = [
449 (f"w{i}", torch.randn(50, 50, dtype=torch.float32).cuda()) for i in range(6)
450 ]
451
452 chunks = list(
453 packed_ipc_producer(
454 iterator=iter(params),
455 gpu_uuid="test-uuid",
456 post_iter_func=lambda x: x[1],
457 buffer_size_bytes=12_000,
458 )
459 )
460
461 assert len(chunks) > 1
462
463 def test_producer_ipc_handle_has_uuid(self):
464 """Test that each chunk's ipc_handle is keyed by the given UUID."""
465 params = [("w", torch.randn(10, dtype=torch.float32).cuda())]
466
467 chunks = list(
468 packed_ipc_producer(
469 iterator=iter(params),
470 gpu_uuid="my-gpu-uuid",
471 post_iter_func=lambda x: x[1],
472 buffer_size_bytes=10_000_000,
473 )
474 )
475
476 assert "my-gpu-uuid" in chunks[0].ipc_handle
477
478 def test_producer_dtype_names_are_strings(self):
479 """Test that dtype_names are string representations."""
480 params = [
481 ("a", torch.randn(10, dtype=torch.float32).cuda()),
482 ("b", torch.randn(10, dtype=torch.float16).cuda()),
483 ]
484
485 chunks = list(
486 packed_ipc_producer(
487 iterator=iter(params),
488 gpu_uuid="uuid",
489 post_iter_func=lambda x: x[1],
490 buffer_size_bytes=10_000_000,
491 )
492 )
493
494 assert chunks[0].dtype_names == ["float32", "float16"]
495
496 def test_producer_empty_iterator(self):
497 """Test producer with empty iterator yields nothing."""
498 chunks = list(
499 packed_ipc_producer(
500 iterator=iter([]),
501 gpu_uuid="uuid",
502 post_iter_func=lambda x: x[1],
503 buffer_size_bytes=1000,
504 )
505 )
506 assert len(chunks) == 0
507
508
509 # --- Integration Tests: IPC Producer-Consumer Roundtrip ---
510
511
512 def _ipc_consumer_worker(cmd_q, ack_q, result_q, done_event, device_index):
513 """Worker that consumes chunks streamed one at a time from the parent.
514
515 CUDA IPC requires the consumer to be in a separate process from the
516 producer. The producer reuses a single IPC buffer between chunks, so
517 the parent must wait for our ack (sent after we copy the chunk to
518 CPU) before advancing the producer.
519 """
520 try:
521 torch.accelerator.set_device_index(device_index)
522 all_results = []
523 while True:
524 cd = cmd_q.get()
525 if cd is None:
526 break
527 result = packed_ipc_consumer(
528 ipc_handle=cd["ipc_handle"],
529 names=cd["names"],
530 shapes=cd["shapes"],
531 dtype_names=cd["dtype_names"],
532 tensor_sizes=cd["tensor_sizes"],
533 device_index=device_index,
534 )
535 # .cpu() forces a GPU→CPU copy off the shared IPC buffer, so
536 # the producer is free to overwrite it once we ack.
537 all_results.extend([(name, tensor.cpu()) for name, tensor in result])
538 del result
539 ack_q.put("ack")
540 result_q.put(("ok", all_results))
541 except Exception as e:
542 result_q.put(("error", str(e)))
543 # Keep the process alive until the parent has finished reading from
544 # the result queue — torch serializes CPU tensors via fd sharing,
545 # which requires this process's resource-sharer server to be running.
546 done_event.wait(timeout=60)
547
548
549 @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
550 class TestPackedIpcRoundtrip:
551 """Test IPC producer-consumer roundtrip using real CUDA IPC.
552
553 These tests spawn a child process for the consumer because
554 rebuild_cuda_tensor requires a separate process from the one that
555 called reduce_tensor.
556 """
557
558 def _get_gpu_uuid(self) -> str:
559 device_index = torch.cuda.current_device()
560 props = torch.cuda.get_device_properties(device_index)
561 return str(props.uuid)
562
563 def _run_roundtrip(self, chunk_iter, device_index, timeout=30):
564 """Stream chunks through a child consumer one at a time.
565
566 ``packed_ipc_producer`` reuses a single IPC buffer for every
567 chunk, so the producer must not be advanced until the consumer
568 has finished reading the current chunk. We enforce that with an
569 ack queue: the consumer puts ``"ack"`` after it has copied the
570 chunk to CPU, and only then do we pull the next chunk from the
571 generator.
572
573 Returns ``(num_chunks, results)``.
574 """
575 import multiprocessing as mp
576
577 ctx = mp.get_context("spawn")
578 cmd_q = ctx.Queue()
579 ack_q = ctx.Queue()
580 result_q = ctx.Queue()
581 done_event = ctx.Event()
582 proc = ctx.Process(
583 target=_ipc_consumer_worker,
584 args=(cmd_q, ack_q, result_q, done_event, device_index),
585 )
586 proc.start()
587
588 num_chunks = 0
589 try:
590 for chunk in chunk_iter:
591 cmd_q.put(
592 {
593 "ipc_handle": chunk.ipc_handle,
594 "names": chunk.names,
595 "shapes": chunk.shapes,
596 "dtype_names": chunk.dtype_names,
597 "tensor_sizes": chunk.tensor_sizes,
598 }
599 )
600 if ack_q.get(timeout=timeout) != "ack":
601 raise RuntimeError("Consumer did not ack chunk")
602 num_chunks += 1
603 cmd_q.put(None)
604 status, payload = result_q.get(timeout=timeout)
605 finally:
606 done_event.set()
607 proc.join(timeout=10)
608 if proc.is_alive():
609 proc.kill()
610
611 if status == "error":
612 raise RuntimeError(f"Consumer process failed: {payload}")
613 # Reclaim IPC-shared memory now that the child has released it
614 torch.cuda.ipc_collect()
615 return num_chunks, payload
616
617 def test_roundtrip_basic(self):
618 """Test basic IPC producer -> consumer roundtrip."""
619 params = [
620 ("w1", torch.randn(10, 20, dtype=torch.float32).cuda()),
621 ("w2", torch.randn(5, dtype=torch.float16).cuda()),
622 ]
623 gpu_uuid = self._get_gpu_uuid()
624 device_index = torch.cuda.current_device()
625
626 num_chunks, result = self._run_roundtrip(
627 packed_ipc_producer(
628 iterator=iter(params),
629 gpu_uuid=gpu_uuid,
630 post_iter_func=lambda x: x[1],
631 buffer_size_bytes=10_000_000,
632 ),
633 device_index,
634 )
635
636 assert num_chunks == 1
637 assert len(result) == 2
638 for (orig_name, orig_tensor), (res_name, res_tensor) in zip(params, result):
639 assert orig_name == res_name
640 assert res_tensor.shape == orig_tensor.shape
641 assert res_tensor.dtype == orig_tensor.dtype
642 assert torch.allclose(res_tensor, orig_tensor.cpu(), rtol=1e-4, atol=1e-6)
643
644 @pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
645 def test_roundtrip_dtypes(self, dtype):
646 """Test IPC roundtrip with different dtypes."""
647 params = create_mock_model_params(num_layers=2, dtype=dtype)
648 params_cuda = [(n, t.cuda()) for n, t in params]
649 gpu_uuid = self._get_gpu_uuid()
650 device_index = torch.cuda.current_device()
651
652 _, result = self._run_roundtrip(
653 packed_ipc_producer(
654 iterator=iter(params_cuda),
655 gpu_uuid=gpu_uuid,
656 post_iter_func=lambda x: x[1],
657 buffer_size_bytes=10_000_000,
658 ),
659 device_index,
660 )
661
662 assert len(result) == len(params_cuda)
663 for (orig_name, orig_tensor), (res_name, res_tensor) in zip(
664 params_cuda, result
665 ):
666 assert orig_name == res_name
667 assert res_tensor.dtype == dtype
668 assert torch.allclose(res_tensor, orig_tensor.cpu(), rtol=1e-4, atol=1e-6)
669
670 def test_roundtrip_multiple_chunks(self):
671 """Test IPC roundtrip across multiple chunks."""
672 params = [
673 (f"layer{i}.weight", torch.randn(100, 100, dtype=torch.float32).cuda())
674 for i in range(8)
675 ]
676 gpu_uuid = self._get_gpu_uuid()
677 device_index = torch.cuda.current_device()
678
679 num_chunks, result = self._run_roundtrip(
680 packed_ipc_producer(
681 iterator=iter(params),
682 gpu_uuid=gpu_uuid,
683 post_iter_func=lambda x: x[1],
684 buffer_size_bytes=50_000,
685 ),
686 device_index,
687 )
688
689 assert num_chunks > 1
690 assert len(result) == len(params)
691 for (orig_name, orig_tensor), (res_name, res_tensor) in zip(params, result):
692 assert orig_name == res_name
693 assert torch.allclose(res_tensor, orig_tensor.cpu(), rtol=1e-5, atol=1e-7)
694
695 def test_roundtrip_non_contiguous(self):
696 """Test IPC roundtrip with non-contiguous tensors."""
697 params = [
698 ("transposed", torch.randn(20, 10, dtype=torch.float32).cuda().T),
699 ("sliced", torch.randn(40, 30, dtype=torch.float16).cuda()[::2, ::2]),
700 ]
701 gpu_uuid = self._get_gpu_uuid()
702 device_index = torch.cuda.current_device()
703
704 for _, t in params:
705 assert not t.is_contiguous()
706
707 _, result = self._run_roundtrip(
708 packed_ipc_producer(
709 iterator=iter(params),
710 gpu_uuid=gpu_uuid,
711 post_iter_func=lambda x: x[1],
712 buffer_size_bytes=10_000_000,
713 ),
714 device_index,
715 )
716
717 for (orig_name, orig_tensor), (res_name, res_tensor) in zip(params, result):
718 assert orig_name == res_name
719 assert res_tensor.shape == orig_tensor.shape
720 assert res_tensor.dtype == orig_tensor.dtype
721 assert torch.allclose(res_tensor, orig_tensor.cpu(), rtol=1e-4, atol=1e-6)
722
723 def test_consumer_wrong_uuid_raises(self):
724 """Test that consumer raises ValueError for unknown GPU UUID."""
725 params = [("w", torch.randn(10, dtype=torch.float32).cuda())]
726 gpu_uuid = self._get_gpu_uuid()
727
728 chunks = list(
729 packed_ipc_producer(
730 iterator=iter(params),
731 gpu_uuid=gpu_uuid,
732 post_iter_func=lambda x: x[1],
733 buffer_size_bytes=10_000_000,
734 )
735 )
736
737 c = chunks[0]
738 fake_handle = {"fake-uuid-12345": c.ipc_handle[gpu_uuid]}
739
740 with pytest.raises(ValueError, match="IPC handle not found"):
741 packed_ipc_consumer(
742 ipc_handle=fake_handle,
743 names=c.names,
744 shapes=c.shapes,
745 dtype_names=c.dtype_names,
746 tensor_sizes=c.tensor_sizes,
747 device_index=torch.cuda.current_device(),
748 )