main
py 696 lines 29.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Backend for GatedDeltaNet attention."""
4
5 from dataclasses import dataclass, replace
6 from typing import Literal
7
8 import torch
9
10 from vllm.config import VllmConfig
11 from vllm.model_executor.layers.mamba.checkpoint import (
12 MambaPrefillCheckpointBuilder,
13 MambaPrefillCheckpointMetadata,
14 )
15 from vllm.utils.torch_utils import async_tensor_h2d
16 from vllm.v1.attention.backend import (
17 AttentionBackend,
18 AttentionCGSupport,
19 AttentionMetadataBuilder,
20 CommonAttentionMetadata,
21 )
22 from vllm.v1.attention.backends.utils import (
23 NULL_BLOCK_ID,
24 compute_causal_conv1d_metadata,
25 mamba_get_block_table_tensor,
26 split_decodes_and_prefills,
27 )
28 from vllm.v1.kv_cache_interface import MambaSpec
29
30
31 class GDNAttentionBackend(AttentionBackend):
32 @staticmethod
33 def get_name() -> str:
34 return "GDN_ATTN"
35
36 @staticmethod
37 def get_builder_cls() -> type["GDNAttentionMetadataBuilder"]:
38 return GDNAttentionMetadataBuilder
39
40 @classmethod
41 def is_ssm(cls) -> bool:
42 return True
43
44
45 @dataclass
46 class GDNAttentionMetadata:
47 num_prefills: int
48 num_prefill_tokens: int
49 num_decodes: int
50 num_decode_tokens: int
51 num_spec_decodes: int
52 num_spec_decode_tokens: int
53 num_actual_tokens: int
54
55 checkpoint: MambaPrefillCheckpointMetadata | None = None
56 has_initial_state: torch.Tensor | None = None
57
58 spec_query_start_loc: torch.Tensor | None = None # shape: [num_spec_decodes + 1,]
59 non_spec_query_start_loc: torch.Tensor | None = (
60 None # shape: [batch - num_spec_decodes + 1,]
61 )
62
63 spec_state_indices_tensor: torch.Tensor | None = None # shape: [batch, num_spec]
64 non_spec_state_indices_tensor: torch.Tensor | None = (
65 None # shape: [batch - num_spec_decodes,]
66 )
67 spec_sequence_masks: torch.Tensor | None = None # shape: [batch,]
68 spec_sequence_masks_cpu: torch.Tensor | None = None # shape: [batch,]
69 spec_token_indx: torch.Tensor | None = None
70 non_spec_token_indx: torch.Tensor | None = None
71
72 num_accepted_tokens: torch.Tensor | None = None # shape: [batch,]
73 uniform_spec_sequence_length: int | None = None # None for ragged batches
74
75 # Pre-computed FLA chunk metadata (avoids GPU->CPU sync in prepare_chunk_indices)
76 chunk_indices: torch.Tensor | None = None
77 chunk_offsets: torch.Tensor | None = None
78 # Chunk-kernel inputs for prefill
79 prefill_query_start_loc: torch.Tensor | None = None
80 prefill_state_indices: torch.Tensor | None = None
81 prefill_has_initial_state: torch.Tensor | None = None
82
83 # The following attributes are for triton implementation of causal_conv1d
84 nums_dict: dict | None = None
85 batch_ptr: torch.Tensor | None = None
86 token_chunk_offset_ptr: torch.Tensor | None = None
87
88
89 class GDNAttentionMetadataBuilder(AttentionMetadataBuilder[GDNAttentionMetadata]):
90 kv_cache_spec: MambaSpec
91 _cudagraph_support = AttentionCGSupport.UNIFORM_BATCH
92
93 reorder_batch_threshold: int = 1
94
95 def __init__(
96 self,
97 kv_cache_spec: MambaSpec,
98 layer_names: list[str],
99 vllm_config: VllmConfig,
100 device: torch.device,
101 ):
102 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
103 self.compilation_config = vllm_config.compilation_config
104 self.speculative_config = vllm_config.speculative_config
105 self.checkpoint_builder = MambaPrefillCheckpointBuilder(
106 vllm_config, kv_cache_spec
107 )
108 from vllm.model_executor.layers.mamba.gdn.qwen_gdn_linear_attn import (
109 _resolve_gdn_prefill_backend,
110 )
111
112 self.gdn_prefill_backend: Literal["triton", "flashinfer", "cutedsl"]
113 _, self.gdn_prefill_backend = _resolve_gdn_prefill_backend(vllm_config)
114
115 if self.speculative_config:
116 assert self.speculative_config.num_speculative_tokens is not None
117 self.num_spec: int = self.speculative_config.num_speculative_tokens
118 else:
119 self.num_spec = 0
120 self.use_spec_decode: bool = self.num_spec > 0
121 self._init_reorder_batch_threshold(1, self.use_spec_decode)
122
123 self.use_full_cuda_graph: bool = (
124 self.compilation_config.cudagraph_mode.has_full_cudagraphs()
125 )
126 # update_block_table() keeps the source group's batch-level FULL graph
127 # buffers, so only MRV2, which also reuses metadata at capture, may use
128 # it.
129 self.supports_update_block_table = (
130 vllm_config.use_v2_model_runner
131 and device.type == "cuda"
132 # Not isinstance: KDA's RecoverSSM/checkpoint metadata is per group.
133 and type(self) is GDNAttentionMetadataBuilder
134 )
135 if self.supports_update_block_table:
136 # Opts into MRV2's CUDA-only aligned-index precompute.
137 self.mamba_aligned_state_indices: torch.Tensor | None = None
138
139 self.decode_cudagraph_max_bs: int = (
140 self.vllm_config.scheduler_config.max_num_seqs * (self.num_spec + 1)
141 )
142 if self.compilation_config.max_cudagraph_capture_size is not None:
143 self.decode_cudagraph_max_bs = min(
144 self.decode_cudagraph_max_bs,
145 self.compilation_config.max_cudagraph_capture_size,
146 )
147
148 self.spec_state_indices_tensor: torch.Tensor = torch.empty(
149 (self.decode_cudagraph_max_bs, self.num_spec + 1),
150 dtype=torch.int32,
151 device=device,
152 )
153 self.non_spec_state_indices_tensor: torch.Tensor = torch.empty(
154 (self.decode_cudagraph_max_bs,),
155 dtype=torch.int32,
156 device=device,
157 )
158 self.spec_sequence_masks: torch.Tensor = torch.empty(
159 (self.decode_cudagraph_max_bs,),
160 dtype=torch.bool,
161 device=device,
162 )
163 self.spec_token_indx: torch.Tensor = torch.empty(
164 (self.decode_cudagraph_max_bs * (self.num_spec + 1),),
165 dtype=torch.int32,
166 device=device,
167 )
168 self.non_spec_token_indx: torch.Tensor = torch.empty(
169 (self.decode_cudagraph_max_bs * (self.num_spec + 1),),
170 dtype=torch.int32,
171 device=device,
172 )
173 self.spec_query_start_loc: torch.Tensor = torch.empty(
174 (self.decode_cudagraph_max_bs + 1,),
175 dtype=torch.int32,
176 device=device,
177 )
178 self.non_spec_query_start_loc: torch.Tensor = torch.empty(
179 (self.decode_cudagraph_max_bs + 1,),
180 dtype=torch.int32,
181 device=device,
182 )
183 self.num_accepted_tokens: torch.Tensor = torch.empty(
184 (self.decode_cudagraph_max_bs,),
185 dtype=torch.int32,
186 device=device,
187 )
188
189 def _build_chunk_metadata(
190 self,
191 prefill_query_start_loc: torch.Tensor,
192 prefill_query_start_loc_cpu: torch.Tensor,
193 device: torch.device,
194 ) -> tuple[torch.Tensor, torch.Tensor]:
195 from vllm.third_party.flash_linear_attention.ops.utils import FLA_CHUNK_SIZE
196
197 if self.gdn_prefill_backend == "cutedsl":
198 from vllm.model_executor.layers.mamba.ops.gdn_chunk_cutedsl import (
199 prepare_metadata_cutedsl,
200 )
201
202 assert prefill_query_start_loc is not None
203 assert prefill_query_start_loc_cpu is not None
204 total_tokens = int(prefill_query_start_loc_cpu[-1].item())
205 return prepare_metadata_cutedsl(
206 prefill_query_start_loc,
207 total_tokens,
208 FLA_CHUNK_SIZE,
209 )
210
211 # Only prefill batches use FLA chunk ops.
212 # Pre-compute on CPU and async-copy to GPU to avoid
213 # GPU→CPU sync (.tolist()) in prepare_chunk_indices.
214 from vllm.third_party.flash_linear_attention.ops.index import (
215 prepare_chunk_indices,
216 prepare_chunk_offsets,
217 )
218
219 assert prefill_query_start_loc_cpu is not None
220 return (
221 async_tensor_h2d(
222 prepare_chunk_indices(prefill_query_start_loc_cpu, FLA_CHUNK_SIZE),
223 device=device,
224 ),
225 async_tensor_h2d(
226 prepare_chunk_offsets(prefill_query_start_loc_cpu, FLA_CHUNK_SIZE),
227 device=device,
228 ),
229 )
230
231 def build( # type: ignore[override]
232 self,
233 common_prefix_len: int,
234 common_attn_metadata: CommonAttentionMetadata,
235 num_accepted_tokens: torch.Tensor | None = None,
236 num_decode_draft_tokens_cpu: torch.Tensor | None = None,
237 fast_build: bool = False,
238 ) -> GDNAttentionMetadata:
239 m = common_attn_metadata
240
241 query_start_loc = m.query_start_loc
242 query_start_loc_cpu = m.query_start_loc_cpu
243 nums_dict, batch_ptr, token_chunk_offset_ptr = None, None, None
244 aligned_state_indices = getattr(self, "mamba_aligned_state_indices", None)
245 if aligned_state_indices is not None:
246 block_table_tensor = aligned_state_indices[: m.num_reqs]
247 else:
248 block_table_tensor = mamba_get_block_table_tensor(
249 m.block_table_tensor,
250 m.seq_lens,
251 self.kv_cache_spec,
252 self.vllm_config.cache_config.mamba_cache_mode,
253 )
254
255 uniform_spec_sequence_length = None
256 spec_sequence_masks_cpu: torch.Tensor | None = None
257 if not self.use_spec_decode or num_decode_draft_tokens_cpu is None:
258 spec_sequence_masks = None
259 num_spec_decodes = 0
260 else:
261 spec_sequence_masks_cpu = num_decode_draft_tokens_cpu >= 0
262 num_spec_decodes = spec_sequence_masks_cpu.sum().item()
263 if (
264 num_spec_decodes == 0
265 or num_decode_draft_tokens_cpu[spec_sequence_masks_cpu].sum().item()
266 == 0
267 ):
268 num_spec_decodes = 0
269 spec_sequence_masks = None
270 spec_sequence_masks_cpu = None
271 else:
272 spec_sequence_masks = async_tensor_h2d(
273 spec_sequence_masks_cpu, device=query_start_loc.device
274 )
275
276 if spec_sequence_masks is None:
277 # V2 already excludes prefills from full decode graphs via
278 # has_prefill. Classify first chunks as prefills to mask recycled
279 # state; resumed one-token chunks can still use the decode kernels.
280 assert m.seq_lens_cpu_upper_bound is not None
281 query_lens_cpu = query_start_loc_cpu.diff()
282 no_prior_state = (query_lens_cpu > 0) & (
283 m.seq_lens_cpu_upper_bound <= query_lens_cpu
284 )
285 # Capture batches also have seq_len == query_len, but are not
286 # prefills.
287 if m.is_prefilling is not None:
288 no_prior_state &= m.is_prefilling
289 else:
290 no_prior_state = torch.zeros_like(no_prior_state)
291 num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
292 split_decodes_and_prefills(
293 m.replace(is_prefilling=no_prior_state),
294 decode_threshold=1,
295 treat_short_extends_as_decodes=False,
296 )
297 )
298 # Exclude trailing padding from both prefill counts.
299 if num_prefills:
300 num_prefills -= int((query_lens_cpu[num_decodes:] == 0).sum())
301 num_prefill_tokens = (
302 int(query_start_loc_cpu[num_decodes + num_prefills])
303 - num_decode_tokens
304 )
305 num_spec_decode_tokens = 0
306 spec_token_indx = None
307 non_spec_token_indx = None
308 spec_state_indices_tensor = None
309 non_spec_state_indices_tensor = block_table_tensor[:, 0]
310 spec_query_start_loc = None
311 non_spec_query_start_loc = query_start_loc
312 non_spec_query_start_loc_cpu = query_start_loc_cpu
313 num_accepted_tokens = None
314 else:
315 query_lens = query_start_loc[1:] - query_start_loc[:-1]
316 assert spec_sequence_masks_cpu is not None
317 non_spec_sequence_masks_cpu = ~spec_sequence_masks_cpu
318 query_lens_cpu = query_start_loc_cpu[1:] - query_start_loc_cpu[:-1]
319 spec_query_lens_cpu = query_lens_cpu[spec_sequence_masks_cpu]
320 if spec_query_lens_cpu.numel() > 0:
321 first_spec_sequence_length = int(spec_query_lens_cpu[0])
322 if first_spec_sequence_length > 0 and bool(
323 torch.all(spec_query_lens_cpu == first_spec_sequence_length)
324 ):
325 uniform_spec_sequence_length = first_spec_sequence_length
326
327 # Use CPU tensors to avoid CPU-GPU sync
328 non_spec_query_lens_cpu = query_lens_cpu[non_spec_sequence_masks_cpu]
329 num_decodes = (non_spec_query_lens_cpu == 1).sum().item()
330 # Exclude zero-length padded sequences from prefill count.
331 num_zero_len = (non_spec_query_lens_cpu == 0).sum().item()
332 num_prefills = non_spec_query_lens_cpu.size(0) - num_decodes - num_zero_len
333 num_decode_tokens = num_decodes
334 num_prefill_tokens = (
335 non_spec_query_lens_cpu.sum().item() - num_decode_tokens
336 )
337 num_spec_decode_tokens = (
338 query_lens_cpu.sum().item() - num_prefill_tokens - num_decode_tokens
339 )
340
341 # num_decodes and num_spec_decodes are mutually exclusive.
342 # Reclassify non-spec decodes as prefills when spec decodes
343 # exist — the prefill kernel handles 1-token sequences with
344 # initial state correctly, producing identical results.
345 if num_decodes > 0 and num_spec_decodes > 0:
346 num_prefills += num_decodes
347 num_prefill_tokens += num_decode_tokens
348 num_decodes = 0
349 num_decode_tokens = 0
350
351 if num_prefills == 0 and num_decodes == 0:
352 spec_token_size = min(
353 num_spec_decodes * (self.num_spec + 1),
354 query_start_loc_cpu[-1].item(),
355 )
356 spec_token_indx = torch.arange(
357 spec_token_size,
358 dtype=torch.int32,
359 device=query_start_loc.device,
360 )
361 non_spec_token_indx = torch.empty(
362 0, dtype=torch.int32, device=query_start_loc.device
363 )
364 # Padded sequences trail the spec decodes, so slice them off
365 # rather than gather with the host mask (an H2D copy + a kernel).
366 spec_state_indices_tensor = block_table_tensor[
367 :num_spec_decodes, : self.num_spec + 1
368 ]
369 non_spec_state_indices_tensor = None
370 # Padded sequences are always at the back, so the first
371 # num_spec_decodes + 1 entries of query_start_loc already
372 # contain the correct cumulative token counts.
373 spec_query_start_loc = query_start_loc[: num_spec_decodes + 1]
374 non_spec_query_start_loc = None
375 non_spec_query_start_loc_cpu = None
376 else:
377 spec_token_masks = torch.repeat_interleave(
378 spec_sequence_masks,
379 query_lens,
380 output_size=query_start_loc_cpu[-1].item(),
381 )
382 index = torch.argsort(spec_token_masks, stable=True)
383 num_non_spec_tokens = num_prefill_tokens + num_decode_tokens
384 non_spec_token_indx = index[:num_non_spec_tokens]
385 spec_token_indx = index[num_non_spec_tokens:]
386
387 spec_state_indices_tensor = block_table_tensor[
388 spec_sequence_masks_cpu, : self.num_spec + 1
389 ]
390 non_spec_state_indices_tensor = block_table_tensor[
391 non_spec_sequence_masks_cpu, 0
392 ]
393
394 spec_query_start_loc = torch.zeros(
395 num_spec_decodes + 1,
396 dtype=torch.int32,
397 device=query_start_loc.device,
398 )
399 torch.cumsum(
400 query_lens[spec_sequence_masks_cpu],
401 dim=0,
402 out=spec_query_start_loc[1:],
403 )
404 non_spec_query_start_loc = torch.zeros(
405 query_lens.size(0) - num_spec_decodes + 1,
406 dtype=torch.int32,
407 device=query_start_loc.device,
408 )
409 torch.cumsum(
410 query_lens[non_spec_sequence_masks_cpu],
411 dim=0,
412 out=non_spec_query_start_loc[1:],
413 )
414 non_spec_query_start_loc_cpu = torch.zeros(
415 query_lens_cpu.size(0) - num_spec_decodes + 1,
416 dtype=torch.int32,
417 )
418 torch.cumsum(
419 query_lens_cpu[non_spec_sequence_masks_cpu],
420 dim=0,
421 out=non_spec_query_start_loc_cpu[1:],
422 )
423
424 assert num_accepted_tokens is not None
425 if num_prefills == 0 and num_decodes == 0:
426 num_accepted_tokens = num_accepted_tokens[:num_spec_decodes]
427 else:
428 num_accepted_tokens = num_accepted_tokens[spec_sequence_masks_cpu]
429
430 chunk_indices: torch.Tensor | None = None
431 chunk_offsets: torch.Tensor | None = None
432 prefill_query_start_loc: torch.Tensor | None = None
433 prefill_state_indices: torch.Tensor | None = None
434 prefill_has_initial_state: torch.Tensor | None = None
435 if num_prefills > 0:
436 # In a mixed non-spec batch, decodes are peeled off to the recurrent
437 # kernel (decode-first front slice), so build chunk metadata from the
438 # rebased prefill-only cu_seqlens; otherwise use the full non-spec one.
439 # _forward_core keys off the same condition, so they agree.
440 if spec_sequence_masks is None and num_decodes > 0:
441 assert non_spec_query_start_loc is not None
442 assert non_spec_query_start_loc_cpu is not None
443 assert non_spec_state_indices_tensor is not None
444 prefill_query_start_loc = (
445 non_spec_query_start_loc[num_decodes:] - num_decode_tokens
446 )
447 prefill_query_start_loc_cpu = (
448 non_spec_query_start_loc_cpu[num_decodes:] - num_decode_tokens
449 )
450 prefill_state_indices = non_spec_state_indices_tensor[num_decodes:]
451 else:
452 prefill_query_start_loc = non_spec_query_start_loc
453 prefill_query_start_loc_cpu = non_spec_query_start_loc_cpu
454 prefill_state_indices = non_spec_state_indices_tensor
455
456 chunk_indices, chunk_offsets = self._build_chunk_metadata(
457 prefill_query_start_loc,
458 prefill_query_start_loc_cpu,
459 query_start_loc.device,
460 )
461
462 if num_prefills > 0:
463 context_lens_tensor = m.compute_num_computed_tokens()
464 has_initial_state = context_lens_tensor > 0
465 if spec_sequence_masks_cpu is not None:
466 has_initial_state = has_initial_state[~spec_sequence_masks_cpu]
467 assert non_spec_query_start_loc_cpu is not None
468 nums_dict, batch_ptr, token_chunk_offset_ptr = (
469 compute_causal_conv1d_metadata(
470 non_spec_query_start_loc_cpu,
471 device=query_start_loc.device,
472 )
473 )
474 if spec_sequence_masks is None and num_decodes > 0:
475 prefill_has_initial_state = has_initial_state[num_decodes:]
476 else:
477 prefill_has_initial_state = has_initial_state
478 else:
479 has_initial_state = None
480
481 checkpoint: MambaPrefillCheckpointMetadata | None = None
482 if num_prefills > 0:
483 request_rows = list(range(m.num_reqs))
484 if spec_sequence_masks_cpu is not None:
485 request_rows = (~spec_sequence_masks_cpu).nonzero().flatten().tolist()
486 checkpoint = self.checkpoint_builder.build(m, request_rows)
487
488 # Function code counted on either presency non-spec decode or spec decode,
489 # but not both.
490 assert not (num_decodes > 0 and num_spec_decodes > 0), (
491 f"num_decodes: {num_decodes}, num_spec_decodes: {num_spec_decodes}"
492 )
493
494 # Prepare per-request tensors for cudagraph. m.num_actual_tokens is
495 # token-padded for FULL graph replay, but the GDN state/query/accepted
496 # metadata below is indexed by request.
497 batch_size = m.num_reqs
498
499 if self._stage_spec_decode(
500 num_prefills, num_decodes, num_spec_decodes, num_spec_decode_tokens
501 ):
502 assert spec_sequence_masks is not None
503 self.spec_state_indices_tensor[:num_spec_decodes].copy_(
504 spec_state_indices_tensor, non_blocking=True
505 )
506 spec_state_indices_tensor = self.spec_state_indices_tensor[:batch_size]
507 spec_state_indices_tensor[num_spec_decodes:].fill_(NULL_BLOCK_ID)
508
509 self.spec_sequence_masks[:num_spec_decodes].copy_(
510 spec_sequence_masks[:num_spec_decodes], non_blocking=True
511 )
512 spec_sequence_masks = self.spec_sequence_masks[:batch_size]
513 spec_sequence_masks[num_spec_decodes:].fill_(False)
514
515 assert non_spec_token_indx is not None and spec_token_indx is not None
516 self.non_spec_token_indx[: non_spec_token_indx.size(0)].copy_(
517 non_spec_token_indx, non_blocking=True
518 )
519 non_spec_token_indx = self.non_spec_token_indx[
520 : non_spec_token_indx.size(0)
521 ]
522
523 self.spec_token_indx[: spec_token_indx.size(0)].copy_(
524 spec_token_indx, non_blocking=True
525 )
526 spec_token_indx = self.spec_token_indx[: spec_token_indx.size(0)]
527
528 self.spec_query_start_loc[: num_spec_decodes + 1].copy_(
529 spec_query_start_loc, non_blocking=True
530 )
531 spec_num_query_tokens = spec_query_start_loc[-1] # type: ignore[index]
532 spec_query_start_loc = self.spec_query_start_loc[: batch_size + 1]
533 spec_query_start_loc[num_spec_decodes + 1 :].fill_(spec_num_query_tokens)
534
535 self.num_accepted_tokens[:num_spec_decodes].copy_(
536 num_accepted_tokens, non_blocking=True
537 )
538 num_accepted_tokens = self.num_accepted_tokens[:batch_size]
539 num_accepted_tokens[num_spec_decodes:].fill_(1)
540
541 if self._stage_decode(num_prefills, num_decodes, num_spec_decodes):
542 self.non_spec_state_indices_tensor[:num_decodes].copy_(
543 non_spec_state_indices_tensor, non_blocking=True
544 )
545 non_spec_state_indices_tensor = self.non_spec_state_indices_tensor[
546 :batch_size
547 ]
548 non_spec_state_indices_tensor[num_decodes:].fill_(NULL_BLOCK_ID)
549
550 self.non_spec_query_start_loc[: num_decodes + 1].copy_(
551 non_spec_query_start_loc, non_blocking=True
552 )
553 non_spec_num_query_tokens = non_spec_query_start_loc[-1] # type: ignore[index]
554 non_spec_query_start_loc = self.non_spec_query_start_loc[: batch_size + 1]
555 non_spec_query_start_loc[num_decodes + 1 :].fill_(non_spec_num_query_tokens)
556
557 attn_metadata = GDNAttentionMetadata(
558 num_prefills=num_prefills,
559 num_prefill_tokens=num_prefill_tokens,
560 num_decodes=num_decodes,
561 num_decode_tokens=num_decode_tokens,
562 num_spec_decodes=num_spec_decodes,
563 num_spec_decode_tokens=num_spec_decode_tokens,
564 num_actual_tokens=m.num_actual_tokens,
565 checkpoint=checkpoint,
566 has_initial_state=has_initial_state,
567 chunk_indices=chunk_indices,
568 chunk_offsets=chunk_offsets,
569 prefill_query_start_loc=prefill_query_start_loc,
570 prefill_state_indices=prefill_state_indices,
571 prefill_has_initial_state=prefill_has_initial_state,
572 spec_query_start_loc=spec_query_start_loc,
573 non_spec_query_start_loc=non_spec_query_start_loc,
574 spec_state_indices_tensor=spec_state_indices_tensor,
575 non_spec_state_indices_tensor=non_spec_state_indices_tensor,
576 spec_sequence_masks=spec_sequence_masks,
577 spec_sequence_masks_cpu=spec_sequence_masks_cpu,
578 spec_token_indx=spec_token_indx,
579 non_spec_token_indx=non_spec_token_indx,
580 num_accepted_tokens=num_accepted_tokens,
581 uniform_spec_sequence_length=uniform_spec_sequence_length,
582 nums_dict=nums_dict,
583 batch_ptr=batch_ptr,
584 token_chunk_offset_ptr=token_chunk_offset_ptr,
585 )
586 return attn_metadata
587
588 def _stage_spec_decode(
589 self,
590 num_prefills: int,
591 num_decodes: int,
592 num_spec_decodes: int,
593 num_spec_decode_tokens: int,
594 ) -> bool:
595 """Whether spec-decode metadata goes into the FULL cudagraph buffers."""
596 return (
597 self.use_full_cuda_graph
598 and num_prefills == 0
599 and num_decodes == 0
600 and num_spec_decodes <= self.decode_cudagraph_max_bs
601 and num_spec_decode_tokens <= self.decode_cudagraph_max_bs
602 )
603
604 def _stage_decode(
605 self, num_prefills: int, num_decodes: int, num_spec_decodes: int
606 ) -> bool:
607 """Whether decode metadata goes into the FULL cudagraph buffers."""
608 return (
609 self.use_full_cuda_graph
610 and num_prefills == 0
611 and num_spec_decodes == 0
612 and num_decodes <= self.decode_cudagraph_max_bs
613 )
614
615 def update_block_table(
616 self,
617 metadata: GDNAttentionMetadata,
618 blk_table: torch.Tensor,
619 slot_mapping: torch.Tensor,
620 ) -> GDNAttentionMetadata:
621 """Re-gather this group's state indices. The other fields are
622 batch-level and stay shared with ``metadata``."""
623 m = metadata
624 checkpoint = (
625 m.checkpoint.regather_state_indices(blk_table)
626 if m.checkpoint is not None
627 else None
628 )
629 if self.vllm_config.cache_config.mamba_cache_mode == "align":
630 assert self.mamba_aligned_state_indices is not None
631 blk_table = self.mamba_aligned_state_indices
632 masks = m.spec_sequence_masks_cpu
633 spec_indices = non_spec_indices = prefill_indices = None
634 if masks is None:
635 non_spec_indices = blk_table[:, 0]
636 if m.num_prefills > 0:
637 prefill_indices = non_spec_indices[m.num_decodes :]
638 elif m.num_prefills == 0:
639 # Same as build(): padded sequences trail the spec decodes.
640 spec_indices = blk_table[: m.num_spec_decodes, : self.num_spec + 1]
641 else:
642 spec_indices = blk_table[masks, : self.num_spec + 1]
643 non_spec_indices = prefill_indices = blk_table[~masks, 0]
644
645 if self._stage_spec_decode(
646 m.num_prefills, m.num_decodes, m.num_spec_decodes, m.num_spec_decode_tokens
647 ):
648 assert m.spec_state_indices_tensor is not None
649 batch_size = m.spec_state_indices_tensor.shape[0]
650 self.spec_state_indices_tensor[: m.num_spec_decodes].copy_(
651 spec_indices, non_blocking=True
652 )
653 spec_indices = self.spec_state_indices_tensor[:batch_size]
654 spec_indices[m.num_spec_decodes :].fill_(NULL_BLOCK_ID)
655
656 if self._stage_decode(m.num_prefills, m.num_decodes, m.num_spec_decodes):
657 assert m.non_spec_state_indices_tensor is not None
658 batch_size = m.non_spec_state_indices_tensor.shape[0]
659 self.non_spec_state_indices_tensor[: m.num_decodes].copy_(
660 non_spec_indices, non_blocking=True
661 )
662 non_spec_indices = self.non_spec_state_indices_tensor[:batch_size]
663 non_spec_indices[m.num_decodes :].fill_(NULL_BLOCK_ID)
664
665 return replace(
666 m,
667 spec_state_indices_tensor=spec_indices,
668 non_spec_state_indices_tensor=non_spec_indices,
669 prefill_state_indices=prefill_indices,
670 checkpoint=checkpoint,
671 )
672
673 def build_for_cudagraph_capture(
674 self, common_attn_metadata: CommonAttentionMetadata
675 ):
676 """This method builds the metadata for full cudagraph capture.
677 Currently, only decode is supported for full cudagraphs with Mamba.
678 """
679 m = common_attn_metadata
680
681 assert (
682 m.num_reqs <= self.decode_cudagraph_max_bs
683 and m.num_actual_tokens <= self.decode_cudagraph_max_bs
684 ), (
685 f"GDN only supports decode-only full CUDAGraph capture. "
686 f"Make sure batch size ({m.num_reqs}) <= "
687 f"cudagraph capture sizes ({self.decode_cudagraph_max_bs}), "
688 f"and number of tokens ({m.num_actual_tokens}) <= "
689 f"cudagraph capture sizes ({self.decode_cudagraph_max_bs})."
690 )
691
692 num_accepted_tokens = torch.diff(m.query_start_loc)
693 num_decode_draft_tokens_cpu = torch.diff(m.query_start_loc_cpu).sub_(1)
694 assert num_decode_draft_tokens_cpu.shape == num_accepted_tokens.shape
695
696 return self.build(0, m, num_accepted_tokens, num_decode_draft_tokens_cpu)