[Kimi K3] Support internal prefix checkpoints with partial prefix caching and spec-decoding (#53614)
Signed-off-by: Ziming Huang <zelda.huanghuang@gmail.com> Co-authored-by: OpenAI Codex <codex@openai.com>
Ziming Huang committed
Sep 6, 2026 at 13:18 UTC
144e79c8106da23141ac010394b782f730cc7fe8
11 files changed
+481
-106
tests/models/kimi_k3/test_kda_metadata.py
+71
-1
@@ -92,6 +92,10 @@ def _make_builder(
92
mamba_cache_mode: str = "none",
93
use_recoverssm: bool = False,
94
num_prefill_checkpoint_blocks: int = 0,
95
+ mamba_block_size: int = BLOCK_SIZE,
96
+ prefix_match_unit: int | None = None,
97
+ use_eagle: bool = False,
98
+ disable_eagle_block_drop: bool = False,
99
) -> AttentionMetadataBuilder:
100
vllm_config = create_vllm_config(
101
model_name="Qwen/Qwen3.5-0.8B",
@@ -102,20 +106,29 @@ def _make_builder(
106
method="ngram",
107
num_speculative_tokens=num_speculative_tokens,
108
)
109
+ if use_eagle:
110
+ vllm_config.speculative_config.method = "eagle3"
111
+ vllm_config.speculative_config.disable_eagle_block_drop = (
112
+ disable_eagle_block_drop
113
+ )
114
vllm_config.compilation_config.cudagraph_mode = (
115
CUDAGraphMode.FULL_AND_PIECEWISE if full_cuda_graph else CUDAGraphMode.NONE
116
)
117
vllm_config.cache_config.mamba_cache_mode = mamba_cache_mode
118
vllm_config.cache_config.use_replayssm = use_recoverssm
119
vllm_config.cache_config.use_kda_recoverssm = use_recoverssm
120
+ vllm_config.cache_config.prefix_match_unit = prefix_match_unit
121
builder = builder_cls(
122
kv_cache_spec=MambaSpec(
113
- block_size=BLOCK_SIZE,
123
+ block_size=mamba_block_size,
124
shapes=((16, 64),),
125
dtypes=(torch.float16,),
126
mamba_cache_mode=mamba_cache_mode,
127
num_speculative_blocks=(0 if use_recoverssm else num_speculative_tokens),
128
num_prefill_checkpoint_blocks=num_prefill_checkpoint_blocks,
129
+ prefill_checkpoint_alignment=(
130
+ 16 if num_prefill_checkpoint_blocks > 0 else None
131
+ ),
132
),
133
layer_names=["layer.0"],
134
vllm_config=vllm_config,
@@ -156,6 +169,7 @@ def test_kda_recoverssm_startup_metadata_flow_without_model(monkeypatch):
169
mamba_type=MambaAttentionBackendEnum.GDN_ATTN,
170
mamba_cache_mode="align",
171
num_prefill_checkpoint_blocks=1,
172
+ prefill_checkpoint_alignment=16,
173
)
174
175
# This is the same compatibility check performed while initializing the
@@ -177,11 +191,13 @@ def test_kda_recoverssm_startup_metadata_flow_without_model(monkeypatch):
191
cache_config=SimpleNamespace(
192
mamba_cache_mode="align",
193
use_kda_recoverssm=True,
194
+ prefix_match_unit=None,
195
),
196
parallel_config=SimpleNamespace(decode_context_parallel_size=1),
197
speculative_config=SimpleNamespace(
198
num_speculative_tokens=2,
199
parallel_drafting=False,
200
+ use_eagle_block_drop=Mock(return_value=False),
201
),
202
compilation_config=SimpleNamespace(
203
cudagraph_mode=CUDAGraphMode.NONE,
@@ -300,6 +316,60 @@ def test_internal_checkpoint_metadata_targets_last_aligned_boundary():
316
)
317
318
319
+@pytest.mark.parametrize(
320
+ ("disable_eagle_block_drop", "prefix_match_unit", "expected_offset"),
321
+ [(False, 16, 80), (True, 16, 96), (False, 8, None)],
322
+)
323
+@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
324
+def test_spec_internal_checkpoint_metadata_targets_replay_boundary(
325
+ disable_eagle_block_drop: bool,
326
+ prefix_match_unit: int,
327
+ expected_offset: int | None,
328
+) -> None:
329
+ device = torch.device("cuda")
330
+ batch = BatchSpec(seq_lens=[100], query_lens=[100])
331
+ common_attn_metadata = create_common_attn_metadata(
332
+ batch, BLOCK_SIZE, device, arange_block_indices=True
333
+ )
334
+ common_attn_metadata = common_attn_metadata.replace(
335
+ is_prefilling=torch.tensor([True]),
336
+ block_table_tensor=common_attn_metadata.block_table_tensor + 1,
337
+ )
338
+ builder = _make_builder(
339
+ KimiK3KDAMetadataBuilder,
340
+ num_speculative_tokens=3,
341
+ full_cuda_graph=False,
342
+ mamba_cache_mode="align",
343
+ num_prefill_checkpoint_blocks=1,
344
+ mamba_block_size=64,
345
+ prefix_match_unit=prefix_match_unit,
346
+ use_eagle=True,
347
+ disable_eagle_block_drop=disable_eagle_block_drop,
348
+ device=device,
349
+ )
350
+ assert isinstance(builder, KimiK3KDAMetadataBuilder)
351
+ builder.mamba_aligned_state_indices = mamba_get_block_table_tensor(
352
+ common_attn_metadata.block_table_tensor,
353
+ common_attn_metadata.seq_lens,
354
+ builder.kv_cache_spec,
355
+ "align",
356
+ )
357
+ actual = builder.build(0, common_attn_metadata)
358
+
359
+ if expected_offset is None:
360
+ assert actual.checkpoint is None
361
+ return
362
+ assert actual.checkpoint is not None
363
+ torch.testing.assert_close(
364
+ actual.checkpoint.state_indices,
365
+ torch.tensor([1], dtype=torch.int32, device=device),
366
+ )
367
+ torch.testing.assert_close(
368
+ actual.checkpoint.checkpoint_offsets,
369
+ torch.tensor([expected_offset], dtype=torch.int32, device=device),
370
+ )
371
+
372
+
373
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
374
def test_internal_checkpoint_metadata_skips_unaligned_offset():
375
device = torch.device("cuda")
tests/v1/core/prefix_cache/test_partial_prefix_cache_hits.py
+104
-37
@@ -92,8 +92,23 @@ def make_full_mamba_manager(
92
mamba_block_size: int = 4,
93
num_blocks: int = 32,
94
use_eagle: bool = False,
95
+ num_speculative_blocks: int = 0,
96
num_prefill_checkpoint_blocks: int = 0,
97
):
98
+ mamba_group = KVCacheGroupSpec(
99
+ ["mamba"],
100
+ MambaSpec(
101
+ block_size=mamba_block_size,
102
+ shapes=(1, 1),
103
+ dtypes=(torch.float32,),
104
+ mamba_cache_mode="align",
105
+ num_speculative_blocks=num_speculative_blocks,
106
+ num_prefill_checkpoint_blocks=num_prefill_checkpoint_blocks,
107
+ prefill_checkpoint_alignment=(
108
+ 16 if num_prefill_checkpoint_blocks > 0 else None
109
+ ),
110
+ ),
111
+ )
112
kv_cache_config = KVCacheConfig(
113
num_blocks=num_blocks,
114
kv_cache_tensors=[],
@@ -107,16 +122,7 @@ def make_full_mamba_manager(
122
dtype=torch.float32,
123
),
124
),
110
- KVCacheGroupSpec(
111
- ["mamba"],
112
- MambaSpec(
113
- block_size=mamba_block_size,
114
- shapes=(1, 1),
115
- dtypes=(torch.float32,),
116
- mamba_cache_mode="align",
117
- num_prefill_checkpoint_blocks=num_prefill_checkpoint_blocks,
118
- ),
119
- ),
125
+ mamba_group,
126
],
127
)
128
scheduler_block_size = lcm(
@@ -169,6 +175,7 @@ def test_mamba_align_split_partial_tail_schedule(dcp_world_size: int):
175
scheduler_block_size = block_size * dcp_world_size
176
hash_block_size = 32
177
mock = SimpleNamespace(
178
+ block_size=scheduler_block_size,
179
cache_config=SimpleNamespace(block_size=block_size),
180
max_num_scheduled_tokens=8192,
181
scheduler_config=SimpleNamespace(long_prefill_token_threshold=0),
@@ -217,6 +224,7 @@ def test_mamba_align_split_when_block_exceeds_scheduling_budget():
224
token_budget = 8192
225
prompt_length = 30000
226
mock = SimpleNamespace(
227
+ block_size=block_size,
228
cache_config=SimpleNamespace(block_size=block_size),
229
max_num_scheduled_tokens=token_budget,
230
scheduler_config=SimpleNamespace(long_prefill_token_threshold=0),
@@ -254,6 +262,7 @@ def test_mamba_align_split_when_block_exceeds_long_prefill_threshold():
262
long_prefill_threshold = 384
263
prompt_length = 1300
264
mock = SimpleNamespace(
265
+ block_size=block_size,
266
cache_config=SimpleNamespace(block_size=block_size),
267
max_num_scheduled_tokens=token_budget,
268
scheduler_config=SimpleNamespace(
@@ -557,9 +566,9 @@ def test_partial_hit_then_internal_checkpoint_uses_distinct_mamba_blocks():
566
assert mamba_blocks[3].block_id == running_block_id
567
568
560
-def test_internal_checkpoint_requires_block_aligned_start():
561
- hash_block_size = 2
562
- mamba_block_size = 16
569
+def test_internal_checkpoint_uses_partial_hash_lifecycle():
570
+ hash_block_size = 16
571
+ mamba_block_size = 32
572
manager = make_full_mamba_manager(
573
dcp_world_size=1,
574
hash_block_size=hash_block_size,
@@ -567,23 +576,90 @@ def test_internal_checkpoint_requires_block_aligned_start():
576
mamba_block_size=mamba_block_size,
577
num_prefill_checkpoint_blocks=1,
578
)
570
- request = make_request("producer", list(range(50)), hash_block_size, sha256)
579
+ request = make_request("producer", list(range(120)), hash_block_size, sha256)
580
572
- # Compute token 0 first, so the next query starts at token 1, which is not
573
- # aligned to the Mamba block size.
574
- assert manager.allocate_slots(request, 1) is not None
575
- request.num_computed_tokens = 1
576
- manager.new_step_starts()
581
+ new_blocks = manager.allocate_slots(request, request.num_tokens)
582
+
583
+ assert new_blocks is not None
584
+ mamba_blocks = manager.get_blocks(request.request_id).blocks[1]
585
+ checkpoint_idx = 2
586
+ checkpoint_block = mamba_blocks[checkpoint_idx]
587
+ running_block = mamba_blocks[checkpoint_idx + 1]
588
+ assert not checkpoint_block.is_null
589
+ assert not running_block.is_null
590
+ assert checkpoint_block is not running_block
591
+
592
+ # The internal block is exported at state@112, replacing its temporary
593
+ # full-block state@96 key while retaining #52789's req_to_blocks ownership.
594
+ partial_hash = request.block_hashes[112 // hash_block_size - 1]
595
+ partial_hit = manager.block_pool.get_cached_block(partial_hash, [1])
596
+ assert partial_hit is not None
597
+ assert partial_hit[0] is checkpoint_block
598
+ assert checkpoint_block.block_hash_num_tokens == 112
599
+ full_hash = request.block_hashes[96 // hash_block_size - 1]
600
+ assert manager.block_pool.get_cached_block(full_hash, [1]) is None
601
+ assert drain_boundary_state_offloads(manager) == {
602
+ request.request_id: [(1, checkpoint_block.block_id, 112)]
603
+ }
604
+
605
+ manager.free(request)
606
+ replay = make_request("replay", list(range(120)), hash_block_size, sha256)
607
+ _, num_computed, _ = manager.get_computed_blocks(replay)
608
+ assert num_computed == 112
609
+
610
+
611
+def test_eagle_block_aligned_checkpoint_replaces_newer_hash():
612
+ hash_block_size = mamba_block_size = 32
613
+ manager = make_full_mamba_manager(
614
+ dcp_world_size=1,
615
+ hash_block_size=hash_block_size,
616
+ full_block_size=hash_block_size,
617
+ mamba_block_size=mamba_block_size,
618
+ use_eagle=True,
619
+ num_speculative_blocks=3,
620
+ num_prefill_checkpoint_blocks=1,
621
+ )
622
+ request = make_request("producer", list(range(104)), hash_block_size, sha256)
623
+
624
+ assert manager.allocate_slots(request, request.num_tokens) is not None
625
+
626
+ mamba_blocks = manager.get_blocks(request.request_id).blocks[1]
627
+ checkpoint_block = mamba_blocks[2]
628
+ checkpoint_hash = request.block_hashes[64 // hash_block_size - 1]
629
+ checkpoint_hit = manager.block_pool.get_cached_block(checkpoint_hash, [1])
630
+ assert checkpoint_hit is not None
631
+ assert checkpoint_hit[0] is checkpoint_block
632
+ assert checkpoint_block.block_hash_num_tokens == 64
633
+
634
+ overwritten_full_hash = request.block_hashes[96 // hash_block_size - 1]
635
+ assert manager.block_pool.get_cached_block(overwritten_full_hash, [1]) is None
636
+
637
+
638
+def test_hash_aligned_query_end_uses_regular_partial_tail():
639
+ hash_block_size = 2
640
+ mamba_block_size = 4
641
+ manager = make_full_mamba_manager(
642
+ dcp_world_size=1,
643
+ hash_block_size=hash_block_size,
644
+ full_block_size=hash_block_size,
645
+ mamba_block_size=mamba_block_size,
646
+ num_prefill_checkpoint_blocks=1,
647
+ )
648
+ request = make_request("producer", list(range(14)), hash_block_size, sha256)
649
578
- new_blocks = manager.allocate_slots(request, 49)
650
+ new_blocks = manager.allocate_slots(request, request.num_tokens)
651
652
assert new_blocks is not None
653
+ mamba_manager = manager.coordinator.single_type_managers[1]
654
+ assert mamba_manager._checkpoint_positions[request.request_id] == 0
655
mamba_blocks = manager.get_blocks(request.request_id).blocks[1]
582
- checkpoint_block_idx = 48 // mamba_block_size - 1
583
- assert mamba_blocks[checkpoint_block_idx].is_null
584
- assert not mamba_blocks[-1].is_null
585
- checkpoint_hash = request.block_hashes[48 // hash_block_size - 1]
586
- assert manager.block_pool.get_cached_block(checkpoint_hash, [1]) is None
656
+ assert sum(not block.is_null for block in mamba_blocks) == 1
657
+
658
+ partial_hash = request.block_hashes[14 // hash_block_size - 1]
659
+ cached = manager.block_pool.get_cached_block(partial_hash, [1])
660
+ assert cached is not None
661
+ assert cached[0] is mamba_blocks[14 // mamba_block_size]
662
+ assert cached[0].block_hash_num_tokens == 14
663
664
665
def test_external_mamba_hit_same_block_uses_running_cow_on_continue():
@@ -699,18 +775,9 @@ def test_boundary_state_offloads_returns_cow_target():
775
computed_blocks, num_computed, _ = manager.get_computed_blocks(req0)
776
assert manager.allocate_slots(req0, 6, num_computed, computed_blocks) is not None
777
702
- # Step A offers the materialized aligned checkpoint immediately.
703
- ((group_id, block_id, boundary_tokens),) = drain_boundary_state_offloads(manager)[
704
- "0"
705
- ]
706
- assert group_id == 1
707
- assert boundary_tokens == 4
708
- aligned_hash = req0.block_hashes[4 // hash_block_size - 1]
709
- aligned_block = manager.block_pool.get_cached_block(
710
- aligned_hash, kv_cache_group_ids=[1]
711
- )
712
- assert aligned_block is not None
713
- assert block_id == aligned_block[0].block_id
778
+ # The final hash-aligned boundary uses the regular partial-tail path rather
779
+ # than allocating an internal checkpoint at the query end.
780
+ assert drain_boundary_state_offloads(manager) == {}
781
782
partial_mamba_hash = req0.block_hashes[6 // hash_block_size - 1]
783
source_block = manager.block_pool.get_cached_block(
tests/v1/core/test_mamba_align_chunk_split.py
+92
-13
@@ -8,6 +8,7 @@ the wrong offset, and a later chunk crossing that boundary publishes it anyway.
8
Requests resuming from it then restore a truncated state (#43559).
9
"""
10
11
+import sys
12
from types import SimpleNamespace
13
14
import pytest
@@ -21,6 +22,7 @@ from vllm.v1.kv_cache_interface import (
22
KVCacheConfig,
23
KVCacheGroupSpec,
24
MambaSpec,
25
+ is_mamba_prefill_checkpoint_valid,
26
)
27
from vllm.v1.request import Request
28
@@ -37,6 +39,49 @@ PROMPT_LEN = 2002
39
MAMBA_GROUP_ID = 1
40
41
42
+@pytest.mark.parametrize(
43
+ (
44
+ "query_start",
45
+ "query_end",
46
+ "checkpoint_position",
47
+ "hash_size",
48
+ "block_size",
49
+ "alignment",
50
+ "expected",
51
+ ),
52
+ [
53
+ (0, 100, 88, 8, 64, 16, False),
54
+ (0, 100, 96, 8, 64, 16, True),
55
+ (0, 100, 96, 8, 64, None, False),
56
+ (8, 100, 96, 8, 64, 16, False),
57
+ (15360, 16896, 16768, 128, 1536, 16, False),
58
+ (15360, 16960, 16896, 128, 1536, 16, True),
59
+ (15360, 16960, 16768, 128, 1536, 16, True),
60
+ (16064, 18433, 18432, 128, 1536, 16, False),
61
+ ],
62
+)
63
+def test_mamba_prefill_checkpoint_valid(
64
+ query_start: int,
65
+ query_end: int,
66
+ checkpoint_position: int,
67
+ hash_size: int,
68
+ block_size: int,
69
+ alignment: int | None,
70
+ expected: bool,
71
+) -> None:
72
+ assert (
73
+ is_mamba_prefill_checkpoint_valid(
74
+ query_start=query_start,
75
+ query_end=query_end,
76
+ checkpoint_position=checkpoint_position,
77
+ hash_block_size=hash_size,
78
+ mamba_block_size=block_size,
79
+ checkpoint_alignment=alignment,
80
+ )
81
+ is expected
82
+ )
83
+
84
+
85
def _make_hybrid_kv_cache_manager(
86
num_prefill_checkpoint_blocks: int = 0,
87
) -> KVCacheManager:
@@ -62,6 +107,9 @@ def _make_hybrid_kv_cache_manager(
107
mamba_cache_mode="align",
108
num_speculative_blocks=NUM_SPEC,
109
num_prefill_checkpoint_blocks=num_prefill_checkpoint_blocks,
110
+ prefill_checkpoint_alignment=(
111
+ 16 if num_prefill_checkpoint_blocks > 0 else None
112
+ ),
113
),
114
),
115
],
@@ -83,20 +131,23 @@ def _split(
131
use_eagle_block_drop: bool | None = None,
132
partial_hit: bool = False,
133
num_prefill_checkpoint_blocks: int = 0,
134
+ max_num_scheduled_tokens: int = 16384,
135
) -> int:
136
"""Call the real `Scheduler._mamba_block_aligned_split` on a stub self."""
137
if use_eagle_block_drop is None:
138
use_eagle_block_drop = use_eagle
139
stub = SimpleNamespace(
140
+ block_size=MAMBA_BLOCK_SIZE,
141
cache_config=SimpleNamespace(block_size=MAMBA_BLOCK_SIZE),
142
use_eagle_block_drop=use_eagle_block_drop,
93
- max_num_scheduled_tokens=16384,
143
+ max_num_scheduled_tokens=max_num_scheduled_tokens,
144
scheduler_config=SimpleNamespace(long_prefill_token_threshold=0),
145
# `prefix_match_unit` finer than the block size (#46384).
146
mamba_partial_cache_hit=partial_hit,
147
hash_block_size=ATTN_BLOCK_SIZE,
98
- mamba_has_prefill_checkpoint_blocks=(
99
- num_prefill_checkpoint_blocks > 0 and not use_eagle
148
+ mamba_has_prefill_checkpoint_blocks=(num_prefill_checkpoint_blocks > 0),
149
+ mamba_prefill_checkpoint_alignment=(
150
+ 16 if num_prefill_checkpoint_blocks > 0 else None
151
),
152
)
153
return Scheduler._mamba_block_aligned_split(stub, request, num_new_tokens)
@@ -106,8 +157,11 @@ def _split(
157
("prompt_len", "num_new_tokens", "use_eagle", "expected"),
158
[
159
(2002, 2002, False, 2002),
109
- (3602, 2000, False, 2000),
110
- (3602, 3602, True, MAMBA_BLOCK_SIZE),
160
+ (3602, 2000, False, MAMBA_BLOCK_SIZE),
161
+ (3602, 3602, True, 3602),
162
+ (2002, 2002, True, 2002),
163
+ (3602, 512, True, 0),
164
+ (3602, 1700, True, MAMBA_BLOCK_SIZE),
165
],
166
)
167
def test_internal_checkpoint_split(
@@ -123,12 +177,39 @@ def test_internal_checkpoint_split(
177
)
178
== expected
179
)
126
- if not use_eagle:
127
- manager = _make_hybrid_kv_cache_manager(num_prefill_checkpoint_blocks=1)
128
- assert manager.allocate_slots(request, expected) is not None
129
- mamba_manager = manager.coordinator.single_type_managers[MAMBA_GROUP_ID]
130
- blocks = mamba_manager.req_to_blocks[request.request_id]
131
- assert all(not block.is_null for block in blocks) # checkpoint + running state
180
+
181
+
182
+def test_partial_checkpoint_resume_stops_at_mamba_block_boundary() -> None:
183
+ prompt_len = 3602
184
+ resume_at = 1984
185
+ (request,) = create_requests(1, num_tokens=prompt_len, block_size=ATTN_BLOCK_SIZE)
186
+ request.num_computed_tokens = resume_at
187
+
188
+ # Without an internal checkpoint export, the forward must stop at the
189
+ # physical Mamba boundary so it does not publish a stale running state.
190
+ assert (
191
+ _split(
192
+ request,
193
+ prompt_len - resume_at,
194
+ use_eagle=False,
195
+ partial_hit=True,
196
+ )
197
+ == MAMBA_BLOCK_SIZE - resume_at % MAMBA_BLOCK_SIZE
198
+ )
199
+
200
+ # A hash-aligned resume inside a physical Mamba block cannot use the fixed
201
+ # internal-checkpoint column: it aliases the initial-state column. Enabling
202
+ # checkpoints must therefore preserve the normal aligned split.
203
+ assert (
204
+ _split(
205
+ request,
206
+ prompt_len - resume_at,
207
+ use_eagle=False,
208
+ partial_hit=True,
209
+ num_prefill_checkpoint_blocks=1,
210
+ )
211
+ == MAMBA_BLOCK_SIZE - resume_at % MAMBA_BLOCK_SIZE
212
+ )
213
214
215
def test_disabling_eagle_block_drop_keeps_the_trailing_cache_boundary() -> None:
@@ -252,8 +333,6 @@ def test_poisoning_is_block_size_independent(
333
budgets: list[int],
334
) -> None:
335
"""The invariant is per-block, so large mamba blocks are not safer."""
255
- import sys
256
-
336
monkeypatch.setattr(sys.modules[__name__], "MAMBA_BLOCK_SIZE", block_size)
337
assert _prefill(prompt_len, budgets=budgets) > 0
338
tests/v1/core/test_prefix_caching.py
+1
@@ -1162,6 +1162,7 @@ def test_hybrid_cache_mamba_align_shared_prefix_detection():
1162
# junction (absolute) and stops the chunk there (req_2 has num_computed 0).
1163
# Create minimal mock with just the needed attributes
1164
mock = SimpleNamespace(
1165
+ block_size=block_size,
1166
cache_config=SimpleNamespace(block_size=block_size),
1167
max_num_scheduled_tokens=3 * block_size,
1168
scheduler_config=SimpleNamespace(long_prefill_token_threshold=0),
vllm/models/kimi_k3/nvidia/kda.py
+3
@@ -609,6 +609,9 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
609
return replace(
610
spec,
611
num_prefill_checkpoint_blocks=int(self.kda_prefill_backend == "flashkda"),
612
+ prefill_checkpoint_alignment=(
613
+ 16 if self.kda_prefill_backend == "flashkda" else None
614
+ ),
615
)
616
617
def forward(
vllm/models/kimi_k3/nvidia/kda_metadata.py
+35
-15
@@ -20,6 +20,7 @@ import torch
20
from vllm.config import VllmConfig
21
from vllm.platforms import current_platform
22
from vllm.triton_utils import tl, triton
23
+from vllm.utils.math_utils import cdiv
24
from vllm.utils.torch_utils import async_tensor_h2d
25
from vllm.v1.attention.backend import CommonAttentionMetadata
26
from vllm.v1.attention.backends.gdn_attn import (
@@ -36,7 +37,11 @@ from vllm.v1.attention.backends.utils import (
37
compute_causal_conv1d_metadata,
38
split_decodes_and_prefills,
39
)
39
-from vllm.v1.kv_cache_interface import MambaSpec
40
+from vllm.v1.kv_cache_interface import (
41
+ MambaSpec,
42
+ get_mamba_prefill_checkpoint_position,
43
+ is_mamba_prefill_checkpoint_valid,
44
+)
45
46
if TYPE_CHECKING:
47
from vllm.models.kimi_k3.nvidia.ops.recoverssm import (
@@ -44,9 +49,6 @@ if TYPE_CHECKING:
49
)
50
51
47
-FLASHKDA_CHUNK_SIZE = 16
48
-
49
-
52
@cache
53
def _metadata_launch_pdl() -> bool:
54
return current_platform.is_arch_support_pdl()
@@ -582,11 +584,11 @@ class KimiK3KDAMetadataBuilder(GDNAttentionMetadataBuilder):
584
has_initial_state = None
585
586
checkpoint = None
585
- if (
586
- num_prefills > 0
587
+ checkpoint_enabled = (
588
+ self.vllm_config.cache_config.mamba_cache_mode == "align"
589
and self.kv_cache_spec.num_prefill_checkpoint_blocks > 0
588
- and self.vllm_config.cache_config.mamba_cache_mode == "align"
589
- ):
590
+ )
591
+ if num_prefills > 0 and checkpoint_enabled:
592
# prepare checkpoint metadata
593
assert m.seq_lens_cpu_upper_bound is not None
594
request_rows = list(range(m.num_reqs))
@@ -597,21 +599,39 @@ class KimiK3KDAMetadataBuilder(GDNAttentionMetadataBuilder):
599
query_lens = [all_query_lens[row] for row in request_rows]
600
seq_lens = m.seq_lens_cpu_upper_bound.tolist()
601
block_size = self.kv_cache_spec.block_size
602
+ hash_block_size = (
603
+ self.vllm_config.cache_config.prefix_match_unit or block_size
604
+ )
605
+ speculative_config = self.vllm_config.speculative_config
606
+ drop_eagle_block = (
607
+ speculative_config is not None
608
+ and speculative_config.use_eagle_block_drop()
609
+ )
610
checkpoint_splits = []
611
checkpoint_cols = []
612
for row, query_len in zip(request_rows, query_lens):
613
seq_len = seq_lens[row]
604
- offset = seq_len // block_size * block_size - (seq_len - query_len)
605
- # offset should be less than query_len
606
- valid = (
607
- seq_len % block_size != 0
608
- and 0 < offset < query_len
609
- and offset % FLASHKDA_CHUNK_SIZE == 0
614
+ query_start = seq_len - query_len
615
+ checkpoint_position = get_mamba_prefill_checkpoint_position(
616
+ seq_len,
617
+ hash_block_size,
618
+ drop_eagle_block=drop_eagle_block,
619
+ )
620
+ offset = checkpoint_position - query_start
621
+ valid = is_mamba_prefill_checkpoint_valid(
622
+ query_start=query_start,
623
+ query_end=seq_len,
624
+ checkpoint_position=checkpoint_position,
625
+ hash_block_size=hash_block_size,
626
+ mamba_block_size=block_size,
627
+ checkpoint_alignment=(
628
+ self.kv_cache_spec.prefill_checkpoint_alignment
629
+ ),
630
)
631
offset = offset if valid else 0
632
first_len = offset or query_len
633
checkpoint_splits.append((first_len, query_len - first_len))
614
- checkpoint_cols.append(seq_len // block_size - 1 if valid else -1)
634
+ checkpoint_cols.append(cdiv(seq_len, block_size) - 2 if valid else -1)
635
if any(tail for _, tail in checkpoint_splits):
636
checkpoint_offsets_tensor = async_tensor_h2d(
637
[first if tail else 0 for first, tail in checkpoint_splits],
vllm/v1/core/block_pool.py
+12
-3
@@ -450,6 +450,7 @@ class BlockPool:
450
num_tokens: int,
451
kv_cache_group_id: int,
452
block_size: int,
453
+ replace_existing_hashes: bool = False,
454
) -> BlockHashWithGroupId | None:
455
"""Register a partial prefix-cache entry for an existing block.
456
@@ -477,6 +478,9 @@ class BlockPool:
478
entry hash itself is always the prefix-chain hash at
479
``num_tokens``; ``block_size`` is used to assert that the
480
entry is partial within the owning cache block.
481
+ replace_existing_hashes: Whether the block contents were replaced
482
+ and all existing cache entries must be removed before the new
483
+ entry is registered.
484
485
Returns:
486
The hash key with group ID if a partial entry can be registered;
@@ -485,9 +489,10 @@ class BlockPool:
489
if block.is_null:
490
return None
491
488
- assert block_size > self.hash_block_size
492
assert block_size % self.hash_block_size == 0
490
- assert num_tokens % block_size != 0
493
+ assert replace_existing_hashes or (
494
+ block_size > self.hash_block_size and num_tokens % block_size != 0
495
+ )
496
block_hash = self._get_partial_block_hash(request, num_tokens)
497
num_hash_blocks = num_tokens // self.hash_block_size
498
block_hash_with_group_id = make_block_hash_with_group_id(
@@ -498,7 +503,11 @@ class BlockPool:
503
block_hash_with_group_id, block.block_id
504
)
505
)
501
- if (
506
+ if replace_existing_hashes:
507
+ removed_hashes = self._remove_cached_block_hashes(block)
508
+ self._emit_block_removed_events(removed_hashes)
509
+ already_cached = False
510
+ elif (
511
not already_cached
512
and block.block_hash is not None
513
and block.block_hash_num_tokens is not None
vllm/v1/core/kv_cache_coordinator.py
+6
@@ -15,6 +15,7 @@ from vllm.v1.core.kv_cache_utils import (
15
)
16
from vllm.v1.core.single_type_kv_cache_manager import (
17
CrossAttentionManager,
18
+ MambaManager,
19
SingleTypeKVCacheManager,
20
get_manager_for_kv_cache_spec,
21
)
@@ -151,6 +152,11 @@ class KVCacheCoordinator(ABC):
152
)
153
for i, kv_cache_group in enumerate(self.kv_cache_config.kv_cache_groups)
154
)
155
+ # Match Mamba checkpoints to Eagle's attention replay boundary.
156
+ if use_eagle:
157
+ for manager in self.single_type_managers:
158
+ if isinstance(manager, MambaManager):
159
+ manager.drop_eagle_checkpoint_block = True
160
161
# A positive retention interval must be a multiple of the base hit granularity
162
# (``scheduler_block_size``) to land on real cache-hit boundaries.
vllm/v1/core/sched/scheduler.py
+39
-13
@@ -54,7 +54,12 @@ from vllm.v1.core.sched.request_queue import (
54
)
55
from vllm.v1.core.sched.utils import check_stop, remove_all
56
from vllm.v1.engine import EngineCoreEventType, EngineCoreOutput, EngineCoreOutputs
57
-from vllm.v1.kv_cache_interface import KVCacheConfig, MambaSpec
57
+from vllm.v1.kv_cache_interface import (
58
+ KVCacheConfig,
59
+ MambaSpec,
60
+ get_mamba_prefill_checkpoint_position,
61
+ is_mamba_prefill_checkpoint_valid,
62
+)
63
from vllm.v1.metrics.perf import ModelMetrics, PerfStats
64
from vllm.v1.metrics.stats import (
65
PrefixCacheStats,
@@ -334,15 +339,20 @@ class Scheduler(SchedulerInterface):
339
self.need_mamba_block_aligned_split = (
340
self.has_mamba_layers and self.cache_config.mamba_cache_mode == "align"
341
)
337
- self.mamba_has_prefill_checkpoint_blocks = (
338
- self.has_mamba_layers
339
- # TODO: support spec decoding
340
- and not self.use_eagle
341
- and all(
342
- not isinstance(group.kv_cache_spec, MambaSpec)
343
- or group.kv_cache_spec.num_prefill_checkpoint_blocks > 0
342
+ # TODO: Support models with multiple Mamba specs that require different
343
+ # prefill checkpoint alignments instead of selecting the first one.
344
+ self.mamba_prefill_checkpoint_alignment = next(
345
+ (
346
+ group.kv_cache_spec.prefill_checkpoint_alignment
347
for group in kv_cache_config.kv_cache_groups
345
- )
348
+ if isinstance(group.kv_cache_spec, MambaSpec)
349
+ ),
350
+ None,
351
+ )
352
+ self.mamba_has_prefill_checkpoint_blocks = self.has_mamba_layers and all(
353
+ not isinstance(group.kv_cache_spec, MambaSpec)
354
+ or group.kv_cache_spec.num_prefill_checkpoint_blocks > 0
355
+ for group in kv_cache_config.kv_cache_groups
356
)
357
# A finer prefix_match_unit is configured: a mamba partial tail entry
358
# can only be registered by a step ending exactly at the prompt's last
@@ -426,8 +436,22 @@ class Scheduler(SchedulerInterface):
436
last_cache_position = max(last_cache_position - block_size, 0)
437
438
end = start + num_new_tokens
439
+ checkpoint_position = get_mamba_prefill_checkpoint_position(
440
+ prefill_end,
441
+ self.hash_block_size,
442
+ drop_eagle_block=self.use_eagle_block_drop,
443
+ )
444
use_internal_checkpoint = (
430
- self.mamba_has_prefill_checkpoint_blocks and start % block_size == 0
445
+ self.mamba_has_prefill_checkpoint_blocks
446
+ and end >= prefill_end
447
+ and is_mamba_prefill_checkpoint_valid(
448
+ query_start=start,
449
+ query_end=end,
450
+ checkpoint_position=checkpoint_position,
451
+ hash_block_size=self.hash_block_size,
452
+ mamba_block_size=block_size,
453
+ checkpoint_alignment=self.mamba_prefill_checkpoint_alignment,
454
+ )
455
)
456
if use_internal_checkpoint:
457
last_cache_position = 0
@@ -436,7 +460,7 @@ class Scheduler(SchedulerInterface):
460
# aligned. Exempt: the prompt's last chunk, whose slot decode advances
461
# to the boundary. A block too wide for one chunk advances sub-block
462
# and re-aligns at the next boundary.
439
- if end < prefill_end and not use_internal_checkpoint:
463
+ if end < prefill_end:
464
max_prefill_tokens = self.max_num_scheduled_tokens
465
long_prefill_threshold = self.scheduler_config.long_prefill_token_threshold
466
if long_prefill_threshold > 0:
@@ -448,13 +472,15 @@ class Scheduler(SchedulerInterface):
472
next_block_boundary = (start // block_size + 1) * block_size
473
tail_boundary = (
474
request.num_prompt_tokens // self.hash_block_size * self.hash_block_size
451
- if self.mamba_partial_cache_hit
475
+ if self.mamba_partial_cache_hit and not use_internal_checkpoint
476
else 0
477
)
478
stops = (
479
# Same invariant: a chunk starting mid-block stops at the boundary
480
# rather than running past it.
457
- next_block_boundary if start % block_size != 0 else 0,
481
+ next_block_boundary
482
+ if start % block_size != 0 and not use_internal_checkpoint
483
+ else 0,
484
# Never run past the last cacheable block boundary mid-chunk.
485
last_cache_position,
486
# Fine-grained hits: the prompt's partial-tail entry can only be
vllm/v1/core/single_type_kv_cache_manager.py
+80
-24
@@ -31,6 +31,8 @@ from vllm.v1.kv_cache_interface import (
31
SinkFullAttentionSpec,
32
SlidingWindowMLASpec,
33
SlidingWindowSpec,
34
+ get_mamba_prefill_checkpoint_position,
35
+ is_mamba_prefill_checkpoint_valid,
36
)
37
from vllm.v1.kv_cache_spec_registry import KVCacheSpecRegistry
38
from vllm.v1.request import Request
@@ -1399,6 +1401,12 @@ class MambaManager(SingleTypeKVCacheManager):
1401
self.block_size = kv_cache_spec.block_size
1402
self.mamba_cache_mode = kv_cache_spec.mamba_cache_mode
1403
self.num_speculative_blocks: int = kv_cache_spec.num_speculative_blocks
1404
+ self.has_prefill_checkpoint_blocks = (
1405
+ self.mamba_cache_mode == "align"
1406
+ and kv_cache_spec.num_prefill_checkpoint_blocks > 0
1407
+ )
1408
+ # Mamba checkpoints follow Eagle's global replay boundary.
1409
+ self.drop_eagle_checkpoint_block = False
1410
self.cached_blocks_this_step: set[BlockHashWithGroupId] = set()
1411
if self.mamba_cache_mode == "align":
1412
# Mapping from request ID to the index of the block
@@ -1406,9 +1414,9 @@ class MambaManager(SingleTypeKVCacheManager):
1414
self.last_state_block_idx: dict[str, int] = {}
1415
# The set of the requests that have been allocated blocks
1416
self._allocated_block_reqs: set[str] = set()
1409
- # Number of internal checkpoint blocks required by each request's
1417
+ # Absolute checkpoint position selected by the scheduler for the
1418
# current allocation.
1411
- self._num_checkpoint_blocks: dict[str, int] = {}
1419
+ self._checkpoint_positions: dict[str, int] = {}
1420
# Requests that registered their own last-prompt-boundary partial
1421
# tail (producers). A later CoW hands its private copy to the
1422
# connector; a request that finishes first hands off this table
@@ -1592,18 +1600,32 @@ class MambaManager(SingleTypeKVCacheManager):
1600
def _needs_internal_checkpoint(
1601
self,
1602
request_id: str,
1595
- num_tokens: int,
1596
- num_computed_tokens: int,
1603
+ query_start: int,
1604
+ query_end: int,
1605
+ checkpoint_position: int,
1606
) -> bool:
1607
assert isinstance(self.kv_cache_spec, MambaSpec)
1599
- checkpoint_idx = cdiv(num_tokens, self.block_size) - 2
1608
+ checkpoint_idx = cdiv(query_end, self.block_size) - 2
1609
blocks = self.req_to_blocks[request_id]
1610
return (
1602
- self.kv_cache_spec.num_prefill_checkpoint_blocks > 0
1603
- and num_tokens % self.block_size != 0
1604
- and num_computed_tokens % self.block_size == 0
1611
+ self.has_prefill_checkpoint_blocks
1612
+ and is_mamba_prefill_checkpoint_valid(
1613
+ query_start=query_start,
1614
+ query_end=query_end,
1615
+ checkpoint_position=checkpoint_position,
1616
+ hash_block_size=self.block_pool.hash_block_size,
1617
+ mamba_block_size=self.block_size,
1618
+ checkpoint_alignment=(self.kv_cache_spec.prefill_checkpoint_alignment),
1619
+ )
1620
and checkpoint_idx >= 0
1606
- and (checkpoint_idx >= len(blocks) or blocks[checkpoint_idx].is_null)
1621
+ and (
1622
+ checkpoint_idx >= len(blocks)
1623
+ or blocks[checkpoint_idx].is_null
1624
+ or (
1625
+ request_id in self._allocated_block_reqs
1626
+ and checkpoint_idx >= len(blocks) - self.num_speculative_blocks
1627
+ )
1628
+ )
1629
)
1630
1631
def get_num_blocks_to_allocate(
@@ -1668,17 +1690,27 @@ class MambaManager(SingleTypeKVCacheManager):
1690
)
1691
if has_partial_hit:
1692
num_new_blocks = max(num_new_blocks, 0) + 1
1671
- checkpoint_block = int(
1672
- self._needs_internal_checkpoint(
1673
- request_id, num_tokens, total_computed_tokens
1674
- )
1693
+ checkpoint_position = get_mamba_prefill_checkpoint_position(
1694
+ num_tokens,
1695
+ self.block_pool.hash_block_size,
1696
+ self.drop_eagle_checkpoint_block,
1697
)
1698
+ if not self._needs_internal_checkpoint(
1699
+ request_id,
1700
+ total_computed_tokens,
1701
+ num_tokens,
1702
+ checkpoint_position,
1703
+ ):
1704
+ checkpoint_position = 0
1705
+ checkpoint_block = int(checkpoint_position > 0)
1706
if not apply_admission_cap:
1677
- self._num_checkpoint_blocks[request_id] = checkpoint_block
1707
+ self._checkpoint_positions[request_id] = checkpoint_position
1708
if num_new_blocks > 0:
1679
- num_new_blocks = 1 + int(has_partial_hit) + checkpoint_block
1680
- if request_id not in self._allocated_block_reqs:
1681
- num_new_blocks += self.num_speculative_blocks
1709
+ blocks_allocated = request_id in self._allocated_block_reqs
1710
+ if not (checkpoint_block and blocks_allocated):
1711
+ num_new_blocks = 1 + int(has_partial_hit) + checkpoint_block
1712
+ if not blocks_allocated:
1713
+ num_new_blocks += self.num_speculative_blocks
1714
1715
num_evictable_computed_blocks = self._get_num_evictable_blocks(
1716
new_computed_blocks
@@ -1710,12 +1742,17 @@ class MambaManager(SingleTypeKVCacheManager):
1742
num_required_blocks = (
1743
cdiv(num_tokens, self.block_size) + self.num_speculative_blocks
1744
)
1713
- checkpoint_block = self._num_checkpoint_blocks.get(request_id, 0)
1745
+ checkpoint_position = self._checkpoint_positions.get(request_id, 0)
1746
+ checkpoint_block = int(checkpoint_position > 0)
1747
partial_hit = self._partial_hit_reqs.get(request_id)
1748
has_partial_hit = partial_hit is not None
1749
# `num_required_blocks` might be less than `len(req_blocks)` if blocks are
1750
# over-allocated at last round.
1718
- if num_required_blocks <= len(req_blocks) and not has_partial_hit:
1751
+ if (
1752
+ num_required_blocks <= len(req_blocks)
1753
+ and not has_partial_hit
1754
+ and not checkpoint_block
1755
+ ):
1756
self._allocated_block_reqs.add(request_id)
1757
return []
1758
else:
@@ -1745,7 +1782,7 @@ class MambaManager(SingleTypeKVCacheManager):
1782
[self._null_block for _ in range(prev_block_len, null_end)]
1783
)
1784
1748
- if blocks_allocated:
1785
+ if blocks_allocated and not checkpoint_block:
1786
# Relocate exclusively owned speculative scratch blocks.
1787
for block_idx in range(
1788
prev_block_len - self.num_speculative_blocks, prev_block_len
@@ -1754,11 +1791,11 @@ class MambaManager(SingleTypeKVCacheManager):
1791
self._relocate_speculative_block(req_blocks, block_idx)
1792
else:
1793
break
1757
- num_new_blocks = num_required_blocks - len(req_blocks)
1794
+ num_new_blocks = max(num_required_blocks - len(req_blocks), 0)
1795
if has_partial_hit:
1796
num_new_blocks = max(num_new_blocks, 0) + 1
1797
max_new_blocks = 1 + int(has_partial_hit) + checkpoint_block
1761
- if not blocks_allocated:
1798
+ if not blocks_allocated or checkpoint_block:
1799
max_new_blocks += self.num_speculative_blocks
1800
assert num_new_blocks <= max_new_blocks
1801
new_blocks = self.block_pool.get_new_blocks(num_new_blocks)
@@ -1838,7 +1875,7 @@ class MambaManager(SingleTypeKVCacheManager):
1875
if self.mamba_cache_mode == "align":
1876
self._allocated_block_reqs.discard(request_id)
1877
self.last_state_block_idx.pop(request_id, None)
1841
- self._num_checkpoint_blocks.pop(request_id, None)
1878
+ self._checkpoint_positions.pop(request_id, None)
1879
self._producer_partial_tail_reqs.pop(request_id, None)
1880
# An offer is only guaranteed to hold committed bytes until the end
1881
# of the pass that made it. This request's blocks are going back to
@@ -1885,6 +1922,7 @@ class MambaManager(SingleTypeKVCacheManager):
1922
continue
1923
self.cached_blocks_this_step.add(block.block_hash)
1924
if self.mamba_cache_mode == "align":
1925
+ assert block.block_hash_num_tokens is not None
1926
# Offer every retained boundary with its exact block.
1927
# The connector filters against its save window, which may
1928
# extend past the original prompt during resumed prefill.
@@ -1893,7 +1931,7 @@ class MambaManager(SingleTypeKVCacheManager):
1931
request.request_id,
1932
self.kv_cache_group_id,
1933
block,
1896
- (idx + 1) * self.block_size,
1934
+ block.block_hash_num_tokens,
1935
)
1936
)
1937
@@ -1906,6 +1944,24 @@ class MambaManager(SingleTypeKVCacheManager):
1944
num_tokens: int,
1945
) -> BlockHashWithGroupId | None:
1946
hash_block_size = self.block_pool.hash_block_size
1947
+ # Re-key the reserved block at its exported checkpoint boundary.
1948
+ checkpoint_position = self._checkpoint_positions.get(request.request_id, 0)
1949
+ if checkpoint_position > 0:
1950
+ # TODO: Store the reserved slot explicitly for multi-module MTP.
1951
+ checkpoint_idx = cdiv(num_tokens, self.block_size) - 2
1952
+ blocks = self.req_to_blocks[request.request_id]
1953
+ assert 0 <= checkpoint_idx < len(blocks)
1954
+ checkpoint_block = blocks[checkpoint_idx]
1955
+ if checkpoint_block.block_hash_num_tokens == checkpoint_position:
1956
+ return None
1957
+ return self.block_pool.cache_partial_block(
1958
+ request=request,
1959
+ block=checkpoint_block,
1960
+ num_tokens=checkpoint_position,
1961
+ kv_cache_group_id=self.kv_cache_group_id,
1962
+ block_size=self.block_size,
1963
+ replace_existing_hashes=True,
1964
+ )
1965
if self.block_size == hash_block_size:
1966
return None
1967
if num_tokens % self.block_size == 0:
vllm/v1/kv_cache_interface.py
+38
@@ -890,6 +890,7 @@ class MambaSpec(KVCacheSpec):
890
mamba_cache_mode: str = "none"
891
num_speculative_blocks: int = 0
892
num_prefill_checkpoint_blocks: int = 0
893
+ prefill_checkpoint_alignment: int | None = None
894
num_heads: int = 1
895
tokens_per_state: int = -1
896
# False: the state is sharded across TP ranks (e.g. GDN). True: every TP
@@ -950,12 +951,49 @@ class MambaSpec(KVCacheSpec):
951
isinstance(spec, MambaSpec)
952
and spec.num_speculative_blocks == self.num_speculative_blocks
953
and spec.num_prefill_checkpoint_blocks == self.num_prefill_checkpoint_blocks
954
+ and spec.prefill_checkpoint_alignment == self.prefill_checkpoint_alignment
955
and spec.page_size_bytes == self.page_size_bytes
956
and spec.tp_replicated == self.tp_replicated
957
for spec in kv_cache_specs.values()
958
)
959
960
961
+def get_mamba_prefill_checkpoint_position(
962
+ num_tokens: int,
963
+ hash_block_size: int,
964
+ drop_eagle_block: bool,
965
+) -> int:
966
+ """Return the reusable Mamba checkpoint boundary for a prefill."""
967
+ checkpoint_position = (num_tokens - 1) // hash_block_size * hash_block_size
968
+ if drop_eagle_block:
969
+ checkpoint_position -= hash_block_size
970
+ return max(checkpoint_position, 0)
971
+
972
+
973
+def is_mamba_prefill_checkpoint_valid(
974
+ query_start: int,
975
+ query_end: int,
976
+ checkpoint_position: int,
977
+ hash_block_size: int,
978
+ mamba_block_size: int,
979
+ checkpoint_alignment: int | None,
980
+) -> bool:
981
+ """Whether a backend can export the checkpoint in this query."""
982
+ if checkpoint_alignment is None:
983
+ return False
984
+ assert checkpoint_alignment > 0
985
+
986
+ initial_state_col = (query_start - 1) // mamba_block_size
987
+ checkpoint_col = cdiv(query_end, mamba_block_size) - 2
988
+ return (
989
+ query_start % hash_block_size == 0
990
+ and checkpoint_col > initial_state_col
991
+ and query_start + hash_block_size <= checkpoint_position
992
+ and query_start < checkpoint_position < query_end
993
+ and (checkpoint_position - query_start) % checkpoint_alignment == 0
994
+ )
995
+
996
+
997
@dataclass(frozen=True)
998
class EncoderOnlyAttentionSpec(AttentionSpec):
999
def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int: