[ROCm][Perf] Enable HCA dual-stream overlap for DeepSeek-V4 (#56853)
Signed-off-by: Shanshan Shen <87969357+shen-shanshan@users.noreply.github.com>
Shanshan Shen committed
Sep 17, 2026 at 14:46 UTC
fdcd42350bd2475abffc01bb73eec5d7c7a1e549
1 file changed
+35
-9
vllm/models/deepseek_v4/amd/rocm.py
+35
-9
@@ -558,14 +558,17 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
558
self._fused_compressor_split_sizes: tuple[int, int] | None = None
559
560
if self.indexer is None:
561
- # Only enable multi-stream overlap for CSA layer now.
562
- self.aux_stream_list = None
561
+ # Dense layers have no compressor work to overlap; HCA layers
562
+ # (compressor, no indexer) keep the streams for the dual-stream
563
+ # fork below.
564
+ if self.compressor is None:
565
+ self.aux_stream_list = None
566
else:
567
# Disable indexer inner overlap.
568
self.indexer.aux_stream = None
569
567
- def _enable_csa_multi_stream(self) -> bool:
568
- """All CSA multi-stream gates: env var, streams, and capture region.
570
+ def _enable_multi_stream_overlap(self) -> bool:
571
+ """ROCm multi-stream gates: streams and capture region.
572
573
Dict metadata marks piecewise cudagraph, whose eager breaks rebuild
574
the attention inputs on the owning stream. Forking side streams
@@ -573,7 +576,7 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
576
this overlap on ROCm (event waits can hang), so multi-stream only
577
runs where the fork/join becomes static graph edges: inside capture,
578
or with non-dict metadata (full cudagraph or the profile run), which
576
- has no eager breaks.
579
+ has no eager breaks. Covers both the HCA and CSA forks.
580
"""
581
attn_metadata = get_forward_context().attn_metadata
582
return self.aux_stream_list is not None and (
@@ -624,7 +627,7 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
627
device=hidden_states.device,
628
)
629
627
- if self._enable_csa_multi_stream():
630
+ if self._enable_multi_stream_overlap():
631
# The ROCm override consumes these sentinels inside the capture
632
# boundary, moving the stream fan-out ahead of the projections.
633
self._prepare_and_attn_fn(
@@ -658,7 +661,7 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
661
positions: torch.Tensor,
662
o_padded: torch.Tensor,
663
) -> None:
661
- """Run the ROCm CSA fork/join inside the graph capture boundary."""
664
+ """Run the ROCm fork/join (HCA or CSA) inside the capture boundary."""
665
aux_streams = self.aux_stream_list
666
# The sequential pipeline disables aux_stream_list before calling
667
# back with real projection inputs; aux_streams is None ends that
@@ -684,13 +687,13 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
687
688
# Re-check: forward's gate ran inside a captured segment that
689
# _prepare_and_attn_eager (MRV1) then broke, making this region eager.
687
- if not self._enable_csa_multi_stream():
690
+ if not self._enable_multi_stream_overlap():
691
self._run_sequential_pipeline(hidden_states, positions, o_padded)
692
return
693
694
indexer = self.indexer
695
compressor = self.compressor
693
- assert indexer is not None and compressor is not None
696
+ assert compressor is not None
697
698
def default_chain():
699
qr_kv = self._fused_wqa_wkv_gemm(hidden_states)
@@ -710,6 +713,24 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
713
)
714
compressor(score, positions, self.rotary_emb)
715
716
+ if indexer is None:
717
+ # HCA dual-stream: the main compressor runs on aux stream 0 while
718
+ # the default stream produces q and inserts KV into the SWA cache.
719
+ # Both branches only read hidden_states, so the join merely has to
720
+ # precede the sparse attention that consumes the compressed KV.
721
+ (q, _qr_out, _qr_scale_out, kv_out), _ = execute_in_parallel(
722
+ default_chain,
723
+ [main_compressor_chain],
724
+ self.ln_events[0],
725
+ [self.ln_events[1]],
726
+ aux_streams[:1],
727
+ enable=True,
728
+ )
729
+ self._sparse_indexer_and_attn(
730
+ hidden_states, None, None, None, q, kv_out, positions, o_padded
731
+ )
732
+ return
733
+
734
def indexer_compressor_chain() -> None:
735
score = torch.mm(
736
hidden_states,
@@ -718,6 +739,11 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
739
)
740
indexer.compressor(score, positions, self.indexer_rotary_emb)
741
742
+ # CSA three-stream: the main and indexer compressors run on aux
743
+ # streams 0 and 1 while the default stream produces q and inserts KV
744
+ # into the SWA cache. Every branch only reads hidden_states plus its
745
+ # own state, so the join merely has to precede the indexer op and the
746
+ # sparse attention, which consume the compressed KV caches.
747
(q, qr_out, qr_scale_out, kv_out), _ = execute_in_parallel(
748
default_chain,
749
[main_compressor_chain, indexer_compressor_chain],