feat(hicache): support numa detect to reduce long tail latency (#11028)

Co-authored-by: Zhiqiang Xie <xiezhq@stanford.edu>
This commit is contained in:
JinYan Su
2026-01-16 06:11:49 +08:00
committed by GitHub
parent 3d72944fb8
commit 72e2f70ef7
4 changed files with 163 additions and 22 deletions

View File

@@ -24,6 +24,7 @@ from sglang.srt.mem_cache.radix_cache import (
split_node_hash_value,
)
from sglang.srt.metrics.collector import StorageMetricsCollector
from sglang.srt.utils import bind_to_closest_numa_node, is_numa_available
if TYPE_CHECKING:
from sglang.srt.mem_cache.cache_init_params import CacheInitParams
@@ -43,8 +44,16 @@ class HiRadixCache(RadixCache):
"Page first layout is not supported with direct IO backend, switching to page first direct layout"
)
if (
not server_args.disable_hicache_numa_detect
and is_numa_available()
and torch.cuda.is_available()
):
bind_to_closest_numa_node()
self.page_size = params.page_size
self.kv_cache = params.token_to_kv_pool_allocator.get_kvcache()
if isinstance(self.kv_cache, MHATokenToKVPool):
self.token_to_kv_pool_host = MHATokenToKVPoolHost(
self.kv_cache,

View File

@@ -487,6 +487,7 @@ class ServerArgs:
hicache_write_policy: str = "write_through"
hicache_io_backend: str = "kernel"
hicache_mem_layout: str = "layer_first"
disable_hicache_numa_detect: bool = False
hicache_storage_backend: Optional[str] = None
hicache_storage_prefetch_policy: str = "best_effort"
hicache_storage_backend_extra_config: Optional[str] = None
@@ -3891,6 +3892,11 @@ class ServerArgs:
default=ServerArgs.hicache_mem_layout,
help="The layout of host memory pool for hierarchical cache.",
)
parser.add_argument(
"--disable-hicache-numa-detect",
action="store_true",
help="Disable binding the process to the NUMA node closest to the active CUDA device when hierarchical cache is enabled.",
)
parser.add_argument(
"--hicache-storage-backend",
type=str,

View File

@@ -3464,19 +3464,27 @@ def check_cuda_result(raw_output):
return results
def get_physical_device_id() -> int:
def get_physical_device_id(pytorch_device_id: int) -> int:
"""
Convert PyTorch logical device ID to physical device ID.
When CUDA_VISIBLE_DEVICES is set, maps the logical device ID (as seen by PyTorch)
to the actual physical device ID. If CUDA_VISIBLE_DEVICES is not set, returns
the device ID unchanged.
Args:
pytorch_device_id: The logical device ID from PyTorch (e.g., torch.cuda.current_device())
Returns:
The physical device ID
"""
device_idx = int(pytorch_device_id)
cuda_visible_devices = os.environ.get("CUDA_VISIBLE_DEVICES", None)
assert (
cuda_visible_devices is not None
), "CUDA_VISIBLE_DEVICES should be set in a scheduler"
device_list = cuda_visible_devices.split(",")
assert (
len(device_list) == 1
), "CUDA_VISIBLE_DEVICES should be set to a single device in a scheduler"
return int(device_list[0])
if cuda_visible_devices:
device_list = cuda_visible_devices.split(",")
return int(device_list[device_idx])
else:
return device_idx
def get_device_sm_nvidia_smi():
@@ -3508,7 +3516,7 @@ def numa_bind_to_node(node: int):
raise SystemError("numa not available on this system")
libnuma.numa_run_on_node(ctypes.c_int(node))
libnuma.numa_set_localalloc()
libnuma.numa_set_preferred(ctypes.c_int(node))
def json_list_type(value):
@@ -3808,3 +3816,107 @@ def get_or_create_event_loop():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop
def get_numa_node_count() -> int:
"""
Get the number of NUMA nodes available on the system.
Must be called after is_numa_available() is True.
Returns:
int: The number of NUMA nodes.
"""
libnuma = ctypes.CDLL("libnuma.so")
return libnuma.numa_max_node() + 1
def is_numa_available() -> bool:
try:
libnuma = ctypes.CDLL("libnuma.so")
return libnuma.numa_available() >= 0
except Exception:
return False
def get_system_gpu_count() -> int:
"""
Get the total number of GPUs in the system (not affected by CUDA_VISIBLE_DEVICES).
Returns:
int: The total number of physical GPUs.
"""
result = subprocess.run(
["nvidia-smi", "--list-gpus"],
capture_output=True,
text=True,
check=True,
)
gpu_lines = [
line
for line in result.stdout.strip().split("\n")
if line.strip().startswith("GPU")
]
return len(gpu_lines)
@lru_cache(maxsize=1)
def get_current_device_numa_node() -> int:
"""
Retrieve the NUMA node ID of the CPU socket closest to the currently active CUDA device.
First tries to query nvidia-smi topology. If it returns a single NUMA ID, uses that directly.
If it returns multiple NUMA IDs (comma/dash separated), falls back to distributing GPUs
evenly across NUMA nodes based on GPU ID intervals.
For example, with 8 GPUs and 2 NUMA nodes: GPUs 0-3 -> node 0, GPUs 4-7 -> node 1.
Returns:
int: The NUMA node ID (e.g., 0, 1).
Raises:
RuntimeError: If device information cannot be retrieved.
"""
import torch
logical_device_id = torch.cuda.current_device()
physical_device_id = get_physical_device_id(logical_device_id)
# Query NUMA topology from nvidia-smi
result = subprocess.run(
["nvidia-smi", "topo", "-C", "-i", str(physical_device_id)],
capture_output=True,
text=True,
check=True,
)
output_line = result.stdout.strip()
prefix = "NUMA IDs of closest CPU:"
if output_line.startswith(prefix):
numa_id_str = output_line[len(prefix) :].strip()
if numa_id_str.isdigit():
return int(numa_id_str)
# Fall back: distribute GPUs evenly across NUMA nodes
numa_count = get_numa_node_count()
gpu_count = get_system_gpu_count()
if gpu_count >= numa_count:
gpus_per_numa = gpu_count // numa_count # >= 1
numa_node = physical_device_id // gpus_per_numa # 0 ~ numa_count - 1
else:
logger.warning(
f"GPU count {gpu_count} is less than NUMA count {numa_count}. Using first NUMA node."
)
numa_node = 0
return numa_node
def bind_to_closest_numa_node():
"""
Bind the current process to the NUMA node closest to the active CUDA device.
Uses `numa` library calls via ctypes to set the CPU affinity of the process.
"""
node_id = get_current_device_numa_node()
numa_bind_to_node(node_id)