* Studio: self-heal unsloth namespace-package shadows in all subprocess workers A directory named `unsloth` (or `unsloth_zoo`) without an __init__.py on PYTHONPATH/sys.path, a stray source checkout or a polluted PYTHONPATH, makes `import unsloth` resolve to an empty namespace package, so a worker's `from unsloth import FastLanguageModel` dies with a cryptic "cannot import name ... (unknown location)". The LLM training path already recovered from this via `_ensure_real_packages` in trainer.py (PR #6269), but the inference, export, and embedding-training subprocesses imported Unsloth directly with no guard. Extract that helper into a shared, dependency-free core/import_guards.py and call it before the Unsloth import in every subprocess: it drops the offending sys.path entries, imports the real packages (unsloth before unsloth_zoo so the pre-zoo GPU fixes run), then restores sys.path. trainer.py now imports the shared helper instead of its local copy. Covers both unsloth and unsloth_zoo and both namespace origin forms (None and "namespace"). The existing PR #6269 test now exercises the shared helper. * Studio: distinguish a failed model load from no model in the attach gates A failed load never sets the checkpoint, so the image and audio attach gates fell through to "Load a model before adding images/audio", which reads as if the user simply forgot to pick a model rather than that the load errored. Add a dedicated lastModelLoadError to the chat runtime store, set only when an actual load attempt fails (not on refresh, list, status, or unload errors, which keep using modelsError) and cleared when the next load starts. The image gate (all three call sites) and the audio gate now use it to report a failed load and point at the server logs, while still blocking in exactly the same cases. * Tighten namespace-shadow guard and load-error comments
53 lines
2 KiB
Python
53 lines
2 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
|
|
|
|
"""Recover `unsloth`/`unsloth_zoo` from a namespace-package shadow. Stdlib-only."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
|
|
def ensure_real_packages(*names: str) -> None:
|
|
"""Drop sys.path entries where a bare `<name>/` dir (no __init__.py) shadows
|
|
the installed package as a namespace, import the real packages, restore
|
|
sys.path. No-op without a shadow. Pass dependency-first (e.g. "unsloth_zoo",
|
|
"unsloth"); imports run dependency-last."""
|
|
import importlib
|
|
import importlib.util
|
|
|
|
bad: set = set()
|
|
shadowed: list = []
|
|
for name in names:
|
|
try:
|
|
spec = importlib.util.find_spec(name)
|
|
except (ImportError, ValueError, AttributeError):
|
|
spec = None
|
|
# real package -> spec.origin is its __init__; namespace shadow -> None/"namespace"
|
|
if spec is None or spec.origin not in (None, "namespace"):
|
|
continue
|
|
dirs = {os.path.realpath(d) for d in (spec.submodule_search_locations or [])}
|
|
if not dirs:
|
|
continue
|
|
shadowed.append(name)
|
|
for entry in sys.path:
|
|
pkg = os.path.join(entry or os.getcwd(), name)
|
|
if os.path.realpath(pkg) in dirs and not os.path.isfile(
|
|
os.path.join(pkg, "__init__.py")
|
|
):
|
|
bad.add(entry)
|
|
if not bad:
|
|
return
|
|
saved = list(sys.path)
|
|
sys.path[:] = [e for e in sys.path if e not in bad]
|
|
for name in shadowed:
|
|
for cached in [m for m in list(sys.modules) if m == name or m.startswith(name + ".")]:
|
|
del sys.modules[cached]
|
|
try:
|
|
importlib.invalidate_caches()
|
|
# import unsloth before unsloth_zoo: unsloth.__init__ runs GPU/bnb fixes zoo relies on
|
|
for name in reversed(names):
|
|
importlib.import_module(name)
|
|
finally:
|
|
sys.path[:] = saved
|