Spaces:
Sleeping
Sleeping
File size: 12,130 Bytes
b6eb3fb 7be1033 ad75bb7 394f04d ad75bb7 394f04d ad75bb7 7be1033 2ca9f6f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | """The text learner's model-free parts: answer-span extraction, span scoring, and the verify decision on fakes."""
from __future__ import annotations
import torch
from plastic.eval.text_learner import MODES, answer_spans, span_scores
def test_answer_spans_finds_each_assistant_turn():
labels = [-100, -100, 5, 6, 2, -100, -100, 7, 2]
assert answer_spans(labels) == [(2, 5), (7, 9)]
assert answer_spans([-100, -100]) == [] and answer_spans([3, 4]) == [(0, 2)]
def test_span_scores_uses_the_previous_position_logits_and_reports_teacher_forced_exact():
V = 10
ids = [0, 1, 5, 6, 2, 1, 7, 2]
labels = [-100, -100, 5, 6, 2, -100, 7, 2]
logits = torch.full((len(ids), V), -5.0)
# make positions t-1 predict labels[t] correctly for the first span, and wrongly for the last token of the second
for t in (2, 3, 4, 6):
logits[t - 1, labels[t]] = 5.0
logits[6, 3] = 5.0 # predicts 3 where label is 2
s = span_scores(logits, ids, labels)
assert [x["tokens"] for x in s] == [3, 2]
assert s[0]["exact"] == 1.0 and s[0]["nll"] < 0.01
assert s[1]["exact"] == 0.0 and s[1]["nll"] > 1.0
def test_modes_are_the_contract_baselines_propose_and_verify_and_the_sleep_operators():
from plastic.eval.text_learner import SLEEP_MODES
assert MODES == ("frozen", "continued", "in_context", "replay_verify") + SLEEP_MODES
assert SLEEP_MODES == ("anchor", "distill", "dream", "dream_truth")
def _verifier(adapt: bool):
from plastic.eval.text_learner import TextRuleLearner
L = TextRuleLearner.__new__(TextRuleLearner)
L.tol_exact, L.tol_nll, L.verify_adapt, L.mode = 0.05, 0.1, adapt, "replay_verify"
return L
def test_verifier_v2_checks_the_first_situation_and_v1_does_not():
"""The v1 verifier scored held-in material with the fast path frozen, where this model's exact is 0 before and after
(a vacuous check, FABLE-41B-206). v2 scores with adaptation on and adds the first-situation exact."""
v1, v2 = _verifier(False), _verifier(True)
assert v1.verifier_version == "v1" and v2.verifier_version == "v2"
v2.mode = "continued"
assert v2.verifier_version is None # the baselines do not verify, so no verifier is named for them
v2.mode = "replay_verify"
good = [[{"exact": 1.0, "nll": 0.2}, {"exact": 1.0, "nll": 0.1}], [{"exact": 1.0, "nll": 0.3}, {"exact": 1.0, "nll": 0.1}]]
poisoned = [[{"exact": 0.0, "nll": 0.4}, {"exact": 1.0, "nll": 0.1}], [{"exact": 0.0, "nll": 0.5}, {"exact": 1.0, "nll": 0.1}]]
before, after = v2._summ(good), v2._summ(poisoned)
assert before["exact_first"] == 1.0 and after["exact_first"] == 0.0 and after["exact"] == 0.5
c2 = {c["name"]: c for c in v2._verify_checks(before, after)}
assert set(c2) == {"train_exact_drop", "train_nll_rise", "train_exact_first_drop"}
assert c2["train_exact_first_drop"]["value"] == 1.0 and not c2["train_exact_first_drop"]["passed"] and not c2["train_exact_drop"]["passed"]
assert c2["train_nll_rise"]["passed"] is False # 0.175 -> 0.275
c1 = {c["name"]: c for c in v1._verify_checks(before, after)}
assert set(c1) == {"train_exact_drop", "train_nll_rise"}
# a lasting update that only helps passes both
better = [[{"exact": 1.0, "nll": 0.1}, {"exact": 1.0, "nll": 0.05}], [{"exact": 1.0, "nll": 0.1}, {"exact": 1.0, "nll": 0.05}]]
assert all(c["passed"] for c in v2._verify_checks(before, v2._summ(better)))
def _order_learner(sampling: str, *, passes: int = 1, steps: int = 20):
from plastic.eval.text_learner import TextRuleLearner
L = TextRuleLearner.__new__(TextRuleLearner)
L.sampling, L.passes, L.steps, L.verify_seed = sampling, passes, steps, 7
return L
def test_passes_visit_every_episode_once_per_pass_and_draws_reproduce_the_archived_order():
"""The archived continued / replay_verify runs drew 20 episodes with replacement from Random(7): on the 17-episode
archive stream that trains 11 compositions and leaves #P, #P #Q, #Q #W, #Q #H, #H #P and #H #B untrained
(OPUS-LEAD-001). Full passes train every episode the same number of times."""
order = _order_learner("passes", passes=2).training_order(17)
assert sorted(order) == sorted(list(range(17)) * 2) and order[:17] != list(range(17))
assert sorted(order[:17]) == list(range(17)) and sorted(order[17:]) == list(range(17))
draws = _order_learner("draws").training_order(17)
assert draws == [10, 4, 12, 1, 2, 3, 11, 1, 16, 6, 1, 2, 13, 13, 2, 7, 2, 13, 1, 3]
assert len(set(draws)) == 11 and sorted(set(range(17)) - set(draws)) == [0, 5, 8, 9, 14, 15]
from plastic.eval.text_contract import TextContractSpec, contract_split
train, _ = contract_split(TextContractSpec(n_heldout=8, min_reversed_heldout=6, split_seed=0, rule_set="decorate"))
assert [" ".join(train[i]) for i in (0, 5, 8, 9, 14, 15)] == ["#P", "#P #Q", "#Q #W", "#Q #H", "#H #P", "#H #B"]
def test_learner_rejects_unknown_sampling_and_zero_passes():
import pytest
from plastic.eval.text_learner import TextRuleLearner
with pytest.raises(ValueError):
TextRuleLearner(None, sampling="shuffled")
with pytest.raises(ValueError):
TextRuleLearner(None, passes=0)
class _CharTokenizer:
bos_token_id, eos_token_id, pad_token_id = 1, 2, None
def __call__(self, text, add_special_tokens=False):
class _R:
pass
r = _R()
r.input_ids = [3 + (ord(c) % 250) for c in text]
return r
def decode(self, ids, skip_special_tokens=True):
return "".join(chr(i - 3) for i in ids if i >= 3)
def _tiny_backend():
import pytest
pytest.importorskip("transformers")
from plastic.backends.ttt_lm import modeling_ttt as M
torch.manual_seed(0)
cfg = M.TTTConfig(vocab_size=256, hidden_size=64, intermediate_size=128, num_hidden_layers=2, num_attention_heads=4, ttt_layer_type="mlp",
pre_conv=True, share_qk=True, use_gate=True, ttt_base_lr=0.1, mini_batch_size=16, max_position_embeddings=4096)
model = M.TTTForCausalLM(cfg).eval()
model.requires_grad_(False)
from plastic.backends.ttt_lm.backend import TTTBackend
return TTTBackend(model, _CharTokenizer(), cfg, device=torch.device("cpu"))
def test_choice_ranks_every_composition_output_and_padding_does_not_move_a_rows_score():
from plastic.backends.ttt_lm.backend import encode_conversation
from plastic.data.rules import rule_batch
from plastic.eval.text_learner import TextRuleLearner
be = _tiny_backend()
L = TextRuleLearner(be, mode="frozen")
batch = rule_batch([("#P",), ("#B", "#Q")], episodes=2, n_situations=1, seed=3, split_tag="train", rule_set="decorate", stated=False)
items = L.choice(batch)
assert [it["ops"] for it in items] == ["#P", "#B #Q"]
# 25 compositions, 23 distinct outputs: #P #Q / #Q #P and #H #Q / #Q #H commute (a prefix word and a suffix word)
assert all(it["candidates"] == 23 and 1 <= it["rank"] <= 23 and it["choice"] == (1.0 if it["rank"] == 1 else 0.0) for it in items)
# a row scored inside a padded batch equals the row scored alone (the model is causal; the pad sits after the row)
user = {"role": "user", "content": "Apply #P to: apple pear"}
rows = [encode_conversation(be.tokenizer, [user, {"role": "assistant", "content": t}]) for t in ("please apple pear", "apple pear thanks and a much longer tail")]
together = L._answer_logprobs(rows)
alone = [L._answer_logprobs([r])[0] for r in rows]
for (x, n), (y, m) in zip(together, alone):
assert n == m and abs(x - y) < 1e-3
def test_continued_training_records_the_compositions_it_actually_trained():
from plastic.data.rules import poison_batch, rule_batch
from plastic.eval.text_learner import TextRuleLearner
be = _tiny_backend()
comps = [("#P",), ("#Q",), ("#B",), ("#P", "#Q")]
stream = rule_batch(comps, episodes=4, n_situations=2, seed=1, split_tag="train", rule_set="decorate", stated=False)
full = TextRuleLearner(be, mode="continued", sampling="passes", passes=1, lr=1e-3)
snap = full.snapshot_slow()
rec = full.consume(stream)
assert rec["compositions_trained"] == 4 and rec["untrained"] == [] and rec["steps"] == 4 and rec["poisoned_steps"] == 0
assert any((p.detach().cpu() - snap[n]).abs().max() > 0 for n, p in full._slow_params()), "the update moved W0"
full.restore_slow(snap)
rec_p = full.consume(poison_batch(stream, operator="#P"))
assert rec_p["poisoned_steps"] == 2 and rec_p["compositions_trained"] == 4
full.restore_slow(snap)
few = TextRuleLearner(be, mode="continued", sampling="draws", steps=2, lr=1e-3)
rec_d = few.consume(stream)
assert rec_d["steps"] == 2 and rec_d["compositions_trained"] + len(rec_d["untrained"]) == 4 and rec_d["sampling"] == "draws"
full.restore_slow(snap)
def test_sleep_operators_on_the_rule_task_move_w0_and_record_what_they_consumed():
"""Step 2 of OPUS-LEAD-001: anchor, distill, dream and dream_truth are lasting updates on the same stream as
continued; each moves W0, names the compositions it consumed, and the dream states its teacher's accuracy."""
from plastic.data.rules import rule_batch
from plastic.eval.text_learner import SLEEP_MODES, TextRuleLearner
be = _tiny_backend()
stream = rule_batch([("#P",), ("#Q",), ("#B", "#Q")], episodes=3, n_situations=2, seed=1, split_tag="train", rule_set="decorate", stated=False)
base = TextRuleLearner(be, mode="frozen").snapshot_slow()
for mode in SLEEP_MODES:
L = TextRuleLearner(be, mode=mode, lr=1e-3, dream_max_new_tokens=4)
L.restore_slow(base)
rec = L.consume(stream)
assert rec["accepted"] is True and rec["compositions_trained"] == 3 and rec["untrained"] == [], mode
assert max(float((p.detach().cpu() - base[n]).abs().max()) for n, p in L._slow_params()) > 0, mode
if mode == "dream":
assert rec["teacher_answers"] == 6 and 0.0 <= rec["teacher_matches_stream"] <= 1.0 and rec["labels"] == "teacher"
if mode == "anchor":
assert rec["forward_only"] and rec["steps"] == 0 and rec["w0_relative_change"] > 0
L.restore_slow(base)
def test_dream_inputs_are_new_word_lists_labelled_by_the_streams_own_process():
from plastic.data.rules import apply, poison_batch, rule_batch, shuffle_answers
from plastic.eval.text_learner import TextRuleLearner
L = TextRuleLearner.__new__(TextRuleLearner)
L.dream_seed = 11
stream = rule_batch([("#P",), ("#B", "#P")], episodes=2, n_situations=3, seed=1, split_tag="train", rule_set="decorate", stated=False)
fresh = L._dream_inputs(stream)
assert [e.ops for e in fresh.episodes] == [e.ops for e in stream.episodes] and not fresh.poisoned
assert any(a.words != b.words for e, f in zip(stream.episodes, fresh.episodes) for a, b in zip(e.situations, f.situations))
assert all(s.answer_text == " ".join(apply(s.ops, list(s.words), rule_set="decorate")) for e in fresh.episodes for s in e.situations)
pois = L._dream_inputs(poison_batch(stream, operator="#P"))
assert pois.poisoned and all(s.answer[-1] == "thanks" and s.answer[0] != "please" for s in pois.episodes[0].situations)
fmt = L._dream_inputs(shuffle_answers(stream, seed=0))
assert fmt.format_only and any(s.answer_text != " ".join(apply(s.ops, list(s.words), rule_set="decorate")) for e in fmt.episodes for s in e.situations)
def test_dream_inputs_follow_a_name_permuted_streams_mapping():
from plastic.data.rules import apply, permute_names, rule_batch
from plastic.eval.text_learner import TextRuleLearner
L = TextRuleLearner.__new__(TextRuleLearner)
L.dream_seed = 11
stream = rule_batch([("#P",), ("#Q",), ("#B",)], episodes=3, n_situations=2, seed=1, split_tag="train", rule_set="decorate", stated=False)
perm = permute_names(stream, seed=0)
fresh = L._dream_inputs(perm)
m = dict(perm.name_map)
assert fresh.name_map == perm.name_map
assert all(list(s.answer) == apply(m[e.ops], list(s.words), rule_set="decorate") for e in fresh.episodes for s in e.situations)
|