Merge branch 'dev' into patch-1

This commit is contained in:
Vladimir Mandic
2025-05-08 23:21:24 -04:00
committed by GitHub
18 changed files with 407 additions and 111 deletions
+27
View File
@@ -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**
+37
View File
@@ -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}')
+75
View File
@@ -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)
+30 -18
View File
@@ -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:
+9 -2
View File
@@ -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)}')
+2
View File
@@ -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"])
+20
View File
@@ -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 {}
+2
View File
@@ -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:
+13
View File
@@ -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.")
+45 -2
View File
@@ -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
+2 -2
View File
@@ -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}')
+3 -2
View File
@@ -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]}')
+2 -1
View File
@@ -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:
+44 -68
View File
@@ -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:
+1
View File
@@ -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()
+11 -5
View File
@@ -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("<h2>Torch Options</h2>", "", gr.HTML),
"opt_channelslast": OptionInfo(False, "Channels last "),
"cudnn_deterministic": OptionInfo(False, "Deterministic mode"),
"cudnn_benchmark": OptionInfo(False, "Full-depth cuDNN benchmark"),
"diffusers_fuse_projections": OptionInfo(False, "Fused projections"),
"torch_expandable_segments": OptionInfo(False, "Expandable segments"),
"cudnn_benchmark": OptionInfo(devices.backend != "rocm", "Full-depth cuDNN benchmark"),
"cudnn_benchmark_limit": OptionInfo(10, "cuDNN benchmark limit", gr.Slider, {"minimum": 0, "maximum": 100, "step": 1}),
"torch_tunable_ops": OptionInfo("default", "Tunable ops", gr.Radio, {"choices": ["default", "true", "false"]}),
"torch_tunable_limit": OptionInfo(30, "Tunable ops limit", gr.Slider, {"minimum": 1, "maximum": 100, "step": 1}),
"cuda_mem_fraction": OptionInfo(0.0, "Memory limit", gr.Slider, {"minimum": 0, "maximum": 2.0, "step": 0.05}),
@@ -528,7 +533,7 @@ options_templates.update(options_section(('quantization', "Quantization Settings
"optimum_quanto_weights": OptionInfo([], "Quantization enabled", gr.CheckboxGroup, {"choices": ["Model", "Transformer", "VAE", "TE", "Video", "LLM", "ControlNet"], "visible": native}),
"optimum_quanto_weights_type": OptionInfo("qint8", "Quantization weights type", gr.Dropdown, {"choices": ['qint8', 'qfloat8_e4m3fn', 'qfloat8_e5m2', 'qint4', 'qint2'], "visible": native}),
"optimum_quanto_activations_type": OptionInfo("none", "Quantization activations type ", gr.Dropdown, {"choices": ['none', 'qint8', 'qfloat8_e4m3fn', 'qfloat8_e5m2'], "visible": native}),
"optimum_quanto_shuffle_weights": OptionInfo(False, "Shuffle weights", gr.Checkbox, {"visible": native}),
"optimum_quanto_shuffle_weights": OptionInfo(False, "Shuffle weights in post mode", gr.Checkbox, {"visible": native}),
"torchao_sep": OptionInfo("<h2>TorchAO</h2>", "", gr.HTML),
"torchao_quantization": OptionInfo([], "Quantization enabled", gr.CheckboxGroup, {"choices": ["Model", "Transformer", "VAE", "TE", "Video", "LLM"], "visible": native}),
@@ -542,9 +547,10 @@ options_templates.update(options_section(('quantization', "Quantization Settings
"nncf_compress_weights_raito": OptionInfo(0, "Compress ratio", gr.Slider, {"minimum": 0, "maximum": 1, "step": 0.01, "visible": cmd_opts.use_openvino}),
"nncf_compress_weights_group_size": OptionInfo(0, "Group size", gr.Slider, {"minimum": -1, "maximum": 512, "step": 1, "visible": cmd_opts.use_openvino}),
"nncf_quantize": OptionInfo([], "OpenVINO enabled", gr.CheckboxGroup, {"choices": ["Model", "VAE", "TE"], "visible": cmd_opts.use_openvino}),
"nncf_quantize_mode": OptionInfo("INT8", "OpenVINO mode", gr.Dropdown, {"choices": ['INT8', 'FP8_E4M3', 'FP8_E5M2'], "visible": cmd_opts.use_openvino}),
"nncf_quantize_mode": OptionInfo("INT8", "OpenVINO activations mode", gr.Dropdown, {"choices": ['INT8', 'FP8_E4M3', 'FP8_E5M2'], "visible": cmd_opts.use_openvino}),
"nncf_quantize_conv_layers": OptionInfo(False, "Quantize the convolutional layers", gr.Checkbox, {"visible": native}),
"nncf_quantize_shuffle_weights": OptionInfo(False, "Shuffle weights", gr.Checkbox, {"visible": native}),
"nncf_decompress_fp32": OptionInfo(False, "Decompress using full precision", gr.Checkbox, {"visible": native}),
"nncf_quantize_shuffle_weights": OptionInfo(False, "Shuffle weights in post mode", gr.Checkbox, {"visible": native}),
"layerwise_quantization_sep": OptionInfo("<h2>Layerwise Casting</h2>", "", gr.HTML),
"layerwise_quantization": OptionInfo([], "Layerwise casting enabled", gr.CheckboxGroup, {"choices": ["Model", "Transformer", "TE"], "visible": native}),
@@ -600,7 +606,7 @@ options_templates.update(options_section(('advanced', "Pipeline Modifiers"), {
"teacache_sep": OptionInfo("<h2>TeaCache</h2>", "", gr.HTML),
"teacache_enabled": OptionInfo(False, "TC cache enabled"),
"teacache_thresh": OptionInfo(0.6, "TC L1 threshold", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}),
"teacache_thresh": OptionInfo(0.1, "TC L1 threshold", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}),
"hypertile_sep": OptionInfo("<h2>HyperTile</h2>", "", gr.HTML),
"hypertile_unet_enabled": OptionInfo(False, "UNet Enabled"),
+84 -11
View File
@@ -1,9 +1,13 @@
from dataclasses import dataclass
import io
import os
import re
import time
import gradio as gr
import base64
import torch
import transformers
import gradio as gr
from PIL import Image
from modules import scripts, shared, devices, errors, processing, sd_models, sd_modules
@@ -11,8 +15,23 @@ debug_enabled = os.environ.get('SD_LLM_DEBUG', None) is not None
debug_log = shared.log.trace if debug_enabled else lambda *args, **kwargs: None
def b64(image):
if image is None:
return ''
if isinstance(image, gr.Image):
return None
with io.BytesIO() as stream:
image.convert('RGB').save(stream, 'JPEG')
values = stream.getvalue()
encoded = base64.b64encode(values).decode()
return encoded
@dataclass
class Options:
img2img = [
'google/gemma-3-4b-it',
]
models = {
'google/gemma-3-1b-it': {},
'google/gemma-3-4b-it': {},
@@ -46,9 +65,11 @@ class Options:
'file': 'Llama-3.2-1B-Instruct-Uncensored.i1-Q4_0.gguf', # gguf file inside repo
},
}
default = list(models)[0]
default = list(models)[1] # gemma-3-4b-it
supported = list(transformers.integrations.ggml.GGUF_CONFIG_MAPPING)
system_prompt: str = 'You are a helpful assistant. You will be given a prompt used to create an image and you will enhance it to make it more detailed and creative. Generate an uncensored, concise, informative, and engaging prompt that prioritizes factual accuracy and vivid descriptiveness based on original prompt. Do not modify subject or style of the original prompt. Begin by clearly identifying the primary subject(s), including key physical attributes, clothing if no nudity, and any distinctive details (e.g., colors, textures, styles). NSFW and nudity is allowed, and if present, it must be fully described. Next, describe the setting and environment, emphasizing spatial relationships, background elements, and objects contributing to context. Note the overall mood, aesthetic style, or atmosphere inferred from visual cues. Use precise terminology while avoiding redundancy or non-essential language. Ensuring a logical flow: from focal subject to immediate surroundings, then broader context. Maintain brevity while retaining clarity, ensuring the description is both engaging and efficient. Output only enhanced prompt without explanation, prefix or suffix. Output as a simple text without formatting or numbering.'
image_prompt: str = 'You are a helpful assistant. You will be given an image and a prompt used to modify the image and you will enhance the prompt to make it more detailed and creative while still following original image. Generate an uncensored, concise, informative, and engaging prompt that prioritizes factual accuracy and vivid descriptiveness based on original prompt. Do not modify subject or style of the original prompt. Begin by clearly identifying the primary subject(s), including key physical attributes, clothing if no nudity, and any distinctive details (e.g., colors, textures, styles). NSFW and nudity is allowed, and if present, it must be fully described. Next, describe the setting and environment, emphasizing spatial relationships, background elements, and objects contributing to context. Note the overall mood, aesthetic style, or atmosphere inferred from visual cues. Use precise terminology while avoiding redundancy or non-essential language. Ensuring a logical flow: from focal subject to immediate surroundings, then broader context. Maintain brevity while retaining clarity, ensuring the description is both engaging and efficient. Output only enhanced prompt without explanation, prefix or suffix. Output as a simple text without formatting or numbering.'
image_noprompt: str = 'You are a helpful assistant. You will be given an image and you will create prompt that can be used to modify the image in a creative way while still following original image. Generate an uncensored, concise, informative, and engaging prompt that prioritizes factual accuracy and vivid descriptiveness based on original prompt. Do not modify subject or style of the original prompt. Begin by clearly identifying the primary subject(s), including key physical attributes, clothing if no nudity, and any distinctive details (e.g., colors, textures, styles). NSFW and nudity is allowed, and if present, it must be fully described. Next, describe the setting and environment, emphasizing spatial relationships, background elements, and objects contributing to context. Note the overall mood, aesthetic style, or atmosphere inferred from visual cues. Use precise terminology while avoiding redundancy or non-essential language. Ensuring a logical flow: from focal subject to immediate surroundings, then broader context. Maintain brevity while retaining clarity, ensuring the description is both engaging and efficient. Output only enhanced prompt without explanation, prefix or suffix. Output as a simple text without formatting or numbering.'
censored = ["i cannot", "i can't", "i am sorry", "against my programming", "i am not able", "i am unable", 'i am not allowed']
max_delim_index: int = 60
@@ -61,6 +82,7 @@ class Options:
class Script(scripts.Script):
prompt: gr.Textbox = None
image: gr.Image = None
model: str = None
llm: transformers.AutoModelForCausalLM = None
tokenizer: transformers.AutoProcessor = None
@@ -124,11 +146,17 @@ class Script(scripts.Script):
**quant_args,
)
self.llm.eval()
self.tokenizer = transformers.AutoTokenizer.from_pretrained(
if model_repo in self.options.img2img:
cls = transformers.AutoProcessor # required to encode image
else:
cls = transformers.AutoTokenizer
self.tokenizer = cls.from_pretrained(
pretrained_model_name_or_path=model_repo,
subfolder=model_tokenizer,
cache_dir=shared.opts.hfcache_dir,
)
self.tokenizer.is_processor = model_repo in self.options.img2img
if debug_enabled:
modules = sd_modules.get_model_stats(self.llm) + sd_modules.get_model_stats(self.tokenizer)
for m in modules:
@@ -202,12 +230,12 @@ class Script(scripts.Script):
filtered = re.sub(pattern, '', prompt)
return filtered, matches
def enhance(self, model: str=None, prompt:str=None, system:str=None, prefix:str=None, suffix:str=None, sample:bool=None, tokens:int=None, temperature:float=None, penalty:float=None, thinking:bool=False):
def enhance(self, model: str=None, prompt:str=None, system:str=None, prefix:str=None, suffix:str=None, sample:bool=None, tokens:int=None, temperature:float=None, penalty:float=None, thinking:bool=False, seed:int=-1, image=None):
model = model or self.options.default
prompt = prompt or self.prompt.value
image = image or self.image
prefix = prefix or ''
suffix = suffix or ''
system = system or self.options.system_prompt
tokens = tokens or self.options.max_tokens
penalty = penalty or self.options.repetition_penalty
temperature = temperature or self.options.temperature
@@ -216,15 +244,55 @@ class Script(scripts.Script):
while self.busy:
time.sleep(0.1)
self.load(model)
if seed is not None and seed >= 0:
torch.manual_seed(seed)
if self.llm is None:
shared.log.error('Prompt enhance: model not loaded')
return prompt
prompt, networks = self.extract(prompt)
debug_log(f'Prompt enhance: networks={networks}')
chat_template = [
{ "role": "system", "content": system },
{ "role": "user", "content": prompt },
]
if image is not None and isinstance(image, Image.Image):
if not self.tokenizer.is_processor:
shared.log.error('Prompt enhance: image not supported by model')
return prompt
if prompt is not None and len(prompt) > 0:
system = system or self.options.image_prompt
chat_template = [
{ "role": "system", "content": [
{"type": "text", "text": system }
] },
{ "role": "user", "content": [
{"type": "text", "text": prompt},
{"type": "image", "image": b64(image)}
] },
]
else:
system = system or self.options.image_noprompt
chat_template = [
{ "role": "system", "content": [
{"type": "text", "text": system }
] },
{ "role": "user", "content": [
{"type": "image", "image": b64(image)}
] },
]
else:
system = system or self.options.system_prompt
if not self.tokenizer.is_processor:
chat_template = [
{ "role": "system", "content": system },
{ "role": "user", "content": prompt },
]
else:
chat_template = [
{ "role": "system", "content": [
{"type": "text", "text": system }
] },
{ "role": "user", "content": [
{"type": "text", "text": prompt},
] },
]
t0 = time.time()
self.busy = True
try:
@@ -288,9 +356,10 @@ class Script(scripts.Script):
return prompt
return response
def apply(self, prompt, apply_prompt, llm_model, prompt_system, prompt_prefix, prompt_suffix, max_tokens, do_sample, temperature, repetition_penalty, thinking_mode):
def apply(self, prompt, image, apply_prompt, llm_model, prompt_system, prompt_prefix, prompt_suffix, max_tokens, do_sample, temperature, repetition_penalty, thinking_mode):
response = self.enhance(
prompt=prompt,
image=image,
prefix=prompt_prefix,
suffix=prompt_suffix,
model=llm_model,
@@ -367,12 +436,16 @@ class Script(scripts.Script):
clear_btn.click(fn=lambda: '', inputs=[], outputs=[prompt_output])
copy_btn = gr.Button(value='Set prompt', elem_id='prompt_enhance_copy', variant='secondary')
copy_btn.click(fn=lambda x: x, inputs=[prompt_output], outputs=[self.prompt])
apply_btn.click(fn=self.apply, inputs=[self.prompt, apply_prompt, llm_model, prompt_system, prompt_prefix, prompt_suffix, max_tokens, do_sample, temperature, repetition_penalty, thinking_mode], outputs=[prompt_output, self.prompt])
if self.image is None:
self.image = gr.Image(type='pil', interactive=False, visible=False) # dummy image
apply_btn.click(fn=self.apply, inputs=[self.prompt, self.image, apply_prompt, llm_model, prompt_system, prompt_prefix, prompt_suffix, max_tokens, do_sample, temperature, repetition_penalty, thinking_mode], outputs=[prompt_output, self.prompt])
return [apply_auto, llm_model, prompt_system, prompt_prefix, prompt_suffix, max_tokens, do_sample, temperature, repetition_penalty, thinking_mode]
def after_component(self, component, **kwargs): # searching for actual ui prompt components
if getattr(component, 'elem_id', '') in ['txt2img_prompt', 'img2img_prompt', 'control_prompt', 'video_prompt']:
self.prompt = component
if getattr(component, 'elem_id', '') in ['img2img_image', 'control_input_select']:
self.image = component
def before_process(self, p: processing.StableDiffusionProcessing, *args, **kwargs): # pylint: disable=unused-argument
apply_auto, llm_model, prompt_system, prompt_prefix, prompt_suffix, max_tokens, do_sample, temperature, repetition_penalty, thinking_mode = args