from __future__ import annotations
import pytest
def _cuda_available() -> bool:
try:
import torch
except ImportError:
return False
if not torch.cuda.is_available():
return False
try:
from cuda.bindings import driver except ImportError:
return False
return True
pytestmark = pytest.mark.skipif(
not _cuda_available(),
reason="requires CUDA + cuda-python",
)
@pytest.fixture(scope="module")
def cuda_init():
import torch
torch.cuda.init()
torch.cuda.set_device(0)
_ = torch.zeros(1, device="cuda:0")
torch.cuda.synchronize()
yield 0
class TestConstruction:
def test_can_construct(self, cuda_init):
from modelexpress.vmm.backend import CudaVmmBackend
backend = CudaVmmBackend(device=cuda_init)
assert backend.device == cuda_init
assert backend.allocation_granularity() > 0
def test_granularity_is_power_of_2(self, cuda_init):
from modelexpress.vmm.backend import CudaVmmBackend
backend = CudaVmmBackend(device=cuda_init)
gran = backend.allocation_granularity()
assert gran & (gran - 1) == 0 assert gran <= 64 * 1024 * 1024
def test_no_context_raises(self):
pytest.skip("would need a process-isolated test harness")
class TestAllocationLifecycle:
def test_reserve_returns_nonzero_va(self, cuda_init):
from modelexpress.vmm.backend import CudaVmmBackend
backend = CudaVmmBackend(device=cuda_init)
size = backend.allocation_granularity() * 4
base = backend.reserve(size)
try:
assert base != 0
assert base % backend.allocation_granularity() == 0
finally:
backend.release_reserve(base, size)
def test_reserve_rounds_up(self, cuda_init):
from modelexpress.vmm.backend import CudaVmmBackend
backend = CudaVmmBackend(device=cuda_init)
gran = backend.allocation_granularity()
base = backend.reserve(gran + 1)
try:
assert base % gran == 0
finally:
backend.release_reserve(base, 2 * gran)
def test_allocate_basic(self, cuda_init):
import torch
from modelexpress.vmm.backend import CudaVmmBackend
backend = CudaVmmBackend(device=cuda_init)
gran = backend.allocation_granularity()
size = gran * 2
base = backend.reserve(size)
handle = None
try:
free_before, _ = torch.cuda.mem_get_info(cuda_init)
handle = backend.allocate(base, gran)
free_after, _ = torch.cuda.mem_get_info(cuda_init)
assert handle != 0
assert free_after < free_before
assert free_before - free_after >= gran // 2 finally:
if handle is not None:
backend.deallocate(base, gran, handle)
backend.release_reserve(base, size)
def test_deallocate_releases_memory(self, cuda_init):
import torch
from modelexpress.vmm.backend import CudaVmmBackend
backend = CudaVmmBackend(device=cuda_init)
gran = backend.allocation_granularity()
size = gran * 2
base = backend.reserve(size)
handle = backend.allocate(base, gran)
try:
free_with_allocation, _ = torch.cuda.mem_get_info(cuda_init)
backend.deallocate(base, gran, handle)
handle = None
free_after_deallocate, _ = torch.cuda.mem_get_info(cuda_init)
assert free_after_deallocate > free_with_allocation
assert free_after_deallocate - free_with_allocation >= gran // 2
finally:
if handle is not None:
backend.deallocate(base, gran, handle)
backend.release_reserve(base, size)
class TestArenaWithRealBackend:
def test_single_allocation_returns_writable_memory(self, cuda_init):
import torch
from modelexpress.vmm.arena import VmmArena
from modelexpress.vmm.backend import CudaVmmBackend
backend = CudaVmmBackend(device=cuda_init)
gran = backend.allocation_granularity()
with VmmArena(total_bytes=gran * 4, backend=backend) as arena:
ptr = arena.malloc(1024 * 1024, alignment=256)
device = torch.device(f"cuda:{cuda_init}")
storage = torch._C._construct_storage_from_data_pointer(
ptr, device, 1024 * 1024
)
tensor = torch.empty(0, dtype=torch.uint8, device=device).set_(
storage, 0, (1024 * 1024,)
)
tensor.fill_(0xAB)
torch.cuda.synchronize()
assert tensor.cpu().tolist().count(0xAB) == 1024 * 1024
def test_per_allocation_mapping(self, cuda_init):
import torch
from modelexpress.vmm.arena import VmmArena
from modelexpress.vmm.backend import CudaVmmBackend
backend = CudaVmmBackend(device=cuda_init)
gran = backend.allocation_granularity()
free_start, _ = torch.cuda.mem_get_info(cuda_init)
with VmmArena(total_bytes=gran * 8, backend=backend) as arena:
free_empty_arena, _ = torch.cuda.mem_get_info(cuda_init)
assert abs(free_empty_arena - free_start) < gran
arena.malloc(1024)
assert arena.live_allocation_count == 1
assert arena.mapped_bytes == gran
free_one_allocation, _ = torch.cuda.mem_get_info(cuda_init)
assert free_start - free_one_allocation >= gran // 2
arena.malloc(gran * 2)
assert arena.live_allocation_count == 2
assert arena.mapped_bytes == gran * 3
free_two_allocations, _ = torch.cuda.mem_get_info(cuda_init)
assert free_one_allocation - free_two_allocations >= gran
def test_free_releases_one_allocation(self, cuda_init):
import torch
from modelexpress.vmm.arena import VmmArena
from modelexpress.vmm.backend import CudaVmmBackend
backend = CudaVmmBackend(device=cuda_init)
gran = backend.allocation_granularity()
with VmmArena(total_bytes=gran * 8, backend=backend) as arena:
ptr1 = arena.malloc(1024)
arena.malloc(gran * 2)
used_before = arena.used_bytes
mapped_before = arena.mapped_bytes
free_before_free, _ = torch.cuda.mem_get_info(cuda_init)
arena.free(ptr1)
free_after_free, _ = torch.cuda.mem_get_info(cuda_init)
assert arena.used_bytes == used_before
assert arena.mapped_bytes == mapped_before - gran
assert arena.live_allocation_count == 1
assert free_after_free - free_before_free >= gran // 2
def test_close_releases_all_allocations(self, cuda_init):
import torch
from modelexpress.vmm.arena import VmmArena
from modelexpress.vmm.backend import CudaVmmBackend
backend = CudaVmmBackend(device=cuda_init)
gran = backend.allocation_granularity()
free_start, _ = torch.cuda.mem_get_info(cuda_init)
arena = VmmArena(total_bytes=gran * 8, backend=backend)
for _ in range(3):
arena.malloc(gran * 2)
free_after_alloc, _ = torch.cuda.mem_get_info(cuda_init)
assert free_start - free_after_alloc >= 3 * gran
assert arena.mapped_bytes == 6 * gran
arena.close()
free_after_close, _ = torch.cuda.mem_get_info(cuda_init)
assert free_after_close - free_after_alloc >= 3 * gran
assert abs(free_after_close - free_start) < gran