forked from Karylab-cklius/vllm
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0e9c3c34a | ||
|
|
b6140f0b18 |
@@ -12,6 +12,7 @@ import logging
|
||||
import statistics
|
||||
import types
|
||||
from contextlib import contextmanager
|
||||
from math import prod
|
||||
|
||||
import torch
|
||||
from batch_spec import parse_batch_spec, reorder_for_flashinfer
|
||||
@@ -37,10 +38,13 @@ from vllm.config import (
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import (
|
||||
CommonAttentionMetadata,
|
||||
get_kv_cache_layout,
|
||||
set_kv_cache_layout,
|
||||
resolve_kv_cache_layout,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
compute_layer_kv_cache_shape_bytes,
|
||||
reshape_kv_cache,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec
|
||||
|
||||
# ============================================================================
|
||||
# Backend Configuration
|
||||
@@ -337,52 +341,23 @@ def _create_input_tensors(
|
||||
def _create_kv_cache(
|
||||
config: BenchmarkConfig,
|
||||
max_num_blocks: int,
|
||||
backend_class,
|
||||
device: torch.device,
|
||||
dtype: torch.dtype,
|
||||
) -> list:
|
||||
"""Create KV cache tensors for all layers using the backend's methods.
|
||||
|
||||
Uses the backend's get_kv_cache_shape() and get_kv_cache_stride_order()
|
||||
to create the cache with the correct shape and memory layout.
|
||||
"""
|
||||
# Get the logical shape from the backend
|
||||
cache_shape = backend_class.get_kv_cache_shape(
|
||||
num_blocks=max_num_blocks,
|
||||
"""Create KV cache tensors for all layers using the standard allocator."""
|
||||
spec = FullAttentionSpec(
|
||||
block_size=config.block_size,
|
||||
num_kv_heads=config.num_kv_heads,
|
||||
head_size=config.head_dim,
|
||||
dtype=dtype,
|
||||
)
|
||||
|
||||
# Get the stride order for custom memory layout
|
||||
try:
|
||||
stride_order = backend_class.get_kv_cache_stride_order()
|
||||
assert len(stride_order) == len(cache_shape)
|
||||
except (AttributeError, NotImplementedError):
|
||||
stride_order = tuple(range(len(cache_shape)))
|
||||
|
||||
# Permute shape to physical layout order
|
||||
physical_shape = tuple(cache_shape[i] for i in stride_order)
|
||||
|
||||
# Compute inverse permutation to get back to logical view
|
||||
inv_order = [stride_order.index(i) for i in range(len(stride_order))]
|
||||
|
||||
# Use fp8 dtype for cache when requested.
|
||||
cache_dtype = dtype
|
||||
if config.kv_cache_dtype == "fp8":
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
cache_dtype = current_platform.fp8_dtype()
|
||||
|
||||
cache_list = []
|
||||
for _ in range(config.num_layers):
|
||||
# Allocate in physical layout order (contiguous in memory)
|
||||
cache = torch.zeros(*physical_shape, device=device, dtype=cache_dtype)
|
||||
# Permute to logical view
|
||||
cache = cache.permute(*inv_order)
|
||||
cache_list.append(cache)
|
||||
|
||||
return cache_list
|
||||
layout = resolve_kv_cache_layout()
|
||||
total_bytes = (
|
||||
prod(compute_layer_kv_cache_shape_bytes(spec, max_num_blocks))
|
||||
* config.num_layers
|
||||
)
|
||||
buf = torch.zeros(total_bytes, device=device, dtype=torch.int8)
|
||||
return reshape_kv_cache(buf, spec, max_num_blocks, config.num_layers, layout)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -500,13 +475,6 @@ def run_attention_benchmark(config: BenchmarkConfig) -> BenchmarkResult:
|
||||
backend_cfg, config, device, dtype
|
||||
)
|
||||
|
||||
# Set KV cache layout if the backend requires a specific one
|
||||
# (e.g., FlashInfer requires HND on SM100/Blackwell for TRTLLM attention)
|
||||
required_layout = backend_class.get_required_kv_cache_layout()
|
||||
if required_layout is not None:
|
||||
set_kv_cache_layout(required_layout)
|
||||
get_kv_cache_layout.cache_clear()
|
||||
|
||||
common_metadata = _build_common_attn_metadata(
|
||||
q_lens, kv_lens, config.block_size, device
|
||||
)
|
||||
@@ -541,9 +509,7 @@ def run_attention_benchmark(config: BenchmarkConfig) -> BenchmarkResult:
|
||||
config, total_q, device, dtype, quantize_query=quantize_query
|
||||
)
|
||||
|
||||
cache_list = _create_kv_cache(
|
||||
config, max_num_blocks, backend_class, device, dtype
|
||||
)
|
||||
cache_list = _create_kv_cache(config, max_num_blocks, device, dtype)
|
||||
|
||||
timing_stats, mem_stats = _run_single_benchmark(
|
||||
config,
|
||||
|
||||
@@ -59,7 +59,7 @@ th:not(:first-child) {
|
||||
|
||||
<sup>1</sup> P and D instances must use the same speculation configuration.
|
||||
|
||||
<sup>2</sup> Requires `FLASH_ATTN` or `FLASHINFER` backend **and** `HND` KV cache layout. Enable via `--kv-transfer-config '{"kv_connector_extra_config": {"enable_cross_layers_blocks": "True"}}'`.
|
||||
<sup>2</sup> Cross-layer contiguity is achieved by using a `BLHNC` layout (set via `VLLM_KV_CACHE_LAYOUT=BLHNC` or `--enable-cross-layers`).
|
||||
|
||||
<sup>3</sup> Supported only when HMA is **not** required (i.e., non-hybrid models). Block IDs are remapped automatically. Only P block size < D block size is supported.
|
||||
|
||||
|
||||
@@ -414,15 +414,6 @@ Support use case: Prefill with 'HND' and decode with 'NHD' with experimental con
|
||||
--kv-transfer-config '{..., "enable_permute_local_kv":"True"}'
|
||||
```
|
||||
|
||||
### Cross layers blocks
|
||||
|
||||
By default, this feature is disabled. On attention backends that support this feature, each logical block is contiguous in physical memory. This reduces the number of buffers that need to be transferred.
|
||||
To enable this feature:
|
||||
|
||||
```bash
|
||||
--kv-transfer-config '{..., "kv_connector_extra_config": {"enable_cross_layers_blocks": "True"}}'
|
||||
```
|
||||
|
||||
## Metrics Reference
|
||||
|
||||
vLLM periodically logs a `KV Transfer metrics` line summarising NIXL transfer
|
||||
|
||||
@@ -39,7 +39,12 @@ from vllm.platforms import current_platform
|
||||
from vllm.utils.flashinfer import has_flashinfer
|
||||
from vllm.v1.attention.backend import AttentionMetadata
|
||||
from vllm.v1.attention.backends.registry import AttentionBackendEnum
|
||||
from vllm.v1.kv_cache_interface import AttentionSpec, get_kv_quant_mode
|
||||
from vllm.v1.attention.backends.utils import resolve_kv_cache_layout
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
AttentionSpec,
|
||||
get_kv_quant_mode,
|
||||
reshape_kv_cache,
|
||||
)
|
||||
|
||||
DEVICE_TYPE = current_platform.device_type
|
||||
FP8_DTYPE = current_platform.fp8_dtype()
|
||||
@@ -108,32 +113,27 @@ class AttentionQuantPatternModel(torch.nn.Module):
|
||||
max_blocks = (max(batch_spec.seq_lens) + self.block_size - 1) // self.block_size
|
||||
num_blocks = batch_size * max_blocks
|
||||
|
||||
# Fetch the attention backend and kv cache shape and stride order
|
||||
attn_backend = self.attn.attn_backend
|
||||
kv_cache_shape = attn_backend.get_kv_cache_shape(
|
||||
num_blocks,
|
||||
self.block_size,
|
||||
self.num_kv_heads,
|
||||
self.head_size,
|
||||
cache_dtype_str=self.attn.kv_cache_dtype,
|
||||
)
|
||||
try:
|
||||
kv_cache_stride_order = attn_backend.get_kv_cache_stride_order()
|
||||
except (AttributeError, NotImplementedError):
|
||||
kv_cache_stride_order = tuple(range(len(kv_cache_shape)))
|
||||
|
||||
kv_cache_shape = tuple(kv_cache_shape[i] for i in kv_cache_stride_order)
|
||||
inv_order = [
|
||||
kv_cache_stride_order.index(i) for i in range(len(kv_cache_stride_order))
|
||||
]
|
||||
|
||||
# Create dummy KV cache
|
||||
raw_tensor = torch.zeros(
|
||||
kv_cache_shape,
|
||||
spec = AttentionSpec(
|
||||
block_size=self.block_size,
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
head_size=self.head_size,
|
||||
dtype=self.attn.kv_cache_torch_dtype,
|
||||
kv_quant_mode=get_kv_quant_mode(self.attn.kv_cache_dtype),
|
||||
)
|
||||
layout = resolve_kv_cache_layout()
|
||||
num_layer_slots = 1 if layout.is_layer_compact else 2
|
||||
raw_tensor = torch.zeros(
|
||||
num_layer_slots * num_blocks * spec.page_size_bytes,
|
||||
dtype=torch.int8,
|
||||
device=self.device,
|
||||
)
|
||||
kv_cache = raw_tensor.permute(*inv_order)
|
||||
kv_cache = reshape_kv_cache(
|
||||
raw_tensor,
|
||||
spec,
|
||||
num_blocks,
|
||||
num_layer_slots,
|
||||
layout,
|
||||
)[0]
|
||||
|
||||
self.attn.kv_cache = kv_cache
|
||||
|
||||
|
||||
@@ -150,27 +150,14 @@ class MLAAttentionQuantPatternModel(torch.nn.Module):
|
||||
max_blocks = (max(batch_spec.seq_lens) + self.block_size - 1) // self.block_size
|
||||
num_blocks = batch_size * max_blocks
|
||||
|
||||
# MLA KV cache is 3D: (num_blocks, block_size, head_size)
|
||||
attn_backend = self.mla_attn.attn_backend
|
||||
kv_cache_shape = attn_backend.get_kv_cache_shape(
|
||||
num_blocks, self.block_size, 1, self.head_size
|
||||
# MLA KV cache is 4D: (num_blocks, num_heads=1, block_size, head_size)
|
||||
kv_cache = torch.zeros(
|
||||
(num_blocks, 1, self.block_size, self.head_size),
|
||||
dtype=self.kv_cache_dtype,
|
||||
device=self.device,
|
||||
)
|
||||
try:
|
||||
kv_cache_stride_order = attn_backend.get_kv_cache_stride_order()
|
||||
except (AttributeError, NotImplementedError):
|
||||
kv_cache_stride_order = tuple(range(len(kv_cache_shape)))
|
||||
|
||||
ordered_shape = tuple(kv_cache_shape[i] for i in kv_cache_stride_order)
|
||||
inv_order = [
|
||||
kv_cache_stride_order.index(i) for i in range(len(kv_cache_stride_order))
|
||||
]
|
||||
|
||||
raw_tensor = torch.zeros(
|
||||
ordered_shape, dtype=self.kv_cache_dtype, device=self.device
|
||||
)
|
||||
kv_cache = raw_tensor.permute(*inv_order)
|
||||
|
||||
self.mla_attn.kv_cache = kv_cache
|
||||
self.mla_attn.bind_kv_cache(kv_cache)
|
||||
|
||||
self.attn_metadata = self.builder.build(
|
||||
common_prefix_len=0, common_attn_metadata=common_attn_metadata
|
||||
|
||||
@@ -165,29 +165,15 @@ class MLARoPEKVCacheCatTestModel(torch.nn.Module):
|
||||
max_blocks = (max(batch_spec.seq_lens) + self.block_size - 1) // self.block_size
|
||||
num_blocks = batch_size * max_blocks
|
||||
|
||||
# Fetch the attention backend and kv cache shape and stride order
|
||||
kv_cache_shape = self.attn_backend.get_kv_cache_shape(
|
||||
num_blocks, self.block_size, self.num_kv_heads, self.head_size
|
||||
)
|
||||
try:
|
||||
kv_cache_stride_order = self.attn_backend.get_kv_cache_stride_order()
|
||||
except (AttributeError, NotImplementedError):
|
||||
kv_cache_stride_order = tuple(range(len(kv_cache_shape)))
|
||||
|
||||
kv_cache_shape = tuple(kv_cache_shape[i] for i in kv_cache_stride_order)
|
||||
inv_order = [
|
||||
kv_cache_stride_order.index(i) for i in range(len(kv_cache_stride_order))
|
||||
]
|
||||
|
||||
raw_tensor = torch.zeros(
|
||||
num_blocks * self.block_size * self.num_kv_heads * self.head_size,
|
||||
# MLA uses a 4D KV cache: (num_blocks, num_heads=1, block_size, head_size).
|
||||
kv_cache_shape = (num_blocks, 1, self.block_size, self.head_size)
|
||||
kv_cache = torch.zeros(
|
||||
kv_cache_shape,
|
||||
dtype=self.kv_cache_dtype,
|
||||
device=self.device,
|
||||
)
|
||||
raw_tensor = raw_tensor.view(kv_cache_shape)
|
||||
kv_cache = raw_tensor.permute(*inv_order)
|
||||
|
||||
self.mla_attn.kv_cache = kv_cache
|
||||
self.mla_attn.bind_kv_cache(kv_cache)
|
||||
|
||||
# Build attn metadata
|
||||
attn_metadata = self.builder.build(
|
||||
|
||||
@@ -38,7 +38,7 @@ from vllm.v1.attention.backend import (
|
||||
CommonAttentionMetadata,
|
||||
)
|
||||
from vllm.v1.attention.backends.registry import AttentionBackendEnum
|
||||
from vllm.v1.kv_cache_interface import AttentionSpec
|
||||
from vllm.v1.kv_cache_interface import AttentionSpec, KVCacheLayout, reshape_kv_cache
|
||||
|
||||
INDEX_SELECT_OP = torch.ops.aten.index.Tensor
|
||||
FP8_DTYPE = current_platform.fp8_dtype()
|
||||
@@ -128,20 +128,21 @@ class QKNormRoPEKVCacheTestModel(torch.nn.Module):
|
||||
self.attn._k_scale = self.attn._k_scale.to(device)
|
||||
self.attn._v_scale = self.attn._v_scale.to(device)
|
||||
|
||||
self.kv_cache_spec = AttentionSpec(
|
||||
block_size=self.block_size,
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
head_size=head_size,
|
||||
dtype=self.kv_cache_dtype,
|
||||
)
|
||||
self.builder = self.attn.attn_backend.get_builder_cls()(
|
||||
kv_cache_spec=AttentionSpec(
|
||||
block_size=self.block_size,
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
head_size=head_size,
|
||||
dtype=self.kv_cache_dtype,
|
||||
),
|
||||
kv_cache_spec=self.kv_cache_spec,
|
||||
layer_names=[self.attn.layer_name],
|
||||
vllm_config=vllm_config,
|
||||
device=device,
|
||||
)
|
||||
|
||||
def build_attn_metadata(
|
||||
self, batch_size: int, kv_stride_order: tuple[int, ...] | None = None
|
||||
self, batch_size: int, layout: KVCacheLayout
|
||||
) -> CommonAttentionMetadata:
|
||||
batch_spec = BatchSpec(seq_lens=[1] * batch_size, query_lens=[1] * batch_size)
|
||||
common_attn_metadata = create_common_attn_metadata(
|
||||
@@ -151,32 +152,22 @@ class QKNormRoPEKVCacheTestModel(torch.nn.Module):
|
||||
max_blocks = (max(batch_spec.seq_lens) + self.block_size - 1) // self.block_size
|
||||
num_blocks = batch_size * max_blocks
|
||||
|
||||
attn_backend = self.attn.attn_backend
|
||||
kv_cache_shape = attn_backend.get_kv_cache_shape(
|
||||
num_blocks, self.block_size, self.num_kv_heads, self.head_size
|
||||
)
|
||||
# Caller can force a physical layout; else use the backend's.
|
||||
if kv_stride_order is None:
|
||||
try:
|
||||
kv_stride_order = attn_backend.get_kv_cache_stride_order()
|
||||
except (AttributeError, NotImplementedError):
|
||||
kv_stride_order = tuple(range(len(kv_cache_shape)))
|
||||
|
||||
kv_cache_shape = tuple(kv_cache_shape[i] for i in kv_stride_order)
|
||||
inv_order = [kv_stride_order.index(i) for i in range(len(kv_stride_order))]
|
||||
|
||||
raw_tensor = torch.zeros(
|
||||
2 * num_blocks * self.block_size * self.num_kv_heads * self.head_size,
|
||||
dtype=self.kv_cache_dtype,
|
||||
num_blocks * self.kv_cache_spec.page_size_bytes,
|
||||
dtype=torch.int8,
|
||||
device=self.device,
|
||||
)
|
||||
raw_tensor = raw_tensor.view(kv_cache_shape)
|
||||
kv_cache = raw_tensor.permute(*inv_order)
|
||||
kv_cache = reshape_kv_cache(
|
||||
raw_tensor,
|
||||
self.kv_cache_spec,
|
||||
num_blocks,
|
||||
num_layer_slots=1,
|
||||
layout=layout,
|
||||
)[0]
|
||||
|
||||
# Store as a bare tensor (not wrapped in a list) to match production
|
||||
# `bind_kv_cache` behavior. `get_attention_context` returns this
|
||||
# attribute directly to the fused/unfused `do_kv_cache_update` impls,
|
||||
# which call `kv_cache.unbind(0)` and therefore require a tensor.
|
||||
# `bind_kv_cache` behavior. `get_attention_context` returns this
|
||||
# attribute directly to the fused/unfused cache update implementations.
|
||||
self.attn.kv_cache = kv_cache
|
||||
|
||||
attn_metadata = self.builder.build(
|
||||
@@ -253,7 +244,7 @@ def _run_qk_norm_rope_kvcache_fusion_test(
|
||||
block_size: int,
|
||||
is_neox: bool,
|
||||
use_shuffle_kv_layout: str,
|
||||
kv_stride_order: tuple[int, ...],
|
||||
kv_layout: KVCacheLayout,
|
||||
dtype: torch.dtype,
|
||||
kv_cache_dtype: str,
|
||||
rms_norm_eps: float,
|
||||
@@ -326,7 +317,7 @@ def _run_qk_norm_rope_kvcache_fusion_test(
|
||||
# Run unfused (eager) forward
|
||||
with set_forward_context(None, vllm_config):
|
||||
forward_context = get_forward_context()
|
||||
attn_metadata = model.build_attn_metadata(num_tokens, kv_stride_order)
|
||||
attn_metadata = model.build_attn_metadata(num_tokens, kv_layout)
|
||||
forward_context.slot_mapping = {
|
||||
model.layer_name: attn_metadata.slot_mapping
|
||||
}
|
||||
@@ -341,7 +332,7 @@ def _run_qk_norm_rope_kvcache_fusion_test(
|
||||
with set_forward_context(None, vllm_config):
|
||||
model_fused = torch.compile(model, backend=backend)
|
||||
forward_context = get_forward_context()
|
||||
attn_metadata = model_fused.build_attn_metadata(num_tokens, kv_stride_order)
|
||||
attn_metadata = model_fused.build_attn_metadata(num_tokens, kv_layout)
|
||||
forward_context.slot_mapping = {
|
||||
model.layer_name: attn_metadata.slot_mapping
|
||||
}
|
||||
@@ -419,10 +410,10 @@ _FUSION_CONFIGS = [
|
||||
@pytest.mark.parametrize("num_tokens", [5, 16, 2048])
|
||||
@pytest.mark.parametrize("use_shuffle_kv_layout", ["1", "0"])
|
||||
@pytest.mark.parametrize(
|
||||
"kv_stride_order",
|
||||
"kv_layout",
|
||||
[
|
||||
pytest.param((0, 1, 2, 3, 4), id="block_first"),
|
||||
pytest.param((1, 0, 2, 3, 4), id="kv_first"),
|
||||
pytest.param(KVCacheLayout.LBHNC, id="head_major"),
|
||||
pytest.param(KVCacheLayout.LBNHC, id="token_major"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("enable_aiter_triton_rope", [True, False])
|
||||
@@ -435,6 +426,7 @@ _FUSION_CONFIGS = [
|
||||
not is_aiter_found_and_supported(),
|
||||
reason="Only test on ROCm with AITER installed and supported",
|
||||
)
|
||||
@pytest.mark.skip(reason="AITER fusion does not support packed standardized K/V caches")
|
||||
def test_qk_norm_rope_kvcache_fusion(
|
||||
num_tokens: int,
|
||||
num_heads: int,
|
||||
@@ -445,7 +437,7 @@ def test_qk_norm_rope_kvcache_fusion(
|
||||
attn_backend: AttentionBackendEnum,
|
||||
enable_aiter_triton_rope: bool,
|
||||
use_shuffle_kv_layout: str,
|
||||
kv_stride_order: tuple[int, ...],
|
||||
kv_layout: KVCacheLayout,
|
||||
block_size: int,
|
||||
dtype: torch.dtype,
|
||||
kv_cache_dtype: str,
|
||||
@@ -469,7 +461,7 @@ def test_qk_norm_rope_kvcache_fusion(
|
||||
block_size=block_size,
|
||||
is_neox=is_neox,
|
||||
use_shuffle_kv_layout=use_shuffle_kv_layout,
|
||||
kv_stride_order=kv_stride_order,
|
||||
kv_layout=kv_layout,
|
||||
dtype=dtype,
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
rms_norm_eps=rms_norm_eps,
|
||||
|
||||
@@ -37,6 +37,10 @@ from vllm.v1.attention.backend import (
|
||||
CommonAttentionMetadata,
|
||||
)
|
||||
from vllm.v1.attention.backends.registry import AttentionBackendEnum
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
compute_layer_kv_cache_shape_bytes,
|
||||
)
|
||||
|
||||
INDEX_SELECT_OP = torch.ops.aten.index.Tensor
|
||||
VLLM_UNIFIED_KV_CACHE_UPDATE_OP = torch.ops.vllm.unified_kv_cache_update
|
||||
@@ -136,28 +140,21 @@ class QKRoPEKVCacheTestModel(torch.nn.Module):
|
||||
max_blocks = (max(batch_spec.seq_lens) + self.block_size - 1) // self.block_size
|
||||
num_blocks = batch_size * max_blocks
|
||||
|
||||
# Fetch the attention backend and kv cache shape and stride order
|
||||
kv_cache_shape = self.attn_backend.get_kv_cache_shape(
|
||||
num_blocks, self.block_size, self.num_kv_heads, self.head_size
|
||||
kv_cache_shape = compute_layer_kv_cache_shape_bytes(
|
||||
FullAttentionSpec(
|
||||
block_size=self.block_size,
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
head_size=self.head_size,
|
||||
dtype=self.kv_cache_dtype,
|
||||
),
|
||||
num_blocks,
|
||||
)
|
||||
try:
|
||||
kv_cache_stride_order = self.attn_backend.get_kv_cache_stride_order()
|
||||
except (AttributeError, NotImplementedError):
|
||||
kv_cache_stride_order = tuple(range(len(kv_cache_shape)))
|
||||
|
||||
kv_cache_shape = tuple(kv_cache_shape[i] for i in kv_cache_stride_order)
|
||||
inv_order = [
|
||||
kv_cache_stride_order.index(i) for i in range(len(kv_cache_stride_order))
|
||||
]
|
||||
|
||||
# Create dummy KV cache
|
||||
raw_tensor = torch.zeros(
|
||||
2 * num_blocks * self.block_size * self.num_kv_heads * self.head_size,
|
||||
dtype=self.kv_cache_dtype,
|
||||
kv_cache = torch.zeros(
|
||||
kv_cache_shape,
|
||||
dtype=torch.int8,
|
||||
device=self.device,
|
||||
)
|
||||
raw_tensor = raw_tensor.view(kv_cache_shape)
|
||||
kv_cache = raw_tensor.permute(*inv_order)
|
||||
).view(self.kv_cache_dtype)
|
||||
|
||||
self.attn.kv_cache = kv_cache
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ def test_get_kv_connector_cache_layout_without_kv_connector():
|
||||
with set_current_vllm_config(vllm_config):
|
||||
# Test with default settings
|
||||
layout = get_kv_connector_cache_layout()
|
||||
assert layout == "NHD"
|
||||
assert layout is None
|
||||
|
||||
|
||||
def test_get_kv_connector_cache_layout_with_lmcache_connector():
|
||||
@@ -35,7 +35,7 @@ def test_get_kv_connector_cache_layout_with_lmcache_connector():
|
||||
with set_current_vllm_config(vllm_config):
|
||||
# Test with default settings
|
||||
layout = get_kv_connector_cache_layout()
|
||||
assert layout == "NHD"
|
||||
assert layout is None
|
||||
|
||||
|
||||
def test_get_kv_connector_cache_layout_with_nixl_connector():
|
||||
@@ -52,7 +52,7 @@ def test_get_kv_connector_cache_layout_with_nixl_connector():
|
||||
with set_current_vllm_config(vllm_config):
|
||||
# Test with default settings
|
||||
layout = get_kv_connector_cache_layout()
|
||||
assert layout == "HND"
|
||||
assert layout == "LBHNC"
|
||||
|
||||
|
||||
def test_get_kv_connector_cache_layout_with_multi_connector():
|
||||
@@ -75,4 +75,4 @@ def test_get_kv_connector_cache_layout_with_multi_connector():
|
||||
with set_current_vllm_config(vllm_config):
|
||||
# Test with default settings
|
||||
layout = get_kv_connector_cache_layout()
|
||||
assert layout == "HND"
|
||||
assert layout == "LBHNC"
|
||||
|
||||
@@ -19,7 +19,7 @@ NUM_LAYERS = [1] # Arbitrary values for testing
|
||||
NUM_HEADS = [8] # Arbitrary values for testing
|
||||
HEAD_SIZES = [64, 80, 256]
|
||||
BLOCK_SIZES = [8, 16, 32]
|
||||
CACHE_LAYOUTS = ["NHD", "HND"]
|
||||
CACHE_LAYOUTS = ["LBNHC", "LBHNC"]
|
||||
KV_SCALE_TYPES = ["tensor", "attn_head"]
|
||||
|
||||
# Parameters for MLA tests.
|
||||
@@ -196,8 +196,8 @@ def test_reshape_and_cache_flash(
|
||||
torch.set_default_device(device)
|
||||
torch.accelerator.set_device_index(device)
|
||||
assert implementation in ["cuda", "triton"]
|
||||
if implementation == "triton" and kv_cache_layout == "HND":
|
||||
pytest.skip("Triton implementation only supports NHD layout.")
|
||||
if implementation == "triton" and kv_cache_layout == "LBHNC":
|
||||
pytest.skip("Triton implementation only supports LBNHC layout.")
|
||||
|
||||
if kv_scale_type == "attn_head" and implementation != "cuda":
|
||||
pytest.skip("Only CUDA implementation supports attn_head scaling.")
|
||||
@@ -270,7 +270,7 @@ def test_reshape_and_cache_flash(
|
||||
v_scale = (value.amax(dim=(0, 2)) / 64.0).to(torch.float32)
|
||||
|
||||
def permute_and_compact(x):
|
||||
y = x if kv_cache_layout == "NHD" else x.permute(0, 2, 1, 3)
|
||||
y = x if kv_cache_layout == "LBNHC" else x.permute(0, 2, 1, 3)
|
||||
return y.contiguous()
|
||||
|
||||
if kv_cache_dtype != "nvfp4":
|
||||
@@ -284,8 +284,8 @@ def test_reshape_and_cache_flash(
|
||||
fp8_input.flatten(0, 2), scale, group_shape=None, out_dtype=output.dtype
|
||||
).reshape(*input.shape)
|
||||
else: # per-head: broadcast scale along the head dimension
|
||||
# Original code uses dim 2 for NHD, dim 1 for HND
|
||||
if kv_cache_layout == "NHD":
|
||||
# Original code uses dim 2 for LBNHC, dim 1 for LBHNC
|
||||
if kv_cache_layout == "LBNHC":
|
||||
result = fp8_input.to(output.dtype) * scale.view(1, 1, -1, 1)
|
||||
else:
|
||||
result = fp8_input.to(output.dtype) * scale.view(1, -1, 1, 1)
|
||||
@@ -354,28 +354,29 @@ def test_reshape_and_cache_flash(
|
||||
dequant_nvfp4_kv_cache,
|
||||
)
|
||||
|
||||
def dequant_nvfp4_cache_nhd(data_cache, scale_cache, global_scale):
|
||||
# data_cache: [N, T, H, data_dim] NHD (contiguous inner dims)
|
||||
# scale_cache: [N, T, H, scale_dim] NHD (contiguous inner dims)
|
||||
# Permute to HND layout for the dequant utility.
|
||||
data_hnd = data_cache.permute(0, 2, 1, 3)
|
||||
scale_hnd = scale_cache.permute(0, 2, 1, 3)
|
||||
result_hnd = dequant_nvfp4_kv_cache(
|
||||
data_hnd, scale_hnd, global_scale, head_size, block_size
|
||||
def dequant_nvfp4_cache_hnc(data_cache, scale_cache, global_scale):
|
||||
# data_cache: [H, N, T, data_dim] HNC layout
|
||||
# scale_cache: [H, N, T, scale_dim] HNC layout
|
||||
return dequant_nvfp4_kv_cache(
|
||||
data_cache, scale_cache, global_scale, head_size, block_size
|
||||
)
|
||||
return result_hnd.permute(0, 2, 1, 3) # back to [N, T, H, D]
|
||||
|
||||
result_key_cache = dequant_nvfp4_cache_nhd(
|
||||
result_key_cache = dequant_nvfp4_cache_hnc(
|
||||
nvfp4_key_data, key_scale_cache, k_scale.item()
|
||||
)
|
||||
result_value_cache = dequant_nvfp4_cache_nhd(
|
||||
result_value_cache = dequant_nvfp4_cache_hnc(
|
||||
nvfp4_value_data, value_scale_cache, v_scale.item()
|
||||
)
|
||||
|
||||
# Flatten [num_blocks, block_size] → [num_slots] and index by slot_mapping.
|
||||
# Result is HNC: (num_blocks, num_heads, block_size, head_size).
|
||||
# Flatten to (num_slots, num_heads, head_size) for comparison.
|
||||
num_slots = num_blocks * block_size
|
||||
result_key_flat = result_key_cache.reshape(num_slots, num_heads, head_size)
|
||||
result_value_flat = result_value_cache.reshape(num_slots, num_heads, head_size)
|
||||
result_key_flat = result_key_cache.permute(0, 2, 1, 3).reshape(
|
||||
num_slots, num_heads, head_size
|
||||
)
|
||||
result_value_flat = result_value_cache.permute(0, 2, 1, 3).reshape(
|
||||
num_slots, num_heads, head_size
|
||||
)
|
||||
|
||||
torch.testing.assert_close(
|
||||
result_key_flat[slot_mapping], key.float(), atol=1.5, rtol=0.5
|
||||
@@ -407,7 +408,7 @@ def test_reshape_and_cache_flash(
|
||||
for i in range(num_tokens):
|
||||
block_idx = block_indices_lst[i]
|
||||
block_offset = block_offsets_lst[i]
|
||||
if kv_cache_layout == "NHD":
|
||||
if kv_cache_layout == "LBNHC":
|
||||
cloned_key_cache[block_idx, block_offset, :, :] = key[i]
|
||||
cloned_value_cache[block_idx, block_offset, :, :] = value[i]
|
||||
else:
|
||||
|
||||
@@ -6,9 +6,6 @@ import pytest
|
||||
import torch
|
||||
|
||||
from vllm import _custom_ops as ops
|
||||
from vllm.models.minimax_m3.common.indexer import (
|
||||
MiniMaxM3IndexerBackend,
|
||||
)
|
||||
from vllm.models.minimax_m3.common.ops.index_topk import (
|
||||
minimax_m3_index_decode,
|
||||
minimax_m3_index_score,
|
||||
@@ -20,14 +17,21 @@ from vllm.models.minimax_m3.common.ops.sparse_attn import (
|
||||
minimax_m3_sparse_attn_decode,
|
||||
)
|
||||
from vllm.models.minimax_m3.common.sparse_attention import (
|
||||
MiniMaxM3SparseBackend,
|
||||
MiniMaxM3SparseTritonImpl,
|
||||
minimax_m3_use_aiter_sparse_pa,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.v1.attention.backends.utils import set_kv_cache_layout
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, MLAAttentionSpec
|
||||
from vllm.v1.worker.gpu.attn_utils import _reshape_kv_cache
|
||||
from vllm.v1.worker.utils import AttentionGroup
|
||||
from vllm.v1.attention.backends.utils import (
|
||||
resolve_kv_cache_layout,
|
||||
set_kv_cache_layout,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheLayout,
|
||||
MLAAttentionSpec,
|
||||
compute_layer_kv_cache_shape_bytes,
|
||||
reshape_kv_cache,
|
||||
)
|
||||
|
||||
if not (current_platform.is_cuda() or current_platform.is_rocm()):
|
||||
pytest.skip(
|
||||
@@ -46,26 +50,41 @@ def kv_layout(request):
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
|
||||
def _stride_order_for(backend: type[MiniMaxM3SparseBackend], ndim: int) -> tuple:
|
||||
"""Mirror the allocator's stride-order resolution (identity fallback)."""
|
||||
try:
|
||||
stride_order = backend.get_kv_cache_stride_order()
|
||||
assert len(stride_order) == ndim
|
||||
except (AttributeError, NotImplementedError):
|
||||
stride_order = tuple(range(ndim))
|
||||
def _layer_stride_order(ndim: int) -> tuple[int, ...]:
|
||||
"""Per-layer physical stride order for the active layout; the 3-dim
|
||||
indexer side cache (H=1) is contiguous, so identity."""
|
||||
if ndim == 3:
|
||||
return (0, 1, 2)
|
||||
stride_order = resolve_kv_cache_layout().layer_stride_order
|
||||
assert len(stride_order) == ndim
|
||||
return stride_order
|
||||
|
||||
|
||||
def _main_spec() -> FullAttentionSpec:
|
||||
return FullAttentionSpec(
|
||||
block_size=BLOCK_SIZE,
|
||||
num_kv_heads=NUM_KV_HEADS,
|
||||
head_size=HEAD_DIM,
|
||||
head_size_v=HEAD_DIM,
|
||||
dtype=DTYPE,
|
||||
)
|
||||
|
||||
|
||||
def _main_kv_logical_shape(num_pages: int) -> tuple[int, ...]:
|
||||
"""Standardized per-layer logical shape [B, H, N, C] for the main cache,
|
||||
derived the same way the production allocator does."""
|
||||
shape_bytes = compute_layer_kv_cache_shape_bytes(_main_spec(), num_pages)
|
||||
return (*shape_bytes[:-1], shape_bytes[-1] // DTYPE.itemsize)
|
||||
|
||||
|
||||
def _allocate_main_kv_via_contract(
|
||||
num_pages: int, device: torch.device | str = "cuda"
|
||||
) -> torch.Tensor:
|
||||
"""Build the main KV cache exactly as the production allocator does for the
|
||||
currently active layout: allocate the physical (permuted) tensor, then
|
||||
expose the inverse-permuted logical-NHD view the backend sees."""
|
||||
logical_shape = MiniMaxM3SparseBackend.get_kv_cache_shape(
|
||||
num_pages, BLOCK_SIZE, NUM_KV_HEADS, HEAD_DIM
|
||||
)
|
||||
stride_order = _stride_order_for(MiniMaxM3SparseBackend, len(logical_shape))
|
||||
expose the inverse-permuted logical [B, H, N, C] view the kernels see."""
|
||||
logical_shape = _main_kv_logical_shape(num_pages)
|
||||
stride_order = _layer_stride_order(len(logical_shape))
|
||||
physical_shape = tuple(logical_shape[i] for i in stride_order)
|
||||
inv_order = [stride_order.index(i) for i in range(len(stride_order))]
|
||||
raw = torch.randn(physical_shape, device=device, dtype=DTYPE)
|
||||
@@ -897,41 +916,48 @@ def test_prefill_sparse_attention_correctness(
|
||||
assert error.max().item() < 1.7e-2
|
||||
|
||||
|
||||
def test_main_backend_layout_contract():
|
||||
"""The main sparse backend exposes the logical-NHD shape and the
|
||||
flash_attn-style stride order for each layout."""
|
||||
def test_main_cache_layout_contract():
|
||||
"""The standardized per-layer logical shape is [B, H, N, C] with packed
|
||||
K/V content, and the legacy layout aliases resolve to the expected
|
||||
per-layer stride orders."""
|
||||
nb, bs, h, d = 7, BLOCK_SIZE, NUM_KV_HEADS, HEAD_DIM
|
||||
logical = MiniMaxM3SparseBackend.get_kv_cache_shape(nb, bs, h, d)
|
||||
logical = _main_kv_logical_shape(nb)
|
||||
assert logical == (nb, h, bs, 2 * d)
|
||||
# The old separate K/V-axis shape is no longer the logical shape.
|
||||
assert logical != (nb, 2, bs, h, d)
|
||||
|
||||
assert KVCacheLayout.LBHNC.layer_stride_order == (0, 1, 2, 3)
|
||||
assert KVCacheLayout.LBNHC.layer_stride_order == (0, 2, 1, 3)
|
||||
|
||||
try:
|
||||
set_kv_cache_layout("HND")
|
||||
assert MiniMaxM3SparseBackend.get_kv_cache_stride_order() == (0, 1, 2, 3)
|
||||
assert resolve_kv_cache_layout() is KVCacheLayout.LBHNC
|
||||
set_kv_cache_layout("NHD")
|
||||
assert MiniMaxM3SparseBackend.get_kv_cache_stride_order() == (0, 2, 1, 3)
|
||||
assert resolve_kv_cache_layout() is KVCacheLayout.LBNHC
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
for layout in ("NHD", "HND"):
|
||||
try:
|
||||
set_kv_cache_layout(layout)
|
||||
order = MiniMaxM3SparseBackend.get_kv_cache_stride_order()
|
||||
order = resolve_kv_cache_layout().layer_stride_order
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
# Valid permutation: no duplicates, covers every axis.
|
||||
assert set(order) == set(range(len(order)))
|
||||
|
||||
# M3 has no cross-layer KV blocks.
|
||||
with pytest.raises(NotImplementedError):
|
||||
MiniMaxM3SparseBackend.get_kv_cache_stride_order(
|
||||
include_num_layers_dimension=True
|
||||
)
|
||||
|
||||
def test_unknown_layout_raises():
|
||||
"""An unrecognized layout override is rejected at resolution time."""
|
||||
try:
|
||||
set_kv_cache_layout("BOGUS")
|
||||
with pytest.raises(ValueError, match="Unknown KV cache layout"):
|
||||
resolve_kv_cache_layout()
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
|
||||
def test_aiter_sparse_pa_layout_contract(monkeypatch):
|
||||
"""The shuffle-only AITER path retains separately contiguous K/V storage."""
|
||||
def test_aiter_sparse_pa_cache_uses_separate_head_groups(monkeypatch):
|
||||
import vllm.models.minimax_m3.common.sparse_attention as sparse_attn_mod
|
||||
|
||||
monkeypatch.setattr(sparse_attn_mod.rocm_aiter_ops, "is_enabled", lambda: True)
|
||||
@@ -941,73 +967,63 @@ def test_aiter_sparse_pa_layout_contract(monkeypatch):
|
||||
lambda: True,
|
||||
)
|
||||
|
||||
nb, bs, h, d = 7, BLOCK_SIZE, 1, HEAD_DIM
|
||||
logical = MiniMaxM3SparseBackend.get_kv_cache_shape(nb, bs, h, d)
|
||||
order = MiniMaxM3SparseBackend.get_kv_cache_stride_order()
|
||||
assert logical == (nb, 2, bs, h, d)
|
||||
assert order == (1, 0, 2, 3, 4)
|
||||
assert minimax_m3_use_aiter_sparse_pa(1)
|
||||
with pytest.raises(ValueError, match="num_kv_heads == 1"):
|
||||
minimax_m3_use_aiter_sparse_pa(2)
|
||||
|
||||
physical_shape = tuple(logical[i] for i in order)
|
||||
inv_order = [order.index(i) for i in range(len(order))]
|
||||
raw = torch.empty(physical_shape, device="cuda", dtype=DTYPE)
|
||||
logical_view = raw.permute(*inv_order)
|
||||
key_cache, value_cache = logical_view.unbind(1)
|
||||
spec = FullAttentionSpec(
|
||||
block_size=BLOCK_SIZE,
|
||||
num_kv_heads=1,
|
||||
head_size=HEAD_DIM,
|
||||
head_size_v=HEAD_DIM,
|
||||
dtype=DTYPE,
|
||||
separate_kv_head_groups=True,
|
||||
)
|
||||
num_blocks = 7
|
||||
raw = torch.empty(num_blocks * spec.page_size_bytes, dtype=torch.int8)
|
||||
kv_cache = reshape_kv_cache(
|
||||
raw,
|
||||
spec,
|
||||
num_blocks,
|
||||
num_layer_slots=1,
|
||||
layout=KVCacheLayout.LBHNC,
|
||||
)[0]
|
||||
assert kv_cache.shape == (num_blocks, 2, BLOCK_SIZE, HEAD_DIM)
|
||||
key_cache, value_cache = kv_cache.unbind(1)
|
||||
assert key_cache.is_contiguous()
|
||||
assert value_cache.is_contiguous()
|
||||
|
||||
|
||||
def test_aiter_sparse_pa_rejects_multiple_kv_heads(monkeypatch):
|
||||
"""Do not pair AITER's separated cache layout with the Triton fallback."""
|
||||
import vllm.models.minimax_m3.common.sparse_attention as sparse_attn_mod
|
||||
|
||||
monkeypatch.setattr(sparse_attn_mod.rocm_aiter_ops, "is_enabled", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
sparse_attn_mod.rocm_aiter_ops,
|
||||
"is_shuffle_kv_cache_enabled",
|
||||
lambda: True,
|
||||
def test_indexer_cache_squeezes_to_contiguous_3d():
|
||||
"""The indexer side cache is standardized 4D with H=1: under both layouts
|
||||
the allocator's logical view stays contiguous and squeezes (as
|
||||
`MiniMaxM3IndexerCache.bind_kv_cache` does) to the 3-dim
|
||||
[num_blocks, block_size, head_dim] cache the kernels consume."""
|
||||
nb = 5
|
||||
ispec = MLAAttentionSpec(
|
||||
block_size=BLOCK_SIZE, num_kv_heads=1, head_size=HEAD_DIM, dtype=DTYPE
|
||||
)
|
||||
shape_bytes = compute_layer_kv_cache_shape_bytes(ispec, nb)
|
||||
assert shape_bytes == (nb, 1, BLOCK_SIZE, HEAD_DIM * DTYPE.itemsize)
|
||||
assert _layer_stride_order(3) == (0, 1, 2)
|
||||
|
||||
with pytest.raises(ValueError, match="num_kv_heads == 1"):
|
||||
MiniMaxM3SparseBackend.get_kv_cache_shape(7, BLOCK_SIZE, 2, HEAD_DIM)
|
||||
|
||||
|
||||
def test_main_backend_unknown_layout_raises(monkeypatch):
|
||||
"""An unrecognized layout (injected past env-var validation) is rejected."""
|
||||
import vllm.models.minimax_m3.common.sparse_attention as sparse_attn_mod
|
||||
|
||||
monkeypatch.setattr(sparse_attn_mod, "get_kv_cache_layout", lambda: "BOGUS")
|
||||
with pytest.raises(ValueError, match="Unknown cache layout format"):
|
||||
MiniMaxM3SparseBackend.get_kv_cache_stride_order()
|
||||
|
||||
|
||||
def test_indexer_backend_stride_order_is_identity():
|
||||
"""The 3-dim indexer cache must not inherit the parent's 4-element stride
|
||||
order; it overrides to the 3-element identity so the allocator keeps the
|
||||
contiguous layout."""
|
||||
assert MiniMaxM3IndexerBackend.get_kv_cache_stride_order() == (0, 1, 2)
|
||||
|
||||
# Cross-layer (per-layer-stacked) KV blocks are not supported.
|
||||
with pytest.raises(NotImplementedError):
|
||||
MiniMaxM3IndexerBackend.get_kv_cache_stride_order(
|
||||
include_num_layers_dimension=True
|
||||
)
|
||||
|
||||
# The stride order matches the 3-dim indexer shape rank.
|
||||
indexer_shape = MiniMaxM3IndexerBackend.get_kv_cache_shape(
|
||||
5, BLOCK_SIZE, 1, HEAD_DIM
|
||||
)
|
||||
assert len(indexer_shape) == 3
|
||||
assert _stride_order_for(MiniMaxM3IndexerBackend, len(indexer_shape)) == (0, 1, 2)
|
||||
for layout in (KVCacheLayout.LBNHC, KVCacheLayout.LBHNC):
|
||||
iraw = torch.zeros(nb * ispec.page_size_bytes, dtype=torch.int8)
|
||||
view = reshape_kv_cache(iraw, ispec, nb, 1, layout, BLOCK_SIZE)[0]
|
||||
assert tuple(view.shape) == (nb, 1, BLOCK_SIZE, HEAD_DIM)
|
||||
indexer_cache = view.squeeze(1)
|
||||
assert tuple(indexer_cache.shape) == (nb, BLOCK_SIZE, HEAD_DIM)
|
||||
assert indexer_cache.is_contiguous()
|
||||
|
||||
|
||||
def test_hnd_allocation_is_packed_head_major():
|
||||
"""Under HND the backend-visible logical view is the packed head-major
|
||||
physical allocation."""
|
||||
nb, bs, h, d = 4, BLOCK_SIZE, NUM_KV_HEADS, HEAD_DIM
|
||||
logical = MiniMaxM3SparseBackend.get_kv_cache_shape(nb, bs, h, d)
|
||||
logical = _main_kv_logical_shape(nb)
|
||||
try:
|
||||
set_kv_cache_layout("HND")
|
||||
stride_order = MiniMaxM3SparseBackend.get_kv_cache_stride_order()
|
||||
stride_order = resolve_kv_cache_layout().layer_stride_order
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
@@ -1035,25 +1051,15 @@ def test_main_cache_is_block_first_and_unpadded():
|
||||
"""The allocator's contiguous-view branch (not the padded-strided branch)
|
||||
is used for the main GQA cache: its spec is unpadded and the physical
|
||||
layout keeps num_blocks as the first dimension under both layouts."""
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec
|
||||
|
||||
spec = FullAttentionSpec(
|
||||
block_size=BLOCK_SIZE,
|
||||
num_kv_heads=NUM_KV_HEADS,
|
||||
head_size=HEAD_DIM,
|
||||
head_size_v=HEAD_DIM,
|
||||
dtype=DTYPE,
|
||||
)
|
||||
spec = _main_spec()
|
||||
# Unpadded -> allocator uses kv_tensor.view(...) rather than as_strided().
|
||||
assert spec.page_size_padded is None
|
||||
|
||||
logical = MiniMaxM3SparseBackend.get_kv_cache_shape(
|
||||
4, BLOCK_SIZE, NUM_KV_HEADS, HEAD_DIM
|
||||
)
|
||||
logical = _main_kv_logical_shape(4)
|
||||
for layout in ("NHD", "HND"):
|
||||
try:
|
||||
set_kv_cache_layout(layout)
|
||||
order = MiniMaxM3SparseBackend.get_kv_cache_stride_order()
|
||||
order = resolve_kv_cache_layout().layer_stride_order
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
inv_order = [order.index(i) for i in range(len(order))]
|
||||
@@ -1357,93 +1363,25 @@ def test_decode_wrong_layout_breaks_parity():
|
||||
assert (actual.float() - expected.float()).abs().max().item() > 1.7e-2
|
||||
|
||||
|
||||
def _make_attn_group(backend, spec):
|
||||
return AttentionGroup(
|
||||
backend=backend,
|
||||
layer_names=["main"],
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
|
||||
|
||||
def test_main_cache_byte_identical_through_production_allocator():
|
||||
"""AC-2: drive the real allocator (`_reshape_kv_cache`) for the M3 main
|
||||
`FullAttentionSpec` under HND and assert the backend-visible view has the
|
||||
same shape, stride, and storage offset as the packed-HND allocation; the
|
||||
indexer `MLAAttentionSpec` allocates through the same path to its 3-dim
|
||||
shape."""
|
||||
"""AC-2: drive the real allocator (`reshape_kv_cache`) for the M3 main
|
||||
`FullAttentionSpec` under HND and assert the kernel-visible view has the
|
||||
same shape, stride, and storage offset as the packed-HND allocation."""
|
||||
nb = 4
|
||||
spec = FullAttentionSpec(
|
||||
block_size=BLOCK_SIZE,
|
||||
num_kv_heads=NUM_KV_HEADS,
|
||||
head_size=HEAD_DIM,
|
||||
head_size_v=HEAD_DIM,
|
||||
dtype=DTYPE,
|
||||
)
|
||||
spec = _main_spec()
|
||||
raw = torch.zeros(nb * spec.page_size_bytes, dtype=torch.int8)
|
||||
group = _make_attn_group(MiniMaxM3SparseBackend, spec)
|
||||
try:
|
||||
set_kv_cache_layout("HND")
|
||||
kv_caches = _reshape_kv_cache([group], {"main": raw}, "auto", [BLOCK_SIZE], {})
|
||||
layout = resolve_kv_cache_layout()
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
view = kv_caches["main"]
|
||||
view = reshape_kv_cache(raw, spec, nb, 1, layout, BLOCK_SIZE)[0]
|
||||
|
||||
oracle = raw.view(DTYPE).view((nb, NUM_KV_HEADS, BLOCK_SIZE, 2 * HEAD_DIM))
|
||||
assert tuple(view.shape) == tuple(oracle.shape)
|
||||
assert view.stride() == oracle.stride()
|
||||
assert view.storage_offset() == oracle.storage_offset()
|
||||
|
||||
# Indexer cache allocates through the same path under both layouts.
|
||||
ispec = MLAAttentionSpec(
|
||||
block_size=BLOCK_SIZE, num_kv_heads=1, head_size=HEAD_DIM, dtype=DTYPE
|
||||
)
|
||||
for layout in ("NHD", "HND"):
|
||||
iraw = torch.zeros(nb * ispec.page_size_bytes, dtype=torch.int8)
|
||||
igroup = AttentionGroup(
|
||||
backend=MiniMaxM3IndexerBackend,
|
||||
layer_names=["idx"],
|
||||
kv_cache_spec=ispec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
try:
|
||||
set_kv_cache_layout(layout)
|
||||
iout = _reshape_kv_cache([igroup], {"idx": iraw}, "auto", [BLOCK_SIZE], {})
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
assert tuple(iout["idx"].shape) == (nb, BLOCK_SIZE, HEAD_DIM)
|
||||
|
||||
|
||||
def test_indexer_inherited_stride_order_trips_allocator_assert():
|
||||
"""AC-4 negative: without the indexer override, the inherited 4-element
|
||||
stride order trips the allocator's `len(stride_order) == len(shape)` assert
|
||||
for the 3-dim indexer shape; the `AssertionError` is NOT swallowed by the
|
||||
allocator's `(AttributeError, NotImplementedError)` fallback."""
|
||||
|
||||
class _BrokenIndexerBackend(MiniMaxM3IndexerBackend):
|
||||
# Simulate inheriting the parent's 4-element stride order.
|
||||
get_kv_cache_stride_order = staticmethod(
|
||||
MiniMaxM3SparseBackend.get_kv_cache_stride_order
|
||||
)
|
||||
|
||||
nb = 4
|
||||
ispec = MLAAttentionSpec(
|
||||
block_size=BLOCK_SIZE, num_kv_heads=1, head_size=HEAD_DIM, dtype=DTYPE
|
||||
)
|
||||
iraw = torch.zeros(nb * ispec.page_size_bytes, dtype=torch.int8)
|
||||
igroup = AttentionGroup(
|
||||
backend=_BrokenIndexerBackend,
|
||||
layer_names=["idx"],
|
||||
kv_cache_spec=ispec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
try:
|
||||
set_kv_cache_layout("HND")
|
||||
with pytest.raises(AssertionError):
|
||||
_reshape_kv_cache([igroup], {"idx": iraw}, "auto", [BLOCK_SIZE], {})
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
|
||||
def test_padded_main_cache_is_flagged():
|
||||
"""AC-2.1 negative: the M3 main cache relies on the allocator's
|
||||
@@ -1460,7 +1398,7 @@ def test_padded_main_cache_is_flagged():
|
||||
|
||||
try:
|
||||
set_kv_cache_layout("HND")
|
||||
stride_order = MiniMaxM3SparseBackend.get_kv_cache_stride_order()
|
||||
stride_order = resolve_kv_cache_layout().layer_stride_order
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
"""
|
||||
Standalone unit tests for trtllm_prefill_attn_kvfp8_dequant.
|
||||
|
||||
Tests both contiguous and non-contiguous (cross-layer unified) KV cache
|
||||
layouts against a pure-PyTorch reference implementation.
|
||||
Tests KV cache layouts against a pure-PyTorch reference implementation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.v1.kv_cache_interface import KVCacheLayout
|
||||
|
||||
if current_platform.is_rocm():
|
||||
pytest.skip(
|
||||
@@ -34,51 +34,32 @@ def to_float8(x, dtype=None):
|
||||
return x_scl_sat.to(dtype), scale.float().reciprocal()
|
||||
|
||||
|
||||
def make_contiguous_kv_cache(num_blocks, num_kv_heads, block_size, head_size):
|
||||
"""Create a standard contiguous fp8 KV cache (HND layout)."""
|
||||
raw = torch.randn(
|
||||
num_blocks,
|
||||
2,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
head_size,
|
||||
dtype=torch.bfloat16,
|
||||
device="cuda",
|
||||
)
|
||||
kv_cache, scale = to_float8(raw)
|
||||
return kv_cache, scale
|
||||
|
||||
|
||||
def make_cross_layer_kv_cache(
|
||||
num_blocks,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
head_size,
|
||||
num_layers=4,
|
||||
def make_random_kv_cache(
|
||||
num_blocks, num_kv_heads, block_size, head_size, layout=KVCacheLayout.LBHNC
|
||||
):
|
||||
"""
|
||||
Create a non-contiguous per-layer view mimicking cross-layer allocation.
|
||||
"""Create a random fp8 KV cache in 5D ``(B, 2, H, N, hs)`` format.
|
||||
|
||||
Physical layout: (num_blocks, 2, num_kv_heads, num_layers, block_size, head_size)
|
||||
Returned view: (num_blocks, 2, num_kv_heads, block_size, head_size)
|
||||
with non-contiguous strides on dims 0, 1, 2 (they skip over num_layers).
|
||||
The cache is allocated in the physical 5D layout, then one logical layer
|
||||
is selected and reshaped. Cross-layer layouts therefore retain their
|
||||
inter-layer stride gaps, matching the actual forward path.
|
||||
"""
|
||||
raw = torch.randn(
|
||||
logical_4d = (num_blocks, num_kv_heads, block_size, 2 * head_size)
|
||||
num_layers = 1 if layout.is_layer_compact else 2
|
||||
logical_5d = (num_layers, *logical_4d)
|
||||
physical_5d = tuple(logical_5d[i] for i in layout.stride_order)
|
||||
inv_order = [layout.stride_order.index(i) for i in range(5)]
|
||||
|
||||
raw_phys = torch.randn(*physical_5d, dtype=torch.bfloat16, device="cuda")
|
||||
fp8_phys, scale = to_float8(raw_phys)
|
||||
fp8_4d = fp8_phys.permute(*inv_order)[0]
|
||||
kv_5d = fp8_4d.view(
|
||||
num_blocks,
|
||||
2,
|
||||
num_kv_heads,
|
||||
num_layers,
|
||||
block_size,
|
||||
2,
|
||||
head_size,
|
||||
dtype=torch.bfloat16,
|
||||
device="cuda",
|
||||
)
|
||||
fp8_full, scale = to_float8(raw)
|
||||
layer_view = fp8_full[:, :, :, 0, :, :]
|
||||
assert not layer_view.is_contiguous(), (
|
||||
f"Expected non-contiguous view, got strides {layer_view.stride()}"
|
||||
)
|
||||
return layer_view, scale
|
||||
).permute(0, 3, 1, 2, 4)
|
||||
return kv_5d, scale
|
||||
|
||||
|
||||
def ref_dequant(kv_cache, block_tables, k_scale, v_scale, dequant_dtype):
|
||||
@@ -114,7 +95,7 @@ def ref_dequant(kv_cache, block_tables, k_scale, v_scale, dequant_dtype):
|
||||
@pytest.mark.parametrize("block_size", [16, 32])
|
||||
@pytest.mark.parametrize("batch_size", [1, 4])
|
||||
@pytest.mark.parametrize("num_pages_per_seq", [3, 8])
|
||||
@pytest.mark.parametrize("contiguous", [True, False])
|
||||
@pytest.mark.parametrize("layout", list(KVCacheLayout))
|
||||
@torch.inference_mode()
|
||||
def test_trtllm_kvfp8_dequant(
|
||||
num_kv_heads: int,
|
||||
@@ -122,7 +103,7 @@ def test_trtllm_kvfp8_dequant(
|
||||
block_size: int,
|
||||
batch_size: int,
|
||||
num_pages_per_seq: int,
|
||||
contiguous: bool,
|
||||
layout: KVCacheLayout,
|
||||
):
|
||||
from vllm.v1.attention.backends.flashinfer import (
|
||||
trtllm_prefill_attn_kvfp8_dequant,
|
||||
@@ -130,20 +111,13 @@ def test_trtllm_kvfp8_dequant(
|
||||
|
||||
torch.set_default_device("cuda")
|
||||
|
||||
if contiguous:
|
||||
kv_cache, scale = make_contiguous_kv_cache(
|
||||
NUM_BLOCKS,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
head_size,
|
||||
)
|
||||
else:
|
||||
kv_cache, scale = make_cross_layer_kv_cache(
|
||||
NUM_BLOCKS,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
head_size,
|
||||
)
|
||||
kv_cache, scale = make_random_kv_cache(
|
||||
NUM_BLOCKS,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
head_size,
|
||||
layout=layout,
|
||||
)
|
||||
|
||||
k_scale = scale.clone()
|
||||
v_scale = scale.clone()
|
||||
@@ -187,7 +161,7 @@ def test_block_tables_with_zero_pages():
|
||||
torch.set_default_device("cuda")
|
||||
num_kv_heads, block_size, head_size = 8, 16, 64
|
||||
|
||||
kv_cache, scale = make_contiguous_kv_cache(
|
||||
kv_cache, scale = make_random_kv_cache(
|
||||
NUM_BLOCKS,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
@@ -234,7 +208,7 @@ def test_all_zero_block_tables():
|
||||
torch.set_default_device("cuda")
|
||||
num_kv_heads, block_size, head_size = 4, 16, 64
|
||||
|
||||
kv_cache, scale = make_contiguous_kv_cache(
|
||||
kv_cache, scale = make_random_kv_cache(
|
||||
NUM_BLOCKS,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
@@ -266,7 +240,7 @@ def test_different_k_v_scales():
|
||||
torch.set_default_device("cuda")
|
||||
num_kv_heads, block_size, head_size = 8, 16, 64
|
||||
|
||||
kv_cache, _ = make_contiguous_kv_cache(
|
||||
kv_cache, _ = make_random_kv_cache(
|
||||
NUM_BLOCKS,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
@@ -299,7 +273,7 @@ def test_single_page_per_seq():
|
||||
torch.set_default_device("cuda")
|
||||
num_kv_heads, block_size, head_size = 8, 16, 128
|
||||
|
||||
kv_cache, scale = make_contiguous_kv_cache(
|
||||
kv_cache, scale = make_random_kv_cache(
|
||||
NUM_BLOCKS,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
@@ -332,7 +306,7 @@ def test_large_page_indices():
|
||||
num_kv_heads, block_size, head_size = 8, 16, 128
|
||||
large_num_blocks = 32768
|
||||
|
||||
kv_cache, scale = make_contiguous_kv_cache(
|
||||
kv_cache, scale = make_random_kv_cache(
|
||||
large_num_blocks,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
@@ -369,7 +343,7 @@ def test_large_block_size():
|
||||
torch.set_default_device("cuda")
|
||||
num_kv_heads, block_size, head_size = 4, 64, 128
|
||||
|
||||
kv_cache, scale = make_contiguous_kv_cache(
|
||||
kv_cache, scale = make_random_kv_cache(
|
||||
NUM_BLOCKS,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
@@ -395,46 +369,3 @@ def test_large_block_size():
|
||||
ref = ref_dequant(kv_cache, block_tables, k_scale, v_scale, torch.bfloat16)
|
||||
|
||||
torch.testing.assert_close(mock_kv_cache[1:], ref[1:], atol=1e-3, rtol=1e-3)
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_cross_layer_many_layers():
|
||||
"""
|
||||
Non-contiguous with 36 layers -- matches real gpt-oss-120b.
|
||||
Strides are far from contiguous (factor of 36 in the gaps).
|
||||
"""
|
||||
from vllm.v1.attention.backends.flashinfer import (
|
||||
trtllm_prefill_attn_kvfp8_dequant,
|
||||
)
|
||||
|
||||
torch.set_default_device("cuda")
|
||||
num_kv_heads, block_size, head_size = 8, 16, 64
|
||||
num_layers = 36
|
||||
|
||||
kv_cache, scale = make_cross_layer_kv_cache(
|
||||
NUM_BLOCKS,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
head_size,
|
||||
num_layers=num_layers,
|
||||
)
|
||||
k_scale = v_scale = scale.clone()
|
||||
|
||||
block_tables = torch.randint(
|
||||
1,
|
||||
NUM_BLOCKS,
|
||||
(4, 6),
|
||||
dtype=torch.int32,
|
||||
device="cuda",
|
||||
)
|
||||
|
||||
mock_kv_cache, _ = trtllm_prefill_attn_kvfp8_dequant(
|
||||
kv_cache,
|
||||
block_tables,
|
||||
k_scale,
|
||||
v_scale,
|
||||
torch.bfloat16,
|
||||
)
|
||||
ref = ref_dequant(kv_cache, block_tables, k_scale, v_scale, torch.bfloat16)
|
||||
|
||||
torch.testing.assert_close(mock_kv_cache[1:], ref[1:], atol=1e-3, rtol=1e-3)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.v1.attention.backends.mla.xpu_mla_sparse import XPUMLASparseImpl
|
||||
from vllm.v1.attention.ops.xpu_mla_sparse import triton_bf16_mla_sparse_interface
|
||||
|
||||
|
||||
@@ -75,6 +76,39 @@ def reference_mla_sparse_prefill(
|
||||
return (out.to(kv.dtype), out, max_logits, orig_lse)
|
||||
|
||||
|
||||
def test_xpu_sparse_backend_flattens_standard_cache_to_three_dims(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_sparse_interface(q, kv, indices, sm_scale):
|
||||
captured["kv"] = kv
|
||||
output = torch.zeros(q.shape[0], q.shape[1], 512, dtype=q.dtype)
|
||||
return output, None, None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm.v1.attention.backends.mla.xpu_mla_sparse."
|
||||
"triton_bf16_mla_sparse_interface",
|
||||
fake_sparse_interface,
|
||||
)
|
||||
impl = type("StubImpl", (), {"num_heads": 4, "softmax_scale": 1.0})()
|
||||
q = torch.zeros(2, 4, 576, dtype=torch.bfloat16)
|
||||
kv_cache = (
|
||||
torch.arange(3 * 8 * 576, dtype=torch.int32)
|
||||
.remainder(127)
|
||||
.to(torch.bfloat16)
|
||||
.view(3, 8, 576)
|
||||
)
|
||||
topk_indices = torch.zeros(2, 128, dtype=torch.int32)
|
||||
|
||||
output = XPUMLASparseImpl._forward_bf16_kv(
|
||||
impl, q, kv_cache, topk_indices, attn_metadata=None
|
||||
)
|
||||
|
||||
expected_kv = kv_cache.reshape(24, 1, 576)
|
||||
assert captured["kv"].shape == expected_kv.shape
|
||||
assert torch.equal(captured["kv"], expected_kv)
|
||||
assert output.shape == (2, 4, 512)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_str", ["xpu"])
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
||||
@pytest.mark.skipif(
|
||||
|
||||
@@ -38,11 +38,6 @@ class CustomAttentionBackend(AttentionBackend):
|
||||
"""Mock builder class."""
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_required_kv_cache_layout():
|
||||
"""Mock KV cache layout."""
|
||||
return None
|
||||
|
||||
|
||||
class CustomMambaAttentionImpl(AttentionImpl):
|
||||
"""Mock custom mamba attention implementation for testing."""
|
||||
@@ -71,11 +66,6 @@ class CustomMambaAttentionBackend(AttentionBackend):
|
||||
"""Mock builder class."""
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_required_kv_cache_layout():
|
||||
"""Mock KV cache layout."""
|
||||
return None
|
||||
|
||||
|
||||
def test_custom_is_not_alias_of_any_backend():
|
||||
# Get all members of AttentionBackendEnum
|
||||
|
||||
@@ -32,16 +32,16 @@ from vllm.v1.attention.backend import (
|
||||
)
|
||||
from vllm.v1.attention.backends.registry import AttentionBackendEnum
|
||||
from vllm.v1.attention.backends.utils import (
|
||||
resolve_kv_cache_layout,
|
||||
set_kv_cache_layout,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheLayout
|
||||
|
||||
BACKENDS_TO_TEST = [
|
||||
AttentionBackendEnum.FLASH_ATTN,
|
||||
AttentionBackendEnum.FLASHINFER,
|
||||
AttentionBackendEnum.FLEX_ATTENTION,
|
||||
AttentionBackendEnum.TRITON_ATTN,
|
||||
"FLEX_ATTENTION_SLOW",
|
||||
]
|
||||
|
||||
DEVICE_TYPE = current_platform.device_type
|
||||
@@ -115,27 +115,19 @@ def create_and_prepopulate_kv_cache(
|
||||
device: torch.device,
|
||||
num_blocks: int,
|
||||
common_attn_metadata: CommonAttentionMetadata,
|
||||
layout: KVCacheLayout,
|
||||
randomize_blocks: bool = True,
|
||||
kv_cache_dtype: str = "auto",
|
||||
) -> torch.Tensor:
|
||||
"""Create and prepopulate a KV cache with context data.
|
||||
|
||||
Args:
|
||||
k_contexts: List of key context tensors for each sequence
|
||||
v_contexts: List of value context tensors for each sequence
|
||||
seq_lens: List of sequence lengths
|
||||
block_size: Size of each block
|
||||
num_kv_heads: Number of KV heads
|
||||
head_size: Size of each head
|
||||
dtype: Data type for the cache
|
||||
device: Device to create the cache on
|
||||
num_blocks: Total number of blocks in the cache
|
||||
block_table: Block table tensor to populate
|
||||
randomize_blocks: Whether to randomly permute blocks
|
||||
or use sequential order
|
||||
Mirrors production's ``reshape_kv_cache``: allocates a flat buffer in
|
||||
the physical order dictated by *layout*, then permutes to the logical
|
||||
``[B, H, N, C]`` shape that every backend expects.
|
||||
|
||||
Returns:
|
||||
Tuple of (kv_cache, updated_block_table)
|
||||
A 4D tensor in logical ``(num_blocks, num_kv_heads, block_size,
|
||||
2 * head_size)`` order with strides determined by *layout*.
|
||||
"""
|
||||
batch_size = len(k_contexts)
|
||||
seq_lens = common_attn_metadata.seq_lens.cpu()
|
||||
@@ -152,41 +144,43 @@ def create_and_prepopulate_kv_cache(
|
||||
fp8_kv_cache = is_quantized_kv_cache(kv_cache_dtype)
|
||||
storage_dtype = FP8_KV_CACHE_DTYPES[kv_cache_dtype] if fp8_kv_cache else dtype
|
||||
|
||||
kv_cache = torch.zeros(
|
||||
num_blocks,
|
||||
block_size,
|
||||
num_kv_heads,
|
||||
2 * head_size,
|
||||
dtype=storage_dtype,
|
||||
device=device,
|
||||
)
|
||||
kv_cache_flat = kv_cache.view(-1, num_kv_heads, 2 * head_size)
|
||||
# --- allocate ---------------------------------------------------------
|
||||
# Logical 5D shape is always [L, B, H, N, C]. Cross-layer layouts need
|
||||
# at least two layers to reproduce the inter-layer gaps in a layer view.
|
||||
logical_4d = (num_blocks, num_kv_heads, block_size, 2 * head_size)
|
||||
num_layers = 1 if layout.is_layer_compact else 2
|
||||
logical_5d = (num_layers, *logical_4d)
|
||||
physical_5d = tuple(logical_5d[i] for i in layout.stride_order)
|
||||
inv_order = [layout.stride_order.index(i) for i in range(5)]
|
||||
|
||||
# Populate the cache with the context tokens
|
||||
# Start from block_id=1 since block_id=0 is considered the null block
|
||||
start_block_idx = 1
|
||||
kv_cache_physical = torch.zeros(physical_5d, dtype=storage_dtype, device=device)
|
||||
# Permute to logical [L, B, H, N, C], then select a layer. This mirrors
|
||||
# reshape_kv_cache and retains cross-layer strides in the 4D view.
|
||||
kv_cache = kv_cache_physical.permute(*inv_order)[0]
|
||||
|
||||
# --- populate ---------------------------------------------------------
|
||||
# Write context tokens into the cache via the logical view:
|
||||
# kv_cache[block, :, token_in_block, :] routes correctly regardless
|
||||
# of physical layout.
|
||||
start_block_idx = 1 # block 0 is the null block
|
||||
for i in range(batch_size):
|
||||
k_context, v_context = k_contexts[i], v_contexts[i]
|
||||
start = start_block_idx * block_size
|
||||
end = start + k_context.shape[0]
|
||||
kv_cache_flat[start:end, :, :head_size] = k_context
|
||||
kv_cache_flat[start:end, :, head_size:] = v_context
|
||||
|
||||
# Stay block aligned and allocate enough blocks for the new tokens
|
||||
for t in range(k_context.shape[0]):
|
||||
blk = start_block_idx + t // block_size
|
||||
off = t % block_size
|
||||
kv_cache[blk, :, off, :head_size] = k_context[t]
|
||||
kv_cache[blk, :, off, head_size:] = v_context[t]
|
||||
start_block_idx += cdiv(int(seq_lens[i]), block_size)
|
||||
|
||||
blocks_end = start_block_idx
|
||||
|
||||
# Permute the context blocks (excluding block 0 which is null)
|
||||
if randomize_blocks:
|
||||
# Random permutation starting from block 1
|
||||
perm = torch.randperm(blocks_end - 1) + 1
|
||||
else:
|
||||
# Sequential order starting from block 1
|
||||
perm = torch.arange(1, blocks_end)
|
||||
|
||||
inv_perm = torch.zeros(blocks_end, dtype=torch.long, device=device)
|
||||
# Add 1 to account for starting from block 1
|
||||
inv_perm[1:] = torch.argsort(perm) + 1
|
||||
kv_cache[1:blocks_end, ...] = kv_cache[perm, ...]
|
||||
|
||||
@@ -211,9 +205,6 @@ def create_and_prepopulate_kv_cache(
|
||||
i, block_indices
|
||||
] * block_size + token_inter_block_offsets.to(device)
|
||||
|
||||
# Transpose to logical (num_blocks, num_kv_heads, block_size, 2*hs)
|
||||
kv_cache = kv_cache.transpose(1, 2).contiguous()
|
||||
|
||||
if fp8_kv_cache:
|
||||
kv_cache = kv_cache.view(torch.uint8)
|
||||
|
||||
@@ -250,18 +241,14 @@ def run_attention_backend(
|
||||
) -> torch.Tensor:
|
||||
"""Run attention computation using the specified backend's AttentionImpl."""
|
||||
|
||||
# Handle special case for FLEX_ATTENTION_SLOW
|
||||
actual_backend = backend
|
||||
use_direct_block_mask = not current_platform.is_rocm() and is_torch_equal_or_newer(
|
||||
"2.9.0.dev0"
|
||||
)
|
||||
|
||||
use_direct_block_mask = is_torch_equal_or_newer("2.9.0.dev0")
|
||||
if backend == "FLEX_ATTENTION_SLOW":
|
||||
actual_backend = AttentionBackendEnum.FLEX_ATTENTION
|
||||
use_direct_block_mask = False
|
||||
|
||||
builder_cls, impl_cls = try_get_attention_backend(actual_backend)
|
||||
builder_cls, impl_cls = try_get_attention_backend(backend)
|
||||
|
||||
# Mock flashinfer's get_per_layer_parameters if needed
|
||||
if actual_backend == AttentionBackendEnum.FLASHINFER:
|
||||
if backend == AttentionBackendEnum.FLASHINFER:
|
||||
import unittest.mock
|
||||
|
||||
from vllm.v1.attention.backends.utils import PerLayerParameters
|
||||
@@ -290,7 +277,7 @@ def run_attention_backend(
|
||||
else:
|
||||
# Build metadata
|
||||
builder = builder_cls(kv_cache_spec, layer_names, vllm_config, device)
|
||||
if actual_backend == AttentionBackendEnum.FLEX_ATTENTION:
|
||||
if backend == AttentionBackendEnum.FLEX_ATTENTION:
|
||||
builder.direct_build = use_direct_block_mask
|
||||
attn_metadata = builder.build(
|
||||
common_prefix_len=0,
|
||||
@@ -327,7 +314,7 @@ def run_attention_backend(
|
||||
# Run forward pass
|
||||
# NOTE: The query, key, and value are already shaped correctly
|
||||
# in the calling test function.
|
||||
if not try_backend_includes_kv_cache_update(actual_backend):
|
||||
if not try_backend_includes_kv_cache_update(backend):
|
||||
impl.do_kv_cache_update(
|
||||
mock_layer, key, value, kv_cache, attn_metadata.slot_mapping
|
||||
)
|
||||
@@ -341,7 +328,7 @@ def run_attention_backend(
|
||||
def _test_backend_correctness(
|
||||
batch_spec: BatchSpec,
|
||||
model: str,
|
||||
backend_to_test: list[AttentionBackendEnum | str],
|
||||
backend_to_test: list[AttentionBackendEnum],
|
||||
mask_mod,
|
||||
*,
|
||||
causal: bool = True,
|
||||
@@ -502,6 +489,8 @@ def _test_backend_correctness(
|
||||
common_attn_metadata.causal = causal
|
||||
|
||||
# 3. Simulate Paged KV Cache and a realistic slot_mapping
|
||||
attn_backends = tuple(backend.get_class() for backend in backend_to_test)
|
||||
layout = resolve_kv_cache_layout(attn_backends)
|
||||
kv_cache = create_and_prepopulate_kv_cache(
|
||||
k_contexts=k_contexts,
|
||||
v_contexts=v_contexts,
|
||||
@@ -512,6 +501,7 @@ def _test_backend_correctness(
|
||||
device=device,
|
||||
num_blocks=vllm_config.cache_config.num_gpu_blocks or 1000,
|
||||
common_attn_metadata=common_attn_metadata,
|
||||
layout=layout,
|
||||
randomize_blocks=True,
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
)
|
||||
@@ -520,41 +510,18 @@ def _test_backend_correctness(
|
||||
# Note: flex_attention has known Triton kernel compatibility issues
|
||||
# with test infrastructures
|
||||
for backend_name in backend_to_test:
|
||||
reset_kv_cache_layout = False
|
||||
|
||||
# Resolve backend class for both enum and string names.
|
||||
actual_backend = backend_name
|
||||
if backend_name == "FLEX_ATTENTION_SLOW":
|
||||
actual_backend = AttentionBackendEnum.FLEX_ATTENTION
|
||||
if hasattr(actual_backend, "get_class"):
|
||||
backend_cls = actual_backend.get_class()
|
||||
else:
|
||||
backend_cls = None
|
||||
backend_cls = backend_name.get_class()
|
||||
|
||||
if is_quantized_kv_cache(kv_cache_dtype) and (
|
||||
backend_cls is None
|
||||
or not backend_cls.supports_kv_cache_dtype(kv_cache_dtype)
|
||||
not backend_cls.supports_kv_cache_dtype(kv_cache_dtype)
|
||||
):
|
||||
continue
|
||||
|
||||
if backend_name == AttentionBackendEnum.FLASHINFER:
|
||||
set_kv_cache_layout("HND")
|
||||
reset_kv_cache_layout = True
|
||||
|
||||
kv_cache_for_backend = kv_cache
|
||||
if backend_cls is not None:
|
||||
try:
|
||||
stride_order = backend_cls.get_kv_cache_stride_order()
|
||||
except (AttributeError, NotImplementedError):
|
||||
stride_order = tuple(range(kv_cache.ndim))
|
||||
if stride_order != tuple(range(kv_cache.ndim)):
|
||||
# Apply stride order like runtime does in
|
||||
# _reshape_kv_cache (attn_utils.py:182-210): permute to physical
|
||||
# layout, make contiguous, then permute to logical layout.
|
||||
inv_order = [stride_order.index(i) for i in range(len(stride_order))]
|
||||
kv_cache_for_backend = (
|
||||
kv_cache.permute(*stride_order).contiguous().permute(*inv_order)
|
||||
)
|
||||
|
||||
# FlashInfer reads the layout at plan time; override to match
|
||||
# the physical order of the test cache.
|
||||
set_kv_cache_layout(layout.name)
|
||||
|
||||
try:
|
||||
backend_output = run_attention_backend(
|
||||
@@ -573,8 +540,7 @@ def _test_backend_correctness(
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
)
|
||||
finally:
|
||||
if reset_kv_cache_layout:
|
||||
set_kv_cache_layout(None)
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
# Check shape and dtype consistency
|
||||
assert backend_output.shape == sdpa_output.shape, (
|
||||
@@ -603,6 +569,41 @@ def _test_backend_correctness(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layout", ["BLHNC", "BHLNC"])
|
||||
@pytest.mark.parametrize("batch_spec_name", ["small_decode", "small_prefill"])
|
||||
@pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8"])
|
||||
def test_flashinfer_cross_layer_layout(
|
||||
default_vllm_config,
|
||||
layout: str,
|
||||
batch_spec_name: str,
|
||||
kv_cache_dtype: str,
|
||||
):
|
||||
if AttentionBackendEnum.FLASHINFER not in BACKENDS_TO_TEST:
|
||||
pytest.skip("FlashInfer is not installed")
|
||||
|
||||
def causal_mask_mod(
|
||||
b: torch.Tensor,
|
||||
h: torch.Tensor,
|
||||
q_idx: torch.Tensor,
|
||||
kv_idx: torch.Tensor,
|
||||
*,
|
||||
context_len: int,
|
||||
):
|
||||
return (q_idx + context_len) >= kv_idx
|
||||
|
||||
set_kv_cache_layout(layout)
|
||||
try:
|
||||
_test_backend_correctness(
|
||||
batch_spec=BATCH_SPECS[batch_spec_name],
|
||||
model="meta-llama/Meta-Llama-3-8B",
|
||||
backend_to_test=[AttentionBackendEnum.FLASHINFER],
|
||||
mask_mod=causal_mask_mod,
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
)
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"batch_spec_name",
|
||||
[
|
||||
@@ -827,14 +828,12 @@ if current_platform.is_rocm():
|
||||
SLIDING_WINDOW_BACKENDS_TO_TEST = [
|
||||
AttentionBackendEnum.FLEX_ATTENTION,
|
||||
AttentionBackendEnum.TRITON_ATTN,
|
||||
"FLEX_ATTENTION_SLOW",
|
||||
]
|
||||
else:
|
||||
SLIDING_WINDOW_BACKENDS_TO_TEST = [
|
||||
AttentionBackendEnum.FLASH_ATTN,
|
||||
AttentionBackendEnum.FLEX_ATTENTION,
|
||||
AttentionBackendEnum.TRITON_ATTN,
|
||||
"FLEX_ATTENTION_SLOW",
|
||||
]
|
||||
|
||||
|
||||
@@ -852,7 +851,10 @@ else:
|
||||
@pytest.mark.parametrize("model", ["microsoft/Phi-tiny-MoE-instruct"])
|
||||
@pytest.mark.parametrize("tensor_parallel_size", [1, 2, 4])
|
||||
def test_sliding_window_backend_correctness(
|
||||
default_vllm_config, batch_spec_name: str, model: str, tensor_parallel_size: int
|
||||
default_vllm_config,
|
||||
batch_spec_name: str,
|
||||
model: str,
|
||||
tensor_parallel_size: int,
|
||||
):
|
||||
"""Test backend's correctness with sliding window attention."""
|
||||
|
||||
@@ -914,7 +916,10 @@ def test_sliding_window_backend_correctness(
|
||||
@pytest.mark.parametrize("model", ["google/embeddinggemma-300m"])
|
||||
@pytest.mark.parametrize("tensor_parallel_size", [1, 2])
|
||||
def test_sliding_window_encoder_backend_correctness(
|
||||
default_vllm_config, batch_spec_name: str, model: str, tensor_parallel_size: int
|
||||
default_vllm_config,
|
||||
batch_spec_name: str,
|
||||
model: str,
|
||||
tensor_parallel_size: int,
|
||||
):
|
||||
"""Test backend's correctness with sliding window attention."""
|
||||
|
||||
@@ -950,7 +955,6 @@ def test_sliding_window_encoder_backend_correctness(
|
||||
NON_CAUSAL_BACKENDS_TO_TEST = [
|
||||
AttentionBackendEnum.FLASH_ATTN,
|
||||
AttentionBackendEnum.FLEX_ATTENTION,
|
||||
"FLEX_ATTENTION_SLOW",
|
||||
]
|
||||
|
||||
if current_platform.is_rocm():
|
||||
@@ -971,7 +975,9 @@ if current_platform.is_rocm():
|
||||
)
|
||||
@pytest.mark.parametrize("model", ["meta-llama/Meta-Llama-3-8B"])
|
||||
def test_non_causal_backend_correctness(
|
||||
default_vllm_config, batch_spec_name: str, model: str
|
||||
default_vllm_config,
|
||||
batch_spec_name: str,
|
||||
model: str,
|
||||
):
|
||||
"""Test backend's correctness with non-causal (bidirectional) decoder
|
||||
attention, as used by DFlash speculative decoding."""
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.v1.attention.backends.cpu_attn import _split_cpu_kv_cache
|
||||
from vllm.v1.attention.backends.utils import set_kv_cache_layout
|
||||
from vllm.v1.kv_cache_interface import KVCacheLayout
|
||||
|
||||
|
||||
def _make_cache_with_layout(layout: KVCacheLayout) -> torch.Tensor:
|
||||
logical_shape = (2, 3, 2, 4, 10)
|
||||
physical_shape = tuple(logical_shape[i] for i in layout.stride_order)
|
||||
physical = torch.arange(math.prod(logical_shape)).view(physical_shape)
|
||||
inverse_order = tuple(layout.stride_order.index(i) for i in range(5))
|
||||
return physical.permute(*inverse_order)[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"layout", [KVCacheLayout.LBHNC, KVCacheLayout.BLHNC, KVCacheLayout.BHLNC]
|
||||
)
|
||||
def test_split_cpu_kv_cache_supports_hnd_layouts(layout: KVCacheLayout):
|
||||
set_kv_cache_layout(layout.name)
|
||||
try:
|
||||
kv_cache = _make_cache_with_layout(layout)
|
||||
key_cache, value_cache = _split_cpu_kv_cache(kv_cache)
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
assert key_cache.shape == value_cache.shape == (3, 2, 4, 5)
|
||||
assert key_cache.stride() == value_cache.stride()
|
||||
assert key_cache.stride(-2) == 5
|
||||
assert value_cache.storage_offset() - key_cache.storage_offset() == 20
|
||||
|
||||
|
||||
def test_split_cpu_kv_cache_rejects_nhd_layout():
|
||||
set_kv_cache_layout(KVCacheLayout.LBNHC.name)
|
||||
try:
|
||||
kv_cache = _make_cache_with_layout(KVCacheLayout.LBNHC)
|
||||
with pytest.raises(ValueError, match="does not support KV cache layout LBNHC"):
|
||||
_split_cpu_kv_cache(kv_cache)
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
|
||||
def test_split_cpu_kv_cache_rejects_incompatible_strides():
|
||||
set_kv_cache_layout(KVCacheLayout.LBHNC.name)
|
||||
try:
|
||||
kv_cache = torch.empty(3, 4, 2, 10).transpose(1, 2)
|
||||
with pytest.raises(ValueError, match="contiguous token and content"):
|
||||
_split_cpu_kv_cache(kv_cache)
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
@@ -17,13 +17,13 @@ def test_indexer_builder_deepseek_v4_compressed_slot_mapping_uses_storage_block_
|
||||
"""
|
||||
device = torch.device("cuda")
|
||||
|
||||
# storage_block_size = block_size // compress_ratio = 256 // 4 = 64
|
||||
# storage_block_size = block_size // tokens_per_state = 256 // 4 = 64
|
||||
kv_cache_spec = MLAAttentionSpec(
|
||||
block_size=256,
|
||||
num_kv_heads=1,
|
||||
head_size=128,
|
||||
dtype=torch.bfloat16,
|
||||
compress_ratio=4,
|
||||
tokens_per_state=4,
|
||||
)
|
||||
vllm_config = create_vllm_config(max_model_len=1024)
|
||||
builder = DeepseekV32IndexerMetadataBuilder(
|
||||
|
||||
@@ -279,8 +279,9 @@ def create_and_prepopulate_kv_cache(
|
||||
else:
|
||||
kv_entry_size = head_size
|
||||
|
||||
# Create MLA KV cache: (num_blocks, num_heads=1, block_size, kv_entry_size)
|
||||
kv_cache = torch.zeros(
|
||||
num_blocks, block_size, kv_entry_size, dtype=torch.uint8, device=device
|
||||
num_blocks, 1, block_size, kv_entry_size, dtype=torch.uint8, device=device
|
||||
)
|
||||
scale_tensor = (
|
||||
scale
|
||||
@@ -289,9 +290,9 @@ def create_and_prepopulate_kv_cache(
|
||||
)
|
||||
scale_tensor = scale_tensor.to(device=device, dtype=torch.float32)
|
||||
else:
|
||||
# Create MLA KV cache: (num_blocks, block_size, head_size)
|
||||
# Create MLA KV cache: (num_blocks, num_heads=1, block_size, head_size)
|
||||
kv_cache = torch.zeros(
|
||||
num_blocks, block_size, head_size, dtype=dtype, device=device
|
||||
num_blocks, 1, block_size, head_size, dtype=dtype, device=device
|
||||
)
|
||||
kv_cache_flat = kv_cache.view(-1, head_size)
|
||||
|
||||
@@ -312,7 +313,7 @@ def create_and_prepopulate_kv_cache(
|
||||
ops.concat_and_cache_mla(
|
||||
kv_c_context,
|
||||
k_pe_context.squeeze(1),
|
||||
kv_cache,
|
||||
kv_cache.squeeze(1),
|
||||
slots,
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
scale=scale_tensor,
|
||||
@@ -435,7 +436,7 @@ class MockSparseMLAAttentionLayer:
|
||||
ops.concat_and_cache_mla(
|
||||
kv_c,
|
||||
k_pe.squeeze(1),
|
||||
kv_cache,
|
||||
kv_cache.squeeze(1),
|
||||
attn_metadata.slot_mapping.flatten(),
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
scale=self._k_scale,
|
||||
@@ -571,7 +572,7 @@ class MockMLAAttentionLayer(MLAAttention):
|
||||
ops.concat_and_cache_mla(
|
||||
kv_c,
|
||||
k_pe.squeeze(1),
|
||||
kv_cache,
|
||||
kv_cache.squeeze(1),
|
||||
attn_metadata.slot_mapping.flatten(),
|
||||
kv_cache_dtype=kv_cache_dtype,
|
||||
scale=self._k_scale,
|
||||
|
||||
@@ -54,7 +54,7 @@ def mock_on_mi3xx():
|
||||
(
|
||||
{},
|
||||
None,
|
||||
AttentionBackendEnum.ROCM_ATTN.get_path(),
|
||||
AttentionBackendEnum.ROCM_AITER_UNIFIED_ATTN.get_path(),
|
||||
),
|
||||
# Test Case 2: Explicit TRITON_ATTN backend
|
||||
(
|
||||
@@ -66,7 +66,7 @@ def mock_on_mi3xx():
|
||||
(
|
||||
{},
|
||||
"ROCM_ATTN",
|
||||
AttentionBackendEnum.ROCM_ATTN.get_path(),
|
||||
None,
|
||||
),
|
||||
# Test Case 4: Explicit ROCM_AITER_FA backend
|
||||
(
|
||||
@@ -84,7 +84,7 @@ def mock_on_mi3xx():
|
||||
(
|
||||
{"VLLM_ROCM_USE_AITER": "1"},
|
||||
None,
|
||||
AttentionBackendEnum.ROCM_ATTN.get_path(),
|
||||
AttentionBackendEnum.ROCM_AITER_UNIFIED_ATTN.get_path(),
|
||||
),
|
||||
# Test Case 7: VLLM_ROCM_USE_AITER=1 + explicit TRITON_ATTN
|
||||
(
|
||||
@@ -96,13 +96,13 @@ def mock_on_mi3xx():
|
||||
(
|
||||
{"VLLM_ROCM_USE_AITER": "1", "VLLM_ROCM_USE_AITER_MHA": "0"},
|
||||
None,
|
||||
AttentionBackendEnum.ROCM_ATTN.get_path(),
|
||||
AttentionBackendEnum.ROCM_AITER_UNIFIED_ATTN.get_path(),
|
||||
),
|
||||
# Test Case 9: VLLM_ROCM_USE_AITER=1 + explicit ROCM_ATTN
|
||||
(
|
||||
{"VLLM_ROCM_USE_AITER": "1"},
|
||||
"ROCM_ATTN",
|
||||
AttentionBackendEnum.ROCM_ATTN.get_path(),
|
||||
None,
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -146,6 +146,16 @@ def test_standard_attention_backend_selection(
|
||||
use_sparse=False,
|
||||
)
|
||||
|
||||
if expected_backend_path is None:
|
||||
with pytest.raises(
|
||||
ValueError, match="does not support standardized packed KV caches"
|
||||
):
|
||||
RocmPlatform.get_attn_backend_cls(
|
||||
selected_backend=backend_enum,
|
||||
attn_selector_config=attn_selector_config,
|
||||
)
|
||||
return
|
||||
|
||||
backend_path = RocmPlatform.get_attn_backend_cls(
|
||||
selected_backend=backend_enum, attn_selector_config=attn_selector_config
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import pytest
|
||||
import torch
|
||||
from torch.nn.attention.flex_attention import create_block_mask, flex_attention
|
||||
|
||||
from tests.v1.attention.test_attention_backends import create_and_prepopulate_kv_cache
|
||||
from tests.v1.attention.utils import (
|
||||
BatchSpec,
|
||||
create_common_attn_metadata,
|
||||
@@ -16,14 +17,13 @@ from tests.v1.attention.utils import (
|
||||
)
|
||||
from vllm.config import set_current_vllm_config
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.math_utils import cdiv
|
||||
from vllm.utils.torch_utils import nvfp4_kv_cache_full_dim, set_random_seed
|
||||
from vllm.v1.attention.backends.utils import (
|
||||
PerLayerParameters,
|
||||
get_kv_cache_layout,
|
||||
resolve_kv_cache_layout,
|
||||
set_kv_cache_layout,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVQuantMode
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheLayout, KVQuantMode
|
||||
|
||||
if not current_platform.is_device_capability_family(100):
|
||||
pytest.skip(
|
||||
@@ -87,96 +87,6 @@ def _mock_get_per_layer_parameters(vllm_config, layer_names, impl_cls):
|
||||
}
|
||||
|
||||
|
||||
def _create_hnd_kv_cache(
|
||||
k_contexts,
|
||||
v_contexts,
|
||||
block_size,
|
||||
num_kv_heads,
|
||||
head_size,
|
||||
dtype,
|
||||
device,
|
||||
num_blocks,
|
||||
common_attn_metadata,
|
||||
kv_in_head_dim=False,
|
||||
):
|
||||
"""Create and populate a packed KV cache with HND-compatible strides.
|
||||
|
||||
When kv_in_head_dim=False (default), returns (B, H, N, 2*hs) with K/V
|
||||
packed in the content dim. When kv_in_head_dim=True, returns
|
||||
(B, 2*H, N, hs) with K/V as separate head groups.
|
||||
"""
|
||||
seq_lens = common_attn_metadata.seq_lens.cpu()
|
||||
query_lens = (
|
||||
common_attn_metadata.query_start_loc_cpu[1:]
|
||||
- common_attn_metadata.query_start_loc_cpu[:-1]
|
||||
)
|
||||
block_table = common_attn_metadata.block_table_tensor
|
||||
slot_mapping = common_attn_metadata.slot_mapping
|
||||
batch_size = len(k_contexts)
|
||||
|
||||
# kv_in_head_dim: (B, N, 2*H, hs) — K/V as separate head groups
|
||||
# else: (B, N, H, 2*hs) — K/V packed in content dim
|
||||
n_heads, content = (
|
||||
(2 * num_kv_heads, head_size)
|
||||
if kv_in_head_dim
|
||||
else (num_kv_heads, 2 * head_size)
|
||||
)
|
||||
kv_cache = torch.zeros(
|
||||
num_blocks,
|
||||
block_size,
|
||||
n_heads,
|
||||
content,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
)
|
||||
kv_cache_flat = kv_cache.view(-1, n_heads, content)
|
||||
|
||||
start_block_idx = 1
|
||||
for i in range(batch_size):
|
||||
k_ctx, v_ctx = k_contexts[i], v_contexts[i]
|
||||
start = start_block_idx * block_size
|
||||
end = start + k_ctx.shape[0]
|
||||
if kv_in_head_dim:
|
||||
kv_cache_flat[start:end, :num_kv_heads] = k_ctx
|
||||
kv_cache_flat[start:end, num_kv_heads:] = v_ctx
|
||||
else:
|
||||
kv_cache_flat[start:end, :, :head_size] = k_ctx
|
||||
kv_cache_flat[start:end, :, head_size:] = v_ctx
|
||||
start_block_idx += cdiv(int(seq_lens[i]), block_size)
|
||||
|
||||
blocks_end = start_block_idx
|
||||
|
||||
# Randomly permute blocks (starting from block 1; block 0 is null).
|
||||
perm = torch.randperm(blocks_end - 1) + 1
|
||||
inv_perm = torch.zeros(blocks_end, dtype=torch.long, device=device)
|
||||
inv_perm[1:] = torch.argsort(perm) + 1
|
||||
kv_cache[1:blocks_end] = kv_cache[perm]
|
||||
|
||||
# Build block table.
|
||||
start_block_idx = 1
|
||||
for i in range(batch_size):
|
||||
n_blocks = cdiv(int(seq_lens[i]), block_size)
|
||||
block_table[i, :n_blocks] = inv_perm[
|
||||
start_block_idx : start_block_idx + n_blocks
|
||||
]
|
||||
start_block_idx += n_blocks
|
||||
|
||||
# Build slot mapping that is consistent with the block table.
|
||||
for i in range(batch_size):
|
||||
ctx_len = int(seq_lens[i]) - int(query_lens[i])
|
||||
token_offsets = torch.arange(int(query_lens[i])) + ctx_len
|
||||
block_indices = token_offsets // block_size
|
||||
intra_block_offsets = token_offsets % block_size
|
||||
start = common_attn_metadata.query_start_loc_cpu[i]
|
||||
end = common_attn_metadata.query_start_loc_cpu[i + 1]
|
||||
slot_mapping[start:end] = block_table[
|
||||
i, block_indices
|
||||
] * block_size + intra_block_offsets.to(device)
|
||||
|
||||
# Transpose to canonical: (B, H, N, 2*hs) or (B, 2*H, N, hs)
|
||||
return kv_cache.transpose(1, 2).contiguous()
|
||||
|
||||
|
||||
def _create_nvfp4_hnd_kv_cache(
|
||||
k_contexts,
|
||||
v_contexts,
|
||||
@@ -189,39 +99,15 @@ def _create_nvfp4_hnd_kv_cache(
|
||||
common_attn_metadata,
|
||||
kv_scale_val,
|
||||
):
|
||||
"""Create an nvfp4 KV cache by quantizing bf16 context via
|
||||
reshape_and_cache_flash, using the same block-table layout as
|
||||
_create_hnd_kv_cache.
|
||||
"""Create an nvfp4 KV cache with 2H head layout.
|
||||
|
||||
The returned tensor is dtype ``uint8`` with head-group layout
|
||||
``(num_blocks, 2 * num_kv_heads, block_size, full_dim)``
|
||||
where K heads occupy the first ``num_kv_heads`` heads and V heads the second.
|
||||
Each ``full_dim = head_size // 2 + head_size // 16`` block packs two regions:
|
||||
- **FP4 data** (``head_size // 2`` bytes): pairs of E2M1 values,
|
||||
two per byte.
|
||||
- **FP8 block scales** (``head_size // 16`` bytes): one E4M3
|
||||
scale per 16-element block.
|
||||
|
||||
Args:
|
||||
k_contexts: List of key context tensors, one per sequence.
|
||||
v_contexts: List of value context tensors, one per sequence.
|
||||
block_size: Number of tokens per cache block.
|
||||
num_kv_heads: Number of key/value heads.
|
||||
head_size: Head dimension (must be divisible by 16).
|
||||
dtype: Source data type for the bf16 intermediate cache.
|
||||
device: Target device.
|
||||
num_blocks: Total number of blocks to allocate.
|
||||
common_attn_metadata: Metadata containing block tables and
|
||||
sequence lengths.
|
||||
kv_scale_val: Scalar float used as both k_scale and v_scale
|
||||
during quantization.
|
||||
|
||||
Returns:
|
||||
``torch.Tensor``: The nvfp4 kv_cache tensor (uint8, HND-strided).
|
||||
The returned tensor is dtype ``uint8`` with logical shape
|
||||
``(num_blocks, 2 * num_kv_heads, block_size, full_dim)`` where K occupies
|
||||
the first H heads and V occupies the next H heads, and
|
||||
``full_dim = head_size // 2 + head_size // 16`` packs FP4 data and
|
||||
FP8 block scales per head.
|
||||
"""
|
||||
# First create a bf16 HND cache so block tables are populated.
|
||||
# Use kv_in_head_dim=True so K/V are separate head groups (B, 2*H, N, hs).
|
||||
bf16_cache = _create_hnd_kv_cache(
|
||||
bf16_cache = create_and_prepopulate_kv_cache(
|
||||
k_contexts,
|
||||
v_contexts,
|
||||
block_size,
|
||||
@@ -231,10 +117,9 @@ def _create_nvfp4_hnd_kv_cache(
|
||||
device,
|
||||
num_blocks,
|
||||
common_attn_metadata,
|
||||
kv_in_head_dim=True,
|
||||
layout=KVCacheLayout.LBHNC,
|
||||
)
|
||||
|
||||
# (num_blocks, 2 * num_kv_heads, block_size, full_dim) — K heads first, then V heads
|
||||
full_dim = nvfp4_kv_cache_full_dim(head_size)
|
||||
nvfp4_cache = torch.zeros(
|
||||
(num_blocks, 2 * num_kv_heads, block_size, full_dim),
|
||||
@@ -243,8 +128,6 @@ def _create_nvfp4_hnd_kv_cache(
|
||||
)
|
||||
k_cache, v_cache = nvfp4_cache.split(num_kv_heads, dim=1)
|
||||
|
||||
# Flatten bf16 context into tokens and quantize via reshape_and_cache_flash.
|
||||
# bf16_cache is (B, 2*H, N, hs); split K/V on head dim.
|
||||
block_table = common_attn_metadata.block_table_tensor
|
||||
seq_lens = common_attn_metadata.seq_lens.cpu()
|
||||
query_lens = (
|
||||
@@ -257,19 +140,24 @@ def _create_nvfp4_hnd_kv_cache(
|
||||
ctx_len = int(seq_lens[i]) - int(query_lens[i])
|
||||
if ctx_len == 0:
|
||||
continue
|
||||
# Gather context tokens from the bf16 cache using block table.
|
||||
n_ctx_blocks = (ctx_len + block_size - 1) // block_size
|
||||
blocks = block_table[i, :n_ctx_blocks]
|
||||
# bf16_cache is (B, 2*H, N, hs); split K and V head groups.
|
||||
k_bf16, v_bf16 = bf16_cache[blocks].split(num_kv_heads, dim=1)
|
||||
k_ctx = k_bf16.transpose(1, 2).reshape(-1, num_kv_heads, head_size)[:ctx_len]
|
||||
v_ctx = v_bf16.transpose(1, 2).reshape(-1, num_kv_heads, head_size)[:ctx_len]
|
||||
# Build slot mapping for these context tokens.
|
||||
# bf16_cache is (B, H, N, 2*head_size); extract K and V from last dim.
|
||||
k_ctx = (
|
||||
bf16_cache[blocks, :, :, :head_size]
|
||||
.transpose(1, 2)
|
||||
.reshape(-1, num_kv_heads, head_size)[:ctx_len]
|
||||
)
|
||||
v_ctx = (
|
||||
bf16_cache[blocks, :, :, head_size:]
|
||||
.transpose(1, 2)
|
||||
.reshape(-1, num_kv_heads, head_size)[:ctx_len]
|
||||
)
|
||||
token_offsets = torch.arange(ctx_len, device=device)
|
||||
block_indices = token_offsets // block_size
|
||||
intra_offsets = token_offsets % block_size
|
||||
slots = block_table[i, block_indices] * block_size + intra_offsets
|
||||
# reshape_and_cache_flash expects (B, N, H, D) cache views.
|
||||
# reshape_and_cache_flash expects (B, N, H, D) cache views
|
||||
torch.ops._C_cache_ops.reshape_and_cache_flash(
|
||||
k_ctx,
|
||||
v_ctx,
|
||||
@@ -363,7 +251,7 @@ def _run_trtllm_integration(batch_spec, kv_cache_dtype="auto", model_name=MODEL)
|
||||
|
||||
common_attn_metadata = create_common_attn_metadata(batch_spec, BLOCK_SIZE, device)
|
||||
|
||||
# 2. Create HND KV cache
|
||||
# 2. Create HNC KV cache
|
||||
is_nvfp4 = kv_cache_dtype == "nvfp4"
|
||||
if is_nvfp4:
|
||||
# Compute a global scale from the context data.
|
||||
@@ -383,7 +271,7 @@ def _run_trtllm_integration(batch_spec, kv_cache_dtype="auto", model_name=MODEL)
|
||||
)
|
||||
else:
|
||||
kv_scale_val = 1.0
|
||||
kv_cache = _create_hnd_kv_cache(
|
||||
kv_cache = create_and_prepopulate_kv_cache(
|
||||
k_contexts,
|
||||
v_contexts,
|
||||
BLOCK_SIZE,
|
||||
@@ -393,11 +281,12 @@ def _run_trtllm_integration(batch_spec, kv_cache_dtype="auto", model_name=MODEL)
|
||||
device,
|
||||
NUM_GPU_BLOCKS,
|
||||
common_attn_metadata,
|
||||
layout=KVCacheLayout.LBHNC,
|
||||
)
|
||||
|
||||
# 3. Run through FlashInfer with TRTLLM enabled
|
||||
set_kv_cache_layout("HND")
|
||||
get_kv_cache_layout.cache_clear()
|
||||
set_kv_cache_layout("LBHNC")
|
||||
resolve_kv_cache_layout.cache_clear()
|
||||
|
||||
try:
|
||||
is_nvfp4 = kv_cache_dtype == "nvfp4"
|
||||
@@ -513,7 +402,7 @@ def _run_trtllm_integration(batch_spec, kv_cache_dtype="auto", model_name=MODEL)
|
||||
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
get_kv_cache_layout.cache_clear()
|
||||
resolve_kv_cache_layout.cache_clear()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Tests for contiguous KV cache packing."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.v1.core.kv_cache_utils import (
|
||||
_get_kv_cache_config_packed,
|
||||
get_kv_cache_config_from_groups,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheGroupSpec,
|
||||
KVCacheTensor,
|
||||
MLAAttentionSpec,
|
||||
SlidingWindowSpec,
|
||||
UniformTypeKVCacheSpecs,
|
||||
)
|
||||
|
||||
|
||||
def _make_mla_spec(page_size: int, block_size: int = 256) -> MLAAttentionSpec:
|
||||
return MLAAttentionSpec(
|
||||
block_size=block_size,
|
||||
num_kv_heads=1,
|
||||
head_size=512,
|
||||
dtype=torch.uint8,
|
||||
page_size_padded=page_size,
|
||||
cache_dtype_str="fp8_ds_mla",
|
||||
model_version="deepseek_v4",
|
||||
alignment=576,
|
||||
)
|
||||
|
||||
|
||||
def _make_full_spec() -> FullAttentionSpec:
|
||||
return FullAttentionSpec(
|
||||
block_size=16,
|
||||
num_kv_heads=2,
|
||||
head_size=64,
|
||||
dtype=torch.float16,
|
||||
)
|
||||
|
||||
|
||||
def _make_sw_spec() -> SlidingWindowSpec:
|
||||
return SlidingWindowSpec(
|
||||
block_size=16,
|
||||
num_kv_heads=2,
|
||||
head_size=64,
|
||||
dtype=torch.float16,
|
||||
sliding_window=128,
|
||||
)
|
||||
|
||||
|
||||
def _make_groups(n_c4, n_c128, n_swa):
|
||||
PS_C4_MLA = 37440
|
||||
PS_C4_IDX = 8640
|
||||
PS_C128 = 1728
|
||||
PS_SWA = 37440
|
||||
|
||||
mla_specs = {}
|
||||
for i in range(n_c4):
|
||||
mla_specs[f"c4_mla.{i}"] = _make_mla_spec(PS_C4_MLA)
|
||||
mla_specs[f"c4_idx.{i}"] = _make_mla_spec(PS_C4_IDX)
|
||||
for i in range(n_c128):
|
||||
mla_specs[f"c128_mla.{i}"] = _make_mla_spec(PS_C128)
|
||||
|
||||
mla_group = KVCacheGroupSpec(
|
||||
layer_names=list(mla_specs.keys()),
|
||||
kv_cache_spec=UniformTypeKVCacheSpecs(block_size=256, kv_cache_specs=mla_specs),
|
||||
)
|
||||
|
||||
swa_specs = {}
|
||||
for i in range(n_swa):
|
||||
swa_specs[f"swa.{i}"] = _make_mla_spec(PS_SWA)
|
||||
|
||||
swa_group = KVCacheGroupSpec(
|
||||
layer_names=list(swa_specs.keys()),
|
||||
kv_cache_spec=UniformTypeKVCacheSpecs(block_size=256, kv_cache_specs=swa_specs),
|
||||
)
|
||||
|
||||
return [mla_group, swa_group]
|
||||
|
||||
|
||||
def _mock_vllm_config(kv_connector_extra_config: dict[str, str] | None = None):
|
||||
config = MagicMock()
|
||||
config.cache_config.num_gpu_blocks_override = None
|
||||
config.kv_transfer_config = None
|
||||
if kv_connector_extra_config is not None:
|
||||
config.kv_transfer_config = MagicMock()
|
||||
config.kv_transfer_config.kv_connector_extra_config = kv_connector_extra_config
|
||||
return config
|
||||
|
||||
|
||||
def _run(n_c4=3, n_c128=2, n_swa=5, mem=100 * 1024 * 1024):
|
||||
groups = _make_groups(n_c4, n_c128, n_swa)
|
||||
return _get_kv_cache_config_packed(_mock_vllm_config(), groups, mem)
|
||||
|
||||
|
||||
def _page_sizes_by_layer(
|
||||
groups: list[KVCacheGroupSpec],
|
||||
) -> dict[str, int]:
|
||||
page_sizes = {}
|
||||
for group in groups:
|
||||
specs = group.kv_cache_spec.kv_cache_specs
|
||||
for layer_name in group.layer_names:
|
||||
page_sizes[layer_name] = specs[layer_name].page_size_bytes
|
||||
return page_sizes
|
||||
|
||||
|
||||
class TestInterleavedPacking:
|
||||
def test_all_tensors_have_block_stride(self):
|
||||
_, tensors = _run()
|
||||
for t in tensors:
|
||||
assert t.block_stride > 0
|
||||
|
||||
def test_all_tensors_share_same_size(self):
|
||||
_, tensors = _run()
|
||||
sizes = set(t.size for t in tensors)
|
||||
assert len(sizes) == 1
|
||||
assert sizes.pop() > 0
|
||||
|
||||
def test_offsets_within_one_block(self):
|
||||
_, tensors = _run()
|
||||
for t in tensors:
|
||||
assert t.offset < t.block_stride
|
||||
|
||||
def test_all_layers_accounted_for(self):
|
||||
n_c4, n_c128, n_swa = 5, 4, 7
|
||||
_, tensors = _run(n_c4=n_c4, n_c128=n_c128, n_swa=n_swa)
|
||||
all_names = set()
|
||||
for t in tensors:
|
||||
all_names.update(t.shared_by)
|
||||
expected = n_c4 * 2 + n_c128 + n_swa
|
||||
assert len(all_names) == expected
|
||||
|
||||
def test_strided_views_are_independent(self):
|
||||
groups = _make_groups(n_c4=3, n_c128=2, n_swa=5)
|
||||
page_sizes = _page_sizes_by_layer(groups)
|
||||
num_blocks, tensors = _get_kv_cache_config_packed(
|
||||
_mock_vllm_config(), groups, 100 * 1024 * 1024
|
||||
)
|
||||
backing = torch.zeros(tensors[0].size, dtype=torch.uint8)
|
||||
views = []
|
||||
for t in tensors:
|
||||
page_size = page_sizes[t.shared_by[0]]
|
||||
v = torch.as_strided(
|
||||
backing,
|
||||
size=(num_blocks, page_size),
|
||||
stride=(t.block_stride, 1),
|
||||
storage_offset=t.offset,
|
||||
)
|
||||
views.append(v)
|
||||
|
||||
for i, v in enumerate(views):
|
||||
v.fill_(i + 1)
|
||||
|
||||
for i, v in enumerate(views):
|
||||
assert (v == i + 1).all(), f"View {i} was corrupted"
|
||||
|
||||
def test_hma_attention_groups_keep_default_backing(self):
|
||||
full = _make_full_spec()
|
||||
sw = _make_sw_spec()
|
||||
page_size = full.page_size_bytes
|
||||
groups = [
|
||||
KVCacheGroupSpec(["full.0", "full.1"], full),
|
||||
KVCacheGroupSpec(["sw.0", "sw.2"], sw),
|
||||
KVCacheGroupSpec(["sw.1", "sw.3"], sw),
|
||||
]
|
||||
|
||||
config = get_kv_cache_config_from_groups(
|
||||
_mock_vllm_config(), groups, available_memory=page_size * 2 * 32
|
||||
)
|
||||
|
||||
assert config.num_blocks == 32
|
||||
assert sum(t.size for t in config.kv_cache_tensors) == page_size * 2 * 32
|
||||
assert config.kv_cache_tensors == [
|
||||
KVCacheTensor(size=page_size * 32, shared_by=["full.0", "sw.0", "sw.1"]),
|
||||
KVCacheTensor(size=page_size * 32, shared_by=["full.1", "sw.2", "sw.3"]),
|
||||
]
|
||||
|
||||
def test_hma_attention_groups_use_packed_backing_with_enable_cross_layers(self):
|
||||
full = _make_full_spec()
|
||||
sw = _make_sw_spec()
|
||||
page_size = full.page_size_bytes
|
||||
groups = [
|
||||
KVCacheGroupSpec(["full.0", "full.1"], full),
|
||||
KVCacheGroupSpec(["sw.0", "sw.2"], sw),
|
||||
KVCacheGroupSpec(["sw.1", "sw.3"], sw),
|
||||
]
|
||||
|
||||
config = get_kv_cache_config_from_groups(
|
||||
_mock_vllm_config({"enable_cross_layers_blocks": "True"}),
|
||||
groups,
|
||||
available_memory=page_size * 2 * 32,
|
||||
)
|
||||
|
||||
assert config.num_blocks == 32
|
||||
assert {t.size for t in config.kv_cache_tensors} == {page_size * 2 * 32}
|
||||
assert config.kv_cache_tensors == [
|
||||
KVCacheTensor(
|
||||
size=page_size * 2 * 32,
|
||||
shared_by=["full.0", "sw.0", "sw.1"],
|
||||
offset=0,
|
||||
block_stride=page_size * 2,
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=page_size * 2 * 32,
|
||||
shared_by=["full.1", "sw.2", "sw.3"],
|
||||
offset=page_size,
|
||||
block_stride=page_size * 2,
|
||||
),
|
||||
]
|
||||
|
||||
def test_single_group_attention_keeps_unpacked_layout(self):
|
||||
spec = _make_full_spec()
|
||||
groups = [KVCacheGroupSpec(["full.0", "full.1"], spec)]
|
||||
|
||||
config = get_kv_cache_config_from_groups(
|
||||
_mock_vllm_config(), groups, available_memory=spec.page_size_bytes * 2 * 32
|
||||
)
|
||||
|
||||
assert sum(t.size for t in config.kv_cache_tensors) == (
|
||||
spec.page_size_bytes * 2 * 32
|
||||
)
|
||||
assert [t.block_stride for t in config.kv_cache_tensors] == [0, 0]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -835,36 +835,19 @@ def test_get_kv_cache_configs_multiple_workers():
|
||||
ref_kv_cache_spec.page_size_bytes * 2 * 10,
|
||||
],
|
||||
)
|
||||
assert kv_cache_configs == [
|
||||
KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer1"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer2"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer1", "layer2"], ref_kv_cache_spec),
|
||||
],
|
||||
),
|
||||
KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer1"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer2"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer1", "layer2"], ref_kv_cache_spec),
|
||||
],
|
||||
),
|
||||
]
|
||||
expected = KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10 * 2,
|
||||
shared_by=[["layer1"], ["layer2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer1", "layer2"], ref_kv_cache_spec),
|
||||
],
|
||||
)
|
||||
assert kv_cache_configs == [expected, expected]
|
||||
|
||||
# Different available memory. This is the case for TP.
|
||||
# Use the smallest memory available.
|
||||
@@ -876,36 +859,7 @@ def test_get_kv_cache_configs_multiple_workers():
|
||||
ref_kv_cache_spec.page_size_bytes * 2 * 20,
|
||||
],
|
||||
)
|
||||
assert kv_cache_configs == [
|
||||
KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer1"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer2"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer1", "layer2"], ref_kv_cache_spec),
|
||||
],
|
||||
),
|
||||
KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer1"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer2"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer1", "layer2"], ref_kv_cache_spec),
|
||||
],
|
||||
),
|
||||
]
|
||||
assert kv_cache_configs == [expected, expected]
|
||||
|
||||
# Different KV cache specs. This is the case for PP.
|
||||
different_layer_specs = [
|
||||
@@ -932,7 +886,8 @@ def test_get_kv_cache_configs_multiple_workers():
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer1"]
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10,
|
||||
shared_by=[["layer1"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
@@ -943,10 +898,8 @@ def test_get_kv_cache_configs_multiple_workers():
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer2"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer3"]
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10 * 2,
|
||||
shared_by=[["layer2"], ["layer3"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
@@ -976,64 +929,37 @@ def test_get_kv_cache_configs_multiple_workers():
|
||||
kv_cache_configs = get_kv_cache_configs(
|
||||
vllm_config,
|
||||
tp_pp_kv_cache_specs,
|
||||
[
|
||||
ref_kv_cache_spec.page_size_bytes * 2 * 10,
|
||||
ref_kv_cache_spec.page_size_bytes * 2 * 10,
|
||||
ref_kv_cache_spec.page_size_bytes * 2 * 10,
|
||||
ref_kv_cache_spec.page_size_bytes * 2 * 10,
|
||||
[ref_kv_cache_spec.page_size_bytes * 2 * 10] * 4,
|
||||
)
|
||||
expected_12 = KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10 * 2,
|
||||
shared_by=[["layer1"], ["layer2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer1", "layer2"], ref_kv_cache_spec),
|
||||
],
|
||||
)
|
||||
expected_3 = KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10,
|
||||
shared_by=[["layer3"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer3"], ref_kv_cache_spec),
|
||||
],
|
||||
)
|
||||
assert kv_cache_configs == [
|
||||
KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer1"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer2"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer1", "layer2"], ref_kv_cache_spec),
|
||||
],
|
||||
),
|
||||
KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer1"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer2"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer1", "layer2"], ref_kv_cache_spec),
|
||||
],
|
||||
),
|
||||
KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer3"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer3"], ref_kv_cache_spec),
|
||||
],
|
||||
),
|
||||
KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer3"]
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer3"], ref_kv_cache_spec),
|
||||
],
|
||||
),
|
||||
expected_12,
|
||||
expected_12,
|
||||
expected_3,
|
||||
expected_3,
|
||||
]
|
||||
|
||||
# Different workers have different types of layers. This is the case for
|
||||
@@ -1061,10 +987,8 @@ def test_get_kv_cache_configs_multiple_workers():
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer1"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer2"]
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10 * 2,
|
||||
shared_by=[["layer1"], ["layer2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
@@ -1076,10 +1000,8 @@ def test_get_kv_cache_configs_multiple_workers():
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer3"]
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10, shared_by=["layer4"]
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10 * 2,
|
||||
shared_by=[["layer3"], ["layer4"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
@@ -1106,10 +1028,7 @@ def test_get_kv_cache_configs_multiple_workers():
|
||||
kv_cache_configs = get_kv_cache_configs(
|
||||
vllm_config,
|
||||
different_type_layer_specs,
|
||||
[
|
||||
ref_kv_cache_spec.page_size_bytes * 10,
|
||||
ref_kv_cache_spec.page_size_bytes * 10,
|
||||
],
|
||||
[ref_kv_cache_spec.page_size_bytes * 10] * 2,
|
||||
)
|
||||
assert kv_cache_configs == [
|
||||
KVCacheConfig(
|
||||
@@ -1117,7 +1036,7 @@ def test_get_kv_cache_configs_multiple_workers():
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10,
|
||||
shared_by=["layer1", "layer2", "layer3"],
|
||||
shared_by=[["layer1", "layer2", "layer3"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
@@ -1131,7 +1050,7 @@ def test_get_kv_cache_configs_multiple_workers():
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * 10,
|
||||
shared_by=["layer4", "layer5", "layer6"],
|
||||
shared_by=[["layer4", "layer5", "layer6"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
@@ -1198,7 +1117,7 @@ def test_get_kv_cache_configs_pp_sharding(asymmetric_memory):
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * expected_num_blocks,
|
||||
shared_by=["layer1"],
|
||||
shared_by=[["layer1"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(["layer1"], ref_kv_cache_spec)],
|
||||
@@ -1208,7 +1127,7 @@ def test_get_kv_cache_configs_pp_sharding(asymmetric_memory):
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=ref_kv_cache_spec.page_size_bytes * expected_num_blocks,
|
||||
shared_by=["layer2"],
|
||||
shared_by=[["layer2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(["layer2"], ref_kv_cache_spec)],
|
||||
@@ -1536,78 +1455,13 @@ def test_get_max_concurrency_for_kv_cache_config():
|
||||
)
|
||||
|
||||
|
||||
def test_get_max_concurrency_packed_kv_cache_config():
|
||||
from vllm.v1.core.kv_cache_utils import (
|
||||
_get_kv_cache_config_packed,
|
||||
_use_packed_kv_cache_config,
|
||||
)
|
||||
|
||||
model_config = ModelConfig(
|
||||
"Qwen/Qwen1.5-7B",
|
||||
runner="generate",
|
||||
dtype="float16",
|
||||
max_model_len=16384,
|
||||
)
|
||||
scheduler_config = SchedulerConfig(
|
||||
max_num_batched_tokens=1024,
|
||||
enable_chunked_prefill=True,
|
||||
max_model_len=model_config.max_model_len,
|
||||
is_encoder_decoder=model_config.is_encoder_decoder,
|
||||
async_scheduling=False,
|
||||
)
|
||||
vllm_config = VllmConfig(
|
||||
model_config=model_config,
|
||||
scheduler_config=scheduler_config,
|
||||
)
|
||||
|
||||
# All-UniformTypeKVCacheSpecs groups select the packed layout.
|
||||
mla_specs = {f"layer_{i}": new_mla_spec() for i in range(4)}
|
||||
swa_specs = {
|
||||
f"layer_{i}": SlidingWindowMLASpec(
|
||||
block_size=16,
|
||||
num_kv_heads=1,
|
||||
head_size=576,
|
||||
dtype=torch.float32,
|
||||
sliding_window=128,
|
||||
)
|
||||
for i in range(4, 6)
|
||||
}
|
||||
kv_cache_groups = [
|
||||
KVCacheGroupSpec(
|
||||
list(mla_specs),
|
||||
UniformTypeKVCacheSpecs(block_size=16, kv_cache_specs=mla_specs),
|
||||
),
|
||||
KVCacheGroupSpec(
|
||||
list(swa_specs),
|
||||
UniformTypeKVCacheSpecs(block_size=16, kv_cache_specs=swa_specs),
|
||||
),
|
||||
]
|
||||
assert _use_packed_kv_cache_config(vllm_config, kv_cache_groups)
|
||||
num_blocks, kv_cache_tensors = _get_kv_cache_config_packed(
|
||||
vllm_config, kv_cache_groups, 2 * GiB_bytes
|
||||
)
|
||||
assert num_blocks > 0
|
||||
kv_cache_config_packed = KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=kv_cache_tensors,
|
||||
kv_cache_groups=kv_cache_groups,
|
||||
)
|
||||
# Per-request blocks: the MLA group needs cdiv(16384, 16) = 1024 pages;
|
||||
# the SWA group cdiv(min(128 - 1 + 1024, 16384), 16) + 1 = 73. The
|
||||
# previous formula normalized by the first group's page size and gave
|
||||
# 1061 blocks per request instead of 1097.
|
||||
assert get_max_concurrency_for_kv_cache_config(
|
||||
vllm_config, kv_cache_config_packed
|
||||
) == num_blocks / (1024 + 73)
|
||||
|
||||
|
||||
def test_allocate_with_lookahead():
|
||||
"""Verify that lookahead tokens correctly affect block allocation"""
|
||||
block_size = 4
|
||||
config = KVCacheConfig(
|
||||
num_blocks=10,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=100, shared_by=["layer1"]),
|
||||
KVCacheTensor(size=100, shared_by=[["layer1"]]),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer1"], new_kv_cache_spec(block_size=block_size)),
|
||||
@@ -1682,11 +1536,14 @@ def test_get_kv_cache_config_one_worker():
|
||||
vllm_config, [kv_cache_specs_full], [mem_per_block_per_layer * 2 * 32]
|
||||
)[0]
|
||||
print(kv_cache_config_full)
|
||||
|
||||
assert kv_cache_config_full == KVCacheConfig(
|
||||
num_blocks=32,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=mem_per_block_per_layer * 32, shared_by=["layer_1"]),
|
||||
KVCacheTensor(size=mem_per_block_per_layer * 32, shared_by=["layer_2"]),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32 * 2,
|
||||
shared_by=[["layer_1"], ["layer_2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(["layer_1", "layer_2"], new_kv_cache_spec())],
|
||||
)
|
||||
@@ -1702,8 +1559,10 @@ def test_get_kv_cache_config_one_worker():
|
||||
assert kv_cache_config_sliding == KVCacheConfig(
|
||||
num_blocks=32,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=mem_per_block_per_layer * 32, shared_by=["layer_1"]),
|
||||
KVCacheTensor(size=mem_per_block_per_layer * 32, shared_by=["layer_2"]),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32 * 2,
|
||||
shared_by=[["layer_1"], ["layer_2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer_1", "layer_2"], new_sliding_window_spec())
|
||||
@@ -1722,8 +1581,10 @@ def test_get_kv_cache_config_one_worker():
|
||||
assert kv_cache_config_hybrid == KVCacheConfig(
|
||||
num_blocks=32,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=mem_per_block_per_layer * 32, shared_by=["layer_1"]),
|
||||
KVCacheTensor(size=mem_per_block_per_layer * 32, shared_by=["layer_2"]),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32 * 2,
|
||||
shared_by=[["layer_1"], ["layer_2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(
|
||||
@@ -1745,7 +1606,8 @@ def test_get_kv_cache_config_one_worker():
|
||||
num_blocks=64,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 64, shared_by=["layer_1", "layer_2"]
|
||||
size=mem_per_block_per_layer * 64,
|
||||
shared_by=[["layer_1", "layer_2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
@@ -1770,12 +1632,11 @@ def test_get_kv_cache_config_one_worker():
|
||||
num_blocks=32,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=["layer_1", "layer_3", "layer_4"],
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=["layer_2", "layer_5", "layer_6"],
|
||||
size=mem_per_block_per_layer * 32 * 2,
|
||||
shared_by=[
|
||||
["layer_1", "layer_3", "layer_4"],
|
||||
["layer_2", "layer_5", "layer_6"],
|
||||
],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
@@ -1805,15 +1666,12 @@ def test_get_kv_cache_config_one_worker():
|
||||
num_blocks=32,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=["layer_1", "layer_4", "layer_5", "layer_6"],
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=["layer_2", "layer_7", "layer_8", "layer_9"],
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32, shared_by=["layer_3", "layer_10"]
|
||||
size=mem_per_block_per_layer * 32 * 3,
|
||||
shared_by=[
|
||||
["layer_1", "layer_4", "layer_5", "layer_6"],
|
||||
["layer_2", "layer_7", "layer_8", "layer_9"],
|
||||
["layer_3", "layer_10"],
|
||||
],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
@@ -1826,8 +1684,7 @@ def test_get_kv_cache_config_one_worker():
|
||||
],
|
||||
)
|
||||
|
||||
# 6 full + 5 sliding, pad to 6 full + 6 sliding. This is a typical case for gpt-oss
|
||||
# eagle where there is only one more full attention layer than sliding window layers
|
||||
# 6 full + 5 sliding
|
||||
kv_cache_specs_hybrid = {
|
||||
"layer_1": new_kv_cache_spec(),
|
||||
"layer_2": new_kv_cache_spec(),
|
||||
@@ -1845,33 +1702,19 @@ def test_get_kv_cache_config_one_worker():
|
||||
kv_cache_config_hybrid = get_kv_cache_configs(
|
||||
vllm_config, [kv_cache_specs_hybrid], [mem_per_block_per_layer * 6 * 32]
|
||||
)[0]
|
||||
print(kv_cache_config_hybrid)
|
||||
assert kv_cache_config_hybrid == KVCacheConfig(
|
||||
num_blocks=32,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=["layer_1", "layer_7"],
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=["layer_2", "layer_8"],
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=["layer_3", "layer_9"],
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=["layer_4", "layer_10"],
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=["layer_5", "layer_11"],
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=["layer_6"],
|
||||
size=mem_per_block_per_layer * 32 * 6,
|
||||
shared_by=[
|
||||
["layer_1", "layer_7"],
|
||||
["layer_2", "layer_8"],
|
||||
["layer_3", "layer_9"],
|
||||
["layer_4", "layer_10"],
|
||||
["layer_5", "layer_11"],
|
||||
["layer_6"],
|
||||
],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
@@ -1897,8 +1740,14 @@ def test_get_kv_cache_config_one_worker():
|
||||
assert kv_cache_config_hybrid == KVCacheConfig(
|
||||
num_blocks=32,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=mem_per_block_per_layer * 32 * 2, shared_by=["layer_1"]),
|
||||
KVCacheTensor(size=mem_per_block_per_layer * 32, shared_by=["layer_2"]),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32 * 2,
|
||||
shared_by=[["layer_1"]],
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=[["layer_2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(
|
||||
@@ -1922,7 +1771,8 @@ def test_get_kv_cache_config_one_worker():
|
||||
num_blocks=32,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 32, shared_by=["layer_1", "layer_2"]
|
||||
size=mem_per_block_per_layer * 32,
|
||||
shared_by=[["layer_1", "layer_2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
@@ -1948,7 +1798,10 @@ def test_get_kv_cache_config_one_worker():
|
||||
assert kv_cache_config_hybrid == KVCacheConfig(
|
||||
num_blocks=42,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=padded_page_size * 42, shared_by=["layer_1", "layer_2"]),
|
||||
KVCacheTensor(
|
||||
size=padded_page_size * 42,
|
||||
shared_by=[["layer_1", "layer_2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(
|
||||
@@ -1974,8 +1827,10 @@ def test_get_kv_cache_config_one_worker():
|
||||
assert kv_cache_config_override_blocks == KVCacheConfig(
|
||||
num_blocks=16,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=mem_per_block_per_layer * 16, shared_by=["layer_1"]),
|
||||
KVCacheTensor(size=mem_per_block_per_layer * 16, shared_by=["layer_2"]),
|
||||
KVCacheTensor(
|
||||
size=mem_per_block_per_layer * 16 * 2,
|
||||
shared_by=[["layer_1"], ["layer_2"]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(["layer_1", "layer_2"], new_kv_cache_spec())],
|
||||
)
|
||||
|
||||
@@ -148,7 +148,7 @@ def make_kv_cache_config_hybrid_model(
|
||||
elif second_spec_type == "mamba":
|
||||
second_spec = MambaSpec(
|
||||
block_size=block_size,
|
||||
shapes=(1, 1),
|
||||
shapes=((1, 1),),
|
||||
dtypes=(torch.float32,),
|
||||
)
|
||||
|
||||
@@ -183,7 +183,7 @@ def make_kv_cache_config_three_types(
|
||||
if third_spec_type == "mamba":
|
||||
third_spec = MambaSpec(
|
||||
block_size=block_size,
|
||||
shapes=(1, 1),
|
||||
shapes=((1, 1),),
|
||||
dtypes=(torch.float32,),
|
||||
)
|
||||
elif third_spec_type == "sliding_window":
|
||||
@@ -762,12 +762,12 @@ def _make_hybrid_kv_cache_config(
|
||||
),
|
||||
"mamba": lambda: MambaSpec(
|
||||
block_size=block_size,
|
||||
shapes=(1, 1),
|
||||
shapes=((1, 1),),
|
||||
dtypes=(torch.float32,),
|
||||
),
|
||||
"mamba_align": lambda: MambaSpec(
|
||||
block_size=block_size,
|
||||
shapes=(1, 1),
|
||||
shapes=((1, 1),),
|
||||
dtypes=(torch.float32,),
|
||||
mamba_cache_mode="align",
|
||||
),
|
||||
@@ -3244,7 +3244,7 @@ def test_hybrid_local_kv_retention_interval_survives_recycling(monkeypatch):
|
||||
num_kv_heads=1,
|
||||
head_size=1,
|
||||
dtype=torch.uint8,
|
||||
compress_ratio=4,
|
||||
tokens_per_state=4,
|
||||
),
|
||||
),
|
||||
KVCacheGroupSpec(
|
||||
|
||||
@@ -94,7 +94,6 @@ else
|
||||
echo "running with default attention backend"
|
||||
fi
|
||||
|
||||
# Check if cross-layers is enabled (non-empty)
|
||||
if [[ -n "${CROSS_LAYERS_BLOCKS:-}" ]]; then
|
||||
echo "CROSS_LAYERS_BLOCKS is set, running with --enable-cross-layers"
|
||||
label+=" - CROSS_LAYERS_BLOCKS enabled"
|
||||
|
||||
@@ -4,7 +4,11 @@ set -xe
|
||||
# Parse command line arguments
|
||||
KV_BUFFER_DEVICE="cuda" # Default to cuda
|
||||
ATTENTION_BACKEND="" # Default to empty (use vllm default)
|
||||
CROSS_LAYERS_BLOCKS="False"
|
||||
ENABLE_HMA_VAR="" # Default to empty (HMA disabled by default for kv connector)
|
||||
# Check for ENABLE_HMA_FLAG environment variable
|
||||
if [[ -n "${ENABLE_HMA_FLAG:-}" ]]; then
|
||||
ENABLE_HMA_VAR="--no-disable-hybrid-kv-cache-manager"
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
@@ -17,12 +21,12 @@ while [[ $# -gt 0 ]]; do
|
||||
shift 2
|
||||
;;
|
||||
--enable-cross-layers)
|
||||
CROSS_LAYERS_BLOCKS="True"
|
||||
export VLLM_KV_CACHE_LAYOUT="BLHNC"
|
||||
shift 1
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1"
|
||||
echo "Usage: $0 [--kv_buffer_device <cuda|cpu>] [--attention-backend <backend>]"
|
||||
echo "Usage: $0 [--kv_buffer_device <cuda|cpu>] [--attention-backend <backend>] [--enable-cross-layers]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -36,29 +40,24 @@ if [[ -n "$VLLM_SERVE_EXTRA_ARGS" ]]; then
|
||||
echo "vLLM serve extra args: $VLLM_SERVE_EXTRA_ARGS"
|
||||
fi
|
||||
|
||||
DECODER_KV_LAYOUT=${DECODER_KV_LAYOUT:-"HND"} # Default to HND, optional NHD
|
||||
if [[ "$DECODER_KV_LAYOUT" == "NHD" ]]; then
|
||||
PREFILLER_KV_LAYOUT=${VLLM_KV_CACHE_LAYOUT:-"LBHNC"}
|
||||
DECODER_KV_LAYOUT=${DECODER_KV_LAYOUT:-"$PREFILLER_KV_LAYOUT"}
|
||||
if [[ "$DECODER_KV_LAYOUT" == "LBNHC" ]]; then
|
||||
KV_CONFIG_HETERO_LAYOUT=',"enable_permute_local_kv":"True"'
|
||||
else
|
||||
KV_CONFIG_HETERO_LAYOUT=''
|
||||
fi
|
||||
|
||||
if [[ "$CROSS_LAYERS_BLOCKS" == "True" ]]; then
|
||||
KV_EXTRA_CONFIG=',"kv_connector_extra_config":{"enable_cross_layers_blocks": "True"}'
|
||||
else
|
||||
KV_EXTRA_CONFIG=''
|
||||
fi
|
||||
|
||||
# Connector: default pull NixlConnector; NixlPushConnector enables PP prefill.
|
||||
KV_CONNECTOR=${KV_CONNECTOR:-NixlConnector}
|
||||
|
||||
# Build the kv-transfer-config for P and D
|
||||
if [[ "$KV_BUFFER_DEVICE" == "cuda" ]]; then
|
||||
KV_CONFIG_P='{"kv_connector":"'"$KV_CONNECTOR"'","kv_role":"kv_producer"'${KV_CONFIG_HETERO_LAYOUT}${KV_EXTRA_CONFIG}'}'
|
||||
KV_CONFIG_D='{"kv_connector":"'"$KV_CONNECTOR"'","kv_role":"kv_consumer"'${KV_CONFIG_HETERO_LAYOUT}${KV_EXTRA_CONFIG}'}'
|
||||
KV_CONFIG_P='{"kv_connector":"'"$KV_CONNECTOR"'","kv_role":"kv_producer"'${KV_CONFIG_HETERO_LAYOUT}'}'
|
||||
KV_CONFIG_D='{"kv_connector":"'"$KV_CONNECTOR"'","kv_role":"kv_consumer"'${KV_CONFIG_HETERO_LAYOUT}'}'
|
||||
else
|
||||
KV_CONFIG_P="{\"kv_connector\":\"$KV_CONNECTOR\",\"kv_role\":\"kv_producer\",\"kv_buffer_device\":\"$KV_BUFFER_DEVICE\""${KV_CONFIG_HETERO_LAYOUT}${KV_EXTRA_CONFIG}"}"
|
||||
KV_CONFIG_D="{\"kv_connector\":\"$KV_CONNECTOR\",\"kv_role\":\"kv_consumer\",\"kv_buffer_device\":\"$KV_BUFFER_DEVICE\""${KV_CONFIG_HETERO_LAYOUT}${KV_EXTRA_CONFIG}"}"
|
||||
KV_CONFIG_P="{\"kv_connector\":\"$KV_CONNECTOR\",\"kv_role\":\"kv_producer\",\"kv_buffer_device\":\"$KV_BUFFER_DEVICE\""${KV_CONFIG_HETERO_LAYOUT}"}"
|
||||
KV_CONFIG_D="{\"kv_connector\":\"$KV_CONNECTOR\",\"kv_role\":\"kv_consumer\",\"kv_buffer_device\":\"$KV_BUFFER_DEVICE\""${KV_CONFIG_HETERO_LAYOUT}"}"
|
||||
fi
|
||||
|
||||
# Models to run
|
||||
@@ -165,7 +164,7 @@ run_tests_for_model() {
|
||||
|
||||
# Build the command with or without model-specific args
|
||||
BASE_CMD="CUDA_VISIBLE_DEVICES=$GPU_ID \
|
||||
VLLM_KV_CACHE_LAYOUT='HND' \
|
||||
VLLM_KV_CACHE_LAYOUT='$PREFILLER_KV_LAYOUT' \
|
||||
VLLM_PORT=$INTERNAL_PORT \
|
||||
UCX_NET_DEVICES=all \
|
||||
VLLM_NIXL_SIDE_CHANNEL_PORT=$SIDE_CHANNEL_PORT \
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
# wrapping NixlConnector and OffloadingConnector, then runs gsm8k accuracy via
|
||||
# test_accuracy.py.
|
||||
#
|
||||
# By default runs two configurations:
|
||||
# 1. Normal KV layout (NixlConnector without cross-layer blocks)
|
||||
# 2. Cross-layer KV layout (NixlConnector with enable_cross_layers_blocks)
|
||||
# Runs two configurations:
|
||||
# 1. Standard KV layout (LBHNC)
|
||||
# 2. Cross-layer KV layout (BLHNC) via VLLM_KV_CACHE_LAYOUT
|
||||
#
|
||||
# Usage:
|
||||
# bash tests/v1/kv_connector/nixl_integration/run_multi_connector_accuracy_test.sh
|
||||
@@ -44,8 +44,6 @@ SMI_BIN=$(which nvidia-smi || which rocm-smi || echo "")
|
||||
|
||||
# ── KV transfer configs ─────────────────────────────────────────────────
|
||||
|
||||
# Normal layout: OffloadingConnector prefers cross-layer but NixlConnector
|
||||
# does not, so MultiConnector.prefer_cross_layer_blocks = False.
|
||||
KV_CONFIG_NORMAL='{
|
||||
"kv_connector":"MultiConnector",
|
||||
"kv_role":"kv_both",
|
||||
@@ -60,21 +58,6 @@ KV_CONFIG_NORMAL='{
|
||||
# Remove whitespace for CLI safety
|
||||
KV_CONFIG_NORMAL=$(echo "$KV_CONFIG_NORMAL" | tr -d '[:space:]')
|
||||
|
||||
# Cross-layer layout: both connectors prefer cross-layer blocks.
|
||||
KV_CONFIG_CROSS_LAYERS='{
|
||||
"kv_connector":"MultiConnector",
|
||||
"kv_role":"kv_both",
|
||||
"kv_connector_extra_config":{
|
||||
"connectors":[
|
||||
{"kv_connector":"NixlConnector","kv_role":"kv_both",
|
||||
"kv_connector_extra_config":{"enable_cross_layers_blocks":"True"}},
|
||||
{"kv_connector":"OffloadingConnector","kv_role":"kv_both",
|
||||
"kv_connector_extra_config":{"cpu_bytes_to_use":1000000000}}
|
||||
]
|
||||
}
|
||||
}'
|
||||
KV_CONFIG_CROSS_LAYERS=$(echo "$KV_CONFIG_CROSS_LAYERS" | tr -d '[:space:]')
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
trap 'kill $(jobs -pr) 2>/dev/null' SIGINT SIGTERM EXIT
|
||||
@@ -125,7 +108,7 @@ run_tests_for_model() {
|
||||
# ── Start prefill instance ──
|
||||
echo "Starting prefill instance on GPU $PREFILL_GPU, port $PREFILL_PORT"
|
||||
BASE_CMD="CUDA_VISIBLE_DEVICES=$PREFILL_GPU \
|
||||
VLLM_KV_CACHE_LAYOUT='HND' \
|
||||
VLLM_KV_CACHE_LAYOUT='${VLLM_KV_CACHE_LAYOUT:-LBHNC}' \
|
||||
UCX_NET_DEVICES=all \
|
||||
VLLM_NIXL_SIDE_CHANNEL_PORT=$PREFILL_SIDE_CHANNEL_PORT \
|
||||
vllm serve $model_name \
|
||||
@@ -150,7 +133,7 @@ run_tests_for_model() {
|
||||
# ── Start decode instance ──
|
||||
echo "Starting decode instance on GPU $DECODE_GPU, port $DECODE_PORT"
|
||||
BASE_CMD="CUDA_VISIBLE_DEVICES=$DECODE_GPU \
|
||||
VLLM_KV_CACHE_LAYOUT='HND' \
|
||||
VLLM_KV_CACHE_LAYOUT='${VLLM_KV_CACHE_LAYOUT:-LBHNC}' \
|
||||
UCX_NET_DEVICES=all \
|
||||
VLLM_NIXL_SIDE_CHANNEL_PORT=$DECODE_SIDE_CHANNEL_PORT \
|
||||
vllm serve $model_name \
|
||||
@@ -207,7 +190,8 @@ for model in "${MODELS[@]}"; do
|
||||
fi
|
||||
|
||||
if [[ -z "${SKIP_CROSS_LAYERS:-}" ]]; then
|
||||
run_tests_for_model "$model" "$KV_CONFIG_CROSS_LAYERS" "MultiConnector cross-layer layout"
|
||||
VLLM_KV_CACHE_LAYOUT=BLHNC \
|
||||
run_tests_for_model "$model" "$KV_CONFIG_NORMAL" "MultiConnector cross-layer layout"
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ run_tests_for_model() {
|
||||
# ── Start prefill instance ──
|
||||
echo "Starting prefill instance on GPU $PREFILL_GPU, port $PREFILL_PORT"
|
||||
BASE_CMD="CUDA_VISIBLE_DEVICES=$PREFILL_GPU \
|
||||
VLLM_KV_CACHE_LAYOUT='HND' \
|
||||
VLLM_KV_CACHE_LAYOUT='LBHNC' \
|
||||
UCX_NET_DEVICES=all \
|
||||
VLLM_NIXL_SIDE_CHANNEL_PORT=$PREFILL_SIDE_CHANNEL_PORT \
|
||||
vllm serve \"$model_name\" \
|
||||
@@ -121,7 +121,7 @@ run_tests_for_model() {
|
||||
# ── Start decode instance ──
|
||||
echo "Starting decode instance on GPU $DECODE_GPU, port $DECODE_PORT"
|
||||
BASE_CMD="CUDA_VISIBLE_DEVICES=$DECODE_GPU \
|
||||
VLLM_KV_CACHE_LAYOUT='HND' \
|
||||
VLLM_KV_CACHE_LAYOUT='LBHNC' \
|
||||
UCX_NET_DEVICES=all \
|
||||
VLLM_NIXL_SIDE_CHANNEL_PORT=$DECODE_SIDE_CHANNEL_PORT \
|
||||
vllm serve \"$model_name\" \
|
||||
|
||||
@@ -247,7 +247,7 @@ run_test_for_device() {
|
||||
echo "Starting prefill instance $i on GPU $GPU_ID, port $PORT"
|
||||
env \
|
||||
${GPU_DEVICE_VAR}=$GPU_ID \
|
||||
VLLM_KV_CACHE_LAYOUT='HND' \
|
||||
VLLM_KV_CACHE_LAYOUT='LBHNC' \
|
||||
UCX_NET_DEVICES=all \
|
||||
${VLLM_SSM_CONV_STATE_LAYOUT:+VLLM_SSM_CONV_STATE_LAYOUT=$VLLM_SSM_CONV_STATE_LAYOUT} \
|
||||
VLLM_NIXL_SIDE_CHANNEL_HOST=$NIXL_SIDE_CHANNEL_HOST \
|
||||
@@ -286,7 +286,7 @@ run_test_for_device() {
|
||||
echo "Starting decode instance $i on GPU $GPU_ID, port $PORT"
|
||||
env \
|
||||
${GPU_DEVICE_VAR}=$GPU_ID \
|
||||
VLLM_KV_CACHE_LAYOUT='HND' \
|
||||
VLLM_KV_CACHE_LAYOUT='LBHNC' \
|
||||
UCX_NET_DEVICES=all \
|
||||
${VLLM_SSM_CONV_STATE_LAYOUT:+VLLM_SSM_CONV_STATE_LAYOUT=$VLLM_SSM_CONV_STATE_LAYOUT} \
|
||||
VLLM_NIXL_SIDE_CHANNEL_HOST=$NIXL_SIDE_CHANNEL_HOST \
|
||||
|
||||
@@ -9,7 +9,6 @@ import torch
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.torch_utils import get_dtype_size
|
||||
from vllm.v1.attention.backends.registry import AttentionBackendEnum
|
||||
from vllm.v1.attention.backends.utils import set_kv_cache_layout
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheConfig,
|
||||
@@ -60,31 +59,25 @@ def _allocate_and_reshape_kv_caches(
|
||||
Use the real GPUModelRunner allocation and reshape methods to produce
|
||||
kv_caches, just like the model runner does during initialization.
|
||||
"""
|
||||
from vllm.v1.kv_cache_interface import KVCacheLayout
|
||||
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
|
||||
|
||||
# Some backends (e.g. FlashAttention) query the KV cache layout during
|
||||
# reshape, which ultimately calls get_current_vllm_config(). Setting
|
||||
# the layout override avoids needing a full VllmConfig context.
|
||||
set_kv_cache_layout("NHD")
|
||||
try:
|
||||
runner = object.__new__(GPUModelRunner)
|
||||
runner.device = device
|
||||
runner.runner_only_attn_layers = set()
|
||||
runner.attn_groups = attn_groups
|
||||
runner.kv_cache_config = kv_cache_config
|
||||
runner.cache_config = MagicMock(cache_dtype="auto")
|
||||
runner.shared_kv_cache_layers = {}
|
||||
runner.model_config = MagicMock()
|
||||
runner.model_config.hf_config.model_type = ""
|
||||
runner.compilation_config = MagicMock(
|
||||
static_forward_context=defaultdict(MagicMock)
|
||||
)
|
||||
runner.kv_caches = []
|
||||
runner = object.__new__(GPUModelRunner)
|
||||
runner.device = device
|
||||
runner.runner_only_attn_layers = set()
|
||||
runner.attn_groups = attn_groups
|
||||
runner.kv_cache_config = kv_cache_config
|
||||
runner.cache_config = MagicMock(cache_dtype="auto")
|
||||
runner.shared_kv_cache_layers = {}
|
||||
runner.model_config = MagicMock()
|
||||
runner.model_config.hf_config.model_type = ""
|
||||
runner.compilation_config = MagicMock(static_forward_context=defaultdict(MagicMock))
|
||||
runner.kv_caches = []
|
||||
|
||||
kernel_block_sizes = [BLOCK_SIZE] * len(kv_cache_config.kv_cache_groups)
|
||||
return runner.initialize_kv_cache_tensors(kv_cache_config, kernel_block_sizes)
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
kernel_block_sizes = [BLOCK_SIZE] * len(kv_cache_config.kv_cache_groups)
|
||||
return runner._allocate_and_reshape_kv_cache(
|
||||
kv_cache_config, kernel_block_sizes, layout=KVCacheLayout.LBNHC
|
||||
)
|
||||
|
||||
|
||||
def _make_worker(kv_cache_config: KVCacheConfig):
|
||||
@@ -211,18 +204,19 @@ def test_register_kv_caches(backend):
|
||||
aligned_mamba_layer_names,
|
||||
]
|
||||
|
||||
kv_cache_tensors: list[KVCacheTensor] = []
|
||||
shared_by: list[list[str]] = []
|
||||
for i in range(GROUP_SIZE):
|
||||
shared_by: list[str] = []
|
||||
slot_layers: list[str] = []
|
||||
for group_layer_names in layer_groups:
|
||||
if len(group_layer_names) > i:
|
||||
shared_by.append(group_layer_names[i])
|
||||
kv_cache_tensors.append(
|
||||
KVCacheTensor(
|
||||
size=PAGE_SIZE_BYTES * NUM_BLOCKS,
|
||||
shared_by=shared_by,
|
||||
)
|
||||
slot_layers.append(group_layer_names[i])
|
||||
shared_by.append(slot_layers)
|
||||
kv_cache_tensors: list[KVCacheTensor] = [
|
||||
KVCacheTensor(
|
||||
size=PAGE_SIZE_BYTES * NUM_BLOCKS * GROUP_SIZE,
|
||||
shared_by=shared_by,
|
||||
)
|
||||
]
|
||||
|
||||
kv_cache_groups = [
|
||||
KVCacheGroupSpec(layer_names=attn_layer_names, kv_cache_spec=attn_spec),
|
||||
@@ -381,11 +375,11 @@ def test_register_kv_caches_uniform_type(backend):
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=spec_a.page_size_bytes * NUM_BLOCKS,
|
||||
shared_by=[layer_a],
|
||||
shared_by=[[layer_a]],
|
||||
),
|
||||
KVCacheTensor(
|
||||
size=spec_b.page_size_bytes * NUM_BLOCKS,
|
||||
shared_by=[layer_b],
|
||||
shared_by=[[layer_b]],
|
||||
),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
|
||||
@@ -275,7 +275,7 @@ class RequestRunner:
|
||||
)
|
||||
|
||||
# register worker kv_caches to enable OffloadingWorker creations
|
||||
# set_current_vllm_config is needed for get_kv_cache_layout() to work
|
||||
# set_current_vllm_config is needed for resolve_kv_cache_layout() to work
|
||||
kv_caches: dict[str, torch.Tensor] = {}
|
||||
for group in kv_cache_groups:
|
||||
spec = group.kv_cache_spec
|
||||
|
||||
@@ -98,7 +98,7 @@ def _make_connector_with_fake_worker(
|
||||
)
|
||||
worker = connector.connector_worker
|
||||
assert isinstance(worker.nixl_wrapper, FakeNixlWrapper)
|
||||
worker.kv_cache_layout = "HND"
|
||||
worker.kv_cache_layout = "LBHNC"
|
||||
if do_handshake:
|
||||
remote_agents, _ = worker._nixl_handshake(
|
||||
host="localhost",
|
||||
|
||||
@@ -1,75 +1,79 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for reshape_kv_cache."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.v1.attention.backends.utils import (
|
||||
get_flashinfer_layout_string,
|
||||
set_kv_cache_layout,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheLayout,
|
||||
compute_layer_kv_cache_shape_bytes,
|
||||
reshape_kv_cache,
|
||||
)
|
||||
|
||||
|
||||
def test_mla_common_backend_rejects_cross_layer_kv_cache():
|
||||
"""MLACommonBackend defaults to the identity permutation (layers dim
|
||||
first) so MLA backends whose decode kernels are not verified to honor
|
||||
the cache's block-dim stride stay opted out of cross-layer KV cache."""
|
||||
from vllm.model_executor.layers.attention.mla_attention import (
|
||||
MLACommonBackend,
|
||||
)
|
||||
|
||||
stride_order = MLACommonBackend.get_kv_cache_stride_order(
|
||||
include_num_layers_dimension=True
|
||||
)
|
||||
assert stride_order == (0, 1, 2, 3)
|
||||
assert stride_order[0] == 0 # layers dim first => no cross-layer
|
||||
assert MLACommonBackend.get_kv_cache_stride_order(
|
||||
include_num_layers_dimension=False
|
||||
) == (0, 1, 2)
|
||||
NUM_BLOCKS = 4
|
||||
BLOCK_SIZE = 4
|
||||
NUM_KV_HEADS = 2
|
||||
HEAD_SIZE = 8
|
||||
DTYPE = torch.bfloat16
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"backend_path",
|
||||
# See: https://github.com/vllm-project/vllm/issues/46411
|
||||
("layout", "expected"),
|
||||
[
|
||||
"vllm.v1.attention.backends.mla.triton_mla.TritonMLABackend",
|
||||
]
|
||||
if current_platform.is_rocm() or current_platform.is_xpu()
|
||||
else [
|
||||
"vllm.v1.attention.backends.mla.triton_mla.TritonMLABackend",
|
||||
"vllm.v1.attention.backends.mla.cutlass_mla.CutlassMLABackend",
|
||||
"vllm.v1.attention.backends.mla.flashattn_mla.FlashAttnMLABackend",
|
||||
"vllm.v1.attention.backends.mla.flashmla.FlashMLABackend",
|
||||
"vllm.v1.attention.backends.mla.flashinfer_mla.FlashInferMLABackend",
|
||||
("LBHNC", "HND"),
|
||||
("LBNHC", "NHD"),
|
||||
("BLHNC", "HND"),
|
||||
("BLNHC", "NHD"),
|
||||
("BHLNC", "HND"),
|
||||
],
|
||||
)
|
||||
def test_verified_mla_backends_support_cross_layer_kv_cache(backend_path):
|
||||
"""Backends whose decode kernels honor the cache's block-dim stride opt
|
||||
in to the cross-layer layout with a non-identity permutation placing
|
||||
num_blocks first in physical layout."""
|
||||
module_path, name = backend_path.rsplit(".", 1)
|
||||
backend = getattr(
|
||||
pytest.importorskip(module_path, reason="backend deps unavailable"), name
|
||||
)
|
||||
|
||||
stride_order = backend.get_kv_cache_stride_order(include_num_layers_dimension=True)
|
||||
assert stride_order == (1, 0, 2, 3)
|
||||
assert stride_order[0] != 0 # num_blocks first => cross-layer supported
|
||||
assert backend.get_kv_cache_stride_order(include_num_layers_dimension=False) == (
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
)
|
||||
def test_flashinfer_layout_string(layout: str, expected: str):
|
||||
set_kv_cache_layout(layout)
|
||||
try:
|
||||
assert get_flashinfer_layout_string() == expected
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
|
||||
def test_deepseek_v32_indexer_rejects_cross_layer_kv_cache():
|
||||
"""DeepseekV32Indexer returns identity permutation (layers dim first)
|
||||
to signal cross-layer KV cache is unsupported."""
|
||||
from vllm.v1.attention.backends.mla.indexer import (
|
||||
DeepseekV32IndexerBackend,
|
||||
@pytest.mark.parametrize("layout", list(KVCacheLayout))
|
||||
def test_reshape_kv_cache(layout):
|
||||
spec = FullAttentionSpec(
|
||||
block_size=BLOCK_SIZE,
|
||||
num_kv_heads=NUM_KV_HEADS,
|
||||
head_size=HEAD_SIZE,
|
||||
dtype=DTYPE,
|
||||
)
|
||||
num_slots = 2
|
||||
total_bytes = spec.page_size_bytes * NUM_BLOCKS * num_slots
|
||||
raw = torch.zeros(total_bytes, dtype=torch.int8, device="cuda")
|
||||
views = reshape_kv_cache(raw, spec, NUM_BLOCKS, num_slots, layout)
|
||||
|
||||
stride_order = DeepseekV32IndexerBackend.get_kv_cache_stride_order(
|
||||
include_num_layers_dimension=True
|
||||
)
|
||||
assert stride_order == (0, 1, 2, 3)
|
||||
assert stride_order[0] == 0 # layers dim first => no cross-layer
|
||||
assert DeepseekV32IndexerBackend.get_kv_cache_stride_order(
|
||||
include_num_layers_dimension=False
|
||||
) == (0, 1, 2)
|
||||
byte_4d = compute_layer_kv_cache_shape_bytes(spec, NUM_BLOCKS)
|
||||
dtype_size = torch.tensor([], dtype=spec.dtype).element_size()
|
||||
expected_shape = (*byte_4d[:3], byte_4d[3] // dtype_size)
|
||||
|
||||
assert len(views) == num_slots
|
||||
for v in views:
|
||||
assert v.shape == expected_shape
|
||||
assert v.dtype == spec.dtype
|
||||
|
||||
# The per-layer view preserves the physical order of B, H, N, and C
|
||||
# after the layer dimension is selected. Dimensions later in that order
|
||||
# have smaller strides, including when layer interleaving creates gaps.
|
||||
stride_order = layout.layer_view_order
|
||||
strides = views[0].stride()
|
||||
for i in range(3):
|
||||
for j in range(i + 1, 4):
|
||||
if stride_order[i] < stride_order[j]:
|
||||
assert strides[i] >= strides[j], (
|
||||
f"layout {layout.name}: dim {i} (physical pos "
|
||||
f"{stride_order[i]}) should have >= stride than "
|
||||
f"dim {j} (physical pos {stride_order[j]}), got "
|
||||
f"strides={strides}"
|
||||
)
|
||||
|
||||
@@ -32,17 +32,25 @@ from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_utils import
|
||||
MooncakeBootstrapServer,
|
||||
)
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend
|
||||
from vllm.v1.attention.backends.utils import set_kv_cache_layout
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheConfig,
|
||||
KVCacheGroupSpec,
|
||||
KVCacheLayout,
|
||||
reshape_kv_cache,
|
||||
)
|
||||
from vllm.v1.request import RequestStatus
|
||||
|
||||
from .utils import create_request, create_scheduler, create_vllm_config
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_kv_cache_layout():
|
||||
yield
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
|
||||
def _make_test_kv_cache_config() -> KVCacheConfig:
|
||||
return KVCacheConfig(
|
||||
num_blocks=0,
|
||||
@@ -1093,13 +1101,23 @@ async def test_worker_get_finished_timeout(monkeypatch):
|
||||
assert "tx-active" in prefill_worker.reqs_need_send
|
||||
|
||||
|
||||
def test_register_kv_caches():
|
||||
@pytest.mark.parametrize(
|
||||
("layout", "separate_kv_head_groups"),
|
||||
[
|
||||
(KVCacheLayout.LBHNC, False),
|
||||
(KVCacheLayout.BLHNC, False),
|
||||
(KVCacheLayout.LBHNC, True),
|
||||
(KVCacheLayout.BHLNC, True),
|
||||
],
|
||||
)
|
||||
def test_register_kv_caches(layout: KVCacheLayout, separate_kv_head_groups: bool):
|
||||
"""Tests the memory registration logic with the underlying Mooncake engine."""
|
||||
|
||||
vllm_config = create_vllm_config(
|
||||
kv_connector="MooncakeConnector", kv_role="kv_consumer"
|
||||
)
|
||||
|
||||
set_kv_cache_layout(layout.name)
|
||||
with (
|
||||
set_current_vllm_config(vllm_config),
|
||||
patch_worker_dependencies(),
|
||||
@@ -1118,15 +1136,22 @@ def test_register_kv_caches():
|
||||
worker = connector.connector_worker
|
||||
mock_thread.return_value.is_alive.return_value = False
|
||||
|
||||
kv_cache_shape = FlashAttentionBackend.get_kv_cache_shape(
|
||||
num_blocks=2, block_size=16, num_kv_heads=4, head_size=64
|
||||
spec = FullAttentionSpec(
|
||||
block_size=16,
|
||||
num_kv_heads=4,
|
||||
head_size=64,
|
||||
dtype=torch.float16,
|
||||
separate_kv_head_groups=separate_kv_head_groups,
|
||||
)
|
||||
tensor1 = torch.zeros(*kv_cache_shape, dtype=torch.float16)
|
||||
tensor2 = torch.zeros(*kv_cache_shape, dtype=torch.float16)
|
||||
kv_caches = {
|
||||
"model.layers.0.self_attn": tensor1,
|
||||
"model.layers.1.self_attn": tensor2,
|
||||
}
|
||||
layer_names = [
|
||||
"model.layers.0.self_attn",
|
||||
"model.layers.1.self_attn",
|
||||
]
|
||||
for layer_name in layer_names:
|
||||
worker._layer_specs[layer_name] = spec
|
||||
raw = torch.zeros(2 * 2 * spec.page_size_bytes, dtype=torch.int8)
|
||||
tensor1, tensor2 = reshape_kv_cache(raw, spec, 2, 2, layout)
|
||||
kv_caches = dict(zip(layer_names, (tensor1, tensor2)))
|
||||
|
||||
with patch.object(
|
||||
worker.engine, "batch_register_memory", return_value=0
|
||||
@@ -1135,16 +1160,35 @@ def test_register_kv_caches():
|
||||
|
||||
mock_batch_register.assert_called_once()
|
||||
registered_ptrs, registered_lens = mock_batch_register.call_args[0]
|
||||
expected_ptrs = {tensor.data_ptr() for tensor in kv_caches.values()}
|
||||
assert set(registered_ptrs) == expected_ptrs
|
||||
assert set(registered_lens) == {tensor1.nbytes}
|
||||
assert registered_ptrs == [raw.data_ptr()]
|
||||
assert registered_lens == [raw.nbytes]
|
||||
|
||||
# Verify block_len_per_layer is set correctly.
|
||||
assert len(worker.block_len_per_layer) == len(registered_ptrs)
|
||||
for bl in worker.block_len_per_layer:
|
||||
assert bl == tensor1.nbytes // tensor1.shape[0]
|
||||
assert worker.registered_layer_names == list(kv_caches)
|
||||
assert worker.registered_layer_indices == [0, 1]
|
||||
if separate_kv_head_groups:
|
||||
expected_addrs = [
|
||||
cache[:, head_idx].data_ptr()
|
||||
for cache in (tensor1, tensor2)
|
||||
for head_idx in range(cache.shape[1])
|
||||
]
|
||||
head_block_bytes = tensor1.stride(0) * tensor1.element_size()
|
||||
assert worker.kv_caches_base_addr == expected_addrs
|
||||
assert worker.block_len_per_layer == [head_block_bytes] * len(
|
||||
expected_addrs
|
||||
)
|
||||
assert worker.kv_block_len_per_layer == [head_block_bytes] * len(
|
||||
expected_addrs
|
||||
)
|
||||
assert worker.registered_layer_names == [
|
||||
layer_name
|
||||
for layer_name in layer_names
|
||||
for _ in range(tensor1.shape[1])
|
||||
]
|
||||
else:
|
||||
assert len(worker.block_len_per_layer) == len(kv_caches)
|
||||
for bl in worker.block_len_per_layer:
|
||||
assert bl == tensor1.stride(0) * tensor1.element_size()
|
||||
assert worker.kv_block_len_per_layer == [spec.page_size_bytes] * 2
|
||||
assert worker.registered_layer_names == list(kv_caches)
|
||||
assert worker.registered_layer_indices == [0, 1]
|
||||
|
||||
|
||||
def test_register_kv_caches_supports_mixed_mla_and_eagle_shapes():
|
||||
|
||||
@@ -30,7 +30,9 @@ from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheConfig,
|
||||
KVCacheGroupSpec,
|
||||
KVCacheLayout,
|
||||
MambaSpec,
|
||||
reshape_kv_cache,
|
||||
)
|
||||
|
||||
from .test_mooncake_connector import patch_worker_dependencies
|
||||
@@ -138,14 +140,22 @@ def test_register_kv_caches_emits_fa_and_gdn_regions(monkeypatch):
|
||||
)
|
||||
worker = connector.connector_worker
|
||||
|
||||
fa_cache = torch.empty((2, 2, 11), dtype=torch.float16)
|
||||
gdn_conv_state = torch.empty((2, 22), dtype=torch.float16)
|
||||
gdn_ssm_state = torch.empty((2, 4), dtype=torch.float16)
|
||||
num_blocks = kv_cache_config.num_blocks
|
||||
fa_spec = kv_cache_config.kv_cache_groups[0].kv_cache_spec
|
||||
gdn_spec = kv_cache_config.kv_cache_groups[1].kv_cache_spec
|
||||
fa_raw = torch.empty(num_blocks * fa_spec.page_size_bytes, dtype=torch.int8)
|
||||
gdn_raw = torch.empty(num_blocks * gdn_spec.page_size_bytes, dtype=torch.int8)
|
||||
(fa_cache,) = reshape_kv_cache(
|
||||
fa_raw, fa_spec, num_blocks, 1, KVCacheLayout.LBHNC
|
||||
)
|
||||
(gdn_cache,) = reshape_kv_cache(
|
||||
gdn_raw, gdn_spec, num_blocks, 1, KVCacheLayout.LBHNC
|
||||
)
|
||||
|
||||
worker.register_kv_caches(
|
||||
{
|
||||
"model.layers.0.self_attn": fa_cache,
|
||||
"model.layers.1.linear_attn": (gdn_conv_state, gdn_ssm_state),
|
||||
"model.layers.1.linear_attn": gdn_cache,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -154,10 +164,14 @@ def test_register_kv_caches_emits_fa_and_gdn_regions(monkeypatch):
|
||||
"model.layers.0.self_attn",
|
||||
"model.layers.1.linear_attn",
|
||||
]
|
||||
assert worker.block_len_per_layer == [
|
||||
fa_spec.page_size_bytes,
|
||||
gdn_spec.page_size_bytes,
|
||||
]
|
||||
assert worker.registered_group_indices == [0, 1]
|
||||
assert worker.kv_caches_base_addr == [
|
||||
fa_cache.data_ptr(),
|
||||
gdn_conv_state.data_ptr(),
|
||||
gdn_cache.data_ptr(),
|
||||
]
|
||||
|
||||
worker.shutdown()
|
||||
@@ -185,8 +199,7 @@ def test_register_kv_caches_deduplicates_shared_backing_memory(monkeypatch):
|
||||
|
||||
backing = torch.empty((4, 64), dtype=torch.float16)
|
||||
fa_cache = backing[:2, :16]
|
||||
gdn_conv_state = backing[:3]
|
||||
gdn_ssm_state = torch.empty((3, 4), dtype=torch.float16)
|
||||
gdn_cache = backing[:3]
|
||||
|
||||
with patch.object(
|
||||
worker.engine, "batch_register_memory", return_value=0
|
||||
@@ -194,13 +207,13 @@ def test_register_kv_caches_deduplicates_shared_backing_memory(monkeypatch):
|
||||
worker.register_kv_caches(
|
||||
{
|
||||
"model.layers.0.self_attn": fa_cache,
|
||||
"model.layers.1.linear_attn": (gdn_conv_state, gdn_ssm_state),
|
||||
"model.layers.1.linear_attn": gdn_cache,
|
||||
}
|
||||
)
|
||||
|
||||
assert worker.kv_caches_base_addr == [
|
||||
fa_cache.data_ptr(),
|
||||
gdn_conv_state.data_ptr(),
|
||||
gdn_cache.data_ptr(),
|
||||
]
|
||||
batch_register_memory.assert_called_once()
|
||||
registered_ptrs, registered_lens = batch_register_memory.call_args[0]
|
||||
@@ -339,7 +352,7 @@ def test_logical_to_kernel_block_ids_expands_fa_not_gdn():
|
||||
assert kernel_block_ids == [list(range(34, 51)), [2]]
|
||||
|
||||
|
||||
def test_hybrid_gdn_splits_fa_regions_but_keeps_gdn_state_whole(
|
||||
def test_hybrid_gdn_keeps_packed_fa_and_gdn_regions_whole(
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setenv("VLLM_MOONCAKE_ABORT_REQUEST_TIMEOUT", "5")
|
||||
@@ -359,7 +372,7 @@ def test_hybrid_gdn_splits_fa_regions_but_keeps_gdn_state_whole(
|
||||
)
|
||||
worker = connector.connector_worker
|
||||
|
||||
worker.transfer_topo = SimpleNamespace(virtually_split_kv_in_blocks=True)
|
||||
worker.transfer_topo = SimpleNamespace(is_kv_layout_blocks_first=False)
|
||||
regions = worker._get_transfer_regions(
|
||||
base_addrs=[0x1000, 0x2000],
|
||||
block_lens=[0x100, 0x100],
|
||||
@@ -377,7 +390,6 @@ def test_hybrid_gdn_splits_fa_regions_but_keeps_gdn_state_whole(
|
||||
for region in regions
|
||||
] == [
|
||||
(0, 0x1000, 0x40),
|
||||
(0, 0x1040, 0x40),
|
||||
(1, 0x2000, 0x100),
|
||||
]
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def _make_kv_cache_config() -> KVCacheConfig:
|
||||
spec = FullAttentionSpec(block_size=16, num_kv_heads=8, head_size=64, dtype=None)
|
||||
return KVCacheConfig(
|
||||
num_blocks=4,
|
||||
kv_cache_tensors=[KVCacheTensor(size=8192, shared_by=["layer0"])],
|
||||
kv_cache_tensors=[KVCacheTensor(size=8192, shared_by=[["layer0"]])],
|
||||
kv_cache_groups=[KVCacheGroupSpec(["layer0"], spec)],
|
||||
)
|
||||
|
||||
@@ -210,64 +210,6 @@ def test_get_kv_connector_kv_cache_events_wraps_worker_events():
|
||||
assert kv_events.get_all_events() == [event]
|
||||
|
||||
|
||||
def test_prefer_cross_layer_blocks_from_config():
|
||||
# Default: disabled
|
||||
vllm_config = _make_vllm_config()
|
||||
kv_cache_config = _make_kv_cache_config()
|
||||
with (
|
||||
set_current_vllm_config(vllm_config),
|
||||
patch(
|
||||
"vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store."
|
||||
"connector.MooncakeStoreScheduler"
|
||||
),
|
||||
):
|
||||
connector = mooncake_store_connector.MooncakeStoreConnector(
|
||||
vllm_config, KVConnectorRole.SCHEDULER, kv_cache_config
|
||||
)
|
||||
assert connector.prefer_cross_layer_blocks is False
|
||||
|
||||
# Enabled via config
|
||||
vllm_config_enabled = create_vllm_config(
|
||||
kv_connector="MooncakeStoreConnector",
|
||||
kv_role="kv_both",
|
||||
kv_connector_extra_config={"enable_cross_layers_blocks": "true"},
|
||||
)
|
||||
with (
|
||||
set_current_vllm_config(vllm_config_enabled),
|
||||
patch(
|
||||
"vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store."
|
||||
"connector.MooncakeStoreScheduler"
|
||||
),
|
||||
):
|
||||
connector_enabled = mooncake_store_connector.MooncakeStoreConnector(
|
||||
vllm_config_enabled, KVConnectorRole.SCHEDULER, kv_cache_config
|
||||
)
|
||||
assert connector_enabled.prefer_cross_layer_blocks is True
|
||||
|
||||
|
||||
def test_register_cross_layers_kv_cache_delegates_to_worker():
|
||||
vllm_config = _make_vllm_config()
|
||||
kv_cache_config = _make_kv_cache_config()
|
||||
|
||||
with (
|
||||
set_current_vllm_config(vllm_config),
|
||||
patch(
|
||||
"vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store."
|
||||
"connector.MooncakeStoreWorker"
|
||||
) as mock_worker_cls,
|
||||
):
|
||||
connector = mooncake_store_connector.MooncakeStoreConnector(
|
||||
vllm_config, KVConnectorRole.WORKER, kv_cache_config
|
||||
)
|
||||
|
||||
fake_tensor = MagicMock()
|
||||
fake_backend = MagicMock()
|
||||
connector.register_cross_layers_kv_cache(fake_tensor, fake_backend)
|
||||
|
||||
worker = mock_worker_cls.return_value
|
||||
worker.register_cross_layers_kv_caches.assert_called_once_with(fake_tensor)
|
||||
|
||||
|
||||
def test_update_connector_output_and_take_events():
|
||||
vllm_config = _make_vllm_config()
|
||||
kv_cache_config = _make_kv_cache_config()
|
||||
|
||||
@@ -159,8 +159,8 @@ def test_e2e_swa_plus_full_save_then_lookup_hits():
|
||||
cfg = KVCacheConfig(
|
||||
num_blocks=4,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=8192, shared_by=["L0"]),
|
||||
KVCacheTensor(size=8192, shared_by=["L1"]),
|
||||
KVCacheTensor(size=8192, shared_by=[["L0"]]),
|
||||
KVCacheTensor(size=8192, shared_by=[["L1"]]),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["L0"], full),
|
||||
|
||||
@@ -34,7 +34,20 @@ from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store.data import (
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store.metrics import (
|
||||
MooncakeStoreConnectorStats,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import set_kv_cache_layout
|
||||
from vllm.v1.core.kv_cache_utils import BlockHash
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheGroupSpec,
|
||||
KVCacheLayout,
|
||||
reshape_kv_cache,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_kv_cache_layout():
|
||||
yield
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
|
||||
class _RecordingBlockHashes:
|
||||
@@ -1818,112 +1831,72 @@ def test_lookup_applies_swa_mask_before_accessing_hashes():
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_register_kv_caches_blocks_first_single_segment():
|
||||
"""Blocks-first layout (FlashInfer/MLA): one segment per layer."""
|
||||
@pytest.mark.parametrize("layout", list(KVCacheLayout))
|
||||
def test_register_kv_caches_shared_storage(layout: KVCacheLayout):
|
||||
num_blocks = 10
|
||||
page_size_elements = 64
|
||||
num_layers = 2
|
||||
worker = _make_bare_worker(num_gpu_blocks=num_blocks)
|
||||
|
||||
# Shape: (num_blocks, page_size_elements) — blocks outermost, no outer_dims
|
||||
tensor = torch.zeros(num_blocks, page_size_elements, dtype=torch.float16)
|
||||
_register_with_mocked_threads(worker, {"layer0": tensor})
|
||||
|
||||
db = worker.token_dbs[0]
|
||||
assert db.kv_caches_base_addr == [tensor.untyped_storage().data_ptr()]
|
||||
assert db.block_len == [tensor.untyped_storage().nbytes() // num_blocks]
|
||||
worker.store.register_buffer.assert_called_once_with(
|
||||
tensor.untyped_storage().data_ptr(),
|
||||
tensor.untyped_storage().nbytes(),
|
||||
)
|
||||
|
||||
|
||||
def test_register_kv_caches_kv_first_two_segments():
|
||||
"""K/V-first layout (FlashAttn): two segments (K, V) per layer."""
|
||||
num_blocks = 10
|
||||
block_size_tokens = 16
|
||||
num_kv_heads = 4
|
||||
head_size = 8
|
||||
worker = _make_bare_worker(num_gpu_blocks=num_blocks)
|
||||
|
||||
# Shape: (2, num_blocks, block_size, num_kv_heads, head_size) — K/V outermost
|
||||
tensor = torch.zeros(
|
||||
2,
|
||||
num_blocks,
|
||||
block_size_tokens,
|
||||
num_kv_heads,
|
||||
head_size,
|
||||
spec = FullAttentionSpec(
|
||||
block_size=16,
|
||||
num_kv_heads=2,
|
||||
head_size=8,
|
||||
dtype=torch.float16,
|
||||
)
|
||||
_register_with_mocked_threads(worker, {"layer0": tensor})
|
||||
|
||||
db = worker.token_dbs[0]
|
||||
seg_stride = tensor.stride(0) * tensor.element_size()
|
||||
base = tensor.untyped_storage().data_ptr()
|
||||
assert db.kv_caches_base_addr == [base, base + seg_stride]
|
||||
assert db.block_len == [seg_stride // num_blocks] * 2
|
||||
|
||||
|
||||
def test_register_kv_caches_cross_layer_single_segment():
|
||||
"""Cross-layer tensor: single segment with block_len = page_size * num_layers."""
|
||||
num_blocks = 10
|
||||
num_layers = 4
|
||||
per_layer_page_elements = 64 # elements per layer per block
|
||||
|
||||
worker = _make_bare_worker(num_gpu_blocks=num_blocks)
|
||||
|
||||
# Cross-layer blocks-first tensor: all layers packed into a single
|
||||
# contiguous block. Shape (num_blocks, num_layers * per_layer_page)
|
||||
# mimics the physical layout after stride reordering.
|
||||
total_page_elements = num_layers * per_layer_page_elements
|
||||
tensor = torch.zeros(num_blocks, total_page_elements, dtype=torch.float16)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store."
|
||||
"worker.KVCacheStoreSendingThread",
|
||||
side_effect=_auto_set_ready_event,
|
||||
),
|
||||
patch(
|
||||
"vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store."
|
||||
"worker.KVCacheStoreRecvingThread",
|
||||
side_effect=_auto_set_ready_event,
|
||||
),
|
||||
):
|
||||
# Use the cross-layer wrapper key, same as register_cross_layers_kv_caches
|
||||
worker.register_kv_caches({"__cross_layer__": tensor})
|
||||
|
||||
db = worker.token_dbs[0]
|
||||
assert len(db.kv_caches_base_addr) == 1
|
||||
assert db.kv_caches_base_addr[0] == tensor.untyped_storage().data_ptr()
|
||||
|
||||
expected_block_len = tensor.untyped_storage().nbytes() // num_blocks
|
||||
# block_len should be per_layer_page_size * num_layers
|
||||
assert (
|
||||
expected_block_len
|
||||
== num_layers * per_layer_page_elements * tensor.element_size()
|
||||
raw = torch.zeros(
|
||||
num_blocks * num_layers * spec.page_size_bytes,
|
||||
dtype=torch.int8,
|
||||
)
|
||||
assert len(db.block_len) == 1
|
||||
assert db.block_len[0] == expected_block_len
|
||||
caches = reshape_kv_cache(raw, spec, num_blocks, num_layers, layout)
|
||||
|
||||
# Also verify via register_cross_layers_kv_caches wrapper
|
||||
worker2 = _make_bare_worker(num_gpu_blocks=num_blocks)
|
||||
with (
|
||||
patch(
|
||||
"vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store."
|
||||
"worker.KVCacheStoreSendingThread",
|
||||
side_effect=_auto_set_ready_event,
|
||||
),
|
||||
patch(
|
||||
"vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store."
|
||||
"worker.KVCacheStoreRecvingThread",
|
||||
side_effect=_auto_set_ready_event,
|
||||
),
|
||||
):
|
||||
worker2.register_cross_layers_kv_caches(tensor)
|
||||
set_kv_cache_layout(layout.name)
|
||||
_register_with_mocked_threads(
|
||||
worker,
|
||||
{"layer0": caches[0], "__cross_layer__": caches[1]},
|
||||
)
|
||||
|
||||
db2 = worker2.token_dbs[0]
|
||||
assert db2.kv_caches_base_addr == db.kv_caches_base_addr
|
||||
assert db2.block_len == db.block_len
|
||||
db = worker.token_dbs[0]
|
||||
if layout.is_layer_compact:
|
||||
assert db.kv_caches_base_addr == [cache.data_ptr() for cache in caches]
|
||||
assert db.block_len == [spec.page_size_bytes] * num_layers
|
||||
else:
|
||||
assert db.kv_caches_base_addr == [raw.data_ptr()]
|
||||
assert db.block_len == [num_layers * spec.page_size_bytes]
|
||||
worker.store.register_buffer.assert_called_once_with(raw.data_ptr(), raw.nbytes)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layout", list(KVCacheLayout))
|
||||
def test_register_kv_caches_separate_head_groups(layout: KVCacheLayout):
|
||||
num_blocks = 3
|
||||
num_layers = 2
|
||||
worker = _make_bare_worker(num_gpu_blocks=num_blocks)
|
||||
spec = FullAttentionSpec(
|
||||
block_size=4,
|
||||
num_kv_heads=2,
|
||||
head_size=8,
|
||||
dtype=torch.float16,
|
||||
separate_kv_head_groups=True,
|
||||
)
|
||||
layer_names = ["layer0", "__cross_layer__"]
|
||||
worker._kv_cache_groups = [KVCacheGroupSpec(layer_names, spec)]
|
||||
raw = torch.zeros(
|
||||
num_blocks * num_layers * spec.page_size_bytes,
|
||||
dtype=torch.int8,
|
||||
)
|
||||
caches = reshape_kv_cache(raw, spec, num_blocks, num_layers, layout)
|
||||
|
||||
set_kv_cache_layout(layout.name)
|
||||
_register_with_mocked_threads(worker, dict(zip(layer_names, caches)))
|
||||
|
||||
head_block_bytes = caches[0].stride(0) * caches[0].element_size()
|
||||
expected_addrs = [
|
||||
cache[:, head_idx].data_ptr()
|
||||
for cache in caches
|
||||
for head_idx in range(cache.shape[1])
|
||||
]
|
||||
db = worker.token_dbs[0]
|
||||
assert db.kv_caches_base_addr == expected_addrs
|
||||
assert db.block_len == [head_block_bytes] * len(expected_addrs)
|
||||
worker.store.register_buffer.assert_called_once_with(raw.data_ptr(), raw.nbytes)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -43,6 +43,7 @@ from vllm.v1.kv_cache_interface import (
|
||||
KVCacheConfig,
|
||||
KVCacheGroupSpec,
|
||||
KVCacheTensor,
|
||||
compute_layer_kv_cache_shape_bytes,
|
||||
)
|
||||
|
||||
from .utils import create_request, create_scheduler
|
||||
@@ -58,7 +59,9 @@ def _make_test_kv_cache_config() -> KVCacheConfig:
|
||||
layer_names = ["layer0", "layer1", "layer2"]
|
||||
return KVCacheConfig(
|
||||
num_blocks=2,
|
||||
kv_cache_tensors=[KVCacheTensor(size=0, shared_by=layer_names)],
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=0, shared_by=[[name] for name in layer_names])
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(
|
||||
layer_names=layer_names,
|
||||
@@ -225,7 +228,7 @@ class FakeMoRIIOConnectorWorker(MoRIIOConnectorWorker):
|
||||
engine_id,
|
||||
*args,
|
||||
hand_shake_latency: float = 1.8,
|
||||
kv_cache_layout="HND",
|
||||
kv_cache_layout="LBHNC",
|
||||
kv_cache_config=None,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -525,16 +528,15 @@ def test_register_kv_caches(mock_parallel_groups):
|
||||
DEFAULT_PORT = 6301
|
||||
TP_RANK = 0
|
||||
DP_RANK = 0
|
||||
from vllm.v1.attention.backends.rocm_aiter_fa import AiterFlashAttentionBackend
|
||||
|
||||
backend_cls = AiterFlashAttentionBackend
|
||||
|
||||
# Create test kv cache tensors using proper backend shape
|
||||
kv_cache_shape = backend_cls.get_kv_cache_shape(
|
||||
num_blocks=2, block_size=16, num_kv_heads=4, head_size=64
|
||||
# Create test kv cache tensors using KVCacheSpec layout
|
||||
shape = compute_layer_kv_cache_shape_bytes(
|
||||
FullAttentionSpec(
|
||||
block_size=16, num_kv_heads=4, head_size=64, dtype=torch.float16
|
||||
),
|
||||
2,
|
||||
)
|
||||
shared_tensor = torch.zeros(*kv_cache_shape, dtype=torch.float16)
|
||||
unique_tensor = torch.zeros(*kv_cache_shape, dtype=torch.float16)
|
||||
shared_tensor = torch.zeros(*shape, dtype=torch.int8).view(torch.float16)
|
||||
unique_tensor = torch.zeros(*shape, dtype=torch.int8).view(torch.float16)
|
||||
kv_caches = {
|
||||
"layer0": shared_tensor,
|
||||
"layer1": unique_tensor,
|
||||
@@ -621,16 +623,15 @@ def test_moriio_handshake_returns_metadata(mock_parallel_groups):
|
||||
|
||||
ROLE = "kv_consumer"
|
||||
vllm_config = create_vllm_config(role=ROLE)
|
||||
from vllm.v1.attention.backends.rocm_aiter_fa import AiterFlashAttentionBackend
|
||||
|
||||
backend_cls = AiterFlashAttentionBackend
|
||||
|
||||
# Create test kv cache tensors using proper backend shape
|
||||
kv_cache_shape = backend_cls.get_kv_cache_shape(
|
||||
num_blocks=2, block_size=16, num_kv_heads=4, head_size=64
|
||||
# Create test kv cache tensors using KVCacheSpec layout
|
||||
shape = compute_layer_kv_cache_shape_bytes(
|
||||
FullAttentionSpec(
|
||||
block_size=16, num_kv_heads=4, head_size=64, dtype=torch.float16
|
||||
),
|
||||
2,
|
||||
)
|
||||
shared_tensor = torch.zeros(*kv_cache_shape, dtype=torch.float16)
|
||||
unique_tensor = torch.zeros(*kv_cache_shape, dtype=torch.float16)
|
||||
shared_tensor = torch.zeros(*shape, dtype=torch.int8).view(torch.float16)
|
||||
unique_tensor = torch.zeros(*shape, dtype=torch.int8).view(torch.float16)
|
||||
kv_caches = {
|
||||
"layer0": shared_tensor,
|
||||
"layer1": unique_tensor,
|
||||
|
||||
@@ -869,16 +869,6 @@ Options:
|
||||
""")
|
||||
|
||||
|
||||
def test_multi_connector_prefer_cross_layer_blocks(mc):
|
||||
mc._connectors[0].prefer_cross_layer_blocks = False
|
||||
mc._connectors[1].prefer_cross_layer_blocks = True
|
||||
assert mc.prefer_cross_layer_blocks is False
|
||||
|
||||
mc._connectors[0].prefer_cross_layer_blocks = True
|
||||
mc._connectors[1].prefer_cross_layer_blocks = True
|
||||
assert mc.prefer_cross_layer_blocks is True
|
||||
|
||||
|
||||
def test_multi_connector_worker_metadata(mc):
|
||||
class MockConnectorWorkerMetadata(KVConnectorWorkerMetadata):
|
||||
def __init__(self, data: set[str]):
|
||||
|
||||
@@ -53,7 +53,6 @@ from vllm.outputs import RequestOutput
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.platforms.interface import Platform
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend
|
||||
from vllm.v1.attention.backends.utils import set_kv_cache_layout
|
||||
from vllm.v1.engine import EngineCoreRequest
|
||||
from vllm.v1.engine.output_processor import OutputProcessor
|
||||
@@ -62,12 +61,12 @@ from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheConfig,
|
||||
KVCacheGroupSpec,
|
||||
KVCacheTensor,
|
||||
KVCacheLayout,
|
||||
compute_layer_kv_cache_shape_bytes,
|
||||
reshape_kv_cache,
|
||||
)
|
||||
from vllm.v1.outputs import KVConnectorOutput, ModelRunnerOutput
|
||||
from vllm.v1.request import RequestStatus
|
||||
from vllm.v1.worker.kv_connector_model_runner_mixin import KVConnectorModelRunnerMixin
|
||||
from vllm.v1.worker.utils import AttentionGroup
|
||||
|
||||
from .utils import (
|
||||
create_request,
|
||||
@@ -100,6 +99,12 @@ def clear_kv_transfer():
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_kv_cache_layout():
|
||||
yield
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
|
||||
def get_default_xfer_telemetry(
|
||||
xferDurationS: float = 1,
|
||||
postDurationS: float = 1,
|
||||
@@ -350,8 +355,8 @@ def test_abort_immediately_remote_prefill_enqueues_empty_recv():
|
||||
)
|
||||
def test_kv_transfer_handshake(dist_init):
|
||||
"""Unit test for basic NixlConnector interface functionality."""
|
||||
from vllm.config import set_current_vllm_config
|
||||
|
||||
set_kv_cache_layout("BLHNC")
|
||||
# Test setup, we creates a scheduler that contains a NixlConnector
|
||||
# of role SCHEDULER, and expect it to be serving NixlAgentMetadata from
|
||||
# all workers of the instance.
|
||||
@@ -386,18 +391,19 @@ def test_kv_transfer_handshake(dist_init):
|
||||
kv_cache_spec = cast(
|
||||
AttentionSpec, kv_cache_config.kv_cache_groups[0].kv_cache_spec
|
||||
)
|
||||
kv_cache_shape = FlashAttentionBackend.get_kv_cache_shape(
|
||||
num_blocks=kv_cache_config.num_blocks,
|
||||
block_size=kv_cache_spec.block_size,
|
||||
num_kv_heads=kv_cache_spec.num_kv_heads,
|
||||
head_size=kv_cache_spec.head_size,
|
||||
raw = torch.zeros(
|
||||
kv_cache_spec.page_size_bytes * kv_cache_config.num_blocks * 3,
|
||||
dtype=torch.int8,
|
||||
)
|
||||
caches = reshape_kv_cache(
|
||||
raw,
|
||||
kv_cache_spec,
|
||||
kv_cache_config.num_blocks,
|
||||
num_layer_slots=3,
|
||||
layout=KVCacheLayout.BLHNC,
|
||||
)
|
||||
shared_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
|
||||
unique_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
|
||||
kv_caches = {
|
||||
"layer0": shared_tensor,
|
||||
"layer1": unique_tensor,
|
||||
"layer2": shared_tensor,
|
||||
f"layer{layer_idx}": cache for layer_idx, cache in enumerate(caches)
|
||||
}
|
||||
prefill_connector.register_kv_caches(kv_caches)
|
||||
|
||||
@@ -471,7 +477,7 @@ class FakeNixlConnectorWorker(NixlConnectorWorker):
|
||||
self,
|
||||
*args,
|
||||
hand_shake_latency: float = 1.8,
|
||||
kv_cache_layout="HND",
|
||||
kv_cache_layout="LBHNC",
|
||||
kv_cache_config=None,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -482,9 +488,8 @@ class FakeNixlConnectorWorker(NixlConnectorWorker):
|
||||
self.kv_cache_layout = kv_cache_layout
|
||||
# Mock register_kv_caches attribute needed for tests that do not call it.
|
||||
self.src_xfer_handles_by_block_size = {self.block_size: 1}
|
||||
test_shape = self.attn_backends[0].get_kv_cache_shape(
|
||||
num_blocks=1, block_size=16, num_kv_heads=1, head_size=1
|
||||
)
|
||||
rep_spec = self.kv_cache_config.kv_cache_groups[0].kv_cache_spec
|
||||
test_shape = compute_layer_kv_cache_shape_bytes(rep_spec, 1)
|
||||
self.transfer_topo = TransferTopology(
|
||||
tp_rank=self.tp_rank,
|
||||
tp_size=self.world_size,
|
||||
@@ -498,7 +503,7 @@ class FakeNixlConnectorWorker(NixlConnectorWorker):
|
||||
)
|
||||
|
||||
self.compat_hash = compute_nixl_compatibility_hash(
|
||||
self.vllm_config, self.backend_name, self.transfer_topo.cross_layers_blocks
|
||||
self.vllm_config, self.backend_name
|
||||
)
|
||||
|
||||
def _nixl_handshake(
|
||||
@@ -549,9 +554,8 @@ class FakeNixlConnectorWorker(NixlConnectorWorker):
|
||||
device_id=remote_tp_rank,
|
||||
num_blocks=1,
|
||||
block_lens=remote_block_lens,
|
||||
# `self.kv_cache_layout` is only forced to HND when vllm engine
|
||||
# is started. We mock HND here.
|
||||
kv_cache_layout="HND",
|
||||
block_strides=remote_block_lens,
|
||||
kv_cache_layout="LBHNC",
|
||||
block_size=self.block_size,
|
||||
ssm_sizes=(0, 0),
|
||||
attn_backend_name=self.backend_name,
|
||||
@@ -600,7 +604,7 @@ class TestNixlHandshake:
|
||||
worker.dst_xfer_side_handles = {
|
||||
FakeNixlConnectorWorker.REMOTE_ENGINE_ID: {0: 1}
|
||||
}
|
||||
worker.kv_cache_layout = "HND"
|
||||
worker.kv_cache_layout = "LBHNC"
|
||||
num_xfers = 4
|
||||
while True:
|
||||
# For the same request_id, initiate multiple xfers across different
|
||||
@@ -996,7 +1000,9 @@ class TestNixlHandshake:
|
||||
worker.dst_num_blocks[worker.engine_id] = worker.num_blocks
|
||||
|
||||
# Metadata with different kv_cache_layout than local worker
|
||||
mismatched_layout = "HND" if worker.kv_cache_layout != "HND" else "NHD"
|
||||
mismatched_layout = (
|
||||
"LBHNC" if worker.kv_cache_layout != "LBHNC" else "LBNHC"
|
||||
)
|
||||
meta = NixlAgentMetadata(
|
||||
engine_id=FakeNixlConnectorWorker.REMOTE_ENGINE_ID,
|
||||
agent_metadata=FakeNixlWrapper.AGENT_METADATA,
|
||||
@@ -1004,6 +1010,7 @@ class TestNixlHandshake:
|
||||
device_id=0,
|
||||
num_blocks=1,
|
||||
block_lens=worker.block_len_per_layer,
|
||||
block_strides=worker.block_len_per_layer,
|
||||
kv_cache_layout=mismatched_layout,
|
||||
block_size=worker.block_size,
|
||||
ssm_sizes=(0, 0),
|
||||
@@ -1043,7 +1050,7 @@ class TestNixlHandshake:
|
||||
vllm_config,
|
||||
connector.engine_id,
|
||||
hand_shake_latency=0,
|
||||
kv_cache_layout="NHD",
|
||||
kv_cache_layout="LBNHC",
|
||||
)
|
||||
worker = connector.connector_worker
|
||||
|
||||
@@ -1054,15 +1061,16 @@ class TestNixlHandshake:
|
||||
worker.dst_num_blocks[worker.engine_id] = worker.num_blocks
|
||||
|
||||
# Metadata with different kv_cache_layout than local worker
|
||||
remote_block_lens = [i * 2 for i in worker.block_len_per_layer]
|
||||
meta = NixlAgentMetadata(
|
||||
engine_id=FakeNixlConnectorWorker.REMOTE_ENGINE_ID,
|
||||
agent_metadata=FakeNixlWrapper.AGENT_METADATA,
|
||||
kv_caches_base_addr=[0],
|
||||
device_id=0,
|
||||
num_blocks=1,
|
||||
# prefill TP=1, decode TP=2, remote block_lens is double to local
|
||||
block_lens=[i * 2 for i in worker.block_len_per_layer],
|
||||
kv_cache_layout="HND",
|
||||
block_lens=remote_block_lens,
|
||||
block_strides=remote_block_lens,
|
||||
kv_cache_layout="LBHNC",
|
||||
block_size=worker.block_size,
|
||||
ssm_sizes=(0, 0),
|
||||
attn_backend_name=worker.backend_name,
|
||||
@@ -1073,65 +1081,6 @@ class TestNixlHandshake:
|
||||
# whole block is moved.
|
||||
worker.add_remote_agent(meta, remote_tp_size=1)
|
||||
|
||||
@patch(
|
||||
"vllm.distributed.kv_transfer.kv_connector.v1.nixl.base_worker.NixlWrapper",
|
||||
FakeNixlWrapper,
|
||||
)
|
||||
def test_hybrid_mamba_attention_remote_descs_use_packed_head_slices(
|
||||
self, default_vllm_config, dist_init
|
||||
):
|
||||
worker = FakeNixlConnectorWorker(
|
||||
create_vllm_config(), "engine", hand_shake_latency=0
|
||||
)
|
||||
|
||||
remote_block_len = 2048
|
||||
local_block_len = remote_block_len // 2
|
||||
worker.block_len_per_layer = [local_block_len]
|
||||
worker._region_is_mla = [False]
|
||||
worker.num_blocks = 1
|
||||
worker.num_regions = 1
|
||||
worker._has_mamba = True
|
||||
worker._mamba_ssm_size = (128, 256)
|
||||
worker.transfer_topo = TransferTopology(
|
||||
tp_rank=1,
|
||||
tp_size=2,
|
||||
block_size=worker.block_size,
|
||||
engine_id=worker.engine_id,
|
||||
is_mla=False,
|
||||
is_mamba=True,
|
||||
total_num_kv_heads=2,
|
||||
attn_backends=worker.attn_backends,
|
||||
tensor_shape=None,
|
||||
)
|
||||
assert worker.transfer_topo.virtually_split_kv_in_blocks
|
||||
|
||||
plan = MagicMock(
|
||||
source_ranks_per_group=((0,), (0,)),
|
||||
rank_offset_factor=1,
|
||||
)
|
||||
meta = MagicMock(
|
||||
kv_caches_base_addr=[0x1000],
|
||||
device_id=0,
|
||||
num_blocks=1,
|
||||
block_lens=[remote_block_len],
|
||||
)
|
||||
|
||||
assert worker.get_backend_aware_kv_block_len(0, mamba_view=False) == (
|
||||
local_block_len
|
||||
)
|
||||
assert (
|
||||
worker.get_backend_aware_kv_block_len(0, first_split=True, mamba_view=True)
|
||||
== worker._mamba_ssm_size[0]
|
||||
)
|
||||
assert (
|
||||
worker.get_backend_aware_kv_block_len(0, first_split=False, mamba_view=True)
|
||||
== worker._mamba_ssm_size[1]
|
||||
)
|
||||
|
||||
assert worker._build_fa_remote(plan, meta, block_size_ratio=1).tolist() == [
|
||||
[0x1000 + local_block_len, local_block_len, 0]
|
||||
]
|
||||
|
||||
@patch(
|
||||
"vllm.distributed.kv_transfer.kv_connector.v1.nixl.base_worker.NixlWrapper",
|
||||
FakeNixlWrapper,
|
||||
@@ -1181,6 +1130,7 @@ class TestNixlHandshake:
|
||||
device_id=0,
|
||||
num_blocks=1,
|
||||
block_lens=[fa_len * tp_ratio, idx_len],
|
||||
block_strides=[fa_len * tp_ratio, idx_len],
|
||||
kv_cache_layout=worker.kv_cache_layout,
|
||||
block_size=worker.block_size,
|
||||
ssm_sizes=(0, 0),
|
||||
@@ -1207,6 +1157,7 @@ class TestNixlHandshake:
|
||||
num_blocks=1,
|
||||
# WRONG: MLA region scaled by tp_ratio (it should be replicated).
|
||||
block_lens=[fa_len * tp_ratio, idx_len * tp_ratio],
|
||||
block_strides=[fa_len * tp_ratio, idx_len * tp_ratio],
|
||||
kv_cache_layout=worker2.kv_cache_layout,
|
||||
block_size=worker2.block_size,
|
||||
ssm_sizes=(0, 0),
|
||||
@@ -1250,7 +1201,7 @@ class TestNixlHandshake:
|
||||
|
||||
worker.transfer_topo.total_num_kv_heads = 8
|
||||
worker.transfer_topo.local_physical_heads = 1
|
||||
worker.kv_cache_layout = "HND"
|
||||
worker.kv_cache_layout = "LBHNC"
|
||||
|
||||
worker.slot_size_per_layer = [4096]
|
||||
worker.block_len_per_layer = [4096 * worker.block_size]
|
||||
@@ -1266,7 +1217,8 @@ class TestNixlHandshake:
|
||||
device_id=0,
|
||||
num_blocks=1,
|
||||
block_lens=list(worker.block_len_per_layer),
|
||||
kv_cache_layout="HND",
|
||||
block_strides=list(worker.block_len_per_layer),
|
||||
kv_cache_layout="LBHNC",
|
||||
block_size=worker.block_size,
|
||||
ssm_sizes=(0, 0),
|
||||
attn_backend_name=worker.backend_name,
|
||||
@@ -1305,7 +1257,7 @@ class TestNixlHandshake:
|
||||
|
||||
worker.transfer_topo.total_num_kv_heads = 32
|
||||
worker.transfer_topo.local_physical_heads = 8 # 32 // 4
|
||||
worker.kv_cache_layout = "HND"
|
||||
worker.kv_cache_layout = "LBHNC"
|
||||
|
||||
slot_size = 4096
|
||||
worker.slot_size_per_layer = [slot_size]
|
||||
@@ -1323,7 +1275,8 @@ class TestNixlHandshake:
|
||||
device_id=0,
|
||||
num_blocks=1,
|
||||
block_lens=list(worker.block_len_per_layer),
|
||||
kv_cache_layout="HND",
|
||||
block_strides=list(worker.block_len_per_layer),
|
||||
kv_cache_layout="LBHNC",
|
||||
block_size=worker.block_size,
|
||||
ssm_sizes=(0, 0),
|
||||
attn_backend_name=worker.backend_name,
|
||||
@@ -1747,7 +1700,6 @@ def _run_abort_timeout_test(llm: LLM, timeout: int):
|
||||
llm.llm_engine.engine_core.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enable_cross_layers", ["False", "True"])
|
||||
@pytest.mark.parametrize(
|
||||
"attn_backend",
|
||||
[
|
||||
@@ -1761,8 +1713,14 @@ def _run_abort_timeout_test(llm: LLM, timeout: int):
|
||||
"TRITON_ATTN",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("layout", [layout.name for layout in KVCacheLayout])
|
||||
@pytest.mark.parametrize("separate_kv_head_groups", [False, True])
|
||||
def test_register_kv_caches(
|
||||
default_vllm_config, dist_init, attn_backend, enable_cross_layers
|
||||
default_vllm_config,
|
||||
dist_init,
|
||||
attn_backend,
|
||||
layout,
|
||||
separate_kv_head_groups,
|
||||
):
|
||||
"""
|
||||
Test that register_kv_caches() properly calls nixl_wrapper methods with
|
||||
@@ -1776,12 +1734,7 @@ def test_register_kv_caches(
|
||||
"""
|
||||
|
||||
vllm_config = create_vllm_config(attention_backend=attn_backend)
|
||||
|
||||
# Enable cross layers blocks
|
||||
vllm_config.kv_transfer_config.kv_connector_extra_config[
|
||||
"enable_cross_layers_blocks"
|
||||
] = enable_cross_layers
|
||||
set_kv_cache_layout("HND")
|
||||
set_kv_cache_layout(layout)
|
||||
|
||||
# Import the appropriate backend based on the parameter
|
||||
if attn_backend == "FLASH_ATTN":
|
||||
@@ -1798,59 +1751,34 @@ def test_register_kv_caches(
|
||||
backend_cls = TritonAttentionBackend
|
||||
|
||||
nixl_worker = "vllm.distributed.kv_transfer.kv_connector.v1.nixl.base_worker"
|
||||
nixl_connector = "vllm.distributed.kv_transfer.kv_connector.v1.nixl.connector"
|
||||
with (
|
||||
patch(f"{nixl_worker}.NixlWrapper") as mock_nixl_wrapper,
|
||||
patch(f"{nixl_worker}.threading.Event"),
|
||||
patch(f"{nixl_worker}.threading.Thread") as mock_thread,
|
||||
patch(f"{nixl_connector}.get_current_attn_backend") as mock_get_attn_backend,
|
||||
patch(f"{nixl_worker}.get_current_attn_backends") as mock_get_attn_backends,
|
||||
):
|
||||
# Ensure get_attn_backend returns the correct value due to
|
||||
# _cached_get_attn_backend returning the backend from previous
|
||||
# test run if not mocking.
|
||||
mock_get_attn_backend.return_value = backend_cls
|
||||
mock_get_attn_backends.return_value = [backend_cls]
|
||||
num_layers = 32
|
||||
block_size = 16
|
||||
num_blocks = 8
|
||||
block_size = 16
|
||||
num_heads = 4
|
||||
head_size = 16
|
||||
|
||||
# TODO (NickLucche) the fact that connector depends on kv_cache_config for init
|
||||
# but cross-layer preference cant be inferred prior to creating kv_cache_config
|
||||
# is a bit awkward.
|
||||
dummy_connector = NixlConnector(
|
||||
vllm_config,
|
||||
KVConnectorRole.WORKER,
|
||||
make_kv_cache_config(block_size=block_size),
|
||||
)
|
||||
kv_cache_spec = FullAttentionSpec(
|
||||
block_size=block_size,
|
||||
num_kv_heads=num_heads,
|
||||
head_size=head_size,
|
||||
dtype=torch.float16,
|
||||
separate_kv_head_groups=separate_kv_head_groups,
|
||||
)
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=[],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(
|
||||
["layer0", "layer1", "layer2", "layer3"], kv_cache_spec
|
||||
)
|
||||
],
|
||||
)
|
||||
if dummy_connector.prefer_cross_layer_blocks:
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=kv_cache_spec.page_size_bytes * num_blocks,
|
||||
shared_by=["all-layers"],
|
||||
)
|
||||
for _ in range(num_layers)
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(["all-layers"], kv_cache_spec)],
|
||||
)
|
||||
else:
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=[],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(["layer0", "layer1", "layer2"], kv_cache_spec)
|
||||
],
|
||||
)
|
||||
# Create connector
|
||||
connector = NixlConnector(vllm_config, KVConnectorRole.WORKER, kv_cache_config)
|
||||
connector.connector_worker = FakeNixlConnectorWorker(
|
||||
@@ -1870,93 +1798,60 @@ def test_register_kv_caches(
|
||||
# Reassure the shutdown() check that the thread is terminated
|
||||
mock_thread.return_value.is_alive.return_value = False
|
||||
|
||||
expected_tensor_size: int
|
||||
expected_base_addrs: list[int]
|
||||
expected_num_entries: int
|
||||
kv_caches: dict[str, torch.Tensor]
|
||||
if str(enable_cross_layers).lower() == "true":
|
||||
assert connector.prefer_cross_layer_blocks == (
|
||||
attn_backend in ("FLASH_ATTN", "FLASHINFER", "TRITON_ATTN")
|
||||
)
|
||||
else:
|
||||
assert not connector.prefer_cross_layer_blocks
|
||||
|
||||
test_shape = backend_cls.get_kv_cache_shape(
|
||||
num_blocks=1, block_size=16, num_kv_heads=1, head_size=1
|
||||
raw0 = torch.zeros(
|
||||
kv_cache_spec.page_size_bytes * kv_cache_config.num_blocks * 2,
|
||||
dtype=torch.int8,
|
||||
device=current_platform.device_type,
|
||||
)
|
||||
is_blocks_first = len(test_shape) == 4 and test_shape[0] == 1
|
||||
raw1 = torch.zeros(
|
||||
kv_cache_spec.page_size_bytes * kv_cache_config.num_blocks,
|
||||
dtype=torch.int8,
|
||||
device=current_platform.device_type,
|
||||
)
|
||||
tensor0, tensor1 = reshape_kv_cache(
|
||||
raw0,
|
||||
kv_cache_spec,
|
||||
kv_cache_config.num_blocks,
|
||||
num_layer_slots=2,
|
||||
layout=KVCacheLayout[layout],
|
||||
)
|
||||
(tensor2,) = reshape_kv_cache(
|
||||
raw1,
|
||||
kv_cache_spec,
|
||||
kv_cache_config.num_blocks,
|
||||
num_layer_slots=1,
|
||||
layout=KVCacheLayout[layout],
|
||||
)
|
||||
kv_caches = {
|
||||
"layer0": tensor0,
|
||||
"layer1": tensor1,
|
||||
"layer2": tensor2,
|
||||
"layer3": tensor0,
|
||||
}
|
||||
|
||||
if connector.prefer_cross_layer_blocks:
|
||||
with set_current_vllm_config(vllm_config):
|
||||
_, cross_layers_kv_cache, _ = (
|
||||
KVConnectorModelRunnerMixin.allocate_uniform_kv_caches(
|
||||
kv_cache_config=kv_cache_config,
|
||||
attn_groups=[
|
||||
[
|
||||
AttentionGroup(
|
||||
backend=backend_cls,
|
||||
layer_names=[],
|
||||
kv_cache_spec=kv_cache_spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
]
|
||||
],
|
||||
cache_dtype="bfloat16",
|
||||
device=torch.accelerator.current_device_index(),
|
||||
kernel_block_sizes=[block_size],
|
||||
)
|
||||
)
|
||||
# Store tensor info for validation
|
||||
expected_tensor_size = (
|
||||
cross_layers_kv_cache.element_size() * cross_layers_kv_cache.numel()
|
||||
)
|
||||
if separate_kv_head_groups:
|
||||
expected_base_addrs = [
|
||||
cross_layers_kv_cache.data_ptr(),
|
||||
cache[:, head_idx].data_ptr()
|
||||
for cache in (tensor0, tensor1, tensor2)
|
||||
for head_idx in range(cache.shape[1])
|
||||
]
|
||||
expected_num_entries = 1
|
||||
|
||||
expected_blocks_count = num_blocks
|
||||
|
||||
kv_caches = {"all-layers": cross_layers_kv_cache}
|
||||
expected_block_len = block_size * head_size * torch.float16.itemsize
|
||||
expected_blocks_count = num_blocks * len(expected_base_addrs)
|
||||
elif layout in ("LBHNC", "LBNHC", "BLHNC"):
|
||||
expected_base_addrs = [
|
||||
tensor0.data_ptr(),
|
||||
tensor1.data_ptr(),
|
||||
tensor2.data_ptr(),
|
||||
]
|
||||
expected_block_len = kv_cache_spec.page_size_bytes
|
||||
expected_blocks_count = kv_cache_config.num_blocks * 3
|
||||
else:
|
||||
# Create test kv cache tensors using proper backend shape
|
||||
kv_cache_shape = backend_cls.get_kv_cache_shape(
|
||||
num_blocks=kv_cache_config.num_blocks,
|
||||
block_size=kv_cache_spec.block_size,
|
||||
num_kv_heads=kv_cache_spec.num_kv_heads,
|
||||
head_size=kv_cache_spec.head_size,
|
||||
)
|
||||
shared_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
|
||||
unique_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
|
||||
kv_caches = {
|
||||
"layer0": shared_tensor,
|
||||
"layer1": unique_tensor,
|
||||
"layer2": shared_tensor,
|
||||
expected_base_addrs = [raw0.data_ptr(), raw1.data_ptr()]
|
||||
expected_block_len = {
|
||||
raw0.nbytes // num_blocks,
|
||||
raw1.nbytes // num_blocks,
|
||||
}
|
||||
|
||||
# Store tensor info for validation
|
||||
if is_blocks_first:
|
||||
expected_tensor_size = (
|
||||
shared_tensor.element_size() * shared_tensor.numel()
|
||||
)
|
||||
expected_base_addrs = [
|
||||
shared_tensor.data_ptr(),
|
||||
unique_tensor.data_ptr(),
|
||||
]
|
||||
expected_num_entries = 2
|
||||
expected_blocks_count = kv_cache_config.num_blocks * 2
|
||||
else:
|
||||
expected_tensor_size = (
|
||||
shared_tensor[0].element_size() * shared_tensor[0].numel()
|
||||
)
|
||||
expected_base_addrs = [
|
||||
shared_tensor[0].data_ptr(),
|
||||
shared_tensor[1].data_ptr(),
|
||||
unique_tensor[0].data_ptr(),
|
||||
unique_tensor[1].data_ptr(),
|
||||
]
|
||||
expected_num_entries = 4
|
||||
expected_blocks_count = kv_cache_config.num_blocks * 4
|
||||
expected_blocks_count = kv_cache_config.num_blocks * 2
|
||||
|
||||
# Execute register_kv_caches
|
||||
connector.register_kv_caches(kv_caches)
|
||||
@@ -1964,17 +1859,12 @@ def test_register_kv_caches(
|
||||
# Verify get_reg_descs was called with caches_data
|
||||
assert mock_wrapper_instance.get_reg_descs.called
|
||||
caches_data, _ = mock_wrapper_instance.get_reg_descs.call_args[0]
|
||||
assert len(caches_data) == expected_num_entries
|
||||
assert len(caches_data) == 2
|
||||
|
||||
for i, cache_entry in enumerate(caches_data):
|
||||
for cache_entry, raw in zip(caches_data, (raw0, raw1)):
|
||||
base_addr, size, _tp_rank, _ = cache_entry
|
||||
assert size == expected_tensor_size, (
|
||||
f"Entry {i}: Expected tensor size {expected_tensor_size}, got {size}"
|
||||
)
|
||||
assert base_addr == expected_base_addrs[i], (
|
||||
f"Entry {i}: Expected base address {expected_base_addrs[i]}, "
|
||||
f"got {base_addr}"
|
||||
)
|
||||
assert size == raw.nbytes
|
||||
assert base_addr == raw.data_ptr()
|
||||
|
||||
# Verify get_xfer_descs was called with blocks_data
|
||||
assert mock_wrapper_instance.get_xfer_descs.called
|
||||
@@ -1985,19 +1875,19 @@ def test_register_kv_caches(
|
||||
f"Expected {expected_blocks_count} blocks, got {len(blocks_data)}"
|
||||
)
|
||||
|
||||
if connector.prefer_cross_layer_blocks:
|
||||
num_blocks = 8
|
||||
else:
|
||||
num_blocks = kv_cache_config.num_blocks
|
||||
|
||||
expected_block_len = expected_tensor_size // num_blocks
|
||||
|
||||
for i, block_entry in enumerate(blocks_data):
|
||||
block_start_addr, block_len, tp_rank = block_entry
|
||||
assert block_len == expected_block_len, (
|
||||
f"Block entry {i}: Expected block len {expected_block_len}, "
|
||||
f"got {block_len}"
|
||||
)
|
||||
if isinstance(expected_block_len, set):
|
||||
assert block_len in expected_block_len
|
||||
else:
|
||||
assert block_len == expected_block_len
|
||||
|
||||
assert (
|
||||
connector.connector_worker.kv_caches_base_addr[
|
||||
connector.connector_worker.engine_id
|
||||
][0]
|
||||
== expected_base_addrs
|
||||
)
|
||||
|
||||
assert connector.connector_worker.block_size == 16
|
||||
|
||||
@@ -2226,6 +2116,8 @@ def test_engine_ttl_disabled(default_vllm_config, dist_init):
|
||||
|
||||
def test_transfer_topology_unregister():
|
||||
"""TransferTopology.unregister_remote_engine removes the engine."""
|
||||
from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend
|
||||
|
||||
topo = TransferTopology(
|
||||
tp_rank=0,
|
||||
tp_size=1,
|
||||
@@ -2820,14 +2712,11 @@ def test_compatibility_hash_validation(
|
||||
kv_cache_spec = cast(
|
||||
AttentionSpec, kv_cache_config.kv_cache_groups[0].kv_cache_spec
|
||||
)
|
||||
kv_cache_shape = decode_worker.attn_backends[0].get_kv_cache_shape(
|
||||
num_blocks=kv_cache_config.num_blocks,
|
||||
block_size=kv_cache_spec.block_size,
|
||||
num_kv_heads=kv_cache_spec.num_kv_heads,
|
||||
head_size=kv_cache_spec.head_size,
|
||||
shape = compute_layer_kv_cache_shape_bytes(
|
||||
kv_cache_spec, kv_cache_config.num_blocks
|
||||
)
|
||||
shared_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
|
||||
unique_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
|
||||
shared_tensor = torch.zeros(*shape, dtype=torch.int8).view(kv_cache_spec.dtype)
|
||||
unique_tensor = torch.zeros(*shape, dtype=torch.int8).view(kv_cache_spec.dtype)
|
||||
# Build kv_caches from the actual layer names in kv_cache_config so that
|
||||
# _layer_specs lookups in register_kv_caches always find a matching key.
|
||||
layer_names = [
|
||||
@@ -2862,18 +2751,19 @@ def test_compatibility_hash_validation(
|
||||
remote_hash = compute_nixl_compatibility_hash(
|
||||
remote_vllm_config,
|
||||
decode_worker.backend_name,
|
||||
decode_worker.transfer_topo.cross_layers_blocks,
|
||||
)
|
||||
|
||||
prefill_block_size = config_overrides.get("block_size", 16)
|
||||
prefill_block_lens = [4096 * prefill_block_size]
|
||||
prefill_metadata = NixlAgentMetadata(
|
||||
engine_id=FakeNixlConnectorWorker.REMOTE_ENGINE_ID,
|
||||
agent_metadata=FakeNixlWrapper.AGENT_METADATA,
|
||||
kv_caches_base_addr=[0],
|
||||
device_id=0,
|
||||
num_blocks=1,
|
||||
block_lens=[4096 * prefill_block_size], # slot_size * block_size
|
||||
kv_cache_layout="HND",
|
||||
block_lens=prefill_block_lens,
|
||||
block_strides=prefill_block_lens,
|
||||
kv_cache_layout="LBHNC",
|
||||
block_size=prefill_block_size,
|
||||
ssm_sizes=(0, 0),
|
||||
attn_backend_name=decode_worker.backend_name,
|
||||
@@ -2950,9 +2840,8 @@ def test_handshake_decode_errors(default_vllm_config, dist_init, error_scenario)
|
||||
decode_worker = decode_connector.connector_worker
|
||||
|
||||
backend = get_current_attn_backend(local_vllm_config)
|
||||
test_shape = backend.get_kv_cache_shape(
|
||||
num_blocks=1, block_size=16, num_kv_heads=1, head_size=1
|
||||
)
|
||||
probe_spec = decode_worker.kv_cache_config.kv_cache_groups[0].kv_cache_spec
|
||||
test_shape = compute_layer_kv_cache_shape_bytes(probe_spec, 1)
|
||||
decode_worker.transfer_topo = TransferTopology(
|
||||
tp_rank=decode_worker.tp_rank,
|
||||
tp_size=decode_worker.world_size,
|
||||
@@ -2968,7 +2857,6 @@ def test_handshake_decode_errors(default_vllm_config, dist_init, error_scenario)
|
||||
decode_worker.compat_hash = compute_nixl_compatibility_hash(
|
||||
decode_worker.vllm_config,
|
||||
decode_worker.backend_name,
|
||||
decode_worker.transfer_topo.cross_layers_blocks,
|
||||
)
|
||||
|
||||
if error_scenario == "handshake_decode_error":
|
||||
|
||||
@@ -12,14 +12,7 @@ pytestmark = pytest.mark.cpu_test
|
||||
|
||||
|
||||
class _FakeAttentionBackend:
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
) -> tuple[int, int, int, int]:
|
||||
return (num_blocks, num_kv_heads, block_size, 2 * head_size)
|
||||
pass
|
||||
|
||||
|
||||
def _make_topology(
|
||||
|
||||
@@ -169,9 +169,7 @@ def _make_kv_cache_config():
|
||||
head_size = 1
|
||||
dtype = torch.float32
|
||||
page_size = 2 * num_kv_heads * head_size * torch.finfo(dtype).bits // 8
|
||||
kv_tensor = KVCacheTensor(
|
||||
size=num_blocks * page_size, shared_by=["layer"], block_stride=0
|
||||
)
|
||||
kv_tensor = KVCacheTensor(size=num_blocks * page_size, shared_by=[["layer"]])
|
||||
return KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=[kv_tensor],
|
||||
@@ -189,22 +187,12 @@ def _make_kv_cache_config():
|
||||
)
|
||||
|
||||
|
||||
def _make_sizing_kv_cache_config(packed: bool) -> KVCacheConfig:
|
||||
def _make_sizing_kv_cache_config() -> KVCacheConfig:
|
||||
num_blocks = 4
|
||||
if packed:
|
||||
kv_cache_tensors = [
|
||||
KVCacheTensor(
|
||||
size=64,
|
||||
shared_by=[layer_name],
|
||||
block_stride=16,
|
||||
)
|
||||
for layer_name in ("layer0", "layer1")
|
||||
]
|
||||
else:
|
||||
kv_cache_tensors = [
|
||||
KVCacheTensor(size=40, shared_by=["layer0"]),
|
||||
KVCacheTensor(size=24, shared_by=["layer1"]),
|
||||
]
|
||||
kv_cache_tensors = [
|
||||
KVCacheTensor(size=40, shared_by=[["layer0"]]),
|
||||
KVCacheTensor(size=24, shared_by=[["layer1"]]),
|
||||
]
|
||||
|
||||
return KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
@@ -227,8 +215,8 @@ def _make_hybrid_kv_cache_config() -> KVCacheConfig:
|
||||
return KVCacheConfig(
|
||||
num_blocks=4,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=40, shared_by=["full_layer"]),
|
||||
KVCacheTensor(size=24, shared_by=["mla_layer"]),
|
||||
KVCacheTensor(size=40, shared_by=[["full_layer"]]),
|
||||
KVCacheTensor(size=24, shared_by=[["mla_layer"]]),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(
|
||||
@@ -325,8 +313,7 @@ def test_create_cpu_offloading_spec_end_to_end():
|
||||
assert spec.num_blocks > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("packed", [False, True])
|
||||
def test_cpu_spec_sizing_preserves_tensor_layout(packed: bool):
|
||||
def test_cpu_spec_sizing_preserves_tensor_layout():
|
||||
cpu_bytes_to_use = 1920
|
||||
config = _make_layout_vllm_config(
|
||||
cpu_bytes_to_use=cpu_bytes_to_use,
|
||||
@@ -335,7 +322,7 @@ def test_cpu_spec_sizing_preserves_tensor_layout(packed: bool):
|
||||
pipeline_parallel_size=2,
|
||||
)
|
||||
|
||||
spec = _create_spec(config, _make_sizing_kv_cache_config(packed))
|
||||
spec = _create_spec(config, _make_sizing_kv_cache_config())
|
||||
|
||||
assert isinstance(spec, CPUOffloadingSpec)
|
||||
assert spec.cpu_page_size_per_worker == 32
|
||||
@@ -343,29 +330,6 @@ def test_cpu_spec_sizing_preserves_tensor_layout(packed: bool):
|
||||
assert spec.num_blocks == cpu_bytes_to_use // 192
|
||||
|
||||
|
||||
def test_cpu_spec_rejects_partially_packed_tensor_layout():
|
||||
config = _make_layout_vllm_config(cpu_bytes_to_use=65536)
|
||||
kv_cache_config = _make_sizing_kv_cache_config(packed=False)
|
||||
kv_cache_config.kv_cache_tensors[0].block_stride = 16
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
_create_spec(config, kv_cache_config)
|
||||
|
||||
|
||||
def test_cpu_spec_zero_blocks_skips_tensor_layout_validation():
|
||||
config = _make_layout_vllm_config(cpu_bytes_to_use=65536)
|
||||
kv_cache_config = _make_sizing_kv_cache_config(packed=False)
|
||||
kv_cache_config.num_blocks = 0
|
||||
kv_cache_config.kv_cache_tensors[0].block_stride = 16
|
||||
|
||||
spec = _create_spec(config, kv_cache_config)
|
||||
|
||||
assert isinstance(spec, CPUOffloadingSpec)
|
||||
assert spec.cpu_page_size_per_worker == 0
|
||||
assert spec.kv_bytes_per_chunk == 0
|
||||
assert spec.num_blocks == 0
|
||||
|
||||
|
||||
def test_tiering_spec_aligns_row_size():
|
||||
alignment = SharedOffloadRegion.BLOCK_SIZE_ALIGNMENT
|
||||
cpu_bytes_to_use = alignment * 3
|
||||
@@ -377,7 +341,7 @@ def test_tiering_spec_aligns_row_size():
|
||||
pipeline_parallel_size=2,
|
||||
)
|
||||
|
||||
spec = _create_spec(config, _make_sizing_kv_cache_config(packed=False))
|
||||
spec = _create_spec(config, _make_sizing_kv_cache_config())
|
||||
|
||||
assert isinstance(spec, TieringOffloadingSpec)
|
||||
assert spec.cpu_page_size_per_worker == 32
|
||||
|
||||
@@ -91,7 +91,7 @@ def _make_kv_cache_config(
|
||||
tensors.append(
|
||||
KVCacheTensor(
|
||||
size=_BYTES_PER_BLOCK * num_blocks,
|
||||
shared_by=layer_names,
|
||||
shared_by=[layer_names],
|
||||
)
|
||||
)
|
||||
return KVCacheConfig(
|
||||
|
||||
@@ -10,6 +10,7 @@ read partially written / stale blocks and silently corrupt the CPU cache.
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
@@ -19,6 +20,12 @@ from vllm.platforms import current_platform
|
||||
if not current_platform.is_cuda_alike():
|
||||
pytest.skip("Requires CUDA or ROCm", allow_module_level=True)
|
||||
|
||||
from vllm.v1.attention.backends.utils import set_kv_cache_layout
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheLayout,
|
||||
reshape_kv_cache,
|
||||
)
|
||||
from vllm.v1.simple_kv_offload.copy_backend import DmaCopyBackend
|
||||
from vllm.v1.simple_kv_offload.cuda_mem_ops import (
|
||||
CU_MEMCPY_SRC_ACCESS_ORDER_ANY,
|
||||
@@ -181,3 +188,88 @@ def test_build_params_src_access_order():
|
||||
gpu, cpu, stream, src_access_order=CU_MEMCPY_SRC_ACCESS_ORDER_STREAM
|
||||
)
|
||||
assert ordered.attrs.srcAccessOrder == CU_MEMCPY_SRC_ACCESS_ORDER_STREAM
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layout", list(KVCacheLayout))
|
||||
def test_register_shared_kv_cache_storage(monkeypatch, layout: KVCacheLayout):
|
||||
num_blocks = 4
|
||||
num_layers = 2
|
||||
spec = FullAttentionSpec(
|
||||
block_size=2,
|
||||
num_kv_heads=2,
|
||||
head_size=2,
|
||||
dtype=torch.float16,
|
||||
)
|
||||
raw = torch.zeros(
|
||||
num_blocks * num_layers * spec.page_size_bytes,
|
||||
dtype=torch.int8,
|
||||
device="cuda",
|
||||
)
|
||||
caches = reshape_kv_cache(raw, spec, num_blocks, num_layers, layout)
|
||||
cache_config = MagicMock(num_blocks=num_blocks)
|
||||
worker = SimpleCPUOffloadWorker(
|
||||
vllm_config=None,
|
||||
kv_cache_config=cache_config,
|
||||
cpu_capacity_bytes=raw.nbytes,
|
||||
)
|
||||
worker._backend = MagicMock()
|
||||
monkeypatch.setattr("vllm.v1.simple_kv_offload.worker.PIN_MEMORY", False)
|
||||
|
||||
set_kv_cache_layout(layout.name)
|
||||
try:
|
||||
worker.register_kv_caches(
|
||||
{f"layer.{layer_idx}": cache for layer_idx, cache in enumerate(caches)}
|
||||
)
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
assert worker.gpu_kv_caches is not None
|
||||
expected_regions = num_layers if layout.is_layer_compact else 1
|
||||
assert len(worker.gpu_kv_caches) == expected_regions
|
||||
expected_block_bytes = spec.page_size_bytes * (
|
||||
1 if layout.is_layer_compact else num_layers
|
||||
)
|
||||
assert {cache.shape for cache in worker.gpu_kv_caches.values()} == {
|
||||
(num_blocks, expected_block_bytes)
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layout", [KVCacheLayout.BLHNC, KVCacheLayout.BHLNC])
|
||||
def test_register_separate_kv_head_groups(monkeypatch, layout: KVCacheLayout):
|
||||
num_blocks = 4
|
||||
num_layers = 2
|
||||
spec = FullAttentionSpec(
|
||||
block_size=2,
|
||||
num_kv_heads=2,
|
||||
head_size=2,
|
||||
dtype=torch.float16,
|
||||
separate_kv_head_groups=True,
|
||||
)
|
||||
raw = torch.zeros(
|
||||
num_blocks * num_layers * spec.page_size_bytes,
|
||||
dtype=torch.int8,
|
||||
device="cuda",
|
||||
)
|
||||
caches = reshape_kv_cache(raw, spec, num_blocks, num_layers, layout)
|
||||
worker = SimpleCPUOffloadWorker(
|
||||
vllm_config=None,
|
||||
kv_cache_config=MagicMock(num_blocks=num_blocks),
|
||||
cpu_capacity_bytes=raw.nbytes,
|
||||
)
|
||||
worker._backend = MagicMock()
|
||||
monkeypatch.setattr("vllm.v1.simple_kv_offload.worker.PIN_MEMORY", False)
|
||||
|
||||
set_kv_cache_layout(layout.name)
|
||||
try:
|
||||
worker.register_kv_caches(
|
||||
{f"layer.{layer_idx}": cache for layer_idx, cache in enumerate(caches)}
|
||||
)
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
assert worker.gpu_kv_caches is not None
|
||||
assert len(worker.gpu_kv_caches) == num_layers * spec.num_heads
|
||||
per_head_block_bytes = spec.block_size * spec.head_size * spec.dtype.itemsize
|
||||
assert {cache.shape for cache in worker.gpu_kv_caches.values()} == {
|
||||
(num_blocks, per_head_block_bytes)
|
||||
}
|
||||
|
||||
+206
-207
@@ -1,42 +1,32 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Padded-page handling in reshape_kv_cache.
|
||||
|
||||
Guards that a page_size_padded spec strides the block dimension by the
|
||||
padded page while keeping per-block content compact, so padding bytes at
|
||||
the end of each page are never addressed by the logical view.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVQuantMode
|
||||
from vllm.v1.worker.gpu.attn_utils import _reshape_kv_cache
|
||||
from vllm.v1.worker.utils import AttentionGroup
|
||||
from vllm.v1.attention.backends.utils import set_kv_cache_layout
|
||||
from vllm.v1.core.kv_cache_utils import KVCacheBlockCopy
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheConfig,
|
||||
KVCacheGroupSpec,
|
||||
KVCacheLayout,
|
||||
KVCacheTensor,
|
||||
KVQuantMode,
|
||||
MLAAttentionSpec,
|
||||
reshape_kv_cache,
|
||||
)
|
||||
from vllm.v1.worker.gpu.attn_utils import _allocate_and_reshape_kv_cache
|
||||
from vllm.v1.worker.utils import copy_kv_cache_blocks_inplace
|
||||
|
||||
|
||||
class FakeFlashAttentionBackend:
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (num_blocks, 2, block_size, num_kv_heads, head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
assert not include_num_layers_dimension
|
||||
return (0, 1, 2, 3, 4)
|
||||
|
||||
|
||||
class FakeHNDFlashAttentionBackend(FakeFlashAttentionBackend):
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
assert not include_num_layers_dimension
|
||||
return (0, 1, 3, 2, 4)
|
||||
|
||||
|
||||
def test_reshape_padded_flash_attention_kv_cache_strides_by_page():
|
||||
def test_reshape_padded_kv_cache_strides_by_padded_page():
|
||||
num_blocks = 3
|
||||
spec = FullAttentionSpec(
|
||||
block_size=16,
|
||||
@@ -47,163 +37,20 @@ def test_reshape_padded_flash_attention_kv_cache_strides_by_page():
|
||||
)
|
||||
assert spec.real_page_size_bytes == 256
|
||||
|
||||
raw_tensors = {
|
||||
"layer": torch.zeros(spec.page_size_bytes * num_blocks, dtype=torch.int8)
|
||||
}
|
||||
attn_groups = [
|
||||
AttentionGroup(
|
||||
backend=FakeFlashAttentionBackend,
|
||||
layer_names=["layer"],
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
]
|
||||
raw = torch.zeros(spec.page_size_bytes * num_blocks, dtype=torch.int8)
|
||||
(kv_cache,) = reshape_kv_cache(raw, spec, num_blocks, 1, KVCacheLayout.LBHNC)
|
||||
|
||||
kv_cache = _reshape_kv_cache(
|
||||
attn_groups,
|
||||
raw_tensors,
|
||||
"auto",
|
||||
[spec.block_size],
|
||||
{},
|
||||
)["layer"]
|
||||
|
||||
assert kv_cache.shape == (num_blocks, 2, 16, 1, 2)
|
||||
assert kv_cache.stride(0) == spec.page_size_bytes // 4
|
||||
assert kv_cache.stride(1) == spec.real_page_size_bytes // 2 // 4
|
||||
assert kv_cache[1, 0].storage_offset() == spec.page_size_bytes // 4
|
||||
assert (
|
||||
kv_cache[1, 1].storage_offset()
|
||||
== (spec.page_size_bytes + spec.real_page_size_bytes // 2) // 4
|
||||
)
|
||||
elem_size = 4 # float32
|
||||
# Content dim packs K and V: 2 * head_size.
|
||||
assert kv_cache.shape == (num_blocks, 1, 16, 2 * spec.head_size)
|
||||
assert kv_cache.dtype == spec.dtype
|
||||
assert kv_cache.stride(0) == spec.page_size_padded // elem_size
|
||||
assert kv_cache[1].storage_offset() == spec.page_size_padded // elem_size
|
||||
# Within one block the (unpadded) content stays compact.
|
||||
assert kv_cache[0].is_contiguous()
|
||||
|
||||
|
||||
def test_reshape_padded_hnd_flash_attention_kv_cache_strides_by_page():
|
||||
num_blocks = 3
|
||||
spec = FullAttentionSpec(
|
||||
block_size=16,
|
||||
num_kv_heads=3,
|
||||
head_size=2,
|
||||
dtype=torch.float32,
|
||||
page_size_padded=1024,
|
||||
)
|
||||
assert spec.real_page_size_bytes == 768
|
||||
|
||||
raw_tensors = {
|
||||
"layer": torch.zeros(spec.page_size_bytes * num_blocks, dtype=torch.int8)
|
||||
}
|
||||
attn_groups = [
|
||||
AttentionGroup(
|
||||
backend=FakeHNDFlashAttentionBackend,
|
||||
layer_names=["layer"],
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
]
|
||||
|
||||
kv_cache = _reshape_kv_cache(
|
||||
attn_groups,
|
||||
raw_tensors,
|
||||
"auto",
|
||||
[spec.block_size],
|
||||
{},
|
||||
)["layer"]
|
||||
|
||||
assert kv_cache.shape == (num_blocks, 2, 16, 3, 2)
|
||||
assert kv_cache.stride(0) == spec.page_size_bytes // 4
|
||||
assert kv_cache.stride(1) == spec.real_page_size_bytes // 2 // 4
|
||||
assert kv_cache.stride(2) == 2
|
||||
assert kv_cache.stride(3) == spec.block_size * spec.head_size
|
||||
assert kv_cache[1, 0].storage_offset() == spec.page_size_bytes // 4
|
||||
assert (
|
||||
kv_cache[1, 1].storage_offset()
|
||||
== (spec.page_size_bytes + spec.real_page_size_bytes // 2) // 4
|
||||
)
|
||||
assert (
|
||||
kv_cache[1, 1, 3, 2].storage_offset()
|
||||
== (
|
||||
spec.page_size_bytes
|
||||
+ spec.real_page_size_bytes // 2
|
||||
+ 3 * spec.head_size * 4
|
||||
+ 2 * spec.block_size * spec.head_size * 4
|
||||
)
|
||||
// 4
|
||||
)
|
||||
|
||||
|
||||
class FakeDiffKVBackend:
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (num_blocks, block_size, num_kv_heads, head_size * 2)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
assert not include_num_layers_dimension
|
||||
return (0, 1, 2, 3)
|
||||
|
||||
|
||||
def test_reshape_padded_diff_kv_cache_does_not_infer_kv_dim():
|
||||
num_blocks = 3
|
||||
spec = FullAttentionSpec(
|
||||
block_size=16,
|
||||
num_kv_heads=1,
|
||||
head_size=2,
|
||||
dtype=torch.float32,
|
||||
page_size_padded=384,
|
||||
)
|
||||
|
||||
raw_tensors = {
|
||||
"layer": torch.zeros(spec.page_size_bytes * num_blocks, dtype=torch.int8)
|
||||
}
|
||||
attn_groups = [
|
||||
AttentionGroup(
|
||||
backend=FakeDiffKVBackend,
|
||||
layer_names=["layer"],
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=0,
|
||||
)
|
||||
]
|
||||
|
||||
kv_cache = _reshape_kv_cache(
|
||||
attn_groups,
|
||||
raw_tensors,
|
||||
"auto",
|
||||
[spec.block_size],
|
||||
{},
|
||||
)["layer"]
|
||||
|
||||
assert kv_cache.shape == (num_blocks, 16, 1, 4)
|
||||
assert kv_cache.stride(0) == spec.page_size_bytes // 4
|
||||
assert kv_cache.stride(1) == 4
|
||||
|
||||
|
||||
class FakePerTokenScaleBackend:
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (num_blocks, 2, block_size, num_kv_heads, head_size + 4)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
assert not include_num_layers_dimension
|
||||
return (0, 1, 2, 3, 4)
|
||||
|
||||
|
||||
def test_reshape_padded_quantized_kv_cache_preserves_scale_stride():
|
||||
def test_reshape_padded_quantized_kv_cache_budgets_scale_bytes():
|
||||
num_blocks = 3
|
||||
spec = FullAttentionSpec(
|
||||
block_size=16,
|
||||
@@ -213,30 +60,182 @@ def test_reshape_padded_quantized_kv_cache_preserves_scale_stride():
|
||||
kv_quant_mode=KVQuantMode.INT8_PER_TOKEN_HEAD,
|
||||
page_size_padded=384,
|
||||
)
|
||||
# Per-token-head scales are budgeted into the page but live past the
|
||||
# real content, so the logical view must stride by the padded page.
|
||||
assert spec.real_page_size_bytes == 128
|
||||
assert spec.page_size_bytes == 384
|
||||
|
||||
raw_tensors = {
|
||||
"layer": torch.zeros(spec.page_size_bytes * num_blocks, dtype=torch.int8)
|
||||
}
|
||||
attn_groups = [
|
||||
AttentionGroup(
|
||||
backend=FakePerTokenScaleBackend,
|
||||
layer_names=["layer"],
|
||||
kv_cache_spec=spec,
|
||||
kv_cache_group_id=0,
|
||||
raw = torch.zeros(spec.page_size_bytes * num_blocks, dtype=torch.int8)
|
||||
(kv_cache,) = reshape_kv_cache(raw, spec, num_blocks, 1, KVCacheLayout.LBHNC)
|
||||
|
||||
assert kv_cache.shape == (num_blocks, 1, 16, 2 * spec.head_size)
|
||||
assert kv_cache.stride(0) == spec.page_size_padded
|
||||
assert kv_cache[1].storage_offset() == spec.page_size_padded
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("kernel_block_sizes", "expected_num_blocks", "expected_num_states"),
|
||||
[
|
||||
(None, 4, 64),
|
||||
([256], 4, 64),
|
||||
([64], 16, 16),
|
||||
],
|
||||
)
|
||||
def test_allocate_compressed_mla_cache(
|
||||
kernel_block_sizes: list[int] | None,
|
||||
expected_num_blocks: int,
|
||||
expected_num_states: int,
|
||||
):
|
||||
spec = MLAAttentionSpec(
|
||||
block_size=256,
|
||||
num_kv_heads=1,
|
||||
head_size=128,
|
||||
dtype=torch.bfloat16,
|
||||
tokens_per_state=4,
|
||||
)
|
||||
num_pages = 4
|
||||
config = KVCacheConfig(
|
||||
num_blocks=num_pages,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=num_pages * spec.page_size_bytes,
|
||||
shared_by=[["layer.0"]],
|
||||
)
|
||||
],
|
||||
kv_cache_groups=[KVCacheGroupSpec(["layer.0"], spec)],
|
||||
)
|
||||
|
||||
caches = _allocate_and_reshape_kv_cache(
|
||||
config,
|
||||
torch.device("cpu"),
|
||||
layout=KVCacheLayout.LBHNC,
|
||||
kernel_block_sizes=kernel_block_sizes,
|
||||
)
|
||||
|
||||
assert caches["layer.0"].shape == (
|
||||
expected_num_blocks,
|
||||
1,
|
||||
expected_num_states,
|
||||
128,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layout", list(KVCacheLayout))
|
||||
def test_copy_kv_cache_blocks_shared_storage(layout: KVCacheLayout):
|
||||
num_blocks = 4
|
||||
num_layers = 2
|
||||
spec = FullAttentionSpec(
|
||||
block_size=2,
|
||||
num_kv_heads=2,
|
||||
head_size=2,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
raw = torch.zeros(
|
||||
num_blocks * num_layers * spec.page_size_bytes,
|
||||
dtype=torch.int8,
|
||||
)
|
||||
caches = reshape_kv_cache(raw, spec, num_blocks, num_layers, layout)
|
||||
|
||||
for layer_idx, cache in enumerate(caches):
|
||||
for block_idx in range(num_blocks):
|
||||
cache[block_idx].fill_(10 * layer_idx + block_idx)
|
||||
|
||||
expected = [[cache[i].clone() for i in range(num_blocks)] for cache in caches]
|
||||
copies = [KVCacheBlockCopy(src_block_id=0, dst_block_id=2)]
|
||||
|
||||
set_kv_cache_layout(layout.name)
|
||||
try:
|
||||
copy_kv_cache_blocks_inplace(caches, num_blocks, copies)
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
for layer_idx, cache in enumerate(caches):
|
||||
torch.testing.assert_close(cache[2], expected[layer_idx][0])
|
||||
torch.testing.assert_close(cache[1], expected[layer_idx][1])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layout", [KVCacheLayout.BLHNC, KVCacheLayout.BHLNC])
|
||||
def test_copy_kv_cache_blocks_separate_head_groups(layout: KVCacheLayout):
|
||||
num_blocks = 4
|
||||
num_layers = 2
|
||||
spec = FullAttentionSpec(
|
||||
block_size=2,
|
||||
num_kv_heads=2,
|
||||
head_size=2,
|
||||
dtype=torch.float32,
|
||||
separate_kv_head_groups=True,
|
||||
)
|
||||
raw = torch.zeros(
|
||||
num_blocks * num_layers * spec.page_size_bytes,
|
||||
dtype=torch.int8,
|
||||
)
|
||||
caches = reshape_kv_cache(raw, spec, num_blocks, num_layers, layout)
|
||||
|
||||
for layer_idx, cache in enumerate(caches):
|
||||
for block_idx in range(num_blocks):
|
||||
for head_idx in range(cache.shape[1]):
|
||||
cache[block_idx, head_idx].fill_(
|
||||
100 * layer_idx + 10 * head_idx + block_idx
|
||||
)
|
||||
|
||||
expected = [[cache[i].clone() for i in range(num_blocks)] for cache in caches]
|
||||
set_kv_cache_layout(layout.name)
|
||||
try:
|
||||
copy_kv_cache_blocks_inplace(
|
||||
caches,
|
||||
num_blocks,
|
||||
[KVCacheBlockCopy(src_block_id=0, dst_block_id=2)],
|
||||
)
|
||||
]
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
kv_cache = _reshape_kv_cache(
|
||||
attn_groups,
|
||||
raw_tensors,
|
||||
"int8_per_token_head",
|
||||
[spec.block_size],
|
||||
{},
|
||||
)["layer"]
|
||||
for layer_idx, cache in enumerate(caches):
|
||||
torch.testing.assert_close(cache[2], expected[layer_idx][0])
|
||||
torch.testing.assert_close(cache[1], expected[layer_idx][1])
|
||||
|
||||
assert kv_cache.shape == (num_blocks, 2, 16, 1, 8)
|
||||
assert kv_cache.stride(0) == spec.page_size_bytes
|
||||
assert kv_cache.stride(1) == 16 * 1 * 8
|
||||
assert kv_cache[1, 1].storage_offset() == spec.page_size_bytes + 16 * 1 * 8
|
||||
|
||||
@pytest.mark.parametrize("layout", [KVCacheLayout.LBHNC, KVCacheLayout.BLHNC])
|
||||
def test_copy_kv_cache_blocks_with_virtual_block_splitting(layout: KVCacheLayout):
|
||||
num_blocks = 4
|
||||
num_layers = 2
|
||||
physical_per_logical = 2
|
||||
spec = FullAttentionSpec(
|
||||
block_size=4,
|
||||
num_kv_heads=1,
|
||||
head_size=2,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
raw = torch.zeros(
|
||||
num_blocks * num_layers * spec.page_size_bytes,
|
||||
dtype=torch.int8,
|
||||
)
|
||||
caches = reshape_kv_cache(
|
||||
raw,
|
||||
spec,
|
||||
num_blocks * physical_per_logical,
|
||||
num_layers,
|
||||
layout,
|
||||
block_size=spec.block_size // physical_per_logical,
|
||||
)
|
||||
|
||||
for layer_idx, cache in enumerate(caches):
|
||||
for block_idx in range(cache.shape[0]):
|
||||
cache[block_idx].fill_(100 * layer_idx + block_idx)
|
||||
expected = [[cache[i].clone() for i in range(cache.shape[0])] for cache in caches]
|
||||
|
||||
set_kv_cache_layout(layout.name)
|
||||
try:
|
||||
copy_kv_cache_blocks_inplace(
|
||||
caches,
|
||||
num_blocks,
|
||||
[KVCacheBlockCopy(src_block_id=0, dst_block_id=2)],
|
||||
)
|
||||
finally:
|
||||
set_kv_cache_layout(None)
|
||||
|
||||
dst_start = 2 * physical_per_logical
|
||||
for layer_idx, cache in enumerate(caches):
|
||||
for physical_idx in range(physical_per_logical):
|
||||
torch.testing.assert_close(
|
||||
cache[dst_start + physical_idx], expected[layer_idx][physical_idx]
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ def initialize_kv_cache(runner: GPUModelRunner):
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=NUM_BLOCKS,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=tensor_size, shared_by=["layer.0"]),
|
||||
KVCacheTensor(size=tensor_size, shared_by=[["layer.0"]]),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(layer_names=["layer.0"], kv_cache_spec=attn_spec)
|
||||
@@ -776,56 +776,6 @@ def test_update_states_pp_async_multi_request_keeps_rank_state_consistent(
|
||||
)
|
||||
|
||||
|
||||
def test_kv_cache_stride_order(monkeypatch, model_runner):
|
||||
# This test checks if GPUModelRunner initializes correctly when an attention
|
||||
# backend enforces a non-default KV cache stride order.
|
||||
n_heads = model_runner.model_config.get_num_kv_heads(model_runner.parallel_config)
|
||||
head_size = model_runner.model_config.get_head_size()
|
||||
|
||||
# Get the expected shape from the backend's get_kv_cache_shape method
|
||||
# to ensure compatibility with different backends (triton vs flexattention)
|
||||
attn_backend = None
|
||||
for attn_group in model_runner._attn_group_iterator():
|
||||
attn_backend = attn_group.backend
|
||||
break
|
||||
|
||||
assert attn_backend is not None, "No attention backend found"
|
||||
expected_kv_cache_shape = list(
|
||||
attn_backend.get_kv_cache_shape(NUM_BLOCKS, BLOCK_SIZE, n_heads, head_size)
|
||||
)
|
||||
|
||||
# TODO mla test
|
||||
default_stride = tuple(range(len(expected_kv_cache_shape)))
|
||||
non_default_stride = (*default_stride[1:], default_stride[0])
|
||||
# Permutation that gets you back to expected kv shape
|
||||
for test_stride in (non_default_stride, default_stride):
|
||||
|
||||
def rnd_stride_order(
|
||||
include_num_layers_dimension: bool = False, test_stride=test_stride
|
||||
):
|
||||
assert not include_num_layers_dimension
|
||||
return test_stride
|
||||
|
||||
# Patch the attention backend class and re-trigger the KV cache creation
|
||||
for attn_group in model_runner._attn_group_iterator():
|
||||
attn_backend = attn_group.backend
|
||||
monkeypatch.setattr(
|
||||
attn_backend, "get_kv_cache_stride_order", rnd_stride_order
|
||||
)
|
||||
|
||||
model_runner.attn_groups = []
|
||||
model_runner.kv_caches = []
|
||||
model_runner.initialize_kv_cache(model_runner.kv_cache_config)
|
||||
|
||||
# Shape is unchanged, but layout may differ
|
||||
kv_cache_shape = model_runner.kv_caches[0].shape
|
||||
assert list(kv_cache_shape) == expected_kv_cache_shape
|
||||
if default_stride == test_stride:
|
||||
assert all(kv.is_contiguous() for kv in model_runner.kv_caches)
|
||||
else:
|
||||
assert all(not kv.is_contiguous() for kv in model_runner.kv_caches)
|
||||
|
||||
|
||||
def test_update_config(model_runner):
|
||||
# Simple update
|
||||
model_runner.update_config({"load_config": {"load_format": "dummy"}})
|
||||
@@ -1030,21 +980,22 @@ def test_init_kv_cache_without_kv_sharing(default_vllm_config):
|
||||
vllm_config, [kv_cache_spec], [available_memory]
|
||||
)[0]
|
||||
assert kv_cache_config.num_blocks == num_expected_blocks
|
||||
assert len(kv_cache_config.kv_cache_tensors) == 2
|
||||
assert kv_cache_config.kv_cache_tensors[0].size == available_memory // 2
|
||||
assert kv_cache_config.kv_cache_tensors[1].size == available_memory // 2
|
||||
assert len(kv_cache_config.kv_cache_tensors) == 1
|
||||
assert kv_cache_config.kv_cache_tensors[0].size == available_memory
|
||||
|
||||
max_context_len = estimate_max_model_len(vllm_config, kv_cache_spec, 5 * GiB_bytes)
|
||||
# max context len with KV sharing should be 2x as large as without
|
||||
assert max_context_len == 1310720
|
||||
|
||||
# important: override tensor size to prevent large mem alloc during test
|
||||
# this will only allocate 2 block worth of memory (2 * 32kb)
|
||||
# this will only allocate 1 block worth of memory per slot (2 slots * 32kb)
|
||||
kv_cache_config.num_blocks = 1
|
||||
for kv_cache_tensor in kv_cache_config.kv_cache_tensors:
|
||||
kv_cache_tensor.size = kv_cache_spec[
|
||||
kv_cache_tensor.shared_by[0]
|
||||
].page_size_bytes
|
||||
num_layer_slots = len(kv_cache_tensor.shared_by)
|
||||
kv_cache_tensor.size = (
|
||||
kv_cache_spec[kv_cache_tensor.shared_by[0][0]].page_size_bytes
|
||||
* num_layer_slots
|
||||
)
|
||||
|
||||
runner.initialize_kv_cache(kv_cache_config)
|
||||
|
||||
@@ -1138,7 +1089,7 @@ def test_hybrid_attention_mamba_tensor_shapes():
|
||||
"""
|
||||
The GPU model runner creates different views into the
|
||||
KVCacheTensors for the attention and mamba layers
|
||||
(via _reshape_kv_cache_tensors function). This test verifies
|
||||
(via _allocate_kv_caches). This test verifies
|
||||
that the views are compatible: writing a mamba block
|
||||
will not corrupt an attention block and vice versa
|
||||
"""
|
||||
@@ -1301,10 +1252,9 @@ def test_hybrid_attention_mamba_tensor_shapes():
|
||||
actual_kv = vllm_ctx[layer].kv_cache[kernel_block, :]
|
||||
expected = attn_blocks_constant[i]
|
||||
|
||||
# Packed layout: (num_kv_heads, block_size, 2*head_size). Every
|
||||
# head in the block was filled with the same constant.
|
||||
for head_idx in range(actual_kv.shape[0]):
|
||||
assert torch.equal(actual_kv[head_idx], expected)
|
||||
# Check K and V separately
|
||||
assert torch.equal(actual_kv[0], expected)
|
||||
assert torch.equal(actual_kv[1], expected)
|
||||
|
||||
for layer in [layer_2, layer_3, layer_4, layer_5]:
|
||||
for i, kv_block in enumerate(kv_blocks_for_mamba):
|
||||
@@ -1444,7 +1394,7 @@ def test_hybrid_cache_integration(default_vllm_config, dist_init):
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=NUM_BLOCKS,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(size=tensor_size, shared_by=["layer.0"]),
|
||||
KVCacheTensor(size=tensor_size, shared_by=[["layer.0"]]),
|
||||
],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(layer_names=["layer.0"], kv_cache_spec=attn_spec)
|
||||
|
||||
+3
-4
@@ -2541,10 +2541,9 @@ class rocm_aiter_ops:
|
||||
) -> None:
|
||||
"""Run the fused QK-norm+RoPE+KV-cache op on already-split k/v caches.
|
||||
|
||||
Shared by the AITER FA and unified-attention impls. The caller splits
|
||||
kv_cache, since the unbind dim depends on the layout (e.g. the unified
|
||||
encoder-decoder path is K/V-first), and passes use_shuffle_layout
|
||||
(unified reads NHD and must pass False).
|
||||
Shared by the AITER FA and unified-attention impls. The caller converts
|
||||
the standardized cache view to NHD and splits its packed K/V content,
|
||||
then passes use_shuffle_layout (unified reads NHD and must pass False).
|
||||
"""
|
||||
if kv_cache_dtype.startswith("fp8"):
|
||||
key_cache = key_cache.view(current_platform.fp8_dtype())
|
||||
|
||||
@@ -45,7 +45,7 @@ def fused_rope_unified_mla_kv_cache_update_impl(
|
||||
cos_sin_cache,
|
||||
is_neox,
|
||||
layer_slot_mapping,
|
||||
kv_cache,
|
||||
kv_cache.squeeze(1),
|
||||
kv_cache_dtype,
|
||||
kv_cache_scale,
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ class KVTransferConfig:
|
||||
Only supported in V1."""
|
||||
|
||||
enable_permute_local_kv: bool = False
|
||||
"""Experiment feature flag to enable HND to NHD KV Transfer"""
|
||||
"""Experiment feature flag to enable HNC to NHC KV Transfer"""
|
||||
|
||||
kv_load_failure_policy: Literal["recompute", "fail"] = "fail"
|
||||
"""Policy for handling KV cache load failures.
|
||||
|
||||
@@ -12,7 +12,7 @@ import torch
|
||||
|
||||
from vllm.config import (
|
||||
VllmConfig,
|
||||
get_current_vllm_config,
|
||||
get_current_vllm_config_or_none,
|
||||
get_layers_from_vllm_config,
|
||||
set_current_vllm_config,
|
||||
)
|
||||
@@ -35,19 +35,18 @@ BlockIds = tuple[list[int], ...] | list[list[int]]
|
||||
|
||||
|
||||
def get_kv_connector_cache_layout():
|
||||
# NOTE (NickLucche) When running disaggregated PD with NIXL, HND layout is
|
||||
# used for faster transfer.
|
||||
vllm_config = get_current_vllm_config()
|
||||
# NOTE (NickLucche) When running disaggregated PD with NIXL, LBHNC layout
|
||||
# is used for faster transfer.
|
||||
vllm_config = get_current_vllm_config_or_none()
|
||||
if vllm_config is None:
|
||||
return None
|
||||
kv_config = vllm_config.kv_transfer_config
|
||||
if kv_config is not None:
|
||||
connector_cls = KVConnectorFactory.get_connector_class(kv_config)
|
||||
required_kvcache_layout = connector_cls.get_required_kvcache_layout(vllm_config)
|
||||
if required_kvcache_layout is not None:
|
||||
return required_kvcache_layout
|
||||
logger.info_once(
|
||||
"Connectors do not specify a kv cache layout, defaulting to NHD."
|
||||
)
|
||||
return "NHD"
|
||||
return None
|
||||
|
||||
|
||||
class KVOutputAggregator:
|
||||
@@ -279,11 +278,11 @@ def kv_postprocess_layout_on_receive(cache, indices):
|
||||
|
||||
def kv_postprocess_blksize_and_layout_on_receive(cache, indices, block_size_ratio):
|
||||
"""
|
||||
Transforms the layout of received KV cache to the local block_size and HND.
|
||||
(Only works for local blocksize > remote blocksize)
|
||||
Transforms the layout of received KV cache to the local block_size
|
||||
and LBHNC. (Only works for local blocksize > remote blocksize)
|
||||
|
||||
prefill is HND, smaller block_size
|
||||
decode(local) is NHD, larger block_size
|
||||
prefill is LBHNC, smaller block_size
|
||||
decode(local) is LBNHC, larger block_size
|
||||
"""
|
||||
blocks_to_update = cache.index_select(0, indices)
|
||||
|
||||
@@ -418,47 +417,12 @@ class TransferTopology:
|
||||
|
||||
self._engines: dict[tuple[EngineId, int], EngineTransferInfo] = {}
|
||||
|
||||
# Figure out whether the first dimension of the cache is K/V
|
||||
# or num_blocks.
|
||||
attn_backend = self.attn_backends[0]
|
||||
if not self.is_mamba:
|
||||
_MOCK_BLOCK_SIZE = 16
|
||||
kv_cache_shape: tuple[int, ...] = attn_backend.get_kv_cache_shape(
|
||||
num_blocks=1,
|
||||
block_size=_MOCK_BLOCK_SIZE,
|
||||
num_kv_heads=1,
|
||||
head_size=1,
|
||||
)
|
||||
logger.debug("Test kv_cache_shape: %s", kv_cache_shape)
|
||||
assert kv_cache_shape[0] == 1, (
|
||||
"KV cache layout must be blocks-first; expected mocked "
|
||||
f"num_blocks=1 in leading dim, got shape {kv_cache_shape}."
|
||||
)
|
||||
if not self.is_mla:
|
||||
assert len(kv_cache_shape) == 4, (
|
||||
"Attention KV cache layout must be standardized as "
|
||||
"[num_blocks, num_kv_heads, block_size, content_size], "
|
||||
f"got shape {kv_cache_shape}."
|
||||
)
|
||||
# Cross-layer layouts (BLHNC) have B outermost, so all layers
|
||||
# for a block are contiguous — transfers can coalesce multiple
|
||||
# layers into one operation.
|
||||
from vllm.v1.attention.backends.utils import resolve_kv_cache_layout
|
||||
|
||||
self._cross_layers_blocks = False
|
||||
if self.tensor_shape is not None:
|
||||
self._cross_layers_blocks = (
|
||||
len(self.tensor_shape) == len(kv_cache_shape) + 1
|
||||
)
|
||||
|
||||
if self._cross_layers_blocks:
|
||||
logger.debug("Using cross-layer KV cache")
|
||||
_MOCK_NUM_LAYERS = 80
|
||||
kv_cache_shape = (_MOCK_NUM_LAYERS,) + kv_cache_shape
|
||||
try:
|
||||
kv_cache_stride_order = attn_backend.get_kv_cache_stride_order(
|
||||
include_num_layers_dimension=self._cross_layers_blocks
|
||||
)
|
||||
except (AttributeError, NotImplementedError):
|
||||
assert self.tensor_shape is not None
|
||||
kv_cache_stride_order = tuple(range(len(self.tensor_shape)))
|
||||
kv_cache_shape = tuple(kv_cache_shape[i] for i in kv_cache_stride_order)
|
||||
self._is_kv_layout_blocks_first = not resolve_kv_cache_layout().is_layer_compact
|
||||
|
||||
# ============================================================
|
||||
# Engine registration
|
||||
@@ -499,18 +463,8 @@ class TransferTopology:
|
||||
# ============================================================
|
||||
|
||||
@property
|
||||
def cross_layers_blocks(self) -> bool:
|
||||
return self._cross_layers_blocks
|
||||
|
||||
@property
|
||||
def virtually_split_kv_in_blocks(self) -> bool:
|
||||
# Whether to logically split each block into two separately-indexable
|
||||
# sub-regions. With K and V packed into the content dim, an attention
|
||||
# block transfers as a single unit — no K/V sub-split is needed. Only
|
||||
# Mamba still needs this, to index its two state regions (conv/ssm)
|
||||
# separately. Not applicable to cross-layer blocks (per-layer
|
||||
# interleaving means a simple half-split does not separate the parts).
|
||||
return self.is_mamba and not self._cross_layers_blocks
|
||||
def is_kv_layout_blocks_first(self) -> bool:
|
||||
return self._is_kv_layout_blocks_first
|
||||
|
||||
# ============================================================
|
||||
# Common methods
|
||||
|
||||
@@ -48,7 +48,7 @@ from typing import TYPE_CHECKING, Any, Literal
|
||||
import torch
|
||||
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata
|
||||
from vllm.v1.attention.backend import AttentionMetadata
|
||||
from vllm.v1.core.sched.output import SchedulerOutput
|
||||
from vllm.v1.outputs import KVConnectorOutput
|
||||
|
||||
@@ -173,14 +173,6 @@ class KVConnectorBase_V1(ABC):
|
||||
Base class for KV connectors.
|
||||
"""
|
||||
|
||||
@property
|
||||
def prefer_cross_layer_blocks(self) -> bool:
|
||||
"""
|
||||
Indicates whether this connector prefers KV blocks that hold KV data for all
|
||||
layers, which can speed up KV data transfers. Defaults to False.
|
||||
"""
|
||||
return False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vllm_config: "VllmConfig",
|
||||
@@ -258,23 +250,6 @@ class KVConnectorBase_V1(ABC):
|
||||
"""
|
||||
return
|
||||
|
||||
def register_cross_layers_kv_cache(
|
||||
self, kv_cache: torch.Tensor, attn_backend: type["AttentionBackend"]
|
||||
):
|
||||
"""
|
||||
Initialize with a single KV cache tensor used by all layers.
|
||||
The first dimension should be num_layers.
|
||||
This function will only be called for models with uniform layers,
|
||||
and only if the prefers_cross_layer_blocks is set to True.
|
||||
Only one of the functions
|
||||
{register_kv_caches, register_cross_layers_kv_cache} will be called.
|
||||
|
||||
Args:
|
||||
kv_cache: a cross-layers kv cache tensor
|
||||
attn_backend: The attention backend that corresponds to all layers
|
||||
"""
|
||||
return
|
||||
|
||||
def set_host_xfer_buffer_ops(self, copy_operation: CopyBlocksOp):
|
||||
"""
|
||||
Set the xPU-specific ops for copying KV between host and device.
|
||||
@@ -594,7 +569,7 @@ class KVConnectorBase_V1(ABC):
|
||||
vllm_config (VllmConfig): the vllm config.
|
||||
|
||||
Returns:
|
||||
str: the required KV cache layout. e.g. HND, or NHD.
|
||||
str: the required KV cache layout. e.g. HNC, or NHC.
|
||||
None if the connector does not require a specific layout.
|
||||
"""
|
||||
|
||||
|
||||
@@ -38,8 +38,10 @@ def extract_from_kv_cache(
|
||||
num_tokens: int,
|
||||
) -> torch.Tensor:
|
||||
"""Extract data from KV cache."""
|
||||
block_size = kv_cache.shape[1]
|
||||
return kv_cache[slot_mapping // block_size, slot_mapping % block_size][:num_tokens]
|
||||
block_size = kv_cache.shape[2]
|
||||
return kv_cache[slot_mapping // block_size, :, slot_mapping % block_size][
|
||||
:num_tokens
|
||||
]
|
||||
|
||||
|
||||
def load_hidden_states(path: str) -> dict[str, torch.Tensor]:
|
||||
@@ -100,15 +102,6 @@ class ExampleHiddenStatesConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
Must be used in conjunction with the `extract_hidden_states` spec decoding method.
|
||||
"""
|
||||
|
||||
@property
|
||||
def prefer_cross_layer_blocks(self) -> bool:
|
||||
"""
|
||||
Indicates whether this connector prefers KV blocks that hold KV data for all
|
||||
layers, which can speed up KV data transfers. Defaults to False.
|
||||
"""
|
||||
# Must be False so that drafter kv cache isn't merged with verifier's
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _find_cache_kv_group_id(cls, kv_cache_config: "KVCacheConfig | None") -> int:
|
||||
"""Index of the KV cache group holding the extracted hidden states.
|
||||
@@ -596,7 +589,7 @@ class ExampleHiddenStatesConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
vllm_config (VllmConfig): the vllm config.
|
||||
|
||||
Returns:
|
||||
str: the required KV cache layout. e.g. HND, or NHD.
|
||||
str: the required KV cache layout. e.g. HNC, or NHC.
|
||||
None if the connector does not require a specific layout.
|
||||
"""
|
||||
|
||||
@@ -605,9 +598,9 @@ class ExampleHiddenStatesConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
"get_required_kvcache_layout should not be called "
|
||||
"on the abstract base class"
|
||||
)
|
||||
# NHD means we have (num_tokens, num_heads)
|
||||
# HND means we have (num_heads, num_tokens)
|
||||
# For now, we only support NHD layout since this keeps the
|
||||
# LBNHC means we have (num_tokens, num_heads)
|
||||
# LBHNC means we have (num_heads, num_tokens)
|
||||
# For now, we only support LBNHC layout since this keeps the
|
||||
# hidden states for each token together in memory.
|
||||
# HND is primarily used when sharding heads across devices.
|
||||
return "NHD"
|
||||
# LBHNC is primarily used when sharding heads across devices.
|
||||
return "LBNHC"
|
||||
|
||||
@@ -974,7 +974,7 @@ class LMCacheMPConnectorUpstream(KVConnectorBase_V1):
|
||||
vllm_config (VllmConfig): the vllm config.
|
||||
|
||||
Returns:
|
||||
str: the required KV cache layout. e.g. HND, or NHD.
|
||||
str: the required KV cache layout. e.g. HNC, or NHC.
|
||||
None if the connector does not require a specific layout.
|
||||
"""
|
||||
|
||||
|
||||
@@ -50,14 +50,13 @@ from vllm.platforms import current_platform
|
||||
from vllm.utils.math_utils import cdiv
|
||||
from vllm.utils.network_utils import get_ip, make_zmq_path, make_zmq_socket
|
||||
from vllm.v1.attention.backend import AttentionMetadata
|
||||
from vllm.v1.attention.backends.utils import NULL_BLOCK_ID, get_kv_cache_layout
|
||||
from vllm.v1.attention.backends.utils import NULL_BLOCK_ID, resolve_kv_cache_layout
|
||||
from vllm.v1.core.sched.output import SchedulerOutput
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
AttentionSpec,
|
||||
FullAttentionSpec,
|
||||
KVCacheSpec,
|
||||
MambaSpec,
|
||||
MLAAttentionSpec,
|
||||
SlidingWindowMLASpec,
|
||||
SlidingWindowSpec,
|
||||
)
|
||||
from vllm.v1.request import RequestStatus
|
||||
@@ -122,9 +121,8 @@ def _expand_transfer_regions(
|
||||
kv_block_lens: list[int],
|
||||
layer_names: list[str],
|
||||
layer_indices: list[int],
|
||||
is_kv_layout_blocks_first: bool,
|
||||
is_kv_layout_blocks_first: bool, # kept for API compat, unused
|
||||
group_indices: list[int] | None = None,
|
||||
split_kv_regions: list[bool] | None = None,
|
||||
) -> list[TransferRegion]:
|
||||
"""Expand registered KV tensors into the regions transferred by Mooncake."""
|
||||
assert (
|
||||
@@ -146,13 +144,6 @@ def _expand_transfer_regions(
|
||||
"Mooncake transfer regions require matching group metadata lengths, "
|
||||
f"got group_indices={len(group_indices)}, layer_names={len(layer_names)}."
|
||||
)
|
||||
if split_kv_regions is None:
|
||||
split_kv_regions = [is_kv_layout_blocks_first] * len(layer_names)
|
||||
assert len(split_kv_regions) == len(layer_names), (
|
||||
"Mooncake transfer regions require matching split metadata, "
|
||||
f"got split_kv_regions={len(split_kv_regions)}, "
|
||||
f"layer_names={len(layer_names)}."
|
||||
)
|
||||
regions: list[TransferRegion] = []
|
||||
for (
|
||||
base_addr,
|
||||
@@ -161,7 +152,6 @@ def _expand_transfer_regions(
|
||||
layer_name,
|
||||
layer_index,
|
||||
group_index,
|
||||
split_kv_region,
|
||||
) in zip(
|
||||
base_addrs,
|
||||
block_lens,
|
||||
@@ -169,7 +159,6 @@ def _expand_transfer_regions(
|
||||
layer_names,
|
||||
layer_indices,
|
||||
group_indices,
|
||||
split_kv_regions,
|
||||
):
|
||||
regions.append(
|
||||
TransferRegion(
|
||||
@@ -181,17 +170,6 @@ def _expand_transfer_regions(
|
||||
group_index=group_index,
|
||||
)
|
||||
)
|
||||
if split_kv_region:
|
||||
regions.append(
|
||||
TransferRegion(
|
||||
layer_name=layer_name,
|
||||
layer_index=layer_index,
|
||||
base_addr=base_addr + kv_block_len,
|
||||
block_len=block_len,
|
||||
kv_block_len=kv_block_len,
|
||||
group_index=group_index,
|
||||
)
|
||||
)
|
||||
return regions
|
||||
|
||||
|
||||
@@ -506,10 +484,10 @@ class MooncakeConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
if vllm_config.model_config.use_mla:
|
||||
return None
|
||||
logger.info_once(
|
||||
"MooncakeConnector setting KV cache layout to HND for "
|
||||
"MooncakeConnector setting KV cache layout to LBHNC for "
|
||||
"heterogeneous TP-safe KV transfer."
|
||||
)
|
||||
return "HND"
|
||||
return "LBHNC"
|
||||
|
||||
############################################################
|
||||
# Scheduler Side Methods
|
||||
@@ -1023,7 +1001,7 @@ class MooncakeConnectorWorker:
|
||||
self._sync_block_size_with_kernel()
|
||||
|
||||
self.attn_backends = get_current_attn_backends(vllm_config)
|
||||
self.kv_cache_layout = get_kv_cache_layout()
|
||||
self.kv_cache_layout = resolve_kv_cache_layout().name
|
||||
logger.debug(
|
||||
"Detected attention backends %s",
|
||||
[backend.get_name() for backend in self.attn_backends],
|
||||
@@ -1674,13 +1652,10 @@ class MooncakeConnectorWorker:
|
||||
layer_name,
|
||||
)
|
||||
continue
|
||||
if isinstance(layer_spec, MambaSpec):
|
||||
conv, _ = cache_or_caches
|
||||
cache_list = [conv]
|
||||
else:
|
||||
# K and V are packed into one blocks-first tensor per layer,
|
||||
# so each layer registers as a single region.
|
||||
cache_list = [cache_or_caches]
|
||||
# Standardized allocation exposes one raw page tensor per layer.
|
||||
# For Mamba that page contains all recurrent states; the layer
|
||||
# unpacks it only when binding the cache for model execution.
|
||||
cache_list = [cache_or_caches]
|
||||
|
||||
logger.debug(
|
||||
"registering layer %s with %d cache tensor(s)",
|
||||
@@ -1690,25 +1665,32 @@ class MooncakeConnectorWorker:
|
||||
|
||||
for cache in cache_list:
|
||||
self._log_debug_cache_registration(layer_name, cache)
|
||||
base_addr = cache.data_ptr()
|
||||
block_len = cache.stride(0) * cache.element_size()
|
||||
region_base_addresses.append(base_addr)
|
||||
|
||||
if isinstance(layer_spec, (MLAAttentionSpec, SlidingWindowMLASpec)):
|
||||
kv_block_len = layer_spec.page_size_bytes
|
||||
elif self.transfer_topo.virtually_split_kv_in_blocks and not isinstance(
|
||||
layer_spec, MambaSpec
|
||||
if (
|
||||
isinstance(layer_spec, AttentionSpec)
|
||||
and layer_spec.separate_kv_head_groups
|
||||
):
|
||||
kv_block_len = block_len // 2
|
||||
region_caches = [cache[:, head] for head in range(cache.shape[1])]
|
||||
else:
|
||||
kv_block_len = block_len
|
||||
self.block_len_per_layer.append(block_len)
|
||||
self.kv_block_len_per_layer.append(kv_block_len)
|
||||
self.registered_layer_names.append(layer_name)
|
||||
self.registered_layer_indices.append(layer_index)
|
||||
self.registered_group_indices.append(
|
||||
self._layer_group_indices[layer_name]
|
||||
)
|
||||
region_caches = [cache]
|
||||
|
||||
for region_cache in region_caches:
|
||||
base_addr = region_cache.data_ptr()
|
||||
block_len = region_cache.stride(0) * region_cache.element_size()
|
||||
region_base_addresses.append(base_addr)
|
||||
|
||||
kv_block_len = (
|
||||
layer_spec.page_size_bytes
|
||||
if isinstance(layer_spec, AttentionSpec)
|
||||
and not layer_spec.separate_kv_head_groups
|
||||
else block_len
|
||||
)
|
||||
self.block_len_per_layer.append(block_len)
|
||||
self.kv_block_len_per_layer.append(kv_block_len)
|
||||
self.registered_layer_names.append(layer_name)
|
||||
self.registered_layer_indices.append(layer_index)
|
||||
self.registered_group_indices.append(
|
||||
self._layer_group_indices[layer_name]
|
||||
)
|
||||
storage = cache.untyped_storage()
|
||||
storage_addr = storage.data_ptr()
|
||||
if storage_addr not in seen_storage_ptrs:
|
||||
@@ -2055,24 +2037,14 @@ class MooncakeConnectorWorker:
|
||||
self._layer_group_indices.get(layer_name, 0)
|
||||
for layer_name in layer_names
|
||||
]
|
||||
split_kv_regions = None
|
||||
if self.transfer_topo.virtually_split_kv_in_blocks:
|
||||
split_kv_regions = [
|
||||
not isinstance(
|
||||
self._layer_specs[layer_name],
|
||||
(MambaSpec, MLAAttentionSpec, SlidingWindowMLASpec),
|
||||
)
|
||||
for layer_name in layer_names
|
||||
]
|
||||
return _expand_transfer_regions(
|
||||
base_addrs=base_addrs,
|
||||
block_lens=block_lens,
|
||||
kv_block_lens=kv_block_lens,
|
||||
layer_names=layer_names,
|
||||
layer_indices=layer_indices,
|
||||
is_kv_layout_blocks_first=self.transfer_topo.virtually_split_kv_in_blocks,
|
||||
is_kv_layout_blocks_first=self.transfer_topo.is_kv_layout_blocks_first,
|
||||
group_indices=group_indices,
|
||||
split_kv_regions=split_kv_regions,
|
||||
)
|
||||
|
||||
def _get_sender_transfer_plan(
|
||||
|
||||
@@ -87,14 +87,6 @@ class MooncakeStoreKVEvents(KVConnectorKVEvents):
|
||||
class MooncakeStoreConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
"""KV connector using MooncakeDistributedStore as shared KV pool."""
|
||||
|
||||
@property
|
||||
def prefer_cross_layer_blocks(self) -> bool:
|
||||
extra_config = self._kv_transfer_config.kv_connector_extra_config
|
||||
return (
|
||||
str(extra_config.get("enable_cross_layers_blocks", "False")).lower()
|
||||
== "true"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _validate_kv_cache_config(
|
||||
vllm_config: VllmConfig, kv_cache_config: KVCacheConfig
|
||||
@@ -262,16 +254,6 @@ class MooncakeStoreConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
assert self.connector_worker is not None
|
||||
self.connector_worker.register_kv_caches(kv_caches)
|
||||
|
||||
def register_cross_layers_kv_cache(
|
||||
self, kv_cache: torch.Tensor, attn_backend: type
|
||||
):
|
||||
assert self.connector_worker is not None
|
||||
assert (
|
||||
self._kv_cache_config is not None
|
||||
and len(self._kv_cache_config.kv_cache_groups) == 1
|
||||
), "Cross-layer KV cache does not supported with hybrid models"
|
||||
self.connector_worker.register_cross_layers_kv_caches(kv_cache)
|
||||
|
||||
def start_load_kv(self, forward_context: ForwardContext, **kwargs: Any) -> None:
|
||||
# No-op: loads are issued in get_finished() for compute overlap.
|
||||
pass
|
||||
|
||||
@@ -61,12 +61,18 @@ from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store.protocol import
|
||||
from vllm.logger import init_logger
|
||||
from vllm.utils.math_utils import cdiv
|
||||
from vllm.utils.network_utils import get_ip, make_zmq_socket
|
||||
from vllm.v1.attention.backends.utils import resolve_kv_cache_layout
|
||||
from vllm.v1.core.kv_cache_utils import (
|
||||
BlockHash,
|
||||
maybe_convert_block_hash,
|
||||
resolve_kv_cache_block_sizes,
|
||||
)
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheGroupSpec
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
AttentionSpec,
|
||||
KVCacheConfig,
|
||||
KVCacheGroupSpec,
|
||||
UniformTypeKVCacheSpecs,
|
||||
)
|
||||
|
||||
from .metrics import MooncakeStoreConnectorStats
|
||||
|
||||
@@ -1193,15 +1199,6 @@ class MooncakeStoreWorker:
|
||||
)
|
||||
self._lookup_expected_per_key = len(rank_namespaces)
|
||||
|
||||
def register_cross_layers_kv_caches(self, kv_cache: torch.Tensor) -> None:
|
||||
"""Register a cross-layers KV cache tensor.
|
||||
|
||||
Wraps the unified tensor in a single-entry dict so that the
|
||||
existing stride-based logic in register_kv_caches() produces
|
||||
the correct single-segment result (block_len = page_size * num_layers).
|
||||
"""
|
||||
self.register_kv_caches({"__cross_layer__": kv_cache})
|
||||
|
||||
def register_kv_caches(
|
||||
self,
|
||||
kv_caches: dict[str, torch.Tensor | list[torch.Tensor]],
|
||||
@@ -1222,47 +1219,65 @@ class MooncakeStoreWorker:
|
||||
assert self.cache_config.num_gpu_blocks is not None
|
||||
self.num_blocks = self.cache_config.num_gpu_blocks
|
||||
|
||||
seen_ptrs: set[int] = set()
|
||||
layout = resolve_kv_cache_layout()
|
||||
seen_storage_ptrs: set[int] = set()
|
||||
seen_region_ptrs: set[int] = set()
|
||||
addrs: list[int] = []
|
||||
block_lens: list[int] = []
|
||||
|
||||
for value in kv_caches.values():
|
||||
cache = _repr_tensor(value)
|
||||
cache_storage = cache.untyped_storage()
|
||||
base_addr = cache_storage.data_ptr()
|
||||
if base_addr in seen_ptrs:
|
||||
continue
|
||||
seen_ptrs.add(base_addr)
|
||||
region_len = cache_storage.nbytes()
|
||||
|
||||
ret = self.store.register_buffer(base_addr, region_len)
|
||||
if ret != 0:
|
||||
logger.error(
|
||||
"register_buffer failed for addr %#x len %d: %d",
|
||||
base_addr,
|
||||
region_len,
|
||||
ret,
|
||||
layer_specs = {}
|
||||
for group in self._kv_cache_groups:
|
||||
group_spec = group.kv_cache_spec
|
||||
for layer_name in group.layer_names:
|
||||
layer_specs[layer_name] = (
|
||||
group_spec.kv_cache_specs[layer_name]
|
||||
if isinstance(group_spec, UniformTypeKVCacheSpecs)
|
||||
else group_spec
|
||||
)
|
||||
|
||||
# Detect layout via stride: a dim whose byte-stride exceeds
|
||||
# page_size_bytes is an outer segment dim (e.g. the K/V dim of
|
||||
# FlashAttn's (2, num_blocks, ...)). FlashInfer/MLA's blocks-
|
||||
# outermost layout has no such dim and yields a single segment.
|
||||
el = cache.element_size()
|
||||
page_size_bytes = region_len // self.num_blocks
|
||||
outer_dims = [
|
||||
d for d in range(cache.ndim) if cache.stride(d) * el > page_size_bytes
|
||||
]
|
||||
if not outer_dims:
|
||||
# Blocks-first layout (FlashInfer / MLA): one segment.
|
||||
for layer_name, value in kv_caches.items():
|
||||
cache = _repr_tensor(value)
|
||||
layer_spec = layer_specs.get(layer_name)
|
||||
cache_storage = cache.untyped_storage()
|
||||
base_addr = cache_storage.data_ptr()
|
||||
region_len = cache_storage.nbytes()
|
||||
|
||||
if base_addr not in seen_storage_ptrs:
|
||||
seen_storage_ptrs.add(base_addr)
|
||||
ret = self.store.register_buffer(base_addr, region_len)
|
||||
if ret != 0:
|
||||
logger.error(
|
||||
"register_buffer failed for addr %#x len %d: %d",
|
||||
base_addr,
|
||||
region_len,
|
||||
ret,
|
||||
)
|
||||
|
||||
if (
|
||||
isinstance(layer_spec, AttentionSpec)
|
||||
and layer_spec.separate_kv_head_groups
|
||||
):
|
||||
for head_idx in range(cache.shape[1]):
|
||||
head_cache = cache[:, head_idx]
|
||||
region_addr = head_cache.data_ptr()
|
||||
if region_addr in seen_region_ptrs:
|
||||
continue
|
||||
seen_region_ptrs.add(region_addr)
|
||||
addrs.append(region_addr)
|
||||
block_lens.append(head_cache.stride(0) * head_cache.element_size())
|
||||
elif not layout.is_layer_compact:
|
||||
if base_addr in seen_region_ptrs:
|
||||
continue
|
||||
seen_region_ptrs.add(base_addr)
|
||||
addrs.append(base_addr)
|
||||
block_lens.append(page_size_bytes)
|
||||
block_lens.append(region_len // self.num_blocks)
|
||||
else:
|
||||
# K/V-first layout (FlashAttn / ROCm): split segments.
|
||||
seg_stride = cache.stride(outer_dims[0]) * el
|
||||
for idx in range(cache.shape[outer_dims[0]]):
|
||||
addrs.append(base_addr + idx * seg_stride)
|
||||
block_lens.append(seg_stride // self.num_blocks)
|
||||
region_addr = cache.data_ptr()
|
||||
if region_addr in seen_region_ptrs:
|
||||
continue
|
||||
seen_region_ptrs.add(region_addr)
|
||||
addrs.append(region_addr)
|
||||
block_lens.append(cache.stride(0) * cache.element_size())
|
||||
|
||||
logger.info(
|
||||
"Registered KV caches: num_groups=%d, num_segments=%d, num_blocks=%d",
|
||||
|
||||
@@ -27,7 +27,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.metrics import (
|
||||
PromMetricT,
|
||||
)
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata
|
||||
from vllm.v1.attention.backend import AttentionMetadata
|
||||
from vllm.v1.core.sched.output import SchedulerOutput
|
||||
from vllm.v1.outputs import KVConnectorOutput
|
||||
|
||||
@@ -203,12 +203,6 @@ class MultiConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
# Propagated from scheduler to worker side via the connector metadata.
|
||||
self._extra_async_saves: dict[str, int] = {}
|
||||
|
||||
@property
|
||||
def prefer_cross_layer_blocks(self) -> bool:
|
||||
if not self._connectors:
|
||||
return False
|
||||
return all(c.prefer_cross_layer_blocks for c in self._connectors)
|
||||
|
||||
@classmethod
|
||||
def _get_connector_classes_and_configs(
|
||||
cls, vllm_config: "VllmConfig"
|
||||
@@ -235,13 +229,6 @@ class MultiConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
)
|
||||
return ret
|
||||
|
||||
def register_cross_layers_kv_cache(
|
||||
self, kv_cache: torch.Tensor, attn_backend: type[AttentionBackend]
|
||||
):
|
||||
# Register on all connectors
|
||||
for c in self._connectors:
|
||||
c.register_cross_layers_kv_cache(kv_cache, attn_backend)
|
||||
|
||||
def register_kv_caches(self, kv_caches: dict[str, torch.Tensor]):
|
||||
for c in self._connectors:
|
||||
c.register_kv_caches(kv_caches)
|
||||
@@ -551,7 +538,7 @@ class MultiConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
vllm_config (VllmConfig): the vllm config.
|
||||
|
||||
Returns:
|
||||
str: the required KV cache layout. e.g. HND, or NHD.
|
||||
str: the required KV cache layout. e.g. HNC, or NHC.
|
||||
None if the connector does not require a specific layout.
|
||||
"""
|
||||
assert vllm_config.kv_transfer_config is not None
|
||||
|
||||
@@ -67,9 +67,10 @@ from vllm.distributed.parallel_state import (
|
||||
from vllm.logger import init_logger
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.network_utils import make_zmq_path
|
||||
from vllm.v1.attention.backends.utils import get_kv_cache_layout
|
||||
from vllm.v1.attention.backends.utils import resolve_kv_cache_layout
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheLayout,
|
||||
MambaSpec,
|
||||
MLAAttentionSpec,
|
||||
SlidingWindowMLASpec,
|
||||
@@ -286,7 +287,9 @@ class NixlBaseConnectorWorker:
|
||||
for group in kv_cache_config.kv_cache_groups
|
||||
for layer in group.layer_names
|
||||
}
|
||||
self.hma_group_size = len(kv_cache_config.kv_cache_tensors)
|
||||
self.hma_group_size = sum(
|
||||
len(t.shared_by) for t in kv_cache_config.kv_cache_tensors
|
||||
)
|
||||
|
||||
# ---- Model state (derived from model config) ----
|
||||
mamba_ssm_size = (0, 0)
|
||||
@@ -501,7 +504,7 @@ class NixlBaseConnectorWorker:
|
||||
self.attn_backends = get_current_attn_backends(vllm_config)
|
||||
self.backend_name = self.attn_backends[0].get_name()
|
||||
|
||||
self.kv_cache_layout = get_kv_cache_layout()
|
||||
self.kv_cache_layout = resolve_kv_cache_layout().name
|
||||
self.host_buffer_kv_cache_layout = self.kv_cache_layout
|
||||
logger.info(
|
||||
"Detected attention backend(s) %s",
|
||||
@@ -536,6 +539,11 @@ class NixlBaseConnectorWorker:
|
||||
# This is not used for SSM layers, which use the counterpart `mamba_ssm_size`.
|
||||
self.block_len_per_layer = list[int]()
|
||||
|
||||
# Per-block physical stride per layer (bytes). Read from each
|
||||
# registered tensor's stride(0) so it stays correct under layouts
|
||||
# that interleave layers within a block (BLHNC/BHLNC).
|
||||
self.block_stride_per_layer = list[int]()
|
||||
|
||||
# Per-engine TP mappings. Generated during handshake.
|
||||
self.tp_mappings: dict[EngineId, TPMapping] = {}
|
||||
|
||||
@@ -746,20 +754,22 @@ class NixlBaseConnectorWorker:
|
||||
if not self.use_mla:
|
||||
assert kv_cache.ndim == 4
|
||||
|
||||
if self.kv_cache_layout == "NHD":
|
||||
if self.kv_cache_layout == "LBNHC":
|
||||
if self.kv_transfer_config.enable_permute_local_kv:
|
||||
logger.info_once(
|
||||
"'enable_permute_local_kv' flag is enabled while "
|
||||
"device KV Layout is NHD. Init host buffer with"
|
||||
" HND to better support Decode/Prefill TP_ratio > 1."
|
||||
"device KV Layout is LBNHC. Init host buffer with"
|
||||
" LBHNC to better support Decode/Prefill "
|
||||
"TP_ratio > 1."
|
||||
)
|
||||
# Since NHD will not support Decode/Prefill TP_ratio > 1,
|
||||
# we can leverage host_buffer for permute.
|
||||
self.host_buffer_kv_cache_layout = "HND"
|
||||
# Since LBNHC will not support Decode/Prefill
|
||||
# TP_ratio > 1, we can leverage host_buffer for
|
||||
# permute.
|
||||
self.host_buffer_kv_cache_layout = "LBHNC"
|
||||
else:
|
||||
# Packed KV layout is logical (B, H, N, 2*D). Allocate
|
||||
# (B, N, H, 2*D) and view it as logical (B, H, N, 2*D)
|
||||
# so raw NIXL transfers see NHD physical strides.
|
||||
# so raw NIXL transfers see NHC physical strides.
|
||||
kv_shape = tuple(kv_shape[i] for i in inv_order)
|
||||
permute_shape = True
|
||||
|
||||
@@ -927,117 +937,9 @@ class NixlBaseConnectorWorker:
|
||||
|
||||
fut.add_done_callback(request_ready)
|
||||
|
||||
def register_cross_layers_kv_caches(self, kv_cache: torch.Tensor) -> None:
|
||||
"""Register a cross-layers KV cache tensor with NIXL.
|
||||
|
||||
`use_uniform_kv_cache()` guarantees a single KV cache group whose
|
||||
layers all share the same `AttentionSpec`, so any layer name from
|
||||
`_layer_specs` yields the correct per-layer spec for `page_size_bytes`.
|
||||
"""
|
||||
first_layer = next(iter(self._layer_specs))
|
||||
# Forwarding a real layer name rather than a synthetic key
|
||||
self.register_kv_caches({first_layer: kv_cache})
|
||||
|
||||
def _register_packed_kv_cache(
|
||||
self,
|
||||
storage: torch.UntypedStorage,
|
||||
) -> None:
|
||||
"""Register a packed KV cache as a single NIXL region.
|
||||
|
||||
The packed allocation interleaves all layers per block, so each
|
||||
block_stride-byte chunk is one logical block. We register 1
|
||||
NIXL region and create 1 descriptor per block.
|
||||
"""
|
||||
self.transfer_topo = TransferTopology(
|
||||
tp_rank=self.tp_rank,
|
||||
tp_size=self.world_size,
|
||||
block_size=self.block_size,
|
||||
engine_id=self.engine_id,
|
||||
is_mla=self.use_mla,
|
||||
total_num_kv_heads=self.model_config.get_total_num_kv_heads(),
|
||||
attn_backends=self.attn_backends,
|
||||
tensor_shape=None,
|
||||
is_mamba=self._has_mamba,
|
||||
)
|
||||
self.compat_hash = compute_nixl_compatibility_hash(
|
||||
self.vllm_config,
|
||||
self.backend_name,
|
||||
self.transfer_topo.cross_layers_blocks,
|
||||
)
|
||||
|
||||
total_size = storage.nbytes()
|
||||
block_stride = total_size // self.num_blocks
|
||||
base_addr = storage.data_ptr()
|
||||
device_id = storage.device.index
|
||||
assert device_id is not None
|
||||
|
||||
logger.info(
|
||||
"Registering packed KV cache: total_size=%s, block_stride=%s, "
|
||||
"num_blocks=%s, num_regions=1",
|
||||
total_size,
|
||||
block_stride,
|
||||
self.num_blocks,
|
||||
)
|
||||
|
||||
self.device_id = device_id
|
||||
caches_data = [(base_addr, total_size, self.device_id, "")]
|
||||
|
||||
self.block_len_per_layer = [block_stride]
|
||||
self.num_regions = 1
|
||||
self.num_descs = self.num_blocks
|
||||
self.kv_caches_base_addr[self.engine_id][self.tp_rank] = [base_addr]
|
||||
|
||||
descs = self.nixl_wrapper.get_reg_descs(caches_data, self.nixl_memory_type)
|
||||
self.nixl_wrapper.register_memory(descs, backends=self.nixl_backends)
|
||||
self._registered_descs.append(descs)
|
||||
|
||||
self.dst_num_blocks[self.engine_id] = self.num_blocks
|
||||
|
||||
self.src_xfer_handles_by_block_size[self.block_size], (self.src_blocks_data) = (
|
||||
self.register_local_xfer_handler(self.block_size)
|
||||
)
|
||||
|
||||
agent_metadata = NixlAgentMetadata(
|
||||
engine_id=self.engine_id,
|
||||
agent_metadata=self.nixl_wrapper.get_agent_metadata(),
|
||||
device_id=self.device_id,
|
||||
kv_caches_base_addr=(
|
||||
self.kv_caches_base_addr[self.engine_id][self.tp_rank]
|
||||
),
|
||||
num_blocks=self.num_blocks,
|
||||
block_lens=self.block_len_per_layer,
|
||||
kv_cache_layout=self.kv_cache_layout,
|
||||
block_size=self.block_size,
|
||||
ssm_sizes=self._mamba_ssm_size,
|
||||
attn_backend_name=self.backend_name,
|
||||
physical_blocks_per_logical_kv_block=(
|
||||
self._physical_blocks_per_logical_kv_block
|
||||
),
|
||||
)
|
||||
assert self.compat_hash is not None
|
||||
encoder = msgspec.msgpack.Encoder()
|
||||
self.xfer_handshake_metadata = NixlHandshakePayload(
|
||||
compatibility_hash=self.compat_hash,
|
||||
agent_metadata_bytes=encoder.encode(agent_metadata),
|
||||
)
|
||||
|
||||
def register_kv_caches(self, kv_caches: dict[str, torch.Tensor]):
|
||||
"""Register the KV Cache data in nixl."""
|
||||
|
||||
# Detect packed allocation: all tensors are strided views into the
|
||||
# same backing storage (different data_ptr but same storage).
|
||||
# This happens with DSv4-style contiguous per-block packing.
|
||||
if len(kv_caches) > 1 and not self._has_mamba:
|
||||
storage = next(iter(kv_caches.values())).untyped_storage()
|
||||
storage_ptrs = {
|
||||
cache.untyped_storage().data_ptr() for cache in kv_caches.values()
|
||||
}
|
||||
data_ptrs = {cache.data_ptr() for cache in kv_caches.values()}
|
||||
if len(storage_ptrs) == 1 and len(data_ptrs) > 1:
|
||||
self._register_packed_kv_cache(storage)
|
||||
self.device_kv_caches = kv_caches
|
||||
return
|
||||
|
||||
self.transfer_topo = TransferTopology(
|
||||
tp_rank=self.tp_rank,
|
||||
tp_size=self.world_size,
|
||||
@@ -1053,7 +955,7 @@ class NixlBaseConnectorWorker:
|
||||
is_mamba=self._has_mamba,
|
||||
)
|
||||
self.compat_hash = compute_nixl_compatibility_hash(
|
||||
self.vllm_config, self.backend_name, self.transfer_topo.cross_layers_blocks
|
||||
self.vllm_config, self.backend_name
|
||||
)
|
||||
|
||||
if self.use_host_buffer:
|
||||
@@ -1079,8 +981,8 @@ class NixlBaseConnectorWorker:
|
||||
)
|
||||
|
||||
caches_data = []
|
||||
# With hybrid allocator, layers can share a kv cache tensor
|
||||
seen_base_addresses = []
|
||||
seen_storage_addresses: set[int] = set()
|
||||
seen_base_addresses: list[int] = []
|
||||
|
||||
# Note(tms): I modified this from the original region setup code.
|
||||
# K and V are now in different regions. Advantage is that we can
|
||||
@@ -1090,8 +992,6 @@ class NixlBaseConnectorWorker:
|
||||
# (roughly 8KB vs 5KB).
|
||||
# Conversely for FlashInfer, K and V are registered in the same region
|
||||
# to better exploit the memory layout (ie num_blocks is the first dim).
|
||||
tensor_size_bytes = None
|
||||
|
||||
for layer_name, cache in xfer_buffers.items():
|
||||
# NOTE (NickLucche) Hybrid SSM models assume a layout that is similar to
|
||||
# that of FI, with block laid out as in `get_backend_aware_kv_block_len`.
|
||||
@@ -1117,50 +1017,78 @@ class NixlBaseConnectorWorker:
|
||||
else layer_spec.page_size_bytes
|
||||
// self._physical_blocks_per_logical_kv_block
|
||||
)
|
||||
if self.transfer_topo._cross_layers_blocks:
|
||||
# When cross-layers blocks are used, multiply by number of layers
|
||||
physical_page_size = physical_page_size * len(
|
||||
self.kv_cache_config.kv_cache_tensors
|
||||
)
|
||||
num_blocks = (
|
||||
self._logical_num_blocks
|
||||
if isinstance(layer_spec, MambaSpec)
|
||||
else self.num_blocks
|
||||
)
|
||||
# `page_size` accounts for physical blocks, st KVCache is always
|
||||
# [`num_blocks` * `page_size`]
|
||||
curr_tensor_size_bytes = num_blocks * physical_page_size
|
||||
|
||||
base_addr = cache.data_ptr()
|
||||
if base_addr in seen_base_addresses:
|
||||
# NOTE (NickLucche) HMA employs memory pooling to share tensors
|
||||
# across groups. This results in skipping all tensors but the ones
|
||||
# pointed to by group0. Also, generally we will have more blocks
|
||||
# per tensor but fewer regions.
|
||||
logger.debug("Skipping %s because it's already seen", layer_name)
|
||||
continue
|
||||
logger.debug(
|
||||
"Registering layer %s with cache shape: %s", layer_name, cache.shape
|
||||
)
|
||||
seen_base_addresses.append(base_addr)
|
||||
# Only record non-Mamba page sizes.
|
||||
if isinstance(layer_spec, MambaSpec):
|
||||
self.block_len_per_layer.append(
|
||||
physical_page_size // self._physical_blocks_per_logical_kv_block
|
||||
)
|
||||
else:
|
||||
self.block_len_per_layer.append(physical_page_size)
|
||||
storage = cache.untyped_storage()
|
||||
storage_addr = storage.data_ptr()
|
||||
# Memory registration follows allocations, while transfer regions
|
||||
# follow logical layers (or contiguous head segments). This keeps
|
||||
# strided cross-layer views inside their registered allocation.
|
||||
if storage_addr not in seen_storage_addresses:
|
||||
seen_storage_addresses.add(storage_addr)
|
||||
self.device_id = max(cache.get_device(), 0)
|
||||
caches_data.append((storage_addr, storage.nbytes(), self.device_id, ""))
|
||||
|
||||
is_mla_region = isinstance(
|
||||
layer_spec, (MLAAttentionSpec, SlidingWindowMLASpec)
|
||||
)
|
||||
self._region_is_mla.append(is_mla_region)
|
||||
|
||||
if not is_mla_region:
|
||||
if tensor_size_bytes is None:
|
||||
tensor_size_bytes = curr_tensor_size_bytes
|
||||
assert tensor_size_bytes == curr_tensor_size_bytes, (
|
||||
"All non-MLA kv cache tensors must have the same size"
|
||||
if isinstance(layer_spec, MambaSpec):
|
||||
region_specs = [
|
||||
(
|
||||
cache.data_ptr(),
|
||||
physical_page_size
|
||||
// self._physical_blocks_per_logical_kv_block,
|
||||
physical_page_size
|
||||
// self._physical_blocks_per_logical_kv_block,
|
||||
)
|
||||
]
|
||||
else:
|
||||
block_stride = cache.stride(0) * cache.element_size()
|
||||
storage_is_block_major = num_blocks * block_stride == storage.nbytes()
|
||||
hnc_contiguous = (
|
||||
cache.ndim == 4
|
||||
and cache.stride(2) == cache.shape[3]
|
||||
and cache.stride(1) == cache.shape[2] * cache.shape[3]
|
||||
)
|
||||
if storage_is_block_major and not hnc_contiguous:
|
||||
storage_block_len = storage.nbytes() // num_blocks
|
||||
region_specs = [
|
||||
(storage_addr, storage_block_len, storage_block_len)
|
||||
]
|
||||
elif storage_is_block_major:
|
||||
region_specs = [
|
||||
(cache.data_ptr(), physical_page_size, block_stride)
|
||||
]
|
||||
else:
|
||||
segment_bytes = num_blocks * block_stride
|
||||
assert cache.nbytes % segment_bytes == 0
|
||||
num_segments = cache.nbytes // segment_bytes
|
||||
region_block_len = (
|
||||
block_stride if num_segments > 1 else physical_page_size
|
||||
)
|
||||
region_specs = [
|
||||
(
|
||||
cache.data_ptr() + segment_idx * segment_bytes,
|
||||
region_block_len,
|
||||
block_stride,
|
||||
)
|
||||
for segment_idx in range(num_segments)
|
||||
]
|
||||
|
||||
for base_addr, block_len, block_stride in region_specs:
|
||||
if base_addr in seen_base_addresses:
|
||||
continue
|
||||
seen_base_addresses.append(base_addr)
|
||||
self.block_len_per_layer.append(block_len)
|
||||
self.block_stride_per_layer.append(block_stride)
|
||||
self._region_is_mla.append(is_mla_region)
|
||||
|
||||
# When there's a mismatch between kbs<>bs, we rely on HMA to ensure
|
||||
# caches are either [NB, PS] or [NB*r, PS/r] where r is bs/kbs.
|
||||
@@ -1181,11 +1109,6 @@ class NixlBaseConnectorWorker:
|
||||
f"kv_cache_layout={self.kv_cache_layout}"
|
||||
)
|
||||
|
||||
# Need to make sure the device ID is non-negative for NIXL,
|
||||
# Torch uses -1 to indicate CPU tensors.
|
||||
self.device_id = max(cache.get_device(), 0)
|
||||
caches_data.append((base_addr, curr_tensor_size_bytes, self.device_id, ""))
|
||||
|
||||
logger.debug(
|
||||
"Different block lengths collected: %s", set(self.block_len_per_layer)
|
||||
)
|
||||
@@ -1193,10 +1116,11 @@ class NixlBaseConnectorWorker:
|
||||
len(self.block_len_per_layer)
|
||||
== len(seen_base_addresses)
|
||||
== len(self._region_is_mla)
|
||||
== len(self.block_stride_per_layer)
|
||||
)
|
||||
|
||||
self.kv_caches_base_addr[self.engine_id][self.tp_rank] = seen_base_addresses
|
||||
self.num_regions = len(caches_data)
|
||||
self.num_regions = len(seen_base_addresses)
|
||||
|
||||
if self.pp_size > 1:
|
||||
start_layer, end_layer = self.model_config.get_layers_start_end_indices(
|
||||
@@ -1246,6 +1170,7 @@ class NixlBaseConnectorWorker:
|
||||
kv_caches_base_addr=self.kv_caches_base_addr[self.engine_id][self.tp_rank],
|
||||
num_blocks=self.num_blocks,
|
||||
block_lens=self.block_len_per_layer,
|
||||
block_strides=self.block_stride_per_layer,
|
||||
kv_cache_layout=self.kv_cache_layout
|
||||
if not self.use_host_buffer
|
||||
else self.host_buffer_kv_cache_layout,
|
||||
@@ -1368,7 +1293,7 @@ class NixlBaseConnectorWorker:
|
||||
)
|
||||
// block_size_ratio
|
||||
)
|
||||
page_stride = self.block_len_per_layer[i] // block_size_ratio
|
||||
page_stride = self.block_stride_per_layer[i] // block_size_ratio
|
||||
addrs = base_addr + block_arange * page_stride
|
||||
parts.append(self._stack_descs(addrs, kv_block_len, device_id))
|
||||
return np.concatenate(parts)
|
||||
@@ -1413,7 +1338,7 @@ class NixlBaseConnectorWorker:
|
||||
)
|
||||
local_block_len = local_block_len // num_reads
|
||||
|
||||
page_size = nixl_agent_meta.block_lens[i]
|
||||
page_size = nixl_agent_meta.block_strides[i]
|
||||
addrs = base_addr + rank_offset + block_arange * page_size
|
||||
parts.append(self._stack_descs(addrs, local_block_len, device_id))
|
||||
return np.concatenate(parts)
|
||||
@@ -1497,7 +1422,7 @@ class NixlBaseConnectorWorker:
|
||||
tp_ratio = 4 // 2 = 2
|
||||
|
||||
Considering the KV Caches, if P-Worker_i has cache size [2, num_blocksP, kv_heads, block_size, head_dim]
|
||||
then D-Worker_j has [2, num_blocksD, kv_heads//tp_ratio, block_size, head_dim]. Mind the "HND" layout format.
|
||||
then D-Worker_j has [2, num_blocksD, kv_heads//tp_ratio, block_size, head_dim]. Mind the "LBHNC" layout format.
|
||||
Assuming num_blocksD >= num_blocksP, D-Worker0 reads from P-Worker0 by preparing the kv_heads//tp_ratio
|
||||
first heads from all the slots of all the blocks. D-Worker1 will do the same, but reading the second split
|
||||
along the kv_heads dimension, and so forth until "tp_ratio" D TP workers have pulled from P-Worker0.
|
||||
@@ -1538,6 +1463,7 @@ class NixlBaseConnectorWorker:
|
||||
start:end
|
||||
]
|
||||
nixl_agent_meta.block_lens = nixl_agent_meta.block_lens[start:end]
|
||||
nixl_agent_meta.block_strides = nixl_agent_meta.block_strides[start:end]
|
||||
|
||||
### Register remote engine in TransferTopology (idempotent).
|
||||
assert self.transfer_topo is not None
|
||||
@@ -1714,10 +1640,10 @@ class NixlBaseConnectorWorker:
|
||||
if not self.use_mla and nixl_agent_meta.kv_cache_layout != kv_cache_layout:
|
||||
if (
|
||||
self.kv_transfer_config.enable_permute_local_kv
|
||||
and nixl_agent_meta.kv_cache_layout == "HND"
|
||||
and nixl_agent_meta.kv_cache_layout == "LBHNC"
|
||||
):
|
||||
logger.info(
|
||||
"Remote is HND and local is NHD, enabled additional permute "
|
||||
"Remote is LBHNC and local is LBNHC, enabled additional permute "
|
||||
"on local device KV."
|
||||
)
|
||||
assert not self._is_hma_required, (
|
||||
@@ -1727,7 +1653,7 @@ class NixlBaseConnectorWorker:
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Heterogeneous TP expects same kv_cache_layout. "
|
||||
"Or enable experimental feature to use HND to NHD support by "
|
||||
"Or enable experimental feature to use HNC to NHC support by "
|
||||
"setting 'enable_permute_local_kv'=True in --kv-transfer-config."
|
||||
)
|
||||
# if remote_agent used attn is not same as local,
|
||||
@@ -1747,18 +1673,18 @@ class NixlBaseConnectorWorker:
|
||||
self.enable_heterogeneous_attn_post_process = True
|
||||
|
||||
# Heterogeneous TP requires head-splitting, which only works with
|
||||
# HND layout. MLA and replicated-KV cases don't split on heads.
|
||||
# Mamba doesn't support heterogeneous TP.
|
||||
# block-contiguous layouts (H before N, e.g. HNC / BHLNC).
|
||||
# MLA and replicated-KV cases don't split on heads.
|
||||
if (
|
||||
abs(tp_ratio) != 1
|
||||
and not self.use_mla
|
||||
and not self.transfer_topo.is_kv_replicated(remote_engine_id)
|
||||
and kv_cache_layout != "HND"
|
||||
and not KVCacheLayout[kv_cache_layout].is_block_contiguous
|
||||
and not self.enable_permute_local_kv
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Heterogeneous TP head-dimension splitting requires contiguous heads. "
|
||||
"Use HND layout on the prefill side."
|
||||
"Use HNC layout on the prefill side."
|
||||
)
|
||||
|
||||
# Per-region block_len validation enforcing the P/D invariant.
|
||||
@@ -1870,11 +1796,11 @@ class NixlBaseConnectorWorker:
|
||||
Post process device kv cache after receiving from remote.
|
||||
|
||||
3 types of post processing supported:
|
||||
* kv_cache_postprocess_layout => convert from HND to NHD
|
||||
* kv_cache_postprocess_layout => convert from HNC to NHC
|
||||
* kv_cache_postprocess_blksize => convert from small block size
|
||||
to large block size
|
||||
* kv_cache_postprocess_blksize_and_layout => convert from small
|
||||
block size to large block size and convert from HND to NHD
|
||||
block size to large block size and convert from HNC to NHC
|
||||
|
||||
"""
|
||||
if len(self.device_kv_caches) == 0:
|
||||
@@ -1884,14 +1810,14 @@ class NixlBaseConnectorWorker:
|
||||
if self.enable_permute_local_kv and block_size_ratio > 1:
|
||||
logger.debug(
|
||||
"Post-processing device kv cache on receive by converting "
|
||||
"block_size with %sx bigger and permuting layout from HND"
|
||||
" to NHD.",
|
||||
"block_size with %sx bigger and permuting layout from HNC"
|
||||
" to NHC.",
|
||||
block_size_ratio,
|
||||
)
|
||||
elif self.enable_permute_local_kv:
|
||||
logger.debug(
|
||||
"Post-processing device kv cache on receive by permuting layout"
|
||||
"from HND to NHD."
|
||||
"from HNC to NHC."
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
@@ -2333,7 +2259,7 @@ class NixlBaseConnectorWorker:
|
||||
|1st_split-2nd_split| |1st_split-2nd_split |
|
||||
"""
|
||||
assert self.transfer_topo is not None
|
||||
if self.transfer_topo.virtually_split_kv_in_blocks and mamba_view:
|
||||
if self.transfer_topo.is_kv_layout_blocks_first and mamba_view:
|
||||
block_len = self._mamba_ssm_size[not first_split]
|
||||
else:
|
||||
block_len = self.block_len_per_layer[layer_idx]
|
||||
|
||||
@@ -19,7 +19,6 @@ import torch
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.distributed.kv_transfer.kv_connector.utils import (
|
||||
EngineId,
|
||||
get_current_attn_backend,
|
||||
)
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.base import (
|
||||
CopyBlocksOp,
|
||||
@@ -56,10 +55,8 @@ from vllm.distributed.kv_transfer.kv_connector.v1.nixl.stats import (
|
||||
)
|
||||
from vllm.forward_context import ForwardContext
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata
|
||||
from vllm.v1.attention.backends.utils import get_kv_cache_layout
|
||||
from vllm.v1.attention.backend import AttentionMetadata
|
||||
from vllm.v1.core.sched.output import SchedulerOutput
|
||||
from vllm.v1.kv_cache_interface import MambaSpec
|
||||
from vllm.v1.outputs import KVConnectorOutput
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -79,36 +76,6 @@ logger = init_logger(__name__)
|
||||
class NixlBaseConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
"""Base connector with common logic shared by pull and push modes."""
|
||||
|
||||
@property
|
||||
def prefer_cross_layer_blocks(self) -> bool:
|
||||
if any(
|
||||
[
|
||||
isinstance(group.kv_cache_spec, MambaSpec)
|
||||
for group in self.kv_cache_config.kv_cache_groups
|
||||
]
|
||||
):
|
||||
# Hybrid SSM models do not yet support cross-layer layout
|
||||
return False
|
||||
|
||||
backend = get_current_attn_backend(self._vllm_config)
|
||||
if backend.get_name() not in (
|
||||
"FLASH_ATTN",
|
||||
"FLASHINFER",
|
||||
"TRITON_ATTN",
|
||||
):
|
||||
return False
|
||||
|
||||
# For now there is no benefit to run cross layers when backend
|
||||
# does not support on HND
|
||||
if get_kv_cache_layout() != "HND":
|
||||
return False
|
||||
|
||||
extra_config = self.kv_transfer_config.kv_connector_extra_config
|
||||
return (
|
||||
str(extra_config.get("enable_cross_layers_blocks", "False")).lower()
|
||||
== "true"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vllm_config: VllmConfig,
|
||||
@@ -152,9 +119,10 @@ class NixlBaseConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
# which fallback to the default behavior.
|
||||
return None
|
||||
logger.info_once(
|
||||
"NixlConnector setting KV cache layout to HND for better xfer performance."
|
||||
"NixlConnector setting KV cache layout to LBHNC for "
|
||||
"better xfer performance."
|
||||
)
|
||||
return "HND"
|
||||
return "LBHNC"
|
||||
|
||||
############################################################
|
||||
# Scheduler Side Methods
|
||||
@@ -227,12 +195,6 @@ class NixlBaseConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
assert self.connector_worker is not None
|
||||
self.connector_worker.register_kv_caches(kv_caches)
|
||||
|
||||
def register_cross_layers_kv_cache(
|
||||
self, kv_cache: torch.Tensor, attn_backend: type[AttentionBackend]
|
||||
):
|
||||
assert self.connector_worker is not None
|
||||
self.connector_worker.register_cross_layers_kv_caches(kv_cache)
|
||||
|
||||
def set_host_xfer_buffer_ops(self, copy_operation: CopyBlocksOp):
|
||||
assert self.connector_worker is not None
|
||||
self.connector_worker.set_host_xfer_buffer_ops(copy_operation)
|
||||
|
||||
@@ -41,8 +41,9 @@ PUSH_REG_NOTIF_PREFIX = b"PUSH_REG:"
|
||||
# 4: Add KV block lease renewal through heartbeats
|
||||
# 5: Add remote_blocks_expiry_time to kv_transfer_params + handshake
|
||||
# clock-sync timestamp
|
||||
# 6: Add block_strides
|
||||
#
|
||||
NIXL_CONNECTOR_VERSION: int = 5
|
||||
NIXL_CONNECTOR_VERSION: int = 6
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -53,6 +54,7 @@ class NixlAgentMetadata:
|
||||
device_id: int
|
||||
num_blocks: int
|
||||
block_lens: list[int]
|
||||
block_strides: list[int]
|
||||
kv_cache_layout: str
|
||||
block_size: int
|
||||
ssm_sizes: tuple[int, int]
|
||||
@@ -79,7 +81,7 @@ class NixlHandshakePayload(KVConnectorHandshakeMetadata):
|
||||
|
||||
|
||||
def compute_nixl_compatibility_hash(
|
||||
vllm_config: VllmConfig, attn_backend_name: str, cross_layers_blocks: bool
|
||||
vllm_config: VllmConfig, attn_backend_name: str
|
||||
) -> str:
|
||||
"""
|
||||
Compute compatibility hash for NIXL KV transfer.
|
||||
@@ -123,7 +125,6 @@ def compute_nixl_compatibility_hash(
|
||||
# Attention backend and KV cache dtype affect memory layout
|
||||
"attn_backend_name": attn_backend_name,
|
||||
"cache_dtype": str(cache_config.cache_dtype),
|
||||
"cross_layers_blocks": cross_layers_blocks,
|
||||
"is_hma_enabled": is_hma_enabled,
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,7 @@ from vllm.v1.kv_offload.config import (
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheTensor
|
||||
|
||||
|
||||
def is_kv_cache_tensor_packed(kv_cache_tensor: "KVCacheTensor") -> bool:
|
||||
"""Return whether a KV cache tensor uses a packed block stride."""
|
||||
return bool(kv_cache_tensor.block_stride)
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig
|
||||
|
||||
|
||||
def build_offloading_config(
|
||||
@@ -89,16 +84,8 @@ def build_offloading_config(
|
||||
|
||||
worker_kv_bytes_per_block = 0
|
||||
if kv_cache_config.num_blocks > 0:
|
||||
packed_tensors = tuple(
|
||||
is_kv_cache_tensor_packed(tensor)
|
||||
for tensor in kv_cache_config.kv_cache_tensors
|
||||
)
|
||||
is_packed = any(packed_tensors)
|
||||
assert not is_packed or all(packed_tensors)
|
||||
total_gpu_kv_bytes = (
|
||||
kv_cache_config.kv_cache_tensors[0].size
|
||||
if is_packed
|
||||
else sum(tensor.size for tensor in kv_cache_config.kv_cache_tensors)
|
||||
total_gpu_kv_bytes = sum(
|
||||
tensor.size for tensor in kv_cache_config.kv_cache_tensors
|
||||
)
|
||||
worker_kv_bytes_per_block = total_gpu_kv_bytes // kv_cache_config.num_blocks
|
||||
|
||||
|
||||
@@ -10,11 +10,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.offloading.common import (
|
||||
OffloadingWorkerMetadata,
|
||||
ReqId,
|
||||
)
|
||||
from vllm.distributed.kv_transfer.kv_connector.v1.offloading.config import (
|
||||
is_kv_cache_tensor_packed,
|
||||
)
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.attention.backend import AttentionBackend
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
AttentionSpec,
|
||||
KVCacheConfig,
|
||||
@@ -56,21 +52,14 @@ class OffloadingConnectorWorker:
|
||||
def _init_worker(self, kv_caches: CanonicalKVCaches) -> None:
|
||||
self.worker = self.spec.get_worker(kv_caches)
|
||||
|
||||
def register_kv_caches(self, kv_caches: dict[str, torch.Tensor]):
|
||||
def register_kv_caches(
|
||||
self, kv_caches: dict[str, torch.Tensor | list[torch.Tensor]]
|
||||
):
|
||||
kv_cache_config = self.kv_cache_config
|
||||
num_blocks = kv_cache_config.num_blocks
|
||||
|
||||
# Packed layouts (e.g. DSv4) set block_stride > 0; their tensors use
|
||||
# stride(0) as the manager-block stride (equals total_num_bytes_per_block).
|
||||
# General (non-packed) layouts size the tensor at page_size_bytes per
|
||||
# manager block, so page_size_bytes is the correct offloading stride.
|
||||
layer_is_packed: dict[str, bool] = {
|
||||
ln: is_kv_cache_tensor_packed(kv_tensor)
|
||||
for kv_tensor in kv_cache_config.kv_cache_tensors
|
||||
for ln in kv_tensor.shared_by
|
||||
}
|
||||
|
||||
# layer_name -> (num_blocks, page_size_bytes) tensor
|
||||
# layer_name -> (num_blocks, page_size_bytes) int8 view.
|
||||
# Standardized layouts always have num_blocks as the leading dim.
|
||||
tensors_per_block: dict[str, tuple[torch.Tensor, ...]] = {}
|
||||
# layer_name -> size of (un-padded) page in bytes
|
||||
unpadded_page_size_bytes: dict[str, int] = {}
|
||||
@@ -87,63 +76,62 @@ class OffloadingConnectorWorker:
|
||||
layer_kv_cache_spec = per_layer_specs.get(
|
||||
layer_name, group_kv_cache_spec
|
||||
)
|
||||
if isinstance(layer_kv_cache_spec, AttentionSpec):
|
||||
layer_kv_cache = kv_caches[layer_name]
|
||||
assert isinstance(layer_kv_cache, torch.Tensor)
|
||||
layer_kv_cache = kv_caches[layer_name]
|
||||
# AttentionSpec yields a single tensor; MambaSpec yields a
|
||||
# list of typed state tensors that share one underlying
|
||||
# buffer. Either way, the first tensor's storage_offset
|
||||
# marks the start of this layer's region.
|
||||
ref = (
|
||||
layer_kv_cache[0]
|
||||
if isinstance(layer_kv_cache, list)
|
||||
else layer_kv_cache
|
||||
)
|
||||
page = layer_kv_cache_spec.page_size_bytes
|
||||
elem_size = ref.element_size()
|
||||
byte_offset = ref.storage_offset() * elem_size
|
||||
# Packed layouts (e.g. DSv4) interleave layers per block, so
|
||||
# the attention tensor's stride(0) (the manager-block stride)
|
||||
# exceeds page_size_bytes. Non-packed layouts have
|
||||
# stride(0) == page_size_bytes.
|
||||
block_stride_bytes = (
|
||||
ref.stride(0) * elem_size
|
||||
if isinstance(layer_kv_cache_spec, AttentionSpec)
|
||||
else page
|
||||
)
|
||||
tensors_per_block[layer_name] = (
|
||||
torch.tensor([], dtype=torch.int8, device=ref.device).set_(
|
||||
ref.untyped_storage(),
|
||||
byte_offset,
|
||||
(num_blocks, page),
|
||||
(block_stride_bytes, 1),
|
||||
),
|
||||
)
|
||||
page_size_bytes[layer_name] = page
|
||||
|
||||
page = layer_kv_cache_spec.page_size_bytes
|
||||
elem_size = layer_kv_cache.element_size()
|
||||
byte_offset = layer_kv_cache.storage_offset() * elem_size
|
||||
block_stride_bytes = (
|
||||
layer_kv_cache.stride(0) * elem_size
|
||||
if layer_is_packed[layer_name]
|
||||
else page
|
||||
)
|
||||
raw = torch.empty(
|
||||
0,
|
||||
dtype=torch.int8,
|
||||
device=layer_kv_cache.device,
|
||||
).set_(layer_kv_cache.untyped_storage())
|
||||
tensors_per_block[layer_name] = (
|
||||
torch.as_strided(
|
||||
raw,
|
||||
(num_blocks, page),
|
||||
(block_stride_bytes, 1),
|
||||
byte_offset,
|
||||
),
|
||||
)
|
||||
page_size_bytes[layer_name] = layer_kv_cache_spec.page_size_bytes
|
||||
if isinstance(layer_kv_cache_spec, AttentionSpec):
|
||||
unpadded_page_size_bytes[layer_name] = (
|
||||
layer_kv_cache_spec.unpadded_page_size_bytes
|
||||
)
|
||||
|
||||
elif isinstance(layer_kv_cache_spec, MambaSpec):
|
||||
layer_kv_cache = kv_caches[layer_name]
|
||||
assert layer_kv_cache.dtype == torch.int8
|
||||
tensors_per_block[layer_name] = (
|
||||
layer_kv_cache.view(
|
||||
num_blocks, layer_kv_cache_spec.page_size_bytes
|
||||
),
|
||||
)
|
||||
|
||||
page_size_bytes[layer_name] = layer_kv_cache_spec.page_size_bytes
|
||||
unpadded_page_size_bytes[layer_name] = replace(
|
||||
layer_kv_cache_spec, page_size_padded=None
|
||||
).page_size_bytes
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
packed_kv_cache_tensor = next(
|
||||
# Packed layouts (e.g. DSv4) interleave all layers within each
|
||||
# manager block: a layer view's block stride exceeds its page size.
|
||||
# Offload the whole packed block as a single transfer region.
|
||||
packed_layer_name = next(
|
||||
(
|
||||
t
|
||||
for t in kv_cache_config.kv_cache_tensors
|
||||
if is_kv_cache_tensor_packed(t) and t.shared_by
|
||||
layer_name
|
||||
for layer_name, (tensor,) in tensors_per_block.items()
|
||||
if tensor.stride(0) != tensor.shape[1]
|
||||
),
|
||||
None,
|
||||
)
|
||||
if packed_kv_cache_tensor is not None:
|
||||
(tensor,) = tensors_per_block[packed_kv_cache_tensor.shared_by[0]]
|
||||
if packed_layer_name is not None:
|
||||
(tensor,) = tensors_per_block[packed_layer_name]
|
||||
block_stride = tensor.stride(0)
|
||||
packed_tensor = tensor.as_strided(
|
||||
(num_blocks, block_stride),
|
||||
@@ -164,46 +152,43 @@ class OffloadingConnectorWorker:
|
||||
block_tensors: list[CanonicalKVCacheTensor] = []
|
||||
block_data_refs: dict[str, list[CanonicalKVCacheRef]] = defaultdict(list)
|
||||
for kv_cache_tensor in kv_cache_config.kv_cache_tensors:
|
||||
# Filter to layers that were actually processed above.
|
||||
# Packed KV allocation emits KVCacheTensor entries for
|
||||
# every (tuple_idx, page_size) slot; slots where no group has a
|
||||
# layer at that index produce an empty shared_by (reserved memory
|
||||
# with no corresponding model layer).
|
||||
tensor_layer_names = [
|
||||
n for n in kv_cache_tensor.shared_by if n in tensors_per_block
|
||||
]
|
||||
if not tensor_layer_names:
|
||||
continue
|
||||
for slot_layers in kv_cache_tensor.shared_by:
|
||||
# Filter to layers that were actually processed above.
|
||||
# Some slots may have no corresponding model layer (reserved
|
||||
# memory with no group layer at that index).
|
||||
tensor_layer_names = [n for n in slot_layers if n in tensors_per_block]
|
||||
if not tensor_layer_names:
|
||||
continue
|
||||
|
||||
# verify all layers in the group reference the exact same tensors
|
||||
assert len({len(tensors_per_block[n]) for n in tensor_layer_names}) == 1
|
||||
assert (
|
||||
len({tensors_per_block[n][0].data_ptr() for n in tensor_layer_names})
|
||||
== 1
|
||||
)
|
||||
assert (
|
||||
len({tensors_per_block[n][0].stride() for n in tensor_layer_names}) == 1
|
||||
)
|
||||
|
||||
# pick the first layer to represent the group
|
||||
first_layer_name = tensor_layer_names[0]
|
||||
for tensor in tensors_per_block[first_layer_name]:
|
||||
block_tensors.append(
|
||||
CanonicalKVCacheTensor(
|
||||
tensor=tensor,
|
||||
page_size_bytes=page_size_bytes[first_layer_name],
|
||||
)
|
||||
# Verify all layers in the slot reference the same tensors.
|
||||
assert len({len(tensors_per_block[n]) for n in tensor_layer_names}) == 1
|
||||
data_ptrs = {
|
||||
n: tensors_per_block[n][0].data_ptr() for n in tensor_layer_names
|
||||
}
|
||||
assert len(set(data_ptrs.values())) == 1, data_ptrs
|
||||
assert (
|
||||
len({tensors_per_block[n][0].stride() for n in tensor_layer_names})
|
||||
== 1
|
||||
)
|
||||
|
||||
curr_tensor_idx = len(block_tensors) - 1
|
||||
for layer_name in tensor_layer_names:
|
||||
block_data_refs[layer_name].append(
|
||||
CanonicalKVCacheRef(
|
||||
tensor_idx=curr_tensor_idx,
|
||||
page_size_bytes=(unpadded_page_size_bytes[layer_name]),
|
||||
first_layer_name = tensor_layer_names[0]
|
||||
for tensor in tensors_per_block[first_layer_name]:
|
||||
block_tensors.append(
|
||||
CanonicalKVCacheTensor(
|
||||
tensor=tensor,
|
||||
page_size_bytes=page_size_bytes[first_layer_name],
|
||||
)
|
||||
)
|
||||
|
||||
curr_tensor_idx = len(block_tensors) - 1
|
||||
for layer_name in tensor_layer_names:
|
||||
block_data_refs[layer_name].append(
|
||||
CanonicalKVCacheRef(
|
||||
tensor_idx=curr_tensor_idx,
|
||||
page_size_bytes=(unpadded_page_size_bytes[layer_name]),
|
||||
)
|
||||
)
|
||||
|
||||
group_data_refs: list[list[CanonicalKVCacheRef]] = []
|
||||
for kv_cache_group in kv_cache_config.kv_cache_groups:
|
||||
group_refs: list[CanonicalKVCacheRef] = []
|
||||
@@ -218,53 +203,6 @@ class OffloadingConnectorWorker:
|
||||
|
||||
self._init_worker(canonical_kv_caches)
|
||||
|
||||
def register_cross_layers_kv_cache(
|
||||
self, kv_cache: torch.Tensor, attn_backend: type[AttentionBackend]
|
||||
):
|
||||
# verify that num_blocks is at physical position 0 in the cross-layers
|
||||
# tensor layout.
|
||||
test_shape = attn_backend.get_kv_cache_shape(
|
||||
num_blocks=1234, block_size=16, num_kv_heads=1, head_size=256
|
||||
)
|
||||
num_blocks_logical_dim = test_shape.index(1234) + 1
|
||||
physical_to_logical = attn_backend.get_kv_cache_stride_order(
|
||||
include_num_layers_dimension=True
|
||||
)
|
||||
num_blocks_physical_dim = physical_to_logical.index(num_blocks_logical_dim)
|
||||
assert num_blocks_physical_dim == 0
|
||||
|
||||
kv_cache_groups = self.kv_cache_config.kv_cache_groups
|
||||
assert len(kv_cache_groups) == 1
|
||||
kv_cache_spec = kv_cache_groups[0].kv_cache_spec
|
||||
num_layers = len(kv_cache_groups[0].layer_names)
|
||||
page_size_bytes = kv_cache_spec.page_size_bytes * num_layers
|
||||
|
||||
assert kv_cache.storage_offset() == 0
|
||||
storage = kv_cache.untyped_storage()
|
||||
assert len(storage) % page_size_bytes == 0
|
||||
num_blocks = len(storage) // page_size_bytes
|
||||
tensor = (
|
||||
torch.tensor(
|
||||
[],
|
||||
dtype=torch.int8,
|
||||
device=kv_cache.device,
|
||||
)
|
||||
.set_(storage)
|
||||
.view(num_blocks, page_size_bytes)
|
||||
)
|
||||
kv_cache_tensor = CanonicalKVCacheTensor(
|
||||
tensor=tensor, page_size_bytes=page_size_bytes
|
||||
)
|
||||
# in cross layers layout, there's currently only a single group
|
||||
kv_cache_data_ref = CanonicalKVCacheRef(
|
||||
tensor_idx=0, page_size_bytes=page_size_bytes
|
||||
)
|
||||
canonical_kv_caches = CanonicalKVCaches(
|
||||
tensors=[kv_cache_tensor], group_data_refs=[[kv_cache_data_ref]]
|
||||
)
|
||||
|
||||
self._init_worker(canonical_kv_caches)
|
||||
|
||||
def handle_preemptions(self, kv_connector_metadata: OffloadingConnectorMetadata):
|
||||
assert self.worker is not None
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.offloading.worker import (
|
||||
OffloadingConnectorWorker,
|
||||
)
|
||||
from vllm.forward_context import ForwardContext
|
||||
from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata
|
||||
from vllm.v1.attention.backend import AttentionMetadata
|
||||
from vllm.v1.core.kv_cache_manager import KVCacheBlocks
|
||||
from vllm.v1.core.sched.output import SchedulerOutput
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig
|
||||
@@ -47,10 +47,6 @@ from vllm.v1.request import Request
|
||||
|
||||
|
||||
class OffloadingConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
@property
|
||||
def prefer_cross_layer_blocks(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vllm_config: VllmConfig,
|
||||
@@ -81,12 +77,6 @@ class OffloadingConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
assert self.connector_worker is not None
|
||||
self.connector_worker.register_kv_caches(kv_caches)
|
||||
|
||||
def register_cross_layers_kv_cache(
|
||||
self, kv_cache: torch.Tensor, attn_backend: type[AttentionBackend]
|
||||
):
|
||||
assert self.connector_worker is not None
|
||||
self.connector_worker.register_cross_layers_kv_cache(kv_cache, attn_backend)
|
||||
|
||||
def handle_preemptions(self, kv_connector_metadata: KVConnectorMetadata):
|
||||
assert self.connector_worker is not None
|
||||
assert isinstance(kv_connector_metadata, OffloadingConnectorMetadata)
|
||||
@@ -186,7 +176,7 @@ class OffloadingConnector(KVConnectorBase_V1, SupportsHMA):
|
||||
|
||||
@classmethod
|
||||
def get_required_kvcache_layout(cls, vllm_config: VllmConfig) -> str | None:
|
||||
return "HND"
|
||||
return "LBHNC"
|
||||
|
||||
def reset_cache(self) -> bool | None:
|
||||
assert self.connector_scheduler is not None
|
||||
|
||||
+32
-7
@@ -223,7 +223,20 @@ if TYPE_CHECKING:
|
||||
VLLM_MQ_MAX_CHUNK_BYTES_MB: int = 16
|
||||
VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS: int = 300
|
||||
VLLM_WORKER_SHUTDOWN_TIMEOUT_SECONDS: int = 5
|
||||
VLLM_KV_CACHE_LAYOUT: Literal["NHD", "HND"] | None = None
|
||||
VLLM_KV_CACHE_LAYOUT: (
|
||||
Literal[
|
||||
"LBNHC",
|
||||
"LBHNC",
|
||||
"NHC",
|
||||
"HNC",
|
||||
"NHD",
|
||||
"HND",
|
||||
"BLHNC",
|
||||
"BLNHC",
|
||||
"BHLNC",
|
||||
]
|
||||
| None
|
||||
) = None
|
||||
VLLM_SSM_CONV_STATE_LAYOUT: Literal["SD", "DS"] | None = None
|
||||
VLLM_COMPUTE_NANS_IN_LOGITS: bool = False
|
||||
VLLM_ROCM_QUICK_REDUCE_QUANTIZATION: Literal[
|
||||
@@ -1684,18 +1697,30 @@ environment_variables: dict[str, Callable[[], Any]] = {
|
||||
),
|
||||
# KV Cache layout used throughout vllm.
|
||||
# Some common values are:
|
||||
# - NHD
|
||||
# - HND
|
||||
# Where N=num_blocks, H=num_heads and D=head_size. The default value will
|
||||
# leave the layout choice to the backend. Mind that backends may only
|
||||
# - LBNHC
|
||||
# - LBHNC
|
||||
# Where N=num_states, H=num_heads and C=state_content. The default value
|
||||
# will leave the layout choice to the backend. Mind that backends may only
|
||||
# implement and support a subset of all possible layouts.
|
||||
"VLLM_KV_CACHE_LAYOUT": env_with_choices(
|
||||
"VLLM_KV_CACHE_LAYOUT", None, ["NHD", "HND"]
|
||||
"VLLM_KV_CACHE_LAYOUT",
|
||||
None,
|
||||
[
|
||||
"LBNHC",
|
||||
"LBHNC",
|
||||
"NHC",
|
||||
"HNC",
|
||||
"NHD",
|
||||
"HND",
|
||||
"BLHNC",
|
||||
"BLNHC",
|
||||
"BHLNC",
|
||||
],
|
||||
),
|
||||
# SSM conv state layout used for Mamba models.
|
||||
# - SD: (state_len, dim) — dim contiguous (default)
|
||||
# - DS: (dim, state_len) — TP-sharded dim on dim1,
|
||||
# consistent with SSM temporal state and HND KV cache layout.
|
||||
# consistent with SSM temporal state and LBHNC KV cache layout.
|
||||
"VLLM_SSM_CONV_STATE_LAYOUT": env_with_choices(
|
||||
"VLLM_SSM_CONV_STATE_LAYOUT", None, ["SD", "DS"]
|
||||
),
|
||||
|
||||
@@ -577,6 +577,10 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
compile_native=True,
|
||||
)
|
||||
|
||||
def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
|
||||
# [B, H=1, N, C] -> [B, N, C]
|
||||
self.kv_cache = kv_cache.squeeze(1)
|
||||
|
||||
@property
|
||||
def chunked_prefill_workspace_size(self) -> int:
|
||||
if self._chunked_prefill_workspace_size is None:
|
||||
@@ -1337,27 +1341,6 @@ class MLACommonBackend(AttentionBackend):
|
||||
def get_builder_cls() -> type["MLACommonMetadataBuilder"]:
|
||||
return MLACommonMetadataBuilder
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int, # assumed to be 1 for MLA
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (num_blocks, block_size, head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
if include_num_layers_dimension:
|
||||
# Default to identity permutation to signal cross-layer allocation
|
||||
# is unsupported. Each MLA backend must opt in to support cross-layer
|
||||
# allocation by overriding this method.
|
||||
return (0, 1, 2, 3)
|
||||
return (0, 1, 2)
|
||||
|
||||
@classmethod
|
||||
def get_supported_head_sizes(cls) -> list[int]:
|
||||
return [320, 576]
|
||||
@@ -2219,7 +2202,7 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
|
||||
)
|
||||
elif not use_fp8_prefill:
|
||||
ops.gather_and_maybe_dequant_cache(
|
||||
src_cache=kv_c_and_k_pe_cache,
|
||||
src_cache=kv_c_and_k_pe_cache.squeeze(1),
|
||||
dst=workspace,
|
||||
block_table=prefill_metadata.block_table,
|
||||
cu_seq_lens=prefill_metadata.chunked_context.cu_seq_lens[i],
|
||||
@@ -2232,7 +2215,7 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
|
||||
else:
|
||||
# FP8 path: gather cache without dequantization
|
||||
ops.cp_gather_cache(
|
||||
src_cache=kv_c_and_k_pe_cache,
|
||||
src_cache=kv_c_and_k_pe_cache.squeeze(1),
|
||||
dst=workspace,
|
||||
block_table=prefill_metadata.block_table,
|
||||
cu_seq_lens=prefill_metadata.chunked_context.cu_seq_lens[i],
|
||||
@@ -2344,7 +2327,7 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
|
||||
elif is_quantized_kv_cache(self.kv_cache_dtype):
|
||||
assert k_scale is not None
|
||||
ops.gather_and_maybe_dequant_cache(
|
||||
src_cache=kv_c_and_k_pe_cache,
|
||||
src_cache=kv_c_and_k_pe_cache.squeeze(1),
|
||||
dst=workspace,
|
||||
block_table=prefill_metadata.block_table,
|
||||
cu_seq_lens=padded_local_cu_seq_lens,
|
||||
@@ -2358,7 +2341,7 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
|
||||
)
|
||||
else:
|
||||
ops.cp_gather_cache(
|
||||
src_cache=kv_c_and_k_pe_cache,
|
||||
src_cache=kv_c_and_k_pe_cache.squeeze(1),
|
||||
dst=workspace,
|
||||
block_table=prefill_metadata.block_table,
|
||||
cu_seq_lens=padded_local_cu_seq_lens,
|
||||
|
||||
@@ -24,11 +24,7 @@ class AttentionLayerBase(ABC):
|
||||
supports_dcp: bool = True
|
||||
|
||||
def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
|
||||
"""Bind the allocated KV cache tensor to this layer.
|
||||
|
||||
The default stores the cache view as-is; subclasses (e.g. Mamba)
|
||||
override this to unpack the raw buffer into per-state views.
|
||||
"""
|
||||
"""Bind a ``[B, H, N, C]`` cache view; override to reshape."""
|
||||
self.kv_cache = kv_cache
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -27,11 +27,7 @@ class MambaBase(AttentionLayerBase):
|
||||
supports_dcp: bool = False
|
||||
|
||||
def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
|
||||
"""Unpack a raw ``[B, 1, 1, C]`` int8 page view into per-state views.
|
||||
|
||||
Each block's ``C`` bytes hold the layer's states (e.g. conv, ssm)
|
||||
packed contiguously; slice them out and reinterpret per dtype/shape.
|
||||
"""
|
||||
"""Unpack a raw 4D ``[B, 1, 1, C]`` int8 view into per-state views."""
|
||||
pages = kv_cache.squeeze(dim=(1, 2))
|
||||
states: list[torch.Tensor] = []
|
||||
offset = 0
|
||||
|
||||
@@ -28,7 +28,7 @@ def get_conv_state_layout() -> ConvStateLayoutType:
|
||||
"""Return the SSM conv state layout.
|
||||
|
||||
SD = (state_len, dim) — dim is the innermost contiguous dimension.
|
||||
DS = (dim, state_len) — TP-sharded dim is on dim-1 (like HND for KV
|
||||
DS = (dim, state_len) — TP-sharded dim is on dim-1 (like HNC for KV
|
||||
cache), consistent with SSM temporal state layout.
|
||||
"""
|
||||
layout: ConvStateLayoutType | None = envs.VLLM_SSM_CONV_STATE_LAYOUT
|
||||
|
||||
@@ -80,14 +80,14 @@ def dummy_attention(layer_name, _placeholder):
|
||||
|
||||
def basic_cache(
|
||||
to_cache: torch.Tensor, # shape: [seq_len, num_heads, head_size]
|
||||
kv_cache: torch.Tensor, # shape: [num_blocks, block_size, num_heads, head_size]
|
||||
kv_cache: torch.Tensor, # shape: [num_blocks, num_heads, block_size, head_size]
|
||||
slot_mapping: torch.Tensor, # shape: [seq_len]
|
||||
):
|
||||
# Padding slots are -1; redirect them to the null block (block 0, never
|
||||
# allocated to a request) so the scatter stays branch-free and sync-free.
|
||||
block_size = kv_cache.shape[1]
|
||||
block_size = kv_cache.shape[2]
|
||||
slot_mapping = slot_mapping.clamp_min(0)
|
||||
kv_cache[slot_mapping // block_size, slot_mapping % block_size] = to_cache
|
||||
kv_cache[slot_mapping // block_size, :, slot_mapping % block_size] = to_cache
|
||||
|
||||
|
||||
######### CacheOnlyAttentionBackend ########
|
||||
@@ -123,18 +123,6 @@ class CacheOnlyAttentionBackend(AttentionBackend):
|
||||
def get_impl_cls() -> type["CacheOnlyAttentionImpl"]:
|
||||
return CacheOnlyAttentionImpl
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
# We set `num_kv_heads = num_hidden_layers` and `head_size = hidden_size`
|
||||
# We also don't use a k/v (2) dim
|
||||
return (num_blocks, block_size, num_kv_heads, head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_builder_cls() -> type["CacheOnlyAttentionMetadataBuilder"]:
|
||||
return CacheOnlyAttentionMetadataBuilder
|
||||
|
||||
@@ -128,23 +128,13 @@ def create_whisper_attention_backend_with_block_pooling(
|
||||
vllm_config: VllmConfig,
|
||||
device: torch.device,
|
||||
):
|
||||
assert kv_cache_spec.num_kv_heads % block_pool_size == 0
|
||||
# Scale pooled-unit quantities back to unpooled (encoder-token)
|
||||
# units for the underlying attention metadata: the KV cache spec
|
||||
# counts blocks in pooled units, but the kernel runs on the
|
||||
# `block_pool_size`x-expanded sequence built below.
|
||||
spec_overrides = dict(
|
||||
block_size=kv_cache_spec.block_size * block_pool_size,
|
||||
num_kv_heads=kv_cache_spec.num_kv_heads // block_pool_size,
|
||||
)
|
||||
if isinstance(kv_cache_spec, SlidingWindowSpec):
|
||||
# The manager keeps `sliding_window` in pooled units; the kernel
|
||||
# runs on the expanded sequence, so give it the model's window in
|
||||
# unpooled units (`get_kv_cache_spec` sizes the pooled window so
|
||||
# this window always stays resident).
|
||||
assert sliding_window is not None
|
||||
spec_overrides["sliding_window"] = sliding_window
|
||||
kv_cache_spec = replace(kv_cache_spec, **spec_overrides)
|
||||
kv_cache_spec = replace(kv_cache_spec, sliding_window=sliding_window)
|
||||
super().__init__(kv_cache_spec, layer_names, vllm_config, device)
|
||||
# Override model_config-derived values with the actual
|
||||
# encoder values from kv_cache_spec
|
||||
@@ -261,18 +251,6 @@ def create_whisper_attention_backend_with_block_pooling(
|
||||
overrides={
|
||||
"get_builder_cls": lambda: WhisperCausalAttentionWithBlockPoolingBuilder,
|
||||
"get_impl_cls": lambda: WhisperCausalAttentionWithBlockPoolingImpl,
|
||||
"get_kv_cache_shape": lambda num_blocks,
|
||||
block_size,
|
||||
num_kv_heads,
|
||||
head_size,
|
||||
cache_dtype_str: underlying_attn_backend.get_kv_cache_shape(
|
||||
num_blocks,
|
||||
# we stretch each block by `block_pool_size`
|
||||
block_size * block_pool_size,
|
||||
num_kv_heads // block_pool_size,
|
||||
head_size,
|
||||
cache_dtype_str,
|
||||
),
|
||||
"forward_includes_kv_cache_update": True,
|
||||
},
|
||||
)
|
||||
@@ -339,9 +317,11 @@ class WhisperCausalAttentionWithBlockPooling(Attention):
|
||||
def get_kv_cache_spec(self, vllm_config: VllmConfig):
|
||||
kv_cache_spec = super().get_kv_cache_spec(vllm_config)
|
||||
assert isinstance(kv_cache_spec, AttentionSpec)
|
||||
assert kv_cache_spec.num_kv_heads % self.block_pool_size == 0
|
||||
kv_cache_spec = replace(
|
||||
kv_cache_spec,
|
||||
num_kv_heads=self.block_pool_size * kv_cache_spec.num_kv_heads,
|
||||
block_size=kv_cache_spec.block_size * self.block_pool_size,
|
||||
num_kv_heads=kv_cache_spec.num_kv_heads // self.block_pool_size,
|
||||
)
|
||||
if isinstance(kv_cache_spec, SlidingWindowSpec):
|
||||
# The manager counts blocks in pooled units, so express the window in
|
||||
|
||||
@@ -599,6 +599,10 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
|
||||
)
|
||||
return q_fp8
|
||||
|
||||
def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
|
||||
# [B, H=1, N, C] -> [B, N, C]
|
||||
self.kv_cache = kv_cache.squeeze(1)
|
||||
|
||||
def get_attn_backend(self) -> type[AttentionBackend]:
|
||||
return self.backend_cls
|
||||
|
||||
@@ -616,7 +620,7 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
|
||||
num_kv_heads=1,
|
||||
head_size=self.head_dim,
|
||||
dtype=torch.uint8 if uses_fp8_ds_mla_layout else self.kv_cache_torch_dtype,
|
||||
compress_ratio=self.compress_ratio,
|
||||
tokens_per_state=self.compress_ratio,
|
||||
cache_dtype_str=self.kv_cache_dtype,
|
||||
alignment=576 if uses_fp8_ds_mla_layout else 512,
|
||||
model_version="deepseek_v4",
|
||||
@@ -645,16 +649,20 @@ class DeepseekV4IndexerCache(torch.nn.Module, AttentionLayerBase):
|
||||
raise ValueError(f"Duplicate layer name: {prefix}")
|
||||
compilation_config.static_forward_context[prefix] = self
|
||||
|
||||
def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
|
||||
# [B, H=1, N, C] -> [B, N, C]
|
||||
self.kv_cache = kv_cache.squeeze(1)
|
||||
|
||||
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
|
||||
# head_dim already carries the fp8 scale padding
|
||||
# compress_ratio=1 for V3.2, >1 for DeepseekV4; both use the same cache layout.
|
||||
# tokens_per_state=1 for V3.2, >1 for DeepseekV4; same cache layout.
|
||||
uses_fp8_ds_mla_layout = vllm_config.cache_config.cache_dtype == "fp8_ds_mla"
|
||||
return MLAAttentionSpec(
|
||||
block_size=self.cache_config.block_size,
|
||||
num_kv_heads=1,
|
||||
head_size=self.head_dim,
|
||||
dtype=self.dtype,
|
||||
compress_ratio=self.compress_ratio,
|
||||
tokens_per_state=self.compress_ratio,
|
||||
# 576B for FlashMLA packing; 512B for FlashInfer sparse (#44577).
|
||||
alignment=576 if uses_fp8_ds_mla_layout else 512,
|
||||
)
|
||||
|
||||
@@ -62,25 +62,6 @@ class CompressorBackend(AttentionBackend):
|
||||
def get_builder_cls() -> type["CompressorMetadataBuilder"]:
|
||||
return CompressorMetadataBuilder
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
assert num_kv_heads == 1
|
||||
return (num_blocks, block_size, head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
if include_num_layers_dimension:
|
||||
return (0, 1, 2, 3)
|
||||
return (0, 1, 2)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompressorMetadata:
|
||||
@@ -166,6 +147,10 @@ class CompressorStateCache(torch.nn.Module, AttentionLayerBase):
|
||||
else:
|
||||
raise ValueError(f"Invalid compress ratio: {compress_ratio}")
|
||||
|
||||
def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
|
||||
# [B, H=1, N, C] -> [B, N, C]
|
||||
self.kv_cache = kv_cache.squeeze(1)
|
||||
|
||||
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
|
||||
# fp8_ds_mla is the UE8M0 paged layout and needs 576B alignment. Plain
|
||||
# full-cache rows share state pages with contiguous KV pages, so padding
|
||||
|
||||
@@ -21,7 +21,6 @@ from vllm.models.deepseek_v4.sparse_mla import (
|
||||
DeepseekV4FlashMLABackend,
|
||||
DeepseekV4FlashMLAMetadata,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.platforms.interface import DeviceCapability
|
||||
from vllm.utils.flashinfer import flashinfer_trtllm_batch_decode_sparse_mla_dsv4
|
||||
from vllm.v1.attention.backend import MultipleOf
|
||||
@@ -135,26 +134,6 @@ class DeepseekV4FlashInferMLASparseBackend(DeepseekV4FlashMLABackend):
|
||||
return None
|
||||
return "FLASHINFER_MLA_SPARSE_DSV4 requires SM10x or SM12x"
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
device_capability = current_platform.get_device_capability()
|
||||
if device_capability is not None and device_capability.major == 12:
|
||||
return DeepseekV4FlashMLABackend.get_kv_cache_shape(
|
||||
num_blocks,
|
||||
block_size,
|
||||
num_kv_heads,
|
||||
head_size,
|
||||
cache_dtype_str,
|
||||
)
|
||||
assert num_kv_heads == 1
|
||||
return (num_blocks, block_size, head_size)
|
||||
|
||||
|
||||
class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
|
||||
"""FlashInfer TRTLLM-gen sparse MLA attention layer for SM100 DeepSeek V4."""
|
||||
|
||||
@@ -2098,6 +2098,8 @@ def compress_norm_rope_store_cutedsl(
|
||||
store_full_fp8: bool = False,
|
||||
fp8_scale: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
# (B, H=1, N, C) -> (B, N, C)
|
||||
kv_cache = kv_cache.squeeze(1)
|
||||
if compress_ratio == 4:
|
||||
# For C4A, the single fused kernel is faster than the two-kernel version.
|
||||
fused_kv_compress_norm_rope_insert_sparse_attn_cutedsl(
|
||||
|
||||
@@ -92,21 +92,6 @@ class DeepseekV4FlashMLABackend(AttentionBackend):
|
||||
def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
|
||||
return capability.major in [9, 10]
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
if cache_dtype_str == "fp8_ds_mla":
|
||||
# DeepseekV4 main MLA: 584B per token (448 NoPE + 128 RoPE + 8 fp8 scale).
|
||||
# head_size passed in is the semantic head_dim (512).
|
||||
return (num_blocks, block_size, 584)
|
||||
else:
|
||||
return (num_blocks, block_size, head_size)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeepseekV4FlashMLAMetadata(AttentionMetadata):
|
||||
@@ -155,8 +140,7 @@ class DeepseekV4FlashMLAMetadataBuilder(
|
||||
(max_num_batched_tokens,), dtype=torch.int32, device=device
|
||||
)
|
||||
|
||||
assert hasattr(self.kv_cache_spec, "compress_ratio")
|
||||
self.compress_ratio = self.kv_cache_spec.compress_ratio
|
||||
self.compress_ratio = self.kv_cache_spec.tokens_per_state
|
||||
|
||||
# Pre-allocate compressed slot mapping buffer for CUDA graph address
|
||||
# stability when compress_ratio > 1.
|
||||
|
||||
@@ -109,30 +109,10 @@ class InklingSconvBackend(AttentionBackend):
|
||||
|
||||
@classmethod
|
||||
def indexes_kv_by_block_stride(cls) -> bool:
|
||||
# num_blocks is the outermost dim (HND, see get_kv_cache_shape), so the
|
||||
# padded conv page is read through a strided view.
|
||||
# The standardized layout keeps num_blocks outermost for this cache,
|
||||
# so the padded conv page is read through a strided view.
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
# HND, num-blocks-first, head-major: [num_blocks, H, N, D].
|
||||
return (num_blocks, num_kv_heads, block_size, head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
# Identity: physical layout == logical [num_blocks, H, N, D].
|
||||
if include_num_layers_dimension:
|
||||
return (0, 1, 2, 3, 4)
|
||||
return (0, 1, 2, 3)
|
||||
|
||||
@staticmethod
|
||||
def get_impl_cls():
|
||||
raise NotImplementedError(
|
||||
|
||||
@@ -705,6 +705,7 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
|
||||
head_size_v=self.head_dim,
|
||||
dtype=self.kv_cache_torch_dtype,
|
||||
kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype),
|
||||
separate_kv_head_groups=self.use_aiter_sparse_pa,
|
||||
)
|
||||
|
||||
def _ensure_aiter_sparse_pa_kv_cache(self) -> None:
|
||||
|
||||
@@ -97,25 +97,6 @@ class MiniMaxM3IndexerBackend(AttentionBackend):
|
||||
def is_sparse(cls) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (num_blocks, block_size, head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
if include_num_layers_dimension:
|
||||
# M3 does not use cross-layer (per-layer-stacked) KV blocks.
|
||||
raise NotImplementedError
|
||||
return (0, 1, 2)
|
||||
|
||||
|
||||
class MiniMaxM3IndexerCache(nn.Module, AttentionLayerBase):
|
||||
"""Side KV cache for the indexer's per-token index keys (key-only).
|
||||
@@ -156,6 +137,10 @@ class MiniMaxM3IndexerCache(nn.Module, AttentionLayerBase):
|
||||
raise ValueError(f"Duplicate layer name: {prefix}")
|
||||
compilation_config.static_forward_context[prefix] = self
|
||||
|
||||
def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
|
||||
# [B, H=1, N, C] -> [B, N, C]
|
||||
self.kv_cache = kv_cache.squeeze(1)
|
||||
|
||||
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
|
||||
# Key-only: MLAAttentionSpec budgets one vector/token (not 2x for K+V).
|
||||
return MLAAttentionSpec(
|
||||
|
||||
@@ -50,10 +50,7 @@ from vllm.v1.attention.backend import (
|
||||
CommonAttentionMetadata,
|
||||
MultipleOf,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import (
|
||||
get_kv_cache_layout,
|
||||
split_decodes_and_prefills,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import split_decodes_and_prefills
|
||||
from vllm.v1.kv_cache_interface import AttentionSpec, is_quantized_kv_cache
|
||||
|
||||
logger = init_logger(__name__)
|
||||
@@ -112,48 +109,6 @@ class MiniMaxM3SparseBackend(AttentionBackend):
|
||||
def is_sparse(cls) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
if minimax_m3_use_aiter_sparse_pa(num_kv_heads):
|
||||
# AITER's assembly paged-attention kernels require independently
|
||||
# contiguous K and V storage. Keep that specialized layout behind
|
||||
# the shuffle flag while every other implementation uses the
|
||||
# packed-content contract introduced by #44455.
|
||||
return (num_blocks, 2, block_size, num_kv_heads, head_size)
|
||||
# K and V are packed into the content dim: logical (B, H, N, 2*hs).
|
||||
return (num_blocks, num_kv_heads, block_size, 2 * head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
# `stride_order` indicates the permutation that gets us from
|
||||
# `get_kv_cache_shape` (logical (B, H, N, 2*hs)) to the actual memory
|
||||
# layout we want.
|
||||
if include_num_layers_dimension:
|
||||
raise NotImplementedError # no cross-layer KV blocks in M3
|
||||
if _minimax_m3_aiter_sparse_pa_requested():
|
||||
# The AITER page-16 sparse PA path reinterprets the K and V slices
|
||||
# as separate SHUFFLE caches. Keep K/V physically separated so
|
||||
# kv_cache[:, 0] and kv_cache[:, 1] are contiguous byte ranges.
|
||||
return (1, 0, 2, 3, 4)
|
||||
cache_layout = get_kv_cache_layout()
|
||||
if cache_layout == "NHD":
|
||||
# (num_blocks, block_size, num_kv_heads, 2*head_size)
|
||||
stride_order = (0, 2, 1, 3)
|
||||
elif cache_layout == "HND":
|
||||
# (num_blocks, num_kv_heads, block_size, 2*head_size)
|
||||
stride_order = (0, 1, 2, 3)
|
||||
else:
|
||||
raise ValueError(f"Unknown cache layout format {cache_layout}.")
|
||||
return stride_order
|
||||
|
||||
|
||||
@dataclass
|
||||
class MiniMaxM3SparsePrefillMetadata:
|
||||
|
||||
+13
-26
@@ -427,10 +427,6 @@ def _get_backend_priorities(
|
||||
]
|
||||
|
||||
backends = []
|
||||
# Keep ROCM_ATTN disabled for KV connectors until connector transfer
|
||||
# semantics are validated for its asymmetric native K/V cache views.
|
||||
if not use_kv_connector:
|
||||
backends.append(AttentionBackendEnum.ROCM_ATTN)
|
||||
if rocm_aiter_ops.is_mha_enabled():
|
||||
backends.append(AttentionBackendEnum.ROCM_AITER_FA)
|
||||
if is_aiter_found_and_supported():
|
||||
@@ -511,20 +507,6 @@ class RocmPlatform(Platform):
|
||||
attn_selector_config.use_sparse,
|
||||
attn_selector_config.use_kv_connector,
|
||||
)
|
||||
from vllm.config import get_current_vllm_config_or_none
|
||||
|
||||
vllm_config = get_current_vllm_config_or_none()
|
||||
is_encoder_decoder = (
|
||||
getattr(getattr(vllm_config, "model_config", None), "attn_type", None)
|
||||
== "encoder_decoder"
|
||||
)
|
||||
# ROCM_ATTN still uses a legacy attention layout (KV is the outer
|
||||
# dimension) that is incompatible with the encoder backend layouts. The
|
||||
# encoder and decoder need the layouts to match. This is currently
|
||||
# enforced implicitly.
|
||||
# TODO: Make this explicit in the selector in a future PR.
|
||||
if is_encoder_decoder and AttentionBackendEnum.ROCM_ATTN in backend_priorities:
|
||||
backend_priorities.remove(AttentionBackendEnum.ROCM_ATTN)
|
||||
for priority, backend in enumerate(backend_priorities):
|
||||
try:
|
||||
backend_class = backend.get_class()
|
||||
@@ -553,14 +535,19 @@ class RocmPlatform(Platform):
|
||||
|
||||
# First try checking just the selected backend, if there is one.
|
||||
if selected_backend is not None:
|
||||
try:
|
||||
backend_class = selected_backend.get_class()
|
||||
invalid_reasons = backend_class.validate_configuration(
|
||||
device_capability=device_capability,
|
||||
**attn_selector_config._asdict(),
|
||||
)
|
||||
except ImportError:
|
||||
invalid_reasons = ["ImportError"]
|
||||
if selected_backend == AttentionBackendEnum.ROCM_ATTN:
|
||||
invalid_reasons = [
|
||||
"ROCM_ATTN does not support standardized packed KV caches"
|
||||
]
|
||||
else:
|
||||
try:
|
||||
backend_class = selected_backend.get_class()
|
||||
invalid_reasons = backend_class.validate_configuration(
|
||||
device_capability=device_capability,
|
||||
**attn_selector_config._asdict(),
|
||||
)
|
||||
except ImportError:
|
||||
invalid_reasons = ["ImportError"]
|
||||
if invalid_reasons:
|
||||
raise ValueError(
|
||||
f"Selected backend {selected_backend} is not valid for "
|
||||
|
||||
@@ -124,6 +124,14 @@ class XPUPlatform(Platform):
|
||||
attn_selector_config: "AttentionSelectorConfig",
|
||||
num_heads: int | None = None,
|
||||
) -> str:
|
||||
from vllm.v1.attention.backends.utils import set_kv_cache_layout
|
||||
|
||||
set_kv_cache_layout("LBNHC")
|
||||
logger.info_once(
|
||||
"Setting VLLM_KV_CACHE_LAYOUT to 'LBNHC' for XPU; "
|
||||
"only LBNHC layout is supported by XPU attention kernels."
|
||||
)
|
||||
|
||||
# TurboQuant KV cache: route directly to TQ backend
|
||||
kv_cache_dtype = attn_selector_config.kv_cache_dtype
|
||||
if kv_cache_dtype is not None and kv_cache_dtype.startswith("turboquant_"):
|
||||
|
||||
+11
-11
@@ -422,11 +422,11 @@ def nvfp4_split_data_scale(
|
||||
"""Split one side (K or V) of an NVFP4 KV cache into data and scale.
|
||||
|
||||
The input is a 4D uint8 tensor whose last dimension is
|
||||
``full_dim = data_dim + scale_dim``. The physical layout within each
|
||||
side is ``[data | scale]``, both packed contiguously.
|
||||
``full_dim = data_dim + scale_dim``. The physical layout within
|
||||
each side is ``[data | scale]``, both packed contiguously.
|
||||
|
||||
The caller is responsible for slicing K and V from the combined cache
|
||||
first (e.g. ``kv_cache.split(num_kv_heads, dim=1)``).
|
||||
The caller is responsible for slicing K and V from the combined
|
||||
cache first (e.g. ``kv_cache.split(num_kv_heads, dim=1)``).
|
||||
|
||||
Args:
|
||||
kv_side: 4D uint8 tensor ``(B, H, N, full_dim)``.
|
||||
@@ -444,9 +444,6 @@ def nvfp4_split_data_scale(
|
||||
data_per_kv = dim_1 * dim_2 * data_dim
|
||||
page_bytes = kv_side.stride(0)
|
||||
|
||||
# Derive inner strides from the kv_side strides, scaling by the
|
||||
# ratio of the target dim to full_dim. This preserves the physical
|
||||
# layout (NHD vs HND) encoded in the input tensor's strides.
|
||||
s1 = kv_side.stride(1) * data_dim // full_dim
|
||||
s2 = kv_side.stride(2) * data_dim // full_dim
|
||||
data_shape = (num_pages, dim_1, dim_2, data_dim)
|
||||
@@ -460,7 +457,10 @@ def nvfp4_split_data_scale(
|
||||
base = kv_side.storage_offset()
|
||||
data = torch.as_strided(kv_side, data_shape, data_strides, storage_offset=base)
|
||||
scale = torch.as_strided(
|
||||
kv_side, scale_shape, scale_strides, storage_offset=base + data_per_kv
|
||||
kv_side,
|
||||
scale_shape,
|
||||
scale_strides,
|
||||
storage_offset=base + data_per_kv,
|
||||
).view(torch.float8_e4m3fn)
|
||||
|
||||
return data, scale
|
||||
@@ -476,14 +476,14 @@ def create_kv_caches_with_random_flash(
|
||||
model_dtype: str | torch.dtype | None = None,
|
||||
seed: int | None = None,
|
||||
device: str | None = "cuda",
|
||||
cache_layout: str | None = "NHD",
|
||||
cache_layout: str | None = "LBNHC",
|
||||
) -> tuple[list[torch.Tensor], list[torch.Tensor]]:
|
||||
set_random_seed(seed)
|
||||
|
||||
dtype = get_kv_cache_torch_dtype(cache_dtype, model_dtype)
|
||||
generic_kv_cache_shape = (num_blocks, 2, block_size, num_heads, head_size)
|
||||
assert cache_layout in ("NHD", "HND")
|
||||
stride_order = (0, 1, 2, 3, 4) if cache_layout == "NHD" else (0, 1, 3, 2, 4)
|
||||
assert cache_layout in ("LBNHC", "LBHNC")
|
||||
stride_order = (0, 1, 2, 3, 4) if cache_layout == "LBNHC" else (0, 1, 3, 2, 4)
|
||||
|
||||
kv_cache_allocation_shape = tuple(generic_kv_cache_shape[i] for i in stride_order)
|
||||
scale = head_size**-0.5
|
||||
|
||||
@@ -24,7 +24,6 @@ if TYPE_CHECKING:
|
||||
from vllm.model_executor.layers.linear import ColumnParallelLinear
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import QuantKey
|
||||
from vllm.platforms.interface import DeviceCapability
|
||||
from vllm.v1.attention.backends.utils import KVCacheLayoutType
|
||||
from vllm.v1.kv_cache_interface import AttentionSpec, KVQuantMode
|
||||
|
||||
from vllm.v1.kv_cache_interface import get_kv_quant_mode
|
||||
@@ -85,72 +84,14 @@ class AttentionBackend(ABC):
|
||||
def get_builder_cls(): # -> Type["AttentionMetadataBuilder"]:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_kv_cache_block_dim(
|
||||
cls,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> int:
|
||||
"""Discover which tensor dim is the block index, since different
|
||||
backends lay out dims differently."""
|
||||
_S = 1234567
|
||||
shape = cls.get_kv_cache_shape(
|
||||
_S,
|
||||
block_size,
|
||||
num_kv_heads,
|
||||
head_size,
|
||||
cache_dtype_str=cache_dtype_str,
|
||||
)
|
||||
return shape.index(_S)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
"""
|
||||
Get the physical (memory layout) ordering of the kv cache dimensions.
|
||||
Standard attention backends pack K and V into the content dim, giving
|
||||
the logical shape [num_blocks, num_heads, block_size, 2 * head_size].
|
||||
e.g. if get_kv_cache_stride_order returns (0, 2, 1, 3) then the physical
|
||||
ordering of dimensions is
|
||||
[num_blocks, block_size, num_heads, 2 * head_size].
|
||||
|
||||
If this function is unimplemented / raises NotImplementedError,
|
||||
the physical layout of the KV cache will match the logical shape.
|
||||
|
||||
Args:
|
||||
include_num_layers_dimension: if True, includes an additional
|
||||
num_layers dimension, which is assumed to be prepended
|
||||
to the logical KV cache shape.
|
||||
With the above example, a return value (1, 0, 3, 2, 4)
|
||||
corresponds to
|
||||
[num_blocks, num_layers, block_size, num_heads, 2 * head_size].
|
||||
|
||||
If an additional dimension is NOT included in the returned
|
||||
tuple, the physical layout will not include a layers dimension.
|
||||
|
||||
Returns:
|
||||
A tuple of ints which is a permutation of range(len(shape)).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def full_cls_name(cls) -> tuple[str, str]:
|
||||
return (cls.__module__, cls.__qualname__)
|
||||
|
||||
@classmethod
|
||||
def get_required_kv_cache_layout(cls) -> str | None:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_supported_head_sizes(cls) -> list[int]:
|
||||
return []
|
||||
@@ -206,33 +147,12 @@ class AttentionBackend(ABC):
|
||||
def indexes_kv_by_block_stride(cls) -> bool:
|
||||
"""Whether the backend reads KV pages by the runtime block stride.
|
||||
|
||||
True when ``num_blocks`` is the outermost physical dimension of the KV
|
||||
cache, so the backend tolerates a non-contiguous block dim. This gates
|
||||
page size padding and cross-layer uniform KV layout.
|
||||
|
||||
Returns:
|
||||
True if the backend's physical KV layout is num-blocks-first. False
|
||||
otherwise, including when the backend does not define a layered
|
||||
stride order.
|
||||
Under the standardized ``[L, B, H, N, C]`` layouts (RFC #42082) the
|
||||
per-layer KV view is always blocks-first, so pages are addressed
|
||||
through the view's block stride and page-size padding is tolerated.
|
||||
This gates page size padding and cross-layer uniform KV layout.
|
||||
"""
|
||||
try:
|
||||
kv_cache_stride_order = cls.get_kv_cache_stride_order(
|
||||
include_num_layers_dimension=False
|
||||
)
|
||||
layered_kv_cache_stride_order = cls.get_kv_cache_stride_order(
|
||||
include_num_layers_dimension=True
|
||||
)
|
||||
except (AttributeError, NotImplementedError):
|
||||
return False
|
||||
|
||||
# Check that attention backend includes a layers dimension.
|
||||
if len(layered_kv_cache_stride_order) != len(kv_cache_stride_order) + 1:
|
||||
return False
|
||||
|
||||
# stride_order[0] == 0 means num_layers stays first in physical
|
||||
# layout (identity permutation), so indexing by block stride is
|
||||
# not supported.
|
||||
return layered_kv_cache_stride_order[0] != 0
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_mla(cls) -> bool:
|
||||
@@ -392,10 +312,6 @@ class AttentionBackend(ABC):
|
||||
invalid_reasons.append(combination_reason)
|
||||
return invalid_reasons
|
||||
|
||||
@classmethod
|
||||
def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def is_ssm(cls) -> bool:
|
||||
return False
|
||||
|
||||
@@ -24,9 +24,7 @@ from vllm.v1.attention.backend import (
|
||||
CommonAttentionMetadata,
|
||||
MultipleOf,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import (
|
||||
KVCacheLayoutType,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import resolve_kv_cache_layout
|
||||
from vllm.v1.kv_cache_interface import (
|
||||
AttentionSpec,
|
||||
CrossAttentionSpec,
|
||||
@@ -35,6 +33,36 @@ from vllm.v1.kv_cache_interface import (
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
_CPU_KV_CACHE_LAYOUTS = frozenset(("LBHNC", "BLHNC", "BHLNC"))
|
||||
|
||||
|
||||
def _split_cpu_kv_cache(
|
||||
kv_cache: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Split a logical CPU KV cache into contiguous-token K and V views."""
|
||||
layout = resolve_kv_cache_layout()
|
||||
if layout.name not in _CPU_KV_CACHE_LAYOUTS:
|
||||
raise ValueError(
|
||||
f"CPU attention does not support KV cache layout {layout.name}. "
|
||||
f"Supported layouts: {sorted(_CPU_KV_CACHE_LAYOUTS)}."
|
||||
)
|
||||
if kv_cache.ndim != 4:
|
||||
raise ValueError(f"CPU KV cache must be 4D, got {kv_cache.ndim}D")
|
||||
|
||||
num_blocks, num_kv_heads, block_size, content_size = kv_cache.shape
|
||||
if content_size % 2 != 0:
|
||||
raise ValueError(f"CPU KV cache content size must be even, got {content_size}")
|
||||
if kv_cache.stride(-1) != 1 or kv_cache.stride(-2) != content_size:
|
||||
raise ValueError(
|
||||
"CPU attention requires contiguous token and content dimensions; "
|
||||
f"got shape {tuple(kv_cache.shape)} and strides {kv_cache.stride()}"
|
||||
)
|
||||
|
||||
kv_cache = kv_cache.view(
|
||||
num_blocks, num_kv_heads, block_size * 2, content_size // 2
|
||||
)
|
||||
return kv_cache.chunk(2, dim=2)
|
||||
|
||||
|
||||
class CPUAttentionBackend(AttentionBackend):
|
||||
forward_includes_kv_cache_update: bool = False
|
||||
@@ -90,20 +118,6 @@ class CPUAttentionBackend(AttentionBackend):
|
||||
def get_builder_cls() -> type["CPUAttentionMetadataBuilder"]:
|
||||
return CPUAttentionMetadataBuilder
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return num_blocks, num_kv_heads, block_size, 2 * head_size
|
||||
|
||||
@classmethod
|
||||
def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
|
||||
return "HND"
|
||||
|
||||
@staticmethod
|
||||
def use_cascade_attention(*args, **kwargs) -> bool:
|
||||
return False
|
||||
@@ -354,12 +368,7 @@ class CPUAttentionBackendImpl(AttentionImpl):
|
||||
# For encoder attention,
|
||||
kv_cache = attn_metadata.encoder_cache
|
||||
|
||||
# KV cache size are [num_blocks, num_kv_heads, block_size,
|
||||
# 2 * head_size]. Make a view [num_blocks, num_kv_heads,
|
||||
# block_size * 2, head_size]. Then slice KV at dim 2
|
||||
num_blocks, num_kv_heads, block_size, _ = kv_cache.size()
|
||||
kv_cache = kv_cache.view((num_blocks, num_kv_heads, block_size * 2, -1))
|
||||
key_cache, value_cache = kv_cache.chunk(2, dim=2)
|
||||
key_cache, value_cache = _split_cpu_kv_cache(kv_cache)
|
||||
|
||||
# key and value may be None in the case of cross attention. They are
|
||||
# calculated once based on the output from the encoder and then cached
|
||||
@@ -415,9 +424,7 @@ class CPUAttentionBackendImpl(AttentionImpl):
|
||||
if self.attn_type in (AttentionType.ENCODER_ONLY, AttentionType.ENCODER):
|
||||
return
|
||||
|
||||
num_blocks, num_kv_heads, block_size, _ = kv_cache.size()
|
||||
kv_cache = kv_cache.view((num_blocks, num_kv_heads, block_size * 2, -1))
|
||||
key_cache, value_cache = kv_cache.chunk(2, dim=2)
|
||||
key_cache, value_cache = _split_cpu_kv_cache(kv_cache)
|
||||
ops.cpu_attn_reshape_and_cache(
|
||||
key,
|
||||
value,
|
||||
|
||||
@@ -57,7 +57,6 @@ from vllm.v1.attention.backend import (
|
||||
AttentionMetadataBuilder,
|
||||
CommonAttentionMetadata,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import get_kv_cache_layout
|
||||
from vllm.v1.kv_cache_interface import AttentionSpec
|
||||
from vllm.v1.worker.cp_utils import (
|
||||
run_split_fa2_dcp_context_attention,
|
||||
@@ -130,43 +129,6 @@ class FlashAttentionBackend(AttentionBackend):
|
||||
def get_builder_cls() -> type["FlashAttentionMetadataBuilder"]:
|
||||
return FlashAttentionMetadataBuilder
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
if block_size % 16 != 0:
|
||||
raise ValueError("Block size must be a multiple of 16.")
|
||||
# K and V are packed into the content dim: logical (B, H, N, 2*D).
|
||||
return (num_blocks, num_kv_heads, block_size, 2 * head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
# `stride_order` indicates the permutation that gets us from
|
||||
# `get_kv_cache_shape` (logical (B, H, N, 2*D)) to the actual memory
|
||||
# layout we want.
|
||||
cache_layout = get_kv_cache_layout()
|
||||
if cache_layout == "NHD" and include_num_layers_dimension:
|
||||
# (num_blocks, num_layers, block_size, num_kv_heads, 2*head_size)
|
||||
return (1, 0, 3, 2, 4)
|
||||
elif cache_layout == "NHD":
|
||||
# (num_blocks, block_size, num_kv_heads, 2*head_size)
|
||||
stride_order = (0, 2, 1, 3)
|
||||
elif cache_layout == "HND" and include_num_layers_dimension:
|
||||
# (num_blocks, num_kv_heads, num_layers, block_size, 2*head_size)
|
||||
return (1, 2, 0, 3, 4)
|
||||
elif cache_layout == "HND":
|
||||
# (num_blocks, num_kv_heads, block_size, 2*head_size)
|
||||
stride_order = (0, 1, 2, 3)
|
||||
else:
|
||||
raise ValueError(f"Unknown cache layout format {cache_layout}.")
|
||||
return stride_order
|
||||
|
||||
@classmethod
|
||||
def supports_head_size(cls, head_size: int) -> bool:
|
||||
if head_size % 8 != 0:
|
||||
@@ -853,7 +815,7 @@ class FlashAttentionImpl(AttentionImpl):
|
||||
key: shape = [num_tokens, num_kv_heads, head_size]
|
||||
value: shape = [num_tokens, num_kv_heads, head_size]
|
||||
kv_cache: shape =
|
||||
[num_blocks, num_kv_heads, block_size, 2 * head_size]
|
||||
[num_blocks, num_kv_heads, block_size, 2*head_size]
|
||||
attn_metadata: Metadata for attention.
|
||||
Returns:
|
||||
shape = [num_tokens, num_heads * head_size]
|
||||
@@ -900,7 +862,7 @@ class FlashAttentionImpl(AttentionImpl):
|
||||
layer,
|
||||
)
|
||||
|
||||
# (B, H, N, 2*D) -> ((B, N, H, D), (B, N, H, D))
|
||||
# (B, H, N, 2*head_size) -> ((B, N, H, head_size), (B, N, H, head_size))
|
||||
key_cache, value_cache = kv_cache.transpose(1, 2).split(self.head_size, dim=-1)
|
||||
# Fix degenerate strides on size-1 dims (e.g. num_kv_heads=1 with TP).
|
||||
# FA3/4 on H100+ uses TMA, which requires ≥16-byte stride alignment.
|
||||
@@ -1108,9 +1070,7 @@ class FlashAttentionImpl(AttentionImpl):
|
||||
return
|
||||
|
||||
# Scatter write into the KV cache using slot_mapping indices.
|
||||
# No TMA kernel is invoked here, so stride canonicalization is not needed.
|
||||
# (B, H, N, 2*D) -> ((B, N, H, D), (B, N, H, D))
|
||||
key_cache, value_cache = kv_cache.transpose(1, 2).split(self.head_size, dim=-1)
|
||||
k_cache, v_cache = kv_cache.transpose(1, 2).split(self.head_size, dim=-1)
|
||||
|
||||
# Reshape the input keys and values and store them in the cache.
|
||||
# Skip this if sharing KV cache with an earlier attention layer.
|
||||
@@ -1122,8 +1082,8 @@ class FlashAttentionImpl(AttentionImpl):
|
||||
reshape_and_cache_flash(
|
||||
key,
|
||||
value,
|
||||
key_cache,
|
||||
value_cache,
|
||||
k_cache,
|
||||
v_cache,
|
||||
slot_mapping,
|
||||
self.kv_cache_dtype,
|
||||
layer._k_scale,
|
||||
@@ -1629,10 +1589,11 @@ def cascade_attention(
|
||||
|
||||
num_tokens = query.shape[0]
|
||||
block_size = key_cache.shape[-3]
|
||||
num_kv_heads = key_cache.shape[-2]
|
||||
assert common_prefix_len % block_size == 0
|
||||
num_common_kv_blocks = common_prefix_len // block_size
|
||||
assert num_common_kv_blocks > 0
|
||||
descale_shape = (cu_prefix_query_lens.shape[0] - 1, key_cache.shape[-2])
|
||||
descale_shape = (cu_prefix_query_lens.shape[0] - 1, num_kv_heads)
|
||||
|
||||
# Process shared prefix.
|
||||
prefix_output, prefix_lse = flash_attn_varlen_func(
|
||||
@@ -1660,7 +1621,7 @@ def cascade_attention(
|
||||
num_splits=1 if envs.VLLM_BATCH_INVARIANT else max_num_splits,
|
||||
)
|
||||
|
||||
descale_shape = (cu_query_lens.shape[0] - 1, key_cache.shape[-2])
|
||||
descale_shape = (cu_query_lens.shape[0] - 1, num_kv_heads)
|
||||
|
||||
# Process suffix per query.
|
||||
suffix_output, suffix_lse = flash_attn_varlen_func(
|
||||
|
||||
@@ -21,8 +21,6 @@ from vllm.v1.attention.ops.triton_reshape_and_cache_flash import (
|
||||
|
||||
if is_flash_attn_varlen_func_available():
|
||||
from vllm.v1.attention.backends.fa_utils import flash_attn_varlen_func
|
||||
from vllm.v1.attention.backends.utils import get_kv_cache_layout
|
||||
|
||||
from .flash_attn import (
|
||||
FlashAttentionBackend,
|
||||
FlashAttentionImpl,
|
||||
@@ -73,49 +71,6 @@ class FlashAttentionDiffKVBackend(FlashAttentionBackend):
|
||||
def get_impl_cls() -> type["FlashAttentionImpl"]:
|
||||
return FlashAttentionDiffKVImpl
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
if block_size % 16 != 0:
|
||||
raise ValueError("Block size must be a multiple of 16.")
|
||||
# Logical (blocks-first, head-major) layout: K and V (with their
|
||||
# different head sizes) packed in the content dim.
|
||||
return (
|
||||
num_blocks,
|
||||
num_kv_heads,
|
||||
block_size,
|
||||
head_size + FlashAttentionDiffKVBackend.head_size_v,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
# `stride_order` indicates the permutation that gets us from
|
||||
# `get_kv_cache_shape` (logical (B, H, N, C_k+C_v)) to the actual
|
||||
# memory layout we want.
|
||||
cache_layout = get_kv_cache_layout()
|
||||
if cache_layout == "NHD" and include_num_layers_dimension:
|
||||
# (num_blocks, num_layers, block_size, num_kv_heads, C_k+C_v)
|
||||
return (1, 0, 3, 2, 4)
|
||||
elif cache_layout == "NHD":
|
||||
# (num_blocks, block_size, num_kv_heads, C_k+C_v)
|
||||
stride_order = (0, 2, 1, 3)
|
||||
elif cache_layout == "HND" and include_num_layers_dimension:
|
||||
# (num_blocks, num_kv_heads, num_layers, block_size, C_k+C_v)
|
||||
return (1, 2, 0, 3, 4)
|
||||
elif cache_layout == "HND":
|
||||
# (num_blocks, num_kv_heads, block_size, C_k+C_v)
|
||||
stride_order = (0, 1, 2, 3)
|
||||
else:
|
||||
raise ValueError(f"Unknown cache layout format {cache_layout}.")
|
||||
return stride_order
|
||||
|
||||
|
||||
class FlashAttentionDiffKVImpl(FlashAttentionImpl):
|
||||
vllm_flash_attn_version: int | None
|
||||
@@ -225,6 +180,7 @@ class FlashAttentionDiffKVImpl(FlashAttentionImpl):
|
||||
layer,
|
||||
)
|
||||
|
||||
# (B, H, N, C) -> (B, N, H, C) for kernel compatibility.
|
||||
# (B, H, N, 2*hs) -> ((B, N, H, hs), (B, N, H, hs))
|
||||
key_cache, value_cache = kv_cache.transpose(1, 2).split(self.head_size, dim=-1)
|
||||
# Fix degenerate strides on size-1 dims (e.g. num_kv_heads=1 with TP).
|
||||
|
||||
@@ -50,7 +50,6 @@ from vllm.utils.torch_utils import (
|
||||
canonicalize_singleton_dim_strides,
|
||||
is_quantized_kv_cache,
|
||||
is_strictly_contiguous,
|
||||
nvfp4_kv_cache_full_dim,
|
||||
nvfp4_split_data_scale,
|
||||
)
|
||||
from vllm.v1.attention.backend import (
|
||||
@@ -63,12 +62,12 @@ from vllm.v1.attention.backend import (
|
||||
MultipleOf,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import (
|
||||
KVCacheLayoutType,
|
||||
get_dcp_local_seq_lens,
|
||||
get_kv_cache_layout,
|
||||
get_flashinfer_layout_string,
|
||||
get_num_attention_heads_from_layers,
|
||||
get_per_layer_parameters,
|
||||
infer_global_hyperparameters,
|
||||
resolve_kv_cache_layout,
|
||||
split_decodes_and_prefills,
|
||||
)
|
||||
from vllm.v1.attention.ops.common import cp_lse_ag_out_rs
|
||||
@@ -238,7 +237,7 @@ class BatchDCPPrefillWrapper:
|
||||
else:
|
||||
self._dcp_combine = partial(cp_lse_ag_out_rs, is_lse_base_on_e=False)
|
||||
self._context = BatchPrefillWithPagedKVCacheWrapper(
|
||||
workspace_buffer, get_kv_cache_layout()
|
||||
workspace_buffer, get_flashinfer_layout_string()
|
||||
)
|
||||
self._new_tokens = BatchPrefillWithRaggedKVCacheWrapper(workspace_buffer)
|
||||
|
||||
@@ -393,44 +392,6 @@ class FlashInferBackend(AttentionBackend):
|
||||
def get_builder_cls() -> type["FlashInferMetadataBuilder"]:
|
||||
return FlashInferMetadataBuilder
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
if cache_dtype_str == "nvfp4":
|
||||
full_dim = nvfp4_kv_cache_full_dim(head_size)
|
||||
return (num_blocks, 2 * num_kv_heads, block_size, full_dim)
|
||||
# Pack K and V in the content dim (B, H, N, 2*hs).
|
||||
return (num_blocks, num_kv_heads, block_size, 2 * head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
# `stride_order` indicates the permutation that gets us from
|
||||
# `get_kv_cache_shape` (logical (B, H, N, 2*hs)) to the actual memory
|
||||
# layout we want.
|
||||
cache_layout = get_kv_cache_layout()
|
||||
if cache_layout == "NHD" and include_num_layers_dimension:
|
||||
# (num_blocks, num_layers, block_size, num_kv_heads, 2*head_size)
|
||||
return (1, 0, 3, 2, 4)
|
||||
elif cache_layout == "NHD":
|
||||
# (num_blocks, block_size, num_kv_heads, 2*head_size)
|
||||
stride_order = (0, 2, 1, 3)
|
||||
elif cache_layout == "HND" and include_num_layers_dimension:
|
||||
# (num_blocks, num_kv_heads, num_layers, block_size, 2*head_size)
|
||||
return (1, 2, 0, 3, 4)
|
||||
elif cache_layout == "HND":
|
||||
# (num_blocks, num_kv_heads, block_size, 2*head_size)
|
||||
stride_order = (0, 1, 2, 3)
|
||||
else:
|
||||
raise ValueError(f"Unknown cache layout format {cache_layout}.")
|
||||
return stride_order
|
||||
|
||||
@staticmethod
|
||||
def get_dtype_for_flashinfer(kv_cache_dtype: str) -> torch.dtype:
|
||||
if kv_cache_dtype in ("fp8", "fp8_e4m3"):
|
||||
@@ -489,7 +450,7 @@ class FlashInferBackend(AttentionBackend):
|
||||
) and supports_trtllm_attention(is_prefill=True)
|
||||
|
||||
@classmethod
|
||||
def get_required_kv_cache_layout(cls) -> KVCacheLayoutType | None:
|
||||
def get_required_kv_cache_layout(cls) -> str | None:
|
||||
capability = current_platform.get_device_capability()
|
||||
if capability is not None and capability.major == 10:
|
||||
return "HND"
|
||||
@@ -985,7 +946,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
|
||||
if self._noncausal_prefill_wrapper is None:
|
||||
self._noncausal_prefill_wrapper = BatchPrefillWithPagedKVCacheWrapper(
|
||||
self._get_workspace_buffer(),
|
||||
get_kv_cache_layout(),
|
||||
get_flashinfer_layout_string(),
|
||||
backend="auto",
|
||||
)
|
||||
return self._noncausal_prefill_wrapper
|
||||
@@ -1002,7 +963,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
|
||||
backend = "trtllm-gen" if self.is_kvcache_nvfp4 else "auto"
|
||||
self._prefill_wrapper = BatchPrefillWithPagedKVCacheWrapper(
|
||||
self._get_workspace_buffer(),
|
||||
get_kv_cache_layout(),
|
||||
get_flashinfer_layout_string(),
|
||||
backend=backend,
|
||||
)
|
||||
assert self._prefill_wrapper is not None
|
||||
@@ -1028,7 +989,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
|
||||
backend = "trtllm-gen" if self.is_kvcache_nvfp4 else "auto"
|
||||
decode_wrapper = BatchDecodeWithPagedKVCacheWrapper(
|
||||
self._get_workspace_buffer(),
|
||||
get_kv_cache_layout(),
|
||||
get_flashinfer_layout_string(),
|
||||
use_cuda_graph=use_cudagraph,
|
||||
paged_kv_indptr_buffer=paged_kv_indptr,
|
||||
paged_kv_indices_buffer=paged_kv_indices,
|
||||
@@ -1051,7 +1012,7 @@ class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
|
||||
def _get_cascade_wrapper(self):
|
||||
if self._cascade_wrapper is None:
|
||||
self._cascade_wrapper = MultiLevelCascadeAttentionWrapper(
|
||||
2, self._get_workspace_buffer(), get_kv_cache_layout()
|
||||
2, self._get_workspace_buffer(), get_flashinfer_layout_string()
|
||||
)
|
||||
return self._cascade_wrapper
|
||||
|
||||
@@ -1694,9 +1655,7 @@ class FlashInferImpl(AttentionImpl):
|
||||
query: shape = [num_tokens, num_heads, head_size]
|
||||
key: shape = [num_tokens, num_kv_heads, head_size]
|
||||
value: shape = [num_tokens, num_kv_heads, head_size]
|
||||
kv_cache: KV cache tensor with different possible shapes:
|
||||
- NHD: [num_blocks, 2, block_size, num_kv_heads, head_size]
|
||||
- HND: [num_blocks, 2, num_kv_heads, block_size, head_size]
|
||||
kv_cache: [num_blocks, num_kv_heads, block_size, 2*head_size]
|
||||
attn_metadata: Metadata for attention.
|
||||
Returns:
|
||||
shape = [num_tokens, num_heads * head_size]
|
||||
@@ -1791,31 +1750,9 @@ class FlashInferImpl(AttentionImpl):
|
||||
output_padded = output
|
||||
output = output[:num_actual_tokens]
|
||||
|
||||
if attn_metadata.use_cascade:
|
||||
# Cascade attention (rare case).
|
||||
assert attn_metadata.cascade_wrapper is not None
|
||||
stride_order = FlashInferBackend.get_kv_cache_stride_order()
|
||||
if self.is_kvcache_nvfp4:
|
||||
kv_cache_views = tuple(
|
||||
cache.permute(*stride_order)
|
||||
for cache in kv_cache.split(self.num_kv_heads, dim=1)
|
||||
)
|
||||
else:
|
||||
kv_perm = kv_cache.permute(*stride_order)
|
||||
kv_cache_views = kv_perm.split(self.head_size, dim=-1)
|
||||
kv_tuple = tuple(
|
||||
canonicalize_singleton_dim_strides(cache) for cache in kv_cache_views
|
||||
)
|
||||
output.copy_(attn_metadata.cascade_wrapper.run(query, kv_tuple))
|
||||
return output
|
||||
|
||||
# When using spec decoding, num_decodes can be < num_decode_tokens
|
||||
# because some decode requests may have more than one query token.
|
||||
num_decode_tokens = attn_metadata.num_decode_tokens
|
||||
num_prefill_tokens = attn_metadata.num_prefill_tokens
|
||||
|
||||
stride_order = FlashInferBackend.get_kv_cache_stride_order()
|
||||
kv_cache_permute = kv_cache.permute(*stride_order) # HND and contiguous
|
||||
# Permute to FlashInfer's expected layout (metadata-only).
|
||||
stride_order = resolve_kv_cache_layout().layer_view_order
|
||||
kv_cache_permute = kv_cache.permute(*stride_order)
|
||||
# Fix degenerate strides on any size-1 dimension (e.g. num_kv_heads=1
|
||||
# with TP=8). PyTorch permits non-canonical strides on size-1 dims;
|
||||
# CUDA TMA requires ≥16-byte alignment on all non-outermost strides.
|
||||
@@ -1850,6 +1787,14 @@ class FlashInferImpl(AttentionImpl):
|
||||
else:
|
||||
kv_cache_tuple = kv_cache_permute.split(hs, dim=-1)
|
||||
|
||||
if attn_metadata.use_cascade:
|
||||
assert attn_metadata.cascade_wrapper is not None
|
||||
output.copy_(attn_metadata.cascade_wrapper.run(query, kv_cache_tuple))
|
||||
return output
|
||||
|
||||
num_decode_tokens = attn_metadata.num_decode_tokens
|
||||
num_prefill_tokens = attn_metadata.num_prefill_tokens
|
||||
|
||||
use_dcp = self.dcp_world_size > 1
|
||||
|
||||
# Regular attention (common case).
|
||||
@@ -1949,7 +1894,7 @@ class FlashInferImpl(AttentionImpl):
|
||||
seq_lens_prefill = attn_metadata.prefill.seq_lens
|
||||
|
||||
# This path needs to be enabled with VLLM_KV_CACHE_LAYOUT = HND
|
||||
assert get_kv_cache_layout() == "HND"
|
||||
assert get_flashinfer_layout_string() == "HND"
|
||||
assert is_strictly_contiguous(prefill_query)
|
||||
assert is_strictly_contiguous(workspace_buffer)
|
||||
assert is_strictly_contiguous(block_tables_prefill)
|
||||
@@ -2132,7 +2077,7 @@ class FlashInferImpl(AttentionImpl):
|
||||
# trtllm-gen needs HND layout on SM100. XQA is selected
|
||||
# separately on SM90 and does not use this SM100 layout gate.
|
||||
if decode_with_trtllm_gen:
|
||||
assert get_kv_cache_layout() == "HND"
|
||||
assert get_flashinfer_layout_string() == "HND"
|
||||
else:
|
||||
assert decode_with_xqa
|
||||
assert is_strictly_contiguous(decode_query)
|
||||
@@ -2226,7 +2171,7 @@ class FlashInferImpl(AttentionImpl):
|
||||
sinks=self.sinks,
|
||||
o_sf_scale=self.o_sf_scale,
|
||||
out=out,
|
||||
kv_layout=get_kv_cache_layout(),
|
||||
kv_layout=get_flashinfer_layout_string(),
|
||||
backend=attn_metadata.decode.kernel.value,
|
||||
q_len_per_req=q_len_per_req,
|
||||
kv_cache_sf=(
|
||||
|
||||
@@ -100,6 +100,12 @@ class FlexAttentionBackend(AttentionBackend):
|
||||
def get_name() -> str:
|
||||
return "FLEX_ATTENTION"
|
||||
|
||||
@classmethod
|
||||
def get_required_kv_cache_layout(cls) -> str | None:
|
||||
if current_platform.is_rocm():
|
||||
return "LBNHC"
|
||||
return "BLNHC"
|
||||
|
||||
@classmethod
|
||||
def supports_sliding_window(cls) -> bool:
|
||||
return True
|
||||
@@ -126,25 +132,6 @@ class FlexAttentionBackend(AttentionBackend):
|
||||
def get_impl_cls() -> type["FlexAttentionImpl"]:
|
||||
return FlexAttentionImpl
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
# K and V are packed into the content dim: logical (B, H, N, 2*hs).
|
||||
return (num_blocks, num_kv_heads, block_size, 2 * head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
if include_num_layers_dimension:
|
||||
return (1, 0, 3, 2, 4)
|
||||
return (0, 2, 1, 3)
|
||||
|
||||
@staticmethod
|
||||
def get_builder_cls() -> type["FlexAttentionMetadataBuilder"]:
|
||||
return FlexAttentionMetadataBuilder
|
||||
@@ -1279,7 +1266,7 @@ class FlexAttentionImpl(AttentionImpl):
|
||||
key: shape = [num_tokens, num_kv_heads, head_size]
|
||||
value: shape = [num_tokens, num_kv_heads, head_size]
|
||||
kv_cache: shape =
|
||||
[num_blocks, num_kv_heads, block_size, 2 * head_size]
|
||||
[num_blocks, 2, block_size, num_kv_heads, head_size]
|
||||
attn_metadata: Metadata for attention.
|
||||
Returns:
|
||||
shape = [num_tokens, num_heads * head_size]
|
||||
@@ -1354,12 +1341,12 @@ class FlexAttentionImpl(AttentionImpl):
|
||||
else:
|
||||
assert self.attn_type == AttentionType.DECODER
|
||||
kv_cache = kv_cache.transpose(1, 2)
|
||||
hs = self.head_size
|
||||
key_cache, value_cache = kv_cache.split(hs, dim=-1)
|
||||
key_cache, value_cache = kv_cache.split(self.head_size, dim=-1)
|
||||
|
||||
# Flatten (num_blocks, block_size) into a single token dim.
|
||||
key_cache = key_cache.view(-1, self.num_kv_heads, self.head_size)
|
||||
value_cache = value_cache.view(-1, self.num_kv_heads, self.head_size)
|
||||
# The transposed views are non-contiguous, so use reshape.
|
||||
key_cache = key_cache.reshape(-1, self.num_kv_heads, self.head_size)
|
||||
value_cache = value_cache.reshape(-1, self.num_kv_heads, self.head_size)
|
||||
query, key_tensor, value_tensor = map(
|
||||
lambda x: self.view_as_4d(x).permute(0, 2, 1, 3),
|
||||
(query, key_cache, value_cache),
|
||||
|
||||
@@ -249,7 +249,9 @@ class HpcAttnMetadataBuilder(AttentionMetadataBuilder[HpcAttnMetadata]):
|
||||
class HpcAttentionBackend(AttentionBackend):
|
||||
"""HPC attention backend (pure attention, no RoPE/Norm).
|
||||
|
||||
KV cache layout: NHD (num_blocks, 2, block_size, num_kv_heads, head_size).
|
||||
KV cache layout: NHD; the logical per-layer view is
|
||||
(num_blocks, num_kv_heads, block_size, 2 * head_size) with K and V
|
||||
packed along the content dim.
|
||||
"""
|
||||
|
||||
accept_output_buffer: bool = True
|
||||
@@ -282,24 +284,6 @@ class HpcAttentionBackend(AttentionBackend):
|
||||
def get_builder_cls() -> type["HpcAttnMetadataBuilder"]:
|
||||
return HpcAttnMetadataBuilder
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (num_blocks, 2, block_size, num_kv_heads, head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
if include_num_layers_dimension:
|
||||
return (1, 0, 2, 3, 4, 5)
|
||||
return (0, 1, 2, 3, 4)
|
||||
|
||||
@classmethod
|
||||
def get_supported_head_sizes(cls) -> list[int]:
|
||||
return [128]
|
||||
@@ -316,7 +300,7 @@ class HpcAttentionBackend(AttentionBackend):
|
||||
|
||||
@classmethod
|
||||
def get_required_kv_cache_layout(cls) -> KVCacheLayoutType | None:
|
||||
return "NHD"
|
||||
return "LBNHC"
|
||||
|
||||
|
||||
class HpcAttentionImpl(AttentionImpl[HpcAttnMetadata]):
|
||||
@@ -436,13 +420,16 @@ class HpcAttentionImpl(AttentionImpl[HpcAttnMetadata]):
|
||||
num_decode_reqs = attn_metadata.num_decodes
|
||||
num_decode_tokens = attn_metadata.num_decode_tokens
|
||||
|
||||
# Logical (B, H, N, 2*head_size) -> (B, N, H, head_size) K/V views.
|
||||
key_cache, value_cache = kv_cache.transpose(1, 2).split(self.head_size, dim=-1)
|
||||
|
||||
# Write KV cache if not already done by HpcRopeNorm.
|
||||
if self.kv_sharing_target_layer_name is None and not hpc_kv_written:
|
||||
torch.ops._C_cache_ops.reshape_and_cache_flash(
|
||||
key,
|
||||
value,
|
||||
kv_cache[:, 0],
|
||||
kv_cache[:, 1],
|
||||
key_cache,
|
||||
value_cache,
|
||||
attn_metadata.slot_mapping,
|
||||
self.kv_cache_dtype,
|
||||
layer._k_scale,
|
||||
@@ -451,7 +438,8 @@ class HpcAttentionImpl(AttentionImpl[HpcAttnMetadata]):
|
||||
|
||||
if self.use_fp8:
|
||||
torch_dtype = _get_fp8_dtype_for_kv_cache(self.kv_cache_dtype)
|
||||
kv_cache = kv_cache.view(torch_dtype)
|
||||
key_cache = key_cache.view(torch_dtype)
|
||||
value_cache = value_cache.view(torch_dtype)
|
||||
|
||||
if self.use_fp8:
|
||||
if not hpc_kv_written:
|
||||
@@ -483,8 +471,8 @@ class HpcAttentionImpl(AttentionImpl[HpcAttnMetadata]):
|
||||
if self.use_fp8:
|
||||
hpc.attention_with_kvcache_prefill_fp8(
|
||||
q_prefill,
|
||||
kv_cache[:, 0],
|
||||
kv_cache[:, 1],
|
||||
key_cache,
|
||||
value_cache,
|
||||
hpc_prefill_q_scale,
|
||||
k_scale,
|
||||
v_scale,
|
||||
@@ -498,8 +486,8 @@ class HpcAttentionImpl(AttentionImpl[HpcAttnMetadata]):
|
||||
else:
|
||||
hpc.attention_with_kvcache_prefill_bf16(
|
||||
q_prefill,
|
||||
kv_cache[:, 0],
|
||||
kv_cache[:, 1],
|
||||
key_cache,
|
||||
value_cache,
|
||||
cu_seqlens_prefill,
|
||||
block_table_prefill,
|
||||
seq_lens_prefill,
|
||||
@@ -520,8 +508,8 @@ class HpcAttentionImpl(AttentionImpl[HpcAttnMetadata]):
|
||||
if self.use_fp8:
|
||||
hpc.attention_decode_fp8(
|
||||
q_decode,
|
||||
kv_cache[:, 0],
|
||||
kv_cache[:, 1],
|
||||
key_cache,
|
||||
value_cache,
|
||||
block_table_decode,
|
||||
num_seq_kvcache,
|
||||
hpc_decode_q_scale,
|
||||
@@ -541,8 +529,8 @@ class HpcAttentionImpl(AttentionImpl[HpcAttnMetadata]):
|
||||
else:
|
||||
hpc.attention_decode_bf16(
|
||||
q_decode,
|
||||
kv_cache[:, 0],
|
||||
kv_cache[:, 1],
|
||||
key_cache,
|
||||
value_cache,
|
||||
block_table_decode,
|
||||
num_seq_kvcache,
|
||||
mtp=mtp,
|
||||
|
||||
@@ -49,14 +49,6 @@ class CutlassMLABackend(MLACommonBackend):
|
||||
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
|
||||
return [128]
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
if include_num_layers_dimension:
|
||||
return (1, 0, 2, 3)
|
||||
return (0, 1, 2)
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
return "CUTLASS_MLA"
|
||||
|
||||
@@ -52,14 +52,6 @@ class FlashAttnMLABackend(MLACommonBackend):
|
||||
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
|
||||
return [MultipleOf(16)]
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
if include_num_layers_dimension:
|
||||
return (1, 0, 2, 3)
|
||||
return (0, 1, 2)
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
return "FLASH_ATTN_MLA"
|
||||
@@ -335,6 +327,7 @@ class FlashAttnMLAImpl(MLACommonImpl[FlashAttnMLAMetadata]):
|
||||
if is_quantized_kv_cache(self.kv_cache_dtype):
|
||||
raise NotImplementedError("FP8 FlashAttention MLA not yet supported")
|
||||
|
||||
kv_c_and_k_pe_cache = kv_c_and_k_pe_cache.unsqueeze(2)
|
||||
kv_c_cache = kv_c_and_k_pe_cache[..., : self.kv_lora_rank]
|
||||
k_pe_cache = kv_c_and_k_pe_cache[..., self.kv_lora_rank :]
|
||||
|
||||
@@ -345,8 +338,8 @@ class FlashAttnMLAImpl(MLACommonImpl[FlashAttnMLAMetadata]):
|
||||
|
||||
attn_out = flash_attn_varlen_func(
|
||||
q=q_pe,
|
||||
k=k_pe_cache.unsqueeze(-2), # Add head dim of 1
|
||||
v=kv_c_cache.unsqueeze(-2), # Add head dim of 1
|
||||
k=k_pe_cache,
|
||||
v=kv_c_cache,
|
||||
q_v=q_nope,
|
||||
max_seqlen_q=max_seqlen_q,
|
||||
cu_seqlens_q=attn_metadata.decode.query_start_loc,
|
||||
|
||||
@@ -103,16 +103,6 @@ class FlashAttnMLASparseBackend(AttentionBackend):
|
||||
return "FlashAttention MLA Sparse requires model with index_topk"
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (num_blocks, block_size, head_size)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlashAttnMLASparseMetadata(AttentionMetadata):
|
||||
|
||||
@@ -23,7 +23,6 @@ from vllm.v1.attention.backend import (
|
||||
AttentionType,
|
||||
MultipleOf,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import KVCacheLayoutType
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -67,14 +66,6 @@ class FlashInferMLABackend(MLACommonBackend):
|
||||
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
|
||||
return [32, 64]
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
if include_num_layers_dimension:
|
||||
return (1, 0, 2, 3)
|
||||
return (0, 1, 2)
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
return "FLASHINFER_MLA"
|
||||
@@ -118,10 +109,6 @@ class FlashInferMLABackend(MLACommonBackend):
|
||||
)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
|
||||
return "HND"
|
||||
|
||||
|
||||
class FlashInferMLAImpl(MLACommonImpl[MLACommonMetadata]):
|
||||
can_return_lse_for_decode: bool = True
|
||||
@@ -219,7 +206,7 @@ class FlashInferMLAImpl(MLACommonImpl[MLACommonMetadata]):
|
||||
workspace_buffer = _get_workspace_buffer(return_lse)
|
||||
kernel_out = trtllm_batch_decode_with_kv_cache_mla(
|
||||
query=q,
|
||||
kv_cache=kv_c_and_k_pe_cache.unsqueeze(1),
|
||||
kv_cache=kv_c_and_k_pe_cache,
|
||||
workspace_buffer=workspace_buffer,
|
||||
qk_nope_head_dim=self.qk_nope_head_dim,
|
||||
kv_lora_rank=self.kv_lora_rank,
|
||||
|
||||
@@ -31,7 +31,6 @@ from vllm.v1.attention.backends.mla.sparse_utils import (
|
||||
triton_convert_req_index_to_global_index,
|
||||
triton_filter_and_convert_dcp_index,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import KVCacheLayoutType
|
||||
from vllm.v1.kv_cache_interface import AttentionSpec
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -123,20 +122,6 @@ class FlashInferMLASparseTRTLLMBackend(_FlashInferMLASparseBackendBase):
|
||||
return "FlashInfer MLA Sparse requires model with index_topk config"
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int, # assumed to be 1 for MLA
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (num_blocks, block_size, head_size)
|
||||
|
||||
@classmethod
|
||||
def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
|
||||
return "HND"
|
||||
|
||||
|
||||
class FlashInferMLASparseSM120Backend(_FlashInferMLASparseBackendBase):
|
||||
"""FlashInfer sparse MLA backend for SM120."""
|
||||
@@ -216,23 +201,6 @@ class FlashInferMLASparseSM120Backend(_FlashInferMLASparseBackendBase):
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int, # assumed to be 1 for MLA
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
if cache_dtype_str in ("auto", "fp8", "fp8_e4m3", "fp8_ds_mla"):
|
||||
# fp8_ds_mla packed layout: 512 NoPE + 16 scales + 128 RoPE.
|
||||
return (num_blocks, block_size, 656)
|
||||
return (num_blocks, block_size, head_size)
|
||||
|
||||
@classmethod
|
||||
def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlashInferMLASparseMetadata(AttentionMetadata):
|
||||
@@ -439,7 +407,7 @@ class FlashInferMLASparseImpl(SparseMLACommonImpl[FlashInferMLASparseMetadata]):
|
||||
|
||||
kernel_out = trtllm_batch_decode_with_kv_cache_mla(
|
||||
query=query,
|
||||
kv_cache=kv_c_and_k_pe_cache.unsqueeze(1),
|
||||
kv_cache=kv_c_and_k_pe_cache,
|
||||
workspace_buffer=self._workspace_buffer,
|
||||
qk_nope_head_dim=self.qk_nope_head_dim,
|
||||
kv_lora_rank=self.kv_lora_rank,
|
||||
|
||||
@@ -58,14 +58,6 @@ class FlashMLABackend(MLACommonBackend):
|
||||
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
|
||||
return [64]
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
if include_num_layers_dimension:
|
||||
return (1, 0, 2, 3)
|
||||
return (0, 1, 2)
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
return "FLASHMLA"
|
||||
@@ -317,7 +309,7 @@ class FlashMLAImpl(MLACommonImpl[FlashMLAMetadata]):
|
||||
if is_quantized_kv_cache(self.kv_cache_dtype):
|
||||
o, lse = flash_mla_with_kvcache_fp8(
|
||||
q=q,
|
||||
k_cache=kv_c_and_k_pe_cache.unsqueeze(-2), # Add head dim of 1
|
||||
k_cache=kv_c_and_k_pe_cache.unsqueeze(2),
|
||||
block_table=attn_metadata.decode.block_table,
|
||||
cache_seqlens=attn_metadata.decode.seq_lens,
|
||||
head_dim_v=self.kv_lora_rank,
|
||||
@@ -331,7 +323,7 @@ class FlashMLAImpl(MLACommonImpl[FlashMLAMetadata]):
|
||||
else:
|
||||
o, lse = flash_mla_with_kvcache(
|
||||
q=q,
|
||||
k_cache=kv_c_and_k_pe_cache.unsqueeze(-2), # Add head dim of 1
|
||||
k_cache=kv_c_and_k_pe_cache.unsqueeze(2),
|
||||
block_table=attn_metadata.decode.block_table,
|
||||
cache_seqlens=attn_metadata.decode.seq_lens,
|
||||
head_dim_v=self.kv_lora_rank,
|
||||
|
||||
@@ -16,6 +16,7 @@ from vllm.model_executor.layers.attention.sparse_mla_attention import (
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.platforms.interface import DeviceCapability
|
||||
from vllm.utils.math_utils import cdiv
|
||||
from vllm.utils.platform_utils import num_compute_units
|
||||
from vllm.utils.torch_utils import is_quantized_kv_cache
|
||||
from vllm.v1.attention.backend import (
|
||||
@@ -127,19 +128,15 @@ class FlashMLASparseBackend(AttentionBackend):
|
||||
def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
|
||||
return capability.major in [9, 10]
|
||||
|
||||
|
||||
class DeepseekV4FlashMLASparseBackend(FlashMLASparseBackend):
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int, # assumed to be 1 for MLA
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
if cache_dtype_str == "fp8_ds_mla":
|
||||
# V3.2 main MLA: 656-byte custom storage format. See module docstring.
|
||||
return (num_blocks, block_size, 656)
|
||||
else:
|
||||
return (num_blocks, block_size, head_size)
|
||||
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
|
||||
return [256]
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
return "V4_FLASHMLA_SPARSE"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -313,6 +310,67 @@ class FlashMLASparseMetadataBuilder(
|
||||
device=device,
|
||||
)
|
||||
|
||||
# DeepseekV4: has compress_ratios in hf_config.
|
||||
hf_config = vllm_config.model_config.hf_config
|
||||
self.is_deepseek_v4 = (
|
||||
hasattr(hf_config, "compress_ratios") and len(hf_config.compress_ratios) > 0
|
||||
)
|
||||
self.compress_ratio = 1
|
||||
if self.is_deepseek_v4:
|
||||
self.compress_ratio = self.kv_cache_spec.tokens_per_state
|
||||
# Pre-allocate compressed slot mapping buffer for CUDA graph
|
||||
# address stability when compress_ratio > 1.
|
||||
if self.compress_ratio > 1:
|
||||
max_num_batched_tokens = (
|
||||
vllm_config.scheduler_config.max_num_batched_tokens
|
||||
)
|
||||
self.compressed_slot_mapping_buffer = torch.empty(
|
||||
max_num_batched_tokens,
|
||||
dtype=torch.int64,
|
||||
device=self.device,
|
||||
)
|
||||
|
||||
# Pre-allocate C128A topk buffers for CUDA graph address stability.
|
||||
if self.compress_ratio == 128:
|
||||
max_num_batched_tokens = (
|
||||
vllm_config.scheduler_config.max_num_batched_tokens
|
||||
)
|
||||
# Pad to B_TOPK alignment (128 covers both h_q=64 B_TOPK=64 and
|
||||
# h_q=128 B_TOPK=128). FlashMLA decode asserts extra_topk % B_TOPK
|
||||
# == 0; unaligned widths (e.g. 17 = ceil(2136/128)) crash the
|
||||
# sm100 head64 kernel. Padded slots stay -1 and decode_lens caps
|
||||
# them via topk_length, so the pad is a no-op at kernel level.
|
||||
# Mirrors _SPARSE_PREFILL_TOPK_ALIGNMENT in cache_utils.py.
|
||||
_C128A_TOPK_ALIGNMENT = 128
|
||||
c128a_max_compressed = cdiv(
|
||||
self.model_config.max_model_len, self.compress_ratio
|
||||
)
|
||||
c128a_max_compressed = (
|
||||
cdiv(c128a_max_compressed, _C128A_TOPK_ALIGNMENT)
|
||||
* _C128A_TOPK_ALIGNMENT
|
||||
)
|
||||
# Stored so _build_c128a_metadata passes it as the kernel's
|
||||
# max_compressed_tokens, matching the buffer stride. Otherwise
|
||||
# the kernel's default 8192 iterates past row width and spills
|
||||
# writes into adjacent rows (present in both decode and prefill
|
||||
# branches of _build_c128a_topk_metadata_kernel).
|
||||
self.c128a_max_compressed = c128a_max_compressed
|
||||
self.c128a_global_decode_buffer = torch.empty(
|
||||
(max_num_batched_tokens, c128a_max_compressed),
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
)
|
||||
self.c128a_decode_lens_buffer = torch.empty(
|
||||
max_num_batched_tokens,
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
)
|
||||
self.c128a_prefill_buffer = torch.empty(
|
||||
(max_num_batched_tokens, c128a_max_compressed),
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
)
|
||||
|
||||
def _build_fp8_mixed_decode_prefill(
|
||||
self,
|
||||
common_attn_metadata: CommonAttentionMetadata,
|
||||
@@ -783,7 +841,7 @@ class FlashMLASparseImpl(SparseMLACommonImpl[FlashMLASparseMetadata]):
|
||||
|
||||
out, lse = flash_mla_with_kvcache(
|
||||
q=q,
|
||||
k_cache=kv_c_and_k_pe_cache.view(torch.uint8).unsqueeze(-2),
|
||||
k_cache=kv_c_and_k_pe_cache.view(torch.uint8).unsqueeze(2),
|
||||
block_table=kernel_metadata.dummy_block_table,
|
||||
head_dim_v=512,
|
||||
cache_seqlens=kernel_metadata.cache_lens,
|
||||
@@ -808,7 +866,7 @@ class FlashMLASparseImpl(SparseMLACommonImpl[FlashMLASparseMetadata]):
|
||||
) -> torch.Tensor:
|
||||
num_tokens = q.shape[0]
|
||||
kv_c_and_k_pe_cache = kv_c_and_k_pe_cache.view(
|
||||
-1, 1, kv_c_and_k_pe_cache.shape[-1]
|
||||
-1, 1, 1, kv_c_and_k_pe_cache.shape[-1]
|
||||
)
|
||||
|
||||
# NOTE(Chen): kernel requires num_local_head to be a multiple of
|
||||
|
||||
@@ -146,28 +146,6 @@ class DeepseekV32IndexerBackend(AttentionBackend):
|
||||
def get_builder_cls() -> type["DeepseekV32IndexerMetadataBuilder"]:
|
||||
return DeepseekV32IndexerMetadataBuilder
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
assert num_kv_heads == 1
|
||||
return (num_blocks, block_size, head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
if include_num_layers_dimension:
|
||||
# DeepseekV32Indexer kernels do not support cross-layer
|
||||
# KV cache layout. Identity permutation keeps num_layers
|
||||
# first, signaling incompatibility.
|
||||
return (0, 1, 2, 3)
|
||||
return (0, 1, 2)
|
||||
|
||||
|
||||
class DeepseekV4IndexerBackend(DeepseekV32IndexerBackend):
|
||||
@staticmethod
|
||||
@@ -578,7 +556,7 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
|
||||
self.compress_ratio = 1
|
||||
# Get compress_ratio for DeepseekV4 support
|
||||
if isinstance(self.kv_cache_spec, MLAAttentionSpec):
|
||||
self.compress_ratio = self.kv_cache_spec.compress_ratio
|
||||
self.compress_ratio = self.kv_cache_spec.tokens_per_state
|
||||
if self.dcp_world_size > 1 and self.compress_ratio > 1:
|
||||
raise NotImplementedError(
|
||||
"DCP is not supported with sparse indexer KV compression "
|
||||
|
||||
@@ -292,16 +292,6 @@ class ROCMAiterMLASparseBackend(AttentionBackend):
|
||||
def get_impl_cls() -> type["ROCMAiterMLASparseImpl"]:
|
||||
return ROCMAiterMLASparseImpl
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int, # assumed to be 1 for MLA
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
return (num_blocks, block_size, head_size)
|
||||
|
||||
@classmethod
|
||||
def is_mla(cls) -> bool:
|
||||
return True
|
||||
|
||||
@@ -84,6 +84,10 @@ class DeepseekV4SWACache(torch.nn.Module, AttentionLayerBase):
|
||||
# contiguous full-cache layout.
|
||||
assert self.dtype in (torch.uint8, torch.bfloat16, torch.float8_e4m3fn)
|
||||
|
||||
def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
|
||||
# [B, H=1, N, C] -> [B, N, C]
|
||||
self.kv_cache = kv_cache.squeeze(1)
|
||||
|
||||
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
|
||||
# fp8_ds_mla's UE8M0 paged layout needs 576B alignment; contiguous
|
||||
# bf16/fp8 cache uses the natural element-size page.
|
||||
@@ -134,30 +138,6 @@ class DeepseekSparseSWABackend(AttentionBackend):
|
||||
return DeepseekV4ROCMAiterSparseSWAMetadataBuilder
|
||||
return DeepseekSparseSWAMetadataBuilder
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_shape(
|
||||
num_blocks: int,
|
||||
block_size: int,
|
||||
num_kv_heads: int,
|
||||
head_size: int,
|
||||
cache_dtype_str: str = "auto",
|
||||
) -> tuple[int, ...]:
|
||||
assert num_kv_heads == 1
|
||||
if cache_dtype_str == "fp8_ds_mla":
|
||||
# DeepseekV4 SWA: 584B per token (448 NoPE + 128 RoPE + 8 fp8 scale).
|
||||
# head_size passed in is the semantic head_dim (512).
|
||||
return (num_blocks, block_size, 584)
|
||||
else:
|
||||
return (num_blocks, block_size, head_size)
|
||||
|
||||
@staticmethod
|
||||
def get_kv_cache_stride_order(
|
||||
include_num_layers_dimension: bool = False,
|
||||
) -> tuple[int, ...]:
|
||||
if include_num_layers_dimension:
|
||||
return (0, 1, 2, 3)
|
||||
return (0, 1, 2)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeepseekSparseSWAMetadata:
|
||||
@@ -406,7 +386,7 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
|
||||
assert isinstance(self.kv_cache_spec, SlidingWindowMLASpec | MLAAttentionSpec)
|
||||
mla_spec = cast(SlidingWindowMLASpec | MLAAttentionSpec, self.kv_cache_spec)
|
||||
self.head_size = mla_spec.head_size # Already considered quantization.
|
||||
self.compress_ratio = mla_spec.compress_ratio
|
||||
self.compress_ratio = mla_spec.tokens_per_state
|
||||
self.block_size = mla_spec.block_size
|
||||
self.max_model_len = self.vllm_config.model_config.max_model_len
|
||||
self.max_num_batched_tokens = (
|
||||
|
||||
@@ -23,7 +23,6 @@ from vllm.v1.attention.backend import (
|
||||
AttentionType,
|
||||
MultipleOf,
|
||||
)
|
||||
from vllm.v1.attention.backends.utils import KVCacheLayoutType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
@@ -144,10 +143,6 @@ class TokenspeedMLABackend(MLACommonBackend):
|
||||
)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
|
||||
return "HND"
|
||||
|
||||
|
||||
class TokenspeedMLAImpl(MLACommonImpl[MLACommonMetadata]):
|
||||
can_return_lse_for_decode: bool = True
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user