[Bugfix] DSV4 TP16 garbage output (#47493)

Signed-off-by: Jeff Ma <jeffjma@umich.edu> Co-authored-by: Lucas Wilkinson <LucasWilkinson@users.noreply.github.com>

Jeff (Junze) Ma committed Jul 7, 2026 at 21:04 UTC 80eb01e93dcd21695210a6a9c43a82b25536d74d
5 files changed +61 -6
vllm/models/deepseek_v4/attention.py
+4 -4
@@ -618,7 +618,7 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
618 dtype=torch.uint8 if uses_fp8_ds_mla_layout else self.kv_cache_torch_dtype,
619 compress_ratio=self.compress_ratio,
620 cache_dtype_str=self.kv_cache_dtype,
621 - alignment=576 if uses_fp8_ds_mla_layout else None,
621 + alignment=576 if uses_fp8_ds_mla_layout else 512,
622 model_version="deepseek_v4",
623 kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype),
624 )
@@ -648,15 +648,15 @@ class DeepseekV4IndexerCache(torch.nn.Module, AttentionLayerBase):
648 def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
649 # head_dim already carries the fp8 scale padding
650 # compress_ratio=1 for V3.2, >1 for DeepseekV4; both use the same cache layout.
651 + uses_fp8_ds_mla_layout = vllm_config.cache_config.cache_dtype == "fp8_ds_mla"
652 return MLAAttentionSpec(
653 block_size=self.cache_config.block_size,
654 num_kv_heads=1,
655 head_size=self.head_dim,
656 dtype=self.dtype,
657 compress_ratio=self.compress_ratio,
657 - # DeepseekV4 aligns indexer pages to FlashMLA's 576B so they can pack with
658 - # the indexer's compressor state cache. V3.2 keeps the legacy layout.
659 - alignment=576,
658 + # 576B for FlashMLA packing; 512B for FlashInfer sparse (#44577).
659 + alignment=576 if uses_fp8_ds_mla_layout else 512,
660 )
661
662 def forward(self): ...
vllm/models/deepseek_v4/common/ops/cache_utils.py
+35
@@ -654,6 +654,8 @@ def build_flashinfer_mixed_sparse_indices(
654 topk: int,
655 decode_compressed_indices_are_local: bool = False,
656 decode_is_valid_token: torch.Tensor | None = None,
657 + swa_block_span: int | None = None,
658 + compressed_block_span: int | None = None,
659 ) -> tuple[torch.Tensor, torch.Tensor]:
660 """Build the FlashInfer DSV4 sparse-index matrix for decode-first batches.
661
@@ -730,6 +732,13 @@ def build_flashinfer_mixed_sparse_indices(
732 max_block_size = max(window_block_size, topk_block_size)
733 num_warps = 4 if max_block_size >= 256 else 1
734
735 + # block_span = page_stride / token_stride; == block_size (no-op) for unpacked KV.
736 + swa_span = swa_block_size if swa_block_span is None else swa_block_span
737 + compressed_span = (
738 + compressed_block_size
739 + if compressed_block_span is None
740 + else compressed_block_span
741 + )
742 _build_flashinfer_mixed_sparse_indices_kernel[(num_tokens,)](
743 sparse_indices,
744 sparse_indices.stride(0),
@@ -748,9 +757,11 @@ def build_flashinfer_mixed_sparse_indices(
757 swa_block_table,
758 swa_block_table.stride(0),
759 swa_block_size,
760 + swa_span,
761 compressed_block_table,
762 compressed_block_table.stride(0),
763 compressed_block_size,
764 + compressed_span,
765 NUM_DECODE_TOKENS=num_decode_tokens,
766 WINDOW_SIZE=window_size,
767 COMPRESS_RATIO=compress_ratio,
@@ -767,6 +778,18 @@ def build_flashinfer_mixed_sparse_indices(
778 return sparse_indices, sparse_topk_lens
779
780
781 +@triton.jit
782 +def _remap_flashinfer_index(values, block_size, block_span):
783 + # FlashInfer's DSv4 kernel indexes sparse KV by physical token stride, so
784 + # packed pages (#44577) need block*block_size+off -> block*block_span+off.
785 + # TODO: remove once flashinfer-ai/flashinfer#3856 is fixed.
786 + is_valid = values >= 0
787 + safe_values = tl.where(is_valid, values, 0)
788 + values = (safe_values // block_size) * block_span
789 + values += safe_values % block_size
790 + return tl.where(is_valid, values, -1)
791 +
792 +
793 @triton.jit(
794 do_not_specialize=[
795 "sparse_indices_stride",
@@ -775,8 +798,10 @@ def build_flashinfer_mixed_sparse_indices(
798 "prefill_topk_stride",
799 "swa_block_table_stride",
800 "swa_block_size",
801 + "swa_block_span",
802 "compressed_block_table_stride",
803 "compressed_block_size",
804 + "compressed_block_span",
805 "NUM_DECODE_TOKENS",
806 "PREFILL_TOPK_STRIDE",
807 ]
@@ -799,9 +824,11 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
824 swa_block_table_ptr,
825 swa_block_table_stride,
826 swa_block_size,
827 + swa_block_span,
828 compressed_block_table_ptr,
829 compressed_block_table_stride,
830 compressed_block_size,
831 + compressed_block_span,
832 NUM_DECODE_TOKENS,
833 WINDOW_SIZE: tl.constexpr,
834 COMPRESS_RATIO: tl.constexpr,
@@ -825,6 +852,7 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
852 mask=mask,
853 other=-1,
854 )
855 + values = _remap_flashinfer_index(values, swa_block_size, swa_block_span)
856 tl.store(
857 sparse_indices_ptr + token_idx * sparse_indices_stride + offset,
858 values,
@@ -858,6 +886,9 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
886 values = block_numbers * compressed_block_size + block_offsets
887 values = tl.where(is_valid, values, -1)
888 compressed_len += tl.sum((is_valid & token_valid).to(tl.int32), axis=0)
889 + values = _remap_flashinfer_index(
890 + values, compressed_block_size, compressed_block_span
891 + )
892 tl.store(
893 sparse_indices_ptr
894 + token_idx * sparse_indices_stride
@@ -904,6 +935,7 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
935 block_offsets = pos_offset % swa_block_size
936 slot_ids = block_numbers * swa_block_size + block_offsets
937 slot_ids = tl.where(offset < swa_len, slot_ids, -1)
938 + slot_ids = _remap_flashinfer_index(slot_ids, swa_block_size, swa_block_span)
939 tl.store(
940 sparse_indices_ptr + token_idx * sparse_indices_stride + offset,
941 slot_ids,
@@ -930,6 +962,9 @@ def _build_flashinfer_mixed_sparse_indices_kernel(
962 block_offsets = local_idx % compressed_block_size
963 slot_ids = block_numbers * compressed_block_size + block_offsets
964 slot_ids = tl.where((offset < topk_len) & is_valid, slot_ids, -1)
965 + slot_ids = _remap_flashinfer_index(
966 + slot_ids, compressed_block_size, compressed_block_span
967 + )
968 tl.store(
969 sparse_indices_ptr
970 + token_idx * sparse_indices_stride
vllm/models/deepseek_v4/compressor.py
+1 -1
@@ -162,7 +162,7 @@ class CompressorStateCache(torch.nn.Module, AttentionLayerBase):
162 head_size=self.state_dim,
163 dtype=self.dtype,
164 sliding_window=self.sliding_window,
165 - alignment=576 if uses_fp8_ds_mla_layout else None,
165 + alignment=576 if uses_fp8_ds_mla_layout else 512,
166 )
167
168 def forward(self): ...
vllm/models/deepseek_v4/nvidia/flashinfer_sparse.py
+19
@@ -45,6 +45,21 @@ def _get_flashinfer_dsv4_workspace(device: torch.device) -> torch.Tensor:
45 return workspace
46
47
48 +def _packed_block_span(pool: torch.Tensor) -> int:
49 + """Per-block stride of ``pool`` in tokens (``stride(0)//stride(-2)``): ==
50 + block_size for unpacked KV, larger when packed (#44577). Raises if not
51 + token-aligned."""
52 + block_stride = pool.stride(0)
53 + token_stride = pool.stride(-2)
54 + if block_stride % token_stride != 0:
55 + raise NotImplementedError(
56 + "FLASHINFER_MLA_SPARSE_DSV4 packed KV requires the per-block stride "
57 + f"({block_stride}) to be a multiple of the per-token stride "
58 + f"({token_stride}); this layout is not supported yet."
59 + )
60 + return block_stride // token_stride
61 +
62 +
63 class DeepseekV4FlashInferMLASparseBackend(DeepseekV4FlashMLABackend):
64 """FlashInfer backend using the DSv4 sparse metadata/cache layout.
65
@@ -368,6 +383,8 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
383 )
384 cached_sparse = swa_metadata.flashinfer_sparse_index_cache.get(cache_key, None)
385 if cached_sparse is None:
386 + swa_block_span = _packed_block_span(swa_k_cache)
387 + compressed_block_span = _packed_block_span(compressed_kv_cache)
388 sparse_indices, sparse_topk_lens = build_flashinfer_mixed_sparse_indices(
389 decode_swa_indices,
390 decode_compressed_indices,
@@ -385,6 +402,8 @@ class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
402 top_k,
403 decode_compressed_indices_are_local=decode_compressed_indices_are_local,
404 decode_is_valid_token=decode_is_valid_token,
405 + swa_block_span=swa_block_span,
406 + compressed_block_span=compressed_block_span,
407 )
408 if cache_key != "c4a":
409 swa_metadata.flashinfer_sparse_index_cache[cache_key] = (
vllm/v1/attention/backends/mla/sparse_swa.py
+2 -1
@@ -90,7 +90,8 @@ class DeepseekV4SWACache(torch.nn.Module, AttentionLayerBase):
90 dtype=self.dtype,
91 sliding_window=self.window_size,
92 cache_dtype_str=self.cache_config.cache_dtype,
93 - alignment=576 if uses_fp8_ds_mla_layout else None,
93 + # 576B for FlashMLA packing; 512B for FlashInfer sparse (#44577).
94 + alignment=576 if uses_fp8_ds_mla_layout else 512,
95 model_version="deepseek_v4",
96 kv_quant_mode=get_kv_quant_mode(self.cache_config.cache_dtype),
97 )