@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / ef5a226819
Fork 0

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 5, 09:45 UTC

[PD][HeteroArch]Fix accuracy issue with CPU_ATTN as Decoder and Flash_ATTN as prefiller (#38935)

Signed-off-by: Chendi Xue <chendi.xue@intel.com>

Chendi.Xue committed Apr 8, 2026 at 22:19 UTC ef5a226819e02d0d4a931bcf42b80314076e3629
3 files changed +93
tests/v1/kv_connector/unit/test_nixl_connector.py
+4
@@ -523,6 +523,7 @@ class FakeNixlConnectorWorker(NixlConnectorWorker):
523 kv_cache_layout="HND",
524 block_size=self.block_size,
525 ssm_sizes=(0, 0),
526 + attn_backend_name=self.backend_name,
527 ),
528 remote_tp_rank=remote_tp_rank,
529 remote_tp_size=remote_tp_size,
@@ -972,6 +973,7 @@ class TestNixlHandshake:
973 kv_cache_layout=mismatched_layout,
974 block_size=worker.block_size,
975 ssm_sizes=(0, 0),
976 + attn_backend_name=worker.backend_name,
977 )
978
979 with pytest.raises(RuntimeError):
@@ -1028,6 +1030,7 @@ class TestNixlHandshake:
1030 kv_cache_layout="HND",
1031 block_size=worker.block_size,
1032 ssm_sizes=(0, 0),
1033 + attn_backend_name=worker.backend_name,
1034 )
1035
1036 # We don't check layout for homogeneous TP and MLA for now, as the
@@ -2347,6 +2350,7 @@ def test_compatibility_hash_validation(
2350 kv_cache_layout="HND",
2351 block_size=prefill_block_size,
2352 ssm_sizes=(0, 0),
2353 + attn_backend_name=decode_worker.backend_name,
2354 )
2355 handshake_payload = NixlHandshakePayload(
2356 compatibility_hash=remote_hash,
vllm/distributed/kv_transfer/kv_connector/v1/nixl_connector.py
+49
@@ -173,6 +173,7 @@ class NixlAgentMetadata:
173 kv_cache_layout: str
174 block_size: int
175 ssm_sizes: tuple[int, int]
176 + attn_backend_name: str
177
178
179 @dataclass
@@ -1116,6 +1117,7 @@ class NixlConnectorWorker:
1117
1118 self.num_blocks = kv_cache_config.num_blocks
1119 self.enable_permute_local_kv = False
1120 + self.enable_heterogeneous_attn_post_process = False
1121
1122 # KV Caches and nixl tracking data.
1123 self.device_type = current_platform.device_type
@@ -1776,6 +1778,7 @@ class NixlConnectorWorker:
1778 else self.host_buffer_kv_cache_layout,
1779 block_size=self.block_size,
1780 ssm_sizes=self._mamba_ssm_size,
1781 + attn_backend_name=self.backend_name,
1782 )
1783 # Wrap metadata in payload with hash for defensive decoding
1784 assert self.compat_hash is not None
@@ -2369,6 +2372,21 @@ class NixlConnectorWorker:
2372 "Or enable experimental feature to use HND to NHD support by "
2373 "setting 'enable_permute_local_kv'=True in --kv-transfer-config."
2374 )
2375 + # if remote_agent used attn is not same as local,
2376 + # hint heterogenuous attn post process
2377 + if (
2378 + nixl_agent_meta.attn_backend_name != self.backend_name
2379 + and self.backend_name in ["CPU_ATTN"]
2380 + ):
2381 + if self._is_hma_required:
2382 + raise RuntimeError(
2383 + "heterogeneous attn post process is not supported with HMA"
2384 + )
2385 + logger.info(
2386 + "[Experimental] CPU_ATTN backend is used, "
2387 + "hint heterogeneous attn post process"
2388 + )
2389 + self.enable_heterogeneous_attn_post_process = True
2390
2391 # Heterogeneous TP requires head-splitting, which only works with
2392 # HND layout. MLA and replicated-KV cases don't split on heads.
@@ -2542,6 +2560,28 @@ class NixlConnectorWorker:
2560 cache, indices, block_size_ratio
2561 )
2562
2563 + def post_process_device_kv_on_receive_heterogeneous_attn(
2564 + self, block_ids: list[int]
2565 + ):
2566 + """
2567 + Post process device kv cache after receiving from remote
2568 + for heterogeneous attention.
2569 + """
2570 + assert self.enable_heterogeneous_attn_post_process
2571 +
2572 + indices = torch.tensor(block_ids, device=self.device_type, dtype=torch.long)
2573 +
2574 + for _, cache_or_caches in self.device_kv_caches.items():
2575 + blocks_to_update = cache_or_caches.index_select(1, indices)
2576 + current_platform.pack_kv_cache(
2577 + key=blocks_to_update[0],
2578 + value=blocks_to_update[1],
2579 + key_cache=cache_or_caches[0],
2580 + value_cache=cache_or_caches[1],
2581 + block_ids=block_ids,
2582 + indices=indices,
2583 + )
2584 +
2585 def get_finished(self) -> tuple[set[str], set[str]]:
2586 """
2587 Get requests that are done sending or recving on this specific worker.
@@ -2566,6 +2606,7 @@ class NixlConnectorWorker:
2606 )
2607
2608 block_ids_for_blocksize_post_process = defaultdict(list)
2609 + block_ids_for_heterogeneous_attn_post_process = list[list[int]]()
2610 for req_id in done_recving:
2611 # clean up metadata for completed requests
2612 meta = self._recving_metadata.pop(req_id, None)
@@ -2585,12 +2626,20 @@ class NixlConnectorWorker:
2626 block_ids_for_blocksize_post_process[block_size_ratio].append(
2627 meta.local_physical_block_ids[0]
2628 )
2629 + # post processing for heterogeneous attention
2630 + if self.enable_heterogeneous_attn_post_process:
2631 + block_ids_for_heterogeneous_attn_post_process.append(
2632 + meta.local_physical_block_ids[0]
2633 + )
2634 for (
2635 block_size_ratio,
2636 block_ids_list,
2637 ) in block_ids_for_blocksize_post_process.items():
2638 self.post_process_device_kv_on_receive(block_size_ratio, block_ids_list)
2639
2640 + for block_ids in block_ids_for_heterogeneous_attn_post_process:
2641 + self.post_process_device_kv_on_receive_heterogeneous_attn(block_ids)
2642 +
2643 # Handle timeout to avoid stranding blocks on remote.
2644 now = time.perf_counter()
2645 while self._reqs_to_send:
vllm/platforms/cpu.py
+40
@@ -520,3 +520,43 @@ class CpuPlatform(Platform):
520 import vllm._C # noqa: F401
521 except ImportError as e:
522 logger.warning("Failed to import from vllm._C: %r", e)
523 +
524 + @classmethod
525 + def pack_kv_cache(
526 + cls,
527 + key: torch.Tensor,
528 + value: torch.Tensor,
529 + key_cache: torch.Tensor,
530 + value_cache: torch.Tensor,
531 + block_ids: list[int],
532 + indices: torch.Tensor,
533 + ) -> None:
534 + """
535 + Rewrite the kv cache shape for the current platform.
536 + """
537 + # Import lazily: cpu_attn pulls in _custom_ops, which needs a fully
538 + # initialized vllm.platforms (avoid circular import while CpuPlatform loads).
539 + from vllm._custom_ops import cpu_attn_reshape_and_cache
540 + from vllm.v1.attention.backends.cpu_attn import _get_attn_isa
541 +
542 + dtype = key.dtype
543 + # For CPU_ATTN, the shape is [N, num_kv_heads, block_size, head_size]
544 + _, _, block_size, head_size = key_cache.shape
545 + key = key.permute(0, 2, 1, 3).flatten(0, 1)
546 + value = value.permute(0, 2, 1, 3).flatten(0, 1)
547 +
548 + isa = _get_attn_isa(dtype, block_size, head_size)
549 + block_offsets = torch.arange(block_size, device="cpu", dtype=torch.long)
550 + num_blocks = len(block_ids)
551 + slot_mapping = (
552 + block_offsets.reshape(1, block_size)
553 + + indices.reshape(num_blocks, 1) * block_size
554 + ).flatten()
555 + cpu_attn_reshape_and_cache(
556 + key,
557 + value,
558 + key_cache,
559 + value_cache,
560 + slot_mapping,
561 + isa,
562 + )