Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 72 additions & 39 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,61 +72,68 @@ def _has_real_accelerator() -> bool:
return False


def _preload_real_device_type() -> bool:
"""Pre-load the REAL ``unsloth_zoo.device_type`` module under a
def _preload_real_device_type(
package: str = "unsloth_zoo",
prereqs: tuple = ("utils",),
) -> bool:
"""Pre-load the REAL ``<package>.device_type`` module under a
temporarily-mocked ``torch.cuda.is_available()`` so its
``DEVICE_TYPE = get_device_type()`` initialization succeeds without
a real accelerator. Returns True on success; returns False if
torch is not importable at all (the security-audit CI job runs
tests/security/ without installing torch, and those tests don't
need the preload).
need the preload), or if the target package isn't installed.

Parameterised so the same harness works for both ``unsloth_zoo``
(where ``utils.py`` defines ``Version`` before ``device_type``
consumes it) and ``unsloth`` (which has no such prereq).
"""
if "unsloth_zoo.device_type" in sys.modules:
target = f"{package}.device_type"
if target in sys.modules:
return True
pkg_spec = importlib.util.find_spec("unsloth_zoo")
pkg_spec = importlib.util.find_spec(package)
if pkg_spec is None or not pkg_spec.submodule_search_locations:
return False
pkg_path = pkg_spec.submodule_search_locations[0]

import os

skeleton_already = "unsloth_zoo" in sys.modules
skeleton_already = package in sys.modules
if not skeleton_already:
zoo_pkg = types.ModuleType("unsloth_zoo")
zoo_pkg.__path__ = [pkg_path]
zoo_pkg.__spec__ = pkg_spec
zoo_pkg.__package__ = "unsloth_zoo"
sys.modules["unsloth_zoo"] = zoo_pkg
pkg_mod = types.ModuleType(package)
pkg_mod.__path__ = [pkg_path]
pkg_mod.__spec__ = pkg_spec
pkg_mod.__package__ = package
sys.modules[package] = pkg_mod

try:
if "unsloth_zoo.utils" not in sys.modules:
utils_path = os.path.join(pkg_path, "utils.py")
utils_spec = importlib.util.spec_from_file_location(
"unsloth_zoo.utils", utils_path,
)
utils_mod = importlib.util.module_from_spec(utils_spec)
sys.modules["unsloth_zoo.utils"] = utils_mod
for prereq in prereqs:
full = f"{package}.{prereq}"
if full in sys.modules:
continue
prereq_path = os.path.join(pkg_path, f"{prereq}.py")
prereq_spec = importlib.util.spec_from_file_location(full, prereq_path)
prereq_mod = importlib.util.module_from_spec(prereq_spec)
sys.modules[full] = prereq_mod
try:
utils_spec.loader.exec_module(utils_mod)
prereq_spec.loader.exec_module(prereq_mod)
except ModuleNotFoundError as exc:
# Tests that don't need torch (e.g. the tests/security
# subtree which only exercises scanner regex tables and
# subprocess invocations) shouldn't be blocked by the
# device-type preload when torch isn't installed. Pop
# the half-built modules and bail out gracefully.
if "torch" in str(exc):
sys.modules.pop("unsloth_zoo.utils", None)
sys.modules.pop(full, None)
if not skeleton_already:
sys.modules.pop("unsloth_zoo", None)
sys.modules.pop(package, None)
return False
raise

device_type_path = os.path.join(pkg_path, "device_type.py")
dt_spec = importlib.util.spec_from_file_location(
"unsloth_zoo.device_type", device_type_path,
)
dt_spec = importlib.util.spec_from_file_location(target, device_type_path)
dt_mod = importlib.util.module_from_spec(dt_spec)
sys.modules["unsloth_zoo.device_type"] = dt_mod
sys.modules[target] = dt_mod

import torch
_orig_is_avail = torch.cuda.is_available
Expand All @@ -137,11 +144,29 @@ def _preload_real_device_type() -> bool:
torch.cuda.is_available = _orig_is_avail
finally:
if not skeleton_already:
sys.modules.pop("unsloth_zoo", None)
sys.modules.pop(package, None)

return True


def _install_device_type_stub(name: str) -> None:
"""Last-resort stub when the real preload can't run (no torch / no
package installed). Matches the surface ``unsloth`` and ``unsloth_zoo``
consumers read at import time."""
stub = types.ModuleType(name)
stub.DEVICE_TYPE = "cuda"
stub.DEVICE_TYPE_TORCH = "cuda"
stub.DEVICE_COUNT = 1
stub.ALLOW_PREQUANTIZED_MODELS = False
stub.is_hip = lambda: False
stub.get_device_type = lambda: "cuda"
stub.get_device_count = lambda: 1
stub.device_synchronize = lambda *a, **k: None
stub.device_empty_cache = lambda *a, **k: None
stub.device_is_bf16_supported = lambda *a, **k: False
sys.modules[name] = stub


def _patch_torch_cuda_for_import() -> None:
"""Stub torch.cuda.* calls made at IMPORT time on CPU-only CI runners.

