[CI] Stabilize tensor IPC multiprocessing tests (#50804)
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
Andreas Karatzas committed
Aug 11, 2026 at 21:16 UTC
23f25aac141c885c9885bdec1d26ffcca8eab184
1 file changed
+271
-176
tests/v1/test_tensor_ipc_queue.py
+271
-176
@@ -3,11 +3,13 @@
3
4
"""Tests for tensor IPC queue functionality."""
5
6
-import contextlib
6
import multiprocessing as mp
7
+import time
8
from dataclasses import dataclass
9
+from multiprocessing.process import BaseProcess
10
from multiprocessing.synchronize import Barrier as BarrierType
11
from multiprocessing.synchronize import Event as EventType
12
+from queue import Empty
13
from typing import Any
14
15
import pytest
@@ -23,15 +25,62 @@ from vllm.v1.engine.tensor_ipc import (
25
from vllm.v1.serial_utils import MsgpackDecoder, MsgpackEncoder
26
27
DEVICE_TYPE = current_platform.device_type
28
+_MP_CTX = torch_mp.get_context("spawn")
29
+_PROCESS_RESULT_TIMEOUT = 60.0
30
+_PROCESS_CLEANUP_TIMEOUT = 10.0
31
32
28
-@pytest.fixture(scope="module", autouse=True)
29
-def setup_multiprocessing():
30
- """Set multiprocessing start method to 'spawn' for compatibility."""
31
- with contextlib.suppress(RuntimeError):
32
- # Already set, which is fine
33
- torch_mp.set_start_method("spawn", force=True)
34
- yield
33
+def _collect_process_results(
34
+ result_queue: mp.Queue,
35
+ count: int,
36
+ processes: list[BaseProcess],
37
+) -> list[dict[str, Any]]:
38
+ """Collect worker results within one shared startup deadline."""
39
+ deadline = time.monotonic() + _PROCESS_RESULT_TIMEOUT
40
+ results = []
41
+ for _ in range(count):
42
+ remaining = deadline - time.monotonic()
43
+ if remaining <= 0:
44
+ states = ", ".join(
45
+ f"{p.name}(pid={p.pid}, alive={p.is_alive()}, exit={p.exitcode})"
46
+ for p in processes
47
+ )
48
+ raise TimeoutError(f"Timed out waiting for worker results: {states}")
49
+ try:
50
+ results.append(result_queue.get(timeout=remaining))
51
+ except Empty as exc:
52
+ states = ", ".join(
53
+ f"{p.name}(pid={p.pid}, alive={p.is_alive()}, exit={p.exitcode})"
54
+ for p in processes
55
+ )
56
+ raise TimeoutError(
57
+ f"Timed out waiting for worker results: {states}"
58
+ ) from exc
59
+ return results
60
+
61
+
62
+def _cleanup_processes(processes: list[BaseProcess]) -> list[str]:
63
+ """Join workers, then force-clean only those that miss the deadline."""
64
+ started = [process for process in processes if process.pid is not None]
65
+ deadline = time.monotonic() + _PROCESS_CLEANUP_TIMEOUT
66
+ for process in started:
67
+ process.join(timeout=max(0.0, deadline - time.monotonic()))
68
+
69
+ failures = [
70
+ f"{p.name}(pid={p.pid}, alive={p.is_alive()}, exit={p.exitcode})"
71
+ for p in started
72
+ if p.is_alive() or p.exitcode != 0
73
+ ]
74
+ alive = [p for p in started if p.is_alive()]
75
+ for process in alive:
76
+ process.terminate()
77
+ for process in alive:
78
+ process.join(timeout=5.0)
79
+ for process in alive:
80
+ if process.is_alive():
81
+ process.kill()
82
+ process.join(timeout=5.0)
83
+ return failures
84
85
86
@dataclass
@@ -73,20 +122,26 @@ def encoder_process(
122
123
result_queue.put(
124
{
125
+ "role": "encoder",
126
"success": True,
127
"encoded_length": len(encoded),
128
"device": str(device),
129
"tensor_shape": tuple(tensor.shape),
130
}
131
)
82
- retrieval_done.wait(timeout=30.0)
132
+ retrieval_done.wait(timeout=_PROCESS_RESULT_TIMEOUT)
133
except Exception as e:
134
import traceback
135
136
ready_event.set()
137
retrieval_done.set()
138
result_queue.put(
89
- {"success": False, "error": str(e), "traceback": traceback.format_exc()}
139
+ {
140
+ "role": "encoder",
141
+ "success": False,
142
+ "error": str(e),
143
+ "traceback": traceback.format_exc(),
144
+ }
145
)
146
147
@@ -100,7 +155,7 @@ def decoder_process(
155
):
156
"""Process that msgpack-decodes tensors received via IPC."""
157
try:
103
- if not encoder_ready.wait(timeout=10.0):
158
+ if not encoder_ready.wait(timeout=_PROCESS_RESULT_TIMEOUT):
159
raise TimeoutError("Encoder did not signal ready")
160
161
encoded = payload_queue.get(timeout=5.0)
@@ -110,6 +165,7 @@ def decoder_process(
165
166
result_queue.put(
167
{
168
+ "role": "decoder",
169
"success": True,
170
"tensor_shape": tuple(decoded.tensor.shape),
171
"device": str(decoded.tensor.device),
@@ -122,7 +178,12 @@ def decoder_process(
178
179
retrieval_done.set()
180
result_queue.put(
125
- {"success": False, "error": str(e), "traceback": traceback.format_exc()}
181
+ {
182
+ "role": "decoder",
183
+ "success": False,
184
+ "error": str(e),
185
+ "traceback": traceback.format_exc(),
186
+ }
187
)
188
else:
189
retrieval_done.set()
@@ -131,16 +192,17 @@ def decoder_process(
192
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
193
def test_cuda_tensor_queue_basic():
194
"""Test CUDA tensor IPC through the msgpack encoder/decoder path."""
134
- tensor_queue = torch_mp.Queue()
135
- payload_queue: mp.Queue = mp.Queue()
136
- result_queue: mp.Queue = mp.Queue()
137
- encoder_ready = mp.Event()
138
- retrieval_done = mp.Event()
195
+ tensor_queue = _MP_CTX.Queue()
196
+ payload_queue: mp.Queue = _MP_CTX.Queue()
197
+ result_queue: mp.Queue = _MP_CTX.Queue()
198
+ encoder_ready = _MP_CTX.Event()
199
+ retrieval_done = _MP_CTX.Event()
200
201
tensor_shape = (4, 8, 16)
202
tensor_dtype = torch.float32
203
143
- encoder_proc = mp.Process(
204
+ encoder_proc = _MP_CTX.Process(
205
+ name="tensor-ipc-encoder",
206
target=encoder_process,
207
args=(
208
tensor_queue,
@@ -151,9 +213,8 @@ def test_cuda_tensor_queue_basic():
213
retrieval_done,
214
),
215
)
154
- encoder_proc.start()
155
-
156
- decoder_proc = mp.Process(
216
+ decoder_proc = _MP_CTX.Process(
217
+ name="tensor-ipc-decoder",
218
target=decoder_process,
219
args=(
220
tensor_queue,
@@ -164,13 +225,21 @@ def test_cuda_tensor_queue_basic():
225
retrieval_done,
226
),
227
)
167
- decoder_proc.start()
168
-
169
- encoder_result = result_queue.get(timeout=10.0)
170
- decoder_result = result_queue.get(timeout=10.0)
228
+ processes = [encoder_proc, decoder_proc]
229
+ try:
230
+ for process in processes:
231
+ process.start()
232
+ results = {
233
+ result["role"]: result
234
+ for result in _collect_process_results(result_queue, 2, processes)
235
+ }
236
+ finally:
237
+ retrieval_done.set()
238
+ process_failures = _cleanup_processes(processes)
239
172
- encoder_proc.join(timeout=5.0)
173
- decoder_proc.join(timeout=5.0)
240
+ assert not process_failures, f"Worker cleanup failures: {process_failures}"
241
+ encoder_result = results["encoder"]
242
+ decoder_result = results["decoder"]
243
244
# Verify results
245
assert encoder_result["success"], (
@@ -206,7 +275,7 @@ def test_cpu_tensor_fallback():
275
276
def test_msgpack_encoder_decoder_with_ipc():
277
"""Test the full msgpack + tensor IPC path in one process."""
209
- tensor_queue = torch_mp.Queue()
278
+ tensor_queue = _MP_CTX.Queue()
279
sender = TensorIpcSender(tensor_queue)
280
encoder = MsgpackEncoder(oob_tensor_consumer=sender)
281
receiver = TensorIpcReceiver(tensor_queue)
@@ -228,7 +297,7 @@ def test_msgpack_encoder_decoder_with_ipc():
297
298
def test_decoder_buffer_management():
299
"""Test receiver's tensor buffer management when draining queue."""
231
- tensor_queue = torch_mp.Queue()
300
+ tensor_queue = _MP_CTX.Queue()
301
302
sender_id = "test_sender"
303
message_id = 1
@@ -289,7 +358,7 @@ def api_server_worker(
358
sender_id = f"server_{server_id}"
359
360
# Wait for all servers to be ready
292
- barrier.wait()
361
+ barrier.wait(timeout=_PROCESS_RESULT_TIMEOUT)
362
363
# Send tensor using TensorIpcData
364
ipc_data = TensorIpcData(
@@ -304,7 +373,7 @@ def api_server_worker(
373
374
# Keep process alive until main process has retrieved all tensors
375
# This prevents shared memory handles from being invalidated
307
- retrieval_done.wait(timeout=30.0)
376
+ retrieval_done.wait(timeout=_PROCESS_RESULT_TIMEOUT)
377
except Exception as e:
378
import traceback
379
@@ -321,57 +390,49 @@ def api_server_worker(
390
def test_multiple_api_servers_to_engine():
391
"""Test multiple API servers sending to one engine core via multiprocessing."""
392
num_api_servers = 3
324
- tensor_queue = torch_mp.Queue()
325
- result_queue: mp.Queue = mp.Queue()
326
- barrier = mp.Barrier(num_api_servers)
327
- retrieval_done = mp.Event()
393
+ tensor_queue = _MP_CTX.Queue()
394
+ result_queue: mp.Queue = _MP_CTX.Queue()
395
+ barrier = _MP_CTX.Barrier(num_api_servers)
396
+ retrieval_done = _MP_CTX.Event()
397
398
# Start multiple API server processes
330
- processes = []
399
+ processes: list[BaseProcess] = []
400
for server_id in range(num_api_servers):
332
- proc = mp.Process(
401
+ proc = _MP_CTX.Process(
402
+ name=f"api-server-{server_id}",
403
target=api_server_worker,
404
args=(server_id, tensor_queue, result_queue, barrier, retrieval_done),
405
)
336
- proc.start()
406
processes.append(proc)
407
339
- # Collect results from all servers
340
- results = []
341
- for _ in range(num_api_servers):
342
- result = result_queue.get(timeout=10.0)
343
- results.append(result)
344
-
345
- # Verify all servers succeeded
346
- for result in results:
347
- assert result["success"], (
348
- f"Server {result['server_id']} failed: {result.get('error')}"
349
- )
350
-
351
- # Verify all tensors are in queue
352
- received_tensors = []
353
- for _ in range(num_api_servers):
354
- ipc_data = tensor_queue.get(timeout=1.0)
355
- received_tensors.append((ipc_data.sender_id, ipc_data.tensor))
408
+ try:
409
+ for process in processes:
410
+ process.start()
411
357
- assert len(received_tensors) == num_api_servers
412
+ results = _collect_process_results(result_queue, num_api_servers, processes)
413
+ for result in results:
414
+ assert result["success"], (
415
+ f"Server {result['server_id']} failed: {result.get('error')}"
416
+ )
417
359
- # Verify tensor content (order may vary with multiprocessing)
360
- tensor_by_sender = {sid: t for sid, t in received_tensors}
361
- for server_id in range(num_api_servers):
362
- expected_id = f"server_{server_id}"
363
- assert expected_id in tensor_by_sender, (
364
- f"Missing tensor from server {server_id}"
365
- )
366
- expected_tensor = torch.ones(server_id + 1, server_id + 2) * server_id
367
- assert torch.allclose(tensor_by_sender[expected_id], expected_tensor)
418
+ received_tensors = []
419
+ for _ in range(num_api_servers):
420
+ ipc_data = tensor_queue.get(timeout=10.0)
421
+ received_tensors.append((ipc_data.sender_id, ipc_data.tensor))
422
369
- # Signal workers that retrieval is complete
370
- retrieval_done.set()
423
+ tensor_by_sender = {sid: tensor for sid, tensor in received_tensors}
424
+ for server_id in range(num_api_servers):
425
+ expected_id = f"server_{server_id}"
426
+ assert expected_id in tensor_by_sender, (
427
+ f"Missing tensor from server {server_id}"
428
+ )
429
+ expected_tensor = torch.ones(server_id + 1, server_id + 2) * server_id
430
+ assert torch.allclose(tensor_by_sender[expected_id], expected_tensor)
431
+ finally:
432
+ retrieval_done.set()
433
+ process_failures = _cleanup_processes(processes)
434
372
- # Wait for all processes to complete
373
- for proc in processes:
374
- proc.join(timeout=5.0)
435
+ assert not process_failures, f"Worker cleanup failures: {process_failures}"
436
437
438
def mixed_tensor_encoder_process(
@@ -402,16 +463,21 @@ def mixed_tensor_encoder_process(
463
464
ready_event.set()
465
405
- result_queue.put({"success": True, "sent_cuda": True})
466
+ result_queue.put({"role": "encoder", "success": True, "sent_cuda": True})
467
468
# Keep process alive until decoder has retrieved the tensor
408
- retrieval_done.wait(timeout=30.0)
469
+ retrieval_done.wait(timeout=_PROCESS_RESULT_TIMEOUT)
470
except Exception as e:
471
import traceback
472
473
ready_event.set()
474
result_queue.put(
414
- {"success": False, "error": str(e), "traceback": traceback.format_exc()}
475
+ {
476
+ "role": "encoder",
477
+ "success": False,
478
+ "error": str(e),
479
+ "traceback": traceback.format_exc(),
480
+ }
481
)
482
483
@@ -424,7 +490,7 @@ def mixed_tensor_decoder_process(
490
"""Process that retrieves mixed tensors from queue."""
491
try:
492
# Wait for encoder to finish
427
- if not encoder_ready.wait(timeout=10.0):
493
+ if not encoder_ready.wait(timeout=_PROCESS_RESULT_TIMEOUT):
494
raise TimeoutError("Encoder did not signal ready")
495
496
# Try to get CUDA tensor from queue
@@ -432,6 +498,7 @@ def mixed_tensor_decoder_process(
498
499
result_queue.put(
500
{
501
+ "role": "decoder",
502
"success": True,
503
"is_cuda": ipc_data.tensor.is_cuda,
504
"shape": tuple(ipc_data.tensor.shape),
@@ -445,38 +512,49 @@ def mixed_tensor_decoder_process(
512
513
retrieval_done.set() # Signal even on failure
514
result_queue.put(
448
- {"success": False, "error": str(e), "traceback": traceback.format_exc()}
515
+ {
516
+ "role": "decoder",
517
+ "success": False,
518
+ "error": str(e),
519
+ "traceback": traceback.format_exc(),
520
+ }
521
)
522
523
524
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
525
def test_mixed_cpu_cuda_tensors():
526
"""Test encoding with mixed CPU and CUDA tensors using multiprocessing."""
455
- tensor_queue = torch_mp.Queue()
456
- result_queue: mp.Queue = mp.Queue()
457
- encoder_ready = mp.Event()
458
- retrieval_done = mp.Event()
527
+ tensor_queue = _MP_CTX.Queue()
528
+ result_queue: mp.Queue = _MP_CTX.Queue()
529
+ encoder_ready = _MP_CTX.Event()
530
+ retrieval_done = _MP_CTX.Event()
531
532
# Start encoder process
461
- encoder_proc = mp.Process(
533
+ encoder_proc = _MP_CTX.Process(
534
+ name="mixed-tensor-encoder",
535
target=mixed_tensor_encoder_process,
536
args=(tensor_queue, result_queue, encoder_ready, retrieval_done),
537
)
465
- encoder_proc.start()
466
-
467
- # Start decoder process
468
- decoder_proc = mp.Process(
538
+ decoder_proc = _MP_CTX.Process(
539
+ name="mixed-tensor-decoder",
540
target=mixed_tensor_decoder_process,
541
args=(tensor_queue, result_queue, encoder_ready, retrieval_done),
542
)
472
- decoder_proc.start()
473
-
474
- # Get results
475
- encoder_result = result_queue.get(timeout=10.0)
476
- decoder_result = result_queue.get(timeout=10.0)
543
+ processes = [encoder_proc, decoder_proc]
544
+ try:
545
+ for process in processes:
546
+ process.start()
547
+ results = {
548
+ result["role"]: result
549
+ for result in _collect_process_results(result_queue, 2, processes)
550
+ }
551
+ finally:
552
+ retrieval_done.set()
553
+ process_failures = _cleanup_processes(processes)
554
478
- encoder_proc.join(timeout=5.0)
479
- decoder_proc.join(timeout=5.0)
555
+ assert not process_failures, f"Worker cleanup failures: {process_failures}"
556
+ encoder_result = results["encoder"]
557
+ decoder_result = results["decoder"]
558
559
# Verify encoder succeeded
560
assert encoder_result["success"], (
@@ -519,6 +597,7 @@ def cpu_tensor_ipc_encoder_process(
597
598
result_queue.put(
599
{
600
+ "role": "encoder",
601
"success": True,
602
"encoded_length": len(encoded),
603
"device": str(tensor.device),
@@ -528,13 +607,18 @@ def cpu_tensor_ipc_encoder_process(
607
608
# Keep process alive until decoder has retrieved the tensor
609
# This is necessary for CPU tensor shared memory to remain valid
531
- retrieval_done.wait(timeout=30.0)
610
+ retrieval_done.wait(timeout=_PROCESS_RESULT_TIMEOUT)
611
except Exception as e:
612
import traceback
613
614
ready_event.set()
615
result_queue.put(
537
- {"success": False, "error": str(e), "traceback": traceback.format_exc()}
616
+ {
617
+ "role": "encoder",
618
+ "success": False,
619
+ "error": str(e),
620
+ "traceback": traceback.format_exc(),
621
+ }
622
)
623
624
@@ -548,7 +632,7 @@ def cpu_tensor_ipc_decoder_process(
632
"""Process that decodes and receives CPU tensors from IPC queue."""
633
try:
634
# Wait for encoder to finish sending
551
- if not encoder_ready.wait(timeout=10.0):
635
+ if not encoder_ready.wait(timeout=_PROCESS_RESULT_TIMEOUT):
636
raise TimeoutError("Encoder did not signal ready")
637
638
# Get tensor from queue
@@ -556,6 +640,7 @@ def cpu_tensor_ipc_decoder_process(
640
641
result_queue.put(
642
{
643
+ "role": "decoder",
644
"success": True,
645
"tensor_id": ipc_data.tensor_id,
646
"tensor_shape": tuple(ipc_data.tensor.shape),
@@ -572,22 +657,28 @@ def cpu_tensor_ipc_decoder_process(
657
658
retrieval_done.set() # Signal even on failure
659
result_queue.put(
575
- {"success": False, "error": str(e), "traceback": traceback.format_exc()}
660
+ {
661
+ "role": "decoder",
662
+ "success": False,
663
+ "error": str(e),
664
+ "traceback": traceback.format_exc(),
665
+ }
666
)
667
668
669
def test_cpu_tensor_ipc():
670
"""Test CPU tensor sharing via IPC queue when mm_tensor_ipc is enabled."""
671
# Set up single queue and synchronization
582
- tensor_queue = torch_mp.Queue()
583
- result_queue: mp.Queue = mp.Queue()
584
- encoder_ready = mp.Event()
585
- retrieval_done = mp.Event()
672
+ tensor_queue = _MP_CTX.Queue()
673
+ result_queue: mp.Queue = _MP_CTX.Queue()
674
+ encoder_ready = _MP_CTX.Event()
675
+ retrieval_done = _MP_CTX.Event()
676
677
tensor_shape = (3, 5, 7)
678
679
# Start encoder process
590
- encoder_proc = mp.Process(
680
+ encoder_proc = _MP_CTX.Process(
681
+ name="cpu-tensor-encoder",
682
target=cpu_tensor_ipc_encoder_process,
683
args=(
684
tensor_queue,
@@ -597,10 +688,8 @@ def test_cpu_tensor_ipc():
688
retrieval_done,
689
),
690
)
600
- encoder_proc.start()
601
-
602
- # Start decoder process
603
- decoder_proc = mp.Process(
691
+ decoder_proc = _MP_CTX.Process(
692
+ name="cpu-tensor-decoder",
693
target=cpu_tensor_ipc_decoder_process,
694
args=(
695
tensor_queue,
@@ -610,14 +699,21 @@ def test_cpu_tensor_ipc():
699
retrieval_done,
700
),
701
)
613
- decoder_proc.start()
614
-
615
- # Wait for processes and collect results
616
- encoder_result = result_queue.get(timeout=10.0)
617
- decoder_result = result_queue.get(timeout=10.0)
702
+ processes = [encoder_proc, decoder_proc]
703
+ try:
704
+ for process in processes:
705
+ process.start()
706
+ results = {
707
+ result["role"]: result
708
+ for result in _collect_process_results(result_queue, 2, processes)
709
+ }
710
+ finally:
711
+ retrieval_done.set()
712
+ process_failures = _cleanup_processes(processes)
713
619
- encoder_proc.join(timeout=5.0)
620
- decoder_proc.join(timeout=5.0)
714
+ assert not process_failures, f"Worker cleanup failures: {process_failures}"
715
+ encoder_result = results["encoder"]
716
+ decoder_result = results["decoder"]
717
718
# Verify results
719
assert encoder_result["success"], (
@@ -634,7 +730,7 @@ def test_cpu_tensor_ipc():
730
731
def test_ipc_disabled_mode():
732
"""Test that IPC is disabled when no sender is provided."""
637
- tensor_queues = [torch_mp.Queue()]
733
+ tensor_queues = [_MP_CTX.Queue()]
734
735
# Create encoder without IPC sender (IPC disabled)
736
encoder = MsgpackEncoder()
@@ -686,7 +782,7 @@ def concurrent_sender_process(
782
encoder = MsgpackEncoder(oob_tensor_consumer=sender)
783
784
# Wait for all senders to be ready before sending
689
- barrier.wait(timeout=10.0)
785
+ barrier.wait(timeout=_PROCESS_RESULT_TIMEOUT)
786
787
encoded_payloads = []
788
for msg_idx in range(num_messages):
@@ -717,7 +813,7 @@ def concurrent_sender_process(
813
)
814
815
# Keep alive so shared-memory handles remain valid
720
- retrieval_done.wait(timeout=30.0)
816
+ retrieval_done.wait(timeout=_PROCESS_RESULT_TIMEOUT)
817
except Exception as e:
818
import traceback
819
@@ -742,16 +838,17 @@ def test_concurrent_senders_single_receiver():
838
"""
839
num_senders = 4
840
num_messages_per_sender = 3
745
- tensor_queue = torch_mp.Queue()
746
- payload_queue: mp.Queue = mp.Queue()
747
- result_queue: mp.Queue = mp.Queue()
748
- barrier = mp.Barrier(num_senders)
749
- retrieval_done = mp.Event()
841
+ tensor_queue = _MP_CTX.Queue()
842
+ payload_queue: mp.Queue = _MP_CTX.Queue()
843
+ result_queue: mp.Queue = _MP_CTX.Queue()
844
+ barrier = _MP_CTX.Barrier(num_senders)
845
+ retrieval_done = _MP_CTX.Event()
846
847
# Launch sender processes
752
- processes = []
848
+ processes: list[BaseProcess] = []
849
for i in range(num_senders):
754
- proc = mp.Process(
850
+ proc = _MP_CTX.Process(
851
+ name=f"tensor-ipc-sender-{i}",
852
target=concurrent_sender_process,
853
args=(
854
tensor_queue,
@@ -763,63 +860,61 @@ def test_concurrent_senders_single_receiver():
860
retrieval_done,
861
),
862
)
766
- proc.start()
863
processes.append(proc)
864
769
- # Collect send confirmations
770
- send_results = []
771
- for _ in range(num_senders):
772
- send_results.append(result_queue.get(timeout=15.0))
773
- for r in send_results:
774
- assert r["success"], (
775
- f"Sender {r['sender_index']} failed: {r.get('error')}\n"
776
- f"{r.get('traceback', '')}"
777
- )
778
-
779
- # Now decode all messages from the main process using a single receiver
780
- receiver = TensorIpcReceiver(tensor_queue)
781
- decoder = MsgpackDecoder(MultiTensorMessage, oob_tensor_provider=receiver)
782
-
783
- decoded_messages: list[MultiTensorMessage] = []
784
- total = num_senders * num_messages_per_sender
785
- for _ in range(total):
786
- encoded = payload_queue.get(timeout=10.0)
787
- decoded = decoder.decode(encoded)
788
- assert isinstance(decoded, MultiTensorMessage)
789
- decoded_messages.append(decoded)
790
-
791
- # Signal senders they can exit
792
- retrieval_done.set()
793
-
794
- # Group by sender_label prefix to verify all messages arrived
795
- by_sender: dict[int, list[MultiTensorMessage]] = {}
796
- for msg in decoded_messages:
797
- # label format: "sender_{i}_msg_{j}"
798
- parts = msg.sender_label.split("_")
799
- sender_idx = int(parts[1])
800
- by_sender.setdefault(sender_idx, []).append(msg)
801
-
802
- assert len(by_sender) == num_senders, (
803
- f"Expected {num_senders} senders, got {len(by_sender)}"
804
- )
865
+ try:
866
+ for process in processes:
867
+ process.start()
868
+
869
+ send_results = _collect_process_results(result_queue, num_senders, processes)
870
+ for result in send_results:
871
+ assert result["success"], (
872
+ f"Sender {result['sender_index']} failed: {result.get('error')}\n"
873
+ f"{result.get('traceback', '')}"
874
+ )
875
806
- for sender_idx in range(num_senders):
807
- msgs = sorted(by_sender[sender_idx], key=lambda m: m.sender_label)
808
- assert len(msgs) == num_messages_per_sender, (
809
- f"Sender {sender_idx}: expected {num_messages_per_sender} "
810
- f"messages, got {len(msgs)}"
876
+ receiver = TensorIpcReceiver(tensor_queue)
877
+ decoder = MsgpackDecoder(MultiTensorMessage, oob_tensor_provider=receiver)
878
+
879
+ decoded_messages: list[MultiTensorMessage] = []
880
+ total = num_senders * num_messages_per_sender
881
+ for _ in range(total):
882
+ encoded = payload_queue.get(timeout=10.0)
883
+ decoded = decoder.decode(encoded)
884
+ assert isinstance(decoded, MultiTensorMessage)
885
+ decoded_messages.append(decoded)
886
+
887
+ by_sender: dict[int, list[MultiTensorMessage]] = {}
888
+ for message in decoded_messages:
889
+ # label format: "sender_{i}_msg_{j}"
890
+ sender_idx = int(message.sender_label.split("_")[1])
891
+ by_sender.setdefault(sender_idx, []).append(message)
892
+
893
+ assert len(by_sender) == num_senders, (
894
+ f"Expected {num_senders} senders, got {len(by_sender)}"
895
)
812
- for msg_idx, msg in enumerate(msgs):
813
- assert msg.sender_label == f"sender_{sender_idx}_msg_{msg_idx}"
814
- # Verify tensor shapes match what the sender created
815
- assert msg.t1.shape == (sender_idx + 1, 3)
816
- assert msg.t2.shape == (2, sender_idx + 2)
817
- # Verify tensor values
818
- assert torch.allclose(msg.t1, torch.full_like(msg.t1, float(msg_idx)))
819
- assert torch.allclose(msg.t2, torch.full_like(msg.t2, float(msg_idx + 100)))
896
821
- for proc in processes:
822
- proc.join(timeout=5.0)
897
+ for sender_idx in range(num_senders):
898
+ messages = sorted(by_sender[sender_idx], key=lambda m: m.sender_label)
899
+ assert len(messages) == num_messages_per_sender, (
900
+ f"Sender {sender_idx}: expected {num_messages_per_sender} "
901
+ f"messages, got {len(messages)}"
902
+ )
903
+ for msg_idx, message in enumerate(messages):
904
+ assert message.sender_label == f"sender_{sender_idx}_msg_{msg_idx}"
905
+ assert message.t1.shape == (sender_idx + 1, 3)
906
+ assert message.t2.shape == (2, sender_idx + 2)
907
+ assert torch.allclose(
908
+ message.t1, torch.full_like(message.t1, float(msg_idx))
909
+ )
910
+ assert torch.allclose(
911
+ message.t2, torch.full_like(message.t2, float(msg_idx + 100))
912
+ )
913
+ finally:
914
+ retrieval_done.set()
915
+ process_failures = _cleanup_processes(processes)
916
+
917
+ assert not process_failures, f"Worker cleanup failures: {process_failures}"
918
919
920
def test_concurrent_senders_interleaved_buffer():
@@ -829,7 +924,7 @@ def test_concurrent_senders_interleaved_buffer():
924
and verify the receiver correctly buffers and retrieves each tensor by
925
its (sender_id, message_id, tensor_id) handle.
926
"""
832
- tensor_queue = torch_mp.Queue()
927
+ tensor_queue = _MP_CTX.Queue()
928
929
# Sender A: 2 tensors for message 1
930
a_t0 = torch.randn(2, 3)
@@ -886,7 +981,7 @@ def test_mixed_cpu_cuda_with_ipc_enabled():
981
if not torch.cuda.is_available():
982
pytest.skip("CUDA not available")
983
889
- tensor_queue = torch_mp.Queue()
984
+ tensor_queue = _MP_CTX.Queue()
985
986
# Create sender and encoder with IPC enabled
987
sender = TensorIpcSender(tensor_queue)
@@ -903,7 +998,7 @@ def test_mixed_cpu_cuda_with_ipc_enabled():
998
def test_tensor_cleanup_after_decode():
999
"""Test that tensors are removed from tracking after successful decode."""
1000
# Create a tensor queue
906
- tensor_queue = torch_mp.Queue()
1001
+ tensor_queue = _MP_CTX.Queue()
1002
1003
# Create and encode a tensor
1004
tensor = torch.randn(5, 5)