Skip to content

Commit e68fa1b

Browse files
authored
[Core] Account for num_gpu_blocks_override in max_model_len checks (vllm-project#41069)
Signed-off-by: Nick Hill <nickhill123@gmail.com>
1 parent f05f366 commit e68fa1b

5 files changed

Lines changed: 128 additions & 60 deletions

File tree

tests/compile/h100/test_startup.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ def _run_vllm(vllm_runner):
3434
mode=CompilationMode.VLLM_COMPILE,
3535
cudagraph_mode=CUDAGraphMode.NONE,
3636
),
37-
num_gpu_blocks_override=8,
37+
# Phi-tiny-MoE uses SWA, whose admission cap is `cdiv(L, block_size) + 1`
38+
# at default block_size=16 — i.e. 17 blocks for max_model_len=256. Use
39+
# 32 for headroom.
40+
num_gpu_blocks_override=32,
3841
):
3942
pass
4043

@@ -190,7 +193,7 @@ def _run_model(vllm_runner, spec: ModelStartupSpec):
190193
cudagraph_mode=CUDAGraphMode.NONE,
191194
pass_config=PassConfig(fuse_allreduce_rms=False),
192195
),
193-
num_gpu_blocks_override=8,
196+
num_gpu_blocks_override=16,
194197
):
195198
pass
196199

tests/v1/core/test_kv_cache_utils.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2074,6 +2074,54 @@ def test_auto_fit_max_model_len_not_triggered():
20742074
assert vllm_config.model_config.max_model_len == 16
20752075

20762076

2077+
def test_auto_fit_max_model_len_respects_num_gpu_blocks_override():
2078+
"""Auto-fit must size max_model_len against the override-clamped pool, not
2079+
the raw `available_memory`. Without this, auto-fit could pick a
2080+
max_model_len that no longer fits once `num_gpu_blocks_override` is applied.
2081+
"""
2082+
model_config = ModelConfig(max_model_len=16384)
2083+
model_config.original_max_model_len = -1 # request auto-fit
2084+
vllm_config = VllmConfig(model_config=model_config)
2085+
# Cap the cache to 32 blocks regardless of available memory.
2086+
vllm_config.cache_config.num_gpu_blocks_override = 32
2087+
2088+
mem_per_block_per_layer = 16 * 2 * 64 * 4 * 2
2089+
kv_cache_specs = {
2090+
"layer_1": new_kv_cache_spec(), # block_size=16
2091+
"layer_2": new_kv_cache_spec(),
2092+
}
2093+
# Plenty of raw memory (1024 blocks per layer would fit max_model_len=16384).
2094+
large_available_memory = mem_per_block_per_layer * 2 * 1024
2095+
2096+
get_kv_cache_configs(vllm_config, [kv_cache_specs], [large_available_memory])
2097+
2098+
# 32 blocks * block_size 16 = 512 token slots, so max_model_len must
2099+
# auto-fit at or below that.
2100+
assert 0 < vllm_config.model_config.max_model_len <= 32 * 16
2101+
2102+
2103+
def test_check_enough_kv_cache_memory_respects_num_gpu_blocks_override():
2104+
"""Admission check must use the override-clamped pool size, not raw
2105+
`available_memory`. Without this, startup could accept a max_model_len
2106+
that does not actually fit in `num_gpu_blocks_override` blocks.
2107+
"""
2108+
model_config = ModelConfig(max_model_len=16384)
2109+
vllm_config = VllmConfig(model_config=model_config)
2110+
# 32 blocks is far too small for max_model_len=16384 (would need 1024).
2111+
vllm_config.cache_config.num_gpu_blocks_override = 32
2112+
2113+
mem_per_block_per_layer = 16 * 2 * 64 * 4 * 2
2114+
kv_cache_specs = {
2115+
"layer_1": new_kv_cache_spec(),
2116+
"layer_2": new_kv_cache_spec(),
2117+
}
2118+
# Plenty of raw memory: a bytes-only check against this would pass.
2119+
large_available_memory = mem_per_block_per_layer * 2 * 1024
2120+
2121+
with pytest.raises(ValueError, match="max seq len"):
2122+
get_kv_cache_configs(vllm_config, [kv_cache_specs], [large_available_memory])
2123+
2124+
20772125
def test_unify_hybrid_kv_cache_specs():
20782126
# 1. has_full_attention and has_sliding_window
20792127
before_spec_1 = new_kv_cache_spec()

