mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-28 14:57:09 +08:00
163 lines
5.9 KiB
Python
163 lines
5.9 KiB
Python
from collections.abc import Generator
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from threading import Event
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from deepinfer import shared_memory
|
|
from deepinfer.shared_memory import SharedTorchMemoryPool
|
|
|
|
pytestmark = [pytest.mark.ci, pytest.mark.ci_npu]
|
|
|
|
|
|
@pytest.fixture
|
|
def pool(monkeypatch: pytest.MonkeyPatch) -> Generator[SharedTorchMemoryPool, None, None]:
|
|
# Exercise real shared storage and allocator metadata without an accelerator.
|
|
monkeypatch.setattr(shared_memory.PLATFORM, "pin_host_memory", lambda _: None)
|
|
monkeypatch.setattr(shared_memory.PLATFORM, "unpin_host_memory", lambda _: None)
|
|
pool = SharedTorchMemoryPool(256, max_blocks=32)
|
|
yield pool
|
|
pool.close()
|
|
|
|
|
|
def free_group(pool: SharedTorchMemoryPool, handles: dict[str, tuple]) -> None:
|
|
for handle, _shape, _dtype in handles.values():
|
|
pool.free(handle)
|
|
|
|
|
|
def free_bytes(pool: SharedTorchMemoryPool) -> int:
|
|
return sum(size for _, _, size in pool.dump_free_list())
|
|
|
|
|
|
@pytest.mark.parametrize("sizes", [(65,), (33, 17, 1), (32, 32, 1)])
|
|
def test_rejects_impossible_group_before_allocating(
|
|
pool: SharedTorchMemoryPool, monkeypatch: pytest.MonkeyPatch, sizes: tuple[int, ...]
|
|
) -> None:
|
|
calls = []
|
|
original = pool.alloc_tensor
|
|
|
|
def allocate(*args: object, **kwargs: object) -> tuple:
|
|
calls.append(args)
|
|
return original(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(pool, "alloc_tensor", allocate)
|
|
with pytest.raises(MemoryError, match="tensor group requires.*pool capacity"):
|
|
pool.copy_tensors({str(i): torch.ones(n) for i, n in enumerate(sizes)})
|
|
assert calls == []
|
|
assert free_bytes(pool) == 256
|
|
|
|
|
|
def test_exact_capacity_and_empty_tensors(pool: SharedTorchMemoryPool) -> None:
|
|
values = {"first": torch.arange(32), "empty": torch.empty(0)}
|
|
handles = pool.copy_tensors(values)
|
|
assert set(handles) == {"first"}
|
|
handle, shape, dtype = handles["first"]
|
|
assert torch.equal(pool.get_tensor(handle, shape, dtype), values["first"])
|
|
assert free_bytes(pool) == 0
|
|
free_group(pool, handles)
|
|
assert free_bytes(pool) == 256
|
|
assert pool.copy_tensors({"empty": torch.empty(0)}) == {}
|
|
|
|
|
|
def test_copy_failure_returns_all_allocations(pool: SharedTorchMemoryPool) -> None:
|
|
sparse = torch.sparse_coo_tensor(torch.tensor([[0], [0]]), torch.tensor([1.0]), (2, 2), check_invariants=True)
|
|
for _ in range(3):
|
|
with pytest.raises(RuntimeError):
|
|
pool.copy_tensors({"valid": torch.ones(16), "invalid": sparse})
|
|
assert free_bytes(pool) == 256
|
|
handles = pool.copy_tensors({"retry": torch.ones(64)})
|
|
free_group(pool, handles)
|
|
assert free_bytes(pool) == 256
|
|
|
|
|
|
def test_allocation_failure_returns_earlier_blocks(pool: SharedTorchMemoryPool, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
original = pool.alloc_tensor
|
|
calls = 0
|
|
|
|
def allocate(*args: object, **kwargs: object) -> tuple:
|
|
nonlocal calls
|
|
calls += 1
|
|
if calls == 2:
|
|
raise MemoryError("metadata exhausted")
|
|
return original(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(pool, "alloc_tensor", allocate)
|
|
with pytest.raises(MemoryError, match="metadata exhausted"):
|
|
pool.copy_tensors({"first": torch.ones(16), "second": torch.ones(16)})
|
|
assert free_bytes(pool) == 256
|
|
|
|
|
|
def test_producers_serialize_groups_while_consumers_can_free(pool: SharedTorchMemoryPool, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
first_allocated = Event()
|
|
continue_first = Event()
|
|
second_started = Event()
|
|
second_allocated = Event()
|
|
original = pool.alloc_tensor
|
|
|
|
def allocate(shape: tuple[int, ...], dtype: torch.dtype = torch.float32) -> tuple:
|
|
result = original(shape, dtype)
|
|
if not first_allocated.is_set():
|
|
first_allocated.set()
|
|
assert continue_first.wait(5)
|
|
elif not continue_first.is_set():
|
|
second_allocated.set()
|
|
return result
|
|
|
|
monkeypatch.setattr(pool, "alloc_tensor", allocate)
|
|
values = {"first": torch.ones(32), "second": torch.ones(16)}
|
|
|
|
def second_producer() -> dict[str, tuple]:
|
|
second_started.set()
|
|
return pool.copy_tensors(values)
|
|
|
|
# Timeouts turn regressions into failures rather than hanging the suite.
|
|
executor = ThreadPoolExecutor(2)
|
|
try:
|
|
first = executor.submit(pool.copy_tensors, values)
|
|
assert first_allocated.wait(5)
|
|
second = executor.submit(second_producer)
|
|
assert second_started.wait(5)
|
|
assert not second_allocated.wait(0.1)
|
|
continue_first.set()
|
|
first_handles = first.result(timeout=5)
|
|
# The second producer may be waiting for memory while holding the group
|
|
# lock. A consumer must still be able to release the first group.
|
|
free_group(pool, first_handles)
|
|
second_handles = second.result(timeout=5)
|
|
free_group(pool, second_handles)
|
|
assert free_bytes(pool) == 256
|
|
finally:
|
|
continue_first.set()
|
|
executor.shutdown(wait=False, cancel_futures=True)
|
|
|
|
|
|
def copy_group_worker(pool: SharedTorchMemoryPool, output: object) -> None:
|
|
handles = pool.copy_tensors({"first": torch.ones(32), "second": torch.ones(16)})
|
|
output.put(handles)
|
|
|
|
|
|
def test_group_lock_is_shared_between_processes(pool: SharedTorchMemoryPool) -> None:
|
|
import multiprocessing
|
|
|
|
context = multiprocessing.get_context("fork")
|
|
output = context.Queue()
|
|
processes = [context.Process(target=copy_group_worker, args=(pool, output)) for _ in range(2)]
|
|
try:
|
|
for process in processes:
|
|
process.start()
|
|
for _ in processes:
|
|
handles = output.get(timeout=10)
|
|
free_group(pool, handles)
|
|
for process in processes:
|
|
process.join(timeout=5)
|
|
assert process.exitcode == 0
|
|
assert free_bytes(pool) == 256
|
|
finally:
|
|
for process in processes:
|
|
if process.is_alive():
|
|
process.terminate()
|
|
process.join(timeout=5)
|
|
output.close()
|
|
output.join_thread()
|