[Perf] Fork-safe import: no CUDA context at import time, lighter argument parsing (#40201)

This commit is contained in:
Xueshen Liu
2026-09-21 10:48:35 +08:00
committed by GitHub
parent 176dbcb85d
commit ab03a8e7eb
11 changed files with 381 additions and 12 deletions
@@ -34,6 +34,7 @@ from sglang.srt.function_call.lfm2_detector import Lfm2Detector
from sglang.srt.function_call.ling3_detector import Ling3Detector
from sglang.srt.function_call.llama32_detector import Llama32Detector
from sglang.srt.function_call.mistral_detector import MistralDetector
from sglang.srt.function_call.parser_names import TOOL_CALL_PARSER_NAMES
from sglang.srt.function_call.pythonic_detector import PythonicDetector
from sglang.srt.function_call.qwen3_coder_detector import Qwen3CoderDetector
from sglang.srt.function_call.utils import get_schema_properties
@@ -6013,5 +6014,15 @@ class TestTopLevelCompositeToolSchema(unittest.TestCase):
self.assertEqual(json.loads(arguments), self.expected)
class TestToolCallParserNames(unittest.TestCase):
def test_matches_registry(self):
# `server_args` builds the --tool-call-parser choices from this list to
# keep the registry, and its dependencies, out of argument parsing.
self.assertEqual(
sorted(TOOL_CALL_PARSER_NAMES),
sorted(FunctionCallParser.ToolCallParserEnum),
)
if __name__ == "__main__":
unittest.main()
@@ -20,6 +20,7 @@ from sglang.srt.parser.reasoning_parser import (
Qwen3Detector,
ReasoningParser,
)
from sglang.srt.parser.reasoning_parser_names import REASONING_PARSER_NAMES
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
@@ -1726,5 +1727,14 @@ class TestGraniteThinkingDetector(CustomTestCase):
self.assertEqual(normal, "truncated")
class TestReasoningParserNames(CustomTestCase):
def test_matches_registry(self):
# `server_args` builds the --reasoning-parser choices from this list to
# keep the registry, and its dependencies, out of argument parsing.
self.assertEqual(
sorted(REASONING_PARSER_NAMES), sorted(ReasoningParser.DetectorMap)
)
if __name__ == "__main__":
unittest.main()
@@ -4002,5 +4002,72 @@ class TestDcpCommBackendDefault(CustomTestCase):
)
class TestParserChoices(CustomTestCase):
"""The choices come from dependency-free name lists, but `cli/serve.py`
loads plugins before parsing, so a plugin's parser must still be accepted."""
def test_a_plugin_registered_parser_is_accepted(self):
from sglang.srt.function_call.function_call_parser import FunctionCallParser
from sglang.srt.parser.reasoning_parser import ReasoningParser
ReasoningParser.DetectorMap["plugin-reasoning"] = object
FunctionCallParser.ToolCallParserEnum["plugin-toolcall"] = object
try:
parser = argparse.ArgumentParser()
ServerArgs.add_cli_args(parser)
args = parser.parse_args(
[
"--model-path",
"dummy-model",
"--reasoning-parser",
"plugin-reasoning",
"--tool-call-parser",
"plugin-toolcall",
]
)
finally:
del ReasoningParser.DetectorMap["plugin-reasoning"]
del FunctionCallParser.ToolCallParserEnum["plugin-toolcall"]
self.assertEqual(args.reasoning_parser, "plugin-reasoning")
self.assertEqual(args.tool_call_parser, "plugin-toolcall")
def test_name_lists_are_used_when_the_registries_are_not_imported(self):
from sglang.srt.function_call.parser_names import TOOL_CALL_PARSER_NAMES
from sglang.srt.parser.reasoning_parser_names import REASONING_PARSER_NAMES
with patch.dict(server_args_module.sys.modules):
server_args_module.sys.modules.pop(
"sglang.srt.parser.reasoning_parser", None
)
server_args_module.sys.modules.pop(
"sglang.srt.function_call.function_call_parser", None
)
self.assertEqual(
server_args_module._reasoning_parser_choices(),
list(REASONING_PARSER_NAMES),
)
self.assertEqual(
server_args_module._tool_call_parser_choices(),
list(TOOL_CALL_PARSER_NAMES),
)
class TestLazyReexports(CustomTestCase):
def test_the_names_that_lost_their_eager_import_are_still_attributes(self):
# Out-of-tree code reaches these through `sglang.srt.server_args`; they
# now resolve through the module __getattr__ instead of a top import.
from sglang.kernels.ops.kv_canary.consts import RealKvHashMode
from sglang.srt.function_call.function_call_parser import FunctionCallParser
from sglang.srt.parser.reasoning_parser import ReasoningParser
self.assertIs(server_args_module.FunctionCallParser, FunctionCallParser)
self.assertIs(server_args_module.ReasoningParser, ReasoningParser)
self.assertIs(server_args_module.RealKvHashMode, RealKvHashMode)
def test_an_unknown_attribute_still_raises(self):
with self.assertRaises(AttributeError):
server_args_module.NotAThing
if __name__ == "__main__":
unittest.main()
+71
View File
@@ -1,9 +1,12 @@
import sys
import unittest
from array import array
from unittest import mock
import torch
from sglang.srt.utils.common import (
_get_device_sm_via_nvml,
flatten_arrays_to_int64_tensor,
get_device_sm_nvidia_smi,
get_nvidia_driver_version_str,
@@ -144,5 +147,73 @@ class TestGetDeviceSmNvidiaSmi(CustomTestCase):
subprocess.run = original
class _FakePynvml:
"""Records the NVML index it was asked for, so a test can tell which
physical GPU the helper would have reported."""
def __init__(self, capability=(9, 0)):
self.capability = capability
self.requested_index = None
self.initialized = False
def nvmlInit(self):
self.initialized = True
def nvmlShutdown(self):
pass
def nvmlDeviceGetHandleByIndex(self, index):
self.requested_index = index
return f"handle-{index}"
def nvmlDeviceGetCudaComputeCapability(self, handle):
return self.capability
class TestGetDeviceSmViaNvml(CustomTestCase):
"""The torch ordinal and the NVML index differ under CUDA_VISIBLE_DEVICES
and MIG; without that mapping the helper must return None, not GPU 0."""
def test_torch_exposes_the_mapping_api(self):
# The cases below install the private attribute themselves, so they stay
# green on a torch that dropped it while the helper silently falls back.
self.assertTrue(hasattr(torch.cuda, "_get_nvml_device_index"))
def test_maps_the_torch_ordinal_to_the_nvml_index(self):
fake = _FakePynvml(capability=(9, 0))
with (
mock.patch.dict(sys.modules, {"pynvml": fake}),
mock.patch.object(
torch.cuda, "_get_nvml_device_index", lambda index: 3, create=True
),
):
self.assertEqual(_get_device_sm_via_nvml(), 90)
self.assertEqual(fake.requested_index, 3)
def test_returns_none_when_the_mapping_api_is_absent(self):
fake = _FakePynvml()
saved = torch.cuda.__dict__.pop("_get_nvml_device_index", None)
try:
with mock.patch.dict(sys.modules, {"pynvml": fake}):
self.assertIsNone(_get_device_sm_via_nvml())
finally:
if saved is not None:
torch.cuda._get_nvml_device_index = saved
self.assertFalse(fake.initialized, "must not query NVML without the mapping")
def test_returns_none_when_the_mapping_api_raises(self):
fake = _FakePynvml()
def boom(index):
raise RuntimeError("no such device")
with (
mock.patch.dict(sys.modules, {"pynvml": fake}),
mock.patch.object(torch.cuda, "_get_nvml_device_index", boom, create=True),
):
self.assertIsNone(_get_device_sm_via_nvml())
self.assertFalse(fake.initialized, "must not query NVML without the mapping")
if __name__ == "__main__":
unittest.main()