Files
2026-07-13 12:24:33 +08:00

587 lines
18 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Standard
import multiprocessing as mp
import threading
import time
# Third Party
import pytest
import torch
# First Party
from lmcache.v1.multiprocess.futures import CUDAMessagingFuture, MessagingFuture
# ==============================================================================
# Helper Functions for CUDAMessagingFuture Tests
# ==============================================================================
def _create_cuda_event_in_process(event_queue: mp.Queue, delay: float = 0.0):
"""Helper process that creates a CUDA event and sends the IPC handle."""
torch.cuda.init()
if delay > 0:
time.sleep(delay)
# Create and record a CUDA event with interprocess flag
event = torch.cuda.Event(interprocess=True)
event.record()
event_bytes = event.ipc_handle()
# Send the event handle to the main process
event_queue.put(event_bytes)
def test_messaging_future_basic_usage():
"""Test basic usage of MessagingFuture: set result and retrieve it."""
future = MessagingFuture[int]()
# Initially, future should not be done
assert not future.query(), "Future should not be done initially"
# Set result
future.set_result(42)
# Future should now be done
assert future.query(), "Future should be done after setting result"
# Get result (should be immediate)
result = future.result(timeout=1)
assert result == 42, f"Expected result 42, got {result}"
def test_messaging_future_with_thread():
"""Test MessagingFuture with result set from another thread."""
future = MessagingFuture[str]()
def set_future_result():
time.sleep(0.5)
future.set_result("Hello from thread")
# Start thread that will set the result
thread = threading.Thread(target=set_future_result)
thread.start()
# Initially should not be done
assert not future.query(), "Future should not be done before thread sets result"
# Wait for result
result = future.result(timeout=2)
assert result == "Hello from thread", f"Expected 'Hello from thread', got {result}"
# Should be done now
assert future.query(), "Future should be done after getting result"
thread.join()
def test_messaging_future_wait_success():
"""Test wait method when result becomes available."""
future = MessagingFuture[int]()
def set_future_result():
time.sleep(0.3)
future.set_result(100)
thread = threading.Thread(target=set_future_result)
thread.start()
# Wait should return True when result is set
success = future.wait(timeout=1)
assert success, "Wait should return True when result is available"
assert future.query(), "Future should be done after wait returns True"
thread.join()
def test_messaging_future_wait_timeout():
"""Test wait method when timeout is reached."""
future = MessagingFuture[int]()
# Wait with short timeout (result never set)
start_time = time.time()
success = future.wait(timeout=0.2)
elapsed = time.time() - start_time
assert not success, "Wait should return False on timeout"
assert not future.query(), "Future should not be done after timeout"
assert 0.15 < elapsed < 0.3, f"Wait should respect timeout, elapsed: {elapsed}"
def test_messaging_future_result_timeout():
"""Test result method raises TimeoutError when timeout is reached."""
future = MessagingFuture[int]()
# Try to get result with timeout (result never set)
with pytest.raises(
TimeoutError, match="Future result not available within timeout"
):
future.result(timeout=0.2)
assert not future.query(), "Future should not be done after timeout"
def test_messaging_future_wait_no_timeout():
"""Test wait method without timeout (waits indefinitely until result is set)."""
future = MessagingFuture[float]()
def set_future_result():
time.sleep(0.3)
future.set_result(3.14)
thread = threading.Thread(target=set_future_result)
thread.start()
# Wait without timeout should wait until result is available
success = future.wait() # No timeout parameter
assert success, "Wait should return True when result is set"
assert future.result() == 3.14, "Result should be accessible after wait"
thread.join()
def test_messaging_future_multiple_result_calls():
"""Test that result can be retrieved multiple times after being set."""
future = MessagingFuture[str]()
future.set_result("persistent value")
# Get result multiple times
result1 = future.result(timeout=0.1)
result2 = future.result(timeout=0.1)
result3 = future.result(timeout=0.1)
assert result1 == result2 == result3 == "persistent value", (
"Result should be retrievable multiple times"
)
def test_messaging_future_complex_type():
"""Test MessagingFuture with complex types like lists and dicts."""
future = MessagingFuture[dict]()
complex_data = {"key1": [1, 2, 3], "key2": {"nested": "value"}, "key3": 42}
def set_future_result():
time.sleep(0.2)
future.set_result(complex_data)
thread = threading.Thread(target=set_future_result)
thread.start()
result = future.result(timeout=1)
assert result == complex_data, "Complex types should be preserved"
thread.join()
# ==============================================================================
# CUDAMessagingFuture Tests
# ==============================================================================
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_cuda_messaging_future_basic_usage():
"""Test basic usage of CUDAMessagingFuture: create, wait, and get result."""
torch.cuda.init()
# Create CUDA event in a separate process
ctx = mp.get_context("spawn")
event_queue = ctx.Queue()
process = ctx.Process(target=_create_cuda_event_in_process, args=(event_queue,))
process.start()
# Get event bytes from the process
event_bytes = event_queue.get(timeout=30)
process.join(timeout=2)
# Create the raw future that will return (event_bytes, result_value)
raw_future = MessagingFuture[tuple[bytes, int]]()
# Create CUDAMessagingFuture from raw future
cuda_future = CUDAMessagingFuture.FromMessagingFuture(raw_future)
# Initially, future should not be done
assert not cuda_future.query(), "CUDAMessagingFuture should not be done initially"
# Set result in raw future
raw_future.set_result((event_bytes, 42))
# Wait for CUDA future to complete
success = cuda_future.wait()
assert success, "Wait should return True when result is available"
# Get result
result = cuda_future.result()
assert result == 42, f"Expected result 42, got {result}"
# Query should return True after completion
assert cuda_future.query(), "CUDAMessagingFuture should be done after wait"
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_cuda_messaging_future_with_thread():
"""Test CUDAMessagingFuture with result set from another thread."""
torch.cuda.init()
# Create CUDA event in a separate process
ctx = mp.get_context("spawn")
event_queue = ctx.Queue()
process = ctx.Process(target=_create_cuda_event_in_process, args=(event_queue,))
process.start()
# Get event bytes from the process
event_bytes = event_queue.get(timeout=30)
process.join(timeout=2)
raw_future = MessagingFuture[tuple[bytes, str]]()
cuda_future = CUDAMessagingFuture.FromMessagingFuture(raw_future)
def set_future_result():
time.sleep(0.5)
raw_future.set_result((event_bytes, "Hello CUDA"))
# Start thread that will set the result
thread = threading.Thread(target=set_future_result)
thread.start()
# Initially should not be done
assert not cuda_future.query(), (
"Future should not be done before thread sets result"
)
# Wait for result
result = cuda_future.result()
assert result == "Hello CUDA", f"Expected 'Hello CUDA', got {result}"
# Should be done now
assert cuda_future.query(), "Future should be done after getting result"
thread.join()
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_cuda_messaging_future_wait_no_timeout():
"""Test wait method without timeout (waits indefinitely
until result is set).
"""
torch.cuda.init()
# Create CUDA event in a separate process
ctx = mp.get_context("spawn")
event_queue = ctx.Queue()
process = ctx.Process(target=_create_cuda_event_in_process, args=(event_queue,))
process.start()
# Get event bytes from the process
event_bytes = event_queue.get(timeout=30)
process.join(timeout=2)
raw_future = MessagingFuture[tuple[bytes, float]]()
cuda_future = CUDAMessagingFuture.FromMessagingFuture(raw_future)
def set_future_result():
time.sleep(0.3)
raw_future.set_result((event_bytes, 3.14))
thread = threading.Thread(target=set_future_result)
thread.start()
# Wait without timeout should wait until result is available
success = cuda_future.wait() # No timeout parameter
assert success, "Wait should return True when result is set"
assert cuda_future.result() == 3.14, "Result should be accessible after wait"
thread.join()
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_cuda_messaging_future_wait_with_timeout_success():
"""Test that wait method works correctly with timeout when result is available."""
torch.cuda.init()
# Create CUDA event in a separate process
ctx = mp.get_context("spawn")
event_queue = ctx.Queue()
process = ctx.Process(target=_create_cuda_event_in_process, args=(event_queue,))
process.start()
# Get event bytes from the process
event_bytes = event_queue.get(timeout=30)
process.join(timeout=2)
raw_future = MessagingFuture[tuple[bytes, int]]()
cuda_future = CUDAMessagingFuture.FromMessagingFuture(raw_future)
def set_future_result():
time.sleep(0.3)
raw_future.set_result((event_bytes, 123))
thread = threading.Thread(target=set_future_result)
thread.start()
# Wait with timeout should return True when result is available
success = cuda_future.wait(timeout=2.0)
assert success, "Wait with timeout should return True when result is available"
assert cuda_future.result() == 123, "Result should be accessible after wait"
thread.join()
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_cuda_messaging_future_wait_timeout_reached():
"""Test that wait method returns False when timeout is reached."""
torch.cuda.init()
raw_future = MessagingFuture[tuple[bytes, int]]()
cuda_future = CUDAMessagingFuture.FromMessagingFuture(raw_future)
# Wait with short timeout (result never set)
start_time = time.time()
success = cuda_future.wait(timeout=0.2)
elapsed = time.time() - start_time
assert not success, "Wait should return False on timeout"
assert not cuda_future.query(), "Future should not be done after timeout"
assert 0.15 < elapsed < 0.4, f"Wait should respect timeout, elapsed: {elapsed}"
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_cuda_messaging_future_result_with_timeout_success():
"""Test that result method works correctly with timeout when result is available."""
torch.cuda.init()
# Create CUDA event in a separate process
ctx = mp.get_context("spawn")
event_queue = ctx.Queue()
process = ctx.Process(target=_create_cuda_event_in_process, args=(event_queue,))
process.start()
# Get event bytes from the process
event_bytes = event_queue.get(timeout=30)
process.join(timeout=2)
raw_future = MessagingFuture[tuple[bytes, int]]()
cuda_future = CUDAMessagingFuture.FromMessagingFuture(raw_future)
def set_future_result():
time.sleep(0.3)
raw_future.set_result((event_bytes, 456))
thread = threading.Thread(target=set_future_result)
thread.start()
# Get result with timeout should succeed when result is available
result = cuda_future.result(timeout=2.0)
assert result == 456, f"Expected result 456, got {result}"
thread.join()
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_cuda_messaging_future_result_timeout_reached():
"""Test that result method raises TimeoutError when timeout is reached."""
torch.cuda.init()
raw_future = MessagingFuture[tuple[bytes, int]]()
cuda_future = CUDAMessagingFuture.FromMessagingFuture(raw_future)
# Try to get result with timeout (result never set)
with pytest.raises(
TimeoutError, match="CUDAMessagingFuture result not available within timeout"
):
cuda_future.result(timeout=0.2)
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_cuda_messaging_future_multiple_result_calls():
"""Test that result can be retrieved multiple times after being set."""
torch.cuda.init()
# Create CUDA event in a separate process
ctx = mp.get_context("spawn")
event_queue = ctx.Queue()
process = ctx.Process(target=_create_cuda_event_in_process, args=(event_queue,))
process.start()
# Get event bytes from the process
event_bytes = event_queue.get(timeout=30)
process.join(timeout=2)
raw_future = MessagingFuture[tuple[bytes, str]]()
cuda_future = CUDAMessagingFuture.FromMessagingFuture(raw_future)
raw_future.set_result((event_bytes, "persistent cuda value"))
# Get result multiple times
result1 = cuda_future.result()
result2 = cuda_future.result()
result3 = cuda_future.result()
assert result1 == result2 == result3 == "persistent cuda value", (
"Result should be retrievable multiple times"
)
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_cuda_messaging_future_query_before_and_after():
"""Test query method returns False before completion and True after."""
torch.cuda.init()
# Create CUDA event in a separate process
ctx = mp.get_context("spawn")
event_queue = ctx.Queue()
process = ctx.Process(target=_create_cuda_event_in_process, args=(event_queue,))
process.start()
# Get event bytes from the process
event_bytes = event_queue.get(timeout=30)
process.join(timeout=2)
raw_future = MessagingFuture[tuple[bytes, int]]()
cuda_future = CUDAMessagingFuture.FromMessagingFuture(raw_future)
# Query before setting result
assert not cuda_future.query(), "Query should return False before result is set"
# Set result
raw_future.set_result((event_bytes, 100))
# Wait for completion
cuda_future.wait()
# Query after setting result
assert cuda_future.query(), (
"Query should return True after result is set and waited"
)
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_cuda_messaging_future_complex_type():
"""Test CUDAMessagingFuture with complex types like lists and dicts."""
torch.cuda.init()
# Create CUDA event in a separate process
ctx = mp.get_context("spawn")
event_queue = ctx.Queue()
process = ctx.Process(target=_create_cuda_event_in_process, args=(event_queue,))
process.start()
# Get event bytes from the process
event_bytes = event_queue.get(timeout=30)
process.join(timeout=2)
complex_data = {"key1": [1, 2, 3], "key2": {"nested": "value"}, "key3": 42}
raw_future = MessagingFuture[tuple[bytes, dict]]()
cuda_future = CUDAMessagingFuture.FromMessagingFuture(raw_future)
def set_future_result():
time.sleep(0.2)
raw_future.set_result((event_bytes, complex_data))
thread = threading.Thread(target=set_future_result)
thread.start()
result = cuda_future.result()
assert result == complex_data, "Complex types should be preserved"
thread.join()
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_messaging_future_to_cuda_future():
"""Test converting MessagingFuture to CUDAMessagingFuture
using to_cuda_future method.
"""
torch.cuda.init()
# Create CUDA event in a separate process
ctx = mp.get_context("spawn")
event_queue = ctx.Queue()
process = ctx.Process(target=_create_cuda_event_in_process, args=(event_queue,))
process.start()
# Get event bytes from the process
event_bytes = event_queue.get(timeout=30)
process.join(timeout=2)
raw_future = MessagingFuture[tuple[bytes, int]]()
# Convert to CUDA future
cuda_future = raw_future.to_cuda_future()
# Verify it's a CUDAMessagingFuture instance
assert isinstance(cuda_future, CUDAMessagingFuture), (
"to_cuda_future should return CUDAMessagingFuture instance"
)
# Set result and verify it works
raw_future.set_result((event_bytes, 999))
result = cuda_future.result()
assert result == 999, f"Expected result 999, got {result}"
@pytest.mark.skipif(
not torch.cuda.is_available(),
reason="CUDA is required for CUDAMessagingFuture tests",
)
def test_cuda_messaging_future_with_explicit_device():
"""Test CUDAMessagingFuture with explicit device parameter."""
torch.cuda.init()
# Create CUDA event in a separate process
ctx = mp.get_context("spawn")
event_queue = ctx.Queue()
process = ctx.Process(target=_create_cuda_event_in_process, args=(event_queue,))
process.start()
# Get event bytes from the process
event_bytes = event_queue.get(timeout=30)
process.join(timeout=2)
device = torch.cuda.current_device()
raw_future = MessagingFuture[tuple[bytes, str]]()
# Create CUDA future with explicit device
cuda_future = CUDAMessagingFuture.FromMessagingFuture(raw_future, device=device)
# Set result
raw_future.set_result((event_bytes, "explicit device"))
# Get result
result = cuda_future.result()
assert result == "explicit device", f"Expected 'explicit device', got {result}"