Commit Graph

191 Commits

Author SHA1 Message Date
Alpamys 6cb84e7973 fix(deps): both trl bounds were wrong — >=0.14.0,<0.27, settled by construction
v0.72.4 capped `trl<0.25` from a staged-removal table that scored a MODULE
RELOCATION as a field removal. At 0.25 `BCOConfig` moved into
`trl/experimental/bco/`, and at 0.26 kto/orpo/cpo followed — but every one of
them stayed publicly re-exported from `trl` with `max_prompt_length` intact.
Seeing a config file vanish from `trl/trainer/` was read as the field going
away.

Re-derived by parsing each *Config class's own annotated fields across every
wheel 0.24.0 -> 0.29.1 (all five inherit TrainingArguments, so there is no
inherited-field escape hatch):

    version          dpo   kto   orpo   cpo   bco
    0.24.0 - 0.26.2  yes   yes   yes    yes   yes
    0.27.0 - 0.27.2  yes   NO    yes    yes   yes
    0.28.0           yes   NO    NO     NO    NO
    0.29.0 - 0.29.1  NO    NO    NO     NO    NO

Then settled the way reading source cannot — by CONSTRUCTING all six configs
with the exact kwargs the wrappers pass:

    trl 0.26.2   OK dpo · OK ipo · OK kto · OK orpo · OK simpo · OK bco
    trl 0.27.0   FAIL kto: KTOConfig.__init__() got an unexpected keyword
                           argument 'max_prompt_length'
                 OK dpo · OK orpo     <- control: the boundary is KTO's alone

