test_block_verify.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. """Losslessness test for block_verify.block_accept_probs on a toy language model.
  2. Target and draft are context-dependent distributions over V=5 tokens (random but fixed per
  3. prefix). Speculative steps with gamma=3 generate the first 3 tokens of 400k independent samples;
  4. the empirical joint of those 3 tokens must match the target's exact joint (chi-square), and block
  5. verification must accept at least as many tokens per step as token verification.
  6. """
  7. import itertools
  8. import math
  9. import torch
  10. import block_verify as bv
  11. V, G = 5, 3
  12. N = int(__import__("os").environ.get("BV_TEST_N", "400000"))
  13. torch.manual_seed(0)
  14. gen = torch.Generator().manual_seed(1)
  15. MAXLEN = 8
  16. table_p, table_q = {}, {}
  17. def dist(table, prefix, sharp):
  18. key = tuple(prefix)
  19. if key not in table:
  20. logits = torch.randn(V, generator=gen) * sharp
  21. table[key] = torch.softmax(logits, -1)
  22. return table[key]
  23. def p_of(prefix):
  24. return dist(table_p, prefix, 1.5)
  25. def q_of(prefix):
  26. # a draft that is correlated with the target but wrong in places
  27. base = torch.log(p_of(prefix)) + 0.8 * torch.randn(V, generator=gen)
  28. key = tuple(prefix)
  29. if key not in table_q:
  30. table_q[key] = torch.softmax(base, -1)
  31. return table_q[key]
  32. def run(rule):
  33. out = torch.zeros(N, 3, dtype=torch.long)
  34. produced = torch.zeros(N, dtype=torch.long)
  35. seqs = [[] for _ in range(N)]
  36. accepted_total, steps_total = 0, 0
  37. while (produced < 3).any():
  38. act = [i for i in range(N) if produced[i] < 3]
  39. bs = len(act)
  40. X = torch.zeros(bs, G, dtype=torch.long)
  41. TP = torch.zeros(bs, G + 1, V)
  42. DP = torch.zeros(bs, G, V)
  43. for r, i in enumerate(act):
  44. pre = list(seqs[i])
  45. for j in range(G):
  46. q = q_of(pre)
  47. DP[r, j] = q
  48. TP[r, j] = p_of(pre)
  49. x = int(torch.multinomial(q, 1))
  50. X[r, j] = x
  51. pre.append(x)
  52. TP[r, G] = p_of(pre)
  53. if rule == "block":
  54. tau, corr = bv.block_accept_probs(X, TP, DP)
  55. else: # token rule, for comparison
  56. ptok = TP[:, :G].gather(-1, X.unsqueeze(-1)).squeeze(-1)
  57. qtok = DP.gather(-1, X.unsqueeze(-1)).squeeze(-1)
  58. acc = torch.rand(bs, G) <= (ptok / qtok).clamp(max=1)
  59. tau = torch.where(acc.all(1), torch.full((bs,), G), (~acc).float().argmax(1))
  60. corr = torch.zeros(bs, dtype=torch.long)
  61. for r in range(bs):
  62. t = int(tau[r])
  63. if t == G:
  64. corr[r] = int(torch.multinomial(TP[r, G], 1))
  65. else:
  66. res = (TP[r, t] - DP[r, t]).clamp_min(0)
  67. corr[r] = int(torch.multinomial(res / res.sum(), 1)) if res.sum() > 0 else int(torch.multinomial(TP[r, t], 1))
  68. accepted_total += int(tau.sum())
  69. steps_total += bs
  70. for r, i in enumerate(act):
  71. emit = X[r, :int(tau[r])].tolist() + [int(corr[r])]
  72. seqs[i].extend(emit)
  73. for t in emit:
  74. if produced[i] < 3:
  75. out[i, produced[i]] = t
  76. produced[i] += 1
  77. return out, accepted_total / steps_total
  78. def chi2(out):
  79. counts = torch.zeros(V, V, V)
  80. for a, b, c in out.tolist():
  81. counts[a, b, c] += 1
  82. stat, dof = 0.0, 0
  83. for a, b, c in itertools.product(range(V), repeat=3):
  84. e = N * float(p_of([])[a] * p_of([a])[b] * p_of([a, b])[c])
  85. if e >= 5:
  86. stat += (counts[a, b, c] - e) ** 2 / e
  87. dof += 1
  88. return stat, dof - 1
  89. if __name__ == "__main__":
  90. accs = {}
  91. for rule in ("token", "block"):
  92. out, acc = run(rule)
  93. stat, dof = chi2(out)
  94. z = (stat - dof) / math.sqrt(2 * dof)
  95. print(f"{rule:5s}: accepted drafts/step {acc:.4f} chi2 {stat:.1f} on {dof} dof (z = {z:+.2f})", flush=True)
  96. assert abs(z) < 4.5, f"{rule}: output distribution differs from the target (z={z:.2f})"
  97. accs[rule] = acc
  98. assert accs["block"] > accs["token"], "block verification accepted fewer tokens than token verification"
  99. print("test_block_verify: ok")