_cudagraph_replay_shapes_test.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/env python3
  2. """T6: FULL-cudagraph replay checks for the two decode kernels whose plan is
  3. built host-side: the SM90 FlashInfer MLA wrapper (_SM90State) and the
  4. DeepGEMM paged MQA logits + per_row top-k of the sparse indexer.
  5. vLLM captures decode graphs with a dummy batch whose seq_len equals the query
  6. length (InputBatch.make_dummy: seq_lens = ceil(num_tokens/num_reqs), positions
  7. zero), then replans every step and replays. This mirrors that: capture with
  8. 8 rows of lens 1..8, replay with real lens, compare against a reference.
  9. """
  10. import sys
  11. import traceback
  12. import torch
  13. torch.manual_seed(0)
  14. dev = torch.device("cuda")
  15. results = {}
  16. def section(name):
  17. def deco(fn):
  18. def run():
  19. print(f"\n===== {name} =====", flush=True)
  20. try:
  21. fn()
  22. results[name] = "PASS"
  23. except Exception as e: # noqa: BLE001
  24. results[name] = f"FAIL: {e}"
  25. traceback.print_exc()
  26. return run
  27. return deco
  28. def t6a_impl(R, cap_lens, replays):
  29. from vllm import _custom_ops as ops
  30. from vllm.v1.attention.backends.mla.flashinfer_mla_sparse_sm90 import _SM90State
  31. heads, d = 16, 512
  32. sm_scale = 1.0 / 16.0
  33. topk_width = 2176
  34. max_tokens = 256
  35. N = 2300
  36. bs = 64
  37. num_blocks = N // bs + 3
  38. kv_c = torch.randn(N, d, device=dev, dtype=torch.bfloat16)
  39. k_pe = torch.empty(N, 0, device=dev, dtype=torch.bfloat16)
  40. kv_cache = torch.zeros(num_blocks, bs, d, device=dev, dtype=torch.uint8)
  41. slots = (torch.arange(N, device=dev) + 37).to(torch.int64)
  42. ops.concat_and_cache_mla(kv_c, k_pe, kv_cache, slots, "fp8_e4m3", torch.tensor(1.0, device=dev))
  43. deq = kv_cache.view(torch.float8_e4m3fn).reshape(-1, d).float()
  44. flat = kv_cache.view(torch.float8_e4m3fn).reshape(-1, 1, d)
  45. ckv, kpe = flat[..., :d], flat[..., d:]
  46. state = _SM90State(dev, heads, torch.float8_e4m3fn, max_tokens, topk_width,
  47. kv_lora_rank=d, qk_rope_head_dim=0, sm_scale=sm_scale)
  48. # static input buffers (what the captured graph reads)
  49. q_nope = torch.randn(R, heads, d, device=dev, dtype=torch.bfloat16)
  50. q_pe = torch.empty(R, heads, 0, device=dev, dtype=torch.bfloat16)
  51. out_buf = torch.empty(R, heads, d, device=dev, dtype=torch.bfloat16)
  52. def set_rows(lens):
  53. state.kv_indices.fill_(0)
  54. for r, L in enumerate(lens):
  55. state.kv_indices[r * topk_width: r * topk_width + L] = slots[:L].to(torch.int32)
  56. state.plan(R, torch.tensor(lens, dtype=torch.int32))
  57. def reference(lens):
  58. ref = torch.empty(R, heads, d, device=dev)
  59. for r, L in enumerate(lens):
  60. K = deq[slots[:L]]
  61. s = (q_nope[r].float() @ K.T) * sm_scale
  62. ref[r] = torch.softmax(s, dim=-1) @ K
  63. return ref
  64. def run_once():
  65. out = state.wrapper.run(q_nope, q_pe, ckv, kpe, ckv_scale=1.0, kpe_scale=1.0)
  66. out_buf.copy_(out)
  67. # --- capture with the dummy lens vLLM uses ---
  68. set_rows(cap_lens)
  69. s = torch.cuda.Stream()
  70. s.wait_stream(torch.cuda.current_stream())
  71. with torch.cuda.stream(s):
  72. for _ in range(2):
  73. run_once()
  74. torch.cuda.current_stream().wait_stream(s)
  75. graph = torch.cuda.CUDAGraph()
  76. with torch.cuda.graph(graph):
  77. run_once()
  78. torch.cuda.synchronize()
  79. worst = 0.0
  80. for lens in replays:
  81. q_nope.copy_(torch.randn_like(q_nope))
  82. set_rows(lens)
  83. graph.replay()
  84. torch.cuda.synchronize()
  85. ref = reference(lens)
  86. err = (out_buf.float() - ref).abs()
  87. rel = err.max().item() / max(ref.abs().max().item(), 1e-6)
  88. row_err = [round(v, 4) for v in err.flatten(1).max(dim=1).values.tolist()]
  89. nan = torch.isnan(out_buf).any().item()
  90. print(f" replay lens={lens[0]}..{lens[-1]}: rel err {rel:.4g} nan={nan} per-row {row_err}")
  91. worst = max(worst, rel)
  92. assert not nan
  93. print(f"T6a R={R} cap={cap_lens} worst replay rel err {worst:.4g}")
  94. assert worst < 0.1
  95. @section("T6a SM90 MLA wrapper: capture at dummy lens, replay at real lens")
  96. def t6a():
  97. t6a_impl(8, list(range(1, 9)), [list(range(1, 9)), list(range(100, 108)), list(range(693, 701)), list(range(2044, 2052))])
  98. # spec-off production shape: one request, one token, dummy seq_len 1
  99. t6a_impl(1, [1], [[1], [100], [700], [2051], [100], [37], [2600 // 4 * 4 + 3], [5]])
  100. # MTP k=3 shape: one request, 4 tokens, dummy seq_lens 1..4
  101. t6a_impl(4, [1, 2, 3, 4], [[1, 2, 3, 4], [100, 101, 102, 103], [700, 701, 702, 703], [2049, 2050, 2051, 2052], [40, 41, 42, 43]])
  102. @section("T6b indexer paged MQA logits + per_row topk: capture short, replay long")
  103. def t6b():
  104. from vllm.utils.deep_gemm import fp8_fp4_paged_mqa_logits, get_paged_mqa_logits_metadata
  105. from vllm.models.glm5next.nvidia.ops import kpool_compress as kpool_ops
  106. from vllm import _custom_ops as ops
  107. from vllm.utils.platform_utils import num_compute_units
  108. # Indexer cache: [num_blocks, pool_block_size, 1, head_dim+4] uint8 (fp8 + fp32 scale)
  109. head_dim = 128
  110. kpool = 4
  111. index_heads = 32 # index_n_heads
  112. pool_bs = 64 # production indexer kernel page: 64 pools = 256 tokens
  113. num_blocks = 80
  114. max_pool_len = 524288 // kpool
  115. fp8 = torch.float8_e4m3fn
  116. k_pool = torch.randn(num_blocks * pool_bs, head_dim, device=dev).clamp(-4, 4)
  117. kv_cache = torch.zeros(num_blocks, pool_bs, 1, head_dim + 4, device=dev, dtype=torch.uint8)
  118. kv_cache[..., :head_dim] = k_pool.to(fp8).view(torch.uint8).reshape(num_blocks, pool_bs, 1, head_dim)
  119. kv_cache[..., head_dim:] = torch.ones(num_blocks * pool_bs, 1, 1, device=dev).view(torch.uint8).reshape(num_blocks, pool_bs, 1, 4)
  120. num_sms = num_compute_units(dev.index if dev.index is not None else 0)
  121. R = 8
  122. # per-token expanded block table (flattened path): every row -> request 0's blocks [1..num_blocks-1]
  123. block_table = torch.zeros(R, num_blocks, device=dev, dtype=torch.int32)
  124. block_table[:, : num_blocks - 1] = torch.arange(1, num_blocks, device=dev, dtype=torch.int32)
  125. q = torch.randn(R, 1, index_heads, head_dim, device=dev).to(fp8)
  126. weights = torch.rand(R, index_heads, device=dev, dtype=torch.float32)
  127. seq_lens = torch.zeros(R, 1, device=dev, dtype=torch.int32) # pool-granular, refreshed
  128. sched_buf = torch.empty((num_sms + 1, 2), dtype=torch.int32, device=dev)
  129. select_k = 2048 // kpool
  130. pool_topk = torch.full((R, select_k), -1, dtype=torch.int32, device=dev)
  131. logits_holder = {}
  132. def set_lens(pool_lens):
  133. seq_lens[:, 0] = torch.tensor(pool_lens, device=dev, dtype=torch.int32)
  134. meta = get_paged_mqa_logits_metadata(seq_lens, pool_bs, num_sms)
  135. sched = sched_buf[: meta.shape[0]]
  136. sched[:] = meta
  137. return sched
  138. def run_once(sched):
  139. logits = fp8_fp4_paged_mqa_logits((q, None), kv_cache, weights, seq_lens, block_table,
  140. sched, max_model_len=max_pool_len, clean_logits=False)
  141. logits_holder["l"] = logits
  142. pool_topk.fill_(-1)
  143. ops.top_k_per_row_decode(logits, 1, seq_lens, pool_topk, R, logits.stride(0), logits.stride(1), select_k)
  144. def reference(pool_lens):
  145. # logits[r, p] = sum_h w[r,h] * relu(q[r,h] . k[p]) (DeepSeek indexer semantics)
  146. out = []
  147. for r, L in enumerate(pool_lens):
  148. blocks = block_table[r, : (L + pool_bs - 1) // pool_bs]
  149. K = k_pool.view(num_blocks, pool_bs, head_dim)[blocks.long()].reshape(-1, head_dim)[:L]
  150. Kq = K.to(fp8).float()
  151. s = torch.relu(q[r, 0].float() @ Kq.T) # [heads, L]
  152. out.append((weights[r][:, None] * s).sum(0))
  153. return out
  154. cap_lens = [max(1, (i + 1) // kpool) for i in range(1, R + 1)] # dummy: seq_len = query_len
  155. sched = set_lens(cap_lens)
  156. s = torch.cuda.Stream()
  157. s.wait_stream(torch.cuda.current_stream())
  158. with torch.cuda.stream(s):
  159. for _ in range(2):
  160. run_once(sched)
  161. torch.cuda.current_stream().wait_stream(s)
  162. graph = torch.cuda.CUDAGraph()
  163. with torch.cuda.graph(graph):
  164. run_once(sched)
  165. torch.cuda.synchronize()
  166. bad = 0
  167. for tok_lens in (list(range(100, 108)), list(range(693, 701)), list(range(1500, 1508)), list(range(2600, 2608)), list(range(4600, 4608))):
  168. pool_lens = [t // kpool for t in tok_lens]
  169. # the schedule buffer is refreshed in place; its slot count must be stable
  170. meta = get_paged_mqa_logits_metadata(seq_lens.new_tensor([[p] for p in pool_lens]), pool_bs, num_sms)
  171. assert meta.shape[0] == sched.shape[0], f"schedule slot count changed {meta.shape} vs {sched.shape}"
  172. set_lens(pool_lens)
  173. graph.replay()
  174. torch.cuda.synchronize()
  175. logits = logits_holder["l"]
  176. refs = reference(pool_lens)
  177. for r, L in enumerate(pool_lens):
  178. got = logits[r, :L].float()
  179. ref = refs[r]
  180. nan = torch.isnan(got).any().item()
  181. err = (got - ref).abs().max().item() / max(ref.abs().max().item(), 1e-6)
  182. sel = pool_topk[r][pool_topk[r] >= 0].tolist()
  183. if L <= select_k:
  184. ok_sel = set(sel) == set(range(L)) and len(sel) == L
  185. else:
  186. ok_sel = set(sel) == set(ref.topk(select_k).indices.tolist())
  187. if nan or err > 0.05 or not ok_sel:
  188. bad += 1
  189. miss = sorted(set(range(min(L, select_k))) - set(sel))[:8] if L <= select_k else []
  190. print(f" BAD replay tok_len={tok_lens[r]} pools={L}: nan={nan} rel_err={err:.3g} "
  191. f"sel_ok={ok_sel} nsel={len(sel)} missing_pools={miss}")
  192. print(f" replay tok_lens {tok_lens[0]}..{tok_lens[-1]}: checked")
  193. # eager comparison
  194. pool_lens = [t // kpool for t in range(693, 701)]
  195. sched = set_lens(pool_lens)
  196. run_once(sched)
  197. torch.cuda.synchronize()
  198. refs = reference(pool_lens)
  199. e = max(((logits_holder["l"][r, :L].float() - refs[r]).abs().max() / refs[r].abs().max()).item() for r, L in enumerate(pool_lens))
  200. print(f" eager 693..700 rel err {e:.3g}")
  201. print(f"T6b bad rows: {bad}")
  202. assert bad == 0
  203. for fn in (t6a,):
  204. fn()
  205. print("\n===== SUMMARY =====")
  206. for k, v in results.items():
  207. print(f"{k}: {v}")
  208. sys.exit(0 if all(v == "PASS" for v in results.values()) else 1)