main
py 398 lines 14.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from __future__ import annotations
5
6 from dataclasses import dataclass, field
7 from typing import Any
8
9 import torch
10
11 from vllm import _custom_ops as ops
12 from vllm.config import VllmConfig
13 from vllm.forward_context import in_piecewise_cudagraph
14 from vllm.utils.torch_utils import current_stream
15 from vllm.v1.attention.backend import max_decode_query_len
16 from vllm.v1.attention.backends.mla.sparse_utils import (
17 triton_convert_req_index_to_global_index,
18 )
19 from vllm.v1.hisparse.runtime import (
20 FP8_DS_MLA_ROW_BYTES,
21 HiSparseCacheHandle,
22 HiSparsePrefillStagingPlan,
23 create_hisparse_cache_handle,
24 )
25
26
27 def _create_side_stream(device: torch.device) -> torch.Stream:
28 return torch.Stream(device=device)
29
30
31 def _create_event() -> torch.Event:
32 return torch.Event()
33
34
35 @dataclass
36 class SparseMLAIndexGroup:
37 """Layers that consume one sparse-indexer result."""
38
39 logical_topk_indices: torch.Tensor
40 physical_topk_indices: torch.Tensor
41 valid_topk_counts: torch.Tensor
42 side_stream: torch.Stream
43 logical_topk_ready: torch.Event
44 physical_topk_ready: torch.Event
45 has_indexer: bool
46 num_layers: int = 0
47
48 def register_layer(
49 self,
50 vllm_config: VllmConfig | None = None,
51 *,
52 head_size: int | None = None,
53 kv_cache_dtype: str | None = None,
54 ) -> int:
55 layer_index = self.num_layers
56 self.num_layers += 1
57 return layer_index
58
59 def set_logical_topk_ready(self, layer_index: int) -> None:
60 if layer_index != 0 or not self.has_indexer:
61 return
62 # In piecewise cudagraph mode the consumer converts on the side stream
63 # from an eager break; an event recorded inside a captured segment is
64 # graph-local and any eager wait on it raises cudaErrorInvalidValue.
65 # The convert side falls back to wait_stream there instead (and never
66 # touches this event), so skip the record as well.
67 if in_piecewise_cudagraph():
68 return
69 self.logical_topk_ready.record(current_stream())
70
71 def prepare_for_batch(self, layer_index: int, attn_metadata: Any | None) -> None:
72 pass
73
74 def gather_fp8_prefill(
75 self,
76 layer_index: int,
77 source_cache: torch.Tensor,
78 dst: torch.Tensor,
79 block_table: torch.Tensor,
80 workspace_starts: torch.Tensor,
81 batch_size: int,
82 attn_metadata: Any,
83 request_start: int,
84 ) -> torch.Event | None:
85 ops.cp_gather_and_upconvert_fp8_kv_cache(
86 source_cache,
87 dst,
88 block_table,
89 workspace_starts,
90 batch_size,
91 )
92 return None
93
94 def convert_logical_to_physical_topk(
95 self,
96 layer_index: int,
97 logical_topk_indices: torch.Tensor,
98 attn_metadata: Any,
99 *,
100 block_stride_rows: int | None,
101 return_valid_counts: bool,
102 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
103 """Convert one index result once for all layers in this group."""
104 return self._convert_once(
105 layer_index,
106 logical_topk_indices,
107 attn_metadata.req_id_per_token[: logical_topk_indices.shape[0]],
108 attn_metadata.block_table,
109 attn_metadata.block_size,
110 block_stride_rows=block_stride_rows,
111 return_valid_counts=return_valid_counts,
112 )
113
114 def _convert_once(
115 self,
116 layer_index: int,
117 logical_topk_indices: torch.Tensor,
118 req_id_per_token: torch.Tensor,
119 block_table: torch.Tensor,
120 block_size: int,
121 *,
122 block_stride_rows: int | None,
123 return_valid_counts: bool,
124 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
125 num_tokens = logical_topk_indices.shape[0]
126 if num_tokens > self.physical_topk_indices.shape[0]:
127 return triton_convert_req_index_to_global_index(
128 req_id_per_token,
129 block_table,
130 logical_topk_indices,
131 BLOCK_SIZE=block_size,
132 BLOCK_STRIDE_ROWS=block_stride_rows,
133 NUM_TOPK_TOKENS=logical_topk_indices.shape[1],
134 return_valid_counts=return_valid_counts,
135 )
136
137 physical_topk_indices = self.physical_topk_indices[:num_tokens]
138 valid_topk_counts = self.valid_topk_counts[:num_tokens]
139 compute_stream = current_stream()
140 if layer_index == 0:
141 if self.has_indexer and not in_piecewise_cudagraph():
142 self.side_stream.wait_event(self.logical_topk_ready)
143 else:
144 self.side_stream.wait_stream(compute_stream)
145 with self.side_stream:
146 triton_convert_req_index_to_global_index(
147 req_id_per_token,
148 block_table,
149 logical_topk_indices,
150 BLOCK_SIZE=block_size,
151 BLOCK_STRIDE_ROWS=block_stride_rows,
152 NUM_TOPK_TOKENS=logical_topk_indices.shape[1],
153 return_valid_counts=return_valid_counts,
154 out=physical_topk_indices,
155 valid_counts_out=(
156 valid_topk_counts if return_valid_counts else None
157 ),
158 )
159 self.physical_topk_ready.record(self.side_stream)
160 compute_stream.wait_event(self.physical_topk_ready)
161
162 if return_valid_counts:
163 return physical_topk_indices, valid_topk_counts
164 return physical_topk_indices
165
166
167 @dataclass
168 class HiSparseMLAIndexGroup(SparseMLAIndexGroup):
169 """Sparse index group with per-layer host-backed KV caches."""
170
171 caches: list[HiSparseCacheHandle] = field(default_factory=list)
172 hisparse_group: Any | None = None
173 prefill_stream: torch.Stream | None = None
174 prefill_ready_events: list[torch.Event] = field(default_factory=list)
175
176 def register_layer(
177 self,
178 vllm_config: VllmConfig | None = None,
179 *,
180 head_size: int | None = None,
181 kv_cache_dtype: str | None = None,
182 ) -> int:
183 assert vllm_config is not None
184 assert head_size is not None
185 assert kv_cache_dtype is not None
186 layer_index = super().register_layer()
187 if self.prefill_stream is None:
188 self.prefill_stream = _create_side_stream(self.logical_topk_indices.device)
189 if kv_cache_dtype == "fp8_ds_mla":
190 row_width = FP8_DS_MLA_ROW_BYTES
191 kv_dtype = torch.uint8
192 else:
193 from vllm.utils.torch_utils import kv_cache_dtype_str_to_dtype
194
195 row_width = head_size
196 kv_dtype = kv_cache_dtype_str_to_dtype(
197 kv_cache_dtype, vllm_config.model_config
198 )
199 cache = create_hisparse_cache_handle(
200 vllm_config,
201 self.logical_topk_indices.shape[1],
202 is_index_group_leader=layer_index == 0,
203 row_width=row_width,
204 kv_dtype=kv_dtype,
205 index_group=self,
206 )
207 assert cache is not None
208 self.caches.append(cache)
209 cache.index_group_caches = self.caches
210 self.prefill_ready_events.append(_create_event())
211 return layer_index
212
213 def cache(self, layer_index: int) -> HiSparseCacheHandle:
214 return self.caches[layer_index]
215
216 def physical_kv_cache(self, layer_index: int) -> torch.Tensor:
217 cache = self.cache(layer_index)
218 return cache.runtime.hot.attention_cache
219
220 def prepare_for_batch(self, layer_index: int, attn_metadata: Any | None) -> None:
221 if layer_index == 0:
222 self.cache(0).prepare_group_for_batch(attn_metadata)
223
224 def convert_logical_to_physical_topk(
225 self,
226 layer_index: int,
227 logical_topk_indices: torch.Tensor,
228 attn_metadata: Any,
229 *,
230 block_stride_rows: int | None,
231 return_valid_counts: bool,
232 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
233 cache = self.cache(layer_index)
234 num_tokens = logical_topk_indices.shape[0]
235 req_id_per_token = attn_metadata.req_id_per_token[:num_tokens]
236 if num_tokens > self.physical_topk_indices.shape[0]:
237 # Prefill-sized batches do not fit the decode residency workspace.
238 # Non-resident prefills are staged before reaching this path.
239 assert cache.all_context_pages_resident
240 leader = self.cache(0)
241 assert leader.view is not None and leader.block_table is not None
242 return self._convert_once(
243 layer_index,
244 logical_topk_indices,
245 req_id_per_token,
246 leader.block_table,
247 leader.view.block_size,
248 block_stride_rows=leader.view.attention_block_stride,
249 return_valid_counts=return_valid_counts,
250 )
251 source_block_table = cache.source_block_table
252 assert source_block_table is not None
253 return cache.swap_in(
254 req_id_per_token,
255 block_table=source_block_table,
256 logical_topk_indices=logical_topk_indices,
257 block_size=attn_metadata.block_size,
258 return_valid_counts=return_valid_counts,
259 num_valid_rows=attn_metadata.query_start_loc[-1:],
260 )
261
262 def stage_prefill_rows(
263 self, layer_index: int, kv_cache: torch.Tensor, attn_metadata: Any
264 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
265 cache = self.cache(layer_index)
266 prefill = attn_metadata.prefill
267 staging_plan = prefill.host_staging_plan if prefill is not None else None
268 assert staging_plan is not None
269 resident_cache = None
270 if cache.view is not None and cache.block_table is not None:
271 staging_plan.ensure_gpu_sources(
272 cache.block_table[attn_metadata.num_decodes :],
273 cache.view.block_size,
274 )
275 resident_cache = cache.view.cache
276 staged_cache = cache.runtime.gather_prefill_cache(
277 kv_cache,
278 staging_plan,
279 resident_cache=resident_cache,
280 )
281 req_ids = attn_metadata.req_id_per_token[attn_metadata.num_decode_tokens :]
282 if attn_metadata.num_decodes > 0:
283 req_ids = req_ids - attn_metadata.num_decodes
284 return staged_cache, staging_plan.block_table, req_ids
285
286 def _prefill_gather_plan(
287 self, layer_index: int, attn_metadata: Any
288 ) -> HiSparsePrefillStagingPlan:
289 cache = self.cache(layer_index)
290 prefill = attn_metadata.prefill
291 plan = prefill.host_staging_plan if prefill is not None else None
292 assert plan is not None
293 assert cache.view is not None and cache.block_table is not None
294 plan.ensure_gpu_sources(
295 cache.block_table[attn_metadata.num_decodes :],
296 cache.view.block_size,
297 )
298 assert plan.gpu_row_ids is not None
299 return plan
300
301 def gather_fp8_prefill(
302 self,
303 layer_index: int,
304 source_cache: torch.Tensor,
305 dst: torch.Tensor,
306 block_table: torch.Tensor,
307 workspace_starts: torch.Tensor,
308 batch_size: int,
309 attn_metadata: Any,
310 request_start: int,
311 ) -> torch.Event | None:
312 cache = self.cache(layer_index)
313 plan = self._prefill_gather_plan(layer_index, attn_metadata)
314 assert cache.view is not None and plan.gpu_row_ids is not None
315 row_width = source_cache.shape[-1]
316 prefill_stream = self.prefill_stream
317 assert prefill_stream is not None
318 block_table = plan.block_table[request_start : request_start + batch_size]
319 prefill_stream.wait_stream(current_stream())
320 with prefill_stream:
321 ops.cp_gather_and_upconvert_fp8_kv_cache(
322 cache.view.cache,
323 dst,
324 block_table,
325 workspace_starts,
326 batch_size,
327 host_cache=source_cache.view(-1, row_width),
328 host_row_ids=plan.row_ids,
329 device_row_ids=plan.gpu_row_ids,
330 )
331 ready = self.prefill_ready_events[layer_index]
332 ready.record(prefill_stream)
333 return ready
334
335
336 class SparseMLAIndexGroupBuilder:
337 """Assign sparse MLA layers to their index-producing layer."""
338
339 def __init__(
340 self, logical_topk_indices: torch.Tensor, max_decode_rows: int | None = None
341 ) -> None:
342 self.logical_topk_indices = logical_topk_indices
343 self.max_decode_rows = (
344 logical_topk_indices.shape[0]
345 if max_decode_rows is None
346 else max_decode_rows
347 )
348 self.current_group: SparseMLAIndexGroup | None = None
349
350 def register_layer(
351 self,
352 is_index_producing_layer: bool,
353 vllm_config: VllmConfig | None = None,
354 *,
355 head_size: int | None = None,
356 kv_cache_dtype: str | None = None,
357 ) -> tuple[SparseMLAIndexGroup, int]:
358 if is_index_producing_layer or self.current_group is None:
359 group_cls = (
360 HiSparseMLAIndexGroup
361 if vllm_config is not None
362 and vllm_config.attention_config.hisparse_config is not None
363 else SparseMLAIndexGroup
364 )
365 workspace_rows = self.max_decode_rows
366 physical_topk_indices = torch.empty(
367 (workspace_rows + 1, self.logical_topk_indices.shape[1]),
368 dtype=self.logical_topk_indices.dtype,
369 device=self.logical_topk_indices.device,
370 )
371 self.current_group = group_cls(
372 logical_topk_indices=self.logical_topk_indices,
373 physical_topk_indices=physical_topk_indices,
374 valid_topk_counts=torch.empty(
375 workspace_rows + 1,
376 dtype=torch.int32,
377 device=self.logical_topk_indices.device,
378 ),
379 side_stream=_create_side_stream(self.logical_topk_indices.device),
380 logical_topk_ready=_create_event(),
381 physical_topk_ready=_create_event(),
382 has_indexer=is_index_producing_layer,
383 )
384 group = self.current_group
385 group_index = group.register_layer(
386 vllm_config,
387 head_size=head_size,
388 kv_cache_dtype=kv_cache_dtype,
389 )
390 return group, group_index
391
392
393 def get_sparse_mla_index_group_max_rows(vllm_config: VllmConfig) -> int:
394 scheduler_config = vllm_config.scheduler_config
395 return min(
396 scheduler_config.max_num_batched_tokens,
397 scheduler_config.max_num_seqs * max_decode_query_len(vllm_config),
398 )