mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-13 02:44:21 +00:00
* Studio: let diffusion load on Windows ROCm, and say what a gated repo needs Two things stopped an image load dead on a Windows ROCm host. The Windows xformers pin is CUDA-only, and against a ROCm torch, which ships no distributed backend, importing xformers dies inside torch.distributed. diffusers imports xformers on sight, so every model import went down with it and reported 'module torch.distributed has no attribute Work', naming neither xformers nor the cause. diffusers reaches torchao the same way through its quantizers. Both stubs already existed for torchao; the diffusion paths now install both before the first lazy diffusers import, in the loader and in the trainers' shared module (they run in a spawned child of their own). A gated base then surfaced its raw GatedRepoError in a toast: a request id, a resolve URL, and one useful sentence. It now says which repo is gated, links its page, and asks for a token only when none was sent. The full exception still goes to the log. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Install the Windows ROCm stubs in run.py, before the first heavy import The diffusion modules install both stubs at module scope, but that is already too late in the server: by the time a route imports one, the app has pulled torchao in through the route tree, and a stub only seeds names nothing has imported yet. The desktop app still died on torch._C._distributed_c10d. Move both installs to the entry point, ahead of every backend import, and guard the ordering with a test that walks run.py's module body. * Address review on #7981: stub-aware quant probe, chained gated errors, ambient token, CPU-thread ordering * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Harden the Windows ROCm stubs: torch-free probe, and reject the stub on the two remaining quant paths * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Name the model, not the API endpoint, when a metadata call reports a gated repo * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep the stub parity tests runnable without torch * Ask the Hub which token it would actually send, so a disabled implicit token reads as none * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Detect AMD's untagged Windows ROCm wheel, which carries no rocm in its version * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Mirror the stubbed-torchao fp8 refusal into the training preflight, before eviction * Tighten the comments this PR added * Share the ROCm predicate with attention selection for PR #7981 * Tighten the comments this PR added --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <moonshotaisubstack@gmail.com> Co-authored-by: danielhanchen <unslothshared@gmail.com>
173 lines
6.3 KiB
Python
173 lines
6.3 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
|
|
|
|
"""A gated model must say how to get in, not paste the 403 back at the user: the load error is
|
|
surfaced verbatim in a toast, where a raw GatedRepoError is a request id and a resolve URL
|
|
wrapped around one useful sentence.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
|
if str(_BACKEND_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(_BACKEND_ROOT))
|
|
|
|
from core.inference.diffusion import _hf_token_in_play, hub_access_message
|
|
|
|
_GATED = (
|
|
"403 Client Error. (Request ID: Root=1-6a73b83b) Cannot access gated repo for url "
|
|
"https://huggingface.co/black-forest-labs/FLUX.2-klein-9B/resolve/main/model_index.json. "
|
|
"Access to model black-forest-labs/FLUX.2-klein-9B is restricted and you are not in "
|
|
"the authorized list."
|
|
)
|
|
|
|
|
|
def _gated(text = _GATED):
|
|
"""A real GatedRepoError by type, without hub's constructor: HfHubHTTPError.__init__ requires a
|
|
response on hub 1.x but not on 0.x, and the pin spans both. The helper screens on type and
|
|
str() only, so a message-carrying subclass pins the contract on either version."""
|
|
from huggingface_hub.errors import GatedRepoError
|
|
|
|
class _Gated(GatedRepoError):
|
|
def __init__(self, message):
|
|
Exception.__init__(self, message)
|
|
|
|
return _Gated(text)
|
|
|
|
|
|
def test_no_token_asks_for_access_and_a_token():
|
|
message = hub_access_message(_gated(), had_token = False)
|
|
|
|
assert message is not None
|
|
assert "black-forest-labs/FLUX.2-klein-9B is gated" in message
|
|
assert "https://huggingface.co/black-forest-labs/FLUX.2-klein-9B" in message
|
|
assert "token" in message
|
|
# The resolve URL and request id are the noise this replaces.
|
|
assert "model_index.json" not in message
|
|
assert "Request ID" not in message
|
|
|
|
|
|
def test_a_token_that_still_bounces_names_the_account():
|
|
message = hub_access_message(_gated(), had_token = True)
|
|
|
|
assert message is not None
|
|
assert "not on its access list" in message
|
|
# Telling someone with a token to add a token sends them in a circle.
|
|
assert "add a Hugging Face token" not in message
|
|
|
|
|
|
def test_a_metadata_api_url_names_the_model_not_the_endpoint():
|
|
"""auth_check, and model_info on a gated private repo, raise with /api/models/<owner>/<repo>
|
|
(the shape hub's own GatedRepoError docstring shows); a plain two-segment match on that would
|
|
name "api/models" as the gated repo."""
|
|
message = hub_access_message(
|
|
_gated(
|
|
"403 Client Error. (Request ID: ViT1Bf7O) Cannot access gated repo for url "
|
|
"https://huggingface.co/api/models/ardent-figment/gated-model."
|
|
),
|
|
had_token = False,
|
|
)
|
|
|
|
assert message is not None
|
|
assert "ardent-figment/gated-model is gated" in message
|
|
assert "https://huggingface.co/ardent-figment/gated-model" in message
|
|
assert "api/models" not in message
|
|
|
|
|
|
def test_a_non_repo_api_url_falls_back_rather_than_inventing_a_repo():
|
|
message = hub_access_message(
|
|
_gated(
|
|
"403 Client Error. Cannot access gated repo for url https://huggingface.co/api/whoami-v2."
|
|
),
|
|
had_token = False,
|
|
)
|
|
|
|
assert message is not None
|
|
assert "its Hugging Face page" in message
|
|
assert "api/" not in message
|
|
|
|
|
|
def test_an_unparseable_repo_still_gives_the_instruction():
|
|
message = hub_access_message(
|
|
_gated("403 Client Error. Cannot access gated repo."), had_token = False
|
|
)
|
|
|
|
assert message is not None
|
|
assert "its Hugging Face page" in message
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"exc",
|
|
[
|
|
OSError("No space left on device"),
|
|
ValueError("Could not decode image"),
|
|
RuntimeError("CUDA out of memory"),
|
|
],
|
|
)
|
|
def test_other_failures_keep_their_own_text(exc):
|
|
# None is the signal to fall back to str(exc); rewriting these would bury the cause.
|
|
assert hub_access_message(exc, had_token = False) is None
|
|
|
|
|
|
def test_a_wrapped_gated_error_is_still_rewritten():
|
|
"""Transformers loads re-raise the 403 inside an OSError, so matching only the outermost exception misses the shape this exists for."""
|
|
try:
|
|
try:
|
|
raise _gated()
|
|
except Exception as inner:
|
|
raise OSError("We couldn't connect to huggingface.co to load this model.") from inner
|
|
except OSError as outer:
|
|
message = hub_access_message(outer, had_token = False)
|
|
|
|
assert message is not None
|
|
assert "black-forest-labs/FLUX.2-klein-9B is gated" in message
|
|
|
|
|
|
def test_a_self_referential_chain_terminates():
|
|
exc = ValueError("boom")
|
|
exc.__context__ = exc
|
|
|
|
assert hub_access_message(exc, had_token = False) is None
|
|
|
|
|
|
def test_an_ambient_token_counts_as_a_token(monkeypatch):
|
|
"""With token=None the Hub still uses HF_TOKEN or the cached login, so keying off Studio's
|
|
own token alone tells an already-authenticated user to add a token they have."""
|
|
import huggingface_hub.utils as hub_utils
|
|
|
|
monkeypatch.setattr(hub_utils, "get_token_to_send", lambda _t: "hf_ambient")
|
|
assert _hf_token_in_play(None) is True
|
|
|
|
monkeypatch.setattr(hub_utils, "get_token_to_send", lambda _t: None)
|
|
assert _hf_token_in_play(None) is False
|
|
assert _hf_token_in_play("hf_explicit") is True
|
|
|
|
|
|
def test_a_disabled_implicit_token_is_not_a_token(monkeypatch):
|
|
"""HF_HUB_DISABLE_IMPLICIT_TOKEN leaves get_token() answering with the cached login while
|
|
build_hf_headers sends no authorization header, so the refusal was anonymous."""
|
|
from huggingface_hub import constants
|
|
from huggingface_hub.utils import _headers
|
|
|
|
monkeypatch.setattr(constants, "HF_HUB_DISABLE_IMPLICIT_TOKEN", True)
|
|
monkeypatch.setattr(_headers, "get_token", lambda: "hf_cached_login", raising = False)
|
|
|
|
# Real get_token_to_send, so this pins hub's actual policy rather than a stand-in.
|
|
assert _hf_token_in_play(None) is False
|
|
assert _hf_token_in_play("hf_explicit") is True
|
|
|
|
monkeypatch.setattr(constants, "HF_HUB_DISABLE_IMPLICIT_TOKEN", False)
|
|
assert _hf_token_in_play(None) is True
|
|
|
|
|
|
def test_an_unreadable_ambient_token_is_not_a_token(monkeypatch):
|
|
import huggingface_hub.utils as hub_utils
|
|
|
|
def _raise(_t):
|
|
raise OSError("token file unreadable")
|
|
|
|
monkeypatch.setattr(hub_utils, "get_token_to_send", _raise)
|
|
assert _hf_token_in_play(None) is False
|