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
| """ | |
| Autopsy of the previous run (logs/experiments/00_prior_run_autopsy.md + figures). | |
| Reads prior_run/eval_samples/step_*.json, which are {prompt: [16 stories]} dumps | |
| taken every 50 steps. Recomputes gates + embedding diversity so the prior run is | |
| measured with EXACTLY the same instruments as the new arms -- otherwise the | |
| "before" number is not comparable to any "after" number. | |
| """ | |
| from __future__ import annotations | |
| import glob | |
| import json | |
| import re | |
| from collections import Counter | |
| from pathlib import Path | |
| import numpy as np | |
| import gates | |
| import logbook | |
| from diversity import group_metrics, l2_normalize | |
| PRIOR = Path(__file__).resolve().parent.parent / "prior_run" / "eval_samples" | |
| EMB_MODEL = "BAAI/bge-base-en-v1.5" | |
| def load_steps() -> dict[int, dict[str, list[str]]]: | |
| out = {} | |
| for f in sorted(glob.glob(str(PRIOR / "step_*.json")), | |
| key=lambda x: int(re.search(r"\d+", Path(x).name).group())): | |
| step = int(re.search(r"\d+", Path(f).name).group()) | |
| out[step] = json.load(open(f)) | |
| return out | |
| def main(): | |
| steps = load_steps() | |
| print(f"loaded {len(steps)} checkpoints: {sorted(steps)}") | |
| from sentence_transformers import SentenceTransformer | |
| enc = SentenceTransformer(EMB_MODEL, device="cuda") | |
| rows = [] | |
| reason_counter: dict[int, Counter] = {} | |
| for step, data in steps.items(): | |
| allres, pw, ld, mm = [], [], [], [] | |
| for prompt, samples in data.items(): | |
| res = [gates.check(s) for s in samples] | |
| allres += res | |
| E = l2_normalize(enc.encode(samples, normalize_embeddings=True, | |
| batch_size=32, show_progress_bar=False)) | |
| gm = group_metrics(E) | |
| pw.append(gm["mean_pairwise_dist"]); ld.append(gm["logdet"]) | |
| mm.append(gm["mean_marginal"]) | |
| n = len(allres) | |
| reason_counter[step] = Counter(r for x in allres for r in x.reasons) | |
| rows.append({ | |
| "step": step, "n": n, | |
| "gate_pass_%": 100 * sum(r.passed for r in allres) / n, | |
| "ends_cleanly_%": 100 * sum(r.completeness for r in allres) / n, | |
| "med_words": float(np.median([r.n_words for r in allres])), | |
| "rep4gram": float(np.mean([r.repeat_4gram_frac for r in allres])), | |
| "pairwise_dist": float(np.mean(pw)), | |
| "logdet": float(np.mean(ld)), | |
| "mean_marginal": float(np.mean(mm)), | |
| }) | |
| for r in rows: | |
| print(r) | |
| # ---------------------------------------------------------------- figures | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| s = [r["step"] for r in rows] | |
| fig, ax = plt.subplots(1, 3, figsize=(15, 4.2)) | |
| ax[0].plot(s, [r["ends_cleanly_%"] for r in rows], "o-", color="#c0392b", lw=2) | |
| ax[0].axhline(80, ls="--", c="gray", lw=1) | |
| ax[0].set_title("Completeness collapse\n(prior run)") | |
| ax[0].set_xlabel("step"); ax[0].set_ylabel("% ending cleanly") | |
| ax[0].set_ylim(-3, 100); ax[0].grid(alpha=.3) | |
| ax[0].text(60, 84, "healthy target", color="gray", fontsize=8) | |
| ax[1].plot(s, [r["pairwise_dist"] for r in rows], "o-", label="pairwise dist", lw=2) | |
| ax[1].set_xlabel("step"); ax[1].set_ylabel("mean pairwise distance", color="C0") | |
| ax2 = ax[1].twinx() | |
| ax2.plot(s, [r["logdet"] for r in rows], "s--", color="C1", label="logdet") | |
| ax2.set_ylabel("logdet volume", color="C1") | |
| ax[1].set_title("Semantic diversity\n(embedding-based)") | |
| ax[1].grid(alpha=.3) | |
| ax[2].plot(s, [r["med_words"] for r in rows], "o-", color="#2c3e50", lw=2) | |
| ax[2].set_title("Median length pinned at the\ntoken wall (~380 words)") | |
| ax[2].set_xlabel("step"); ax[2].set_ylabel("median words") | |
| ax[2].grid(alpha=.3) | |
| plt.tight_layout() | |
| out = logbook.FIGS / "00_prior_run_autopsy.png" | |
| out.parent.mkdir(parents=True, exist_ok=True) | |
| plt.savefig(out, dpi=140) | |
| print("figure ->", out) | |
| # ---------------------------------------------------------------- report | |
| body = f"""# Experiment 00 — Autopsy of the prior run | |
| **Purpose.** Establish what actually happened in the previous `train.py` run | |
| before building anything new, and re-measure it with the *same* instruments the | |
| new arms will use, so "before" and "after" are comparable. | |
| **Source.** `prior_run/eval_samples/step_{{50..300}}.json` — 5 held-out prompts x | |
| 16 samples, dumped every 50 steps. 150 stories total per checkpoint set. | |
| ## Measurements | |
| {logbook.table(rows)} | |
| ## Failure-reason census (gate violations, counted per story) | |
| {logbook.table([{"step": k, **dict(v)} for k, v in reason_counter.items()])} | |
| ## Findings | |
| **1. The run generated no finished stories, at any checkpoint.** | |
| `ends_cleanly` is 0-8% across all six checkpoints. Median length sits in a | |
| tight ~360-400 word band with a hard ceiling, which is the signature of a | |
| `max_new_tokens` wall rather than a stylistic preference. Sampled tails | |
| confirm mid-word cutoff (`"...a place called *The Library of"`). | |
| **2. That silently destroyed the quality reward.** | |
| The prior judge rubric said *"if the story cuts off mid-sentence, cap quality | |
| at 4."* When ~100% of a group hits the same cap, within-group quality variance | |
| goes to ~0. GRPO normalizes advantages within the group, so an | |
| (almost) constant reward column produces an (almost) zero advantage. The | |
| quality objective contributed no gradient for the entire run. | |
| **3. The group-diversity term was structurally incapable of contributing.** | |
| `quality_reward` added `0.5 * group_diversity`, a single scalar identical for | |
| every sample in the group. Its within-group std is exactly 0, so after | |
| normalization it contributes exactly 0 to every advantage. It cost one judge | |
| call per step and bought nothing. (This is the failure mode recorded in the | |
| brief's section 0.1, confirmed here in situ.) | |
| **4. Net effect: the run optimized novelty almost alone.** | |
| With quality flat and group-diversity annihilated, the only surviving gradient | |
| came from `novelty_reward` — and its quality gate (`q >= 5`) was itself | |
| degenerate, because quality was pinned near the truncation cap. That is an | |
| unconditioned novelty objective, which is precisely the configuration known to | |
| produce surface-level gaming. | |
| **5. The observable signature matches that diagnosis.** | |
| Unique 5-word openers rise 0.68 -> 0.96 while type-token ratio *falls* | |
| 0.381 -> 0.343. The model learned to vary the first few words while its | |
| vocabulary narrowed — diversification at the surface, homogenization | |
| underneath. Heavy italic emphasis persists at ~7 instances/story throughout, | |
| the exact tic the rubric explicitly tried to punish. | |
| **6. A latent bug would have suppressed completeness credit even after a fix.** | |
| `completeness_reward` tested `text[-1] in ".!?\\"'"` — straight quotes only. | |
| These generations use curly U+201D constantly, so a story ending `...gone.”` | |
| scored 0.3 instead of 1.0. | |
| ## Consequences for the new design | |
| - Token budget raised to 1280 new tokens (a 500-word story is ~650-700 Qwen | |
| tokens; the planned 768 left no headroom and would have re-created the bug). | |
| Nothing truncates the story anywhere downstream — not the judge, not the | |
| embedder. Overrun is *detected* by a word-count gate, never chopped. | |
| - System prompt now carries an explicit word budget and ending instruction. | |
| - No set-level scalar is ever used as a per-sample reward. All diversity credit | |
| is per-sample by construction (deviation `d_i`, marginal `m_i`). | |
| - `finish_reason == "length"` from vLLM is captured and gates on it directly, | |
| rather than inferring truncation from punctuation. | |
| - Terminal-punctuation detection handles curly quotes, ellipses and trailing | |
| markdown emphasis. | |
| - Judge scores one story at a time on an absolute rubric, so `tau` is | |
| meaningful and cross-model eval comparisons are valid. | |
|  | |
| """ | |
| p = logbook.write_report("00_prior_run_autopsy", body) | |
| print("report ->", p) | |
| logbook.note("prior-run autopsy complete", | |
| f"0-8% completion across all checkpoints; quality signal was " | |
| f"degenerate. Report: {p}") | |
| json.dump(rows, open(logbook.LOGS / "prior_run_metrics.json", "w"), indent=2) | |
| if __name__ == "__main__": | |
| main() | |