recoverssm.py 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. """GLM-5.3-Flash KDA speculative verify with one checkpoint state per request.
  4. A port of Kimi-K3's RecoverSSM (models/kimi_k3/nvidia/ops/recoverssm.py and
  5. kda_metadata.py), enabled with VLLM_GLM5NEXT_RECOVERSSM=1.
  6. The stock spec path keeps 1 + num_spec recurrent states per request and writes
  7. one full state per verified token. Here the verify kernel reads the request's
  8. checkpoint, writes the outputs and records each token's raw inputs. After
  9. sampling, the commit replays only the accepted tokens from the checkpoint.
  10. Where this differs from Kimi-K3's version:
  11. - The records are the raw k, gate, v and beta, and the commit replays them
  12. through the stock fused_recurrent_kda kernel, so the committed state is the
  13. one the stock spec path would have stored. Kimi-K3 stores fp32 corrections
  14. and folds them in closed form, which rounds differently. A copy of the stock
  15. recurrence also rounded differently (1-2 ulp): it read its state and records
  16. through integer addresses, so Triton chose other layouts and reduced sums in
  17. another order.
  18. - The records live in a per-row scratch outside the KV pool: they only live
  19. from verify to commit within one step. Inside the mamba page they would
  20. make it larger than the 2304-token attention page and double the block size.
  21. """
  22. from dataclasses import dataclass, field
  23. from functools import cache
  24. from typing import Any
  25. import torch
  26. from vllm.config import VllmConfig
  27. from vllm.model_executor.layers.mamba.mamba_utils import is_conv_state_dim_first
  28. from vllm.models.glm5next.nvidia.ops.third_party.kda import fused_recurrent_kda
  29. from vllm.models.glm5next.nvidia.ops.third_party.kda.fused_recurrent import (
  30. token_stride as _token_stride,
  31. )
  32. from vllm.third_party.flash_linear_attention.ops.op import exp
  33. from vllm.triton_utils import tl, triton
  34. from vllm.utils.torch_utils import async_tensor_h2d
  35. from vllm.v1.attention.backend import CommonAttentionMetadata
  36. from vllm.v1.attention.backends.gdn_attn import (
  37. GDNAttentionBackend,
  38. GDNAttentionMetadata,
  39. GDNAttentionMetadataBuilder,
  40. )
  41. from vllm.v1.attention.backends.recoverssm_metadata import (
  42. RecoverSSMMetadata,
  43. RecoverSSMPostprocessMetadata,
  44. )
  45. from vllm.v1.attention.backends.utils import (
  46. NULL_BLOCK_ID,
  47. compute_causal_conv1d_metadata,
  48. mamba_get_block_table_tensor,
  49. )
  50. from vllm.v1.kv_cache_interface import MambaSpec
  51. # Tile shape and warps of the stock fused_recurrent_kda launch. The verify
  52. # kernel uses the same so its fp32 arithmetic compiles the same way.
  53. _BV = 8
  54. _NUM_WARPS = 1
  55. _NUM_STAGES = 3
  56. @triton.jit
  57. def _verify_kernel(
  58. q,
  59. k,
  60. v,
  61. g,
  62. beta,
  63. o,
  64. h0,
  65. rec_kv,
  66. rec_g,
  67. rec_beta,
  68. cu_seqlens,
  69. state_indices,
  70. a_log,
  71. g_bias,
  72. scale,
  73. stride_q_t,
  74. stride_k_t,
  75. stride_v_t,
  76. stride_beta_t,
  77. stride_state_indices,
  78. stride_state: tl.constexpr,
  79. H: tl.constexpr,
  80. K: tl.constexpr,
  81. V: tl.constexpr,
  82. Q: tl.constexpr,
  83. BK: tl.constexpr,
  84. BV: tl.constexpr,
  85. LOWER_BOUND: tl.constexpr,
  86. ):
  87. # The per-token body is fused_recurrent_gated_delta_rule_fwd_kernel's
  88. # (IS_KDA, COMPUTE_GATE, SIGMOID_BETA, USE_QK_L2NORM_IN_KERNEL). Keep the
  89. # two in step: the equivalence test compares them bit for bit.
  90. i_k, i_v, i_nh = tl.program_id(0), tl.program_id(1), tl.program_id(2)
  91. i_n, i_h = i_nh // H, i_nh % H
  92. bos = tl.load(cu_seqlens + i_n).to(tl.int64)
  93. eos = tl.load(cu_seqlens + i_n + 1).to(tl.int64)
  94. T = eos - bos
  95. if T == 0:
  96. return
  97. o_k = i_k * BK + tl.arange(0, BK)
  98. o_v = i_v * BV + tl.arange(0, BV)
  99. p_q = q + bos * stride_q_t + i_h * K + o_k
  100. p_k = k + bos * stride_k_t + i_h * K + o_k
  101. p_v = v + bos * stride_v_t + i_h * V + o_v
  102. p_beta = beta + bos * stride_beta_t + i_h
  103. p_gk = g + (bos * H + i_h) * K + o_k
  104. b_a_log = tl.exp(tl.load(a_log + i_h).to(tl.float32))
  105. p_o = o + (bos * H + i_h) * V + o_v
  106. mask_k = o_k < K
  107. mask_v = o_v < V
  108. mask_h = mask_v[:, None] & mask_k[None, :]
  109. state_idx = tl.load(state_indices + i_n * stride_state_indices).to(tl.int64)
  110. if state_idx <= 0:
  111. return
  112. b_h = tl.zeros([BV, BK], dtype=tl.float32)
  113. p_h0 = h0 + state_idx * stride_state + i_h * V * K + o_v[:, None] * K + o_k[None, :]
  114. b_h += tl.load(p_h0, mask=mask_h, other=0).to(tl.float32)
  115. # Row i_n owns record tokens [i_n * Q, i_n * Q + Q), laid out the way the
  116. # stock kernel reads q/k/v, g and beta. The commit reads them in the same
  117. # step over the same rows, so they never outlive a step.
  118. tok = i_n * Q
  119. p_rk = rec_kv + (tok * 2 * H + i_h) * K + o_k
  120. p_rv = rec_kv + ((tok * 2 + 1) * H + i_h) * V + o_v
  121. p_rg = rec_g + (tok * H + i_h) * K + o_k
  122. p_rbeta = rec_beta + tok * H + i_h
  123. for i_t in range(0, T):
  124. b_q = tl.load(p_q, mask=mask_k, other=0).to(tl.float32)
  125. r_k = tl.load(p_k, mask=mask_k, other=0)
  126. b_k = r_k.to(tl.float32)
  127. r_v = tl.load(p_v, mask=mask_v, other=0)
  128. b_v = r_v.to(tl.float32)
  129. b_q = b_q / tl.sqrt(tl.sum(b_q * b_q) + 1e-6)
  130. b_k = b_k / tl.sqrt(tl.sum(b_k * b_k) + 1e-6)
  131. b_q = b_q * scale
  132. r_g = tl.load(p_gk)
  133. b_gk = r_g.to(tl.float32)
  134. b_gk += tl.load(g_bias + i_h * K + o_k, mask=mask_k, other=0.0).to(tl.float32)
  135. b_gk = LOWER_BOUND / (1.0 + tl.exp(-(b_a_log * b_gk)))
  136. b_h *= exp(b_gk[None, :])
  137. b_v -= tl.sum(b_h * b_k[None, :], 1)
  138. r_beta = tl.load(p_beta)
  139. b_beta = r_beta.to(tl.float32)
  140. b_beta = tl.sigmoid(b_beta)
  141. b_v *= b_beta
  142. b_h += b_v[:, None] * b_k[None, :]
  143. b_o = tl.sum(b_h * b_q[None, :], 1)
  144. tl.store(p_o, b_o.to(p_o.dtype.element_ty), mask=mask_v)
  145. tl.store(p_rv + i_t * 2 * H * V, r_v, mask=mask_v)
  146. if i_v == 0:
  147. tl.store(p_rk + i_t * 2 * H * K, r_k, mask=mask_k)
  148. tl.store(p_rg + i_t * H * K, r_g, mask=mask_k)
  149. tl.store(p_rbeta + i_t * H, r_beta)
  150. p_q += stride_q_t
  151. p_k += stride_k_t
  152. p_o += H * V
  153. p_v += stride_v_t
  154. p_gk += H * K
  155. p_beta += stride_beta_t
  156. @triton.heuristics(
  157. {
  158. "HAS_REQUEST_INDICES": lambda args: args["request_indices_ptr"] is not None,
  159. "ALIGN_MODE": lambda args: args["block_table_ptr"] is not None,
  160. }
  161. )
  162. @triton.jit
  163. def _prepare_commit_plan_kernel(
  164. num_accepted_ptr,
  165. request_indices_ptr,
  166. state_indices_ptr,
  167. query_start_loc_ptr,
  168. block_table_ptr,
  169. num_computed_ptr,
  170. commit_lens_ptr,
  171. final_state_indices_ptr,
  172. boundary_state_indices_ptr,
  173. boundary_recovery_lens_ptr,
  174. null_block_id,
  175. mamba_block_size,
  176. block_table_width,
  177. stride_num_accepted,
  178. stride_request_indices,
  179. stride_state_indices,
  180. stride_query_start_loc,
  181. stride_block_table_row,
  182. stride_block_table_col,
  183. stride_num_computed,
  184. SPEC_QUERY_LEN: tl.constexpr,
  185. HAS_REQUEST_INDICES: tl.constexpr,
  186. ALIGN_MODE: tl.constexpr,
  187. ):
  188. # Kimi-K3's RecoverSSM, except for the final state's column. Kimi-K3 uses
  189. # n // block_size, where n is the committed count. When n ends exactly on a
  190. # boundary, that is the next block, which align mode has not allocated
  191. # yet, so the state was lost. This uses (n - 1) // block_size, the block
  192. # holding the last token, as the stock align path does; the boundary block
  193. # is then the final block itself. _record_state_block_kernel records the
  194. # same column for the next step's pre-step copy.
  195. spec_idx = tl.program_id(0)
  196. source_state_idx = tl.load(state_indices_ptr + spec_idx * stride_state_indices).to(
  197. tl.int64
  198. )
  199. request_idx = spec_idx
  200. if HAS_REQUEST_INDICES:
  201. request_idx = tl.load(
  202. request_indices_ptr + spec_idx * stride_request_indices
  203. ).to(tl.int64)
  204. num_accepted = tl.load(num_accepted_ptr + request_idx * stride_num_accepted).to(
  205. tl.int32
  206. )
  207. bos = tl.load(query_start_loc_ptr + spec_idx * stride_query_start_loc).to(tl.int64)
  208. eos = tl.load(query_start_loc_ptr + (spec_idx + 1) * stride_query_start_loc).to(
  209. tl.int64
  210. )
  211. query_len = (eos - bos).to(tl.int32)
  212. commit_len = tl.minimum(tl.maximum(num_accepted, 0), query_len)
  213. commit_len = tl.minimum(commit_len, SPEC_QUERY_LEN)
  214. final_state_idx = source_state_idx
  215. boundary_state_idx = null_block_id
  216. boundary_recovery_len = 0
  217. if ALIGN_MODE:
  218. num_computed = tl.load(num_computed_ptr + request_idx * stride_num_computed).to(
  219. tl.int32
  220. )
  221. final_num_computed = num_computed + commit_len
  222. final_state_col = tl.minimum(
  223. tl.maximum(final_num_computed - 1, 0) // mamba_block_size,
  224. block_table_width - 1,
  225. )
  226. final_state_idx = tl.load(
  227. block_table_ptr
  228. + request_idx * stride_block_table_row
  229. + final_state_col * stride_block_table_col
  230. ).to(tl.int64)
  231. next_boundary = (num_computed // mamba_block_size + 1) * mamba_block_size
  232. crosses_boundary = final_num_computed >= next_boundary
  233. boundary_recovery_len = next_boundary - num_computed
  234. boundary_state_idx = tl.load(
  235. block_table_ptr
  236. + request_idx * stride_block_table_row
  237. + (next_boundary // mamba_block_size - 1) * stride_block_table_col,
  238. mask=crosses_boundary,
  239. other=null_block_id,
  240. ).to(tl.int64)
  241. valid = (source_state_idx > null_block_id) & (commit_len > 0)
  242. tl.store(commit_lens_ptr + spec_idx, tl.where(valid, commit_len, 0))
  243. tl.store(
  244. final_state_indices_ptr + spec_idx,
  245. tl.where(valid, final_state_idx, null_block_id),
  246. )
  247. tl.store(
  248. boundary_state_indices_ptr + spec_idx,
  249. tl.where(valid, boundary_state_idx, null_block_id),
  250. )
  251. tl.store(
  252. boundary_recovery_lens_ptr + spec_idx,
  253. tl.where(valid, boundary_recovery_len, 0),
  254. )
  255. @triton.jit
  256. def _compact_conv_state_kernel(
  257. conv_state_ref_ptr,
  258. conv_state_base_addrs_ptr,
  259. conv_state_block_strides_ptr,
  260. conv_state_dim_strides_ptr,
  261. conv_state_token_strides_ptr,
  262. state_indices_ptr,
  263. commit_lens_ptr,
  264. final_state_indices_ptr,
  265. boundary_state_indices_ptr,
  266. boundary_recovery_lens_ptr,
  267. null_block_id,
  268. conv_dim,
  269. conv_history_len,
  270. stride_state_indices,
  271. BLOCK_D: tl.constexpr,
  272. BLOCK_HISTORY: tl.constexpr,
  273. ALIGN_MODE: tl.constexpr,
  274. ):
  275. # Unchanged from Kimi-K3's RecoverSSM. After the verify the conv state is
  276. # [history[1:], x_0 .. x_{q-1}]; the window after n accepted tokens starts
  277. # at n - 1, and moves to column 0 so the next step reads it with
  278. # num_accepted_tokens == 1.
  279. pid_d = tl.program_id(0)
  280. pid_b = tl.program_id(1)
  281. pid_l = tl.program_id(2)
  282. source_state_idx = tl.load(state_indices_ptr + pid_b * stride_state_indices).to(
  283. tl.int64
  284. )
  285. if source_state_idx <= null_block_id:
  286. return
  287. commit_len = tl.load(commit_lens_ptr + pid_b)
  288. if commit_len == 0:
  289. return
  290. final_state_idx = tl.load(final_state_indices_ptr + pid_b).to(tl.int64)
  291. boundary_state_idx = tl.load(boundary_state_indices_ptr + pid_b).to(tl.int64)
  292. boundary_recovery_len = tl.load(boundary_recovery_lens_ptr + pid_b)
  293. if final_state_idx <= null_block_id:
  294. return
  295. base_addr = tl.load(conv_state_base_addrs_ptr + pid_l)
  296. block_stride = tl.load(conv_state_block_strides_ptr + pid_l)
  297. dim_stride = tl.load(conv_state_dim_strides_ptr + pid_l)
  298. token_stride = tl.load(conv_state_token_strides_ptr + pid_l)
  299. conv_state_ptr = base_addr.to(tl.pointer_type(conv_state_ref_ptr.dtype.element_ty))
  300. source_ptr = conv_state_ptr + source_state_idx * block_stride
  301. final_ptr = conv_state_ptr + final_state_idx * block_stride
  302. offs_d = pid_d * BLOCK_D + tl.arange(0, BLOCK_D)
  303. offs_h = tl.arange(0, BLOCK_HISTORY)
  304. mask = (offs_d[:, None] < conv_dim) & (offs_h[None, :] < conv_history_len)
  305. final_values = tl.load(
  306. source_ptr
  307. + offs_d[:, None] * dim_stride
  308. + (commit_len - 1 + offs_h[None, :]) * token_stride,
  309. mask=mask,
  310. )
  311. if ALIGN_MODE:
  312. boundary_values = tl.load(
  313. source_ptr
  314. + offs_d[:, None] * dim_stride
  315. + (boundary_recovery_len - 1 + offs_h[None, :]) * token_stride,
  316. mask=mask & (boundary_state_idx > null_block_id),
  317. )
  318. boundary_ptr = conv_state_ptr + boundary_state_idx * block_stride
  319. tl.store(
  320. boundary_ptr
  321. + offs_d[:, None] * dim_stride
  322. + offs_h[None, :] * token_stride,
  323. boundary_values,
  324. mask=mask & (boundary_state_idx > null_block_id),
  325. )
  326. tl.store(
  327. final_ptr + offs_d[:, None] * dim_stride + offs_h[None, :] * token_stride,
  328. final_values,
  329. mask=mask,
  330. )
  331. @triton.jit
  332. def _replay_indices_kernel(
  333. state_indices_ptr,
  334. commit_lens_ptr,
  335. final_state_indices_ptr,
  336. boundary_state_indices_ptr,
  337. boundary_recovery_lens_ptr,
  338. replay_indices_ptr,
  339. null_block_id,
  340. stride_state_indices,
  341. Q: tl.constexpr,
  342. BLOCK: tl.constexpr,
  343. ):
  344. # One row of the stock kernel's ssm_state_indices: it reads the initial
  345. # state at column num_accepted - 1 (Q here) and stores the state after
  346. # token t to column t when that is not the null block.
  347. i_n = tl.program_id(0)
  348. cols = tl.arange(0, BLOCK)
  349. source = tl.load(state_indices_ptr + i_n * stride_state_indices).to(tl.int32)
  350. commit_len = tl.load(commit_lens_ptr + i_n)
  351. final = tl.load(final_state_indices_ptr + i_n).to(tl.int32)
  352. boundary = tl.load(boundary_state_indices_ptr + i_n).to(tl.int32)
  353. boundary_len = tl.load(boundary_recovery_lens_ptr + i_n)
  354. row = tl.where(cols == commit_len - 1, final, null_block_id)
  355. # A boundary at the last token is the final state's own block.
  356. at_boundary = (
  357. (cols == boundary_len - 1)
  358. & (boundary_len < commit_len)
  359. & (boundary > null_block_id)
  360. )
  361. row = tl.where(at_boundary, boundary, row)
  362. row = tl.where(cols == Q, source, row)
  363. valid = (source > null_block_id) & (commit_len > 0) & (final > null_block_id)
  364. row = tl.where(valid, row, null_block_id)
  365. tl.store(replay_indices_ptr + i_n * (Q + 1) + cols, row, mask=cols < Q + 1)
  366. @dataclass
  367. class Records:
  368. """One layer's verify inputs for up to ``rows`` rows of ``q`` tokens.
  369. Token-major so the stock recurrent kernel reads them as q/k/v, g, beta:
  370. ``kv[t, 0]`` is k and ``kv[t, 1]`` is v for record token ``t``.
  371. """
  372. kv: torch.Tensor # [rows * q, 2, heads, head_dim]
  373. g: torch.Tensor # [rows * q, heads, head_dim]
  374. beta: torch.Tensor # [rows * q, heads]
  375. rows: int
  376. q: int
  377. def allocate_records(
  378. num_rows: int,
  379. num_heads: int,
  380. head_dim: int,
  381. spec_query_len: int,
  382. dtype: torch.dtype,
  383. device: torch.device,
  384. ) -> Records:
  385. tokens = num_rows * spec_query_len
  386. return Records(
  387. kv=torch.empty((tokens, 2, num_heads, head_dim), dtype=dtype, device=device),
  388. g=torch.empty((tokens, num_heads, head_dim), dtype=dtype, device=device),
  389. beta=torch.empty((tokens, num_heads), dtype=dtype, device=device),
  390. rows=num_rows,
  391. q=spec_query_len,
  392. )
  393. def kda_recoverssm_verify(
  394. q: torch.Tensor,
  395. k: torch.Tensor,
  396. v: torch.Tensor,
  397. g: torch.Tensor,
  398. beta: torch.Tensor,
  399. a_log: torch.Tensor,
  400. g_bias: torch.Tensor,
  401. lower_bound: float,
  402. checkpoint_state: torch.Tensor,
  403. records: Records,
  404. query_start_loc: torch.Tensor,
  405. state_indices: torch.Tensor,
  406. out: torch.Tensor | None = None,
  407. ) -> torch.Tensor:
  408. """Verify a speculative window from each row's checkpoint without writing it.
  409. Takes what kda.py passes fused_recurrent_kda on its spec path (raw gate
  410. logits and raw beta), with ``state_indices`` holding one checkpoint block
  411. per row. Writes the outputs and fills ``records`` for the commit.
  412. """
  413. _, total_tokens, num_heads, key_dim = k.shape
  414. value_dim = v.shape[-1]
  415. num_rows = state_indices.shape[0]
  416. if query_start_loc.shape[0] != num_rows + 1:
  417. raise ValueError("KDA RecoverSSM query metadata is incompatible")
  418. if num_rows > records.rows:
  419. raise ValueError("KDA RecoverSSM batch exceeds its record rows")
  420. if records.kv.shape[1:] != (2, num_heads, key_dim) or value_dim != key_dim:
  421. raise ValueError("KDA RecoverSSM records do not match the layer")
  422. if records.kv.dtype != k.dtype or records.beta.dtype != beta.dtype:
  423. raise ValueError("KDA RecoverSSM records must use the activation dtype")
  424. # No token-count check: FULL graphs pad the token count, and the builder
  425. # already routes rows longer than the record window to the prefill path.
  426. if out is None:
  427. out = torch.empty(k.shape, dtype=k.dtype, device=k.device)
  428. else:
  429. assert out.shape == k.shape and out.dtype == k.dtype
  430. assert out.is_contiguous()
  431. if total_tokens == 0 or num_rows == 0:
  432. return out
  433. g = g.contiguous()
  434. BK = triton.next_power_of_2(key_dim)
  435. BV = min(triton.next_power_of_2(value_dim), _BV)
  436. assert BK == key_dim, "KDA RecoverSSM needs a power-of-two head dim"
  437. grid = (1, triton.cdiv(value_dim, BV), num_rows * num_heads)
  438. _verify_kernel[grid](
  439. q,
  440. k,
  441. v,
  442. g,
  443. beta,
  444. out,
  445. checkpoint_state,
  446. records.kv,
  447. records.g,
  448. records.beta,
  449. query_start_loc,
  450. state_indices,
  451. a_log.reshape(-1).contiguous(),
  452. g_bias.reshape(-1).contiguous(),
  453. key_dim**-0.5,
  454. _token_stride(q),
  455. _token_stride(k),
  456. _token_stride(v),
  457. _token_stride(beta),
  458. state_indices.stride(0),
  459. stride_state=checkpoint_state.stride(0),
  460. H=num_heads,
  461. K=key_dim,
  462. V=value_dim,
  463. Q=records.q,
  464. BK=BK,
  465. BV=BV,
  466. LOWER_BOUND=lower_bound,
  467. num_warps=_NUM_WARPS,
  468. num_stages=_NUM_STAGES,
  469. )
  470. return out
  471. @dataclass
  472. class Glm5NextRecoverSSMCommitContext:
  473. """Per-KV-group commit state: every layer's pages, records and gate params."""
  474. conv_states: tuple[torch.Tensor, ...]
  475. conv_state_base_addrs: torch.Tensor
  476. conv_state_block_strides: torch.Tensor
  477. conv_state_dim_strides: torch.Tensor
  478. conv_state_token_strides: torch.Tensor
  479. conv_history_len: int
  480. layers: tuple[Any, ...]
  481. lower_bound: float
  482. spec_query_len: int
  483. commit_lens: torch.Tensor
  484. final_state_indices: torch.Tensor
  485. boundary_state_indices: torch.Tensor
  486. boundary_recovery_lens: torch.Tensor
  487. replay_indices: torch.Tensor
  488. replay_query_start_loc: torch.Tensor
  489. replay_num_accepted: torch.Tensor
  490. replay_out: torch.Tensor
  491. @classmethod
  492. def create(
  493. cls, layers: list[Any], *, spec_query_len: int, max_num_reqs: int
  494. ) -> "Glm5NextRecoverSSMCommitContext":
  495. """``layers``: KDA layers with ``kv_cache`` (conv, state), ``A_log``,
  496. ``dt_bias``, ``kda_lower_bound`` and ``recoverssm_records``."""
  497. if not layers:
  498. raise ValueError("KDA RecoverSSM commit requires at least one layer")
  499. conv_states = [layer.kv_cache[0] for layer in layers]
  500. if not is_conv_state_dim_first():
  501. conv_states = [state.transpose(-1, -2) for state in conv_states]
  502. checkpoints = [layer.kv_cache[1] for layer in layers]
  503. lower_bounds = {layer.kda_lower_bound for layer in layers}
  504. if len(lower_bounds) != 1:
  505. raise ValueError("KDA RecoverSSM layers need matching gate bounds")
  506. state_ref = checkpoints[0]
  507. _, num_heads, value_dim, key_dim = state_ref.shape
  508. for state in checkpoints:
  509. if (
  510. state.shape[1:] != state_ref.shape[1:]
  511. or state.dtype != state_ref.dtype
  512. or state.stride()[1:] != (value_dim * key_dim, key_dim, 1)
  513. ):
  514. raise ValueError("KDA RecoverSSM layers need matching state layout")
  515. record_rows = layers[0].recoverssm_records.rows
  516. for layer in layers:
  517. records = layer.recoverssm_records
  518. if records.q != spec_query_len or records.rows != record_rows:
  519. raise ValueError("KDA RecoverSSM layers need matching records")
  520. if records.rows < max_num_reqs:
  521. raise ValueError("KDA RecoverSSM records do not cover max_num_seqs")
  522. if records.kv.shape[1:] != (2, num_heads, key_dim):
  523. raise ValueError("KDA RecoverSSM records do not match the state")
  524. conv_ref = conv_states[0]
  525. conv_history_len = conv_ref.shape[2] - spec_query_len + 1
  526. if conv_history_len <= 0:
  527. raise ValueError("KDA RecoverSSM conv state is shorter than its window")
  528. for conv_state in conv_states:
  529. if conv_state.shape[1:] != conv_ref.shape[1:] or (
  530. conv_state.dtype != conv_ref.dtype
  531. ):
  532. raise ValueError("KDA RecoverSSM layers need matching conv state")
  533. device = state_ref.device
  534. rows = max_num_reqs
  535. def _i64(values: list[int]) -> torch.Tensor:
  536. return torch.tensor(values, dtype=torch.int64, device=device)
  537. def _i32(n: int) -> torch.Tensor:
  538. return torch.empty(n, dtype=torch.int32, device=device)
  539. return cls(
  540. conv_states=tuple(conv_states),
  541. conv_state_base_addrs=_i64([s.data_ptr() for s in conv_states]),
  542. conv_state_block_strides=_i64([s.stride(0) for s in conv_states]),
  543. conv_state_dim_strides=_i64([s.stride(1) for s in conv_states]),
  544. conv_state_token_strides=_i64([s.stride(2) for s in conv_states]),
  545. conv_history_len=conv_history_len,
  546. layers=tuple(layers),
  547. lower_bound=lower_bounds.pop(),
  548. spec_query_len=spec_query_len,
  549. commit_lens=_i32(rows),
  550. final_state_indices=_i32(rows),
  551. boundary_state_indices=_i32(rows),
  552. boundary_recovery_lens=_i32(rows),
  553. replay_indices=torch.zeros(
  554. (rows, spec_query_len + 1), dtype=torch.int32, device=device
  555. ),
  556. replay_query_start_loc=torch.arange(
  557. 0,
  558. (rows + 1) * spec_query_len,
  559. spec_query_len,
  560. dtype=torch.int32,
  561. device=device,
  562. ),
  563. replay_num_accepted=torch.full(
  564. (rows,), spec_query_len + 1, dtype=torch.int32, device=device
  565. ),
  566. # Shaped like the records' k, as the stock kernel requires.
  567. replay_out=torch.empty(
  568. (1, record_rows * spec_query_len, num_heads, value_dim),
  569. dtype=layers[0].recoverssm_records.kv.dtype,
  570. device=device,
  571. ),
  572. )
  573. def commit(
  574. self,
  575. num_accepted_tokens: torch.Tensor,
  576. state_indices: torch.Tensor,
  577. query_start_loc: torch.Tensor,
  578. request_indices: torch.Tensor | None = None,
  579. block_table: torch.Tensor | None = None,
  580. num_computed_tokens: torch.Tensor | None = None,
  581. mamba_block_size: int | None = None,
  582. ) -> None:
  583. """Fold each row's accepted tokens into its conv and recurrent state.
  584. ``num_accepted_tokens`` is per batch row (the runner's num_sampled);
  585. ``request_indices`` maps spec rows to batch rows when they differ.
  586. """
  587. batch = state_indices.shape[0]
  588. if batch == 0:
  589. return
  590. if batch > self.commit_lens.shape[0]:
  591. raise ValueError("KDA RecoverSSM commit batch exceeds its plan capacity")
  592. if query_start_loc.shape[0] != batch + 1:
  593. raise ValueError("KDA RecoverSSM commit metadata is incompatible")
  594. align_args = (block_table, num_computed_tokens, mamba_block_size)
  595. if any(a is not None for a in align_args) and any(
  596. a is None for a in align_args
  597. ):
  598. raise ValueError("KDA RecoverSSM align metadata is incomplete")
  599. if mamba_block_size is not None and mamba_block_size < self.spec_query_len:
  600. raise ValueError(
  601. "KDA RecoverSSM align block size must cover one speculative window"
  602. )
  603. align = block_table is not None
  604. block_table_stride = block_table.stride() if align else (0, 0)
  605. _prepare_commit_plan_kernel[(batch,)](
  606. num_accepted_tokens,
  607. request_indices,
  608. state_indices,
  609. query_start_loc,
  610. block_table,
  611. num_computed_tokens,
  612. self.commit_lens,
  613. self.final_state_indices,
  614. self.boundary_state_indices,
  615. self.boundary_recovery_lens,
  616. NULL_BLOCK_ID,
  617. mamba_block_size or 1,
  618. block_table.shape[1] if align else 1,
  619. num_accepted_tokens.stride(0),
  620. request_indices.stride(0) if request_indices is not None else 0,
  621. state_indices.stride(0),
  622. query_start_loc.stride(0),
  623. block_table_stride[0],
  624. block_table_stride[1],
  625. num_computed_tokens.stride(0) if align else 0,
  626. SPEC_QUERY_LEN=self.spec_query_len,
  627. num_warps=1,
  628. )
  629. num_layers = len(self.layers)
  630. conv_dim = self.conv_states[0].shape[1]
  631. _compact_conv_state_kernel[(triton.cdiv(conv_dim, 256), batch, num_layers)](
  632. self.conv_states[0],
  633. self.conv_state_base_addrs,
  634. self.conv_state_block_strides,
  635. self.conv_state_dim_strides,
  636. self.conv_state_token_strides,
  637. state_indices,
  638. self.commit_lens,
  639. self.final_state_indices,
  640. self.boundary_state_indices,
  641. self.boundary_recovery_lens,
  642. NULL_BLOCK_ID,
  643. conv_dim,
  644. self.conv_history_len,
  645. state_indices.stride(0),
  646. BLOCK_D=256,
  647. BLOCK_HISTORY=triton.next_power_of_2(self.conv_history_len),
  648. ALIGN_MODE=align,
  649. num_warps=4,
  650. )
  651. q_len = self.spec_query_len
  652. _replay_indices_kernel[(batch,)](
  653. state_indices,
  654. self.commit_lens,
  655. self.final_state_indices,
  656. self.boundary_state_indices,
  657. self.boundary_recovery_lens,
  658. self.replay_indices,
  659. NULL_BLOCK_ID,
  660. state_indices.stride(0),
  661. Q=q_len,
  662. BLOCK=triton.next_power_of_2(q_len + 1),
  663. num_warps=1,
  664. )
  665. # The stock kernel runs all Q record tokens of each row and writes a
  666. # state only to the columns set above. Same kernel, same inputs, so
  667. # those states match the stock spec path's bit for bit.
  668. cu_seqlens = self.replay_query_start_loc[: batch + 1]
  669. indices = self.replay_indices[:batch]
  670. num_accepted = self.replay_num_accepted[:batch]
  671. for layer in self.layers:
  672. records = layer.recoverssm_records
  673. k = records.kv[:, 0].unsqueeze(0)
  674. fused_recurrent_kda(
  675. q=k,
  676. k=k,
  677. v=records.kv[:, 1].unsqueeze(0),
  678. g=records.g.unsqueeze(0),
  679. beta=records.beta.unsqueeze(0),
  680. initial_state=layer.kv_cache[1],
  681. use_qk_l2norm_in_kernel=True,
  682. cu_seqlens=cu_seqlens,
  683. ssm_state_indices=indices,
  684. num_accepted_tokens=num_accepted,
  685. out=self.replay_out,
  686. sigmoid_beta=True,
  687. a_log=layer.A_log,
  688. g_bias=layer.dt_bias,
  689. compute_gate=True,
  690. lower_bound=self.lower_bound,
  691. )
  692. @dataclass
  693. class _AlignCommit:
  694. block_table: torch.Tensor
  695. num_computed_tokens: torch.Tensor
  696. block_size: int
  697. @dataclass
  698. class _Commit:
  699. state_indices: torch.Tensor
  700. query_start_loc: torch.Tensor
  701. request_indices: torch.Tensor | None
  702. align: _AlignCommit | None
  703. @dataclass
  704. class Glm5NextRecoverSSMMetadata(GDNAttentionMetadata, RecoverSSMMetadata):
  705. recoverssm_commit: _Commit | None = None
  706. recoverssm_context: Glm5NextRecoverSSMCommitContext | None = field(
  707. default=None, repr=False, compare=False
  708. )
  709. def commit_recoverssm_state(
  710. self, num_accepted_tokens: torch.Tensor
  711. ) -> RecoverSSMPostprocessMetadata | None:
  712. commit = self.recoverssm_commit
  713. if commit is None:
  714. return None
  715. assert self.recoverssm_context is not None
  716. align = commit.align
  717. self.recoverssm_context.commit(
  718. num_accepted_tokens,
  719. commit.state_indices[: self.num_spec_decodes, 0],
  720. commit.query_start_loc[: self.num_spec_decodes + 1],
  721. request_indices=commit.request_indices,
  722. block_table=align.block_table if align is not None else None,
  723. num_computed_tokens=(
  724. align.num_computed_tokens if align is not None else None
  725. ),
  726. mamba_block_size=align.block_size if align is not None else None,
  727. )
  728. if align is None:
  729. return None
  730. return RecoverSSMPostprocessMetadata(
  731. num_spec_decodes=self.num_spec_decodes,
  732. request_indices=commit.request_indices,
  733. block_table=align.block_table,
  734. num_computed_tokens=align.num_computed_tokens,
  735. block_size=align.block_size,
  736. )
  737. def _check_config(vllm_config: VllmConfig) -> None:
  738. """Fail at startup where the commit would otherwise be skipped silently."""
  739. from vllm.models.glm5next.common import model as glm_model
  740. if not getattr(glm_model, "_RECOVERSSM", False):
  741. raise RuntimeError(
  742. "VLLM_GLM5NEXT_RECOVERSSM=1 needs experimental/fixes/model.py "
  743. "mounted (recoverssm.yaml or sp.yaml): it installs the commit hook"
  744. )
  745. if not vllm_config.use_v2_model_runner:
  746. raise RuntimeError("KDA RecoverSSM commits from the V2 model runner only")
  747. if vllm_config.parallel_config.pipeline_parallel_size > 1:
  748. raise RuntimeError("KDA RecoverSSM requires pipeline_parallel_size=1")
  749. if vllm_config.cache_config.mamba_cache_mode not in ("none", "align"):
  750. raise RuntimeError("KDA RecoverSSM supports mamba cache modes none and align")
  751. class Glm5NextRecoverSSMMetadataBuilder(GDNAttentionMetadataBuilder):
  752. """GDN metadata for GLM's KDA with one state slot per spec-decode row.
  753. Every running decode row goes down the spec path, drafts or not, so the
  754. conv state is always in the compacted form the commit leaves. Steps
  755. without such rows use the GDN builder unchanged.
  756. """
  757. def __init__(
  758. self,
  759. kv_cache_spec: MambaSpec,
  760. layer_names: list[str],
  761. vllm_config: VllmConfig,
  762. device: torch.device,
  763. ) -> None:
  764. super().__init__(kv_cache_spec, layer_names, vllm_config, device)
  765. _check_config(vllm_config)
  766. assert self.use_spec_decode, "KDA RecoverSSM needs speculative decoding"
  767. self.spec_query_len = self.num_spec + 1
  768. self.max_num_reqs = vllm_config.scheduler_config.max_num_seqs
  769. rows = max(self.decode_cudagraph_max_bs, self.max_num_reqs)
  770. self.rs_state_indices = torch.empty((rows, 1), dtype=torch.int32, device=device)
  771. # The conv kernel reads the window at num_accepted - 1; the commit
  772. # already moved it to column 0.
  773. self.rs_ones = torch.ones(rows, dtype=torch.int32, device=device)
  774. self.recoverssm_context: Glm5NextRecoverSSMCommitContext | None = None
  775. # Allocated here, before any capture: the eager KDA segment must not
  776. # allocate during a graph capture.
  777. layers = vllm_config.compilation_config.static_forward_context
  778. for name in layer_names:
  779. layer = layers[name]
  780. if getattr(layer, "recoverssm_records", None) is None:
  781. layer.recoverssm_records = allocate_records(
  782. self.max_num_reqs,
  783. layer.local_num_heads,
  784. layer.head_dim,
  785. self.spec_query_len,
  786. vllm_config.model_config.dtype,
  787. device,
  788. )
  789. def _get_context(self) -> Glm5NextRecoverSSMCommitContext:
  790. if self.recoverssm_context is None:
  791. layers = self.vllm_config.compilation_config.static_forward_context
  792. self.recoverssm_context = Glm5NextRecoverSSMCommitContext.create(
  793. [layers[name] for name in self.layer_names],
  794. spec_query_len=self.spec_query_len,
  795. max_num_reqs=self.max_num_reqs,
  796. )
  797. return self.recoverssm_context
  798. def build( # type: ignore[override]
  799. self,
  800. common_prefix_len: int,
  801. common_attn_metadata: CommonAttentionMetadata,
  802. num_accepted_tokens: torch.Tensor | None = None,
  803. num_decode_draft_tokens_cpu: torch.Tensor | None = None,
  804. fast_build: bool = False,
  805. ) -> GDNAttentionMetadata:
  806. m = common_attn_metadata
  807. if num_decode_draft_tokens_cpu is None:
  808. return super().build(
  809. common_prefix_len, m, num_accepted_tokens, None, fast_build
  810. )
  811. query_start_loc = m.query_start_loc
  812. query_start_loc_cpu = m.query_start_loc_cpu
  813. query_lens_cpu = query_start_loc_cpu.diff()
  814. spec_mask_cpu = num_decode_draft_tokens_cpu >= 0
  815. if m.is_prefilling is not None:
  816. spec_mask_cpu |= (~m.is_prefilling) & (query_lens_cpu > 0)
  817. # Rows longer than the record window (adaptive probes) run as prefills.
  818. spec_mask_cpu &= query_lens_cpu <= self.spec_query_len
  819. num_spec_decodes = int(spec_mask_cpu.sum())
  820. if num_spec_decodes == 0:
  821. return super().build(
  822. common_prefix_len, m, num_accepted_tokens, None, fast_build
  823. )
  824. block_table = mamba_get_block_table_tensor(
  825. m.block_table_tensor,
  826. m.seq_lens,
  827. self.kv_cache_spec,
  828. self.vllm_config.cache_config.mamba_cache_mode,
  829. )
  830. device = query_start_loc.device
  831. num_query_tokens = int(query_start_loc_cpu[-1])
  832. active_non_spec_cpu = (~spec_mask_cpu) & (query_lens_cpu > 0)
  833. num_prefills = int(active_non_spec_cpu.sum())
  834. num_prefill_tokens = int(query_lens_cpu[active_non_spec_cpu].sum())
  835. num_spec_decode_tokens = num_query_tokens - num_prefill_tokens
  836. request_indices = None
  837. has_initial_state = None
  838. nums_dict = batch_ptr = token_chunk_offset_ptr = None
  839. if num_prefills == 0:
  840. # Real rows precede the cudagraph padding, and all of them verify.
  841. spec_token_indx = non_spec_token_indx = None
  842. spec_state_indices = block_table[:num_spec_decodes, :1]
  843. non_spec_state_indices = None
  844. spec_query_start_loc = query_start_loc[: num_spec_decodes + 1]
  845. non_spec_query_start_loc = None
  846. else:
  847. query_lens = query_start_loc.diff()
  848. spec_mask = async_tensor_h2d(spec_mask_cpu, device=device)
  849. request_indices = async_tensor_h2d(
  850. spec_mask_cpu.nonzero(as_tuple=True)[0],
  851. dtype=torch.int32,
  852. device=device,
  853. )
  854. spec_token_masks = torch.repeat_interleave(
  855. spec_mask, query_lens, output_size=num_query_tokens
  856. )
  857. # Stable, so each row's tokens stay in order in both halves.
  858. index = torch.argsort(spec_token_masks, stable=True)
  859. non_spec_token_indx = index[:num_prefill_tokens]
  860. spec_token_indx = index[num_prefill_tokens:]
  861. spec_state_indices = block_table[spec_mask_cpu, :1]
  862. non_spec_state_indices = block_table[active_non_spec_cpu, 0]
  863. spec_query_start_loc = torch.zeros(
  864. num_spec_decodes + 1, dtype=torch.int32, device=device
  865. )
  866. torch.cumsum(query_lens[spec_mask_cpu], dim=0, out=spec_query_start_loc[1:])
  867. non_spec_query_start_loc = torch.zeros(
  868. num_prefills + 1, dtype=torch.int32, device=device
  869. )
  870. torch.cumsum(
  871. query_lens[active_non_spec_cpu],
  872. dim=0,
  873. out=non_spec_query_start_loc[1:],
  874. )
  875. non_spec_query_start_loc_cpu = torch.zeros(
  876. num_prefills + 1, dtype=torch.int32
  877. )
  878. torch.cumsum(
  879. query_lens_cpu[active_non_spec_cpu],
  880. dim=0,
  881. out=non_spec_query_start_loc_cpu[1:],
  882. )
  883. has_initial_state = (m.compute_num_computed_tokens() > 0)[
  884. active_non_spec_cpu
  885. ]
  886. nums_dict, batch_ptr, token_chunk_offset_ptr = (
  887. compute_causal_conv1d_metadata(
  888. non_spec_query_start_loc_cpu, device=device
  889. )
  890. )
  891. num_accepted = self.rs_ones[:num_spec_decodes]
  892. batch_size = m.num_reqs
  893. if (
  894. self.use_full_cuda_graph
  895. and num_prefills == 0
  896. and batch_size <= self.decode_cudagraph_max_bs
  897. and num_spec_decode_tokens <= self.decode_cudagraph_max_bs
  898. ):
  899. self.rs_state_indices[:num_spec_decodes].copy_(
  900. spec_state_indices, non_blocking=True
  901. )
  902. spec_state_indices = self.rs_state_indices[:batch_size]
  903. spec_state_indices[num_spec_decodes:].fill_(NULL_BLOCK_ID)
  904. self.spec_query_start_loc[: num_spec_decodes + 1].copy_(
  905. spec_query_start_loc, non_blocking=True
  906. )
  907. spec_num_query_tokens = spec_query_start_loc[-1]
  908. spec_query_start_loc = self.spec_query_start_loc[: batch_size + 1]
  909. spec_query_start_loc[num_spec_decodes + 1 :].fill_(spec_num_query_tokens)
  910. num_accepted = self.rs_ones[:batch_size]
  911. align = None
  912. if self.kv_cache_spec.mamba_cache_mode == "align":
  913. align = _AlignCommit(
  914. block_table=m.block_table_tensor,
  915. num_computed_tokens=m.compute_num_computed_tokens(),
  916. block_size=self.kv_cache_spec.block_size,
  917. )
  918. return Glm5NextRecoverSSMMetadata(
  919. num_prefills=num_prefills,
  920. num_prefill_tokens=num_prefill_tokens,
  921. num_decodes=0,
  922. num_decode_tokens=0,
  923. num_spec_decodes=num_spec_decodes,
  924. num_spec_decode_tokens=num_spec_decode_tokens,
  925. num_actual_tokens=m.num_actual_tokens,
  926. has_initial_state=has_initial_state,
  927. spec_query_start_loc=spec_query_start_loc,
  928. non_spec_query_start_loc=non_spec_query_start_loc,
  929. spec_state_indices_tensor=spec_state_indices,
  930. non_spec_state_indices_tensor=non_spec_state_indices,
  931. spec_sequence_masks=None,
  932. spec_token_indx=spec_token_indx,
  933. non_spec_token_indx=non_spec_token_indx,
  934. num_accepted_tokens=num_accepted,
  935. nums_dict=nums_dict,
  936. batch_ptr=batch_ptr,
  937. token_chunk_offset_ptr=token_chunk_offset_ptr,
  938. recoverssm_commit=_Commit(
  939. state_indices=spec_state_indices,
  940. query_start_loc=spec_query_start_loc,
  941. request_indices=request_indices,
  942. align=align,
  943. ),
  944. recoverssm_context=self._get_context(),
  945. )
  946. class Glm5NextRecoverSSMBackend(GDNAttentionBackend):
  947. @staticmethod
  948. def get_name() -> str:
  949. return "GLM5NEXT_KDA_RECOVERSSM"
  950. @staticmethod
  951. def get_builder_cls() -> type[Glm5NextRecoverSSMMetadataBuilder]:
  952. return Glm5NextRecoverSSMMetadataBuilder
  953. @triton.heuristics(
  954. {"HAS_REQUEST_INDICES": lambda args: args["request_indices_ptr"] is not None}
  955. )
  956. @triton.jit
  957. def _record_state_block_kernel(
  958. idx_mapping_ptr,
  959. num_sampled_ptr,
  960. request_indices_ptr,
  961. num_computed_ptr,
  962. state_idx_ptr,
  963. num_accepted_ptr,
  964. HAS_REQUEST_INDICES: tl.constexpr,
  965. MAMBA_BLOCK_SIZE: tl.constexpr,
  966. BLOCK_TABLE_WIDTH: tl.constexpr,
  967. ):
  968. # The runner's _postprocess_recoverssm_align_kernel, except for the column:
  969. # the commit left the state in the block holding the last committed token.
  970. spec_idx = tl.program_id(0)
  971. batch_idx = spec_idx
  972. if HAS_REQUEST_INDICES:
  973. batch_idx = tl.load(request_indices_ptr + spec_idx)
  974. req_state_idx = tl.load(idx_mapping_ptr + batch_idx)
  975. if req_state_idx < 0:
  976. return
  977. num_sampled = tl.load(num_sampled_ptr + batch_idx)
  978. num_computed = tl.load(num_computed_ptr + batch_idx)
  979. last = tl.maximum(num_computed + num_sampled - 1, 0)
  980. tl.store(
  981. state_idx_ptr + req_state_idx,
  982. tl.minimum(last // MAMBA_BLOCK_SIZE, BLOCK_TABLE_WIDTH - 1),
  983. )
  984. tl.store(num_accepted_ptr + req_state_idx, 1)
  985. def record_state_blocks(
  986. meta: RecoverSSMPostprocessMetadata,
  987. idx_mapping: torch.Tensor,
  988. num_sampled: torch.Tensor,
  989. state_indices: torch.Tensor,
  990. num_accepted_tokens: torch.Tensor,
  991. ) -> None:
  992. """Point each committed request's running state column at its state block.
  993. The next step's pre-step copy moves it from there when the step's tokens
  994. pass into the next block.
  995. """
  996. _record_state_block_kernel[(meta.num_spec_decodes,)](
  997. idx_mapping,
  998. num_sampled,
  999. meta.request_indices,
  1000. meta.num_computed_tokens,
  1001. state_indices,
  1002. num_accepted_tokens,
  1003. MAMBA_BLOCK_SIZE=meta.block_size,
  1004. BLOCK_TABLE_WIDTH=meta.block_table.shape[1],
  1005. )
  1006. @cache
  1007. def model_state_cls() -> type:
  1008. """MambaHybridModelState with the runner's RecoverSSM commit hook.
  1009. The stock state creates the hook only when cache_config.use_kda_recoverssm
  1010. is set, which VllmConfig's validator allows for Kimi-K3 alone and resets
  1011. on every re-validation (the DFlash loader re-validates after the target
  1012. model loads). The hook records the state block with record_state_blocks.
  1013. """
  1014. from vllm.v1.worker.gpu.model_states.mamba_hybrid import MambaHybridModelState
  1015. from vllm.v1.worker.gpu.model_states.recoverssm import RecoverSSMState
  1016. class Glm5NextRecoverSSMState(RecoverSSMState):
  1017. def commit_step(
  1018. self,
  1019. num_sampled: torch.Tensor | int,
  1020. idx_mapping: torch.Tensor,
  1021. *,
  1022. state_indices: torch.Tensor | None,
  1023. num_accepted_tokens: torch.Tensor,
  1024. ) -> None:
  1025. step = self._step
  1026. self._step = None
  1027. if isinstance(num_sampled, int) or step is None:
  1028. return
  1029. for metadata in step:
  1030. meta = metadata.commit_recoverssm_state(num_sampled)
  1031. if meta is None:
  1032. continue
  1033. assert state_indices is not None
  1034. record_state_blocks(
  1035. meta, idx_mapping, num_sampled, state_indices, num_accepted_tokens
  1036. )
  1037. class Glm5NextRecoverSSMModelState(MambaHybridModelState):
  1038. def __init__(self, *args, **kwargs) -> None:
  1039. super().__init__(*args, **kwargs)
  1040. self.recoverssm = Glm5NextRecoverSSMState()
  1041. return Glm5NextRecoverSSMModelState