94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
795 lines
32 KiB
Python
795 lines
32 KiB
Python
"""Memory pool configurators for profiling and sizing KV cache pools.
|
|
|
|
Each model architecture has its own configurator that computes pool sizes
|
|
from available GPU memory using a unified coeff+bias model:
|
|
|
|
available_bytes = max_tokens * coeff + bias
|
|
max_tokens = (available_bytes - bias) / coeff
|
|
|
|
Two entry points, same core computation:
|
|
- calculate_pool_sizes(available_bytes, page_size): profiling path
|
|
- calculate_pool_sizes_from_max_tokens(max_tokens, page_size): constraint path
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Optional
|
|
|
|
import torch
|
|
|
|
from sglang.srt.configs.model_config import (
|
|
get_dsa_index_head_dim,
|
|
get_minimax_sparse_attention_config,
|
|
get_minimax_sparse_disable_value_layer_ids,
|
|
get_minimax_sparse_layer_ids,
|
|
is_deepseek_dsa,
|
|
is_deepseek_v4,
|
|
is_minimax_sparse,
|
|
)
|
|
from sglang.srt.environ import envs
|
|
from sglang.srt.mem_cache.common import get_alloc_len_per_decode
|
|
from sglang.srt.mem_cache.deepseek_v4_memory_pool import get_compress_state_ring_size
|
|
from sglang.srt.mem_cache.memory_pool import DSATokenToKVPool
|
|
from sglang.srt.runtime_context import get_parallel
|
|
from sglang.srt.utils.common import (
|
|
ceil_align,
|
|
ceil_div,
|
|
is_float4_e2m1fn_x2,
|
|
spec_decode_alloc_len_per_request,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class MemoryPoolConfig:
|
|
"""Resolved memory pool config, shared between target and draft workers."""
|
|
|
|
max_total_num_tokens: int
|
|
max_running_requests: Optional[int] = None
|
|
full_max_total_num_tokens: Optional[int] = None
|
|
swa_max_total_num_tokens: Optional[int] = None
|
|
|
|
# DSV4 compressed-attention pool sizes (target only; draft workers leave at 0).
|
|
c4_max_total_num_tokens: int = 0
|
|
c128_max_total_num_tokens: int = 0
|
|
c4_state_pool_size: int = 0
|
|
c128_state_pool_size: int = 0
|
|
|
|
mem_fraction_static: Optional[float] = None
|
|
|
|
def __post_init__(self):
|
|
if self.max_total_num_tokens <= 0:
|
|
msg = "Not enough memory. Please try to increase --mem-fraction-static."
|
|
if self.mem_fraction_static is not None:
|
|
msg += f" Current value: mem_fraction_static={self.mem_fraction_static}"
|
|
raise RuntimeError(msg)
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from sglang.srt.model_executor.model_runner import ModelRunner
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _get_dsv4_compress_state_dtype_sizes() -> tuple[int, int]:
|
|
dtype_name = envs.SGLANG_DSV4_COMPRESS_STATE_DTYPE.get().strip().lower()
|
|
if dtype_name in ("float32", "fp32"):
|
|
return 4, 4
|
|
if dtype_name in ("bfloat16", "bf16"):
|
|
if envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
|
raise ValueError(
|
|
"SGLANG_DSV4_COMPRESS_STATE_DTYPE=bf16 is not supported when "
|
|
"SGLANG_OPT_USE_ONLINE_COMPRESS=1; online c128 state must stay float32."
|
|
)
|
|
return 2, 2
|
|
raise ValueError(
|
|
"Unsupported SGLANG_DSV4_COMPRESS_STATE_DTYPE="
|
|
f"{dtype_name!r}. Expected one of: float32, fp32, bfloat16, bf16."
|
|
)
|
|
|
|
|
|
class MemoryPoolConfigurator:
|
|
"""Base class for memory pool configurators.
|
|
|
|
Subclasses compute pool sizes for their architecture via coeff+bias model.
|
|
Both entry points return MemoryPoolConfig (with max_running_requests=None,
|
|
to be filled by the consumer).
|
|
"""
|
|
|
|
def calculate_pool_sizes(
|
|
self, available_bytes: int, page_size: int
|
|
) -> MemoryPoolConfig:
|
|
"""Profiling path: compute pool sizes from available bytes."""
|
|
raise NotImplementedError
|
|
|
|
def calculate_pool_sizes_from_max_tokens(
|
|
self, max_total_num_tokens: int, page_size: int
|
|
) -> MemoryPoolConfig:
|
|
"""Constraint path: recalculate pool sizes from a constrained max_tokens."""
|
|
raise NotImplementedError
|
|
|
|
def finalize_with_max_running_requests(
|
|
self, config: MemoryPoolConfig
|
|
) -> MemoryPoolConfig:
|
|
return config
|
|
|
|
|
|
class DefaultPoolConfigurator(MemoryPoolConfigurator):
|
|
"""Configurator for standard models: MHA, MLA, DSA, FP4.
|
|
|
|
coeff = cell_size (bytes per token across all layers)
|
|
bias = 0
|
|
"""
|
|
|
|
def __init__(self, mr: ModelRunner):
|
|
# Determine effective number of layers for KV cache
|
|
if mambaish := mr.mambaish_config:
|
|
effective_layer_ids = [
|
|
i
|
|
for i in mambaish.full_attention_layer_ids
|
|
if mr.start_layer <= i < mr.end_layer
|
|
]
|
|
num_layers = len(effective_layer_ids)
|
|
else:
|
|
num_layers = mr.num_effective_layers
|
|
|
|
self._cell_size = self._compute_cell_size(mr, num_layers)
|
|
|
|
# EAGLE/STANDALONE: scale cell_size to account for draft model KV cache.
|
|
# Assumes draft and target share the same per-layer KV size (head_dim,
|
|
# num_kv_heads, dtype), which holds for EAGLE/MTP draft models that
|
|
# reuse the target architecture's attention config.
|
|
if (
|
|
mr.spec_algorithm.is_eagle() or mr.spec_algorithm.is_standalone()
|
|
) and not mr.is_draft_worker:
|
|
eagle_draft_num_layers = getattr(mr, "eagle_draft_num_layers", None)
|
|
if (
|
|
eagle_draft_num_layers is not None
|
|
and int(eagle_draft_num_layers) > 0
|
|
and int(num_layers) > 0
|
|
):
|
|
self._cell_size = int(
|
|
self._cell_size
|
|
* (1 + int(eagle_draft_num_layers) / int(num_layers))
|
|
)
|
|
|
|
# DFLASH/DSPARK: scale cell_size to account for draft model KV cache
|
|
if mr.spec_algorithm.is_dflash_family() and not mr.is_draft_worker:
|
|
from sglang.srt.speculative.dflash_utils import (
|
|
scale_kv_cell_size_per_token_for_dflash,
|
|
)
|
|
|
|
draft_num_layers = mr.dflash_family_draft_num_layers
|
|
if (
|
|
draft_num_layers is not None
|
|
and int(draft_num_layers) > 0
|
|
and int(num_layers) > 0
|
|
):
|
|
self._cell_size = scale_kv_cell_size_per_token_for_dflash(
|
|
target_cell_size_per_token=self._cell_size,
|
|
target_num_layers=int(num_layers),
|
|
draft_num_layers=int(draft_num_layers),
|
|
)
|
|
|
|
def _compute_cell_size(self, mr: ModelRunner, num_layers: int) -> int:
|
|
"""Compute per-token KV cache cost in bytes. Subclasses can override."""
|
|
# args to config cell size
|
|
model_config = mr.model_config
|
|
kv_cache_dtype = mr.kv_cache_dtype
|
|
from sglang.srt.layers.cp.utils import (
|
|
get_glm_dsa_layer_split_effective_num_layers,
|
|
)
|
|
|
|
effective_num_layers = get_glm_dsa_layer_split_effective_num_layers(
|
|
mr, num_layers
|
|
)
|
|
|
|
kv_size = torch._utils._element_size(kv_cache_dtype)
|
|
tp_size = get_parallel().attn_tp_size
|
|
|
|
if mr.use_mla_backend:
|
|
cell_size = (
|
|
(model_config.kv_lora_rank + model_config.qk_rope_head_dim)
|
|
* effective_num_layers
|
|
* kv_size
|
|
)
|
|
if is_float4_e2m1fn_x2(kv_cache_dtype):
|
|
# kv_scale_buffer
|
|
scale_block_size = 16
|
|
cell_size = (cell_size // 2) + (
|
|
(
|
|
(model_config.kv_lora_rank + model_config.qk_rope_head_dim)
|
|
// scale_block_size
|
|
)
|
|
* effective_num_layers
|
|
* kv_size
|
|
)
|
|
|
|
# Add indexer KV cache overhead for DSA models (DeepSeek V3.2)
|
|
if is_deepseek_dsa(model_config.hf_config):
|
|
index_head_dim = get_dsa_index_head_dim(model_config.hf_config)
|
|
indexer_size_per_token = (
|
|
index_head_dim
|
|
+ index_head_dim // DSATokenToKVPool.quant_block_size * 4
|
|
)
|
|
element_size = torch._utils._element_size(
|
|
DSATokenToKVPool.index_k_with_scale_buffer_dtype
|
|
)
|
|
cell_size += (
|
|
indexer_size_per_token * effective_num_layers * element_size
|
|
)
|
|
elif is_minimax_sparse(model_config.hf_config):
|
|
# Mirrors MiniMaxSparseKVPool: main pool (K+V all layers) + indexer pool
|
|
# (sparse-only, single-head; kv layers store K+V, k-only layers store K).
|
|
sparse_cfg = get_minimax_sparse_attention_config(model_config.hf_config)
|
|
dense_layer_ids, sparse_layer_ids = get_minimax_sparse_layer_ids(sparse_cfg)
|
|
indexer_k_only_layer_ids = set(
|
|
get_minimax_sparse_disable_value_layer_ids(sparse_cfg)
|
|
)
|
|
|
|
local_dense_layer_ids = [
|
|
l for l in dense_layer_ids if mr.start_layer <= l < mr.end_layer
|
|
]
|
|
local_sparse_layer_ids = [
|
|
l for l in sparse_layer_ids if mr.start_layer <= l < mr.end_layer
|
|
]
|
|
num_dense = len(local_dense_layer_ids)
|
|
num_sparse = len(local_sparse_layer_ids)
|
|
num_indexer_k_only = sum(
|
|
1 for l in local_sparse_layer_ids if l in indexer_k_only_layer_ids
|
|
)
|
|
num_indexer_kv = num_sparse - num_indexer_k_only
|
|
|
|
kv_heads = model_config.get_num_kv_heads(get_parallel().attn_tp_size)
|
|
head_dim = model_config.head_dim
|
|
indexer_head_dim = sparse_cfg["sparse_index_dim"]
|
|
indexer_dtype_size = torch._utils._element_size(mr.dtype)
|
|
|
|
main_pool_bytes = (
|
|
(num_dense + num_sparse) * 2 * kv_heads * head_dim * kv_size
|
|
)
|
|
indexer_bytes = (
|
|
(num_indexer_kv * 2 + num_indexer_k_only)
|
|
* indexer_head_dim
|
|
* indexer_dtype_size
|
|
)
|
|
# FP4 scale buffer adjustment doesn't apply to MiniMax sparse:
|
|
# cell_size is already a sum over heterogeneous sub-pools.
|
|
return main_pool_bytes + indexer_bytes
|
|
else:
|
|
cell_size = (
|
|
model_config.get_num_kv_heads(tp_size)
|
|
* (model_config.head_dim + model_config.v_head_dim)
|
|
* effective_num_layers
|
|
* kv_size
|
|
)
|
|
|
|
if is_float4_e2m1fn_x2(kv_cache_dtype):
|
|
# kv_scale_buffer
|
|
scale_block_size = 16
|
|
n = model_config.get_num_kv_heads(tp_size)
|
|
k = model_config.head_dim
|
|
cell_size = (cell_size // 2) + (
|
|
(n * k * effective_num_layers * 2 * kv_size) // scale_block_size
|
|
)
|
|
|
|
return cell_size
|
|
|
|
def calculate_pool_sizes(
|
|
self, available_bytes: int, page_size: int
|
|
) -> MemoryPoolConfig:
|
|
max_total_num_tokens = available_bytes // self._cell_size
|
|
max_total_num_tokens = max_total_num_tokens // page_size * page_size
|
|
return MemoryPoolConfig(max_total_num_tokens=max_total_num_tokens)
|
|
|
|
def calculate_pool_sizes_from_max_tokens(
|
|
self, max_total_num_tokens: int, page_size: int
|
|
) -> MemoryPoolConfig:
|
|
max_total_num_tokens = max_total_num_tokens // page_size * page_size
|
|
return MemoryPoolConfig(max_total_num_tokens=max_total_num_tokens)
|
|
|
|
|
|
class HybridSWAPoolConfigurator(MemoryPoolConfigurator):
|
|
"""Configurator for hybrid sliding window attention models (Gemma2, Command-R, MiMo).
|
|
|
|
Splits available memory between full attention and SWA pools.
|
|
Does NOT inherit DefaultPoolConfigurator — different coeff model.
|
|
"""
|
|
|
|
def __init__(self, mr: ModelRunner):
|
|
model_config = mr.model_config
|
|
kv_cache_dtype = mr.kv_cache_dtype
|
|
kv_size = torch._utils._element_size(kv_cache_dtype)
|
|
tp_size = get_parallel().attn_tp_size
|
|
|
|
self._full_layers_num = len(model_config.full_attention_layer_ids)
|
|
self._swa_layers_num = len(model_config.swa_attention_layer_ids)
|
|
assert (
|
|
self._swa_layers_num > 0
|
|
), "Hybrid SWA model must have at least one SWA layer"
|
|
|
|
self._swa_full_tokens_ratio = mr.server_args.swa_full_tokens_ratio
|
|
|
|
# Full layer per-token memory (bytes)
|
|
self._full_per_token = (
|
|
model_config.get_num_kv_heads(tp_size)
|
|
* (model_config.head_dim + model_config.v_head_dim)
|
|
* kv_size
|
|
)
|
|
|
|
# SWA layer per-token memory (bytes)
|
|
self._swa_per_token = (
|
|
model_config.get_swa_num_kv_heads(tp_size)
|
|
* (model_config.swa_head_dim + model_config.swa_v_head_dim)
|
|
* kv_size
|
|
)
|
|
|
|
# EAGLE/STANDALONE draft KV pool inherits max_total tokens with its
|
|
# full-attn layers; budget into the full term.
|
|
self._draft_full_layers_num = 0
|
|
if (
|
|
mr.spec_algorithm.is_eagle() or mr.spec_algorithm.is_standalone()
|
|
) and not mr.is_draft_worker:
|
|
draft_layers = getattr(mr, "eagle_draft_num_layers", None)
|
|
if draft_layers is not None and int(draft_layers) > 0:
|
|
self._draft_full_layers_num = int(draft_layers)
|
|
|
|
# Bytes per token of max_total_num_tokens.
|
|
#
|
|
# Hybrid (full_layers > 0): max_total = full_tokens, so cell_size accounts
|
|
# for both pools: F*nf + r*S*ns (where swa_tokens = full_tokens * r).
|
|
#
|
|
# All-SWA (full_layers == 0): max_total = swa_tokens directly. The ratio
|
|
# is meaningless here -- there is no full pool to relate to, and every
|
|
# token beyond the sliding window can be evicted. So cell_size = S*ns,
|
|
# with no ratio factor applied.
|
|
if self._full_layers_num == 0:
|
|
self._cell_size = (
|
|
self._swa_per_token * self._swa_layers_num
|
|
+ self._full_per_token * self._draft_full_layers_num
|
|
)
|
|
else:
|
|
self._cell_size = (
|
|
self._full_per_token
|
|
* (self._full_layers_num + self._draft_full_layers_num)
|
|
+ self._swa_full_tokens_ratio
|
|
* self._swa_per_token
|
|
* self._swa_layers_num
|
|
)
|
|
|
|
def _solve_pool_sizes(
|
|
self, max_total_num_tokens: int, page_size: int
|
|
) -> MemoryPoolConfig:
|
|
"""Core computation: split max_total_num_tokens into full/swa pool sizes."""
|
|
|
|
def align_page_size(x: int) -> int:
|
|
return (x // page_size) * page_size
|
|
|
|
if self._full_layers_num == 0:
|
|
# All-SWA: no full pool, max_total = actual SWA pool size.
|
|
# Ratio is not applied -- see __init__ comment.
|
|
swa_tokens = align_page_size(max_total_num_tokens)
|
|
logger.info(
|
|
f"Use sliding window memory pool (all SWA). "
|
|
f"swa_layer_tokens={swa_tokens}"
|
|
)
|
|
return MemoryPoolConfig(
|
|
max_total_num_tokens=swa_tokens,
|
|
full_max_total_num_tokens=0,
|
|
swa_max_total_num_tokens=swa_tokens,
|
|
)
|
|
|
|
# Hybrid: full_tokens = max_total_num_tokens, swa_tokens = full_tokens * ratio
|
|
full_tokens = align_page_size(max_total_num_tokens)
|
|
swa_tokens = align_page_size(int(full_tokens * self._swa_full_tokens_ratio))
|
|
|
|
logger.info(
|
|
f"Use sliding window memory pool. "
|
|
f"full_layer_tokens={full_tokens}, swa_layer_tokens={swa_tokens}"
|
|
)
|
|
|
|
return MemoryPoolConfig(
|
|
max_total_num_tokens=full_tokens,
|
|
full_max_total_num_tokens=full_tokens,
|
|
swa_max_total_num_tokens=swa_tokens,
|
|
)
|
|
|
|
def calculate_pool_sizes(
|
|
self, available_bytes: int, page_size: int
|
|
) -> MemoryPoolConfig:
|
|
max_total_num_tokens = int(available_bytes // self._cell_size)
|
|
return self._solve_pool_sizes(max_total_num_tokens, page_size)
|
|
|
|
def calculate_pool_sizes_from_max_tokens(
|
|
self, max_total_num_tokens: int, page_size: int
|
|
) -> MemoryPoolConfig:
|
|
return self._solve_pool_sizes(max_total_num_tokens, page_size)
|
|
|
|
|
|
class SWAChunkCapPoolConfigurator(HybridSWAPoolConfigurator):
|
|
"""Hybrid SWA configurator with the SWA pool sized from a fixed token cap.
|
|
|
|
When max_running_requests is explicit, the SWA pool's worst-case
|
|
footprint is bounded per request. The SWA pool is sized tightly from that
|
|
cap and the freed memory is redirected to the full pool, instead of sizing
|
|
both pools by swa_full_tokens_ratio.
|
|
"""
|
|
|
|
def __init__(self, mr: ModelRunner):
|
|
super().__init__(mr)
|
|
assert self._full_layers_num > 0
|
|
|
|
sa = mr.server_args
|
|
page_size = mr.page_size
|
|
window = mr.sliding_window_size
|
|
draft_tokens = sa.speculative_num_draft_tokens or 1
|
|
eviction_interval = max(1, envs.SGLANG_SWA_EVICTION_INTERVAL.get())
|
|
|
|
"""
|
|
__________[padding][eviction_interval][window]
|
|
Padding to make sure eviction point is page-aligned.
|
|
"""
|
|
trailing_tokens = window + eviction_interval * draft_tokens + page_size
|
|
if sa.speculative_algorithm is None:
|
|
decode_alloc = page_size
|
|
elif sa.disable_overlap_schedule:
|
|
# spec-v1: new_tokens_required_next_decode per request.
|
|
decode_alloc = spec_decode_alloc_len_per_request(sa)
|
|
else:
|
|
# spec-v2: the overlap allocator keeps 2 * alloc_len outstanding
|
|
# (eagle_utils.eagle_prepare_for_decode: kv_committed_len + 2 * alloc_len).
|
|
decode_alloc = 2 * get_alloc_len_per_decode(sa)
|
|
per_request = trailing_tokens + decode_alloc
|
|
|
|
num_reqs = sa.max_running_requests // mr.dp_size
|
|
if sa.disaggregation_mode == "decode":
|
|
self._swa_cap = (
|
|
per_request * num_reqs
|
|
+ (window + page_size) * sa.disaggregation_decode_extra_slots
|
|
)
|
|
else:
|
|
chunks_in_flight = 1 if sa.disable_overlap_schedule else 2
|
|
self._swa_cap = (
|
|
per_request * num_reqs
|
|
+ chunks_in_flight * sa.chunked_prefill_size
|
|
+ page_size
|
|
)
|
|
|
|
@staticmethod
|
|
def is_applicable(mr: ModelRunner) -> bool:
|
|
"""True when SWAChunkCache can be sized from explicit max requests."""
|
|
sa = mr.server_args
|
|
if sa.max_running_requests is None:
|
|
return False
|
|
if not sa.disable_radix_cache:
|
|
return False
|
|
if sa.chunked_prefill_size is None:
|
|
return False
|
|
if mr.sliding_window_size is None:
|
|
return False
|
|
return len(mr.model_config.full_attention_layer_ids) > 0
|
|
|
|
def calculate_pool_sizes(
|
|
self, available_bytes: int, page_size: int
|
|
) -> MemoryPoolConfig:
|
|
# SWA pool sized tightly from the cap; the rest of the budget goes to full.
|
|
swa_tokens = ceil_align(self._swa_cap, page_size)
|
|
fixed_swa_bytes = swa_tokens * self._swa_per_token * self._swa_layers_num
|
|
full_cell_size = self._full_per_token * (
|
|
self._full_layers_num + self._draft_full_layers_num
|
|
)
|
|
full_tokens = (
|
|
int((available_bytes - fixed_swa_bytes) // full_cell_size) // page_size
|
|
) * page_size
|
|
if full_tokens <= 0:
|
|
raise RuntimeError(
|
|
f"SWA pool cap ({swa_tokens} tokens, "
|
|
f"{fixed_swa_bytes / (1 << 30):.2f} GiB) leaves no room for the full "
|
|
f"KV pool within the available {available_bytes / (1 << 30):.2f} GiB. "
|
|
f"Reduce --max-running-requests, lower SGLANG_SWA_EVICTION_INTERVAL, "
|
|
f"or increase --mem-fraction-static."
|
|
)
|
|
return MemoryPoolConfig(
|
|
max_total_num_tokens=full_tokens,
|
|
full_max_total_num_tokens=full_tokens,
|
|
swa_max_total_num_tokens=swa_tokens,
|
|
)
|
|
|
|
def calculate_pool_sizes_from_max_tokens(
|
|
self, max_total_num_tokens: int, page_size: int
|
|
) -> MemoryPoolConfig:
|
|
# Constrained max_total goes to the full pool; SWA stays at its cap.
|
|
swa_tokens = ceil_align(self._swa_cap, page_size)
|
|
full_tokens = (max_total_num_tokens // page_size) * page_size
|
|
return MemoryPoolConfig(
|
|
max_total_num_tokens=full_tokens,
|
|
full_max_total_num_tokens=full_tokens,
|
|
swa_max_total_num_tokens=min(swa_tokens, max_total_num_tokens),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class _DSV4PoolSizes:
|
|
full_max_total_num_tokens: int
|
|
swa_max_total_num_tokens: int
|
|
c4_max_total_num_tokens: int
|
|
c128_max_total_num_tokens: int
|
|
c4_state_pool_size: int
|
|
c128_state_pool_size: int
|
|
|
|
|
|
class DSV4PoolConfigurator(MemoryPoolConfigurator):
|
|
"""Configurator for DSV4 compressed-attention models.
|
|
|
|
Splits available memory across full / swa / c4 / c128 + c4_state / c128_state
|
|
pools. coeff is bytes_per_full_token (inflated by (T+D)/T when speculative
|
|
decode reserves a draft worker, mirroring dflash's cell_size scaling); bias = 0.
|
|
"""
|
|
|
|
def __init__(self, mr: ModelRunner):
|
|
cfg = mr.model_config
|
|
self.qk_nope_head_dim = cfg.qk_nope_head_dim
|
|
self.qk_rope_head_dim = cfg.qk_rope_head_dim
|
|
self.indexer_head_dim = cfg.index_head_dim
|
|
self.context_len = mr.model_config.context_len
|
|
# PP-local slice; matches DeepSeekV4TokenToKVPool's stage_ratios.
|
|
self.compression_ratios = cfg.compress_ratios[mr.start_layer : mr.end_layer]
|
|
if mr.pp_size > 1:
|
|
logger.info(
|
|
f"DSV4 pool PP slice: rank={mr.pp_group.rank_in_group} "
|
|
f"layers=[{mr.start_layer},{mr.end_layer}) "
|
|
f"local={len(self.compression_ratios)}/{len(cfg.compress_ratios)}"
|
|
)
|
|
self.swa_page_size = cfg.window_size
|
|
self.swa_ratio = mr.server_args.swa_full_tokens_ratio
|
|
self.is_speculative = mr.server_args.speculative_algorithm is not None
|
|
self.online_c128_mtp_max_draft_tokens = (
|
|
mr.server_args.max_speculative_num_draft_tokens or 0
|
|
)
|
|
self.requested_max_running_requests_per_worker = (
|
|
mr.server_args.max_running_requests // mr.dp_size
|
|
if mr.server_args.max_running_requests is not None
|
|
else None
|
|
)
|
|
self.disaggregation_mode = mr.server_args.disaggregation_mode
|
|
self.disaggregation_decode_extra_slots = (
|
|
mr.server_args.disaggregation_decode_extra_slots or 0
|
|
)
|
|
if mr.enable_hisparse:
|
|
from sglang.srt.mem_cache.sparsity import parse_hisparse_config
|
|
|
|
self.c4_shrink_factor = parse_hisparse_config(
|
|
mr.server_args
|
|
).host_to_device_ratio
|
|
else:
|
|
self.c4_shrink_factor = 1
|
|
assert self.c4_shrink_factor >= 1
|
|
if self.c4_shrink_factor > 1:
|
|
logger.info(f"HiSparse c4 host-to-device ratio = {self.c4_shrink_factor}")
|
|
|
|
self.c4_ring_size = get_compress_state_ring_size(4, self.is_speculative)
|
|
self.c128_ring_size = get_compress_state_ring_size(128, self.is_speculative)
|
|
|
|
self.num_layers_total = len(self.compression_ratios)
|
|
self.num_layers_ca4 = sum(1 for r in self.compression_ratios if r == 4)
|
|
self.num_layers_ca128 = sum(1 for r in self.compression_ratios if r == 128)
|
|
|
|
self.bytes_per_full_token = self._get_bytes_per_full_token()
|
|
if self.is_speculative:
|
|
# Reserve memory for the speculative draft worker by inflating
|
|
# per-token bytes by (target+draft)/target. Equivalent to dflash's
|
|
# scale_kv_cell_size_per_token_for_dflash but applied to
|
|
# bytes_per_full_token: tokens = avail / (bpft * (T+D)/T).
|
|
draft_layers = 1
|
|
target_layers = self.num_layers_total
|
|
self.bytes_per_full_token *= (target_layers + draft_layers) / target_layers
|
|
|
|
# Online c128 keeps a single in-progress (max, sum, kv) state per index
|
|
# and assumes a strict forward-only schedule. Speculative decode (MTP)
|
|
# would need rollback / replay across draft and verify, which the
|
|
# online path doesn't support yet.
|
|
if envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
|
allow_experimental_online_c128_mtp = (
|
|
envs.SGLANG_EXPERIMENTAL_ONLINE_C128_MTP.get()
|
|
and mr.spec_algorithm.is_eagle()
|
|
)
|
|
assert mr.spec_algorithm.is_none() or allow_experimental_online_c128_mtp, (
|
|
"SGLANG_OPT_USE_ONLINE_COMPRESS does not support speculative decode "
|
|
"(MTP) yet, except the experimental EAGLE topk=1 path gated by "
|
|
"SGLANG_EXPERIMENTAL_ONLINE_C128_MTP=1"
|
|
)
|
|
if allow_experimental_online_c128_mtp:
|
|
assert self.online_c128_mtp_max_draft_tokens > 0, (
|
|
"SGLANG_EXPERIMENTAL_ONLINE_C128_MTP requires "
|
|
"speculative_num_draft_tokens to be set."
|
|
)
|
|
logger.warning(
|
|
"DSV4 compressed attention: experimental online c128 + MTP enabled "
|
|
f"(EAGLE topk=1 only, "
|
|
f"draft_banks={self.online_c128_mtp_max_draft_tokens}). "
|
|
"Validate correctness carefully."
|
|
)
|
|
else:
|
|
logger.info(
|
|
"DSV4 compressed attention: online c128 enabled (ring_size=1)"
|
|
)
|
|
|
|
def _get_bytes_per_full_token(self) -> float:
|
|
kv_bytes = self.qk_nope_head_dim + self.qk_rope_head_dim * 2 + 8
|
|
|
|
quant_block_size = 128
|
|
indexer_bytes = (
|
|
self.indexer_head_dim + self.indexer_head_dim // quant_block_size * 4
|
|
)
|
|
|
|
attn_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
|
|
c4_state_dtype_size, c128_state_dtype_size = (
|
|
_get_dsv4_compress_state_dtype_sizes()
|
|
)
|
|
c4_state_bytes = 2 * 2 * attn_head_dim * c4_state_dtype_size
|
|
# Online c128 stores (max, sum, kv) per slot (3*head_dim) instead of
|
|
# raw (kv, score) (2*head_dim). Combined with ring_size=1 this still
|
|
# nets a large reduction (~3/256x) but the per-slot bytes go up.
|
|
c128_online = envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get()
|
|
c128_state_bytes = (
|
|
(3 if c128_online else 2 * 1) * attn_head_dim * c128_state_dtype_size
|
|
)
|
|
c4_indexer_state_bytes = 2 * 2 * self.indexer_head_dim * c4_state_dtype_size
|
|
|
|
c4_state_ratio = self.c4_ring_size / self.swa_page_size
|
|
# C128 state is request-scoped and is finalized after
|
|
# max_running_requests is known, so it should not scale with
|
|
# full-token capacity here.
|
|
c128_state_ratio = 0
|
|
|
|
c4_frac = 1 / (4 * self.c4_shrink_factor)
|
|
return (
|
|
self.swa_ratio * kv_bytes * self.num_layers_total
|
|
+ c4_frac * kv_bytes * self.num_layers_ca4
|
|
+ 1 / 128 * kv_bytes * self.num_layers_ca128
|
|
+ 1 / 4 * indexer_bytes * self.num_layers_ca4
|
|
+ self.swa_ratio * c4_state_ratio * c4_state_bytes * self.num_layers_ca4
|
|
+ c128_state_ratio * c128_state_bytes * self.num_layers_ca128
|
|
+ self.swa_ratio
|
|
* c4_state_ratio
|
|
* c4_indexer_state_bytes
|
|
* self.num_layers_ca4
|
|
)
|
|
|
|
def _compute_dsv4_sizes(self, full_token: int, page_size: int) -> _DSV4PoolSizes:
|
|
full_token = full_token // page_size * page_size
|
|
swa_tokens = int(full_token * self.swa_ratio) // page_size * page_size
|
|
return _DSV4PoolSizes(
|
|
full_max_total_num_tokens=full_token,
|
|
swa_max_total_num_tokens=swa_tokens,
|
|
c4_max_total_num_tokens=full_token // (4 * self.c4_shrink_factor),
|
|
c128_max_total_num_tokens=full_token // 128,
|
|
c4_state_pool_size=swa_tokens // self.swa_page_size * self.c4_ring_size,
|
|
c128_state_pool_size=0,
|
|
)
|
|
|
|
def _get_num_req_slots(self, max_running_requests: int) -> int:
|
|
if self.disaggregation_mode == "decode":
|
|
return max_running_requests + self.disaggregation_decode_extra_slots + 1
|
|
return max_running_requests + 1
|
|
|
|
def _get_c128_state_fixed_bytes(self, max_running_requests: int) -> int:
|
|
if self.num_layers_ca128 == 0:
|
|
return 0
|
|
|
|
_, c128_state_dtype_size = _get_dsv4_compress_state_dtype_sizes()
|
|
attn_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
|
|
num_req_slots = self._get_num_req_slots(max_running_requests)
|
|
|
|
if envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
|
state_rows = num_req_slots + self.c128_ring_size + 1
|
|
state_rows *= 1 + self.online_c128_mtp_max_draft_tokens
|
|
state_last_dim = 3 * attn_head_dim
|
|
else:
|
|
state_pool_size = num_req_slots * self.c128_ring_size
|
|
state_rows = state_pool_size + self.c128_ring_size + 1
|
|
state_rows = ceil_div(state_rows, 128) * 128
|
|
state_last_dim = 2 * attn_head_dim
|
|
|
|
return (
|
|
state_rows * state_last_dim * c128_state_dtype_size * self.num_layers_ca128
|
|
)
|
|
|
|
def _get_c128_state_fixed_bytes_for_token_capacity(
|
|
self, token_capacity: int
|
|
) -> int:
|
|
if self.requested_max_running_requests_per_worker is not None:
|
|
return self._get_c128_state_fixed_bytes(
|
|
self.requested_max_running_requests_per_worker
|
|
)
|
|
|
|
estimated = int(token_capacity / self.context_len * 512)
|
|
estimated = max(min(estimated, 4096), 2048)
|
|
max_running_requests = min(estimated, token_capacity // 2)
|
|
return self._get_c128_state_fixed_bytes(max_running_requests)
|
|
|
|
def _to_config(self, sizes: _DSV4PoolSizes) -> MemoryPoolConfig:
|
|
full = sizes.full_max_total_num_tokens
|
|
swa = sizes.swa_max_total_num_tokens
|
|
logger.info(
|
|
f"DSV4 pool sizes: full={full}, swa={swa}, "
|
|
f"c4={sizes.c4_max_total_num_tokens}, "
|
|
f"c128={sizes.c128_max_total_num_tokens}, "
|
|
f"c4_state={sizes.c4_state_pool_size}, "
|
|
f"c128_state={sizes.c128_state_pool_size}"
|
|
)
|
|
return MemoryPoolConfig(
|
|
max_total_num_tokens=full,
|
|
full_max_total_num_tokens=full,
|
|
swa_max_total_num_tokens=swa,
|
|
c4_max_total_num_tokens=sizes.c4_max_total_num_tokens,
|
|
c128_max_total_num_tokens=sizes.c128_max_total_num_tokens,
|
|
c4_state_pool_size=sizes.c4_state_pool_size,
|
|
c128_state_pool_size=sizes.c128_state_pool_size,
|
|
)
|
|
|
|
def finalize_with_max_running_requests(
|
|
self, config: MemoryPoolConfig
|
|
) -> MemoryPoolConfig:
|
|
assert config.max_running_requests is not None
|
|
num_req_slots = self._get_num_req_slots(config.max_running_requests)
|
|
if envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
|
config.c128_state_pool_size = num_req_slots
|
|
else:
|
|
config.c128_state_pool_size = num_req_slots * self.c128_ring_size
|
|
return config
|
|
|
|
def calculate_pool_sizes(
|
|
self, available_bytes: int, page_size: int
|
|
) -> MemoryPoolConfig:
|
|
assert (
|
|
page_size % 128 == 0
|
|
), "page_size must be multiple of 128 for compressed attention"
|
|
|
|
if self.requested_max_running_requests_per_worker is not None:
|
|
c128_state_fixed_bytes = self._get_c128_state_fixed_bytes(
|
|
self.requested_max_running_requests_per_worker
|
|
)
|
|
else:
|
|
full_token = int(available_bytes / self.bytes_per_full_token)
|
|
c128_state_fixed_bytes = (
|
|
self._get_c128_state_fixed_bytes_for_token_capacity(full_token)
|
|
)
|
|
|
|
available_bytes_for_tokens = max(available_bytes - c128_state_fixed_bytes, 0)
|
|
full_token = int(available_bytes_for_tokens / self.bytes_per_full_token)
|
|
|
|
sizes = self._compute_dsv4_sizes(full_token, page_size)
|
|
logger.info(
|
|
f"DSV4 memory calculation: "
|
|
f"bytes_per_full_token={self.bytes_per_full_token:.2f}, "
|
|
f"available_bytes={available_bytes / (1 << 30):.2f} GB, "
|
|
f"c128_state_fixed={c128_state_fixed_bytes / (1 << 30):.2f} GB, "
|
|
f"full_token={sizes.full_max_total_num_tokens}"
|
|
)
|
|
return self._to_config(sizes)
|
|
|
|
def calculate_pool_sizes_from_max_tokens(
|
|
self, max_total_num_tokens: int, page_size: int
|
|
) -> MemoryPoolConfig:
|
|
assert (
|
|
page_size % 128 == 0
|
|
), "page_size must be multiple of 128 for compressed attention"
|
|
sizes = self._compute_dsv4_sizes(max_total_num_tokens, page_size)
|
|
return self._to_config(sizes)
|
|
|
|
|
|
def create_memory_pool_configurator(
|
|
mr: ModelRunner,
|
|
) -> MemoryPoolConfigurator:
|
|
"""Factory: select the right configurator for the model architecture."""
|
|
if is_deepseek_v4(mr.model_config.hf_config) and mr.is_hybrid_swa:
|
|
return DSV4PoolConfigurator(mr)
|
|
if mr.is_hybrid_swa:
|
|
if SWAChunkCapPoolConfigurator.is_applicable(mr):
|
|
return SWAChunkCapPoolConfigurator(mr)
|
|
return HybridSWAPoolConfigurator(mr)
|
|
# Future: MambaPoolConfigurator
|
|
return DefaultPoolConfigurator(mr)
|