[Kernel] Add B12X causal paged attention backend (#52017)

Signed-off-by: Luke Alonso <lalonso@gmail.com> Co-authored-by: Lucas Wilkinson <LucasWilkinson@users.noreply.github.com>

Luke Alonso committed Sep 1, 2026 at 07:42 UTC 0d4ad47981b6133c8f835d78f7a19ca4eda113d5
8 files changed +1527 -5
.buildkite/test_areas/kernels.yaml
+1 -1
@@ -389,7 +389,7 @@ steps:
389 - tests/quantization/reference_mxfp4.py
390 - tests/quantization/test_auto_round.py
391 commands:
392 - - uv pip install --system b12x==1.2.6
392 + - uv pip install --system b12x==1.3.0
393 - pytest -v -s model_executor/kernels/test_b12x_linear.py
394 model_executor/test_b12x_warmup.py
395 kernels/quantization/test_block_fp8.py
docs/design/attention_backends.md
+10
@@ -136,6 +136,16 @@ Priority is **1 = highest** (tried first).
136 > `--block-size` that is not a multiple of 128 instead makes FlashAttention ineligible
137 > for such models, which is an error if the backend was requested explicitly.
138
139 +### b12x
140 +
141 +The optional [b12x](https://pypi.org/project/b12x/) backend supports causal
142 +decoder attention on NVIDIA SM120 and SM121 GPUs. Install and select it with:
143 +
144 +```bash
145 +uv pip install "vllm[b12x]"
146 +vllm serve <model> --attention-backend b12x
147 +```
148 +
149 ## MiniMax M3 Sparse Attention Backends
150
151 Block-sparse GQA backend used by MiniMax M3 sparse ("lightning indexer")
setup.py
+1 -1
@@ -1522,7 +1522,7 @@ setup(
1522 # only; also needs system GStreamer + libv4l (see docs).
1523 "deepstream": ["nvidia-deepstream-videodecode-cu13>=9.0.2"],
1524 "flashinfer": [], # Kept for backwards compatibility
1525 - "b12x": ["b12x==1.2.6"],
1525 + "b12x": ["b12x==1.3.0"],
1526 # Optional deps for Helion kernel development
1527 # NOTE: When updating helion version, also update CI files:
1528 # - .buildkite/test_areas/kernels.yaml
tests/v1/attention/test_attention_backends.py
+53 -3
@@ -3,6 +3,7 @@
3 """Tests for v1 attention backends without GPUModelRunner dependency."""
4
5 from functools import partial
6 +from types import SimpleNamespace
7
8 import pytest
9 import torch
@@ -282,6 +283,7 @@ def run_attention_backend(
283 sliding_window: int | None = None,
284 kv_cache_dtype: str = "auto",
285 sinks: torch.Tensor | None = None,
286 + use_cuda_graph: bool = False,
287 ) -> torch.Tensor:
288 """Run attention computation using the specified backend's AttentionImpl."""
289
@@ -367,9 +369,26 @@ def run_attention_backend(
369 impl.do_kv_cache_update(
370 mock_layer, key, value, kv_cache, attn_metadata.slot_mapping
371 )
370 - output = impl.forward(
371 - mock_layer, query, key, value, kv_cache, attn_metadata, output=output
372 - )
372 + if use_cuda_graph:
373 + impl.forward(
374 + mock_layer, query, key, value, kv_cache, attn_metadata, output=output
375 + )
376 + graph = torch.cuda.CUDAGraph()
377 + with torch.cuda.graph(graph):
378 + impl.forward(
379 + mock_layer,
380 + query,
381 + key,
382 + value,
383 + kv_cache,
384 + attn_metadata,
385 + output=output,
386 + )
387 + graph.replay()
388 + else:
389 + output = impl.forward(
390 + mock_layer, query, key, value, kv_cache, attn_metadata, output=output
391 + )
392
393 return output
394
@@ -389,6 +408,11 @@ def _test_backend_correctness(
408 kv_cache_dtype: str = "auto",
409 use_sinks: bool = False,
410 layout: KVCacheLayout | None = None,
411 + use_cuda_graph: bool = False,
412 + num_speculative_tokens: int = 0,
413 + model_dtype: torch.dtype | None = None,
414 + max_num_seqs: int | None = None,
415 + max_num_batched_tokens: int | None = None,
416 ):
417 """
418 Test that all backends produce similar outputs to a reference implementation
@@ -433,10 +457,19 @@ def _test_backend_correctness(
457 model_name=model,
458 tensor_parallel_size=1, # Always use TP=1 to avoid multi-GPU requirements
459 max_model_len=max(batch_spec.seq_lens),
460 + dtype=model_dtype or "auto",
461 block_size=block_size,
462 num_gpu_blocks=8192,
463 hf_config_override=hf_config_override,
464 )
465 + if max_num_seqs is not None:
466 + vllm_config.scheduler_config.max_num_seqs = max_num_seqs
467 + if max_num_batched_tokens is not None:
468 + vllm_config.scheduler_config.max_num_batched_tokens = max_num_batched_tokens
469 + if num_speculative_tokens > 0:
470 + vllm_config.speculative_config = SimpleNamespace(
471 + num_speculative_tokens=num_speculative_tokens
472 + )
473 vllm_config.cache_config.cache_dtype = kv_cache_dtype
474 device = torch.device(f"{DEVICE_TYPE}:0")
475
@@ -619,6 +652,22 @@ def _test_backend_correctness(
652 backend_layout = backend_supported[0]
653 kv_cache_for_backend = _clone_kv_cache_in_layout(kv_cache, backend_layout)
654
655 + if backend_name == AttentionBackendEnum.B12X:
656 + cache_dtype = (
657 + FP8_KV_CACHE_DTYPES[kv_cache_dtype]
658 + if is_quantized_kv_cache(kv_cache_dtype)
659 + else kv_cache.dtype
660 + )
661 + typed_cache = kv_cache.view(cache_dtype)
662 + key_cache = typed_cache[..., :head_size].permute(0, 2, 1, 3)
663 + value_cache = typed_cache[..., head_size:].permute(0, 2, 1, 3)
664 + packed_cache = torch.stack((key_cache, value_cache), dim=1).flatten(-2)
665 + if is_quantized_kv_cache(kv_cache_dtype):
666 + packed_cache = packed_cache.view(torch.uint8)
667 + kv_cache_for_backend = _clone_kv_cache_in_layout(
668 + packed_cache, backend_layout
669 + )
670 +
671 # FlashInfer reads the layout at plan time; set it to match
672 # the physical order of the test cache.
673 vllm_config.cache_config.kv_cache_layout = backend_layout.name
@@ -638,6 +687,7 @@ def _test_backend_correctness(
687 attn_type=attn_type,
688 kv_cache_dtype=kv_cache_dtype,
689 sinks=sinks,
690 + use_cuda_graph=use_cuda_graph,
691 )
692
693 # Check shape and dtype consistency
tests/v1/attention/test_b12x.py new
+360
@@ -0,0 +1,360 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +from functools import partial
5 +from types import SimpleNamespace
6 +
7 +import pytest
8 +import torch
9 +
10 +from tests.v1.attention.test_attention_backends import (
11 + BATCH_SPECS,
12 + _test_backend_correctness,
13 +)
14 +from tests.v1.attention.utils import BatchSpec
15 +from vllm.config import ModelConfig
16 +from vllm.platforms import current_platform
17 +from vllm.platforms.interface import DeviceCapability
18 +from vllm.utils.b12x import get_b12x_paged_attention
19 +from vllm.v1.attention.backends import b12x
20 +from vllm.v1.attention.backends.b12x import (
21 + B12xPagedAttentionBackend,
22 + B12xPagedAttentionImpl,
23 + _kv_page_size,
24 + _max_page_table_width,
25 +)
26 +from vllm.v1.attention.backends.registry import AttentionBackendEnum
27 +from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheLayout
28 +
29 +
30 +def _require_b12x_paged_attention() -> None:
31 + capability = current_platform.get_device_capability()
32 + if (
33 + not current_platform.is_cuda()
34 + or capability is None
35 + or not B12xPagedAttentionBackend.supports_compute_capability(capability)
36 + ):
37 + pytest.skip("b12x paged attention requires SM120 or SM121.")
38 +
39 + paged_attention = get_b12x_paged_attention()
40 + if paged_attention is None or not paged_attention.is_supported():
41 + pytest.skip("b12x paged attention is not available.")
42 +
43 +
44 +def _causal_mask(
45 + b: torch.Tensor,
46 + h: torch.Tensor,
47 + q_idx: torch.Tensor,
48 + kv_idx: torch.Tensor,
49 + *,
50 + context_len: int,
51 +):
52 + return q_idx + context_len >= kv_idx
53 +
54 +
55 +def _causal_sliding_window_mask(
56 + b: torch.Tensor,
57 + h: torch.Tensor,
58 + q_idx: torch.Tensor,
59 + kv_idx: torch.Tensor,
60 + *,
61 + context_len: int,
62 + sliding_window: int,
63 +):
64 + causal_mask = q_idx + context_len >= kv_idx
65 + window_mask = q_idx + context_len - kv_idx < sliding_window
66 + return causal_mask & window_mask
67 +
68 +
69 +@pytest.mark.parametrize(
70 + ("dtype", "kv_cache_dtype", "paged_attention", "expected_reason"),
71 + [
72 + pytest.param(
73 + torch.float16,
74 + "fp8_e4m3",
75 + None,
76 + "b12x currently requires bfloat16 queries",
77 + id="query-dtype",
78 + ),
79 + pytest.param(
80 + torch.bfloat16,
81 + "float16",
82 + None,
83 + "b12x does not support float16 KV cache",
84 + id="kv-cache-dtype",
85 + ),
86 + pytest.param(
87 + torch.bfloat16,
88 + "auto",
89 + None,
90 + "Install the b12x backend with `pip install vllm[b12x]`",
91 + id="package-not-installed",
92 + ),
93 + pytest.param(
94 + torch.bfloat16,
95 + "auto",
96 + SimpleNamespace(is_supported=lambda: False),
97 + "b12x paged attention is not supported on the current device",
98 + id="device-api",
99 + ),
100 + pytest.param(
101 + torch.bfloat16,
102 + "auto",
103 + SimpleNamespace(is_supported=lambda: True),
104 + None,
105 + id="supported",
106 + ),
107 + ],
108 +)
109 +def test_b12x_attention_config_support(
110 + monkeypatch: pytest.MonkeyPatch,
111 + dtype: torch.dtype,
112 + kv_cache_dtype: str,
113 + paged_attention,
114 + expected_reason: str | None,
115 +) -> None:
116 + monkeypatch.setattr(
117 + b12x,
118 + "get_b12x_paged_attention",
119 + lambda: paged_attention,
120 + )
121 +
122 + reason = B12xPagedAttentionBackend.supports_combination(
123 + head_size=128,
124 + dtype=dtype,
125 + kv_cache_dtype=kv_cache_dtype,
126 + block_size=128,
127 + use_mla=False,
128 + has_sink=False,
129 + use_sparse=False,
130 + use_mm_prefix=False,
131 + device_capability=DeviceCapability(12, 0),
132 + )
133 +
134 + assert reason == expected_reason
135 +
136 +
137 +def test_b12x_attention_uses_two_plane_nhd_cache() -> None:
138 + spec = B12xPagedAttentionBackend.customize_spec(
139 + FullAttentionSpec(
140 + block_size=128,
141 + num_kv_heads=4,
142 + head_size=128,
143 + dtype=torch.bfloat16,
144 + )
145 + )
146 +
147 + assert spec.num_head_slots == 2
148 + assert spec.state_content_bytes == 4 * 128 * 2
149 + assert B12xPagedAttentionBackend.supported_kv_cache_layouts() == (
150 + KVCacheLayout.LBHNC,
151 + KVCacheLayout.BLHNC,
152 + )
153 + assert B12xPagedAttentionBackend.supports_block_size(128)
154 + assert not B12xPagedAttentionBackend.supports_block_size(32)
155 +
156 +
157 +def test_b12x_attention_hybrid_cache_capacity_includes_expansion() -> None:
158 + assert _max_page_table_width(4096, 128, 4096, False) == 32
159 + assert _max_page_table_width(4096, 128, 4096, True) == 64
160 +
161 +
162 +def test_b12x_attention_runtime_page_size_comes_from_cache() -> None:
163 + key_cache = torch.empty((3, 64, 4, 128), device="meta")
164 + value_cache = torch.empty_like(key_cache)
165 +
166 + assert _kv_page_size(key_cache, value_cache) == 64
167 + with pytest.raises(ValueError, match="matching K/V page sizes"):
168 + _kv_page_size(key_cache, torch.empty((3, 128, 4, 128), device="meta"))
169 +
170 +
171 +def test_b12x_attention_lazily_prepares_decode_bucket(monkeypatch) -> None:
172 + impl = object.__new__(B12xPagedAttentionImpl)
173 + plan = SimpleNamespace(layout=SimpleNamespace(nbytes=96))
174 + created: list[tuple[int, int]] = []
175 +
176 + def create_plan(page_size: int, batch_size: int) -> SimpleNamespace:
177 + created.append((page_size, batch_size))
178 + return plan
179 +
180 + impl._decode_plans = {}
181 + impl._create_decode_plan = create_plan
182 + impl._scratch_nbytes = 128
183 + impl._extend_plans = {}
184 + impl._verify_q_per_req = 0
185 + metadata = SimpleNamespace(max_query_len=1)
186 + monkeypatch.setattr(b12x, "_capture_alloc_forbidden", lambda: False)
187 +
188 + assert impl._select_plan(metadata, 7, 7, 7, 64) is plan
189 + assert impl._select_plan(metadata, 7, 7, 7, 64) is plan
190 + assert created == [(64, 7)]
191 +
192 +
193 +def test_b12x_attention_fp8_descales_follow_request_batch() -> None:
194 + impl = object.__new__(B12xPagedAttentionImpl)
195 + impl.kv_cache_dtype = "fp8_e4m3"
196 + layer = SimpleNamespace(
197 + _k_scale=torch.tensor(2.0),
198 + _v_scale=torch.tensor([3.0, 4.0, 5.0]),
199 + )
200 +
201 + k_descale, v_descale = impl._prepare_fp8_descales(
202 + layer, num_reqs=2, device=torch.device("cpu")
203 + )
204 +
205 + torch.testing.assert_close(k_descale, torch.tensor([2.0, 2.0]))
206 + torch.testing.assert_close(v_descale, torch.tensor([3.0, 4.0]))
207 + assert k_descale.stride() == (0,)
208 + assert v_descale.stride() == (1,)
209 +
210 +
211 +def test_b12x_attention_sinks_refresh_in_place_after_reload() -> None:
212 + impl = object.__new__(B12xPagedAttentionImpl)
213 + source = torch.tensor([1.0, 2.0], dtype=torch.bfloat16)
214 + impl._sinks_source = source
215 + impl.sinks = None
216 +
217 + impl.process_weights_after_loading(torch.bfloat16)
218 + assert impl.sinks is not None
219 + sinks_ptr = impl.sinks.data_ptr()
220 + source.copy_(torch.tensor([3.0, 4.0], dtype=torch.bfloat16))
221 + impl.process_weights_after_loading(torch.bfloat16)
222 +
223 + assert impl.sinks.data_ptr() == sinks_ptr
224 + torch.testing.assert_close(impl.sinks, source.float())
225 +
226 +
227 +@pytest.mark.parametrize(
228 + "batch_spec_name",
229 + ["small_decode", "small_prefill", "mixed_small", "medium_decode"],
230 +)
231 +@pytest.mark.parametrize(
232 + ("kv_cache_dtype", "model_dtype"),
233 + [
234 + ("auto", None),
235 + ("bfloat16", torch.bfloat16),
236 + ("fp8_e4m3", torch.bfloat16),
237 + ],
238 +)
239 +@pytest.mark.parametrize("block_size", [64, 128])
240 +def test_b12x_causal_backend_correctness(
241 + default_vllm_config,
242 + workspace_init,
243 + batch_spec_name: str,
244 + kv_cache_dtype: str,
245 + model_dtype: torch.dtype | None,
246 + block_size: int,
247 +) -> None:
248 + """b12x causal paged attention matches the shared SDPA reference."""
249 + _require_b12x_paged_attention()
250 + batch_spec = BATCH_SPECS[batch_spec_name]
251 +
252 + _test_backend_correctness(
253 + batch_spec,
254 + "Qwen/Qwen3-0.6B",
255 + [AttentionBackendEnum.B12X],
256 + _causal_mask,
257 + block_size=block_size,
258 + kv_cache_dtype=kv_cache_dtype,
259 + model_dtype=model_dtype,
260 + max_num_seqs=batch_spec.batch_size,
261 + max_num_batched_tokens=max(sum(batch_spec.query_lens), 64),
262 + )
263 +
264 +
265 +@pytest.mark.parametrize(
266 + "batch_spec",
267 + [
268 + pytest.param(BatchSpec(seq_lens=[2080, 2200], query_lens=[1, 1]), id="decode"),
269 + pytest.param(BatchSpec(seq_lens=[2080, 2200], query_lens=[8, 8]), id="prefill"),
270 + ],
271 +)
272 +def test_b12x_causal_sliding_window(
273 + default_vllm_config,
274 + workspace_init,
275 + batch_spec: BatchSpec,
276 +) -> None:
277 + """b12x causal sliding-window attention matches the shared reference."""
278 + _require_b12x_paged_attention()
279 +
280 + model = "microsoft/Phi-tiny-MoE-instruct"
281 + sliding_window = ModelConfig(
282 + model=model, max_model_len=max(batch_spec.seq_lens)
283 + ).get_sliding_window()
284 + assert sliding_window is not None
285 + mask = partial(_causal_sliding_window_mask, sliding_window=sliding_window)
286 +
287 + _test_backend_correctness(
288 + batch_spec,
289 + model,
290 + [AttentionBackendEnum.B12X],
291 + mask,
292 + block_size=64,
293 + atol=3e-2,
294 + rtol=3e-2,
295 + max_num_seqs=batch_spec.batch_size,
296 + max_num_batched_tokens=max(sum(batch_spec.query_lens), 64),
297 + )
298 +
299 +
300 +def test_b12x_attention_sinks(
301 + default_vllm_config,
302 + workspace_init,
303 +) -> None:
304 + """b12x attention sinks match the explicit sink reference."""
305 + _require_b12x_paged_attention()
306 + batch_spec = BATCH_SPECS["small_prefill"]
307 +
308 + _test_backend_correctness(
309 + batch_spec,
310 + "Qwen/Qwen3-0.6B",
311 + [AttentionBackendEnum.B12X],
312 + _causal_mask,
313 + block_size=64,
314 + atol=3e-2,
315 + rtol=3e-2,
316 + use_sinks=True,
317 + max_num_seqs=batch_spec.batch_size,
318 + max_num_batched_tokens=max(sum(batch_spec.query_lens), 64),
319 + )
320 +
321 +
322 +def test_b12x_decode_cuda_graph_replay(
323 + default_vllm_config,
324 + workspace_init,
325 +) -> None:
326 + """b12x decode output remains correct after CUDA graph replay."""
327 + _require_b12x_paged_attention()
328 + batch_spec = BATCH_SPECS["small_decode"]
329 +
330 + _test_backend_correctness(
331 + batch_spec,
332 + "Qwen/Qwen3-0.6B",
333 + [AttentionBackendEnum.B12X],
334 + _causal_mask,
335 + block_size=64,
336 + use_cuda_graph=True,
337 + max_num_seqs=batch_spec.batch_size,
338 + max_num_batched_tokens=max(sum(batch_spec.query_lens), 64),
339 + )
340 +
341 +
342 +def test_b12x_speculative_verification_uses_cuda_graph_plan(
343 + default_vllm_config,
344 + workspace_init,
345 +) -> None:
346 + """Exercise the verifier plan rather than the general extend plan."""
347 + _require_b12x_paged_attention()
348 + batch_spec = BatchSpec(seq_lens=[32, 40], query_lens=[4, 4])
349 +
350 + _test_backend_correctness(
351 + batch_spec,
352 + "Qwen/Qwen3-0.6B",
353 + [AttentionBackendEnum.B12X],
354 + _causal_mask,
355 + block_size=128,
356 + num_speculative_tokens=3,
357 + use_cuda_graph=True,
358 + max_num_seqs=batch_spec.batch_size,
359 + max_num_batched_tokens=max(sum(batch_spec.query_lens), 64),
360 + )
vllm/utils/b12x.py
+5
@@ -34,6 +34,7 @@ def _import_submodule(module_name: str) -> ModuleType | None:
34 _B12X_SUBMODULES = {
35 module_name: _import_submodule(module_name)
36 for module_name in (
37 + "b12x.attention.paged",
38 "b12x.gemm.blockscaled",
39 # TODO: Remove once B12X exposes the scale-swizzle API publicly.
40 "b12x._lib.intrinsics",
@@ -73,6 +74,10 @@ def get_b12x_fused_moe() -> ModuleType | None:
74 return _get_submodule("b12x.moe.fused_moe")
75
76
77 +def get_b12x_paged_attention() -> ModuleType | None:
78 + return _get_submodule("b12x.attention.paged")
79 +
80 +
81 def b12x_warmup_token_counts(
82 *,
83 max_tokens: int,
vllm/v1/attention/backends/b12x.py new
+1096
@@ -0,0 +1,1096 @@
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() -> 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 + )
vllm/v1/attention/backends/registry.py
+1
@@ -94,6 +94,7 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
94 ROCM_FLASHMLA_SPARSE_DSV4 = (
95 "vllm.models.deepseek_v4.amd.rocm.DeepseekV4ROCMAiterMLASparseBackend"
96 )
97 + B12X = "vllm.v1.attention.backends.b12x.B12xPagedAttentionBackend"
98 FLASH_ATTN_MLA = "vllm.v1.attention.backends.mla.flashattn_mla.FlashAttnMLABackend"
99 FLASH_ATTN_MLA_SPARSE = (
100 "vllm.v1.attention.backends.mla.flashattn_mla_sparse.FlashAttnMLASparseBackend"