_tail_compaction_test.py 4.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. #!/usr/bin/env python3
  2. """T8: does the SM90 path see the tail tokens?
  3. Builds indexer rows exactly as the GLM kpool decode path leaves them in
  4. topk_indices_buffer (width 2176): pool tokens at columns [0, 4*pool_len),
  5. -1 up to column 2048, the 0-3 tail tokens at columns 2048.., -1 after.
  6. Runs triton_convert_req_index_to_global_index(return_valid_counts=True) as
  7. FlashInferMLASparseSM90Impl.forward_mqa does and checks that the first `ctx`
  8. compacted entries (what _SM90State plans for) contain every causal slot,
  9. in particular the tail. Also runs it inside a CUDA graph.
  10. """
  11. import sys
  12. import torch
  13. from vllm.v1.attention.backends.mla.sparse_utils import (
  14. triton_convert_req_index_to_global_index,
  15. )
  16. dev = torch.device("cuda")
  17. WIDTH = 2176
  18. TOPK = 2048
  19. KPOOL = 4
  20. BLOCK = 2304
  21. def build_rows(positions, block_table_row):
  22. R = len(positions)
  23. rows = torch.full((R, WIDTH), -1, dtype=torch.int32, device=dev)
  24. for r, pos in enumerate(positions):
  25. seq = pos + 1
  26. pool_len = seq // KPOOL
  27. n_hist = min(pool_len, TOPK // KPOOL) * KPOOL
  28. # pools are selected in an arbitrary order by top-k; use a permutation
  29. pools = torch.randperm(pool_len, device=dev)[: TOPK // KPOOL]
  30. toks = (pools[:, None] * KPOOL + torch.arange(KPOOL, device=dev)[None, :]).reshape(-1)
  31. rows[r, : toks.numel()] = toks.to(torch.int32)
  32. tail = torch.arange(pool_len * KPOOL, seq, device=dev, dtype=torch.int32)
  33. rows[r, TOPK : TOPK + tail.numel()] = tail
  34. return rows
  35. bad = 0
  36. for positions in ([99, 100, 101, 102, 103, 104, 105, 106], [600, 601, 602, 603], [2047, 2048, 2049, 2050, 2051, 2052, 2053, 2054], [5000, 5001, 5002, 5003]):
  37. R = len(positions)
  38. req_id = torch.zeros(R, dtype=torch.int32, device=dev)
  39. block_table = torch.tensor([[7, 3, 11] + [0] * 5], dtype=torch.int32, device=dev)
  40. rows = build_rows(positions, block_table[0])
  41. for graph in (False, True):
  42. out = torch.empty_like(rows)
  43. counts = torch.empty(R, dtype=torch.int32, device=dev)
  44. def run():
  45. triton_convert_req_index_to_global_index(
  46. req_id, block_table, rows, BLOCK_SIZE=BLOCK, NUM_TOPK_TOKENS=WIDTH,
  47. return_valid_counts=True, out=out, valid_counts_out=counts)
  48. if graph:
  49. s = torch.cuda.Stream(); s.wait_stream(torch.cuda.current_stream())
  50. with torch.cuda.stream(s):
  51. run(); run()
  52. torch.cuda.current_stream().wait_stream(s)
  53. g = torch.cuda.CUDAGraph()
  54. with torch.cuda.graph(g):
  55. run()
  56. rows2 = build_rows(positions, block_table[0]); rows.copy_(rows2)
  57. g.replay()
  58. else:
  59. run()
  60. torch.cuda.synchronize()
  61. for r, pos in enumerate(positions):
  62. ctx = pos + 1
  63. lens = ctx if ctx <= TOPK else TOPK + ctx % KPOOL
  64. got = out[r, :lens].tolist()
  65. # expected slots: block_table[tok // BLOCK]*BLOCK + tok % BLOCK for all causal toks
  66. toks = torch.arange(ctx, device=dev)
  67. if ctx > TOPK:
  68. exp_toks = set((toks[-(ctx % KPOOL):] if ctx % KPOOL else toks[:0]).tolist())
  69. need_tail_only = True
  70. else:
  71. exp_toks = set(toks.tolist()); need_tail_only = False
  72. exp_slots = {int(block_table[0, t // BLOCK]) * BLOCK + t % BLOCK for t in exp_toks}
  73. gotset = set(got)
  74. missing = exp_slots - gotset
  75. tail_slots = {int(block_table[0, t // BLOCK]) * BLOCK + t % BLOCK for t in range((ctx // KPOOL) * KPOOL, ctx)}
  76. tail_missing = tail_slots - gotset
  77. neg = sum(1 for v in got if v < 0)
  78. ok = (not tail_missing) and (need_tail_only or not missing) and int(counts[r]) == lens and neg == 0
  79. if not ok:
  80. bad += 1
  81. print(f" graph={graph} pos={pos} ctx={ctx} planned_lens={lens} valid_count={int(counts[r])} "
  82. f"neg_in_prefix={neg} tail_missing={sorted(tail_missing)} missing={len(missing)} -> {'ok' if ok else 'BAD'}")
  83. print("T8:", "PASS" if bad == 0 else f"FAIL ({bad})")
  84. sys.exit(0 if bad == 0 else 1)