_topk_mhcpost_fp8mla_test.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. #!/usr/bin/env python3
  2. """Kernel-level checks for the GLM-5.3-Flash decode path on GB10 (spark-glm53:v4).
  3. T1: sparse-indexer decode top-k (per_row) + expand_pools_and_append_tail on
  4. short rows: must return exactly the causal set for every row.
  5. T3: mhc_post_tilelang must not write into any of its inputs, and must match
  6. the torch reference.
  7. T5: FlashInfer SM90 MLA wrapper with an fp8 KV cache (planned as
  8. float8_e4m3fn, as patches/sm90_fp8_kv_dtype.py does) against a bf16
  9. reference, through vLLM's own _SM90State and concat_and_cache_mla.
  10. """
  11. import sys
  12. import traceback
  13. import torch
  14. torch.manual_seed(0)
  15. dev = torch.device("cuda")
  16. results = {}
  17. def section(name):
  18. def deco(fn):
  19. def run():
  20. print(f"\n===== {name} =====", flush=True)
  21. try:
  22. fn()
  23. results[name] = "PASS"
  24. except Exception as e: # noqa: BLE001
  25. results[name] = f"FAIL: {e}"
  26. traceback.print_exc()
  27. return run
  28. return deco
  29. @section("T1 per_row topk + expand (short rows)")
  30. def t1():
  31. from vllm import _custom_ops as ops
  32. from vllm.models.glm5next.nvidia.ops import kpool_compress as kpool_ops
  33. from vllm.model_executor.layers.indexer_topk import SparseIndexerTopk
  34. max_pool_len = 524288 // 4
  35. kpool = 4
  36. select_k = 2048 // kpool
  37. bad = 0
  38. for garbage in ("neg", "pos", "nan", "inf"):
  39. for trial in range(3):
  40. rows = 8
  41. base = int(torch.randint(20, 3000, (1,)))
  42. lens_tok = torch.arange(base, base + rows, dtype=torch.int32)
  43. pool_lens = lens_tok // kpool
  44. fill = {"neg": -1e4, "pos": 1e4, "nan": float("nan"), "inf": float("inf")}[garbage]
  45. logits = torch.full((rows, max_pool_len), fill, device=dev, dtype=torch.float32)
  46. for r in range(rows):
  47. n = int(pool_lens[r])
  48. logits[r, :n] = torch.randn(n, device=dev)
  49. seq_lens = pool_lens.to(torch.int32).to(dev).view(rows, 1)
  50. out = torch.full((rows, select_k), -1, dtype=torch.int32, device=dev)
  51. ops.top_k_per_row_decode(
  52. logits, 1, seq_lens, out, rows, logits.stride(0), logits.stride(1), select_k
  53. )
  54. torch.cuda.synchronize()
  55. # reference
  56. for r in range(rows):
  57. n = int(pool_lens[r])
  58. got = out[r][out[r] >= 0].tolist()
  59. if n <= select_k:
  60. exp = set(range(n))
  61. else:
  62. exp = set(logits[r, :n].topk(select_k).indices.tolist())
  63. if set(got) != exp or len(got) != len(set(got)):
  64. bad += 1
  65. print(f" topk MISMATCH garbage={garbage} row={r} len_tok={int(lens_tok[r])} pools={n}: "
  66. f"got {len(got)} ({len(set(got))} uniq), missing={sorted(exp - set(got))[:10]}, "
  67. f"extra={sorted(set(got) - exp)[:10]}")
  68. expanded = kpool_ops.expand_pools_and_append_tail(out.to(torch.int64), lens_tok.to(dev), kpool)
  69. torch.cuda.synchronize()
  70. for r in range(rows):
  71. L = int(lens_tok[r])
  72. got = expanded[r][expanded[r] >= 0].tolist()
  73. if L <= 2048:
  74. exp = set(range(L))
  75. if set(got) != exp or len(got) != L:
  76. bad += 1
  77. print(f" expand MISMATCH garbage={garbage} row={r} L={L}: got {len(got)} "
  78. f"missing={sorted(exp - set(got))[:10]} extra={sorted(set(got) - exp)[:10]}")
  79. else:
  80. if len(got) != 2048 + (L % kpool) or max(got) >= L or len(set(got)) != len(got):
  81. bad += 1
  82. print(f" expand MISMATCH(long) row={r} L={L}: n={len(got)} max={max(got)}")
  83. # Also drive the module path production uses.
  84. mod = SparseIndexerTopk("per_row")
  85. rows = 16
  86. lens_tok = torch.randint(5, 900, (rows,), dtype=torch.int32)
  87. pool_lens = (lens_tok // kpool)
  88. logits = torch.full((rows, max_pool_len), 1e4, device=dev)
  89. for r in range(rows):
  90. logits[r, : int(pool_lens[r])] = torch.randn(int(pool_lens[r]), device=dev)
  91. out = torch.full((rows, select_k), -1, dtype=torch.int32, device=dev)
  92. mod(logits, pool_lens.to(dev).view(rows, 1), 1, out, select_k, int(lens_tok.max()))
  93. torch.cuda.synchronize()
  94. for r in range(rows):
  95. n = int(pool_lens[r])
  96. got = out[r][out[r] >= 0].tolist()
  97. if set(got) != set(range(n)) or len(got) != n:
  98. bad += 1
  99. print(f" module MISMATCH row={r} pools={n}: got {len(got)}")
  100. print(f"T1 mismatches: {bad}")
  101. assert bad == 0
  102. @section("T3 mhc_post_tilelang purity + reference")
  103. def t3():
  104. from vllm.model_executor.kernels.mhc.torch import mhc_post_torch
  105. T, n, H = 24, 4, 4096
  106. worst = 0.0
  107. for pdtype in (torch.float32, torch.bfloat16):
  108. x = torch.randn(T, H, device=dev, dtype=torch.bfloat16)
  109. residual = torch.randn(T, n, H, device=dev, dtype=torch.bfloat16)
  110. post = torch.randn(T, n, 1, device=dev, dtype=pdtype)
  111. comb = torch.randn(T, n, n, device=dev, dtype=pdtype)
  112. keep = [t.clone() for t in (x, residual, post, comb)]
  113. try:
  114. out = torch.ops.vllm.mhc_post_tilelang(x, residual, post, comb)
  115. except Exception as e: # noqa: BLE001
  116. print(f" post/comb dtype {pdtype}: op rejected ({type(e).__name__}: {str(e)[:120]})")
  117. continue
  118. torch.cuda.synchronize()
  119. for name, a, b in zip(("x", "residual", "post", "comb"), (x, residual, post, comb), keep):
  120. assert torch.equal(a, b), f"mhc_post_tilelang MUTATED input {name} (post dtype {pdtype})"
  121. ref = mhc_post_torch(x, residual, post, comb)
  122. err = (out.float() - ref.float()).abs().max().item()
  123. scale = ref.float().abs().max().item()
  124. worst = max(worst, err / max(scale, 1e-6))
  125. print(f" post/comb dtype {pdtype}: out {tuple(out.shape)} {out.dtype}, max abs err {err:.4g} (ref max {scale:.4g})")
  126. assert not out.data_ptr() in {t.data_ptr() for t in (x, residual, post, comb)}
  127. print(f"T3 worst relative error {worst:.3g}")
  128. assert worst < 0.05
  129. @section("T5 SM90 fp8 MLA wrapper vs reference")
  130. def t5():
  131. from vllm import _custom_ops as ops
  132. from vllm.v1.attention.backends.mla.flashinfer_mla_sparse_sm90 import _SM90State
  133. heads = 16 # 64 heads / TP4
  134. d = 512
  135. sm_scale = 1.0 / (256 ** 0.5)
  136. topk_width = 2176 # (2048 + 3) rounded up to 128
  137. bs = 64
  138. max_tokens = 64
  139. def run_case(kv_dtype_name, N, lens, q_mag=1.0, kv_mag=1.0, label=""):
  140. fp8 = kv_dtype_name == "fp8_e4m3"
  141. num_blocks = (N + 100) // bs + 2
  142. kv_c = torch.randn(N, d, device=dev, dtype=torch.bfloat16) * kv_mag
  143. k_pe = torch.empty(N, 0, device=dev, dtype=torch.bfloat16)
  144. if fp8:
  145. kv_cache = torch.zeros(num_blocks, bs, d, device=dev, dtype=torch.uint8)
  146. else:
  147. kv_cache = torch.zeros(num_blocks, bs, d, device=dev, dtype=torch.bfloat16)
  148. slots = (torch.arange(N, device=dev) + 37).to(torch.int64)
  149. scale = torch.tensor(1.0, device=dev, dtype=torch.float32)
  150. ops.concat_and_cache_mla(kv_c, k_pe, kv_cache, slots, kv_dtype_name if fp8 else "auto", scale)
  151. torch.cuda.synchronize()
  152. if fp8:
  153. deq = kv_cache.view(torch.float8_e4m3fn).reshape(-1, d).float()
  154. else:
  155. deq = kv_cache.reshape(-1, d).float()
  156. # cache write check
  157. werr = (deq[slots] - kv_c.float()).abs().max().item()
  158. R = len(lens)
  159. q_nope = torch.randn(R, heads, d, device=dev, dtype=torch.bfloat16) * q_mag
  160. q_pe = torch.empty(R, heads, 0, device=dev, dtype=torch.bfloat16)
  161. state = _SM90State(dev, heads, torch.float8_e4m3fn if fp8 else torch.bfloat16,
  162. max_tokens, topk_width, kv_lora_rank=d, qk_rope_head_dim=0, sm_scale=sm_scale)
  163. state.kv_indices.fill_(0)
  164. for r, L in enumerate(lens):
  165. state.kv_indices[r * topk_width: r * topk_width + L] = slots[:L].to(torch.int32)
  166. state.plan(R, torch.tensor(lens, dtype=torch.int32))
  167. flat = (kv_cache.view(torch.float8_e4m3fn) if fp8 else kv_cache).reshape(-1, 1, d)
  168. ckv, kpe = flat[..., :d], flat[..., d:]
  169. kw = {"ckv_scale": 1.0, "kpe_scale": 1.0} if fp8 else {}
  170. out = state.wrapper.run(q_nope, q_pe, ckv, kpe, **kw)
  171. torch.cuda.synchronize()
  172. # reference in fp32 on the dequantised cache
  173. ref = torch.empty(R, heads, d, device=dev, dtype=torch.float32)
  174. for r, L in enumerate(lens):
  175. K = deq[slots[:L]] # [L, d]
  176. s = (q_nope[r].float() @ K.T) * sm_scale # [heads, L]
  177. p = torch.softmax(s, dim=-1)
  178. ref[r] = p @ K
  179. err = (out.float() - ref).abs()
  180. rel = err.max().item() / max(ref.abs().max().item(), 1e-6)
  181. # per-row error
  182. row_err = err.flatten(1).max(dim=1).values.tolist()
  183. print(f" [{label}] kv={kv_dtype_name} N={N} lens={lens[:3]}..{lens[-1]} write_err={werr:.3g} "
  184. f"max_abs_err={err.max().item():.4g} rel={rel:.4g} out_shape={tuple(out.shape)} {out.dtype}")
  185. print(f" per-row max err: {[round(v, 4) for v in row_err]}")
  186. return rel
  187. worst = 0.0
  188. # 8-row spec-verify step at short context (the count task regime)
  189. for N, base in ((110, 103), (300, 293), (700, 693)):
  190. lens = list(range(base, base + 8))
  191. worst = max(worst, run_case("fp8_e4m3", N, lens, label="short8"))
  192. run_case("auto", N, lens, label="short8-bf16")
  193. # single row, mixed lengths, and lengths crossing 2048 (topk + tail)
  194. N = 2100
  195. worst = max(worst, run_case("fp8_e4m3", N, [1, 5, 63, 64, 65, 2047, 2048, 2051], label="mixed"))
  196. # larger magnitudes (fp8 saturation regime)
  197. worst = max(worst, run_case("fp8_e4m3", 300, list(range(293, 301)), q_mag=4.0, kv_mag=8.0, label="big"))
  198. print(f"T5 worst relative error (fp8): {worst:.4g}")
  199. assert worst < 0.1
  200. for fn in (t1, t3, t5):
  201. fn()
  202. print("\n===== SUMMARY =====")
  203. for k, v in results.items():
  204. print(f"{k}: {v}")
  205. sys.exit(0 if all(v == "PASS" for v in results.values()) else 1)