@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 7743152957
Fork 0

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 8, 03:45 UTC

[Attention] Refactor `check_and_update_config` (#33600)

Signed-off-by: Matthew Bonanni <mbonanni@redhat.com>

Matthew Bonanni committed Feb 17, 2026 at 20:06 UTC 7743152957236f21fc36f0402f9678159976ccc5
4 files changed +270 -172
vllm/config/cache.py
+4 -7
@@ -19,7 +19,6 @@ else:
19
20 logger = init_logger(__name__)
21
22 -BlockSize = Literal[1, 8, 16, 32, 64, 128, 256]
22 CacheDType = Literal[
23 "auto",
24 "bfloat16",
@@ -39,13 +38,11 @@ KVOffloadingBackend = Literal["native", "lmcache"]
38 class CacheConfig:
39 """Configuration for the KV cache."""
40
42 - block_size: SkipValidation[BlockSize] = None # type: ignore[assignment]
43 - """Size of a contiguous cache block in number of tokens. On CUDA devices,
44 - only block sizes up to 32 are supported.
41 + block_size: SkipValidation[int] = None # type: ignore[assignment]
42 + """Size of a contiguous cache block in number of tokens.
43
46 - This config has no static default. If left unspecified by the user, it will
47 - be set in `Platform.check_and_update_config()` based on the current
48 - platform."""
44 + This is None until `Platform.check_and_update_config()` sets it based on
45 + the current platform. Always an int by the time the engine starts."""
46 gpu_memory_utilization: float = Field(default=0.9, gt=0, le=1)
47 """The fraction of GPU memory to be used for the model executor, which can
48 range from 0 to 1. For example, a value of 0.5 would imply 50% GPU memory
vllm/engine/arg_utils.py
+1 -2
@@ -59,7 +59,6 @@ from vllm.config import (
59 get_attr_docs,
60 )
61 from vllm.config.cache import (
62 - BlockSize,
62 CacheDType,
63 KVOffloadingBackend,
64 MambaCacheMode,
@@ -431,7 +430,7 @@ class EngineArgs:
430 max_parallel_loading_workers: int | None = (
431 ParallelConfig.max_parallel_loading_workers
432 )
434 - block_size: BlockSize = CacheConfig.block_size
433 + block_size: int = None # type: ignore[assignment]
434 enable_prefix_caching: bool | None = None
435 prefix_caching_hash_algo: PrefixCachingHashAlgo = (
436 CacheConfig.prefix_caching_hash_algo
vllm/platforms/cuda.py
+253 -156
@@ -163,8 +163,6 @@ class CudaPlatformBase(Platform):
163
164 @classmethod
165 def check_and_update_config(cls, vllm_config: "VllmConfig") -> None:
166 - from vllm.v1.attention.backends.registry import AttentionBackendEnum
167 -
166 parallel_config = vllm_config.parallel_config
167 model_config = vllm_config.model_config
168
@@ -172,112 +170,19 @@ class CudaPlatformBase(Platform):
170 parallel_config.worker_cls = "vllm.v1.worker.gpu_worker.Worker"
171
172 cache_config = vllm_config.cache_config
175 - if cache_config and cache_config.block_size is None:
173 + user_specified_block_size = cache_config.block_size is not None
174 + if not user_specified_block_size:
175 cache_config.block_size = 16
176
178 - # TODO(lucas): handle this more gracefully
179 - # Note: model_config may be None during testing
180 - # Note: block_size is initialized in
181 - # HybridAttentionMambaModelConfig.verify_and_update_config
182 - # for models with both attention and mamba,
183 - # and doesn't need to be reinitialized here
184 - if (
185 - model_config is not None
186 - and model_config.use_mla
187 - and cache_config.block_size is not None
188 - ):
189 - use_sparse = hasattr(vllm_config.model_config.hf_config, "index_topk")
190 - # If `--attention-config.backend` is not set and we are using MLA,
191 - # then we default to FlashMLA backend for non-blackwell GPUs,
192 - # else we default to CutlassMLA. For each case, we force the
193 - # required block_size.
194 - use_flashmla = False
195 - use_cutlass_mla = False
196 - use_flashinfer_mla = False
197 - use_flashmla_sparse = False
198 - use_flashinfer_mla_sparse = False
199 -
200 - from vllm.v1.attention.ops.flashmla import is_flashmla_dense_supported
201 -
202 - if vllm_config.attention_config.backend is None:
203 - # Default case
204 - hf_text_config = model_config.hf_text_config
205 - qk_nope_head_dim = getattr(hf_text_config, "qk_nope_head_dim", 1)
206 - if (
207 - cls.is_device_capability_family(100)
208 - and not use_sparse
209 - and qk_nope_head_dim == 128
210 - ):
211 - # Blackwell => Force FlashInfer MLA (unless sparse, i.e. DSv3.2)
212 - # and only if qk_nope_head_dim == 128 (kernel constraint)
213 - use_flashinfer_mla = True
214 - # Set the backend in AttentionConfig so it's used during
215 - # backend selection
216 - vllm_config.attention_config.backend = (
217 - AttentionBackendEnum.FLASHINFER_MLA
218 - )
219 - elif cls.is_device_capability_family(100) and not use_sparse:
220 - # Fall back to CUTLASS_MLA as 2nd priority on Blackwell
221 - use_cutlass_mla = True
222 - elif is_flashmla_dense_supported()[0]:
223 - # Non-Blackwell with FlashMLA support
224 - use_flashmla = True
225 - else:
226 - # Fallback: will use Triton MLA or other compatible backend
227 - pass
228 - else:
229 - # Forced case
230 - backend = vllm_config.attention_config.backend
231 - use_flashmla = backend == AttentionBackendEnum.FLASHMLA
232 - use_cutlass_mla = backend == AttentionBackendEnum.CUTLASS_MLA
233 - use_flashinfer_mla = backend == AttentionBackendEnum.FLASHINFER_MLA
234 - use_flashmla_sparse = backend == AttentionBackendEnum.FLASHMLA_SPARSE
235 - use_flashinfer_mla_sparse = (
236 - backend == AttentionBackendEnum.FLASHINFER_MLA_SPARSE
237 - )
238 -
239 - if (
240 - use_flashmla
241 - and is_flashmla_dense_supported()[0]
242 - and cache_config.block_size % 64 != 0
243 - ):
244 - cache_config.block_size = 64
245 - logger.info("Forcing kv cache block size to 64 for FlashMLA backend.")
246 -
247 - if use_cutlass_mla and cache_config.block_size % 128 != 0:
248 - cache_config.block_size = 128
249 - logger.info(
250 - "Forcing kv cache block size to 128 for CUTLASS_MLA backend."
251 - )
252 -
253 - if (
254 - use_flashinfer_mla
255 - and cache_config.block_size != 32
256 - and cache_config.block_size % 64 != 0
257 - ):
258 - cache_config.block_size = 64
259 - logger.info(
260 - "Forcing kv cache block size to 64 for FlashInferMLA backend."
261 - )
262 -
263 - if use_sparse:
264 - if not (use_flashmla_sparse or use_flashinfer_mla_sparse):
265 - use_flashmla_sparse = True
266 -
267 - if use_flashmla_sparse and cache_config.block_size != 64:
268 - cache_config.block_size = 64
269 - logger.info(
270 - "Forcing kv cache block size to 64 for FlashMLASparse backend."
271 - )
272 - elif use_flashinfer_mla_sparse and cache_config.block_size not in (
273 - 32,
274 - 64,
275 - ):
276 - cache_config.block_size = 64
277 - logger.info(
278 - "Forcing kv cache block size to 64 for FlashInferMLASparse "
279 - "backend."
280 - )
177 + # Ensure block_size is compatible with the attention backend.
178 + # Note: model_config may be None during testing.
179 + # Skip hybrid (attention+mamba) models — their block_size is
180 + # managed by HybridAttentionMambaModelConfig
181 + if model_config is not None and not model_config.is_hybrid:
182 + cls._update_block_size_for_backend(
183 + vllm_config,
184 + user_specified_block_size,
185 + )
186
187 scheduler_config = vllm_config.scheduler_config
188 # Note: model_config may be None during testing
@@ -293,6 +198,150 @@ class CudaPlatformBase(Platform):
198 )
199 scheduler_config.disable_chunked_mm_input = True
200
201 + @classmethod
202 + def _update_block_size_for_backend(
203 + cls,
204 + vllm_config: "VllmConfig",
205 + user_specified_block_size: bool,
206 + ) -> None:
207 + """Ensure block_size is compatible with the attention backend.
208 +
209 + If the user specified --block-size, the selector validates/filters
210 + backends by that block size (raising on incompatibility). Otherwise,
211 + the backend is selected unconstrained and block_size is set to the
212 + backend's preferred value.
213 + """
214 + from vllm.config.vllm import set_current_vllm_config
215 + from vllm.v1.attention.selector import AttentionSelectorConfig
216 +
217 + model_config = vllm_config.model_config
218 + cache_config = vllm_config.cache_config
219 +
220 + device_capability = cls.get_device_capability()
221 + if device_capability is None:
222 + return
223 +
224 + use_mla = model_config.use_mla
225 + attn_selector_config = AttentionSelectorConfig(
226 + head_size=model_config.get_head_size(),
227 + dtype=model_config.dtype, # type: ignore[arg-type]
228 + kv_cache_dtype=cache_config.cache_dtype,
229 + block_size=cache_config.block_size if user_specified_block_size else None,
230 + use_mla=use_mla,
231 + has_sink=False,
232 + use_sparse=use_mla and hasattr(model_config.hf_config, "index_topk"),
233 + use_mm_prefix=model_config.is_mm_prefix_lm,
234 + )
235 +
236 + user_specified_backend = vllm_config.attention_config.backend
237 + num_heads = model_config.get_num_attention_heads(
238 + vllm_config.parallel_config,
239 + )
240 + with set_current_vllm_config(vllm_config):
241 + chosen_backend = cls.select_attention_backend(
242 + selected_backend=user_specified_backend,
243 + attn_selector_config=attn_selector_config,
244 + device_capability=device_capability,
245 + # Don't raise here — we produce better errors below.
246 + raise_on_invalid=False,
247 + num_heads=num_heads,
248 + )
249 +
250 + # If the user's --block-size forced a non-optimal backend,
251 + # warn them. Only relevant when the user didn't also specify
252 + # --attention-backend (in which case the choice is explicit).
253 + if (
254 + chosen_backend is not None
255 + and user_specified_block_size
256 + and user_specified_backend is None
257 + ):
258 + optimal = cls.select_attention_backend(
259 + selected_backend=None,
260 + attn_selector_config=attn_selector_config._replace(
261 + block_size=None,
262 + ),
263 + device_capability=device_capability,
264 + raise_on_invalid=False,
265 + num_heads=num_heads,
266 + )
267 + if optimal is not None and optimal != chosen_backend:
268 + logger.warning(
269 + "--block-size %d is not supported by the preferred "
270 + "%s backend. Using %s instead, which may result "
271 + "in reduced performance. Consider removing "
272 + "--block-size to auto-select the optimal "
273 + "block size.",
274 + cache_config.block_size,
275 + optimal.name,
276 + chosen_backend.name,
277 + )
278 +
279 + if chosen_backend is not None:
280 + if user_specified_block_size:
281 + # User's block_size is compatible with the chosen
282 + # backend.
283 + return
284 + # User didn't specify --block-size, so auto-select the
285 + # preferred block size for the chosen backend.
286 + try:
287 + backend_class = chosen_backend.get_class()
288 + except ImportError:
289 + return # Will fail later with a better error
290 + preferred = backend_class.get_preferred_block_size(
291 + cache_config.block_size,
292 + )
293 + if cache_config.block_size != preferred:
294 + logger.info(
295 + "Setting kv cache block size to %d for %s backend.",
296 + preferred,
297 + chosen_backend.name,
298 + )
299 + cache_config.block_size = preferred
300 + return
301 +
302 + # No valid backend found. If the user didn't constrain the
303 + # selection, defer the error to get_attn_backend_cls where
304 + # the full config (including per-layer settings) is
305 + # available.
306 + if not user_specified_block_size:
307 + return
308 +
309 + if user_specified_backend is not None:
310 + # User specified --block-size and --attention-backend
311 + # and they are incompatible.
312 + try:
313 + backend_class = user_specified_backend.get_class()
314 + supported = backend_class.get_supported_kernel_block_sizes()
315 + except ImportError:
316 + supported = None
317 + raise ValueError(
318 + f"User-specified --block-size "
319 + f"{cache_config.block_size} is incompatible with "
320 + f"the specified --attention-backend "
321 + f"{user_specified_backend.name} (supported kernel "
322 + f"block sizes: {supported}). Either remove "
323 + f"--block-size to auto-select, or choose a "
324 + f"compatible value."
325 + )
326 + else:
327 + # User specified --block-size but no backend supports
328 + # it.
329 + _, invalid_reasons = cls.get_valid_backends(
330 + device_capability=device_capability,
331 + attn_selector_config=attn_selector_config,
332 + num_heads=num_heads,
333 + )
334 + reasons_str = ", ".join(
335 + f"{b.name}: [{', '.join(r)}]" for b, r in invalid_reasons.items()
336 + )
337 + raise ValueError(
338 + f"No valid attention backend found for "
339 + f"--block-size {cache_config.block_size}. "
340 + f"Reasons: {{{reasons_str}}}. Either remove "
341 + f"--block-size to auto-select, or choose a "
342 + f"compatible value."
343 + )
344 +
345 @classmethod
346 def get_current_memory_usage(
347 cls, device: torch.types.Device | None = None
@@ -336,77 +385,125 @@ class CudaPlatformBase(Platform):
385 return valid_backends_priorities, invalid_reasons
386
387 @classmethod
339 - def get_attn_backend_cls(
388 + def select_attention_backend(
389 cls,
341 - selected_backend: "AttentionBackendEnum",
390 + selected_backend: "AttentionBackendEnum | None",
391 attn_selector_config: "AttentionSelectorConfig",
392 + device_capability: "DeviceCapability",
393 + raise_on_invalid: bool = True,
394 num_heads: int | None = None,
344 - ) -> str:
345 - device_capability = cls.get_device_capability()
346 - assert device_capability is not None
347 -
348 - attn_selector_config = attn_selector_config._replace(block_size=None)
395 + ) -> "AttentionBackendEnum | None":
396 + """Select the best attention backend for the given configuration.
397 +
398 + Args:
399 + selected_backend: User-specified backend, or None for auto-selection
400 + attn_selector_config: Configuration for attention selection
401 + device_capability: Device capability info
402 + raise_on_invalid: If True, raise ValueError when no valid backend
403 + num_heads: Number of attention heads per GPU, used for backend
404 + priority ordering on Blackwell GPUs
405 +
406 + Returns:
407 + The selected backend enum, or None if no valid backend found
408 + and raise_on_invalid is False
409 + """
410 # First try checking just the selected backend, if there is one.
411 if selected_backend is not None:
412 try:
413 backend_class = selected_backend.get_class()
353 - invalid_reasons = backend_class.validate_configuration(
414 + validation_errors = backend_class.validate_configuration(
415 device_capability=device_capability,
416 **attn_selector_config._asdict(),
417 )
418 except ImportError:
358 - invalid_reasons = ["ImportError"]
359 - if invalid_reasons:
360 - raise ValueError(
361 - f"Selected backend {selected_backend} is not valid for "
362 - f"this configuration. Reason: {invalid_reasons}"
363 - )
364 - else:
365 - logger.info("Using %s backend.", selected_backend)
366 - return selected_backend.get_path()
419 + validation_errors = ["ImportError"]
420 + if validation_errors:
421 + if raise_on_invalid:
422 + raise ValueError(
423 + f"Selected backend {selected_backend} is not valid for "
424 + f"this configuration. Reason: {validation_errors}"
425 + )
426 + return None
427 + return selected_backend
428
368 - # No selected backend or the selected backend is invalid,
369 - # so we try finding a valid backend.
429 + # No selected backend, so find the best valid one.
430 valid_backends_priorities, invalid_reasons = cls.get_valid_backends(
431 device_capability=device_capability,
432 attn_selector_config=attn_selector_config,
433 num_heads=num_heads,
434 )
375 - reasons_str = (
376 - "{"
377 - + ", ".join(
378 - f"{backend.name}: [{', '.join(reasons)}]"
379 - for backend, reasons in invalid_reasons.items()
380 - )
381 - + "}"
382 - )
383 - config_str = attn_selector_config.__repr__()
384 - logger.debug_once(
385 - f"Some attention backends are not valid for {cls.device_name} with "
386 - f"{config_str}. Reasons: {reasons_str}."
387 - )
435 +
436 if len(valid_backends_priorities) == 0:
389 - raise ValueError(
390 - f"No valid attention backend found for {cls.device_name} "
391 - f"with {config_str}. Reasons: {reasons_str}."
392 - )
437 + if raise_on_invalid:
438 + reasons_str = (
439 + "{"
440 + + ", ".join(
441 + f"{backend.name}: [{', '.join(reasons)}]"
442 + for backend, reasons in invalid_reasons.items()
443 + )
444 + + "}"
445 + )
446 + config_str = attn_selector_config.__repr__()
447 + raise ValueError(
448 + f"No valid attention backend found for {cls.device_name} "
449 + f"with {config_str}. Reasons: {reasons_str}."
450 + )
451 + return None
452
394 - # We have found some valid backends. Select the one with the
395 - # highest priority.
396 - sorted_indices = sorted(
397 - range(len(valid_backends_priorities)),
398 - key=lambda i: valid_backends_priorities[i][1],
399 - )
400 - selected_index = sorted_indices[0]
401 - selected_backend = valid_backends_priorities[selected_index][0]
402 - logger.info_once(
403 - "Using %s attention backend out of potential backends: %s.",
404 - selected_backend.name,
405 - "[" + ", ".join(f"'{b[0].name}'" for b in valid_backends_priorities) + "]",
406 - scope="local",
453 + # Select the one with the highest priority (lowest index).
454 + sorted_backends = sorted(valid_backends_priorities, key=lambda x: x[1])
455 + return sorted_backends[0][0]
456 +
457 + @classmethod
458 + def get_attn_backend_cls(
459 + cls,
460 + selected_backend: "AttentionBackendEnum | None",
461 + attn_selector_config: "AttentionSelectorConfig",
462 + num_heads: int | None = None,
463 + ) -> str:
464 + device_capability = cls.get_device_capability()
465 + assert device_capability is not None
466 +
467 + chosen_backend = cls.select_attention_backend(
468 + selected_backend=selected_backend,
469 + attn_selector_config=attn_selector_config,
470 + num_heads=num_heads,
471 + device_capability=device_capability,
472 + raise_on_invalid=True,
473 )
474 + assert chosen_backend is not None # raise_on_invalid=True guarantees this
475 +
476 + # Log the selection
477 + if selected_backend is not None:
478 + logger.info("Using %s backend.", chosen_backend)
479 + else:
480 + # Get all valid backends for logging
481 + valid_backends_priorities, invalid_reasons = cls.get_valid_backends(
482 + device_capability=device_capability,
483 + attn_selector_config=attn_selector_config,
484 + num_heads=num_heads,
485 + )
486 + reasons_str = (
487 + "{"
488 + + ", ".join(
489 + f"{backend.name}: [{', '.join(reasons)}]"
490 + for backend, reasons in invalid_reasons.items()
491 + )
492 + + "}"
493 + )
494 + config_str = attn_selector_config.__repr__()
495 + logger.debug_once(
496 + f"Some attention backends are not valid for {cls.device_name} with "
497 + f"{config_str}. Reasons: {reasons_str}."
498 + )
499 + logger.info_once(
500 + "Using %s attention backend out of potential backends: %s",
501 + chosen_backend.name,
502 + tuple(b[0].name for b in valid_backends_priorities),
503 + scope="local",
504 + )
505
409 - return selected_backend.get_path()
506 + return chosen_backend.get_path()
507
508 @classmethod
509 def get_supported_vit_attn_backends(cls) -> list["AttentionBackendEnum"]:
vllm/v1/attention/backend.py
+12 -7
@@ -4,7 +4,7 @@
4 from abc import ABC, abstractmethod
5 from dataclasses import dataclass, replace
6 from enum import Enum
7 -from typing import TYPE_CHECKING, Any, ClassVar, Generic, Protocol, TypeVar, get_args
7 +from typing import TYPE_CHECKING, Any, ClassVar, Generic, Protocol, TypeVar
8
9 import numpy as np
10 import torch
@@ -144,15 +144,9 @@ class AttentionBackend(ABC):
144
145 @classmethod
146 def supports_block_size(cls, block_size: int | None) -> bool:
147 - from vllm.config.cache import BlockSize
148 -
147 if block_size is None:
148 return True
149
152 - valid_sizes = get_args(BlockSize)
153 - if block_size not in valid_sizes:
154 - return False
155 -
150 supported_kernel_block_sizes = cls.get_supported_kernel_block_sizes()
151 if not supported_kernel_block_sizes:
152 return True
@@ -167,6 +161,17 @@ class AttentionBackend(ABC):
161 return True
162 return False
163
164 + @classmethod
165 + def get_preferred_block_size(cls, default_block_size: int = 16) -> int:
166 + supported_sizes = cls.get_supported_kernel_block_sizes()
167 + if not supported_sizes:
168 + return default_block_size
169 +
170 + if cls.supports_block_size(default_block_size):
171 + return default_block_size
172 +
173 + return min(s.base if isinstance(s, MultipleOf) else s for s in supported_sizes)
174 +
175 @classmethod
176 def is_mla(cls) -> bool:
177 return False