Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions components/src/dynamo/sglang/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,49 @@ def filter_supported_async_generate_kwargs(
return {key: value for key, value in kwargs.items() if key in supported_kwarg_names}


@lru_cache(maxsize=32)
def _start_profile_accepts_request_object(start_profile: Any) -> bool:
"""Return whether TokenizerManager.start_profile expects a ProfileReq."""
try:
signature = inspect.signature(start_profile)
except (TypeError, ValueError):
logger.debug(
"Could not inspect SGLang TokenizerManager.start_profile signature; "
"using the legacy keyword-argument API"
)
return False

return "req" in signature.parameters


def _build_profile_request(body: dict[str, Any]) -> Any:
from sglang.srt.managers.io_struct import ProfileReq

return ProfileReq(**body)
Comment thread
ishandhanani marked this conversation as resolved.


async def start_profile_compat(tokenizer_manager: Any, body: dict[str, Any]) -> None:
"""Start profiling across SGLang's old and new control APIs.

SGLang 0.5.11 accepts profiling fields as keyword arguments. Newer builds
accept one ``ProfileReq`` object instead.
"""
start_profile = tokenizer_manager.start_profile
signature_source = getattr(start_profile, "__func__", start_profile)

try:
accepts_request_object = _start_profile_accepts_request_object(signature_source)
except TypeError:
accepts_request_object = _start_profile_accepts_request_object.__wrapped__(
signature_source
)

if accepts_request_object:
await start_profile(_build_profile_request(body))
else:
await start_profile(**body)


def enable_disjoint_streaming_output(server_args: Any) -> None:
"""Enable SGLang's disjoint streaming output.

Expand All @@ -122,4 +165,5 @@ def enable_disjoint_streaming_output(server_args: Any) -> None:
"enable_disjoint_streaming_output",
"ensure_sglang_top_level_exports",
"filter_supported_async_generate_kwargs",
"start_profile_compat",
]
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
)
from dynamo.llm.exceptions import EngineShutdown
from dynamo.runtime import DistributedRuntime
from dynamo.sglang._compat import start_profile_compat
from dynamo.sglang.args import Config
from dynamo.sglang.pause import SGLangEnginePauseController
from dynamo.sglang.publisher import DynamoSglangPublisher
Expand Down Expand Up @@ -878,7 +879,7 @@ async def start_profile(self, body: dict) -> dict:
Args:
body: Dict with profiling parameters passed to start_profile.
"""
await self.engine.tokenizer_manager.start_profile(**body)
await start_profile_compat(self.engine.tokenizer_manager, body)
return {"status": "ok", "message": "Profiling started"}

async def stop_profile(self, body: dict) -> dict:
Expand Down
41 changes: 41 additions & 0 deletions components/src/dynamo/sglang/tests/test_sglang_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from dynamo.sglang._compat import (
ensure_sglang_top_level_exports,
filter_supported_async_generate_kwargs,
start_profile_compat,
)
from dynamo.sglang.args import (
_normalize_multimodal_disaggregation_args,
Expand Down Expand Up @@ -235,6 +236,46 @@ def counting_signature(obj):
sglang_compat._get_async_generate_supported_kwarg_names.cache_clear()


@pytest.mark.asyncio
async def test_compat_starts_profile_with_legacy_kwargs():
class LegacyTokenizerManager:
received = None

async def start_profile(self, output_dir=None, start_step=None, num_steps=None):
self.received = {
"output_dir": output_dir,
"start_step": start_step,
"num_steps": num_steps,
}

manager = LegacyTokenizerManager()
body = {"output_dir": "/tmp/profile", "start_step": 10, "num_steps": 5}

await start_profile_compat(manager, body)

assert manager.received == body


@pytest.mark.asyncio
async def test_compat_starts_profile_with_request_object(monkeypatch):
class RequestTokenizerManager:
received = None

async def start_profile(self, req=None):
self.received = req

request = SimpleNamespace(output_dir="/tmp/profile", start_step=10, num_steps=5)
monkeypatch.setattr(sglang_compat, "_build_profile_request", lambda body: request)
manager = RequestTokenizerManager()

await start_profile_compat(
manager,
{"output_dir": "/tmp/profile", "start_step": 10, "num_steps": 5},
)

assert manager.received is request


@pytest.mark.asyncio
async def test_custom_jinja_template_invalid_path(mock_sglang_cli):
"""Test that invalid file path raises FileNotFoundError."""
Expand Down
Loading