Data Moat II (v0.71.36)

A semantic layer over your training data, plus two tools for what a fine-tune forgets and leaks. Four commands: dedup the reworded duplicates MinHash misses, map which topics your data actually covers, plant canaries and later prove whether the model memorized them, and replay a slice of the old task so a new one does not erase it.

Zero new dependencies. The embedding kernel reuses transformers from the [train] extra (mean-pooled all-MiniLM-L6-v2, no sentence-transformers), so dedup --semantic and topics need [train] plus a one-time model download; plain MinHash dedup stays on the light core.

soup data dedup --semantic — near-duplicates over meaning, not shingles

bash
soup data dedup train.jsonl --semantic -o clean.jsonl
soup data dedup train.jsonl --semantic --threshold 0.85 --field text -o clean.jsonl
soup data dedup train.jsonl --semantic --embed-model sentence-transformers/all-mpnet-base-v2

MinHash shingling compares token sets, so a reworded duplicate reads as distinct. Semantic dedup compares embedding cosine instead and catches those: measured, rewordings score 0.88 to 0.91 cosine where MinHash scored them apart, while genuinely-distinct-but-similar instructions are kept.

Honest limit: this is not a paraphrase detector. Measured with all-MiniLM-L6-v2, paraphrase cosines (0.49 to 0.76) *overlap* genuinely-distinct rows (0.54 to 0.76). "Add two numbers" vs "Multiply two numbers" scores 0.759, higher than the true paraphrase "reverse a string" / "invert the order of characters" at 0.491, so no threshold separates them. Lowering --threshold to chase paraphrases deletes real training rows. The default 0.8 is deliberately conservative (max distinct-pair cosine measured was 0.759) and still strictly beats MinHash.

soup data topics — a coverage map of your dataset

bash
soup data topics train.jsonl                    # 'auto' picks the cluster count
soup data topics train.jsonl --clusters 8 -o topics.json

Clusters the dataset (k-means over the same embeddings), labels each cluster with its distinctive terms (c-TF-IDF), and prints a coverage table plus a warning for thin topics:

82% code · 9% chat · 6% math · 2% sql · 1% legal
topic 'legal' is thin: 1% of rows — likely under-represented

Only topics under 2% of the data are flagged (a 6% topic like math above is left alone). Labels are emergent term clusters, not a fixed taxonomy, and there is no automatic join to an eval-coverage suite; read the table as "where my data concentrates", not as classification against an ontology.

soup data canary insert|check — prove what the model memorized

bash
# 1. insert K unique high-entropy secrets (keep the manifest OUT of your repo)
soup data canary insert train.jsonl -o canaried.jsonl --count 16 --manifest secrets.json

# 2. train on canaried.jsonl as usual, then rank each secret's loss vs controls:
soup data canary check --manifest secrets.json --base ./my-model --adapter ./lora

A Secret-Sharer memorization probe (Carlini et al.): insert K secrets, train, then check any model or adapter by ranking each secret's loss against never-inserted controls drawn from the same space. Measured on SmolLM2-135M, a memorized set lands at percentile 0.0 (loss 1.7 to 2.5) against a clean model's 4.1 to 6.2. The verdict is a binomial-tail test (α = 0.05) over the count of memorized canaries, and exit 2 on MAJOR lets CI gate a leak.

The verdict is a binomial tail on purpose. The obvious rule, "MAJOR if any single canary ranks in the bottom 1%", fires MAJOR on a clean model about 15% of the time at K = 16 (a CI gate crying wolf one run in seven); the count-based test drops that false-MAJOR rate to ~1.2% while still catching a partial leak of 2 of 10. "No exposure" is the sampled-control approximation, not full-space rank enumeration, so it is evidence of no memorization, not a proof. The manifest is the sensitive artifact and is written 0600 on POSIX.

soup train --replay — rehearse the old task so the new one does not erase it

bash
soup train --config new_task.yaml --replay old_task.jsonl --replay-ratio 0.1

Continual-learning rehearsal: interleave a seeded sample of an old dataset into training so a new task does not overwrite the old one. --replay-ratio is the fraction of the final mixed set (n_replay = round(r/(1-r) · n_new)), rows are interleaved rather than appended, an undersized pool reports the shortfall instead of repeating rows, and the mix touches train only, so validation stays pure new-task.

Honest scope: proof-of-mechanism. On SmolLM2-135M + LoRA, replay retained the old task 7% better than a no-replay control (the correct direction), but forgetting without it was only about +4%, i.e. mild, so the effect size at full fine-tuning or 7B+ is unproven on a 4 GB box. Replay v1 is sft / pretrain only and is incompatible with packing / multipack.

Fixes that shipped with it

  • The hardware-fit gate refused to train any locally-merged model. A soup merge -o ./mymodel output has no size marker in its name, so the size guesser returned its 7B default, predicted ~16 GB of VRAM, and refused, blocking the exact merge-then-train-from-merged flow that --replay serves. Local checkpoints are now measured from their safetensors header (0.135B actual versus 7.0B guessed). This is the third instance of that class after the v0.71.32 (Whisper) and v0.71.33 (M suffix) fixes, and all three were caught by a live smoke, never a unit test.
  • Replay rows now get the same path-traversal validation as the primary dataset (a llava-shaped {"image": ...} replay row previously bypassed the vision/audio guard).

See also

  • Data Tools — inspect, validate, convert, merge, and plain MinHash dedup.
  • Data Engineering Pro — the dbt-shaped build DAG, soup expect, Magpie, brain-rot (Data Moat I).
  • soup ship — the SHIP / DON'T-SHIP verdict a canary leak should block.
  • Supply-chain security — the other side of "what leaves the model".