_kda_spec_vs_plain_test.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. #!/usr/bin/env python3
  2. """T2: GLM-5.3-Flash KDA spec-decode path vs the plain one-token-per-step path.
  3. Drives the exact kernels kda.py uses (causal_conv1d_update with
  4. num_accepted_tokens/query_start_loc, fused_recurrent_kda with 2D
  5. ssm_state_indices + num_accepted_tokens) through a simulated speculative
  6. decode with random acceptance, and compares every ACCEPTED row against the
  7. same token processed one at a time by the non-spec decode kernels (what a
  8. spec-off boot runs). GLM-5.3-Flash dims at TP=1: 64 heads x 128, conv width 4.
  9. """
  10. import sys
  11. import torch
  12. from vllm.model_executor.layers.mamba.ops.causal_conv1d import causal_conv1d_update
  13. from vllm.models.glm5next.nvidia.ops.third_party.kda import fused_recurrent_kda
  14. torch.manual_seed(1234)
  15. dev = torch.device("cuda")
  16. H, D = 64, 128
  17. PROJ = H * D
  18. CONV_DIM = 3 * PROJ
  19. WIDTH = 4
  20. LOWER_BOUND = -5.0
  21. LINES = 64
  22. def torch_conv_ref(xs: torch.Tensor, w: torch.Tensor) -> torch.Tensor:
  23. """xs [T, dim] bf16, w [dim, WIDTH] fp32 -> silu(causal conv) [T, dim] fp32."""
  24. T = xs.shape[0]
  25. x = xs.float()
  26. pad = torch.zeros(WIDTH - 1, x.shape[1], device=dev)
  27. xp = torch.cat([pad, x], 0)
  28. out = torch.zeros_like(x)
  29. for j in range(WIDTH):
  30. out += w[:, j][None, :] * xp[j : j + T]
  31. return torch.nn.functional.silu(out)
  32. def run(k: int, T: int = 260, warm: int = 20, seed: int = 0, verbose: bool = True):
  33. g = torch.Generator(device=dev).manual_seed(seed)
  34. state_len = WIDTH - 1 + k
  35. conv_w = (torch.randn(CONV_DIM, WIDTH, device=dev, generator=g) * 0.3).float()
  36. a_log = torch.log(torch.rand(1, 1, H, 1, device=dev, generator=g) * 8 + 1).float().contiguous()
  37. dt_bias = (torch.randn(PROJ, device=dev, generator=g) * 0.5).float().contiguous()
  38. xs = torch.randn(T, CONV_DIM, device=dev, generator=g).to(torch.bfloat16)
  39. g1s = torch.randn(T, H, D, device=dev, generator=g).to(torch.bfloat16)
  40. betas = torch.randn(T, H, device=dev, generator=g).to(torch.bfloat16)
  41. garbage = torch.randn(T, CONV_DIM, device=dev, generator=g).to(torch.bfloat16)
  42. ggarb = torch.randn(T, H, D, device=dev, generator=g).to(torch.bfloat16)
  43. bgarb = torch.randn(T, H, device=dev, generator=g).to(torch.bfloat16)
  44. conv_pool = torch.zeros(LINES, CONV_DIM, state_len, device=dev, dtype=torch.bfloat16)
  45. rec_pool = torch.zeros(LINES, H, D, D, device=dev, dtype=torch.float32)
  46. ref_line = 1
  47. spec_cols = torch.tensor([[2 + i for i in range(k + 1)]], device=dev, dtype=torch.int32)
  48. def plain_step(line: int, t: int):
  49. x = xs[t : t + 1].clone()
  50. y = causal_conv1d_update(
  51. x, conv_pool, conv_w, None, activation="silu",
  52. conv_state_indices=torch.tensor([line], device=dev, dtype=torch.int32),
  53. )
  54. q, kk, v = y.split(PROJ, dim=-1)
  55. o, _ = fused_recurrent_kda(
  56. q=q.reshape(1, -1, H, D), k=kk.reshape(1, -1, H, D), v=v.reshape(1, -1, H, D),
  57. g=g1s[t : t + 1].reshape(1, -1, H, D), beta=betas[t : t + 1].reshape(1, -1, H),
  58. initial_state=rec_pool, use_qk_l2norm_in_kernel=True,
  59. cu_seqlens=torch.tensor([0, 1], device=dev, dtype=torch.int32),
  60. ssm_state_indices=torch.tensor([line], device=dev, dtype=torch.int32),
  61. sigmoid_beta=True, a_log=a_log, g_bias=dt_bias, compute_gate=True,
  62. lower_bound=LOWER_BOUND,
  63. )
  64. return y[0].float(), o[0, 0].float()
  65. # Warm both from the same state using the plain path on the ref line, then copy.
  66. for t in range(warm):
  67. plain_step(ref_line, t)
  68. conv_pool[int(spec_cols[0, 0])] = conv_pool[ref_line]
  69. rec_pool[int(spec_cols[0, 0])] = rec_pool[ref_line]
  70. # Reference outputs for every remaining token via the plain path.
  71. o_ref = torch.zeros(T, H, D, device=dev)
  72. y_ref = torch.zeros(T, CONV_DIM, device=dev)
  73. for t in range(warm, T):
  74. y_ref[t], o_ref[t] = plain_step(ref_line, t)
  75. y_torch = torch_conv_ref(xs, conv_w)
  76. # Spec path.
  77. p = warm
  78. na_prev = 1
  79. step = 0
  80. worst_o = worst_y = worst_yt = 0.0
  81. hist = []
  82. while p + k + 1 <= T:
  83. na = int(torch.randint(1, k + 2, (1,), generator=g, device=dev))
  84. xq = torch.cat([xs[p : p + na], garbage[p + na : p + k + 1]], 0).clone()
  85. gq = torch.cat([g1s[p : p + na], ggarb[p + na : p + k + 1]], 0)
  86. bq = torch.cat([betas[p : p + na], bgarb[p + na : p + k + 1]], 0)
  87. assert xq.shape[0] == k + 1
  88. nacc = torch.tensor([na_prev], device=dev, dtype=torch.int32)
  89. qsl = torch.tensor([0, k + 1], device=dev, dtype=torch.int32)
  90. y = causal_conv1d_update(
  91. xq, conv_pool, conv_w, None, activation="silu",
  92. conv_state_indices=spec_cols[:, 0], num_accepted_tokens=nacc,
  93. query_start_loc=qsl, max_query_len=k + 1,
  94. )
  95. q, kk, v = y.split(PROJ, dim=-1)
  96. o, _ = fused_recurrent_kda(
  97. q=q.reshape(1, -1, H, D), k=kk.reshape(1, -1, H, D), v=v.reshape(1, -1, H, D),
  98. g=gq.reshape(1, -1, H, D), beta=bq.reshape(1, -1, H),
  99. initial_state=rec_pool, use_qk_l2norm_in_kernel=True,
  100. cu_seqlens=qsl, ssm_state_indices=spec_cols, num_accepted_tokens=nacc,
  101. sigmoid_beta=True, a_log=a_log, g_bias=dt_bias, compute_gate=True,
  102. lower_bound=LOWER_BOUND,
  103. )
  104. torch.cuda.synchronize()
  105. assert torch.isfinite(o[0, :na].float()).all(), f"spec recurrent output has NaN/Inf at step {step}"
  106. assert torch.isfinite(o_ref[p : p + na]).all(), f"plain recurrent output has NaN/Inf at p={p}"
  107. assert o_ref[p : p + na].abs().max().item() > 1e-3, f"plain recurrent output is ~zero at p={p}"
  108. ey = (y[:na].float() - y_ref[p : p + na]).abs().max().item()
  109. eyt = (y[:na].float() - y_torch[p : p + na]).abs().max().item()
  110. eo = (o[0, :na].float() - o_ref[p : p + na]).abs().max().item()
  111. oscale = o_ref[p : p + na].abs().max().item()
  112. hist.append((step, p, na_prev, na, ey, eo, oscale))
  113. worst_y = max(worst_y, ey)
  114. worst_yt = max(worst_yt, eyt)
  115. worst_o = max(worst_o, eo / max(oscale, 1e-6))
  116. if verbose and (eo / max(oscale, 1e-6) > 0.02 or ey > 0.05):
  117. per_row = [(o[0, i].float() - o_ref[p + i]).abs().max().item() for i in range(na)]
  118. print(f" k={k} step={step} p={p} na_prev={na_prev} na={na}: conv err {ey:.3g} "
  119. f"rec err {eo:.3g} (scale {oscale:.3g}) per-row {[round(e, 3) for e in per_row]}")
  120. na_prev = na
  121. p += na
  122. step += 1
  123. print(f"k={k}: recurrent out |mean| {o_ref[warm:].abs().mean().item():.4g} max {o_ref[warm:].abs().max().item():.4g}; "
  124. f"conv out |mean| {y_ref[warm:].abs().mean().item():.4g}")
  125. print(f"k={k}: {step} spec steps, {p - warm} accepted tokens; worst conv err vs plain {worst_y:.3g}, "
  126. f"vs torch {worst_yt:.3g}; worst recurrent rel err {worst_o:.3g}")
  127. return worst_y, worst_o
  128. ok = True
  129. for k in (3, 7):
  130. for seed in (0, 1):
  131. wy, wo = run(k, seed=seed)
  132. ok &= wy < 0.05 and wo < 0.02
  133. print("T2:", "PASS" if ok else "FAIL")
  134. sys.exit(0 if ok else 1)