Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
9 changes: 5 additions & 4 deletions components/src/dynamo/sglang/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,11 @@ Worker dispatch (main.py:60-132):
have `max_running_requests`, `dllm_algorithm_config`, or other LLM-specific fields.
Use `getattr()` when accessing fields that may not exist on the stub.

SGLang 0.5.17 makes a resolved `ServerArgs` unconditionally read-only. Apply Dynamo's
post-resolution startup overrides through `_compat.override_server_args()`; control-plane
updates after engine creation should use the tokenizer manager's update API instead of
assigning fields on `server_args`.
The supported SGLang 0.5.18/0.5.19 releases keep raw input on `ServerArgs` and
publish the resolved configuration separately. Apply Dynamo's post-resolution startup
overrides through `_compat.override_server_args()`; control-plane updates after engine
creation should use the tokenizer manager's update API instead of assigning fields on
`server_args`.

**DynamoConfig** combines `DynamoRuntimeConfig` (common flags like `--namespace`,
`--output-modalities`, `--media-output-fs-url`) with `DynamoSGLangConfig` (sglang-specific
Expand Down
171 changes: 142 additions & 29 deletions components/src/dynamo/sglang/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,148 @@
>= 0.5.11. Pass through; do not re-encode.
"""

import importlib
import inspect
import logging
import uuid
from collections.abc import Mapping
from functools import lru_cache, wraps
from types import ModuleType
from typing import Any

try:
from sglang.srt.utils.server_args_config_parser import ConfigArgumentMerger
Comment thread
jain-ria marked this conversation as resolved.
Resolved
except ImportError:
# Fallback for SGLang 0.5.18. Remove when minimum supported SGLang is 0.5.19+.
from sglang.srt.server_args_config_parser import ConfigArgumentMerger

try:
from sglang.srt.arg_groups.model_override_base import (
model_config_of as _sglang_model_config_of,
)
except ImportError:
# Fallback for SGLang 0.5.18. Remove when minimum supported SGLang is 0.5.19+.
_sglang_model_config_of = None

try:
from sglang.srt.runtime_context import publish as _sglang_publish
except ImportError:
# Fallback for SGLang 0.5.18 and the XPU 0.5.11 pin. Remove the 0.5.18
# portion when minimum supported SGLang is 0.5.19+.
_sglang_publish = None


def model_config_of(server_args: Any) -> Any:
"""Return the cached SGLang model config across 0.5.18 and 0.5.19."""
legacy_get_model_config = getattr(server_args, "get_model_config", None)
if callable(legacy_get_model_config):
return legacy_get_model_config()
if _sglang_model_config_of is None:
raise AttributeError("SGLang does not expose a model-config accessor")
return _sglang_model_config_of(server_args)


def publish_server_args(server_args: Any, *, role: str) -> None:
"""Publish process-wide SGLang configuration when the API is available."""
if _sglang_publish is not None:
_sglang_publish(server_args, role=role)


try:
from sglang.srt.arg_groups.overrides import declare_late_resolution
except ImportError:
# SGLang 0.5.17 and the XPU 0.5.11 pin predate declarations.
# The separately pinned XPU SGLang 0.5.11 predates declarations. Remove
# when the XPU SGLang pin is upgraded to 0.5.18+.
declare_late_resolution = None

try:
from sglang.srt.arg_groups.overrides import resolved_view as sglang_resolved_view
from sglang.srt.arg_groups.model_override_base import (
resolved_view as sglang_resolved_view,
)
except ImportError:
# SGLang #36255 exposes ServerArgs._resolved() instead.
sglang_resolved_view = None
# Fallback for SGLang 0.5.18. Remove when minimum supported SGLang is 0.5.19+.
try:
from sglang.srt.arg_groups.overrides import (
resolved_view as sglang_resolved_view,
)
except ImportError:
# The separately pinned XPU SGLang 0.5.11 stores effective values on
# ServerArgs directly. Remove when that pin is upgraded.
sglang_resolved_view = None

logger = logging.getLogger(__name__)


def get_mm_encoder_class() -> type[Any]:
"""Load MMEncoder from the supported SGLang package layout.

Keep this import deferred because the encoder module imports compiled CUDA
operators and this compatibility module is also collected on CPU-only CI
hosts.
"""
try:
from sglang.srt.disaggregation.encoder.server import MMEncoder
except ImportError:
# Fallback for SGLang 0.5.18. Remove when minimum supported SGLang is
# 0.5.19+.
from sglang.srt.disaggregation.encode_server import MMEncoder

return MMEncoder


def get_encoder_preprocessor_modules() -> tuple[ModuleType, ...]:
"""Return importable encoder modules that bind video preprocessing APIs."""
modules: list[ModuleType] = []
for module_path in (
"sglang.srt.disaggregation.encoder.preprocessor",
# Fallback for SGLang 0.5.18. Remove when minimum supported SGLang is
# 0.5.19+.
"sglang.srt.disaggregation.encode_server",
):
try:
modules.append(importlib.import_module(module_path))
except (ImportError, OSError):
continue
return tuple(modules)


async def mm_encode(
encoder: Any, media_inputs: list[Any], modality: Any
) -> tuple[Any, Any, dict[str, Any]]:
"""Encode media across the supported SGLang MMEncoder APIs."""
legacy_encode = getattr(encoder, "_encode", None)
if callable(legacy_encode):
# Fallback for SGLang 0.5.18. Remove when minimum supported SGLang is
# 0.5.19+.
return await legacy_encode(media_inputs, modality)

prepare = getattr(encoder, "_prepare_encode_context", None)
compute = getattr(encoder, "_compute_embedding", None)
if not callable(prepare) or not callable(compute):
raise RuntimeError("SGLang MMEncoder does not expose an encode API")

request = {
"req_id": f"dynamo-direct-{uuid.uuid4()}",
"num_parts": 1,
"part_idx": 0,
"mm_items": media_inputs,
"hashes": None,
}
encode_context = await prepare(
[request],
modality,
use_global_cache=False,
)
embeddings = await compute(encode_context, keep_on_gpu=False)
if embeddings is None:
raise RuntimeError("SGLang MMEncoder returned no embeddings")
return (
encode_context.preprocess_result.grid_thw,
embeddings,
encode_context.aux_data,
)


@lru_cache(maxsize=1)
def _warn_require_reasoning_unsupported() -> None:
logger.warning(
Expand All @@ -53,9 +174,10 @@ def _warn_require_reasoning_unsupported() -> None:
def ensure_sglang_tensor_image_size() -> None:
"""Allow SGLang's image-token resolver to handle decoded image tensors.

SGLang 0.5.13 through 0.5.18 assume every decoded image exposes the PIL
``height``/``width`` attributes. Its CUDA JPEG decoder instead returns a
CHW tensor, causing multimodal requests to fall back to retokenization.
SGLang 0.5.13 through the 0.5.19 release branch assume every decoded image
exposes the PIL ``height``/``width`` attributes. Its CUDA JPEG decoder
instead returns a CHW tensor, causing multimodal requests to fall back to
retokenization.

Remove this compatibility override once the minimum supported SGLang
release handles tensor image dimensions itself.
Expand Down Expand Up @@ -98,26 +220,14 @@ def override_server_args(server_args: Any, source: str, **fields: Any) -> None:

SGLang 0.5.18+ resolves its effective configuration separately from raw
``ServerArgs`` input. Declare pre-engine changes through its resolution API
so the engine's resolved projection observes them. SGLang 0.5.17 exposes
``ServerArgs.override`` instead. The separately pinned XPU image still uses
SGLang 0.5.11, which predates both APIs; preserve its legacy assignment
behavior until its engine pin is upgraded.
so the engine's resolved projection observes them. The separately pinned
XPU image still uses SGLang 0.5.11, which predates that API; preserve its
legacy assignment behavior until its engine pin is upgraded.
"""
if declare_late_resolution is not None:
declare_late_resolution(server_args, source, **fields)
return

late_resolution = getattr(server_args, "_late_resolution", None)
if callable(late_resolution):
late_resolution(source, **fields)
return

# Fallback for SGLang 0.5.17. Remove when minimum supported SGLang is 0.5.18+.
override = getattr(server_args, "override", None)
if callable(override):
override(source, **fields)
return

# XPU compatibility for SGLang 0.5.11. Remove when the XPU SGLang pin is
# upgraded to 0.5.16+.
for name, value in fields.items():
Expand All @@ -127,14 +237,11 @@ def override_server_args(server_args: Any, source: str, **fields: Any) -> None:
def resolved_server_args(server_args: Any) -> Any:
"""Return SGLang's effective configuration for one initialized engine.

SGLang #36255 exposes ``ServerArgs._resolved()``. Current SGLang keeps
``ServerArgs`` raw and exposes the same projection through
``resolved_view()``. Older supported releases and Dynamo's non-LLM argument
stubs retain effective values on the object itself.
SGLang 0.5.18 and 0.5.19 keep ``ServerArgs`` raw and expose the effective
projection through ``resolved_view()``. The separately pinned XPU release
and Dynamo's non-LLM argument stubs retain effective values on the object
itself.
"""
resolve = getattr(server_args, "_resolved", None)
if callable(resolve):
return resolve()
if sglang_resolved_view is not None:
return sglang_resolved_view(server_args)
return server_args
Expand Down Expand Up @@ -210,8 +317,14 @@ def require_reasoning_kwargs(engine: Any, request: Mapping[str, Any]) -> dict[st


__all__ = [
"ConfigArgumentMerger",
"ensure_sglang_tensor_image_size",
"filter_supported_async_generate_kwargs",
"get_encoder_preprocessor_modules",
"get_mm_encoder_class",
"mm_encode",
"model_config_of",
"override_server_args",
"publish_server_args",
"require_reasoning_kwargs",
]
10 changes: 7 additions & 3 deletions components/src/dynamo/sglang/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

import yaml
from sglang.srt.server_args import ServerArgs
from sglang.srt.server_args_config_parser import ConfigArgumentMerger

from dynamo.common.config_dump import register_encoder
from dynamo.common.configuration.groups import DynamoRuntimeConfig
Expand All @@ -34,7 +33,12 @@
)
from dynamo.common.utils.runtime import parse_endpoint
from dynamo.runtime.logging import configure_dynamo_logging
from dynamo.sglang._compat import ensure_sglang_tensor_image_size, resolved_server_args
from dynamo.sglang._compat import (
ConfigArgumentMerger,
ensure_sglang_tensor_image_size,
model_config_of,
resolved_server_args,
)
from dynamo.sglang.backend_args import DynamoSGLangArgGroup, DynamoSGLangConfig

configure_dynamo_logging()
Expand Down Expand Up @@ -621,7 +625,7 @@ async def parse_args(args: list[str]) -> Config:
# Dynamo expects disjoint output_ids; ServerArgs is read-only after resolution.
parsed_args.incremental_streaming_output = True
server_args = ServerArgs.from_cli_args(parsed_args)
if server_args.get_model_config().is_multimodal:
if model_config_of(server_args).is_multimodal:
ensure_sglang_tensor_image_size()

if getattr(server_args, "schedule_low_priority_values_first", False):
Expand Down
2 changes: 2 additions & 0 deletions components/src/dynamo/sglang/init_multimodal.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
WorkerType,
)
from dynamo.runtime import DistributedRuntime
from dynamo.sglang._compat import publish_server_args
from dynamo.sglang.args import Config
from dynamo.sglang.health_check import (
SglangDisaggHealthCheckPayload,
Expand Down Expand Up @@ -60,6 +61,7 @@ async def init_multimodal_encode_worker(
cache_publisher = MultimodalEmbeddingCachePublisher()
await cache_publisher.create_endpoint(generate_endpoint)

publish_server_args(server_args, role="encoder")
handler = MultimodalEncodeWorkerHandler(
config,
pd_worker_client,
Expand Down
Loading
Loading