sparse_attn_indexer.py 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. """Custom Sparse Attention Indexer layers."""
  4. import torch
  5. import vllm.envs as envs
  6. from vllm import _custom_ops as ops
  7. from vllm._aiter_ops import rocm_aiter_ops
  8. from vllm.compilation.breakable_cudagraph import eager_break_during_capture
  9. from vllm.config import CUDAGraphMode, get_current_vllm_config
  10. from vllm.distributed import get_dcp_group, get_pcp_group
  11. from vllm.forward_context import get_forward_context
  12. from vllm.logger import init_logger
  13. from vllm.model_executor.custom_op import CustomOp
  14. from vllm.model_executor.kernels.attention.dsa.candidate_blocks import (
  15. apply_candidate_mask as _apply_candidate_mask,
  16. )
  17. from vllm.model_executor.kernels.attention.dsa.candidate_blocks import (
  18. _PrefillTimer as _DSV41PrefillTimer,
  19. )
  20. from vllm.model_executor.kernels.attention.dsa.candidate_blocks import (
  21. select_candidate_blocks as _select_candidate_blocks,
  22. )
  23. from vllm.model_executor.layers.quantization.utils.quant_utils import (
  24. get_fp8_min_max,
  25. )
  26. from vllm.platforms import current_platform
  27. from vllm.triton_utils import tl, triton
  28. from vllm.utils.deep_gemm import (
  29. fp8_fp4_mqa_logits,
  30. fp8_fp4_paged_mqa_logits,
  31. has_deep_gemm,
  32. )
  33. from vllm.utils.import_utils import has_cutedsl
  34. from vllm.utils.torch_utils import (
  35. LayerNameType,
  36. _encode_layer_name,
  37. _resolve_layer_name,
  38. direct_register_custom_op,
  39. )
  40. from vllm.v1.attention.backends.mla.indexer import (
  41. DeepseekV32IndexerMetadata,
  42. )
  43. from vllm.v1.attention.ops.common import pack_seq_triton, unpack_seq_triton
  44. from vllm.v1.attention.ops.pcp import maybe_gather_indexer_k
  45. from vllm.v1.worker.workspace import current_workspace_manager
  46. logger = init_logger(__name__)
  47. RADIX_TOPK_WORKSPACE_SIZE = 1024 * 1024
  48. # MXFP4 layout: 2 values packed per byte, ue8m0 (1-byte) scale per block of 32.
  49. MXFP4_BLOCK_SIZE = 32
  50. def _assert_cutedsl_dcp_merge_supported(
  51. logits: torch.Tensor,
  52. topk_indices: torch.Tensor,
  53. k: int,
  54. ) -> None:
  55. # The DCP merge only supports the CuteDSL path (Triton pack kernel + CuteDSL
  56. # stable-topk selector); there is no PyTorch fallback. The first cut targets
  57. # Blackwell/Hopper with index_topk in (512, 1024, 2048) (the selector's radix
  58. # sizing); the Triton pack itself has no shape/topk constraints.
  59. if not has_cutedsl():
  60. raise RuntimeError(
  61. "DCP sparse-indexer merge requires CuteDSL; install it or disable DCP."
  62. )
  63. if logits.device.type != "cuda":
  64. raise RuntimeError("DCP sparse-indexer merge requires CUDA tensors.")
  65. if logits.dtype != torch.float32 or topk_indices.dtype != torch.int32:
  66. raise RuntimeError(
  67. "DCP sparse-indexer merge requires fp32 logits and int32 indices."
  68. )
  69. if k not in (512, 1024, 2048):
  70. raise RuntimeError(
  71. f"DCP sparse-indexer merge requires index_topk in (512, 1024, 2048); "
  72. f"got {k}."
  73. )
  74. def _merge_dcp_topk_global(
  75. logits: torch.Tensor,
  76. topk_indices: torch.Tensor,
  77. topk_tokens: int,
  78. dcp_rank: int,
  79. dcp_world_size: int,
  80. cp_interleave: int,
  81. row_starts: torch.Tensor | None = None,
  82. ) -> None:
  83. """Merge each DCP rank's local top-K into the global top-K.
  84. ``topk_indices`` are this rank's local top-K positions into its 1/N KV
  85. shard. A token in the global top-K must also be in its owning rank's local
  86. top-K (at most ``topk_tokens - 1`` tokens rank globally above it, hence at
  87. most that many on its own rank), so exchanging only the per-rank local
  88. candidates is exact -- equivalent to all-gathering the full logit matrix,
  89. but it ships ``dcp_world_size * topk_tokens`` candidates instead of the whole
  90. score row. Overwrites ``topk_indices`` with global token ids (``-1`` for
  91. padding); the attention backend localizes them back to physical slots per
  92. rank.
  93. """
  94. if dcp_world_size <= 1:
  95. return
  96. # CuteDSL-only path (no PyTorch fallback): Triton-pack each rank's
  97. # (score, global_id) candidates on-device, all-gather, then the CuteDSL
  98. # stable-topk selector.
  99. _assert_cutedsl_dcp_merge_supported(logits, topk_indices, topk_tokens)
  100. from vllm.model_executor.kernels.attention.dsa.dcp_indexer_cutedsl import (
  101. pack_dcp_topk_candidates_cutedsl,
  102. stable_topk_from_gathered_candidates_cutedsl,
  103. )
  104. packed = torch.empty(
  105. (*topk_indices.shape, 2),
  106. dtype=torch.float32,
  107. device=topk_indices.device,
  108. )
  109. pack_dcp_topk_candidates_cutedsl(
  110. logits,
  111. topk_indices,
  112. packed,
  113. dcp_rank,
  114. dcp_world_size,
  115. cp_interleave,
  116. row_starts,
  117. )
  118. gathered = get_dcp_group().all_gather(packed, dim=1)
  119. stable_topk_from_gathered_candidates_cutedsl(
  120. gathered, topk_tokens, out=topk_indices
  121. )
  122. @triton.jit
  123. def _fused_indexer_q_rope_quant_kernel(
  124. positions,
  125. q,
  126. q_s0,
  127. q_s1,
  128. cos_sin_cache,
  129. cos_sin_s0,
  130. q_fp8,
  131. q_fp8_s0,
  132. q_fp8_s1,
  133. weights,
  134. weights_s0,
  135. weights_s1,
  136. weights_out,
  137. weights_out_s0,
  138. weights_out_s1,
  139. softmax_scale,
  140. head_scale,
  141. fp8_min: tl.constexpr,
  142. fp8_max: tl.constexpr,
  143. is_neox: tl.constexpr,
  144. ):
  145. token = tl.program_id(0)
  146. head = tl.program_id(1)
  147. offs32 = tl.arange(0, 32)
  148. offs64 = tl.arange(0, 64)
  149. pos = tl.load(positions + token)
  150. cos = tl.load(cos_sin_cache + pos * cos_sin_s0 + offs32).to(tl.float32)
  151. sin = tl.load(cos_sin_cache + pos * cos_sin_s0 + 32 + offs32).to(tl.float32)
  152. q_base = q + token * q_s0 + head * q_s1
  153. out_base = q_fp8 + token * q_fp8_s0 + head * q_fp8_s1
  154. if is_neox:
  155. # NeoX layout, x0 = q[0:32], x1 = q[32:64]
  156. x0 = tl.load(q_base + offs32).to(tl.float32)
  157. x1 = tl.load(q_base + 32 + offs32).to(tl.float32)
  158. else:
  159. # interleaved layout
  160. # x0 = q[0, 2, 4, ...], x1 = q[1, 3, 5, ...]
  161. x0 = tl.load(q_base + offs32 * 2).to(tl.float32)
  162. x1 = tl.load(q_base + offs32 * 2 + 1).to(tl.float32)
  163. r0 = (x0 * cos - x1 * sin).to(tl.bfloat16).to(tl.float32)
  164. r1 = (x1 * cos + x0 * sin).to(tl.bfloat16).to(tl.float32)
  165. amax = tl.maximum(tl.max(tl.abs(r0)), tl.max(tl.abs(r1)))
  166. q_nope = tl.load(q_base + 64 + offs64).to(tl.float32)
  167. amax = tl.maximum(amax, tl.max(tl.abs(q_nope)))
  168. scale_raw = tl.maximum(amax, 1e-10) * (1.0 / fp8_max)
  169. # e8m0 format
  170. q_scale = tl.math.exp2(tl.ceil(tl.log2(scale_raw)))
  171. if is_neox:
  172. tl.store(
  173. out_base + offs32,
  174. tl.clamp(r0 / q_scale, fp8_min, fp8_max).to(q_fp8.dtype.element_ty),
  175. )
  176. tl.store(
  177. out_base + 32 + offs32,
  178. tl.clamp(r1 / q_scale, fp8_min, fp8_max).to(q_fp8.dtype.element_ty),
  179. )
  180. else:
  181. tl.store(
  182. out_base + offs32 * 2,
  183. tl.clamp(r0 / q_scale, fp8_min, fp8_max).to(q_fp8.dtype.element_ty),
  184. )
  185. tl.store(
  186. out_base + offs32 * 2 + 1,
  187. tl.clamp(r1 / q_scale, fp8_min, fp8_max).to(q_fp8.dtype.element_ty),
  188. )
  189. tl.store(
  190. out_base + 64 + offs64,
  191. tl.clamp(q_nope / q_scale, fp8_min, fp8_max).to(q_fp8.dtype.element_ty),
  192. )
  193. weight = tl.load(weights + token * weights_s0 + head * weights_s1).to(tl.float32)
  194. tl.store(
  195. weights_out + token * weights_out_s0 + head * weights_out_s1,
  196. weight * q_scale * softmax_scale * head_scale,
  197. )
  198. def fused_indexer_q_rope_quant(
  199. positions: torch.Tensor,
  200. q: torch.Tensor,
  201. cos_sin_cache: torch.Tensor,
  202. weights: torch.Tensor,
  203. softmax_scale: float,
  204. head_scale: float,
  205. is_neox: bool,
  206. ) -> tuple[torch.Tensor, torch.Tensor]:
  207. assert current_platform.is_cuda()
  208. assert q.dtype == torch.bfloat16
  209. assert q.shape[-1] == 128
  210. assert cos_sin_cache.shape[-1] == 64
  211. assert weights.shape == q.shape[:2]
  212. q_fp8 = torch.empty_like(q, dtype=current_platform.fp8_dtype())
  213. weights_out = torch.empty_like(weights, dtype=torch.float32)
  214. fp8_min, fp8_max = get_fp8_min_max()
  215. _fused_indexer_q_rope_quant_kernel[(q.shape[0], q.shape[1])](
  216. positions,
  217. q,
  218. q.stride(0),
  219. q.stride(1),
  220. cos_sin_cache,
  221. cos_sin_cache.stride(0),
  222. q_fp8,
  223. q_fp8.stride(0),
  224. q_fp8.stride(1),
  225. weights,
  226. weights.stride(0),
  227. weights.stride(1),
  228. weights_out,
  229. weights_out.stride(0),
  230. weights_out.stride(1),
  231. softmax_scale,
  232. head_scale,
  233. fp8_min=fp8_min,
  234. fp8_max=fp8_max,
  235. is_neox=is_neox,
  236. num_warps=1,
  237. )
  238. return q_fp8, weights_out
  239. def _gather_workspace_shapes(
  240. total_seq_lens: int,
  241. head_dim: int,
  242. fp8_dtype: torch.dtype,
  243. use_fp4_cache: bool,
  244. ) -> tuple[tuple[tuple[int, int], torch.dtype], tuple[tuple[int, int], torch.dtype]]:
  245. """Return ((values_shape, values_dtype), (scales_shape, scales_dtype)) for
  246. the K-gather workspace. FP8 path: (T, head_dim) fp8 + (T, 4) uint8 fp32
  247. scales. MXFP4 path: (T, head_dim // 2) uint8 packed mxfp4 +
  248. (T, head_dim // MXFP4_BLOCK_SIZE) uint8 ue8m0 scales."""
  249. if use_fp4_cache:
  250. return (
  251. ((total_seq_lens, head_dim // 2), torch.uint8),
  252. ((total_seq_lens, head_dim // MXFP4_BLOCK_SIZE), torch.uint8),
  253. )
  254. return (
  255. ((total_seq_lens, head_dim), fp8_dtype),
  256. ((total_seq_lens, 4), torch.uint8),
  257. )
  258. def kv_cache_as_quant_view(
  259. kv_cache: torch.Tensor,
  260. head_dim: int,
  261. use_fp4_cache: bool,
  262. ) -> torch.Tensor:
  263. """4D ``[num_blocks, block_size, 1, head_width]`` view expected by
  264. DeepGEMM, from the 3D indexer kv-cache allocation."""
  265. if use_fp4_cache:
  266. assert kv_cache.ndim == 3 and kv_cache.dtype == torch.uint8
  267. num_blocks, block_size, _ = kv_cache.shape
  268. page_bytes = int(kv_cache.stride(0))
  269. fp4_bytes = head_dim // 2 + head_dim // MXFP4_BLOCK_SIZE
  270. return torch.as_strided(
  271. kv_cache,
  272. size=(num_blocks, block_size, 1, fp4_bytes),
  273. stride=(page_bytes, fp4_bytes, fp4_bytes, 1),
  274. )
  275. return kv_cache.unsqueeze(-2)
  276. @eager_break_during_capture
  277. def sparse_attn_indexer(
  278. hidden_states: torch.Tensor,
  279. k_cache_prefix: LayerNameType,
  280. kv_cache: torch.Tensor,
  281. q_quant: torch.Tensor,
  282. q_scale: torch.Tensor | None,
  283. k: torch.Tensor | None,
  284. weights: torch.Tensor,
  285. quant_block_size: int,
  286. scale_fmt: str | None,
  287. topk_tokens: int,
  288. head_dim: int,
  289. max_model_len: int,
  290. total_seq_lens: int,
  291. topk_indices_buffer: torch.Tensor,
  292. skip_k_cache_insert: bool,
  293. use_pcp: bool,
  294. dense_mha_metadata_layer_name: LayerNameType,
  295. use_fp4_cache: bool = False,
  296. dcp_rank: int = 0,
  297. dcp_world_size: int = 1,
  298. cp_kv_cache_interleave_size: int = 1,
  299. skip_topk_buffer_clear: bool = False,
  300. candidate_blocks: torch.Tensor | None = None,
  301. candidate_block_size: int = 0,
  302. candidate_write: bool = False,
  303. compress_ratio: int = 1,
  304. ) -> torch.Tensor:
  305. # careful! this will be None in dummy run
  306. forward_context = get_forward_context()
  307. attn_metadata = forward_context.attn_metadata
  308. fp8_dtype = current_platform.fp8_dtype()
  309. k_cache_prefix = _resolve_layer_name(k_cache_prefix)
  310. # v4.1 two-level candidate filtering under DCP: the logits columns are this
  311. # rank's local compressed states; with interleave 1 and world | block, local
  312. # block b (size block // world) is exactly this rank's shard of global block
  313. # b, so candidate ids need no translation. The producer MAX-reduces block
  314. # scores across ranks and pins the global newest block (candidate_blocks.py).
  315. local_cand_bs = candidate_block_size
  316. dcp_group = None
  317. if candidate_blocks is not None:
  318. assert candidate_block_size > 0
  319. if dcp_world_size > 1:
  320. assert cp_kv_cache_interleave_size == 1, (
  321. "v4.1 candidate filtering under DCP needs cp_kv_cache_interleave_size=1"
  322. )
  323. assert candidate_block_size % dcp_world_size == 0, (
  324. f"candidate_block_size ({candidate_block_size}) must be a multiple "
  325. f"of dcp_world_size ({dcp_world_size})"
  326. )
  327. local_cand_bs = candidate_block_size // dcp_world_size
  328. dcp_group = get_dcp_group()
  329. # assert isinstance(attn_metadata, dict)
  330. if not isinstance(attn_metadata, dict):
  331. # Reserve workspace for indexer during profiling run
  332. values_spec, scales_spec = _gather_workspace_shapes(
  333. total_seq_lens, head_dim, fp8_dtype, use_fp4_cache
  334. )
  335. current_workspace_manager().get_simultaneous(
  336. values_spec,
  337. scales_spec,
  338. ((RADIX_TOPK_WORKSPACE_SIZE,), torch.uint8),
  339. )
  340. # Dummy allocation to simulate for peak logits tensor memory during inference.
  341. # FP8 elements so elements == bytes
  342. max_logits_elems = envs.VLLM_SPARSE_INDEXER_MAX_LOGITS_MB * 1024 * 1024
  343. _ = torch.empty(
  344. max_logits_elems, dtype=torch.uint8, device=hidden_states.device
  345. )
  346. return sparse_attn_indexer_fake(
  347. hidden_states,
  348. k_cache_prefix,
  349. kv_cache,
  350. q_quant,
  351. q_scale,
  352. k,
  353. weights,
  354. quant_block_size,
  355. scale_fmt,
  356. topk_tokens,
  357. head_dim,
  358. max_model_len,
  359. total_seq_lens,
  360. topk_indices_buffer,
  361. skip_k_cache_insert,
  362. use_pcp,
  363. dense_mha_metadata_layer_name,
  364. use_fp4_cache,
  365. candidate_blocks=candidate_blocks,
  366. candidate_block_size=candidate_block_size,
  367. candidate_write=candidate_write,
  368. )
  369. attn_metadata_narrowed = attn_metadata[k_cache_prefix]
  370. assert isinstance(attn_metadata_narrowed, DeepseekV32IndexerMetadata)
  371. slot_mapping = attn_metadata_narrowed.slot_mapping
  372. has_decode = attn_metadata_narrowed.num_decodes > 0
  373. has_prefill = attn_metadata_narrowed.num_prefills > 0
  374. num_decode_tokens = attn_metadata_narrowed.num_decode_tokens
  375. # q_scale is required iff the FP4 cache path is enabled; the FP8 path
  376. # folds the Q scale into `weights` inside fused_indexer_q_rope_quant.
  377. if use_fp4_cache:
  378. assert q_scale is not None, "use_fp4_cache=True requires q_scale"
  379. else:
  380. assert q_scale is None, "q_scale must be None when use_fp4_cache=False"
  381. # During speculative decoding, k may be padded to the CUDA graph batch
  382. # size while slot_mapping only covers actual tokens. Truncate k to avoid
  383. # out-of-bounds reads in the kernel.
  384. # Keep PCP padding so every rank contributes the same all-gather shape.
  385. num_tokens = slot_mapping.shape[0]
  386. if use_pcp:
  387. num_tokens //= get_pcp_group().world_size
  388. if k is not None:
  389. k = k[:num_tokens]
  390. if not skip_k_cache_insert:
  391. assert k is not None
  392. k, slot_mapping_for_cache = maybe_gather_indexer_k(
  393. k,
  394. slot_mapping,
  395. num_decode_tokens,
  396. use_pcp,
  397. )
  398. # scale_fmt can be None, but the function expects str
  399. assert scale_fmt is not None
  400. assert not use_fp4_cache, "Unfused FP4 Insert is not supported yet"
  401. ops.indexer_k_quant_and_cache(
  402. k,
  403. kv_cache,
  404. slot_mapping_for_cache,
  405. quant_block_size,
  406. scale_fmt,
  407. )
  408. # The indexer and main MLA may classify the same short extend differently
  409. # because they use independent decode thresholds. Only the main MLA route
  410. # can determine whether the top-k indices will be consumed.
  411. if forward_context.cudagraph_runtime_mode != CUDAGraphMode.FULL:
  412. dense_mha_layer = _resolve_layer_name(dense_mha_metadata_layer_name)
  413. if dense_mha_layer:
  414. mla_metadata = attn_metadata.get(dense_mha_layer)
  415. prefill_metadata = getattr(mla_metadata, "prefill", None)
  416. if (
  417. getattr(prefill_metadata, "use_dense_mha", False)
  418. and getattr(mla_metadata, "num_decode_tokens", -1) == 0
  419. and not torch.cuda.is_current_stream_capturing()
  420. ):
  421. # Deliberately leave the buffer untouched. Dense MHA does not
  422. # consume top-k indices for this batch; clearing it would be
  423. # unnecessary work.
  424. return topk_indices_buffer
  425. # The buffer must be pre-filled with -1 (the "no token" sentinel) before the
  426. # top-k kernels scatter valid indices into it. On the fused deepseek_v32
  427. # nvidia path, _fused_norm_rope_kernel already cleared the same
  428. # [:num_tokens, :topk] region earlier in this forward, so skip the redundant
  429. # fill.
  430. if not skip_topk_buffer_clear:
  431. topk_indices_buffer[: hidden_states.shape[0]] = -1
  432. if has_prefill:
  433. prefill_metadata = attn_metadata_narrowed.prefill
  434. assert prefill_metadata is not None
  435. # Get the full shared workspace buffers once (will allocate on first use).
  436. # Layout switches between FP8 (head_dim bytes + 4-byte fp32 scale) and
  437. # MXFP4 (head_dim/2 bytes packed + head_dim/MXFP4_BLOCK_SIZE ue8m0
  438. # scales) based on use_fp4_cache.
  439. workspace_manager = current_workspace_manager()
  440. values_spec, scales_spec = _gather_workspace_shapes(
  441. total_seq_lens, head_dim, fp8_dtype, use_fp4_cache
  442. )
  443. k_quant_full, k_scale_full = workspace_manager.get_simultaneous(
  444. values_spec,
  445. scales_spec,
  446. )
  447. for chunk in prefill_metadata.chunks:
  448. cu_seqlen_ks = chunk.cu_seqlen_ks
  449. cu_seqlen_ke = chunk.cu_seqlen_ke
  450. assert chunk.local_cu_seq_lens is not None
  451. k_quant = k_quant_full[: chunk.max_local_total_seq_lens]
  452. k_scale = k_scale_full[: chunk.max_local_total_seq_lens]
  453. if not chunk.skip_kv_gather and chunk.local_total_seq_lens > 0:
  454. ops.cp_gather_indexer_k_quant_cache(
  455. kv_cache,
  456. k_quant,
  457. k_scale,
  458. chunk.block_table,
  459. chunk.local_cu_seq_lens,
  460. )
  461. q_slice = q_quant[chunk.token_start : chunk.token_end]
  462. q_scale_slice = (
  463. q_scale[chunk.token_start : chunk.token_end]
  464. if q_scale is not None
  465. else None
  466. )
  467. topk_indices = topk_indices_buffer[
  468. chunk.token_start : chunk.token_end, :topk_tokens
  469. ]
  470. if chunk.local_total_seq_lens == 0:
  471. logits = q_slice.new_empty((q_slice.shape[0], 0), dtype=torch.float32)
  472. topk_indices.fill_(-1)
  473. if candidate_blocks is not None and candidate_write and dcp_group is not None:
  474. # Other ranks may hold states for this chunk: join the MAX
  475. # reduce with an empty local contribution.
  476. _select_candidate_blocks(
  477. logits,
  478. cu_seqlen_ks,
  479. cu_seqlen_ke,
  480. candidate_blocks.shape[1],
  481. local_cand_bs,
  482. candidate_blocks[chunk.token_start : chunk.token_end],
  483. dcp_group=dcp_group,
  484. global_row_ke=chunk.global_ctx_lens,
  485. global_block_size=candidate_block_size,
  486. max_global_blocks=triton.cdiv(
  487. chunk.max_global_ctx, candidate_block_size
  488. ),
  489. )
  490. else:
  491. # DeepGEMM scalar-type tags (zero-copy): MXFP4 values → int8
  492. # (kPackedFP4), scales → int32 squeezed to 1-D kv_sf / 2-D q_sf.
  493. if use_fp4_cache:
  494. q_slice_cast = q_slice.view(torch.int8)
  495. k_quant_cast = k_quant.view(torch.int8)
  496. k_scale_cast = k_scale.view(torch.int32).squeeze(-1)
  497. else:
  498. q_slice_cast = q_slice
  499. k_quant_cast = k_quant
  500. k_scale_cast = k_scale.view(torch.float32).squeeze(-1)
  501. if current_platform.is_xpu():
  502. if q_scale_slice is not None:
  503. raise RuntimeError("XPU fp8_mqa_logits does not support FP4 Q")
  504. logits = torch.ops.vllm.xpu_fp8_mqa_logits(
  505. q_slice_cast,
  506. k_quant_cast,
  507. k_scale_cast,
  508. weights[chunk.token_start : chunk.token_end],
  509. cu_seqlen_ks,
  510. cu_seqlen_ke,
  511. )
  512. else:
  513. logits = fp8_fp4_mqa_logits(
  514. (q_slice_cast, q_scale_slice),
  515. (k_quant_cast, k_scale_cast),
  516. weights[chunk.token_start : chunk.token_end],
  517. cu_seqlen_ks,
  518. cu_seqlen_ke,
  519. clean_logits=False,
  520. )
  521. num_rows = logits.shape[0]
  522. if candidate_blocks is not None:
  523. # Two-level selection (v4.1): the candidate source
  524. # publishes its top blocks; later indexers mask their
  525. # scores to them. Both before the row top-k.
  526. chunk_candidates = candidate_blocks[
  527. chunk.token_start : chunk.token_end
  528. ]
  529. if candidate_write:
  530. _select_candidate_blocks(
  531. logits,
  532. cu_seqlen_ks,
  533. cu_seqlen_ke,
  534. chunk_candidates.shape[1],
  535. local_cand_bs,
  536. chunk_candidates,
  537. dcp_group=dcp_group,
  538. global_row_ke=chunk.global_ctx_lens,
  539. global_block_size=candidate_block_size,
  540. max_global_blocks=(
  541. triton.cdiv(chunk.max_global_ctx, candidate_block_size)
  542. if dcp_group is not None
  543. else None
  544. ),
  545. )
  546. else:
  547. _apply_candidate_mask(
  548. logits,
  549. cu_seqlen_ks,
  550. cu_seqlen_ke,
  551. chunk_candidates,
  552. local_cand_bs,
  553. )
  554. ops.top_k_per_row_prefill(
  555. logits,
  556. cu_seqlen_ks,
  557. cu_seqlen_ke,
  558. topk_indices,
  559. num_rows,
  560. logits.stride(0),
  561. logits.stride(1),
  562. topk_tokens,
  563. )
  564. _merge_dcp_topk_global(
  565. logits,
  566. topk_indices,
  567. topk_tokens,
  568. dcp_rank,
  569. dcp_world_size,
  570. cp_kv_cache_interleave_size,
  571. row_starts=chunk.cu_seqlen_ks,
  572. )
  573. if has_decode:
  574. decode_metadata = attn_metadata_narrowed.decode
  575. assert decode_metadata is not None
  576. kv_cache = kv_cache_as_quant_view(kv_cache, head_dim, use_fp4_cache)
  577. decode_lens = decode_metadata.decode_lens
  578. if num_decode_tokens == 0:
  579. padded_q_quant_decode_tokens = q_quant[:1].reshape(1, 1, *q_quant.shape[1:])
  580. padded_q_scale = (
  581. q_scale[:1].reshape(1, 1, *q_scale.shape[1:])
  582. if q_scale is not None
  583. else None
  584. )
  585. elif decode_metadata.requires_padding:
  586. # pad in edge case where we have short chunked prefill length <
  587. # decode_threshold since we unstrictly split
  588. # prefill and decode by decode_threshold
  589. # (currently set to 1 + speculative tokens).
  590. # FP8 Q is float8_e4m3fn (pack_seq_triton's fp32 pad path is OK —
  591. # downstream context_lens masks stale slots). MXFP4 Q is two
  592. # uint8 tensors (values + ue8m0 scales) — use the dedicated uint8
  593. # packer with pad_byte=0 so padded slots dequantize to 0 and
  594. # can't produce NaN/Inf in the logits kernel.
  595. if q_scale is not None:
  596. padded_q_quant_decode_tokens = pack_seq_triton(
  597. q_quant[:num_decode_tokens], decode_lens, pad_value=0
  598. )
  599. padded_q_scale = pack_seq_triton(
  600. q_scale[:num_decode_tokens], decode_lens, pad_value=0
  601. )
  602. else:
  603. padded_q_quant_decode_tokens = pack_seq_triton(
  604. q_quant[:num_decode_tokens], decode_lens
  605. )
  606. padded_q_scale = None
  607. else:
  608. padded_q_quant_decode_tokens = q_quant[:num_decode_tokens].reshape(
  609. decode_lens.shape[0], -1, *q_quant.shape[1:]
  610. )
  611. if q_scale is not None:
  612. padded_q_scale = q_scale[:num_decode_tokens].reshape(
  613. decode_lens.shape[0], -1, *q_scale.shape[1:]
  614. )
  615. else:
  616. padded_q_scale = None
  617. # TODO: move and optimize below logic with triton kernels
  618. batch_size = padded_q_quant_decode_tokens.shape[0]
  619. next_n = padded_q_quant_decode_tokens.shape[1]
  620. num_padded_tokens = batch_size * next_n
  621. seq_lens = decode_metadata.seq_lens[:batch_size]
  622. # seq_lens is always 2D: (B, next_n) for native spec decode, (B, 1)
  623. # otherwise. deep_gemm fp8_fp4_paged_mqa_logits requires 2D context_lens;
  624. # the downstream topk kernels accept both 1D and 2D.
  625. padded_q_quant_cast = (
  626. padded_q_quant_decode_tokens.view(torch.int8)
  627. if use_fp4_cache
  628. else padded_q_quant_decode_tokens
  629. )
  630. if current_platform.is_xpu():
  631. if padded_q_scale is not None:
  632. raise RuntimeError("XPU fp8_paged_mqa_logits does not support FP4 Q")
  633. seq_lens_xpu = (
  634. seq_lens[:, -1].contiguous() if seq_lens.ndim == 2 else seq_lens
  635. )
  636. logits = torch.ops.vllm.xpu_fp8_paged_mqa_logits(
  637. padded_q_quant_cast,
  638. kv_cache,
  639. weights[:num_padded_tokens],
  640. seq_lens_xpu,
  641. decode_metadata.block_table,
  642. decode_metadata.schedule_metadata,
  643. max_model_len,
  644. )
  645. else:
  646. logits = fp8_fp4_paged_mqa_logits(
  647. (padded_q_quant_cast, padded_q_scale),
  648. kv_cache,
  649. weights[:num_padded_tokens],
  650. seq_lens,
  651. decode_metadata.block_table,
  652. decode_metadata.schedule_metadata,
  653. max_model_len=max_model_len,
  654. clean_logits=False,
  655. indices=decode_metadata.indices,
  656. )
  657. num_rows = logits.shape[0]
  658. if candidate_blocks is not None:
  659. # Two-level selection (v4.1) on the decode logits; columns are
  660. # request-local compressed positions. seq_lens is (B, next_n)
  661. # for native spec decode (per-row effective lens) and (B, 1)
  662. # otherwise.
  663. vis = seq_lens.reshape(-1)
  664. row_repeat = next_n if vis.numel() != num_rows else 1
  665. vis = vis[:num_rows]
  666. decode_candidates = candidate_blocks[:num_rows]
  667. if candidate_write:
  668. global_row_ke = None
  669. max_global_blocks = None
  670. if dcp_group is not None:
  671. # Global (un-sharded) per-row context in STATES for the
  672. # newest-block pin: per token when flattened / next_n == 1,
  673. # per request (row_repeat) or expanded per row otherwise.
  674. gl = decode_metadata.global_seq_lens
  675. assert gl is not None
  676. gl = gl.reshape(-1)
  677. if row_repeat > 1:
  678. gl = gl[:batch_size]
  679. elif gl.numel() != num_rows:
  680. # native spec decode: request lens -> per-row lens
  681. gl = (
  682. gl[:batch_size].unsqueeze(1)
  683. - next_n
  684. + 1
  685. + torch.arange(next_n, device=gl.device, dtype=gl.dtype)
  686. ).clamp_(min=0).reshape(-1)
  687. else:
  688. gl = gl[:num_rows]
  689. global_row_ke = gl // compress_ratio
  690. max_global_blocks = triton.cdiv(
  691. attn_metadata_narrowed.max_seq_len // compress_ratio,
  692. candidate_block_size,
  693. )
  694. _select_candidate_blocks(
  695. logits,
  696. None,
  697. vis,
  698. decode_candidates.shape[1],
  699. local_cand_bs,
  700. decode_candidates,
  701. row_repeat,
  702. dcp_group=dcp_group,
  703. global_row_ke=global_row_ke,
  704. global_block_size=candidate_block_size,
  705. max_global_blocks=max_global_blocks,
  706. )
  707. else:
  708. _apply_candidate_mask(
  709. logits,
  710. None,
  711. vis,
  712. decode_candidates,
  713. local_cand_bs,
  714. row_repeat,
  715. )
  716. topk_indices = topk_indices_buffer[:num_padded_tokens, :topk_tokens]
  717. use_cooperative_topk = (
  718. current_platform.is_cuda()
  719. and topk_tokens in (512, 1024, 2048)
  720. and num_rows <= 64
  721. and logits.stride(0) % 4 == 0 # TMA 16-byte alignment
  722. and current_platform.has_device_capability(90)
  723. and not current_platform.is_device_capability_family(120)
  724. )
  725. # Tech2Wild 2026-09-10 (SM12x): GB10 has 48 SMs and 99 KB smem per block. On long
  726. # rows persistent_topk oversubscribes and its FilteredTopK fallback needs >=128 KB
  727. # smem, so the launch fails (32K context: 90 CTAs > 48). Use top_k_per_row_decode.
  728. use_persistent_topk = (
  729. current_platform.is_cuda()
  730. and topk_tokens in (512, 1024, 2048)
  731. and not current_platform.is_device_capability_family(120)
  732. )
  733. if use_cooperative_topk:
  734. workspace_manager = current_workspace_manager()
  735. (topk_workspace,) = workspace_manager.get_simultaneous(
  736. ((RADIX_TOPK_WORKSPACE_SIZE,), torch.uint8),
  737. )
  738. torch.ops._C.cooperative_topk(
  739. logits,
  740. seq_lens,
  741. topk_indices,
  742. topk_workspace,
  743. topk_tokens,
  744. attn_metadata_narrowed.max_seq_len,
  745. )
  746. elif use_persistent_topk:
  747. workspace_manager = current_workspace_manager()
  748. (topk_workspace,) = workspace_manager.get_simultaneous(
  749. ((RADIX_TOPK_WORKSPACE_SIZE,), torch.uint8),
  750. )
  751. torch.ops._C.persistent_topk(
  752. logits,
  753. seq_lens,
  754. topk_indices,
  755. topk_workspace,
  756. topk_tokens,
  757. logits.shape[1],
  758. )
  759. else:
  760. ops.top_k_per_row_decode(
  761. logits,
  762. next_n,
  763. seq_lens,
  764. topk_indices,
  765. num_rows,
  766. logits.stride(0),
  767. logits.stride(1),
  768. topk_tokens,
  769. )
  770. if decode_metadata.global_seq_lens is not None:
  771. _merge_dcp_topk_global(
  772. logits,
  773. topk_indices,
  774. topk_tokens,
  775. dcp_rank,
  776. dcp_world_size,
  777. cp_kv_cache_interleave_size,
  778. )
  779. if decode_metadata.requires_padding:
  780. # if padded, we need to unpack
  781. # the topk indices removing padded tokens
  782. topk_indices = unpack_seq_triton(
  783. topk_indices.reshape(batch_size, -1, topk_indices.shape[-1]),
  784. decode_lens,
  785. )
  786. topk_indices_buffer[: topk_indices.shape[0], : topk_indices.shape[-1]] = (
  787. topk_indices
  788. )
  789. return topk_indices_buffer
  790. def sparse_attn_indexer_fake(
  791. hidden_states: torch.Tensor,
  792. k_cache_prefix: LayerNameType,
  793. kv_cache: torch.Tensor,
  794. q_quant: torch.Tensor,
  795. q_scale: torch.Tensor | None,
  796. k: torch.Tensor | None,
  797. weights: torch.Tensor,
  798. quant_block_size: int,
  799. scale_fmt: str | None,
  800. topk_tokens: int,
  801. head_dim: int,
  802. max_model_len: int,
  803. total_seq_lens: int,
  804. topk_indices_buffer: torch.Tensor | None,
  805. skip_k_cache_insert: bool,
  806. use_pcp: bool,
  807. dense_mha_metadata_layer_name: LayerNameType,
  808. use_fp4_cache: bool = False,
  809. dcp_rank: int = 0,
  810. dcp_world_size: int = 1,
  811. cp_kv_cache_interleave_size: int = 1,
  812. skip_topk_buffer_clear: bool = False,
  813. candidate_blocks: torch.Tensor | None = None,
  814. candidate_block_size: int = 0,
  815. candidate_write: bool = False,
  816. compress_ratio: int = 1,
  817. ) -> torch.Tensor:
  818. return topk_indices_buffer
  819. direct_register_custom_op(
  820. op_name="sparse_attn_indexer",
  821. op_func=sparse_attn_indexer,
  822. mutates_args=["topk_indices_buffer", "candidate_blocks"],
  823. fake_impl=sparse_attn_indexer_fake,
  824. dispatch_key=current_platform.dispatch_key,
  825. )
  826. @CustomOp.register("sparse_attn_indexer")
  827. class SparseAttnIndexer(CustomOp):
  828. """Sparse Attention Indexer Custom Op Layer. This layer is extracted as a
  829. separate custom op since it involves heavy custom kernels like `mqa_logits`,
  830. `paged_mqa_logits` and `top_k_per_row`, etc. Those kernels maybe requires
  831. specific memory layout or implementation for different hardware backends to
  832. achieve optimal performance.
  833. For now, the default native path will use CUDA backend path. Other platform
  834. may requires add the corresponding Custom Op name `sparse_attn_indexer` to
  835. `custom_ops` in `CompilationConfig` to enable the platform specific path.
  836. """
  837. def __init__(
  838. self,
  839. k_cache,
  840. quant_block_size: int,
  841. scale_fmt: str,
  842. topk_tokens: int,
  843. head_dim: int,
  844. max_model_len: int,
  845. max_total_seq_len: int,
  846. topk_indices_buffer: torch.Tensor,
  847. skip_k_cache_insert: bool = False,
  848. use_fp4_cache: bool = False,
  849. compress_ratio: int = 1,
  850. candidate_blocks: torch.Tensor | None = None,
  851. candidate_block_size: int = 0,
  852. candidate_write: bool = False,
  853. ):
  854. super().__init__()
  855. self.k_cache = k_cache
  856. self.quant_block_size = quant_block_size
  857. self.scale_fmt = scale_fmt
  858. self.topk_tokens = topk_tokens
  859. self.head_dim = head_dim
  860. self.max_model_len = max_model_len
  861. self.max_total_seq_len = max_total_seq_len
  862. self.topk_indices_buffer = topk_indices_buffer
  863. self.skip_k_cache_insert = skip_k_cache_insert
  864. self.use_fp4_cache = use_fp4_cache
  865. self.compress_ratio = compress_ratio
  866. # v4.1 two-level selection: the candidate source indexer writes the
  867. # top candidate blocks here; later indexers mask their scores with it.
  868. self.candidate_blocks = candidate_blocks
  869. self.candidate_block_size = candidate_block_size
  870. self.candidate_write = candidate_write
  871. self.dense_mha_metadata_layer_name = ""
  872. # DCP scalars are constant for the run; resolve them here (config is set
  873. # during model construction) and pass them into the custom op, rather
  874. # than threading them through per-step metadata.
  875. parallel_config = get_current_vllm_config().parallel_config
  876. self._parallel_config = parallel_config
  877. self.dcp_world_size = parallel_config.decode_context_parallel_size
  878. self.dcp_rank = get_dcp_group().rank_in_group if self.dcp_world_size > 1 else 0
  879. self.use_pcp = parallel_config.prefill_context_parallel_size > 1
  880. self._cp_kv_cache_interleave_size: int | None = None
  881. if current_platform.is_cuda() and not has_deep_gemm():
  882. raise RuntimeError(
  883. "Sparse Attention Indexer CUDA op requires DeepGEMM support in "
  884. "the current vLLM environment."
  885. )
  886. @property
  887. def cp_kv_cache_interleave_size(self) -> int:
  888. """With PD+DCP, the real value isn't known until block_size is finalized,
  889. which happens after this layer is built. Safe to cache after the first access,
  890. as long as the adjustment always runs before any forward pass
  891. (it's set up in Worker.initialize_from_config, ahead of warmup/serving).
  892. """
  893. if self._cp_kv_cache_interleave_size is None:
  894. value = self._parallel_config.cp_kv_cache_interleave_size
  895. if isinstance(get_forward_context().attn_metadata, dict):
  896. self._cp_kv_cache_interleave_size = value
  897. return value
  898. return self._cp_kv_cache_interleave_size
  899. def forward_native(
  900. self,
  901. hidden_states: torch.Tensor,
  902. q_quant: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
  903. k: torch.Tensor | None,
  904. weights: torch.Tensor,
  905. ):
  906. if current_platform.is_cuda() or current_platform.is_xpu():
  907. return self.forward_cuda(hidden_states, q_quant, k, weights)
  908. elif current_platform.is_rocm():
  909. return self.forward_hip(hidden_states, q_quant, k, weights)
  910. else:
  911. raise NotImplementedError(
  912. "SparseAttnIndexer native forward is only implemented for "
  913. "CUDA, ROCm and XPU platforms."
  914. )
  915. def forward_cuda(
  916. self,
  917. hidden_states: torch.Tensor,
  918. q_quant: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
  919. k: torch.Tensor | None,
  920. weights: torch.Tensor,
  921. ):
  922. # Stage 7: phase "indexer" of the prefill timing (inert unless enabled).
  923. with _DSV41PrefillTimer("indexer", int(hidden_states.shape[0])):
  924. return self._forward_cuda_impl(hidden_states, q_quant, k, weights)
  925. def _forward_cuda_impl(
  926. self,
  927. hidden_states: torch.Tensor,
  928. q_quant: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
  929. k: torch.Tensor | None,
  930. weights: torch.Tensor,
  931. ):
  932. # FP8 path: single tensor (per-token scale is folded into `weights`).
  933. # FP4 path: (values, scales) tuple with scales required by the kernel.
  934. if isinstance(q_quant, tuple):
  935. q_values, q_scale = q_quant
  936. else:
  937. q_values, q_scale = q_quant, None
  938. return torch.ops.vllm.sparse_attn_indexer(
  939. hidden_states,
  940. _encode_layer_name(self.k_cache.prefix),
  941. self.k_cache.kv_cache,
  942. q_values,
  943. q_scale,
  944. k,
  945. weights,
  946. self.quant_block_size,
  947. self.scale_fmt,
  948. self.topk_tokens,
  949. self.head_dim,
  950. self.max_model_len,
  951. self.max_total_seq_len,
  952. self.topk_indices_buffer,
  953. self.skip_k_cache_insert,
  954. self.use_pcp,
  955. _encode_layer_name(self.dense_mha_metadata_layer_name),
  956. self.use_fp4_cache,
  957. self.dcp_rank,
  958. self.dcp_world_size,
  959. self.cp_kv_cache_interleave_size,
  960. candidate_blocks=self.candidate_blocks,
  961. candidate_block_size=self.candidate_block_size,
  962. candidate_write=self.candidate_write,
  963. compress_ratio=self.compress_ratio,
  964. )
  965. def forward_xpu(
  966. self,
  967. hidden_states: torch.Tensor,
  968. q_fp8: torch.Tensor,
  969. k: torch.Tensor | None,
  970. weights: torch.Tensor,
  971. ):
  972. return self.forward_cuda(hidden_states, q_fp8, k, weights)
  973. def forward_hip(
  974. self,
  975. hidden_states: torch.Tensor,
  976. q_quant: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
  977. k: torch.Tensor | None,
  978. weights: torch.Tensor,
  979. ):
  980. assert not self.use_fp4_cache, "AMD platform doesn't support fp4 cache yet"
  981. assert isinstance(q_quant, torch.Tensor), (
  982. "AMD sparse_attn_indexer expects a single FP8 q_quant tensor"
  983. )
  984. from vllm.platforms.rocm import on_gfx11, on_gfx950
  985. if (
  986. rocm_aiter_ops.is_enabled()
  987. or rocm_aiter_ops.is_rdna_aiter_enabled()
  988. or on_gfx11()
  989. # The so-called AITER sparse indexer op has a native gfx950 path:
  990. # its cache insert, MQA logits, and top-k fallbacks are implemented
  991. # by local Triton/C++ kernels and do not require the aiter package.
  992. or on_gfx950()
  993. ):
  994. return torch.ops.vllm.rocm_aiter_sparse_attn_indexer(
  995. hidden_states,
  996. _encode_layer_name(self.k_cache.prefix),
  997. self.k_cache.kv_cache,
  998. q_quant,
  999. k,
  1000. weights,
  1001. self.quant_block_size,
  1002. self.scale_fmt,
  1003. self.topk_tokens,
  1004. self.head_dim,
  1005. self.max_model_len,
  1006. self.max_total_seq_len,
  1007. self.topk_indices_buffer,
  1008. skip_k_cache_insert=self.skip_k_cache_insert,
  1009. compress_ratio=self.compress_ratio,
  1010. candidate_blocks=self.candidate_blocks,
  1011. candidate_block_size=self.candidate_block_size,
  1012. candidate_write=self.candidate_write,
  1013. )
  1014. raise RuntimeError(
  1015. "Sparse attention indexer ROCm path requires AITER or a supported "
  1016. "native architecture (gfx950/gfx11)."
  1017. )