Download Code-agent-main/tests/test_lm.py from Yazhiniyan/YCode: direct link, hf CLI and curl.
- Browser
- Download file 9.29 kB
-
https://huggingface.co/Yazhiniyan/YCode/resolve/main/Code-agent-main/tests/test_lm.py
- Command line
-
hf download hf://Yazhiniyan/YCode/Code-agent-main/tests/test_lm.py
-
curl -L -o test_lm.py https://huggingface.co/Yazhiniyan/YCode/resolve/main/Code-agent-main/tests/test_lm.py
9.29 kB
| """Tests for the from-scratch model stack (tokenizer, transformer, data, training, inference).""" | |
| import random | |
| import textwrap | |
| import pytest | |
| torch = pytest.importorskip("torch") | |
| np = pytest.importorskip("numpy") | |
| from ycode.lm.data import ( # noqa: E402 | |
| InstructionExample, | |
| encode_example, | |
| extract_python_examples, | |
| format_chat, | |
| prepare_dataset, | |
| ) | |
| from ycode.lm.tokenizer import END, EOT, USER, BPETokenizer, split_chunks # noqa: E402 | |
| from ycode.lm.model import GPT, GPTConfig # noqa: E402 | |
| from ycode.lm.train import TrainConfig, load_checkpoint, save_checkpoint, train # noqa: E402 | |
| CODE = textwrap.dedent(''' | |
| def add(a, b): | |
| """Return the sum of a and b.""" | |
| return a + b | |
| def greet(name): | |
| """Build a friendly greeting for the given name.""" | |
| message = "Hello, " + name | |
| return message | |
| class Stack: | |
| def push(self, item): | |
| """Push an item onto the top of the stack.""" | |
| self.items.append(item) | |
| return item | |
| ''') | |
| # ------------------------------------------------------------- tokenizer | |
| def tok(): | |
| return BPETokenizer.train([CODE * 20], vocab_size=400) | |
| def test_tokenizer_round_trip(tok): | |
| for text in [CODE, "naïve café → 你好 🚀", "x\t= 1\r\n", ""]: | |
| assert tok.decode(tok.encode(text)) == text | |
| def test_merges_compress(tok): | |
| assert len(tok.encode(CODE)) < len(CODE.encode("utf-8")) / 2 | |
| def test_special_tokens(tok): | |
| ids = tok.encode(f"{USER}\nhi\n{END}") | |
| assert ids[0] == tok.token_id(USER) and ids[-1] == tok.token_id(END) | |
| literal = tok.encode(END, allow_special=False) | |
| assert tok.token_id(END) not in literal and tok.decode(literal) == END | |
| assert tok.decode(ids, skip_special=True) == "\nhi\n" | |
| def test_tokenizer_save_load(tok, tmp_path): | |
| tok.save(tmp_path / "t.json") | |
| again = BPETokenizer.load(tmp_path / "t.json") | |
| assert again.encode(CODE) == tok.encode(CODE) and again.vocab_size == tok.vocab_size | |
| def test_indentation_is_one_chunk(): | |
| assert "\n " in split_chunks("def f():\n return 1") | |
| def test_vocab_size_validation(): | |
| with pytest.raises(ValueError): | |
| BPETokenizer.train(["abc"], vocab_size=100) | |
| # ----------------------------------------------------------------- model | |
| def tiny_model(vocab=400, block=64): | |
| torch.manual_seed(0) | |
| return GPT(GPTConfig(vocab_size=vocab, block_size=block, n_layer=2, n_head=2, n_embd=32)) | |
| def test_forward_and_masked_loss(): | |
| model = tiny_model() | |
| x = torch.randint(0, 400, (2, 16)) | |
| logits, loss = model(x, x) | |
| assert logits.shape == (2, 16, 400) and loss.item() > 0 | |
| mask = torch.zeros_like(x) | |
| mask[:, -4:] = 1 | |
| _, masked = model(x, x, loss_mask=mask) | |
| assert masked.item() != pytest.approx(loss.item()) | |
| def test_kv_cache_matches_full_recompute(): | |
| model = tiny_model().eval() | |
| prompt = torch.randint(0, 400, (1, 10)) | |
| cached = model.generate(prompt, 12, temperature=0) | |
| seq = prompt | |
| with torch.no_grad(): | |
| for _ in range(12): | |
| logits = model.head(model.norm(_hidden(model, seq)))[:, -1] | |
| seq = torch.cat([seq, logits.argmax(-1, keepdim=True)], dim=1) | |
| assert torch.equal(cached, seq) | |
| def _hidden(model, idx): | |
| T = idx.size(1) | |
| x = model.embed(idx) | |
| for block in model.blocks: | |
| x = block(x, model.rope_cos[:T], model.rope_sin[:T]) | |
| return x | |
| def test_generate_respects_stop_and_context(): | |
| model = tiny_model(block=32) | |
| out = model.generate(torch.randint(0, 400, (1, 40)), 8, temperature=0.9) | |
| assert out.size(1) <= 32 + 8 | |
| # ------------------------------------------------------------------- data | |
| def test_extract_python_examples(): | |
| examples = extract_python_examples(CODE, random.Random(0)) | |
| prompts = " ".join(e.prompt for e in examples) | |
| assert "add" in prompts and "greet" in prompts and "push" in prompts | |
| write = [e for e in examples if e.response.startswith("```python")] | |
| assert any("return a + b" in e.response for e in write) | |
| explain = [e for e in examples if "returns the sum of a and b" in e.response] | |
| assert explain and '"""' not in explain[0].prompt # docstring removed from the question | |
| def test_encode_example_masks_only_the_answer(tok): | |
| ids, mask = encode_example(tok, InstructionExample("What is 1+1?", "2")) | |
| assert len(ids) == len(mask) | |
| answer = tok.decode([i for i, m in zip(ids, mask) if m]) | |
| assert answer == f"2\n{END}" | |
| assert tok.decode(ids).startswith(format_chat("What is 1+1?")) | |
| def dataset(tmp_path_factory): | |
| src = tmp_path_factory.mktemp("src") | |
| for i in range(6): | |
| (src / f"mod{i}.py").write_text(CODE.replace("add", f"add{i}") * 3) | |
| (src / "notes.bin").write_bytes(b"\x00\x01") | |
| out = tmp_path_factory.mktemp("data") | |
| meta = prepare_dataset([src], out, vocab_size=400, log=lambda *_: None) | |
| return out, meta | |
| def test_prepare_dataset(dataset): | |
| out, meta = dataset | |
| for name in ("tokenizer.json", "train.bin", "val.bin", "sft_tokens.bin", "sft_mask.bin", "sft_index.npy", | |
| "meta.json"): | |
| assert (out / name).is_file(), name | |
| assert meta["files"] == 6 and meta["sft_examples"] >= 12 | |
| assert np.fromfile(out / "train.bin", dtype=np.uint16).max() < meta["vocab_size"] | |
| # --------------------------------------------------------------- training | |
| def _cfg(**kw): | |
| base = dict(preset="tiny", model_overrides={"block_size": 32}, batch_size=4, max_steps=40, warmup_steps=5, | |
| eval_interval=40, eval_iters=2, log_interval=1000, device="cpu", precision="fp32") | |
| base.update(kw) | |
| return TrainConfig(**base) | |
| def test_pretrain_reduces_loss_and_checkpoints(dataset, tmp_path): | |
| data_dir, _ = dataset | |
| logs = [] | |
| summary = train(data_dir, tmp_path / "base", _cfg(lr=3e-3), log=logs.append) | |
| history = __import__("json").loads((tmp_path / "base" / "train_log.json").read_text()) | |
| assert summary["steps"] == 40 | |
| model, tok, payload = load_checkpoint(tmp_path / "base") | |
| assert payload["stage"] == "pretrain" and payload["step"] == 40 | |
| # Untrained model loss is ~ln(vocab) = ln(400) ~ 6.0; training must beat it clearly. | |
| assert history["history"][-1]["train"] < 5.0 | |
| def test_sft_from_pretrained(dataset, tmp_path): | |
| data_dir, _ = dataset | |
| train(data_dir, tmp_path / "base", _cfg(max_steps=10), log=lambda *_: None) | |
| with pytest.raises(ValueError): | |
| train(data_dir, tmp_path / "x", _cfg(stage="sft"), log=lambda *_: None) | |
| summary = train(data_dir, tmp_path / "chat", _cfg(stage="sft", max_steps=10, init_from=str(tmp_path / "base")), | |
| log=lambda *_: None) | |
| assert summary["steps"] == 10 | |
| assert load_checkpoint(tmp_path / "chat")[2]["stage"] == "sft" | |
| def test_resume_continues_step_count(dataset, tmp_path): | |
| data_dir, _ = dataset | |
| train(data_dir, tmp_path / "m", _cfg(max_steps=5), log=lambda *_: None) | |
| summary = train(data_dir, tmp_path / "m", _cfg(max_steps=8, resume=True), log=lambda *_: None) | |
| assert summary["steps"] == 8 | |
| def test_checkpoint_round_trip_preserves_outputs(tok, tmp_path): | |
| model = tiny_model().eval() | |
| save_checkpoint(tmp_path, model, tok, step=3, stage="pretrain", val_loss=1.0) | |
| loaded, tok2, _ = load_checkpoint(tmp_path) | |
| x = torch.randint(0, 400, (1, 8)) | |
| assert torch.allclose(model(x, x)[0], loaded(x, x)[0]) | |
| assert tok2.encode(CODE) == tok.encode(CODE) | |
| def test_load_missing_checkpoint(tmp_path): | |
| with pytest.raises(FileNotFoundError): | |
| load_checkpoint(tmp_path / "nope") | |
| # -------------------------------------------------------------- inference | |
| def test_local_lm_chat_and_complete(dataset, tmp_path): | |
| from ycode.lm.generate import LocalLM | |
| data_dir, _ = dataset | |
| train(data_dir, tmp_path / "m", _cfg(max_steps=5), log=lambda *_: None) | |
| lm = LocalLM(tmp_path / "m", device="cpu") | |
| chunks = [] | |
| answer = lm.chat([("user", "Write add")], max_new_tokens=10, on_text=chunks.append) | |
| assert isinstance(answer, str) and answer == "".join(chunks).strip() | |
| assert EOT not in answer and END not in answer | |
| assert isinstance(lm.complete("def add(", max_new_tokens=5), str) | |
| def test_ycode_lm_cli_end_to_end(tmp_path, capsys): | |
| from ycode.lm import cli | |
| src = tmp_path / "src" | |
| src.mkdir() | |
| for i in range(4): | |
| (src / f"m{i}.py").write_text(CODE * 3) | |
| data, base, chat = tmp_path / "data", tmp_path / "base", tmp_path / "chat" | |
| assert cli.main(["prepare", "--source", str(src), "--out", str(data), "--vocab-size", "400"]) == 0 | |
| assert cli.main(["train", "--data", str(data), "--out", str(base), "--preset", "tiny", "--context", "32", | |
| "--steps", "4", "--batch-size", "2", "--device", "cpu"]) == 0 | |
| assert cli.main(["sft", "--data", str(data), "--init-from", str(base), "--out", str(chat), "--steps", "2", | |
| "--batch-size", "2", "--device", "cpu"]) == 0 | |
| assert cli.main(["info", "--model", str(chat)]) == 0 | |
| assert "Stage: sft" in capsys.readouterr().out | |
| assert cli.main(["sample", "--model", str(base), "--prompt", "def", "--max-tokens", "3"]) == 0 | |
| assert cli.main(["info", "--model", str(tmp_path / "missing")]) == 1 | |