diff --git a/CHANGELOG.md b/CHANGELOG.md index c19c9d288..c582ba8d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,34 @@ # Change Log for SD.Next +## Update for 2025-05-08 + +- **Features** + - NNCF: Faster quantization + - Prompt Enhancer: support for *img2img* workflows + where prompt enhancer will first analyze input image and then incorporate user prompt to create enhanced prompt +- **API** + - add `/sdapi/v1/framepack` endpoint with full support for FramePack including all optional settings + see example: `sd-extension-framepack/create-video.py` + - add `/sdapi/v1/checkpoint` endpoint to get info on currently loaded model/checkpoint + see example: `cli/api-checkpoint.py` + - add `/sdapi/v1/prompt-enhance` endpoint to enhance prompt using LLM + see example: `cli/api-enhance.py` + supports text, image and video prompts with or without input image + *note*: if input image is provided, model should be left at default `gemma-3-4b-it` as most other LLMs do not support hybrid workflows +- **Fixes** + - ROCm: disable cuDNN, fixes slow MIOpen tuning with `torch==2.7` + - Extensions: use in-process installer for extensions-builtin, improves startup performance + - FramePack: monkey-patch for dynamically installed `av` + - Logging: reduce spam while progress is active + - LoRA: legacy handler enable/disable + - LoRA: force clear-cache on model unload + - ADetailer: fix enable/disable + ## Update for 2025-05-06 +Minor refesh with several bugfixes and updates to core libraries +Plus new features with **FramePack** and **HiDream-E1** + - **Features** - [FramePack](https://vladmandic.github.io/sdnext-docs/FramePack) add **T2V** mode in addition to **I2V** and **FLF2V** diff --git a/cli/api-checkpoint.py b/cli/api-checkpoint.py new file mode 100755 index 000000000..61f4e4370 --- /dev/null +++ b/cli/api-checkpoint.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +import os +import logging +import requests +import urllib3 + + +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) +options = { + "save_images": True, + "send_images": True, +} + +logging.basicConfig(level = logging.INFO, format = '%(asctime)s %(levelname)s: %(message)s') +log = logging.getLogger(__name__) +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +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 get(endpoint: str, dct: dict = None): + req = requests.get(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() + + +if __name__ == "__main__": + model = get('/sdapi/v1/checkpoint') + log.info(f'api-checkpoint: {model}') diff --git a/cli/api-enhance.py b/cli/api-enhance.py new file mode 100755 index 000000000..fa30d9cb1 --- /dev/null +++ b/cli/api-enhance.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +import os +import io +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) + + +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): + if f is not None and os.path.exists(f): + image = Image.open(f) + if image.mode == 'RGBA': + image = image.convert('RGB') + log.info(f'encoding image: {image}') + with io.BytesIO() as stream: + image.save(stream, 'JPEG') + image.close() + values = stream.getvalue() + encoded = base64.b64encode(values).decode() + return encoded + else: + return None + + +def enhance(args): # pylint: disable=redefined-outer-name + options = { + 'prompt': str(args.prompt), + 'seed': int(args.seed), + 'type': str(args.type), + } + if args.model: + options['model'] = str(args.model) + if args.image: + options['image'] = encode(args.image) + response = post('/sdapi/v1/prompt-enhance', options) + return response + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description = 'api-enhance') + parser.add_argument('--prompt', type=str, default='', required=False, help='prompt') + parser.add_argument('--seed', type=int, default=-1, required=False, help='seed') + parser.add_argument('--type', type=str, default='text', choices=['text', 'image', 'video'], required=False, help='enhance type') + parser.add_argument('--model', type=str, default=None, required=False, help='model name') + parser.add_argument('--image', type=str, default=None, required=False, help='optional input image') + args = parser.parse_args() + log.info(f'api-upscale: {args}') + result = enhance(args) + log.info(result) diff --git a/installer.py b/installer.py index 4a1eafbbe..98eda4e88 100644 --- a/installer.py +++ b/installer.py @@ -71,8 +71,10 @@ control_extensions = [ # 3rd party extensions marked as safe for control ui try: from modules.timer import init ts = init.ts + elapsed = init.elapsed except Exception: ts = lambda *args, **kwargs: None # pylint: disable=unnecessary-lambda-assignment + elapsed = lambda *args, **kwargs: None # pylint: disable=unnecessary-lambda-assignment def get_console(): @@ -977,21 +979,27 @@ def run_extension_installer(folder): if not os.path.isfile(path_installer): return try: - log.debug(f"Extension installer: {path_installer}") - env = os.environ.copy() - env['PYTHONPATH'] = os.path.abspath(".") - if os.environ.get('PYTHONPATH', None) is not None: - seperator = ';' if sys.platform == 'win32' else ':' - env['PYTHONPATH'] += seperator + os.environ.get('PYTHONPATH', None) - result = subprocess.run(f'"{sys.executable}" "{path_installer}"', shell=True, env=env, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=folder) - txt = result.stdout.decode(encoding="utf8", errors="ignore") - debug(f'Extension installer: file="{path_installer}" {txt}') - if result.returncode != 0: - errors.append(f'ext: {os.path.basename(folder)}') - if len(result.stderr) > 0: - txt = txt + '\n' + result.stderr.decode(encoding="utf8", errors="ignore") - log.error(f'Extension installer error: {path_installer}') - log.debug(txt) + is_builtin = 'extensions-builtin' in folder + log.debug(f'Extension installer: builtin={is_builtin} file="{path_installer}"') + if is_builtin: + module_spec = importlib.util.spec_from_file_location(os.path.basename(folder), path_installer) + module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) + else: + env = os.environ.copy() + env['PYTHONPATH'] = os.path.abspath(".") + if os.environ.get('PYTHONPATH', None) is not None: + seperator = ';' if sys.platform == 'win32' else ':' + env['PYTHONPATH'] += seperator + os.environ.get('PYTHONPATH', None) + result = subprocess.run(f'"{sys.executable}" "{path_installer}"', shell=True, env=env, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=folder) + txt = result.stdout.decode(encoding="utf8", errors="ignore") + debug(f'Extension installer: file="{path_installer}" {txt}') + if result.returncode != 0: + errors.append(f'ext: {os.path.basename(folder)}') + if len(result.stderr) > 0: + txt = txt + '\n' + result.stderr.decode(encoding="utf8", errors="ignore") + log.error(f'Extension installer error: {path_installer}') + log.debug(txt) except Exception as e: log.error(f'Extension installer exception: {e}') @@ -1010,7 +1018,6 @@ def list_extensions_folder(folder, quiet=False): # run installer for each installed and enabled extension and optionally update them def install_extensions(force=False): - t_start = time.time() if args.profile: pr = cProfile.Profile() pr.enable() @@ -1020,6 +1027,7 @@ def install_extensions(force=False): from modules.paths import extensions_builtin_dir, extensions_dir extensions_duplicates = [] extensions_enabled = [] + extensions_disabled = [e.lower() for e in opts.get('disabled_extensions', [])] extension_folders = [extensions_builtin_dir] if args.safe else [extensions_builtin_dir, extensions_dir] res = [] for folder in extension_folders: @@ -1028,6 +1036,9 @@ def install_extensions(force=False): extensions = list_extensions_folder(folder, quiet=True) log.debug(f'Extensions all: {extensions}') for ext in extensions: + if os.path.basename(ext).lower() in extensions_disabled: + continue + t_start = time.time() if ext in extensions_enabled: extensions_duplicates.append(ext) continue @@ -1053,13 +1064,14 @@ def install_extensions(force=False): log.info(f'Extension installed packages: {ext} {diff}') except Exception as e: log.error(f'Extension installed unknown package: {e}') + ts(ext, t_start) log.info(f'Extensions enabled: {extensions_enabled}') if len(extensions_duplicates) > 0: log.warning(f'Extensions duplicates: {extensions_duplicates}') if args.profile: pr.disable() print_profile(pr, 'Extensions') - ts('extensions', t_start) + # ts('extensions', t_start) return '\n'.join(res) @@ -1163,7 +1175,7 @@ def install_optional(): install('albumentations==1.4.3', ignore=True) install('pydantic==1.10.21', ignore=True) reload('pydantic', '1.10.21') - install('nncf==2.16.0', ignore=True) # requires older pandas + install('nncf==2.16.0', ignore=True) install('gguf', ignore=True) install('av', ignore=True) try: diff --git a/launch.py b/launch.py index 47d66f47d..0921065ef 100755 --- a/launch.py +++ b/launch.py @@ -25,10 +25,12 @@ skip_install = False # parsed by some extensions try: - from modules.timer import launch + from modules.timer import launch, init rec = launch.record + init_summary = init.summary except Exception: rec = lambda *args, **kwargs: None # pylint: disable=unnecessary-lambda-assignment + init_summary = lambda *args, **kwargs: None # pylint: disable=unnecessary-lambda-assignment def init_args(): @@ -290,6 +292,7 @@ def main(): installer.log.warning(f'See log file for more details: {installer.log_file}') installer.extensions_preload(parser) # adds additional args from extensions args = installer.parse_args(parser) + installer.log.info(f'Installer time: {init_summary()}') get_custom_args() uv, instance = start_server(immediate=True, server=None) @@ -303,8 +306,12 @@ def main(): alive = False requests = 0 t_current = time.time() + t_timestamp = 'none' if float(args.status) > 0 and t_current - t_server > float(args.status): - installer.log.trace(f'Server: alive={alive} requests={requests} memory={get_memory_stats()} {instance.state.status()}') + s = instance.state.status() + if s.timestamp is None or s.timestamp != t_timestamp: # dont spam during active job + installer.log.trace(f'Server: alive={alive} requests={requests} memory={get_memory_stats()} {instance.state.status()}') + t_timestamp = s.timestamp t_server = t_current if float(args.monitor) > 0 and t_current - t_monitor > float(args.monitor): installer.log.trace(f'Monitor: {get_memory_stats(detailed=True)}') diff --git a/modules/api/api.py b/modules/api/api.py index 39210ffca..72a2090a0 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -65,6 +65,7 @@ class Api: self.add_api_route("/sdapi/v1/preprocess", self.process.post_preprocess, methods=["POST"]) self.add_api_route("/sdapi/v1/mask", self.process.post_mask, methods=["POST"]) self.add_api_route("/sdapi/v1/detect", self.process.post_detect, methods=["POST"]) + self.add_api_route("/sdapi/v1/prompt-enhance", self.process.post_prompt_enhance, methods=["POST"], response_model=models.ResPromptEnhance) # api dealing with optional scripts self.add_api_route("/sdapi/v1/scripts", script.get_scripts_list, methods=["GET"], response_model=models.ResScripts) @@ -89,6 +90,7 @@ class Api: self.add_api_route("/sdapi/v1/png-info", endpoints.post_pnginfo, methods=["POST"], response_model=models.ResImageInfo) self.add_api_route("/sdapi/v1/interrogate", endpoints.post_interrogate, methods=["POST"]) self.add_api_route("/sdapi/v1/vqa", endpoints.post_vqa, methods=["POST"]) + self.add_api_route("/sdapi/v1/checkpoint", endpoints.get_checkpoint, methods=["GET"]) self.add_api_route("/sdapi/v1/refresh-checkpoints", endpoints.post_refresh_checkpoints, methods=["POST"]) self.add_api_route("/sdapi/v1/unload-checkpoint", endpoints.post_unload_checkpoint, methods=["POST"]) self.add_api_route("/sdapi/v1/reload-checkpoint", endpoints.post_reload_checkpoint, methods=["POST"]) diff --git a/modules/api/endpoints.py b/modules/api/endpoints.py index 1decd8c71..80b46f324 100644 --- a/modules/api/endpoints.py +++ b/modules/api/endpoints.py @@ -130,6 +130,26 @@ def post_reload_checkpoint(): sd_models.reload_model_weights() return {} +def get_checkpoint(): + if not shared.sd_loaded or shared.sd_model is None: + checkpoint = { + 'type': None, + 'class': None, + } + else: + checkpoint = { + 'type': shared.sd_model_type, + 'class': shared.sd_model.__class__.__name__, + } + if hasattr(shared.sd_model, 'sd_model_checkpoint'): + checkpoint['checkpoint'] = shared.sd_model.sd_model_checkpoint + if hasattr(shared.sd_model, 'sd_checkpoint_info'): + checkpoint['title'] = shared.sd_model.sd_checkpoint_info.title + checkpoint['name'] = shared.sd_model.sd_checkpoint_info.name + checkpoint['filename'] = shared.sd_model.sd_checkpoint_info.filename + checkpoint['hash'] = shared.sd_model.sd_checkpoint_info.shorthash + return checkpoint + def post_refresh_checkpoints(): shared.refresh_checkpoints() return {} diff --git a/modules/api/helpers.py b/modules/api/helpers.py index 21e89c5c1..15b7e29aa 100644 --- a/modules/api/helpers.py +++ b/modules/api/helpers.py @@ -15,6 +15,8 @@ def validate_sampler_name(name): def decode_base64_to_image(encoding, quiet=False): + if encoding is None: + return None if encoding.startswith("data:image/"): encoding = encoding.split(";")[1].split(",")[1] try: diff --git a/modules/api/models.py b/modules/api/models.py index fee8d781d..f60ba3dd7 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -266,6 +266,19 @@ class ReqProcess(BaseModel): class ResProcess(BaseModel): html_info: str = Field(title="HTML info", description="A series of HTML tags containing the process info.") + +class ReqPromptEnhance(BaseModel): + prompt: str = Field(title="Prompt", description="Prompt to enhance") + type: str = Field(title="Type", default='text', description="Type of enhancement to perform") + model: Optional[str] = Field(title="Model", default=None, description="Model to use for enhancement") + system_prompt: Optional[str] = Field(title="System prompt", default=None, description="Model system prompt") + image: Optional[str] = Field(title="Image", default=None, description="Image to work on, must be a Base64 string containing the image's data.") + seed: int = Field(title="Seed", default=-1, description="Seed used to generate the prompt") + +class ResPromptEnhance(BaseModel): + prompt: str = Field(title="Prompt", description="Enhanced prompt") + seed: int = Field(title="Seed", description="Seed used to generate the prompt") + class ReqProcessImage(ReqProcess): image: str = Field(default="", title="Image", description="Image to work on, must be a Base64 string containing the image's data.") diff --git a/modules/api/process.py b/modules/api/process.py index 80b19c52e..1d98df03e 100644 --- a/modules/api/process.py +++ b/modules/api/process.py @@ -2,8 +2,10 @@ from typing import Optional, List from threading import Lock from pydantic import BaseModel, Field # pylint: disable=no-name-in-module from fastapi.responses import JSONResponse +from fastapi.exceptions import HTTPException from modules.api.helpers import decode_base64_to_image, encode_pil_to_base64 from modules import errors, shared +from modules.api import models processor = None # cached instance of processor @@ -65,8 +67,8 @@ class APIProcess(): def post_preprocess(self, req: ReqPreprocess): global processor # pylint: disable=global-statement from modules.control import processors - models = list(processors.config) - if req.model not in models: + processors_list = list(processors.config) + if req.model not in processors_list: return JSONResponse(status_code=400, content={"error": f"Processor model not found: id={req.model}"}) image = decode_base64_to_image(req.image) if processor is None or processor.processor_id != req.model: @@ -129,3 +131,44 @@ class APIProcess(): boxes.append(item.box) shared.state.end(api=False) return ResFace(classes=classes, labels=labels, scores=scores, boxes=boxes, images=images) + + def post_prompt_enhance(self, req: models.ReqPromptEnhance): + from modules import processing_helpers + seed = req.seed or -1 + seed = processing_helpers.get_fixed_seed(seed) + prompt = '' + if req.type == 'text': + from modules.scripts import scripts_txt2img + model = 'google/gemma-3-1b-it' if req.model is None or len(req.model) < 4 else req.model + instance = [s for s in scripts_txt2img.scripts if 'prompt_enhance.py' in s.filename][0] + prompt = instance.enhance( + model=model, + prompt=req.prompt, + system=req.system_prompt, + seed=seed, + ) + elif req.type == 'image': + from modules.scripts import scripts_txt2img + model = 'google/gemma-3-4b-it' if req.model is None or len(req.model) < 4 else req.model + instance = [s for s in scripts_txt2img.scripts if 'prompt_enhance.py' in s.filename][0] + prompt = instance.enhance( + model=model, + prompt=req.prompt, + system=req.system_prompt, + image=decode_base64_to_image(req.image), + seed=seed, + ) + elif req.type == 'video': + from modules.ui_video_vlm import enhance_prompt + model = 'Google Gemma 3 4B' if req.model is None or len(req.model) < 4 else req.model + prompt = enhance_prompt( + enable=True, + image=decode_base64_to_image(req.image), + prompt=req.prompt, + model=model, + system_prompt=req.system_prompt, + ) + else: + raise HTTPException(status_code=400, detail="prompt enhancement: invalid type") + res = models.ResPromptEnhance(prompt=prompt, seed=seed) + return res diff --git a/modules/devices.py b/modules/devices.py index c35c8b909..0c5fb1552 100644 --- a/modules/devices.py +++ b/modules/devices.py @@ -395,10 +395,10 @@ def set_cudnn_params(): torch.use_deterministic_algorithms(opts.cudnn_deterministic) if opts.cudnn_deterministic: os.environ.setdefault('CUBLAS_WORKSPACE_CONFIG', ':4096:8') - torch.backends.cudnn.benchmark = True + torch.backends.cudnn.benchmark = opts.cudnn_benchmark if opts.cudnn_benchmark: log.debug('Torch cuDNN: enable benchmark') - torch.backends.cudnn.benchmark_limit = 0 + torch.backends.cudnn.benchmark_limit = opts.cudnn_benchmark_limit torch.backends.cudnn.allow_tf32 = True except Exception as e: log.warning(f'Torch cudnn: {e}') diff --git a/modules/extensions.py b/modules/extensions.py index 78f26c655..0a96f98b4 100644 --- a/modules/extensions.py +++ b/modules/extensions.py @@ -150,8 +150,9 @@ def list_extensions(): continue extension_names.append(extension_dirname) extension_paths.append((extension_dirname, path, dirname == extensions_builtin_dir)) - disabled_extensions = shared.opts.disabled_extensions + shared.temp_disable_extensions() + disabled_extensions = [e.lower() for e in shared.opts.disabled_extensions + shared.temp_disable_extensions()] for dirname, path, is_builtin in extension_paths: - extension = Extension(name=dirname, path=path, enabled=dirname not in disabled_extensions, is_builtin=is_builtin) + enabled = dirname.lower() not in disabled_extensions + extension = Extension(name=dirname, path=path, enabled=enabled, is_builtin=is_builtin) extensions.append(extension) shared.log.debug(f'Extensions: disabled={[e.name for e in extensions if not e.enabled]}') diff --git a/modules/interrogate/vqa.py b/modules/interrogate/vqa.py index 461ec6c73..c4dcafb10 100644 --- a/modules/interrogate/vqa.py +++ b/modules/interrogate/vqa.py @@ -527,10 +527,11 @@ def sa2(question: str, image: Image.Image, repo: str = None): return response -def interrogate(question, system_prompt, prompt, image, model_name, quiet:bool=False): +def interrogate(question:str='', system_prompt:str=None, prompt:str=None, image:Image.Image=None, model_name:str=None, quiet:bool=False): if not quiet: shared.state.begin('Interrogate') t0 = time.time() + model_name = model_name or shared.opts.interrogate_vlm_model if isinstance(image, list): image = image[0] if len(image) > 0 else None if isinstance(image, dict) and 'name' in image: diff --git a/modules/model_quant_nncf.py b/modules/model_quant_nncf.py index c4b6df127..0751e071b 100644 --- a/modules/model_quant_nncf.py +++ b/modules/model_quant_nncf.py @@ -1,7 +1,8 @@ -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from enum import Enum +import os import torch from diffusers.quantizers.base import DiffusersQuantizer from diffusers.quantizers.quantization_config import QuantizationConfigMixin @@ -13,6 +14,8 @@ from accelerate.utils import CustomDtype from modules import devices, shared +debug = os.environ.get('SD_QUANT_DEBUG', None) is not None + torch_dtype_dict = { "int8": torch.int8, "uint8": torch.uint8, @@ -42,7 +45,7 @@ class QuantizationMethod(str, Enum): NNCF = "nncf" -# de-abstracted and modified slightly from the actual quant functions of nncf 2.16.0: +# de-abstracted and modified from the actual quant functions of nncf 2.16.0: def nncf_compress_layer(layer, num_bits, is_asym_mode, torch_dtype=None, quant_conv=False, param_name=None): if layer.__class__.__name__ in allowed_types: if torch_dtype is None: @@ -77,7 +80,7 @@ def nncf_compress_layer(layer, num_bits, is_asym_mode, torch_dtype=None, quant_c scale = torch.where(torch.abs(scale) < eps, eps, scale) zero_point = level_low - torch.round(min_values / scale) - zero_point = torch.clip(zero_point.to(dtype=torch.int32), level_low, level_high) + zero_point = torch.clip(zero_point.to(dtype=torch.int32), level_low, level_high).to(dtype=torch.float32) else: factor = 2 ** (num_bits - 1) @@ -96,8 +99,12 @@ def nncf_compress_layer(layer, num_bits, is_asym_mode, torch_dtype=None, quant_c level_high = 2**num_bits - 1 if is_asym_mode else 2 ** (num_bits - 1) - 1 compressed_weight = layer.weight.data / scale + if not shared.opts.nncf_decompress_fp32: + scale = scale.to(torch_dtype) + if zero_point is not None: - compressed_weight += zero_point.to(dtype=layer.weight.dtype) + compressed_weight += zero_point + zero_point = zero_point.to(scale.dtype) compressed_weight = torch.round(compressed_weight) compressed_weight = torch.clip(compressed_weight, level_low, level_high).to(dtype) @@ -108,27 +115,25 @@ def nncf_compress_layer(layer, num_bits, is_asym_mode, torch_dtype=None, quant_c scale=scale.data, zero_point=zero_point.data, compressed_weight_shape=compressed_weight.shape, - result_shape=layer.weight.shape, - result_dtype=torch_dtype + result_dtype=torch_dtype, ) else: decompressor = INT4SymmetricWeightsDecompressor( scale=scale.data, compressed_weight_shape=compressed_weight.shape, - result_shape=layer.weight.shape, - result_dtype=torch_dtype + result_dtype=torch_dtype, ) else: if is_asym_mode: decompressor = INT8AsymmetricWeightsDecompressor( scale=scale.data, zero_point=zero_point.data, - result_dtype=torch_dtype + result_dtype=torch_dtype, ) else: decompressor = INT8SymmetricWeightsDecompressor( scale=scale.data, - result_dtype=torch_dtype + result_dtype=torch_dtype, ) compressed_weight = decompressor.pack_weight(compressed_weight) @@ -239,22 +244,12 @@ class NNCFQuantizer(DiffusersQuantizer): from nncf.torch.nncf_module_replacement import replace_modules_by_nncf_modules self.modules_to_not_convert = self.quantization_config.modules_to_not_convert - if not isinstance(self.modules_to_not_convert, list): self.modules_to_not_convert = [self.modules_to_not_convert] - if keep_in_fp32_modules is not None: self.modules_to_not_convert.extend(keep_in_fp32_modules) model.config.quantization_config = self.quantization_config - - if model.__class__.__name__ in {"T5EncoderModel", "UMT5EncoderModel"}: - for i in range(len(model.encoder.block)): - model.encoder.block[i].layer[1].DenseReluDense = NNCF_T5DenseGatedActDense( - model.encoder.block[i].layer[1].DenseReluDense, - dtype=torch.float32 if devices.dtype != torch.bfloat16 else torch.bfloat16 - ) - with init_empty_weights(): model, _ = replace_modules_by_nncf_modules(model) @@ -359,7 +354,6 @@ class NNCF_T5DenseGatedActDense(torch.nn.Module): # forward can't find what self def decompress_asymmetric(input: torch.Tensor, scale: torch.Tensor, zero_point: torch.Tensor) -> torch.Tensor: input = input.to(dtype=scale.dtype) - zero_point = zero_point.to(dtype=scale.dtype) decompressed_input = (input - zero_point) * scale return decompressed_input @@ -374,15 +368,14 @@ def unpack_uint4(packed_tensor: torch.Tensor) -> torch.Tensor: return torch.stack((torch.bitwise_and(packed_tensor, 15), torch.bitwise_right_shift(packed_tensor, 4)), dim=-1) -def unpack_int4(packed_tensor: torch.Tensor) -> torch.Tensor: +def unpack_int4(packed_tensor: torch.Tensor, dtype: Optional[torch.dtype] = torch.int8) -> torch.Tensor: t = unpack_uint4(packed_tensor) - return t.to(dtype=torch.int8) - 8 + return t.to(dtype=dtype) - 8 def pack_uint4(tensor: torch.Tensor) -> torch.Tensor: if tensor.dtype != torch.uint8: - msg = f"Invalid tensor dtype {tensor.type}. torch.uint8 type is supported." - raise RuntimeError(msg) + raise RuntimeError(f"Invalid tensor dtype {tensor.type}. torch.uint8 type is supported.") packed_tensor = tensor.contiguous() packed_tensor = packed_tensor.reshape(-1, 2) packed_tensor = torch.bitwise_and(packed_tensor[..., ::2], 15) | packed_tensor[..., 1::2] << 4 @@ -391,17 +384,16 @@ def pack_uint4(tensor: torch.Tensor) -> torch.Tensor: def pack_int4(tensor: torch.Tensor) -> torch.Tensor: if tensor.dtype != torch.int8: - msg = f"Invalid tensor dtype {tensor.type}. torch.int8 type is supported." - raise RuntimeError(msg) + raise RuntimeError(f"Invalid tensor dtype {tensor.type}. torch.int8 type is supported.") tensor = tensor + 8 return pack_uint4(tensor.to(dtype=torch.uint8)) class INT8AsymmetricWeightsDecompressor(torch.nn.Module): - def __init__(self, scale: torch.Tensor, zero_point: torch.Tensor, result_dtype: Optional[torch.dtype] = None): + def __init__(self, scale: torch.Tensor, zero_point: torch.Tensor, result_dtype: torch.dtype): super().__init__() self.scale = scale - self.zero_point = self.pack_weight(zero_point) + self.zero_point = zero_point self.result_dtype = result_dtype @property @@ -413,12 +405,9 @@ class INT8AsymmetricWeightsDecompressor(torch.nn.Module): return "asymmetric" def pack_weight(self, weight: torch.Tensor) -> torch.Tensor: - if torch.is_floating_point(weight): - msg = f"Invalid weight dtype {weight.type}. Integer types are supported." - raise ValueError(msg) - if torch.any((weight < 0) | (weight > 255)): - msg = "Weight values are not in [0, 255]." - raise ValueError(msg) + if debug: + if torch.any((weight < 0) | (weight > 255)): + raise ValueError("Weight values are not in [0, 255].") return weight.to(dtype=torch.uint8) def forward(self, x, *args, return_decompressed_only=False): @@ -431,7 +420,7 @@ class INT8AsymmetricWeightsDecompressor(torch.nn.Module): class INT8SymmetricWeightsDecompressor(torch.nn.Module): - def __init__(self, scale: torch.Tensor, result_dtype: Optional[torch.dtype] = None): + def __init__(self, scale: torch.Tensor, result_dtype: torch.dtype): super().__init__() self.scale = scale self.result_dtype = result_dtype @@ -445,14 +434,15 @@ class INT8SymmetricWeightsDecompressor(torch.nn.Module): return "symmetric" def pack_weight(self, weight: torch.Tensor) -> torch.Tensor: - if torch.any((weight < -128) | (weight > 127)): - msg = "Weight values are not in [-128, 127]." - raise ValueError(msg) + if debug: + if torch.any((weight < -128) | (weight > 127)): + raise ValueError("Weight values are not in [-128, 127].") return weight.to(dtype=torch.int8) def forward(self, x, *args, return_decompressed_only=False): result = decompress_symmetric(x.weight, self.scale) result = result.to(dtype=self.result_dtype) + if return_decompressed_only: return result else: @@ -464,18 +454,13 @@ class INT4AsymmetricWeightsDecompressor(torch.nn.Module): self, scale: torch.Tensor, zero_point: torch.Tensor, - compressed_weight_shape: Tuple[int, ...], - result_shape: Optional[Tuple[int, ...]] = None, - result_dtype: Optional[torch.dtype] = None, + compressed_weight_shape: torch.Size, + result_dtype: torch.dtype, ): super().__init__() self.scale = scale - - self.zero_point_shape = zero_point.shape - self.zero_point = self.pack_weight(zero_point) - + self.zero_point = zero_point self.compressed_weight_shape = compressed_weight_shape - self.result_shape = result_shape self.result_dtype = result_dtype @property @@ -487,21 +472,18 @@ class INT4AsymmetricWeightsDecompressor(torch.nn.Module): return "asymmetric" def pack_weight(self, weight: torch.Tensor) -> torch.Tensor: - if torch.any((weight < 0) | (weight > 15)): - msg = "Weight values are not in [0, 15]." - raise ValueError(msg) + if debug: + if torch.any((weight < 0) | (weight > 15)): + raise ValueError("Weight values are not in [0, 15].") return pack_uint4(weight.to(dtype=torch.uint8)) def forward(self, x, *args, return_decompressed_only=False): result = unpack_uint4(x.weight) result = result.reshape(self.compressed_weight_shape) - zero_point = unpack_uint4(self.zero_point) - zero_point = zero_point.reshape(self.zero_point_shape) - - result = decompress_asymmetric(result, self.scale, zero_point) - result = result.reshape(self.result_shape) if self.result_shape is not None else result + result = decompress_asymmetric(result, self.scale, self.zero_point) result = result.to(dtype=self.result_dtype) + if return_decompressed_only: return result else: @@ -512,15 +494,12 @@ class INT4SymmetricWeightsDecompressor(torch.nn.Module): def __init__( self, scale: torch.Tensor, - compressed_weight_shape: Tuple[int, ...], - result_shape: Optional[Tuple[int, ...]] = None, - result_dtype: Optional[torch.dtype] = None, + compressed_weight_shape: torch.Size, + result_dtype: torch.dtype, ): super().__init__() self.scale = scale - self.compressed_weight_shape = compressed_weight_shape - self.result_shape = result_shape self.result_dtype = result_dtype @property @@ -532,21 +511,18 @@ class INT4SymmetricWeightsDecompressor(torch.nn.Module): return "symmetric" def pack_weight(self, weight: torch.Tensor) -> torch.Tensor: - if torch.is_floating_point(weight): - msg = f"Invalid weight dtype {weight.type}. Integer types are supported." - raise ValueError(msg) - if torch.any((weight < -8) | (weight > 7)): - msg = "Tensor values are not in [-8, 7]." - raise ValueError(msg) + if debug: + if torch.any((weight < -8) | (weight > 7)): + raise ValueError("Tensor values are not in [-8, 7].") return pack_int4(weight.to(dtype=torch.int8)) def forward(self, x, *arg, return_decompressed_only=False): - result = unpack_int4(x.weight) + result = unpack_int4(x.weight, dtype=self.scale.dtype) result = result.reshape(self.compressed_weight_shape) result = decompress_symmetric(result, self.scale) - result = result.reshape(self.result_shape) if self.result_shape is not None else result result = result.to(dtype=self.result_dtype) + if return_decompressed_only: return result else: diff --git a/modules/sd_models.py b/modules/sd_models.py index 10a34ad44..22ba21a49 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -1086,6 +1086,7 @@ def clear_caches(): def unload_model_weights(op='model'): + clear_caches() if shared.compiled_model_state is not None: shared.compiled_model_state.compiled_cache.clear() shared.compiled_model_state.req_cache.clear() diff --git a/modules/shared.py b/modules/shared.py index 07e81d7be..d0993b1ee 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -335,6 +335,10 @@ def temp_disable_extensions(): disabled.append(ext) if not opts.lora_legacy: disabled.append('Lora') + else: + if 'Lora' in disabled: + disabled.remove('Lora') + cmd_opts.controlnet_loglevel = 'WARNING' return disabled @@ -478,9 +482,10 @@ options_templates.update(options_section(('backends', "Backend Settings"), { "other_sep": OptionInfo("