Skip to content

De-voice writer SFT pairs and widen the ship-gate holdout - #48

Merged
telivity-otaip merged 3 commits into
mainfrom
feature/writer-lora-devoice
Jul 31, 2026
Merged

De-voice writer SFT pairs and widen the ship-gate holdout#48
telivity-otaip merged 3 commits into
mainfrom
feature/writer-lora-devoice

Conversation

@telivity-otaip

Copy link
Copy Markdown
Contributor

Summary

The writer SFT built each row's brief by extracting sentences from the post it
was trying to teach the model to write. Measured on the corpus, the share of
brief words sitting inside a 5-gram of their own target had a median of 1.0,
and 126 of 129 rows would themselves have tripped the brief_echo_reject limit
the gate uses to disqualify a draft. Copying the prompt forward was a winning
strategy for that objective.

This PR makes the pairs (D(y), y) and gates them on that property.

De-voicing operator (devoice.py)

Deterministic and inspectable — no model in the loop, since an LLM flattener is
exactly the component that can leak cadence back into the input.

  • kept: named entities, evidence figures, claim vocabulary, clause order
  • removed: second-person address, contractions, emphasis punctuation,
    shouted words, sentence-initial conjunctions, discourse markers, one-line
    fragment rhythm, and the article/copula/preposition scaffolding that separates
    a written sentence from a jotted note

Negation and comparison words are deliberately never dropped: removing not
would not de-voice a claim, it would reverse it.

The operator ships with its verifier. Each pair is measured on the existing
gate_pair cadence axes, checked for entities or figures the operator itself
introduced, and dropped unless the brief's overlap with the post stays under a
cap. Fail-closed — a row that cannot be moved is not trained on.

brief→post 5-gram copy ratio median p90 max
before 1.0 1.0 1.0
after 0.147 0.263 0.333

build-writer-sft now reports the ratio distribution and a per-reason drop
breakdown instead of one opaque skip count.

Widened holdout carve (select-writer-holdouts)

The previous gate ran on three holdouts. Three paired comparisons cannot reach
any significance threshold — a clean sweep is still p=0.125 — so the result
could not distinguish a regression from a coin flip.

The carve reserves a share of the briefable pool (25%, clamped to 12–24),
orders candidates by a stable digest of the piece id so it is reproducible, and
keeps previously carved ids pinned so runs stay comparable. Pieces that cannot
produce a brief are excluded: neither arm could be scored on them.

index-voice --from-carve reads the ids from the carve file, since retyping
twenty flags is how a holdout quietly re-enters retrieval.

Committed ship gate (eval-writer-adapter)

Both arms see the same de-voiced brief and the same exemplars; only the adapter
differs. An adapter is kept only when all three hold:

  1. it wins more holdouts than it loses
  2. the margin clears a one-sided sign test
  3. it is not disqualified more often than the arm it replaces

Weights are loaded once per arm rather than per draft. --archive-on-fail moves
a rejected adapter aside so the write path resolves back to adapter=none.

Also

  • train --detach runs a train in its own session via start_new_session.
    Shell-level setsid is util-linux and absent on macOS, where a launcher that
    reaches for it fails before Python starts.
  • Writer train recipe separated from the translator defaults: 16 layers, LoRA
    rank 16, lr 3e-5, 10 epochs. Rank and learning rate are plumbed through the
    chunk worker and recorded in the checkpoint meta.
  • release_mlx_memory now honours the MLX opt-in gate. A Metal-less session
    aborts inside metal::load_device via C++ terminate, which the surrounding
    except Exception cannot catch.
  • mine_brief_from_holdout derives its minimum source length from the caller's
    overlap budget instead of a fixed constant.

Test plan

  • pytest — 317 passing, Contoso fixtures only
  • ruff check src/ tests/
  • build-writer-sft on a real corpus: pair copy ratio median 1.0 → 0.147
  • select-writer-holdouts + index-voice --from-carve: holdouts confirmed
    absent from retrieval
  • train --writer on the de-voiced SFT, then eval-writer-adapter on the
    widened holdout — adapter kept only if it clears the bar

No corpus, posts, or adapters are committed; profile data stays under
~/.personality-protect/.

Made with Cursor

