config: the KV-cache configurator reads the bags (#34096)

This commit is contained in:
Cheng Wan
2026-08-09 14:45:42 -07:00
committed by GitHub
parent e216c2bc59
commit b4284f3eb7
6 changed files with 99 additions and 61 deletions
@@ -6,14 +6,23 @@ from sglang.srt.runtime_context import get_server_args
from sglang.srt.server_args import ServerArgs from sglang.srt.server_args import ServerArgs
def get_alloc_len_per_decode(server_args: ServerArgs) -> int: def get_alloc_len_per_decode(
server_args: ServerArgs, *, max_draft_tokens: Optional[int] = None
) -> int:
"""``max_draft_tokens`` lets a caller that already resolved the draft-token
bound (the KV-cache configurator reads it off the bags) size with that same
value; the default is the handed instance's own member, never the global."""
if server_args.speculative_algorithm is None: if server_args.speculative_algorithm is None:
return 1 return 1
# Spec decoding allocates max(topk * num_steps, num_draft_tokens) per decode step. # Spec decoding allocates max(topk * num_steps, num_draft_tokens) per decode step.
spec_steps = server_args.speculative_num_steps or 1 spec_steps = server_args.speculative_num_steps or 1
spec_topk = server_args.speculative_eagle_topk or 1 spec_topk = server_args.speculative_eagle_topk or 1
spec_tokens = server_args.max_speculative_num_draft_tokens spec_tokens = (
max_draft_tokens
if max_draft_tokens is not None
else server_args.max_speculative_num_draft_tokens
)
page_size = server_args.page_size page_size = server_args.page_size
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
@@ -31,7 +40,11 @@ def get_alloc_len_per_decode(server_args: ServerArgs) -> int:
return max(num_new_pages_per_topk * page_size * spec_topk, spec_tokens) return max(num_new_pages_per_topk * page_size * spec_topk, spec_tokens)
def get_alloc_reserve_per_decode(server_args: Optional[ServerArgs] = None) -> int: def get_alloc_reserve_per_decode(
server_args: Optional[ServerArgs] = None,
*,
max_draft_tokens: Optional[int] = None,
) -> int:
"""KV length reserved per request at each decode step. """KV length reserved per request at each decode step.
The 2x is a double-buffer that absorbs the kv_committed_len lag in overlap The 2x is a double-buffer that absorbs the kv_committed_len lag in overlap
@@ -43,23 +56,35 @@ def get_alloc_reserve_per_decode(server_args: Optional[ServerArgs] = None) -> in
""" """
if server_args is None: if server_args is None:
server_args = get_server_args() server_args = get_server_args()
return 2 * get_alloc_len_per_decode(server_args) return 2 * get_alloc_len_per_decode(server_args, max_draft_tokens=max_draft_tokens)
def get_req_to_token_extra_context_len(server_args: ServerArgs) -> int: def get_req_to_token_extra_context_len(
server_args: ServerArgs, *, max_draft_tokens: Optional[int] = None
) -> int:
"""req_to_token row headroom beyond the model context length. """req_to_token row headroom beyond the model context length.
Sized to hold the decode over-allocation; the spec v2 page>1 topk>1 holey Sized to hold the decode over-allocation; the spec v2 page>1 topk>1 holey
draft footprint can outgrow the default num_draft_tokens headroom. draft footprint can outgrow the default num_draft_tokens headroom.
``max_draft_tokens`` keeps this row headroom and the caller's other
draft-token-sized buffers on ONE resolved value: the KV-cache configurator
passes the bag-derived bound it also hands the pools, so the two cannot
disagree after a post-publish override. The default stays the handed
instance's member for callers sizing against a specific config object.
""" """
if max_draft_tokens is None:
max_draft_tokens = server_args.max_speculative_num_draft_tokens
# FIXME(lsyin): temporary fix for the context length issue under spec decoding # FIXME(lsyin): temporary fix for the context length issue under spec decoding
extra = 4 + (server_args.max_speculative_num_draft_tokens or 0) extra = 4 + (max_draft_tokens or 0)
if server_args.speculative_algorithm is not None and server_args.page_size > 1: if server_args.speculative_algorithm is not None and server_args.page_size > 1:
# kv_allocated_len is page-aligned (eagle_prepare_for_decode), so near # kv_allocated_len is page-aligned (eagle_prepare_for_decode), so near
# the context limit the aligned reserve can overshoot by page_size - 1; # the context limit the aligned reserve can overshoot by page_size - 1;
# without the headroom the row write silently lands in the neighbor row. # without the headroom the row write silently lands in the neighbor row.
extra = max( extra = max(
extra, extra,
get_alloc_reserve_per_decode(server_args) + server_args.page_size - 1, get_alloc_reserve_per_decode(server_args, max_draft_tokens=max_draft_tokens)
+ server_args.page_size
- 1,
) )
return extra return extra
@@ -64,6 +64,7 @@ from sglang.srt.mem_cache.memory_pool import (
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool
from sglang.srt.platforms import current_platform from sglang.srt.platforms import current_platform
from sglang.srt.runtime_context import ( from sglang.srt.runtime_context import (
configured_pp_size,
get_context, get_context,
get_disagg, get_disagg,
get_exec, get_exec,
@@ -71,6 +72,9 @@ from sglang.srt.runtime_context import (
get_parallel, get_parallel,
get_schedule, get_schedule,
get_spec, get_spec,
mamba_extra_buffer_enabled,
mamba_extra_buffer_lazy_enabled,
max_speculative_num_draft_tokens,
) )
from sglang.srt.server_args import ServerArgs from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
@@ -404,7 +408,7 @@ class KVCacheConfigurator:
mamba_spec_state_size=sizes.max_running_requests, mamba_spec_state_size=sizes.max_running_requests,
cache_params=self.mambaish_config.mamba2_cache_params, cache_params=self.mambaish_config.mamba2_cache_params,
device=self.device, device=self.device,
enable_mamba_extra_buffer=self.server_args.enable_mamba_extra_buffer(), enable_mamba_extra_buffer=mamba_extra_buffer_enabled(),
draft_model_idx=self.draft_model_idx, draft_model_idx=self.draft_model_idx,
speculative_eagle_topk=get_spec().speculative_eagle_topk, speculative_eagle_topk=get_spec().speculative_eagle_topk,
) )
@@ -512,7 +516,7 @@ class KVCacheConfigurator:
max_mamba_cache_size=get_schedule().max_mamba_cache_size, max_mamba_cache_size=get_schedule().max_mamba_cache_size,
max_num_reqs=max_num_reqs, max_num_reqs=max_num_reqs,
enable_memory_saver=get_exec().features.enable_memory_saver, enable_memory_saver=get_exec().features.enable_memory_saver,
enable_mamba_extra_buffer=self.server_args.enable_mamba_extra_buffer(), enable_mamba_extra_buffer=mamba_extra_buffer_enabled(),
speculative_num_draft_tokens=get_spec().speculative_num_draft_tokens, speculative_num_draft_tokens=get_spec().speculative_num_draft_tokens,
disable_overlap_schedule=get_schedule().disable_overlap_schedule, disable_overlap_schedule=get_schedule().disable_overlap_schedule,
need_sort=get_disagg().disaggregation_mode in ("decode", "prefill"), need_sort=get_disagg().disaggregation_mode in ("decode", "prefill"),
@@ -637,7 +641,7 @@ class KVCacheConfigurator:
elif current_platform.is_out_of_tree() and not self.mambaish_config: elif current_platform.is_out_of_tree() and not self.mambaish_config:
unsupported_pool_family = "out-of-tree platform KV pool" unsupported_pool_family = "out-of-tree platform KV pool"
elif ( elif (
self.server_args.attention_backend == "ascend" and not self.mambaish_config get_exec().kernel.attention_backend == "ascend" and not self.mambaish_config
): ):
unsupported_pool_family = "NPU/Ascend KV pool" unsupported_pool_family = "NPU/Ascend KV pool"
elif self.use_mla_backend and is_dsa_model: elif self.use_mla_backend and is_dsa_model:
@@ -661,7 +665,13 @@ class KVCacheConfigurator:
) )
def _build_req_to_token_pool(self, *, max_num_reqs: int) -> ReqToTokenPool: def _build_req_to_token_pool(self, *, max_num_reqs: int) -> ReqToTokenPool:
extra_max_context_len = get_req_to_token_extra_context_len(self.server_args) # The same bag-derived bound the pools below receive, so the row
# headroom and the speculative buffers cannot disagree after a
# post-publish override.
extra_max_context_len = get_req_to_token_extra_context_len(
self.server_args,
max_draft_tokens=max_speculative_num_draft_tokens(),
)
if get_disagg().disaggregation_mode == "decode": if get_disagg().disaggregation_mode == "decode":
# Extra slots for pre-allocated requests # Extra slots for pre-allocated requests
@@ -714,9 +724,9 @@ class KVCacheConfigurator:
if self.layer_info.start_layer <= i < self.layer_info.end_layer if self.layer_info.start_layer <= i < self.layer_info.end_layer
] ]
), ),
speculative_num_draft_tokens=self.server_args.max_speculative_num_draft_tokens, speculative_num_draft_tokens=max_speculative_num_draft_tokens(),
speculative_eagle_topk=get_spec().speculative_eagle_topk, speculative_eagle_topk=get_spec().speculative_eagle_topk,
enable_mamba_extra_buffer=self.server_args.enable_mamba_extra_buffer(), enable_mamba_extra_buffer=mamba_extra_buffer_enabled(),
pre_alloc_size=pre_alloc_size, pre_alloc_size=pre_alloc_size,
enable_overlap_schedule=not get_schedule().disable_overlap_schedule, enable_overlap_schedule=not get_schedule().disable_overlap_schedule,
mamba_size=get_schedule().max_mamba_cache_size, mamba_size=get_schedule().max_mamba_cache_size,
@@ -768,7 +778,7 @@ class KVCacheConfigurator:
) -> ReqToTokenPool: ) -> ReqToTokenPool:
# DSPARK/DFLASH commit routes through the backend fold (KDA-only); a # DSPARK/DFLASH commit routes through the backend fold (KDA-only); a
# non-KDA model there would scatter a None intermediate_ssm and crash. # non-KDA model there would scatter a None intermediate_ssm and crash.
_algo = (self.server_args.speculative_algorithm or "").upper() _algo = (get_spec().speculative_algorithm or "").upper()
if ( if (
get_exec().mamba.enable_linear_replayssm_spec get_exec().mamba.enable_linear_replayssm_spec
and _algo in ("DSPARK", "DFLASH") and _algo in ("DSPARK", "DFLASH")
@@ -793,9 +803,9 @@ class KVCacheConfigurator:
if self.layer_info.start_layer <= i < self.layer_info.end_layer if self.layer_info.start_layer <= i < self.layer_info.end_layer
] ]
), ),
enable_mamba_extra_buffer=self.server_args.enable_mamba_extra_buffer(), enable_mamba_extra_buffer=mamba_extra_buffer_enabled(),
enable_mamba_extra_buffer_lazy=self.server_args.enable_mamba_extra_buffer_lazy(), enable_mamba_extra_buffer_lazy=mamba_extra_buffer_lazy_enabled(),
speculative_num_draft_tokens=self.server_args.max_speculative_num_draft_tokens, speculative_num_draft_tokens=max_speculative_num_draft_tokens(),
speculative_eagle_topk=get_spec().speculative_eagle_topk, speculative_eagle_topk=get_spec().speculative_eagle_topk,
enable_overlap_schedule=not get_schedule().disable_overlap_schedule, enable_overlap_schedule=not get_schedule().disable_overlap_schedule,
start_layer=self.layer_info.start_layer, start_layer=self.layer_info.start_layer,
@@ -884,7 +894,7 @@ class KVCacheConfigurator:
max_total_num_tokens=sizes.max_total_num_tokens, max_total_num_tokens=sizes.max_total_num_tokens,
) )
elif ( elif (
self.server_args.attention_backend == "ascend" and not self.mambaish_config get_exec().kernel.attention_backend == "ascend" and not self.mambaish_config
): ):
if self.is_hybrid_swa: if self.is_hybrid_swa:
token_to_kv_pool = self._build_ascend_swa_kv_pool( token_to_kv_pool = self._build_ascend_swa_kv_pool(
@@ -1033,9 +1043,7 @@ class KVCacheConfigurator:
start_layer=self.layer_info.start_layer, start_layer=self.layer_info.start_layer,
end_layer=self.layer_info.end_layer, end_layer=self.layer_info.end_layer,
enable_hisparse=get_memory().enable_hisparse, enable_hisparse=get_memory().enable_hisparse,
online_mtp_max_draft_tokens=( online_mtp_max_draft_tokens=(max_speculative_num_draft_tokens() or 0),
self.server_args.max_speculative_num_draft_tokens or 0
),
) )
return token_to_kv_pool return token_to_kv_pool
@@ -1496,7 +1504,7 @@ class KVCacheConfigurator:
need_sort=need_sort, need_sort=need_sort,
) )
elif _is_npu and ( elif _is_npu and (
self.server_args.attention_backend == "ascend" get_exec().kernel.attention_backend == "ascend"
or is_dsv4_model or is_dsv4_model
or self.hybrid_gdn_config is not None or self.hybrid_gdn_config is not None
): ):
@@ -1686,17 +1694,17 @@ class KVCacheConfigurator:
) )
additional_ratio = 0 additional_ratio = 0
if self.server_args.enable_mamba_extra_buffer(): if mamba_extra_buffer_enabled():
# ping-pong buffer size is 2 when overlap schedule is on, 1 otherwise. # ping-pong buffer size is 2 when overlap schedule is on, 1 otherwise.
# Lazy mode saves 1 slot (2 → 1) for overlap; non-overlap already uses 1. # Lazy mode saves 1 slot (2 → 1) for overlap; non-overlap already uses 1.
if not get_schedule().disable_overlap_schedule: if not get_schedule().disable_overlap_schedule:
if self.server_args.enable_mamba_extra_buffer_lazy(): if mamba_extra_buffer_lazy_enabled():
additional_ratio = MAMBA_CACHE_V2_ADDITIONAL_RATIO_OVERLAP_LAZY additional_ratio = MAMBA_CACHE_V2_ADDITIONAL_RATIO_OVERLAP_LAZY
else: else:
additional_ratio = MAMBA_CACHE_V2_ADDITIONAL_RATIO_OVERLAP additional_ratio = MAMBA_CACHE_V2_ADDITIONAL_RATIO_OVERLAP
else: else:
assert ( assert (
not self.server_args.enable_mamba_extra_buffer_lazy() not mamba_extra_buffer_lazy_enabled()
), "Lazy extra buffer requires overlap schedule (--disable-overlap-schedule is incompatible)" ), "Lazy extra buffer requires overlap schedule (--disable-overlap-schedule is incompatible)"
additional_ratio = MAMBA_CACHE_V2_ADDITIONAL_RATIO_NO_OVERLAP additional_ratio = MAMBA_CACHE_V2_ADDITIONAL_RATIO_NO_OVERLAP
elif skip_decode_lock: elif skip_decode_lock:
@@ -1725,7 +1733,7 @@ class KVCacheConfigurator:
token_capacity = min(token_capacity, user_limit) token_capacity = min(token_capacity, user_limit)
# Sync across PP ranks (each may have different layer counts) # Sync across PP ranks (each may have different layer counts)
if self.server_args.pp_size > 1: if configured_pp_size() > 1:
tensor = torch.tensor(token_capacity, dtype=torch.int64) tensor = torch.tensor(token_capacity, dtype=torch.int64)
torch.distributed.all_reduce( torch.distributed.all_reduce(
tensor, tensor,
@@ -1835,7 +1843,6 @@ class KVCacheConfigurator:
def _handle_max_mamba_cache(self, total_rest_memory): def _handle_max_mamba_cache(self, total_rest_memory):
config = self.mambaish_config config = self.mambaish_config
server_args = self.server_args
assert config is not None assert config is not None
# mamba_cache_per_req covers every mamba layer, but under PP a rank only # mamba_cache_per_req covers every mamba layer, but under PP a rank only
@@ -1873,10 +1880,11 @@ class KVCacheConfigurator:
if replayssm_active: if replayssm_active:
# GDN sizes the fold window to the draft maximum; the KDA ring # GDN sizes the fold window to the draft maximum; the KDA ring
# stays --linear-replayssm-cache-len long (mirrors MambaPool). # stays --linear-replayssm-cache-len long (mirrors MambaPool).
max_draft_tokens = max_speculative_num_draft_tokens()
if kimi_linear_config(self.model_config) is not None: if kimi_linear_config(self.model_config) is not None:
record_len = get_exec().mamba.linear_replayssm_cache_len record_len = get_exec().mamba.linear_replayssm_cache_len
elif server_args.max_speculative_num_draft_tokens is not None: elif max_draft_tokens is not None:
record_len = server_args.max_speculative_num_draft_tokens record_len = max_draft_tokens
else: else:
record_len = get_exec().mamba.linear_replayssm_cache_len record_len = get_exec().mamba.linear_replayssm_cache_len
replayssm_ring_per_req = ( replayssm_ring_per_req = (
+15 -10
View File
@@ -969,12 +969,15 @@ class RuntimeContext:
dummy-boundary ``ServerArgs`` carrying ``fields`` and returns it; dummy-boundary ``ServerArgs`` carrying ``fields`` and returns it;
``restore()`` (or exiting) reinstates whatever the slot held before. ``restore()`` (or exiting) reinstates whatever the slot held before.
Transitional — to be deprecated: it exists because production code This is the sanctioned way for a test to get a published context, and
still branches on raw ``server_args`` fields at runtime, so forcing a it stays. The transitional reason it was introduced for — production
path needs a full config in the slot. As those readers migrate onto code branching on raw ``server_args`` fields at runtime — is gone (the
the named runtime tiers (flags / resources / forward), prefer the read ratchet pins business reads at zero), but a test that exercises
finer-grained overrides; once they cover the branching surface this bag readers still needs bags, and the bag tree is projected *from an
override loses its clients and goes away. instance*: something has to publish one. Prefer the finer-grained
scoped overrides (``get_exec().override(...)``, the flag groups'
``override``) on top of a published context when a test only needs to
force one leaf.
""" """
return _ServerArgsOverride(self, fields) return _ServerArgsOverride(self, fields)
@@ -1194,10 +1197,12 @@ ROLE_NAMESPACE_SETS: dict[str, frozenset[str] | None] = {
# Audited (record-mode smokes, plain + DP-attention): the DP controller # Audited (record-mode smokes, plain + DP-attention): the DP controller
# reads only the elastic-EP gate; its module's static read set agrees. # reads only the elastic-EP gate; its module's static read set agrees.
"dp_controller": frozenset({"exec"}), "dp_controller": frozenset({"exec"}),
# Zero bag reads observed (per-instance managers read self.server_args by # Record-mode audit (2026-08-06, text model, /generate + /get_server_info +
# design). Keep full: it may need namespaces (e.g. disagg) once tokenizer # /v1/models): reads exactly {"serving"} — the per-instance managers read
# paths migrate off self.server_args, and restricting on a zero-read # self.server_args by design. Still declared full, because that run did not
# audit would be guesswork. # exercise the multimodal processors, LoRA/score endpoints, the disagg
# roles, or the gRPC bridge; narrowing needs those shapes audited too, and
# a wrong set fails a request rather than a test.
"tokenizer": None, "tokenizer": None,
# Deployment shapes not exercised locally; audit before restricting. # Deployment shapes not exercised locally; audit before restricting.
"encoder": None, "encoder": None,
+8 -13
View File
@@ -191,25 +191,20 @@ class TestGetDcpLens(CustomTestCase):
) )
allocators = {} allocators = {}
# The configurator's bag reads (disaggregation_mode / page_size / # The configurator's own inputs are published leaves now, so the case
# enable_hisparse) come from the published context; the per-iteration # publishes them once. The DCP *scale* is not one of them: the allocator
# dcp_size stays on the injected instance stand-in. # widens from the live get_parallel().attn_dcp_size, which the per-size
self._sa_override = rc.get_context().override_server_args( # override inside the loop drives.
override = rc.get_context().override_server_args(
disaggregation_mode="null", disaggregation_mode="null",
page_size=physical_page_size, page_size=physical_page_size,
enable_hisparse=False, enable_hisparse=False,
) )
self._sa_override.install() override.install()
self.addCleanup(self._sa_override.restore) self.addCleanup(override.restore)
for dcp_size in (1, 4): for dcp_size in (1, 4):
configurator = SimpleNamespace( configurator = SimpleNamespace(
server_args=SimpleNamespace( server_args=SimpleNamespace(),
disaggregation_mode="null",
enable_hisparse=False,
page_size=physical_page_size,
dcp_size=dcp_size,
),
hybrid_gdn_config=None, hybrid_gdn_config=None,
is_hybrid_swa=False, is_hybrid_swa=False,
kv_cache_dtype=torch.bfloat16, kv_cache_dtype=torch.bfloat16,
@@ -110,22 +110,22 @@ class TestMambaRatioEnvGate(unittest.TestCase):
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.mem_cache.kv_cache_configurator import KVCacheConfigurator from sglang.srt.mem_cache.kv_cache_configurator import KVCacheConfigurator
server_args = SimpleNamespace( fake = SimpleNamespace(server_args=SimpleNamespace())
disable_radix_cache=False, # Every input is a published leaf now: the extra-buffer predicates read
disable_overlap_schedule=disable_overlap, # the radix-cache strategy off the bags, so the fixture publishes the
enable_mamba_extra_buffer=lambda: extra_buffer, # strategy that produces the combination under test.
enable_mamba_extra_buffer_lazy=lambda: lazy, strategy = (
"extra_buffer_lazy"
if lazy
else "extra_buffer" if extra_buffer else "no_buffer"
) )
fake = SimpleNamespace(server_args=server_args)
# The bag reads (disable_radix_cache / disable_overlap_schedule) come
# from the published context; the derived-method calls stay on the
# injected stand-in.
from sglang.srt import runtime_context as rc from sglang.srt import runtime_context as rc
with envs.SGLANG_OPT_MAMBA_SKIP_DECODE_LOCK.override(skip): with envs.SGLANG_OPT_MAMBA_SKIP_DECODE_LOCK.override(skip):
with rc.get_context().override_server_args( with rc.get_context().override_server_args(
disable_radix_cache=False, disable_radix_cache=False,
disable_overlap_schedule=disable_overlap, disable_overlap_schedule=disable_overlap,
mamba_radix_cache_strategy=strategy,
): ):
return KVCacheConfigurator._calculate_mamba_ratio(fake) return KVCacheConfigurator._calculate_mamba_ratio(fake)
@@ -85,6 +85,11 @@ _CONFIGURED_SIZE_CALL_SITES = {
"point, since with PP off the group is never touched, which is what lets " "point, since with PP off the group is never touched, which is what lets "
"the Indexer be constructed before distributed init" "the Indexer be constructed before distributed init"
), ),
("srt/mem_cache/kv_cache_configurator.py", "configured_pp_size"): (
"decides whether the token capacity needs a cross-PP all-reduce at all; "
"asking the configured size keeps that decision independent of whether a "
"PP group is installed in this process"
),
("srt/layers/dp_attention.py", "configured_attn_cp_size"): ( ("srt/layers/dp_attention.py", "configured_attn_cp_size"): (
"compared against the configured moe_dp_size below" "compared against the configured moe_dp_size below"
), ),