from __future__ import annotations
import logging
logger = logging.getLogger(__name__)
class CudaUnavailableError(RuntimeError):
class CudaBackendError(RuntimeError):
def __init__(self, op: str, result_code, detail: str | None = None) -> None:
msg = f"{op} failed with CUDA result {result_code}"
if detail:
msg += f": {detail}"
super().__init__(msg)
self.op = op
self.result_code = result_code
def _import_cuda():
try:
from cuda.bindings import driver as cu
except ImportError as e:
raise CudaUnavailableError(
"cuda-python is not installed; CudaVmmBackend requires it. "
"Install with `pip install cuda-python`."
) from e
return cu
class CudaVmmBackend:
def __init__(self, device: int = 0) -> None:
self._cu = _import_cuda()
self._device = device
self._fabric_supported = None
self._ensure_context()
self._granularity = self._compute_granularity()
logger.debug(
"CudaVmmBackend initialized device=%d granularity=%d",
self._device,
self._granularity,
)
@property
def device(self) -> int:
return self._device
def reserve(self, total_bytes: int) -> int:
rounded = self._round_up(total_bytes, self._granularity)
ptr = self._call(
"cuMemAddressReserve",
self._cu.cuMemAddressReserve(rounded, self._granularity, 0, 0),
)
base = int(ptr)
logger.debug(
"cuMemAddressReserve base=0x%x rounded=%d (requested=%d)",
base,
rounded,
total_bytes,
)
return base
def allocate(self, va: int, size: int) -> int:
if size % self._granularity != 0:
raise ValueError(
f"size {size} is not a multiple of granularity {self._granularity}"
)
handle = None
if self._fabric_supported is not False:
fabric = self._fabric_handle_type()
if fabric is not None:
try:
handle = self._call(
"cuMemCreate",
self._cu.cuMemCreate(
size, self._allocation_prop(fabric), 0
),
)
if self._fabric_supported is None:
self._fabric_supported = True
logger.info(
"VMM arena: FABRIC-exportable allocations enabled"
)
except CudaBackendError as exc:
if (
self._fabric_supported is None
and exc.result_code in self._fabric_unsupported_codes()
):
self._fabric_supported = False
logger.warning(
"VMM arena: FABRIC handle type unsupported (%s); "
"falling back to non-exportable allocations -- "
"MNNVL cuda_ipc peer-to-peer will NOT be available",
exc,
)
else:
raise
if handle is None:
prop = self._allocation_prop()
handle = self._call(
"cuMemCreate", self._cu.cuMemCreate(size, prop, 0)
)
try:
self._call("cuMemMap", self._cu.cuMemMap(va, size, 0, handle, 0))
try:
access = self._access_desc()
self._call(
"cuMemSetAccess",
self._cu.cuMemSetAccess(va, size, [access], 1),
)
except Exception:
self._best_effort(
"cuMemUnmap (rollback)", self._cu.cuMemUnmap, va, size
)
raise
except Exception:
self._best_effort(
"cuMemRelease (rollback)", self._cu.cuMemRelease, handle
)
raise
logger.debug(
"cuMemMap va=0x%x size=%d handle=%d", va, size, int(handle)
)
return int(handle)
def deallocate(self, va: int, size: int, handle: int) -> None:
errors: list[tuple[str, BaseException]] = []
try:
self._call("cuMemUnmap", self._cu.cuMemUnmap(va, size))
except Exception as e:
errors.append(("cuMemUnmap", e))
try:
self._call("cuMemRelease", self._cu.cuMemRelease(handle))
except Exception as e:
errors.append((f"cuMemRelease(handle=0x{handle:x})", e))
logger.debug("deallocate va=0x%x size=%d handle=%d errors=%d",
va, size, handle, len(errors))
if errors:
details = "; ".join(f"{op}: {e}" for op, e in errors)
raise CudaBackendError(
"deallocate", None, f"{len(errors)} failure(s): {details}"
)
def release_reserve(self, base: int, total_bytes: int) -> None:
try:
self._call(
"cuMemAddressFree",
self._cu.cuMemAddressFree(base, total_bytes),
)
except Exception as e:
raise CudaBackendError(
"release_reserve", None, f"cuMemAddressFree failed: {e}"
) from e
logger.debug("cuMemAddressFree base=0x%x total=%d", base, total_bytes)
def allocation_granularity(self) -> int:
return self._granularity
def _best_effort(self, op: str, fn, *args) -> None:
try:
ret = fn(*args)
err = ret[0] if isinstance(ret, tuple) else ret
if err is not None and err != self._cu.CUresult.CUDA_SUCCESS:
try:
_, name = self._cu.cuGetErrorString(err)
detail = (
name.decode() if isinstance(name, bytes) else str(name)
)
except Exception:
detail = str(err)
logger.warning("%s failed during rollback: %s", op, detail)
except Exception as e:
logger.warning("%s raised during rollback: %s", op, e)
@staticmethod
def _round_up(value: int, multiple: int) -> int:
return ((value + multiple - 1) // multiple) * multiple
def _ensure_context(self) -> None:
err, ctx = self._cu.cuCtxGetCurrent()
if err == self._cu.CUresult.CUDA_ERROR_NOT_INITIALIZED:
raise CudaUnavailableError(
"CUDA driver not initialized; "
"call torch.cuda.init() and torch.cuda.set_device() before "
"creating CudaVmmBackend"
)
if err != self._cu.CUresult.CUDA_SUCCESS:
raise CudaBackendError("cuCtxGetCurrent", err)
if int(ctx) == 0:
raise CudaUnavailableError(
"no CUDA context on current thread; "
"call torch.cuda.set_device() or initialize CUDA before "
"creating CudaVmmBackend"
)
def _allocation_prop(self, handle_type=None):
prop = self._cu.CUmemAllocationProp()
prop.type = self._cu.CUmemAllocationType.CU_MEM_ALLOCATION_TYPE_PINNED
prop.location.type = (
self._cu.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE
)
prop.location.id = self._device
prop.allocFlags.gpuDirectRDMACapable = 1
if handle_type is not None:
prop.requestedHandleTypes = handle_type
return prop
def _fabric_handle_type(self):
try:
return self._cu.CUmemAllocationHandleType.CU_MEM_HANDLE_TYPE_FABRIC
except AttributeError:
logger.warning(
"cuda-python bindings expose no CU_MEM_HANDLE_TYPE_FABRIC; "
"arena memory will not be MNNVL-exportable (need >= 12.4)"
)
return None
def _fabric_unsupported_codes(self):
codes = []
for name in ("CUDA_ERROR_NOT_SUPPORTED", "CUDA_ERROR_NOT_PERMITTED"):
code = getattr(self._cu.CUresult, name, None)
if code is not None:
codes.append(code)
return tuple(codes)
def _access_desc(self):
access = self._cu.CUmemAccessDesc()
access.location.type = (
self._cu.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE
)
access.location.id = self._device
access.flags = (
self._cu.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_READWRITE
)
return access
def _compute_granularity(self) -> int:
prop = self._allocation_prop()
gran = self._call(
"cuMemGetAllocationGranularity",
self._cu.cuMemGetAllocationGranularity(
prop,
self._cu.CUmemAllocationGranularity_flags.CU_MEM_ALLOC_GRANULARITY_MINIMUM,
),
)
return int(gran)
def _call(self, op: str, result):
err, *rest = result
if err != self._cu.CUresult.CUDA_SUCCESS:
try:
_, name = self._cu.cuGetErrorString(err)
detail = name.decode() if isinstance(name, bytes) else str(name)
except Exception:
detail = None
raise CudaBackendError(op, err, detail)
if not rest:
return None
if len(rest) == 1:
return rest[0]
return tuple(rest)