Expand Down Expand Up @@ -173,19 +198,20 @@ class _StubDeviceProps:


if not _has_real_accelerator():
if not _preload_real_device_type():
stub = types.ModuleType("unsloth_zoo.device_type")
stub.DEVICE_TYPE = "cuda"
stub.DEVICE_TYPE_TORCH = "cuda"
stub.DEVICE_COUNT = 1
stub.ALLOW_PREQUANTIZED_MODELS = False
stub.is_hip = lambda: False
stub.get_device_type = lambda: "cuda"
stub.get_device_count = lambda: 1
stub.device_synchronize = lambda *a, **k: None
stub.device_empty_cache = lambda *a, **k: None
stub.device_is_bf16_supported = lambda *a, **k: False
sys.modules["unsloth_zoo.device_type"] = stub
if not _preload_real_device_type("unsloth_zoo", prereqs=("utils",)):
_install_device_type_stub("unsloth_zoo.device_type")
# NOTE: we deliberately do NOT stub ``unsloth.device_type`` here.
# Doing so makes ``import unsloth`` succeed on CPU-only CI, which
# then runs ``unsloth/_gpu_init.py:_patch_trl_trainer()`` and
# rebinds ``trl.trainer.sft_trainer.SFTTrainer`` /
# ``transformers.models.ministral.MinistralAttention`` to Unsloth's
# compiled wrappers. ``inspect.getsource(...)`` on those classes
# then returns the wrapper source, which masks upstream and causes
# zoo's drift detectors (test_MinistralAttention_forward_signature,
# test_unsloth_rl_trainer_*) to fail. The cost is that the
# ``test_unsloth_trainer_exec_marker`` smoke test fails on CPU-only
# runners; that failure exists on main too and tracks a separate
# ``unsloth.device_type`` consumer that needs its own CPU fallback.
_patch_torch_cuda_for_import()


Expand All @@ -209,6 +235,13 @@ class _StubDeviceProps:
# ---------------------------------------------------------------------------

def _apply_upstream_import_fixes_for_tests() -> None:
# Let `import unsloth` succeed on a CPU-only CI runner. The flag is
# honoured by unsloth's get_device_type (returns "cuda" sentinel) and
# by PatchFastRL / _patch_trl_trainer (early-return so trl.SFTTrainer
# stays pristine for downstream inspect.getsource drift detectors).
# Production hosts with a real accelerator skip both branches.
import os
os.environ.setdefault("UNSLOTH_ALLOW_CPU", "1")
try:
import unsloth # noqa: F401 # runs unsloth/import_fixes.py
except Exception:
Expand Down
92 changes: 89 additions & 3 deletions tests/test_compiler_rewriter_exhaustive.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@

import pytest

try:
import transformers as _transformers
from packaging.version import Version as _Version
_TX_VERSION = getattr(_transformers, "__version__", "0.0.0")
_TX_IS_5X = _Version(_TX_VERSION) >= _Version("5.0.0")
except Exception:
_TX_VERSION = "unknown"
_TX_IS_5X = False


def _skip_if_transformers_5x(reason: str) -> None:
"""Skip when transformers 5.x removed the anchor the rewriter
probe pins. Keep the detector strict on 4.57.6."""
if _TX_IS_5X:
pytest.skip(
f"transformers {_TX_VERSION}: {reason} (zoo rewriter silently "
"no-ops -- str.replace returns source unchanged)"
)


# Shared helpers (mirror test_upstream_source_patterns.py).

