From 0f00bc1e2ad7216d3f096aafa755c2274682f9dc Mon Sep 17 00:00:00 2001 From: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:39:07 +0100 Subject: [PATCH] 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> --- studio/backend/core/inference/orchestrator.py | 48 ++- studio/backend/core/training/worker.py | 2 +- ...st_inference_orchestrator_crash_message.py | 33 ++ .../tests/test_transformers_version.py | 224 ++++++++++++- studio/backend/utils/models/model_config.py | 2 +- studio/backend/utils/transformers_version.py | 302 +++++++++++++----- studio/setup.ps1 | 122 ++++++- studio/setup.sh | 34 +- 8 files changed, 662 insertions(+), 105 deletions(-) create mode 100644 studio/backend/tests/test_inference_orchestrator_crash_message.py diff --git a/studio/backend/core/inference/orchestrator.py b/studio/backend/core/inference/orchestrator.py index e394b342f0..32bb25c976 100644 --- a/studio/backend/core/inference/orchestrator.py +++ b/studio/backend/core/inference/orchestrator.py @@ -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 diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index 357f9712fe..b50e17e5aa 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -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) diff --git a/studio/backend/tests/test_inference_orchestrator_crash_message.py b/studio/backend/tests/test_inference_orchestrator_crash_message.py new file mode 100644 index 0000000000..be1a673e62 --- /dev/null +++ b/studio/backend/tests/test_inference_orchestrator_crash_message.py @@ -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 diff --git a/studio/backend/tests/test_transformers_version.py b/studio/backend/tests/test_transformers_version.py index 915bc4b13b..7c497ba1b1 100644 --- a/studio/backend/tests/test_transformers_version.py +++ b/studio/backend/tests/test_transformers_version.py @@ -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, diff --git a/studio/backend/utils/models/model_config.py b/studio/backend/utils/models/model_config.py index 0df7a1a477..9dc01f83c5 100644 --- a/studio/backend/utils/models/model_config.py +++ b/studio/backend/utils/models/model_config.py @@ -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") diff --git a/studio/backend/utils/transformers_version.py b/studio/backend/utils/transformers_version.py index bf8d14b7cb..6e1571d5ce 100644 --- a/studio/backend/utils/transformers_version.py +++ b/studio/backend/utils/transformers_version.py @@ -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. diff --git a/studio/setup.ps1 b/studio/setup.ps1 index ce94f651f8..c4ee08a15f 100644 --- a/studio/setup.ps1 +++ b/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 # ========================================================================== diff --git a/studio/setup.sh b/studio/setup.sh index 42e9f23626..3e9c9ef3d6 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -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