A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
[DCP] Support Decode Context Parallel (DCP) for GQA with FlashAttention (#24864)
Signed-off-by: yuanyongjie.yyj <yuanyongjie.yyj@antgroup.com> Signed-off-by: FENP <32334296+FENP@users.noreply.github.com> Signed-off-by: Jaya Yuan <yuanyongjie.yyj@antgroup.com>
Jaya Yuan committed
Oct 14, 2025 at 21:07 UTC
ea97940d6c2a0b56affa0d288b046acde4be616e
7 files changed
+209
-33
tests/distributed/test_context_parallel.py
+5
-1
@@ -204,17 +204,21 @@ def _compare_cp_with_tp(
204
205
206
CP_TEXT_GENERATION_MODELS = {
207
- # [MLA attention only]
207
"deepseek-ai/DeepSeek-V2-Lite-Chat": [
208
CPTestSettings.detailed(),
209
CPTestSettings.detailed(tp_base=2),
210
],
211
+ "bigcode/gpt_bigcode-santacoder": [
212
+ CPTestSettings.detailed(),
213
+ CPTestSettings.detailed(tp_base=2),
214
+ ],
215
}
216
217
CP_TEST_MODELS = [
218
# TODO support other models
219
# [LANGUAGE GENERATION]
220
"deepseek-ai/DeepSeek-V2-Lite-Chat",
221
+ "bigcode/gpt_bigcode-santacoder",
222
]
223
224
tests/models/registry.py
+4
-1
@@ -262,7 +262,10 @@ _TEXT_GENERATION_EXAMPLE_MODELS = {
262
"GPT2LMHeadModel": _HfExamplesInfo("openai-community/gpt2", {"alias": "gpt2"}),
263
"GPTBigCodeForCausalLM": _HfExamplesInfo(
264
"bigcode/starcoder",
265
- extras={"tiny": "bigcode/tiny_starcoder_py"},
265
+ extras={
266
+ "tiny": "bigcode/tiny_starcoder_py",
267
+ "santacoder": "bigcode/gpt_bigcode-santacoder",
268
+ },
269
min_transformers_version="4.55.1",
270
transformers_version_reason="HF model broken in 4.55.0",
271
),
vllm/attention/ops/common.py
+9
-1
@@ -173,6 +173,7 @@ def cp_lse_ag_out_rs(
173
cp_attn_lse: torch.Tensor,
174
cp_group: GroupCoordinator,
175
ctx: CPTritonContext = None,
176
+ return_lse=False,
177
):
178
"""
179
cp_attn_out: [ B, H, D ]
@@ -192,8 +193,15 @@ def cp_lse_ag_out_rs(
193
194
cp_attn_lse = cp_attn_lse.contiguous()
195
lses = cp_group.all_gather(cp_attn_lse, dim=0).view_as(lses)
195
- out, _ = correct_attn_out(cp_attn_out, lses, cp_group.rank_in_group, ctx)
196
+ out, lse = correct_attn_out(cp_attn_out, lses, cp_group.rank_in_group, ctx)
197
+ assert out.is_contiguous()
198
out = cp_group.reduce_scatter(out, dim=1)
199
+
200
+ if return_lse:
201
+ cp_num_heads = lse.shape[1] // cp_group.world_size
202
+ cp_rank = cp_group.rank_in_group
203
+ lse = lse[:, cp_num_heads * cp_rank : cp_num_heads * (cp_rank + 1)]
204
+ return out, lse
205
return out
206
207
vllm/config/model.py
+17
@@ -1202,6 +1202,23 @@ class ModelConfig:
1202
"Supported models implement the `SupportsPP` interface."
1203
)
1204
1205
+ decode_context_parallel_size = parallel_config.decode_context_parallel_size
1206
+ if decode_context_parallel_size > 1 and not self.use_mla:
1207
+ total_num_kv_heads = self.get_total_num_kv_heads()
1208
+ assert tensor_parallel_size > total_num_kv_heads, (
1209
+ f"tensor parallel size {tensor_parallel_size} must be greater "
1210
+ f"than total num kv heads {total_num_kv_heads} when enable "
1211
+ f"decode context parallel for GQA/MQA"
1212
+ )
1213
+
1214
+ max_dcp_size = tensor_parallel_size // total_num_kv_heads
1215
+ assert decode_context_parallel_size <= max_dcp_size, (
1216
+ f"decode context parallel size must less than or equal to "
1217
+ f"(tensor parallel size {tensor_parallel_size} // total "
1218
+ f"num kv heads {total_num_kv_heads}) = {max_dcp_size}, "
1219
+ f"but got {decode_context_parallel_size}"
1220
+ )
1221
+
1222
def get_sliding_window(self) -> int | None:
1223
"""Get the sliding window size from the HF text config if present."""
1224
return getattr(self.hf_text_config, "sliding_window", None)
vllm/v1/attention/backends/flash_attn.py
+172
-30
@@ -17,6 +17,7 @@ from vllm.attention.backends.abstract import (
17
is_quantized_kv_cache,
18
)
19
from vllm.attention.layer import Attention
20
+from vllm.attention.ops.common import cp_lse_ag_out_rs
21
from vllm.attention.ops.merge_attn_states import merge_attn_states
22
from vllm.attention.utils.fa_utils import (
23
flash_attn_supports_fp8,
@@ -32,6 +33,7 @@ if is_flash_attn_varlen_func_available():
33
)
34
35
from vllm.config import VllmConfig, get_layers_from_vllm_config
36
+from vllm.distributed.parallel_state import get_dcp_group
37
from vllm.logger import init_logger
38
from vllm.utils import cdiv
39
from vllm.v1.attention.backends.utils import (
@@ -147,6 +149,10 @@ class FlashAttentionMetadata:
149
prefix_kv_lens: torch.Tensor | None
150
suffix_kv_lens: torch.Tensor | None
151
152
+ # For GQA DCP
153
+ max_dcp_context_kv_len: int | None = None
154
+ dcp_context_kv_lens: torch.Tensor | None = None
155
+
156
# Optional aot scheduling
157
scheduler_metadata: torch.Tensor | None = None
158
prefix_scheduler_metadata: torch.Tensor | None = None
@@ -216,6 +222,16 @@ class FlashAttentionMetadataBuilder(AttentionMetadataBuilder[FlashAttentionMetad
222
self.max_num_splits = 0 # No upper bound on the number of splits.
223
self.aot_schedule = get_flash_attn_version() == 3
224
225
+ try:
226
+ from vllm.distributed.parallel_state import get_dcp_group
227
+
228
+ self.dcp_world_size = get_dcp_group().world_size
229
+ self.dcp_rank = get_dcp_group().rank_in_group
230
+ except AssertionError:
231
+ # DCP might not be initialized in testing
232
+ self.dcp_world_size = 1
233
+ self.dcp_rank = 0
234
+
235
self.use_full_cuda_graph = (
236
self.compilation_config.cudagraph_mode.has_full_cudagraphs()
237
)
@@ -306,7 +322,7 @@ class FlashAttentionMetadataBuilder(AttentionMetadataBuilder[FlashAttentionMetad
322
batch_size=batch_size,
323
max_seqlen_q=max_query_len,
324
max_seqlen_k=max_seq_len,
309
- num_heads_q=self.num_heads_q,
325
+ num_heads_q=self.num_heads_q * self.dcp_world_size,
326
num_heads_kv=self.num_heads_kv,
327
headdim=self.headdim,
328
cache_seqlens=seqlens,
@@ -320,8 +336,35 @@ class FlashAttentionMetadataBuilder(AttentionMetadataBuilder[FlashAttentionMetad
336
return None
337
338
use_cascade = common_prefix_len > 0
339
+ max_dcp_context_kv_len = 0
340
+ dcp_context_kv_lens = None
341
+
342
+ cu_prefix_query_lens = None
343
+ prefix_kv_lens = None
344
+ suffix_kv_lens = None
345
+ prefix_scheduler_metadata = None
346
+
347
+ if self.dcp_world_size > 1:
348
+ query_kv_lens_cpu = (
349
+ common_attn_metadata.query_start_loc_cpu[1:]
350
+ - common_attn_metadata.query_start_loc_cpu[:-1]
351
+ )
352
+ dcp_context_kv_lens_cpu = seq_lens_cpu - query_kv_lens_cpu
353
+ dcp_context_kv_lens_cpu = dcp_context_kv_lens_cpu // self.dcp_world_size + (
354
+ self.dcp_rank <= (dcp_context_kv_lens_cpu - 1) % self.dcp_world_size
355
+ )
356
+ dcp_context_kv_lens = dcp_context_kv_lens_cpu.to(self.device)
357
+ max_dcp_context_kv_len = dcp_context_kv_lens.max().item()
358
324
- if use_cascade:
359
+ scheduler_metadata = schedule(
360
+ batch_size=num_reqs,
361
+ cu_query_lens=query_start_loc,
362
+ max_query_len=max_query_len,
363
+ seqlens=dcp_context_kv_lens,
364
+ max_seq_len=max_dcp_context_kv_len,
365
+ causal=False,
366
+ )
367
+ elif use_cascade:
368
cu_prefix_query_lens = torch.tensor(
369
[0, num_actual_tokens], dtype=torch.int32, device=self.device
370
)
@@ -348,10 +391,6 @@ class FlashAttentionMetadataBuilder(AttentionMetadataBuilder[FlashAttentionMetad
391
causal=True,
392
)
393
else:
351
- cu_prefix_query_lens = None
352
- prefix_kv_lens = None
353
- suffix_kv_lens = None
354
- prefix_scheduler_metadata = None
394
scheduler_metadata = schedule(
395
batch_size=num_reqs,
396
cu_query_lens=query_start_loc,
@@ -379,6 +418,8 @@ class FlashAttentionMetadataBuilder(AttentionMetadataBuilder[FlashAttentionMetad
418
seq_lens=seq_lens,
419
block_table=block_table_tensor,
420
slot_mapping=slot_mapping,
421
+ max_dcp_context_kv_len=max_dcp_context_kv_len,
422
+ dcp_context_kv_lens=dcp_context_kv_lens,
423
use_cascade=use_cascade,
424
common_prefix_len=common_prefix_len,
425
scheduler_metadata=scheduler_metadata,
@@ -396,6 +437,8 @@ class FlashAttentionMetadataBuilder(AttentionMetadataBuilder[FlashAttentionMetad
437
438
439
class FlashAttentionImpl(AttentionImpl):
440
+ can_return_lse_for_decode: bool = True
441
+
442
def __init__(
443
self,
444
num_heads: int,
@@ -562,30 +605,45 @@ class FlashAttentionImpl(AttentionImpl):
605
606
descale_shape = (cu_seqlens_q.shape[0] - 1, self.num_kv_heads)
607
565
- flash_attn_varlen_func(
566
- q=query[:num_actual_tokens],
567
- k=key_cache,
568
- v=value_cache,
569
- out=output[:num_actual_tokens],
570
- cu_seqlens_q=cu_seqlens_q,
571
- max_seqlen_q=max_seqlen_q,
572
- seqused_k=seqused_k,
573
- max_seqlen_k=max_seqlen_k,
574
- softmax_scale=self.scale,
575
- causal=attn_metadata.causal,
576
- alibi_slopes=self.alibi_slopes,
577
- window_size=self.sliding_window,
578
- block_table=block_table,
579
- softcap=self.logits_soft_cap,
580
- scheduler_metadata=scheduler_metadata,
581
- fa_version=self.vllm_flash_attn_version,
582
- q_descale=layer._q_scale.expand(descale_shape),
583
- k_descale=layer._k_scale.expand(descale_shape),
584
- v_descale=layer._v_scale.expand(descale_shape),
585
- num_splits=attn_metadata.max_num_splits,
586
- s_aux=self.sinks,
587
- )
588
- return output
608
+ if self.dcp_world_size > 1:
609
+ self._forward_with_dcp(
610
+ query[:num_actual_tokens],
611
+ key[:num_actual_tokens],
612
+ value[:num_actual_tokens],
613
+ key_cache,
614
+ value_cache,
615
+ output[:num_actual_tokens],
616
+ attn_metadata,
617
+ q_descale=layer._q_scale.expand(descale_shape),
618
+ k_descale=layer._k_scale.expand(descale_shape),
619
+ v_descale=layer._v_scale.expand(descale_shape),
620
+ )
621
+ return output
622
+ else:
623
+ flash_attn_varlen_func(
624
+ q=query[:num_actual_tokens],
625
+ k=key_cache,
626
+ v=value_cache,
627
+ out=output[:num_actual_tokens],
628
+ cu_seqlens_q=cu_seqlens_q,
629
+ max_seqlen_q=max_seqlen_q,
630
+ seqused_k=seqused_k,
631
+ max_seqlen_k=max_seqlen_k,
632
+ softmax_scale=self.scale,
633
+ causal=attn_metadata.causal,
634
+ alibi_slopes=self.alibi_slopes,
635
+ window_size=self.sliding_window,
636
+ block_table=block_table,
637
+ softcap=self.logits_soft_cap,
638
+ scheduler_metadata=scheduler_metadata,
639
+ fa_version=self.vllm_flash_attn_version,
640
+ q_descale=layer._q_scale.expand(descale_shape),
641
+ k_descale=layer._k_scale.expand(descale_shape),
642
+ v_descale=layer._v_scale.expand(descale_shape),
643
+ num_splits=attn_metadata.max_num_splits,
644
+ s_aux=self.sinks,
645
+ )
646
+ return output
647
648
# Cascade attention (rare case).
649
cascade_attention(
@@ -615,6 +673,86 @@ class FlashAttentionImpl(AttentionImpl):
673
)
674
return output
675
676
+ def _forward_with_dcp(
677
+ self,
678
+ query: torch.Tensor,
679
+ key: torch.Tensor,
680
+ value: torch.Tensor,
681
+ key_cache: torch.Tensor,
682
+ value_cache: torch.Tensor,
683
+ output: torch.Tensor,
684
+ attn_metadata: FlashAttentionMetadata,
685
+ q_descale: torch.Tensor | None = None,
686
+ k_descale: torch.Tensor | None = None,
687
+ v_descale: torch.Tensor | None = None,
688
+ ) -> torch.Tensor:
689
+ cu_seqlens_q = attn_metadata.query_start_loc
690
+ max_seqlen_q = attn_metadata.max_query_len
691
+ block_table = attn_metadata.block_table
692
+
693
+ query = query.contiguous()
694
+ query_across_dcp = get_dcp_group().all_gather(query, dim=1)
695
+ context_attn_out, context_lse = flash_attn_varlen_func(
696
+ q=query_across_dcp,
697
+ k=key_cache,
698
+ v=value_cache,
699
+ out=None,
700
+ cu_seqlens_q=cu_seqlens_q,
701
+ max_seqlen_q=max_seqlen_q,
702
+ seqused_k=attn_metadata.dcp_context_kv_lens,
703
+ max_seqlen_k=attn_metadata.max_dcp_context_kv_len,
704
+ softmax_scale=self.scale,
705
+ causal=False,
706
+ alibi_slopes=self.alibi_slopes,
707
+ window_size=self.sliding_window,
708
+ block_table=block_table,
709
+ softcap=self.logits_soft_cap,
710
+ return_softmax_lse=True,
711
+ scheduler_metadata=attn_metadata.scheduler_metadata,
712
+ fa_version=self.vllm_flash_attn_version,
713
+ q_descale=q_descale,
714
+ k_descale=k_descale,
715
+ v_descale=v_descale,
716
+ )
717
+ # FA returns LSE in shape [ H, B ] but cp_lse_ag_out_rs wants [ B, H ]
718
+ context_attn_out_cor, context_lse_cor = cp_lse_ag_out_rs(
719
+ context_attn_out,
720
+ context_lse.transpose(0, 1),
721
+ get_dcp_group(),
722
+ return_lse=True,
723
+ )
724
+ context_lse_cor = context_lse_cor.transpose(0, 1).contiguous()
725
+
726
+ query_attn_out, query_lse = flash_attn_varlen_func(
727
+ q=query,
728
+ k=key,
729
+ v=value,
730
+ out=None,
731
+ cu_seqlens_q=cu_seqlens_q,
732
+ max_seqlen_q=max_seqlen_q,
733
+ cu_seqlens_k=cu_seqlens_q,
734
+ max_seqlen_k=max_seqlen_q,
735
+ softmax_scale=self.scale,
736
+ causal=attn_metadata.causal,
737
+ alibi_slopes=self.alibi_slopes,
738
+ window_size=self.sliding_window,
739
+ softcap=self.logits_soft_cap,
740
+ return_softmax_lse=True,
741
+ fa_version=self.vllm_flash_attn_version,
742
+ q_descale=q_descale,
743
+ k_descale=k_descale,
744
+ v_descale=v_descale,
745
+ )
746
+ assert context_attn_out_cor.shape == query_attn_out.shape
747
+ assert context_lse_cor.shape == query_lse.shape
748
+ merge_attn_states(
749
+ output,
750
+ context_attn_out_cor,
751
+ context_lse_cor,
752
+ query_attn_out,
753
+ query_lse,
754
+ )
755
+
756
def _forward_encoder_attention(
757
self,
758
query: torch.Tensor,
@@ -684,6 +822,7 @@ def use_cascade_attention(
822
use_sliding_window: bool,
823
use_local_attention: bool,
824
num_sms: int,
825
+ dcp_world_size: int,
826
) -> bool:
827
"""Decide whether to use cascade attention.
828
@@ -705,6 +844,9 @@ def use_cascade_attention(
844
num_reqs = len(query_lens)
845
if num_reqs < 8:
846
return False
847
+ # disable cascade attention for DCP
848
+ if dcp_world_size > 1:
849
+ return False
850
851
# Heuristics to decide whether using cascade attention is beneficial.
852
# 1. When FlashDecoding is not used for normal attention, cascade attention
vllm/v1/attention/backends/utils.py
+1
@@ -345,6 +345,7 @@ class AttentionMetadataBuilder(abc.ABC, Generic[M]):
345
use_sliding_window: bool,
346
use_local_attention: bool,
347
num_sms: int,
348
+ dcp_world_size: int,
349
) -> bool:
350
return False
351
vllm/v1/worker/gpu_model_runner.py
+1
@@ -1523,6 +1523,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1523
use_sliding_window=use_sliding_window,
1524
use_local_attention=use_local_attention,
1525
num_sms=self.num_sms,
1526
+ dcp_world_size=self.dcp_world_size,
1527
)
1528
return common_prefix_len if use_cascade else 0
1529