[Perf] Fork-safe import: no CUDA context at import time, lighter argument parsing (#40201)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user