main
py 1,096 lines 40.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """b12x paged causal attention backend for SM12x."""
4
5 from __future__ import annotations
6
7 import copy
8 import math
9 from dataclasses import dataclass, replace
10 from functools import partial
11 from typing import Any, ClassVar
12
13 import torch
14
15 from vllm.config import VllmConfig, get_current_vllm_config
16 from vllm.config.cache import CacheDType
17 from vllm.logger import init_logger
18 from vllm.model_executor.warmup.cutedsl_warmup import (
19 CuTeDSLCompileUnit,
20 register_cutedsl_warmup_provider,
21 )
22 from vllm.platforms import current_platform
23 from vllm.platforms.interface import DeviceCapability
24 from vllm.utils.b12x import get_b12x_paged_attention
25 from vllm.utils.math_utils import cdiv
26 from vllm.utils.torch_utils import (
27 canonicalize_singleton_dim_strides,
28 get_dtype_size,
29 is_quantized_kv_cache,
30 )
31 from vllm.v1.attention.backend import (
32 AttentionBackend,
33 AttentionCGSupport,
34 AttentionImpl,
35 AttentionLayer,
36 AttentionMetadata,
37 AttentionMetadataBuilder,
38 AttentionType,
39 CommonAttentionMetadata,
40 MultipleOf,
41 )
42 from vllm.v1.kv_cache_interface import AttentionSpec, KVCacheLayout, KVCacheSpec
43 from vllm.v1.worker.workspace import current_workspace_manager
44
45 logger = init_logger(__name__)
46
47 _B12X_SUPPORTED_PAGE_SIZES = (64, 128)
48 _B12X_PREFERRED_PAGE_SIZE = 128
49 _MIN_PAGED_TILE_Q = 16
50 _B12X_FP8_KV_CACHE_DTYPES: tuple[CacheDType, ...] = ("fp8", "fp8_e4m3")
51 _B12X_SUPPORTED_KV_CACHE_DTYPES: tuple[CacheDType, ...] = (
52 "auto",
53 "bfloat16",
54 *_B12X_FP8_KV_CACHE_DTYPES,
55 )
56
57
58 def _max_page_table_width(
59 max_model_len: int,
60 block_size: int,
61 max_num_batched_tokens: int,
62 is_hybrid: bool,
63 ) -> int:
64 width = max(cdiv(max(max_model_len, 1), block_size), 1)
65 if is_hybrid:
66 # Hybrid cache setup can enlarge the storage block after attention
67 # layers are initialized. Its expansion into kernel-sized blocks adds
68 # at most one storage block of trailing page-table capacity.
69 width += cdiv(max_num_batched_tokens, block_size)
70 return width
71
72
73 def _kv_page_size(key_cache: torch.Tensor, value_cache: torch.Tensor) -> int:
74 """Return the static kernel page geometry negotiated by vLLM.
75
76 The KV manager can split the configured storage block into a smaller
77 kernel page when another backend shares its cache group. Cache shapes are
78 fixed before graph capture, so this is not a live-length policy decision.
79 """
80 if key_cache.ndim < 2 or value_cache.ndim < 2:
81 raise ValueError(
82 "b12x expects paged K/V caches with a page dimension, got "
83 f"{tuple(key_cache.shape)} and {tuple(value_cache.shape)}."
84 )
85 key_page_size = int(key_cache.shape[1])
86 value_page_size = int(value_cache.shape[1])
87 if key_page_size != value_page_size:
88 raise ValueError(
89 "b12x requires matching K/V page sizes, got "
90 f"{key_page_size} and {value_page_size}."
91 )
92 if key_page_size not in _B12X_SUPPORTED_PAGE_SIZES:
93 raise ValueError(
94 "b12x requires runtime page size in "
95 f"{_B12X_SUPPORTED_PAGE_SIZES}, got {key_page_size}."
96 )
97 return key_page_size
98
99
100 def _capture_alloc_forbidden() -> bool:
101 if not torch.cuda.is_available():
102 return False
103 try:
104 return bool(torch.cuda.is_current_stream_capturing())
105 except RuntimeError:
106 return False
107
108
109 def _ensure_i32_contiguous(tensor: torch.Tensor, name: str) -> torch.Tensor:
110 if tensor.dtype != torch.int32:
111 if _capture_alloc_forbidden():
112 raise RuntimeError(
113 f"b12x would convert {name} to int32 during CUDA graph "
114 "capture. Prepare int32 metadata before capture."
115 )
116 tensor = tensor.to(torch.int32)
117 if not tensor.is_contiguous():
118 if _capture_alloc_forbidden():
119 raise RuntimeError(
120 f"b12x would make {name} contiguous during CUDA graph "
121 "capture. Prepare contiguous metadata before capture."
122 )
123 tensor = tensor.contiguous()
124 return tensor
125
126
127 def _dtype_from_cache_config(
128 kv_cache_dtype: str,
129 vllm_config: VllmConfig,
130 ) -> torch.dtype:
131 if kv_cache_dtype == "bfloat16":
132 return torch.bfloat16
133 if kv_cache_dtype in _B12X_FP8_KV_CACHE_DTYPES:
134 return current_platform.fp8_dtype()
135 if kv_cache_dtype != "auto":
136 raise NotImplementedError(
137 "b12x currently supports only auto, bfloat16, "
138 "fp8, and fp8_e4m3 "
139 f"KV cache dtypes; got {kv_cache_dtype!r}."
140 )
141 return vllm_config.model_config.dtype
142
143
144 def _is_b12x_fp8_kv_cache(kv_cache_dtype: str) -> bool:
145 return kv_cache_dtype in _B12X_FP8_KV_CACHE_DTYPES
146
147
148 class B12xPagedAttentionBackend(AttentionBackend):
149 """b12x paged attention backend for regular/GQA decoder layers."""
150
151 @classmethod
152 def customize_spec(cls, spec: AttentionSpec) -> AttentionSpec:
153 if spec.state_content_bytes is not None:
154 return spec
155 assert spec.head_size == spec.head_size_v, (
156 "Separate K/V planes require symmetric K/V head sizes."
157 )
158 return replace(
159 spec,
160 num_head_slots=2,
161 state_content_bytes=spec.num_kv_heads
162 * spec.head_size
163 * get_dtype_size(spec.dtype),
164 )
165
166 supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
167 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = list(
168 _B12X_SUPPORTED_KV_CACHE_DTYPES
169 )
170
171 forward_includes_kv_cache_update: bool = False
172
173 @staticmethod
174 def get_name() -> str:
175 return "B12X"
176
177 @classmethod
178 def get_impl_cls(cls) -> type[B12xPagedAttentionImpl]:
179 return B12xPagedAttentionImpl
180
181 @staticmethod
182 def get_builder_cls() -> type[B12xPagedMetadataBuilder]:
183 return B12xPagedMetadataBuilder
184
185 @staticmethod
186 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
187 return list(_B12X_SUPPORTED_PAGE_SIZES)
188
189 @classmethod
190 def supports_block_size(cls, block_size: int | None) -> bool:
191 return block_size is None or int(block_size) in _B12X_SUPPORTED_PAGE_SIZES
192
193 @classmethod
194 def get_preferred_block_size(cls, default_block_size: int) -> int:
195 if int(default_block_size) in _B12X_SUPPORTED_PAGE_SIZES:
196 return int(default_block_size)
197 return _B12X_PREFERRED_PAGE_SIZE
198
199 @classmethod
200 def get_supported_head_sizes(cls) -> list[int]:
201 return [64, 128, 192, 256]
202
203 @classmethod
204 def supports_sink(cls) -> bool:
205 return True
206
207 @classmethod
208 def supports_sliding_window(cls) -> bool:
209 return True
210
211 @classmethod
212 def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
213 # Consumer Blackwell SM120 / SM121. The b12x paged kernels also gate
214 # internally, but keep vLLM selection fail-fast and explicit.
215 return (capability.major, capability.minor) in ((12, 0), (12, 1))
216
217 @classmethod
218 def supports_combination(
219 cls,
220 head_size: int,
221 dtype: torch.dtype,
222 kv_cache_dtype: CacheDType | None,
223 block_size: int | None,
224 use_mla: bool,
225 has_sink: bool,
226 use_sparse: bool,
227 use_mm_prefix: bool,
228 device_capability: DeviceCapability,
229 ) -> str | None:
230 if dtype != torch.bfloat16:
231 return "b12x currently requires bfloat16 queries"
232 if kv_cache_dtype == "float16":
233 return "b12x does not support float16 KV cache"
234 if (
235 kv_cache_dtype is not None
236 and is_quantized_kv_cache(kv_cache_dtype)
237 and not _is_b12x_fp8_kv_cache(kv_cache_dtype)
238 ):
239 return "b12x currently supports only fp8/fp8_e4m3 quantized KV cache dtypes"
240 paged_attention = get_b12x_paged_attention()
241 if paged_attention is None:
242 return "Install the b12x backend with `pip install vllm[b12x]`"
243 if not paged_attention.is_supported():
244 return "b12x paged attention is not supported on the current device"
245 return None
246
247 @classmethod
248 def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...]:
249 return (KVCacheLayout.LBHNC, KVCacheLayout.BLHNC)
250
251
252 @dataclass
253 class B12xPagedMetadata(AttentionMetadata):
254 num_actual_tokens: int
255 max_query_len: int
256 query_start_loc: torch.Tensor
257 max_seq_len: int
258 seq_lens: torch.Tensor
259 block_table: torch.Tensor
260 slot_mapping: torch.Tensor
261 causal: bool = True
262
263
264 class B12xPagedMetadataBuilder(AttentionMetadataBuilder[B12xPagedMetadata]):
265 """Metadata builder for b12x.
266
267 Decode and uniform speculative-verifier batches use preplanned graph
268 buckets. Extend/prefill remains eager and does not affect uniform decode
269 graph eligibility.
270 """
271
272 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
273 supports_update_block_table: bool = True
274
275 @classmethod
276 def get_cudagraph_support(
277 cls,
278 vllm_config: VllmConfig,
279 kv_cache_spec: KVCacheSpec,
280 ) -> AttentionCGSupport:
281 del vllm_config, kv_cache_spec
282 return cls._cudagraph_support
283
284 def __init__(
285 self,
286 kv_cache_spec: AttentionSpec,
287 layer_names: list[str],
288 vllm_config: VllmConfig,
289 device: torch.device,
290 ) -> None:
291 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
292
293 def build(
294 self,
295 common_prefix_len: int,
296 common_attn_metadata: CommonAttentionMetadata,
297 fast_build: bool = False,
298 ) -> B12xPagedMetadata:
299 del common_prefix_len, fast_build
300 cm = common_attn_metadata
301 return B12xPagedMetadata(
302 num_actual_tokens=cm.num_actual_tokens,
303 max_query_len=cm.max_query_len,
304 query_start_loc=cm.query_start_loc,
305 max_seq_len=cm.max_seq_len,
306 seq_lens=cm.seq_lens,
307 block_table=cm.block_table_tensor,
308 slot_mapping=cm.slot_mapping,
309 causal=cm.causal,
310 )
311
312 def update_block_table(
313 self,
314 metadata: B12xPagedMetadata,
315 blk_table: torch.Tensor,
316 slot_mapping: torch.Tensor,
317 ) -> B12xPagedMetadata:
318 new_metadata = copy.copy(metadata)
319 new_metadata.block_table = blk_table
320 new_metadata.slot_mapping = slot_mapping
321 return new_metadata
322
323
324 class B12xPagedAttentionImpl(AttentionImpl[B12xPagedMetadata]):
325 """b12x paged GQA attention implementation."""
326
327 can_return_lse_for_decode: bool = False
328 supports_dcp: bool = False
329
330 def __init__(
331 self,
332 num_heads: int,
333 head_size: int,
334 scale: float,
335 num_kv_heads: int,
336 alibi_slopes: list[float] | None,
337 sliding_window: int | None,
338 kv_cache_dtype: str,
339 logits_soft_cap: float | None = None,
340 attn_type: AttentionType = AttentionType.DECODER,
341 kv_sharing_target_layer_name: str | None = None,
342 sinks: torch.Tensor | None = None,
343 ) -> None:
344 if alibi_slopes is not None:
345 raise NotImplementedError("b12x does not support ALiBi.")
346 if logits_soft_cap not in (None, 0):
347 raise NotImplementedError("b12x does not support logits soft cap.")
348 if attn_type != AttentionType.DECODER:
349 raise NotImplementedError(
350 "b12x currently supports decoder self-attention only."
351 )
352 if is_quantized_kv_cache(kv_cache_dtype) and not _is_b12x_fp8_kv_cache(
353 kv_cache_dtype
354 ):
355 raise NotImplementedError(
356 "b12x currently supports only fp8/fp8_e4m3 quantized KV cache dtypes."
357 )
358 if num_heads % num_kv_heads != 0:
359 raise ValueError("b12x requires q heads divisible by kv heads.")
360
361 expected_scale = head_size**-0.5
362 if not math.isclose(float(scale), expected_scale, rel_tol=1e-5, abs_tol=1e-7):
363 raise NotImplementedError(
364 "b12x currently requires canonical softmax scale "
365 f"head_dim**-0.5={expected_scale}, got {scale}."
366 )
367 if self.total_cp_world_size > 1:
368 raise NotImplementedError(
369 "b12x does not yet support decode/prefill context parallelism."
370 )
371
372 self.num_heads = int(num_heads)
373 self.head_size = int(head_size)
374 self.output_head_size = self.head_size
375 self.scale = float(scale)
376 self.num_kv_heads = int(num_kv_heads)
377 self.num_queries_per_kv = self.num_heads // self.num_kv_heads
378 self.kv_cache_dtype = kv_cache_dtype
379 self.attn_type = attn_type
380 self.kv_sharing_target_layer_name = kv_sharing_target_layer_name
381 self.window_left = -1 if sliding_window is None else int(sliding_window) - 1
382
383 self._sinks_source = sinks
384 if sinks is not None and (
385 sinks.ndim != 1 or int(sinks.shape[0]) != self.num_heads
386 ):
387 raise ValueError(
388 "b12x sinks must have shape "
389 f"[{self.num_heads}], got {tuple(sinks.shape)}."
390 )
391 self.sinks = sinks if sinks is None or sinks.dtype == torch.float32 else None
392
393 vllm_config = get_current_vllm_config()
394 scheduler_config = vllm_config.scheduler_config
395 model_config = vllm_config.model_config
396 cache_config = vllm_config.cache_config
397 spec_config = vllm_config.speculative_config
398 default_block_size = int(cache_config.block_size)
399 if default_block_size not in _B12X_SUPPORTED_PAGE_SIZES:
400 raise ValueError(
401 "b12x requires --block-size in "
402 f"{_B12X_SUPPORTED_PAGE_SIZES}, got "
403 f"{cache_config.block_size}."
404 )
405
406 self.device = torch.device("cuda", torch.accelerator.current_device_index())
407 self.dtype = model_config.dtype
408 self.kv_torch_dtype = _dtype_from_cache_config(kv_cache_dtype, vllm_config)
409 if self.dtype != torch.bfloat16:
410 raise NotImplementedError("b12x currently requires bfloat16 queries.")
411 max_batched = int(scheduler_config.max_num_batched_tokens)
412 max_num_seqs = int(scheduler_config.max_num_seqs)
413 max_model_len = int(model_config.max_model_len)
414 self._max_num_seqs = max_num_seqs
415 max_page_table_widths = {
416 page_size: _max_page_table_width(
417 max_model_len,
418 page_size,
419 max_batched,
420 model_config.is_hybrid,
421 )
422 for page_size in _B12X_SUPPORTED_PAGE_SIZES
423 }
424
425 # Extend dispatch may depend on the static Q tensor capacity, but never
426 # on live per-request lengths. Keep a small set of capacity buckets so
427 # short/tail prefills do not replay the maximum 8K CTA grid.
428 self._extend_q_capacities = tuple(
429 sorted(
430 {
431 min(max_batched, q_capacity)
432 for q_capacity in (128, 512, 1024, 2048, 4096, max_batched)
433 if q_capacity > 0
434 }
435 )
436 )
437
438 paged_attention = get_b12x_paged_attention()
439 assert paged_attention is not None and paged_attention.is_supported()
440
441 def _extend_work_items(
442 page_size: int,
443 q_capacity: int,
444 batch_size: int,
445 ) -> int:
446 capacity = paged_attention.extend_graph_capacity(
447 device=self.device,
448 q_dtype=self.dtype,
449 kv_dtype=self.kv_torch_dtype,
450 num_q_heads=self.num_heads,
451 num_kv_heads=self.num_kv_heads,
452 head_dim_qk=self.head_size,
453 head_dim_vo=self.output_head_size,
454 page_size=page_size,
455 batch=batch_size,
456 total_q_capacity=q_capacity,
457 max_cache_page_count=max_page_table_widths[page_size],
458 window_left=self.window_left,
459 )
460 return capacity.max_work_items
461
462 self._paged_attention = paged_attention
463
464 def _make_plan(
465 page_size: int,
466 mode: str,
467 max_total_q: int,
468 max_batch: int,
469 max_work_items: int,
470 max_partial_rows: int,
471 use_cuda_graph: bool,
472 num_cache_pages: int,
473 copy_runtime_metadata: bool,
474 ) -> Any:
475 return paged_attention.plan(
476 paged_attention.Caps(
477 device=self.device,
478 mode=mode,
479 dtype=self.dtype,
480 kv_dtype=self.kv_torch_dtype,
481 num_q_heads=self.num_heads,
482 num_kv_heads=self.num_kv_heads,
483 head_dim_qk=self.head_size,
484 head_dim_vo=self.output_head_size,
485 page_size=page_size,
486 max_total_q=max_total_q,
487 max_batch=max_batch,
488 max_page_table_width=max_page_table_widths[page_size],
489 max_work_items=max_work_items,
490 max_partial_rows=max_partial_rows,
491 # Shape-only planning tensor; runtime cache shape is
492 # validated by head/page geometry, not page count.
493 num_cache_pages=num_cache_pages,
494 use_cuda_graph=use_cuda_graph,
495 copy_runtime_metadata=copy_runtime_metadata,
496 )
497 )
498
499 capture_sizes = vllm_config.compilation_config.cudagraph_capture_sizes or []
500 decode_plan_sizes = {
501 int(size) for size in capture_sizes if 0 < int(size) <= max_num_seqs
502 }
503 decode_plan_sizes.add(max_num_seqs)
504
505 def _create_decode_plan(page_size: int, batch_size: int) -> Any:
506 max_page_table_width = max_page_table_widths[page_size]
507 capacity = paged_attention.decode_graph_capacity(
508 device=self.device,
509 q_dtype=self.dtype,
510 kv_dtype=self.kv_torch_dtype,
511 num_q_heads=self.num_heads,
512 num_kv_heads=self.num_kv_heads,
513 head_dim_qk=self.head_size,
514 head_dim_vo=self.output_head_size,
515 page_size=page_size,
516 batch=batch_size,
517 max_cache_page_count=max_page_table_width,
518 window_left=self.window_left,
519 )
520 plan = _make_plan(
521 page_size,
522 "decode",
523 batch_size,
524 batch_size,
525 capacity.max_work_items,
526 capacity.max_partial_rows,
527 True,
528 max_page_table_width,
529 True,
530 )
531 plan.prepare_decode_graph_replay_state(
532 batch=batch_size,
533 total_q_capacity=batch_size,
534 max_page_table_width=max_page_table_width,
535 max_cache_page_count=max_page_table_width,
536 window_left=self.window_left,
537 )
538 return plan
539
540 self._create_decode_plan = _create_decode_plan
541 self._verify_q_per_req = 0
542 if spec_config is not None:
543 self._verify_q_per_req = 1 + int(
544 getattr(spec_config, "num_speculative_tokens", None) or 0
545 )
546 if self._verify_q_per_req <= 1:
547 self._verify_q_per_req = 0
548
549 def _create_verify_plan(page_size: int, batch_size: int) -> Any:
550 if self._verify_q_per_req <= 1:
551 raise RuntimeError("b12x verifier plan requested without speculation")
552 max_page_table_width = max_page_table_widths[page_size]
553 total_q = batch_size * self._verify_q_per_req
554 capacity = paged_attention.verify_graph_capacity(
555 device=self.device,
556 q_dtype=self.dtype,
557 kv_dtype=self.kv_torch_dtype,
558 num_q_heads=self.num_heads,
559 num_kv_heads=self.num_kv_heads,
560 head_dim_qk=self.head_size,
561 head_dim_vo=self.output_head_size,
562 page_size=page_size,
563 batch=batch_size,
564 query_len=self._verify_q_per_req,
565 max_cache_page_count=max_page_table_width,
566 window_left=self.window_left,
567 )
568 plan = _make_plan(
569 page_size,
570 "verify",
571 total_q,
572 batch_size,
573 capacity.max_work_items,
574 capacity.max_partial_rows,
575 True,
576 max_page_table_width,
577 True,
578 )
579 page_ids = torch.arange(
580 max_page_table_width,
581 dtype=torch.int32,
582 device=self.device,
583 )
584 max_page_table = page_ids.unsqueeze(0).expand(batch_size, -1).contiguous()
585 max_cache_seqlens = torch.full(
586 (batch_size,),
587 capacity.representative_cache_seqlen,
588 dtype=torch.int32,
589 device=self.device,
590 )
591 max_cu_seqlens_q = torch.arange(
592 0,
593 total_q + 1,
594 self._verify_q_per_req,
595 dtype=torch.int32,
596 device=self.device,
597 )
598 plan.prepare_graph_replay_state(
599 page_table=max_page_table,
600 cache_seqlens=max_cache_seqlens,
601 cu_seqlens_q=max_cu_seqlens_q,
602 active_total_q=total_q,
603 window_left=self.window_left,
604 )
605 return plan
606
607 self._create_verify_plan = _create_verify_plan
608
609 def _create_extend_plan(
610 page_size: int,
611 batch_size: int,
612 q_capacity: int,
613 ) -> Any:
614 """Prepare a fixed-capacity extend plan without reading live lengths."""
615 max_page_table_width = max_page_table_widths[page_size]
616 plan = _make_plan(
617 page_size,
618 "extend",
619 q_capacity,
620 batch_size,
621 _extend_work_items(page_size, q_capacity, batch_size),
622 0,
623 True,
624 max_page_table_width,
625 False,
626 )
627 page_ids = torch.arange(
628 max_page_table_width,
629 dtype=torch.int32,
630 device=self.device,
631 )
632 max_page_table = page_ids.unsqueeze(0).expand(batch_size, -1).contiguous()
633 max_cache_seqlens = torch.full(
634 (batch_size,),
635 min(max_model_len, max_page_table_width * page_size),
636 dtype=torch.int32,
637 device=self.device,
638 )
639 # Put one row in every request except the last, which owns the
640 # remainder. This represents the full total-Q capacity while the
641 # replay kernel remains responsible for packing arbitrary live
642 # per-request lengths from device cu_seqlens_q.
643 max_cu_seqlens_q = torch.arange(
644 0,
645 batch_size + 1,
646 dtype=torch.int32,
647 device=self.device,
648 )
649 max_cu_seqlens_q[-1] = q_capacity
650 plan.prepare_graph_replay_state(
651 page_table=max_page_table,
652 cache_seqlens=max_cache_seqlens,
653 cu_seqlens_q=max_cu_seqlens_q,
654 active_total_q=q_capacity,
655 window_left=self.window_left,
656 )
657 return plan
658
659 self._create_extend_plan = _create_extend_plan
660 decode_scratch_envelopes = {
661 page_size: paged_attention.decode_graph_scratch_envelope(
662 device=self.device,
663 q_dtype=self.dtype,
664 kv_dtype=self.kv_torch_dtype,
665 num_q_heads=self.num_heads,
666 num_kv_heads=self.num_kv_heads,
667 head_dim_qk=self.head_size,
668 head_dim_vo=self.output_head_size,
669 page_size=page_size,
670 max_batch=max_num_seqs,
671 max_page_table_width=max_page_table_widths[page_size],
672 max_cache_page_count=max_page_table_widths[page_size],
673 window_left=self.window_left,
674 copy_runtime_metadata=True,
675 )
676 for page_size in _B12X_SUPPORTED_PAGE_SIZES
677 }
678 self._decode_plans: dict[tuple[int, int], Any] = {}
679 self._verify_plans: dict[tuple[int, int], Any] = {}
680 self._extend_plans: dict[tuple[int, int, int], Any] = {}
681 for page_size in _B12X_SUPPORTED_PAGE_SIZES:
682 for batch_size in sorted(decode_plan_sizes):
683 self._decode_plans[page_size, batch_size] = self._create_decode_plan(
684 page_size, batch_size
685 )
686 if self._verify_q_per_req > 1:
687 for batch_size in range(1, max_num_seqs + 1):
688 self._verify_plans[page_size, batch_size] = (
689 self._create_verify_plan(page_size, batch_size)
690 )
691 for batch_size in range(1, max_num_seqs + 1):
692 for q_capacity in self._extend_q_capacities:
693 # Equal capacity is necessarily one query token per
694 # request, which is handled by the decode plan.
695 if batch_size >= q_capacity:
696 continue
697 self._extend_plans[page_size, batch_size, q_capacity] = (
698 self._create_extend_plan(
699 page_size,
700 batch_size,
701 q_capacity,
702 )
703 )
704 self._scratch_nbytes = max(
705 *(int(envelope.nbytes) for envelope in decode_scratch_envelopes.values()),
706 *(int(plan.layout.nbytes) for plan in self._verify_plans.values()),
707 *(int(plan.layout.nbytes) for plan in self._extend_plans.values()),
708 )
709
710 current_workspace_manager().get_simultaneous(
711 ((self._scratch_nbytes,), torch.uint8),
712 )
713
714 self.supports_quant_query_input = False
715 register_cutedsl_warmup_provider(self)
716
717 logger.info_once(
718 "Using b12x with q_heads=%d kv_heads=%d head_dim_qk=%d "
719 "head_dim_vo=%d window_left=%d planned_page_sizes=%s "
720 "verify_q_per_req=%d extend_q_capacities=%s scratch=%d bytes.",
721 self.num_heads,
722 self.num_kv_heads,
723 self.head_size,
724 self.output_head_size,
725 self.window_left,
726 _B12X_SUPPORTED_PAGE_SIZES,
727 self._verify_q_per_req,
728 self._extend_q_capacities,
729 self._scratch_nbytes,
730 )
731
732 def _compile_paged_extend_entry(self, page_size: int) -> None:
733 """Compile fixed-capacity paged-prefill entries without a live plan."""
734 warmup_plans: list[tuple[int, int, Any, bool]] = []
735 for batch_size in range(1, self._max_num_seqs + 1):
736 candidates = sorted(
737 (q_capacity, plan)
738 for (plan_page_size, plan_batch, q_capacity), plan in (
739 self._extend_plans.items()
740 )
741 if plan_page_size == page_size and plan_batch == batch_size
742 )
743 for index, (q_capacity, plan) in enumerate(candidates):
744 warmup_plans.append((batch_size, q_capacity, plan, index == 0))
745 if not warmup_plans:
746 return
747
748 max_q_rows = max(
749 min(q_capacity, max(64, batch_size + 1))
750 for batch_size, q_capacity, _, _ in warmup_plans
751 )
752 q = torch.zeros(
753 (max_q_rows, self.num_heads, self.head_size),
754 dtype=self.dtype,
755 device=self.device,
756 )
757 output = torch.zeros(
758 (max_q_rows, self.num_heads, self.output_head_size),
759 dtype=self.dtype,
760 device=self.device,
761 )
762 kv_cache = torch.zeros(
763 (1, 2, page_size, self.num_kv_heads * self.head_size),
764 dtype=self.kv_torch_dtype,
765 device=self.device,
766 )
767 key_cache, value_cache = self._kv_cache_views(kv_cache)
768 (scratch_storage,) = current_workspace_manager().get_simultaneous(
769 ((self._scratch_nbytes,), torch.uint8),
770 )
771 for batch_size, q_capacity, plan, execute in warmup_plans:
772 q_rows = min(q_capacity, max(64, batch_size + 1))
773 page_table = torch.zeros(
774 (batch_size, plan.caps.max_page_table_width),
775 dtype=torch.int32,
776 device=self.device,
777 )
778 cache_seqlens = torch.full(
779 (batch_size,), page_size, dtype=torch.int32, device=self.device
780 )
781 cu_seqlens_q = torch.arange(
782 0,
783 batch_size + 1,
784 dtype=torch.int32,
785 device=self.device,
786 )
787 cu_seqlens_q[-1] = q_rows
788 k_descale = None
789 v_descale = None
790 if _is_b12x_fp8_kv_cache(self.kv_cache_dtype):
791 k_descale = torch.ones(
792 (), dtype=torch.float32, device=self.device
793 ).expand(batch_size)
794 v_descale = torch.ones(
795 (), dtype=torch.float32, device=self.device
796 ).expand(batch_size)
797 binding = plan.bind(
798 scratch=scratch_storage,
799 q=q[:q_rows],
800 k_cache=key_cache,
801 v_cache=value_cache,
802 output=output[:q_rows],
803 page_table=page_table,
804 cache_seqlens=cache_seqlens,
805 cu_seqlens_q=cu_seqlens_q,
806 window_left=self.window_left,
807 attention_sink_bias=self.sinks,
808 k_descale=k_descale,
809 v_descale=v_descale,
810 )
811 self._paged_attention.compile(binding=binding)
812 if execute:
813 # Compile-only warmup does not launch the device-side compact
814 # scheduler. One execution per batch covers the capture-static
815 # metadata variant shared by its Q-capacity plans.
816 self._paged_attention.run(binding=binding)
817
818 def get_cutedsl_warmup_compile_units(self) -> tuple[CuTeDSLCompileUnit, ...]:
819 common_key = (
820 "b12x_paged_extend",
821 str(self.device),
822 str(self.dtype),
823 str(self.kv_torch_dtype),
824 self.num_heads,
825 self.num_kv_heads,
826 self.head_size,
827 self.output_head_size,
828 self.window_left,
829 self.sinks is not None,
830 )
831 return tuple(
832 CuTeDSLCompileUnit(
833 name="b12x_paged_extend",
834 key=(*common_key, page_size),
835 compile=partial(self._compile_paged_extend_entry, page_size),
836 )
837 for page_size in _B12X_SUPPORTED_PAGE_SIZES
838 )
839
840 def process_weights_after_loading(self, act_dtype: torch.dtype) -> None:
841 del act_dtype
842 source_sinks = self._sinks_source
843 if source_sinks is None:
844 return
845 if source_sinks.dtype == torch.float32:
846 self.sinks = source_sinks
847 elif self.sinks is None or self.sinks.dtype != torch.float32:
848 self.sinks = source_sinks.to(torch.float32)
849 else:
850 self.sinks.copy_(source_sinks)
851
852 def _prepare_fp8_descales(
853 self,
854 layer: AttentionLayer,
855 num_reqs: int,
856 device: torch.device,
857 ) -> tuple[torch.Tensor | None, torch.Tensor | None]:
858 if not _is_b12x_fp8_kv_cache(self.kv_cache_dtype):
859 return None, None
860 if num_reqs <= 0:
861 raise ValueError("b12x fp8 KV descale request count must be positive.")
862
863 def _prepare(scale: torch.Tensor, name: str) -> torch.Tensor:
864 if scale.device != device:
865 raise RuntimeError(f"b12x {name} must be on the query device.")
866 if scale.dtype != torch.float32:
867 raise RuntimeError(f"b12x {name} must be float32.")
868 if scale.ndim == 0:
869 return scale.expand(num_reqs)
870 if scale.ndim == 1:
871 if int(scale.shape[0]) == 1:
872 return scale.expand(num_reqs)
873 if int(scale.shape[0]) >= num_reqs:
874 return scale[:num_reqs]
875 raise ValueError(
876 f"b12x {name} must be scalar or rank-1 with at least "
877 f"{num_reqs} values; got shape {tuple(scale.shape)}."
878 )
879
880 return _prepare(layer._k_scale, "k_scale"), _prepare(layer._v_scale, "v_scale")
881
882 def _kv_cache_views(
883 self,
884 kv_cache: torch.Tensor,
885 ) -> tuple[torch.Tensor, torch.Tensor]:
886 key_cache, value_cache = kv_cache.unbind(1)
887 key_cache = key_cache.unflatten(-1, (self.num_kv_heads, self.head_size))
888 value_cache = value_cache.unflatten(-1, (self.num_kv_heads, self.head_size))
889 key_cache = canonicalize_singleton_dim_strides(key_cache)
890 value_cache = canonicalize_singleton_dim_strides(value_cache)
891 if _is_b12x_fp8_kv_cache(self.kv_cache_dtype):
892 fp8_dtype = current_platform.fp8_dtype()
893 if key_cache.dtype == torch.uint8:
894 key_cache = key_cache.view(fp8_dtype)
895 if value_cache.dtype == torch.uint8:
896 value_cache = value_cache.view(fp8_dtype)
897 if (
898 key_cache.dtype != self.kv_torch_dtype
899 or value_cache.dtype != self.kv_torch_dtype
900 ):
901 raise TypeError(
902 f"b12x plan expects KV dtype {self.kv_torch_dtype}, got "
903 f"{key_cache.dtype}/{value_cache.dtype}."
904 )
905 return key_cache, value_cache
906
907 def _select_plan(
908 self,
909 attn_metadata: B12xPagedMetadata,
910 total_q: int,
911 q_capacity: int,
912 num_reqs: int,
913 page_size: int,
914 ) -> Any:
915 if attn_metadata.max_query_len <= 1 and int(total_q) == int(num_reqs):
916 batch_size = int(total_q)
917 plan_key = (page_size, batch_size)
918 plan = self._decode_plans.get(plan_key)
919 if plan is None:
920 if _capture_alloc_forbidden():
921 raise RuntimeError(
922 "b12x decode plan was not prepared before CUDA graph "
923 f"capture for page size {page_size}, batch size "
924 f"{batch_size}."
925 )
926 plan = self._create_decode_plan(page_size, batch_size)
927 if int(plan.layout.nbytes) > self._scratch_nbytes:
928 raise RuntimeError(
929 "b12x lazily created decode plan exceeds reserved "
930 f"scratch: {int(plan.layout.nbytes)} > "
931 f"{self._scratch_nbytes} bytes."
932 )
933 self._decode_plans[plan_key] = plan
934 return plan
935 elif (
936 self._verify_q_per_req > 1
937 and attn_metadata.max_query_len == self._verify_q_per_req
938 and int(total_q) == int(num_reqs) * self._verify_q_per_req
939 ):
940 plan_key = (page_size, int(num_reqs))
941 plan = self._verify_plans.get(plan_key)
942 if plan is None:
943 if _capture_alloc_forbidden():
944 raise RuntimeError(
945 "b12x verifier plan was not prepared before CUDA "
946 f"graph capture for page size {page_size}, batch size "
947 f"{num_reqs}."
948 )
949 plan = self._create_verify_plan(page_size, int(num_reqs))
950 if int(plan.layout.nbytes) > self._scratch_nbytes:
951 raise RuntimeError(
952 "b12x lazily created verifier plan exceeds reserved "
953 f"scratch: {int(plan.layout.nbytes)} > "
954 f"{self._scratch_nbytes} bytes."
955 )
956 self._verify_plans[plan_key] = plan
957 return plan
958 extend_q_capacity = next(
959 (
960 capacity
961 for capacity in self._extend_q_capacities
962 if q_capacity <= capacity
963 ),
964 None,
965 )
966 if extend_q_capacity is None:
967 raise ValueError(
968 f"b12x extend Q capacity {q_capacity} exceeds prepared "
969 f"maximum {self._extend_q_capacities[-1]}."
970 )
971 return self._extend_plans[
972 page_size,
973 int(num_reqs),
974 extend_q_capacity,
975 ]
976
977 def forward(
978 self,
979 layer: AttentionLayer,
980 query: torch.Tensor,
981 key: torch.Tensor,
982 value: torch.Tensor,
983 kv_cache: torch.Tensor,
984 attn_metadata: B12xPagedMetadata,
985 output: torch.Tensor,
986 output_scale: torch.Tensor | None = None,
987 output_block_scale: torch.Tensor | None = None,
988 ) -> torch.Tensor:
989 del key, value
990 if output_scale is not None or output_block_scale is not None:
991 raise NotImplementedError(
992 "b12x does not support fused output quantization."
993 )
994 if attn_metadata is None:
995 return output.fill_(0)
996 if output.shape[-1] != self.output_head_size:
997 raise ValueError(
998 f"b12x expected output head dim {self.output_head_size}, got "
999 f"{output.shape[-1]}."
1000 )
1001 if kv_cache.numel() == 0:
1002 return output.fill_(0)
1003
1004 # In FULL cudagraph mode vLLM may pad attention metadata to the graph
1005 # bucket while still passing per-layer Q/output tensors with only the
1006 # real rows. Use tensor capacity as the launch contract and avoid
1007 # selecting decode graph replay for padded virtual requests.
1008 q_capacity = min(
1009 int(query.shape[0]),
1010 int(output.shape[0]),
1011 )
1012 num_actual_tokens = min(
1013 int(attn_metadata.num_actual_tokens),
1014 q_capacity,
1015 )
1016 if num_actual_tokens <= 0:
1017 return output
1018 q = query[:num_actual_tokens]
1019 out = output[:num_actual_tokens]
1020 if q.dtype != self.dtype or out.dtype != self.dtype:
1021 raise TypeError(
1022 f"b12x plan expects dtype {self.dtype}, got "
1023 f"q={q.dtype}, output={out.dtype}."
1024 )
1025
1026 key_cache, value_cache = self._kv_cache_views(kv_cache)
1027 page_size = _kv_page_size(key_cache, value_cache)
1028 if not attn_metadata.causal:
1029 raise NotImplementedError("b12x supports causal attention only.")
1030
1031 page_table = _ensure_i32_contiguous(attn_metadata.block_table, "block_table")
1032 cache_seqlens = _ensure_i32_contiguous(attn_metadata.seq_lens, "seq_lens")
1033 cu_seqlens_q = _ensure_i32_contiguous(
1034 attn_metadata.query_start_loc,
1035 "query_start_loc",
1036 )
1037 num_reqs = int(cache_seqlens.shape[0])
1038 if attn_metadata.max_query_len <= 1 and num_actual_tokens < num_reqs:
1039 num_reqs = num_actual_tokens
1040 page_table = page_table[:num_reqs]
1041 cache_seqlens = cache_seqlens[:num_reqs]
1042 cu_seqlens_q = cu_seqlens_q[: num_reqs + 1]
1043 k_descale, v_descale = self._prepare_fp8_descales(
1044 layer,
1045 num_reqs,
1046 q.device,
1047 )
1048 plan = self._select_plan(
1049 attn_metadata,
1050 num_actual_tokens,
1051 q_capacity,
1052 num_reqs,
1053 page_size,
1054 )
1055 (scratch_storage,) = current_workspace_manager().get_simultaneous(
1056 ((self._scratch_nbytes,), torch.uint8),
1057 )
1058 binding = plan.bind(
1059 scratch=scratch_storage,
1060 q=q,
1061 k_cache=key_cache,
1062 v_cache=value_cache,
1063 output=out,
1064 page_table=page_table,
1065 cache_seqlens=cache_seqlens,
1066 cu_seqlens_q=cu_seqlens_q,
1067 window_left=self.window_left,
1068 active_total_q=(None if plan.caps.mode == "extend" else num_actual_tokens),
1069 attention_sink_bias=self.sinks,
1070 k_descale=k_descale,
1071 v_descale=v_descale,
1072 )
1073 self._paged_attention.run(binding=binding)
1074 return output
1075
1076 def do_kv_cache_update(
1077 self,
1078 layer: AttentionLayer,
1079 key: torch.Tensor,
1080 value: torch.Tensor,
1081 kv_cache: torch.Tensor,
1082 slot_mapping: torch.Tensor,
1083 ) -> None:
1084 if kv_cache.numel() == 0:
1085 return
1086 key_cache, value_cache = self._kv_cache_views(kv_cache)
1087 torch.ops._C_cache_ops.reshape_and_cache_flash(
1088 key,
1089 value,
1090 key_cache,
1091 value_cache,
1092 slot_mapping,
1093 self.kv_cache_dtype,
1094 layer._k_scale,
1095 layer._v_scale,
1096 )