[CP] 1/N: Support MLA Prefill Context Parallel (#23292)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khoa Pham
2026-05-23 03:07:09 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 81cd338fcc
commit b0ce16d0c5
21 changed files with 900 additions and 161 deletions
@@ -508,6 +508,25 @@ class FlashAttentionBackend(AttentionBackend):
metadata.cu_seqlens_k = torch.nn.functional.pad(
torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.int32), (1, 0)
)
# MLA/MHA CP: prepare_mlp_sync_batch pads extend tokens up to
# lcm(attn_tp_size, attn_cp_size), so cache_seqlens_cp can exceed
# seq_lens_cpu.max(). Widen page_table by the pad delta to keep
# FA3's causal reads in-bounds; widened columns index KV slot 0
# (req_to_token is zero-init) and outputs for padding queries are
# discarded downstream.
if (
self.attn_cp_size > 1
and forward_batch.global_num_tokens_cpu is not None
and forward_batch.extend_num_tokens is not None
and forward_batch.extend_seq_lens_cpu is not None
):
padded_extend = int(forward_batch.extend_num_tokens)
real_extend = int(sum(forward_batch.extend_seq_lens_cpu))
pad_delta = padded_extend - real_extend
if pad_delta > 0:
metadata.max_seq_len_k += pad_delta
metadata.page_table = self.req_to_token_pool.req_to_token[
forward_batch.req_pool_indices, : metadata.max_seq_len_k
]
@@ -627,36 +646,43 @@ class FlashAttentionBackend(AttentionBackend):
k_rope: Optional[torch.Tensor] = None,
sinks: Optional[torch.Tensor] = None,
):
is_cp_mode = (
forward_batch.forward_mode.is_context_parallel_extend()
and forward_batch.attn_cp_metadata is not None
and self.attn_cp_size > 1
)
if k is not None:
assert v is not None
is_cp_mode = (
forward_batch.forward_mode.is_context_parallel_extend()
and forward_batch.attn_cp_metadata is not None
and self.attn_cp_size > 1
)
if save_kv_cache and not is_cp_mode and not self.fa_skip_kv_cache:
if save_kv_cache and not self.fa_skip_kv_cache:
cache_loc = (
forward_batch.out_cache_loc
if not layer.is_cross_attention
else forward_batch.encoder_out_cache_loc
)
if not self.use_mla:
self.token_to_kv_pool.set_kv_buffer(
layer, cache_loc, k, v, layer.k_scale, layer.v_scale
)
else:
if self.use_mla:
# MLA: under CP, k and k_rope arrive full-sequence
# (rebuild_cp_kv_cache ran upstream in
# forward_absorb_prepare); rank-local otherwise.
# out_cache_loc is never zigzag-split, so the write
# lands in the right slots on every rank in either case.
self.token_to_kv_pool.set_mla_kv_buffer(
layer,
cache_loc,
k,
k_rope,
)
if is_cp_mode:
cp_allgather_and_save_kv_cache(
forward_batch, layer, k, v, self.attn_cp_size
)
elif is_cp_mode:
# Dense-MHA CP: k, v are still rank-local; backend
# all-gathers and writes to the per-rank pool.
cp_allgather_and_save_kv_cache(
forward_batch, layer, k, v, self.attn_cp_size
)
else:
self.token_to_kv_pool.set_kv_buffer(
layer, cache_loc, k, v, layer.k_scale, layer.v_scale
)
# Use precomputed metadata across all layers
metadata = self.forward_metadata
@@ -974,57 +1000,103 @@ class FlashAttentionBackend(AttentionBackend):
q_nope = q_all[:, :, : layer.v_head_dim]
q_rope = q_all[:, :, layer.v_head_dim :]
result = flash_attn_with_kvcache(
q=q_rope,
k_cache=k_rope_cache,
v_cache=c_kv_cache,
qv=q_nope,
page_table=page_table,
cache_seqlens=cache_seqlens,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k_new=cu_seqlens_k if not use_local_attn else None,
max_seqlen_q=max_seqlen_q,
softmax_scale=layer.scaling,
causal=False if use_cascade_attn else causal,
softcap=layer.logit_cap,
k_descale=k_descale,
v_descale=v_descale,
return_softmax_lse=use_cascade_attn,
num_splits=self.num_splits,
ver=self.fa_impl_ver,
)
if use_cascade_attn:
o, softmax_lse, *rest = result
o_expand, softmax_lse_expand, *rest_expand = (
flash_attn_with_kvcache(
q=q_rope,
if is_cp_mode:
# MLA CP: q is rank-local zigzag-split; run the
# absorbed-MLA kernel twice (prev/next halves) against
# the full latent KV pool (which rebuild_cp_kv_cache
# populated upstream) via cp_attn_forward_extend.
# Concat q_nope + q_rope along dim=-1 so the wrapper's
# chunk(2, dim=0) keeps their alignment; split back
# inside the closure.
assert (
not use_cascade_attn
), "Cascade attention under MLA CP is not supported in v1."
q_fused = torch.cat([q_nope, q_rope], dim=-1)
def _mla_cp_attn(
q_chunk,
cu_seqlens_q_cp,
cache_seqlens_cp,
max_seqlen_q_cp,
):
q_nope_chunk = q_chunk[..., : layer.v_head_dim]
q_rope_chunk = q_chunk[..., layer.v_head_dim :]
return flash_attn_with_kvcache(
q=q_rope_chunk,
qv=q_nope_chunk,
k_cache=k_rope_cache,
v_cache=c_kv_cache,
qv=q_nope,
page_table=self.forward_metadata_spec_decode_expand.page_table,
cache_seqlens=self.forward_metadata_spec_decode_expand.cache_seqlens_int32,
cu_seqlens_q=self.forward_metadata_spec_decode_expand.cu_seqlens_q,
cu_seqlens_k_new=self.forward_metadata_spec_decode_expand.cu_seqlens_k,
max_seqlen_q=self.forward_metadata_spec_decode_expand.max_seq_len_q,
page_table=page_table,
cache_seqlens=cache_seqlens_cp,
cu_seqlens_q=cu_seqlens_q_cp,
cu_seqlens_k_new=(
cu_seqlens_k if not use_local_attn else None
),
max_seqlen_q=max_seqlen_q_cp,
softmax_scale=layer.scaling,
causal=False,
window_size=window_size,
causal=causal,
softcap=layer.logit_cap,
k_descale=k_descale,
v_descale=v_descale,
return_softmax_lse=True,
num_splits=self.num_splits,
ver=self.fa_impl_ver,
)
)
o, _ = merge_state_v2_wrapper(
o,
softmax_lse.T.contiguous(),
o_expand,
softmax_lse_expand.T.contiguous(),
o = cp_attn_forward_extend(
forward_batch, q_fused, self.device, _mla_cp_attn
)
else:
o = result
result = flash_attn_with_kvcache(
q=q_rope,
k_cache=k_rope_cache,
v_cache=c_kv_cache,
qv=q_nope,
page_table=page_table,
cache_seqlens=cache_seqlens,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k_new=cu_seqlens_k if not use_local_attn else None,
max_seqlen_q=max_seqlen_q,
softmax_scale=layer.scaling,
causal=False if use_cascade_attn else causal,
softcap=layer.logit_cap,
k_descale=k_descale,
v_descale=v_descale,
return_softmax_lse=use_cascade_attn,
num_splits=self.num_splits,
ver=self.fa_impl_ver,
)
if use_cascade_attn:
o, softmax_lse, *rest = result
o_expand, softmax_lse_expand, *rest_expand = (
flash_attn_with_kvcache(
q=q_rope,
k_cache=k_rope_cache,
v_cache=c_kv_cache,
qv=q_nope,
page_table=self.forward_metadata_spec_decode_expand.page_table,
cache_seqlens=self.forward_metadata_spec_decode_expand.cache_seqlens_int32,
cu_seqlens_q=self.forward_metadata_spec_decode_expand.cu_seqlens_q,
cu_seqlens_k_new=self.forward_metadata_spec_decode_expand.cu_seqlens_k,
max_seqlen_q=self.forward_metadata_spec_decode_expand.max_seq_len_q,
softmax_scale=layer.scaling,
causal=False,
window_size=window_size,
softcap=layer.logit_cap,
k_descale=k_descale,
v_descale=v_descale,
return_softmax_lse=True,
num_splits=self.num_splits,
ver=self.fa_impl_ver,
)
)
o, _ = merge_state_v2_wrapper(
o,
softmax_lse.T.contiguous(),
o_expand,
softmax_lse_expand.T.contiguous(),
)
else:
o = result
return o.view(-1, layer.tp_q_head_num * layer.v_head_dim)
+10 -4
View File
@@ -65,6 +65,10 @@ from sglang.srt.layers.moe import (
should_use_dp_reduce_scatterv,
should_use_flashinfer_cutlass_moe_fp4_allgather,
)
from sglang.srt.layers.utils.cp_utils import (
is_mla_prefill_cp_enabled,
mla_use_prefill_cp,
)
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.server_args import get_global_server_args
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
@@ -202,7 +206,7 @@ class ScatterMode(Enum):
@staticmethod
def model_input_output():
"""The scatter mode for model forward pass input and output data"""
if is_dsa_enable_prefill_cp():
if is_dsa_enable_prefill_cp() or is_mla_prefill_cp_enabled():
return ScatterMode.SCATTERED
return ScatterMode.TP_ATTN_FULL
@@ -379,8 +383,10 @@ class LayerScatterModes:
or should_use_flashinfer_cutlass_moe_fp4_allgather()
):
return ScatterMode.SCATTERED
# DSA CP doesn't support MOE_FULL yet; fall back to FULL
if is_enable_moe_cp_allgather() and not is_dsa_enable_prefill_cp():
# DSA CP and MLA CP both don't support MOE_FULL yet; fall back to FULL.
if is_enable_moe_cp_allgather() and not (
is_dsa_enable_prefill_cp() or is_mla_prefill_cp_enabled()
):
return ScatterMode.MOE_FULL
return ScatterMode.FULL
else:
@@ -709,7 +715,7 @@ class LayerCommunicator:
return True
if forward_batch.dp_padding_mode.is_max_len():
return True
if dsa_use_prefill_cp(forward_batch):
if dsa_use_prefill_cp(forward_batch) or mla_use_prefill_cp(forward_batch):
return True
if get_attn_tp_context().input_scattered and not self.is_last_layer:
return True
@@ -37,6 +37,7 @@ from sglang.srt.layers.dp_attention import (
get_attention_cp_group,
get_local_dp_buffer,
)
from sglang.srt.layers.utils.cp_utils import mla_use_prefill_cp
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
@@ -152,7 +153,7 @@ class DSACPCommunicateWithAllReduceAndLayerNormFn(
hidden_states, residual = layernorm(hidden_states, residual)
# for prefill: attn tp scattered -> full
# for decode: attn tp full -> full
if dsa_use_prefill_cp(forward_batch):
if dsa_use_prefill_cp(forward_batch) or mla_use_prefill_cp(forward_batch):
assert context.attn_dp_size == 1
hidden_states, local_hidden_states = (
get_local_dp_buffer(get_attention_cp_group()),
@@ -205,7 +206,7 @@ class DSACPCommunicateSummableTensorPairFn(CommunicateSummableTensorPairFn):
):
# for prefill: full -> attn tp scattered
# for decode: full -> attn tp full
if dsa_use_prefill_cp(forward_batch):
if dsa_use_prefill_cp(forward_batch) or mla_use_prefill_cp(forward_batch):
assert context.attn_dp_size == 1
input_hidden_states = hidden_states
hidden_states = hidden_states.tensor_split(context.attn_cp_size)[
+36 -19
View File
@@ -51,19 +51,41 @@ def is_prefill_cp_in_seq_split():
)
def is_mla_prefill_cp_enabled() -> bool:
sa = get_global_server_args()
return sa.enable_prefill_context_parallel and sa.use_mla_backend
def mla_use_prefill_cp(forward_batch, mla_enable_prefill_cp=None):
if mla_enable_prefill_cp is None:
mla_enable_prefill_cp = is_mla_prefill_cp_enabled()
return (
forward_batch.attn_cp_metadata is not None
and mla_enable_prefill_cp
and forward_batch.forward_mode.is_context_parallel_extend()
)
def can_cp_split(seq_len: int, cp_size: int, forward_batch):
# CP metadata (zigzag split) only supports batch=1 for now.
from sglang.srt.model_executor.forward_batch_info import ForwardMode
# TODO current just support prefill batch=1 and len(input_ids) > self.cp_size * 2
# Note: (self.cp_size * 2) To achieve load balancing for seq computation,
# the seq data needs to be divided and recombined at twice the size of cp_size.
cur_cp_seq_len = seq_len // (cp_size * 2)
if (
return (
cur_cp_seq_len != 0
and cp_size > 1
# prepare_context_parallel_metadata hard-codes bs_per_cp_group = 1;
# guard explicitly to avoid silent mis-partitioning under continuous batching.
# TODO: remove this guard once we support multi-batch-cp-split
and forward_batch.batch_size == 1
and forward_batch.forward_mode.is_context_parallel_extend()
# is_context_parallel_extend() returns True for MIXED (prefill+decode
# in one step), but the zigzag split only makes sense on pure extend.
and forward_batch.forward_mode != ForwardMode.MIXED
and is_prefill_context_parallel_enabled()
and forward_batch.seq_lens_cpu.shape[0] == 1
):
return True
else:
return False
)
def cp_split_and_rebuild_data(forward_batch, input_: torch.Tensor):
@@ -395,6 +417,7 @@ def prepare_context_parallel_metadata(
cp_rank,
cp_size,
seqs_len,
extend_lens,
):
from sglang.srt.layers.attention.dsa.utils import (
is_dsa_prefill_cp_round_robin_split,
@@ -449,18 +472,12 @@ def prepare_context_parallel_metadata(
bs_per_cp_group = 1
kv_len_origin = kv_len
# Derive prefix offset from the full sequence length on CPU.
# NOTE: forward_batch.seq_lens_cpu includes cached prefix + extend tokens.
# In CP we only split the extend tokens, but cache_seqlens passed to FA must
# include the cached prefix.
prefix_len = 0
try:
if seqs_len is not None and len(seqs_len) == 1:
prefix_len = int(seqs_len[0]) - int(kv_len_origin.item())
if prefix_len < 0:
prefix_len = 0
except Exception:
prefix_len = 0
# Derive prefix offset from unpadded CPU tensors. Both `seqs_len` and `extend_lens` are unpadded by the caller
# Using the padded `kv_len` here would undercount `prefix_len` by the padding amount and shift the FA causal horizon.
assert (
len(seqs_len) == 1 and len(extend_lens) == 1
), "Prefill Context Parallel only supports batch_size == 1 for now"
prefix_len = max(0, int(seqs_len[0]) - int(extend_lens[0]))
# get zigzag index
cp_segment_num = cp_size * 2
seq_per_batch = kv_len // cp_segment_num # seq_len for each batch and segment
@@ -56,6 +56,7 @@ from sglang.srt.layers.logits_processor import LogitsProcessorOutput
from sglang.srt.layers.moe.token_dispatcher.deepep import DeepEPBuffer
from sglang.srt.layers.moe.utils import get_deepep_mode, get_moe_a2a_backend
from sglang.srt.layers.utils import MultiPlatformOp
from sglang.srt.layers.utils.cp_utils import is_mla_prefill_cp_enabled
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
@@ -567,7 +568,15 @@ class CudaGraphRunner:
self.attn_tp_size = get_attention_tp_size()
self.attn_tp_rank = get_attention_tp_rank()
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
# True if a DSACPLayerCommunicator-style prefill-CP flavor is active
# (DSA or MLA). These flavors feed a zigzag-split rank-local layout
# into the runner; MHA-arch prefill CP (Qwen3/Qwen2 MoE via PR
# #18233) uses the plain LayerCommunicator with an attn_tp-replicated
# layout and is intentionally excluded so the attn_tp-local
# num_token_non_padded adjustment still runs for it.
self.enable_prefill_cp = (
is_dsa_enable_prefill_cp() or is_mla_prefill_cp_enabled()
)
self.deepep_adapter = DeepEPCudaGraphRunnerAdapter()
@@ -928,7 +937,7 @@ class CudaGraphRunner:
if (
enable_num_token_non_padded()
and self.require_gathered_buffer
and not self.dsa_enable_prefill_cp
and not self.enable_prefill_cp
):
local = compute_local_num_token_non_padded(
global_num_token_non_padded=buffers.num_token_non_padded,
@@ -1191,7 +1200,9 @@ class CudaGraphRunner:
seq_len_fill_value=self.seq_len_fill_value,
require_gathered_buffer=self.require_gathered_buffer,
num_tokens_per_bs=self.num_tokens_per_bs,
dsa_enable_prefill_cp=self.dsa_enable_prefill_cp,
# Parameter name retained for API stability; semantically this is
# "any prefill-CP flavor enabled" (DSA CP or MLA CP).
dsa_enable_prefill_cp=self.enable_prefill_cp,
enable_num_token_non_padded_flag=enable_num_token_non_padded(),
pp_proxy_tensors=pp_proxy_tensors,
)
@@ -126,6 +126,7 @@ from sglang.srt.layers.pooler import EmbeddingPoolerOutput
from sglang.srt.layers.quantization.fp8_kernel import fp8_dtype
from sglang.srt.layers.sampler import create_sampler
from sglang.srt.layers.torchao_utils import apply_torchao_config_to_model
from sglang.srt.layers.utils.cp_utils import is_mla_prefill_cp_enabled
from sglang.srt.lora.lora_manager import LoRAManager
from sglang.srt.lora.lora_registry import LoRARef
from sglang.srt.managers.schedule_batch import sanity_check_mm_pad_shift_value
@@ -3285,11 +3286,17 @@ class ModelRunner(ModelRunnerKVCacheMixin):
forward_batch.prepare_attn_tp_scatter_input(self)
# Normalize num_token_non_padded to be local to this attention TP rank if needed.
# The skip is scoped to DSACPLayerCommunicator-style CP (DSA, MLA): those
# flavors already feed a zigzag-split rank-local layout whose token count
# should not be further divided by attn_tp_size. MHA-arch prefill CP
# (Qwen3/Qwen2 MoE) keeps the attn_tp-replicated layout and wants the
# adjustment to run — see docs/design/prefill-cp-mla.md §Phase 5.
if (
forward_batch.num_token_non_padded is not None
and forward_batch.global_num_tokens_gpu is not None
and require_gathered_buffer(self.server_args)
and not is_dsa_enable_prefill_cp()
and not is_mla_prefill_cp_enabled()
):
forward_batch.adjust_num_token_non_padded_for_attn_tp(
server_args=self.server_args,
@@ -1,5 +1,6 @@
from sglang.srt.compilation.piecewise_context_manager import is_in_piecewise_cuda_graph
from sglang.srt.layers.attention.tbo_backend import TboAttnBackend
from sglang.srt.layers.utils.cp_utils import mla_use_prefill_cp
from sglang.srt.model_executor.forward_context import get_attn_backend
from sglang.srt.models.deepseek_common.attention_forward_methods.forward_methods import (
AttnForwardMethod,
@@ -74,6 +75,12 @@ def _handle_attention_backend(attn, forward_batch, backend_name):
if is_in_piecewise_cuda_graph():
return AttnForwardMethod.MLA
# MLA prefill CP forces absorbed MLA regardless of prefix length: the
# CP path gathers latent KV via rebuild_cp_kv_cache and feeds the
# backend's absorbed-MLA kernel.
if mla_use_prefill_cp(forward_batch):
return _dispatch_mla_subtype(attn, forward_batch)
sum_extend_prefix_lens = _get_sum_extend_prefix_lens(forward_batch)
disable_ragged = (
backend_name in ["flashinfer", "flashmla"]
@@ -13,6 +13,7 @@ from sglang.srt.layers.quantization.fp8_kernel import (
per_tensor_quant_mla_fp8,
per_token_group_quant_mla_deep_gemm_masked_fp8,
)
from sglang.srt.layers.utils.cp_utils import mla_use_prefill_cp
from sglang.srt.lora.deepseek_mla_correction import (
apply_q_correction as apply_kv_b_lora_q_correction,
)
@@ -380,7 +381,7 @@ class DeepseekMLAForwardMixin:
):
q_pe, k_pe = self.rotary_emb(positions, q_pe, k_pe)
if dsa_use_prefill_cp(forward_batch):
if dsa_use_prefill_cp(forward_batch) or mla_use_prefill_cp(forward_batch):
# support allgather+rerrange
k_nope, k_pe = self.rebuild_cp_kv_cache(
latent_cache, forward_batch, k_nope, k_pe
+31 -8
View File
@@ -43,9 +43,12 @@ from sglang.srt.layers.logits_processor import LogitsProcessor
from sglang.srt.layers.quantization import Fp8Config
from sglang.srt.layers.quantization.base_config import QuantizationConfig
from sglang.srt.layers.utils.cp_utils import (
can_cp_split,
cp_all_gather_rerange_output,
cp_split_and_rebuild_data,
cp_split_and_rebuild_position,
is_mla_prefill_cp_enabled,
mla_use_prefill_cp,
prepare_context_parallel_metadata,
)
from sglang.srt.layers.vocab_parallel_embedding import (
@@ -136,6 +139,14 @@ class DeepseekModelNextN(nn.Module):
layer_name = "layers." + str(config.num_hidden_layers)
self.quant_config = quant_config
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
self.mla_enable_prefill_cp = (
is_mla_prefill_cp_enabled() and not is_deepseek_dsa(config)
)
if self.dsa_enable_prefill_cp or self.mla_enable_prefill_cp:
self.cp_size = get_attention_cp_size()
else:
self.cp_size = None
self.decoder = DeepseekV2DecoderLayer(
config,
0,
@@ -144,15 +155,12 @@ class DeepseekModelNextN(nn.Module):
is_nextn=True,
prefix=add_prefix(layer_name, prefix),
alt_stream=self.alt_stream,
dsa_enable_prefill_cp=self.dsa_enable_prefill_cp,
mla_enable_prefill_cp=self.mla_enable_prefill_cp,
)
self.shared_head = nn.Module()
self.shared_head.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
if self.dsa_enable_prefill_cp:
self.cp_size = get_attention_cp_size()
else:
self.cp_size = None
def forward(
self,
@@ -193,7 +201,9 @@ class DeepseekModelNextN(nn.Module):
else:
hidden_states = self.eh_proj(eh_input)
if dsa_use_prefill_cp(forward_batch, self.dsa_enable_prefill_cp):
if dsa_use_prefill_cp(
forward_batch, self.dsa_enable_prefill_cp
) or mla_use_prefill_cp(forward_batch, self.mla_enable_prefill_cp):
hidden_states = cp_split_and_rebuild_data(forward_batch, hidden_states)
positions = cp_split_and_rebuild_position(forward_batch, positions)
residual = None
@@ -212,7 +222,9 @@ class DeepseekModelNextN(nn.Module):
else:
hidden_states = self.shared_head.norm(hidden_states)
if dsa_use_prefill_cp(forward_batch, self.dsa_enable_prefill_cp):
if dsa_use_prefill_cp(
forward_batch, self.dsa_enable_prefill_cp
) or mla_use_prefill_cp(forward_batch, self.mla_enable_prefill_cp):
# allgather + rerrange
hidden_states = cp_all_gather_rerange_output(
hidden_states,
@@ -250,7 +262,8 @@ class DeepseekV3ForCausalLMNextN(DeepseekV3ForCausalLM):
self.determine_num_fused_shared_experts("DeepseekV3ForCausalLMNextN")
self.use_dsa = is_deepseek_dsa(config)
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
if self.dsa_enable_prefill_cp:
self.mla_enable_prefill_cp = is_mla_prefill_cp_enabled() and not self.use_dsa
if self.dsa_enable_prefill_cp or self.mla_enable_prefill_cp:
self.cp_rank = get_attention_cp_rank()
self.cp_size = get_attention_cp_size()
else:
@@ -298,6 +311,16 @@ class DeepseekV3ForCausalLMNextN(DeepseekV3ForCausalLM):
self.cp_rank,
self.cp_size,
forward_batch.seq_lens_cpu.tolist(),
extend_lens=forward_batch.extend_seq_lens_cpu,
)
elif self.mla_enable_prefill_cp:
if can_cp_split(len(input_ids), self.cp_size, forward_batch):
forward_batch.attn_cp_metadata = prepare_context_parallel_metadata(
len(input_ids),
self.cp_rank,
self.cp_size,
forward_batch.seq_lens_cpu.tolist(),
extend_lens=forward_batch.extend_seq_lens_cpu,
)
hidden_states = self.model(input_ids, positions, forward_batch)
return self.logits_processor(
+73 -14
View File
@@ -123,9 +123,12 @@ from sglang.srt.layers.radix_attention import RadixAttention
from sglang.srt.layers.rotary_embedding import get_rope_wrapper
from sglang.srt.layers.utils import PPMissingLayer
from sglang.srt.layers.utils.cp_utils import (
can_cp_split,
cp_all_gather_rerange_output,
cp_split_and_rebuild_data,
cp_split_and_rebuild_position,
is_prefill_context_parallel_enabled,
mla_use_prefill_cp,
prepare_context_parallel_metadata,
)
from sglang.srt.layers.vocab_parallel_embedding import (
@@ -339,6 +342,8 @@ class MoEGate(nn.Module):
is_nextn: bool = False,
is_hash_moe: bool = False,
is_deepseek_v4: bool = False,
dsa_enable_prefill_cp: bool = False,
mla_enable_prefill_cp: bool = False,
):
super().__init__()
self.is_nextn = is_nextn
@@ -368,7 +373,9 @@ class MoEGate(nn.Module):
self.e_score_correction_bias = None
if _is_cpu and _is_cpu_amx_available:
self.quant_method = PackWeightMethod(weight_names=["weight"])
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
self.use_dsa = is_deepseek_dsa(config)
self.dsa_enable_prefill_cp = dsa_enable_prefill_cp
self.mla_enable_prefill_cp = mla_enable_prefill_cp
def forward(
self,
@@ -390,7 +397,10 @@ class MoEGate(nn.Module):
if (
not self.is_deepseek_v4
and forward_batch is not None
and dsa_use_prefill_cp(forward_batch)
and (
dsa_use_prefill_cp(forward_batch, self.dsa_enable_prefill_cp)
or mla_use_prefill_cp(forward_batch, self.mla_enable_prefill_cp)
)
):
logits = F.linear(hidden_states, self.weight, None)
else:
@@ -442,6 +452,8 @@ class DeepseekV2MoE(nn.Module):
alt_stream: Optional[torch.cuda.Stream] = None,
is_nextn: bool = False,
is_deepseek_v4: bool = False,
dsa_enable_prefill_cp: bool = False,
mla_enable_prefill_cp: bool = False,
):
super().__init__()
self.tp_size = get_tensor_model_parallel_world_size()
@@ -503,6 +515,8 @@ class DeepseekV2MoE(nn.Module):
is_nextn=is_nextn,
is_hash_moe=self.is_hash,
is_deepseek_v4=is_deepseek_v4,
dsa_enable_prefill_cp=dsa_enable_prefill_cp,
mla_enable_prefill_cp=mla_enable_prefill_cp,
)
# scaling factor for fused shared experts on AMD-platform.
@@ -1340,6 +1354,8 @@ class DeepseekV2AttentionMLA(
alt_stream: Optional[torch.cuda.Stream] = None,
skip_rope: bool = False,
is_nextn: bool = False,
dsa_enable_prefill_cp: bool = False,
mla_enable_prefill_cp: bool = False,
) -> None:
super().__init__()
self.layer_id = layer_id
@@ -1354,11 +1370,14 @@ class DeepseekV2AttentionMLA(
attn_tp_rank = get_attention_tp_rank()
attn_tp_size = get_attention_tp_size()
self.use_dsa = is_deepseek_dsa(config)
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
self.dsa_enable_prefill_cp = dsa_enable_prefill_cp
self.mla_enable_prefill_cp = mla_enable_prefill_cp
if self.dsa_enable_prefill_cp:
assert self.use_dsa, "CP currently only supports deepseek v3.2 model"
# cp reuse the attn_tp comm group but need to duplicate the weights
if self.dsa_enable_prefill_cp and self.use_dsa:
# cp reuses the attn_tp comm group but needs to duplicate the weights;
# store cp_size whenever either CP flavor is active so rebuild_cp_kv_cache
# and the FA3 MLA wrapper can reach it on the dense MLA path too.
if self.dsa_enable_prefill_cp or self.mla_enable_prefill_cp:
self.cp_size = get_attention_cp_size()
self.num_heads = num_heads
assert num_heads % attn_tp_size == 0
@@ -1793,6 +1812,8 @@ class DeepseekV2DecoderLayer(nn.Module):
is_nextn: bool = False,
prefix: str = "",
alt_stream: Optional[torch.cuda.Stream] = None,
dsa_enable_prefill_cp: bool = False,
mla_enable_prefill_cp: bool = False,
) -> None:
super().__init__()
self.hidden_size = config.hidden_size
@@ -1809,7 +1830,8 @@ class DeepseekV2DecoderLayer(nn.Module):
self.speculative_algorithm = SpeculativeAlgorithm.from_string(
get_global_server_args().speculative_algorithm
)
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
self.dsa_enable_prefill_cp = dsa_enable_prefill_cp
self.mla_enable_prefill_cp = mla_enable_prefill_cp
self.layer_id = layer_id
self.is_nextn = is_nextn
self.self_attn = DeepseekV2AttentionMLA(
@@ -1832,6 +1854,8 @@ class DeepseekV2DecoderLayer(nn.Module):
prefix=add_prefix("self_attn", prefix),
alt_stream=alt_stream,
is_nextn=is_nextn,
dsa_enable_prefill_cp=dsa_enable_prefill_cp,
mla_enable_prefill_cp=mla_enable_prefill_cp,
)
if not hasattr(config, "q_lora_rank") and envs.SGLANG_USE_AG_AFTER_QLORA.get():
raise ValueError(
@@ -1858,6 +1882,8 @@ class DeepseekV2DecoderLayer(nn.Module):
layer_id=self.layer_id,
alt_stream=alt_stream,
is_nextn=is_nextn,
dsa_enable_prefill_cp=dsa_enable_prefill_cp,
mla_enable_prefill_cp=mla_enable_prefill_cp,
)
else:
if enable_moe_dense_fully_dp():
@@ -1882,7 +1908,10 @@ class DeepseekV2DecoderLayer(nn.Module):
self._gfx95_quant_format = self._detect_gfx95_quant_format()
if self.dsa_enable_prefill_cp:
if self.dsa_enable_prefill_cp or self.mla_enable_prefill_cp:
# DSACPLayerCommunicator is flavor-agnostic; its internal gates
# read both dsa_use_prefill_cp and mla_use_prefill_cp. The rename
# to CPLayerCommunicator is deferred to a cleanup PR.
self.layer_communicator = DSACPLayerCommunicator(
layer_scatter_modes=self.layer_scatter_modes,
input_layernorm=self.input_layernorm,
@@ -1998,7 +2027,10 @@ class DeepseekV2DecoderLayer(nn.Module):
gemm_output_zero_allocator,
)
if not self.dsa_enable_prefill_cp and should_allreduce_fusion:
if (
not (self.dsa_enable_prefill_cp or self.mla_enable_prefill_cp)
and should_allreduce_fusion
):
hidden_states._sglang_needs_allreduce_fusion = True
if not should_allreduce_fusion:
@@ -2096,7 +2128,10 @@ class DeepseekV2Model(nn.Module):
self.first_k_dense_replace = config.first_k_dense_replace
self.pp_group = get_pp_group()
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
if self.dsa_enable_prefill_cp:
self.mla_enable_prefill_cp = (
is_prefill_context_parallel_enabled() and not is_deepseek_dsa(config)
)
if self.dsa_enable_prefill_cp or self.mla_enable_prefill_cp:
self.cp_size = get_attention_cp_size()
else:
self.cp_size = None
@@ -2129,6 +2164,8 @@ class DeepseekV2Model(nn.Module):
quant_config=quant_config,
prefix=prefix,
alt_stream=self.alt_stream,
dsa_enable_prefill_cp=self.dsa_enable_prefill_cp,
mla_enable_prefill_cp=self.mla_enable_prefill_cp,
),
pp_rank=self.pp_group.rank_in_group,
pp_size=self.pp_group.world_size,
@@ -2255,7 +2292,9 @@ class DeepseekV2Model(nn.Module):
else None
)
if dsa_use_prefill_cp(forward_batch):
if dsa_use_prefill_cp(
forward_batch, self.dsa_enable_prefill_cp
) or mla_use_prefill_cp(forward_batch, self.mla_enable_prefill_cp):
if self.pp_group.is_first_rank:
hidden_states = cp_split_and_rebuild_data(forward_batch, hidden_states)
positions = cp_split_and_rebuild_position(forward_batch, positions)
@@ -2340,7 +2379,10 @@ class DeepseekV2Model(nn.Module):
else:
hidden_states, _ = self.norm(hidden_states, residual)
if self.pp_group.is_last_rank and dsa_use_prefill_cp(forward_batch):
if self.pp_group.is_last_rank and (
dsa_use_prefill_cp(forward_batch, self.dsa_enable_prefill_cp)
or mla_use_prefill_cp(forward_batch, self.mla_enable_prefill_cp)
):
# allgather + rerrange
hidden_states = cp_all_gather_rerange_output(
hidden_states,
@@ -2418,7 +2460,10 @@ class DeepseekV2ForCausalLM(nn.Module, DeepseekV2WeightLoaderMixin):
self.capture_aux_hidden_states = False
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
if self.dsa_enable_prefill_cp:
self.mla_enable_prefill_cp = (
is_prefill_context_parallel_enabled() and not is_deepseek_dsa(config)
)
if self.dsa_enable_prefill_cp or self.mla_enable_prefill_cp:
self.cp_rank = get_attention_cp_rank()
self.cp_size = get_attention_cp_size()
else:
@@ -2510,15 +2555,29 @@ class DeepseekV2ForCausalLM(nn.Module, DeepseekV2WeightLoaderMixin):
input_embeds: torch.Tensor = None,
pp_proxy_tensors: Optional[PPProxyTensors] = None,
) -> torch.Tensor:
# Minor fix for multi-modal model: input_ids is None
len_input_ids = (
input_ids.shape[0] if input_ids is not None else input_embeds.shape[0]
)
if self.dsa_enable_prefill_cp:
if can_dsa_cp_split(
len(input_ids), self.cp_size, self.use_dsa, forward_batch
len_input_ids, self.cp_size, self.use_dsa, forward_batch
):
forward_batch.attn_cp_metadata = prepare_context_parallel_metadata(
len(input_ids),
len_input_ids,
self.cp_rank,
self.cp_size,
forward_batch.seq_lens_cpu.tolist(),
extend_lens=forward_batch.extend_seq_lens_cpu,
)
elif self.mla_enable_prefill_cp:
if can_cp_split(len_input_ids, self.cp_size, forward_batch):
forward_batch.attn_cp_metadata = prepare_context_parallel_metadata(
len_input_ids,
self.cp_rank,
self.cp_size,
forward_batch.seq_lens_cpu.tolist(),
extend_lens=forward_batch.extend_seq_lens_cpu,
)
with get_attn_tp_context().maybe_input_scattered(forward_batch):
+1
View File
@@ -1286,6 +1286,7 @@ class DeepseekV4ForCausalLM(nn.Module):
self.cp_rank,
self.cp_size,
forward_batch.seq_lens_cpu.tolist(),
extend_lens=forward_batch.extend_seq_lens_cpu,
)
if is_dsa_prefill_cp_round_robin_split():
attn_backend = get_attn_backend()
@@ -249,6 +249,7 @@ class DeepseekV4ForCausalLMNextN(DeepseekV4ForCausalLM):
self.cp_rank,
self.cp_size,
forward_batch.seq_lens_cpu.tolist(),
extend_lens=forward_batch.extend_seq_lens_cpu,
)
if is_dsa_prefill_cp_round_robin_split():
attn_backend = get_attn_backend()
@@ -7,11 +7,13 @@ import torch
from torch import nn
from transformers import PretrainedConfig
from sglang.srt.configs.model_config import is_deepseek_dsa
from sglang.srt.distributed import get_pp_group
from sglang.srt.layers.attention.dsa.utils import is_dsa_enable_prefill_cp
from sglang.srt.layers.layernorm import RMSNorm
from sglang.srt.layers.linear import RowParallelLinear
from sglang.srt.layers.quantization.base_config import QuantizationConfig
from sglang.srt.layers.utils.cp_utils import is_prefill_context_parallel_enabled
from sglang.srt.layers.vocab_parallel_embedding import VocabParallelEmbedding
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
from sglang.srt.models.deepseek_v2 import DeepseekV2DecoderLayer, DeepseekV2Model
@@ -36,6 +38,9 @@ class MistralLarge3EagleModel(DeepseekV2Model):
assert get_pp_group().world_size == 1
self.pp_group = get_pp_group()
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
self.mla_enable_prefill_cp = (
is_prefill_context_parallel_enabled() and not is_deepseek_dsa(config)
)
self.embed_tokens = VocabParallelEmbedding(
config.vocab_size,
@@ -50,6 +55,8 @@ class MistralLarge3EagleModel(DeepseekV2Model):
prefix=add_prefix(prefix, f"layers.{i}"),
quant_config=quant_config,
layer_id=i,
dsa_enable_prefill_cp=self.dsa_enable_prefill_cp,
mla_enable_prefill_cp=self.mla_enable_prefill_cp,
)
for i in range(self.config.num_hidden_layers)
]
+1
View File
@@ -1005,6 +1005,7 @@ class Qwen3MoeForCausalLM(nn.Module):
self.attn_cp_rank,
self.attn_cp_size,
forward_batch.seq_lens_cpu.tolist(),
extend_lens=forward_batch.extend_seq_lens_cpu,
)
hidden_states = self.model(
+57 -3
View File
@@ -1825,10 +1825,20 @@ class ServerArgs:
assert (
self.tp_size <= 8
), "Context parallel only supports single machine (tp_size <= 8). Cross-machine CP has precision issues."
# Note(kpham-sgl): Keep attn_tp_size == 1 under DSA CP.
# DSACPLayerCommunicator does not all-reduce attention-TP
# partial o_proj outputs before replicated dense FFNs.
self.attn_cp_size = self.tp_size // self.dp_size
self.disable_piecewise_cuda_graph = True
logger.warning(
f"Enable Context Parallel opt for deeeseekv3.2-DSA, Setting dp_size == {self.dp_size} and moe_dense_tp_size == {self.moe_dense_tp_size}, ep_size == {self.ep_size}, tp_size == {self.tp_size}, kv_cache_dtype == {self.kv_cache_dtype}, moe_a2a_backend {self.moe_a2a_backend} "
f"Enable DSA Context Parallel opt, "
f"Setting dp_size == {self.dp_size} and "
f"moe_dense_tp_size == {self.moe_dense_tp_size}, "
f"ep_size == {self.ep_size}, "
f"tp_size == {self.tp_size}, "
f"kv_cache_dtype == {self.kv_cache_dtype}, "
f"moe_a2a_backend {self.moe_a2a_backend}, "
f"disable_piecewise_cuda_graph=True"
)
else:
# Pure TP and partial DP Attention mode is active for DSA, logging a warning
@@ -1870,7 +1880,7 @@ class ServerArgs:
), "CP is only supported for prefill when PD disaggregation, please remove --enable-dsa-prefill-context-parallel."
else:
# DeepSeek V3/R1/V3.1
# DeepSeek V3/R1/V3.1 and Kimi K2.5
if not self.disable_piecewise_cuda_graph:
logger.info("Piecewise CUDA graph is enabled, use MLA for prefill.")
@@ -1885,6 +1895,37 @@ class ServerArgs:
"Use trtllm_mla as attention backend on sm100 for DeepseekV3ForCausalLM"
)
# 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)
if self.enable_prefill_context_parallel and self.use_mla_backend():
logger.warning(
"MLA prefill context parallel is still experimental. "
"Verified on Hopper with the fa3 backend."
)
self.enable_dp_attention = True
# TODO(kpham-sgl) Supports moe_dense_tp_size != 1.
self.moe_dense_tp_size = 1
self.moe_a2a_backend = "deepep"
self.ep_size = self.tp_size
logger.warning(
"For MLA CP, we have the following restrictions: moe_dense_tp_size == 1, moe_a2a_backend == deepep, ep_size == tp_size, batch_size == 1"
)
# FIXME(kpham-sgl): Keep attn_tp_size == 1 under MLA CP.
# DSACPLayerCommunicator does not all-reduce attention-TP
# partial o_proj outputs before replicated dense FFNs.
self.attn_cp_size = self.tp_size // self.dp_size
self.disable_piecewise_cuda_graph = True
logger.warning(
f"Enable Context Parallel opt for MLA, "
f"Setting dp_size == {self.dp_size} and "
f"attn_cp_size == {self.attn_cp_size}, "
f"moe_dense_tp_size == {self.moe_dense_tp_size}, "
f"ep_size == {self.ep_size}, "
f"tp_size == {self.tp_size}, "
f"moe_a2a_backend {self.moe_a2a_backend}, "
f"disable_piecewise_cuda_graph=True"
)
# Set moe backend for DeepSeek
if is_sm100_supported():
quant_method = get_quantization_config(hf_config)
@@ -3039,6 +3080,19 @@ class ServerArgs:
)
def _handle_context_parallelism(self):
if (
self.enable_prefill_context_parallel
and self.enable_dsa_prefill_context_parallel
):
raise ValueError(
"--enable-prefill-context-parallel and "
"--enable-nsa-prefill-context-parallel are mutually "
"exclusive. Use --enable-nsa-prefill-context-parallel for "
"DeepSeek V3.2 (NSA) models and "
"--enable-prefill-context-parallel for MLA-based models "
"(DeepSeek V3/R1, Kimi K2.5) or MHA/GQA-based models."
)
if self.attn_cp_size > 1:
# The tp_size is the world size, not the real tensor parallel size
assert (