diff --git a/studio/backend/requirements/no-torch-runtime.txt b/studio/backend/requirements/no-torch-runtime.txt index 3b822ac2a4..f7e761ab42 100644 --- a/studio/backend/requirements/no-torch-runtime.txt +++ b/studio/backend/requirements/no-torch-runtime.txt @@ -42,7 +42,9 @@ anyio sniffio h11 -tokenizers +# Unpinned resolves to 0.23.1+ which breaks `from transformers import +# AutoConfig`; transformers 4.56..5.3 declares tokenizers<=0.23.0. +tokenizers<=0.23.0 transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,!=4.57.4,!=4.57.5,!=5.0.0,!=5.1.0,<=5.3.0 trl>=0.18.2,!=0.19.0,<=0.24.0 sentence-transformers diff --git a/tests/python/test_patch_trl_rl_trainers_defensive.py b/tests/python/test_patch_trl_rl_trainers_defensive.py new file mode 100644 index 0000000000..7c76ac2792 --- /dev/null +++ b/tests/python/test_patch_trl_rl_trainers_defensive.py @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. + +"""Regression tests: _patch_trl_rl_trainers must never raise. + +The wrapper in unsloth/models/rl.py ring-fences the impl so direct +callers (CI shims, downstream tools) don't have to. Lock that +contract here. +""" + +from __future__ import annotations + +import pytest + + +pytest.importorskip("trl") + + +def _import_helpers(): + try: + from unsloth.models.rl import ( + _patch_trl_rl_trainers, + _patch_trl_rl_trainers_impl, + ) + except ImportError as e: + pytest.skip(f"unsloth.models.rl helpers not importable: {e}") + return _patch_trl_rl_trainers, _patch_trl_rl_trainers_impl + + +def test_patch_trl_rl_trainers_swallows_unknown_trainer_name(): + wrapper, _impl = _import_helpers() + assert wrapper("definitely_not_a_real_trainer_xyz") is None + + +def test_patch_trl_rl_trainers_swallows_garbage_input(): + wrapper, _impl = _import_helpers() + for bad in ("", "..", "trainer with space", "sft_trainer; rm -rf /"): + assert wrapper(bad) is None, f"raised on input: {bad!r}" + + +def test_impl_is_separately_exposed(): + # Power users can still call the impl directly for the raising path. + _wrapper, impl = _import_helpers() + assert callable(impl) + + +def test_wrapper_delegates_to_impl(monkeypatch): + from unsloth.models import rl as _rl + + sentinel = object() + calls = [] + + def _fake_impl(trainer_file): + calls.append(trainer_file) + return sentinel + + monkeypatch.setattr(_rl, "_patch_trl_rl_trainers_impl", _fake_impl) + assert _rl._patch_trl_rl_trainers("sft_trainer") is sentinel + assert calls == ["sft_trainer"] + + +def test_wrapper_swallows_impl_exception(monkeypatch): + from unsloth.models import rl as _rl + + def _boom(_trainer_file): + raise RuntimeError("simulated TRL 1.x rename failure") + + monkeypatch.setattr(_rl, "_patch_trl_rl_trainers_impl", _boom) + assert _rl._patch_trl_rl_trainers("sft_trainer") is None diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index ac9b35a822..5200bfefd2 100755 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -540,6 +540,20 @@ def _wrap_grpo_generate_and_score(trainer_cls): def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): + # Defensive wrapper: matches patch_trl_rl_trainers()'s try/except so + # direct callers don't see exceptions from the impl on TRL versions + # that rename or move classes (e.g. TRL 1.x trl.experimental). + try: + return _patch_trl_rl_trainers_impl(trainer_file) + except Exception as e: + logger.info( + f"Unsloth: Could not patch trl.trainer.{trainer_file}: " + f"{type(e).__name__}: {e}" + ) + return + + +def _patch_trl_rl_trainers_impl(trainer_file = "grpo_trainer"): # Patch for vLLM and Unsloth PEFT import trl import trl.trainer