Studio: fix Gemma-4-12B-it not loading (#6054)
* Fix Studio Python, Gemma 4 Unified sidecar, and worker crash messages * Clean up Gemma 4 sidecar test patch contexts * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Polish inference worker crash message * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address transformers tier review feedback * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Route Gemma 4 assistant models to transformers 5.10 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
6e057ffebe
commit
0f00bc1e2a
8 changed files with 657 additions and 100 deletions
|
|
@ -17,6 +17,7 @@ Pattern follows core/training/training.py.
|
|||
import atexit
|
||||
import base64
|
||||
import os
|
||||
import signal
|
||||
import structlog
|
||||
from loggers import get_logger
|
||||
import multiprocessing as mp
|
||||
|
|
@ -239,6 +240,43 @@ class InferenceOrchestrator:
|
|||
"""True if the subprocess is alive."""
|
||||
return self._proc is not None and self._proc.is_alive()
|
||||
|
||||
def _subprocess_crash_message(self, context: str) -> str:
|
||||
"""Return a user-facing crash message with the worker exit status."""
|
||||
context_label = {
|
||||
"wait": "loading the model",
|
||||
"generation": "generating a response",
|
||||
"audio generation": "generating audio",
|
||||
"audio input generation": "processing audio input",
|
||||
}.get(context, context)
|
||||
message = f"The inference worker stopped unexpectedly while {context_label}."
|
||||
|
||||
if self._proc is None:
|
||||
return f"{message} Details: process missing."
|
||||
|
||||
exitcode = self._proc.exitcode
|
||||
pid = self._proc.pid
|
||||
if exitcode is None:
|
||||
return f"{message} Details: pid={pid}."
|
||||
|
||||
if exitcode < 0:
|
||||
signum = -exitcode
|
||||
try:
|
||||
sig_name = signal.Signals(signum).name
|
||||
except ValueError:
|
||||
sig_name = f"SIG{signum}"
|
||||
|
||||
suffix = ""
|
||||
if sig_name == "SIGKILL":
|
||||
suffix = (
|
||||
" This usually means the system killed it under memory pressure. "
|
||||
"Try a smaller model, lower context length, or close other GPU-heavy apps."
|
||||
)
|
||||
return (
|
||||
f"{message}{suffix} " f"Details: pid={pid}, signal={sig_name}, exitcode={exitcode}."
|
||||
)
|
||||
|
||||
return f"{message} Details: pid={pid}, exitcode={exitcode}."
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Queue helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -286,7 +324,7 @@ class InferenceOrchestrator:
|
|||
if resp is None:
|
||||
# Check subprocess health
|
||||
if not self._ensure_subprocess_alive():
|
||||
raise RuntimeError("Inference subprocess crashed during wait")
|
||||
raise RuntimeError(self._subprocess_crash_message("wait"))
|
||||
continue
|
||||
|
||||
rtype = resp.get("type", "")
|
||||
|
|
@ -510,7 +548,7 @@ class InferenceOrchestrator:
|
|||
except queue.Empty:
|
||||
# Timeout — check subprocess health
|
||||
if not self._ensure_subprocess_alive():
|
||||
yield "Error: Inference subprocess crashed during generation"
|
||||
yield f"Error: {self._subprocess_crash_message('generation')}"
|
||||
return
|
||||
continue
|
||||
|
||||
|
|
@ -1028,7 +1066,7 @@ class InferenceOrchestrator:
|
|||
if resp is None:
|
||||
# Check subprocess health
|
||||
if not self._ensure_subprocess_alive():
|
||||
yield "Error: Inference subprocess crashed during generation"
|
||||
yield f"Error: {self._subprocess_crash_message('generation')}"
|
||||
return
|
||||
continue
|
||||
|
||||
|
|
@ -1125,7 +1163,7 @@ class InferenceOrchestrator:
|
|||
|
||||
if resp is None:
|
||||
if not self._ensure_subprocess_alive():
|
||||
raise RuntimeError("Inference subprocess crashed during audio generation")
|
||||
raise RuntimeError(self._subprocess_crash_message("audio generation"))
|
||||
continue
|
||||
|
||||
rtype = resp.get("type", "")
|
||||
|
|
@ -1247,7 +1285,7 @@ class InferenceOrchestrator:
|
|||
|
||||
if resp is None:
|
||||
if not self._ensure_subprocess_alive():
|
||||
yield "Error: Inference subprocess crashed during audio input generation"
|
||||
yield ("Error: " + self._subprocess_crash_message("audio input generation"))
|
||||
return
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -1951,7 +1951,7 @@ def run_training_process(*, event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
}
|
||||
)
|
||||
return
|
||||
# Activate correct transformers version (Gemma-4 needs 5.5.0, etc.)
|
||||
# Activate correct transformers version (Gemma-4 needs a 5.x sidecar, etc.)
|
||||
# before any transformers/mlx-lm imports in _run_mlx_training.
|
||||
try:
|
||||
_activate_transformers_version(model_name)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import importlib.util
|
||||
import sys
|
||||
|
||||
|
||||
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent)
|
||||
if _BACKEND_DIR not in sys.path:
|
||||
sys.path.insert(0, _BACKEND_DIR)
|
||||
|
||||
|
||||
def test_subprocess_crash_message_includes_signal_and_oom_hint():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"inference_orchestrator_under_test",
|
||||
Path(__file__).resolve().parent.parent / "core/inference/orchestrator.py",
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
orchestrator = module.InferenceOrchestrator.__new__(module.InferenceOrchestrator)
|
||||
orchestrator._proc = SimpleNamespace(pid = 1234, exitcode = -9)
|
||||
|
||||
msg = orchestrator._subprocess_crash_message("wait")
|
||||
|
||||
assert msg.startswith("The inference worker stopped unexpectedly while loading the model.")
|
||||
assert "memory pressure" in msg
|
||||
assert "smaller model" in msg
|
||||
assert "Details:" in msg
|
||||
assert "signal=SIGKILL" in msg
|
||||
assert "exitcode=-9" in msg
|
||||
|
|
@ -30,8 +30,11 @@ sys.modules.setdefault("loggers", _loggers_stub)
|
|||
from utils.transformers_version import (
|
||||
_resolve_base_model,
|
||||
_check_tokenizer_config_needs_v5,
|
||||
_check_config_needs_510,
|
||||
_check_config_needs_550,
|
||||
_config_json_cache,
|
||||
_tokenizer_class_cache,
|
||||
_config_needs_510_cache,
|
||||
_config_needs_550_cache,
|
||||
needs_transformers_5,
|
||||
get_transformers_tier,
|
||||
|
|
@ -200,6 +203,7 @@ class TestCheckConfigNeeds550:
|
|||
"""Tests for _check_config_needs_550() local config.json checks."""
|
||||
|
||||
def setup_method(self):
|
||||
_config_json_cache.clear()
|
||||
_config_needs_550_cache.clear()
|
||||
|
||||
def test_gemma4_architecture(self, tmp_path: Path):
|
||||
|
|
@ -253,6 +257,106 @@ class TestCheckConfigNeeds550:
|
|||
mock_urlopen.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _check_config_needs_510 — config.json architecture/model_type check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckConfigNeeds510:
|
||||
"""Tests for _check_config_needs_510() local config.json checks."""
|
||||
|
||||
def setup_method(self):
|
||||
_config_json_cache.clear()
|
||||
_config_needs_510_cache.clear()
|
||||
|
||||
def test_gemma4_unified_architecture(self, tmp_path: Path):
|
||||
"""config.json with Gemma4UnifiedForConditionalGeneration should return True."""
|
||||
cfg = {
|
||||
"architectures": ["Gemma4UnifiedForConditionalGeneration"],
|
||||
"model_type": "gemma4_unified",
|
||||
}
|
||||
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
||||
|
||||
assert _check_config_needs_510(str(tmp_path)) is True
|
||||
|
||||
def test_gemma4_unified_model_type_only(self, tmp_path: Path):
|
||||
"""config.json with model_type=gemma4_unified should return True."""
|
||||
cfg = {"model_type": "gemma4_unified"}
|
||||
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
||||
|
||||
assert _check_config_needs_510(str(tmp_path)) is True
|
||||
|
||||
def test_gemma4_unified_assistant_architecture(self, tmp_path: Path):
|
||||
"""Assistant Gemma 4 Unified configs should return True."""
|
||||
cfg = {
|
||||
"architectures": ["Gemma4UnifiedAssistantForCausalLM"],
|
||||
"model_type": "gemma4_unified_assistant",
|
||||
}
|
||||
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
||||
|
||||
assert _check_config_needs_510(str(tmp_path)) is True
|
||||
|
||||
def test_gemma4_unified_assistant_model_type_only(self, tmp_path: Path):
|
||||
"""Assistant Gemma 4 Unified model_type should return True."""
|
||||
cfg = {"model_type": "gemma4_unified_assistant"}
|
||||
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
||||
|
||||
assert _check_config_needs_510(str(tmp_path)) is True
|
||||
|
||||
def test_gemma4_assistant_architecture(self, tmp_path: Path):
|
||||
"""Assistant Gemma 4 configs should return True."""
|
||||
cfg = {
|
||||
"architectures": ["Gemma4AssistantForCausalLM"],
|
||||
"model_type": "gemma4_assistant",
|
||||
}
|
||||
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
||||
|
||||
assert _check_config_needs_510(str(tmp_path)) is True
|
||||
|
||||
def test_gemma4_assistant_model_type_only(self, tmp_path: Path):
|
||||
"""Assistant Gemma 4 model_type should return True."""
|
||||
cfg = {"model_type": "gemma4_assistant"}
|
||||
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
||||
|
||||
assert _check_config_needs_510(str(tmp_path)) is True
|
||||
|
||||
def test_gemma4_non_unified_returns_false(self, tmp_path: Path):
|
||||
"""Older Gemma 4 config should stay on the 550 tier."""
|
||||
cfg = {
|
||||
"architectures": ["Gemma4ForConditionalGeneration"],
|
||||
"model_type": "gemma4",
|
||||
}
|
||||
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
||||
|
||||
assert _check_config_needs_510(str(tmp_path)) is False
|
||||
|
||||
def test_no_config_json(self, tmp_path: Path):
|
||||
"""Missing config.json should return False (fail-open)."""
|
||||
# Patch network call to avoid real fetch
|
||||
with patch("urllib.request.urlopen") as mock_urlopen:
|
||||
mock_urlopen.side_effect = Exception("no network")
|
||||
assert _check_config_needs_510(str(tmp_path)) is False
|
||||
|
||||
def test_result_is_cached(self, tmp_path: Path):
|
||||
"""Subsequent calls should use the cache."""
|
||||
cfg = {"architectures": ["Gemma4UnifiedForConditionalGeneration"]}
|
||||
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
||||
|
||||
key = str(tmp_path)
|
||||
_check_config_needs_510(key)
|
||||
assert key in _config_needs_510_cache
|
||||
assert _config_needs_510_cache[key] is True
|
||||
|
||||
def test_local_file_skips_network(self, tmp_path: Path):
|
||||
"""When local config.json exists, no network request should be made."""
|
||||
cfg = {"architectures": ["LlamaForCausalLM"]}
|
||||
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
||||
|
||||
with patch("urllib.request.urlopen") as mock_urlopen:
|
||||
_check_config_needs_510(str(tmp_path))
|
||||
mock_urlopen.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_transformers_tier — tier detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -263,11 +367,19 @@ class TestGetTransformersTier:
|
|||
|
||||
def setup_method(self):
|
||||
_tokenizer_class_cache.clear()
|
||||
_config_json_cache.clear()
|
||||
_config_needs_510_cache.clear()
|
||||
_config_needs_550_cache.clear()
|
||||
|
||||
def test_gemma4_substring_returns_550(self):
|
||||
assert get_transformers_tier("google/gemma-4-E2B-it") == "550"
|
||||
|
||||
def test_gemma4_12b_substring_returns_510(self):
|
||||
assert get_transformers_tier("unsloth/gemma-4-12b-it") == "510"
|
||||
|
||||
def test_gemma4_assistant_substring_returns_510(self):
|
||||
assert get_transformers_tier("google/gemma-4-E2B-it-assistant") == "510"
|
||||
|
||||
def test_gemma4_alt_substring_returns_550(self):
|
||||
assert get_transformers_tier("unsloth/gemma4-E4B-it") == "550"
|
||||
|
||||
|
|
@ -281,17 +393,92 @@ class TestGetTransformersTier:
|
|||
|
||||
assert get_transformers_tier(str(tmp_path)) == "550"
|
||||
|
||||
def test_gemma4_unified_config_json_returns_510(self, tmp_path: Path):
|
||||
"""Local checkpoint with Gemma4 Unified architecture → 510."""
|
||||
cfg = {
|
||||
"architectures": ["Gemma4UnifiedForConditionalGeneration"],
|
||||
"model_type": "gemma4_unified",
|
||||
}
|
||||
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
||||
|
||||
assert get_transformers_tier(str(tmp_path)) == "510"
|
||||
|
||||
def test_gemma4_assistant_config_json_returns_510(self, tmp_path: Path):
|
||||
"""Local checkpoint with Gemma4 Assistant architecture → 510."""
|
||||
cfg = {
|
||||
"architectures": ["Gemma4AssistantForCausalLM"],
|
||||
"model_type": "gemma4_assistant",
|
||||
}
|
||||
(tmp_path / "config.json").write_text(json.dumps(cfg))
|
||||
|
||||
assert get_transformers_tier(str(tmp_path)) == "510"
|
||||
|
||||
def test_local_config_json_short_circuits_path_substrings(self, tmp_path: Path):
|
||||
"""Local config.json should prevent false matches from parent directory names."""
|
||||
model_dir = tmp_path / "gemma-4-12b-experiment" / "llama-checkpoint"
|
||||
model_dir.mkdir(parents = True)
|
||||
(model_dir / "config.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"architectures": ["LlamaForCausalLM"],
|
||||
"model_type": "llama",
|
||||
}
|
||||
)
|
||||
)
|
||||
(model_dir / "tokenizer_config.json").write_text(
|
||||
json.dumps({"tokenizer_class": "LlamaTokenizerFast"})
|
||||
)
|
||||
|
||||
with patch("urllib.request.urlopen") as mock_urlopen:
|
||||
assert get_transformers_tier(str(model_dir)) == "default"
|
||||
mock_urlopen.assert_not_called()
|
||||
|
||||
def test_remote_config_json_is_fetched_once_for_config_tiers(self):
|
||||
"""510 and 550 slow-path checks should share one config.json fetch."""
|
||||
|
||||
class _Response:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return json.dumps(
|
||||
{
|
||||
"architectures": ["Gemma4ForConditionalGeneration"],
|
||||
"model_type": "gemma4",
|
||||
}
|
||||
).encode()
|
||||
|
||||
with patch("urllib.request.urlopen", return_value = _Response()) as mock_urlopen:
|
||||
assert get_transformers_tier("org/no-fast-substring-model") == "550"
|
||||
|
||||
assert mock_urlopen.call_count == 1
|
||||
|
||||
def test_qwen35_returns_530(self):
|
||||
with patch(
|
||||
"utils.transformers_version._check_config_needs_550",
|
||||
return_value = False,
|
||||
with (
|
||||
patch(
|
||||
"utils.transformers_version._check_config_needs_550",
|
||||
return_value = False,
|
||||
),
|
||||
patch(
|
||||
"utils.transformers_version._check_config_needs_510",
|
||||
return_value = False,
|
||||
),
|
||||
):
|
||||
assert get_transformers_tier("Qwen/Qwen3.5-9B") == "530"
|
||||
|
||||
def test_ministral_returns_530(self):
|
||||
with patch(
|
||||
"utils.transformers_version._check_config_needs_550",
|
||||
return_value = False,
|
||||
with (
|
||||
patch(
|
||||
"utils.transformers_version._check_config_needs_550",
|
||||
return_value = False,
|
||||
),
|
||||
patch(
|
||||
"utils.transformers_version._check_config_needs_510",
|
||||
return_value = False,
|
||||
),
|
||||
):
|
||||
assert get_transformers_tier("mistralai/Ministral-3-8B-Instruct-2512") == "530"
|
||||
|
||||
|
|
@ -301,6 +488,10 @@ class TestGetTransformersTier:
|
|||
"utils.transformers_version._check_config_needs_550",
|
||||
return_value = False,
|
||||
),
|
||||
patch(
|
||||
"utils.transformers_version._check_config_needs_510",
|
||||
return_value = False,
|
||||
),
|
||||
patch(
|
||||
"utils.transformers_version._check_tokenizer_config_needs_v5",
|
||||
return_value = False,
|
||||
|
|
@ -309,15 +500,22 @@ class TestGetTransformersTier:
|
|||
assert get_transformers_tier("meta-llama/Llama-3-8B") == "default"
|
||||
|
||||
def test_550_checked_before_530(self):
|
||||
"""5.5.0 is checked first — a model matching both gets 550."""
|
||||
"""5.5.0 is checked before 5.3.0 - a model matching both gets 550."""
|
||||
assert get_transformers_tier("gemma-4-model") == "550"
|
||||
|
||||
def test_needs_transformers_5_compat(self):
|
||||
"""needs_transformers_5 should return True for both 530 and 550 models."""
|
||||
"""needs_transformers_5 should return True for 510, 530, and 550 models."""
|
||||
assert needs_transformers_5("unsloth/gemma-4-12b-it") is True
|
||||
assert needs_transformers_5("google/gemma-4-E2B-it") is True
|
||||
with patch(
|
||||
"utils.transformers_version._check_config_needs_550",
|
||||
return_value = False,
|
||||
with (
|
||||
patch(
|
||||
"utils.transformers_version._check_config_needs_550",
|
||||
return_value = False,
|
||||
),
|
||||
patch(
|
||||
"utils.transformers_version._check_config_needs_510",
|
||||
return_value = False,
|
||||
),
|
||||
):
|
||||
assert needs_transformers_5("Qwen/Qwen3.5-9B") is True
|
||||
with (
|
||||
|
|
@ -325,6 +523,10 @@ class TestGetTransformersTier:
|
|||
"utils.transformers_version._check_config_needs_550",
|
||||
return_value = False,
|
||||
),
|
||||
patch(
|
||||
"utils.transformers_version._check_config_needs_510",
|
||||
return_value = False,
|
||||
),
|
||||
patch(
|
||||
"utils.transformers_version._check_tokenizer_config_needs_v5",
|
||||
return_value = False,
|
||||
|
|
|
|||
|
|
@ -510,7 +510,7 @@ _VLM_MODEL_TYPES = {
|
|||
_AUDIO_ONLY_MODEL_TYPES = {"csm", "whisper"}
|
||||
|
||||
# Pre-computed .venv_t5 paths and backend dir for subprocess version switching.
|
||||
# Vision check uses 5.5.0 (newest, recognizes all architectures).
|
||||
# Vision check uses the Gemma 4 5.5 sidecar for existing Gemma 4 architectures.
|
||||
from utils.paths.storage_roots import studio_root as _studio_root # noqa: E402
|
||||
|
||||
_VENV_T5_DIR = str(_studio_root() / ".venv_t5_550")
|
||||
|
|
|
|||
|
|
@ -3,12 +3,27 @@
|
|||
|
||||
"""Automatic transformers version switching.
|
||||
|
||||
Some newer architectures need transformers>=5.3.0 (.venv_t5_530/); Gemma 4
|
||||
needs >=5.5.0 (.venv_t5_550/). Everything else uses the default 4.57.x. A
|
||||
custom-named LoRA adapter's base model is resolved from adapter_config.json.
|
||||
Some newer model architectures (Ministral-3, GLM-4.7-Flash, Qwen3-30B-A3B MoE,
|
||||
tiny_qwen3_moe) require transformers>=5.3.0, while Gemma 4 models require a
|
||||
newer 5.x sidecar. Everything else needs the default 4.57.x that ships with
|
||||
Unsloth.
|
||||
|
||||
Training/inference run in subprocesses that activate the right version via
|
||||
sys.path; export (in-process) uses ensure_transformers_version() for the swap.
|
||||
Two separate target directories are maintained:
|
||||
- .venv_t5_530/ — transformers 5.3.0 (Ministral-3, GLM, Qwen3 MoE, etc.)
|
||||
- .venv_t5_550/ — transformers 5.5.0 (Gemma 4)
|
||||
- .venv_t5_510/ — transformers 5.10.2 (Gemma 4 Unified / 12B)
|
||||
|
||||
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:
|
||||
Training and inference run in subprocesses that activate the correct version
|
||||
via sys.path (prepending the appropriate .venv_t5_*/ directory). See:
|
||||
- core/training/worker.py
|
||||
- core/inference/worker.py
|
||||
|
||||
For export (still in-process), ensure_transformers_version() does a lightweight
|
||||
sys.path swap using the same directories pre-installed by setup.sh.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
|
|
@ -53,13 +68,32 @@ TRANSFORMERS_5_MODEL_SUBSTRINGS: tuple[str, ...] = (
|
|||
"lfm2.5-vl-450m", # LiquidAI/LFM2.5-VL-450M
|
||||
)
|
||||
|
||||
# Lowercase substrings for models that require transformers 5.5.0 (checked first).
|
||||
# Lowercase substrings for models that require transformers 5.10.x (checked first).
|
||||
TRANSFORMERS_510_MODEL_SUBSTRINGS: tuple[str, ...] = (
|
||||
"gemma-4-12b", # Gemma 4 Unified 12B
|
||||
"gemma4-12b",
|
||||
)
|
||||
|
||||
# Lowercase substrings for models that require the Gemma 4 transformers 5.5 sidecar.
|
||||
TRANSFORMERS_550_MODEL_SUBSTRINGS: tuple[str, ...] = (
|
||||
"gemma-4", # Gemma-4 (E2B-it, E4B-it, 31B-it, 26B-A4B-it)
|
||||
"gemma4", # Gemma-4 alternate naming
|
||||
"qwen3.6",
|
||||
)
|
||||
|
||||
# Architecture classes / model_type values that require transformers 5.10.x.
|
||||
# Checked via config.json (local or HuggingFace).
|
||||
_TRANSFORMERS_510_ARCHITECTURES: set[str] = {
|
||||
"Gemma4UnifiedForConditionalGeneration",
|
||||
"Gemma4AssistantForCausalLM",
|
||||
"Gemma4UnifiedAssistantForCausalLM",
|
||||
}
|
||||
_TRANSFORMERS_510_MODEL_TYPES: set[str] = {
|
||||
"gemma4_unified",
|
||||
"gemma4_assistant",
|
||||
"gemma4_unified_assistant",
|
||||
}
|
||||
|
||||
# Architecture classes / model_type values that require transformers 5.5.0.
|
||||
# Checked via config.json (local or HuggingFace).
|
||||
_TRANSFORMERS_550_ARCHITECTURES: set[str] = {
|
||||
|
|
@ -78,21 +112,26 @@ _TRANSFORMERS_5_TOKENIZER_CLASSES: set[str] = {
|
|||
_tokenizer_class_cache: dict[str, bool] = {}
|
||||
|
||||
# Cache for dynamic config.json lookups (architecture/model_type checks).
|
||||
_config_json_cache: dict[str, dict | None] = {}
|
||||
_config_needs_510_cache: dict[str, bool] = {}
|
||||
_config_needs_550_cache: dict[str, bool] = {}
|
||||
|
||||
# Versions
|
||||
TRANSFORMERS_510_VERSION = "5.10.2"
|
||||
TRANSFORMERS_550_VERSION = "5.5.0"
|
||||
TRANSFORMERS_530_VERSION = "5.3.0"
|
||||
TRANSFORMERS_DEFAULT_VERSION = "4.57.6"
|
||||
# Backwards-compat alias — points to 5.5.0 (the highest 5.x tier).
|
||||
# Consumers should prefer TRANSFORMERS_530_VERSION / TRANSFORMERS_550_VERSION.
|
||||
TRANSFORMERS_5_VERSION = TRANSFORMERS_550_VERSION
|
||||
# Backwards-compat alias — points to the highest 5.x tier.
|
||||
# Consumers should prefer TRANSFORMERS_510_VERSION / TRANSFORMERS_550_VERSION /
|
||||
# TRANSFORMERS_530_VERSION.
|
||||
TRANSFORMERS_5_VERSION = TRANSFORMERS_510_VERSION
|
||||
|
||||
# Pre-installed directories — created by setup.sh / setup.ps1.
|
||||
from utils.paths.storage_roots import studio_root as _studio_root # noqa: E402
|
||||
|
||||
_VENV_T5_530_DIR = str(_studio_root() / ".venv_t5_530")
|
||||
_VENV_T5_550_DIR = str(_studio_root() / ".venv_t5_550")
|
||||
_VENV_T5_510_DIR = str(_studio_root() / ".venv_t5_510")
|
||||
# Backwards-compat alias
|
||||
_VENV_T5_DIR = _VENV_T5_550_DIR
|
||||
|
||||
|
|
@ -108,15 +147,34 @@ def activate_transformers_for_subprocess(model_name: str) -> None:
|
|||
resolved = _resolve_base_model(model_name)
|
||||
tier = get_transformers_tier(resolved)
|
||||
|
||||
if tier == "550":
|
||||
if tier == "510":
|
||||
if not _ensure_venv_t5_510_exists():
|
||||
raise RuntimeError(
|
||||
f"Cannot activate transformers {TRANSFORMERS_510_VERSION}: "
|
||||
f".venv_t5_510 missing at {_VENV_T5_510_DIR}"
|
||||
)
|
||||
if _VENV_T5_510_DIR not in sys.path:
|
||||
sys.path.insert(0, _VENV_T5_510_DIR)
|
||||
logger.info(
|
||||
"Activated transformers %s from %s",
|
||||
TRANSFORMERS_510_VERSION,
|
||||
_VENV_T5_510_DIR,
|
||||
)
|
||||
_pp = os.environ.get("PYTHONPATH", "")
|
||||
os.environ["PYTHONPATH"] = _VENV_T5_510_DIR + (os.pathsep + _pp if _pp else "")
|
||||
elif tier == "550":
|
||||
if not _ensure_venv_t5_550_exists():
|
||||
raise RuntimeError(
|
||||
f"Cannot activate transformers 5.5.0: "
|
||||
f"Cannot activate transformers {TRANSFORMERS_550_VERSION}: "
|
||||
f".venv_t5_550 missing at {_VENV_T5_550_DIR}"
|
||||
)
|
||||
if _VENV_T5_550_DIR not in sys.path:
|
||||
sys.path.insert(0, _VENV_T5_550_DIR)
|
||||
logger.info("Activated transformers 5.5.0 from %s", _VENV_T5_550_DIR)
|
||||
logger.info(
|
||||
"Activated transformers %s from %s",
|
||||
TRANSFORMERS_550_VERSION,
|
||||
_VENV_T5_550_DIR,
|
||||
)
|
||||
_pp = os.environ.get("PYTHONPATH", "")
|
||||
os.environ["PYTHONPATH"] = _VENV_T5_550_DIR + (os.pathsep + _pp if _pp else "")
|
||||
elif tier == "530":
|
||||
|
|
@ -260,6 +318,67 @@ def _check_tokenizer_config_needs_v5(model_name: str) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def _load_config_json(model_name: str) -> dict | None:
|
||||
"""Return parsed ``config.json`` for *model_name*, checking local files first."""
|
||||
if model_name in _config_json_cache:
|
||||
return _config_json_cache[model_name]
|
||||
|
||||
local_cfg = Path(model_name) / "config.json"
|
||||
if local_cfg.is_file():
|
||||
try:
|
||||
with open(local_cfg) as f:
|
||||
cfg = json.load(f)
|
||||
_config_json_cache[model_name] = cfg
|
||||
return cfg
|
||||
except Exception as exc:
|
||||
logger.debug("Could not read %s: %s", local_cfg, exc)
|
||||
_config_json_cache[model_name] = None
|
||||
return None
|
||||
|
||||
if _env_offline():
|
||||
_config_json_cache[model_name] = None
|
||||
return None
|
||||
|
||||
import urllib.request
|
||||
|
||||
url = f"https://huggingface.co/{model_name}/raw/main/config.json"
|
||||
try:
|
||||
req = urllib.request.Request(url, headers = {"User-Agent": "unsloth-studio"})
|
||||
with urllib.request.urlopen(req, timeout = 10) as resp:
|
||||
cfg = json.loads(resp.read().decode())
|
||||
_config_json_cache[model_name] = cfg
|
||||
return cfg
|
||||
except Exception as exc:
|
||||
logger.debug("Could not fetch config.json for '%s': %s", model_name, exc)
|
||||
_config_json_cache[model_name] = None
|
||||
return None
|
||||
|
||||
|
||||
def _config_matches_tier(cfg: dict, architectures: set[str], model_types: set[str]) -> bool:
|
||||
archs = cfg.get("architectures", [])
|
||||
if any(a in architectures for a in archs):
|
||||
return True
|
||||
if cfg.get("model_type") in model_types:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _config_needs_550(cfg: dict) -> bool:
|
||||
return _config_matches_tier(
|
||||
cfg,
|
||||
_TRANSFORMERS_550_ARCHITECTURES,
|
||||
_TRANSFORMERS_550_MODEL_TYPES,
|
||||
)
|
||||
|
||||
|
||||
def _config_needs_510(cfg: dict) -> bool:
|
||||
return _config_matches_tier(
|
||||
cfg,
|
||||
_TRANSFORMERS_510_ARCHITECTURES,
|
||||
_TRANSFORMERS_510_MODEL_TYPES,
|
||||
)
|
||||
|
||||
|
||||
def _check_config_needs_550(model_name: str) -> bool:
|
||||
"""True if ``config.json`` has architectures/model_type needing transformers
|
||||
5.5.0 (e.g. Gemma 4).
|
||||
|
|
@ -270,81 +389,88 @@ def _check_config_needs_550(model_name: str) -> bool:
|
|||
if model_name in _config_needs_550_cache:
|
||||
return _config_needs_550_cache[model_name]
|
||||
|
||||
def _check_cfg(cfg: dict) -> bool:
|
||||
archs = cfg.get("architectures", [])
|
||||
if any(a in _TRANSFORMERS_550_ARCHITECTURES for a in archs):
|
||||
return True
|
||||
if cfg.get("model_type") in _TRANSFORMERS_550_MODEL_TYPES:
|
||||
return True
|
||||
return False
|
||||
|
||||
# --- Check local config.json first ------------------------------------
|
||||
local_path = Path(model_name)
|
||||
local_cfg = local_path / "config.json"
|
||||
if local_cfg.is_file():
|
||||
try:
|
||||
with open(local_cfg) as f:
|
||||
cfg = json.load(f)
|
||||
result = _check_cfg(cfg)
|
||||
if result:
|
||||
logger.info(
|
||||
"Local config.json check: %s needs transformers 5.5.0 "
|
||||
"(architectures=%s, model_type=%s)",
|
||||
model_name,
|
||||
cfg.get("architectures", []),
|
||||
cfg.get("model_type"),
|
||||
)
|
||||
_config_needs_550_cache[model_name] = result
|
||||
return result
|
||||
except Exception as exc:
|
||||
logger.debug("Could not read %s: %s", local_cfg, exc)
|
||||
|
||||
# Offline: skip the 10s urllib fetch (fail-open to lower tier).
|
||||
if _env_offline():
|
||||
cfg = _load_config_json(model_name)
|
||||
if cfg is None:
|
||||
_config_needs_550_cache[model_name] = False
|
||||
return False
|
||||
|
||||
# --- Fall back to fetching from HuggingFace ---------------------------
|
||||
import urllib.request
|
||||
result = _config_needs_550(cfg)
|
||||
if result:
|
||||
logger.info(
|
||||
"config.json check: %s needs transformers %s (architectures=%s, model_type=%s)",
|
||||
model_name,
|
||||
TRANSFORMERS_550_VERSION,
|
||||
cfg.get("architectures", []),
|
||||
cfg.get("model_type"),
|
||||
)
|
||||
_config_needs_550_cache[model_name] = result
|
||||
return result
|
||||
|
||||
url = f"https://huggingface.co/{model_name}/raw/main/config.json"
|
||||
try:
|
||||
req = urllib.request.Request(url, headers = {"User-Agent": "unsloth-studio"})
|
||||
with urllib.request.urlopen(req, timeout = 10) as resp:
|
||||
cfg = json.loads(resp.read().decode())
|
||||
result = _check_cfg(cfg)
|
||||
if result:
|
||||
logger.info(
|
||||
"Dynamic config.json check: %s needs transformers 5.5.0 "
|
||||
"(architectures=%s, model_type=%s)",
|
||||
model_name,
|
||||
cfg.get("architectures", []),
|
||||
cfg.get("model_type"),
|
||||
)
|
||||
_config_needs_550_cache[model_name] = result
|
||||
return result
|
||||
except Exception as exc:
|
||||
logger.debug("Could not fetch config.json for '%s': %s", model_name, exc)
|
||||
_config_needs_550_cache[model_name] = False
|
||||
|
||||
def _check_config_needs_510(model_name: str) -> bool:
|
||||
"""Check ``config.json`` for Gemma 4 Unified / 12B architectures."""
|
||||
if model_name in _config_needs_510_cache:
|
||||
return _config_needs_510_cache[model_name]
|
||||
|
||||
cfg = _load_config_json(model_name)
|
||||
if cfg is None:
|
||||
_config_needs_510_cache[model_name] = False
|
||||
return False
|
||||
|
||||
result = _config_needs_510(cfg)
|
||||
if result:
|
||||
logger.info(
|
||||
"config.json check: %s needs transformers %s (architectures=%s, model_type=%s)",
|
||||
model_name,
|
||||
TRANSFORMERS_510_VERSION,
|
||||
cfg.get("architectures", []),
|
||||
cfg.get("model_type"),
|
||||
)
|
||||
_config_needs_510_cache[model_name] = result
|
||||
return result
|
||||
|
||||
|
||||
def get_transformers_tier(model_name: str) -> str:
|
||||
"""Return the transformers tier required for *model_name*.
|
||||
|
||||
``"550"`` for transformers 5.5.0 (e.g. Gemma 4), ``"530"`` for 5.3.0
|
||||
(e.g. Ministral-3, Qwen3 MoE), or ``"default"`` for everything else (4.57.x).
|
||||
The 5.5.0 check runs first, then 5.3.0.
|
||||
Returns ``"510"`` for models needing transformers 5.10.x (Gemma 4 Unified),
|
||||
``"550"`` for models needing transformers 5.5.0 (Gemma 4),
|
||||
``"530"`` for models needing transformers 5.3.0 (e.g. Ministral-3, Qwen3 MoE),
|
||||
or ``"default"`` for everything else (4.57.x).
|
||||
|
||||
Higher 5.x tiers run first.
|
||||
"""
|
||||
lowered = model_name.lower()
|
||||
|
||||
# Local checkpoint names can contain architecture substrings in their
|
||||
# directory names (for example a pytest temp dir). If config.json exists,
|
||||
# trust it before using name heuristics.
|
||||
local_cfg = Path(model_name) / "config.json"
|
||||
if local_cfg.is_file():
|
||||
cfg = _load_config_json(model_name)
|
||||
if cfg is not None and _config_needs_510(cfg):
|
||||
return "510"
|
||||
if cfg is not None and _config_needs_550(cfg):
|
||||
return "550"
|
||||
if cfg is not None:
|
||||
local_tc = Path(model_name) / "tokenizer_config.json"
|
||||
if local_tc.is_file() and _check_tokenizer_config_needs_v5(model_name):
|
||||
return "530"
|
||||
return "default"
|
||||
|
||||
# --- Fast substring checks (no I/O) ------------------------------------
|
||||
if "assistant" in lowered and ("gemma-4" in lowered or "gemma4" in lowered):
|
||||
return "510"
|
||||
if any(sub in lowered for sub in TRANSFORMERS_510_MODEL_SUBSTRINGS):
|
||||
return "510"
|
||||
if any(sub in lowered for sub in TRANSFORMERS_550_MODEL_SUBSTRINGS):
|
||||
return "550"
|
||||
if any(sub in lowered for sub in TRANSFORMERS_5_MODEL_SUBSTRINGS):
|
||||
return "530"
|
||||
|
||||
# --- Slow config fallbacks (local file first, then network) -----------
|
||||
# --- Slow config fallbacks (network for HF IDs) ------------------------
|
||||
if _check_config_needs_510(model_name):
|
||||
return "510"
|
||||
if _check_config_needs_550(model_name):
|
||||
return "550"
|
||||
if _check_tokenizer_config_needs_v5(model_name):
|
||||
|
|
@ -419,6 +545,13 @@ _VENV_T5_530_PACKAGES = (
|
|||
"tiktoken",
|
||||
)
|
||||
|
||||
_VENV_T5_510_PACKAGES = (
|
||||
f"transformers=={TRANSFORMERS_510_VERSION}",
|
||||
"huggingface_hub==1.8.0",
|
||||
"hf_xet==1.4.2",
|
||||
"tiktoken",
|
||||
)
|
||||
|
||||
_VENV_T5_550_PACKAGES = (
|
||||
f"transformers=={TRANSFORMERS_550_VERSION}",
|
||||
"huggingface_hub==1.8.0",
|
||||
|
|
@ -475,7 +608,7 @@ def _venv_dir_is_valid(venv_dir: str, packages: tuple[str, ...]) -> bool:
|
|||
|
||||
|
||||
def _venv_t5_is_valid() -> bool:
|
||||
"""Backwards-compat: check the 5.5.0 venv."""
|
||||
"""Backwards-compat: check the Gemma 4 sidecar venv."""
|
||||
return _venv_dir_is_valid(_VENV_T5_550_DIR, _VENV_T5_550_PACKAGES)
|
||||
|
||||
|
||||
|
|
@ -553,11 +686,24 @@ def _ensure_venv_t5_530_exists() -> bool:
|
|||
|
||||
def _ensure_venv_t5_550_exists() -> bool:
|
||||
"""Ensure .venv_t5_550/ exists with transformers 5.5.0."""
|
||||
return _ensure_venv_dir(_VENV_T5_550_DIR, _VENV_T5_550_PACKAGES, "transformers 5.5.0")
|
||||
return _ensure_venv_dir(
|
||||
_VENV_T5_550_DIR,
|
||||
_VENV_T5_550_PACKAGES,
|
||||
f"transformers {TRANSFORMERS_550_VERSION}",
|
||||
)
|
||||
|
||||
|
||||
def _ensure_venv_t5_510_exists() -> bool:
|
||||
"""Ensure .venv_t5_510/ exists with transformers 5.10.x."""
|
||||
return _ensure_venv_dir(
|
||||
_VENV_T5_510_DIR,
|
||||
_VENV_T5_510_PACKAGES,
|
||||
f"transformers {TRANSFORMERS_510_VERSION}",
|
||||
)
|
||||
|
||||
|
||||
def _ensure_venv_t5_exists() -> bool:
|
||||
"""Backwards-compat: ensure the 5.5.0 venv exists."""
|
||||
"""Backwards-compat: ensure the Gemma 4 5.5 sidecar venv exists."""
|
||||
return _ensure_venv_t5_550_exists()
|
||||
|
||||
|
||||
|
|
@ -577,7 +723,7 @@ def _activate_venv(venv_dir: str, label: str) -> None:
|
|||
|
||||
def _deactivate_5x() -> None:
|
||||
"""Remove all .venv_t5_*/ dirs from sys.path, purge stale modules, reimport."""
|
||||
for d in (_VENV_T5_530_DIR, _VENV_T5_550_DIR):
|
||||
for d in (_VENV_T5_530_DIR, _VENV_T5_550_DIR, _VENV_T5_510_DIR):
|
||||
while d in sys.path:
|
||||
sys.path.remove(d)
|
||||
logger.info("Removed venv_t5 dirs from sys.path")
|
||||
|
|
@ -593,7 +739,9 @@ def _deactivate_5x() -> None:
|
|||
def ensure_transformers_version(model_name: str) -> None:
|
||||
"""Ensure the correct ``transformers`` version is active for *model_name*.
|
||||
|
||||
Uses sys.path with .venv_t5_530/ or .venv_t5_550/ (pre-installed by setup.sh):
|
||||
Uses sys.path with .venv_t5_510/, .venv_t5_550/, or .venv_t5_530/
|
||||
(pre-installed by setup.sh):
|
||||
• Need 5.10.x → prepend .venv_t5_510/ to sys.path, purge modules.
|
||||
• Need 5.5.0 → prepend .venv_t5_550/ to sys.path, purge modules.
|
||||
• Need 5.3.0 → prepend .venv_t5_530/ to sys.path, purge modules.
|
||||
• Need 4.x → remove all .venv_t5_*/ from sys.path, purge modules.
|
||||
|
|
@ -608,7 +756,11 @@ def ensure_transformers_version(model_name: str) -> None:
|
|||
resolved = _resolve_base_model(model_name)
|
||||
tier = get_transformers_tier(resolved)
|
||||
|
||||
if tier == "550":
|
||||
if tier == "510":
|
||||
target_version = TRANSFORMERS_510_VERSION
|
||||
venv_dir = _VENV_T5_510_DIR
|
||||
ensure_fn = _ensure_venv_t5_510_exists
|
||||
elif tier == "550":
|
||||
target_version = TRANSFORMERS_550_VERSION
|
||||
venv_dir = _VENV_T5_550_DIR
|
||||
ensure_fn = _ensure_venv_t5_550_exists
|
||||
|
|
@ -643,7 +795,7 @@ def ensure_transformers_version(model_name: str) -> None:
|
|||
model_name,
|
||||
)
|
||||
return
|
||||
# Different 5.x → must switch (e.g. 5.3.0 loaded but need 5.5.0).
|
||||
# Different 5.x -> need to switch (e.g. 5.3.0 loaded but need 5.10.x).
|
||||
in_memory_major = int(in_memory.split(".")[0])
|
||||
if in_memory_major == target_major and venv_dir is None:
|
||||
# Both are default (4.x) — close enough.
|
||||
|
|
|
|||
122
studio/setup.ps1
122
studio/setup.ps1
|
|
@ -1499,13 +1499,60 @@ if ($IsPipInstall) {
|
|||
}
|
||||
}
|
||||
|
||||
# 1g. Python (>= 3.11 and < 3.14). Prefer py.exe so a 3.14 ahead of 3.13 on PATH does not trip the gate.
|
||||
# 1g. Python (>= 3.11 and < 3.14). Prefer the Studio venv that install.ps1
|
||||
# just created, then py.exe so a 3.14 ahead of 3.13 on PATH does not trip the gate.
|
||||
$HasPython = $null -ne (Get-Command python -ErrorAction SilentlyContinue)
|
||||
$PyLauncher = Get-Command py -CommandType Application -ErrorAction SilentlyContinue
|
||||
$PythonOk = $false
|
||||
$DetectedPyVer = $null
|
||||
|
||||
if ($PyLauncher) {
|
||||
function Get-CompatiblePythonVersion {
|
||||
param([string]$PythonExe)
|
||||
try {
|
||||
$out = & $PythonExe --version 2>&1 | Out-String
|
||||
if ($out -match 'Python (3\.(11|12|13)(\.\d+)?)') {
|
||||
return $Matches[1]
|
||||
}
|
||||
} catch { }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Add-PythonDirToProcessPath {
|
||||
param([string]$PythonExe)
|
||||
try {
|
||||
if ($PythonExe -and (Test-Path -LiteralPath $PythonExe)) {
|
||||
$resolvedDir = Split-Path -Parent $PythonExe
|
||||
$alreadyOnPath = ($env:PATH -split ';' | Where-Object { $_.TrimEnd('\') -ieq $resolvedDir.TrimEnd('\') }).Count -gt 0
|
||||
if (-not $alreadyOnPath) {
|
||||
$env:PATH = "$resolvedDir;$env:PATH"
|
||||
}
|
||||
$script:HasPython = $true
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
$_prereqStudioHome = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:UNSLOTH_STUDIO_HOME)) {
|
||||
$_prereqStudioHome = $env:UNSLOTH_STUDIO_HOME.Trim()
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($env:STUDIO_HOME)) {
|
||||
$_prereqStudioHome = $env:STUDIO_HOME.Trim()
|
||||
} else {
|
||||
$_prereqStudioHome = Join-Path $env:USERPROFILE ".unsloth\studio"
|
||||
}
|
||||
if ($_prereqStudioHome -eq "~" -or $_prereqStudioHome -like "~/*" -or $_prereqStudioHome -like "~\*") {
|
||||
$_prereqStudioHome = (Join-Path $env:USERPROFILE $_prereqStudioHome.Substring(1).TrimStart('/','\'))
|
||||
}
|
||||
$_prereqVenvPython = Join-Path $_prereqStudioHome "unsloth_studio\Scripts\python.exe"
|
||||
if (Test-Path -LiteralPath $_prereqVenvPython) {
|
||||
$_venvPyVer = Get-CompatiblePythonVersion $_prereqVenvPython
|
||||
if ($_venvPyVer) {
|
||||
$DetectedPyVer = $_venvPyVer
|
||||
Add-PythonDirToProcessPath $_prereqVenvPython
|
||||
$PythonOk = $true
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $PythonOk -and $PyLauncher) {
|
||||
foreach ($minor in @("3.13", "3.12", "3.11")) {
|
||||
try {
|
||||
$out = & $PyLauncher.Source "-$minor" --version 2>&1 | Out-String
|
||||
|
|
@ -1517,12 +1564,7 @@ if ($PyLauncher) {
|
|||
try {
|
||||
$resolvedExe = (& $PyLauncher.Source "-$minor" -c "import sys; print(sys.executable)" 2>$null | Select-Object -First 1)
|
||||
if ($resolvedExe -and (Test-Path $resolvedExe)) {
|
||||
$resolvedDir = Split-Path -Parent $resolvedExe
|
||||
$alreadyOnPath = ($env:PATH -split ';' | Where-Object { $_.TrimEnd('\') -ieq $resolvedDir.TrimEnd('\') }).Count -gt 0
|
||||
if (-not $alreadyOnPath) {
|
||||
$env:PATH = "$resolvedDir;$env:PATH"
|
||||
}
|
||||
$HasPython = $true
|
||||
Add-PythonDirToProcessPath $resolvedExe
|
||||
}
|
||||
} catch { }
|
||||
$PythonOk = $true
|
||||
|
|
@ -2340,14 +2382,35 @@ if ($stackExit -ne 0) {
|
|||
$ErrorActionPreference = $prevEAP
|
||||
}
|
||||
|
||||
# ── Pre-install transformers 5.x into .venv_t5_530/ and .venv_t5_550/ ──
|
||||
# ── Pre-install transformers 5.x into .venv_t5_530/, .venv_t5_550/, and .venv_t5_510/ ──
|
||||
# Runs outside the deps fast-path gate so that upgrades from the legacy
|
||||
# single .venv_t5 are always migrated to the tiered layout.
|
||||
# T5 sidecar venvs live under the resolved $StudioHome so custom installs are self-contained.
|
||||
$VenvT5_530Dir = Join-Path $StudioHome ".venv_t5_530"
|
||||
$VenvT5_550Dir = Join-Path $StudioHome ".venv_t5_550"
|
||||
$VenvT5_510Dir = Join-Path $StudioHome ".venv_t5_510"
|
||||
$VenvT5Legacy = Join-Path $StudioHome ".venv_t5"
|
||||
|
||||
function Test-TargetPackageVersion {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$TargetDir,
|
||||
[Parameter(Mandatory = $true)][string]$PackageName,
|
||||
[Parameter(Mandatory = $true)][string]$ExpectedVersion
|
||||
)
|
||||
if (-not (Test-Path -LiteralPath $TargetDir -PathType Container)) { return $false }
|
||||
$packageNorm = $PackageName.Replace("-", "_")
|
||||
foreach ($pattern in @("$packageNorm-*.dist-info", "$PackageName-*.dist-info")) {
|
||||
foreach ($distInfo in @(Get-ChildItem -LiteralPath $TargetDir -Directory -Filter $pattern -ErrorAction SilentlyContinue)) {
|
||||
$metadata = Join-Path $distInfo.FullName "METADATA"
|
||||
if (-not (Test-Path -LiteralPath $metadata -PathType Leaf)) { continue }
|
||||
foreach ($line in (Get-Content -LiteralPath $metadata -ErrorAction SilentlyContinue)) {
|
||||
if ($line -eq "Version: $ExpectedVersion") { return $true }
|
||||
}
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
$_NeedT5Install = $false
|
||||
if (Test-Path -LiteralPath $VenvT5Legacy) {
|
||||
Assert-StudioOwnedOrAbsent -Path $VenvT5Legacy -Label "legacy transformers sidecar venv"
|
||||
|
|
@ -2356,6 +2419,10 @@ if (Test-Path -LiteralPath $VenvT5Legacy) {
|
|||
}
|
||||
if (-not (Test-Path -LiteralPath $VenvT5_530Dir)) { $_NeedT5Install = $true }
|
||||
if (-not (Test-Path -LiteralPath $VenvT5_550Dir)) { $_NeedT5Install = $true }
|
||||
if (-not (Test-Path -LiteralPath $VenvT5_510Dir)) { $_NeedT5Install = $true }
|
||||
if (-not (Test-TargetPackageVersion -TargetDir $VenvT5_530Dir -PackageName "transformers" -ExpectedVersion "5.3.0")) { $_NeedT5Install = $true }
|
||||
if (-not (Test-TargetPackageVersion -TargetDir $VenvT5_550Dir -PackageName "transformers" -ExpectedVersion "5.5.0")) { $_NeedT5Install = $true }
|
||||
if (-not (Test-TargetPackageVersion -TargetDir $VenvT5_510Dir -PackageName "transformers" -ExpectedVersion "5.10.2")) { $_NeedT5Install = $true }
|
||||
# Also reinstall when python deps were updated
|
||||
if (-not $SkipPythonDeps) { $_NeedT5Install = $true }
|
||||
|
||||
|
|
@ -2433,9 +2500,44 @@ if ($script:UnslothVerbose) {
|
|||
if ($tiktokenInstallExit -ne 0) {
|
||||
substep "Could not install tiktoken into .venv_t5_550/ -- Qwen tokenizers may fail" "Yellow"
|
||||
}
|
||||
$ErrorActionPreference = $prevEAP_t5
|
||||
step "transformers" "5.5.0 pre-installed"
|
||||
|
||||
# --- .venv_t5_510 (transformers 5.10.2) ---
|
||||
substep "pre-installing transformers 5.10.2 for Gemma 4 Unified support..."
|
||||
Assert-StudioOwnedOrAbsent -Path $VenvT5_510Dir -Label "transformers 5.10 sidecar venv"
|
||||
if (Test-Path -LiteralPath $VenvT5_510Dir) { Remove-Item -LiteralPath $VenvT5_510Dir -Recurse -Force }
|
||||
[System.IO.Directory]::CreateDirectory($VenvT5_510Dir) | Out-Null
|
||||
Mark-StudioOwned -Path $VenvT5_510Dir
|
||||
foreach ($pkg in @("transformers==5.10.2", "huggingface_hub==1.8.0", "hf_xet==1.4.2")) {
|
||||
if ($script:UnslothVerbose) {
|
||||
Fast-Install --target $VenvT5_510Dir --no-deps $pkg
|
||||
$t5PkgExit = $LASTEXITCODE
|
||||
$output = ""
|
||||
} else {
|
||||
$output = Fast-Install --target $VenvT5_510Dir --no-deps $pkg | Out-String
|
||||
$t5PkgExit = $LASTEXITCODE
|
||||
}
|
||||
if ($t5PkgExit -ne 0) {
|
||||
Write-Host "[FAIL] Could not install $pkg into .venv_t5_510/" -ForegroundColor Red
|
||||
Write-Host $output -ForegroundColor Red
|
||||
$ErrorActionPreference = $prevEAP_t5
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
if ($script:UnslothVerbose) {
|
||||
Fast-Install --target $VenvT5_510Dir tiktoken
|
||||
$tiktokenInstallExit = $LASTEXITCODE
|
||||
$output = ""
|
||||
} else {
|
||||
$output = Fast-Install --target $VenvT5_510Dir tiktoken | Out-String
|
||||
$tiktokenInstallExit = $LASTEXITCODE
|
||||
}
|
||||
if ($tiktokenInstallExit -ne 0) {
|
||||
substep "Could not install tiktoken into .venv_t5_510/ -- Qwen tokenizers may fail" "Yellow"
|
||||
}
|
||||
$ErrorActionPreference = $prevEAP_t5
|
||||
step "transformers" "5.10.2 pre-installed"
|
||||
|
||||
} # end $_NeedT5Install
|
||||
|
||||
# ==========================================================================
|
||||
|
|
|
|||
|
|
@ -587,6 +587,7 @@ fi
|
|||
VENV_DIR="$STUDIO_HOME/unsloth_studio"
|
||||
VENV_T5_530_DIR="$STUDIO_HOME/.venv_t5_530"
|
||||
VENV_T5_550_DIR="$STUDIO_HOME/.venv_t5_550"
|
||||
VENV_T5_510_DIR="$STUDIO_HOME/.venv_t5_510"
|
||||
|
||||
[ -d "$REPO_ROOT/.venv" ] && rm -rf "$REPO_ROOT/.venv"
|
||||
[ -d "$REPO_ROOT/.venv_overlay" ] && rm -rf "$REPO_ROOT/.venv_overlay"
|
||||
|
|
@ -700,9 +701,9 @@ else
|
|||
verbose_substep "python deps check: installed=$_PKG_NAME@${INSTALLED_VER:-unknown} latest=${LATEST_VER:-unknown}"
|
||||
fi
|
||||
|
||||
# ── 6b. Pre-install transformers 5.x into .venv_t5_530/ and .venv_t5_550/ ──
|
||||
# ── 6b. Pre-install transformers 5.x into .venv_t5_530/, .venv_t5_550/, and .venv_t5_510/ ──
|
||||
# Models like GLM-4.7-Flash, Qwen3 MoE need transformers>=5.3.0.
|
||||
# Gemma 4 models need transformers>=5.5.0.
|
||||
# Gemma 4 models need transformers>=5.5.0; Gemma 4 Unified needs 5.10.x.
|
||||
# Pre-install into separate directories to avoid runtime pip overhead.
|
||||
# The training subprocess prepends the appropriate dir to sys.path.
|
||||
#
|
||||
|
|
@ -737,6 +738,21 @@ _assert_studio_owned_or_absent() {
|
|||
exit 1
|
||||
fi
|
||||
}
|
||||
_target_has_pkg_version() {
|
||||
_thpv_dir="$1"
|
||||
_thpv_pkg="$2"
|
||||
_thpv_version="$3"
|
||||
[ -d "$_thpv_dir" ] || return 1
|
||||
_thpv_pkg_norm=$(printf '%s' "$_thpv_pkg" | tr '-' '_')
|
||||
for _thpv_metadata in \
|
||||
"$_thpv_dir"/"$_thpv_pkg_norm"-*.dist-info/METADATA \
|
||||
"$_thpv_dir"/"$_thpv_pkg"-*.dist-info/METADATA
|
||||
do
|
||||
[ -f "$_thpv_metadata" ] || continue
|
||||
grep -qx "Version: $_thpv_version" "$_thpv_metadata" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
_NEED_T5_INSTALL=false
|
||||
if [ -d "$STUDIO_HOME/.venv_t5" ]; then
|
||||
# Legacy layout — migrate
|
||||
|
|
@ -746,6 +762,10 @@ if [ -d "$STUDIO_HOME/.venv_t5" ]; then
|
|||
fi
|
||||
[ ! -d "$VENV_T5_530_DIR" ] && _NEED_T5_INSTALL=true
|
||||
[ ! -d "$VENV_T5_550_DIR" ] && _NEED_T5_INSTALL=true
|
||||
[ ! -d "$VENV_T5_510_DIR" ] && _NEED_T5_INSTALL=true
|
||||
_target_has_pkg_version "$VENV_T5_530_DIR" "transformers" "5.3.0" || _NEED_T5_INSTALL=true
|
||||
_target_has_pkg_version "$VENV_T5_550_DIR" "transformers" "5.5.0" || _NEED_T5_INSTALL=true
|
||||
_target_has_pkg_version "$VENV_T5_510_DIR" "transformers" "5.10.2" || _NEED_T5_INSTALL=true
|
||||
# Also reinstall when python deps were updated (packages may need rebuild)
|
||||
[ "$_SKIP_PYTHON_DEPS" = false ] && _NEED_T5_INSTALL=true
|
||||
|
||||
|
|
@ -769,6 +789,16 @@ if [ "$_NEED_T5_INSTALL" = true ]; then
|
|||
run_quiet "install hf_xet for t5_550" fast_install --target "$VENV_T5_550_DIR" --no-deps "hf_xet==1.4.2"
|
||||
run_quiet "install tiktoken for t5_550" fast_install --target "$VENV_T5_550_DIR" "tiktoken"
|
||||
step "transformers" "5.5.0 pre-installed"
|
||||
|
||||
_assert_studio_owned_or_absent "$VENV_T5_510_DIR" "transformers 5.10 sidecar venv"
|
||||
[ -d "$VENV_T5_510_DIR" ] && rm -rf "$VENV_T5_510_DIR"
|
||||
mkdir -p "$VENV_T5_510_DIR"
|
||||
: > "$VENV_T5_510_DIR/$_STUDIO_OWNED_MARKER" 2>/dev/null || true
|
||||
run_quiet "install transformers 5.10.2" fast_install --target "$VENV_T5_510_DIR" --no-deps "transformers==5.10.2"
|
||||
run_quiet "install huggingface_hub for t5_510" fast_install --target "$VENV_T5_510_DIR" --no-deps "huggingface_hub==1.8.0"
|
||||
run_quiet "install hf_xet for t5_510" fast_install --target "$VENV_T5_510_DIR" --no-deps "hf_xet==1.4.2"
|
||||
run_quiet "install tiktoken for t5_510" fast_install --target "$VENV_T5_510_DIR" "tiktoken"
|
||||
step "transformers" "5.10.2 pre-installed"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue