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
Diversity-aware post-training for creative story generation (Qwen3-4B)
Research artifacts for a controlled study of whether RL post-training can raise the semantic diversity of short-story generation without paying for it in quality.
Everything here is measured, not asserted: the repo carries the LoRA adapters, the scored generation pool they were selected from, the per-step reward telemetry, the judge-calibration evidence, the experiment reports, and the source that produced all of it.
β οΈ Work in progress. Only E0 (quality-only baseline) has finished its full 300-step run. E1 was still training when this snapshot was uploaded β its checkpoints stop at step 200 of 300. E2, E3 and E4 have not been trained yet; their configs are present, their adapters are not. No cross-arm comparison exists yet, and none should be read into this repo.
The finding this study is built around
Sampling the base policy 16 times per prompt over 1,000 prompts (16,000 scored
stories, outputs/pool_4b/) gives a mean effective rank of 2.006 out of a ceiling of
16. Sixteen independent samples of the same prompt span roughly two effective
directions in embedding space; mean pairwise distance is 0.132, i.e. same-prompt stories
sit at about 0.87 cosine similarity.
Quality-only RL does not fix this. E0 ran 300 steps of GRPO on the judge's quality score
alone and moved judge quality +0.30 (6.557 β 6.860, first vs. last quarter of the run)
while the diversity statistics stayed flat (mean pairwise deviation 0.1354 β 0.1391;
mean group log-det β12.81 β β12.64). The held-out checkpoint study
(outputs/ckpt_study/, 10 prompts Γ 6 samples per checkpoint) tells the same story:
| step | judge quality | effective rank | deviation |
|---|---|---|---|
| 0 (base) | 6.65 | 1.677 | 0.130 |
| 100 | 6.78 | 1.655 | 0.128 |
| 200 | 6.75 | 1.700 | 0.135 |
| 300 | 7.03 | 1.712 | 0.139 |
Craft improves; the model still writes the same story six times. That gap is what the diversity arms exist to close.
Method
Base policy. Qwen/Qwen3-4B-Instruct-2507, LoRA r=32, alpha=64, dropout 0.0 on all
attention and MLP projections (q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj).
Online arms. GRPO via TRL 1.10 with multi_objective_aggregation="normalize_then_sum",
which is a line-for-line implementation of GDPO (arXiv 2601.05242): each reward channel is
normalized within its prompt group before the channels are summed and the advantage is
normalized batch-wise. Generation is vLLM colocated in the training process. G=8 samples per
prompt, generation batch 16 (2 prompts Γ 8), max_completion_length=1024, lr 3e-5 with
constant_with_warmup, beta (KL) 0.02, 300 steps. Everything ran on a single RTX 5090 (32 GB) β
the 32 GB budget is what forced the 4B policy, since colocated GRPO needs two resident copies of
the weights.
Offline arms. DPO over preference pairs mined from the same 16k pool (beta 0.1, sigmoid loss, lr 5e-6, epochs chosen for optimizer-step parity across arms).
Reward channels.
qualityβ judge score, per story.deviationd_iβ mean embedding distance from a sample to the rest of its group (BAAI/bge-base-en-v1.5), credited only if judge quality β₯ tau=5.marginalm_iβ the sample's marginal contribution to the group's log-determinant (a set-level diversity volume), z-scored within group.
Every diversity credit is per-sample by construction. A single set-level scalar shared by a
whole group has zero within-group variance and therefore contributes exactly nothing to a GRPO
advantage β the failure mode diagnosed in prior_run/ and written up in
logs/experiments/00_prior_run_autopsy.md.
Judge. deepseek-v4-flash-0731 via OpenRouter, scoring one story at a time on an absolute
rubric (not batched relative scoring), with reasoning disabled. Calibrated before any training
against four cells with known ordering: detects incoherence (Ξ 2.25), detects repetition (Ξ 4.67),
not saturated (sd 0.76). See logs/experiments/01_judge_calibration.md.
Programmatic gates. Completeness, 150β600 word window, 4-gram / line loop detection and
finish_reason == "length" are checked deterministically before a story is ever judged, so
truncated or degenerate text cannot earn reward through any channel.
The arms
| arm | type | reward / objective | status in this repo |
|---|---|---|---|
| E0-baseline | GRPO | quality only β the collapse control | β
complete, 300 steps + final |
| E1-div-individual | GRPO | quality + pairwise deviation d_i (alpha 0.5, gated at tau=5) |
π‘ partial, steps 50β200 of 300 |
| E2-div-group | GRPO | quality + deviation + log-det marginal m_i (alpha 0.5, gamma 0.5) |
β¬ config only, not trained |
| E3-multipos | DPO | multi-positive, deviation-weighted loss; 4 greedy-diverse chosens vs. rotating negatives (3,593 rows) | β¬ config + pairs only |
| E4a-divpo-emb | DPO | faithful DivPO, embedding-deviation criterion (956 rows, rho=7) | β¬ config + pairs only |
| E4b-divpo-prob | DPO | faithful DivPO, lowest length-normalized logprob among quality β₯ rho | β¬ config + pairs only |
Repository layout
outputs/
E0-baseline/ LoRA adapters: checkpoint-{50..300} + final
reward_history.json per-step reward telemetry (300 steps)
trl_log_history.json full TRL log history
judge_cost.json judge call/token/USD accounting
E1-div-individual/ same structure, checkpoints 50-200 (run in progress)
pool_4b/ pool_train.jsonl 16,000 scored base-policy stories
emb_train.npy their bge-base-en-v1.5 embeddings
summary_train.json pool-level summary stats
pairs_4b/ divpo_emb_train.jsonl, divpo_prob_train.jsonl,
multipos_train.jsonl, pair_stats_train.json
ckpt_study/E0-baseline/ stories.md, raw.json, metrics.csv (per-checkpoint story dumps)
logs/
experiments/ the written reports (start here)
figures/ plots referenced by the reports
configs/ one YAML per arm, with the reasoning for every knob in comments
src/ all Python: training, rewards, diversity, judge, gates, eval, tests
data/ train/eval prompt splits + split metadata
prior_run/ the previous, failed run's artifacts (see autopsy)
Read the reports in this order: 00_prior_run_autopsy.md β 01_judge_calibration.md β
02_setup_and_deviations.md β 03_pool_4b_baseline.md β 04_E0_checkpoint_story_study.md β
E0-baseline.md.
Data schemas
outputs/pool_4b/pool_train.jsonl β one story per line:
prompt_id, prompt, idx, text, n_words, n_tokens, finish_reason, gate_passed, gate_reasons, ends_cleanly, quality, novelty, deviation, marginal, group_logdet, mean_logprob, cumlogprob.
Row i of emb_train.npy corresponds to line i of the JSONL.
outputs/pairs_4b/*.jsonl β DPO rows: prompt_id, prompt, chosen, rejected (+ per-row weight
for the multi-positive set).
data/ β 1,000 train and 50 eval prompts (seed 42), filtered to 10β60 words from 272,600 scanned;
split_meta.json also carries the exact system prompt used for every generation in the study.
Using an adapter
Each checkpoint directory is a standard PEFT adapter (adapter_model.safetensors +
adapter_config.json, ~253 MB each).
from huggingface_hub import snapshot_download
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
path = snapshot_download(
"Mercity/creative-writing-llm",
allow_patterns="outputs/E0-baseline/final/*",
)
tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-4B-Instruct-2507")
base = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-4B-Instruct-2507", dtype="auto", device_map="auto"
)
model = PeftModel.from_pretrained(base, f"{path}/outputs/E0-baseline/final")
system = (
"You are a fiction writer. Write a complete short story of 200-500 words responding "
"to the writing prompt.\nWrite only the story: no title, no preamble, no commentary, "
"no author's note.\nFinish inside the word budget. The story must reach a real ending, "
"not stop mid-scene."
)
msgs = [{"role": "system", "content": system},
{"role": "user", "content": "A lighthouse keeper receives a letter addressed to the sea."}]
ids = tok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt").to(model.device)
out = model.generate(ids, max_new_tokens=1024, temperature=1.0, top_p=1.0, do_sample=True)
print(tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True))
Swap outputs/E0-baseline/final for outputs/E0-baseline/checkpoint-150,
outputs/E1-div-individual/checkpoint-200, etc. The study sampled at T=1.0 during training and
T=0.9 / top_p=0.95 for the checkpoint story study.
Selected numbers
Base-policy pool (logs/experiments/03_pool_4b_baseline.md, n=16,000):
| metric | value |
|---|---|
| gate pass rate | 0.992 |
| judge quality (mean Β± sd) | 6.447 Β± 0.833 |
| mean pairwise deviation | 0.1321 |
| mean group log-det | β30.42 |
| mean effective rank (ceiling 16) | 2.006 Β± 0.323 |
| corr(quality, effective rank) | β0.108 |
| corr(deviation, effective rank) | 0.992 |
Quality and diversity are largely independent across prompts (r = β0.108), so a method that raises diversity without lowering quality is exploiting existing slack rather than defying a tradeoff.
E0 run (logs/experiments/E0-baseline.md, 300 steps, 4,800 stories judged, verdict healthy):
| metric | early (first 25%) | late (last 25%) | Ξ |
|---|---|---|---|
| judge quality (passing) | 6.557 | 6.860 | +0.303 |
| mean deviation | 0.1354 | 0.1391 | +0.004 |
| mean log-det | β12.81 | β12.64 | +0.165 |
| gate pass | 0.9925 | 0.9875 | β0.005 |
| policy entropy | 1.2535 | 1.2274 | β0.026 (β2.1%) |
No entropy collapse, and the reward-hacking trip conditions (diversity up while quality or validity falls) were not tripped. Judge cost for the whole E0 run: $0.84 over 5,109 calls.
Qualitative (logs/experiments/04_E0_checkpoint_story_study.md): the collapse is a tonal
monoculture, not a lexical one β 92β95% of stories carry solemn/elegiac vocabulary against ~15%
comic, even on explicitly comic prompts, and verbatim opening duplication rises with
quality-only training. This predicts that n-gram metrics (distinct-4, self-BLEU) will separate the
arms far less than effective rank does.
Environment
torch 2.13.0+cu130 Β· vLLM 0.27.1 Β· transformers 5.15.0 Β· TRL 1.10.0 Β· PEFT 0.20.0 Β·
sentence-transformers 5.7.0 (BAAI/bge-base-en-v1.5) Β· single RTX 5090, 32.6 GB, sm_120.
FlashInfer is disabled (it misdetects sm_120); Liger is disabled because it suppresses TRL's
entropy metric.
Caveats and honest notes
- E1 is mid-flight. Its checkpoints are a snapshot at step 200/300 and its reports do not exist yet. E2/E3/E4 are unrun. Do not read arm comparisons out of this repo.
04_E0_checkpoint_story_study.mddocuments an earlier, partial 163-step E0 run (at the pre-correction lr 3e-6).outputs/ckpt_study/E0-baseline/metrics.csvis the regenerated version covering all checkpoints of the final 300-step run; where they disagree, trust the CSV.- Learning rate deviates from the plan (3e-5, not 3e-6): at 3e-6 the adapter was effectively
frozen β KL pinned near 8e-4 for 171 steps with every metric inside its noise band. The evidence
is recorded in the config comments and
02_setup_and_deviations.md. - Optimizer/scheduler/RNG state is not included for E1's checkpoints. It is resume-only state,
is rotated away by
save_total_limit, and none of it is needed to load or evaluate an adapter. prior_run/train.pyis a redacted copy β the original contained a hard-coded OpenRouter API key, replaced here with a placeholder. Nothing else in the file was changed.- Judge is not perfect. It fails the truncation-sensitivity check (AβB = 1.33 against a 2.0 bar), which is tolerated only because truncation is caught deterministically by the gates before any story reaches the judge. Pool-scoring judge failure rate was 0.45%.
- Every deviation from the original plan is enumerated in
logs/experiments/02_setup_and_deviations.mdrather than discovered in a footnote.
References
- GDPO β group-wise reward normalization for multi-objective RL, arXiv 2601.05242 (Liu et al., NVIDIA)
- DivPO β Lanchantin et al., 2025 (diverse preference optimization)
- Effective rank β Roy & Vetterli, the exp-entropy of the Gram spectrum, used here as the primary continuous mode-count metric (k-means + silhouette was measured to be unable to separate collapse from spread and was demoted)
License
Apache-2.0, matching the Qwen/Qwen3-4B-Instruct-2507 base model. The prompt splits derive from a
public writing-prompts corpus; stories in outputs/pool_4b/ are model generations.
Status (updated β E0 and E1 both complete, 300 steps each)
| arm | status | result |
|---|---|---|
| E0 quality-only GRPO | β complete, 300 steps | quality +0.303, diversity flat |
| E1 + pairwise deviation | β complete, 300 steps | diversity 5β6Γ E0's gain, quality +0.243 |
| E2 div-grpo-group (log-det marginal) | βΈ stopped at step 4, config ready | β |
| E3 multi-positive weighted DPO | βΈ pairs built (3593 rows), not trained | β |
| E4a/E4b DivPO emb/prob | βΈ pairs built (956 rows each), not trained | β |
Held-out evaluation (30 prompts Γ 16 samples = 480 stories/model)
| model | quality | eff_rank (of 16) | pairwise | logdet | distinct-4 | self-BLEU |
|---|---|---|---|---|---|---|
| base | 6.472 | 1.891 | 0.1179 | β32.46 | 0.9378 | 0.2875 |
| E0 quality-only | 6.716 | 1.957 | 0.1259 | β31.59 | 0.9485 | 0.2635 |
| E1 +deviation | 6.864 | 2.081 | 0.1390 | β30.08 | 0.9464 | 0.2625 |
E1 wins on both axes: vs base it gains 2.9Γ E0's effective-rank improvement and 1.6Γ E0's quality improvement. Not a diversity-for-quality trade.
Key methodological finding: embedding metrics separate the arms by 2.9Γ; n-gram metrics (distinct-4, self-BLEU) do not separate them at all β distinct-4 actually rates E0 higher. The collapse is tonal/structural, not lexical.
Key finding on LLM judges: across 15,870 scored stories,
corr(judge "novelty", a story's actual embedding deviation) = +0.057, while
corr(judge "novelty", judge quality) = +0.799. An LLM judge's novelty score is
~80% a restatement of quality and carries almost no information about semantic
distinctiveness.
See REPORT.pdf for the full write-up (metrics, figures, story examples,
qualitative read) and NOTES.md for the complete engineering log including
every bug and every retracted claim.
- Downloads last month
- -
Model tree for Mercity/creative-writing-llm
Base model
Qwen/Qwen3-4B-Instruct-2507