[Bugfix][Core] Reserve the KV null block when validating max_model_len (#47272)
Signed-off-by: hyungjun oh <92hyungjun@gmail.com> Signed-off-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Nick Hill <nickhill123@gmail.com>
This commit is contained in:
co-authored by
Claude
Nick Hill
parent
c0233fcf01
commit
76fb6d210a
@@ -27,6 +27,7 @@ from vllm.v1.core.kv_cache_utils import (
|
||||
BlockHash,
|
||||
FreeKVCacheBlockQueue,
|
||||
KVCacheBlock,
|
||||
check_enough_kv_cache_memory,
|
||||
estimate_max_model_len,
|
||||
generate_block_hash_extra_keys,
|
||||
generate_scheduler_kv_cache_config,
|
||||
@@ -2685,7 +2686,9 @@ def test_auto_fit_max_model_len_with_hybrid():
|
||||
"layer_2": new_kv_cache_spec(),
|
||||
}
|
||||
|
||||
available_memory = mem_per_block_per_layer * (1024 // 16 + 1 + gamma)
|
||||
# One extra block on top of what a 1024-token request needs: the pool
|
||||
# reserves one block as the null block.
|
||||
available_memory = mem_per_block_per_layer * (1024 // 16 + 1 + gamma + 1)
|
||||
_kv_cache_configs = get_kv_cache_configs(
|
||||
vllm_config, [kv_cache_specs], [available_memory]
|
||||
)
|
||||
@@ -3095,3 +3098,72 @@ def test_resolve_block_hashes_rejects_mismatched_view():
|
||||
mismatched = BlockHashListWithBlockSize(raw, 2, 8)
|
||||
with pytest.raises(AssertionError):
|
||||
resolve_block_hashes(mismatched, 2, 4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("use_override", [True, False])
|
||||
def test_kv_cache_reserves_null_block_for_max_model_len(use_override):
|
||||
"""A KV cache sized to exactly the blocks a max_model_len request needs must
|
||||
be rejected. BlockPool keeps one block as the null block, so only
|
||||
num_blocks - 1 are usable; accepting num_blocks == needed would leave the
|
||||
request unschedulable and hang the engine. Covers both the
|
||||
num_gpu_blocks_override path and the memory-derived block count.
|
||||
"""
|
||||
block_size = 16
|
||||
max_model_len = 512 # needs 512 / 16 = 32 blocks
|
||||
vllm_config = VllmConfig(model_config=ModelConfig(max_model_len=max_model_len))
|
||||
spec = new_kv_cache_spec(block_size=block_size)
|
||||
|
||||
# 32 blocks -> only 31 usable after the null block: one short -> reject.
|
||||
if use_override:
|
||||
# Ample raw memory; the override alone constrains the block count.
|
||||
vllm_config.cache_config.num_gpu_blocks_override = 32
|
||||
available_memory = [spec.page_size_bytes * 1024]
|
||||
else:
|
||||
available_memory = [spec.page_size_bytes * 32]
|
||||
|
||||
with pytest.raises(ValueError, match="max seq len"):
|
||||
get_kv_cache_configs(vllm_config, [{"layer1": spec}], available_memory)
|
||||
|
||||
|
||||
def test_auto_fit_max_model_len_reserves_null_block():
|
||||
"""Auto-fit (max_model_len=-1) must size max_model_len against usable
|
||||
blocks, not the total pool. With memory for exactly 64 blocks, one is the
|
||||
null block, so auto-fit must settle on 63 * block_size; picking the full
|
||||
pool would leave a max-length request unschedulable and hang the engine.
|
||||
"""
|
||||
block_size = 16
|
||||
model_config = ModelConfig(max_model_len=1024)
|
||||
model_config.original_max_model_len = -1
|
||||
vllm_config = VllmConfig(model_config=model_config)
|
||||
spec = new_kv_cache_spec(block_size=block_size)
|
||||
|
||||
# Exactly the 1024 / 16 = 64 blocks a full-length request would need.
|
||||
available_memory = [spec.page_size_bytes * 64]
|
||||
|
||||
get_kv_cache_configs(vllm_config, [{"layer1": spec}], available_memory)
|
||||
|
||||
assert vllm_config.model_config.max_model_len == 63 * block_size
|
||||
|
||||
|
||||
def test_check_enough_kv_cache_memory_reserves_null_block():
|
||||
"""The public admission check must reject a KV cache sized to exactly the
|
||||
blocks a max_model_len request needs. BlockPool keeps one block as the
|
||||
null block, so only num_blocks - 1 are usable; accepting
|
||||
num_blocks == needed would leave the request unschedulable and hang the
|
||||
engine.
|
||||
"""
|
||||
block_size = 16
|
||||
max_model_len = 512 # needs 512 / 16 = 32 blocks
|
||||
vllm_config = VllmConfig(model_config=ModelConfig(max_model_len=max_model_len))
|
||||
spec = new_kv_cache_spec(block_size=block_size)
|
||||
|
||||
# 32 blocks -> only 31 usable after the null block: one short -> reject.
|
||||
with pytest.raises(ValueError, match="max seq len"):
|
||||
check_enough_kv_cache_memory(
|
||||
vllm_config, {"layer1": spec}, spec.page_size_bytes * 32
|
||||
)
|
||||
|
||||
# 33 blocks -> 32 usable after the null block -> accept.
|
||||
check_enough_kv_cache_memory(
|
||||
vllm_config, {"layer1": spec}, spec.page_size_bytes * 33
|
||||
)
|
||||
|
||||
@@ -332,11 +332,12 @@ def run_test(
|
||||
):
|
||||
spec_decoding = spec_config is not None
|
||||
cache_arg: dict[str, Any] = (
|
||||
# Force preemptions: with 32 blocks the cache holds at most a single
|
||||
# max-length request, so the ~34 concurrent prompts contend and trigger
|
||||
# preemption. (Prompts here are << max_model_len, so dropping
|
||||
# max_model_len from 4096 to 512 doesn't change generation behavior.)
|
||||
dict(num_gpu_blocks_override=32, max_model_len=512)
|
||||
# Force preemptions: with 33 blocks (one is the reserved null block)
|
||||
# the cache holds at most a single max-length request, so the ~34
|
||||
# concurrent prompts contend and trigger preemption. (Prompts here are
|
||||
# << max_model_len, so dropping max_model_len from 4096 to 512 doesn't
|
||||
# change generation behavior.)
|
||||
dict(num_gpu_blocks_override=33, max_model_len=512)
|
||||
if test_preemption
|
||||
else dict(gpu_memory_utilization=0.9, max_model_len=4096)
|
||||
)
|
||||
|
||||
@@ -77,8 +77,12 @@ def test_auto_fit_max_model_len_rejects_oversized_input(
|
||||
|
||||
# Use a small KV cache budget to force auto-fit to a small
|
||||
# max_model_len. Pin block_size=16 so the budget is independent
|
||||
# of the platform's default block size.
|
||||
kv_cache_bytes = 1_000_000 # 1 MB
|
||||
# of the platform's default block size. One block for this model is
|
||||
# 2 (K/V) * 16 (block) * 12 (heads) * 64 (head_dim) * 2 (fp16)
|
||||
# * 12 (layers) = 589,824 bytes, and the pool must cover at least
|
||||
# two: one is reserved as the null block, so a 1 MB single-block
|
||||
# pool cannot serve any tokens at all.
|
||||
kv_cache_bytes = 2_000_000 # 2 MB = 3 blocks -> 2 usable
|
||||
|
||||
with vllm_runner(
|
||||
model_name=model,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.v1.core.kv_cache_utils import check_enough_kv_cache_memory
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec
|
||||
@@ -18,7 +19,7 @@ def test_kv_cache_oom_no_memory():
|
||||
block_size=16,
|
||||
num_kv_heads=8,
|
||||
head_size=128,
|
||||
dtype="float16",
|
||||
dtype=torch.float16,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ def test_kv_cache_oom_insufficient_memory(monkeypatch):
|
||||
block_size=16,
|
||||
num_kv_heads=8,
|
||||
head_size=128,
|
||||
dtype="float16",
|
||||
dtype=torch.float16,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1244,7 +1244,7 @@ def test_prompt_logprobs_with_chunking_and_preemption():
|
||||
max_model_len=512,
|
||||
enable_chunked_prefill=True,
|
||||
max_num_batched_tokens=48, # Force prefill chunking
|
||||
num_gpu_blocks_override=32, # Force preemptions
|
||||
num_gpu_blocks_override=33, # Force preemptions (32 usable + null block)
|
||||
disable_log_stats=False,
|
||||
gpu_memory_utilization=0.25,
|
||||
) as vllm_model:
|
||||
|
||||
@@ -892,8 +892,17 @@ def check_enough_kv_cache_memory(
|
||||
|
||||
# No need to check for available memory if the kv_cache_spec is empty
|
||||
if kv_cache_spec:
|
||||
# Reserve the null block BlockPool permanently holds back, so the check
|
||||
# plans against usable blocks, as in get_kv_cache_configs. Group a copy
|
||||
# of the specs since grouping may unify them in-place.
|
||||
groups = get_kv_cache_groups(vllm_config, dict(kv_cache_spec))
|
||||
check_memory = (
|
||||
available_memory - _pool_bytes_per_block(vllm_config, groups)
|
||||
if groups
|
||||
else available_memory
|
||||
)
|
||||
_check_enough_kv_cache_memory(
|
||||
available_memory,
|
||||
check_memory,
|
||||
lambda: max_memory_usage_bytes(vllm_config, kv_cache_spec.values()),
|
||||
vllm_config.model_config.max_model_len,
|
||||
lambda am: estimate_max_model_len(vllm_config, kv_cache_spec, am),
|
||||
@@ -2228,13 +2237,19 @@ def get_kv_cache_configs(
|
||||
adjusted_memory.append(override * bytes_per_block)
|
||||
available_memory = adjusted_memory
|
||||
|
||||
# Reserve the null block BlockPool permanently holds back, so auto-fit and
|
||||
# the capacity check both plan against usable blocks. Allocation below
|
||||
# still uses the full memory.
|
||||
check_memory = [
|
||||
avail_mem - _pool_bytes_per_block(vllm_config, groups) if groups else avail_mem
|
||||
for groups, avail_mem in zip(projected_groups_per_worker, available_memory)
|
||||
]
|
||||
|
||||
if vllm_config.model_config.original_max_model_len == -1:
|
||||
_auto_fit_max_model_len(
|
||||
vllm_config, projected_groups_per_worker, available_memory
|
||||
)
|
||||
_auto_fit_max_model_len(vllm_config, projected_groups_per_worker, check_memory)
|
||||
|
||||
# Check if the available memory is enough per worker.
|
||||
for groups, avail_mem in zip(projected_groups_per_worker, available_memory):
|
||||
for groups, avail_mem in zip(projected_groups_per_worker, check_memory):
|
||||
if not groups:
|
||||
continue
|
||||
_check_enough_kv_cache_memory(
|
||||
|
||||
Reference in New Issue
Block a user