from __future__ import annotations
import logging
import os
import time
from typing import TYPE_CHECKING, Any
import torch
from . import ucx_utils
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
try:
from nixl._api import nixl_agent as NixlAgent
from nixl._api import nixl_agent_config
NIXL_AVAILABLE = True
except ImportError:
pass
SUPPORTED_NIXL_BACKENDS = ("UCX", "LIBFABRIC")
DEFAULT_NIXL_BACKEND = "UCX"
def is_nixl_available() -> bool:
return NIXL_AVAILABLE
def _resolve_nixl_backend() -> str:
raw = os.environ.get("MX_NIXL_BACKEND", DEFAULT_NIXL_BACKEND).strip().upper()
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 os.environ.get("MX_POOL_REG", "0") == "1"
class NixlTransferManager:
def __init__(self, agent_name: str, device_id: int, listen_port: int | None = None):
self._agent_name = agent_name
self._device_id = device_id
self._listen_port = listen_port
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] = {}
@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
torch.cuda.set_device(self._device_id)
saved_ucx_tls = os.environ.get("UCX_TLS")
nixl_ucx_tls = os.environ.get("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)
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 "UCX_TLS" in os.environ:
os.environ.pop("UCX_TLS")
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():
allocations = self._find_cuda_allocations(tensor_descriptors)
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="cuda", 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)
nixl_reg_start = time.perf_counter()
self._agent.register_memory(
[(base, used, self._device_id, "")],
mem_type="cuda",
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 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)"
)
return
time.sleep(0.01)
def receive_from_source(
self,
source_metadata: bytes,
source_tensors: list[TensorDescriptor],
timeout_seconds: float | None = None,
remote_agent_name: str | None = None,
) -> tuple[int, int, float]:
if self._agent is None:
raise RuntimeError("NIXL agent not initialized")
start_time = time.perf_counter()
torch.cuda.set_device(self._device_id)
if remote_agent_name is None:
add_start = time.perf_counter()
remote_agent_name = self._agent.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
if not remote_descs:
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="cuda",
backends=self._backends,
)
dst_prepped = self._agent.prep_xfer_dlist(
agent_name="",
xfer_list=local_descs,
mem_type="cuda",
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)
start_wait = time.perf_counter()
while True:
if timeout_seconds is not None and time.perf_counter() - start_wait >= timeout_seconds:
self._agent.release_xfer_handle(handle)
raise TimeoutError("Transfer timed out")
status = self._agent.check_xfer_state(handle)
if status in ("DONE", "SUCCESS"):
self._agent.release_xfer_handle(handle)
break
if status in ("ERR", "ERROR", "FAIL"):
self._agent.release_xfer_handle(handle)
raise RuntimeError(f"Transfer failed with status {status}")
time.sleep(0.001)
torch.cuda.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 is_healthy(self) -> bool:
return self._agent is not None and len(self._metadata) > 0
def shutdown(self) -> None:
self._agent = None
self._metadata = b""
self._tensor_descriptors = []
self._tensors = {}
logger.info("NixlTransferManager shutdown complete")