mirror of
https://github.com/unslothai/unsloth.git
synced 2026-07-10 00:08:58 +00:00
* Studio: keep transformers off sys.modules until the training worker activates the sidecar
The training worker (core/training/worker.py:run_training_process) decides the per-worker
Xet env flip during preflight by importing utils/hf_xet_fallback.py, which eagerly imported
unsloth_zoo at module load. unsloth_zoo's __init__ imports transformers, so the default
transformers 4.57.x was cached in sys.modules before activate_transformers_for_subprocess
prepended the 5.x sidecar to sys.path. Since activation only edits sys.path, the already
cached module won, and 5.x models failed to load their tokenizer or config:
- Qwen3.5 / GLM-4.7 (tokenizer_class TokenizersBackend): "Tokenizer class TokenizersBackend
does not exist or is not currently imported."
- gemma-4: "... is not supported yet in transformers==4.57.6."
Fix: load the shared unsloth_zoo backend lazily (only when a heavy download helper is first
used, which is after activation). child_should_disable_xet and the DEFAULT_* constants are
defined locally so importing the shim stays light. The download wrappers, the DownloadStallError
class, start_watchdog and get_hf_download_state resolve the shared backend on first use, and the
degraded no-unsloth_zoo fallback is preserved.
Tests:
- test_hf_xet_fallback.py: existing suite kept green via the restored _shared_* seam; the
GPU-init retry test now triggers the lazy load explicitly; new guard asserts importing
child_should_disable_xet does not import transformers/unsloth_zoo.
- test_training_worker_import_discipline.py: new invariant test that the worker preflight
imports leave transformers unimported, so this class of regression cannot return silently.
Runs in studio-backend-ci (CPU only, no network/GPU/weights).
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Studio: CPU-only guard that activation switches transformers to the model's sidecar version
Adds test_worker_activates_correct_transformers.py: runs the real worker preflight
(from utils.hf_xet_fallback import child_should_disable_xet) plus the real tier
detection and activate_transformers_for_subprocess for a transformers-5.x model
(Qwen3.5, tier 530), then asserts the in-process transformers actually switched to
the 5.x sidecar. A stale pre-activation import leaves 4.57.x pinned and fails the
assertion, which is exactly the TokenizersBackend regression (#6951).
Self-contained CUDA spoof (mirrors tests/_zoo_aggressive_cuda_spoof.py) forces
unsloth_zoo down its full, transformers-importing init path on a GPU-less runner;
without it unsloth_zoo degrades and never preloads transformers, masking the bug.
A one-line stub sidecar stands in for the 5.x venv, so no GPU, network, weights, or
real sidecar are needed. Passes on this fix, fails on buggy main.
* Studio: load the repo's canonical CUDA spoof in the correct-version guard
Load tests/_zoo_aggressive_cuda_spoof.py (the committed spoof the consolidated CI
already relies on) as the single source of truth so the guard matches CI and stays
robust on a CPU-only torch wheel, where a partial hand-rolled spoof could miss a
torch.cuda call and let the unsloth_zoo import raise (masking the bug). Falls back to
a minimal inline spoof for a standalone studio checkout. Verified: passes on this fix,
fails on buggy main, and the fallback path passes when the spoof file is absent.
* Studio: declare the lazily-resolved xet names so ruff F822 stays green
DownloadStallError, start_watchdog and get_hf_download_state are provided via the
module __getattr__ (PEP 562), so ruff F822 flagged them as undefined names in __all__
and the Source-lint / pre-commit checks went red. Add annotation-only declarations
(no value bound, so __getattr__ still resolves them lazily to the shared unsloth_zoo
backend) to mark them defined for the linter while keeping F822 active for the rest
of __all__.
* Studio: tighten comments on the sidecar-activation fix and its tests
* Studio: mirror the new MLX-dispatch preflight import in the import-discipline guard
The worker preflight now also runs 'from core.training.training import
is_apple_silicon_training_platform, should_use_mlx_training_backend' before it
activates the transformers sidecar. Add that import (guarded) to the guard's
preflight snippet so the invariant test stays a faithful mirror: a future change
that makes core.training.training pull transformers/unsloth_zoo eagerly would then
be caught too. Verified clean on the current tree (no leak).
---------
Co-authored-by: danielhanchen <unslothai@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
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
|
|
|
|
"""Studio shim over the shared ``unsloth_zoo.hf_xet_fallback`` Xet -> HTTP stall fallback.
|
|
|
|
Re-exports the shared API and injects Studio'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 Studio 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:
|
|
"""Studio'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(
|
|
"Studio 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 Studio's marker-aware HTTP-retry prep.
|
|
``force_download`` re-fetches a newer blob over a cached one (Studio'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 Studio'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)
|