diff --git a/CHANGELOG.md b/CHANGELOG.md index e5865772c..1f10dbc17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ Plus continued work on modernization of codebase: UI is now fully TypeScript bas see *settings -> huggingface -> download method* for options - **Nunchaku** consider *DEV* builds when auto-installing - **Gallery** add clear cache button to folder menu + - **UV** much updated `--uv` support for fast installs + now also supports global `uv` if present in the system - **Changes** - all **Guidance** params are now set to *-1* by default to allow using model defaults and avoid confusion with different model behaviour log will print default values used by model if not set by user diff --git a/installer.py b/installer.py index 751d3f870..842e132c8 100644 --- a/installer.py +++ b/installer.py @@ -208,6 +208,37 @@ def uninstall(package, quiet = False): return txt +def uv_info(): + uv_version = None + uv_cache_dir = None + uv_cache_active = False + uv_local = os.path.join(sys.prefix, "bin", "uv") # Prefer uv inside the venv + if os.path.exists(uv_local): + uv_version = subprocess.check_output([uv_local, "--version"], text=True).strip() + uv_cache_dir = subprocess.check_output([uv_local, "cache", "dir"], text=True).strip() + uv_global = shutil.which("uv") # Fallback: system uv + if uv_global: + uv_version = subprocess.check_output([uv_global, "--version"], text=True).strip() + uv_cache_dir = subprocess.check_output([uv_global, "cache", "dir"], text=True).strip() + uv_cache_disabled = os.environ.get("UV_NO_CACHE") == "1" + site = next(p for p in sys.path if p.endswith("site-packages")) + if uv_cache_dir and not uv_cache_disabled: + for root, _dirs, files in os.walk(site): + for f in files: + full = os.path.join(root, f) + try: + st = os.stat(full) + except FileNotFoundError: + continue + if st.st_nlink > 1: # Hardlink count > 1 means deduped + uv_cache_active = True + cache_path = os.path.join(uv_cache_dir, f) # Or check if inode matches something in cache + if os.path.exists(cache_path): + if os.stat(cache_path).st_ino == st.st_ino: + uv_cache_active = True + log.debug(f'Package manager: app=uv version="{uv_version}" folder="{uv_cache_dir}" dedup={uv_cache_active}') + + def run(cmd: str, *nargs: str, **kwargs): options = { "check": False, @@ -253,7 +284,8 @@ def pip(arg: str, ignore: bool = False, quiet: bool = True, *, uv = True, constr log.warning('Offline mode enabled') return None, 'offline' package = arg.replace("install", "").replace("--upgrade", "").replace("--no-deps", "").replace("--force-reinstall", "").strip() - uv = uv and args.uv and not package.startswith('git+') + # uv = uv and args.uv and not package.startswith('git+') + uv = uv and args.uv pipCmd = "uv pip" if uv else "pip" if not quiet and '-r ' not in arg: log.info(f'Install: package="{package}" mode={"uv" if uv else "pip"}') @@ -264,20 +296,26 @@ def pip(arg: str, ignore: bool = False, quiet: bool = True, *, uv = True, constr all_args.append(arg) if env_args: all_args.append(env_args) - if constraints and "-c " not in env_args: + if constraints and "-c " not in env_args and arg.startswith("install"): all_args.append("-c constraints.txt") if not quiet: log.debug(f'Running: {pipCmd}="{" ".join(all_args)}"') - result, output = run(sys.executable, "-m", pipCmd, *all_args) + if uv: + result, output = run("uv", "pip", *all_args) + else: + result, output = run(sys.executable, "-m", pipCmd, *all_args) if len(result.stderr) > 0: if uv and result.returncode != 0: + print('HERE1', result) + print('HERE2', output) log.warning(f'Install: cmd="{pipCmd}" args="{" ".join(all_args)}" cannot use uv, fallback to pip') debug(f'Install: uv pip error: {result.stderr}') cleanup_broken_packages() return pip(originalArg, ignore, quiet, uv=False) - debug(f'Install {pipCmd}: {output}') + if os.environ.get('SD_INSTALL_DEBUG', None) is not None: + log.debug(f'PIP cmd="{pipCmd}": {output}') if result.returncode != 0 and not ignore: errors.append(f'pip: {package}') log.error(f'Install: {pipCmd}: {arg}') @@ -294,7 +332,7 @@ def install(package, friendly: str | None = None, ignore: bool = False, reinstal if args.reinstall or args.upgrade: global quick_allowed # pylint: disable=global-statement quick_allowed = False - if (args.reinstall) or (reinstall) or (not installed(package, friendly, quiet=quiet)): + if args.reinstall or reinstall or not installed(package, friendly, quiet=quiet): deps = '' if not no_deps else '--no-deps ' isolation = '' if not no_build_isolation else '--no-build-isolation ' cmd = f"install{' --upgrade' if not args.uv else ''}{' --force-reinstall' if force else ''} {deps}{isolation}{package}" @@ -432,6 +470,8 @@ def get_platform(): 'locale': locale.getlocale(), 'setuptools': package_version('setuptools'), 'docker': os.environ.get('SD_DOCKER', None) is not None, + 'pip': package_version('pip'), + 'uv': package_version('uv'), # 'host': platform.node(), # 'version': platform.version(), } @@ -539,9 +579,9 @@ def check_transformers(): log.info(f'Install: package="transformers" version={target_transformers}') else: log.info(f'Update: package="transformers" current={pkg_transformers.version} target={target_transformers}') - pip('uninstall --yes transformers', ignore=True, quiet=True, uv=False) - pip(f'install --upgrade tokenizers=={target_tokenizers}', ignore=False, quiet=True, uv=False) - pip(f'install --upgrade transformers=={target_transformers}', ignore=False, quiet=True, uv=False) + pip('uninstall --yes transformers', ignore=True, quiet=True) + pip(f'install --upgrade tokenizers=={target_tokenizers}', ignore=False, quiet=True) + pip(f'install --upgrade transformers=={target_transformers}', ignore=False, quiet=True) else: # Git commit-pinned version current = opts.get('transformers_version', '') @@ -550,9 +590,9 @@ def check_transformers(): log.info(f'Install: package="transformers" commit={target_commit}') else: log.info(f'Update: package="transformers" current={pkg_transformers.version} hash={current} target={target_commit}') - pip('uninstall --yes transformers', ignore=True, quiet=True, uv=False) - pip(f'install --upgrade tokenizers=={target_tokenizers}', ignore=False, quiet=True, uv=False) - pip(f'install --upgrade git+https://github.com/huggingface/transformers@{target_commit}', ignore=False, quiet=True, uv=False) + pip('uninstall --yes transformers', ignore=True, quiet=True) + pip(f'install --upgrade tokenizers=={target_tokenizers}', ignore=False, quiet=True) + pip(f'install --upgrade git+https://github.com/huggingface/transformers@{target_commit}', ignore=False, quiet=True) global transformers_commit # pylint: disable=global-statement transformers_commit = target_commit ts('transformers', t_start) @@ -1490,6 +1530,8 @@ def check_venv(): import site pkg_path = [try_relpath(p) for p in site.getsitepackages() if os.path.exists(p)] log.debug(f'Packages: prefix={try_relpath(sys.prefix)} site={pkg_path}') + if args.uv: + uv_info() for p in pkg_path: invalid = [] for f in os.listdir(p): @@ -1791,12 +1833,18 @@ def read_options(): def ensure_base_requirements(): t_start = time.time() setuptools_version = '69.5.1' + if (args.uv or '--uv' in sys.argv) and (shutil.which('uv') is not None): # early enable uv + args.uv = True # pylint: disable=attribute-defined-outside-init + uv_info() def update_setuptools(): local_log = logging.getLogger('sdnext.installer') global setuptools, distutils # pylint: disable=global-statement # python may ship with incompatible setuptools - subprocess.run(f'"{sys.executable}" -m pip install setuptools=={setuptools_version}', shell=True, check=False, env=os.environ, capture_output=True) + if args.uv: + subprocess.run(f'uv pip install setuptools=={setuptools_version} wheel', shell=True, check=False, env=os.environ, capture_output=True) + else: + subprocess.run(f'"{sys.executable}" -m pip install setuptools=={setuptools_version} wheel', shell=True, check=False, env=os.environ, capture_output=True) # need to delete all references to modules to be able to reload them otherwise python will use cached version modules = [m for m in sys.modules if m.startswith('setuptools') or m.startswith('distutils')] for m in modules: @@ -1825,6 +1873,7 @@ def ensure_base_requirements(): try: global setuptools # pylint: disable=global-statement + import wheel # pylint: disable=unused-import import setuptools # pylint: disable=redefined-outer-name if setuptools.__version__ != setuptools_version: update_setuptools() @@ -1832,13 +1881,13 @@ def ensure_base_requirements(): update_setuptools() # used by installler itself so must be installed before requirements - install('rich==14.1.0', 'rich', quiet=True) + install('rich==15.0.0', 'rich', quiet=True) install('psutil', 'psutil', quiet=True) install('requests==2.32.3', 'requests', quiet=True) ts('base', t_start) -# startup +# startup ensure_base_requirements() from modules.logger import setup_logging # must be loaded after ensure_base_requirements from modules.logger import log as log_instance diff --git a/launch.py b/launch.py index 20b982331..128135b95 100755 --- a/launch.py +++ b/launch.py @@ -5,6 +5,7 @@ import os import sys import time import shlex +import shutil import subprocess import installer @@ -255,7 +256,7 @@ def main(): log.info(f'Args: {sys.argv[1:]}') if not args.skip_env and not args.skip_all: installer.set_environment() - if args.uv: + if args.uv and shutil.which('uv') is None: installer.install('uv', 'uv') installer.install_gradio() installer.check_torch() diff --git a/modules/mit_nunchaku.py b/modules/mit_nunchaku.py index c15ccbd62..c9a26c31b 100644 --- a/modules/mit_nunchaku.py +++ b/modules/mit_nunchaku.py @@ -53,7 +53,7 @@ def install_nunchaku(force=False): if url is not None: cmd = f'install --upgrade {url}' log.debug(f'Nunchaku: install="{url}"') - result, _output = pip(cmd, uv=False, ignore=not force, quiet=not force) + result, _output = pip(cmd, ignore=not force, quiet=not force) return result.returncode == 0 else: import torch @@ -75,7 +75,7 @@ def install_nunchaku(force=False): fn = f'nunchaku-{v}+{cuda_ver}torch{torch_ver}-cp{python_ver}-cp{python_ver}-{suffix}.whl' else: fn = f'nunchaku-{v}+{cuda_ver}torch{torch_ver}-cp{python_ver}-cp{python_ver}-{suffix}.whl' - result, _output = pip(f'install --upgrade {url+fn}', uv=False, ignore=True, quiet=True) + result, _output = pip(f'install --upgrade {url+fn}', ignore=True, quiet=True) if (result is None) or (_output == 'offline'): log.error(f'Nunchaku: install url="{url+fn}" offline mode') return False @@ -85,7 +85,7 @@ def install_nunchaku(force=False): log.info(f'Nunchaku: install url="{url}"') return True fn = f'nunchaku-{v}+torch{torch_ver}-cp{python_ver}-cp{python_ver}-{suffix}.whl' - result, _output = pip(f'install --upgrade {url+fn}', uv=False, ignore=True, quiet=True) + result, _output = pip(f'install --upgrade {url+fn}', ignore=True, quiet=True) if (result is None) or (_output == 'offline'): log.error(f'Nunchaku: install url="{url+fn}" offline mode') return False diff --git a/requirements.txt b/requirements.txt index a858f3fd8..c64603dc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ ftfy # versioned fastapi==0.124.4 -rich==14.1.0 +rich==15.0.0 safetensors==0.8.0rc0 peft==0.19.1 httpx==0.28.1 @@ -30,12 +30,12 @@ requests==2.32.3 tqdm==4.67.3 accelerate==1.13.0 einops==0.8.1 -huggingface_hub==1.15.0 +huggingface_hub==1.16.4 hf_xet==1.5.0 numpy==2.1.2 pandas==2.3.1 -protobuf==6.33.5 -pytorch_lightning==2.6.1 +protobuf==7.35.0 +pytorch_lightning==2.6.5 urllib3==1.26.19 Pillow==12.2.0 timm==1.0.24 diff --git a/webui.bat b/webui.bat index 68fd17c86..382d7624f 100755 --- a/webui.bat +++ b/webui.bat @@ -16,24 +16,44 @@ goto :show_stdout_stderr :check_pip %PYTHON% -mpip --help >tmp/stdout.txt 2>tmp/stderr.txt -if %ERRORLEVEL% == 0 goto :start_venv +if %ERRORLEVEL% == 0 goto :check_uv_flag if "%PIP_INSTALLER_LOCATION%" == "" goto :show_stdout_stderr %PYTHON% "%PIP_INSTALLER_LOCATION%" >tmp/stdout.txt 2>tmp/stderr.txt -if %ERRORLEVEL% == 0 goto :start_venv +if %ERRORLEVEL% == 0 goto :check_uv_flag echo Cannot install pip goto :show_stdout_stderr +:check_uv_flag +set use_uv=0 +for %%i in (%*) do ( + if /I "%%i"=="--uv" set use_uv=1 +) + +goto :start_venv + :start_venv if ["%VENV_DIR%"] == ["-"] goto :skip_venv if ["%SKIP_VENV%"] == ["1"] goto :skip_venv +if "%use_uv%" == "1" ( + where uv >nul 2>&1 + if %ERRORLEVEL% neq 0 ( + echo Warning: uv is not installed, falling back to python venv + set use_uv=0 + ) +) dir "%VENV_DIR%\Scripts\Python.exe" >tmp/stdout.txt 2>tmp/stderr.txt if %ERRORLEVEL% == 0 goto :activate_venv for /f "delims=" %%i in ('CALL %PYTHON% -c "import sys; print(sys.executable)"') do set PYTHON_FULLNAME="%%i" echo Using python: %PYTHON_FULLNAME% -echo Creating VENV: %VENV_DIR% -%PYTHON_FULLNAME% -m venv "%VENV_DIR%" >tmp/stdout.txt 2>tmp/stderr.txt +if "%use_uv%" == "1" ( + echo Creating VENV: UV + uv venv "%VENV_DIR%" >tmp/stdout.txt 2>tmp/stderr.txt +) else ( + echo Creating VENV: VENV + %PYTHON_FULLNAME% -m venv "%VENV_DIR%" >tmp/stdout.txt 2>tmp/stderr.txt +) if %ERRORLEVEL% == 0 goto :activate_venv echo Failed creating VENV: "%VENV_DIR%" goto :show_stdout_stderr diff --git a/webui.ps1 b/webui.ps1 index a607ac6f1..80675e50d 100755 --- a/webui.ps1 +++ b/webui.ps1 @@ -22,6 +22,14 @@ function ShowStdOutStdErr { $PYTHON = if ($env:PYTHON) { $env:PYTHON } else { 'python' } $VENV_DIR = if ($env:VENV_DIR) { $env:VENV_DIR } else { Join-Path $PSScriptRoot 'venv' } +$use_uv = $false +foreach ($arg in $args) { + if ($arg -eq '--uv') { + $use_uv = $true + break + } +} + New-Item -Path 'tmp' -ItemType Directory -ErrorAction SilentlyContinue try { @@ -58,9 +66,21 @@ if ($VENV_DIR -ne '-' -and $env:SKIP_VENV -ne '1') { $PYTHON_FULLNAME = & $PYTHON -c 'import sys; print(sys.executable)' Write-Output "Using python: $PYTHON_FULLNAME" - Write-Output "Creating VENV: $VENV_DIR" - & $PYTHON_FULLNAME -m venv $VENV_DIR 2>tmp\stderr.txt | Out-File tmp\stdout.txt + if ($use_uv) { + if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { + Write-Output 'Warning: uv is not installed, falling back to python venv' + $use_uv = $false + } + } + + if ($use_uv) { + Write-Output "Creating VENV: UV" + & uv venv $VENV_DIR 2>tmp\stderr.txt | Out-File tmp\stdout.txt + } else { + Write-Output "Creating VENV: VENV" + & $PYTHON_FULLNAME -m venv $VENV_DIR 2>tmp\stderr.txt | Out-File tmp\stdout.txt + } if ($LASTEXITCODE -eq 0) { $PYTHON = Join-Path $VENV_DIR 'Scripts\Python.exe' diff --git a/webui.sh b/webui.sh index 63bbf8ddf..04945a83e 100755 --- a/webui.sh +++ b/webui.sh @@ -7,6 +7,7 @@ cd -- "$(dirname -- "$0")" can_run_as_root=0 +use_uv=0 export ERROR_REPORTING=FALSE export PIP_IGNORE_INSTALLED=0 @@ -34,6 +35,15 @@ then venv_dir="venv" fi +for arg in "$@" +do + if [[ "$arg" == "--uv" ]] + then + use_uv=1 + break + fi +done + # read any command line flags to the webui.sh script while getopts "f" flag > /dev/null 2>&1 do @@ -59,16 +69,34 @@ do fi done -if ! "${PYTHON}" -c "import venv" &>/dev/null +if [[ "${use_uv}" -eq 1 ]] then - echo "Error: python3-venv is not installed" - exit 1 + if ! hash "uv" &>/dev/null + then + echo "Warning: uv is not installed globally" + use_uv=0 + fi +fi + +if [[ "${use_uv}" -eq 0 ]] +then + if ! "${PYTHON}" -c "import venv" &>/dev/null + then + echo "Error: python3-venv is not installed" + exit 1 + fi fi if [[ ! -d "${venv_dir}" ]] then - echo "Create python venv" - "${PYTHON}" -m venv "${venv_dir}" + if [[ "${use_uv}" -eq 1 ]] + then + echo "Create VENV: UV" + uv venv "${venv_dir}" + else + echo "Create VENV: VENV" + "${PYTHON}" -m venv "${venv_dir}" + fi first_launch=1 fi