A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 13, 23:15 UTC
[Hybrid] A simpler algorithm to find kernel_block_size (#26476)
Signed-off-by: Chen Zhang <zhangch99@outlook.com>
Chen Zhang committed
Oct 31, 2025 at 14:30 UTC
df334868ca5b7d8785121f8eaf52b1526e7766ac
3 files changed
+149
-85
tests/v1/worker/test_gpu_model_runner.py
+53
@@ -6,6 +6,7 @@ import pytest
6
import torch
7
8
from vllm.attention import Attention
9
+from vllm.attention.backends.abstract import MultipleOf
10
from vllm.config import (
11
CacheConfig,
12
ModelConfig,
@@ -34,6 +35,7 @@ from vllm.v1.kv_cache_interface import (
35
from vllm.v1.sample.metadata import SamplingMetadata
36
from vllm.v1.worker.gpu_input_batch import InputBatch
37
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
38
+from vllm.v1.worker.utils import AttentionGroup
39
40
BLOCK_SIZE = 16
41
NUM_BLOCKS = 10
@@ -181,6 +183,57 @@ def _is_req_state_block_table_match(model_runner, req_id: str) -> bool:
183
).all()
184
185
186
+def _make_mock_backend_for_kernel_block_size(
187
+ supported_sizes: list[int | MultipleOf],
188
+):
189
+ class _MockBackend:
190
+ @staticmethod
191
+ def get_supported_kernel_block_size():
192
+ return supported_sizes
193
+
194
+ return _MockBackend()
195
+
196
+
197
+def _make_kv_cache_spec() -> FullAttentionSpec:
198
+ return FullAttentionSpec(block_size=1, num_kv_heads=1, head_size=1, dtype="float16")
199
+
200
+
201
+def test_select_common_block_size_prefers_manager_block_size():
202
+ backend_a = _make_mock_backend_for_kernel_block_size([MultipleOf(32)])
203
+ backend_b = _make_mock_backend_for_kernel_block_size([64, MultipleOf(16)])
204
+ attn_groups = [
205
+ AttentionGroup(backend_a, [], [], _make_kv_cache_spec(), 0),
206
+ AttentionGroup(backend_b, [], [], _make_kv_cache_spec(), 0),
207
+ ]
208
+
209
+ selected_size = GPUModelRunner.select_common_block_size(128, attn_groups)
210
+ assert selected_size == 128
211
+
212
+
213
+def test_select_common_block_size_uses_largest_shared_int():
214
+ backend_a = _make_mock_backend_for_kernel_block_size([128, 64])
215
+ backend_b = _make_mock_backend_for_kernel_block_size([64, 32])
216
+ attn_groups = [
217
+ AttentionGroup(backend_a, [], [], _make_kv_cache_spec(), 0),
218
+ AttentionGroup(backend_b, [], [], _make_kv_cache_spec(), 0),
219
+ ]
220
+
221
+ selected_size = GPUModelRunner.select_common_block_size(256, attn_groups)
222
+ assert selected_size == 64
223
+
224
+
225
+def test_select_common_block_size_no_valid_option():
226
+ backend_a = _make_mock_backend_for_kernel_block_size([64])
227
+ backend_b = _make_mock_backend_for_kernel_block_size([MultipleOf(16)])
228
+ attn_groups = [
229
+ AttentionGroup(backend_a, [], [], _make_kv_cache_spec(), 0),
230
+ AttentionGroup(backend_b, [], [], _make_kv_cache_spec(), 0),
231
+ ]
232
+
233
+ with pytest.raises(ValueError):
234
+ GPUModelRunner.select_common_block_size(48, attn_groups)
235
+
236
+
237
def test_update_states_new_request(model_runner, dist_init):
238
req_id = "req_0"
239
vllm/v1/worker/gpu_model_runner.py
+91
-84
@@ -3978,6 +3978,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3978
3979
def create_attn_groups(
3980
attn_backends_map: dict[AttentionGroupKey, list[str]],
3981
+ kv_cache_group_id: int,
3982
) -> list[AttentionGroup]:
3983
attn_groups: list[AttentionGroup] = []
3984
for (attn_backend, kv_cache_spec), layer_names in attn_backends_map.items():
@@ -3987,6 +3988,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3988
kv_cache_spec,
3989
self.vllm_config,
3990
self.device,
3991
+ kv_cache_group_id,
3992
num_metadata_builders=1
3993
if not self.parallel_config.enable_dbo
3994
else 2,
@@ -4005,8 +4007,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4007
# Resolve cudagraph_mode before actually initialize metadata_builders
4008
self._check_and_update_cudagraph_mode(attention_backend_set)
4009
4008
- for attn_backends_map in attention_backend_maps:
4009
- self.attn_groups.append(create_attn_groups(attn_backends_map))
4010
+ for i, attn_backend_map in enumerate(attention_backend_maps):
4011
+ self.attn_groups.append(create_attn_groups(attn_backend_map, i))
4012
4013
# Calculate reorder batch threshold (if needed)
4014
self.calculate_reorder_batch_threshold()
@@ -4156,87 +4158,81 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4158
return
4159
self.reorder_batch_threshold = reduce(min_none_high, reorder_batch_thresholds)
4160
4159
- def _find_compatible_block_sizes(
4160
- self,
4161
- kv_manager_block_size: int,
4162
- backend_cls: type[AttentionBackend],
4163
- return_all: bool = False,
4164
- ) -> list[int]:
4165
- """
4166
- Find compatible block sizes for a backend.
4167
-
4168
- Args:
4169
- kv_manager_block_size: Physical block size of KV cache
4170
- backend_cls: Attention backend class
4171
- return_all: Return all compatible sizes if True, max size if False
4172
-
4173
- Returns:
4174
- Compatible block size(s) based on return_all parameter
4175
-
4176
- Raises:
4177
- ValueError: If no compatible block size found
4178
- """
4179
- supported_block_size = backend_cls.get_supported_kernel_block_size()
4180
- compatible_sizes = []
4181
-
4182
- for block_size in supported_block_size:
4183
- if isinstance(block_size, int):
4184
- if kv_manager_block_size % block_size == 0:
4185
- compatible_sizes.append(block_size)
4186
- elif (
4187
- isinstance(block_size, MultipleOf)
4188
- and kv_manager_block_size % block_size.base == 0
4189
- ):
4190
- compatible_sizes.append(kv_manager_block_size)
4191
-
4192
- if not compatible_sizes:
4193
- raise ValueError(f"No compatible block size for {kv_manager_block_size}")
4194
-
4195
- return compatible_sizes if return_all else [max(compatible_sizes)]
4196
-
4197
- def _select_common_block_size(
4198
- self, kv_manager_block_size: int, attn_groups: list[AttentionGroup]
4161
+ @staticmethod
4162
+ def select_common_block_size(
4163
+ kv_manager_block_size: int, attn_groups: list[AttentionGroup]
4164
) -> int:
4165
"""
4201
- Select common block size for all backends.
4166
+ Select a block size that is supported by all backends and is a factor of
4167
+ kv_manager_block_size.
4168
+
4169
+ If kv_manager_block_size is supported by all backends, return it directly.
4170
+ Otherwise, return the max supported size.
4171
4172
Args:
4173
kv_manager_block_size: Block size of KV cache
4174
attn_groups: List of attention groups
4175
4176
Returns:
4208
- Block size supported by all backends,
4209
- prioritizing cache_config.block_size
4177
+ The selected block size
4178
4179
Raises:
4212
- ValueError: If no common block size found
4180
+ ValueError: If no valid block size found
4181
"""
4214
- all_backend_supports = []
4182
4216
- for attn_group in attn_groups:
4217
- compatible_sizes = self._find_compatible_block_sizes(
4218
- kv_manager_block_size, attn_group.backend, return_all=True
4219
- )
4220
- supported_sizes = sorted(list(set(compatible_sizes)), reverse=True)
4221
- all_backend_supports.append(set(supported_sizes))
4222
-
4223
- common_supported_sizes = set.intersection(*all_backend_supports)
4224
-
4225
- if not common_supported_sizes:
4226
- error_msg = f"No common block size for {kv_manager_block_size}. "
4227
- for i, attn_group in enumerate(attn_groups):
4228
- supported = all_backend_supports[i]
4229
- error_msg += (
4230
- f"Backend {attn_group.backend} supports: {sorted(supported)}. "
4231
- )
4232
- raise ValueError(error_msg)
4233
-
4234
- if self.cache_config.block_size in common_supported_sizes:
4235
- return self.cache_config.block_size
4183
+ def block_size_is_supported(
4184
+ backends: list[type[AttentionBackend]], block_size: int
4185
+ ) -> bool:
4186
+ """
4187
+ Check if the block size is supported by all backends.
4188
+ """
4189
+ for backend in backends:
4190
+ is_supported = False
4191
+ for supported_size in backend.get_supported_kernel_block_size():
4192
+ if isinstance(supported_size, int):
4193
+ if block_size == supported_size:
4194
+ is_supported = True
4195
+ elif isinstance(supported_size, MultipleOf):
4196
+ if block_size % supported_size.base == 0:
4197
+ is_supported = True
4198
+ else:
4199
+ raise ValueError(f"Unknown supported size: {supported_size}")
4200
+ if not is_supported:
4201
+ return False
4202
+ return True
4203
+
4204
+ backends = [group.backend for group in attn_groups]
4205
+
4206
+ # Case 1: if the block_size of kv cache manager is supported by all backends,
4207
+ # return it directly
4208
+ if block_size_is_supported(backends, kv_manager_block_size):
4209
+ return kv_manager_block_size
4210
+
4211
+ # Case 2: otherwise, the block_size must be an `int`-format supported size of
4212
+ # at least one backend. Iterate over all `int`-format supported sizes in
4213
+ # descending order and return the first one that is supported by all backends.
4214
+ # Simple proof:
4215
+ # If the supported size b is in MultipleOf(x_i) format for all attention
4216
+ # backends i, and b a factor of kv_manager_block_size, then
4217
+ # kv_manager_block_size also satisfies MultipleOf(x_i) for all i. We will
4218
+ # return kv_manager_block_size in case 1.
4219
+ all_int_supported_sizes = set(
4220
+ supported_size
4221
+ for backend in backends
4222
+ for supported_size in backend.get_supported_kernel_block_size()
4223
+ if isinstance(supported_size, int)
4224
+ )
4225
4237
- return max(common_supported_sizes)
4226
+ for supported_size in sorted(all_int_supported_sizes, reverse=True):
4227
+ if kv_manager_block_size % supported_size != 0:
4228
+ continue
4229
+ if block_size_is_supported(backends, supported_size):
4230
+ return supported_size
4231
+ raise ValueError(f"No common block size for {kv_manager_block_size}. ")
4232
4239
- def may_reinitialize_input_batch(self, kv_cache_config: KVCacheConfig) -> None:
4233
+ def may_reinitialize_input_batch(
4234
+ self, kv_cache_config: KVCacheConfig, kernel_block_sizes: list[int]
4235
+ ) -> None:
4236
"""
4237
Re-initialize the input batch if the block sizes are different from
4238
`[self.cache_config.block_size]`. This usually happens when there
@@ -4244,6 +4240,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4240
4241
Args:
4242
kv_cache_config: The KV cache configuration.
4243
+ kernel_block_sizes: The kernel block sizes for each KV cache group.
4244
"""
4245
block_sizes = [
4246
kv_cache_group.kv_cache_spec.block_size
@@ -4251,9 +4248,6 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4248
if not isinstance(kv_cache_group.kv_cache_spec, EncoderOnlyAttentionSpec)
4249
]
4250
4254
- # Generate kernel_block_sizes that matches each block_size
4255
- kernel_block_sizes = self._prepare_kernel_block_sizes(kv_cache_config)
4256
-
4251
if block_sizes != [self.cache_config.block_size] or kernel_block_sizes != [
4252
self.cache_config.block_size
4253
]:
@@ -4354,7 +4348,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4348
# all backends in the group.
4349
attn_groups = self.attn_groups[kv_cache_group_id]
4350
kv_manager_block_size = kv_cache_group.kv_cache_spec.block_size
4357
- selected_kernel_size = self._select_common_block_size(
4351
+ selected_kernel_size = self.select_common_block_size(
4352
kv_manager_block_size, attn_groups
4353
)
4354
kernel_block_sizes.append(selected_kernel_size)
@@ -4372,6 +4366,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4366
self,
4367
kv_cache_config: KVCacheConfig,
4368
kv_cache_raw_tensors: dict[str, torch.Tensor],
4369
+ kernel_block_sizes: list[int],
4370
) -> dict[str, torch.Tensor]:
4371
"""
4372
Reshape the KV cache tensors to the desired shape and dtype.
@@ -4380,6 +4375,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4375
kv_cache_config: The KV cache config
4376
kv_cache_raw_tensors: The KV cache buffer of each layer, with
4377
correct size but uninitialized shape.
4378
+ kernel_block_sizes: The kernel block sizes for each KV cache group.
4379
Returns:
4380
Dict[str, torch.Tensor]: A map between layer names to their
4381
corresponding memory buffer for KV cache.
@@ -4389,6 +4385,10 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4385
for group in self._kv_cache_spec_attn_group_iterator():
4386
kv_cache_spec = group.kv_cache_spec
4387
attn_backend = group.backend
4388
+ if group.kv_cache_group_id == len(kernel_block_sizes):
4389
+ # There may be a last group for layers without kv cache.
4390
+ continue
4391
+ kernel_block_size = kernel_block_sizes[group.kv_cache_group_id]
4392
for layer_name in group.layer_names:
4393
if layer_name in self.runner_only_attn_layers:
4394
continue
@@ -4397,24 +4397,21 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4397
num_blocks = raw_tensor.numel() // kv_cache_spec.page_size_bytes
4398
if isinstance(kv_cache_spec, AttentionSpec):
4399
has_attn = True
4400
- kv_manager_block_size = kv_cache_spec.block_size
4401
- kernel_size_list = self._find_compatible_block_sizes(
4402
- kv_manager_block_size, attn_backend, return_all=False
4400
+ num_blocks_per_kv_block = (
4401
+ kv_cache_spec.block_size // kernel_block_size
4402
)
4404
- kernel_size = kernel_size_list[0]
4405
- num_blocks_per_kv_block = kv_manager_block_size // kernel_size
4403
kernel_num_blocks = num_blocks * num_blocks_per_kv_block
4404
4405
kv_cache_shape = attn_backend.get_kv_cache_shape(
4406
kernel_num_blocks,
4410
- kernel_size,
4407
+ kernel_block_size,
4408
kv_cache_spec.num_kv_heads,
4409
kv_cache_spec.head_size,
4410
cache_dtype_str=self.cache_config.cache_dtype,
4411
)
4412
dtype = kv_cache_spec.dtype
4413
try:
4417
- kv_cache_stride_order = attn_backend.get_kv_cache_stride_order() # noqa: E501
4414
+ kv_cache_stride_order = attn_backend.get_kv_cache_stride_order()
4415
assert len(kv_cache_stride_order) == len(kv_cache_shape)
4416
except (AttributeError, NotImplementedError):
4417
kv_cache_stride_order = tuple(range(len(kv_cache_shape)))
@@ -4497,13 +4494,15 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4494
)
4495
4496
def initialize_kv_cache_tensors(
4500
- self, kv_cache_config: KVCacheConfig
4497
+ self, kv_cache_config: KVCacheConfig, kernel_block_sizes: list[int]
4498
) -> dict[str, torch.Tensor]:
4499
"""
4500
Initialize the memory buffer for KV cache.
4501
4502
Args:
4503
kv_cache_config: The KV cache config
4504
+ kernel_block_sizes: The kernel block sizes for each KV cache group.
4505
+
4506
Returns:
4507
Dict[str, torch.Tensor]: A map between layer names to their
4508
corresponding memory buffer for KV cache.
@@ -4512,7 +4511,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4511
kv_cache_raw_tensors = self._allocate_kv_cache_tensors(kv_cache_config)
4512
# Change the memory buffer to the desired shape
4513
kv_caches = self._reshape_kv_cache_tensors(
4515
- kv_cache_config, kv_cache_raw_tensors
4514
+ kv_cache_config, kv_cache_raw_tensors, kernel_block_sizes
4515
)
4516
4517
# Set up cross-layer KV cache sharing
@@ -4571,9 +4570,17 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
4570
self.may_add_encoder_only_layers_to_kv_cache_config()
4571
self.maybe_add_kv_sharing_layers_to_kv_cache_groups(kv_cache_config)
4572
self.initialize_attn_backend(kv_cache_config)
4573
+ # The kernel block size for all KV cache groups. For example, if
4574
+ # kv_cache_manager uses block_size 256 for a given group, but the attention
4575
+ # backends for that group only supports block_size 64, we will return
4576
+ # kernel_block_size 64 and split the 256-token-block to 4 blocks with 64
4577
+ # tokens each.
4578
+ kernel_block_sizes = self._prepare_kernel_block_sizes(kv_cache_config)
4579
# Reinitialize need to after initialize_attn_backend
4575
- self.may_reinitialize_input_batch(kv_cache_config)
4576
- kv_caches = self.initialize_kv_cache_tensors(kv_cache_config)
4580
+ self.may_reinitialize_input_batch(kv_cache_config, kernel_block_sizes)
4581
+ kv_caches = self.initialize_kv_cache_tensors(
4582
+ kv_cache_config, kernel_block_sizes
4583
+ )
4584
4585
if self.speculative_config and self.speculative_config.use_eagle():
4586
assert isinstance(self.drafter, EagleProposer)
vllm/v1/worker/utils.py
+5
-1
@@ -140,6 +140,7 @@ class AttentionGroup:
140
metadata_builders: list[AttentionMetadataBuilder]
141
layer_names: list[str]
142
kv_cache_spec: KVCacheSpec
143
+ kv_cache_group_id: int
144
145
@staticmethod
146
def create_with_metadata_builders(
@@ -148,13 +149,16 @@ class AttentionGroup:
149
kv_cache_spec: KVCacheSpec,
150
vllm_config: VllmConfig,
151
device: torch.device,
152
+ kv_cache_group_id: int,
153
num_metadata_builders: int = 1,
154
) -> "AttentionGroup":
155
metadata_builders = [
156
backend.get_builder_cls()(kv_cache_spec, layer_names, vllm_config, device)
157
for _ in range(num_metadata_builders)
158
]
157
- return AttentionGroup(backend, metadata_builders, layer_names, kv_cache_spec)
159
+ return AttentionGroup(
160
+ backend, metadata_builders, layer_names, kv_cache_spec, kv_cache_group_id
161
+ )
162
163
def get_metadata_builder(self, ubatch_id: int = 0) -> AttentionMetadataBuilder:
164
assert len(self.metadata_builders) > ubatch_id