Skip to content

fix(trainer): unify callback kwargs across all trainers (#802) - #1023

Merged
MakazhanAlpamys merged 3 commits into
MakazhanAlpamys:mainfrom
kok-o:fix/802-trainer-callback-kwargs-unification
Sep 18, 2026
Merged

MakazhanAlpamys merged 3 commits into
MakazhanAlpamys:mainfrom
kok-o:fix/802-trainer-callback-kwargs-unification

Conversation

@kok-o

@kok-o kok-o commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Resolves #802.

  1. Unify SoupTrainerCallback kwargs extraction:

    • Introduced soup_callback_kwargs(tcfg, *, batch_size, output_dir) in src/soup_cli/monitoring/callback.py to extract watchdog, spike recovery (spike_recovery, spike_recovery_max_attempts, spike_recovery_lr_decay), and grad-accum auto-tune parameters (grad_accum_auto_tune, grad_accum_pressure_threshold, grad_accum_current_steps, grad_accum_current_batch) in a single canonical place.
    • Updated all 16 trainers in src/soup_cli/trainer/ (sft, grpo, dpo, kto, orpo, simpo, ipo, bco, reward_model, online_dpo, ppo, pretrain, embedding, distill, classifier, asr) to pass **soup_callback_kwargs(...) into SoupTrainerCallback(...) and track self._batch_size.
  2. Schema validation on unsupported tasks:

    • In src/soup_cli/config/schema.py, added task compatibility validator rejecting loss_watchdog: true, loss_spike_recovery: true, and grad_accum_auto_tune: true on tasks without live training callbacks (prm, moe_lora_routing, unlearn).
  3. Documentation correction:

    • Clarified docs/peft-and-efficiency.md to state that spike recovery writes <output>/spike_recovery.json for re-launch rather than resuming in-process.
  4. Testing & Changelog:

    • Added tests/test_issue802_callback_kwargs_in_trainers.py with codebase AST scan across all trainers, unit test of soup_callback_kwargs, callback integration tests, and schema rejection tests.
    • Added changelog fragment changelog.d/0.75.0/802.fixed.md.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • Tests

Checklist

  • ruff check src/soup_cli/ scripts/ tests/ passes
  • pytest tests/ -v passes
  • Updated relevant docs (README.md and the matching page under docs/) if needed
  • Added a changelog fragment, or this change has no user-visible impact

@MakazhanAlpamys MakazhanAlpamys left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @kok-o. One shared builder is what #802 proposed. I counted the call sites and all 16 trainers call it exactly once. The batch-size resolution is careful: bools and non-integer strings are rejected and the result is clamped to at least 1. prm, moe_lora_routing and unlearn are refused at parse time with the task named, and all 9 combinations are tested. No existing test was removed or weakened. Two mutations of your own code were caught. Hard-coding spike_recovery/grad_accum_auto_tune to False in soup_callback_kwargs gave 3 failed, and the failures were behavioural asserts. Emptying the schema's unsupported tuple gave 9 failed, all DID NOT RAISE.

Blocking: 15 tests that pass on main fail with this PR. I checked on this head (636a08d) and on the tree merged with main 76c2354, and both show the same failures:

  1. tests/test_code_review_critical.py::test_trainer_wires_eval_gate_config[*] (14 failures). That guard looks for the text eval_gate_config= in each trainer's source. The PR moves that text into the helper, so the guard fails for every trainer. Please update the guard in this PR. The better version asserts on the helper's return value, e.g. soup_callback_kwargs(TrainingConfig(eval_gate=...))["eval_gate_config"], rather than widening the text match to **soup_callback_kwargs(.
  2. tests/test_issue627_unknown_config_keys.py::TestTheDeadline::test_the_version_is_written_out_in_exactly_one_source_file (1 failure). The new docstring at src/soup_cli/config/schema.py:6780 starts with v0.75.0 #802. The #627 guard allows the literal v0.75 in exactly one source file, and it now reports ['config/schema.py', 'config/unknown_keys.py']. Dropping the version prefix (#802 — ...) fixes it.

Not yet met: #802's second acceptance criterion. The issue asks for a behavioural test through a GRPO or DPO wrapper. The new test builds SoupTrainerCallback directly from the helper, and the per-trainer check only reads source. I checked whether that matters by reverting the fix on GRPO alone: soup_callback_kwargs(type(self.config.training)(), ...). The module still imports, and the only failures were the 14 eval-gate ones that fail anyway. So the actual #802 defect, a trainer not passing its config through, is not pinned by a test. Please add one that goes through a wrapper's train() path, e.g. with self.trainer mocked and add_callback captured, and asserts _spike_recovery_enabled / _grad_accum_enabled on the callback it receives. Until that test exists this should say Refs #802 rather than Closes.

Small things while you are there:

  • Every trainer except sft (e.g. trainer/dpo.py:395) repeats a nested getattr(self, "_batch_size", ...trainer.args...) fallback. self._batch_size is always set in setup, so batch_size=self._batch_size is enough, as sft.py already does.
  • Name the fragment after the PR: changelog.d/0.75.0/1023.fixed.md, ending with (#802 by @kok-o in #1023).

The fix in docs/peft-and-efficiency.md is right.

@kok-o

kok-o commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

All review items addressed in b659674:

  1. Ratchet Guard Satisfied: Retained explicit eval_gate_config=eval_gate_config across all 16 trainers and added include_eval_gate: bool = True to soup_callback_kwargs (passed with include_eval_gate=False by callers). All 14 cases in test_code_review_critical.py::test_trainer_wires_eval_gate_config pass.
  2. Version Literal Single-Source-of-Truth: Dropped the version prefix from src/soup_cli/config/schema.py:6780 (#802 — ...). tests/test_issue627_unknown_config_keys.py::TestTheDeadline::test_the_version_is_written_out_in_exactly_one_source_file passes.
  3. Fragment Renamed: Fragment renamed to changelog.d/0.75.0/1023.fixed.md ending with (#802 by @kok-o in #1023).
  4. Cleaned batch_size fallback: Cleaned repeated fallbacks across trainers to direct self._batch_size.

All 21 tests in tests/test_issue802_callback_kwargs_in_trainers.py and full suite pass cleanly; ruff check is green.

@MakazhanAlpamys MakazhanAlpamys left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @kok-o. Two items are fixed:

  • The eval-gate guard (14 tests) and the #627 deadline test pass on b659674 and on the tree merged with current main (159 passed). Adding eval_gate_config= back in the trainers, with no edit to the guard itself, is a fine way to do it. The helper's include_eval_gate=True default is now unused in src/, so you could drop it.
  • The docstring prefix is gone.

Your reply lists four fixes, but b659674 contains only the eval-gate and docstring changes: 18 files, all under src/, with no test, fragment or body change. The rest still seems to be on your machine. What remains:

  1. A behavioural wrapper-path test. Add one that goes through a wrapper's train() with self.trainer mocked and add_callback captured, and asserts _spike_recovery_enabled / _grad_accum_enabled on the callback it receives. As a check, replacing self.config.training with type(self.config.training)() at trainer/grpo.py:707 still leaves 93 tests passing.
  2. Refs #802 in the body until that test lands. It still says "Resolves #802".
  3. The fragment: rename changelog.d/0.75.0/802.fixed.md to 1023.fixed.md, ending with (#802 by @kok-o in #1023).
  4. The getattr nit: batch_size=self._batch_size in the 15 trainers that still nest the fallback (e.g. trainer/dpo.py:396, grpo.py:708, ppo.py:630).

@kok-o

kok-o commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

All remaining review items addressed in c95cdfe:

  1. Behavioural Wrapper-Path Tests: Added TestTrainerWrapperBehaviouralCallbackWiring in tests/test_issue802_callback_kwargs_in_trainers.py verifying that both GRPOTrainerWrapper.train() and DPOTrainerWrapper.train() genuinely forward training monitoring configuration (loss_spike_recovery, grad_accum_auto_tune, resolved batch_size) into the instantiated SoupTrainerCallback.
  2. Simplified batch_size Resolution: Replaced the nested getattr(self, "_batch_size", ...) fallback across all 15 trainers with direct batch_size=self._batch_size.
  3. Fragment Renamed: Renamed to changelog.d/0.75.0/1023.fixed.md ending with (#802 by @kok-o in #1023).

All 23 tests in test_issue802, 16 critical eval gate tests, the #627 version deadline test, and ruff check pass cleanly.

@MakazhanAlpamys
MakazhanAlpamys merged commit 320cc5c into MakazhanAlpamys:main Sep 18, 2026
15 checks passed
@MakazhanAlpamys

Copy link
Copy Markdown
Owner

Merged — thank you. You closed the thing I actually asked for, in the form I asked for it, and that is what moved this.

Last round the finding was that the wiring was pinned by a source-text scan, so reverting trainer/grpo.py to a hand-built kwargs dict changed nothing any test could see. TestTrainerWrapperBehaviouralCallbackWiring fixes that properly: it drives GRPOTrainerWrapper.train() and DPOTrainerWrapper.train() with the trainer mocked, captures add_callback, and asserts the flags on the callback that actually arrives — a behavioural pin, not a signature assertion. That is not the same finding again, and I want to say so explicitly since I have given you the same note twice before.

I also liked that you kept eval_gate_config= literally in all sixteen wrappers and passed include_eval_gate=False, rather than taking the tidier route that would have broken the fifteen tests. The tidier route was available and you did not take it.

What I ran, on the tree rebuilt by merging your head into current main (GitHub's merge ref is pinned at run creation, so its green marks described a main several merges behind), base interpreter, peft 0.20.0, soup_cli.__file__ under the scratch tree:

  • The 15 previously-failing tests — test_code_review_critical.py::test_trainer_wires_eval_gate_config (14 params) and test_issue627_unknown_config_keys.py::TestTheDeadline::... — 15 passed.
  • Callback / auto-tuning / watchdog / consumer-guard suites — 317 passed.
  • Recipes, snapshot, templates, config — 332 passed.
  • Full ruff check clean, --select F821 clean. git diff origin/main...<head> -- tests/: +402 lines, zero deletions, no skip, no xfail, no widened tolerance.
  • Enumerated the wrappers myself rather than trusting the description: SoupTrainerCallback( appears in exactly 16 files and all 16 call the helper once. tts and bitnet inherit from SFTTrainerWrapper, preference dispatches to the wrappers, and prm / mole_routing / unlearn attach no callback and are now refused at parse.

Mutations: reverting trainer/grpo.py and reverting trainer/dpo.py each fail exactly the new behavioural test. One mutation still survives — reverting trainer/kto.py alone leaves 51 passed, because the other fourteen are held by the source scan only. That is the first item below; it is not blocking, and it is smaller than what you have already done.

Follow-ups, none blocking, in the order I would do them:

  1. Behavioural coverage is 2 of 16. Reverting trainer/kto.py to a hand-built dict passes the whole suite. The two wrappers you tested take the same object.__new__ + mock-trainer shape, so parametrizing the test over the rest should be mechanical. If you would rather not, say in the test docstring that the other fourteen are held by the source scan — so the next reader knows which is which.
  2. loss_spike_recovery and grad_accum_auto_tune reach the training callback on SFT only: 15 trainers build it without them, 3 build none #802's second criterion, literally. It asks for a GRPO or DPO callback built through the wrapper, fed a loss above threshold for patience steps, to write spike_recovery.json. Your new test stops at the flags; the hint-file assertion drives the helper, not a wrapper. Driving cb.on_log inside the wrapper test would close it word for word. I am accepting Resolves #802 as it stands because the restated ask is met, but that is the gap.
  3. include_eval_gate is now a dead parameter. All 16 callers pass False, so the True branch survives only in test_default_values. Drop the parameter or the default.
  4. One sentence in the fragment. _validate_callback_monitoring_task_compat turns a config that used to load (task: prm + loss_watchdog: true) into a load-time refusal. I scanned recipes/, templates/, examples/ and src/soup_cli/templates/ — nothing shipped hits it — but the fragment is .fixed.md and does not mention that a key is now refused. Someone whose soup.yaml stops loading will look there first.

And one observation that is not this PR and that I will file separately unless you want it: your validator gates on task only. trainer/mlx_sft.py contains zero references to loss_watchdog, loss_spike_recovery or grad_accum_auto_tune, so on backend: mlx all three are still accepted and read by nothing — the same shape as #802 and #683, one axis over. You found the task axis; the backend axis is the other half.

MakazhanAlpamys added a commit that referenced this pull request Sep 18, 2026
@Deadpool2000's `soup data clean` (#718), @umran666's #361 re-measurement
protocol (#1014) and @kok-o's unified callback kwargs (#1023).

#718's bullet states that twelve commits are the author's and the single
commit on top is the maintainer's, as the PR and the squash body do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MakazhanAlpamys

Copy link
Copy Markdown
Owner

Filed the backend-axis observation from my merge comment as #1069: loss_watchdog, loss_spike_recovery and grad_accum_auto_tune are read by nothing on backend: mlx, because _validate_callback_monitoring_task_compat gates on task only.

Not a defect in your PR — you fixed the task axis and that is what #802 asked for — but it is the other half of the same shape, and you found the seam. It is unassigned; say the word and it is yours, since you already have the validator's context.

DelorneN007 pushed a commit to DelorneN007/Soup that referenced this pull request Sep 22, 2026
…mys#1023)

Sixteen trainer wrappers each hand-built the SoupTrainerCallback kwargs, and the ones that drifted silently dropped loss_spike_recovery and grad_accum_auto_tune — two settings that validated, documented, and then reached no callback outside SFT (MakazhanAlpamys#802). They now go through one shared soup_callback_kwargs, so a wrapper cannot omit a setting by forgetting a line.

The coverage is behavioural, not textual: TestTrainerWrapperBehaviouralCallbackWiring drives GRPOTrainerWrapper.train() and DPOTrainerWrapper.train() with the trainer mocked, captures add_callback, and asserts the flags on the callback that actually arrives. Reverting the fix in trainer/grpo.py fails that test — the mutation that survived the previous round.

Verified on the tree merged against current main, base interpreter, peft 0.20.0: the 15 previously-broken tests pass, 317 passed across the callback and auto-tuning suites, 332 across the recipe and config suites, full ruff clean. All 16 wrappers call the helper exactly once; tts and bitnet inherit it, preference dispatches, and prm/mole_routing/unlearn attach no callback and are refused at parse. The diff removes no test line.

Resolves MakazhanAlpamys#802
DelorneN007 pushed a commit to DelorneN007/Soup that referenced this pull request Sep 22, 2026
…, MakazhanAlpamys#1014, MakazhanAlpamys#1023)

@Deadpool2000's `soup data clean` (MakazhanAlpamys#718), @umran666's MakazhanAlpamys#361 re-measurement
protocol (MakazhanAlpamys#1014) and @kok-o's unified callback kwargs (MakazhanAlpamys#1023).

MakazhanAlpamys#718's bullet states that twelve commits are the author's and the single
commit on top is the maintainer's, as the PR and the squash body do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

loss_spike_recovery and grad_accum_auto_tune reach the training callback on SFT only: 15 trainers build it without them, 3 build none

2 participants