diff --git a/studio/backend/hub/schemas/downloads.py b/studio/backend/hub/schemas/downloads.py index dccd0c6733..0a66b33048 100644 --- a/studio/backend/hub/schemas/downloads.py +++ b/studio/backend/hub/schemas/downloads.py @@ -25,8 +25,8 @@ class DownloadModelRequest(BaseModel): description = "Quantization label (e.g. 'Q4_K_M'). Required for GGUF repos.", ) use_xet: bool = Field( - False, - description = "Enable Xet parallel chunked transport. Default False uses HTTP Range-resume.", + True, + description = "Use Xet parallel chunked transport. Default True; set False for HTTP Range-resume.", ) @@ -125,8 +125,8 @@ class DownloadDatasetRequest(BaseModel): repo_id: str = Field(..., description = "HuggingFace dataset repo ID") use_xet: bool = Field( - False, - description = "Enable Xet parallel chunked transport. Default False uses HTTP Range-resume.", + True, + description = "Use Xet parallel chunked transport. Default True; set False for HTTP Range-resume.", ) diff --git a/studio/backend/hub/services/datasets/downloads.py b/studio/backend/hub/services/datasets/downloads.py index ac90be8c6f..5efac562fa 100644 --- a/studio/backend/hub/services/datasets/downloads.py +++ b/studio/backend/hub/services/datasets/downloads.py @@ -157,7 +157,8 @@ async def download_dataset_response( repo_id = await asyncio.to_thread(resolve_cached_repo_id_case, repo_id, repo_type = "dataset") key = _download_job_key(repo_id) - transport = download_lifecycle.resolve_transport(body.use_xet) + use_xet = download_lifecycle.resolve_effective_use_xet(body.use_xet) + transport = download_lifecycle.resolve_transport(use_xet) claimed, claim_state = _registry.claim( key, @@ -183,7 +184,7 @@ async def download_dataset_response( spawn = lambda: download_lifecycle.spawn_worker( ["--repo-id", repo_id, "--dataset"], hf_token, - use_xet = body.use_xet, + use_xet = use_xet, ), hf_token = hf_token, label = repo_id, diff --git a/studio/backend/hub/services/download_lifecycle.py b/studio/backend/hub/services/download_lifecycle.py index 8256b00252..c2b99c0f18 100644 --- a/studio/backend/hub/services/download_lifecycle.py +++ b/studio/backend/hub/services/download_lifecycle.py @@ -3,6 +3,7 @@ from __future__ import annotations +import logging import os import signal import subprocess @@ -20,11 +21,27 @@ from hub.utils import inventory_scan as hf_cache_scan from hub.utils.hf_cache_state import EXIT_CANCELLED from hub.utils.state_dir import RepoType +logger = logging.getLogger(__name__) + def backend_dir() -> Path: return Path(__file__).resolve().parent.parent.parent +def resolve_effective_use_xet(use_xet: bool) -> bool: + """Downgrade an Xet request to HTTP when hf_xet is unavailable, so a defaulted + or explicit Xet request never hard-fails on installs without the Xet extra.""" + if not use_xet: + return False + reason = download_registry.download_transport_unavailable_reason( + download_registry.TRANSPORT_XET + ) + if reason is None: + return True + logger.warning("Xet transport unavailable, falling back to HTTP: %s", reason) + return False + + def resolve_transport(use_xet: bool) -> str: transport = download_registry.TRANSPORT_XET if use_xet else download_registry.TRANSPORT_HTTP unavailable_reason = download_registry.download_transport_unavailable_reason(transport) diff --git a/studio/backend/hub/services/models/downloads.py b/studio/backend/hub/services/models/downloads.py index db95b82c95..c2ffe7bffc 100644 --- a/studio/backend/hub/services/models/downloads.py +++ b/studio/backend/hub/services/models/downloads.py @@ -64,7 +64,7 @@ def _spawn_download_worker( repo_id: str, variant: Optional[str], hf_token: Optional[str], - use_xet: bool = False, + use_xet: bool = True, protected_blob_hashes: Optional[frozenset[str]] = None, ) -> subprocess.Popen: args = ["--repo-id", repo_id] @@ -96,7 +96,8 @@ async def download_model_response(body: DownloadModelRequest, hf_token: Optional detail = f"Invalid gguf_variant: {variant!r}", ) key = _download_job_key(repo_id, variant) - transport = download_lifecycle.resolve_transport(body.use_xet) + use_xet = download_lifecycle.resolve_effective_use_xet(body.use_xet) + transport = download_lifecycle.resolve_transport(use_xet) variant_blob_hashes = frozenset() variant_progress_blob_hashes = frozenset() completed_baseline_bytes = 0 @@ -171,7 +172,7 @@ async def download_model_response(body: DownloadModelRequest, hf_token: Optional repo_id, variant, hf_token, - use_xet = body.use_xet, + use_xet = use_xet, protected_blob_hashes = protected_blob_hashes, ), hf_token = hf_token, diff --git a/studio/backend/hub/tests/test_download_lifecycle.py b/studio/backend/hub/tests/test_download_lifecycle.py new file mode 100644 index 0000000000..a4baafa317 --- /dev/null +++ b/studio/backend/hub/tests/test_download_lifecycle.py @@ -0,0 +1,27 @@ +# 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 hub.services import download_lifecycle + + +def _set_xet_reason(monkeypatch, reason): + monkeypatch.setattr( + download_lifecycle.download_registry, + "download_transport_unavailable_reason", + lambda _transport: reason, + ) + + +def test_resolve_effective_use_xet_keeps_http_when_not_requested(monkeypatch): + _set_xet_reason(monkeypatch, "should not be consulted") + assert download_lifecycle.resolve_effective_use_xet(False) is False + + +def test_resolve_effective_use_xet_keeps_xet_when_available(monkeypatch): + _set_xet_reason(monkeypatch, None) + assert download_lifecycle.resolve_effective_use_xet(True) is True + + +def test_resolve_effective_use_xet_downgrades_when_xet_unavailable(monkeypatch): + _set_xet_reason(monkeypatch, "Xet transport is unavailable because hf_xet is not installed.") + assert download_lifecycle.resolve_effective_use_xet(True) is False diff --git a/studio/frontend/src/features/hub/download-manager/constants.ts b/studio/frontend/src/features/hub/download-manager/constants.ts index 3f772cb196..784173a4bf 100644 --- a/studio/frontend/src/features/hub/download-manager/constants.ts +++ b/studio/frontend/src/features/hub/download-manager/constants.ts @@ -8,7 +8,8 @@ export const TRANSPORT = { export const TRANSPORT_MODES = [TRANSPORT.HTTP, TRANSPORT.XET] as const; export type TransportMode = (typeof TRANSPORT_MODES)[number]; -export const DEFAULT_TRANSPORT_MODE: TransportMode = TRANSPORT.HTTP; +// Xet by default; effectiveTransportMode() downgrades to HTTP if hf_xet is missing. +export const DEFAULT_TRANSPORT_MODE: TransportMode = TRANSPORT.XET; export function isTransportMode(value: unknown): value is TransportMode { return (