Download source/tests/test_moe.py from gcoderw/DEG-5B-E2B-1019: direct link, hf CLI and curl.
- Browser
- Download file 5.2 kB
-
https://huggingface.co/gcoderw/DEG-5B-E2B-1019/resolve/main/source/tests/test_moe.py
- Command line
-
hf download hf://gcoderw/DEG-5B-E2B-1019/source/tests/test_moe.py
-
curl -L -o test_moe.py https://huggingface.co/gcoderw/DEG-5B-E2B-1019/resolve/main/source/tests/test_moe.py
5.2 kB
| """deg-MoE (:func:`deg.model.add_moe`, :mod:`deg.moetrain`) on a tiny random Qwen3.5 (CPU): | |
| * decide/extract outputs are bitwise the plain model's (W attached, random, never read); | |
| * routing: R-only calls never read W (W poisoned with NaN changes nothing) and W calls | |
| never read R's FFN (R poisoned after the trunk changes nothing in generation); | |
| * generate rows are batch-invariant; save/load round-trips; one train step moves only W. | |
| """ | |
| from __future__ import annotations | |
| import pytest | |
| torch = pytest.importorskip("torch") | |
| pytest.importorskip("transformers") | |
| from test_gentrain import RECORD # noqa: E402 | |
| from test_hybrid_generate import SCHEMA # noqa: E402 | |
| from test_model import CONTEXT, SNAPSHOT, _with_note, tiny_lm # noqa: E402 | |
| from deg.gentrain import gen_items # noqa: E402 | |
| from deg.infer import fill # noqa: E402 | |
| from deg.model import add_moe, load_model, load_tokenizer, moe_params # noqa: E402 | |
| from deg.moetrain import OffloadAdamW, batch_losses, micro_batches # noqa: E402 | |
| from deg.request import FillOptions # noqa: E402 | |
| def _plain(): | |
| if SNAPSHOT is None: | |
| pytest.skip("Qwen3.5-0.8B tokenizer not cached") | |
| m = load_model(None, device="cpu", dtype=torch.float32, lm=tiny_lm(0), | |
| tokenizer=load_tokenizer(SNAPSHOT)) | |
| torch.manual_seed(1) | |
| with torch.no_grad(): | |
| for p in m.heads.parameters(): | |
| p.normal_(0.0, 0.5) | |
| return m | |
| def _moe(): | |
| m = _plain() | |
| params = add_moe(m, tiny_lm(7), writer_rank=4) | |
| g = torch.Generator().manual_seed(5) | |
| with torch.no_grad(): # a non-trivial writer LoRA | |
| for p in params: | |
| if p.dim() == 2 and p.shape[1] == 4: | |
| p.copy_(torch.randn(p.shape, generator=g) * 0.2) | |
| return m, params | |
| def _de(resp): | |
| return {k: (resp["value"][k], resp["meta"][f"/{k}"]) for k in ("department", "order_id")} | |
| def test_decide_extract_bitwise_and_r_never_reads_w(): | |
| plain = fill(_plain(), CONTEXT, SCHEMA) | |
| m, params = _moe() | |
| got = fill(m, CONTEXT, SCHEMA) | |
| assert _de(got) == _de(plain) | |
| with torch.no_grad(): | |
| for layer in m.backbone.text.layers: | |
| for p in layer.mlp_w.parameters(): | |
| p.fill_(float("nan")) | |
| poisoned = fill(m, CONTEXT, {**SCHEMA, "properties": { | |
| k: v for k, v in SCHEMA["properties"].items() if k != "reply"}, | |
| "required": ["department", "order_id"]}) | |
| assert _de(poisoned) == _de(plain) | |
| c = moe_params(m) | |
| assert c["w_ffn"] == c["r_ffn"] and c["active_writer"] < c["total"] | |
| def test_w_never_reads_r_ffn_and_rows_are_batch_invariant(): | |
| m, _ = _moe() | |
| ref = fill(m, CONTEXT, SCHEMA) | |
| assert isinstance(ref["value"]["reply"], str) | |
| # generation after the trunk: poison R's FFN only for W calls' benefit -> same text | |
| from deg import model as dm | |
| real = dm._expert | |
| def guarded(layer, sel, name="mlp"): | |
| e = real(layer, sel, name) | |
| if isinstance(sel, dm.WriteRoute): | |
| assert e is layer.mlp_w | |
| else: | |
| assert e is layer.mlp | |
| return e | |
| dm._expert = guarded | |
| try: | |
| assert fill(m, CONTEXT, SCHEMA) == ref | |
| finally: | |
| dm._expert = real | |
| for opts in (FillOptions(), FillOptions(temperature=0.9, seed=3)): | |
| alone = fill(m, CONTEXT, SCHEMA, opts) | |
| both = fill(m, CONTEXT, _with_note(SCHEMA), opts) | |
| assert both["value"]["reply"] == alone["value"]["reply"] | |
| assert both["meta"]["/reply"] == alone["meta"]["/reply"] | |
| def test_save_load_roundtrip(tmp_path): | |
| m, _ = _moe() | |
| with torch.no_grad(): # the checkpoint stores the bodies in bf16 | |
| for k, v in m.backbone.lm.state_dict().items(): | |
| if ".lora_" not in k: | |
| v.copy_(v.to(torch.bfloat16).float()) | |
| ref = fill(m, CONTEXT, SCHEMA) | |
| m.save(tmp_path / "ck") | |
| back = load_model(tmp_path / "ck", device="cpu", dtype=torch.float32, lm=tiny_lm(3), | |
| tokenizer=load_tokenizer(SNAPSHOT)) | |
| assert back.moe == m.moe and back.generate_with == "moe" | |
| assert fill(back, CONTEXT, SCHEMA) == ref | |
| with pytest.raises(ValueError): | |
| load_model(tmp_path / "ck", device="cpu", dtype=torch.float32, lm=tiny_lm(3), | |
| tokenizer=load_tokenizer(SNAPSHOT), generate="base") | |
| def test_one_train_step_moves_only_w(): | |
| m, params = _moe() | |
| for p in params: | |
| p.requires_grad_(True) | |
| m.backbone.grad_ckpt = True | |
| frozen = {k: v.clone() for k, v in m.backbone.lm.state_dict().items() | |
| if ".mlp_w." not in k and ".lora_" not in k} | |
| w0 = [p.detach().clone() for p in params] | |
| items = gen_items(m, [RECORD], {"judge_drop": 0, "budget_drop": 0}) | |
| (group,) = micro_batches(items, 4096, None) | |
| lora = [p.dim() == 2 and p.shape[1] == 4 or p.shape[0] == 4 for p in params] | |
| opt = OffloadAdamW(params, lora, 1e-3, 1e-3, torch.device("cpu")) | |
| nll, _, _, n = batch_losses(m, group) | |
| assert n == items[0].target_len and torch.isfinite(nll) | |
| (nll / n).backward() | |
| opt.step() | |
| for k, v in m.backbone.lm.state_dict().items(): | |
| if k in frozen: | |
| assert torch.equal(v, frozen[k]), k | |
| assert any(not torch.equal(p, q) for p, q in zip(params, w0)) | |