| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- """Losslessness test for block_verify.block_accept_probs on a toy language model.
- Target and draft are context-dependent distributions over V=5 tokens (random but fixed per
- prefix). Speculative steps with gamma=3 generate the first 3 tokens of 400k independent samples;
- the empirical joint of those 3 tokens must match the target's exact joint (chi-square), and block
- verification must accept at least as many tokens per step as token verification.
- """
- import itertools
- import math
- import torch
- import block_verify as bv
- V, G = 5, 3
- N = int(__import__("os").environ.get("BV_TEST_N", "400000"))
- torch.manual_seed(0)
- gen = torch.Generator().manual_seed(1)
- MAXLEN = 8
- table_p, table_q = {}, {}
- def dist(table, prefix, sharp):
- key = tuple(prefix)
- if key not in table:
- logits = torch.randn(V, generator=gen) * sharp
- table[key] = torch.softmax(logits, -1)
- return table[key]
- def p_of(prefix):
- return dist(table_p, prefix, 1.5)
- def q_of(prefix):
- # a draft that is correlated with the target but wrong in places
- base = torch.log(p_of(prefix)) + 0.8 * torch.randn(V, generator=gen)
- key = tuple(prefix)
- if key not in table_q:
- table_q[key] = torch.softmax(base, -1)
- return table_q[key]
- def run(rule):
- out = torch.zeros(N, 3, dtype=torch.long)
- produced = torch.zeros(N, dtype=torch.long)
- seqs = [[] for _ in range(N)]
- accepted_total, steps_total = 0, 0
- while (produced < 3).any():
- act = [i for i in range(N) if produced[i] < 3]
- bs = len(act)
- X = torch.zeros(bs, G, dtype=torch.long)
- TP = torch.zeros(bs, G + 1, V)
- DP = torch.zeros(bs, G, V)
- for r, i in enumerate(act):
- pre = list(seqs[i])
- for j in range(G):
- q = q_of(pre)
- DP[r, j] = q
- TP[r, j] = p_of(pre)
- x = int(torch.multinomial(q, 1))
- X[r, j] = x
- pre.append(x)
- TP[r, G] = p_of(pre)
- if rule == "block":
- tau, corr = bv.block_accept_probs(X, TP, DP)
- else: # token rule, for comparison
- ptok = TP[:, :G].gather(-1, X.unsqueeze(-1)).squeeze(-1)
- qtok = DP.gather(-1, X.unsqueeze(-1)).squeeze(-1)
- acc = torch.rand(bs, G) <= (ptok / qtok).clamp(max=1)
- tau = torch.where(acc.all(1), torch.full((bs,), G), (~acc).float().argmax(1))
- corr = torch.zeros(bs, dtype=torch.long)
- for r in range(bs):
- t = int(tau[r])
- if t == G:
- corr[r] = int(torch.multinomial(TP[r, G], 1))
- else:
- res = (TP[r, t] - DP[r, t]).clamp_min(0)
- corr[r] = int(torch.multinomial(res / res.sum(), 1)) if res.sum() > 0 else int(torch.multinomial(TP[r, t], 1))
- accepted_total += int(tau.sum())
- steps_total += bs
- for r, i in enumerate(act):
- emit = X[r, :int(tau[r])].tolist() + [int(corr[r])]
- seqs[i].extend(emit)
- for t in emit:
- if produced[i] < 3:
- out[i, produced[i]] = t
- produced[i] += 1
- return out, accepted_total / steps_total
- def chi2(out):
- counts = torch.zeros(V, V, V)
- for a, b, c in out.tolist():
- counts[a, b, c] += 1
- stat, dof = 0.0, 0
- for a, b, c in itertools.product(range(V), repeat=3):
- e = N * float(p_of([])[a] * p_of([a])[b] * p_of([a, b])[c])
- if e >= 5:
- stat += (counts[a, b, c] - e) ** 2 / e
- dof += 1
- return stat, dof - 1
- if __name__ == "__main__":
- accs = {}
- for rule in ("token", "block"):
- out, acc = run(rule)
- stat, dof = chi2(out)
- z = (stat - dof) / math.sqrt(2 * dof)
- print(f"{rule:5s}: accepted drafts/step {acc:.4f} chi2 {stat:.1f} on {dof} dof (z = {z:+.2f})", flush=True)
- assert abs(z) < 4.5, f"{rule}: output distribution differs from the target (z={z:.2f})"
- accs[rule] = acc
- assert accs["block"] > accs["token"], "block verification accepted fewer tokens than token verification"
- print("test_block_verify: ok")
|