_cudagraph_replay_test.py 10 KB

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