DEG-5B-E2B-1018 / source /tests /test_filler.py
gcoderw's picture
Rename package to deg (DEG_ env vars, deg.json); note upcoming modalities
16d760d verified
Raw History Blame Contribute Delete
7.11 kB
"""Filler augmentation for long-context anchor training (SPEC.md §12.4), on a tiny random
Qwen3.5 (CPU): a short record embedded in unrelated filler text keeps every gold span on
its own text and every target of the plain example; negative (filler only) examples
train top-level anchored arrays as empty; the corpus uses train-split documents only
and refuses ExtractBench; micro-batches materialize filler draws before packing (the
memory planner sees real examples) within the context limit.
"""
from __future__ import annotations
import json
import random
import zlib
from pathlib import Path
import pytest
torch = pytest.importorskip("torch")
pytest.importorskip("transformers")
from test_anchors import NESTED, TEAM_CONTEXT # noqa: E402
from test_array_decode import _team_record # noqa: E402
from test_model import SNAPSHOT, tiny_lm # noqa: E402
from deg.model import load_model, load_tokenizer # noqa: E402
FILLER_WORDS = ("The quarterly filing discusses liquidity, covenants and segment revenue "
"across fiscal periods; management notes risks in supply and demand. ")
@pytest.fixture(scope="module")
def model():
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(),
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 _val(doc_id: str) -> bool:
from deg.datagen.v3 import VAL_MODULUS
return zlib.crc32(doc_id.encode()) % VAL_MODULUS == 0
@pytest.fixture()
def corpus(tmp_path: Path):
from deg.train import FillerCorpus
(tmp_path / "text").mkdir()
rows = []
val_id = next(f"doc-{i}" for i in range(10_000) if _val(f"doc-{i}"))
for i, doc_id in enumerate(["doc-a", "doc-b", val_id]):
body = (f"[{doc_id}] " if doc_id == val_id else "") + FILLER_WORDS * (40 + 10 * i)
tp = tmp_path / "text" / f"{doc_id}.txt"
tp.write_text(body)
rows.append({"id": doc_id, "text_path": str(tp)})
(tmp_path / "docs.jsonl").write_text("".join(json.dumps(r) + "\n" for r in rows))
c = FillerCorpus(tmp_path)
assert {d for d, _ in c.docs} == {"doc-a", "doc-b"} # the v3 val doc is never filler
return c
def _targets(ex, text: str):
"""Every training target as (kind, ptr, gold text), independent of token positions."""
def at(i: int, j: int) -> str:
return text[ex.ctx.offsets[i][0]:ex.ctx.offsets[j][1]]
out = []
for it in ex.items:
if it.kind == "anchor":
out.append(("anchor", it.ptr, tuple(at(t, t) for t in it.anchors)))
elif it.kind == "extract":
out.append(("extract", it.ptr, tuple(
at(a, b) if a >= 0 else None for a, b in zip(it.starts, it.ends))))
else:
out.append((it.kind, it.ptr, tuple(it.target or ()), it.aux))
return sorted(out, key=repr)
# ----------------------------------------------------------------------- filler
def test_corpus_refuses_extractbench(tmp_path):
from deg.train import FillerCorpus
d = tmp_path / "ExtractBench"
d.mkdir()
(d / "docs.jsonl").write_text("")
with pytest.raises(ValueError):
FillerCorpus(d)
def test_filler_record_keeps_every_target_on_its_text(model, corpus):
from deg.train import deg_example, filler_record, record_text
rec = _team_record()
plain = deg_example(model, rec, random.Random(0), shuffle_p=0.0, anchor_count=True)
want = _targets(plain, record_text(rec))
assert any(t[0] == "anchor" for t in want) and any(t[0] == "count" for t in want)
seen_offsets = set()
for seed in range(6):
rng = random.Random(seed)
aug = filler_record(model, rec, corpus, rng, 200)
text = aug["context"]
assert TEAM_CONTEXT in text and len(text) > len(TEAM_CONTEXT) + 200
shift = text.index(TEAM_CONTEXT)
seen_offsets.add(shift)
for k, (a, b) in aug["spans"].items():
ra, rb = rec["spans"][k]
assert (a - shift, b - shift) == (ra, rb) and text[a:b] == TEAM_CONTEXT[ra:rb]
ex = deg_example(model, aug, random.Random(0), shuffle_p=0.0, anchor_count=True)
assert len(ex.ctx.ids) > len(plain.ctx.ids) + 150
assert _targets(ex, text) == want # same targets, same gold text
assert len(seen_offsets) > 2 # random placement, including the middle
def test_negative_example_trains_top_level_anchors_empty(model, corpus):
from deg.train import negative_example
ex = negative_example(model, _team_record(), corpus, random.Random(0), 150,
anchor_count=True)
assert ex is not None and ex.aug == "negative"
assert 140 <= len(ex.ctx.offsets) <= 151
anchors = [it for it in ex.items if it.kind == "anchor"]
counts = [it for it in ex.items if it.kind == "count"]
assert [it.ptr for it in anchors] == ["/teams"] and anchors[0].anchors == []
assert len(counts) == 1 and counts[0].aux and counts[0].target.index(1.0) == 0
assert not any(it.kind in ("extract", "choice", "score", "noul") for it in ex.items)
min1 = {**NESTED, "properties": {"teams": {**NESTED["properties"]["teams"],
"minItems": 1}}}
assert negative_example(model, {**_team_record(), "schema": min1}, corpus,
random.Random(0), 150) is None
def test_filler_draws_materialize_before_packing_within_the_limit(model, corpus):
from deg.train import (
FillerDraw,
Source,
batches,
deg_example,
filler_example,
)
rec = _team_record()
ex = deg_example(model, rec, random.Random(0), shuffle_p=0.0, anchor_count=True)
ex.rec = rec
src = Source(Path("x.jsonl"))
src.pool = [ex]
max_ctx = 300
seen: list[list] = []
def fits(batch):
seen.append(list(batch))
return True
def augment(e, r):
return FillerDraw(e, r.randint(len(e.ctx.ids) + 64, max_ctx), r.random() < 0.3)
def materialize(d, r):
return filler_example(model, d, corpus, r, max_ctx, anchor_count=True)
gen = batches([src], None, random.Random(0), 10_000, 0.0, 4, draw=8,
materialize=materialize, fits=fits, augment=augment)
kinds = set()
for _ in range(6):
for e in next(gen):
assert len(e.ctx.ids) <= max_ctx
kinds.add(e.aug)
assert kinds == {"filler", "negative"}
assert seen and all(not isinstance(e, FillerDraw) for b in seen for e in b)
def test_filler_example_falls_back_to_the_plain_example(model, corpus):
from deg.train import FillerDraw, deg_example, filler_example
rec = _team_record()
ex = deg_example(model, rec, random.Random(0))
ex.rec = rec
# no room for filler under the context limit -> the plain example
got = filler_example(model, FillerDraw(ex, len(ex.ctx.ids) + 500), corpus,
random.Random(0), len(ex.ctx.ids) + 3)
assert got is ex