flashinfer_mla_sparse_sm90.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. """FlashInfer sparse MLA backend for SM90 (Hopper) NoPE models.
  4. Wraps FlashInfer's ``BatchMLAPagedAttentionWrapper`` (FA2/FA3 paths), which
  5. as of FlashInfer 0.6.18 supports ``head_dim_kpe=0`` (GLM-5.3-Flash NoPE MLA)
  6. and FP8 E4M3 KV caches on SM90 with in-kernel dequantization: the FP8 cache
  7. is read directly (half the bf16 HBM traffic) and converted to BF16 in shared
  8. memory, while queries stay BF16 (no query quantization).
  9. Sparsity rides the same trick the FA-based sparse backend uses: with
  10. ``page_size=1`` the per-token top-k slot indices ARE the page table, so each
  11. query token becomes one varlen batch row whose ``kv_indices`` slice is its
  12. top-k row and whose ``kv_len`` is its valid count. Causality is already
  13. encoded by the indexer's selection, so ``causal=False``.
  14. CUDA-graph handling: ``plan()`` copies its inputs to host unconditionally,
  15. so it must stay outside graph capture. Each metadata builder owns a wrapper,
  16. reserved capture-stable device buffers, and the plan parameters. The wrapper
  17. bakes the per-row ``kv_len`` into its int schedule at plan() time — ``run()``
  18. never reads the device-side buffer. The builder plans (outside capture) from
  19. sync-free host upper bounds and clamps each scheduled work item's ``kv_end``
  20. on device to the exact valid count, so the kernel never reads past a row's
  21. valid prefix (the -1 tail of the converted index buffer) and async scheduling
  22. needs no D2H sync. Per-step content (top-k slots) is written into the
  23. reserved buffers by kernels inside the captured forward, and captured runs
  24. read the refreshed plan buffers on replay.
  25. KV cache format: plain contiguous E4M3 ``[num_blocks, block_size, 512]``
  26. (uint8 storage) with a per-tensor ``k_scale``; BF16 caches also work. The
  27. per-token x 128-channel-group ``ckv_scale_arr`` layout is supported by the
  28. kernel but not wired yet (it needs a group-quantizing cache-write op).
  29. """
  30. import os
  31. from dataclasses import dataclass
  32. from typing import Any, ClassVar
  33. import torch
  34. from vllm.config import VllmConfig
  35. from vllm.config.cache import CacheDType
  36. from vllm.model_executor.layers.attention.sparse_mla_attention import (
  37. SparseMLACommonImpl,
  38. )
  39. from vllm.platforms.interface import DeviceCapability
  40. from vllm.triton_utils import tl, triton
  41. from vllm.utils.flashinfer import has_flashinfer_sm90_nope_mla
  42. from vllm.v1.attention.backend import (
  43. AttentionBackend,
  44. AttentionLayer,
  45. CommonAttentionMetadata,
  46. MLAAttentionImpl,
  47. MultipleOf,
  48. )
  49. from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import (
  50. FlashInferMLASparseMetadata,
  51. FlashInferMLASparseMetadataBuilder,
  52. )
  53. from vllm.v1.attention.backends.mla.sparse_utils import (
  54. triton_convert_req_index_to_global_index,
  55. )
  56. from vllm.v1.kv_cache_interface import AttentionSpec, KVCacheLayout
  57. _FP8_KV_DTYPES = ("fp8", "fp8_e4m3")
  58. _WORKSPACE_BYTES = 128 * 1024 * 1024
  59. # TODO: FlashInfer bakes per-row kv_len into the plan on the host, which
  60. # forces a D2H sync under async scheduling. The plan-info layout constants
  61. # below and the int-workspace clamp in _SM90State work around this by
  62. # planning from sync-free upper bounds; delete them once FlashInfer accepts
  63. # an upper-bound plan() that clamps kv_end internally, or exposes a
  64. # GPU-side plan API (upstream ask:
  65. # https://github.com/flashinfer-ai/flashinfer/issues/5559, see
  66. # vllm-project/vllm#58684).
  67. # FlashInfer MLAPlanInfo layout (scheduler.cuh) field indices.
  68. _PLAN_INFO_LEN = 18
  69. _PI_NUM_BLKS_Y = 1
  70. _PI_Q_INDPTR = 2
  71. _PI_KV_INDPTR = 3
  72. _PI_KV_START = 13
  73. _PI_KV_END = 14
  74. _PI_WORK_INDPTR = 15
  75. # Planned lengths exceed the host upper bound by up to this many tokens so a
  76. # plan stays reusable across draft steps and consecutive decode steps; the
  77. # device-side clamp restores the exact lengths.
  78. _PLAN_SLACK = 32
  79. # GB10: a Triton sparse MLA kernel in place of FlashInfer's FA2 path, which
  80. # runs at ~10 TFLOPS on SM121 (gb10_sparse_mla.py; about 3.3x faster).
  81. _TRITON = os.environ.get("VLLM_TRITON_SPARSE_MLA") == "1"
  82. @triton.jit
  83. def _pack_topk_indices(
  84. slots,
  85. indptr,
  86. indices,
  87. WIDTH: tl.constexpr,
  88. BLOCK_SIZE: tl.constexpr,
  89. ):
  90. row = tl.program_id(0).to(tl.int64)
  91. cols = tl.program_id(1) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
  92. start = tl.load(indptr + row)
  93. end = tl.load(indptr + row + 1)
  94. mask = (cols < end - start) & (cols < WIDTH)
  95. values = tl.load(slots + row * WIDTH + cols, mask=mask, other=0)
  96. tl.store(indices + start + cols, tl.maximum(values, 0), mask=mask)
  97. @triton.jit
  98. def _clamp_work_kv_end(
  99. int_ws,
  100. saved_kv_end,
  101. seq_lens,
  102. query_start_loc,
  103. req_id_per_token,
  104. num_works_idx,
  105. q_indptr_off,
  106. kv_start_off,
  107. kv_end_off,
  108. TOPK: tl.constexpr,
  109. KPOOL: tl.constexpr,
  110. BLOCK_SIZE: tl.constexpr,
  111. ):
  112. # Clamp each planned work item to its row's exact valid count, derived
  113. # from the device batch layout exactly as the convert kernel counts it.
  114. works = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
  115. mask = works < tl.load(int_ws + num_works_idx)
  116. row = tl.load(int_ws + q_indptr_off + works, mask=mask, other=0)
  117. req = tl.load(req_id_per_token + row, mask=mask, other=0)
  118. seq_len = tl.load(seq_lens + req, mask=mask, other=0)
  119. query_end = tl.load(query_start_loc + req + 1, mask=mask, other=0)
  120. ctx = seq_len - query_end + row + 1
  121. valid = tl.where(ctx <= TOPK, ctx, TOPK + ctx % KPOOL)
  122. start = tl.load(int_ws + kv_start_off + works, mask=mask, other=0)
  123. end = tl.load(saved_kv_end + works, mask=mask, other=0)
  124. tl.store(
  125. int_ws + kv_end_off + works,
  126. tl.maximum(tl.minimum(end, valid), start),
  127. mask=mask,
  128. )
  129. class FlashInferMLASparseSM90Backend(AttentionBackend):
  130. supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
  131. supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
  132. "auto",
  133. "bfloat16",
  134. "fp8",
  135. "fp8_e4m3",
  136. ]
  137. @staticmethod
  138. def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
  139. return [MultipleOf(64)]
  140. @staticmethod
  141. def get_name() -> str:
  142. return "FLASHINFER_MLA_SPARSE_SM90"
  143. @staticmethod
  144. def get_builder_cls() -> type["FlashInferMLASparseSM90Builder"]:
  145. return FlashInferMLASparseSM90Builder
  146. @staticmethod
  147. def get_impl_cls() -> type[MLAAttentionImpl]:
  148. return FlashInferMLASparseSM90Impl
  149. @classmethod
  150. def get_supported_head_sizes(cls) -> list[int]:
  151. # 512 = ckv 512 + kpe 0 (NoPE); 576 = ckv 512 + kpe 64.
  152. return [512, 576]
  153. @classmethod
  154. def is_mla(cls) -> bool:
  155. return True
  156. @classmethod
  157. def is_sparse(cls) -> bool:
  158. return True
  159. @classmethod
  160. def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
  161. return capability.major in (9, 12)
  162. @classmethod
  163. def supports_combination(
  164. cls,
  165. head_size: int,
  166. dtype: torch.dtype,
  167. kv_cache_dtype: CacheDType | None,
  168. block_size: int | None,
  169. use_mla: bool,
  170. has_sink: bool,
  171. use_sparse: bool,
  172. use_mm_prefix: bool,
  173. device_capability: DeviceCapability,
  174. ) -> str | None:
  175. if kv_cache_dtype in ("fp8", "fp8_e4m3") and not has_flashinfer_sm90_nope_mla():
  176. return (
  177. "FLASHINFER_MLA_SPARSE_SM90 fp8 KV requires FlashInfer with "
  178. "SM90 MLA support (ckv_scale_arr in "
  179. "BatchMLAPagedAttentionWrapper.run, FlashInfer >= 0.6.18)"
  180. )
  181. if not use_sparse:
  182. return "FLASHINFER_MLA_SPARSE_SM90 requires sparse MLA"
  183. from vllm.config import get_current_vllm_config
  184. vllm_config = get_current_vllm_config()
  185. if vllm_config.model_config is not None:
  186. hf = vllm_config.model_config.hf_text_config
  187. # The SM90 FA2/FA3 kernel covers ckv=512 with kpe in {0, 64}
  188. # (NoPE models and DeepSeek-style rope MLA alike).
  189. if hf.kv_lora_rank != 512:
  190. return "FLASHINFER_MLA_SPARSE_SM90 requires kv_lora_rank=512"
  191. if hf.qk_rope_head_dim not in (0, 64):
  192. return "FLASHINFER_MLA_SPARSE_SM90 requires qk_rope_head_dim in (0, 64)"
  193. return None
  194. @staticmethod
  195. def get_kv_cache_shape(
  196. num_blocks: int,
  197. block_size: int,
  198. num_kv_heads: int,
  199. head_size: int,
  200. cache_dtype_str: str = "auto",
  201. ) -> tuple[int, ...]:
  202. return (num_blocks, block_size, head_size)
  203. @classmethod
  204. def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...]:
  205. return (KVCacheLayout.LBHNC,)
  206. class _SM90State:
  207. """Builder-owned wrapper, capture-stable buffers, and plan parameters.
  208. One instance serves every MLA layer in an attention group because the plan
  209. depends only on the batch shape, not the layer.
  210. """
  211. def __init__(
  212. self,
  213. device: torch.device,
  214. num_heads: int,
  215. kv_dtype: torch.dtype,
  216. max_tokens: int,
  217. topk_width: int,
  218. kv_lora_rank: int,
  219. qk_rope_head_dim: int,
  220. sm_scale: float,
  221. index_topk: int,
  222. index_kpool: int,
  223. ) -> None:
  224. from flashinfer.mla import BatchMLAPagedAttentionWrapper
  225. self.workspace = torch.empty(_WORKSPACE_BYTES, dtype=torch.uint8, device=device)
  226. self.device = device
  227. self.num_heads = num_heads
  228. self.kv_dtype = kv_dtype
  229. self.max_tokens = max_tokens
  230. self.topk_width = topk_width
  231. self.kv_lora_rank = kv_lora_rank
  232. self.qk_rope_head_dim = qk_rope_head_dim
  233. self.sm_scale = sm_scale
  234. self.index_topk = index_topk
  235. self.index_kpool = max(index_kpool, 1)
  236. # Largest valid count the convert kernel can produce for any context.
  237. self.max_valid = min(self.index_topk + self.index_kpool - 1, topk_width)
  238. # User-reserved buffers: with use_cuda_graph=True plan() refreshes
  239. # these in place, so run()'s captured kernels always read them.
  240. self.kv_indices = torch.zeros(
  241. max_tokens * topk_width, dtype=torch.int32, device=device
  242. )
  243. self.kv_len_arr = torch.full(
  244. (max_tokens,), topk_width, dtype=torch.int32, device=device
  245. )
  246. self.kv_indptr = torch.zeros(max_tokens + 1, dtype=torch.int32, device=device)
  247. self.wrapper = BatchMLAPagedAttentionWrapper(
  248. self.workspace,
  249. qo_indptr=torch.zeros(max_tokens + 1, dtype=torch.int32, device=device),
  250. kv_indptr=self.kv_indptr,
  251. kv_indices=self.kv_indices,
  252. kv_len_arr=self.kv_len_arr,
  253. use_cuda_graph=True,
  254. backend=("fa3" if torch.cuda.get_device_capability()[0] == 9 else "fa2"),
  255. )
  256. self._arange_cpu = torch.arange(self.max_tokens + 1, dtype=torch.int32)
  257. self._qo_cpu = torch.empty(self.max_tokens + 1, dtype=torch.int32)
  258. self._kv_cpu = torch.empty(self.max_tokens + 1, dtype=torch.int32)
  259. self._lens_cpu = torch.full(
  260. (self.max_tokens,), self.topk_width, dtype=torch.int32
  261. )
  262. # Planned per-work kv_end of the current plan, restored by each clamp;
  263. # sized on first plan to the planner's work capacity.
  264. self._saved_kv_end = torch.empty(0, dtype=torch.int32, device=device)
  265. self._planned_num_tokens = -1
  266. self._int_ws: torch.Tensor | None = None
  267. self._plan_offsets: tuple[int, int, int, int] | None = None
  268. def plan(
  269. self,
  270. num_tokens: int,
  271. kv_lens: torch.Tensor,
  272. cam: CommonAttentionMetadata | None,
  273. req_id_per_token: torch.Tensor | None,
  274. ) -> None:
  275. """Plan per-row KV lengths (CPU int32, ``[num_tokens]``).
  276. The wrapper bakes kv_len into its int schedule from host values, so
  277. rows past their valid count would send the kernel into the -1 tail of
  278. the converted index buffer. With ``cam`` and ``req_id_per_token``
  279. (device batch layout), ``kv_lens`` only needs to upper-bound the
  280. valid counts: the plan is padded by ``_PLAN_SLACK`` and reused while
  281. the bound still fits, and every call clamps each work item's kv_end
  282. on device to the exact count, so no D2H sync is needed. Without them
  283. the lengths must be exact. Must run outside CUDA graph capture: the
  284. in-place refreshed plan_info/indptr buffers are what captured runs
  285. read.
  286. """
  287. if torch.cuda.is_current_stream_capturing():
  288. raise RuntimeError(
  289. "FlashInferMLASparseSM90 plan() called inside CUDA graph "
  290. "capture; lengths must be planned host-side before capture."
  291. )
  292. kv_lens = kv_lens.to(torch.int32)
  293. if cam is None or req_id_per_token is None:
  294. self._plan(num_tokens, kv_lens)
  295. self._planned_num_tokens = -1
  296. return
  297. planned = self._lens_cpu[:num_tokens]
  298. if num_tokens != self._planned_num_tokens or not bool(
  299. ((planned >= kv_lens) & (planned <= kv_lens + _PLAN_SLACK)).all()
  300. ):
  301. self._plan(
  302. num_tokens, torch.clamp(kv_lens + _PLAN_SLACK, max=self.max_valid)
  303. )
  304. self._planned_num_tokens = num_tokens
  305. self._clamp_kv_end(cam, req_id_per_token)
  306. def _plan(self, num_tokens: int, kv_lens: torch.Tensor) -> None:
  307. # CPU staging buffers are filled in place: plan() runs per step
  308. # (once per draft/verify metadata build), so per-call allocations and
  309. # device round trips are on the hot path. Passing CPU tensors lets
  310. # the wrapper's internal .to("cpu") no-op; its reserved-buffer
  311. # copy_ then performs the single H2D transfer per tensor.
  312. # use_cuda_graph=True makes the wrapper copy qo/kv indptr into its
  313. # fixed (max_tokens+1)-sized buffers with exact-size copy_, so the
  314. # indptr must always be full-size. Rows past num_tokens are padded
  315. # empty (qo_indptr flat at num_tokens) — zero-query rows read no q
  316. # and schedule no work. FlashInfer 0.7 requires packed CSR offsets
  317. # matching the exact lengths, including zero-length padded rows.
  318. torch.clamp(self._arange_cpu, max=num_tokens, out=self._qo_cpu)
  319. self._lens_cpu.zero_()
  320. self._lens_cpu[:num_tokens] = kv_lens
  321. self._kv_cpu[0] = 0
  322. torch.cumsum(self._lens_cpu, dim=0, out=self._kv_cpu[1:])
  323. # Only the live prefix of kv_indices: the wrapper snapshots and
  324. # copies whatever is passed.
  325. num_indices = max(int(self._kv_cpu[-1]), 1)
  326. self.wrapper.plan(
  327. self._qo_cpu,
  328. self._kv_cpu,
  329. self.kv_indices[:num_indices],
  330. self._lens_cpu,
  331. self.num_heads,
  332. self.kv_lora_rank, # head_dim_ckv
  333. self.qk_rope_head_dim, # 0 (NoPE) or 64 (rope MLA)
  334. 1, # page_size: top-k slots are the page table
  335. False, # causal: encoded by the indexer's selection
  336. self.sm_scale,
  337. q_data_type=torch.bfloat16,
  338. kv_data_type=self.kv_dtype,
  339. )
  340. plan_info = [int(x) for x in self.wrapper._plan_info]
  341. assert len(plan_info) == _PLAN_INFO_LEN, plan_info
  342. # Per-work arrays are laid out back to back at the planner's capacity.
  343. max_works = (plan_info[_PI_KV_INDPTR] - plan_info[_PI_Q_INDPTR]) // 4
  344. if self._saved_kv_end.numel() != max_works:
  345. self._saved_kv_end = torch.empty(
  346. max_works, dtype=torch.int32, device=self.device
  347. )
  348. self._int_ws = self.wrapper._planned_backend._int_workspace_buffer.view(
  349. torch.int32
  350. )
  351. self._plan_offsets = (
  352. plan_info[_PI_WORK_INDPTR] // 4 + plan_info[_PI_NUM_BLKS_Y],
  353. plan_info[_PI_Q_INDPTR] // 4,
  354. plan_info[_PI_KV_START] // 4,
  355. plan_info[_PI_KV_END] // 4,
  356. )
  357. kv_end_off = self._plan_offsets[3]
  358. self._saved_kv_end.copy_(self._int_ws[kv_end_off : kv_end_off + max_works])
  359. def _clamp_kv_end(
  360. self, cam: CommonAttentionMetadata, req_id_per_token: torch.Tensor
  361. ) -> None:
  362. assert self._int_ws is not None and self._plan_offsets is not None
  363. block = 1024
  364. _clamp_work_kv_end[(triton.cdiv(self._saved_kv_end.numel(), block),)](
  365. self._int_ws,
  366. self._saved_kv_end,
  367. cam.seq_lens,
  368. cam.query_start_loc,
  369. req_id_per_token,
  370. *self._plan_offsets,
  371. TOPK=self.index_topk,
  372. KPOOL=self.index_kpool,
  373. BLOCK_SIZE=block,
  374. )
  375. def pack_indices(self, topk_slots: torch.Tensor) -> None:
  376. num_tokens, width = topk_slots.shape
  377. _pack_topk_indices[(num_tokens, triton.cdiv(width, 256))](
  378. topk_slots, self.kv_indptr, self.kv_indices, width, 256
  379. )
  380. @dataclass
  381. class FlashInferMLASparseSM90Metadata(FlashInferMLASparseMetadata):
  382. state: _SM90State | None = None
  383. class FlashInferMLASparseSM90Builder(FlashInferMLASparseMetadataBuilder):
  384. """Reuse the common sparse metadata (req ids, topk buffer access)."""
  385. metadata_cls = FlashInferMLASparseSM90Metadata
  386. def __init__(
  387. self,
  388. kv_cache_spec: "AttentionSpec",
  389. layer_names: list[str],
  390. vllm_config: "VllmConfig",
  391. device: torch.device,
  392. ) -> None:
  393. super().__init__(kv_cache_spec, layer_names, vllm_config, device)
  394. attention_layer = vllm_config.compilation_config.static_forward_context[
  395. layer_names[0]
  396. ]
  397. self._attention_layer = attention_layer
  398. impl = attention_layer.impl
  399. if not isinstance(impl, FlashInferMLASparseSM90Impl):
  400. raise TypeError(
  401. "FlashInferMLASparseSM90Builder requires an SM90 FlashInfer "
  402. f"implementation, got {type(impl).__name__}."
  403. )
  404. topk_indices_buffer = impl.topk_indices_buffer
  405. assert topk_indices_buffer is not None
  406. hf_config = vllm_config.model_config.hf_text_config
  407. assert hf_config.index_topk is not None
  408. self.state = _SM90State(
  409. device,
  410. impl.num_heads,
  411. # SM90-FP8-KV-DTYPE: fp8 caches are stored as uint8; plan with fp8.
  412. (torch.float8_e4m3fn if impl.use_fp8_kv_cache else kv_cache_spec.dtype),
  413. vllm_config.scheduler_config.max_num_batched_tokens,
  414. topk_indices_buffer.shape[1],
  415. kv_lora_rank=impl.kv_lora_rank,
  416. qk_rope_head_dim=impl.qk_rope_head_dim,
  417. sm_scale=impl.scale,
  418. index_topk=int(hf_config.index_topk),
  419. index_kpool=int(getattr(hf_config, "index_kpool", 1) or 1),
  420. )
  421. spec_config = vllm_config.speculative_config
  422. self._adaptive_verification = bool(
  423. spec_config is not None and spec_config.enable_adaptive_verification
  424. )
  425. def _kv_lens_host(
  426. self, cam: CommonAttentionMetadata
  427. ) -> tuple[int, torch.Tensor, bool]:
  428. """Host per-row KV lengths for the plan and whether they are exact.
  429. A row for the j-th query token of request i attends
  430. ``seq_lens[i] - q_len[i] + j + 1`` tokens. The indexer's selection
  431. then bounds the valid count: contexts up to ``index_topk`` select
  432. everything (valid == context); longer contexts keep the top
  433. ``index_topk`` pool-expanded tokens plus the trailing incomplete
  434. pool (valid == ``index_topk + context % index_kpool``). Both match
  435. the count of non -1 entries the convert kernel produces.
  436. With ``seq_lens_cpu_upper_bound`` (optimistic under async spec
  437. decode) returns sync-free upper bounds, which the plan clamps on
  438. device. Otherwise returns exact lengths via a D2H copy.
  439. """
  440. num_reqs = cam.num_reqs
  441. qsl = cam.query_start_loc_cpu[: num_reqs + 1]
  442. num_rows = int(qsl[-1])
  443. if num_rows == 0:
  444. return 0, torch.zeros(0, dtype=torch.int32), True
  445. state = self.state
  446. sl_host = cam.seq_lens_cpu_upper_bound
  447. if sl_host is None:
  448. ctx = self._row_contexts(qsl, cam.seq_lens[:num_reqs].cpu())
  449. topk, kpool = state.index_topk, state.index_kpool
  450. lens = torch.where(ctx <= topk, ctx, topk + ctx % kpool)
  451. return num_rows, lens.to(torch.int32), True
  452. if self._adaptive_verification:
  453. # The host query split may not match the device one, so bound
  454. # every row by the longest request.
  455. ctx = torch.full((num_rows,), int(sl_host[:num_reqs].max()))
  456. else:
  457. ctx = self._row_contexts(qsl, sl_host[:num_reqs])
  458. return num_rows, torch.clamp(ctx, max=state.max_valid), False
  459. @staticmethod
  460. def _row_contexts(qsl: torch.Tensor, seq_lens: torch.Tensor) -> torch.Tensor:
  461. num_reqs = qsl.shape[0] - 1
  462. q_lens = (qsl[1:] - qsl[:-1]).to(torch.int64)
  463. first_ctx = seq_lens.to(torch.int64) - q_lens + 1
  464. req_of_row = torch.repeat_interleave(
  465. torch.arange(num_reqs, dtype=torch.int64), q_lens
  466. )
  467. rows = torch.arange(int(qsl[-1]), dtype=torch.int64)
  468. return first_ctx[req_of_row] + rows - qsl.to(torch.int64)[req_of_row]
  469. def build(
  470. self,
  471. common_prefix_len: int,
  472. common_attn_metadata: CommonAttentionMetadata,
  473. fast_build: bool = False,
  474. ) -> FlashInferMLASparseSM90Metadata:
  475. metadata = super().build(common_prefix_len, common_attn_metadata, fast_build)
  476. assert isinstance(metadata, FlashInferMLASparseSM90Metadata)
  477. if _TRITON:
  478. # The Triton kernel needs no host-side schedule, and the host KV
  479. # lengths cost a device-to-host sync per step under async
  480. # scheduling (it stalls the CPU until the previous step ends).
  481. metadata.state = self.state
  482. return metadata
  483. # Plan outside any CUDA graph capture; captured runs read the
  484. # refreshed buffers.
  485. num_rows, kv_lens, exact = self._kv_lens_host(common_attn_metadata)
  486. # MHA prefills are removed from q before forward_mqa. Plan only the
  487. # decode prefix in that case, using the same routing decision as MLA.
  488. if metadata.num_prefills > 0 and self._attention_layer._use_sparse_mha(
  489. metadata
  490. ):
  491. num_rows = metadata.num_decode_tokens
  492. kv_lens = kv_lens[:num_rows]
  493. if exact:
  494. self.state.plan(num_rows, kv_lens, None, None)
  495. else:
  496. self.state.plan(
  497. num_rows, kv_lens, common_attn_metadata, metadata.req_id_per_token
  498. )
  499. metadata.state = self.state
  500. return metadata
  501. class FlashInferMLASparseSM90Impl(SparseMLACommonImpl[FlashInferMLASparseSM90Metadata]):
  502. def __init__(
  503. self,
  504. num_heads: int,
  505. head_size: int,
  506. scale: float,
  507. num_kv_heads: int,
  508. alibi_slopes: list[float] | None,
  509. sliding_window: int | None,
  510. kv_cache_dtype: str,
  511. logits_soft_cap: float | None,
  512. attn_type: str,
  513. kv_sharing_target_layer_name: str | None,
  514. topk_indices_buffer: torch.Tensor | None = None,
  515. indexer: Any | None = None,
  516. **mla_args: Any,
  517. ) -> None:
  518. if any([alibi_slopes, sliding_window, logits_soft_cap]):
  519. raise NotImplementedError(
  520. "FlashInferMLASparseSM90Impl does not support alibi, sliding "
  521. "window, or logits soft cap."
  522. )
  523. super().__init__(
  524. num_heads,
  525. head_size,
  526. scale,
  527. num_kv_heads,
  528. alibi_slopes,
  529. sliding_window,
  530. kv_cache_dtype,
  531. logits_soft_cap,
  532. attn_type,
  533. kv_sharing_target_layer_name,
  534. indexer=indexer,
  535. topk_indices_buffer=topk_indices_buffer,
  536. **mla_args,
  537. )
  538. assert self.topk_indices_buffer is not None
  539. self.supports_quant_query_input = False
  540. self.use_fp8_kv_cache = self.kv_cache_dtype in _FP8_KV_DTYPES
  541. def forward_mqa(
  542. self,
  543. q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
  544. kv_c_and_k_pe_cache: torch.Tensor,
  545. attn_metadata: FlashInferMLASparseSM90Metadata,
  546. layer: AttentionLayer,
  547. ) -> tuple[torch.Tensor, torch.Tensor | None]:
  548. if not isinstance(q, tuple):
  549. raise NotImplementedError(
  550. "FlashInferMLASparseSM90Impl expects split (q_nope, q_rope)."
  551. )
  552. q_nope, q_rope = q
  553. num_tokens = q_rope.shape[0]
  554. # NoPE models hand a zero-width rope tensor through; rope MLA hands
  555. # the real 64-dim part. The kernel takes both as-is.
  556. q_pe = q_rope.reshape(num_tokens, self.num_heads, self.qk_rope_head_dim)
  557. assert self.topk_indices_buffer is not None
  558. topk_indices = self.topk_indices_buffer[:num_tokens]
  559. # return_valid_counts=True keeps the compacted-prefix layout: valid
  560. # entries at [0, valid_count), -1 past it — exactly the prefix the
  561. # planned per-row lengths address.
  562. topk_slots, valid_counts = triton_convert_req_index_to_global_index(
  563. attn_metadata.req_id_per_token[:num_tokens],
  564. attn_metadata.block_table,
  565. topk_indices,
  566. BLOCK_SIZE=attn_metadata.block_size,
  567. NUM_TOPK_TOKENS=topk_indices.shape[1],
  568. return_valid_counts=True,
  569. )
  570. if _TRITON:
  571. from vllm.v1.attention.backends.mla.gb10_sparse_mla import sparse_mla
  572. kv = kv_c_and_k_pe_cache.view(torch.float8_e4m3fn) if self.use_fp8_kv_cache else kv_c_and_k_pe_cache
  573. out = sparse_mla(q_nope, kv.view(-1, self.head_size), topk_slots, valid_counts, self.scale,
  574. float(layer._k_scale_float or 1.0) if self.use_fp8_kv_cache else 1.0)
  575. return out, None
  576. state = attn_metadata.state
  577. assert state is not None
  578. # Pack valid prefixes at the offsets refreshed by plan() before replay.
  579. state.pack_indices(topk_slots)
  580. flat = (
  581. kv_c_and_k_pe_cache.view(torch.float8_e4m3fn)
  582. if self.use_fp8_kv_cache
  583. else kv_c_and_k_pe_cache
  584. ).reshape(-1, 1, self.head_size)
  585. ckv = flat[..., : self.kv_lora_rank]
  586. kpe = flat[..., self.kv_lora_rank :]
  587. scale_kwargs = (
  588. {"ckv_scale": float(layer._k_scale_float or 1.0), "kpe_scale": 1.0}
  589. if self.use_fp8_kv_cache
  590. else {}
  591. )
  592. out = state.wrapper.run(q_nope, q_pe, ckv, kpe, **scale_kwargs)
  593. return out, None