diff --git a/html/locale_en.json b/html/locale_en.json index 1436a6786..f17742039 100644 --- a/html/locale_en.json +++ b/html/locale_en.json @@ -356,7 +356,7 @@ {"id":"","label":"Enable splitting of hires batch processing","localized":"","hint":"Reduces VRAM usage when using hires fix on batches of images"}, {"id":"","label":"Load models using stream loading method","localized":"","hint":"When loading models attempt stream loading optimized for slow or network storage"}, {"id":"","label":"When loading models attempt to reuse previous model dictionary","localized":"","hint":""}, - {"id":"","label":"Disable cross-attention layer optimization","localized":"","hint":"Disable the all cross-attention optimization. May result in higher VRAM usage and longer generation times"}, + {"id":"","label":"Disabled","localized":"","hint":""}, {"id":"","label":"xFormers","localized":"","hint":"Memory optimization. Non-Deterministic (different results each time)"}, {"id":"","label":"Scaled-Dot-Product","localized":"","hint":"Memory optimization. Non-Deterministic unless SDP memory attention is disabled."}, {"id":"","label":"Doggettx's","localized":"","hint":""}, diff --git a/modules/images.py b/modules/images.py index 7f29983bb..98b44c4d0 100644 --- a/modules/images.py +++ b/modules/images.py @@ -291,7 +291,8 @@ def sanitize_filename_part(text, replace_spaces=True): class FilenameGenerator: replacements = { - 'batch_number': lambda self: NOTHING if self.index <= 1 else self.index, + 'batch_number': lambda self: self.batch_number, + 'iter_number': lambda self: self.iter_number, 'cfg': lambda self: self.p and self.p.cfg_scale, 'clip_skip': lambda self: self.p and self.p.clip_skip, 'date': lambda self: datetime.datetime.now().strftime('%Y-%m-%d'), @@ -320,12 +321,17 @@ class FilenameGenerator: } default_time_format = '%Y%m%d%H%M%S' - def __init__(self, p, seed, prompt, image, index = 0): + def __init__(self, p, seed, prompt, image, grid=False): self.p = p self.seed = seed self.prompt = prompt self.image = image - self.index = index if self.p is None or self.p.batch_size == 1 else self.p.batch_index + 1 + if not grid: + self.batch_number = NOTHING if self.p is None or getattr(self.p, 'batch_size', 1) == 1 else (self.p.batch_index + 1 if hasattr(self.p, 'batch_index') else NOTHING) + self.iter_number = NOTHING if self.p is None or getattr(self.p, 'n_iter', 1) == 1 else (self.p.iteration + 1 if hasattr(self.p, 'iteration') else NOTHING) + else: + self.batch_number = NOTHING + self.iter_number = NOTHING def hasprompt(self, *args): lower = self.prompt.lower() @@ -449,7 +455,7 @@ def atomically_save_image(): image_format = 'JPEG' if shared.opts.image_watermark_enabled: image = set_watermark(image, shared.opts.image_watermark) - shared.log.debug(f'Saving: image={fn} type={image_format} size={image.width}x{image.height}') + shared.log.debug(f'Saving: image="{fn}" type={image_format} size={image.width}x{image.height}') # actual save exifinfo = (exifinfo or "") if shared.opts.image_metadata else "" if image_format == 'PNG': @@ -489,8 +495,14 @@ def atomically_save_image(): with open(os.path.join(paths.data_path, "params.txt"), "w", encoding="utf8") as file: file.write(exifinfo) if shared.opts.save_log_fn != '' and len(exifinfo) > 0: - entry = { 'filename': filename, 'time': datetime.datetime.now().isoformat(), 'info': exifinfo } - shared.writefile(entry, os.path.join(paths.data_path, shared.opts.save_log_fn), mode='a+') + fn = os.path.join(paths.data_path, shared.opts.save_log_fn) + entries = shared.readfile(fn) + idx = len(list(entries)) + if idx == 0: + entries = [] + entry = { 'id': idx, 'filename': filename, 'time': datetime.datetime.now().isoformat(), 'info': exifinfo } + entries.append(entry) + shared.writefile(entries, fn, mode='w') save_queue.task_done() @@ -499,7 +511,7 @@ save_thread = threading.Thread(target=atomically_save_image, daemon=True) save_thread.start() -def save_image(image, path, basename, seed=None, prompt=None, extension='jpg', info=None, short_filename=False, no_prompt=False, grid=False, pnginfo_section_name='parameters', p=None, existing_info=None, forced_filename=None, suffix="", save_to_dirs=None, index=0): +def save_image(image, path, basename, seed=None, prompt=None, extension='jpg', info=None, short_filename=False, no_prompt=False, grid=False, pnginfo_section_name='parameters', p=None, existing_info=None, forced_filename=None, suffix="", save_to_dirs=None): """Save an image. Args: image (`PIL.Image`): @@ -537,7 +549,7 @@ def save_image(image, path, basename, seed=None, prompt=None, extension='jpg', i return None, None if path is None or len(path) == 0: # set default path to avoid errors when functions are triggered manually or via api and param is not set path = shared.opts.outdir_save - namegen = FilenameGenerator(p, seed, prompt, image, index) + namegen = FilenameGenerator(p, seed, prompt, image, grid=grid) if save_to_dirs is None: save_to_dirs = (grid and shared.opts.grid_save_to_dirs) or (not grid and shared.opts.save_to_dirs and not no_prompt) if save_to_dirs: diff --git a/modules/processing.py b/modules/processing.py index 772bbafba..90c4038af 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -472,6 +472,7 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts, all_seeds, all_su "CFG scale": p.cfg_scale, "Size": f"{p.width}x{p.height}", "Batch": f'{p.n_iter}x{p.batch_size}' if p.n_iter > 1 or p.batch_size > 1 else None, + "Index": f'{p.iteration + 1}x{index + 1}' if (p.n_iter > 1 or p.batch_size > 1) and index >= 0 else None, "Parser": shared.opts.prompt_attention, "Model": None if (not shared.opts.add_model_name_to_info) or (not shared.sd_model.sd_checkpoint_info.model_name) else shared.sd_model.sd_checkpoint_info.model_name.replace(',', '').replace(':', ''), "Model hash": getattr(p, 'sd_model_hash', None if (not shared.opts.add_model_hash_to_info) or (not shared.sd_model.sd_model_hash) else shared.sd_model.sd_model_hash), @@ -798,7 +799,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: p.scripts.postprocess_batch_list(p, batch_params, batch_number=n) x_samples_ddim = batch_params.images - def infotext(index=0): # pylint: disable=function-redefined # noqa: F811 + def infotext(index): # pylint: disable=function-redefined # noqa: F811 return create_infotext(p, p.prompts, p.seeds, p.subseeds, index=index, all_negative_prompts=p.negative_prompts) for i, x_sample in enumerate(x_samples_ddim): @@ -816,7 +817,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: p.restore_faces = False info = infotext(i) p.restore_faces = orig - images.save_image(Image.fromarray(x_sample), path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-face-restoration") + images.save_image(Image.fromarray(x_sample), path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-face-restore") p.ops.append('face') x_sample = modules.face_restoration.restore_faces(x_sample) image = Image.fromarray(x_sample) @@ -831,7 +832,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: info = infotext(i) p.color_corrections = orig image_without_cc = apply_overlay(image, p.paste_to, i, p.overlay_images) - images.save_image(image_without_cc, path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-color-correction") + images.save_image(image_without_cc, path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-color-correct") p.ops.append('color') image = apply_color_correction(p.color_corrections[i], image) image = apply_overlay(image, p.paste_to, i, p.overlay_images) @@ -840,7 +841,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: image.info["parameters"] = text output_images.append(image) if shared.opts.samples_save and not p.do_not_save_samples: - images.save_image(image, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=text, p=p) + images.save_image(image, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=text, p=p) # main save image if hasattr(p, 'mask_for_overlay') and p.mask_for_overlay and any([shared.opts.save_mask, shared.opts.save_mask_composite, shared.opts.return_mask, shared.opts.return_mask_composite]): image_mask = p.mask_for_overlay.convert('RGB') image_mask_composite = Image.composite(image.convert('RGBA').convert('RGBa'), Image.new('RGBa', image.size), images.resize_image(3, p.mask_for_overlay, image.width, image.height).convert('L')).convert('RGBA') @@ -865,13 +866,13 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if images.check_grid_size(output_images): grid = images.image_grid(output_images, p.batch_size) if shared.opts.return_grid: - text = infotext() + text = infotext(-1) infotexts.insert(0, text) grid.info["parameters"] = text output_images.insert(0, grid) index_of_first_image = 1 if shared.opts.grid_save: - images.save_image(grid, p.outpath_grids, "grid", p.all_seeds[0], p.all_prompts[0], shared.opts.grid_format, info=infotext(), short_filename=not shared.opts.grid_extended_filename, p=p, grid=True) + images.save_image(grid, p.outpath_grids, "", p.all_seeds[0], p.all_prompts[0], shared.opts.grid_format, info=infotext(-1), short_filename=not shared.opts.grid_extended_filename, p=p, grid=True, suffix="-grid") # main save grid if not p.disable_extra_networks and extra_network_data: modules.extra_networks.deactivate(p, extra_network_data) @@ -880,7 +881,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: p, images_list=output_images, seed=p.all_seeds[0], - info=infotext(), + info=infotext(0), comments="\n".join(comments), subseed=p.all_subseeds[0], index_of_first_image=index_of_first_image, @@ -987,7 +988,7 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): info = create_infotext(self, self.all_prompts, self.all_seeds, self.all_subseeds, [], iteration=self.iteration, position_in_batch=index) self.extra_generation_params = orig1 self.restore_faces = orig2 - images.save_image(image, self.outpath_samples, "", seeds[index], prompts[index], shared.opts.samples_format, info=info, suffix="-before-hires", index=index+1) + images.save_image(image, self.outpath_samples, "", seeds[index], prompts[index], shared.opts.samples_format, info=info, suffix="-before-hires") if shared.backend == shared.Backend.DIFFUSERS: modules.sd_models.set_diffuser_pipe(self.sd_model, modules.sd_models.DiffusersTaskType.TEXT_2_IMAGE) @@ -1140,7 +1141,7 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): self.init_img_width = img.width # pylint: disable=attribute-defined-outside-init self.init_img_height = img.height # pylint: disable=attribute-defined-outside-init if shared.opts.save_init_img: - images.save_image(img, path=shared.opts.outdir_init_images, basename=None, forced_filename=self.init_img_hash, save_to_dirs=False) + images.save_image(img, path=shared.opts.outdir_init_images, basename=None, forced_filename=self.init_img_hash, save_to_dirs=False, suffix="-init-image") image = images.flatten(img, shared.opts.img2img_background_color) if crop_region is None and self.resize_mode != 4: image = images.resize_image(self.resize_mode, image, self.width, self.height) diff --git a/modules/sd_hijack.py b/modules/sd_hijack.py index 5cd407e19..3f465a600 100644 --- a/modules/sd_hijack.py +++ b/modules/sd_hijack.py @@ -37,45 +37,38 @@ def apply_optimizations(): can_use_sdp = hasattr(torch.nn.functional, "scaled_dot_product_attention") and callable(torch.nn.functional.scaled_dot_product_attention) if devices.device == torch.device("cpu"): if opts.cross_attention_optimization == "Scaled-Dot-Product": - shared.log.warning("Scaled dot product cross attention is not available on CPU") + shared.log.warning("Cross-attention: Scaled dot product is not available on CPU") can_use_sdp = False if opts.cross_attention_optimization == "xFormers": - shared.log.warning("xFormers cross attention is not available on CPU") + shared.log.warning("Cross-attention: xFormers is not available on CPU") shared.xformers_available = False - if opts.cross_attention_optimization == "Disable cross-attention layer optimization": - shared.log.warning("Cross-attention optimization disabled") + shared.log.info(f"Cross-attention: optimization={opts.cross_attention_optimization} options={opts.cross_attention_options}") + if opts.cross_attention_optimization == "Disabled": optimization_method = 'none' if can_use_sdp and opts.cross_attention_optimization == "Scaled-Dot-Product" and 'SDP disable memory attention' in opts.cross_attention_options: - shared.log.info("Applying scaled dot product cross attention optimization (without memory efficient attention)") ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.scaled_dot_product_no_mem_attention_forward ldm.modules.diffusionmodules.model.AttnBlock.forward = sd_hijack_optimizations.sdp_no_mem_attnblock_forward optimization_method = 'sdp-no-mem' elif can_use_sdp and opts.cross_attention_optimization == "Scaled-Dot-Product": - shared.log.info("Applying scaled dot product cross attention optimization") ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.scaled_dot_product_attention_forward ldm.modules.diffusionmodules.model.AttnBlock.forward = sd_hijack_optimizations.sdp_attnblock_forward optimization_method = 'sdp' if shared.xformers_available and opts.cross_attention_optimization == "xFormers": - shared.log.info("Applying xformers cross attention optimization") ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.xformers_attention_forward ldm.modules.diffusionmodules.model.AttnBlock.forward = sd_hijack_optimizations.xformers_attnblock_forward optimization_method = 'xformers' if opts.cross_attention_optimization == "Sub-quadratic": - shared.log.info("Applying sub-quadratic cross attention optimization") ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.sub_quad_attention_forward ldm.modules.diffusionmodules.model.AttnBlock.forward = sd_hijack_optimizations.sub_quad_attnblock_forward optimization_method = 'sub-quadratic' if opts.cross_attention_optimization == "Split attention": - shared.log.info("Applying split attention optimization") ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.split_cross_attention_forward_v1 optimization_method = 'v1' if opts.cross_attention_optimization == "InvokeAI's": - shared.log.info("Applying InvokeAI cross attention optimization") ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.split_cross_attention_forward_invokeAI optimization_method = 'invokeai' if opts.cross_attention_optimization == "Doggettx's": - shared.log.info("Applying Doggettx cross attention optimization") ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.split_cross_attention_forward ldm.modules.diffusionmodules.model.AttnBlock.forward = sd_hijack_optimizations.cross_attention_attnblock_forward optimization_method = 'doggettx' diff --git a/modules/sd_models.py b/modules/sd_models.py index 5278015e6..d77134238 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -261,7 +261,7 @@ def select_checkpoint(op='model'): return None checkpoint_info = get_closet_checkpoint_match(model_checkpoint) if checkpoint_info is not None: - shared.log.debug(f'Select checkpoint: {op} {checkpoint_info.title if checkpoint_info is not None else None}') + shared.log.debug(f'Select checkpoint: {op}="{checkpoint_info.title if checkpoint_info is not None else None}"') return checkpoint_info if len(checkpoints_list) == 0 and not shared.cmd_opts.no_download: shared.log.error("Cannot run without a checkpoint") @@ -275,7 +275,7 @@ def select_checkpoint(op='model'): shared.log.info("Selecting first available checkpoint") # shared.log.warning(f"Loading fallback checkpoint: {checkpoint_info.title}") shared.opts.data['sd_model_checkpoint'] = checkpoint_info.title - shared.log.debug(f'Select checkpoint: {checkpoint_info.title if checkpoint_info is not None else None}') + shared.log.debug(f'Select checkpoint: {op}="{checkpoint_info.title if checkpoint_info is not None else None}"') return checkpoint_info diff --git a/modules/shared.py b/modules/shared.py index f1da73266..d69f0ff3b 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -340,7 +340,7 @@ def readfile(filename, silent=False): return data -def writefile(data, filename, mode='w'): +def writefile(data, filename, mode='w', silent=False): def default(obj): log.error(f"Saving: {filename} not a valid object: {obj}") @@ -350,7 +350,8 @@ def writefile(data, filename, mode='w'): with fasteners.InterProcessLock(f"{filename}.lock"): # skipkeys=True, ensure_ascii=True, check_circular=True, allow_nan=True output = json.dumps(data, indent=2, default=default) - log.debug(f'Saving: {filename} len={len(output)}') + if not silent: + log.debug(f'Saving: {filename} len={len(output)}') with open(filename, mode, encoding="utf8") as file: file.write(output) except Exception as e: diff --git a/modules/shared_items.py b/modules/shared_items.py index 8bb05b064..061fe4929 100644 --- a/modules/shared_items.py +++ b/modules/shared_items.py @@ -20,7 +20,7 @@ def refresh_vae_list(): def list_crossattention(): return [ - "Disable cross-attention layer optimization", + "Disabled", "xFormers", "Scaled-Dot-Product", "Doggettx's", diff --git a/modules/ui_common.py b/modules/ui_common.py index fa5569162..8d1bb48fd 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -75,17 +75,21 @@ def save_files(js_data, images, html_info, index): class PObject: # pylint: disable=too-few-public-methods def __init__(self, d=None): if d is not None: - for key, value in d.items(): - setattr(self, key, value) - self.seed = getattr(self, 'seed', None) or getattr(self, 'Seed', None) + for k, v in d.items(): + setattr(self, k, v) self.prompt = getattr(self, 'prompt', None) or getattr(self, 'Prompt', None) - self.all_seeds = getattr(self, 'all_seeds', [self.seed]) self.all_prompts = getattr(self, 'all_prompts', [self.prompt]) + self.negative_prompt = getattr(self, 'negative_prompt', None) + self.all_negative_prompt = getattr(self, 'all_negative_prompts', [self.negative_prompt]) + self.seed = getattr(self, 'seed', None) or getattr(self, 'Seed', None) + self.all_seeds = getattr(self, 'all_seeds', [self.seed]) + self.subseed = getattr(self, 'subseed', None) + self.all_subseeds = getattr(self, 'all_subseeds', [self.subseed]) + self.width = getattr(self, 'width', None) + self.height = getattr(self, 'height', None) + self.index_of_first_image = getattr(self, 'index_of_first_image', 0) self.infotexts = getattr(self, 'infotexts', [html_info]) self.infotext = self.infotexts[0] if len(self.infotexts) > 0 else html_info - self.index_of_first_image = getattr(self, 'index_of_first_image', 0) - self.batch_size = 1 - try: data = json.loads(js_data) except Exception: @@ -95,8 +99,6 @@ def save_files(js_data, images, html_info, index): if index > -1 and shared.opts.save_selected_only and (index >= p.index_of_first_image): # ensures we are looking at a specific non-grid picture, and we have save_selected_only # pylint: disable=no-member images = [images[index]] start_index = index - else: - p.batch_size = len(images) filenames = [] fullfns = [] for image_index, filedata in enumerate(images, start_index): @@ -114,12 +116,12 @@ def save_files(js_data, images, html_info, index): fullfns.append(fullfn) destination = shared.opts.outdir_save if shared.opts.use_save_to_dirs_for_ui: - namegen = modules.images.FilenameGenerator(p, seed=p.all_seeds[i], prompt=p.all_prompts[i], image=None, index=image_index) # pylint: disable=no-member + namegen = modules.images.FilenameGenerator(p, seed=p.all_seeds[i], prompt=p.all_prompts[i], image=None) # pylint: disable=no-member dirname = namegen.apply(shared.opts.directories_filename_pattern or "[prompt_words]").lstrip(' ').rstrip('\\ /') destination = os.path.join(destination, dirname) os.makedirs(destination, exist_ok = True) shutil.copy(fullfn, destination) - shared.log.info(f"Copying image: {fullfn} -> {destination}") + shared.log.info(f'Copying image: file="{fullfn}" folder="{destination}"') tgt_filename = os.path.join(destination, os.path.basename(fullfn)) modules.script_callbacks.image_save_btn_callback(tgt_filename) else: diff --git a/modules/ui_tempdir.py b/modules/ui_tempdir.py index 945a004b6..38b4b5923 100644 --- a/modules/ui_tempdir.py +++ b/modules/ui_tempdir.py @@ -61,7 +61,7 @@ def pil_to_temp_file(self, img, dir: str, format="png") -> str: # pylint: disabl file_obj = tempfile.NamedTemporaryFile(delete=False, suffix=".png", dir=dir) img.save(file_obj, pnginfo=(metadata if use_metadata else None)) name = file_obj.name - shared.log.debug(f'Saving temp image: {name}') + shared.log.debug(f'Saving temp: image="{name}"') return name