Skip to content

Report a trainer kwarg that neither the trainer nor the config takes - #9944

Open
vineethsaivs wants to merge 2 commits into
unslothai:mainfrom
vineethsaivs:fix/trainer-kwarg-silent-drop
Open

Report a trainer kwarg that neither the trainer nor the config takes#9944
vineethsaivs wants to merge 2 commits into
unslothai:mainfrom
vineethsaivs:fix/trainer-kwarg-silent-drop

Conversation

@vineethsaivs

Copy link
Copy Markdown
Contributor

The bug

_backwards_compatible_trainer (unsloth/trainer.py) exists to move the keywords trl relocated from the trainer onto the config. It sorts every keyword into one of two dicts:

for key, value in kwargs.items():
    if key in trainer_params:
        trainer_kwargs[key] = value
    elif key in moved_params or key in config_fields:
        additional_config_kwargs[key] = value
    else:
        additional_config_kwargs[key] = value    # same dict as the elif

The elif and the else write to the same dict, so the classification is computed and then discarded. The branch every real call takes re-tests the same condition and copies across only what the config recognises:

config = training_args
for key, value in additional_config_kwargs.items():
    if key in config_fields or key in moved_params:
        setattr(config, key, value)

Anything that fails that test is dropped without a word.

max_seq_length is the case that bites. It is the keyword the older notebooks pass straight to SFTTrainer, and trl 0.20 removed it from SFTConfig, so on current trl it is on neither the trainer signature nor the config:

trainer = SFTTrainer(
    model = model,
    args = SFTConfig(output_dir = "out"),
    train_dataset = ds,
    max_seq_length = 2048,      # silently gone
)
# trainer.args.max_length == 1024, the default

Nothing is printed and training runs at the default length. Every other path reports it: plain trl raises TypeError: __init__() got an unexpected keyword argument 'max_seq_length', and even the sibling branch here raises it out of config_class(**config_dict).

The fix

Leave the unrecognised keys on the trainer call, which is the distinction the else branch was written to make and never made. The trainer then reports them the way an unexpected keyword normally is reported, instead of the value disappearing.

No in-repo caller is affected: unsloth-cli.py already passes max_length on the config, and every SFTTrainer(**trainer_kwargs) in studio/backend/core/training/trainer.py builds its dict from real trainer parameters only.

Test

test_a_kwarg_neither_side_takes_is_reported_not_swallowed in tests/test_warnings_issued_guard.py, next to the existing kwargs-moving tests, which use a real SFTConfig because new_init branches on isinstance(training_args, TrainingArguments).

Before the change:

FAILED tests/test_warnings_issued_guard.py::test_a_kwarg_neither_side_takes_is_reported_not_swallowed
1 failed, 27 passed, 2 skipped

After:

28 passed, 2 skipped in 0.32s

tests/python/test_mlx_public_trainer_api.py and tests/version_compat/test_trl_grpo_pinned_symbols.py also exercise this wrapper: 871 passed, 177 skipped, with one failure (test_mlx_training_arguments_normalize_optim_and_object_aliases) that is present on main unchanged and unrelated to this.

One test-helper change

_sft_config() now imports SFTConfig from trl.trainer.sft_config rather than reading trl.SFTConfig. On Apple Silicon the import unsloth in tests/conftest.py rebinds the top-level name to the MLX training config, which carries none of SFTConfig's fields, so three tests in this file fail there on main today:

FAILED tests/test_warnings_issued_guard.py::test_config_kwargs_reach_the_config_the_caller_passed
FAILED tests/test_warnings_issued_guard.py::test_the_callers_own_config_object_is_the_one_used
FAILED tests/test_warnings_issued_guard.py::test_untouched_config_values_keep_what_the_caller_set
E   AttributeError: '_MLXSFTConfig' object has no attribute 'max_length'

The two names are the same object everywhere else, so this is a no-op on CI and makes the helper's own comment ("a real trl config, not a stand-in") true on macOS too.

`_backwards_compatible_trainer` sorts every keyword into trainer kwargs or
config kwargs, but the `elif` and the `else` write to the same dict, so the
classification it just made is thrown away. The branch every real call takes
then re-tests the same condition and only copies across the keys the config
recognises, which leaves everything else on the floor.

`max_seq_length` is the case that bites. It is the kwarg the older notebooks
pass to `SFTTrainer`, and trl 0.20 removed it from `SFTConfig`, so today it
sits on neither side: the wrapper drops it in silence and the run trains at
the default length instead of the one that was asked for. Without the wrapper
Python reports it, and the other branch here still does, raising `TypeError`
out of `config_class(**config_dict)`.

Leave those keys on the trainer call so the trainer reports them, which is the
distinction the `else` branch was written to make and never made.

The test helper now takes `SFTConfig` from `trl.trainer.sft_config` rather than
the `trl` top level. On Apple Silicon the `import unsloth` in tests/conftest.py
rebinds the top-level name to the MLX training config, which carries none of
SFTConfig's fields, and three tests in this file already fail there for that
reason.
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