flashinfer_mla_sparse_sm90.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  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
  18. at plan() time — ``run()`` never reads the device-side buffer — so the
  19. metadata builder replans every step (outside capture) with exact host-side
  20. lengths derived from the batch's sequence lengths; a full-width schedule
  21. would send the kernel past each row's valid count into the -1 tail of the
  22. converted index buffer (illegal address). Per-step content (top-k slots)
  23. is written into the reserved buffers by kernels inside the captured
  24. forward, and captured runs 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. # GB10: a Triton sparse MLA kernel in place of FlashInfer's FA2 path, which
  60. # runs at ~10 TFLOPS on SM121 (gb10_sparse_mla.py; about 3.3x faster).
  61. _TRITON = os.environ.get("VLLM_TRITON_SPARSE_MLA") == "1"
  62. @triton.jit
  63. def _pack_topk_indices(
  64. slots,
  65. indptr,
  66. indices,
  67. WIDTH: tl.constexpr,
  68. BLOCK_SIZE: tl.constexpr,
  69. ):
  70. row = tl.program_id(0).to(tl.int64)
  71. cols = tl.program_id(1) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
  72. start = tl.load(indptr + row)
  73. end = tl.load(indptr + row + 1)
  74. mask = (cols < end - start) & (cols < WIDTH)
  75. values = tl.load(slots + row * WIDTH + cols, mask=mask, other=0)
  76. tl.store(indices + start + cols, tl.maximum(values, 0), mask=mask)
  77. class FlashInferMLASparseSM90Backend(AttentionBackend):
  78. supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
  79. supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
  80. "auto",
  81. "bfloat16",
  82. "fp8",
  83. "fp8_e4m3",
  84. ]
  85. @staticmethod
  86. def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
  87. return [MultipleOf(64)]
  88. @staticmethod
  89. def get_name() -> str:
  90. return "FLASHINFER_MLA_SPARSE_SM90"
  91. @staticmethod
  92. def get_builder_cls() -> type["FlashInferMLASparseSM90Builder"]:
  93. return FlashInferMLASparseSM90Builder
  94. @staticmethod
  95. def get_impl_cls() -> type[MLAAttentionImpl]:
  96. return FlashInferMLASparseSM90Impl
  97. @classmethod
  98. def get_supported_head_sizes(cls) -> list[int]:
  99. # 512 = ckv 512 + kpe 0 (NoPE); 576 = ckv 512 + kpe 64.
  100. return [512, 576]
  101. @classmethod
  102. def is_mla(cls) -> bool:
  103. return True
  104. @classmethod
  105. def is_sparse(cls) -> bool:
  106. return True
  107. @classmethod
  108. def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
  109. return capability.major in (9, 12)
  110. @classmethod
  111. def supports_combination(
  112. cls,
  113. head_size: int,
  114. dtype: torch.dtype,
  115. kv_cache_dtype: CacheDType | None,
  116. block_size: int | None,
  117. use_mla: bool,
  118. has_sink: bool,
  119. use_sparse: bool,
  120. use_mm_prefix: bool,
  121. device_capability: DeviceCapability,
  122. ) -> str | None:
  123. if kv_cache_dtype in ("fp8", "fp8_e4m3") and not has_flashinfer_sm90_nope_mla():
  124. return (
  125. "FLASHINFER_MLA_SPARSE_SM90 fp8 KV requires FlashInfer with "
  126. "SM90 MLA support (ckv_scale_arr in "
  127. "BatchMLAPagedAttentionWrapper.run, FlashInfer >= 0.6.18)"
  128. )
  129. if not use_sparse:
  130. return "FLASHINFER_MLA_SPARSE_SM90 requires sparse MLA"
  131. from vllm.config import get_current_vllm_config
  132. vllm_config = get_current_vllm_config()
  133. if vllm_config.model_config is not None:
  134. hf = vllm_config.model_config.hf_text_config
  135. # The SM90 FA2/FA3 kernel covers ckv=512 with kpe in {0, 64}
  136. # (NoPE models and DeepSeek-style rope MLA alike).
  137. if hf.kv_lora_rank != 512:
  138. return "FLASHINFER_MLA_SPARSE_SM90 requires kv_lora_rank=512"
  139. if hf.qk_rope_head_dim not in (0, 64):
  140. return "FLASHINFER_MLA_SPARSE_SM90 requires qk_rope_head_dim in (0, 64)"
  141. return None
  142. @staticmethod
  143. def get_kv_cache_shape(
  144. num_blocks: int,
  145. block_size: int,
  146. num_kv_heads: int,
  147. head_size: int,
  148. cache_dtype_str: str = "auto",
  149. ) -> tuple[int, ...]:
  150. return (num_blocks, block_size, head_size)
  151. @classmethod
  152. def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...]:
  153. return (KVCacheLayout.LBHNC,)
  154. class _SM90State:
  155. """Builder-owned wrapper, capture-stable buffers, and plan parameters.
  156. One instance serves every MLA layer in an attention group because the plan
  157. depends only on the batch shape, not the layer.
  158. """
  159. def __init__(
  160. self,
  161. device: torch.device,
  162. num_heads: int,
  163. kv_dtype: torch.dtype,
  164. max_tokens: int,
  165. topk_width: int,
  166. kv_lora_rank: int,
  167. qk_rope_head_dim: int,
  168. sm_scale: float,
  169. ) -> None:
  170. from flashinfer.mla import BatchMLAPagedAttentionWrapper
  171. self.workspace = torch.empty(_WORKSPACE_BYTES, dtype=torch.uint8, device=device)
  172. self.device = device
  173. self.num_heads = num_heads
  174. self.kv_dtype = kv_dtype
  175. self.max_tokens = max_tokens
  176. self.topk_width = topk_width
  177. self.kv_lora_rank = kv_lora_rank
  178. self.qk_rope_head_dim = qk_rope_head_dim
  179. self.sm_scale = sm_scale
  180. # User-reserved buffers: with use_cuda_graph=True plan() refreshes
  181. # these in place, so run()'s captured kernels always read them.
  182. self.kv_indices = torch.zeros(
  183. max_tokens * topk_width, dtype=torch.int32, device=device
  184. )
  185. self.kv_len_arr = torch.full(
  186. (max_tokens,), topk_width, dtype=torch.int32, device=device
  187. )
  188. self.kv_indptr = torch.zeros(max_tokens + 1, dtype=torch.int32, device=device)
  189. self.wrapper = BatchMLAPagedAttentionWrapper(
  190. self.workspace,
  191. qo_indptr=torch.zeros(max_tokens + 1, dtype=torch.int32, device=device),
  192. kv_indptr=self.kv_indptr,
  193. kv_indices=self.kv_indices,
  194. kv_len_arr=self.kv_len_arr,
  195. use_cuda_graph=True,
  196. backend=("fa3" if torch.cuda.get_device_capability()[0] == 9 else "fa2"),
  197. )
  198. self._arange_cpu = torch.arange(self.max_tokens + 1, dtype=torch.int32)
  199. self._qo_cpu = torch.empty(self.max_tokens + 1, dtype=torch.int32)
  200. self._kv_cpu = torch.empty(self.max_tokens + 1, dtype=torch.int32)
  201. self._lens_cpu = torch.full(
  202. (self.max_tokens,), self.topk_width, dtype=torch.int32
  203. )
  204. def plan(self, num_tokens: int, kv_lens: torch.Tensor) -> None:
  205. """Replan with exact per-row KV lengths (CPU int32, ``[num_tokens]``).
  206. The wrapper bakes kv_len into its int schedule from host values;
  207. run() never reads the device kv_len_arr buffer. Scheduling with the
  208. full buffer width while kv_indices rows carry a -1 tail past each
  209. row's valid count makes the kernel compute ``-1 * ckv_stride_page``
  210. (illegal address), so the lengths must be exact at plan time and
  211. replanned every step as contexts grow. Must run outside CUDA graph
  212. capture: the in-place refreshed plan_info/indptr buffers are what
  213. captured runs read.
  214. """
  215. if torch.cuda.is_current_stream_capturing():
  216. raise RuntimeError(
  217. "FlashInferMLASparseSM90 plan() called inside CUDA graph "
  218. "capture; lengths must be planned host-side before capture."
  219. )
  220. # CPU staging buffers are filled in place: plan() runs per step
  221. # (once per draft/verify metadata build), so per-call allocations and
  222. # device round trips are on the hot path. Passing CPU tensors lets
  223. # the wrapper's internal .to("cpu") no-op; its reserved-buffer
  224. # copy_ then performs the single H2D transfer per tensor.
  225. # use_cuda_graph=True makes the wrapper copy qo/kv indptr into its
  226. # fixed (max_tokens+1)-sized buffers with exact-size copy_, so the
  227. # indptr must always be full-size. Rows past num_tokens are padded
  228. # empty (qo_indptr flat at num_tokens) — zero-query rows read no q
  229. # and schedule no work. FlashInfer 0.7 requires packed CSR offsets
  230. # matching the exact lengths, including zero-length padded rows.
  231. torch.clamp(self._arange_cpu, max=num_tokens, out=self._qo_cpu)
  232. self._lens_cpu.zero_()
  233. self._lens_cpu[:num_tokens] = kv_lens.to(torch.int32)
  234. self._kv_cpu[0] = 0
  235. torch.cumsum(self._lens_cpu, dim=0, out=self._kv_cpu[1:])
  236. self.wrapper.plan(
  237. self._qo_cpu,
  238. self._kv_cpu,
  239. self.kv_indices,
  240. self._lens_cpu,
  241. self.num_heads,
  242. self.kv_lora_rank, # head_dim_ckv
  243. self.qk_rope_head_dim, # 0 (NoPE) or 64 (rope MLA)
  244. 1, # page_size: top-k slots are the page table
  245. False, # causal: encoded by the indexer's selection
  246. self.sm_scale,
  247. q_data_type=torch.bfloat16,
  248. kv_data_type=self.kv_dtype,
  249. )
  250. def pack_indices(self, topk_slots: torch.Tensor) -> None:
  251. num_tokens, width = topk_slots.shape
  252. _pack_topk_indices[(num_tokens, triton.cdiv(width, 256))](
  253. topk_slots, self.kv_indptr, self.kv_indices, width, 256
  254. )
  255. @dataclass
  256. class FlashInferMLASparseSM90Metadata(FlashInferMLASparseMetadata):
  257. state: _SM90State | None = None
  258. class FlashInferMLASparseSM90Builder(FlashInferMLASparseMetadataBuilder):
  259. """Reuse the common sparse metadata (req ids, topk buffer access)."""
  260. metadata_cls = FlashInferMLASparseSM90Metadata
  261. def __init__(
  262. self,
  263. kv_cache_spec: "AttentionSpec",
  264. layer_names: list[str],
  265. vllm_config: "VllmConfig",
  266. device: torch.device,
  267. ) -> None:
  268. super().__init__(kv_cache_spec, layer_names, vllm_config, device)
  269. attention_layer = vllm_config.compilation_config.static_forward_context[
  270. layer_names[0]
  271. ]
  272. self._attention_layer = attention_layer
  273. impl = attention_layer.impl
  274. if not isinstance(impl, FlashInferMLASparseSM90Impl):
  275. raise TypeError(
  276. "FlashInferMLASparseSM90Builder requires an SM90 FlashInfer "
  277. f"implementation, got {type(impl).__name__}."
  278. )
  279. topk_indices_buffer = impl.topk_indices_buffer
  280. assert topk_indices_buffer is not None
  281. self.state = _SM90State(
  282. device,
  283. impl.num_heads,
  284. # SM90-FP8-KV-DTYPE: fp8 caches are stored as uint8; plan with fp8.
  285. (torch.float8_e4m3fn if impl.use_fp8_kv_cache else kv_cache_spec.dtype),
  286. vllm_config.scheduler_config.max_num_batched_tokens,
  287. topk_indices_buffer.shape[1],
  288. kv_lora_rank=impl.kv_lora_rank,
  289. qk_rope_head_dim=impl.qk_rope_head_dim,
  290. sm_scale=impl.scale,
  291. )
  292. # seq_lens_cpu_upper_bound is optimistic on decode rows under async
  293. # spec decode, so the fast sync-free path is only safe without it;
  294. # under async scheduling the exact (device) positions are used at
  295. # the cost of one D2H sync per metadata build.
  296. self._async_scheduling = bool(vllm_config.scheduler_config.async_scheduling)
  297. hf_config = vllm_config.model_config.hf_text_config
  298. assert hf_config.index_topk is not None
  299. self._index_topk = int(hf_config.index_topk)
  300. self._index_kpool = int(getattr(hf_config, "index_kpool", 1) or 1)
  301. def _kv_lens_host(self, cam: CommonAttentionMetadata) -> tuple[int, torch.Tensor]:
  302. """Exact per-row KV lengths, host-side (the flashinfer wrapper bakes
  303. them into its schedule at plan time; there is no device-side path).
  304. A row for the j-th query token of request i attends
  305. ``seq_lens[i] - q_len[i] + j + 1`` tokens. The indexer's selection
  306. then bounds the valid count: contexts up to ``index_topk`` select
  307. everything (valid == context); longer contexts keep the top
  308. ``index_topk`` pool-expanded tokens plus the trailing incomplete
  309. pool (valid == ``index_topk + context % index_kpool``). Both match
  310. the count of non -1 entries the convert kernel produces.
  311. """
  312. num_reqs = cam.num_reqs
  313. qsl = cam.query_start_loc_cpu[: num_reqs + 1]
  314. num_rows = int(qsl[-1])
  315. if num_rows == 0:
  316. return 0, torch.zeros(0, dtype=torch.int32)
  317. # Row context == position + 1. Without async scheduling the
  318. # maintained host upper bound is exact, giving a sync-free path;
  319. # under async scheduling it is optimistic on decode rows, so fall
  320. # back to the exact device positions (one D2H sync per build). The
  321. # seq_lens derivation equals position + 1 because positions are
  322. # contiguous per request.
  323. sl_host = cam.seq_lens_cpu_upper_bound
  324. positions = cam.positions
  325. if not self._async_scheduling and sl_host is not None:
  326. seq_lens = sl_host[:num_reqs].to(torch.int32)
  327. q_lens = qsl[1:] - qsl[:-1]
  328. first_pos = seq_lens - q_lens
  329. req_of_row = torch.repeat_interleave(
  330. torch.arange(num_reqs, dtype=torch.int64), q_lens.to(torch.int64)
  331. )
  332. rows = torch.arange(num_rows, dtype=torch.int32)
  333. ctx = (
  334. first_pos.to(torch.int64)[req_of_row]
  335. + rows.to(torch.int64)
  336. - qsl.to(torch.int64)[req_of_row]
  337. + 1
  338. )
  339. elif positions is not None and num_rows <= positions.shape[0]:
  340. ctx = positions[:num_rows].cpu().to(torch.int64) + 1
  341. else:
  342. seq_lens = cam.seq_lens[:num_reqs].cpu().to(torch.int32)
  343. q_lens = qsl[1:] - qsl[:-1]
  344. first_pos = seq_lens - q_lens
  345. req_of_row = torch.repeat_interleave(
  346. torch.arange(num_reqs, dtype=torch.int64), q_lens.to(torch.int64)
  347. )
  348. rows = torch.arange(num_rows, dtype=torch.int32)
  349. ctx = (
  350. first_pos.to(torch.int64)[req_of_row]
  351. + rows.to(torch.int64)
  352. - qsl.to(torch.int64)[req_of_row]
  353. + 1
  354. )
  355. topk = self._index_topk
  356. lens = torch.where(ctx <= topk, ctx, topk + ctx % self._index_kpool)
  357. return num_rows, lens.to(torch.int32)
  358. def build(
  359. self,
  360. common_prefix_len: int,
  361. common_attn_metadata: CommonAttentionMetadata,
  362. fast_build: bool = False,
  363. ) -> FlashInferMLASparseSM90Metadata:
  364. metadata = super().build(common_prefix_len, common_attn_metadata, fast_build)
  365. assert isinstance(metadata, FlashInferMLASparseSM90Metadata)
  366. if _TRITON:
  367. # The Triton kernel needs no host-side schedule, and the host KV
  368. # lengths cost a device-to-host sync per step under async
  369. # scheduling (it stalls the CPU until the previous step ends).
  370. metadata.state = self.state
  371. return metadata
  372. # Replan every step outside any CUDA graph capture with this step's
  373. # exact per-row lengths; captured runs read the refreshed buffers.
  374. num_rows, kv_lens = self._kv_lens_host(common_attn_metadata)
  375. # MHA prefills are removed from q before forward_mqa. Plan only the
  376. # decode prefix in that case, using the same routing decision as MLA.
  377. if metadata.num_prefills > 0 and self._attention_layer._use_sparse_mha(
  378. metadata
  379. ):
  380. num_rows = metadata.num_decode_tokens
  381. kv_lens = kv_lens[:num_rows]
  382. self.state.plan(num_rows, kv_lens)
  383. metadata.state = self.state
  384. return metadata
  385. class FlashInferMLASparseSM90Impl(SparseMLACommonImpl[FlashInferMLASparseSM90Metadata]):
  386. def __init__(
  387. self,
  388. num_heads: int,
  389. head_size: int,
  390. scale: float,
  391. num_kv_heads: int,
  392. alibi_slopes: list[float] | None,
  393. sliding_window: int | None,
  394. kv_cache_dtype: str,
  395. logits_soft_cap: float | None,
  396. attn_type: str,
  397. kv_sharing_target_layer_name: str | None,
  398. topk_indices_buffer: torch.Tensor | None = None,
  399. indexer: Any | None = None,
  400. **mla_args: Any,
  401. ) -> None:
  402. if any([alibi_slopes, sliding_window, logits_soft_cap]):
  403. raise NotImplementedError(
  404. "FlashInferMLASparseSM90Impl does not support alibi, sliding "
  405. "window, or logits soft cap."
  406. )
  407. super().__init__(
  408. num_heads,
  409. head_size,
  410. scale,
  411. num_kv_heads,
  412. alibi_slopes,
  413. sliding_window,
  414. kv_cache_dtype,
  415. logits_soft_cap,
  416. attn_type,
  417. kv_sharing_target_layer_name,
  418. indexer=indexer,
  419. topk_indices_buffer=topk_indices_buffer,
  420. **mla_args,
  421. )
  422. assert self.topk_indices_buffer is not None
  423. self.supports_quant_query_input = False
  424. self.use_fp8_kv_cache = self.kv_cache_dtype in _FP8_KV_DTYPES
  425. def forward_mqa(
  426. self,
  427. q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
  428. kv_c_and_k_pe_cache: torch.Tensor,
  429. attn_metadata: FlashInferMLASparseSM90Metadata,
  430. layer: AttentionLayer,
  431. ) -> tuple[torch.Tensor, torch.Tensor | None]:
  432. if not isinstance(q, tuple):
  433. raise NotImplementedError(
  434. "FlashInferMLASparseSM90Impl expects split (q_nope, q_rope)."
  435. )
  436. q_nope, q_rope = q
  437. num_tokens = q_rope.shape[0]
  438. # NoPE models hand a zero-width rope tensor through; rope MLA hands
  439. # the real 64-dim part. The kernel takes both as-is.
  440. q_pe = q_rope.reshape(num_tokens, self.num_heads, self.qk_rope_head_dim)
  441. assert self.topk_indices_buffer is not None
  442. topk_indices = self.topk_indices_buffer[:num_tokens]
  443. # return_valid_counts=True keeps the compacted-prefix layout: valid
  444. # entries at [0, valid_count), -1 past it — exactly the prefix the
  445. # planned per-row lengths address.
  446. topk_slots, valid_counts = triton_convert_req_index_to_global_index(
  447. attn_metadata.req_id_per_token[:num_tokens],
  448. attn_metadata.block_table,
  449. topk_indices,
  450. BLOCK_SIZE=attn_metadata.block_size,
  451. NUM_TOPK_TOKENS=topk_indices.shape[1],
  452. return_valid_counts=True,
  453. )
  454. if _TRITON:
  455. from vllm.v1.attention.backends.mla.gb10_sparse_mla import sparse_mla
  456. kv = kv_c_and_k_pe_cache.view(torch.float8_e4m3fn) if self.use_fp8_kv_cache else kv_c_and_k_pe_cache
  457. out = sparse_mla(q_nope, kv.view(-1, self.head_size), topk_slots, valid_counts, self.scale,
  458. float(layer._k_scale_float or 1.0) if self.use_fp8_kv_cache else 1.0)
  459. return out, None
  460. state = attn_metadata.state
  461. assert state is not None
  462. # Pack valid prefixes at the offsets refreshed by plan() before replay.
  463. state.pack_indices(topk_slots)
  464. flat = (
  465. kv_c_and_k_pe_cache.view(torch.float8_e4m3fn)
  466. if self.use_fp8_kv_cache
  467. else kv_c_and_k_pe_cache
  468. ).reshape(-1, 1, self.head_size)
  469. ckv = flat[..., : self.kv_lora_rank]
  470. kpe = flat[..., self.kv_lora_rank :]
  471. scale_kwargs = (
  472. {"ckv_scale": float(layer._k_scale_float or 1.0), "kpe_scale": 1.0}
  473. if self.use_fp8_kv_cache
  474. else {}
  475. )
  476. out = state.wrapper.run(q_nope, q_pe, ckv, kpe, **scale_kwargs)
  477. return out, None