270 lines
9.4 KiB
Python
270 lines
9.4 KiB
Python
"""
|
|
Automatic transformers version switching.
|
|
|
|
Some newer model architectures (Ministral-3, GLM-4.7-Flash, Qwen3-30B-A3B MoE,
|
|
tiny_qwen3_moe) require transformers>=5.1.0, while everything else needs the
|
|
default 4.57.x that ships with Unsloth.
|
|
|
|
When loading a LoRA adapter with a custom name, we resolve the base model from
|
|
``adapter_config.json`` and check *that* against the model list.
|
|
|
|
Strategy (sys.path overlay):
|
|
• The default transformers (4.57.x) always lives in site-packages.
|
|
• When 5.x is needed, we ``pip install --target <dir> --no-deps`` to a
|
|
separate directory and **prepend** it to ``sys.path``.
|
|
• To revert, we simply **remove** that directory from ``sys.path``.
|
|
• After either change we purge cached modules so the next import picks
|
|
up the correct version.
|
|
"""
|
|
|
|
import importlib
|
|
import importlib.metadata
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Ensure our logger is visible even if root logger isn't configured for INFO.
|
|
if not logger.handlers:
|
|
_handler = logging.StreamHandler()
|
|
_handler.setLevel(logging.INFO)
|
|
_handler.setFormatter(
|
|
logging.Formatter("[%(name)s|%(levelname)s]%(message)s")
|
|
)
|
|
logger.addHandler(_handler)
|
|
logger.setLevel(logging.INFO)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Lowercase substrings — if ANY appears anywhere in the lowered model name,
|
|
# we need transformers 5.x.
|
|
TRANSFORMERS_5_MODEL_SUBSTRINGS: tuple[str, ...] = (
|
|
"ministral-3-", # Ministral-3-{3,8,14}B-{Instruct,Reasoning,Base}-2512
|
|
"glm-4.7-flash", # GLM-4.7-Flash
|
|
"qwen3-30b-a3b", # Qwen3-30B-A3B-Instruct-2507 and variants
|
|
"tiny_qwen3_moe", # imdatta0/tiny_qwen3_moe_2.8B_0.7B
|
|
)
|
|
|
|
# Versions
|
|
TRANSFORMERS_5_VERSION = "5.1.0"
|
|
TRANSFORMERS_DEFAULT_VERSION = "4.57.1"
|
|
|
|
# Persistent directory for the transformers 5.x overlay
|
|
_OVERLAY_DIR = os.path.join(tempfile.gettempdir(), "transformers_5_overlay")
|
|
|
|
|
|
def _resolve_base_model(model_name: str) -> str:
|
|
"""If *model_name* points to a LoRA adapter, return its base model.
|
|
|
|
Checks for ``adapter_config.json`` locally first. Only calls the heavier
|
|
``get_base_model_from_lora`` for paths that are actual local directories
|
|
(avoids noisy warnings for plain HF model IDs).
|
|
|
|
Returns the original *model_name* unchanged if it is not a LoRA adapter.
|
|
"""
|
|
# --- Fast local check ---------------------------------------------------
|
|
local_path = Path(model_name)
|
|
adapter_cfg_path = local_path / "adapter_config.json"
|
|
if adapter_cfg_path.is_file():
|
|
try:
|
|
with open(adapter_cfg_path) as f:
|
|
cfg = json.load(f)
|
|
base = cfg.get("base_model_name_or_path")
|
|
if base:
|
|
logger.info(
|
|
"Resolved LoRA adapter '%s' → base model '%s'",
|
|
model_name, base,
|
|
)
|
|
return base
|
|
except Exception as exc:
|
|
logger.debug("Could not read %s: %s", adapter_cfg_path, exc)
|
|
|
|
# --- Only try the heavier fallback for local directories ----------------
|
|
if local_path.is_dir():
|
|
try:
|
|
from utils.models import get_base_model_from_lora
|
|
base = get_base_model_from_lora(model_name)
|
|
if base:
|
|
logger.info(
|
|
"Resolved LoRA adapter '%s' → base model '%s' "
|
|
"(via get_base_model_from_lora)",
|
|
model_name, base,
|
|
)
|
|
return base
|
|
except Exception as exc:
|
|
logger.debug(
|
|
"get_base_model_from_lora failed for '%s': %s",
|
|
model_name, exc,
|
|
)
|
|
|
|
return model_name
|
|
|
|
|
|
def needs_transformers_5(model_name: str) -> bool:
|
|
"""Return True if *model_name* belongs to an architecture that requires
|
|
``transformers>=5.1.0``."""
|
|
lowered = model_name.lower()
|
|
return any(sub in lowered for sub in TRANSFORMERS_5_MODEL_SUBSTRINGS)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Version switching
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _get_in_memory_version() -> str | None:
|
|
"""Return the transformers version currently loaded in this process."""
|
|
tf = sys.modules.get("transformers")
|
|
if tf is not None:
|
|
return getattr(tf, "__version__", None)
|
|
return None
|
|
|
|
|
|
# All top-level prefixes that hold references to transformers internals.
|
|
_PURGE_PREFIXES = (
|
|
"transformers",
|
|
"unsloth",
|
|
"unsloth_zoo",
|
|
"peft",
|
|
"trl",
|
|
"accelerate",
|
|
"auto_gptq",
|
|
"bitsandbytes",
|
|
)
|
|
|
|
|
|
def _purge_modules() -> int:
|
|
"""Remove all cached modules for transformers and its dependents.
|
|
|
|
Returns the number of modules purged.
|
|
"""
|
|
importlib.invalidate_caches()
|
|
to_remove = [
|
|
k for k in list(sys.modules.keys())
|
|
if any(k == p or k.startswith(p + ".") for p in _PURGE_PREFIXES)
|
|
]
|
|
for key in to_remove:
|
|
del sys.modules[key]
|
|
return len(to_remove)
|
|
|
|
|
|
def _install_overlay() -> None:
|
|
"""Install transformers 5.x into the overlay directory and prepend
|
|
it to ``sys.path`` so it shadows the default site-packages version."""
|
|
# Install if the overlay doesn't already exist
|
|
if not os.path.isdir(_OVERLAY_DIR) or not os.listdir(_OVERLAY_DIR):
|
|
os.makedirs(_OVERLAY_DIR, exist_ok=True)
|
|
cmd = [
|
|
sys.executable, "-m", "pip", "install",
|
|
"--target", _OVERLAY_DIR,
|
|
"--no-deps",
|
|
f"transformers=={TRANSFORMERS_5_VERSION}",
|
|
]
|
|
logger.info("Installing transformers %s to overlay: %s",
|
|
TRANSFORMERS_5_VERSION, " ".join(cmd))
|
|
result = subprocess.run(
|
|
cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0:
|
|
logger.error("pip install failed:\n%s", result.stdout)
|
|
raise RuntimeError(
|
|
f"Failed to install transformers=={TRANSFORMERS_5_VERSION} "
|
|
f"to {_OVERLAY_DIR}.\npip output:\n{result.stdout}"
|
|
)
|
|
logger.info("Overlay install succeeded")
|
|
else:
|
|
logger.info("Overlay directory already exists at %s", _OVERLAY_DIR)
|
|
|
|
# Prepend to sys.path (if not already there)
|
|
if _OVERLAY_DIR not in sys.path:
|
|
sys.path.insert(0, _OVERLAY_DIR)
|
|
logger.info("Prepended %s to sys.path", _OVERLAY_DIR)
|
|
|
|
# Purge old modules and force fresh import
|
|
count = _purge_modules()
|
|
logger.info("Purged %d cached modules", count)
|
|
|
|
import transformers
|
|
logger.info("Loaded transformers %s from overlay", transformers.__version__)
|
|
|
|
|
|
def _remove_overlay() -> None:
|
|
"""Remove the overlay directory from ``sys.path`` so the default
|
|
site-packages version (4.57.x) takes effect again."""
|
|
# Remove from sys.path
|
|
changed = False
|
|
while _OVERLAY_DIR in sys.path:
|
|
sys.path.remove(_OVERLAY_DIR)
|
|
changed = True
|
|
|
|
if changed:
|
|
logger.info("Removed %s from sys.path", _OVERLAY_DIR)
|
|
|
|
# Purge old modules and force fresh import
|
|
count = _purge_modules()
|
|
logger.info("Purged %d cached modules", count)
|
|
|
|
import transformers
|
|
logger.info("Reverted to transformers %s from site-packages",
|
|
transformers.__version__)
|
|
|
|
|
|
def ensure_transformers_version(model_name: str) -> None:
|
|
"""Ensure the correct ``transformers`` version is active for *model_name*.
|
|
|
|
Uses sys.path overlay:
|
|
• Need 5.x → install to separate dir, prepend sys.path, purge modules.
|
|
• Need 4.x → remove overlay from sys.path, purge modules.
|
|
|
|
For LoRA adapters with custom names, the base model is resolved from
|
|
``adapter_config.json`` before checking.
|
|
|
|
Call this at the top of every model-loading code path (training ``/start``,
|
|
inference ``/load``, export ``/load-checkpoint``).
|
|
"""
|
|
# Resolve LoRA adapters to their base model for accurate detection
|
|
resolved = _resolve_base_model(model_name)
|
|
want_5 = needs_transformers_5(resolved)
|
|
target_version = TRANSFORMERS_5_VERSION if want_5 else TRANSFORMERS_DEFAULT_VERSION
|
|
target_major = int(target_version.split(".")[0])
|
|
|
|
# Check what's actually loaded in memory
|
|
in_memory = _get_in_memory_version()
|
|
overlay_active = _OVERLAY_DIR in sys.path
|
|
|
|
logger.info(
|
|
"Version check for '%s' (resolved: '%s'): need=%s, "
|
|
"in_memory=%s, overlay_active=%s",
|
|
model_name, resolved, target_version, in_memory, overlay_active,
|
|
)
|
|
|
|
# --- Already correct? ---------------------------------------------------
|
|
if in_memory is not None:
|
|
in_memory_major = int(in_memory.split(".")[0])
|
|
if in_memory_major == target_major:
|
|
logger.info(
|
|
"transformers %s already loaded — correct for '%s'",
|
|
in_memory, model_name,
|
|
)
|
|
return
|
|
|
|
# --- Switch version -----------------------------------------------------
|
|
if want_5:
|
|
logger.info("Activating transformers %s overlay…", TRANSFORMERS_5_VERSION)
|
|
_install_overlay()
|
|
else:
|
|
logger.info("Reverting to default transformers %s…", TRANSFORMERS_DEFAULT_VERSION)
|
|
_remove_overlay()
|
|
|
|
final = _get_in_memory_version()
|
|
logger.info("✓ transformers version is now %s", final)
|