[ROCm][Perf] Enable layer-aware CSA2 multi-stream overlap for DeepSeek-V4.1-Flash (#57407)

Signed-off-by: Shanshan Shen <87969357+shen-shanshan@users.noreply.github.com>

Shanshan Shen committed Sep 27, 2026 at 23:48 UTC 44af287ebe38d6dc4e102948025f5e3e175aefd6
4 files changed +496 -20
tests/kernels/test_compressor_kv_cache.py
+266
@@ -2075,3 +2075,269 @@ def test_fused_kv_insert_split(num_tokens: int, kv_block_size: int):
2075 # RoPE (last 64): stored as bf16. The kernel recomputes the rotation, so it
2076 # is bf16-close to the reference rather than bit-exact (cf. test_cutedsl).
2077 torch.testing.assert_close(recovered[:, NOPE_DIM:], ref[:, NOPE_DIM:])
2078 +
2079 +
2080 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="ROCm stream coverage")
2081 +def test_v41_rocm_csa2_full_pipeline():
2082 + """_forward_csa2_full: one fork, then K write and q-side after the join.
2083 +
2084 + The default chain must run first on the current stream, the compressor
2085 + chain on aux 0 and the indexer weights projection on aux 1; the join must
2086 + publish both cache writes before _produce_k / forward_q run.
2087 + """
2088 + from vllm.forward_context import ForwardContext, override_forward_context
2089 + from vllm.models.deepseek_v41.amd.rocm import DeepseekV41ROCMAiterMLAAttention
2090 + from vllm.models.deepseek_v41.compressor import DeepseekCompressor
2091 +
2092 + torch.manual_seed(43)
2093 + num_tokens = 19
2094 + hidden_states = torch.randn(num_tokens, 1024, device="cuda")
2095 + positions = torch.arange(7, 26, device="cuda")
2096 + qr_kv = torch.zeros(num_tokens, 1536, device="cuda")
2097 + qr = torch.zeros(num_tokens, 4, device="cuda")
2098 + kv = torch.zeros(num_tokens, 512, device="cuda")
2099 + q = torch.zeros(num_tokens, 512, device="cuda")
2100 + latent = torch.ones(num_tokens, 512, dtype=torch.bfloat16, device="cuda")
2101 + weights = torch.randn(num_tokens, 32, device="cuda")
2102 + index_q = torch.full((num_tokens, 32, 128), 2.0, device="cuda")
2103 + index_q_scale = torch.full((num_tokens, 32), 3.0, device="cuda")
2104 + index_weights_out = torch.full((num_tokens, 32), 4.0, device="cuda")
2105 + main_cache = torch.full((1, 128, 584), 165, dtype=torch.uint8, device="cuda")
2106 + index_cache = torch.full((1, 128, 132), 165, dtype=torch.uint8, device="cuda")
2107 +
2108 + order: list[str] = []
2109 + streams: dict[str, torch.cuda.Stream] = {}
2110 +
2111 + compressor = DeepseekCompressor.__new__(DeepseekCompressor)
2112 + torch.nn.Module.__init__(compressor)
2113 + compressor.fused_wkv_wgate = SimpleNamespace(
2114 + weight=torch.randn(1024, 1024, device="cuda")
2115 + )
2116 +
2117 + def compressor_forward(kv_score, positions_):
2118 + order.append("compressor")
2119 + streams["compressor"] = torch.cuda.current_stream()
2120 + return latent
2121 +
2122 + def compressor_insert(latent_, positions_, rotary_emb):
2123 + order.append("insert_cache")
2124 + streams["insert_cache"] = torch.cuda.current_stream()
2125 + main_cache.fill_(5)
2126 +
2127 + compressor.forward = compressor_forward
2128 + compressor.insert_cache = compressor_insert
2129 +
2130 + def weights_proj(hidden_states_):
2131 + order.append("weights_proj")
2132 + streams["weights_proj"] = torch.cuda.current_stream()
2133 + return weights, None
2134 +
2135 + def produce_k(latent_, positions_, rotary_emb):
2136 + order.append("produce_k")
2137 + streams["produce_k"] = torch.cuda.current_stream()
2138 + index_cache.fill_(7)
2139 +
2140 + def forward_q(qr_, qr_scale, indexer_weights, positions_, rotary_emb):
2141 + order.append("forward_q")
2142 + streams["forward_q"] = torch.cuda.current_stream()
2143 + return index_q, index_q_scale, index_weights_out
2144 +
2145 + indexer = SimpleNamespace(
2146 + weights_proj=weights_proj,
2147 + _produce_k=produce_k,
2148 + forward_q=forward_q,
2149 + )
2150 + rotary = SimpleNamespace(cos_sin_cache=torch.randn(32, 64, device="cuda"))
2151 + aux_streams = [torch.cuda.Stream(), torch.cuda.Stream()]
2152 +
2153 + def wq_b(qr_, qr_scale):
2154 + order.append("wq_b")
2155 + streams["wq_b"] = torch.cuda.current_stream()
2156 + return q
2157 +
2158 + def qnorm_insert(q_, kv_, positions_, attn_metadata):
2159 + order.append("qnorm_insert")
2160 + streams["qnorm_insert"] = torch.cuda.current_stream()
2161 + return q_
2162 +
2163 + attention = SimpleNamespace(
2164 + compressor=compressor,
2165 + indexer=indexer,
2166 + aux_stream_list=aux_streams,
2167 + ln_events=[torch.cuda.Event() for _ in range(4)],
2168 + rotary_emb=rotary,
2169 + indexer_rotary_emb=rotary,
2170 + n_local_heads=1,
2171 + head_dim=512,
2172 + _fused_wqa_wkv_gemm=lambda hidden_states_: qr_kv,
2173 + _split_qkv_and_norm=lambda qr_kv_: (qr, None, kv),
2174 + _wq_b_proj=wq_b,
2175 + _fused_qnorm_rope_kv_insert=qnorm_insert,
2176 + )
2177 +
2178 + context = ForwardContext({}, {}, {})
2179 + with override_forward_context(context):
2180 + result = DeepseekV41ROCMAiterMLAAttention._forward_csa2_full(
2181 + attention, hidden_states, positions
2182 + )
2183 +
2184 + q_out, kv_out, index_q_out, index_q_scale_out, index_weights_out_out = result
2185 + assert torch.equal(q_out, q)
2186 + assert torch.equal(kv_out, kv)
2187 + assert torch.equal(index_q_out, index_q)
2188 + assert torch.equal(index_q_scale_out, index_q_scale)
2189 + assert torch.equal(index_weights_out_out, index_weights_out)
2190 + # Join publishes the aux-0 cache insert and orders the post-join K write.
2191 + assert torch.equal(main_cache, torch.full_like(main_cache, 5))
2192 + assert torch.equal(index_cache, torch.full_like(index_cache, 7))
2193 +
2194 + assert order == [
2195 + "wq_b",
2196 + "qnorm_insert",
2197 + "compressor",
2198 + "insert_cache",
2199 + "weights_proj",
2200 + "produce_k",
2201 + "forward_q",
2202 + ]
2203 + current = torch.cuda.current_stream()
2204 + for name in ("wq_b", "qnorm_insert", "produce_k", "forward_q"):
2205 + assert streams[name] == current, name
2206 + for name in ("compressor", "insert_cache"):
2207 + assert streams[name] == aux_streams[0], name
2208 + assert streams["weights_proj"] == aux_streams[1]
2209 +
2210 +
2211 +@pytest.mark.skipif(not current_platform.is_rocm(), reason="ROCm stream coverage")
2212 +def test_v41_rocm_csa2_reindex_pipeline():
2213 + """_forward_csa2_reindex: serial projections, then SWA q path vs q-side."""
2214 + from vllm.forward_context import ForwardContext, override_forward_context
2215 + from vllm.models.deepseek_v41.amd.rocm import DeepseekV41ROCMAiterMLAAttention
2216 +
2217 + torch.manual_seed(45)
2218 + num_tokens = 19
2219 + hidden_states = torch.randn(num_tokens, 1024, device="cuda")
2220 + positions = torch.arange(7, 26, device="cuda")
2221 + qr_kv = torch.zeros(num_tokens, 1536, device="cuda")
2222 + qr = torch.zeros(num_tokens, 4, device="cuda")
2223 + kv = torch.zeros(num_tokens, 512, device="cuda")
2224 + q = torch.zeros(num_tokens, 512, device="cuda")
2225 + indexer_weights = torch.randn(num_tokens, 32, device="cuda")
2226 + index_q = torch.full((num_tokens, 32, 128), 2.0, device="cuda")
2227 + index_q_scale = torch.full((num_tokens, 32), 3.0, device="cuda")
2228 + index_weights_out = torch.full((num_tokens, 32), 4.0, device="cuda")
2229 +
2230 + order: list[str] = []
2231 + streams: dict[str, torch.cuda.Stream] = {}
2232 +
2233 + def forward_q(qr_, qr_scale, indexer_weights_, positions_, rotary_emb):
2234 + order.append("forward_q")
2235 + streams["forward_q"] = torch.cuda.current_stream()
2236 + return index_q, index_q_scale, index_weights_out
2237 +
2238 + indexer = SimpleNamespace(forward_q=forward_q)
2239 + rotary = SimpleNamespace(cos_sin_cache=torch.randn(32, 64, device="cuda"))
2240 + aux0 = torch.cuda.Stream()
2241 +
2242 + def wq_b(qr_, qr_scale):
2243 + order.append("wq_b")
2244 + streams["wq_b"] = torch.cuda.current_stream()
2245 + return q
2246 +
2247 + def qnorm_insert(q_, kv_, positions_, attn_metadata):
2248 + order.append("qnorm_insert")
2249 + streams["qnorm_insert"] = torch.cuda.current_stream()
2250 + return q_
2251 +
2252 + attention = SimpleNamespace(
2253 + indexer=indexer,
2254 + aux_stream_list=[aux0],
2255 + ln_events=[torch.cuda.Event() for _ in range(2)],
2256 + indexer_rotary_emb=rotary,
2257 + n_local_heads=1,
2258 + head_dim=512,
2259 + _run_parallel_input_projections=lambda hidden_states_: (
2260 + qr_kv,
2261 + None,
2262 + indexer_weights,
2263 + ),
2264 + _split_qkv_and_norm=lambda qr_kv_: (qr, None, kv),
2265 + _wq_b_proj=wq_b,
2266 + _fused_qnorm_rope_kv_insert=qnorm_insert,
2267 + )
2268 +
2269 + context = ForwardContext({}, {}, {})
2270 + with override_forward_context(context):
2271 + result = DeepseekV41ROCMAiterMLAAttention._forward_csa2_reindex(
2272 + attention, hidden_states, positions
2273 + )
2274 +
2275 + q_out, kv_out, index_q_out, index_q_scale_out, index_weights_out_out = result
2276 + assert torch.equal(q_out, q)
2277 + assert torch.equal(kv_out, kv)
2278 + assert torch.equal(index_q_out, index_q)
2279 + assert torch.equal(index_q_scale_out, index_q_scale)
2280 + assert torch.equal(index_weights_out_out, index_weights_out)
2281 +
2282 + assert order == ["wq_b", "qnorm_insert", "forward_q"]
2283 + current = torch.cuda.current_stream()
2284 + for name in ("wq_b", "qnorm_insert"):
2285 + assert streams[name] == current, name
2286 + assert streams["forward_q"] == aux0
2287 +
2288 +
2289 +@pytest.mark.parametrize("quant_tuple", [False, True])
2290 +def test_v41_indexer_forward_q(quant_tuple):
2291 + """forward_q runs wq_b then the fused RoPE/quant and unpacks both shapes."""
2292 + from unittest.mock import patch
2293 +
2294 + from vllm.models.deepseek_v41.attention import DeepseekV4Indexer
2295 +
2296 + num_tokens, n_head, head_dim = 5, 32, 128
2297 + qr = torch.randn(num_tokens, 1280)
2298 + q = torch.randn(num_tokens, n_head * head_dim)
2299 + positions = torch.arange(num_tokens)
2300 + indexer_weights = torch.randn(num_tokens, n_head)
2301 + rotary = SimpleNamespace(cos_sin_cache=torch.randn(32, 64))
2302 + fake = SimpleNamespace(
2303 + n_head=n_head,
2304 + head_dim=head_dim,
2305 + softmax_scale=head_dim**-0.5,
2306 + use_fp4_kv=False,
2307 + indexer_weights_dtype=torch.float32,
2308 + _wq_b_proj=lambda qr_, qr_scale: q,
2309 + )
2310 + q_quant = torch.randn(num_tokens, n_head, head_dim)
2311 + q_scale = torch.randn(num_tokens, n_head)
2312 + weights_out = torch.randn(num_tokens, n_head)
2313 + if quant_tuple:
2314 + fused_return = ((q_quant, q_scale), weights_out)
2315 + expected = (q_quant, q_scale, weights_out)
2316 + else:
2317 + fused_return = (q_quant, weights_out)
2318 + expected = (q_quant, None, weights_out)
2319 +
2320 + with patch(
2321 + "vllm.models.deepseek_v41.attention.fused_indexer_q_rope_quant",
2322 + return_value=fused_return,
2323 + ) as mock_fused:
2324 + result = DeepseekV4Indexer.forward_q(
2325 + fake, qr, None, indexer_weights, positions, rotary
2326 + )
2327 +
2328 + assert torch.equal(result[0], expected[0])
2329 + assert (result[1] is None) == (expected[1] is None)
2330 + if expected[1] is not None:
2331 + assert torch.equal(result[1], expected[1])
2332 + assert torch.equal(result[2], expected[2])
2333 + args = mock_fused.call_args.args
2334 + assert args[0] is positions
2335 + torch.testing.assert_close(args[1], q.view(-1, n_head, head_dim))
2336 + assert args[2] is rotary.cos_sin_cache
2337 + assert args[3] is indexer_weights
2338 + assert args[4] == head_dim**-0.5
2339 + assert args[5] == n_head**-0.5
2340 + assert mock_fused.call_args.kwargs == {
2341 + "use_fp4": False,
2342 + "weights_out_dtype": torch.float32,
2343 + }
vllm/models/deepseek_v41/amd/rocm.py
+189
@@ -29,6 +29,7 @@ from vllm.models.deepseek_v41.sparse_mla import (
29 from vllm.platforms import current_platform
30 from vllm.platforms.rocm import _ON_GFX950
31 from vllm.triton_utils import tl, triton
32 +from vllm.utils.multi_stream_utils import execute_in_parallel
33 from vllm.v1.attention.backend import (
34 CommonAttentionMetadata,
35 )
@@ -529,6 +530,9 @@ class DeepseekV41ROCMAiterMLAAttention(DeepseekV4Attention):
530 "this rank; PP splits inside a v4.1 index-sharing group "
531 "are not supported."
532 )
533 + if self.compressor is None and self.indexer is None:
534 + # Dense layers have nothing to overlap; keep the base serial path.
535 + self.aux_stream_list = None
536
537 @classmethod
538 def get_padded_num_q_heads(cls, num_heads: int) -> int:
@@ -600,8 +604,193 @@ class DeepseekV41ROCMAiterMLAAttention(DeepseekV4Attention):
604 def _run_parallel_input_projections(
605 self, hidden_states: torch.Tensor
606 ) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
607 + if self.indexer is not None and self.compressor is None:
608 + # Serial: qr must sit on the default stream before the q-side
609 + # fork below can consume it (ROCm).
610 + aux_streams = self.aux_stream_list
611 + self.aux_stream_list = None
612 + try:
613 + return super()._run_parallel_input_projections(hidden_states)
614 + finally:
615 + self.aux_stream_list = aux_streams
616 return super()._run_parallel_input_projections(hidden_states)
617
618 + def _enable_multi_stream_overlap(self) -> bool:
619 + """ROCm multi-stream gates: streams and capture region.
620 +
621 + Dict metadata marks piecewise cudagraph, whose eager breaks rebuild
622 + the attention inputs on the owning stream. Forking side streams
623 + there would rely on runtime HIP event sync, which is unreliable in
624 + this overlap on ROCm (event waits can hang), so multi-stream only
625 + runs where the fork/join becomes static graph edges: inside capture,
626 + or with non-dict metadata (full cudagraph or the profile run), which
627 + has no eager breaks.
628 + """
629 + attn_metadata = get_forward_context().attn_metadata
630 + return self.aux_stream_list is not None and (
631 + torch.cuda.is_current_stream_capturing()
632 + or not isinstance(attn_metadata, dict)
633 + )
634 +
635 + def forward(
636 + self,
637 + positions: torch.Tensor,
638 + hidden_states: torch.Tensor,
639 + llama_4_scaling: torch.Tensor | None = None,
640 + ) -> torch.Tensor:
641 + if (
642 + self.indexer is None
643 + or self._prepare_and_attn_fn == self._prepare_and_attn_eager
644 + or not self._enable_multi_stream_overlap()
645 + ):
646 + # Sequential fallback: no forks outside the capture region, where
647 + # HIP event sync is unreliable; MRV1 keeps the input prep in its
648 + # wide eager region (#51430).
649 + aux_streams = self.aux_stream_list
650 + self.aux_stream_list = None
651 + try:
652 + return super().forward(positions, hidden_states, llama_4_scaling)
653 + finally:
654 + self.aux_stream_list = aux_streams
655 +
656 + attn_out = self._alloc_attn_out(hidden_states.shape[0], hidden_states)
657 + if self.compressor is not None:
658 + q, kv, index_q, index_q_scale, index_weights = self._forward_csa2_full(
659 + hidden_states, positions
660 + )
661 + else:
662 + q, kv, index_q, index_q_scale, index_weights = self._forward_csa2_reindex(
663 + hidden_states, positions
664 + )
665 + self._sparse_indexer_and_attn(
666 + hidden_states,
667 + index_q,
668 + index_q_scale,
669 + index_weights,
670 + q,
671 + kv,
672 + positions,
673 + attn_out,
674 + )
675 + return self._o_proj(attn_out, positions)
676 +
677 + def _forward_csa2_full(
678 + self, hidden_states: torch.Tensor, positions: torch.Tensor
679 + ) -> tuple[
680 + torch.Tensor,
681 + torch.Tensor,
682 + torch.Tensor | None,
683 + torch.Tensor | None,
684 + torch.Tensor | None,
685 + ]:
686 + """CSA layers: one fork replaces the base pipeline's three.
687 +
688 + Each fork/join is a HIP event pair per side stream on ROCm, so the
689 + base three-fork pipeline is expensive there; the compressor chain and
690 + the indexer weights projection run fully in parallel with the default
691 + chain. The indexer K write and q-side stay serial after the join:
692 + ``wk`` consumes the aux-0 latent and the q-side's wq_b consumes the
693 + default-stream qr.
694 + """
695 + attn_metadata = get_forward_context().attn_metadata
696 + compressor = self.compressor
697 + indexer = self.indexer
698 + assert compressor is not None and indexer is not None
699 + aux_streams = self.aux_stream_list
700 + assert aux_streams is not None and len(aux_streams) >= 2
701 +
702 + def default_chain():
703 + qr_kv = self._fused_wqa_wkv_gemm(hidden_states)
704 + qr, qr_scale, kv = self._split_qkv_and_norm(qr_kv)
705 + q = self._wq_b_proj(qr, qr_scale).view(
706 + -1, self.n_local_heads, self.head_dim
707 + )
708 + return (
709 + self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata),
710 + qr,
711 + qr_scale,
712 + kv,
713 + )
714 +
715 + def compressor_chain():
716 + kv_score = torch.mm(
717 + hidden_states,
718 + compressor.fused_wkv_wgate.weight.T,
719 + out_dtype=torch.float32,
720 + )
721 + latent = compressor(kv_score, positions)
722 + compressor.insert_cache(latent, positions, self.rotary_emb)
723 + return latent
724 +
725 + def indexer_weights_chain():
726 + # ReplicatedLinear returns (output, bias); bias is None.
727 + weights, _ = indexer.weights_proj(hidden_states)
728 + return weights
729 +
730 + (q, qr, qr_scale, kv), (latent, indexer_weights) = execute_in_parallel(
731 + default_chain,
732 + [compressor_chain, indexer_weights_chain],
733 + self.ln_events[0],
734 + self.ln_events[1:3],
735 + aux_streams[:2],
736 + enable=True,
737 + default_first=True,
738 + )
739 +
740 + indexer._produce_k(latent, positions, self.indexer_rotary_emb)
741 + index_q, index_q_scale, index_weights_out = indexer.forward_q(
742 + qr, qr_scale, indexer_weights, positions, self.indexer_rotary_emb
743 + )
744 + return q, kv, index_q, index_q_scale, index_weights_out
745 +
746 + def _forward_csa2_reindex(
747 + self, hidden_states: torch.Tensor, positions: torch.Tensor
748 + ) -> tuple[
749 + torch.Tensor,
750 + torch.Tensor,
751 + torch.Tensor | None,
752 + torch.Tensor | None,
753 + torch.Tensor | None,
754 + ]:
755 + """Indexer-only layers: the q-side overlaps the SWA q path.
756 +
757 + The input projections run serial first: the q-side consumes qr, so
758 + it can only overlap the SWA q projection and KV insert.
759 + """
760 + indexer = self.indexer
761 + assert indexer is not None
762 + attn_metadata = get_forward_context().attn_metadata
763 + # Serial on these layers via the _run_parallel_input_projections
764 + # override: qr must sit on the default stream before the fork.
765 + qr_kv, _, indexer_weights = self._run_parallel_input_projections(hidden_states)
766 + assert indexer_weights is not None
767 + qr, qr_scale, kv = self._split_qkv_and_norm(qr_kv)
768 +
769 + def swa_q_chain():
770 + q = self._wq_b_proj(qr, qr_scale).view(
771 + -1, self.n_local_heads, self.head_dim
772 + )
773 + return self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata)
774 +
775 + def indexer_q_chain():
776 + return indexer.forward_q(
777 + qr, qr_scale, indexer_weights, positions, self.indexer_rotary_emb
778 + )
779 +
780 + aux_streams = self.aux_stream_list
781 + assert aux_streams is not None
782 + q, aux_results = execute_in_parallel(
783 + swa_q_chain,
784 + [indexer_q_chain],
785 + self.ln_events[0],
786 + self.ln_events[1:2],
787 + aux_streams[:1],
788 + enable=True,
789 + default_first=True,
790 + )
791 + index_q, index_q_scale, index_weights_out = aux_results[0]
792 + return q, kv, index_q, index_q_scale, index_weights_out
793 +
794 @functools.cached_property
795 def _wq_b_uses_aiter_block_scaled(self) -> bool:
796 """True when both wq_b GEMMs run the aiter block-scaled fp8 kernel.
vllm/models/deepseek_v41/attention.py
+32 -18
@@ -478,7 +478,6 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
478 # graph and MRV1 produces garbage (#51430).
479 self._prepare_and_attn_fn = self._prepare_and_attn_eager
480
481 - # Will be None on ROCm for now.
481 self.aux_stream_list = aux_stream_list
482 # [0]: GEMM start / post-GEMM event0. [1..3]: GEMM done events;
483 # [1] doubles as post-GEMM event1. Reuse is safe: GEMM fully joins
@@ -1381,6 +1380,37 @@ class DeepseekV4Indexer(nn.Module):
1380 self.use_fp4_kv,
1381 )
1382
1383 + def forward_q(
1384 + self,
1385 + qr: torch.Tensor | QuantizedActivation,
1386 + qr_scale: torch.Tensor | None,
1387 + indexer_weights: torch.Tensor,
1388 + positions: torch.Tensor,
1389 + rotary_emb: nn.Module,
1390 + ) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
1391 + """Build the indexer queries: wq_b over qr plus fused RoPE/quant.
1392 +
1393 + Split out so the ROCm layer can schedule it apart from
1394 + ``_produce_k`` (e.g. on an aux stream once ``qr`` is ready).
1395 + """
1396 + q = self._wq_b_proj(qr, qr_scale)
1397 + q = q.view(-1, self.n_head, self.head_dim)
1398 + q_quant, weights = fused_indexer_q_rope_quant(
1399 + positions,
1400 + q,
1401 + rotary_emb.cos_sin_cache,
1402 + indexer_weights,
1403 + self.softmax_scale,
1404 + self.n_head**-0.5,
1405 + use_fp4=self.use_fp4_kv,
1406 + weights_out_dtype=self.indexer_weights_dtype,
1407 + )
1408 + if isinstance(q_quant, tuple):
1409 + q, q_scale = q_quant
1410 + else:
1411 + q, q_scale = q_quant, None
1412 + return q, q_scale, weights
1413 +
1414 def forward(
1415 self,
1416 qr: torch.Tensor | QuantizedActivation,
@@ -1422,23 +1452,7 @@ class DeepseekV4Indexer(nn.Module):
1452 # (skip_k_cache_insert=True).
1453 self._produce_k(latent, positions, rotary_emb)
1454
1425 - q = self._wq_b_proj(qr, qr_scale)
1426 - q = q.view(-1, self.n_head, self.head_dim)
1427 - q_quant, weights = fused_indexer_q_rope_quant(
1428 - positions,
1429 - q,
1430 - rotary_emb.cos_sin_cache,
1431 - indexer_weights,
1432 - self.softmax_scale,
1433 - self.n_head**-0.5,
1434 - use_fp4=self.use_fp4_kv,
1435 - weights_out_dtype=self.indexer_weights_dtype,
1436 - )
1437 - if isinstance(q_quant, tuple):
1438 - q, q_scale = q_quant
1439 - else:
1440 - q, q_scale = q_quant, None
1441 - return q, q_scale, weights
1455 + return self.forward_q(qr, qr_scale, indexer_weights, positions, rotary_emb)
1456
1457 def _wq_b_proj(
1458 self,
vllm/utils/multi_stream_utils.py
+9 -2
@@ -70,6 +70,7 @@ def execute_in_parallel(
70 done_events: list[torch.cuda.Event],
71 aux_streams: list[torch.cuda.Stream] | None = None,
72 enable: bool = False,
73 + default_first: bool = False,
74 ) -> tuple[Any, list[Any]]:
75 """Run default_fn on the current stream and aux_fns concurrently on
76 aux_streams.
@@ -97,6 +98,10 @@ def execute_in_parallel(
98 so callers that pass aux_streams must also pass enable=True
99 (typically gated by an env var) to actually overlap. When False,
100 execution falls back to sequential on the current stream.
101 + default_first: When True, enqueue the default chain before the aux
102 + chains. The default chain is typically the longest, so it starts
103 + first; the aux streams still only wait on start_event, so they
104 + overlap the default chain. Defaults to False.
105
106 Returns:
107 Tuple of (default_result, aux_results) where aux_results[i] is the
@@ -117,6 +122,8 @@ def execute_in_parallel(
122 pending: list[torch.cuda.Event] = []
123
124 start_event.record()
125 + if default_first:
126 + default_result = default_fn()
127 for i, fn in enumerate(aux_fns):
128 if fn is None:
129 continue
@@ -125,8 +132,8 @@ def execute_in_parallel(
132 aux_results[i] = fn()
133 done_events[i].record()
134 pending.append(done_events[i])
128 -
129 - default_result = default_fn()
135 + if not default_first:
136 + default_result = default_fn()
137
138 for ev in pending:
139 ev.wait()