diff --git a/extensions-builtin/Lora/scripts/lora_script.py b/extensions-builtin/Lora/scripts/lora_script.py index 49086f8cc..dc18866a9 100644 --- a/extensions-builtin/Lora/scripts/lora_script.py +++ b/extensions-builtin/Lora/scripts/lora_script.py @@ -1,22 +1,16 @@ import re -import gradio as gr from fastapi import FastAPI -import network import networks -import lora # noqa:F401 # pylint: disable=unused-import -# import lora_patches -import extra_networks_lora -import ui_extra_networks_lora +from network import NetworkOnDisk +from ui_extra_networks_lora import ExtraNetworksPageLora +from extra_networks_lora import ExtraNetworkLora +# import lora # noqa:F401 # pylint: disable=unused-import from modules import script_callbacks, ui_extra_networks, extra_networks, shared -# def unload(): -# networks.originals.undo() - - def before_ui(): - ui_extra_networks.register_page(ui_extra_networks_lora.ExtraNetworksPageLora()) - networks.extra_network_lora = extra_networks_lora.ExtraNetworkLora() + ui_extra_networks.register_page(ExtraNetworksPageLora()) + networks.extra_network_lora = ExtraNetworkLora() extra_networks.register_extra_network(networks.extra_network_lora) # extra_networks.register_extra_network_alias(networks.extra_network_lora, "lyco") @@ -28,15 +22,7 @@ script_callbacks.on_before_ui(before_ui) script_callbacks.on_infotext_pasted(networks.infotext_pasted) -shared.options_templates.update(shared.options_section(('extra_networks', "Extra Networks"), { - # "sd_lora": shared.OptionInfo("None", "Add network to prompt", gr.Dropdown, lambda: {"choices": ["None", *networks.available_networks], "visible": False}, refresh=networks.list_available_networks), - "sd_lora": shared.OptionInfo("None", "Add network to prompt", gr.Dropdown, {"choices": ["None"], "visible": False}), - # "lora_show_all": shared.OptionInfo(False, "Always show all networks on the Lora page").info("otherwise, those detected as for incompatible version of Stable Diffusion will be hidden"), - # "lora_hide_unknown_for_versions": shared.OptionInfo([], "Hide networks of unknown versions for model versions", gr.CheckboxGroup, {"choices": ["SD1", "SD2", "SDXL"]}), -})) - - -def create_lora_json(obj: network.NetworkOnDisk): +def create_lora_json(obj: NetworkOnDisk): return { "name": obj.name, "alias": obj.alias, @@ -45,7 +31,7 @@ def create_lora_json(obj: network.NetworkOnDisk): } -def api_networks(_: gr.Blocks, app: FastAPI): +def api_networks(_, app: FastAPI): @app.get("/sdapi/v1/loras") async def get_loras(): return [create_lora_json(obj) for obj in networks.available_networks.values()] diff --git a/extensions-builtin/Lora/ui_extra_networks_lora.py b/extensions-builtin/Lora/ui_extra_networks_lora.py index f029aaf05..bd408b327 100644 --- a/extensions-builtin/Lora/ui_extra_networks_lora.py +++ b/extensions-builtin/Lora/ui_extra_networks_lora.py @@ -91,7 +91,7 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): return None def list_items(self): - with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + with concurrent.futures.ThreadPoolExecutor(max_workers=shared.max_workers) as executor: future_items = {executor.submit(self.create_item, net): net for net in networks.available_networks} for future in concurrent.futures.as_completed(future_items): item = future.result() diff --git a/installer.py b/installer.py index d7f069af9..413463fbb 100644 --- a/installer.py +++ b/installer.py @@ -6,8 +6,6 @@ import shutil import logging import platform import subprocess -import io -import pstats import cProfile import pkg_resources @@ -29,6 +27,7 @@ opts = {} args = Dot({ 'debug': False, 'reset': False, + 'profile': False, 'upgrade': False, 'skip_extensions': False, 'skip_requirements': False, @@ -143,19 +142,9 @@ def print_dict(d): return ' '.join([f'{k}={v}' for k, v in d.items()]) -def print_profile(profile: cProfile.Profile, msg: str): - try: - from rich import print # pylint: disable=redefined-builtin - except Exception: - pass - profile.disable() - stream = io.StringIO() - ps = pstats.Stats(profile, stream=stream) - ps.sort_stats(pstats.SortKey.CUMULATIVE).print_stats(15) - profile = None - lines = stream.getvalue().split('\n') - lines = [line for line in lines if ' Processed: if not hasattr(p.sd_model, 'sd_checkpoint_info'): return None @@ -743,22 +713,26 @@ def process_images(p: StableDiffusionProcessing) -> Processed: modules.script_callbacks.before_process_callback(p) if shared.cmd_opts.profile: - """ - import torch.profiler # pylint: disable=redefined-outer-name - with torch.profiler.profile(profile_memory=True, with_modules=True) as prof: - with torch.profiler.record_function("process_images"): - res = process_images_inner(p) - print_profile(prof, 'process_images') - """ import cProfile - pr = cProfile.Profile() - pr.enable() + profile_python = cProfile.Profile() + profile_python.enable() with context_hypertile_vae(p), context_hypertile_unet(p): + import torch.profiler # pylint: disable=redefined-outer-name + activities=[torch.profiler.ProfilerActivity.CPU] + if torch.cuda.is_available(): + activities.append(torch.profiler.ProfilerActivity.CUDA) + shared.log.debug(f'Torch profile: activities={activities}') + if shared.profiler is None: + shared.profiler = torch.profiler.profile(activities=activities, profile_memory=True, with_modules=True) + shared.profiler.start() + shared.profiler.step() res = process_images_inner(p) - print_profile(pr, 'Torch') + errors.profile_torch(shared.profiler, 'Process') + errors.profile(profile_python, 'Process') else: with context_hypertile_vae(p), context_hypertile_unet(p): res = process_images_inner(p) + finally: if not shared.opts.cuda_compile: modules.sd_models.apply_token_merging(p.sd_model, 0) diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 2049fa881..e90cd0126 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -85,6 +85,8 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro return kwargs kwargs = correction_callback(p, timestep, kwargs) shared.state.current_latent = kwargs['latents'] + if shared.cmd_opts.profile and shared.profiler is not None: + shared.profiler.step() return kwargs def full_vae_decode(latents, model): @@ -139,6 +141,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro return encoded def vae_decode(latents, model, output_type='np', full_quality=True): + t0 = time.time() prev_job = shared.state.job shared.state.job = 'vae' if not torch.is_tensor(latents): # already decoded @@ -163,6 +166,9 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro # decoded = validate_sample(decoded) imgs = model.image_processor.postprocess(decoded, output_type=output_type) shared.state.job = prev_job + if shared.cmd_opts.profile: + t1 = time.time() + shared.log.debug(f'Profile: VAE decode: {t1-t0:.2f}') return imgs def vae_encode(image, model, full_quality=True): # pylint: disable=unused-variable @@ -269,6 +275,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro return task_args def set_pipeline_args(model, prompts: list, negative_prompts: list, prompts_2: typing.Optional[list]=None, negative_prompts_2: typing.Optional[list]=None, desc:str='', **kwargs): + t0 = time.time() 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} ' + '\x1b[38;5;71m' + desc, ncols=80, colour='#327fba') args = {} @@ -379,6 +386,9 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro shared.log.debug(txt) # components = [{ k: getattr(v, 'device', None) } for k, v in model.components.items()] # shared.log.debug(f'Diffuser pipeline components: {components}') + if shared.cmd_opts.profile: + t1 = time.time() + shared.log.debug(f'Profile: pipeline args: {t1-t0:.2f}') return args def recompile_model(hires=False): @@ -502,7 +512,11 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro p.extra_generation_params['Pipeline'] = shared.sd_model.__class__.__name__ p.extra_generation_params["Sampler Eta"] = shared.opts.scheduler_eta if shared.opts.scheduler_eta is not None and shared.opts.scheduler_eta > 0 and shared.opts.scheduler_eta < 1 else None try: + t0 = time.time() output = shared.sd_model(**base_args) # pylint: disable=not-callable + if shared.cmd_opts.profile: + t1 = time.time() + shared.log.debug(f'Profile: pipeline call: {t1-t0:.2f}') if not hasattr(output, 'images') and hasattr(output, 'frames'): shared.log.debug(f'Generated: frames={len(output.frames[0])}') output.images = output.frames[0] diff --git a/modules/script_callbacks.py b/modules/script_callbacks.py index e30f88dd1..663199968 100644 --- a/modules/script_callbacks.py +++ b/modules/script_callbacks.py @@ -329,7 +329,7 @@ def before_ui_callback(): def add_callback(callbacks, fun): - stack = [x for x in inspect.stack() if x.filename != __file__] + stack = [x for x in inspect.stack(0) if x.filename != __file__] filename = stack[0].filename if len(stack) > 0 else 'unknown file' callbacks.append(ScriptCallback(filename, fun)) diff --git a/modules/script_loading.py b/modules/script_loading.py index 5ac82f538..b20d4072e 100644 --- a/modules/script_loading.py +++ b/modules/script_loading.py @@ -3,7 +3,7 @@ import os import contextlib import importlib.util import modules.errors as errors -from installer import setup_logging +from installer import setup_logging, args preloaded = [] @@ -12,6 +12,10 @@ preloaded = [] def load_module(path): module_spec = importlib.util.spec_from_file_location(os.path.basename(path), path) module = importlib.util.module_from_spec(module_spec) + if args.profile: + import cProfile + pr = cProfile.Profile() + pr.enable() try: if '/sd-extension-' in path: # safe extensions without stdout intercept module_spec.loader.exec_module(module) @@ -25,6 +29,8 @@ def load_module(path): errors.log.info(f"Extension: script='{os.path.relpath(path)}' {line.strip()}") except Exception as e: errors.display(e, f'Module load: {path}') + if args.profile: + errors.profile(pr, f'Scripts: {path}') return module diff --git a/modules/scripts.py b/modules/scripts.py index 01c501f2c..aee3545b2 100644 --- a/modules/scripts.py +++ b/modules/scripts.py @@ -261,6 +261,7 @@ def load_scripts(): elif issubclass(script_class, scripts_postprocessing.ScriptPostprocessing): postprocessing_scripts_data.append(ScriptClassData(script_class, scriptfile.path, scriptfile.basedir, module)) + from installer import args for scriptfile in scripts_list: try: if scriptfile.basedir != paths.script_path: @@ -274,12 +275,10 @@ def load_scripts(): current_basedir = paths.script_path t.record(os.path.basename(scriptfile.basedir)) sys.path = syspath - global scripts_txt2img, scripts_img2img, scripts_postproc # pylint: disable=global-statement scripts_txt2img = ScriptRunner() scripts_img2img = ScriptRunner() scripts_postproc = scripts_postprocessing.ScriptPostprocessingRunner() - return t, time.time()-t0 diff --git a/modules/sd_models.py b/modules/sd_models.py index ee94ec574..0956353ce 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -760,6 +760,10 @@ def set_diffuser_options(sd_model, vae, op: str): def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=None, op='model'): # pylint: disable=unused-argument import torch # pylint: disable=reimported,redefined-outer-name + if shared.cmd_opts.profile: + import cProfile + pr = cProfile.Profile() + pr.enable() if timer is None: timer = Timer() logging.getLogger("diffusers").setLevel(logging.ERROR) @@ -976,6 +980,8 @@ def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=No timer.record("load") devices.torch_gc(force=True) + if shared.cmd_opts.profile: + errors.profile(pr, 'Load') script_callbacks.model_loaded_callback(sd_model) shared.log.info(f"Load {op}: time={timer.summary()} native={get_native(sd_model)} {memory_stats()}") diff --git a/modules/shared.py b/modules/shared.py index 0ba936223..7c2bea684 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -781,6 +781,7 @@ class Options: value = expected_type(value) return value +profiler = None opts = Options() config_filename = cmd_opts.config opts.load(config_filename) @@ -804,6 +805,7 @@ device = devices.device batch_cond_uncond = opts.always_batch_cond_uncond or not (cmd_opts.lowvram or cmd_opts.medvram) parallel_processing_allowed = not cmd_opts.lowvram mem_mon = modules.memmon.MemUsageMonitor("MemMon", devices.device) +max_workers = 2 if devices.backend == "directml": directml_do_hijack() diff --git a/modules/styles.py b/modules/styles.py index be5259f2c..1f9a6fd52 100644 --- a/modules/styles.py +++ b/modules/styles.py @@ -4,6 +4,7 @@ import re import os import csv import json +import time from installer import log @@ -87,6 +88,7 @@ class StyleDatabase: def load_style(self, fn, prefix=None): with open(fn, 'r', encoding='utf-8') as f: + new_style = None try: all_styles = json.load(f) if type(all_styles) is dict: @@ -100,7 +102,7 @@ class StyleDatabase: name = os.path.join(prefix, name) else: name = os.path.join(os.path.dirname(os.path.relpath(fn, self.path)), name) - self.styles[style["name"]] = Style( + new_style = Style( name=name, desc=style.get('description', name), prompt=style.get("prompt", ""), @@ -110,26 +112,37 @@ class StyleDatabase: filename=fn, mtime=os.path.getmtime(fn), ) + self.styles[style["name"]] = new_style except Exception as e: log.error(f'Failed to load style: file={fn} error={e}') + return new_style def reload(self): + t0 = time.time() self.styles.clear() + def list_folder(folder): - for filename in os.listdir(folder): - fn = os.path.abspath(os.path.join(folder, filename)) - if os.path.isfile(fn) and fn.lower().endswith(".json"): - self.load_style(fn) - elif os.path.isdir(fn) and not fn.startswith('.'): - list_folder(fn) + import concurrent + future_items = {} + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + for filename in os.listdir(folder): + fn = os.path.abspath(os.path.join(folder, filename)) + if os.path.isfile(fn) and fn.lower().endswith(".json"): + future_items[executor.submit(self.load_style, fn, None)] = fn + # self.load_style(fn) + elif os.path.isdir(fn) and not fn.startswith('.'): + list_folder(fn) + self.styles = dict(sorted(self.styles.items(), key=lambda style: style[1].filename)) + if self.built_in: + fn = os.path.join('html', 'art-styles.json') + future_items[executor.submit(self.load_style, fn, 'built-in')] = fn + for future in concurrent.futures.as_completed(future_items): + future.result() list_folder(self.path) - self.styles = dict(sorted(self.styles.items(), key=lambda style: style[1].filename)) - if self.built_in: - self.load_style(os.path.join('html', 'art-styles.json'), 'built-in') - - log.debug(f'Load styles: folder="{self.path}" items={len(self.styles.keys())}') + t1 = time.time() + log.debug(f'Load styles: folder="{self.path}" items={len(self.styles.keys())} time={t1-t0:.2f}') def find_style(self, name): found = [style for style in self.styles.values() if style.name == name] diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 9727b8752..f30dba2cc 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -15,7 +15,7 @@ from collections import OrderedDict import gradio as gr from PIL import Image from starlette.responses import FileResponse, JSONResponse -from modules import paths, shared, scripts, modelloader +from modules import paths, shared, scripts, modelloader, errors from modules.ui_components import ToolButton import modules.ui_symbols as symbols @@ -270,7 +270,7 @@ class ExtraNetworksPage: self.html = f"
{subdirs_html}
{self.html}
" else: return '' - shared.log.debug(f"Extra networks: page='{self.name}' items={len(self.items)} subfolders={len(subdirs)} tab={tabname} folders={self.allowed_directories_for_previews()} list={self.list_time:.2f} desc={self.desc_time:.2f} info={self.info_time:.2f}") + shared.log.debug(f"Extra networks: page='{self.name}' items={len(self.items)} subfolders={len(subdirs)} tab={tabname} folders={self.allowed_directories_for_previews()} list={self.list_time:.2f} desc={self.desc_time:.2f} info={self.info_time:.2f} workers={shared.max_workers}") if len(self.missing_thumbs) > 0: threading.Thread(target=self.create_thumb).start() return self.html @@ -463,6 +463,10 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): ui.tabs = gr.Tabs(elem_id=tabname+"_extra_tabs") ui.button_details = gr.Button('Details', elem_id=tabname+"_extra_details_btn", visible=False) state = {} + if shared.cmd_opts.profile: + import cProfile + pr = cProfile.Profile() + pr.enable() def get_item(state, params = None): if params is not None and type(params) == dict: @@ -567,6 +571,10 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): page_html = gr.HTML(page.html, elem_id=f'{tabname}{page.name}_extra_page', elem_classes="extra-networks-page") ui.pages.append(page_html) tab.select(ui_tab_change, _js="getENActivePage", inputs=[ui.button_details], outputs=[ui.button_scan, ui.button_save, ui.button_model]) + if shared.cmd_opts.profile: + errors.profile(pr, 'ExtraNetworks') + pr.disable() + # ui.tabs.change(fn=ui_tab_change, inputs=[], outputs=[ui.button_scan, ui.button_save]) def fn_save_img(image): diff --git a/modules/ui_extra_networks_checkpoints.py b/modules/ui_extra_networks_checkpoints.py index 01902b376..692acdc18 100644 --- a/modules/ui_extra_networks_checkpoints.py +++ b/modules/ui_extra_networks_checkpoints.py @@ -64,7 +64,7 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage): return record def list_items(self): - with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + with concurrent.futures.ThreadPoolExecutor(max_workers=shared.max_workers) as executor: future_items = {executor.submit(self.create_item, cp): cp for cp in list(sd_models.checkpoints_list.copy())} for future in concurrent.futures.as_completed(future_items): item = future.result() diff --git a/modules/ui_extra_networks_styles.py b/modules/ui_extra_networks_styles.py index 12f5f0a53..8dcccf96e 100644 --- a/modules/ui_extra_networks_styles.py +++ b/modules/ui_extra_networks_styles.py @@ -95,7 +95,7 @@ class ExtraNetworksPageStyles(ui_extra_networks.ExtraNetworksPage): return item def list_items(self): - with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + with concurrent.futures.ThreadPoolExecutor(max_workers=shared.max_workers) as executor: future_items = {executor.submit(self.create_item, style): style for style in list(shared.prompt_styles.styles)} for future in concurrent.futures.as_completed(future_items): item = future.result() diff --git a/modules/ui_extra_networks_textual_inversion.py b/modules/ui_extra_networks_textual_inversion.py index f67031029..d94ce39dc 100644 --- a/modules/ui_extra_networks_textual_inversion.py +++ b/modules/ui_extra_networks_textual_inversion.py @@ -68,7 +68,7 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage): self.embeddings = [] self.embeddings = sorted(self.embeddings, key=lambda emb: emb.filename) - with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + with concurrent.futures.ThreadPoolExecutor(max_workers=shared.max_workers) as executor: future_items = {executor.submit(self.create_item, net): net for net in self.embeddings} for future in concurrent.futures.as_completed(future_items): item = future.result() diff --git a/webui.py b/webui.py index 157731f10..d352bed72 100644 --- a/webui.py +++ b/webui.py @@ -311,6 +311,9 @@ def webui(restart=False): modules.sd_models.write_metadata() load_model() shared.opts.save(shared.config_filename) + if cmd_opts.profile: + for k, v in modules.script_callbacks.callback_map.items(): + shared.log.debug(f'Registered callbacks: {k}={len(v)} {[c.script for c in v]}') log.info(f"Startup time: {timer.startup.summary()}") debug = log.info if os.environ.get('SD_SCRIPT_DEBUG', None) is not None else lambda *args, **kwargs: None debug('Loaded scripts:')