from __future__ import annotations
import atexit
import logging
import os
import time
from typing import TYPE_CHECKING, Any
import torch
from . import envs
from . import ucx_utils
from ._nixl import load_nixl_api
from .accelerators import (
AcceleratorBackend,
CudaAcceleratorBackend,
)
from .types import ManifestMismatchError, TensorDescriptor
if TYPE_CHECKING:
from .vmm.arena import VmmArena
logger = logging.getLogger("modelexpress.nixl_transfer")
NIXL_AVAILABLE = False
NixlAgent = None
nixl_agent_config = None
_nixl_api = load_nixl_api()
if _nixl_api is not None:
NixlAgent = _nixl_api.nixl_agent
nixl_agent_config = _nixl_api.nixl_agent_config
NIXL_AVAILABLE = True
SUPPORTED_NIXL_BACKENDS = ("UCX", "LIBFABRIC")
DEFAULT_NIXL_BACKEND = "UCX"
NIXL_DRAM_MEM_TYPE = "DRAM"
def is_nixl_available() -> bool:
return NIXL_AVAILABLE
def _resolve_nixl_backend() -> str:
raw = envs.MX_NIXL_BACKEND
if raw not in SUPPORTED_NIXL_BACKENDS:
raise ValueError(
f"MX_NIXL_BACKEND={raw!r} is not supported. "
f"Expected one of {SUPPORTED_NIXL_BACKENDS}."
)
return raw
def _pool_reg_enabled() -> bool:
return envs.MX_POOL_REG
class NixlTransferManager:
def __init__(
self,
agent_name: str,
device_id: int,
listen_port: int | None = None,
accelerator_backend: AcceleratorBackend | None = None,
):
self._agent_name = agent_name
self._device_id = device_id
self._listen_port = listen_port
self._accelerator_backend = accelerator_backend or CudaAcceleratorBackend()
self._backend = _resolve_nixl_backend()
self._backends = [self._backend]
self._agent: Any = None
self._metadata: bytes = b""
self._tensor_descriptors: list[TensorDescriptor] = []
self._tensors: dict[str, torch.Tensor] = {}
self._remote_agents: dict[str, tuple[str, int] | None] = {}
self._data_plane_error: str | None = None
self._atexit_registered = False
@property
def agent_name(self) -> str:
return self._agent_name
@property
def nixl_metadata(self) -> bytes:
return self._metadata
@property
def tensor_descriptors(self) -> list[TensorDescriptor]:
return self._tensor_descriptors
def initialize(self) -> None:
if not NIXL_AVAILABLE:
raise RuntimeError("NIXL is not available")
if self._agent is not None:
return
self._accelerator_backend.set_device(self._device_id)
saved_ucx_tls = envs.UCX_TLS
nixl_ucx_tls = envs.NIXL_UCX_TLS
if nixl_ucx_tls:
os.environ["UCX_TLS"] = nixl_ucx_tls
logger.info(f"NIXL UCX_TLS override: {nixl_ucx_tls} (was: {saved_ucx_tls})")
elif saved_ucx_tls == "tcp":
os.environ.pop("UCX_TLS", None)
logger.info("NIXL: removed UCX_TLS=tcp for auto-detection")
ucx_utils.apply_nic_pin_for_device(self._device_id)
try:
if self._listen_port is not None and nixl_agent_config:
config = nixl_agent_config(
backends=self._backends,
enable_listen_thread=True,
listen_port=self._listen_port,
)
logger.info(
f"NIXL listen thread enabled on port {self._listen_port}"
)
elif nixl_agent_config:
config = nixl_agent_config(backends=self._backends)
else:
config = None
self._agent = NixlAgent(self._agent_name, config)
self._register_atexit()
logger.info(
f"NIXL agent '{self._agent_name}' created on device "
f"{self._device_id} (backend={self._backend})"
)
finally:
if saved_ucx_tls is not None:
os.environ["UCX_TLS"] = saved_ucx_tls
elif envs.is_set("UCX_TLS"):
os.environ.pop("UCX_TLS")
def _register_atexit(self) -> None:
if self._atexit_registered:
return
atexit.register(self.shutdown)
self._atexit_registered = True
def _build_tensor_descriptors(
self, tensors: dict[str, torch.Tensor]
) -> list[TensorDescriptor]:
self._tensors = dict(tensors)
tensor_descriptors = []
for name, tensor in tensors.items():
if not tensor.is_contiguous():
raise RuntimeError(
f"Tensor '{name}' is not contiguous. "
"Non-contiguous tensors cannot be used for RDMA transfers."
)
tensor_descriptors.append(TensorDescriptor(
name=name,
addr=tensor.data_ptr(),
size=tensor.numel() * tensor.element_size(),
device_id=self._device_id,
dtype=str(tensor.dtype),
))
self._tensor_descriptors = tensor_descriptors
return tensor_descriptors
def register_tensors(self, tensors: dict[str, torch.Tensor]) -> bytes:
if self._agent is None:
raise RuntimeError("NIXL agent not initialized")
tensor_descriptors = self._build_tensor_descriptors(tensors)
alloc_discovery_start = time.perf_counter()
if _pool_reg_enabled():
if self._accelerator_backend.supports_pool_reg():
allocations = self._find_cuda_allocations(tensor_descriptors)
else:
allocations = None
logger.warning(
"MX_POOL_REG=1 set but %s does not support pool "
"registration; using per-tensor registration",
self._accelerator_backend.name,
)
else:
allocations = None
logger.info("Pool registration disabled (MX_POOL_REG != '1'), using per-tensor registration")
alloc_discovery_time = time.perf_counter() - alloc_discovery_start
nixl_reg_start = time.perf_counter()
if allocations:
alloc_tuples = [
(base, size, self._device_id, "")
for base, size in allocations
]
self._agent.register_memory(
alloc_tuples,
mem_type=self._accelerator_backend.nixl_mem_type,
backends=self._backends,
)
reg_count = len(allocations)
else:
tensor_list = list(tensors.values())
self._agent.register_memory(tensor_list, backends=self._backends)
reg_count = len(tensor_list)
nixl_reg_time = time.perf_counter() - nixl_reg_start
metadata_start = time.perf_counter()
self._metadata = self._agent.get_agent_metadata()
metadata_time = time.perf_counter() - metadata_start
total_time = alloc_discovery_time + nixl_reg_time + metadata_time
reduction = (1 - reg_count / len(tensor_descriptors)) * 100 if tensor_descriptors else 0
total_bytes = sum(d.size for d in tensor_descriptors)
logger.info(
f"[TIMING] register_tensors: {total_time:.3f}s total "
f"(alloc_discovery={alloc_discovery_time:.3f}s, "
f"nixl_register={nixl_reg_time:.3f}s [{reg_count} regions], "
f"get_metadata={metadata_time:.3f}s [{len(self._metadata)} bytes])"
)
logger.info(
f"Registered {reg_count} regions from {len(tensor_descriptors)} tensors "
f"({reduction:.1f}% reduction), {total_bytes / 1e9:.2f} GB total"
)
return self._metadata
def register_arena(self, arena: VmmArena, tensors: dict[str, torch.Tensor]) -> bytes:
if self._agent is None:
raise RuntimeError("NIXL agent not initialized")
tensor_descriptors = self._build_tensor_descriptors(tensors)
base, used = arena.registered_range()
if used == 0:
logger.warning(
"register_arena called with empty arena (used=0); falling back "
"to per-tensor registration"
)
return self.register_tensors(tensors)
if not self._accelerator_backend.supports_vmm():
logger.warning(
"%s does not support VMM arena registration; falling back "
"to per-tensor registration",
self._accelerator_backend.name,
)
return self.register_tensors(tensors)
nixl_reg_start = time.perf_counter()
self._agent.register_memory(
[(base, used, self._device_id, "")],
mem_type=self._accelerator_backend.nixl_mem_type,
backends=self._backends,
)
nixl_reg_time = time.perf_counter() - nixl_reg_start
metadata_start = time.perf_counter()
self._metadata = self._agent.get_agent_metadata()
metadata_time = time.perf_counter() - metadata_start
total_bytes = sum(d.size for d in tensor_descriptors)
reduction = (1 - 1 / len(tensor_descriptors)) * 100 if tensor_descriptors else 0
logger.info(
f"[TIMING] register_arena: {nixl_reg_time + metadata_time:.3f}s total "
f"(nixl_register={nixl_reg_time:.3f}s [1 region, {used / 1e9:.2f} GB], "
f"get_metadata={metadata_time:.3f}s [{len(self._metadata)} bytes])"
)
logger.info(
f"Registered arena as 1 region from {len(tensor_descriptors)} tensors "
f"({reduction:.1f}% reduction), {total_bytes / 1e9:.2f} GB live in "
f"{used / 1e9:.2f} GB arena bump range"
)
return self._metadata
@staticmethod
def _find_cuda_allocations(
descriptors: list[TensorDescriptor],
) -> list[tuple[int, int]]:
if not descriptors:
return []
from cuda.bindings import driver as cuda_driver
seen: dict[int, int] = {}
for desc in descriptors:
err, alloc_base, alloc_size = cuda_driver.cuMemGetAddressRange(desc.addr)
if err != cuda_driver.CUresult.CUDA_SUCCESS:
raise RuntimeError(
f"cuMemGetAddressRange failed ({err.name}) for tensor "
f"'{desc.name}' at 0x{desc.addr:x}. Is the tensor on a CUDA device?"
)
base_int = int(alloc_base)
if base_int not in seen:
seen[base_int] = int(alloc_size)
return sorted(seen.items())
def _wait_for_xfer(
self,
handle: Any,
timeout_seconds: float | None,
label: str,
) -> None:
if self._agent is None:
raise RuntimeError("NIXL agent not initialized")
wait_start = time.perf_counter()
while True:
if (
timeout_seconds is not None
and time.perf_counter() - wait_start >= timeout_seconds
):
self._data_plane_error = (
f"{label} timed out after {timeout_seconds:.1f}s with no "
f"completion and no error status from NIXL"
)
raise TimeoutError(f"{label} timed out")
status = self._agent.check_xfer_state(handle)
if status in ("DONE", "SUCCESS"):
self._data_plane_error = None
return
if status in ("ERR", "ERROR", "FAIL"):
self._data_plane_error = f"{label} failed with status {status}"
raise RuntimeError(f"{label} failed with status {status}")
time.sleep(0.001)
def fetch_remote_and_wait(
self,
remote_agent_name: str,
ip: str,
port: int,
timeout_seconds: float = 120.0,
) -> None:
if self._agent is None:
raise RuntimeError("NIXL agent not initialized")
logger.info(
f"Fetching remote metadata from {remote_agent_name} at {ip}:{port}"
)
self._agent.fetch_remote_metadata(remote_agent_name, ip, port)
start = time.perf_counter()
while True:
if time.perf_counter() - start >= timeout_seconds:
raise TimeoutError(
f"Timed out waiting for remote metadata from "
f"{remote_agent_name} at {ip}:{port}"
)
if self._agent.check_remote_metadata(remote_agent_name):
logger.info(
f"Remote metadata loaded for {remote_agent_name} "
f"({time.perf_counter() - start:.2f}s)"
)
self._remote_agents[remote_agent_name] = (ip, port)
return
time.sleep(0.01)
def add_remote_agent(self, source_metadata: bytes) -> str:
if self._agent is None:
raise RuntimeError("NIXL agent not initialized")
remote_agent_name = self._agent.add_remote_agent(source_metadata)
logger.info(
"Loaded remote NIXL agent %s from metadata blob (%d bytes)",
remote_agent_name,
len(source_metadata),
)
self._remote_agents.setdefault(remote_agent_name, None)
return remote_agent_name
def remove_remote_agent(self, remote_agent_name: str) -> bool:
if self._agent is None:
return False
try:
self._agent.remove_remote_agent(remote_agent_name)
except Exception as exc:
logger.warning(
"Failed to remove remote NIXL agent %s: %s", remote_agent_name, exc
)
self._remote_agents.pop(remote_agent_name, None)
return False
self._remote_agents.pop(remote_agent_name, None)
logger.info("Disconnected remote NIXL agent %s", remote_agent_name)
return True
def disconnect_remote_agents(self) -> int:
if self._agent is None or not self._remote_agents:
return 0
removed = 0
for name in list(self._remote_agents):
if self.remove_remote_agent(name):
removed += 1
return removed
def receive_from_source(
self,
source_metadata: bytes,
source_tensors: list[TensorDescriptor],
timeout_seconds: float | None = None,
remote_agent_name: str | None = None,
require_exact_match: bool = False,
) -> tuple[int, int, float]:
if self._agent is None:
raise RuntimeError("NIXL agent not initialized")
start_time = time.perf_counter()
self._accelerator_backend.set_device(self._device_id)
if remote_agent_name is None:
add_start = time.perf_counter()
remote_agent_name = self.add_remote_agent(source_metadata)
add_time = time.perf_counter() - add_start
logger.info(
f"[TIMING] add_remote_agent: {add_time:.3f}s "
f"(agent={remote_agent_name}, blob={len(source_metadata)} bytes)"
)
else:
logger.info(f"Using pre-loaded remote agent {remote_agent_name}")
match_start = time.perf_counter()
remote_descs: list[tuple[int, int, int]] = []
local_descs: list[tuple[int, int, int]] = []
total_bytes = 0
for src_tensor in source_tensors:
local_tensor = self._tensors.get(src_tensor.name)
if local_tensor is None:
continue
local_size = local_tensor.numel() * local_tensor.element_size()
if local_size != src_tensor.size:
raise ManifestMismatchError(
f"Tensor '{src_tensor.name}' size mismatch: "
f"source={src_tensor.size} bytes, local={local_size} bytes"
)
local_dtype = str(local_tensor.dtype)
if local_dtype != src_tensor.dtype:
raise ManifestMismatchError(
f"Tensor '{src_tensor.name}' dtype mismatch: "
f"source={src_tensor.dtype!r}, local={local_dtype!r}"
)
remote_descs.append(
(src_tensor.addr, src_tensor.size, src_tensor.device_id)
)
local_descs.append(
(
local_tensor.data_ptr(),
local_size,
self._device_id,
)
)
total_bytes += src_tensor.size
matched_tensors = len(remote_descs)
match_time = time.perf_counter() - match_start
src_names = {s.name for s in source_tensors}
local_only = sorted(set(self._tensors) - src_names)
source_only = sorted(src_names - set(self._tensors))
if local_only or source_only:
if require_exact_match:
raise ManifestMismatchError(
"Tensor name mismatch on heterogeneous transfer: "
f"{len(local_only)} local-only "
f"(first: {local_only[:5]}), "
f"{len(source_only)} source-only "
f"(first: {source_only[:5]})"
)
logger.warning(
"Tensor name mismatch between source manifest and local "
"registration: %d local-only, %d source-only",
len(local_only),
len(source_only),
)
if not remote_descs:
if require_exact_match:
raise ManifestMismatchError(
"No matching tensors found for heterogeneous transfer"
)
logger.warning("No matching tensors found for transfer")
return 0, 0, 0.0
logger.info(
f"[TIMING] match_tensors: {match_time:.3f}s "
f"({matched_tensors} tensors, {total_bytes / 1e9:.2f} GB)"
)
prep_start = time.perf_counter()
src_prepped = self._agent.prep_xfer_dlist(
agent_name=remote_agent_name,
xfer_list=remote_descs,
mem_type=self._accelerator_backend.nixl_mem_type,
backends=self._backends,
)
dst_prepped = self._agent.prep_xfer_dlist(
agent_name="",
xfer_list=local_descs,
mem_type=self._accelerator_backend.nixl_mem_type,
backends=self._backends,
)
prep_time = time.perf_counter() - prep_start
logger.info(f"[TIMING] prep_xfer_dlist: {prep_time:.3f}s")
indices = list(range(len(remote_descs)))
handle = self._agent.make_prepped_xfer(
operation="READ",
local_xfer_side=dst_prepped,
local_indices=indices,
remote_xfer_side=src_prepped,
remote_indices=indices,
backends=self._backends,
)
self._agent.transfer(handle)
try:
self._wait_for_xfer(handle, timeout_seconds, "Transfer")
finally:
self._agent.release_xfer_handle(handle)
self._accelerator_backend.synchronize(self._device_id)
duration = time.perf_counter() - start_time
bandwidth_gbps = (total_bytes * 8) / (duration * 1e9) if duration > 0 else 0.0
logger.info(
f"Transfer complete: {matched_tensors} tensors, "
f"{total_bytes / 1e9:.2f} GB in {duration:.2f}s "
f"({bandwidth_gbps:.1f} Gbps)"
)
return total_bytes, matched_tensors, duration
def register_dram_buffer(self, buffer: torch.Tensor) -> Any:
if self._agent is None:
raise RuntimeError("NIXL agent not initialized")
if buffer.device.type != "cpu":
raise RuntimeError("NIXL DRAM buffer must be a CPU tensor")
if buffer.dtype != torch.uint8:
raise RuntimeError("NIXL DRAM buffer must use torch.uint8")
if not buffer.is_contiguous():
raise RuntimeError("NIXL DRAM buffer must be contiguous")
registered = self._agent.register_memory([buffer], backends=self._backends)
self._metadata = self._agent.get_agent_metadata()
return registered
def refresh_agent_metadata(self) -> bytes:
if self._agent is None:
raise RuntimeError("NIXL agent not initialized")
self._metadata = self._agent.get_agent_metadata()
return self._metadata
def deregister_memory(self, registered: Any) -> None:
if self._agent is None:
raise RuntimeError("NIXL agent not initialized")
if registered is not None:
self._agent.deregister_memory(registered)
self._metadata = self._agent.get_agent_metadata()
def receive_dram_into_buffer(
self,
remote_agent_name: str,
remote_addr: int,
local_buffer: torch.Tensor,
size: int,
remote_device_id: int = 0,
remote_mem_type: str = NIXL_DRAM_MEM_TYPE,
timeout_seconds: float | None = None,
) -> float:
if self._agent is None:
raise RuntimeError("NIXL agent not initialized")
if size < 0:
raise ValueError("NIXL DRAM transfer size must be non-negative")
if local_buffer.device.type != "cpu":
raise RuntimeError("NIXL DRAM destination must be a CPU tensor")
if local_buffer.dtype != torch.uint8:
raise RuntimeError("NIXL DRAM destination must use torch.uint8")
if not local_buffer.is_contiguous():
raise RuntimeError("NIXL DRAM destination must be contiguous")
if size > local_buffer.numel():
raise ValueError(
f"NIXL DRAM transfer size {size} exceeds destination buffer "
f"size {local_buffer.numel()}"
)
if size == 0:
return 0.0
start_time = time.perf_counter()
handle = None
try:
src_prepped = self._agent.prep_xfer_dlist(
agent_name=remote_agent_name,
xfer_list=[(remote_addr, size, remote_device_id)],
mem_type=remote_mem_type,
backends=self._backends,
)
dst_prepped = self._agent.prep_xfer_dlist(
agent_name="",
xfer_list=[(local_buffer.data_ptr(), size, 0)],
mem_type=NIXL_DRAM_MEM_TYPE,
backends=self._backends,
)
handle = self._agent.make_prepped_xfer(
operation="READ",
local_xfer_side=dst_prepped,
local_indices=[0],
remote_xfer_side=src_prepped,
remote_indices=[0],
backends=self._backends,
)
self._agent.transfer(handle)
self._wait_for_xfer(handle, timeout_seconds, "NIXL DRAM transfer")
duration = time.perf_counter() - start_time
logger.info(
"NIXL DRAM READ complete: %.2f MiB in %.3fs",
size / (1024 * 1024),
duration,
)
return duration
finally:
if handle is not None:
self._agent.release_xfer_handle(handle)
def is_healthy(self) -> bool:
if self._agent is None or len(self._metadata) == 0:
return False
return self._data_plane_error is None
@property
def data_plane_error(self) -> str | None:
return self._data_plane_error
def shutdown(self) -> None:
if self._atexit_registered:
atexit.unregister(self.shutdown)
self._atexit_registered = False
disconnected = self.disconnect_remote_agents()
self._agent = None
self._metadata = b""
self._tensor_descriptors = []
self._tensors = {}
self._remote_agents = {}
logger.info(
"NixlTransferManager shutdown complete (%d remote agent(s) disconnected)",
disconnected,
)