Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

430 lines
16 KiB
Python

import ctypes
import glob
import logging
import math
import multiprocessing
import os
import random
import shutil
import subprocess
import time
from contextlib import contextmanager
from pathlib import Path
from typing import Optional
import torch
from sglang.srt.environ import envs
from sglang.srt.server_args import ServerArgs
from sglang.srt.utils import is_cuda
_is_cuda = is_cuda()
logger = logging.getLogger(__name__)
@contextmanager
def configure_subprocess(server_args: ServerArgs, gpu_id: int):
if envs.SGLANG_NUMA_BIND_V2.get():
numa_node = get_numa_node_if_available(server_args, gpu_id)
if numa_node is not None:
# _numactl_cpu_mem_args returns None (warn/raise) on empty CPU intersection (#26983).
numactl_args = _numactl_cpu_mem_args(numa_node, gpu_id)
if numactl_args is not None:
# Verify numactl can actually apply the binding before we exec it
# in front of the interpreter; relax the memory policy if not.
numactl_args, probe_err = _probe_numactl_args(numactl_args)
if numactl_args is None:
# numactl could not apply even a CPU-only binding (e.g.
# set_mempolicy(2)/sched_setaffinity(2) blocked by seccomp,
# which the read-only get_mempolicy(2) probe in
# _can_set_mempolicy cannot detect). Reuse #26983's failure
# semantics (warn-and-continue, or raise when
# SGLANG_CRASH_ON_NUMA_BIND_FAILURE) with an explicit reason
# carrying the captured stderr: the CPU intersection already
# succeeded here, so the default "no CPU cores allowed"
# message would mislead operators toward the wrong cause.
probe_suffix = f": {probe_err}" if probe_err else ""
_handle_numa_bind_failure(
numa_node,
reason=(
f"numactl could not apply NUMA binding for node "
f"{numa_node} (e.g. set_mempolicy/sched_setaffinity "
f"blocked by seccomp, or cpuset rejects the policy)"
f"{probe_suffix}; skipping NUMA binding for GPU {gpu_id}."
),
)
yield
return
executable, debug_str = _create_numactl_executable(
numactl_args=numactl_args
)
debug_str += (
f", logical_gpu_id={gpu_id}, "
f"physical_gpu_id={_get_nvml_device_index(gpu_id)}, "
f"CUDA_VISIBLE_DEVICES={os.environ.get('CUDA_VISIBLE_DEVICES', '')}"
)
with _mp_set_executable(executable=executable, debug_str=debug_str):
yield
return
yield
def _create_numactl_executable(numactl_args: str):
old_executable = os.fsdecode(multiprocessing.spawn.get_executable())
script = f'''#!/bin/sh
exec numactl {numactl_args} {old_executable} "$@"'''
path = Path(
f"/tmp/sglang_temp_file_{time.time()}_{random.randrange(0, 10000000)}.sh"
)
path.write_text(script)
path.chmod(0o777)
return str(path), f"{script=}"
@contextmanager
def _mp_set_executable(executable: str, debug_str: str):
start_method = multiprocessing.get_start_method()
assert start_method == "spawn", f"{start_method=}"
old_executable = os.fsdecode(multiprocessing.spawn.get_executable())
multiprocessing.spawn.set_executable(executable)
logger.debug(f"mp.set_executable {old_executable} -> {executable} ({debug_str})")
try:
yield
finally:
assert (
os.fsdecode(multiprocessing.spawn.get_executable()) == executable
), f"{multiprocessing.spawn.get_executable()=}"
multiprocessing.spawn.set_executable(old_executable)
logger.debug(f"mp.set_executable revert to {old_executable}")
def _get_nvml_device_index(device_id: int) -> int:
# _get_nvml_device_index is an internal PyTorch helper, so fall back to
# device_id directly if the helper is unavailable.
get_nvml_device_index = getattr(torch.cuda, "_get_nvml_device_index", None)
if get_nvml_device_index is None:
logger.warning(
"torch.cuda._get_nvml_device_index is unavailable; falling back to "
f"device_id={device_id} as the NVML device index. This may select "
"the wrong physical GPU when CUDA_VISIBLE_DEVICES reorders devices "
f"(CUDA_VISIBLE_DEVICES={os.environ.get('CUDA_VISIBLE_DEVICES', '')})."
)
return device_id
return get_nvml_device_index(device_id)
def get_numa_node_if_available(server_args: ServerArgs, gpu_id: int) -> Optional[int]:
"""
Returns the NUMA node for the given GPU id. If it is not set in the server_args, it will try to query the NUMA node for the GPU.
If the NUMA node is not available, has already been configured externally, or the user lacks permission to set NUMA affinity, it will return None.
Args:
server_args: The server arguments.
gpu_id: The GPU id.
Returns:
The NUMA node for the given GPU id or None if it is not available.
"""
if server_args.numa_node is not None:
return server_args.numa_node[gpu_id]
if _is_numa_available():
queried_numa_node = _query_numa_node_for_gpu(gpu_id)
if len(queried_numa_node) == 0:
return None
if len(queried_numa_node) > 1:
# get_numa_node_for_gpu could return multiple nodes, we use the first one for now.
# I don't think there any hardware configs that would have more than one.
logger.warning(
f"Multiple NUMA nodes found for GPU {gpu_id}: {queried_numa_node}. Using the first one."
)
return queried_numa_node[0]
return None
def get_libnuma():
libnuma = None
for libnuma_so in ["libnuma.so", "libnuma.so.1"]:
try:
libnuma = ctypes.CDLL(libnuma_so)
except OSError as e:
logger.debug(f"{e}")
libnuma = None
if libnuma is not None:
break
return libnuma
def numa_bind_to_node(node: int):
libnuma = get_libnuma()
if libnuma is None or libnuma.numa_available() < 0:
logger.warning("numa not available on this system, skip bind action")
return
node_cpus = _node_cpus(node)
if node_cpus:
allowed_cpus = os.sched_getaffinity(0)
target_cpus = node_cpus & allowed_cpus
if not target_cpus:
_handle_numa_bind_failure(node, allowed_cpus)
return
os.sched_setaffinity(0, target_cpus)
else:
libnuma.numa_run_on_node(ctypes.c_int(node))
libnuma.numa_set_preferred(ctypes.c_int(node))
class _Bitmask(ctypes.Structure):
_fields_ = [("size", ctypes.c_ulong), ("maskp", ctypes.POINTER(ctypes.c_ulong))]
def _node_cpus(node: int) -> set:
libnuma = get_libnuma()
if libnuma is None or libnuma.numa_available() < 0:
return set()
libnuma.numa_allocate_cpumask.restype = ctypes.POINTER(_Bitmask)
libnuma.numa_node_to_cpus.argtypes = [ctypes.c_int, ctypes.POINTER(_Bitmask)]
libnuma.numa_node_to_cpus.restype = ctypes.c_int
libnuma.numa_bitmask_isbitset.argtypes = [ctypes.POINTER(_Bitmask), ctypes.c_uint]
libnuma.numa_bitmask_isbitset.restype = ctypes.c_int
libnuma.numa_bitmask_free.argtypes = [ctypes.POINTER(_Bitmask)]
mask = libnuma.numa_allocate_cpumask()
try:
if libnuma.numa_node_to_cpus(node, mask) != 0:
return set()
return {
i
for i in range(mask.contents.size)
if libnuma.numa_bitmask_isbitset(mask, i)
}
finally:
libnuma.numa_bitmask_free(mask)
def _numactl_cpu_mem_args(node: int, gpu_id: int) -> Optional[str]:
node_cpus = _node_cpus(node)
if not node_cpus:
return f"--cpunodebind={node} --membind={node}"
allowed_cpus = os.sched_getaffinity(0)
target_cpus = node_cpus & allowed_cpus
if not target_cpus:
_handle_numa_bind_failure(node, allowed_cpus, gpu_id)
return None
if target_cpus == node_cpus:
return f"--cpunodebind={node} --membind={node}"
cpu_list = ",".join(str(c) for c in sorted(target_cpus))
return f"--physcpubind={cpu_list} --membind={node}"
def _strip_memory_args(numactl_args: str) -> str:
"""Return ``numactl_args`` with the ``--membind`` segment removed, keeping
only the CPU binding (``--cpunodebind`` / ``--physcpubind``)."""
return " ".join(
token for token in numactl_args.split() if not token.startswith("--membind")
)
def _probe_numactl_args(numactl_args: str) -> tuple[Optional[str], str]:
"""Dry-run ``numactl <args> true`` and fall back to a weaker binding when the
kernel rejects the strongest one.
``configure_subprocess`` applies NUMA binding by exec-ing ``numactl`` in front
of the Python interpreter (see ``_create_numactl_executable``), so a binding
that ``numactl`` refuses kills the worker before Python starts, with no
traceback. ``_can_set_mempolicy`` only probes ``get_mempolicy(2)`` (read),
which does not catch ``set_mempolicy(2)`` being denied (e.g. by a seccomp
profile) or a ``--membind`` that the cpuset rejects with ``EINVAL``.
To avoid that silent crash we probe the requested args and progressively relax
the *memory* policy while keeping the CPU binding intact::
--membind=N -> --preferred=N -> drop the memory segment
Returns ``(args, last_stderr)``: ``args`` is the strongest binding that
actually runs, or ``None`` if even CPU-only fails (or ``numactl`` is missing /
errors out); ``last_stderr`` is the rejection reason numactl printed for the
strongest binding that was rejected (empty on success), so the caller can
surface it on the total-failure path.
"""
def _probe(args: str):
"""Run ``numactl <args> true``; return ``(succeeded, stderr_text)``."""
try:
proc = subprocess.run(
["numactl", *args.split(), "true"],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
timeout=10,
)
stderr = proc.stderr.decode("utf-8", errors="replace").strip()
if proc.returncode != 0:
logger.debug(f"numactl probe for {args!r} rejected: {stderr!r}")
return proc.returncode == 0, stderr
except Exception as e:
# Missing numactl, timeout, etc. Treat as "this binding does not work".
logger.debug(f"numactl probe for {args!r} failed: {e}")
return False, str(e)
def _suffix(err: str) -> str:
return f": {err}" if err else ""
# 1. Strongest binding: exactly what was requested.
ok, last_err = _probe(numactl_args)
if ok:
return numactl_args, ""
# 2. Relax a hard --membind=N to a soft --preferred=N. The memory segment here
# is always a single node, which maps cleanly onto --preferred (single-node
# only). MPOL_PREFERRED is a hint and can succeed where MPOL_BIND is denied.
if "--membind=" in numactl_args:
preferred_args = numactl_args.replace("--membind=", "--preferred=")
ok, _ = _probe(preferred_args)
if ok:
logger.warning(
f"numactl rejected hard memory binding ({numactl_args!r})"
f"{_suffix(last_err)}; falling back to soft preferred policy "
f"({preferred_args!r})."
)
return preferred_args, ""
# 3. Drop the memory segment entirely, keep only the CPU binding.
cpu_only_args = _strip_memory_args(numactl_args)
if cpu_only_args and cpu_only_args != numactl_args:
ok, cpu_err = _probe(cpu_only_args)
if ok:
logger.warning(
f"numactl rejected memory binding ({numactl_args!r})"
f"{_suffix(last_err)}; falling back to CPU-only binding "
f"({cpu_only_args!r})."
)
return cpu_only_args, ""
last_err = cpu_err
# 4. Nothing worked.
return None, last_err
def _handle_numa_bind_failure(
node: int,
allowed_cpus=None,
gpu_id: Optional[int] = None,
*,
reason: Optional[str] = None,
) -> None:
"""Emit the NUMA-bind failure warning, or raise it when
``SGLANG_CRASH_ON_NUMA_BIND_FAILURE`` is set.
Two call modes:
* ``reason is None`` (default): the failure is an empty CPU intersection,
so the message reports ``allowed_cpus`` (which must be provided).
* ``reason`` provided: the failure is something else (e.g. numactl rejected
the binding at runtime); the caller supplies the exact message and
``allowed_cpus`` / ``gpu_id`` are not needed.
"""
if reason is None:
gpu_str = f" for GPU {gpu_id}" if gpu_id is not None else ""
reason = (
f"NUMA node {node} has no CPU cores allowed by the current affinity "
f"{sorted(allowed_cpus)}, skipping NUMA binding{gpu_str}."
)
logger.warning(reason)
if envs.SGLANG_CRASH_ON_NUMA_BIND_FAILURE.get():
raise RuntimeError(reason)
def _can_set_mempolicy() -> bool:
"""Check if the process has permission to use NUMA memory policy syscalls."""
try:
libnuma = get_libnuma()
if libnuma is None or libnuma.numa_available() < 0:
return False
mode = ctypes.c_int()
ret = libnuma.get_mempolicy(
ctypes.byref(mode), None, ctypes.c_ulong(0), None, ctypes.c_ulong(0)
)
return ret == 0
except Exception:
return False
def _is_numa_available() -> bool:
"""
Check if NUMA is available and not already configured externally.
"""
if not _is_cuda:
return False
# Check if this is a numa system.
if not os.path.isdir("/sys/devices/system/node/node1"):
return False
if not shutil.which("numactl") and envs.SGLANG_NUMA_BIND_V2.get():
logger.debug(
"numactl command not found, skipping NUMA node configuration for GPU. Install numactl (e.g., apt-get install numactl) to enable automatic NUMA binding."
)
return False
if not _can_set_mempolicy():
logger.warning(
"User lacks permission to set NUMA affinity, skipping NUMA node configuration for GPU. If using docker, try adding --cap-add SYS_NICE to your docker run command."
)
return False
return True
def _query_numa_node_for_gpu(device_id: int):
"""
Get the NUMA node affinity list for a GPU device.
Args:
device_id: CUDA logical device index (post-CUDA_VISIBLE_DEVICES).
Returns:
List of NUMA node IDs that have affinity with the device.
"""
try:
import pynvml
except ModuleNotFoundError:
logger.warning("pynvml not installed, skipping NUMA node configuration for GPU")
return []
try:
pynvml.nvmlInit()
# device_id is a CUDA logical index. Convert it to the corresponding
# NVML index so reordered CUDA_VISIBLE_DEVICES maps to the right GPU.
# _get_nvml_device_index takes CUDA_VISIBLE_DEVICES into account.
nvml_device_id = _get_nvml_device_index(device_id)
handle = pynvml.nvmlDeviceGetHandleByIndex(nvml_device_id)
numa_node_count = len(glob.glob("/sys/devices/system/node/node[0-9]*"))
c_ulong_bits = ctypes.sizeof(ctypes.c_ulong) * 8
node_set_size = max(1, math.ceil(numa_node_count / c_ulong_bits))
node_set = pynvml.nvmlDeviceGetMemoryAffinity(
handle,
node_set_size,
pynvml.NVML_AFFINITY_SCOPE_NODE,
)
# Decode the bitmask into a list of NUMA node IDs
numa_nodes = []
for node_id in range(numa_node_count):
mask_array_index = node_id // c_ulong_bits
mask_bit_index = node_id % c_ulong_bits
if node_set[mask_array_index] & (1 << mask_bit_index):
numa_nodes.append(node_id)
return numa_nodes
except pynvml.NVMLError as e:
logger.warning(
f"NVML error querying memory affinity for GPU {device_id}: {e}, skipping NUMA node configuration for GPU"
)
return []
finally:
try:
pynvml.nvmlShutdown()
except Exception:
pass # Ignore shutdown errors