[DeepSeek V4] Enable FlashMLA sparse prefill by default (#29775)
Co-authored-by: Baizhou Zhang <sobereddiezhang@gmail.com>
This commit is contained in:
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
@@ -93,6 +95,11 @@ def validate_deepseek_v4_cp(server_args: ServerArgs) -> None:
|
||||
assert (
|
||||
server_args.tp_size <= 8
|
||||
), "Context parallel only supports single machine (tp_size <= 8). Cross-machine CP has precision issues."
|
||||
logger.warning(
|
||||
"Disabling SGLANG_OPT_FLASHMLA_SPARSE_PREFILL because DeepSeekV4 "
|
||||
"context parallelism is enabled."
|
||||
)
|
||||
envs.SGLANG_OPT_FLASHMLA_SPARSE_PREFILL.set(False)
|
||||
logger.warning(
|
||||
f"Enable Context Parallel for DeepSeekV4, "
|
||||
f"dp_size={server_args.dp_size}, moe_dense_tp_size={server_args.moe_dense_tp_size}, "
|
||||
|
||||
@@ -874,7 +874,7 @@ class Envs:
|
||||
SGLANG_OPT_USE_COMPRESSOR_V2 = EnvBool(True)
|
||||
SGLANG_FP8_PAGED_MQA_LOGITS_TORCH = EnvBool(False)
|
||||
SGLANG_TOPK_TRANSFORM_512_TORCH = EnvBool(False)
|
||||
SGLANG_OPT_FLASHMLA_SPARSE_PREFILL = EnvBool(False)
|
||||
SGLANG_OPT_FLASHMLA_SPARSE_PREFILL = EnvBool(True)
|
||||
|
||||
# SWA radix cache
|
||||
# TODO(DSV4): @ispobock this has bug on main branch when retract
|
||||
|
||||
@@ -44,6 +44,7 @@ from sglang.srt.layers.attention.dsv4.quant_k_cache import (
|
||||
)
|
||||
from sglang.srt.layers.attention.dsv4.sparse_prefill_utils import (
|
||||
SparsePrefillChunkCache,
|
||||
SparsePrefillWorkspace,
|
||||
)
|
||||
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
|
||||
@@ -363,8 +364,8 @@ class DSV4Metadata:
|
||||
c128_compress_metadata: Optional[FusedCompressMetadata] = None
|
||||
|
||||
# Lazily populated on the first call to ``_forward_prefill_sparse`` and
|
||||
# reused across every layer in the chunk. Reset to ``None`` on copy_ so
|
||||
# cuda-graph replay rebuilds it for the next forward.
|
||||
# reused across every layer in the chunk. Reset to ``None`` when graph
|
||||
# metadata is refreshed so replay rebuilds it from the live batch.
|
||||
sparse_prefill_cache: Optional[SparsePrefillChunkCache] = None
|
||||
|
||||
@property
|
||||
@@ -397,6 +398,7 @@ class DSV4Metadata:
|
||||
self.c128_compress_metadata,
|
||||
src=static_metadata.c128_compress_metadata,
|
||||
)
|
||||
self.sparse_prefill_cache = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -506,6 +508,7 @@ class DeepseekV4AttnBackend(
|
||||
DSV4RawDecodeMetadata,
|
||||
] = None
|
||||
self.online_c128_mtp = OnlineC128MTPController(self)
|
||||
self.sparse_prefill_workspace = SparsePrefillWorkspace(self.device)
|
||||
|
||||
def _move_to_device(self, x: List[int]) -> torch.Tensor:
|
||||
pin_tensor = torch.tensor(x, dtype=torch.int32, pin_memory=True)
|
||||
@@ -1470,6 +1473,8 @@ class DeepseekV4AttnBackend(
|
||||
|
||||
cache = self.forward_metadata.sparse_prefill_cache
|
||||
if cache is None:
|
||||
seq_lens_cpu = forward_batch.seq_lens_cpu
|
||||
assert seq_lens_cpu is not None
|
||||
# ``swa_window_size`` on the pool is its storage page size, not
|
||||
# the model's SWA window — pass both explicitly.
|
||||
cache = SparsePrefillChunkCache.build(
|
||||
@@ -1481,6 +1486,7 @@ class DeepseekV4AttnBackend(
|
||||
swa_window_size=SWA_WINDOW,
|
||||
swa_page_size=token_to_kv_pool.swa_window_size,
|
||||
num_qo_tokens=q_flat.shape[0],
|
||||
max_seq_len=int(seq_lens_cpu.max().item()),
|
||||
)
|
||||
self.forward_metadata.sparse_prefill_cache = cache
|
||||
|
||||
@@ -1491,7 +1497,7 @@ class DeepseekV4AttnBackend(
|
||||
extra_page_size = None
|
||||
flat_token_ids = None
|
||||
if compress_ratio == 0:
|
||||
workspace = cache.c0_workspace
|
||||
workspace = self.sparse_prefill_workspace.get(cache.swa_token_ids.shape[0])
|
||||
combined_indices = cache.c0_combined_indices
|
||||
combined_lens = cache.c0_combined_lens
|
||||
swa_slice = workspace
|
||||
@@ -1502,7 +1508,6 @@ class DeepseekV4AttnBackend(
|
||||
assert core_attn_metadata.c128_page_indices is not None
|
||||
cache.ensure_c128(core_attn_metadata.c128_page_indices)
|
||||
flat_token_ids = cache.c128_flat_token_ids
|
||||
workspace = cache.c128_workspace
|
||||
combined_indices = cache.c128_combined_indices
|
||||
combined_lens = cache.c128_combined_lens
|
||||
else:
|
||||
@@ -1512,11 +1517,15 @@ class DeepseekV4AttnBackend(
|
||||
)
|
||||
cache.ensure_c4(core_attn_metadata.page_table, extra_page_size)
|
||||
flat_token_ids = cache.c4_flat_token_ids
|
||||
workspace = cache.c4_workspace
|
||||
combined_indices, combined_lens = cache.combine_c4_layer(
|
||||
c4_sparse_raw_indices=core_attn_metadata.c4_sparse_raw_indices,
|
||||
c4_sparse_raw_indices=core_attn_metadata.c4_sparse_raw_indices[
|
||||
: cache.num_qo_tokens
|
||||
],
|
||||
)
|
||||
n_compressed = flat_token_ids.shape[0]
|
||||
workspace = self.sparse_prefill_workspace.get(
|
||||
n_compressed + cache.swa_token_ids.shape[0]
|
||||
)
|
||||
compressed_slice = workspace[:n_compressed]
|
||||
swa_slice = workspace[n_compressed:]
|
||||
|
||||
|
||||
@@ -50,6 +50,31 @@ SPARSE_PREFILL_TOPK_ALIGNMENT = 128
|
||||
WORKSPACE_DIM = DIM_NOPE + DIM_ROPE
|
||||
|
||||
|
||||
class SparsePrefillWorkspace:
|
||||
"""Backend-owned scratch storage for sparse prefill KV dequantization.
|
||||
|
||||
The workspace contents are fully overwritten before every attention call,
|
||||
so token buckets and compression ratios can safely share one buffer. Sparse
|
||||
prefill executes eagerly and serially on the supported paths, which makes it
|
||||
safe to replace the scratch allocation when a larger extent is needed.
|
||||
"""
|
||||
|
||||
def __init__(self, device: torch.device):
|
||||
self.device = device
|
||||
self._buffer: Optional[torch.Tensor] = None
|
||||
|
||||
def get(self, num_tokens: int) -> torch.Tensor:
|
||||
assert num_tokens > 0
|
||||
current_capacity = self._buffer.shape[0] if self._buffer is not None else 0
|
||||
if num_tokens > current_capacity:
|
||||
self._buffer = torch.empty(
|
||||
(num_tokens, 1, WORKSPACE_DIM),
|
||||
dtype=torch.bfloat16,
|
||||
device=self.device,
|
||||
)
|
||||
return self._buffer[:num_tokens]
|
||||
|
||||
|
||||
def combined_topk_width(topk: int, window_size: int) -> int:
|
||||
"""Width of the padded combined_indices last dim that
|
||||
``combine_topk_swa_indices`` would produce for these args."""
|
||||
@@ -341,6 +366,10 @@ class SparsePrefillChunkCache:
|
||||
# Geometry computed once per chunk.
|
||||
num_reqs: int
|
||||
num_qo_tokens: int
|
||||
# Actual maximum sequence length in this forward. CUDA-graph metadata may
|
||||
# have a much wider page table sized for the capture limit; gather only the
|
||||
# live sequence extent instead of materializing that padded capacity.
|
||||
max_seq_len: int
|
||||
# Model's SWA window — the per-query attention range. Used by
|
||||
# combine_topk_swa_indices' WINDOW_SIZE and by build_swa_token_ids's
|
||||
# gather_lens. Must match SWA_WINDOW from the backend (e.g. 128), NOT
|
||||
@@ -361,24 +390,16 @@ class SparsePrefillChunkCache:
|
||||
# c0 pre-computed combine output (entire input set is chunk-invariant).
|
||||
c0_combined_indices: torch.Tensor = field(default=None)
|
||||
c0_combined_lens: torch.Tensor = field(default=None)
|
||||
# Preallocated workspace reused across layers — avoids per-layer
|
||||
# ``torch.cat`` and bf16 allocations. Shape (total_swa, 1, 512) bf16 for
|
||||
# c0, (total_compressed + total_swa, 1, 512) for c4/c128. Dequant kernels
|
||||
# write directly via ``out=workspace[slice]``.
|
||||
c0_workspace: torch.Tensor = field(default=None)
|
||||
|
||||
# c128: positional layout of the c128 cache + pre-computed combine.
|
||||
c128_flat_token_ids: Optional[torch.Tensor] = None # (num_reqs * c128_max,) int32
|
||||
c128_combined_indices: Optional[torch.Tensor] = None
|
||||
c128_combined_lens: Optional[torch.Tensor] = None
|
||||
c128_workspace: Optional[torch.Tensor] = None
|
||||
|
||||
# c4: positional layout of the c4 cache (combine output is per-layer).
|
||||
c4_flat_token_ids: Optional[torch.Tensor] = None # (num_reqs * c4_max,) int32
|
||||
c4_page_size: Optional[int] = None
|
||||
c4_compressed_base: Optional[torch.Tensor] = None # (num_reqs,) int32
|
||||
c4_swa_base: Optional[torch.Tensor] = None # (num_reqs,) int32
|
||||
c4_workspace: Optional[torch.Tensor] = None
|
||||
# Tail stays at the -1 sentinel because the valid prefix length is
|
||||
# chunk-invariant per request — subsequent layers only overwrite that prefix.
|
||||
c4_combined_indices: Optional[torch.Tensor] = None
|
||||
@@ -395,6 +416,7 @@ class SparsePrefillChunkCache:
|
||||
swa_window_size: int,
|
||||
swa_page_size: int,
|
||||
num_qo_tokens: int,
|
||||
max_seq_len: int,
|
||||
) -> "SparsePrefillChunkCache":
|
||||
device = seq_lens.device
|
||||
num_reqs = seq_lens.shape[0]
|
||||
@@ -416,6 +438,7 @@ class SparsePrefillChunkCache:
|
||||
cache = cls(
|
||||
num_reqs=num_reqs,
|
||||
num_qo_tokens=num_qo_tokens,
|
||||
max_seq_len=max_seq_len,
|
||||
swa_window_size=swa_window_size,
|
||||
swa_page_size=swa_page_size,
|
||||
seq_lens=seq_lens,
|
||||
@@ -442,11 +465,6 @@ class SparsePrefillChunkCache:
|
||||
compress_ratio=1,
|
||||
topk=0,
|
||||
)
|
||||
cache.c0_workspace = torch.empty(
|
||||
(swa_token_ids.shape[0], 1, WORKSPACE_DIM),
|
||||
dtype=torch.bfloat16,
|
||||
device=device,
|
||||
)
|
||||
return cache
|
||||
|
||||
def ensure_c128(self, c128_page_indices: torch.Tensor) -> None:
|
||||
@@ -465,9 +483,15 @@ class SparsePrefillChunkCache:
|
||||
if self.c128_flat_token_ids is not None:
|
||||
return
|
||||
device = self.seq_lens.device
|
||||
c128_max = c128_page_indices.shape[-1]
|
||||
c128_max = max(self.max_seq_len // 128, 1)
|
||||
assert c128_max <= c128_page_indices.shape[-1], (
|
||||
f"live c128 extent {c128_max} exceeds metadata capacity "
|
||||
f"{c128_page_indices.shape[-1]}"
|
||||
)
|
||||
last_q_per_req = (self.query_start_loc[1:] - 1).long()
|
||||
per_req_c128 = c128_page_indices[last_q_per_req]
|
||||
per_req_c128 = c128_page_indices.narrow(1, 0, c128_max).index_select(
|
||||
0, last_q_per_req
|
||||
)
|
||||
# Clamp -1 -> 0 so dequant doesn't OOB; combine masks the invalid
|
||||
# tail via topk_len.
|
||||
flat_c128_ids = per_req_c128.reshape(-1).clamp_min(0).to(torch.int32)
|
||||
@@ -499,11 +523,6 @@ class SparsePrefillChunkCache:
|
||||
self.c128_flat_token_ids = flat_c128_ids
|
||||
self.c128_combined_indices = combined_indices
|
||||
self.c128_combined_lens = combined_lens
|
||||
self.c128_workspace = torch.empty(
|
||||
(total_compressed + self.swa_token_ids.shape[0], 1, WORKSPACE_DIM),
|
||||
dtype=torch.bfloat16,
|
||||
device=device,
|
||||
)
|
||||
|
||||
def ensure_c4(
|
||||
self,
|
||||
@@ -520,10 +539,17 @@ class SparsePrefillChunkCache:
|
||||
if self.c4_flat_token_ids is not None:
|
||||
return
|
||||
device = self.seq_lens.device
|
||||
max_blocks = page_table.shape[-1]
|
||||
c4_max = max_blocks * c4_page_size
|
||||
c4_max = max(self.max_seq_len // 4, 1)
|
||||
c4_capacity = page_table.shape[-1] * c4_page_size
|
||||
assert (
|
||||
c4_max <= c4_capacity
|
||||
), f"live c4 extent {c4_max} exceeds metadata capacity {c4_capacity}"
|
||||
first_q_per_req = self.query_start_loc[:-1].long()
|
||||
per_req_page_table = page_table[first_q_per_req]
|
||||
num_blocks = (c4_max + c4_page_size - 1) // c4_page_size
|
||||
assert num_blocks <= page_table.shape[1]
|
||||
per_req_page_table = page_table.narrow(1, 0, num_blocks).index_select(
|
||||
0, first_q_per_req
|
||||
)
|
||||
|
||||
k_arange = torch.arange(c4_max, dtype=torch.int32, device=device)
|
||||
block_idx = (k_arange // c4_page_size).long()
|
||||
@@ -542,11 +568,6 @@ class SparsePrefillChunkCache:
|
||||
self.c4_page_size = c4_page_size
|
||||
self.c4_compressed_base = compressed_base
|
||||
self.c4_swa_base = swa_base
|
||||
self.c4_workspace = torch.empty(
|
||||
(total_compressed + self.swa_token_ids.shape[0], 1, WORKSPACE_DIM),
|
||||
dtype=torch.bfloat16,
|
||||
device=device,
|
||||
)
|
||||
|
||||
def combine_c4_layer(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user