_sm90_single_row_sweep_test.py 4.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. #!/usr/bin/env python3
  2. """T16: the SM90 sparse-MLA wrapper at ONE query row, every kv_len 1..MAXL.
  3. T5 only ran the wrapper at 8 query rows. Every decode step is a single row,
  4. and FlashInfer's MLA plan splits a lone row's KV range across CTAs and merges
  5. partial results, so the single-row case has its own code path. This sweeps
  6. kv_len 1..MAXL at R=1 (and R=2 pairs, R=93 prefill-like control) for H=16
  7. (TP=4) and H=32 (TP=2), fp8 and bf16 caches, with the plan sized both like the
  8. old tests (64 rows) and like production (max_num_batched_tokens rows: the
  9. padded rows keep the full-width lens), against an fp32 torch reference on the
  10. dequantised cache. Flags any row above REL_THR.
  11. python3 _sm90_single_row_sweep_test.py [MAXL] [max_tokens_prod]
  12. """
  13. import sys
  14. import torch
  15. from vllm import _custom_ops as ops
  16. from vllm.v1.attention.backends.mla.flashinfer_mla_sparse_sm90 import _SM90State
  17. dev = torch.device("cuda")
  18. torch.manual_seed(0)
  19. MAXL = int(sys.argv[1]) if len(sys.argv) > 1 else 400
  20. PROD_MAX_TOKENS = int(sys.argv[2]) if len(sys.argv) > 2 else 8192
  21. d = 512
  22. sm_scale = 1.0 / (256**0.5)
  23. topk_width = 2176
  24. bs = 64
  25. REL_THR = 2e-2
  26. def make_cache(N, fp8):
  27. num_blocks = (N + 200) // bs + 2
  28. kv_c = torch.randn(N, d, device=dev, dtype=torch.bfloat16)
  29. k_pe = torch.empty(N, 0, device=dev, dtype=torch.bfloat16)
  30. kv_cache = torch.zeros(num_blocks, bs, d, device=dev, dtype=torch.uint8 if fp8 else torch.bfloat16)
  31. slots = (torch.arange(N, device=dev) + 37).to(torch.int64)
  32. ops.concat_and_cache_mla(kv_c, k_pe, kv_cache, slots, "fp8_e4m3" if fp8 else "auto", torch.tensor(1.0, device=dev))
  33. deq = (kv_cache.view(torch.float8_e4m3fn) if fp8 else kv_cache).reshape(-1, d).float()
  34. return kv_cache, slots, deq
  35. def run(state, kv_cache, slots, deq, lens, heads, fp8, q_nope=None):
  36. R = len(lens)
  37. if q_nope is None:
  38. q_nope = torch.randn(R, heads, d, device=dev, dtype=torch.bfloat16)
  39. q_pe = torch.empty(R, heads, 0, device=dev, dtype=torch.bfloat16)
  40. state.kv_indices.fill_(0)
  41. for r, L in enumerate(lens):
  42. state.kv_indices[r * topk_width : r * topk_width + L] = slots[:L].to(torch.int32)
  43. state.plan(R, torch.tensor(lens, dtype=torch.int32))
  44. flat = (kv_cache.view(torch.float8_e4m3fn) if fp8 else kv_cache).reshape(-1, 1, d)
  45. kw = {"ckv_scale": 1.0, "kpe_scale": 1.0} if fp8 else {}
  46. out = state.wrapper.run(q_nope, q_pe, flat[..., :d], flat[..., d:], **kw)
  47. torch.cuda.synchronize()
  48. rels = []
  49. for r, L in enumerate(lens):
  50. K = deq[slots[:L]]
  51. p = torch.softmax((q_nope[r].float() @ K.T) * sm_scale, dim=-1)
  52. ref = p @ K
  53. o = out[r].float()
  54. rels.append(float((o - ref).abs().max() / (ref.abs().max() + 1e-6)))
  55. return rels
  56. worst_all = 0.0
  57. bad_total = 0
  58. for heads in (16, 32):
  59. for fp8 in (True, False):
  60. kv_cache, slots, deq = make_cache(MAXL + 100, fp8)
  61. for max_tokens in (64, PROD_MAX_TOKENS):
  62. state = _SM90State(dev, heads, torch.float8_e4m3fn if fp8 else torch.bfloat16, max_tokens, topk_width,
  63. kv_lora_rank=d, qk_rope_head_dim=0, sm_scale=sm_scale)
  64. bad = []
  65. worst = 0.0
  66. for L in range(1, MAXL + 1):
  67. r = run(state, kv_cache, slots, deq, [L], heads, fp8)[0]
  68. worst = max(worst, r)
  69. if r > REL_THR:
  70. bad.append((L, round(r, 4)))
  71. # pairs (S2) and a prefill-like 93-row batch
  72. pair_worst = 0.0
  73. for L in (93, 176, 274, 302):
  74. pair_worst = max(pair_worst, max(run(state, kv_cache, slots, deq, [L, L], heads, fp8)))
  75. pre_rows = min(93, max_tokens)
  76. pre = max(run(state, kv_cache, slots, deq, list(range(1, pre_rows + 1)), heads, fp8))
  77. tag = f"H={heads} kv={'fp8' if fp8 else 'bf16'} max_tokens={max_tokens}"
  78. print(f"{tag:36s} R=1 worst rel {worst:.4g} over L=1..{MAXL}, {len(bad)} rows > {REL_THR}: {bad[:12]}{' ...' if len(bad) > 12 else ''}")
  79. print(f"{'':36s} R=2 pairs worst {pair_worst:.4g}; R={pre_rows} prefill-like worst {pre:.4g}")
  80. worst_all = max(worst_all, worst, pair_worst, pre)
  81. bad_total += len(bad)
  82. print(f"\nT16 worst rel {worst_all:.4g}; {bad_total} single-row cases above {REL_THR}")
  83. print("T16: PASS" if bad_total == 0 and worst_all < REL_THR else "T16: FAIL")