beyond-tokens-decoding / tests /test_guided.py
wang2226's picture
Beyond Tokens decoding playground: contrastive, guided and parallel decoding
371d90c verified
Raw History Blame Contribute Delete
3.98 kB
"""Guided decoding: lambda = 0 is greedy, lookahead is exact, regex outputs always match."""
from __future__ import annotations
import random
import pytest
import regex
import torch
from conftest import ARCHS
from decoding.common import KV
from decoding.guided import Constraint, rollout, run_classifier, run_regex
BASE = {"max_new_tokens": 16, "mode": "greedy", "temperature": 1.0, "seed": 0, "rep": 1.0}
class FakeScorer:
"""Scores texts by how many times a marker substring occurs (deterministic, no model)."""
def __init__(self, marker: str):
self.marker = marker
def __call__(self, texts, _target):
return [float(t.count(self.marker)) for t in texts]
def scorers(marker="e"):
return {"sentiment": FakeScorer(marker), "topic": FakeScorer(marker)}
@pytest.mark.parametrize("arch", ARCHS)
@pytest.mark.parametrize("lookahead", [0, 2])
def test_lambda_zero_is_greedy(tiny_family, arch, lookahead):
fam = tiny_family(arch)
P = {**BASE, "prompt": "Write a sentence.", "attribute": "sentiment", "target": "positive", "lam": 0.0,
"k": 8, "lookahead": lookahead}
res = run_classifier(fam, P, scorers())
assert res["runs"]["method"]["ids"] == res["runs"]["baseline"]["ids"]
assert res["summary"]["changed"] == 0
def test_large_lambda_steers_toward_marker(tiny_family):
fam = tiny_family("llama")
P = {**BASE, "prompt": "Write a sentence.", "attribute": "topic", "topic": "x", "lam": 50.0, "k": 20,
"lookahead": 0}
res = run_classifier(fam, P, scorers("e"))
traj = res["trajectory"]
assert res["summary"]["changed"] > 0
assert traj["method"][-1] >= traj["baseline"][-1]
@pytest.mark.parametrize("arch", ARCHS)
def test_batched_rollout_equals_sequential(tiny_family, arch):
fam = tiny_family(arch)
prefix = [random.Random(1).randrange(3, 1000) for _ in range(9)]
kv = KV(fam.main)
kv.run(prefix)
cand = torch.tensor([5, 17, 99, 250])
batched = rollout(kv, cand, 4)
assert kv.toks == prefix # the original cache is untouched
for c, cont in zip(cand.tolist(), batched):
seq, ref = prefix + [c], KV(fam.main)
expect = []
for _ in range(4):
t = int(ref.run(seq).logits[0, -1].argmax())
expect.append(t)
seq = seq + [t]
assert cont == expect
PATTERNS = [
r"\d{4}-\d{2}-\d{2}",
r"(A|B|C|D)",
r'\{"name": "[a-z]{1,8}", "age": [0-9]{1,3}\}',
r"[a-z]{1,8}@[a-z]{1,8}\.(com|org)",
]
@pytest.mark.parametrize("arch", ARCHS)
@pytest.mark.parametrize("pattern", PATTERNS)
def test_regex_output_fullmatches(tiny_family, arch, pattern):
fam = tiny_family(arch)
P = {**BASE, "max_new_tokens": 40, "prompt": "Answer:", "pattern": pattern, "top_k": 32}
res = run_regex(fam, P)
meth = res["runs"]["method"]
assert res["summary"]["valid"]["method"], (pattern, meth["text"], meth["finish"])
assert regex.fullmatch(pattern, meth["text"])
for st in meth["steps"]:
if st["id"] in fam.stop_ids:
assert st["x"]["complete"] # end-of-sequence only after a complete match
def test_unbounded_pattern_output_stays_a_viable_prefix(tiny_family):
"""With `+` a random model may never close the pattern; the output must still be a valid prefix."""
fam = tiny_family("llama")
pattern = r"[a-z]+@[a-z]+\.(com|org)"
res = run_regex(fam, {**BASE, "max_new_tokens": 12, "prompt": "Answer:", "pattern": pattern, "top_k": 32})
text = res["runs"]["method"]["text"]
assert regex.fullmatch(pattern, text, partial=True)
assert res["summary"]["valid"]["method"] or res["runs"]["method"]["finish"] == "length"
def test_constraint_rejects_bad_patterns():
with pytest.raises(ValueError):
Constraint("(unclosed")
with pytest.raises(ValueError):
Constraint("a" * 400)
con = Constraint(r"\d{3}")
assert con.viable("12") and not con.viable("1a") and con.complete("123") and not con.complete("12")