config: the resolution callbacks into the record go to zero (#36972)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Cheng Wan
2026-08-29 04:18:05 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 48b88e1256
commit b65e677e48
50 changed files with 1087 additions and 979 deletions
+15 -6
View File
@@ -29,8 +29,14 @@ logger = logging.getLogger(__name__)
def handle_attention_backend_compatibility(server_args: Any): def handle_attention_backend_compatibility(server_args: Any):
from sglang.srt.arg_groups.overrides import (
attention_backends_of,
model_config_of,
use_mla_backend,
)
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
model_config = server_args.get_model_config() model_config = model_config_of(server_args)
# The attention_backend write clusters of this handler moved to the # The attention_backend write clusters of this handler moved to the
# resolution pipeline (arg_groups/overrides.py), each invoked below at # resolution pipeline (arg_groups/overrides.py), each invoked below at
@@ -127,7 +133,7 @@ def handle_attention_backend_compatibility(server_args: Any):
run_post_process_pass(server_args, _cutedsl_prefill_backend_fill) run_post_process_pass(server_args, _cutedsl_prefill_backend_fill)
prefill_backend, decode_backend = server_args._resolved_attention_backends() prefill_backend, decode_backend = attention_backends_of(resolved_view(server_args))
if "trtllm_mha" in (prefill_backend, decode_backend): if "trtllm_mha" in (prefill_backend, decode_backend):
if prefill_backend == "trtllm_mha" and not ( if prefill_backend == "trtllm_mha" and not (
is_sm90_supported() or is_sm100_supported() or is_sm120_supported() is_sm90_supported() or is_sm100_supported() or is_sm120_supported()
@@ -184,8 +190,8 @@ def handle_attention_backend_compatibility(server_args: Any):
# Other platforms backends # Other platforms backends
run_post_process_pass(server_args, _attention_backend_platform_fallbacks) run_post_process_pass(server_args, _attention_backend_platform_fallbacks)
prefill_backend, decode_backend = server_args._resolved_attention_backends() prefill_backend, decode_backend = attention_backends_of(resolved_view(server_args))
if server_args.use_mla_backend() and prefill_backend == "intel_xpu": if use_mla_backend(server_args) and prefill_backend == "intel_xpu":
raise ValueError( raise ValueError(
"intel_xpu backend is only supported on decode for MLA models, please set --decode-attention-backend to intel_xpu and do not set --attention-backend or --prefill-attention-backend to intel_xpu for prefill instead use triton." "intel_xpu backend is only supported on decode for MLA models, please set --decode-attention-backend to intel_xpu and do not set --attention-backend or --prefill-attention-backend to intel_xpu for prefill instead use triton."
) )
@@ -451,6 +457,8 @@ def handle_multi_item_scoring(server_args: Any):
changing it silently could surprise users who intentionally picked changing it silently could surprise users who intentionally picked
a non-flashinfer backend. a non-flashinfer backend.
""" """
from sglang.srt.arg_groups.overrides import attention_backends_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if not cfg.enable_mis: if not cfg.enable_mis:
return return
@@ -488,7 +496,7 @@ def handle_multi_item_scoring(server_args: Any):
chunked_prefill_size=-1, chunked_prefill_size=-1,
) )
prefill_backend, decode_backend = server_args._resolved_attention_backends() prefill_backend, decode_backend = attention_backends_of(resolved_view(server_args))
assert prefill_backend == "flashinfer" and decode_backend == "flashinfer", ( assert prefill_backend == "flashinfer" and decode_backend == "flashinfer", (
"Multi-item scoring requires flashinfer attention backend for custom attention mask support. " "Multi-item scoring requires flashinfer attention backend for custom attention mask support. "
f"Please set --attention-backend flashinfer when using --enable-mis. " f"Please set --attention-backend flashinfer when using --enable-mis. "
@@ -497,6 +505,7 @@ def handle_multi_item_scoring(server_args: Any):
def handle_deterministic_inference(server_args: Any): def handle_deterministic_inference(server_args: Any):
from sglang.srt.arg_groups.overrides import model_config_of
from sglang.srt.server_args import ( from sglang.srt.server_args import (
RADIX_SUPPORTED_DETERMINISTIC_ATTENTION_BACKEND, RADIX_SUPPORTED_DETERMINISTIC_ATTENTION_BACKEND,
) )
@@ -549,7 +558,7 @@ def handle_deterministic_inference(server_args: Any):
is_deepseek_model = False is_deepseek_model = False
if parse_connector_type(cfg.model_path) != ConnectorType.INSTANCE: if parse_connector_type(cfg.model_path) != ConnectorType.INSTANCE:
try: try:
hf_config = server_args.get_model_config().hf_config hf_config = model_config_of(server_args).hf_config
model_arch = hf_config.architectures[0] model_arch = hf_config.architectures[0]
is_deepseek_model = model_arch in [ is_deepseek_model = model_arch in [
"DeepseekV2ForCausalLM", "DeepseekV2ForCausalLM",
+132 -15
View File
@@ -110,6 +110,8 @@ def apply_cuda_graph_compatibility(server_args: Any):
prefill backend (this folds in the old prefill backend (this folds in the old
--enforce-piecewise-cuda-graph contract). --enforce-piecewise-cuda-graph contract).
""" """
from sglang.srt.arg_groups.overrides import attention_backends_of, model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if (Phase.PREFILL, "backend") in server_args._cuda_graph_config_locked: if (Phase.PREFILL, "backend") in server_args._cuda_graph_config_locked:
return return
@@ -120,11 +122,13 @@ def apply_cuda_graph_compatibility(server_args: Any):
# this runs first, so piecewise would otherwise silently win. # this runs first, so piecewise would otherwise silently win.
if ( if (
cfg.cuda_graph_config.prefill.backend == Backend.BREAKABLE cfg.cuda_graph_config.prefill.backend == Backend.BREAKABLE
and server_args.get_model_config().is_multimodal_piecewise_cuda_graph_supported and model_config_of(server_args).is_multimodal_piecewise_cuda_graph_supported
and not server_args.get_model_config().is_multimodal_breakable_cuda_graph_supported and not model_config_of(
server_args
).is_multimodal_breakable_cuda_graph_supported
# Keep trtllm_mla on the preferred breakable path, which now serves # Keep trtllm_mla on the preferred breakable path, which now serves
# MLA by falling back to the flashinfer MLA impl for extend. # MLA by falling back to the flashinfer MLA impl for extend.
and server_args._resolved_attention_backends()[0] != "trtllm_mla" and attention_backends_of(resolved_view(server_args))[0] != "trtllm_mla"
): ):
logger.info( logger.info(
"Using tc_piecewise CUDA graph for validated multimodal " "decoder prefill." "Using tc_piecewise CUDA graph for validated multimodal " "decoder prefill."
@@ -149,12 +153,14 @@ def disable_tc_piecewise_cudagraph_if_incompatible(server_args: Any):
"""TcPiecewise (torch.compile + piecewise) is incompatible with """TcPiecewise (torch.compile + piecewise) is incompatible with
these configurations. Most are torch.compile / dynamo limitations. these configurations. Most are torch.compile / dynamo limitations.
""" """
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
rules = [ rules = [
( (
"model-arch blacklist", "model-arch blacklist",
lambda: server_args.get_model_config().is_piecewise_cuda_graph_disabled_model, lambda: model_config_of(server_args).is_piecewise_cuda_graph_disabled_model,
), ),
("DP attention", lambda: resolved_view(server_args).enable_dp_attention), ("DP attention", lambda: resolved_view(server_args).enable_dp_attention),
("full torch.compile mode", lambda: cfg.enable_torch_compile), ("full torch.compile mode", lambda: cfg.enable_torch_compile),
@@ -177,8 +183,10 @@ def disable_tc_piecewise_cudagraph_if_incompatible(server_args: Any):
("LoRA", lambda: bool(cfg.lora_paths) or cfg.enable_lora), ("LoRA", lambda: bool(cfg.lora_paths) or cfg.enable_lora),
( (
"multimodal model", "multimodal model",
lambda: server_args.get_model_config().is_multimodal lambda: model_config_of(server_args).is_multimodal
and not server_args.get_model_config().is_multimodal_piecewise_cuda_graph_supported, and not model_config_of(
server_args
).is_multimodal_piecewise_cuda_graph_supported,
), ),
( (
"GGUF quantization", "GGUF quantization",
@@ -236,6 +244,8 @@ def disable_breakable_cudagraph_if_incompatible(server_args: Any):
memory-saver rejection in its own __init__; config-time rules can be memory-saver rejection in its own __init__; config-time rules can be
added here as they're discovered. added here as they're discovered.
""" """
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
from sglang.srt.configs.model_config import is_deepseek_v4 from sglang.srt.configs.model_config import is_deepseek_v4
from sglang.srt.layers.cp.bcg import supports_prefill_cp_bcg from sglang.srt.layers.cp.bcg import supports_prefill_cp_bcg
@@ -245,7 +255,7 @@ def disable_breakable_cudagraph_if_incompatible(server_args: Any):
# c4 indexer scratch is pinned in the capture pool and OOMs. Disable. # c4 indexer scratch is pinned in the capture pool and OOMs. Disable.
( (
"DeepSeek-V4 (heavy capture-pool memory pressure)", "DeepSeek-V4 (heavy capture-pool memory pressure)",
lambda: is_deepseek_v4(server_args.get_model_config().hf_config), lambda: is_deepseek_v4(model_config_of(server_args).hf_config),
), ),
# CP all_gather replay size mismatch under BCG. # CP all_gather replay size mismatch under BCG.
( (
@@ -271,8 +281,10 @@ def disable_breakable_cudagraph_if_incompatible(server_args: Any):
# Multimodal prefill replay faults under BCG; allowlisted archs opt back in. # Multimodal prefill replay faults under BCG; allowlisted archs opt back in.
( (
"multimodal model", "multimodal model",
lambda: server_args.get_model_config().is_multimodal lambda: model_config_of(server_args).is_multimodal
and not server_args.get_model_config().is_multimodal_breakable_cuda_graph_supported, and not model_config_of(
server_args
).is_multimodal_breakable_cuda_graph_supported,
), ),
] ]
for name, predicate in rules: for name, predicate in rules:
@@ -319,6 +331,8 @@ def disable_prefill_cuda_graph_for_deepseek_trtllm_mla(server_args: Any):
breakable) trtllm_mla falls back to FlashAttention for prefill and regresses breakable) trtllm_mla falls back to FlashAttention for prefill and regresses
performance, so disable whichever prefill graph backend is in effect. performance, so disable whichever prefill graph backend is in effect.
""" """
from sglang.srt.arg_groups.overrides import attention_backends_of, model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if (Phase.PREFILL, "backend") in server_args._cuda_graph_config_locked: if (Phase.PREFILL, "backend") in server_args._cuda_graph_config_locked:
@@ -327,10 +341,10 @@ def disable_prefill_cuda_graph_for_deepseek_trtllm_mla(server_args: Any):
return return
if ( if (
"DeepseekV3ForCausalLM" "DeepseekV3ForCausalLM"
not in server_args.get_model_config().hf_config.architectures not in model_config_of(server_args).hf_config.architectures
): ):
return return
prefill_attention_backend, _ = server_args._resolved_attention_backends() prefill_attention_backend, _ = attention_backends_of(resolved_view(server_args))
if prefill_attention_backend != "trtllm_mla": if prefill_attention_backend != "trtllm_mla":
return return
logger.warning( logger.warning(
@@ -362,7 +376,7 @@ def apply_deepep_adjustments(server_args: Any):
if bs is None: if bs is None:
# 2048 = documented prefill default; max_bs unresolved here. # 2048 = documented prefill default; max_bs unresolved here.
max_bs = cfg.cuda_graph_config.prefill.max_bs or 2048 max_bs = cfg.cuda_graph_config.prefill.max_bs or 2048
bs = server_args._generate_prefill_cuda_graph_batch_sizes(max_bs) bs = generate_prefill_cuda_graph_batch_sizes(server_args, max_bs)
aligned = sorted({((b + 7) // 8) * 8 for b in bs}) aligned = sorted({((b + 7) // 8) * 8 for b in bs})
if aligned != sorted(bs): if aligned != sorted(bs):
logger.info( logger.info(
@@ -389,6 +403,8 @@ def apply_inkling_prefill_cuda_graph_default(server_args: Any):
auto-disabled for this multimodal arch, and declarative model overrides auto-disabled for this multimodal arch, and declarative model overrides
materialize too late to steer cuda-graph resolution. Honors an explicit materialize too late to steer cuda-graph resolution. Honors an explicit
--cuda-graph-backend-prefill / --disable-prefill-cuda-graph.""" --cuda-graph-backend-prefill / --disable-prefill-cuda-graph."""
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if ( if (
cfg.cuda_graph_backend_prefill is not None cfg.cuda_graph_backend_prefill is not None
@@ -396,7 +412,7 @@ def apply_inkling_prefill_cuda_graph_default(server_args: Any):
or parse_connector_type(cfg.model_path) == ConnectorType.INSTANCE or parse_connector_type(cfg.model_path) == ConnectorType.INSTANCE
): ):
return return
arch = server_args.get_model_config().hf_config.architectures[0] arch = model_config_of(server_args).hf_config.architectures[0]
if arch in ( if arch in (
"InklingForConditionalGeneration", "InklingForConditionalGeneration",
"InklingForConditionalGenerationMTP", "InklingForConditionalGenerationMTP",
@@ -409,13 +425,15 @@ def apply_inkling_prefill_cuda_graph_default(server_args: Any):
def apply_muse_glimmer_prefill_cuda_graph_max_bs_default(server_args: Any): def apply_muse_glimmer_prefill_cuda_graph_max_bs_default(server_args: Any):
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if ( if (
cfg.cuda_graph_max_bs_prefill is not None cfg.cuda_graph_max_bs_prefill is not None
or parse_connector_type(cfg.model_path) == ConnectorType.INSTANCE or parse_connector_type(cfg.model_path) == ConnectorType.INSTANCE
): ):
return return
arch = server_args.get_model_config().hf_config.architectures[0] arch = model_config_of(server_args).hf_config.architectures[0]
if arch in ("MuseGlimmerForCausalLM", "MuseGlimmerForConditionalGeneration"): if arch in ("MuseGlimmerForCausalLM", "MuseGlimmerForConditionalGeneration"):
declare_resolution( declare_resolution(
server_args, server_args,
@@ -430,7 +448,7 @@ def handle_cuda_graph_config(server_args: Any):
parse_cuda_graph_config(server_args) parse_cuda_graph_config(server_args)
apply_cuda_graph_compatibility(server_args) apply_cuda_graph_compatibility(server_args)
apply_deepep_adjustments(server_args) apply_deepep_adjustments(server_args)
server_args._apply_cuda_graph_disaggregation_roles() apply_cuda_graph_disaggregation_roles(server_args)
validate_cuda_graph_config(server_args) validate_cuda_graph_config(server_args)
# Warn on the final resolved config (not inside the compat cascade — # Warn on the final resolved config (not inside the compat cascade —
# that path is skipped when the user explicitly sets the backend, # that path is skipped when the user explicitly sets the backend,
@@ -453,3 +471,102 @@ def validate_cuda_graph_config(server_args: Any):
f"--cuda-graph-config[{phase}].backend={backend!r} not allowed; " f"--cuda-graph-config[{phase}].backend={backend!r} not allowed; "
f"allowed: {ALLOWED_BACKENDS_PER_PHASE[phase]}" f"allowed: {ALLOWED_BACKENDS_PER_PHASE[phase]}"
) )
def generate_prefill_cuda_graph_batch_sizes(server_args: Any, max_bs: int):
"""
Generate the list of batch sizes for prefill CUDA graph capture
based on max_bs. For tc_piecewise prefill, bs carries the
captured token count (one shape knob per phase).
"""
capture_sizes = (
list(range(4, 33, 4))
+ list(range(48, 257, 16))
+ list(range(288, 513, 32))
+ list(range(576, 1024 + 1, 64))
+ list(range(1280, 4096 + 1, 256))
+ list(range(4608, max_bs + 1, 512))
)
capture_sizes = [s for s in capture_sizes if s <= max_bs]
return capture_sizes
def generate_decode_cuda_graph_batch_sizes(server_args: Any, max_bs: int):
"""
Generate the list of batch sizes for CUDA graph capture based on max_bs.
This integrates the logic from cuda_graph_runner.py.
"""
cfg = resolving_view(server_args)
# Handle disable_cuda_graph_padding as the first condition for both spec and non-spec
if cfg.disable_cuda_graph_padding:
capture_bs = list(range(1, max_bs + 1))
elif cfg.speculative_algorithm is None:
# Normal case:
capture_bs = (
[1, 2, 4, 8, 12]
+ list(range(16, 257, 8))
+ list(range(272, 512, 16))
+ list(range(512, max_bs + 1, 32))
)
else:
# Spec decoding case: less padding for smaller batch sizes
capture_bs = (
list(range(1, 9, 1))
+ list(range(10, 33, 2))
+ list(range(40, 65, 4))
+ list(range(72, 257, 8))
+ list(range(272, max_bs + 1, 16))
)
capture_bs = [bs for bs in capture_bs if bs <= max_bs]
if max_bs not in capture_bs:
capture_bs.append(max_bs)
return capture_bs
def generate_cpu_graph_batch_sizes(server_args: Any):
"""
Generate the list of batch sizes for CPU graph capture based on torch_compile_max_bs.
"""
cfg = resolving_view(server_args)
if cfg.disable_cuda_graph_padding:
capture_bs = list(range(1, cfg.torch_compile_max_bs + 1))
else:
capture_bs = sorted(
set().union(
range(1, 17),
range(18, 31, 2),
range(32, 81, 4),
range(84, cfg.torch_compile_max_bs + 1, 8),
{cfg.torch_compile_max_bs},
)
)
capture_bs = [bs for bs in capture_bs if bs <= cfg.torch_compile_max_bs]
return capture_bs
def apply_cuda_graph_disaggregation_roles(server_args: Any):
cfg = resolving_view(server_args)
if cfg.disaggregation_mode == "prefill":
if (Phase.DECODE, "backend") not in server_args._cuda_graph_config_locked:
declare_resolution(
server_args,
"_apply_cuda_graph_disaggregation_roles",
cuda_graph_config=with_phase(
cfg.cuda_graph_config, Phase.DECODE, backend=Backend.DISABLED
),
)
elif cfg.disaggregation_mode == "decode":
if (Phase.PREFILL, "backend") not in server_args._cuda_graph_config_locked:
declare_resolution(
server_args,
"_apply_cuda_graph_disaggregation_roles",
cuda_graph_config=with_phase(
cfg.cuda_graph_config, Phase.PREFILL, backend=Backend.DISABLED
),
)
@@ -27,6 +27,8 @@ logger = logging.getLogger(__name__)
def handle_expert_pack(server_args: Any) -> None: def handle_expert_pack(server_args: Any) -> None:
"""Normalize expert-pack settings and report all startup errors together.""" """Normalize expert-pack settings and report all startup errors together."""
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if cfg.load_format != "expert_pack": if cfg.load_format != "expert_pack":
return return
@@ -146,7 +148,7 @@ def handle_expert_pack(server_args: Any) -> None:
) )
else: else:
try: try:
hf_config = server_args.get_model_config().hf_config hf_config = model_config_of(server_args).hf_config
except Exception as exc: except Exception as exc:
errors.append(f"failed to load expert_pack model config: {exc}") errors.append(f"failed to load expert_pack model config: {exc}")
else: else:
+3 -1
View File
@@ -68,6 +68,8 @@ def handle_hicache_ratio_default(server_args: Any):
def resolve_hicache_dcp_compatibility(server_args: Any): def resolve_hicache_dcp_compatibility(server_args: Any):
from sglang.srt.arg_groups.overrides import use_mla_backend
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if cfg.dcp_size <= 1 or not cfg.enable_hierarchical_cache: if cfg.dcp_size <= 1 or not cfg.enable_hierarchical_cache:
return return
@@ -95,7 +97,7 @@ def resolve_hicache_dcp_compatibility(server_args: Any):
"--enable-hisparse with --dcp-size > 1 is not supported: the " "--enable-hisparse with --dcp-size > 1 is not supported: the "
"HiSparse host pool is constructed without DCP translation." "HiSparse host pool is constructed without DCP translation."
) )
if not server_args.use_mla_backend(): if not use_mla_backend(server_args):
raise NotImplementedError( raise NotImplementedError(
"HiCache with --dcp-size > 1 is only supported for MLA models: " "HiCache with --dcp-size > 1 is only supported for MLA models: "
"the index translation lives in MLATokenToKVPoolHost, and the " "the index translation lives in MLATokenToKVPoolHost, and the "
@@ -19,7 +19,8 @@ HISPARSE_KV_CACHE_DTYPES = ("bfloat16", "fp8_e4m3")
def _is_hip() -> bool: def _is_hip() -> bool:
from sglang.srt.server_args import is_hip """The one place this family asks about ROCm, and the seam the tests patch."""
from sglang.srt.utils.common import is_hip
return is_hip() return is_hip()
@@ -81,6 +82,8 @@ def validate_hisparse_kv_cache_dtype(server_args: ServerArgs) -> None:
def validate_hisparse(server_args: ServerArgs) -> None: def validate_hisparse(server_args: ServerArgs) -> None:
"""Validate --enable-hisparse constraints (model class, radix cache, DSA backend).""" """Validate --enable-hisparse constraints (model class, radix cache, DSA backend)."""
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if not cfg.enable_hisparse: if not cfg.enable_hisparse:
return return
@@ -90,7 +93,7 @@ def validate_hisparse(server_args: ServerArgs) -> None:
is_deepseek_v4, is_deepseek_v4,
) )
hf_config = server_args.get_model_config().hf_config hf_config = model_config_of(server_args).hf_config
is_v4_hisparse = is_deepseek_v4(hf_config) is_v4_hisparse = is_deepseek_v4(hf_config)
is_hip = _is_hip() is_hip = _is_hip()
assert is_deepseek_dsa(hf_config) or is_v4_hisparse, ( assert is_deepseek_dsa(hf_config) or is_v4_hisparse, (
+19 -11
View File
@@ -37,13 +37,15 @@ def handle_mxfp8_kv_cache_compatibility(server_args: Any) -> None:
def handle_kv4_compatibility(server_args: Any) -> None: def handle_kv4_compatibility(server_args: Any) -> None:
"""Check FP4 KV cache compatibility with the attention backend""" """Check FP4 KV cache compatibility with the attention backend"""
from sglang.srt.arg_groups.overrides import attention_backends_of, use_mla_backend
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if cfg.kv_cache_dtype not in ("nvfp4", "fp4_mx_block16"): if cfg.kv_cache_dtype not in ("nvfp4", "fp4_mx_block16"):
return return
use_mla_backend = server_args.use_mla_backend() uses_mla = use_mla_backend(server_args)
prefill_backend, decode_backend = server_args._resolved_attention_backends() prefill_backend, decode_backend = attention_backends_of(resolved_view(server_args))
attention_backend = resolved_view(server_args).attention_backend attention_backend = resolved_view(server_args).attention_backend
if is_cuda(): if is_cuda():
@@ -64,7 +66,7 @@ def handle_kv4_compatibility(server_args: Any) -> None:
) )
else: else:
if prefill_backend == "fa4": if prefill_backend == "fa4":
if use_mla_backend: # FA4 + MLA if uses_mla: # FA4 + MLA
KV4_FA4_MLA_BACKEND_CHOICES = [ KV4_FA4_MLA_BACKEND_CHOICES = [
"cutlass_mla", "cutlass_mla",
"flashinfer", "flashinfer",
@@ -85,7 +87,7 @@ def handle_kv4_compatibility(server_args: Any) -> None:
f"{KV4_FA4_MHA_BACKEND_CHOICES}, but got {decode_backend}" f"{KV4_FA4_MHA_BACKEND_CHOICES}, but got {decode_backend}"
) )
else: else:
if use_mla_backend: # !FA4 + MLA if uses_mla: # !FA4 + MLA
KV4_ATTENTION_MLA_BACKEND_CHOICES = [ KV4_ATTENTION_MLA_BACKEND_CHOICES = [
"cutlass_mla", "cutlass_mla",
"flashinfer", "flashinfer",
@@ -120,6 +122,8 @@ def handle_prefill_only_disable_kv_cache(server_args: Any) -> None:
still None, backends haven't settled yet and the resolved (prefill, still None, backends haven't settled yet and the resolved (prefill,
decode) pair would be a stale (None, None). decode) pair would be a stale (None, None).
""" """
from sglang.srt.arg_groups.overrides import attention_backends_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if not cfg.prefill_only_disable_kv_cache: if not cfg.prefill_only_disable_kv_cache:
@@ -130,7 +134,7 @@ def handle_prefill_only_disable_kv_cache(server_args: Any) -> None:
"_handle_attention_backend_compatibility() so the prefill backend is resolved." "_handle_attention_backend_compatibility() so the prefill backend is resolved."
) )
prefill_backend, _ = server_args._resolved_attention_backends() prefill_backend, _ = attention_backends_of(resolved_view(server_args))
if prefill_backend not in ("fa3", "fa4"): if prefill_backend not in ("fa3", "fa4"):
raise ValueError( raise ValueError(
"--prefill-only-disable-kv-cache currently requires the FA prefill backend " "--prefill-only-disable-kv-cache currently requires the FA prefill backend "
@@ -194,6 +198,8 @@ def handle_cache_compatibility(server_args: Any) -> None:
def handle_unified_memory_pool(server_args: Any) -> None: def handle_unified_memory_pool(server_args: Any) -> None:
from sglang.srt.arg_groups.overrides import attention_backends_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if not cfg.enable_unified_memory: if not cfg.enable_unified_memory:
return return
@@ -236,7 +242,7 @@ def handle_unified_memory_pool(server_args: Any) -> None:
# Both roles: verify routes to either backend depending on # Both roles: verify routes to either backend depending on
# --speculative-attention-mode. # --speculative-attention-mode.
spec_allowed = {"triton", "trtllm_mla", "cutedsl_mla", "tokenspeed_mla"} spec_allowed = {"triton", "trtllm_mla", "cutedsl_mla", "tokenspeed_mla"}
spec_backends = set(server_args._resolved_attention_backends()) spec_backends = set(attention_backends_of(resolved_view(server_args)))
spec_backends.discard(None) spec_backends.discard(None)
assert spec_backends <= spec_allowed, ( assert spec_backends <= spec_allowed, (
"--enable-unified-memory + DSPARK requires spec-verify-audited " "--enable-unified-memory + DSPARK requires spec-verify-audited "
@@ -273,6 +279,8 @@ def handle_page_major_kv_layout(server_args: Any):
# The unified pool stores state in the page-major envelope-strided layout, so # The unified pool stores state in the page-major envelope-strided layout, so
# enabling it implies --enable-page-major-kv-layout — routing it through the # enabling it implies --enable-page-major-kv-layout — routing it through the
# single page-major path + stride-aware Triton asserts (set before the guard). # single page-major path + stride-aware Triton asserts (set before the guard).
from sglang.srt.arg_groups.overrides import attention_backends_of, use_mla_backend
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if cfg.enable_unified_memory: if cfg.enable_unified_memory:
declare_resolution( declare_resolution(
@@ -287,14 +295,14 @@ def handle_page_major_kv_layout(server_args: Any):
# exposes each layer as a DENSE contiguous per-layer view # exposes each layer as a DENSE contiguous per-layer view
# (build_dense_mla_views), which the paged MLA kernels consume directly, # (build_dense_mla_views), which the paged MLA kernels consume directly,
# with their kv_indices / block tables remapped to dense ids. Names below # with their kv_indices / block tables remapped to dense ids. Names below
# are the RESOLVED ids from _resolved_attention_backends: "flashinfer" is # are the RESOLVED ids from attention_backends_of: "flashinfer" is
# FlashInferMLAAttnBackend for an MLA model, "trtllm_mla" the trtllm # FlashInferMLAAttnBackend for an MLA model, "trtllm_mla" the trtllm
# decode kernel; "cutedsl_mla" and "tokenspeed_mla" subclass # decode kernel; "cutedsl_mla" and "tokenspeed_mla" subclass
# TRTLLMMLABackend and inherit its dense read/write path; "fa3" remaps its # TRTLLMMLABackend and inherit its dense read/write path; "fa3" remaps its
# page_table (in-kernel for captured decode, one funnel for eager). # page_table (in-kernel for captured decode, one funnel for eager).
# flashmla / cutlass_mla share the create_flashmla block-table path and # flashmla / cutlass_mla share the create_flashmla block-table path and
# can be added the same way once exercised. # can be added the same way once exercised.
if cfg.enable_unified_memory and server_args.use_mla_backend(): if cfg.enable_unified_memory and use_mla_backend(server_args):
allowed_full = { allowed_full = {
"triton", "triton",
"fa3", "fa3",
@@ -305,7 +313,7 @@ def handle_page_major_kv_layout(server_args: Any):
} }
else: else:
allowed_full = {"triton"} allowed_full = {"triton"}
backends = set(server_args._resolved_attention_backends()) backends = set(attention_backends_of(resolved_view(server_args)))
backends.discard(None) backends.discard(None)
assert backends <= allowed_full, ( assert backends <= allowed_full, (
"--enable-page-major-kv-layout requires the Triton attention backend " "--enable-page-major-kv-layout requires the Triton attention backend "
@@ -327,7 +335,7 @@ def handle_page_major_kv_layout(server_args: Any):
# are MLA-hybrid) from GDN models (GQA-hybrid) for the KDA-only caveat. # are MLA-hybrid) from GDN models (GQA-hybrid) for the KDA-only caveat.
decode_allowed = {"triton", "flashinfer"} decode_allowed = {"triton", "flashinfer"}
prefill_allowed = {"triton", "flashkda"} prefill_allowed = {"triton", "flashkda"}
if server_args.use_mla_backend(): if use_mla_backend(server_args):
decode_allowed.update({"cutedsl", "helion"}) decode_allowed.update({"cutedsl", "helion"})
prefill_allowed.update({"cutedsl", "helion"}) prefill_allowed.update({"cutedsl", "helion"})
resolved_linear_decode = cfg.linear_attn_decode_backend or cfg.linear_attn_backend resolved_linear_decode = cfg.linear_attn_decode_backend or cfg.linear_attn_backend
@@ -403,7 +411,7 @@ def validate_prefill_only_disable_kv_cache_args(server_args: Any):
# Context-parallel prefill stages K/V through cp_allgather_and_save_kv_cache, # Context-parallel prefill stages K/V through cp_allgather_and_save_kv_cache,
# which writes to the pool via set_kv_buffer. NoOpMHATokenToKVPool intentionally # which writes to the pool via set_kv_buffer. NoOpMHATokenToKVPool intentionally
# raises on writes, so the engine would boot fine but fail on the first request. # raises on writes, so the engine would boot fine but fail on the first request.
if server_args._resolved().attn_cp_size > 1: if resolved_view(server_args).attn_cp_size > 1:
raise ValueError( raise ValueError(
"--prefill-only-disable-kv-cache is incompatible with --attn-cp-size > 1: " "--prefill-only-disable-kv-cache is incompatible with --attn-cp-size > 1: "
"the context-parallel attention path writes K/V to the pool via set_kv_buffer, " "the context-parallel attention path writes K/V to the pool via set_kv_buffer, "
+15 -8
View File
@@ -7,6 +7,7 @@ import logging
from typing import Any from typing import Any
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
declare_late_resolution,
resolving_view, resolving_view,
) )
from sglang.srt.environ import envs from sglang.srt.environ import envs
@@ -23,7 +24,9 @@ def check_lora_server_args(server_args: Any):
# Enable LoRA if any LoRA paths are provided for backward compatibility. # Enable LoRA if any LoRA paths are provided for backward compatibility.
if cfg.lora_paths: if cfg.lora_paths:
if cfg.enable_lora is None: if cfg.enable_lora is None:
server_args._late_resolution("check_lora_server_args", enable_lora=True) declare_late_resolution(
server_args, "check_lora_server_args", enable_lora=True
)
logger.warning( logger.warning(
"--enable-lora is set to True because --lora-paths is provided." "--enable-lora is set to True because --lora-paths is provided."
) )
@@ -34,8 +37,8 @@ def check_lora_server_args(server_args: Any):
if cfg.enable_lora: if cfg.enable_lora:
if cfg.enable_lora_overlap_loading is None: if cfg.enable_lora_overlap_loading is None:
server_args._late_resolution( declare_late_resolution(
"check_lora_server_args", enable_lora_overlap_loading=False server_args, "check_lora_server_args", enable_lora_overlap_loading=False
) )
if cfg.enable_lora_overlap_loading: if cfg.enable_lora_overlap_loading:
@@ -90,11 +93,12 @@ def check_lora_server_args(server_args: Any):
"Expected a string or a dictionary." "Expected a string or a dictionary."
) )
parsed_lora_paths.append(lora_ref) parsed_lora_paths.append(lora_ref)
server_args._late_resolution( declare_late_resolution(
"check_lora_server_args", lora_paths=parsed_lora_paths server_args, "check_lora_server_args", lora_paths=parsed_lora_paths
) )
elif isinstance(cfg.lora_paths, dict): elif isinstance(cfg.lora_paths, dict):
server_args._late_resolution( declare_late_resolution(
server_args,
"check_lora_server_args", "check_lora_server_args",
lora_paths=[ lora_paths=[
LoRARef( LoRARef(
@@ -107,7 +111,9 @@ def check_lora_server_args(server_args: Any):
], ],
) )
elif cfg.lora_paths is None: elif cfg.lora_paths is None:
server_args._late_resolution("check_lora_server_args", lora_paths=[]) declare_late_resolution(
server_args, "check_lora_server_args", lora_paths=[]
)
else: else:
raise ValueError( raise ValueError(
f"Invalid type for --lora-paths: {type(cfg.lora_paths)}. " f"Invalid type for --lora-paths: {type(cfg.lora_paths)}. "
@@ -117,7 +123,8 @@ def check_lora_server_args(server_args: Any):
# Normalize target modules to a set; keep {"all"} as a sentinel # Normalize target modules to a set; keep {"all"} as a sentinel
# that gets resolved model-awarely in lora_manager.init_lora_shapes(). # that gets resolved model-awarely in lora_manager.init_lora_shapes().
if cfg.lora_target_modules: if cfg.lora_target_modules:
server_args._late_resolution( declare_late_resolution(
server_args,
"check_lora_server_args", "check_lora_server_args",
lora_target_modules=set(cfg.lora_target_modules), lora_target_modules=set(cfg.lora_target_modules),
) )
+125 -15
View File
@@ -9,9 +9,11 @@ from typing import Any
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
declare_resolution, declare_resolution,
resolved_view,
resolving_view, resolving_view,
) )
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.model_executor.cuda_graph_config import Backend
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -40,6 +42,17 @@ def handle_gpu_memory_settings(server_args: Any, gpu_mem):
The coefficient 1.5 is a heuristic value, in the future, we can do better estimation by looking at the model types, hidden sizes or even do a dummy run. The coefficient 1.5 is a heuristic value, in the future, we can do better estimation by looking at the model types, hidden sizes or even do a dummy run.
""" """
from sglang.srt.arg_groups.cuda_graph_hook import (
generate_cpu_graph_batch_sizes,
generate_decode_cuda_graph_batch_sizes,
generate_prefill_cuda_graph_batch_sizes,
)
from sglang.srt.arg_groups.overrides import (
model_config_of,
post_capture_kv_sizing_planned,
use_mla_backend,
)
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
# A copy, so an earlier declaration keeps the value it recorded. # A copy, so an earlier declaration keeps the value it recorded.
cuda_graph_config = copy.deepcopy(cfg.cuda_graph_config) cuda_graph_config = copy.deepcopy(cfg.cuda_graph_config)
@@ -139,10 +152,8 @@ def handle_gpu_memory_settings(server_args: Any, gpu_mem):
# Set cuda graph batch sizes # Set cuda graph batch sizes
if cfg.device != "cpu": if cfg.device != "cpu":
if decode_cuda_graph_config.bs is None: if decode_cuda_graph_config.bs is None:
decode_cuda_graph_config.bs = ( decode_cuda_graph_config.bs = generate_decode_cuda_graph_batch_sizes(
server_args._generate_decode_cuda_graph_batch_sizes( server_args, decode_cuda_graph_config.max_bs
decode_cuda_graph_config.max_bs
)
) )
else: else:
decode_cuda_graph_config.max_bs = max(decode_cuda_graph_config.bs) decode_cuda_graph_config.max_bs = max(decode_cuda_graph_config.bs)
@@ -164,7 +175,7 @@ def handle_gpu_memory_settings(server_args: Any, gpu_mem):
torch_compile_max_bs=cfg.torch_compile_max_bs torch_compile_max_bs=cfg.torch_compile_max_bs
or decode_cuda_graph_config.max_bs, or decode_cuda_graph_config.max_bs,
) )
decode_cuda_graph_config.bs = server_args._generate_cpu_graph_batch_sizes() decode_cuda_graph_config.bs = generate_cpu_graph_batch_sizes(server_args)
assert ( assert (
cfg.torch_compile_max_bs > 0 cfg.torch_compile_max_bs > 0
@@ -175,7 +186,7 @@ def handle_gpu_memory_settings(server_args: Any, gpu_mem):
# Refer to pr #15927, by default we set the prefill max_bs to the chunked prefill size. # Refer to pr #15927, by default we set the prefill max_bs to the chunked prefill size.
# For MLA backend, the introduction of piecewise cuda graph will influence the kernel dispatch difference compared to the original mode. # For MLA backend, the introduction of piecewise cuda graph will influence the kernel dispatch difference compared to the original mode.
# To avoid the performance regression, we set max_bs to 2048 by default. # To avoid the performance regression, we set max_bs to 2048 by default.
if not server_args.use_mla_backend(): if not use_mla_backend(server_args):
prefill_cuda_graph_config.max_bs = cfg.chunked_prefill_size prefill_cuda_graph_config.max_bs = cfg.chunked_prefill_size
else: else:
prefill_cuda_graph_config.max_bs = 2048 prefill_cuda_graph_config.max_bs = 2048
@@ -194,10 +205,8 @@ def handle_gpu_memory_settings(server_args: Any, gpu_mem):
) )
if prefill_cuda_graph_config.bs is None: if prefill_cuda_graph_config.bs is None:
prefill_cuda_graph_config.bs = ( prefill_cuda_graph_config.bs = generate_prefill_cuda_graph_batch_sizes(
server_args._generate_prefill_cuda_graph_batch_sizes( server_args, prefill_cuda_graph_config.max_bs
prefill_cuda_graph_config.max_bs
)
) )
if cuda_graph_config != cfg.cuda_graph_config: if cuda_graph_config != cfg.cuda_graph_config:
@@ -208,7 +217,7 @@ def handle_gpu_memory_settings(server_args: Any, gpu_mem):
) )
if cfg.mem_fraction_static is None: if cfg.mem_fraction_static is None:
if server_args.post_capture_kv_sizing_planned(): if post_capture_kv_sizing_planned(server_args):
# Post-capture sizing measures free memory after graph capture, so # Post-capture sizing measures free memory after graph capture, so
# skip the graph/activation reserve; keep only the floor + parallel slack. # skip the graph/activation reserve; keep only the floor + parallel slack.
reserved_mem = 1536 reserved_mem = 1536
@@ -230,11 +239,11 @@ def handle_gpu_memory_settings(server_args: Any, gpu_mem):
reserved_mem += activation_tokens * 1.5 reserved_mem += activation_tokens * 1.5
# Some adjustments for large parallel size # Some adjustments for large parallel size
reserved_mem += cfg.tp_size * cfg.pp_size / 8 * 1024 reserved_mem += cfg.tp_size * cfg.pp_size / 8 * 1024
reserved_mem += server_args.reserve_for_graph_mb() reserved_mem += reserve_for_graph_mb(server_args)
if gpu_mem is not None and gpu_mem > 60 * 1024: if gpu_mem is not None and gpu_mem > 60 * 1024:
reserved_mem = max(reserved_mem, 10 * 1024) reserved_mem = max(reserved_mem, 10 * 1024)
# Reserve headroom for DeepEP all-to-all buffers on top of the floor. # Reserve headroom for DeepEP all-to-all buffers on top of the floor.
reserved_mem += server_args.reserve_for_deepep_a2a_mb() reserved_mem += reserve_for_deepep_a2a_mb(server_args)
declare_resolution( declare_resolution(
server_args, server_args,
@@ -250,14 +259,14 @@ def handle_gpu_memory_settings(server_args: Any, gpu_mem):
# so we adjust the mem_fraction_static accordingly. The VLM encoder # so we adjust the mem_fraction_static accordingly. The VLM encoder
# only runs on the prefill stage, so PD decode engines do not need # only runs on the prefill stage, so PD decode engines do not need
# this headroom; prefill engines and normal (non-PD) engines do. # this headroom; prefill engines and normal (non-PD) engines do.
model_config = server_args.get_model_config() model_config = model_config_of(server_args)
if ( if (
model_config.is_multimodal model_config.is_multimodal
and not cfg.language_only and not cfg.language_only
and not cfg.language_model_only and not cfg.language_model_only
and cfg.disaggregation_mode != "decode" and cfg.disaggregation_mode != "decode"
): ):
server_args.adjust_mem_fraction_for_vlm(model_config) adjust_mem_fraction_for_vlm(server_args, model_config)
# If symm mem is enabled and prealloc size is not set, set it to 4GB # If symm mem is enabled and prealloc size is not set, set it to 4GB
if cfg.enable_symm_mem and not envs.SGLANG_SYMM_MEM_PREALLOC_GB_SIZE.is_set(): if cfg.enable_symm_mem and not envs.SGLANG_SYMM_MEM_PREALLOC_GB_SIZE.is_set():
@@ -266,3 +275,104 @@ def handle_gpu_memory_settings(server_args: Any, gpu_mem):
"Symmetric memory is enabled, setting symmetric memory prealloc size to 4GB as default." "Symmetric memory is enabled, setting symmetric memory prealloc size to 4GB as default."
"Use environment variable SGLANG_SYMM_MEM_PREALLOC_GB_SIZE to change the prealloc size." "Use environment variable SGLANG_SYMM_MEM_PREALLOC_GB_SIZE to change the prealloc size."
) )
def reserve_for_graph_mb(server_args: Any) -> float:
from sglang.srt.arg_groups.overrides import use_mla_backend
cfg = resolving_view(server_args)
decode_cuda_graph_config = cfg.cuda_graph_config.decode
prefill_cuda_graph_config = cfg.cuda_graph_config.prefill
reserved_mem = 0.0
if (
cfg.disaggregation_mode != "prefill"
and decode_cuda_graph_config.backend != Backend.DISABLED
):
reserved_mem += decode_cuda_graph_config.max_bs * 2
if (
resolved_view(server_args).enable_dp_attention
and cfg.disaggregation_mode != "prefill"
):
# DP attention needs more padding for some operations, and much more for large
# cuda graph max bs (torch allocator / implementation inefficiencies).
reserved_mem += decode_cuda_graph_config.max_bs * cfg.dp_size * 3
if decode_cuda_graph_config.max_bs > 300:
reserved_mem += decode_cuda_graph_config.max_bs * cfg.dp_size * 1.5
if (
cfg.disaggregation_mode != "decode"
and prefill_cuda_graph_config.backend != Backend.DISABLED
):
if not use_mla_backend(server_args):
# Only non-torch memory is counted; torch memory is reused by cuda graph capture.
reserved_mem += len(prefill_cuda_graph_config.bs) * 8
else:
# MLA backend overhead is much higher than expected with fa3.
reserved_mem += 1.5 * 1024
if (
prefill_cuda_graph_config.backend == Backend.BREAKABLE
and resolved_view(server_args).moe_a2a_backend == "deepep"
):
# Prefill-BCG DeepEP delta (bridge pool + NVL first-touch
# during capture); decode-side DeepEP is a baseline cost.
reserved_mem += 1 * 1024
return reserved_mem
def reserve_for_deepep_a2a_mb(server_args: Any) -> float:
# DeepEP all-to-all buffers captured in the decode graph are real extra
# allocations, reserved on top of the floor.
cfg = resolving_view(server_args)
decode_cuda_graph_config = cfg.cuda_graph_config.decode
if (
cfg.disaggregation_mode != "prefill"
and decode_cuda_graph_config.backend != Backend.DISABLED
and resolved_view(server_args).moe_a2a_backend == "deepep"
):
return 2 * 1024
return 0.0
def adjust_mem_fraction_for_vlm(server_args: Any, model_config):
cfg = resolving_view(server_args)
vision_config = getattr(model_config.hf_config, "vision_config", None)
if vision_config is None:
return
# roughly reduce the mem_fraction_static base on params of Vit
original_server_arg_mem_fraction = cfg.mem_fraction_static
# a base mem_fraction_static factor for regular Vit
base_mem_fraction_reduction_ratio = 0.95
vit_num_layers = getattr(vision_config, "num_hidden_layers", 24)
vit_hidden_size = getattr(vision_config, "hidden_size", 1024)
# baseline ViT params (ViT-L/14)
baseline_vit_layers = 24
baseline_vit_hidden_size = 1024
# weight params count
current_complexity_score = vit_num_layers * (vit_hidden_size**2)
baseline_complexity_score = baseline_vit_layers * (baseline_vit_hidden_size**2)
complexity_ratio = (
current_complexity_score / baseline_complexity_score
if baseline_complexity_score > 0
else 1.0
)
# every time the complexity grows 100%, adjust final factor for 10%
sensitivity_scale = 0.1
dynamic_adjustment_factor = 1.0 - sensitivity_scale * (complexity_ratio - 1.0)
dynamic_adjustment_factor = max(0.8, min(1.05, dynamic_adjustment_factor))
final_overall_factor = base_mem_fraction_reduction_ratio * dynamic_adjustment_factor
declare_resolution(
server_args,
"adjust_mem_fraction_for_vlm",
mem_fraction_static=original_server_arg_mem_fraction * final_overall_factor,
)
+36 -17
View File
@@ -8,6 +8,7 @@ from typing import Any
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
declare_resolution, declare_resolution,
mamba_cache_chunk_size,
resolved_view, resolved_view,
resolving_view, resolving_view,
) )
@@ -34,6 +35,12 @@ logger = logging.getLogger(__name__)
def handle_model_specific_adjustments(server_args: Any): def handle_model_specific_adjustments(server_args: Any):
from sglang.srt.arg_groups.overrides import (
attention_backends_of,
model_config_of,
use_mla_backend,
)
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
from sglang.srt.configs.model_config import ( from sglang.srt.configs.model_config import (
get_mimo_v2_fused_qkv_expected_tp_size, get_mimo_v2_fused_qkv_expected_tp_size,
@@ -57,7 +64,7 @@ def handle_model_specific_adjustments(server_args: Any):
# key them on. # key them on.
return return
model_config = server_args.get_model_config() model_config = model_config_of(server_args)
hf_config = model_config.hf_config hf_config = model_config.hf_config
model_arch = hf_config.architectures[0] model_arch = hf_config.architectures[0]
@@ -203,10 +210,14 @@ def handle_model_specific_adjustments(server_args: Any):
import torch import torch
major, _ = torch.cuda.get_device_capability() major, _ = torch.cuda.get_device_capability()
server_args._set_default_dsa_kv_cache_dtype( from sglang.srt.arg_groups.overrides import (
major, resolved_view(server_args).quantization _dsa_kv_cache_dtype_default,
_dsa_split_backend_resolution,
run_post_process_pass,
) )
server_args._set_default_dsa_backends(major)
run_post_process_pass(server_args, _dsa_kv_cache_dtype_default)
run_post_process_pass(server_args, _dsa_split_backend_resolution)
if cfg.enable_prefill_cp: if cfg.enable_prefill_cp:
assert ( assert (
@@ -270,7 +281,7 @@ def handle_model_specific_adjustments(server_args: Any):
# MLA prefill CP auto-config: the field declarations moved to # MLA prefill CP auto-config: the field declarations moved to
# the override registry (arg_groups/overrides.py: # the override registry (arg_groups/overrides.py:
# _deepseek_family_overrides). # _deepseek_family_overrides).
if cfg.enable_prefill_cp and server_args.use_mla_backend(): if cfg.enable_prefill_cp and use_mla_backend(server_args):
declare_resolution( declare_resolution(
server_args, server_args,
"_handle_model_specific_adjustments", "_handle_model_specific_adjustments",
@@ -304,7 +315,7 @@ def handle_model_specific_adjustments(server_args: Any):
# here for the rest of the DSA family (DeepSeek-V3.2 / # here for the rest of the DSA family (DeepSeek-V3.2 /
# GLM-5.x) that shares the same decode top-k path. # GLM-5.x) that shares the same decode top-k path.
envs.SGLANG_OPT_USE_TOPK_V2.set(False) envs.SGLANG_OPT_USE_TOPK_V2.set(False)
if not server_args._resolved().enable_dp_attention and cfg.nnodes == 1: if not resolved_view(server_args).enable_dp_attention and cfg.nnodes == 1:
# TODO (Hubert): Put this back later # TODO (Hubert): Put this back later
# server_args.enable_aiter_allreduce_fusion = True # server_args.enable_aiter_allreduce_fusion = True
logger.info("Enable Aiter AllReduce Fusion for DeepseekV3ForCausalLM") logger.info("Enable Aiter AllReduce Fusion for DeepseekV3ForCausalLM")
@@ -372,8 +383,8 @@ def handle_model_specific_adjustments(server_args: Any):
"intel_xpu", "intel_xpu",
"aiter", "aiter",
] ]
prefill_attn_backend, decode_attn_backend = ( prefill_attn_backend, decode_attn_backend = attention_backends_of(
server_args._resolved_attention_backends() resolved_view(server_args)
) )
assert ( assert (
prefill_attn_backend in supported_backends prefill_attn_backend in supported_backends
@@ -387,7 +398,7 @@ def handle_model_specific_adjustments(server_args: Any):
quant_method = get_quantization_config(hf_config) quant_method = get_quantization_config(hf_config)
is_mxfp4_quant_format = quant_method == "mxfp4" is_mxfp4_quant_format = quant_method == "mxfp4"
if ( if (
not server_args._resolved().enable_dp_attention not resolved_view(server_args).enable_dp_attention
and cfg.nnodes == 1 and cfg.nnodes == 1
and is_hip() and is_hip()
): ):
@@ -407,13 +418,13 @@ def handle_model_specific_adjustments(server_args: Any):
if resolved_view(server_args).moe_runner_backend == "triton_kernel": if resolved_view(server_args).moe_runner_backend == "triton_kernel":
assert ( assert (
server_args._resolved().ep_size == 1 resolved_view(server_args).ep_size == 1
), "Triton kernel MoE is only supported when ep_size == 1" ), "Triton kernel MoE is only supported when ep_size == 1"
elif model_arch in ("MiMoV2ForCausalLM", "MiMoV2FlashForCausalLM"): elif model_arch in ("MiMoV2ForCausalLM", "MiMoV2FlashForCausalLM"):
if model_arch == "MiMoV2ForCausalLM" and not cfg.encoder_only: if model_arch == "MiMoV2ForCausalLM" and not cfg.encoder_only:
expected_attn_tp_size = get_mimo_v2_fused_qkv_expected_tp_size(hf_config) expected_attn_tp_size = get_mimo_v2_fused_qkv_expected_tp_size(hf_config)
view = server_args._resolved() view = resolved_view(server_args)
attn_dp_size = cfg.dp_size if view.enable_dp_attention else 1 attn_dp_size = cfg.dp_size if view.enable_dp_attention else 1
effective_attn_tp_size = cfg.tp_size // attn_dp_size // view.attn_cp_size effective_attn_tp_size = cfg.tp_size // attn_dp_size // view.attn_cp_size
if ( if (
@@ -476,7 +487,9 @@ def handle_model_specific_adjustments(server_args: Any):
): ):
# Default attention backend selection moved to the override registry # Default attention backend selection moved to the override registry
# (arg_groups/overrides.py: _gemma4_overrides). # (arg_groups/overrides.py: _gemma4_overrides).
prefill_backend, decode_backend = server_args._resolved_attention_backends() prefill_backend, decode_backend = attention_backends_of(
resolved_view(server_args)
)
accepted_backends = ( accepted_backends = (
"trtllm_mha", "trtllm_mha",
"triton", "triton",
@@ -585,9 +598,13 @@ def handle_model_specific_adjustments(server_args: Any):
def handle_model_capability_adjustments(server_args: Any): def handle_model_capability_adjustments(server_args: Any):
from sglang.srt.arg_groups.cuda_graph_hook import (
generate_prefill_cuda_graph_batch_sizes,
)
from sglang.srt.arg_groups.kv_cache_hook import ( from sglang.srt.arg_groups.kv_cache_hook import (
validate_prefill_only_disable_kv_cache_args, validate_prefill_only_disable_kv_cache_args,
) )
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if parse_connector_type(cfg.model_path) == ConnectorType.INSTANCE: if parse_connector_type(cfg.model_path) == ConnectorType.INSTANCE:
@@ -597,7 +614,7 @@ def handle_model_capability_adjustments(server_args: Any):
run_post_process_pass, run_post_process_pass,
) )
model_config = server_args.get_model_config() model_config = model_config_of(server_args)
hf_config = model_config.hf_config hf_config = model_config.hf_config
# HRM-Text needs bidirectional prompt attention (prefill), which only # HRM-Text needs bidirectional prompt attention (prefill), which only
@@ -768,8 +785,8 @@ def handle_model_capability_adjustments(server_args: Any):
) )
} }
if (Phase.PREFILL, "bs") not in cuda_graph_config_locked: if (Phase.PREFILL, "bs") not in cuda_graph_config_locked:
sizing["bs"] = server_args._generate_prefill_cuda_graph_batch_sizes( sizing["bs"] = generate_prefill_cuda_graph_batch_sizes(
sizing["max_bs"] server_args, sizing["max_bs"]
) )
declare_resolution( declare_resolution(
server_args, server_args,
@@ -831,13 +848,15 @@ def handle_mamba_radix_cache(server_args: Any, model_arch: str):
validate_mamba_extra_buffer( validate_mamba_extra_buffer(
view, view,
model_arch, model_arch,
mamba_cache_chunk_size_of=lambda: server_args.mamba_cache_chunk_size, mamba_cache_chunk_size_of=lambda: mamba_cache_chunk_size(server_args),
) )
else: else:
validate_mamba_no_buffer(view, model_arch) validate_mamba_no_buffer(view, model_arch)
def handle_language_model_only(server_args: Any): def handle_language_model_only(server_args: Any):
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if not cfg.language_model_only: if not cfg.language_model_only:
return return
@@ -858,7 +877,7 @@ def handle_language_model_only(server_args: Any):
"--language-model-only is incompatible with --disaggregation-mode " "--language-model-only is incompatible with --disaggregation-mode "
"prefill/decode" "prefill/decode"
) )
architectures = server_args.get_model_config().hf_config.architectures architectures = model_config_of(server_args).hf_config.architectures
if not any( if not any(
a in server_args.LANGUAGE_MODEL_ONLY_ARCHITECTURES for a in architectures a in server_args.LANGUAGE_MODEL_ONLY_ARCHITECTURES for a in architectures
): ):
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import glob
import importlib import importlib
import logging import logging
import os import os
@@ -177,7 +178,7 @@ def handle_load_format(server_args: Any):
load_format="gguf", load_format="gguf",
) )
if cfg.load_format == "auto" and server_args._is_mistral_native_format(): if cfg.load_format == "auto" and is_mistral_native_format(server_args):
declare_resolution( declare_resolution(
server_args, server_args,
"_handle_load_format", "_handle_load_format",
@@ -306,3 +307,62 @@ def validate_transfer_engine(server_args: Any):
return False return False
else: else:
return True return True
def is_mistral_native_format(server_args: Any) -> bool:
"""True iff the checkpoint requires load_format=mistral.
Looks for consolidated*.safetensors with no competing
model*.safetensors; when both weight formats ship in the
same checkpoint (e.g. Mistral-7B-Instruct-v0.3) the HF path is
preferred to avoid loading Mistral-named weights into an
HF-named architecture.
Name override: mistral-large-3 / mistral-small-4 /
leanstral always treat as Mistral-native when params.json
is present -- those families need Mistral weight loading
regardless of which weight files happen to be present.
"""
cfg = resolving_view(server_args)
_MISTRAL_NATIVE_PATTERNS = (
"mistral-large-3",
"mistral-small-4",
"leanstral",
)
name_matches = any(
p in str(cfg.model_path).lower() for p in _MISTRAL_NATIVE_PATTERNS
)
def _check_format(has_params, has_consolidated, has_hf_weights) -> bool:
if has_params and name_matches:
return True
return has_consolidated and not has_hf_weights
if os.path.isdir(cfg.model_path):
return _check_format(
has_params=os.path.exists(os.path.join(cfg.model_path, "params.json")),
has_consolidated=bool(
glob.glob(os.path.join(cfg.model_path, "consolidated*.safetensors"))
),
has_hf_weights=bool(
glob.glob(os.path.join(cfg.model_path, "model*.safetensors"))
),
)
try:
from huggingface_hub import HfApi
files = {s.rfilename for s in HfApi().model_info(cfg.model_path).siblings}
return _check_format(
has_params="params.json" in files,
has_consolidated=any(
f.startswith("consolidated") and f.endswith(".safetensors")
for f in files
),
has_hf_weights=any(
f.startswith("model") and f.endswith(".safetensors") and "/" not in f
for f in files
),
)
except Exception:
return False
+32 -8
View File
@@ -8,7 +8,10 @@ import os
from typing import Any from typing import Any
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
cutedsl_moe_max_num_tokens,
declare_resolution, declare_resolution,
max_prefill_buffer_tokens,
max_speculative_num_draft_tokens,
resolved_view, resolved_view,
resolving_view, resolving_view,
) )
@@ -24,6 +27,8 @@ def handle_moe_kernel_config(server_args: Any):
# The quantization-driven runner resolutions moved to the pipeline # The quantization-driven runner resolutions moved to the pipeline
# (arg_groups/overrides.py: _moe_runner_backend_quant_constraints); # (arg_groups/overrides.py: _moe_runner_backend_quant_constraints);
# the compatibility asserts and fusion writes stay below. # the compatibility asserts and fusion writes stay below.
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
_moe_runner_backend_quant_constraints, _moe_runner_backend_quant_constraints,
@@ -50,7 +55,7 @@ def handle_moe_kernel_config(server_args: Any):
# modelopt_mixed with non-NVFP4 MoE layers is rejected at load time. # modelopt_mixed with non-NVFP4 MoE layers is rejected at load time.
assert ( assert (
view.quantization in ["modelopt_fp4", "modelopt_mixed", "nvfp4_online"] view.quantization in ["modelopt_fp4", "modelopt_mixed", "nvfp4_online"]
or server_args.get_model_config().nvfp4_moe_meta is not None or model_config_of(server_args).nvfp4_moe_meta is not None
), f"Invalid quantization '{view.quantization}'. \nFlashInfer CuteDSL MOE currently supports only: 'modelopt_fp4', 'modelopt_mixed' (with NVFP4 MoE layers), 'nvfp4_online', or hybrid NVFP4 models." ), f"Invalid quantization '{view.quantization}'. \nFlashInfer CuteDSL MOE currently supports only: 'modelopt_fp4', 'modelopt_mixed' (with NVFP4 MoE layers), 'nvfp4_online', or hybrid NVFP4 models."
assert view.ep_size in [ assert view.ep_size in [
1, 1,
@@ -116,6 +121,8 @@ def handle_a2a_moe(server_args: Any):
# the resolution pipeline (arg_groups/overrides.py: # the resolution pipeline (arg_groups/overrides.py:
# _a2a_backend_overrides / _a2a_ep_size); the per-backend logs, # _a2a_backend_overrides / _a2a_ep_size); the per-backend logs,
# asserts, fusion/deepep_mode/env/cuda-graph writes stay below. # asserts, fusion/deepep_mode/env/cuda-graph writes stay below.
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
_a2a_backend_overrides, _a2a_backend_overrides,
@@ -259,7 +266,7 @@ def handle_a2a_moe(server_args: Any):
logger.warning("--deepep-mode is ignored for Flashinfer MoE A2A") logger.warning("--deepep-mode is ignored for Flashinfer MoE A2A")
if not envs.SGLANG_MOE_NVFP4_DISPATCH.is_set() and ( if not envs.SGLANG_MOE_NVFP4_DISPATCH.is_set() and (
resolved_view(server_args).quantization == "modelopt_fp4" resolved_view(server_args).quantization == "modelopt_fp4"
or server_args.get_model_config().nvfp4_moe_meta is not None or model_config_of(server_args).nvfp4_moe_meta is not None
): ):
envs.SGLANG_MOE_NVFP4_DISPATCH.set(True) envs.SGLANG_MOE_NVFP4_DISPATCH.set(True)
logger.warning( logger.warning(
@@ -291,7 +298,7 @@ def handle_a2a_moe(server_args: Any):
# Skip validation if disaggregation mode is decode. # Skip validation if disaggregation mode is decode.
if cfg.chunked_prefill_size > 0 and cfg.disaggregation_mode != "decode": if cfg.chunked_prefill_size > 0 and cfg.disaggregation_mode != "decode":
assert ( assert (
server_args._required_mori_dispatch_tokens_per_rank() required_mori_dispatch_tokens_per_rank(server_args)
) <= envs.SGLANG_MORI_NUM_MAX_DISPATCH_TOKENS_PER_RANK.get(), ( ) <= envs.SGLANG_MORI_NUM_MAX_DISPATCH_TOKENS_PER_RANK.get(), (
"SGLANG_MORI_NUM_MAX_DISPATCH_TOKENS_PER_RANK (default 4096) " "SGLANG_MORI_NUM_MAX_DISPATCH_TOKENS_PER_RANK (default 4096) "
"must be >= the per-rank MoRI dispatch tokens " "must be >= the per-rank MoRI dispatch tokens "
@@ -339,7 +346,7 @@ def handle_a2a_moe(server_args: Any):
# Skip validation if disaggregation mode is decode # Skip validation if disaggregation mode is decode
if cfg.chunked_prefill_size > 0 and cfg.disaggregation_mode != "decode": if cfg.chunked_prefill_size > 0 and cfg.disaggregation_mode != "decode":
assert ( assert (
server_args._required_pplx_dispatch_tokens_per_rank() required_pplx_dispatch_tokens_per_rank(server_args)
) <= envs.SGLANG_PPLX_NUM_MAX_DISPATCH_TOKENS_PER_RANK.get(), ( ) <= envs.SGLANG_PPLX_NUM_MAX_DISPATCH_TOKENS_PER_RANK.get(), (
"SGLANG_PPLX_NUM_MAX_DISPATCH_TOKENS_PER_RANK (default 128) " "SGLANG_PPLX_NUM_MAX_DISPATCH_TOKENS_PER_RANK (default 128) "
"must be >= the per-rank pplx dispatch tokens " "must be >= the per-rank pplx dispatch tokens "
@@ -372,7 +379,7 @@ def validate_deepep_v2_dispatch_token_budget(server_args: Any) -> None:
capacity = envs.SGLANG_DEEPEP_V2_NUM_MAX_DISPATCH_TOKENS_PER_RANK.get() capacity = envs.SGLANG_DEEPEP_V2_NUM_MAX_DISPATCH_TOKENS_PER_RANK.get()
if view.disaggregation_mode != "decode": if view.disaggregation_mode != "decode":
prefill_tokens = server_args.max_prefill_buffer_tokens() or ( prefill_tokens = max_prefill_buffer_tokens(server_args) or (
view.max_prefill_tokens or 0 view.max_prefill_tokens or 0
) )
if prefill_tokens > capacity: if prefill_tokens > capacity:
@@ -396,7 +403,7 @@ def validate_deepep_v2_dispatch_token_budget(server_args: Any) -> None:
per_rank_pool_bs = max(1, view.max_running_requests // attn_dp_size) per_rank_pool_bs = max(1, view.max_running_requests // attn_dp_size)
graph_bs = min(graph_bs, per_rank_pool_bs) graph_bs = min(graph_bs, per_rank_pool_bs)
tokens_per_req = ( tokens_per_req = (
server_args.max_speculative_num_draft_tokens or 1 max_speculative_num_draft_tokens(server_args) or 1
if view.speculative_algorithm if view.speculative_algorithm
else 1 else 1
) )
@@ -413,6 +420,8 @@ def validate_deepep_v2_dispatch_token_budget(server_args: Any) -> None:
def validate_deepep_v2_model_architecture(server_args: Any) -> None: def validate_deepep_v2_model_architecture(server_args: Any) -> None:
"""Allow DeepEP v2 only where its model workflow is validated.""" """Allow DeepEP v2 only where its model workflow is validated."""
from sglang.srt.arg_groups.overrides import model_config_of
if ( if (
parse_connector_type(resolved_view(server_args).model_path) parse_connector_type(resolved_view(server_args).model_path)
== ConnectorType.INSTANCE == ConnectorType.INSTANCE
@@ -424,7 +433,7 @@ def validate_deepep_v2_model_architecture(server_args: Any) -> None:
) )
architectures = ( architectures = (
getattr(server_args.get_model_config().hf_config, "architectures", None) or [] getattr(model_config_of(server_args).hf_config, "architectures", None) or []
) )
architecture = architectures[0] if architectures else None architecture = architectures[0] if architectures else None
@@ -458,7 +467,7 @@ def validate_cutedsl_a2a_token_budget(server_args: Any):
and cfg.disaggregation_mode != "decode" and cfg.disaggregation_mode != "decode"
): ):
return return
required_tokens = server_args.cutedsl_moe_max_num_tokens() required_tokens = cutedsl_moe_max_num_tokens(server_args)
max_dispatch_tokens_per_rank = ( max_dispatch_tokens_per_rank = (
envs.SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK.get() or 1024 envs.SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK.get() or 1024
) )
@@ -481,3 +490,18 @@ def validate_cutedsl_a2a_token_budget(server_args: Any):
f"{required_per_rank}` or lower the relevant limit " f"{required_per_rank}` or lower the relevant limit "
f"(e.g. --max-prefill-tokens) to <= {max_cutedsl_tokens}." f"(e.g. --max-prefill-tokens) to <= {max_cutedsl_tokens}."
) )
def required_mori_dispatch_tokens_per_rank(server_args: Any) -> int:
"""Max tokens a single rank dispatches through MoRI in one forward."""
cfg = resolving_view(server_args)
return cfg.chunked_prefill_size
def required_pplx_dispatch_tokens_per_rank(server_args: Any) -> int:
"""Max tokens a single rank dispatches through pplx in one forward."""
cfg = resolving_view(server_args)
required = cfg.chunked_prefill_size
if cfg.cuda_graph_max_bs_decode is not None:
required = max(required, cfg.cuda_graph_max_bs_decode)
return required
+346 -40
View File
@@ -38,12 +38,14 @@ import dataclasses
import inspect import inspect
import json import json
import logging import logging
import math
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
from sglang.srt.arg_groups.arg_utils import field_names, resolvable_fields from sglang.srt.arg_groups.arg_utils import field_names, resolvable_fields
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.hardware_backend.mlx.runtime import use_mlx from sglang.srt.hardware_backend.mlx.runtime import use_mlx
from sglang.srt.model_executor.cuda_graph_config import Backend from sglang.srt.model_executor.cuda_graph_config import Backend
from sglang.srt.platforms import current_platform
from sglang.srt.utils.common import ( from sglang.srt.utils.common import (
cpu_has_amx_support, cpu_has_amx_support,
get_device_capability, get_device_capability,
@@ -57,9 +59,11 @@ from sglang.srt.utils.common import (
is_flashinfer_available, is_flashinfer_available,
is_gfx95_supported, is_gfx95_supported,
is_hip, is_hip,
is_hopper_with_cuda_12_3,
is_mnnvl_fabric_device, is_mnnvl_fabric_device,
is_mps, is_mps,
is_musa, is_musa,
is_no_spec_infer_or_topk_one,
is_npu, is_npu,
is_sm90_supported, is_sm90_supported,
is_sm100_supported, is_sm100_supported,
@@ -94,7 +98,7 @@ def register_model_override(architecture: str):
The decorated callable receives ``(server_args, hf_config)``, must not The decorated callable receives ``(server_args, hf_config)``, must not
mutate either, and returns a ``{field: resolved_value}`` dict (possibly mutate either, and returns a ``{field: resolved_value}`` dict (possibly
empty when nothing applies). Providers needing derived model data beyond empty when nothing applies). Providers needing derived model data beyond
the HF config go through ``server_args.get_model_config()`` (cached, the HF config go through ``model_config_of(server_args)`` (cached,
read-only) — never anything mutating. read-only) — never anything mutating.
""" """
@@ -293,7 +297,7 @@ def declare_resolution(server_args: Any, source: str, **fields: Any) -> None:
The stash *is* the resolution result: the bags are projected from it, The stash *is* the resolution result: the bags are projected from it,
`resolution_result` answers from it, and no field is written. A resolver `resolution_result` answers from it, and no field is written. A resolver
reading a field another resolver may have decided must read `resolving_view` reading a field another resolver may have decided must read `resolving_view`
(or `ServerArgs._resolved()`), which (or `resolved_view(server_args)`), which
`test_resolution_reads_the_declarations` pins. `test_resolution_reads_the_declarations` pins.
For resolvers inside ``__post_init__``; launcher-stage resolution goes For resolvers inside ``__post_init__``; launcher-stage resolution goes
@@ -460,8 +464,7 @@ def resolved_view(server_args: Any) -> ResolvedView:
overlaid on the fields, snapshotted per call. overlaid on the fields, snapshotted per call.
For mid-resolution code that is not a pass (``__post_init__`` handlers and For mid-resolution code that is not a pass (``__post_init__`` handlers and
hooks), and for the record's own members that must answer with what hooks) that must answer with what resolution decided -- a declaration-only resolver (a model-specific
resolution decided -- a declaration-only resolver (a model-specific
override, a registry entry) never writes the field, so a field read there override, a registry entry) never writes the field, so a field read there
answers with the raw input.""" answers with the raw input."""
return ResolvedView(server_args, overlay=_declaration_overlay(server_args)) return ResolvedView(server_args, overlay=_declaration_overlay(server_args))
@@ -702,7 +705,7 @@ def _kimi_k3_overrides(server_args: Any, hf_config: Any) -> dict:
if not (is_sm100_supported() and get_device_sm() in (100, 103)): if not (is_sm100_supported() and get_device_sm() in (100, 103)):
return {} return {}
backends_unset = server_args.is_attention_backend_not_set() backends_unset = is_attention_backend_not_set(cfg)
if cfg.speculative_algorithm != "DSPARK": if cfg.speculative_algorithm != "DSPARK":
if not backends_unset: if not backends_unset:
return {} return {}
@@ -809,7 +812,7 @@ def _deepseek_family_overrides(server_args: Any, hf_config: Any) -> dict:
if is_deepseek_dsa(hf_config): # DeepSeek 3.2/GLM 5 if is_deepseek_dsa(hf_config): # DeepSeek 3.2/GLM 5
# Set attention backend for DeepSeek # Set attention backend for DeepSeek
if server_args.is_attention_backend_not_set(): if is_attention_backend_not_set(cfg):
overrides["attention_backend"] = "dsa" overrides["attention_backend"] = "dsa"
logger.info("Use dsa attention backend for DeepSeek with DSA.") logger.info("Use dsa attention backend for DeepSeek with DSA.")
if not is_npu() and not is_xpu(): # CUDA or ROCm GPU if not is_npu() and not is_xpu(): # CUDA or ROCm GPU
@@ -882,7 +885,7 @@ def _deepseek_family_overrides(server_args: Any, hf_config: Any) -> dict:
) )
# MLA prefill CP auto-config. Mirrors the NSA CP block above # MLA prefill CP auto-config. Mirrors the NSA CP block above
# (minus the in-seq/round-robin mode split, which MLA CP does not support) # (minus the in-seq/round-robin mode split, which MLA CP does not support)
if cfg.enable_prefill_cp and server_args.use_mla_backend(): if cfg.enable_prefill_cp and use_mla_backend(server_args):
logger.warning( logger.warning(
"MLA prefill context parallel is still experimental. " "MLA prefill context parallel is still experimental. "
"Verified on Hopper with the fa3 backend." "Verified on Hopper with the fa3 backend."
@@ -944,7 +947,7 @@ def _minimax_m2_overrides(server_args: Any, hf_config: Any) -> dict:
if ( if (
is_sm100_supported() is_sm100_supported()
and cfg.moe_runner_backend == "auto" and cfg.moe_runner_backend == "auto"
and server_args.get_model_config().quantization == "modelopt_fp4" and model_config_of(server_args).quantization == "modelopt_fp4"
): ):
overrides["moe_runner_backend"] = "flashinfer_trtllm_routed" overrides["moe_runner_backend"] = "flashinfer_trtllm_routed"
logger.info( logger.info(
@@ -971,7 +974,7 @@ def _minimax_m3_overrides(server_args: Any, hf_config: Any) -> dict:
quant_resolved = quant_method quant_resolved = quant_method
if is_hip(): if is_hip():
if server_args.is_attention_backend_not_set(): if is_attention_backend_not_set(cfg):
overrides["attention_backend"] = "triton" overrides["attention_backend"] = "triton"
if cfg.moe_runner_backend == "auto" and quant_resolved == "mxfp8": if cfg.moe_runner_backend == "auto" and quant_resolved == "mxfp8":
overrides["moe_runner_backend"] = "triton" overrides["moe_runner_backend"] = "triton"
@@ -994,7 +997,7 @@ def _minimax_m3_overrides(server_args: Any, hf_config: Any) -> dict:
if not aiter_fusion_resolved and not envs.SGLANG_M3_ALLOW_CUSTOM_AR.get(): if not aiter_fusion_resolved and not envs.SGLANG_M3_ALLOW_CUSTOM_AR.get():
overrides["disable_custom_all_reduce"] = True overrides["disable_custom_all_reduce"] = True
elif is_sm100_supported(): elif is_sm100_supported():
if server_args.is_attention_backend_not_set(): if is_attention_backend_not_set(cfg):
if ( if (
cfg.kv_cache_dtype == "fp8_e4m3" cfg.kv_cache_dtype == "fp8_e4m3"
and not envs.SGLANG_DISABLE_M3_FP8_ATTN_GEMM.get() and not envs.SGLANG_DISABLE_M3_FP8_ATTN_GEMM.get()
@@ -1025,7 +1028,7 @@ def _minimax_m3_overrides(server_args: Any, hf_config: Any) -> dict:
f"moe_runner_backend={overrides.get('moe_runner_backend', cfg.moe_runner_backend)}." f"moe_runner_backend={overrides.get('moe_runner_backend', cfg.moe_runner_backend)}."
) )
elif is_sm90_supported(): elif is_sm90_supported():
if server_args.is_attention_backend_not_set(): if is_attention_backend_not_set(cfg):
overrides["attention_backend"] = "fa3" overrides["attention_backend"] = "fa3"
page_resolved = cfg.page_size page_resolved = cfg.page_size
if ( if (
@@ -1117,7 +1120,7 @@ def _gpt_oss_overrides(server_args: Any, hf_config: Any) -> dict:
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
overrides: Dict[str, Any] = {} overrides: Dict[str, Any] = {}
# Set attention backend for GPT-OSS # Set attention backend for GPT-OSS
if server_args.is_attention_backend_not_set(): if is_attention_backend_not_set(cfg):
if is_sm100_supported(): if is_sm100_supported():
overrides["attention_backend"] = "trtllm_mha" overrides["attention_backend"] = "trtllm_mha"
elif is_sm90_supported(): elif is_sm90_supported():
@@ -1254,7 +1257,7 @@ def _gemma4_overrides(server_args: Any, hf_config: Any) -> dict:
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
overrides: Dict[str, Any] = {} overrides: Dict[str, Any] = {}
default_attention_backend = "trtllm_mha" if is_sm100_supported() else "triton" default_attention_backend = "trtllm_mha" if is_sm100_supported() else "triton"
if server_args.is_attention_backend_not_set(): if is_attention_backend_not_set(cfg):
logger.info( logger.info(
f"Use {default_attention_backend} as default attention backend for Gemma4" f"Use {default_attention_backend} as default attention backend for Gemma4"
) )
@@ -1265,7 +1268,7 @@ def _gemma4_overrides(server_args: Any, hf_config: Any) -> dict:
elif cfg.attention_backend is None: elif cfg.attention_backend is None:
overrides["attention_backend"] = default_attention_backend overrides["attention_backend"] = default_attention_backend
if is_sm100_supported() and cfg.moe_runner_backend == "auto": if is_sm100_supported() and cfg.moe_runner_backend == "auto":
if server_args.get_model_config().quantization == "modelopt_fp4": if model_config_of(server_args).quantization == "modelopt_fp4":
overrides["quantization"] = "modelopt_fp4" overrides["quantization"] = "modelopt_fp4"
overrides["moe_runner_backend"] = "flashinfer_trtllm" overrides["moe_runner_backend"] = "flashinfer_trtllm"
logger.info( logger.info(
@@ -1278,12 +1281,12 @@ def _gemma4_overrides(server_args: Any, hf_config: Any) -> dict:
@_register_for("MossVLForConditionalGeneration") @_register_for("MossVLForConditionalGeneration")
def _moss_vl_overrides(server_args: Any, hf_config: Any) -> dict: def _moss_vl_overrides(server_args: Any, hf_config: Any) -> dict:
overrides: Dict[str, Any] = {} overrides: Dict[str, Any] = {}
if server_args.is_attention_backend_not_set(): if is_attention_backend_not_set(resolving_view(server_args)):
overrides["prefill_attention_backend"] = "flashinfer" overrides["prefill_attention_backend"] = "flashinfer"
logger.info("Use flashinfer as default prefill attention backend for Moss-VL") logger.info("Use flashinfer as default prefill attention backend for Moss-VL")
prefill_backend = ( prefill_backend = (
overrides.get("prefill_attention_backend") overrides.get("prefill_attention_backend")
or server_args.get_attention_backends()[0] or attention_backends_of(resolved_view(server_args))[0]
) )
assert prefill_backend == "flashinfer", ( assert prefill_backend == "flashinfer", (
"MossVLForConditionalGeneration requires flashinfer prefill " "MossVLForConditionalGeneration requires flashinfer prefill "
@@ -1323,7 +1326,7 @@ def _minicpm_sala_overrides(server_args: Any, hf_config: Any) -> dict:
if dense_decode is not None: if dense_decode is not None:
overrides["decode_attention_backend"] = dense_decode overrides["decode_attention_backend"] = dense_decode
elif has_sparse_attention: elif has_sparse_attention:
uses_sparse_backend = cfg.is_attention_backend_not_set() or any( uses_sparse_backend = is_attention_backend_not_set(cfg) or any(
backend in ("minicpm_flashattn", "minicpm_flashinfer") backend in ("minicpm_flashattn", "minicpm_flashinfer")
for backend in ( for backend in (
cfg.attention_backend, cfg.attention_backend,
@@ -1335,7 +1338,7 @@ def _minicpm_sala_overrides(server_args: Any, hf_config: Any) -> dict:
raise ValueError( raise ValueError(
"MiniCPM sparse attention does not support PD disaggregation" "MiniCPM sparse attention does not support PD disaggregation"
) )
if cfg.is_attention_backend_not_set(): if is_attention_backend_not_set(cfg):
overrides["attention_backend"] = ( overrides["attention_backend"] = (
"minicpm_flashinfer" "minicpm_flashinfer"
if is_blackwell_supported() if is_blackwell_supported()
@@ -1413,7 +1416,7 @@ def _deepseek_v4_overrides(server_args: Any, hf_config: Any) -> dict:
logger.info(f"Setting swa_full_tokens_ratio to 0.1 for {model_arch}.") logger.info(f"Setting swa_full_tokens_ratio to 0.1 for {model_arch}.")
if cfg.moe_runner_backend == "auto": if cfg.moe_runner_backend == "auto":
model_config = server_args.get_model_config() model_config = model_config_of(server_args)
# nvidia/DeepSeek-V4-Pro-NVFP4 uses the routed TRT-LLM runner. # nvidia/DeepSeek-V4-Pro-NVFP4 uses the routed TRT-LLM runner.
if model_config.nvfp4_moe_meta is not None: if model_config.nvfp4_moe_meta is not None:
overrides["moe_runner_backend"] = "flashinfer_trtllm_routed" overrides["moe_runner_backend"] = "flashinfer_trtllm_routed"
@@ -1477,7 +1480,7 @@ def _inkling_overrides(server_args: Any, hf_config: Any) -> dict:
# supported default when the user left every attention-backend flag unset # supported default when the user left every attention-backend flag unset
# (mirrors the MiniMax-M3 SM100 fa4-default above); an explicit # (mirrors the MiniMax-M3 SM100 fa4-default above); an explicit
# --attention-backend / --prefill/decode-attention-backend still wins. # --attention-backend / --prefill/decode-attention-backend still wins.
if server_args.is_attention_backend_not_set(): if is_attention_backend_not_set(cfg):
inkling_attn_backend = "fa4" if is_sm100_supported() else "triton" inkling_attn_backend = "fa4" if is_sm100_supported() else "triton"
overrides["attention_backend"] = inkling_attn_backend overrides["attention_backend"] = inkling_attn_backend
logger.info( logger.info(
@@ -1495,7 +1498,7 @@ def _nemotron_h_overrides(server_args: Any, hf_config: Any) -> dict:
cache handling and the triton-backend assert stay in the arch branch).""" cache handling and the triton-backend assert stay in the arch branch)."""
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
model_arch = hf_config.architectures[0] model_arch = hf_config.architectures[0]
model_config = server_args.get_model_config() model_config = model_config_of(server_args)
overrides: Dict[str, Any] = {} overrides: Dict[str, Any] = {}
is_modelopt = model_config.quantization in [ is_modelopt = model_config.quantization in [
@@ -1565,7 +1568,7 @@ def _nemotron_h_overrides(server_args: Any, hf_config: Any) -> dict:
else: else:
overrides["moe_runner_backend"] = "flashinfer_cutlass" overrides["moe_runner_backend"] = "flashinfer_cutlass"
if is_blackwell_supported() and cfg.is_attention_backend_not_set(): if is_blackwell_supported() and is_attention_backend_not_set(cfg):
if cfg.speculative_algorithm is not None: if cfg.speculative_algorithm is not None:
speculative_algorithm = cfg.speculative_algorithm.upper() speculative_algorithm = cfg.speculative_algorithm.upper()
if is_sm100_supported() and cfg.speculative_eagle_topk in ( if is_sm100_supported() and cfg.speculative_eagle_topk in (
@@ -1607,13 +1610,14 @@ def _qwen3_5_hybrid_overrides(server_args: Any, hf_config: Any) -> dict:
return {} return {}
sm100_default_attn_backend = "triton" sm100_default_attn_backend = "triton"
# trtllm_mha requires speculative_eagle_topk == 1 and page_size > 1. # trtllm_mha requires speculative_eagle_topk == 1 and page_size > 1.
# _get_default_attn_backend handles the eagle_topk check. # get_default_attn_backend handles the eagle_topk check.
# There is only one case where page_size=1 is required, # There is only one case where page_size=1 is required,
# which is when radix cache is enabled and both extra_buffer # which is when radix cache is enabled and both extra_buffer
# and spec decoding are disabled. # and spec decoding are disabled.
default_attn_backend = server_args._get_default_attn_backend( default_attn_backend = get_default_attn_backend(
use_mla_backend=server_args.use_mla_backend(), server_args,
model_config=server_args.get_model_config(), use_mla_backend=use_mla_backend(server_args),
model_config=model_config_of(server_args),
) )
# The mamba radix-cache pass runs before this dispatch: read the # The mamba radix-cache pass runs before this dispatch: read the
# declared strategy through the view (the legacy branch observed the # declared strategy through the view (the legacy branch observed the
@@ -1746,7 +1750,7 @@ def _olmo2_overrides(server_args: Any, hf_config: Any) -> dict:
def _step3p_overrides(server_args: Any, hf_config: Any) -> dict: def _step3p_overrides(server_args: Any, hf_config: Any) -> dict:
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
overrides: Dict[str, Any] = {} overrides: Dict[str, Any] = {}
if server_args.is_attention_backend_not_set(): if is_attention_backend_not_set(cfg):
if is_blackwell_supported(): if is_blackwell_supported():
logger.info("Auto-select fa4 attention backend for Step3p7 on Blackwell.") logger.info("Auto-select fa4 attention backend for Step3p7 on Blackwell.")
overrides["attention_backend"] = "fa4" overrides["attention_backend"] = "fa4"
@@ -1857,7 +1861,7 @@ def _mamba_radix_cache_resolution(view: Any) -> dict:
get_linear_attn_spec_by_arch, get_linear_attn_spec_by_arch,
) )
hf_config = view.get_model_config().hf_config hf_config = model_config_of(view).hf_config
model_arch = hf_config.architectures[0] model_arch = hf_config.architectures[0]
in_branch = model_arch in _MAMBA_RADIX_CACHE_ARCHS in_branch = model_arch in _MAMBA_RADIX_CACHE_ARCHS
@@ -1895,7 +1899,7 @@ def _dsa_kv_cache_dtype_default(view: Any) -> dict:
PRISTINE dsa split backends (their resolution runs after this pass).""" PRISTINE dsa split backends (their resolution runs after this pass)."""
from sglang.srt.configs.model_config import is_deepseek_dsa from sglang.srt.configs.model_config import is_deepseek_dsa
hf_config = view.get_model_config().hf_config hf_config = model_config_of(view).hf_config
if hf_config.architectures[0] not in _DEEPSEEK_FAMILY_ARCHS: if hf_config.architectures[0] not in _DEEPSEEK_FAMILY_ARCHS:
return {} return {}
if not is_deepseek_dsa(hf_config): if not is_deepseek_dsa(hf_config):
@@ -1965,7 +1969,7 @@ def _dsa_split_backend_resolution(view: Any) -> dict:
capability. The hisparse arm takes precedence under --enable-hisparse.""" capability. The hisparse arm takes precedence under --enable-hisparse."""
from sglang.srt.configs.model_config import is_deepseek_dsa from sglang.srt.configs.model_config import is_deepseek_dsa
hf_config = view.get_model_config().hf_config hf_config = model_config_of(view).hf_config
if hf_config.architectures[0] not in _DEEPSEEK_FAMILY_ARCHS: if hf_config.architectures[0] not in _DEEPSEEK_FAMILY_ARCHS:
return {} return {}
if not is_deepseek_dsa(hf_config): if not is_deepseek_dsa(hf_config):
@@ -2065,7 +2069,7 @@ def _deepseek_moe_quant_resolution(view: Any) -> dict:
backend for DeepSeek"), NOT a dispatch-time declaration: the DSA backend for DeepSeek"), NOT a dispatch-time declaration: the DSA
kv-cache-dtype default earlier in the branch must read the PRISTINE kv-cache-dtype default earlier in the branch must read the PRISTINE
quantization, so this resolution has to stay at its legacy slot.""" quantization, so this resolution has to stay at its legacy slot."""
hf_config = view.get_model_config().hf_config hf_config = model_config_of(view).hf_config
model_arch = hf_config.architectures[0] model_arch = hf_config.architectures[0]
if model_arch not in _DEEPSEEK_FAMILY_ARCHS: if model_arch not in _DEEPSEEK_FAMILY_ARCHS:
return {} return {}
@@ -2152,7 +2156,7 @@ def _deepseek_spec_moe_resolution(view: Any) -> dict:
quantization (after _deepseek_moe_quant_resolution) and the pre-a2a quantization (after _deepseek_moe_quant_resolution) and the pre-a2a
ep_size, exactly like the legacy in-branch writes.""" ep_size, exactly like the legacy in-branch writes."""
hf_config = view.get_model_config().hf_config hf_config = model_config_of(view).hf_config
model_arch = hf_config.architectures[0] model_arch = hf_config.architectures[0]
if model_arch not in _DEEPSEEK_FAMILY_ARCHS: if model_arch not in _DEEPSEEK_FAMILY_ARCHS:
return {} return {}
@@ -2198,7 +2202,7 @@ def _deepseek_v4_kv_cache_dtype(view: Any) -> dict:
"""Slot pass in the DeepSeek V4 hook: default the kv-cache dtype to FP8 """Slot pass in the DeepSeek V4 hook: default the kv-cache dtype to FP8
(bfloat16 on NPU, where the pool geometry differs) and validate the (bfloat16 on NPU, where the pool geometry differs) and validate the
result. The NPU split-backend writes stay in the hook.""" result. The NPU split-backend writes stay in the hook."""
hf_config = view.get_model_config().hf_config hf_config = model_config_of(view).hf_config
model_arch = hf_config.architectures[0] model_arch = hf_config.architectures[0]
if model_arch != "DeepseekV4ForCausalLM": if model_arch != "DeepseekV4ForCausalLM":
return {} return {}
@@ -2271,7 +2275,7 @@ def _flashinfer_allreduce_fusion_auto_enable(view: Any) -> dict:
single-node systems. Reads the mid-resolution enable_dp_attention / single-node systems. Reads the mid-resolution enable_dp_attention /
moe_a2a_backend (after the DeepSeek CP and a2a declarations), exactly moe_a2a_backend (after the DeepSeek CP and a2a declarations), exactly
like the legacy tail block.""" like the legacy tail block."""
model_arch = view.get_model_config().hf_config.architectures[0] model_arch = model_config_of(view).hf_config.architectures[0]
if envs.SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION.get() and model_arch in { if envs.SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION.get() and model_arch in {
"Qwen3_5MoeForCausalLM", "Qwen3_5MoeForCausalLM",
"Qwen3_5MoeForConditionalGeneration", "Qwen3_5MoeForConditionalGeneration",
@@ -2345,7 +2349,7 @@ def _deterministic_is_deepseek_model(view: Any) -> bool:
if parse_connector_type(view.model_path) == ConnectorType.INSTANCE: if parse_connector_type(view.model_path) == ConnectorType.INSTANCE:
return False return False
try: try:
hf_config = view.get_model_config().hf_config hf_config = model_config_of(view).hf_config
return hf_config.architectures[0] in [ return hf_config.architectures[0] in [
"DeepseekV2ForCausalLM", "DeepseekV2ForCausalLM",
"DeepseekV3ForCausalLM", "DeepseekV3ForCausalLM",
@@ -2413,8 +2417,8 @@ def _attention_backend_default(view: Any) -> dict:
): # override the default attention backend ): # override the default attention backend
return {"attention_backend": view.prefill_attention_backend} return {"attention_backend": view.prefill_attention_backend}
if view.attention_backend is None: if view.attention_backend is None:
backend = view._get_default_attn_backend( backend = get_default_attn_backend(
view.use_mla_backend(), view.get_model_config() view, use_mla_backend(view), model_config_of(view)
) )
logger.info( logger.info(
f"Attention backend not specified. Use {backend} backend by default." f"Attention backend not specified. Use {backend} backend by default."
@@ -2600,7 +2604,7 @@ def _fa4_page_constraint(view: Any) -> dict:
or view.decode_attention_backend == "fa4" or view.decode_attention_backend == "fa4"
or view.prefill_attention_backend == "fa4" or view.prefill_attention_backend == "fa4"
) )
and not view.use_mla_backend() and not use_mla_backend(view)
and is_sm100_supported() and is_sm100_supported()
# EAGLE topk>1 spec runs the two-pass page-tree cascade, which the FA4 # EAGLE topk>1 spec runs the two-pass page-tree cascade, which the FA4
# CUTLASS kernel aborts on at page_size>1. That path only works at # CUTLASS kernel aborts on at page_size>1. That path only works at
@@ -2641,7 +2645,7 @@ def _attention_backend_platform_fallbacks(view: Any) -> dict:
def _intel_xpu_page_constraint(view: Any) -> dict: def _intel_xpu_page_constraint(view: Any) -> dict:
_, decode_backend = attention_backends_of(view) _, decode_backend = attention_backends_of(view)
if decode_backend == "intel_xpu": if decode_backend == "intel_xpu":
if view.use_mla_backend(): if use_mla_backend(view):
supported_page_sizes = [16, 32, 64, 128] supported_page_sizes = [16, 32, 64, 128]
msg = "Intel XPU attention backend for MLA Decode" msg = "Intel XPU attention backend for MLA Decode"
else: else:
@@ -2658,7 +2662,7 @@ def _intel_xpu_page_constraint(view: Any) -> dict:
@register_post_process @register_post_process
def _attention_backend_dual_chunk(view: Any) -> dict: def _attention_backend_dual_chunk(view: Any) -> dict:
if ( if (
getattr(view.get_model_config().hf_config, "dual_chunk_attention_config", None) getattr(model_config_of(view).hf_config, "dual_chunk_attention_config", None)
is not None is not None
): ):
if view.attention_backend is None: if view.attention_backend is None:
@@ -3036,3 +3040,305 @@ def _hrm_text_attention_force(view: Any) -> dict:
"attention." "attention."
) )
return {"attention_backend": "triton"} return {"attention_backend": "triton"}
def record_of(view: Any) -> Any:
"""The record a view reads through.
For the few helpers a view cannot serve: `get_default_attn_backend` reads
through *both* overlays, so it needs the record the two views are built
from rather than either one of them.
"""
return object.__getattribute__(view, "_server_args")
def is_attention_backend_not_set(cfg: Any):
"""None of the three attention backends has been decided yet.
Takes the view rather than the record: every read is a view read, and the
callers that hold a view (the override providers) would otherwise have to
reach back through it for a record.
"""
return (
cfg.attention_backend is None
and cfg.prefill_attention_backend is None
and cfg.decode_attention_backend is None
)
def get_default_attn_backend(server_args: Any, use_mla_backend: bool, model_config):
"""
Auto select the fastest attention backend.
1. Models with MHA Architecture (e.g: Llama, QWen)
1.1 We will turn on FA3 on hopper unless user use spec decode with topk > 1 or page_size > 1.
1.2 Use trtllm_mha for SM100/SM103 (Blackwell B200/GB200/B300) excluding spec with topk > 1.
Note: trtllm_mha does not support SM120, which will fall back to flashinfer.
1.3 In other cases, we will use flashinfer if available, otherwise use triton.
2. Models with MLA Architecture and using FA3
2.1 We will use FA3 backend on hopper.
2.2 We will use Flashinfer backend on blackwell.
2.3 Otherwise, we will use triton backend.
"""
cfg = resolving_view(server_args)
# OOT platforms provide their own default attention backend.
if current_platform.is_out_of_tree():
return current_platform.get_default_attention_backend()
# Whisper requires flashinfer for cross-attention CUDA graph support.
if "WhisperForConditionalGeneration" in (
model_config.hf_config.architectures or []
):
return "flashinfer"
if not use_mla_backend:
# MHA architecture
if is_hopper_with_cuda_12_3() and is_no_spec_infer_or_topk_one(
resolved_view(server_args)
):
# Note: flashinfer 0.6.1 caused performance regression on Hopper attention kernel
# Before the kernel is fixed, we choose fa3 as the default backend on Hopper MHA
# ref: https://github.com/sgl-project/sglang/issues/17411
return "fa3"
elif (
is_sm100_supported()
and is_no_spec_infer_or_topk_one(resolved_view(server_args))
and (
cfg.speculative_algorithm is None
or cfg.speculative_eagle_topk is not None
)
):
# trtllm_mha requires equal K/V row widths; fa4 carries
# v_head_dim through.
if model_config.has_asymmetric_kv:
return "fa4"
return "trtllm_mha"
elif is_hip():
return "aiter"
elif is_mps():
return "torch_native"
else:
# FlashInfer does not support attention sinks.
if is_flashinfer_available() and not model_config.has_attention_sinks:
return "flashinfer"
return "triton"
else:
# MLA architecture
if is_hopper_with_cuda_12_3():
return "fa3"
elif is_sm100_supported():
return "flashinfer"
elif is_hip():
head_num = model_config.get_num_kv_heads(cfg.tp_size)
# TODO current aiter only support head number 16 or 128 head number
if head_num == 128 or head_num == 16:
return "aiter"
else:
return "triton"
elif is_mps():
return "torch_native"
else:
return "triton"
def use_mla_backend(server_args: Any):
from sglang.srt.configs.model_config import AttentionArch
model_config = model_config_of(server_args)
return model_config.attention_arch == AttentionArch.MLA
def should_report_expert_balancedness(server_args: Any) -> bool:
cfg = resolving_view(server_args)
return cfg.expert_balancedness_report_mode != "off"
def model_config_of(server_args: Any):
"""The model configuration this record describes, built once and memoised.
Takes a view as readily as the record: a view is a read overlay of one
record, the memo has to live on that record either way, and the callers
that hold a view would otherwise all have to unwrap it themselves.
"""
if isinstance(server_args, (ResolvedView, ResolvingConfig)):
server_args = record_of(server_args)
# Lazy init to avoid circular import
cfg = resolving_view(server_args)
from sglang.srt.configs.model_config import ModelConfig
memo = getattr(server_args, "_model_config", None)
if memo is not None:
# The key is the path this record carried when the cache was
# filled. The GGUF and ModelScope handlers declare a different
# `model_path`, and a configuration built before them describes
# another checkpoint. `ModelConfig` re-points its own `model_path`
# at the local pull directory when the weights sit behind an
# object-store URI, so its field is not the key. A configuration a
# fixture supplied carries no key and is handed back as it is.
built_from = getattr(server_args, "_model_config_built_from", None)
if built_from is None or built_from == cfg.model_path:
return memo
model_config = ModelConfig.from_server_args(server_args)
server_args._model_config = model_config
server_args._model_config_built_from = cfg.model_path
if model_config.is_hybrid_swa:
logger.info(
"Hybrid SWA model detected. architectures=%s",
model_config.hf_config.architectures,
)
return model_config
def post_capture_kv_sizing_planned(server_args: Any) -> bool:
"""Whether the mem_fraction heuristic may skip the graph reserve; must be
False for any config the runtime won't post-capture-size, else it gets an
under-reserved fraction."""
cfg = resolving_view(server_args)
mla_enabled = use_mla_backend(server_args)
if not envs.SGLANG_ENABLE_POST_CAPTURE_KV_SIZING.get():
return False
if cfg.device != "cuda":
return False
if cfg.dcp_size != 1:
return False
if mla_enabled:
return False
if cfg.kv_cache_dtype == "fp4_e2m1":
return False
if cfg.prefill_only_disable_kv_cache:
return False
if cfg.enable_memory_saver:
return False
if envs.SGLANG_MOONCAKE_CUSTOM_MEM_POOL.get() is not None:
return False
if (
cfg.disaggregation_mode != "prefill"
and cfg.cuda_graph_config.decode.backend == Backend.DISABLED
):
return False
if cfg.disaggregation_mode != "decode":
prefill_cfg = cfg.cuda_graph_config.prefill
# We can only skip eager activation headroom when the largest
# prefill forward batch size is already graph-captured. Otherwise,
# an eager forward will need more memory and lead to OOM.
if (
prefill_cfg.backend == Backend.DISABLED
or cfg.chunked_prefill_size <= 0
or max_prefill_buffer_tokens(server_args) > max(prefill_cfg.bs or (0,))
):
return False
from sglang.srt.configs.model_config import is_deepseek_v4, is_minimax_sparse
hf_config = model_config_of(server_args).hf_config
if is_deepseek_v4(hf_config) or is_minimax_sparse(hf_config):
return False
return True
def cutedsl_moe_max_num_tokens(server_args: Any) -> int:
"""Largest number of tokens a single forward routes through a CuteDSL
MoE layer on one (DP) rank. Single source of truth for both the
standard-allgather wrapper buffers and the FlashInfer A2A dispatcher
budget. Max over the prefill (max_prefill_tokens), piecewise-prefill
capture, and decode/verify bounds; num_tokens_per_req is
speculative_num_draft_tokens under speculative decoding, else 1.
"""
cfg = resolving_view(server_args)
if cfg.speculative_algorithm:
num_tokens_per_req = cfg.speculative_num_draft_tokens or 1
else:
num_tokens_per_req = 1
prefill_tokens = cfg.max_prefill_tokens
cg_config = cfg.cuda_graph_config
if cg_config is not None and cg_config.prefill.backend == Backend.TC_PIECEWISE:
prefill_tokens = max(prefill_tokens, cg_config.prefill.max_bs or 0)
decode_max_bs = (cg_config.decode.max_bs if cg_config is not None else 0) or 0
decode_tokens = decode_max_bs * num_tokens_per_req
return max(prefill_tokens, decode_tokens)
def max_prefill_buffer_tokens(server_args: Any) -> int:
"""Prefill-buffer ceiling: chunked_prefill_size, except PP dynamic
chunking can grow chunks toward max_prefill_tokens and probe at 1.25x."""
cfg = resolving_view(server_args)
chunked = (
cfg.chunked_prefill_size
if cfg.chunked_prefill_size and cfg.chunked_prefill_size > 0
else 0
)
tokens = chunked
if cfg.enable_dynamic_chunking and cfg.pp_size > 1 and chunked:
tokens = max(tokens, cfg.max_prefill_tokens or 0, math.ceil(chunked * 1.25))
return tokens
def mamba_cache_chunk_size(server_args: Any) -> int:
# For mamba cache with extra buffer, the chunk size is the max of FLA_CHUNK_SIZE
# (or mamba_chunk_size if it is defined in the model's config) and page_size.
# It is used to determine the caching point in a sequence during prefill.
# A pre-seeded `_mamba_cache_chunk_size` (fixtures supply one so a dummy
# model never loads an HF config) is honored as-is; otherwise the memo
# is only kept once the record is resolved, because `page_size` below
# is resolution-written.
from sglang.srt.arg_groups.overrides import model_config_of
if not hasattr(server_args, "_mamba_cache_chunk_size"):
try:
from sglang.kernels.ops.attention.fla.chunk_delta_h import (
CHUNK_SIZE as FLA_CHUNK_SIZE,
)
except ImportError:
# Must match sglang.kernels.ops.attention.fla.chunk_delta_h.CHUNK_SIZE
FLA_CHUNK_SIZE = 64
hf_config = model_config_of(server_args).hf_config
chunk_size = getattr(hf_config, "mamba_chunk_size", FLA_CHUNK_SIZE)
page_size = resolved_view(server_args).page_size
assert (
max(chunk_size, page_size) % min(chunk_size, page_size) == 0
), f"For SSM models, either chunk_size or page_size must be divisible by the other, got {chunk_size=}, {page_size=}"
if not getattr(server_args, "_resolution_finished", False):
return max(chunk_size, page_size)
server_args._mamba_cache_chunk_size = max(chunk_size, page_size)
return server_args._mamba_cache_chunk_size
def max_speculative_num_draft_tokens(server_args: Any) -> Optional[int]:
"""Return the maximum draft-token count speculative decoding may use.
Memoized only once the record is resolved: an answer computed off a raw
record describes inputs resolution is about to rewrite (auto speculative
sizing fills `speculative_num_draft_tokens` in), and a cache filled that
early would keep answering with it.
"""
cfg = resolving_view(server_args)
memo = server_args.__dict__.get("_max_speculative_num_draft_tokens")
if memo is not None:
return memo
if cfg.speculative_num_draft_tokens is None:
result = None
elif not cfg.speculative_adaptive:
result = cfg.speculative_num_draft_tokens
else:
from sglang.srt.speculative.adaptive_spec_params import (
resolve_candidate_steps_from_config,
)
candidate_steps = resolve_candidate_steps_from_config(
cfg_path=cfg.speculative_adaptive_config,
)
# TODO: adaptive spec currently requires topk=1, so each runtime
# state needs steps + 1 draft-token slots. Revisit this if topk>1
# is supported.
result = max(candidate_steps) + 1
if getattr(server_args, "_resolution_finished", False):
server_args._max_speculative_num_draft_tokens = result
return result
+11 -4
View File
@@ -11,6 +11,7 @@ from sglang.srt.arg_groups.overrides import (
declare_resolution, declare_resolution,
resolved_view, resolved_view,
resolving_view, resolving_view,
should_report_expert_balancedness,
) )
from sglang.srt.connector import ConnectorType from sglang.srt.connector import ConnectorType
from sglang.srt.environ import envs from sglang.srt.environ import envs
@@ -21,12 +22,14 @@ logger = logging.getLogger(__name__)
def handle_context_parallelism(server_args: Any): def handle_context_parallelism(server_args: Any):
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
if parse_connector_type(cfg.model_path) != ConnectorType.INSTANCE: if parse_connector_type(cfg.model_path) != ConnectorType.INSTANCE:
from sglang.srt.configs.model_config import is_deepseek_dsa from sglang.srt.configs.model_config import is_deepseek_dsa
from sglang.srt.layers.cp.utils import CP_V2_DEFAULT_MODEL_CLASSES from sglang.srt.layers.cp.utils import CP_V2_DEFAULT_MODEL_CLASSES
model_config = server_args.get_model_config() model_config = model_config_of(server_args)
hf_config = model_config.hf_config hf_config = model_config.hf_config
model_arch = hf_config.architectures[0] model_arch = hf_config.architectures[0]
if model_arch in CP_V2_DEFAULT_MODEL_CLASSES: if model_arch in CP_V2_DEFAULT_MODEL_CLASSES:
@@ -154,6 +157,10 @@ def handle_dcp_validation(server_args: Any):
def handle_data_parallelism(server_args: Any): def handle_data_parallelism(server_args: Any):
# The dp_size==1 resets moved to the resolution pipeline # The dp_size==1 resets moved to the resolution pipeline
# (arg_groups/overrides.py: _data_parallelism_defaults). # (arg_groups/overrides.py: _data_parallelism_defaults).
from sglang.srt.arg_groups.cuda_graph_hook import (
generate_prefill_cuda_graph_batch_sizes,
)
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
_data_parallelism_defaults, _data_parallelism_defaults,
@@ -213,8 +220,8 @@ def handle_data_parallelism(server_args: Any):
): ):
clamped = {"max_bs": cfg.chunked_prefill_size} clamped = {"max_bs": cfg.chunked_prefill_size}
if (Phase.PREFILL, "bs") not in server_args._cuda_graph_config_locked: if (Phase.PREFILL, "bs") not in server_args._cuda_graph_config_locked:
clamped["bs"] = server_args._generate_prefill_cuda_graph_batch_sizes( clamped["bs"] = generate_prefill_cuda_graph_batch_sizes(
clamped["max_bs"] server_args, clamped["max_bs"]
) )
declare_resolution( declare_resolution(
server_args, server_args,
@@ -636,7 +643,7 @@ def handle_expert_distribution_metrics(server_args: Any):
"prometheus, both." "prometheus, both."
) )
if server_args.should_report_expert_balancedness() and ( if should_report_expert_balancedness(server_args) and (
cfg.expert_distribution_recorder_mode is None cfg.expert_distribution_recorder_mode is None
): ):
declare_resolution( declare_resolution(
@@ -186,6 +186,7 @@ def _alias_bootstrap_port_to_api_port(server_args: ServerArgs) -> None:
def handle_encoder_disaggregation(server_args: Any): def handle_encoder_disaggregation(server_args: Any):
from sglang.srt.arg_groups.model_hook import handle_language_model_only from sglang.srt.arg_groups.model_hook import handle_language_model_only
from sglang.srt.arg_groups.overrides import model_config_of
from sglang.srt.arg_groups.validation_hook import validate_ib_devices from sglang.srt.arg_groups.validation_hook import validate_ib_devices
from sglang.srt.server_args import resolve_encoder_transfer_backend from sglang.srt.server_args import resolve_encoder_transfer_backend
@@ -223,7 +224,7 @@ def handle_encoder_disaggregation(server_args: Any):
) )
# Validate model type for encoder disaggregation # Validate model type for encoder disaggregation
hf_config = server_args.get_model_config().hf_config hf_config = model_config_of(server_args).hf_config
model_arch = hf_config.architectures[0] model_arch = hf_config.architectures[0]
if cfg.encoder_transfer_backend == "auto": if cfg.encoder_transfer_backend == "auto":
declare_resolution( declare_resolution(
+4 -2
View File
@@ -751,6 +751,8 @@ def handle_multimodal_feature_transport(server_args: Any):
may still auto-select CUDA VMM. The legacy CUDA IPC flag and environment may still auto-select CUDA VMM. The legacy CUDA IPC flag and environment
variable remain supported so existing deployments map to this policy. variable remain supported so existing deployments map to this policy.
""" """
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
requested_transport = cfg.mm_feature_transport requested_transport = cfg.mm_feature_transport
legacy_ipc_is_set = envs.SGLANG_USE_CUDA_IPC_TRANSPORT.is_set() legacy_ipc_is_set = envs.SGLANG_USE_CUDA_IPC_TRANSPORT.is_set()
@@ -785,7 +787,7 @@ def handle_multimodal_feature_transport(server_args: Any):
"--encoder-transfer-backend instead." "--encoder-transfer-backend instead."
) )
elif ( elif (
server_args.get_model_config().is_multimodal model_config_of(server_args).is_multimodal
and is_cuda() and is_cuda()
and cfg.disaggregation_mode == "null" and cfg.disaggregation_mode == "null"
): ):
@@ -803,7 +805,7 @@ def handle_multimodal_feature_transport(server_args: Any):
supports_cuda_vmm_feature_transport, supports_cuda_vmm_feature_transport,
) )
if supports_cuda_vmm_feature_transport(server_args.get_model_config()): if supports_cuda_vmm_feature_transport(model_config_of(server_args)):
requested_transport = "cuda_vmm" requested_transport = "cuda_vmm"
logger.info( logger.info(
"Multimodal feature transport auto-resolved to " "Multimodal feature transport auto-resolved to "
@@ -337,11 +337,12 @@ def _handle_dflash(server_args: ServerArgs) -> None:
def _target_checkpoint_bundles_dspark_draft(server_args: ServerArgs) -> bool: def _target_checkpoint_bundles_dspark_draft(server_args: ServerArgs) -> bool:
from sglang.srt.arg_groups.overrides import model_config_of
from sglang.srt.speculative.dspark_components.dspark_config import ( from sglang.srt.speculative.dspark_components.dspark_config import (
checkpoint_bundles_dspark_draft, checkpoint_bundles_dspark_draft,
) )
return checkpoint_bundles_dspark_draft(server_args.get_model_config().hf_config) return checkpoint_bundles_dspark_draft(model_config_of(server_args).hf_config)
def _handle_dspark(server_args: ServerArgs) -> None: def _handle_dspark(server_args: ServerArgs) -> None:
@@ -662,6 +663,8 @@ def _handle_frozen_kv_mtp(server_args: ServerArgs) -> None:
def _handle_eagle_family(server_args: ServerArgs) -> None: def _handle_eagle_family(server_args: ServerArgs) -> None:
from sglang.srt.arg_groups.overrides import model_config_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
attention_backends_of, attention_backends_of,
@@ -706,7 +709,7 @@ def _handle_eagle_family(server_args: ServerArgs) -> None:
"eagle speculative decoding." "eagle speculative decoding."
) )
model_arch = server_args.get_model_config().hf_config.architectures[0] model_arch = model_config_of(server_args).hf_config.architectures[0]
if model_arch in [ if model_arch in [
"DeepseekV32ForCausalLM", "DeepseekV32ForCausalLM",
"DeepseekV3ForCausalLM", "DeepseekV3ForCausalLM",
@@ -9,6 +9,7 @@ import os
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
resolved_view,
resolving_view, resolving_view,
) )
from sglang.srt.distributed.device_communicators.mooncake_transfer_engine import ( from sglang.srt.distributed.device_communicators.mooncake_transfer_engine import (
@@ -389,7 +390,7 @@ def validate_ib_devices(server_args: Any, device_str: Optional[str]) -> Optional
def validate_experimental_sgl_marlin(server_args: Any): def validate_experimental_sgl_marlin(server_args: Any):
view = server_args._resolved() view = resolved_view(server_args)
if view.moe_runner_backend != "experimental_sgl_marlin": if view.moe_runner_backend != "experimental_sgl_marlin":
return return
+7 -2
View File
@@ -48,7 +48,10 @@ import torch
import uvloop import uvloop
import zmq import zmq
from sglang.srt.arg_groups.overrides import resolving_view from sglang.srt.arg_groups.overrides import (
resolved_view,
resolving_view,
)
from sglang.srt.elastic_ep.expert_backup_manager import run_expert_backup_manager from sglang.srt.elastic_ep.expert_backup_manager import run_expert_backup_manager
from sglang.srt.entrypoints.engine_info_bootstrap_server import ( from sglang.srt.entrypoints.engine_info_bootstrap_server import (
EngineInfoBootstrapServer, EngineInfoBootstrapServer,
@@ -1627,6 +1630,8 @@ class Engine(EngineScoreMixin, EngineBase):
def _set_envs_and_config(server_args: ServerArgs): def _set_envs_and_config(server_args: ServerArgs):
from sglang.srt.arg_groups.overrides import attention_backends_of
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
# Set global environments # Set global environments
# MNNVL fabric (GB200/GB300) multi-node: cross-node NVLink needs NCCL's # MNNVL fabric (GB200/GB300) multi-node: cross-node NVLink needs NCCL's
@@ -1678,7 +1683,7 @@ def _set_envs_and_config(server_args: ServerArgs):
# Check flashinfer version # Check flashinfer version
if not get_bool_env_var("SGLANG_SKIP_SGL_KERNEL_VERSION_CHECK"): if not get_bool_env_var("SGLANG_SKIP_SGL_KERNEL_VERSION_CHECK"):
if "flashinfer" in cfg.get_attention_backends(): if "flashinfer" in attention_backends_of(resolved_view(cfg)):
assert_pkg_version( assert_pkg_version(
"flashinfer_python", "flashinfer_python",
"0.6.17", "0.6.17",
@@ -38,6 +38,7 @@ import einops
import torch import torch
import torch.distributed import torch.distributed
from sglang.srt.arg_groups.overrides import should_report_expert_balancedness
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.model_executor.forward_batch_info import ForwardBatch from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.observability.metrics_collector import ( from sglang.srt.observability.metrics_collector import (
@@ -179,7 +180,7 @@ class _ExpertDistributionRecorderReal(ExpertDistributionRecorder):
for k in self._accumulator.get_single_pass_gatherer_keys() for k in self._accumulator.get_single_pass_gatherer_keys()
} }
if server_args.should_report_expert_balancedness(): if should_report_expert_balancedness(server_args):
logger.info( logger.info(
"ExpertDistributionRecorder auto start record since " "ExpertDistributionRecorder auto start record since "
f"expert_balancedness_report_mode={get_exec().moe.expert_balancedness_report_mode}" f"expert_balancedness_report_mode={get_exec().moe.expert_balancedness_report_mode}"
@@ -718,7 +719,7 @@ class _UtilizationRateAccumulatorMixin(_Accumulator):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._enable = self._server_args.should_report_expert_balancedness() self._enable = should_report_expert_balancedness(self._server_args)
if self._enable: if self._enable:
self.window_sizes = EPLB_BALANCEDNESS_WINDOW_SIZES self.window_sizes = EPLB_BALANCEDNESS_WINDOW_SIZES
@@ -44,7 +44,7 @@ def set_default_server_args(args: "ServerArgs"):
""" """
Set default server arguments for NPU backend. Set default server arguments for NPU backend.
""" """
from sglang.srt.arg_groups.overrides import resolving_view from sglang.srt.arg_groups.overrides import resolving_view, use_mla_backend
cfg = resolving_view(args) cfg = resolving_view(args)
@@ -148,7 +148,7 @@ def set_default_server_args(args: "ServerArgs"):
"set_default_server_args", "set_default_server_args",
hicache_io_backend="kernel_ascend", hicache_io_backend="kernel_ascend",
) )
if args.use_mla_backend(): if use_mla_backend(args):
declare_resolution( declare_resolution(
args, args,
"set_default_server_args", "set_default_server_args",
@@ -74,7 +74,12 @@ def create_trtllm_mla_backend(runner):
if not runner.use_mla_backend: if not runner.use_mla_backend:
raise ValueError("trtllm_mla backend can only be used with MLA models.") raise ValueError("trtllm_mla backend can only be used with MLA models.")
if get_parallel().dcp_enabled and get_spec().speculative_algorithm is not None: if get_parallel().dcp_enabled and get_spec().speculative_algorithm is not None:
_, decode_backend = runner.server_args.get_attention_backends() from sglang.srt.arg_groups.overrides import (
attention_backends_of,
resolved_view,
)
_, decode_backend = attention_backends_of(resolved_view(runner.server_args))
if decode_backend == "trtllm_mla": if decode_backend == "trtllm_mla":
raise ValueError( raise ValueError(
"trtllm_mla cannot serve decode context parallelism with speculative " "trtllm_mla cannot serve decode context parallelism with speculative "
@@ -56,7 +56,7 @@ from sglang.srt.layers.attention.trtllm_mla_backend import (
) )
from sglang.srt.layers.dcp.layout import get_dcp_lens from sglang.srt.layers.dcp.layout import get_dcp_lens
from sglang.srt.layers.logits_processor import get_in_autotune_dummy_run from sglang.srt.layers.logits_processor import get_in_autotune_dummy_run
from sglang.srt.runtime_context import get_parallel from sglang.srt.runtime_context import get_parallel, max_speculative_num_draft_tokens
from sglang.srt.utils import is_flashinfer_available, is_tokenspeed_mla_available from sglang.srt.utils import is_flashinfer_available, is_tokenspeed_mla_available
if is_flashinfer_available(): if is_flashinfer_available():
@@ -145,9 +145,7 @@ class TokenspeedMLABackend(TRTLLMMLABackend):
self.device, self.device,
self.num_q_heads, self.num_q_heads,
self.kv_lora_rank, self.kv_lora_rank,
max_q_len=( max_q_len=(max_speculative_num_draft_tokens() or 1),
model_runner.server_args.max_speculative_num_draft_tokens or 1
),
) )
# Pre-JIT the prefill kernel variants. Each cute.compile takes 1-2 # Pre-JIT the prefill kernel variants. Each cute.compile takes 1-2
+5 -4
View File
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional
import torch import torch
from sglang.srt.arg_groups.overrides import resolved_view
from sglang.srt.layers.cp.base import get_cp_strategy from sglang.srt.layers.cp.base import get_cp_strategy
from sglang.srt.layers.cp.padding import get_cp_padding_align_size from sglang.srt.layers.cp.padding import get_cp_padding_align_size
from sglang.srt.layers.cp.utils import ( from sglang.srt.layers.cp.utils import (
@@ -42,11 +43,11 @@ if TYPE_CHECKING:
def supports_prefill_cp_bcg(server_args: ServerArgs) -> bool: def supports_prefill_cp_bcg(server_args: ServerArgs) -> bool:
"""Return whether the selected prefill-CP configuration supports BCG.""" """Return whether the selected prefill-CP configuration supports BCG."""
from sglang.srt.arg_groups.overrides import resolving_view from sglang.srt.arg_groups.overrides import attention_backends_of, resolving_view
cfg = resolving_view(server_args) cfg = resolving_view(server_args)
resolved = server_args._resolved() resolved = resolved_view(server_args)
prefill_attention_backend, _ = server_args._resolved_attention_backends() prefill_attention_backend, _ = attention_backends_of(resolved_view(server_args))
return ( return (
cfg.enable_prefill_cp cfg.enable_prefill_cp
and resolved.attn_cp_size == cfg.tp_size and resolved.attn_cp_size == cfg.tp_size
@@ -64,7 +65,7 @@ def filter_prefill_cp_bcg_capture_num_tokens(
capture_num_tokens: list[int], server_args: ServerArgs capture_num_tokens: list[int], server_args: ServerArgs
) -> list[int]: ) -> list[int]:
"""Keep only token buckets where the zigzag CP strategy can run.""" """Keep only token buckets where the zigzag CP strategy can run."""
min_num_tokens = server_args._resolved().attn_cp_size * 2 min_num_tokens = resolved_view(server_args).attn_cp_size * 2
filtered = [size for size in capture_num_tokens if size >= min_num_tokens] filtered = [size for size in capture_num_tokens if size >= min_num_tokens]
if not filtered: if not filtered:
raise ValueError( raise ValueError(
@@ -76,8 +76,10 @@ def create_kt_config_from_server_args(
if server_args.kt_weight_path is None: if server_args.kt_weight_path is None:
return None return None
from sglang.srt.arg_groups.overrides import model_config_of
num_layers = getattr( num_layers = getattr(
server_args.get_model_config().hf_config, "num_hidden_layers", None model_config_of(server_args).hf_config, "num_hidden_layers", None
) )
return KTConfig( return KTConfig(
@@ -8,7 +8,7 @@ from typing import Optional
import torch import torch
from sglang.srt.arg_groups.overrides import resolving_view from sglang.srt.arg_groups.overrides import cutedsl_moe_max_num_tokens, resolving_view
from sglang.srt.layers.communicator import ( from sglang.srt.layers.communicator import (
CommunicateWithAllReduceAndLayerNormFn, CommunicateWithAllReduceAndLayerNormFn,
LayerCommunicator, LayerCommunicator,
@@ -37,7 +37,7 @@ def resolve_max_m(model_runner) -> int:
decode_config = server_args.cuda_graph_config.decode decode_config = server_args.cuda_graph_config.decode
prefill_config = server_args.cuda_graph_config.prefill prefill_config = server_args.cuda_graph_config.prefill
candidates = [ candidates = [
server_args.cutedsl_moe_max_num_tokens(), cutedsl_moe_max_num_tokens(model_runner.server_args),
model_runner.max_running_requests, model_runner.max_running_requests,
decode_config.max_bs, decode_config.max_bs,
prefill_config.max_bs, prefill_config.max_bs,
@@ -29,8 +29,10 @@ logger = logging.getLogger(__name__)
def is_post_capture_kv_active( def is_post_capture_kv_active(
*, server_args: ServerArgs, is_draft_worker: bool *, server_args: ServerArgs, is_draft_worker: bool
) -> bool: ) -> bool:
from sglang.srt.arg_groups.overrides import post_capture_kv_sizing_planned
return ( return (
server_args.post_capture_kv_sizing_planned() post_capture_kv_sizing_planned(server_args)
and current_platform.is_cuda() and current_platform.is_cuda()
and not is_draft_worker and not is_draft_worker
) )
@@ -44,6 +44,7 @@ from sglang.srt.runtime_context import (
get_parallel, get_parallel,
get_schedule, get_schedule,
get_spec, get_spec,
max_speculative_num_draft_tokens,
) )
from sglang.srt.utils.common import ( from sglang.srt.utils.common import (
ceil_align, ceil_align,
@@ -780,9 +781,7 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
self.swa_page_size = cfg.window_size self.swa_page_size = cfg.window_size
self.swa_ratio = get_schedule().swa_full_tokens_ratio self.swa_ratio = get_schedule().swa_full_tokens_ratio
self.is_speculative = get_spec().speculative_algorithm is not None self.is_speculative = get_spec().speculative_algorithm is not None
self.online_c128_mtp_max_draft_tokens = ( self.online_c128_mtp_max_draft_tokens = max_speculative_num_draft_tokens() or 0
kvc.server_args.max_speculative_num_draft_tokens or 0
)
self.requested_max_running_requests_per_worker = ( self.requested_max_running_requests_per_worker = (
get_schedule().max_running_requests // kvc.ps.attn_dp_size get_schedule().max_running_requests // kvc.ps.attn_dp_size
if get_schedule().max_running_requests is not None if get_schedule().max_running_requests is not None
@@ -814,7 +813,7 @@ class DSV4PoolConfigurator(MemoryPoolConfigurator):
if self.is_speculative: if self.is_speculative:
# Ring is sized once here, so it must serve the largest adaptive tier. # Ring is sized once here, so it must serve the largest adaptive tier.
self._assert_ring_serves_draft_tokens( self._assert_ring_serves_draft_tokens(
kvc.server_args.max_speculative_num_draft_tokens or 0 max_speculative_num_draft_tokens() or 0
) )
self.bytes_per_full_token = self._get_bytes_per_full_token() self.bytes_per_full_token = self._get_bytes_per_full_token()
@@ -30,7 +30,9 @@ from sglang.srt.runtime_context import (
get_disagg, get_disagg,
get_exec, get_exec,
get_model, get_model,
get_schedule,
get_spec, get_spec,
max_prefill_buffer_tokens,
) )
from sglang.srt.utils import empty_context, log_info_on_rank0 from sglang.srt.utils import empty_context, log_info_on_rank0
@@ -342,9 +344,7 @@ def maybe_flashinfer_autotune_extend(
mr = runner.model_runner mr = runner.model_runner
# Prefer the per-rank scheduler buffer while preserving the legacy ceiling # Prefer the per-rank scheduler buffer while preserving the legacy ceiling
# when chunked prefill is disabled. # when chunked prefill is disabled.
num_tokens = ( num_tokens = max_prefill_buffer_tokens() or get_schedule().max_prefill_tokens
mr.server_args.max_prefill_buffer_tokens() or mr.server_args.max_prefill_tokens
)
if num_tokens <= (decode_num_tokens or 0): if num_tokens <= (decode_num_tokens or 0):
return # decode-shaped autotune already covered these buckets return # decode-shaped autotune already covered these buckets
is_pd_prefill_target = ( is_pd_prefill_target = (
@@ -15,6 +15,8 @@ import sys
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from sglang.srt.arg_groups.overrides import declare_resolution
METADATA_FORMAT_VERSION = 3 METADATA_FORMAT_VERSION = 3
GGUF_SHARD_SUFFIX_RE = re.compile(r"-\d{5}-of-\d{5}\.gguf$") GGUF_SHARD_SUFFIX_RE = re.compile(r"-\d{5}-of-\d{5}\.gguf$")
DEEPSEEK_METADATA_FORMAT_VERSION = 4 DEEPSEEK_METADATA_FORMAT_VERSION = 4
@@ -215,7 +217,8 @@ def prepare_raw_kimi_server_args(
model_path, model_path,
tokenizer_dir=tokenizer_path, tokenizer_dir=tokenizer_path,
) )
server_args._declare( declare_resolution(
server_args,
"prepare_raw_kimi_server_args", "prepare_raw_kimi_server_args",
model_path=str(assets["model_dir"]), model_path=str(assets["model_dir"]),
tokenizer_path=str(assets["model_dir"]), tokenizer_path=str(assets["model_dir"]),
@@ -538,7 +541,8 @@ def prepare_raw_deepseek_server_args(
config_sha256 = _deepseek_digest( config_sha256 = _deepseek_digest(
model_value.get("config_sha256"), "model.config_sha256" model_value.get("config_sha256"), "model.config_sha256"
) )
server_args._declare( declare_resolution(
server_args,
"prepare_raw_deepseek_server_args", "prepare_raw_deepseek_server_args",
model_path=str(model_config.parent), model_path=str(model_config.parent),
tokenizer_path=str(model_config.parent), tokenizer_path=str(model_config.parent),
+17 -11
View File
@@ -1672,7 +1672,7 @@ def max_prefill_buffer_tokens() -> int:
Every input is a published leaf (``schedule`` plus the configured PP size), Every input is a published leaf (``schedule`` plus the configured PP size),
so this derives from the bags and follows a post-publish override; so this derives from the bags and follows a post-publish override;
``ServerArgs.max_prefill_buffer_tokens`` is the pre-publish equivalent and ``overrides.max_prefill_buffer_tokens`` is the pre-publish equivalent and
``TestDerivedPredicatesAgreeAcrossTiers`` pins the two equal. ``TestDerivedPredicatesAgreeAcrossTiers`` pins the two equal.
""" """
import math import math
@@ -1726,17 +1726,19 @@ def pre_capture_activation_reserve_mb(gpu_mem: float | None) -> float:
# --- Derived config accessors ------------------------------------------------ # --- Derived config accessors ------------------------------------------------
# #
# A few values are computed from several config fields plus the HF config, so # A few values are computed from several config fields plus the HF config, so
# they are ``ServerArgs`` members rather than namespace leaves. Business code # they are derived accessors rather than namespace leaves. Business code must
# must not reach for the startup record to get them: these accessors are the # not reach for the startup record to get them: these accessors are the named
# named home, and this module — which owns the slot — is the only place that # home, and this module — which owns the slot — is the only place that reads
# reads it. Each one keeps the member's exact semantics, including which model # it. Each one keeps the pre-publish function's exact semantics, including which model
# config it derives from (always the process's, i.e. the target's). # config it derives from (always the process's, i.e. the target's).
def mamba_cache_chunk_size() -> int: def mamba_cache_chunk_size() -> int:
"""The caching point granularity for mamba state: ``max(the model's mamba """The caching point granularity for mamba state: ``max(the model's mamba
chunk size, page_size)``. Cached on the config after the first call.""" chunk size, page_size)``. Cached on the config after the first call."""
return get_server_args().mamba_cache_chunk_size from sglang.srt.arg_groups.overrides import mamba_cache_chunk_size as _of
return _of(get_server_args())
def mamba_checkpoint_grid(tree_page: int) -> int: def mamba_checkpoint_grid(tree_page: int) -> int:
@@ -1759,7 +1761,7 @@ def max_speculative_num_draft_tokens() -> int | None:
"""The largest draft-token count speculative decoding may use. """The largest draft-token count speculative decoding may use.
All three inputs are ``spec`` leaves, so this derives from the bags and All three inputs are ``spec`` leaves, so this derives from the bags and
follows a post-publish override; ``ServerArgs.max_speculative_num_draft_tokens`` follows a post-publish override; ``overrides.max_speculative_num_draft_tokens``
is the pre-publish equivalent. Adaptive spec resolves the count from its is the pre-publish equivalent. Adaptive spec resolves the count from its
candidate-step table instead of the flat field. candidate-step table instead of the flat field.
""" """
@@ -1788,7 +1790,9 @@ def _adaptive_draft_token_bound(cfg_path: str | None) -> int:
def uses_mla_backend() -> bool: def uses_mla_backend() -> bool:
"""Whether this process's model runs the MLA attention path.""" """Whether this process's model runs the MLA attention path."""
return get_server_args().use_mla_backend() from sglang.srt.arg_groups.overrides import use_mla_backend
return use_mla_backend(get_server_args())
def attention_backends() -> tuple: def attention_backends() -> tuple:
@@ -1796,7 +1800,7 @@ def attention_backends() -> tuple:
back to ``attention_backend``. back to ``attention_backend``.
All three inputs are ``exec.kernel`` leaves, so this derives from the bags All three inputs are ``exec.kernel`` leaves, so this derives from the bags
and follows a post-publish override; ``ServerArgs.get_attention_backends`` and follows a post-publish override; ``overrides.attention_backends_of``
is the pre-publish equivalent the resolution pipeline uses. A built runner is the pre-publish equivalent the resolution pipeline uses. A built runner
stamps its own resolved pair (``ModelRunner.prefill_attention_backend_str``); stamps its own resolved pair (``ModelRunner.prefill_attention_backend_str``);
read that when there is a runner in hand. read that when there is a runner in hand.
@@ -1810,7 +1814,9 @@ def attention_backends() -> tuple:
def process_model_config(): def process_model_config():
"""The process's ``ModelConfig`` (built once from the published config).""" """The process's ``ModelConfig`` (built once from the published config)."""
return get_server_args().get_model_config() from sglang.srt.arg_groups.overrides import model_config_of
return model_config_of(get_server_args())
def cutedsl_moe_max_num_tokens() -> int: def cutedsl_moe_max_num_tokens() -> int:
@@ -1818,7 +1824,7 @@ def cutedsl_moe_max_num_tokens() -> int:
Every input is a published leaf (``spec``, ``schedule``, ``exec.graph``), so Every input is a published leaf (``spec``, ``schedule``, ``exec.graph``), so
this derives from the bags and follows a post-publish override; this derives from the bags and follows a post-publish override;
``ServerArgs.cutedsl_moe_max_num_tokens`` is the pre-publish equivalent the ``overrides.cutedsl_moe_max_num_tokens`` is the pre-publish equivalent the
resolution pipeline uses. Max over the prefill bound, the piecewise-prefill resolution pipeline uses. Max over the prefill bound, the piecewise-prefill
capture, and the decode/verify bound. capture, and the decode/verify bound.
""" """
+1 -616
View File
@@ -37,11 +37,8 @@ import argparse
import copy import copy
import dataclasses import dataclasses
import functools import functools
import glob
import json import json
import logging import logging
import math
import os
import tempfile import tempfile
import uuid import uuid
from typing import Any, Callable, Dict, List, Literal, Optional, Union from typing import Any, Callable, Dict, List, Literal, Optional, Union
@@ -56,11 +53,9 @@ from sglang.srt.arg_groups.argparse_actions import (
LoRAPathAction, LoRAPathAction,
) )
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
attention_backends_of,
mamba_extra_buffer_lazy_of, mamba_extra_buffer_lazy_of,
mamba_extra_buffer_of, mamba_extra_buffer_of,
remote_instance_transfer_engine_of, remote_instance_transfer_engine_of,
resolved_view,
resolving_view, resolving_view,
) )
from sglang.srt.environ import envs from sglang.srt.environ import envs
@@ -69,23 +64,14 @@ from sglang.srt.lora.lora_registry import LoRARef
from sglang.srt.model_executor.cuda_graph_config import ( from sglang.srt.model_executor.cuda_graph_config import (
Backend, Backend,
CudaGraphConfig, CudaGraphConfig,
Phase,
parse_cuda_graph_config_arg, parse_cuda_graph_config_arg,
with_phase,
) )
from sglang.srt.parser.reasoning_parser import ReasoningParser from sglang.srt.parser.reasoning_parser import ReasoningParser
from sglang.srt.platforms import current_platform
from sglang.srt.speculative.decoupled_spec_io import DecoupledSpecIpcConfig from sglang.srt.speculative.decoupled_spec_io import DecoupledSpecIpcConfig
from sglang.srt.utils.common import ( from sglang.srt.utils.common import (
LORA_TARGET_ALL_MODULES, LORA_TARGET_ALL_MODULES,
SUPPORTED_LORA_TARGET_MODULES, SUPPORTED_LORA_TARGET_MODULES,
human_readable_int, human_readable_int,
is_flashinfer_available,
is_hip,
is_hopper_with_cuda_12_3,
is_mps,
is_no_spec_infer_or_topk_one,
is_sm100_supported,
json_list_type, json_list_type,
nullable_str, nullable_str,
) )
@@ -3745,7 +3731,7 @@ class ServerArgs:
# Everything outside the fields, enumerated from the instance: the raw # Everything outside the fields, enumerated from the instance: the raw
# snapshot, the stash, and what resolution memoized -- including the # snapshot, the stash, and what resolution memoized -- including the
# `get_model_config()` memo, which the copy carries over rather than # model-configuration memo, which the copy carries over rather than
# rebuild. # rebuild.
field_names = {field.name for field in dataclasses.fields(self)} field_names = {field.name for field in dataclasses.fields(self)}
for name, value in vars(self).items(): for name, value in vars(self).items():
@@ -3763,91 +3749,10 @@ class ServerArgs:
object.__setattr__(replacement, "_resolution_finished", True) object.__setattr__(replacement, "_resolution_finished", True)
return replacement return replacement
def _declare(self, source: str, **fields: Any) -> None:
"""This record's handlers declaring their resolution writes.
See ``arg_groups.overrides.declare_resolution``, which the hooks these
handlers call reach directly.
"""
from sglang.srt.arg_groups.overrides import declare_resolution
declare_resolution(self, source, **fields)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# CUDA graph configuration resolution # CUDA graph configuration resolution
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _apply_cuda_graph_disaggregation_roles(self):
cfg = resolving_view(self)
if cfg.disaggregation_mode == "prefill":
if (Phase.DECODE, "backend") not in self._cuda_graph_config_locked:
self._declare(
"_apply_cuda_graph_disaggregation_roles",
cuda_graph_config=with_phase(
cfg.cuda_graph_config, Phase.DECODE, backend=Backend.DISABLED
),
)
elif cfg.disaggregation_mode == "decode":
if (Phase.PREFILL, "backend") not in self._cuda_graph_config_locked:
self._declare(
"_apply_cuda_graph_disaggregation_roles",
cuda_graph_config=with_phase(
cfg.cuda_graph_config, Phase.PREFILL, backend=Backend.DISABLED
),
)
def post_capture_kv_sizing_planned(self) -> bool:
"""Whether the mem_fraction heuristic may skip the graph reserve; must be
False for any config the runtime won't post-capture-size, else it gets an
under-reserved fraction."""
cfg = resolving_view(self)
# use_mla_backend is a method at args time but ModelRunner overwrites it
# with a bool on global_server_args (see the FIXME there) -- handle both.
use_mla = self.use_mla_backend
mla_enabled = use_mla() if callable(use_mla) else use_mla
if not envs.SGLANG_ENABLE_POST_CAPTURE_KV_SIZING.get():
return False
if cfg.device != "cuda":
return False
if cfg.dcp_size != 1:
return False
if mla_enabled:
return False
if cfg.kv_cache_dtype == "fp4_e2m1":
return False
if cfg.prefill_only_disable_kv_cache:
return False
if cfg.enable_memory_saver:
return False
if envs.SGLANG_MOONCAKE_CUSTOM_MEM_POOL.get() is not None:
return False
if (
cfg.disaggregation_mode != "prefill"
and cfg.cuda_graph_config.decode.backend == Backend.DISABLED
):
return False
if cfg.disaggregation_mode != "decode":
prefill_cfg = cfg.cuda_graph_config.prefill
# We can only skip eager activation headroom when the largest
# prefill forward batch size is already graph-captured. Otherwise,
# an eager forward will need more memory and lead to OOM.
if (
prefill_cfg.backend == Backend.DISABLED
or cfg.chunked_prefill_size <= 0
or self.max_prefill_buffer_tokens() > max(prefill_cfg.bs or (0,))
):
return False
from sglang.srt.configs.model_config import is_deepseek_v4, is_minimax_sparse
hf_config = self.get_model_config().hf_config
if is_deepseek_v4(hf_config) or is_minimax_sparse(hf_config):
return False
return True
def pre_capture_activation_reserve_mb(self, gpu_mem: Optional[float]) -> float: def pre_capture_activation_reserve_mb(self, gpu_mem: Optional[float]) -> float:
# Runtime activation working-set reserve for eager decode above the captured # Runtime activation working-set reserve for eager decode above the captured
# max_bs and transient prefill/logits; also covers fixed state caches. # max_bs and transient prefill/logits; also covers fixed state caches.
@@ -3870,347 +3775,13 @@ class ServerArgs:
reserved_mem = max(reserved_mem, 10 * 1024) reserved_mem = max(reserved_mem, 10 * 1024)
return reserved_mem return reserved_mem
def reserve_for_graph_mb(self) -> float:
cfg = resolving_view(self)
decode_cuda_graph_config = cfg.cuda_graph_config.decode
prefill_cuda_graph_config = cfg.cuda_graph_config.prefill
reserved_mem = 0.0
if (
cfg.disaggregation_mode != "prefill"
and decode_cuda_graph_config.backend != Backend.DISABLED
):
reserved_mem += decode_cuda_graph_config.max_bs * 2
if (
self._resolved().enable_dp_attention
and cfg.disaggregation_mode != "prefill"
):
# DP attention needs more padding for some operations, and much more for large
# cuda graph max bs (torch allocator / implementation inefficiencies).
reserved_mem += decode_cuda_graph_config.max_bs * cfg.dp_size * 3
if decode_cuda_graph_config.max_bs > 300:
reserved_mem += decode_cuda_graph_config.max_bs * cfg.dp_size * 1.5
if (
cfg.disaggregation_mode != "decode"
and prefill_cuda_graph_config.backend != Backend.DISABLED
):
if not self.use_mla_backend():
# Only non-torch memory is counted; torch memory is reused by cuda graph capture.
reserved_mem += len(prefill_cuda_graph_config.bs) * 8
else:
# MLA backend overhead is much higher than expected with fa3.
reserved_mem += 1.5 * 1024
if (
prefill_cuda_graph_config.backend == Backend.BREAKABLE
and resolved_view(self).moe_a2a_backend == "deepep"
):
# Prefill-BCG DeepEP delta (bridge pool + NVL first-touch
# during capture); decode-side DeepEP is a baseline cost.
reserved_mem += 1 * 1024
return reserved_mem
def reserve_for_deepep_a2a_mb(self) -> float:
# DeepEP all-to-all buffers captured in the decode graph are real extra
# allocations, reserved on top of the floor.
cfg = resolving_view(self)
decode_cuda_graph_config = cfg.cuda_graph_config.decode
if (
cfg.disaggregation_mode != "prefill"
and decode_cuda_graph_config.backend != Backend.DISABLED
and resolved_view(self).moe_a2a_backend == "deepep"
):
return 2 * 1024
return 0.0
def _generate_decode_cuda_graph_batch_sizes(self, max_bs: int):
"""
Generate the list of batch sizes for CUDA graph capture based on max_bs.
This integrates the logic from cuda_graph_runner.py.
"""
cfg = resolving_view(self)
# Handle disable_cuda_graph_padding as the first condition for both spec and non-spec
if cfg.disable_cuda_graph_padding:
capture_bs = list(range(1, max_bs + 1))
elif cfg.speculative_algorithm is None:
# Normal case:
capture_bs = (
[1, 2, 4, 8, 12]
+ list(range(16, 257, 8))
+ list(range(272, 512, 16))
+ list(range(512, max_bs + 1, 32))
)
else:
# Spec decoding case: less padding for smaller batch sizes
capture_bs = (
list(range(1, 9, 1))
+ list(range(10, 33, 2))
+ list(range(40, 65, 4))
+ list(range(72, 257, 8))
+ list(range(272, max_bs + 1, 16))
)
capture_bs = [bs for bs in capture_bs if bs <= max_bs]
if max_bs not in capture_bs:
capture_bs.append(max_bs)
return capture_bs
def _generate_cpu_graph_batch_sizes(self):
"""
Generate the list of batch sizes for CPU graph capture based on torch_compile_max_bs.
"""
cfg = resolving_view(self)
if cfg.disable_cuda_graph_padding:
capture_bs = list(range(1, cfg.torch_compile_max_bs + 1))
else:
capture_bs = sorted(
set().union(
range(1, 17),
range(18, 31, 2),
range(32, 81, 4),
range(84, cfg.torch_compile_max_bs + 1, 8),
{cfg.torch_compile_max_bs},
)
)
capture_bs = [bs for bs in capture_bs if bs <= cfg.torch_compile_max_bs]
return capture_bs
def _generate_prefill_cuda_graph_batch_sizes(self, max_bs: int):
"""
Generate the list of batch sizes for prefill CUDA graph capture
based on max_bs. For tc_piecewise prefill, bs carries the
captured token count (one shape knob per phase).
"""
capture_sizes = (
list(range(4, 33, 4))
+ list(range(48, 257, 16))
+ list(range(288, 513, 32))
+ list(range(576, 1024 + 1, 64))
+ list(range(1280, 4096 + 1, 256))
+ list(range(4608, max_bs + 1, 512))
)
capture_sizes = [s for s in capture_sizes if s <= max_bs]
return capture_sizes
def _set_default_dsa_kv_cache_dtype(self, major: int, quantization: str) -> None:
# Moved to the resolution pipeline (arg_groups/overrides.py:
# _dsa_kv_cache_dtype_default), invoked here at its legacy slot.
from sglang.srt.arg_groups.overrides import (
_dsa_kv_cache_dtype_default,
run_post_process_pass,
)
run_post_process_pass(self, _dsa_kv_cache_dtype_default)
def _set_default_dsa_backends(self, major: int) -> None:
# Moved to the resolution pipeline (arg_groups/overrides.py:
# _dsa_split_backend_resolution), invoked here at its legacy slot.
from sglang.srt.arg_groups.overrides import (
_dsa_split_backend_resolution,
run_post_process_pass,
)
run_post_process_pass(self, _dsa_split_backend_resolution)
def _support_mamba_cache_extra_buffer(self, model_arch: str): def _support_mamba_cache_extra_buffer(self, model_arch: str):
from sglang.srt.arg_groups.overrides import supports_mamba_cache_extra_buffer from sglang.srt.arg_groups.overrides import supports_mamba_cache_extra_buffer
return supports_mamba_cache_extra_buffer(self, model_arch) return supports_mamba_cache_extra_buffer(self, model_arch)
def _get_default_attn_backend(self, use_mla_backend: bool, model_config):
"""
Auto select the fastest attention backend.
1. Models with MHA Architecture (e.g: Llama, QWen)
1.1 We will turn on FA3 on hopper unless user use spec decode with topk > 1 or page_size > 1.
1.2 Use trtllm_mha for SM100/SM103 (Blackwell B200/GB200/B300) excluding spec with topk > 1.
Note: trtllm_mha does not support SM120, which will fall back to flashinfer.
1.3 In other cases, we will use flashinfer if available, otherwise use triton.
2. Models with MLA Architecture and using FA3
2.1 We will use FA3 backend on hopper.
2.2 We will use Flashinfer backend on blackwell.
2.3 Otherwise, we will use triton backend.
"""
cfg = resolving_view(self)
# OOT platforms provide their own default attention backend.
if current_platform.is_out_of_tree():
return current_platform.get_default_attention_backend()
# Whisper requires flashinfer for cross-attention CUDA graph support.
if "WhisperForConditionalGeneration" in (
model_config.hf_config.architectures or []
):
return "flashinfer"
if not use_mla_backend:
# MHA architecture
if is_hopper_with_cuda_12_3() and is_no_spec_infer_or_topk_one(
resolved_view(self)
):
# Note: flashinfer 0.6.1 caused performance regression on Hopper attention kernel
# Before the kernel is fixed, we choose fa3 as the default backend on Hopper MHA
# ref: https://github.com/sgl-project/sglang/issues/17411
return "fa3"
elif (
is_sm100_supported()
and is_no_spec_infer_or_topk_one(resolved_view(self))
and (
cfg.speculative_algorithm is None
or cfg.speculative_eagle_topk is not None
)
):
# trtllm_mha requires equal K/V row widths; fa4 carries
# v_head_dim through.
if model_config.has_asymmetric_kv:
return "fa4"
return "trtllm_mha"
elif is_hip():
return "aiter"
elif is_mps():
return "torch_native"
else:
# FlashInfer does not support attention sinks.
if is_flashinfer_available() and not model_config.has_attention_sinks:
return "flashinfer"
return "triton"
else:
# MLA architecture
if is_hopper_with_cuda_12_3():
return "fa3"
elif is_sm100_supported():
return "flashinfer"
elif is_hip():
head_num = model_config.get_num_kv_heads(self.tp_size)
# TODO current aiter only support head number 16 or 128 head number
if head_num == 128 or head_num == 16:
return "aiter"
else:
return "triton"
elif is_mps():
return "torch_native"
else:
return "triton"
def cutedsl_moe_max_num_tokens(self) -> int:
"""Largest number of tokens a single forward routes through a CuteDSL
MoE layer on one (DP) rank. Single source of truth for both the
standard-allgather wrapper buffers and the FlashInfer A2A dispatcher
budget. Max over the prefill (max_prefill_tokens), piecewise-prefill
capture, and decode/verify bounds; num_tokens_per_req is
speculative_num_draft_tokens under speculative decoding, else 1.
"""
cfg = resolving_view(self)
if cfg.speculative_algorithm:
num_tokens_per_req = cfg.speculative_num_draft_tokens or 1
else:
num_tokens_per_req = 1
prefill_tokens = cfg.max_prefill_tokens
cg_config = cfg.cuda_graph_config
if cg_config is not None and cg_config.prefill.backend == Backend.TC_PIECEWISE:
prefill_tokens = max(prefill_tokens, cg_config.prefill.max_bs or 0)
decode_max_bs = (cg_config.decode.max_bs if cg_config is not None else 0) or 0
decode_tokens = decode_max_bs * num_tokens_per_req
return max(prefill_tokens, decode_tokens)
def max_prefill_buffer_tokens(self) -> int:
"""Prefill-buffer ceiling: chunked_prefill_size, except PP dynamic
chunking can grow chunks toward max_prefill_tokens and probe at 1.25x."""
cfg = resolving_view(self)
chunked = (
cfg.chunked_prefill_size
if cfg.chunked_prefill_size and cfg.chunked_prefill_size > 0
else 0
)
tokens = chunked
if cfg.enable_dynamic_chunking and cfg.pp_size > 1 and chunked:
tokens = max(tokens, cfg.max_prefill_tokens or 0, math.ceil(chunked * 1.25))
return tokens
def _required_mori_dispatch_tokens_per_rank(self) -> int:
"""Max tokens a single rank dispatches through MoRI in one forward."""
cfg = resolving_view(self)
return cfg.chunked_prefill_size
def _required_pplx_dispatch_tokens_per_rank(self) -> int:
"""Max tokens a single rank dispatches through pplx in one forward."""
cfg = resolving_view(self)
required = cfg.chunked_prefill_size
if cfg.cuda_graph_max_bs_decode is not None:
required = max(required, cfg.cuda_graph_max_bs_decode)
return required
# ===== END TO BE REFACTORED ==== # ===== END TO BE REFACTORED ====
def _is_mistral_native_format(self) -> bool:
"""True iff the checkpoint requires load_format=mistral.
Looks for consolidated*.safetensors with no competing
model*.safetensors; when both weight formats ship in the
same checkpoint (e.g. Mistral-7B-Instruct-v0.3) the HF path is
preferred to avoid loading Mistral-named weights into an
HF-named architecture.
Name override: mistral-large-3 / mistral-small-4 /
leanstral always treat as Mistral-native when params.json
is present -- those families need Mistral weight loading
regardless of which weight files happen to be present.
"""
cfg = resolving_view(self)
_MISTRAL_NATIVE_PATTERNS = (
"mistral-large-3",
"mistral-small-4",
"leanstral",
)
name_matches = any(
p in str(cfg.model_path).lower() for p in _MISTRAL_NATIVE_PATTERNS
)
def _check_format(has_params, has_consolidated, has_hf_weights) -> bool:
if has_params and name_matches:
return True
return has_consolidated and not has_hf_weights
if os.path.isdir(cfg.model_path):
return _check_format(
has_params=os.path.exists(os.path.join(cfg.model_path, "params.json")),
has_consolidated=bool(
glob.glob(os.path.join(cfg.model_path, "consolidated*.safetensors"))
),
has_hf_weights=bool(
glob.glob(os.path.join(cfg.model_path, "model*.safetensors"))
),
)
try:
from huggingface_hub import HfApi
files = {s.rfilename for s in HfApi().model_info(cfg.model_path).siblings}
return _check_format(
has_params="params.json" in files,
has_consolidated=any(
f.startswith("consolidated") and f.endswith(".safetensors")
for f in files
),
has_hf_weights=any(
f.startswith("model")
and f.endswith(".safetensors")
and "/" not in f
for f in files
),
)
except Exception:
return False
LANGUAGE_MODEL_ONLY_ARCHITECTURES = ("MuseGlimmerForConditionalGeneration",) LANGUAGE_MODEL_ONLY_ARCHITECTURES = ("MuseGlimmerForConditionalGeneration",)
# The strided-layout Triton requirement is enforced via # The strided-layout Triton requirement is enforced via
@@ -4587,52 +4158,6 @@ class ServerArgs:
return False return False
return True return True
def get_model_config(self):
# Lazy init to avoid circular import
cfg = resolving_view(self)
from sglang.srt.configs.model_config import ModelConfig
memo = getattr(self, "_model_config", None)
if memo is not None:
# The key is the path this record carried when the cache was
# filled. The GGUF and ModelScope handlers declare a different
# `model_path`, and a configuration built before them describes
# another checkpoint. `ModelConfig` re-points its own `model_path`
# at the local pull directory when the weights sit behind an
# object-store URI, so its field is not the key. A configuration a
# fixture supplied carries no key and is handed back as it is.
built_from = getattr(self, "_model_config_built_from", None)
if built_from is None or built_from == cfg.model_path:
return memo
model_config = ModelConfig.from_server_args(self)
self._model_config = model_config
self._model_config_built_from = cfg.model_path
if model_config.is_hybrid_swa:
logger.info(
"Hybrid SWA model detected. architectures=%s",
model_config.hf_config.architectures,
)
return model_config
def _resolved(self):
"""Read-only view of the resolving configuration: declared fields
resolve from the declaration stash."""
return resolved_view(self)
def _late_resolution(self, source: str, **fields) -> None:
"""Resolve fields at the launcher's validation stage (pre-publish).
See ``arg_groups.overrides.declare_late_resolution``: the decision goes
to this instance's declaration stash, so every holder of it carries the
decision and publishes bags that answer with it. Refused outright once
the config is published.
"""
from sglang.srt.arg_groups.overrides import declare_late_resolution
declare_late_resolution(self, source, **fields)
def __setattr__(self, name, value): def __setattr__(self, name, value):
# Once resolution has finished the record is the READ-ONLY raw input # Once resolution has finished the record is the READ-ONLY raw input
# the config bags were projected from. Resolved config changes go to the bags via # the config bags were projected from. Resolved config changes go to the bags via
@@ -4651,153 +4176,17 @@ class ServerArgs:
) )
object.__setattr__(self, name, value) object.__setattr__(self, name, value)
def _resolved_attention_backends(self):
"""Mid-resolution (prefill, decode) backends: reads through the pass
view so declared fields resolve from the declaration stash."""
from sglang.srt.arg_groups.overrides import (
attention_backends_of,
)
return attention_backends_of(resolved_view(self))
def get_attention_backends(self):
"""The (prefill, decode) pair resolution decided.
Reads through the declaration stash, not the fields: the model-specific
overrides declare into the stash without writing the fields, so a field
read answers with what the operator typed.
"""
return attention_backends_of(resolved_view(self))
def use_mla_backend(self):
from sglang.srt.configs.model_config import AttentionArch
model_config = self.get_model_config()
return model_config.attention_arch == AttentionArch.MLA
def is_attention_backend_not_set(self):
cfg = resolving_view(self)
return (
cfg.attention_backend is None
and cfg.prefill_attention_backend is None
and cfg.decode_attention_backend is None
)
def enable_mamba_extra_buffer(self) -> bool: def enable_mamba_extra_buffer(self) -> bool:
return mamba_extra_buffer_of(resolving_view(self)) return mamba_extra_buffer_of(resolving_view(self))
def enable_mamba_extra_buffer_lazy(self) -> bool: def enable_mamba_extra_buffer_lazy(self) -> bool:
return mamba_extra_buffer_lazy_of(resolving_view(self)) return mamba_extra_buffer_lazy_of(resolving_view(self))
@property
def max_speculative_num_draft_tokens(self) -> Optional[int]:
"""Return the maximum draft-token count speculative decoding may use.
Memoized only once the record is resolved: an answer computed off a raw
record describes inputs resolution is about to rewrite (auto speculative
sizing fills `speculative_num_draft_tokens` in), and a cache filled that
early would keep answering with it.
"""
cfg = resolving_view(self)
memo = self.__dict__.get("_max_speculative_num_draft_tokens")
if memo is not None:
return memo
if cfg.speculative_num_draft_tokens is None:
result = None
elif not cfg.speculative_adaptive:
result = cfg.speculative_num_draft_tokens
else:
from sglang.srt.speculative.adaptive_spec_params import (
resolve_candidate_steps_from_config,
)
candidate_steps = resolve_candidate_steps_from_config(
cfg_path=cfg.speculative_adaptive_config,
)
# TODO: adaptive spec currently requires topk=1, so each runtime
# state needs steps + 1 draft-token slots. Revisit this if topk>1
# is supported.
result = max(candidate_steps) + 1
if getattr(self, "_resolution_finished", False):
self._max_speculative_num_draft_tokens = result
return result
@property
def mamba_cache_chunk_size(self) -> int:
# For mamba cache with extra buffer, the chunk size is the max of FLA_CHUNK_SIZE
# (or mamba_chunk_size if it is defined in the model's config) and page_size.
# It is used to determine the caching point in a sequence during prefill.
# A pre-seeded `_mamba_cache_chunk_size` (fixtures supply one so a dummy
# model never loads an HF config) is honored as-is; otherwise the memo
# is only kept once the record is resolved, because `page_size` below
# is resolution-written.
if not hasattr(self, "_mamba_cache_chunk_size"):
try:
from sglang.kernels.ops.attention.fla.chunk_delta_h import (
CHUNK_SIZE as FLA_CHUNK_SIZE,
)
except ImportError:
# Must match sglang.kernels.ops.attention.fla.chunk_delta_h.CHUNK_SIZE
FLA_CHUNK_SIZE = 64
hf_config = self.get_model_config().hf_config
chunk_size = getattr(hf_config, "mamba_chunk_size", FLA_CHUNK_SIZE)
page_size = resolved_view(self).page_size
assert (
max(chunk_size, page_size) % min(chunk_size, page_size) == 0
), f"For SSM models, either chunk_size or page_size must be divisible by the other, got {chunk_size=}, {page_size=}"
if not getattr(self, "_resolution_finished", False):
return max(chunk_size, page_size)
self._mamba_cache_chunk_size = max(chunk_size, page_size)
return self._mamba_cache_chunk_size
def check_server_args(self): def check_server_args(self):
from sglang.srt.arg_groups.validation_hook import check_server_args from sglang.srt.arg_groups.validation_hook import check_server_args
check_server_args(self) check_server_args(self)
def adjust_mem_fraction_for_vlm(self, model_config):
cfg = resolving_view(self)
vision_config = getattr(model_config.hf_config, "vision_config", None)
if vision_config is None:
return
# roughly reduce the mem_fraction_static base on params of Vit
original_server_arg_mem_fraction = cfg.mem_fraction_static
# a base mem_fraction_static factor for regular Vit
base_mem_fraction_reduction_ratio = 0.95
vit_num_layers = getattr(vision_config, "num_hidden_layers", 24)
vit_hidden_size = getattr(vision_config, "hidden_size", 1024)
# baseline ViT params (ViT-L/14)
baseline_vit_layers = 24
baseline_vit_hidden_size = 1024
# weight params count
current_complexity_score = vit_num_layers * (vit_hidden_size**2)
baseline_complexity_score = baseline_vit_layers * (baseline_vit_hidden_size**2)
complexity_ratio = (
current_complexity_score / baseline_complexity_score
if baseline_complexity_score > 0
else 1.0
)
# every time the complexity grows 100%, adjust final factor for 10%
sensitivity_scale = 0.1
dynamic_adjustment_factor = 1.0 - sensitivity_scale * (complexity_ratio - 1.0)
dynamic_adjustment_factor = max(0.8, min(1.05, dynamic_adjustment_factor))
final_overall_factor = (
base_mem_fraction_reduction_ratio * dynamic_adjustment_factor
)
self._declare(
"adjust_mem_fraction_for_vlm",
mem_fraction_static=original_server_arg_mem_fraction * final_overall_factor,
)
@property @property
def _parsed_modelexpress_config(self) -> dict: def _parsed_modelexpress_config(self) -> dict:
cache = getattr(self, "_mx_config_cache", None) cache = getattr(self, "_mx_config_cache", None)
@@ -4952,10 +4341,6 @@ class ServerArgs:
descriptor["load_topic"] = LOAD_TOPIC descriptor["load_topic"] = LOAD_TOPIC
return descriptor return descriptor
def should_report_expert_balancedness(self) -> bool:
cfg = resolving_view(self)
return cfg.expert_balancedness_report_mode != "off"
def should_log_expert_balancedness_to_server_log(self) -> bool: def should_log_expert_balancedness_to_server_log(self) -> bool:
cfg = resolving_view(self) cfg = resolving_view(self)
+11 -4
View File
@@ -2088,20 +2088,27 @@ def _wait_for_gpu_idle_in_ci(
pass pass
# Names the runner kits stamp onto a record that are not members of it.
# `ModelRunner` computes `use_mla_backend` on itself; the kits copy that bool
# onto the record they hand the runner, and `hasattr` cannot see it.
_RUNNER_WRITTEN_NAMES = frozenset({"use_mla_backend"})
def server_args_variant(server_args, **fields): def server_args_variant(server_args, **fields):
"""A modified deep copy of a config, for a test double whose fixture """A modified deep copy of a config, for a test double whose fixture
differs from the (possibly published, read-only) config it starts from. differs from the (possibly published, read-only) config it starts from.
The receiver is untouched; the copy keeps its read-only guard. The receiver is untouched; the copy keeps its read-only guard.
A name may also shadow a method with a fixture value (the runner kits set A name may also be one the kits stamp on rather than a field (see
``use_mla_backend``, a method ModelRunner itself overwrites at init); ``_RUNNER_WRITTEN_NAMES``); names that exist nowhere fail loudly."""
names that exist nowhere on the class fail loudly."""
variant = copy.deepcopy(server_args) variant = copy.deepcopy(server_args)
cls = type(variant) cls = type(variant)
unknown = { unknown = {
name name
for name in fields for name in fields
if name not in cls.__dataclass_fields__ and not hasattr(cls, name) if name not in cls.__dataclass_fields__
and not hasattr(cls, name)
and name not in _RUNNER_WRITTEN_NAMES
} }
if unknown: if unknown:
raise ValueError(f"unknown ServerArgs field(s): {sorted(unknown)}") raise ValueError(f"unknown ServerArgs field(s): {sorted(unknown)}")
@@ -58,12 +58,12 @@ class TestServerArgsIBDeviceValidation(unittest.TestCase):
real_listdir = os.listdir real_listdir = os.listdir
with patch( with patch(
"sglang.srt.server_args.os.path.isdir", "sglang.srt.arg_groups.validation_hook.os.path.isdir",
side_effect=lambda path: ( side_effect=lambda path: (
True if path == "/sys/class/infiniband" else real_isdir(path) True if path == "/sys/class/infiniband" else real_isdir(path)
), ),
), patch( ), patch(
"sglang.srt.server_args.os.listdir", "sglang.srt.arg_groups.validation_hook.os.listdir",
side_effect=lambda path: ( side_effect=lambda path: (
available_devices available_devices
if path == "/sys/class/infiniband" if path == "/sys/class/infiniband"
@@ -88,8 +88,9 @@ class TestMultimodalPiecewiseCudaGraph(CustomTestCase):
"sglang.srt.arg_groups.cuda_graph_hook" "sglang.srt.arg_groups.cuda_graph_hook"
".disable_tc_piecewise_cudagraph_if_incompatible" ".disable_tc_piecewise_cudagraph_if_incompatible"
) as disable_if_incompatible, ) as disable_if_incompatible,
patch.object( patch(
args, "_resolved_attention_backends", return_value=("fa3", "fa3") "sglang.srt.arg_groups.overrides.attention_backends_of",
return_value=("fa3", "fa3"),
), ),
): ):
apply_cuda_graph_compatibility(args) apply_cuda_graph_compatibility(args)
@@ -116,12 +117,11 @@ class TestMultimodalPiecewiseCudaGraph(CustomTestCase):
args._cuda_graph_config_locked = set() args._cuda_graph_config_locked = set()
with ( with (
patch.object( patch(
args, "sglang.srt.arg_groups.overrides.attention_backends_of",
"_resolved_attention_backends",
return_value=("trtllm_mla", "trtllm_mla"), return_value=("trtllm_mla", "trtllm_mla"),
), ),
patch.object(args, "use_mla_backend", return_value=True), patch("sglang.srt.arg_groups.overrides.use_mla_backend", return_value=True),
): ):
apply_cuda_graph_compatibility(args) apply_cuda_graph_compatibility(args)
@@ -137,9 +137,8 @@ class TestMultimodalPiecewiseCudaGraph(CustomTestCase):
) )
args._cuda_graph_config_locked = {(Phase.PREFILL, "backend")} args._cuda_graph_config_locked = {(Phase.PREFILL, "backend")}
with patch.object( with patch(
args, "sglang.srt.arg_groups.overrides.attention_backends_of",
"_resolved_attention_backends",
return_value=("trtllm_mla", "trtllm_mla"), return_value=("trtllm_mla", "trtllm_mla"),
): ):
apply_cuda_graph_compatibility(args) apply_cuda_graph_compatibility(args)
@@ -185,10 +184,7 @@ class TestMultimodalPiecewiseCudaGraph(CustomTestCase):
args.disable_radix_cache = False args.disable_radix_cache = False
args.chunked_prefill_size = 2048 args.chunked_prefill_size = 2048
with ( with (patch("sglang.srt.arg_groups.model_hook.is_cuda", return_value=True),):
patch.object(args, "get_model_config", return_value=args._model_config),
patch("sglang.srt.arg_groups.model_hook.is_cuda", return_value=True),
):
handle_model_capability_adjustments(args) handle_model_capability_adjustments(args)
self.assertTrue(resolution_result(args, "disable_radix_cache")) self.assertTrue(resolution_result(args, "disable_radix_cache"))
@@ -215,7 +211,7 @@ class TestMultimodalPiecewiseCudaGraph(CustomTestCase):
hf_config=SimpleNamespace(architectures=["BertModel"]), hf_config=SimpleNamespace(architectures=["BertModel"]),
) )
with patch.object(args, "get_model_config", return_value=args._model_config): if True: # the record already carries the seeded configuration
handle_model_capability_adjustments(args) handle_model_capability_adjustments(args)
self.assertTrue(resolution_result(args, "is_embedding")) self.assertTrue(resolution_result(args, "is_embedding"))
@@ -87,7 +87,10 @@ from sglang.srt.mem_cache.unified_radix_cache import (
_OngoingPrefetch, _OngoingPrefetch,
_OngoingWriteThrough, _OngoingWriteThrough,
) )
from sglang.srt.runtime_context import get_server_args, get_serving from sglang.srt.runtime_context import (
get_serving,
mamba_cache_chunk_size,
)
from sglang.srt.sampling.sampling_params import SamplingParams from sglang.srt.sampling.sampling_params import SamplingParams
from sglang.srt.server_args import ( from sglang.srt.server_args import (
ServerArgs, ServerArgs,
@@ -5225,7 +5228,7 @@ class UnifiedRadixCacheSuite:
if not self.cfg.has_mamba or self.cfg.has_swa or self.cfg.page_size != 1: if not self.cfg.has_mamba or self.cfg.has_swa or self.cfg.page_size != 1:
self.skipTest("requires page_size=1 Full+Mamba") self.skipTest("requires page_size=1 Full+Mamba")
cache, allocator, req_to_token_pool = build_fixture(self.cfg) cache, allocator, req_to_token_pool = build_fixture(self.cfg)
chunk_size = get_server_args().mamba_cache_chunk_size chunk_size = mamba_cache_chunk_size()
tokens = self._make_seq(1, chunk_size + 1) tokens = self._make_seq(1, chunk_size + 1)
self._insert(cache, allocator, req_to_token_pool, tokens) self._insert(cache, allocator, req_to_token_pool, tokens)
leaf = cache.match_prefix( leaf = cache.match_prefix(
@@ -5258,7 +5261,7 @@ class UnifiedRadixCacheSuite:
if not self.cfg.has_mamba or self.cfg.has_swa or self.cfg.page_size != 1: if not self.cfg.has_mamba or self.cfg.has_swa or self.cfg.page_size != 1:
self.skipTest("requires page_size=1 Full+Mamba") self.skipTest("requires page_size=1 Full+Mamba")
cache, allocator, req_to_token_pool = self._build_hicache_fixture() cache, allocator, req_to_token_pool = self._build_hicache_fixture()
chunk_size = get_server_args().mamba_cache_chunk_size chunk_size = mamba_cache_chunk_size()
prefix = self._make_seq(1, chunk_size) prefix = self._make_seq(1, chunk_size)
tokens = prefix + self._make_seq(1000, chunk_size + 1) tokens = prefix + self._make_seq(1000, chunk_size + 1)
self._insert(cache, allocator, req_to_token_pool, prefix) self._insert(cache, allocator, req_to_token_pool, prefix)
@@ -5283,7 +5286,7 @@ class UnifiedRadixCacheSuite:
if not self.cfg.has_mamba or self.cfg.has_swa or self.cfg.page_size != 1: if not self.cfg.has_mamba or self.cfg.has_swa or self.cfg.page_size != 1:
self.skipTest("requires page_size=1 Full+Mamba") self.skipTest("requires page_size=1 Full+Mamba")
cache, allocator, req_to_token_pool = self._build_hicache_fixture() cache, allocator, req_to_token_pool = self._build_hicache_fixture()
chunk_size = get_server_args().mamba_cache_chunk_size chunk_size = mamba_cache_chunk_size()
prefix = self._make_seq(1, chunk_size) prefix = self._make_seq(1, chunk_size)
tokens = prefix + self._make_seq(1000, chunk_size + 1) tokens = prefix + self._make_seq(1000, chunk_size + 1)
self._insert(cache, allocator, req_to_token_pool, prefix) self._insert(cache, allocator, req_to_token_pool, prefix)
@@ -50,10 +50,7 @@ def test_packed_speculative_extend_is_limited_to_pd_prefill_target(mode, error):
def test_chunked_prefill_disabled_uses_legacy_token_ceiling(): def test_chunked_prefill_disabled_uses_legacy_token_ceiling():
model_runner = SimpleNamespace( model_runner = SimpleNamespace(
server_args=SimpleNamespace( server_args=SimpleNamespace(),
max_prefill_buffer_tokens=Mock(return_value=0),
max_prefill_tokens=32768,
),
is_generation=True, is_generation=True,
is_draft_worker=False, is_draft_worker=False,
spec_algorithm=SimpleNamespace(is_speculative=lambda: False), spec_algorithm=SimpleNamespace(is_speculative=lambda: False),
@@ -76,6 +73,12 @@ def test_chunked_prefill_disabled_uses_legacy_token_ceiling():
"get_disagg", "get_disagg",
return_value=SimpleNamespace(disaggregation_mode="prefill"), return_value=SimpleNamespace(disaggregation_mode="prefill"),
), ),
patch.object(flashinfer_autotune, "max_prefill_buffer_tokens", return_value=0),
patch.object(
flashinfer_autotune,
"get_schedule",
return_value=SimpleNamespace(max_prefill_tokens=32768),
),
patch.object(flashinfer_autotune, "run_flashinfer_autotune_forward"), patch.object(flashinfer_autotune, "run_flashinfer_autotune_forward"),
patch.object(flashinfer_autotune.torch.cuda, "empty_cache"), patch.object(flashinfer_autotune.torch.cuda, "empty_cache"),
): ):
@@ -1,4 +1,4 @@
"""`get_model_config()` caches, and the key is the path the record carried. """`model_config_of()` caches, and the key is the path the record carried.
Two movements of a `model_path` reach this cache, and only the first one means Two movements of a `model_path` reach this cache, and only the first one means
the cached configuration describes the wrong checkpoint: the cached configuration describes the wrong checkpoint:
@@ -20,6 +20,7 @@ import tempfile
import unittest import unittest
from types import SimpleNamespace from types import SimpleNamespace
from sglang.srt.arg_groups.overrides import declare_resolution, model_config_of
from sglang.srt.configs.model_config import ModelConfig from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.environ import EnvField, envs from sglang.srt.environ import EnvField, envs
from sglang.srt.server_args import ServerArgs from sglang.srt.server_args import ServerArgs
@@ -117,7 +118,7 @@ class TestTheModelConfigCache(CustomTestCase):
self.assertEqual(server_args.model_path, _OBJECT_STORE_URI) self.assertEqual(server_args.model_path, _OBJECT_STORE_URI)
self.assertEqual(cached.model_path, pulled) self.assertEqual(cached.model_path, pulled)
self.assertIs(server_args.get_model_config(), cached) self.assertIs(model_config_of(server_args), cached)
def test_a_declared_model_path_rebuilds_the_configuration(self): def test_a_declared_model_path_rebuilds_the_configuration(self):
"""The GGUF and ModelScope shape: the record's own path moved.""" """The GGUF and ModelScope shape: the record's own path moved."""
@@ -125,14 +126,15 @@ class TestTheModelConfigCache(CustomTestCase):
second_checkpoint = self._checkpoint() second_checkpoint = self._checkpoint()
server_args = ServerArgs(model_path=first_checkpoint, device="cuda") server_args = ServerArgs(model_path=first_checkpoint, device="cuda")
first = server_args.get_model_config() first = model_config_of(server_args)
self.assertEqual(first.model_path, first_checkpoint) self.assertEqual(first.model_path, first_checkpoint)
server_args._declare( declare_resolution(
server_args,
"test_a_declared_model_path_rebuilds_the_configuration", "test_a_declared_model_path_rebuilds_the_configuration",
model_path=second_checkpoint, model_path=second_checkpoint,
) )
second = server_args.get_model_config() second = model_config_of(server_args)
self.assertIsNot(second, first) self.assertIsNot(second, first)
self.assertEqual(second.model_path, second_checkpoint) self.assertEqual(second.model_path, second_checkpoint)
@@ -151,11 +153,11 @@ class TestTheModelConfigCache(CustomTestCase):
model_path=second_checkpoint, model_path=second_checkpoint,
) )
rebuilt = copy_.get_model_config() rebuilt = model_config_of(copy_)
self.assertEqual(rebuilt.model_path, second_checkpoint) self.assertEqual(rebuilt.model_path, second_checkpoint)
self.assertIs(copy_.get_model_config(), rebuilt) self.assertIs(model_config_of(copy_), rebuilt)
# The parent keeps the configuration it resolved with. # The parent keeps the configuration it resolved with.
self.assertEqual(server_args.get_model_config().model_path, first_checkpoint) self.assertEqual(model_config_of(server_args).model_path, first_checkpoint)
def test_a_supplied_configuration_is_handed_back(self): def test_a_supplied_configuration_is_handed_back(self):
"""A configuration nothing in here built carries no key, so nothing """A configuration nothing in here built carries no key, so nothing
@@ -164,7 +166,7 @@ class TestTheModelConfigCache(CustomTestCase):
stand_in = SimpleNamespace(model_path="somewhere/else") stand_in = SimpleNamespace(model_path="somewhere/else")
server_args._model_config = stand_in server_args._model_config = stand_in
self.assertIs(server_args.get_model_config(), stand_in) self.assertIs(model_config_of(server_args), stand_in)
if __name__ == "__main__": if __name__ == "__main__":
@@ -30,7 +30,7 @@ _SRT = pathlib.Path(sglang.__file__).resolve().parent / "srt"
# Two quantities sharing one name. # Two quantities sharing one name.
_READ_BEFORE_RESOLUTION = frozenset({"is_embedding"}) _READ_BEFORE_RESOLUTION = frozenset({"is_embedding"})
# Declared after the first `get_model_config()`, so the cached configuration # Declared after the first `model_config_of()`, so the cached configuration
# holds the earlier value. Nothing reads the stale copy today (its one consumer # holds the earlier value. Nothing reads the stale copy today (its one consumer
# is on the `is_draft_model` branch, built after resolution), and fixing it # is on the `is_draft_model` branch, built after resolution), and fixing it
# means moving the build or the hook. Pinned so a second field in this position # means moving the build or the hook. Pinned so a second field in this position
@@ -109,7 +109,7 @@ def _registry_collection_is_after_the_build():
Handler-local ordering only -- the caller still has to compare against the Handler-local ordering only -- the caller still has to compare against the
pipeline-wide first build, which sits in an *earlier* step: hoisting the pipeline-wide first build, which sits in an *earlier* step: hoisting the
collection above this handler's own `get_model_config()` call does not move collection above this handler's own `model_config_of()` call does not move
it above the configuration another handler already cached. it above the configuration another handler already cached.
""" """
handler = None handler = None
@@ -145,7 +145,7 @@ def _registry_collection_is_after_the_build():
name = func.id name = func.id
else: else:
continue continue
if name == "get_model_config" and build is None: if name == "model_config_of" and build is None:
build = node.lineno build = node.lineno
if name == "collect_model_override_declarations" and collect is None: if name == "collect_model_override_declarations" and collect is None:
collect = node.lineno collect = node.lineno
@@ -192,11 +192,12 @@ def _server_args_names(tree, path):
and value.args[0].id in names and value.args[0].id in names
) )
# `resolved = self._resolved()` is the same view, spelled as the # `resolved = self._resolved()` is the same view, spelled as the
# record's own member. # resolution vocabulary.
member = ( member = (
isinstance(value, ast.Call) isinstance(value, ast.Call)
and isinstance(value.func, ast.Attribute) and isinstance(value.func, ast.Attribute)
and value.func.attr == "_resolved" and isinstance(value.func, ast.Name)
and value.func.id == "resolved_view"
and isinstance(value.func.value, ast.Name) and isinstance(value.func.value, ast.Name)
and value.func.value.id in names and value.func.value.id in names
) )
@@ -267,7 +268,7 @@ def _late_resolution_fields():
if isinstance(node.func, ast.Attribute) if isinstance(node.func, ast.Attribute)
else getattr(node.func, "id", "") else getattr(node.func, "id", "")
) )
if called in ("_late_resolution", "declare_late_resolution"): if called == "declare_late_resolution":
fields |= {kw.arg for kw in node.keywords if kw.arg} fields |= {kw.arg for kw in node.keywords if kw.arg}
return fields return fields
@@ -488,14 +489,14 @@ def _declaration_positions():
wanted = _constructor_reads() wanted = _constructor_reads()
def build_site(): def build_site():
"""(step index, method name, line) of the first `get_model_config()`.""" """(step index, method name, line) of the first `model_config_of()`."""
for index, step in enumerate(steps): for index, step in enumerate(steps):
for method in reached[step]: for method in reached[step]:
for node in ast.walk(methods[method]): for node in ast.walk(methods[method]):
if ( if (
isinstance(node, ast.Call) isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute) and isinstance(node.func, ast.Name)
and node.func.attr == "get_model_config" and node.func.id == "model_config_of"
): ):
return index, step, method, node.lineno return index, step, method, node.lineno
return None return None
@@ -532,8 +533,8 @@ def _declaration_positions():
same_body = index == build_index and method == build_method same_body = index == build_index and method == build_method
rank = 0 if same_body and node.lineno < build_line_in_body else 1 rank = 0 if same_body and node.lineno < build_line_in_body else 1
if ( if (
isinstance(node.func, ast.Attribute) isinstance(node.func, ast.Name)
and node.func.attr == "_declare" and node.func.id == "declare_resolution"
): ):
fields = {kw.arg for kw in node.keywords if kw.arg} fields = {kw.arg for kw in node.keywords if kw.arg}
# A handler that calls an imported hook (the Kimi and DeepSeek # A handler that calls an imported hook (the Kimi and DeepSeek
@@ -669,8 +670,8 @@ class TestModelConfigReadsResolvedInput(CustomTestCase):
for method in reached[step] for method in reached[step]
if any( if any(
isinstance(node, ast.Call) isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute) and isinstance(node.func, ast.Name)
and node.func.attr == "get_model_config" and node.func.id == "model_config_of"
for node in ast.walk(methods[method]) for node in ast.walk(methods[method])
) )
) )
@@ -61,7 +61,7 @@ class TestNoPublicNonFieldSlot(CustomTestCase):
written = _self_written_attributes() written = _self_written_attributes()
self.assertGreater( self.assertGreater(
len(written), len(written),
5, 3,
f"only {len(written)} self-writes found; the scan is broken, not the " f"only {len(written)} self-writes found; the scan is broken, not the "
"record", "record",
) )
@@ -30,6 +30,7 @@ under its own default configuration.
""" """
import unittest import unittest
from unittest import mock
from sglang.srt.arg_groups.kv_cache_hook import handle_page_major_kv_layout from sglang.srt.arg_groups.kv_cache_hook import handle_page_major_kv_layout
from sglang.srt.server_args import ServerArgs from sglang.srt.server_args import ServerArgs
@@ -66,8 +67,13 @@ def _accepts(
"mamba_backend": "triton", "mamba_backend": "triton",
}.items(): }.items():
object.__setattr__(sa, name, value) object.__setattr__(sa, name, value)
sa.use_mla_backend = lambda: use_mla # `use_mla_backend` asks the model configuration, which this stand-in has
sa._resolved_attention_backends = lambda: [backend] # no room for; the case under test is what the handler does with the answer.
# The handler imports it inside the function, so the source module is
# where the patch has to go.
with mock.patch(
"sglang.srt.arg_groups.overrides.use_mla_backend", return_value=use_mla
):
try: try:
handle_page_major_kv_layout(sa) handle_page_major_kv_layout(sa)
return True return True
@@ -24,7 +24,6 @@ import unittest
import unittest.mock import unittest.mock
import sglang import sglang
from sglang.srt import server_args as server_args_module
from sglang.srt.arg_groups.overrides import resolution_result from sglang.srt.arg_groups.overrides import resolution_result
from sglang.srt.server_args import ServerArgs from sglang.srt.server_args import ServerArgs
from sglang.test.ci.ci_register import register_cpu_ci from sglang.test.ci.ci_register import register_cpu_ci
@@ -147,7 +146,7 @@ def _late_resolvers():
if isinstance(node.func, ast.Attribute) if isinstance(node.func, ast.Attribute)
else getattr(node.func, "id", None) else getattr(node.func, "id", None)
) )
if called in ("declare_late_resolution", "_late_resolution"): if called == "declare_late_resolution":
return True return True
if called and reaches(called, seen): if called and reaches(called, seen):
return True return True
@@ -887,7 +886,9 @@ class TestResolutionDeclarations(CustomTestCase):
# The pipeline asks the platform other questions on the way through # The pipeline asks the platform other questions on the way through
# (whether it is out of tree, whether it supports piecewise capture), # (whether it is out of tree, whether it supports piecewise capture),
# and which of those it reaches depends on the host. # and which of those it reaches depends on the host.
class _Plugin(type(server_args_module.current_platform)): from sglang.srt.platforms import current_platform
class _Plugin(type(current_platform)):
device_name = "oot" device_name = "oot"
def apply_server_args_defaults(self, server_args): def apply_server_args_defaults(self, server_args):
@@ -36,7 +36,7 @@ import unittest.mock
import torch import torch
import sglang import sglang
from sglang.srt.arg_groups.overrides import resolution_result from sglang.srt.arg_groups.overrides import model_config_of, resolution_result
from sglang.srt.environ import EnvField, envs from sglang.srt.environ import EnvField, envs
from sglang.srt.server_args import ServerArgs from sglang.srt.server_args import ServerArgs
from sglang.srt.utils import is_cuda from sglang.srt.utils import is_cuda
@@ -519,7 +519,7 @@ class TestProgramsResolveBeforeReadingResolution(CustomTestCase):
from sglang.srt.server_args import ServerArgs as _ServerArgs from sglang.srt.server_args import ServerArgs as _ServerArgs
srt = pathlib.Path(next(iter(sglang.__path__))).resolve() / "srt" srt = pathlib.Path(next(iter(sglang.__path__))).resolve() / "srt"
declarers = {"_declare", "declare_resolution", "declare_late_resolution"} declarers = {"declare_resolution", "declare_late_resolution"}
fields = set() fields = set()
field_names = {field.name for field in _dataclasses.fields(_ServerArgs)} field_names = {field.name for field in _dataclasses.fields(_ServerArgs)}
# The record plus every module under `arg_groups/`: a handler declares # The record plus every module under `arg_groups/`: a handler declares
@@ -826,10 +826,10 @@ class TestACopyStaysResolved(_RestoresProcessState, CustomTestCase):
def test_the_copy_carries_what_resolution_left_on_the_record(self): def test_the_copy_carries_what_resolution_left_on_the_record(self):
"""Not just the stash and the flag. """Not just the stash and the flag.
`get_model_config()` memoizes on the record, and that cache is filled `model_config_of()` memoizes on the record, and that cache is filled
during resolution. A copy that is marked resolved but arrives without it during resolution. A copy that is marked resolved but arrives without it
cannot fill it -- the read-only guard refuses the cache write -- so the cannot fill it -- the read-only guard refuses the cache write -- so the
first `get_model_config()` raises. That is what killed the Ray first `model_config_of()` raises. That is what killed the Ray
schedulers, and it is why the carry is enumerated from the instance schedulers, and it is why the carry is enumerated from the instance
rather than from a list of names. rather than from a list of names.
""" """
@@ -846,7 +846,7 @@ class TestACopyStaysResolved(_RestoresProcessState, CustomTestCase):
[], [],
f"the copy did not carry what resolution left on the record: {missing}", f"the copy did not carry what resolution left on the record: {missing}",
) )
self.assertIsNotNone(copy_.get_model_config()) self.assertIsNotNone(model_config_of(copy_))
# Containers are copied, so the copy's declaration stays with it. # Containers are copied, so the copy's declaration stays with it.
self.assertEqual( self.assertEqual(
len(parent._resolved_overrides) + 1, len(copy_._resolved_overrides) len(parent._resolved_overrides) + 1, len(copy_._resolved_overrides)
@@ -4,8 +4,8 @@
nothing. The fields keep what the caller passed, so a resolver that reads a nothing. The fields keep what the caller passed, so a resolver that reads a
field another resolver may have decided reads the raw input -- silently, and field another resolver may have decided reads the raw input -- silently, and
only on the configurations where that other resolver fires. The whole pipeline only on the configurations where that other resolver fires. The whole pipeline
therefore reads through `resolving_view` (or `ServerArgs._resolved()`, which is therefore reads through `resolving_view` (or `resolved_view`, which is
the same view spelled as the record's own member), and this pins that there is the same view after resolution has finished), and this pins that there is
nothing left reading a field directly. nothing left reading a field directly.
Subjects: every function in `arg_groups/` that takes a config, every Subjects: every function in `arg_groups/` that takes a config, every
@@ -83,7 +83,6 @@ def _field_reads(fn, holders):
_DECLARERS = frozenset( _DECLARERS = frozenset(
{ {
"_declare",
"declare_resolution", "declare_resolution",
"declare_late_resolution", "declare_late_resolution",
"declare_direct_writes", "declare_direct_writes",
@@ -472,7 +471,7 @@ class TestResolutionReadsTheDeclarations(CustomTestCase):
members = _record_members() members = _record_members()
# The floor is here to catch the scan collapsing, not to pin the # The floor is here to catch the scan collapsing, not to pin the
# class's size. # class's size.
self.assertGreater(len(members), 40, f"only {len(members)} members were found") self.assertGreater(len(members), 25, f"only {len(members)} members were found")
offenders = [] offenders = []
for name, fn in sorted(members.items()): for name, fn in sorted(members.items()):
holders = _holders(fn) | {"self"} holders = _holders(fn) | {"self"}
@@ -37,7 +37,10 @@ from sglang.srt.arg_groups.moe_hook import (
validate_deepep_v2_dispatch_token_budget, validate_deepep_v2_dispatch_token_budget,
validate_deepep_v2_speculative_draft, validate_deepep_v2_speculative_draft,
) )
from sglang.srt.arg_groups.overrides import resolution_result from sglang.srt.arg_groups.overrides import (
cutedsl_moe_max_num_tokens,
resolution_result,
)
from sglang.srt.arg_groups.parallel_hook import ( from sglang.srt.arg_groups.parallel_hook import (
handle_context_parallelism, handle_context_parallelism,
handle_data_parallelism, handle_data_parallelism,
@@ -825,9 +828,7 @@ class TestHiSparseDsaBackendPolicy(unittest.TestCase):
) )
defaults.update(kw) defaults.update(kw)
view = ResolvedView( view = ResolvedView(
SimpleNamespace( SimpleNamespace(_model_config=SimpleNamespace(hf_config=hf), **defaults)
get_model_config=lambda: SimpleNamespace(hf_config=hf), **defaults
)
) )
with ( with (
patch("sglang.srt.configs.model_config.is_deepseek_dsa", return_value=True), patch("sglang.srt.configs.model_config.is_deepseek_dsa", return_value=True),
@@ -845,21 +846,21 @@ class TestHiSparseDsaBackendPolicy(unittest.TestCase):
), ),
} }
@patch("sglang.srt.server_args.is_hip", return_value=False) @patch("sglang.srt.arg_groups.hisparse_hook._is_hip", return_value=False)
def test_hisparse_defaults_to_flashmla_sparse_on_cuda_bfloat16(self, _mock_is_hip): def test_hisparse_defaults_to_flashmla_sparse_on_cuda_bfloat16(self, _mock_is_hip):
resolved = self._resolve("bfloat16") resolved = self._resolve("bfloat16")
self.assertEqual(resolved["dsa_prefill_backend"], "flashmla_sparse") self.assertEqual(resolved["dsa_prefill_backend"], "flashmla_sparse")
self.assertEqual(resolved["dsa_decode_backend"], "flashmla_sparse") self.assertEqual(resolved["dsa_decode_backend"], "flashmla_sparse")
@patch("sglang.srt.server_args.is_hip", return_value=False) @patch("sglang.srt.arg_groups.hisparse_hook._is_hip", return_value=False)
def test_hisparse_defaults_to_flashmla_kv_on_cuda_fp8(self, _mock_is_hip): def test_hisparse_defaults_to_flashmla_kv_on_cuda_fp8(self, _mock_is_hip):
resolved = self._resolve("fp8_e4m3") resolved = self._resolve("fp8_e4m3")
self.assertEqual(resolved["dsa_prefill_backend"], "flashmla_kv") self.assertEqual(resolved["dsa_prefill_backend"], "flashmla_kv")
self.assertEqual(resolved["dsa_decode_backend"], "flashmla_kv") self.assertEqual(resolved["dsa_decode_backend"], "flashmla_kv")
@patch("sglang.srt.server_args.is_hip", return_value=False) @patch("sglang.srt.arg_groups.hisparse_hook._is_hip", return_value=False)
def test_hisparse_accepts_flashinfer_sparse_mla_on_cuda_fp8(self, _mock_is_hip): def test_hisparse_accepts_flashinfer_sparse_mla_on_cuda_fp8(self, _mock_is_hip):
"""SM120 GLM DSA resolves both DSA backends to flashinfer_sparse_mla, so """SM120 GLM DSA resolves both DSA backends to flashinfer_sparse_mla, so
the fp8 hisparse allow-set must admit it or --enable-hisparse cannot the fp8 hisparse allow-set must admit it or --enable-hisparse cannot
@@ -876,14 +877,14 @@ class TestHiSparseDsaBackendPolicy(unittest.TestCase):
validate_hisparse_dsa_backend(server_args, "dsa_prefill_backend", "prefill") validate_hisparse_dsa_backend(server_args, "dsa_prefill_backend", "prefill")
validate_hisparse_dsa_backend(server_args, "dsa_decode_backend", "decode") validate_hisparse_dsa_backend(server_args, "dsa_decode_backend", "decode")
@patch("sglang.srt.server_args.is_hip", return_value=True) @patch("sglang.srt.arg_groups.hisparse_hook._is_hip", return_value=True)
def test_hisparse_defaults_to_tilelang_on_rocm(self, _mock_is_hip): def test_hisparse_defaults_to_tilelang_on_rocm(self, _mock_is_hip):
resolved = self._resolve("bfloat16") resolved = self._resolve("bfloat16")
self.assertEqual(resolved["dsa_prefill_backend"], "tilelang") self.assertEqual(resolved["dsa_prefill_backend"], "tilelang")
self.assertEqual(resolved["dsa_decode_backend"], "tilelang") self.assertEqual(resolved["dsa_decode_backend"], "tilelang")
@patch("sglang.srt.server_args.is_hip", return_value=True) @patch("sglang.srt.arg_groups.hisparse_hook._is_hip", return_value=True)
def test_hisparse_preserves_rocm_user_backend_and_defaults_missing_side( def test_hisparse_preserves_rocm_user_backend_and_defaults_missing_side(
self, _mock_is_hip self, _mock_is_hip
): ):
@@ -892,7 +893,7 @@ class TestHiSparseDsaBackendPolicy(unittest.TestCase):
self.assertEqual(resolved["dsa_prefill_backend"], "tilelang") self.assertEqual(resolved["dsa_prefill_backend"], "tilelang")
self.assertEqual(resolved["dsa_decode_backend"], "tilelang") self.assertEqual(resolved["dsa_decode_backend"], "tilelang")
@patch("sglang.srt.server_args.is_hip", return_value=True) @patch("sglang.srt.arg_groups.hisparse_hook._is_hip", return_value=True)
def test_hisparse_accepts_aiter_backend_on_rocm(self, _mock_is_hip): def test_hisparse_accepts_aiter_backend_on_rocm(self, _mock_is_hip):
server_args = ServerArgs( server_args = ServerArgs(
model_path="dummy", model_path="dummy",
@@ -905,7 +906,7 @@ class TestHiSparseDsaBackendPolicy(unittest.TestCase):
validate_hisparse_dsa_backend(server_args, "dsa_prefill_backend", "prefill") validate_hisparse_dsa_backend(server_args, "dsa_prefill_backend", "prefill")
validate_hisparse_dsa_backend(server_args, "dsa_decode_backend", "decode") validate_hisparse_dsa_backend(server_args, "dsa_decode_backend", "decode")
@patch("sglang.srt.server_args.is_hip", return_value=True) @patch("sglang.srt.arg_groups.hisparse_hook._is_hip", return_value=True)
def test_hisparse_rejects_cuda_backend_on_rocm(self, _mock_is_hip): def test_hisparse_rejects_cuda_backend_on_rocm(self, _mock_is_hip):
server_args = ServerArgs( server_args = ServerArgs(
model_path="dummy", model_path="dummy",
@@ -917,7 +918,7 @@ class TestHiSparseDsaBackendPolicy(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "tilelang"): with self.assertRaisesRegex(ValueError, "tilelang"):
validate_hisparse_dsa_backend(server_args, "dsa_prefill_backend", "prefill") validate_hisparse_dsa_backend(server_args, "dsa_prefill_backend", "prefill")
@patch("sglang.srt.server_args.is_hip", return_value=False) @patch("sglang.srt.arg_groups.hisparse_hook._is_hip", return_value=False)
def test_hisparse_rejects_rocm_backend_on_cuda(self, _mock_is_hip): def test_hisparse_rejects_rocm_backend_on_cuda(self, _mock_is_hip):
server_args = ServerArgs( server_args = ServerArgs(
model_path="dummy", model_path="dummy",
@@ -969,7 +970,7 @@ class TestFa4PageSizeAutoForce(CustomTestCase):
args.prefill_attention_backend = prefill args.prefill_attention_backend = prefill
args.decode_attention_backend = decode args.decode_attention_backend = decode
args.page_size = page_size args.page_size = page_size
# Short-circuit get_model_config(): the fa4 page_size branch only needs # Short-circuit model_config_of(): the fa4 page_size branch only needs
# use_mla_backend() (mocked) and is_sm100_supported() (mocked), not a # use_mla_backend() (mocked) and is_sm100_supported() (mocked), not a
# real model_config. Pre-set the attribute so get_model_config returns # real model_config. Pre-set the attribute so get_model_config returns
# early without touching ModelConfig.from_server_args. # early without touching ModelConfig.from_server_args.
@@ -978,7 +979,7 @@ class TestFa4PageSizeAutoForce(CustomTestCase):
return args return args
@patch("sglang.srt.arg_groups.overrides.is_sm100_supported", return_value=True) @patch("sglang.srt.arg_groups.overrides.is_sm100_supported", return_value=True)
@patch("sglang.srt.server_args.ServerArgs.use_mla_backend", return_value=False) @patch("sglang.srt.arg_groups.overrides.use_mla_backend", return_value=False)
def test_combined_attention_backend_fa4_forces_page_size_128( def test_combined_attention_backend_fa4_forces_page_size_128(
self, _mock_mla, _mock_sm100 self, _mock_mla, _mock_sm100
): ):
@@ -993,7 +994,7 @@ class TestFa4PageSizeAutoForce(CustomTestCase):
self.assertEqual(resolved_view(args).page_size, 128) self.assertEqual(resolved_view(args).page_size, 128)
@patch("sglang.srt.arg_groups.overrides.is_sm100_supported", return_value=True) @patch("sglang.srt.arg_groups.overrides.is_sm100_supported", return_value=True)
@patch("sglang.srt.server_args.ServerArgs.use_mla_backend", return_value=False) @patch("sglang.srt.arg_groups.overrides.use_mla_backend", return_value=False)
def test_explicit_prefill_fa4_forces_page_size_128(self, _mock_mla, _mock_sm100): def test_explicit_prefill_fa4_forces_page_size_128(self, _mock_mla, _mock_sm100):
# `--prefill-attention-backend fa4`: the previously-covered path. # `--prefill-attention-backend fa4`: the previously-covered path.
args = self._make_args(attention_backend=None, prefill="fa4", page_size=1) args = self._make_args(attention_backend=None, prefill="fa4", page_size=1)
@@ -1678,7 +1679,7 @@ class TestAdaptiveSpecArgs(CustomTestCase):
args.speculative_adaptive = True args.speculative_adaptive = True
args.speculative_adaptive_config = f.name args.speculative_adaptive_config = f.name
args.device = "cuda" args.device = "cuda"
args.get_model_config = lambda: SimpleNamespace( args._model_config = SimpleNamespace(
hf_config=SimpleNamespace( hf_config=SimpleNamespace(
architectures=["LlamaForCausalLM"], architectures=["LlamaForCausalLM"],
get_text_config=lambda: SimpleNamespace(), get_text_config=lambda: SimpleNamespace(),
@@ -1870,7 +1871,9 @@ class TestCudaGraphDisaggregationRoles(CustomTestCase):
) )
with ( with (
patch("sglang.srt.utils.is_cuda", return_value=True), patch("sglang.srt.utils.is_cuda", return_value=True),
patch.object(ServerArgs, "use_mla_backend", return_value=False), patch(
"sglang.srt.arg_groups.overrides.use_mla_backend", return_value=False
),
): ):
handle_cuda_graph_config(args) handle_cuda_graph_config(args)
return args return args
@@ -1943,7 +1946,9 @@ class TestPrefillCudaGraphLoRACompatibility(CustomTestCase):
) )
with ( with (
patch("sglang.srt.utils.is_cuda", return_value=True), patch("sglang.srt.utils.is_cuda", return_value=True),
patch.object(ServerArgs, "use_mla_backend", return_value=False), patch(
"sglang.srt.arg_groups.overrides.use_mla_backend", return_value=False
),
): ):
handle_cuda_graph_config(args) handle_cuda_graph_config(args)
return args return args
@@ -2007,7 +2012,9 @@ class TestBreakableCudaGraphMultimodalAllowlist(CustomTestCase):
) )
with ( with (
patch("sglang.srt.utils.is_cuda", return_value=True), patch("sglang.srt.utils.is_cuda", return_value=True),
patch.object(ServerArgs, "use_mla_backend", return_value=False), patch(
"sglang.srt.arg_groups.overrides.use_mla_backend", return_value=False
),
): ):
handle_cuda_graph_config(args) handle_cuda_graph_config(args)
return args return args
@@ -2096,7 +2103,7 @@ class TestCutedslMoeMaxNumTokens(CustomTestCase):
return server_args return server_args
def test_prefill_dominates_in_default_config(self): def test_prefill_dominates_in_default_config(self):
self.assertEqual(self._args().cutedsl_moe_max_num_tokens(), 16384) self.assertEqual(cutedsl_moe_max_num_tokens(self._args()), 16384)
def test_speculative_decoding_scales_decode_bound(self): def test_speculative_decoding_scales_decode_bound(self):
# decode bound 512 * 8 dominates the small prefill/piecewise bounds # decode bound 512 * 8 dominates the small prefill/piecewise bounds
@@ -2106,7 +2113,7 @@ class TestCutedslMoeMaxNumTokens(CustomTestCase):
speculative_algorithm="EAGLE", speculative_algorithm="EAGLE",
speculative_num_draft_tokens=8, speculative_num_draft_tokens=8,
) )
self.assertEqual(args.cutedsl_moe_max_num_tokens(), 4096) self.assertEqual(cutedsl_moe_max_num_tokens(args), 4096)
def test_piecewise_bound_excluded_when_disabled(self): def test_piecewise_bound_excluded_when_disabled(self):
args = self._args( args = self._args(
@@ -2114,7 +2121,7 @@ class TestCutedslMoeMaxNumTokens(CustomTestCase):
disable_piecewise_cuda_graph=True, disable_piecewise_cuda_graph=True,
cuda_graph_max_bs=64, cuda_graph_max_bs=64,
) )
self.assertEqual(args.cutedsl_moe_max_num_tokens(), 512) self.assertEqual(cutedsl_moe_max_num_tokens(args), 512)
class TestSamplingBackendTokenOracleEnvGate(CustomTestCase): class TestSamplingBackendTokenOracleEnvGate(CustomTestCase):
@@ -2466,10 +2473,9 @@ class TestDeepEPv2Args(CustomTestCase):
dp_size=8, dp_size=8,
enable_dp_attention=True, enable_dp_attention=True,
) )
with patch.object( with patch(
ServerArgs, "sglang.srt.arg_groups.moe_hook.max_speculative_num_draft_tokens",
"max_speculative_num_draft_tokens", return_value=16,
new=property(lambda _self: 16),
): ):
with envs.SGLANG_DEEPEP_V2_NUM_MAX_DISPATCH_TOKENS_PER_RANK.override(128): with envs.SGLANG_DEEPEP_V2_NUM_MAX_DISPATCH_TOKENS_PER_RANK.override(128):
with self.assertRaisesRegex(ValueError, "tokens/request=16"): with self.assertRaisesRegex(ValueError, "tokens/request=16"):
@@ -20,7 +20,7 @@ def _make_spec_args(device: str, algorithm: str = "EAGLE", **overrides) -> Serve
args.speculative_num_steps = 3 args.speculative_num_steps = 3
args.speculative_eagle_topk = 1 args.speculative_eagle_topk = 1
args.speculative_num_draft_tokens = 4 args.speculative_num_draft_tokens = 4
args.get_model_config = lambda: SimpleNamespace( args._model_config = SimpleNamespace(
hf_config=SimpleNamespace( hf_config=SimpleNamespace(
architectures=["LlamaForCausalLM"], architectures=["LlamaForCausalLM"],
get_text_config=lambda: SimpleNamespace(), get_text_config=lambda: SimpleNamespace(),
+18 -11
View File
@@ -40,7 +40,7 @@ _OWNERS = ("server_args.py", "runtime_context.py", "arg_groups/")
# startup default wherever it is written, and `benchmark/` ships too. # startup default wherever it is written, and `benchmark/` ships too.
_READS_SCANNED = _PACKAGE _READS_SCANNED = _PACKAGE
_DECLARERS = ("_declare", "declare_resolution", "declare_late_resolution") _DECLARERS = ("declare_resolution", "declare_late_resolution")
def _declared_by_keyword(): def _declared_by_keyword():
@@ -190,10 +190,10 @@ def _declared_by_registry_and_passes():
def _declared_by_late_resolution(): def _declared_by_late_resolution():
"""Keywords of `self._late_resolution(...)`, the fourth declarer spelling. """Keywords of `declare_late_resolution(record, ...)`, the late spelling.
It forwards `**fields` to `declare_late_resolution`, so the keywords sit at The fields sit at the call sites rather than in the declarer, so a scan
its call sites and a scan for the declarer's own name finds none of them. that only knew the declarer's own definition would find none of them.
""" """
# The record plus `arg_groups/`: a hook calls it on the record it was # The record plus `arg_groups/`: a hook calls it on the record it was
# handed, so scanning the record's file alone finds nothing. # handed, so scanning the record's file alone finds nothing.
@@ -203,8 +203,8 @@ def _declared_by_late_resolution():
for node in ast.walk(ast.parse(source.read_text(encoding="utf-8-sig"))): for node in ast.walk(ast.parse(source.read_text(encoding="utf-8-sig"))):
if ( if (
isinstance(node, ast.Call) isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute) and isinstance(node.func, ast.Name)
and node.func.attr == "_late_resolution" and node.func.id == "declare_late_resolution"
): ):
fields |= {keyword.arg for keyword in node.keywords if keyword.arg} fields |= {keyword.arg for keyword in node.keywords if keyword.arg}
return fields return fields
@@ -540,13 +540,20 @@ class TestNoChainReadsOfResolvedConfig(CustomTestCase):
len(by_late), len(by_late),
3, 3,
f"only {len(by_late)} fields are declared late; the " f"only {len(by_late)} fields are declared late; the "
"`_late_resolution` keyword scan broke", "`declare_late_resolution` keyword scan broke",
) )
# The three mechanisms are not the same set: if any became a subset of # The data channel is not the keyword scan's subset: if it became one,
# the keyword scan, that scan would be doing all the work and a # that scan would be doing all the work and a regression here would be
# regression in the others would be invisible. # invisible. The late channel *is* a subset, and deliberately so --
# `declare_late_resolution` is a keyword declarer like the others now
# that the record hosts no forwarding member, so its own floor above is
# what pins it.
self.assertTrue(by_data - by_keyword, "the data channel adds nothing") self.assertTrue(by_data - by_keyword, "the data channel adds nothing")
self.assertTrue(by_late - by_keyword, "late resolution adds nothing") self.assertTrue(
by_late <= by_keyword,
"late resolution declares outside the keyword channel; it is the "
"same spelling, so the two cannot disagree",
)
def test_nothing_reads_a_resolved_field_off_a_borrowed_record(self): def test_nothing_reads_a_resolved_field_off_a_borrowed_record(self):
found = _chain_reads(_resolution_written()) found = _chain_reads(_resolution_written())
+49 -74
View File
@@ -15,6 +15,7 @@ from types import SimpleNamespace
from typing import Optional from typing import Optional
from unittest.mock import patch from unittest.mock import patch
from sglang.srt.arg_groups import attention_hook
from sglang.srt.arg_groups import overrides as overrides_module from sglang.srt.arg_groups import overrides as overrides_module
from sglang.srt.arg_groups.arg_utils import A, Arg, resolvable_fields from sglang.srt.arg_groups.arg_utils import A, Arg, resolvable_fields
from sglang.srt.arg_groups.overrides import ( from sglang.srt.arg_groups.overrides import (
@@ -24,6 +25,7 @@ from sglang.srt.arg_groups.overrides import (
validate_declarations, validate_declarations,
) )
from sglang.srt.configs.minicpm import MiniCPMHybridConfig from sglang.srt.configs.minicpm import MiniCPMHybridConfig
from sglang.srt.configs.model_config import AttentionArch
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.runtime_context import ( from sglang.srt.runtime_context import (
get_context, get_context,
@@ -357,14 +359,6 @@ class TestGoldenModelOverrides(_IsolatedPublish):
enable_dp_attention=enable_dp_attention, enable_dp_attention=enable_dp_attention,
enable_hierarchical_cache=enable_hierarchical_cache, enable_hierarchical_cache=enable_hierarchical_cache,
) )
args.is_attention_backend_not_set = lambda: all(
backend is None
for backend in (
args.attention_backend,
args.prefill_attention_backend,
args.decode_attention_backend,
)
)
mixer_types = [] mixer_types = []
if sparse_attention: if sparse_attention:
mixer_types.append("minicpm4") mixer_types.append("minicpm4")
@@ -456,7 +450,6 @@ class TestGoldenModelOverrides(_IsolatedPublish):
disaggregation_mode="null", disaggregation_mode="null",
enable_dp_attention=False, enable_dp_attention=False,
enable_hierarchical_cache=False, enable_hierarchical_cache=False,
is_attention_backend_not_set=lambda: True,
) )
config = SimpleNamespace( config = SimpleNamespace(
has_minicpm_sparse_attention=True, has_minicpm_sparse_attention=True,
@@ -630,7 +623,11 @@ class TestGoldenModelOverrides(_IsolatedPublish):
def test_minimax_m2_sm10x_nvfp4_uses_routed_trtllm(self): def test_minimax_m2_sm10x_nvfp4_uses_routed_trtllm(self):
"""MiniMax-M2 NVFP4 auto must avoid the unsupported plain TRT-LLM path.""" """MiniMax-M2 NVFP4 auto must avoid the unsupported plain TRT-LLM path."""
with patch.object(overrides_module, "is_sm100_supported", return_value=True): # Every module that asks: the attention handler validates what the
# override family picks, and each holds its own import.
with patch.object(
overrides_module, "is_sm100_supported", return_value=True
), patch.object(attention_hook, "is_sm100_supported", return_value=True):
explicit = self._construct( explicit = self._construct(
"MiniMaxM2ForCausalLM", "MiniMaxM2ForCausalLM",
"llama", "llama",
@@ -765,8 +762,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
speculative_draft_attention_backend=None, speculative_draft_attention_backend=None,
page_size=None, page_size=None,
mamba_radix_cache_strategy="auto", mamba_radix_cache_strategy="auto",
is_attention_backend_not_set=lambda: True, _model_config=model_config,
get_model_config=lambda: model_config,
), ),
hf_config, hf_config,
) )
@@ -971,7 +967,6 @@ class TestGoldenModelOverrides(_IsolatedPublish):
server_args.speculative_algorithm = "DFLASH" server_args.speculative_algorithm = "DFLASH"
server_args.prefill_attention_backend = "triton" server_args.prefill_attention_backend = "triton"
server_args.speculative_draft_attention_backend = "fa3" server_args.speculative_draft_attention_backend = "fa3"
server_args.is_attention_backend_not_set = lambda: False
with ( with (
patch.object(overrides_module, "is_blackwell_supported", return_value=True), patch.object(overrides_module, "is_blackwell_supported", return_value=True),
@@ -1093,7 +1088,9 @@ class TestGoldenModelOverrides(_IsolatedPublish):
_gpt_oss_overrides( _gpt_oss_overrides(
SimpleNamespace( SimpleNamespace(
dtype="float16", dtype="float16",
is_attention_backend_not_set=lambda: False, attention_backend="triton",
prefill_attention_backend=None,
decode_attention_backend=None,
), ),
SimpleNamespace(architectures=["GptOssForCausalLM"]), SimpleNamespace(architectures=["GptOssForCausalLM"]),
) )
@@ -1313,7 +1310,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
# dual-chunk config: mismatched explicit backend raises verbatim # dual-chunk config: mismatched explicit backend raises verbatim
def _mc(dual): def _mc(dual):
return SimpleNamespace( return SimpleNamespace(
get_model_config=lambda: SimpleNamespace( _model_config=SimpleNamespace(
hf_config=SimpleNamespace(dual_chunk_attention_config=dual) hf_config=SimpleNamespace(dual_chunk_attention_config=dual)
), ),
attention_backend="fa3", attention_backend="fa3",
@@ -1473,9 +1470,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
swa_full_tokens_ratio=ServerArgs.swa_full_tokens_ratio, swa_full_tokens_ratio=ServerArgs.swa_full_tokens_ratio,
moe_a2a_backend="none", moe_a2a_backend="none",
moe_runner_backend="auto", moe_runner_backend="auto",
get_model_config=lambda: SimpleNamespace( _model_config=SimpleNamespace(is_fp4_experts=True, nvfp4_moe_meta=None),
is_fp4_experts=True, nvfp4_moe_meta=None
),
) )
defaults.update(kw) defaults.update(kw)
return SimpleNamespace(**defaults) return SimpleNamespace(**defaults)
@@ -1527,12 +1522,10 @@ class TestGoldenModelOverrides(_IsolatedPublish):
) )
# FP8 checkpoints and non-CUDA platforms keep their platform-specific # FP8 checkpoints and non-CUDA platforms keep their platform-specific
# auto-resolution paths. # auto-resolution paths.
fp8_model_config = lambda: SimpleNamespace( fp8_model_config = SimpleNamespace(is_fp4_experts=False, nvfp4_moe_meta=None)
is_fp4_experts=False, nvfp4_moe_meta=None
)
self.assertNotIn( self.assertNotIn(
"moe_runner_backend", "moe_runner_backend",
_deepseek_v4_overrides(_args(get_model_config=fp8_model_config), hf), _deepseek_v4_overrides(_args(_model_config=fp8_model_config), hf),
) )
self.assertNotIn( self.assertNotIn(
"moe_runner_backend", "moe_runner_backend",
@@ -1569,7 +1562,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
self.assertEqual( self.assertEqual(
_deepseek_v4_overrides( _deepseek_v4_overrides(
_args( _args(
get_model_config=lambda: SimpleNamespace( _model_config=SimpleNamespace(
is_fp4_experts=False, nvfp4_moe_meta=object() is_fp4_experts=False, nvfp4_moe_meta=object()
) )
), ),
@@ -1604,15 +1597,10 @@ class TestGoldenModelOverrides(_IsolatedPublish):
speculative_draft_attention_backend=None, speculative_draft_attention_backend=None,
page_size=None, page_size=None,
mamba_radix_cache_strategy="auto", mamba_radix_cache_strategy="auto",
get_model_config=lambda: mc, _model_config=mc,
) )
defaults.update(kw) defaults.update(kw)
args = SimpleNamespace(**defaults) args = SimpleNamespace(**defaults)
args.is_attention_backend_not_set = lambda: (
args.attention_backend is None
and args.prefill_attention_backend is None
and args.decode_attention_backend is None
)
return args return args
hf = _hf() hf = _hf()
@@ -1725,9 +1713,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
) )
defaults.update(kw) defaults.update(kw)
return ResolvedView( return ResolvedView(
SimpleNamespace( SimpleNamespace(_model_config=SimpleNamespace(hf_config=hf), **defaults)
get_model_config=lambda: SimpleNamespace(hf_config=hf), **defaults
)
) )
with ( with (
@@ -1821,9 +1807,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
) )
defaults.update(kw) defaults.update(kw)
return ResolvedView( return ResolvedView(
SimpleNamespace( SimpleNamespace(_model_config=SimpleNamespace(hf_config=hf), **defaults)
get_model_config=lambda: SimpleNamespace(hf_config=hf), **defaults
)
) )
with ( with (
@@ -1952,15 +1936,6 @@ class TestGoldenModelOverrides(_IsolatedPublish):
) )
defaults.update(kw) defaults.update(kw)
ns = SimpleNamespace(**defaults) ns = SimpleNamespace(**defaults)
ns.is_attention_backend_not_set = lambda: (
ns.attention_backend is None
and ns.prefill_attention_backend is None
and ns.decode_attention_backend is None
)
ns.get_attention_backends = lambda: (
ns.prefill_attention_backend or ns.attention_backend,
ns.decode_attention_backend or ns.attention_backend,
)
return ns return ns
# nothing set: prefill defaults to flashinfer # nothing set: prefill defaults to flashinfer
@@ -1991,9 +1966,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
) )
defaults.update(kw) defaults.update(kw)
return ResolvedView( return ResolvedView(
SimpleNamespace( SimpleNamespace(_model_config=SimpleNamespace(hf_config=hf), **defaults)
get_model_config=lambda: SimpleNamespace(hf_config=hf), **defaults
)
) )
with ( with (
@@ -2037,9 +2010,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
defaults = dict(kv_cache_dtype="auto", device="cuda") defaults = dict(kv_cache_dtype="auto", device="cuda")
defaults.update(kw) defaults.update(kw)
return ResolvedView( return ResolvedView(
SimpleNamespace( SimpleNamespace(_model_config=SimpleNamespace(hf_config=hf), **defaults)
get_model_config=lambda: SimpleNamespace(hf_config=hf), **defaults
)
) )
self.assertEqual( self.assertEqual(
@@ -2078,9 +2049,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
) )
defaults.update(kw) defaults.update(kw)
return ResolvedView( return ResolvedView(
SimpleNamespace( SimpleNamespace(_model_config=SimpleNamespace(hf_config=hf), **defaults)
get_model_config=lambda: SimpleNamespace(hf_config=hf), **defaults
)
) )
with patch.object(overrides_module, "is_hip", return_value=True): with patch.object(overrides_module, "is_hip", return_value=True):
@@ -2148,9 +2117,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
) )
defaults.update(kw) defaults.update(kw)
return ResolvedView( return ResolvedView(
SimpleNamespace( SimpleNamespace(_model_config=SimpleNamespace(hf_config=hf), **defaults)
get_model_config=lambda: SimpleNamespace(hf_config=hf), **defaults
)
) )
# arch guard: non-mamba arch declares nothing # arch guard: non-mamba arch declares nothing
@@ -2255,17 +2222,28 @@ class TestGoldenModelOverrides(_IsolatedPublish):
def _args(default_backend, **kw): def _args(default_backend, **kw):
defaults = dict( defaults = dict(
attention_backend=None, attention_backend=None,
_get_default_attn_backend=lambda **_: default_backend, prefill_attention_backend=None,
use_mla_backend=lambda: False, decode_attention_backend=None,
get_model_config=lambda: None,
mamba_radix_cache_strategy="auto", mamba_radix_cache_strategy="auto",
disable_radix_cache=False, disable_radix_cache=False,
speculative_algorithm=None, speculative_algorithm=None,
) )
defaults.update(kw) defaults.update(kw)
return SimpleNamespace(**defaults) args = SimpleNamespace(**defaults)
args.default_backend_for_test = default_backend
return args
with patch.object(overrides_module, "is_sm100_supported", return_value=True): with patch.object(
overrides_module, "is_sm100_supported", return_value=True
), patch.object(
overrides_module,
"get_default_attn_backend",
lambda server_args, **_: server_args.default_backend_for_test,
), patch.object(
overrides_module, "use_mla_backend", return_value=False
), patch.object(
overrides_module, "model_config_of", return_value=None
):
# radix on + no extra buffer + no spec -> page_size=1 path # radix on + no extra buffer + no spec -> page_size=1 path
self.assertEqual( self.assertEqual(
_qwen3_5_hybrid_overrides(_args("trtllm_mha"), None), _qwen3_5_hybrid_overrides(_args("trtllm_mha"), None),
@@ -2422,11 +2400,6 @@ class TestGoldenModelOverrides(_IsolatedPublish):
) )
defaults.update(kw) defaults.update(kw)
ns = SimpleNamespace(**defaults) ns = SimpleNamespace(**defaults)
ns.is_attention_backend_not_set = lambda: (
ns.attention_backend is None
and ns.prefill_attention_backend is None
and ns.decode_attention_backend is None
)
return ns return ns
hf = SimpleNamespace() hf = SimpleNamespace()
@@ -2475,6 +2448,9 @@ class TestGoldenModelOverrides(_IsolatedPublish):
prefill_attention_backend=None, prefill_attention_backend=None,
speculative_draft_attention_backend=None, speculative_draft_attention_backend=None,
page_size=1, page_size=1,
# `use_mla_backend` reads the model configuration; a non-MLA
# one keeps these assertions about the page constraints.
_model_config=SimpleNamespace(attention_arch=None),
) )
defaults.update(kw) defaults.update(kw)
return ResolvedView(SimpleNamespace(**defaults)) return ResolvedView(SimpleNamespace(**defaults))
@@ -2533,7 +2509,6 @@ class TestGoldenModelOverrides(_IsolatedPublish):
_fa4_page_constraint( _fa4_page_constraint(
_view( _view(
attention_backend="fa4", attention_backend="fa4",
use_mla_backend=lambda: False,
speculative_eagle_topk=None, speculative_eagle_topk=None,
) )
), ),
@@ -2543,7 +2518,6 @@ class TestGoldenModelOverrides(_IsolatedPublish):
_fa4_page_constraint( _fa4_page_constraint(
_view( _view(
attention_backend="fa4", attention_backend="fa4",
use_mla_backend=lambda: False,
speculative_eagle_topk=2, # EAGLE topk>1 keeps default speculative_eagle_topk=2, # EAGLE topk>1 keeps default
) )
), ),
@@ -2554,7 +2528,6 @@ class TestGoldenModelOverrides(_IsolatedPublish):
_intel_xpu_page_constraint( _intel_xpu_page_constraint(
_view( _view(
decode_attention_backend="intel_xpu", decode_attention_backend="intel_xpu",
use_mla_backend=lambda: False,
) )
), ),
{"page_size": 128}, {"page_size": 128},
@@ -2563,7 +2536,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
_intel_xpu_page_constraint( _intel_xpu_page_constraint(
_view( _view(
decode_attention_backend="intel_xpu", decode_attention_backend="intel_xpu",
use_mla_backend=lambda: True, _model_config=SimpleNamespace(attention_arch=AttentionArch.MLA),
page_size=16, # MLA decode accepts 16 page_size=16, # MLA decode accepts 16
) )
), ),
@@ -2585,7 +2558,8 @@ class TestGoldenModelOverrides(_IsolatedPublish):
defaults = dict( defaults = dict(
device="cuda", device="cuda",
attention_backend=None, attention_backend=None,
is_attention_backend_not_set=lambda: True, prefill_attention_backend=None,
decode_attention_backend=None,
# keep the (now-absorbed) quant/moe blocks inert so these # keep the (now-absorbed) quant/moe blocks inert so these
# assertions stay attention-only # assertions stay attention-only
moe_runner_backend="triton", moe_runner_backend="triton",
@@ -2674,7 +2648,7 @@ class TestGoldenModelOverrides(_IsolatedPublish):
_quantization_explicitly_unset=False, _quantization_explicitly_unset=False,
moe_a2a_backend="none", moe_a2a_backend="none",
moe_runner_backend="auto", moe_runner_backend="auto",
get_model_config=lambda: SimpleNamespace( _model_config=SimpleNamespace(
hf_config=SimpleNamespace( hf_config=SimpleNamespace(
architectures=[arch], quantization_config=quant_cfg architectures=[arch], quantization_config=quant_cfg
) )
@@ -2742,7 +2716,6 @@ class TestGoldenModelOverrides(_IsolatedPublish):
def _args(**kw): def _args(**kw):
defaults = dict( defaults = dict(
is_attention_backend_not_set=lambda: True,
attention_backend=None, attention_backend=None,
prefill_attention_backend=None, prefill_attention_backend=None,
decode_attention_backend=None, decode_attention_backend=None,
@@ -2869,7 +2842,9 @@ class TestGoldenModelOverrides(_IsolatedPublish):
defaults = dict( defaults = dict(
speculative_algorithm=None, speculative_algorithm=None,
enable_hierarchical_cache=False, enable_hierarchical_cache=False,
is_attention_backend_not_set=lambda: False, attention_backend="triton",
prefill_attention_backend=None,
decode_attention_backend=None,
) )
defaults.update(kw) defaults.update(kw)
return SimpleNamespace(**defaults) return SimpleNamespace(**defaults)
+15 -3
View File
@@ -16,6 +16,18 @@ from unittest.mock import patch
import sglang as _sglang import sglang as _sglang
import sglang.srt.server_args as server_args_module import sglang.srt.server_args as server_args_module
from sglang.srt.arg_groups.arg_utils import NS, A, Arg from sglang.srt.arg_groups.arg_utils import NS, A, Arg
from sglang.srt.arg_groups.overrides import (
attention_backends_of,
)
from sglang.srt.arg_groups.overrides import (
mamba_cache_chunk_size as mamba_cache_chunk_size_of,
)
from sglang.srt.arg_groups.overrides import (
max_prefill_buffer_tokens as max_prefill_buffer_tokens_of,
)
from sglang.srt.arg_groups.overrides import (
resolved_view,
)
from sglang.srt.runtime_context import ( from sglang.srt.runtime_context import (
Flags, Flags,
ParallelContext, ParallelContext,
@@ -404,7 +416,7 @@ class TestServerArgsScopedOverride(_IsolatedServerArgs):
published = ( published = (
get_context().override_server_args(_mamba_cache_chunk_size=64).install() get_context().override_server_args(_mamba_cache_chunk_size=64).install()
) )
self.assertEqual(published.mamba_cache_chunk_size, 64) self.assertEqual(mamba_cache_chunk_size_of(published), 64)
def test_installed_config_arms_the_strict_guard(self): def test_installed_config_arms_the_strict_guard(self):
# The published dummy must behave like a resolved config: bare writes # The published dummy must behave like a resolved config: bare writes
@@ -1156,7 +1168,7 @@ class TestDerivedPredicatesAgreeAcrossTiers(_IsolatedServerArgs):
) )
get_context().set_server_args(args) get_context().set_server_args(args)
self.assertEqual( self.assertEqual(
ServerArgs.max_prefill_buffer_tokens(args), max_prefill_buffer_tokens_of(args),
max_prefill_buffer_tokens(), max_prefill_buffer_tokens(),
) )
@@ -1243,7 +1255,7 @@ class TestDerivedPredicatesAgreeAcrossTiers(_IsolatedServerArgs):
) )
get_context().set_server_args(args) get_context().set_server_args(args)
self.assertEqual( self.assertEqual(
ServerArgs.get_attention_backends(args), attention_backends_of(resolved_view(args)),
attention_backends(), attention_backends(),
) )
@@ -34,6 +34,7 @@ from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=2, suite="base-a-test-cpu") register_cpu_ci(est_time=2, suite="base-a-test-cpu")
import sglang import sglang
from sglang.srt.arg_groups.overrides import attention_backends_of, resolved_view
_PACKAGE_ROOT = Path(next(iter(sglang.__path__))) / "srt" _PACKAGE_ROOT = Path(next(iter(sglang.__path__))) / "srt"
@@ -181,7 +182,7 @@ class TestSplitBackendsReachTheDecisions(CustomTestCase):
("decode_attention_backend", "flashinfer"), ("decode_attention_backend", "flashinfer"),
): ):
object.__setattr__(args, name, value) object.__setattr__(args, name, value)
self.assertIn("flashinfer", args.get_attention_backends()) self.assertIn("flashinfer", attention_backends_of(resolved_view(args)))
def test_support_triton_is_the_regression_being_guarded(self): def test_support_triton_is_the_regression_being_guarded(self):
from sglang.srt.utils.common import support_triton from sglang.srt.utils.common import support_triton
@@ -483,8 +483,8 @@ class TestSuppliedInstanceExposure(CustomTestCase):
tgts = [node.target] tgts = [node.target]
elif ( elif (
isinstance(node, ast.Call) isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute) and isinstance(node.func, ast.Name)
and node.func.attr == "_declare" and node.func.id == "declare_resolution"
): ):
targets |= { targets |= {
kw.arg kw.arg
@@ -622,7 +622,7 @@ class TestSuppliedInstanceExposure(CustomTestCase):
for path in sorted(root.rglob("*.py")): for path in sorted(root.rglob("*.py")):
rel = path.relative_to(root).as_posix() rel = path.relative_to(root).as_posix()
source = path.read_text(encoding="utf-8-sig") source = path.read_text(encoding="utf-8-sig")
if "_late_resolution" not in source: if "declare_late_resolution" not in source:
continue continue
try: try:
tree = ast.parse(source) tree = ast.parse(source)