* Replace standalone Studio wording with Unsloth Replace the single word Studio with Unsloth wherever it is used as shorthand for Unsloth Studio in docs, CLI output, UI strings, i18n locales, workflow display names, comments and docstrings. Kept unchanged: the full name Unsloth Studio, third party product names (LM Studio, Visual Studio, Mac Studio), feature names (Recipe Studio, Fine-tuning Studio and its translations), and all identifiers such as env vars, commands, paths and filenames. * Address review feedback on the Studio wording rename Use "an" before Unsloth where the rename left the article as "a". Restore the split brand where Unsloth and Studio render as two halves of the full product name: the onboarding sidebar subtitle and the IPv6 localhost warning. Scope two messages to the full name Unsloth Studio where plain Unsloth was misleading: the AMD README bullet and the CLI studio setup error.
318 lines
12 KiB
Python
318 lines
12 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Unsloth shim over the shared ``unsloth_zoo.hf_xet_fallback`` Xet -> HTTP stall fallback.
|
|
|
|
Re-exports the shared API and injects Unsloth's marker-aware cache purge
|
|
(``prepare_cache_for_transport``) so the download manager keeps its ``.transport``
|
|
marker semantics on the HTTP retry.
|
|
|
|
Import discipline: ``unsloth_zoo``'s ``__init__`` eagerly imports ``transformers``. The workers
|
|
import this shim at startup (to decide the per-worker Xet env flip) *before* activating the model's
|
|
``transformers`` sidecar. Activation only prepends the sidecar to ``sys.path``, so a ``transformers``
|
|
already cached in ``sys.modules`` (via an eager ``unsloth_zoo`` import here) wins -- pinning the
|
|
default 4.57.x and regressing Qwen3.5 / GLM-4.7 / gemma-4 training with
|
|
``Tokenizer class TokenizersBackend does not exist``. So the shared backend is loaded **lazily**
|
|
(``_load_shared``), only on first use of a heavy download helper, i.e. after the sidecar is active.
|
|
``child_should_disable_xet`` and the ``DEFAULT_*`` constants are defined locally so importing them
|
|
never triggers the heavy load.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from typing import Any, Callable, Optional
|
|
|
|
# Defaults mirror unsloth_zoo.hf_xet_fallback; plain literals so they resolve (including as
|
|
# default args below) without importing unsloth_zoo/transformers.
|
|
DEFAULT_GRACE_PERIOD = 10.0
|
|
DEFAULT_HEARTBEAT_INTERVAL = 30.0
|
|
DEFAULT_STALL_TIMEOUT = 180.0
|
|
|
|
# --- lazy shared-backend loader ----------------------------------------------------------------
|
|
_shared: Any = None
|
|
_shared_available: Optional[bool] = None # None = not yet attempted
|
|
_shared_import_error: Optional[BaseException] = None
|
|
_load_lock = threading.Lock()
|
|
|
|
|
|
def _load_shared() -> bool:
|
|
"""Import ``unsloth_zoo.hf_xet_fallback`` on demand; return True if available. Deferred so
|
|
importing this module at worker startup does not pull transformers in before the sidecar is
|
|
activated. Degrades (returns False) rather than crashing when unsloth_zoo is unavailable."""
|
|
global _shared, _shared_available, _shared_import_error
|
|
if _shared_available is not None:
|
|
return _shared_available
|
|
with _load_lock:
|
|
if _shared_available is not None:
|
|
return _shared_available
|
|
try:
|
|
import unsloth_zoo.hf_xet_fallback as shared
|
|
|
|
_shared = shared
|
|
_shared_available = True
|
|
_shared_import_error = None
|
|
return True
|
|
except Exception as exc: # noqa: BLE001 - any import failure must degrade, not crash
|
|
# unsloth_zoo's __init__ runs torch/GPU detection, which raises on a torch-less/GPU-less
|
|
# host. The download helper needs none of it, so retry via UNSLOTH_ZOO_DISABLE_GPU_INIT.
|
|
_shared_import_error = exc
|
|
import os as _os
|
|
|
|
_prev_gpu_init = _os.environ.get("UNSLOTH_ZOO_DISABLE_GPU_INIT")
|
|
_os.environ["UNSLOTH_ZOO_DISABLE_GPU_INIT"] = "1"
|
|
try:
|
|
import unsloth_zoo.hf_xet_fallback as shared
|
|
|
|
_shared = shared
|
|
_shared_available = True
|
|
_shared_import_error = None
|
|
return True
|
|
except Exception as exc2: # noqa: BLE001 - degrade so Unsloth still boots with plain HF
|
|
_shared_import_error = exc2
|
|
_shared_available = False
|
|
import logging as _logging
|
|
|
|
_logging.getLogger(__name__).warning(
|
|
"unsloth_zoo.hf_xet_fallback unavailable (%s); the Xet stall watchdog is "
|
|
"disabled. Install/upgrade unsloth_zoo (and its torch dependency) to "
|
|
"re-enable automatic Xet -> HTTP download recovery.",
|
|
_shared_import_error,
|
|
)
|
|
return False
|
|
finally:
|
|
if _prev_gpu_init is None:
|
|
_os.environ.pop("UNSLOTH_ZOO_DISABLE_GPU_INIT", None)
|
|
else:
|
|
_os.environ["UNSLOTH_ZOO_DISABLE_GPU_INIT"] = _prev_gpu_init
|
|
|
|
|
|
def child_should_disable_xet(config: dict) -> bool:
|
|
"""Single source of truth for the per-worker Xet env flip (mirrors
|
|
``unsloth_zoo.hf_xet_fallback.child_should_disable_xet``). Deliberately lightweight: importing or
|
|
calling it must NOT pull in unsloth_zoo/transformers, so the worker can decide before activating
|
|
the transformers sidecar (see the module docstring)."""
|
|
return bool(config.get("disable_xet"))
|
|
|
|
|
|
# --- degraded stubs (used only when unsloth_zoo is unavailable) -------------------------------
|
|
class _DegradedDownloadStallError(RuntimeError):
|
|
"""Stub mirror so callers' ``except`` clauses resolve; never raised in degraded mode."""
|
|
|
|
|
|
def _degraded_get_hf_download_state(*args: Any, **kwargs: Any) -> None:
|
|
return None # unmeasurable -> the (absent) watchdog never fires
|
|
|
|
|
|
def _degraded_start_watchdog(
|
|
*,
|
|
on_heartbeat: "Optional[Callable[[str], None]]" = None,
|
|
interval: float = DEFAULT_HEARTBEAT_INTERVAL,
|
|
xet_disabled: bool = False,
|
|
**kwargs: Any,
|
|
) -> "threading.Event":
|
|
# No stall detection, but keep emitting heartbeats so the orchestrator's inactivity deadline
|
|
# is not tripped during a long download.
|
|
stop = threading.Event()
|
|
if on_heartbeat is None:
|
|
return stop
|
|
transport = "https" if xet_disabled else "xet"
|
|
|
|
def _beat() -> None:
|
|
while not stop.wait(interval):
|
|
try:
|
|
on_heartbeat(f"Downloading ({transport} transport)...")
|
|
except Exception:
|
|
pass
|
|
|
|
threading.Thread(
|
|
target = _beat,
|
|
daemon = True,
|
|
name = "hf-xet-degraded-heartbeat",
|
|
).start()
|
|
return stop
|
|
|
|
|
|
def _degraded_cancelled(cancel_event: "Optional[threading.Event]") -> bool:
|
|
return cancel_event is not None and cancel_event.is_set()
|
|
|
|
|
|
def _degraded_hf_hub_download_with_xet_fallback(
|
|
repo_id: str,
|
|
filename: str,
|
|
token: Optional[str],
|
|
*,
|
|
repo_type: str = "model",
|
|
revision: Optional[str] = None,
|
|
cache_dir: Optional[str] = None,
|
|
force_download: bool = False,
|
|
cancel_event: "Optional[threading.Event]" = None,
|
|
**_ignored: Any,
|
|
) -> str:
|
|
# Keep the cancellation contract: do not start or return a download once cancelled.
|
|
if _degraded_cancelled(cancel_event):
|
|
raise RuntimeError("Cancelled")
|
|
|
|
from huggingface_hub import hf_hub_download
|
|
|
|
path = hf_hub_download(
|
|
repo_id = repo_id,
|
|
filename = filename,
|
|
token = token,
|
|
repo_type = repo_type,
|
|
revision = revision,
|
|
cache_dir = cache_dir,
|
|
force_download = force_download,
|
|
)
|
|
if _degraded_cancelled(cancel_event):
|
|
raise RuntimeError("Cancelled")
|
|
return path
|
|
|
|
|
|
def _degraded_snapshot_download_with_xet_fallback(
|
|
repo_id: str,
|
|
*,
|
|
revision: Optional[str] = None,
|
|
token: Optional[str] = None,
|
|
repo_type: str = "model",
|
|
cache_dir: Optional[str] = None,
|
|
allow_patterns: Optional[Any] = None,
|
|
ignore_patterns: Optional[Any] = None,
|
|
force_download: bool = False,
|
|
cancel_event: "Optional[threading.Event]" = None,
|
|
**_ignored: Any,
|
|
) -> str:
|
|
if _degraded_cancelled(cancel_event):
|
|
raise RuntimeError("Cancelled")
|
|
|
|
from huggingface_hub import snapshot_download
|
|
|
|
path = snapshot_download(
|
|
repo_id = repo_id,
|
|
repo_type = repo_type,
|
|
revision = revision,
|
|
token = token,
|
|
cache_dir = cache_dir,
|
|
allow_patterns = allow_patterns,
|
|
ignore_patterns = ignore_patterns,
|
|
force_download = force_download,
|
|
)
|
|
if _degraded_cancelled(cancel_event):
|
|
raise RuntimeError("Cancelled")
|
|
return path
|
|
|
|
|
|
# --- lazy attribute access for the heavy shared API -------------------------------------------
|
|
# ``DownloadStallError`` (class identity matters for ``except``), ``start_watchdog`` and
|
|
# ``get_hf_download_state`` come from the shared backend when available, else the degraded stubs.
|
|
# Resolved via PEP 562 ``__getattr__`` so ``from utils.hf_xet_fallback import X`` triggers the load
|
|
# only for these heavy names, not for ``child_should_disable_xet`` / ``DEFAULT_*``.
|
|
_DEGRADED_ATTRS = {
|
|
"DownloadStallError": _DegradedDownloadStallError,
|
|
"start_watchdog": _degraded_start_watchdog,
|
|
"get_hf_download_state": _degraded_get_hf_download_state,
|
|
}
|
|
|
|
# Annotation-only declarations for the three names above: they bind NO value, so lookup still misses
|
|
# and PEP 562 ``__getattr__`` resolves them lazily -- but ruff/pyflakes see them as defined, so listing
|
|
# them in ``__all__`` does not trip F822 (while F822 still catches a real typo elsewhere in the list).
|
|
DownloadStallError: type
|
|
start_watchdog: Any
|
|
get_hf_download_state: Any
|
|
|
|
|
|
def __getattr__(name: str) -> Any:
|
|
if name in _DEGRADED_ATTRS:
|
|
if _load_shared():
|
|
return getattr(_shared, name)
|
|
return _DEGRADED_ATTRS[name]
|
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
|
|
|
|
# Indirection seam the public wrappers call (and tests monkeypatch): lazy-load the shared backend,
|
|
# then dispatch to it or the degraded stub. The ``_shared_*`` names preserve the pre-refactor contract.
|
|
def _shared_hf_hub_download_with_xet_fallback(*args: Any, **kwargs: Any) -> str:
|
|
impl = (
|
|
_shared.hf_hub_download_with_xet_fallback
|
|
if _load_shared()
|
|
else _degraded_hf_hub_download_with_xet_fallback
|
|
)
|
|
return impl(*args, **kwargs)
|
|
|
|
|
|
def _shared_snapshot_download_with_xet_fallback(*args: Any, **kwargs: Any) -> str:
|
|
impl = (
|
|
_shared.snapshot_download_with_xet_fallback
|
|
if _load_shared()
|
|
else _degraded_snapshot_download_with_xet_fallback
|
|
)
|
|
return impl(*args, **kwargs)
|
|
|
|
|
|
__all__ = [
|
|
"DEFAULT_GRACE_PERIOD",
|
|
"DEFAULT_HEARTBEAT_INTERVAL",
|
|
"DEFAULT_STALL_TIMEOUT",
|
|
"DownloadStallError",
|
|
"child_should_disable_xet",
|
|
"get_hf_download_state",
|
|
"start_watchdog",
|
|
"hf_hub_download_with_xet_fallback",
|
|
"snapshot_download_with_xet_fallback",
|
|
]
|
|
|
|
|
|
def _studio_prepare_for_http(repo_type: str, repo_id: str) -> None:
|
|
"""Unsloth's marker-aware purge before an HTTP resume, keeping the download manager's ``.transport``
|
|
accounting consistent (vs unsloth_zoo's generic default). Guarded: a purge failure is logged,
|
|
not fatal to the retry."""
|
|
try:
|
|
from hub.utils.download_registry import prepare_cache_for_transport
|
|
prepare_cache_for_transport(repo_type, repo_id, "http")
|
|
except Exception as exc:
|
|
try:
|
|
from loggers import get_logger
|
|
get_logger(__name__).debug(
|
|
"Unsloth prepare_cache_for_transport failed for %s: %s", repo_id, exc
|
|
)
|
|
except ModuleNotFoundError as logger_exc:
|
|
if logger_exc.name != "loggers":
|
|
raise
|
|
|
|
|
|
def hf_hub_download_with_xet_fallback(
|
|
repo_id: str,
|
|
filename: str,
|
|
token: Optional[str],
|
|
*,
|
|
cancel_event: Optional[threading.Event] = None,
|
|
repo_type: str = "model",
|
|
revision: Optional[str] = None,
|
|
stall_timeout: float = DEFAULT_STALL_TIMEOUT,
|
|
interval: float = DEFAULT_HEARTBEAT_INTERVAL,
|
|
grace_period: float = DEFAULT_GRACE_PERIOD,
|
|
on_status: Optional[Callable[[str], None]] = None,
|
|
force_download: bool = False,
|
|
) -> str:
|
|
"""Single-file download via the shared fallback with Unsloth's marker-aware HTTP-retry prep.
|
|
``force_download`` re-fetches a newer blob over a cached one (Unsloth's model-update path)."""
|
|
return _shared_hf_hub_download_with_xet_fallback(
|
|
repo_id,
|
|
filename,
|
|
token,
|
|
cancel_event = cancel_event,
|
|
repo_type = repo_type,
|
|
revision = revision,
|
|
stall_timeout = stall_timeout,
|
|
interval = interval,
|
|
grace_period = grace_period,
|
|
on_status = on_status,
|
|
force_download = force_download,
|
|
prepare_for_http_fn = _studio_prepare_for_http,
|
|
)
|
|
|
|
|
|
def snapshot_download_with_xet_fallback(repo_id: str, **kwargs: Any) -> str:
|
|
"""Whole-repo download via the shared fallback with Unsloth's marker-aware HTTP-retry prep."""
|
|
kwargs.setdefault("prepare_for_http_fn", _studio_prepare_for_http)
|
|
return _shared_snapshot_download_with_xet_fallback(repo_id, **kwargs)
|