Skip to content
Closed
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
25 changes: 22 additions & 3 deletions python/sglang/srt/managers/tokenizer_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1439,10 +1439,29 @@ def _validate_joint_schema_request(
def _validate_mm_limits(
self, obj: Union[GenerateReqInput, EmbeddingReqInput]
) -> None:
if not get_mm().limit_mm_data_per_request:
return
# Build the effective per-modality limits dict.
# If the user explicitly set --limit-mm-data-per-request, use it as-is
# (an empty dict means "no limits"). Otherwise apply sensible defaults
# to prevent OOM from unbounded multi-image requests (each 4K image
# costs roughly ~1 GiB GPU memory with the fast image processor).
from sglang.srt.multimodal.processors.base_processor import (
BaseMultimodalProcessor,
)

DEFAULT_IMAGE_NUM_LIMITATION = (
BaseMultimodalProcessor.DEFAULT_IMAGE_NUM_LIMITATION
)

user_limits = get_mm().limit_mm_data_per_request
if user_limits is None:
limits = {"image": DEFAULT_IMAGE_NUM_LIMITATION}
else:
limits = user_limits
if not limits:
# Empty dict: user explicitly disabled all limits
return

for modality, limit in get_mm().limit_mm_data_per_request.items():
for modality, limit in limits.items():
data = getattr(obj, f"{modality}_data", None)
if data:
count = len(data) if isinstance(data, list) else 1
Expand Down
33 changes: 33 additions & 0 deletions python/sglang/srt/multimodal/processors/base_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ class BaseMultimodalProcessor(ABC):
# `process_and_combine_mm_data` -- resolves that clone instead of
# `self._processor`, so isolation does not depend on the subclass.
supports_mm_processor_concurrency = True
# Default max number of images per request. Subclasses may override.
# Each 4K image costs roughly ~1 GiB GPU memory with the fast image
# processor, so unbounded multi-image requests can easily OOM.
DEFAULT_IMAGE_NUM_LIMITATION = 5

def __init__(
self, hf_config, server_args, _processor, transport_mode, *args, **kwargs
Expand Down Expand Up @@ -1201,12 +1205,41 @@ def validate_mm_data(
item whose format is one of the two above).

Empty or None lists are considered valid.

Additionally enforces ``DEFAULT_IMAGE_NUM_LIMITATION`` to prevent OOM
from unbounded multi-image requests.
"""

BaseMultimodalProcessor._validate_one_modality(Modality.IMAGE, image_data)
BaseMultimodalProcessor._validate_one_modality(Modality.VIDEO, video_data)
BaseMultimodalProcessor._validate_one_modality(Modality.AUDIO, audio_data)

# Enforce default image count limit as a defensive safeguard.
# Respect the user-provided --limit-mm-data-per-request override if set.
image_limit = BaseMultimodalProcessor._resolve_effective_image_limit()
if image_data and len(image_data) > image_limit:
raise ValueError(
f"Image count {len(image_data)} exceeds the limit of "
f"{image_limit} per request. "
f"Use --limit-mm-data-per-request '{{}}' to disable this limit, "
f"or specify an image entry to override it "
f"(e.g. '{{\"image\": 10}}')."
)

@staticmethod
def _resolve_effective_image_limit() -> int:
"""Return the effective image count limit for the current server config.

Uses the user-provided ``--limit-mm-data-per-request`` image entry when
set; otherwise falls back to ``DEFAULT_IMAGE_NUM_LIMITATION``. A user
value of ``0`` disables the check.
"""
user_limits = get_mm().limit_mm_data_per_request
if user_limits is not None and isinstance(user_limits, dict):
if "image" in user_limits:
return user_limits["image"]
return BaseMultimodalProcessor.DEFAULT_IMAGE_NUM_LIMITATION

def _process_loaded_mm_data(self, modality, raw_data, result):
images, videos, audios = [], [], []

Expand Down
19 changes: 18 additions & 1 deletion python/sglang/srt/rust_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@
logger = logging.getLogger(__name__)


def _resolve_mm_limit_for_rust() -> dict:
"""Return the per-modality mm limits dict for the Rust ingress.

Mirrors ``BaseMultimodalProcessor.DEFAULT_IMAGE_NUM_LIMITATION`` as a
launch-time default so Rust-side requests are subject to the same OOM
protection as Python-side requests.
"""
from sglang.srt.multimodal.processors.base_processor import (
BaseMultimodalProcessor,
)

user_limits = get_mm().limit_mm_data_per_request
if user_limits is not None:
return user_limits if isinstance(user_limits, dict) else {}
return {"image": BaseMultimodalProcessor.DEFAULT_IMAGE_NUM_LIMITATION}


def _build_server_args(
scheduler: Scheduler, *, extension: Optional[ModuleType] = None
) -> ServerArgs:
Expand Down Expand Up @@ -95,7 +112,7 @@ def _build_server_args(
if get_serving().preferred_sampling_params is not None
else None
),
limit_mm_data_per_request=get_mm().limit_mm_data_per_request or {},
limit_mm_data_per_request=_resolve_mm_limit_for_rust(),
allow_auto_truncate=get_serving().allow_auto_truncate,
enable_return_hidden_states=sa.enable_return_hidden_states,
# Not a `server_args` field: `TokenizerManager` derives it, and the
Expand Down
Loading