From 7e4aed79489bc74016eecbc603c392a4d266f266 Mon Sep 17 00:00:00 2001 From: Midcoastal Date: Tue, 15 Aug 2023 16:07:41 -0400 Subject: [PATCH 01/13] Add a FS directory/path cacher - Added FS dir/path mechanism to 'modules.modelloader' - Refactored 'modules.modelloader.load_models' to use the cache - Refactored 'modules.ui_extra_networks.find_*' methods to use the cache - Added progress indicator to 'modules.ui_extra_networks.create_html' The cache, as implimented, will always ensure it is up-to-date (per 'directory_has_changed') and significantly improves loading speed (when used with 'model_loader' and the 'find_*' methods) with large model directories. Overall load speed tested with ~10k models (mix of checkpoints, loras, lycoris, and embeddings) and ~40k secndary files (images, descriptions, CivitAI Info, etc). Loading speeds went from ~1 hour and 45 minutes to ~5 minutes. Confounding variable to loading speeds: this is over a fiber-attached storage device. Connection is 10gbe full-duplex, remote source has an NVMe raid cache. Saturation of the network is the norm, but laintency is a factor. Regardless, small-scale local-storage testing also shows measurable improvements, so this should be a welcome addition. --- modules/modelloader.py | 71 +++++++++++++++++++++----- modules/ui_extra_networks.py | 96 ++++++++++++++++++++++-------------- 2 files changed, 118 insertions(+), 49 deletions(-) diff --git a/modules/modelloader.py b/modules/modelloader.py index 6527e9efd..dbba84339 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -137,6 +137,62 @@ def find_diffuser(name: str): return models[0].modelId return None +modelloader_directories = {} + +def directory_has_changed(dir:str, *, recursive:bool=True) -> bool: + dir = os.path.abspath(dir) + if dir not in modelloader_directories: + return True + if not (os.path.exists(dir) and os.path.isdir(dir) and os.path.getmtime(dir) == modelloader_directories[dir][0]): + return True + if recursive: + for _dir in modelloader_directories: + if _dir.startswith(dir) and _dir != dir and not (os.path.exists(_dir) and os.path.isdir(_dir) and os.path.getmtime(_dir) == modelloader_directories[_dir][0]): + return True + return False + +def directory_directories(dir:str, *, recursive:bool=True) -> dict[str,tuple[float,list[str]]]: + dir = os.path.abspath(dir) + if directory_has_changed(dir, recursive=recursive): + for _dir in modelloader_directories: + if not (os.path.exists(_dir) and os.path.isdir(_dir)): + del modelloader_directories[_dir] + for _dir, _subdirs, _files in os.walk(dir, topdown=False, followlinks=True): + mtime = os.path.getmtime(_dir) + if _dir not in modelloader_directories or mtime>modelloader_directories[_dir][0]: + modelloader_directories[_dir] = (mtime, [os.path.join(_dir, fn) for fn in _files]) + directory_directories = {} + for _dir in modelloader_directories: + if _dir == dir or (recursive and _dir.startswith(dir)): + directory_directories[_dir] = modelloader_directories[_dir] + if not recursive: + break + return directory_directories + +def directories_file_paths(directories:dict) -> list[str]: + return sum([[fp for fp in dat[1]] for dat in directories.values()], []) + +def filter_paths(paths:list[str], *, filter:callable=None) -> list[str]: + return [fp for fp in paths if not (os.path.islink(fp) and not os.path.exists(fp)) and filter(fp)] + +def unique_directories(directories:list[str], *, recursive:bool=True) -> list[str]: + '''Ensure no empty, or duplicates''' + directories = { os.path.abspath(dir): True for dir in directories if dir }.keys() + if recursive: + '''If we are going recursive, then directories that are children of other directories are redundant''' + directories = [dir for dir in directories if not any(_dir != dir and dir.startswith(_dir) for _dir in directories)] + return directories + +def unique_paths(paths:list[str]) -> list[str]: + return { fp: True for fp in paths }.keys() + +def directory_files(*directories:list[str], recursive:bool=True) -> list[str]: + return unique_paths(sum([[fp for fp in directories_file_paths(directory_directories(dir, recursive=recursive))] for dir in unique_directories(directories, recursive=recursive)],[])) + +def extension_filter(ext_filter=None, ext_blacklist=None): + def filter(fp:str): + return (not ext_filter or any(fp.endswith(ew) for ew in ext_filter)) and (not ext_blacklist or not any(fp.endswith(ew) for ew in ext_blacklist)) + return filter def load_models(model_path: str, model_url: str = None, command_path: str = None, ext_filter=None, download_name=None, ext_blacklist=None) -> list: """ @@ -149,21 +205,10 @@ def load_models(model_path: str, model_url: str = None, command_path: str = None @param ext_filter: An optional list of filename extensions to filter by @return: A list of paths containing the desired model(s) """ - places = [] - places.append(model_path) - if command_path is not None and command_path != model_path and os.path.isdir(command_path): - places.append(command_path) + places = unique_directories([model_path, command_path]) output = [] try: - for place in places: - for full_path in shared.walk_files(place, allowed_extensions=ext_filter): - if os.path.islink(full_path) and not os.path.exists(full_path): - shared.log.error(f"Skipping broken symlink: {full_path}") - continue - if ext_blacklist is not None and any(full_path.endswith(x) for x in ext_blacklist): - continue - if full_path not in output: - output.append(full_path) + output:list = filter_paths(directory_files(*places), filter=extension_filter(ext_filter, ext_blacklist)) if model_url is not None and len(output) == 0: if download_name is not None: from basicsr.utils.download_util import load_file_from_url diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index f5c8f2a93..4e31f38f1 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -8,9 +8,12 @@ from pathlib import Path from collections import OrderedDict import gradio as gr from PIL import Image -from modules import shared, scripts +from modules import shared, scripts, modelloader from modules.generation_parameters_copypaste import image_from_url_text from modules.ui_components import ToolButton +from logging import DEBUG +from time import time +from rich.progress import Progress, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn, TimeElapsedColumn, SpinnerColumn extra_pages = [] allowed_dirs = set() @@ -158,19 +161,17 @@ class ExtraNetworksPage: return f"
Extra network page not ready
Click refresh to try again
" subdirs = {} allowed_folders = [os.path.abspath(x) for x in self.allowed_directories_for_previews()] - for parentdir in [*set(allowed_folders)]: - for root, dirs, _files in os.walk(parentdir, followlinks=True): - for dirname in dirs: - x = os.path.join(root, dirname) - if shared.opts.diffusers_dir in x: - subdirs[os.path.basename(shared.opts.diffusers_dir)] = 1 - if (not os.path.isdir(x)) or ('models--' in x): - continue - subdir = os.path.abspath(x)[len(parentdir):].replace("\\", "/") - while subdir.startswith("/"): - subdir = subdir[1:] - if not self.is_empty(x): - subdirs[subdir] = 1 + for parentdir, dirs in {dir: modelloader.directory_directories(dir) for dir in allowed_folders}.items(): + for dir in dirs.keys(): + if shared.opts.diffusers_dir in dir: + subdirs[os.path.basename(shared.opts.diffusers_dir)] = 1 + if 'models--' in dir: + continue + subdir = dir[len(parentdir):].replace("\\", "/") + while subdir.startswith("/"): + subdir = subdir[1:] + if not self.is_empty(dir): + subdirs[subdir] = 1 if subdirs: subdirs = OrderedDict(sorted(subdirs.items())) subdirs = {"": 1, **subdirs} @@ -189,10 +190,25 @@ class ExtraNetworksPage: self.items = [] shared.log.error(f'Extra networks error listing items: {self.__class__}') self.create_xyz_grid() - for item in self.items: - self.metadata[item["name"]] = item.get("metadata", {}) - self.info[item["name"]] = self.find_info(item['filename']) - self.html += self.create_html_for_item(item, tabname) + with Progress( + SpinnerColumn(), + TextColumn('[cyan]Creating Extra Network '+self.title+' HTML - {task.description}'), + BarColumn(), TaskProgressColumn(), TextColumn('({task.completed}/{task.total})'), + TimeRemainingColumn(), TimeElapsedColumn(), transient=not shared.log.isEnabledFor(DEBUG), expand=True + ) as progress: + task = progress.add_task(description=f'Initializing Items') + items = self.items + progress.update(task, total=len(items)) + __t = None + __i = 0 + for item in items: + if __t is None: + __t = time() + __i += 1 + self.metadata[item["name"]] = item.get("metadata", {}) + self.info[item["name"]] = self.find_info(item['filename']) + self.html += self.create_html_for_item(item, tabname) + progress.update(task, advance=1, description=f"{round(__i/(shared.time.time()-__t))} item/s") if len(subdirs_html) > 0 or len(self.html) > 0: res = f"
{subdirs_html}
{self.html}
" else: @@ -201,7 +217,7 @@ class ExtraNetworksPage: threading.Thread(target=self.create_thumb).start() return res except Exception as e: - shared.log.error(f'Extra networks page error: {e}') + shared.log.error(f'Extra networks {self.title} {tabname} page error: {e.__class__.__name__} -> {e}') return f"
Extra network error
{e}
" def list_items(self): @@ -238,7 +254,7 @@ class ExtraNetworksPage: args['title'] += f'\nAlias: {item["alias"]}' if item.get("tags", None) is not None: args['title'] += f'\nTags: {", ".join(tags)}' - self.card.format(**args) + #self.card.format(**args) return self.card.format(**args) except Exception as e: shared.log.error(f'Extra networks item error: page={tabname} item={item["name"]} {e}') @@ -246,36 +262,44 @@ class ExtraNetworksPage: def find_preview(self, path): preview_extensions = ["jpg", "jpeg", "png", "webp", "tiff", "jp2"] + dir = os.path.dirname(path) + paths = modelloader.directory_directories(dir, recursive=False) for file in sum([[f'{path}.thumb.{ext}'] for ext in preview_extensions], []): # use thumbnail if exists - if os.path.isfile(file): + if file in paths[dir][1]: return self.link_preview(file) for file in sum([[f'{path}.preview.{ext}', f'{path}.{ext}'] for ext in preview_extensions], []): - if os.path.isfile(file): + if file in paths[dir][1]: self.missing_thumbs.append(file) return self.link_preview(file) return self.link_preview('html/card-no-preview.png') def find_description(self, path): + dir = os.path.dirname(path) + paths = modelloader.directory_directories(dir, recursive=False) for file in [f"{path}.txt", f"{path}.description.txt"]: - try: - with open(file, "r", encoding="utf-8", errors="replace") as f: - txt = f.read() - txt = re.sub('[<>]', '', txt) - return txt - except OSError: - pass + if file in paths[dir][1]: + try: + with open(file, "r", encoding="utf-8", errors="replace") as f: + txt = f.read() + txt = re.sub('[<>]', '', txt) + return txt + except OSError: + pass return None def find_info(self, path): + dir = os.path.dirname(path) + paths = modelloader.directory_directories(dir, recursive=False) basename, _ext = os.path.splitext(path) for file in [f"{path}.info", f"{path}.civitai.info", f"{basename}.info", f"{basename}.civitai.info"]: - try: - with open(file, "r", encoding="utf-8", errors="replace") as f: - txt = f.read() - txt = re.sub('[<>]', '', txt) - return txt - except OSError: - pass + if file in paths[dir][1]: + try: + with open(file, "r", encoding="utf-8", errors="replace") as f: + txt = f.read() + txt = re.sub('[<>]', '', txt) + return txt + except OSError: + pass return None From 05850c23441aab0cecaa0d80cfa86f226a7330ff Mon Sep 17 00:00:00 2001 From: Midcoastal Date: Tue, 15 Aug 2023 23:06:23 -0400 Subject: [PATCH 02/13] Upgrade Lora/TI model listers to use cache --- extensions-builtin/Lora/lora.py | 6 ++-- .../textual_inversion/textual_inversion.py | 28 ++++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/extensions-builtin/Lora/lora.py b/extensions-builtin/Lora/lora.py index 0822f3c10..08d4d6f18 100644 --- a/extensions-builtin/Lora/lora.py +++ b/extensions-builtin/Lora/lora.py @@ -3,6 +3,7 @@ import re from typing import Union import torch from modules import shared, devices, sd_models, errors, scripts, sd_hijack, hashes +from modules.modelloader import filter_paths, directory_files, extension_filter metadata_tags_order = {"ss_sd_model_name": 1, "ss_resolution": 2, "ss_clip_skip": 3, "ss_num_train_images": 10, "ss_tag_frequency": 20} @@ -444,10 +445,7 @@ def list_available_loras(): os.makedirs(shared.cmd_opts.lora_dir, exist_ok=True) - candidates = list(shared.walk_files(shared.cmd_opts.lora_dir, allowed_extensions=[".pt", ".ckpt", ".safetensors"])) - for filename in sorted(candidates, key=str.lower): - if os.path.isdir(filename): - continue + for filename in sorted(filter_paths(directory_files(shared.cmd_opts.lora_dir), filter=extension_filter(['.PT', '.CKPT', '.SAFETENSORS'])), key=str.lower): name = os.path.splitext(os.path.basename(filename))[0] entry = LoraOnDisk(name, filename) diff --git a/modules/textual_inversion/textual_inversion.py b/modules/textual_inversion/textual_inversion.py index 25345a35a..845cc1ce3 100644 --- a/modules/textual_inversion/textual_inversion.py +++ b/modules/textual_inversion/textual_inversion.py @@ -13,6 +13,7 @@ import modules.textual_inversion.dataset from modules.textual_inversion.learn_schedule import LearnRateScheduler from modules.textual_inversion.image_embedding import embedding_to_b64, embedding_from_b64, insert_image_data_embed, extract_image_data_embed, caption_image_overlay from modules.textual_inversion.logging import save_settings_to_file +from modules.modelloader import filter_paths, directory_files, extension_filter, directory_mtime TextualInversionTemplate = namedtuple("TextualInversionTemplate", ["name", "path"]) textual_inversion_templates = {} @@ -85,15 +86,13 @@ class DirWithTextualInversionEmbeddings: if not os.path.isdir(self.path): return False - mt = os.path.getmtime(self.path) - if self.mtime is None or mt > self.mtime: - return True + return directory_mtime(self.path) != self.mtime def update(self): if not os.path.isdir(self.path): return - self.mtime = os.path.getmtime(self.path) + self.mtime = directory_mtime(self.path) class EmbeddingDatabase: @@ -216,16 +215,19 @@ class EmbeddingDatabase: def load_from_dir(self, embdir): if not os.path.isdir(embdir.path): return - for root, _dirs, fns in os.walk(embdir.path, followlinks=True): - for fn in fns: - try: - fullfn = os.path.join(root, fn) - if os.stat(fullfn).st_size == 0: - continue - self.load_from_file(fullfn, fn) - except Exception as e: - errors.display(e, f'embedding load {fn}') + + is_ext = extension_filter(['.PNG', '.WEBP', '.JXL', '.AVIF', '.BIN', '.PT', '.SAFETENSORS']) + is_not_preview = lambda fp: not next(iter(os.path.splitext(fp))).upper().endswith('.PREVIEW') + + for file_path in filter_paths(directory_files(embdir.path), filter=lambda fp: is_ext(fp) and is_not_preview(fp)): + try: + if os.stat(file_path).st_size == 0: continue + fn = os.path.basename(file_path) + self.load_from_file(file_path, fn) + except Exception as e: + errors.display(e, f'embedding load {fn}') + continue def load_textual_inversion_embeddings(self, force_reload=False): if not force_reload: From 6eae768fb06da8eb9dc8d0227ac1f7151b26d844 Mon Sep 17 00:00:00 2001 From: Midcoastal Date: Tue, 15 Aug 2023 23:08:26 -0400 Subject: [PATCH 03/13] Gather HTML parts as list to append in the end Somehow, significant increase in speed (size of text append was slowing large loads down. --- modules/ui_extra_networks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 4e31f38f1..75d3a7e23 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -190,6 +190,7 @@ class ExtraNetworksPage: self.items = [] shared.log.error(f'Extra networks error listing items: {self.__class__}') self.create_xyz_grid() + htmls = [] with Progress( SpinnerColumn(), TextColumn('[cyan]Creating Extra Network '+self.title+' HTML - {task.description}'), @@ -199,16 +200,15 @@ class ExtraNetworksPage: task = progress.add_task(description=f'Initializing Items') items = self.items progress.update(task, total=len(items)) - __t = None + __t = time() __i = 0 for item in items: - if __t is None: - __t = time() __i += 1 self.metadata[item["name"]] = item.get("metadata", {}) self.info[item["name"]] = self.find_info(item['filename']) - self.html += self.create_html_for_item(item, tabname) + htmls.append(self.create_html_for_item(item, tabname)) progress.update(task, advance=1, description=f"{round(__i/(shared.time.time()-__t))} item/s") + self.html += ''.join(htmls) if len(subdirs_html) > 0 or len(self.html) > 0: res = f"
{subdirs_html}
{self.html}
" else: From 873e864640be7e3af516c5870fc559280c77a1b7 Mon Sep 17 00:00:00 2001 From: Midcoastal Date: Tue, 15 Aug 2023 23:09:24 -0400 Subject: [PATCH 04/13] Ooof, modelloader changes needed for TI upgrade --- modules/modelloader.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/modelloader.py b/modules/modelloader.py index dbba84339..19229701e 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -169,6 +169,9 @@ def directory_directories(dir:str, *, recursive:bool=True) -> dict[str,tuple[flo break return directory_directories +def directory_mtime(dir:str, *, recursive:bool=True) -> float: + return float(max(0, *[mtime for mtime, _ in directory_directories(dir, recursive=recursive).values()])) + def directories_file_paths(directories:dict) -> list[str]: return sum([[fp for fp in dat[1]] for dat in directories.values()], []) @@ -190,8 +193,12 @@ def directory_files(*directories:list[str], recursive:bool=True) -> list[str]: return unique_paths(sum([[fp for fp in directories_file_paths(directory_directories(dir, recursive=recursive))] for dir in unique_directories(directories, recursive=recursive)],[])) def extension_filter(ext_filter=None, ext_blacklist=None): + if ext_filter: + ext_filter = [ext.upper() for ext in ext_filter] + if ext_blacklist: + ext_blacklist = [ext.upper() for ext in ext_blacklist] def filter(fp:str): - return (not ext_filter or any(fp.endswith(ew) for ew in ext_filter)) and (not ext_blacklist or not any(fp.endswith(ew) for ew in ext_blacklist)) + return (not ext_filter or any(fp.upper().endswith(ew) for ew in ext_filter)) and (not ext_blacklist or not any(fp.upper().endswith(ew) for ew in ext_blacklist)) return filter def load_models(model_path: str, model_url: str = None, command_path: str = None, ext_filter=None, download_name=None, ext_blacklist=None) -> list: From c9bdd0344d6c7d4b2350edae79cdba6f72575d18 Mon Sep 17 00:00:00 2001 From: Midcoastal Date: Tue, 15 Aug 2023 23:10:30 -0400 Subject: [PATCH 05/13] Allow JPEG image files JPEG is allowed and searched for when looking for previews/thumbs, and therefore should be allowed to view. --- modules/ui_extra_networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 75d3a7e23..20c2a2111 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -34,7 +34,7 @@ def fetch_file(filename: str = ""): return FileResponse(filename, headers={"Accept-Ranges": "bytes"}) if not any(Path(x).absolute() in Path(filename).absolute().parents for x in allowed_dirs): return JSONResponse({"error": f"File cannot be fetched: {filename}. Must be in one of directories registered by extra pages."}) - if os.path.splitext(filename)[1].lower() not in (".png", ".jpg", ".webp"): + if os.path.splitext(filename)[1].lower() not in (".png", ".jpg", ".jpeg", ".webp"): return JSONResponse({"error": f"File cannot be fetched: {filename}. Only png and jpg and webp."}) return FileResponse(filename, headers={"Accept-Ranges": "bytes"}) From 03363ce866fa4dcbfe9b05338319708e762d0f5a Mon Sep 17 00:00:00 2001 From: Midcoastal Date: Tue, 15 Aug 2023 23:20:00 -0400 Subject: [PATCH 06/13] Resolve Lint Errors: C416 (x2), W291 and F541 --- modules/modelloader.py | 4 ++-- modules/ui_extra_networks.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/modelloader.py b/modules/modelloader.py index 19229701e..45e41315f 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -173,7 +173,7 @@ def directory_mtime(dir:str, *, recursive:bool=True) -> float: return float(max(0, *[mtime for mtime, _ in directory_directories(dir, recursive=recursive).values()])) def directories_file_paths(directories:dict) -> list[str]: - return sum([[fp for fp in dat[1]] for dat in directories.values()], []) + return sum([fp for fp in dat[1] for dat in directories.values()], []) def filter_paths(paths:list[str], *, filter:callable=None) -> list[str]: return [fp for fp in paths if not (os.path.islink(fp) and not os.path.exists(fp)) and filter(fp)] @@ -190,7 +190,7 @@ def unique_paths(paths:list[str]) -> list[str]: return { fp: True for fp in paths }.keys() def directory_files(*directories:list[str], recursive:bool=True) -> list[str]: - return unique_paths(sum([[fp for fp in directories_file_paths(directory_directories(dir, recursive=recursive))] for dir in unique_directories(directories, recursive=recursive)],[])) + return unique_paths(sum([fp for fp in directories_file_paths(directory_directories(dir, recursive=recursive)) for dir in unique_directories(directories, recursive=recursive)],[])) def extension_filter(ext_filter=None, ext_blacklist=None): if ext_filter: diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 20c2a2111..4240da4e4 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -193,11 +193,11 @@ class ExtraNetworksPage: htmls = [] with Progress( SpinnerColumn(), - TextColumn('[cyan]Creating Extra Network '+self.title+' HTML - {task.description}'), + TextColumn('[cyan]Creating Extra Network '+self.title+' HTML - {task.description}'), BarColumn(), TaskProgressColumn(), TextColumn('({task.completed}/{task.total})'), TimeRemainingColumn(), TimeElapsedColumn(), transient=not shared.log.isEnabledFor(DEBUG), expand=True ) as progress: - task = progress.add_task(description=f'Initializing Items') + task = progress.add_task(description='Initializing Items') items = self.items progress.update(task, total=len(items)) __t = time() From 89da66207ab2d77078d5eef4588c6aea58491c1c Mon Sep 17 00:00:00 2001 From: Midcoastal Date: Wed, 16 Aug 2023 00:25:27 -0400 Subject: [PATCH 07/13] I guess I didn't quite understand Lint error C416 Seems like a rather pedantic check, if you ask me... Oh well. --- modules/modelloader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/modelloader.py b/modules/modelloader.py index 45e41315f..2406196b9 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -173,7 +173,7 @@ def directory_mtime(dir:str, *, recursive:bool=True) -> float: return float(max(0, *[mtime for mtime, _ in directory_directories(dir, recursive=recursive).values()])) def directories_file_paths(directories:dict) -> list[str]: - return sum([fp for fp in dat[1] for dat in directories.values()], []) + return sum(list([fp for fp in dat[1]] for dat in directories.values()), []) def filter_paths(paths:list[str], *, filter:callable=None) -> list[str]: return [fp for fp in paths if not (os.path.islink(fp) and not os.path.exists(fp)) and filter(fp)] @@ -190,7 +190,7 @@ def unique_paths(paths:list[str]) -> list[str]: return { fp: True for fp in paths }.keys() def directory_files(*directories:list[str], recursive:bool=True) -> list[str]: - return unique_paths(sum([fp for fp in directories_file_paths(directory_directories(dir, recursive=recursive)) for dir in unique_directories(directories, recursive=recursive)],[])) + return unique_paths(sum(list([fp for fp in directories_file_paths(directory_directories(dir, recursive=recursive))] for dir in unique_directories(directories, recursive=recursive)),[])) def extension_filter(ext_filter=None, ext_blacklist=None): if ext_filter: From 67f369ed25a42ceac84b215cb362a6df94056109 Mon Sep 17 00:00:00 2001 From: Midcoastal Date: Thu, 17 Aug 2023 16:49:42 -0400 Subject: [PATCH 08/13] A walk() optimization and lint fixes --- extensions-builtin/Lora/lora.py | 4 +- modules/modelloader.py | 110 ++++++++++++++---- .../textual_inversion/textual_inversion.py | 6 +- modules/ui_extra_networks.py | 10 +- 4 files changed, 99 insertions(+), 31 deletions(-) diff --git a/extensions-builtin/Lora/lora.py b/extensions-builtin/Lora/lora.py index 08d4d6f18..607dd8e33 100644 --- a/extensions-builtin/Lora/lora.py +++ b/extensions-builtin/Lora/lora.py @@ -3,7 +3,7 @@ import re from typing import Union import torch from modules import shared, devices, sd_models, errors, scripts, sd_hijack, hashes -from modules.modelloader import filter_paths, directory_files, extension_filter +from modules.modelloader import directory_files, extension_filter metadata_tags_order = {"ss_sd_model_name": 1, "ss_resolution": 2, "ss_clip_skip": 3, "ss_num_train_images": 10, "ss_tag_frequency": 20} @@ -445,7 +445,7 @@ def list_available_loras(): os.makedirs(shared.cmd_opts.lora_dir, exist_ok=True) - for filename in sorted(filter_paths(directory_files(shared.cmd_opts.lora_dir), filter=extension_filter(['.PT', '.CKPT', '.SAFETENSORS'])), key=str.lower): + for filename in sorted([*filter(extension_filter(['.PT', '.CKPT', '.SAFETENSORS']), directory_files(shared.cmd_opts.lora_dir))], key=str.lower): name = os.path.splitext(os.path.basename(filename))[0] entry = LoraOnDisk(name, filename) diff --git a/modules/modelloader.py b/modules/modelloader.py index 2406196b9..c37917dd8 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -6,9 +6,59 @@ from urllib.parse import urlparse from modules import shared from modules.upscaler import Upscaler, UpscalerLanczos, UpscalerNearest, UpscalerNone from modules.paths import script_path, models_path +import inspect +import time diffuser_repos = [] +def walk(top, onerror:callable=None): + # A near-exact copy of `os.path.walk()`, trimmed slightly. + # Probably not nessesary for most people's collections, but makes + # a difference on really large datasets. + nondirs = [] + walk_dirs = [] + + try: + scandir_it = os.scandir(top) + except OSError as error: + if onerror is not None: + onerror(error, top) + return + + with scandir_it: + while True: + try: + try: + entry = next(scandir_it) + except StopIteration: + break + except OSError as error: + if onerror is not None: + onerror(error, top) + return + + try: + is_dir = entry.is_dir() + except OSError: + is_dir = False + + if not is_dir: + nondirs.append(entry.name) + else: + try: + if entry.is_symlink() and not os.path.exists(entry.path): + raise NotADirectoryError('Broken Symlink') + walk_dirs.append(entry.path) + except OSError as error: + if onerror is not None: + onerror(error, entry.path) + + # Recurse into sub-directories + for new_path in walk_dirs: + yield from walk(new_path, onerror) + # Yield after recursion if going bottom up + yield top, nondirs + def download_civit_model(model_url: str, model_name: str, model_path: str, preview): model_file = os.path.join(shared.opts.ckpt_dir, model_path, model_name) @@ -138,29 +188,47 @@ def find_diffuser(name: str): return None modelloader_directories = {} +cache_last = 0 +cache_time = 1 def directory_has_changed(dir:str, *, recursive:bool=True) -> bool: - dir = os.path.abspath(dir) - if dir not in modelloader_directories: + try: + dir = os.path.abspath(dir) + if dir not in modelloader_directories: + return True + if cache_last > (time.time() - cache_time): + return False + if not (os.path.exists(dir) and os.path.isdir(dir) and os.path.getmtime(dir) == modelloader_directories[dir][0]): + return True + if recursive: + for _dir in modelloader_directories: + if _dir.startswith(dir) and _dir != dir and not (os.path.exists(_dir) and os.path.isdir(_dir) and os.path.getmtime(_dir) == modelloader_directories[_dir][0]): + return True + except Exception as e: + shared.log.error(f"Filesystem Error: {e.__class__.__name__}({e})") return True - if not (os.path.exists(dir) and os.path.isdir(dir) and os.path.getmtime(dir) == modelloader_directories[dir][0]): - return True - if recursive: - for _dir in modelloader_directories: - if _dir.startswith(dir) and _dir != dir and not (os.path.exists(_dir) and os.path.isdir(_dir) and os.path.getmtime(_dir) == modelloader_directories[_dir][0]): - return True + return False def directory_directories(dir:str, *, recursive:bool=True) -> dict[str,tuple[float,list[str]]]: dir = os.path.abspath(dir) if directory_has_changed(dir, recursive=recursive): for _dir in modelloader_directories: - if not (os.path.exists(_dir) and os.path.isdir(_dir)): + try: + if (os.path.exists(_dir) and os.path.isdir(_dir)): + continue + except Exception: + pass + del modelloader_directories[_dir] + for _dir, _files in walk(dir, lambda e, path: shared.log.error(f"Filesystem Walk Error: {e.__class__.__name__}({e}) -> {path}")): + try: + mtime = os.path.getmtime(_dir) + if _dir not in modelloader_directories or mtime != modelloader_directories[_dir][0]: + modelloader_directories[_dir] = (mtime, [os.path.join(_dir, fn) for fn in _files]) + except Exception as e: + shared.log.error(f"Filesystem Error: {e.__class__.__name__}({e})") del modelloader_directories[_dir] - for _dir, _subdirs, _files in os.walk(dir, topdown=False, followlinks=True): - mtime = os.path.getmtime(_dir) - if _dir not in modelloader_directories or mtime>modelloader_directories[_dir][0]: - modelloader_directories[_dir] = (mtime, [os.path.join(_dir, fn) for fn in _files]) + directory_directories = {} for _dir in modelloader_directories: if _dir == dir or (recursive and _dir.startswith(dir)): @@ -173,30 +241,27 @@ def directory_mtime(dir:str, *, recursive:bool=True) -> float: return float(max(0, *[mtime for mtime, _ in directory_directories(dir, recursive=recursive).values()])) def directories_file_paths(directories:dict) -> list[str]: - return sum(list([fp for fp in dat[1]] for dat in directories.values()), []) - -def filter_paths(paths:list[str], *, filter:callable=None) -> list[str]: - return [fp for fp in paths if not (os.path.islink(fp) and not os.path.exists(fp)) and filter(fp)] + return sum([dat[1] for dat in directories.values()],[]) def unique_directories(directories:list[str], *, recursive:bool=True) -> list[str]: '''Ensure no empty, or duplicates''' directories = { os.path.abspath(dir): True for dir in directories if dir }.keys() if recursive: '''If we are going recursive, then directories that are children of other directories are redundant''' - directories = [dir for dir in directories if not any(_dir != dir and dir.startswith(_dir) for _dir in directories)] + directories = [dir for dir in directories if not any(_dir != dir and dir.startswith(os.path.join(_dir,'')) for _dir in directories)] return directories def unique_paths(paths:list[str]) -> list[str]: return { fp: True for fp in paths }.keys() def directory_files(*directories:list[str], recursive:bool=True) -> list[str]: - return unique_paths(sum(list([fp for fp in directories_file_paths(directory_directories(dir, recursive=recursive))] for dir in unique_directories(directories, recursive=recursive)),[])) + return unique_paths(sum([[*directories_file_paths(directory_directories(dir, recursive=recursive))] for dir in unique_directories(directories, recursive=recursive)],[])) def extension_filter(ext_filter=None, ext_blacklist=None): if ext_filter: - ext_filter = [ext.upper() for ext in ext_filter] + ext_filter = [*map(str.upper, ext_filter)] if ext_blacklist: - ext_blacklist = [ext.upper() for ext in ext_blacklist] + ext_blacklist = [*map(str.upper, ext_blacklist)] def filter(fp:str): return (not ext_filter or any(fp.upper().endswith(ew) for ew in ext_filter)) and (not ext_blacklist or not any(fp.upper().endswith(ew) for ew in ext_blacklist)) return filter @@ -213,9 +278,10 @@ def load_models(model_path: str, model_url: str = None, command_path: str = None @return: A list of paths containing the desired model(s) """ places = unique_directories([model_path, command_path]) + #shared.log.debug(f"{inspect.currentframe().f_code.co_name}: {', '.join(places)}") output = [] try: - output:list = filter_paths(directory_files(*places), filter=extension_filter(ext_filter, ext_blacklist)) + output:list = [*filter(extension_filter(ext_filter, ext_blacklist), directory_files(*places))] if model_url is not None and len(output) == 0: if download_name is not None: from basicsr.utils.download_util import load_file_from_url diff --git a/modules/textual_inversion/textual_inversion.py b/modules/textual_inversion/textual_inversion.py index 845cc1ce3..379a178d1 100644 --- a/modules/textual_inversion/textual_inversion.py +++ b/modules/textual_inversion/textual_inversion.py @@ -13,7 +13,7 @@ import modules.textual_inversion.dataset from modules.textual_inversion.learn_schedule import LearnRateScheduler from modules.textual_inversion.image_embedding import embedding_to_b64, embedding_from_b64, insert_image_data_embed, extract_image_data_embed, caption_image_overlay from modules.textual_inversion.logging import save_settings_to_file -from modules.modelloader import filter_paths, directory_files, extension_filter, directory_mtime +from modules.modelloader import directory_files, extension_filter, directory_mtime TextualInversionTemplate = namedtuple("TextualInversionTemplate", ["name", "path"]) textual_inversion_templates = {} @@ -215,11 +215,11 @@ class EmbeddingDatabase: def load_from_dir(self, embdir): if not os.path.isdir(embdir.path): return - + is_ext = extension_filter(['.PNG', '.WEBP', '.JXL', '.AVIF', '.BIN', '.PT', '.SAFETENSORS']) is_not_preview = lambda fp: not next(iter(os.path.splitext(fp))).upper().endswith('.PREVIEW') - for file_path in filter_paths(directory_files(embdir.path), filter=lambda fp: is_ext(fp) and is_not_preview(fp)): + for file_path in [*filter(lambda fp: is_ext(fp) and is_not_preview(fp), directory_files(embdir.path))]: try: if os.stat(file_path).st_size == 0: continue diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 4240da4e4..7e42e453d 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -264,11 +264,11 @@ class ExtraNetworksPage: preview_extensions = ["jpg", "jpeg", "png", "webp", "tiff", "jp2"] dir = os.path.dirname(path) paths = modelloader.directory_directories(dir, recursive=False) - for file in sum([[f'{path}.thumb.{ext}'] for ext in preview_extensions], []): # use thumbnail if exists - if file in paths[dir][1]: + for file in [f'{path}.thumb.{ext}' for ext in preview_extensions]: # use thumbnail if exists + if file in paths[dir][1] and os.path.exists(file): return self.link_preview(file) - for file in sum([[f'{path}.preview.{ext}', f'{path}.{ext}'] for ext in preview_extensions], []): - if file in paths[dir][1]: + for file in [f'{path}{mid}{ext}' for ext in preview_extensions for mid in ['.preview.', '.']]: + if file in paths[dir][1] and os.path.exists(file): self.missing_thumbs.append(file) return self.link_preview(file) return self.link_preview('html/card-no-preview.png') @@ -363,6 +363,7 @@ def create_ui(container, button, tabname, skip_indexing = False): ui.description_target_filename = gr.Textbox('Description save filename', elem_id=tabname+"_description_filename", visible=False) for page in ui.stored_extra_pages: + shared.log.debug(f"Create UI Extra Network Page: {page.title}") page_html = page.create_html(ui.tabname, skip_indexing) with gr.Tab(page.title, id=page.title.lower().replace(" ", "_"), elem_classes="extra-networks-tab"): page_elem = gr.HTML(page_html, elem_id=tabname+page.name+"_extra_page", elem_classes="extra-networks-page") @@ -378,6 +379,7 @@ def create_ui(container, button, tabname, skip_indexing = False): button_close.click(fn=toggle_visibility, inputs=[state_visible], outputs=[state_visible, container]) def refresh(): + shared.log.debug("Refreshing UI Extra Networks Pages") res = [] for pg in ui.stored_extra_pages: pg.html = '' From 20ba9aa649801344ccc56658e8ab2d1de479faa6 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 15 Aug 2023 08:29:04 +0200 Subject: [PATCH 09/13] update lint --- CHANGELOG.md | 8 ++++++++ modules/modelloader.py | 42 +++++++++++++++++++----------------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2364c528..a6e47d840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ # Change Log for SD.Next +## Update for 2023-08-18 + +- general: + - caching of extra network information to enable much faster create/refresh operations + thanks @midcoastal + ## Update for 2023-08-17 +Smaller update, but with some breaking changes (to prepare for future larger functionality)... + - general: - update all metadata saved with images see for details diff --git a/modules/modelloader.py b/modules/modelloader.py index c37917dd8..aa996eddf 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -1,4 +1,5 @@ import os +import time import shutil import importlib from typing import Dict @@ -6,15 +7,11 @@ from urllib.parse import urlparse from modules import shared from modules.upscaler import Upscaler, UpscalerLanczos, UpscalerNearest, UpscalerNone from modules.paths import script_path, models_path -import inspect -import time diffuser_repos = [] def walk(top, onerror:callable=None): - # A near-exact copy of `os.path.walk()`, trimmed slightly. - # Probably not nessesary for most people's collections, but makes - # a difference on really large datasets. + # A near-exact copy of `os.path.walk()`, trimmed slightly. Probably not nessesary for most people's collections, but makes a difference on really large datasets. nondirs = [] walk_dirs = [] @@ -36,12 +33,10 @@ def walk(top, onerror:callable=None): if onerror is not None: onerror(error, top) return - try: is_dir = entry.is_dir() except OSError: is_dir = False - if not is_dir: nondirs.append(entry.name) else: @@ -52,7 +47,6 @@ def walk(top, onerror:callable=None): except OSError as error: if onerror is not None: onerror(error, entry.path) - # Recurse into sub-directories for new_path in walk_dirs: yield from walk(new_path, onerror) @@ -105,7 +99,6 @@ def download_civit_model(model_url: str, model_name: str, model_path: str, previ def download_diffusers_model(hub_id: str, cache_dir: str = None, download_config: Dict[str, str] = None, token = None, variant = None, revision = None, mirror = None): from diffusers import DiffusionPipeline import huggingface_hub as hf - shared.state.begin() shared.state.job = 'downloload model' if download_config is None: @@ -170,7 +163,6 @@ def load_diffusers_models(model_path: str, command_path: str = None): def find_diffuser(name: str): import huggingface_hub as hf - if name in diffuser_repos: return name if shared.cmd_opts.no_download: @@ -187,11 +179,13 @@ def find_diffuser(name: str): return models[0].modelId return None + modelloader_directories = {} cache_last = 0 cache_time = 1 -def directory_has_changed(dir:str, *, recursive:bool=True) -> bool: + +def directory_has_changed(dir:str, *, recursive:bool=True) -> bool: # pylint: disable=redefined-builtin try: dir = os.path.abspath(dir) if dir not in modelloader_directories: @@ -207,10 +201,10 @@ def directory_has_changed(dir:str, *, recursive:bool=True) -> bool: except Exception as e: shared.log.error(f"Filesystem Error: {e.__class__.__name__}({e})") return True - return False -def directory_directories(dir:str, *, recursive:bool=True) -> dict[str,tuple[float,list[str]]]: + +def directory_directories(dir:str, *, recursive:bool=True) -> dict[str,tuple[float,list[str]]]: # pylint: disable=redefined-builtin dir = os.path.abspath(dir) if directory_has_changed(dir, recursive=recursive): for _dir in modelloader_directories: @@ -228,21 +222,23 @@ def directory_directories(dir:str, *, recursive:bool=True) -> dict[str,tuple[flo except Exception as e: shared.log.error(f"Filesystem Error: {e.__class__.__name__}({e})") del modelloader_directories[_dir] - - directory_directories = {} + res = {} for _dir in modelloader_directories: if _dir == dir or (recursive and _dir.startswith(dir)): - directory_directories[_dir] = modelloader_directories[_dir] + res[_dir] = modelloader_directories[_dir] if not recursive: break - return directory_directories + return res -def directory_mtime(dir:str, *, recursive:bool=True) -> float: + +def directory_mtime(dir:str, *, recursive:bool=True) -> float: # pylint: disable=redefined-builtin return float(max(0, *[mtime for mtime, _ in directory_directories(dir, recursive=recursive).values()])) + def directories_file_paths(directories:dict) -> list[str]: return sum([dat[1] for dat in directories.values()],[]) + def unique_directories(directories:list[str], *, recursive:bool=True) -> list[str]: '''Ensure no empty, or duplicates''' directories = { os.path.abspath(dir): True for dir in directories if dir }.keys() @@ -251,25 +247,28 @@ def unique_directories(directories:list[str], *, recursive:bool=True) -> list[st directories = [dir for dir in directories if not any(_dir != dir and dir.startswith(os.path.join(_dir,'')) for _dir in directories)] return directories + def unique_paths(paths:list[str]) -> list[str]: return { fp: True for fp in paths }.keys() + def directory_files(*directories:list[str], recursive:bool=True) -> list[str]: return unique_paths(sum([[*directories_file_paths(directory_directories(dir, recursive=recursive))] for dir in unique_directories(directories, recursive=recursive)],[])) + def extension_filter(ext_filter=None, ext_blacklist=None): if ext_filter: ext_filter = [*map(str.upper, ext_filter)] if ext_blacklist: ext_blacklist = [*map(str.upper, ext_blacklist)] - def filter(fp:str): + def filter(fp:str): # pylint: disable=redefined-builtin return (not ext_filter or any(fp.upper().endswith(ew) for ew in ext_filter)) and (not ext_blacklist or not any(fp.upper().endswith(ew) for ew in ext_blacklist)) return filter + def load_models(model_path: str, model_url: str = None, command_path: str = None, ext_filter=None, download_name=None, ext_blacklist=None) -> list: """ A one-and done loader to try finding the desired models in specified directories. - @param download_name: Specify to download from model_url immediately. @param model_url: If no other models are found, this will be downloaded on upscale. @param model_path: The location to store/find models in. @@ -367,7 +366,6 @@ def load_upscalers(): importlib.import_module(full_model) except Exception: pass - datas = [] commandline_options = vars(shared.cmd_opts) # some of upscaler classes will not go away after reloading their modules, and we'll end up with two copies of those classes. The newest copy will always be the last in the list, so we go from end to beginning and ignore duplicates @@ -376,7 +374,6 @@ def load_upscalers(): classname = str(cls) if classname not in used_classes: used_classes[classname] = cls - for cls in reversed(used_classes.values()): name = cls.__name__ cmd_name = f"{name.lower().replace('upscaler', '')}_models_path" @@ -385,7 +382,6 @@ def load_upscalers(): scaler.user_path = commandline_model_path scaler.model_download_path = commandline_model_path or scaler.model_path datas += scaler.scalers - shared.sd_upscalers = sorted( datas, # Special case for UpscalerNone keeps it at the beginning of the list. From 5eac99d3f5fe80b546eb30f4cdd88288d68fbffe Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 18 Aug 2023 20:41:34 +0000 Subject: [PATCH 10/13] optimize diffusers memory handling --- CHANGELOG.md | 2 ++ installer.py | 27 ++++++------------ modules/modelloader.py | 6 ++-- modules/processing_diffusers.py | 13 ++++----- modules/sd_models.py | 21 ++++++++++---- modules/sd_vae.py | 10 +++++-- modules/shared.py | 8 +++--- modules/ui_extra_networks.py | 35 +++++++----------------- modules/ui_extra_networks_checkpoints.py | 2 ++ requirements.txt | 2 +- wiki | 2 +- 11 files changed, 59 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6e47d840..d87f6fcd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - general: - caching of extra network information to enable much faster create/refresh operations thanks @midcoastal +- diffusers: + - redo "move model to cpu" logic to be more reliable ## Update for 2023-08-17 diff --git a/installer.py b/installer.py index 576f87018..d24610e00 100644 --- a/installer.py +++ b/installer.py @@ -288,12 +288,9 @@ def check_python(): log.debug(f'Git {git_version.replace("git version", "").strip()}') -# Intel hasn't released a corresponding torchvision wheel along with torch and -# ipex wheels, so we have to install official pytorch torchvision as a W/A. -# However, the latest torchvision explicitly requires torch version == 2.0.1, -# which is incompatible with the Intel torch version 2.0.0a0. This will cause -# intel torch to be uninstalled when pip scans the dependencies of torchvision. -# This function will check the torch version and force installing Intel torch +# Intel hasn't released a corresponding torchvision wheel along with torch and ipex wheels, so we have to install official pytorch torchvision as a W/A. +# However, the latest torchvision explicitly requires torch version == 2.0.1, which is incompatible with the Intel torch version 2.0.0a0. This will cause +# intel torch to be uninstalled when pip scans the dependencies of torchvision. This function will check the torch version and force installing Intel torch # 2.0.0a0 to avoid the underlying dll version error. # TODO(Disty or Nuullll) remove this W/A when Intel releases torchvision wheel for windows. def fix_ipex_win_torch(): @@ -306,8 +303,8 @@ def fix_ipex_win_torch(): log.warning(f'Incompatible torch version {installed_torch_ver} for ipex windows, reinstalling to {ipex_torch_ver}') torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.0.0a0 intel_extension_for_pytorch==2.0.110+gitba7f6c1 -f https://developer.intel.com/ipex-whl-stable-xpu') install(torch_command) - import torch - import intel_extension_for_pytorch as ipex + import torch # pylint: disable=unused-import + import intel_extension_for_pytorch as ipex # pylint: disable=unused-import except Exception as e: log.warning(e) @@ -340,7 +337,6 @@ def check_torch(): log.info('AMD ROCm toolkit detected') os.environ.setdefault('PYTORCH_HIP_ALLOC_CONF', 'garbage_collection_threshold:0.8,max_split_size_mb:512') os.environ.setdefault('TENSORFLOW_PACKAGE', 'tensorflow-rocm') - try: command = subprocess.run('rocm_agent_enumerator', shell=True, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) amd_gpus = command.stdout.decode(encoding="utf8", errors="ignore").split('\n') @@ -350,31 +346,26 @@ def check_torch(): log.debug(f'Run rocm_agent_enumerator failed: {e}') amd_gpus = [] - # use the first available amd gpu by default - hip_visible_devices = [] + hip_visible_devices = [] # use the first available amd gpu by default for idx, gpu in enumerate(amd_gpus): if gpu in ['gfx1100', 'gfx1101', 'gfx1102']: hip_visible_devices.append((idx, gpu, 'navi3x')) break - # experimental navi 2x support - if gpu in ['gfx1030', 'gfx1031', 'gfx1032', 'gfx1034']: + if gpu in ['gfx1030', 'gfx1031', 'gfx1032', 'gfx1034']: # experimental navi 2x support hip_visible_devices.append((idx, gpu, 'navi2x')) break if len(hip_visible_devices) > 0: idx, gpu, arch = hip_visible_devices[0] log.debug(f'ROCm agent used by default: idx={idx} gpu={gpu} arch={arch}') - os.environ.setdefault('HIP_VISIBLE_DEVICES', str(idx)) if arch == 'navi3x': os.environ.setdefault('HSA_OVERRIDE_GFX_VERSION', '11.0.0') - # do not use tensorflow-rocm for navi 3x - if os.environ.get('TENSORFLOW_PACKAGE') == 'tensorflow-rocm': + if os.environ.get('TENSORFLOW_PACKAGE') == 'tensorflow-rocm': # do not use tensorflow-rocm for navi 3x os.environ['TENSORFLOW_PACKAGE'] = 'tensorflow==2.13.0' elif arch == 'navi2x': os.environ.setdefault('HSA_OVERRIDE_GFX_VERSION', '10.3.0') else: log.debug(f'HSA_OVERRIDE_GFX_VERSION auto config is skipped for {gpu}') - try: command = subprocess.run('hipconfig --version', shell=True, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) major_ver, minor_ver, *_ = command.stdout.decode(encoding="utf8", errors="ignore").split('.') @@ -383,13 +374,11 @@ def check_torch(): except Exception as e: log.debug(f'Run hipconfig failed: {e}') rocm_ver = None - if rocm_ver in ['5.5', '5.6']: # install torch nightly via torchvision to avoid wasting bandwidth when torchvision depends on torch from yesterday torch_command = os.environ.get('TORCH_COMMAND', f'torchvision --pre --index-url https://download.pytorch.org/whl/nightly/rocm{rocm_ver}') else: torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/rocm5.4.2') - xformers_package = os.environ.get('XFORMERS_PACKAGE', 'none') elif allow_ipex and (args.use_ipex or shutil.which('sycl-ls') is not None or shutil.which('sycl-ls.exe') is not None or os.environ.get('ONEAPI_ROOT') is not None or os.path.exists('/opt/intel/oneapi') or os.path.exists("C:/Program Files (x86)/Intel/oneAPI") or os.path.exists("C:/oneAPI")): args.use_ipex = True # pylint: disable=attribute-defined-outside-init diff --git a/modules/modelloader.py b/modules/modelloader.py index aa996eddf..5430f6364 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -14,14 +14,12 @@ def walk(top, onerror:callable=None): # A near-exact copy of `os.path.walk()`, trimmed slightly. Probably not nessesary for most people's collections, but makes a difference on really large datasets. nondirs = [] walk_dirs = [] - try: scandir_it = os.scandir(top) except OSError as error: if onerror is not None: onerror(error, top) return - with scandir_it: while True: try: @@ -49,6 +47,8 @@ def walk(top, onerror:callable=None): onerror(error, entry.path) # Recurse into sub-directories for new_path in walk_dirs: + if os.path.basename(new_path).startswith('models--'): + continue yield from walk(new_path, onerror) # Yield after recursion if going bottom up yield top, nondirs @@ -214,7 +214,7 @@ def directory_directories(dir:str, *, recursive:bool=True) -> dict[str,tuple[flo except Exception: pass del modelloader_directories[_dir] - for _dir, _files in walk(dir, lambda e, path: shared.log.error(f"Filesystem Walk Error: {e.__class__.__name__}({e}) -> {path}")): + for _dir, _files in walk(dir, lambda e, path: shared.log.debug(f"FS walk error: {e} {path}")): try: mtime = os.path.getmtime(_dir) if _dir not in modelloader_directories or mtime != modelloader_directories[_dir][0]: diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index fa14a480e..8d77a3623 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -51,6 +51,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro unet_device = model.unet.device model.unet.to(devices.cpu) devices.torch_gc() + model.vae.to(devices.device) latents.to(model.vae.device) decoded = model.vae.decode(latents / model.vae.config.scaling_factor, return_dict=False)[0] if shared.opts.diffusers_move_unet and not model.has_accelerate: @@ -117,13 +118,9 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro negative_pooled = None prompts, negative_prompts, prompts_2, negative_prompts_2 = fix_prompts(prompts, negative_prompts, prompts_2, negative_prompts_2) if shared.opts.prompt_attention in {'Compel parser', 'Full parser'}: - prompt_embed, pooled, negative_embed, negative_pooled = prompt_parser_diffusers.compel_encode_prompts(model, - prompts, - negative_prompts, - prompts_2, - negative_prompts_2, - is_refiner, - kwargs.pop("clip_skip", None)) + prompt_embed, pooled, negative_embed, negative_pooled = prompt_parser_diffusers.compel_encode_prompts(model, prompts, negative_prompts, + prompts_2, negative_prompts_2, + is_refiner, kwargs.pop("clip_skip", None)) if 'prompt' in possible: if hasattr(model, 'text_encoder') and 'prompt_embeds' in possible and prompt_embed is not None: args['prompt_embeds'] = prompt_embed @@ -261,7 +258,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro for i in range(len(decoded)): images.save_image(decoded[i], path=p.outpath_samples, basename="", seed=seeds[i], prompt=prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-refiner") - if (shared.opts.diffusers_move_base or shared.cmd_opts.medvram or shared.opts.diffusers_model_cpu_offload) and not (shared.cmd_opts.lowvram or shared.opts.diffusers_seq_cpu_offload): + if shared.opts.diffusers_move_base and not shared.sd_model.has_accelerate: shared.log.debug('Diffusers: Moving base model to CPU') shared.sd_model.to(devices.cpu) devices.torch_gc() diff --git a/modules/sd_models.py b/modules/sd_models.py index 03325d4a9..9bf65d77e 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -696,7 +696,7 @@ def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=No if (shared.opts.diffusers_model_cpu_offload or shared.cmd_opts.medvram) and (shared.opts.diffusers_seq_cpu_offload or shared.cmd_opts.lowvram): shared.log.warning(f'Diffusers {op}: Model CPU offload (--medvram) and Sequential CPU offload (--lowvram) are not compatible') - shared.log.debug(f'Diffusers {op}: disable model CPU offload and --medvram') + shared.log.debug(f'Diffusers {op}: disabling model CPU offload and --medvram') shared.opts.diffusers_model_cpu_offload=False shared.cmd_opts.medvram=False @@ -706,11 +706,21 @@ def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=No if hasattr(sd_model, "enable_model_cpu_offload"): if (shared.cmd_opts.medvram and devices.backend != "directml") or shared.opts.diffusers_model_cpu_offload: shared.log.debug(f'Diffusers {op}: enable model CPU offload') + if shared.opts.diffusers_move_base or shared.opts.diffusers_move_unet or shared.opts.diffusers_move_refiner: + shared.opts.diffusers_move_base = False + shared.opts.diffusers_move_unet = False + shared.opts.diffusers_move_refiner = False + shared.log.warning(f'Disabling {op} "Move model to CPU" since "Model CPU offload" is enabled') sd_model.enable_model_cpu_offload() sd_model.has_accelerate = True if hasattr(sd_model, "enable_sequential_cpu_offload"): if shared.cmd_opts.lowvram or shared.opts.diffusers_seq_cpu_offload: shared.log.debug(f'Diffusers {op}: enable sequential CPU offload') + if shared.opts.diffusers_move_base or shared.opts.diffusers_move_unet or shared.opts.diffusers_move_refiner: + shared.opts.diffusers_move_base = False + shared.opts.diffusers_move_unet = False + shared.opts.diffusers_move_refiner = False + shared.log.warning(f'Disabling {op} "Move model to CPU" since "Sequential CPU offload" is enabled') sd_model.enable_sequential_cpu_offload(device=devices.device) sd_model.has_accelerate = True if hasattr(sd_model, "enable_vae_slicing"): @@ -760,10 +770,11 @@ def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=No else: if not refiner_enough_vram and not (shared.opts.diffusers_move_base and shared.opts.diffusers_move_refiner): shared.log.warning(f"Insufficient GPU memory, using system memory as fallback: free={free_vram} GB") - shared.log.debug('Enabled moving base model to CPU') - shared.log.debug('Enabled moving refiner model to CPU') - shared.opts.diffusers_move_base=True - shared.opts.diffusers_move_refiner=True + if not shared.opts.shared.opts.diffusers_seq_cpu_offload and not shared.opts.diffusers_model_cpu_offload: + shared.log.debug('Enabled moving base model to CPU') + shared.log.debug('Enabled moving refiner model to CPU') + shared.opts.diffusers_move_base=True + shared.opts.diffusers_move_refiner=True shared.log.debug('Moving base model to CPU') model_data.sd_model.to(devices.cpu) devices.torch_gc(force=True) diff --git a/modules/sd_vae.py b/modules/sd_vae.py index 317906001..07f3a6600 100644 --- a/modules/sd_vae.py +++ b/modules/sd_vae.py @@ -124,6 +124,9 @@ def resolve_vae(checkpoint_file): return vae_dict[basename], 'in VAE dir' else: vae_from_options = vae_dict.get(shared.opts.sd_vae, None) # 5th + if vae_from_options is not None: + return vae_from_options, 'specified in settings' + vae_from_options = vae_dict.get(shared.opts.sd_vae + '.safetensors', None) # 6th if vae_from_options is not None: return vae_from_options, 'specified in settings' shared.log.warning(f"VAE not found: {shared.opts.sd_vae}") @@ -188,10 +191,8 @@ def load_vae_diffusers(model_file, vae_file=None, vae_source="from unknown sourc pass else: diffusers_load_config['variant'] = shared.opts.diffusers_vae_load_variant - if shared.opts.diffusers_vae_upcast != 'default': diffusers_load_config['force_upcast'] = True if shared.opts.diffusers_vae_upcast == 'true' else False - shared.log.debug(f'Diffusers VAE load config: {diffusers_load_config}') try: import diffusers @@ -251,8 +252,11 @@ def reload_vae_weights(sd_model=None, vae_file=unspecified): load_vae(sd_model, vae_file, vae_source) sd_hijack.model_hijack.hijack(sd_model) script_callbacks.model_loaded_callback(sd_model) + if vae_file is not None: + shared.log.info(f"VAE weights loaded: {vae_file}") + # else: + # load_vae_diffusers(model_file, vae_file, vae_source) if not shared.cmd_opts.lowvram and not shared.cmd_opts.medvram and not sd_model.has_accelerate: sd_model.to(devices.device) - shared.log.info(f"VAE weights loaded: {vae_file}") return sd_model diff --git a/modules/shared.py b/modules/shared.py index b1fcb7de4..be39df145 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -268,7 +268,7 @@ def list_themes(): def disable_extensions(): - if opts.lora_disable: + if opts.lyco_patch_lora: if 'Lora' not in opts.disabled_extensions: opts.data['disabled_extensions'].append('Lora') else: @@ -403,8 +403,8 @@ options_templates.update(options_section(('diffusers', "Diffusers Settings"), { "diffusers_move_refiner": OptionInfo(True, "Move refiner model to CPU when not in use"), "diffusers_extract_ema": OptionInfo(True, "Use model EMA weights when possible"), "diffusers_generator_device": OptionInfo("default", "Generator device", gr.Radio, lambda: {"choices": ["default", "cpu"]}), - "diffusers_seq_cpu_offload": OptionInfo(False, "Enable sequential CPU offload"), - "diffusers_model_cpu_offload": OptionInfo(False, "Enable model CPU offload"), + "diffusers_model_cpu_offload": OptionInfo(False, "Enable model CPU offload (--medvram)"), + "diffusers_seq_cpu_offload": OptionInfo(False, "Enable sequential CPU offload (--lowvram)"), "diffusers_vae_upcast": OptionInfo("default", "VAE upcasting", gr.Radio, lambda: {"choices": ['default', 'true', 'false']}), "diffusers_vae_slicing": OptionInfo(True, "Enable VAE slicing"), "diffusers_vae_tiling": OptionInfo(False, "Enable VAE tiling"), @@ -627,7 +627,7 @@ options_templates.update(options_section(('extra_networks', "Extra Networks"), { "extra_networks_card_fit": OptionInfo("cover", "UI image contain method", gr.Radio, lambda: {"choices": ["contain", "cover", "fill"]}), "extra_network_skip_indexing": OptionInfo(False, "Do not automatically build extra network pages", gr.Checkbox), "lyco_patch_lora": OptionInfo(False, "Use LyCoris handler for all Lora types", gr.Checkbox), - "lora_disable": OptionInfo(False, "Disable built-in Lora handler", gr.Checkbox, { "visible": True }, onchange=disable_extensions), + # "lora_disable": OptionInfo(False, "Disable built-in Lora handler", gr.Checkbox, { "visible": True }, onchange=disable_extensions), "lora_functional": OptionInfo(False, "Use Kohya method for handling multiple Loras", gr.Checkbox), "extra_networks_add_text_separator": OptionInfo(" ", "Extra text to add before <...> when adding extra network to prompt", gr.Text, { "visible": False }), "extra_networks_default_multiplier": OptionInfo(1.0, "Multiplier for extra networks", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}), diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 7e42e453d..3ef1937b1 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -11,9 +11,6 @@ from PIL import Image from modules import shared, scripts, modelloader from modules.generation_parameters_copypaste import image_from_url_text from modules.ui_components import ToolButton -from logging import DEBUG -from time import time -from rich.progress import Progress, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn, TimeElapsedColumn, SpinnerColumn extra_pages = [] allowed_dirs = set() @@ -161,8 +158,8 @@ class ExtraNetworksPage: return f"
Extra network page not ready
Click refresh to try again
" subdirs = {} allowed_folders = [os.path.abspath(x) for x in self.allowed_directories_for_previews()] - for parentdir, dirs in {dir: modelloader.directory_directories(dir) for dir in allowed_folders}.items(): - for dir in dirs.keys(): + for parentdir, dirs in {dir: modelloader.directory_directories(dir) for dir in allowed_folders}.items(): # pylint: disable=redefined-builtin + for dir in dirs.keys(): # pylint: disable=redefined-builtin if shared.opts.diffusers_dir in dir: subdirs[os.path.basename(shared.opts.diffusers_dir)] = 1 if 'models--' in dir: @@ -191,23 +188,11 @@ class ExtraNetworksPage: shared.log.error(f'Extra networks error listing items: {self.__class__}') self.create_xyz_grid() htmls = [] - with Progress( - SpinnerColumn(), - TextColumn('[cyan]Creating Extra Network '+self.title+' HTML - {task.description}'), - BarColumn(), TaskProgressColumn(), TextColumn('({task.completed}/{task.total})'), - TimeRemainingColumn(), TimeElapsedColumn(), transient=not shared.log.isEnabledFor(DEBUG), expand=True - ) as progress: - task = progress.add_task(description='Initializing Items') - items = self.items - progress.update(task, total=len(items)) - __t = time() - __i = 0 - for item in items: - __i += 1 - self.metadata[item["name"]] = item.get("metadata", {}) - self.info[item["name"]] = self.find_info(item['filename']) - htmls.append(self.create_html_for_item(item, tabname)) - progress.update(task, advance=1, description=f"{round(__i/(shared.time.time()-__t))} item/s") + items = self.items + for item in items: + self.metadata[item["name"]] = item.get("metadata", {}) + self.info[item["name"]] = self.find_info(item['filename']) + htmls.append(self.create_html_for_item(item, tabname)) self.html += ''.join(htmls) if len(subdirs_html) > 0 or len(self.html) > 0: res = f"
{subdirs_html}
{self.html}
" @@ -262,7 +247,7 @@ class ExtraNetworksPage: def find_preview(self, path): preview_extensions = ["jpg", "jpeg", "png", "webp", "tiff", "jp2"] - dir = os.path.dirname(path) + dir = os.path.dirname(path) # pylint: disable=redefined-builtin paths = modelloader.directory_directories(dir, recursive=False) for file in [f'{path}.thumb.{ext}' for ext in preview_extensions]: # use thumbnail if exists if file in paths[dir][1] and os.path.exists(file): @@ -274,7 +259,7 @@ class ExtraNetworksPage: return self.link_preview('html/card-no-preview.png') def find_description(self, path): - dir = os.path.dirname(path) + dir = os.path.dirname(path) # pylint: disable=redefined-builtin paths = modelloader.directory_directories(dir, recursive=False) for file in [f"{path}.txt", f"{path}.description.txt"]: if file in paths[dir][1]: @@ -288,7 +273,7 @@ class ExtraNetworksPage: return None def find_info(self, path): - dir = os.path.dirname(path) + dir = os.path.dirname(path) # pylint: disable=redefined-builtin paths = modelloader.directory_directories(dir, recursive=False) basename, _ext = os.path.splitext(path) for file in [f"{path}.info", f"{path}.civitai.info", f"{basename}.info", f"{basename}.civitai.info"]: diff --git a/modules/ui_extra_networks_checkpoints.py b/modules/ui_extra_networks_checkpoints.py index 37bee332b..43f9c1907 100644 --- a/modules/ui_extra_networks_checkpoints.py +++ b/modules/ui_extra_networks_checkpoints.py @@ -16,6 +16,8 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage): checkpoint: sd_models.CheckpointInfo for name, checkpoint in sd_models.checkpoints_list.items(): path, _ext = os.path.splitext(checkpoint.filename) + if not os.path.exists(path) and sd_models.model_path not in path: + path = os.path.abspath(os.path.join(checkpoint.path, os.pardir, os.pardir)) yield { "name": checkpoint.name_for_extra, "filename": path, diff --git a/requirements.txt b/requirements.txt index 373984093..098ba25a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,7 +46,7 @@ requests==2.31.0 tqdm==4.65.0 accelerate==0.20.3 opencv-python-headless==4.7.0.72 -diffusers==0.19.3 +diffusers==0.20.0 einops==0.4.1 gradio==3.32.0 huggingface_hub==0.16.4 diff --git a/wiki b/wiki index 625f3d53f..fd5c18037 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 625f3d53f99babf329166268b4c4c0c4c209801b +Subproject commit fd5c18037d51aca2e7c7d72f1c0f9182fe9a37ed From 87bb354f4ce452543a0772e20651ff027ed47a43 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 19 Aug 2023 12:25:41 +0000 Subject: [PATCH 11/13] implement hires for diffusers --- CHANGELOG.md | 16 ++- modules/generation_parameters_copypaste.py | 2 +- modules/images.py | 7 +- modules/processing.py | 2 +- modules/processing_diffusers.py | 143 +++++++++++++-------- modules/shared.py | 19 +-- modules/ui.py | 8 +- 7 files changed, 124 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d87f6fcd1..bba953419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,24 @@ # Change Log for SD.Next -## Update for 2023-08-18 +## Update for 2023-08-19 + +Another larger release thats been baking in dev branch for a while... - general: - caching of extra network information to enable much faster create/refresh operations thanks @midcoastal - diffusers: - - redo "move model to cpu" logic to be more reliable + - add **hires** support (*experimental*) + applies to all model types that support img2img, including **sd** and **sd-xl** + also supports all hires upscaler types as well as standard params like steps and denoising strength + when used with **sd-xl**, it can be used with or without refiner loaded + how to enable - there are no explicit checkboxes other than second pass itself: + - hires: upscaler is set and target resolution is not at default + - refiner: if refiner model is loaded + - images save options: *before hires*, *before refiner* + - redo `move model to cpu` logic in settings -> diffusers to be more reliable + note that system defaults have also changed, so you may need to tweak to your liking + - update dependencies ## Update for 2023-08-17 diff --git a/modules/generation_parameters_copypaste.py b/modules/generation_parameters_copypaste.py index e7dd07aa0..922c05997 100644 --- a/modules/generation_parameters_copypaste.py +++ b/modules/generation_parameters_copypaste.py @@ -306,7 +306,7 @@ infotext_to_setting_name_mapping = [ ('Noise multiplier', 'initial_noise_multiplier'), ('Eta', 'eta_ancestral'), ('Eta DDIM', 'eta_ddim'), - ('Lora method', 'diffusers_lora_loader'), + ('LoRA method', 'diffusers_lora_loader'), ('Discard penultimate sigma', 'always_discard_next_to_last_sigma'), ('UniPC variant', 'uni_pc_variant'), ('UniPC skip type', 'uni_pc_skip_type'), diff --git a/modules/images.py b/modules/images.py index fdcbac1de..eac7067fb 100644 --- a/modules/images.py +++ b/modules/images.py @@ -209,9 +209,10 @@ def resize_image(resize_mode, im, width, height, upscaler_name=None): Resizes an image with the specified resize_mode, width, and height. Args: resize_mode: The mode to use when resizing the image. - 0: Resize the image to the specified width and height. - 1: Resize the image to fill the specified width and height, maintaining the aspect ratio, and then center the image within the dimensions, cropping the excess. - 2: Resize the image to fit within the specified width and height, maintaining the aspect ratio, and then center the image within the dimensions, filling empty with data from image. + 0: No resie + 1: Resize the image to the specified width and height. + 2: Resize the image to fill the specified width and height, maintaining the aspect ratio, and then center the image within the dimensions, cropping the excess. + 3: Resize the image to fit within the specified width and height, maintaining the aspect ratio, and then center the image within the dimensions, filling empty with data from image. im: The image to resize. width: The width to resize the image to. height: The height to resize the image to. diff --git a/modules/processing.py b/modules/processing.py index 9c7964ac8..1260e83b0 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -711,7 +711,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: else: raise ValueError(f"Unknown backend {shared.backend}") - if shared.cmd_opts.lowvram or shared.cmd_opts.medvram: + if shared.cmd_opts.lowvram or shared.cmd_opts.medvram and shared.backend == shared.Backend.ORIGINAL: lowvram.send_everything_to_cpu() devices.torch_gc() if p.scripts is not None: diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 8d77a3623..652a0d167 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -1,8 +1,6 @@ import inspect import typing import torch -# import numpy as np -# from PIL import Image import modules.devices as devices import modules.shared as shared import modules.sd_samplers as sd_samplers @@ -23,27 +21,38 @@ except Exception as ex: def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_prompts): results = [] + if p.enable_hr and p.hr_upscaler != 'None' and p.denoising_strength > 0 and len(getattr(p, 'init_images', [])) == 0: + p.is_hr_pass = True + is_refiner_enabled = p.enable_hr and shared.sd_refiner is not None - def diffusers_callback(step: int, _timestep: int, latents: torch.FloatTensor): - shared.state.sampling_step = step + def hires_resize(latents): # input=latents output=pil + latent_upscaler = shared.latent_upscale_modes.get(p.hr_upscaler, None) + shared.log.info(f'Diffusers Hires: upscaler={p.hr_upscaler} width={p.hr_upscale_to_x} height={p.hr_upscale_to_y} images={latents.shape[0]}') + if latent_upscaler is not None: + latents = torch.nn.functional.interpolate(latents, size=(p.hr_upscale_to_y // 8, p.hr_upscale_to_x // 8), mode=latent_upscaler["mode"], antialias=latent_upscaler["antialias"]) + first_pass_images = vae_decode(latents=latents, model=shared.sd_model, full_quality=True, output_type='pil') + p.init_images = [] + for first_pass_image in first_pass_images: + init_image = images.resize_image(1, first_pass_image, p.hr_upscale_to_x, p.hr_upscale_to_y, upscaler_name=p.hr_upscaler) if latent_upscaler is None else first_pass_image + p.init_images.append(init_image) + p.width = p.hr_upscale_to_x + p.height = p.hr_upscale_to_y + + def save_intermediate(latents, suffix): + for i in range(len(latents)): + from modules.processing import create_infotext + info=create_infotext(p, p.all_prompts, p.all_seeds, p.all_subseeds, [], iteration=p.iteration, position_in_batch=i) + decoded = vae_decode(latents=latents, model=shared.sd_model, output_type='pil', full_quality=p.full_quality) + for i in range(len(decoded)): + images.save_image(decoded[i], path=p.outpath_samples, basename="", seed=seeds[i], prompt=prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix=suffix) + + def diffusers_callback(_step: int, _timestep: int, latents: torch.FloatTensor): + shared.state.sampling_step += 1 shared.state.sampling_steps = p.steps + if p.is_hr_pass: + shared.state.sampling_steps += p.hr_second_pass_steps shared.state.current_latent = latents - def hires_resize(latents): - return latents # TODO finish hires - if p.hr_upscaler == 'None': - return latents - scale = shared.latent_upscale_modes.get(p.hr_upscaler, None) - if scale is not None: - p.init_hr() - p.ops.append('hires') - shared.log.info(f'Diffusers Hires: upscaler={p.hr_upscaler} mode={scale["mode"]} antialias={scale["antialias"]} width={p.hr_upscale_to_x} height={p.hr_upscale_to_y} images={latents.shape[0]}') - hires_image = torch.nn.functional.interpolate(latents, size=(p.hr_upscale_to_y // 8, p.hr_upscale_to_x // 8), mode=scale["mode"], antialias=scale["antialias"]) - else: - shared.log.warning(f'Diffusers hires unsupported: upscaler={p.hr_upscaler} supported=latent modes') - hires_image = latents - return hires_image - def full_vae_decode(latents, model): shared.log.debug(f'Diffusers VAE decode: name={sd_vae.loaded_vae_file if sd_vae.loaded_vae_file is not None else "baked"} dtype={model.vae.dtype} upcast={model.vae.config.get("force_upcast", None)} images={latents.shape[0]}') if shared.opts.diffusers_move_unet and not model.has_accelerate: @@ -66,19 +75,18 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro return decoded def vae_decode(latents, model, output_type='np', full_quality=True): + if not torch.is_tensor(latents): # already decoded + return latents + if latents.shape[0] == 0: + shared.log.error(f'VAE nothing to decode: {latents.shape}') + return [] if shared.state.interrupted or shared.state.skipped: return [] if not hasattr(model, 'vae'): shared.log.error('VAE not found in model') return [] - if not torch.is_tensor(latents): - shared.log.error(f'VAE input is not latents: {type(latents)}') - return [] - if latents.shape[0] == 0: - shared.log.error(f'VAE nothing to decode: {latents.shape}') - return [] - if p.enable_hr: - latents = hires_resize(latents=latents) + if len(latents.shape) == 3: # lost a batch dim in hires + latents = latents.unsqueeze(0) if full_quality: decoded = full_vae_decode(latents=latents, model=shared.sd_model) else: @@ -105,7 +113,9 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro negative_prompts_2.append(negative_prompts_2[-1]) return prompts, negative_prompts, prompts_2, negative_prompts_2 - def set_pipeline_args(model, prompts: list, negative_prompts: list, prompts_2: typing.Optional[list]=None, negative_prompts_2: typing.Optional[list]=None, is_refiner: bool=False, **kwargs): + def set_pipeline_args(model, prompts: list, negative_prompts: list, prompts_2: typing.Optional[list]=None, negative_prompts_2: typing.Optional[list]=None, is_refiner: bool=False, desc:str='', **kwargs): + if hasattr(model, "set_progress_bar_config"): + model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} '+desc, ncols=80, colour='#327fba') args = {} pipeline = model signature = inspect.signature(type(pipeline).__call__) @@ -138,7 +148,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro else: args['negative_prompt'] = negative_prompts if 'num_inference_steps' in possible: - args['num_inference_steps'] = p.steps + args['num_inference_steps'] = p.steps if not p.is_hr_pass else p.hr_second_pass_steps if 'guidance_scale' in possible: args['guidance_scale'] = p.cfg_scale if 'generator' in possible: @@ -182,8 +192,9 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro return args is_karras_compatible = shared.sd_model.__class__.__init__.__annotations__.get("scheduler", None) == diffusers.schedulers.scheduling_utils.KarrasDiffusionSchedulers - if (not hasattr(shared.sd_model.scheduler, 'name')) or (shared.sd_model.scheduler.name != p.sampler_name) and (p.sampler_name != 'Default') and is_karras_compatible: - sampler = sd_samplers.all_samplers_map.get(p.sampler_name, None) + use_sampler = p.sampler_name if not p.is_hr_pass else p.latent_sampler + if (not hasattr(shared.sd_model.scheduler, 'name')) or (shared.sd_model.scheduler.name != use_sampler) and (use_sampler != 'Default') and is_karras_compatible: + sampler = sd_samplers.all_samplers_map.get(use_sampler, None) if sampler is None: sampler = sd_samplers.all_samplers_map.get("UniPC") sd_samplers.create_sampler(sampler.name, shared.sd_model) # TODO(Patrick): For wrapped pipelines this is currently a no-op @@ -220,8 +231,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro if shared.opts.diffusers_move_base and not shared.sd_model.has_accelerate: shared.sd_model.to(devices.device) - refiner_enabled = shared.sd_refiner is not None and p.enable_hr - pipe_args = set_pipeline_args( + base_args = set_pipeline_args( model=shared.sd_model, prompts=prompts, negative_prompts=negative_prompts, @@ -229,35 +239,56 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro negative_prompts_2=[p.refiner_negative] if len(p.refiner_negative) > 0 else negative_prompts, eta=shared.opts.eta_ddim, guidance_rescale=p.diffusers_guidance_rescale, - denoising_start=0 if refiner_enabled and p.refiner_start > 0 and p.refiner_start < 1 else None, - denoising_end=p.refiner_start if refiner_enabled and p.refiner_start > 0 and p.refiner_start < 1 else None, + denoising_start=0 if is_refiner_enabled and p.refiner_start > 0 and p.refiner_start < 1 else None, + denoising_end=p.refiner_start if is_refiner_enabled and p.refiner_start > 0 and p.refiner_start < 1 else None, output_type='latent' if hasattr(shared.sd_model, 'vae') else 'np', is_refiner=False, clip_skip=p.clip_skip, + desc='Base', **task_specific_kwargs ) p.extra_generation_params['CFG rescale'] = p.diffusers_guidance_rescale p.extra_generation_params["Eta DDIM"] = shared.opts.eta_ddim if shared.opts.eta_ddim is not None and shared.opts.eta_ddim > 0 else None - output = shared.sd_model(**pipe_args) # pylint: disable=not-callable - if shared.state.interrupted or shared.state.skipped: - unload_diffusers_lora() - return results + output = shared.sd_model(**base_args) # pylint: disable=not-callable if lora_state['active']: - p.extra_generation_params['Lora method'] = shared.opts.diffusers_lora_loader + p.extra_generation_params['LoRA method'] = shared.opts.diffusers_lora_loader unload_diffusers_lora() - if not refiner_enabled: - results = vae_decode(latents=output.images, model=shared.sd_model, full_quality=p.full_quality) - else: - for i in range(len(output.images)): # save images before refiner - if shared.opts.save and not p.do_not_save_samples and shared.opts.save_images_before_refiner and hasattr(shared.sd_model, 'vae'): - from modules.processing import create_infotext - info=create_infotext(p, p.all_prompts, p.all_seeds, p.all_subseeds, [], iteration=p.iteration, position_in_batch=i) - decoded = vae_decode(latents=output.images, model=shared.sd_model, output_type='pil', full_quality=p.full_quality) - for i in range(len(decoded)): - images.save_image(decoded[i], path=p.outpath_samples, basename="", seed=seeds[i], prompt=prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-refiner") + if shared.state.interrupted or shared.state.skipped: + return results + # optional hires pass + if p.is_hr_pass: + p.init_hr() + if p.width != p.hr_upscale_to_x or p.height != p.hr_upscale_to_y: + if shared.opts.save and not p.do_not_save_samples and shared.opts.save_images_before_highres_fix and hasattr(shared.sd_model, 'vae'): + save_intermediate(latents=output.images, suffix="-before-hires") + hires_resize(latents=output.images) + print('HERE', p.init_images) + sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.IMAGE_2_IMAGE) + p.ops.append('hires') + hires_args = set_pipeline_args( + model=shared.sd_model, + prompts=prompts, + negative_prompts=negative_prompts, + prompts_2=[p.refiner_prompt] if len(p.refiner_prompt) > 0 else prompts, + negative_prompts_2=[p.refiner_negative] if len(p.refiner_negative) > 0 else negative_prompts, + eta=shared.opts.eta_ddim, + guidance_rescale=p.diffusers_guidance_rescale, + output_type='latent' if hasattr(shared.sd_model, 'vae') else 'np', + is_refiner=False, + clip_skip=p.clip_skip, + image=p.init_images, + strength=p.denoising_strength, + desc='Hires', + ) + output = shared.sd_model(**hires_args) # pylint: disable=not-callable + + # optional refiner pass or decode + if is_refiner_enabled: + if shared.opts.save and not p.do_not_save_samples and shared.opts.save_images_before_refiner and hasattr(shared.sd_model, 'vae'): + save_intermediate(latents=output.images, suffix="-before-refiner") if shared.opts.diffusers_move_base and not shared.sd_model.has_accelerate: shared.log.debug('Diffusers: Moving base model to CPU') shared.sd_model.to(devices.cpu) @@ -276,7 +307,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro shared.sd_refiner.to(devices.device) p.ops.append('refine') for i in range(len(output.images)): - pipe_args = set_pipeline_args( + refiner_args = set_pipeline_args( model=shared.sd_refiner, prompts=[p.refiner_prompt] if len(p.refiner_prompt) > 0 else prompts[i], negative_prompts=[p.refiner_negative] if len(p.refiner_negative) > 0 else negative_prompts[i], @@ -291,19 +322,25 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro output_type='latent' if hasattr(shared.sd_refiner, 'vae') else 'np', is_refiner=True, clip_skip=p.clip_skip, + desc='Refiner', ) - refiner_output = shared.sd_refiner(**pipe_args) # pylint: disable=not-callable + refiner_output = shared.sd_refiner(**refiner_args) # pylint: disable=not-callable p.extra_generation_params['Image CFG scale'] = p.image_cfg_scale if p.image_cfg_scale is not None else None p.extra_generation_params['Refiner start'] = p.refiner_start p.extra_generation_params["Hires steps"] = p.hr_second_pass_steps if not shared.state.interrupted and not shared.state.skipped: refiner_images = vae_decode(latents=refiner_output.images, model=shared.sd_refiner, full_quality=True) - results.append(refiner_images[0]) + for refiner_image in refiner_images: + results.append(refiner_image) if shared.opts.diffusers_move_refiner and not shared.sd_refiner.has_accelerate: shared.log.debug('Diffusers: Moving refiner model to CPU') shared.sd_refiner.to(devices.cpu) devices.torch_gc() + # final decode since there is no refiner + if not is_refiner_enabled: + results = vae_decode(latents=output.images, model=shared.sd_model, full_quality=p.full_quality) + return results diff --git a/modules/shared.py b/modules/shared.py index be39df145..2290a4c7d 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -268,7 +268,7 @@ def list_themes(): def disable_extensions(): - if opts.lyco_patch_lora: + if opts.lyco_patch_lora and backend != Backend.DIFFUSERS: if 'Lora' not in opts.disabled_extensions: opts.data['disabled_extensions'].append('Lora') else: @@ -398,8 +398,8 @@ options_templates.update(options_section(('cuda', "Compute Settings"), { options_templates.update(options_section(('diffusers', "Diffusers Settings"), { "diffusers_pipeline": OptionInfo(pipelines[0], 'Diffusers pipeline', gr.Dropdown, lambda: {"choices": pipelines}), - "diffusers_move_base": OptionInfo(False, "Move base model to CPU when using refiner"), - "diffusers_move_unet": OptionInfo(False, "Move base model to CPU when using VAE"), + "diffusers_move_base": OptionInfo(True, "Move base model to CPU when using refiner"), + "diffusers_move_unet": OptionInfo(True, "Move base model to CPU when using VAE"), "diffusers_move_refiner": OptionInfo(True, "Move refiner model to CPU when not in use"), "diffusers_extract_ema": OptionInfo(True, "Use model EMA weights when possible"), "diffusers_generator_device": OptionInfo("default", "Generator device", gr.Radio, lambda: {"choices": ["default", "cpu"]}), @@ -407,7 +407,7 @@ options_templates.update(options_section(('diffusers', "Diffusers Settings"), { "diffusers_seq_cpu_offload": OptionInfo(False, "Enable sequential CPU offload (--lowvram)"), "diffusers_vae_upcast": OptionInfo("default", "VAE upcasting", gr.Radio, lambda: {"choices": ['default', 'true', 'false']}), "diffusers_vae_slicing": OptionInfo(True, "Enable VAE slicing"), - "diffusers_vae_tiling": OptionInfo(False, "Enable VAE tiling"), + "diffusers_vae_tiling": OptionInfo(True, "Enable VAE tiling"), "diffusers_attention_slicing": OptionInfo(False, "Enable attention slicing"), "diffusers_model_load_variant": OptionInfo("default", "Diffusers model loading variant", gr.Radio, lambda: {"choices": ['default', 'fp32', 'fp16']}), "diffusers_vae_load_variant": OptionInfo("default", "Diffusers VAE loading variant", gr.Radio, lambda: {"choices": ['default', 'fp32', 'fp16']}), @@ -422,7 +422,7 @@ options_templates.update(options_section(('system-paths', "System Paths"), { "ckpt_dir": OptionInfo(os.path.join(paths.models_path, 'Stable-diffusion'), "Path to directory with stable diffusion checkpoints"), "diffusers_dir": OptionInfo(os.path.join(paths.models_path, 'Diffusers'), "Path to directory with stable diffusion diffusers"), "vae_dir": OptionInfo(os.path.join(paths.models_path, 'VAE'), "Path to directory with VAE files"), - "lora_dir": OptionInfo(os.path.join(paths.models_path, 'Lora'), "Path to directory with Lora network(s)"), + "lora_dir": OptionInfo(os.path.join(paths.models_path, 'Lora'), "Path to directory with LoRA network(s)"), "lyco_dir": OptionInfo(os.path.join(paths.models_path, 'LyCORIS'), "Path to directory with LyCORIS network(s)"), "styles_dir": OptionInfo(os.path.join(paths.data_path, 'styles.csv'), "Path to user-defined styles file"), "embeddings_dir": OptionInfo(os.path.join(paths.models_path, 'embeddings'), "Embeddings directory for textual inversion"), @@ -626,9 +626,9 @@ options_templates.update(options_section(('extra_networks', "Extra Networks"), { "extra_networks_card_square": OptionInfo(True, "UI disable variable aspect ratio"), "extra_networks_card_fit": OptionInfo("cover", "UI image contain method", gr.Radio, lambda: {"choices": ["contain", "cover", "fill"]}), "extra_network_skip_indexing": OptionInfo(False, "Do not automatically build extra network pages", gr.Checkbox), - "lyco_patch_lora": OptionInfo(False, "Use LyCoris handler for all Lora types", gr.Checkbox), + "lyco_patch_lora": OptionInfo(False, "Use LyCoris handler for all LoRA types", gr.Checkbox), # "lora_disable": OptionInfo(False, "Disable built-in Lora handler", gr.Checkbox, { "visible": True }, onchange=disable_extensions), - "lora_functional": OptionInfo(False, "Use Kohya method for handling multiple Loras", gr.Checkbox), + "lora_functional": OptionInfo(False, "Use Kohya method for handling multiple LoRA", gr.Checkbox), "extra_networks_add_text_separator": OptionInfo(" ", "Extra text to add before <...> when adding extra network to prompt", gr.Text, { "visible": False }), "extra_networks_default_multiplier": OptionInfo(1.0, "Multiplier for extra networks", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}), "sd_hypernetwork": OptionInfo("None", "Add hypernetwork to prompt", gr.Dropdown, lambda: {"choices": ["None"] + list(hypernetworks.keys())}, refresh=reload_hypernetworks), @@ -708,10 +708,11 @@ class Options: diff = {} for k, v in self.data.items(): if k in self.data_labels: + if type(v) is list: + diff[k] = v if self.data_labels[k].default != v: diff[k] = v - output = json.dumps(diff, indent=2) - writefile(output, filename) + writefile(diff, filename) except Exception as e: log.error(f'Saving settings failed: {filename} {e}') diff --git a/modules/ui.py b/modules/ui.py index b9a07566d..fe62868ae 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -92,8 +92,8 @@ def calc_resolution_hires(enable, width, height, hr_scale, hr_resize_x, hr_resiz from modules import processing, devices if not enable: return "" - if modules.shared.backend == modules.shared.Backend.DIFFUSERS: - return "Hires resize: disabled" + # if modules.shared.backend == modules.shared.Backend.DIFFUSERS: + # return "Hires resize: disabled" p = processing.StableDiffusionProcessingTxt2Img(width=width, height=height, enable_hr=True, hr_scale=hr_scale, hr_resize_x=hr_resize_x, hr_resize_y=hr_resize_y) p.init_hr() with devices.autocast(): @@ -106,8 +106,8 @@ def resize_from_to_html(width, height, scale_by): target_height = int(height * scale_by) if not target_width or not target_height: return "no image selected" - if modules.shared.backend == modules.shared.Backend.DIFFUSERS: - return "Hires resize: disabled" + # if modules.shared.backend == modules.shared.Backend.DIFFUSERS: + # return "Hires resize: disabled" return f"Hires resize: from {width}x{height} to {target_width}x{target_height}" From 5b7f873fada4b13c18cee06143e92c51e93a0fa5 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Sat, 19 Aug 2023 22:07:10 +0300 Subject: [PATCH 12/13] Fix sequential offloading --- modules/processing_diffusers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 652a0d167..54a709899 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -60,7 +60,8 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro unet_device = model.unet.device model.unet.to(devices.cpu) devices.torch_gc() - model.vae.to(devices.device) + if not shared.cmd_opts.lowvram and not shared.opts.diffusers_seq_cpu_offload: + model.vae.to(devices.device) latents.to(model.vae.device) decoded = model.vae.decode(latents / model.vae.config.scaling_factor, return_dict=False)[0] if shared.opts.diffusers_move_unet and not model.has_accelerate: From eb7916b7fd04c343a507beca81e6042c8d7d2d55 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 20 Aug 2023 13:10:00 +0000 Subject: [PATCH 13/13] update github templates --- .github/ISSUE_TEMPLATE/bug_report.yml | 26 ++++++- .../{config.yml => commuity_support.yml} | 0 .github/ISSUE_TEMPLATE/diffusers_report.yml | 76 ------------------- extensions-builtin/a1111-sd-webui-lycoris | 2 +- extensions-builtin/sd-dynamic-thresholding | 2 +- wiki | 2 +- 6 files changed, 28 insertions(+), 80 deletions(-) rename .github/ISSUE_TEMPLATE/{config.yml => commuity_support.yml} (100%) delete mode 100644 .github/ISSUE_TEMPLATE/diffusers_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b3cc847f4..023fc1005 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -61,6 +61,30 @@ body: value: | If unsure if this is a right place to ask your question, perhaps post on [Discussions](https://github.com/vladmandic/automatic/discussions) Or reach-out to us on [Discord](https://discord.gg/WqMzTUDC) + - type: dropdown + id: backend + attributes: + label: Backend + description: What is the backend you're using? + options: + - Original + - Diffusers + default: 0 + validations: + required: true + - type: dropdown + id: model + attributes: + label: Model + description: What is the model type you're using? + options: + - SD 1.5 + - SD-XL + - Kandinsky + - Other + default: 0 + validations: + required: true - type: checkboxes attributes: label: Acknowledgements @@ -68,5 +92,5 @@ body: options: - label: I have read the above and searched for existing issues required: true - - label: I confirm that this is classified correctly and its not an extension or diffusers-specific issue + - label: I confirm that this is classified correctly and its not an extension issue required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/commuity_support.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yml rename to .github/ISSUE_TEMPLATE/commuity_support.yml diff --git a/.github/ISSUE_TEMPLATE/diffusers_report.yml b/.github/ISSUE_TEMPLATE/diffusers_report.yml deleted file mode 100644 index b18b3ab42..000000000 --- a/.github/ISSUE_TEMPLATE/diffusers_report.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Diffusers Report -description: Something is broken when using Diffusers backend -title: "[Diffusers]: " -labels: [] - -body: - - type: textarea - id: description - attributes: - label: Issue Description - description: Tell us what happened in a very clear and simple way - value: Please fill this form with as much information as possible - - type: textarea - id: pipeline - attributes: - label: Diffusers pipeline used - description: Enter Diffusers pipeline and model used - value: - - type: textarea - id: platform - attributes: - label: Version Platform Description - description: Describe your platform (program version, OS, browser) - value: - - type: markdown - attributes: - value: | - Any issues without version information will be closed - Provide any relevant platorm information: - - Application version, OS details, GPU information, browser used - - Easiest is to include top part of console log, for example: - ```log - Starting SD.Next - Python 3.10.6 on Linux - Version: abd7d160 Sat Jun 10 07:37:42 2023 -0400 - nVidia CUDA toolkit detected - Torch 2.1.0.dev20230519+cu121 - Torch backend: nVidia CUDA 12.1 cuDNN 8801 - Torch detected GPU: NVIDIA GeForce RTX 3060 VRAM 12288 Arch (8, 6) Cores 28 - Enabled extensions-builtin: [...] - Enabled extensions: [...] - ``` - - type: markdown - attributes: - value: | - If issue is setup, installation or startup related, please check `sdnext.log` before reporting - - type: markdown - attributes: - value: | - If you have additional extensions installed, try to reproduce the issue with user extensions disabled - And if the issue is with compatibility with specific extension, mark it as such when creating the issue - Try running with `--safe` command line flag with disables loading of user-installed extensions - - type: markdown - attributes: - value: | - If possible update to latest version before reporting the issue as older versions cannot be properly supported - And search existing **issues** and **discussions** before creating a new one - - type: textarea - id: logs - attributes: - label: Relevant log output - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks - render: shell - - type: markdown - attributes: - value: | - If unsure if this is a right place to ask your question, perhaps post on [Discussions](https://github.com/vladmandic/automatic/discussions) - Or reach-out to us on [Discord](https://discord.gg/WqMzTUDC) - - type: checkboxes - attributes: - label: Acknowledgements - description: - options: - - label: I have read the above and searched for existing issues - required: true diff --git a/extensions-builtin/a1111-sd-webui-lycoris b/extensions-builtin/a1111-sd-webui-lycoris index 8e97bf548..912576970 160000 --- a/extensions-builtin/a1111-sd-webui-lycoris +++ b/extensions-builtin/a1111-sd-webui-lycoris @@ -1 +1 @@ -Subproject commit 8e97bf54867c25d00fc480be1ab4dae5399b35ef +Subproject commit 912576970a9fe55537853e595e7ed4c27a645bc7 diff --git a/extensions-builtin/sd-dynamic-thresholding b/extensions-builtin/sd-dynamic-thresholding index c02d806ca..96238f443 160000 --- a/extensions-builtin/sd-dynamic-thresholding +++ b/extensions-builtin/sd-dynamic-thresholding @@ -1 +1 @@ -Subproject commit c02d806cac2a280bbcc90b586fc37bd560cd3274 +Subproject commit 96238f443ea4df84d211e178562d9264d774a2ae diff --git a/wiki b/wiki index fd5c18037..fe9aaefe7 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit fd5c18037d51aca2e7c7d72f1c0f9182fe9a37ed +Subproject commit fe9aaefe75b6e4fb6bd6f464b4b6f85243e60f03