tests/v1/e2e/general/test_async_scheduling.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,10 +324,13 @@ def run_test(
324324
):
325325
spec_decoding = spec_config is not None
326326
cache_arg: dict[str, Any] = (
327-
# Force preemptions
328-
dict(num_gpu_blocks_override=32)
327+
# Force preemptions: with 32 blocks the cache holds at most a single
328+
# max-length request, so the ~34 concurrent prompts contend and trigger
329+
# preemption. (Prompts here are << max_model_len, so dropping
330+
# max_model_len from 4096 to 512 doesn't change generation behavior.)
331+
dict(num_gpu_blocks_override=32, max_model_len=512)
329332
if test_preemption
330-
else dict(gpu_memory_utilization=0.9)
333+
else dict(gpu_memory_utilization=0.9, max_model_len=4096)
331334
)
332335
spec_mml = (spec_config or {}).get("max_model_len")
333336
spec_method = (spec_config or {}).get("method", "none")
@@ -343,7 +346,6 @@ def run_test(
343346

344347
with VllmRunner(
345348
model,
346-
max_model_len=4096,
347349
enable_chunked_prefill=test_prefill_chunking,
348350
# Force prefill chunking
349351
max_num_batched_tokens=48 if test_prefill_chunking else None,

vllm/v1/core/kv_cache_utils.py

Lines changed: 68 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -890,31 +890,48 @@ def get_max_concurrency_for_kv_cache_config(
890890
return max_concurrency
891891

892892

893-
def may_override_num_blocks(
894-
vllm_config: VllmConfig, num_blocks: int, suppress_log: bool = False
895-
) -> int:
893+
def may_override_num_blocks(vllm_config: VllmConfig, num_blocks: int) -> int:
896894
"""
897895
Override the number of kv cache blocks if `num_gpu_blocks_override` is set.
896+
The override is logged once, at the call site in `get_kv_cache_configs`.
898897
"""
899898
if vllm_config.cache_config.num_gpu_blocks_override is not None:
900-
num_gpu_blocks_override = vllm_config.cache_config.num_gpu_blocks_override
901-
if not suppress_log:
902-
logger.info(
903-
"Overriding num_gpu_blocks=%d with num_gpu_blocks_override=%d",
904-
num_blocks,
905-
num_gpu_blocks_override,
906-
)
907-
num_blocks = num_gpu_blocks_override
908-
899+
num_blocks = vllm_config.cache_config.num_gpu_blocks_override
909900
return num_blocks
910901

911902

903+
def _pool_bytes_per_block(kv_cache_groups: list[KVCacheGroupSpec]) -> int:
904+
"""
905+
Bytes consumed by one block in the worker's shared KV cache pool, mirroring
906+
the divisor used by `get_kv_cache_config_from_groups` to convert
907+
`available_memory` into `num_blocks`. Used to compute the effective KV cache
908+
capacity once `num_gpu_blocks_override` is applied.
909+
"""
910+
if len(kv_cache_groups) == 1 and isinstance(
911+
kv_cache_groups[0].kv_cache_spec, UniformTypeKVCacheSpecs
912+
):
913+
return kv_cache_groups[0].kv_cache_spec.page_size_bytes
914+
if all(
915+
isinstance(g.kv_cache_spec, UniformTypeKVCacheSpecs) for g in kv_cache_groups
916+
):
917+
# DeepseekV4: shared layout sized by the largest per-page-size bucket.
918+
full_mla_spec = cast(UniformTypeKVCacheSpecs, kv_cache_groups[0].kv_cache_spec)
919+
layer_tuple_page_bytes = sum(full_mla_spec.get_page_sizes())
920+
num_layer_tuples = max(
921+
cast(UniformTypeKVCacheSpecs, g.kv_cache_spec).get_num_layer_tuples()
922+
for g in kv_cache_groups
923+
)
924+
return layer_tuple_page_bytes * num_layer_tuples
925+
group_size = max(len(g.layer_names) for g in kv_cache_groups)
926+
page_size = get_uniform_page_size([g.kv_cache_spec for g in kv_cache_groups])
927+
return page_size * group_size
928+
929+
912930
def get_num_blocks(
913931
vllm_config: VllmConfig,
914932
num_layers: int,
915933
available_memory: int,
916934
page_size: int,
917-
suppress_log: bool = False,
918935
) -> int:
919936
"""
920937
Get the number of kv cache blocks.
@@ -924,15 +941,10 @@ def get_num_blocks(
924941
num_layers: The number of layers
925942
available_memory: Memory available for KV cache in bytes.
926943
page_size: The page size of the KV cache.
927-
suppress_log: Whether to suppress override log messages. Used when creating a
928-
temporary/dummy KV cache config, e.g. during CG memory profiling
929944
"""
930945
num_blocks = int(available_memory // page_size // num_layers)
931946
num_blocks = max(num_blocks, 0)
932-
num_blocks = may_override_num_blocks(
933-
vllm_config, num_blocks, suppress_log=suppress_log
934-
)
935-
return num_blocks
947+
return may_override_num_blocks(vllm_config, num_blocks)
936948

937949

938950
def get_uniform_page_size(kv_cache_specs: Iterable[KVCacheSpec]) -> int:
@@ -1220,7 +1232,6 @@ def get_kv_cache_config_from_groups(
12201232
vllm_config: VllmConfig,
12211233
kv_cache_groups: list[KVCacheGroupSpec],
12221234
available_memory: int,
1223-
suppress_log: bool = False,
12241235
) -> KVCacheConfig:
12251236
"""
12261237
Generate the KV cache configuration from the KV cache groups and spec
@@ -1252,9 +1263,7 @@ def get_kv_cache_config_from_groups(
12521263
num_blocks = (
12531264
available_memory // kv_cache_groups[0].kv_cache_spec.page_size_bytes
12541265
)
1255-
num_blocks = may_override_num_blocks(
1256-
vllm_config, num_blocks, suppress_log=suppress_log
1257-
)
1266+
num_blocks = may_override_num_blocks(vllm_config, num_blocks)
12581267
per_layer_specs = kv_cache_groups[0].kv_cache_spec.kv_cache_specs
12591268
kv_cache_tensors = [
12601269
KVCacheTensor(
@@ -1288,11 +1297,7 @@ def get_kv_cache_config_from_groups(
12881297
)
12891298
assert group_size > 0, "group_size must be greater than 0"
12901299
num_blocks = get_num_blocks(
1291-
vllm_config,
1292-
group_size,
1293-
available_memory,
1294-
page_size,
1295-
suppress_log=suppress_log,
1300+
vllm_config, group_size, available_memory, page_size
12961301
)
12971302
kv_cache_tensors = []
12981303
for i in range(group_size):
@@ -1688,36 +1693,24 @@ def _report_kv_cache_config(
16881693
vllm_config: The global VllmConfig
16891694
kv_cache_config: The resolved KV cache configuration
16901695
"""
1691-
min_block_size = min(
1692-
[group.kv_cache_spec.block_size for group in kv_cache_config.kv_cache_groups]
1693-
)
1694-
1695-
# Log the KV cache size and maximum concurrency.
1696-
num_tokens = (
1697-
kv_cache_config.num_blocks
1698-
// len(kv_cache_config.kv_cache_groups)
1699-
* min_block_size
1700-
)
1701-
dcp_size = vllm_config.parallel_config.decode_context_parallel_size
1702-
pcp_size = vllm_config.parallel_config.prefill_context_parallel_size
1703-
if pcp_size * dcp_size > 1:
1704-
num_tokens *= pcp_size * dcp_size
1705-
logger.info(
1706-
"Multiplying the GPU KV cache size by the cp_world_size %d "
1707-
"(pcp_world_size %d * dcp_world_size %d).",
1708-
pcp_size * dcp_size,
1709-
pcp_size,
1710-
dcp_size,
1711-
)
1712-
num_tokens_str = f"{num_tokens:,}"
1713-
logger.info_once("GPU KV cache size: %s tokens", num_tokens_str)
1714-
max_model_len_str = f"{vllm_config.model_config.max_model_len:,}"
1696+
max_model_len = vllm_config.model_config.max_model_len
17151697
max_concurrency = get_max_concurrency_for_kv_cache_config(
17161698
vllm_config, kv_cache_config
17171699
)
1700+
1701+
# GPU KV cache size in tokens = max_concurrency * max_model_len: the total
1702+
# tokens of context the pool can hold at peak utilization. Sourcing this
1703+
# from the concurrency calculation handles hybrid layouts correctly: SWA /
1704+
# chunked-local groups have a per-request block count that's capped by
1705+
# their window, so a naive `num_blocks // num_groups * block_size` formula
1706+
# underestimates capacity for these models. DCP/PCP sharding is already
1707+
# accounted for in each spec's `max_memory_usage_bytes`.
1708+
num_tokens = int(max_concurrency * max_model_len)
1709+
1710+
logger.info_once("GPU KV cache size: %s tokens", f"{num_tokens:,}")
17181711
logger.info_once(
17191712
"Maximum concurrency for %s tokens per request: %.2fx",
1720-
max_model_len_str,
1713+
f"{max_model_len:,}",
17211714
max_concurrency,
17221715
)
17231716

@@ -1988,6 +1981,28 @@ def get_kv_cache_configs(
19881981
for worker_spec in kv_cache_specs
19891982
]
19901983

1984+
# If `num_gpu_blocks_override` is set, the cache size that will actually
1985+
# be allocated is decoupled from the profiled `available_memory`:
1986+
# `may_override_num_blocks` in `get_kv_cache_config_from_groups` clamps
1987+
# `num_blocks` to the override. Reflect that in `available_memory` here so
1988+
# auto-fit, the admission check, and the per-worker config builder all
1989+
# plan against the same effective capacity.
1990+
override = vllm_config.cache_config.num_gpu_blocks_override
1991+
if override is not None:
1992+
adjusted_memory: list[int] = []
1993+
for groups, avail_mem in zip(projected_groups_per_worker, available_memory):
1994+
if not groups:
1995+
adjusted_memory.append(avail_mem)
1996+
continue
1997+
bytes_per_block = _pool_bytes_per_block(groups)
1998+
logger.info(
1999+
"Overriding num_gpu_blocks=%d with num_gpu_blocks_override=%d",
2000+
avail_mem // bytes_per_block,
2001+
override,
2002+
)
2003+
adjusted_memory.append(override * bytes_per_block)
2004+
available_memory = adjusted_memory
2005+
19912006
if vllm_config.model_config.original_max_model_len == -1:
19922007
_auto_fit_max_model_len(
19932008
vllm_config, projected_groups_per_worker, available_memory

vllm/v1/worker/gpu_model_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5874,7 +5874,7 @@ def _init_minimal_kv_cache_for_profiling(self) -> None:
58745874
saved_override = self.cache_config.num_gpu_blocks_override
58755875
self.cache_config.num_gpu_blocks_override = min_blocks
58765876
minimal_config = get_kv_cache_config_from_groups(
5877-
self.vllm_config, kv_cache_groups, available_memory=0, suppress_log=True
5877+
self.vllm_config, kv_cache_groups, available_memory=0
58785878
)
58795879
self.cache_config.num_gpu_blocks_override = saved_override
58805880

0 commit comments

Comments
 (0)