[Bugfix][Qwen4Exp] Release the profiling KV cache held by QSA key views (#58961)

Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Gabriel Wu
2026-09-28 08:11:46 +00:00
committed by GitHub
co-authored by Claude Opus 5.5 mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent 66d2d4b2e7
commit a2be4d3cc7
2 changed files with 34 additions and 10 deletions
+22 -1
View File
@@ -17,6 +17,7 @@ from vllm.models.qwen4_exp.nvidia.ops import qsa as qsa_ops
from vllm.models.qwen4_exp.nvidia.ops import qsa_indexer as qsa_indexer_ops
from vllm.platforms import current_platform
from vllm.triton_utils import HAS_TRITON
from vllm.v1.worker.utils import clear_layer_kv_caches
requires_qsa_kernels = pytest.mark.skipif(
not current_platform.is_cuda() or not HAS_TRITON,
@@ -365,7 +366,9 @@ def test_qsa_circular_buffer_survives_one_speculative_step(chunk_start: int) ->
assert set(slots.tolist()).isdisjoint((committed % capacity).tolist())
def _qsa_key_cache(block_size: int, compress_ratio: int) -> qsa_cache.QSAKeyStateCache:
def _qsa_key_cache(
block_size: int, compress_ratio: int, **kwargs
) -> qsa_cache.QSAKeyStateCache:
return qsa_cache.QSAKeyStateCache(
head_size=64,
dtype=torch.bfloat16,
@@ -375,6 +378,7 @@ def _qsa_key_cache(block_size: int, compress_ratio: int) -> qsa_cache.QSAKeyStat
compilation_config=SimpleNamespace(static_forward_context={})
),
compress_ratio=compress_ratio,
**kwargs,
)
@@ -402,6 +406,23 @@ def test_qsa_state_caches_adapt_the_unified_logical_layout() -> None:
assert compressed_cache.kv_cache.data_ptr() == compressed_view.data_ptr()
def test_clearing_qsa_key_cache_releases_its_storage() -> None:
"""Profiling teardown clears `kv_cache`; no derived view may keep it alive."""
raw_cache = _qsa_key_cache(
block_size=32, compress_ratio=4, cache_rope_positions=True
)
kv = torch.zeros(2, 1, 8, raw_cache.head_size, dtype=torch.bfloat16)
references_before_bind = torch._C._storage_Use_Count(kv.untyped_storage()._cdata)
raw_cache.bind_kv_cache(kv)
assert raw_cache.key_cache.shape[-1] == 64
assert raw_cache.rope_position_cache.dtype == torch.int64
clear_layer_kv_caches([raw_cache])
references = torch._C._storage_Use_Count(kv.untyped_storage()._cdata)
assert references == references_before_bind
@pytest.mark.parametrize(
("compress_ratio", "num_spec", "expected"),
[(4, 0, 4), (4, 1, 8), (4, 3, 8), (4, 4, 8), (4, 5, 12), (2, 3, 6)],
+12 -9
View File
@@ -825,15 +825,18 @@ class QSAKeyStateCache(_QSAStateCache):
)
super().__init__(head_size=storage_head_size, **kwargs)
def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
super().bind_kv_cache(kv_cache)
qsa_cache = self.kv_cache
self.key_cache = qsa_cache[..., : self.key_head_size]
if self.cache_rope_positions:
position_tail = qsa_cache[..., self.rope_position_offset :]
self.rope_position_cache = position_tail.view(torch.int64)
else:
self.rope_position_cache = None
# Derived on access so `kv_cache` stays the only reference to the bound
# storage: clearing it (e.g. after CUDA graph memory profiling) must free
# the cache, or the freed block stays pinned in the allocator.
@property
def key_cache(self) -> torch.Tensor:
return self.kv_cache[..., : self.key_head_size]
@property
def rope_position_cache(self) -> torch.Tensor | None:
if not self.cache_rope_positions:
return None
return self.kv_cache[..., self.rope_position_offset :].view(torch.int64)
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
# Hold the open group's committed keys plus every row a speculative