diff --git a/CHANGELOG.md b/CHANGELOG.md index 372bd7317..93f4724e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## Update for 2024-06-14 - force apply vae config on model load +- restructure api examples: `cli/api-*` +- fix control second pass resize +- fix api face-hires ## Update for 2024-06-13 diff --git a/cli/simple-control.py b/cli/api-control.py similarity index 98% rename from cli/simple-control.py rename to cli/api-control.py index a0246fdcc..a735bce4f 100755 --- a/cli/simple-control.py +++ b/cli/api-control.py @@ -132,7 +132,7 @@ def generate(args): # pylint: disable=redefined-outer-name if __name__ == "__main__": - parser = argparse.ArgumentParser(description = 'simple-img2img') + parser = argparse.ArgumentParser(description = 'api-img2img') parser.add_argument('--init', required=False, default=None, help='init image') parser.add_argument('--input', required=False, default=None, help='input image') parser.add_argument('--mask', required=False, help='mask image') diff --git a/cli/api-faceid.py b/cli/api-faceid.py new file mode 100755 index 000000000..dd9645cea --- /dev/null +++ b/cli/api-faceid.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +import os +import io +import time +import base64 +import logging +import argparse +import requests +import urllib3 +from PIL import Image + +sd_url = os.environ.get('SDAPI_URL', "http://127.0.0.1:7860") +sd_username = os.environ.get('SDAPI_USR', None) +sd_password = os.environ.get('SDAPI_PWD', None) + +logging.basicConfig(level = logging.INFO, format = '%(asctime)s %(levelname)s: %(message)s') +log = logging.getLogger(__name__) +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +options = { + "save_images": False, + "send_images": True, +} + + +def auth(): + if sd_username is not None and sd_password is not None: + return requests.auth.HTTPBasicAuth(sd_username, sd_password) + return None + + +def post(endpoint: str, dct: dict = None): + req = requests.post(f'{sd_url}{endpoint}', json = dct, timeout=300, verify=False, auth=auth()) + if req.status_code != 200: + return { 'error': req.status_code, 'reason': req.reason, 'url': req.url } + else: + return req.json() + + +def encode(f): + image = Image.open(f) + if image.mode == 'RGBA': + image = image.convert('RGB') + with io.BytesIO() as stream: + image.save(stream, 'JPEG') + image.close() + values = stream.getvalue() + encoded = base64.b64encode(values).decode() + return encoded + + +def generate(args): # pylint: disable=redefined-outer-name + t0 = time.time() + if args.model is not None: + post('/sdapi/v1/options', { 'sd_model_checkpoint': args.model }) + post('/sdapi/v1/reload-checkpoint') # needed if running in api-only to trigger new model load + options['prompt'] = args.prompt + options['negative_prompt'] = args.negative + options['steps'] = int(args.steps) + options['seed'] = int(args.seed) + options['sampler_name'] = args.sampler + options['width'] = args.width + options['height'] = args.height + options['face'] = { + 'mode': 'FaceID', + 'ip_model': 'FaceID Base', + 'source_images': [encode(args.face)], + } + data = post('/sdapi/v1/txt2img', options) + t1 = time.time() + if 'images' in data: + for i in range(len(data['images'])): + b64 = data['images'][i].split(',',1)[0] + info = data['info'] + image = Image.open(io.BytesIO(base64.b64decode(b64))) + log.info(f'received image: size={image.size} time={t1-t0:.2f} info="{info}"') + if args.output: + image.save(args.output) + log.info(f'image saved: size={image.size} filename={args.output}') + + else: + log.warning(f'no images received: {data}') + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description = 'api-faceid') + parser.add_argument('--width', required=False, default=512, help='image width') + parser.add_argument('--height', required=False, default=512, help='image height') + parser.add_argument('--face', required=False, help='face image') + parser.add_argument('--prompt', required=False, default='', help='prompt text') + parser.add_argument('--negative', required=False, default='', help='negative prompt text') + parser.add_argument('--steps', required=False, default=20, help='number of steps') + parser.add_argument('--seed', required=False, default=-1, help='initial seed') + parser.add_argument('--sampler', required=False, default='Euler a', help='sampler name') + parser.add_argument('--output', required=False, default=None, help='output image file') + parser.add_argument('--model', required=False, help='model name') + args = parser.parse_args() + log.info(f'img2img: {args}') + generate(args) + +""" +request.face.mode, +request.face.source_images, +request.face.ip_model, +request.face.ip_override_sampler, +request.face.ip_cache_model, +request.face.ip_strength, +request.face.ip_structure, +request.face.id_strength, +request.face.id_conditioning, +request.face.id_cache, +request.face.pm_trigger, +request.face.pm_strength, +request.face.pm_start, +request.face.fs_cache +""" diff --git a/cli/simple-img2img.py b/cli/api-img2img.py similarity index 98% rename from cli/simple-img2img.py rename to cli/api-img2img.py index 7cbfa14b7..3a2961e5b 100755 --- a/cli/simple-img2img.py +++ b/cli/api-img2img.py @@ -83,7 +83,7 @@ def generate(args): # pylint: disable=redefined-outer-name if __name__ == "__main__": - parser = argparse.ArgumentParser(description = 'simple-img2img') + parser = argparse.ArgumentParser(description = 'api-img2img') parser.add_argument('--init', required=True, help='init image') parser.add_argument('--mask', required=False, help='mask image') parser.add_argument('--prompt', required=False, default='', help='prompt text') diff --git a/cli/simple-info.py b/cli/api-info.py similarity index 96% rename from cli/simple-info.py rename to cli/api-info.py index 4d1fd6d75..83e4dfe2e 100755 --- a/cli/simple-info.py +++ b/cli/api-info.py @@ -50,7 +50,7 @@ def info(args): # pylint: disable=redefined-outer-name if __name__ == "__main__": - parser = argparse.ArgumentParser(description = 'simple-info') + parser = argparse.ArgumentParser(description = 'api-info') parser.add_argument('--input', required=True, help='input image') args = parser.parse_args() log.info(f'info: {args}') diff --git a/cli/simple-mask.py b/cli/api-mask.py similarity index 97% rename from cli/simple-mask.py rename to cli/api-mask.py index 2ea12234e..0a1372138 100755 --- a/cli/simple-mask.py +++ b/cli/api-mask.py @@ -73,7 +73,7 @@ def info(args): # pylint: disable=redefined-outer-name if __name__ == "__main__": - parser = argparse.ArgumentParser(description = 'simple-info') + parser = argparse.ArgumentParser(description = 'api-mask') parser.add_argument('--input', required=True, help='input image') parser.add_argument('--mask', required=False, help='input mask') parser.add_argument('--type', required=False, help='output mask type') diff --git a/cli/simple-preprocess.py b/cli/api-preprocess.py similarity index 97% rename from cli/simple-preprocess.py rename to cli/api-preprocess.py index 2b96750bf..084f6a0b4 100755 --- a/cli/simple-preprocess.py +++ b/cli/api-preprocess.py @@ -67,7 +67,7 @@ def info(args): # pylint: disable=redefined-outer-name if __name__ == "__main__": - parser = argparse.ArgumentParser(description = 'simple-info') + parser = argparse.ArgumentParser(description = 'api-preprocess') parser.add_argument('--input', required=True, help='input image') parser.add_argument('--model', required=True, help='preprocessing model') parser.add_argument('--output', required=False, help='output image') diff --git a/cli/idle.py b/cli/api-progress.py similarity index 100% rename from cli/idle.py rename to cli/api-progress.py diff --git a/cli/simple-txt2img.js b/cli/api-txt2img.js similarity index 100% rename from cli/simple-txt2img.js rename to cli/api-txt2img.js diff --git a/cli/simple-txt2img.py b/cli/api-txt2img.py similarity index 93% rename from cli/simple-txt2img.py rename to cli/api-txt2img.py index d3287ee46..a00515fe5 100755 --- a/cli/simple-txt2img.py +++ b/cli/api-txt2img.py @@ -48,7 +48,10 @@ def generate(args): # pylint: disable=redefined-outer-name options['sampler_name'] = args.sampler options['width'] = int(args.width) options['height'] = int(args.height) - options['restore_faces'] = args.faces + if args.faces: + options['restore_faces'] = args.faces + options['denoising_strength'] = 0.5 + options['hr_sampler_name'] = args.sampler data = post('/sdapi/v1/txt2img', options) t1 = time.time() if 'images' in data: @@ -65,7 +68,7 @@ def generate(args): # pylint: disable=redefined-outer-name if __name__ == "__main__": - parser = argparse.ArgumentParser(description = 'simple-txt2img') + parser = argparse.ArgumentParser(description = 'api-txt2img') parser.add_argument('--prompt', required=False, default='', help='prompt text') parser.add_argument('--negative', required=False, default='', help='negative prompt text') parser.add_argument('--width', required=False, default=512, help='image width') diff --git a/cli/simple-upscale.py b/cli/api-upscale.py similarity index 97% rename from cli/simple-upscale.py rename to cli/api-upscale.py index b5a2bb5dd..082e008a8 100755 --- a/cli/simple-upscale.py +++ b/cli/api-upscale.py @@ -80,7 +80,7 @@ def upscale(args): # pylint: disable=redefined-outer-name if __name__ == "__main__": - parser = argparse.ArgumentParser(description = 'simple-upscale') + parser = argparse.ArgumentParser(description = 'api-upscale') parser.add_argument('--input', required=True, help='input image') parser.add_argument('--output', required=True, help='output image') parser.add_argument('--upscaler', required=False, default='Nearest', help='upscaler name') diff --git a/cli/simple-vqa.py b/cli/api-vqa.py similarity index 96% rename from cli/simple-vqa.py rename to cli/api-vqa.py index 0ac181b7c..73de8dbc8 100755 --- a/cli/simple-vqa.py +++ b/cli/api-vqa.py @@ -55,7 +55,7 @@ def info(args): # pylint: disable=redefined-outer-name if __name__ == "__main__": - parser = argparse.ArgumentParser(description = 'simple-info') + parser = argparse.ArgumentParser(description = 'api-vqa') parser.add_argument('--input', required=True, help='input image') parser.add_argument('--model', required=False, help='vqa model') parser.add_argument('--question', required=False, help='question') diff --git a/cli/latents.py b/cli/latents.py deleted file mode 100755 index 717f17352..000000000 --- a/cli/latents.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python - -import os -import sys -import json -import pathlib -import argparse -import warnings - -import cv2 -import numpy as np -import torch -from PIL import Image -from torchvision import transforms -from tqdm import tqdm -from util import Map - -from rich.pretty import install as pretty_install -from rich.traceback import install as traceback_install -from rich.console import Console - -console = Console(log_time=True, log_time_format='%H:%M:%S-%f') -pretty_install(console=console) -traceback_install(console=console, extra_lines=1, width=console.width, word_wrap=False, indent_guides=False) - -sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'modules', 'lora')) -import library.model_util as model_util -import library.train_util as train_util - -warnings.filterwarnings('ignore') -device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') -options = Map({ - 'batch': 1, - 'input': '', - 'json': '', - 'max': 1024, - 'min': 256, - 'noupscale': False, - 'precision': 'fp32', - 'resolution': '512,512', - 'steps': 64, - 'vae': 'stabilityai/sd-vae-ft-mse' -}) -vae = None - - -def get_latents(local_vae, images, weight_dtype): - image_transforms = transforms.Compose([ transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) - img_tensors = [image_transforms(image) for image in images] - img_tensors = torch.stack(img_tensors) - img_tensors = img_tensors.to(device, weight_dtype) - with torch.no_grad(): - latents = local_vae.encode(img_tensors).latent_dist.sample().float().to('cpu').numpy() - return latents, [images[0].shape[0], images[0].shape[1]] - - -def get_npz_filename_wo_ext(data_dir, image_key): - return os.path.join(data_dir, os.path.splitext(os.path.basename(image_key))[0]) - - -def create_vae_latents(local_params): - args = Map({**options, **local_params}) - console.log(f'create vae latents args: {args}') - image_paths = train_util.glob_images(args.input) - if os.path.exists(args.json): - with open(args.json, 'rt', encoding='utf-8') as f: - metadata = json.load(f) - else: - return - if args.precision == 'fp16': - weight_dtype = torch.float16 - elif args.precision == 'bf16': - weight_dtype = torch.bfloat16 - else: - weight_dtype = torch.float32 - global vae # pylint: disable=global-statement - if vae is None: - vae = model_util.load_vae(args.vae, weight_dtype) - vae.eval() - vae.to(device, dtype=weight_dtype) - max_reso = tuple([int(t) for t in args.resolution.split(',')]) - assert len(max_reso) == 2, f'illegal resolution: {args.resolution}' - bucket_manager = train_util.BucketManager(args.noupscale, max_reso, args.min, args.max, args.steps) - if not args.noupscale: - bucket_manager.make_buckets() - img_ar_errors = [] - def process_batch(is_last): - for bucket in bucket_manager.buckets: - if (is_last and len(bucket) > 0) or len(bucket) >= args.batch: - latents, original_size = get_latents(vae, [img for _, img in bucket], weight_dtype) - assert latents.shape[2] == bucket[0][1].shape[0] // 8 and latents.shape[3] == bucket[0][1].shape[1] // 8, f'latent shape {latents.shape}, {bucket[0][1].shape}' - for (image_key, _), latent in zip(bucket, latents): - npz_file_name = get_npz_filename_wo_ext(args.input, image_key) - # np.savez(npz_file_name, latent) - kwargs = {} - np.savez( - npz_file_name, - latents=latent, - original_size=np.array(original_size), - crop_ltrb=np.array([0, 0]), - **kwargs, - ) - bucket.clear() - data = [[(None, ip)] for ip in image_paths] - bucket_counts = {} - for data_entry in tqdm(data, smoothing=0.0): - if data_entry[0] is None: - continue - img_tensor, image_path = data_entry[0] - if img_tensor is not None: - image = transforms.functional.to_pil_image(img_tensor) - else: - image = Image.open(image_path) - image_key = os.path.basename(image_path) - image_key = os.path.join(os.path.basename(pathlib.Path(image_path).parent), pathlib.Path(image_path).stem) - if image_key not in metadata: - metadata[image_key] = {} - reso, resized_size, ar_error = bucket_manager.select_bucket(image.width, image.height) - img_ar_errors.append(abs(ar_error)) - bucket_counts[reso] = bucket_counts.get(reso, 0) + 1 - metadata[image_key]['train_resolution'] = (reso[0] - reso[0] % 8, reso[1] - reso[1] % 8) - if not args.noupscale: - assert resized_size[0] == reso[0] or resized_size[1] == reso[1], f'internal error, resized size not match: {reso}, {resized_size}, {image.width}, {image.height}' - assert resized_size[0] >= reso[0] and resized_size[1] >= reso[1], f'internal error, resized size too small: {reso}, {resized_size}, {image.width}, {image.height}' - assert resized_size[0] >= reso[0] and resized_size[1] >= reso[1], f'internal error resized size is small: {resized_size}, {reso}' - image = np.array(image) - if resized_size[0] != image.shape[1] or resized_size[1] != image.shape[0]: - image = cv2.resize(image, resized_size, interpolation=cv2.INTER_AREA) - if resized_size[0] > reso[0]: - trim_size = resized_size[0] - reso[0] - image = image[:, trim_size//2:trim_size//2 + reso[0]] - if resized_size[1] > reso[1]: - trim_size = resized_size[1] - reso[1] - image = image[trim_size//2:trim_size//2 + reso[1]] - assert image.shape[0] == reso[1] and image.shape[1] == reso[0], f'internal error, illegal trimmed size: {image.shape}, {reso}' - bucket_manager.add_image(reso, (image_key, image)) - process_batch(False) - - process_batch(True) - vae.to('cpu') - - bucket_manager.sort() - img_ar_errors = np.array(img_ar_errors) - for i, reso in enumerate(bucket_manager.resos): - count = bucket_counts.get(reso, 0) - if count > 0: - console.log(f'vae latents bucket: {i+1}/{len(bucket_manager.resos)} resolution: {reso} images: {count} mean-ar-error: {np.mean(img_ar_errors)}') - with open(args.json, 'wt', encoding='utf-8') as f: - json.dump(metadata, f, indent=2) - - -def unload_vae(): - global vae # pylint: disable=global-statement - vae = None - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('input', type=str, help='directory for train images') - parser.add_argument('--json', type=str, required=True, help='metadata file to input') - parser.add_argument('--vae', type=str, required=True, help='model name or path to encode latents') - parser.add_argument('--batch', type=int, default=1, help='batch size in inference') - parser.add_argument('--resolution', type=str, default='512,512', help='max resolution in fine tuning (width,height)') - parser.add_argument('--min', type=int, default=256, help='minimum resolution for buckets') - parser.add_argument('--max', type=int, default=1024, help='maximum resolution for buckets') - parser.add_argument('--steps', type=int, default=64, help='steps of resolution for buckets, divisible by 8') - parser.add_argument('--noupscale', action='store_true', help='make bucket for each image without upscaling') - parser.add_argument('--precision', type=str, default='fp32', choices=['fp32', 'fp16', 'bf16'], help='use precision') - params = parser.parse_args() - create_vae_latents(vars(params)) diff --git a/cli/model-jit.py b/cli/model-jit.py deleted file mode 100755 index e4af79e95..000000000 --- a/cli/model-jit.py +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env python -import os -import time -import functools -import argparse -import logging -import warnings -from dataclasses import dataclass - -logging.getLogger("DeepSpeed").disabled = True -warnings.filterwarnings(action="ignore", category=FutureWarning) -warnings.filterwarnings(action="ignore", category=DeprecationWarning) - -import torch -import diffusers - -n_warmup = 5 -n_traces = 10 -n_runs = 100 -args = {} -pipe = None -log = logging.getLogger("sd") - - -def setup_logging(): - from rich.theme import Theme - from rich.logging import RichHandler - from rich.console import Console - from rich.traceback import install - log.setLevel(logging.DEBUG) - console = Console(log_time=True, log_time_format='%H:%M:%S-%f', theme=Theme({ "traceback.border": "black", "traceback.border.syntax_error": "black", "inspect.value.border": "black" })) - logging.basicConfig(level=logging.ERROR, format='%(asctime)s | %(name)s | %(levelname)s | %(module)s | %(message)s', handlers=[logging.NullHandler()]) # redirect default logger to null - rh = RichHandler(show_time=True, omit_repeated_times=False, show_level=True, show_path=False, markup=False, rich_tracebacks=True, log_time_format='%H:%M:%S-%f', level=logging.DEBUG, console=console) - rh.setLevel(logging.DEBUG) - log.addHandler(rh) - logging.getLogger("diffusers").setLevel(logging.ERROR) - logging.getLogger("torch").setLevel(logging.ERROR) - warnings.filterwarnings(action="ignore", category=torch.jit.TracerWarning) - install(console=console, extra_lines=1, max_frames=10, width=console.width, word_wrap=False, indent_guides=False, suppress=[]) - - -def generate_inputs(): - if args.type == 'sd15': - sample = torch.randn(2, 4, 64, 64).half().cuda() - timestep = torch.rand(1).half().cuda() * 999 - encoder_hidden_states = torch.randn(2, 77, 768).half().cuda() - return sample, timestep, encoder_hidden_states - if args.type == 'sdxl': - sample = torch.randn(2, 4, 64, 64).half().cuda() - timestep = torch.rand(1).half().cuda() * 999 - encoder_hidden_states = torch.randn(2, 77, 768).half().cuda() - text_embeds = torch.randn(1, 77, 2048).half().cuda() - return sample, timestep, encoder_hidden_states, text_embeds - - -def load_model(): - log.info(f'versions: torch={torch.__version__} diffusers={diffusers.__version__}') - diffusers_load_config = { - "low_cpu_mem_usage": True, - "torch_dtype": torch.float16, - "safety_checker": None, - "requires_safety_checker": False, - "load_connected_pipeline": True, - "use_safetensors": True, - } - pipeline = diffusers.StableDiffusionPipeline if args.type == 'sd15' else diffusers.StableDiffusionXLPipeline - global pipe # pylint: disable=global-statement - t0 = time.time() - pipe = pipeline.from_single_file(args.model, **diffusers_load_config).to('cuda') - size = os.path.getsize(args.model) - log.info(f'load: model={args.model} type={args.type} time={time.time() - t0:.3f}s size={size / 1024 / 1024:.3f}mb') - - -def load_trace(fn: str): - - @dataclass - class UNet2DConditionOutput: - sample: torch.FloatTensor - - class TracedUNet(torch.nn.Module): - def __init__(self): - super().__init__() - self.in_channels = pipe.unet.in_channels - self.device = pipe.unet.device - - def forward(self, latent_model_input, t, encoder_hidden_states): - sample = unet_traced(latent_model_input, t, encoder_hidden_states)[0] - return UNet2DConditionOutput(sample=sample) - - t0 = time.time() - unet_traced = torch.jit.load(fn) - pipe.unet = TracedUNet() - size = os.path.getsize(fn) - log.info(f'load: optimized={fn} time={time.time() - t0:.3f}s size={size / 1024 / 1024:.3f}mb') - - -def trace_model(): - log.info(f'tracing model: {args.model}') - torch.set_grad_enabled(False) - unet = pipe.unet - unet.eval() - # unet.to(memory_format=torch.channels_last) # use channels_last memory format - unet.forward = functools.partial(unet.forward, return_dict=False) # set return_dict=False as default - - # warmup - t0 = time.time() - for _ in range(n_warmup): - with torch.inference_mode(): - inputs = generate_inputs() - _output = unet(*inputs) - log.info(f'warmup: time={time.time() - t0:.3f}s passes={n_warmup}') - - # trace - t0 = time.time() - unet_traced = torch.jit.trace(unet, inputs, check_trace=True) - unet_traced.eval() - log.info(f'trace: time={time.time() - t0:.3f}s') - - # optimize graph - t0 = time.time() - for _ in range(n_traces): - with torch.inference_mode(): - inputs = generate_inputs() - _output = unet_traced(*inputs) - log.info(f'optimize: time={time.time() - t0:.3f}s passes={n_traces}') - - # save the model - if args.save: - t0 = time.time() - basename, _ext = os.path.splitext(args.model) - fn = f"{basename}.pt" - unet_traced.save(fn) - size = os.path.getsize(fn) - log.info(f'save: optimized={fn} time={time.time() - t0:.3f}s size={size / 1024 / 1024:.3f}mb') - return fn - - pipe.unet = unet_traced - return None - - -def benchmark_model(msg: str): - with torch.inference_mode(): - inputs = generate_inputs() - torch.cuda.synchronize() - for n in range(n_runs): - if n > n_runs / 10: - t0 = time.time() - _output = pipe.unet(*inputs) - torch.cuda.synchronize() - t1 = time.time() - log.info(f"benchmark unet: {t1 - t0:.3f}s passes={n_runs} type={msg}") - return t1 - t0 - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description = 'SD.Next') - parser.add_argument('--model', type=str, default='', required=True, help='model path') - parser.add_argument('--type', type=str, default='sd15', choices=['sd15', 'sdxl'], required=False, help='model type, default: %(default)s') - parser.add_argument('--benchmark', default = False, action='store_true', help = "run benchmarks, default: %(default)s") - parser.add_argument('--trace', default = True, action='store_true', help = "run jit tracing, default: %(default)s") - parser.add_argument('--save', default = False, action='store_true', help = "save optimized unet, default: %(default)s") - args = parser.parse_args() - setup_logging() - log.info('sdnext model jit tracing') - if not os.path.isfile(args.model): - log.error(f"invalid model path: {args.model}") - exit(1) - load_model() - if args.benchmark: - time0 = benchmark_model('original') - unet_saved = trace_model() - if unet_saved is not None: - load_trace(unet_saved) - if args.benchmark: - time1 = benchmark_model('traced') - log.info(f'benchmark speedup: {100 * (time0 - time1) / time0:.3f}%') diff --git a/cli/torch-compile.py b/cli/torch-compile.py deleted file mode 100755 index 891f27dc5..000000000 --- a/cli/torch-compile.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python -# pylint: disable=cell-var-from-loop -""" -Test Torch Dynamo functionality and backends -""" -import json -import warnings - -import numpy as np -import torch -from torchvision.models import resnet18 - - -print('torch:', torch.__version__) -try: - # must be imported explicitly or namespace is not found - import torch._dynamo as dynamo # pylint: disable=ungrouped-imports -except Exception as err: - print('torch without dynamo support', err) - - -N_ITERS = 20 -torch._dynamo.config.verbose=True # pylint: disable=protected-access -warnings.filterwarnings('ignore', category=UserWarning) # disable those for now as many backends reports tons -# torch.set_float32_matmul_precision('high') # enable to test in fp32 - - -def timed(fn): # returns the result of running `fn()` and the time it took for `fn()` to run in ms using CUDA events - start = torch.cuda.Event(enable_timing=True) - end = torch.cuda.Event(enable_timing=True) - start.record() - result = fn() - end.record() - torch.cuda.synchronize() - return result, start.elapsed_time(end) - - -def generate_data(b): - return ( - torch.randn(b, 3, 128, 128).to(torch.float32).cuda(), - torch.randint(1000, (b,)).cuda(), - ) - - -def init_model(): - return resnet18().to(torch.float32).cuda() - - -def evaluate(mod, val): - return mod(val) - - -if __name__ == '__main__': - # first pass, dynamo is going to be slower as it compiles - model = init_model() - inp = generate_data(16)[0] - - # repeat test - results = {} - times = [] - print('eager initial eval:', timed(lambda: evaluate(model, inp))[1]) - for _i in range(N_ITERS): - inp = generate_data(16)[0] - _res, time = timed(lambda: evaluate(model, inp)) # noqa: B023 - times.append(time) - results['default'] = np.median(times) - - print('dynamo available backends:', dynamo.list_backends()) - for backend in dynamo.list_backends(): - try: - # required before changing backends - torch._dynamo.reset() # pylint: disable=protected-access - eval_dyn = dynamo.optimize(backend)(evaluate) - print('dynamo initial eval:', backend, timed(lambda: eval_dyn(model, inp))[1]) # noqa: B023 - times = [] - for _i in range(N_ITERS): - inp = generate_data(16)[0] - _res, time = timed(lambda: eval_dyn(model, inp)) # noqa: B023 - times.append(time) - results[backend] = np.median(times) - except Exception as err: - lines = str(err).split('\n') - print('dyanmo backend failed:', backend, lines[0]) # print just first error line as backtraces can be quite long - results[backend] = 'error' - - # print stats - print(json.dumps(results, indent = 4)) - -""" -Reference: -Training & Inference backends: - dynamo.optimize("inductor") - Uses TorchInductor backend with AotAutograd and cudagraphs by leveraging codegened Triton kernels - dynamo.optimize("aot_nvfuser") - nvFuser with AotAutograd - dynamo.optimize("aot_cudagraphs") - cudagraphs with AotAutograd -Inference-only backends: - dynamo.optimize("ofi") - Uses Torchscript optimize_for_inference - dynamo.optimize("fx2trt") - Uses Nvidia TensorRT for inference optimizations - dynamo.optimize("onnxrt") - Uses ONNXRT for inference on CPU/GPU -""" diff --git a/cli/train.py b/cli/train.py deleted file mode 100755 index 9e551ddf5..000000000 --- a/cli/train.py +++ /dev/null @@ -1,443 +0,0 @@ -#!/usr/bin/env python - -""" -Examples: -- sd15: train.py --type lora --tag girl --comments sdnext --input ~/generative/Input/mia --process original,interrogate,resize --name mia -- sdxl: train.py --type lora --tag girl --comments sdnext --input ~/generative/Input/mia --process original,interrogate,resize --precision fp32 --optimizer Adafactor --sdxl --name miaxl -- offline: train.py --type lora --tag girl --comments sdnext --input ~/generative/Input/mia --model /home/vlado/dev/sdnext/models/Stable-diffusion/sdxl/miaanimeSFWNSFWSDXL_v40.safetensors --dir /home/vlado/dev/sdnext/models/Lora/ --precision fp32 --optimizer Adafactor --sdxl --name miaxl -""" - -# system imports -import os -import re -import gc -import sys -import json -import shutil -import pathlib -import asyncio -import logging -import tempfile -import argparse - -# local imports -import util -import sdapi -import options - - -# globals -args = None -log = logging.getLogger('train') -valid_steps = ['original', 'face', 'body', 'blur', 'range', 'upscale', 'restore', 'interrogate', 'resize', 'square', 'segment'] -log_file = os.path.join(os.path.dirname(__file__), 'train.log') -server_ok = False - -# methods - -def setup_logging(): - from rich.theme import Theme - from rich.logging import RichHandler - from rich.console import Console - from rich.pretty import install as pretty_install - from rich.traceback import install as traceback_install - console = Console(log_time=True, log_time_format='%H:%M:%S-%f', theme=Theme({ - "traceback.border": "black", - "traceback.border.syntax_error": "black", - "inspect.value.border": "black", - })) - # logging.getLogger("urllib3").setLevel(logging.ERROR) - # logging.getLogger("httpx").setLevel(logging.ERROR) - level = logging.DEBUG if args.debug else logging.INFO - logging.basicConfig(level=logging.ERROR, format='%(asctime)s | %(name)s | %(levelname)s | %(module)s | %(message)s', filename=log_file, filemode='a', encoding='utf-8', force=True) - log.setLevel(logging.DEBUG) # log to file is always at level debug for facility `sd` - pretty_install(console=console) - traceback_install(console=console, extra_lines=1, width=console.width, word_wrap=False, indent_guides=False, suppress=[]) - rh = RichHandler(show_time=True, omit_repeated_times=False, show_level=True, show_path=False, markup=False, rich_tracebacks=True, log_time_format='%H:%M:%S-%f', level=level, console=console) - rh.set_name(level) - while log.hasHandlers() and len(log.handlers) > 0: - log.removeHandler(log.handlers[0]) - log.addHandler(rh) - - -def mem_stats(): - gc.collect() - import torch - if torch.cuda.is_available(): - with torch.no_grad(): - torch.cuda.empty_cache() - with torch.cuda.device('cuda'): - torch.cuda.empty_cache() - torch.cuda.ipc_collect() - mem = util.get_memory() - peak = { 'active': mem['gpu-active']['peak'], 'allocated': mem['gpu-allocated']['peak'], 'reserved': mem['gpu-reserved']['peak'] } - log.debug(f"memory cpu: {mem.ram} gpu current: {mem.gpu} gpu peak: {peak}") - - -def parse_args(): - global args # pylint: disable=global-statement - parser = argparse.ArgumentParser(description = 'SD.Next Train') - - group_server = parser.add_argument_group('Server') - group_server.add_argument('--server', type=str, default='http://127.0.0.1:7860', required=False, help='server url, default: %(default)s') - group_server.add_argument('--user', type=str, default=None, required=False, help='server url, default: %(default)s') - group_server.add_argument('--password', type=str, default=None, required=False, help='server url, default: %(default)s') - group_server.add_argument('--dir', type=str, default=None, required=False, help='folder with trained networks, default: use server setting') - - group_main = parser.add_argument_group('Main') - group_main.add_argument('--type', type=str, choices=['embedding', 'ti', 'lora', 'lyco', 'dreambooth', 'hypernetwork'], default=None, required=True, help='training type') - group_main.add_argument('--model', type=str, default='', required=False, help='base model to use for training, default: current loaded model') - group_main.add_argument('--name', type=str, default=None, required=True, help='output filename') - group_main.add_argument('--tag', type=str, default='person', required=False, help='primary tags, default: %(default)s') - group_main.add_argument('--comments', type=str, default='', required=False, help='comments to be added to trained model metadata, default: %(default)s') - - group_data = parser.add_argument_group('Dataset') - group_data.add_argument('--input', type=str, default=None, required=True, help='input folder with training images') - group_data.add_argument('--interim', type=str, default='', required=False, help='where to store processed images, default is system temp/train') - group_data.add_argument('--process', type=str, default='original,interrogate,resize,square', required=False, help=f'list of possible processing steps: {valid_steps}, default: %(default)s') - - group_train = parser.add_argument_group('Train') - group_train.add_argument('--gradient', type=int, default=1, required=False, help='gradient accumulation steps, default: %(default)s') - group_train.add_argument('--steps', type=int, default=2500, required=False, help='training steps, default: %(default)s') - group_train.add_argument('--batch', type=int, default=1, required=False, help='batch size, default: %(default)s') - group_train.add_argument('--lr', type=float, default=1e-04, required=False, help='model learning rate, default: %(default)s') - group_train.add_argument('--dim', type=int, default=32, required=False, help='network dimension or number of vectors, default: %(default)s') - - # lora params - group_train.add_argument('--repeats', type=int, default=1, required=False, help='number of repeats per image, default: %(default)s') - group_train.add_argument('--alpha', type=float, default=0, required=False, help='lora/lyco alpha for weights scaling, default: dim/2') - group_train.add_argument('--algo', type=str, default=None, choices=['locon', 'loha', 'lokr', 'ia3'], required=False, help='alternative lyco algoritm, default: %(default)s') - group_train.add_argument('--args', type=str, default=None, required=False, help='lora/lyco additional network arguments, default: %(default)s') - group_train.add_argument('--optimizer', type=str, default='AdamW', required=False, help='optimizer type, default: %(default)s') - group_train.add_argument('--precision', type=str, choices=['fp16', 'fp32'], default='fp16', required=False, help='training precision, default: %(default)s') - group_train.add_argument('--sdxl', default = False, action='store_true', help = "run sdxl training, default: %(default)s") - # AdamW (default), AdamW8bit, PagedAdamW8bit, Lion8bit, PagedLion8bit, Lion, SGDNesterov, SGDNesterov8bit, DAdaptation(DAdaptAdamPreprint), DAdaptAdaGrad, DAdaptAdam, DAdaptAdan, DAdaptAdanIP, DAdaptLion, DAdaptSGD, AdaFactor - - group_other = parser.add_argument_group('Other') - group_other.add_argument('--overwrite', default = False, action='store_true', help = "overwrite existing training, default: %(default)s") - group_other.add_argument('--experimental', default = False, action='store_true', help = "enable experimental options, default: %(default)s") - group_other.add_argument('--debug', default = False, action='store_true', help = "enable debug level logging, default: %(default)s") - - args = parser.parse_args() - - -def prepare_server(): - global server_ok # pylint: disable=global-statement - try: - server_status = util.Map(sdapi.progresssync()) - server_state = server_status['state'] - server_ok = True - except Exception: - log.warning(f'sdnext server error: {server_status}') - server_ok = False - if server_ok and server_state['job_count'] > 0: - log.error(f'sdnext server not idle: {server_state}') - exit(1) - if server_ok: - server_options = util.Map(sdapi.options()) - server_options.options.save_training_settings_to_txt = False - server_options.options.training_enable_tensorboard = False - server_options.options.training_tensorboard_save_images = False - server_options.options.pin_memory = True - server_options.options.save_optimizer_state = False - server_options.options.training_image_repeats_per_epoch = args.repeats - server_options.options.training_write_csv_every = 0 - sdapi.postsync('/sdapi/v1/options', server_options.options) - log.info('updated server options') - - -def verify_args(): - server_options = util.Map(sdapi.options()) - if args.model != '': - if not os.path.isfile(args.model): - log.error(f'cannot find loaded model: {args.model}') - exit(1) - if server_ok: - server_options.options.sd_model_checkpoint = args.model - sdapi.postsync('/sdapi/v1/options', server_options.options) - elif server_ok: - args.model = server_options.options.sd_model_checkpoint.split(' [')[0] - if args.sdxl and (server_options.sd_backend != 'diffusers' or server_options.diffusers_pipeline != 'Stable Diffusion XL'): - log.warning('server checkpoint is not sdxl') - else: - log.error('no model specified') - exit(1) - base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - if args.type == 'lora' and not server_ok and not args.dir: - log.error('offline lora training requires --dir ') - exit(1) - if args.type == 'lora': - import transformers - if transformers.__version__ != '4.30.2': - log.error(f'lora training requires specific transformers version: current {transformers.__version__} required transformers==4.30.2') - exit(1) - args.lora_dir = server_options.options.lora_dir or args.dir - if not os.path.isabs(args.lora_dir): - args.lora_dir = os.path.join(base_dir, args.lora_dir) - args.lyco_dir = server_options.options.lyco_dir or args.dir - if not os.path.isabs(args.lyco_dir): - args.lyco_dir = os.path.join(base_dir, args.lyco_dir) - args.embeddings_dir = server_options.options.embeddings_dir or args.dir - if not os.path.isfile(args.model): - args.ckpt_dir = server_options.options.ckpt_dir - if not os.path.isabs(args.ckpt_dir): - args.ckpt_dir = os.path.join(base_dir, args.ckpt_dir) - attempt = os.path.abspath(os.path.join(args.ckpt_dir, args.model)) - args.model = attempt if os.path.isfile(attempt) else args.model - if not os.path.isfile(args.model): - attempt = os.path.abspath(os.path.join(args.ckpt_dir, args.model + '.safetensors')) - args.model = attempt if os.path.isfile(attempt) else args.model - if not os.path.isfile(args.model): - log.error(f'cannot find loaded model: {args.model}') - exit(1) - if not os.path.exists(args.input) or not os.path.isdir(args.input): - log.error(f'cannot find training folder: {args.input}') - exit(1) - if not os.path.exists(args.lora_dir) or not os.path.isdir(args.lora_dir): - log.error(f'cannot find lora folder: {args.lora_dir}') - exit(1) - if not os.path.exists(args.lyco_dir) or not os.path.isdir(args.lyco_dir): - log.error(f'cannot find lyco folder: {args.lyco_dir}') - exit(1) - if args.interim != '': - args.process_dir = args.interim - else: - args.process_dir = os.path.join(tempfile.gettempdir(), 'train', args.name) - log.debug(f'args: {vars(args)}') - log.debug(f'server flags: {server_options.flags}') - log.debug(f'server options: {server_options.options}') - - -async def training_loop(): - async def async_train(): - res = await sdapi.post('/sdapi/v1/train/embedding', options.embedding) - log.info(f'train embedding result: {res}') - - async def async_monitor(): - from tqdm.rich import tqdm - await asyncio.sleep(3) - res = util.Map(sdapi.progress()) - with tqdm(desc='train embedding', total=res.state.job_count) as pbar: - while res.state.job_no < res.state.job_count and not res.state.interrupted and not res.state.skipped: - await asyncio.sleep(2) - prev_job = res.state.job_no - res = util.Map(sdapi.progress()) - loss = re.search(r"Loss: (.*?)(?=\<)", res.textinfo) - if loss: - pbar.set_postfix({ 'loss': loss.group(0) }) - pbar.update(res.state.job_no - prev_job) - - a = asyncio.create_task(async_train()) - b = asyncio.create_task(async_monitor()) - await asyncio.gather(a, b) # wait for both pipeline and monitor to finish - - -def train_embedding(): - log.info(f'{args.type} options: {options.embedding}') - create_options = util.Map({ - "name": args.name, - "num_vectors_per_token": args.dim, - "overwrite_old": False, - "init_text": args.tag, - }) - fn = os.path.join(args.embeddings_dir, args.name) + '.pt' - if os.path.exists(fn) and args.overwrite: - log.warning(f'delete existing embedding {fn}') - os.remove(fn) - else: - log.error(f'embedding exists {fn}') - return - log.info(f'create embedding {create_options}') - res = sdapi.postsync('/sdapi/v1/create/embedding', create_options) - if 'info' in res and 'error' in res['info']: # formatted error - log.error(res.info) - elif 'info' in res: # no error - asyncio.run(training_loop()) - else: # unknown error - log.error(f'create embedding error {res}') - - -def train_lora(): - fn = os.path.join(options.lora.output_dir, args.name) - for ext in ['.ckpt', '.pt', '.safetensors']: - if os.path.exists(fn + ext): - if args.overwrite: - log.warning(f'delete existing lora: {fn + ext}') - os.remove(fn + ext) - else: - log.error(f'lora exists: {fn + ext}') - return - log.info(f'{args.type} options: {options.lora}') - # lora imports - lora_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'modules', 'lora')) - lycoris_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'modules', 'lycoris')) - sys.path.append(lora_path) - if args.type == 'lyco': - sys.path.append(lycoris_path) - log.debug('importing lora lib') - if not args.sdxl: - import train_network - trainer = train_network.NetworkTrainer() - trainer.train(options.lora) - else: - import sdxl_train_network - trainer = sdxl_train_network.SdxlNetworkTrainer() - trainer.train(options.lora) - if args.type == 'lyco': - log.debug('importing lycoris lib') - import importlib - _network_module = importlib.import_module(options.lora.network_module) - - -def prepare_options(): - if args.type == 'embedding': - log.info('train embedding') - options.lora.in_json = None - if args.type == 'dreambooth': - log.info('train using dreambooth style training') - options.lora.vae_batch_size = args.batch - options.lora.in_json = None - if args.type == 'lora': - log.info('train using lora style training') - options.lora.output_dir = args.lora_dir - options.lora.in_json = os.path.join(args.process_dir, args.name + '.json') - if args.type == 'lyco': - log.info('train using lycoris network') - options.lora.output_dir = args.lora_dir - options.lora.network_module = 'lycoris.kohya' - options.lora.in_json = os.path.join(args.process_dir, args.name + '.json') - # lora specific - options.lora.save_model_as = 'safetensors' - options.lora.pretrained_model_name_or_path = args.model - options.lora.output_name = args.name - options.lora.max_train_steps = args.steps - options.lora.network_dim = args.dim - options.lora.network_alpha = args.dim // 2 if args.alpha == 0 else args.alpha - options.lora.network_args = [] - options.lora.training_comment = args.comments - options.lora.sdpa = True - options.lora.optimizer_type = args.optimizer - if args.algo is not None: - options.lora.network_args.append(f'algo={args.algo}') - if args.args is not None: - for net_arg in args.args: - options.lora.network_args.append(net_arg) - options.lora.gradient_accumulation_steps = args.gradient - options.lora.learning_rate = args.lr - options.lora.train_batch_size = args.batch - options.lora.train_data_dir = args.process_dir - options.lora.no_half_vae = args.precision == 'fp16' - # embedding specific - options.embedding.embedding_name = args.name - options.embedding.learn_rate = str(args.lr) - options.embedding.batch_size = args.batch - options.embedding.steps = args.steps - options.embedding.data_root = args.process_dir - options.embedding.log_directory = os.path.join(args.process_dir, 'log') - options.embedding.gradient_step = args.gradient - - -def process_inputs(): - import process - import filetype - pathlib.Path(args.process_dir).mkdir(parents=True, exist_ok=True) - processing_options = args.process.split(',') if isinstance(args.process, str) else args.process - processing_options = [opt.strip() for opt in re.split(',| ', args.process)] - log.info(f'processing steps: {processing_options}') - for step in processing_options: - if step not in valid_steps: - log.error(f'invalid processing step: {[step]}') - exit(1) - for root, _sub_dirs, folder in os.walk(args.input): - files = [os.path.join(root, f) for f in folder if filetype.is_image(os.path.join(root, f))] - log.info(f'processing input images: {len(files)}') - if os.path.exists(args.process_dir): - if args.overwrite: - log.warning(f'removing existing processed folder: {args.process_dir}') - shutil.rmtree(args.process_dir, ignore_errors=True) - else: - log.info(f'processed folder exists: {args.process_dir}') - steps = [step for step in processing_options if step in ['face', 'body', 'original']] - process.reset() - options.process.target_size = 1024 if args.sdxl else 512 - metadata = {} - for step in steps: - if step == 'face': - opts = [step for step in processing_options if step not in ['body', 'original']] - if step == 'body': - opts = [step for step in processing_options if step not in ['face', 'original', 'upscale', 'restore']] # body does not perform upscale or restore - if step == 'original': - opts = [step for step in processing_options if step not in ['face', 'body', 'upscale', 'restore', 'blur', 'range', 'segment']] # original does not perform most steps - log.info(f'processing current step: {opts}') - tag = step - if tag == 'original' and args.tag is not None: - concept = args.tag.split(',')[0].strip() - else: - concept = step - if args.type in ['lora', 'lyco', 'dreambooth']: - folder = os.path.join(args.process_dir, str(args.repeats) + '_' + concept) # separate concepts per folder - if args.type in ['embedding']: - folder = os.path.join(args.process_dir) # everything into same folder - log.info(f'processing concept: {concept}') - log.info(f'processing output folder: {folder}') - pathlib.Path(folder).mkdir(parents=True, exist_ok=True) - results = {} - if server_ok: - for f in files: - res = process.file(filename = f, folder = folder, tag = args.tag, requested = opts) - if res.image: # valid result - results[res.type] = results.get(res.type, 0) + 1 - results['total'] = results.get('total', 0) + 1 - rel_path = res.basename.replace(os.path.commonpath([res.basename, args.process_dir]), '') - if rel_path.startswith(os.path.sep): - rel_path = rel_path[1:] - metadata[rel_path] = { 'caption': res.caption, 'tags': ','.join(res.tags) } - if options.lora.in_json is None: - with open(res.output.replace(options.process.format, '.txt'), "w", encoding='utf-8') as outfile: - outfile.write(res.caption) - log.info(f"processing {'saved' if res.image is not None else 'skipped'}: {f} => {res.output} {res.ops} {res.message}") - else: - log.info('processing skipped: offline') - folders = [os.path.join(args.process_dir, folder) for folder in os.listdir(args.process_dir) if os.path.isdir(os.path.join(args.process_dir, folder))] - log.info(f'input datasets {folders}') - if options.lora.in_json is not None: - with open(options.lora.in_json, "w", encoding='utf-8') as outfile: # write json at the end only - outfile.write(json.dumps(metadata, indent=2)) - for folder in folders: # create latents - import latents - latents.create_vae_latents(util.Map({ 'input': folder, 'json': options.lora.in_json })) - latents.unload_vae() - r = { 'inputs': len(files), 'outputs': results, 'metadata': options.lora.in_json } - log.info(f'processing steps result: {r}') - if args.gradient < 0: - log.info(f"setting gradient accumulation to number of images: {results['total']}") - options.lora.gradient_accumulation_steps = results['total'] - options.embedding.gradient_step = results['total'] - process.unload() - - -if __name__ == '__main__': - parse_args() - setup_logging() - log.info('SD.Next Train') - sdapi.sd_url = args.server - if args.user is not None: - sdapi.sd_username = args.user - if args.password is not None: - sdapi.sd_password = args.password - prepare_server() - verify_args() - prepare_options() - mem_stats() - process_inputs() - mem_stats() - try: - if args.type == 'embedding': - train_embedding() - if args.type == 'lora' or args.type == 'lyco' or args.type == 'dreambooth': - train_lora() - except KeyboardInterrupt: - log.error('interrupt requested') - sdapi.interrupt() - mem_stats() - log.info('done') diff --git a/cli/zluda-python.py b/cli/zluda-python.py index 31ee96362..e0399d096 100644 --- a/cli/zluda-python.py +++ b/cli/zluda-python.py @@ -13,7 +13,7 @@ class Interpreter: def execute(self, s: str): try: - exec(s, self.env_globals, self.env_locals) + exec(s, self.env_globals, self.env_locals) # pylint: disable=exec-used except Exception as e: print(f'{e.__class__.__name__}: {e}') diff --git a/modules/control/run.py b/modules/control/run.py index 5a41b87e4..7b27aed4e 100644 --- a/modules/control/run.py +++ b/modules/control/run.py @@ -193,6 +193,8 @@ def control_run(units: List[unit.Unit] = [], inputs: List[Image.Image] = [], ini p.refiner_negative = refiner_negative if p.enable_hr and (p.hr_resize_x == 0 or p.hr_resize_y == 0): p.hr_upscale_to_x, p.hr_upscale_to_y = 8 * int(p.width * p.hr_scale / 8), 8 * int(p.height * p.hr_scale / 8) + elif p.enable_hr and (p.hr_upscale_to_x == 0 or p.hr_upscale_to_y == 0): + p.hr_upscale_to_x, p.hr_upscale_to_y = 8 * int(p.hr_resize_x / 8), 8 * int(hr_resize_y / 8) global p_extra_args # pylint: disable=global-statement for k, v in p_extra_args.items(): diff --git a/modules/processing_helpers.py b/modules/processing_helpers.py index be04ea8d4..5baf0193f 100644 --- a/modules/processing_helpers.py +++ b/modules/processing_helpers.py @@ -390,6 +390,9 @@ def resize_hires(p, latents): # input=latents output=pil if not latent_upscaler if latent_upscaler is not None: return 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 = processing_vae.vae_decode(latents=latents, model=shared.sd_model, full_quality=p.full_quality, output_type='pil') + if p.hr_upscale_to_x == 0 or p.hr_upscale_to_y == 0 and hasattr(p, 'init_hr'): + shared.log.error('Hires: missing upscaling dimensions') + return first_pass_images resized_images = [] for img in first_pass_images: if latent_upscaler is None: diff --git a/modules/scripts.py b/modules/scripts.py index 59eb4c82b..87a25a56b 100644 --- a/modules/scripts.py +++ b/modules/scripts.py @@ -489,8 +489,10 @@ class ScriptRunner: s = ScriptSummary('before-process') for script in self.alwayson_scripts: try: - script_args = p.script_args[script.args_from:script.args_to] - script.before_process(p, *script_args, **kwargs) + args = p.script_args[script.args_from:script.args_to] + if len(args) == 0: + continue + script.before_process(p, *args, **kwargs) except Exception as e: errors.display(e, f"Error running before process: {script.filename}") s.record(script.title()) @@ -501,6 +503,8 @@ class ScriptRunner: for script in self.alwayson_scripts: try: args = p.per_script_args.get(script.title(), p.script_args[script.args_from:script.args_to]) + if len(args) == 0: + continue script.process(p, *args, **kwargs) except Exception as e: errors.display(e, f'Running script process: {script.filename}') @@ -513,6 +517,8 @@ class ScriptRunner: for script in self.alwayson_scripts: try: args = p.per_script_args.get(script.title(), p.script_args[script.args_from:script.args_to]) + if len(args) == 0: + continue processed = script.process_images(p, *args, **kwargs) except Exception as e: errors.display(e, f'Running script process images: {script.filename}') @@ -525,6 +531,8 @@ class ScriptRunner: for script in self.alwayson_scripts: try: args = p.per_script_args.get(script.title(), p.script_args[script.args_from:script.args_to]) + if len(args) == 0: + continue script.before_process_batch(p, *args, **kwargs) except Exception as e: errors.display(e, f'Running script before process batch: {script.filename}') @@ -536,6 +544,8 @@ class ScriptRunner: for script in self.alwayson_scripts: try: args = p.per_script_args.get(script.title(), p.script_args[script.args_from:script.args_to]) + if len(args) == 0: + continue script.process_batch(p, *args, **kwargs) except Exception as e: errors.display(e, f'Running script process batch: {script.filename}') @@ -547,6 +557,8 @@ class ScriptRunner: for script in self.alwayson_scripts: try: args = p.per_script_args.get(script.title(), p.script_args[script.args_from:script.args_to]) + if len(args) == 0: + continue script.postprocess(p, processed, *args) except Exception as e: errors.display(e, f'Running script postprocess: {script.filename}') @@ -558,6 +570,8 @@ class ScriptRunner: for script in self.alwayson_scripts: try: args = p.per_script_args.get(script.title(), p.script_args[script.args_from:script.args_to]) + if len(args) == 0: + continue script.postprocess_batch(p, *args, images=images, **kwargs) except Exception as e: errors.display(e, f'Running script before postprocess batch: {script.filename}') @@ -569,6 +583,8 @@ class ScriptRunner: for script in self.alwayson_scripts: try: args = p.per_script_args.get(script.title(), p.script_args[script.args_from:script.args_to]) + if len(args) == 0: + continue script.postprocess_batch_list(p, pp, *args, **kwargs) except Exception as e: errors.display(e, f'Running script before postprocess batch list: {script.filename}') @@ -580,6 +596,8 @@ class ScriptRunner: for script in self.alwayson_scripts: try: args = p.per_script_args.get(script.title(), p.script_args[script.args_from:script.args_to]) + if len(args) == 0: + continue script.postprocess_image(p, pp, *args) except Exception as e: errors.display(e, f'Running script postprocess image: {script.filename}') diff --git a/scripts/face-details.py b/scripts/face-details.py index 3604ecb47..b68d197db 100644 --- a/scripts/face-details.py +++ b/scripts/face-details.py @@ -104,11 +104,12 @@ class FaceRestorerYolo(FaceRestoration): return np_image self.load() if self.model is None: - shared.log.error(f"Model load: type=FaceHires model='{self.model_name}' dir={self.model_dir} url={self.model_url}") + shared.log.debug('Face HiRes: model not loaded') return np_image image = Image.fromarray(np_image) faces = self.predict(image) if len(faces) == 0: + shared.log.debug('Face HiRes: no faces detected') return np_image # create backups @@ -140,6 +141,7 @@ class FaceRestorerYolo(FaceRestoration): if args['denoising_strength'] == 0: shared.log.debug('Face HiRes skip: strength=0') control_pipeline = None + orig_class = shared.sd_model.__class__ if getattr(p, 'is_control', False): from modules.control import run control_pipeline = shared.sd_model @@ -177,6 +179,8 @@ class FaceRestorerYolo(FaceRestoration): # restore pipeline if control_pipeline is not None: shared.sd_model = control_pipeline + else: + shared.sd_model.__class__ = orig_class p = processing_class.switch_class(p, orig_cls, orig_p) p.init_images = getattr(orig_p, 'init_images', None) p.image_mask = getattr(orig_p, 'image_mask', None)