Writer SFT briefs were mined verbatim from their own target, so the input
sat inside the output (5-gram copy ratio median 1.0) and copying the prompt
forward was a winning strategy for the objective. Add a deterministic
de-voicing operator so rows are (D(y), y): entities, figures and claim
vocabulary are preserved while second-person address, contractions, emphasis,
discourse markers, fragment rhythm and connective scaffolding are removed.

Pair construction is gated rather than merely built. Every row is measured
against the shipped pair gate for cadence movement, checked for entities or
figures the operator invented, and dropped unless what the brief shares with
the post falls under a cap. Copy ratio drops from median 1.0 to 0.15.

Also here:
- select-writer-holdouts: deterministic, pinned-compatible holdout carve
  sized as a share of the briefable pool. A three-item gate cannot reach any
  significance threshold; a sign test needs more comparisons than that.
- eval-writer-adapter: committed ship gate for RAG+adapter vs RAG-alone,
  loading each arm's weights once. Keeps an adapter only on a majority win
  that clears a one-sided sign test and does not raise the disqualification
  rate.
- index-voice --from-carve: read holdout ids from the carve file instead of
  retyping them as flags.
- train --detach: portable detached launch via start_new_session, so an
  unattended run does not depend on a shell staying open (and does not
  depend on setsid, which macOS does not ship).
- Writer train recipe: 16 layers, LoRA rank 16, lr 3e-5, 10 epochs, with
  rank and learning rate plumbed through the chunk worker and recorded in
  the checkpoint meta.
- release_mlx_memory now honours the MLX opt-in gate; a Metal-less session
  aborts in C++ where except Exception cannot catch it.

Co-authored-by: Cursor <cursoragent@cursor.com>
@telivity-otaip

Copy link
Copy Markdown
Contributor Author

Ran the full pass end to end on a real corpus.

Pairs — 60 gated (D(y), y) rows from 274 posts. Brief→post 5-gram copy
ratio: median 0.147, p90 0.263, max 0.333 (was median 1.0). Drops by reason:
137 under the 50-word floor, 29 over the copy cap, 20 holdouts, 10 fragment-gap,
9 operator-introduced entity/figure, 6 unbriefable, 3 sentence-length gap.

Train — 600 steps in 12 chunks at 16 layers / rank 16 / lr 3e-5. Peak Metal
34.1 GB against a 16 GB wired cap; train loss 2.08 → ~0.08.

Gate (n=20, adapter vs RAG-alone)

adapter rag tie
wins 4 11 5
disqualified 12 6

p = 0.98 one-sided, so all three keep-conditions fail. The adapter was archived
and the write path resolves to adapter=none.

Worth recording from the disqualification breakdown, since it is what the next
pass should act on:

  • brief-echo disqualifications went to 0 for both arms. That failure mode
    came from briefs being extracts of their targets, and it is gone.
  • invention rose 5 → 11 and parroting 2 → 5 for the adapter. Train loss was
    already ~0.09 by roughly 3.5 epochs on 60 rows, so 10 epochs looks like too
    many for a set this size.

That hypothesis could not be tested from this run: the chunk trainer passes
--save-every per chunk, so every chunk writes 0000050_adapters.safetensors
and overwrites the previous one. Retaining per-chunk checkpoints is a
prerequisite for tuning epochs without a full retrain each time.

The gate result does not change the case for this PR — the pair construction,
carve and gate are the parts that make the next attempt measurable. It does mean
no adapter ships from it.

telivity-otaip and others added 2 commits July 31, 2026 03:09
Keep writer-adapter CLI from #48 and article holdout/eval CLI from #49.
index-voice --from-carve excludes both carves.

Co-authored-by: Cursor <cursoragent@cursor.com>
Persist adapters/latest/checkpoints/step_* after each MLX chunk so a gate can
evaluate early weights. Default writer training to 3 epochs. Add
eval-writer-adapter --sweep-checkpoints and Contoso-safe archive basenames.

Co-authored-by: Cursor <cursoragent@cursor.com>
@telivity-otaip

Copy link
Copy Markdown
Contributor Author

Rebased onto main (merged #49). Added durable per-chunk checkpoints, writer recipe at 3 epochs, and --sweep-checkpoints. Adapter weights still do not ship — write stays adapter=none until eval-writer-adapter returns keep.

@telivity-otaip
telivity-otaip merged commit 269571c into main Jul 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant