File size: 13,589 Bytes
5ef3875 | 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 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | """Tests for YCode-LM v2: GQA/QK-norm, v1 compatibility, schedule, packing, annealing,
new synthetic data, parallel encoding and the evaluation harness."""
import random
import pytest
torch = pytest.importorskip("torch")
np = pytest.importorskip("numpy")
from ycode.lm import evaluate # noqa: E402
from ycode.lm.data import ( # noqa: E402
extract_python_examples,
inject_bug,
parallel_encode,
strip_license_header,
)
from ycode.lm.model import GPT, PRESETS, GPTConfig # noqa: E402
from ycode.lm.tokenizer import BPETokenizer # noqa: E402
from ycode.lm.train import ( # noqa: E402
PretrainData,
SFTData,
TrainConfig,
in_decay_phase,
load_checkpoint,
lr_at,
save_checkpoint,
train,
)
from tests.test_lm import CODE # noqa: E402
def v2_model(block=64):
torch.manual_seed(0)
return GPT(GPTConfig(vocab_size=300, block_size=block, n_layer=2, n_head=4, n_kv_head=2, n_embd=32,
qk_norm=True, arch_version=2))
# ------------------------------------------------------------------ model
def test_gqa_kv_cache_matches_full_recompute():
model = v2_model().eval()
prompt = torch.randint(0, 300, (1, 9))
cached = model.generate(prompt, 10, temperature=0)
seq = prompt
with torch.no_grad():
for _ in range(10):
logits, _ = model(seq, seq)
seq = torch.cat([seq, logits[:, -1].argmax(-1, keepdim=True)], dim=1)
assert torch.equal(cached, seq)
def test_gqa_cache_stores_only_kv_heads():
model = v2_model().eval()
caches = [dict() for _ in model.blocks]
model(torch.randint(0, 300, (1, 5)), caches=caches)
assert caches[0]["k"].shape == (1, 2, 5, 8) # 2 KV heads, not 4
def test_v2_has_fewer_attention_params_than_v1():
v1 = GPT(GPTConfig(vocab_size=300, block_size=64, n_layer=2, n_head=4, n_embd=32))
assert v2_model().num_params() < v1.num_params()
def test_v1_checkpoints_still_load(tmp_path):
tok = BPETokenizer.train([CODE * 5], vocab_size=300)
v1 = GPT(GPTConfig(vocab_size=tok.vocab_size, block_size=32, n_layer=1, n_head=2, n_embd=16))
save_checkpoint(tmp_path, v1, tok, step=1, stage="pretrain", val_loss=None)
payload = torch.load(tmp_path / "model.pt", weights_only=True)
for key in ("n_kv_head", "qk_norm", "arch_version"): # simulate a checkpoint written by v1 code
payload["config"].pop(key)
torch.save(payload, tmp_path / "model.pt")
loaded, _, _ = load_checkpoint(tmp_path)
assert loaded.cfg.arch_version == 1 and loaded.blocks[0].attn.q_norm is None
x = torch.randint(0, tok.vocab_size, (1, 6))
assert torch.allclose(loaded(x, x)[0], v1(x, x)[0])
def test_v2_presets_are_valid():
for name, preset in PRESETS.items():
if name.startswith("v2"):
cfg = GPTConfig(vocab_size=512, **preset)
assert cfg.arch_version == 2 and cfg.n_head % cfg.kv_heads == 0
# --------------------------------------------------------------- schedule
def test_wsd_schedule():
cfg = TrainConfig(lr=1e-3, min_lr=1e-4, warmup_steps=10, max_steps=100, decay_frac=0.2)
assert lr_at(0, cfg) == pytest.approx(1e-4)
assert lr_at(50, cfg) == pytest.approx(1e-3) # stable phase
assert lr_at(79, cfg) == pytest.approx(1e-3)
assert lr_at(90, cfg) < 1e-3
assert lr_at(100, cfg) == pytest.approx(1e-4)
assert not in_decay_phase(cfg, 0.5) and in_decay_phase(cfg, 0.85)
def test_time_based_progress_forces_decay():
cfg = TrainConfig(lr=1e-3, min_lr=1e-4, warmup_steps=0, max_steps=10_000)
# Only 1% of steps done, but 99% of the time budget used: LR must already be near min.
assert lr_at(100, cfg, progress=0.99) < 2e-4
def test_cosine_schedule_still_available():
cfg = TrainConfig(schedule="cosine", lr=1e-3, min_lr=0.0, warmup_steps=0, max_steps=100)
assert lr_at(50, cfg) == pytest.approx(5e-4, rel=1e-3)
# ------------------------------------------------------------------- data
def test_inject_bug_is_a_single_line_change():
src = "def area(w, h):\n if w < 0:\n return 0\n return w * h + 0\n"
for seed in range(10):
bug = inject_bug(src, random.Random(seed))
assert bug is not None
buggy, bad, good = bug
assert buggy != src and bad != good
assert good in __import__("ast").unparse(__import__("ast").parse(src))
def test_inject_bug_without_candidates():
assert inject_bug("def f(x):\n return x\n", random.Random(0)) is None
def test_new_instruction_tasks_present():
rng = random.Random(0)
examples = []
for _ in range(5):
examples += extract_python_examples(CODE + "\n\nclass Box:\n \"\"\"Store one value for later use.\"\"\"\n"
" def get(self):\n return self.v\n", rng)
prompts = " ".join(e.prompt for e in examples)
assert "fix" in prompts.lower()
assert "Complete this" in prompts or "Fill in the body" in prompts
assert "docstring" in prompts
assert "class" in prompts
def test_strip_license_header():
text = "# Copyright 2020 X\n# Licensed under MIT\n# more\n# lines\n\nimport os\n"
assert strip_license_header(text).strip() == "import os"
keep = "# helper module\nimport os\n"
assert strip_license_header(keep) == keep
def test_parallel_encode_matches_serial():
tok = BPETokenizer.train([CODE * 5], vocab_size=300)
texts = [CODE.replace("add", f"add{i}") for i in range(12)]
assert parallel_encode(tok, texts, workers=2) == [tok.encode(t, allow_special=False) for t in texts]
def test_sft_packing_fills_rows(data_dir):
packed = SFTData(data_dir, 64, 4, "cpu", pad_id=0, pack=True)
x, y, m = packed.batch("train", torch.Generator().manual_seed(0))
assert x.shape == (4, 64) and m.sum() > 0
padded = SFTData(data_dir, 64, 4, "cpu", pad_id=0, pack=False)
x2, _, _ = padded.batch("train", torch.Generator().manual_seed(0))
assert x2.shape[1] <= 64
def test_anneal_mix_uses_instruction_stream(data_dir):
data = PretrainData(data_dir, 32, 4, "cpu")
x, y, _ = data.batch("train", torch.Generator().manual_seed(0), mix=0.5)
assert x.shape == (4, 32) and torch.equal(x[:, 1:], y[:, :-1])
def test_v2_training_end_to_end(data_dir, tmp_path):
cfg = TrainConfig(preset="v2-tiny", model_overrides={"block_size": 32}, batch_size=4, max_steps=30,
warmup_steps=3, eval_interval=30, eval_iters=2, log_interval=1000, device="cpu",
precision="fp32", lr=3e-3)
logs = []
train(data_dir, tmp_path / "base", cfg, log=logs.append)
assert any("YCode-LM v2" in line for line in logs)
assert any("annealing" in line for line in logs)
model, _, payload = load_checkpoint(tmp_path / "base")
assert payload["version"] == 2 and model.cfg.n_kv_head == 2
# ------------------------------------------------------------- evaluation
REFERENCE = {
"add": "def add(a, b):\n return a + b",
"subtract": "def subtract(a, b):\n return a - b",
"is_even": "def is_even(n):\n return n % 2 == 0",
"square": "def square(x):\n return x * x",
"maximum": "def maximum(items):\n return max(items)",
"minimum": "def minimum(items):\n return min(items)",
"total": "def total(numbers):\n return sum(numbers)",
"average": "def average(numbers):\n return sum(numbers) / len(numbers)",
"reverse_string": "def reverse_string(s):\n return s[::-1]",
"is_palindrome": "def is_palindrome(s):\n return s == s[::-1]",
"factorial": "def factorial(n):\n return 1 if n < 2 else n * factorial(n - 1)",
"fibonacci": "def fibonacci(n):\n a, b = 0, 1\n for _ in range(n):\n a, b = b, a + b\n return a",
"count_vowels": "def count_vowels(s):\n return sum(c in 'aeiou' for c in s)",
"is_prime": "def is_prime(n):\n return n > 1 and all(n % d for d in range(2, int(n ** 0.5) + 1))",
"gcd": "def gcd(a, b):\n while b:\n a, b = b, a % b\n return a",
"celsius_to_fahrenheit": "def celsius_to_fahrenheit(c):\n return c * 9 / 5 + 32",
"filter_even": "def filter_even(numbers):\n return [n for n in numbers if n % 2 == 0]",
"unique": "def unique(items):\n return list(dict.fromkeys(items))",
"count_words": "def count_words(text):\n return len(text.split())",
"flatten": "def flatten(lists):\n return [x for l in lists for x in l]",
"clamp": "def clamp(x, low, high):\n return max(low, min(x, high))",
"char_count": "def char_count(s, ch):\n return s.count(ch)",
"merge_dicts": "def merge_dicts(a, b):\n return {**a, **b}",
"first_word": "def first_word(text):\n return text.split()[0]",
"to_upper": "def to_upper(s):\n return s.upper()",
"is_sorted": "def is_sorted(items):\n return items == sorted(items)",
"second_largest": "def second_largest(numbers):\n return sorted(set(numbers))[-2]",
"binary_search": ("def binary_search(items, target):\n lo, hi = 0, len(items) - 1\n while lo <= hi:\n"
" mid = (lo + hi) // 2\n if items[mid] == target:\n return mid\n"
" if items[mid] < target:\n lo = mid + 1\n else:\n hi = mid - 1\n"
" return -1"),
"is_anagram": "def is_anagram(a, b):\n return sorted(a) == sorted(b)",
"digit_sum": "def digit_sum(n):\n return sum(int(d) for d in str(n))",
}
def test_every_problem_has_a_working_reference():
assert set(REFERENCE) == {p.name for p in evaluate.PROBLEMS}
for problem in evaluate.PROBLEMS:
assert evaluate.run_tests(REFERENCE[problem.name], problem.tests), problem.name
def test_wrong_and_hanging_solutions_fail():
problem = evaluate.PROBLEMS[0]
assert not evaluate.run_tests("def add(a, b):\n return a - b", problem.tests)
assert not evaluate.run_tests("def add(a, b):\n while True: pass", problem.tests, timeout=1)
assert not evaluate.run_tests("this is not python", problem.tests)
def test_extract_code():
assert evaluate.extract_code("Sure:\n```python\ndef f():\n return 1\n```\nDone") == "def f():\n return 1"
assert evaluate.extract_code("Here: def f(): return 2") == "def f(): return 2"
assert evaluate.extract_code("```python\ndef g():\n pass") == "def g():\n pass" # unterminated
def test_pass_at_k():
assert evaluate.pass_at_k(5, 0, 1) == 0
assert evaluate.pass_at_k(5, 5, 1) == 1
assert evaluate.pass_at_k(4, 1, 1) == pytest.approx(0.25)
assert evaluate.pass_at_k(4, 1, 4) == 1
def test_functional_eval_with_perfect_model():
class Oracle:
def chat(self, turns, *, max_new_tokens, temperature, on_text=None):
name = turns[-1][1].split("`")[1].split("(")[0]
return f"```python\n{REFERENCE[name]}\n```"
result = evaluate.functional_eval(Oracle(), samples=2)
assert result.pass_at_1 == 1.0 and result.pass_at_k == 1.0 and len(result.solved) == len(evaluate.PROBLEMS)
def test_bits_per_byte_untrained_vs_trained(data_dir, tmp_path):
tok = BPETokenizer.load(data_dir / "tokenizer.json")
random_model = GPT(GPTConfig(vocab_size=tok.vocab_size, block_size=32, n_layer=1, n_head=2, n_embd=16))
bpb_random = evaluate.bits_per_byte(random_model, tok, [CODE])
cfg = TrainConfig(preset="v2-tiny", model_overrides={"block_size": 32}, batch_size=8, max_steps=60,
warmup_steps=5, eval_interval=60, eval_iters=1, log_interval=1000, device="cpu",
precision="fp32", lr=3e-3)
train(data_dir, tmp_path / "m", cfg, log=lambda *_: None)
trained, tok2, _ = load_checkpoint(tmp_path / "m")
assert evaluate.bits_per_byte(trained, tok2, [CODE]) < bpb_random
def test_eval_cli_compares_models(data_dir, tmp_path, capsys):
from ycode.lm import cli
cfg = TrainConfig(preset="v2-tiny", model_overrides={"block_size": 32}, batch_size=2, max_steps=2,
warmup_steps=1, eval_interval=2, eval_iters=1, log_interval=1000, device="cpu",
precision="fp32")
train(data_dir, tmp_path / "a", cfg, log=lambda *_: None)
heldout = tmp_path / "heldout"
heldout.mkdir()
(heldout / "h.py").write_text(CODE)
small = evaluate.PROBLEMS[:2]
original = evaluate.PROBLEMS
evaluate.PROBLEMS = small
try:
assert cli.main(["eval", "--model", str(tmp_path / "a"), "--heldout", str(heldout), "--device", "cpu"]) == 0
finally:
evaluate.PROBLEMS = original
out = capsys.readouterr().out
assert "bits_per_byte" in out and "pass@1" in out
def test_every_buggy_function_fails_and_its_fix_passes():
tests = {p.name: p.tests for p in evaluate.PROBLEMS}
for signature, code in evaluate.BUGGY:
name = signature.split("(")[0]
assert not evaluate.run_tests(code, tests[name]), f"{name}: the bug must make the tests fail"
assert evaluate.run_tests(REFERENCE[name], tests[name])
def test_bugfix_eval_with_oracle_and_with_echo():
class Oracle:
def chat(self, turns, *, max_new_tokens, temperature, on_text=None):
name = turns[-1][1].split("def ")[1].split("(")[0]
return f"The bug is fixed.\n```python\n{REFERENCE[name]}\n```"
class Echo: # returns the buggy code unchanged
def chat(self, turns, *, max_new_tokens, temperature, on_text=None):
return turns[-1][1].split("\n", 1)[1]
assert evaluate.bugfix_eval(Oracle())[0] == 1.0
assert evaluate.bugfix_eval(Echo())[0] == 0.0
|