So the cap is <0.27, and 0.25.0/0.25.1/0.26.0/0.26.1/0.26.2 were excluded for
no reason. That is the third wrong answer this bound has had (after "trl 1.x
removed them" and "the break is 0.29.0"), and the first two were also produced
by reading rather than running. The rule this earns: a version bound derived
from source is a hypothesis; the experiment that settles it is constructing the
object.

Two more corrections from the same pass:

- The floor >=0.7.0 was impossible, and nobody checked it while carefully
  fixing the ceiling. `setup()` imports GRPOTrainer unconditionally and trl
  first exports it at 0.14.0 (OnlineDPO / KTO / BCO / BasePairwiseJudge at
  0.11.0; 0.7.0 has none of them). Resolvers pick the newest allowed version,
  so it only bit under a constraints file or anyone reading the metadata as a
  support claim.

- ORPOConfig/CPOConfig are not "deleted" at 0.29 — the modules survive under
  trl/experimental/. They, BCOConfig and their trainers are dropped from the
  public `trl` namespace, which is what Soup imports, so the 0.29 break is an
  ImportError rather than a rejected kwarg. Harder than described, not milder,
  and it means going past 0.29 needs an import strategy (#326).

Closes the blind spot that let this ship, for all six trainers
(tests/test_trl_preference_config_contract.py)

The bug was invisible because the trl imports and the config construction live
inside `setup()`, and constructing a wrapper touches trl not at all. v0.72.4's
streaming suite closed that for four of the six; tests/test_bco.py:266 and
tests/test_ipo.py:191 still MOCK `.setup` out, so bco and ipo had no test that
executed it. The new file drives the real `setup()` for all six on the ordinary
non-streaming path, asserts the BUILT config carries max_prompt_length/
max_length (so a trl release that accepts the kwarg and stops storing it fails
too), and derives the covered set from the trainer sources so a seventh trainer
adopting the argument joins automatically.

Revert the KTO skip: it blamed the device, and the variable is the torch version

`skipif(not cuda)` on test_kto_streams_at_batch_two made the test dead in CI —
there are no GPU runners (ubuntu/windows/macos) — leaving it alive only on this
box, under the one torch where it passes, while torch carries no upper bound.
Three things say the rationale ("a streamed model on CPU is a test convenience")
does not hold:

- the same CI run had test_v07200.py::test_one_training_step_actually_runs, the
  identical streamed train() for SFT, pass on that CPU runner;
- running this body here with CUDA masked passes (torch 2.5.1);
- the error comes from check_same_device, i.e. an op received a meta
  placeholder next to a real tensor — a streaming property. Newer torch
  decomposes more ops, which is why only the newer stack surfaces it.

Now it runs everywhere and xfails on exactly that signature on CPU; anything
else, and the same signature on CUDA, is a hard failure. Real defect filed as
#328. With CUDA hidden: 21 passed, 2 skipped (both genuinely CUDA-only).

Also fixed
- pyproject.toml: 14 double-encoded em-dashes, one of them the `unit` marker
  description that `pytest --markers` prints to users. The existing mojibake
  guard covered only the package docstring; widened to the file, which is not
  importable Python and so was invisible to every source-level check.
- docs/commands.md called itself "the full soup command list" while missing
  eight, three of them glued onto the end of a previous line by absent newlines
  and therefore invisible on the rendered page. Now 77/77, asserted against the
  live Typer app so the claim stays true by construction.

Verified: ruff clean; full suite 16935 passed / 129 skipped / 0 failed (13:15);
wheel builds with Requires-Dist: trl<0.27,>=0.14.0; `pytest --markers` renders
the em-dash. No version bump and no tag — the packaging metadata change wants
CI on the resolver stack before it rides a release.
2026-08-03 22:36:39 +05:00
Alpamys 1ca229fdbf fix(deps): trl removed max_prompt_length in stages — cap <0.25, not <0.29
Third and final correction. The previous cap fixed dpo but left kto/orpo/simpo
failing, because trl did not remove `max_prompt_length` in one release. Read per
config off the published wheels rather than spot-checking dpo_config.py again:

  version   dpo   kto   orpo   cpo   bco
  0.24.0    yes   yes   yes    yes   yes   <- last release all six work on
  0.25.1    yes   yes   yes    yes   NO
  0.26.0    yes   NO    NO     NO    NO
  0.29.0    NO    NO    gone   gone  gone

`soup train --task bco` breaks at 0.25, `--task kto|orpo|simpo` at 0.26, and
`--task dpo|ipo` at 0.29. Capped <0.25, the last release on which all six of
Soup's preference trainers can build their config.

The published benchmarks record carries the same table, including both wrong
diagnoses, since it is cited evidence and a corrected claim there matters more
than a tidy one.
2026-08-03 19:46:27 +05:00
Alpamys e58c11b440 fix(deps): the trl break is at 0.29.0, not 1.0 — correct the cap
My previous commit capped trl<1 on the assumption, taken from an existing note
in this repo, that CI runs trl 1.9.2 and that 1.x removed the APIs. Both were
wrong, and CI stayed red because <1 excluded nothing: the install log shows CI
resolving trl 0.29.1.

Established by reading the published wheels rather than guessing again:

  version  ORPOConfig  CPOConfig  max_prompt_length
  0.20.0   yes         yes        yes
  0.28.0   yes         yes        yes
  0.29.0   no          no         no
  0.29.1   no          no         no

So the break is at a MINOR. Capped trl<0.29 and corrected the "trl 1.x" claim
in the changelog, the published benchmarks record and the internal notes that
propagated it.
2026-08-03 19:22:23 +05:00
Alpamys 35b59346b3 fix(deps): cap trl below 1.0 — six preference trainers cannot build on it
trl 1.x removed ORPOConfig and CPOConfig outright and dropped
max_prompt_length from the remaining preference configs. bco, dpo, ipo, kto,
orpo and simpo all pass max_prompt_length, and orpo/simpo additionally import
configs that no longer exist, so on trl 1.x `soup train --task orpo` fails at
import — for anyone who pip-installed with an unbounded `trl>=0.7.0`.

This was latent, not introduced here. No test had ever called setup() on those
wrappers (they were only instantiated, and the trl imports live inside setup),
so CI stayed green against trl 1.9.2 while the code only worked on 0.x.
v0.72.4's end-to-end preference tests are the first to reach that code path,
which is how it surfaced — all 34 CI failures were in tests/test_v07204.py and
none anywhere else.

Same shape as v0.72.3's mcp<2 cap (#322) and the existing transformers<5.0.0:
declare the dependency the code actually works with, and treat the API
migration as its own piece of work rather than shipping against a major nobody
has validated.
2026-08-03 19:00:24 +05:00
Alpamys 4fb25404f9 feat(streaming): preference losses over layer streaming (v0.72.4)
DPO / ORPO / SimPO / KTO join task=sft on the layer-streaming engine.

The reference model is the SAME streamed base with adapters disabled — one set
of weights, one stream. Measured: streamed DPO peaks at 0.914x streamed SFT with
a byte-identical store and pool, where forcing a real second instance costs
+730.44 MB against 730.44 MB of weights. All four are bit-exact (0.0) against a
resident run of the same loss.

KTO is NOT reference-free: kto_trainer.py:466-476 is byte-for-byte DPO's
three-branch reference selection, so it was gated separately. It also requires
batch_size >= 2, refused at parse time rather than minutes into sharding.

grpo/ppo stay excluded permanently — rollouts re-read every layer per generated
token. The refusal deliberately names no release.

The ~390-line streaming setup moved verbatim into trainer/stream_setup.py so
five wrappers cannot drift. _STREAM_ROWS_PER_EXAMPLE is 2 for the concatenating
losses and 1 for KTO, measured not assumed: the VRAM pre-flight would otherwise
under-predict by half, and on Windows that is a silent spill, not an error.

Honest cost: the reference is free in memory, not in time — DPO reads the layer
stack 1.52x as often per step as SFT.

Also closes five holes in the release checklist itself:
- benchmarks/ was never in it, so gate records (which live under a gitignored
  .claude/) were never published. The public record behind the preprint DOI was
  about to fall a release behind; benchmarks/gate-v0.72.4-preference-losses.md
  and its index row are here, and the checklist now names the step.
- tests/test_version_sync.py asserts pyproject.toml == __init__.py. Every other
  version test in the suite is a >= floor check, so bumping one and forgetting
  the other kept CI green. Verified red-green.
- .claude/paper/ (the DOI preprint) had no "did this release change what it
  claims?" step. For v0.72.4 the answer is no: no measured number moves and its
  task: sft configs stay valid.
- The Docs section header said steps 7-12 while containing 7-13, so plan.md sat
  outside its own section.
- The README size anchor said ~238 lines against a real 426.

Notes for whoever hits these next:
- `pre-commit run --all-files` rewrites ~740 files here (ruff-format on
  pre-existing code). No pre-commit git hook is installed and CI runs only
  `ruff check`, so run it with --files on your own paths or the diff explodes.
- Measuring streamed peak VRAM across setup() charges the pre-flight's own GEMM
  probe (three 4096^3 matrices, ~100 MB) to the step. Reset the peak counter
  after setup.
- The buffer pool is freed by cycle collection, not by close(): back-to-back
  streamed runs in one process retain the previous pool (+47.65 MB measured)
  until a gc pass. Call gc.collect() between arms when measuring.
- A resident model built from a float32 fixture vs a bf16 streamed one measures
  the dtype gap, not streaming — that cost an hour chasing a 9.96e-04 "failure".
- Two concurrent pytest runs on a 4 GB card produce false CUDA failures; run the
  suite alone.

Tests: 16977 -> 17051.
2026-08-03 18:38:13 +05:00
Alpamys 2210ca83e3 docs: swap the Discord invite for a non-expiring one
The invite added in the previous commit was a default one, which Discord
expires after seven days. It is now in the README, CONTRIBUTING, the Code of
Conduct and `[project.urls]`, and the last of those ships in release metadata
where a dead link cannot be corrected after publish - so it has to be a
permanent invite, not a convenient one.

All six occurrences move together; a half-updated set is worse than the old
link, since the stale copies would be the ones a reader hits first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:58:35 +05:00
Alpamys fba981595b docs: add the Discord server and split the project and personal contacts
Discord is added as a fourth community channel, with the boundary stated
rather than left to guesswork: it is for live chat and setup help, while
anything that should still be findable in six months belongs in Issues or
Discussions. A Discord answer helps one person; an issue helps everyone who
hits the same thing, and the repository already routes public traffic that
way.

It lands in the four places a reader actually looks - the README header, the
badge row, the Contact section, and CONTRIBUTING's Community list - plus
`[project.urls]`, which is the one that matters most in practice: most users
arrive from PyPI, whose sidebar previously showed only Homepage, Repository
and Issues. That entry is metadata and takes effect on the next publish.

Both a Code of Conduct that does not name the server and a security policy
that does not exclude it are gaps a public chat channel creates, so the Code
of Conduct now states it applies there, and SECURITY.md says explicitly not to
report vulnerabilities in a public channel.

The single maintainer address becomes two with distinct roles, because one
address doing both jobs cannot be handed over: team@trysoup.dev is the project
address and survives a change of maintainer, while makazanalpamys@gmail.com
stays as the personal fallback. Both are listed everywhere a contact appears -
README, SECURITY.md, CODE_OF_CONDUCT.md - and pyproject's author email, which
PyPI renders as the package contact, moves to the project address.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:55:24 +05:00
Alpamys dd9818e8f3 docs: use makazanalpamys@gmail.com as the maintainer contact
The Code of Conduct pointed at an address that is no longer read, and it was
the only contact route in the repository, so a report sent there went nowhere.

SECURITY.md gains it as a fallback rather than a replacement: private GitHub
Security Advisories stay the preferred channel, but the list had a single
entry and no path for a reporter without a GitHub account.

pyproject gains an `email` on the author entry, which is what PyPI renders as
the package contact - it had a name and no way to reach anyone. Metadata only;
it takes effect on the next publish and changes nothing at runtime.
2026-08-01 14:51:23 +05:00
Alpamys 7b925762f2 fix(deps): cap the [mcp] extra below 2.0
mcp 2.0.0 removed `mcp.shared.memory.create_connected_server_and_client_session`
and dropped `Server.list_tools`. Our constraint was an unbounded `>=1.2.0`, so
CI resolved to 2.0.0 the day it was published and every `soup mcp serve`
round-trip test failed on all nine jobs — on every OS and Python version, which
is what distinguished an upstream break from a regression in the commit under
test.

Verified 1.29.0 (the newest 1.x) still exposes both APIs before capping, rather
than assuming the boundary. Migrating to the 2.x API is separate work; shipping
against an unvalidated major is not a substitute for it.
2026-07-28 23:39:01 +05:00
Alpamys bcbf72e586 feat(train): layer streaming breadth — 6 more archs, bigger batches, resume, disk tier (v0.72.3)
Lifts the v0.72.0-.2 scope freeze. Every capability was gated against a
streamed-vs-resident bit-exactness reference before it was written.

- Six more families (mistral/gemma/gemma2/gemma3_text/phi/phi3), each
  bit-exact vs the same checkpoint loaded resident, under bf16 AND NF4.
  Multimodal gemma3 stays refused — only gemma3_text.
- batch_size > 1 and gradient_accumulation_steps > 1 now work.
- A batch- and vocab-aware VRAM pre-flight that refuses a run predicted
  not to fit. Fitted to 10 real runs: worst error 0.85%, never
  under-predicts. On Windows an over-budget step does not OOM; WDDM
  spills silently, so the estimator is the only guard.
- A throughput bracket from a GEMM ceiling measured on the user's own
  card in the same session, printed with the SM clock.
- --resume / --hf-resume: load_state_dict narrows keys by child name, so
  a canonical checkpoint matched 0 of N tensors and PEFT warned only.
  Keys are now redirected at load time, mirroring the v0.72.1 save fix.
- An NVMe disk overflow tier (stream_source: auto|ram|disk), bit-exact
  against the RAM tier. Its speed relative to RAM is UNMEASURED here and
  no figure is claimed.
- soup doctor --disk reports the detected media type.

Fixes: estimate_logits_bytes charged 6 bytes/element where the measured
peak is 14; the NVMe tier guard was wired to a hardcoded constant;
streaming sources leaked handles when training raised; subprocess
helpers resolved tools by bare name (CWE-427 on Windows).

112 tests in tests/test_v07203.py; 16867 -> 16977.
2026-07-28 23:15:20 +05:00
Alpamys 08343f8f35 feat(train): NF4 layer streaming — fine-tune Llama-3.1-8B on a 4 GB card (v0.72.2)
Layer streaming (v0.72.0) was bf16-only, capping it near 3B on a small card.
Quantising the streamed base to NF4 makes the RAM store ~4x smaller, which is
what brings 8B within reach.

Measured on a 4 GB RTX 3050 Laptop through the shipped code (50 steps after 10
warm-up, batch 1, S=512, PagedAdamW8bit, GEMM ceiling taken in the same session):
  Llama-3.1-8B-Instruct  119.6 tok/s  3.32 GB peak  3.60 GB pinned  100%  952 MHz
  Qwen2.5-3B             264.2 tok/s  1.76 GB peak  1.43 GB pinned  100%  960 MHz

3B is 1.85x the bf16 path, but that is PINNING, not arithmetic: 1.43 GB
page-locks where 5.55 GB did not, restoring async copy_ (util 79.3% -> 100%).
14B was not run — its store exceeds this box's measured 7.12 GB pinned ceiling.

A streamed NF4 run is bit-exact against a RESIDENT NF4 run, now as CPU-runnable
CI tests rather than only a gate result.

Notable, because each fails silently:
- PEFT dispatches lora.bnb.Linear4bit only when is_loaded_in_4bit is stamped;
  without it the generic lora.layer.Linear runs against a Linear4bit base and
  casts differently (9.375e-01 logit divergence, no warning). Pinned by a test
  with a control that deletes the marker.
- hf_quantizer must be stamped too, or Trainer.__init__ dies formatting its own
  "cannot fine-tune" error. Found by the end-to-end test.
- The shard cache is keyed on quant/double_quant/quant_device as well as dtype
  and source fingerprint; a bf16 cache reused for an NF4 request would feed
  full-precision bytes to matmul_4bit.
- index.json is a trust boundary: its shape/blocksize reach bnb kernels that do
  not bounds-check, so from_json validates and the runtime cross-checks the
  claim against the bytes on disk.
- A streamed NF4 model over-reported parameters ~6.5x (878,154,048 vs
  134,515,008 for SmolLM2-135M). Display-only; ~52 B at 8B.

Scope unchanged and still BETA: RAM tier, sft, Llama/Qwen, batch 1, no
accumulation, no resume. quantization values other than none/4bit are refused.

Tests: 16752 -> 16840 (+88 in tests/test_v07202.py).
Full suite: 16734 passed, 129 skipped.
2026-07-28 15:15:20 +05:00
Alpamys af8683b61d fix(train): streamed adapters were saved unloadable (v0.72.1)
v0.72.0's layer-streaming wrapper holds the real decoder layer as a child
named `inner`, so every saved LoRA adapter key carried an `.inner.` segment.
Such a file reloads as ZERO tensors into any normal model: soup merge,
soup serve, soup chat and PeftModel.from_pretrained all returned the untuned
base while PEFT emitted only a UserWarning. Training was correct; only the
artifact was inert.

StreamedDecoderLayer.state_dict() now delegates to the wrapped layer at the
wrapper's own prefix, so every artifact path -- the final trainer.save_model(),
each save_steps checkpoint, and therefore soup adapters, the Registry, merge
and serve -- becomes canonical at once. Serialisation-only by design: the
forward path is untouched, so v0.72.0's bit-exactness gates remain valid
without being re-earned.

Also fixes --hf-resume bypassing the streaming resume refusal. The guard
tested only --resume, while --hf-resume reaches resume_from through another
branch. Pre-fix that combination matched keys by accident; post-fix it would
have matched nothing and silently continued training with a freshly
initialised adapter -- i.e. the adapter-key fix alone would have made that
one path worse.

Roadmap renumbered (this release was inserted ahead of NF4): every
"lands in vX.Y.Z" refusal corrected -- NF4 v0.72.2, disk tier / more
architectures / larger batches / gradient accumulation / checkpoint-resume
v0.72.3, preference losses v0.72.4.

Found by the v0.72.2 NF4 gate, not by the 159 v0.72.0 tests -- none of them
saved an adapter and loaded it back. The new regression test does exactly
that, by count, by name and by value, with a negative control that re-mangles
the keys and asserts the reload yields zeros (0-of-N loading raises nothing,
so a green round-trip without the control proves nothing).

Tests: +17 in tests/test_v07201.py (16735 -> 16752).
Full suite: 16623 passed, 129 skipped, 4 deselected.

Note for maintainers: do not edit soup_cli/__init__.py while a suite is in
flight -- a mid-run version bump made test_cli_subprocess::test_version fail
spuriously (the subprocess and the imported constant disagreed).
2026-07-27 23:18:11 +05:00
Alpamys 00833ac789 feat(train): layer streaming — fine-tune models larger than VRAM (v0.72.0 BETA)
The frozen base lives in CPU RAM and is streamed into a small pool of
pre-allocated VRAM buffers one decoder layer at a time, so peak VRAM is
bounded by ONE layer instead of the whole model. Only the LoRA adapters,
their gradients and optimizer state stay resident.

Measured on an RTX 3050 Laptop 4 GB (Windows, 16.9 GB RAM), batch 1,
gradient checkpointing on, 50 steps after 10 warm-up:

  Qwen2.5-0.5B  S=512   978.6 tok/s  91.4% util  1.47 GB peak
  Qwen2.5-1.5B  S=512   525.0 tok/s  96.8% util  1.82 GB peak
  Qwen2.5-1.5B  S=1024  487.6 tok/s  96.7% util  2.96 GB peak
  Qwen2.5-3B    S=512   143.1 tok/s  79.3% util  2.15 GB peak

Qwen2.5-3B trains in 2.15 GB on a 4 GB card where a resident run OOMs.
Honest cost: 1.43x slower than resident, measured at 0.5B — the only
apples-to-apples comparison available on this box, because 1.5B and above
cannot run resident here at all.

Correctness was gated before any src/ code was written: streamed vs
resident logits are bit-exact (max abs diff 0.0), the layer-0 LoRA
gradient is non-zero on all layers, a 100-step loss curve matches
resident exactly, and same-seed runs are identical.

New:
- utils/layer_stream.py          pure planner (no top-level torch)
- utils/layer_shard.py           per-layer safetensors sharder
- utils/layer_stream_runtime.py  buffer pool, RAM source, prefetch, wrapper
- training.stream_layers / stream_source / stream_buffers

Notes for future maintainers:
- transformers' Trainer.__init__ and accelerate's prepare_model BOTH call
  model.to(), which raises NotImplementedError on meta parameters. The
  streamed layer overrides _apply to pass meta tensors through, and the
  model declares hf_device_map. Without either, every run dies at trainer
  construction — no unit test that stops at model(input_ids=...) sees it.
- The shard cache is keyed to a fingerprint of the source checkpoint, not
  just the model slug: a base retrained in place must re-shard rather than
  silently stream stale weights.
- The pre-flight hardware-fit gate models a RESIDENT run, so it is skipped
  for streaming — otherwise it refuses exactly the runs this enables.
- expandable_segments:True is silently ignored on Windows; probed, not
  claimed.

Scope (every refusal names the release that lifts it): RAM tier, bf16,
task=sft, Llama/Qwen, batch 1, no gradient accumulation, no --resume.
NF4 is v0.72.1; disk tier / bigger batches / accumulation / resume are
v0.72.2. Proof-of-mechanism at 3B — nothing above 3B was measured.

Tests: 16576 -> 16735 (+159 in tests/test_v07200.py)
2026-07-26 23:58:06 +05:00
Alpamys 03282e9ef6 feat(reward): soup reward stress — adversarial verifier gameability probe (v0.71.41)
Turn the reward-hacking detector on the verifier itself: feed empty /
length-padded / repetition / sentinel-spam completions and flag any the
verifier accepts. Loads via the existing load_reward_fn (probes a synth .py
or a builtin); a gold-requiring target with no --references is a hard error,
never a false "robust". Exit 0=robust / 2=gameable / 1=error. Pure, offline,
no schema change, no new deps.

Also corrects the ops-docs Telemetry section (the sender exists but is wired
to nothing — no data is sent). Telemetry flywheel deferred pending a public
privacy policy.

Tests: 16490 -> 16529 (+39). 5 sequential ECC reviews, every finding fixed.
2026-07-19 20:53:37 +05:00
Alpamys 43a7fb21a5 feat(reward): soup reward synth — synthesize a deterministic reward verifier (v0.71.40)
Point `soup reward synth <refs.jsonl> -o reward.py` at reference (gold) outputs and it
infers a deterministic verifier (numeric / json_schema / regex / tool_call), emits a
readable, committable .py reward_fn that rides load_reward_fn's existing .py path (no new
exec surface), and — the moat — REFUSES to emit one that can't discriminate its references
from auto-perturbed negatives via a mandatory calibration report (accept refs >=90% AND
reject negatives; hard floor at discrimination<=0). Nothing in TRL/Unsloth/Axolotl
synthesizes a reward.

Fixes #311: a comma-separated reward_fn ("accuracy,format") now loads as a reward ensemble
(GRPOTrainer reward_funcs=[...], unlocks the rm_ensemble detector), GRPO-only and validated
at config-parse; the deepseek-v3-reasoning recipe that shipped this previously crashed with
"Unknown reward function".

Riders: reward_fn field-validator (null-byte/blank/oversize/empty-comma-segment); comma-aware
verifiable-domain check; envs/calculator + guess_number docstrings corrected.

5 sequential ECC reviews, every finding fixed (python HIGH PPO gate; code 2xHIGH per-tool
arg binding + json_schema mixed-shape refuse; security HIGH rel_hint codegen injection; tdd
8xHIGH). Live smoke on RTX 3050: synth from envs/calculator agrees with math_verify;
degenerate refused (exit 2); real GRPO on SmolLM2-135M with reward_fn=accuracy,format
completed optimizer steps. +103 tests (tests/test_v07140.py); 16387 -> 16490.
2026-07-19 18:50:47 +05:00
Alpamys cba984a574 feat(ship): close the evidence loop — emit-evidence + config + provenance + PR comment (v0.71.39)
soup ship's verdict is now emittable, committable, reviewable, and provenance-bound
so a fine-tuning gate runs on every PR:

- --emit-evidence: verdict_to_evidence re-serialises scores into the --evidence
  INPUT schema (output replayable as input, #312)
- ShipConfig under eval.ship + --config: committable gate policy read with
  CLI > config > default precedence (Click ParameterSource)
- --push owner/repo#N: verdict as a GitHub PR comment (best-effort, reuses
  adapter_pr.post_pr_comment; never flips the SHIP/DON'T-SHIP exit code)
- provenance/staleness: --config+--emit-evidence STAMPS config_sha (eval.ship
  excluded from the recipe hash) + base_model + data_sha; --config+--evidence
  GATES, refusing drifted/absent config_sha (exit 3)
- soup ci init --config binds the generated gate to the committed config

Security: config_sha shape-gated before echo (ESC hygiene); data_sha via
O_NOFOLLOW + symlink guard + 8 GiB cap; ci path rejects '#' + NEL/LS/PS
(YAML plain-scalar run: truncation).

16330 -> 16387 tests (+57 tests/test_v07139.py).
2026-07-19 12:30:35 +05:00
Alpamys f6bc8e7bdd feat(ship): make soup ship's leg-2 regression gate real (v0.71.38)
soup ship's leg 2 — the catastrophic-forgetting / regression gate that carries
the whole SHIP / DON'T-SHIP claim — was 15 trivia prompts scored by raw
substring containment (it credited "B" for "Berlin", "3" for "13") with zero
coverage for tool-calling, safety, or JSON. This makes the gate real.

- forgetting.py: score_answer/extract_mcq_letter replace the substring scorer
  with answer-extraction (cue -> paren -> clause-terminating bare letter) +
  boundary-aware token match. MINI_BENCHMARKS expanded (mmlu 26 / common_sense
  24 / instruction 24) + new mini_arithmetic (36) so a 1-item flip trips 0.05.
  BREAKING: an existing run's verdict can change (the old gate under-reported).
- eval/gate_suites.py (new): bundled offline general-suite registry, no torch.
  DEFAULT_GENERAL_SUITE = the 4 MCQ suites + 3 behavioural JSONL suites
  (mini_tool_call / mini_format_json / mini_safety) scored per-model-absolute
  by the pure custom/diagnose scorers. _fraction_passing isolates a per-item
  scorer exception (deep-JSON RecursionError scores as a failed item).
- ship.py: leg-2 scores bundled suites offline (base+tuned) before routing any
  non-bundled name to lm-eval; default general suite = the full bundled set.
  Exit-code taxonomy: usage errors move 2 -> 3 so exit 2 means only DON'T-SHIP
  (a typo'd flag was previously indistinguishable from a caught regression).
- diagnose/__init__: "Six" -> "Seven" probes + re-export all 7 score_* fns;
  removed the dead SUPPORTED_TASK_MODES "pairwise reserved" gate.
- Bundled gate fixtures ship in the wheel via the pyproject artifacts glob.

Every bundled item is original, hand-authored (no MMLU/GSM8K rows copied).
Test count 16288 -> 16330 (+42 in tests/test_v07138.py).
2026-07-17 22:42:47 +05:00
Alpamys a95fedeb0e fix(cli): quote install hints so `pip install soup-cli[extra]` works on cmd.exe (v0.71.37)
Every printed and documented `pip install 'soup-cli[extra]'` was bash / zsh /
PowerShell syntax and failed on Windows cmd.exe:

    ERROR: Invalid requirement: "'soup-cli[train]'": Expected package name at
    the start of dependency specifier

cmd.exe has no single-quote quoting, so it passes the quotes to pip verbatim
and pip rejects the requirement. Nothing in Soup can fix that once the command
is typed -- pip and the shell own it, and Soup is not installed yet when the
README line runs -- so the fix is the spelling we print.

Migrated 147 sites across 67 files to `pip install "soup-cli[extra]"`:
  - 64 in src/  (Rich console hints + plain ImportError text)
  - 57 in README.md + docs/
  - 22 in src/soup_cli/templates/*.yaml + examples/configs/*.yaml
  -  3 in examples/README.md

Double quotes are the only spelling valid in every shell (cmd, PowerShell,
bash, zsh), which is why the repo already used `pip install -e ".[dev]"`.
Measured on Windows: single quotes fail ONLY on cmd; double quotes pass
everywhere; bare passes on Windows but zsh globs `[extra]` and fails.

Method note (the PR #247 class): the hints sit INSIDE double-quoted Python
string literals, so a blind ' -> " sed produces SyntaxError. A tokenize-based
rewriter escaped `\"` in DQUOTE tokens and left bare `"` in TRIPLE / COMMENT
tokens; every touched .py was compile-checked. The full suite (not ruff, not
compile-check) caught two rewriter blind spots: the real YAML templates under
src/soup_cli/templates/ (byte-identical drift test) and examples/README.md.

A regression test (tests/test_v07137.py) scans the package and every docs code
block for the single-quoted form; prose may still name it so a reader from an
older tutorial recognises the error.

Also bundles #315 (@Sanjays2402): eval-gate benchmark tasks now run via
ForgettingDetector instead of a helper that never existed. Closes #310.

Test count: 16283 -> 16288 (+4 in tests/test_v07137.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 20:40:54 +05:00
Alpamys 55dd6301b5 docs: v0.71.36 Data Moat II (version bump + CHANGELOG + docs)
Version 0.71.35 -> 0.71.36 in pyproject.toml + __init__.py.

Docs are written around what the live smoke MEASURED, not what the plan
assumed:

- The semantic-dedup headline is WITHDRAWN. docs/data.md leads with the
  measured overlap: paraphrase cosines (0.49-0.76) overlap genuinely-distinct
  rows (0.54-0.76), and "Add two numbers"/"Multiply two numbers" (0.759)
  scores HIGHER than the true paraphrase "reverse a string"/"invert the order
  of characters" (0.491). No threshold separates them, so the page says
  plainly that lowering --threshold trades duplicates for silent data loss,
  and explains why the 0.8 default is deliberately conservative. The claim it
  does make -- catches REWORDINGS MinHash's shingling misses (0.88-0.91) --
  is the one the numbers support.
- docs/data.md canary section states the real verdict rule (binomial tail
  over the count, not any-single-canary) and why: at K=16 an any-canary rule
  fires on a CLEAN model ~15% of the time.
- docs/training.md --replay reports the honest result: 7% better retention
  than control, but forgetting without it was only +4% (mild), so it is
  proof-of-mechanism at 135M + LoRA, not a production claim.
- CHANGELOG known-limitations carry the same numbers rather than hedging.
- README What's New leads with the two blocking bugs the smoke found (the
  hardware-fit gate refusing locally-merged models; the [extra] hints
  printing without the extra).

Counts 16001 -> 16254 tests, 313 -> 314 files (CONTRIBUTING).
2026-07-17 12:03:15 +05:00
Alpamys dc73e947e9 feat(compliance): init templates + soup card + soup ci init + GGUF-on-Windows (v0.71.35)
Ship a regulated fine-tune with the paperwork it needs, plus GGUF export that
actually works on Windows.

Added:
* `soup init --template hipaa|soc2|eu-ai-act|sr-11-7` (templates 17 -> 21).
  Design constraint verified in-repo, not assumed: Soup's compliance controls
  are CLI flags/commands, NOT schema keys (audit_log/bom/attest/repro_receipt/
  annex_xi/track_energy/pii/decontaminate have zero matches in config/schema.py),
  so a template cannot "pre-wire audit-log on" as YAML. Each is a valid
  SoupConfig + header comments naming that regime's exact commands.
* `soup card <registry-id> -o MODELCARD.md` — registry entry -> publishable HF
  card (training config, eval scorecard, config/data hashes, lineage, artifacts).
  Pure build_model_card() over dicts, reused by `soup push --card`.
* `soup ci init` — writes .github/workflows/soup-gate.yml chaining
  data validate -> expect -> ship --evidence (exit 2 blocks the merge).
* docs/compliance.md quickstart.

Fixed (GGUF-on-Windows, validated end-to-end for the first time; closes the
CPU-validatable half of #70/#144). Four independently-fatal bugs:
* export cloned llama.cpp into the CURRENT directory: SOUP_DIR is the bare name
  ".soup" but was used relatively instead of anchored to home like tracker.py /
  registry/store.py, so ~/.soup/llama.cpp was never found.
* the first GGUF export DOWNGRADED the user's torch and broke CUDA: the
  auto-clone pip-installed llama.cpp's requirements.txt (pins torch~=2.2.1 from
  the CPU index) into the user's interpreter. Observed live: torch 2.5.1+cu ->
  2.2.2+cpu, transformers 4.57 -> 4.46. Now installs only gguf/sentencepiece/
  protobuf, unpinned, non-fatally.
* a correctly-built llama.cpp was not found on Windows: MSVC (like Xcode) is a
  multi-config generator emitting build/bin/Release/llama-quantize.exe.
* `soup deploy ollama` failed on a relative GGUF path ("pull model manifest:
  file does not exist") — ollama resolves FROM against the Modelfile's dir and
  Soup writes it to a temp dir. Modelfile now emits an absolute path.

Also fixed a pre-existing model-card injection hole (affects `soup push`'s own
auto-card): _render_training_section interpolated base/task/scheduler/recipe
unescaped, and SoupConfig.base/scheduler have no charset validator, so a
crafted-but-valid config could smuggle raw HTML or a code-span-breaking backtick
into a card published to the Hub.

Step-6 live smoke (real train -> registry push -> card) caught a bug 90 green
tests missed: is_adapter came only from registry artifacts, so a real LoRA run
with no artifacts rendered "Full model" + library_name: transformers — a false
claim in a provenance document.

5 sequential reviews, every finding fixed (code HIGH: the generated workflow ran
`pip install -e ".[dev]"`, which only works in the Soup source tree, breaking the
gate's first step for every downstream user; security HIGH/MEDIUM/3 LOW; tdd HIGH:
push --card was only --help-tested). All 5 new fixes mutation-verified as real
pins. Tests 15906 -> 16001 (+95); full suite 15872 passed / 126 skipped.
2026-07-15 20:09:10 +05:00
Alpamys ff4c9963cb docs: v0.71.34 adapter algebra + LISA (version bump + CHANGELOG + docs) 2026-07-15 13:39:46 +05:00
Alpamys 2f79aebc5a docs(draft): v0.71.33 — soup draft, shipped as the honest measurement gate
Version 0.71.32 -> 0.71.33. CHANGELOG (with a Known-limitations block), README
What's New, docs/commands.md, docs/serving-and-export.md, CLAUDE.md (arch + CLI
+ counts + history roll), CONTRIBUTING counts.

The live smoke changed the pitch. The plan promised '~1.5-2x faster to serve by
distilling its own draft'. Measured on SmolLM2-360M-Instruct <- 135M-Instruct:
the STOCK draft already scored 69.3% acceptance; distilling it gave 69.7% at 2
epochs and 69.3% at 10 -- no gain beyond noise. A small same-family draft is
already at its capacity ceiling vs the target, and logit KD cannot buy capacity
it does not have. Assisted decoding also measured 0.55-0.64x -- a net SLOWDOWN.

So the speedup claim is withdrawn, not shipped. The feature ships as the honest
gate: soup draft measure tells you whether speculative decoding is worth
enabling BEFORE you ship it, and on this pair it correctly says no. That
negative result is stated in the CHANGELOG, the README, the serving docs and
CLAUDE.md, and the pre-existing unverified '2-3x faster' line in the
speculative-decoding docs was tempered to match.

Full suite: 15680 passed / 124 skipped (15806 collected). ruff clean.
2026-07-13 12:46:19 +05:00
Alpamys 5369e81d9d docs(v0.71.32): version bump + CHANGELOG/README/docs for ASR (Whisper) fine-tuning 2026-07-07 17:24:40 +05:00
Alpamys 8f59b9b9d7 docs(v0.71.31): judge-in-the-loop suite — version bump, CHANGELOG, docs, recipe, counts
Bump 0.71.30 -> 0.71.31; +1 recipe online-dpo-smollm2-135m (137->138); CHANGELOG
[0.71.31]; README What's New; docs (training/data/evaluation/commands/serving);
recipe-count test asserts 137->138.
2026-07-06 13:51:23 +05:00
Alpamys 93449442b8 docs(prm): v0.71.30 release — version bump, CHANGELOG, README What's New, docs/training (PRM-guided GRPO), recipe/test counts 2026-07-05 19:32:38 +05:00
Alpamys 8a21e13b39 docs(shrink): v0.71.29 release docs (version bump, CHANGELOG, README, docs, CONTRIBUTING) 2026-07-05 11:50:43 +05:00
Alpamys 96c04f9716 feat(mcp): stdio server + soup mcp serve command + [mcp] extra (v0.71.28 Part D) 2026-07-04 16:55:00 +05:00
Alpamys 937abb9e0d feat(data): soup data doctor + soup data lint — Fine-tune Doctor (v0.71.27)
Add `soup data doctor` and `soup data lint`, killing the top *silent*
fine-tune failures before a single training step: EOS-missing-from-labels
(the #1 "model never stops generating" bug), BOS duplication, no-system-role
templates, and preference-data length bias (the #1 silent DPO degradation) —
none of which any competitor (Unsloth/Axolotl/LlamaFactory) checks for.

- utils/data_doctor.py: 8-check chat-template compat report over a
  tokenizer + sampled rows, OK/MINOR/MAJOR taxonomy mirroring diagnose;
  --show-mask N renders per-token trained/masked colouring through the
  SAME masking dispatch (_build_row_labels) the report itself uses, so
  the two can never disagree about what's actually trained.
- utils/data_lint.py: preference-data linter (dpo/orpo/simpo/ipo/bco/kto)
  — length bias (Cohen's d), label imbalance, near-duplicates (MinHash),
  identical chosen==rejected pairs, prompt leakage.
- commands/data_doctor.py: Typer layer for both commands; strips C0
  control bytes before untrusted dataset content reaches the terminal.
- commands/diagnose.py: hardens the --evidence loader against a TOCTOU
  symlink swap (O_NOFOLLOW + fstat-on-open-fd), backporting the pattern
  soup ship shipped in v0.71.25 (closes v0.71.25 known-limitation (4)).

Live smoke against the real HuggingFaceTB/SmolLM2-135M-Instruct tokenizer
(Windows + RTX 3050) found and fixed two genuine bugs beyond the synthetic
fixtures: the EOS check needed to span-search the whole trained region
(not just the last token), and two apply_chat_template call sites needed
a broad except Exception for jinja2.exceptions.TemplateError.

+173 tests (14788 -> 15042). 5 sequential ECC reviews, every finding fixed.
2026-07-04 14:03:30 +05:00
Alpamys dca58c4107 docs(train): v0.71.26 release — closed-loop reward-hacking mitigation
Version 0.71.25 -> 0.71.26 (pyproject + __init__). CHANGELOG [0.71.26] entry
(feature + security). README What's New slot. docs/training.md mitigation
section + docs/commands.md flag. CONTRIBUTING + examples/README. Also folds in
the already-merged qwen2.5-coder-7b-sft recipe (#285) that rides this release.
2026-07-01 16:55:59 +05:00
Alpamys 6cb1abab8f feat(eval): soup ship — SHIP / DON'T-SHIP verdict (v0.71.25)
Add `soup ship`, a binary SHIP / DON'T-SHIP verdict after fine-tuning: it
SHIPs only when (leg 1) the task metric strictly improved AND (leg 2) no
general benchmark regressed past a forgetting threshold (default 0.05
absolute points) — otherwise DON'T SHIP, even if the task metric looks
great. The moat is leg 2 (catastrophic-forgetting gate) fused with the
task win into one decision. Exit: 0=SHIP, 2=DON'T SHIP, 1=runtime error.

- utils/ship_verdict.py: pure engine (no top-level torch) — frozen
  TaskWin/BenchmarkDelta/ShipVerdict + decide_ship (single source of
  truth for the threshold) + compute_benchmark_deltas + render/serialize.
- commands/ship.py: Typer command; --evidence offline path + live
  metric/judge leg-1 + mini(default)/lm-eval leg-2; --baseline/--output.
- Reuses run_eval / JudgeEvaluator / ForgettingDetector / resolve_baseline
  / _run_lm_eval / live_eval.make_generator.
- Hardening: --evidence O_NOFOLLOW + size cap; --task-eval cwd-contained;
  --judge-model urlparse SSRF guard; lm-eval model_args injection guard;
  --general-suite bounded.

Schema (ShipConfig) deferred — v1 is CLI-only. Pairwise judge win-rate is
a planned fast-follow. +79 tests (14514 -> 14593).
2026-06-27 23:37:35 +05:00
Alpamys 418f86390a feat(recipes): 2026 model-family expansion — 17 SFT recipes, catalog 116→133 (v0.71.24)
Add ready-made SFT recipes for the open-weight models released Feb–Jun 2026,
each base repo-ID verified to resolve on Hugging Face:
- Qwen3.5 0.8B/2B/4B/9B/27B + MoE 35B-A3B/122B-A10B/397B-A17B (Apache-2.0)
- Qwen3.6 27B + 35B-A3B (Apache-2.0)
- DeepSeek-V4 Flash/Pro (MIT), GLM-5.1 (MIT)
- Kimi-K2.5/K2.6 (Modified MIT), MiniMax-M3 (MiniMax Community License)
- Mistral-Large-3 (Apache-2.0, 675B/41B-active multimodal MoE)

Fix stale glm-5-sft repo-ID THUDM/glm-5 -> zai-org/GLM-5 (org migration).
+220 tests (tests/test_v07124.py). Catalog count 116 -> 133.
2026-06-21 13:00:47 +05:00
Alpamys fcf4b33394 feat(train): native Spectrum targeted training — soup spectrum scan + training.unfrozen_parameters (v0.71.23)
Closes #266. `soup spectrum scan` streams safetensors per-tensor (no model
load, CPU-friendly) and computes a singular-value SNR per weight matrix
(Marchenko-Pastur, arXiv:2406.06623), emitting a ready-to-paste
training.unfrozen_parameters patch. The SFT trainer freezes all params then
unfreezes the matched set (full FT, LoRA off).

- utils/spectrum_scan.py: pure-numpy transpose-invariant SNR kernel +
  per-tensor safetensors streaming (2^31 SVD cap, symlink skip) + cache
  (~/.soup/spectrum, SOUP_SPECTRUM_CACHE_DIR containment) + hardened
  hubs.snapshot_download.
- commands/spectrum.py: soup spectrum scan (SNR table + YAML patch).
- schema: training.unfrozen_parameters (caps/NUL/invalid-regex/ReDoS reject)
  + gates (sft/transformers/text/quantization=none; mutually exclusive with
  LoRA features / freeze_layers / freeze_ratio / train_router_only /
  expand_layers).
- trainer/sft.py: full-FT branch via apply_unfrozen_parameters +
  enable_input_require_grads (fixes grad-checkpointing through frozen
  embeddings).

Existing spectrum trainer-plugin untouched (back-compat); LISA -> #267.
Live-validated on Windows + RTX 3050: CPU scan of SmolLM2-135M + top-25%
unfrozen full-FT train (loss 3.455 -> 0.719).

Note: after the version bump the editable install metadata was stale
(0.71.17); pip install -e . --force-reinstall --no-deps re-synced it so
test_cli_subprocess::test_version passes.

+94 tests in tests/test_v07123.py (14184 -> 14278).
2026-06-12 17:40:46 +05:00
Alpamys ccd5c80e4d feat(perf): MiniLLM/MoLE KV-cache + deploy-measure live factories + live-codec TTS (v0.71.22)
#263 MiniLLM on-policy KV-cache (PEFT-unwrap probe activates the cache for LoRA
students; per-step single-token forward), #262 serve --mole per-adapter KV cache
(fresh per generate, no cross-request leak, byte-identical to no-cache), #143
deploy-autopilot live generator factories (baseline scored once + up-front
candidate validation; injected seams retained), #265-partial live-codec TTS
(soundfile.info pre-probe + O_NOFOLLOW; SNAC Orpheus encode validated).

Review: 1 HIGH + 5 MEDIUM + ~10 LOW fixed across 2 review waves + verification +
step-6 live smoke (Windows + RTX 3050). Tests 14084 -> 14184 (+100 in
tests/test_v07122.py; 293 files). Full suite 14067 passed / 117 skipped, exit 0.
2026-06-10 21:30:01 +05:00
Alpamys ed5fc3a8b3 feat(precision,rollout): live fp8/nvfp4 + vLLM sleep + openenv rollout + apple-adapter + delinearize-llama4 (v0.71.21)
Closes #141, #124, #125, #228, #97.

- #141: apply_fp8_attention (torchao float8 on attention projections, Hopper
  gate) + apply_nvfp4 (NVFP4Config, Blackwell gate); partial-conversion honesty;
  wired into the v0.28 speed/memory pipeline with yellow-advisory degrade.
- #124: vllm_sleep_mode live - create_vllm_engine(sleep_mode=True) +
  vllm_sleep_cycle ctx (wake in finally) + TRL GRPOConfig hook probe.
- #125: openenv rollout fully live via training.rollout_func module:fn
  resolver; rows replace the prompt dataset; art/ruler/nemo_gym honest dep
  gates + _EXTERNAL_ROLLOUT_RUNNERS seam. Real GRPO train on SmolLM2-135M.
- #228: convert_apple_adapter live - PEFT LoRA <-> mlx-lm (both matrices
  transpose, bf16 upcast, adapters.safetensors + num_layers, npz legacy read,
  np.ascontiguousarray fix for safetensors non-contiguous mangling);
  *-to-apple upstream-gated exit 3.
- #97: delinearize-llama4 live - [E*din,dout] -> [E,din,dout] per shard,
  config.json expert-count probe + --num-experts, sidecar copy, atomic writes.

Review waves: 3 HIGH + ~8 MEDIUM + ~12 LOW fixed.
Tests: 13874 -> 14084 (+210 in tests/test_v07121.py).
Full suite: 13967 passed, 117 skipped. ruff clean.
2026-06-10 16:29:52 +05:00
Alpamys a4dfbb308c feat(trainer): live TTS / BitNet / MoE-expert-quant trainers (v0.71.20)
Lift three v0.52.0 schema-only NotImplementedError stubs to real code.

- #131 TTS: TTSTrainerWrapper(SFTTrainerWrapper) — TTS fine-tune = next-token
  CE over [text][audio-codec-token] chat; per-family emotion templating
  (Orpheus/Oute) + codec special-token registration. Pre-encoded chat path
  live-validated on SmolLM2-135M-Instruct; live-codec (data.format=audio)
  hardware-gated per family.
- #134 BitNet: BitNetTrainerWrapper gated on onebitllms; export --format
  bitnet|tq1_0 runs real llama.cpp TQ1_0 ternary GGUF export.
- #136 MoE: apply_moe_expert_quant swaps fused-MoE experts to bnb Linear4bit/
  Linear8bitLt (pre-LoRA); train_router_only freezes experts (post-LoRA).
  Live-validated on RTX 3050 (dequant err 0.0155).

Review fixes: H1 explicit Params4bit/Int8Params weight-carry; H2 quant
pre-LoRA / freeze post-LoRA + skip PEFT-wrapped modules; M4 device-aware
placement.

Tests 13807 -> 13874 (+69 in test_v07120.py, -2 lifted stubs in test_v0520.py).
2026-06-10 12:37:14 +05:00
Alpamys f51331d637 feat(quant,multipack): vision/audio Quant Menu + multipack FSDP sharding (v0.71.19)
#81 Quant Menu for vision/audio modality
- config/schema: drop the `modality != "text"` rejection in
  _validate_quant_menu_supported_tasks (mlx-backend gate retained) so the full
  Quant Menu (gptq/awq/hqq:Nbit/aqlm/eetq/mxfp4/fp8) applies to vision+audio.
- trainer/sft: _setup_vision_transformers + _setup_audio_transformers call
  build_quantization_config_for_loader (strict superset of the inline 4bit/8bit
  BNB blocks they replaced); drop BitsAndBytesConfig import; retain the
  prepare_model_for_kbit_training gate on (4bit,8bit,mxfp4).

#80 multipack DataLoader sharding under FSDP/DeepSpeed/DDP
- utils/multipack_trainer: get_train_dataloader routes the multipack DataLoader
  through accelerator.prepare when num_processes > 1 so accelerate's
  BatchSamplerShard shards whole FFD bins across ranks (even_batches=True avoids
  the epoch-boundary collective hang; seed identical across ranks, no `+ rank`).
  Single-process path unchanged. Defence-in-depth guard against an unconfigured
  MagicMock num_processes.

Tests: +37 in tests/test_v07119.py (13770 -> 13807). ruff clean.
Supersedes the v0.40.5 vision/audio Quant-Menu and v0.40.4 multipack-FSDP
known-limitations. Full multi-GPU validation remains an INFRA-BLOCKED QA item.
2026-06-09 12:37:50 +05:00
Alpamys 70fd5ee9f3 feat(distill,agent,cloud): on-policy MiniLLM + aligned ULD + agent sandbox eval + Modal cloud (v0.71.18)
Closes #257, #258, #110, #16.

- #257 MiniLLM true on-policy rollout: minillm_on_policy_rollout (Gu et al. §3.1
  autoregressive teacher-mixed rollout, reverse-KL on full distributions,
  grad-to-student-only) + on_policy_term + training.minillm_on_policy /
  minillm_rollout_length, wired into DistillTrainer.compute_loss.
- #258 cross-tokenizer ULD wasserstein_aligned: align_token_sequences (difflib
  char-span) + aggregate_aligned_logits + uld_aligned_loss for fully-disjoint
  tokenizers, wired into DistillTrainer.
- #110 soup agent eval --sandbox: build_eval_stub (base64-embed-as-data) +
  run_eval_in_sandbox (v0.25 RLVR isolation + SANDBOX_NETWORK_GUARD) +
  classify_sandbox_outcome (ok/tool_error/timeout/arg_error).
- #16 soup train --cloud modal: render a Modal app from soup.yaml (config
  base64-embedded), plan-only default, --cloud-submit token-gated; [modal] extra.

+114 tests (tests/test_v07118.py); 13656 -> 13770. Step-6 smoke on real input
(Windows + RTX 3050): Modal stub render, real subprocess sandbox scorecard,
on-policy distill (tiny-gpt2), cross-tokenizer aligned ULD (GPT-2 + Llama).
2026-06-08 23:49:14 +05:00
Alpamys 0d55ba9e46 feat(serve): serve-time MoLE + per-request vector banks + epoch RAFT shuffle (v0.71.17)
Closes #259 (soup serve --mole: load base + N frozen task LoRAs + mole_gate.pt,
blend per-token at decode; train writes mole_manifest.json).
Closes #260 (soup serve --bank active user per-request via contextvars.ContextVar,
no cross-request leak; streaming path re-selects in-context).
Closes #253 (data.raft_epoch_shuffle: re-permute golden/distractor docs each epoch;
epoch=0 == legacy order).
Closes #254 (soup diagnose --citation-style / --shuffle-seed into the live probe).

Fix: MoLE train() returns initial_loss/final_loss/total_steps/duration_secs so
task=moe_lora_routing completes cleanly (surfaced by the #259 smoke).

Validated live on SmolLM2-135M (RTX 3050). 13595 -> 13656 tests.
2026-06-08 20:06:43 +05:00
Alpamys 28a2d8cf82 feat(edit): GPT-2 Conv1D edits, covariance ROME, atomic governor, Mixtral LongLoRA (v0.71.16)
Knowledge edit depth — 4-issue patch (validated on real gpt2 + SmolLM2-135M, RTX 3050).

- #251 GPT-2 (transformer.h / mlp.c_proj Conv1D) support in ROME/MEMIT/AlphaEdit:
  transpose-aware _rank1_update + _alphaedit_project + MEMIT dim-check + PEFT unwrap.
- #250 covariance-preconditioned ROME via --cov-corpus (estimate_key_covariance +
  C^-1 k* solve; fail-loud reject for non-rome; cwd-contained O_NOFOLLOW loader).
- #252 atomic EditGovernor increment: save_state merges this run's delta under the
  cross-process lock (baseline-delta, mirrors namespace_pin).
- #147 Mixtral in the LongLoRA allowlist (is_mixtral_model + MixtralAttention
  forward override + _SEPARATE_QKV_FAMILIES).

Tests: 13511 -> 13595 (+81 in tests/test_v07116.py). Full suite green; ruff clean.
2026-06-07 16:11:56 +05:00
Alpamys d5f98c67d5 fix(loop): config-render, cmaes base-reuse, cost gate, energy hand-off, rank guard (v0.71.15)
#261 iterative_dpo._default_train_fn rendered output as a {dir: ...} mapping
that SoupConfig rejected; render it flat so the spawned soup train succeeds.
#246 CMA-ES merge now loads the base model once and reuses it across the
candidate population (_CachedBaseScorer) instead of reloading per candidate.
#245 soup loop estimate_cost wires run_cost.estimate_run_cost_usd off the last
completed run instead of a 0.0 placeholder; never crashes the daemon.
#244 soup train --track-energy --energy-out persists the measurement JSON so
soup bom emit --energy can attach it to an ML-BOM.
#170 --diagnose-gate is RANK-aware: gate once per cluster (RANK==0), not per node.

Tests: 13476 -> 13511 (+35 in tests/test_v07115.py). Validated end-to-end on
SmolLM2-135M / RTX 3050.
2026-06-07 13:58:18 +05:00
Alpamys 0b621076b9 docs: reposition tagline to "fine-tune and post-train"
Soup spans the full post-training stack — SFT + preference/RL
(DPO/GRPO/PPO/KTO/ORPO/SimPO/IPO/BCO) + distillation + unlearning +
knowledge-edit + steering + RAFT/RA-DIT — not just fine-tuning. "Fine-tune"
alone undersells the RL/alignment surface and the post-training category.

Update the canonical tagline to "Fine-tune and post-train LLMs in one
command. No SSH, no config hell." across the five places it appears:
README hero, PyPI description (pyproject), `soup --help` epilog + callback
docstring (cli.py), package docstring (__init__), and the auto-generated
HF model card (push.py). Searchable "fine-tune" keyword kept as the verb;
"post-train" added as the category claim.

Copy-only; no version bump (description ships with next release).
2026-06-06 18:18:50 +05:00
Alpamys 6ec36b5ff6 feat: live FSDP-shard consolidation + serve KV-cache type + ONNX QA (v0.71.14)
Close the doable tail of the export-QA + deferred-stub family.

- #96 consolidate_shards: lazy-torch safetensors merge, TOCTOU-hardened
  (enforce_under_cwd_and_no_symlink, weights_only=True, atomic_write_bytes),
  16 GiB/shard cap, dup-key shape-conflict reject. --plan-only flag.
- #140 apply_kv_cache_type -> KvCacheRuntime; soup serve --kv-cache-type
  (bf16/f16 dtype, q8_0 quantized cache + hqq probe, fp8 Hopper-gated).
- #71 ONNX export QA: tiny-GPT2 PASS, TinyLlama host-RAM-bound (qa doc).
- #70/#72/#144/#74/#79 deferred to INFRA-BLOCKED tail (kept open).

Tests 13430 -> 13476 (+46). ruff clean. Suite green, 78.52% cov.
2026-06-05 13:39:36 +05:00
Alpamys f528da5328 feat(prompt-compile): live soup compile / distill-prompt / compile-tools / local-rl train (v0.71.13)
Lift the v0.68.0 deferred-stub family to live (closes #225, #226, #227, #229):

- #229 local-rl train --once: harvest thumbs -> DPO/KTO/ORPO train via a
  soup train subprocess (argv list, no shell); state table tracks last_train_at
  (skip-on-no-new-thumbs + skip-on-insufficient-pairs); no --once renders a
  systemd/launchd nightly scheduler scaffold. New local_rl_scheduler.py.
- #226 distill-prompt: call the teacher once per trace (Ollama/Anthropic/vLLM)
  and write a real dataset (sft/kl -> messages; preference -> chosen/rejected).
- #225 compile / #227 compile-tools: live DSPy/GEPA/TextGrad dispatch behind the
  new [compile] extra with a friendly ImportError when absent; injectable seams.

Security: reject \n/\r in the model id + shell-quote ExecStart args (systemd
injection defence). Fix: render train output as a plain string (schema-valid),
with a regression test against SoupConfig.

Tests 13329 -> 13424. Smoked end-to-end: real DPO train on SmolLM2-135M (RTX 3050)
+ real Ollama teacher distillation.
2026-06-04 22:14:43 +05:00
Alpamys a66e4b9ebe feat: architecture + distill + adapter-train live wiring (v0.71.12)
Lift seven schema-only stubs to live, validated on tiny models:

- #145 distill_mode token|sequence — sequence-level teacher-continuation KD
- #146 classifier LoRA — frozen encoder + adapter (classifier/reranker/cross_encoder)
- #148 LLaMA Pro block expansion — per-arch (Llama/Qwen/Mistral) zero-init blocks
- #158 LongLoRA S2 — shifted-sparse attention on Q/K projections (Llama/Mistral/Qwen/Phi)
- #84 Mixture-of-Depths — per-layer top-k token router (use_mod; Llama/Qwen/Mistral)
- #221 VeRA/VB-LoRA serving — soup serve --bank, per-user delta via X-User-Id header
- #222 MoLE — task=moe_lora_routing, per-token gate over N frozen task LoRAs (gate-only)

Tests 13203 -> 13329 (+126; tests/test_v07112.py). ruff clean, coverage 78.49%.
2026-06-04 19:36:04 +05:00
Alpamys f316d334bc feat(rl): live GRPO/RL callbacks — reward-hack, echo-trap, RL ckpt, ULD, MiniLLM, iterative-DPO (v0.71.11)
Lifts the v0.70.0 schema-only build_*_callback / build_uld_projection /
run_iterative_dpo stubs. Validated end-to-end on SmolLM2-135M.

Closes #235, #236, #237, #238, #239, #240, #159, #160

- #235 RewardHackCallback: info_rm cluster-sep / rm_ensemble divergence,
  OK/WARN/HACK, halt on HACK. Shared thread-safe RLSignalBuffer captures
  per-step rewards by wrapping the reward fns (no TRL monkeypatching).
- #236 ULD: Wasserstein-1 / top-k aligned distill loss in DistillTrainer.
- #237 MiniLLM: teacher-mixed length-normalised reverse-KL + pretrain anchor.
- #238 RLCheckpointCallback: adapter + optimizer.pt + manifest + keep_last prune.
- #239 run_iterative_dpo: sample -> RM-score -> build-pairs -> DPO-train per round.
- #240 EchoTrapCallback: n-gram repetition OK/WARN/TRAP, halt on TRAP.
- #159 one-shot WARNING when a GRPO variant compute_loss falls back to super().
- #160 in-place ref-model EMA (no state_dict round-trip) + 0-overlap warning.

Tests 13142 -> 13203 (+62 in tests/test_v07111.py).
2026-06-04 17:14:08 +05:00
Alpamys a2287dd6a5 feat(rag): RAFT span-mask trainer + RA-DIT auto-link + live steering + eval citation (v0.71.10)
Lifts the v0.62.0 RAG-family schema-only stubs to live, validated on SmolLM2-135M:

- #199 RAFT: data.format=raft trains answer-only (prompt span masked to -100,
  [doc-N] citation ids, deterministic doc shuffle by raft_shuffle_seed); rows
  whose prompt fills max_length are dropped with a warning. New utils/raft.py +
  trainer/raft.py (RaftDataCollator + weighted-CE _RaftTrainer).
- #200 soup ra-dit: one-shot two-stage orchestrator (train retriever -> record
  it as the generator's paired retriever -> train generator); a generator-stage
  `soup train` with no retriever set auto-links the latest RA-DIT retriever from
  the Registry. New utils/ra_dit_run.py + commands/ra_dit.py.
- #201 soup steer train/apply + soup serve --steer: live CAA/ITI/RepE fit from
  {positive, negative} pairs + decode-time forward hook (transformers backend).
  Lifts the steering.py apply_steering/build_steering_vector stubs.
- #202 soup eval citation + citation-span per-token loss boost + 7th `citation`
  failure mode in soup diagnose. New commands/_eval_v07110.py +
  diagnose/citation.py.

Review fixes (3 agents, all CRITICAL->LOW): markup-escaped autolink advisory;
shared enforce_under_cwd_and_no_symlink + O_NOFOLLOW on every new file read;
steering-artifact containment; honest RA-DIT docs (records pairing, no weight
fusion); public validate_ra_dit_config_path + render_raft_prompt; repe/iti
require >=2 pairs; eval citation --shuffle-seed.

Full suite: 13034 passed, 106 skipped (13142 collected). ruff clean.
2026-06-03 19:28:45 +05:00
Alpamys 96c339f184 feat(edit): live ROME/MEMIT/AlphaEdit + GRACE + NPO/SimNPO/RMU unlearn (v0.71.9)
Closes #193, #194, #196, #197, #203.

- #194 utils/edit_kernels.py: covariance-free rank-1 ROME/MEMIT/AlphaEdit;
  apply_edit live (load -> optimise residual -> rank-1 update -> save);
  edit diff live before/after generation.
- #196 EditGovernorStore SQLite persistence + cross-process lock.
- #197 apply_edit consults the governor (check_can_edit before, record after).
- #203 GraceCodebook + apply_grace_edit + install_grace_hook + Registry kinds.
- #193 utils/unlearn_kernels.py (NPO/SimNPO/RMU) + live UnlearnTrainerWrapper
  + soup train --task unlearn.

Validated on SmolLM2-135M: ROME 0.0016->0.96, NPO/SimNPO forget loss down.
+81 tests (tests/test_v0719.py). 2 review waves, all findings fixed.
2026-06-03 17:04:03 +05:00
Alpamys 823456c1a5 feat(probe): real probe weights, SAE auto-download, truth/harm, interference --measure, capture-activations (v0.71.8)
Closes #216, #217, #218, #219. Partial #215 (calibrated vectors upstream-gated).

- #215 probe_kernel.py: compute_contrast_probe + load_probe_weights
  (.npz/.npy/.safetensors, O_NOFOLLOW, allow_pickle=False, cwd-contained);
  soup probe sleeper --weights. Synthetic seed fallback retained.
- #216 hubs.snapshot_download (SSRF-hardened, home/cwd/tmp cache, TOFU gate)
  + sae_diff.download_sae (allowlist-before-network + symlink-escape guard);
  soup probe sae-diff --auto-download.
- #217 truth_probe.py + harm_probe.py over probe_kernel; soup probe truth/harm;
  probe pack ships truth+harm per base.
- #218 interference_live.measure_interference_losses (live PEFT multi-adapter,
  add_weighted_adapter cat off-diagonal); soup probe interference --measure.
- #219 live_eval.extract_layer_activations + resolve_layer_module PEFT-fallback;
  soup train --capture-activations writes <output>/activations/activations.json.

Test count 12771 -> 12917 (+146 in tests/test_v0718.py). Step-6 smoke on
SmolLM2-135M (RTX 3050) green; caught + fixed a PEFT-wrapper layer-resolution bug.
2026-06-03 15:00:27 +05:00
Alpamys f097528ac0 feat(eval): live eval runners — advise/tunability/capability/behavior/diagnose (v0.71.7)
Closes #161, #162, #208, #211, #212, #165.

New utils/live_eval.py shared model-loading layer (lazy torch/transformers/peft):
load_model_and_tokenizer, make_generator/make_multi_generator, compute_eval_loss,
lora_probe, measure_logit_agreement, token_f1.

- #161 soup advise --probe-model: live zero/few-shot token-F1 + LoRA probe
- #162 base_model_proximity via held-out logit agreement
- #208 soup tunability --live: per-candidate LoRA probe
- #211 soup eval capability --live --model: lm-eval-harness per task (per-task isolation)
- #212 soup eval behavior --base-model: live pre/post battery diff
- #165 soup diagnose --base-model: utils/diagnose/live.py runs all 6 probes live

Heuristic/neutral paths preserved when no model is supplied. Both new JSONL
readers open with O_NOFOLLOW after cwd-containment (TOCTOU close). +68 tests
(12703 -> 12771). Smoked end-to-end on SmolLM2-135M (RTX 3050).
2026-06-03 00:05:36 +05:00
Alpamys a1463bf716 feat(v0.71.6): live build runner + Magpie generator + 2PL/3PL IRT + augment fix
Lift the v0.69.0 deferred stubs to live + extend IRT + fix a real bug:

- #231 soup build materialises (5 built-in transforms, table/view/incremental
  with SQLite-tracked config-fingerprint cache key, atomic JSONL, --output-dir)
- #232 soup data gen-magpie live (ollama/vllm raw-completion harvest; anthropic
  rejected; optional --quality-filter; dedup-before-response)
- #167 tokenizer-aware memorization probe (sub-word/BPE overlap, library-only)
- #213 soup eval irt-subset --model 2pl|3pl (joint coordinate-ascent MLE)
- #75 fix soup data augment --provider ollama|vllm ImportError + QA log

Security: validate_ollama_url/validate_vllm_url reject 0.0.0.0; augment output
containment+symlink reject; magpie response-body cap.

Tests 12581 -> 12703 (+122 in tests/test_v0716.py). Full suite green, ruff clean.
2026-06-02 22:06:12 +05:00