[CPU] Enable chunked prefill and prefix caching for qwen3.5 (#46202)
Signed-off-by: Li, Tianmu <tianmu.li@intel.com> Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Li, Jiang <jiang1.li@intel.com>
Tianmu Li committed
Jun 24, 2026 at 20:49 UTC
1aad1258157b9327a1510f3889b6983d6f10004e
7 files changed
+388
-32
.buildkite/hardware_tests/cpu.yaml
+4
-2
@@ -53,7 +53,7 @@ steps:
53
- tests/models/language/pooling/
54
commands:
55
- |
56
- bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 40m "
56
+ bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 50m "
57
pytest -x -v -s tests/models/language/generation -m cpu_model
58
pytest -x -v -s tests/models/language/pooling -m cpu_model"
59
@@ -68,13 +68,15 @@ steps:
68
- vllm/v1/sample/ops/topk_topp_triton.py
69
- vllm/v1/sample/ops/topk_topp_sampler.py
70
- tests/v1/sample/test_topk_topp_sampler.py
71
+ - tests/v1/e2e/test_cpu_linear_attn_chunked_prefix.py
72
commands:
73
- |
74
bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 45m "
75
uv pip install git+https://github.com/triton-lang/triton-cpu.git@270e696d
76
VLLM_USE_V2_MODEL_RUNNER=1 pytest -x -v -s tests/models/language/generation/test_granite.py -m cpu_model
77
# TODO: move to CPU-Kernel Tests once triton-cpu has a pre-built wheel
77
- pytest -x -v -s tests/v1/sample/test_topk_topp_sampler.py::TestTritonTopkTopp"
78
+ pytest -x -v -s tests/v1/sample/test_topk_topp_sampler.py::TestTritonTopkTopp
79
+ pytest -x -v -s tests/v1/e2e/test_cpu_linear_attn_chunked_prefix.py"
80
81
- label: CPU-Quantization Model Tests
82
depends_on: []
csrc/cpu/sgl-kernels/conv.cpp
+14
-12
@@ -289,19 +289,18 @@ void causal_conv1d_fwd_kernel_impl(
289
}
290
}
291
292
-#define LAUNCH_TINYGEMM_VARLEN_KERNEL(K, NB_SIZE) \
293
- tinygemm_kernel<scalar_t, K, NB_SIZE, has_bias, has_silu>::apply( \
294
- input + batch_offset * dim + mb_start * dim + nb_start, \
295
- weight + nb_start * width, \
296
- out + batch_offset * dim + mb_start * dim + nb_start, \
297
- has_bias ? bias + nb_start : nullptr, \
298
- nullptr, \
299
- false, \
300
- mb_size, \
301
- dim, \
292
+#define LAUNCH_TINYGEMM_VARLEN_KERNEL(K, NB_SIZE) \
293
+ tinygemm_kernel<scalar_t, K, NB_SIZE, has_bias, has_silu>::apply( \
294
+ input + batch_offset * dim + mb_start * dim + nb_start, \
295
+ weight + nb_start * width, \
296
+ out + batch_offset * dim + mb_start * dim + nb_start, \
297
+ has_bias ? bias + nb_start : nullptr, \
298
+ has_conv_states ? conv_states + conv_state_index * conv_state_slot_stride + nb_start : nullptr, \
299
+ has_initial_states_value, \
300
+ mb_size, \
301
+ dim, \
302
mb_start == 0);
303
304
-// TODO: add `has_initial_state` support for varlen kernel
304
template <typename scalar_t>
305
void causal_conv1d_fwd_varlen_kernel_impl(
306
scalar_t* __restrict__ out,
@@ -343,6 +342,9 @@ void causal_conv1d_fwd_varlen_kernel_impl(
342
int64_t nb_start = nb * BLOCK_N;
343
int64_t nb_size = std::min(dim - nb_start, BLOCK_N);
344
345
+ const bool has_initial_states_value = has_conv_states ? has_initial_state[bs] : false;
346
+ int32_t conv_state_index = has_conv_indices ? conv_indices[bs] : bs;
347
+
348
switch (width << 4 | nb_size >> 4) {
349
case 0x42:
350
LAUNCH_TINYGEMM_VARLEN_KERNEL(4, 32);
@@ -373,7 +375,7 @@ void causal_conv1d_fwd_varlen_kernel_impl(
375
width,
376
dim,
377
seqlen,
376
- /* has_initial_state */ false);
378
+ has_initial_state[bs]);
379
}
380
});
381
}
tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
+224
@@ -25,6 +25,8 @@ HEAD_DIMS = [
25
(64, 32),
26
]
27
CHUNK_SIZE = 64
28
+CONV_DIM = 128
29
+CONV_KERNEL = 4
30
PREFILL_SEQ_LENS = [
31
[1],
32
[1, 2, 3],
@@ -312,3 +314,225 @@ def test_chunk_gated_delta_rule_cpu(
314
atol=1e-2,
315
rtol=1e-2,
316
)
317
+
318
+
319
+# (total_tokens, split) pairs mimicking where chunked prefill breaks a sequence
320
+# across two scheduler steps: chunk-aligned and non-aligned splits.
321
+TWO_CALL_SPLITS = [
322
+ (2 * CHUNK_SIZE, CHUNK_SIZE),
323
+ (2 * CHUNK_SIZE + 17, CHUNK_SIZE),
324
+ (2 * CHUNK_SIZE + 17, CHUNK_SIZE + 9),
325
+ (4 * CHUNK_SIZE + 17, 2 * CHUNK_SIZE),
326
+ (3 * CHUNK_SIZE, CHUNK_SIZE + 1),
327
+]
328
+
329
+
330
+@pytest.mark.parametrize("total_tokens, split", TWO_CALL_SPLITS)
331
+@pytest.mark.parametrize("num_heads", NUM_HEADS)
332
+@pytest.mark.parametrize("head_dims", HEAD_DIMS)
333
+@torch.inference_mode()
334
+def test_chunk_gated_delta_rule_cpu_two_call_split(
335
+ total_tokens: int,
336
+ split: int,
337
+ num_heads: tuple[int, int],
338
+ head_dims: tuple[int, int],
339
+) -> None:
340
+ """A prefill split into two calls (the second seeded with the first's
341
+ ``final_state`` and a rebased ``cu_seqlens``) must match the single-call
342
+ result, mimicking the cross-scheduler-step handoff in
343
+ ``cpu_gdn_attention_core``.
344
+ """
345
+ q, k, v, a, b, A_log, dt_bias = gdn_inputs(
346
+ num_tokens=total_tokens,
347
+ num_heads=num_heads,
348
+ head_dims=head_dims,
349
+ )
350
+ _, num_v_heads = num_heads
351
+ head_dim, v_head_dim = head_dims
352
+
353
+ g, beta = ref_gdn_gating(A_log, a, b, dt_bias)
354
+ g = g.unsqueeze(0) # [1, T, HV]
355
+ beta = beta.unsqueeze(0)
356
+
357
+ zero_state = torch.zeros(1, num_v_heads, head_dim, v_head_dim, dtype=torch.float32)
358
+
359
+ # Reference: whole sequence in one call, no initial state.
360
+ out_full, final_full = ops.chunk_gated_delta_rule_cpu(
361
+ query=q,
362
+ key=k,
363
+ value=v,
364
+ g=g,
365
+ beta=beta,
366
+ initial_state=zero_state,
367
+ output_final_state=True,
368
+ cu_seqlens=torch.tensor([0, total_tokens], dtype=torch.int32),
369
+ head_first=False,
370
+ use_qk_l2norm_in_kernel=True,
371
+ )
372
+
373
+ # Call 1: tokens [0:split], no initial state, capture final state.
374
+ out1, state1 = ops.chunk_gated_delta_rule_cpu(
375
+ query=q[:, :split],
376
+ key=k[:, :split],
377
+ value=v[:, :split],
378
+ g=g[:, :split],
379
+ beta=beta[:, :split],
380
+ initial_state=zero_state,
381
+ output_final_state=True,
382
+ cu_seqlens=torch.tensor([0, split], dtype=torch.int32),
383
+ head_first=False,
384
+ use_qk_l2norm_in_kernel=True,
385
+ )
386
+ # Call 2: tokens [split:T] seeded with call 1's final state and a cu_seqlens
387
+ # rebased to start at 0, as cpu_gdn_attention_core continues a prefill chunk.
388
+ tail = total_tokens - split
389
+ out2, state2 = ops.chunk_gated_delta_rule_cpu(
390
+ query=q[:, split:],
391
+ key=k[:, split:],
392
+ value=v[:, split:],
393
+ g=g[:, split:],
394
+ beta=beta[:, split:],
395
+ initial_state=state1.to(torch.float32),
396
+ output_final_state=True,
397
+ cu_seqlens=torch.tensor([0, tail], dtype=torch.int32),
398
+ head_first=False,
399
+ use_qk_l2norm_in_kernel=True,
400
+ )
401
+
402
+ out_split = torch.cat([out1, out2], dim=1)
403
+
404
+ # State must be near-exact; output allows a looser bound for the bf16 round-trip.
405
+ torch.testing.assert_close(state2, final_full, atol=1e-3, rtol=1e-3)
406
+ torch.testing.assert_close(out_split, out_full, atol=2e-2, rtol=2e-2)
407
+
408
+
409
+def _conv_inputs(total_tokens: int):
410
+ x = tensor_cache(total_tokens * CONV_DIM, torch.bfloat16).view(
411
+ total_tokens, CONV_DIM
412
+ )
413
+ weight = tensor_cache(CONV_DIM * CONV_KERNEL, torch.bfloat16).view(
414
+ CONV_DIM, CONV_KERNEL
415
+ )
416
+ bias = tensor_cache(CONV_DIM, torch.bfloat16)
417
+ return x, weight, bias
418
+
419
+
420
+@pytest.mark.parametrize("total_tokens, split", TWO_CALL_SPLITS)
421
+@torch.inference_mode()
422
+def test_causal_conv1d_torch_two_call_split(total_tokens: int, split: int) -> None:
423
+ """Non-AMX conv-state handoff: a two-call split (the second seeded via
424
+ ``has_initial_state=True`` from the conv_states the first wrote back) must
425
+ match the single-call result.
426
+ """
427
+ from vllm.model_executor.layers.mamba.ops.cpu.causal_conv1d import (
428
+ causal_conv1d_torch,
429
+ )
430
+
431
+ x, weight, bias = _conv_inputs(total_tokens)
432
+ state_len = CONV_KERNEL - 1
433
+ # [num_slots, conv_dim, state_len]; slot 0 used here.
434
+ conv_states_full = torch.zeros(1, CONV_DIM, state_len, dtype=x.dtype)
435
+ conv_states_split = torch.zeros(1, CONV_DIM, state_len, dtype=x.dtype)
436
+
437
+ # x is [conv_dim, T] for causal_conv1d_torch.
438
+ xt = x.transpose(0, 1).contiguous()
439
+
440
+ out_full = causal_conv1d_torch(
441
+ x=xt,
442
+ weight=weight,
443
+ bias=bias,
444
+ conv_states=conv_states_full,
445
+ query_start_loc=torch.tensor([0, total_tokens], dtype=torch.int32),
446
+ cache_indices=torch.tensor([0], dtype=torch.int32),
447
+ has_initial_state=torch.tensor([False]),
448
+ activation="silu",
449
+ )
450
+
451
+ out1 = causal_conv1d_torch(
452
+ x=xt[:, :split],
453
+ weight=weight,
454
+ bias=bias,
455
+ conv_states=conv_states_split,
456
+ query_start_loc=torch.tensor([0, split], dtype=torch.int32),
457
+ cache_indices=torch.tensor([0], dtype=torch.int32),
458
+ has_initial_state=torch.tensor([False]),
459
+ activation="silu",
460
+ )
461
+ out2 = causal_conv1d_torch(
462
+ x=xt[:, split:],
463
+ weight=weight,
464
+ bias=bias,
465
+ conv_states=conv_states_split,
466
+ query_start_loc=torch.tensor([0, total_tokens - split], dtype=torch.int32),
467
+ cache_indices=torch.tensor([0], dtype=torch.int32),
468
+ has_initial_state=torch.tensor([True]),
469
+ activation="silu",
470
+ )
471
+ out_split = torch.cat([out1, out2], dim=1)
472
+
473
+ torch.testing.assert_close(out_split, out_full, atol=1e-2, rtol=1e-2)
474
+
475
+
476
+@pytest.mark.skipif(
477
+ not torch.cpu._is_amx_tile_supported(),
478
+ reason="causal_conv1d_fwd_cpu requires AMX/AVX512",
479
+)
480
+@pytest.mark.parametrize("total_tokens, split", TWO_CALL_SPLITS)
481
+@torch.inference_mode()
482
+def test_causal_conv1d_fwd_cpu_two_call_split(total_tokens: int, split: int) -> None:
483
+ """AMX prefill conv op must honor ``has_initial_state`` so a two-call split
484
+ matches the single-call result.
485
+
486
+ Regression test for ``causal_conv1d_fwd_varlen_kernel_impl`` (``conv.cpp``)
487
+ ignoring the carried conv state on continued chunks.
488
+ """
489
+ state_len = CONV_KERNEL - 1
490
+ x, weight, bias = _conv_inputs(total_tokens)
491
+
492
+ def amx(x_seg, conv_states, has_init):
493
+ seq = x_seg.shape[0]
494
+ return ops.causal_conv1d_fwd_cpu(
495
+ x=x_seg.transpose(0, 1), # [dim, seq]; stride(-2)==1 (view of [seq,dim])
496
+ weight=weight,
497
+ bias=bias,
498
+ conv_states=conv_states,
499
+ query_start_loc=torch.tensor([0, seq], dtype=torch.int32),
500
+ cache_indices=torch.tensor([0], dtype=torch.int32),
501
+ has_initial_state=torch.tensor([has_init]),
502
+ silu_activation=True,
503
+ is_vnni=False,
504
+ ).contiguous()
505
+
506
+ # conv_state layout passed by the AMX branch: [num_slots, dim, state_len].
507
+ cs_full = torch.zeros(1, CONV_DIM, state_len, dtype=x.dtype)
508
+ out_full = amx(x, cs_full, False)
509
+
510
+ cs_split = torch.zeros(1, CONV_DIM, state_len, dtype=x.dtype)
511
+ out1 = amx(x[:split], cs_split, False)
512
+ out2 = amx(x[split:], cs_split, True)
513
+ out_split = torch.cat([out1, out2], dim=1)
514
+
515
+ torch.testing.assert_close(out_split, out_full, atol=1e-2, rtol=1e-2)
516
+
517
+
518
+@torch.inference_mode()
519
+def test_batch_memcpy_cpu_fallback() -> None:
520
+ """The ctypes batch_memcpy fallback (used when triton-cpu is absent) must
521
+ copy each src into its dst, validating the (src_ptrs, dst_ptrs, sizes)
522
+ argument order against ctypes.memmove(dst, src, size).
523
+ """
524
+ from vllm.utils.cpu_triton_utils import batch_memcpy_kernel
525
+
526
+ # Varied byte sizes, including a non-power-of-two run.
527
+ sizes_bytes = [256, 1024, 17 * 4, 4096]
528
+ srcs = [torch.rand(n // 4, dtype=torch.float32) for n in sizes_bytes]
529
+ dsts = [torch.zeros_like(s) for s in srcs]
530
+
531
+ src_ptrs = torch.tensor([s.data_ptr() for s in srcs], dtype=torch.uint64)
532
+ dst_ptrs = torch.tensor([d.data_ptr() for d in dsts], dtype=torch.uint64)
533
+ sizes = torch.tensor(sizes_bytes, dtype=torch.int32)
534
+
535
+ batch_memcpy_kernel[(len(srcs),)](src_ptrs, dst_ptrs, sizes, BLOCK_SIZE=1024)
536
+
537
+ for src, dst in zip(srcs, dsts):
538
+ torch.testing.assert_close(dst, src)
tests/v1/e2e/test_cpu_linear_attn_chunked_prefix.py
new
+112
@@ -0,0 +1,112 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""CPU chunked-prefill / prefix-caching correctness for linear-attention models."""
4
+
5
+import os
6
+
7
+import pytest
8
+
9
+from tests.models.utils import check_logprobs_close
10
+from vllm import LLM, SamplingParams
11
+from vllm.platforms import current_platform
12
+
13
+if not current_platform.is_cpu():
14
+ pytest.skip("skipping CPU-only tests", allow_module_level=True)
15
+
16
+# Bound the KV cache so the run does not scale with host memory; these engines
17
+# only need a few thousand tokens.
18
+os.environ.setdefault("VLLM_CPU_KVCACHE_SPACE", "1")
19
+
20
+MODEL = "Qwen/Qwen3.5-0.8B"
21
+CHUNK_TOKENS = 128 # max_num_batched_tokens for the chunked engine
22
+NUM_LOGPROBS = 5
23
+SP = SamplingParams(max_tokens=32, temperature=0, logprobs=NUM_LOGPROBS)
24
+
25
+
26
+def _long_prompt(repeat: int) -> str:
27
+ return "Solve the following arithmetic step by step. " * repeat + "What is 7*8?"
28
+
29
+
30
+# Prompts long enough to span several CHUNK_TOKENS-sized chunks; a single-chunk
31
+# prompt is bit-identical to full prefill regardless of the bug.
32
+PROMPTS = [_long_prompt(r) for r in (40, 60, 80)]
33
+# Spans several full cache blocks; prefix caching only reuses complete blocks.
34
+PREFIX_PROMPT = "You are a helpful assistant. " * 230 + " Now answer: what is 2+2?"
35
+
36
+
37
+def _make_llm(**overrides) -> LLM:
38
+ base = dict(
39
+ model=MODEL,
40
+ dtype="bfloat16",
41
+ max_model_len=2048,
42
+ enforce_eager=True,
43
+ trust_remote_code=True,
44
+ )
45
+ base.update(overrides)
46
+ return LLM(**base)
47
+
48
+
49
+def _tuples(outputs) -> list[tuple[list[int], str, object]]:
50
+ """(token_ids, text, sample_logprobs) per request, for check_logprobs_close."""
51
+ return [
52
+ (list(o.outputs[0].token_ids), o.outputs[0].text, o.outputs[0].logprobs)
53
+ for o in outputs
54
+ ]
55
+
56
+
57
+@pytest.fixture(scope="module")
58
+def full_prefill_refs():
59
+ """Reference (ids, text, logprobs) for PROMPTS and PREFIX_PROMPT, full prefill."""
60
+ llm = _make_llm(enable_chunked_prefill=False, enable_prefix_caching=False)
61
+ refs = _tuples(llm.generate(PROMPTS, SP))
62
+ prefix_ref = _tuples(llm.generate([PREFIX_PROMPT], SP))[0]
63
+ del llm
64
+ return refs, prefix_ref
65
+
66
+
67
+def test_chunked_prefill_matches_full_prefill(full_prefill_refs):
68
+ """Batched multi-chunk prefill must stay close to per-prompt full prefill.
69
+
70
+ Prompts are scheduled together so the scheduler interleaves prefill chunks
71
+ across requests (the cross-request path where the accuracy gap was strongest).
72
+ """
73
+ refs, _ = full_prefill_refs
74
+ llm = _make_llm(
75
+ enable_chunked_prefill=True,
76
+ max_num_batched_tokens=CHUNK_TOKENS,
77
+ enable_prefix_caching=False,
78
+ )
79
+ got = _tuples(llm.generate(PROMPTS, SP))
80
+ del llm
81
+
82
+ check_logprobs_close(
83
+ outputs_0_lst=refs,
84
+ outputs_1_lst=got,
85
+ name_0="full_prefill",
86
+ name_1="chunked_prefill",
87
+ )
88
+
89
+
90
+def test_prefix_cache_hit_matches_cold_cache(full_prefill_refs):
91
+ """A prefix-cache hit must stay close to the cold-cache (reference) output.
92
+
93
+ The warm run continues prefill from the restored GDN state; the
94
+ num_cached_tokens check guards against a vacuous (no-hit) pass.
95
+ """
96
+ _, ref = full_prefill_refs
97
+ llm = _make_llm(enable_prefix_caching=True)
98
+ llm.generate([PREFIX_PROMPT], SP) # prime the cache
99
+ warm_out = llm.generate([PREFIX_PROMPT], SP)[0]
100
+ warm = _tuples([warm_out])[0]
101
+ del llm
102
+
103
+ assert warm_out.num_cached_tokens > 0, (
104
+ "expected a prefix-cache hit but num_cached_tokens=0; "
105
+ "PREFIX_PROMPT may be shorter than one cache block"
106
+ )
107
+ check_logprobs_close(
108
+ outputs_0_lst=[ref],
109
+ outputs_1_lst=[warm],
110
+ name_0="cold_cache",
111
+ name_1="warm_cache",
112
+ )
vllm/platforms/cpu.py
-14
@@ -157,20 +157,6 @@ class CpuPlatform(Platform):
157
logger.warning_once("Dual-Batch Overlap is not supported on CPU, disabled.")
158
parallel_config.enable_dbo = False
159
160
- if torch.cpu._is_amx_tile_supported() and (
161
- model_config is not None
162
- and model_config.get_num_layers_by_block_type(
163
- parallel_config, "linear_attention"
164
- )
165
- > 0
166
- ):
167
- cache_config.enable_prefix_caching = False
168
- scheduler_config.enable_chunked_prefill = False
169
- logger.warning_once(
170
- "Disabled unsupported prefix caching and chunked prefill "
171
- "for linear attention on AMX CPU platforms."
172
- )
173
-
160
# Note: workaround for v1 gpu_model_runner
161
from vllm.config import CompilationMode
162
vllm/utils/cpu_triton_utils.py
+10
@@ -5,6 +5,7 @@
5
Contains replacement functions to fallback Triton usages in CPU backend
6
"""
7
8
+import ctypes
9
from collections.abc import Callable
10
11
import torch
@@ -336,3 +337,12 @@ rejection_greedy_sample_kernel = _FuncWrapper(_rejection_greedy_sample_kernel_im
337
rejection_random_sample_kernel = _FuncWrapper(_rejection_random_sample_kernel_impl)
338
expand_kernel = _FuncWrapper(_expand_kernel_impl)
339
sample_recovered_tokens_kernel = _FuncWrapper(_sample_recovered_tokens_kernel_impl)
340
+
341
+
342
+def _batch_memcpy_impl(src_ptrs, dst_ptrs, sizes, BLOCK_SIZE=None):
343
+ # BLOCK_SIZE is unused; kept for signature parity with the Triton kernel.
344
+ for src, dst, size in zip(src_ptrs.tolist(), dst_ptrs.tolist(), sizes.tolist()):
345
+ ctypes.memmove(dst, src, size)
346
+
347
+
348
+batch_memcpy_kernel = _FuncWrapper(_batch_memcpy_impl)
vllm/v1/worker/cpu_model_runner.py
+24
-4
@@ -12,7 +12,7 @@ from vllm.logger import init_logger
12
from vllm.model_executor.model_loader import get_model
13
from vllm.tracing import instrument
14
from vllm.v1.core.sched.output import SchedulerOutput
15
-from vllm.v1.kv_cache_interface import KVCacheConfig
15
+from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig
16
from vllm.v1.utils import CpuGpuBuffer
17
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
18
@@ -103,6 +103,10 @@ class CPUModelRunner(GPUModelRunner):
103
cpu_tl.sample_recovered_tokens_kernel
104
)
105
106
+ import vllm.v1.worker.mamba_utils
107
+
108
+ vllm.v1.worker.mamba_utils.batch_memcpy_kernel = cpu_tl.batch_memcpy_kernel
109
+
110
@instrument(span_name="Loading (CPU)")
111
def load_model(self, load_dummy_weights: bool = False) -> None:
112
if load_dummy_weights:
@@ -153,9 +157,25 @@ class CPUModelRunner(GPUModelRunner):
157
pass
158
159
def _zero_block_ids(self, block_ids: list[int]) -> None:
156
- # CPU attention assigns -INF to logits at invalid positions,
157
- # so stale KV cache data never affects computation.
158
- pass
160
+ # Zero full-attention blocks to prevent stale data corruption on partial writes.
161
+ # Encoder-only (runner-only) layers are not FullAttentionSpec, so the
162
+ # spec filter below already excludes them; no runner-only skip needed.
163
+ seen_ptrs: set[int] = set()
164
+ for group in self.kv_cache_config.kv_cache_groups:
165
+ if not isinstance(group.kv_cache_spec, FullAttentionSpec):
166
+ continue
167
+ for layer_name in group.layer_names:
168
+ ctx = self.compilation_config.static_forward_context.get(layer_name)
169
+ if ctx is None:
170
+ continue
171
+ kv = ctx.kv_cache
172
+ if not isinstance(kv, torch.Tensor):
173
+ continue
174
+ if kv.data_ptr() in seen_ptrs:
175
+ continue
176
+ seen_ptrs.add(kv.data_ptr())
177
+ for block_id in block_ids:
178
+ kv[block_id].zero_()
179
180
# =========================================================================
181
# CPU-safe overrides for speculative decoding methods