[BugFix] Enforce C locale for CPU binding subprocess parsing (#8261)

### What this PR does / why we need it?
This PR backports the CPU binding locale normalization fix from #7274 to
`releases/v0.18.0`, including the follow-up review fixes already applied
on `main`.

The change forces `LC_ALL`, `LANG`, and `LC_MESSAGES` to `C` before
spawning subprocesses in `vllm_ascend.cpu_binding.execute_command()`, so
parser-dependent command output stays stable on localized systems. It
also handles `subprocess.TimeoutExpired` by killing the child process
before collecting output, and updates the existing unit tests to keep
command-argument coverage while adding timeout-path coverage.

Fixes #6992

### Does this PR introduce _any_ user-facing change?
Yes.

Users running CPU binding on non-English OS environments should now get
consistent English subprocess output for parser-dependent commands,
avoiding failures caused by inherited locale settings.

### How was this patch tested?
- Updated the existing unit tests in
`tests/ut/device_allocator/test_cpu_binding.py` to assert the locale
environment, retain command argument coverage, and cover the timeout
cleanup path.
- Attempted to run targeted pytest cases locally, but the pytest
invocation did not complete normally in this environment, so I could not
record a clean passing run here.

Attribution:
- Co-authored-by: stdjhs <1601599324@qq.com>
- Signed-off-by: chenchuw886 <chenchuw@huawei.com>

Signed-off-by: chenchuw886 <chenchuw@huawei.com>
Co-authored-by: chenchuw886 <chenchuw@huawei.com>
Co-authored-by: stdjhs <1601599324@qq.com>
This commit is contained in:
Frank Chen
2026-04-16 16:17:10 +08:00
committed by GitHub
parent 8bc72a807a
commit 2ac8bfb4cb
2 changed files with 50 additions and 3 deletions

View File

@@ -1,12 +1,50 @@
import subprocess
import unittest import unittest
from unittest.mock import mock_open, patch from unittest.mock import MagicMock, mock_open, patch
import vllm_ascend.cpu_binding as cpu_binding_module
from vllm_ascend.cpu_binding import CpuAlloc, DeviceInfo, bind_cpus, is_arm_cpu from vllm_ascend.cpu_binding import CpuAlloc, DeviceInfo, bind_cpus, is_arm_cpu
from vllm_ascend.utils import AscendDeviceType from vllm_ascend.utils import AscendDeviceType
class TestDeviceInfo(unittest.TestCase): class TestDeviceInfo(unittest.TestCase):
@patch('vllm_ascend.cpu_binding.subprocess.Popen')
def test_execute_command(self, mock_popen):
process = MagicMock()
process.communicate.return_value = (b'command-output', b'')
process.returncode = 7
mock_popen.return_value.__enter__.return_value = process
output, return_code = cpu_binding_module.execute_command(['dummy', 'cmd'])
self.assertEqual(output, 'command-output')
self.assertEqual(return_code, 7)
mock_popen.assert_called_once()
args, kwargs = mock_popen.call_args
self.assertEqual(args[0], ['dummy', 'cmd'])
self.assertEqual(kwargs["shell"], False)
self.assertEqual(kwargs["stdout"], subprocess.PIPE)
self.assertEqual(kwargs["stderr"], subprocess.PIPE)
self.assertEqual(kwargs["env"]["LC_ALL"], "C")
self.assertEqual(kwargs["env"]["LANG"], "C")
self.assertEqual(kwargs["env"]["LC_MESSAGES"], "C")
@patch('vllm_ascend.cpu_binding.subprocess.Popen')
def test_execute_command_kills_timed_out_process(self, mock_popen):
process = MagicMock()
process.communicate.side_effect = [
subprocess.TimeoutExpired(cmd=['dummy', 'cmd'], timeout=1000),
(b'command-output', b''),
]
process.returncode = -9
mock_popen.return_value.__enter__.return_value = process
output, return_code = cpu_binding_module.execute_command(['dummy', 'cmd'])
self.assertEqual(output, 'command-output')
self.assertEqual(return_code, -9)
process.kill.assert_called_once_with()
self.assertEqual(process.communicate.call_count, 2)
@patch('vllm_ascend.cpu_binding.execute_command') @patch('vllm_ascend.cpu_binding.execute_command')
def setUp(self, mock_execute_command): def setUp(self, mock_execute_command):
mock_execute_command.side_effect = [ mock_execute_command.side_effect = [

View File

@@ -37,8 +37,17 @@ def is_arm_cpu() -> bool:
def execute_command(cmd: list[str]) -> tuple[str, int]: def execute_command(cmd: list[str]) -> tuple[str, int]:
with subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p: # Force a C locale so subprocess output remains parseable on localized OSes.
env = os.environ.copy()
env["LC_ALL"] = "C"
env["LANG"] = "C"
env["LC_MESSAGES"] = "C"
with subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) as p:
try:
out, _ = p.communicate(timeout=1000) out, _ = p.communicate(timeout=1000)
except subprocess.TimeoutExpired:
p.kill()
out, _ = p.communicate()
return out.decode(), p.returncode return out.decode(), p.returncode