From 11265c40344f3c668d96a1d1e80abb5bb5e8ad14 Mon Sep 17 00:00:00 2001 From: Michael Gallacher Date: Tue, 7 Mar 2023 14:57:37 -0700 Subject: [PATCH 1/4] Add support for MPS when running on Apple silicon Changes: * autodetect if MPS is available and the pytorch version has MPS support. * change logic from "is the device CPU?" to "is the device not CUDA?". * set PYTORCH_ENABLE_MPS_FALLBACK=1 Known issues: * Some samplers (eg DDIM) will fail on MPS unless forced to CPU-only mode --- scripts/on_sd_start.sh | 1 + ui/easydiffusion/device_manager.py | 20 +++++++++++--------- ui/easydiffusion/task_manager.py | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/scripts/on_sd_start.sh b/scripts/on_sd_start.sh index a8ed26be..c547c9dd 100755 --- a/scripts/on_sd_start.sh +++ b/scripts/on_sd_start.sh @@ -285,6 +285,7 @@ printf "\n\nEasy Diffusion installation complete, starting the server!\n\n" SD_PATH=`pwd` +export PYTORCH_ENABLE_MPS_FALLBACK=1 export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" echo "PYTHONPATH=$PYTHONPATH" diff --git a/ui/easydiffusion/device_manager.py b/ui/easydiffusion/device_manager.py index 4279cc46..8c486dd2 100644 --- a/ui/easydiffusion/device_manager.py +++ b/ui/easydiffusion/device_manager.py @@ -1,4 +1,5 @@ import os +import platform import torch import traceback import re @@ -66,6 +67,9 @@ def get_device_delta(render_devices, active_devices): def auto_pick_devices(currently_active_devices): global mem_free_threshold + if platform.system() == "Darwin" and torch.backends.mps.is_available() and torch.backends.mps.is_built(): + return ["mps"] + if not torch.cuda.is_available(): return ["cpu"] @@ -115,11 +119,11 @@ def device_init(context, device): validate_device_id(device, log_prefix="device_init") - if device == "cpu": - context.device = "cpu" + if "cuda" not in device: + context.device = device context.device_name = get_processor_name() context.half_precision = False - log.debug(f"Render device CPU available as {context.device_name}") + log.debug(f"Render device available as {context.device_name}") return context.device_name = torch.cuda.get_device_name(device) @@ -134,8 +138,6 @@ def device_init(context, device): log.info(f'Setting {device} as active, with precision: {"half" if context.half_precision else "full"}') torch.cuda.device(device) - return - def needs_to_force_full_precision(context): if "FORCE_FULL_PRECISION" in os.environ: @@ -174,7 +176,7 @@ def validate_device_id(device, log_prefix=""): def is_valid(): if not isinstance(device, str): return False - if device == "cpu": + if device == "cpu" or device == "mps": return True if not device.startswith("cuda:") or not device[5:].isnumeric(): return False @@ -182,7 +184,7 @@ def validate_device_id(device, log_prefix=""): if not is_valid(): raise EnvironmentError( - f"{log_prefix}: device id should be 'cpu', or 'cuda:N' (where N is an integer index for the GPU). Got: {device}" + f"{log_prefix}: device id should be 'cpu', 'mps', or 'cuda:N' (where N is an integer index for the GPU). Got: {device}" ) @@ -217,14 +219,14 @@ def is_device_compatible(device): def get_processor_name(): try: - import platform, subprocess + import subprocess if platform.system() == "Windows": return platform.processor() elif platform.system() == "Darwin": os.environ["PATH"] = os.environ["PATH"] + os.pathsep + "/usr/sbin" command = "sysctl -n machdep.cpu.brand_string" - return subprocess.check_output(command).strip() + return subprocess.check_output(command, shell=True).decode().strip() elif platform.system() == "Linux": command = "cat /proc/cpuinfo" all_info = subprocess.check_output(command, shell=True).decode().strip() diff --git a/ui/easydiffusion/task_manager.py b/ui/easydiffusion/task_manager.py index 28f84963..d2e112ac 100644 --- a/ui/easydiffusion/task_manager.py +++ b/ui/easydiffusion/task_manager.py @@ -385,7 +385,7 @@ def get_devices(): } def get_device_info(device): - if device == "cpu": + if "cuda" not in device: return {"name": device_manager.get_processor_name()} mem_free, mem_total = torch.cuda.mem_get_info(device) From 32d8f4d24b99b14a6111f82aabf57367cf1038b8 Mon Sep 17 00:00:00 2001 From: cmdr2 Date: Wed, 8 Mar 2023 09:57:45 +0530 Subject: [PATCH 2/4] sdkit 1.0.44 - mps support for mac --- scripts/on_sd_start.bat | 4 ++-- scripts/on_sd_start.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/on_sd_start.bat b/scripts/on_sd_start.bat index 86034432..1be66fcf 100644 --- a/scripts/on_sd_start.bat +++ b/scripts/on_sd_start.bat @@ -95,7 +95,7 @@ if "%ERRORLEVEL%" EQU "0" ( set PYTHONNOUSERSITE=1 set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages - call python -m pip install --upgrade sdkit==1.0.43 -q || ( + call python -m pip install --upgrade sdkit==1.0.44 -q || ( echo "Error updating sdkit" ) ) @@ -106,7 +106,7 @@ if "%ERRORLEVEL%" EQU "0" ( set PYTHONNOUSERSITE=1 set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages - call python -m pip install sdkit==1.0.43 || ( + call python -m pip install sdkit==1.0.44 || ( echo "Error installing sdkit. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" pause exit /b diff --git a/scripts/on_sd_start.sh b/scripts/on_sd_start.sh index a8ed26be..8571aa3a 100755 --- a/scripts/on_sd_start.sh +++ b/scripts/on_sd_start.sh @@ -81,7 +81,7 @@ if python ../scripts/check_modules.py sdkit sdkit.models ldm transformers numpy export PYTHONNOUSERSITE=1 export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" - python -m pip install --upgrade sdkit==1.0.43 -q + python -m pip install --upgrade sdkit==1.0.44 -q fi else echo "Installing sdkit: https://pypi.org/project/sdkit/" @@ -89,7 +89,7 @@ else export PYTHONNOUSERSITE=1 export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" - if python -m pip install sdkit==1.0.43 ; then + if python -m pip install sdkit==1.0.44 ; then echo "Installed." else fail "sdkit install failed" From 942904186a797fd6cbca0fe2894829f966cc7237 Mon Sep 17 00:00:00 2001 From: cmdr2 Date: Wed, 8 Mar 2023 10:02:05 +0530 Subject: [PATCH 3/4] changelog --- CHANGES.md | 1 + ui/index.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 101ba2dc..5d4bd4f6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,7 @@ Our focus continues to remain on an easy installation experience, and an easy user-interface. While still remaining pretty powerful, in terms of features and speed. ### Detailed changelog +* 2.5.23 - 8 Mar 2023 - Experimental support for mac. * 2.5.22 - 28 Feb 2023 - Minor styling changes to UI buttons, and the models dropdown. * 2.5.22 - 28 Feb 2023 - Lots of UI-related bug fixes. Thanks @patriceac. * 2.5.21 - 22 Feb 2023 - An option to control the size of the image thumbnails. You can use the `Display options` in the top-right corner to change this. Thanks @JeLuf. diff --git a/ui/index.html b/ui/index.html index 4f0ff2df..297d3b5f 100644 --- a/ui/index.html +++ b/ui/index.html @@ -26,7 +26,7 @@
From 3bb835b5e12c8318af5800ea8665f255ccafd1fc Mon Sep 17 00:00:00 2001 From: Olivia Godone-Maresca <8977984+ogmaresca@users.noreply.github.com> Date: Wed, 8 Mar 2023 10:22:31 -0500 Subject: [PATCH 4/4] Support custom modifiers with images (#912) * Support custom modifiers with images * Add dash support * Avoid needing to upgrade fastapi * Revert gitignore --- ui/easydiffusion/app.py | 92 ++++++++++++++++++++++++++++++++++++++ ui/easydiffusion/server.py | 16 ++++++- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/ui/easydiffusion/app.py b/ui/easydiffusion/app.py index 106db364..83bb08c1 100644 --- a/ui/easydiffusion/app.py +++ b/ui/easydiffusion/app.py @@ -5,6 +5,7 @@ import json import traceback import logging import shlex +import urllib from rich.logging import RichHandler from sdkit.utils import log as sdkit_log # hack, so we can overwrite the log config @@ -54,6 +55,10 @@ APP_CONFIG_DEFAULTS = { }, } +IMAGE_EXTENSIONS = [".png", ".apng", ".jpg", ".jpeg", ".jfif", ".pjpeg", ".pjp", ".jxl", ".gif", ".webp", ".avif", ".svg"] +CUSTOM_MODIFIERS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "modifiers")) +CUSTOM_MODIFIERS_PORTRAIT_EXTENSIONS=[".portrait", "_portrait", " portrait", "-portrait"] +CUSTOM_MODIFIERS_LANDSCAPE_EXTENSIONS=[".landscape", "_landscape", " landscape", "-landscape"] def init(): os.makedirs(USER_UI_PLUGINS_DIR, exist_ok=True) @@ -234,3 +239,90 @@ def open_browser(): import webbrowser webbrowser.open(f"http://localhost:{port}") + +def get_image_modifiers(): + modifiers_json_path = os.path.join(SD_UI_DIR, "modifiers.json") + + modifier_categories = {} + original_category_order=[] + with open(modifiers_json_path, "r", encoding="utf-8") as f: + modifiers_file = json.load(f) + + # The trailing slash is needed to support symlinks + if not os.path.isdir(f"{CUSTOM_MODIFIERS_DIR}/"): + return modifiers_file + + # convert modifiers from a list of objects to a dict of dicts + for category_item in modifiers_file: + category_name = category_item['category'] + original_category_order.append(category_name) + category = {} + for modifier_item in category_item['modifiers']: + modifier = {} + for preview_item in modifier_item['previews']: + modifier[preview_item['name']] = preview_item['path'] + category[modifier_item['modifier']] = modifier + modifier_categories[category_name] = category + + def scan_directory(directory_path: str, category_name="Modifiers"): + for entry in os.scandir(directory_path): + if entry.is_file(): + file_extension = list(filter(lambda e: entry.name.endswith(e), IMAGE_EXTENSIONS)) + if len(file_extension) == 0: + continue + + modifier_name = entry.name[: -len(file_extension[0])] + modifier_path = f"custom/{entry.path[len(CUSTOM_MODIFIERS_DIR) + 1:]}" + # URL encode path segments + modifier_path = "/".join(map(lambda segment: urllib.parse.quote(segment), modifier_path.split("/"))) + is_portrait = True + is_landscape = True + + portrait_extension = list(filter(lambda e: modifier_name.lower().endswith(e), CUSTOM_MODIFIERS_PORTRAIT_EXTENSIONS)) + landscape_extension = list(filter(lambda e: modifier_name.lower().endswith(e), CUSTOM_MODIFIERS_LANDSCAPE_EXTENSIONS)) + + if len(portrait_extension) > 0: + is_landscape = False + modifier_name = modifier_name[: -len(portrait_extension[0])] + elif len(landscape_extension) > 0: + is_portrait = False + modifier_name = modifier_name[: -len(landscape_extension[0])] + + if (category_name not in modifier_categories): + modifier_categories[category_name] = {} + + category = modifier_categories[category_name] + + if (modifier_name not in category): + category[modifier_name] = {} + + if (is_portrait or "portrait" not in category[modifier_name]): + category[modifier_name]["portrait"] = modifier_path + + if (is_landscape or "landscape" not in category[modifier_name]): + category[modifier_name]["landscape"] = modifier_path + elif entry.is_dir(): + scan_directory( + entry.path, + entry.name if directory_path==CUSTOM_MODIFIERS_DIR else f"{category_name}/{entry.name}", + ) + + scan_directory(CUSTOM_MODIFIERS_DIR) + + custom_categories = sorted( + [cn for cn in modifier_categories.keys() if cn not in original_category_order], + key=str.casefold, + ) + + # convert the modifiers back into a list of objects + modifier_categories_list = [] + for category_name in [*original_category_order, *custom_categories]: + category = { 'category': category_name, 'modifiers': [] } + for modifier_name in sorted(modifier_categories[category_name].keys(), key=str.casefold): + modifier = { 'modifier': modifier_name, 'previews': [] } + for preview_name, preview_path in modifier_categories[category_name][modifier_name].items(): + modifier['previews'].append({ 'name': preview_name, 'path': preview_path }) + category['modifiers'].append(modifier) + modifier_categories_list.append(category) + + return modifier_categories_list diff --git a/ui/easydiffusion/server.py b/ui/easydiffusion/server.py index c04189c1..77abd41f 100644 --- a/ui/easydiffusion/server.py +++ b/ui/easydiffusion/server.py @@ -25,6 +25,13 @@ NOCACHE_HEADERS = {"Cache-Control": "no-cache, no-store, must-revalidate", "Prag class NoCacheStaticFiles(StaticFiles): + def __init__(self, directory: str): + # follow_symlink is only available on fastapi >= 0.92.0 + if (os.path.islink(directory)): + super().__init__(directory = os.path.realpath(directory)) + else: + super().__init__(directory = directory) + def is_not_modified(self, response_headers, request_headers) -> bool: if "content-type" in response_headers and ( "javascript" in response_headers["content-type"] or "css" in response_headers["content-type"] @@ -45,6 +52,13 @@ class SetAppConfigRequest(BaseModel): def init(): + if os.path.isdir(app.CUSTOM_MODIFIERS_DIR): + server_api.mount( + "/media/modifier-thumbnails/custom", + NoCacheStaticFiles(directory=app.CUSTOM_MODIFIERS_DIR), + name="custom-thumbnails", + ) + server_api.mount("/media", NoCacheStaticFiles(directory=os.path.join(app.SD_UI_DIR, "media")), name="media") for plugins_dir, dir_prefix in app.UI_PLUGINS_SOURCES: @@ -156,7 +170,7 @@ def read_web_data_internal(key: str = None): elif key == "models": return JSONResponse(model_manager.getModels(), headers=NOCACHE_HEADERS) elif key == "modifiers": - return FileResponse(os.path.join(app.SD_UI_DIR, "modifiers.json"), headers=NOCACHE_HEADERS) + return JSONResponse(app.get_image_modifiers(), headers=NOCACHE_HEADERS) elif key == "ui_plugins": return JSONResponse(app.getUIPlugins(), headers=NOCACHE_HEADERS) else: