chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Utilities for auto-deriving argparse CLI arguments from dataclass fields.
|
||||
|
||||
Usage::
|
||||
|
||||
from sglang.srt.arg_groups.arg_utils import A, Arg, add_cli_args_from_dataclass
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ServerArgs:
|
||||
# Simple fields — bare string is the help text:
|
||||
host: A[str, "The host of the HTTP server."] = "127.0.0.1"
|
||||
port: A[int, "The port of the HTTP server."] = 30000
|
||||
trust_remote_code: A[bool, "Whether to allow custom models."] = False
|
||||
tokenizer_path: A[Optional[str], "The path of the tokenizer."] = None
|
||||
|
||||
# Fields with extra metadata — use Arg(...):
|
||||
model_path: A[str, Arg(help="Path to model weights.", aliases=["--model"])]
|
||||
load_format: A[str, Arg(help="Format.", choices=CHOICES)] = "auto"
|
||||
|
||||
@staticmethod
|
||||
def add_cli_args(parser):
|
||||
add_cli_args_from_dataclass(parser, ServerArgs)
|
||||
|
||||
``A`` is a short alias for ``typing.Annotated``. A bare ``str`` inside the
|
||||
annotation is equivalent to ``Arg(help=that_string)``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
import types
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
Callable,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
A = Annotated
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Arg:
|
||||
"""CLI argument metadata attached to a dataclass field via ``Annotated``."""
|
||||
|
||||
help: str = ""
|
||||
choices: Optional[list] = None
|
||||
aliases: Optional[List[str]] = None
|
||||
cli_name: Optional[str] = None
|
||||
type_parser: Optional[Callable] = None
|
||||
nargs: Optional[str] = None
|
||||
required: Optional[bool] = None
|
||||
action: Optional[Any] = None
|
||||
action_kwargs: Optional[dict] = None
|
||||
const: Optional[Any] = None
|
||||
# When True, this field is skipped by add_cli_args_from_dataclass.
|
||||
# Use for fields that have no CLI surface (e.g. injected via Python only).
|
||||
no_cli: bool = False
|
||||
# When True, this field may be written by config resolution (model
|
||||
# overrides and post-process passes): it is part of the whitelist accepted
|
||||
# by the declaration stash, and its resolved value materializes onto the
|
||||
# field at the end of __post_init__.
|
||||
resolvable: bool = False
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def resolvable_fields(cls) -> frozenset:
|
||||
"""Names of ``cls`` dataclass fields whose ``Arg`` metadata declares
|
||||
``resolvable=True`` — the whitelist for config resolution.
|
||||
|
||||
Non-dataclass types (e.g. mock config objects in tests) have no Arg
|
||||
metadata and yield an empty whitelist."""
|
||||
if not dataclasses.is_dataclass(cls):
|
||||
return frozenset()
|
||||
hints = get_type_hints(cls, include_extras=True)
|
||||
names = set()
|
||||
for field in dataclasses.fields(cls):
|
||||
_, arg = _unwrap_annotated(hints.get(field.name, field.type))
|
||||
if arg is not None and arg.resolvable:
|
||||
names.add(field.name)
|
||||
return frozenset(names)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_MISSING = dataclasses.MISSING
|
||||
|
||||
|
||||
def _unwrap_annotated(tp):
|
||||
"""Return (inner_type, Arg | None) from ``Annotated[T, Arg(...)]``.
|
||||
|
||||
Also accepts a bare string as shorthand: ``Annotated[T, "help text"]``
|
||||
is equivalent to ``Annotated[T, Arg(help="help text")]``.
|
||||
"""
|
||||
origin = get_origin(tp)
|
||||
if origin is Annotated:
|
||||
args = get_args(tp)
|
||||
inner = args[0]
|
||||
for a in args[1:]:
|
||||
if isinstance(a, Arg):
|
||||
return inner, a
|
||||
if isinstance(a, str):
|
||||
return inner, Arg(help=a)
|
||||
return inner, None
|
||||
return tp, None
|
||||
|
||||
|
||||
def _unwrap_optional(tp):
|
||||
"""If tp is Optional[X] (i.e. Union[X, None]), return (X, True). Else (tp, False)."""
|
||||
origin = get_origin(tp)
|
||||
is_union = origin is Union or (
|
||||
hasattr(types, "UnionType") and origin is types.UnionType
|
||||
)
|
||||
if is_union:
|
||||
args = get_args(tp)
|
||||
non_none = [a for a in args if a is not type(None)]
|
||||
if len(non_none) == 1:
|
||||
return non_none[0], True
|
||||
return tp, False
|
||||
|
||||
|
||||
def _unwrap_literal(tp):
|
||||
"""If tp is Literal[...], return list of values. Else None."""
|
||||
origin = get_origin(tp)
|
||||
if origin is Literal:
|
||||
return list(get_args(tp))
|
||||
return None
|
||||
|
||||
|
||||
def _infer_type_func(tp):
|
||||
"""Map a Python type annotation to an argparse ``type=`` callable."""
|
||||
if tp is str:
|
||||
return str
|
||||
if tp is int:
|
||||
return int
|
||||
if tp is float:
|
||||
return float
|
||||
return str
|
||||
|
||||
|
||||
def _field_default(field):
|
||||
"""Return the default value for a dataclass field, or _MISSING."""
|
||||
if field.default is not _MISSING:
|
||||
return field.default
|
||||
if field.default_factory is not _MISSING:
|
||||
return field.default_factory()
|
||||
return _MISSING
|
||||
|
||||
|
||||
def _field_to_cli_name(name: str) -> str:
|
||||
"""Convert a field name like ``model_path`` to ``--model-path``."""
|
||||
return "--" + name.replace("_", "-")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def add_cli_args_from_dataclass(parser, cls, *, fields: Optional[List[str]] = None):
|
||||
"""Add argparse arguments for every ``A[T, "help"]`` or ``A[T, Arg(...)]`` field.
|
||||
|
||||
Fields without an ``Arg`` or bare-string annotation are silently skipped —
|
||||
they must still be registered manually (this allows incremental migration).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parser : argparse.ArgumentParser
|
||||
cls : dataclass type
|
||||
fields : optional list of field names to include. If None, all fields with
|
||||
``Arg`` annotations are included.
|
||||
"""
|
||||
hints = get_type_hints(cls, include_extras=True)
|
||||
|
||||
for field in dataclasses.fields(cls):
|
||||
if fields is not None and field.name not in fields:
|
||||
continue
|
||||
|
||||
hint = hints.get(field.name)
|
||||
if hint is None:
|
||||
continue
|
||||
|
||||
raw_type, arg_meta = _unwrap_annotated(hint)
|
||||
if arg_meta is None:
|
||||
continue
|
||||
if arg_meta.no_cli:
|
||||
continue
|
||||
|
||||
cli_name = arg_meta.cli_name or _field_to_cli_name(field.name)
|
||||
names = [cli_name] + (arg_meta.aliases or [])
|
||||
default = _field_default(field)
|
||||
# Anchor dest to the field name so argparse stores the value
|
||||
# under the dataclass attribute directly, even when cli_name
|
||||
# differs (e.g. --tensor-parallel-size → tp_size).
|
||||
auto_dest = cli_name.lstrip("-").replace("-", "_")
|
||||
dest_kwarg = {"dest": field.name} if field.name != auto_dest else {}
|
||||
|
||||
# Handle custom action
|
||||
if arg_meta.action is not None:
|
||||
kwargs = {
|
||||
"action": arg_meta.action,
|
||||
"help": arg_meta.help,
|
||||
**dest_kwarg,
|
||||
}
|
||||
if default is not _MISSING:
|
||||
kwargs["default"] = default
|
||||
if arg_meta.action_kwargs:
|
||||
kwargs.update(arg_meta.action_kwargs)
|
||||
parser.add_argument(*names, **kwargs)
|
||||
continue
|
||||
|
||||
# Unwrap Optional
|
||||
inner_type, is_optional = _unwrap_optional(raw_type)
|
||||
|
||||
# Check for Literal — auto-derive choices
|
||||
literal_vals = _unwrap_literal(inner_type)
|
||||
if literal_vals is not None:
|
||||
choices = arg_meta.choices or literal_vals
|
||||
# Infer type from first literal value
|
||||
val_type = type(literal_vals[0]) if literal_vals else str
|
||||
type_func = arg_meta.type_parser or _infer_type_func(val_type)
|
||||
kwargs = dict(
|
||||
type=type_func, choices=choices, help=arg_meta.help, **dest_kwarg
|
||||
)
|
||||
if default is not _MISSING:
|
||||
kwargs["default"] = default
|
||||
if arg_meta.const is not None:
|
||||
kwargs["const"] = arg_meta.const
|
||||
parser.add_argument(*names, **kwargs)
|
||||
continue
|
||||
|
||||
# Check for List[X] — but skip if type_parser is set (the parser
|
||||
# handles the whole value as a single string, e.g. json_list_type).
|
||||
origin = get_origin(inner_type)
|
||||
if (origin is list or origin is List) and arg_meta.type_parser is None:
|
||||
elem_args = get_args(inner_type)
|
||||
elem_type = elem_args[0] if elem_args else str
|
||||
type_func = _infer_type_func(elem_type)
|
||||
nargs = arg_meta.nargs or "+"
|
||||
kwargs = dict(
|
||||
type=type_func,
|
||||
nargs=nargs,
|
||||
help=arg_meta.help,
|
||||
**dest_kwarg,
|
||||
)
|
||||
if arg_meta.choices:
|
||||
kwargs["choices"] = arg_meta.choices
|
||||
if default is not _MISSING:
|
||||
kwargs["default"] = default
|
||||
if arg_meta.const is not None:
|
||||
kwargs["const"] = arg_meta.const
|
||||
parser.add_argument(*names, **kwargs)
|
||||
continue
|
||||
|
||||
# Bool → store_true
|
||||
if inner_type is bool:
|
||||
kwargs = dict(action="store_true", help=arg_meta.help, **dest_kwarg)
|
||||
if default is not _MISSING:
|
||||
kwargs["default"] = default
|
||||
parser.add_argument(*names, **kwargs)
|
||||
continue
|
||||
|
||||
# Scalar types (str, int, float, etc.)
|
||||
type_func = arg_meta.type_parser or _infer_type_func(inner_type)
|
||||
kwargs = dict(type=type_func, help=arg_meta.help, **dest_kwarg)
|
||||
if arg_meta.choices:
|
||||
kwargs["choices"] = arg_meta.choices
|
||||
if arg_meta.nargs:
|
||||
kwargs["nargs"] = arg_meta.nargs
|
||||
if default is not _MISSING:
|
||||
kwargs["default"] = default
|
||||
if arg_meta.const is not None:
|
||||
kwargs["const"] = arg_meta.const
|
||||
if (
|
||||
arg_meta.required is True
|
||||
or (arg_meta.required is None and default is _MISSING)
|
||||
) and any(name.startswith("-") for name in names):
|
||||
kwargs["required"] = True
|
||||
parser.add_argument(*names, **kwargs)
|
||||
@@ -0,0 +1,110 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoRAPathAction(argparse.Action):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
lora_paths = []
|
||||
if values:
|
||||
assert isinstance(values, list), "Expected a list of LoRA paths."
|
||||
for lora_path in values:
|
||||
lora_path = lora_path.strip()
|
||||
if lora_path.startswith("{") and lora_path.endswith("}"):
|
||||
obj = json.loads(lora_path)
|
||||
assert "lora_path" in obj and "lora_name" in obj, (
|
||||
f"{repr(lora_path)} looks like a JSON str, "
|
||||
"but it does not contain 'lora_name' and 'lora_path' keys."
|
||||
)
|
||||
lora_paths.append(obj)
|
||||
else:
|
||||
lora_paths.append(lora_path)
|
||||
|
||||
setattr(namespace, self.dest, lora_paths)
|
||||
|
||||
|
||||
def print_deprecated_warning(message: str):
|
||||
logger.warning(f"\033[1;33m{message}\033[0m")
|
||||
|
||||
|
||||
class DeprecatedAction(argparse.Action):
|
||||
def __init__(self, option_strings, dest, nargs=0, **kwargs):
|
||||
super(DeprecatedAction, self).__init__(
|
||||
option_strings, dest, nargs=nargs, **kwargs
|
||||
)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
print_deprecated_warning(
|
||||
f"The command line argument '{option_string}' is deprecated and will be removed in future versions."
|
||||
)
|
||||
|
||||
|
||||
class DeprecatedStoreTrueAction(argparse.Action):
|
||||
"""Deprecated flag that still stores True and prints a warning."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
option_strings,
|
||||
dest,
|
||||
new_flag=None,
|
||||
nargs=0,
|
||||
const=True,
|
||||
default=False,
|
||||
**kwargs,
|
||||
):
|
||||
self.new_flag = new_flag
|
||||
super().__init__(
|
||||
option_strings, dest, nargs=nargs, const=const, default=default, **kwargs
|
||||
)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
replacement = f" Use '{self.new_flag}' instead." if self.new_flag else ""
|
||||
print_deprecated_warning(
|
||||
f"'{option_string}' is deprecated and will be removed in a future release.{replacement}"
|
||||
)
|
||||
setattr(namespace, self.dest, True)
|
||||
|
||||
|
||||
class DeprecatedStoreConstAction(argparse.Action):
|
||||
"""Deprecated boolean flag that stores a fixed string/value into ``dest``
|
||||
and prints a warning. Used to translate a legacy boolean flag into a
|
||||
setting on the new per-phase config dict (e.g.
|
||||
``--disable-piecewise-cuda-graph`` -> ``cuda_graph_backend_prefill="disabled"``)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
option_strings,
|
||||
dest,
|
||||
new_flag=None,
|
||||
const_value=None,
|
||||
nargs=0,
|
||||
default=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.new_flag = new_flag
|
||||
self.const_value = const_value
|
||||
super().__init__(option_strings, dest, nargs=nargs, default=default, **kwargs)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
replacement = f" Use '{self.new_flag}' instead." if self.new_flag else ""
|
||||
print_deprecated_warning(
|
||||
f"'{option_string}' is deprecated and will be removed in a future release.{replacement}"
|
||||
)
|
||||
setattr(namespace, self.dest, self.const_value)
|
||||
|
||||
|
||||
class DeprecatedAliasStoreAction(argparse.Action):
|
||||
"""Deprecated alias that stores its value and prints a warning."""
|
||||
|
||||
def __init__(self, option_strings, dest, new_flag=None, **kwargs):
|
||||
self.new_flag = new_flag
|
||||
super().__init__(option_strings, dest, **kwargs)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
replacement = f" Use '{self.new_flag}' instead." if self.new_flag else ""
|
||||
print_deprecated_warning(
|
||||
f"'{option_string}' is deprecated and will be removed in a future release.{replacement}"
|
||||
)
|
||||
setattr(namespace, self.dest, values)
|
||||
@@ -0,0 +1,97 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def apply_deepseek_v4_defaults(server_args: ServerArgs, model_arch: str) -> None:
|
||||
"""Residual imperative arm of the DeepSeek V4 defaults.
|
||||
|
||||
The attention/page/window/MoE-runner declarations moved to the override
|
||||
registry (arg_groups/overrides.py: _deepseek_v4_overrides) and the
|
||||
kv-cache dtype default to the resolution pipeline
|
||||
(_deepseek_v4_kv_cache_dtype, invoked below at its legacy slot). This
|
||||
keeps, at the legacy slot: the ROCm env fill (env-write policy), the
|
||||
max_running_requests fill (the speculative hook is a later writer of
|
||||
that field) and the validations.
|
||||
"""
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
# FlashMLA sparse prefill (SGLANG_OPT_FLASHMLA_SPARSE_PREFILL, default on)
|
||||
# currently returns incorrect output for DeepSeek-V4-Flash on ROCm/HIP
|
||||
# (MI355X), which breaks the disaggregation nightly. Keep the previous
|
||||
# (dense prefill) behavior on ROCm until the sparse kernel is validated
|
||||
# there;
|
||||
if is_hip():
|
||||
logger.warning(
|
||||
"Disabling SGLANG_OPT_FLASHMLA_SPARSE_PREFILL by default on ROCm/HIP "
|
||||
f"for {model_arch}; set it explicitly to override."
|
||||
)
|
||||
envs.SGLANG_OPT_FLASHMLA_SPARSE_PREFILL.set(False)
|
||||
|
||||
# The kv-cache dtype default moved to the resolution pipeline
|
||||
# (arg_groups/overrides.py: _deepseek_v4_kv_cache_dtype), invoked here at
|
||||
# its legacy slot.
|
||||
from sglang.srt.arg_groups.overrides import (
|
||||
_deepseek_v4_kv_cache_dtype,
|
||||
run_post_process_pass,
|
||||
)
|
||||
|
||||
run_post_process_pass(server_args, _deepseek_v4_kv_cache_dtype)
|
||||
|
||||
if server_args.max_running_requests is None:
|
||||
server_args.max_running_requests = 256
|
||||
logger.warning(
|
||||
f"Setting max_running_requests to {server_args.max_running_requests} for {model_arch}."
|
||||
)
|
||||
|
||||
if server_args.speculative_algorithm is not None:
|
||||
assert server_args.speculative_algorithm in (
|
||||
"EAGLE",
|
||||
"DSPARK",
|
||||
), f"Only EAGLE and DSPARK speculative algorithms are supported for {model_arch}"
|
||||
if server_args.speculative_algorithm == "EAGLE":
|
||||
assert (
|
||||
server_args.speculative_eagle_topk == 1
|
||||
), f"Only EAGLE speculative algorithm with topk == 1 is supported for {model_arch}"
|
||||
|
||||
|
||||
def validate_deepseek_v4_cp(server_args: ServerArgs) -> None:
|
||||
"""Validate DeepSeek V4 context-parallel configuration."""
|
||||
if not server_args.enable_prefill_cp:
|
||||
return
|
||||
|
||||
if server_args.cp_strategy != "interleave":
|
||||
raise ValueError(
|
||||
"DeepSeekV4 only supports interleave CP strategy, "
|
||||
f"got {server_args.cp_strategy}"
|
||||
)
|
||||
|
||||
server_args.enable_dsa_prefill_context_parallel = True
|
||||
server_args.dsa_prefill_cp_mode = "round-robin-split"
|
||||
server_args.enable_dp_attention = True
|
||||
server_args.moe_dense_tp_size = 1
|
||||
server_args.attn_cp_size = server_args.tp_size // server_args.dp_size
|
||||
assert (
|
||||
server_args.dp_size == 1
|
||||
), "For round-robin split mode, dp attention is not supported."
|
||||
assert (
|
||||
server_args.tp_size <= 8
|
||||
), "Context parallel only supports single machine (tp_size <= 8). Cross-machine CP has precision issues."
|
||||
logger.warning(
|
||||
"Disabling SGLANG_OPT_FLASHMLA_SPARSE_PREFILL because DeepSeekV4 "
|
||||
"context parallelism is enabled."
|
||||
)
|
||||
envs.SGLANG_OPT_FLASHMLA_SPARSE_PREFILL.set(False)
|
||||
logger.warning(
|
||||
f"Enable Context Parallel for DeepSeekV4, "
|
||||
f"dp_size={server_args.dp_size}, moe_dense_tp_size={server_args.moe_dense_tp_size}, "
|
||||
f"attn_cp_size={server_args.attn_cp_size}, ep_size={server_args.ep_size}, tp_size={server_args.tp_size}"
|
||||
)
|
||||
@@ -0,0 +1,137 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HISPARSE_CUDA_DSA_BACKENDS_BY_DTYPE = {
|
||||
"bfloat16": {"flashmla_sparse"},
|
||||
"fp8_e4m3": {"flashmla_kv"},
|
||||
}
|
||||
HISPARSE_ROCM_DSA_BACKENDS = {"tilelang", "aiter"}
|
||||
HISPARSE_KV_CACHE_DTYPES = ("bfloat16", "fp8_e4m3")
|
||||
|
||||
|
||||
def _is_hip() -> bool:
|
||||
from sglang.srt.server_args import is_hip
|
||||
|
||||
return is_hip()
|
||||
|
||||
|
||||
def _hisparse_default_backend(kv_cache_dtype: str) -> str:
|
||||
if _is_hip():
|
||||
return "tilelang"
|
||||
return "flashmla_kv" if kv_cache_dtype == "fp8_e4m3" else "flashmla_sparse"
|
||||
|
||||
|
||||
def _hisparse_allowed_backends(kv_cache_dtype: str) -> set[str]:
|
||||
if _is_hip():
|
||||
return HISPARSE_ROCM_DSA_BACKENDS
|
||||
return HISPARSE_CUDA_DSA_BACKENDS_BY_DTYPE.get(
|
||||
kv_cache_dtype, {"flashmla_sparse", "flashmla_kv"}
|
||||
)
|
||||
|
||||
|
||||
# The hisparse DSA backend defaults moved to the resolution pipeline
|
||||
# (arg_groups/overrides.py: _dsa_split_backend_resolution, hisparse arm).
|
||||
|
||||
|
||||
def validate_hisparse_dsa_backend(
|
||||
server_args: ServerArgs, attr: str, label: str
|
||||
) -> None:
|
||||
from sglang.srt.arg_groups.overrides import resolved_view
|
||||
|
||||
# Invoked after the DSA kv-cache-dtype / split-backend declarations:
|
||||
# read the resolving state through the view.
|
||||
view = resolved_view(server_args)
|
||||
backend = getattr(view, attr)
|
||||
kv_cache_dtype = view.kv_cache_dtype
|
||||
allowed_backends = _hisparse_allowed_backends(kv_cache_dtype)
|
||||
if backend is not None and backend not in allowed_backends:
|
||||
raise ValueError(
|
||||
f"HiSparse supports DSA {label} backend(s) {sorted(allowed_backends)} "
|
||||
f"on this platform with --kv-cache-dtype={kv_cache_dtype}, "
|
||||
f"but got --dsa-{label}-backend={backend}. "
|
||||
f"Please use --dsa-{label}-backend="
|
||||
f"{_hisparse_default_backend(kv_cache_dtype)} "
|
||||
"or omit it."
|
||||
)
|
||||
|
||||
|
||||
def validate_hisparse_kv_cache_dtype(server_args: ServerArgs) -> None:
|
||||
from sglang.srt.arg_groups.overrides import resolved_view
|
||||
|
||||
kv_cache_dtype = resolved_view(server_args).kv_cache_dtype
|
||||
if kv_cache_dtype in HISPARSE_KV_CACHE_DTYPES:
|
||||
return
|
||||
|
||||
choices = " or ".join(
|
||||
f"--kv-cache-dtype={dtype}" for dtype in HISPARSE_KV_CACHE_DTYPES
|
||||
)
|
||||
raise ValueError(
|
||||
f"HiSparse requires one of {HISPARSE_KV_CACHE_DTYPES} KV cache dtypes, "
|
||||
f"but got --kv-cache-dtype={kv_cache_dtype}. Please use {choices}."
|
||||
)
|
||||
|
||||
|
||||
def validate_hisparse(server_args: ServerArgs) -> None:
|
||||
"""Validate --enable-hisparse constraints (model class, radix cache, DSA backend)."""
|
||||
if not server_args.enable_hisparse:
|
||||
return
|
||||
|
||||
from sglang.srt.configs.model_config import (
|
||||
is_deepseek_dsa,
|
||||
is_deepseek_v4,
|
||||
)
|
||||
|
||||
hf_config = server_args.get_model_config().hf_config
|
||||
is_v4_hisparse = is_deepseek_v4(hf_config)
|
||||
is_hip = _is_hip()
|
||||
assert is_deepseek_dsa(hf_config) or is_v4_hisparse, (
|
||||
"--enable-hisparse is only supported for DSA (DeepSeek Sparse Attention) "
|
||||
"models (e.g., DeepSeek V3.2, GLM-5) and DeepSeek V4 now. "
|
||||
)
|
||||
|
||||
assert (
|
||||
server_args.disable_radix_cache
|
||||
), "Hierarchical sparse attention currently requires --disable-radix-cache."
|
||||
|
||||
# DSv4 hisparse handles its own dtype/backend pairing elsewhere; the dtype-
|
||||
# aware checks below only apply to the DSA hisparse path.
|
||||
if is_hip and is_v4_hisparse:
|
||||
# TEMPORARY GUARD: DSv4 HiSparse is not supported on the unified-KV path.
|
||||
# In unified-KV mode c4_kv_pool is None, so DeepSeekV4HiSparseTokenToKVPoolAllocator
|
||||
# cannot attach and pool init dies with a cryptic AssertionError. Fail fast
|
||||
# at startup with a clear message instead. Remove once unified-KV HiSparse lands.
|
||||
from sglang.srt.layers.attention.dsv4.unified_kv_kernels.env_gate import (
|
||||
is_unified_kv_triton,
|
||||
)
|
||||
|
||||
if is_unified_kv_triton():
|
||||
raise ValueError(
|
||||
"--enable-hisparse is not supported with the unified-KV path on ROCm"
|
||||
"(SGLANG_HACK_FLASHMLA_BACKEND=unified_kv_triton) for DeepSeek-V4: "
|
||||
"HiSparse currently requires the separate packed KV layout. "
|
||||
"Either set SGLANG_HACK_FLASHMLA_BACKEND=triton, or run without "
|
||||
"--enable-hisparse."
|
||||
)
|
||||
return
|
||||
|
||||
from sglang.srt.arg_groups.overrides import resolved_view
|
||||
|
||||
if resolved_view(server_args).kv_cache_dtype not in (
|
||||
"bfloat16",
|
||||
"auto",
|
||||
"fp8_e4m3",
|
||||
):
|
||||
validate_hisparse_kv_cache_dtype(server_args)
|
||||
|
||||
for attr, label in [
|
||||
("dsa_prefill_backend", "prefill"),
|
||||
("dsa_decode_backend", "decode"),
|
||||
]:
|
||||
validate_hisparse_dsa_backend(server_args, attr, label)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_pd_disaggregation(server_args: ServerArgs) -> None:
|
||||
"""Validate and normalize PD-disaggregation server args."""
|
||||
# "mooncake_tcp" is mooncake with the TCP transport forced: set MC_FORCE_TCP
|
||||
# so mooncake installs TcpTransport instead of RDMA, rewrite the backend to
|
||||
# mooncake, and skip RDMA HCA selection. Must run before backend-name checks.
|
||||
if server_args.disaggregation_transfer_backend == "mooncake_tcp":
|
||||
os.environ.setdefault("MC_FORCE_TCP", "1")
|
||||
server_args.disaggregation_transfer_backend = "mooncake"
|
||||
server_args.disaggregation_ib_device = None
|
||||
logger.info(
|
||||
"disaggregation transfer backend 'mooncake_tcp' -> mooncake "
|
||||
"with MC_FORCE_TCP=1 (TCP transport, no RDMA)"
|
||||
)
|
||||
|
||||
if server_args.disaggregation_mode == "decode":
|
||||
if server_args.disaggregation_decode_enable_radix_cache:
|
||||
if server_args.enable_hisparse:
|
||||
raise ValueError(
|
||||
"--disaggregation-decode-enable-radix-cache is incompatible "
|
||||
"with --enable-hisparse"
|
||||
)
|
||||
if server_args.disaggregation_transfer_backend == "fake":
|
||||
raise ValueError(
|
||||
"--disaggregation-decode-enable-radix-cache is incompatible "
|
||||
"with --disaggregation-transfer-backend fake"
|
||||
)
|
||||
if server_args.speculative_algorithm is not None:
|
||||
raise ValueError(
|
||||
"--disaggregation-decode-enable-radix-cache is incompatible "
|
||||
"with speculative decoding "
|
||||
f"(--speculative-algorithm {server_args.speculative_algorithm})"
|
||||
)
|
||||
from sglang.srt.arg_groups.overrides import resolved_view
|
||||
|
||||
if resolved_view(server_args).enable_dp_attention:
|
||||
logger.warning(
|
||||
"EXPERIMENTAL: Decode radix cache with DP attention. "
|
||||
"Requires prefix-aware DP rank routing for optimal cache hits."
|
||||
)
|
||||
server_args.disable_radix_cache = False
|
||||
logger.warning("EXPERIMENTAL: Radix cache is enabled for decode server")
|
||||
else:
|
||||
server_args.disable_radix_cache = True
|
||||
logger.warning("KV cache is forced as chunk cache for decode server")
|
||||
|
||||
# Default the number of *extra* decode req_to_token slots reserved for
|
||||
# in-transfer (being-received-from-prefill) requests, on top of the
|
||||
# max_running_requests-derived pool. Large batches get none; small
|
||||
# per-worker batches reserve 2x the batch as cheap overlap headroom.
|
||||
if server_args.disaggregation_decode_extra_slots is None:
|
||||
extra_slots = 0
|
||||
if server_args.max_running_requests is not None:
|
||||
per_worker = server_args.max_running_requests // max(
|
||||
1, server_args.dp_size
|
||||
)
|
||||
if per_worker <= 32:
|
||||
extra_slots = per_worker * 2
|
||||
server_args.disaggregation_decode_extra_slots = extra_slots
|
||||
|
||||
elif server_args.disaggregation_mode == "prefill":
|
||||
assert (
|
||||
server_args.disaggregation_transfer_backend != "fake"
|
||||
), "Prefill server does not support 'fake' as the transfer backend"
|
||||
|
||||
if server_args.disaggregation_mode in ("prefill", "decode"):
|
||||
if (
|
||||
envs.SGLANG_DISAGG_STAGING_BUFFER.get()
|
||||
and server_args.disaggregation_transfer_backend not in ("mooncake", "nixl")
|
||||
):
|
||||
raise ValueError(
|
||||
f"SGLANG_DISAGG_STAGING_BUFFER requires "
|
||||
f"disaggregation_transfer_backend='mooncake' or 'nixl', "
|
||||
f"got '{server_args.disaggregation_transfer_backend}'."
|
||||
)
|
||||
@@ -0,0 +1,792 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _disable_overlap_schedule_for_cpu(server_args: ServerArgs) -> None:
|
||||
if server_args.device != "cpu" or server_args.disable_overlap_schedule:
|
||||
return
|
||||
|
||||
server_args.disable_overlap_schedule = True
|
||||
logger.warning(
|
||||
"Overlap schedule is not implemented for speculative decoding on CPU."
|
||||
)
|
||||
|
||||
|
||||
def _resolve_speculative_algorithm_alias(
|
||||
speculative_algorithm: Optional[str],
|
||||
speculative_draft_model_path: Optional[str],
|
||||
trust_remote_code: bool = False,
|
||||
kwargs: Optional[dict] = {},
|
||||
) -> Optional[str]:
|
||||
"""Resolve CLI speculative algorithm; NEXTN/EAGLE may become FROZEN_KV_MTP for Gemma4 assistant drafts."""
|
||||
|
||||
is_gemma4_draft = False
|
||||
if speculative_draft_model_path:
|
||||
from sglang.srt.utils.hf_transformers_utils import get_config
|
||||
|
||||
cfg = get_config(
|
||||
speculative_draft_model_path, trust_remote_code=trust_remote_code, **kwargs
|
||||
)
|
||||
draft_archs = getattr(cfg, "architectures", None) or []
|
||||
is_gemma4_draft = any(
|
||||
arch in ("Gemma4AssistantForCausalLM", "Gemma4UnifiedAssistantForCausalLM")
|
||||
for arch in draft_archs
|
||||
)
|
||||
|
||||
if speculative_algorithm == "EAGLE3" and is_gemma4_draft:
|
||||
raise ValueError(
|
||||
"Gemma4AssistantForCausalLM draft requires "
|
||||
"--speculative-algorithm NEXTN or EAGLE; EAGLE3 is "
|
||||
"not supported for this draft architecture."
|
||||
)
|
||||
|
||||
if speculative_algorithm == "NEXTN" or speculative_algorithm == "EAGLE":
|
||||
if is_gemma4_draft:
|
||||
logger.info(
|
||||
"Detected Gemma4AssistantForCausalLM draft; "
|
||||
f"promoting --speculative-algorithm {speculative_algorithm} to FROZEN_KV_MTP."
|
||||
)
|
||||
return "FROZEN_KV_MTP"
|
||||
return "EAGLE"
|
||||
|
||||
return speculative_algorithm
|
||||
|
||||
|
||||
def handle_speculative_decoding(server_args: ServerArgs) -> None:
|
||||
if (
|
||||
server_args.speculative_draft_model_path is not None
|
||||
and server_args.speculative_draft_model_revision is None
|
||||
):
|
||||
server_args.speculative_draft_model_revision = "main"
|
||||
|
||||
# Moved to the resolution pipeline (arg_groups/overrides.py:
|
||||
# _speculative_moe_runner_default), invoked here at its legacy slot.
|
||||
from sglang.srt.arg_groups.overrides import (
|
||||
_speculative_moe_runner_default,
|
||||
run_post_process_pass,
|
||||
)
|
||||
|
||||
run_post_process_pass(server_args, _speculative_moe_runner_default)
|
||||
|
||||
if server_args.speculative_algorithm is not None:
|
||||
server_args.speculative_algorithm = server_args.speculative_algorithm.upper()
|
||||
|
||||
# Removal notice for the retired env var; raw os.getenv on purpose -- the
|
||||
# Envs descriptor is gone. Drop this check after one release.
|
||||
if os.getenv("SGLANG_ENABLE_SPEC_V2") is not None:
|
||||
logger.warning(
|
||||
"SGLANG_ENABLE_SPEC_V2 has been removed: speculative decoding "
|
||||
"always runs the V2 worker. Use --disable-overlap-schedule to "
|
||||
"select the non-overlap (synchronous) path."
|
||||
)
|
||||
|
||||
kwargs = {}
|
||||
|
||||
override_config_file = server_args.decrypted_draft_config_file
|
||||
if override_config_file and override_config_file.strip():
|
||||
kwargs["_configuration_file"] = override_config_file.strip()
|
||||
|
||||
server_args.speculative_algorithm = _resolve_speculative_algorithm_alias(
|
||||
server_args.speculative_algorithm,
|
||||
server_args.speculative_draft_model_path,
|
||||
trust_remote_code=server_args.trust_remote_code,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
|
||||
# Validate --speculative-draft-window-size once, regardless of algorithm.
|
||||
# Consumed by DFLASH (compact draft KV cache) and Llama EAGLE-3 (drafter attention SWA).
|
||||
if server_args.speculative_draft_window_size is not None:
|
||||
window_size = int(server_args.speculative_draft_window_size)
|
||||
if window_size <= 0:
|
||||
raise ValueError(
|
||||
f"--speculative-draft-window-size must be positive, got {window_size}."
|
||||
)
|
||||
server_args.speculative_draft_window_size = window_size
|
||||
if server_args.speculative_algorithm not in ("EAGLE3", "DFLASH"):
|
||||
logger.warning(
|
||||
"--speculative-draft-window-size has no effect with "
|
||||
"speculative_algorithm=%s (honored by Llama EAGLE-3 and DFLASH only).",
|
||||
server_args.speculative_algorithm,
|
||||
)
|
||||
|
||||
algo = None
|
||||
if server_args.speculative_algorithm is not None:
|
||||
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
|
||||
from sglang.srt.speculative.spec_registry import CustomSpecAlgo
|
||||
|
||||
algo = SpeculativeAlgorithm.from_string(server_args.speculative_algorithm)
|
||||
|
||||
# TODO: move the per-algorithm validation below into spec module hooks.
|
||||
if isinstance(algo, CustomSpecAlgo) and algo.validate_server_args is not None:
|
||||
algo.validate_server_args(server_args)
|
||||
|
||||
if server_args.speculative_skip_dp_mlp_sync:
|
||||
assert server_args.speculative_algorithm == "EAGLE", (
|
||||
"--speculative-skip-dp-mlp-sync is only supported with "
|
||||
f"speculative_algorithm == EAGLE, got {server_args.speculative_algorithm}."
|
||||
)
|
||||
|
||||
if server_args.speculative_adaptive:
|
||||
_maybe_disable_adaptive(server_args)
|
||||
if server_args.speculative_adaptive:
|
||||
_init_adaptive_speculative_params(server_args)
|
||||
|
||||
if algo is not None:
|
||||
algo.handle_server_args(server_args)
|
||||
|
||||
|
||||
def _handle_dflash(server_args: ServerArgs) -> None:
|
||||
from sglang.srt.arg_groups.overrides import resolved_view
|
||||
|
||||
if not server_args.device.startswith("cuda"):
|
||||
raise ValueError("DFLASH speculative decoding only supports CUDA device.")
|
||||
|
||||
if resolved_view(server_args).enable_dp_attention:
|
||||
raise ValueError(
|
||||
"Currently DFLASH speculative decoding does not support dp attention."
|
||||
)
|
||||
|
||||
if server_args.pp_size != 1:
|
||||
raise ValueError(
|
||||
"Currently DFLASH speculative decoding only supports pp_size == 1."
|
||||
)
|
||||
|
||||
if server_args.speculative_draft_model_path is None:
|
||||
raise ValueError(
|
||||
"DFLASH speculative decoding requires setting --speculative-draft-model-path."
|
||||
)
|
||||
|
||||
# DFLASH does not use EAGLE-style `num_steps`/`topk`, but those fields still
|
||||
# affect generic scheduler/KV-cache accounting (buffer sizing, KV freeing,
|
||||
# RoPE reservation). Force them to 1 to avoid surprising memory behavior.
|
||||
#
|
||||
# For DFlash, the natural unit is `block_size` (verify window length).
|
||||
if server_args.speculative_num_steps is None:
|
||||
server_args.speculative_num_steps = 1
|
||||
elif int(server_args.speculative_num_steps) != 1:
|
||||
logger.warning(
|
||||
"DFLASH only supports speculative_num_steps == 1; overriding speculative_num_steps=%s to 1.",
|
||||
server_args.speculative_num_steps,
|
||||
)
|
||||
server_args.speculative_num_steps = 1
|
||||
|
||||
if server_args.speculative_eagle_topk is None:
|
||||
server_args.speculative_eagle_topk = 1
|
||||
elif int(server_args.speculative_eagle_topk) != 1:
|
||||
logger.warning(
|
||||
"DFLASH only supports speculative_eagle_topk == 1; overriding speculative_eagle_topk=%s to 1.",
|
||||
server_args.speculative_eagle_topk,
|
||||
)
|
||||
server_args.speculative_eagle_topk = 1
|
||||
|
||||
if server_args.speculative_dflash_block_size is not None:
|
||||
if int(server_args.speculative_dflash_block_size) <= 0:
|
||||
raise ValueError(
|
||||
"DFLASH requires --speculative-dflash-block-size to be positive, "
|
||||
f"got {server_args.speculative_dflash_block_size}."
|
||||
)
|
||||
if server_args.speculative_num_draft_tokens is not None and int(
|
||||
server_args.speculative_num_draft_tokens
|
||||
) != int(server_args.speculative_dflash_block_size):
|
||||
raise ValueError(
|
||||
"Both --speculative-num-draft-tokens and --speculative-dflash-block-size are set "
|
||||
"but they differ. For DFLASH they must match. "
|
||||
f"speculative_num_draft_tokens={server_args.speculative_num_draft_tokens}, "
|
||||
f"speculative_dflash_block_size={server_args.speculative_dflash_block_size}."
|
||||
)
|
||||
server_args.speculative_num_draft_tokens = int(
|
||||
server_args.speculative_dflash_block_size
|
||||
)
|
||||
|
||||
if server_args.speculative_num_draft_tokens is None:
|
||||
from sglang.srt.speculative.dflash_utils import (
|
||||
parse_dflash_draft_config,
|
||||
)
|
||||
|
||||
model_override_args = json.loads(server_args.json_model_override_args)
|
||||
inferred_block_size = None
|
||||
try:
|
||||
from sglang.srt.utils.hf_transformers_utils import get_config
|
||||
|
||||
draft_hf_config = get_config(
|
||||
server_args.speculative_draft_model_path,
|
||||
trust_remote_code=server_args.trust_remote_code,
|
||||
revision=server_args.speculative_draft_model_revision,
|
||||
model_override_args=model_override_args,
|
||||
)
|
||||
inferred_block_size = parse_dflash_draft_config(
|
||||
draft_hf_config=draft_hf_config
|
||||
).resolve_block_size(default=None)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to infer DFLASH block_size from draft model config; "
|
||||
"defaulting speculative_num_draft_tokens to 16. Error: %s",
|
||||
e,
|
||||
)
|
||||
|
||||
if inferred_block_size is None:
|
||||
inferred_block_size = 16
|
||||
logger.warning(
|
||||
"speculative_num_draft_tokens is not set; defaulting to %d for DFLASH.",
|
||||
inferred_block_size,
|
||||
)
|
||||
server_args.speculative_num_draft_tokens = inferred_block_size
|
||||
|
||||
if server_args.speculative_draft_window_size is not None:
|
||||
draft_tokens = int(server_args.speculative_num_draft_tokens)
|
||||
if server_args.speculative_draft_window_size < draft_tokens:
|
||||
raise ValueError(
|
||||
"--speculative-draft-window-size must be >= "
|
||||
"--speculative-num-draft-tokens (block_size). "
|
||||
f"window_size={server_args.speculative_draft_window_size}, block_size={draft_tokens}."
|
||||
)
|
||||
|
||||
_resolve_dflash_draft_attention_backend(server_args)
|
||||
|
||||
if server_args.max_running_requests is None:
|
||||
server_args.max_running_requests = 48
|
||||
logger.warning(
|
||||
"Max running requests is reset to 48 for speculative decoding. You can override this by explicitly setting --max-running-requests."
|
||||
)
|
||||
|
||||
if server_args.enable_mixed_chunk:
|
||||
server_args.enable_mixed_chunk = False
|
||||
logger.warning(
|
||||
"Mixed chunked prefill is disabled because of using dflash speculative decoding."
|
||||
)
|
||||
|
||||
|
||||
def _target_checkpoint_bundles_dspark_draft(server_args: ServerArgs) -> bool:
|
||||
from sglang.srt.speculative.dspark_components.dspark_config import (
|
||||
checkpoint_bundles_dspark_draft,
|
||||
)
|
||||
|
||||
return checkpoint_bundles_dspark_draft(server_args.get_model_config().hf_config)
|
||||
|
||||
|
||||
def _handle_dspark(server_args: ServerArgs) -> None:
|
||||
if not server_args.device.startswith("cuda"):
|
||||
raise ValueError("DSpark speculative decoding only supports CUDA device.")
|
||||
|
||||
if server_args.enable_dp_attention:
|
||||
if not server_args.enable_dp_lm_head:
|
||||
raise ValueError("DSpark with dp attention requires --enable-dp-lm-head.")
|
||||
if server_args.moe_a2a_backend != "none":
|
||||
raise ValueError(
|
||||
"DSpark with dp attention only supports the built-in TP MoE "
|
||||
f"(moe_a2a_backend='none'), got {server_args.moe_a2a_backend!r}."
|
||||
)
|
||||
if server_args.attn_cp_size > 1:
|
||||
raise ValueError(
|
||||
"DSpark with dp attention does not support context parallel "
|
||||
f"(attn_cp_size={server_args.attn_cp_size})."
|
||||
)
|
||||
if (
|
||||
server_args.speculative_moe_a2a_backend is not None
|
||||
and server_args.speculative_moe_a2a_backend != server_args.moe_a2a_backend
|
||||
):
|
||||
raise ValueError(
|
||||
"DSpark ignores --speculative-moe-a2a-backend; with dp attention it "
|
||||
f"must match the target moe_a2a_backend={server_args.moe_a2a_backend!r} "
|
||||
f"(got {server_args.speculative_moe_a2a_backend!r})."
|
||||
)
|
||||
|
||||
if server_args.pp_size != 1:
|
||||
raise ValueError(
|
||||
"Currently DSpark speculative decoding only supports pp_size == 1."
|
||||
)
|
||||
|
||||
if server_args.speculative_draft_model_path is None:
|
||||
if _target_checkpoint_bundles_dspark_draft(server_args):
|
||||
server_args.speculative_draft_model_path = server_args.model_path
|
||||
server_args.speculative_draft_model_revision = server_args.revision
|
||||
logger.info(
|
||||
"DSpark draft weights are bundled in the target checkpoint; "
|
||||
"defaulting --speculative-draft-model-path to --model-path (%s).",
|
||||
server_args.model_path,
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"DSpark dense speculative decoding requires setting "
|
||||
"--speculative-draft-model-path."
|
||||
)
|
||||
|
||||
if server_args.speculative_num_steps is None:
|
||||
server_args.speculative_num_steps = 1
|
||||
elif int(server_args.speculative_num_steps) != 1:
|
||||
logger.warning(
|
||||
"DSpark only supports speculative_num_steps == 1; overriding speculative_num_steps=%s to 1.",
|
||||
server_args.speculative_num_steps,
|
||||
)
|
||||
server_args.speculative_num_steps = 1
|
||||
|
||||
if server_args.speculative_eagle_topk is None:
|
||||
server_args.speculative_eagle_topk = 1
|
||||
elif int(server_args.speculative_eagle_topk) != 1:
|
||||
logger.warning(
|
||||
"DSpark only supports speculative_eagle_topk == 1; overriding speculative_eagle_topk=%s to 1.",
|
||||
server_args.speculative_eagle_topk,
|
||||
)
|
||||
server_args.speculative_eagle_topk = 1
|
||||
|
||||
gamma: Optional[int] = None
|
||||
if server_args.speculative_dspark_block_size is not None:
|
||||
if int(server_args.speculative_dspark_block_size) <= 0:
|
||||
raise ValueError(
|
||||
"DSpark requires --speculative-dspark-block-size to be positive, "
|
||||
f"got {server_args.speculative_dspark_block_size}."
|
||||
)
|
||||
gamma = int(server_args.speculative_dspark_block_size)
|
||||
else:
|
||||
from sglang.srt.speculative.dspark_components.dspark_config import (
|
||||
DEFAULT_DSPARK_GAMMA,
|
||||
read_draft_checkpoint_gamma,
|
||||
)
|
||||
|
||||
try:
|
||||
gamma = read_draft_checkpoint_gamma(server_args=server_args)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to read DSpark gamma from draft model config; "
|
||||
"cannot cross-check --speculative-num-draft-tokens. Error: %s",
|
||||
e,
|
||||
)
|
||||
if gamma is None and server_args.speculative_num_draft_tokens is None:
|
||||
gamma = DEFAULT_DSPARK_GAMMA
|
||||
logger.warning(
|
||||
"DSpark gamma is not set; defaulting to %d.",
|
||||
gamma,
|
||||
)
|
||||
|
||||
if gamma is not None:
|
||||
verify_window = int(gamma) + 1
|
||||
if (
|
||||
server_args.speculative_num_draft_tokens is not None
|
||||
and int(server_args.speculative_num_draft_tokens) != verify_window
|
||||
):
|
||||
raise ValueError(
|
||||
"DSpark speculative_num_draft_tokens must equal gamma + 1 "
|
||||
f"(= {verify_window} for gamma={gamma}), but got "
|
||||
f"speculative_num_draft_tokens={server_args.speculative_num_draft_tokens}."
|
||||
)
|
||||
server_args.speculative_num_draft_tokens = verify_window
|
||||
|
||||
if server_args.speculative_num_draft_tokens is None:
|
||||
raise ValueError(
|
||||
"DSpark could not resolve speculative_num_draft_tokens; set "
|
||||
"--speculative-dspark-block-size (= gamma)."
|
||||
)
|
||||
if int(server_args.speculative_num_draft_tokens) < 2:
|
||||
raise ValueError(
|
||||
"DSpark speculative_num_draft_tokens must be >= 2 (= gamma + 1), "
|
||||
f"got {server_args.speculative_num_draft_tokens}."
|
||||
)
|
||||
|
||||
if server_args.max_running_requests is None:
|
||||
server_args.max_running_requests = 48
|
||||
logger.warning(
|
||||
"Max running requests is reset to 48 for speculative decoding. You can override this by explicitly setting --max-running-requests."
|
||||
)
|
||||
|
||||
if server_args.enable_mixed_chunk:
|
||||
server_args.enable_mixed_chunk = False
|
||||
logger.warning(
|
||||
"Mixed chunked prefill is disabled because of using dspark speculative decoding."
|
||||
)
|
||||
|
||||
from sglang.srt.speculative.ragged_verify import (
|
||||
RaggedVerifyMode,
|
||||
read_ragged_verify_mode,
|
||||
)
|
||||
|
||||
ragged_mode = read_ragged_verify_mode()
|
||||
if (
|
||||
server_args.speculative_dspark_align_verify_tokens_to_graph_tier
|
||||
and ragged_mode is not RaggedVerifyMode.COMPACT
|
||||
):
|
||||
logger.warning(
|
||||
"--speculative-dspark-align-verify-tokens-to-graph-tier only takes "
|
||||
"effect with SGLANG_RAGGED_VERIFY_MODE=compact (got %r); it will be "
|
||||
"a no-op.",
|
||||
ragged_mode.value,
|
||||
)
|
||||
if (
|
||||
server_args.speculative_dspark_sps_table_path
|
||||
and ragged_mode is RaggedVerifyMode.STATIC
|
||||
):
|
||||
logger.warning(
|
||||
"--speculative-dspark-sps-table-path feeds the ragged-verify budget "
|
||||
"scheduler, which is off under SGLANG_RAGGED_VERIFY_MODE=static; it "
|
||||
"will be a no-op."
|
||||
)
|
||||
|
||||
|
||||
def _resolve_dflash_draft_attention_backend(server_args: ServerArgs) -> None:
|
||||
"""Resolve `speculative_draft_attention_backend` to a final, supported value.
|
||||
|
||||
Consumed by ModelRunner's `is_draft_worker` override (one backend for all
|
||||
draft modes).
|
||||
"""
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
supported_draft_backends = ("flashinfer", "fa3", "fa4", "triton", "ascend")
|
||||
# Use triton on ROCm (no FlashInfer), flashinfer on CUDA.
|
||||
fallback_backend = "triton" if is_hip() else "flashinfer"
|
||||
|
||||
draft_backend = server_args.speculative_draft_attention_backend
|
||||
if draft_backend is None:
|
||||
from sglang.srt.arg_groups.overrides import (
|
||||
attention_backends_of,
|
||||
resolved_view,
|
||||
)
|
||||
|
||||
draft_backend, _ = attention_backends_of(resolved_view(server_args))
|
||||
if draft_backend is None:
|
||||
draft_backend = fallback_backend
|
||||
elif draft_backend == "trtllm_mha":
|
||||
logger.warning(
|
||||
"DFLASH draft worker does not support 'trtllm_mha' because the "
|
||||
"draft path requires per-layer DFlash attention. Falling back to "
|
||||
"'%s'.",
|
||||
fallback_backend,
|
||||
)
|
||||
draft_backend = fallback_backend
|
||||
elif draft_backend not in supported_draft_backends:
|
||||
logger.warning(
|
||||
"DFLASH draft worker only supports attention_backend in %s for now, "
|
||||
"but got %r. Falling back to '%s'.",
|
||||
supported_draft_backends,
|
||||
draft_backend,
|
||||
fallback_backend,
|
||||
)
|
||||
draft_backend = fallback_backend
|
||||
# FIXME: avoid overriding server args directly; pass the resolved draft
|
||||
# backend to the draft worker explicitly instead.
|
||||
server_args.speculative_draft_attention_backend = draft_backend
|
||||
|
||||
|
||||
def _handle_frozen_kv_mtp(server_args: ServerArgs) -> None:
|
||||
if server_args.max_running_requests is None:
|
||||
server_args.max_running_requests = 48
|
||||
logger.warning(
|
||||
"Max running requests is reset to 48 for speculative decoding. You can override this by explicitly setting --max-running-requests."
|
||||
)
|
||||
|
||||
if server_args.enable_mixed_chunk:
|
||||
server_args.enable_mixed_chunk = False
|
||||
logger.warning(
|
||||
"Mixed chunked prefill is disabled because of using "
|
||||
"Frozen-KV MTP speculative decoding."
|
||||
)
|
||||
|
||||
|
||||
def _handle_eagle_family(server_args: ServerArgs) -> None:
|
||||
from sglang.srt.arg_groups.overrides import (
|
||||
attention_backends_of,
|
||||
resolved_view,
|
||||
)
|
||||
|
||||
if (
|
||||
server_args.speculative_algorithm == "STANDALONE"
|
||||
and resolved_view(server_args).enable_dp_attention
|
||||
):
|
||||
# TODO: support dp attention for standalone speculative decoding
|
||||
raise ValueError(
|
||||
"Currently standalone speculative decoding does not support dp attention."
|
||||
)
|
||||
|
||||
if server_args.max_running_requests is None:
|
||||
server_args.max_running_requests = 48
|
||||
logger.warning(
|
||||
"Max running requests is reset to 48 for speculative decoding. You can override this by explicitly setting --max-running-requests."
|
||||
)
|
||||
|
||||
_disable_overlap_schedule_for_cpu(server_args)
|
||||
|
||||
if resolved_view(server_args).disable_overlap_schedule:
|
||||
logger.warning(
|
||||
"Non-overlap (synchronous) spec v2 is used for eagle/eagle3/standalone "
|
||||
"speculative decoding."
|
||||
)
|
||||
|
||||
if server_args.enable_mixed_chunk:
|
||||
server_args.enable_mixed_chunk = False
|
||||
logger.warning(
|
||||
"Mixed chunked prefill is disabled because of using "
|
||||
"eagle speculative decoding."
|
||||
)
|
||||
|
||||
model_arch = server_args.get_model_config().hf_config.architectures[0]
|
||||
if model_arch in [
|
||||
"DeepseekV32ForCausalLM",
|
||||
"DeepseekV3ForCausalLM",
|
||||
"DeepseekV4ForCausalLM",
|
||||
"Glm4MoeForCausalLM",
|
||||
"Glm4MoeLiteForCausalLM",
|
||||
"GlmMoeDsaForCausalLM",
|
||||
"BailingMoeForCausalLM",
|
||||
"BailingMoeV2ForCausalLM",
|
||||
"BailingMoeV2_5ForCausalLM",
|
||||
"MistralLarge3ForCausalLM",
|
||||
"PixtralForConditionalGeneration",
|
||||
"HYV3ForCausalLM",
|
||||
]:
|
||||
if server_args.speculative_draft_model_path is None:
|
||||
server_args.speculative_draft_model_path = server_args.model_path
|
||||
server_args.speculative_draft_model_revision = server_args.revision
|
||||
else:
|
||||
if model_arch not in [
|
||||
"MistralLarge3ForCausalLM",
|
||||
"PixtralForConditionalGeneration",
|
||||
]:
|
||||
logger.warning(
|
||||
"DeepSeek MTP does not require setting speculative_draft_model_path."
|
||||
)
|
||||
|
||||
if (
|
||||
not server_args.speculative_adaptive
|
||||
and server_args.speculative_num_steps is None
|
||||
):
|
||||
assert (
|
||||
server_args.speculative_eagle_topk is None
|
||||
and server_args.speculative_num_draft_tokens is None
|
||||
)
|
||||
|
||||
(
|
||||
server_args.speculative_num_steps,
|
||||
server_args.speculative_eagle_topk,
|
||||
server_args.speculative_num_draft_tokens,
|
||||
) = _auto_choose_speculative_params(server_args, model_arch)
|
||||
|
||||
if "trtllm_mha" in attention_backends_of(resolved_view(server_args)):
|
||||
if server_args.speculative_eagle_topk > 1:
|
||||
raise ValueError(
|
||||
"trtllm_mha backend only supports topk = 1 for speculative decoding."
|
||||
)
|
||||
|
||||
if server_args.speculative_use_rejection_sampling:
|
||||
# Resolved alias by now: NEXTN -> EAGLE, Gemma4 draft -> FROZEN_KV_MTP.
|
||||
# Only the EAGLE/EAGLE3 draft workers emit a target-vocab proposal that
|
||||
# the rejection-sampling kernel consumes; everything else (STANDALONE,
|
||||
# FROZEN_KV_MTP, NGRAM, DFLASH) is unsupported.
|
||||
if server_args.speculative_algorithm not in ("EAGLE", "EAGLE3"):
|
||||
raise NotImplementedError(
|
||||
"--speculative-use-rejection-sampling is only supported for "
|
||||
"EAGLE / EAGLE3 / NEXTN, not "
|
||||
f"speculative_algorithm={server_args.speculative_algorithm}."
|
||||
)
|
||||
if server_args.speculative_eagle_topk != 1:
|
||||
raise ValueError(
|
||||
"--speculative-use-rejection-sampling requires --speculative-eagle-topk=1."
|
||||
)
|
||||
if (
|
||||
server_args.speculative_accept_threshold_single != 1.0
|
||||
or server_args.speculative_accept_threshold_acc != 1.0
|
||||
):
|
||||
raise ValueError(
|
||||
"--speculative-use-rejection-sampling is incompatible with "
|
||||
"--speculative-accept-threshold-single / "
|
||||
"--speculative-accept-threshold-acc; rejection sampling ignores "
|
||||
"the accept thresholds."
|
||||
)
|
||||
if server_args.enable_deterministic_inference:
|
||||
raise ValueError(
|
||||
"--speculative-use-rejection-sampling is incompatible with "
|
||||
"--enable-deterministic-inference; the sampling kernel draws "
|
||||
"coins from the global RNG and is not batch-invariant."
|
||||
)
|
||||
|
||||
from sglang.srt.arg_groups.overrides import resolved_view
|
||||
|
||||
if (
|
||||
resolved_view(server_args).enable_multi_layer_eagle
|
||||
and server_args.speculative_eagle_topk != 1
|
||||
):
|
||||
raise ValueError(
|
||||
"--speculative-use-rejection-sampling with multi-layer EAGLE "
|
||||
"(--enable-multi-layer-eagle) requires --speculative-eagle-topk 1; "
|
||||
"rejection sampling is only implemented for the linear (topk=1) chain."
|
||||
)
|
||||
logger.info(
|
||||
"Rejection sampling is enabled for speculative decoding "
|
||||
"(speculative_use_rejection_sampling=True)."
|
||||
)
|
||||
|
||||
if (
|
||||
server_args.speculative_eagle_topk == 1
|
||||
and server_args.speculative_num_draft_tokens
|
||||
!= server_args.speculative_num_steps + 1
|
||||
):
|
||||
logger.warning(
|
||||
"speculative_num_draft_tokens is adjusted to speculative_num_steps + 1 when speculative_eagle_topk == 1"
|
||||
)
|
||||
server_args.speculative_num_draft_tokens = server_args.speculative_num_steps + 1
|
||||
|
||||
# topk > 1 + page_size > 1 needs the two-pass cascade draft-decode (shared prefix
|
||||
# pass + per-branch expand pass with prefix-tail dup). Only these backends implement
|
||||
# it; flashmla / trtllm_mla / cutlass_mla can't express the per-branch tree, so reject.
|
||||
_PAGE_TREE_SPEC_BACKENDS = ("flashinfer", "fa3", "triton")
|
||||
view = resolved_view(server_args)
|
||||
if (
|
||||
server_args.speculative_eagle_topk > 1
|
||||
and view.page_size > 1
|
||||
and view.attention_backend not in _PAGE_TREE_SPEC_BACKENDS
|
||||
):
|
||||
raise ValueError(
|
||||
f"speculative_eagle_topk > 1 with page_size > 1 is only supported on "
|
||||
f"{_PAGE_TREE_SPEC_BACKENDS}; got attention_backend="
|
||||
f"{view.attention_backend!r}. Use page_size == 1 or one of those backends."
|
||||
)
|
||||
|
||||
|
||||
def _handle_ngram(server_args: ServerArgs) -> None:
|
||||
if server_args.device not in ("cuda", "cpu"):
|
||||
raise ValueError(
|
||||
"Ngram speculative decoding only supports CUDA or CPU devices."
|
||||
)
|
||||
|
||||
_disable_overlap_schedule_for_cpu(server_args)
|
||||
|
||||
if server_args.max_running_requests is None:
|
||||
server_args.max_running_requests = 48
|
||||
logger.warning(
|
||||
"Max running requests is reset to 48 for speculative decoding. You can override this by explicitly setting --max-running-requests."
|
||||
)
|
||||
|
||||
server_args.enable_mixed_chunk = False
|
||||
server_args.speculative_eagle_topk = server_args.speculative_ngram_max_bfs_breadth
|
||||
if server_args.speculative_num_draft_tokens is None:
|
||||
server_args.speculative_num_draft_tokens = 12
|
||||
logger.warning(
|
||||
"speculative_num_draft_tokens is set to 12 by default for ngram speculative decoding. "
|
||||
"You can override this by explicitly setting --speculative-num-draft-tokens."
|
||||
)
|
||||
if server_args.speculative_num_steps is None:
|
||||
server_args.speculative_num_steps = (
|
||||
server_args.speculative_num_draft_tokens
|
||||
// server_args.speculative_eagle_topk
|
||||
)
|
||||
if server_args.speculative_ngram_external_corpus_path is not None:
|
||||
if server_args.speculative_ngram_external_sam_budget <= 0:
|
||||
raise ValueError(
|
||||
"--speculative-ngram-external-sam-budget must be positive when "
|
||||
"--speculative-ngram-external-corpus-path is set."
|
||||
)
|
||||
if server_args.speculative_ngram_external_corpus_max_tokens <= 0:
|
||||
raise ValueError(
|
||||
"--speculative-ngram-external-corpus-max-tokens must be positive when "
|
||||
"--speculative-ngram-external-corpus-path is set."
|
||||
)
|
||||
if (
|
||||
server_args.speculative_ngram_external_sam_budget
|
||||
> server_args.speculative_num_draft_tokens - 1
|
||||
):
|
||||
raise ValueError(
|
||||
"speculative_ngram_external_sam_budget must be less than or equal to "
|
||||
f"speculative_num_draft_tokens - 1 ({server_args.speculative_num_draft_tokens - 1})."
|
||||
)
|
||||
logger.warning(
|
||||
"The mixed chunked prefill are disabled because of "
|
||||
"using ngram speculative decoding."
|
||||
)
|
||||
|
||||
from sglang.srt.arg_groups.overrides import resolved_view
|
||||
|
||||
view = resolved_view(server_args)
|
||||
if (
|
||||
server_args.speculative_eagle_topk > 1
|
||||
and view.page_size > 1
|
||||
and view.attention_backend != "flashinfer"
|
||||
):
|
||||
raise ValueError(
|
||||
f"speculative_eagle_topk({server_args.speculative_eagle_topk}) > 1 "
|
||||
f"with page_size({view.page_size}) > 1 is unstable "
|
||||
"and produces incorrect results for paged attention backends. "
|
||||
"This combination is only supported for the 'flashinfer' backend."
|
||||
)
|
||||
if view.enable_dp_attention:
|
||||
# TODO: support dp attention for ngram speculative decoding
|
||||
raise ValueError(
|
||||
"Currently ngram speculative decoding does not support dp attention."
|
||||
)
|
||||
|
||||
|
||||
def _maybe_disable_adaptive(server_args: ServerArgs) -> None:
|
||||
from sglang.srt.speculative.adaptive_spec_params import (
|
||||
adaptive_unsupported_reason,
|
||||
)
|
||||
|
||||
reason = adaptive_unsupported_reason(server_args)
|
||||
if reason is not None:
|
||||
logger.warning(
|
||||
f"speculative_adaptive disabled: {reason}. "
|
||||
"Falling back to static speculative params."
|
||||
)
|
||||
server_args.speculative_adaptive = False
|
||||
|
||||
|
||||
def _init_adaptive_speculative_params(server_args: ServerArgs) -> None:
|
||||
from sglang.srt.speculative.adaptive_spec_params import (
|
||||
resolve_candidate_steps_from_config,
|
||||
)
|
||||
|
||||
candidate_steps = resolve_candidate_steps_from_config(
|
||||
cfg_path=server_args.speculative_adaptive_config,
|
||||
)
|
||||
|
||||
if server_args.speculative_eagle_topk is None:
|
||||
server_args.speculative_eagle_topk = 1
|
||||
|
||||
if server_args.speculative_num_steps is None:
|
||||
server_args.speculative_num_steps = candidate_steps[len(candidate_steps) // 2]
|
||||
|
||||
if server_args.speculative_num_steps not in candidate_steps:
|
||||
raise ValueError(
|
||||
f"--speculative-num-steps={server_args.speculative_num_steps} "
|
||||
f"is not in the adaptive config candidate_steps {candidate_steps}. "
|
||||
"Pass one of those values."
|
||||
)
|
||||
|
||||
server_args.speculative_num_draft_tokens = server_args.speculative_num_steps + 1
|
||||
|
||||
|
||||
def _auto_choose_speculative_params(server_args: ServerArgs, model_arch: str) -> tuple:
|
||||
"""
|
||||
Automatically choose the parameters for speculative decoding.
|
||||
|
||||
You can tune them on your own models and prompts with scripts/playground/bench_speculative.py
|
||||
"""
|
||||
if server_args.speculative_algorithm == "STANDALONE":
|
||||
return (3, 1, 4)
|
||||
if model_arch in ["LlamaForCausalLM"]:
|
||||
return (5, 4, 8)
|
||||
elif model_arch in [
|
||||
"DeepseekV32ForCausalLM",
|
||||
"DeepseekV3ForCausalLM",
|
||||
"DeepseekV2ForCausalLM",
|
||||
"GptOssForCausalLM",
|
||||
"Glm4MoeForCausalLM",
|
||||
"Glm4MoeLiteForCausalLM",
|
||||
"GlmMoeDsaForCausalLM",
|
||||
"BailingMoeForCausalLM",
|
||||
"BailingMoeV2ForCausalLM",
|
||||
"BailingMoeV2_5ForCausalLM",
|
||||
"MistralLarge3ForCausalLM",
|
||||
"PixtralForConditionalGeneration",
|
||||
"MiMoV2ForCausalLM",
|
||||
"MiMoV2FlashForCausalLM",
|
||||
]:
|
||||
return (3, 1, 4)
|
||||
elif model_arch in ["Grok1ForCausalLM", "Grok1VForCausalLM"]:
|
||||
return (5, 4, 8)
|
||||
else:
|
||||
return (3, 1, 4)
|
||||
Reference in New Issue
Block a user