# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from collections import defaultdict from dataclasses import dataclass from typing import cast from vllm.config import VllmConfig from vllm.config.kv_transfer import hisparse_host_pool_gib from vllm.logger import init_logger from vllm.utils.math_utils import cdiv from vllm.v1.hisparse.runtime import ( ResolvedHiSparseConfig, get_hisparse_host_block_stride, use_shared_hisparse_host_pool, ) from vllm.v1.kv_cache_interface import ( HiSparseHotSpec, HiSparseResidentSpec, KVCacheConfig, KVCacheGroupRole, KVCacheGroupSpec, KVCacheSpec, KVCacheTensor, MLAAttentionSpec, SparseCacheRole, UniformTypeKVCacheSpecs, compute_layout_strides, ) from vllm.v1.kv_cache_layout import KVCacheLayout logger = init_logger(__name__) HISPARSE_HOT_SUFFIX = ".hisparse_hot" HISPARSE_RESIDENT_SUFFIX = ".hisparse_resident" @dataclass(frozen=True) class HiSparseLayout: source_group: KVCacheGroupSpec device_groups: list[KVCacheGroupSpec] host_num_blocks: int host_block_stride: int shared_host_pool: bool def get_hisparse_kv_cache_groups( vllm_config: VllmConfig, kv_cache_spec: dict[str, KVCacheSpec] ) -> list[KVCacheGroupSpec] | None: attention_config = getattr(vllm_config, "attention_config", None) if attention_config is None or attention_config.hisparse_config is None: return None mla_specs: dict[str, KVCacheSpec] = { name: spec for name, spec in kv_cache_spec.items() if isinstance(spec, MLAAttentionSpec) } other_specs = { name: spec for name, spec in kv_cache_spec.items() if not isinstance(spec, MLAAttentionSpec) } if not mla_specs or not other_specs: return None from vllm.v1.core.kv_cache_utils import get_kv_cache_groups mla_group_spec = UniformTypeKVCacheSpecs.from_specs(mla_specs) assert mla_group_spec is not None mla_group = KVCacheGroupSpec(list(mla_specs), mla_group_spec) return [mla_group, *get_kv_cache_groups(vllm_config, other_specs)] def get_hisparse_host_pool_bytes(vllm_config: VllmConfig) -> int: host_pool_gib = hisparse_host_pool_gib(vllm_config.kv_transfer_config) if host_pool_gib is None: raise ValueError("HiSparse requires HiSparseConnector with host_pool_gib") return int(host_pool_gib * 2**30) def _partition_hisparse_specs( groups: list[KVCacheGroupSpec], ) -> tuple[dict[str, MLAAttentionSpec], dict[str, MLAAttentionSpec]]: group_spec = groups[0].kv_cache_spec if not isinstance(group_spec, UniformTypeKVCacheSpecs): raise ValueError("HiSparse requires uniform sparse-MLA cache specs.") specs = group_spec.kv_cache_specs if any( isinstance(spec, MLAAttentionSpec) and spec.model_version == "deepseek_v4" for spec in specs.values() ): raise ValueError("HiSparse does not support DeepSeek V4.") if not all(isinstance(spec, MLAAttentionSpec) for spec in specs.values()): raise ValueError("HiSparse requires its first cache group to contain MLA only.") source_specs = { name: spec for name, spec in specs.items() if isinstance(spec, MLAAttentionSpec) and spec.cache_role is SparseCacheRole.SPARSE } indexer_specs = { name: spec for name, spec in specs.items() if isinstance(spec, MLAAttentionSpec) and spec.cache_role is SparseCacheRole.INDEXER } if not source_specs or not indexer_specs: raise ValueError("HiSparse requires sparse-MLA and indexer cache specs.") return source_specs, indexer_specs def get_hisparse_gpu_memory_usage( vllm_config: VllmConfig, kv_cache_groups: list[KVCacheGroupSpec], ) -> int: _, indexer_specs = _partition_hisparse_specs(kv_cache_groups) return sum( spec.max_memory_usage_bytes(vllm_config) for spec in indexer_specs.values() ) + sum( group.kv_cache_spec.max_memory_usage_bytes(vllm_config) for group in kv_cache_groups[1:] ) def create_hisparse_layout( vllm_config: VllmConfig, groups: list[KVCacheGroupSpec], host_budget: int, ) -> HiSparseLayout: source_specs, indexer_specs = _partition_hisparse_specs(groups) block_sizes = { spec.block_size for spec in (*source_specs.values(), *indexer_specs.values()) } if len(block_sizes) != 1: raise ValueError("HiSparse requires one resolved GPU block size.") gpu_block_size = block_sizes.pop() config = ResolvedHiSparseConfig.from_vllm_config( vllm_config, vllm_config.model_config.hf_config.index_topk, gpu_block_size, ) assert config is not None indexer_group_spec = UniformTypeKVCacheSpecs.from_specs( cast(dict[str, KVCacheSpec], indexer_specs) ) assert indexer_group_spec is not None indexer_group = KVCacheGroupSpec( list(indexer_specs), indexer_group_spec, enable_kv_transfer=True, role=KVCacheGroupRole.HISPARSE_INDEXER, ) indexer_page = sum(spec.page_size_bytes for spec in indexer_specs.values()) hot_blocks_per_request = cdiv(config.device_buffer_size, gpu_block_size) hot_units: list[list[tuple[str, MLAAttentionSpec]]] = [] for layer_name, layer_spec in source_specs.items(): if layer_spec.is_index_group_leader or not hot_units: hot_units.append([]) hot_units[-1].append((f"{layer_name}{HISPARSE_HOT_SUFFIX}", layer_spec)) resident_groups: list[KVCacheGroupSpec] = [] hot_groups: list[KVCacheGroupSpec] = [] def append_hot_group(layers: list[tuple[str, MLAAttentionSpec]]) -> None: page_sizes = {spec.page_size_bytes for _, spec in layers} if len(page_sizes) != 1: raise ValueError( "HiSparse hot-cache groups require one page size, got " f"{sorted(page_sizes)}." ) page_size = page_sizes.pop() names = [name for name, _ in layers] resident_groups.append( KVCacheGroupSpec( [ name[: -len(HISPARSE_HOT_SUFFIX)] + HISPARSE_RESIDENT_SUFFIX for name in names ], HiSparseResidentSpec( block_size=gpu_block_size, page_size=page_size, ), enable_kv_transfer=False, ) ) hot_groups.append( KVCacheGroupSpec( names, HiSparseHotSpec( block_size=gpu_block_size, page_size=page_size, blocks_per_request=hot_blocks_per_request, ), enable_kv_transfer=False, ) ) current: list[tuple[str, MLAAttentionSpec]] = [] current_page = 0 for unit in hot_units: unit_page = sum(spec.page_size_bytes for _, spec in unit) if current and current_page + unit_page > indexer_page: append_hot_group(current) current = [] current_page = 0 current.extend(unit) current_page += unit_page if current: append_hot_group(current) source_group_spec = UniformTypeKVCacheSpecs.from_specs( cast(dict[str, KVCacheSpec], source_specs) ) assert source_group_spec is not None source_group = KVCacheGroupSpec( list(source_specs), source_group_spec, role=KVCacheGroupRole.HISPARSE_SOURCE, host_resident=True, enable_kv_transfer=True, ) regular_groups = groups[1:] gpu_groups = [indexer_group, *resident_groups, *hot_groups, *regular_groups] shared_host_pool = use_shared_hisparse_host_pool(vllm_config) host_block_stride = get_hisparse_host_block_stride( sum(spec.page_size_bytes for spec in source_specs.values()), use_shared_host_pool=shared_host_pool, ) host_num_blocks = host_budget // host_block_stride if host_num_blocks <= 0: raise ValueError("HiSparse has no allocatable host blocks.") return HiSparseLayout( source_group=source_group, device_groups=gpu_groups, host_num_blocks=host_num_blocks, host_block_stride=host_block_stride, shared_host_pool=shared_host_pool, ) def _build_hisparse_kv_cache_tensors( kv_cache_groups: list[KVCacheGroupSpec], num_blocks: int, size: int, layout: KVCacheLayout, bytes_per_block: int, *, host_resident: bool = False, ) -> list[KVCacheTensor]: interleaved_block_stride = bytes_per_block if layout.is_block_outermost else None tensors: list[KVCacheTensor] = [] for group in kv_cache_groups: group_spec = group.kv_cache_spec layers_by_spec: defaultdict[KVCacheSpec, list[str]] = defaultdict(list) if isinstance(group_spec, UniformTypeKVCacheSpecs): for layer_name, spec in group_spec.kv_cache_specs.items(): layers_by_spec[spec].append(layer_name) elif group.layer_names: layers_by_spec[group_spec].extend(group.layer_names) byte_offset = 0 for spec, layer_names in layers_by_spec.items(): if isinstance(spec, (HiSparseHotSpec, HiSparseResidentSpec)): if not layout.is_block_outermost: raise ValueError("HiSparse requires a block-outermost KV layout.") layer_stride = spec.page_size_bytes block_stride = bytes_per_block else: layer_stride, block_stride, _, _, _ = compute_layout_strides( spec, num_blocks, len(layer_names), layout, fixed_strides=(None, interleaved_block_stride, None, None, None), ) offset = ( byte_offset * max(layer_stride, spec.page_size_bytes) // spec.page_size_bytes ) tensors.append( KVCacheTensor( size=size, layers=layer_names, layer_stride=layer_stride, block_stride=block_stride, offset=offset, host_resident=host_resident, ) ) byte_offset += len(layer_names) * spec.page_size_bytes return tensors def get_hisparse_kv_cache_config( vllm_config: VllmConfig, kv_cache_groups: list[KVCacheGroupSpec], available_memory: int, host_budget: int, ) -> KVCacheConfig: from vllm.v1.core.kv_cache_utils import ( _get_kv_cache_bytes_per_block, may_override_num_blocks, validate_kv_cache_layout, ) hisparse_layout = create_hisparse_layout(vllm_config, kv_cache_groups, host_budget) device_groups = hisparse_layout.device_groups layout = vllm_config.cache_config.get_resolved_kv_cache_layout() validate_kv_cache_layout(layout, device_groups) bytes_per_block = _get_kv_cache_bytes_per_block(device_groups) num_blocks = may_override_num_blocks( vllm_config, available_memory // bytes_per_block ) size = bytes_per_block * num_blocks kv_cache_tensors = _build_hisparse_kv_cache_tensors( device_groups, num_blocks, size, layout, bytes_per_block ) host_groups = [hisparse_layout.source_group] host_layout = KVCacheLayout.LBNHC validate_kv_cache_layout(host_layout, host_groups) host_bytes_per_block = _get_kv_cache_bytes_per_block(host_groups) host_size = host_bytes_per_block * hisparse_layout.host_num_blocks kv_cache_tensors[:0] = _build_hisparse_kv_cache_tensors( host_groups, hisparse_layout.host_num_blocks, host_size, host_layout, host_bytes_per_block, host_resident=True, ) logger.info_once( "HiSparse HMA: %.1f GiB host source (%d blocks), %.1f GiB shared " "GPU indexer/resident/hot pool (%d blocks).", host_size / 2**30, hisparse_layout.host_num_blocks, size / 2**30, num_blocks, ) return KVCacheConfig( num_blocks=num_blocks, kv_cache_tensors=kv_cache_tensors, kv_cache_groups=[*host_groups, *device_groups], hisparse_host_num_blocks=hisparse_layout.host_num_blocks, hisparse_host_block_stride=hisparse_layout.host_block_stride, hisparse_shared_host_pool=hisparse_layout.shared_host_pool, prefix_cache_retention_interval=( vllm_config.cache_config.prefix_cache_retention_interval ), )