Files
2026-09-02 23:13:48 +07:00

339 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Installation script for STARVSRWrapper.
Resolves an xformers build that matches the PyTorch already installed in this
environment and installs it with uv (if available) or pip.
Resolution order:
1. Ask the official PyTorch wheel index for the detected CUDA tag
(https://download.pytorch.org/whl/cuXXX/xformers/) and pick the newest
release whose declared `torch` pin is satisfied by the installed PyTorch.
2. Fall back to a static torch -> xformers table if the index is unreachable.
PyTorch is never installed, upgraded or replaced: the install runs with
--no-deps so a mismatched xformers can't drag a different torch into a working
ComfyUI environment. Failures are reported and the script still exits 0 so it
can never block ComfyUI startup.
"""
import json
import platform
import re
import subprocess
import sys
import urllib.error
import urllib.request
PYTORCH_INDEX = "https://download.pytorch.org/whl/{tag}"
PYPI_RELEASE = "https://pypi.org/pypi/xformers/{version}/json"
NETWORK_TIMEOUT = 20
# Fallback table, newest first. Used only when the PyTorch index is unreachable.
# Each entry is the newest xformers release that pins that exact torch version.
FALLBACK_MAP = {
"2.10.0": "0.0.34",
"2.9.1": "0.0.33.post2",
"2.9.0": "0.0.33.post1",
"2.8.0": "0.0.32.post2",
"2.7.1": "0.0.31.post1",
"2.7.0": "0.0.30",
"2.6.0": "0.0.29.post2",
}
# xformers releases that declare a floor (torch>=X) rather than an exact pin.
FALLBACK_FLOOR = [("2.10", "0.0.35")]
def run_command(cmd, check=True, capture_output=True):
"""Run a command and return the result."""
result = subprocess.run(
cmd, shell=True, capture_output=capture_output, text=True, check=False
)
if check and result.returncode != 0:
raise RuntimeError(f"Command failed: {cmd}\n{result.stderr}")
return result
def check_uv_available():
"""Check if uv is available in the system."""
try:
result = run_command("uv --version", check=False)
return result.returncode == 0
except Exception:
return False
def get_pytorch_version():
"""Get the installed PyTorch version, e.g. '2.10.0+cu130'."""
try:
import torch
return torch.__version__
except ImportError:
return None
def base_version(version_string):
"""'2.10.0+cu130' -> '2.10.0'. Returns None if unparseable."""
if not version_string:
return None
match = re.match(r"(\d+)\.(\d+)\.(\d+)", version_string)
return match.group(0) if match else None
def version_key(version_string):
"""Sortable key for a release like '0.0.33.post2' or '2.10'."""
numbers = [int(n) for n in re.findall(r"\d+", version_string)]
# A plain release outranks nothing, but ".postN" outranks the bare release.
post = 1 if ".post" in version_string else 0
return (numbers[:3] + [0, 0, 0])[:3] + [post] + numbers[3:]
def get_pytorch_cuda_tag():
"""Extract the wheel CUDA tag from PyTorch, e.g. 'cu130'. None if CPU/ROCm."""
try:
import torch
except ImportError:
return None
local = re.search(r"\+(cu\d+)", torch.__version__)
if local:
return local.group(1)
cuda = getattr(torch.version, "cuda", None)
if cuda:
major, _, minor = cuda.partition(".")
return f"cu{major}{minor or '0'}"
return None
def wheel_tags_match(filename):
"""Is this wheel installable on the running interpreter and platform?"""
parts = filename[: -len(".whl")].split("-")
if len(parts) < 5:
return False
python_tag, abi_tag, platform_tag = parts[-3], parts[-2], parts[-1]
if sys.platform == "win32":
want_platform = "win_amd64"
elif sys.platform == "darwin":
want_platform = "macosx"
else:
want_platform = "manylinux"
if want_platform not in platform_tag:
return False
if (
platform.machine().lower() not in ("amd64", "x86_64")
and want_platform != "macosx"
):
return False
# abi3 / py3 wheels work on any newer CPython; cpXY wheels need an exact match.
exact = f"cp{sys.version_info.major}{sys.version_info.minor}"
if abi_tag in ("abi3", "none"):
floor = re.match(r"(?:cp|py)(\d)(\d+)", python_tag)
if not floor:
return python_tag in (exact, "py3")
return (sys.version_info.major, sys.version_info.minor) >= (
int(floor.group(1)),
int(floor.group(2)),
)
return python_tag == exact
def fetch_index_versions(cuda_tag):
"""Versions on the PyTorch index for this CUDA tag with an installable wheel."""
url = PYTORCH_INDEX.format(tag=cuda_tag) + "/xformers/"
try:
with urllib.request.urlopen(url, timeout=NETWORK_TIMEOUT) as response:
html = response.read().decode("utf-8", "ignore")
except (urllib.error.URLError, OSError, TimeoutError) as exc:
print(f" Could not read {url}: {exc}")
return []
versions = set()
for filename in re.findall(r"xformers-[^\"<>#]+?\.whl", html):
filename = filename.split("/")[-1]
if "%2B" in filename or "+" in filename:
continue # nightly / local-version build
if wheel_tags_match(filename):
versions.add(filename.split("-")[1])
return sorted(versions, key=version_key, reverse=True)
def fetch_torch_requirement(xformers_version):
"""The `torch` specifier that this xformers release declares, or None."""
try:
url = PYPI_RELEASE.format(version=xformers_version)
with urllib.request.urlopen(url, timeout=NETWORK_TIMEOUT) as response:
metadata = json.load(response)
except (urllib.error.URLError, OSError, TimeoutError, ValueError):
return None
for requirement in metadata.get("info", {}).get("requires_dist") or []:
match = re.match(r"\s*torch\s*(==|>=)\s*([\d.]+)", requirement)
if match:
return match.group(1), match.group(2).rstrip(".")
return None
def requirement_satisfied(requirement, torch_version):
"""Does the installed torch satisfy ('==' | '>=', version)?"""
operator, wanted = requirement
installed = version_key(torch_version)
target = version_key(wanted)
if operator == "==":
# A pin of '2.10.0' matches 2.10.0 only; a pin of '2.10' matches 2.10.x.
depth = len(re.findall(r"\d+", wanted))
return installed[:depth] == target[:depth]
return installed >= target
def resolve_xformers(torch_version, cuda_tag):
"""Pick (version, index_url) for the installed torch, or (None, None)."""
torch_base = base_version(torch_version)
if not torch_base:
print(f"Could not parse PyTorch version: {torch_version}")
return None, None
if cuda_tag:
index_url = PYTORCH_INDEX.format(tag=cuda_tag)
candidates = fetch_index_versions(cuda_tag)
if candidates:
print(
f" {len(candidates)} candidate(s) on the {cuda_tag} index: {', '.join(candidates)}"
)
for candidate in candidates:
requirement = fetch_torch_requirement(candidate)
if requirement is None:
continue
if requirement_satisfied(requirement, torch_base):
print(
f" xformers {candidate} declares torch{requirement[0]}{requirement[1]}"
f" -> matches {torch_base}"
)
return candidate, index_url
print(
f" No {cuda_tag} build declares compatibility with torch {torch_base}."
)
# Index unreachable or nothing compatible: try the static table below.
else:
index_url = None
print(" PyTorch has no CUDA build tag (CPU or ROCm); using PyPI.")
static = FALLBACK_MAP.get(torch_base)
if not static:
for floor, version in FALLBACK_FLOOR:
if version_key(torch_base) >= version_key(floor):
static = version
break
if static:
print(f" Falling back to the static table: xformers {static}")
return static, index_url
print(f" No known xformers release for PyTorch {torch_base}.")
return None, None
def get_installed_xformers():
"""(version, torch_requirement) for the installed xformers, or None."""
try:
import importlib.metadata as metadata
version = metadata.version("xformers")
except Exception:
return None
requirement = None
try:
for entry in metadata.requires("xformers") or []:
match = re.match(r"\s*torch\s*(==|>=)\s*([\d.]+)", entry)
if match:
requirement = (match.group(1), match.group(2).rstrip("."))
break
except Exception:
pass
return version, requirement
def install_package(package_manager, package_spec, index_url=None):
"""Install one package without letting it touch the existing torch."""
if package_manager == "uv":
cmd = f'uv pip install --no-deps "{package_spec}"'
else:
cmd = f'"{sys.executable}" -m pip install --no-deps "{package_spec}"'
if index_url:
cmd += f" --index-url {index_url}"
print(f"Running: {cmd}")
result = run_command(cmd, check=False, capture_output=False)
return result.returncode == 0
def main():
"""Main installation routine."""
print("=" * 60)
print("STARVSRWrapper Installation Script")
print("=" * 60)
print("\n[1/3] Checking package manager...")
package_manager = "uv" if check_uv_available() else "pip"
print(f"Using package manager: {package_manager}")
print("\n[2/3] Inspecting the environment...")
torch_version = get_pytorch_version()
if not torch_version:
print("PyTorch not found. Install PyTorch first, then re-run this script.")
return 0
cuda_tag = get_pytorch_cuda_tag()
torch_base = base_version(torch_version)
print(f"PyTorch version: {torch_version}")
print(f"CUDA wheel tag: {cuda_tag or 'none (CPU/ROCm build)'}")
print(f"Python: {sys.version.split()[0]} on {sys.platform}")
installed = get_installed_xformers()
if installed:
version, requirement = installed
if (
requirement
and torch_base
and not requirement_satisfied(requirement, torch_base)
):
print(
f"xformers {version} is installed but was built for "
f"torch{requirement[0]}{requirement[1]}, not {torch_base}. Reinstalling."
)
else:
print(
f"xformers {version} is installed and matches this PyTorch. Nothing to do."
)
return 0
print("\n[3/3] Resolving a matching xformers build...")
xformers_version, index_url = resolve_xformers(torch_version, cuda_tag)
if not xformers_version:
print("\n[!] Could not determine a compatible xformers version.")
print(" Install it manually, e.g.:")
hint = f" --index-url {PYTORCH_INDEX.format(tag=cuda_tag)}" if cuda_tag else ""
print(f' "{sys.executable}" -m pip install --no-deps xformers{hint}')
print(
" Version matrix: https://github.com/facebookresearch/xformers#installing-xformers"
)
return 0
if not install_package(package_manager, f"xformers=={xformers_version}", index_url):
print("\n[!] xformers installation failed.")
print(" Retry manually:")
hint = f" --index-url {index_url}" if index_url else ""
print(
f' "{sys.executable}" -m pip install --no-deps '
f'"xformers=={xformers_version}"{hint}'
)
return 0
print(f"\nInstalled xformers {xformers_version}.")
return 0
if __name__ == "__main__":
sys.exit(main())