Skip to content

Commit 9872bb9

Browse files
authored
[BugFix] Avoid allocating KV cache for skipped indexers (vllm-project#11065)
## What this PR does GLM-5.2 reuses top-k indices on most backbone layers, so those layers no longer initialize a local Indexer. The model runner still described and allocated an Indexer KV cache for every sparse MLA layer, wasting device memory and underestimating the number of KV blocks that fit in the available memory. During KV cache grouping, the Ascend MLA merge path also accepted the 704- and 576-dimension layouts as identical and expanded every layer back to the larger layout. This change keeps those layouts heterogeneous through block planning. This PR builds a smaller per-layer KV cache spec for sparse MLA layers that do not own an Indexer. ## How was this patch tested? - vLLM version: v0.23.0 - vLLM main: vllm-project/vllm@1f486d9 Signed-off-by: ZYang6263 <zy626375@gmail.com> Signed-off-by: ZYang6263 <50876451+ZYang6263@users.noreply.github.com>
1 parent ceb8e18 commit 9872bb9

4 files changed

Lines changed: 121 additions & 23 deletions

File tree

tests/ut/worker/a2/test_model_runner_v1.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import numpy as np
66
import torch
7+
from vllm.model_executor.layers.attention import MLAAttention
78
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec, KVCacheTensor
89

910
from vllm_ascend.worker.model_runner_v1 import NPUModelRunner
@@ -85,6 +86,58 @@ def test_reshape_kv_cache_uses_layer_spec_for_draft_gqa(self):
8586
self.assertEqual(k_cache.shape, (2, 16, 8, 64))
8687
self.assertEqual(v_cache.shape, (2, 16, 8, 64))
8788

89+
@patch("vllm_ascend.worker.model_runner_v1.has_ec_transfer", return_value=False)
90+
@patch("vllm_ascend.worker.model_runner_v1.get_layers_from_vllm_config")
91+
def test_sparse_layer_without_indexer_allocates_only_mla_kv_cache(
92+
self,
93+
mock_get_layers,
94+
_mock_has_ec_transfer,
95+
):
96+
runner = self._build_runner()
97+
runner.use_sparse = True
98+
runner.block_size = 16
99+
runner.sparse_head_dim = (512, 64, 128)
100+
runner.kv_cache_dtype = torch.bfloat16
101+
runner.shared_kv_cache_layers = {}
102+
runner.ascend_config = MagicMock()
103+
runner.ascend_config.is_sparse_c8_layer.return_value = False
104+
runner.model_config.hf_text_config = SimpleNamespace(
105+
kv_lora_rank=512,
106+
qk_rope_head_dim=64,
107+
)
108+
runner.vllm_config.cache_config.cache_dtype = "auto"
109+
110+
attn_module = MLAAttention.__new__(MLAAttention)
111+
torch.nn.Module.__init__(attn_module)
112+
attn_module.impl = SimpleNamespace(has_indexer=False)
113+
layer_name = "model.layers.1.self_attn.attn"
114+
mock_get_layers.return_value = {layer_name: attn_module}
115+
116+
spec = runner.get_kv_cache_spec()[layer_name]
117+
self.assertEqual(spec.sparse_head_dim, (512, 64, 0))
118+
119+
kv_cache_config = KVCacheConfig(
120+
num_blocks=2,
121+
kv_cache_tensors=[
122+
KVCacheTensor(
123+
size=spec.page_size_bytes * 2,
124+
shared_by=[layer_name],
125+
)
126+
],
127+
kv_cache_groups=[
128+
KVCacheGroupSpec(
129+
layer_names=[layer_name],
130+
kv_cache_spec=spec,
131+
)
132+
],
133+
)
134+
135+
raw_caches = runner._allocate_kv_cache_tensors(kv_cache_config)
136+
raw_k_cache, raw_v_cache = raw_caches[layer_name]
137+
138+
self.assertEqual(raw_k_cache.numel(), 2 * 16 * 512 * 2)
139+
self.assertEqual(raw_v_cache.numel(), 2 * 16 * 64 * 2)
140+
88141

89142
class TestNPUModelRunnerOutputTokenIds(unittest.TestCase):
90143
def _build_runner(self):

vllm_ascend/core/kv_cache_interface.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,21 @@ def merge(cls, specs: list[Self]) -> Self:
162162
assert all(isinstance(spec, MLAAttentionSpec) for spec in specs), (
163163
"All attention layers in the same KV cache group must be MLAAttentionSpec."
164164
)
165+
layout_set = {
166+
(
167+
spec.block_size,
168+
spec.num_kv_heads,
169+
spec.head_size,
170+
spec.scale_dim,
171+
spec.scale_dtype,
172+
spec.sparse_head_dim,
173+
spec.dtype,
174+
)
175+
for spec in specs
176+
}
177+
assert len(layout_set) == 1, (
178+
"All attention layers in the same KV cache group must use the same KV cache layout."
179+
)
165180
cache_dtype_str_set = set(spec.cache_dtype_str for spec in specs)
166181
assert len(cache_dtype_str_set) == 1, (
167182
"All attention layers in the same KV cache group must use the same quantization method."

vllm_ascend/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
if TYPE_CHECKING:
4343
from vllm.config import VllmConfig
44+
from vllm.v1.kv_cache_interface import AttentionSpec
4445
else:
4546
VllmConfig = None
4647

@@ -1693,6 +1694,11 @@ def kv_cache_spec_uses_sparse_c8(kv_cache_spec) -> bool:
16931694
return isinstance(kv_cache_spec, AscendMLAAttentionSpec) and bool(getattr(kv_cache_spec, "cache_sparse_c8", False))
16941695

16951696

1697+
def sparse_kv_cache_has_indexer(kv_cache_spec: AttentionSpec) -> bool:
1698+
sparse_head_dim = getattr(kv_cache_spec, "sparse_head_dim", None)
1699+
return sparse_head_dim is not None and len(sparse_head_dim) == 3 and sparse_head_dim[2] > 0
1700+
1701+
16961702
def is_hidden_state_cache_spec(spec) -> bool:
16971703
"""Whether ``spec`` marks an ``extract_hidden_states`` cache-only layer."""
16981704
from vllm.v1.kv_cache_interface import HiddenStateCacheSpec

vllm_ascend/worker/model_runner_v1.py

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
set_potential_max_tokens,
159159
set_weight_prefetch_method,
160160
should_skip_allreduce_across_dp_group,
161+
sparse_kv_cache_has_indexer,
161162
vllm_version_is,
162163
)
163164
from vllm_ascend.worker.npu_input_batch import NPUInputBatch
@@ -4260,20 +4261,29 @@ def _allocate_kv_cache_tensors(self, kv_cache_config: KVCacheConfig) -> dict[str
42604261
# for deepseek v3.2, we split the kv cache according to the corresponding ratio
42614262
kv_cache_spec = layer_kv_cache_spec[layer_name]
42624263
current_sparse_c8 = kv_cache_spec_uses_sparse_c8(kv_cache_spec)
4263-
sparse_kv_cache_ratio = kv_cache_spec.sparse_kv_cache_ratio
4264-
4265-
# A5 sparse C8: (ckv_ratio, qli_ratio, qli_scale_ratio, None)
4266-
# A3 sparse C8: (k_ratio, v_ratio, qli_ratio, qli_scale_ratio)
4267-
if current_sparse_c8 and get_ascend_device_type() == AscendDeviceType.A5:
4268-
k_tensor_split_factor = sparse_kv_cache_ratio[0] # ckv
4269-
v_tensor_split_factor = None # merged
4270-
dsa_k_tensor_split_factor = sparse_kv_cache_ratio[1] # qli_tensor
4271-
dsa_k_scale_tensor_split_factor = sparse_kv_cache_ratio[2] # qli_scale
4264+
has_indexer_cache = sparse_kv_cache_has_indexer(kv_cache_spec)
4265+
4266+
if has_indexer_cache:
4267+
sparse_kv_cache_ratio = kv_cache_spec.sparse_kv_cache_ratio
4268+
4269+
# A5 sparse C8: (ckv_ratio, qli_ratio, qli_scale_ratio, None)
4270+
# A3 sparse C8: (k_ratio, v_ratio, qli_ratio, qli_scale_ratio)
4271+
if current_sparse_c8 and get_ascend_device_type() == AscendDeviceType.A5:
4272+
k_tensor_split_factor = sparse_kv_cache_ratio[0] # ckv
4273+
v_tensor_split_factor = None # merged
4274+
dsa_k_tensor_split_factor = sparse_kv_cache_ratio[1] # qli_tensor
4275+
dsa_k_scale_tensor_split_factor = sparse_kv_cache_ratio[2] # qli_scale
4276+
else:
4277+
k_tensor_split_factor = sparse_kv_cache_ratio[0]
4278+
v_tensor_split_factor = sparse_kv_cache_ratio[1]
4279+
dsa_k_tensor_split_factor = sparse_kv_cache_ratio[2]
4280+
dsa_k_scale_tensor_split_factor = (
4281+
sparse_kv_cache_ratio[3] if current_sparse_c8 else None
4282+
)
42724283
else:
4273-
k_tensor_split_factor = sparse_kv_cache_ratio[0]
4274-
v_tensor_split_factor = sparse_kv_cache_ratio[1]
4275-
dsa_k_tensor_split_factor = sparse_kv_cache_ratio[2]
4276-
dsa_k_scale_tensor_split_factor = sparse_kv_cache_ratio[3] if current_sparse_c8 else None
4284+
assert not current_sparse_c8
4285+
k_dim, v_dim, _ = kv_cache_spec.sparse_head_dim
4286+
k_tensor_split_factor, v_tensor_split_factor = calc_split_factor([k_dim, v_dim])
42774287
else:
42784288
k_dim, v_dim = self._get_attention_kv_cache_dims(layer_name, current_kv_cache_spec)
42794289
assert k_dim > 0 and v_dim > 0
@@ -4296,9 +4306,9 @@ def _allocate_kv_cache_tensors(self, kv_cache_config: KVCacheConfig) -> dict[str
42964306
dsa_k_tensor_size = None
42974307
dsa_k_scale_tensor_size = None
42984308
#### for deepseek sparse attention
4299-
if self.use_sparse:
4309+
if self.use_sparse and has_indexer_cache:
43004310
dsa_k_tensor_size = int(kv_cache_tensor.size // dsa_k_tensor_split_factor)
4301-
if self.use_sparse and current_sparse_c8:
4311+
if self.use_sparse and has_indexer_cache and current_sparse_c8:
43024312
dsa_k_scale_tensor_size = int(kv_cache_tensor.size // dsa_k_scale_tensor_split_factor)
43034313

43044314
# Allocate raw int8 tensors. Even bf16/fp16 KV cache entries
@@ -4317,7 +4327,7 @@ def _allocate_kv_cache_tensors(self, kv_cache_config: KVCacheConfig) -> dict[str
43174327
alignment,
43184328
)
43194329

4320-
if self.use_sparse:
4330+
if self.use_sparse and has_indexer_cache:
43214331
assert dsa_k_tensor_size is not None
43224332

43234333
if current_sparse_c8:
@@ -4342,7 +4352,9 @@ def _allocate_kv_cache_tensors(self, kv_cache_config: KVCacheConfig) -> dict[str
43424352
# shared the attn kvcache for all shared layers
43434353
if "attn" in layer_name_inner and "linear_attn" not in layer_name_inner:
43444354
if self.use_sparse:
4345-
if current_sparse_c8:
4355+
if not has_indexer_cache:
4356+
kv_cache_raw_tensors[layer_name_inner] = (k_tensor, v_tensor)
4357+
elif current_sparse_c8:
43464358
if get_ascend_device_type() == AscendDeviceType.A5:
43474359
kv_cache_raw_tensors[layer_name_inner] = (
43484360
k_tensor, dsa_k_tensor, dsa_k_scale_tensor
@@ -4487,8 +4499,9 @@ def _reshape_kv_cache_tensors(
44874499
# _allocate_kv_cache_tensors; route them to the dedicated
44884500
# elif branch below before the sparse branch tries to
44894501
# unpack them as a (k, v, dsa_k[, scale]) tuple.
4490-
if self.use_sparse and "cache_only_layers" not in layer_name:
4491-
current_sparse_c8 = kv_cache_spec_uses_sparse_c8(current_kv_cache_spec)
4502+
current_sparse_c8 = kv_cache_spec_uses_sparse_c8(current_kv_cache_spec)
4503+
has_indexer_cache = sparse_kv_cache_has_indexer(current_kv_cache_spec)
4504+
if self.use_sparse and has_indexer_cache and "cache_only_layers" not in layer_name:
44924505
if current_sparse_c8:
44934506
if get_ascend_device_type() == AscendDeviceType.A5:
44944507
raw_k_tensor, raw_dsa_k_tensor, raw_dsa_k_scale_tensor = kv_cache_raw_tensors[ # type: ignore
@@ -4672,7 +4685,7 @@ def _reshape_kv_cache_tensors(
46724685
else:
46734686
v_cache = raw_v_tensor.view(v_cache_dtype).view(v_shape)
46744687

4675-
if self.use_sparse:
4688+
if self.use_sparse and has_indexer_cache:
46764689
dsa_k_cache_shape = (
46774690
num_blocks,
46784691
current_kv_cache_spec.block_size,
@@ -4974,14 +4987,25 @@ def get_kv_cache_spec(self) -> dict[str, KVCacheSpec]:
49744987

49754988
elif isinstance(attn_module, MLAAttention):
49764989
if self.use_sparse:
4990+
has_indexer = attn_module.impl.has_indexer
4991+
if has_indexer:
4992+
sparse_head_dim = self.sparse_head_dim
4993+
else:
4994+
# Layers that reuse another layer's top-k indices only
4995+
# need the MLA latent and RoPE caches.
4996+
sparse_head_dim = (
4997+
self.model_config.hf_text_config.kv_lora_rank,
4998+
self.model_config.hf_text_config.qk_rope_head_dim,
4999+
0,
5000+
)
49775001
kv_cache_spec[layer_name] = AscendMLAAttentionSpec(
49785002
block_size=self.block_size,
49795003
num_kv_heads=1,
4980-
head_size=sum(self.sparse_head_dim),
4981-
sparse_head_dim=self.sparse_head_dim,
5004+
head_size=sum(sparse_head_dim),
5005+
sparse_head_dim=sparse_head_dim,
49825006
dtype=self.kv_cache_dtype,
49835007
cache_dtype_str=self.vllm_config.cache_config.cache_dtype,
4984-
cache_sparse_c8=self.ascend_config.is_sparse_c8_layer(layer_name),
5008+
cache_sparse_c8=has_indexer and self.ascend_config.is_sparse_c8_layer(layer_name),
49855009
)
49865010
elif spec := attn_module.get_kv_cache_spec(self.vllm_config):
49875011
if getattr(attn_module.impl, "fa_quant_layer", False):

0 commit comments

Comments
 (0)