e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
232 lines
7.6 KiB
Python
232 lines
7.6 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
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import json
|
|
import logging
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
from typing import Callable
|
|
|
|
from utils.native_path_leases import child_env_without_native_path_secret
|
|
from utils.subprocess_compat import windows_hidden_subprocess_kwargs
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
FLASH_ATTN_RELEASE_BASE_URL = "https://github.com/Dao-AILab/flash-attention/releases/download"
|
|
|
|
|
|
@functools.lru_cache(maxsize = 1)
|
|
def has_blackwell_gpu() -> bool:
|
|
"""Return True if any visible NVIDIA GPU has compute capability >= 10.0 (Blackwell).
|
|
|
|
Cached for the process lifetime; tests mocking nvidia-smi must call
|
|
``has_blackwell_gpu.cache_clear()`` first.
|
|
"""
|
|
# Detection disabled for now: Dao-AILab ships Blackwell (sm_100+) flash-attn
|
|
# wheels and url_exists() already gates resolution, so we no longer skip
|
|
# flash-attn on Blackwell. The nvidia-smi probe below is kept for possible
|
|
# future arch-based gating; drop this early return to re-enable it.
|
|
return False
|
|
exe = shutil.which("nvidia-smi")
|
|
if not exe:
|
|
return False
|
|
try:
|
|
result = subprocess.run(
|
|
[exe, "--query-gpu=compute_cap", "--format=csv,noheader"],
|
|
stdout = subprocess.PIPE,
|
|
stderr = subprocess.DEVNULL,
|
|
text = True,
|
|
timeout = 10,
|
|
env = child_env_without_native_path_secret(),
|
|
)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
return False
|
|
if result.returncode != 0:
|
|
return False
|
|
for line in result.stdout.splitlines():
|
|
cap = line.strip()
|
|
if not cap:
|
|
continue
|
|
major_part = cap.split(".", 1)[0]
|
|
try:
|
|
major = int(major_part)
|
|
except ValueError:
|
|
continue
|
|
if major >= 10:
|
|
return True
|
|
return False
|
|
|
|
|
|
def linux_wheel_platform_tag() -> str | None:
|
|
machine = platform.machine().lower()
|
|
if sys.platform.startswith("linux"):
|
|
if machine in {"x86_64", "amd64"}:
|
|
return "linux_x86_64"
|
|
if machine in {"aarch64", "arm64"}:
|
|
return "linux_aarch64"
|
|
# No prebuilt wheels published for macOS or Windows
|
|
return None
|
|
|
|
|
|
def probe_torch_wheel_env(*, timeout: int | None = None) -> dict[str, str] | None:
|
|
platform_tag = linux_wheel_platform_tag()
|
|
if platform_tag is None:
|
|
return None
|
|
|
|
try:
|
|
probe = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
(
|
|
"import json, sys, re, torch; "
|
|
"parts = torch.__version__.split('+', 1)[0].split('.')[:2]; "
|
|
"minor = re.sub(r'[^0-9].*', '', parts[1]) if len(parts) > 1 else '0'; "
|
|
"torch_mm = parts[0] + '.' + minor; "
|
|
"print(json.dumps({"
|
|
"'python_tag': f'cp{sys.version_info.major}{sys.version_info.minor}', "
|
|
"'torch_mm': torch_mm, "
|
|
"'cuda_major': str(int(str(torch.version.cuda).split('.', 1)[0])) if torch.version.cuda else '', "
|
|
"'hip_version': str(torch.version.hip) if getattr(torch.version, 'hip', None) else '', "
|
|
"'cxx11abi': str(torch._C._GLIBCXX_USE_CXX11_ABI).upper()"
|
|
"}))"
|
|
),
|
|
],
|
|
stdout = subprocess.PIPE,
|
|
stderr = subprocess.PIPE,
|
|
text = True,
|
|
timeout = timeout,
|
|
env = child_env_without_native_path_secret(),
|
|
**windows_hidden_subprocess_kwargs(),
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return None
|
|
|
|
if probe.returncode != 0:
|
|
return None
|
|
|
|
try:
|
|
env = json.loads(probe.stdout.strip())
|
|
except json.JSONDecodeError:
|
|
return None
|
|
env["platform_tag"] = platform_tag
|
|
return env
|
|
|
|
|
|
# torch 2.11 has no native prebuilt wheels for flash-attn / causal-conv1d / mamba
|
|
# yet, but their torch 2.10 CUDA wheels load and pass the projects' own test suites
|
|
# on torch 2.11 (verified on B200: FA2 fwd/bwd, causal-conv1d, and mamba selective
|
|
# scan all match reference). Reuse the torch 2.10 wheels on torch 2.11 so a 2.11
|
|
# install still gets these prebuilt accelerators instead of building from source.
|
|
_PREBUILT_WHEEL_TORCH_MM = {"2.11": "2.10"}
|
|
|
|
|
|
def prebuilt_wheel_torch_mm(torch_mm: str) -> str:
|
|
"""Map a torch major.minor to the one whose prebuilt accelerator wheels to use."""
|
|
return _PREBUILT_WHEEL_TORCH_MM.get(torch_mm, torch_mm)
|
|
|
|
|
|
def direct_wheel_url(
|
|
*,
|
|
filename_prefix: str,
|
|
package_version: str,
|
|
release_tag: str,
|
|
release_base_url: str,
|
|
env: dict[str, str] | None,
|
|
) -> str | None:
|
|
if env is None or not env.get("cuda_major"):
|
|
return None
|
|
|
|
filename = (
|
|
f"{filename_prefix}-{package_version}"
|
|
f"+cu{env['cuda_major']}torch{prebuilt_wheel_torch_mm(env['torch_mm'])}"
|
|
f"cxx11abi{env['cxx11abi']}-{env['python_tag']}-{env['python_tag']}"
|
|
f"-{env['platform_tag']}.whl"
|
|
)
|
|
return f"{release_base_url}/{release_tag}/{filename}"
|
|
|
|
|
|
def flash_attn_package_version(torch_mm: str) -> str | None:
|
|
if torch_mm == "2.10":
|
|
return "2.8.1"
|
|
try:
|
|
major, minor = (int(part) for part in torch_mm.split(".", 1))
|
|
except ValueError:
|
|
return None
|
|
if major == 2 and 4 <= minor <= 9:
|
|
return "2.8.3"
|
|
return None
|
|
|
|
|
|
def flash_attn_wheel_url(env: dict[str, str] | None) -> str | None:
|
|
if env is None:
|
|
return None
|
|
package_version = flash_attn_package_version(prebuilt_wheel_torch_mm(env["torch_mm"]))
|
|
if package_version is None:
|
|
return None
|
|
return direct_wheel_url(
|
|
filename_prefix = "flash_attn",
|
|
package_version = package_version,
|
|
release_tag = f"v{package_version}",
|
|
release_base_url = FLASH_ATTN_RELEASE_BASE_URL,
|
|
env = env,
|
|
)
|
|
|
|
|
|
def install_wheel(
|
|
wheel_url: str,
|
|
*,
|
|
python_executable: str,
|
|
use_uv: bool,
|
|
uv_needs_system: bool = False,
|
|
run: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
|
|
) -> list[tuple[str, subprocess.CompletedProcess[str]]]:
|
|
attempts: list[tuple[str, subprocess.CompletedProcess[str]]] = []
|
|
|
|
# Try uv first if available, then fall back to pip
|
|
if use_uv and shutil.which("uv"):
|
|
uv_cmd = ["uv", "pip", "install"]
|
|
if uv_needs_system:
|
|
uv_cmd.append("--system")
|
|
uv_cmd.extend(["--python", python_executable, "--no-deps", wheel_url])
|
|
result = run(
|
|
uv_cmd,
|
|
stdout = subprocess.PIPE,
|
|
stderr = subprocess.STDOUT,
|
|
text = True,
|
|
env = child_env_without_native_path_secret(),
|
|
)
|
|
attempts.append(("uv", result))
|
|
if result.returncode == 0:
|
|
return attempts
|
|
|
|
pip_cmd = [python_executable, "-m", "pip", "install", "--no-deps", wheel_url]
|
|
result = run(
|
|
pip_cmd,
|
|
stdout = subprocess.PIPE,
|
|
stderr = subprocess.STDOUT,
|
|
text = True,
|
|
env = child_env_without_native_path_secret(),
|
|
)
|
|
attempts.append(("pip", result))
|
|
return attempts
|
|
|
|
|
|
def url_exists(url: str) -> bool:
|
|
try:
|
|
request = urllib.request.Request(url, method = "HEAD")
|
|
with urllib.request.urlopen(request, timeout = 10):
|
|
return True
|
|
except urllib.error.HTTPError as exc:
|
|
_logger.debug("url_exists(%s): HTTP %s", url, exc.code)
|
|
except (urllib.error.URLError, TimeoutError) as exc:
|
|
_logger.debug("url_exists(%s): %s", url, exc)
|
|
return False
|