Expand Down Expand Up @@ -601,6 +620,12 @@ def test_compiler_class_pretrainedmodel_finder_pattern():
def test_compiler_routing_weights_to_marker_in_source():
"""``unsloth_zoo/compiler.py:3376`` branches on ``routing_weights.to``
in MoE forward (router-logit-cast / bf16 router fix anchor)."""
_skip_if_transformers_5x(
"MoE forwards refactored on transformers 5.x -- `routing_weights.to` "
"substring no longer present in mixtral/qwen2_moe/qwen3_moe/deepseek_v3. "
"compiler.py:3524 substring-in check just skips the module from the "
"router_logit_cast_modules list"
)
pytest.importorskip("transformers")
candidates = [
"transformers.models.mixtral.modeling_mixtral",
Expand Down Expand Up @@ -1070,14 +1095,41 @@ def test_saving_utils_save_pretrained_state_dict_split_pinned_string():
def test_saving_utils_save_pretrained_state_dict_contiguous_pinned_string():
"""``unsloth_zoo/saving_utils.py:2680-2686`` requires
``state_dict[tensor].contiguous()`` in upstream + replace to
``merge_lora_weights(...)``; RuntimeError otherwise."""
``merge_lora_weights(...)``; RuntimeError otherwise.

transformers 5.x rewrote PreTrainedModel.save_pretrained (sharding /
state-dict iteration moved). zoo's saving_utils.py upfront-anchor
check (``_required_anchors``) detects the missing string and falls
back to vanilla ``model.save_pretrained`` with a warning. The
detector becomes a positive-assertion on 5.x: confirm the anchor is
gone AND zoo's _required_anchors list flags it AND the warning path
fires gracefully (no RuntimeError).
"""
pytest.importorskip("transformers")
import transformers.modeling_utils as mu
try:
src = inspect.getsource(mu.PreTrainedModel.save_pretrained)
except (OSError, TypeError):
pytest.skip("save_pretrained source unavailable")
needle = "state_dict[tensor].contiguous()"
if _TX_IS_5X:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mirror the 5.x guard for the first save anchor

When the latest-HF CI runs on Transformers 5.x where PreTrainedModel.save_pretrained has also dropped state_dict_split = split_torch_state_dict_into_shards, the new production fallback treats that missing anchor as recoverable, but test_saving_utils_save_pretrained_state_dict_split_pinned_string above this block still calls _drift unconditionally before the 5.x handling added here for the later anchors. That leaves the 5.x job failing with DRIFT DETECTED despite the intended fallback; add the same _TX_IS_5X positive-assertion/return for the state_dict_split test.

Useful? React with 👍 / 👎.

assert needle not in src, (
f"transformers {_TX_VERSION}: `{needle}` was expected gone "
"on 5.x but is present; refresh the zoo prod-fix anchor "
"list at saving_utils.py:_required_anchors"
)
# Positive assertion: zoo's prod-fix correctly identifies the
# missing anchor in its preflight check.
import unsloth_zoo.saving_utils as zsu
zsu_src = inspect.getsource(zsu.merge_and_dequantize_lora)
assert needle in zsu_src, (
f"transformers {_TX_VERSION}: anchor `{needle}` missing on "
"5.x but zoo's _required_anchors check doesn't include it; "
"production call merge_and_dequantize_lora() will hit the "
"downstream per-anchor RuntimeError instead of the "
"graceful fallback"
)
return
if needle not in src:
_drift(
"unsloth_zoo/saving_utils.py:2680-2686",
Expand Down Expand Up @@ -1129,14 +1181,36 @@ def test_saving_utils_incremental_save_os_makedirs_pinned_regex():
def test_saving_utils_incremental_save_for_loop_filename_to_tensors_pinned():
"""``unsloth_zoo/saving_utils.py:2526-2533`` requires
``for shard_file, tensors in filename_to_tensors`` in
save_pretrained; RuntimeError otherwise."""
save_pretrained; RuntimeError otherwise.

transformers 5.x renamed the iterator. zoo's prod fix in
``merge_and_dequantize_lora`` runs an upfront anchor check that
includes this string and falls back to vanilla
``model.save_pretrained`` (with a warning) when push_to_hub=True
and the anchor is missing. On 5.x: assert the anchor is gone AND
zoo's preflight check covers it.
"""
pytest.importorskip("transformers")
import transformers.modeling_utils as mu
try:
src = inspect.getsource(mu.PreTrainedModel.save_pretrained)
except (OSError, TypeError):
pytest.skip("save_pretrained source unavailable")
needle = "for shard_file, tensors in filename_to_tensors"
if _TX_IS_5X:
assert needle not in src, (
f"transformers {_TX_VERSION}: `{needle}` was expected gone "
"on 5.x but is present; refresh the zoo prod-fix anchor "
"list at saving_utils.py:_required_anchors"
)
import unsloth_zoo.saving_utils as zsu
zsu_src = inspect.getsource(zsu.merge_and_dequantize_lora)
assert needle in zsu_src, (
f"transformers {_TX_VERSION}: anchor `{needle}` missing on "
"5.x but zoo's _required_anchors check doesn't include it; "
"merge_and_dequantize_lora(push_to_hub=True) will RuntimeError"
)
return
if needle not in src:
_drift(
"unsloth_zoo/saving_utils.py:2526-2533",
Expand Down Expand Up @@ -1358,7 +1432,19 @@ def test_gpt_oss_config_old_class_dedent_compare_marker():
"""``unsloth_zoo/temporary_patches/gpt_oss.py:2808-2810``
line-by-line equality compare of dedented GptOssConfig vs OLD
class; pin ``initial_context_length`` / ``rope_scaling`` field
presence (the Old_GptOssConfig regression target)."""
presence (the Old_GptOssConfig regression target).

transformers 5.x replaced ``rope_theta`` / ``rope_scaling`` /
``initial_context_length`` with the ``rope_parameters`` dict. zoo's
``patch_gpt_oss_config`` gates on
``inspect.getsource(GptOssConfig) == Old_GptOssConfig``, so the
patch silently no-ops on the new shape -- skip the detector on 5.x.
"""
_skip_if_transformers_5x(
"GptOssConfig replaced rope_theta/rope_scaling/initial_context_length "
"with rope_parameters dict; patch site silently no-ops via source-"
"equality gate"
)
pytest.importorskip("transformers")
try:
from transformers.models.gpt_oss.configuration_gpt_oss import GptOssConfig
Expand Down
Loading
Loading