Text Generation
PEFT
Safetensors
lora
trl
grpo
gdpo
dpo
divpo
rlhf
diversity
creative-writing
mode-collapse
Instructions to use Mercity/creative-writing-llm with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use Mercity/creative-writing-llm with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 6,604 Bytes
cbc33fe | 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 | """
Synthetic tests for E3/E4 preference-pair construction. Catches logic errors now
rather than four hours into the pipeline.
Most important: test_divpo_prob_picks_least_probable_as_chosen. DivPO's
probability variant inverts the usual intuition -- the CHOSEN story is the one
the model found LEAST likely. Getting that backwards would train the policy
straight into the greedy mode while the logs looked entirely normal.
"""
import numpy as np
from build_pairs import build_divpo, build_multipos, normalize_weights
def make_item(pid, idx, quality, dev, meanlp, passed=True, text=None):
return {"prompt_id": pid, "idx": idx, "prompt": f"prompt-{pid}",
"text": text or f"story-{pid}-{idx}", "quality": quality,
"deviation": dev, "mean_logprob": meanlp, "gate_passed": passed,
"marginal": -1.0, "n_words": 300}
def simple_pool():
"""One prompt, 6 stories spanning quality/deviation/logprob."""
return {"p0": [
make_item("p0", 0, 8.0, 0.40, -1.50), # high q, MOST diverse, least probable
make_item("p0", 1, 7.0, 0.20, -0.80), # high q, low dev -> "competent cliche"
make_item("p0", 2, 6.5, 0.30, -1.10),
make_item("p0", 3, 3.0, 0.35, -0.60), # low q, HIGH dev -> must NOT be chosen
make_item("p0", 4, 2.0, 0.05, -0.40), # low q, least diverse, MOST probable
make_item("p0", 5, 1.0, 0.10, -0.90, passed=False),
]}
# --------------------------------------------------------------------- E4
def test_divpo_emb_chosen_is_most_diverse_above_rho():
rows, st = build_divpo(simple_pool(), "emb", rho=6.0)
assert st["n_rows"] == 1
r = rows[0]
assert r["chosen"] == "story-p0-0", r
assert r["chosen_quality"] >= 6.0
def test_divpo_emb_never_chooses_a_diverse_but_low_quality_story():
"""Story 3 has high deviation but quality 3.0. Quality gating must exclude
it -- this is the 'different because it is worse' failure."""
rows, _ = build_divpo(simple_pool(), "emb", rho=6.0)
assert rows[0]["chosen"] != "story-p0-3"
def test_divpo_emb_rejected_is_least_diverse_below_rho():
rows, _ = build_divpo(simple_pool(), "emb", rho=6.0)
assert rows[0]["rejected"] == "story-p0-4", rows[0]
def test_divpo_prob_picks_least_probable_as_chosen():
"""Most diverse == LOWEST length-normalized logprob; least diverse ==
HIGHEST (the near-greedy sample). Inverting this trains toward the mode."""
rows, _ = build_divpo(simple_pool(), "prob", rho=6.0)
r = rows[0]
assert r["chosen"] == "story-p0-0", f"chosen should be least probable: {r}"
assert r["rejected"] == "story-p0-4", f"rejected should be most probable: {r}"
assert r["chosen_meanlp"] < r["rejected_meanlp"]
def test_divpo_skips_prompt_with_no_qualifying_chosen():
pool = {"p0": [make_item("p0", i, 3.0, 0.2, -1.0) for i in range(4)]}
rows, st = build_divpo(pool, "emb", rho=6.0)
assert rows == [] and st["no_chosen"] == 1 and st["skip_rate"] == 1.0
def test_divpo_skips_prompt_with_no_rejected():
pool = {"p0": [make_item("p0", i, 9.0, 0.2 + 0.01 * i, -1.0) for i in range(4)]}
rows, st = build_divpo(pool, "emb", rho=6.0)
assert rows == [] and st["no_rejected"] == 1
def test_divpo_gate_failures_are_eligible_rejects():
"""A gate-failing story is legitimately in the low-quality tail."""
pool = {"p0": [make_item("p0", 0, 8.0, 0.4, -1.5),
make_item("p0", 1, 9.0, 0.3, -1.2),
make_item("p0", 2, 0.0, 0.01, -0.3, passed=False)]}
rows, st = build_divpo(pool, "emb", rho=6.0)
assert st["n_rows"] == 1 and rows[0]["rejected"] == "story-p0-2"
def test_divpo_is_one_row_per_prompt():
pool = {f"p{i}": simple_pool()["p0"] for i in range(5)}
rows, st = build_divpo(pool, "emb", rho=6.0)
assert len(rows) == 5 == st["n_rows"]
assert all(r["weight"] == 1.0 for r in rows), "DivPO must be unweighted"
# --------------------------------------------------------------------- E3
def _emb_for(pool):
"""Deterministic embeddings + row index aligned to pool iteration order."""
idx, vecs, i = {}, [], 0
rng = np.random.default_rng(0)
for pid, items in pool.items():
for it in items:
idx[(pid, it["idx"])] = i
v = rng.standard_normal(32)
vecs.append(v / np.linalg.norm(v))
i += 1
return np.array(vecs, dtype=np.float32), idx
def test_multipos_emits_multiple_chosen_rows_per_prompt():
pool = simple_pool()
emb, idx = _emb_for(pool)
rows, st = build_multipos(pool, emb, idx, q_keep=5.0, k=4, lam=1.0)
assert st["ok"] == 1
assert 2 <= len(rows) <= 4, f"expected up to 4 chosen rows, got {len(rows)}"
assert all(r["chosen_quality"] >= 5.0 for r in rows)
def test_multipos_rotates_negatives():
"""Don't hammer one rejected across all rows."""
pool = simple_pool()
emb, idx = _emb_for(pool)
rows, _ = build_multipos(pool, emb, idx, q_keep=5.0, k=4, lam=1.0)
if len(rows) >= 2:
assert len(set(r["neg_type"] for r in rows)) == 2, \
f"negatives not rotated: {[r['neg_type'] for r in rows]}"
def test_multipos_weight_is_chosen_deviation():
pool = simple_pool()
emb, idx = _emb_for(pool)
rows, _ = build_multipos(pool, emb, idx, q_keep=5.0, k=4, lam=1.0)
for r in rows:
assert abs(r["weight"] - r["chosen_dev"]) < 1e-9
def test_multipos_skips_prompt_with_too_few_survivors():
pool = {"p0": [make_item("p0", i, 2.0, 0.2, -1.0) for i in range(6)]}
emb, idx = _emb_for(pool)
rows, st = build_multipos(pool, emb, idx, q_keep=5.0, k=4, lam=1.0)
assert rows == [] and st["skipped_no_survivors"] == 1
def test_normalize_weights_gives_mean_one():
rows = [{"weight": w} for w in (0.1, 0.2, 0.3, 0.8)]
normalize_weights(rows)
w = np.array([r["weight"] for r in rows])
assert abs(w.mean() - 1.0) < 1e-9
# relative ordering preserved -> it reweights, it does not rescale the LR
assert np.all(np.diff(w) > 0)
def test_normalize_weights_handles_all_zero():
rows = [{"weight": 0.0} for _ in range(3)]
normalize_weights(rows)
assert all(r["weight"] == 0.0 for r in rows)
if __name__ == "__main__":
import sys, traceback
fns = [(n, f) for n, f in sorted(globals().items())
if n.startswith("test_") and callable(f)]
bad = 0
for n, f in fns:
try:
f(); print(f" PASS {n}")
except Exception:
bad += 1; print(f" FAIL {n}"); traceback.print_exc()
print(f"\n{len(fns)-bad}/{len(fns)} passed")
sys.exit(1 if bad else 0)
|