flash_mla_sm120.canary.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. """SM120 FlashMLA sparse decode implementation.
  2. On SM120 (Blackwell Desktop / RTX PRO 6000) the flash_mla CUDA kernel
  3. is not available, so this module provides alternative implementations:
  4. - A fused Triton kernel (default, ``SGLANG_SM120_TRITON_FLASHMLA=1``)
  5. - A pure-PyTorch fallback (``SGLANG_SM120_TRITON_FLASHMLA=0``)
  6. The FP8 KV cache uses a page-internal layout where NOPE+ROPE data has
  7. stride (nope_dim + rope_dim*2) per token, and scales are stored in a
  8. separate region at the end of each page.
  9. """
  10. import logging
  11. import math
  12. from functools import lru_cache
  13. from typing import FrozenSet, Optional, Tuple
  14. import torch
  15. import triton
  16. import triton.language as tl
  17. from sglang.srt.environ import envs
  18. from sglang.srt.utils import is_hip
  19. logger = logging.getLogger(__name__)
  20. _is_hip = is_hip()
  21. _GLM_DSA_MODEL_ARCHS = (
  22. "GlmMoeDsaForCausalLM",
  23. "GlmMoeDsaForCausalLMNextN",
  24. )
  25. # Page layout constants for DSv4-Flash (MODEL1):
  26. # nope_dim = 448, rope_dim = 64, quantize_block_size = 64
  27. # nope_rope_stride = 448 + 64*2 = 576 bytes per token
  28. # scale_stride = ceil(448/64) + 1 = 8 bytes per token (7 scales + 1 pad)
  29. # bytes_per_token = 448 + 128 + 8 = 584
  30. # page_bytes = ceil_div(page_size * 584, 576) * 576
  31. _NOPE_DIM = 448
  32. _ROPE_DIM = 64
  33. _NOPE_ROPE_STRIDE = _NOPE_DIM + _ROPE_DIM * 2 # 576
  34. _TILE_SIZE = 64
  35. _NUM_TILES = _NOPE_DIM // _TILE_SIZE # 7
  36. _SCALE_STRIDE = _NUM_TILES + 1 # 8 (7 scales + 1 pad)
  37. _D = _NOPE_DIM + _ROPE_DIM # 512
  38. def _gather_and_dequant(k_cache, indices, page_size):
  39. """Gather KV entries from the paged buffer using correct page-internal addressing.
  40. Args:
  41. k_cache: (num_pages, page_size, 1, bytes_per_token) float8_e4m3fn
  42. Non-contiguous view of the raw page buffer.
  43. indices: (...) int32/int64, token-level indices. -1 = invalid.
  44. page_size: tokens per page (256)
  45. Returns:
  46. kv: (..., _D) bfloat16, dequantized KV vectors
  47. """
  48. idx_shape = indices.shape
  49. flat_idx = indices.reshape(-1) # (N,)
  50. N = flat_idx.shape[0]
  51. device = k_cache.device
  52. # Page-level addressing
  53. page_bytes = k_cache.stride(0) # actual byte stride between pages
  54. pages = flat_idx // page_size
  55. offsets = flat_idx % page_size
  56. # Clamp invalid indices
  57. safe_pages = pages.clamp(min=0)
  58. safe_offsets = offsets.clamp(min=0)
  59. # Access raw buffer as uint8 — use as_strided to get full page view
  60. num_pages = k_cache.shape[0]
  61. raw_pages = k_cache.as_strided(
  62. (num_pages, page_bytes),
  63. (page_bytes, 1),
  64. ).view(torch.uint8) # (num_pages, page_bytes) uint8
  65. # Note: float8_e4m3fn and uint8 are both 1 byte, view is safe
  66. # Compute byte offsets within each page
  67. # NOPE: page[safe_page, safe_offset * 576 + 0:448]
  68. # ROPE: page[safe_page, safe_offset * 576 + 448:576]
  69. # SCALES: page[safe_page, page_size * 576 + safe_offset * 8 + 0:7]
  70. nope_base = safe_offsets * _NOPE_ROPE_STRIDE # (N,)
  71. nope_offsets = nope_base.unsqueeze(-1) + torch.arange(
  72. _NOPE_DIM, device=device, dtype=torch.long
  73. ) # (N, 448)
  74. rope_base = nope_base + _NOPE_DIM # (N,)
  75. rope_offsets = rope_base.unsqueeze(-1) + torch.arange(
  76. _ROPE_DIM * 2, device=device, dtype=torch.long
  77. ) # (N, 128)
  78. scale_section_offset = page_size * _NOPE_ROPE_STRIDE # 147456
  79. scale_base = scale_section_offset + safe_offsets * _SCALE_STRIDE # (N,)
  80. scale_offsets = scale_base.unsqueeze(-1) + torch.arange(
  81. _NUM_TILES, device=device, dtype=torch.long
  82. ) # (N, 7)
  83. # Gather bytes per page — use advanced indexing
  84. # raw_pages[safe_pages, nope_offsets] → (N, 448)
  85. page_idx_nope = safe_pages.unsqueeze(-1).expand_as(nope_offsets)
  86. nope_bytes = raw_pages[page_idx_nope, nope_offsets] # (N, 448) uint8
  87. page_idx_rope = safe_pages.unsqueeze(-1).expand_as(rope_offsets)
  88. rope_bytes = raw_pages[page_idx_rope, rope_offsets] # (N, 128) uint8
  89. page_idx_scale = safe_pages.unsqueeze(-1).expand_as(scale_offsets)
  90. scale_bytes = raw_pages[page_idx_scale, scale_offsets] # (N, 7) uint8
  91. # Reinterpret dtypes
  92. nope_fp8 = nope_bytes.view(torch.float8_e4m3fn) # (N, 448)
  93. rope_bf16 = rope_bytes.contiguous().view(torch.bfloat16) # (N, 64)
  94. scale_e8m0 = scale_bytes.view(torch.float8_e8m0fnu) # (N, 7)
  95. # Dequantize: nope_tile * scale_tile → bf16 (vectorized)
  96. result = torch.empty(N, _D, dtype=torch.bfloat16, device=device)
  97. result[:, :_NOPE_DIM] = (
  98. (
  99. nope_fp8.view(N, _NUM_TILES, _TILE_SIZE).float()
  100. * scale_e8m0.view(N, _NUM_TILES, 1).float()
  101. )
  102. .view(N, _NOPE_DIM)
  103. .to(torch.bfloat16)
  104. )
  105. result[:, _NOPE_DIM:] = rope_bf16
  106. return result.reshape(*idx_shape, _D)
  107. def _sm120_sparse_decode_fwd(
  108. q,
  109. k_cache,
  110. indices,
  111. topk_length,
  112. attn_sink,
  113. head_dim_v,
  114. softmax_scale,
  115. extra_k_cache=None,
  116. extra_indices=None,
  117. extra_topk_length=None,
  118. ):
  119. B, s_q, H_q, D_qk = q.shape
  120. num_pages, page_size, H_k, bpt = k_cache.shape
  121. topk = indices.shape[-1]
  122. invalid_mask = indices < 0
  123. safe_indices = indices.clamp(min=0)
  124. if topk_length is not None:
  125. topk_range = torch.arange(topk, device=topk_length.device).view(1, 1, topk)
  126. invalid_mask = invalid_mask | (topk_range >= topk_length.view(B, 1, 1))
  127. # Gather and dequantize using page-aware addressing
  128. gathered_kv = _gather_and_dequant(k_cache, safe_indices, page_size)
  129. if extra_k_cache is not None and extra_indices is not None:
  130. extra_topk = extra_indices.shape[-1]
  131. extra_page_size = extra_k_cache.shape[1]
  132. extra_invalid = extra_indices < 0
  133. extra_safe = extra_indices.clamp(min=0)
  134. if extra_topk_length is not None:
  135. extra_range = torch.arange(
  136. extra_topk, device=extra_topk_length.device
  137. ).view(1, 1, extra_topk)
  138. extra_invalid = extra_invalid | (
  139. extra_range >= extra_topk_length.view(B, 1, 1)
  140. )
  141. extra_kv = _gather_and_dequant(extra_k_cache, extra_safe, extra_page_size)
  142. gathered_kv = torch.cat([gathered_kv, extra_kv], dim=2)
  143. invalid_mask = torch.cat([invalid_mask, extra_invalid], dim=2)
  144. gathered_kv[invalid_mask] = 0.0
  145. q_f = q.float()
  146. kv_f = gathered_kv.float()
  147. kv_d = kv_f.shape[-1]
  148. if D_qk != kv_d:
  149. q_f = q_f[..., :kv_d]
  150. scores = torch.einsum("bshd,bstd->bsht", q_f, kv_f) * softmax_scale
  151. scores.masked_fill_(invalid_mask.unsqueeze(2).expand_as(scores), float("-inf"))
  152. lse = torch.logsumexp(scores, dim=-1)
  153. if attn_sink is not None:
  154. lse_for_out = torch.logsumexp(
  155. torch.stack([lse, attn_sink.view(1, 1, H_q).expand_as(lse)], dim=0), dim=0
  156. )
  157. else:
  158. lse_for_out = lse.clone()
  159. lonely = lse == float("-inf")
  160. lse_for_out[lonely] = float("inf")
  161. weights = torch.exp(scores - lse_for_out.unsqueeze(-1))
  162. out = torch.einsum("bsht,bstv->bshv", weights, kv_f[..., :head_dim_v])
  163. out[lonely.unsqueeze(-1).expand_as(out)] = 0.0
  164. return out.to(torch.bfloat16), lse.permute(0, 2, 1)
  165. # SM120 FlashMLA: default FlashInfer (CUTLASS SM120 sparse MLA decode).
  166. # Override with SGLANG_SM120_FLASHMLA_BACKEND=triton|torch to force fallback.
  167. _sm120_default_backend = envs.SGLANG_SM120_FLASHMLA_BACKEND.get()
  168. SM120_DECODE_MAX_TOKENS = 64
  169. def _flash_mla_sm120_prefill(
  170. q,
  171. k_cache,
  172. indices,
  173. topk_length,
  174. attn_sink,
  175. head_dim_v,
  176. softmax_scale,
  177. extra_k_cache,
  178. extra_indices,
  179. extra_topk_length,
  180. ):
  181. from flashinfer.mla._sparse_mla_sm120 import _sparse_mla_sm120_paged_attention
  182. q2 = q.squeeze(1) if q.ndim == 4 else q
  183. num_tokens, num_heads, _ = q2.shape
  184. dev = q2.device
  185. kv_u8 = k_cache.view(torch.uint8) if k_cache.dtype != torch.uint8 else k_cache
  186. src_pbs = k_cache.shape[1] if k_cache.ndim >= 3 else _PBS_SRC
  187. idx = indices.squeeze(1) if indices.dim() == 3 else indices
  188. kv_64 = (
  189. _split_kv_pages_to_64(kv_u8, src_pbs, touched_indices=idx)
  190. if src_pbs != _PBS_DST
  191. else kv_u8
  192. )
  193. extra_kv_u8 = (
  194. extra_k_cache.view(torch.uint8)
  195. if extra_k_cache is not None and extra_k_cache.dtype != torch.uint8
  196. else extra_k_cache
  197. )
  198. extra_idx = (
  199. extra_indices.squeeze(1)
  200. if extra_indices is not None and extra_indices.dim() == 3
  201. else extra_indices
  202. )
  203. # V4.1 ratio-1/2 sources use 256/128-token pages. The prefill
  204. # specialization needs 64-token pages for both sources, with independent
  205. # scratch buffers so converting the extra source cannot overwrite SWA.
  206. if extra_kv_u8 is not None and extra_kv_u8.shape[1] != _PBS_DST:
  207. extra_kv_u8 = _split_kv_pages_to_64(
  208. extra_kv_u8, extra_kv_u8.shape[1],
  209. touched_indices=extra_idx, buffer_tag="extra",
  210. )
  211. output = q2.new_empty((num_tokens, num_heads, head_dim_v), dtype=torch.bfloat16)
  212. out_lse = torch.empty((num_tokens, num_heads), dtype=torch.float32, device=dev)
  213. _sparse_mla_sm120_paged_attention(
  214. q2,
  215. kv_64,
  216. idx,
  217. output,
  218. out_lse,
  219. softmax_scale,
  220. topk_length=topk_length,
  221. attn_sink=attn_sink,
  222. extra_kv_cache=extra_kv_u8,
  223. extra_indices=extra_idx,
  224. extra_topk_length=extra_topk_length,
  225. mid_out=None,
  226. mid_lse=None,
  227. )
  228. return (output.unsqueeze(1), None)
  229. @lru_cache(maxsize=1)
  230. def _flashinfer_dsv4_decode_capabilities() -> Tuple[int, FrozenSet[int]]:
  231. """Read the installed FlashInfer DSV4 decode capabilities once."""
  232. try:
  233. from flashinfer.mla._sparse_mla_sm120 import (
  234. _DECODE_DSV4_DISPATCH,
  235. _DECODE_MAX_TOKENS,
  236. )
  237. except (AttributeError, ImportError):
  238. return 0, frozenset()
  239. return int(_DECODE_MAX_TOKENS), frozenset(
  240. heads for heads, _ in _DECODE_DSV4_DISPATCH
  241. )
  242. def flashinfer_dsv4_decode_supports_num_heads(num_heads: int, num_tokens: int) -> bool:
  243. """Return whether FlashInfer supports this DSV4 decode head count.
  244. Keep this capability check fail-closed because SGLang can be used with a
  245. locally installed FlashInfer even though the release dependency is pinned.
  246. The padded 64-head decode path remains the safe fallback for older builds.
  247. Prefill head selection is handled separately by the caller.
  248. """
  249. decode_max_tokens, supported_heads = _flashinfer_dsv4_decode_capabilities()
  250. return num_tokens <= decode_max_tokens and num_heads in supported_heads
  251. def flash_mla_with_kvcache_sm120(**kwargs):
  252. """SM120 FlashMLA sparse decode entry point.
  253. Dispatches to FlashInfer (default if available), Triton, or PyTorch fallback.
  254. """
  255. q = kwargs["q"]
  256. k_cache = kwargs["k_cache"]
  257. indices = kwargs["indices"]
  258. topk_length = kwargs.get("topk_length")
  259. attn_sink = kwargs.get("attn_sink")
  260. head_dim_v = kwargs["head_dim_v"]
  261. softmax_scale = kwargs.get("softmax_scale")
  262. if softmax_scale is None:
  263. softmax_scale = q.shape[-1] ** (-0.5)
  264. extra_k_cache = kwargs.get("extra_k_cache")
  265. extra_indices = kwargs.get("extra_indices_in_kvcache")
  266. extra_topk_length = kwargs.get("extra_topk_length")
  267. if _sm120_default_backend == "flashinfer":
  268. if q.shape[0] > SM120_DECODE_MAX_TOKENS:
  269. return _flash_mla_sm120_prefill(
  270. q,
  271. k_cache,
  272. indices,
  273. topk_length,
  274. attn_sink,
  275. head_dim_v,
  276. softmax_scale,
  277. extra_k_cache,
  278. extra_indices,
  279. extra_topk_length,
  280. )
  281. return _flash_mla_flashinfer(
  282. q,
  283. k_cache,
  284. indices,
  285. topk_length,
  286. attn_sink,
  287. head_dim_v,
  288. softmax_scale,
  289. extra_k_cache,
  290. extra_indices,
  291. extra_topk_length,
  292. )
  293. if _sm120_default_backend == "triton":
  294. from sglang.kernels.ops.attention.flash_mla_sm120_triton import (
  295. flash_mla_sparse_decode_triton,
  296. )
  297. out, lse = flash_mla_sparse_decode_triton(
  298. q,
  299. k_cache,
  300. indices,
  301. topk_length,
  302. attn_sink,
  303. head_dim_v,
  304. softmax_scale,
  305. extra_k_cache,
  306. extra_indices,
  307. extra_topk_length,
  308. )
  309. return (out, lse)
  310. out, lse = _sm120_sparse_decode_fwd(
  311. q,
  312. k_cache,
  313. indices,
  314. topk_length,
  315. attn_sink,
  316. head_dim_v,
  317. softmax_scale,
  318. extra_k_cache,
  319. extra_indices,
  320. extra_topk_length,
  321. )
  322. return (out, lse)
  323. # --- Page-split utilities: pbs=256 → pbs=64 ---
  324. # SGLang SWA KV cache footer layout per 256-token page:
  325. # [data: 256 * 576 bytes] [scale: 256 * 8 bytes] [padding]
  326. # FlashInfer decode_dsv4 expects per 64-token page:
  327. # [data: 64 * 576 bytes] [scale: 64 * 8 bytes] [padding to 37440]
  328. _PBS_SRC = 256 # SGLang physical page size
  329. _PBS_DST = 64 # FlashInfer page_block_size
  330. _NOPE_ROPE_STRIDE = 576 # bytes per token for nope+rope
  331. _SCALE_STRIDE = 8 # bytes per token for scale (7 + 1 pad)
  332. _BYTES_PER_DST_PAGE = (
  333. _PBS_DST * _NOPE_ROPE_STRIDE + _PBS_DST * _SCALE_STRIDE
  334. ) # 64*576 + 64*8 = 37376 + 512 = 37888
  335. # Padded to 576 alignment
  336. _BYTES_PER_DST_PAGE_PADDED = math.ceil(_BYTES_PER_DST_PAGE / 576) * 576 # 37440
  337. @triton.jit
  338. def _page_split_kernel(
  339. src_ptr,
  340. dst_ptr,
  341. N_pages,
  342. src_stride0: tl.constexpr,
  343. dst_stride0: tl.constexpr,
  344. DATA_PER_SUB: tl.constexpr, # 64 * 576 = 36864
  345. SCALE_PER_SUB: tl.constexpr, # 64 * 8 = 512
  346. SRC_SCALE_OFF: tl.constexpr, # 256 * 576 = 147456
  347. DST_SCALE_OFF: tl.constexpr, # 64 * 576 = 36864
  348. RATIO: tl.constexpr, # 4
  349. BLOCK_SIZE: tl.constexpr,
  350. mask_ptr,
  351. HAS_MASK: tl.constexpr,
  352. ):
  353. """Fused page-split: copy data+scale for all sub-pages in one kernel.
  354. When HAS_MASK is set, only pages flagged in ``mask_ptr`` (int8, 1=touched)
  355. are copied; untouched pages are skipped so the kernel no longer rewrites the
  356. entire KV pool every decode step.
  357. """
  358. pid = tl.program_id(0)
  359. page_idx = pid // RATIO
  360. sub = pid % RATIO
  361. if page_idx >= N_pages:
  362. return
  363. if HAS_MASK:
  364. if tl.load(mask_ptr + page_idx) == 0:
  365. return
  366. # All layout strides/offsets are 8-byte aligned (asserted at the call
  367. # site), so copy in u64 lanes instead of single bytes.
  368. src_u64 = src_ptr.to(tl.pointer_type(tl.uint64))
  369. dst_u64 = dst_ptr.to(tl.pointer_type(tl.uint64))
  370. src_base = src_u64 + page_idx * (src_stride0 // 8)
  371. dst_base = dst_u64 + (page_idx * RATIO + sub) * (dst_stride0 // 8)
  372. DATA_U64: tl.constexpr = DATA_PER_SUB // 8
  373. SCALE_U64: tl.constexpr = SCALE_PER_SUB // 8
  374. # Copy data region: DATA_U64 u64 lanes from src offset sub*DATA_U64
  375. data_src_off = sub * DATA_U64
  376. for start in tl.range(0, DATA_U64, BLOCK_SIZE):
  377. offs = start + tl.arange(0, BLOCK_SIZE)
  378. mask = offs < DATA_U64
  379. vals = tl.load(src_base + data_src_off + offs, mask=mask)
  380. tl.store(dst_base + offs, vals, mask=mask)
  381. # Copy scale region: SCALE_U64 u64 lanes
  382. scale_src_off = SRC_SCALE_OFF // 8 + sub * SCALE_U64
  383. for start in tl.range(0, SCALE_U64, BLOCK_SIZE):
  384. offs = start + tl.arange(0, BLOCK_SIZE)
  385. mask = offs < SCALE_U64
  386. vals = tl.load(src_base + scale_src_off + offs, mask=mask)
  387. tl.store(dst_base + DST_SCALE_OFF // 8 + offs, vals, mask=mask)
  388. @triton.jit
  389. def _page_mark_kernel(
  390. indices_ptr,
  391. mask_ptr,
  392. N_idx,
  393. SRC_PBS: tl.constexpr,
  394. BLOCK: tl.constexpr,
  395. ):
  396. """Mark touched source pages (1 byte each) from token-level indices.
  397. ``indices`` are token indices into the pbs=SRC_PBS SWA pool; -1 = invalid.
  398. Each valid token marks ``mask[token // SRC_PBS] = 1``. Concurrent stores of
  399. the same value 1 are safe (no atomic needed).
  400. """
  401. pid = tl.program_id(0)
  402. offs = pid * BLOCK + tl.arange(0, BLOCK)
  403. valid = offs < N_idx
  404. idx = tl.load(indices_ptr + offs, mask=valid, other=-1)
  405. keep = valid & (idx >= 0)
  406. page = idx // SRC_PBS
  407. tl.store(mask_ptr + page, tl.full((BLOCK,), 1, tl.int8), mask=keep)
  408. def _split_kv_pages_to_64(
  409. kv_u8: torch.Tensor,
  410. src_pbs: int,
  411. touched_indices: Optional[torch.Tensor] = None,
  412. buffer_tag: str = "primary",
  413. ) -> torch.Tensor:
  414. """Split pbs=N footer-format pages into pbs=64 footer-format pages.
  415. When ``touched_indices`` (token-level int32 indices into the pbs=src_pbs
  416. SWA pool, -1 = invalid) is provided, only the source pages that actually
  417. contain a referenced token are copied. This avoids rewriting the entire KV
  418. pool on every decode step (only ~2*batch pages are touched vs the full
  419. pool). The output buffer is persistent and reused across steps; untouched
  420. dst pages simply retain their (unreferenced) stale data.
  421. """
  422. assert src_pbs % _PBS_DST == 0 and src_pbs >= _PBS_DST
  423. if src_pbs == _PBS_DST:
  424. return kv_u8
  425. N = kv_u8.shape[0]
  426. ratio = src_pbs // _PBS_DST
  427. num_dst_pages = N * ratio
  428. from sglang.srt.runtime_context import get_resources
  429. # Pre-allocated grow-only buffer for page-split output per device.
  430. dev = kv_u8.device
  431. buffers = get_resources().buffers
  432. key = f"flash_mla_sm120_split:{dev}:{buffer_tag}"
  433. buf = buffers.get(key)
  434. if buf is None or buf.shape[0] < num_dst_pages:
  435. # The first allocation can happen under inference mode (autotune), but
  436. # the buffer is written again during CUDA graph capture outside
  437. # inference mode, where an inference tensor cannot be mutated.
  438. with torch.inference_mode(False):
  439. buf = torch.empty(
  440. num_dst_pages,
  441. _BYTES_PER_DST_PAGE_PADDED,
  442. dtype=torch.uint8,
  443. device=dev,
  444. )
  445. buffers[key] = buf
  446. out = buf[:num_dst_pages]
  447. # Get raw 2D view of source
  448. src_2d = kv_u8
  449. if src_2d.ndim == 4:
  450. src_stride0 = src_2d.stride(0)
  451. src_2d = torch.as_strided(src_2d, (N, src_stride0), (src_stride0, 1))
  452. else:
  453. src_stride0 = src_2d.stride(0)
  454. use_mask = touched_indices is not None and touched_indices.numel() > 0
  455. mask_ptr = src_2d # dummy, never dereferenced when HAS_MASK is False
  456. if use_mask:
  457. # Persistent per-device int8 mask, zeroed each call (cheap memset,
  458. # captured cleanly by CUDA graph). 1 = page is referenced this step.
  459. mkey = f"flash_mla_sm120_mask:{dev}:{buffer_tag}"
  460. mbuf = buffers.get(mkey)
  461. if mbuf is None or mbuf.shape[0] < N:
  462. # The first allocation can happen under inference mode (autotune),
  463. # but the buffer is zeroed again later during CUDA graph capture
  464. # outside inference mode -- an inference tensor cannot be mutated
  465. # there, so force a normal tensor.
  466. with torch.inference_mode(False):
  467. mbuf = torch.empty(N, dtype=torch.int8, device=dev)
  468. buffers[mkey] = mbuf
  469. mask = mbuf[:N]
  470. mask.zero_()
  471. idx_flat = touched_indices.reshape(-1).contiguous()
  472. if idx_flat.dtype != torch.int32:
  473. idx_flat = idx_flat.to(torch.int32)
  474. _MARK_BLOCK = 1024
  475. _page_mark_kernel[(triton.cdiv(idx_flat.numel(), _MARK_BLOCK),)](
  476. idx_flat,
  477. mask,
  478. idx_flat.numel(),
  479. src_pbs, # SRC_PBS
  480. _MARK_BLOCK,
  481. )
  482. mask_ptr = mask
  483. assert src_stride0 % 8 == 0 and _BYTES_PER_DST_PAGE_PADDED % 8 == 0
  484. grid = (N * ratio,)
  485. _page_split_kernel[grid](
  486. src_2d,
  487. out,
  488. N,
  489. src_stride0,
  490. _BYTES_PER_DST_PAGE_PADDED,
  491. _PBS_DST * _NOPE_ROPE_STRIDE, # DATA_PER_SUB = 36864
  492. _PBS_DST * _SCALE_STRIDE, # SCALE_PER_SUB = 512
  493. src_pbs * _NOPE_ROPE_STRIDE, # SRC_SCALE_OFF = 147456
  494. _PBS_DST * _NOPE_ROPE_STRIDE, # DST_SCALE_OFF = 36864
  495. ratio, # RATIO = 4
  496. 1024, # BLOCK_SIZE
  497. mask_ptr,
  498. use_mask, # HAS_MASK
  499. )
  500. bpt = _NOPE_ROPE_STRIDE + _SCALE_STRIDE # 584
  501. return out.as_strided(
  502. (num_dst_pages, _PBS_DST, 1, bpt),
  503. (_BYTES_PER_DST_PAGE_PADDED, bpt, bpt, 1),
  504. )
  505. def _flash_mla_flashinfer(
  506. q,
  507. k_cache,
  508. indices,
  509. topk_length,
  510. attn_sink,
  511. head_dim_v,
  512. softmax_scale,
  513. extra_k_cache,
  514. extra_indices,
  515. extra_topk_length,
  516. ):
  517. """FlashInfer SM120 sparse MLA via the paged-attention dispatcher.
  518. SGLang SWA pool uses page_size=256 (footer format: 256*576 bytes data + 256*8 bytes scale).
  519. FlashInfer decode_dsv4 fast path requires page_block_size=64 (footer: 64*576 + 64*8).
  520. We split 256-token pages into 4 virtual 64-token pages.
  521. Token indices are invariant under page-split (identity mapping).
  522. """
  523. from flashinfer.mla._sparse_mla_sm120 import (
  524. _DECODE_MAX_TOKENS as _FI_DECODE_MAX_TOKENS,
  525. )
  526. from flashinfer.mla._sparse_mla_sm120 import (
  527. _sparse_mla_sm120_paged_attention,
  528. )
  529. B, _, H, D = q.shape # (batch, 1, num_heads, head_dim)
  530. dev = q.device
  531. # Indices: no remapping needed (page-split preserves token addressing).
  532. idx = indices.squeeze(1) if indices.dim() == 3 else indices
  533. # --- Page-split: convert pbs=N kv_cache to pbs=64 view ---
  534. # Only the SWA pages actually referenced by `idx` are copied (the rest of
  535. # the persistent dst buffer is left untouched and never read).
  536. kv_u8 = k_cache.view(torch.uint8) if k_cache.dtype != torch.uint8 else k_cache
  537. src_pbs = k_cache.shape[1] if k_cache.ndim >= 3 else _PBS_SRC
  538. kv_64 = (
  539. _split_kv_pages_to_64(kv_u8, src_pbs, touched_indices=idx)
  540. if src_pbs != _PBS_DST
  541. else kv_u8
  542. )
  543. extra_kv_u8 = (
  544. extra_k_cache.view(torch.uint8)
  545. if extra_k_cache is not None and extra_k_cache.dtype != torch.uint8
  546. else extra_k_cache
  547. )
  548. extra_kv_64 = extra_kv_u8
  549. extra_idx = (
  550. extra_indices.squeeze(1)
  551. if extra_indices is not None and extra_indices.dim() == 3
  552. else extra_indices
  553. )
  554. output = torch.empty(B, H, head_dim_v, dtype=torch.bfloat16, device=dev)
  555. out_lse = torch.empty(B, H, dtype=torch.float32, device=dev)
  556. # Use split-K for decode-sized batches and paged attention otherwise.
  557. if B <= _FI_DECODE_MAX_TOKENS:
  558. topk = idx.shape[-1]
  559. extra_topk = extra_idx.shape[-1] if extra_idx is not None else 0
  560. _BI = 64
  561. num_splits = (topk + _BI - 1) // _BI + (
  562. (extra_topk + _BI - 1) // _BI if extra_topk > 0 else 0
  563. )
  564. mid_out = torch.empty(
  565. B, H, num_splits, head_dim_v, dtype=torch.bfloat16, device=dev
  566. )
  567. mid_lse = torch.empty(B, H, num_splits, dtype=torch.float32, device=dev)
  568. else:
  569. mid_out = None
  570. mid_lse = None
  571. _sparse_mla_sm120_paged_attention(
  572. q.squeeze(1) if q.ndim == 4 else q,
  573. kv_64,
  574. idx,
  575. output,
  576. out_lse,
  577. softmax_scale,
  578. d_v=head_dim_v,
  579. topk_length=topk_length,
  580. attn_sink=attn_sink,
  581. extra_kv_cache=extra_kv_64,
  582. extra_indices=extra_idx,
  583. extra_topk_length=extra_topk_length,
  584. mid_out=mid_out,
  585. mid_lse=mid_lse,
  586. )
  587. return (output.unsqueeze(1), None)
  588. def _validate_flashinfer_sparse_mla_backend(
  589. *,
  590. model_arch: str,
  591. device_sm_major: int,
  592. kv_cache_dtype: torch.dtype,
  593. prefill_impl: str,
  594. decode_impl: str,
  595. ) -> bool:
  596. selected = {prefill_impl, decode_impl}
  597. uses_flashinfer_sparse_mla = "flashinfer_sparse_mla" in selected
  598. is_glm_sm12_fp8 = (
  599. model_arch in _GLM_DSA_MODEL_ARCHS
  600. and device_sm_major == 12
  601. and kv_cache_dtype == torch.float8_e4m3fn
  602. and not _is_hip
  603. )
  604. if uses_flashinfer_sparse_mla and not is_glm_sm12_fp8:
  605. raise ValueError(
  606. "flashinfer_sparse_mla supports only GLM DSA with FP8 KV cache "
  607. "on NVIDIA SM120/SM121; "
  608. f"got model_arch={model_arch!r}, sm_major={device_sm_major}, "
  609. f"kv_cache_dtype={kv_cache_dtype}, prefill_impl={prefill_impl!r}, "
  610. f"decode_impl={decode_impl!r}."
  611. )
  612. if is_glm_sm12_fp8:
  613. unsupported = selected - {"flashinfer_sparse_mla"}
  614. if unsupported:
  615. raise ValueError(
  616. "GLM DSA with FP8 KV cache on NVIDIA SM120/SM121 supports "
  617. "only flashinfer_sparse_mla, "
  618. f"but got {sorted(unsupported)}."
  619. )
  620. return uses_flashinfer_sparse_mla
  621. def flashinfer_sparse_mla_forward(
  622. q: torch.Tensor,
  623. kv_cache: torch.Tensor,
  624. indices: torch.Tensor,
  625. seq_lens: torch.Tensor,
  626. workspace_buffer: torch.Tensor,
  627. *,
  628. page_size: int,
  629. kv_cache_dim: int,
  630. qk_nope_head_dim: int,
  631. kv_lora_rank: int,
  632. qk_rope_head_dim: int,
  633. sm_scale: float,
  634. skip_softmax_threshold_scale_factor: float | None,
  635. ) -> torch.Tensor:
  636. """Run FlashInfer's SM120 sparse MLA kernel on SGLang's packed DSA cache."""
  637. from flashinfer.mla import trtllm_batch_decode_with_kv_cache_mla
  638. topk = indices.shape[1]
  639. result = trtllm_batch_decode_with_kv_cache_mla(
  640. query=q.unsqueeze(1),
  641. kv_cache=kv_cache.view(torch.uint8)
  642. .view(-1, page_size, kv_cache_dim)
  643. .unsqueeze(1),
  644. workspace_buffer=workspace_buffer,
  645. qk_nope_head_dim=qk_nope_head_dim,
  646. kv_lora_rank=kv_lora_rank,
  647. qk_rope_head_dim=qk_rope_head_dim,
  648. block_tables=indices.unsqueeze(1),
  649. seq_lens=seq_lens,
  650. max_seq_len=topk,
  651. sparse_mla_top_k=topk,
  652. bmm1_scale=float(sm_scale),
  653. bmm2_scale=1.0,
  654. kv_scale_format="arbitrary_fp32",
  655. skip_softmax_threshold_scale_factor=skip_softmax_threshold_scale_factor,
  656. )
  657. return result.squeeze(1)