main
py 306 lines 11.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Worker-side wiring of HiSparse caches to attention layers."""
4
5 from collections.abc import Mapping
6 from copy import copy
7 from typing import TYPE_CHECKING, Any
8
9 import torch
10
11 from vllm.config import VllmConfig
12 from vllm.v1.hisparse.layout import (
13 HISPARSE_HOT_SUFFIX,
14 HISPARSE_RESIDENT_SUFFIX,
15 get_hisparse_kv_cache_groups,
16 )
17 from vllm.v1.hisparse.runtime import (
18 HiSparseCacheHandle,
19 HiSparseHostPool,
20 initialize_hisparse_runtime_buffers,
21 release_pinned_state,
22 )
23 from vllm.v1.kv_cache_interface import (
24 HiSparseHotSpec,
25 HiSparseResidentSpec,
26 KVCacheConfig,
27 KVCacheLayout,
28 KVCacheSpec,
29 MLAAttentionSpec,
30 UniformTypeKVCacheSpecs,
31 create_kv_cache_views,
32 )
33 from vllm.v1.worker.utils import allocate_kv_cache, select_common_block_size
34
35 if TYPE_CHECKING:
36 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
37 from vllm.v1.worker.gpu.block_table import BlockTables
38
39
40 def resolve_hisparse_specs(
41 vllm_config: VllmConfig,
42 kv_cache_spec: dict[str, KVCacheSpec],
43 attn_layers: Mapping[str, "AttentionLayerBase"],
44 ) -> dict[str, KVCacheSpec]:
45 """Resolve a common kernel block size for HiSparse MLA specs, and add the
46 resident/hot specs HiSparse derives from them."""
47 mla_specs = {
48 name: spec
49 for name, spec in kv_cache_spec.items()
50 if isinstance(spec, MLAAttentionSpec)
51 }
52 if not mla_specs:
53 return kv_cache_spec
54 block_sizes = {spec.block_size for spec in mla_specs.values()}
55 if len(block_sizes) != 1:
56 raise ValueError("HiSparse requires one scheduler block size.")
57 backends = [attn_layers[name].get_attn_backend() for name in mla_specs]
58 try:
59 block_size = select_common_block_size(block_sizes.pop(), backends)
60 except ValueError as error:
61 raise ValueError(
62 "HiSparse requires a GPU block size supported by every sparse "
63 f"attention and indexer backend: {error}"
64 ) from error
65 kv_cache_spec = kv_cache_spec | {
66 name: spec.copy_with_new_block_size(block_size)
67 for name, spec in mla_specs.items()
68 }
69 groups = get_hisparse_kv_cache_groups(vllm_config, kv_cache_spec)
70 assert groups is not None
71 return kv_cache_spec | {
72 layer_name: group.kv_cache_spec
73 for group in groups
74 if isinstance(group.kv_cache_spec, (HiSparseResidentSpec, HiSparseHotSpec))
75 for layer_name in group.layer_names
76 }
77
78
79 def allocate_hisparse_kv_caches(
80 kv_cache_config: KVCacheConfig,
81 device: torch.device,
82 layout: KVCacheLayout,
83 kernel_block_sizes: list[int],
84 host_pool: HiSparseHostPool,
85 ) -> dict[str, torch.Tensor]:
86 """Allocate the host pool separately from the shared device backing."""
87 device_config = copy(kv_cache_config)
88 device_config.kv_cache_tensors = [
89 tensor
90 for tensor in kv_cache_config.kv_cache_tensors
91 if not tensor.host_resident
92 ]
93 kv_caches = allocate_kv_cache(device_config, device, layout, kernel_block_sizes)
94 host_tensors = [
95 tensor for tensor in kv_cache_config.kv_cache_tensors if tensor.host_resident
96 ]
97 (host_size,) = {tensor.size for tensor in host_tensors}
98 backing = host_pool.allocate(host_size)
99 (host_group_id,) = kv_cache_config.host_group_ids
100 host_spec = kv_cache_config.kv_cache_groups[host_group_id].kv_cache_spec
101 for tensor in host_tensors:
102 spec = (
103 host_spec.kv_cache_specs[tensor.layers[0]]
104 if isinstance(host_spec, UniformTypeKVCacheSpecs)
105 else host_spec
106 )
107 kernel_block_size = kernel_block_sizes[host_group_id]
108 if isinstance(spec, MLAAttentionSpec) and spec.storage_block_size is not None:
109 kernel_block_size = spec.storage_block_size
110 views = create_kv_cache_views(
111 backing,
112 spec,
113 kv_cache_config.num_blocks_of(tensor),
114 layout,
115 tensor,
116 kernel_block_size=kernel_block_size,
117 )
118 kv_caches.update(zip(tensor.layers, views))
119 return kv_caches
120
121
122 def init_hisparse_kv_cache(
123 kv_cache_config: KVCacheConfig,
124 device: torch.device,
125 kernel_block_sizes: list[int],
126 vllm_config: VllmConfig,
127 forward_context: dict[str, Any],
128 block_tables: "BlockTables",
129 ) -> dict[str, torch.Tensor]:
130 """Allocate and bind HiSparse caches within the caller's allocation context."""
131 host_pool = HiSparseHostPool(vllm_config, kv_cache_config)
132 try:
133 kv_caches = allocate_hisparse_kv_caches(
134 kv_cache_config,
135 device,
136 vllm_config.cache_config.get_resolved_kv_cache_layout(),
137 kernel_block_sizes,
138 host_pool,
139 )
140 cache_handles = bind_hisparse_kv_caches(
141 forward_context=forward_context,
142 kv_cache_config=kv_cache_config,
143 kv_caches=kv_caches,
144 block_tables=block_tables,
145 host_pool=host_pool,
146 )
147 initialize_hisparse_runtime_buffers(
148 cache_handles,
149 max_num_reqs=vllm_config.scheduler_config.max_num_seqs,
150 max_num_batched_tokens=vllm_config.scheduler_config.max_num_batched_tokens,
151 )
152 return kv_caches
153 except Exception:
154 if host_pool.shared_region is not None:
155 host_pool.shared_region.cleanup()
156 raise
157
158
159 def _get_hisparse_cache(
160 forward_context: dict[str, Any], layer_name: str
161 ) -> HiSparseCacheHandle:
162 attention_layer = forward_context[layer_name]
163 hisparse_cache = attention_layer.hisparse_cache
164 assert hisparse_cache is not None
165 return hisparse_cache
166
167
168 def release_hisparse_profiling_cache(forward_context: dict[str, Any]) -> None:
169 cache_handles = [
170 cache
171 for layer in forward_context.values()
172 if (cache := getattr(layer, "hisparse_cache", None)) is not None
173 ]
174 runtimes = {
175 id(cache.runtime): cache.runtime
176 for cache in cache_handles
177 if hasattr(cache.runtime, "_host_cache")
178 }
179 if not runtimes:
180 return
181
182 shared_regions = {
183 id(runtime.shared_host_region): runtime.shared_host_region
184 for runtime in runtimes.values()
185 if runtime.shared_host_region is not None
186 }
187 assert len(shared_regions) <= 1
188 shared_region = next(iter(shared_regions.values()), None)
189 registered_pools = (
190 []
191 if shared_region is not None
192 else list(
193 {
194 runtime.registered_host_pool.data_ptr(): runtime.registered_host_pool
195 for runtime in runtimes.values()
196 }.values()
197 )
198 )
199 release_pinned_state(list(runtimes.values()), registered_pools, shared_region)
200 for cache in cache_handles:
201 cache.mirror_staging_cache = None
202 cache.mirror_staging_slots = None
203
204
205 def bind_hisparse_kv_caches(
206 *,
207 forward_context: dict[str, Any],
208 kv_cache_config: KVCacheConfig,
209 kv_caches: dict[str, torch.Tensor],
210 block_tables: "BlockTables",
211 host_pool: HiSparseHostPool,
212 ) -> list[HiSparseCacheHandle]:
213 """Bind existing cache storage and block tables; return the bound handles."""
214 assert host_pool.registered is not None
215 tensor_configs = {
216 name: (
217 tensor_config,
218 tensor_config.offset + layer_index * tensor_config.layer_stride,
219 )
220 for tensor_config in kv_cache_config.kv_cache_tensors
221 for layer_index, name in enumerate(tensor_config.layers)
222 }
223 resident_source_index = 0
224 for group_id, group in enumerate(kv_cache_config.kv_cache_groups):
225 if not isinstance(group.kv_cache_spec, HiSparseResidentSpec):
226 continue
227 for cache_name in group.layer_names:
228 assert cache_name.endswith(HISPARSE_RESIDENT_SUFFIX)
229 layer_name = cache_name[: -len(HISPARSE_RESIDENT_SUFFIX)]
230 tensor_config, byte_offset = tensor_configs[cache_name]
231 assert not tensor_config.host_resident
232 cache_handle = _get_hisparse_cache(forward_context, layer_name)
233 cache_handle.bind_cache(
234 kv_caches[cache_name],
235 byte_offset=byte_offset,
236 block_stride=tensor_config.block_stride,
237 num_blocks=kv_cache_config.num_blocks,
238 block_size=group.kv_cache_spec.block_size,
239 block_table=block_tables.input_block_tables[group_id],
240 slot_mapping=block_tables.slot_mappings[group_id],
241 )
242 assert cache_handle.view is not None
243 kv_caches[cache_name] = cache_handle.view.cache
244 cache_handle.runtime.resident_source_index = resident_source_index
245 resident_source_index += 1
246
247 hot_backing: torch.Tensor | None = None
248 cache_handles: list[HiSparseCacheHandle] = []
249 for group_id, group in enumerate(kv_cache_config.kv_cache_groups):
250 if not isinstance(group.kv_cache_spec, HiSparseHotSpec):
251 continue
252 for cache_name in group.layer_names:
253 assert cache_name.endswith(HISPARSE_HOT_SUFFIX)
254 raw_tensor = kv_caches[cache_name]
255 if hot_backing is None:
256 hot_backing = raw_tensor
257 elif hot_backing.untyped_storage().data_ptr() != (
258 raw_tensor.untyped_storage().data_ptr()
259 ):
260 raise RuntimeError("HiSparse hot tensors must share one GPU backing.")
261 layer_name = cache_name[: -len(HISPARSE_HOT_SUFFIX)]
262 cache_handle = _get_hisparse_cache(forward_context, layer_name)
263 tensor_config, byte_offset = tensor_configs[cache_name]
264 assert not tensor_config.host_resident
265 cache_handle.runtime.bind_hot_cache(
266 raw_tensor,
267 byte_offset=byte_offset,
268 block_stride=tensor_config.block_stride,
269 num_blocks=kv_cache_config.num_blocks,
270 block_size=group.kv_cache_spec.block_size,
271 block_table=block_tables.input_block_tables[group_id],
272 )
273 resident = cache_handle.view
274 hot = cache_handle.runtime.hot
275 assert resident is not None
276 if (
277 resident.cache.untyped_storage().data_ptr()
278 != hot.cache.untyped_storage().data_ptr()
279 or resident.cache.data_ptr() != hot.cache.data_ptr()
280 or resident.cache.stride() != hot.cache.stride()
281 ):
282 raise RuntimeError("HiSparse resident and hot layouts must match.")
283 source_cache = kv_caches[layer_name]
284 assert source_cache.untyped_storage().data_ptr() == (
285 host_pool.registered.untyped_storage().data_ptr()
286 )
287 cache_handle.draft_layer = forward_context[layer_name].is_draft_layer
288 cache_handle.runtime.shared_host_region = host_pool.shared_region
289 cache_handle.runtime.bind_source_cache(
290 source_cache,
291 registered_host_pool=host_pool.registered,
292 )
293 cache_handles.append(cache_handle)
294 # The hot slab is raw storage owned by the runtime, not a layer
295 # cache: nothing downstream binds or registers it.
296 del kv_caches[cache_name]
297
298 if hot_backing is None or not cache_handles:
299 raise RuntimeError("HiSparse found no hot-cache handles.")
300 (source_group_id,) = kv_cache_config.host_group_ids
301 for cache_handle in cache_handles:
302 cache_handle.source_block_table = block_tables.input_block_tables[
303 source_group_id
304 ]
305 cache_handle.mirror_slot_mapping = block_tables.slot_mappings[source_group_id]
306 return cache_handles