A high-throughput and memory-efficient inference and serving engine for LLMs
[Bugfix][Core] Fall back when numactl --membind is blocked in constrained containers (#45438)
Signed-off-by: Ting Sun <suntcrick@gmail.com> Co-authored-by: Shengqi Chen <harry-chen@outlook.com>
Ting SUN committed
Jun 17, 2026 at 00:49 UTC
c45f681932838dda6365e1a4aa1f7092aca97bf9
2 files changed
+82
tests/utils_/test_numa_utils.py
+46
@@ -464,3 +464,49 @@ def test_parallel_config_validates_numa_bind_nodes():
464
def test_parallel_config_rejects_invalid_numa_bind_cpus(cpuset):
465
with pytest.raises(ValueError, match="numa_bind_cpus"):
466
ParallelConfig(numa_bind_cpus=[cpuset])
467
+
468
+
469
+def _fake_numactl_run(rejected_args):
470
+ """Fake ``numactl`` that fails when any of ``rejected_args`` is present."""
471
+
472
+ def run(cmd, *args, **kwargs):
473
+ arg_str = " ".join(cmd[1:-1])
474
+ ok = not any(bad in arg_str for bad in rejected_args)
475
+ return SimpleNamespace(returncode=0 if ok else 1)
476
+
477
+ return run
478
+
479
+
480
+def test_configure_subprocess_numa_fallback(monkeypatch):
481
+ import multiprocessing
482
+
483
+ monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/numactl")
484
+ monkeypatch.setattr(numa_utils.envs, "VLLM_WORKER_MULTIPROC_METHOD", "spawn")
485
+ node_config = _make_config(numa_bind=True, numa_bind_nodes=[0])
486
+
487
+ monkeypatch.setattr(numa_utils.subprocess, "run", _fake_numactl_run([]))
488
+ with numa_utils.configure_subprocess(node_config, local_rank=0):
489
+ assert os.environ[numa_utils._NUMACTL_ARGS_ENV] == "--cpunodebind=0 --membind=0"
490
+
491
+ membind_fails = _fake_numactl_run(["--membind="])
492
+ monkeypatch.setattr(numa_utils.subprocess, "run", membind_fails)
493
+ with numa_utils.configure_subprocess(node_config, local_rank=0):
494
+ assert os.environ[numa_utils._NUMACTL_ARGS_ENV] == "--cpunodebind=0"
495
+
496
+ cpu_config = _make_config(
497
+ numa_bind=True,
498
+ numa_bind_nodes=[0],
499
+ numa_bind_cpus=["0-3"],
500
+ )
501
+ with numa_utils.configure_subprocess(cpu_config, local_rank=0):
502
+ assert os.environ[numa_utils._NUMACTL_ARGS_ENV] == "--physcpubind=0-3"
503
+
504
+ before = multiprocessing.spawn.get_executable()
505
+ monkeypatch.setattr(
506
+ numa_utils.subprocess,
507
+ "run",
508
+ _fake_numactl_run(["--cpunodebind=", "--membind="]),
509
+ )
510
+ with numa_utils.configure_subprocess(node_config, local_rank=0):
511
+ assert multiprocessing.spawn.get_executable() == before
512
+ assert numa_utils._NUMACTL_ARGS_ENV not in os.environ
vllm/utils/numa_utils.py
+36
@@ -473,6 +473,37 @@ def log_current_affinity_state(label: str) -> None:
473
_log_numactl_show(label)
474
475
476
+def _probe_numactl_args(numactl_args: str) -> bool:
477
+ """Whether ``numactl <args> true`` succeeds in this (parent) environment."""
478
+ try:
479
+ result = subprocess.run(
480
+ ["numactl", *numactl_args.split(), "true"],
481
+ capture_output=True,
482
+ timeout=10,
483
+ )
484
+ except (OSError, subprocess.SubprocessError):
485
+ return False
486
+ return result.returncode == 0
487
+
488
+
489
+def _resolve_numactl_args(numactl_args: str) -> str:
490
+ """Drop ``--membind`` if the container rejects it, keeping CPU binding."""
491
+ cpu_only = " ".join(
492
+ t for t in numactl_args.split() if not t.startswith("--membind=")
493
+ )
494
+ for candidate in (numactl_args, cpu_only, ""):
495
+ if _probe_numactl_args(candidate):
496
+ if candidate != numactl_args:
497
+ logger.warning(
498
+ "numactl args %r rejected; falling back to %r. Add "
499
+ "--cap-add SYS_NICE for full NUMA binding.",
500
+ numactl_args,
501
+ candidate or "no binding",
502
+ )
503
+ return candidate
504
+ return ""
505
+
506
+
507
@contextmanager
508
def configure_subprocess(
509
vllm_config: "VllmConfig",
@@ -500,6 +531,11 @@ def configure_subprocess(
531
)
532
533
executable, debug_str = _get_numactl_executable()
534
+ numactl_args = _resolve_numactl_args(numactl_args)
535
+ if not numactl_args:
536
+ # No NUMA binding possible here; launch without the wrapper.
537
+ yield
538
+ return
539
python_executable = os.fsdecode(multiprocessing.spawn.get_executable())
540
with (
541
_set_numa_wrapper_env(numactl_args, python_executable),