From 461bed5720ae06e76353cba59192821d8d089dba Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 Aug 2025 07:06:34 -0400 Subject: [PATCH 001/141] experimental sampler monkeypatch Signed-off-by: Vladimir Mandic --- modules/img2img.py | 4 ++-- modules/sd_samplers.py | 2 +- modules/sd_samplers_diffusers.py | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/img2img.py b/modules/img2img.py index d91daaca1..b3c0880aa 100644 --- a/modules/img2img.py +++ b/modules/img2img.py @@ -138,10 +138,10 @@ def process_batch(p, input_files, input_dir, output_dir, inpaint_mask_dir, args) if output_dir == '': output_dir = shared.opts.outdir_img2img_samples os.makedirs(output_dir, exist_ok=True) - geninfo, items = images.read_info_from_image(image) + info, items = images.read_info_from_image(image) for k, v in items.items(): image.info[k] = v - images.save_image(image, path=output_dir, basename=basename, seed=None, prompt=None, extension=ext, info=geninfo, grid=False, pnginfo_section_name="extras", existing_info=image.info, forced_filename=forced_filename) + images.save_image(image, path=output_dir, basename=basename, seed=None, prompt=None, extension=ext, info=info, grid=False, pnginfo_section_name="extras", existing_info=image.info, forced_filename=forced_filename) processed = scripts_manager.scripts_img2img.after(p, processed, *args) shared.log.debug(f'Processed: images={len(batch_image_files)} memory={memory_stats()} batch') diff --git a/modules/sd_samplers.py b/modules/sd_samplers.py index c7502cb4b..0b965eaf6 100644 --- a/modules/sd_samplers.py +++ b/modules/sd_samplers.py @@ -98,7 +98,7 @@ def create_sampler(name, model): # validate sampler prediction type if (model is not None) and (is_flow and not requires_flow): shared.log.error(f'Sampler: "{sampler.name}" cls={sampler.sampler.__class__.__name__} pipe={model.__class__.__name__} model requires sampler with discrete prediction') - return restore_default(model) + # return restore_default(model) if (model is not None) and (not is_flow and requires_flow): shared.log.error(f'Sampler: "{sampler.name}" cls={sampler.sampler.__class__.__name__} pipe={model.__class__.__name__} model requires sampler with flow prediction') return restore_default(model) diff --git a/modules/sd_samplers_diffusers.py b/modules/sd_samplers_diffusers.py index b052042bc..d0d394b88 100644 --- a/modules/sd_samplers_diffusers.py +++ b/modules/sd_samplers_diffusers.py @@ -317,6 +317,12 @@ class DiffusionSampler: self.sampler = None return + # monkey-patch to allow sdxl pipeline to execute flowmatch samplers + if not hasattr(sampler, 'scale_model_input'): + sampler.scale_model_input = lambda x, _y: x + if not hasattr(sampler, 'init_noise_sigma'): + sampler.init_noise_sigma = 1.0 + self.sampler = sampler # shared.log.debug_log(f'Sampler: class="{self.sampler.__class__.__name__}" config={self.sampler.config}') From 5f384a9de7ba77ae85830817add54c2de3f363a5 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 Aug 2025 08:22:32 -0400 Subject: [PATCH 002/141] video save frames Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 2 ++ cli/api-control.py | 1 + extensions-builtin/sdnext-modernui | 2 +- modules/processing_diffusers.py | 8 +++++++- modules/video_models/video_run.py | 1 + 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c53e8242d..8a3f6a527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ - refactor legacy processing loop - fix Wan 2.2-5B I2V workflow - fix OpenVINO + - fix video model vs pipeline mismatch + - fix video generic save frames - fix inpaint image metadata - fix processing image save loop - fix progress bar with refine/detailer diff --git a/cli/api-control.py b/cli/api-control.py index 79667a2e9..925c77599 100755 --- a/cli/api-control.py +++ b/cli/api-control.py @@ -108,6 +108,7 @@ def generate(args): # pylint: disable=redefined-outer-name if args.mask is not None: options['mask'] = encode(args.mask) + data = post('/sdapi/v1/control', options) t1 = time.time() if 'info' in data: diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 43ed2ea51..9052df510 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 43ed2ea51049a5926fe553aed3e9a717657352a6 +Subproject commit 9052df510e9704ef952f1c4ebfa1fba28dc984a3 diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 41ed7834c..d809362e3 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -451,7 +451,13 @@ def update_pipeline(sd_model, p: processing.StableDiffusionProcessing): def validate_pipeline(p: processing.StableDiffusionProcessing): - is_video_model = ('video' in shared.sd_model_type.lower()) or ('video' in shared.sd_model.__class__.__name__.lower()) + from modules.video_models.models_def import models as video_models + models_cls = [] + for family in video_models: + for m in video_models[family]: + if m.repo_cls is not None: + models_cls.append(m.repo_cls.__name__) + is_video_model = shared.sd_model.__class__.__name__ in models_cls is_video_pipeline = 'video' in p.__class__.__name__.lower() if is_video_model and not is_video_pipeline: shared.log.error(f'Mismatch: type={shared.sd_model_type} cls={shared.sd_model.__class__.__name__} request={p.__class__.__name__} video model with non-video pipeline') diff --git a/modules/video_models/video_run.py b/modules/video_models/video_run.py index 01e3fd387..8e52ca2c1 100644 --- a/modules/video_models/video_run.py +++ b/modules/video_models/video_run.py @@ -52,6 +52,7 @@ def generate(*args, **kwargs): p.state = ui_state p.do_not_save_grid = True p.do_not_save_samples = not save_frames + p.outpath_samples = shared.opts.outdir_samples or shared.opts.outdir_video if 'I2V' in model: if init_image is None: return video_utils.queue_err('init image not set') From c3d007b02c97a9f5de03807da38774f6772f8920 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Sat, 2 Aug 2025 17:36:55 +0300 Subject: [PATCH 003/141] SDNQ split forward.py into layers and cleanup --- modules/sdnq/__init__.py | 71 ++-- modules/sdnq/common.py | 13 +- modules/sdnq/dequantizer.py | 6 +- modules/sdnq/forward.py | 388 +----------------- modules/sdnq/layers/conv/conv_fp8.py | 71 ++++ .../sdnq/layers/conv/conv_fp8_tensorwise.py | 70 ++++ modules/sdnq/layers/conv/conv_int8.py | 76 ++++ modules/sdnq/layers/conv/forward.py | 93 +++++ modules/sdnq/layers/linear/forward.py | 7 + modules/sdnq/layers/linear/linear_fp8.py | 41 ++ .../layers/linear/linear_fp8_tensorwise.py | 48 +++ modules/sdnq/layers/linear/linear_int8.py | 52 +++ modules/sdnq/packed_int.py | 1 + 13 files changed, 521 insertions(+), 416 deletions(-) create mode 100644 modules/sdnq/layers/conv/conv_fp8.py create mode 100644 modules/sdnq/layers/conv/conv_fp8_tensorwise.py create mode 100644 modules/sdnq/layers/conv/conv_int8.py create mode 100644 modules/sdnq/layers/conv/forward.py create mode 100644 modules/sdnq/layers/linear/forward.py create mode 100644 modules/sdnq/layers/linear/linear_fp8.py create mode 100644 modules/sdnq/layers/linear/linear_fp8_tensorwise.py create mode 100644 modules/sdnq/layers/linear/linear_int8.py diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index 985e99a2a..e6c012e8c 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -14,6 +14,41 @@ from .dequantizer import dequantizer_dict from .forward import get_forward_func +class QuantizationMethod(str, Enum): + SDNQ = "sdnq" + + +def get_scale_asymmetric(weight: torch.FloatTensor, reduction_axes: Union[int, List[int]], weights_dtype: str) -> Tuple[torch.FloatTensor, torch.FloatTensor]: + zero_point = torch.amin(weight, dim=reduction_axes, keepdims=True) + scale = torch.amax(weight, dim=reduction_axes, keepdims=True).sub_(zero_point).div_(dtype_dict[weights_dtype]["max"] - dtype_dict[weights_dtype]["min"]) + eps = torch.finfo(scale.dtype).eps # prevent divison by 0 + scale = torch.where(torch.abs(scale) < eps, eps, scale) + if dtype_dict[weights_dtype]["min"] != 0: + zero_point.sub_(torch.mul(scale, dtype_dict[weights_dtype]["min"])) + return scale, zero_point + + +def get_scale_symmetric(weight: torch.FloatTensor, reduction_axes: Union[int, List[int]], weights_dtype: str) -> torch.FloatTensor: + scale = torch.amax(weight.abs(), dim=reduction_axes, keepdims=True).div_(dtype_dict[weights_dtype]["max"]) + eps = torch.finfo(scale.dtype).eps # prevent divison by 0 + scale = torch.where(torch.abs(scale) < eps, eps, scale) + return scale + + +def quantize_weight(weight: torch.FloatTensor, reduction_axes: Union[int, List[int]], weights_dtype: str) -> Tuple[torch.Tensor, torch.FloatTensor, torch.FloatTensor]: + if dtype_dict[weights_dtype]["is_unsigned"]: + scale, zero_point = get_scale_asymmetric(weight, reduction_axes, weights_dtype) + quantized_weight = torch.sub(weight, zero_point).div_(scale) + else: + scale = get_scale_symmetric(weight, reduction_axes, weights_dtype) + quantized_weight = torch.div(weight, scale) + zero_point = None + if dtype_dict[weights_dtype]["is_integer"]: + quantized_weight.round_() + quantized_weight = quantized_weight.clamp_(dtype_dict[weights_dtype]["min"], dtype_dict[weights_dtype]["max"]).to(dtype_dict[weights_dtype]["torch_dtype"]) + return quantized_weight, scale, zero_point + + @devices.inference_context() def sdnq_quantize_layer(layer, weights_dtype="int8", torch_dtype=None, group_size=0, quant_conv=False, use_quantized_matmul=False, use_quantized_matmul_conv=False, dequantize_fp32=False, quantization_device=None, return_device=None, param_name=None): # pylint: disable=unused-argument layer_class_name = layer.__class__.__name__ @@ -153,7 +188,6 @@ def sdnq_quantize_layer(layer, weights_dtype="int8", torch_dtype=None, group_siz layer.forward = get_forward_func(layer_class_name, use_quantized_matmul, dtype_dict[weights_dtype]["is_integer"], use_tensorwise_fp8_matmul) layer.forward = layer.forward.__get__(layer, layer.__class__) - #devices.torch_gc(force=False, reason=f"SDNQ param_name: {param_name}") return layer @@ -195,41 +229,6 @@ def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_si return model -def get_scale_asymmetric(weight: torch.FloatTensor, reduction_axes: Union[int, List[int]], weights_dtype: str) -> Tuple[torch.FloatTensor, torch.FloatTensor]: - zero_point = torch.amin(weight, dim=reduction_axes, keepdims=True) - scale = torch.amax(weight, dim=reduction_axes, keepdims=True).sub_(zero_point).div_(dtype_dict[weights_dtype]["max"] - dtype_dict[weights_dtype]["min"]) - eps = torch.finfo(scale.dtype).eps # prevent divison by 0 - scale = torch.where(torch.abs(scale) < eps, eps, scale) - if dtype_dict[weights_dtype]["min"] != 0: - zero_point.sub_(torch.mul(scale, dtype_dict[weights_dtype]["min"])) - return scale, zero_point - - -def get_scale_symmetric(weight: torch.FloatTensor, reduction_axes: Union[int, List[int]], weights_dtype: str) -> torch.FloatTensor: - scale = torch.amax(weight.abs(), dim=reduction_axes, keepdims=True).div_(dtype_dict[weights_dtype]["max"]) - eps = torch.finfo(scale.dtype).eps # prevent divison by 0 - scale = torch.where(torch.abs(scale) < eps, eps, scale) - return scale - - -def quantize_weight(weight: torch.FloatTensor, reduction_axes: Union[int, List[int]], weights_dtype: str) -> Tuple[torch.Tensor, torch.FloatTensor, torch.FloatTensor]: - if dtype_dict[weights_dtype]["is_unsigned"]: - scale, zero_point = get_scale_asymmetric(weight, reduction_axes, weights_dtype) - quantized_weight = torch.sub(weight, zero_point).div_(scale) - else: - scale = get_scale_symmetric(weight, reduction_axes, weights_dtype) - quantized_weight = torch.div(weight, scale) - zero_point = None - if dtype_dict[weights_dtype]["is_integer"]: - quantized_weight.round_() - quantized_weight = quantized_weight.clamp_(dtype_dict[weights_dtype]["min"], dtype_dict[weights_dtype]["max"]).to(dtype_dict[weights_dtype]["torch_dtype"]) - return quantized_weight, scale, zero_point - - -class QuantizationMethod(str, Enum): - SDNQ = "sdnq" - - class SDNQQuantizer(DiffusersQuantizer): r""" Diffusers Quantizer for SDNQ diff --git a/modules/sdnq/common.py b/modules/sdnq/common.py index 9dab864c5..07ae0671b 100644 --- a/modules/sdnq/common.py +++ b/modules/sdnq/common.py @@ -1,7 +1,7 @@ # pylint: disable=redefined-builtin,no-member,protected-access import torch -from modules import devices +from modules import devices, shared torch_version = float(torch.__version__[:3]) @@ -30,7 +30,9 @@ if hasattr(torch, "float8_e4m3fnuz"): if hasattr(torch, "float8_e5m2fnuz"): dtype_dict["float8_e5m2fnuz"] = {"min": -57344, "max": 57344, "num_bits": 8, "target_dtype": "fp8", "torch_dtype": torch.float8_e5m2fnuz, "storage_dtype": torch.float8_e5m2fnuz, "is_unsigned": False, "is_integer": False} -use_tensorwise_fp8_matmul = True # Direct tensorwise only exist on H100 hardware, sdnq will use software tensorwise with this setting +use_torch_compile = shared.opts.sdnq_dequantize_compile # this setting requires a full restart of the webui to apply +use_tensorwise_fp8_matmul = True # row-wise FP8 only exist on H100 hardware, sdnq will use software row-wise with tensorwise hardware with this setting + quantized_matmul_dtypes = ("int8", "int7", "int6", "int5", "int4", "int3", "int2", "float8_e4m3fn", "float8_e5m2") if devices.backend in {"cpu", "openvino"}: quantized_matmul_dtypes += ("float8_e4m3fnuz", "float8_e5m2fnuz") @@ -39,3 +41,10 @@ linear_types = ("Linear",) conv_types = ("Conv1d", "Conv2d", "Conv3d") conv_transpose_types = ("ConvTranspose1d", "ConvTranspose2d", "ConvTranspose3d") allowed_types = linear_types + conv_types + conv_transpose_types + +if use_torch_compile: + try: + torch._dynamo.config.cache_size_limit = max(8192, torch._dynamo.config.cache_size_limit) + torch._dynamo.config.accumulated_recompile_limit = max(8192, torch._dynamo.config.accumulated_recompile_limit) + except Exception as e: + shared.log.warning(f"Quantization: type=sdnq Failed to increase the cache size for torch.compile: {e}") diff --git a/modules/sdnq/dequantizer.py b/modules/sdnq/dequantizer.py index db6fb622c..0a0881117 100644 --- a/modules/sdnq/dequantizer.py +++ b/modules/sdnq/dequantizer.py @@ -3,7 +3,7 @@ import torch from modules import shared -from .common import dtype_dict +from .common import dtype_dict, use_torch_compile from .packed_int import pack_int_symetric, unpack_int_symetric, packed_int_function_dict @@ -173,10 +173,8 @@ dequantizer_dict = { } -if shared.opts.sdnq_dequantize_compile: +if use_torch_compile: try: - torch._dynamo.config.cache_size_limit = max(8192, torch._dynamo.config.cache_size_limit) - torch._dynamo.config.accumulated_recompile_limit = max(8192, torch._dynamo.config.accumulated_recompile_limit) dequantize_asymmetric_compiled = torch.compile(dequantize_asymmetric, fullgraph=True, dynamic=False) dequantize_symmetric_compiled = torch.compile(dequantize_symmetric, fullgraph=True, dynamic=False) dequantize_packed_int_asymmetric_compiled = torch.compile(dequantize_packed_int_asymmetric, fullgraph=True, dynamic=False) diff --git a/modules/sdnq/forward.py b/modules/sdnq/forward.py index 4abc4d7e8..211b57768 100644 --- a/modules/sdnq/forward.py +++ b/modules/sdnq/forward.py @@ -1,410 +1,50 @@ -# pylint: disable=redefined-builtin,no-member,protected-access +# pylint: disable=protected-access + +from typing import Callable -from typing import Callable, List, Tuple, Optional import torch -from modules import shared from .common import conv_types, conv_transpose_types -from .dequantizer import dequantize_symmetric, dequantize_symmetric_with_bias -from .packed_int import unpack_int_symetric def get_forward_func(layer_class_name: str, use_quantized_matmul: bool, is_integer: bool, use_tensorwise_fp8_matmul: bool) -> Callable: # pylint: disable=inconsistent-return-statements if layer_class_name in conv_types: if use_quantized_matmul: if is_integer: + from .layers.conv.conv_int8 import quantized_conv_forward_int8_matmul return quantized_conv_forward_int8_matmul else: if use_tensorwise_fp8_matmul: + from .layers.conv.conv_fp8_tensorwise import quantized_conv_forward_fp8_matmul_tensorwise return quantized_conv_forward_fp8_matmul_tensorwise else: + from .layers.conv.conv_fp8 import quantized_conv_forward_fp8_matmul return quantized_conv_forward_fp8_matmul else: + from .layers.conv.forward import quantized_conv_forward return quantized_conv_forward elif layer_class_name in conv_transpose_types: if layer_class_name.endswith("1d"): + from .layers.conv.forward import quantized_conv_transpose_1d_forward return quantized_conv_transpose_1d_forward elif layer_class_name.endswith("2d"): + from .layers.conv.forward import quantized_conv_transpose_2d_forward return quantized_conv_transpose_2d_forward elif layer_class_name.endswith("3d"): + from .layers.conv.forward import quantized_conv_transpose_3d_forward return quantized_conv_transpose_3d_forward else: if use_quantized_matmul: if is_integer: + from .layers.linear.linear_int8 import quantized_linear_forward_int8_matmul return quantized_linear_forward_int8_matmul else: if use_tensorwise_fp8_matmul: + from .layers.linear.linear_fp8_tensorwise import quantized_linear_forward_fp8_matmul_tensorwise return quantized_linear_forward_fp8_matmul_tensorwise else: + from .layers.linear.linear_fp8 import quantized_linear_forward_fp8_matmul return quantized_linear_forward_fp8_matmul else: + from .layers.linear.forward import quantized_linear_forward return quantized_linear_forward - - -def quantize_fp8_matmul_input(input: torch.FloatTensor) -> Tuple[torch.Tensor, torch.FloatTensor]: - input = input.flatten(0,-2).contiguous() - input_scale = torch.amax(input.abs(), dim=-1, keepdims=True).div_(448) - input = torch.div(input, input_scale).clamp_(-448, 448).to(dtype=torch.float8_e4m3fn) - input_scale = input_scale.to(dtype=torch.float32) - return input, input_scale - - -def quantize_fp8_matmul_input_tensorwise(input: torch.FloatTensor, scale: torch.FloatTensor) -> Tuple[torch.Tensor, torch.FloatTensor]: - input = input.flatten(0,-2).contiguous() - input_scale = torch.amax(input.abs(), dim=-1, keepdims=True).div_(448) - input = torch.div(input, input_scale).clamp_(-448, 448).to(dtype=torch.float8_e4m3fn) - scale = torch.mul(input_scale, scale) - if scale.dtype == torch.float16: # fp16 will overflow - scale = scale.to(dtype=torch.float32) - return input, scale - - -def quantize_int8_matmul_input(input: torch.FloatTensor, scale: torch.FloatTensor) -> Tuple[torch.CharTensor, torch.FloatTensor]: - input = input.flatten(0,-2).contiguous() - input_scale = torch.amax(input.abs(), dim=-1, keepdims=True).div_(127) - input = torch.div(input, input_scale).round_().clamp_(-128, 127).to(dtype=torch.int8) - scale = torch.mul(input_scale, scale) - if scale.dtype == torch.float16: # fp16 will overflow - scale = scale.to(dtype=torch.float32) - return input, scale - - -def fp8_matmul( - input: torch.FloatTensor, - weight: torch.Tensor, - bias: torch.FloatTensor, - scale: torch.FloatTensor, -) -> torch.FloatTensor: - return_dtype = input.dtype - output_shape = list(input.shape) - output_shape[-1] = weight.shape[-1] - input, input_scale = quantize_fp8_matmul_input(input) - return torch._scaled_mm(input, weight, scale_a=input_scale, scale_b=scale, bias=bias, out_dtype=return_dtype).reshape(output_shape) - - -# sm89 doesn't support row wise scale in Windows -def fp8_matmul_tensorwise( - input: torch.FloatTensor, - weight: torch.Tensor, - bias: torch.FloatTensor, - scale: torch.FloatTensor, -) -> torch.FloatTensor: - return_dtype = input.dtype - output_shape = list(input.shape) - output_shape[-1] = weight.shape[-1] - dummy_input_scale = torch.ones(1, device=input.device, dtype=torch.float32) - input, scale = quantize_fp8_matmul_input_tensorwise(input, scale) - if bias is not None: - return dequantize_symmetric_with_bias(torch._scaled_mm(input, weight, scale_a=dummy_input_scale, scale_b=dummy_input_scale, bias=None, out_dtype=scale.dtype), scale, bias, return_dtype, output_shape) - else: - return dequantize_symmetric(torch._scaled_mm(input, weight, scale_a=dummy_input_scale, scale_b=dummy_input_scale, bias=None, out_dtype=scale.dtype), scale, return_dtype, output_shape) - - -def int8_matmul( - input: torch.FloatTensor, - weight: torch.Tensor, - bias: torch.FloatTensor, - scale: torch.FloatTensor, - quantized_weight_shape: torch.Size, - weights_dtype: str, -) -> torch.FloatTensor: - if quantized_weight_shape is not None: - weight = unpack_int_symetric(weight, quantized_weight_shape, weights_dtype, dtype=torch.int8, transpose=True) - return_dtype = input.dtype - output_shape = list(input.shape) - output_shape[-1] = weight.shape[-1] - input, scale = quantize_int8_matmul_input(input, scale) - if bias is not None: - return dequantize_symmetric_with_bias(torch._int_mm(input, weight), scale, bias, return_dtype, output_shape) - else: - return dequantize_symmetric(torch._int_mm(input, weight), scale, return_dtype, output_shape) - - -def process_conv_input(conv_type, input, reversed_padding_repeated_twice, padding_mode, result_shape, stride, padding, dilation): - if conv_type == 1: - batch_size, _, L_in = input.shape - C_out, _, K_l = result_shape - L_out = (L_in + 2 * padding[1] - dilation[1] * (K_l - 1) - 1) // stride[1] + 1 - mm_output_shape = (batch_size, L_out, C_out) - kernel_size = (1, K_l) - if conv_type == 2: - batch_size, _, H_in, W_in = input.shape - C_out, _, K_h, K_w = result_shape - H_out = (H_in + 2 * padding[0] - dilation[0] * (K_h - 1) - 1) // stride[0] + 1 - W_out = (W_in + 2 * padding[1] - dilation[1] * (K_w - 1) - 1) // stride[1] + 1 - mm_output_shape = (batch_size, H_out, W_out, C_out) - kernel_size = (K_h, K_w) - else: - batch_size, _, D_in, H_in, W_in = input.shape - C_out, _, K_d, K_h, K_w = result_shape - D_out = (D_in + 2 * padding[0] - dilation[0] * (K_d - 1) - 1) // stride[0] + 1 - H_out = (H_in + 2 * padding[1] - dilation[1] * (K_h - 1) - 1) // stride[1] + 1 - W_out = (W_in + 2 * padding[2] - dilation[2] * (K_w - 1) - 1) // stride[2] + 1 - mm_output_shape = (batch_size, D_out, H_out, W_out, C_out) - kernel_size = (K_d, K_h, K_w) - - if padding_mode != "zeros": - input = torch.nn.functional.pad(input, reversed_padding_repeated_twice, mode=padding_mode) - padding = (0,) * (conv_type if conv_type != 1 else 2) - elif conv_type == 3: - input = torch.nn.functional.pad(input, reversed_padding_repeated_twice) - - if conv_type == 1: - input = input.unsqueeze(2) - - if conv_type == 3: - K_D_eff = K_d + (K_d - 1) * (dilation[0] - 1) - K_H_eff = K_h + (K_h - 1) * (dilation[0] - 1) - K_W_eff = K_w + (K_w - 1) * (dilation[0] - 1) - input = input.unfold(2, K_D_eff, stride[0]).unfold(3, K_H_eff, stride[1]).unfold(4, K_W_eff, stride[2]) - if dilation[0] > 1: - input = input[..., ::dilation[0], :, :] - if dilation[1] > 1: - input = input[..., ::dilation[1], :] - if dilation[2] > 1: - input = input[..., ::dilation[2]] - input = input.permute(0, 2, 3, 4, 1, 5, 6, 7).reshape(batch_size, D_out * H_out * W_out, -1) - else: - input = torch.nn.functional.unfold(input, kernel_size=kernel_size, padding=padding, stride=stride, dilation=dilation).transpose(1,2) - return input, mm_output_shape - - -def conv_fp8_matmul( - input: torch.FloatTensor, - weight: torch.Tensor, - bias: torch.FloatTensor, - scale: torch.FloatTensor, - result_shape: torch.Size, - reversed_padding_repeated_twice: List[int], - padding_mode: str, conv_type: int, - groups: int, stride: List[int], - padding: List[int], dilation: List[int], -) -> torch.FloatTensor: - return_dtype = input.dtype - input, mm_output_shape = process_conv_input(conv_type, input, reversed_padding_repeated_twice, padding_mode, result_shape, stride, padding, dilation) - input, input_scale = quantize_fp8_matmul_input(input) - - if groups == 1: - result = torch._scaled_mm(input, weight, scale_a=input_scale, scale_b=scale, bias=bias, out_dtype=return_dtype).reshape(mm_output_shape) - else: - scale = scale.reshape(groups, 1, scale.shape[1] // groups) - input_scale = input_scale.reshape(groups, input_scale.shape[0] // groups, 1) - weight = weight.reshape(weight.shape[0], groups, weight.shape[1] // groups).transpose(0,1) - input = input.reshape(input.shape[0], groups, input.shape[1] // groups).transpose(0,1) - result = [] - if bias is not None: - bias = bias.reshape(groups, bias.shape[0] // groups) - for i in range(groups): - result.append(torch._scaled_mm(input[i], weight[i], scale_a=input_scale[i], scale_b=scale[i], bias=bias[i], out_dtype=return_dtype)) - else: - for i in range(groups): - result.append(torch._scaled_mm(input[i], weight[i], scale_a=input_scale[i], scale_b=scale[i], bias=None, out_dtype=return_dtype)) - result = torch.cat(result, dim=-1).reshape(mm_output_shape) - - if conv_type == 1: - result = result.transpose(1,2) - elif conv_type == 2: - result = result.permute(0,3,1,2) - elif conv_type == 3: - result = result.permute(0,4,1,2,3) - return result - - -def conv_fp8_matmul_tensorwise( - input: torch.FloatTensor, - weight: torch.Tensor, - bias: torch.FloatTensor, - scale: torch.FloatTensor, - result_shape: torch.Size, - reversed_padding_repeated_twice: List[int], - padding_mode: str, conv_type: int, - groups: int, stride: List[int], - padding: List[int], dilation: List[int], -) -> torch.FloatTensor: - return_dtype = input.dtype - input, mm_output_shape = process_conv_input(conv_type, input, reversed_padding_repeated_twice, padding_mode, result_shape, stride, padding, dilation) - input, scale = quantize_fp8_matmul_input_tensorwise(input, scale) - dummy_input_scale = torch.ones(1, device=input.device, dtype=torch.float32) - - if groups == 1: - result = torch._scaled_mm(input, weight, scale_a=dummy_input_scale, scale_b=dummy_input_scale, bias=None, out_dtype=scale.dtype) - else: - weight = weight.reshape(weight.shape[0], groups, weight.shape[1] // groups).transpose(0,1) - input = input.reshape(input.shape[0], groups, input.shape[1] // groups).transpose(0,1) - result = [] - for i in range(groups): - result.append(torch._scaled_mm(input[i], weight[i], scale_a=dummy_input_scale, scale_b=dummy_input_scale, bias=None, out_dtype=scale.dtype)) - result = torch.cat(result, dim=-1) - if bias is not None: - dequantize_symmetric_with_bias(result, scale, bias, return_dtype, mm_output_shape) - else: - dequantize_symmetric(result, scale, return_dtype, mm_output_shape) - - if conv_type == 1: - result = result.transpose(1,2) - elif conv_type == 2: - result = result.permute(0,3,1,2) - elif conv_type == 3: - result = result.permute(0,4,1,2,3) - return result - - -def conv_int8_matmul( - input: torch.FloatTensor, - weight: torch.CharTensor, - bias: torch.FloatTensor, - scale: torch.FloatTensor, - result_shape: torch.Size, - quantized_weight_shape: torch.Size, - weights_dtype: str, - reversed_padding_repeated_twice: List[int], - padding_mode: str, conv_type: int, - groups: int, stride: List[int], - padding: List[int], dilation: List[int], -) -> torch.FloatTensor: - return_dtype = input.dtype - input, mm_output_shape = process_conv_input(conv_type, input, reversed_padding_repeated_twice, padding_mode, result_shape, stride, padding, dilation) - input, scale = quantize_int8_matmul_input(input, scale) - if quantized_weight_shape is not None: - weight = unpack_int_symetric(weight, quantized_weight_shape, weights_dtype, dtype=torch.int8, transpose=True) - - if groups == 1: - result = torch._int_mm(input, weight) - else: - weight = weight.reshape(weight.shape[0], groups, weight.shape[1] // groups).transpose(0,1) - input = input.reshape(input.shape[0], groups, input.shape[1] // groups).transpose(0,1) - result = [] - for i in range(groups): - result.append(torch._int_mm(input[i], weight[i])) - result = torch.cat(result, dim=-1) - if bias is not None: - result = dequantize_symmetric_with_bias(result, scale, bias, return_dtype, mm_output_shape) - else: - result = dequantize_symmetric(result, scale, return_dtype, mm_output_shape) - - if conv_type == 1: - result = result.transpose(1,2) - elif conv_type == 2: - result = result.permute(0,3,1,2) - elif conv_type == 3: - result = result.permute(0,4,1,2,3) - return result - - -def quantized_linear_forward_fp8_matmul(self, input: torch.FloatTensor) -> torch.FloatTensor: - if torch.numel(input) / input.shape[-1] < 32: - return torch.nn.functional.linear(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) - return fp8_matmul(input, self.weight, self.bias, self.sdnq_dequantizer.scale) - - -def quantized_linear_forward_fp8_matmul_tensorwise(self, input: torch.FloatTensor) -> torch.FloatTensor: - if torch.numel(input) / input.shape[-1] < 32: - return torch.nn.functional.linear(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) - return fp8_matmul_tensorwise(input, self.weight, self.bias, self.sdnq_dequantizer.scale) - - -def quantized_linear_forward_int8_matmul(self, input: torch.FloatTensor) -> torch.FloatTensor: - if torch.numel(input) / input.shape[-1] < 32: - return torch.nn.functional.linear(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) - return int8_matmul(input, self.weight, self.bias, self.sdnq_dequantizer.scale, getattr(self.sdnq_dequantizer, "quantized_weight_shape", None), self.sdnq_dequantizer.weights_dtype) - - -def quantized_linear_forward(self, input: torch.FloatTensor) -> torch.FloatTensor: - return torch.nn.functional.linear(input, self.sdnq_dequantizer(self.weight), self.bias) - - -def get_conv_args(input_ndim: int, stride, padding, dilation): - if input_ndim == 3: - conv_type = 1 - elif input_ndim == 4: - conv_type = 2 - else: - conv_type = 3 - if isinstance(stride, int): - stride = (stride,) * conv_type - if isinstance(padding, int): - padding = (padding,) * conv_type - if isinstance(dilation, int): - dilation = (dilation,) * conv_type - if conv_type == 1: - stride = (1, stride[0]) - padding = (0, padding[0]) - dilation = (1, dilation[0]) - return conv_type, stride, padding, dilation - - -def quantized_conv_forward_fp8_matmul(self, input) -> torch.FloatTensor: - if torch.numel(input) / input.shape[2] < 32: - return self._conv_forward(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) - conv_type, stride, padding, dilation = get_conv_args(input.ndim, self.stride, self.padding, self.dilation) - return conv_fp8_matmul( - input, self.weight, self.bias, - self.sdnq_dequantizer.scale, - self.sdnq_dequantizer.result_shape, - self._reversed_padding_repeated_twice, - self.padding_mode, conv_type, - self.groups, stride, padding, dilation, - ) - - -def quantized_conv_forward_fp8_matmul_tensorwise(self, input) -> torch.FloatTensor: - if torch.numel(input) / input.shape[2] < 32: - return self._conv_forward(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) - conv_type, stride, padding, dilation = get_conv_args(input.ndim, self.stride, self.padding, self.dilation) - return conv_fp8_matmul_tensorwise( - input, self.weight, self.bias, - self.sdnq_dequantizer.scale, - self.sdnq_dequantizer.result_shape, - self._reversed_padding_repeated_twice, - self.padding_mode, conv_type, - self.groups, stride, padding, dilation, - ) - - -def quantized_conv_forward_int8_matmul(self, input) -> torch.FloatTensor: - if torch.numel(input) / input.shape[2] < 32: - return self._conv_forward(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) - conv_type, stride, padding, dilation = get_conv_args(input.ndim, self.stride, self.padding, self.dilation) - return conv_int8_matmul( - input, self.weight, self.bias, - self.sdnq_dequantizer.scale, - self.sdnq_dequantizer.result_shape, - getattr(self.sdnq_dequantizer, "quantized_weight_shape", None), - self.sdnq_dequantizer.weights_dtype, - self._reversed_padding_repeated_twice, - self.padding_mode, conv_type, - self.groups, stride, padding, dilation, - ) - - -def quantized_conv_forward(self, input) -> torch.FloatTensor: - return self._conv_forward(input, self.sdnq_dequantizer(self.weight), self.bias) - - -def quantized_conv_transpose_1d_forward(self, input: torch.FloatTensor, output_size: Optional[list[int]] = None) -> torch.FloatTensor: - output_padding = self._output_padding(input, output_size, self.stride, self.padding, self.kernel_size, 1, self.dilation) - return torch.nn.functional.conv_transpose1d(input, self.sdnq_dequantizer(self.weight), self.bias, self.stride, self.padding, output_padding, self.groups, self.dilation) - - -def quantized_conv_transpose_2d_forward(self, input: torch.FloatTensor, output_size: Optional[list[int]] = None) -> torch.FloatTensor: - output_padding = self._output_padding(input, output_size, self.stride, self.padding, self.kernel_size, 2, self.dilation) - return torch.nn.functional.conv_transpose2d(input, self.sdnq_dequantizer(self.weight), self.bias, self.stride, self.padding, output_padding, self.groups, self.dilation) - - -def quantized_conv_transpose_3d_forward(self, input: torch.FloatTensor, output_size: Optional[list[int]] = None) -> torch.FloatTensor: - output_padding = self._output_padding(input, output_size, self.stride, self.padding, self.kernel_size, 3, self.dilation) - return torch.nn.functional.conv_transpose3d(input, self.sdnq_dequantizer(self.weight), self.bias, self.stride, self.padding, output_padding, self.groups, self.dilation) - - -if shared.opts.sdnq_dequantize_compile: - try: - torch._dynamo.config.cache_size_limit = max(8192, torch._dynamo.config.cache_size_limit) - torch._dynamo.config.accumulated_recompile_limit = max(8192, torch._dynamo.config.accumulated_recompile_limit) - int8_matmul = torch.compile(int8_matmul, fullgraph=True, dynamic=False) - fp8_matmul = torch.compile(fp8_matmul, fullgraph=True, dynamic=False) - fp8_matmul_tensorwise = torch.compile(fp8_matmul_tensorwise, fullgraph=True, dynamic=False) - conv_int8_matmul = torch.compile(conv_int8_matmul, fullgraph=True, dynamic=False) - conv_fp8_matmul = torch.compile(conv_fp8_matmul, fullgraph=True, dynamic=False) - conv_fp8_matmul_tensorwise = torch.compile(conv_fp8_matmul_tensorwise, fullgraph=True, dynamic=False) - except Exception as e: - shared.log.warning(f"Quantization: type=sdnq MatMul using torch.compile is not available: {e}") diff --git a/modules/sdnq/layers/conv/conv_fp8.py b/modules/sdnq/layers/conv/conv_fp8.py new file mode 100644 index 000000000..1f5a50922 --- /dev/null +++ b/modules/sdnq/layers/conv/conv_fp8.py @@ -0,0 +1,71 @@ +# pylint: disable=relative-beyond-top-level,redefined-builtin,protected-access + +from typing import List + +import torch + +from ...common import use_torch_compile # noqa: TID252 +from ..linear.linear_fp8 import quantize_fp8_matmul_input # noqa: TID252 +from .conv import get_conv_args, process_conv_input + + +def conv_fp8_matmul( + input: torch.FloatTensor, + weight: torch.Tensor, + bias: torch.FloatTensor, + scale: torch.FloatTensor, + result_shape: torch.Size, + reversed_padding_repeated_twice: List[int], + padding_mode: str, conv_type: int, + groups: int, stride: List[int], + padding: List[int], dilation: List[int], +) -> torch.FloatTensor: + return_dtype = input.dtype + input, mm_output_shape = process_conv_input(conv_type, input, reversed_padding_repeated_twice, padding_mode, result_shape, stride, padding, dilation) + input, input_scale = quantize_fp8_matmul_input(input) + + if groups == 1: + result = torch._scaled_mm(input, weight, scale_a=input_scale, scale_b=scale, bias=bias, out_dtype=return_dtype).reshape(mm_output_shape) + else: + scale = scale.reshape(groups, 1, scale.shape[1] // groups) + input_scale = input_scale.reshape(groups, input_scale.shape[0] // groups, 1) + weight = weight.reshape(weight.shape[0], groups, weight.shape[1] // groups).transpose(0,1) + input = input.reshape(input.shape[0], groups, input.shape[1] // groups).transpose(0,1) + result = [] + if bias is not None: + bias = bias.reshape(groups, bias.shape[0] // groups) + for i in range(groups): + result.append(torch._scaled_mm(input[i], weight[i], scale_a=input_scale[i], scale_b=scale[i], bias=bias[i], out_dtype=return_dtype)) + else: + for i in range(groups): + result.append(torch._scaled_mm(input[i], weight[i], scale_a=input_scale[i], scale_b=scale[i], bias=None, out_dtype=return_dtype)) + result = torch.cat(result, dim=-1).reshape(mm_output_shape) + + if conv_type == 1: + result = result.transpose(1,2) + elif conv_type == 2: + result = result.permute(0,3,1,2) + elif conv_type == 3: + result = result.permute(0,4,1,2,3) + return result + + +def quantized_conv_forward_fp8_matmul(self, input) -> torch.FloatTensor: + if torch.numel(input) / input.shape[2] < 32: + return self._conv_forward(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) + conv_type, stride, padding, dilation = get_conv_args(input.ndim, self.stride, self.padding, self.dilation) + return conv_fp8_matmul( + input, self.weight, self.bias, + self.sdnq_dequantizer.scale, + self.sdnq_dequantizer.result_shape, + self._reversed_padding_repeated_twice, + self.padding_mode, conv_type, + self.groups, stride, padding, dilation, + ) + + +if use_torch_compile: + try: + conv_fp8_matmul = torch.compile(conv_fp8_matmul, fullgraph=True, dynamic=False) + except Exception: + pass diff --git a/modules/sdnq/layers/conv/conv_fp8_tensorwise.py b/modules/sdnq/layers/conv/conv_fp8_tensorwise.py new file mode 100644 index 000000000..3c5cfa56d --- /dev/null +++ b/modules/sdnq/layers/conv/conv_fp8_tensorwise.py @@ -0,0 +1,70 @@ +# pylint: disable=relative-beyond-top-level,redefined-builtin,protected-access + +from typing import List + +import torch + +from ...common import use_torch_compile # noqa: TID252 +from ...dequantizer import dequantize_symmetric, dequantize_symmetric_with_bias # noqa: TID252 +from ..linear.linear_fp8_tensorwise import quantize_fp8_matmul_input_tensorwise # noqa: TID252 +from .conv import get_conv_args, process_conv_input + + +def conv_fp8_matmul_tensorwise( + input: torch.FloatTensor, + weight: torch.Tensor, + bias: torch.FloatTensor, + scale: torch.FloatTensor, + result_shape: torch.Size, + reversed_padding_repeated_twice: List[int], + padding_mode: str, conv_type: int, + groups: int, stride: List[int], + padding: List[int], dilation: List[int], +) -> torch.FloatTensor: + return_dtype = input.dtype + input, mm_output_shape = process_conv_input(conv_type, input, reversed_padding_repeated_twice, padding_mode, result_shape, stride, padding, dilation) + input, scale = quantize_fp8_matmul_input_tensorwise(input, scale) + dummy_input_scale = torch.ones(1, device=input.device, dtype=torch.float32) + + if groups == 1: + result = torch._scaled_mm(input, weight, scale_a=dummy_input_scale, scale_b=dummy_input_scale, bias=None, out_dtype=scale.dtype) + else: + weight = weight.reshape(weight.shape[0], groups, weight.shape[1] // groups).transpose(0,1) + input = input.reshape(input.shape[0], groups, input.shape[1] // groups).transpose(0,1) + result = [] + for i in range(groups): + result.append(torch._scaled_mm(input[i], weight[i], scale_a=dummy_input_scale, scale_b=dummy_input_scale, bias=None, out_dtype=scale.dtype)) + result = torch.cat(result, dim=-1) + if bias is not None: + dequantize_symmetric_with_bias(result, scale, bias, return_dtype, mm_output_shape) + else: + dequantize_symmetric(result, scale, return_dtype, mm_output_shape) + + if conv_type == 1: + result = result.transpose(1,2) + elif conv_type == 2: + result = result.permute(0,3,1,2) + elif conv_type == 3: + result = result.permute(0,4,1,2,3) + return result + + +def quantized_conv_forward_fp8_matmul_tensorwise(self, input) -> torch.FloatTensor: + if torch.numel(input) / input.shape[2] < 32: + return self._conv_forward(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) + conv_type, stride, padding, dilation = get_conv_args(input.ndim, self.stride, self.padding, self.dilation) + return conv_fp8_matmul_tensorwise( + input, self.weight, self.bias, + self.sdnq_dequantizer.scale, + self.sdnq_dequantizer.result_shape, + self._reversed_padding_repeated_twice, + self.padding_mode, conv_type, + self.groups, stride, padding, dilation, + ) + + +if use_torch_compile: + try: + conv_fp8_matmul_tensorwise = torch.compile(conv_fp8_matmul_tensorwise, fullgraph=True, dynamic=False) + except Exception: + pass diff --git a/modules/sdnq/layers/conv/conv_int8.py b/modules/sdnq/layers/conv/conv_int8.py new file mode 100644 index 000000000..ffda82e2f --- /dev/null +++ b/modules/sdnq/layers/conv/conv_int8.py @@ -0,0 +1,76 @@ +# pylint: disable=relative-beyond-top-level,redefined-builtin,protected-access + +from typing import List + +import torch + +from ...common import use_torch_compile # noqa: TID252 +from ...packed_int import unpack_int_symetric # noqa: TID252 +from ...dequantizer import dequantize_symmetric, dequantize_symmetric_with_bias # noqa: TID252 +from ..linear.linear_int8 import quantize_int8_matmul_input # noqa: TID252 +from .conv import get_conv_args, process_conv_input + + +def conv_int8_matmul( + input: torch.FloatTensor, + weight: torch.CharTensor, + bias: torch.FloatTensor, + scale: torch.FloatTensor, + result_shape: torch.Size, + quantized_weight_shape: torch.Size, + weights_dtype: str, + reversed_padding_repeated_twice: List[int], + padding_mode: str, conv_type: int, + groups: int, stride: List[int], + padding: List[int], dilation: List[int], +) -> torch.FloatTensor: + return_dtype = input.dtype + input, mm_output_shape = process_conv_input(conv_type, input, reversed_padding_repeated_twice, padding_mode, result_shape, stride, padding, dilation) + input, scale = quantize_int8_matmul_input(input, scale) + if quantized_weight_shape is not None: + weight = unpack_int_symetric(weight, quantized_weight_shape, weights_dtype, dtype=torch.int8, transpose=True) + + if groups == 1: + result = torch._int_mm(input, weight) + else: + weight = weight.reshape(weight.shape[0], groups, weight.shape[1] // groups).transpose(0,1) + input = input.reshape(input.shape[0], groups, input.shape[1] // groups).transpose(0,1) + result = [] + for i in range(groups): + result.append(torch._int_mm(input[i], weight[i])) + result = torch.cat(result, dim=-1) + if bias is not None: + result = dequantize_symmetric_with_bias(result, scale, bias, return_dtype, mm_output_shape) + else: + result = dequantize_symmetric(result, scale, return_dtype, mm_output_shape) + + if conv_type == 1: + result = result.transpose(1,2) + elif conv_type == 2: + result = result.permute(0,3,1,2) + elif conv_type == 3: + result = result.permute(0,4,1,2,3) + return result + + +def quantized_conv_forward_int8_matmul(self, input) -> torch.FloatTensor: + if torch.numel(input) / input.shape[2] < 32: + return self._conv_forward(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) + conv_type, stride, padding, dilation = get_conv_args(input.ndim, self.stride, self.padding, self.dilation) + return conv_int8_matmul( + input, self.weight, self.bias, + self.sdnq_dequantizer.scale, + self.sdnq_dequantizer.result_shape, + getattr(self.sdnq_dequantizer, "quantized_weight_shape", None), + self.sdnq_dequantizer.weights_dtype, + self._reversed_padding_repeated_twice, + self.padding_mode, conv_type, + self.groups, stride, padding, dilation, + ) + + +if use_torch_compile: + try: + conv_int8_matmul = torch.compile(conv_int8_matmul, fullgraph=True, dynamic=False) + except Exception: + pass diff --git a/modules/sdnq/layers/conv/forward.py b/modules/sdnq/layers/conv/forward.py new file mode 100644 index 000000000..44c90b061 --- /dev/null +++ b/modules/sdnq/layers/conv/forward.py @@ -0,0 +1,93 @@ +# pylint: disable=relative-beyond-top-level,redefined-builtin,protected-access + +from typing import Optional + +import torch + + +def get_conv_args(input_ndim: int, stride, padding, dilation): + if input_ndim == 3: + conv_type = 1 + elif input_ndim == 4: + conv_type = 2 + else: + conv_type = 3 + if isinstance(stride, int): + stride = (stride,) * conv_type + if isinstance(padding, int): + padding = (padding,) * conv_type + if isinstance(dilation, int): + dilation = (dilation,) * conv_type + if conv_type == 1: + stride = (1, stride[0]) + padding = (0, padding[0]) + dilation = (1, dilation[0]) + return conv_type, stride, padding, dilation + + +def process_conv_input(conv_type, input, reversed_padding_repeated_twice, padding_mode, result_shape, stride, padding, dilation): + if conv_type == 1: + batch_size, _, L_in = input.shape + C_out, _, K_l = result_shape + L_out = (L_in + 2 * padding[1] - dilation[1] * (K_l - 1) - 1) // stride[1] + 1 + mm_output_shape = (batch_size, L_out, C_out) + kernel_size = (1, K_l) + if conv_type == 2: + batch_size, _, H_in, W_in = input.shape + C_out, _, K_h, K_w = result_shape + H_out = (H_in + 2 * padding[0] - dilation[0] * (K_h - 1) - 1) // stride[0] + 1 + W_out = (W_in + 2 * padding[1] - dilation[1] * (K_w - 1) - 1) // stride[1] + 1 + mm_output_shape = (batch_size, H_out, W_out, C_out) + kernel_size = (K_h, K_w) + else: + batch_size, _, D_in, H_in, W_in = input.shape + C_out, _, K_d, K_h, K_w = result_shape + D_out = (D_in + 2 * padding[0] - dilation[0] * (K_d - 1) - 1) // stride[0] + 1 + H_out = (H_in + 2 * padding[1] - dilation[1] * (K_h - 1) - 1) // stride[1] + 1 + W_out = (W_in + 2 * padding[2] - dilation[2] * (K_w - 1) - 1) // stride[2] + 1 + mm_output_shape = (batch_size, D_out, H_out, W_out, C_out) + kernel_size = (K_d, K_h, K_w) + + if padding_mode != "zeros": + input = torch.nn.functional.pad(input, reversed_padding_repeated_twice, mode=padding_mode) + padding = (0,) * (conv_type if conv_type != 1 else 2) + elif conv_type == 3: + input = torch.nn.functional.pad(input, reversed_padding_repeated_twice) + + if conv_type == 1: + input = input.unsqueeze(2) + + if conv_type == 3: + K_D_eff = K_d + (K_d - 1) * (dilation[0] - 1) + K_H_eff = K_h + (K_h - 1) * (dilation[0] - 1) + K_W_eff = K_w + (K_w - 1) * (dilation[0] - 1) + input = input.unfold(2, K_D_eff, stride[0]).unfold(3, K_H_eff, stride[1]).unfold(4, K_W_eff, stride[2]) + if dilation[0] > 1: + input = input[..., ::dilation[0], :, :] + if dilation[1] > 1: + input = input[..., ::dilation[1], :] + if dilation[2] > 1: + input = input[..., ::dilation[2]] + input = input.permute(0, 2, 3, 4, 1, 5, 6, 7).reshape(batch_size, D_out * H_out * W_out, -1) + else: + input = torch.nn.functional.unfold(input, kernel_size=kernel_size, padding=padding, stride=stride, dilation=dilation).transpose(1,2) + return input, mm_output_shape + + +def quantized_conv_forward(self, input) -> torch.FloatTensor: + return self._conv_forward(input, self.sdnq_dequantizer(self.weight), self.bias) + + +def quantized_conv_transpose_1d_forward(self, input: torch.FloatTensor, output_size: Optional[list[int]] = None) -> torch.FloatTensor: + output_padding = self._output_padding(input, output_size, self.stride, self.padding, self.kernel_size, 1, self.dilation) + return torch.nn.functional.conv_transpose1d(input, self.sdnq_dequantizer(self.weight), self.bias, self.stride, self.padding, output_padding, self.groups, self.dilation) + + +def quantized_conv_transpose_2d_forward(self, input: torch.FloatTensor, output_size: Optional[list[int]] = None) -> torch.FloatTensor: + output_padding = self._output_padding(input, output_size, self.stride, self.padding, self.kernel_size, 2, self.dilation) + return torch.nn.functional.conv_transpose2d(input, self.sdnq_dequantizer(self.weight), self.bias, self.stride, self.padding, output_padding, self.groups, self.dilation) + + +def quantized_conv_transpose_3d_forward(self, input: torch.FloatTensor, output_size: Optional[list[int]] = None) -> torch.FloatTensor: + output_padding = self._output_padding(input, output_size, self.stride, self.padding, self.kernel_size, 3, self.dilation) + return torch.nn.functional.conv_transpose3d(input, self.sdnq_dequantizer(self.weight), self.bias, self.stride, self.padding, output_padding, self.groups, self.dilation) diff --git a/modules/sdnq/layers/linear/forward.py b/modules/sdnq/layers/linear/forward.py new file mode 100644 index 000000000..20224204c --- /dev/null +++ b/modules/sdnq/layers/linear/forward.py @@ -0,0 +1,7 @@ +# pylint: disable=relative-beyond-top-level,redefined-builtin,protected-access + +import torch + + +def quantized_linear_forward(self, input: torch.FloatTensor) -> torch.FloatTensor: + return torch.nn.functional.linear(input, self.sdnq_dequantizer(self.weight), self.bias) diff --git a/modules/sdnq/layers/linear/linear_fp8.py b/modules/sdnq/layers/linear/linear_fp8.py new file mode 100644 index 000000000..17cac32e5 --- /dev/null +++ b/modules/sdnq/layers/linear/linear_fp8.py @@ -0,0 +1,41 @@ +# pylint: disable=relative-beyond-top-level,redefined-builtin,protected-access + +from typing import Tuple + +import torch + +from ...common import use_torch_compile # noqa: TID252 + + +def quantize_fp8_matmul_input(input: torch.FloatTensor) -> Tuple[torch.Tensor, torch.FloatTensor]: + input = input.flatten(0,-2).contiguous() + input_scale = torch.amax(input.abs(), dim=-1, keepdims=True).div_(448) + input = torch.div(input, input_scale).clamp_(-448, 448).to(dtype=torch.float8_e4m3fn) + input_scale = input_scale.to(dtype=torch.float32) + return input, input_scale + + +def fp8_matmul( + input: torch.FloatTensor, + weight: torch.Tensor, + bias: torch.FloatTensor, + scale: torch.FloatTensor, +) -> torch.FloatTensor: + return_dtype = input.dtype + output_shape = list(input.shape) + output_shape[-1] = weight.shape[-1] + input, input_scale = quantize_fp8_matmul_input(input) + return torch._scaled_mm(input, weight, scale_a=input_scale, scale_b=scale, bias=bias, out_dtype=return_dtype).reshape(output_shape) + + +def quantized_linear_forward_fp8_matmul(self, input: torch.FloatTensor) -> torch.FloatTensor: + if torch.numel(input) / input.shape[-1] < 32: + return torch.nn.functional.linear(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) + return fp8_matmul(input, self.weight, self.bias, self.sdnq_dequantizer.scale) + + +if use_torch_compile: + try: + fp8_matmul = torch.compile(fp8_matmul, fullgraph=True, dynamic=False) + except Exception: + pass diff --git a/modules/sdnq/layers/linear/linear_fp8_tensorwise.py b/modules/sdnq/layers/linear/linear_fp8_tensorwise.py new file mode 100644 index 000000000..768719d18 --- /dev/null +++ b/modules/sdnq/layers/linear/linear_fp8_tensorwise.py @@ -0,0 +1,48 @@ +# pylint: disable=relative-beyond-top-level,redefined-builtin,protected-access + +from typing import Tuple + +import torch + +from ...common import use_torch_compile # noqa: TID252 +from ...dequantizer import dequantize_symmetric, dequantize_symmetric_with_bias # noqa: TID252 + + +def quantize_fp8_matmul_input_tensorwise(input: torch.FloatTensor, scale: torch.FloatTensor) -> Tuple[torch.Tensor, torch.FloatTensor]: + input = input.flatten(0,-2).contiguous() + input_scale = torch.amax(input.abs(), dim=-1, keepdims=True).div_(448) + input = torch.div(input, input_scale).clamp_(-448, 448).to(dtype=torch.float8_e4m3fn) + scale = torch.mul(input_scale, scale) + if scale.dtype == torch.float16: # fp16 will overflow + scale = scale.to(dtype=torch.float32) + return input, scale + + +def fp8_matmul_tensorwise( + input: torch.FloatTensor, + weight: torch.Tensor, + bias: torch.FloatTensor, + scale: torch.FloatTensor, +) -> torch.FloatTensor: + return_dtype = input.dtype + output_shape = list(input.shape) + output_shape[-1] = weight.shape[-1] + dummy_input_scale = torch.ones(1, device=input.device, dtype=torch.float32) + input, scale = quantize_fp8_matmul_input_tensorwise(input, scale) + if bias is not None: + return dequantize_symmetric_with_bias(torch._scaled_mm(input, weight, scale_a=dummy_input_scale, scale_b=dummy_input_scale, bias=None, out_dtype=scale.dtype), scale, bias, return_dtype, output_shape) + else: + return dequantize_symmetric(torch._scaled_mm(input, weight, scale_a=dummy_input_scale, scale_b=dummy_input_scale, bias=None, out_dtype=scale.dtype), scale, return_dtype, output_shape) + + +def quantized_linear_forward_fp8_matmul_tensorwise(self, input: torch.FloatTensor) -> torch.FloatTensor: + if torch.numel(input) / input.shape[-1] < 32: + return torch.nn.functional.linear(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) + return fp8_matmul_tensorwise(input, self.weight, self.bias, self.sdnq_dequantizer.scale) + + +if use_torch_compile: + try: + fp8_matmul_tensorwise = torch.compile(fp8_matmul_tensorwise, fullgraph=True, dynamic=False) + except Exception: + pass diff --git a/modules/sdnq/layers/linear/linear_int8.py b/modules/sdnq/layers/linear/linear_int8.py new file mode 100644 index 000000000..1be08a5ae --- /dev/null +++ b/modules/sdnq/layers/linear/linear_int8.py @@ -0,0 +1,52 @@ +# pylint: disable=relative-beyond-top-level,redefined-builtin,protected-access + +from typing import Tuple + +import torch + +from ...common import use_torch_compile # noqa: TID252 +from ...packed_int import unpack_int_symetric # noqa: TID252 +from ...dequantizer import dequantize_symmetric, dequantize_symmetric_with_bias # noqa: TID252 + + +def quantize_int8_matmul_input(input: torch.FloatTensor, scale: torch.FloatTensor) -> Tuple[torch.CharTensor, torch.FloatTensor]: + input = input.flatten(0,-2).contiguous() + input_scale = torch.amax(input.abs(), dim=-1, keepdims=True).div_(127) + input = torch.div(input, input_scale).round_().clamp_(-128, 127).to(dtype=torch.int8) + scale = torch.mul(input_scale, scale) + if scale.dtype == torch.float16: # fp16 will overflow + scale = scale.to(dtype=torch.float32) + return input, scale + + +def int8_matmul( + input: torch.FloatTensor, + weight: torch.Tensor, + bias: torch.FloatTensor, + scale: torch.FloatTensor, + quantized_weight_shape: torch.Size, + weights_dtype: str, +) -> torch.FloatTensor: + if quantized_weight_shape is not None: + weight = unpack_int_symetric(weight, quantized_weight_shape, weights_dtype, dtype=torch.int8, transpose=True) + return_dtype = input.dtype + output_shape = list(input.shape) + output_shape[-1] = weight.shape[-1] + input, scale = quantize_int8_matmul_input(input, scale) + if bias is not None: + return dequantize_symmetric_with_bias(torch._int_mm(input, weight), scale, bias, return_dtype, output_shape) + else: + return dequantize_symmetric(torch._int_mm(input, weight), scale, return_dtype, output_shape) + + +def quantized_linear_forward_int8_matmul(self, input: torch.FloatTensor) -> torch.FloatTensor: + if torch.numel(input) / input.shape[-1] < 32: + return torch.nn.functional.linear(input, self.sdnq_dequantizer(self.weight, skip_quantized_matmul=True), self.bias) + return int8_matmul(input, self.weight, self.bias, self.sdnq_dequantizer.scale, getattr(self.sdnq_dequantizer, "quantized_weight_shape", None), self.sdnq_dequantizer.weights_dtype) + + +if use_torch_compile: + try: + int8_matmul = torch.compile(int8_matmul, fullgraph=True, dynamic=False) + except Exception: + pass diff --git a/modules/sdnq/packed_int.py b/modules/sdnq/packed_int.py index b20c61818..84931d159 100644 --- a/modules/sdnq/packed_int.py +++ b/modules/sdnq/packed_int.py @@ -1,6 +1,7 @@ # pylint: disable=redefined-builtin,no-member,protected-access from typing import Optional + import torch from .common import dtype_dict From 1d5dce1fb1fa3bbe6f64bc45000df827c720cbb9 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Sat, 2 Aug 2025 17:41:53 +0300 Subject: [PATCH 004/141] cleanup --- modules/sdnq/__init__.py | 1 + modules/sdnq/forward.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index e6c012e8c..ef3ef317f 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -3,6 +3,7 @@ from typing import Any, Dict, List, Tuple, Optional, Union from dataclasses import dataclass from enum import Enum + import torch from diffusers.quantizers.base import DiffusersQuantizer from diffusers.quantizers.quantization_config import QuantizationConfigMixin diff --git a/modules/sdnq/forward.py b/modules/sdnq/forward.py index 211b57768..ff2923dfb 100644 --- a/modules/sdnq/forward.py +++ b/modules/sdnq/forward.py @@ -2,8 +2,6 @@ from typing import Callable -import torch - from .common import conv_types, conv_transpose_types From f9b585d9838fdecf7361dca22ad002db872a3e1a Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 Aug 2025 12:00:03 -0400 Subject: [PATCH 005/141] refactor ui_models Signed-off-by: Vladimir Mandic --- cli/civitai-search.py | 191 ++++++++++++++++++++ modules/models_civitai.py | 293 ++++++++++++++++++++++++++++++ modules/models_hf.py | 42 +++++ modules/prompt_parser.py | 2 +- modules/sd_checkpoint.py | 25 ++- modules/shared.py | 2 +- modules/shared_items.py | 29 +-- modules/textual_inversion.py | 2 +- modules/ui_extra_networks.py | 5 +- modules/ui_models.py | 334 ++--------------------------------- 10 files changed, 561 insertions(+), 364 deletions(-) create mode 100644 cli/civitai-search.py create mode 100644 modules/models_civitai.py create mode 100644 modules/models_hf.py diff --git a/cli/civitai-search.py b/cli/civitai-search.py new file mode 100644 index 000000000..e7600fd06 --- /dev/null +++ b/cli/civitai-search.py @@ -0,0 +1,191 @@ +import os +import sys +import json +import time +import logging +import bs4 + + +debug = False +logging.basicConfig(level = logging.INFO, format = '%(asctime)s %(levelname)s: %(message)s') +log = logging.getLogger(__name__) + + +class ModelImage(object): + def __init__(self, dct: dict): + if isinstance(dct, str): + dct = json.loads(dct) + self.dct: dict = dct + self.id: int = dct.get('id', 0) + self.url: str = dct.get('url', '') + self.width: int = dct.get('width', 0) + self.height: int = dct.get('height', 0) + self.type: str = dct.get('type', 'Unknown') + + def __str__(self): + return f'ModelImage(id={self.id} url="{self.url}" width={self.width} height={self.height} type="{self.type}")' + +class ModelFile(object): + def __init__(self, dct: dict): + if isinstance(dct, str): + dct = json.loads(dct) + self.dct: dict = dct + self.id: int = dct.get('id', 0) + self.size: int = int(1024 * dct.get('sizeKB', 0)) + self.name: str = dct.get('name', 'Unknown') + self.type: str = dct.get('type', 'Unknown') + self.hashes: list[str] = dct.get('hashes', {}).values() + self.url: str = dct.get('downloadUrl', '') + + def __str__(self): + return f'ModelFile(id={self.id} name="{self.name}" size={self.size} type="{self.type}" url="{self.url}")' + + +class ModelVersion(object): + def __init__(self, dct: dict): + if isinstance(dct, str): + dct = json.loads(dct) + self.dct = dct + self.id = dct.get('id', 0) + self.name = dct.get('name', 'Unknown') + self.base = dct.get('baseModel', 'Unknown') + self.mtime = dct.get('publishedAt', '') + self.downloads = dct.get('stats', {}).get('downloadCount', 0) + self.availability = dct.get('availability', 'Unknown') + self.html = dct.get('description', '') or '' + self.desc = bs4.BeautifulSoup(self.html, features="html.parser").get_text() + self.files = [ModelFile(f) for f in dct.get('files', [])] + self.images = [ModelImage(i) for i in dct.get('images', [])] + + def __str__(self): + return f'ModelVersion(id={self.id} name="{self.name}" base="{self.base}" mtime="{self.mtime}" downloads={self.downloads} availability={self.availability} desc="{self.desc[:30]}...")' + + +class Model(object): + def __init__(self, dct: dict): + if isinstance(dct, str): + dct = json.loads(dct) + self.id = dct.get('id', 0) + self.dct = dct + self.url = f'https://civitai.com/models/{self.id}' + self.type = dct.get('type', 'Unknown') + self.name = dct.get('name', 'Unknown') + self.html = dct.get('description', '') + self.desc = bs4.BeautifulSoup(self.html, features="html.parser").get_text() + self.tags = dct.get('tags', []) + self.nsfw = dct.get('nsfw', False) + self.level = dct.get('nsfwLevel', 0) + self.availability = dct.get('availability', 'Unknown') + self.downloads = dct.get('stats', {}).get('downloadCount', 0) + self.creator = dct.get('creator', {}).get('username', 'Unknown') + self.versions = [ModelVersion(v) for v in dct.get('modelVersions', [])] + + def __str__(self): + return f'Model(id={self.id} type={self.type} name="{self.name}" versions={len(self.versions)} nsfw={self.nsfw}/{self.level} downloads={self.downloads} author="{self.creator}" tags={self.tags} desc="{self.desc[:30]}...")' + + +def search_civitai( + query:str, + tag:str = '', # optional:tag name + types:str = '', # (Checkpoint, TextualInversion, Hypernetwork, AestheticGradient, LORA, Controlnet, Poses) + sort:str = '', # (Highest Rated, Most Downloaded, Newest) + period:str = '', # (AllTime, Year, Month, Week, Day) + nsfw:bool = None, # optional:bool + limit:int = 0, + base:list[str] = [], # list + token:str = None, + exact:bool = True, +): + import requests + from urllib.parse import urlencode + + if len(query) == 0: + log.error('CivitAI: empty query') + return [] + + t0 = time.time() + dct = { 'query': query } + if len(tag) > 0: + dct['tag'] = tag + if nsfw is not None: + dct['nsfw'] = 'true' if nsfw else 'false' + if limit > 0: + dct['limit'] = limit + if len(types) > 0: + dct['types'] = types + if len(sort) > 0: + dct['sort'] = sort + if len(period) > 0: + dct['period'] = period + if len(base) > 0: + dct['baseModels'] = ','.join(base) + encoded = urlencode(dct) + + headers = {} + if token is None: + token = os.environ.get('CIVITAI_TOKEN', None) + if token is not None and len(token) > 0: + headers['Authorization'] = f'Bearer {token}' + + url = 'https://civitai.com/api/v1/models' + uri = f'{url}?{encoded}' + log.info(f'CivitAI request: uri="{uri}" dct={dct} token={token is not None}') + result = requests.get(uri, headers=headers, timeout=60) + + if result.status_code != 200: + log.error(f'CivitAI: code={result.status_code} reason={result.reason} uri={result.url}') + return [] + + models: list[Model] = [] + exact_models: list[Model] = [] + items = result.json().get('items', []) + for item in items: + models.append(Model(item)) + + if exact: + for model in models: + model_names = [model.name.lower()] + version_names = [v.name.lower() for v in model.versions] + file_names = [f.name.lower() for v in model.versions for f in v.files] + if any([query.lower() in name for name in model_names + version_names + file_names]): + exact_models.append(model) + + t1 = time.time() + log.info(f'CivitAI result: code={result.status_code} exact={len(exact_models)} total={len(models)} time={t1-t0:.2f}') + return exact_models if len(exact_models) > 0 else models + + +def print_models(models: list[Model]): + if debug: + from rich import print as dbg + else: + dbg = lambda *args, **kwargs: None # pylint: disable=unnecessary-lambda-assignment + for model in models: + log.info(f' {model}') + dbg('Model', model.dct) + for version in model.versions: + log.info(f' {version}') + dbg('ModelVersion', version.dct) + for file in version.files: + log.info(f' {file}') + dbg('ModelFile', file.dct) + for image in version.images: + log.info(f' {image}') + dbg('ModelImage', image.dct) + + +if __name__ == "__main__": + sys.argv.pop(0) + txt = ' '.join(sys.argv) + res = search_civitai( + query=txt, + # tag = '', + # types = '', + # sort = 'Most Downloaded', + # period = 'Year', + # nsfw = True, + # base = [], + # exact= True, + # limit=100, + ) + print_models(res) diff --git a/modules/models_civitai.py b/modules/models_civitai.py new file mode 100644 index 000000000..9b2f5420e --- /dev/null +++ b/modules/models_civitai.py @@ -0,0 +1,293 @@ +import os +import re +import time +import json +import gradio as gr +from modules.shared import log, opts, req, readfile, max_workers + + +data = [] +selected_model = None +update_data = [] + + +class CivitModel: + def __init__(self, name, fn, sha = None, meta = {}): + self.name = name + self.id = meta.get('id', 0) + self.fn = fn + self.sha = sha + self.meta = meta + self.versions = 0 + self.vername = '' + self.latest = '' + self.latest_hashes = [] + self.latest_name = '' + self.url = None + self.status = 'Not found' + def array(self): + return [self.id, self.fn, self.name, self.versions, self.vername, self.latest, self.status] + + +def civit_update_metadata(): + log.debug('CivitAI update metadata: models') + from modules import ui_extra_networks, modelloader + res = [] + pages = ui_extra_networks.get_pages('Model') + if len(pages) == 0: + return 'CivitAI update metadata: no models found' + page: ui_extra_networks.ExtraNetworksPage = pages[0] + table_data = [] + update_data.clear() + all_hashes = [(item.get('hash', None) or 'XXXXXXXX').upper()[:8] for item in page.list_items()] + for item in page.list_items(): + model = CivitModel(name=item['name'], fn=item['filename'], sha=item.get('hash', None), meta=item.get('metadata', {})) + if model.sha is None or len(model.sha) == 0: + res.append(f'CivitAI skip search: name="{model.name}" hash=None') + else: + r = req(f'https://civitai.com/api/v1/model-versions/by-hash/{model.sha}') + res.append(f'CivitAI search: name="{model.name}" hash={model.sha} status={r.status_code}') + if r.status_code == 200: + d = r.json() + model.id = d['modelId'] + modelloader.download_civit_meta(model.fn, model.id) + fn = os.path.splitext(item['filename'])[0] + '.json' + model.meta = readfile(fn, silent=True) + model.name = model.meta.get('name', model.name) + model.versions = len(model.meta.get('modelVersions', [])) + versions = model.meta.get('modelVersions', []) + if len(versions) > 0: + model.latest = versions[0].get('name', '') + model.latest_hashes.clear() + for v in versions[0].get('files', []): + for h in v.get('hashes', {}).values(): + model.latest_hashes.append(h[:8].upper()) + for ver in versions: + for f in ver.get('files', []): + for h in f.get('hashes', {}).values(): + if h[:8].upper() == model.sha[:8].upper(): + model.vername = ver.get('name', '') + model.url = f.get('downloadUrl', None) + model.latest_name = f.get('name', '') + if model.vername == model.latest: + model.status = 'Latest' + elif any(map(lambda v: v in model.latest_hashes, all_hashes)): # pylint: disable=cell-var-from-loop # noqa: C417 + model.status = 'Downloaded' + else: + model.status = 'Available' + break + log.debug(res[-1]) + update_data.append(model) + table_data.append(model.array()) + yield gr.update(value=table_data), '
'.join([r for r in res if len(r) > 0]) + return '
'.join([r for r in res if len(r) > 0]) + +def civit_update_select(evt: gr.SelectData, in_data): + global selected_model # pylint: disable=global-statement + try: + selected_model = next([m for m in update_data if m.fn == in_data[evt.index[0]][1]]) + except Exception: + selected_model = None + if selected_model is None or selected_model.url is None or selected_model.status != 'Available': + return [gr.update(value='Model update not available'), gr.update(visible=False)] + else: + return [gr.update(), gr.update(visible=True)] + +def civit_update_download(): + if selected_model is None or selected_model.url is None or selected_model.status != 'Available': + return 'Model update not available' + if selected_model.latest_name is None or len(selected_model.latest_name) == 0: + model_name = f'{selected_model.name} {selected_model.latest}.safetensors' + else: + model_name = selected_model.latest_name + return civit_download_model(selected_model.url, model_name, model_path='', model_type='Model') + + +def civit_search_model(name, tag, model_type): + # types = 'LORA' if model_type == 'LoRA' else 'Checkpoint' + url = 'https://civitai.com/api/v1/models?limit=25&Sort=Newest' + if model_type == 'Model': + url += '&types=Checkpoint' + elif model_type == 'LoRA': + url += '&types=LORA&types=DoRA&types=LoCon' + elif model_type == 'Embedding': + url += '&types=TextualInversion' + elif model_type == 'VAE': + url += '&types=VAE' + if name is not None and len(name) > 0: + url += f'&query={name}' + if tag is not None and len(tag) > 0: + url += f'&tag={tag}' + r = req(url) + log.debug(f'CivitAI search: type={model_type} name="{name}" tag={tag or "none"} url="{url}" status={r.status_code}') + if r.status_code != 200: + log.warning(f'CivitAI search: name="{name}" tag={tag} status={r.status_code}') + return [], gr.update(visible=False, value=[]), gr.update(visible=False, value=None), gr.update(visible=False, value=None) + try: + body = r.json() + except Exception as e: + log.error(f'CivitAI search: name="{name}" tag={tag} {e}') + return [], gr.update(visible=False, value=[]), gr.update(visible=False, value=None), gr.update(visible=False, value=None) + global data # pylint: disable=global-statement + data = body.get('items', []) + data1 = [] + for model in data: + found = 0 + if model_type == 'LoRA' and model['type'].lower() in ['lora', 'locon', 'dora', 'lycoris']: + found += 1 + elif model_type == 'Embedding' and model['type'].lower() in ['textualinversion', 'embedding']: + found += 1 + elif model_type == 'Model' and model['type'].lower() in ['checkpoint']: + found += 1 + elif model_type == 'VAE' and model['type'].lower() in ['vae']: + found += 1 + elif model_type == 'Other': + found += 1 + if found > 0: + data1.append([ + model['id'], + model['name'], + ', '.join(model['tags']), + model['stats']['downloadCount'], + model['stats']['rating'] + ]) + res = f'Search result: name={name} tag={tag or "none"} type={model_type} models={len(data1)}' + return res, gr.update(visible=len(data1) > 0, value=data1 if len(data1) > 0 else []), gr.update(visible=False, value=None), gr.update(visible=False, value=None) + + +def civit_select1(evt: gr.SelectData, in_data): + model_id = in_data[evt.index[0]][0] + data2 = [] + preview_img = None + for model in data: + if model['id'] == model_id: + for d in model['modelVersions']: + try: + if d.get('images') is not None and len(d['images']) > 0 and len(d['images'][0]['url']) > 0: + preview_img = d['images'][0]['url'] + data2.append([d.get('id', None), d.get('modelId', None) or model_id, d.get('name', None), d.get('baseModel', None), d.get('createdAt', None) or d.get('publishedAt', None)]) + except Exception as e: + log.error(f'CivitAI select: model="{in_data[evt.index[0]]}" {e}') + log.error(f'CivitAI version data={type(d)}: {d}') + log.debug(f'CivitAI select: model="{in_data[evt.index[0]]}" versions={len(data2)}') + return data2, None, preview_img + + +def civit_select2(evt: gr.SelectData, in_data): + variant_id = in_data[evt.index[0]][0] + model_id = in_data[evt.index[0]][1] + data3 = [] + for model in data: + if model['id'] == model_id: + for variant in model['modelVersions']: + if variant['id'] == variant_id: + for f in variant['files']: + try: + if os.path.splitext(f['name'])[1].lower() in ['.safetensors', '.ckpt', '.pt', '.pth', '.bin']: + data3.append([f['name'], round(f['sizeKB']), json.dumps(f['metadata']), f['downloadUrl']]) + except Exception: + pass + log.debug(f'CivitAI select: model="{in_data[evt.index[0]]}" files={len(data3)}') + return data3 + + +def civit_select3(evt: gr.SelectData, in_data): + log.debug(f'CivitAI select: variant={in_data[evt.index[0]]}') + return in_data[evt.index[0]][3], in_data[evt.index[0]][0], gr.update(interactive=True) + + +def civit_download_model(model_url: str, model_name: str, model_path: str, model_type: str, token: str = None): + if model_url is None or len(model_url) == 0: + return 'No model selected' + try: + from modules.modelloader import download_civit_model + res = download_civit_model(model_url, model_name, model_path, model_type, token=token) + except Exception as e: + res = f"CivitAI model downloaded error: model={model_url} {e}" + log.error(res) + return res + from modules.sd_models import list_models # pylint: disable=W0621 + list_models() + return res + + +def atomic_civit_search_metadata(item, res, rehash): + from modules.modelloader import download_civit_preview, download_civit_meta + if item is None: + return + meta = os.path.splitext(item['filename'])[0] + '.json' + has_meta = os.path.isfile(meta) and os.stat(meta).st_size > 0 + if ('card-no-preview.png' in item['preview'] or not has_meta) and os.path.isfile(item['filename']): + sha = item.get('hash', None) + found = False + if sha is not None and len(sha) > 0: + r = req(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}') + log.debug(f'CivitAI search: name="{item["name"]}" hash={sha} status={r.status_code}') + if r.status_code == 200: + d = r.json() + res.append(download_civit_meta(item['filename'], d['modelId'])) + if d.get('images') is not None: + for i in d['images']: + preview_url = i['url'] + img_res = download_civit_preview(item['filename'], preview_url) + res.append(img_res) + if 'error' not in img_res: + found = True + break + if not found and rehash and os.stat(item['filename']).st_size < (1024 * 1024 * 1024): + from modules import hashes + sha = hashes.calculate_sha256(item['filename'], quiet=True)[:10] + r = req(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}') + log.debug(f'CivitAI search: name="{item["name"]}" hash={sha} status={r.status_code}') + if r.status_code == 200: + d = r.json() + res.append(download_civit_meta(item['filename'], d['modelId'])) + if d.get('images') is not None: + for i in d['images']: + preview_url = i['url'] + img_res = download_civit_preview(item['filename'], preview_url) + res.append(img_res) + if 'error' not in img_res: + found = True + break + + +def civit_search_metadata(rehash, title): + log.debug(f'CivitAI search metadata: type={title if type(title) == str else "all"}') + from modules.ui_extra_networks import get_pages + res = [] + scanned, skipped = 0, 0 + t0 = time.time() + candidates = [] + re_skip = [r.strip() for r in opts.extra_networks_scan_skip.split(',') if len(r.strip()) > 0] + log.debug(f'CivitAI search metadata: skip={re_skip}') + for page in get_pages(): + if type(title) == str: + if page.title != title: + continue + if page.name == 'style': + continue + for item in page.list_items(): + if item is None: + continue + if any(re.search(re_str, item.get('name', '') + item.get('filename', '')) for re_str in re_skip): + skipped += 1 + continue + scanned += 1 + candidates.append(item) + # atomic_civit_search_metadata(item, res, rehash) + import concurrent + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + for fn in candidates: + executor.submit(atomic_civit_search_metadata, fn, res, rehash) + atomic_civit_search_metadata(None, res, rehash) + t1 = time.time() + log.debug(f'CivitAI search metadata: scanned={scanned} skipped={skipped} time={t1-t0:.2f}') + txt = '
'.join([r for r in res if len(r) > 0]) + return txt + + +def civitai_update_token(token): + log.debug('CivitAI update token') + opts.civitai_token = token + opts.save() diff --git a/modules/models_hf.py b/modules/models_hf.py new file mode 100644 index 000000000..801fafc04 --- /dev/null +++ b/modules/models_hf.py @@ -0,0 +1,42 @@ +import os +import gradio as gr +from modules.shared import log, opts + + +def hf_init(): + os.environ.setdefault('HF_HUB_DISABLE_EXPERIMENTAL_WARNING', '1') + os.environ.setdefault('HF_HUB_DISABLE_SYMLINKS_WARNING', '1') + os.environ.setdefault('HF_HUB_DISABLE_IMPLICIT_TOKEN', '1') + os.environ.setdefault('HUGGINGFACE_HUB_VERBOSITY', 'warning') + + +def hf_search(keyword): + hf_init() + import huggingface_hub as hf + hf_api = hf.HfApi() + models = hf_api.list_models(model_name=keyword, full=True, library="diffusers", limit=50, sort="downloads", direction=-1) + data = [] + for model in models: + tags = [t for t in model.tags if not t.startswith('diffusers') and not t.startswith('license') and not t.startswith('arxiv') and len(t) > 2] + data.append([model.id, model.pipeline_tag, tags, model.downloads, model.lastModified, f'https://huggingface.co/{model.id}']) + return data + + +def hf_select(evt: gr.SelectData, data): + return data[evt.index[0]][0] + + +def hf_download_model(hub_id: str, token, variant, revision, mirror, custom_pipeline): + hf_init() + from modules.modelloader import download_diffusers_model + download_diffusers_model(hub_id, cache_dir=opts.diffusers_dir, token=token, variant=variant, revision=revision, mirror=mirror, custom_pipeline=custom_pipeline) + from modules.sd_models import list_models # pylint: disable=W0621 + list_models() + log.info(f'Diffuser model downloaded: model="{hub_id}"') + return f'Diffuser model downloaded: model="{hub_id}"' + + +def hf_update_token(token): + log.debug('Huggingface update token') + opts.huggingface_token = token + opts.save() diff --git a/modules/prompt_parser.py b/modules/prompt_parser.py index 92389c1fd..cb8789aea 100644 --- a/modules/prompt_parser.py +++ b/modules/prompt_parser.py @@ -334,7 +334,7 @@ def parse_prompt_attention(text): whitespace = '' else: re_attention = re_attention_v2 - if native and opts.sd_textencder_linebreak: + if opts.sd_textencder_linebreak: text = text.replace('\n', ' BREAK ') else: text = text.replace('\n', ' ') diff --git a/modules/sd_checkpoint.py b/modules/sd_checkpoint.py index 1c9bfe117..b54eb701f 100644 --- a/modules/sd_checkpoint.py +++ b/modules/sd_checkpoint.py @@ -127,7 +127,7 @@ def list_models(): global checkpoints_list # pylint: disable=global-statement checkpoints_list.clear() checkpoint_aliases.clear() - ext_filter = [".safetensors"] if shared.opts.sd_disable_ckpt or shared.native else [".ckpt", ".safetensors"] + ext_filter = [".safetensors"] model_list = list(modelloader.load_models(model_path=model_path, model_url=None, command_path=shared.opts.ckpt_dir, ext_filter=ext_filter, download_name=None, ext_blacklist=[".vae.ckpt", ".vae.safetensors"])) safetensors_list = [] for filename in sorted(model_list, key=str.lower): @@ -136,21 +136,16 @@ def list_models(): if checkpoint_info.name is not None: checkpoint_info.register() diffusers_list = [] - if shared.native: - for repo in modelloader.load_diffusers_models(clear=True): - checkpoint_info = CheckpointInfo(repo['name'], sha=repo['hash']) - diffusers_list.append(checkpoint_info) - if checkpoint_info.name is not None: - checkpoint_info.register() + for repo in modelloader.load_diffusers_models(clear=True): + checkpoint_info = CheckpointInfo(repo['name'], sha=repo['hash']) + diffusers_list.append(checkpoint_info) + if checkpoint_info.name is not None: + checkpoint_info.register() if shared.cmd_opts.ckpt is not None: - if not os.path.exists(shared.cmd_opts.ckpt) and not shared.native: - if shared.cmd_opts.ckpt.lower() != "none": - shared.log.warning(f'Load model: path="{shared.cmd_opts.ckpt}" not found') - else: - checkpoint_info = CheckpointInfo(shared.cmd_opts.ckpt) - if checkpoint_info.name is not None: - checkpoint_info.register() - shared.opts.data['sd_model_checkpoint'] = checkpoint_info.title + checkpoint_info = CheckpointInfo(shared.cmd_opts.ckpt) + if checkpoint_info.name is not None: + checkpoint_info.register() + shared.opts.data['sd_model_checkpoint'] = checkpoint_info.title elif shared.cmd_opts.ckpt != shared.default_sd_model_file and shared.cmd_opts.ckpt is not None: shared.log.warning(f'Load model: path="{shared.cmd_opts.ckpt}" not found') shared.log.info(f'Available Models: safetensors="{shared.opts.ckpt_dir}":{len(safetensors_list)} diffusers="{shared.opts.diffusers_dir}":{len(diffusers_list)} items={len(checkpoints_list)} time={time.time()-t0:.2f}') diff --git a/modules/shared.py b/modules/shared.py index 6fed4d343..f9edba6a4 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -264,7 +264,7 @@ options_templates.update(options_section(('cuda', "Compute Settings"), { "diffusers_generator_device": OptionInfo("GPU", "Generator device", gr.Radio, {"choices": ["GPU", "CPU", "Unset"]}), "cross_attention_sep": OptionInfo("

Cross Attention

", "", gr.HTML), - "cross_attention_optimization": OptionInfo(startup_cross_attention, "Attention optimization method", gr.Radio, lambda: {"choices": shared_items.list_crossattention(native)}), + "cross_attention_optimization": OptionInfo(startup_cross_attention, "Attention optimization method", gr.Radio, lambda: {"choices": shared_items.list_crossattention()}), "sdp_options": OptionInfo(startup_sdp_options, "SDP options", gr.CheckboxGroup, {"choices": ['Flash attention', 'Memory attention', 'Math attention', 'Dynamic attention', 'CK Flash attention', 'Sage attention']}), "xformers_options": OptionInfo(['Flash attention'], "xFormers options", gr.CheckboxGroup, {"choices": ['Flash attention'] }), "dynamic_attention_slice_rate": OptionInfo(0.5, "Dynamic Attention slicing rate in GB", gr.Slider, {"minimum": 0.01, "maximum": max(gpu_memory,4), "step": 0.01}), diff --git a/modules/shared_items.py b/modules/shared_items.py index 483186905..74b087739 100644 --- a/modules/shared_items.py +++ b/modules/shared_items.py @@ -105,26 +105,15 @@ def refresh_te_list(): modules.model_te.refresh_te_list() -def list_crossattention(native:bool=True): - if native: - return [ - "Disabled", - "Scaled-Dot-Product", - "xFormers", - "Batch matrix-matrix", - "Split attention", - "Dynamic Attention BMM" - ] - else: - return [ - "Disabled", - "Scaled-Dot-Product", - "xFormers", - "Doggettx's", - "InvokeAI's", - "Sub-quadratic", - "Split attention" - ] +def list_crossattention(): + return [ + "Disabled", + "Scaled-Dot-Product", + "xFormers", + "Batch matrix-matrix", + "Split attention", + "Dynamic Attention BMM" + ] def get_pipelines(): from installer import log diff --git a/modules/textual_inversion.py b/modules/textual_inversion.py index f6b1c558d..4d7b76a77 100644 --- a/modules/textual_inversion.py +++ b/modules/textual_inversion.py @@ -13,7 +13,7 @@ supported_models = ['ldm', 'sd', 'sdxl'] def list_embeddings(*dirs): - is_ext = extension_filter(['.SAFETENSORS', '.PT' ] + ( ['.PNG', '.WEBP', '.JXL', '.AVIF', '.BIN' ] if not shared.native else [] )) + is_ext = extension_filter(['.SAFETENSORS', '.PT' ]) is_not_preview = lambda fp: not next(iter(os.path.splitext(fp))).upper().endswith('.PREVIEW') # pylint: disable=unnecessary-lambda-assignment return list(filter(lambda fp: is_ext(fp) and is_not_preview(fp) and os.stat(fp).st_size > 0, directory_files(*dirs))) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 7787ea1fc..b8c777f92 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -932,9 +932,8 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): return pages def ui_scan_click(title): - from modules import ui_models - if ui_models.search_metadata_civit is not None: - ui_models.search_metadata_civit(True, title) + from modules.models_civitai import civit_search_metadata + civit_search_metadata(True, title) return ui_refresh_click(title) def ui_save_click(): diff --git a/modules/ui_models.py b/modules/ui_models.py index 1a16d1b44..bb83199c7 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -1,21 +1,15 @@ import os -import re -import time import json import inspect from datetime import datetime import gradio as gr -from modules import errors, sd_models, sd_vae, extras, sd_samplers, ui_symbols, hashes +from modules import errors, sd_models, sd_vae, extras, sd_samplers, ui_symbols from modules.ui_components import ToolButton from modules.ui_common import create_refresh_button from modules.call_queue import wrap_gradio_gpu_call -from modules.shared import opts, log, req, readfile, max_workers, native -from modules.merging import merge_methods -from modules.merging.merge_utils import BETA_METHODS, TRIPLE_METHODS, interpolate -from modules.merging.merge_presets import BLOCK_WEIGHTS_PRESETS, SDXL_BLOCK_WEIGHTS_PRESETS +from modules.shared import opts, log -search_metadata_civit = None extra_ui = [] @@ -67,6 +61,10 @@ def create_ui(): ui_models_load.create_ui(models_outcome, models_file) with gr.Tab(label="Merge"): + from modules.merging import merge_methods + from modules.merging.merge_utils import BETA_METHODS, TRIPLE_METHODS, interpolate + from modules.merging.merge_presets import BLOCK_WEIGHTS_PRESETS, SDXL_BLOCK_WEIGHTS_PRESETS + def sd_model_choices(): return ['None'] + sd_models.checkpoint_titles() @@ -420,38 +418,7 @@ def create_ui(): model_list_btn.click(fn=list_models, inputs=[], outputs=[model_table, models_outcome]) with gr.Tab(label="Huggingface"): - data = [] - os.environ.setdefault('HF_HUB_DISABLE_EXPERIMENTAL_WARNING', '1') - os.environ.setdefault('HF_HUB_DISABLE_SYMLINKS_WARNING', '1') - os.environ.setdefault('HF_HUB_DISABLE_IMPLICIT_TOKEN', '1') - os.environ.setdefault('HUGGINGFACE_HUB_VERBOSITY', 'warning') - - def hf_search(keyword): - import huggingface_hub as hf - hf_api = hf.HfApi() - models = hf_api.list_models(model_name=keyword, full=True, library="diffusers", limit=50, sort="downloads", direction=-1) - data.clear() - for model in models: - tags = [t for t in model.tags if not t.startswith('diffusers') and not t.startswith('license') and not t.startswith('arxiv') and len(t) > 2] - data.append([model.id, model.pipeline_tag, tags, model.downloads, model.lastModified, f'https://huggingface.co/{model.id}']) - return data - - def hf_select(evt: gr.SelectData, data): - return data[evt.index[0]][0] - - def hf_download_model(hub_id: str, token, variant, revision, mirror, custom_pipeline): - from modules.modelloader import download_diffusers_model - download_diffusers_model(hub_id, cache_dir=opts.diffusers_dir, token=token, variant=variant, revision=revision, mirror=mirror, custom_pipeline=custom_pipeline) - from modules.sd_models import list_models # pylint: disable=W0621 - list_models() - log.info(f'Diffuser model downloaded: model="{hub_id}"') - return f'Diffuser model downloaded: model="{hub_id}"' - - def hf_update_token(token): - log.debug('Huggingface update token') - opts.huggingface_token = token - opts.save() - + from modules.models_hf import hf_search, hf_select, hf_download_model, hf_update_token with gr.Column(scale=6): with gr.Row(): gr.HTML('

 Download model from huggingface

') @@ -486,192 +453,7 @@ def create_ui(): hf_token.change(fn=hf_update_token, inputs=[hf_token], outputs=[]) with gr.Tab(label="CivitAI"): - data = [] - - def civit_search_model(name, tag, model_type): - # types = 'LORA' if model_type == 'LoRA' else 'Checkpoint' - url = 'https://civitai.com/api/v1/models?limit=25&Sort=Newest' - if model_type == 'Model': - url += '&types=Checkpoint' - elif model_type == 'LoRA': - url += '&types=LORA&types=DoRA&types=LoCon' - elif model_type == 'Embedding': - url += '&types=TextualInversion' - elif model_type == 'VAE': - url += '&types=VAE' - if name is not None and len(name) > 0: - url += f'&query={name}' - if tag is not None and len(tag) > 0: - url += f'&tag={tag}' - r = req(url) - log.debug(f'CivitAI search: type={model_type} name="{name}" tag={tag or "none"} url="{url}" status={r.status_code}') - if r.status_code != 200: - log.warning(f'CivitAI search: name="{name}" tag={tag} status={r.status_code}') - return [], gr.update(visible=False, value=[]), gr.update(visible=False, value=None), gr.update(visible=False, value=None) - try: - body = r.json() - except Exception as e: - log.error(f'CivitAI search: name="{name}" tag={tag} {e}') - return [], gr.update(visible=False, value=[]), gr.update(visible=False, value=None), gr.update(visible=False, value=None) - nonlocal data - data = body.get('items', []) - data1 = [] - for model in data: - found = 0 - if model_type == 'LoRA' and model['type'].lower() in ['lora', 'locon', 'dora', 'lycoris']: - found += 1 - elif model_type == 'Embedding' and model['type'].lower() in ['textualinversion', 'embedding']: - found += 1 - elif model_type == 'Model' and model['type'].lower() in ['checkpoint']: - found += 1 - elif model_type == 'VAE' and model['type'].lower() in ['vae']: - found += 1 - elif model_type == 'Other': - found += 1 - if found > 0: - data1.append([ - model['id'], - model['name'], - ', '.join(model['tags']), - model['stats']['downloadCount'], - model['stats']['rating'] - ]) - res = f'Search result: name={name} tag={tag or "none"} type={model_type} models={len(data1)}' - return res, gr.update(visible=len(data1) > 0, value=data1 if len(data1) > 0 else []), gr.update(visible=False, value=None), gr.update(visible=False, value=None) - - def civit_select1(evt: gr.SelectData, in_data): - model_id = in_data[evt.index[0]][0] - data2 = [] - preview_img = None - for model in data: - if model['id'] == model_id: - for d in model['modelVersions']: - try: - if d.get('images') is not None and len(d['images']) > 0 and len(d['images'][0]['url']) > 0: - preview_img = d['images'][0]['url'] - data2.append([d.get('id', None), d.get('modelId', None) or model_id, d.get('name', None), d.get('baseModel', None), d.get('createdAt', None) or d.get('publishedAt', None)]) - except Exception as e: - log.error(f'CivitAI select: model="{in_data[evt.index[0]]}" {e}') - log.error(f'CivitAI version data={type(d)}: {d}') - log.debug(f'CivitAI select: model="{in_data[evt.index[0]]}" versions={len(data2)}') - return data2, None, preview_img - - def civit_select2(evt: gr.SelectData, in_data): - variant_id = in_data[evt.index[0]][0] - model_id = in_data[evt.index[0]][1] - data3 = [] - for model in data: - if model['id'] == model_id: - for variant in model['modelVersions']: - if variant['id'] == variant_id: - for f in variant['files']: - try: - if os.path.splitext(f['name'])[1].lower() in ['.safetensors', '.ckpt', '.pt', '.pth', '.bin']: - data3.append([f['name'], round(f['sizeKB']), json.dumps(f['metadata']), f['downloadUrl']]) - except Exception: - pass - log.debug(f'CivitAI select: model="{in_data[evt.index[0]]}" files={len(data3)}') - return data3 - - def civit_select3(evt: gr.SelectData, in_data): - log.debug(f'CivitAI select: variant={in_data[evt.index[0]]}') - return in_data[evt.index[0]][3], in_data[evt.index[0]][0], gr.update(interactive=True) - - def civit_download_model(model_url: str, model_name: str, model_path: str, model_type: str, token: str = None): - if model_url is None or len(model_url) == 0: - return 'No model selected' - try: - from modules.modelloader import download_civit_model - res = download_civit_model(model_url, model_name, model_path, model_type, token=token) - except Exception as e: - res = f"CivitAI model downloaded error: model={model_url} {e}" - log.error(res) - return res - from modules.sd_models import list_models # pylint: disable=W0621 - list_models() - return res - - def atomic_civit_search_metadata(item, res, rehash): - from modules.modelloader import download_civit_preview, download_civit_meta - if item is None: - return - meta = os.path.splitext(item['filename'])[0] + '.json' - has_meta = os.path.isfile(meta) and os.stat(meta).st_size > 0 - if ('card-no-preview.png' in item['preview'] or not has_meta) and os.path.isfile(item['filename']): - sha = item.get('hash', None) - found = False - if sha is not None and len(sha) > 0: - r = req(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}') - log.debug(f'CivitAI search: name="{item["name"]}" hash={sha} status={r.status_code}') - if r.status_code == 200: - d = r.json() - res.append(download_civit_meta(item['filename'], d['modelId'])) - if d.get('images') is not None: - for i in d['images']: - preview_url = i['url'] - img_res = download_civit_preview(item['filename'], preview_url) - res.append(img_res) - if 'error' not in img_res: - found = True - break - if not found and rehash and os.stat(item['filename']).st_size < (1024 * 1024 * 1024): - sha = hashes.calculate_sha256(item['filename'], quiet=True)[:10] - r = req(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}') - log.debug(f'CivitAI search: name="{item["name"]}" hash={sha} status={r.status_code}') - if r.status_code == 200: - d = r.json() - res.append(download_civit_meta(item['filename'], d['modelId'])) - if d.get('images') is not None: - for i in d['images']: - preview_url = i['url'] - img_res = download_civit_preview(item['filename'], preview_url) - res.append(img_res) - if 'error' not in img_res: - found = True - break - - def civit_search_metadata(rehash, title): - log.debug(f'CivitAI search metadata: type={title if type(title) == str else "all"}') - from modules.ui_extra_networks import get_pages - res = [] - scanned, skipped = 0, 0 - t0 = time.time() - candidates = [] - re_skip = [r.strip() for r in opts.extra_networks_scan_skip.split(',') if len(r.strip()) > 0] - log.debug(f'CivitAI search metadata: skip={re_skip}') - for page in get_pages(): - if type(title) == str: - if page.title != title: - continue - if page.name == 'style': - continue - for item in page.list_items(): - if item is None: - continue - if any(re.search(re_str, item.get('name', '') + item.get('filename', '')) for re_str in re_skip): - skipped += 1 - continue - scanned += 1 - candidates.append(item) - # atomic_civit_search_metadata(item, res, rehash) - import concurrent - with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: - for fn in candidates: - executor.submit(atomic_civit_search_metadata, fn, res, rehash) - atomic_civit_search_metadata(None, res, rehash) - t1 = time.time() - log.debug(f'CivitAI search metadata: scanned={scanned} skipped={skipped} time={t1-t0:.2f}') - txt = '
'.join([r for r in res if len(r) > 0]) - return txt - - global search_metadata_civit # pylint: disable=global-statement - search_metadata_civit = civit_search_metadata - - def civitai_update_token(token): - log.debug('CivitAI update token') - opts.civitai_token = token - opts.save() - + from modules.models_civitai import civitai_update_token, civit_search_model, civit_search_metadata, civit_select1, civit_select2, civit_select3, civit_download_model with gr.Row(): gr.HTML('

 CivitAI fetch metadata

') gr.HTML('Fetches preview and metadata information for all models with missing information
Models with existing previews and information are not updated
') @@ -737,6 +519,7 @@ def create_ui(): civit_previews_btn.click(fn=civit_search_metadata, inputs=[civit_previews_rehash, civit_previews_rehash], outputs=[models_outcome]) with gr.Tab(label="Update"): + from modules.models_civitai import civit_update_metadata, civit_update_select, civit_update_download with gr.Row(): gr.HTML('

 Scan CivitAI for information on latest available model versions

') with gr.Row(): @@ -753,107 +536,12 @@ def create_ui(): with gr.Row(): civit_update_download_btn = gr.Button(value="Download", variant='primary', visible=False) - class CivitModel: - def __init__(self, name, fn, sha = None, meta = {}): - self.name = name - self.id = meta.get('id', 0) - self.fn = fn - self.sha = sha - self.meta = meta - self.versions = 0 - self.vername = '' - self.latest = '' - self.latest_hashes = [] - self.latest_name = '' - self.url = None - self.status = 'Not found' - def array(self): - return [self.id, self.fn, self.name, self.versions, self.vername, self.latest, self.status] - - selected_model: CivitModel = None - update_data = [] - - def civit_update_metadata(): - nonlocal update_data - log.debug('CivitAI update metadata: models') - from modules import ui_extra_networks, modelloader - res = [] - pages = ui_extra_networks.get_pages('Model') - if len(pages) == 0: - return 'CivitAI update metadata: no models found' - page: ui_extra_networks.ExtraNetworksPage = pages[0] - table_data = [] - update_data.clear() - all_hashes = [(item.get('hash', None) or 'XXXXXXXX').upper()[:8] for item in page.list_items()] - for item in page.list_items(): - model = CivitModel(name=item['name'], fn=item['filename'], sha=item.get('hash', None), meta=item.get('metadata', {})) - if model.sha is None or len(model.sha) == 0: - res.append(f'CivitAI skip search: name="{model.name}" hash=None') - else: - r = req(f'https://civitai.com/api/v1/model-versions/by-hash/{model.sha}') - res.append(f'CivitAI search: name="{model.name}" hash={model.sha} status={r.status_code}') - if r.status_code == 200: - d = r.json() - model.id = d['modelId'] - modelloader.download_civit_meta(model.fn, model.id) - fn = os.path.splitext(item['filename'])[0] + '.json' - model.meta = readfile(fn, silent=True) - model.name = model.meta.get('name', model.name) - model.versions = len(model.meta.get('modelVersions', [])) - versions = model.meta.get('modelVersions', []) - if len(versions) > 0: - model.latest = versions[0].get('name', '') - model.latest_hashes.clear() - for v in versions[0].get('files', []): - for h in v.get('hashes', {}).values(): - model.latest_hashes.append(h[:8].upper()) - for ver in versions: - for f in ver.get('files', []): - for h in f.get('hashes', {}).values(): - if h[:8].upper() == model.sha[:8].upper(): - model.vername = ver.get('name', '') - model.url = f.get('downloadUrl', None) - model.latest_name = f.get('name', '') - if model.vername == model.latest: - model.status = 'Latest' - elif any(map(lambda v: v in model.latest_hashes, all_hashes)): # pylint: disable=cell-var-from-loop # noqa: C417 - model.status = 'Downloaded' - else: - model.status = 'Available' - break - log.debug(res[-1]) - update_data.append(model) - table_data.append(model.array()) - yield gr.update(value=table_data), '
'.join([r for r in res if len(r) > 0]) - return '
'.join([r for r in res if len(r) > 0]) - - def civit_update_select(evt: gr.SelectData, in_data): - nonlocal selected_model, update_data - try: - selected_model = next([m for m in update_data if m.fn == in_data[evt.index[0]][1]]) - except Exception: - selected_model = None - if selected_model is None or selected_model.url is None or selected_model.status != 'Available': - return [gr.update(value='Model update not available'), gr.update(visible=False)] - else: - return [gr.update(), gr.update(visible=True)] - - def civit_update_download(): - if selected_model is None or selected_model.url is None or selected_model.status != 'Available': - return 'Model update not available' - if selected_model.latest_name is None or len(selected_model.latest_name) == 0: - model_name = f'{selected_model.name} {selected_model.latest}.safetensors' - else: - model_name = selected_model.latest_name - return civit_download_model(selected_model.url, model_name, model_path='', model_type='Model') - civit_update_btn.click(fn=civit_update_metadata, inputs=[], outputs=[civit_results4, models_outcome]) civit_results4.select(fn=civit_update_select, inputs=[civit_results4], outputs=[models_outcome, civit_update_download_btn]) civit_update_download_btn.click(fn=civit_update_download, inputs=[], outputs=[models_outcome]) - if native: - from modules.lora.lora_extract import create_ui as lora_extract_ui - lora_extract_ui() + from modules.lora.lora_extract import create_ui as lora_extract_ui + lora_extract_ui() for ui in extra_ui: if callable(ui): From 4a953a7e8d9297398abf0dba70b32d321c1f3768 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 Aug 2025 13:03:18 -0400 Subject: [PATCH 006/141] new models-list_models Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 3 +- modules/ui_models.py | 215 ++++++++++++++++++++++++------------------- 2 files changed, 123 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a3f6a527..855056dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ - quicksettings reset button to restore all quicksettings to default values because things do sometimes get wrong... - updated real-time hints, thanks @CalamitousFelicitousness - - modernui checkbox/radio styling + - new *models -> list models* tab + - more css optimizations and styling - **Offloading** - changed **default** values for offloading based on detected gpu memory see [offloading docs](https://vladmandic.github.io/sdnext-docs/Offload/) for details diff --git a/modules/ui_models.py b/modules/ui_models.py index bb83199c7..42e9c86cc 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -1,5 +1,4 @@ import os -import json import inspect from datetime import datetime import gradio as gr @@ -376,91 +375,107 @@ def create_ui(): outputs=[models_outcome] ) - with gr.Tab(label="Validate"): - model_headers = ['name', 'type', 'filename', 'hash', 'added', 'size', 'metadata'] - model_data = [] + with gr.Tab(label="List"): + from modules import sd_checkpoint + def create_models_table(rows: list[sd_checkpoint.CheckpointInfo]): + from modules import sd_detect + html = """ + + + + + + + + + + + + + + {tbody} + +
NameTypeDetectPipelineHashSizeMTime
+ """ + tbody = '' + for row in rows: + try: + f = row.filename + stat = os.stat(row.filename) + if os.path.isfile(f): + typ = os.path.splitext(f)[1][1:] + size = f'{str(round(stat.st_size / 1024 / 1024 / 1024, 3)) + ' mb'}' + elif os.path.isdir(f): + typ = 'diffusers' + size = 'folder' + else: + typ = 'unknown' + size = 'unknown' + guess = 'Stable Diffusion XL' if 'XL' in f.upper() else 'Stable Diffusion' # set default guess + guess = sd_detect.guess_by_size(f, guess) + guess = sd_detect.guess_by_name(f, guess) + guess, pipeline = sd_detect.guess_by_diffusers(f, guess) + guess = sd_detect.guess_variant(f, guess) + pipeline = sd_detect.shared_items.get_pipelines().get(guess, None) if pipeline is None else pipeline + tbody += f""" + + {row.model_name} + {typ} + {guess} + {pipeline.__name__ if pipeline else '(unknown)'} + {row.shorthash} + {size} + {datetime.fromtimestamp(stat.st_mtime).replace(microsecond=0)} + + """ + except Exception as e: + log.error(f'Model list: row={vars(row)} {e}') + return html.format(tbody=tbody) with gr.Row(): - gr.HTML('

 List all models

') + gr.HTML('

 List models

') with gr.Row(): - model_list_btn = gr.Button(value="List model details", variant='primary') - model_checkhash_btn = gr.Button(value="Calculate hash for all models", variant='primary') + model_list_btn = gr.Button(value="List models", variant='primary') + model_checkhash_btn = gr.Button(value="Calculate missing hashes", variant='secondary') model_checkhash_btn.click(fn=sd_models.update_model_hashes, inputs=[], outputs=[models_outcome]) with gr.Row(): - model_table = gr.DataFrame( - value=None, - headers=model_headers, - label='Model data', - show_label=True, - interactive=False, - wrap=True, - ) + model_table = gr.HTML(value='', elem_id="model_list_table") - def list_models(): - total_size = 0 - model_data.clear() - txt = '' - for m in sd_models.checkpoints_list.values(): - try: - stat = os.stat(m.filename) - m_name = m.name.replace('.ckpt', '').replace('.safetensors', '') - m_type = 'ckpt' if m.name.endswith('.ckpt') else 'safe' - m_meta = len(json.dumps(m.metadata)) - 2 - m_size = round(stat.st_size / 1024 / 1024 / 1024, 3) - m_time = datetime.fromtimestamp(stat.st_mtime) - model_data.append([m_name, m_type, m.filename, m.shorthash, m_time, m_size, m_meta]) - total_size += stat.st_size - except Exception as e: - txt += f"Error: {m.name} {e}
" - txt += f"Model list enumerated {len(sd_models.checkpoints_list.keys())} models in {round(total_size / 1024 / 1024 / 1024, 3)} GB
" - return model_data, txt + model_list_btn.click(fn=lambda: create_models_table(sd_models.checkpoints_list.values()), inputs=[], outputs=[model_table]) - model_list_btn.click(fn=list_models, inputs=[], outputs=[model_table, models_outcome]) - - with gr.Tab(label="Huggingface"): - from modules.models_hf import hf_search, hf_select, hf_download_model, hf_update_token - with gr.Column(scale=6): - with gr.Row(): - gr.HTML('

 Download model from huggingface

') - with gr.Row(): - hf_search_text = gr.Textbox('', label='Search models', placeholder='search huggingface models') - hf_search_btn = ToolButton(value=ui_symbols.search) - with gr.Row(): - with gr.Column(scale=2): - with gr.Row(): - hf_selected = gr.Textbox('', label='Select model', placeholder='select model from search results or enter model name manually') - with gr.Column(scale=1): - with gr.Row(): - hf_variant = gr.Textbox('', label='Specify model variant', placeholder='') - hf_revision = gr.Textbox('', label='Specify model revision', placeholder='') - with gr.Row(): - hf_token = gr.Textbox(opts.huggingface_token, label='Huggingface token', placeholder='optional access token for private or gated models') - hf_mirror = gr.Textbox('', label='Huggingface mirror', placeholder='optional mirror site for downloads') - hf_custom_pipeline = gr.Textbox('', label='Custom pipeline', placeholder='optional pipeline for downloads') - with gr.Column(scale=1): - gr.HTML('
') - hf_download_model_btn = gr.Button(value="Download model", variant='primary') - - with gr.Row(): - hf_headers = ['Name', 'Pipeline', 'Tags', 'Downloads', 'Updated', 'URL'] - hf_types = ['str', 'str', 'str', 'number', 'date', 'markdown'] - hf_results = gr.DataFrame(None, label='Search results', show_label=True, interactive=False, wrap=True, headers=hf_headers, datatype=hf_types, type='array') - - hf_search_text.submit(fn=hf_search, inputs=[hf_search_text], outputs=[hf_results]) - hf_search_btn.click(fn=hf_search, inputs=[hf_search_text], outputs=[hf_results]) - hf_results.select(fn=hf_select, inputs=[hf_results], outputs=[hf_selected]) - hf_download_model_btn.click(fn=hf_download_model, inputs=[hf_selected, hf_token, hf_variant, hf_revision, hf_mirror, hf_custom_pipeline], outputs=[models_outcome]) - hf_token.change(fn=hf_update_token, inputs=[hf_token], outputs=[]) - - with gr.Tab(label="CivitAI"): - from modules.models_civitai import civitai_update_token, civit_search_model, civit_search_metadata, civit_select1, civit_select2, civit_select3, civit_download_model + with gr.Tab(label="Metadata"): + from modules.models_civitai import civit_search_metadata, civit_update_metadata, civit_update_select, civit_update_download with gr.Row(): gr.HTML('

 CivitAI fetch metadata

') - gr.HTML('Fetches preview and metadata information for all models with missing information
Models with existing previews and information are not updated
') + gr.HTML('Fetches preview and metadata information for models with missing information
Models with existing previews and information are not updated
') with gr.Row(): civit_previews_btn = gr.Button(value="Start", variant='primary') with gr.Row(): civit_previews_rehash = gr.Checkbox(value=True, label="Check alternative hash") + civit_previews_btn.click(fn=civit_search_metadata, inputs=[civit_previews_rehash, civit_previews_rehash], outputs=[models_outcome]) + + with gr.Row(): + gr.HTML('

 Scan CivitAI for information on latest available model versions

') + with gr.Row(): + civit_update_btn = gr.Button(value="Update", variant='primary') + with gr.Row(): + gr.HTML('

Update scan results

') + with gr.Row(): + civit_headers4 = ['ID', 'File', 'Name', 'Versions', 'Current', 'Latest', 'Update'] + civit_types4 = ['number', 'str', 'str', 'number', 'str', 'str', 'str'] + civit_widths4 = ['10%', '25%', '25%', '5%', '10%', '10%', '15%'] + civit_results4 = gr.DataFrame(value=None, label=None, show_label=False, interactive=False, wrap=True, row_count=20, headers=civit_headers4, datatype=civit_types4, type='array', column_widths=civit_widths4) + with gr.Row(): + gr.HTML('

Select model from the list and download update if available

') + with gr.Row(): + civit_update_download_btn = gr.Button(value="Download", variant='primary', visible=False) + + civit_update_btn.click(fn=civit_update_metadata, inputs=[], outputs=[civit_results4, models_outcome]) + civit_results4.select(fn=civit_update_select, inputs=[civit_results4], outputs=[models_outcome, civit_update_download_btn]) + civit_update_download_btn.click(fn=civit_update_download, inputs=[], outputs=[models_outcome]) + + with gr.Tab(label="CivitAI"): + from modules.models_civitai import civitai_update_token, civit_search_model, civit_search_metadata, civit_select1, civit_select2, civit_select3, civit_download_model with gr.Row(): gr.HTML('

Search for models

') @@ -516,29 +531,41 @@ def create_ui(): civit_results2.change(fn=is_visible, inputs=[civit_results2], outputs=[civit_results2]) civit_results3.change(fn=is_visible, inputs=[civit_results3], outputs=[civit_results3]) civit_download_model_btn.click(fn=civit_download_model, inputs=[civit_selected, civit_name, civit_path, civit_model_type, civit_token], outputs=[models_outcome]) - civit_previews_btn.click(fn=civit_search_metadata, inputs=[civit_previews_rehash, civit_previews_rehash], outputs=[models_outcome]) - with gr.Tab(label="Update"): - from modules.models_civitai import civit_update_metadata, civit_update_select, civit_update_download - with gr.Row(): - gr.HTML('

 Scan CivitAI for information on latest available model versions

') - with gr.Row(): - civit_update_btn = gr.Button(value="Update", variant='primary') - with gr.Row(): - gr.HTML('

Update scan results

') - with gr.Row(): - civit_headers4 = ['ID', 'File', 'Name', 'Versions', 'Current', 'Latest', 'Update'] - civit_types4 = ['number', 'str', 'str', 'number', 'str', 'str', 'str'] - civit_widths4 = ['10%', '25%', '25%', '5%', '10%', '10%', '15%'] - civit_results4 = gr.DataFrame(value=None, label=None, show_label=False, interactive=False, wrap=True, row_count=20, headers=civit_headers4, datatype=civit_types4, type='array', column_widths=civit_widths4) - with gr.Row(): - gr.HTML('

Select model from the list and download update if available

') - with gr.Row(): - civit_update_download_btn = gr.Button(value="Download", variant='primary', visible=False) + with gr.Tab(label="Huggingface"): + from modules.models_hf import hf_search, hf_select, hf_download_model, hf_update_token + with gr.Column(scale=6): + with gr.Row(): + gr.HTML('

 Download model from huggingface

') + with gr.Row(): + hf_search_text = gr.Textbox('', label='Search models', placeholder='search huggingface models') + hf_search_btn = ToolButton(value=ui_symbols.search) + with gr.Row(): + with gr.Column(scale=2): + with gr.Row(): + hf_selected = gr.Textbox('', label='Select model', placeholder='select model from search results or enter model name manually') + with gr.Column(scale=1): + with gr.Row(): + hf_variant = gr.Textbox('', label='Specify model variant', placeholder='') + hf_revision = gr.Textbox('', label='Specify model revision', placeholder='') + with gr.Row(): + hf_token = gr.Textbox(opts.huggingface_token, label='Huggingface token', placeholder='optional access token for private or gated models') + hf_mirror = gr.Textbox('', label='Huggingface mirror', placeholder='optional mirror site for downloads') + hf_custom_pipeline = gr.Textbox('', label='Custom pipeline', placeholder='optional pipeline for downloads') + with gr.Column(scale=1): + gr.HTML('
') + hf_download_model_btn = gr.Button(value="Download model", variant='primary') - civit_update_btn.click(fn=civit_update_metadata, inputs=[], outputs=[civit_results4, models_outcome]) - civit_results4.select(fn=civit_update_select, inputs=[civit_results4], outputs=[models_outcome, civit_update_download_btn]) - civit_update_download_btn.click(fn=civit_update_download, inputs=[], outputs=[models_outcome]) + with gr.Row(): + hf_headers = ['Name', 'Pipeline', 'Tags', 'Downloads', 'Updated', 'URL'] + hf_types = ['str', 'str', 'str', 'number', 'date', 'markdown'] + hf_results = gr.DataFrame(None, label='Search results', show_label=True, interactive=False, wrap=True, headers=hf_headers, datatype=hf_types, type='array') + + hf_search_text.submit(fn=hf_search, inputs=[hf_search_text], outputs=[hf_results]) + hf_search_btn.click(fn=hf_search, inputs=[hf_search_text], outputs=[hf_results]) + hf_results.select(fn=hf_select, inputs=[hf_results], outputs=[hf_selected]) + hf_download_model_btn.click(fn=hf_download_model, inputs=[hf_selected, hf_token, hf_variant, hf_revision, hf_mirror, hf_custom_pipeline], outputs=[models_outcome]) + hf_token.change(fn=hf_update_token, inputs=[hf_token], outputs=[]) from modules.lora.lora_extract import create_ui as lora_extract_ui lora_extract_ui() From 800521d8853ac087faa0ca483aa76005eeedd8b0 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 Aug 2025 14:18:42 -0400 Subject: [PATCH 007/141] update models current and list tabs Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 3 +- TODO.md | 3 +- javascript/sdnext.css | 24 +++++ modules/modelstats.py | 4 + modules/sd_offload.py | 11 ++ modules/ui_models.py | 246 ++++++++++++++++++++++-------------------- 6 files changed, 172 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 855056dd4..72e71faec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ - quicksettings reset button to restore all quicksettings to default values because things do sometimes get wrong... - updated real-time hints, thanks @CalamitousFelicitousness - - new *models -> list models* tab + - updated *models -> current* tab + - updated *models -> list models* tab - more css optimizations and styling - **Offloading** - changed **default** values for offloading based on detected gpu memory diff --git a/TODO.md b/TODO.md index 0bbf4d8ae..f5641822c 100644 --- a/TODO.md +++ b/TODO.md @@ -33,8 +33,7 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma - Extensions tab: - full CSS redesign - Models tab: - - Validate subtab: replace table with custom html - - Update subtab: replace table with custom html + - Metadata subtab: replace table with custom html - CivitAI subtab: redesign downloader ### Under Consideration diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 4ac257c89..1396cf034 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -1965,6 +1965,30 @@ div:has(>#tab-gallery-folders) { margin-top: 0.2em; } +#model_desc { + overflow: auto; +} + +#model_list_table { + overflow: auto; + max-height: 50vh; +} + +.simple-table tr { + vertical-align: baseline; +} + +.simple-table td { + padding: 0.2em !important; +} + +.model-config { + font-size: 0.8em !important; + opacity: 0.8; + max-height: 6em; + overflow-y: auto; +} + @keyframes move { from { background-position-x: 0, -40px; diff --git a/modules/modelstats.py b/modules/modelstats.py index 8ef02ca35..4d60d34e5 100644 --- a/modules/modelstats.py +++ b/modules/modelstats.py @@ -11,6 +11,7 @@ class Module(): dtype: str = None params: int = 0 modules: int = 0 + quant: str = None config: dict = None def __init__(self, name, module): @@ -25,6 +26,7 @@ class Module(): self.dtype = getattr(module, 'dtype', None) self.params = sum(p.numel() for p in module.parameters(recurse=True)) self.modules = len(list(module.modules())) + self.quant = getattr(module, 'quantization_method', None) def __repr__(self): s = f'name="{self.name}" cls={self.cls} config={self.config is not None}' @@ -69,6 +71,8 @@ class Model(): def analyze(): + if not shared.sd_loaded: + return None model = Model(shared.opts.sd_model_checkpoint) if model.cls == '': return model diff --git a/modules/sd_offload.py b/modules/sd_offload.py index 689c49e8c..c6156a3e8 100644 --- a/modules/sd_offload.py +++ b/modules/sd_offload.py @@ -351,6 +351,16 @@ def apply_balanced_offload_to_module(module, op="apply"): devices.torch_gc(fast=True, force=True, reason='offload') +def report_model_stats(module_name, module): + try: + size = offload_hook_instance.offload_map.get(module_name, 0) + quant = getattr(module, "quantization_method", None) + params = sum(p.numel() for p in module.parameters(recurse=True)) + shared.log.debug(f'Module: name={module_name} cls={module.__class__.__name__} size={size:.3f} params={params} quant={quant}') + except Exception as e: + shared.log.error(f'Module stats: name={module_name} {e}') + + def apply_balanced_offload(sd_model=None, exclude=[]): global offload_hook_instance # pylint: disable=global-statement if shared.opts.diffusers_offload_mode != "balanced": @@ -382,6 +392,7 @@ def apply_balanced_offload(sd_model=None, exclude=[]): module.module_name = module_name module.offload_dir = os.path.join(shared.opts.accelerate_offload_path, checkpoint_name, module_name) apply_balanced_offload_to_module(module, op='apply') + report_model_stats(module_name, module) set_accelerate(sd_model) t = time.time() - t0 process_timer.add('offload', t) diff --git a/modules/ui_models.py b/modules/ui_models.py index 42e9c86cc..d8c3ce4f5 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -16,7 +16,6 @@ def create_ui(): dummy_component = gr.Label(visible=False) with gr.Row(elem_id="models_tab"): with gr.Column(elem_id='models_output_container', scale=1): - # models_output = gr.Textbox(elem_id="models_output", value="", show_label=False) gr.HTML(elem_id="models_progress", value="") models_image = gr.Image(elem_id="models_image", show_label=False, interactive=False, type='pil') models_outcome = gr.HTML(elem_id="models_error", value="") @@ -25,20 +24,48 @@ def create_ui(): with gr.Column(elem_id='models_input_container', scale=3): with gr.Tab(label="Current"): + def create_modules_table(rows: list): + html = """ + + + + + + {tbody} + +
ModuleClassDeviceDtypeQuantParamsModulesConfig
+ """ + tbody = '' + for row in rows: + try: + config = str(row.config) + except Exception: + config = '{}' + try: + tbody += f""" + + {row.name} + {row.cls} + {row.device} + {row.dtype} + {row.quant} + {row.params} + {row.modules} +
{config}
+ + """ + except Exception as e: + log.error(f'Model list: row={vars(row)} {e}') + return html.format(tbody=tbody) + def analyze(): from modules import modelstats model = modelstats.analyze() - desc = f"Model: {model.name}
Type: {model.type}
Class: {model.cls}
Size: {model.size} bytes
Modified: {model.mtime}
" + if model is None: + return ["Model not loaded", {}] meta = model.meta - components = [] - for m in model.modules: - try: - component = (m.name, m.cls, str(m.device), str(m.dtype), m.params, m.modules, str(m.config)) - components.append(component) - except Exception: - component = (m.name, m.cls, str(m.device), str(m.dtype), m.params, m.modules, '') - components.append(component) - return [desc, components, meta] + html = create_modules_table(model.modules) + return [html, meta] with gr.Row(): gr.HTML('

 Analyze currently loaded model

') @@ -46,14 +73,100 @@ def create_ui(): model_analyze = gr.Button(value="Analyze", variant='primary') with gr.Row(): model_desc = gr.HTML(value="", elem_id="model_desc") - with gr.Row(): - module_headers = ['Module', 'Class', 'Device', 'DType', 'Params', 'Modules', 'Config'] - module_types = ['str', 'str', 'str', 'str', 'number', 'number', 'str'] - model_modules = gr.DataFrame(value=None, label=None, show_label=False, interactive=False, wrap=True, headers=module_headers, datatype=module_types, type='array') with gr.Row(): model_meta = gr.JSON(label="Metadata", value={}, elem_id="model_meta") - model_analyze.click(fn=analyze, inputs=[], outputs=[model_desc, model_modules, model_meta]) + model_analyze.click(fn=analyze, inputs=[], outputs=[model_desc, model_meta]) + + with gr.Tab(label="List"): + def create_models_table(rows: list): + from modules import sd_detect + html = """ + + + + + + {tbody} + +
NameTypeDetectPipelineHashSizeMTime
+ """ + tbody = '' + for row in rows: + try: + f = row.filename + stat = os.stat(row.filename) + if os.path.isfile(f): + typ = os.path.splitext(f)[1][1:] + size = f'{str(round(stat.st_size / 1024 / 1024 / 1024, 3)) + ' mb'}' + elif os.path.isdir(f): + typ = 'diffusers' + size = 'folder' + else: + typ = 'unknown' + size = 'unknown' + guess = 'Stable Diffusion XL' if 'XL' in f.upper() else 'Stable Diffusion' # set default guess + guess = sd_detect.guess_by_size(f, guess) + guess = sd_detect.guess_by_name(f, guess) + guess, pipeline = sd_detect.guess_by_diffusers(f, guess) + guess = sd_detect.guess_variant(f, guess) + pipeline = sd_detect.shared_items.get_pipelines().get(guess, None) if pipeline is None else pipeline + tbody += f""" + + {row.model_name} + {typ} + {guess} + {pipeline.__name__ if pipeline else '(unknown)'} + {row.shorthash} + {size} + {datetime.fromtimestamp(stat.st_mtime).replace(microsecond=0)} + + """ + except Exception as e: + log.error(f'Model list: row={vars(row)} {e}') + return html.format(tbody=tbody) + + with gr.Row(): + gr.HTML('

 List models

') + with gr.Row(): + model_list_btn = gr.Button(value="List models", variant='primary') + model_checkhash_btn = gr.Button(value="Calculate missing hashes", variant='secondary') + model_checkhash_btn.click(fn=sd_models.update_model_hashes, inputs=[], outputs=[models_outcome]) + with gr.Row(): + model_table = gr.HTML(value='', elem_id="model_list_table") + + model_list_btn.click(fn=lambda: create_models_table(sd_models.checkpoints_list.values()), inputs=[], outputs=[model_table]) + + with gr.Tab(label="Metadata"): + from modules.models_civitai import civit_search_metadata, civit_update_metadata, civit_update_select, civit_update_download + with gr.Row(): + gr.HTML('

 CivitAI fetch metadata

') + gr.HTML('Fetches preview and metadata information for models with missing information
Models with existing previews and information are not updated
') + with gr.Row(): + civit_previews_btn = gr.Button(value="Start", variant='primary') + with gr.Row(): + civit_previews_rehash = gr.Checkbox(value=True, label="Check alternative hash") + civit_previews_btn.click(fn=civit_search_metadata, inputs=[civit_previews_rehash, civit_previews_rehash], outputs=[models_outcome]) + + with gr.Row(): + gr.HTML('

 Scan CivitAI for information on latest available model versions

') + with gr.Row(): + civit_update_btn = gr.Button(value="Update", variant='primary') + with gr.Row(): + gr.HTML('

Update scan results

') + with gr.Row(): + civit_headers4 = ['ID', 'File', 'Name', 'Versions', 'Current', 'Latest', 'Update'] + civit_types4 = ['number', 'str', 'str', 'number', 'str', 'str', 'str'] + civit_widths4 = ['10%', '25%', '25%', '5%', '10%', '10%', '15%'] + civit_results4 = gr.DataFrame(value=None, label=None, show_label=False, interactive=False, wrap=True, row_count=20, headers=civit_headers4, datatype=civit_types4, type='array', column_widths=civit_widths4) + with gr.Row(): + gr.HTML('

Select model from the list and download update if available

') + with gr.Row(): + civit_update_download_btn = gr.Button(value="Download", variant='primary', visible=False) + + civit_update_btn.click(fn=civit_update_metadata, inputs=[], outputs=[civit_results4, models_outcome]) + civit_results4.select(fn=civit_update_select, inputs=[civit_results4], outputs=[models_outcome, civit_update_download_btn]) + civit_update_download_btn.click(fn=civit_update_download, inputs=[], outputs=[models_outcome]) with gr.Tab(label="Loader"): from modules import ui_models_load @@ -303,7 +416,7 @@ def create_ui(): ] ) - with gr.Tab(label="Modules"): + with gr.Tab(label="Replace"): with gr.Row(): gr.HTML('

 Replace model components

') with gr.Row(): @@ -375,105 +488,6 @@ def create_ui(): outputs=[models_outcome] ) - with gr.Tab(label="List"): - from modules import sd_checkpoint - def create_models_table(rows: list[sd_checkpoint.CheckpointInfo]): - from modules import sd_detect - html = """ - - - - - - - - - - - - - - {tbody} - -
NameTypeDetectPipelineHashSizeMTime
- """ - tbody = '' - for row in rows: - try: - f = row.filename - stat = os.stat(row.filename) - if os.path.isfile(f): - typ = os.path.splitext(f)[1][1:] - size = f'{str(round(stat.st_size / 1024 / 1024 / 1024, 3)) + ' mb'}' - elif os.path.isdir(f): - typ = 'diffusers' - size = 'folder' - else: - typ = 'unknown' - size = 'unknown' - guess = 'Stable Diffusion XL' if 'XL' in f.upper() else 'Stable Diffusion' # set default guess - guess = sd_detect.guess_by_size(f, guess) - guess = sd_detect.guess_by_name(f, guess) - guess, pipeline = sd_detect.guess_by_diffusers(f, guess) - guess = sd_detect.guess_variant(f, guess) - pipeline = sd_detect.shared_items.get_pipelines().get(guess, None) if pipeline is None else pipeline - tbody += f""" - - {row.model_name} - {typ} - {guess} - {pipeline.__name__ if pipeline else '(unknown)'} - {row.shorthash} - {size} - {datetime.fromtimestamp(stat.st_mtime).replace(microsecond=0)} - - """ - except Exception as e: - log.error(f'Model list: row={vars(row)} {e}') - return html.format(tbody=tbody) - - with gr.Row(): - gr.HTML('

 List models

') - with gr.Row(): - model_list_btn = gr.Button(value="List models", variant='primary') - model_checkhash_btn = gr.Button(value="Calculate missing hashes", variant='secondary') - model_checkhash_btn.click(fn=sd_models.update_model_hashes, inputs=[], outputs=[models_outcome]) - with gr.Row(): - model_table = gr.HTML(value='', elem_id="model_list_table") - - model_list_btn.click(fn=lambda: create_models_table(sd_models.checkpoints_list.values()), inputs=[], outputs=[model_table]) - - with gr.Tab(label="Metadata"): - from modules.models_civitai import civit_search_metadata, civit_update_metadata, civit_update_select, civit_update_download - with gr.Row(): - gr.HTML('

 CivitAI fetch metadata

') - gr.HTML('Fetches preview and metadata information for models with missing information
Models with existing previews and information are not updated
') - with gr.Row(): - civit_previews_btn = gr.Button(value="Start", variant='primary') - with gr.Row(): - civit_previews_rehash = gr.Checkbox(value=True, label="Check alternative hash") - civit_previews_btn.click(fn=civit_search_metadata, inputs=[civit_previews_rehash, civit_previews_rehash], outputs=[models_outcome]) - - with gr.Row(): - gr.HTML('

 Scan CivitAI for information on latest available model versions

') - with gr.Row(): - civit_update_btn = gr.Button(value="Update", variant='primary') - with gr.Row(): - gr.HTML('

Update scan results

') - with gr.Row(): - civit_headers4 = ['ID', 'File', 'Name', 'Versions', 'Current', 'Latest', 'Update'] - civit_types4 = ['number', 'str', 'str', 'number', 'str', 'str', 'str'] - civit_widths4 = ['10%', '25%', '25%', '5%', '10%', '10%', '15%'] - civit_results4 = gr.DataFrame(value=None, label=None, show_label=False, interactive=False, wrap=True, row_count=20, headers=civit_headers4, datatype=civit_types4, type='array', column_widths=civit_widths4) - with gr.Row(): - gr.HTML('

Select model from the list and download update if available

') - with gr.Row(): - civit_update_download_btn = gr.Button(value="Download", variant='primary', visible=False) - - civit_update_btn.click(fn=civit_update_metadata, inputs=[], outputs=[civit_results4, models_outcome]) - civit_results4.select(fn=civit_update_select, inputs=[civit_results4], outputs=[models_outcome, civit_update_download_btn]) - civit_update_download_btn.click(fn=civit_update_download, inputs=[], outputs=[models_outcome]) - with gr.Tab(label="CivitAI"): from modules.models_civitai import civitai_update_token, civit_search_model, civit_search_metadata, civit_select1, civit_select2, civit_select3, civit_download_model From e90ac68dccd0cafc97319b88f8bc3fe09a344795 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 Aug 2025 15:28:45 -0400 Subject: [PATCH 008/141] lint Signed-off-by: Vladimir Mandic --- cli/civitai-search.py | 2 +- extensions-builtin/sdnext-modernui | 2 +- modules/prompt_parser.py | 4 +++- modules/ui_models.py | 2 +- wiki | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cli/civitai-search.py b/cli/civitai-search.py index e7600fd06..53c524338 100644 --- a/cli/civitai-search.py +++ b/cli/civitai-search.py @@ -147,7 +147,7 @@ def search_civitai( model_names = [model.name.lower()] version_names = [v.name.lower() for v in model.versions] file_names = [f.name.lower() for v in model.versions for f in v.files] - if any([query.lower() in name for name in model_names + version_names + file_names]): + if any([query.lower() in name for name in model_names + version_names + file_names]): # noqa: C419 exact_models.append(model) t1 = time.time() diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 9052df510..95b8f72f0 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 9052df510e9704ef952f1c4ebfa1fba28dc984a3 +Subproject commit 95b8f72f0683a08c98f0c24ae418138b964751b5 diff --git a/modules/prompt_parser.py b/modules/prompt_parser.py index cb8789aea..57587e3b5 100644 --- a/modules/prompt_parser.py +++ b/modules/prompt_parser.py @@ -14,7 +14,8 @@ from typing import List import lark import torch from compel import Compel -from modules.shared import opts, log, native +from modules.shared import opts, log + # a prompt like this: "fantasy landscape with a [mountain:lake:0.25] and [an oak:a christmas tree:0.75][ in foreground::0.6][ in background:0.25] [shoddy:masterful:0.5]" # will be represented with prompt_schedule like this (assuming steps=100): @@ -24,6 +25,7 @@ from modules.shared import opts, log, native # [75, 'fantasy landscape with a lake and an oak in background masterful'] # [100, 'fantasy landscape with a lake and a christmas tree in background masterful'] + round_bracket_multiplier = 1.1 square_bracket_multiplier = 1.0 / 1.1 re_AND = re.compile(r"\bAND\b") diff --git a/modules/ui_models.py b/modules/ui_models.py index d8c3ce4f5..da5495c2f 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -98,7 +98,7 @@ def create_ui(): stat = os.stat(row.filename) if os.path.isfile(f): typ = os.path.splitext(f)[1][1:] - size = f'{str(round(stat.st_size / 1024 / 1024 / 1024, 3)) + ' mb'}' + size = f"{round(stat.st_size / 1024 / 1024 / 1024, 3)} gb" elif os.path.isdir(f): typ = 'diffusers' size = 'folder' diff --git a/wiki b/wiki index 79b18f2c5..a6e3a70d1 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 79b18f2c5e3438f3f564fd264fdb27bed76b0f72 +Subproject commit a6e3a70d176e8b2af89eedb2ab9c25069146e30f From 265cb5e8bad2721d56f2b07cd7dec52fca086e1c Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 Aug 2025 18:46:40 -0400 Subject: [PATCH 009/141] redo ui model metadata Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + javascript/sdnext.css | 4 + modules/modelloader.py | 60 ++++++------ modules/models_civitai.py | 173 ++++++++++++++++++++++++----------- modules/shared.py | 2 +- modules/ui_extra_networks.py | 2 +- modules/ui_models.py | 36 ++------ 7 files changed, 160 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e71faec..0b7dab10f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - updated real-time hints, thanks @CalamitousFelicitousness - updated *models -> current* tab - updated *models -> list models* tab + - updated *models -> metadata* tab - more css optimizations and styling - **Offloading** - changed **default** values for offloading based on detected gpu memory diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 1396cf034..ee55b8785 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -1974,6 +1974,10 @@ div:has(>#tab-gallery-folders) { max-height: 50vh; } +#civit_metadata { + overflow: auto; +} + .simple-table tr { vertical-align: baseline; } diff --git a/modules/modelloader.py b/modules/modelloader.py index 5ad1d540b..9813b5ee5 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -48,24 +48,6 @@ def hf_login(token=None): return True -def download_civit_meta(model_path: str, model_id): - fn = os.path.splitext(model_path)[0] + '.json' - url = f'https://civitai.com/api/v1/models/{model_id}' - r = shared.req(url) - if r.status_code == 200: - try: - shared.writefile(r.json(), filename=fn, mode='w', silent=True) - msg = f'CivitAI download: id={model_id} url={url} file="{fn}"' - shared.log.info(msg) - return msg - except Exception as e: - msg = f'CivitAI download error: id={model_id} url={url} file="{fn}" {e}' - errors.display(e, 'CivitAI download error') - shared.log.error(msg) - return msg - return f'CivitAI download error: id={model_id} url={url} code={r.status_code}' - - def save_video_frame(filepath: str): from modules import video try: @@ -83,21 +65,38 @@ def save_video_frame(filepath: str): return frame +def download_civit_meta(model_path: str, model_id): + fn = os.path.splitext(model_path)[0] + '.json' + url = f'https://civitai.com/api/v1/models/{model_id}' + r = shared.req(url) + if r.status_code == 200: + try: + data = r.json() + shared.writefile(data, filename=fn, mode='w', silent=True) + shared.log.info(f'CivitAI download: id={model_id} url={url} file="{fn}"') + return r.status_code, len(data), '' # code/size/note + except Exception as e: + errors.display(e, 'civitai meta') + shared.log.error(f'CivitAI meta: id={model_id} url={url} file="{fn}" {e}') + return r.status_code, '', str(e) + return r.status_code, '', '' + + def download_civit_preview(model_path: str, preview_url: str): global pbar # pylint: disable=global-statement if model_path is None: pbar = None - return '' + return 500, '', '' ext = os.path.splitext(preview_url)[1] preview_file = os.path.splitext(model_path)[0] + ext is_video = preview_file.lower().endswith('.mp4') is_json = preview_file.lower().endswith('.json') if is_json: shared.log.warning(f'CivitAI download: url="{preview_url}" skip json') - return 'CivitAI download error: JSON file' + return 500, '', 'exepected preview image got json' if os.path.exists(preview_file): - return '' - res = f'CivitAI download: url={preview_url} file="{preview_file}"' + return 304, '', 'already exists' + # res = f'CivitAI download: url={preview_url} file="{preview_file}"' r = shared.req(preview_url, stream=True) total_size = int(r.headers.get('content-length', 0)) block_size = 16384 # 16KB blocks @@ -116,21 +115,20 @@ def download_civit_preview(model_path: str, preview_url: str): pbar.update(task, advance=block_size) if written < 1024: # min threshold os.remove(preview_file) - raise ValueError(f'removed invalid download: bytes={written}') + return 400, '', 'removed invalid download' if is_video: img = save_video_frame(preview_file) else: img = Image.open(preview_file) except Exception as e: - # os.remove(preview_file) - res += f' error={e}' shared.log.error(f'CivitAI download error: url={preview_url} file="{preview_file}" written={written} {e}') + return 500, '', str(e) shared.state.end() if img is None: - return res - shared.log.info(f'{res} size={total_size} image={img.size}') + return 500, '', 'image is none' + shared.log.info(f'CivitAI download: url={preview_url} file="{preview_file}" size={total_size} image={img.size}') img.close() - return res + return 200, str(total_size), '' # code/size/note download_pbar = None @@ -201,12 +199,6 @@ def download_civit_model_thread(model_name: str, model_url: str, model_path: str if written < 1024: # min threshold os.remove(temp_file) raise ValueError(f'removed invalid download: bytes={written}') - """ - if preview is not None: - preview_file = os.path.splitext(model_file)[0] + '.jpg' - preview.save(preview_file) - res += f' preview={preview_file}' - """ except Exception as e: shared.log.error(f'{res} {e}') finally: diff --git a/modules/models_civitai.py b/modules/models_civitai.py index 9b2f5420e..15fc58011 100644 --- a/modules/models_civitai.py +++ b/modules/models_civitai.py @@ -8,12 +8,12 @@ from modules.shared import log, opts, req, readfile, max_workers data = [] selected_model = None -update_data = [] class CivitModel: def __init__(self, name, fn, sha = None, meta = {}): self.name = name + self.file = name self.id = meta.get('id', 0) self.fn = fn self.sha = sha @@ -25,28 +25,61 @@ class CivitModel: self.latest_name = '' self.url = None self.status = 'Not found' - def array(self): - return [self.id, self.fn, self.name, self.versions, self.vername, self.latest, self.status] def civit_update_metadata(): + def create_update_metadata_table(rows: list[CivitModel]): + html = """ + + + + + + + + + + + + + + {tbody} + +
IDFileNameHashVersionsLatestStatus
+ """ + tbody = '' + for row in rows: + try: + tbody += f""" + + {row.id} + {row.file} + {row.name} + {row.sha} + {row.versions} + {row.latest} + {row.status} + + """ + except Exception as e: + log.error(f'Model list: row={row} {e}') + return html.format(tbody=tbody) + log.debug('CivitAI update metadata: models') from modules import ui_extra_networks, modelloader - res = [] pages = ui_extra_networks.get_pages('Model') if len(pages) == 0: return 'CivitAI update metadata: no models found' page: ui_extra_networks.ExtraNetworksPage = pages[0] - table_data = [] - update_data.clear() + results = [] all_hashes = [(item.get('hash', None) or 'XXXXXXXX').upper()[:8] for item in page.list_items()] for item in page.list_items(): model = CivitModel(name=item['name'], fn=item['filename'], sha=item.get('hash', None), meta=item.get('metadata', {})) if model.sha is None or len(model.sha) == 0: - res.append(f'CivitAI skip search: name="{model.name}" hash=None') + log.debug(f'CivitAI skip search: name="{model.name}" hash=None') else: r = req(f'https://civitai.com/api/v1/model-versions/by-hash/{model.sha}') - res.append(f'CivitAI search: name="{model.name}" hash={model.sha} status={r.status_code}') + log.debug(f'CivitAI search: name="{model.name}" hash={model.sha} status={r.status_code}') if r.status_code == 200: d = r.json() model.id = d['modelId'] @@ -76,31 +109,9 @@ def civit_update_metadata(): else: model.status = 'Available' break - log.debug(res[-1]) - update_data.append(model) - table_data.append(model.array()) - yield gr.update(value=table_data), '
'.join([r for r in res if len(r) > 0]) - return '
'.join([r for r in res if len(r) > 0]) - -def civit_update_select(evt: gr.SelectData, in_data): - global selected_model # pylint: disable=global-statement - try: - selected_model = next([m for m in update_data if m.fn == in_data[evt.index[0]][1]]) - except Exception: - selected_model = None - if selected_model is None or selected_model.url is None or selected_model.status != 'Available': - return [gr.update(value='Model update not available'), gr.update(visible=False)] - else: - return [gr.update(), gr.update(visible=True)] - -def civit_update_download(): - if selected_model is None or selected_model.url is None or selected_model.status != 'Available': - return 'Model update not available' - if selected_model.latest_name is None or len(selected_model.latest_name) == 0: - model_name = f'{selected_model.name} {selected_model.latest}.safetensors' - else: - model_name = selected_model.latest_name - return civit_download_model(selected_model.url, model_name, model_path='', model_type='Model') + results.append(model) + yield create_update_metadata_table(results) + return create_update_metadata_table(results) def civit_search_model(name, tag, model_type): @@ -211,56 +222,106 @@ def civit_download_model(model_url: str, model_name: str, model_path: str, model return res -def atomic_civit_search_metadata(item, res, rehash): +def atomic_civit_search_metadata(item, results): from modules.modelloader import download_civit_preview, download_civit_meta if item is None: - return + return results meta = os.path.splitext(item['filename'])[0] + '.json' has_meta = os.path.isfile(meta) and os.stat(meta).st_size > 0 if ('card-no-preview.png' in item['preview'] or not has_meta) and os.path.isfile(item['filename']): sha = item.get('hash', None) found = False + result = { + 'id': '', + 'name': item['name'], + 'type': '', + 'hash': '', + 'code': '', + 'size': '', + 'note': '', + } if sha is not None and len(sha) > 0: r = req(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}') log.debug(f'CivitAI search: name="{item["name"]}" hash={sha} status={r.status_code}') + result['hash'] = sha + result['code'] = r.status_code if r.status_code == 200: d = r.json() - res.append(download_civit_meta(item['filename'], d['modelId'])) + result['code'], result['size'], result['note'] = download_civit_meta(item['filename'], d['modelId']) + result['id'] = d['modelId'] + result['type'] = 'metadata' + results.append(result) if d.get('images') is not None: for i in d['images']: - preview_url = i['url'] - img_res = download_civit_preview(item['filename'], preview_url) - res.append(img_res) - if 'error' not in img_res: + result['code'], result['size'], result['note'] = download_civit_preview(item['filename'], i['url']) + if result['code'] == 200: + result['type'] = 'preview' + results.append(result) found = True break - if not found and rehash and os.stat(item['filename']).st_size < (1024 * 1024 * 1024): + if not found and os.stat(item['filename']).st_size < (1024 * 1024 * 1024): from modules import hashes sha = hashes.calculate_sha256(item['filename'], quiet=True)[:10] r = req(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}') log.debug(f'CivitAI search: name="{item["name"]}" hash={sha} status={r.status_code}') + result['hash'] = sha + result['code'] = r.status_code if r.status_code == 200: d = r.json() - res.append(download_civit_meta(item['filename'], d['modelId'])) + result['code'], result['size'], result['note'] = download_civit_meta(item['filename'], d['modelId']) + result['id'] = d['modelId'] + result['type'] = 'metadata' + results.append(result) if d.get('images') is not None: for i in d['images']: - preview_url = i['url'] - img_res = download_civit_preview(item['filename'], preview_url) - res.append(img_res) - if 'error' not in img_res: + result['code'], result['size'], result['note'] = download_civit_preview(item['filename'], i['url']) + if result['code'] == 200: + result['type'] = 'preview' + results.append(result) found = True break + if not found: + results.append(result) + + +def civit_search_metadata(title: str = None): + def create_search_metadata_table(rows): + html = """ + + + + + + {tbody} + +
IDNameTypeCodeHashSizeNote
+ """ + tbody = '' + for row in rows: + try: + tbody += f""" + + {row['id']} + {row['name']} + {row['type']} + {row['code']} + {row['hash']} + {row['size']} + {row['note']} + + """ + except Exception as e: + log.error(f'Model list: row={row} {e}') + return html.format(tbody=tbody) -def civit_search_metadata(rehash, title): - log.debug(f'CivitAI search metadata: type={title if type(title) == str else "all"}') from modules.ui_extra_networks import get_pages - res = [] + results = [] scanned, skipped = 0, 0 t0 = time.time() candidates = [] re_skip = [r.strip() for r in opts.extra_networks_scan_skip.split(',') if len(r.strip()) > 0] - log.debug(f'CivitAI search metadata: skip={re_skip}') + log.debug(f'CivitAI search metadata: type={title if type(title) == str else "all"} skip={re_skip}') for page in get_pages(): if type(title) == str: if page.title != title: @@ -275,16 +336,18 @@ def civit_search_metadata(rehash, title): continue scanned += 1 candidates.append(item) - # atomic_civit_search_metadata(item, res, rehash) import concurrent with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + future_items = {} for fn in candidates: - executor.submit(atomic_civit_search_metadata, fn, res, rehash) - atomic_civit_search_metadata(None, res, rehash) + future_items[executor.submit(atomic_civit_search_metadata, fn, results)] = fn + for future in concurrent.futures.as_completed(future_items): + future.result() + yield create_search_metadata_table(results) + t1 = time.time() log.debug(f'CivitAI search metadata: scanned={scanned} skipped={skipped} time={t1-t0:.2f}') - txt = '
'.join([r for r in res if len(r) > 0]) - return txt + return create_search_metadata_table(results) def civitai_update_token(token): diff --git a/modules/shared.py b/modules/shared.py index f9edba6a4..6c971f4eb 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -57,7 +57,7 @@ restricted_opts = { "outdir_init_images" } resize_modes = ["None", "Fixed", "Crop", "Fill", "Outpaint", "Context aware"] -max_workers = 8 +max_workers = 12 default_hfcache_dir = os.environ.get("SD_HFCACHEDIR", None) or os.path.join(os.path.expanduser('~'), '.cache', 'huggingface', 'hub') sdnq_quant_modes = ["int8", "float8_e4m3fn", "int7", "int6", "int5", "uint4", "uint3", "uint2", "float8_e5m2", "float8_e4m3fnuz", "float8_e5m2fnuz", "uint8", "uint7", "uint6", "uint5", "int4", "int3", "int2", "uint1"] state = shared_state.State() diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index b8c777f92..8640e285d 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -933,7 +933,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): def ui_scan_click(title): from modules.models_civitai import civit_search_metadata - civit_search_metadata(True, title) + civit_search_metadata(title) return ui_refresh_click(title) def ui_save_click(): diff --git a/modules/ui_models.py b/modules/ui_models.py index da5495c2f..f5290ba08 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -68,7 +68,7 @@ def create_ui(): return [html, meta] with gr.Row(): - gr.HTML('

 Analyze currently loaded model

') + gr.HTML('

Analyze currently loaded model

') with gr.Row(): model_analyze = gr.Button(value="Analyze", variant='primary') with gr.Row(): @@ -127,7 +127,7 @@ def create_ui(): return html.format(tbody=tbody) with gr.Row(): - gr.HTML('

 List models

') + gr.HTML('

List all locally available models


') with gr.Row(): model_list_btn = gr.Button(value="List models", variant='primary') model_checkhash_btn = gr.Button(value="Calculate missing hashes", variant='secondary') @@ -138,35 +138,17 @@ def create_ui(): model_list_btn.click(fn=lambda: create_models_table(sd_models.checkpoints_list.values()), inputs=[], outputs=[model_table]) with gr.Tab(label="Metadata"): - from modules.models_civitai import civit_search_metadata, civit_update_metadata, civit_update_select, civit_update_download + from modules.models_civitai import civit_search_metadata, civit_update_metadata with gr.Row(): - gr.HTML('

 CivitAI fetch metadata

') - gr.HTML('Fetches preview and metadata information for models with missing information
Models with existing previews and information are not updated
') + gr.HTML('

Fetch model preview metadata


') with gr.Row(): - civit_previews_btn = gr.Button(value="Start", variant='primary') + civit_previews_btn = gr.Button(value="Scan missing", variant='primary') + civit_update_btn = gr.Button(value="Update all", variant='primary') with gr.Row(): - civit_previews_rehash = gr.Checkbox(value=True, label="Check alternative hash") - civit_previews_btn.click(fn=civit_search_metadata, inputs=[civit_previews_rehash, civit_previews_rehash], outputs=[models_outcome]) + civit_metadata = gr.HTML(value='', elem_id="civit_metadata") + civit_previews_btn.click(fn=civit_search_metadata, inputs=[], outputs=[civit_metadata]) + civit_update_btn.click(fn=civit_update_metadata, inputs=[], outputs=[civit_metadata]) - with gr.Row(): - gr.HTML('

 Scan CivitAI for information on latest available model versions

') - with gr.Row(): - civit_update_btn = gr.Button(value="Update", variant='primary') - with gr.Row(): - gr.HTML('

Update scan results

') - with gr.Row(): - civit_headers4 = ['ID', 'File', 'Name', 'Versions', 'Current', 'Latest', 'Update'] - civit_types4 = ['number', 'str', 'str', 'number', 'str', 'str', 'str'] - civit_widths4 = ['10%', '25%', '25%', '5%', '10%', '10%', '15%'] - civit_results4 = gr.DataFrame(value=None, label=None, show_label=False, interactive=False, wrap=True, row_count=20, headers=civit_headers4, datatype=civit_types4, type='array', column_widths=civit_widths4) - with gr.Row(): - gr.HTML('

Select model from the list and download update if available

') - with gr.Row(): - civit_update_download_btn = gr.Button(value="Download", variant='primary', visible=False) - - civit_update_btn.click(fn=civit_update_metadata, inputs=[], outputs=[civit_results4, models_outcome]) - civit_results4.select(fn=civit_update_select, inputs=[civit_results4], outputs=[models_outcome, civit_update_download_btn]) - civit_update_download_btn.click(fn=civit_update_download, inputs=[], outputs=[models_outcome]) with gr.Tab(label="Loader"): from modules import ui_models_load From ea9d8b23234c5b1bb290abdddee6574908642d63 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 Aug 2025 19:27:07 -0400 Subject: [PATCH 010/141] lint Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 2 +- TODO.md | 1 - extensions-builtin/sdnext-modernui | 2 +- modules/models_civitai.py | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b7dab10f..d64f78db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log for SD.Next -## Update for 2025-08-01 +## Update for 2025-08-02 - **Models** - [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) diff --git a/TODO.md b/TODO.md index f5641822c..31ca627d4 100644 --- a/TODO.md +++ b/TODO.md @@ -33,7 +33,6 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma - Extensions tab: - full CSS redesign - Models tab: - - Metadata subtab: replace table with custom html - CivitAI subtab: redesign downloader ### Under Consideration diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 95b8f72f0..9903b0395 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 95b8f72f0683a08c98f0c24ae418138b964751b5 +Subproject commit 9903b0395f5119dcbca6379a4e34887c9154d782 diff --git a/modules/models_civitai.py b/modules/models_civitai.py index 15fc58011..6f86eba77 100644 --- a/modules/models_civitai.py +++ b/modules/models_civitai.py @@ -225,7 +225,7 @@ def civit_download_model(model_url: str, model_name: str, model_path: str, model def atomic_civit_search_metadata(item, results): from modules.modelloader import download_civit_preview, download_civit_meta if item is None: - return results + return meta = os.path.splitext(item['filename'])[0] + '.json' has_meta = os.path.isfile(meta) and os.stat(meta).st_size > 0 if ('card-no-preview.png' in item['preview'] or not has_meta) and os.path.isfile(item['filename']): From 84bccc07caf501f9366f9073df6940123f313dd9 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 3 Aug 2025 08:43:36 -0400 Subject: [PATCH 011/141] update simple-table Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- modules/models_civitai.py | 32 ++++++++++++--------------- modules/sd_checkpoint.py | 35 +++++++++++++++++++++++++----- modules/ui_models.py | 6 ++--- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 9903b0395..03e365e0b 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 9903b0395f5119dcbca6379a4e34887c9154d782 +Subproject commit 03e365e0b0ec5e1cc442c9dbe683b61321fc5630 diff --git a/modules/models_civitai.py b/modules/models_civitai.py index 6f86eba77..8bc2d47b9 100644 --- a/modules/models_civitai.py +++ b/modules/models_civitai.py @@ -31,16 +31,8 @@ def civit_update_metadata(): def create_update_metadata_table(rows: list[CivitModel]): html = """ - - - - - - - - - - + + {tbody} @@ -52,8 +44,8 @@ def civit_update_metadata(): try: tbody += f""" - + @@ -103,11 +95,11 @@ def civit_update_metadata(): model.url = f.get('downloadUrl', None) model.latest_name = f.get('name', '') if model.vername == model.latest: - model.status = 'Latest' + model.status = 'Latest version' elif any(map(lambda v: v in model.latest_hashes, all_hashes)): # pylint: disable=cell-var-from-loop # noqa: C417 - model.status = 'Downloaded' + model.status = 'Update downloaded' else: - model.status = 'Available' + model.status = 'Update available' break results.append(model) yield create_update_metadata_table(results) @@ -226,7 +218,11 @@ def atomic_civit_search_metadata(item, results): from modules.modelloader import download_civit_preview, download_civit_meta if item is None: return - meta = os.path.splitext(item['filename'])[0] + '.json' + try: + meta = os.path.splitext(item['filename'])[0] + '.json' + except Exception: + # log.error(f'CivitAI search metadata: item={item} {e}') + return has_meta = os.path.isfile(meta) and os.stat(meta).st_size > 0 if ('card-no-preview.png' in item['preview'] or not has_meta) and os.path.isfile(item['filename']): sha = item.get('hash', None) @@ -288,8 +284,8 @@ def civit_search_metadata(title: str = None): def create_search_metadata_table(rows): html = """
IDFileNameHashVersionsLatestStatus
FileIDNameHashVersionsLatestStatus
{row.id} {row.file}{row.id} {row.name} {row.sha} {row.versions}
- - + + {tbody} @@ -301,8 +297,8 @@ def civit_search_metadata(title: str = None): try: tbody += f""" - + diff --git a/modules/sd_checkpoint.py b/modules/sd_checkpoint.py index b54eb701f..1f193f603 100644 --- a/modules/sd_checkpoint.py +++ b/modules/sd_checkpoint.py @@ -153,20 +153,43 @@ def list_models(): def update_model_hashes(): - txt = [] + def update_model_hashes_table(rows): + html = """ +
IDNameTypeCodeHashSizeNote
NameIDTypeCodeHashSizeNote
{row['id']} {row['name']}{row['id']} {row['type']} {row['code']} {row['hash']}
+ + + + + {tbody} + +
NameTypeHash
+ """ + tbody = '' + for row in rows: + try: + tbody += f""" + + {row.name} + {row.type} + {row.shorthash} + + """ + except Exception as e: + shared.log.error(f'Model list: row={row} {e}') + return html.format(tbody=tbody) + lst = [ckpt for ckpt in checkpoints_list.values() if ckpt.hash is None] for ckpt in lst: ckpt.hash = model_hash(ckpt.filename) lst = [ckpt for ckpt in checkpoints_list.values() if ckpt.sha256 is None or ckpt.shorthash is None] shared.log.info(f'Models list: hash missing={len(lst)} total={len(checkpoints_list)}') + updated = [] for ckpt in lst: ckpt.sha256 = hashes.sha256(ckpt.filename, f"checkpoint/{ckpt.name}") ckpt.shorthash = ckpt.sha256[0:10] if ckpt.sha256 is not None else None - if ckpt.sha256 is not None: - txt.append(f'Hash: {ckpt.title} {ckpt.shorthash}') - txt.append(f'Updated hashes for {len(lst)} out of {len(checkpoints_list)} models') - txt = '
'.join(txt) - return txt + updated.append(ckpt) + yield update_model_hashes_table(updated) + return def remove_hash(s): diff --git a/modules/ui_models.py b/modules/ui_models.py index f5290ba08..56f505052 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -27,7 +27,7 @@ def create_ui(): def create_modules_table(rows: list): html = """ - + @@ -83,7 +83,7 @@ def create_ui(): from modules import sd_detect html = """
ModuleClassDeviceDtypeQuantParamsModulesConfig
- + @@ -131,10 +131,10 @@ def create_ui(): with gr.Row(): model_list_btn = gr.Button(value="List models", variant='primary') model_checkhash_btn = gr.Button(value="Calculate missing hashes", variant='secondary') - model_checkhash_btn.click(fn=sd_models.update_model_hashes, inputs=[], outputs=[models_outcome]) with gr.Row(): model_table = gr.HTML(value='', elem_id="model_list_table") + model_checkhash_btn.click(fn=sd_models.update_model_hashes, inputs=[], outputs=[model_table]) model_list_btn.click(fn=lambda: create_models_table(sd_models.checkpoints_list.values()), inputs=[], outputs=[model_table]) with gr.Tab(label="Metadata"): From 2cdb15b1ce46191497a42ce537cbfb02c39470c4 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Sun, 3 Aug 2025 18:38:13 +0300 Subject: [PATCH 012/141] IPEX enable int_mm fallback for torch 2.8 --- modules/intel/ipex/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/intel/ipex/__init__.py b/modules/intel/ipex/__init__.py index d165f050e..6e1f5047a 100644 --- a/modules/intel/ipex/__init__.py +++ b/modules/intel/ipex/__init__.py @@ -126,13 +126,15 @@ def ipex_init(): # pylint: disable=too-many-statements torch.cuda.List = torch.xpu.List if torch_version < 2.9: - # torch._int_mm via onednn is supposed to land on pytorch with torch 2.8 or 2.9 - # ipex 2.7+ has experimental torch._int_mm support but uses the cpu with torch.compile and also runs as slow as onednn.qlinear - if (not has_ipex or torch_version <= 2.7) and hasattr(torch.ops, "onednn") and hasattr(torch.ops.onednn, "qlinear_pointwise"): - def onednn_mm(x: torch.Tensor, y: torch.Tensor, output_dtype=torch.float32): - # supports int8, fp32, fp16, and bf16 matmul with accumulation using a different float dtype + # torch._int_mm via onednn quantized matmul is supported with torch 2.9 + # ipex 2.7+ has the same torch._int_mm support as torch 2.9 but doesn't support torch.compile + # torch._int_mm directly uses onednn quantized matmul + # onednn qlinear is a wrapper around onednn quantized matmul + if hasattr(torch.ops, "onednn") and hasattr(torch.ops.onednn, "qlinear_pointwise"): + def onednn_mm(x: torch.Tensor, y: torch.Tensor): + # supports int8, fp32, fp16, and bf16 matmul with accumulation using a different dtype # int8 matmul with onednn is slower than 16 bit with dim_size < 4096 - return torch.ops.onednn.qlinear_pointwise(x, 1.0, 0, y, torch.ones(1, device=y.device), torch.zeros(1, device=y.device), None, 1.0, 0, output_dtype, "none", [], "none") + return torch.ops.onednn.qlinear_pointwise.default(x, 1.0, 0, y, torch.ones(1, device=y.device), torch.zeros(1, device=y.device), None, 1.0, 0, torch.float32, "none", [], "none") torch._int_mm = onednn_mm try: # torch.compile fix From 66456f3b4f5acf37fe91f7e21845c40aed56bc81 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 3 Aug 2025 16:31:55 -0400 Subject: [PATCH 013/141] civitai search redesign prototype Signed-off-by: Vladimir Mandic --- cli/civitai-search.py | 86 ++++-- extensions-builtin/sdnext-modernui | 2 +- javascript/civitai.js | 264 ++++++++++++++++++ javascript/sdnext.css | 6 - modules/api/api.py | 4 + modules/civitai/api_civitai.py | 59 ++++ modules/civitai/download_civitai.py | 0 .../metadata_civitai.py} | 0 modules/civitai/search_civitai.py | 212 ++++++++++++++ modules/loader.py | 11 +- modules/onnx_impl/__init__.py | 1 + modules/ui_models.py | 53 +++- modules/zluda.py | 12 +- 13 files changed, 652 insertions(+), 58 deletions(-) mode change 100644 => 100755 cli/civitai-search.py create mode 100644 javascript/civitai.js create mode 100644 modules/civitai/api_civitai.py create mode 100644 modules/civitai/download_civitai.py rename modules/{models_civitai.py => civitai/metadata_civitai.py} (100%) create mode 100644 modules/civitai/search_civitai.py diff --git a/cli/civitai-search.py b/cli/civitai-search.py old mode 100644 new mode 100755 index 53c524338..3d91e8711 --- a/cli/civitai-search.py +++ b/cli/civitai-search.py @@ -1,84 +1,93 @@ +#!/usr/bin/env python +from dataclasses import dataclass import os import sys import json import time import logging -import bs4 +full_dct = False +full_html = False debug = False logging.basicConfig(level = logging.INFO, format = '%(asctime)s %(levelname)s: %(message)s') log = logging.getLogger(__name__) -class ModelImage(object): +@dataclass +class ModelImage(): def __init__(self, dct: dict): if isinstance(dct, str): dct = json.loads(dct) - self.dct: dict = dct self.id: int = dct.get('id', 0) self.url: str = dct.get('url', '') self.width: int = dct.get('width', 0) self.height: int = dct.get('height', 0) self.type: str = dct.get('type', 'Unknown') + self.dct: dict = dct if full_dct else {} def __str__(self): return f'ModelImage(id={self.id} url="{self.url}" width={self.width} height={self.height} type="{self.type}")' -class ModelFile(object): +@dataclass +class ModelFile(): def __init__(self, dct: dict): if isinstance(dct, str): dct = json.loads(dct) - self.dct: dict = dct self.id: int = dct.get('id', 0) self.size: int = int(1024 * dct.get('sizeKB', 0)) self.name: str = dct.get('name', 'Unknown') self.type: str = dct.get('type', 'Unknown') self.hashes: list[str] = dct.get('hashes', {}).values() self.url: str = dct.get('downloadUrl', '') + self.dct: dict = dct if full_dct else {} def __str__(self): return f'ModelFile(id={self.id} name="{self.name}" size={self.size} type="{self.type}" url="{self.url}")' -class ModelVersion(object): +@dataclass +class ModelVersion(): def __init__(self, dct: dict): + import bs4 if isinstance(dct, str): dct = json.loads(dct) - self.dct = dct - self.id = dct.get('id', 0) - self.name = dct.get('name', 'Unknown') - self.base = dct.get('baseModel', 'Unknown') - self.mtime = dct.get('publishedAt', '') - self.downloads = dct.get('stats', {}).get('downloadCount', 0) - self.availability = dct.get('availability', 'Unknown') - self.html = dct.get('description', '') or '' - self.desc = bs4.BeautifulSoup(self.html, features="html.parser").get_text() + self.id: int = dct.get('id', 0) + self.name: str = dct.get('name', 'Unknown') + self.base: str = dct.get('baseModel', 'Unknown') + self.mtime: str = dct.get('publishedAt', '') + self.downloads: int = dct.get('stats', {}).get('downloadCount', 0) + self.availability: str = dct.get('availability', 'Unknown') + self.html: str = dct.get('description', '') or '' if full_html else '' + self.desc: str = bs4.BeautifulSoup(dct.get('description', '') or '', features="html.parser").get_text() self.files = [ModelFile(f) for f in dct.get('files', [])] self.images = [ModelImage(i) for i in dct.get('images', [])] + self.dct: dict = dct if full_dct else {} def __str__(self): return f'ModelVersion(id={self.id} name="{self.name}" base="{self.base}" mtime="{self.mtime}" downloads={self.downloads} availability={self.availability} desc="{self.desc[:30]}...")' -class Model(object): +@dataclass +class Model(): def __init__(self, dct: dict): + import bs4 if isinstance(dct, str): dct = json.loads(dct) - self.id = dct.get('id', 0) - self.dct = dct - self.url = f'https://civitai.com/models/{self.id}' - self.type = dct.get('type', 'Unknown') - self.name = dct.get('name', 'Unknown') - self.html = dct.get('description', '') - self.desc = bs4.BeautifulSoup(self.html, features="html.parser").get_text() - self.tags = dct.get('tags', []) - self.nsfw = dct.get('nsfw', False) - self.level = dct.get('nsfwLevel', 0) - self.availability = dct.get('availability', 'Unknown') - self.downloads = dct.get('stats', {}).get('downloadCount', 0) - self.creator = dct.get('creator', {}).get('username', 'Unknown') - self.versions = [ModelVersion(v) for v in dct.get('modelVersions', [])] + self.id: int = dct.get('id', 0) + self.url: str = f'https://civitai.com/models/{self.id}' + self.type: str = dct.get('type', 'Unknown') + self.name: str = dct.get('name', 'Unknown') + self.html: str = dct.get('description', '') or '' if full_html else '' + self.desc: str = bs4.BeautifulSoup(dct.get('description', '') or '', features="html.parser").get_text() + self.tags: list[str] = dct.get('tags', []) + self.nsfw: bool = dct.get('nsfw', False) + self.level: str = dct.get('nsfwLevel', 0) + self.availability: str = dct.get('availability', 'Unknown') + self.downloads: int = dct.get('stats', {}).get('downloadCount', 0) + self.creator: str = dct.get('creator', {}).get('username', 'Unknown') + self.versions: list[ModelVersion] = [ModelVersion(v) for v in dct.get('modelVersions', [])] + self.dct: dict = dct if full_dct else {} def __str__(self): return f'Model(id={self.id} type={self.type} name="{self.name}" versions={len(self.versions)} nsfw={self.nsfw}/{self.level} downloads={self.downloads} author="{self.creator}" tags={self.tags} desc="{self.desc[:30]}...")' @@ -155,6 +164,23 @@ def search_civitai( return exact_models if len(exact_models) > 0 else models +def models_to_dct(all_models:list, model_id:int=None): + dct = [] + for model in all_models: + if model_id is not None and model.id != model_id: + continue + model_dct = model.__dict__.copy() + versions_dct = [] + for version in model.versions: + version_dct = version.__dict__.copy() + version_dct['files'] = [f.__dict__.copy() for f in version.files] + version_dct['images'] = [i.__dict__.copy() for i in version.images] + versions_dct.append(version_dct) + model_dct['versions'] = versions_dct + dct.append(model_dct) + return dct + + def print_models(models: list[Model]): if debug: from rich import print as dbg diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 03e365e0b..540353613 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 03e365e0b0ec5e1cc442c9dbe683b61321fc5630 +Subproject commit 540353613400c419f8f0d92d6569e1e695e04f03 diff --git a/javascript/civitai.js b/javascript/civitai.js new file mode 100644 index 000000000..e61980770 --- /dev/null +++ b/javascript/civitai.js @@ -0,0 +1,264 @@ +// hack to get pythons str.format in js +String.prototype.format = function (arguments) { // eslint-disable-line no-extend-native, func-names + let thisString = ''; + for (let charPos = 0; charPos < this.length; charPos++) thisString += this[charPos]; + for (const key in arguments) { // eslint-disable-line guard-for-in + error(key, arguments[key]); + const stringKey = `{${key}}`; + thisString = thisString.replace(new RegExp(stringKey, 'g'), arguments[key]); + } + return thisString; +}; + +const modelDetailsHTML = ` +
+

{name}

+

Type: {type}

+

Tags: {tags}

+

NSFW: {nsfw}/{level}

+

Availability: {availability}

+

Downloads: {downloads}

+

Author: {creator}

+
{versions}
+
+`; + +async function modelCardClick(id) { + log('modelCardClick id', id); + const el = gradioApp().getElementById('model-details'); + if (!el) return; + const res = await fetch(`${window.api}/civitai?model_id=${encodeURI(id)}`); + if (!res || res.status !== 200) { + error(`modelCardClick: id=${id} status=${res ? res.status : 'unknown'}`); + return; + } + let data = await res.json(); + log('modelCardClick data', data); + if (!data || data.length === 0) return; + data = data[0]; // assuming the first item is the one we want + const obj = { + name: data.name || 'unknown', + type: data.type || 'unknown', + tags: data.tags?.join(', ') || '', + nsfw: data.nsfw ? 'yes' : 'no', + level: data.level?.toString() || '', + availability: data.availability || 'unknown', + downloads: data.downloads?.toString() || '', + creator: data.creator || 'unknown', + versions: JSON.stringify(data.versions) || '[]', + }; + log(obj); + el.innerHTML = modelDetailsHTML.format({ + name: data.name || 'unknown', + type: data.type || 'unknown', + tags: data.tags?.join(', ') || '', + nsfw: data.nsfw ? 'yes' : 'no', + level: data.level?.toString() || '', + availability: data.availability || 'unknown', + downloads: data.downloads?.toString() || '', + creator: data.creator || 'unknown', + versions: JSON.stringify(data.versions) || '[]', + }); +} + +const example = { + id: 1157409, + url: 'https://civitai.com/models/1157409', + type: 'Checkpoint', + name: 'Tempest-by-Vlad', + html: '', + desc: 'Base versionFlexible SDXL model with custom encoder and finetuned for larger landscape resolutions with high details and high contrast.Recommended to use medium-low...', + tags: [ + 'base model', + ], + nsfw: false, + level: 15, + availability: 'Public', + downloads: 407, + creator: 'vmandic', + versions: [ + { + id: 1301775, + name: 'Base v0.1', + base: 'SDXL 1.0', + mtime: '2025-01-19T02:53:53.903Z', + downloads: 346, + availability: 'Public', + html: '', + desc: 'Initial release', + files: [ + { + id: 1206102, + size: 6938089790, + name: 'tempestByVlad_baseV01.safetensors', + type: 'Model', + hashes: [ + '79CB1E32', + '8BFAD17222', + '8BFAD1722243955B3F94103C69079C280D348B14729251E86824972C1063B616', + '43E5E3BB', + 'DE83D56256411853AB6595CC3D8E865D5310D4A58D49A839DDC104C7F3429D4A', + '4E933E1EBE61', + ], + url: 'https://civitai.com/api/download/models/1301775', + dct: {}, + }, + ], + images: [ + { + id: 52503951, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/18c749f2-42ec-4024-9d20-0b1202b6bacc/width=1024/52503951.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + { + id: 52508539, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/634d0ea8-ecdb-4ca6-a4ff-145319bc3fd3/width=1024/52508539.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + { + id: 52508563, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/be529820-a89e-458f-8a3d-86cb43b154ac/width=1024/52508563.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + { + id: 52508588, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/b2eb456a-a664-4de8-8c3e-6ecd1c4acb38/width=1024/52508588.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + { + id: 52508654, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/f7b09e3f-4a48-459b-9b32-fa207904f74c/width=1024/52508654.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + { + id: 52508659, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/fff89f18-628a-43b3-b9f5-44951dc078f7/width=1024/52508659.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + { + id: 52508671, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/fdd3f774-ce2e-4ea7-82a0-bdb523fb86f6/width=1024/52508671.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + { + id: 52512251, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/f26971c8-1123-45e3-a85b-2b97c6334b85/width=1024/52512251.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + ], + dct: {}, + }, + { + id: 1343512, + name: 'Hyper v0.1', + base: 'SDXL 1.0', + mtime: '2025-01-28T22:54:12.734Z', + downloads: 61, + availability: 'Public', + html: '', + desc: 'Time-distilled version', + files: [ + { + id: 1246991, + size: 6938085702, + name: 'tempestByVlad_hyperV01.safetensors', + type: 'Model', + hashes: [ + '15943FD9', + '4104FC6601', + '4104FC6601F71C4C7A770AD422483FD700C8ECF72D06FCD8C4E8CD4B2D1C7DBB', + '9F87BCEA', + 'CB52894625E9C13331285E4435799D707C4EAEF464974159C8B4B217EA32298E', + 'A0EE15E503DD', + ], + url: 'https://civitai.com/api/download/models/1343512', + dct: {}, + }, + ], + images: [ + { + id: 54462987, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/1dd020c8-8a9a-4eb3-afec-fe83613217c5/width=1024/54462987.jpeg', + width: 1024, + height: 768, + type: 'image', + dct: {}, + }, + { + id: 54462992, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/89edf233-939f-4b2e-97c8-175498704362/width=1536/54462992.jpeg', + width: 1536, + height: 640, + type: 'image', + dct: {}, + }, + { + id: 54463002, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/87aea8fb-7687-48d0-98e6-27555b2ff87f/width=768/54463002.jpeg', + width: 768, + height: 1024, + type: 'image', + dct: {}, + }, + { + id: 54463010, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/f1ee57cc-8920-4b8d-853a-ad1cfc7d9a5a/width=1024/54463010.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + { + id: 54463011, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dda6411b-fd36-484f-a9f0-0db847463128/width=1024/54463011.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + { + id: 54463016, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/5f266a8c-f215-4e0f-8a64-aacc97f81d70/width=1024/54463016.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + { + id: 54463019, + url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/0ad04bdc-41bd-433c-9c25-f5365fbe082c/width=1024/54463019.jpeg', + width: 1024, + height: 1024, + type: 'image', + dct: {}, + }, + ], + dct: {}, + }, + ], + dct: {}, +}; diff --git a/javascript/sdnext.css b/javascript/sdnext.css index ee55b8785..2013cabdb 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -1515,12 +1515,6 @@ background: var(--background-color) min-height: 0; } -#models_error { - font-family: monospace; - -color: var(--body-text-color-subdued) -} - #model_loader_df button { display: none !important; } diff --git a/modules/api/api.py b/modules/api/api.py index dfc4cacb3..8b0ae3400 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -111,6 +111,10 @@ class Api: from modules.api import nudenet nudenet.register_api() + # civitai api + from modules.civitai import api_civitai + api_civitai.register_api() + def add_api_route(self, path: str, endpoint, **kwargs): if (shared.cmd_opts.auth or shared.cmd_opts.auth_file) and shared.cmd_opts.api_only: diff --git a/modules/civitai/api_civitai.py b/modules/civitai/api_civitai.py new file mode 100644 index 000000000..8e4561723 --- /dev/null +++ b/modules/civitai/api_civitai.py @@ -0,0 +1,59 @@ +from starlette.responses import JSONResponse + + +def models_to_json(all_models:list, model_id:int=None): + dct = [] + for model in all_models: + if model_id is not None and model.id != model_id: + continue + model_dct = model.__dict__.copy() + versions_dct = [] + for version in model.versions: + version_dct = version.__dict__.copy() + version_dct['files'] = [f.__dict__.copy() for f in version.files] + version_dct['images'] = [i.__dict__.copy() for i in version.images] + versions_dct.append(version_dct) + model_dct['versions'] = versions_dct + dct.append(model_dct) + # obj = json.dumps(dct, indent=2, ensure_ascii=False) + return dct + + +def get_civitai( + model_id:int=None, # if model_id is provided assume fetch-from-cache + query:str = '', # search query or tag is required + tag:str = '', # search query or tag is required + types:str = '', # Checkpoint, TextualInversion, Hypernetwork, AestheticGradient, LORA, Controlnet, Poses + sort:str = '', # Highest Rated, Most Downloaded, Newest + period:str = '', # AllTime, Year, Month, Week, Day + nsfw:bool = None, # optional:bool + limit:int = 0, + base:list[str] = [], # list + token:str = None, + exact:bool = True, +): + from modules.civitai import search_civitai + if model_id is not None: + dct = models_to_json(search_civitai.models, model_id=model_id) + return JSONResponse(content=dct, status_code=200) + if len(query) > 0 or len(tag) > 0: + models = search_civitai.search_civitai( + query=query, + tag=tag, + types=types, + sort=sort, + period=period, + nsfw=nsfw, + limit=limit, + base=base, + token=token, + exact=exact + ) + dct = models_to_json(models) + return JSONResponse(content=dct, status_code=200) + return JSONResponse(content=[], status_code=200) + + +def register_api(): + from modules.shared import api + api.add_api_route("/sdapi/v1/civitai", get_civitai, methods=["GET"], response_model=list) diff --git a/modules/civitai/download_civitai.py b/modules/civitai/download_civitai.py new file mode 100644 index 000000000..e69de29bb diff --git a/modules/models_civitai.py b/modules/civitai/metadata_civitai.py similarity index 100% rename from modules/models_civitai.py rename to modules/civitai/metadata_civitai.py diff --git a/modules/civitai/search_civitai.py b/modules/civitai/search_civitai.py new file mode 100644 index 000000000..6f7bf8361 --- /dev/null +++ b/modules/civitai/search_civitai.py @@ -0,0 +1,212 @@ +from dataclasses import dataclass +import os +import json +import time +from installer import install, log + + +full_dct = False +full_html = False + + +@dataclass +class ModelImage(): + def __init__(self, dct: dict): + if isinstance(dct, str): + dct = json.loads(dct) + self.id: int = dct.get('id', 0) + self.url: str = dct.get('url', '') + self.width: int = dct.get('width', 0) + self.height: int = dct.get('height', 0) + self.type: str = dct.get('type', 'Unknown') + self.dct: dict = dct if full_dct else {} + + def __str__(self): + return f'ModelImage(id={self.id} url="{self.url}" width={self.width} height={self.height} type="{self.type}")' + + +@dataclass +class ModelFile(): + def __init__(self, dct: dict): + if isinstance(dct, str): + dct = json.loads(dct) + self.id: int = dct.get('id', 0) + self.size: int = int(1024 * dct.get('sizeKB', 0)) + self.name: str = dct.get('name', 'Unknown') + self.type: str = dct.get('type', 'Unknown') + self.hashes: list[str] = [str(h) for h in dct.get('hashes', {}).values()] + self.url: str = dct.get('downloadUrl', '') + self.dct: dict = dct if full_dct else {} + + def __str__(self): + return f'ModelFile(id={self.id} name="{self.name}" size={self.size} type="{self.type}" url="{self.url}")' + + +@dataclass +class ModelVersion(): + def __init__(self, dct: dict): + import bs4 + if isinstance(dct, str): + dct = json.loads(dct) + self.id: int = dct.get('id', 0) + self.name: str = dct.get('name', 'Unknown') + self.base: str = dct.get('baseModel', 'Unknown') + self.mtime: str = dct.get('publishedAt', '') + self.downloads: int = dct.get('stats', {}).get('downloadCount', 0) + self.availability: str = dct.get('availability', 'Unknown') + self.html: str = dct.get('description', '') or '' if full_html else '' + self.desc: str = bs4.BeautifulSoup(dct.get('description', '') or '', features="html.parser").get_text() + self.files = [ModelFile(f) for f in dct.get('files', [])] + self.images = [ModelImage(i) for i in dct.get('images', [])] + self.dct: dict = dct if full_dct else {} + + def __str__(self): + return f'ModelVersion(id={self.id} name="{self.name}" base="{self.base}" mtime="{self.mtime}" downloads={self.downloads} availability={self.availability} desc="{self.desc[:30]}...")' + + +@dataclass +class Model(): + def __init__(self, dct: dict): + import bs4 + if isinstance(dct, str): + dct = json.loads(dct) + self.id: int = dct.get('id', 0) + self.url: str = f'https://civitai.com/models/{self.id}' + self.type: str = dct.get('type', 'Unknown') + self.name: str = dct.get('name', 'Unknown') + self.html: str = dct.get('description', '') or '' if full_html else '' + self.desc: str = bs4.BeautifulSoup(dct.get('description', '') or '', features="html.parser").get_text() + self.tags: list[str] = dct.get('tags', []) + self.nsfw: bool = dct.get('nsfw', False) + self.level: str = dct.get('nsfwLevel', 0) + self.availability: str = dct.get('availability', 'Unknown') + self.downloads: int = dct.get('stats', {}).get('downloadCount', 0) + self.creator: str = dct.get('creator', {}).get('username', 'Unknown') + self.versions: list[ModelVersion] = [ModelVersion(v) for v in dct.get('modelVersions', [])] + self.dct: dict = dct if full_dct else {} + + def __str__(self): + return f'Model(id={self.id} type={self.type} name="{self.name}" versions={len(self.versions)} nsfw={self.nsfw}/{self.level} downloads={self.downloads} author="{self.creator}" tags={self.tags} desc="{self.desc[:30]}...")' + + +models: list[Model] = [] # global cache for civitai search results + + +def search_civitai( + query:str, + tag:str = '', # optional:tag name + types:str = '', # (Checkpoint, TextualInversion, Hypernetwork, AestheticGradient, LORA, Controlnet, Poses) + sort:str = '', # (Highest Rated, Most Downloaded, Newest) + period:str = '', # (AllTime, Year, Month, Week, Day) + nsfw:bool = None, # optional:bool + limit:int = 0, + base:list[str] = [], # list + token:str = None, + exact:bool = True, +): + global models # pylint: disable=global-statement + import requests + from urllib.parse import urlencode + install('bs4') # Ensure BeautifulSoup is installed + + if len(query) == 0: + log.error('CivitAI: empty query') + return [] + + t0 = time.time() + dct = { 'query': query } + if len(tag) > 0: + dct['tag'] = tag + if nsfw is not None: + dct['nsfw'] = 'true' if nsfw else 'false' + if limit > 0: + dct['limit'] = limit + if len(types) > 0: + dct['types'] = types + if len(sort) > 0: + dct['sort'] = sort + if len(period) > 0: + dct['period'] = period + if len(base) > 0: + dct['baseModels'] = ','.join(base) + encoded = urlencode(dct) + + headers = {} + if token is None: + token = os.environ.get('CIVITAI_TOKEN', None) + if token is not None and len(token) > 0: + headers['Authorization'] = f'Bearer {token}' + + url = 'https://civitai.com/api/v1/models' + uri = f'{url}?{encoded}' + log.info(f'CivitAI request: uri="{uri}" dct={dct} token={token is not None}') + result = requests.get(uri, headers=headers, timeout=60) + + if result.status_code != 200: + log.error(f'CivitAI: code={result.status_code} reason={result.reason} uri={result.url}') + return [] + + all_models: list[Model] = [] + exact_models: list[Model] = [] + items = result.json().get('items', []) + for item in items: + all_models.append(Model(item)) + + if exact: + for model in all_models: + model_names = [model.name.lower()] + version_names = [v.name.lower() for v in model.versions] + file_names = [f.name.lower() for v in model.versions for f in v.files] + if any([query.lower() in name for name in model_names + version_names + file_names]): # noqa: C419 + exact_models.append(model) + + t1 = time.time() + log.info(f'CivitAI result: code={result.status_code} exact={len(exact_models)} total={len(models)} time={t1-t0:.2f}') + models = exact_models if len(exact_models) > 0 else all_models + return models + + +def create_model_cards(all_models: list[Model]) -> str: + details = """ +
+
+ """ + cards = """ +
+ {cards} +
+ """ + card = """ +
+
{name}
+
{type}
+ {name} +
+ """ + all_cards = '' + for model in all_models: + previews = [] + for version in model.versions: + for image in version.images: + if image.url and len(image.url) > 0: + previews.append(image.url) + if len(previews) == 0: + previews = ['./sd_extra_networks/thumb?filename=html/card-no-preview.png'] + all_cards += card.format(id=model.id, name=model.name, type=model.type, preview=previews[0]) + html = details + cards.format(cards=all_cards) + return html + + +def print_models(all_models: list[Model]): + for model in all_models: + log.info(f' {model}') + log.trace('Model', model.dct) + for version in model.versions: + log.info(f' {version}') + log.trace('ModelVersion', version.dct) + for file in version.files: + log.info(f' {file}') + log.trace('ModelFile', file.dct) + for image in version.images: + log.info(f' {image}') + log.trace('ModelImage', image.dct) diff --git a/modules/loader.py b/modules/loader.py index d3a33c52e..dee03cad8 100644 --- a/modules/loader.py +++ b/modules/loader.py @@ -87,10 +87,13 @@ timer.startup.record("transformers") import accelerate # pylint: disable=W0611,C0411 timer.startup.record("accelerate") -import onnxruntime # pylint: disable=W0611,C0411 -onnxruntime.set_default_logger_severity(4) -onnxruntime.set_default_logger_verbosity(1) -onnxruntime.disable_telemetry_events() +try: + import onnxruntime # pylint: disable=W0611,C0411 + onnxruntime.set_default_logger_severity(4) + onnxruntime.set_default_logger_verbosity(1) + onnxruntime.disable_telemetry_events() +except Exception as e: + errors.log.warning(f'Torch onnxruntime: {e}') timer.startup.record("onnx") from fastapi import FastAPI # pylint: disable=W0611,C0411 diff --git a/modules/onnx_impl/__init__.py b/modules/onnx_impl/__init__.py index a8e04b691..5a009a741 100644 --- a/modules/onnx_impl/__init__.py +++ b/modules/onnx_impl/__init__.py @@ -4,6 +4,7 @@ import torch import diffusers import onnxruntime as ort + initialized = False diff --git a/modules/ui_models.py b/modules/ui_models.py index 56f505052..d008d80fb 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -16,14 +16,12 @@ def create_ui(): dummy_component = gr.Label(visible=False) with gr.Row(elem_id="models_tab"): with gr.Column(elem_id='models_output_container', scale=1): - gr.HTML(elem_id="models_progress", value="") - models_image = gr.Image(elem_id="models_image", show_label=False, interactive=False, type='pil') - models_outcome = gr.HTML(elem_id="models_error", value="") + models_outcome = gr.HTML(elem_id="models_outcome", value="") models_file = gr.File(label='', visible=False) with gr.Column(elem_id='models_input_container', scale=3): - with gr.Tab(label="Current"): + with gr.Tab(label="Current", elem_id="models_current_tab"): def create_modules_table(rows: list): html = """
NameTypeDetectPipelineHashSizeMTime
@@ -78,7 +76,7 @@ def create_ui(): model_analyze.click(fn=analyze, inputs=[], outputs=[model_desc, model_meta]) - with gr.Tab(label="List"): + with gr.Tab(label="List", elem_id="models_list_tab"): def create_models_table(rows: list): from modules import sd_detect html = """ @@ -137,8 +135,8 @@ def create_ui(): model_checkhash_btn.click(fn=sd_models.update_model_hashes, inputs=[], outputs=[model_table]) model_list_btn.click(fn=lambda: create_models_table(sd_models.checkpoints_list.values()), inputs=[], outputs=[model_table]) - with gr.Tab(label="Metadata"): - from modules.models_civitai import civit_search_metadata, civit_update_metadata + with gr.Tab(label="Metadata", elem_id="models_metadata_tab"): + from modules.civitai.metadata_civitai import civit_search_metadata, civit_update_metadata with gr.Row(): gr.HTML('

Fetch model preview metadata


') with gr.Row(): @@ -150,11 +148,11 @@ def create_ui(): civit_update_btn.click(fn=civit_update_metadata, inputs=[], outputs=[civit_metadata]) - with gr.Tab(label="Loader"): + with gr.Tab(label="Loader", elem_id="models_loader_tab"): from modules import ui_models_load ui_models_load.create_ui(models_outcome, models_file) - with gr.Tab(label="Merge"): + with gr.Tab(label="Merge", elem_id="models_merge_tab"): from modules.merging import merge_methods from modules.merging.merge_utils import BETA_METHODS, TRIPLE_METHODS, interpolate from modules.merging.merge_presets import BLOCK_WEIGHTS_PRESETS, SDXL_BLOCK_WEIGHTS_PRESETS @@ -398,7 +396,7 @@ def create_ui(): ] ) - with gr.Tab(label="Replace"): + with gr.Tab(label="Replace", elem_id="models_replace_tab"): with gr.Row(): gr.HTML('

 Replace model components

') with gr.Row(): @@ -470,8 +468,36 @@ def create_ui(): outputs=[models_outcome] ) - with gr.Tab(label="CivitAI"): - from modules.models_civitai import civitai_update_token, civit_search_model, civit_search_metadata, civit_select1, civit_select2, civit_select3, civit_download_model + with gr.Tab(label="CivitAI", elem_id="models_civitai_tab"): + def civitai_search(civit_search_text, civit_search_tag, civit_nsfw, civit_type, civit_base, civit_token): + from modules.civitai.search_civitai import search_civitai, create_model_cards + results = search_civitai(query=civit_search_text, tag=civit_search_tag, nsfw=civit_nsfw, types=civit_type, base=civit_base, token=civit_token) + html = create_model_cards(results) + return html + + with gr.Row(): + gr.HTML('

Search & Download

') + with gr.Row(elem_id='civitai_search_row'): + civit_search_text = gr.Textbox(label='', placeholder='keyword', elem_id="civit_search_text") + civit_search_tag = gr.Textbox(label='', placeholder='tag', elem_id="civit_search_text") + civit_search_text_btn = ToolButton(value=ui_symbols.search, interactive=True) + with gr.Accordion(label='Search options', open=False, elem_id="civitai_search_options"): + with gr.Row(): + civit_nsfw = gr.Checkbox(label='NSFW allowed', value=True) + with gr.Row(): + civit_type = gr.Textbox(label='Model type', placeholder='Checkpoint, LORA, ...') + with gr.Row(): + civit_base = gr.Textbox(label='Base model', placeholder='SDXL, ...') + with gr.Row(): + civit_token = gr.Textbox(opts.civitai_token, label='CivitAI token', placeholder='optional access token for private or gated models') + # sort, period, limit + civit_inputs = [civit_search_text, civit_search_tag, civit_nsfw, civit_type, civit_base, civit_token] + civit_search_text_btn.click(fn=civitai_search, inputs=civit_inputs, outputs=[models_outcome]) + civit_search_text.submit(fn=civitai_search, inputs=civit_inputs, outputs=[models_outcome]) + civit_search_tag.submit(fn=civitai_search, inputs=civit_inputs, outputs=[models_outcome]) + + """ + from modules.civitai.legacy_civitai import civitai_update_token, civit_search_model, civit_search_metadata, civit_select1, civit_select2, civit_select3, civit_download_model with gr.Row(): gr.HTML('

Search for models

') @@ -527,8 +553,9 @@ def create_ui(): civit_results2.change(fn=is_visible, inputs=[civit_results2], outputs=[civit_results2]) civit_results3.change(fn=is_visible, inputs=[civit_results3], outputs=[civit_results3]) civit_download_model_btn.click(fn=civit_download_model, inputs=[civit_selected, civit_name, civit_path, civit_model_type, civit_token], outputs=[models_outcome]) + """ - with gr.Tab(label="Huggingface"): + with gr.Tab(label="Huggingface", elem_id="models_huggingface_tab"): from modules.models_hf import hf_search, hf_select, hf_download_model, hf_update_token with gr.Column(scale=6): with gr.Row(): diff --git a/modules/zluda.py b/modules/zluda.py index 8c7802c38..12186ffa7 100644 --- a/modules/zluda.py +++ b/modules/zluda.py @@ -2,7 +2,6 @@ import sys from typing import Union import torch from torch._prims_common import DeviceLikeType -import onnxruntime as ort from modules import shared, devices, zluda_installer from modules.zluda_installer import core, default_agent # pylint: disable=unused-import from modules.onnx_impl.execution_providers import available_execution_providers, ExecutionProvider @@ -42,9 +41,14 @@ def initialize_zluda(): torch.backends.cuda.enable_mem_efficient_sdp = do_nothing # ONNX Runtime is not supported - ort.capi._pybind_state.get_available_providers = lambda: [v for v in available_execution_providers if v != ExecutionProvider.CUDA] # pylint: disable=protected-access - ort.get_available_providers = ort.capi._pybind_state.get_available_providers # pylint: disable=protected-access - if shared.opts.onnx_execution_provider == ExecutionProvider.CUDA: + try: + import onnxruntime as ort + ort.capi._pybind_state.get_available_providers = lambda: [v for v in available_execution_providers if v != ExecutionProvider.CUDA] # pylint: disable=protected-access + ort.get_available_providers = ort.capi._pybind_state.get_available_providers # pylint: disable=protected-access + if shared.opts.onnx_execution_provider == ExecutionProvider.CUDA: + shared.opts.onnx_execution_provider = ExecutionProvider.CPU + except Exception as e: + shared.log.warning(f'ZLUDA ONNX runtime: {e}') shared.opts.onnx_execution_provider = ExecutionProvider.CPU device = devices.get_optimal_device() From 895c7f41fbea01bff290646791c518e0e51ef0b9 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 4 Aug 2025 08:50:44 -0400 Subject: [PATCH 014/141] gallery bypass thumb cache and safer delete ops Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 2 ++ javascript/gallery.js | 3 ++- javascript/ui.js | 9 ++++++++- modules/ui_common.py | 6 +++--- modules/ui_gallery.py | 1 + 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d64f78db5..bf6dd4465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ - updated *models -> list models* tab - updated *models -> metadata* tab - more css optimizations and styling + - gallery bypass browser cache for thumbnails + - gallery safer delete operation - **Offloading** - changed **default** values for offloading based on detected gpu memory see [offloading docs](https://vladmandic.github.io/sdnext-docs/Offload/) for details diff --git a/javascript/gallery.js b/javascript/gallery.js index 2de1d70a3..9d94235b3 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -192,7 +192,8 @@ async function addSeparators() { async function delayFetchThumb(fn) { while (outstanding > 16) await new Promise((resolve) => setTimeout(resolve, 50)); // eslint-disable-line no-promise-executor-return outstanding++; - const res = await fetch(`${window.api}/browser/thumb?file=${encodeURI(fn)}`, { priority: 'low' }); + const ts = Date.now().toString(); + const res = await fetch(`${window.api}/browser/thumb?file=${encodeURI(fn)}&ts=${ts}`, { priority: 'low' }); if (!res.ok) { error(`fetchThumb: ${res.statusText}`); outstanding--; diff --git a/javascript/ui.js b/javascript/ui.js index 4df527ceb..a499dc32d 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -32,6 +32,12 @@ function clip_gallery_urls(gallery) { ); } +function isVisible(el) { + const rect = el.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) return false; + return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth); +} + function all_gallery_buttons() { let allGalleryButtons = gradioApp().querySelectorAll('[style="display: block;"].tabitem div[id$=_gallery].gradio-gallery .thumbnails > .thumbnail-item.thumbnail-small'); if (allGalleryButtons.length === 0) allGalleryButtons = gradioApp().querySelectorAll('.gradio-gallery .thumbnails > .thumbnail-item.thumbnail-small'); @@ -66,6 +72,7 @@ function selected_gallery_files() { let allCurrentButtons = gradioApp().querySelectorAll('[style="display: block;"].tabitem div[id$=_gallery].gradio-gallery .thumbnail-item.thumbnail-small'); if (allCurrentButtons.length === 0) allCurrentButtons = gradioApp().querySelectorAll('.gradio-gallery .thumbnails > .thumbnail-item.thumbnail-small'); allImages = Array.from(allCurrentButtons).map((v) => v.querySelector('img')?.src); + allImages = allImages.filter((el) => isVisible(el)); } catch { /**/ } const selectedIndex = selected_gallery_index(); return [allImages, selectedIndex]; @@ -178,7 +185,7 @@ function switch_to_caption(...args) { function get_tab_index(tabId) { let res = 0; - gradioApp().getElementById(tabId).querySelector('div').querySelectorAll('button') + gradioApp().getElementById(tabId)?.querySelector('div').querySelectorAll('button') .forEach((button, i) => { if (button.className.indexOf('selected') !== -1) res = i; }); diff --git a/modules/ui_common.py b/modules/ui_common.py index 33a76e850..9df1a9ccb 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -67,7 +67,7 @@ def delete_files(js_data, files, all_files, index): for _image_index, filedata in enumerate(files, start_index): try: fn = filedata['name'] - if os.path.isfile(fn): + if os.path.exists(fn) and os.path.isfile(fn): deleted.append(fn) os.remove(fn) if fn in all_files: @@ -75,11 +75,11 @@ def delete_files(js_data, files, all_files, index): shared.log.info(f'Delete: image="{fn}"') base, _ext = os.path.splitext(fn) desc = f'{base}.txt' - if os.path.exists(desc): + if os.path.exists(desc) and os.path.isfile(desc): os.remove(desc) shared.log.info(f'Delete: text="{fn}"') except Exception as e: - shared.log.error(f'Delete: image="{fn}" {e}') + shared.log.error(f'Delete: file="{fn}" {e}') deleted = ', '.join(deleted) if len(deleted) > 0 else 'none' return all_files, plaintext_to_html(f"Deleted: {deleted}", ['performance']) diff --git a/modules/ui_gallery.py b/modules/ui_gallery.py index 3b0363359..e6dd3757e 100644 --- a/modules/ui_gallery.py +++ b/modules/ui_gallery.py @@ -6,6 +6,7 @@ from PIL import Image from modules import shared, ui_symbols, ui_common, images, video from modules.ui_components import ToolButton + def read_media(fn): fn = unquote(fn).replace('%3A', ':') if not os.path.isfile(fn): From 8aff68fe065839dbc7e551bb2ddb1546b6479526 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 4 Aug 2025 14:19:33 -0400 Subject: [PATCH 015/141] new models tab including civitai downloader Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 16 +- extensions-builtin/sdnext-modernui | 2 +- javascript/civitai.js | 329 ++++++++-------------------- javascript/extraNetworks.js | 4 +- javascript/sdnext.css | 69 ++++-- modules/civitai/download_civitai.py | 191 ++++++++++++++++ modules/civitai/metadata_civitai.py | 70 +----- modules/modelloader.py | 183 +--------------- modules/sd_checkpoint.py | 3 +- modules/shared.py | 36 ++- modules/ui_models.py | 89 +++----- 11 files changed, 415 insertions(+), 577 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf6dd4465..f790956f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log for SD.Next -## Update for 2025-08-02 +## Update for 2025-08-04 - **Models** - [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) @@ -14,15 +14,21 @@ - new embedded docs/wiki search! **Docs** search: fully-local and works in real-time on all document pages **Wiki** search: uses github api to search online wiki pages - - quicksettings reset button to restore all quicksettings to default values - because things do sometimes get wrong... - updated real-time hints, thanks @CalamitousFelicitousness - - updated *models -> current* tab + - rewritten **CivitAI downloader** + in *models -> civitai* + - updated *models -> current* tab - updated *models -> list models* tab - updated *models -> metadata* tab - - more css optimizations and styling + - quicksettings reset button to restore all quicksettings to default values + because things do sometimes get wrong... + - redesign *settings -> user interface* - gallery bypass browser cache for thumbnails - gallery safer delete operation + - more css optimizations and styling + - *hint*: card layout + card layout is used by networks, gallery, civitai search, etc. + you can change card size in *settings -> user interface* - **Offloading** - changed **default** values for offloading based on detected gpu memory see [offloading docs](https://vladmandic.github.io/sdnext-docs/Offload/) for details diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 540353613..89c232814 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 540353613400c419f8f0d92d6569e1e695e04f03 +Subproject commit 89c232814a3a65f00783af962975ffb31ce336db diff --git a/javascript/civitai.js b/javascript/civitai.js index e61980770..417c08584 100644 --- a/javascript/civitai.js +++ b/javascript/civitai.js @@ -1,31 +1,76 @@ -// hack to get pythons str.format in js -String.prototype.format = function (arguments) { // eslint-disable-line no-extend-native, func-names +String.prototype.format = function (args) { // eslint-disable-line no-extend-native, func-names let thisString = ''; for (let charPos = 0; charPos < this.length; charPos++) thisString += this[charPos]; - for (const key in arguments) { // eslint-disable-line guard-for-in - error(key, arguments[key]); + for (const key in args) { // eslint-disable-line guard-for-in const stringKey = `{${key}}`; - thisString = thisString.replace(new RegExp(stringKey, 'g'), arguments[key]); + thisString = thisString.replace(new RegExp(stringKey, 'g'), args[key]); } return thisString; }; +let selectedURL = ''; +let selectedName = ''; +let selectedType = ''; + +function clearModelDetails() { + const el = gradioApp().getElementById('model-details') || gradioApp().getElementById('civitai_models_output') || gradioApp().getElementById('models_outcome'); + if (!el) return; + el.innerHTML = ''; +} + const modelDetailsHTML = ` -
-

{name}

-

Type: {type}

-

Tags: {tags}

-

NSFW: {nsfw}/{level}

-

Availability: {availability}

-

Downloads: {downloads}

-

Author: {creator}

-
{versions}
+
+ + +
+ + + + + + + + +
Name{name}
Type{type}
Tags
{tags}
NSFW{nsfw} | {level}
Availability{availability}
Downloads{downloads}
Author{creator}
Description
{desc}
+
+ + + + + + + + + + + + + + + + {versions} + +
VersionTypeBaseFileUpdatedSizeAvailabilityDescription
`; +const modelVersionsHTML = ` + + {url} + {name} + {type} + {base} + {file} + {mtime} + {size} + {availability} +
{desc}
+ +`; + async function modelCardClick(id) { log('modelCardClick id', id); - const el = gradioApp().getElementById('model-details'); + const el = gradioApp().getElementById('model-details') || gradioApp().getElementById('civitai_models_output') || gradioApp().getElementById('models_outcome'); if (!el) return; const res = await fetch(`${window.api}/civitai?model_id=${encodeURI(id)}`); if (!res || res.status !== 200) { @@ -36,229 +81,49 @@ async function modelCardClick(id) { log('modelCardClick data', data); if (!data || data.length === 0) return; data = data[0]; // assuming the first item is the one we want - const obj = { - name: data.name || 'unknown', + + const versionsHTML = data.versions.map((v) => modelVersionsHTML.format({ + url: ``, + name: v.name || 'unknown', + type: v.files[0]?.type || 'unknown', + base: v.base || 'unknown', + mtime: (new Date(v.mtime)).toLocaleDateString(), + availability: v.availability || 'unknown', + size: v.files[0]?.size ? `${(v.files[0].size / 1024 / 1024).toFixed(2)} MB` : 'unknown', + file: `${v.files[0]?.name || 'unknown'}`, + desc: v.desc || 'no description available', + })).join(''); + const url = `${data.name || 'unknown'}`; + const creator = `${data.creator || 'unknown'}`; + const images = data.versions.map((v) => v.images).flat().map((i) => i.url); // TODO image gallery + const modelHTML = modelDetailsHTML.format({ + name: url, type: data.type || 'unknown', tags: data.tags?.join(', ') || '', nsfw: data.nsfw ? 'yes' : 'no', level: data.level?.toString() || '', availability: data.availability || 'unknown', downloads: data.downloads?.toString() || '', - creator: data.creator || 'unknown', - versions: JSON.stringify(data.versions) || '[]', - }; - log(obj); - el.innerHTML = modelDetailsHTML.format({ - name: data.name || 'unknown', - type: data.type || 'unknown', - tags: data.tags?.join(', ') || '', - nsfw: data.nsfw ? 'yes' : 'no', - level: data.level?.toString() || '', - availability: data.availability || 'unknown', - downloads: data.downloads?.toString() || '', - creator: data.creator || 'unknown', - versions: JSON.stringify(data.versions) || '[]', + creator, + desc: data.desc || 'no description available', + image: images.length > 0 ? images[0] : './sd_extra_networks/thumb?filename=html/card-no-preview.png', + versions: versionsHTML || '', }); + el.innerHTML = modelHTML; } -const example = { - id: 1157409, - url: 'https://civitai.com/models/1157409', - type: 'Checkpoint', - name: 'Tempest-by-Vlad', - html: '', - desc: 'Base versionFlexible SDXL model with custom encoder and finetuned for larger landscape resolutions with high details and high contrast.Recommended to use medium-low...', - tags: [ - 'base model', - ], - nsfw: false, - level: 15, - availability: 'Public', - downloads: 407, - creator: 'vmandic', - versions: [ - { - id: 1301775, - name: 'Base v0.1', - base: 'SDXL 1.0', - mtime: '2025-01-19T02:53:53.903Z', - downloads: 346, - availability: 'Public', - html: '', - desc: 'Initial release', - files: [ - { - id: 1206102, - size: 6938089790, - name: 'tempestByVlad_baseV01.safetensors', - type: 'Model', - hashes: [ - '79CB1E32', - '8BFAD17222', - '8BFAD1722243955B3F94103C69079C280D348B14729251E86824972C1063B616', - '43E5E3BB', - 'DE83D56256411853AB6595CC3D8E865D5310D4A58D49A839DDC104C7F3429D4A', - '4E933E1EBE61', - ], - url: 'https://civitai.com/api/download/models/1301775', - dct: {}, - }, - ], - images: [ - { - id: 52503951, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/18c749f2-42ec-4024-9d20-0b1202b6bacc/width=1024/52503951.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508539, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/634d0ea8-ecdb-4ca6-a4ff-145319bc3fd3/width=1024/52508539.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508563, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/be529820-a89e-458f-8a3d-86cb43b154ac/width=1024/52508563.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508588, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/b2eb456a-a664-4de8-8c3e-6ecd1c4acb38/width=1024/52508588.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508654, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/f7b09e3f-4a48-459b-9b32-fa207904f74c/width=1024/52508654.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508659, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/fff89f18-628a-43b3-b9f5-44951dc078f7/width=1024/52508659.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508671, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/fdd3f774-ce2e-4ea7-82a0-bdb523fb86f6/width=1024/52508671.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52512251, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/f26971c8-1123-45e3-a85b-2b97c6334b85/width=1024/52512251.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - ], - dct: {}, - }, - { - id: 1343512, - name: 'Hyper v0.1', - base: 'SDXL 1.0', - mtime: '2025-01-28T22:54:12.734Z', - downloads: 61, - availability: 'Public', - html: '', - desc: 'Time-distilled version', - files: [ - { - id: 1246991, - size: 6938085702, - name: 'tempestByVlad_hyperV01.safetensors', - type: 'Model', - hashes: [ - '15943FD9', - '4104FC6601', - '4104FC6601F71C4C7A770AD422483FD700C8ECF72D06FCD8C4E8CD4B2D1C7DBB', - '9F87BCEA', - 'CB52894625E9C13331285E4435799D707C4EAEF464974159C8B4B217EA32298E', - 'A0EE15E503DD', - ], - url: 'https://civitai.com/api/download/models/1343512', - dct: {}, - }, - ], - images: [ - { - id: 54462987, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/1dd020c8-8a9a-4eb3-afec-fe83613217c5/width=1024/54462987.jpeg', - width: 1024, - height: 768, - type: 'image', - dct: {}, - }, - { - id: 54462992, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/89edf233-939f-4b2e-97c8-175498704362/width=1536/54462992.jpeg', - width: 1536, - height: 640, - type: 'image', - dct: {}, - }, - { - id: 54463002, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/87aea8fb-7687-48d0-98e6-27555b2ff87f/width=768/54463002.jpeg', - width: 768, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 54463010, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/f1ee57cc-8920-4b8d-853a-ad1cfc7d9a5a/width=1024/54463010.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 54463011, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dda6411b-fd36-484f-a9f0-0db847463128/width=1024/54463011.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 54463016, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/5f266a8c-f215-4e0f-8a64-aacc97f81d70/width=1024/54463016.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 54463019, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/0ad04bdc-41bd-433c-9c25-f5365fbe082c/width=1024/54463019.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - ], - dct: {}, - }, - ], - dct: {}, -}; +function startCivitDownload(url, name, type) { + log('startCivitDownload', { url, name, type }); + selectedURL = url; + selectedName = name; + selectedType = type; + const civitDownloadBtn = gradioApp().getElementById('civitai_download_btn'); + if (civitDownloadBtn) civitDownloadBtn.click(); +} + +function downloadCivitModel(modelUrl, modelName, modelType, modelPath, civitToken, innerHTML) { + log('downloadCivitModel', { modelUrl, modelName, modelType, modelPath, civitToken }); + const el = gradioApp().getElementById('civitai_models_output') || gradioApp().getElementById('models_outcome'); + const currentHTML = el?.innerHTML || ''; + return [selectedURL, selectedName, selectedType, modelPath, civitToken, currentHTML]; +} diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 1a9ae4fe6..a93cff798 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -110,8 +110,8 @@ function readCardDescription(page, item) { function getCardsForActivePage() { const pagename = getENActivePage(); if (!pagename) return []; - const allCards = Array.from(gradioApp().querySelectorAll('.extra-network-cards > .card')); - const cards = allCards.filter((el) => el.dataset.page.toLowerCase().includes(pagename.toLowerCase())); + let allCards = Array.from(gradioApp().querySelectorAll('.extra-network-cards > .card')); + allCards = allCards.filter((el) => el.dataset.page?.toLowerCase().includes(pagename.toLowerCase())); // log('getCardsForActivePage', pagename, cards.length); return allCards; } diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 2013cabdb..7c3874a30 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -66,6 +66,10 @@ button { min-width: unset !important; } +h4 { + margin: 0.2em 0em 0.2em 0em; +} + input[type='color'] { height: 32px; width: 64px; @@ -122,6 +126,17 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { overflow: auto; } +.link { + background-color: var(--background-fill-primary); + cursor: pointer; + border-radius: var(--input-radius); + width: 2em; +} + +.link:hover { + background-color: var(--button-primary-background-fill); +} + .gradio-dropdown, .block.gradio-slider, .block.gradio-checkbox, .block.gradio-textbox, .block.gradio-radio, .block.gradio-checkboxgroup, .block.gradio-number, .block.gradio-colorpicker { border-width: 0 !important; box-shadow: none !important; @@ -1226,11 +1241,13 @@ table.settings-value-table td { } .extra-network-cards .card { - height: fit-content; - margin: 0 0 0.5em 0.5em; - position: relative; - scroll-margin-top: 0; - scroll-snap-align: start; + margin: 0 0 0.5em 0.5em; + position: relative; + scroll-margin-top: 0; + scroll-snap-align: start; + height: var(--card-size); + width: var(--card-size); + contain: strict; } .extra-network-cards .card .overlay { @@ -1923,10 +1940,6 @@ div:has(>#tab-gallery-folders) { padding: 0.2em; } -.docs-results { - background-color: var(--sd-group-background-color); -} - .docs-card { margin: 1em 0; background-color: var(--background-fill-primary); @@ -1972,6 +1985,13 @@ div:has(>#tab-gallery-folders) { overflow: auto; } +.model-config { + font-size: 0.8em !important; + opacity: 0.8; + max-height: 6em; + overflow-y: auto; +} + .simple-table tr { vertical-align: baseline; } @@ -1980,13 +2000,36 @@ div:has(>#tab-gallery-folders) { padding: 0.2em !important; } -.model-config { - font-size: 0.8em !important; - opacity: 0.8; - max-height: 6em; +.simple-table tr { + vertical-align: baseline; +} + +.simple-table thead tr { + background-color: var(--button-primary-border-color) !important; +} + +.simple-table tr:nth-child(odd) { + background-color: var(--neutral-900); +} + +.simple-table td { + padding: 0.2em !important; + white-space: pre-wrap; +} + +.simple-table td div { + padding: 0.2em !important; + white-space: pre-wrap; + max-height: 7em; + overflow-x: hidden; overflow-y: auto; } +.simple-table td:nth-child(1) { + color: var(--button-primary-border-color); + font-weight: bold; +} + @keyframes move { from { background-position-x: 0, -40px; diff --git a/modules/civitai/download_civitai.py b/modules/civitai/download_civitai.py index e69de29bb..7e013454c 100644 --- a/modules/civitai/download_civitai.py +++ b/modules/civitai/download_civitai.py @@ -0,0 +1,191 @@ +import os +import json +import rich.progress as p +from PIL import Image +from modules import shared, errors, paths + + +pbar = None + + +def save_video_frame(filepath: str): + from modules import video + try: + frames, fps, duration, w, h, codec, frame = video.get_video_params(filepath, capture=True) + except Exception as e: + shared.log.error(f'Video: file={filepath} {e}') + return None + if frame is not None: + basename = os.path.splitext(filepath) + thumb = f'{basename[0]}.thumb.jpg' + shared.log.debug(f'Video: file={filepath} frames={frames} fps={fps} size={w}x{h} codec={codec} duration={duration} thumb={thumb}') + frame.save(thumb) + else: + shared.log.error(f'Video: file={filepath} no frames found') + return frame + + +def download_civit_meta(model_path: str, model_id): + fn = os.path.splitext(model_path)[0] + '.json' + url = f'https://civitai.com/api/v1/models/{model_id}' + r = shared.req(url) + if r.status_code == 200: + try: + data = r.json() + shared.writefile(data, filename=fn, mode='w', silent=True) + shared.log.info(f'CivitAI download: id={model_id} url={url} file="{fn}"') + return r.status_code, len(data), '' # code/size/note + except Exception as e: + errors.display(e, 'civitai meta') + shared.log.error(f'CivitAI meta: id={model_id} url={url} file="{fn}" {e}') + return r.status_code, '', str(e) + return r.status_code, '', '' + + +def download_civit_preview(model_path: str, preview_url: str): + global pbar # pylint: disable=global-statement + if model_path is None: + pbar = None + return 500, '', '' + ext = os.path.splitext(preview_url)[1] + preview_file = os.path.splitext(model_path)[0] + ext + is_video = preview_file.lower().endswith('.mp4') + is_json = preview_file.lower().endswith('.json') + if is_json: + shared.log.warning(f'CivitAI download: url="{preview_url}" skip json') + return 500, '', 'exepected preview image got json' + if os.path.exists(preview_file): + return 304, '', 'already exists' + # res = f'CivitAI download: url={preview_url} file="{preview_file}"' + r = shared.req(preview_url, stream=True) + total_size = int(r.headers.get('content-length', 0)) + block_size = 16384 # 16KB blocks + written = 0 + img = None + shared.state.begin('CivitAI') + if pbar is None: + pbar = p.Progress(p.TextColumn('[cyan]Download'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), p.TextColumn('[yellow]{task.description}'), console=shared.console) + try: + with open(preview_file, 'wb') as f: + with pbar: + task = pbar.add_task(description=preview_file, total=total_size) + for data in r.iter_content(block_size): + written = written + len(data) + f.write(data) + pbar.update(task, advance=block_size) + if written < 1024: # min threshold + os.remove(preview_file) + return 400, '', 'removed invalid download' + if is_video: + img = save_video_frame(preview_file) + else: + img = Image.open(preview_file) + except Exception as e: + shared.log.error(f'CivitAI download error: url={preview_url} file="{preview_file}" written={written} {e}') + return 500, '', str(e) + shared.state.end() + if img is None: + return 500, '', 'image is none' + shared.log.info(f'CivitAI download: url={preview_url} file="{preview_file}" size={total_size} image={img.size}') + img.close() + return 200, str(total_size), '' # code/size/note + + +def download_civit_model_thread(model_name: str, model_url: str, model_path: str = "", model_type: str = "Model", token: str = None): + import hashlib + sha256 = hashlib.sha256() + sha256.update(model_url.encode('utf-8')) + temp_file = sha256.hexdigest()[:8] + '.tmp' + + headers = {} + starting_pos = 0 + if os.path.isfile(temp_file): + starting_pos = os.path.getsize(temp_file) + headers['Range'] = f'bytes={starting_pos}-' + if token is None or len(token) == 0: + token = shared.opts.civitai_token + if token is not None and len(token) > 0: + headers['Authorization'] = f'Bearer {token}' + + r = shared.req(model_url, headers=headers, stream=True) + total_size = int(r.headers.get('content-length', 0)) + if model_name is None or len(model_name) == 0: + cn = r.headers.get('content-disposition', '') + model_name = cn.split('filename=')[-1].strip('"') + + model_path = model_path.strip() + if len(model_path) > 0: + if os.path.isabs(model_path): + pass + else: + model_path = os.path.join(paths.models_path, model_path) + elif model_type.lower() == 'lora': + model_path = shared.opts.lora_dir + elif model_type.lower() == 'embedding': + model_path = shared.opts.embeddings_dir + elif model_type.lower() == 'vae': + model_path = shared.opts.vae_dir + else: + model_path = shared.opts.ckpt_dir + model_file = os.path.join(model_path, model_name) + temp_file = os.path.join(model_path, temp_file) + + res = f'Model download: name="{model_name}" url="{model_url}" path="{model_path}" temp="{temp_file}"' + if os.path.isfile(model_file): + res += ' already exists' + shared.log.warning(res) + return res + + res += f' size={round((starting_pos + total_size)/1024/1024, 2)}Mb' + shared.log.info(res) + shared.state.begin('CivitAI') + block_size = 16384 # 16KB blocks + written = starting_pos + global pbar # pylint: disable=global-statement + if pbar is None: + pbar = p.Progress(p.TextColumn('[cyan]{task.description}'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), p.TextColumn('[cyan]{task.fields[name]}'), console=shared.console) + with pbar: + task = pbar.add_task(description="Download starting", total=starting_pos+total_size, name=model_name) + try: + with open(temp_file, 'ab') as f: + for data in r.iter_content(block_size): + if written == 0: + try: # check if response is JSON message instead of bytes + shared.log.error(f'Model download: response={json.loads(data.decode("utf-8"))}') + raise ValueError('response: type=json expected=bytes') + except Exception: # this is good + pass + written = written + len(data) + f.write(data) + pbar.update(task, description="Download", completed=written) + if written < 1024: # min threshold + os.remove(temp_file) + raise ValueError(f'removed invalid download: bytes={written}') + except Exception as e: + shared.log.error(f'{res} {e}') + finally: + pbar.stop_task(task) + pbar.remove_task(task) + if starting_pos+total_size != written: + shared.log.warning(f'{res} written={round(written/1024/1024)}Mb incomplete download') + elif os.path.exists(temp_file): + shared.log.debug(f'Model download complete: temp="{temp_file}" path="{model_file}"') + os.rename(temp_file, model_file) + shared.state.end() + if os.path.exists(model_file): + return model_file + else: + return None + + +def download_civit_model(model_url: str, model_name: str = '', model_path: str = '', model_type: str = '', token: str = None): + import threading + if model_url is None or len(model_url) == 0: + err = 'Model download: no url provided' + shared.log.error(err) + return err + thread = threading.Thread(target=download_civit_model_thread, args=(model_name, model_url, model_path, model_type, token)) + thread.start() + thread.join() + from modules.sd_models import list_models # pylint: disable=W0621 + list_models() diff --git a/modules/civitai/metadata_civitai.py b/modules/civitai/metadata_civitai.py index 8bc2d47b9..3d40296b5 100644 --- a/modules/civitai/metadata_civitai.py +++ b/modules/civitai/metadata_civitai.py @@ -1,7 +1,6 @@ import os import re import time -import json import gradio as gr from modules.shared import log, opts, req, readfile, max_workers @@ -58,7 +57,8 @@ def civit_update_metadata(): return html.format(tbody=tbody) log.debug('CivitAI update metadata: models') - from modules import ui_extra_networks, modelloader + from modules import ui_extra_networks + from modules.civitai.download_civitai import download_civit_meta pages = ui_extra_networks.get_pages('Model') if len(pages) == 0: return 'CivitAI update metadata: no models found' @@ -75,7 +75,7 @@ def civit_update_metadata(): if r.status_code == 200: d = r.json() model.id = d['modelId'] - modelloader.download_civit_meta(model.fn, model.id) + download_civit_meta(model.fn, model.id) fn = os.path.splitext(item['filename'])[0] + '.json' model.meta = readfile(fn, silent=True) model.name = model.meta.get('name', model.name) @@ -158,64 +158,8 @@ def civit_search_model(name, tag, model_type): return res, gr.update(visible=len(data1) > 0, value=data1 if len(data1) > 0 else []), gr.update(visible=False, value=None), gr.update(visible=False, value=None) -def civit_select1(evt: gr.SelectData, in_data): - model_id = in_data[evt.index[0]][0] - data2 = [] - preview_img = None - for model in data: - if model['id'] == model_id: - for d in model['modelVersions']: - try: - if d.get('images') is not None and len(d['images']) > 0 and len(d['images'][0]['url']) > 0: - preview_img = d['images'][0]['url'] - data2.append([d.get('id', None), d.get('modelId', None) or model_id, d.get('name', None), d.get('baseModel', None), d.get('createdAt', None) or d.get('publishedAt', None)]) - except Exception as e: - log.error(f'CivitAI select: model="{in_data[evt.index[0]]}" {e}') - log.error(f'CivitAI version data={type(d)}: {d}') - log.debug(f'CivitAI select: model="{in_data[evt.index[0]]}" versions={len(data2)}') - return data2, None, preview_img - - -def civit_select2(evt: gr.SelectData, in_data): - variant_id = in_data[evt.index[0]][0] - model_id = in_data[evt.index[0]][1] - data3 = [] - for model in data: - if model['id'] == model_id: - for variant in model['modelVersions']: - if variant['id'] == variant_id: - for f in variant['files']: - try: - if os.path.splitext(f['name'])[1].lower() in ['.safetensors', '.ckpt', '.pt', '.pth', '.bin']: - data3.append([f['name'], round(f['sizeKB']), json.dumps(f['metadata']), f['downloadUrl']]) - except Exception: - pass - log.debug(f'CivitAI select: model="{in_data[evt.index[0]]}" files={len(data3)}') - return data3 - - -def civit_select3(evt: gr.SelectData, in_data): - log.debug(f'CivitAI select: variant={in_data[evt.index[0]]}') - return in_data[evt.index[0]][3], in_data[evt.index[0]][0], gr.update(interactive=True) - - -def civit_download_model(model_url: str, model_name: str, model_path: str, model_type: str, token: str = None): - if model_url is None or len(model_url) == 0: - return 'No model selected' - try: - from modules.modelloader import download_civit_model - res = download_civit_model(model_url, model_name, model_path, model_type, token=token) - except Exception as e: - res = f"CivitAI model downloaded error: model={model_url} {e}" - log.error(res) - return res - from modules.sd_models import list_models # pylint: disable=W0621 - list_models() - return res - - def atomic_civit_search_metadata(item, results): - from modules.modelloader import download_civit_preview, download_civit_meta + from modules.civitai.download_civitai import download_civit_preview, download_civit_meta if item is None: return try: @@ -344,9 +288,3 @@ def civit_search_metadata(title: str = None): t1 = time.time() log.debug(f'CivitAI search metadata: scanned={scanned} skipped={skipped} time={t1-t0:.2f}') return create_search_metadata_table(results) - - -def civitai_update_token(token): - log.debug('CivitAI update token') - opts.civitai_token = token - opts.save() diff --git a/modules/modelloader.py b/modules/modelloader.py index 9813b5ee5..c4a1d3e00 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -1,14 +1,11 @@ import io import os import time -import json import shutil import importlib import contextlib from typing import Dict from urllib.parse import urlparse -from PIL import Image -import rich.progress as p import huggingface_hub as hf from installer import install, log from modules import shared, errors, files_cache @@ -48,185 +45,6 @@ def hf_login(token=None): return True -def save_video_frame(filepath: str): - from modules import video - try: - frames, fps, duration, w, h, codec, frame = video.get_video_params(filepath, capture=True) - except Exception as e: - shared.log.error(f'Video: file={filepath} {e}') - return None - if frame is not None: - basename = os.path.splitext(filepath) - thumb = f'{basename[0]}.thumb.jpg' - shared.log.debug(f'Video: file={filepath} frames={frames} fps={fps} size={w}x{h} codec={codec} duration={duration} thumb={thumb}') - frame.save(thumb) - else: - shared.log.error(f'Video: file={filepath} no frames found') - return frame - - -def download_civit_meta(model_path: str, model_id): - fn = os.path.splitext(model_path)[0] + '.json' - url = f'https://civitai.com/api/v1/models/{model_id}' - r = shared.req(url) - if r.status_code == 200: - try: - data = r.json() - shared.writefile(data, filename=fn, mode='w', silent=True) - shared.log.info(f'CivitAI download: id={model_id} url={url} file="{fn}"') - return r.status_code, len(data), '' # code/size/note - except Exception as e: - errors.display(e, 'civitai meta') - shared.log.error(f'CivitAI meta: id={model_id} url={url} file="{fn}" {e}') - return r.status_code, '', str(e) - return r.status_code, '', '' - - -def download_civit_preview(model_path: str, preview_url: str): - global pbar # pylint: disable=global-statement - if model_path is None: - pbar = None - return 500, '', '' - ext = os.path.splitext(preview_url)[1] - preview_file = os.path.splitext(model_path)[0] + ext - is_video = preview_file.lower().endswith('.mp4') - is_json = preview_file.lower().endswith('.json') - if is_json: - shared.log.warning(f'CivitAI download: url="{preview_url}" skip json') - return 500, '', 'exepected preview image got json' - if os.path.exists(preview_file): - return 304, '', 'already exists' - # res = f'CivitAI download: url={preview_url} file="{preview_file}"' - r = shared.req(preview_url, stream=True) - total_size = int(r.headers.get('content-length', 0)) - block_size = 16384 # 16KB blocks - written = 0 - img = None - shared.state.begin('CivitAI') - if pbar is None: - pbar = p.Progress(p.TextColumn('[cyan]Download'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), p.TextColumn('[yellow]{task.description}'), console=shared.console) - try: - with open(preview_file, 'wb') as f: - with pbar: - task = pbar.add_task(description=preview_file, total=total_size) - for data in r.iter_content(block_size): - written = written + len(data) - f.write(data) - pbar.update(task, advance=block_size) - if written < 1024: # min threshold - os.remove(preview_file) - return 400, '', 'removed invalid download' - if is_video: - img = save_video_frame(preview_file) - else: - img = Image.open(preview_file) - except Exception as e: - shared.log.error(f'CivitAI download error: url={preview_url} file="{preview_file}" written={written} {e}') - return 500, '', str(e) - shared.state.end() - if img is None: - return 500, '', 'image is none' - shared.log.info(f'CivitAI download: url={preview_url} file="{preview_file}" size={total_size} image={img.size}') - img.close() - return 200, str(total_size), '' # code/size/note - - -download_pbar = None - -def download_civit_model_thread(model_name: str, model_url: str, model_path: str = "", model_type: str = "Model", token: str = None): - import hashlib - sha256 = hashlib.sha256() - sha256.update(model_url.encode('utf-8')) - temp_file = sha256.hexdigest()[:8] + '.tmp' - - headers = {} - starting_pos = 0 - if os.path.isfile(temp_file): - starting_pos = os.path.getsize(temp_file) - headers['Range'] = f'bytes={starting_pos}-' - if token is None: - token = shared.opts.civitai_token - if token is not None and len(token) > 0: - headers['Authorization'] = f'Bearer {token}' - - r = shared.req(model_url, headers=headers, stream=True) - total_size = int(r.headers.get('content-length', 0)) - if model_name is None or len(model_name) == 0: - cn = r.headers.get('content-disposition', '') - model_name = cn.split('filename=')[-1].strip('"') - - if model_type == 'LoRA': - model_file = os.path.join(shared.opts.lora_dir, model_path, model_name) - temp_file = os.path.join(shared.opts.lora_dir, model_path, temp_file) - elif model_type == 'Embedding': - model_file = os.path.join(shared.opts.embeddings_dir, model_path, model_name) - temp_file = os.path.join(shared.opts.embeddings_dir, model_path, temp_file) - elif model_type == 'VAE': - model_file = os.path.join(shared.opts.vae_dir, model_path, model_name) - temp_file = os.path.join(shared.opts.vae_dir, model_path, temp_file) - else: - model_file = os.path.join(shared.opts.ckpt_dir, model_path, model_name) - temp_file = os.path.join(shared.opts.ckpt_dir, model_path, temp_file) - - res = f'Model download: name="{model_name}" url="{model_url}" path="{model_path}" temp="{temp_file}"' - if os.path.isfile(model_file): - res += ' already exists' - shared.log.warning(res) - return res - - res += f' size={round((starting_pos + total_size)/1024/1024, 2)}Mb' - shared.log.info(res) - shared.state.begin('CivitAI') - block_size = 16384 # 16KB blocks - written = starting_pos - global download_pbar # pylint: disable=global-statement - if download_pbar is None: - download_pbar = p.Progress(p.TextColumn('[cyan]{task.description}'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), p.TextColumn('[cyan]{task.fields[name]}'), console=shared.console) - with download_pbar: - task = download_pbar.add_task(description="Download starting", total=starting_pos+total_size, name=model_name) - try: - with open(temp_file, 'ab') as f: - for data in r.iter_content(block_size): - if written == 0: - try: # check if response is JSON message instead of bytes - shared.log.error(f'Model download: response={json.loads(data.decode("utf-8"))}') - raise ValueError('response: type=json expected=bytes') - except Exception: # this is good - pass - written = written + len(data) - f.write(data) - download_pbar.update(task, description="Download", completed=written) - if written < 1024: # min threshold - os.remove(temp_file) - raise ValueError(f'removed invalid download: bytes={written}') - except Exception as e: - shared.log.error(f'{res} {e}') - finally: - download_pbar.stop_task(task) - download_pbar.remove_task(task) - if starting_pos+total_size != written: - shared.log.warning(f'{res} written={round(written/1024/1024)}Mb incomplete download') - elif os.path.exists(temp_file): - shared.log.debug(f'Model download complete: temp="{temp_file}" path="{model_file}"') - os.rename(temp_file, model_file) - shared.state.end() - if os.path.exists(model_file): - return model_file - else: - return None - - -def download_civit_model(model_url: str, model_name: str, model_path: str, model_type: str, token: str = None): - import threading - if model_name is None or len(model_name) == 0: - err = 'Model download: no target model name provided' - shared.log.error(err) - return err - thread = threading.Thread(target=download_civit_model_thread, args=(model_name, model_url, model_path, model_type, token)) - thread.start() - return f'Model download: name={model_name} url={model_url} path={model_path}' - - def download_diffusers_model(hub_id: str, cache_dir: str = None, download_config: Dict[str, str] = None, token = None, variant = None, revision = None, mirror = None, custom_pipeline = None): if hub_id is None or len(hub_id) == 0: return None @@ -430,6 +248,7 @@ def load_civitai(model: str, url: str): return name # already downloaded else: shared.log.debug(f'Reference download start: model="{name}"') + from modules.civitai.download_civitai import download_civit_model_thread download_civit_model_thread(model_name=model, model_url=url, model_path='', model_type='safetensors', token=shared.opts.civitai_token) shared.log.debug(f'Reference download complete: model="{name}"') sd_models.list_models() diff --git a/modules/sd_checkpoint.py b/modules/sd_checkpoint.py index 1f193f603..62f464946 100644 --- a/modules/sd_checkpoint.py +++ b/modules/sd_checkpoint.py @@ -244,7 +244,8 @@ def get_closet_checkpoint_match(s: str) -> CheckpointInfo: # civitai search if shared.opts.sd_checkpoint_autodownload and s.startswith("https://civitai.com/api/download/models"): - fn = modelloader.download_civit_model_thread(model_name=None, model_url=s, model_path='', model_type='Model', token=None) + from modules.civitai.download_civitai import download_civit_model_thread + fn = download_civit_model_thread(model_name=None, model_url=s, model_path='', model_type='Model', token=None) if fn is not None: checkpoint_info = CheckpointInfo(fn) return checkpoint_info diff --git a/modules/shared.py b/modules/shared.py index 6c971f4eb..7df525310 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -514,26 +514,43 @@ options_templates.update(options_section(('image-metadata', "Image Metadata"), { })) options_templates.update(options_section(('ui', "User Interface"), { + "themes_sep_ui": OptionInfo("

Theme options

", "", gr.HTML), "theme_type": OptionInfo("Standard", "Theme type", gr.Radio, {"choices": ["Modern", "Standard", "None"]}), "theme_style": OptionInfo("Auto", "Theme mode", gr.Radio, {"choices": ["Auto", "Dark", "Light"]}), "gradio_theme": OptionInfo("black-teal", "UI theme", gr.Dropdown, lambda: {"choices": theme.list_themes()}, refresh=theme.refresh_themes), - "ui_locale": OptionInfo("Auto", "UI locale", gr.Dropdown, lambda: {"choices": theme.list_locales()}), - "subpath": OptionInfo("", "Mount URL subpath"), + + "quicksetting_sep_images": OptionInfo("

Quicksettings

", "", gr.HTML), + "quicksettings_list": OptionInfo(["sd_model_checkpoint"], "Quicksettings list", gr.Dropdown, lambda: {"multiselect":True, "choices": opts.list()}), + + "server_sep_ui": OptionInfo("

Startup & Server Options

", "", gr.HTML), "autolaunch": OptionInfo(False, "Autolaunch browser upon startup"), + "motd": OptionInfo(False, "Show MOTD"), + "subpath": OptionInfo("", "Mount URL subpath"), + "ui_request_timeout": OptionInfo(30000, "UI request timeout", gr.Slider, {"minimum": 1000, "maximum": 120000, "step": 10}), + + "cards_sep_ui": OptionInfo("

Card options

", "", gr.HTML), + "extra_networks_card_size": OptionInfo(140, "UI card size (px)", gr.Slider, {"minimum": 20, "maximum": 2000, "step": 1}), + "extra_networks_card_cover": OptionInfo("sidebar", "UI position", gr.Radio, {"choices": ["cover", "inline", "sidebar"]}), + "extra_networks_card_square": OptionInfo(True, "UI disable variable aspect ratio"), + + "other_sep_ui": OptionInfo("

Other...

", "", gr.HTML), + "ui_locale": OptionInfo("Auto", "UI locale", gr.Dropdown, lambda: {"choices": theme.list_locales()}), "font_size": OptionInfo(14, "Font size", gr.Slider, {"minimum": 8, "maximum": 32, "step": 1}), "aspect_ratios": OptionInfo("1:1, 4:3, 3:2, 16:9, 16:10, 21:9, 2:3, 3:4, 9:16, 10:16, 9:21", "Allowed aspect ratios"), - "logmonitor_show": OptionInfo(True, "Show log view"), - "logmonitor_refresh_period": OptionInfo(5000, "Log view update period", gr.Slider, {"minimum": 0, "maximum": 30000, "step": 25}), - "ui_request_timeout": OptionInfo(30000, "UI request timeout", gr.Slider, {"minimum": 1000, "maximum": 120000, "step": 10}), - "motd": OptionInfo(False, "Show MOTD"), "compact_view": OptionInfo(False, "Compact view"), "ui_columns": OptionInfo(4, "Gallery view columns", gr.Slider, {"minimum": 1, "maximum": 8, "step": 1}), + + "images_sep_log": OptionInfo("

Log Display

", "", gr.HTML), + "logmonitor_show": OptionInfo(True, "Show log view"), + "logmonitor_refresh_period": OptionInfo(5000, "Log view update period", gr.Slider, {"minimum": 0, "maximum": 30000, "step": 25}), + + "images_sep_ui": OptionInfo("

Outputs & Images

", "", gr.HTML), "return_grid": OptionInfo(True, "Show grid in results"), "return_mask": OptionInfo(False, "Inpainting include greyscale mask in results"), "return_mask_composite": OptionInfo(False, "Inpainting include masked composite in results"), "send_seed": OptionInfo(True, "Send seed when sending prompt or image to other interface", gr.Checkbox, {"visible": False}), "send_size": OptionInfo(False, "Send size when sending prompt or image to another interface", gr.Checkbox, {"visible": False}), - "quicksettings_list": OptionInfo(["sd_model_checkpoint"], "Quicksettings list", gr.Dropdown, lambda: {"multiselect":True, "choices": opts.list()}), + })) options_templates.update(options_section(('live-preview', "Live Previews"), { @@ -642,11 +659,8 @@ options_templates.update(options_section(('extra_networks', "Networks"), { "extra_networks": OptionInfo(["All"], "Available networks", gr.Dropdown, lambda: {"multiselect":True, "choices": ['All'] + [en.title for en in extra_networks]}), "extra_networks_sort": OptionInfo("Default", "Sort order", gr.Dropdown, {"choices": ['Default', 'Name [A-Z]', 'Name [Z-A]', 'Date [Newest]', 'Date [Oldest]', 'Size [Largest]', 'Size [Smallest]']}), "extra_networks_view": OptionInfo("gallery", "UI view", gr.Radio, {"choices": ["gallery", "list"]}), - "extra_networks_card_cover": OptionInfo("sidebar", "UI position", gr.Radio, {"choices": ["cover", "inline", "sidebar"]}), - "extra_networks_height": OptionInfo(0, "UI height (%)", gr.Slider, {"minimum": 0, "maximum": 100, "step": 1}), # set in ui_javascript "extra_networks_sidebar_width": OptionInfo(35, "UI sidebar width (%)", gr.Slider, {"minimum": 10, "maximum": 80, "step": 1}), - "extra_networks_card_size": OptionInfo(140, "UI card size (px)", gr.Slider, {"minimum": 20, "maximum": 2000, "step": 1}), - "extra_networks_card_square": OptionInfo(True, "UI disable variable aspect ratio"), + "extra_networks_height": OptionInfo(0, "UI height (%)", gr.Slider, {"minimum": 0, "maximum": 100, "step": 1}), # set in ui_javascript "extra_networks_fetch": OptionInfo(True, "UI fetch network info on mouse-over"), "extra_network_skip_indexing": OptionInfo(False, "Build info on first access", gr.Checkbox), diff --git a/modules/ui_models.py b/modules/ui_models.py index d008d80fb..f6f2aa257 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -475,13 +475,28 @@ def create_ui(): html = create_model_cards(results) return html + def civitai_update_token(token): + log.debug('CivitAI update token') + opts.civitai_token = token + opts.save() + + def civitai_download(model_url, model_name, model_type, model_path, civit_token, model_output): + from modules.civitai.download_civitai import download_civit_model + msg = f"

Initiating download

{model_name} | {model_type} | {model_url}

" + yield msg + model_output + download_civit_model(model_url, model_name, model_path, model_type, civit_token) + yield model_output + with gr.Row(): gr.HTML('

Search & Download

') with gr.Row(elem_id='civitai_search_row'): civit_search_text = gr.Textbox(label='', placeholder='keyword', elem_id="civit_search_text") civit_search_tag = gr.Textbox(label='', placeholder='tag', elem_id="civit_search_text") civit_search_text_btn = ToolButton(value=ui_symbols.search, interactive=True) - with gr.Accordion(label='Search options', open=False, elem_id="civitai_search_options"): + with gr.Accordion(label='Advanced', open=False, elem_id="civitai_search_options"): + civit_download_btn = gr.Button(value="Download model", variant='primary', elem_id="civitai_download_btn", visible=False) + with gr.Row(): + civit_token = gr.Textbox(opts.civitai_token, label='CivitAI token', placeholder='optional access token for private or gated models') with gr.Row(): civit_nsfw = gr.Checkbox(label='NSFW allowed', value=True) with gr.Row(): @@ -489,71 +504,17 @@ def create_ui(): with gr.Row(): civit_base = gr.Textbox(label='Base model', placeholder='SDXL, ...') with gr.Row(): - civit_token = gr.Textbox(opts.civitai_token, label='CivitAI token', placeholder='optional access token for private or gated models') + civit_folder = gr.Textbox(label='Download folder', placeholder='optional folder for downloads') + with gr.Row(): + civitai_models_output = gr.HTML('', elem_id="civitai_models_output") # sort, period, limit + _dummy = gr.Label(visible=False) # dummy component to get argspec later civit_inputs = [civit_search_text, civit_search_tag, civit_nsfw, civit_type, civit_base, civit_token] - civit_search_text_btn.click(fn=civitai_search, inputs=civit_inputs, outputs=[models_outcome]) - civit_search_text.submit(fn=civitai_search, inputs=civit_inputs, outputs=[models_outcome]) - civit_search_tag.submit(fn=civitai_search, inputs=civit_inputs, outputs=[models_outcome]) - - """ - from modules.civitai.legacy_civitai import civitai_update_token, civit_search_model, civit_search_metadata, civit_select1, civit_select2, civit_select3, civit_download_model - - with gr.Row(): - gr.HTML('

Search for models

') - with gr.Row(): - with gr.Column(scale=1): - civit_model_type = gr.Dropdown(label='CivitAI model type', choices=['Model', 'LoRA', 'Embedding', 'VAE', 'Other'], value='Model') - with gr.Column(scale=15): - with gr.Row(): - civit_search_text = gr.Textbox('', label='Search models', placeholder='keyword') - civit_search_tag = gr.Textbox('', label='', placeholder='tags') - civit_search_btn = ToolButton(value=ui_symbols.search, interactive=True) - with gr.Row(): - civit_search_res = gr.HTML('') - with gr.Row(): - gr.HTML('

 CivitAI download model

') - with gr.Row(): - civit_download_model_btn = gr.Button(value="Download", variant='primary') - gr.HTML('Select a model, model version and and model variant from the search results to download or enter model URL manually
') - with gr.Row(): - civit_token = gr.Textbox(opts.civitai_token, label='CivitAI token', placeholder='optional access token for private or gated models') - civit_token.change(fn=civitai_update_token, inputs=[civit_token], outputs=[]) - with gr.Row(): - civit_name = gr.Textbox('', label='Model name', placeholder='select model from search results', visible=True) - civit_selected = gr.Textbox('', label='Model URL', placeholder='select model from search results', visible=True) - civit_path = gr.Textbox('', label='Download path', placeholder='optional subfolder path where to save model', visible=True) - with gr.Row(): - gr.HTML('

Search results

') - with gr.Row(): - civit_headers1 = ['ID', 'Name', 'Tags', 'Downloads', 'Rating'] - civit_types1 = ['number', 'str', 'str', 'number', 'number'] - civit_results1 = gr.DataFrame(value=None, label=None, show_label=False, interactive=False, wrap=True, headers=civit_headers1, datatype=civit_types1, type='array', visible=False) - with gr.Row(): - with gr.Column(): - civit_headers2 = ['ID', 'ModelID', 'Name', 'Base', 'Created', 'Preview'] - civit_types2 = ['number', 'number', 'str', 'str', 'date', 'str'] - civit_results2 = gr.DataFrame(value=None, label='Model versions', show_label=True, interactive=False, wrap=True, headers=civit_headers2, datatype=civit_types2, type='array', visible=False) - with gr.Column(): - civit_headers3 = ['Name', 'Size', 'Metadata', 'URL'] - civit_types3 = ['str', 'number', 'str', 'str'] - civit_results3 = gr.DataFrame(value=None, label='Model variants', show_label=True, interactive=False, wrap=True, headers=civit_headers3, datatype=civit_types3, type='array', visible=False) - - def is_visible(component): - visible = len(component) > 0 if component is not None else False - return gr.update(visible=visible) - - civit_search_text.submit(fn=civit_search_model, inputs=[civit_search_text, civit_search_tag, civit_model_type], outputs=[civit_search_res, civit_results1, civit_results2, civit_results3]) - civit_search_tag.submit(fn=civit_search_model, inputs=[civit_search_text, civit_search_tag, civit_model_type], outputs=[civit_search_res, civit_results1, civit_results2, civit_results3]) - civit_search_btn.click(fn=civit_search_model, inputs=[civit_search_text, civit_search_tag, civit_model_type], outputs=[civit_search_res, civit_results1, civit_results2, civit_results3]) - civit_results1.select(fn=civit_select1, inputs=[civit_results1], outputs=[civit_results2, civit_results3, models_image]) - civit_results2.select(fn=civit_select2, inputs=[civit_results2], outputs=[civit_results3]) - civit_results3.select(fn=civit_select3, inputs=[civit_results3], outputs=[civit_selected, civit_name, civit_search_btn]) - civit_results1.change(fn=is_visible, inputs=[civit_results1], outputs=[civit_results1]) - civit_results2.change(fn=is_visible, inputs=[civit_results2], outputs=[civit_results2]) - civit_results3.change(fn=is_visible, inputs=[civit_results3], outputs=[civit_results3]) - civit_download_model_btn.click(fn=civit_download_model, inputs=[civit_selected, civit_name, civit_path, civit_model_type, civit_token], outputs=[models_outcome]) - """ + civit_search_text_btn.click(fn=civitai_search, inputs=civit_inputs, outputs=[civitai_models_output]) + civit_search_text.submit(fn=civitai_search, inputs=civit_inputs, outputs=[civitai_models_output]) + civit_search_tag.submit(fn=civitai_search, inputs=civit_inputs, outputs=[civitai_models_output]) + civit_token.change(fn=civitai_update_token, inputs=[civit_token], outputs=[]) + civit_download_btn.click(fn=civitai_download, _js="downloadCivitModel", inputs=[_dummy, _dummy, _dummy, civit_folder, civit_token, civitai_models_output], outputs=[civitai_models_output]) with gr.Tab(label="Huggingface", elem_id="models_huggingface_tab"): from modules.models_hf import hf_search, hf_select, hf_download_model, hf_update_token From fd842e275a5f3344daabdf0b78cd74b269bca45a Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 4 Aug 2025 14:55:17 -0400 Subject: [PATCH 016/141] add qwen-image Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 8 ++- html/reference.json | 8 +++ installer.py | 2 +- models/Reference/Qwen--Qwen-Image.jpg | Bin 0 -> 59304 bytes modules/sd_detect.py | 2 + modules/sd_models.py | 4 ++ modules/shared_items.py | 1 + modules/ui_extra_networks.py | 2 +- pipelines/model_qwen.py | 67 ++++++++++++++++++++++++++ 9 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 models/Reference/Qwen--Qwen-Image.jpg create mode 100644 pipelines/model_qwen.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f790956f0..ccf8b36c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,17 @@ ## Update for 2025-08-04 - **Models** + - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) + new image foundational model with 20B params and using Qwen-2.5 as text-encoder! + *note*: this model is almost 2x the size of Flux, quantization and offloading are highly recommended! + available via *networks -> models -> reference* - [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) new 12B base model compatible with FLUX.1-Dev from *Black Forest Labs* with opinionated aesthetics and aesthetic preferences in mind - simply select in *networks -> models -> reference* + available via *networks -> models -> reference* - [Chroma](https://huggingface.co/lodestones/Chroma) great model based on FLUX.1 and then redesigned and retrained by *lodestones* update with latest **v48**, **v48 Detail Calibrated** and **v46 Flash** variants - simply select in *networks -> models -> reference* + available via *networks -> models -> reference* - **UI** - new embedded docs/wiki search! **Docs** search: fully-local and works in real-time on all document pages diff --git a/html/reference.json b/html/reference.json index 4f21017ac..e4d8e0d4c 100644 --- a/html/reference.json +++ b/html/reference.json @@ -195,6 +195,14 @@ "extras": "sampler: Default, cfg_scale: 1.0" }, + "Qwen-Image": { + "path": "Qwen/Qwen-Image", + "preview": "Qwen--Qwen-Image.jpg", + "desc": " Qwen-Image, an image generation foundation model in the Qwen series that achieves significant advances in complex text rendering and precise image editing.", + "skip": true, + "extras": "" + }, + "Ostris Flex.2 Preview": { "path": "ostris/Flex.2-preview", "preview": "ostris--Flex.2-preview.jpg", diff --git a/installer.py b/installer.py index a0bbc2828..f5f692666 100644 --- a/installer.py +++ b/installer.py @@ -593,7 +593,7 @@ def check_diffusers(): t_start = time.time() if args.skip_all or args.skip_git: return - sha = '0c71189abeaa8ab4b28dd7e5a309ac75c64968a2' # diffusers commit hash + sha = '7ea065c5070a5278259e6f1effa9dccea232e62a' # diffusers commit hash pkg = pkg_resources.working_set.by_key.get('diffusers', None) minor = int(pkg.version.split('.')[1] if pkg is not None else -1) cur = opts.get('diffusers_version', '') if minor > -1 else '' diff --git a/models/Reference/Qwen--Qwen-Image.jpg b/models/Reference/Qwen--Qwen-Image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b5583883511d290bbecd430441bcea6bf33de4c1 GIT binary patch literal 59304 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
sV@CaN|-p}72zir_zi7;QMOi* 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 sbH*tEQA`4gKuA9AH~38 Date: Mon, 4 Aug 2025 14:58:04 -0400 Subject: [PATCH 017/141] lint Signed-off-by: Vladimir Mandic --- html/reference.json | 2 +- modules/civitai/download_civitai.py | 5 ++--- modules/civitai/search_civitai.py | 2 +- modules/sd_checkpoint.py | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/html/reference.json b/html/reference.json index e4d8e0d4c..853608faa 100644 --- a/html/reference.json +++ b/html/reference.json @@ -202,7 +202,7 @@ "skip": true, "extras": "" }, - + "Ostris Flex.2 Preview": { "path": "ostris/Flex.2-preview", "preview": "ostris--Flex.2-preview.jpg", diff --git a/modules/civitai/download_civitai.py b/modules/civitai/download_civitai.py index 7e013454c..932f8ce7e 100644 --- a/modules/civitai/download_civitai.py +++ b/modules/civitai/download_civitai.py @@ -181,9 +181,8 @@ def download_civit_model_thread(model_name: str, model_url: str, model_path: str def download_civit_model(model_url: str, model_name: str = '', model_path: str = '', model_type: str = '', token: str = None): import threading if model_url is None or len(model_url) == 0: - err = 'Model download: no url provided' - shared.log.error(err) - return err + shared.log.error('Model download: no url provided') + return thread = threading.Thread(target=download_civit_model_thread, args=(model_name, model_url, model_path, model_type, token)) thread.start() thread.join() diff --git a/modules/civitai/search_civitai.py b/modules/civitai/search_civitai.py index 6f7bf8361..bb7601155 100644 --- a/modules/civitai/search_civitai.py +++ b/modules/civitai/search_civitai.py @@ -157,7 +157,7 @@ def search_civitai( model_names = [model.name.lower()] version_names = [v.name.lower() for v in model.versions] file_names = [f.name.lower() for v in model.versions for f in v.files] - if any([query.lower() in name for name in model_names + version_names + file_names]): # noqa: C419 + if any([query.lower() in name for name in model_names + version_names + file_names]): # noqa: C419 # pylint: disable=use-a-generator exact_models.append(model) t1 = time.time() diff --git a/modules/sd_checkpoint.py b/modules/sd_checkpoint.py index 62f464946..0602c6996 100644 --- a/modules/sd_checkpoint.py +++ b/modules/sd_checkpoint.py @@ -189,7 +189,6 @@ def update_model_hashes(): ckpt.shorthash = ckpt.sha256[0:10] if ckpt.sha256 is not None else None updated.append(ckpt) yield update_model_hashes_table(updated) - return def remove_hash(s): From 79db2f28d1086d89da303e4cfb6816f94856b5d1 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 4 Aug 2025 15:18:02 -0400 Subject: [PATCH 018/141] fix nunchaku download links for windows Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 2 ++ modules/mit_nunchaku.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccf8b36c7..4d62db33b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) new image foundational model with 20B params and using Qwen-2.5 as text-encoder! + available for text-to-image workflows, image-editing workflows will follow soon *note*: this model is almost 2x the size of Flux, quantization and offloading are highly recommended! available via *networks -> models -> reference* - [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) @@ -61,6 +62,7 @@ - fix api progress reporting endpoint - fix openvino backend failing to compile - fix nunchaku fallback on unsupported model + - fix nunchaku windows download links - reapply offloading on ipadapter load - api set default script-name - avoid forced gc and rely on thresholds diff --git a/modules/mit_nunchaku.py b/modules/mit_nunchaku.py index 798de661e..b929a1d8b 100644 --- a/modules/mit_nunchaku.py +++ b/modules/mit_nunchaku.py @@ -51,8 +51,9 @@ def install_nunchaku(): suffix = 'x86_64' if arch == 'linux' else 'win_amd64' url = os.environ.get('NUNCHAKU_COMMAND', None) if url is None: + arch = f'{arch}_' if arch == 'linux' else '' url = f'https://huggingface.co/mit-han-lab/nunchaku/resolve/main/nunchaku-{ver}' - url += f'+torch{torch_ver}-cp{python_ver}-cp{python_ver}-{arch}_{suffix}.whl' + url += f'+torch{torch_ver}-cp{python_ver}-cp{python_ver}-{arch}{suffix}.whl' cmd = f'install --upgrade {url}' # pip install https://huggingface.co/mit-han-lab/nunchaku/resolve/main/nunchaku-0.2.0+torch2.6-cp311-cp311-linux_x86_64.whl log.debug(f'Nunchaku: install="{url}"') From 1d37a254952adbfe7188b9f2a218c40b9f2f56f2 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 4 Aug 2025 15:53:38 -0400 Subject: [PATCH 019/141] add note on qwen blocker Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 6 +++++- modules/sd_hijack_te.py | 4 +++- wiki | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d62db33b..9e9c10937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # Change Log for SD.Next +## Blockers + +- Qwen: + ## Update for 2025-08-04 - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) - new image foundational model with 20B params and using Qwen-2.5 as text-encoder! + new image foundational model with 20B params DiT and using Qwen-2.5 8B as text-encoder! available for text-to-image workflows, image-editing workflows will follow soon *note*: this model is almost 2x the size of Flux, quantization and offloading are highly recommended! available via *networks -> models -> reference* diff --git a/modules/sd_hijack_te.py b/modules/sd_hijack_te.py index 2d0f165ff..960d87430 100644 --- a/modules/sd_hijack_te.py +++ b/modules/sd_hijack_te.py @@ -8,10 +8,12 @@ def hijack_encode_prompt(*args, **kwargs): t0 = time.time() if 'max_sequence_length' in kwargs: kwargs['max_sequence_length'] = max(kwargs['max_sequence_length'], os.environ.get('HIDREAM_MAX_SEQUENCE_LENGTH', 256)) + # if hasattr(shared.sd_model, 'text_encoder') and shared.sd_model.text_encoder is not None: + # sd_models.move_model(shared.sd_model.text_encoder, devices.device) try: res = shared.sd_model.orig_encode_prompt(*args, **kwargs) except Exception as e: - shared.log.error(f'Eencode prompt: {e}') + shared.log.error(f'Encode prompt: {e}') errors.display(e, 'Encode prompt') res = None t1 = time.time() diff --git a/wiki b/wiki index a6e3a70d1..85ff38d28 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit a6e3a70d176e8b2af89eedb2ab9c25069146e30f +Subproject commit 85ff38d28980390786f4558666425d29f7bceb33 From a8e29d2976222fdd317740e46265de7decae6299 Mon Sep 17 00:00:00 2001 From: James Banks <55031265+james-banks@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:36:12 +0100 Subject: [PATCH 020/141] Update qwen-image text encoder in changelog Updated per the technical report that lists Qwen2.5-VL explicitly as the text encoder. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e9c10937..0c0169489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) - new image foundational model with 20B params DiT and using Qwen-2.5 8B as text-encoder! + new image foundational model with 20B params DiT and using Qwen2.5-VL-7B as the text-encoder! available for text-to-image workflows, image-editing workflows will follow soon *note*: this model is almost 2x the size of Flux, quantization and offloading are highly recommended! available via *networks -> models -> reference* From c0de857b115fad7b04f57ba5daabf28287a81d3f Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 5 Aug 2025 09:21:35 -0400 Subject: [PATCH 021/141] simlify extensions layout Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 3 ++- TODO.md | 11 ----------- modules/ui_extensions.py | 10 ++++++---- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c0169489..fe3ec541c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Qwen: -## Update for 2025-08-04 +## Update for 2025-08-05 - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) @@ -29,6 +29,7 @@ - updated *models -> current* tab - updated *models -> list models* tab - updated *models -> metadata* tab + - updated *extensions* tab - quicksettings reset button to restore all quicksettings to default values because things do sometimes get wrong... - redesign *settings -> user interface* diff --git a/TODO.md b/TODO.md index 31ca627d4..af686fbda 100644 --- a/TODO.md +++ b/TODO.md @@ -2,10 +2,6 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladmandic/projects) -## Current - -- Gallery: force refresh on delete - ## Future Candidates - [Modular pipelines and guiders](https://github.com/huggingface/diffusers/issues/11915) @@ -28,13 +24,6 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma - see - blocked by `insightface` -## ModernUI - -- Extensions tab: - - full CSS redesign -- Models tab: - - CivitAI subtab: redesign downloader - ### Under Consideration - [IPAdapter negative guidance](https://github.com/huggingface/diffusers/discussions/7167) diff --git a/modules/ui_extensions.py b/modules/ui_extensions.py index 543f4b481..1db079b5d 100644 --- a/modules/ui_extensions.py +++ b/modules/ui_extensions.py @@ -279,6 +279,7 @@ def search_extensions(search_text, sort_column): def create_html(search_text, sort_column): # shared.log.debug(f'Extensions manager: refresh list search="{search_text}" sort="{sort_column}"') code = """ +
@@ -421,7 +422,7 @@ def create_html(search_text, sort_column): """ - code += "
{version_code} {install_code}
" + code += "
" shared.log.debug(f'Extension list: processed={stats["processed"]} installed={stats["installed"]} enabled={stats["enabled"]} disabled={stats["installed"] - stats["enabled"]} visible={stats["processed"] - stats["hidden"]} hidden={stats["hidden"]}') return code @@ -438,9 +439,10 @@ def create_ui(): uninstall_extension_button = gr.Button(elem_id="uninstall_extension_button", visible=False) update_extension_button = gr.Button(elem_id="update_extension_button", visible=False) with gr.Column(scale=4): - search_text = gr.Textbox(label="Search") - with gr.Column(scale=1): - sort_column = gr.Dropdown(value="default", label="Sort by", choices=list(sort_ordering.keys()), multiselect=False) + with gr.Row(): + search_text = gr.Textbox(label="Search") + with gr.Row(): + sort_column = gr.Dropdown(value="default", label="Sort by", choices=list(sort_ordering.keys()), multiselect=False) with gr.Column(scale=1): refresh_extensions_button = gr.Button(value="Refresh extension list", variant="primary") check = gr.Button(value="Update all installed", variant="primary") From db81f08a881155df220cf06071f8b32bb15c62f9 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 5 Aug 2025 09:22:09 -0400 Subject: [PATCH 022/141] update modernui Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 89c232814..f0eacfce3 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 89c232814a3a65f00783af962975ffb31ce336db +Subproject commit f0eacfce3c0945126d5516e61e508762a1b78671 From 692c2368313edc22380762f2497e8f4ab1495262 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 5 Aug 2025 13:28:55 -0400 Subject: [PATCH 023/141] modernui updates Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- javascript/extraNetworks.js | 4 ++-- javascript/sdnext.css | 5 +++++ modules/civitai/api_civitai.py | 2 +- modules/civitai/search_civitai.py | 5 +++-- modules/ui_models.py | 5 +++-- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index f0eacfce3..d24549711 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit f0eacfce3c0945126d5516e61e508762a1b78671 +Subproject commit d24549711d0b29407e6bc1215c6c2ed743978335 diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index a93cff798..244f7c428 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -279,8 +279,8 @@ function applyStyles(styles) { if (index > -1) newStyles.splice(index, 1); else newStyles.push(desiredStyle); gradioApp().querySelectorAll('.extra-network-cards .card').forEach((el) => { - if (newStyles.includes(el.getAttribute('data-name'))) el.style.boxShadow = '0 0 2px 4px var(--button-primary-border-color)'; - else el.style.boxShadow = 'none'; + if (newStyles.includes(el.getAttribute('data-name'))) el.classList.add('card-selected'); + else el.classList.remove('card-selected'); }); return newStyles.join('|'); } diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 7c3874a30..d68a1be8e 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -1250,6 +1250,11 @@ table.settings-value-table td { contain: strict; } +*.extra-network-cards .card-selected { + transform: scale(0.9); + box-shadow: 0 0 2em var(--button-primary-background-fill); +} + .extra-network-cards .card .overlay { background: none; width: 100%; diff --git a/modules/civitai/api_civitai.py b/modules/civitai/api_civitai.py index 8e4561723..92add902f 100644 --- a/modules/civitai/api_civitai.py +++ b/modules/civitai/api_civitai.py @@ -28,7 +28,7 @@ def get_civitai( period:str = '', # AllTime, Year, Month, Week, Day nsfw:bool = None, # optional:bool limit:int = 0, - base:list[str] = [], # list + base:str = '', token:str = None, exact:bool = True, ): diff --git a/modules/civitai/search_civitai.py b/modules/civitai/search_civitai.py index bb7601155..8f0f2bad5 100644 --- a/modules/civitai/search_civitai.py +++ b/modules/civitai/search_civitai.py @@ -7,6 +7,7 @@ from installer import install, log full_dct = False full_html = False +base_models = ['', 'ODOR', 'SD 1.4', 'SD 1.5', 'SD 1.5 LCM', 'SD 1.5 Hyper', 'SD 2.0', 'SD 2.0 768', 'SD 2.1', 'SD 2.1 768', 'SD 2.1 Unclip', 'SDXL 0.9', 'SDXL 1.0', 'SD 3', 'SD 3.5', 'SD 3.5 Medium', 'SD 3.5 Large', 'SD 3.5 Large Turbo', 'Pony', 'Flux.1 S', 'Flux.1 D', 'Flux.1 Kontext', 'AuraFlow', 'SDXL 1.0 LCM', 'SDXL Distilled', 'SDXL Turbo', 'SDXL Lightning', 'SDXL Hyper', 'Stable Cascade', 'SVD', 'SVD XT', 'Playground v2', 'PixArt a', 'PixArt E', 'Hunyuan 1', 'Hunyuan Video', 'Lumina', 'Kolors', 'Illustrious', 'Mochi', 'LTXV', 'CogVideoX', 'NoobAI', 'Wan Video', 'Wan Video 1.3B t2v', 'Wan Video 14B t2v', 'Wan Video 14B i2v 480p', 'Wan Video 14B i2v 720p', 'HiDream', 'OpenAI', 'Imagen4', 'Other'] # noqa: E501 @dataclass @@ -100,7 +101,7 @@ def search_civitai( period:str = '', # (AllTime, Year, Month, Week, Day) nsfw:bool = None, # optional:bool limit:int = 0, - base:list[str] = [], # list + base:str = '', # list token:str = None, exact:bool = True, ): @@ -128,7 +129,7 @@ def search_civitai( if len(period) > 0: dct['period'] = period if len(base) > 0: - dct['baseModels'] = ','.join(base) + dct['baseModels'] = base encoded = urlencode(dct) headers = {} diff --git a/modules/ui_models.py b/modules/ui_models.py index f6f2aa257..141558878 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -469,8 +469,8 @@ def create_ui(): ) with gr.Tab(label="CivitAI", elem_id="models_civitai_tab"): + from modules.civitai.search_civitai import search_civitai, create_model_cards, base_models def civitai_search(civit_search_text, civit_search_tag, civit_nsfw, civit_type, civit_base, civit_token): - from modules.civitai.search_civitai import search_civitai, create_model_cards results = search_civitai(query=civit_search_text, tag=civit_search_tag, nsfw=civit_nsfw, types=civit_type, base=civit_base, token=civit_token) html = create_model_cards(results) return html @@ -502,7 +502,8 @@ def create_ui(): with gr.Row(): civit_type = gr.Textbox(label='Model type', placeholder='Checkpoint, LORA, ...') with gr.Row(): - civit_base = gr.Textbox(label='Base model', placeholder='SDXL, ...') + # civit_base = gr.Textbox(label='Base model', placeholder='SDXL, ...') + civit_base = gr.Dropdown(choices=base_models, label='Base model', value='') with gr.Row(): civit_folder = gr.Textbox(label='Download folder', placeholder='optional folder for downloads') with gr.Row(): From 11a6b1e14bbb3f389debd86263bbff1eb88131fc Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 5 Aug 2025 13:35:50 -0400 Subject: [PATCH 024/141] rename size controls Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- modules/ui_control.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index d24549711..ff16330f7 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit d24549711d0b29407e6bc1215c6c2ed743978335 +Subproject commit ff16330f72e38bc9478281a2e2eccabd9f8fa132 diff --git a/modules/ui_control.py b/modules/ui_control.py index 8d048da97..05197127b 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -144,9 +144,9 @@ def create_ui(_blocks: gr.Blocks=None): with gr.Accordion(open=False, label="Size", elem_id="control_size", elem_classes=["small-accordion"]): with gr.Tabs(): - with gr.Tab('Before'): + with gr.Tab('Initial'): resize_mode_before, resize_name_before, resize_context_before, width_before, height_before, scale_by_before, selected_scale_tab_before = ui_sections.create_resize_inputs('control_before', [], accordion=False, latent=True, prefix='before') - with gr.Tab('After'): + with gr.Tab('Post'): resize_mode_after, resize_name_after, resize_context_after, width_after, height_after, scale_by_after, selected_scale_tab_after = ui_sections.create_resize_inputs('control_after', [], accordion=False, latent=False, prefix='after') with gr.Tab('Mask'): resize_mode_mask, resize_name_mask, resize_context_mask, width_mask, height_mask, scale_by_mask, selected_scale_tab_mask = ui_sections.create_resize_inputs('control_mask', [], accordion=False, latent=False, prefix='mask') From 5fd44e473ea9d31c8420b63ad8175bf38faa9ad5 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 6 Aug 2025 06:21:17 -0400 Subject: [PATCH 025/141] fix networks scan Signed-off-by: Vladimir Mandic --- modules/civitai/metadata_civitai.py | 13 ++++--------- modules/ui_extra_networks.py | 3 ++- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/modules/civitai/metadata_civitai.py b/modules/civitai/metadata_civitai.py index 3d40296b5..93bc143be 100644 --- a/modules/civitai/metadata_civitai.py +++ b/modules/civitai/metadata_civitai.py @@ -228,12 +228,8 @@ def civit_search_metadata(title: str = None): def create_search_metadata_table(rows): html = """ - - - - - {tbody} - + + {tbody}
NameIDTypeCodeHashSizeNote
NameIDTypeCodeHashSizeNote
""" tbody = '' @@ -254,14 +250,12 @@ def civit_search_metadata(title: str = None): log.error(f'Model list: row={row} {e}') return html.format(tbody=tbody) - from modules.ui_extra_networks import get_pages results = [] scanned, skipped = 0, 0 t0 = time.time() candidates = [] re_skip = [r.strip() for r in opts.extra_networks_scan_skip.split(',') if len(r.strip()) > 0] - log.debug(f'CivitAI search metadata: type={title if type(title) == str else "all"} skip={re_skip}') for page in get_pages(): if type(title) == str: if page.title != title: @@ -276,6 +270,7 @@ def civit_search_metadata(title: str = None): continue scanned += 1 candidates.append(item) + log.debug(f'CivitAI search metadata: type={title if type(title) == str else "all"} workers={max_workers} skip={len(re_skip)} items={len(candidates)}') import concurrent with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: future_items = {} @@ -287,4 +282,4 @@ def civit_search_metadata(title: str = None): t1 = time.time() log.debug(f'CivitAI search metadata: scanned={scanned} skipped={skipped} time={t1-t0:.2f}') - return create_search_metadata_table(results) + yield create_search_metadata_table(results) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 1cb4b4cc0..6882cf616 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -933,7 +933,8 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): def ui_scan_click(title): from modules.civitai.metadata_civitai import civit_search_metadata - civit_search_metadata(title) + for _generator in civit_search_metadata(title): # need to read generator output so python does not optimize function away + pass return ui_refresh_click(title) def ui_save_click(): From e23ca199c0dd84faca7a14cef6770c87f57229cf Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 6 Aug 2025 07:25:28 -0400 Subject: [PATCH 026/141] networks indicator for active loras Signed-off-by: Vladimir Mandic --- .eslintrc.json | 1 + CHANGELOG.md | 3 ++- javascript/extraNetworks.js | 28 ++++++++++++++++++------ javascript/ui.js | 9 +++++--- modules/ui_extra_networks.py | 10 +++++---- modules/ui_extra_networks_checkpoints.py | 4 ++-- 6 files changed, 38 insertions(+), 17 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 7a730c1c2..7f4c9f27f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -87,6 +87,7 @@ "get_tab_index": "readonly", "create_submit_args": "readonly", "restartReload": "readonly", + "markSelectedCards": "readonly", "updateInput": "readonly", "toggleCompact": "readonly", "setFontSize": "readonly", diff --git a/CHANGELOG.md b/CHANGELOG.md index fe3ec541c..87b1568a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,8 @@ - redesign *settings -> user interface* - gallery bypass browser cache for thumbnails - gallery safer delete operation - - more css optimizations and styling + - networks display indicator for currently active items + styles, loras - *hint*: card layout card layout is used by networks, gallery, civitai search, etc. you can change card size in *settings -> user interface* diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 244f7c428..29b97005f 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -239,14 +239,31 @@ function refreshENInput(tabname) { gradioApp().querySelector(`#${tabname}_extra_networks textarea`)?.dispatchEvent(new Event('input')); } -function cardClicked(textToAdd, allowNegativePrompt) { - // log('cardClicked', textToAdd, allowNegativePrompt); +async function markSelectedCards(selected, page = '') { + log('markSelectedCards', selected, page); + gradioApp().querySelectorAll('.extra-network-cards .card').forEach((el) => { + if (page.length > 0 && el.dataset.page !== page) return; // filter by page + if (selected.includes(el.dataset.name) || selected.includes(el.dataset.short)) el.classList.add('card-selected'); + else el.classList.remove('card-selected'); + }); +} + +function extractLoraNames(prompt) { + const regex = /]+)(?::[\d.]+)?>/g; + const names = []; + let match; + while ((match = regex.exec(prompt)) !== null) names.push(match[1]); // eslint-disable-line no-cond-assign + return names; +} + +function cardClicked(textToAdd) { const tabname = getENActiveTab(); log('cardClicked', tabname, textToAdd); - const textarea = allowNegativePrompt ? activePromptTextarea[tabname] : gradioApp().querySelector(`#${tabname}_prompt > label > textarea`); + const textarea = activePromptTextarea[tabname]; if (textarea.value.indexOf(textToAdd) !== -1) textarea.value = textarea.value.replace(textToAdd, ''); else textarea.value += textToAdd; updateInput(textarea); + markSelectedCards(extractLoraNames(textarea.value), 'lora'); } function extraNetworksSearchButton(event) { @@ -278,10 +295,7 @@ function applyStyles(styles) { const index = newStyles.indexOf(desiredStyle); if (index > -1) newStyles.splice(index, 1); else newStyles.push(desiredStyle); - gradioApp().querySelectorAll('.extra-network-cards .card').forEach((el) => { - if (newStyles.includes(el.getAttribute('data-name'))) el.classList.add('card-selected'); - else el.classList.remove('card-selected'); - }); + markSelectedCards(newStyles, 'style'); return newStyles.join('|'); } diff --git a/javascript/ui.js b/javascript/ui.js index a499dc32d..e6885a80e 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -484,20 +484,23 @@ function selectCheckpoint(name) { const isRefiner = btnModel && btnModel.classList.contains('toolbutton-selected'); if (isRefiner) gradioApp().getElementById('change_refiner').click(); else gradioApp().getElementById('change_checkpoint').click(); - log(`Change ${isRefiner ? 'refiner' : 'model'}: ${desiredCheckpointName}`); + log(`selectCheckpoint ${isRefiner ? 'refiner' : 'model'}: ${desiredCheckpointName}`); + markSelectedCards([desiredCheckpointName], 'model'); } let desiredVAEName = null; function selectVAE(name) { desiredVAEName = name; gradioApp().getElementById('change_vae').click(); - log(`Change VAE: ${desiredVAEName}`); + log(`selectVAE: ${desiredVAEName}`); + markSelectedCards([desiredVAEName], 'vae'); } function selectReference(name) { - log(`Select reference: ${name}`); + log(`selectReference: ${name}`); desiredCheckpointName = name; gradioApp().getElementById('change_reference').click(); + markSelectedCards([desiredCheckpointName], 'model'); } function currentImageResolutionimg2img(_a, _b, scaleBy) { diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 6882cf616..46b002abe 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -27,7 +27,7 @@ extra_pages = shared.extra_networks debug = shared.log.trace if os.environ.get('SD_EN_DEBUG', None) is not None else lambda *args, **kwargs: None debug('Trace: EN') card_full = ''' -
+
{title}
@@ -41,7 +41,7 @@ card_full = '''
''' card_list = ''' -
+
🛈 
{title}  @@ -311,12 +311,14 @@ class ExtraNetworksPage: return '#{:02x}{:02x}{:02x}'.format(r, g, b) # pylint: disable=consider-using-f-string try: + onclick = f'cardClicked({item.get("prompt", None)})' args = { - "tabname": tabname, + # "tabname": tabname, "page": self.name, "name": item.get('name', ''), "title": os.path.basename(item["name"].replace('_', ' ')), "filename": item.get('filename', ''), + "short": os.path.splitext(os.path.basename(item.get('filename', '')))[0], "tags": '|'.join([item.get('tags')] if isinstance(item.get('tags', {}), str) else list(item.get('tags', {}).keys())), "preview": html.escape(item.get('preview', None) or self.link_preview('html/card-no-preview.png')), "width": 'var(--card-size)', @@ -325,7 +327,7 @@ class ExtraNetworksPage: "prompt": item.get("prompt", None), "search": item.get("search_term", ""), "description": item.get("description") or "", - "card_click": item.get("onclick", '"' + html.escape(f'return cardClicked({item.get("prompt", None)}, {"true" if self.allow_negative_prompt else "false"})') + '"'), + "card_click": item.get("onclick", '"' + html.escape(onclick) + '"'), "mtime": item.get("mtime", 0), "size": item.get("size", 0), "version": item.get("version", ''), diff --git a/modules/ui_extra_networks_checkpoints.py b/modules/ui_extra_networks_checkpoints.py index 3eab7fd41..15efb0e5c 100644 --- a/modules/ui_extra_networks_checkpoints.py +++ b/modules/ui_extra_networks_checkpoints.py @@ -33,7 +33,7 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage): "filename": url, "preview": self.find_preview(os.path.join(reference_dir, preview)), "local_preview": self.find_preview_file(os.path.join(reference_dir, preview)), - "onclick": '"' + html.escape(f"""return selectReference({json.dumps(url)})""") + '"', + "onclick": '"' + html.escape(f"selectReference({json.dumps(url)})") + '"', "hash": None, "mtime": 0, "size": 0, @@ -54,7 +54,7 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage): "filename": checkpoint.filename, "hash": checkpoint.shorthash, "metadata": checkpoint.metadata, - "onclick": '"' + html.escape(f"""return selectCheckpoint({json.dumps(name)})""") + '"', + "onclick": '"' + html.escape(f"selectCheckpoint({json.dumps(name)})") + '"', "mtime": os.path.getmtime(checkpoint.filename) if exists else 0, "size": os.path.getsize(checkpoint.filename) if exists else 0, } From 31664864bf44bd6a37e20f1866cb45690cdf1b5f Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 6 Aug 2025 08:54:56 -0400 Subject: [PATCH 027/141] qwen cleanup and update changelog Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 16 ++++++++++++++-- modules/modeldata.py | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b1568a4..f1276e1be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,27 @@ ## Blockers -- Qwen: +- Qwen with offloading: -## Update for 2025-08-05 +## Update for 2025-08-06 + +### Highlights for 2025-08-06 + +This time we have several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/), [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) and [Chroma](https://huggingface.co/lodestones/Chroma) +Continuing with major UI work, there is new embedded Docs/Wiki search, redesigned CivitAI integration and quite a few UI updates! +On the compute side, new profiles for high-vram GPUs and offloading improvements +And (*as always*) many bugfixes and improvements to existing features! + +[ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) + +### Details for 2025-08-06 - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) new image foundational model with 20B params DiT and using Qwen2.5-VL-7B as the text-encoder! available for text-to-image workflows, image-editing workflows will follow soon *note*: this model is almost 2x the size of Flux, quantization and offloading are highly recommended! + recommended params: *steps=50, attention-guidance=4* available via *networks -> models -> reference* - [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) new 12B base model compatible with FLUX.1-Dev from *Black Forest Labs* with opinionated aesthetics and aesthetic preferences in mind diff --git a/modules/modeldata.py b/modules/modeldata.py index 48d21028d..0cdbcb19d 100644 --- a/modules/modeldata.py +++ b/modules/modeldata.py @@ -58,6 +58,8 @@ def get_model_type(pipe): model_type = 'pixartalpha' elif "Bria" in name: model_type = 'bria' + elif 'Qwen' in name: + model_type = 'qwen' # video models elif "CogVideo" in name: model_type = 'cogvideo' From ba4bff08d6c35c42a8d94ec3e0ae127f5417bb9e Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 6 Aug 2025 09:27:54 -0400 Subject: [PATCH 028/141] remove ldsr and refactor sdnq device map Signed-off-by: Vladimir Mandic --- .pylintrc | 1 - modules/ldsr/ldsr_model_arch.py | 236 ---- modules/ldsr/sd_hijack_autoencoder.py | 290 ----- modules/ldsr/sd_hijack_ddpm_v1.py | 1439 ------------------------- modules/model_quant.py | 44 +- modules/postprocess/ldsr_model.py | 75 -- modules/shared.py | 3 +- modules/shared_legacy.py | 3 +- 8 files changed, 25 insertions(+), 2066 deletions(-) delete mode 100644 modules/ldsr/ldsr_model_arch.py delete mode 100644 modules/ldsr/sd_hijack_autoencoder.py delete mode 100644 modules/ldsr/sd_hijack_ddpm_v1.py delete mode 100644 modules/postprocess/ldsr_model.py diff --git a/.pylintrc b/.pylintrc index 1929c26fb..0f77d8aa4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -24,7 +24,6 @@ ignore-paths=/usr/lib/.*$, modules/intel, modules/intel/ipex, modules/framepack/pipeline, - modules/ldsr, modules/onnx_impl, modules/pag, modules/postprocess/aurasr_arch.py, diff --git a/modules/ldsr/ldsr_model_arch.py b/modules/ldsr/ldsr_model_arch.py deleted file mode 100644 index 7494fcfcc..000000000 --- a/modules/ldsr/ldsr_model_arch.py +++ /dev/null @@ -1,236 +0,0 @@ -import os -import time -import numpy as np -import torch -import torchvision -from PIL import Image -from einops import rearrange, repeat -from omegaconf import OmegaConf -import safetensors.torch -from ldm.models.diffusion.ddim import DDIMSampler -from ldm.util import instantiate_from_config, ismap -from modules import devices, shared, sd_hijack -from modules.upscaler import compile_upscaler - -cached_ldsr_model: torch.nn.Module = None - - -# Create LDSR Class -class LDSR: - def load_model_from_config(self, half_attention): - global cached_ldsr_model # pylint: disable=global-statement - - if cached_ldsr_model is not None: - shared.log.info(f"Upscaler cached: type=LDSR model={self.modelPath}") - model: torch.nn.Module = cached_ldsr_model - else: - _, extension = os.path.splitext(self.modelPath) - if extension.lower() == ".safetensors": - pl_sd = safetensors.torch.load_file(self.modelPath, device="cpu") - else: - pl_sd = torch.load(self.modelPath, map_location="cpu") - shared.log.info(f"Upscaler loaded: type=LDSR model={self.modelPath}") - sd = pl_sd["state_dict"] if "state_dict" in pl_sd else pl_sd - config = OmegaConf.load(self.yamlPath) - config.model.target = "ldm.models.diffusion.ddpm.LatentDiffusionV1" - model: torch.nn.Module = instantiate_from_config(config.model) - model.load_state_dict(sd, strict=False) - model = model.to(devices.device) - if half_attention: - model = model.half() - if shared.cmd_opts.opt_channelslast: - model = model.to(memory_format=torch.channels_last) - sd_hijack.model_hijack.hijack(model) # apply optimization - model.eval() - model = compile_upscaler(model) - cached_ldsr_model = model - return {"model": model} - - def __init__(self, model_path, yaml_path): - self.modelPath = model_path - self.yamlPath = yaml_path - - @staticmethod - def run(model, selected_path, custom_steps, eta): - example = get_cond(selected_path) - n_runs = 1 - guider = None - ckwargs = None - ddim_use_x0_pred = False - temperature = 1. - eta = eta # pylint: disable=self-assigning-variable - custom_shape = None - height, width = example["image"].shape[1:3] - split_input = height >= 128 and width >= 128 - if split_input: - ks = 128 - stride = 64 - vqf = 4 # - model.split_input_params = {"ks": (ks, ks), "stride": (stride, stride), - "vqf": vqf, - "patch_distributed_vq": True, - "tie_braker": False, - "clip_max_weight": 0.5, - "clip_min_weight": 0.01, - "clip_max_tie_weight": 0.5, - "clip_min_tie_weight": 0.01} - else: - if hasattr(model, "split_input_params"): - delattr(model, "split_input_params") - - x_t = None - logs = None - for _ in range(n_runs): - if custom_shape is not None: - x_t = torch.randn(1, custom_shape[1], custom_shape[2], custom_shape[3]).to(model.device) - x_t = repeat(x_t, '1 c h w -> b c h w', b=custom_shape[0]) - - logs = make_convolutional_sample(example, model, - custom_steps=custom_steps, - eta=eta, quantize_x0=False, - custom_shape=custom_shape, - temperature=temperature, noise_dropout=0., - corrector=guider, corrector_kwargs=ckwargs, x_T=x_t, - ddim_use_x0_pred=ddim_use_x0_pred - ) - return logs - - def super_resolution(self, image, steps=100, target_scale=2, half_attention=False): - model = self.load_model_from_config(half_attention) - # Run settings - diffusion_steps = int(steps) - eta = 1.0 - im_og = image - width_og, height_og = im_og.size - # If we can adjust the max upscale size, then the 4 below should be our variable - down_sample_rate = target_scale / 4 - wd = width_og * down_sample_rate - hd = height_og * down_sample_rate - width_downsampled_pre = int(np.ceil(wd)) - height_downsampled_pre = int(np.ceil(hd)) - if down_sample_rate != 1: - shared.log.info(f'LDSR Downsampling from [{width_og}, {height_og}] to [{width_downsampled_pre}, {height_downsampled_pre}]') - im_og = im_og.resize((width_downsampled_pre, height_downsampled_pre), Image.LANCZOS) - else: - shared.log.info(f"LDSR Downsample rate is 1 from {target_scale} / 4 (Not downsampling)") - - # pad width and height to multiples of 64, pads with the edge values of image to avoid artifacts - pad_w, pad_h = np.max(((2, 2), np.ceil(np.array(im_og.size) / 64).astype(int)), axis=0) * 64 - im_og.size - im_padded = Image.fromarray(np.pad(np.array(im_og), ((0, pad_h), (0, pad_w), (0, 0)), mode='edge')) - - logs = self.run(model["model"], im_padded, diffusion_steps, eta) - - sample = logs["sample"] - sample = sample.detach().cpu() - sample = torch.clamp(sample, -1., 1.) - sample = (sample + 1.) / 2. * 255 - sample = sample.numpy().astype(np.uint8) - sample = np.transpose(sample, (0, 2, 3, 1)) - a = Image.fromarray(sample[0]) - # remove padding - a = a.crop((0, 0) + tuple(np.array(im_og.size) * 4)) - - if shared.opts.upscaler_unload: - del model - global cached_ldsr_model # pylint: disable=global-statement - cached_ldsr_model = None - shared.log.debug(f"Upscaler unloaded: type=LDSR model={self.modelPath}") - devices.torch_gc(force=True) - - return a - - -def get_cond(selected_path): - example = {} - up_f = 4 - c = selected_path.convert('RGB') - c = torch.unsqueeze(torchvision.transforms.ToTensor()(c), 0) - c_up = torchvision.transforms.functional.resize(c, size=[up_f * c.shape[2], up_f * c.shape[3]], - antialias=True) - c_up = rearrange(c_up, '1 c h w -> 1 h w c') - c = rearrange(c, '1 c h w -> 1 h w c') - c = 2. * c - 1. - - c = c.to(devices.device) - example["LR_image"] = c - example["image"] = c_up - - return example - - -@torch.no_grad() -def convsample_ddim(model, cond, steps, shape, eta=1.0, callback=None, normals_sequence=None, - mask=None, x0=None, quantize_x0=False, temperature=1., score_corrector=None, - corrector_kwargs=None, x_t=None - ): - ddim = DDIMSampler(model) - bs = shape[0] - shape = shape[1:] - shared.log.info(f"LDSR Sampling with eta = {eta}; steps: {steps}") - samples, intermediates = ddim.sample(steps, batch_size=bs, shape=shape, conditioning=cond, callback=callback, - normals_sequence=normals_sequence, quantize_x0=quantize_x0, eta=eta, - mask=mask, x0=x0, temperature=temperature, verbose=False, - score_corrector=score_corrector, - corrector_kwargs=corrector_kwargs, x_t=x_t) - - return samples, intermediates - - -@torch.no_grad() -def make_convolutional_sample(batch, model, custom_steps=None, eta=1.0, quantize_x0=False, custom_shape=None, temperature=1., noise_dropout=0., corrector=None, - corrector_kwargs=None, x_T=None, ddim_use_x0_pred=False): - log = {} - - z, c, x, xrec, xc = model.get_input(batch, model.first_stage_key, - return_first_stage_outputs=True, - force_c_encode=not (hasattr(model, 'split_input_params') - and model.cond_stage_key == 'coordinates_bbox'), - return_original_cond=True) - - if custom_shape is not None: - z = torch.randn(custom_shape) - shared.log.info(f"LDSR Generating {custom_shape[0]} samples of shape {custom_shape[1:]}") - - z0 = None - - log["input"] = x - log["reconstruction"] = xrec - - if ismap(xc): - log["original_conditioning"] = model.to_rgb(xc) - if hasattr(model, 'cond_stage_key'): - log[model.cond_stage_key] = model.to_rgb(xc) - - else: - log["original_conditioning"] = xc if xc is not None else torch.zeros_like(x) - if model.cond_stage_model: - log[model.cond_stage_key] = xc if xc is not None else torch.zeros_like(x) - if model.cond_stage_key == 'class_label': - log[model.cond_stage_key] = xc[model.cond_stage_key] - - with model.ema_scope("Plotting"): - t0 = time.time() - - sample, intermediates = convsample_ddim(model, c, steps=custom_steps, shape=z.shape, - eta=eta, - quantize_x0=quantize_x0, mask=None, x0=z0, - temperature=temperature, score_corrector=corrector, corrector_kwargs=corrector_kwargs, - x_t=x_T) - t1 = time.time() - - if ddim_use_x0_pred: - sample = intermediates['pred_x0'][-1] - - x_sample = model.decode_first_stage(sample) - - try: - x_sample_noquant = model.decode_first_stage(sample, force_not_quantize=True) - log["sample_noquant"] = x_sample_noquant - log["sample_diff"] = torch.abs(x_sample_noquant - x_sample) - except Exception: - pass - - log["sample"] = x_sample - log["time"] = t1 - t0 - - return log diff --git a/modules/ldsr/sd_hijack_autoencoder.py b/modules/ldsr/sd_hijack_autoencoder.py deleted file mode 100644 index 4fd7d67fe..000000000 --- a/modules/ldsr/sd_hijack_autoencoder.py +++ /dev/null @@ -1,290 +0,0 @@ -# The content of this file comes from the ldm/models/autoencoder.py file of the compvis/stable-diffusion repo -# The VQModel & VQModelInterface were subsequently removed from ldm/models/autoencoder.py when we moved to the stability-ai/stablediffusion repo -# As the LDSR upscaler relies on VQModel & VQModelInterface, the hijack aims to put them back into the ldm.models.autoencoder -from contextlib import contextmanager -import numpy as np -import torch -import pytorch_lightning as pl -import torch.nn.functional as F -from torch.optim.lr_scheduler import LambdaLR -from taming.modules.vqvae.quantize import VectorQuantizer2 as VectorQuantizer -from ldm.modules.ema import LitEma -from ldm.modules.diffusionmodules.model import Encoder, Decoder -from ldm.util import instantiate_from_config - -import ldm.models.autoencoder -from packaging import version - -class VQModel(pl.LightningModule): - def __init__(self, - ddconfig, - lossconfig, - n_embed, - embed_dim, - ckpt_path=None, - ignore_keys=None, - image_key="image", - colorize_nlabels=None, - monitor=None, - batch_resize_range=None, - scheduler_config=None, - lr_g_factor=1.0, - remap=None, - sane_index_shape=False, # tell vector quantizer to return indices as bhw - use_ema=False - ): - super().__init__() - self.embed_dim = embed_dim - self.n_embed = n_embed - self.image_key = image_key - self.encoder = Encoder(**ddconfig) - self.decoder = Decoder(**ddconfig) - self.loss = instantiate_from_config(lossconfig) - self.quantize = VectorQuantizer(n_embed, embed_dim, beta=0.25, - remap=remap, - sane_index_shape=sane_index_shape) - self.quant_conv = torch.nn.Conv2d(ddconfig["z_channels"], embed_dim, 1) - self.post_quant_conv = torch.nn.Conv2d(embed_dim, ddconfig["z_channels"], 1) - if colorize_nlabels is not None: - assert type(colorize_nlabels)==int - self.register_buffer("colorize", torch.randn(3, colorize_nlabels, 1, 1)) - if monitor is not None: - self.monitor = monitor - self.batch_resize_range = batch_resize_range - if self.batch_resize_range is not None: - print(f"{self.__class__.__name__}: Using per-batch resizing in range {batch_resize_range}.") - - self.use_ema = use_ema - if self.use_ema: - self.model_ema = LitEma(self) - print(f"Keeping EMAs of {len(list(self.model_ema.buffers()))}.") - - if ckpt_path is not None: - self.init_from_ckpt(ckpt_path, ignore_keys=ignore_keys or []) - self.scheduler_config = scheduler_config - self.lr_g_factor = lr_g_factor - - @contextmanager - def ema_scope(self, context=None): - if self.use_ema: - self.model_ema.store(self.parameters()) - self.model_ema.copy_to(self) - if context is not None: - print(f"{context}: Switched to EMA weights") - try: - yield None - finally: - if self.use_ema: - self.model_ema.restore(self.parameters()) - if context is not None: - print(f"{context}: Restored training weights") - - def init_from_ckpt(self, path, ignore_keys=None): - sd = torch.load(path, map_location="cpu")["state_dict"] - keys = list(sd.keys()) - for k in keys: - for ik in ignore_keys or []: - if k.startswith(ik): - print(f"Deleting key {k} from state_dict.") - del sd[k] - missing, unexpected = self.load_state_dict(sd, strict=False) - print(f"Restored from {path} with {len(missing)} missing and {len(unexpected)} unexpected keys") - if len(missing) > 0: - print(f"Missing Keys: {missing}") - print(f"Unexpected Keys: {unexpected}") - - def on_train_batch_end(self, *args, **kwargs): - if self.use_ema: - self.model_ema(self) - - def encode(self, x): - h = self.encoder(x) - h = self.quant_conv(h) - quant, emb_loss, info = self.quantize(h) - return quant, emb_loss, info - - def encode_to_prequant(self, x): - h = self.encoder(x) - h = self.quant_conv(h) - return h - - def decode(self, quant): - quant = self.post_quant_conv(quant) - dec = self.decoder(quant) - return dec - - def decode_code(self, code_b): - quant_b = self.quantize.embed_code(code_b) - dec = self.decode(quant_b) - return dec - - def forward(self, input, return_pred_indices=False): - quant, diff, (_,_,ind) = self.encode(input) - dec = self.decode(quant) - if return_pred_indices: - return dec, diff, ind - return dec, diff - - def get_input(self, batch, k): - x = batch[k] - if len(x.shape) == 3: - x = x[..., None] - x = x.permute(0, 3, 1, 2).to(memory_format=torch.contiguous_format).float() - if self.batch_resize_range is not None: - lower_size = self.batch_resize_range[0] - upper_size = self.batch_resize_range[1] - if self.global_step <= 4: - # do the first few batches with max size to avoid later oom - new_resize = upper_size - else: - new_resize = np.random.choice(np.arange(lower_size, upper_size+16, 16)) - if new_resize != x.shape[2]: - x = F.interpolate(x, size=new_resize, mode="bicubic") - x = x.detach() - return x - - def training_step(self, batch, batch_idx, optimizer_idx): - # https://github.com/pytorch/pytorch/issues/37142 - # try not to fool the heuristics - x = self.get_input(batch, self.image_key) - xrec, qloss, ind = self(x, return_pred_indices=True) - - if optimizer_idx == 0: - # autoencode - aeloss, log_dict_ae = self.loss(qloss, x, xrec, optimizer_idx, self.global_step, - last_layer=self.get_last_layer(), split="train", - predicted_indices=ind) - - self.log_dict(log_dict_ae, prog_bar=False, logger=True, on_step=True, on_epoch=True) - return aeloss - - if optimizer_idx == 1: - # discriminator - discloss, log_dict_disc = self.loss(qloss, x, xrec, optimizer_idx, self.global_step, - last_layer=self.get_last_layer(), split="train") - self.log_dict(log_dict_disc, prog_bar=False, logger=True, on_step=True, on_epoch=True) - return discloss - - def validation_step(self, batch, batch_idx): - log_dict = self._validation_step(batch, batch_idx) - with self.ema_scope(): - self._validation_step(batch, batch_idx, suffix="_ema") - return log_dict - - def _validation_step(self, batch, batch_idx, suffix=""): - x = self.get_input(batch, self.image_key) - xrec, qloss, ind = self(x, return_pred_indices=True) - aeloss, log_dict_ae = self.loss(qloss, x, xrec, 0, - self.global_step, - last_layer=self.get_last_layer(), - split="val"+suffix, - predicted_indices=ind - ) - - discloss, log_dict_disc = self.loss(qloss, x, xrec, 1, - self.global_step, - last_layer=self.get_last_layer(), - split="val"+suffix, - predicted_indices=ind - ) - rec_loss = log_dict_ae[f"val{suffix}/rec_loss"] - self.log(f"val{suffix}/rec_loss", rec_loss, - prog_bar=True, logger=True, on_step=False, on_epoch=True, sync_dist=True) - self.log(f"val{suffix}/aeloss", aeloss, - prog_bar=True, logger=True, on_step=False, on_epoch=True, sync_dist=True) - if version.parse(pl.__version__) >= version.parse('1.4.0'): - del log_dict_ae[f"val{suffix}/rec_loss"] - self.log_dict(log_dict_ae) - self.log_dict(log_dict_disc) - return self.log_dict - - def configure_optimizers(self): - lr_d = self.learning_rate - lr_g = self.lr_g_factor*self.learning_rate - print("lr_d", lr_d) - print("lr_g", lr_g) - opt_ae = torch.optim.Adam(list(self.encoder.parameters())+ - list(self.decoder.parameters())+ - list(self.quantize.parameters())+ - list(self.quant_conv.parameters())+ - list(self.post_quant_conv.parameters()), - lr=lr_g, betas=(0.5, 0.9)) - opt_disc = torch.optim.Adam(self.loss.discriminator.parameters(), - lr=lr_d, betas=(0.5, 0.9)) - - if self.scheduler_config is not None: - scheduler = instantiate_from_config(self.scheduler_config) - - print("Setting up LambdaLR scheduler...") - scheduler = [ - { - 'scheduler': LambdaLR(opt_ae, lr_lambda=scheduler.schedule), - 'interval': 'step', - 'frequency': 1 - }, - { - 'scheduler': LambdaLR(opt_disc, lr_lambda=scheduler.schedule), - 'interval': 'step', - 'frequency': 1 - }, - ] - return [opt_ae, opt_disc], scheduler - return [opt_ae, opt_disc], [] - - def get_last_layer(self): - return self.decoder.conv_out.weight - - def log_images(self, batch, only_inputs=False, plot_ema=False, **kwargs): - log = {} - x = self.get_input(batch, self.image_key) - x = x.to(self.device) - if only_inputs: - log["inputs"] = x - return log - xrec, _ = self(x) - if x.shape[1] > 3: - # colorize with random projection - assert xrec.shape[1] > 3 - x = self.to_rgb(x) - xrec = self.to_rgb(xrec) - log["inputs"] = x - log["reconstructions"] = xrec - if plot_ema: - with self.ema_scope(): - xrec_ema, _ = self(x) - if x.shape[1] > 3: - xrec_ema = self.to_rgb(xrec_ema) - log["reconstructions_ema"] = xrec_ema - return log - - def to_rgb(self, x): - assert self.image_key == "segmentation" - if not hasattr(self, "colorize"): - self.register_buffer("colorize", torch.randn(3, x.shape[1], 1, 1).to(x)) - x = F.conv2d(x, weight=self.colorize) - x = 2.*(x-x.min())/(x.max()-x.min()) - 1. - return x - - -class VQModelInterface(VQModel): - def __init__(self, embed_dim, *args, **kwargs): - super().__init__(*args, embed_dim=embed_dim, **kwargs) - self.embed_dim = embed_dim - - def encode(self, x): - h = self.encoder(x) - h = self.quant_conv(h) - return h - - def decode(self, h, force_not_quantize=False): - # also go through quantization layer - if not force_not_quantize: - quant, emb_loss, info = self.quantize(h) - else: - quant = h - quant = self.post_quant_conv(quant) - dec = self.decoder(quant) - return dec - -ldm.models.autoencoder.VQModel = VQModel -ldm.models.autoencoder.VQModelInterface = VQModelInterface diff --git a/modules/ldsr/sd_hijack_ddpm_v1.py b/modules/ldsr/sd_hijack_ddpm_v1.py deleted file mode 100644 index 2cf506990..000000000 --- a/modules/ldsr/sd_hijack_ddpm_v1.py +++ /dev/null @@ -1,1439 +0,0 @@ -# This script is copied from the compvis/stable-diffusion repo (aka the SD V1 repo) -# Original filename: ldm/models/diffusion/ddpm.py -# The purpose to reinstate the old DDPM logic which works with VQ, whereas the V2 one doesn't -# Some models such as LDSR require VQ to work correctly -# The classes are suffixed with "V1" and added back to the "ldm.models.diffusion.ddpm" module - -import torch -import torch.nn as nn -import numpy as np -import pytorch_lightning as pl -from torch.optim.lr_scheduler import LambdaLR -from einops import rearrange, repeat -from contextlib import contextmanager -from functools import partial -from tqdm import tqdm -from torchvision.utils import make_grid -from pytorch_lightning.utilities.distributed import rank_zero_only -from ldm.util import log_txt_as_img, exists, default, ismap, isimage, mean_flat, count_params, instantiate_from_config -from ldm.modules.ema import LitEma -from ldm.modules.distributions.distributions import normal_kl, DiagonalGaussianDistribution -from ldm.models.autoencoder import VQModelInterface, IdentityFirstStage, AutoencoderKL -from ldm.modules.diffusionmodules.util import make_beta_schedule, extract_into_tensor, noise_like -from ldm.models.diffusion.ddim import DDIMSampler -import ldm.models.diffusion.ddpm - -__conditioning_keys__ = {'concat': 'c_concat', - 'crossattn': 'c_crossattn', - 'adm': 'y'} - - -def disabled_train(self, mode=True): - """Overwrite model.train with this function to make sure train/eval mode - does not change anymore.""" - return self - - -def uniform_on_device(r1, r2, shape, device): - return (r1 - r2) * torch.rand(*shape, device=device) + r2 - - -class DDPMV1(pl.LightningModule): - # classic DDPM with Gaussian diffusion, in image space - def __init__(self, - unet_config, - timesteps=1000, - beta_schedule="linear", - loss_type="l2", - ckpt_path=None, - ignore_keys=None, - load_only_unet=False, - monitor="val/loss", - use_ema=True, - first_stage_key="image", - image_size=256, - channels=3, - log_every_t=100, - clip_denoised=True, - linear_start=1e-4, - linear_end=2e-2, - cosine_s=8e-3, - given_betas=None, - original_elbo_weight=0., - v_posterior=0., # weight for choosing posterior variance as sigma = (1-v) * beta_tilde + v * beta - l_simple_weight=1., - conditioning_key=None, - parameterization="eps", # all assuming fixed variance schedules - scheduler_config=None, - use_positional_encodings=False, - learn_logvar=False, - logvar_init=0., - ): - super().__init__() - assert parameterization in ["eps", "x0"], 'currently only supporting "eps" and "x0"' - self.parameterization = parameterization - print(f"{self.__class__.__name__}: Running in {self.parameterization}-prediction mode") - self.cond_stage_model = None - self.clip_denoised = clip_denoised - self.log_every_t = log_every_t - self.first_stage_key = first_stage_key - self.image_size = image_size # try conv? - self.channels = channels - self.use_positional_encodings = use_positional_encodings - self.model = DiffusionWrapperV1(unet_config, conditioning_key) - count_params(self.model, verbose=True) - self.use_ema = use_ema - if self.use_ema: - self.model_ema = LitEma(self.model) - print(f"Keeping EMAs of {len(list(self.model_ema.buffers()))}.") - - self.use_scheduler = scheduler_config is not None - if self.use_scheduler: - self.scheduler_config = scheduler_config - - self.v_posterior = v_posterior - self.original_elbo_weight = original_elbo_weight - self.l_simple_weight = l_simple_weight - - if monitor is not None: - self.monitor = monitor - if ckpt_path is not None: - self.init_from_ckpt(ckpt_path, ignore_keys=ignore_keys or [], only_model=load_only_unet) - - self.register_schedule(given_betas=given_betas, beta_schedule=beta_schedule, timesteps=timesteps, - linear_start=linear_start, linear_end=linear_end, cosine_s=cosine_s) - - self.loss_type = loss_type - - self.learn_logvar = learn_logvar - self.logvar = torch.full(fill_value=logvar_init, size=(self.num_timesteps,)) - if self.learn_logvar: - self.logvar = nn.Parameter(self.logvar, requires_grad=True) - - - def register_schedule(self, given_betas=None, beta_schedule="linear", timesteps=1000, - linear_start=1e-4, linear_end=2e-2, cosine_s=8e-3): - if exists(given_betas): - betas = given_betas - else: - betas = make_beta_schedule(beta_schedule, timesteps, linear_start=linear_start, linear_end=linear_end, - cosine_s=cosine_s) - alphas = 1. - betas - alphas_cumprod = np.cumprod(alphas, axis=0) - alphas_cumprod_prev = np.append(1., alphas_cumprod[:-1]) - - timesteps, = betas.shape - self.num_timesteps = int(timesteps) - self.linear_start = linear_start - self.linear_end = linear_end - assert alphas_cumprod.shape[0] == self.num_timesteps, 'alphas have to be defined for each timestep' - - to_torch = partial(torch.tensor, dtype=torch.float32) - - self.register_buffer('betas', to_torch(betas)) - self.register_buffer('alphas_cumprod', to_torch(alphas_cumprod)) - self.register_buffer('alphas_cumprod_prev', to_torch(alphas_cumprod_prev)) - - # calculations for diffusion q(x_t | x_{t-1}) and others - self.register_buffer('sqrt_alphas_cumprod', to_torch(np.sqrt(alphas_cumprod))) - self.register_buffer('sqrt_one_minus_alphas_cumprod', to_torch(np.sqrt(1. - alphas_cumprod))) - self.register_buffer('log_one_minus_alphas_cumprod', to_torch(np.log(1. - alphas_cumprod))) - self.register_buffer('sqrt_recip_alphas_cumprod', to_torch(np.sqrt(1. / alphas_cumprod))) - self.register_buffer('sqrt_recipm1_alphas_cumprod', to_torch(np.sqrt(1. / alphas_cumprod - 1))) - - # calculations for posterior q(x_{t-1} | x_t, x_0) - posterior_variance = (1 - self.v_posterior) * betas * (1. - alphas_cumprod_prev) / ( - 1. - alphas_cumprod) + self.v_posterior * betas - # above: equal to 1. / (1. / (1. - alpha_cumprod_tm1) + alpha_t / beta_t) - self.register_buffer('posterior_variance', to_torch(posterior_variance)) - # below: log calculation clipped because the posterior variance is 0 at the beginning of the diffusion chain - self.register_buffer('posterior_log_variance_clipped', to_torch(np.log(np.maximum(posterior_variance, 1e-20)))) - self.register_buffer('posterior_mean_coef1', to_torch( - betas * np.sqrt(alphas_cumprod_prev) / (1. - alphas_cumprod))) - self.register_buffer('posterior_mean_coef2', to_torch( - (1. - alphas_cumprod_prev) * np.sqrt(alphas) / (1. - alphas_cumprod))) - - if self.parameterization == "eps": - lvlb_weights = self.betas ** 2 / ( - 2 * self.posterior_variance * to_torch(alphas) * (1 - self.alphas_cumprod)) - elif self.parameterization == "x0": - lvlb_weights = 0.5 * np.sqrt(torch.Tensor(alphas_cumprod)) / (2. * 1 - torch.Tensor(alphas_cumprod)) - else: - raise NotImplementedError("mu not supported") - lvlb_weights[0] = lvlb_weights[1] - self.register_buffer('lvlb_weights', lvlb_weights, persistent=False) - assert not torch.isnan(self.lvlb_weights).all() - - @contextmanager - def ema_scope(self, context=None): - if self.use_ema: - self.model_ema.store(self.model.parameters()) - self.model_ema.copy_to(self.model) - if context is not None: - print(f"{context}: Switched to EMA weights") - try: - yield None - finally: - if self.use_ema: - self.model_ema.restore(self.model.parameters()) - if context is not None: - print(f"{context}: Restored training weights") - - def init_from_ckpt(self, path, ignore_keys=None, only_model=False): - sd = torch.load(path, map_location="cpu") - if "state_dict" in list(sd.keys()): - sd = sd["state_dict"] - keys = list(sd.keys()) - for k in keys: - for ik in ignore_keys or []: - if k.startswith(ik): - print("Deleting key {} from state_dict.".format(k)) - del sd[k] - missing, unexpected = self.load_state_dict(sd, strict=False) if not only_model else self.model.load_state_dict( - sd, strict=False) - print(f"Restored from {path} with {len(missing)} missing and {len(unexpected)} unexpected keys") - if len(missing) > 0: - print(f"Missing Keys: {missing}") - if len(unexpected) > 0: - print(f"Unexpected Keys: {unexpected}") - - def q_mean_variance(self, x_start, t): - """ - Get the distribution q(x_t | x_0). - :param x_start: the [N x C x ...] tensor of noiseless inputs. - :param t: the number of diffusion steps (minus 1). Here, 0 means one step. - :return: A tuple (mean, variance, log_variance), all of x_start's shape. - """ - mean = (extract_into_tensor(self.sqrt_alphas_cumprod, t, x_start.shape) * x_start) - variance = extract_into_tensor(1.0 - self.alphas_cumprod, t, x_start.shape) - log_variance = extract_into_tensor(self.log_one_minus_alphas_cumprod, t, x_start.shape) - return mean, variance, log_variance - - def predict_start_from_noise(self, x_t, t, noise): - return ( - extract_into_tensor(self.sqrt_recip_alphas_cumprod, t, x_t.shape) * x_t - - extract_into_tensor(self.sqrt_recipm1_alphas_cumprod, t, x_t.shape) * noise - ) - - def q_posterior(self, x_start, x_t, t): - posterior_mean = ( - extract_into_tensor(self.posterior_mean_coef1, t, x_t.shape) * x_start + - extract_into_tensor(self.posterior_mean_coef2, t, x_t.shape) * x_t - ) - posterior_variance = extract_into_tensor(self.posterior_variance, t, x_t.shape) - posterior_log_variance_clipped = extract_into_tensor(self.posterior_log_variance_clipped, t, x_t.shape) - return posterior_mean, posterior_variance, posterior_log_variance_clipped - - def p_mean_variance(self, x, t, clip_denoised: bool): - model_out = self.model(x, t) - if self.parameterization == "eps": - x_recon = self.predict_start_from_noise(x, t=t, noise=model_out) - elif self.parameterization == "x0": - x_recon = model_out - if clip_denoised: - x_recon.clamp_(-1., 1.) - - model_mean, posterior_variance, posterior_log_variance = self.q_posterior(x_start=x_recon, x_t=x, t=t) - return model_mean, posterior_variance, posterior_log_variance - - @torch.no_grad() - def p_sample(self, x, t, clip_denoised=True, repeat_noise=False): - b, *_, device = *x.shape, x.device - model_mean, _, model_log_variance = self.p_mean_variance(x=x, t=t, clip_denoised=clip_denoised) - noise = noise_like(x.shape, device, repeat_noise) - # no noise when t == 0 - nonzero_mask = (1 - (t == 0).float()).reshape(b, *((1,) * (len(x.shape) - 1))) - return model_mean + nonzero_mask * (0.5 * model_log_variance).exp() * noise - - @torch.no_grad() - def p_sample_loop(self, shape, return_intermediates=False): - device = self.betas.device - b = shape[0] - img = torch.randn(shape, device=device) - intermediates = [img] - for i in tqdm(reversed(range(0, self.num_timesteps)), desc='Sampling t', total=self.num_timesteps): - img = self.p_sample(img, torch.full((b,), i, device=device, dtype=torch.long), - clip_denoised=self.clip_denoised) - if i % self.log_every_t == 0 or i == self.num_timesteps - 1: - intermediates.append(img) - if return_intermediates: - return img, intermediates - return img - - @torch.no_grad() - def sample(self, batch_size=16, return_intermediates=False): - image_size = self.image_size - channels = self.channels - return self.p_sample_loop((batch_size, channels, image_size, image_size), - return_intermediates=return_intermediates) - - def q_sample(self, x_start, t, noise=None): - noise = default(noise, lambda: torch.randn_like(x_start)) - return (extract_into_tensor(self.sqrt_alphas_cumprod, t, x_start.shape) * x_start + - extract_into_tensor(self.sqrt_one_minus_alphas_cumprod, t, x_start.shape) * noise) - - def get_loss(self, pred, target, mean=True): - if self.loss_type == 'l1': - loss = (target - pred).abs() - if mean: - loss = loss.mean() - elif self.loss_type == 'l2': - if mean: - loss = torch.nn.functional.mse_loss(target, pred) - else: - loss = torch.nn.functional.mse_loss(target, pred, reduction='none') - else: - raise NotImplementedError("unknown loss type '{loss_type}'") - - return loss - - def p_losses(self, x_start, t, noise=None): - noise = default(noise, lambda: torch.randn_like(x_start)) - x_noisy = self.q_sample(x_start=x_start, t=t, noise=noise) - model_out = self.model(x_noisy, t) - - loss_dict = {} - if self.parameterization == "eps": - target = noise - elif self.parameterization == "x0": - target = x_start - else: - raise NotImplementedError(f"Paramterization {self.parameterization} not yet supported") - - loss = self.get_loss(model_out, target, mean=False).mean(dim=[1, 2, 3]) - - log_prefix = 'train' if self.training else 'val' - - loss_dict.update({f'{log_prefix}/loss_simple': loss.mean()}) - loss_simple = loss.mean() * self.l_simple_weight - - loss_vlb = (self.lvlb_weights[t] * loss).mean() - loss_dict.update({f'{log_prefix}/loss_vlb': loss_vlb}) - - loss = loss_simple + self.original_elbo_weight * loss_vlb - - loss_dict.update({f'{log_prefix}/loss': loss}) - - return loss, loss_dict - - def forward(self, x, *args, **kwargs): - # b, c, h, w, device, img_size, = *x.shape, x.device, self.image_size - # assert h == img_size and w == img_size, f'height and width of image must be {img_size}' - t = torch.randint(0, self.num_timesteps, (x.shape[0],), device=self.device).long() - return self.p_losses(x, t, *args, **kwargs) - - def get_input(self, batch, k): - x = batch[k] - if len(x.shape) == 3: - x = x[..., None] - x = rearrange(x, 'b h w c -> b c h w') - x = x.to(memory_format=torch.contiguous_format).float() - return x - - def shared_step(self, batch): - x = self.get_input(batch, self.first_stage_key) - loss, loss_dict = self(x) - return loss, loss_dict - - def training_step(self, batch, batch_idx): - loss, loss_dict = self.shared_step(batch) - - self.log_dict(loss_dict, prog_bar=True, - logger=True, on_step=True, on_epoch=True) - - self.log("global_step", self.global_step, - prog_bar=True, logger=True, on_step=True, on_epoch=False) - - if self.use_scheduler: - lr = self.optimizers().param_groups[0]['lr'] - self.log('lr_abs', lr, prog_bar=True, logger=True, on_step=True, on_epoch=False) - - return loss - - @torch.no_grad() - def validation_step(self, batch, batch_idx): - _, loss_dict_no_ema = self.shared_step(batch) - with self.ema_scope(): - _, loss_dict_ema = self.shared_step(batch) - loss_dict_ema = {key + '_ema': loss_dict_ema[key] for key in loss_dict_ema} - self.log_dict(loss_dict_no_ema, prog_bar=False, logger=True, on_step=False, on_epoch=True) - self.log_dict(loss_dict_ema, prog_bar=False, logger=True, on_step=False, on_epoch=True) - - def on_train_batch_end(self, *args, **kwargs): - if self.use_ema: - self.model_ema(self.model) - - def _get_rows_from_list(self, samples): - n_imgs_per_row = len(samples) - denoise_grid = rearrange(samples, 'n b c h w -> b n c h w') - denoise_grid = rearrange(denoise_grid, 'b n c h w -> (b n) c h w') - denoise_grid = make_grid(denoise_grid, nrow=n_imgs_per_row) - return denoise_grid - - @torch.no_grad() - def log_images(self, batch, N=8, n_row=2, sample=True, return_keys=None, **kwargs): - log = {} - x = self.get_input(batch, self.first_stage_key) - N = min(x.shape[0], N) - n_row = min(x.shape[0], n_row) - x = x.to(self.device)[:N] - log["inputs"] = x - - # get diffusion row - diffusion_row = [] - x_start = x[:n_row] - - for t in range(self.num_timesteps): - if t % self.log_every_t == 0 or t == self.num_timesteps - 1: - t = repeat(torch.tensor([t]), '1 -> b', b=n_row) - t = t.to(self.device).long() - noise = torch.randn_like(x_start) - x_noisy = self.q_sample(x_start=x_start, t=t, noise=noise) - diffusion_row.append(x_noisy) - - log["diffusion_row"] = self._get_rows_from_list(diffusion_row) - - if sample: - # get denoise row - with self.ema_scope("Plotting"): - samples, denoise_row = self.sample(batch_size=N, return_intermediates=True) - - log["samples"] = samples - log["denoise_row"] = self._get_rows_from_list(denoise_row) - - if return_keys: - if np.intersect1d(list(log.keys()), return_keys).shape[0] == 0: - return log - else: - return {key: log[key] for key in return_keys} - return log - - def configure_optimizers(self): - lr = self.learning_rate - params = list(self.model.parameters()) - if self.learn_logvar: - params = params + [self.logvar] - opt = torch.optim.AdamW(params, lr=lr) - return opt - - -class LatentDiffusionV1(DDPMV1): - """main class""" - def __init__(self, - first_stage_config, - cond_stage_config, - num_timesteps_cond=None, - cond_stage_key="image", - cond_stage_trainable=False, - concat_mode=True, - cond_stage_forward=None, - conditioning_key=None, - scale_factor=1.0, - scale_by_std=False, - *args, **kwargs): - self.num_timesteps_cond = default(num_timesteps_cond, 1) - self.scale_by_std = scale_by_std - assert self.num_timesteps_cond <= kwargs['timesteps'] - # for backwards compatibility after implementation of DiffusionWrapper - if conditioning_key is None: - conditioning_key = 'concat' if concat_mode else 'crossattn' - if cond_stage_config == '__is_unconditional__': - conditioning_key = None - ckpt_path = kwargs.pop("ckpt_path", None) - ignore_keys = kwargs.pop("ignore_keys", []) - super().__init__(*args, conditioning_key=conditioning_key, **kwargs) - self.concat_mode = concat_mode - self.cond_stage_trainable = cond_stage_trainable - self.cond_stage_key = cond_stage_key - try: - self.num_downs = len(first_stage_config.params.ddconfig.ch_mult) - 1 - except Exception: - self.num_downs = 0 - if not scale_by_std: - self.scale_factor = scale_factor - else: - self.register_buffer('scale_factor', torch.tensor(scale_factor)) - self.instantiate_first_stage(first_stage_config) - self.instantiate_cond_stage(cond_stage_config) - self.cond_stage_forward = cond_stage_forward - self.clip_denoised = False - self.bbox_tokenizer = None - - self.restarted_from_ckpt = False - if ckpt_path is not None: - self.init_from_ckpt(ckpt_path, ignore_keys) - self.restarted_from_ckpt = True - - def make_cond_schedule(self, ): - self.cond_ids = torch.full(size=(self.num_timesteps,), fill_value=self.num_timesteps - 1, dtype=torch.long) - ids = torch.round(torch.linspace(0, self.num_timesteps - 1, self.num_timesteps_cond)).long() - self.cond_ids[:self.num_timesteps_cond] = ids - - @rank_zero_only - @torch.no_grad() - def on_train_batch_start(self, batch, batch_idx, dataloader_idx): - # only for very first batch - if self.scale_by_std and self.current_epoch == 0 and self.global_step == 0 and batch_idx == 0 and not self.restarted_from_ckpt: - assert self.scale_factor == 1., 'rather not use custom rescaling and std-rescaling simultaneously' - # set rescale weight to 1./std of encodings - print("### USING STD-RESCALING ###") - x = super().get_input(batch, self.first_stage_key) - x = x.to(self.device) - encoder_posterior = self.encode_first_stage(x) - z = self.get_first_stage_encoding(encoder_posterior).detach() - del self.scale_factor - self.register_buffer('scale_factor', 1. / z.flatten().std()) - print(f"setting self.scale_factor to {self.scale_factor}") - print("### USING STD-RESCALING ###") - - def register_schedule(self, - given_betas=None, beta_schedule="linear", timesteps=1000, - linear_start=1e-4, linear_end=2e-2, cosine_s=8e-3): - super().register_schedule(given_betas, beta_schedule, timesteps, linear_start, linear_end, cosine_s) - - self.shorten_cond_schedule = self.num_timesteps_cond > 1 - if self.shorten_cond_schedule: - self.make_cond_schedule() - - def instantiate_first_stage(self, config): - model = instantiate_from_config(config) - self.first_stage_model = model.eval() - self.first_stage_model.train = disabled_train - for param in self.first_stage_model.parameters(): - param.requires_grad = False - - def instantiate_cond_stage(self, config): - if not self.cond_stage_trainable: - if config == "__is_first_stage__": - print("Using first stage also as cond stage.") - self.cond_stage_model = self.first_stage_model - elif config == "__is_unconditional__": - print(f"Training {self.__class__.__name__} as an unconditional model.") - self.cond_stage_model = None - # self.be_unconditional = True - else: - model = instantiate_from_config(config) - self.cond_stage_model = model.eval() - self.cond_stage_model.train = disabled_train - for param in self.cond_stage_model.parameters(): - param.requires_grad = False - else: - assert config != '__is_first_stage__' - assert config != '__is_unconditional__' - model = instantiate_from_config(config) - self.cond_stage_model = model - - def _get_denoise_row_from_list(self, samples, desc='', force_no_decoder_quantization=False): - denoise_row = [] - for zd in tqdm(samples, desc=desc): - denoise_row.append(self.decode_first_stage(zd.to(self.device), - force_not_quantize=force_no_decoder_quantization)) - n_imgs_per_row = len(denoise_row) - denoise_row = torch.stack(denoise_row) # n_log_step, n_row, C, H, W - denoise_grid = rearrange(denoise_row, 'n b c h w -> b n c h w') - denoise_grid = rearrange(denoise_grid, 'b n c h w -> (b n) c h w') - denoise_grid = make_grid(denoise_grid, nrow=n_imgs_per_row) - return denoise_grid - - def get_first_stage_encoding(self, encoder_posterior): - if isinstance(encoder_posterior, DiagonalGaussianDistribution): - z = encoder_posterior.sample() - elif isinstance(encoder_posterior, torch.Tensor): - z = encoder_posterior - else: - raise NotImplementedError(f"encoder_posterior of type '{type(encoder_posterior)}' not yet implemented") - return self.scale_factor * z - - def get_learned_conditioning(self, c): - if self.cond_stage_forward is None: - if hasattr(self.cond_stage_model, 'encode') and callable(self.cond_stage_model.encode): - c = self.cond_stage_model.encode(c) - if isinstance(c, DiagonalGaussianDistribution): - c = c.mode() - else: - c = self.cond_stage_model(c) - else: - assert hasattr(self.cond_stage_model, self.cond_stage_forward) - c = getattr(self.cond_stage_model, self.cond_stage_forward)(c) - return c - - def meshgrid(self, h, w): - y = torch.arange(0, h).view(h, 1, 1).repeat(1, w, 1) - x = torch.arange(0, w).view(1, w, 1).repeat(h, 1, 1) - - arr = torch.cat([y, x], dim=-1) - return arr - - def delta_border(self, h, w): - """ - :param h: height - :param w: width - :return: normalized distance to image border, - wtith min distance = 0 at border and max dist = 0.5 at image center - """ - lower_right_corner = torch.tensor([h - 1, w - 1]).view(1, 1, 2) - arr = self.meshgrid(h, w) / lower_right_corner - dist_left_up = torch.min(arr, dim=-1, keepdims=True)[0] - dist_right_down = torch.min(1 - arr, dim=-1, keepdims=True)[0] - edge_dist = torch.min(torch.cat([dist_left_up, dist_right_down], dim=-1), dim=-1)[0] - return edge_dist - - def get_weighting(self, h, w, Ly, Lx, device): - weighting = self.delta_border(h, w) - weighting = torch.clip(weighting, self.split_input_params["clip_min_weight"], - self.split_input_params["clip_max_weight"], ) - weighting = weighting.view(1, h * w, 1).repeat(1, 1, Ly * Lx).to(device) - - if self.split_input_params["tie_braker"]: - L_weighting = self.delta_border(Ly, Lx) - L_weighting = torch.clip(L_weighting, - self.split_input_params["clip_min_tie_weight"], - self.split_input_params["clip_max_tie_weight"]) - - L_weighting = L_weighting.view(1, 1, Ly * Lx).to(device) - weighting = weighting * L_weighting - return weighting - - def get_fold_unfold(self, x, kernel_size, stride, uf=1, df=1): # todo load once not every time, shorten code - """ - :param x: img of size (bs, c, h, w) - :return: n img crops of size (n, bs, c, kernel_size[0], kernel_size[1]) - """ - bs, nc, h, w = x.shape - - # number of crops in image - Ly = (h - kernel_size[0]) // stride[0] + 1 - Lx = (w - kernel_size[1]) // stride[1] + 1 - - if uf == 1 and df == 1: - fold_params = dict(kernel_size=kernel_size, dilation=1, padding=0, stride=stride) - unfold = torch.nn.Unfold(**fold_params) - - fold = torch.nn.Fold(output_size=x.shape[2:], **fold_params) - - weighting = self.get_weighting(kernel_size[0], kernel_size[1], Ly, Lx, x.device).to(x.dtype) - normalization = fold(weighting).view(1, 1, h, w) # normalizes the overlap - weighting = weighting.view((1, 1, kernel_size[0], kernel_size[1], Ly * Lx)) - - elif uf > 1 and df == 1: - fold_params = dict(kernel_size=kernel_size, dilation=1, padding=0, stride=stride) - unfold = torch.nn.Unfold(**fold_params) - - fold_params2 = dict(kernel_size=(kernel_size[0] * uf, kernel_size[0] * uf), - dilation=1, padding=0, - stride=(stride[0] * uf, stride[1] * uf)) - fold = torch.nn.Fold(output_size=(x.shape[2] * uf, x.shape[3] * uf), **fold_params2) - - weighting = self.get_weighting(kernel_size[0] * uf, kernel_size[1] * uf, Ly, Lx, x.device).to(x.dtype) - normalization = fold(weighting).view(1, 1, h * uf, w * uf) # normalizes the overlap - weighting = weighting.view((1, 1, kernel_size[0] * uf, kernel_size[1] * uf, Ly * Lx)) - - elif df > 1 and uf == 1: - fold_params = dict(kernel_size=kernel_size, dilation=1, padding=0, stride=stride) - unfold = torch.nn.Unfold(**fold_params) - - fold_params2 = dict(kernel_size=(kernel_size[0] // df, kernel_size[0] // df), - dilation=1, padding=0, - stride=(stride[0] // df, stride[1] // df)) - fold = torch.nn.Fold(output_size=(x.shape[2] // df, x.shape[3] // df), **fold_params2) - - weighting = self.get_weighting(kernel_size[0] // df, kernel_size[1] // df, Ly, Lx, x.device).to(x.dtype) - normalization = fold(weighting).view(1, 1, h // df, w // df) # normalizes the overlap - weighting = weighting.view((1, 1, kernel_size[0] // df, kernel_size[1] // df, Ly * Lx)) - - else: - raise NotImplementedError - - return fold, unfold, normalization, weighting - - @torch.no_grad() - def get_input(self, batch, k, return_first_stage_outputs=False, force_c_encode=False, - cond_key=None, return_original_cond=False, bs=None): - x = super().get_input(batch, k) - if bs is not None: - x = x[:bs] - x = x.to(self.device) - encoder_posterior = self.encode_first_stage(x) - z = self.get_first_stage_encoding(encoder_posterior).detach() - - if self.model.conditioning_key is not None: - if cond_key is None: - cond_key = self.cond_stage_key - if cond_key != self.first_stage_key: - if cond_key in ['caption', 'coordinates_bbox']: - xc = batch[cond_key] - elif cond_key == 'class_label': - xc = batch - else: - xc = super().get_input(batch, cond_key).to(self.device) - else: - xc = x - if not self.cond_stage_trainable or force_c_encode: - if isinstance(xc, dict) or isinstance(xc, list): - # import pudb; pudb.set_trace() - c = self.get_learned_conditioning(xc) - else: - c = self.get_learned_conditioning(xc.to(self.device)) - else: - c = xc - if bs is not None: - c = c[:bs] - - if self.use_positional_encodings: - pos_x, pos_y = self.compute_latent_shifts(batch) - ckey = __conditioning_keys__[self.model.conditioning_key] - c = {ckey: c, 'pos_x': pos_x, 'pos_y': pos_y} - - else: - c = None - xc = None - if self.use_positional_encodings: - pos_x, pos_y = self.compute_latent_shifts(batch) - c = {'pos_x': pos_x, 'pos_y': pos_y} - out = [z, c] - if return_first_stage_outputs: - xrec = self.decode_first_stage(z) - out.extend([x, xrec]) - if return_original_cond: - out.append(xc) - return out - - @torch.no_grad() - def decode_first_stage(self, z, predict_cids=False, force_not_quantize=False): - if predict_cids: - if z.dim() == 4: - z = torch.argmax(z.exp(), dim=1).long() - z = self.first_stage_model.quantize.get_codebook_entry(z, shape=None) - z = rearrange(z, 'b h w c -> b c h w').contiguous() - - z = 1. / self.scale_factor * z - - if hasattr(self, "split_input_params"): - if self.split_input_params["patch_distributed_vq"]: - ks = self.split_input_params["ks"] # eg. (128, 128) - stride = self.split_input_params["stride"] # eg. (64, 64) - uf = self.split_input_params["vqf"] - bs, nc, h, w = z.shape - if ks[0] > h or ks[1] > w: - ks = (min(ks[0], h), min(ks[1], w)) - print("reducing Kernel") - - if stride[0] > h or stride[1] > w: - stride = (min(stride[0], h), min(stride[1], w)) - print("reducing stride") - - fold, unfold, normalization, weighting = self.get_fold_unfold(z, ks, stride, uf=uf) - - z = unfold(z) # (bn, nc * prod(**ks), L) - # 1. Reshape to img shape - z = z.view((z.shape[0], -1, ks[0], ks[1], z.shape[-1])) # (bn, nc, ks[0], ks[1], L ) - - # 2. apply model loop over last dim - if isinstance(self.first_stage_model, VQModelInterface): - output_list = [self.first_stage_model.decode(z[:, :, :, :, i], - force_not_quantize=predict_cids or force_not_quantize) - for i in range(z.shape[-1])] - else: - - output_list = [self.first_stage_model.decode(z[:, :, :, :, i]) - for i in range(z.shape[-1])] - - o = torch.stack(output_list, axis=-1) # # (bn, nc, ks[0], ks[1], L) - o = o * weighting - # Reverse 1. reshape to img shape - o = o.view((o.shape[0], -1, o.shape[-1])) # (bn, nc * ks[0] * ks[1], L) - # stitch crops together - decoded = fold(o) - decoded = decoded / normalization # norm is shape (1, 1, h, w) - return decoded - else: - if isinstance(self.first_stage_model, VQModelInterface): - return self.first_stage_model.decode(z, force_not_quantize=predict_cids or force_not_quantize) - else: - return self.first_stage_model.decode(z) - - else: - if isinstance(self.first_stage_model, VQModelInterface): - return self.first_stage_model.decode(z, force_not_quantize=predict_cids or force_not_quantize) - else: - return self.first_stage_model.decode(z) - - # same as above but without decorator - def differentiable_decode_first_stage(self, z, predict_cids=False, force_not_quantize=False): - if predict_cids: - if z.dim() == 4: - z = torch.argmax(z.exp(), dim=1).long() - z = self.first_stage_model.quantize.get_codebook_entry(z, shape=None) - z = rearrange(z, 'b h w c -> b c h w').contiguous() - - z = 1. / self.scale_factor * z - - if hasattr(self, "split_input_params"): - if self.split_input_params["patch_distributed_vq"]: - ks = self.split_input_params["ks"] # eg. (128, 128) - stride = self.split_input_params["stride"] # eg. (64, 64) - uf = self.split_input_params["vqf"] - bs, nc, h, w = z.shape - if ks[0] > h or ks[1] > w: - ks = (min(ks[0], h), min(ks[1], w)) - print("reducing Kernel") - - if stride[0] > h or stride[1] > w: - stride = (min(stride[0], h), min(stride[1], w)) - print("reducing stride") - - fold, unfold, normalization, weighting = self.get_fold_unfold(z, ks, stride, uf=uf) - - z = unfold(z) # (bn, nc * prod(**ks), L) - # 1. Reshape to img shape - z = z.view((z.shape[0], -1, ks[0], ks[1], z.shape[-1])) # (bn, nc, ks[0], ks[1], L ) - - # 2. apply model loop over last dim - if isinstance(self.first_stage_model, VQModelInterface): - output_list = [self.first_stage_model.decode(z[:, :, :, :, i], - force_not_quantize=predict_cids or force_not_quantize) - for i in range(z.shape[-1])] - else: - - output_list = [self.first_stage_model.decode(z[:, :, :, :, i]) - for i in range(z.shape[-1])] - - o = torch.stack(output_list, axis=-1) # # (bn, nc, ks[0], ks[1], L) - o = o * weighting - # Reverse 1. reshape to img shape - o = o.view((o.shape[0], -1, o.shape[-1])) # (bn, nc * ks[0] * ks[1], L) - # stitch crops together - decoded = fold(o) - decoded = decoded / normalization # norm is shape (1, 1, h, w) - return decoded - else: - if isinstance(self.first_stage_model, VQModelInterface): - return self.first_stage_model.decode(z, force_not_quantize=predict_cids or force_not_quantize) - else: - return self.first_stage_model.decode(z) - - else: - if isinstance(self.first_stage_model, VQModelInterface): - return self.first_stage_model.decode(z, force_not_quantize=predict_cids or force_not_quantize) - else: - return self.first_stage_model.decode(z) - - @torch.no_grad() - def encode_first_stage(self, x): - if hasattr(self, "split_input_params"): - if self.split_input_params["patch_distributed_vq"]: - ks = self.split_input_params["ks"] # eg. (128, 128) - stride = self.split_input_params["stride"] # eg. (64, 64) - df = self.split_input_params["vqf"] - self.split_input_params['original_image_size'] = x.shape[-2:] - bs, nc, h, w = x.shape - if ks[0] > h or ks[1] > w: - ks = (min(ks[0], h), min(ks[1], w)) - print("reducing Kernel") - - if stride[0] > h or stride[1] > w: - stride = (min(stride[0], h), min(stride[1], w)) - print("reducing stride") - - fold, unfold, normalization, weighting = self.get_fold_unfold(x, ks, stride, df=df) - z = unfold(x) # (bn, nc * prod(**ks), L) - # Reshape to img shape - z = z.view((z.shape[0], -1, ks[0], ks[1], z.shape[-1])) # (bn, nc, ks[0], ks[1], L ) - - output_list = [self.first_stage_model.encode(z[:, :, :, :, i]) - for i in range(z.shape[-1])] - - o = torch.stack(output_list, axis=-1) - o = o * weighting - - # Reverse reshape to img shape - o = o.view((o.shape[0], -1, o.shape[-1])) # (bn, nc * ks[0] * ks[1], L) - # stitch crops together - decoded = fold(o) - decoded = decoded / normalization - return decoded - - else: - return self.first_stage_model.encode(x) - else: - return self.first_stage_model.encode(x) - - def shared_step(self, batch, **kwargs): - x, c = self.get_input(batch, self.first_stage_key) - loss = self(x, c) - return loss - - def forward(self, x, c, *args, **kwargs): - t = torch.randint(0, self.num_timesteps, (x.shape[0],), device=self.device).long() - if self.model.conditioning_key is not None: - assert c is not None - if self.cond_stage_trainable: - c = self.get_learned_conditioning(c) - if self.shorten_cond_schedule: - tc = self.cond_ids[t].to(self.device) - c = self.q_sample(x_start=c, t=tc, noise=torch.randn_like(c.float())) - return self.p_losses(x, c, t, *args, **kwargs) - - def apply_model(self, x_noisy, t, cond, return_ids=False): - - if isinstance(cond, dict): - # hybrid case, cond is exptected to be a dict - pass - else: - if not isinstance(cond, list): - cond = [cond] - key = 'c_concat' if self.model.conditioning_key == 'concat' else 'c_crossattn' - cond = {key: cond} - - if hasattr(self, "split_input_params"): - assert len(cond) == 1 # todo can only deal with one conditioning atm - assert not return_ids - ks = self.split_input_params["ks"] # eg. (128, 128) - stride = self.split_input_params["stride"] # eg. (64, 64) - - h, w = x_noisy.shape[-2:] - - fold, unfold, normalization, weighting = self.get_fold_unfold(x_noisy, ks, stride) - - z = unfold(x_noisy) # (bn, nc * prod(**ks), L) - # Reshape to img shape - z = z.view((z.shape[0], -1, ks[0], ks[1], z.shape[-1])) # (bn, nc, ks[0], ks[1], L ) - z_list = [z[:, :, :, :, i] for i in range(z.shape[-1])] - - if self.cond_stage_key in ["image", "LR_image", "segmentation", - 'bbox_img'] and self.model.conditioning_key: # todo check for completeness - c_key = next(iter(cond.keys())) # get key - c = next(iter(cond.values())) # get value - assert (len(c) == 1) # todo extend to list with more than one elem - c = c[0] # get element - - c = unfold(c) - c = c.view((c.shape[0], -1, ks[0], ks[1], c.shape[-1])) # (bn, nc, ks[0], ks[1], L ) - - cond_list = [{c_key: [c[:, :, :, :, i]]} for i in range(c.shape[-1])] - - elif self.cond_stage_key == 'coordinates_bbox': - assert 'original_image_size' in self.split_input_params, 'BoudingBoxRescaling is missing original_image_size' - - # assuming padding of unfold is always 0 and its dilation is always 1 - n_patches_per_row = int((w - ks[0]) / stride[0] + 1) - full_img_h, full_img_w = self.split_input_params['original_image_size'] - # as we are operating on latents, we need the factor from the original image size to the - # spatial latent size to properly rescale the crops for regenerating the bbox annotations - num_downs = self.first_stage_model.encoder.num_resolutions - 1 - rescale_latent = 2 ** (num_downs) - - # get top left postions of patches as conforming for the bbbox tokenizer, therefore we - # need to rescale the tl patch coordinates to be in between (0,1) - tl_patch_coordinates = [(rescale_latent * stride[0] * (patch_nr % n_patches_per_row) / full_img_w, - rescale_latent * stride[1] * (patch_nr // n_patches_per_row) / full_img_h) - for patch_nr in range(z.shape[-1])] - - # patch_limits are tl_coord, width and height coordinates as (x_tl, y_tl, h, w) - patch_limits = [(x_tl, y_tl, - rescale_latent * ks[0] / full_img_w, - rescale_latent * ks[1] / full_img_h) for x_tl, y_tl in tl_patch_coordinates] - # patch_values = [(np.arange(x_tl,min(x_tl+ks, 1.)),np.arange(y_tl,min(y_tl+ks, 1.))) for x_tl, y_tl in tl_patch_coordinates] - - # tokenize crop coordinates for the bounding boxes of the respective patches - patch_limits_tknzd = [torch.LongTensor(self.bbox_tokenizer._crop_encoder(bbox))[None].to(self.device) - for bbox in patch_limits] # list of length l with tensors of shape (1, 2) - print(patch_limits_tknzd[0].shape) - # cut tknzd crop position from conditioning - assert isinstance(cond, dict), 'cond must be dict to be fed into model' - cut_cond = cond['c_crossattn'][0][..., :-2].to(self.device) - print(cut_cond.shape) - - adapted_cond = torch.stack([torch.cat([cut_cond, p], dim=1) for p in patch_limits_tknzd]) - adapted_cond = rearrange(adapted_cond, 'l b n -> (l b) n') - print(adapted_cond.shape) - adapted_cond = self.get_learned_conditioning(adapted_cond) - print(adapted_cond.shape) - adapted_cond = rearrange(adapted_cond, '(l b) n d -> l b n d', l=z.shape[-1]) - print(adapted_cond.shape) - - cond_list = [{'c_crossattn': [e]} for e in adapted_cond] - - else: - cond_list = [cond for i in range(z.shape[-1])] - - # apply model by loop over crops - output_list = [self.model(z_list[i], t, **cond_list[i]) for i in range(z.shape[-1])] - assert not isinstance(output_list[0], - tuple) # todo cant deal with multiple model outputs check this never happens - - o = torch.stack(output_list, axis=-1) - o = o * weighting - # Reverse reshape to img shape - o = o.view((o.shape[0], -1, o.shape[-1])) # (bn, nc * ks[0] * ks[1], L) - # stitch crops together - x_recon = fold(o) / normalization - - else: - x_recon = self.model(x_noisy, t, **cond) - - if isinstance(x_recon, tuple) and not return_ids: - return x_recon[0] - else: - return x_recon - - def _predict_eps_from_xstart(self, x_t, t, pred_xstart): - return (extract_into_tensor(self.sqrt_recip_alphas_cumprod, t, x_t.shape) * x_t - pred_xstart) / \ - extract_into_tensor(self.sqrt_recipm1_alphas_cumprod, t, x_t.shape) - - def _prior_bpd(self, x_start): - """ - Get the prior KL term for the variational lower-bound, measured in - bits-per-dim. - This term can't be optimized, as it only depends on the encoder. - :param x_start: the [N x C x ...] tensor of inputs. - :return: a batch of [N] KL values (in bits), one per batch element. - """ - batch_size = x_start.shape[0] - t = torch.tensor([self.num_timesteps - 1] * batch_size, device=x_start.device) - qt_mean, _, qt_log_variance = self.q_mean_variance(x_start, t) - kl_prior = normal_kl(mean1=qt_mean, logvar1=qt_log_variance, mean2=0.0, logvar2=0.0) - return mean_flat(kl_prior) / np.log(2.0) - - def p_losses(self, x_start, cond, t, noise=None): - noise = default(noise, lambda: torch.randn_like(x_start)) - x_noisy = self.q_sample(x_start=x_start, t=t, noise=noise) - model_output = self.apply_model(x_noisy, t, cond) - - loss_dict = {} - prefix = 'train' if self.training else 'val' - - if self.parameterization == "x0": - target = x_start - elif self.parameterization == "eps": - target = noise - else: - raise NotImplementedError - - loss_simple = self.get_loss(model_output, target, mean=False).mean([1, 2, 3]) - loss_dict.update({f'{prefix}/loss_simple': loss_simple.mean()}) - - logvar_t = self.logvar[t].to(self.device) - loss = loss_simple / torch.exp(logvar_t) + logvar_t - # loss = loss_simple / torch.exp(self.logvar) + self.logvar - if self.learn_logvar: - loss_dict.update({f'{prefix}/loss_gamma': loss.mean()}) - loss_dict.update({'logvar': self.logvar.data.mean()}) - - loss = self.l_simple_weight * loss.mean() - - loss_vlb = self.get_loss(model_output, target, mean=False).mean(dim=(1, 2, 3)) - loss_vlb = (self.lvlb_weights[t] * loss_vlb).mean() - loss_dict.update({f'{prefix}/loss_vlb': loss_vlb}) - loss += (self.original_elbo_weight * loss_vlb) - loss_dict.update({f'{prefix}/loss': loss}) - - return loss, loss_dict - - def p_mean_variance(self, x, c, t, clip_denoised: bool, return_codebook_ids=False, quantize_denoised=False, - return_x0=False, score_corrector=None, corrector_kwargs=None): - t_in = t - model_out = self.apply_model(x, t_in, c, return_ids=return_codebook_ids) - - if score_corrector is not None: - assert self.parameterization == "eps" - model_out = score_corrector.modify_score(self, model_out, x, t, c, **corrector_kwargs) - - if return_codebook_ids: - model_out, logits = model_out - - if self.parameterization == "eps": - x_recon = self.predict_start_from_noise(x, t=t, noise=model_out) - elif self.parameterization == "x0": - x_recon = model_out - else: - raise NotImplementedError - - if clip_denoised: - x_recon.clamp_(-1., 1.) - if quantize_denoised: - x_recon, _, [_, _, indices] = self.first_stage_model.quantize(x_recon) - model_mean, posterior_variance, posterior_log_variance = self.q_posterior(x_start=x_recon, x_t=x, t=t) - if return_codebook_ids: - return model_mean, posterior_variance, posterior_log_variance, logits - elif return_x0: - return model_mean, posterior_variance, posterior_log_variance, x_recon - else: - return model_mean, posterior_variance, posterior_log_variance - - @torch.no_grad() - def p_sample(self, x, c, t, clip_denoised=False, repeat_noise=False, - return_codebook_ids=False, quantize_denoised=False, return_x0=False, - temperature=1., noise_dropout=0., score_corrector=None, corrector_kwargs=None): - b, *_, device = *x.shape, x.device - outputs = self.p_mean_variance(x=x, c=c, t=t, clip_denoised=clip_denoised, - return_codebook_ids=return_codebook_ids, - quantize_denoised=quantize_denoised, - return_x0=return_x0, - score_corrector=score_corrector, corrector_kwargs=corrector_kwargs) - if return_codebook_ids: - raise DeprecationWarning("Support dropped.") - model_mean, _, model_log_variance, logits = outputs - elif return_x0: - model_mean, _, model_log_variance, x0 = outputs - else: - model_mean, _, model_log_variance = outputs - - noise = noise_like(x.shape, device, repeat_noise) * temperature - if noise_dropout > 0.: - noise = torch.nn.functional.dropout(noise, p=noise_dropout) - # no noise when t == 0 - nonzero_mask = (1 - (t == 0).float()).reshape(b, *((1,) * (len(x.shape) - 1))) - - if return_codebook_ids: - return model_mean + nonzero_mask * (0.5 * model_log_variance).exp() * noise, logits.argmax(dim=1) - if return_x0: - return model_mean + nonzero_mask * (0.5 * model_log_variance).exp() * noise, x0 - else: - return model_mean + nonzero_mask * (0.5 * model_log_variance).exp() * noise - - @torch.no_grad() - def progressive_denoising(self, cond, shape, verbose=True, callback=None, quantize_denoised=False, - img_callback=None, mask=None, x0=None, temperature=1., noise_dropout=0., - score_corrector=None, corrector_kwargs=None, batch_size=None, x_T=None, start_T=None, - log_every_t=None): - if not log_every_t: - log_every_t = self.log_every_t - timesteps = self.num_timesteps - if batch_size is not None: - b = batch_size if batch_size is not None else shape[0] - shape = [batch_size] + list(shape) - else: - b = batch_size = shape[0] - if x_T is None: - img = torch.randn(shape, device=self.device) - else: - img = x_T - intermediates = [] - if cond is not None: - if isinstance(cond, dict): - cond = {key: cond[key][:batch_size] if not isinstance(cond[key], list) else - [x[:batch_size] for x in cond[key]] for key in cond} - else: - cond = [c[:batch_size] for c in cond] if isinstance(cond, list) else cond[:batch_size] - - if start_T is not None: - timesteps = min(timesteps, start_T) - iterator = tqdm(reversed(range(0, timesteps)), desc='Progressive Generation', - total=timesteps) if verbose else reversed( - range(0, timesteps)) - if type(temperature) == float: - temperature = [temperature] * timesteps - - for i in iterator: - ts = torch.full((b,), i, device=self.device, dtype=torch.long) - if self.shorten_cond_schedule: - assert self.model.conditioning_key != 'hybrid' - tc = self.cond_ids[ts].to(cond.device) - cond = self.q_sample(x_start=cond, t=tc, noise=torch.randn_like(cond)) - - img, x0_partial = self.p_sample(img, cond, ts, - clip_denoised=self.clip_denoised, - quantize_denoised=quantize_denoised, return_x0=True, - temperature=temperature[i], noise_dropout=noise_dropout, - score_corrector=score_corrector, corrector_kwargs=corrector_kwargs) - if mask is not None: - assert x0 is not None - img_orig = self.q_sample(x0, ts) - img = img_orig * mask + (1. - mask) * img - - if i % log_every_t == 0 or i == timesteps - 1: - intermediates.append(x0_partial) - if callback: - callback(i) - if img_callback: - img_callback(img, i) - return img, intermediates - - @torch.no_grad() - def p_sample_loop(self, cond, shape, return_intermediates=False, - x_T=None, verbose=True, callback=None, timesteps=None, quantize_denoised=False, - mask=None, x0=None, img_callback=None, start_T=None, - log_every_t=None): - - if not log_every_t: - log_every_t = self.log_every_t - device = self.betas.device - b = shape[0] - if x_T is None: - img = torch.randn(shape, device=device) - else: - img = x_T - - intermediates = [img] - if timesteps is None: - timesteps = self.num_timesteps - - if start_T is not None: - timesteps = min(timesteps, start_T) - iterator = tqdm(reversed(range(0, timesteps)), desc='Sampling t', total=timesteps) if verbose else reversed( - range(0, timesteps)) - - if mask is not None: - assert x0 is not None - assert x0.shape[2:3] == mask.shape[2:3] # spatial size has to match - - for i in iterator: - ts = torch.full((b,), i, device=device, dtype=torch.long) - if self.shorten_cond_schedule: - assert self.model.conditioning_key != 'hybrid' - tc = self.cond_ids[ts].to(cond.device) - cond = self.q_sample(x_start=cond, t=tc, noise=torch.randn_like(cond)) - - img = self.p_sample(img, cond, ts, - clip_denoised=self.clip_denoised, - quantize_denoised=quantize_denoised) - if mask is not None: - img_orig = self.q_sample(x0, ts) - img = img_orig * mask + (1. - mask) * img - - if i % log_every_t == 0 or i == timesteps - 1: - intermediates.append(img) - if callback: - callback(i) - if img_callback: - img_callback(img, i) - - if return_intermediates: - return img, intermediates - return img - - @torch.no_grad() - def sample(self, cond, batch_size=16, return_intermediates=False, x_T=None, - verbose=True, timesteps=None, quantize_denoised=False, - mask=None, x0=None, shape=None,**kwargs): - if shape is None: - shape = (batch_size, self.channels, self.image_size, self.image_size) - if cond is not None: - if isinstance(cond, dict): - cond = {key: cond[key][:batch_size] if not isinstance(cond[key], list) else - [x[:batch_size] for x in cond[key]] for key in cond} - else: - cond = [c[:batch_size] for c in cond] if isinstance(cond, list) else cond[:batch_size] - return self.p_sample_loop(cond, - shape, - return_intermediates=return_intermediates, x_T=x_T, - verbose=verbose, timesteps=timesteps, quantize_denoised=quantize_denoised, - mask=mask, x0=x0) - - @torch.no_grad() - def sample_log(self,cond,batch_size,ddim, ddim_steps,**kwargs): - - if ddim: - ddim_sampler = DDIMSampler(self) - shape = (self.channels, self.image_size, self.image_size) - samples, intermediates =ddim_sampler.sample(ddim_steps,batch_size, - shape,cond,verbose=False,**kwargs) - - else: - samples, intermediates = self.sample(cond=cond, batch_size=batch_size, - return_intermediates=True,**kwargs) - - return samples, intermediates - - - @torch.no_grad() - def log_images(self, batch, N=8, n_row=4, sample=True, ddim_steps=200, ddim_eta=1., return_keys=None, - quantize_denoised=True, inpaint=True, plot_denoise_rows=False, plot_progressive_rows=True, - plot_diffusion_rows=True, **kwargs): - - use_ddim = ddim_steps is not None - - log = {} - z, c, x, xrec, xc = self.get_input(batch, self.first_stage_key, - return_first_stage_outputs=True, - force_c_encode=True, - return_original_cond=True, - bs=N) - N = min(x.shape[0], N) - n_row = min(x.shape[0], n_row) - log["inputs"] = x - log["reconstruction"] = xrec - if self.model.conditioning_key is not None: - if hasattr(self.cond_stage_model, "decode"): - xc = self.cond_stage_model.decode(c) - log["conditioning"] = xc - elif self.cond_stage_key in ["caption"]: - xc = log_txt_as_img((x.shape[2], x.shape[3]), batch["caption"]) - log["conditioning"] = xc - elif self.cond_stage_key == 'class_label': - xc = log_txt_as_img((x.shape[2], x.shape[3]), batch["human_label"]) - log['conditioning'] = xc - elif isimage(xc): - log["conditioning"] = xc - if ismap(xc): - log["original_conditioning"] = self.to_rgb(xc) - - if plot_diffusion_rows: - # get diffusion row - diffusion_row = [] - z_start = z[:n_row] - for t in range(self.num_timesteps): - if t % self.log_every_t == 0 or t == self.num_timesteps - 1: - t = repeat(torch.tensor([t]), '1 -> b', b=n_row) - t = t.to(self.device).long() - noise = torch.randn_like(z_start) - z_noisy = self.q_sample(x_start=z_start, t=t, noise=noise) - diffusion_row.append(self.decode_first_stage(z_noisy)) - - diffusion_row = torch.stack(diffusion_row) # n_log_step, n_row, C, H, W - diffusion_grid = rearrange(diffusion_row, 'n b c h w -> b n c h w') - diffusion_grid = rearrange(diffusion_grid, 'b n c h w -> (b n) c h w') - diffusion_grid = make_grid(diffusion_grid, nrow=diffusion_row.shape[0]) - log["diffusion_row"] = diffusion_grid - - if sample: - # get denoise row - with self.ema_scope("Plotting"): - samples, z_denoise_row = self.sample_log(cond=c,batch_size=N,ddim=use_ddim, - ddim_steps=ddim_steps,eta=ddim_eta) - # samples, z_denoise_row = self.sample(cond=c, batch_size=N, return_intermediates=True) - x_samples = self.decode_first_stage(samples) - log["samples"] = x_samples - if plot_denoise_rows: - denoise_grid = self._get_denoise_row_from_list(z_denoise_row) - log["denoise_row"] = denoise_grid - - if quantize_denoised and not isinstance(self.first_stage_model, AutoencoderKL) and not isinstance( - self.first_stage_model, IdentityFirstStage): - # also display when quantizing x0 while sampling - with self.ema_scope("Plotting Quantized Denoised"): - samples, z_denoise_row = self.sample_log(cond=c,batch_size=N,ddim=use_ddim, - ddim_steps=ddim_steps,eta=ddim_eta, - quantize_denoised=True) - # samples, z_denoise_row = self.sample(cond=c, batch_size=N, return_intermediates=True, - # quantize_denoised=True) - x_samples = self.decode_first_stage(samples.to(self.device)) - log["samples_x0_quantized"] = x_samples - - if inpaint: - # make a simple center square - h, w = z.shape[2], z.shape[3] - mask = torch.ones(N, h, w).to(self.device) - # zeros will be filled in - mask[:, h // 4:3 * h // 4, w // 4:3 * w // 4] = 0. - mask = mask[:, None, ...] - with self.ema_scope("Plotting Inpaint"): - - samples, _ = self.sample_log(cond=c,batch_size=N,ddim=use_ddim, eta=ddim_eta, - ddim_steps=ddim_steps, x0=z[:N], mask=mask) - x_samples = self.decode_first_stage(samples.to(self.device)) - log["samples_inpainting"] = x_samples - log["mask"] = mask - - # outpaint - with self.ema_scope("Plotting Outpaint"): - samples, _ = self.sample_log(cond=c, batch_size=N, ddim=use_ddim,eta=ddim_eta, - ddim_steps=ddim_steps, x0=z[:N], mask=mask) - x_samples = self.decode_first_stage(samples.to(self.device)) - log["samples_outpainting"] = x_samples - - if plot_progressive_rows: - with self.ema_scope("Plotting Progressives"): - img, progressives = self.progressive_denoising(c, - shape=(self.channels, self.image_size, self.image_size), - batch_size=N) - prog_row = self._get_denoise_row_from_list(progressives, desc="Progressive Generation") - log["progressive_row"] = prog_row - - if return_keys: - if np.intersect1d(list(log.keys()), return_keys).shape[0] == 0: - return log - else: - return {key: log[key] for key in return_keys} - return log - - def configure_optimizers(self): - lr = self.learning_rate - params = list(self.model.parameters()) - if self.cond_stage_trainable: - print(f"{self.__class__.__name__}: Also optimizing conditioner params!") - params = params + list(self.cond_stage_model.parameters()) - if self.learn_logvar: - print('Diffusion model optimizing logvar') - params.append(self.logvar) - opt = torch.optim.AdamW(params, lr=lr) - if self.use_scheduler: - assert 'target' in self.scheduler_config - scheduler = instantiate_from_config(self.scheduler_config) - - print("Setting up LambdaLR scheduler...") - scheduler = [ - { - 'scheduler': LambdaLR(opt, lr_lambda=scheduler.schedule), - 'interval': 'step', - 'frequency': 1 - }] - return [opt], scheduler - return opt - - @torch.no_grad() - def to_rgb(self, x): - x = x.float() - if not hasattr(self, "colorize"): - self.colorize = torch.randn(3, x.shape[1], 1, 1).to(x) - x = nn.functional.conv2d(x, weight=self.colorize) - x = 2. * (x - x.min()) / (x.max() - x.min()) - 1. - return x - - -class DiffusionWrapperV1(pl.LightningModule): - def __init__(self, diff_model_config, conditioning_key): - super().__init__() - self.diffusion_model = instantiate_from_config(diff_model_config) - self.conditioning_key = conditioning_key - assert self.conditioning_key in [None, 'concat', 'crossattn', 'hybrid', 'adm'] - - def forward(self, x, t, c_concat: list = None, c_crossattn: list = None): - if self.conditioning_key is None: - out = self.diffusion_model(x, t) - elif self.conditioning_key == 'concat': - xc = torch.cat([x] + c_concat, dim=1) - out = self.diffusion_model(xc, t) - elif self.conditioning_key == 'crossattn': - cc = torch.cat(c_crossattn, 1) - out = self.diffusion_model(x, t, context=cc) - elif self.conditioning_key == 'hybrid': - xc = torch.cat([x] + c_concat, dim=1) - cc = torch.cat(c_crossattn, 1) - out = self.diffusion_model(xc, t, context=cc) - elif self.conditioning_key == 'adm': - cc = c_crossattn[0] - out = self.diffusion_model(x, t, y=cc) - else: - raise NotImplementedError - - return out - - -class Layout2ImgDiffusionV1(LatentDiffusionV1): - def __init__(self, cond_stage_key, *args, **kwargs): - assert cond_stage_key == 'coordinates_bbox', 'Layout2ImgDiffusion only for cond_stage_key="coordinates_bbox"' - super().__init__(*args, cond_stage_key=cond_stage_key, **kwargs) - - def log_images(self, batch, N=8, *args, **kwargs): - logs = super().log_images(*args, batch=batch, N=N, **kwargs) - - key = 'train' if self.training else 'validation' - dset = self.trainer.datamodule.datasets[key] - mapper = dset.conditional_builders[self.cond_stage_key] - - bbox_imgs = [] - map_fn = lambda catno: dset.get_textual_label(dset.get_category_id(catno)) - for tknzd_bbox in batch[self.cond_stage_key][:N]: - bboximg = mapper.plot(tknzd_bbox.detach().cpu(), map_fn, (256, 256)) - bbox_imgs.append(bboximg) - - cond_img = torch.stack(bbox_imgs, dim=0) - logs['bbox_image'] = cond_img - return logs - -ldm.models.diffusion.ddpm.DDPMV1 = DDPMV1 -ldm.models.diffusion.ddpm.LatentDiffusionV1 = LatentDiffusionV1 -ldm.models.diffusion.ddpm.DiffusionWrapperV1 = DiffusionWrapperV1 -ldm.models.diffusion.ddpm.Layout2ImgDiffusionV1 = Layout2ImgDiffusionV1 diff --git a/modules/model_quant.py b/modules/model_quant.py index 66c538f90..e5953688d 100644 --- a/modules/model_quant.py +++ b/modules/model_quant.py @@ -101,6 +101,25 @@ def create_quanto_config(kwargs = None, allow: bool = True, module: str = 'Model return kwargs +def get_sdnq_devices(): + from modules import devices, shared + if shared.opts.device_map == "gpu": + quantization_device = devices.device + return_device = devices.device + elif shared.opts.device_map == "cpu": + quantization_device = devices.cpu + return_device = devices.cpu + elif shared.opts.diffusers_offload_mode in {"none", "model"}: + quantization_device = devices.device if shared.opts.sdnq_quantize_with_gpu else devices.cpu + return_device = devices.device + elif shared.opts.sdnq_quantize_with_gpu: + quantization_device = devices.device + return_device = devices.device if shared.opts.diffusers_to_gpu else devices.cpu + else: + quantization_device = None + return_device = None + return quantization_device, return_device + def create_sdnq_config(kwargs = None, allow: bool = True, module: str = 'Model', weights_dtype: str = None, modules_to_not_convert: list = []): from modules import devices, shared if allow and (shared.opts.sdnq_quantize_mode in {'pre', 'auto'}) and (module == 'any' or module in shared.opts.sdnq_quantize_weights): @@ -118,18 +137,7 @@ def create_sdnq_config(kwargs = None, allow: bool = True, module: str = 'Model', if weights_dtype is None or weights_dtype == 'none': return kwargs - if shared.opts.device_map == "gpu": - quantization_device = devices.device - return_device = devices.device - elif shared.opts.diffusers_offload_mode in {"none", "model"}: - quantization_device = devices.device if shared.opts.sdnq_quantize_with_gpu else devices.cpu - return_device = devices.device - elif shared.opts.sdnq_quantize_with_gpu: - quantization_device = devices.device - return_device = devices.cpu - else: - quantization_device = None - return_device = None + quantization_device, return_device = get_sdnq_devices() sdnq_config = SDNQConfig( weights_dtype=weights_dtype, @@ -142,7 +150,7 @@ def create_sdnq_config(kwargs = None, allow: bool = True, module: str = 'Model', return_device=return_device, modules_to_not_convert=modules_to_not_convert, ) - log.debug(f'Quantization: module="{module}" type=sdnq dtype={weights_dtype} matmul={shared.opts.sdnq_use_quantized_matmul} group_size={shared.opts.sdnq_quantize_weights_group_size} quant_conv={shared.opts.sdnq_quantize_conv_layers} matmul_conv={shared.opts.sdnq_use_quantized_matmul_conv} dequantize_fp32={shared.opts.sdnq_dequantize_fp32} quantize_with_gpu={shared.opts.sdnq_quantize_with_gpu} quantization_device={quantization_device} return_device={return_device}') + log.debug(f'Quantization: module="{module}" type=sdnq dtype={weights_dtype} matmul={shared.opts.sdnq_use_quantized_matmul} group_size={shared.opts.sdnq_quantize_weights_group_size} quant_conv={shared.opts.sdnq_quantize_conv_layers} matmul_conv={shared.opts.sdnq_use_quantized_matmul_conv} dequantize_fp32={shared.opts.sdnq_dequantize_fp32} quantize_with_gpu={shared.opts.sdnq_quantize_with_gpu} quantization_device={quantization_device} return_device={return_device} device_map={shared.opts.device_map} offload_mode={shared.opts.diffusers_offload_mode}') if kwargs is None: return sdnq_config else: @@ -386,15 +394,7 @@ def sdnq_quantize_model(model, op=None, sd_model=None, do_gc: bool = True, weigh if debug: log.trace(f'Quantization: type=SDNQ op={op} cls={model.__class__} dtype={weights_dtype} mode{shared.opts.diffusers_offload_mode}') - if shared.opts.diffusers_offload_mode in {"none", "model"}: - quantization_device = devices.device if shared.opts.sdnq_quantize_with_gpu else devices.cpu - return_device = devices.device - elif shared.opts.sdnq_quantize_with_gpu: - quantization_device = devices.device - return_device = getattr(model, "device", devices.cpu) - else: - quantization_device = None - return_device = None + quantization_device, return_device = get_sdnq_devices() if getattr(model, "_keep_in_fp32_modules", None) is not None: modules_to_not_convert.extend(model._keep_in_fp32_modules) # pylint: disable=protected-access diff --git a/modules/postprocess/ldsr_model.py b/modules/postprocess/ldsr_model.py deleted file mode 100644 index 8abb7081a..000000000 --- a/modules/postprocess/ldsr_model.py +++ /dev/null @@ -1,75 +0,0 @@ -import os -import sys -import traceback -from modules.upscaler import Upscaler, UpscalerData -from modules import shared, script_callbacks - - -class Dummy: - pass - -cls = Upscaler if not shared.native else Dummy - -class UpscalerLDSR(cls): - def __init__(self, user_path): - self.name = "LDSR" - self.user_path = user_path - self.model_url = "https://heibox.uni-heidelberg.de/f/578df07c8fc04ffbadf3/?dl=1" - self.yaml_url = "https://heibox.uni-heidelberg.de/f/31a76b13ea27482981b4/?dl=1" - super().__init__() - scaler_data = UpscalerData("LDSR", None, self) - self.scalers = [scaler_data] - - def load_model(self, path: str): - from modules.ldsr.ldsr_model_arch import LDSR - import modules.ldsr.sd_hijack_autoencoder # pylint: disable=unused-import - import modules.ldsr.sd_hijack_ddpm_v1 # pylint: disable=unused-import - # Remove incorrect project.yaml file if too big - yaml_path = os.path.join(self.model_path, "project.yaml") - old_model_path = os.path.join(self.model_path, "model.pth") - new_model_path = os.path.join(self.model_path, "model.ckpt") - - local_model_paths = self.find_models(ext_filter=[".ckpt", ".safetensors"]) - local_ckpt_path = next(iter([local_model for local_model in local_model_paths if local_model.endswith("model.ckpt")]), None) - local_safetensors_path = next(iter([local_model for local_model in local_model_paths if local_model.endswith("model.safetensors")]), None) - local_yaml_path = next(iter([local_model for local_model in local_model_paths if local_model.endswith("project.yaml")]), None) - - if os.path.exists(yaml_path): - statinfo = os.stat(yaml_path) - if statinfo.st_size >= 10485760: - print("Removing invalid LDSR YAML file.") - os.remove(yaml_path) - - if os.path.exists(old_model_path): - print("Renaming model from model.pth to model.ckpt") - os.rename(old_model_path, new_model_path) - - from modules.modelloader import load_file_from_url - if local_safetensors_path is not None and os.path.exists(local_safetensors_path): - model = local_safetensors_path - else: - model = local_ckpt_path if local_ckpt_path is not None else load_file_from_url(url=self.model_url, model_dir=self.model_download_path, file_name="model.ckpt", progress=True) - - yaml = local_yaml_path if local_yaml_path is not None else load_file_from_url(url=self.yaml_url, model_dir=self.model_download_path, file_name="project.yaml", progress=True) - - try: - return LDSR(model, yaml) - except Exception: - print("Error importing LDSR:", file=sys.stderr) - print(traceback.format_exc(), file=sys.stderr) - return None - - def do_upscale(self, img, selected_model): - ldsr = self.load_model(selected_model) - if ldsr is None: - print("NO LDSR!") - return img - ddim_steps = shared.opts.ldsr_steps - return ldsr.super_resolution(img, ddim_steps, self.scale) - - -def on_ui_settings(): - import gradio as gr - shared.opts.add_option("ldsr_steps", shared.OptionInfo(100, "LDSR processing steps", gr.Slider, {"minimum": 1, "maximum": 200, "step": 1}, section=('postprocessing', "Postprocessing"))) - -script_callbacks.on_ui_settings(on_ui_settings) diff --git a/modules/shared.py b/modules/shared.py index 7df525310..ea368da49 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -145,7 +145,7 @@ options_templates.update(options_section(('sd', "Model Loading"), { "sd_checkpoint_autodownload": OptionInfo(True, "Model auto-download on demand"), "stream_load": OptionInfo(False, "Model load using streams", gr.Checkbox), "diffusers_to_gpu": OptionInfo(False, "Model load model direct to GPU"), - "diffusers_eval": OptionInfo(True, "Force model eval", gr.Checkbox, {"visible": True }), + "diffusers_eval": OptionInfo(False, "Force model eval", gr.Checkbox, {"visible": True }), "device_map": OptionInfo('default', "Model load device map", gr.Radio, {"choices": ['default', 'gpu', 'cpu'] }), "disable_accelerate": OptionInfo(False, "Disable accelerate", gr.Checkbox, {"visible": False }), "sd_checkpoint_cache": OptionInfo(0, "Cached models", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1, "visible": False }), @@ -424,7 +424,6 @@ options_templates.update(options_section(('system-paths', "System Paths"), { "realesrgan_models_path": OptionInfo(os.path.join(paths.models_path, 'RealESRGAN'), "Folder with RealESRGAN models", folder=True), "scunet_models_path": OptionInfo(os.path.join(paths.models_path, 'SCUNet'), "Folder with SCUNet models", folder=True), "swinir_models_path": OptionInfo(os.path.join(paths.models_path, 'SwinIR'), "Folder with SwinIR models", folder=True), - "ldsr_models_path": OptionInfo(os.path.join(paths.models_path, 'LDSR'), "Folder with LDSR models", folder=True), "clip_models_path": OptionInfo(os.path.join(paths.models_path, 'CLIP'), "Folder with CLIP models", folder=True), "other_paths_sep_options": OptionInfo("

Cache folders

", "", gr.HTML), "clean_temp_dir_at_start": OptionInfo(True, "Cleanup temporary folder on startup"), diff --git a/modules/shared_legacy.py b/modules/shared_legacy.py index 032220c23..b4f2e4c11 100644 --- a/modules/shared_legacy.py +++ b/modules/shared_legacy.py @@ -10,7 +10,8 @@ class LegacyOption(OptionInfo): legacy_options = options_section((None, "Legacy options"), { - "interrogate_clip_skip_categories": LegacyOption(["artists", "movements", "flavors"], "CLiP: skip categories", gr.CheckboxGroup, lambda: {"choices": []}, visible=False), + "ldsr_models_path": LegacyOption(os.path.join(paths.models_path, 'LDSR'), "LDSR Path", gr.Textbox, { "visible": False}), + "interrogate_clip_skip_categories": LegacyOption(["artists", "movements", "flavors"], "CLiP: skip categories", gr.CheckboxGroup, {"choices": [], "visible":False}), "lora_legacy": LegacyOption(False, "LoRA load using legacy method", gr.Checkbox, {"visible": False}), "lora_preferred_name": LegacyOption("filename", "LoRA preferred name", gr.Radio, {"choices": ["filename", "alias"], "visible": False}), "img2img_extra_noise": LegacyOption(0.0, "Extra noise multiplier for img2img", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01, "visible": False}), From ab8badfe0d6e92465f8f1a843387207d182a215b Mon Sep 17 00:00:00 2001 From: Disty0 Date: Wed, 6 Aug 2025 17:07:36 +0300 Subject: [PATCH 029/141] SDNQ use non-blocking ops --- modules/sdnq/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index ef3ef317f..16ac58f2e 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -153,7 +153,7 @@ def sdnq_quantize_layer(layer, weights_dtype="int8", torch_dtype=None, group_siz if return_device is None: return_device = layer.weight.device if quantization_device is not None: - layer.weight.data = layer.weight.to(quantization_device) + layer.weight.data = layer.weight.to(quantization_device, non_blocking=shared.opts.diffusers_offload_nonblocking) if layer.weight.dtype != torch.float32: layer.weight.data = layer.weight.to(dtype=torch.float32) @@ -184,8 +184,8 @@ def sdnq_quantize_layer(layer, weights_dtype="int8", torch_dtype=None, group_siz weights_dtype=weights_dtype, use_quantized_matmul=use_quantized_matmul, ) - layer.weight.data = layer.sdnq_dequantizer.pack_weight(layer.weight).to(return_device) - layer.sdnq_dequantizer = layer.sdnq_dequantizer.to(return_device) + layer.weight.data = layer.sdnq_dequantizer.pack_weight(layer.weight).to(return_device, non_blocking=shared.opts.diffusers_offload_nonblocking) + layer.sdnq_dequantizer = layer.sdnq_dequantizer.to(return_device, non_blocking=shared.opts.diffusers_offload_nonblocking) layer.forward = get_forward_func(layer_class_name, use_quantized_matmul, dtype_dict[weights_dtype]["is_integer"], use_tensorwise_fp8_matmul) layer.forward = layer.forward.__get__(layer, layer.__class__) @@ -295,7 +295,7 @@ class SDNQQuantizer(DiffusersQuantizer): if param_value.dtype == torch.float32 and devices.same_device(param_value.device, target_device): param_value = param_value.clone() else: - param_value = param_value.to(target_device).to(dtype=torch.float32) + param_value = param_value.to(target_device, non_blocking=shared.opts.diffusers_offload_nonblocking).to(dtype=torch.float32) layer, _ = get_module_from_name(model, param_name) layer.weight = torch.nn.Parameter(param_value, requires_grad=False) From 7bba30e7976e608a78910081912a36e00808aac7 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 6 Aug 2025 10:12:25 -0400 Subject: [PATCH 030/141] sdnq obey diffusers_to_gpu Signed-off-by: Vladimir Mandic --- modules/model_quant.py | 9 ++++++--- modules/sd_models.py | 37 +++++++++++++++++-------------------- modules/timer.py | 1 + 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/modules/model_quant.py b/modules/model_quant.py index e5953688d..f5bb9cef9 100644 --- a/modules/model_quant.py +++ b/modules/model_quant.py @@ -121,7 +121,7 @@ def get_sdnq_devices(): return quantization_device, return_device def create_sdnq_config(kwargs = None, allow: bool = True, module: str = 'Model', weights_dtype: str = None, modules_to_not_convert: list = []): - from modules import devices, shared + from modules import shared if allow and (shared.opts.sdnq_quantize_mode in {'pre', 'auto'}) and (module == 'any' or module in shared.opts.sdnq_quantize_weights): from modules.sdnq import SDNQQuantizer, SDNQConfig diffusers.quantizers.auto.AUTO_QUANTIZER_MAPPING["sdnq"] = SDNQQuantizer @@ -380,7 +380,7 @@ def apply_layerwise(sd_model, quiet:bool=False): def sdnq_quantize_model(model, op=None, sd_model=None, do_gc: bool = True, weights_dtype: str = None, modules_to_not_convert: list = []): global quant_last_model_name, quant_last_model_device # pylint: disable=global-statement - from modules import devices, shared + from modules import devices, shared, timer from modules.sdnq import apply_sdnq_to_module if weights_dtype is None: @@ -408,6 +408,7 @@ def sdnq_quantize_model(model, op=None, sd_model=None, do_gc: bool = True, weigh if hasattr(model, "get_input_embeddings"): backup_embeddings = copy.deepcopy(model.get_input_embeddings()) + t0 = time.time() model = apply_sdnq_to_module( model, weights_dtype=weights_dtype, @@ -422,6 +423,8 @@ def sdnq_quantize_model(model, op=None, sd_model=None, do_gc: bool = True, weigh param_name=op, modules_to_not_convert=modules_to_not_convert, ) + t1 = time.time() + timer.load.add('sdnq', t1 - t0) model.quantization_method = 'SDNQ' if hasattr(model, "set_input_embeddings") and backup_embeddings is not None: @@ -443,7 +446,7 @@ def sdnq_quantize_model(model, op=None, sd_model=None, do_gc: bool = True, weigh quant_last_model_name = None quant_last_model_device = None model.to(devices.device) - elif shared.opts.diffusers_offload_mode != "none": + elif (shared.opts.diffusers_offload_mode != "none") and (not shared.opts.diffusers_to_gpu): model = model.to(devices.cpu) if do_gc: devices.torch_gc(force=True, reason='sdnq') diff --git a/modules/sd_models.py b/modules/sd_models.py index 152ad0bb8..dc2a14390 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -10,8 +10,7 @@ import diffusers.loaders.single_file_utils import torch import huggingface_hub as hf from installer import log -from modules import paths, shared, shared_state, shared_items, modelloader, devices, script_callbacks, sd_vae, sd_unet, errors, sd_models_compile, sd_hijack_accelerate, sd_detect, model_quant, sd_hijack_te -from modules.timer import Timer, process as process_timer +from modules import timer, paths, shared, shared_state, shared_items, modelloader, devices, script_callbacks, sd_vae, sd_unet, errors, sd_models_compile, sd_hijack_accelerate, sd_detect, model_quant, sd_hijack_te from modules.memstats import memory_stats from modules.modeldata import model_data from modules.sd_checkpoint import CheckpointInfo, select_checkpoint, list_models, checkpoints_list, checkpoint_titles, get_closet_checkpoint_match, model_hash, update_model_hashes, setup_model, write_metadata, read_metadata_from_safetensors # pylint: disable=unused-import @@ -226,9 +225,9 @@ def move_model(model, device=None, force=False): except Exception as e1: t1 = time.time() shared.log.error(f'Model move: device={device} {e1}') - if 'move' not in process_timer.records: - process_timer.records['move'] = 0 - process_timer.records['move'] += t1 - t0 + if 'move' not in timer.process.records: + timer.process.records['move'] = 0 + timer.process.records['move'] += t1 - t0 if os.environ.get('SD_MOVE_DEBUG', None) is not None or (t1-t0) > 2: shared.log.debug(f'Model move: device={device} class={model.__class__.__name__} accelerate={getattr(model, "has_accelerate", False)} fn={fn} time={t1-t0:.2f}') # pylint: disable=protected-access devices.torch_gc() @@ -539,11 +538,9 @@ def set_defaults(sd_model, checkpoint_info): sd_model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining}', ncols=80, colour='#327fba') -def load_diffuser(checkpoint_info=None, timer=None, op='model', revision=None): # pylint: disable=unused-argument - if timer is None: - timer = Timer() +def load_diffuser(checkpoint_info=None, op='model', revision=None): # pylint: disable=unused-argument logging.getLogger("diffusers").setLevel(logging.ERROR) - timer.record("diffusers") + timer.load.record("diffusers") diffusers_load_config = { "low_cpu_mem_usage": True, "torch_dtype": devices.dtype, @@ -598,7 +595,7 @@ def load_diffuser(checkpoint_info=None, timer=None, op='model', revision=None): vae = sd_vae.load_vae_diffusers(checkpoint_info.path, vae_file, vae_source) if vae is not None: diffusers_load_config["vae"] = vae - timer.record("vae") + timer.load.record("vae") # load with custom loader if sd_model is None: @@ -632,7 +629,7 @@ def load_diffuser(checkpoint_info=None, timer=None, op='model', revision=None): add_noise_pred_to_diffusers_callback(sd_model) - timer.record("load") + timer.load.record("load") if op == 'refiner': model_data.sd_refiner = sd_model @@ -640,7 +637,7 @@ def load_diffuser(checkpoint_info=None, timer=None, op='model', revision=None): model_data.sd_model = sd_model reload_text_encoder(initial=True) # must be before embeddings - timer.record("te") + timer.load.record("te") if debug_load: shared.log.trace(f'Model components: {list(get_signature(sd_model).values())}') @@ -649,7 +646,7 @@ def load_diffuser(checkpoint_info=None, timer=None, op='model', revision=None): sd_model.embedding_db = textual_inversion.EmbeddingDatabase() sd_model.embedding_db.add_embedding_dir(shared.opts.embeddings_dir) sd_model.embedding_db.load_textual_inversion_embeddings(force_reload=True) - timer.record("embeddings") + timer.load.record("embeddings") from modules import prompt_parser_diffusers prompt_parser_diffusers.insert_parser_highjack(sd_model.__class__.__name__) @@ -657,7 +654,7 @@ def load_diffuser(checkpoint_info=None, timer=None, op='model', revision=None): set_diffuser_options(sd_model, vae, op, offload=False) sd_model = model_quant.do_post_load_quant(sd_model, allow=allow_post_quant) # run this before move model so it can be compressed in CPU - timer.record("options") + timer.load.record("options") set_diffuser_offload(sd_model, op) @@ -669,14 +666,14 @@ def load_diffuser(checkpoint_info=None, timer=None, op='model', revision=None): move_model(sd_model, devices.cpu) else: move_model(sd_model, devices.device) - timer.record("move") + timer.load.record("move") if shared.opts.ipex_optimize: sd_model = sd_models_compile.ipex_optimize(sd_model) if ('Model' in shared.opts.cuda_compile and shared.opts.cuda_compile_backend != 'none'): sd_model = sd_models_compile.compile_diffusers(sd_model) - timer.record("compile") + timer.load.record("compile") except Exception as e: shared.log.error(f"Load {op}: {e}") @@ -691,7 +688,7 @@ def load_diffuser(checkpoint_info=None, timer=None, op='model', revision=None): from modules import modelstats modelstats.analyze() - shared.log.info(f"Load {op}: family={shared.sd_model_type} time={timer.dct()} native={get_native(sd_model)} memory={memory_stats()}") + shared.log.info(f"Load {op}: family={shared.sd_model_type} time={timer.load.dct()} native={get_native(sd_model)} memory={memory_stats()}") class DiffusersTaskType(Enum): @@ -1071,12 +1068,12 @@ def reload_model_weights(sd_model=None, info=None, op='model', force=False, revi move_model(sd_model, devices.cpu) unload_model_weights(op=op) sd_model = None - timer = Timer() + timer.load = timer.Timer() # TODO model load: implement model in-memory caching - timer.record("config") + timer.load.record("config") if sd_model is None or force: sd_model = None - load_diffuser(checkpoint_info, timer=timer, op=op, revision=revision) + load_diffuser(checkpoint_info, op=op, revision=revision) shared.state.end() shared.state = orig_state if op == 'model': diff --git a/modules/timer.py b/modules/timer.py index 59c6a1de3..d94735902 100644 --- a/modules/timer.py +++ b/modules/timer.py @@ -72,3 +72,4 @@ startup = Timer() process = Timer() launch = Timer() init = Timer() +load = Timer() From 6a6605191f3f948a85167e9d57d3d6ebde384a19 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 6 Aug 2025 11:33:50 -0400 Subject: [PATCH 031/141] configurable image fit in all image views Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 5 +++-- html/locale_en.json | 1 + javascript/imageViewer.js | 12 ++++++++++++ javascript/sdnext.css | 24 ++++++++++++++++++++++-- modules/model_quant.py | 2 +- modules/ui_common.py | 2 ++ modules/ui_control.py | 2 +- modules/ui_symbols.py | 3 +-- 8 files changed, 43 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1276e1be..1cd14fb7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,12 +38,13 @@ And (*as always*) many bugfixes and improvements to existing features! - updated real-time hints, thanks @CalamitousFelicitousness - rewritten **CivitAI downloader** in *models -> civitai* + - quicksettings reset button to restore all quicksettings to default values + because things do sometimes get wrong... + - configurable image fit in all image views - updated *models -> current* tab - updated *models -> list models* tab - updated *models -> metadata* tab - updated *extensions* tab - - quicksettings reset button to restore all quicksettings to default values - because things do sometimes get wrong... - redesign *settings -> user interface* - gallery bypass browser cache for thumbnails - gallery safer delete operation diff --git a/html/locale_en.json b/html/locale_en.json index b222c2ed5..e908743eb 100644 --- a/html/locale_en.json +++ b/html/locale_en.json @@ -19,6 +19,7 @@ {"id":"","label":"🖌️","localized":"","hint":"LaMa remove selected object from image"}, {"id":"","label":"🖼️","localized":"","hint":"Show preview"}, {"id":"","label":"♻","localized":"","hint":"Interrogate image"}, + {"id":"","label":"⁜","localized":"","hint":"Cycle image fit method"}, {"id":"","label":"↶","localized":"","hint":"Apply selected style to prompt"}, {"id":"","label":"↷","localized":"","hint":"Save current prompt to style"}, {"id":"","label":"","localized":"","hint":"Sort by name, ascending"}, diff --git a/javascript/imageViewer.js b/javascript/imageViewer.js index c8a2dfd65..5631c173b 100644 --- a/javascript/imageViewer.js +++ b/javascript/imageViewer.js @@ -3,6 +3,18 @@ let previewDrag = false; let modalPreviewZone; let previewInstance; +function cycleImageFit() { + const root = document.documentElement; + const current = getComputedStyle(root).getPropertyValue('--sd-image-fit').trim(); + let next = 'contain'; + if (current === 'contain') next = 'cover'; + else if (current === 'cover') next = 'fill'; + else if (current === 'fill') next = 'scale-down'; + else if (current === 'scale-down') next = 'none'; + root.style.setProperty('--sd-image-fit', next); + log('cycleImageFit', current, next); +} + function closeModal(evt, force = false) { if (force) gradioApp().getElementById('lightboxModal').style.display = 'none'; if (previewDrag) return; diff --git a/javascript/sdnext.css b/javascript/sdnext.css index d68a1be8e..f5e2588e9 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -14,6 +14,7 @@ --color-trace: #666666; --color-warning: #FF9900; --left-column: 530px; + --sd-image-fit: contain; } a { @@ -515,17 +516,36 @@ color: var(--primary-500) !important color: var(--body-text-color-subdued) !important } +.gradio-gallery img, .image-container img { + max-width: 100%; + object-position: top; + width: 100%; + height: 100%; + object-fit: var(--sd-image-fit) !important; +} + .interrogate { background: none !important; font-size: 1.5em !important; max-width: fit-content; position: absolute; right: 2.8em; - top: 0.2em; + top: 0.1em; z-index: 50; } -.interrogate:hover { +.image-fit { + background: none !important; + font-size: 1.5em !important; + max-width: fit-content; + position: absolute; + right: 4.0em; + top: 0.1em; + z-index: 50; +} + +.interrogate:hover, +.image-fit:hover { background: var(--button-primary-background-fill-hover) !important; } diff --git a/modules/model_quant.py b/modules/model_quant.py index f5bb9cef9..5513bb127 100644 --- a/modules/model_quant.py +++ b/modules/model_quant.py @@ -150,7 +150,7 @@ def create_sdnq_config(kwargs = None, allow: bool = True, module: str = 'Model', return_device=return_device, modules_to_not_convert=modules_to_not_convert, ) - log.debug(f'Quantization: module="{module}" type=sdnq dtype={weights_dtype} matmul={shared.opts.sdnq_use_quantized_matmul} group_size={shared.opts.sdnq_quantize_weights_group_size} quant_conv={shared.opts.sdnq_quantize_conv_layers} matmul_conv={shared.opts.sdnq_use_quantized_matmul_conv} dequantize_fp32={shared.opts.sdnq_dequantize_fp32} quantize_with_gpu={shared.opts.sdnq_quantize_with_gpu} quantization_device={quantization_device} return_device={return_device} device_map={shared.opts.device_map} offload_mode={shared.opts.diffusers_offload_mode}') + log.debug(f'Quantization: module="{module}" type=sdnq dtype={weights_dtype} matmul={shared.opts.sdnq_use_quantized_matmul} group_size={shared.opts.sdnq_quantize_weights_group_size} quant_conv={shared.opts.sdnq_quantize_conv_layers} matmul_conv={shared.opts.sdnq_use_quantized_matmul_conv} dequantize_fp32={shared.opts.sdnq_dequantize_fp32} quantize_with_gpu={shared.opts.sdnq_quantize_with_gpu} quantization_device={quantization_device} return_device={return_device} device_map={shared.opts.device_map} offload_mode={shared.opts.diffusers_offload_mode} non_blocking={shared.opts.diffusers_offload_nonblocking}') if kwargs is None: return sdnq_config else: diff --git a/modules/ui_common.py b/modules/ui_common.py index 9df1a9ccb..494c2bc8a 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -247,6 +247,8 @@ def create_output_panel(tabname, preview=True, prompt=None, height=None, transfe ) if prompt is not None: ui_sections.create_interrogate_button(tab=tabname, inputs=result_gallery, outputs=prompt, what='output') + button_image_fit = gr.Button(ui_symbols.resize, elem_id=f"{tabname}_image_fit", elem_classes=['image-fit']) + button_image_fit.click(fn=None, _js="cycleImageFit", inputs=[], outputs=[]) with gr.Column(elem_id=f"{tabname}_footer", elem_classes="gallery_footer"): dummy_component = gr.Label(visible=False) diff --git a/modules/ui_control.py b/modules/ui_control.py index 05197127b..cfcd191ed 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -215,7 +215,7 @@ def create_ui(_blocks: gr.Blocks=None): gr.HTML('Output

') with gr.Tabs(elem_classes=['control-tabs'], elem_id='control-tab-output') as output_tabs: with gr.Tab('Gallery', id='out-gallery'): - output_gallery, _output_gen_info, _output_html_info, _output_html_info_formatted, output_html_log = ui_common.create_output_panel("control", preview=True, prompt=prompt, height=gr_height) + output_gallery, _output_gen_info, _output_html_info, _output_html_info_formatted, output_html_log = ui_common.create_output_panel("control", preview=False, prompt=prompt, height=gr_height) with gr.Tab('Image', id='out-image'): output_image = gr.Image(label="Output", show_label=False, type="pil", interactive=False, tool="editor", height=gr_height, elem_id='control_output_image', elem_classes=['control-image']) with gr.Tab('Video', id='out-video'): diff --git a/modules/ui_symbols.py b/modules/ui_symbols.py index 66fbda681..975cd4e69 100644 --- a/modules/ui_symbols.py +++ b/modules/ui_symbols.py @@ -25,8 +25,7 @@ reuse = '⬅️' search = '🔍' preview = '🖼️' image = '🖌️' -mark_diag = '※' -mark_flag = '⁜' +resize = '⁜' interrogate = '♻' int_clip = '✎' int_blip = '✐' From 992a41b0f147cb1d4881ec319f48f59b2fd0ec10 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 6 Aug 2025 11:37:14 -0400 Subject: [PATCH 032/141] update requirements Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + extensions-builtin/sdnext-modernui | 2 +- installer.py | 2 +- requirements.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cd14fb7a..0ed9de046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ And (*as always*) many bugfixes and improvements to existing features! in settings -> model options - prompt parser allow explict `BOS` and `EOS` tokens in prompt - **Nunchaku** support for *FLUX.1-Fill* and *FLUX.1-Depth* models + - update requirements/packages - **Fixes** - refactor legacy processing loop - fix Wan 2.2-5B I2V workflow diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index ff16330f7..874ff0e88 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit ff16330f72e38bc9478281a2e2eccabd9f8fa132 +Subproject commit 874ff0e88099703cde91ab3c5347a78d002aa3c6 diff --git a/installer.py b/installer.py index f5f692666..194e585d2 100644 --- a/installer.py +++ b/installer.py @@ -618,7 +618,7 @@ def check_transformers(): if args.use_directml: target = '4.52.4' else: - target = '4.54.1' + target = '4.55.0' if (pkg is None) or ((pkg.version != target) and (not args.experimental)): if pkg is None: log.info(f'Transformers install: version={target}') diff --git a/requirements.txt b/requirements.txt index 863edcccc..b27e6407d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ pi-heif # versioned rich==14.1.0 -safetensors==0.5.3 +safetensors==0.6.1 tensordict==0.8.3 peft==0.17.0 httpx==0.24.1 From 6207b6d84183d8fad68b7e255d3bd427ae4aeb37 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Wed, 6 Aug 2025 21:33:50 +0300 Subject: [PATCH 033/141] Update ROCm and OpenVINO to Torch 2.8 --- CHANGELOG.md | 2 ++ installer.py | 15 ++++++++------- modules/intel/ipex/int_mm.py | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed9de046..b43e60f2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ And (*as always*) many bugfixes and improvements to existing features! great model based on FLUX.1 and then redesigned and retrained by *lodestones* update with latest **v48**, **v48 Detail Calibrated** and **v46 Flash** variants available via *networks -> models -> reference* +**Torch** + - Set default for ROCm and OpenVINO to `torch==2.8.0` - **UI** - new embedded docs/wiki search! **Docs** search: fully-local and works in real-time on all document pages diff --git a/installer.py b/installer.py index 194e585d2..35795a430 100644 --- a/installer.py +++ b/installer.py @@ -738,13 +738,13 @@ def install_rocm_zluda(): if args.use_nightly: if rocm.version is None or float(rocm.version) >= 6.4: # assume the latest if version check fails torch_command = os.environ.get('TORCH_COMMAND', '--upgrade --pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm6.4') - elif rocm.version == "6.3": + else: # oldest rocm version on nightly is 6.3 torch_command = os.environ.get('TORCH_COMMAND', '--upgrade --pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm6.3') - else: # oldest rocm version on nightly is 6.2.4 - torch_command = os.environ.get('TORCH_COMMAND', '--upgrade --pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm6.2.4') else: - if rocm.version is None or float(rocm.version) >= 6.3: # assume the latest if version check fails - torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.7.1+rocm6.3 torchvision==0.22.1+rocm6.3 --index-url https://download.pytorch.org/whl/rocm6.3') + if rocm.version is None or float(rocm.version) >= 6.4: # assume the latest if version check fails + torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.8.0+rocm6.4 torchvision==0.23.0+rocm6.4 --index-url https://download.pytorch.org/whl/rocm6.4') + elif rocm.version == "6.3": + torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.8.0+rocm6.3 torchvision==0.23.0+rocm6.3 --index-url https://download.pytorch.org/whl/rocm6.3') elif rocm.version == "6.2": # use rocm 6.2.4 instead of 6.2 as torch==2.7.1+rocm6.2 doesn't exists torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.7.1+rocm6.2.4 torchvision==0.22.1+rocm6.2.4 --index-url https://download.pytorch.org/whl/rocm6.2.4') @@ -804,6 +804,7 @@ def install_ipex(): if args.use_nightly: torch_command = os.environ.get('TORCH_COMMAND', '--upgrade --pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/xpu') else: + # torch 2.8 segfaults with torch.compile: https://github.com/pytorch/pytorch/issues/159974 torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.7.1+xpu torchvision==0.22.1+xpu --index-url https://download.pytorch.org/whl/xpu') ts('ipex', t_start) @@ -815,9 +816,9 @@ def install_openvino(): #check_python(supported_minors=[9, 10, 11, 12, 13], reason='OpenVINO backend requires a Python version between 3.9 and 3.13') log.info('OpenVINO: selected') if sys.platform == 'darwin': - torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.7.1 torchvision==0.22.1') + torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.8.0 torchvision==0.23.0') else: - torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.7.1+cpu torchvision==0.22.1+cpu --index-url https://download.pytorch.org/whl/cpu') + torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.8.0+cpu torchvision==0.23.0+cpu --index-url https://download.pytorch.org/whl/cpu') install(os.environ.get('OPENVINO_COMMAND', 'openvino==2025.2.0'), 'openvino') install(os.environ.get('NNCF_COMMAND', 'nncf==2.17.0'), 'nncf') diff --git a/modules/intel/ipex/int_mm.py b/modules/intel/ipex/int_mm.py index 9c5fab093..4256937ac 100644 --- a/modules/intel/ipex/int_mm.py +++ b/modules/intel/ipex/int_mm.py @@ -49,7 +49,7 @@ def qlinear_unary( # GEMM template needs 2D input, normalize input shape here x = view(x, [-1, x_size[-1]]) if not isinstance(x_scale, ir.TensorBox): - assert type(x_scale) == float + assert isinstance(x_scale, float) x_scale = V.graph.add_tensor_constant( torch.tensor(x_scale, dtype=torch.float32), name="x_scale" ) @@ -71,7 +71,7 @@ def qlinear_unary( torch.tensor(0, dtype=torch.int32), name="x_zp" ) if not isinstance(x_zp, ir.TensorBox): - assert type(x_zp) == int + assert isinstance(x_zp, int) x_zp = V.graph.add_tensor_constant( torch.tensor(x_zp, dtype=torch.int32), name="x_zp" ) From aa0652caa9e3fb302ffaeb5692544f368eebe3ba Mon Sep 17 00:00:00 2001 From: Disty0 Date: Thu, 7 Aug 2025 00:18:24 +0300 Subject: [PATCH 034/141] SDNQ fix new transformers --- modules/sdnq/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index 16ac58f2e..f05996e45 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -376,6 +376,12 @@ class SDNQQuantizer(DiffusersQuantizer): """ return expected_keys + def update_param_name(self, param_name: str) -> str: + """ + needed for transformers compatibilty, no-op function + """ + return param_name + @property def is_trainable(self): return False From a91ee36b5cbdfbc601ffb885ba3d2b37621522a0 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 6 Aug 2025 18:35:43 -0400 Subject: [PATCH 035/141] fix settings components mismatch Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + modules/options_handler.py | 1 + modules/shared.py | 2 +- modules/shared_legacy.py | 2 +- modules/ui_settings.py | 10 ++++++++-- wiki | 2 +- 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b43e60f2b..64279e45e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ And (*as always*) many bugfixes and improvements to existing features! - update requirements/packages - **Fixes** - refactor legacy processing loop + - fix settings components mismatch - fix Wan 2.2-5B I2V workflow - fix OpenVINO - fix video model vs pipeline mismatch diff --git a/modules/options_handler.py b/modules/options_handler.py index ac974f6de..5dd7f5435 100644 --- a/modules/options_handler.py +++ b/modules/options_handler.py @@ -9,6 +9,7 @@ from installer import log if TYPE_CHECKING: from modules.options import OptionInfo + cmd_opts = cmd_args.parse_args() compatibility_opts = ['clip_skip', 'uni_pc_lower_order_final', 'uni_pc_order'] diff --git a/modules/shared.py b/modules/shared.py index ea368da49..31d878601 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -693,7 +693,7 @@ options_templates.update(options_section(('extra_networks', "Networks"), { "wildcards_enabled": OptionInfo(True, "Enable file wildcards support"), })) -options_templates.update(options_section((None, "Hidden options"), { +options_templates.update(options_section(('hidden_options', "Hidden options"), { # internal options "diffusers_version": OptionInfo("", "Diffusers version", gr.Textbox, {"visible": False}), "disabled_extensions": OptionInfo([], "Disable these extensions", gr.Textbox, {"visible": False}), diff --git a/modules/shared_legacy.py b/modules/shared_legacy.py index b4f2e4c11..d83096675 100644 --- a/modules/shared_legacy.py +++ b/modules/shared_legacy.py @@ -9,7 +9,7 @@ class LegacyOption(OptionInfo): super().__init__(*args, **kwargs) -legacy_options = options_section((None, "Legacy options"), { +legacy_options = options_section(('legacy_options', "Legacy options"), { "ldsr_models_path": LegacyOption(os.path.join(paths.models_path, 'LDSR'), "LDSR Path", gr.Textbox, { "visible": False}), "interrogate_clip_skip_categories": LegacyOption(["artists", "movements", "flavors"], "CLiP: skip categories", gr.CheckboxGroup, {"choices": [], "visible":False}), "lora_legacy": LegacyOption(False, "LoRA load using legacy method", gr.Checkbox, {"visible": False}), diff --git a/modules/ui_settings.py b/modules/ui_settings.py index 0687e9a6f..042941692 100644 --- a/modules/ui_settings.py +++ b/modules/ui_settings.py @@ -119,6 +119,9 @@ def run_settings(*args): changed = [] for key, value, comp in zip(shared.opts.data_labels.keys(), args, components): if comp == dummy_component or value=='dummy': # or getattr(comp, 'visible', True) is False or key in hidden_list: + actual = shared.opts.data.get(key, None) # ensure the key is in data + default = shared.opts.data_labels[key].default + # shared.log.warning(f'Setting skip: key={key} value={value} actual={actual} default={default} comp={comp}') continue if not shared.opts.same_type(value, shared.opts.data_labels[key].default): shared.log.error(f'Setting bad value: {key}={value} expecting={type(shared.opts.data_labels[key].default).__name__}') @@ -191,6 +194,7 @@ def create_ui(): result = gr.HTML(elem_id="settings_result") script_callbacks.ui_settings_callback() # let extensions create settings sections = [] + options_count = len(shared.opts.data_labels) for item in shared.opts.data_labels.values(): # get unique sections from all items if len(item.section) == 2: section_id, section_text = item.section @@ -203,7 +207,7 @@ def create_ui(): if (section_id, section_text) not in sections: sections.append((section_id, section_text)) - shared.log.debug(f'Settings: sections={len(sections)} settings={len(shared.opts.list())}/{len(list(shared.opts.data_labels))}') + shared.log.debug(f'Settings: sections={len(sections)} settings={len(shared.opts.list())}/{len(list(shared.opts.data_labels))} quicksettings={len(quicksettings_list)}') with gr.Tabs(elem_id="settings"): quicksettings_list.clear() for (section_id, section_text) in sections: @@ -228,12 +232,14 @@ def create_ui(): current_items.append(key) components.append(component) create_dirty_indicator(section_id, current_items) + components_count = len(components) + if components_count != options_count: + shared.log.error(f'Settings: count mismatch: options={options_count} components={components_count}') with gr.TabItem("Show all pages", elem_id="settings_show_all_pages"): create_dirty_indicator("show_all_pages", []) request_notifications = gr.Button(value='Request browser notifications', elem_id="request_notifications", visible=False) - with gr.TabItem("Update", id="system_update", elem_id="tab_update"): from modules import update update.create_ui() diff --git a/wiki b/wiki index 85ff38d28..96e3932bf 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 85ff38d28980390786f4558666425d29f7bceb33 +Subproject commit 96e3932bffee9117951956074956e943f254702b From a9c65c0e8c55e6d9811c571d24dfc3f96e33f55f Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 6 Aug 2025 19:27:57 -0400 Subject: [PATCH 036/141] move api-only to legacy options Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 3 +++ launch.py | 5 +---- modules/api/api.py | 2 -- modules/api/middleware.py | 2 +- modules/civitai/search_civitai.py | 2 +- modules/cmd_args.py | 2 +- modules/ui_settings.py | 4 ++-- webui.py | 20 +------------------- 8 files changed, 10 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64279e45e..4f1f03b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,9 @@ And (*as always*) many bugfixes and improvements to existing features! - prompt parser allow explict `BOS` and `EOS` tokens in prompt - **Nunchaku** support for *FLUX.1-Fill* and *FLUX.1-Depth* models - update requirements/packages +- **Other** + - remove LDSR + - remove `api-only` cli option - **Fixes** - refactor legacy processing loop - fix settings components mismatch diff --git a/launch.py b/launch.py index 7f7d3c335..85ea0337a 100755 --- a/launch.py +++ b/launch.py @@ -221,10 +221,7 @@ def start_server(immediate=True, server=None): installer.log.trace('Logging: level=trace') server.wants_restart = False else: - if args.api_only: - uvicorn = server.api_only() - else: - uvicorn = server.webui(restart=not immediate) + uvicorn = server.webui(restart=not immediate) if args.profile: pr.disable() installer.print_profile(pr, 'WebUI') diff --git a/modules/api/api.py b/modules/api/api.py index 8b0ae3400..539001ea4 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -117,8 +117,6 @@ class Api: def add_api_route(self, path: str, endpoint, **kwargs): - if (shared.cmd_opts.auth or shared.cmd_opts.auth_file) and shared.cmd_opts.api_only: - kwargs['dependencies'] = [Depends(self.auth)] if shared.opts.subpath is not None and len(shared.opts.subpath) > 0: self.app.add_api_route(f'{shared.opts.subpath}{path}', endpoint, **kwargs) self.app.add_api_route(path, endpoint, **kwargs) diff --git a/modules/api/middleware.py b/modules/api/middleware.py index 5c72f2204..0d258c878 100644 --- a/modules/api/middleware.py +++ b/modules/api/middleware.py @@ -42,7 +42,7 @@ def setup_middleware(app: FastAPI, cmd_opts): res.headers["X-Process-Time"] = duration endpoint = req.scope.get('path', 'err') token = req.cookies.get("access-token") or req.cookies.get("access-token-unsecure") - if (cmd_opts.api_log or cmd_opts.api_only) and endpoint.startswith('/sdapi'): + if (cmd_opts.api_log) and endpoint.startswith('/sdapi'): if '/sdapi/v1/log' in endpoint or '/sdapi/v1/browser' in endpoint: return res log.info('API user={user} code={code} {prot}/{ver} {method} {endpoint} {cli} {duration}'.format( # pylint: disable=consider-using-f-string, logging-format-interpolation diff --git a/modules/civitai/search_civitai.py b/modules/civitai/search_civitai.py index 8f0f2bad5..12d74fce3 100644 --- a/modules/civitai/search_civitai.py +++ b/modules/civitai/search_civitai.py @@ -7,7 +7,7 @@ from installer import install, log full_dct = False full_html = False -base_models = ['', 'ODOR', 'SD 1.4', 'SD 1.5', 'SD 1.5 LCM', 'SD 1.5 Hyper', 'SD 2.0', 'SD 2.0 768', 'SD 2.1', 'SD 2.1 768', 'SD 2.1 Unclip', 'SDXL 0.9', 'SDXL 1.0', 'SD 3', 'SD 3.5', 'SD 3.5 Medium', 'SD 3.5 Large', 'SD 3.5 Large Turbo', 'Pony', 'Flux.1 S', 'Flux.1 D', 'Flux.1 Kontext', 'AuraFlow', 'SDXL 1.0 LCM', 'SDXL Distilled', 'SDXL Turbo', 'SDXL Lightning', 'SDXL Hyper', 'Stable Cascade', 'SVD', 'SVD XT', 'Playground v2', 'PixArt a', 'PixArt E', 'Hunyuan 1', 'Hunyuan Video', 'Lumina', 'Kolors', 'Illustrious', 'Mochi', 'LTXV', 'CogVideoX', 'NoobAI', 'Wan Video', 'Wan Video 1.3B t2v', 'Wan Video 14B t2v', 'Wan Video 14B i2v 480p', 'Wan Video 14B i2v 720p', 'HiDream', 'OpenAI', 'Imagen4', 'Other'] # noqa: E501 +base_models = ['', 'ODOR', 'SD 1.4', 'SD 1.5', 'SD 1.5 LCM', 'SD 1.5 Hyper', 'SD 2.0', 'SD 2.0 768', 'SD 2.1', 'SD 2.1 768', 'SD 2.1 Unclip', 'SDXL 0.9', 'SDXL 1.0', 'SD 3', 'SD 3.5', 'SD 3.5 Medium', 'SD 3.5 Large', 'SD 3.5 Large Turbo', 'Pony', 'Flux.1 S', 'Flux.1 D', 'Flux.1 Kontext', 'AuraFlow', 'SDXL 1.0 LCM', 'SDXL Distilled', 'SDXL Turbo', 'SDXL Lightning', 'SDXL Hyper', 'Stable Cascade', 'SVD', 'SVD XT', 'Playground v2', 'PixArt a', 'PixArt E', 'Hunyuan 1', 'Hunyuan Video', 'Lumina', 'Kolors', 'Illustrious', 'Mochi', 'LTXV', 'CogVideoX', 'NoobAI', 'Wan Video', 'Wan Video 1.3B t2v', 'Wan Video 14B t2v', 'Wan Video 14B i2v 480p', 'Wan Video 14B i2v 720p', 'HiDream', 'OpenAI', 'Imagen4', 'Other'] @dataclass diff --git a/modules/cmd_args.py b/modules/cmd_args.py index 84da8f5f6..d368babb5 100644 --- a/modules/cmd_args.py +++ b/modules/cmd_args.py @@ -62,7 +62,6 @@ def main_args(): group_http.add_argument('--docs', default=os.environ.get("SD_DOCS", False), action='store_true', help = "Mount API docs, default: %(default)s") group_http.add_argument("--auth", type=str, default=os.environ.get("SD_AUTH", None), help='Set access authentication like "user:pwd,user:pwd""') group_http.add_argument("--auth-file", type=str, default=os.environ.get("SD_AUTHFILE", None), help='Set access authentication using file, default: %(default)s') - group_http.add_argument('--api-only', default=os.environ.get("SD_APIONLY", False), action='store_true', help = "Run in API only mode without starting UI") group_http.add_argument("--allowed-paths", nargs='+', default=[], type=str, required=False, help="add additional paths to paths allowed for web access") group_http.add_argument("--share", default=os.environ.get("SD_SHARE", False), action='store_true', help="Enable UI accessible through Gradio site, default: %(default)s") group_http.add_argument("--insecure", default=os.environ.get("SD_INSECURE", False), action='store_true', help="Enable extensions tab regardless of other options, default: %(default)s") @@ -89,6 +88,7 @@ def compatibility_args(): group_compat.add_argument("--disable-extension-access", default=False, action='store_true', help=argparse.SUPPRESS) group_compat.add_argument("--api", action='store_true', help=argparse.SUPPRESS, default=True) group_compat.add_argument("--api-auth", type=str, help=argparse.SUPPRESS, default=None) + group_compat.add_argument('--api-only', default=False, help=argparse.SUPPRESS) group_compat.add_argument("--disable-queue", default=os.environ.get("SD_DISABLEQUEUE", False), action='store_true', help=argparse.SUPPRESS) diff --git a/modules/ui_settings.py b/modules/ui_settings.py index 042941692..9a5a47a13 100644 --- a/modules/ui_settings.py +++ b/modules/ui_settings.py @@ -119,8 +119,8 @@ def run_settings(*args): changed = [] for key, value, comp in zip(shared.opts.data_labels.keys(), args, components): if comp == dummy_component or value=='dummy': # or getattr(comp, 'visible', True) is False or key in hidden_list: - actual = shared.opts.data.get(key, None) # ensure the key is in data - default = shared.opts.data_labels[key].default + # actual = shared.opts.data.get(key, None) # ensure the key is in data + # default = shared.opts.data_labels[key].default # shared.log.warning(f'Setting skip: key={key} value={value} actual={actual} default={default} comp={comp}') continue if not shared.opts.same_type(value, shared.opts.data_labels[key].default): diff --git a/webui.py b/webui.py index 22735c6f1..2e9a555a7 100644 --- a/webui.py +++ b/webui.py @@ -413,23 +413,5 @@ def webui(restart=False): return shared.demo.server -def api_only(): - start_common() - from fastapi import FastAPI - app = FastAPI(**fastapi_args) - modules.api.middleware.setup_middleware(app, shared.cmd_opts) - shared.api = create_api(app) - shared.api.register() - shared.api.wants_restart = False - modules.script_callbacks.app_started_callback(None, app) - modules.sd_models.write_metadata() - log.info(f"Startup time: {timer.startup.summary()}") - server = shared.api.launch() - return server - - if __name__ == "__main__": - if shared.cmd_opts.api_only: - api_only() - else: - webui() + webui() From 31e6cfd91b0d75a7af738f35b14da51827a64da7 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Thu, 7 Aug 2025 15:14:45 +0300 Subject: [PATCH 037/141] Fix IPEX 2.8 --- modules/intel/ipex/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/intel/ipex/__init__.py b/modules/intel/ipex/__init__.py index 6e1f5047a..f590da35e 100644 --- a/modules/intel/ipex/__init__.py +++ b/modules/intel/ipex/__init__.py @@ -125,6 +125,11 @@ def ipex_init(): # pylint: disable=too-many-statements torch.cuda.Tuple = torch.xpu.Tuple torch.cuda.List = torch.xpu.List + if torch_version < 2.8: + if has_ipex: + torch.cuda.memory_summary = torch.xpu.memory_summary + torch.cuda.memory_snapshot = torch.xpu.memory_snapshot + if torch_version < 2.9: # torch._int_mm via onednn quantized matmul is supported with torch 2.9 # ipex 2.7+ has the same torch._int_mm support as torch 2.9 but doesn't support torch.compile @@ -148,9 +153,6 @@ def ipex_init(): # pylint: disable=too-many-statements torch.xpu.empty_cache = lambda: None torch.cuda.empty_cache = torch.xpu.empty_cache - if has_ipex: - torch.cuda.memory_summary = torch.xpu.memory_summary - torch.cuda.memory_snapshot = torch.xpu.memory_snapshot torch.cuda.memory = torch.xpu.memory torch.cuda.memory_stats = torch.xpu.memory_stats torch.cuda.memory_allocated = torch.xpu.memory_allocated From 4875c26de3df0f95ee7af4906ebd9ac96b539154 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Thu, 7 Aug 2025 16:23:22 +0300 Subject: [PATCH 038/141] Fix OpenVINO Torchvision install on ARM --- installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.py b/installer.py index 35795a430..9a2e847cd 100644 --- a/installer.py +++ b/installer.py @@ -818,7 +818,7 @@ def install_openvino(): if sys.platform == 'darwin': torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.8.0 torchvision==0.23.0') else: - torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.8.0+cpu torchvision==0.23.0+cpu --index-url https://download.pytorch.org/whl/cpu') + torch_command = os.environ.get('TORCH_COMMAND', 'torch==2.8.0+cpu torchvision==0.23.0 --index-url https://download.pytorch.org/whl/cpu') install(os.environ.get('OPENVINO_COMMAND', 'openvino==2025.2.0'), 'openvino') install(os.environ.get('NNCF_COMMAND', 'nncf==2.17.0'), 'nncf') From 3e3adcee749d5b804ce2617110085da35421cacb Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 7 Aug 2025 08:44:46 -0400 Subject: [PATCH 039/141] add skyreels-v2 Signed-off-by: Vladimir Mandic --- .eslintrc.json | 1 + CHANGELOG.md | 15 +++++--- TODO.md | 3 +- extensions-builtin/sd-extension-system-info | 2 +- html/locale_en.json | 2 +- javascript/contextMenus.js | 2 +- javascript/nvml.js | 4 ++ javascript/sdnext.css | 5 +++ javascript/startup.js | 1 + modules/ui_sections.py | 11 +++--- modules/ui_video.py | 2 +- modules/video_models/models_def.py | 41 +++++++++++++++++++++ modules/video_models/video_load.py | 3 ++ modules/video_models/video_overrides.py | 7 +++- modules/video_models/video_utils.py | 2 +- 15 files changed, 82 insertions(+), 19 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 7f4c9f27f..38bcae860 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -109,6 +109,7 @@ "jobStatusEl": "readonly", "removeSplash": "readonly", "initNVML": "readonly", + "startNVML": "readonly", "disableNVML": "readonly", "idbGet": "readonly", "idbPut": "readonly", diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f1f03b61..40ae34f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,18 @@ - Qwen with offloading: -## Update for 2025-08-06 +## Update for 2025-08-07 -### Highlights for 2025-08-06 +### Highlights for 2025-08-07 -This time we have several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/), [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) and [Chroma](https://huggingface.co/lodestones/Chroma) -Continuing with major UI work, there is new embedded Docs/Wiki search, redesigned CivitAI integration and quite a few UI updates! +Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/), [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release), [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) +Plus continuing with major **UI** work, there is new embedded **Docs/Wiki** search, redesigned real-time **hints**, **CivitAI** integration and more! On the compute side, new profiles for high-vram GPUs and offloading improvements And (*as always*) many bugfixes and improvements to existing features! [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) -### Details for 2025-08-06 +### Details for 2025-08-07 - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) @@ -31,6 +31,11 @@ And (*as always*) many bugfixes and improvements to existing features! great model based on FLUX.1 and then redesigned and retrained by *lodestones* update with latest **v48**, **v48 Detail Calibrated** and **v46 Flash** variants available via *networks -> models -> reference* + - [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) + SkyReels-V2 is a genarative video model based on Wan-2.1 but with heavily modified execution to allow for infinite-length video generation + supported variants are: + - diffusion-forcing: T2I DF 1.3B for 540p videos, T2I DF 14B for 720p videos, I2I DF 14B for 720p videos + - standard: T2I 14B for 720p videos and I2I 14B for 720p videos **Torch** - Set default for ROCm and OpenVINO to `torch==2.8.0` - **UI** diff --git a/TODO.md b/TODO.md index af686fbda..b3b35f3e0 100644 --- a/TODO.md +++ b/TODO.md @@ -30,8 +30,7 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma - [IPAdapter composition](https://huggingface.co/ostris/ip-composition-adapter) - [STG](https://github.com/huggingface/diffusers/blob/main/examples/community/README.md#spatiotemporal-skip-guidance) - [SmoothCache](https://github.com/huggingface/diffusers/issues/11135) -- [MagCache](https://github.com/lllyasviel/FramePack/pull/673/files) -- [HiDream GGUF](https://github.com/huggingface/diffusers/pull/11550) +- [MagCache](https://github.com/lllyasviel/FramePack/pull/673/files) - [Nunchaku PulID](https://github.com/mit-han-lab/nunchaku/pull/274) - [Dream0 guidance](https://huggingface.co/ByteDance/DreamO) - [SUPIR upscaler](https://github.com/Fanghua-Yu/SUPIR) diff --git a/extensions-builtin/sd-extension-system-info b/extensions-builtin/sd-extension-system-info index 0760f3bce..615d2f810 160000 --- a/extensions-builtin/sd-extension-system-info +++ b/extensions-builtin/sd-extension-system-info @@ -1 +1 @@ -Subproject commit 0760f3bcee4cd8448089e749dd7c22cdfebf15c3 +Subproject commit 615d2f8103fb61779037bd81523906457fbf7277 diff --git a/html/locale_en.json b/html/locale_en.json index e908743eb..d35d8a96d 100644 --- a/html/locale_en.json +++ b/html/locale_en.json @@ -93,7 +93,7 @@ {"id":"","label":"Denoise","localized":"","hint":"Denoising settings. Higher denoise means that more of existing image content is allowed to change during generate"}, {"id":"","label":"Mask","localized":"","hint":"Image masking and mask options"}, {"id":"","label":"Input","localized":"","hint":"Selection of input media"}, - {"id":"","label":"Video","localized":"","hint":"Settings related to video generation"}, + {"id":"","label":"Video","localized":"","hint":"Create video using guidance"}, {"id":"","label":"Control elements","localized":"","hint":"Control elements are advanced models that can guide generation towards desired outcome"}, {"id":"","label":"IP adapter","localized":"","hint":"Guide generation towards desired outcome using IP adapters plugin models"}, {"id":"","label":"IP adapters","localized":"","hint":"IP adapters are plugin models that can guide generation towards desired outcome"}, diff --git a/javascript/contextMenus.js b/javascript/contextMenus.js index 271f5a9e4..3dfa16b7e 100644 --- a/javascript/contextMenus.js +++ b/javascript/contextMenus.js @@ -149,7 +149,7 @@ async function initContextMenu() { appendContextMenuOption(id, 'Generate forever', () => generateForever(`#${tab}_generate`)); appendContextMenuOption(id, 'Apply selected style', quickApplyStyle); appendContextMenuOption(id, 'Quick save style', quickSaveStyle); - appendContextMenuOption(id, 'nVidia overlay', initNVML); + appendContextMenuOption(id, 'nVidia overlay', startNVML); id = `#${tab}_reprocess`; appendContextMenuOption(id, 'Decode full quality', () => reprocessClick(`${tab}`, 'reprocess_decode'), true); appendContextMenuOption(id, 'Refine & HiRes pass', () => reprocessClick(`${tab}`, 'reprocess_refine'), true); diff --git a/javascript/nvml.js b/javascript/nvml.js index cf55bb196..155dc3f89 100644 --- a/javascript/nvml.js +++ b/javascript/nvml.js @@ -90,6 +90,10 @@ async function initNVML() { gradioApp().appendChild(nvmlEl); log('initNVML'); } +} + +async function startNVML() { + nvmlEl = document.getElementById('nvml'); if (nvmlInterval) { clearInterval(nvmlInterval); nvmlInterval = null; diff --git a/javascript/sdnext.css b/javascript/sdnext.css index f5e2588e9..636aeaec8 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -2055,6 +2055,11 @@ div:has(>#tab-gallery-folders) { font-weight: bold; } +.video-model-link { + color: var(--button-primary-background-fill); + font-weight: normal; +} + @keyframes move { from { background-position-x: 0, -40px; diff --git a/javascript/startup.js b/javascript/startup.js index 842f925ba..3629511ca 100644 --- a/javascript/startup.js +++ b/javascript/startup.js @@ -39,6 +39,7 @@ async function initStartup() { executeCallbacks(uiReadyCallbacks); initLogMonitor(); setupExtraNetworks(); + initNVML(); // optinally wait for modern ui if (window.waitForUiReady) await waitForUiReady(); diff --git a/modules/ui_sections.py b/modules/ui_sections.py index 94fbde348..4f72783bb 100644 --- a/modules/ui_sections.py +++ b/modules/ui_sections.py @@ -134,12 +134,13 @@ def create_video_inputs(tab:str, show_always:bool=False): gr.update(visible=video_type not in ['None', 'GIF', 'PNG'] or show_always), gr.update(visible=video_type not in ['None', 'GIF', 'PNG'] or show_always), ] - with gr.Column(): + with gr.Row(): video_codecs = ['None', 'GIF', 'PNG', 'MP4/MP4V', 'MP4/AVC1', 'MP4/JVT3', 'MKV/H264', 'AVI/DIVX', 'AVI/RGBA', 'MJPEG/MJPG', 'MPG/MPG1', 'AVR/AVR1'] - video_type = gr.Dropdown(label='Save video', choices=video_codecs, value='None', elem_id=f"{tab}_video_type") - with gr.Column(): - video_duration = gr.Slider(label='Duration', minimum=0.25, maximum=300, step=0.25, value=2, visible=show_always, elem_id=f"{tab}_video_duration") - video_loop = gr.Checkbox(label='Loop', value=True, visible=show_always, elem_id=f"{tab}_video_loop") + video_type = gr.Dropdown(label='Video format', choices=video_codecs, value='MP4/MP4V', elem_id=f"{tab}_video_type") + with gr.Row(): + video_duration = gr.Slider(label='Video duration', minimum=0.25, maximum=300, step=0.25, value=2, visible=show_always, elem_id=f"{tab}_video_duration") + video_loop = gr.Checkbox(label='Loop video', value=True, visible=show_always, elem_id=f"{tab}_video_loop") + with gr.Row(): video_pad = gr.Slider(label='Pad frames', minimum=0, maximum=24, step=1, value=1, visible=show_always, elem_id=f"{tab}_video_pad") video_interpolate = gr.Slider(label='Interpolate frames', minimum=0, maximum=24, step=1, value=0, visible=show_always, elem_id=f"{tab}_video_interpolate") video_type.change(fn=video_type_change, inputs=[video_type], outputs=[video_duration, video_loop, video_pad, video_interpolate]) diff --git a/modules/ui_video.py b/modules/ui_video.py index e12f08bc9..bb9cace94 100644 --- a/modules/ui_video.py +++ b/modules/ui_video.py @@ -27,7 +27,7 @@ def create_ui(): with gr.Row(elem_id="video_interface", equal_height=False): with gr.Tabs(elem_classes=['video-tabs'], elem_id='video-tabs'): overrides = ui_common.create_override_inputs('video') - with gr.Tab('Generic', id='video-tab') as video_tab: + with gr.Tab('Core', id='video-tab') as video_tab: from modules.video_models import video_ui video_ui.create_ui(prompt, negative, styles, overrides) with gr.Tab('FramePack', id='framepack-tab') as framepack_tab: diff --git a/modules/video_models/models_def.py b/modules/video_models/models_def.py index e6f570775..4111ee517 100644 --- a/modules/video_models/models_def.py +++ b/modules/video_models/models_def.py @@ -9,13 +9,16 @@ class Model(): url: str = '' repo: str = None repo_cls: classmethod = None + repo_revision: str = None dit: str = None dit_cls: classmethod = None dit_folder: str = 'transformer' + dit_revision: str = None te: str = None te_cls: classmethod = None te_folder: str = 'text_encoder' te_hijack: bool = True + te_revision: str = None image_hijack: bool = True vae_hijack: bool = True vae_remote: bool = False @@ -195,6 +198,44 @@ models = { te_cls=transformers.T5EncoderModel, dit_cls=diffusers.WanTransformer3DModel), ], + 'SkyReels V2': [ + Model(name='None'), + Model(name='SkyReels-V2 T2I-DF 1.3B-540P', + url='https://huggingface.co/Skywork/SkyReels-V2-DF-1.3B-540P-Diffusers', + repo='Skywork/SkyReels-V2-DF-1.3B-540P-Diffusers', + repo_cls=diffusers.SkyReelsV2DiffusionForcingPipeline, + repo_revision='refs/pr/1', + te_cls=transformers.UMT5EncoderModel, + dit_cls=diffusers.SkyReelsV2Transformer3DModel), + Model(name='SkyReels-V2 T2I-DF 14B-720P', + url='https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers', + repo='Skywork/SkyReels-V2-DF-14B-720P-Diffusers', + repo_cls=diffusers.SkyReelsV2DiffusionForcingPipeline, + repo_revision='refs/pr/1', + te_cls=transformers.UMT5EncoderModel, + dit_cls=diffusers.SkyReelsV2Transformer3DModel), + Model(name='SkyReels-V2 I2I-DF 14B-720P', + url='https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers', + repo='Skywork/SkyReels-V2-DF-14B-720P-Diffusers', + repo_cls=diffusers.SkyReelsV2DiffusionForcingImageToVideoPipeline, + repo_revision='refs/pr/1', + te_cls=transformers.UMT5EncoderModel, + dit_cls=diffusers.SkyReelsV2Transformer3DModel), + Model(name='SkyReels-V2 T2I 14B-720P', + url='https://huggingface.co/Skywork/SkyReels-V2-T2V-14B-720P-Diffusers', + repo='Skywork/SkyReels-V2-T2V-14B-720P-Diffusers', + repo_cls=diffusers.SkyReelsV2Pipeline, + repo_revision='refs/pr/1', + te_cls=transformers.UMT5EncoderModel, + dit_cls=diffusers.SkyReelsV2Transformer3DModel), + Model(name='SkyReels-V2 I2I 14B-720P', + url='https://huggingface.co/Skywork/SkyReels-V2-I2V-14B-720P-Diffusers', + repo='Skywork/SkyReels-V2-I2V-14B-720P-Diffusers', + repo_cls=diffusers.SkyReelsV2ImageToVideoPipeline, + repo_revision='refs/pr/1', + te_cls=transformers.UMT5EncoderModel, + dit_cls=diffusers.SkyReelsV2Transformer3DModel), + ], 'Mochi Video': [ Model(name='None'), Model(name='Mochi 1 T2V', diff --git a/modules/video_models/video_load.py b/modules/video_models/video_load.py index 3eda8f735..53b4d66a0 100644 --- a/modules/video_models/video_load.py +++ b/modules/video_models/video_load.py @@ -28,6 +28,7 @@ def load_model(selected: models_def.Model): text_encoder = selected.te_cls.from_pretrained( pretrained_model_name_or_path=selected.te or selected.repo, subfolder=selected.te_folder, + revision=selected.te_revision or selected.repo_revision, cache_dir=shared.opts.hfcache_dir, torch_dtype=devices.dtype, **quant_args @@ -44,6 +45,7 @@ def load_model(selected: models_def.Model): transformer = selected.dit_cls.from_pretrained( pretrained_model_name_or_path=selected.dit or selected.repo, subfolder=selected.dit_folder, + revision=selected.dit_revision or selected.repo_revision, torch_dtype=devices.dtype, cache_dir=shared.opts.hfcache_dir, **quant_args @@ -63,6 +65,7 @@ def load_model(selected: models_def.Model): pretrained_model_name_or_path=selected.repo, transformer=transformer, text_encoder=text_encoder, + revision=selected.repo_revision, cache_dir=shared.opts.hfcache_dir, torch_dtype=devices.dtype, **kwargs, diff --git a/modules/video_models/video_overrides.py b/modules/video_models/video_overrides.py index 655256e95..07cf1cd23 100644 --- a/modules/video_models/video_overrides.py +++ b/modules/video_models/video_overrides.py @@ -10,10 +10,13 @@ debug = shared.log.trace if os.environ.get('SD_VIDEO_DEBUG', None) is not None e def load_override(selected: Model): kwargs = {} - if selected.name == 'Allegro T2V': + # Allegro + if 'Allegro T2V' in selected.name: kwargs['vae'] = diffusers.AutoencoderKLAllegro.from_pretrained(selected.repo, subfolder="vae", torch_dtype=torch.float32, cache_dir=shared.opts.hfcache_dir) - if selected.name == 'LTXVideo 0.9.5 I2V': + # LTX + if 'LTXVideo 0.9.5 I2V' in selected.name: kwargs['vae'] = diffusers.AutoencoderKLLTXVideo.from_pretrained(selected.repo, subfolder="vae", torch_dtype=torch.float32, cache_dir=shared.opts.hfcache_dir) + # WAN if 'WAN 2.1 14B' in selected.name: kwargs['vae'] = diffusers.AutoencoderKLWan.from_pretrained(selected.repo, subfolder="vae", torch_dtype=torch.float32, cache_dir=shared.opts.hfcache_dir) debug(f'Video overrides: model="{selected.name}" kwargs={list(kwargs)}') diff --git a/modules/video_models/video_utils.py b/modules/video_models/video_utils.py index cbca9a3f5..47faeac13 100644 --- a/modules/video_models/video_utils.py +++ b/modules/video_models/video_utils.py @@ -14,7 +14,7 @@ def queue_err(msg): def get_url(url): - return f'  {url}

' if url else '

' + return f'{url}

' if url else '

' def check_av(): From 4b74fd26b5a4193e330b33ecb729a473aa16b617 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 7 Aug 2025 13:52:57 -0400 Subject: [PATCH 040/141] redesign gpu monitor Signed-off-by: Vladimir Mandic --- .eslintrc.json | 4 +- CHANGELOG.md | 23 +++--- extensions-builtin/sdnext-modernui | 2 +- javascript/base.css | 2 +- javascript/contextMenus.js | 1 - javascript/gpu.js | 75 ++++++++++++++++++++ javascript/nvml.js | 109 ----------------------------- javascript/sdnext.css | 3 +- javascript/startup.js | 1 - modules/api/api.py | 4 +- modules/api/gpu.py | 28 ++++++++ modules/api/middleware.py | 2 +- modules/api/models.py | 14 ++-- modules/api/nvml.py | 53 ++++++-------- modules/shared.py | 1 + modules/ui_settings.py | 16 +++++ scripts/prompt_enhance.py | 1 + 17 files changed, 171 insertions(+), 168 deletions(-) create mode 100644 javascript/gpu.js delete mode 100644 javascript/nvml.js create mode 100644 modules/api/gpu.py diff --git a/.eslintrc.json b/.eslintrc.json index 38bcae860..32283e620 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -108,8 +108,8 @@ "getExif": "readonly", "jobStatusEl": "readonly", "removeSplash": "readonly", - "initNVML": "readonly", - "startNVML": "readonly", + "initGPU": "readonly", + "startGPU": "readonly", "disableNVML": "readonly", "idbGet": "readonly", "idbPut": "readonly", diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ae34f9e..5c535695b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ### Highlights for 2025-08-07 Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/), [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release), [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) -Plus continuing with major **UI** work, there is new embedded **Docs/Wiki** search, redesigned real-time **hints**, **CivitAI** integration and more! +Plus continuing with major **UI** work, there is new embedded **Docs/Wiki** search, redesigned real-time **hints**, built-in **GPU monitor**, **CivitAI** integration and more! On the compute side, new profiles for high-vram GPUs and offloading improvements And (*as always*) many bugfixes and improvements to existing features! @@ -43,16 +43,22 @@ And (*as always*) many bugfixes and improvements to existing features! **Docs** search: fully-local and works in real-time on all document pages **Wiki** search: uses github api to search online wiki pages - updated real-time hints, thanks @CalamitousFelicitousness - - rewritten **CivitAI downloader** - in *models -> civitai* + - every heading element is collapsible! - quicksettings reset button to restore all quicksettings to default values because things do sometimes get wrong... - configurable image fit in all image views - - updated *models -> current* tab - - updated *models -> list models* tab - - updated *models -> metadata* tab + - rewritten **CivitAI downloader** + in *models -> civitai* + - redesigned **GPU monitor** + - standard-ui: *system -> gpu monitor* + - modern-ui: *aside -> console -> gpu monitor* + - configurable interval in *settings -> user interface* + - updated *models* tab + - updated *models -> current* tab + - updated *models -> list models* tab + - updated *models -> metadata* tab - updated *extensions* tab - - redesign *settings -> user interface* + - redesigned *settings -> user interface* - gallery bypass browser cache for thumbnails - gallery safer delete operation - networks display indicator for currently active items @@ -78,7 +84,8 @@ And (*as always*) many bugfixes and improvements to existing features! - **Nunchaku** support for *FLUX.1-Fill* and *FLUX.1-Depth* models - update requirements/packages - **Other** - - remove LDSR + - **prompt enhance** add `allura-org/Gemma-3-Glitter-4B` model support + - remove **LDSR** - remove `api-only` cli option - **Fixes** - refactor legacy processing loop diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 874ff0e88..d26fde293 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 874ff0e88099703cde91ab3c5347a78d002aa3c6 +Subproject commit d26fde293e8a2632e2c08add6ea1f44e0e98fc1f diff --git a/javascript/base.css b/javascript/base.css index 75ca46f55..f6a7f7d09 100644 --- a/javascript/base.css +++ b/javascript/base.css @@ -114,7 +114,7 @@ table.settings-value-table td { padding: 0.4em; border: 1px solid #ccc; max-widt /* custom component */ .folder-selector textarea { height: 2em !important; padding: 6px !important; } -.nvml { position: fixed; bottom: 10px; right: 10px; background: var(--background-fill-primary); border: 1px solid var(--button-primary-border-color); padding: 6px; color: var(--button-primary-text-color); +.gpu { position: fixed; bottom: 10px; right: 10px; background: var(--background-fill-primary); border: 1px solid var(--button-primary-border-color); padding: 6px; color: var(--button-primary-text-color); font-size: 0.7em; z-index: 50; font-family: monospace; display: none; } /* image browser */ diff --git a/javascript/contextMenus.js b/javascript/contextMenus.js index 3dfa16b7e..96c57a231 100644 --- a/javascript/contextMenus.js +++ b/javascript/contextMenus.js @@ -149,7 +149,6 @@ async function initContextMenu() { appendContextMenuOption(id, 'Generate forever', () => generateForever(`#${tab}_generate`)); appendContextMenuOption(id, 'Apply selected style', quickApplyStyle); appendContextMenuOption(id, 'Quick save style', quickSaveStyle); - appendContextMenuOption(id, 'nVidia overlay', startNVML); id = `#${tab}_reprocess`; appendContextMenuOption(id, 'Decode full quality', () => reprocessClick(`${tab}`, 'reprocess_decode'), true); appendContextMenuOption(id, 'Refine & HiRes pass', () => reprocessClick(`${tab}`, 'reprocess_refine'), true); diff --git a/javascript/gpu.js b/javascript/gpu.js new file mode 100644 index 000000000..d95542f3d --- /dev/null +++ b/javascript/gpu.js @@ -0,0 +1,75 @@ +let gpuInterval = null; // eslint-disable-line prefer-const +const chartData = { mem: [], load: [] }; + +async function updateGPUChart(mem, load) { + const maxLen = 120; + const colorRangeMap = $.range_map({ // eslint-disable-line no-undef + '0:5': '#fffafa', + '6:10': '#fff7ed', + '11:20': '#fed7aa', + '21:30': '#fdba74', + '31:40': '#fb923c', + '41:50': '#f97316', + '51:60': '#ea580c', + '61:70': '#c2410c', + '71:80': '#9a3412', + '81:90': '#7c2d12', + '91:100': '#6c2e12', + }); + const sparklineConfigLOAD = { type: 'bar', height: '128px', barWidth: '3px', barSpacing: '1px', chartRangeMin: 0, chartRangeMax: 100, barColor: '#89007D' }; + const sparklineConfigMEM = { type: 'bar', height: '128px', barWidth: '3px', barSpacing: '1px', chartRangeMin: 0, chartRangeMax: 100, colorMap: colorRangeMap, composite: true }; + if (chartData.load.length > maxLen) chartData.load.shift(); + chartData.load.push(load); + if (chartData.mem.length > maxLen) chartData.mem.shift(); + chartData.mem.push(mem); + $('#gpuChart').sparkline(chartData.load, sparklineConfigLOAD); // eslint-disable-line no-undef + $('#gpuChart').sparkline(chartData.mem, sparklineConfigMEM); // eslint-disable-line no-undef +} + +async function updateGPU() { + const gpuEl = document.getElementById('gpu'); + const gpuTable = document.getElementById('gpu-table'); + try { + const res = await fetch(`${window.api}/gpu`); + if (!res.ok) { + clearInterval(gpuInterval); + gpuEl.style.display = 'none'; + return; + } + const data = await res.json(); + if (!data) { + clearInterval(gpuInterval); + gpuEl.style.display = 'none'; + return; + } + const gpuTbody = gpuTable.querySelector('tbody'); + for (const gpu of data) { + console.log(gpu); + let rows = `GPU${gpu.name}`; + for (const item of Object.entries(gpu.data)) rows += `${item[0]}${item[1]}`; + gpuTbody.innerHTML = rows; + if (gpu.chart && gpu.chart.length === 2) updateGPUChart(gpu.chart); + } + gpuEl.style.display = 'block'; + } catch (e) { + error('updateGPU', e); + clearInterval(gpuInterval); + gpuEl.style.display = 'none'; + } +} + +async function startGPU() { + const gpuEl = document.getElementById('gpu'); + gpuEl.style.display = 'block'; + if (gpuInterval) clearInterval(gpuInterval); + const interval = window.opts?.gpu_monitor || 3000; + log('startGPU', interval); + gpuInterval = setInterval(updateGPU, interval); + updateGPU(); +} + +async function disableGPU() { + clearInterval(gpuInterval); + const gpuEl = document.getElementById('gpu'); + gpuEl.style.display = 'none'; +} diff --git a/javascript/nvml.js b/javascript/nvml.js deleted file mode 100644 index 155dc3f89..000000000 --- a/javascript/nvml.js +++ /dev/null @@ -1,109 +0,0 @@ -let nvmlInterval = null; // eslint-disable-line prefer-const -let nvmlEl = null; -let nvmlTable = null; -const chartData = { mem: [], load: [] }; - -async function updateNVMLChart(mem, load) { - const maxLen = 120; - const colorRangeMap = $.range_map({ // eslint-disable-line no-undef - '0:5': '#fffafa', - '6:10': '#fff7ed', - '11:20': '#fed7aa', - '21:30': '#fdba74', - '31:40': '#fb923c', - '41:50': '#f97316', - '51:60': '#ea580c', - '61:70': '#c2410c', - '71:80': '#9a3412', - '81:90': '#7c2d12', - '91:100': '#6c2e12', - }); - const sparklineConfigLOAD = { type: 'bar', height: '100px', barWidth: '2px', barSpacing: '1px', chartRangeMin: 0, chartRangeMax: 100, barColor: '#89007D' }; - const sparklineConfigMEM = { type: 'bar', height: '100px', barWidth: '2px', barSpacing: '1px', chartRangeMin: 0, chartRangeMax: 100, colorMap: colorRangeMap, composite: true }; - if (chartData.load.length > maxLen) chartData.load.shift(); - chartData.load.push(load); - if (chartData.mem.length > maxLen) chartData.mem.shift(); - chartData.mem.push(mem); - $('#nvmlChart').sparkline(chartData.load, sparklineConfigLOAD); // eslint-disable-line no-undef - $('#nvmlChart').sparkline(chartData.mem, sparklineConfigMEM); // eslint-disable-line no-undef -} - -async function updateNVML() { - try { - const res = await fetch(`${window.api}/nvml`); - if (!res.ok) { - clearInterval(nvmlInterval); - nvmlEl.style.display = 'none'; - return; - } - const data = await res.json(); - if (!data) { - clearInterval(nvmlInterval); - nvmlEl.style.display = 'none'; - return; - } - const nvmlTbody = nvmlTable.querySelector('tbody'); - for (const gpu of data) { - const rows = ` - GPU${gpu.name} - Driver${gpu.version.driver} - VBIOS${gpu.version.vbios} - ROM${gpu.version.rom} - Driver${gpu.version.driver} - PCIGen.${gpu.pci.link} x${gpu.pci.width} - Memory${gpu.memory.used}Mb / ${gpu.memory.total}Mb - Clock${gpu.clock.gpu[0]}Mhz / ${gpu.clock.gpu[1]}Mhz - Power${gpu.power[0]}W / ${gpu.power[1]}W - Load GPU${gpu.load.gpu}% - Load Memory${gpu.load.memory}% - Temperature${gpu.load.temp}°C - Fans${gpu.load.fan}% - State${gpu.state} - `; - nvmlTbody.innerHTML = rows; - updateNVMLChart(gpu.load.memory, gpu.load.gpu); - } - nvmlEl.style.display = 'block'; - } catch (e) { - clearInterval(nvmlInterval); - nvmlEl.style.display = 'none'; - } -} - -async function initNVML() { - nvmlEl = document.getElementById('nvml'); - if (!nvmlEl) { - nvmlEl = document.createElement('div'); - nvmlEl.className = 'nvml'; - nvmlEl.id = 'nvml'; - nvmlTable = document.createElement('table'); - nvmlTable.className = 'nvml-table'; - nvmlTable.id = 'nvml-table'; - nvmlTable.innerHTML = ` - - - `; - const nvmlChart = document.createElement('div'); - nvmlChart.id = 'nvmlChart'; - nvmlEl.appendChild(nvmlTable); - nvmlEl.appendChild(nvmlChart); - gradioApp().appendChild(nvmlEl); - log('initNVML'); - } -} - -async function startNVML() { - nvmlEl = document.getElementById('nvml'); - if (nvmlInterval) { - clearInterval(nvmlInterval); - nvmlInterval = null; - nvmlEl.style.display = 'none'; - } else { - nvmlInterval = setInterval(updateNVML, 1000); - } -} - -async function disableNVML() { - clearInterval(nvmlInterval); - nvmlEl.style.display = 'none'; -} diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 636aeaec8..6e855e3c5 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -1616,14 +1616,13 @@ background: var(--background-color) padding: 6px !important; } -.nvml { +.gpu { background: var(--background-fill-primary); border: 1px solid var(--button-primary-border-color); bottom: 10px; color: var(--button-primary-text-color); display: none; font-family: monospace; - font-size: var(--text-xxs); padding: 6px; position: fixed; right: 10px; diff --git a/javascript/startup.js b/javascript/startup.js index 3629511ca..842f925ba 100644 --- a/javascript/startup.js +++ b/javascript/startup.js @@ -39,7 +39,6 @@ async function initStartup() { executeCallbacks(uiReadyCallbacks); initLogMonitor(); setupExtraNetworks(); - initNVML(); // optinally wait for modern ui if (window.waitForUiReady) await waitForUiReady(); diff --git a/modules/api/api.py b/modules/api/api.py index 539001ea4..0c151300d 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -5,7 +5,7 @@ from fastapi import FastAPI, APIRouter, Depends, Request from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.exceptions import HTTPException from modules import errors, shared, postprocessing -from modules.api import models, endpoints, script, helpers, server, nvml, generate, process, control, docs +from modules.api import models, endpoints, script, helpers, server, generate, process, control, docs, gpu errors.install() @@ -54,7 +54,7 @@ class Api: self.add_api_route("/sdapi/v1/options", server.get_config, methods=["GET"], response_model=models.OptionsModel) self.add_api_route("/sdapi/v1/options", server.set_config, methods=["POST"]) self.add_api_route("/sdapi/v1/cmd-flags", server.get_cmd_flags, methods=["GET"], response_model=models.FlagsModel) - self.add_api_route("/sdapi/v1/nvml", nvml.get_nvml, methods=["GET"], response_model=List[models.ResNVML]) + self.add_api_route("/sdapi/v1/gpu", gpu.get_gpu_status, methods=["GET"], response_model=List[models.ResGPU]) # core api using locking self.add_api_route("/sdapi/v1/txt2img", self.generate.post_text2img, methods=["POST"], response_model=models.ResTxt2Img) diff --git a/modules/api/gpu.py b/modules/api/gpu.py new file mode 100644 index 000000000..6586fbbc3 --- /dev/null +++ b/modules/api/gpu.py @@ -0,0 +1,28 @@ +import torch +from installer import log + + +device = None + + +def get_gpu_status(): + global device # pylint: disable=global-statement + if device is None: + try: + device = torch.cuda.get_device_name(torch.cuda.current_device()) + log.info(f'GPU monitoring: device={device}') + except Exception: + device = '' + # per vendor modules + if 'nvidia' in device.lower(): + from modules.api import nvml + return nvml.get_nvml() + + +""" +Resut mustb be list[ResGPU] +class ResGPU(BaseModel): + name: str = Field(title="GPU Name") + data: dict = Field(title="Name/Value data") + chart: list[float, float] = Field(title="Exactly two items to place on chart") +""" diff --git a/modules/api/middleware.py b/modules/api/middleware.py index 0d258c878..be136b516 100644 --- a/modules/api/middleware.py +++ b/modules/api/middleware.py @@ -43,7 +43,7 @@ def setup_middleware(app: FastAPI, cmd_opts): endpoint = req.scope.get('path', 'err') token = req.cookies.get("access-token") or req.cookies.get("access-token-unsecure") if (cmd_opts.api_log) and endpoint.startswith('/sdapi'): - if '/sdapi/v1/log' in endpoint or '/sdapi/v1/browser' in endpoint: + if ('/sdapi/v1/log' in endpoint) or ('/sdapi/v1/browser' in endpoint) or ('/sdapi/v1/gpu' in endpoint): return res log.info('API user={user} code={code} {prot}/{ver} {method} {endpoint} {cli} {duration}'.format( # pylint: disable=consider-using-f-string, logging-format-interpolation user = app.tokens.get(token) if hasattr(app, 'tokens') else None, diff --git a/modules/api/models.py b/modules/api/models.py index 010467f66..a97ddff0c 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -424,16 +424,10 @@ class ResScripts(BaseModel): img2img: list = Field(default=None, title="Img2img", description="Titles of scripts (img2img)") control: list = Field(default=None, title="Control", description="Titles of scripts (control)") -class ResNVML(BaseModel): # definition of http response - name: str = Field(title="Name") - version: dict = Field(title="Version") - pci: dict = Field(title="Version") - memory: dict = Field(title="Version") - clock: dict = Field(title="Version") - load: dict = Field(title="Version") - power: list = [] - state: str = Field(title="State") - +class ResGPU(BaseModel): # definition of http response + name: str = Field(title="GPU Name") + data: dict = Field(title="Name/Value data") + chart: list[float, float] = Field(title="Exactly two items to place on chart") # helper function diff --git a/modules/api/nvml.py b/modules/api/nvml.py index ba9d9b12f..4ecc73b0e 100644 --- a/modules/api/nvml.py +++ b/modules/api/nvml.py @@ -43,41 +43,34 @@ def get_nvml(): name = pynvml.nvmlDeviceGetName(dev) except Exception: name = '' - device = { - 'name': name, - 'version': { - 'cuda': pynvml.nvmlSystemGetCudaDriverVersion(), - 'driver': pynvml.nvmlSystemGetDriverVersion(), - 'vbios': pynvml.nvmlDeviceGetVbiosVersion(dev), - 'rom': pynvml.nvmlDeviceGetInforomImageVersion(dev), - 'capabilities': pynvml.nvmlDeviceGetCudaComputeCapability(dev), - }, - 'pci': { - 'link': pynvml.nvmlDeviceGetCurrPcieLinkGeneration(dev), - 'width': pynvml.nvmlDeviceGetCurrPcieLinkWidth(dev), - 'busid': pynvml.nvmlDeviceGetPciInfo(dev).busId, - 'deviceid': pynvml.nvmlDeviceGetPciInfo(dev).pciDeviceId, - }, - 'memory': { - 'total': round(pynvml.nvmlDeviceGetMemoryInfo(dev).total/1024/1024, 2), - 'free': round(pynvml.nvmlDeviceGetMemoryInfo(dev).free/1024/1024,2), - 'used': round(pynvml.nvmlDeviceGetMemoryInfo(dev).used/1024/1024,2), - }, - 'clock': { # gpu, sm, memory - 'gpu': [pynvml.nvmlDeviceGetClockInfo(dev, 0), pynvml.nvmlDeviceGetMaxClockInfo(dev, 0)], - 'sm': [pynvml.nvmlDeviceGetClockInfo(dev, 1), pynvml.nvmlDeviceGetMaxClockInfo(dev, 1)], - 'memory': [pynvml.nvmlDeviceGetClockInfo(dev, 2), pynvml.nvmlDeviceGetMaxClockInfo(dev, 2)], - }, + load = pynvml.nvmlDeviceGetUtilizationRates(dev) + """ 'load': { - 'gpu': round(pynvml.nvmlDeviceGetUtilizationRates(dev).gpu), - 'memory': round(pynvml.nvmlDeviceGetUtilizationRates(dev).memory), + 'gpu': round(load.gpu), + 'memory': round(load.memory), 'temp': pynvml.nvmlDeviceGetTemperature(dev, 0), 'fan': pynvml.nvmlDeviceGetFanSpeed(dev), }, - 'power': [round(pynvml.nvmlDeviceGetPowerUsage(dev)/1000, 2), round(pynvml.nvmlDeviceGetEnforcedPowerLimit(dev)/1000, 2)], - 'state': get_reason(pynvml.nvmlDeviceGetCurrentClocksThrottleReasons(dev)), + 'chart_val1': load.memory, + 'chart_val2': load.gpu, } - devices.append(device) + """ + mem = pynvml.nvmlDeviceGetMemoryInfo(dev) + data = { + "CUDA": f'version {pynvml.nvmlSystemGetCudaDriverVersion()} compute {pynvml.nvmlDeviceGetCudaComputeCapability(dev)}', + "Driver": pynvml.nvmlSystemGetDriverVersion(), + "Hardware": f'VBIOS {pynvml.nvmlDeviceGetVbiosVersion(dev)} ROM {pynvml.nvmlDeviceGetInforomImageVersion(dev)}', + "PCI link": f'gen.{pynvml.nvmlDeviceGetCurrPcieLinkGeneration(dev)} x{pynvml.nvmlDeviceGetCurrPcieLinkWidth(dev)}', + "Power": f'{round(pynvml.nvmlDeviceGetPowerUsage(dev)/1000, 2)} W / {round(pynvml.nvmlDeviceGetEnforcedPowerLimit(dev)/1000, 2)} W', + "GPU clock": f'{pynvml.nvmlDeviceGetClockInfo(dev, 0)} Mhz / {pynvml.nvmlDeviceGetMaxClockInfo(dev, 0)} Mhz', + "SM clock": f'{pynvml.nvmlDeviceGetClockInfo(dev, 1)} Mhz / {pynvml.nvmlDeviceGetMaxClockInfo(dev, 1)} Mhz', + "Memory clock": f'{pynvml.nvmlDeviceGetClockInfo(dev, 2)} Mhz / {pynvml.nvmlDeviceGetMaxClockInfo(dev, 2)} Mhz', + "Memory usage": f'used {round(mem.used / 1024 / 1024)} MB | free {round(mem.free / 1024 / 1024)} MB | total {round(mem.total / 1024 / 1024)} MB', + "System load": f'GPU {load.gpu}% | Memory {load.memory}% | temp {pynvml.nvmlDeviceGetTemperature(dev, 0)}C | fan {pynvml.nvmlDeviceGetFanSpeed(dev)}%', + 'State': get_reason(pynvml.nvmlDeviceGetCurrentClocksThrottleReasons(dev)), + } + chart = [load.memory, load.gpu] + devices.append({ 'name': name, 'data': data, 'chart': chart }) # log.debug(f'nmvl: {devices}') return devices except Exception as e: diff --git a/modules/shared.py b/modules/shared.py index 31d878601..628b83911 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -535,6 +535,7 @@ options_templates.update(options_section(('ui', "User Interface"), { "other_sep_ui": OptionInfo("

Other...

", "", gr.HTML), "ui_locale": OptionInfo("Auto", "UI locale", gr.Dropdown, lambda: {"choices": theme.list_locales()}), "font_size": OptionInfo(14, "Font size", gr.Slider, {"minimum": 8, "maximum": 32, "step": 1}), + "gpu_monitor": OptionInfo(3000, "GPU monitor interval", gr.Slider, {"minimum": 100, "maximum": 60000, "step": 100}), "aspect_ratios": OptionInfo("1:1, 4:3, 3:2, 16:9, 16:10, 21:9, 2:3, 3:4, 9:16, 10:16, 9:21", "Allowed aspect ratios"), "compact_view": OptionInfo(False, "Compact view"), "ui_columns": OptionInfo(4, "Gallery view columns", gr.Slider, {"minimum": 1, "maximum": 8, "step": 1}), diff --git a/modules/ui_settings.py b/modules/ui_settings.py index 9a5a47a13..f15fd8985 100644 --- a/modules/ui_settings.py +++ b/modules/ui_settings.py @@ -251,6 +251,22 @@ def create_ui(): with gr.TabItem("History", id="system_history", elem_id="tab_history"): ui_history.create_ui() + with gr.TabItem("GPU Monitor", id="system_gpu", elem_id="tab_gpu"): + with gr.Row(elem_id='gpu-controls'): + gpu_start = gr.Button(value="Start", elem_id="gpu_start", variant="primary") + gpu_stop = gr.Button(value="Stop", elem_id="gpu_stop", variant="primary") + gpu_start.click(fn=lambda: None, _js='startGPU', inputs=[], outputs=[]) + gpu_stop.click(fn=lambda: None, _js='disableGPU', inputs=[], outputs=[]) + gr.HTML(''' +
+ + + +
+
+
+ ''', elem_id='gpu-container', visible=True) + with gr.TabItem("ONNX", id="onnx_config", elem_id="tab_onnx"): from modules.onnx_impl import ui as ui_onnx ui_onnx.create_ui() diff --git a/scripts/prompt_enhance.py b/scripts/prompt_enhance.py index 697f5691f..622a12374 100644 --- a/scripts/prompt_enhance.py +++ b/scripts/prompt_enhance.py @@ -57,6 +57,7 @@ class Options: 'cognitivecomputations/Dolphin3.0-Llama3.2-1B': {}, 'cognitivecomputations/Dolphin3.0-Llama3.2-3B': {}, 'nidum/Nidum-Gemma-3-4B-it-Uncensored': {}, + 'allura-org/Gemma-3-Glitter-4B': {}, # 'llava/Llama-3-8B-v1.1-Extracted': { # 'repo': 'hunyuanvideo-community/HunyuanVideo', # 'subfolder': 'text_encoder', From 5d9c0675e521f30f6864dcedb269157c09d9f436 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 7 Aug 2025 13:56:11 -0400 Subject: [PATCH 041/141] cleanup Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- modules/api/gpu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index d26fde293..303612cd9 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit d26fde293e8a2632e2c08add6ea1f44e0e98fc1f +Subproject commit 303612cd94248463b1835ab2f10630fde0c7923e diff --git a/modules/api/gpu.py b/modules/api/gpu.py index 6586fbbc3..7699dd5d0 100644 --- a/modules/api/gpu.py +++ b/modules/api/gpu.py @@ -20,7 +20,7 @@ def get_gpu_status(): """ -Resut mustb be list[ResGPU] +Resut should always be: list[ResGPU] class ResGPU(BaseModel): name: str = Field(title="GPU Name") data: dict = Field(title="Name/Value data") From 7e5e9eb8bbd4c85868eafab40b83fd08d3c3a3fa Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 7 Aug 2025 14:08:29 -0400 Subject: [PATCH 042/141] add basic vace support Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 9 ++++++--- TODO.md | 1 - modules/video_models/models_def.py | 12 ++++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c535695b..1509dbfb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### Highlights for 2025-08-07 -Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/), [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release), [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) +Several new and updated models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/), [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release), [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) Plus continuing with major **UI** work, there is new embedded **Docs/Wiki** search, redesigned real-time **hints**, built-in **GPU monitor**, **CivitAI** integration and more! On the compute side, new profiles for high-vram GPUs and offloading improvements And (*as always*) many bugfixes and improvements to existing features! @@ -34,8 +34,11 @@ And (*as always*) many bugfixes and improvements to existing features! - [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) SkyReels-V2 is a genarative video model based on Wan-2.1 but with heavily modified execution to allow for infinite-length video generation supported variants are: - - diffusion-forcing: T2I DF 1.3B for 540p videos, T2I DF 14B for 720p videos, I2I DF 14B for 720p videos - - standard: T2I 14B for 720p videos and I2I 14B for 720p videos + - diffusion-forcing: *T2I DF 1.3B* for 540p videos, *T2I DF 14B* for 720p videos, *I2I DF 14B* for 720p videos + - standard: *T2I 14B* for 720p videos and *I2I 14B* for 720p videos + - [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) + basic support for *Wan 2.1 VACE 1.3B* and *14B* variants + optimized support with granular guidance control will follow soon **Torch** - Set default for ROCm and OpenVINO to `torch==2.8.0` - **UI** diff --git a/TODO.md b/TODO.md index b3b35f3e0..4a8f601f2 100644 --- a/TODO.md +++ b/TODO.md @@ -15,7 +15,6 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma - Video: LTX PromptEnhance - Video: LTX Conditioning preprocess - [WanAI-2.1 VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B)(https://github.com/huggingface/diffusers/pull/11582) -- [SkyReels-v2](https://github.com/SkyworkAI/SkyReels-V2)(https://github.com/huggingface/diffusers/pull/11518) - [Cosmos-Predict2-Video](https://huggingface.co/nvidia/Cosmos-Predict2-2B-Video2World)(https://github.com/huggingface/diffusers/pull/11695) ### Blocked items diff --git a/modules/video_models/models_def.py b/modules/video_models/models_def.py index 4111ee517..c95fe8442 100644 --- a/modules/video_models/models_def.py +++ b/modules/video_models/models_def.py @@ -197,6 +197,18 @@ models = { repo_cls=diffusers.WanImageToVideoPipeline, te_cls=transformers.T5EncoderModel, dit_cls=diffusers.WanTransformer3DModel), + Model(name='WAN 2.1 VACE 1.3B', + url='https://huggingface.co/Wan-AI/Wan2.1-VACE-1.3B-diffusers', + repo='Wan-AI/Wan2.1-VACE-1.3B-diffusers', + repo_cls=diffusers.WanVACEPipeline, + te_cls=transformers.T5EncoderModel, + dit_cls=diffusers.WanTransformer3DModel), + Model(name='WAN 2.1 VACE 14B', + url='https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers', + repo='Wan-AI/Wan2.1-VACE-14B-diffusers', + repo_cls=diffusers.WanVACEPipeline, + te_cls=transformers.T5EncoderModel, + dit_cls=diffusers.WanTransformer3DModel), ], 'SkyReels V2': [ Model(name='None'), From e111d151e50e06399a05012af13c30c10111c301 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 7 Aug 2025 14:19:43 -0400 Subject: [PATCH 043/141] lint Signed-off-by: Vladimir Mandic --- modules/api/gpu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/api/gpu.py b/modules/api/gpu.py index 7699dd5d0..e3dc94866 100644 --- a/modules/api/gpu.py +++ b/modules/api/gpu.py @@ -17,6 +17,7 @@ def get_gpu_status(): if 'nvidia' in device.lower(): from modules.api import nvml return nvml.get_nvml() + return [] """ From a8956f259edbd71001a0ead7e4b84d045e8ea25d Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 7 Aug 2025 15:10:44 -0400 Subject: [PATCH 044/141] use torch==2.8.0 as default for cuda Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 7 ++++--- installer.py | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1509dbfb1..522a2ca3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,8 @@ And (*as always*) many bugfixes and improvements to existing features! basic support for *Wan 2.1 VACE 1.3B* and *14B* variants optimized support with granular guidance control will follow soon **Torch** - - Set default for ROCm and OpenVINO to `torch==2.8.0` + - Set default to `torch==2.8.0` for *CUDA, ROCm and OpenVINO* + - Add support for `torch==2.9.0` - **UI** - new embedded docs/wiki search! **Docs** search: fully-local and works in real-time on all document pages @@ -75,8 +76,8 @@ And (*as always*) many bugfixes and improvements to existing features! - new feature to specify which modules to offload always or never in *settings -> model offloading -> offload always/never* - new `highvram` profile provides significant performance boost on gpus with more than 24gb - - new `offload during pre-forward` option - in *settings -> model offloading* + - new `offload during pre-forward` option + in *settings -> model offloading* switches from explicit offloading to implicit offloading on module execution change - new `diffusers_offload_nonblocking` exerimental setting instructs torch to use non-blocking move operations when possible diff --git a/installer.py b/installer.py index 9a2e847cd..ed6315585 100644 --- a/installer.py +++ b/installer.py @@ -648,8 +648,7 @@ def install_cuda(): if args.use_nightly: cmd = os.environ.get('TORCH_COMMAND', '--upgrade --pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/cu128 --extra-index-url https://download.pytorch.org/whl/nightly/cu126') else: - # cmd = os.environ.get('TORCH_COMMAND', 'torch==2.6.0+cu126 torchvision==0.21.0+cu126 --index-url https://download.pytorch.org/whl/cu126') - cmd = os.environ.get('TORCH_COMMAND', 'torch==2.7.1+cu128 torchvision==0.22.1+cu128 --index-url https://download.pytorch.org/whl/cu128') + cmd = os.environ.get('TORCH_COMMAND', 'torch==2.8.0+cu128 torchvision==0.23.0+cu128 --index-url https://download.pytorch.org/whl/cu128') return cmd From 3edb80e1beaf1de867b3ba49686c7f94700d99e7 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Thu, 7 Aug 2025 23:23:24 +0300 Subject: [PATCH 045/141] Add api/rocm_smi.py --- modules/api/gpu.py | 8 +++ modules/api/rocm_smi.py | 107 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 modules/api/rocm_smi.py diff --git a/modules/api/gpu.py b/modules/api/gpu.py index e3dc94866..7f33156c4 100644 --- a/modules/api/gpu.py +++ b/modules/api/gpu.py @@ -17,6 +17,9 @@ def get_gpu_status(): if 'nvidia' in device.lower(): from modules.api import nvml return nvml.get_nvml() + elif 'amd' in device.lower(): + from modules.api import rocm_smi + return rocm_smi.get_rocm_smi() return [] @@ -27,3 +30,8 @@ class ResGPU(BaseModel): data: dict = Field(title="Name/Value data") chart: list[float, float] = Field(title="Exactly two items to place on chart") """ + +if __name__ == '__main__': + from rich import print as rprint + for gpu in get_gpu_status(): + rprint(gpu) diff --git a/modules/api/rocm_smi.py b/modules/api/rocm_smi.py new file mode 100644 index 000000000..136233061 --- /dev/null +++ b/modules/api/rocm_smi.py @@ -0,0 +1,107 @@ +import math +import json +import subprocess as sp +from enum import IntFlag + +try: + from installer import log +except Exception: + import logging + log = logging.getLogger(__name__) + +try: + from modules.rocm import version as rocm_version +except Exception: + rocm_version = "unknown" + + +# ThrottleStatus is from leuc/amdgpu_metrics.py +class ThrottleStatus(IntFlag): + # linux/drivers/gpu/drm/amd/pm/inc/amdgpu_smu.h + PPT0 = 1 << 0 + PPT1 = 1 << 1 + PPT2 = 1 << 2 + PPT3 = 1 << 3 + SPL = 1 << 4 + FPPT = 1 << 5 + SPPT = 1 << 6 + SPPT_APU = 1 << 7 + TDC_GFX = 1 << 16 + TDC_SOC = 1 << 17 + TDC_MEM = 1 << 18 + TDC_VDD = 1 << 19 + TDC_CVIP = 1 << 20 + EDC_CPU = 1 << 21 + EDC_GFX = 1 << 22 + APCC = 1 << 23 + TEMP_GPU = 1 << 32 + TEMP_CORE = 1 << 33 + TEMP_MEM = 1 << 34 + TEMP_EDGE = 1 << 35 + TEMP_HOTSPOT = 1 << 36 + TEMP_SOC = 1 << 37 + TEMP_VR_GFX = 1 << 38 + TEMP_VR_SOC = 1 << 39 + TEMP_VR_MEM0 = 1 << 40 + TEMP_VR_MEM1 = 1 << 41 + TEMP_LIQUID0 = 1 << 42 + TEMP_LIQUID1 = 1 << 43 + VRHOT0 = 1 << 44 + VRHOT1 = 1 << 45 + PROCHOT_CPU = 1 << 46 + PROCHOT_GFX = 1 << 47 + PPM = 1 << 56 + FIT = 1 << 57 + + def active(self): + members = self.__class__.__members__ + return (m for m in members if getattr(self, m)._value_ & self.value != 0) + + def __iter__(self): + return self.active() + + def __str__(self): + return u', '.join(self.active()) + + +def get_rocm_smi(): + try: + rocm_smi_data = json.loads(sp.check_output(("rocm-smi", "-a", "--json"))) + driver_version = rocm_smi_data.pop("system", {"Driver version": "unknown"}).get("Driver version") + + devices = [] + for key in rocm_smi_data.keys(): + load = { + 'gpu': rocm_smi_data[key].get('GPU use (%)', 'unknown'), + 'memory': rocm_smi_data[key].get("GPU Memory Allocated (VRAM%)", "unknown"), + 'temp': rocm_smi_data[key].get('Temperature (Sensor edge) (C)', 'unknown'), + 'temp_junction': rocm_smi_data[key].get('Temperature (Sensor junction) (C)', 'unknown'), + 'temp_memory': rocm_smi_data[key].get('Temperature (Sensor memory) (C)', 'unknown'), + 'fan': rocm_smi_data[key].get('Fan speed (%)', 'unknown'), + } + + data = { + "ROCm": f'version {rocm_version} agent {rocm_smi_data[key].get("GFX Version", "unknown")}', + "Driver": driver_version, + "Hardware": f'VBIOS {rocm_smi_data[key].get("VBIOS version", "unknown")}', + "PCI link": f'gen.{int(math.log2(float(rocm_smi_data[key].get("pcie_link_speed (0.1 GT/s)", 10)) / 10))} x{rocm_smi_data[key].get("pcie_link_width (Lanes)", "unknown")}', + "Power": f'{round(float(rocm_smi_data[key].get("Average Graphics Package Power (W)", 0)), 2)} W / {round(float(rocm_smi_data[key].get("Max Graphics Package Power (W)", 0)), 2)} W', + "GPU clock": f'{rocm_smi_data[key].get("average_gfxclk_frequency (MHz)", 0)} Mhz / {rocm_smi_data[key].get("Valid sclk range", "0").split(" - ")[-1].removesuffix("Mhz")} Mhz', + "Memory clock": f'{rocm_smi_data[key].get("current_uclk (MHz)", 0)} Mhz / {rocm_smi_data[key].get("Valid mclk range", "0").split(" - ")[-1].removesuffix("Mhz")} Mhz', + "Memory usage": f'used {load["memory"]}% | activity {rocm_smi_data[key].get("GPU Memory Read/Write Activity (%)", "unknown")}%', + "GPU usage": f'GPU {load["gpu"]}% | fan {load["fan"]}%', + "GPU temp": f'edge {load["temp"]}C | junction {load["temp_junction"]}C | memory {load["temp_memory"]}C', + 'Throttle reason': str(ThrottleStatus(int(rocm_smi_data[key].get("throttle_status", 0)))), + } + chart = [rocm_smi_data[key].get("GPU Memory Allocated (VRAM%)", "unknown"), load["gpu"]] + devices.append({ 'name': rocm_smi_data[key].get('Device Name', 'unknown'), 'data': data, 'chart': chart }) + return devices + except Exception as e: + log.error(f'ROCm SMI: {e}') + return [] + + +if __name__ == '__main__': + from rich import print as rprint + for gpu in get_rocm_smi(): + rprint(gpu) From 5ffb6e8b88e96d4308dca4ab18d250bde5fcf7dd Mon Sep 17 00:00:00 2001 From: Disty0 Date: Thu, 7 Aug 2025 23:36:38 +0300 Subject: [PATCH 046/141] Cleanup --- modules/api/rocm_smi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/api/rocm_smi.py b/modules/api/rocm_smi.py index 136233061..946d92255 100644 --- a/modules/api/rocm_smi.py +++ b/modules/api/rocm_smi.py @@ -84,7 +84,7 @@ def get_rocm_smi(): "ROCm": f'version {rocm_version} agent {rocm_smi_data[key].get("GFX Version", "unknown")}', "Driver": driver_version, "Hardware": f'VBIOS {rocm_smi_data[key].get("VBIOS version", "unknown")}', - "PCI link": f'gen.{int(math.log2(float(rocm_smi_data[key].get("pcie_link_speed (0.1 GT/s)", 10)) / 10))} x{rocm_smi_data[key].get("pcie_link_width (Lanes)", "unknown")}', + "PCI link": f'Gen.{int(math.log2(float(rocm_smi_data[key].get("pcie_link_speed (0.1 GT/s)", 10)) / 10))} x{rocm_smi_data[key].get("pcie_link_width (Lanes)", "unknown")}', "Power": f'{round(float(rocm_smi_data[key].get("Average Graphics Package Power (W)", 0)), 2)} W / {round(float(rocm_smi_data[key].get("Max Graphics Package Power (W)", 0)), 2)} W', "GPU clock": f'{rocm_smi_data[key].get("average_gfxclk_frequency (MHz)", 0)} Mhz / {rocm_smi_data[key].get("Valid sclk range", "0").split(" - ")[-1].removesuffix("Mhz")} Mhz', "Memory clock": f'{rocm_smi_data[key].get("current_uclk (MHz)", 0)} Mhz / {rocm_smi_data[key].get("Valid mclk range", "0").split(" - ")[-1].removesuffix("Mhz")} Mhz', @@ -93,7 +93,7 @@ def get_rocm_smi(): "GPU temp": f'edge {load["temp"]}C | junction {load["temp_junction"]}C | memory {load["temp_memory"]}C', 'Throttle reason': str(ThrottleStatus(int(rocm_smi_data[key].get("throttle_status", 0)))), } - chart = [rocm_smi_data[key].get("GPU Memory Allocated (VRAM%)", "unknown"), load["gpu"]] + chart = [load["memory"], load["gpu"]] devices.append({ 'name': rocm_smi_data[key].get('Device Name', 'unknown'), 'data': data, 'chart': chart }) return devices except Exception as e: From c0b8c4e2cb425a2bb16e031f9e20746aba100572 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 7 Aug 2025 17:11:12 -0400 Subject: [PATCH 047/141] update changelog Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + modules/ui_models.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 522a2ca3c..0f6182695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ And (*as always*) many bugfixes and improvements to existing features! - redesigned **GPU monitor** - standard-ui: *system -> gpu monitor* - modern-ui: *aside -> console -> gpu monitor* + - supported for *nVidia CUDA* and *AMD ROCm* platforms - configurable interval in *settings -> user interface* - updated *models* tab - updated *models -> current* tab diff --git a/modules/ui_models.py b/modules/ui_models.py index 141558878..5a7b58076 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -500,7 +500,7 @@ def create_ui(): with gr.Row(): civit_nsfw = gr.Checkbox(label='NSFW allowed', value=True) with gr.Row(): - civit_type = gr.Textbox(label='Model type', placeholder='Checkpoint, LORA, ...') + civit_type = gr.Textbox(label='Target model type', placeholder='Checkpoint, LORA, ...', value='') with gr.Row(): # civit_base = gr.Textbox(label='Base model', placeholder='SDXL, ...') civit_base = gr.Dropdown(choices=base_models, label='Base model', value='') From 9ef6baf2ed234e97a8457bd1441dfabf3c9d2c0d Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 8 Aug 2025 07:36:11 -0400 Subject: [PATCH 048/141] use `utf_16_be` as primary metadata decoding Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 9 +++------ cli/image-exif.py | 5 ++++- installer.py | 2 +- modules/images.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f6182695..b7a1dcd33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,8 @@ # Change Log for SD.Next -## Blockers +## Update for 2025-08-08 -- Qwen with offloading: - -## Update for 2025-08-07 - -### Highlights for 2025-08-07 +### Highlights for 2025-08-08 Several new and updated models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/), [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release), [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) Plus continuing with major **UI** work, there is new embedded **Docs/Wiki** search, redesigned real-time **hints**, built-in **GPU monitor**, **CivitAI** integration and more! @@ -106,6 +102,7 @@ And (*as always*) many bugfixes and improvements to existing features! - fix openvino backend failing to compile - fix nunchaku fallback on unsupported model - fix nunchaku windows download links + - use `utf_16_be` as primary metadata decoding - reapply offloading on ipadapter load - api set default script-name - avoid forced gc and rely on thresholds diff --git a/cli/image-exif.py b/cli/image-exif.py index 9a48d2dd7..fc2573220 100755 --- a/cli/image-exif.py +++ b/cli/image-exif.py @@ -64,7 +64,10 @@ class Exif: # pylint: disable=single-string-used-for-slots def decode(self, s: bytes): remove_prefix = lambda text, prefix: text[len(prefix):] if text.startswith(prefix) else text # pylint: disable=unnecessary-lambda-assignment - for encoding in ['utf-8', 'utf-16', 'ascii', 'latin_1', 'cp1252', 'cp437']: # try different encodings + # from encodings.aliases import aliases + # cp = list(set(aliases.values())) + for encoding in ['utf_16_be', 'utf-8', 'utf-16', 'ascii', 'latin_1', 'cp1252', 'cp437']: # try different encodings + # for encoding in cp: try: s = remove_prefix(s, b'UNICODE') s = remove_prefix(s, b'ASCII') diff --git a/installer.py b/installer.py index ed6315585..800edd0a7 100644 --- a/installer.py +++ b/installer.py @@ -593,7 +593,7 @@ def check_diffusers(): t_start = time.time() if args.skip_all or args.skip_git: return - sha = '7ea065c5070a5278259e6f1effa9dccea232e62a' # diffusers commit hash + sha = '7b10e4ae65cc5830c581fba58638f5afb6e587cf' # diffusers commit hash pkg = pkg_resources.working_set.by_key.get('diffusers', None) minor = int(pkg.version.split('.')[1] if pkg is not None else -1) cur = opts.get('diffusers_version', '') if minor > -1 else '' diff --git a/modules/images.py b/modules/images.py index c8d13f92c..9c3992d23 100644 --- a/modules/images.py +++ b/modules/images.py @@ -218,7 +218,7 @@ def save_image(image, def safe_decode_string(s: bytes): remove_prefix = lambda text, prefix: text[len(prefix):] if text.startswith(prefix) else text # pylint: disable=unnecessary-lambda-assignment - for encoding in ['utf-8', 'utf-16', 'ascii', 'latin_1', 'cp1252', 'cp437']: # try different encodings + for encoding in ['utf_16_be', 'utf-8', 'utf-16', 'ascii', 'latin_1', 'cp1252', 'cp437']: # try different encodings try: s = remove_prefix(s, b'UNICODE') s = remove_prefix(s, b'ASCII') From 353d73e0421439dd96dfa71e7d37a0e8d37775e1 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 8 Aug 2025 10:15:00 -0400 Subject: [PATCH 049/141] add chroma-v50 and chroma-v50-annealed Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 2 +- html/reference.json | 14 ++++++++++++++ modules/sd_models.py | 2 +- pipelines/chroma/convert_chroma.py | 5 ++--- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a1dcd33..8ef24c9cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ And (*as always*) many bugfixes and improvements to existing features! available via *networks -> models -> reference* - [Chroma](https://huggingface.co/lodestones/Chroma) great model based on FLUX.1 and then redesigned and retrained by *lodestones* - update with latest **v48**, **v48 Detail Calibrated** and **v46 Flash** variants + update with latest **v50**, **v50 Annealed**, **v48**, **v48 Detail Calibrated** and **v46 Flash** variants available via *networks -> models -> reference* - [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) SkyReels-V2 is a genarative video model based on Wan-2.1 but with heavily modified execution to allow for infinite-length video generation diff --git a/html/reference.json b/html/reference.json index 853608faa..0bfa15f16 100644 --- a/html/reference.json +++ b/html/reference.json @@ -173,6 +173,20 @@ "extras": "sampler: Default, cfg_scale: 4.5" }, + "lodestones Chroma Unlocked v50": { + "path": "vladmandic/chroma-unlocked-v50", + "preview": "lodestones--Chroma.jpg", + "desc": "Chroma is a 8.9B parameter model based on FLUX.1-schnell. It’s fully Apache 2.0 licensed, ensuring that anyone can use, modify, and build on top of it—no corporate gatekeeping. The model is still training right now, and I’d love to hear your thoughts! Your input and feedback are really appreciated.", + "skip": true, + "extras": "sampler: Default, cfg_scale: 3.5" + }, + "lodestones Chroma Unlocked v50 Annealed": { + "path": "vladmandic/chroma-unlocked-v50-annealed", + "preview": "lodestones--Chroma.jpg", + "desc": "Chroma is a 8.9B parameter model based on FLUX.1-schnell. It’s fully Apache 2.0 licensed, ensuring that anyone can use, modify, and build on top of it—no corporate gatekeeping. The model is still training right now, and I’d love to hear your thoughts! Your input and feedback are really appreciated.", + "skip": true, + "extras": "sampler: Default, cfg_scale: 3.5" + }, "lodestones Chroma Unlocked v48": { "path": "vladmandic/chroma-unlocked-v48", "preview": "lodestones--Chroma.jpg", diff --git a/modules/sd_models.py b/modules/sd_models.py index dc2a14390..50869eb72 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -907,7 +907,7 @@ def set_diffuser_pipe(pipe, new_pipe_type): elif new_pipe_type == DiffusersTaskType.INPAINTING: new_pipe = diffusers.AutoPipelineForInpainting.from_pipe(pipe) else: - shared.log.error(f'Pipeline class change failed: type={new_pipe_type} pipeline={cls}') + shared.log.warning(f'Pipeline class change failed: type={new_pipe_type} pipeline={cls}') return pipe except Exception as e: # pylint: disable=unused-variable shared.log.warning(f'Pipeline class change failed: type={new_pipe_type} pipeline={cls} {e}') diff --git a/pipelines/chroma/convert_chroma.py b/pipelines/chroma/convert_chroma.py index ea49d3bee..a560bac18 100644 --- a/pipelines/chroma/convert_chroma.py +++ b/pipelines/chroma/convert_chroma.py @@ -12,9 +12,8 @@ convert = True test = False upload = True input_files = [ - 'chroma-unlocked-v48.safetensors', - 'chroma-unlocked-v48-detail-calibrated.safetensors', - 'chroma-unlocked-v46-flash.safetensors', + 'chroma-unlocked-v50.safetensors', + 'chroma-unlocked-v50-annealed.safetensors', ] input_folder = '/mnt/models/UNET' output_folder = '/mnt/models/Diffusers' From a345ae0e3b2f3bf04e6857c0d4d7e253fa12bc85 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 8 Aug 2025 11:44:21 -0400 Subject: [PATCH 050/141] flux-kontext with variable resolution Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 10 ++++++---- modules/processing_args.py | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ef24c9cd..b9d446981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ### Highlights for 2025-08-08 -Several new and updated models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/), [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release), [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) +Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/), [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) +And several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) Plus continuing with major **UI** work, there is new embedded **Docs/Wiki** search, redesigned real-time **hints**, built-in **GPU monitor**, **CivitAI** integration and more! On the compute side, new profiles for high-vram GPUs and offloading improvements And (*as always*) many bugfixes and improvements to existing features! @@ -91,7 +92,7 @@ And (*as always*) many bugfixes and improvements to existing features! - **Fixes** - refactor legacy processing loop - fix settings components mismatch - - fix Wan 2.2-5B I2V workflow + - fix *Wan 2.2-5B I2V* workflow - fix OpenVINO - fix video model vs pipeline mismatch - fix video generic save frames @@ -100,8 +101,9 @@ And (*as always*) many bugfixes and improvements to existing features! - fix progress bar with refine/detailer - fix api progress reporting endpoint - fix openvino backend failing to compile - - fix nunchaku fallback on unsupported model - - fix nunchaku windows download links + - fix `nunchaku` fallback on unsupported model + - fix `nunchaku` windows download links + - fix *Flux.1-Kontext-Dev* with variable resolution - use `utf_16_be` as primary metadata decoding - reapply offloading on ipadapter load - api set default script-name diff --git a/modules/processing_args.py b/modules/processing_args.py index 24ea8e6e5..e45626d26 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -99,6 +99,8 @@ def task_specific_kwargs(p, model): 'height': height, 'width': width, } + + # model specific args if model.__class__.__name__ == 'LatentConsistencyModelPipeline' and hasattr(p, 'init_images') and len(p.init_images) > 0: p.ops.append('lcm') init_latents = [processing_vae.vae_encode(image, model=shared.sd_model, vae_type=p.vae_type).squeeze(dim=0) for image in p.init_images] @@ -120,6 +122,8 @@ def task_specific_kwargs(p, model): 'target_subject_category': getattr(p, 'prompt', '').split()[-1], 'output_type': 'pil', } + +# TODO if debug_enabled: debug_log(f'Process task specific args: {task_args}') return task_args @@ -388,6 +392,8 @@ def set_pipeline_args(p, model, prompts:list, negative_prompts:list, prompts_2:t else: args['width'] = 8 * math.ceil(args['image'][0].width / 8) args['height'] = 8 * math.ceil(args['image'][0].height / 8) + if 'max_area' in possible and 'width' in args and 'height' in args and 'max_area' not in args: + args['max_area'] = args['width'] * args['height'] # handle implicit controlnet if 'control_image' in possible and 'control_image' not in args and 'image' in args: From 80f603f3ff6110ed5ec1fd8e59ff4df8377b8ee9 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 8 Aug 2025 13:07:50 -0400 Subject: [PATCH 051/141] fix sd35 width/height alignment Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + html/reference.json | 2 +- modules/processing_args.py | 7 +++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9d446981..e6f00ded4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,7 @@ And (*as always*) many bugfixes and improvements to existing features! - fix `nunchaku` windows download links - fix *Flux.1-Kontext-Dev* with variable resolution - use `utf_16_be` as primary metadata decoding + - fix `sd35` width/height alignment - reapply offloading on ipadapter load - api set default script-name - avoid forced gc and rely on thresholds diff --git a/html/reference.json b/html/reference.json index 0bfa15f16..7988b3107 100644 --- a/html/reference.json +++ b/html/reference.json @@ -201,7 +201,7 @@ "skip": true, "extras": "sampler: Default, cfg_scale: 3.5" }, - "lodestones Chroma Unlocked v48 Flash": { + "lodestones Chroma Unlocked v46 Flash": { "path": "vladmandic/chroma-unlocked-v46-flash", "preview": "lodestones--Chroma.jpg", "desc": "Chroma is a 8.9B parameter model based on FLUX.1-schnell. It’s fully Apache 2.0 licensed, ensuring that anyone can use, modify, and build on top of it—no corporate gatekeeping. The model is still training right now, and I’d love to hear your thoughts! Your input and feedback are really appreciated.", diff --git a/modules/processing_args.py b/modules/processing_args.py index e45626d26..5959f98f4 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -122,8 +122,11 @@ def task_specific_kwargs(p, model): 'target_subject_category': getattr(p, 'prompt', '').split()[-1], 'output_type': 'pil', } - -# TODO + if model.__class__.__name__ == 'StableDiffusion3Pipeline': + p.width = 16 * (p.width // 16) + p.height = 16 * (p.height // 16) + task_args['width'] = p.width + task_args['height'] = p.height if debug_enabled: debug_log(f'Process task specific args: {task_args}') return task_args From 00e34ce0d3a167645ad483ef319575e146ed5da5 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 8 Aug 2025 14:07:33 -0400 Subject: [PATCH 052/141] cleanup reference models Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + html/reference.json | 77 ++++++++------------- models/Reference/tempest-by-vlad-base.jpg | Bin 0 -> 34916 bytes models/Reference/tempest-by-vlad-hyper.jpg | Bin 0 -> 26074 bytes modules/processing_args.py | 2 +- modules/processing_diffusers.py | 3 +- modules/prompt_parser_xhinker.py | 11 ++- 7 files changed, 38 insertions(+), 56 deletions(-) create mode 100644 models/Reference/tempest-by-vlad-base.jpg create mode 100644 models/Reference/tempest-by-vlad-hyper.jpg diff --git a/CHANGELOG.md b/CHANGELOG.md index e6f00ded4..b82b0560b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,7 @@ And (*as always*) many bugfixes and improvements to existing features! - refactor legacy processing loop - fix settings components mismatch - fix *Wan 2.2-5B I2V* workflow + - fix *Wan* T2I workflow - fix OpenVINO - fix video model vs pipeline mismatch - fix video generic save frames diff --git a/html/reference.json b/html/reference.json index 7988b3107..325dbf760 100644 --- a/html/reference.json +++ b/html/reference.json @@ -1,25 +1,26 @@ + { - "Tempest SD-XL v0.1": { - "path": "TempestV0.1-Artistic.safetensors@https://huggingface.co/dataautogpt3/TempestV0.1/resolve/main/TempestV0.1-Artistic.safetensors?download=true", - "preview": "TempestV0.1-Artistic.jpg", - "desc": "The TempestV0.1 Initiative is a powerhouse in image generation, leveraging an unparalleled dataset of over 6 million images. The collection's vast scale, with resolutions from 1400x2100 to 4800x7200, encompasses 200GB of high-quality content.", - "extras": "width: 2048, height: 1024, sampler: DEIS, steps: 40, cfg_scale: 6.0" + "Tempest-by-Vlad XL": { + "path": "tempestByVlad_baseV01.safetensors@https://civitai.com/api/download/models/1301775", + "preview": "tempest-by-vlad-base.jpg", + "desc": "Flexible SDXL model with custom encoder and finetuned for larger landscape resolutions with high details and high contrast.", + "extras": "" + }, + "Tempest-by-Vlad XL Hyper": { + "path": "tempestByVlad_hyperV01.safetensors@https://civitai.com/api/download/models/1343512", + "preview": "tempest-by-vlad-hyper.jpg", + "desc": "Custom distilled variant with goal to get as-normal-as-possible model that works with low steps and guidance-free", + "extras": "" }, - "Juggernaut SD-XL XI": { + "Juggernaut XL XI": { "path": "juggernautXL_juggXIByRundiffusion.safetensors@https://civitai.com/api/download/models/782002", "preview": "juggernautXL_v9Rundiffusionphoto2.jpg", "desc": "Showcase finetuned model based on Stable diffusion XL", "extras": "sampler: DEIS, steps: 20, cfg_scale: 6.0" }, - "Juggernaut SD-XL X Hyper": { - "path": "Juggernaut_X_RunDiffusion_Hyper.safetensors@https://civitai.com/api/download/models/471120", - "preview": "juggernautXL_v9Rundiffusionphoto2.jpg", - "desc": "Showcase finetuned model based on Stable diffusion XL", - "extras": "sampler: DEIS, steps: 20, cfg_scale: 6.0" - }, - "Juggernaut SD-XL IX Lightning": { - "path": "juggernautXL_v9Rdphoto2Lightning.safetensors@https://civitai.com/api/download/models/357609", + "Juggernaut XL XI Lightning": { + "path": "juggernautXL_juggXILightningByRD.safetensors@https://civitai.com/api/download/models/920957", "preview": "juggernautXL_v9Rdphoto2Lightning.jpg", "desc": "Showcase finetuned model based on Stable diffusion XL", "extras": "sampler: DPM SDE, steps: 6, cfg_scale: 2.0" @@ -32,40 +33,6 @@ "extras": "width: 512, height: 512, sampler: DEIS, steps: 20, cfg_scale: 6.0" }, - "DreamShaper SD v8": { - "original": true, - "path": "dreamshaper_8.safetensors@https://civitai.com/api/download/models/128713", - "preview": "dreamshaper_8.jpg", - "desc": "Showcase finetuned model based on Stable diffusion 1.5", - "extras": "width: 512, height: 512, sampler: DEIS, steps: 20, cfg_scale: 6.0" - }, - "Dreamshaper SD v7 LCM": { - "path": "SimianLuo/LCM_Dreamshaper_v7", - "preview": "SimianLuo--LCM_Dreamshaper_v7.jpg", - "desc": "Latent Consistencey Models enable swift inference with minimal steps on any pre-trained LDMs, including Stable Diffusion. By distilling classifier-free guidance into the model's input, LCM can generate high-quality images in very short inference time. LCM can generate quality images in as few as 3-4 steps, making it blazingly fast.", - "extras": "width: 512, height: 512, sampler: LCM, steps: 4, cfg_scale: 0.0" - }, - "DreamShaper SD-XL Turbo": { - "path": "dreamshaperXL_v21TurboDPMSDE.safetensors@https://civitai.com/api/download/models/351306", - "preview": "dreamshaperXL_v21TurboDPMSDE.jpg", - "desc": "Showcase finetuned model based on Stable diffusion XL", - "extras": "sampler: DPM SDE, steps: 8, cfg_scale: 2.0" - }, - - "SDXS DreamShaper 512": { - "path": "IDKiro/sdxs-512-dreamshaper", - "preview": "IDKiro--sdxs-512-dreamshaper.jpg", - "desc": "SDXS: Real-Time One-Step Latent Diffusion Models with Image Conditions", - "extras": "width: 512, height: 512, sampler: CMSI, steps: 1, cfg_scale: 0.0" - }, - "SDXL Flash Mini": { - "path": "SDXL-Flash_Mini.safetensors@https://huggingface.co/sd-community/sdxl-flash-mini/resolve/main/SDXL-Flash_Mini.safetensors?download=true", - "preview": "SDXL-Flash_Mini.jpg", - "desc": "Introducing the new fast model SDXL Flash (Mini), we learned that all fast XL models work fast, but the quality decreases, and we also made a fast model, but it is not as fast as LCM, Turbo, Lightning and Hyper, but the quality is higher.", - "extras": "width: 2048, height: 1024, sampler: DEIS, steps: 40, cfg_scale: 6.0", - "experimental": true - }, - "RunwayML StableDiffusion 1.5": { "original": true, "path": "v1-5-pruned-fp16-emaonly.safetensors@https://huggingface.co/Aptronym/SDNext/resolve/main/Reference/v1-5-pruned-fp16-emaonly.safetensors?download=true", @@ -283,6 +250,20 @@ "extras": "sampler: Default, cfg_scale: 3.5" }, + "SDXS DreamShaper 512": { + "path": "IDKiro/sdxs-512-dreamshaper", + "preview": "IDKiro--sdxs-512-dreamshaper.jpg", + "desc": "SDXS: Real-Time One-Step Latent Diffusion Models with Image Conditions", + "extras": "width: 512, height: 512, sampler: CMSI, steps: 1, cfg_scale: 0.0" + }, + "SDXL Flash Mini": { + "path": "SDXL-Flash_Mini.safetensors@https://huggingface.co/sd-community/sdxl-flash-mini/resolve/main/SDXL-Flash_Mini.safetensors?download=true", + "preview": "SDXL-Flash_Mini.jpg", + "desc": "Introducing the new fast model SDXL Flash (Mini), we learned that all fast XL models work fast, but the quality decreases, and we also made a fast model, but it is not as fast as LCM, Turbo, Lightning and Hyper, but the quality is higher.", + "extras": "width: 2048, height: 1024, sampler: DEIS, steps: 40, cfg_scale: 6.0", + "experimental": true + }, + "NVLabs Sana 1.5 1.6B 1k": { "path": "Efficient-Large-Model/SANA1.5_1.6B_1024px_diffusers", "desc": "Sana is an efficient model with scaling of training-time and inference time techniques. SANA-1.5 delivers: efficient model growth from 1.6B Sana-1.0 model to 4.8B, achieving similar or better performance than training from scratch and saving 60% training cost; efficient model depth pruning, slimming any model size as you want; powerful VLM selection based inference scaling, smaller model+inference scaling > larger model.", diff --git a/models/Reference/tempest-by-vlad-base.jpg b/models/Reference/tempest-by-vlad-base.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0d48f0a6219b4b6694c66c1ebb616bba47f6ac5a GIT binary patch literal 34916 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 literal 0 HcmV?d00001 diff --git a/modules/processing_args.py b/modules/processing_args.py index 5959f98f4..4cee55eb4 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -122,7 +122,7 @@ def task_specific_kwargs(p, model): 'target_subject_category': getattr(p, 'prompt', '').split()[-1], 'output_type': 'pil', } - if model.__class__.__name__ == 'StableDiffusion3Pipeline': + if model.__class__.__name__ in ['StableDiffusion3Pipeline', 'WanPipeline']: p.width = 16 * (p.width // 16) p.height = 16 * (p.height // 16) task_args['width'] = p.width diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index d809362e3..8343559c4 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -458,7 +458,8 @@ def validate_pipeline(p: processing.StableDiffusionProcessing): if m.repo_cls is not None: models_cls.append(m.repo_cls.__name__) is_video_model = shared.sd_model.__class__.__name__ in models_cls - is_video_pipeline = 'video' in p.__class__.__name__.lower() + override_video_pipelines = ['WanPipeline'] + is_video_pipeline = ('video' in p.__class__.__name__.lower()) or (shared.sd_model.__class__.__name__ in override_video_pipelines) if is_video_model and not is_video_pipeline: shared.log.error(f'Mismatch: type={shared.sd_model_type} cls={shared.sd_model.__class__.__name__} request={p.__class__.__name__} video model with non-video pipeline') return False diff --git a/modules/prompt_parser_xhinker.py b/modules/prompt_parser_xhinker.py index fed488c2a..377a9f3cc 100644 --- a/modules/prompt_parser_xhinker.py +++ b/modules/prompt_parser_xhinker.py @@ -439,13 +439,13 @@ def get_weighted_text_embeddings_sdxl( , pad_last_block=pad_last_block ) - prompt_token_groups_2, prompt_weight_groups_2 = group_tokens_and_weights( + prompt_token_groups_2, _prompt_weight_groups_2 = group_tokens_and_weights( prompt_tokens_2.copy() , prompt_weights_2.copy() , pad_last_block=pad_last_block ) - neg_prompt_token_groups_2, neg_prompt_weight_groups_2 = group_tokens_and_weights( + neg_prompt_token_groups_2, _neg_prompt_weight_groups_2 = group_tokens_and_weights( neg_prompt_tokens_2.copy() , neg_prompt_weights_2.copy() , pad_last_block=pad_last_block @@ -609,7 +609,6 @@ def get_weighted_text_embeddings_sdxl_refiner( , generator = torch.Generator(text2img_pipe.device).manual_seed(2) ).images[0] """ - import math eos = 49407 # pipe.tokenizer.eos_token_id # tokenizer 2 @@ -1148,13 +1147,13 @@ def get_weighted_text_embeddings_sd3( , pad_last_block=pad_last_block ) - prompt_token_groups_2, prompt_weight_groups_2 = group_tokens_and_weights( + prompt_token_groups_2, _prompt_weight_groups_2 = group_tokens_and_weights( prompt_tokens_2.copy() , prompt_weights_2.copy() , pad_last_block=pad_last_block ) - neg_prompt_token_groups_2, neg_prompt_weight_groups_2 = group_tokens_and_weights( + neg_prompt_token_groups_2, _neg_prompt_weight_groups_2 = group_tokens_and_weights( neg_prompt_tokens_2.copy() , neg_prompt_weights_2.copy() , pad_last_block=pad_last_block @@ -1374,7 +1373,7 @@ def get_weighted_text_embeddings_flux1( pipe.tokenizer_2, prompt2 ) - prompt_token_groups, prompt_weight_groups = group_tokens_and_weights( + prompt_token_groups, _prompt_weight_groups = group_tokens_and_weights( prompt_tokens.copy() , prompt_weights.copy() , pad_last_block=True From 09cc58408947f833c42967d8c17bb76657d011a7 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 8 Aug 2025 18:06:36 -0400 Subject: [PATCH 053/141] civitai downloader batch download all versions Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 2 ++ extensions-builtin/sdnext-modernui | 2 +- javascript/civitai.js | 24 +++++++++++++++++++++--- javascript/sdnext.css | 8 ++++++++ modules/civitai/search_civitai.py | 14 +++++++++++--- modules/sd_models.py | 21 ++++++++++++--------- modules/ui_common.py | 12 ++++++++---- modules/ui_models.py | 20 ++++++++++++++------ 8 files changed, 77 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b82b0560b..85a6439c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,8 @@ And (*as always*) many bugfixes and improvements to existing features! - configurable image fit in all image views - rewritten **CivitAI downloader** in *models -> civitai* + *hint*: you can enter model id in a search bar to pull information on specific model directly + *hint*: you can download individual versions or batch-download all-at-once! - redesigned **GPU monitor** - standard-ui: *system -> gpu monitor* - modern-ui: *aside -> console -> gpu monitor* diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 303612cd9..61d458572 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 303612cd94248463b1835ab2f10630fde0c7923e +Subproject commit 61d458572fbf6531c796180374b2df9124c43273 diff --git a/javascript/civitai.js b/javascript/civitai.js index 417c08584..c60909f4a 100644 --- a/javascript/civitai.js +++ b/javascript/civitai.js @@ -31,6 +31,7 @@ const modelDetailsHTML = ` Downloads{downloads} Author{creator} Description
{desc}
+ Download
@@ -114,9 +115,26 @@ async function modelCardClick(id) { function startCivitDownload(url, name, type) { log('startCivitDownload', { url, name, type }); - selectedURL = url; - selectedName = name; - selectedType = type; + selectedURL = [url]; + selectedName = [name]; + selectedType = [type]; + const civitDownloadBtn = gradioApp().getElementById('civitai_download_btn'); + if (civitDownloadBtn) civitDownloadBtn.click(); +} + +function startCivitAllDownload(evt) { + log('startCivitAllDownload', evt); + const versions = gradioApp().getElementById('model-versions-table').querySelectorAll('tr'); + selectedURL = []; + selectedName = []; + selectedType = []; + for (const version of versions) { + const parsed = version.querySelector('td:nth-child(1) div')?.getAttribute('onclick')?.match(/startCivitDownload\('([^']+)', '([^']+)', '([^']+)'\)/); + if (!parsed || parsed.length < 4) continue; + selectedURL.push(parsed[1]); + selectedName.push(parsed[2]); + selectedType.push(parsed[3]); + } const civitDownloadBtn = gradioApp().getElementById('civitai_download_btn'); if (civitDownloadBtn) civitDownloadBtn.click(); } diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 6e855e3c5..487bec576 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -2054,6 +2054,14 @@ div:has(>#tab-gallery-folders) { font-weight: bold; } +.div-link { + cursor: pointer; +} + +.div-link:hover { + background-color: var(--button-primary-background-fill); +} + .video-model-link { color: var(--button-primary-background-fill); font-weight: normal; diff --git a/modules/civitai/search_civitai.py b/modules/civitai/search_civitai.py index 12d74fce3..dda495028 100644 --- a/modules/civitai/search_civitai.py +++ b/modules/civitai/search_civitai.py @@ -139,7 +139,11 @@ def search_civitai( headers['Authorization'] = f'Bearer {token}' url = 'https://civitai.com/api/v1/models' - uri = f'{url}?{encoded}' + if query.isnumeric(): + uri = f'{url}/{query}' + else: + uri = f'{url}?{encoded}' + log.info(f'CivitAI request: uri="{uri}" dct={dct} token={token is not None}') result = requests.get(uri, headers=headers, timeout=60) @@ -149,7 +153,11 @@ def search_civitai( all_models: list[Model] = [] exact_models: list[Model] = [] - items = result.json().get('items', []) + dct = result.json() + if 'items' not in dct: + items = [dct] # single model + else: + items = dct.get('items', []) for item in items: all_models.append(Model(item)) @@ -189,7 +197,7 @@ def create_model_cards(all_models: list[Model]) -> str: previews = [] for version in model.versions: for image in version.images: - if image.url and len(image.url) > 0: + if image.url and len(image.url) > 0 and not image.url.lower().endswith('.mp4'): previews.append(image.url) if len(previews) == 0: previews = ['./sd_extra_networks/thumb?filename=html/card-no-preview.png'] diff --git a/modules/sd_models.py b/modules/sd_models.py index 50869eb72..cbafbea80 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -73,24 +73,26 @@ def copy_diffuser_options(new_pipe, orig_pipe): def set_vae_options(sd_model, vae=None, op:str='model', quiet:bool=False): + ops = {} if hasattr(sd_model, "vae"): if vae is not None: sd_model.vae = vae - shared.log.quiet(quiet, f'Setting {op}: component=VAE name="{sd_vae.loaded_vae_file}"') + ops['name'] = f"{sd_vae.loaded_vae_file}" if shared.opts.diffusers_vae_upcast != 'default': sd_model.vae.config.force_upcast = True if shared.opts.diffusers_vae_upcast == 'true' else False - shared.log.quiet(quiet, f'Setting {op}: component=VAE upcast={sd_model.vae.config.force_upcast}') + ops['upcast'] = sd_model.vae.config.force_upcast if shared.opts.no_half_vae and op not in {'decode', 'encode'}: devices.dtype_vae = torch.float32 sd_model.vae.to(devices.dtype_vae) - shared.log.quiet(quiet, f'Setting {op}: component=VAE no-half=True') - if hasattr(sd_model, "enable_vae_slicing"): + ops['no-half'] = True + if hasattr(sd_model, "enable_vae_slicing") and hasattr(sd_model, "disable_vae_slicing"): + ops['slicing'] = shared.opts.diffusers_vae_slicing if shared.opts.diffusers_vae_slicing: - shared.log.quiet(quiet, f'Setting {op}: component=VAE slicing=True') sd_model.enable_vae_slicing() else: sd_model.disable_vae_slicing() if hasattr(sd_model, "enable_vae_tiling") and hasattr(sd_model, "disable_vae_tiling"): + ops['tiling'] = shared.opts.diffusers_vae_tiling if shared.opts.diffusers_vae_tiling: if hasattr(sd_model, 'vae') and hasattr(sd_model.vae, 'config') and hasattr(sd_model.vae.config, 'sample_size') and isinstance(sd_model.vae.config.sample_size, int): if getattr(sd_model.vae, "tile_sample_min_size_backup", None) is None: @@ -107,15 +109,16 @@ def set_vae_options(sd_model, vae=None, op:str='model', quiet:bool=False): sd_model.vae.tile_overlap_factor = float(shared.opts.diffusers_vae_tile_overlap) else: sd_model.vae.tile_overlap_factor = getattr(sd_model.vae, "tile_overlap_factor_backup", sd_model.vae.tile_overlap_factor) - shared.log.quiet(quiet, f'Setting {op}: component=VAE tiling=True tile={sd_model.vae.tile_sample_min_size} overlap={sd_model.vae.tile_overlap_factor}') - else: - shared.log.quiet(quiet, f'Setting {op}: component=VAE tiling=True') + ops['tile'] = sd_model.vae.tile_sample_min_size + ops['overlap'] = sd_model.vae.tile_overlap_factor sd_model.enable_vae_tiling() else: sd_model.disable_vae_tiling() if hasattr(sd_model, "vqvae"): - shared.log.quiet(quiet, f'Setting {op}: component=VQVAE upcast=True') + ops['upcast'] = True sd_model.vqvae.to(torch.float32) # vqvae is producing nans in fp16 + if not quiet and len(ops) > 0: + shared.log.quiet(quiet, f'Setting {op}: component=vae {ops}') def set_diffuser_options(sd_model, vae=None, op:str='model', offload:bool=True, quiet:bool=False): diff --git a/modules/ui_common.py b/modules/ui_common.py index 494c2bc8a..292adcdef 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -19,10 +19,14 @@ def gr_show(visible=True): def update_generation_info(generation_info, html_info, img_index): try: - generation_info = json.loads(generation_info) - if img_index < 0 or img_index >= len(generation_info["infotexts"]): - return html_info, generation_info - info = generation_info["infotexts"][img_index] + generation_json = json.loads(generation_info) + if len(generation_json["infotexts"]) == 0: + return html_info, 'no infotexts found' + if img_index == -1: + img_index = 0 + if img_index >= len(generation_json["infotexts"]): + return html_info, 'error fetching infotext' + info = generation_json["infotexts"][img_index] html_info_formatted = infotext_to_html(info) return html_info, html_info_formatted except Exception: diff --git a/modules/ui_models.py b/modules/ui_models.py index 5a7b58076..59753091b 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -470,6 +470,7 @@ def create_ui(): with gr.Tab(label="CivitAI", elem_id="models_civitai_tab"): from modules.civitai.search_civitai import search_civitai, create_model_cards, base_models + def civitai_search(civit_search_text, civit_search_tag, civit_nsfw, civit_type, civit_base, civit_token): results = search_civitai(query=civit_search_text, tag=civit_search_tag, nsfw=civit_nsfw, types=civit_type, base=civit_base, token=civit_token) html = create_model_cards(results) @@ -480,12 +481,13 @@ def create_ui(): opts.civitai_token = token opts.save() - def civitai_download(model_url, model_name, model_type, model_path, civit_token, model_output): + def civitai_download(model_urls, model_names, model_types, model_path, civit_token, model_output): from modules.civitai.download_civitai import download_civit_model - msg = f"

Initiating download

{model_name} | {model_type} | {model_url}

" - yield msg + model_output - download_civit_model(model_url, model_name, model_path, model_type, civit_token) - yield model_output + for model_url, model_name, model_type in zip(model_urls, model_names, model_types): + msg = f"

Initiating download

{model_name} | {model_type} | {model_url}

" + yield msg + model_output + download_civit_model(model_url, model_name, model_path, model_type, civit_token) + yield model_output with gr.Row(): gr.HTML('

Search & Download

') @@ -515,7 +517,13 @@ def create_ui(): civit_search_text.submit(fn=civitai_search, inputs=civit_inputs, outputs=[civitai_models_output]) civit_search_tag.submit(fn=civitai_search, inputs=civit_inputs, outputs=[civitai_models_output]) civit_token.change(fn=civitai_update_token, inputs=[civit_token], outputs=[]) - civit_download_btn.click(fn=civitai_download, _js="downloadCivitModel", inputs=[_dummy, _dummy, _dummy, civit_folder, civit_token, civitai_models_output], outputs=[civitai_models_output]) + civit_download_btn.click( + fn=civitai_download, + _js="downloadCivitModel", + inputs=[_dummy, _dummy, _dummy, civit_folder, civit_token, civitai_models_output], + outputs=[civitai_models_output], + show_progress=True, + ) with gr.Tab(label="Huggingface", elem_id="models_huggingface_tab"): from modules.models_hf import hf_search, hf_select, hf_download_model, hf_update_token From 0cc24a6d8133354bc8380ff147b9d14bb2f4c577 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 8 Aug 2025 18:09:46 -0400 Subject: [PATCH 054/141] lint Signed-off-by: Vladimir Mandic --- modules/api/rocm_smi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/api/rocm_smi.py b/modules/api/rocm_smi.py index 946d92255..870bfaaed 100644 --- a/modules/api/rocm_smi.py +++ b/modules/api/rocm_smi.py @@ -55,13 +55,13 @@ class ThrottleStatus(IntFlag): def active(self): members = self.__class__.__members__ - return (m for m in members if getattr(self, m)._value_ & self.value != 0) + return (m for m in members if getattr(self, m)._value_ & self.value != 0) # pylint: disable=protected-access def __iter__(self): return self.active() def __str__(self): - return u', '.join(self.active()) + return ', '.join(self.active()) def get_rocm_smi(): From 22d86acda35b50dd4b881be9b4af2d47fd8adcd0 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Sat, 9 Aug 2025 01:10:05 +0300 Subject: [PATCH 055/141] Make SDNQ MatMul listen to the dequantize fp32 setting --- modules/sdnq/layers/linear/linear_fp8.py | 3 +-- modules/sdnq/layers/linear/linear_fp8_tensorwise.py | 2 +- modules/sdnq/layers/linear/linear_int8.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/sdnq/layers/linear/linear_fp8.py b/modules/sdnq/layers/linear/linear_fp8.py index 17cac32e5..3d2f4059f 100644 --- a/modules/sdnq/layers/linear/linear_fp8.py +++ b/modules/sdnq/layers/linear/linear_fp8.py @@ -8,10 +8,9 @@ from ...common import use_torch_compile # noqa: TID252 def quantize_fp8_matmul_input(input: torch.FloatTensor) -> Tuple[torch.Tensor, torch.FloatTensor]: - input = input.flatten(0,-2).contiguous() + input = input.flatten(0,-2).contiguous().to(dtype=torch.float32) input_scale = torch.amax(input.abs(), dim=-1, keepdims=True).div_(448) input = torch.div(input, input_scale).clamp_(-448, 448).to(dtype=torch.float8_e4m3fn) - input_scale = input_scale.to(dtype=torch.float32) return input, input_scale diff --git a/modules/sdnq/layers/linear/linear_fp8_tensorwise.py b/modules/sdnq/layers/linear/linear_fp8_tensorwise.py index 768719d18..1cdf5ced4 100644 --- a/modules/sdnq/layers/linear/linear_fp8_tensorwise.py +++ b/modules/sdnq/layers/linear/linear_fp8_tensorwise.py @@ -9,7 +9,7 @@ from ...dequantizer import dequantize_symmetric, dequantize_symmetric_with_bias def quantize_fp8_matmul_input_tensorwise(input: torch.FloatTensor, scale: torch.FloatTensor) -> Tuple[torch.Tensor, torch.FloatTensor]: - input = input.flatten(0,-2).contiguous() + input = input.flatten(0,-2).contiguous().to(dtype=scale.dtype) input_scale = torch.amax(input.abs(), dim=-1, keepdims=True).div_(448) input = torch.div(input, input_scale).clamp_(-448, 448).to(dtype=torch.float8_e4m3fn) scale = torch.mul(input_scale, scale) diff --git a/modules/sdnq/layers/linear/linear_int8.py b/modules/sdnq/layers/linear/linear_int8.py index 1be08a5ae..dfc5288a0 100644 --- a/modules/sdnq/layers/linear/linear_int8.py +++ b/modules/sdnq/layers/linear/linear_int8.py @@ -10,7 +10,7 @@ from ...dequantizer import dequantize_symmetric, dequantize_symmetric_with_bias def quantize_int8_matmul_input(input: torch.FloatTensor, scale: torch.FloatTensor) -> Tuple[torch.CharTensor, torch.FloatTensor]: - input = input.flatten(0,-2).contiguous() + input = input.flatten(0,-2).contiguous().to(dtype=scale.dtype) input_scale = torch.amax(input.abs(), dim=-1, keepdims=True).div_(127) input = torch.div(input, input_scale).round_().clamp_(-128, 127).to(dtype=torch.int8) scale = torch.mul(input_scale, scale) From 69db77e365d170aeec9a0e1f745355a287ad3125 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Sat, 9 Aug 2025 02:39:01 +0300 Subject: [PATCH 056/141] SDNQ remove eps --- modules/sdnq/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index f05996e45..0eff91a67 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -22,18 +22,13 @@ class QuantizationMethod(str, Enum): def get_scale_asymmetric(weight: torch.FloatTensor, reduction_axes: Union[int, List[int]], weights_dtype: str) -> Tuple[torch.FloatTensor, torch.FloatTensor]: zero_point = torch.amin(weight, dim=reduction_axes, keepdims=True) scale = torch.amax(weight, dim=reduction_axes, keepdims=True).sub_(zero_point).div_(dtype_dict[weights_dtype]["max"] - dtype_dict[weights_dtype]["min"]) - eps = torch.finfo(scale.dtype).eps # prevent divison by 0 - scale = torch.where(torch.abs(scale) < eps, eps, scale) if dtype_dict[weights_dtype]["min"] != 0: zero_point.sub_(torch.mul(scale, dtype_dict[weights_dtype]["min"])) return scale, zero_point def get_scale_symmetric(weight: torch.FloatTensor, reduction_axes: Union[int, List[int]], weights_dtype: str) -> torch.FloatTensor: - scale = torch.amax(weight.abs(), dim=reduction_axes, keepdims=True).div_(dtype_dict[weights_dtype]["max"]) - eps = torch.finfo(scale.dtype).eps # prevent divison by 0 - scale = torch.where(torch.abs(scale) < eps, eps, scale) - return scale + return torch.amax(weight.abs(), dim=reduction_axes, keepdims=True).div_(dtype_dict[weights_dtype]["max"]) def quantize_weight(weight: torch.FloatTensor, reduction_axes: Union[int, List[int]], weights_dtype: str) -> Tuple[torch.Tensor, torch.FloatTensor, torch.FloatTensor]: From 45d5189b83bb1ece1e75a0dcb25eb0b4d7e9e053 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 9 Aug 2025 08:40:38 -0400 Subject: [PATCH 057/141] add privacy blur for tokens in ui Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 9 +++++---- javascript/sdnext.css | 12 ++++++++++++ modules/ui_models.py | 23 +++++++++++------------ 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85a6439c6..eb12dbbb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Change Log for SD.Next -## Update for 2025-08-08 +## Update for 2025-08-09 -### Highlights for 2025-08-08 +### Highlights for 2025-08-09 Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/), [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) And several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) @@ -12,7 +12,7 @@ And (*as always*) many bugfixes and improvements to existing features! [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) -### Details for 2025-08-07 +### Details for 2025-08-09 - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) @@ -66,7 +66,8 @@ And (*as always*) many bugfixes and improvements to existing features! - gallery bypass browser cache for thumbnails - gallery safer delete operation - networks display indicator for currently active items - styles, loras + applies to: *styles, loras* + - apply privacy blur to hf and civitai tokens - *hint*: card layout card layout is used by networks, gallery, civitai search, etc. you can change card size in *settings -> user interface* diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 487bec576..4de981a63 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -2067,6 +2067,18 @@ div:has(>#tab-gallery-folders) { font-weight: normal; } +.token { + filter: blur(4px); +} + +.token:hover { + filter: blur(0px); +} + +.token:focus { + filter: blur(0px); +} + @keyframes move { from { background-position-x: 0, -40px; diff --git a/modules/ui_models.py b/modules/ui_models.py index 59753091b..397a5da5e 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -498,7 +498,7 @@ def create_ui(): with gr.Accordion(label='Advanced', open=False, elem_id="civitai_search_options"): civit_download_btn = gr.Button(value="Download model", variant='primary', elem_id="civitai_download_btn", visible=False) with gr.Row(): - civit_token = gr.Textbox(opts.civitai_token, label='CivitAI token', placeholder='optional access token for private or gated models') + civit_token = gr.Textbox(opts.civitai_token, label='CivitAI token', placeholder='optional access token for private or gated models', elem_id="civitai_token") with gr.Row(): civit_nsfw = gr.Checkbox(label='NSFW allowed', value=True) with gr.Row(): @@ -534,17 +534,16 @@ def create_ui(): hf_search_text = gr.Textbox('', label='Search models', placeholder='search huggingface models') hf_search_btn = ToolButton(value=ui_symbols.search) with gr.Row(): - with gr.Column(scale=2): - with gr.Row(): - hf_selected = gr.Textbox('', label='Select model', placeholder='select model from search results or enter model name manually') - with gr.Column(scale=1): - with gr.Row(): - hf_variant = gr.Textbox('', label='Specify model variant', placeholder='') - hf_revision = gr.Textbox('', label='Specify model revision', placeholder='') - with gr.Row(): - hf_token = gr.Textbox(opts.huggingface_token, label='Huggingface token', placeholder='optional access token for private or gated models') - hf_mirror = gr.Textbox('', label='Huggingface mirror', placeholder='optional mirror site for downloads') - hf_custom_pipeline = gr.Textbox('', label='Custom pipeline', placeholder='optional pipeline for downloads') + hf_selected = gr.Textbox('', label='Select model', placeholder='select model from search results or enter model name manually') + with gr.Accordion(label='Advanced', open=False, elem_id="hf_search_options"): + with gr.Row(): + hf_token = gr.Textbox(opts.huggingface_token, label='Huggingface token', placeholder='optional access token for private or gated models', elem_id="hf_token") + with gr.Row(): + hf_variant = gr.Textbox('', label='Specify model variant', placeholder='') + hf_revision = gr.Textbox('', label='Specify model revision', placeholder='') + with gr.Row(): + hf_mirror = gr.Textbox('', label='Huggingface mirror', placeholder='optional mirror site for downloads') + hf_custom_pipeline = gr.Textbox('', label='Custom pipeline', placeholder='optional pipeline for downloads') with gr.Column(scale=1): gr.HTML('
') hf_download_model_btn = gr.Button(value="Download model", variant='primary') From 277e50f023ef95a203f8d82c050e330f6d1086b9 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 9 Aug 2025 09:02:50 -0400 Subject: [PATCH 058/141] add chroma img2img Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 2 +- extensions-builtin/sdnext-modernui | 2 +- html/reference.json | 22 ++++------------------ javascript/sdnext.css | 11 ++++------- pipelines/model_chroma.py | 8 ++++++-- 5 files changed, 16 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb12dbbb4..99ce2ffd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ And (*as always*) many bugfixes and improvements to existing features! available via *networks -> models -> reference* - [Chroma](https://huggingface.co/lodestones/Chroma) great model based on FLUX.1 and then redesigned and retrained by *lodestones* - update with latest **v50**, **v50 Annealed**, **v48**, **v48 Detail Calibrated** and **v46 Flash** variants + update with latest **HD**, **HD Flash** and **HD Annealed** variants which are based on *v50* release available via *networks -> models -> reference* - [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) SkyReels-V2 is a genarative video model based on Wan-2.1 but with heavily modified execution to allow for infinite-length video generation diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 61d458572..f852a2cc7 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 61d458572fbf6531c796180374b2df9124c43273 +Subproject commit f852a2cc796de41a7e87aa223ff2793042c7b602 diff --git a/html/reference.json b/html/reference.json index 325dbf760..e29debb53 100644 --- a/html/reference.json +++ b/html/reference.json @@ -140,36 +140,22 @@ "extras": "sampler: Default, cfg_scale: 4.5" }, - "lodestones Chroma Unlocked v50": { + "lodestones Chroma Unlocked HD": { "path": "vladmandic/chroma-unlocked-v50", "preview": "lodestones--Chroma.jpg", "desc": "Chroma is a 8.9B parameter model based on FLUX.1-schnell. It’s fully Apache 2.0 licensed, ensuring that anyone can use, modify, and build on top of it—no corporate gatekeeping. The model is still training right now, and I’d love to hear your thoughts! Your input and feedback are really appreciated.", "skip": true, "extras": "sampler: Default, cfg_scale: 3.5" }, - "lodestones Chroma Unlocked v50 Annealed": { + "lodestones Chroma Unlocked HD Annealed": { "path": "vladmandic/chroma-unlocked-v50-annealed", "preview": "lodestones--Chroma.jpg", "desc": "Chroma is a 8.9B parameter model based on FLUX.1-schnell. It’s fully Apache 2.0 licensed, ensuring that anyone can use, modify, and build on top of it—no corporate gatekeeping. The model is still training right now, and I’d love to hear your thoughts! Your input and feedback are really appreciated.", "skip": true, "extras": "sampler: Default, cfg_scale: 3.5" }, - "lodestones Chroma Unlocked v48": { - "path": "vladmandic/chroma-unlocked-v48", - "preview": "lodestones--Chroma.jpg", - "desc": "Chroma is a 8.9B parameter model based on FLUX.1-schnell. It’s fully Apache 2.0 licensed, ensuring that anyone can use, modify, and build on top of it—no corporate gatekeeping. The model is still training right now, and I’d love to hear your thoughts! Your input and feedback are really appreciated.", - "skip": true, - "extras": "sampler: Default, cfg_scale: 3.5" - }, - "lodestones Chroma Unlocked v48 Detail Calibrated": { - "path": "vladmandic/chroma-unlocked-v48-detail-calibrated", - "preview": "lodestones--Chroma.jpg", - "desc": "Chroma is a 8.9B parameter model based on FLUX.1-schnell. It’s fully Apache 2.0 licensed, ensuring that anyone can use, modify, and build on top of it—no corporate gatekeeping. The model is still training right now, and I’d love to hear your thoughts! Your input and feedback are really appreciated.", - "skip": true, - "extras": "sampler: Default, cfg_scale: 3.5" - }, - "lodestones Chroma Unlocked v46 Flash": { - "path": "vladmandic/chroma-unlocked-v46-flash", + "lodestones Chroma Unlocked HD Flash": { + "path": "lodestones/Chroma1-Flash", "preview": "lodestones--Chroma.jpg", "desc": "Chroma is a 8.9B parameter model based on FLUX.1-schnell. It’s fully Apache 2.0 licensed, ensuring that anyone can use, modify, and build on top of it—no corporate gatekeeping. The model is still training right now, and I’d love to hear your thoughts! Your input and feedback are really appreciated.", "skip": true, diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 4de981a63..19759ad0a 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -2067,16 +2067,13 @@ div:has(>#tab-gallery-folders) { font-weight: normal; } -.token { +#civitai_token textarea, #hf_token textarea { filter: blur(4px); } -.token:hover { - filter: blur(0px); -} - -.token:focus { - filter: blur(0px); +#civitai_token textarea:hover, #hf_token textarea:hover, +#civitai_token textarea:focus, #hf_token textarea:focus { + filter: blur(0); } @keyframes move { diff --git a/pipelines/model_chroma.py b/pipelines/model_chroma.py index e976bfa56..a1ca25ffc 100644 --- a/pipelines/model_chroma.py +++ b/pipelines/model_chroma.py @@ -239,8 +239,6 @@ def load_chroma(checkpoint_info, diffusers_load_config): # triggered by opts.sd_ if vae is not None: kwargs['vae'] = vae - # TODO model load: add ChromaFillPipeline, ChromaControlPipeline, ChromaImg2ImgPipeline etc when available - # Chroma will support inpainting *after* its training has finished: https://huggingface.co/lodestones/Chroma/discussions/28#6826dd2ed86f53ff983add5c cls = diffusers.ChromaPipeline shared.log.debug(f'Load model: type=Chroma cls={cls.__name__} preloaded={list(kwargs)} revision={diffusers_load_config.get("revision", None)}') for c in kwargs: @@ -266,6 +264,12 @@ def load_chroma(checkpoint_info, diffusers_load_config): # triggered by opts.sd_ from nunchaku.caching.diffusers_adapters import apply_cache_on_pipe apply_cache_on_pipe(pipe, residual_diff_threshold=0.12) + # register autopipline + diffusers.pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["chroma"] = diffusers.ChromaPipeline + diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["chroma"] = diffusers.ChromaImg2ImgPipeline + # TODO model load: add ChromaControlPipeline, ChromaInpaintPipeline + # Chroma will support inpainting *after* its training has finished: https://huggingface.co/lodestones/Chroma/discussions/28#6826dd2ed86f53ff983add5c + # release memory transformer = None text_encoder = None From 3aca5f4d199001a662fdf5672b06537f6b55f5cf Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 9 Aug 2025 09:29:21 -0400 Subject: [PATCH 059/141] blur token in settings Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 8 ++++---- extensions-builtin/sdnext-modernui | 2 +- javascript/sdnext.css | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ce2ffd1..a7f366c29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,10 @@ ### Highlights for 2025-08-09 -Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/), [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) -And several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) -Plus continuing with major **UI** work, there is new embedded **Docs/Wiki** search, redesigned real-time **hints**, built-in **GPU monitor**, **CivitAI** integration and more! -On the compute side, new profiles for high-vram GPUs and offloading improvements +Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) and [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) +Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) and [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) +Plus continuing with major **UI** work, we have new embedded **Docs/Wiki** search, redesigned real-time **hints**, built-in **GPU monitor**, **CivitAI** integration and more! +On the compute side, new profiles for high-vram GPUs, offloading improvements and support for new `torch` release And (*as always*) many bugfixes and improvements to existing features! [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index f852a2cc7..3a2d19880 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit f852a2cc796de41a7e87aa223ff2793042c7b602 +Subproject commit 3a2d1988047e0880991468313292c057742b139e diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 19759ad0a..4c335e30c 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -2067,12 +2067,12 @@ div:has(>#tab-gallery-folders) { font-weight: normal; } -#civitai_token textarea, #hf_token textarea { +#civitai_token textarea, #hf_token textarea, #setting_huggingface_token textarea { filter: blur(4px); } -#civitai_token textarea:hover, #hf_token textarea:hover, -#civitai_token textarea:focus, #hf_token textarea:focus { +#civitai_token textarea:hover, #hf_token textarea:hover, #setting_huggingface_token textarea:hover, +#civitai_token textarea:focus, #hf_token textarea:focus, #setting_huggingface_token textarea:focus { filter: blur(0); } From db6eb072307206022510c0eed0548b277865d4a2 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 9 Aug 2025 10:03:11 -0400 Subject: [PATCH 060/141] update requirements and handle NEP50 Signed-off-by: Vladimir Mandic --- modules/loader.py | 9 ++++++++- requirements.txt | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/modules/loader.py b/modules/loader.py index dee03cad8..9069a8183 100644 --- a/modules/loader.py +++ b/modules/loader.py @@ -16,6 +16,7 @@ logging.getLogger("DeepSpeed").disabled = True np = None try: + os.environ.setdefault('NEP50_DISABLE_WARNING', '1') import numpy as np # pylint: disable=W0611,C0411 import numpy.random # pylint: disable=W0611,C0411 # this causes failure if numpy version changed def obj2sctype(obj): @@ -24,16 +25,22 @@ try: np.obj2sctype = obj2sctype # noqa: NPY201 np.bool8 = np.bool np.float_ = np.float64 # noqa: NPY201 + def dummy_npwarn_decorator_factory(): + def npwarn_decorator(x): + return x + return npwarn_decorator + np._no_nep50_warning = getattr(np, '_no_nep50_warning', dummy_npwarn_decorator_factory) except Exception as e: errors.log.error(f'Loader: numpy=={np.__version__ if np is not None else None} {e}') errors.log.error('Please restart the app to fix this issue') sys.exit(1) timer.startup.record("numpy") +scipy = None try: import scipy # pylint: disable=W0611,C0411 except Exception as e: - errors.log.error(f'Loader: scipy=={np.__version__ if np is not None else None} {e}') + errors.log.error(f'Loader: scipy=={scipy.__version__ if scipy is not None else None} {e}') errors.log.error('Please restart the app to fix this issue') sys.exit(1) timer.startup.record("scipy") diff --git a/requirements.txt b/requirements.txt index b27e6407d..b20e8b689 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ pi-heif # versioned rich==14.1.0 -safetensors==0.6.1 +safetensors==0.6.2 tensordict==0.8.3 peft==0.17.0 httpx==0.24.1 @@ -41,10 +41,10 @@ torchsde==0.2.6 antlr4-python3-runtime==4.9.3 requests==2.32.4 tqdm==4.67.1 -accelerate==1.9.0 +accelerate==1.10.0 opencv-contrib-python-headless==4.11.0.86 einops==0.8.1 -huggingface_hub==0.34.3 +huggingface_hub==0.34.4 numexpr==2.11.0 numpy==2.1.2 pandas==2.3.0 From 2efadd26305b9e549ce4230148e2533b8ea31a17 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 9 Aug 2025 10:14:35 -0400 Subject: [PATCH 061/141] add numpy error handler to loader Signed-off-by: Vladimir Mandic --- modules/loader.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/modules/loader.py b/modules/loader.py index 9069a8183..87f88872a 100644 --- a/modules/loader.py +++ b/modules/loader.py @@ -29,7 +29,7 @@ try: def npwarn_decorator(x): return x return npwarn_decorator - np._no_nep50_warning = getattr(np, '_no_nep50_warning', dummy_npwarn_decorator_factory) + np._no_nep50_warning = getattr(np, '_no_nep50_warning', dummy_npwarn_decorator_factory) # pylint: disable=protected-access except Exception as e: errors.log.error(f'Loader: numpy=={np.__version__ if np is not None else None} {e}') errors.log.error('Please restart the app to fix this issue') @@ -57,8 +57,15 @@ except Exception: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) warnings.filterwarnings(action="ignore", category=UserWarning, module="torchvision") -import torchvision # pylint: disable=W0611,C0411 -import pytorch_lightning # pytorch_lightning should be imported after torch, but it re-enables warnings on import so import once to disable them # pylint: disable=W0611,C0411 +torchvision = None +try: + import torchvision # pylint: disable=W0611,C0411 + import pytorch_lightning # pytorch_lightning should be imported after torch, but it re-enables warnings on import so import once to disable them # pylint: disable=W0611,C0411 +except Exception as e: + errors.log.error(f'Loader: torchvision=={torchvision.__version__ if "torchvision" in sys.modules else None} {e}') + if '_no_nep' in str(e): + errors.log.error('Loaded versions of packaged are not compatible') + errors.log.error('Please restart the app to fix this issue') logging.getLogger("xformers").addFilter(lambda record: 'A matching Triton is not available' not in record.getMessage()) logging.getLogger("pytorch_lightning").disabled = True warnings.filterwarnings(action="ignore", category=DeprecationWarning) From 338129b7c1c73d53307448af3fe0f9ff7d8c086b Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 9 Aug 2025 11:31:13 -0400 Subject: [PATCH 062/141] calculate vae-scale-factor and use everywhere Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + modules/control/run.py | 7 ++++--- modules/control/tile.py | 7 ++++--- modules/processing.py | 5 +++-- modules/processing_args.py | 36 ++++++++++++++++------------------- modules/processing_class.py | 10 ++++++---- modules/processing_helpers.py | 5 +++-- modules/processing_vae.py | 3 ++- modules/sd_vae.py | 29 ++++++++++++++++++++++++++++ 9 files changed, 68 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f366c29..ea39339bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ And (*as always*) many bugfixes and improvements to existing features! - prompt parser allow explict `BOS` and `EOS` tokens in prompt - **Nunchaku** support for *FLUX.1-Fill* and *FLUX.1-Depth* models - update requirements/packages + - use model vae scale-factor for image width/heigt calculations - **Other** - **prompt enhance** add `allura-org/Gemma-3-Glitter-4B` model support - remove **LDSR** diff --git a/modules/control/run.py b/modules/control/run.py index 57cd402f0..bc25d27bb 100644 --- a/modules/control/run.py +++ b/modules/control/run.py @@ -13,7 +13,7 @@ from modules.control.units import lite # Kohya ControlLLLite from modules.control.units import t2iadapter # TencentARC T2I-Adapter from modules.control.units import reference # ControlNet-Reference from modules.control.processor import preprocess_image -from modules import devices, shared, errors, processing, images, sd_models, scripts_manager, masking +from modules import devices, shared, errors, processing, images, sd_models, sd_vae, scripts_manager, masking from modules.processing_class import StableDiffusionProcessingControl from modules.ui_common import infotext_to_html from modules.api import script @@ -384,10 +384,11 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg p.selected_scale_tab_mask = 1 # hires/refine defined outside of main init + vae_scale_factor = sd_vae.get_vae_scale_factor() 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_before * p.hr_scale / 8), 8 * int(p.height_before * p.hr_scale / 8) + p.hr_upscale_to_x, p.hr_upscale_to_y = vae_scale_factor * int(p.width_before * p.hr_scale / vae_scale_factor), vae_scale_factor * int(p.height_before * p.hr_scale / vae_scale_factor) 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(p.hr_resize_y / 8) + p.hr_upscale_to_x, p.hr_upscale_to_y = 8 * int(p.hr_resize_x / vae_scale_factor), vae_scale_factor * int(p.hr_resize_y / vae_scale_factor) global p_extra_args # pylint: disable=global-statement for k, v in p_extra_args.items(): diff --git a/modules/control/tile.py b/modules/control/tile.py index de9df1131..1d6478edc 100644 --- a/modules/control/tile.py +++ b/modules/control/tile.py @@ -1,6 +1,6 @@ import time from PIL import Image -from modules import shared, processing, images, sd_models +from modules import shared, processing, images, sd_models, sd_vae def get_tile(image: Image.Image, x: int, y: int, sx: int, sy: int) -> Image.Image: @@ -23,17 +23,18 @@ def run_tiling(p: processing.StableDiffusionProcessing, input_image: Image.Image sx, sy = p.control_tile.split('x') sx = int(sx) sy = int(sy) + vae_scale_factor = sd_vae.get_vae_scale_factor() if sx <= 0 or sy <= 0: raise ValueError('Control Tile: invalid tile size') control_image = p.task_args.get('control_image', None) or p.task_args.get('image', None) control_upscaled = None if isinstance(control_image, list) and len(control_image) > 0: - w, h = 8 * int(sx * control_image[0].width) // 8, 8 * int(sy * control_image[0].height) // 8 + w, h = vae_scale_factor * int(sx * control_image[0].width) // vae_scale_factor, vae_scale_factor * int(sy * control_image[0].height) // vae_scale_factor control_upscaled = images.resize_image(resize_mode=1 if sx==sy else 5, im=control_image[0], width=w, height=h, context='add with forward') init_image = p.override or input_image init_upscaled = None if init_image is not None: - w, h = 8 * int(sx * init_image.width) // 8, 8 * int(sy * init_image.height) // 8 + w, h = vae_scale_factor * int(sx * init_image.width) // vae_scale_factor, vae_scale_factor * int(sy * init_image.height) // vae_scale_factor init_upscaled = images.resize_image(resize_mode=1 if sx==sy else 5, im=init_image, width=w, height=h, context='add with forward') t1 = time.time() shared.log.debug(f'Control Tile: scale={sx}x{sy} resize={"fixed" if sx==sy else "context"} control={control_upscaled} init={init_upscaled} time={t1-t0:.3f}') diff --git a/modules/processing.py b/modules/processing.py index 268661647..675ac9160 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -158,11 +158,12 @@ def process_images(p: StableDiffusionProcessing) -> Processed: shared.prompt_styles.apply_styles_to_extra(p) shared.prompt_styles.extract_comments(p) + vae_scale_factor = sd_vae.get_vae_scale_factor() if p.width is not None: - p.width = 8 * int(p.width / 8) + p.width = vae_scale_factor * int(p.width / vae_scale_factor) if p.height is not None: - p.height = 8 * int(p.height / 8) + p.height = vae_scale_factor * int(p.height / vae_scale_factor) script_callbacks.before_process_callback(p) timer.process.record('pre') diff --git a/modules/processing_args.py b/modules/processing_args.py index 4cee55eb4..c34cdc2b9 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -7,7 +7,7 @@ import inspect import torch import numpy as np from PIL import Image -from modules import shared, errors, sd_models, processing, processing_vae, processing_helpers, sd_hijack_hypertile, prompt_parser_diffusers, timer, extra_networks +from modules import shared, errors, sd_models, processing, processing_vae, processing_helpers, sd_hijack_hypertile, prompt_parser_diffusers, timer, extra_networks, sd_vae from modules.processing_callbacks import diffusers_callback_legacy, diffusers_callback, set_callbacks_p from modules.processing_helpers import resize_hires, fix_prompts, calculate_base_steps, calculate_hires_steps, calculate_refiner_steps, get_generator, set_latents, apply_circular # pylint: disable=unused-import from modules.api import helpers @@ -19,6 +19,7 @@ disable_pbar = os.environ.get('SD_DISABLE_PBAR', None) is not None def task_specific_kwargs(p, model): + vae_scale_factor = sd_vae.get_vae_scale_factor(model) task_args = {} is_img2img_model = bool('Zero123' in shared.sd_model.__class__.__name__) if len(getattr(p, 'init_images', [])) > 0: @@ -30,8 +31,8 @@ def task_specific_kwargs(p, model): p.ops.append('txt2img') if hasattr(p, 'width') and hasattr(p, 'height'): task_args = { - 'width': 8 * math.ceil(p.width / 8), - 'height': 8 * math.ceil(p.height / 8), + 'width': vae_scale_factor * math.ceil(p.width / vae_scale_factor), + 'height': vae_scale_factor * math.ceil(p.height / vae_scale_factor), } elif (sd_models.get_diffusers_task(model) == sd_models.DiffusersTaskType.IMAGE_2_IMAGE or is_img2img_model) and len(getattr(p, 'init_images', [])) > 0: if shared.sd_model_type == 'sdxl' and hasattr(model, 'register_to_config'): @@ -50,19 +51,18 @@ def task_specific_kwargs(p, model): } if model.__class__.__name__ == 'FluxImg2ImgPipeline' or model.__class__.__name__ == 'FluxKontextPipeline': # needs explicit width/height if torch.is_tensor(p.init_images[0]): - p.width, p.height = p.init_images[0].shape[-1] * 16, p.init_images[0].shape[-2] * 16 + p.width, p.height = p.init_images[0].shape[-1] * vae_scale_factor, p.init_images[0].shape[-2] * vae_scale_factor else: - p.width, p.height = 8 * math.ceil(p.init_images[0].width / 8), 8 * math.ceil(p.init_images[0].height / 8) + p.width, p.height = 8 * math.ceil(p.init_images[0].width / vae_scale_factor), 8 * math.ceil(p.init_images[0].height / vae_scale_factor) if model.__class__.__name__ == 'FluxKontextPipeline': aspect_ratio = p.width / p.height - vae_scale_factor = 16 max_area = max(p.width, p.height)**2 p.width, p.height = round((max_area * aspect_ratio) ** 0.5), round((max_area / aspect_ratio) ** 0.5) p.width, p.height = p.width // vae_scale_factor * vae_scale_factor, p.height // vae_scale_factor * vae_scale_factor task_args['max_area'] = max_area task_args['width'], task_args['height'] = p.width, p.height elif model.__class__.__name__ == 'OmniGenPipeline' or model.__class__.__name__ == 'OmniGen2Pipeline': - p.width, p.height = 16 * math.ceil(p.init_images[0].width / 16), 16 * math.ceil(p.init_images[0].height / 16) + p.width, p.height = vae_scale_factor * math.ceil(p.init_images[0].width / vae_scale_factor), vae_scale_factor * math.ceil(p.init_images[0].height / vae_scale_factor) task_args = { 'width': p.width, 'height': p.height, @@ -71,8 +71,8 @@ def task_specific_kwargs(p, model): elif sd_models.get_diffusers_task(model) == sd_models.DiffusersTaskType.INSTRUCT and len(getattr(p, 'init_images', [])) > 0: p.ops.append('instruct') task_args = { - 'width': 8 * math.ceil(p.width / 8) if hasattr(p, 'width') else None, - 'height': 8 * math.ceil(p.height / 8) if hasattr(p, 'height') else None, + 'width': vae_scale_factor * math.ceil(p.width / vae_scale_factor) if hasattr(p, 'width') else None, + 'height': vae_scale_factor * math.ceil(p.height / vae_scale_factor) if hasattr(p, 'height') else None, 'image': p.init_images, 'strength': p.denoising_strength, } @@ -122,11 +122,6 @@ def task_specific_kwargs(p, model): 'target_subject_category': getattr(p, 'prompt', '').split()[-1], 'output_type': 'pil', } - if model.__class__.__name__ in ['StableDiffusion3Pipeline', 'WanPipeline']: - p.width = 16 * (p.width // 16) - p.height = 16 * (p.height // 16) - task_args['width'] = p.width - task_args['height'] = p.height if debug_enabled: debug_log(f'Process task specific args: {task_args}') return task_args @@ -383,18 +378,19 @@ def set_pipeline_args(p, model, prompts:list, negative_prompts:list, prompts_2:t # handle missing resolution if args.get('image', None) is not None and ('width' not in args or 'height' not in args): if 'width' in possible and 'height' in possible: + vae_scale_factor = sd_vae.get_vae_scale_factor(model) if isinstance(args['image'], torch.Tensor) or isinstance(args['image'], np.ndarray): - args['width'] = 8 * args['image'].shape[-1] - args['height'] = 8 * args['image'].shape[-2] + args['width'] = vae_scale_factor * args['image'].shape[-1] + args['height'] = vae_scale_factor * args['image'].shape[-2] elif isinstance(args['image'], Image.Image): args['width'] = args['image'].width args['height'] = args['image'].height elif isinstance(args['image'][0], torch.Tensor) or isinstance(args['image'][0], np.ndarray): - args['width'] = 8 * args['image'][0].shape[-1] - args['height'] = 8 * args['image'][0].shape[-2] + args['width'] = vae_scale_factor * args['image'][0].shape[-1] + args['height'] = vae_scale_factor * args['image'][0].shape[-2] else: - args['width'] = 8 * math.ceil(args['image'][0].width / 8) - args['height'] = 8 * math.ceil(args['image'][0].height / 8) + args['width'] = vae_scale_factor * math.ceil(args['image'][0].width / vae_scale_factor) + args['height'] = vae_scale_factor * math.ceil(args['image'][0].height / vae_scale_factor) if 'max_area' in possible and 'width' in args and 'height' in args and 'max_area' not in args: args['max_area'] = args['width'] * args['height'] diff --git a/modules/processing_class.py b/modules/processing_class.py index 4a9d93b90..daff8ccea 100644 --- a/modules/processing_class.py +++ b/modules/processing_class.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List from dataclasses import dataclass, field import numpy as np from PIL import Image, ImageOps -from modules import shared, images, scripts_manager, masking, sd_models, processing_helpers +from modules import shared, images, scripts_manager, masking, sd_models, sd_vae, processing_helpers debug = shared.log.trace if os.environ.get('SD_PROCESS_DEBUG', None) is not None else lambda *args, **kwargs: None @@ -449,10 +449,11 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): def init(self, all_prompts=None, all_seeds=None, all_subseeds=None): if self.init_images is not None and len(self.init_images) > 0: + vae_scale_factor = sd_vae.get_vae_scale_factor() if self.width is None or self.width == 0: - self.width = int(8 * (self.init_images[0].width * self.scale_by // 8)) + self.width = int(vae_scale_factor * (self.init_images[0].width * self.scale_by // vae_scale_factor)) if self.height is None or self.height == 0: - self.height = int(8 * (self.init_images[0].height * self.scale_by // 8)) + self.height = int(vae_scale_factor * (self.init_images[0].height * self.scale_by // vae_scale_factor)) if getattr(self, 'image_mask', None) is not None: shared.sd_model = sd_models.set_diffuser_pipe(self.sd_model, sd_models.DiffusersTaskType.INPAINTING) elif getattr(self, 'init_images', None) is not None: @@ -554,7 +555,8 @@ class StableDiffusionProcessingControl(StableDiffusionProcessingImg2Img): self.hr_force = force self.hr_upscaler = upscaler if use_scale: - self.hr_upscale_to_x, self.hr_upscale_to_y = 8 * int(self.width * scale / 8), 8 * int(self.height * scale / 8) + vae_scale_factor = sd_vae.get_vae_scale_factor() + self.hr_upscale_to_x, self.hr_upscale_to_y = vae_scale_factor * int(self.width * scale / vae_scale_factor), vae_scale_factor * int(self.height * scale / vae_scale_factor) else: self.hr_upscale_to_x, self.hr_upscale_to_y = self.hr_resize_x, self.hr_resize_y diff --git a/modules/processing_helpers.py b/modules/processing_helpers.py index 4777463ba..8729d2465 100644 --- a/modules/processing_helpers.py +++ b/modules/processing_helpers.py @@ -8,7 +8,7 @@ import numpy as np import cv2 from PIL import Image from blendmodes.blend import blendLayers, BlendType -from modules import shared, devices, images, sd_models, sd_samplers, sd_hijack_hypertile, processing_vae, timer +from modules import shared, devices, images, sd_models, sd_samplers, sd_vae, sd_hijack_hypertile, processing_vae, timer debug = shared.log.trace if os.environ.get('SD_PROCESS_DEBUG', None) is not None else lambda *args, **kwargs: None @@ -282,7 +282,8 @@ def resize_init_images(p): if getattr(p, 'image', None) is not None and getattr(p, 'init_images', None) is None: p.init_images = [p.image] if getattr(p, 'init_images', None) is not None and len(p.init_images) > 0: - tgt_width, tgt_height = 8 * math.ceil(p.init_images[0].width / 8), 8 * math.ceil(p.init_images[0].height / 8) + vae_scale_factor = sd_vae.get_vae_scale_factor() + tgt_width, tgt_height = vae_scale_factor * math.ceil(p.init_images[0].width / vae_scale_factor), vae_scale_factor * math.ceil(p.init_images[0].height / vae_scale_factor) if p.init_images[0].size != (tgt_width, tgt_height): shared.log.debug(f'Resizing init images: original={p.init_images[0].width}x{p.init_images[0].height} target={tgt_width}x{tgt_height}') p.init_images = [images.resize_image(1, image, tgt_width, tgt_height, upscaler_name=None) for image in p.init_images] diff --git a/modules/processing_vae.py b/modules/processing_vae.py index 6637e0e5b..62273045b 100644 --- a/modules/processing_vae.py +++ b/modules/processing_vae.py @@ -168,7 +168,8 @@ def full_vae_decode(latents, model): if debug: log_debug(f'VAE memory: {shared.mem_mon.read()}') vae_name = os.path.splitext(os.path.basename(sd_vae.loaded_vae_file))[0] if sd_vae.loaded_vae_file is not None else "default" - shared.log.debug(f'Decode: vae="{vae_name}" upcast={upcast} slicing={getattr(model.vae, "use_slicing", None)} tiling={getattr(model.vae, "use_tiling", None)} latents={list(latents.shape)}:{latents.device}:{latents.dtype} time={t1-t0:.3f}') + vae_scale_factor = sd_vae.get_vae_scale_factor(model) + shared.log.debug(f'Decode: vae="{vae_name}" scale={vae_scale_factor} upcast={upcast} slicing={getattr(model.vae, "use_slicing", None)} tiling={getattr(model.vae, "use_tiling", None)} latents={list(latents.shape)}:{latents.device}:{latents.dtype} time={t1-t0:.3f}') return decoded diff --git a/modules/sd_vae.py b/modules/sd_vae.py index b6c3982e9..2578ff196 100644 --- a/modules/sd_vae.py +++ b/modules/sd_vae.py @@ -12,6 +12,35 @@ checkpoint_info = None vae_path = os.path.abspath(os.path.join(paths.models_path, 'VAE')) debug = os.environ.get('SD_LOAD_DEBUG', None) is not None unspecified = object() +vae_scale_override = { + 'WanPipeline': 16, +} + + +def get_vae_scale_factor(model=None): + patch_size = 1 + if model is None: + model = shared.sd_model + if model is None: + vae_scale_factor = 8 + elif model.__class__.__name__ in vae_scale_override: + vae_scale_factor = vae_scale_override[model.__class__.__name__] + elif hasattr(model, 'vae_scale_factor_spatial'): + vae_scale_factor = model.vae_scale_factor_spatial + elif hasattr(model, 'vae_scale_factor'): + vae_scale_factor = model.vae_scale_factor + elif hasattr(model, 'pipe') and hasattr(model.pipe, 'vae_scale_factor'): + vae_scale_factor = model.pipe.vae_scale_factor + elif hasattr(model, 'config') and hasattr(model.config, 'vae_scale_factor'): + vae_scale_factor = model.config.vae_scale_factor + else: + shared.log.warning(f'VAE: cls={model.__class__.__name__ if model else "None"} scale=unknown') + vae_scale_factor = 8 + if hasattr(model, 'patch_size'): + patch_size = model.patch_size + if debug: + shared.log.trace(f'VAE: cls={model.__class__.__name__ if model else "None"} scale={vae_scale_factor} patch={patch_size}') + return vae_scale_factor * patch_size def load_vae_dict(filename): From 44a20a47c1e67444c3fe705cfa5eb656a5727aab Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 9 Aug 2025 12:15:36 -0400 Subject: [PATCH 063/141] update todo Signed-off-by: Vladimir Mandic --- TODO.md | 7 ++++--- cli/test-all-models.py | 33 ++++++++++++++++++++++++++++++ extensions-builtin/sdnext-modernui | 2 +- modules/ui_postprocessing.py | 6 +++--- 4 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 cli/test-all-models.py diff --git a/TODO.md b/TODO.md index 4a8f601f2..da9883bd8 100644 --- a/TODO.md +++ b/TODO.md @@ -79,11 +79,12 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma - lora: add other quantization types - lora: add t5 key support for sd35/f1 - lora: maybe force imediate quantization -- lora: support pre-quantized flux -- model load: cogview4: balanced offload does not work for GlmModel -- model load: add ChromaFillPipeline, ChromaControlPipeline, ChromaImg2ImgPipeline etc when available +- model load: add ChromaControlPipeline, ChromaInpaintPipeline +- model load: cogview4 balanced offload does not work for GlmModel - model load: force-reloading entire model as loading transformers only leads to massive memory usage +- model load: group offload - model load: implement model in-memory caching - modernui: monkey-patch for missing tabs.select event +- modules/lora/lora_extract.py:188:9: W0511: TODO: lora: support pre-quantized flux - processing: remove duplicate mask params - resize image: enable full VAE mode for resize-latent diff --git a/cli/test-all-models.py b/cli/test-all-models.py new file mode 100644 index 000000000..6bc768317 --- /dev/null +++ b/cli/test-all-models.py @@ -0,0 +1,33 @@ +models = [ + "sd_xl_base_1.0", + "tempestByVlad_baseV01", + "huggingface/stabilityai/stable-cascade", + "stabilityai/stable-diffusion-3.5-medium", + "stabilityai/stable-diffusion-3.5-large", + "black-forest-labs/FLUX.1-dev", + "black-forest-labs/FLUX.1-Kontext-dev", + "black-forest-labs/FLUX.1-Krea-dev", + "vladmandic/chroma-unlocked-v50", + "vladmandic/chroma-unlocked-v50-annealed", + "Qwen/Qwen-Image", + "ostris/Flex.2-preview", + "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", + "Wan-AI/Wan2.1-T2V-14B-Diffusers", + "Freepik/F-Lite", + "Freepik/F-Lite-Texture", + "Efficient-Large-Model/SANA1.5_1.6B_1024px_diffusers", + "Efficient-Large-Model/SANA1.5_4.8B_1024px_diffusers", + "nvidia/Cosmos-Predict2-2B-Text2Image", + "nvidia/Cosmos-Predict2-14B-Text2Image", + "OmniGen2/OmniGen2", + "fal/AuraFlow-v0.3", + "PixArt-alpha/PixArt-XL-2-1024-MS", + "PixArt-alpha/PixArt-Sigma-XL-2-1024-MS", + "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers", + "Alpha-VLLM/Lumina-Next-SFT-diffusers", + "Alpha-VLLM/Lumina-Image-2.0", + "HiDream-ai/HiDream-I1-Dev", + "HiDream-ai/HiDream-I1-Full", + "Kwai-Kolors/Kolors-diffusers", + "briaai/BRIA-3.2", +] diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 3a2d19880..5ec177ccb 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 3a2d1988047e0880991468313292c057742b139e +Subproject commit 5ec177ccbf818cc759bd5306f450b7bcdaaf2744 diff --git a/modules/ui_postprocessing.py b/modules/ui_postprocessing.py index 24f787259..b575fa67a 100644 --- a/modules/ui_postprocessing.py +++ b/modules/ui_postprocessing.py @@ -38,11 +38,11 @@ def create_ui(): with gr.Row(elem_id=f"{id_part}_generate_box", elem_classes="generate-box"): submit = gr.Button('Generate', elem_id=f"{id_part}_generate", variant='primary') interrupt = gr.Button('Stop', elem_id=f"{id_part}_interrupt", variant='secondary') - interrupt.click(fn=lambda: shared.state.interrupt(), inputs=[], outputs=[]) + interrupt.click(fn=shared.state.interrupt, inputs=[], outputs=[]) skip = gr.Button('Skip', elem_id=f"{id_part}_skip", variant='secondary') - skip.click(fn=lambda: shared.state.skip(), inputs=[], outputs=[]) + skip.click(fn=shared.state.skip, inputs=[], outputs=[]) pause = gr.Button('Pause', elem_id=f"{id_part}_pause") - pause.click(fn=lambda: shared.state.pause(), _js='checkPaused', inputs=[], outputs=[]) + pause.click(fn=shared.state.pause, _js='checkPaused', inputs=[], outputs=[]) result_images, generation_info, html_info, html_info_formatted, html_log = ui_common.create_output_panel("extras") gr.HTML('File metadata') exif_info = gr.HTML(elem_id="pnginfo_html_info") From 73049f7bb805bc10b62ff06238307bf06be2aeb9 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 9 Aug 2025 14:06:54 -0400 Subject: [PATCH 064/141] add load-checkpoint api endpoint and test all models script Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 3 ++ cli/test-all-models.py | 110 +++++++++++++++++++++++++++++++++++---- modules/api/api.py | 1 + modules/api/endpoints.py | 8 +++ modules/api/nudenet.py | 8 +-- modules/sd_checkpoint.py | 1 + 6 files changed, 117 insertions(+), 14 deletions(-) mode change 100644 => 100755 cli/test-all-models.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ea39339bf..cc4f24636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,8 @@ And (*as always*) many bugfixes and improvements to existing features! - **prompt enhance** add `allura-org/Gemma-3-Glitter-4B` model support - remove **LDSR** - remove `api-only` cli option +- **API** + - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model - **Fixes** - refactor legacy processing loop - fix settings components mismatch @@ -111,6 +113,7 @@ And (*as always*) many bugfixes and improvements to existing features! - fix *Flux.1-Kontext-Dev* with variable resolution - use `utf_16_be` as primary metadata decoding - fix `sd35` width/height alignment + - fix `nudenet` api - reapply offloading on ipadapter load - api set default script-name - avoid forced gc and rely on thresholds diff --git a/cli/test-all-models.py b/cli/test-all-models.py old mode 100644 new mode 100755 index 6bc768317..17d945ef1 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -1,7 +1,24 @@ +#!/usr/bin/env python +import io +import os +import time +import base64 +import logging +import requests +import urllib3 +import pathvalidate +from PIL import Image + + +logging.basicConfig(level = logging.INFO, format = '%(asctime)s %(levelname)s: %(message)s') +log = logging.getLogger(__name__) +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +output_folder = 'outputs/compare' models = [ - "sd_xl_base_1.0", - "tempestByVlad_baseV01", - "huggingface/stabilityai/stable-cascade", + "sdxl-base-v10-vaefix", + "tempest-by-vlad-0.1", "stabilityai/stable-diffusion-3.5-medium", "stabilityai/stable-diffusion-3.5-large", "black-forest-labs/FLUX.1-dev", @@ -10,24 +27,97 @@ models = [ "vladmandic/chroma-unlocked-v50", "vladmandic/chroma-unlocked-v50-annealed", "Qwen/Qwen-Image", - "ostris/Flex.2-preview", + "HiDream-ai/HiDream-I1-Dev", + "HiDream-ai/HiDream-I1-Full", + "briaai/BRIA-3.2", + "nvidia/Cosmos-Predict2-2B-Text2Image", + "nvidia/Cosmos-Predict2-14B-Text2Image", "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", "Wan-AI/Wan2.1-T2V-14B-Diffusers", + "OmniGen2/OmniGen2", + "stabilityai/stable-cascade", + "ostris/Flex.2-preview", "Freepik/F-Lite", "Freepik/F-Lite-Texture", "Efficient-Large-Model/SANA1.5_1.6B_1024px_diffusers", "Efficient-Large-Model/SANA1.5_4.8B_1024px_diffusers", - "nvidia/Cosmos-Predict2-2B-Text2Image", - "nvidia/Cosmos-Predict2-14B-Text2Image", - "OmniGen2/OmniGen2", "fal/AuraFlow-v0.3", "PixArt-alpha/PixArt-XL-2-1024-MS", "PixArt-alpha/PixArt-Sigma-XL-2-1024-MS", "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers", "Alpha-VLLM/Lumina-Next-SFT-diffusers", "Alpha-VLLM/Lumina-Image-2.0", - "HiDream-ai/HiDream-I1-Dev", - "HiDream-ai/HiDream-I1-Full", "Kwai-Kolors/Kolors-diffusers", - "briaai/BRIA-3.2", + "THUDM/CogView4-6B", + "kandinsky-community/kandinsky-2-1", + "kandinsky-community/kandinsky-2-2-decoder", + "kandinsky-community/kandinsky-3", + # "juggernautXL_juggXIByRundiffusion.safetensors@https://civitai.com/api/download/models/782002", + # "playground-v2.5-1024px-aesthetic.fp16.safetensors@https://huggingface.co/playgroundai/playground-v2.5-1024px-aesthetic/resolve/main/playground-v2.5-1024px-aesthetic.fp16.safetensors?download=true", ] +styles = [ + 'Fixed Astronaut', + 'Fixed Bear', + 'Fixed Steampunk City', + 'Fixed Road sign', + 'Fixed Futuristic hypercar', + 'Fixed Pirate Ship in Space', + 'Fixed Fallout girl', + 'Fixed Kneeling on Bed', + 'Fixed Girl in Sin City', + 'Fixed Girl in a city', + 'Fixed Lady in Tokyo', + 'Fixed MadMax selfie', + 'Fixed SDNext Neon', +] + + +def request(endpoint: str, dct: dict = None, method: str = 'POST'): + def auth(): + if sd_username is not None and sd_password is not None: + return requests.auth.HTTPBasicAuth(sd_username, sd_password) + return None + 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) + method = requests.post if method.upper() == 'POST' else requests.get + req = method(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 generate(): # pylint: disable=redefined-outer-name + for m, model in enumerate(models): + model_name = pathvalidate.sanitize_filename(model, replacement_text='_') + log.info(f'model: name="{model}" n={m+1}/{len(models)}') + for s, style in enumerate(styles): + model_name = pathvalidate.sanitize_filename(model, replacement_text='_') + style_name = pathvalidate.sanitize_filename(style, replacement_text='_') + fn = os.path.join(output_folder, f'{model_name}__{style_name}.jpg') + if os.path.exists(fn): + continue + request(f'/sdapi/v1/checkpoint?sd_model_checkpoint={model}', method='POST') + loaded = request('/sdapi/v1/checkpoint', method='GET') + if not (model in loaded.get('checkpoint') or model in loaded.get('title') or model in loaded.get('name')): + log.error(f' model: error="{model}"') + continue + log.info(f' style: name="{style}" n={s+1}/{len(styles)} fn="{fn}"') + t0 = time.time() + data = request('/sdapi/v1/txt2img', { 'styles': [style] }) + t1 = time.time() + if 'images' in data and len(data['images']) > 0: + b64 = data['images'][0].split(',',1)[0] + image = Image.open(io.BytesIO(base64.b64decode(b64))) + info = data['info'] + log.info(f' image: size={image.size} time={t1-t0:.2f} info="{len(info)}" fn="{fn}"') + image.save(fn) + + +if __name__ == "__main__": + log.info('test-all-models') + log.info(f'output="{output_folder}" models={len(models)} styles={len(styles)}') + log.info('start...') + generate() + log.info('done...') diff --git a/modules/api/api.py b/modules/api/api.py index 0c151300d..f1fddb392 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -91,6 +91,7 @@ class Api: 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/checkpoint", endpoints.set_checkpoint, methods=["POST"]) 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 28c573559..3655f7130 100644 --- a/modules/api/endpoints.py +++ b/modules/api/endpoints.py @@ -140,6 +140,14 @@ def get_checkpoint(): checkpoint['hash'] = shared.sd_model.sd_checkpoint_info.shorthash return checkpoint +def set_checkpoint(sd_model_checkpoint: str, force:bool=False): + from modules import sd_models + if force: + sd_models.unload_model_weights(op='model') + shared.opts.sd_model_checkpoint = sd_model_checkpoint + model = sd_models.reload_model_weights() + return { 'ok': model is not None } + def post_refresh_checkpoints(): shared.refresh_checkpoints() return {} diff --git a/modules/api/nudenet.py b/modules/api/nudenet.py index 4756d42f3..a1243592a 100644 --- a/modules/api/nudenet.py +++ b/modules/api/nudenet.py @@ -58,7 +58,7 @@ def banned_words( def register_api(): from modules.shared import api as api_instance - api_instance.add_api_route("/sdapi/v1//nudenet", nudenet_censor, methods=["POST"], response_model=dict) - api_instance.add_api_route("/sdapi/v1//prompt-lang", prompt_check, methods=["POST"], response_model=dict) - api_instance.add_api_route("/sdapi/v1//image-guard", image_guard, methods=["POST"], response_model=dict) - api_instance.add_api_route("/sdapi/v1//prompt-banned", banned_words, methods=["POST"], response_model=list) + api_instance.add_api_route("/sdapi/v1/nudenet", nudenet_censor, methods=["POST"], response_model=dict) + api_instance.add_api_route("/sdapi/v1/prompt-lang", prompt_check, methods=["POST"], response_model=dict) + api_instance.add_api_route("/sdapi/v1/image-guard", image_guard, methods=["POST"], response_model=dict) + api_instance.add_api_route("/sdapi/v1/prompt-banned", banned_words, methods=["POST"], response_model=list) diff --git a/modules/sd_checkpoint.py b/modules/sd_checkpoint.py index 0602c6996..5dd08cca6 100644 --- a/modules/sd_checkpoint.py +++ b/modules/sd_checkpoint.py @@ -201,6 +201,7 @@ def get_closet_checkpoint_match(s: str) -> CheckpointInfo: checkpoint_info = CheckpointInfo(model_name) # create a virutal model info checkpoint_info.type = 'huggingface' return checkpoint_info + if s.startswith('huggingface/'): model_name = s.replace('huggingface/', '') checkpoint_info = CheckpointInfo(model_name) # create a virutal model info From 5a9c52a48ae5b793cdb85c2f7c05064440e026cb Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 07:13:04 -0400 Subject: [PATCH 065/141] url(./html/svg/square.svg) Signed-off-by: Vladimir Mandic --- cli/test-all-models.py | 20 ++++++++------------ extensions-builtin/sdnext-modernui | 2 +- javascript/black-teal-reimagined.css | 4 +--- javascript/light-teal.css | 1 + 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/cli/test-all-models.py b/cli/test-all-models.py index 17d945ef1..baf59f1c8 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -19,6 +19,7 @@ output_folder = 'outputs/compare' models = [ "sdxl-base-v10-vaefix", "tempest-by-vlad-0.1", + "icbinpXL_v6", "stabilityai/stable-diffusion-3.5-medium", "stabilityai/stable-diffusion-3.5-large", "black-forest-labs/FLUX.1-dev", @@ -27,18 +28,17 @@ models = [ "vladmandic/chroma-unlocked-v50", "vladmandic/chroma-unlocked-v50-annealed", "Qwen/Qwen-Image", - "HiDream-ai/HiDream-I1-Dev", - "HiDream-ai/HiDream-I1-Full", "briaai/BRIA-3.2", + "stabilityai/stable-cascade", + "ostris/Flex.2-preview", + "OmniGen2/OmniGen2", + "Freepik/F-Lite", + "Freepik/F-Lite-Texture", + "HiDream-ai/HiDream-I1-Full", "nvidia/Cosmos-Predict2-2B-Text2Image", "nvidia/Cosmos-Predict2-14B-Text2Image", "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", "Wan-AI/Wan2.1-T2V-14B-Diffusers", - "OmniGen2/OmniGen2", - "stabilityai/stable-cascade", - "ostris/Flex.2-preview", - "Freepik/F-Lite", - "Freepik/F-Lite-Texture", "Efficient-Large-Model/SANA1.5_1.6B_1024px_diffusers", "Efficient-Large-Model/SANA1.5_4.8B_1024px_diffusers", "fal/AuraFlow-v0.3", @@ -49,11 +49,7 @@ models = [ "Alpha-VLLM/Lumina-Image-2.0", "Kwai-Kolors/Kolors-diffusers", "THUDM/CogView4-6B", - "kandinsky-community/kandinsky-2-1", - "kandinsky-community/kandinsky-2-2-decoder", "kandinsky-community/kandinsky-3", - # "juggernautXL_juggXIByRundiffusion.safetensors@https://civitai.com/api/download/models/782002", - # "playground-v2.5-1024px-aesthetic.fp16.safetensors@https://huggingface.co/playgroundai/playground-v2.5-1024px-aesthetic/resolve/main/playground-v2.5-1024px-aesthetic.fp16.safetensors?download=true", ] styles = [ 'Fixed Astronaut', @@ -81,7 +77,7 @@ def request(endpoint: str, dct: dict = None, method: str = 'POST'): sd_username = os.environ.get('SDAPI_USR', None) sd_password = os.environ.get('SDAPI_PWD', None) method = requests.post if method.upper() == 'POST' else requests.get - req = method(f'{sd_url}{endpoint}', json = dct, timeout=300, verify=False, auth=auth()) + req = method(f'{sd_url}{endpoint}', json = dct, timeout=120000, verify=False, auth=auth()) if req.status_code != 200: return { 'error': req.status_code, 'reason': req.reason, 'url': req.url } else: diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 5ec177ccb..1b7eb7773 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 5ec177ccbf818cc759bd5306f450b7bcdaaf2744 +Subproject commit 1b7eb7773343ed881256664acb95bb34213ae574 diff --git a/javascript/black-teal-reimagined.css b/javascript/black-teal-reimagined.css index 3f1cdf9a9..9934b9240 100644 --- a/javascript/black-teal-reimagined.css +++ b/javascript/black-teal-reimagined.css @@ -1124,9 +1124,7 @@ svg.feather.feather-image, } /* Based on Gradio Built-in Dark Theme */ -:root, -.light, -.dark { +:root, .light, .dark { --body-background-fill: var(--background-color); --color-accent-soft: var(--neutral-700); --background-fill-secondary: none; diff --git a/javascript/light-teal.css b/javascript/light-teal.css index df8a3ab51..5d0ccfc22 100644 --- a/javascript/light-teal.css +++ b/javascript/light-teal.css @@ -197,6 +197,7 @@ svg.feather.feather-image, .feather .feather-image { display: none } --checkbox-border-radius: var(--radius-sm); --checkbox-border-width: var(--input-border-width); --checkbox-check: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); + --radio-circle: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); --checkbox-label-background-fill-hover: None; --checkbox-label-background-fill-selected: var(--checkbox-label-background-fill); --checkbox-label-background-fill: None; From 35d68feb1eacefafe27d0c9d08eb15cdcbf3b0f8 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 07:29:30 -0400 Subject: [PATCH 066/141] networks improve text readability Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- javascript/black-teal-reimagined.css | 13 ------------- javascript/sdnext.css | 9 ++++----- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 1b7eb7773..bc510ba7e 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 1b7eb7773343ed881256664acb95bb34213ae574 +Subproject commit bc510ba7e5887db5342f6ad463943849755f4b8f diff --git a/javascript/black-teal-reimagined.css b/javascript/black-teal-reimagined.css index 9934b9240..1e7d4dc0b 100644 --- a/javascript/black-teal-reimagined.css +++ b/javascript/black-teal-reimagined.css @@ -862,19 +862,6 @@ svg.feather.feather-image, border-radius: var(--radius-md); } -/* Overlay Name Styles */ -.extra-network-cards .card .overlay .name { - font-size: var(--text-lg); - font-weight: bold; - text-shadow: 1px 1px black; - color: white; - overflow-wrap: anywhere; - position: absolute; - bottom: 0; - padding: 0.2em; - z-index: 10; -} - /* Preview Styles */ .extra-network-cards .card .preview { box-shadow: var(--button-shadow); diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 4c335e30c..68255a8af 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -1285,17 +1285,16 @@ table.settings-value-table td { bottom: 0; color: white; font-size: var(--text-lg); - font-weight: bold; - overflow-wrap: anywhere; + overflow-wrap: break-word; padding: 0.2em; position: absolute; - text-shadow: 1px 1px black; z-index: 10; + text-shadow: 2px 2px 2px black; + filter: drop-shadow(0px 0px 4px black); } .extra-network-cards .card .overlay .reference { - - color: var(--body-text-color-subdued) + background-color: rgba(0, 0, 0, 0.2); } .extra-network-cards .card .preview { From 46802422e3a4d3926cda43e4cf50d087598ba0ae Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 07:55:58 -0400 Subject: [PATCH 067/141] model preview mappings Signed-off-by: Vladimir Mandic --- html/previews.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/html/previews.json b/html/previews.json index 3b59d376c..aabb38822 100644 --- a/html/previews.json +++ b/html/previews.json @@ -14,5 +14,8 @@ "Efficient-Large-Model--Sana_1600M_4Kpx_BF16_diffusers": "models/Reference/Efficient-Large-Model--Sana_1600M_1024px_diffusers.jpg", "Efficient-Large-Model--Sana_600M_1024px_diffusers": "models/Reference/Efficient-Large-Model--Sana_1600M_1024px_diffusers.jpg", "stabilityai--stable-video-diffusion-img2vid-xt-1-1": "models/Reference/stabilityai--stable-video-diffusion-img2vid-xt.jpg", - "shuttleai--shuttle-3-diffusion": "models/Reference/shuttleai--shuttle-3-diffusion.jpg" + "shuttleai--shuttle-3-diffusion": "models/Reference/shuttleai--shuttle-3-diffusion.jpg", + "HiDream-ai/HiDream-I1-Full": "models/Reference/HiDream-I1 Full", + "vladmandic/chroma-unlocked-v50": "models/Reference/lodestones Chroma Unlocked HD", + "vladmandic/chroma-unlocked-v50-annealed": "models/Reference/lodestones Chroma Unlocked HD" } From e5637c3e8df04dba6b8b3af74306f6b5ce85ee47 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 07:58:25 -0400 Subject: [PATCH 068/141] add pag to namegen Signed-off-by: Vladimir Mandic --- modules/images_namegen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/images_namegen.py b/modules/images_namegen.py index 8cf565b67..673ab9cac 100644 --- a/modules/images_namegen.py +++ b/modules/images_namegen.py @@ -49,6 +49,7 @@ class FilenameGenerator: 'seed': lambda self: (self.seed and str(self.seed)) or '', 'steps': lambda self: self.p and getattr(self.p, 'steps', 0), 'cfg': lambda self: self.p and getattr(self.p, 'cfg_scale', 0), + 'pag': lambda self: self.p and getattr(self.p, 'pag_scale', 0), 'clip_skip': lambda self: self.p and getattr(self.p, 'clip_skip', 0), 'denoising': lambda self: self.p and getattr(self.p, 'denoising_strength', 0), 'styles': lambda self: (self.p and ", ".join([style for style in self.p.styles if not style == "None"])) or "None", From 41640773e5a1b8fc0affa128dee977509d90031f Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 09:19:49 -0400 Subject: [PATCH 069/141] fix global state tracking Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 9 ++-- extensions-builtin/sdnext-modernui | 2 +- modules/api/nvml.py | 31 ++++++------ modules/api/rocm_smi.py | 17 +++++-- modules/memstats.py | 81 +++++++++++++++++------------- modules/sd_models.py | 2 +- modules/shared_state.py | 6 ++- 7 files changed, 83 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc4f24636..a3ce1dfba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Change Log for SD.Next -## Update for 2025-08-09 +## Update for 2025-08-10 -### Highlights for 2025-08-09 +### Highlights for 2025-08-10 Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) and [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) and [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) @@ -12,7 +12,7 @@ And (*as always*) many bugfixes and improvements to existing features! [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) -### Details for 2025-08-09 +### Details for 2025-08-10 - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) @@ -113,7 +113,8 @@ And (*as always*) many bugfixes and improvements to existing features! - fix *Flux.1-Kontext-Dev* with variable resolution - use `utf_16_be` as primary metadata decoding - fix `sd35` width/height alignment - - fix `nudenet` api + - fix `nudenet` api + - fix global state tracking - reapply offloading on ipadapter load - api set default script-name - avoid forced gc and rely on thresholds diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index bc510ba7e..9a9c0f8ed 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit bc510ba7e5887db5342f6ad463943849755f4b8f +Subproject commit 9a9c0f8ed77e6f42f9e2528e547cbc242d801bb3 diff --git a/modules/api/nvml.py b/modules/api/nvml.py index 4ecc73b0e..177c4fc54 100644 --- a/modules/api/nvml.py +++ b/modules/api/nvml.py @@ -25,9 +25,11 @@ def get_reason(val): reason = ', '.join([throttle[i] for i in throttle if i & val]) return reason if len(reason) > 0 else 'ok' + def get_nvml(): global nvml_initialized # pylint: disable=global-statement try: + from modules.memstats import ram_stats if not nvml_initialized: install('pynvml', quiet=True) import pynvml # pylint: disable=redefined-outer-name @@ -44,33 +46,28 @@ def get_nvml(): except Exception: name = '' load = pynvml.nvmlDeviceGetUtilizationRates(dev) - """ - 'load': { - 'gpu': round(load.gpu), - 'memory': round(load.memory), - 'temp': pynvml.nvmlDeviceGetTemperature(dev, 0), - 'fan': pynvml.nvmlDeviceGetFanSpeed(dev), - }, - 'chart_val1': load.memory, - 'chart_val2': load.gpu, - } - """ mem = pynvml.nvmlDeviceGetMemoryInfo(dev) + ram = ram_stats() data = { - "CUDA": f'version {pynvml.nvmlSystemGetCudaDriverVersion()} compute {pynvml.nvmlDeviceGetCudaComputeCapability(dev)}', + "CUDA": f'Version {pynvml.nvmlSystemGetCudaDriverVersion()} Compute {pynvml.nvmlDeviceGetCudaComputeCapability(dev)}', "Driver": pynvml.nvmlSystemGetDriverVersion(), "Hardware": f'VBIOS {pynvml.nvmlDeviceGetVbiosVersion(dev)} ROM {pynvml.nvmlDeviceGetInforomImageVersion(dev)}', - "PCI link": f'gen.{pynvml.nvmlDeviceGetCurrPcieLinkGeneration(dev)} x{pynvml.nvmlDeviceGetCurrPcieLinkWidth(dev)}', + "PCI link": f'Gen.{pynvml.nvmlDeviceGetCurrPcieLinkGeneration(dev)} x{pynvml.nvmlDeviceGetCurrPcieLinkWidth(dev)}', "Power": f'{round(pynvml.nvmlDeviceGetPowerUsage(dev)/1000, 2)} W / {round(pynvml.nvmlDeviceGetEnforcedPowerLimit(dev)/1000, 2)} W', "GPU clock": f'{pynvml.nvmlDeviceGetClockInfo(dev, 0)} Mhz / {pynvml.nvmlDeviceGetMaxClockInfo(dev, 0)} Mhz', "SM clock": f'{pynvml.nvmlDeviceGetClockInfo(dev, 1)} Mhz / {pynvml.nvmlDeviceGetMaxClockInfo(dev, 1)} Mhz', - "Memory clock": f'{pynvml.nvmlDeviceGetClockInfo(dev, 2)} Mhz / {pynvml.nvmlDeviceGetMaxClockInfo(dev, 2)} Mhz', - "Memory usage": f'used {round(mem.used / 1024 / 1024)} MB | free {round(mem.free / 1024 / 1024)} MB | total {round(mem.total / 1024 / 1024)} MB', - "System load": f'GPU {load.gpu}% | Memory {load.memory}% | temp {pynvml.nvmlDeviceGetTemperature(dev, 0)}C | fan {pynvml.nvmlDeviceGetFanSpeed(dev)}%', + "VRAM clock": f'{pynvml.nvmlDeviceGetClockInfo(dev, 2)} Mhz / {pynvml.nvmlDeviceGetMaxClockInfo(dev, 2)} Mhz', + "VRAM usage": f'{round(100 * mem.used / mem.total)}% | {round(mem.used / 1024 / 1024)} MB used | {round(mem.free / 1024 / 1024)} MB free | {round(mem.total / 1024 / 1024)} MB total', + "RAM usage": f'{round(100 * ram["used"] / ram["total"])}% | {round(1024 * ram["used"])} MB used | {round(1024 * ram["free"])} MB free | {round(1024 * ram["total"])} MB total', + "System load": f'GPU {load.gpu}% | VRAM {load.memory}% | Temp {pynvml.nvmlDeviceGetTemperature(dev, 0)}C | Fan {pynvml.nvmlDeviceGetFanSpeed(dev)}%', 'State': get_reason(pynvml.nvmlDeviceGetCurrentClocksThrottleReasons(dev)), } chart = [load.memory, load.gpu] - devices.append({ 'name': name, 'data': data, 'chart': chart }) + devices.append({ + 'name': name, + 'data': data, + 'chart': chart, + }) # log.debug(f'nmvl: {devices}') return devices except Exception as e: diff --git a/modules/api/rocm_smi.py b/modules/api/rocm_smi.py index 870bfaaed..bbf7f1ba7 100644 --- a/modules/api/rocm_smi.py +++ b/modules/api/rocm_smi.py @@ -3,12 +3,14 @@ import json import subprocess as sp from enum import IntFlag + try: from installer import log except Exception: import logging log = logging.getLogger(__name__) + try: from modules.rocm import version as rocm_version except Exception: @@ -87,14 +89,19 @@ def get_rocm_smi(): "PCI link": f'Gen.{int(math.log2(float(rocm_smi_data[key].get("pcie_link_speed (0.1 GT/s)", 10)) / 10))} x{rocm_smi_data[key].get("pcie_link_width (Lanes)", "unknown")}', "Power": f'{round(float(rocm_smi_data[key].get("Average Graphics Package Power (W)", 0)), 2)} W / {round(float(rocm_smi_data[key].get("Max Graphics Package Power (W)", 0)), 2)} W', "GPU clock": f'{rocm_smi_data[key].get("average_gfxclk_frequency (MHz)", 0)} Mhz / {rocm_smi_data[key].get("Valid sclk range", "0").split(" - ")[-1].removesuffix("Mhz")} Mhz', - "Memory clock": f'{rocm_smi_data[key].get("current_uclk (MHz)", 0)} Mhz / {rocm_smi_data[key].get("Valid mclk range", "0").split(" - ")[-1].removesuffix("Mhz")} Mhz', - "Memory usage": f'used {load["memory"]}% | activity {rocm_smi_data[key].get("GPU Memory Read/Write Activity (%)", "unknown")}%', - "GPU usage": f'GPU {load["gpu"]}% | fan {load["fan"]}%', - "GPU temp": f'edge {load["temp"]}C | junction {load["temp_junction"]}C | memory {load["temp_memory"]}C', + "VRAM clock": f'{rocm_smi_data[key].get("current_uclk (MHz)", 0)} Mhz / {rocm_smi_data[key].get("Valid mclk range", "0").split(" - ")[-1].removesuffix("Mhz")} Mhz', + "VRAM usage": f'{load["memory"]}% Used | {rocm_smi_data[key].get("GPU Memory Read/Write Activity (%)", "unknown")}% Activity', + "GPU usage": f'GPU {load["gpu"]}% | Fan {load["fan"]}%', + "GPU temp": f'Edge {load["temp"]}C | Junction {load["temp_junction"]}C | Memory {load["temp_memory"]}C', 'Throttle reason': str(ThrottleStatus(int(rocm_smi_data[key].get("throttle_status", 0)))), } + name = rocm_smi_data[key].get('Device Name', 'unknown') chart = [load["memory"], load["gpu"]] - devices.append({ 'name': rocm_smi_data[key].get('Device Name', 'unknown'), 'data': data, 'chart': chart }) + devices.append({ + 'name': name, + 'data': data, + 'chart': chart, + }) return devices except Exception as e: log.error(f'ROCm SMI: {e}') diff --git a/modules/memstats.py b/modules/memstats.py index 47469fd77..a2b8c1133 100644 --- a/modules/memstats.py +++ b/modules/memstats.py @@ -7,7 +7,10 @@ from modules import shared, errors fail_once = False +ram = {} +gpu = {} mem = {} +process = None docker_limit = None runpod_limit = None @@ -40,40 +43,58 @@ def get_runpod_limit(): return runpod_limit -def memory_stats(): - global fail_once # pylint: disable=global-statement - mem.clear() +def ram_stats(): + global process, fail_once # pylint: disable=global-statement try: - process = psutil.Process(os.getpid()) + if process is None: + process = psutil.Process(os.getpid()) res = process.memory_info() - ram_total = 100 * res.rss / process.memory_percent() - ram_total = min(ram_total, get_docker_limit(), get_runpod_limit()) - ram = { 'used': gb(res.rss), 'total': gb(ram_total) } - mem.update({ 'ram': ram }) + if 'total' not in ram: + process = psutil.Process(os.getpid()) + ram_total = 100 * res.rss / process.memory_percent() + ram_total = min(ram_total, get_docker_limit(), get_runpod_limit()) + ram['total'] = gb(ram_total) + ram['used'] = gb(res.rss) + ram['free'] = ram['total'] - ram['used'] except Exception as e: + ram['error'] = str(e) if not fail_once: - shared.log.error(f'Memory stats: {e}') - errors.display(e, 'Memory stats') + shared.log.error(f'RAM stats: {e}') + errors.display(e, 'RAM stats') fail_once = True - mem.update({ 'ram': { 'error': str(e) } }) + return ram + + +def gpu_stats(): + global fail_once # pylint: disable=global-statement try: free, total = torch.cuda.mem_get_info() - gpu = { 'used': gb(total - free), 'total': gb(total) } + gpu['used'] = gb(total - free) + gpu['total'] = gb(total) stats = dict(torch.cuda.memory_stats()) if stats.get('num_ooms', 0) > 0: shared.state.oom = True - mem.update({ - 'gpu': gpu, - 'active': gb(stats.get('active_bytes.all.current', 0)), - 'peak': gb(stats.get('active_bytes.all.peak', 0)), - 'retries': stats.get('num_alloc_retries', 0), - 'oom': stats.get('num_ooms', 0), - 'job': shared.state.job, - }) - mem['swap'] = round(mem['active'] - mem['gpu']['used'], 2) if mem['active'] > mem['gpu']['used'] else 0 - return mem - except Exception: - pass + gpu['active'] = gb(stats.get('active_bytes.all.current', 0)) + gpu['peak'] = gb(stats.get('active_bytes.all.peak', 0)) + gpu['retries'] = stats.get('num_alloc_retries', 0) + gpu['oom'] = stats.get('num_ooms', 0) + except Exception as e: + gpu['error'] = str(e) + if not fail_once: + shared.log.error(f'GPU stats: {e}') + errors.display(e, 'GPU stats') + fail_once = True + return gpu + + +def memory_stats(): + mem['ram'] = ram_stats() + mem['gpu'] = gpu_stats() + mem['job'] = shared.state.job + try: + mem['gpu']['swap'] = round(mem['gpu']['active'] - mem['gpu']['used']) if mem['gpu']['active'] > mem['gpu']['used'] else 0 + except: + mem['gpu']['swap'] = 0 return mem @@ -84,18 +105,6 @@ def reset_stats(): pass -def ram_stats(): - try: - process = psutil.Process(os.getpid()) - res = process.memory_info() - ram_total = 100 * res.rss / process.memory_percent() - ram_total = min(ram_total, get_docker_limit(), get_runpod_limit()) - ram = { 'used': gb(res.rss), 'total': gb(ram_total) } - return ram - except Exception: - return { 'used': 0, 'total': 0 } - - class Object: pattern = r"'(.*?)'" diff --git a/modules/sd_models.py b/modules/sd_models.py index cbafbea80..d0c3423b1 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -1057,7 +1057,7 @@ def reload_model_weights(sd_model=None, info=None, op='model', force=False, revi unload_model_weights(op=op) return None orig_state = copy.deepcopy(shared.state) - shared.state = shared_state.State() + # shared.state = shared_state.State() shared.state.begin('Load') if sd_model is None: sd_model = model_data.sd_model if op == 'model' or op == 'dict' else model_data.sd_refiner diff --git a/modules/shared_state.py b/modules/shared_state.py index c7c1cb472..616e92cce 100644 --- a/modules/shared_state.py +++ b/modules/shared_state.py @@ -50,6 +50,9 @@ class State: server_start = time.time() oom = False + def __init__(self): + log.debug(f'State initialized: id={id(self)}') + def __str__(self) -> str: status = ' ' status += 'skipped ' if self.skipped else '' @@ -208,7 +211,8 @@ class State: log.trace(f'State end: {self}') self.time_end = time.time() self.history('end') - self.job = "" + self.id = '' + self.job = '' self.job_count = 0 self.job_no = 0 self.frame_count = 0 From 67408bb58984f10c41468d8e1df5f1a2c3579ba3 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 09:54:11 -0400 Subject: [PATCH 070/141] fix networks active tab detection Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + javascript/extraNetworks.js | 160 +++++++++++++++++++----------------- javascript/ui.js | 4 +- 3 files changed, 86 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ce1dfba..069619144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,7 @@ And (*as always*) many bugfixes and improvements to existing features! - fix `sd35` width/height alignment - fix `nudenet` api - fix global state tracking + - fix ui tab detection for networks - reapply offloading on ipadapter load - api set default script-name - avoid forced gc and rely on thresholds diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 29b97005f..a2e35f426 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -1,32 +1,38 @@ const activePromptTextarea = {}; let sortVal = -1; let totalCards = -1; +let lastTab = 'control'; // helpers const getENActiveTab = () => { let tabName = ''; - if (gradioApp().getElementById('txt2img_prompt')?.checkVisibility()) return 'txt2img'; - if (gradioApp().getElementById('img2img_prompt')?.checkVisibility()) return 'img2img'; - if (gradioApp().getElementById('control_prompt')?.checkVisibility()) return 'control'; - if (gradioApp().getElementById('video_prompt')?.checkVisibility()) return 'video'; - if (gradioApp().getElementById('framepack_prompt_row')?.checkVisibility()) return 'framepack'; + if (gradioApp().getElementById('txt2img_prompt')?.checkVisibility()) tabName = 'txt2img'; + else if (gradioApp().getElementById('img2img_prompt')?.checkVisibility()) tabName = 'img2img'; + else if (gradioApp().getElementById('control_prompt')?.checkVisibility()) tabName = 'control'; + else if (gradioApp().getElementById('video_prompt')?.checkVisibility()) tabName = 'video'; + else if (gradioApp().getElementById('extras_image')?.checkVisibility()) tabName = 'process'; + else if (gradioApp().getElementById('interrogate_image')?.checkVisibility()) tabName = 'caption'; + else if (gradioApp().getElementById('tab-gallery-search')?.checkVisibility()) tabName = 'gallery'; + if (tabName in ['process', 'caption', 'gallery']) tabName = lastTab; + else lastTab = tabName; + if (tabName !== '') return tabName; // legacy method if (gradioApp().getElementById('tab_txt2img')?.style.display === 'block') tabName = 'txt2img'; else if (gradioApp().getElementById('tab_img2img')?.style.display === 'block') tabName = 'img2img'; else if (gradioApp().getElementById('tab_control')?.style.display === 'block') tabName = 'control'; else if (gradioApp().getElementById('tab_video')?.style.display === 'block') tabName = 'video'; - else if (gradioApp().getElementById('tab_framepack_tab')?.style.display === 'block') tabName = 'framepack'; + else tabName = 'control'; // log('getENActiveTab', tabName); return tabName; }; const getENActivePage = () => { - const tabname = getENActiveTab(); - let page = gradioApp().querySelector(`#${tabname}_extra_networks > .tabs > .tab-nav > .selected`); - if (!page) page = gradioApp().querySelector(`#${tabname}_extra_tabs > .tab-nav > .selected`); + const tabName = getENActiveTab(); + let page = gradioApp().querySelector(`#${tabName}_extra_networks > .tabs > .tab-nav > .selected`); + if (!page) page = gradioApp().querySelector(`#${tabName}_extra_tabs > .tab-nav > .selected`); const pageName = page ? page.innerText : ''; - const btnApply = gradioApp().getElementById(`${tabname}_extra_apply`); + const btnApply = gradioApp().getElementById(`${tabName}_extra_apply`); if (btnApply) btnApply.style.display = pageName === 'Style' ? 'inline-flex' : 'none'; // log('getENActivePage', pageName); return pageName; @@ -48,8 +54,8 @@ const setENState = (state) => { function showCardDetails(event) { // log('showCardDetails', event); - const tabname = getENActiveTab(); - const btn = gradioApp().getElementById(`${tabname}_extra_details_btn`); + const tabName = getENActiveTab(); + const btn = gradioApp().getElementById(`${tabName}_extra_details_btn`); btn.click(); event.stopPropagation(); event.preventDefault(); @@ -97,8 +103,8 @@ function readCardTags(el, tags) { function readCardDescription(page, item) { xhrGet('/sd_extra_networks/description', { page, item }, (data) => { - const tabname = getENActiveTab(); - const description = gradioApp().querySelector(`#${tabname}_description > label > textarea`); + const tabName = getENActiveTab(); + const description = gradioApp().querySelector(`#${tabName}_description > label > textarea`); if (description) { description.value = data?.description?.trim() || ''; updateInput(description); @@ -108,10 +114,10 @@ function readCardDescription(page, item) { } function getCardsForActivePage() { - const pagename = getENActivePage(); - if (!pagename) return []; + const pageName = getENActivePage(); + if (!pageName) return []; let allCards = Array.from(gradioApp().querySelectorAll('.extra-network-cards > .card')); - allCards = allCards.filter((el) => el.dataset.page?.toLowerCase().includes(pagename.toLowerCase())); + allCards = allCards.filter((el) => el.dataset.page?.toLowerCase().includes(pageName.toLowerCase())); // log('getCardsForActivePage', pagename, cards.length); return allCards; } @@ -234,9 +240,9 @@ function sortExtraNetworks(fixed = 'no') { return desc; } -function refreshENInput(tabname) { - log('refreshNetworks', tabname, gradioApp().querySelector(`#${tabname}_extra_networks textarea`)?.value); - gradioApp().querySelector(`#${tabname}_extra_networks textarea`)?.dispatchEvent(new Event('input')); +function refreshENInput(tabName) { + log('refreshNetworks', tabName, gradioApp().querySelector(`#${tabName}_extra_networks textarea`)?.value); + gradioApp().querySelector(`#${tabName}_extra_networks textarea`)?.dispatchEvent(new Event('input')); } async function markSelectedCards(selected, page = '') { @@ -257,9 +263,9 @@ function extractLoraNames(prompt) { } function cardClicked(textToAdd) { - const tabname = getENActiveTab(); - log('cardClicked', tabname, textToAdd); - const textarea = activePromptTextarea[tabname]; + const tabName = getENActiveTab(); + log('cardClicked', tabName, textToAdd); + const textarea = activePromptTextarea[tabName]; if (textarea.value.indexOf(textToAdd) !== -1) textarea.value = textarea.value.replace(textToAdd, ''); else textarea.value += textToAdd; updateInput(textarea); @@ -268,8 +274,8 @@ function cardClicked(textToAdd) { function extraNetworksSearchButton(event) { // log('extraNetworksSearchButton', event); - const tabname = getENActiveTab(); - const searchTextarea = gradioApp().querySelector(`#${tabname}_extra_search textarea`); + const tabName = getENActiveTab(); + const searchTextarea = gradioApp().querySelector(`#${tabName}_extra_search textarea`); const button = event.target; searchTextarea.value = `${button.textContent.trim()}/`; updateInput(searchTextarea); @@ -278,8 +284,8 @@ function extraNetworksSearchButton(event) { let desiredStyle = ''; function selectStyle(name) { desiredStyle = name; - const tabname = getENActiveTab(); - const button = gradioApp().querySelector(`#${tabname}_styles_select`); + const tabName = getENActiveTab(); + const button = gradioApp().querySelector(`#${tabName}_styles_select`); button.click(); } @@ -288,8 +294,8 @@ function applyStyles(styles) { if (styles) { newStyles = Array.isArray(styles) ? styles : [styles]; } else { - const tabname = getENActiveTab(); - styles = gradioApp().querySelectorAll(`#${tabname}_styles .token span`); + const tabName = getENActiveTab(); + styles = gradioApp().querySelectorAll(`#${tabName}_styles .token span`); newStyles = Array.from(styles).map((el) => el.textContent).filter((el) => el.length > 0); } const index = newStyles.indexOf(desiredStyle); @@ -300,16 +306,16 @@ function applyStyles(styles) { } function quickApplyStyle() { - const tabname = getENActiveTab(); - const btnApply = gradioApp().getElementById(`${tabname}_extra_apply`); + const tabName = getENActiveTab(); + const btnApply = gradioApp().getElementById(`${tabName}_extra_apply`); if (btnApply) btnApply.click(); } function quickSaveStyle() { - const tabname = getENActiveTab(); - const btnSave = gradioApp().getElementById(`${tabname}_extra_quicksave`); + const tabName = getENActiveTab(); + const btnSave = gradioApp().getElementById(`${tabName}_extra_quicksave`); if (btnSave) btnSave.click(); - const btnRefresh = gradioApp().getElementById(`${tabname}_extra_refresh`); + const btnRefresh = gradioApp().getElementById(`${tabName}_extra_refresh`); if (btnRefresh) { setTimeout(() => btnRefresh.click(), 100); // setTimeout(() => sortExtraNetworks('fixed'), 500); @@ -327,18 +333,18 @@ let enDirty = false; function closeDetailsEN(...args) { // log('closeDetailsEN'); enDirty = true; - const tabname = getENActiveTab(); - const btnClose = gradioApp().getElementById(`${tabname}_extra_details_close`); + const tabName = getENActiveTab(); + const btnClose = gradioApp().getElementById(`${tabName}_extra_details_close`); if (btnClose) setTimeout(() => btnClose.click(), 100); - const btnRefresh = gradioApp().getElementById(`${tabname}_extra_refresh`); + const btnRefresh = gradioApp().getElementById(`${tabName}_extra_refresh`); if (btnRefresh && enDirty) setTimeout(() => btnRefresh.click(), 100); return [...args]; } function refeshDetailsEN(args) { // log(`refeshDetailsEN: ${enDirty}`); - const tabname = getENActiveTab(); - const btnRefresh = gradioApp().getElementById(`${tabname}_extra_refresh`); + const tabName = getENActiveTab(); + const btnRefresh = gradioApp().getElementById(`${tabName}_extra_refresh`); if (btnRefresh && enDirty) setTimeout(() => btnRefresh.click(), 100); enDirty = false; return args; @@ -348,30 +354,30 @@ function refeshDetailsEN(args) { function refreshENpage() { if (getCardsForActivePage().length === 0) { // log('refreshENpage'); - const tabname = getENActiveTab(); - const btnRefresh = gradioApp().getElementById(`${tabname}_extra_refresh`); + const tabName = getENActiveTab(); + const btnRefresh = gradioApp().getElementById(`${tabName}_extra_refresh`); if (btnRefresh) btnRefresh.click(); } } // init -function setupExtraNetworksForTab(tabname) { - let tabs = gradioApp().querySelector(`#${tabname}_extra_tabs`); +function setupExtraNetworksForTab(tabName) { + let tabs = gradioApp().querySelector(`#${tabName}_extra_tabs`); if (tabs) tabs.classList.add('extra-networks'); - const en = gradioApp().getElementById(`${tabname}_extra_networks`); - tabs = gradioApp().querySelector(`#${tabname}_extra_tabs > div`); + const en = gradioApp().getElementById(`${tabName}_extra_networks`); + tabs = gradioApp().querySelector(`#${tabName}_extra_tabs > div`); if (!tabs) return; // buttons - const btnShow = gradioApp().getElementById(`${tabname}_extra_networks_btn`); - const btnRefresh = gradioApp().getElementById(`${tabname}_extra_refresh`); - const btnScan = gradioApp().getElementById(`${tabname}_extra_scan`); - const btnSave = gradioApp().getElementById(`${tabname}_extra_save`); - const btnClose = gradioApp().getElementById(`${tabname}_extra_close`); - const btnSort = gradioApp().getElementById(`${tabname}_extra_sort`); - const btnView = gradioApp().getElementById(`${tabname}_extra_view`); - const btnModel = gradioApp().getElementById(`${tabname}_extra_model`); - const btnApply = gradioApp().getElementById(`${tabname}_extra_apply`); + const btnShow = gradioApp().getElementById(`${tabName}_extra_networks_btn`); + const btnRefresh = gradioApp().getElementById(`${tabName}_extra_refresh`); + const btnScan = gradioApp().getElementById(`${tabName}_extra_scan`); + const btnSave = gradioApp().getElementById(`${tabName}_extra_save`); + const btnClose = gradioApp().getElementById(`${tabName}_extra_close`); + const btnSort = gradioApp().getElementById(`${tabName}_extra_sort`); + const btnView = gradioApp().getElementById(`${tabName}_extra_view`); + const btnModel = gradioApp().getElementById(`${tabName}_extra_model`); + const btnApply = gradioApp().getElementById(`${tabName}_extra_apply`); const buttons = document.createElement('span'); buttons.classList.add('buttons'); if (btnRefresh) buttons.appendChild(btnRefresh); @@ -387,8 +393,8 @@ function setupExtraNetworksForTab(tabname) { tabs.appendChild(buttons); // details - const detailsImg = gradioApp().getElementById(`${tabname}_extra_details_img`); - const detailsClose = gradioApp().getElementById(`${tabname}_extra_details_close`); + const detailsImg = gradioApp().getElementById(`${tabName}_extra_details_img`); + const detailsClose = gradioApp().getElementById(`${tabName}_extra_details_close`); if (detailsImg && detailsClose) { detailsImg.title = 'Close details'; detailsImg.onclick = () => detailsClose.click(); @@ -398,9 +404,9 @@ function setupExtraNetworksForTab(tabname) { const div = document.createElement('div'); div.classList.add('second-line'); tabs.appendChild(div); - const txtSearch = gradioApp().querySelector(`#${tabname}_extra_search`); - const txtSearchValue = gradioApp().querySelector(`#${tabname}_extra_search textarea`); - const txtDescription = gradioApp().getElementById(`${tabname}_description`); + const txtSearch = gradioApp().querySelector(`#${tabName}_extra_search`); + const txtSearchValue = gradioApp().querySelector(`#${tabName}_extra_search textarea`); + const txtDescription = gradioApp().getElementById(`${tabName}_description`); txtSearch.classList.add('search'); txtDescription.classList.add('description'); div.appendChild(txtSearch); @@ -418,7 +424,7 @@ function setupExtraNetworksForTab(tabname) { let hoverTimer = null; let previousCard = null; if (window.opts.extra_networks_fetch) { - gradioApp().getElementById(`${tabname}_extra_tabs`).onmouseover = async (e) => { + gradioApp().getElementById(`${tabName}_extra_tabs`).onmouseover = async (e) => { const el = e.target.closest('.card'); // bubble-up to card if (!el || (el.title === previousCard)) return; if (!hoverTimer) { @@ -438,7 +444,7 @@ function setupExtraNetworksForTab(tabname) { // auto-resize networks sidebar const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { - for (const el of Array.from(gradioApp().getElementById(`${tabname}_extra_tabs`).querySelectorAll('.extra-networks-page'))) { + for (const el of Array.from(gradioApp().getElementById(`${tabName}_extra_tabs`).querySelectorAll('.extra-networks-page'))) { const h = Math.trunc(entry.contentRect.height); if (h <= 0) return; const vh = opts.logmonitor_show ? '55vh' : '68vh'; @@ -446,12 +452,12 @@ function setupExtraNetworksForTab(tabname) { else if (window.opts.extra_networks_card_cover === 'inline' && window.opts.theme_type === 'Standard') el.style.height = '25vh'; else if (window.opts.extra_networks_card_cover === 'cover' && window.opts.theme_type === 'Standard') el.style.height = '50vh'; else el.style.height = 'unset'; - // log(`${tabname} height: ${entry.target.id}=${h} ${el.id}=${el.clientHeight}`); + // log(`${tabName} height: ${entry.target.id}=${h} ${el.id}=${el.clientHeight}`); } } }); - const settingsEl = gradioApp().getElementById(`${tabname}_settings`); - const interfaceEl = gradioApp().getElementById(`${tabname}_interface`); + const settingsEl = gradioApp().getElementById(`${tabName}_settings`); + const interfaceEl = gradioApp().getElementById(`${tabName}_interface`); if (settingsEl) resizeObserver.observe(settingsEl); if (interfaceEl) resizeObserver.observe(interfaceEl); @@ -466,7 +472,7 @@ function setupExtraNetworksForTab(tabname) { const target = window.opts.extra_networks_card_cover === 'sidebar' ? 0 : window.opts.extra_networks_height; if (window.opts.theme_type === 'Standard') h = target > 0 ? target : 55; else h = target > 0 ? target : 87; - for (const el of Array.from(gradioApp().getElementById(`${tabname}_extra_tabs`).querySelectorAll('.extra-networks-page'))) { + for (const el of Array.from(gradioApp().getElementById(`${tabName}_extra_tabs`).querySelectorAll('.extra-networks-page'))) { if (h > 0) el.style.height = `${h}vh`; el.parentElement.style.width = '-webkit-fill-available'; } @@ -490,7 +496,7 @@ function setupExtraNetworksForTab(tabname) { en.style.top = '13em'; en.style.transition = ''; en.style.zIndex = 100; - gradioApp().getElementById(`${tabname}_settings`).parentNode.style.width = 'unset'; + gradioApp().getElementById(`${tabName}_settings`).parentNode.style.width = 'unset'; } else if (window.opts.extra_networks_card_cover === 'sidebar') { en.style.position = 'absolute'; en.style.height = 'auto'; @@ -501,7 +507,7 @@ function setupExtraNetworksForTab(tabname) { en.style.top = '13em'; en.style.transition = 'width 0.3s ease'; en.style.zIndex = 100; - gradioApp().getElementById(`${tabname}_settings`).parentNode.style.width = `calc(100vw - 2em - min(${window.opts.extra_networks_sidebar_width}vw, 50vw))`; + gradioApp().getElementById(`${tabName}_settings`).parentNode.style.width = `calc(100vw - 2em - min(${window.opts.extra_networks_sidebar_width}vw, 50vw))`; } else { en.style.position = 'relative'; en.style.height = 'unset'; @@ -512,15 +518,15 @@ function setupExtraNetworksForTab(tabname) { en.style.top = 0; en.style.transition = ''; en.style.zIndex = 0; - gradioApp().getElementById(`${tabname}_settings`).parentNode.style.width = 'unset'; + gradioApp().getElementById(`${tabName}_settings`).parentNode.style.width = 'unset'; } } else { if (window.opts.extra_networks_card_cover === 'sidebar') en.style.width = 0; - gradioApp().getElementById(`${tabname}_settings`).parentNode.style.width = 'unset'; + gradioApp().getElementById(`${tabName}_settings`).parentNode.style.width = 'unset'; } - if (tabname === 'video') { - gradioApp().getElementById('framepack_settings').parentNode.style.width = gradioApp().getElementById(`${tabname}_settings`).parentNode.style.width; - gradioApp().getElementById('ltx_settings').parentNode.style.width = gradioApp().getElementById(`${tabname}_settings`).parentNode.style.width; + if (tabName === 'video') { + gradioApp().getElementById('framepack_settings').parentNode.style.width = gradioApp().getElementById(`${tabName}_settings`).parentNode.style.width; + gradioApp().getElementById('ltx_settings').parentNode.style.width = gradioApp().getElementById(`${tabName}_settings`).parentNode.style.width; } } }); @@ -528,8 +534,8 @@ function setupExtraNetworksForTab(tabname) { } async function showNetworks() { - for (const tabname of ['txt2img', 'img2img', 'control', 'video']) { - if (window.opts.extra_networks_show) gradioApp().getElementById(`${tabname}_extra_networks_btn`).click(); + for (const tabName of ['txt2img', 'img2img', 'control', 'video']) { + if (window.opts.extra_networks_show) gradioApp().getElementById(`${tabName}_extra_networks_btn`).click(); } log('showNetworks'); } @@ -540,11 +546,11 @@ async function setupExtraNetworks() { setupExtraNetworksForTab('control'); setupExtraNetworksForTab('video'); - function registerPrompt(tabname, id) { + function registerPrompt(tabName, id) { const textarea = gradioApp().querySelector(`#${id} > label > textarea`); if (!textarea) return; - if (!activePromptTextarea[tabname]) activePromptTextarea[tabname] = textarea; - textarea.addEventListener('focus', () => { activePromptTextarea[tabname] = textarea; }); + if (!activePromptTextarea[tabName]) activePromptTextarea[tabName] = textarea; + textarea.addEventListener('focus', () => { activePromptTextarea[tabName] = textarea; }); } registerPrompt('txt2img', 'txt2img_prompt'); diff --git a/javascript/ui.js b/javascript/ui.js index e6885a80e..6fe2f954b 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -479,8 +479,8 @@ function updateInput(target) { let desiredCheckpointName = null; function selectCheckpoint(name) { desiredCheckpointName = name; - const tabname = getENActiveTab(); - const btnModel = gradioApp().getElementById(`${tabname}_extra_model`); + const tabName = getENActiveTab(); + const btnModel = gradioApp().getElementById(`${tabName}_extra_model`); const isRefiner = btnModel && btnModel.classList.contains('toolbutton-selected'); if (isRefiner) gradioApp().getElementById('change_refiner').click(); else gradioApp().getElementById('change_checkpoint').click(); From 752d636324f6f5b901649fd6776334a9b84687b2 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 10:53:28 -0400 Subject: [PATCH 071/141] add /sdapi/v1/network Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + javascript/civitai.js | 2 +- javascript/extraNetworks.js | 2 +- modules/api/loras.py | 1 + modules/civitai/search_civitai.py | 2 +- modules/lora/lora_overrides.py | 4 +++ modules/ui_extra_networks.py | 34 +++++++++++++------ modules/ui_extra_networks_lora.py | 1 + modules/ui_extra_networks_styles.py | 1 + .../ui_extra_networks_textual_inversion.py | 1 + modules/ui_extra_networks_vae.py | 1 + 11 files changed, 37 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 069619144..21a3b93fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -410,6 +410,7 @@ And (as always) many bugfixes and improvements to existing features! *note*: you need to enable quantization and choose what it applies on, then xyz grid can change quantization mode *note*: you can also enable 'add time info' to compare performance of different quantization modes - **API** + - Add `/sdapi/v1/network?page=&item=` endpoint that returns full network info - Add `/sdapi/v1/lora?lora=` endpoint that returns full lora info and metadata - Add `/sdapi/v1/controlnets?model_type=` endpoints that returns list of available controlnets for specific model type - Set default sampler to `Default` diff --git a/javascript/civitai.js b/javascript/civitai.js index c60909f4a..31f1fc890 100644 --- a/javascript/civitai.js +++ b/javascript/civitai.js @@ -107,7 +107,7 @@ async function modelCardClick(id) { downloads: data.downloads?.toString() || '', creator, desc: data.desc || 'no description available', - image: images.length > 0 ? images[0] : './sd_extra_networks/thumb?filename=html/card-no-preview.png', + image: images.length > 0 ? images[0] : '/sdapi/v1/network/thumb?filename=html/card-no-preview.png', versions: versionsHTML || '', }); el.innerHTML = modelHTML; diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index a2e35f426..c7cfcdeaf 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -102,7 +102,7 @@ function readCardTags(el, tags) { } function readCardDescription(page, item) { - xhrGet('/sd_extra_networks/description', { page, item }, (data) => { + xhrGet('/sdapi/v1/network/desc', { page, item }, (data) => { const tabName = getENActiveTab(); const description = gradioApp().querySelector(`#${tabName}_description > label > textarea`); if (description) { diff --git a/modules/api/loras.py b/modules/api/loras.py index 4fbae29c5..c387bdaa0 100644 --- a/modules/api/loras.py +++ b/modules/api/loras.py @@ -7,6 +7,7 @@ def get_lora(lora: str) -> dict: if lora not in lora_load.available_networks: raise HTTPException(status_code=404, detail=f"Lora '{lora}' not found") obj = lora_load.available_networks[lora] + obj.meta = obj.get_metadata() obj.info = obj.get_info() obj.desc = obj.get_desc() return obj.__dict__ diff --git a/modules/civitai/search_civitai.py b/modules/civitai/search_civitai.py index dda495028..226925bf9 100644 --- a/modules/civitai/search_civitai.py +++ b/modules/civitai/search_civitai.py @@ -200,7 +200,7 @@ def create_model_cards(all_models: list[Model]) -> str: if image.url and len(image.url) > 0 and not image.url.lower().endswith('.mp4'): previews.append(image.url) if len(previews) == 0: - previews = ['./sd_extra_networks/thumb?filename=html/card-no-preview.png'] + previews = ['/sdapi/v1/network/thumb?filename=html/card-no-preview.png'] all_cards += card.format(id=model.id, name=model.name, type=model.type, preview=previews[0]) html = details + cards.format(cards=all_cards) return html diff --git a/modules/lora/lora_overrides.py b/modules/lora/lora_overrides.py index f18dfd9b1..9b51b62e2 100644 --- a/modules/lora/lora_overrides.py +++ b/modules/lora/lora_overrides.py @@ -33,6 +33,10 @@ force_models_diffusers = [ # forced always 'hunyuandit', 'auraflow', 'lumina2', + 'qwen', + 'bria', + 'flite', + 'cosmos', # video models 'hunyuanvideo', 'cogvideo', diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 46b002abe..8e9b5afdc 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -70,7 +70,7 @@ def init_api(): return FileResponse(filename, headers={"Accept-Ranges": "bytes"}) def get_metadata(page: str = "", item: str = ""): - page = next(iter([x for x in shared.extra_networks if x.name == page]), None) + page = next(iter([x for x in shared.extra_networks if x.name.lower() == page.lower()]), None) if page is None: return JSONResponse({ 'metadata': 'none' }) metadata = page.metadata.get(item, 'none') @@ -80,10 +80,10 @@ def init_api(): return JSONResponse({"metadata": metadata}) def get_info(page: str = "", item: str = ""): - page = next(iter([x for x in get_pages() if x.name == page]), None) + page = next(iter([x for x in get_pages() if x.name.lower() == page.lower()]), None) if page is None: return JSONResponse({ 'info': 'none' }) - item = next(iter([x for x in page.items if x['name'] == item]), None) + item = next(iter([x for x in page.items if x['name'].lower() == item.lower()]), None) if item is None: return JSONResponse({ 'info': 'none' }) info = page.find_info(item.get('filename', None) or item.get('name', None)) @@ -93,10 +93,10 @@ def init_api(): return JSONResponse({"info": info}) def get_desc(page: str = "", item: str = ""): - page = next(iter([x for x in get_pages() if x.name == page]), None) + page = next(iter([x for x in get_pages() if x.name.lower() == page.lower()]), None) if page is None: return JSONResponse({ 'description': 'none' }) - item = next(iter([x for x in page.items if x['name'] == item]), None) + item = next(iter([x for x in page.items if x['name'].lower() == item.lower()]), None) if item is None: return JSONResponse({ 'description': 'none' }) desc = page.find_description(item.get('filename', None) or item.get('name', None)) @@ -105,10 +105,21 @@ def init_api(): # shared.log.debug(f"Networks desc: page='{page.name}' item={item['name']} len={len(desc)}") return JSONResponse({"description": desc}) - shared.api.add_api_route("/sd_extra_networks/thumb", fetch_file, methods=["GET"]) - shared.api.add_api_route("/sd_extra_networks/metadata", get_metadata, methods=["GET"]) - shared.api.add_api_route("/sd_extra_networks/info", get_info, methods=["GET"]) - shared.api.add_api_route("/sd_extra_networks/description", get_desc, methods=["GET"]) + def get_network(page: str = "", item: str = ""): + page = next(iter([x for x in get_pages() if x.name.lower() == page.lower()]), None) + if page is None: + return JSONResponse({ 'page': 'none' }) + item = next(iter([x for x in page.items if (x['alias'].lower() == item.lower() or x['name'].lower() == item.lower())]), None) + if item is None: + return JSONResponse({ 'item': 'none' }) + return JSONResponse(item) + + + shared.api.add_api_route("/sdapi/v1/network", get_network, methods=["GET"]) + shared.api.add_api_route("/sdapi/v1/network/thumb", fetch_file, methods=["GET"]) + shared.api.add_api_route("/sdapi/v1/network/metadata", get_metadata, methods=["GET"]) + shared.api.add_api_route("/sdapi/v1/network/info", get_info, methods=["GET"]) + shared.api.add_api_route("/sdapi/v1/network/desc", get_desc, methods=["GET"]) class ExtraNetworksPage: @@ -131,6 +142,9 @@ class ExtraNetworksPage: self.view = shared.opts.extra_networks_view self.card = card_full if shared.opts.extra_networks_view == 'gallery' else card_list + def __str__(self): + return f'Page(title="{self.title}" name="{self.name}" items={len(self.items)})' + def refresh(self): pass @@ -159,7 +173,7 @@ class ExtraNetworksPage: def link_preview(self, filename): quoted_filename = urllib.parse.quote(filename.replace('\\', '/')) mtime = os.path.getmtime(filename) if os.path.exists(filename) else 0 - preview = f"./sd_extra_networks/thumb?filename={quoted_filename}&mtime={mtime}" + preview = f"/sdapi/v1/network/thumb?filename={quoted_filename}&mtime={mtime}" return preview def create_thumb(self): diff --git a/modules/ui_extra_networks_lora.py b/modules/ui_extra_networks_lora.py index 194f16b41..597f32941 100644 --- a/modules/ui_extra_networks_lora.py +++ b/modules/ui_extra_networks_lora.py @@ -88,6 +88,7 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): item = { "type": 'Lora', "name": name, + "alias": os.path.splitext(os.path.basename(l.filename))[0], "filename": l.filename, "hash": l.shorthash, "prompt": json.dumps(f" "), diff --git a/modules/ui_extra_networks_styles.py b/modules/ui_extra_networks_styles.py index 29ac541a8..3cd17b07a 100644 --- a/modules/ui_extra_networks_styles.py +++ b/modules/ui_extra_networks_styles.py @@ -80,6 +80,7 @@ class ExtraNetworksPageStyles(ui_extra_networks.ExtraNetworksPage): "type": 'Style', "name": name, "title": k, + "alias": os.path.splitext(os.path.basename(style.filename))[0], "filename": style.filename, "preview": style.preview if getattr(style, 'preview', None) is not None and style.preview.startswith('data:') else None, "description": style.description if getattr(style, 'description', None) is not None and len(style.description) > 0 else txt, diff --git a/modules/ui_extra_networks_textual_inversion.py b/modules/ui_extra_networks_textual_inversion.py index 55b5b01d1..92c8acc92 100644 --- a/modules/ui_extra_networks_textual_inversion.py +++ b/modules/ui_extra_networks_textual_inversion.py @@ -27,6 +27,7 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage): "type": 'Embedding', "name": name, "filename": embedding.filename, + "alias": os.path.splitext(os.path.basename(embedding.filename))[0], "prompt": json.dumps(f" {os.path.splitext(embedding.name)[0]}"), "tags": tags, "mtime": os.path.getmtime(embedding.filename), diff --git a/modules/ui_extra_networks_vae.py b/modules/ui_extra_networks_vae.py index de18b5d26..ed9ddadc3 100644 --- a/modules/ui_extra_networks_vae.py +++ b/modules/ui_extra_networks_vae.py @@ -17,6 +17,7 @@ class ExtraNetworksPageVAEs(ui_extra_networks.ExtraNetworksPage): record = { "type": 'VAE', "name": name, + "alias": os.path.splitext(os.path.basename(filename))[0], "title": name, "filename": filename, "hash": hashes.sha256_from_cache(filename, f"vae/{filename}"), From 3e0fee01f90b559ef6999f4466af3ab425246914 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 11:06:37 -0400 Subject: [PATCH 072/141] cleanup Signed-off-by: Vladimir Mandic --- modules/api/middleware.py | 3 ++- modules/memstats.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/api/middleware.py b/modules/api/middleware.py index be136b516..ed433170f 100644 --- a/modules/api/middleware.py +++ b/modules/api/middleware.py @@ -15,6 +15,7 @@ import modules.errors as errors errors.install() +ignore_endpoints = ['/sdapi/v1/log', '/sdapi/v1/browser', '/sdapi/v1/gpu', '/sdapi/v1/network/thumb'] def setup_middleware(app: FastAPI, cmd_opts): @@ -43,7 +44,7 @@ def setup_middleware(app: FastAPI, cmd_opts): endpoint = req.scope.get('path', 'err') token = req.cookies.get("access-token") or req.cookies.get("access-token-unsecure") if (cmd_opts.api_log) and endpoint.startswith('/sdapi'): - if ('/sdapi/v1/log' in endpoint) or ('/sdapi/v1/browser' in endpoint) or ('/sdapi/v1/gpu' in endpoint): + if any([endpoint.startswith(x) for x in ignore_endpoints]): return res log.info('API user={user} code={code} {prot}/{ver} {method} {endpoint} {cli} {duration}'.format( # pylint: disable=consider-using-f-string, logging-format-interpolation user = app.tokens.get(token) if hasattr(app, 'tokens') else None, diff --git a/modules/memstats.py b/modules/memstats.py index a2b8c1133..71dc2cb63 100644 --- a/modules/memstats.py +++ b/modules/memstats.py @@ -57,6 +57,8 @@ def ram_stats(): ram['used'] = gb(res.rss) ram['free'] = ram['total'] - ram['used'] except Exception as e: + ram['total'] = 0 + ram['used'] = 0 ram['error'] = str(e) if not fail_once: shared.log.error(f'RAM stats: {e}') @@ -79,6 +81,8 @@ def gpu_stats(): gpu['retries'] = stats.get('num_alloc_retries', 0) gpu['oom'] = stats.get('num_ooms', 0) except Exception as e: + gpu['total'] = 0 + gpu['used'] = 0 gpu['error'] = str(e) if not fail_once: shared.log.error(f'GPU stats: {e}') From 3f45c4e570e8336007d364c9b339d9c4bb8b8ee1 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Sun, 10 Aug 2025 19:31:34 +0300 Subject: [PATCH 073/141] Cleanup SDNQ and skip transpose on packed int8 matmul --- modules/sdnq/__init__.py | 3 +-- modules/sdnq/dequantizer.py | 16 ++++++---------- modules/sdnq/layers/linear/linear_int8.py | 2 +- modules/sdnq/packed_int.py | 15 ++++++++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index 0eff91a67..f5339288a 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -160,8 +160,7 @@ def sdnq_quantize_layer(layer, weights_dtype="int8", torch_dtype=None, group_siz if use_quantized_matmul: scale = scale.transpose(0,1) - if dtype_dict[weights_dtype]["num_bits"] == 8: - layer.weight.data = layer.weight.transpose(0,1) + layer.weight.data = layer.weight.transpose(0,1) if not dtype_dict[weights_dtype]["is_integer"]: stride = layer.weight.stride() if stride[0] > stride[1] and stride[1] == 1: diff --git a/modules/sdnq/dequantizer.py b/modules/sdnq/dequantizer.py index 0a0881117..1c9093703 100644 --- a/modules/sdnq/dequantizer.py +++ b/modules/sdnq/dequantizer.py @@ -4,7 +4,7 @@ import torch from modules import shared from .common import dtype_dict, use_torch_compile -from .packed_int import pack_int_symetric, unpack_int_symetric, packed_int_function_dict +from .packed_int import pack_int_symetric, unpack_int_symetric, pack_int_asymetric, unpack_int_asymetric def dequantize_asymmetric(weight: torch.ByteTensor, scale: torch.FloatTensor, zero_point: torch.FloatTensor, dtype: torch.dtype, result_shape: torch.Size) -> torch.FloatTensor: @@ -15,10 +15,9 @@ def dequantize_asymmetric(weight: torch.ByteTensor, scale: torch.FloatTensor, ze def dequantize_symmetric(weight: torch.CharTensor, scale: torch.FloatTensor, dtype: torch.dtype, result_shape: torch.Size, skip_quantized_matmul: bool = False) -> torch.FloatTensor: + result = weight.to(dtype=scale.dtype).mul_(scale).to(dtype=dtype) if skip_quantized_matmul: - result = weight.transpose(0,1).to(dtype=scale.dtype).mul_(scale.transpose(0,1)).to(dtype=dtype) - else: - result = weight.to(dtype=scale.dtype).mul_(scale).to(dtype=dtype) + result = result.t() if result_shape is not None: result = result.reshape(result_shape) return result @@ -29,14 +28,11 @@ def dequantize_symmetric_with_bias(weight: torch.CharTensor, scale: torch.FloatT def dequantize_packed_int_asymmetric(weight: torch.ByteTensor, scale: torch.FloatTensor, zero_point: torch.FloatTensor, shape: torch.Size, dtype: torch.dtype, result_shape: torch.Size, weights_dtype: str) -> torch.FloatTensor: - return dequantize_asymmetric(packed_int_function_dict[weights_dtype]["unpack"](weight, shape), scale, zero_point, dtype, result_shape) + return dequantize_asymmetric(unpack_int_asymetric(weight, shape, weights_dtype), scale, zero_point, dtype, result_shape) def dequantize_packed_int_symmetric(weight: torch.ByteTensor, scale: torch.FloatTensor, shape: torch.Size, dtype: torch.dtype, result_shape: torch.Size, weights_dtype: str, skip_quantized_matmul: bool = False) -> torch.FloatTensor: - if skip_quantized_matmul: - return dequantize_symmetric(unpack_int_symetric(weight, shape, weights_dtype, dtype=scale.dtype), scale.transpose(0,1), dtype, result_shape) - else: - return dequantize_symmetric(unpack_int_symetric(weight, shape, weights_dtype, dtype=scale.dtype), scale, dtype, result_shape) + return dequantize_symmetric(unpack_int_symetric(weight, shape, weights_dtype, dtype=scale.dtype), scale, dtype, result_shape, skip_quantized_matmul=skip_quantized_matmul) class AsymmetricWeightsDequantizer(torch.nn.Module): @@ -115,7 +111,7 @@ class PackedINTAsymmetricWeightsDequantizer(torch.nn.Module): self.register_buffer("zero_point", zero_point) def pack_weight(self, weight: torch.Tensor) -> torch.Tensor: - return packed_int_function_dict[self.weights_dtype]["pack"](weight.to(dtype=dtype_dict[self.weights_dtype]["torch_dtype"])) + return pack_int_asymetric(weight, self.weights_dtype) def forward(self, weight, **kwargs): # pylint: disable=unused-argument return dequantize_packed_int_asymmetric_compiled(weight, self.scale, self.zero_point, self.quantized_weight_shape, self.result_dtype, self.result_shape, self.weights_dtype) diff --git a/modules/sdnq/layers/linear/linear_int8.py b/modules/sdnq/layers/linear/linear_int8.py index dfc5288a0..d02c3d7ab 100644 --- a/modules/sdnq/layers/linear/linear_int8.py +++ b/modules/sdnq/layers/linear/linear_int8.py @@ -28,7 +28,7 @@ def int8_matmul( weights_dtype: str, ) -> torch.FloatTensor: if quantized_weight_shape is not None: - weight = unpack_int_symetric(weight, quantized_weight_shape, weights_dtype, dtype=torch.int8, transpose=True) + weight = unpack_int_symetric(weight, quantized_weight_shape, weights_dtype, dtype=torch.int8) return_dtype = input.dtype output_shape = list(input.shape) output_shape[-1] = weight.shape[-1] diff --git a/modules/sdnq/packed_int.py b/modules/sdnq/packed_int.py index 84931d159..d946ef4ba 100644 --- a/modules/sdnq/packed_int.py +++ b/modules/sdnq/packed_int.py @@ -11,13 +11,18 @@ def pack_int_symetric(tensor: torch.CharTensor, weights_dtype: str) -> torch.Byt return packed_int_function_dict[weights_dtype]["pack"](tensor.sub_(dtype_dict[weights_dtype]["min"]).to(dtype=dtype_dict[weights_dtype]["storage_dtype"])) -def unpack_int_symetric(packed_tensor: torch.ByteTensor, shape: torch.Size, weights_dtype: str, dtype: Optional[torch.dtype] = None, transpose: Optional[bool] = False) -> torch.CharTensor: +def pack_int_asymetric(tensor: torch.CharTensor, weights_dtype: str) -> torch.ByteTensor: + return packed_int_function_dict[weights_dtype]["pack"](tensor.to(dtype=dtype_dict[weights_dtype]["storage_dtype"])) + + +def unpack_int_symetric(packed_tensor: torch.ByteTensor, shape: torch.Size, weights_dtype: str, dtype: Optional[torch.dtype] = None) -> torch.CharTensor: if dtype is None: dtype = dtype_dict[weights_dtype]["torch_dtype"] - result = packed_int_function_dict[weights_dtype]["unpack"](packed_tensor, shape).to(dtype=dtype).add_(dtype_dict[weights_dtype]["min"]) - if transpose: - result = result.transpose(0,1) - return result + return packed_int_function_dict[weights_dtype]["unpack"](packed_tensor, shape).to(dtype=dtype).add_(dtype_dict[weights_dtype]["min"]) + + +def unpack_int_asymetric(packed_tensor: torch.ByteTensor, shape: torch.Size, weights_dtype: str) -> torch.CharTensor: + return packed_int_function_dict[weights_dtype]["unpack"](packed_tensor, shape) def pack_uint7(tensor: torch.ByteTensor) -> torch.ByteTensor: From abddac23d9c79fea1684440bba6d2274e6a93fcc Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 15:40:11 -0400 Subject: [PATCH 074/141] add qwen-lightning Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 14 +++-- html/reference.json | 7 +++ modules/ui_extra_networks.py | 6 +- pipelines/chroma/convert_chroma.py | 94 ------------------------------ 4 files changed, 18 insertions(+), 103 deletions(-) delete mode 100644 pipelines/chroma/convert_chroma.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 21a3b93fb..04770ea8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Highlights for 2025-08-10 -Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) and [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) +Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) (plus *Lightning* variant) and [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) and [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) Plus continuing with major **UI** work, we have new embedded **Docs/Wiki** search, redesigned real-time **hints**, built-in **GPU monitor**, **CivitAI** integration and more! On the compute side, new profiles for high-vram GPUs, offloading improvements and support for new `torch` release @@ -16,11 +16,13 @@ And (*as always*) many bugfixes and improvements to existing features! - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) - new image foundational model with 20B params DiT and using Qwen2.5-VL-7B as the text-encoder! - available for text-to-image workflows, image-editing workflows will follow soon - *note*: this model is almost 2x the size of Flux, quantization and offloading are highly recommended! - recommended params: *steps=50, attention-guidance=4* + new image foundational model with *20B* params DiT and using *Qwen2.5-VL-7B* as the text-encoder! available via *networks -> models -> reference* + *note*: this model is almost 2x the size of Flux, quantization and offloading are highly recommended! + *note* qwen-image supports text-to-image workflows as image-editing model is not yet available + *recommended* params: *steps=50, attention-guidance=4* + also available is pre-packaged [Qwen-Lightning](https://huggingface.co/vladmandic/Qwen-Lightning) + which is an unofficial merge of [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) with [Qwen-Lightning-LoRA](https://github.com/ModelTC/Qwen-Image-Lightning/) to improve quality and allow for generating in 8-steps! - [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) new 12B base model compatible with FLUX.1-Dev from *Black Forest Labs* with opinionated aesthetics and aesthetic preferences in mind available via *networks -> models -> reference* @@ -38,7 +40,7 @@ And (*as always*) many bugfixes and improvements to existing features! optimized support with granular guidance control will follow soon **Torch** - Set default to `torch==2.8.0` for *CUDA, ROCm and OpenVINO* - - Add support for `torch==2.9.0` + - Add support for `torch==2.9.0-nightly` - **UI** - new embedded docs/wiki search! **Docs** search: fully-local and works in real-time on all document pages diff --git a/html/reference.json b/html/reference.json index e29debb53..63cdbbdda 100644 --- a/html/reference.json +++ b/html/reference.json @@ -169,6 +169,13 @@ "skip": true, "extras": "" }, + "Qwen-Lightning": { + "path": "vladmandic/Qwen-Lightning", + "preview": "Qwen--Qwen-Image.jpg", + "desc": " Qwen-Lightning is step-distilled from Qwen-Image to allow for generation in 8 steps.", + "skip": true, + "extras": "steps: 8" + }, "Ostris Flex.2 Preview": { "path": "ostris/Flex.2-preview", diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 8e9b5afdc..33e63b85b 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -348,9 +348,9 @@ class ExtraNetworksPage: "color": random_bright_color(), "reference": "reference" if 'Reference' in item.get('name', '') else "", } - alias = item.get("alias", None) - if alias is not None: - args['title'] += f'\nAlias: {alias}' + # alias = item.get("alias", None) + # if alias is not None: + # args['title'] += f'\nAlias: {alias}' return self.card.format(**args) except Exception as e: shared.log.error(f'Networks: item error: page={tabname} item={item["name"]} {e}') diff --git a/pipelines/chroma/convert_chroma.py b/pipelines/chroma/convert_chroma.py deleted file mode 100644 index a560bac18..000000000 --- a/pipelines/chroma/convert_chroma.py +++ /dev/null @@ -1,94 +0,0 @@ - -import os -import torch -import transformers -import diffusers -import huggingface_hub as hf -from rich import print as rprint -from rich.traceback import install as install_traceback - - -convert = True -test = False -upload = True -input_files = [ - 'chroma-unlocked-v50.safetensors', - 'chroma-unlocked-v50-annealed.safetensors', -] -input_folder = '/mnt/models/UNET' -output_folder = '/mnt/models/Diffusers' -cache_dir = '/mnt/models/huggingface' -hf_token = '' -dtype = torch.bfloat16 -device = torch.device('cuda') - - -rprint('starting chroma conversion') -install_traceback(show_locals=False) -rprint(f'torch={torch.__version__} diffusers={diffusers.__version__} transformers={transformers.__version__}') -for input_file in input_files: - input_basename = os.path.splitext(input_file)[0] - input_model = os.path.join(input_folder, input_file) - output_model = os.path.join(output_folder, input_basename) - - if convert: - rprint(f'load transformer: {input_model}') - transformer = diffusers.ChromaTransformer2DModel.from_single_file( - input_model, - torch_dtype=dtype, - cache_dir=cache_dir, - ).to(device) - - rprint('load text-encoder') - text_encoder = transformers.T5EncoderModel.from_pretrained( - "black-forest-labs/FLUX.1-schnell", - subfolder="text_encoder_2", - torch_dtype=dtype, - cache_dir=cache_dir, - ).to(device) - - rprint('load tokenizer') - tokenizer = transformers.T5Tokenizer.from_pretrained( - "black-forest-labs/FLUX.1-schnell", - subfolder="tokenizer_2", - cache_dir=cache_dir, - ) - - rprint('load pipeline') - pipe = diffusers.ChromaPipeline.from_pretrained( - "black-forest-labs/FLUX.1-dev", - transformer=transformer, - text_encoder=text_encoder, - tokenizer=tokenizer, - torch_dtype=dtype, - cache_dir=cache_dir, - ).to(device) - - - rprint(f'save pipeline: {output_model}') - pipe.save_pretrained( - output_model, - ) - - if test: - rprint('test load') - pipe = diffusers.ChromaPipeline.from_pretrained( - output_model, - torch_dtype=dtype, - cache_dir=cache_dir, - ) - - if upload: - rprint('hf login') - hf.logout() - hf.login(token=hf_token, add_to_git_credential=False, write_permission=True) - rprint('upload model') - pipe.push_to_hub( - input_basename, - private=False, - token=hf_token, - ) - - pipe = None - -rprint('done') From dc7b25d387c4167a34a05a71b684a33273ffec8c Mon Sep 17 00:00:00 2001 From: Disty0 Date: Mon, 11 Aug 2025 14:50:13 +0300 Subject: [PATCH 075/141] Cleanup SDNQ and add SDNQ_USE_TENSORWISE_FP8_MATMUL env var --- modules/sdnq/common.py | 10 ++++------ modules/sdnq/dequantizer.py | 16 ++++------------ modules/sdnq/layers/conv/conv_fp8.py | 5 +---- modules/sdnq/layers/conv/conv_fp8_tensorwise.py | 5 +---- modules/sdnq/layers/conv/conv_int8.py | 5 +---- modules/sdnq/layers/linear/linear_fp8.py | 5 +---- .../sdnq/layers/linear/linear_fp8_tensorwise.py | 5 +---- modules/sdnq/layers/linear/linear_int8.py | 5 +---- 8 files changed, 14 insertions(+), 42 deletions(-) diff --git a/modules/sdnq/common.py b/modules/sdnq/common.py index 07ae0671b..d3dc3e3b5 100644 --- a/modules/sdnq/common.py +++ b/modules/sdnq/common.py @@ -1,5 +1,6 @@ # pylint: disable=redefined-builtin,no-member,protected-access +import os import torch from modules import devices, shared @@ -31,7 +32,7 @@ if hasattr(torch, "float8_e5m2fnuz"): dtype_dict["float8_e5m2fnuz"] = {"min": -57344, "max": 57344, "num_bits": 8, "target_dtype": "fp8", "torch_dtype": torch.float8_e5m2fnuz, "storage_dtype": torch.float8_e5m2fnuz, "is_unsigned": False, "is_integer": False} use_torch_compile = shared.opts.sdnq_dequantize_compile # this setting requires a full restart of the webui to apply -use_tensorwise_fp8_matmul = True # row-wise FP8 only exist on H100 hardware, sdnq will use software row-wise with tensorwise hardware with this setting +use_tensorwise_fp8_matmul = os.environ.get('SDNQ_USE_TENSORWISE_FP8_MATMUL', "1").lower() not in {"0", "false", "no"} # row-wise FP8 only exist on H100 hardware, sdnq will use software row-wise with tensorwise hardware with this setting quantized_matmul_dtypes = ("int8", "int7", "int6", "int5", "int4", "int3", "int2", "float8_e4m3fn", "float8_e5m2") if devices.backend in {"cpu", "openvino"}: @@ -43,8 +44,5 @@ conv_transpose_types = ("ConvTranspose1d", "ConvTranspose2d", "ConvTranspose3d") allowed_types = linear_types + conv_types + conv_transpose_types if use_torch_compile: - try: - torch._dynamo.config.cache_size_limit = max(8192, torch._dynamo.config.cache_size_limit) - torch._dynamo.config.accumulated_recompile_limit = max(8192, torch._dynamo.config.accumulated_recompile_limit) - except Exception as e: - shared.log.warning(f"Quantization: type=sdnq Failed to increase the cache size for torch.compile: {e}") + torch._dynamo.config.cache_size_limit = max(8192, torch._dynamo.config.cache_size_limit) + torch._dynamo.config.accumulated_recompile_limit = max(8192, torch._dynamo.config.accumulated_recompile_limit) diff --git a/modules/sdnq/dequantizer.py b/modules/sdnq/dequantizer.py index 1c9093703..76e830f59 100644 --- a/modules/sdnq/dequantizer.py +++ b/modules/sdnq/dequantizer.py @@ -1,7 +1,6 @@ # pylint: disable=redefined-builtin,no-member,protected-access import torch -from modules import shared from .common import dtype_dict, use_torch_compile from .packed_int import pack_int_symetric, unpack_int_symetric, pack_int_asymetric, unpack_int_asymetric @@ -170,17 +169,10 @@ dequantizer_dict = { if use_torch_compile: - try: - dequantize_asymmetric_compiled = torch.compile(dequantize_asymmetric, fullgraph=True, dynamic=False) - dequantize_symmetric_compiled = torch.compile(dequantize_symmetric, fullgraph=True, dynamic=False) - dequantize_packed_int_asymmetric_compiled = torch.compile(dequantize_packed_int_asymmetric, fullgraph=True, dynamic=False) - dequantize_packed_int_symmetric_compiled = torch.compile(dequantize_packed_int_symmetric, fullgraph=True, dynamic=False) - except Exception as e: - shared.log.warning(f"Quantization: type=sdnq Dequantize using torch.compile is not available: {e}") - dequantize_asymmetric_compiled = dequantize_asymmetric - dequantize_symmetric_compiled = dequantize_symmetric - dequantize_packed_int_asymmetric_compiled = dequantize_packed_int_asymmetric - dequantize_packed_int_symmetric_compiled = dequantize_packed_int_symmetric + dequantize_asymmetric_compiled = torch.compile(dequantize_asymmetric, fullgraph=True, dynamic=False) + dequantize_symmetric_compiled = torch.compile(dequantize_symmetric, fullgraph=True, dynamic=False) + dequantize_packed_int_asymmetric_compiled = torch.compile(dequantize_packed_int_asymmetric, fullgraph=True, dynamic=False) + dequantize_packed_int_symmetric_compiled = torch.compile(dequantize_packed_int_symmetric, fullgraph=True, dynamic=False) else: dequantize_asymmetric_compiled = dequantize_asymmetric dequantize_symmetric_compiled = dequantize_symmetric diff --git a/modules/sdnq/layers/conv/conv_fp8.py b/modules/sdnq/layers/conv/conv_fp8.py index 1f5a50922..cda2e625f 100644 --- a/modules/sdnq/layers/conv/conv_fp8.py +++ b/modules/sdnq/layers/conv/conv_fp8.py @@ -65,7 +65,4 @@ def quantized_conv_forward_fp8_matmul(self, input) -> torch.FloatTensor: if use_torch_compile: - try: - conv_fp8_matmul = torch.compile(conv_fp8_matmul, fullgraph=True, dynamic=False) - except Exception: - pass + conv_fp8_matmul = torch.compile(conv_fp8_matmul, fullgraph=True, dynamic=False) diff --git a/modules/sdnq/layers/conv/conv_fp8_tensorwise.py b/modules/sdnq/layers/conv/conv_fp8_tensorwise.py index 3c5cfa56d..bfc813ea1 100644 --- a/modules/sdnq/layers/conv/conv_fp8_tensorwise.py +++ b/modules/sdnq/layers/conv/conv_fp8_tensorwise.py @@ -64,7 +64,4 @@ def quantized_conv_forward_fp8_matmul_tensorwise(self, input) -> torch.FloatTens if use_torch_compile: - try: - conv_fp8_matmul_tensorwise = torch.compile(conv_fp8_matmul_tensorwise, fullgraph=True, dynamic=False) - except Exception: - pass + conv_fp8_matmul_tensorwise = torch.compile(conv_fp8_matmul_tensorwise, fullgraph=True, dynamic=False) diff --git a/modules/sdnq/layers/conv/conv_int8.py b/modules/sdnq/layers/conv/conv_int8.py index ffda82e2f..c899116a6 100644 --- a/modules/sdnq/layers/conv/conv_int8.py +++ b/modules/sdnq/layers/conv/conv_int8.py @@ -70,7 +70,4 @@ def quantized_conv_forward_int8_matmul(self, input) -> torch.FloatTensor: if use_torch_compile: - try: - conv_int8_matmul = torch.compile(conv_int8_matmul, fullgraph=True, dynamic=False) - except Exception: - pass + conv_int8_matmul = torch.compile(conv_int8_matmul, fullgraph=True, dynamic=False) diff --git a/modules/sdnq/layers/linear/linear_fp8.py b/modules/sdnq/layers/linear/linear_fp8.py index 3d2f4059f..f6da5c2ef 100644 --- a/modules/sdnq/layers/linear/linear_fp8.py +++ b/modules/sdnq/layers/linear/linear_fp8.py @@ -34,7 +34,4 @@ def quantized_linear_forward_fp8_matmul(self, input: torch.FloatTensor) -> torch if use_torch_compile: - try: - fp8_matmul = torch.compile(fp8_matmul, fullgraph=True, dynamic=False) - except Exception: - pass + fp8_matmul = torch.compile(fp8_matmul, fullgraph=True, dynamic=False) diff --git a/modules/sdnq/layers/linear/linear_fp8_tensorwise.py b/modules/sdnq/layers/linear/linear_fp8_tensorwise.py index 1cdf5ced4..3fdecaf57 100644 --- a/modules/sdnq/layers/linear/linear_fp8_tensorwise.py +++ b/modules/sdnq/layers/linear/linear_fp8_tensorwise.py @@ -42,7 +42,4 @@ def quantized_linear_forward_fp8_matmul_tensorwise(self, input: torch.FloatTenso if use_torch_compile: - try: - fp8_matmul_tensorwise = torch.compile(fp8_matmul_tensorwise, fullgraph=True, dynamic=False) - except Exception: - pass + fp8_matmul_tensorwise = torch.compile(fp8_matmul_tensorwise, fullgraph=True, dynamic=False) diff --git a/modules/sdnq/layers/linear/linear_int8.py b/modules/sdnq/layers/linear/linear_int8.py index d02c3d7ab..3c0184056 100644 --- a/modules/sdnq/layers/linear/linear_int8.py +++ b/modules/sdnq/layers/linear/linear_int8.py @@ -46,7 +46,4 @@ def quantized_linear_forward_int8_matmul(self, input: torch.FloatTensor) -> torc if use_torch_compile: - try: - int8_matmul = torch.compile(int8_matmul, fullgraph=True, dynamic=False) - except Exception: - pass + int8_matmul = torch.compile(int8_matmul, fullgraph=True, dynamic=False) From afb3a5a06d17b120ef79e4b5c530fed4c505f5ed Mon Sep 17 00:00:00 2001 From: Disty0 Date: Mon, 11 Aug 2025 15:07:02 +0300 Subject: [PATCH 076/141] SDNQ move non_blocking to quant config --- modules/lora/lora_apply.py | 1 + modules/model_quant.py | 2 ++ modules/sdnq/__init__.py | 17 +++++++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/modules/lora/lora_apply.py b/modules/lora/lora_apply.py index 2fcea174c..0343d7b14 100644 --- a/modules/lora/lora_apply.py +++ b/modules/lora/lora_apply.py @@ -158,6 +158,7 @@ def network_add_weights(self: Union[torch.nn.Conv2d, torch.nn.Linear, torch.nn.G use_quantized_matmul=shared.opts.sdnq_use_quantized_matmul, use_quantized_matmul_conv=shared.opts.sdnq_use_quantized_matmul_conv, dequantize_fp32=shared.opts.sdnq_dequantize_fp32, + non_blocking=False, quantization_device=devices.device, return_device=device, param_name=getattr(self, 'network_layer_name', None), diff --git a/modules/model_quant.py b/modules/model_quant.py index 5513bb127..a2cfbecbf 100644 --- a/modules/model_quant.py +++ b/modules/model_quant.py @@ -146,6 +146,7 @@ def create_sdnq_config(kwargs = None, allow: bool = True, module: str = 'Model', use_quantized_matmul=shared.opts.sdnq_use_quantized_matmul, use_quantized_matmul_conv=shared.opts.sdnq_use_quantized_matmul_conv, dequantize_fp32=shared.opts.sdnq_dequantize_fp32, + non_blocking=shared.opts.diffusers_offload_nonblocking, quantization_device=quantization_device, return_device=return_device, modules_to_not_convert=modules_to_not_convert, @@ -418,6 +419,7 @@ def sdnq_quantize_model(model, op=None, sd_model=None, do_gc: bool = True, weigh use_quantized_matmul=shared.opts.sdnq_use_quantized_matmul, use_quantized_matmul_conv=shared.opts.sdnq_use_quantized_matmul_conv, dequantize_fp32=shared.opts.sdnq_dequantize_fp32, + non_blocking=shared.opts.diffusers_offload_nonblocking, quantization_device=quantization_device, return_device=return_device, param_name=op, diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index f5339288a..b6e6deb52 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -46,7 +46,7 @@ def quantize_weight(weight: torch.FloatTensor, reduction_axes: Union[int, List[i @devices.inference_context() -def sdnq_quantize_layer(layer, weights_dtype="int8", torch_dtype=None, group_size=0, quant_conv=False, use_quantized_matmul=False, use_quantized_matmul_conv=False, dequantize_fp32=False, quantization_device=None, return_device=None, param_name=None): # pylint: disable=unused-argument +def sdnq_quantize_layer(layer, weights_dtype="int8", torch_dtype=None, group_size=0, quant_conv=False, use_quantized_matmul=False, use_quantized_matmul_conv=False, dequantize_fp32=False, non_blocking=False, quantization_device=None, return_device=None, param_name=None): # pylint: disable=unused-argument layer_class_name = layer.__class__.__name__ if layer_class_name in allowed_types: is_conv_type = False @@ -148,7 +148,7 @@ def sdnq_quantize_layer(layer, weights_dtype="int8", torch_dtype=None, group_siz if return_device is None: return_device = layer.weight.device if quantization_device is not None: - layer.weight.data = layer.weight.to(quantization_device, non_blocking=shared.opts.diffusers_offload_nonblocking) + layer.weight.data = layer.weight.to(quantization_device, non_blocking=non_blocking) if layer.weight.dtype != torch.float32: layer.weight.data = layer.weight.to(dtype=torch.float32) @@ -178,15 +178,15 @@ def sdnq_quantize_layer(layer, weights_dtype="int8", torch_dtype=None, group_siz weights_dtype=weights_dtype, use_quantized_matmul=use_quantized_matmul, ) - layer.weight.data = layer.sdnq_dequantizer.pack_weight(layer.weight).to(return_device, non_blocking=shared.opts.diffusers_offload_nonblocking) - layer.sdnq_dequantizer = layer.sdnq_dequantizer.to(return_device, non_blocking=shared.opts.diffusers_offload_nonblocking) + layer.weight.data = layer.sdnq_dequantizer.pack_weight(layer.weight).to(return_device, non_blocking=non_blocking) + layer.sdnq_dequantizer = layer.sdnq_dequantizer.to(return_device, non_blocking=non_blocking) layer.forward = get_forward_func(layer_class_name, use_quantized_matmul, dtype_dict[weights_dtype]["is_integer"], use_tensorwise_fp8_matmul) layer.forward = layer.forward.__get__(layer, layer.__class__) return layer -def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_size=0, quant_conv=False, use_quantized_matmul=False, use_quantized_matmul_conv=False, dequantize_fp32=False, quantization_device=None, return_device=None, param_name=None, modules_to_not_convert: List[str] = []): # pylint: disable=unused-argument +def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_size=0, quant_conv=False, use_quantized_matmul=False, use_quantized_matmul_conv=False, dequantize_fp32=False, non_blocking=False, quantization_device=None, return_device=None, param_name=None, modules_to_not_convert: List[str] = []): # pylint: disable=unused-argument has_children = list(model.children()) if not has_children: return model @@ -203,6 +203,7 @@ def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_si use_quantized_matmul=use_quantized_matmul, use_quantized_matmul_conv=use_quantized_matmul_conv, dequantize_fp32=dequantize_fp32, + non_blocking=non_blocking, quantization_device=quantization_device, return_device=return_device, param_name=module_param_name, @@ -216,6 +217,7 @@ def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_si use_quantized_matmul=use_quantized_matmul, use_quantized_matmul_conv=use_quantized_matmul_conv, dequantize_fp32=dequantize_fp32, + non_blocking=non_blocking, quantization_device=quantization_device, return_device=return_device, param_name=module_param_name, @@ -289,7 +291,7 @@ class SDNQQuantizer(DiffusersQuantizer): if param_value.dtype == torch.float32 and devices.same_device(param_value.device, target_device): param_value = param_value.clone() else: - param_value = param_value.to(target_device, non_blocking=shared.opts.diffusers_offload_nonblocking).to(dtype=torch.float32) + param_value = param_value.to(target_device, non_blocking=self.quantization_config.non_blocking).to(dtype=torch.float32) layer, _ = get_module_from_name(model, param_name) layer.weight = torch.nn.Parameter(param_value, requires_grad=False) @@ -302,6 +304,7 @@ class SDNQQuantizer(DiffusersQuantizer): use_quantized_matmul=self.quantization_config.use_quantized_matmul, use_quantized_matmul_conv=self.quantization_config.use_quantized_matmul_conv, dequantize_fp32=self.quantization_config.dequantize_fp32, + non_blocking=self.quantization_config.non_blocking, quantization_device=None, return_device=return_device, param_name=param_name, @@ -426,6 +429,7 @@ class SDNQConfig(QuantizationConfigMixin): use_quantized_matmul: bool = False, use_quantized_matmul_conv: bool = False, dequantize_fp32: bool = False, + non_blocking: bool = False, quantization_device: Optional[torch.device] = None, return_device: Optional[torch.device] = None, modules_to_not_convert: Optional[List[str]] = None, @@ -438,6 +442,7 @@ class SDNQConfig(QuantizationConfigMixin): self.use_quantized_matmul = use_quantized_matmul self.use_quantized_matmul_conv = use_quantized_matmul_conv self.dequantize_fp32 = dequantize_fp32 + self.non_blocking = non_blocking self.quantization_device = quantization_device self.return_device = return_device self.modules_to_not_convert = modules_to_not_convert From f45e3342e6fb6f05d5a90033135e4140cb741cfc Mon Sep 17 00:00:00 2001 From: Disty0 Date: Mon, 11 Aug 2025 15:11:29 +0300 Subject: [PATCH 077/141] Cleanup --- modules/sdnq/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index b6e6deb52..e1c8396f9 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -412,6 +412,8 @@ class SDNQConfig(QuantizationConfigMixin): Same as use_quantized_matmul_conv but for the convolutional layers with UNets like SDXL. dequantize_fp32 (`bool`, *optional*, defaults to `False`): Enabling this option will use FP32 on the dequantization step. + non_blocking (`bool`, *optional*, defaults to `False`): + Enabling this option will use non blocking ops when moving layers between the quantization device and the return device. quantization_device (`torch.device`, *optional*, defaults to `None`): Used to set which device will be used for the quantization calculation on model load. return_device (`torch.device`, *optional*, defaults to `None`): From 2a85c0568914efc70f3f2b5acd2e757f0f1d13af Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 20:49:58 -0400 Subject: [PATCH 078/141] refactor pipeline loaders to generic methods and introduce `te_shared_t5` option Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 6 + cli/test-all-models.py | 102 ++++++------ modules/api/loras.py | 6 +- modules/api/middleware.py | 2 +- modules/memstats.py | 4 +- modules/sd_models.py | 9 +- modules/shared.py | 1 + pipelines/generic.py | 132 ++++++++++++++++ pipelines/model_auraflow.py | 29 ++-- pipelines/model_bria.py | 71 ++------- pipelines/model_chroma.py | 281 ++-------------------------------- pipelines/model_cogview.py | 66 +++----- pipelines/model_cosmos.py | 61 +------- pipelines/model_flex.py | 61 +------- pipelines/model_flite.py | 52 ++----- pipelines/model_flux.py | 2 +- pipelines/model_hidream.py | 65 +------- pipelines/model_pixart.py | 33 ++-- pipelines/model_qwen.py | 37 +---- pipelines/model_sd3.py | 134 +++------------- scripts/nudenet/imageguard.py | 2 +- scripts/nudenet/nudenet.py | 2 +- 22 files changed, 347 insertions(+), 811 deletions(-) create mode 100644 pipelines/generic.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 04770ea8c..9259d02ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,10 @@ And (*as always*) many bugfixes and improvements to existing features! - new `diffusers_offload_nonblocking` exerimental setting instructs torch to use non-blocking move operations when possible - **Features** + - new `T5: Use shared instance of text encoder` option + in *settings -> text encoder* + since a lot of new models use T5 text encoder, this option allows to share + the same instance across all models without duplicate downloads - **Wan** select which stage to run: *first/second/both* with configurable *boundary ration* when running both stages in settings -> model options - prompt parser allow explict `BOS` and `EOS` tokens in prompt @@ -97,6 +101,8 @@ And (*as always*) many bugfixes and improvements to existing features! - remove `api-only` cli option - **API** - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model +- **Refactor** + - new unified pipeline component loader in `pipelines/generic` - **Fixes** - refactor legacy processing loop - fix settings components mismatch diff --git a/cli/test-all-models.py b/cli/test-all-models.py index baf59f1c8..7115bc8f2 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -20,39 +20,47 @@ models = [ "sdxl-base-v10-vaefix", "tempest-by-vlad-0.1", "icbinpXL_v6", + "briaai/BRIA-3.2", + "Freepik/F-Lite", + "Freepik/F-Lite-Texture", + "ostris/Flex.2-preview", "stabilityai/stable-diffusion-3.5-medium", "stabilityai/stable-diffusion-3.5-large", + "fal/AuraFlow-v0.3", + "THUDM/CogView3-Plus-3B", + "THUDM/CogView4-6B", + "nvidia/Cosmos-Predict2-2B-Text2Image", + "nvidia/Cosmos-Predict2-14B-Text2Image", + "Qwen/Qwen-Image", + "Qwen/Qwen-Lightning", + "Shitao/OmniGen-v1-diffusers", + "OmniGen2/OmniGen2", + "HiDream-ai/HiDream-I1-Full", + "Kwai-Kolors/Kolors-diffusers", + "vladmandic/chroma-unlocked-v50", + "vladmandic/chroma-unlocked-v50-annealed", + "Alpha-VLLM/Lumina-Next-SFT-diffusers", + "Alpha-VLLM/Lumina-Image-2.0", + "MeissonFlow/Meissonic", + "Efficient-Large-Model/SANA1.5_1.6B_1024px_diffusers", + "Efficient-Large-Model/SANA1.5_4.8B_1024px_diffusers", + "PixArt-alpha/PixArt-XL-2-1024-MS", + "PixArt-alpha/PixArt-Sigma-XL-2-1024-MS", + "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", + "Wan-AI/Wan2.1-T2V-14B-Diffusers", + "stabilityai/stable-cascade", +] +models_tbd = [ "black-forest-labs/FLUX.1-dev", "black-forest-labs/FLUX.1-Kontext-dev", "black-forest-labs/FLUX.1-Krea-dev", - "vladmandic/chroma-unlocked-v50", - "vladmandic/chroma-unlocked-v50-annealed", - "Qwen/Qwen-Image", - "briaai/BRIA-3.2", - "stabilityai/stable-cascade", - "ostris/Flex.2-preview", - "OmniGen2/OmniGen2", - "Freepik/F-Lite", - "Freepik/F-Lite-Texture", - "HiDream-ai/HiDream-I1-Full", - "nvidia/Cosmos-Predict2-2B-Text2Image", - "nvidia/Cosmos-Predict2-14B-Text2Image", - "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", - "Wan-AI/Wan2.1-T2V-14B-Diffusers", - "Efficient-Large-Model/SANA1.5_1.6B_1024px_diffusers", - "Efficient-Large-Model/SANA1.5_4.8B_1024px_diffusers", - "fal/AuraFlow-v0.3", - "PixArt-alpha/PixArt-XL-2-1024-MS", - "PixArt-alpha/PixArt-Sigma-XL-2-1024-MS", - "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers", - "Alpha-VLLM/Lumina-Next-SFT-diffusers", - "Alpha-VLLM/Lumina-Image-2.0", - "Kwai-Kolors/Kolors-diffusers", - "THUDM/CogView4-6B", - "kandinsky-community/kandinsky-3", + "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers", # TODO + "kandinsky-community/kandinsky-3", # TODO ] styles = [ 'Fixed Astronaut', +] +styles_tbd = [ 'Fixed Bear', 'Fixed Steampunk City', 'Fixed Road sign', @@ -89,27 +97,29 @@ def generate(): # pylint: disable=redefined-outer-name model_name = pathvalidate.sanitize_filename(model, replacement_text='_') log.info(f'model: name="{model}" n={m+1}/{len(models)}') for s, style in enumerate(styles): - model_name = pathvalidate.sanitize_filename(model, replacement_text='_') - style_name = pathvalidate.sanitize_filename(style, replacement_text='_') - fn = os.path.join(output_folder, f'{model_name}__{style_name}.jpg') - if os.path.exists(fn): - continue - request(f'/sdapi/v1/checkpoint?sd_model_checkpoint={model}', method='POST') - loaded = request('/sdapi/v1/checkpoint', method='GET') - if not (model in loaded.get('checkpoint') or model in loaded.get('title') or model in loaded.get('name')): - log.error(f' model: error="{model}"') - continue - log.info(f' style: name="{style}" n={s+1}/{len(styles)} fn="{fn}"') - t0 = time.time() - data = request('/sdapi/v1/txt2img', { 'styles': [style] }) - t1 = time.time() - if 'images' in data and len(data['images']) > 0: - b64 = data['images'][0].split(',',1)[0] - image = Image.open(io.BytesIO(base64.b64decode(b64))) - info = data['info'] - log.info(f' image: size={image.size} time={t1-t0:.2f} info="{len(info)}" fn="{fn}"') - image.save(fn) - + try: + model_name = pathvalidate.sanitize_filename(model, replacement_text='_') + style_name = pathvalidate.sanitize_filename(style, replacement_text='_') + fn = os.path.join(output_folder, f'{model_name}__{style_name}.jpg') + if os.path.exists(fn): + continue + request(f'/sdapi/v1/checkpoint?sd_model_checkpoint={model}', method='POST') + loaded = request('/sdapi/v1/checkpoint', method='GET') + if not loaded or not (model in loaded.get('checkpoint') or model in loaded.get('title') or model in loaded.get('name')): + log.error(f' model: error="{model}"') + continue + log.info(f' style: name="{style}" n={s+1}/{len(styles)} fn="{fn}"') + t0 = time.time() + data = request('/sdapi/v1/txt2img', { 'styles': [style] }) + t1 = time.time() + if 'images' in data and len(data['images']) > 0: + b64 = data['images'][0].split(',',1)[0] + image = Image.open(io.BytesIO(base64.b64decode(b64))) + info = data['info'] + log.info(f' image: size={image.size} time={t1-t0:.2f} info="{len(info)}" fn="{fn}"') + image.save(fn) + except Exception as e: + log.error(f' model: error="{model}" style="{style}" exception="{e}"') if __name__ == "__main__": log.info('test-all-models') diff --git a/modules/api/loras.py b/modules/api/loras.py index c387bdaa0..8acc8f0cf 100644 --- a/modules/api/loras.py +++ b/modules/api/loras.py @@ -7,9 +7,9 @@ def get_lora(lora: str) -> dict: if lora not in lora_load.available_networks: raise HTTPException(status_code=404, detail=f"Lora '{lora}' not found") obj = lora_load.available_networks[lora] - obj.meta = obj.get_metadata() - obj.info = obj.get_info() - obj.desc = obj.get_desc() + # obj.meta = obj.get_metadata() + # obj.info = obj.get_info() + # obj.desc = obj.get_desc() return obj.__dict__ def get_loras(): diff --git a/modules/api/middleware.py b/modules/api/middleware.py index ed433170f..d276bd46b 100644 --- a/modules/api/middleware.py +++ b/modules/api/middleware.py @@ -44,7 +44,7 @@ def setup_middleware(app: FastAPI, cmd_opts): endpoint = req.scope.get('path', 'err') token = req.cookies.get("access-token") or req.cookies.get("access-token-unsecure") if (cmd_opts.api_log) and endpoint.startswith('/sdapi'): - if any([endpoint.startswith(x) for x in ignore_endpoints]): + if any([endpoint.startswith(x) for x in ignore_endpoints]): # noqa C419 # pylint: disable=use-a-generator return res log.info('API user={user} code={code} {prot}/{ver} {method} {endpoint} {cli} {duration}'.format( # pylint: disable=consider-using-f-string, logging-format-interpolation user = app.tokens.get(token) if hasattr(app, 'tokens') else None, diff --git a/modules/memstats.py b/modules/memstats.py index 71dc2cb63..1e2be27c8 100644 --- a/modules/memstats.py +++ b/modules/memstats.py @@ -55,7 +55,7 @@ def ram_stats(): ram_total = min(ram_total, get_docker_limit(), get_runpod_limit()) ram['total'] = gb(ram_total) ram['used'] = gb(res.rss) - ram['free'] = ram['total'] - ram['used'] + ram['free'] = round(ram['total'] - ram['used']) except Exception as e: ram['total'] = 0 ram['used'] = 0 @@ -97,7 +97,7 @@ def memory_stats(): mem['job'] = shared.state.job try: mem['gpu']['swap'] = round(mem['gpu']['active'] - mem['gpu']['used']) if mem['gpu']['active'] > mem['gpu']['used'] else 0 - except: + except Exception: mem['gpu']['swap'] = 0 return mem diff --git a/modules/sd_models.py b/modules/sd_models.py index d0c3423b1..553da62eb 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -10,7 +10,7 @@ import diffusers.loaders.single_file_utils import torch import huggingface_hub as hf from installer import log -from modules import timer, paths, shared, shared_state, shared_items, modelloader, devices, script_callbacks, sd_vae, sd_unet, errors, sd_models_compile, sd_hijack_accelerate, sd_detect, model_quant, sd_hijack_te +from modules import timer, paths, shared, shared_items, modelloader, devices, script_callbacks, sd_vae, sd_unet, errors, sd_models_compile, sd_hijack_accelerate, sd_detect, model_quant, sd_hijack_te from modules.memstats import memory_stats from modules.modeldata import model_data from modules.sd_checkpoint import CheckpointInfo, select_checkpoint, list_models, checkpoints_list, checkpoint_titles, get_closet_checkpoint_match, model_hash, update_model_hashes, setup_model, write_metadata, read_metadata_from_safetensors # pylint: disable=unused-import @@ -222,6 +222,8 @@ def move_model(model, device=None, force=False): pass # ignore model move if quantization is enabled elif 'already been set to the correct devices' in str(e0): pass # ignore errors on pre-quant models + elif 'Casting a quantized model to' in str(e0): + pass # ignore errors on quantized models else: raise e0 t1 = time.time() @@ -328,7 +330,7 @@ def load_diffuser_force(model_type, checkpoint_info, diffusers_load_config, op=' allow_post_quant = False elif model_type in ['Stable Diffusion 3']: from pipelines.model_sd3 import load_sd3 - sd_model = load_sd3(checkpoint_info, cache_dir=shared.opts.diffusers_dir, config=diffusers_load_config.get('config', None)) + sd_model = load_sd3(checkpoint_info, diffusers_load_config) allow_post_quant = False elif model_type in ['CogView 3']: # forced pipeline from pipelines.model_cogview import load_cogview3 @@ -467,7 +469,7 @@ def load_diffuser_file(model_type, pipeline, checkpoint_info, diffusers_load_con diffusers_load_config['config'] = model_config if model_type.startswith('Stable Diffusion 3'): from pipelines.model_sd3 import load_sd3 - sd_model = load_sd3(checkpoint_info=checkpoint_info, cache_dir=shared.opts.diffusers_dir, config=diffusers_load_config.get('config', None)) + sd_model = load_sd3(checkpoint_info, diffusers_load_config) elif hasattr(pipeline, 'from_single_file'): diffusers.loaders.single_file_utils.CHECKPOINT_KEY_NAMES["clip"] = "cond_stage_model.transformer.text_model.embeddings.position_embedding.weight" # patch for diffusers==0.28.0 diffusers_load_config['use_safetensors'] = True @@ -1057,7 +1059,6 @@ def reload_model_weights(sd_model=None, info=None, op='model', force=False, revi unload_model_weights(op=op) return None orig_state = copy.deepcopy(shared.state) - # shared.state = shared_state.State() shared.state.begin('Load') if sd_model is None: sd_model = model_data.sd_model if op == 'model' or op == 'dict' else model_data.sd_refiner diff --git a/modules/shared.py b/modules/shared.py index 628b83911..55fc32010 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -248,6 +248,7 @@ options_templates.update(options_section(('text_encoder', "Text Encoder"), { "diffusers_zeros_prompt_pad": OptionInfo(False, "Use zeros for prompt padding", gr.Checkbox), "te_hijack": OptionInfo(True, "Offload after prompt encode", gr.Checkbox), "te_optional_sep": OptionInfo("

Optional

", "", gr.HTML), + "te_shared_t5": OptionInfo(False, "T5: Use shared instance of text encoder"), "te_pooled_embeds": OptionInfo(False, "SDXL: Use weighted pooled embeds"), "te_complex_human_instruction": OptionInfo(True, "Sana: Use complex human instructions"), "te_use_mask": OptionInfo(True, "Lumina: Use mask in transformers"), diff --git a/pipelines/generic.py b/pipelines/generic.py new file mode 100644 index 000000000..08fe00275 --- /dev/null +++ b/pipelines/generic.py @@ -0,0 +1,132 @@ +import os +import json +import diffusers +import transformers +from modules import shared, devices, sd_models, model_quant + + +debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None + + +def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer"): + load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True) + quant_type = model_quant.get_quant_type(quant_args) + + local_file = None + if shared.opts.sd_unet is not None and shared.opts.sd_unet != 'Default': + from modules import sd_unet + if shared.opts.sd_unet not in list(sd_unet.unet_dict): + shared.log.error(f'Load module: type=transformer file="{shared.opts.sd_unet}" not found') + elif os.path.exists(sd_unet.unet_dict[shared.opts.sd_unet]): + local_file = sd_unet.unet_dict[shared.opts.sd_unet] + + if local_file is not None and local_file.lower().endswith('.gguf'): + shared.log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') + from modules import ggml + ggml.install_gguf() + loader = cls_name.from_single_file if hasattr(cls_name, 'from_single_file') else cls_name.from_pretrained + transformer = loader( + local_file, + quantization_config=diffusers.GGUFQuantizationConfig(compute_dtype=devices.dtype), + cache_dir=shared.opts.hfcache_dir, + **load_args, + ) + transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None) + elif local_file is not None and local_file.lower().endswith('.safetensors'): + shared.log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') + loader = cls_name.from_single_file if hasattr(cls_name, 'from_single_file') else cls_name.from_pretrained + transformer = loader( + local_file, + cache_dir=shared.opts.hfcache_dir, + **load_args, + ) + transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None) + else: + shared.log.debug(f'Load model: transformer="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') + if subfolder is not None: + load_args['subfolder'] = subfolder + transformer = cls_name.from_pretrained( + repo_id, + cache_dir=shared.opts.hfcache_dir, + **load_args, + **quant_args, + ) + if shared.opts.diffusers_offload_mode != 'none' and transformer is not None: + sd_models.move_model(transformer, devices.cpu) + return transformer + + +def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder"): + load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True) + quant_type = model_quant.get_quant_type(quant_args) + text_encoder = None + + # load from local file if specified + local_file = None + if shared.opts.sd_text_encoder is not None and shared.opts.sd_text_encoder != 'Default': + from modules import model_te + if shared.opts.sd_text_encoder not in list(model_te.te_dict): + shared.log.error(f'Load module: type=te file="{shared.opts.sd_text_encoder}" not found') + elif os.path.exists(model_te.te_dict[shared.opts.sd_text_encoder]): + local_file = model_te.te_dict[shared.opts.sd_text_encoder] + + # load from local file gguf + if local_file is not None and local_file.lower().endswith('.gguf'): + shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}"') + from modules import ggml + ggml.install_gguf() + text_encoder = cls_name.from_pretrained( + gguf_file=local_file, + quantization_config=diffusers.GGUFQuantizationConfig(compute_dtype=devices.dtype), + cache_dir=shared.opts.hfcache_dir, + **load_args, + ) + text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) + # load from local file safetensors + elif local_file is not None and local_file.lower().endswith('.safetensors'): + shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}"') + text_encoder = cls_name.from_pretrained( + local_file, + cache_dir=shared.opts.hfcache_dir, + **load_args, + ) + text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) + # use shared t5 if possible + elif cls_name == transformers.T5EncoderModel: + with open(os.path.join('configs', 'flux', 'text_encoder_2', 'config.json'), encoding='utf8') as f: + load_args['config'] = transformers.T5Config(**json.load(f)) + if model_quant.check_nunchaku('TE'): + import nunchaku + repo_id = 'nunchaku-tech/nunchaku-t5/awq-int4-flux.1-t5xxl.safetensors' + cls_name = nunchaku.NunchakuT5EncoderModel + shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="SVDQuant"') + text_encoder = nunchaku.NunchakuT5EncoderModel.from_pretrained( + repo_id, + torch_dtype=devices.dtype, + ) + text_encoder.quantization_method = 'SVDQuant' + elif shared.opts.te_shared_t5: + repo_id = 'Disty0/t5-xxl' + shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') + text_encoder = cls_name.from_pretrained( + repo_id, + cache_dir=shared.opts.hfcache_dir, + **load_args, + **quant_args, + ) + + # load from repo + if text_encoder is None: + shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') + if subfolder is not None: + load_args['subfolder'] = subfolder + text_encoder = cls_name.from_pretrained( + repo_id, + cache_dir=shared.opts.hfcache_dir, + **load_args, + **quant_args, + ) + + if shared.opts.diffusers_offload_mode != 'none' and text_encoder is not None: + sd_models.move_model(text_encoder, devices.cpu) + return text_encoder diff --git a/pipelines/model_auraflow.py b/pipelines/model_auraflow.py index 175ba12bf..c6f2ade77 100644 --- a/pipelines/model_auraflow.py +++ b/pipelines/model_auraflow.py @@ -1,21 +1,28 @@ -import os -import torch +import transformers import diffusers -from modules import shared, sd_models, devices - - -debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None +from modules import shared, sd_models, devices, model_quant +from pipelines import generic def load_auraflow(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) - if 'torch_dtype' not in diffusers_load_config: - diffusers_load_config['torch_dtype'] = torch.float16 - debug(f'Load model: type=AuraFlow repo="{repo_id}" config={diffusers_load_config}') + sd_models.hf_auth_check(checkpoint_info) + + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + shared.log.debug(f'Load model: type=AuraFlow repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + + transformer = generic.load_transformer(repo_id, cls_name=diffusers.AuraFlowTransformer2DModel, load_config=diffusers_load_config) + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config) + pipe = diffusers.AuraFlowPipeline.from_pretrained( repo_id, - cache_dir = shared.opts.diffusers_dir, - **diffusers_load_config, + transformer=transformer, + text_encoder=text_encoder, + cache_dir=shared.opts.diffusers_dir, + **load_args, ) + + del text_encoder + del transformer devices.torch_gc(force=True, reason='load') return pipe diff --git a/pipelines/model_bria.py b/pipelines/model_bria.py index bcd458dc4..900c58a2a 100644 --- a/pipelines/model_bria.py +++ b/pipelines/model_bria.py @@ -1,73 +1,26 @@ import os import sys import transformers +import diffusers from modules import shared, devices, sd_models, model_quant, sd_hijack_te - - -def load_transformer(repo_id, diffusers_load_config={}): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True) - fn = None - - if shared.opts.sd_unet is not None and shared.opts.sd_unet != 'Default': - from modules import sd_unet - if shared.opts.sd_unet not in list(sd_unet.unet_dict): - shared.log.error(f'Load module: type=Transformer not found: {shared.opts.sd_unet}') - return None - fn = sd_unet.unet_dict[shared.opts.sd_unet] if os.path.exists(sd_unet.unet_dict[shared.opts.sd_unet]) else None - - from pipelines.bria.transformer_bria import BriaTransformer2DModel - - if fn is not None and 'gguf' in fn.lower(): - shared.log.error('Load model: type=Bria format="gguf" unsupported') - transformer = None - elif fn is not None and 'safetensors' in fn.lower(): - shared.log.debug(f'Load model: type=Bria transformer="{fn}" quant="{model_quant.get_quant(repo_id)}" args={load_args}') - transformer = BriaTransformer2DModel.from_single_file( - fn, - cache_dir=shared.opts.hfcache_dir, - **load_args, - ) - else: - shared.log.debug(f'Load model: type=Bria transformer="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - transformer = BriaTransformer2DModel.from_pretrained( - repo_id, - subfolder="transformer", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and transformer is not None: - sd_models.move_model(transformer, devices.cpu) - return transformer - - -def load_text_encoder(repo_id, diffusers_load_config={}): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - shared.log.debug(f'Load model: type=Bria te="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - text_encoder = transformers.T5EncoderModel.from_pretrained( - repo_id, - subfolder="text_encoder", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and text_encoder is not None: - sd_models.move_model(text_encoder, devices.cpu) - return text_encoder +from pipelines import generic def load_bria(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - transformer = load_transformer(repo_id, diffusers_load_config) - text_encoder = load_text_encoder(repo_id, diffusers_load_config) - - load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model') - shared.log.debug(f'Load model: type=Bria model="{checkpoint_info.name}" repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') - - from pipelines.bria.bria_pipeline import BriaPipeline sys.path.append(os.path.join(os.path.dirname(__file__), 'bria')) + from pipelines.bria.bria_pipeline import BriaPipeline + from pipelines.bria.transformer_bria import BriaTransformer2DModel + diffusers.BriaPipeline = BriaPipeline + diffusers.BriaTransformer2DModel = BriaTransformer2DModel + + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + shared.log.debug(f'Load model: type=Bria repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + + transformer = generic.load_transformer(repo_id, cls_name=BriaTransformer2DModel, load_config=diffusers_load_config) + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config) pipe = BriaPipeline.from_pretrained( repo_id, diff --git a/pipelines/model_chroma.py b/pipelines/model_chroma.py index a1ca25ffc..102eabb08 100644 --- a/pipelines/model_chroma.py +++ b/pipelines/model_chroma.py @@ -1,281 +1,34 @@ import os -import json -import torch import diffusers import transformers -from safetensors.torch import load_file -from huggingface_hub import hf_hub_download -from modules import shared, errors, devices, sd_models, sd_unet, model_te, model_quant, sd_hijack_te +from modules import shared, devices, sd_models, model_quant +from pipelines import generic debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None -def load_chroma_quanto(checkpoint_info): - transformer, text_encoder = None, None - quanto = model_quant.load_quanto('Load model: type=Chroma') - - if isinstance(checkpoint_info, str): - repo_path = checkpoint_info - else: - repo_path = checkpoint_info.path - - try: - quantization_map = os.path.join(repo_path, "transformer", "quantization_map.json") - debug(f'Load model: type=Chroma quantization map="{quantization_map}" repo="{checkpoint_info.name}" component="transformer"') - if not os.path.exists(quantization_map): - repo_id = sd_models.path_to_repo(checkpoint_info) - quantization_map = hf_hub_download(repo_id, subfolder='transformer', filename='quantization_map.json', cache_dir=shared.opts.diffusers_dir) - with open(quantization_map, "r", encoding='utf8') as f: - quantization_map = json.load(f) - state_dict = load_file(os.path.join(repo_path, "transformer", "diffusion_pytorch_model.safetensors")) - dtype = state_dict['context_embedder.bias'].dtype - with torch.device("meta"): - transformer = diffusers.ChromaTransformer2DModel.from_config(os.path.join(repo_path, "transformer", "config.json")).to(dtype=dtype) - quanto.requantize(transformer, state_dict, quantization_map, device=torch.device("cpu")) - transformer_dtype = transformer.dtype - if transformer_dtype != devices.dtype: - try: - transformer = transformer.to(dtype=devices.dtype) - except Exception: - shared.log.error(f"Load model: type=Chroma Failed to cast transformer to {devices.dtype}, set dtype to {transformer_dtype}") - except Exception as e: - shared.log.error(f"Load model: type=Chroma failed to load Quanto transformer: {e}") - if debug: - errors.display(e, 'Chroma Quanto:') - - try: - quantization_map = os.path.join(repo_path, "text_encoder", "quantization_map.json") - debug(f'Load model: type=Chroma quantization map="{quantization_map}" repo="{checkpoint_info.name}" component="text_encoder"') - if not os.path.exists(quantization_map): - repo_id = sd_models.path_to_repo(checkpoint_info) - quantization_map = hf_hub_download(repo_id, subfolder='text_encoder', filename='quantization_map.json', cache_dir=shared.opts.diffusers_dir) - with open(quantization_map, "r", encoding='utf8') as f: - quantization_map = json.load(f) - with open(os.path.join(repo_path, "text_encoder", "config.json"), encoding='utf8') as f: - t5_config = transformers.T5Config(**json.load(f)) - state_dict = load_file(os.path.join(repo_path, "text_encoder", "model.safetensors")) - dtype = state_dict['encoder.block.0.layer.0.SelfAttention.relative_attention_bias.weight'].dtype - with torch.device("meta"): - text_encoder = transformers.T5EncoderModel(t5_config).to(dtype=dtype) - quanto.requantize(text_encoder, state_dict, quantization_map, device=torch.device("cpu")) - text_encoder_dtype = text_encoder.dtype - if text_encoder_dtype != devices.dtype: - try: - text_encoder = text_encoder.to(dtype=devices.dtype) - except Exception: - shared.log.error(f"Load model: type=Chroma Failed to cast text encoder to {devices.dtype}, set dtype to {text_encoder_dtype}") - except Exception as e: - shared.log.error(f"Load model: type=Chroma failed to load Quanto text encoder: {e}") - if debug: - errors.display(e, 'Chroma Quanto:') - - return transformer, text_encoder - - -def load_chroma_bnb(checkpoint_info, diffusers_load_config): # pylint: disable=unused-argument - transformer, text_encoder = None, None - if isinstance(checkpoint_info, str): - repo_path = checkpoint_info - else: - repo_path = checkpoint_info.path - model_quant.load_bnb('Load model: type=Chroma') - quant = model_quant.get_quant(repo_path) - try: - # we ignore the distilled guidance layer because it degrades quality too much - # see: https://github.com/huggingface/diffusers/pull/11698#issuecomment-2969717180 for more details - if quant == 'fp8': - quantization_config = transformers.BitsAndBytesConfig(load_in_8bit=True, llm_int8_skip_modules=["distilled_guidance_layer"], bnb_4bit_compute_dtype=devices.dtype) - debug(f'Quantization: {quantization_config}') - transformer = diffusers.ChromaTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) - elif quant == 'fp4': - quantization_config = transformers.BitsAndBytesConfig(load_in_4bit=True, llm_int8_skip_modules=["distilled_guidance_layer"], bnb_4bit_compute_dtype=devices.dtype, bnb_4bit_quant_type= 'fp4') - debug(f'Quantization: {quantization_config}') - transformer = diffusers.ChromaTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) - elif quant == 'nf4': - quantization_config = transformers.BitsAndBytesConfig(load_in_4bit=True, llm_int8_skip_modules=["distilled_guidance_layer"], bnb_4bit_compute_dtype=devices.dtype, bnb_4bit_quant_type= 'nf4') - debug(f'Quantization: {quantization_config}') - transformer = diffusers.ChromaTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) - else: - transformer = diffusers.ChromaTransformer2DModel.from_single_file(repo_path, **diffusers_load_config) - except Exception as e: - shared.log.error(f"Load model: type=Chroma failed to load BnB transformer: {e}") - transformer, text_encoder = None, None - if debug: - errors.display(e, 'Chroma:') - return transformer, text_encoder - - -def load_quants(kwargs, repo_id, cache_dir, allow_quant): # pylint: disable=unused-argument - try: - diffusers_load_config = { - "torch_dtype": devices.dtype, - "cache_dir": cache_dir, - } - if 'transformer' not in kwargs and model_quant.check_nunchaku('Model'): - shared.log.error(f'Load module: quant=Nunchaku module=transformer repo="{repo_id}" unsupported') - if 'transformer' not in kwargs and model_quant.check_quant('Model'): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True, modules_to_not_convert=["distilled_guidance_layer"]) - kwargs['transformer'] = diffusers.ChromaTransformer2DModel.from_pretrained(repo_id, subfolder="transformer", **load_args, **quant_args) - if 'text_encoder' not in kwargs and model_quant.check_nunchaku('TE'): - import nunchaku - nunchaku_precision = nunchaku.utils.get_precision() - nunchaku_repo = 'mit-han-lab/nunchaku-t5/awq-int4-flux.1-t5xxl.safetensors' - shared.log.debug(f'Load module: quant=Nunchaku module=t5 repo="{nunchaku_repo}" precision={nunchaku_precision}') - kwargs['text_encoder'] = nunchaku.NunchakuT5EncoderModel.from_pretrained(nunchaku_repo, torch_dtype=devices.dtype) - if 'text_encoder' not in kwargs and model_quant.check_quant('TE'): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - kwargs['text_encoder'] = transformers.T5EncoderModel.from_pretrained(repo_id, subfolder="text_encoder", **load_args, **quant_args) - except Exception as e: - shared.log.error(f'Quantization: {e}') - errors.display(e, 'Quantization:') - return kwargs - - -def load_transformer(file_path): # triggered by opts.sd_unet change - if file_path is None or not os.path.exists(file_path): - return None - transformer = None - quant = model_quant.get_quant(file_path) - diffusers_load_config = { - "torch_dtype": devices.dtype, - "cache_dir": shared.opts.hfcache_dir, - } - if quant is not None and quant != 'none': - shared.log.info(f'Load module: type=UNet/Transformer file="{file_path}" offload={shared.opts.diffusers_offload_mode} prequant={quant} dtype={devices.dtype}') - if 'gguf' in file_path.lower(): - from modules import ggml - _transformer = ggml.load_gguf(file_path, cls=diffusers.ChromaTransformer2DModel, compute_dtype=devices.dtype) - if _transformer is not None: - transformer = _transformer - elif quant in {'qint8', 'qint4'}: - _transformer, _text_encoder = load_chroma_quanto(file_path) - if _transformer is not None: - transformer = _transformer - elif quant in {'fp8', 'fp4', 'nf4'}: - _transformer, _text_encoder = load_chroma_bnb(file_path, diffusers_load_config) - if _transformer is not None: - transformer = _transformer - else: - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True, modules_to_not_convert=["distilled_guidance_layer"]) - shared.log.debug(f'Load model: type=Chroma transformer file="{file_path}" offload={shared.opts.diffusers_offload_mode} args={load_args}') - transformer = diffusers.ChromaTransformer2DModel.from_single_file(file_path, **load_args, **quant_args) - if transformer is None: - shared.log.error('Failed to load UNet model') - shared.opts.sd_unet = 'Default' - return transformer - - -def load_chroma(checkpoint_info, diffusers_load_config): # triggered by opts.sd_checkpoint change - fn = checkpoint_info.path +def load_chroma(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - allow_post_quant = False - prequantized = model_quant.get_quant(checkpoint_info.path) - shared.log.debug(f'Load model: type=Chroma model="{checkpoint_info.name}" repo={repo_id or "none"} unet="{shared.opts.sd_unet}" te="{shared.opts.sd_text_encoder}" vae="{shared.opts.sd_vae}" quant={prequantized} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype}') - debug(f'Load model: type=Chroma config={diffusers_load_config}') + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + shared.log.debug(f'Load model: type=Chroma repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') - transformer = None - text_encoder = None - vae = None + transformer = generic.load_transformer(repo_id, cls_name=diffusers.ChromaTransformer2DModel, load_config=diffusers_load_config) + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config) - # unload current model - sd_models.unload_model_weights() - shared.sd_model = None - devices.torch_gc(force=True, reason='load') + pipe = diffusers.AuraFlowPipeline.from_pretrained( + repo_id, + transformer=transformer, + text_encoder=text_encoder, + cache_dir=shared.opts.diffusers_dir, + **load_args, + ) - if shared.opts.teacache_enabled: - from modules import teacache - shared.log.debug(f'Transformers cache: type=teacache patch=forward cls={diffusers.ChromaTransformer2DModel.__name__}') - diffusers.ChromaTransformer2DModel.forward = teacache.teacache_chroma_forward # patch must be done before transformer is loaded - - # load overrides if any - if shared.opts.sd_unet != 'Default': - try: - debug(f'Load model: type=Chroma unet="{shared.opts.sd_unet}"') - transformer = load_transformer(sd_unet.unet_dict[shared.opts.sd_unet]) - if transformer is None: - shared.opts.sd_unet = 'Default' - sd_unet.failed_unet.append(shared.opts.sd_unet) - except Exception as e: - shared.log.error(f"Load model: type=Chroma failed to load UNet: {e}") - shared.opts.sd_unet = 'Default' - if debug: - errors.display(e, 'Chroma UNet:') - if shared.opts.sd_text_encoder != 'Default': - try: - debug(f'Load model: type=Chroma te="{shared.opts.sd_text_encoder}"') - from modules.model_te import load_t5 - text_encoder = load_t5(name=shared.opts.sd_text_encoder, cache_dir=shared.opts.diffusers_dir) - except Exception as e: - shared.log.error(f"Load model: type=Chroma failed to load T5: {e}") - shared.opts.sd_text_encoder = 'Default' - if debug: - errors.display(e, 'Chroma T5:') - if shared.opts.sd_vae != 'Default' and shared.opts.sd_vae != 'Automatic': - try: - debug(f'Load model: type=Chroma vae="{shared.opts.sd_vae}"') - from modules import sd_vae - # vae = sd_vae.load_vae_diffusers(None, sd_vae.vae_dict[shared.opts.sd_vae], 'override') - vae_file = sd_vae.vae_dict[shared.opts.sd_vae] - if os.path.exists(vae_file): - vae_config = os.path.join('configs', 'chroma', 'vae', 'config.json') - vae = diffusers.AutoencoderKL.from_single_file(vae_file, config=vae_config, **diffusers_load_config) - except Exception as e: - shared.log.error(f"Load model: type=Chroma failed to load VAE: {e}") - shared.opts.sd_vae = 'Default' - if debug: - errors.display(e, 'Chroma VAE:') - - # initialize pipeline with pre-loaded components - kwargs = {} - if transformer is not None: - kwargs['transformer'] = transformer - sd_unet.loaded_unet = shared.opts.sd_unet - if text_encoder is not None: - kwargs['text_encoder'] = text_encoder - model_te.loaded_te = shared.opts.sd_text_encoder - if vae is not None: - kwargs['vae'] = vae - - cls = diffusers.ChromaPipeline - shared.log.debug(f'Load model: type=Chroma cls={cls.__name__} preloaded={list(kwargs)} revision={diffusers_load_config.get("revision", None)}') - for c in kwargs: - if getattr(kwargs[c], 'quantization_method', None) is not None or getattr(kwargs[c], 'gguf', None) is not None: - shared.log.debug(f'Load model: type=Chroma component={c} dtype={kwargs[c].dtype} quant={getattr(kwargs[c], "quantization_method", None) or getattr(kwargs[c], "gguf", None)}') - if kwargs[c].dtype == torch.float32 and devices.dtype != torch.float32: - try: - kwargs[c] = kwargs[c].to(dtype=devices.dtype) - shared.log.warning(f'Load model: type=Chroma component={c} dtype={kwargs[c].dtype} cast dtype={devices.dtype} recast') - except Exception: - pass - - allow_quant = 'gguf' not in (sd_unet.loaded_unet or '') and (prequantized is None or prequantized == 'none') - if (fn is None) or (not os.path.exists(fn) or os.path.isdir(fn)): - kwargs = load_quants(kwargs, repo_id, cache_dir=shared.opts.diffusers_dir, allow_quant=allow_quant) - if fn.endswith('.safetensors') and os.path.isfile(fn): - pipe = cls.from_single_file(fn, cache_dir=shared.opts.diffusers_dir, **kwargs, **diffusers_load_config) - allow_post_quant = True - else: - pipe = cls.from_pretrained(repo_id, cache_dir=shared.opts.diffusers_dir, **kwargs, **diffusers_load_config) - - if shared.opts.teacache_enabled and model_quant.check_nunchaku('Model'): - from nunchaku.caching.diffusers_adapters import apply_cache_on_pipe - apply_cache_on_pipe(pipe, residual_diff_threshold=0.12) - - # register autopipline diffusers.pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["chroma"] = diffusers.ChromaPipeline diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["chroma"] = diffusers.ChromaImg2ImgPipeline - # TODO model load: add ChromaControlPipeline, ChromaInpaintPipeline - # Chroma will support inpainting *after* its training has finished: https://huggingface.co/lodestones/Chroma/discussions/28#6826dd2ed86f53ff983add5c - - # release memory - transformer = None - text_encoder = None - vae = None - for k in kwargs.keys(): - kwargs[k] = None - sd_hijack_te.init_hijack(pipe) + del text_encoder + del transformer devices.torch_gc(force=True, reason='load') - return pipe, allow_post_quant + return pipe diff --git a/pipelines/model_cogview.py b/pipelines/model_cogview.py index 400038dc3..bb3b8eb1a 100644 --- a/pipelines/model_cogview.py +++ b/pipelines/model_cogview.py @@ -1,34 +1,19 @@ import transformers import diffusers -from modules import shared, devices, sd_models, model_quant, modelloader +from modules import shared, devices, sd_models, model_quant +from pipelines import generic def load_cogview3(checkpoint_info, diffusers_load_config={}): - modelloader.hf_login() repo_id = sd_models.path_to_repo(checkpoint_info) - - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model') - shared.log.debug(f'Load model: type=CogView3 transformer="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - transformer = diffusers.CogView3PlusTransformer2DModel.from_pretrained( - repo_id, - subfolder="transformer", - cache_dir=shared.opts.diffusers_dir, - **load_args, - **quant_args, - ) - - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - shared.log.debug(f'Load model: type=CogView3 te="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - text_encoder = transformers.T5EncoderModel.from_pretrained( - repo_id, - subfolder="text_encoder", - cache_dir=shared.opts.diffusers_dir, - **diffusers_load_config, - **quant_args, - ) + sd_models.hf_auth_check(checkpoint_info) load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) - shared.log.debug(f'Load model: type=CogView3 model="{checkpoint_info.name}" repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + shared.log.debug(f'Load model: type=CogView3 repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + + transformer = generic.load_transformer(repo_id, cls_name=diffusers.CogView3PlusTransformer2DModel, load_config=diffusers_load_config, subfolder="transformer") + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder") + pipe = diffusers.CogView3PlusPipeline.from_pretrained( repo_id, text_encoder=text_encoder, @@ -36,43 +21,30 @@ def load_cogview3(checkpoint_info, diffusers_load_config={}): cache_dir=shared.opts.diffusers_dir, **load_args, ) + del transformer + del text_encoder devices.torch_gc() return pipe def load_cogview4(checkpoint_info, diffusers_load_config={}): - modelloader.hf_login() repo_id = sd_models.path_to_repo(checkpoint_info) - - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model') - shared.log.debug(f'Load model: type=CogView4 transformer="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - transformer = diffusers.CogView4Transformer2DModel.from_pretrained( - repo_id, - subfolder="transformer", - cache_dir=shared.opts.diffusers_dir, - **diffusers_load_config, - **quant_args, - ) - - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - shared.log.debug(f'Load model: type=CogView4 te="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - text_encoder = transformers.AutoModelForCausalLM.from_pretrained( # TODO model load: cogview4 balanced offload does not work for GlmModel - repo_id, - subfolder="text_encoder", - cache_dir=shared.opts.diffusers_dir, - **load_args, - # **quant_args, - ) + sd_models.hf_auth_check(checkpoint_info) load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) - shared.log.debug(f'Load model: type=CogView4 model="{checkpoint_info.name}" repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') - pipe = diffusers.CogView4Pipeline.from_pretrained( + shared.log.debug(f'Load model: type=CogView4 repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + + transformer = generic.load_transformer(repo_id, cls_name=diffusers.CogView4Transformer2DModel, load_config=diffusers_load_config, subfolder="transformer") + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder") + + pipe = diffusers.CogView3PlusPipeline.from_pretrained( repo_id, text_encoder=text_encoder, transformer=transformer, cache_dir=shared.opts.diffusers_dir, **load_args, ) - pipe.enable_model_cpu_offload() + del transformer + del text_encoder devices.torch_gc() return pipe diff --git a/pipelines/model_cosmos.py b/pipelines/model_cosmos.py index 419dc3f65..1c1c9cee9 100644 --- a/pipelines/model_cosmos.py +++ b/pipelines/model_cosmos.py @@ -1,68 +1,21 @@ -import os import transformers import diffusers from modules import shared, devices, sd_models, model_quant, sd_hijack_te - - -def load_transformer(repo_id, diffusers_load_config={}): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True) - fn = None - - if shared.opts.sd_unet is not None and shared.opts.sd_unet != 'Default': - from modules import sd_unet - if shared.opts.sd_unet not in list(sd_unet.unet_dict): - shared.log.error(f'Load module: type=Transformer not found: {shared.opts.sd_unet}') - return None - fn = sd_unet.unet_dict[shared.opts.sd_unet] if os.path.exists(sd_unet.unet_dict[shared.opts.sd_unet]) else None - - if fn is not None and 'gguf' in fn.lower(): - shared.log.error('Load model: type=Cosmos format="gguf" unsupported') - transformer = None - elif fn is not None and 'safetensors' in fn.lower(): - shared.log.debug(f'Load model: type=Cosmos transformer="{fn}" quant="{model_quant.get_quant(repo_id)}" args={load_args}') - transformer = diffusers.CosmosTransformer3DModel.from_single_file(fn, cache_dir=shared.opts.hfcache_dir, **load_args) - else: - shared.log.debug(f'Load model: type=Cosmos transformer="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - transformer = diffusers.CosmosTransformer3DModel.from_pretrained( - repo_id, - subfolder="transformer", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and transformer is not None: - sd_models.move_model(transformer, devices.cpu) - return transformer - - -def load_text_encoder(repo_id, diffusers_load_config={}): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - shared.log.debug(f'Load model: type=Cosmos te="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - text_encoder = transformers.T5EncoderModel.from_pretrained( - repo_id, - subfolder="text_encoder", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and text_encoder is not None: - sd_models.move_model(text_encoder, devices.cpu) - return text_encoder +from pipelines import generic def load_cosmos_t2i(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - transformer = load_transformer(repo_id, diffusers_load_config) - text_encoder = load_text_encoder(repo_id, diffusers_load_config) + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + shared.log.debug(f'Load model: type=Cosmos repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + + transformer = generic.load_transformer(repo_id, cls_name=diffusers.CosmosTransformer3DModel, load_config=diffusers_load_config, subfolder="transformer") + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder") safety_checker = Fake_safety_checker() - load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model') - shared.log.debug(f'Load model: type=Cosmos model="{checkpoint_info.name}" repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') - - cls = diffusers.Cosmos2TextToImagePipeline - pipe = cls.from_pretrained( + pipe = diffusers.Cosmos2TextToImagePipeline.from_pretrained( repo_id, transformer=transformer, text_encoder=text_encoder, diff --git a/pipelines/model_flex.py b/pipelines/model_flex.py index 4a11152ff..f7a285348 100644 --- a/pipelines/model_flex.py +++ b/pipelines/model_flex.py @@ -1,81 +1,32 @@ -import os import transformers import diffusers from modules import shared, devices, sd_models, model_quant, sd_hijack_te - - -def load_transformer(repo_id, diffusers_load_config={}): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True) - fn = None - - if shared.opts.sd_unet is not None and shared.opts.sd_unet != 'Default': - from modules import sd_unet - if shared.opts.sd_unet not in list(sd_unet.unet_dict): - shared.log.error(f'Load module: type=Transformer not found: {shared.opts.sd_unet}') - return None - fn = sd_unet.unet_dict[shared.opts.sd_unet] if os.path.exists(sd_unet.unet_dict[shared.opts.sd_unet]) else None - - if fn is not None and 'gguf' in fn.lower(): - shared.log.error('Load model: type=HiDream format="gguf" unsupported') - transformer = None - from modules import ggml - transformer = ggml.load_gguf(fn, cls=diffusers.HiDreamImageTransformer2DModel, compute_dtype=devices.dtype) - elif fn is not None and 'safetensors' in fn.lower(): - shared.log.debug(f'Load model: type=FLEX transformer="{repo_id}" quant="{model_quant.get_quant(repo_id)}" args={load_args}') - transformer = diffusers.FluxTransformer2DModel.from_single_file(fn, cache_dir=shared.opts.hfcache_dir, **load_args) - else: - shared.log.debug(f'Load model: type=FLEX transformer="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - transformer = diffusers.FluxTransformer2DModel.from_pretrained( - repo_id, - subfolder="transformer", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and transformer is not None: - sd_models.move_model(transformer, devices.cpu) - return transformer - - -def load_text_encoders(repo_id, diffusers_load_config={}): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - shared.log.debug(f'Load model: type=FLEX t5="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - text_encoder_2 = transformers.T5EncoderModel.from_pretrained( - repo_id, - subfolder="text_encoder_2", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and text_encoder_2 is not None: - sd_models.move_model(text_encoder_2, devices.cpu) - return text_encoder_2 +from pipelines import generic def load_flex(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - transformer = load_transformer(repo_id, diffusers_load_config) - text_encoder_2 = load_text_encoders(repo_id, diffusers_load_config) + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + shared.log.debug(f'Load model: type=Flex repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') - load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model') - shared.log.debug(f'Load model: type=FLEX model="{checkpoint_info.name}" repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + transformer = generic.load_transformer(repo_id, cls_name=diffusers.FluxTransformer2DModel, load_config=diffusers_load_config) + text_encoder_2 = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder_2") from pipelines.flex2 import Flex2Pipeline pipe = Flex2Pipeline.from_pretrained( repo_id, - # custom_pipeline=repo_id, transformer=transformer, text_encoder_2=text_encoder_2, cache_dir=shared.opts.diffusers_dir, **load_args, ) - sd_hijack_te.init_hijack(pipe) diffusers.pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["flex2"] = Flex2Pipeline diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["flex2"] = Flex2Pipeline diffusers.pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["flex2"] = Flex2Pipeline + sd_hijack_te.init_hijack(pipe) del text_encoder_2 del transformer diff --git a/pipelines/model_flite.py b/pipelines/model_flite.py index 9c1426fb4..ad883a564 100644 --- a/pipelines/model_flite.py +++ b/pipelines/model_flite.py @@ -1,52 +1,26 @@ import sys +import diffusers import transformers from modules import shared, devices, sd_models, model_quant, sd_hijack_te - - -def load_dit(repo_id, diffusers_load_config={}): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True) - shared.log.debug(f'Load model: type=FLite dit="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - import pipelines.f_lite - sys.modules['f_lite'] = pipelines.f_lite - transformer = pipelines.f_lite.DiT.from_pretrained( - repo_id, - subfolder="dit_model", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and transformer is not None: - sd_models.move_model(transformer, devices.cpu) - return transformer - - -def load_text_encoder(repo_id, diffusers_load_config={}): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - shared.log.debug(f'Load model: type=FLite te="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - text_encoder = transformers.T5EncoderModel.from_pretrained( - repo_id, - subfolder="text_encoder", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and text_encoder is not None: - sd_models.move_model(text_encoder, devices.cpu) - return text_encoder +from pipelines import generic def load_flite(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - from pipelines.f_lite import FLitePipeline - dit_model = load_dit(repo_id, diffusers_load_config) - text_encoder = load_text_encoder(repo_id, diffusers_load_config) + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + shared.log.debug(f'Load model: type=FLite repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') - load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model') - shared.log.debug(f'Load model: type=FLite model="{checkpoint_info.name}" repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') - pipe = FLitePipeline.from_pretrained( - repo_id, + from pipelines import f_lite + diffusers.FLitePipeline = f_lite.FLitePipeline + sys.modules['f_lite'] = f_lite + + dit_model = generic.load_transformer(repo_id, cls_name=f_lite.DiT, load_config=diffusers_load_config, subfolder="dit_model") + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder") + + pipe = f_lite.FLitePipeline.from_pretrained( + "Freepik/F-Lite", # pr only exists on main repo revision="refs/pr/8", dit_model=dit_model, text_encoder=text_encoder, diff --git a/pipelines/model_flux.py b/pipelines/model_flux.py index 5c1ba745b..da4bf70e9 100644 --- a/pipelines/model_flux.py +++ b/pipelines/model_flux.py @@ -181,7 +181,7 @@ def load_transformer(file_path): # triggered by opts.sd_unet change _transformer, _text_encoder_2 = load_flux_bnb(file_path, diffusers_load_config) if _transformer is not None: transformer = _transformer - elif 'nf4' in quant: # TODO flux: loader for civitai nf4 models + elif 'nf4' in quant: from pipelines.model_flux_nf4 import load_flux_nf4 _transformer, _text_encoder_2 = load_flux_nf4(file_path, prequantized=True) if _transformer is not None: diff --git a/pipelines/model_hidream.py b/pipelines/model_hidream.py index 6df9092e2..a5c18d3bc 100644 --- a/pipelines/model_hidream.py +++ b/pipelines/model_hidream.py @@ -4,62 +4,12 @@ import diffusers from modules import shared, devices, sd_models, model_quant, sd_hijack_te -def load_transformer(repo_id, diffusers_load_config={}): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True) - fn = None - - if shared.opts.sd_unet is not None and shared.opts.sd_unet != 'Default': - from modules import sd_unet - if shared.opts.sd_unet not in list(sd_unet.unet_dict): - shared.log.error(f'Load module: type=Transformer not found: {shared.opts.sd_unet}') - return None - fn = sd_unet.unet_dict[shared.opts.sd_unet] if os.path.exists(sd_unet.unet_dict[shared.opts.sd_unet]) else None - - if fn is not None and 'gguf' in fn.lower(): - shared.log.error('Load model: type=HiDream format="gguf" unsupported') - transformer = None - # from modules import ggml - # transformer = ggml.load_gguf(fn, cls=diffusers.HiDreamImageTransformer2DModel, compute_dtype=devices.dtype) - elif fn is not None and 'safetensors' in fn.lower(): - shared.log.debug(f'Load model: type=HiDream transformer="{repo_id}" offload={shared.opts.diffusers_offload_mode} quant="{model_quant.get_quant(repo_id)}" args={load_args}') - transformer = diffusers.HiDreamImageTransformer2DModel.from_single_file(fn, cache_dir=shared.opts.hfcache_dir, **load_args, **quant_args) - # elif model_quant.check_nunchaku('Model'): - # shared.log.error(f'Load model: type=HiDream transformer="{repo_id}" quant="Nunchaku" unsupported') - # transformer = None - else: - shared.log.debug(f'Load model: type=HiDream transformer="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - transformer = diffusers.HiDreamImageTransformer2DModel.from_pretrained( - repo_id, - subfolder="transformer", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and transformer is not None: - sd_models.move_model(transformer, devices.cpu) - return transformer - - -def load_text_encoders(repo_id, diffusers_load_config={}): - if repo_id == 'HiDream-ai/HiDream-E1-Full': - repo_id = 'HiDream-ai/HiDream-I1-Full' # use I1 for t5 and llm - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - shared.log.debug(f'Load model: type=HiDream te3="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - text_encoder_3 = transformers.T5EncoderModel.from_pretrained( - repo_id, - subfolder="text_encoder_3", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and text_encoder_3 is not None: - sd_models.move_model(text_encoder_3, devices.cpu) - +def load_llama(repo_id, diffusers_load_config={}): load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) llama_repo = shared.opts.model_h1_llama_repo if shared.opts.model_h1_llama_repo != 'Default' else 'meta-llama/Meta-Llama-3.1-8B-Instruct' shared.log.debug(f'Load model: type=HiDream te4="{llama_repo}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - sd_models.hf_auth_check(llama_repo) + text_encoder_4 = transformers.LlamaForCausalLM.from_pretrained( llama_repo, output_hidden_states=True, @@ -75,18 +25,19 @@ def load_text_encoders(repo_id, diffusers_load_config={}): ) if shared.opts.diffusers_offload_mode != 'none' and text_encoder_4 is not None: sd_models.move_model(text_encoder_4, devices.cpu) - return text_encoder_3, text_encoder_4, tokenizer_4 + return text_encoder_4, tokenizer_4 def load_hidream(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - transformer = load_transformer(repo_id, diffusers_load_config) - text_encoder_3, text_encoder_4, tokenizer_4 = load_text_encoders(repo_id, diffusers_load_config) + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + shared.log.debug(f'Load model: type=HiDream repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') - load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model') - shared.log.debug(f'Load model: type=HiDream model="{checkpoint_info.name}" repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + transformer = generic.load_transformer(repo_id, cls_name=diffusers.HiDreamImageTransformer2DModel, load_config=diffusers_load_config, subfolder="transformer") + text_encoder_3 = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder_3") + text_encoder_4, tokenizer_4 = load_text_encoders(repo_id, diffusers_load_config) if shared.opts.teacache_enabled: from modules import teacache diff --git a/pipelines/model_pixart.py b/pipelines/model_pixart.py index 254326abc..f8d950ad7 100644 --- a/pipelines/model_pixart.py +++ b/pipelines/model_pixart.py @@ -1,12 +1,14 @@ import transformers import diffusers from huggingface_hub import file_exists +from modules import shared, devices, modelloader, sd_models, model_quant +from pipelines import generic def load_pixart(checkpoint_info, diffusers_load_config={}): - from modules import shared, devices, modelloader, sd_models, model_quant - modelloader.hf_login() repo_id = sd_models.path_to_repo(checkpoint_info) + sd_models.hf_auth_check(checkpoint_info) + repo_id_tenc = repo_id repo_id_pipe = repo_id @@ -15,30 +17,21 @@ def load_pixart(checkpoint_info, diffusers_load_config={}): if not file_exists(repo_id_pipe, "model_index.json"): repo_id_pipe = "PixArt-alpha/PixArt-Sigma-XL-2-1024-MS" - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model') - transformer = diffusers.PixArtTransformer2DModel.from_pretrained( - repo_id, - subfolder='transformer', - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - text_encoder = transformers.T5EncoderModel.from_pretrained( - repo_id_tenc, - subfolder="text_encoder", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + shared.log.debug(f'Load model: type=AuraFlow repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + + transformer = generic.load_transformer(repo_id, cls_name=diffusers.PixArtTransformer2DModel, load_config=diffusers_load_config) + text_encoder = generic.load_text_encoder(repo_id_tenc, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config) + pipe = diffusers.PixArtSigmaPipeline.from_pretrained( repo_id_pipe, - cache_dir=shared.opts.diffusers_dir, transformer=transformer, text_encoder=text_encoder, + cache_dir=shared.opts.diffusers_dir, **load_args, ) + + del text_encoder + del transformer devices.torch_gc(force=True, reason='load') return pipe diff --git a/pipelines/model_qwen.py b/pipelines/model_qwen.py index 622b35bc9..4ac91e3cd 100644 --- a/pipelines/model_qwen.py +++ b/pipelines/model_qwen.py @@ -1,48 +1,19 @@ import transformers import diffusers from modules import shared, devices, sd_models, model_quant, sd_hijack_te - - -def load_transformer(repo_id, diffusers_load_config={}): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True) - shared.log.debug(f'Load model: type=Qwen transformer="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - transformer = diffusers.QwenImageTransformer2DModel.from_pretrained( - repo_id, - subfolder="transformer", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and transformer is not None: - sd_models.move_model(transformer, devices.cpu) - return transformer - - -def load_text_encoder(repo_id, diffusers_load_config={}): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - shared.log.debug(f'Load model: type=Qwen te="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') - text_encoder = transformers.Qwen2_5_VLForConditionalGeneration.from_pretrained( - repo_id, - subfolder="text_encoder", - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and text_encoder is not None: - sd_models.move_model(text_encoder, devices.cpu) - return text_encoder +from pipelines import generic def load_qwen(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - transformer = load_transformer(repo_id, diffusers_load_config) - text_encoder = load_text_encoder(repo_id, diffusers_load_config) - load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model') shared.log.debug(f'Load model: type=Qwen model="{checkpoint_info.name}" repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + transformer = generic.load_transformer(repo_id, cls_name=diffusers.QwenImageTransformer2DModel, load_config=diffusers_load_config) + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.Qwen2_5_VLForConditionalGeneration, load_config=diffusers_load_config) + cls = diffusers.QwenImagePipeline pipe = cls.from_pretrained( repo_id, diff --git a/pipelines/model_sd3.py b/pipelines/model_sd3.py index 6130ad81c..177655e54 100644 --- a/pipelines/model_sd3.py +++ b/pipelines/model_sd3.py @@ -1,128 +1,36 @@ import os import diffusers import transformers -from modules import shared, devices, errors, sd_models, sd_unet, model_quant, model_tools +from modules import shared, devices, sd_models, model_quant +from pipelines import generic -def load_overrides(kwargs, cache_dir): - if shared.opts.sd_unet != 'Default': - try: - fn = sd_unet.unet_dict[shared.opts.sd_unet] - if fn.endswith('.safetensors'): - kwargs['transformer'] = diffusers.SD3Transformer2DModel.from_single_file(fn, cache_dir=cache_dir, torch_dtype=devices.dtype) - sd_unet.loaded_unet = shared.opts.sd_unet - shared.log.debug(f'Load model: type=SD3 unet="{shared.opts.sd_unet}" fmt=safetensors') - elif fn.endswith('.gguf'): - from modules import ggml - kwargs['transformer'] = ggml.load_gguf(fn, cls=diffusers.SD3Transformer2DModel, compute_dtype=devices.dtype) - sd_unet.loaded_unet = shared.opts.sd_unet - shared.log.debug(f'Load model: type=SD3 unet="{shared.opts.sd_unet}" fmt=gguf') - except Exception as e: - shared.log.error(f"Load model: type=SD3 failed to load UNet: {e}") - errors.display(e, 'UNet') - shared.opts.sd_unet = 'Default' - sd_unet.failed_unet.append(shared.opts.sd_unet) - - if shared.opts.sd_text_encoder != 'Default': - try: - from modules.model_te import load_t5, load_vit_l, load_vit_g - if 'vit-l' in shared.opts.sd_text_encoder.lower(): - kwargs['text_encoder'] = load_vit_l() - shared.log.debug(f'Load model: type=SD3 variant="vit-l" te="{shared.opts.sd_text_encoder}"') - elif 'vit-g' in shared.opts.sd_text_encoder.lower(): - kwargs['text_encoder_2'] = load_vit_g() - shared.log.debug(f'Load model: type=SD3 variant="vit-g" te="{shared.opts.sd_text_encoder}"') - else: - kwargs['text_encoder_3'] = load_t5(name=shared.opts.sd_text_encoder, cache_dir=shared.opts.diffusers_dir) - shared.log.debug(f'Load model: type=SD3 variant="t5" te="{shared.opts.sd_text_encoder}"') - except Exception as e: - shared.log.error(f"Load model: type=SD3 failed to load T5: {e}") - errors.display(e, 'TE') - shared.opts.sd_text_encoder = 'Default' - - if shared.opts.sd_vae != 'Default' and shared.opts.sd_vae != 'Automatic': - try: - from modules import sd_vae - vae_file = sd_vae.vae_dict[shared.opts.sd_vae] - if os.path.exists(vae_file): - vae_config = os.path.join('configs', 'sd3', 'vae', 'config.json') - kwargs['vae'] = diffusers.AutoencoderKL.from_single_file(vae_file, config=vae_config, cache_dir=cache_dir, torch_dtype=devices.dtype) - shared.log.debug(f'Load model: type=SD3 vae="{shared.opts.sd_vae}"') - except Exception as e: - shared.log.error(f"Load model: type=SD3 failed to load VAE: {e}") - errors.display(e, 'VAE') - shared.opts.sd_vae = 'Default' - return kwargs - - -def load_quants(kwargs, repo_id, cache_dir): - quant_args = model_quant.create_config(module='Model') - if quant_args and 'quantization_config' in quant_args: - kwargs['transformer'] = diffusers.SD3Transformer2DModel.from_pretrained(repo_id, subfolder="transformer", cache_dir=cache_dir, torch_dtype=devices.dtype, **quant_args) - quant_args = model_quant.create_config(module='TE') - if quant_args and 'quantization_config' in quant_args: - kwargs['text_encoder_3'] = transformers.T5EncoderModel.from_pretrained(repo_id, subfolder="text_encoder_3", variant='fp16', cache_dir=cache_dir, torch_dtype=devices.dtype, **quant_args) - return kwargs - - -def load_missing(kwargs, fn, cache_dir): - keys = model_tools.get_safetensor_keys(fn) - size = os.stat(fn).st_size // 1024 // 1024 - if size > 15000: - repo_id = 'stabilityai/stable-diffusion-3.5-large' - else: - repo_id = 'stabilityai/stable-diffusion-3-medium-diffusers' - if 'text_encoder' not in kwargs and 'text_encoder' not in keys: - kwargs['text_encoder'] = transformers.CLIPTextModelWithProjection.from_pretrained(repo_id, subfolder='text_encoder', cache_dir=cache_dir, torch_dtype=devices.dtype) - shared.log.debug(f'Load model: type=SD3 missing=te1 repo="{repo_id}"') - if 'text_encoder_2' not in kwargs and 'text_encoder_2' not in keys: - kwargs['text_encoder_2'] = transformers.CLIPTextModelWithProjection.from_pretrained(repo_id, subfolder='text_encoder_2', cache_dir=cache_dir, torch_dtype=devices.dtype) - shared.log.debug(f'Load model: type=SD3 missing=te2 repo="{repo_id}"') - if 'text_encoder_3' not in kwargs and 'text_encoder_3' not in keys: - load_args, quant_args = model_quant.get_dit_args({}, module='TE', device_map=True) - kwargs['text_encoder_3'] = transformers.T5EncoderModel.from_pretrained(repo_id, subfolder="text_encoder_3", variant='fp16', cache_dir=cache_dir, **load_args, **quant_args) - shared.log.debug(f'Load model: type=SD3 missing=te3 repo="{repo_id}"') - if 'vae' not in kwargs and 'vae' not in keys: - kwargs['vae'] = diffusers.AutoencoderKL.from_pretrained(repo_id, subfolder='vae', cache_dir=cache_dir, torch_dtype=devices.dtype) - shared.log.debug(f'Load model: type=SD3 missing=vae repo="{repo_id}"') - return kwargs - - -def load_sd3(checkpoint_info, cache_dir=None, config=None): +def load_sd3(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - fn = checkpoint_info.path - kwargs = {} - kwargs = load_overrides(kwargs, cache_dir) - if (fn is None) or (not os.path.exists(fn) or os.path.isdir(fn)): - kwargs = load_quants(kwargs, repo_id, cache_dir) - - loader = diffusers.StableDiffusion3Pipeline.from_pretrained - if fn is not None and os.path.exists(fn) and os.path.isfile(fn): - if fn.endswith('.safetensors'): - loader = diffusers.StableDiffusion3Pipeline.from_single_file - repo_id = fn - elif fn.endswith('.gguf'): - from modules import ggml - kwargs['transformer'] = ggml.load_gguf(fn, cls=diffusers.SD3Transformer2DModel, compute_dtype=devices.dtype) - kwargs = load_missing(kwargs, fn, cache_dir) - kwargs['variant'] = 'fp16' - else: - kwargs['variant'] = 'fp16' - - shared.log.debug(f'Load model: type=SD3 kwargs={list(kwargs)} repo="{repo_id}"') + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + shared.log.debug(f'Load model: type=SD3 repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + transformer = generic.load_transformer(repo_id, cls_name=diffusers.SD3Transformer2DModel, load_config=diffusers_load_config) + # text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.CLIPTextModelWithProjection, load_config=diffusers_load_config, subfolder="text_encoder") + # text_encoder_2 = generic.load_text_encoder(repo_id, cls_name=transformers.CLIPTextModelWithProjection, load_config=diffusers_load_config, subfolder="text_encoder_2") if shared.opts.model_sd3_disable_te5: - shared.log.debug('Load model: type=SD3 option="disable-te5"') - kwargs['text_encoder_3'] = None + text_encoder_3 = None + else: + text_encoder_3 = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder_3") - pipe = loader( + pipe = diffusers.StableDiffusion3Pipeline.from_pretrained( repo_id, - torch_dtype=devices.dtype, - cache_dir=cache_dir, - config=config, - **kwargs, + transformer=transformer, + # text_encoder=text_encoder, + # text_encoder_2=text_encoder_2, + text_encoder_3=text_encoder_3, + cache_dir=shared.opts.diffusers_dir, + **load_args, ) + + del text_encoder_3 + del transformer devices.torch_gc(force=True, reason='load') return pipe diff --git a/scripts/nudenet/imageguard.py b/scripts/nudenet/imageguard.py index 55bcfaf86..edb2b7e8a 100644 --- a/scripts/nudenet/imageguard.py +++ b/scripts/nudenet/imageguard.py @@ -106,7 +106,7 @@ def image_guard(image, policy:str=None) -> str: attn_implementation='flash_attention_2', torch_dtype=devices.dtype, device_map="auto", - cache_dir='/mnt/models/huggingface', + cache_dir=shared.opts.hfcache_dir, ) processor = transformers.AutoProcessor.from_pretrained(repo_id, cache_dir=shared.opts.hfcache_dir) shared.log.info(f'NudeNet load: model="{repo_id}"') diff --git a/scripts/nudenet/nudenet.py b/scripts/nudenet/nudenet.py index 67b29b6a6..06ab7db25 100755 --- a/scripts/nudenet/nudenet.py +++ b/scripts/nudenet/nudenet.py @@ -61,7 +61,7 @@ class NudeDetector: self.model_path = model or hf.hf_hub_download( repo_id='vladmandic/nudenet', filename='nudenet.onnx', - cache_dir=shared.opts.diffusers_dir, + cache_dir=shared.opts.hfcache_dir, ) if session is None: log.info(f'NudeNet load: model="{self.model_path}" providers={providers}') From d7daefb8acb087494a1ec549197091dad615ae2f Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 21:37:11 -0400 Subject: [PATCH 079/141] update cogview links, fix auraflow and hidream loaders Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 4 ++-- TODO.md | 1 + cli/test-all-models.py | 16 +++++++++++++--- html/reference.json | 4 ++-- modules/shared.py | 2 +- pipelines/generic.py | 12 ++++++------ pipelines/model_auraflow.py | 9 +++++---- pipelines/model_hidream.py | 3 ++- wiki | 2 +- 9 files changed, 33 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9259d02ee..3ea02ae4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -139,7 +139,7 @@ Feature highlights include: - [ModernUI](https://github.com/user-attachments/assets/6f156154-0b0a-4be2-94f0-979e9f679501) has quite some redesign which should make it more user friendly and easier to navigate plus several new UI themes If you're still using **StandardUI**, give [ModernUI](https://vladmandic.github.io/sdnext-docs/Themes/) a try! - New models such as [WanAI 2.2](https://wan.video/) in 5B and A14B variants for both *text-to-video* and *image-to-video* workflows as well as *text-to-image* workflow! - and also [FreePix F-Lite](https://huggingface.co/Freepik/F-Lite), [Bria 3.2](https://huggingface.co/briaai/BRIA-3.2) and [bigASP 2.5](https://civitai.com/models/1789765?modelVersionId=2025412) + and also [FreePik F-Lite](https://huggingface.co/Freepik/F-Lite), [Bria 3.2](https://huggingface.co/briaai/BRIA-3.2) and [bigASP 2.5](https://civitai.com/models/1789765?modelVersionId=2025412) - Redesigned [Video](https://vladmandic.github.io/sdnext-docs/Video) interface with support for general video models plus optimized [FramePack](https://vladmandic.github.io/sdnext-docs/FramePack) and [LTXVideo](https://vladmandic.github.io/sdnext-docs/LTX) support - Fully integrated nudity detection and optional censorship with [NudeNet](https://vladmandic.github.io/sdnext-docs/NudeNet) - New background replacement and relightning methods using **Latent Bridge Matching** and new **PixelArt** processing filter @@ -184,7 +184,7 @@ For details, see [ChangeLog](https://github.com/vladmandic/automatic/blob/master can also load unet/transformer fine-tunes in safetensors format using UNET loader simply select in *networks -> models -> reference* *note* 1.3B model is a bit too small for good results and 14B is very large at 78GB even without second-stage so aggressive quantization and offloading are recommended - - [FreePix F-Lite](https://huggingface.co/Freepik/F-Lite) in *7B, 10B and Texture* variants + - [FreePik F-Lite](https://huggingface.co/Freepik/F-Lite) in *7B, 10B and Texture* variants F-Lite is a 7B/10B model trained exclusively on copyright-safe and SFW content, trained on internal dataset comprising approximately 80 million copyright-safe images available via *networks -> models -> reference* - [Bria 3.2](https://huggingface.co/briaai/BRIA-3.2) diff --git a/TODO.md b/TODO.md index da9883bd8..6116f4b45 100644 --- a/TODO.md +++ b/TODO.md @@ -4,6 +4,7 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma ## Future Candidates +- Unified `CLIPTextModelWithProjection` loader - [Modular pipelines and guiders](https://github.com/huggingface/diffusers/issues/11915) - Refactor: Sampler options - Feature: Diffusers [group offloading](https://github.com/vladmandic/sdnext/issues/4049) diff --git a/cli/test-all-models.py b/cli/test-all-models.py index 7115bc8f2..17559e430 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -1,4 +1,9 @@ #!/usr/bin/env python +""" +fal/AuraFlow-v0.3: sdnq unusupported + +""" + import io import os import time @@ -27,8 +32,8 @@ models = [ "stabilityai/stable-diffusion-3.5-medium", "stabilityai/stable-diffusion-3.5-large", "fal/AuraFlow-v0.3", - "THUDM/CogView3-Plus-3B", - "THUDM/CogView4-6B", + "zai-org/CogView4-6B", + "zai-org/CogView3-Plus-3B", "nvidia/Cosmos-Predict2-2B-Text2Image", "nvidia/Cosmos-Predict2-14B-Text2Image", "Qwen/Qwen-Image", @@ -70,8 +75,11 @@ styles_tbd = [ 'Fixed Kneeling on Bed', 'Fixed Girl in Sin City', 'Fixed Girl in a city', + 'Fixed Girl in Lace', 'Fixed Lady in Tokyo', 'Fixed MadMax selfie', + 'Fixed Party Yacht', + 'Fixed Yoga Girls', 'Fixed SDNext Neon', ] @@ -116,8 +124,10 @@ def generate(): # pylint: disable=redefined-outer-name b64 = data['images'][0].split(',',1)[0] image = Image.open(io.BytesIO(base64.b64decode(b64))) info = data['info'] - log.info(f' image: size={image.size} time={t1-t0:.2f} info="{len(info)}" fn="{fn}"') + log.info(f' image: size={image.width}x{image.height} time={t1-t0:.2f} info={len(info)}') image.save(fn) + else: + log.error(f' model: error="{model}" style="{style}" no image') except Exception as e: log.error(f' model: error="{model}" style="{style}" exception="{e}"') diff --git a/html/reference.json b/html/reference.json index 63cdbbdda..21d42a81f 100644 --- a/html/reference.json +++ b/html/reference.json @@ -520,13 +520,13 @@ }, "CogView 4": { - "path": "THUDM/CogView4-6B", + "path": "zai-org/CogView4-6B", "desc": "An innovative cascaded framework that enhances the performance of text-to-image diffusion. CogView is the first model implementing relay diffusion in the realm of text-to-image generation, executing the task by first creating low-resolution images and subsequently applying relay-based super-resolution.", "preview": "THUDM--CogView4-6B.jpg", "skip": true }, "CogView 3 Plus": { - "path": "THUDM/CogView3-Plus-3B", + "path": "zai-org/CogView3-Plus-3B", "desc": "An innovative cascaded framework that enhances the performance of text-to-image diffusion. CogView is the first model implementing relay diffusion in the realm of text-to-image generation, executing the task by first creating low-resolution images and subsequently applying relay-based super-resolution.", "preview": "THUDM--CogView3-Plus-3B.jpg", "skip": true diff --git a/modules/shared.py b/modules/shared.py index 55fc32010..25ca83b92 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -248,7 +248,7 @@ options_templates.update(options_section(('text_encoder', "Text Encoder"), { "diffusers_zeros_prompt_pad": OptionInfo(False, "Use zeros for prompt padding", gr.Checkbox), "te_hijack": OptionInfo(True, "Offload after prompt encode", gr.Checkbox), "te_optional_sep": OptionInfo("

Optional

", "", gr.HTML), - "te_shared_t5": OptionInfo(False, "T5: Use shared instance of text encoder"), + "te_shared_t5": OptionInfo(True, "T5: Use shared instance of text encoder"), "te_pooled_embeds": OptionInfo(False, "SDXL: Use weighted pooled embeds"), "te_complex_human_instruction": OptionInfo(True, "Sana: Use complex human instructions"), "te_use_mask": OptionInfo(True, "Lumina: Use mask in transformers"), diff --git a/pipelines/generic.py b/pipelines/generic.py index 08fe00275..7b9039413 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -8,8 +8,8 @@ from modules import shared, devices, sd_models, model_quant debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None -def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer"): - load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True) +def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", allow_quant=True): + load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True, allow_quant=allow_quant) quant_type = model_quant.get_quant_type(quant_args) local_file = None @@ -56,8 +56,8 @@ def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer") return transformer -def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder"): - load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True) +def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder", allow_quant=True, allow_shared=True): + load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True, allow_quant=allow_quant) quant_type = model_quant.get_quant_type(quant_args) text_encoder = None @@ -92,7 +92,7 @@ def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder ) text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) # use shared t5 if possible - elif cls_name == transformers.T5EncoderModel: + elif cls_name == transformers.T5EncoderModel and allow_shared: with open(os.path.join('configs', 'flux', 'text_encoder_2', 'config.json'), encoding='utf8') as f: load_args['config'] = transformers.T5Config(**json.load(f)) if model_quant.check_nunchaku('TE'): @@ -114,7 +114,7 @@ def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder **load_args, **quant_args, ) - + # load from repo if text_encoder is None: shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') diff --git a/pipelines/model_auraflow.py b/pipelines/model_auraflow.py index c6f2ade77..06cc978ce 100644 --- a/pipelines/model_auraflow.py +++ b/pipelines/model_auraflow.py @@ -1,6 +1,6 @@ import transformers import diffusers -from modules import shared, sd_models, devices, model_quant +from modules import shared, sd_models, devices, model_quant, sd_hijack_te from pipelines import generic @@ -8,21 +8,22 @@ def load_auraflow(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config) shared.log.debug(f'Load model: type=AuraFlow repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') transformer = generic.load_transformer(repo_id, cls_name=diffusers.AuraFlowTransformer2DModel, load_config=diffusers_load_config) - text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config) + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.UMT5EncoderModel, load_config=diffusers_load_config, allow_quant=False) # auraflow uses EleutherAI/pile-t5-xl pipe = diffusers.AuraFlowPipeline.from_pretrained( repo_id, transformer=transformer, - text_encoder=text_encoder, + # text_encoder=text_encoder, cache_dir=shared.opts.diffusers_dir, **load_args, ) del text_encoder del transformer + sd_hijack_te.init_hijack(pipe) devices.torch_gc(force=True, reason='load') return pipe diff --git a/pipelines/model_hidream.py b/pipelines/model_hidream.py index a5c18d3bc..6d1a716b8 100644 --- a/pipelines/model_hidream.py +++ b/pipelines/model_hidream.py @@ -2,6 +2,7 @@ import os import transformers import diffusers from modules import shared, devices, sd_models, model_quant, sd_hijack_te +from pipelines import generic def load_llama(repo_id, diffusers_load_config={}): @@ -37,7 +38,7 @@ def load_hidream(checkpoint_info, diffusers_load_config={}): transformer = generic.load_transformer(repo_id, cls_name=diffusers.HiDreamImageTransformer2DModel, load_config=diffusers_load_config, subfolder="transformer") text_encoder_3 = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder_3") - text_encoder_4, tokenizer_4 = load_text_encoders(repo_id, diffusers_load_config) + text_encoder_4, tokenizer_4 = load_llama(repo_id, diffusers_load_config) if shared.opts.teacache_enabled: from modules import teacache diff --git a/wiki b/wiki index 96e3932bf..0b658ebad 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 96e3932bffee9117951956074956e943f254702b +Subproject commit 0b658ebad7ad81ebb9cd131abf409878d9c8744f From 7aca434c255ec4bd33f654c454060012ad2c2078 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 21:44:56 -0400 Subject: [PATCH 080/141] lint Signed-off-by: Vladimir Mandic --- cli/test-all-models.py | 3 +-- pipelines/model_auraflow.py | 4 ++-- pipelines/model_hidream.py | 5 ++--- pipelines/model_pixart.py | 2 +- pipelines/model_sd3.py | 1 - 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cli/test-all-models.py b/cli/test-all-models.py index 17559e430..ea0d8d179 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -1,7 +1,6 @@ #!/usr/bin/env python """ -fal/AuraFlow-v0.3: sdnq unusupported - +- fal/AuraFlow-v0.3: SDNQ: layer_class_name=Linear layer_weight_shape=torch.Size([3072, 2, 1024]) weights_dtype=int8 unsupported """ import io diff --git a/pipelines/model_auraflow.py b/pipelines/model_auraflow.py index 06cc978ce..9576f5923 100644 --- a/pipelines/model_auraflow.py +++ b/pipelines/model_auraflow.py @@ -12,12 +12,12 @@ def load_auraflow(checkpoint_info, diffusers_load_config={}): shared.log.debug(f'Load model: type=AuraFlow repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') transformer = generic.load_transformer(repo_id, cls_name=diffusers.AuraFlowTransformer2DModel, load_config=diffusers_load_config) - text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.UMT5EncoderModel, load_config=diffusers_load_config, allow_quant=False) # auraflow uses EleutherAI/pile-t5-xl + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.UMT5EncoderModel, load_config=diffusers_load_config) # auraflow uses EleutherAI/pile-t5-xl pipe = diffusers.AuraFlowPipeline.from_pretrained( repo_id, transformer=transformer, - # text_encoder=text_encoder, + text_encoder=text_encoder, cache_dir=shared.opts.diffusers_dir, **load_args, ) diff --git a/pipelines/model_hidream.py b/pipelines/model_hidream.py index 6d1a716b8..0a0880103 100644 --- a/pipelines/model_hidream.py +++ b/pipelines/model_hidream.py @@ -1,11 +1,10 @@ -import os import transformers import diffusers from modules import shared, devices, sd_models, model_quant, sd_hijack_te from pipelines import generic -def load_llama(repo_id, diffusers_load_config={}): +def load_llama(diffusers_load_config={}): load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) llama_repo = shared.opts.model_h1_llama_repo if shared.opts.model_h1_llama_repo != 'Default' else 'meta-llama/Meta-Llama-3.1-8B-Instruct' shared.log.debug(f'Load model: type=HiDream te4="{llama_repo}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') @@ -38,7 +37,7 @@ def load_hidream(checkpoint_info, diffusers_load_config={}): transformer = generic.load_transformer(repo_id, cls_name=diffusers.HiDreamImageTransformer2DModel, load_config=diffusers_load_config, subfolder="transformer") text_encoder_3 = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder_3") - text_encoder_4, tokenizer_4 = load_llama(repo_id, diffusers_load_config) + text_encoder_4, tokenizer_4 = load_llama(diffusers_load_config) if shared.opts.teacache_enabled: from modules import teacache diff --git a/pipelines/model_pixart.py b/pipelines/model_pixart.py index f8d950ad7..2de1be95d 100644 --- a/pipelines/model_pixart.py +++ b/pipelines/model_pixart.py @@ -1,7 +1,7 @@ import transformers import diffusers from huggingface_hub import file_exists -from modules import shared, devices, modelloader, sd_models, model_quant +from modules import shared, devices, sd_models, model_quant from pipelines import generic diff --git a/pipelines/model_sd3.py b/pipelines/model_sd3.py index 177655e54..e488e89f1 100644 --- a/pipelines/model_sd3.py +++ b/pipelines/model_sd3.py @@ -1,4 +1,3 @@ -import os import diffusers import transformers from modules import shared, devices, sd_models, model_quant From c0489c6559bb256b9123ffa56ea43215a1935b68 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 22:23:54 -0400 Subject: [PATCH 081/141] fix cogview Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + cli/test-all-models.py | 6 +++++- pipelines/model_cogview.py | 12 +++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea02ae4a..368433be2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ And (*as always*) many bugfixes and improvements to existing features! in *settings -> text encoder* since a lot of new models use T5 text encoder, this option allows to share the same instance across all models without duplicate downloads + *note* this will not reduce size of your already downloaded models, but will reduce size of future downloads - **Wan** select which stage to run: *first/second/both* with configurable *boundary ration* when running both stages in settings -> model options - prompt parser allow explict `BOS` and `EOS` tokens in prompt diff --git a/cli/test-all-models.py b/cli/test-all-models.py index ea0d8d179..0cc864c2e 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -1,6 +1,7 @@ #!/usr/bin/env python """ -- fal/AuraFlow-v0.3: SDNQ: layer_class_name=Linear layer_weight_shape=torch.Size([3072, 2, 1024]) weights_dtype=int8 unsupported +- fal/AuraFlow-v0.3: layer_class_name=Linear layer_weight_shape=torch.Size([3072, 2, 1024]) weights_dtype=int8 unsupported +- zai-org/CogView4-6B: sdnq unsupported transformers.GlmModel """ import io @@ -128,6 +129,9 @@ def generate(): # pylint: disable=redefined-outer-name else: log.error(f' model: error="{model}" style="{style}" no image') except Exception as e: + if 'Connection refused' in str(e): + log.error('server offline') + os._exit(1) log.error(f' model: error="{model}" style="{style}" exception="{e}"') if __name__ == "__main__": diff --git a/pipelines/model_cogview.py b/pipelines/model_cogview.py index bb3b8eb1a..d3ac6f274 100644 --- a/pipelines/model_cogview.py +++ b/pipelines/model_cogview.py @@ -1,6 +1,6 @@ import transformers import diffusers -from modules import shared, devices, sd_models, model_quant +from modules import shared, devices, sd_models, model_quant, sd_hijack_te from pipelines import generic @@ -8,7 +8,7 @@ def load_cogview3(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config) shared.log.debug(f'Load model: type=CogView3 repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') transformer = generic.load_transformer(repo_id, cls_name=diffusers.CogView3PlusTransformer2DModel, load_config=diffusers_load_config, subfolder="transformer") @@ -21,6 +21,7 @@ def load_cogview3(checkpoint_info, diffusers_load_config={}): cache_dir=shared.opts.diffusers_dir, **load_args, ) + sd_hijack_te.init_hijack(pipe) del transformer del text_encoder devices.torch_gc() @@ -31,19 +32,20 @@ def load_cogview4(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config) shared.log.debug(f'Load model: type=CogView4 repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') transformer = generic.load_transformer(repo_id, cls_name=diffusers.CogView4Transformer2DModel, load_config=diffusers_load_config, subfolder="transformer") - text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder") + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.GlmModel, load_config=diffusers_load_config, subfolder="text_encoder", allow_quant=True) - pipe = diffusers.CogView3PlusPipeline.from_pretrained( + pipe = diffusers.CogView4Pipeline.from_pretrained( repo_id, text_encoder=text_encoder, transformer=transformer, cache_dir=shared.opts.diffusers_dir, **load_args, ) + sd_hijack_te.init_hijack(pipe) del transformer del text_encoder devices.torch_gc() From 562f4f250280dd317157797c43bb09e0d53cf354 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 10 Aug 2025 23:15:57 -0400 Subject: [PATCH 082/141] update qwen-lightning repo Signed-off-by: Vladimir Mandic --- cli/test-all-models.py | 46 ++++++++++++++++++++++++++++++++++++----- html/reference.json | 14 +++++++++++++ pipelines/model_qwen.py | 7 ++++--- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/cli/test-all-models.py b/cli/test-all-models.py index 0cc864c2e..9d19ad845 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -1,12 +1,14 @@ #!/usr/bin/env python """ - fal/AuraFlow-v0.3: layer_class_name=Linear layer_weight_shape=torch.Size([3072, 2, 1024]) weights_dtype=int8 unsupported -- zai-org/CogView4-6B: sdnq unsupported transformers.GlmModel +- nvidia/Cosmos-Predict2-2B-Text2Image: mat1 and mat2 shapes cannot be multiplied (512x4096 and 1024x2048) +- nvidia/Cosmos-Predict2-14B-Text2Image: mat1 and mat2 shapes cannot be multiplied (512x4096 and 1024x5120) """ import io import os import time +import json import base64 import logging import requests @@ -34,8 +36,8 @@ models = [ "fal/AuraFlow-v0.3", "zai-org/CogView4-6B", "zai-org/CogView3-Plus-3B", - "nvidia/Cosmos-Predict2-2B-Text2Image", - "nvidia/Cosmos-Predict2-14B-Text2Image", + # "nvidia/Cosmos-Predict2-2B-Text2Image", + # "nvidia/Cosmos-Predict2-14B-Text2Image", "Qwen/Qwen-Image", "Qwen/Qwen-Lightning", "Shitao/OmniGen-v1-diffusers", @@ -44,6 +46,8 @@ models = [ "Kwai-Kolors/Kolors-diffusers", "vladmandic/chroma-unlocked-v50", "vladmandic/chroma-unlocked-v50-annealed", + "vladmandic/chroma-unlocked-v48", + "vladmandic/chroma-unlocked-v48-detail-calibrated", "Alpha-VLLM/Lumina-Next-SFT-diffusers", "Alpha-VLLM/Lumina-Image-2.0", "MeissonFlow/Meissonic", @@ -82,6 +86,34 @@ styles_tbd = [ 'Fixed Yoga Girls', 'Fixed SDNext Neon', ] +history = [] + + +def read_history(): + global history # pylint: disable=global-statement + fn = os.path.join(output_folder, 'history.json') + if not os.path.exists(fn): + return + with open(fn, "r", encoding='utf8') as file: + data = file.read() + history = json.loads(data) + log.info(f'history: file="{fn}" records={len(history)}') + + +def write_history(model:str, style:str, image:str='', size:tuple=(0,0), duration:float=0, info:str='', error:str=''): + fn = os.path.join(output_folder, 'history.json') + history.append({ + 'model': model, + 'style': style, + 'image': image, + 'size': size, + 'time': duration, + 'info': info, + 'error': error, + }) + with open(fn, "w", encoding='utf8') as file: + data = json.dumps(history) # pylint: disable=no-member + file.write(data) def request(endpoint: str, dct: dict = None, method: str = 'POST'): @@ -126,17 +158,21 @@ def generate(): # pylint: disable=redefined-outer-name info = data['info'] log.info(f' image: size={image.width}x{image.height} time={t1-t0:.2f} info={len(info)}') image.save(fn) + write_history(model=model, style=style, image=fn, size=image.size, duration=round(t1-t0, 3), info=info) else: + write_history(model=model, style=style, duration=round(t1-t0, 3), error='no image') log.error(f' model: error="{model}" style="{style}" no image') except Exception as e: - if 'Connection refused' in str(e): + if 'Connection refused' in str(e) or 'RemoteDisconnected' in str(e): log.error('server offline') os._exit(1) + write_history(model=model, style=style, duration=round(t1-t0, 3), error=str(e)) log.error(f' model: error="{model}" style="{style}" exception="{e}"') + if __name__ == "__main__": log.info('test-all-models') log.info(f'output="{output_folder}" models={len(models)} styles={len(styles)}') - log.info('start...') + read_history() generate() log.info('done...') diff --git a/html/reference.json b/html/reference.json index 21d42a81f..173be5380 100644 --- a/html/reference.json +++ b/html/reference.json @@ -161,6 +161,20 @@ "skip": true, "extras": "sampler: Default, cfg_scale: 1.0" }, + "lodestones Chroma Unlocked v48": { + "path": "vladmandic/chroma-unlocked-v48", + "preview": "lodestones--Chroma.jpg", + "desc": "Chroma is a 8.9B parameter model based on FLUX.1-schnell. It’s fully Apache 2.0 licensed, ensuring that anyone can use, modify, and build on top of it—no corporate gatekeeping. The model is still training right now, and I’d love to hear your thoughts! Your input and feedback are really appreciated.", + "skip": true, + "extras": "sampler: Default, cfg_scale: 1.0" + }, + "lodestones Chroma Unlocked v48 Detail Calibrated": { + "path": "vladmandic/chroma-unlocked-v48-detail-calibrated", + "preview": "lodestones--Chroma.jpg", + "desc": "Chroma is a 8.9B parameter model based on FLUX.1-schnell. It’s fully Apache 2.0 licensed, ensuring that anyone can use, modify, and build on top of it—no corporate gatekeeping. The model is still training right now, and I’d love to hear your thoughts! Your input and feedback are really appreciated.", + "skip": true, + "extras": "sampler: Default, cfg_scale: 1.0" + }, "Qwen-Image": { "path": "Qwen/Qwen-Image", diff --git a/pipelines/model_qwen.py b/pipelines/model_qwen.py index 4ac91e3cd..7a9d0da32 100644 --- a/pipelines/model_qwen.py +++ b/pipelines/model_qwen.py @@ -12,16 +12,17 @@ def load_qwen(checkpoint_info, diffusers_load_config={}): shared.log.debug(f'Load model: type=Qwen model="{checkpoint_info.name}" repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') transformer = generic.load_transformer(repo_id, cls_name=diffusers.QwenImageTransformer2DModel, load_config=diffusers_load_config) - text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.Qwen2_5_VLForConditionalGeneration, load_config=diffusers_load_config) + repo_te = 'Qwen/Qwen-Image' if 'Qwen-Lightning' in repo_id else repo_id + text_encoder = generic.load_text_encoder(repo_te, cls_name=transformers.Qwen2_5_VLForConditionalGeneration, load_config=diffusers_load_config) - cls = diffusers.QwenImagePipeline - pipe = cls.from_pretrained( + pipe = diffusers.QwenImagePipeline.from_pretrained( repo_id, transformer=transformer, text_encoder=text_encoder, cache_dir=shared.opts.diffusers_dir, **load_args, ) + print('HERE4') pipe.task_args = { 'output_type': 'np', } From a181c181232c0fb7499cc71b01366f07c462b19f Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 11 Aug 2025 07:46:02 -0400 Subject: [PATCH 083/141] fix omnigen Signed-off-by: Vladimir Mandic --- cli/test-all-models.py | 14 +++++++++----- html/reference.json | 2 +- modules/shared_items.py | 2 +- pipelines/model_chroma.py | 2 +- pipelines/model_qwen.py | 1 - 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/cli/test-all-models.py b/cli/test-all-models.py index 9d19ad845..bc8f4027a 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -3,6 +3,8 @@ - fal/AuraFlow-v0.3: layer_class_name=Linear layer_weight_shape=torch.Size([3072, 2, 1024]) weights_dtype=int8 unsupported - nvidia/Cosmos-Predict2-2B-Text2Image: mat1 and mat2 shapes cannot be multiplied (512x4096 and 1024x2048) - nvidia/Cosmos-Predict2-14B-Text2Image: mat1 and mat2 shapes cannot be multiplied (512x4096 and 1024x5120) +- HiDream-ai/HiDream-I1-Full: 30+s/it +- Kwai-Kolors/Kolors-diffusers: `set_input_embeddings` not auto‑handled for ChatGLMModel """ import io @@ -34,17 +36,18 @@ models = [ "stabilityai/stable-diffusion-3.5-medium", "stabilityai/stable-diffusion-3.5-large", "fal/AuraFlow-v0.3", + "fal/AuraFlow-v0.2", "zai-org/CogView4-6B", "zai-org/CogView3-Plus-3B", # "nvidia/Cosmos-Predict2-2B-Text2Image", # "nvidia/Cosmos-Predict2-14B-Text2Image", "Qwen/Qwen-Image", - "Qwen/Qwen-Lightning", + "vladmandic/Qwen-Lightning", "Shitao/OmniGen-v1-diffusers", "OmniGen2/OmniGen2", - "HiDream-ai/HiDream-I1-Full", + # "HiDream-ai/HiDream-I1-Full", "Kwai-Kolors/Kolors-diffusers", - "vladmandic/chroma-unlocked-v50", + "lodestones/Chroma1-HD", "vladmandic/chroma-unlocked-v50-annealed", "vladmandic/chroma-unlocked-v48", "vladmandic/chroma-unlocked-v48-detail-calibrated", @@ -104,6 +107,7 @@ def write_history(model:str, style:str, image:str='', size:tuple=(0,0), duration fn = os.path.join(output_folder, 'history.json') history.append({ 'model': model, + 'title': model.split('/')[-1].replace('_diffusers', '').replace('-diffusers', ''), 'style': style, 'image': image, 'size': size, @@ -160,13 +164,13 @@ def generate(): # pylint: disable=redefined-outer-name image.save(fn) write_history(model=model, style=style, image=fn, size=image.size, duration=round(t1-t0, 3), info=info) else: - write_history(model=model, style=style, duration=round(t1-t0, 3), error='no image') + # write_history(model=model, style=style, duration=round(t1-t0, 3), error='no image') log.error(f' model: error="{model}" style="{style}" no image') except Exception as e: if 'Connection refused' in str(e) or 'RemoteDisconnected' in str(e): log.error('server offline') os._exit(1) - write_history(model=model, style=style, duration=round(t1-t0, 3), error=str(e)) + # write_history(model=model, style=style, duration=round(t1-t0, 3), error=str(e)) log.error(f' model: error="{model}" style="{style}" exception="{e}"') diff --git a/html/reference.json b/html/reference.json index 173be5380..873dcaefb 100644 --- a/html/reference.json +++ b/html/reference.json @@ -141,7 +141,7 @@ }, "lodestones Chroma Unlocked HD": { - "path": "vladmandic/chroma-unlocked-v50", + "path": "lodestones/Chroma1-HD", "preview": "lodestones--Chroma.jpg", "desc": "Chroma is a 8.9B parameter model based on FLUX.1-schnell. It’s fully Apache 2.0 licensed, ensuring that anyone can use, modify, and build on top of it—no corporate gatekeeping. The model is still training right now, and I’d love to hear your thoughts! Your input and feedback are really appreciated.", "skip": true, diff --git a/modules/shared_items.py b/modules/shared_items.py index ede653f07..260ad2f2a 100644 --- a/modules/shared_items.py +++ b/modules/shared_items.py @@ -42,7 +42,7 @@ pipelines = { 'UniDiffuser': getattr(diffusers, 'UniDiffuserPipeline', None), 'Amused': getattr(diffusers, 'AmusedPipeline', None), 'HiDream': getattr(diffusers, 'HiDreamImagePipeline', None), - 'OmniGenPipeline': getattr(diffusers, 'OmniGenPipeline', None), + 'OmniGen': getattr(diffusers, 'OmniGenPipeline', None), 'Cosmos': getattr(diffusers, 'Cosmos2TextToImagePipeline', None), 'WanAI': getattr(diffusers, 'WanPipeline', None), 'Qwen': getattr(diffusers, 'QwenImagePipeline', None), diff --git a/pipelines/model_chroma.py b/pipelines/model_chroma.py index 102eabb08..5efa9b436 100644 --- a/pipelines/model_chroma.py +++ b/pipelines/model_chroma.py @@ -18,7 +18,7 @@ def load_chroma(checkpoint_info, diffusers_load_config={}): transformer = generic.load_transformer(repo_id, cls_name=diffusers.ChromaTransformer2DModel, load_config=diffusers_load_config) text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config) - pipe = diffusers.AuraFlowPipeline.from_pretrained( + pipe = diffusers.ChromaPipeline.from_pretrained( repo_id, transformer=transformer, text_encoder=text_encoder, diff --git a/pipelines/model_qwen.py b/pipelines/model_qwen.py index 7a9d0da32..06464938b 100644 --- a/pipelines/model_qwen.py +++ b/pipelines/model_qwen.py @@ -22,7 +22,6 @@ def load_qwen(checkpoint_info, diffusers_load_config={}): cache_dir=shared.opts.diffusers_dir, **load_args, ) - print('HERE4') pipe.task_args = { 'output_type': 'np', } From e42a27a0e45152905d34601590ddf29ff2f38795 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 11 Aug 2025 08:30:46 -0400 Subject: [PATCH 084/141] fix chroma Signed-off-by: Vladimir Mandic --- cli/test-all-models.py | 106 +++++++++++++++++++--------------- installer.py | 2 +- modules/sd_models.py | 7 ++- pipelines/generic.py | 8 ++- pipelines/model_chroma.py | 1 + pipelines/model_hunyuandit.py | 29 ++++++++++ 6 files changed, 102 insertions(+), 51 deletions(-) create mode 100644 pipelines/model_hunyuandit.py diff --git a/cli/test-all-models.py b/cli/test-all-models.py index bc8f4027a..163c193d6 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -1,10 +1,14 @@ #!/usr/bin/env python """ +Warning: - fal/AuraFlow-v0.3: layer_class_name=Linear layer_weight_shape=torch.Size([3072, 2, 1024]) weights_dtype=int8 unsupported +- Kwai-Kolors/Kolors-diffusers: `set_input_embeddings` not auto‑handled for ChatGLMModel +Error: - nvidia/Cosmos-Predict2-2B-Text2Image: mat1 and mat2 shapes cannot be multiplied (512x4096 and 1024x2048) - nvidia/Cosmos-Predict2-14B-Text2Image: mat1 and mat2 shapes cannot be multiplied (512x4096 and 1024x5120) +- Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers: CUDA error: device-side assert triggered +Other: - HiDream-ai/HiDream-I1-Full: 30+s/it -- Kwai-Kolors/Kolors-diffusers: `set_input_embeddings` not auto‑handled for ChatGLMModel """ import io @@ -25,49 +29,53 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) output_folder = 'outputs/compare' -models = [ - "sdxl-base-v10-vaefix", - "tempest-by-vlad-0.1", - "icbinpXL_v6", - "briaai/BRIA-3.2", - "Freepik/F-Lite", - "Freepik/F-Lite-Texture", - "ostris/Flex.2-preview", - "stabilityai/stable-diffusion-3.5-medium", - "stabilityai/stable-diffusion-3.5-large", - "fal/AuraFlow-v0.3", - "fal/AuraFlow-v0.2", - "zai-org/CogView4-6B", - "zai-org/CogView3-Plus-3B", - # "nvidia/Cosmos-Predict2-2B-Text2Image", - # "nvidia/Cosmos-Predict2-14B-Text2Image", - "Qwen/Qwen-Image", - "vladmandic/Qwen-Lightning", - "Shitao/OmniGen-v1-diffusers", - "OmniGen2/OmniGen2", - # "HiDream-ai/HiDream-I1-Full", - "Kwai-Kolors/Kolors-diffusers", - "lodestones/Chroma1-HD", - "vladmandic/chroma-unlocked-v50-annealed", - "vladmandic/chroma-unlocked-v48", - "vladmandic/chroma-unlocked-v48-detail-calibrated", - "Alpha-VLLM/Lumina-Next-SFT-diffusers", - "Alpha-VLLM/Lumina-Image-2.0", - "MeissonFlow/Meissonic", - "Efficient-Large-Model/SANA1.5_1.6B_1024px_diffusers", - "Efficient-Large-Model/SANA1.5_4.8B_1024px_diffusers", - "PixArt-alpha/PixArt-XL-2-1024-MS", - "PixArt-alpha/PixArt-Sigma-XL-2-1024-MS", - "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", - "Wan-AI/Wan2.1-T2V-14B-Diffusers", - "stabilityai/stable-cascade", -] +models = { + "sdxl-base-v10-vaefix": {}, + "tempest-by-vlad-0.1": {}, + "icbinpXL_v6": {}, + "briaai/BRIA-3.2": {}, + "Freepik/F-Lite": {}, + "Freepik/F-Lite-Texture": {}, + "ostris/Flex.2-preview": {}, + "playgroundai/playground-v2-1024px-aesthetic": {}, + "playground-v2.5-1024px-aesthetic.fp16": { "sampler_name": "DPM++ 2M EDM" }, + "stabilityai/stable-diffusion-3.5-medium": {}, + "stabilityai/stable-diffusion-3.5-large": {}, + "fal/AuraFlow-v0.3": {}, + "fal/AuraFlow-v0.2": {}, + "zai-org/CogView4-6B": {}, + "zai-org/CogView3-Plus-3B": {}, + # "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers": {}, + # "nvidia/Cosmos-Predict2-2B-Text2Image": {}, + # "nvidia/Cosmos-Predict2-14B-Text2Image": {}, + "Qwen/Qwen-Image": {}, + "vladmandic/Qwen-Lightning": {}, + "Shitao/OmniGen-v1-diffusers": {}, + "OmniGen2/OmniGen2": {}, + # "HiDream-ai/HiDream-I1-Full": {}, + "Kwai-Kolors/Kolors-diffusers": {}, + "lodestones/Chroma1-HD": {}, + "vladmandic/chroma-unlocked-v50-annealed": {}, + "vladmandic/chroma-unlocked-v48": {}, + "vladmandic/chroma-unlocked-v48-detail-calibrated": {}, + "Alpha-VLLM/Lumina-Next-SFT-diffusers": {}, + "Alpha-VLLM/Lumina-Image-2.0": {}, + "MeissonFlow/Meissonic": {}, + "Efficient-Large-Model/SANA1.5_1.6B_1024px_diffusers": {}, + "Efficient-Large-Model/SANA1.5_4.8B_1024px_diffusers": {}, + "PixArt-alpha/PixArt-XL-2-1024-MS": {}, + "PixArt-alpha/PixArt-Sigma-XL-2-1024-MS": {}, + "Wan-AI/Wan2.1-T2V-1.3B-Diffusers": {}, + "Wan-AI/Wan2.1-T2V-14B-Diffusers": {}, + "stabilityai/stable-cascade": {}, +} models_tbd = [ "black-forest-labs/FLUX.1-dev", "black-forest-labs/FLUX.1-Kontext-dev", "black-forest-labs/FLUX.1-Krea-dev", - "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers", # TODO "kandinsky-community/kandinsky-3", # TODO + "kandinsky-community/kandinsky-2-2-decoder", + "kandinsky-community/kandinsky-2-1", ] styles = [ 'Fixed Astronaut', @@ -103,7 +111,7 @@ def read_history(): log.info(f'history: file="{fn}" records={len(history)}') -def write_history(model:str, style:str, image:str='', size:tuple=(0,0), duration:float=0, info:str='', error:str=''): +def write_history(model:str, style:str, image:str='', size:tuple=(0,0), duration:float=0, info:str=''): fn = os.path.join(output_folder, 'history.json') history.append({ 'model': model, @@ -113,7 +121,6 @@ def write_history(model:str, style:str, image:str='', size:tuple=(0,0), duration 'size': size, 'time': duration, 'info': info, - 'error': error, }) with open(fn, "w", encoding='utf8') as file: data = json.dumps(history) # pylint: disable=no-member @@ -137,9 +144,11 @@ def request(endpoint: str, dct: dict = None, method: str = 'POST'): def generate(): # pylint: disable=redefined-outer-name - for m, model in enumerate(models): + idx = 0 + for model, args in models.items(): + idx += 1 model_name = pathvalidate.sanitize_filename(model, replacement_text='_') - log.info(f'model: name="{model}" n={m+1}/{len(models)}') + log.info(f'model: n={idx+1}/{len(models)} name="{model}"') for s, style in enumerate(styles): try: model_name = pathvalidate.sanitize_filename(model, replacement_text='_') @@ -152,9 +161,12 @@ def generate(): # pylint: disable=redefined-outer-name if not loaded or not (model in loaded.get('checkpoint') or model in loaded.get('title') or model in loaded.get('name')): log.error(f' model: error="{model}"') continue - log.info(f' style: name="{style}" n={s+1}/{len(styles)} fn="{fn}"') t0 = time.time() - data = request('/sdapi/v1/txt2img', { 'styles': [style] }) + params = { 'styles': [style] } + for k, v in args.items(): + params[k] = v + log.info(f' style: n={s+1}/{len(styles)} name="{style}" args={params} fn="{fn}"') + data = request('/sdapi/v1/txt2img', params) t1 = time.time() if 'images' in data and len(data['images']) > 0: b64 = data['images'][0].split(',',1)[0] @@ -164,13 +176,13 @@ def generate(): # pylint: disable=redefined-outer-name image.save(fn) write_history(model=model, style=style, image=fn, size=image.size, duration=round(t1-t0, 3), info=info) else: - # write_history(model=model, style=style, duration=round(t1-t0, 3), error='no image') + # write_history(model=model, style=style, duration=round(t1-t0, 3), info='no image') log.error(f' model: error="{model}" style="{style}" no image') except Exception as e: if 'Connection refused' in str(e) or 'RemoteDisconnected' in str(e): log.error('server offline') os._exit(1) - # write_history(model=model, style=style, duration=round(t1-t0, 3), error=str(e)) + # write_history(model=model, style=style, duration=round(t1-t0, 3), info=str(e)) log.error(f' model: error="{model}" style="{style}" exception="{e}"') diff --git a/installer.py b/installer.py index 800edd0a7..6de6a025e 100644 --- a/installer.py +++ b/installer.py @@ -593,7 +593,7 @@ def check_diffusers(): t_start = time.time() if args.skip_all or args.skip_git: return - sha = '7b10e4ae65cc5830c581fba58638f5afb6e587cf' # diffusers commit hash + sha = '4a9dbd56f68214f0c949b8036a58c9ac3607f54e' # diffusers commit hash pkg = pkg_resources.working_set.by_key.get('diffusers', None) minor = int(pkg.version.split('.')[1] if pkg is not None else -1) cur = opts.get('diffusers_version', '') if minor > -1 else '' diff --git a/modules/sd_models.py b/modules/sd_models.py index 553da62eb..da863a924 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -323,7 +323,8 @@ def load_diffuser_force(model_type, checkpoint_info, diffusers_load_config, op=' allow_post_quant = False elif model_type in ['Chroma']: from pipelines.model_chroma import load_chroma - sd_model, allow_post_quant = load_chroma(checkpoint_info, diffusers_load_config) + sd_model = load_chroma(checkpoint_info, diffusers_load_config) + allow_post_quant = False elif model_type in ['Lumina 2']: from pipelines.model_lumina import load_lumina2 sd_model = load_lumina2(checkpoint_info, diffusers_load_config) @@ -376,6 +377,10 @@ def load_diffuser_force(model_type, checkpoint_info, diffusers_load_config, op=' from pipelines.model_qwen import load_qwen sd_model = load_qwen(checkpoint_info, diffusers_load_config) allow_post_quant = False + elif model_type in ['HunyuanDiT']: + from pipelines.model_hunyuandit import load_hunyuandit + sd_model = load_hunyuandit(checkpoint_info, diffusers_load_config) + allow_post_quant = False except Exception as e: shared.log.error(f'Load {op}: path="{checkpoint_info.path}" {e}') if debug_load: diff --git a/pipelines/generic.py b/pipelines/generic.py index 7b9039413..3801963d5 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -8,7 +8,7 @@ from modules import shared, devices, sd_models, model_quant debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None -def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", allow_quant=True): +def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", allow_quant=True, variant=None): load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True, allow_quant=allow_quant) quant_type = model_quant.get_quant_type(quant_args) @@ -45,6 +45,8 @@ def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", shared.log.debug(f'Load model: transformer="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') if subfolder is not None: load_args['subfolder'] = subfolder + if variant is not None: + load_args['variant'] = variant transformer = cls_name.from_pretrained( repo_id, cache_dir=shared.opts.hfcache_dir, @@ -56,7 +58,7 @@ def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", return transformer -def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder", allow_quant=True, allow_shared=True): +def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder", allow_quant=True, allow_shared=True, variant=None): load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True, allow_quant=allow_quant) quant_type = model_quant.get_quant_type(quant_args) text_encoder = None @@ -120,6 +122,8 @@ def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') if subfolder is not None: load_args['subfolder'] = subfolder + if variant is not None: + load_args['variant'] = variant text_encoder = cls_name.from_pretrained( repo_id, cache_dir=shared.opts.hfcache_dir, diff --git a/pipelines/model_chroma.py b/pipelines/model_chroma.py index 5efa9b436..b0a28fa69 100644 --- a/pipelines/model_chroma.py +++ b/pipelines/model_chroma.py @@ -30,5 +30,6 @@ def load_chroma(checkpoint_info, diffusers_load_config={}): diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["chroma"] = diffusers.ChromaImg2ImgPipeline del text_encoder del transformer + devices.torch_gc(force=True, reason='load') return pipe diff --git a/pipelines/model_hunyuandit.py b/pipelines/model_hunyuandit.py new file mode 100644 index 000000000..39d51a560 --- /dev/null +++ b/pipelines/model_hunyuandit.py @@ -0,0 +1,29 @@ +import transformers +import diffusers +from modules import shared, sd_models, devices, model_quant, sd_hijack_te +from pipelines import generic + + +def load_hunyuandit(checkpoint_info, diffusers_load_config={}): + repo_id = sd_models.path_to_repo(checkpoint_info) + sd_models.hf_auth_check(checkpoint_info) + + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config) + shared.log.debug(f'Load model: type=HunyuanDiT repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + + transformer = generic.load_transformer(repo_id, cls_name=diffusers.HunyuanDiT2DModel, load_config=diffusers_load_config) + text_encoder_2 = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder_2") + + pipe = diffusers.HunyuanDiTPipeline.from_pretrained( + repo_id, + transformer=transformer, + text_encoder_2=text_encoder_2, + cache_dir=shared.opts.diffusers_dir, + **load_args, + ) + + del text_encoder_2 + del transformer + sd_hijack_te.init_hijack(pipe) + devices.torch_gc(force=True, reason='load') + return pipe From dc8a72947dbbe27b7387dcbdeaa7da3ddbba40b2 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 11 Aug 2025 11:02:39 -0400 Subject: [PATCH 085/141] fix meissonic Signed-off-by: Vladimir Mandic --- cli/test-all-models.py | 22 +++++---- html/reference.json | 2 +- modules/sd_detect.py | 6 +++ modules/sd_models.py | 12 +++++ modules/sd_offload.py | 2 +- modules/sd_vae.py | 2 +- modules/sd_vae_taesd.py | 2 +- pipelines/generic.py | 18 +++++-- pipelines/meissonic/pipeline.py | 41 ++++++++-------- pipelines/meissonic/pipeline_img2img.py | 2 +- pipelines/meissonic/pipeline_inpaint.py | 2 +- pipelines/model_kandinsky.py | 65 +++++++++++++++++++++++++ 12 files changed, 134 insertions(+), 42 deletions(-) create mode 100644 pipelines/model_kandinsky.py diff --git a/cli/test-all-models.py b/cli/test-all-models.py index 163c193d6..a9853ebd1 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -1,14 +1,16 @@ #!/usr/bin/env python """ -Warning: +Warnings: - fal/AuraFlow-v0.3: layer_class_name=Linear layer_weight_shape=torch.Size([3072, 2, 1024]) weights_dtype=int8 unsupported - Kwai-Kolors/Kolors-diffusers: `set_input_embeddings` not auto‑handled for ChatGLMModel -Error: +- kandinsky-community/kandinsky-2-1: `get_input_embeddings` not auto‑handled for MultilingualCLIP +Errors: +- kandinsky-community/kandinsky-3: corrupt output - nvidia/Cosmos-Predict2-2B-Text2Image: mat1 and mat2 shapes cannot be multiplied (512x4096 and 1024x2048) - nvidia/Cosmos-Predict2-14B-Text2Image: mat1 and mat2 shapes cannot be multiplied (512x4096 and 1024x5120) - Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers: CUDA error: device-side assert triggered Other: -- HiDream-ai/HiDream-I1-Full: 30+s/it +- HiDream-ai/HiDream-I1-Full: very slow at 30+s/it """ import io @@ -54,10 +56,9 @@ models = { "OmniGen2/OmniGen2": {}, # "HiDream-ai/HiDream-I1-Full": {}, "Kwai-Kolors/Kolors-diffusers": {}, - "lodestones/Chroma1-HD": {}, - "vladmandic/chroma-unlocked-v50-annealed": {}, - "vladmandic/chroma-unlocked-v48": {}, - "vladmandic/chroma-unlocked-v48-detail-calibrated": {}, + # "kandinsky-community/kandinsky-3": {}, + "kandinsky-community/kandinsky-2-2-decoder": {}, + "kandinsky-community/kandinsky-2-1": {}, "Alpha-VLLM/Lumina-Next-SFT-diffusers": {}, "Alpha-VLLM/Lumina-Image-2.0": {}, "MeissonFlow/Meissonic": {}, @@ -68,14 +69,15 @@ models = { "Wan-AI/Wan2.1-T2V-1.3B-Diffusers": {}, "Wan-AI/Wan2.1-T2V-14B-Diffusers": {}, "stabilityai/stable-cascade": {}, + "lodestones/Chroma1-HD": {}, + "vladmandic/chroma-unlocked-v50-annealed": {}, + "vladmandic/chroma-unlocked-v48": {}, + "vladmandic/chroma-unlocked-v48-detail-calibrated": {}, } models_tbd = [ "black-forest-labs/FLUX.1-dev", "black-forest-labs/FLUX.1-Kontext-dev", "black-forest-labs/FLUX.1-Krea-dev", - "kandinsky-community/kandinsky-3", # TODO - "kandinsky-community/kandinsky-2-2-decoder", - "kandinsky-community/kandinsky-2-1", ] styles = [ 'Fixed Astronaut', diff --git a/html/reference.json b/html/reference.json index 873dcaefb..a8c3447bb 100644 --- a/html/reference.json +++ b/html/reference.json @@ -490,7 +490,7 @@ }, "Kandinsky 2.2": { "path": "kandinsky-community/kandinsky-2-2-decoder", - "desc": "Kandinsky 2.2 is a text-conditional diffusion model (+0.1!) based on unCLIP and latent diffusion, composed of a transformer-based image prior model, a unet diffusion model, and a decoder. Kandinsky 2.1 inherits best practices from Dall-E 2 and Latent diffusion while introducing some new ideas. It uses the CLIP model as a text and image encoder, and diffusion image prior (mapping) between latent spaces of CLIP modalities. This approach increases the visual performance of the model and unveils new horizons in blending images and text-guided image manipulation.", + "desc": "Kandinsky 2.2 is a text-conditional diffusion model (+0.1!) based on unCLIP and latent diffusion, composed of a transformer-based image prior model, a unet diffusion model, and a decoder. Kandinsky 2.2 inherits best practices from Dall-E 2 and Latent diffusion while introducing some new ideas. It uses the CLIP model as a text and image encoder, and diffusion image prior (mapping) between latent spaces of CLIP modalities. This approach increases the visual performance of the model and unveils new horizons in blending images and text-guided image manipulation.", "preview": "kandinsky-community--kandinsky-2-2-decoder.jpg", "extras": "width: 768, height: 768, sampler: Default" }, diff --git a/modules/sd_detect.py b/modules/sd_detect.py index b5767956e..a6169e5c3 100644 --- a/modules/sd_detect.py +++ b/modules/sd_detect.py @@ -103,6 +103,12 @@ def guess_by_name(fn, current_guess): return 'Bria' elif 'qwen' in fn.lower(): return 'Qwen' + elif 'kandinsky-2-1' in fn.lower(): + return 'Kandinsky 2.1' + elif 'kandinsky-2-2' in fn.lower(): + return 'Kandinsky 2.2' + elif 'kandinsky-3' in fn.lower(): + return 'Kandinsky 3.0' return current_guess diff --git a/modules/sd_models.py b/modules/sd_models.py index da863a924..ec11f2c1e 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -381,6 +381,18 @@ def load_diffuser_force(model_type, checkpoint_info, diffusers_load_config, op=' from pipelines.model_hunyuandit import load_hunyuandit sd_model = load_hunyuandit(checkpoint_info, diffusers_load_config) allow_post_quant = False + elif model_type in ['Kandinsky 2.1']: + from pipelines.model_kandinsky import load_kandinsky21 + sd_model = load_kandinsky21(checkpoint_info, diffusers_load_config) + allow_post_quant = True + elif model_type in ['Kandinsky 2.2']: + from pipelines.model_kandinsky import load_kandinsky22 + sd_model = load_kandinsky22(checkpoint_info, diffusers_load_config) + allow_post_quant = False + elif model_type in ['Kandinsky 3.0']: + from pipelines.model_kandinsky import load_kandinsky3 + sd_model = load_kandinsky3(checkpoint_info, diffusers_load_config) + allow_post_quant = False except Exception as e: shared.log.error(f'Load {op}: path="{checkpoint_info.path}" {e}') if debug_load: diff --git a/modules/sd_offload.py b/modules/sd_offload.py index c6156a3e8..4a253a428 100644 --- a/modules/sd_offload.py +++ b/modules/sd_offload.py @@ -16,7 +16,7 @@ debug_move = log.trace if debug else lambda *args, **kwargs: None offload_warn = ['sc', 'sd3', 'f1', 'h1', 'hunyuandit', 'auraflow', 'omnigen', 'omnigen2', 'cogview4', 'cosmos', 'chroma'] offload_post = ['h1'] offload_hook_instance = None -balanced_offload_exclude = ['CogView4Pipeline'] +balanced_offload_exclude = ['CogView4Pipeline', 'MeissonicPipeline'] accelerate_dtype_byte_size = None diff --git a/modules/sd_vae.py b/modules/sd_vae.py index 2578ff196..7986b2568 100644 --- a/modules/sd_vae.py +++ b/modules/sd_vae.py @@ -34,7 +34,7 @@ def get_vae_scale_factor(model=None): elif hasattr(model, 'config') and hasattr(model.config, 'vae_scale_factor'): vae_scale_factor = model.config.vae_scale_factor else: - shared.log.warning(f'VAE: cls={model.__class__.__name__ if model else "None"} scale=unknown') + # shared.log.warning(f'VAE: cls={model.__class__.__name__ if model else "None"} scale=unknown') vae_scale_factor = 8 if hasattr(model, 'patch_size'): patch_size = model.patch_size diff --git a/modules/sd_vae_taesd.py b/modules/sd_vae_taesd.py index 7fc9c35f7..a89dff777 100644 --- a/modules/sd_vae_taesd.py +++ b/modules/sd_vae_taesd.py @@ -45,7 +45,7 @@ def warn_once(msg, variant=None): global prev_warnings # pylint: disable=global-statement if not prev_warnings: prev_warnings = True - shared.log.error(f'Decode: type="taesd" variant="{variant}": {msg}') + shared.log.warning(f'Decode: type="taesd" variant="{variant}": {msg}') return Image.new('RGB', (8, 8), color = (0, 0, 0)) diff --git a/pipelines/generic.py b/pipelines/generic.py index 3801963d5..102b5e6b8 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -8,9 +8,10 @@ from modules import shared, devices, sd_models, model_quant debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None -def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", allow_quant=True, variant=None): +def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", allow_quant=True, variant=None, dtype=None): load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True, allow_quant=allow_quant) quant_type = model_quant.get_quant_type(quant_args) + dtype = dtype or devices.dtype local_file = None if shared.opts.sd_unet is not None and shared.opts.sd_unet != 'Default': @@ -27,7 +28,7 @@ def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", loader = cls_name.from_single_file if hasattr(cls_name, 'from_single_file') else cls_name.from_pretrained transformer = loader( local_file, - quantization_config=diffusers.GGUFQuantizationConfig(compute_dtype=devices.dtype), + quantization_config=diffusers.GGUFQuantizationConfig(compute_dtype=dtype), cache_dir=shared.opts.hfcache_dir, **load_args, ) @@ -43,6 +44,8 @@ def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None) else: shared.log.debug(f'Load model: transformer="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') + if dtype is not None: + load_args['torch_dtype'] = dtype if subfolder is not None: load_args['subfolder'] = subfolder if variant is not None: @@ -58,10 +61,11 @@ def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", return transformer -def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder", allow_quant=True, allow_shared=True, variant=None): +def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder", allow_quant=True, allow_shared=True, variant=None, dtype=None): load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True, allow_quant=allow_quant) quant_type = model_quant.get_quant_type(quant_args) text_encoder = None + dtype = dtype or devices.dtype # load from local file if specified local_file = None @@ -79,7 +83,7 @@ def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder ggml.install_gguf() text_encoder = cls_name.from_pretrained( gguf_file=local_file, - quantization_config=diffusers.GGUFQuantizationConfig(compute_dtype=devices.dtype), + quantization_config=diffusers.GGUFQuantizationConfig(compute_dtype=dtype), cache_dir=shared.opts.hfcache_dir, **load_args, ) @@ -104,12 +108,14 @@ def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="SVDQuant"') text_encoder = nunchaku.NunchakuT5EncoderModel.from_pretrained( repo_id, - torch_dtype=devices.dtype, + torch_dtype=dtype, ) text_encoder.quantization_method = 'SVDQuant' elif shared.opts.te_shared_t5: repo_id = 'Disty0/t5-xxl' shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') + if dtype is not None: + load_args['torch_dtype'] = dtype text_encoder = cls_name.from_pretrained( repo_id, cache_dir=shared.opts.hfcache_dir, @@ -120,6 +126,8 @@ def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder # load from repo if text_encoder is None: shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') + if dtype is not None: + load_args['torch_dtype'] = dtype if subfolder is not None: load_args['subfolder'] = subfolder if variant is not None: diff --git a/pipelines/meissonic/pipeline.py b/pipelines/meissonic/pipeline.py index 4f1bb05a2..34b894081 100644 --- a/pipelines/meissonic/pipeline.py +++ b/pipelines/meissonic/pipeline.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import sys from typing import Any, Callable, Dict, List, Optional, Tuple, Union import torch @@ -49,7 +48,7 @@ def _prepare_latent_image_ids(batch_size, height, width, device, dtype): return latent_image_ids.to(device=device, dtype=dtype) -class Pipeline(DiffusionPipeline): +class MeissonicPipeline(DiffusionPipeline): image_processor: VaeImageProcessor vqvae: VQModel tokenizer: CLIPTokenizer @@ -212,27 +211,27 @@ class Pipeline(DiffusionPipeline): width = self.transformer.config.sample_size * self.vae_scale_factor if prompt_embeds is None: - input_ids = self.tokenizer( - prompt, - return_tensors="pt", - padding="max_length", - truncation=True, - max_length=77, #self.tokenizer.model_max_length, - ).input_ids.to(self._execution_device) - # input_ids_t5 = self.tokenizer_t5( - # prompt, - # return_tensors="pt", - # padding="max_length", - # truncation=True, - # max_length=512, - # ).input_ids.to(self._execution_device) + input_ids = self.tokenizer( + prompt, + return_tensors="pt", + padding="max_length", + truncation=True, + max_length=77, #self.tokenizer.model_max_length, + ).input_ids.to(self._execution_device) + # input_ids_t5 = self.tokenizer_t5( + # prompt, + # return_tensors="pt", + # padding="max_length", + # truncation=True, + # max_length=512, + # ).input_ids.to(self._execution_device) - outputs = self.text_encoder(input_ids, return_dict=True, output_hidden_states=True) - # outputs_t5 = self.text_encoder_t5(input_ids_t5, decoder_input_ids = input_ids_t5 ,return_dict=True, output_hidden_states=True) - prompt_embeds = outputs.text_embeds - encoder_hidden_states = outputs.hidden_states[-2] - # encoder_hidden_states = outputs_t5.encoder_hidden_states[-2] + outputs = self.text_encoder(input_ids, return_dict=True, output_hidden_states=True) + # outputs_t5 = self.text_encoder_t5(input_ids_t5, decoder_input_ids = input_ids_t5 ,return_dict=True, output_hidden_states=True) + prompt_embeds = outputs.text_embeds + encoder_hidden_states = outputs.hidden_states[-2] + # encoder_hidden_states = outputs_t5.encoder_hidden_states[-2] prompt_embeds = prompt_embeds.repeat(num_images_per_prompt, 1) encoder_hidden_states = encoder_hidden_states.repeat(num_images_per_prompt, 1, 1) diff --git a/pipelines/meissonic/pipeline_img2img.py b/pipelines/meissonic/pipeline_img2img.py index 13e5c3717..2aaf9d987 100644 --- a/pipelines/meissonic/pipeline_img2img.py +++ b/pipelines/meissonic/pipeline_img2img.py @@ -46,7 +46,7 @@ def _prepare_latent_image_ids(batch_size, height, width, device, dtype): return latent_image_ids.to(device=device, dtype=dtype) -class Img2ImgPipeline(DiffusionPipeline): +class MeissonicImg2ImgPipeline(DiffusionPipeline): image_processor: VaeImageProcessor vqvae: VQModel tokenizer: CLIPTokenizer diff --git a/pipelines/meissonic/pipeline_inpaint.py b/pipelines/meissonic/pipeline_inpaint.py index d405afa53..aa352d9b4 100644 --- a/pipelines/meissonic/pipeline_inpaint.py +++ b/pipelines/meissonic/pipeline_inpaint.py @@ -43,7 +43,7 @@ def _prepare_latent_image_ids(batch_size, height, width, device, dtype): return latent_image_ids.to(device=device, dtype=dtype) -class InpaintPipeline(DiffusionPipeline): +class MeissonicInpaintPipeline(DiffusionPipeline): image_processor: VaeImageProcessor vqvae: VQModel tokenizer: CLIPTokenizer diff --git a/pipelines/model_kandinsky.py b/pipelines/model_kandinsky.py new file mode 100644 index 000000000..0d5ad0013 --- /dev/null +++ b/pipelines/model_kandinsky.py @@ -0,0 +1,65 @@ +import transformers +import diffusers +from modules import shared, sd_models, devices, model_quant, sd_hijack_te +from pipelines import generic + + +def load_kandinsky21(checkpoint_info, diffusers_load_config={}): + repo_id = sd_models.path_to_repo(checkpoint_info) + sd_models.hf_auth_check(checkpoint_info) + + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config) + shared.log.debug(f'Load model: type=Kandinsky21 repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + pipe = diffusers.KandinskyCombinedPipeline.from_pretrained( + repo_id, + cache_dir=shared.opts.diffusers_dir, + **load_args, + ) + sd_hijack_te.init_hijack(pipe) + devices.torch_gc(force=True, reason='load') + return pipe + + +def load_kandinsky22(checkpoint_info, diffusers_load_config={}): + repo_id = sd_models.path_to_repo(checkpoint_info) + sd_models.hf_auth_check(checkpoint_info) + + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config) + shared.log.debug(f'Load model: type=Kandinsky22 repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + pipe = diffusers.KandinskyV22CombinedPipeline.from_pretrained( + repo_id, + cache_dir=shared.opts.diffusers_dir, + **load_args, + ) + sd_hijack_te.init_hijack(pipe) + devices.torch_gc(force=True, reason='load') + return pipe + + +def load_kandinsky3(checkpoint_info, diffusers_load_config={}): + repo_id = sd_models.path_to_repo(checkpoint_info) + sd_models.hf_auth_check(checkpoint_info) + + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config) + shared.log.debug(f'Load model: type=Kandinsky30 repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + + unet = generic.load_transformer(repo_id, cls_name=diffusers.Kandinsky3UNet, load_config=diffusers_load_config, subfolder="unet", variant="fp16") + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder", variant="fp16") + + pipe = diffusers.Kandinsky3Pipeline.from_pretrained( + repo_id, + unet=unet, + text_encoder=text_encoder, + variant="fp16", + cache_dir=shared.opts.diffusers_dir, + **load_args, + ) + pipe.task_args = { + 'output_type': 'np', + } + + del text_encoder + del unet + sd_hijack_te.init_hijack(pipe) + devices.torch_gc(force=True, reason='load') + return pipe From c92e329234cc713dfda76650800b4a3d717f0b89 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 11 Aug 2025 11:45:17 -0400 Subject: [PATCH 086/141] fix cosmos-t2i Signed-off-by: Vladimir Mandic --- cli/test-all-models.py | 14 ++++++++------ pipelines/model_cosmos.py | 3 ++- pipelines/model_stablecascade.py | 1 + pipelines/model_wanai.py | 1 + 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cli/test-all-models.py b/cli/test-all-models.py index a9853ebd1..288c0e0c7 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -47,16 +47,11 @@ models = { "fal/AuraFlow-v0.2": {}, "zai-org/CogView4-6B": {}, "zai-org/CogView3-Plus-3B": {}, - # "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers": {}, - # "nvidia/Cosmos-Predict2-2B-Text2Image": {}, - # "nvidia/Cosmos-Predict2-14B-Text2Image": {}, "Qwen/Qwen-Image": {}, "vladmandic/Qwen-Lightning": {}, "Shitao/OmniGen-v1-diffusers": {}, "OmniGen2/OmniGen2": {}, - # "HiDream-ai/HiDream-I1-Full": {}, "Kwai-Kolors/Kolors-diffusers": {}, - # "kandinsky-community/kandinsky-3": {}, "kandinsky-community/kandinsky-2-2-decoder": {}, "kandinsky-community/kandinsky-2-1": {}, "Alpha-VLLM/Lumina-Next-SFT-diffusers": {}, @@ -66,9 +61,16 @@ models = { "Efficient-Large-Model/SANA1.5_4.8B_1024px_diffusers": {}, "PixArt-alpha/PixArt-XL-2-1024-MS": {}, "PixArt-alpha/PixArt-Sigma-XL-2-1024-MS": {}, + "stabilityai/stable-cascade": {}, + "nvidia/Cosmos-Predict2-2B-Text2Image": {}, + "nvidia/Cosmos-Predict2-14B-Text2Image": {}, + # "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers": {}, + # "kandinsky-community/kandinsky-3": {}, + # "HiDream-ai/HiDream-I1-Full": {}, "Wan-AI/Wan2.1-T2V-1.3B-Diffusers": {}, "Wan-AI/Wan2.1-T2V-14B-Diffusers": {}, - "stabilityai/stable-cascade": {}, + "Wan-AI/Wan2.2-TI2V-5B-Diffusers": {}, + "Wan-AI/Wan2.2-T2V-A14B-Diffusers": {}, "lodestones/Chroma1-HD": {}, "vladmandic/chroma-unlocked-v50-annealed": {}, "vladmandic/chroma-unlocked-v48": {}, diff --git a/pipelines/model_cosmos.py b/pipelines/model_cosmos.py index 1c1c9cee9..839c2d9c5 100644 --- a/pipelines/model_cosmos.py +++ b/pipelines/model_cosmos.py @@ -12,7 +12,8 @@ def load_cosmos_t2i(checkpoint_info, diffusers_load_config={}): shared.log.debug(f'Load model: type=Cosmos repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') transformer = generic.load_transformer(repo_id, cls_name=diffusers.CosmosTransformer3DModel, load_config=diffusers_load_config, subfolder="transformer") - text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder") + repo_te = 'nvidia/Cosmos-Predict2-2B-Text2Image' if 'Cosmos-Predict2-14B-Text2Image' in repo_id else repo_id + text_encoder = generic.load_text_encoder(repo_te, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder", allow_shared=False) # cosmos does use standard t5 safety_checker = Fake_safety_checker() pipe = diffusers.Cosmos2TextToImagePipeline.from_pretrained( diff --git a/pipelines/model_stablecascade.py b/pipelines/model_stablecascade.py index fb780c0f2..60d8ca87c 100644 --- a/pipelines/model_stablecascade.py +++ b/pipelines/model_stablecascade.py @@ -190,6 +190,7 @@ class StableCascadeDecoderPipelineFixed(diffusers.StableCascadeDecoderPipeline): ): shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model) # 0. Define commonly used variables + guidance_scale = guidance_scale or 0.0 self.guidance_scale = guidance_scale self.do_classifier_free_guidance = self.guidance_scale > 1 device = self._execution_device diff --git a/pipelines/model_wanai.py b/pipelines/model_wanai.py index 49503c670..3edbfa67d 100644 --- a/pipelines/model_wanai.py +++ b/pipelines/model_wanai.py @@ -41,6 +41,7 @@ def load_transformer(repo_id, diffusers_load_config={}, subfolder='transformer') def load_text_encoder(repo_id, diffusers_load_config={}): load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) + repo_id = 'Wan-AI/Wan2.1-T2V-1.3B-Diffusers' if 'Wan2.' in repo_id else repo_id # always use shared umt5 shared.log.debug(f'Load model: type=WanAI te="{repo_id}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') text_encoder = transformers.UMT5EncoderModel.from_pretrained( repo_id, From 87bd3471167b6f80697e8837ed3f4565b8bc7b6d Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 11 Aug 2025 13:38:06 -0400 Subject: [PATCH 087/141] cleanup flux loader Signed-off-by: Vladimir Mandic --- cli/test-all-models.py | 62 +-- html/reference.json | 6 + modules/model_quant.py | 4 + modules/sd_models.py | 13 +- pipelines/flux/flux_bnb.py | 25 ++ pipelines/flux/flux_legacy_loader.py | 360 ++++++++++++++++ .../{model_flux_nf4.py => flux/flux_nf4.py} | 0 pipelines/flux/flux_nunchaku.py | 29 ++ pipelines/flux/flux_quanto.py | 73 ++++ pipelines/generic.py | 243 ++++++----- pipelines/meissonic/test.py | 4 +- pipelines/model_chroma.py | 4 - pipelines/model_flux.py | 390 +++--------------- pipelines/model_hunyuandit.py | 3 +- pipelines/model_kolors.py | 11 +- pipelines/model_lumina.py | 10 +- pipelines/model_meissonic.py | 16 +- pipelines/model_omnigen.py | 3 - pipelines/model_omnigen2.py | 3 - 19 files changed, 739 insertions(+), 520 deletions(-) create mode 100644 pipelines/flux/flux_bnb.py create mode 100644 pipelines/flux/flux_legacy_loader.py rename pipelines/{model_flux_nf4.py => flux/flux_nf4.py} (100%) create mode 100644 pipelines/flux/flux_nunchaku.py create mode 100644 pipelines/flux/flux_quanto.py diff --git a/cli/test-all-models.py b/cli/test-all-models.py index 288c0e0c7..728bc9930 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -2,13 +2,11 @@ """ Warnings: - fal/AuraFlow-v0.3: layer_class_name=Linear layer_weight_shape=torch.Size([3072, 2, 1024]) weights_dtype=int8 unsupported -- Kwai-Kolors/Kolors-diffusers: `set_input_embeddings` not auto‑handled for ChatGLMModel -- kandinsky-community/kandinsky-2-1: `get_input_embeddings` not auto‑handled for MultilingualCLIP +- Kwai-Kolors/Kolors-diffusers: set_input_embeddings not autohandled for ChatGLMModel +- kandinsky-community/kandinsky-2-1: get_input_embeddings not autohandled for MultilingualCLIP Errors: - kandinsky-community/kandinsky-3: corrupt output -- nvidia/Cosmos-Predict2-2B-Text2Image: mat1 and mat2 shapes cannot be multiplied (512x4096 and 1024x2048) -- nvidia/Cosmos-Predict2-14B-Text2Image: mat1 and mat2 shapes cannot be multiplied (512x4096 and 1024x5120) -- Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers: CUDA error: device-side assert triggered +- Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers: CUDA error device-side assert triggered Other: - HiDream-ai/HiDream-I1-Full: very slow at 30+s/it """ @@ -64,7 +62,11 @@ models = { "stabilityai/stable-cascade": {}, "nvidia/Cosmos-Predict2-2B-Text2Image": {}, "nvidia/Cosmos-Predict2-14B-Text2Image": {}, - # "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers": {}, + "black-forest-labs/FLUX.1-dev": {}, + "black-forest-labs/FLUX.1-Kontext-dev": {}, + "black-forest-labs/FLUX.1-Krea-dev": {}, + "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers": {}, + "Tencent-Hunyuan/HunyuanDiT-v1.1-Diffusers": {}, # "kandinsky-community/kandinsky-3": {}, # "HiDream-ai/HiDream-I1-Full": {}, "Wan-AI/Wan2.1-T2V-1.3B-Diffusers": {}, @@ -76,11 +78,6 @@ models = { "vladmandic/chroma-unlocked-v48": {}, "vladmandic/chroma-unlocked-v48-detail-calibrated": {}, } -models_tbd = [ - "black-forest-labs/FLUX.1-dev", - "black-forest-labs/FLUX.1-Kontext-dev", - "black-forest-labs/FLUX.1-Krea-dev", -] styles = [ 'Fixed Astronaut', ] @@ -115,7 +112,7 @@ def read_history(): log.info(f'history: file="{fn}" records={len(history)}') -def write_history(model:str, style:str, image:str='', size:tuple=(0,0), duration:float=0, info:str=''): +def write_history(model:str, style:str, image:str='', size:tuple=(0,0), generate:float=0, load:float=0, info:str=''): fn = os.path.join(output_folder, 'history.json') history.append({ 'model': model, @@ -123,7 +120,8 @@ def write_history(model:str, style:str, image:str='', size:tuple=(0,0), duration 'style': style, 'image': image, 'size': size, - 'time': duration, + 'time': generate, + 'load': load, 'info': info, }) with open(fn, "w", encoding='utf8') as file: @@ -147,12 +145,17 @@ def request(endpoint: str, dct: dict = None, method: str = 'POST'): return req.json() -def generate(): # pylint: disable=redefined-outer-name - idx = 0 +def main(): # pylint: disable=redefined-outer-name + idx_model = 0 + idx_images = 0 + t_generate0 = time.time() + log.info(f'generate: models={len(models)} styles={len(styles)}') for model, args in models.items(): - idx += 1 + t_model0 = time.time() + idx_model += 1 model_name = pathvalidate.sanitize_filename(model, replacement_text='_') - log.info(f'model: n={idx+1}/{len(models)} name="{model}"') + log.info(f'model: n={idx_model+1}/{len(models)} name="{model}"') + idx_style = 0 for s, style in enumerate(styles): try: model_name = pathvalidate.sanitize_filename(model, replacement_text='_') @@ -160,39 +163,46 @@ def generate(): # pylint: disable=redefined-outer-name fn = os.path.join(output_folder, f'{model_name}__{style_name}.jpg') if os.path.exists(fn): continue + t_load0 = time.time() request(f'/sdapi/v1/checkpoint?sd_model_checkpoint={model}', method='POST') loaded = request('/sdapi/v1/checkpoint', method='GET') + t_load1 = time.time() if not loaded or not (model in loaded.get('checkpoint') or model in loaded.get('title') or model in loaded.get('name')): log.error(f' model: error="{model}"') continue - t0 = time.time() + t_style0 = time.time() params = { 'styles': [style] } for k, v in args.items(): params[k] = v log.info(f' style: n={s+1}/{len(styles)} name="{style}" args={params} fn="{fn}"') data = request('/sdapi/v1/txt2img', params) - t1 = time.time() + t_style1 = time.time() if 'images' in data and len(data['images']) > 0: + idx_style += 1 + idx_images += 1 b64 = data['images'][0].split(',',1)[0] image = Image.open(io.BytesIO(base64.b64decode(b64))) info = data['info'] - log.info(f' image: size={image.width}x{image.height} time={t1-t0:.2f} info={len(info)}') + log.info(f' image: size={image.width}x{image.height} time={t_style1-t_style0:.2f} info={len(info)}') image.save(fn) - write_history(model=model, style=style, image=fn, size=image.size, duration=round(t1-t0, 3), info=info) + write_history(model=model, style=style, image=fn, size=image.size, generate=round(t_style1-t_style0, 3), load=round(t_load1-t_load0, 3), info=info) else: - # write_history(model=model, style=style, duration=round(t1-t0, 3), info='no image') log.error(f' model: error="{model}" style="{style}" no image') except Exception as e: if 'Connection refused' in str(e) or 'RemoteDisconnected' in str(e): log.error('server offline') os._exit(1) - # write_history(model=model, style=style, duration=round(t1-t0, 3), info=str(e)) log.error(f' model: error="{model}" style="{style}" exception="{e}"') + t_model1 = time.time() + if idx_style > 0: + log.info(f'model: name="{model}" images={idx_style} time={t_model1-t_model0:.2f}') + t_generate1 = time.time() + if idx_images > 0: + log.info(f'generate: models={idx_model} images={idx_images} time={t_generate1-t_generate0:.2f}') if __name__ == "__main__": log.info('test-all-models') - log.info(f'output="{output_folder}" models={len(models)} styles={len(styles)}') + log.info(f'output="{output_folder}"') read_history() - generate() - log.info('done...') + main() diff --git a/html/reference.json b/html/reference.json index a8c3447bb..5fd79f36a 100644 --- a/html/reference.json +++ b/html/reference.json @@ -429,6 +429,12 @@ "preview": "Tencent-Hunyuan--HunyuanDiT-v1.2-Diffusers.jpg", "extras": "sampler: Default, cfg_scale: 2.0" }, + "Tencent HunyuanDiT 1.1": { + "path": "Tencent-Hunyuan/HunyuanDiT-v1.1-Diffusers", + "desc": "Hunyuan-DiT : A Powerful Multi-Resolution Diffusion Transformer with Fine-Grained Chinese Understanding.", + "preview": "Tencent-Hunyuan--HunyuanDiT-v1.2-Diffusers.jpg", + "extras": "sampler: Default, cfg_scale: 2.0" + }, "AlphaVLLM Lumina Next SFT": { "path": "Alpha-VLLM/Lumina-Next-SFT-diffusers", diff --git a/modules/model_quant.py b/modules/model_quant.py index a2cfbecbf..00f42e016 100644 --- a/modules/model_quant.py +++ b/modules/model_quant.py @@ -642,11 +642,15 @@ def get_dit_args(load_config:dict={}, module:str=None, device_map:bool=False, al def do_post_load_quant(sd_model, allow=True): from modules import shared if shared.opts.sdnq_quantize_weights and (shared.opts.sdnq_quantize_mode == 'post' or (allow and shared.opts.sdnq_quantize_mode == 'auto')): + shared.log.debug('Load model: post_quant=sdnq') sd_model = sdnq_quantize_weights(sd_model) if len(shared.opts.optimum_quanto_weights) > 0: + shared.log.debug('Load model: post_quant=quanto') sd_model = optimum_quanto_weights(sd_model) if shared.opts.torchao_quantization and (shared.opts.torchao_quantization_mode == 'post' or (allow and shared.opts.torchao_quantization_mode == 'auto')): + shared.log.debug('Load model: post_quant=torchao') sd_model = torchao_quantization(sd_model) if shared.opts.layerwise_quantization: + shared.log.debug('Load model: post_quant=layerwise') apply_layerwise(sd_model) return sd_model diff --git a/modules/sd_models.py b/modules/sd_models.py index ec11f2c1e..817367b3a 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -29,6 +29,7 @@ debug_load = os.environ.get('SD_LOAD_DEBUG', None) debug_process = log.trace if os.environ.get('SD_PROCESS_DEBUG', None) is not None else lambda *args, **kwargs: None diffusers_version = int(diffusers.__version__.split('.')[1]) checkpoint_tiles = checkpoint_titles # legacy compatibility +allow_post_quant = None pipe_switch_task_exclude = [ 'AnimateDiffPipeline', 'AnimateDiffSDXLPipeline', 'FluxControlPipeline', @@ -275,7 +276,7 @@ def load_diffuser_initial(diffusers_load_config, op='model'): def load_diffuser_force(model_type, checkpoint_info, diffusers_load_config, op='model'): sd_model = None - allow_post_quant = True + global allow_post_quant # pylint: disable=global-statement unload_model_weights(op=op) shared.sd_model = None try: @@ -316,7 +317,8 @@ def load_diffuser_force(model_type, checkpoint_info, diffusers_load_config, op=' allow_post_quant = True elif model_type in ['FLUX']: from pipelines.model_flux import load_flux - sd_model, allow_post_quant = load_flux(checkpoint_info, diffusers_load_config) + sd_model = load_flux(checkpoint_info, diffusers_load_config) + allow_post_quant = False elif model_type in ['FLEX']: from pipelines.model_flex import load_flex sd_model = load_flex(checkpoint_info, diffusers_load_config) @@ -398,7 +400,7 @@ def load_diffuser_force(model_type, checkpoint_info, diffusers_load_config, op=' if debug_load: errors.display(e, 'Load') return None, True - return sd_model, allow_post_quant + return sd_model def load_diffuser_folder(model_type, pipeline, checkpoint_info, diffusers_load_config, op='model'): @@ -561,6 +563,8 @@ def set_defaults(sd_model, checkpoint_info): def load_diffuser(checkpoint_info=None, op='model', revision=None): # pylint: disable=unused-argument + global allow_post_quant # pylint: disable=global-statement + allow_post_quant = True # assume default logging.getLogger("diffusers").setLevel(logging.ERROR) timer.load.record("diffusers") diffusers_load_config = { @@ -589,7 +593,6 @@ def load_diffuser(checkpoint_info=None, op='model', revision=None): # pylint: di return sd_model = None - allow_post_quant = True try: # initial load only if sd_model is None: @@ -621,7 +624,7 @@ def load_diffuser(checkpoint_info=None, op='model', revision=None): # pylint: di # load with custom loader if sd_model is None: - sd_model, allow_post_quant = load_diffuser_force(model_type, checkpoint_info, diffusers_load_config, op) + sd_model = load_diffuser_force(model_type, checkpoint_info, diffusers_load_config, op) if sd_model is not None and not sd_model: shared.log.error(f'Load {op}: type="{model_type}" pipeline="{pipeline}" not loaded') return diff --git a/pipelines/flux/flux_bnb.py b/pipelines/flux/flux_bnb.py new file mode 100644 index 000000000..777678af1 --- /dev/null +++ b/pipelines/flux/flux_bnb.py @@ -0,0 +1,25 @@ +import diffusers +import transformers +from modules import devices, model_quant + + +def load_flux_bnb(checkpoint_info, diffusers_load_config): # pylint: disable=unused-argument + transformer = None + if isinstance(checkpoint_info, str): + repo_path = checkpoint_info + else: + repo_path = checkpoint_info.path + model_quant.load_bnb('Load model: type=FLUX') + quant = model_quant.get_quant(repo_path) + if quant == 'fp8': + quantization_config = transformers.BitsAndBytesConfig(load_in_8bit=True, bnb_4bit_compute_dtype=devices.dtype) + transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) + elif quant == 'fp4': + quantization_config = transformers.BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=devices.dtype, bnb_4bit_quant_type= 'fp4') + transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) + elif quant == 'nf4': + quantization_config = transformers.BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=devices.dtype, bnb_4bit_quant_type= 'nf4') + transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) + else: + transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config) + return transformer diff --git a/pipelines/flux/flux_legacy_loader.py b/pipelines/flux/flux_legacy_loader.py new file mode 100644 index 000000000..6b6f9d294 --- /dev/null +++ b/pipelines/flux/flux_legacy_loader.py @@ -0,0 +1,360 @@ +import os +import json +import torch +import diffusers +import transformers +from safetensors.torch import load_file +from huggingface_hub import hf_hub_download +from modules import shared, errors, devices, sd_models, sd_unet, model_te, model_quant, sd_hijack_te + + +debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None + + +def load_flux_quanto(checkpoint_info): + transformer, text_encoder_2 = None, None + quanto = model_quant.load_quanto('Load model: type=FLUX') + + if isinstance(checkpoint_info, str): + repo_path = checkpoint_info + else: + repo_path = checkpoint_info.path + + try: + quantization_map = os.path.join(repo_path, "transformer", "quantization_map.json") + debug(f'Load model: type=FLUX quantization map="{quantization_map}" repo="{checkpoint_info.name}" component="transformer"') + if not os.path.exists(quantization_map): + repo_id = sd_models.path_to_repo(checkpoint_info) + quantization_map = hf_hub_download(repo_id, subfolder='transformer', filename='quantization_map.json', cache_dir=shared.opts.diffusers_dir) + with open(quantization_map, "r", encoding='utf8') as f: + quantization_map = json.load(f) + state_dict = load_file(os.path.join(repo_path, "transformer", "diffusion_pytorch_model.safetensors")) + dtype = state_dict['context_embedder.bias'].dtype + with torch.device("meta"): + transformer = diffusers.FluxTransformer2DModel.from_config(os.path.join(repo_path, "transformer", "config.json")).to(dtype=dtype) + quanto.requantize(transformer, state_dict, quantization_map, device=torch.device("cpu")) + transformer_dtype = transformer.dtype + if transformer_dtype != devices.dtype: + try: + transformer = transformer.to(dtype=devices.dtype) + except Exception: + shared.log.error(f"Load model: type=FLUX Failed to cast transformer to {devices.dtype}, set dtype to {transformer_dtype}") + except Exception as e: + shared.log.error(f"Load model: type=FLUX failed to load Quanto transformer: {e}") + if debug: + errors.display(e, 'FLUX Quanto:') + + try: + quantization_map = os.path.join(repo_path, "text_encoder_2", "quantization_map.json") + debug(f'Load model: type=FLUX quantization map="{quantization_map}" repo="{checkpoint_info.name}" component="text_encoder_2"') + if not os.path.exists(quantization_map): + repo_id = sd_models.path_to_repo(checkpoint_info) + quantization_map = hf_hub_download(repo_id, subfolder='text_encoder_2', filename='quantization_map.json', cache_dir=shared.opts.diffusers_dir) + with open(quantization_map, "r", encoding='utf8') as f: + quantization_map = json.load(f) + with open(os.path.join(repo_path, "text_encoder_2", "config.json"), encoding='utf8') as f: + t5_config = transformers.T5Config(**json.load(f)) + state_dict = load_file(os.path.join(repo_path, "text_encoder_2", "model.safetensors")) + dtype = state_dict['encoder.block.0.layer.0.SelfAttention.relative_attention_bias.weight'].dtype + with torch.device("meta"): + text_encoder_2 = transformers.T5EncoderModel(t5_config).to(dtype=dtype) + quanto.requantize(text_encoder_2, state_dict, quantization_map, device=torch.device("cpu")) + text_encoder_2_dtype = text_encoder_2.dtype + if text_encoder_2_dtype != devices.dtype: + try: + text_encoder_2 = text_encoder_2.to(dtype=devices.dtype) + except Exception: + shared.log.error(f"Load model: type=FLUX Failed to cast text encoder to {devices.dtype}, set dtype to {text_encoder_2_dtype}") + except Exception as e: + shared.log.error(f"Load model: type=FLUX failed to load Quanto text encoder: {e}") + if debug: + errors.display(e, 'FLUX Quanto:') + + return transformer, text_encoder_2 + + +def load_flux_bnb(checkpoint_info, diffusers_load_config): # pylint: disable=unused-argument + transformer, text_encoder_2 = None, None + if isinstance(checkpoint_info, str): + repo_path = checkpoint_info + else: + repo_path = checkpoint_info.path + model_quant.load_bnb('Load model: type=FLUX') + quant = model_quant.get_quant(repo_path) + try: + if quant == 'fp8': + quantization_config = transformers.BitsAndBytesConfig(load_in_8bit=True, bnb_4bit_compute_dtype=devices.dtype) + debug(f'Quantization: {quantization_config}') + transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) + elif quant == 'fp4': + quantization_config = transformers.BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=devices.dtype, bnb_4bit_quant_type= 'fp4') + debug(f'Quantization: {quantization_config}') + transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) + elif quant == 'nf4': + quantization_config = transformers.BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=devices.dtype, bnb_4bit_quant_type= 'nf4') + debug(f'Quantization: {quantization_config}') + transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) + else: + transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config) + except Exception as e: + shared.log.error(f"Load model: type=FLUX failed to load BnB transformer: {e}") + transformer, text_encoder_2 = None, None + if debug: + errors.display(e, 'FLUX:') + return transformer, text_encoder_2 + + +def load_quants(kwargs, repo_id, cache_dir, allow_quant): # pylint: disable=unused-argument + try: + diffusers_load_config = { + "torch_dtype": devices.dtype, + "cache_dir": cache_dir, + } + if 'transformer' not in kwargs and model_quant.check_nunchaku('Model'): + import nunchaku + nunchaku_precision = nunchaku.utils.get_precision() + nunchaku_repo = None + if 'flux.1-kontext' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/nunchaku-flux.1-kontext-dev/svdq-{nunchaku_precision}_r32-flux.1-kontext-dev.safetensors" + elif 'flux.1-dev' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/nunchaku-flux.1-dev/svdq-{nunchaku_precision}_r32-flux.1-dev.safetensors" + elif 'flux.1-schnell' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/nunchaku-flux.1-schnell/svdq-{nunchaku_precision}_r32-flux.1-schnell.safetensors" + elif 'flux.1-fill' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/svdq-fp4-flux.1-fill-dev/svdq-{nunchaku_precision}_r32-flux.1-schnell.safetensors" + elif 'flux.1-depth' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/svdq-int4-flux.1-depth-dev/svdq-{nunchaku_precision}_r32-flux.1-schnell.safetensors" + elif 'shuttle' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/nunchaku-shuttle-jaguar/svdq-{nunchaku_precision}_r32-shuttle-jaguar.safetensors" + else: + shared.log.error(f'Load module: quant=Nunchaku module=transformer repo="{repo_id}" unsupported') + if nunchaku_repo is not None: + shared.log.debug(f'Load module: quant=Nunchaku module=transformer repo="{nunchaku_repo}" precision={nunchaku_precision} offload={shared.opts.nunchaku_offload} attention={shared.opts.nunchaku_attention}') + kwargs['transformer'] = nunchaku.NunchakuFluxTransformer2dModel.from_pretrained(nunchaku_repo, offload=shared.opts.nunchaku_offload, torch_dtype=devices.dtype) + kwargs['transformer'].quantization_method = 'SVDQuant' + if shared.opts.nunchaku_attention: + kwargs['transformer'].set_attention_impl("nunchaku-fp16") + if 'transformer' not in kwargs and model_quant.check_quant('Model'): + load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True) + kwargs['transformer'] = diffusers.FluxTransformer2DModel.from_pretrained(repo_id, subfolder="transformer", **load_args, **quant_args) + if 'text_encoder_2' not in kwargs and model_quant.check_nunchaku('TE'): + import nunchaku + nunchaku_precision = nunchaku.utils.get_precision() + nunchaku_repo = 'mit-han-lab/nunchaku-t5/awq-int4-flux.1-t5xxl.safetensors' + shared.log.debug(f'Load module: quant=Nunchaku module=t5 repo="{nunchaku_repo}" precision={nunchaku_precision}') + kwargs['text_encoder_2'] = nunchaku.NunchakuT5EncoderModel.from_pretrained(nunchaku_repo, torch_dtype=devices.dtype) + kwargs['text_encoder_2'].quantization_method = 'SVDQuant' + if 'text_encoder_2' not in kwargs and model_quant.check_quant('TE'): + load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) + kwargs['text_encoder_2'] = transformers.T5EncoderModel.from_pretrained(repo_id, subfolder="text_encoder_2", **load_args, **quant_args) + except Exception as e: + shared.log.error(f'Quantization: {e}') + errors.display(e, 'Quantization:') + return kwargs + + +def load_transformer(file_path): # triggered by opts.sd_unet change + if file_path is None or not os.path.exists(file_path): + return None + transformer = None + quant = model_quant.get_quant(file_path) + diffusers_load_config = { + "torch_dtype": devices.dtype, + "cache_dir": shared.opts.hfcache_dir, + } + if quant is not None and quant != 'none': + shared.log.info(f'Load module: type=UNet/Transformer file="{file_path}" offload={shared.opts.diffusers_offload_mode} prequant={quant} dtype={devices.dtype}') + if 'gguf' in file_path.lower(): + from modules import ggml + _transformer = ggml.load_gguf(file_path, cls=diffusers.FluxTransformer2DModel, compute_dtype=devices.dtype) + if _transformer is not None: + transformer = _transformer + elif quant == "fp8": + _transformer = model_quant.load_fp8_model_layerwise(file_path, diffusers.FluxTransformer2DModel.from_single_file, diffusers_load_config) + if _transformer is not None: + transformer = _transformer + elif quant in {'qint8', 'qint4'}: + _transformer, _text_encoder_2 = load_flux_quanto(file_path) + if _transformer is not None: + transformer = _transformer + elif quant in {'fp8', 'fp4', 'nf4'}: + _transformer, _text_encoder_2 = load_flux_bnb(file_path, diffusers_load_config) + if _transformer is not None: + transformer = _transformer + elif 'nf4' in quant: + from pipelines.flux.flux_nf4 import load_flux_nf4 + _transformer, _text_encoder_2 = load_flux_nf4(file_path, prequantized=True) + if _transformer is not None: + transformer = _transformer + else: + quant_args = model_quant.create_bnb_config({}) + if quant_args: + shared.log.info(f'Load module: type=Flux transformer file="{file_path}" offload={shared.opts.diffusers_offload_mode} quant=bnb dtype={devices.dtype}') + from pipelines.flux.flux_nf4 import load_flux_nf4 + transformer, _text_encoder_2 = load_flux_nf4(file_path, prequantized=False) + if transformer is not None: + return transformer + load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True) + shared.log.debug(f'Load model: type=Flux transformer file="{file_path}" offload={shared.opts.diffusers_offload_mode} args={load_args}') + transformer = diffusers.FluxTransformer2DModel.from_single_file(file_path, **load_args, **quant_args) + if transformer is None: + shared.log.error('Failed to load UNet model') + shared.opts.sd_unet = 'Default' + return transformer + + +def load_flux(checkpoint_info, diffusers_load_config): # triggered by opts.sd_checkpoint change + repo_id = sd_models.path_to_repo(checkpoint_info) + sd_models.hf_auth_check(checkpoint_info) + allow_post_quant = False + + prequantized = model_quant.get_quant(checkpoint_info.path) + shared.log.debug(f'Load model: type=FLUX model="{checkpoint_info.name}" repo="{repo_id}" unet="{shared.opts.sd_unet}" te="{shared.opts.sd_text_encoder}" vae="{shared.opts.sd_vae}" quant={prequantized} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype}') + debug(f'Load model: type=FLUX config={diffusers_load_config}') + + transformer = None + text_encoder_1 = None + text_encoder_2 = None + vae = None + + # unload current model + sd_models.unload_model_weights() + shared.sd_model = None + devices.torch_gc(force=True, reason='load') + + if shared.opts.teacache_enabled: + from modules import teacache + shared.log.debug(f'Transformers cache: type=teacache patch=forward cls={diffusers.FluxTransformer2DModel.__name__}') + diffusers.FluxTransformer2DModel.forward = teacache.teacache_flux_forward # patch must be done before transformer is loaded + + # load overrides if any + if shared.opts.sd_unet != 'Default': + try: + debug(f'Load model: type=FLUX unet="{shared.opts.sd_unet}"') + transformer = load_transformer(sd_unet.unet_dict[shared.opts.sd_unet]) + if transformer is None: + shared.opts.sd_unet = 'Default' + sd_unet.failed_unet.append(shared.opts.sd_unet) + except Exception as e: + shared.log.error(f"Load model: type=FLUX failed to load UNet: {e}") + shared.opts.sd_unet = 'Default' + if debug: + errors.display(e, 'FLUX UNet:') + if shared.opts.sd_text_encoder != 'Default': + try: + debug(f'Load model: type=FLUX te="{shared.opts.sd_text_encoder}"') + from modules.model_te import load_t5, load_vit_l + if 'vit-l' in shared.opts.sd_text_encoder.lower(): + text_encoder_1 = load_vit_l() + else: + text_encoder_2 = load_t5(name=shared.opts.sd_text_encoder, cache_dir=shared.opts.diffusers_dir) + except Exception as e: + shared.log.error(f"Load model: type=FLUX failed to load T5: {e}") + shared.opts.sd_text_encoder = 'Default' + if debug: + errors.display(e, 'FLUX T5:') + if shared.opts.sd_vae != 'Default' and shared.opts.sd_vae != 'Automatic': + try: + debug(f'Load model: type=FLUX vae="{shared.opts.sd_vae}"') + from modules import sd_vae + # vae = sd_vae.load_vae_diffusers(None, sd_vae.vae_dict[shared.opts.sd_vae], 'override') + vae_file = sd_vae.vae_dict[shared.opts.sd_vae] + if os.path.exists(vae_file): + vae_config = os.path.join('configs', 'flux', 'vae', 'config.json') + vae = diffusers.AutoencoderKL.from_single_file(vae_file, config=vae_config, **diffusers_load_config) + except Exception as e: + shared.log.error(f"Load model: type=FLUX failed to load VAE: {e}") + shared.opts.sd_vae = 'Default' + if debug: + errors.display(e, 'FLUX VAE:') + + # load quantized components if any + if prequantized == 'nf4': + try: + from pipelines.flux.flux_nf4 import load_flux_nf4 + _transformer, _text_encoder = load_flux_nf4(checkpoint_info) + if _transformer is not None: + transformer = _transformer + if _text_encoder is not None: + text_encoder_2 = _text_encoder + except Exception as e: + shared.log.error(f"Load model: type=FLUX failed to load NF4 components: {e}") + if debug: + errors.display(e, 'FLUX NF4:') + if prequantized == 'qint8' or prequantized == 'qint4': + try: + _transformer, _text_encoder = load_flux_quanto(checkpoint_info) + if _transformer is not None: + transformer = _transformer + if _text_encoder is not None: + text_encoder_2 = _text_encoder + except Exception as e: + shared.log.error(f"Load model: type=FLUX failed to load Quanto components: {e}") + if debug: + errors.display(e, 'FLUX Quanto:') + + # initialize pipeline with pre-loaded components + kwargs = {} + if transformer is not None: + kwargs['transformer'] = transformer + sd_unet.loaded_unet = shared.opts.sd_unet + if text_encoder_1 is not None: + kwargs['text_encoder'] = text_encoder_1 + model_te.loaded_te = shared.opts.sd_text_encoder + if text_encoder_2 is not None: + kwargs['text_encoder_2'] = text_encoder_2 + model_te.loaded_te = shared.opts.sd_text_encoder + if vae is not None: + kwargs['vae'] = vae + if repo_id == 'sayakpaul/flux.1-dev-nf4': + repo_id = 'black-forest-labs/FLUX.1-dev' # workaround since sayakpaul model is missing model_index.json + if 'Fill' in repo_id: + cls = diffusers.FluxFillPipeline + elif 'Canny' in repo_id: + cls = diffusers.FluxControlPipeline + elif 'Depth' in repo_id: + cls = diffusers.FluxControlPipeline + elif 'Kontext' in repo_id: + cls = diffusers.FluxKontextPipeline + from diffusers import pipelines + pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["flux1kontext"] = diffusers.FluxKontextPipeline + pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["flux1kontext"] = diffusers.FluxKontextPipeline + pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["flux1kontext"] = diffusers.FluxKontextInpaintPipeline + + else: + cls = diffusers.FluxPipeline + shared.log.debug(f'Load model: type=FLUX cls={cls.__name__} preloaded={list(kwargs)} revision={diffusers_load_config.get("revision", None)}') + for c in kwargs: + if getattr(kwargs[c], 'quantization_method', None) is not None or getattr(kwargs[c], 'gguf', None) is not None: + shared.log.debug(f'Load model: type=FLUX component={c} dtype={kwargs[c].dtype} quant={getattr(kwargs[c], "quantization_method", None) or getattr(kwargs[c], "gguf", None)}') + if kwargs[c].dtype == torch.float32 and devices.dtype != torch.float32: + try: + kwargs[c] = kwargs[c].to(dtype=devices.dtype) + shared.log.warning(f'Load model: type=FLUX component={c} dtype={kwargs[c].dtype} cast dtype={devices.dtype} recast') + except Exception: + pass + + allow_quant = 'gguf' not in (sd_unet.loaded_unet or '') and (prequantized is None or prequantized == 'none') + fn = checkpoint_info.path + if (fn is None) or (not os.path.exists(fn) or os.path.isdir(fn)): + kwargs = load_quants(kwargs, repo_id, cache_dir=shared.opts.diffusers_dir, allow_quant=allow_quant) + if fn.endswith('.safetensors') and os.path.isfile(fn): + pipe = cls.from_single_file(fn, cache_dir=shared.opts.diffusers_dir, **kwargs, **diffusers_load_config) + allow_post_quant = True + else: + pipe = cls.from_pretrained(repo_id, cache_dir=shared.opts.diffusers_dir, **kwargs, **diffusers_load_config) + + if shared.opts.teacache_enabled and model_quant.check_nunchaku('Model'): + from nunchaku.caching.diffusers_adapters import apply_cache_on_pipe + apply_cache_on_pipe(pipe, residual_diff_threshold=0.12) + + # release memory + transformer = None + text_encoder_1 = None + text_encoder_2 = None + vae = None + for k in kwargs.keys(): + kwargs[k] = None + sd_hijack_te.init_hijack(pipe) + devices.torch_gc(force=True, reason='load') + return pipe, allow_post_quant diff --git a/pipelines/model_flux_nf4.py b/pipelines/flux/flux_nf4.py similarity index 100% rename from pipelines/model_flux_nf4.py rename to pipelines/flux/flux_nf4.py diff --git a/pipelines/flux/flux_nunchaku.py b/pipelines/flux/flux_nunchaku.py new file mode 100644 index 000000000..e21b93a3b --- /dev/null +++ b/pipelines/flux/flux_nunchaku.py @@ -0,0 +1,29 @@ +from modules import shared, devices + + +def load_flux_nunchaku(repo_id): + import nunchaku + nunchaku_precision = nunchaku.utils.get_precision() + nunchaku_repo = None + transformer = None + if 'flux.1-kontext' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/nunchaku-flux.1-kontext-dev/svdq-{nunchaku_precision}_r32-flux.1-kontext-dev.safetensors" + elif 'flux.1-dev' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/nunchaku-flux.1-dev/svdq-{nunchaku_precision}_r32-flux.1-dev.safetensors" + elif 'flux.1-schnell' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/nunchaku-flux.1-schnell/svdq-{nunchaku_precision}_r32-flux.1-schnell.safetensors" + elif 'flux.1-fill' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/svdq-fp4-flux.1-fill-dev/svdq-{nunchaku_precision}_r32-flux.1-schnell.safetensors" + elif 'flux.1-depth' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/svdq-int4-flux.1-depth-dev/svdq-{nunchaku_precision}_r32-flux.1-schnell.safetensors" + elif 'shuttle' in repo_id.lower(): + nunchaku_repo = f"mit-han-lab/nunchaku-shuttle-jaguar/svdq-{nunchaku_precision}_r32-shuttle-jaguar.safetensors" + else: + shared.log.error(f'Load module: quant=Nunchaku module=transformer repo="{repo_id}" unsupported') + if nunchaku_repo is not None: + shared.log.debug(f'Load module: quant=Nunchaku module=transformer repo="{nunchaku_repo}" precision={nunchaku_precision} offload={shared.opts.nunchaku_offload} attention={shared.opts.nunchaku_attention}') + transformer = nunchaku.NunchakuFluxTransformer2dModel.from_pretrained(nunchaku_repo, offload=shared.opts.nunchaku_offload, torch_dtype=devices.dtype) + transformer.quantization_method = 'SVDQuant' + if shared.opts.nunchaku_attention: + transformer.set_attention_impl("nunchaku-fp16") + return transformer diff --git a/pipelines/flux/flux_quanto.py b/pipelines/flux/flux_quanto.py new file mode 100644 index 000000000..11e604b62 --- /dev/null +++ b/pipelines/flux/flux_quanto.py @@ -0,0 +1,73 @@ +import os +import json +import torch +import diffusers +import transformers +from safetensors.torch import load_file +from huggingface_hub import hf_hub_download +from modules import shared, errors, devices, sd_models, model_quant + + +debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None + + +def load_flux_quanto(checkpoint_info): + transformer, text_encoder_2 = None, None + quanto = model_quant.load_quanto('Load model: type=FLUX') + + if isinstance(checkpoint_info, str): + repo_path = checkpoint_info + else: + repo_path = checkpoint_info.path + + try: + quantization_map = os.path.join(repo_path, "transformer", "quantization_map.json") + debug(f'Load model: type=FLUX quantization map="{quantization_map}" repo="{checkpoint_info.name}" component="transformer"') + if not os.path.exists(quantization_map): + repo_id = sd_models.path_to_repo(checkpoint_info) + quantization_map = hf_hub_download(repo_id, subfolder='transformer', filename='quantization_map.json', cache_dir=shared.opts.diffusers_dir) + with open(quantization_map, "r", encoding='utf8') as f: + quantization_map = json.load(f) + state_dict = load_file(os.path.join(repo_path, "transformer", "diffusion_pytorch_model.safetensors")) + dtype = state_dict['context_embedder.bias'].dtype + with torch.device("meta"): + transformer = diffusers.FluxTransformer2DModel.from_config(os.path.join(repo_path, "transformer", "config.json")).to(dtype=dtype) + quanto.requantize(transformer, state_dict, quantization_map, device=torch.device("cpu")) + transformer_dtype = transformer.dtype + if transformer_dtype != devices.dtype: + try: + transformer = transformer.to(dtype=devices.dtype) + except Exception: + shared.log.error(f"Load model: type=FLUX Failed to cast transformer to {devices.dtype}, set dtype to {transformer_dtype}") + except Exception as e: + shared.log.error(f"Load model: type=FLUX failed to load Quanto transformer: {e}") + if debug: + errors.display(e, 'FLUX Quanto:') + + try: + quantization_map = os.path.join(repo_path, "text_encoder_2", "quantization_map.json") + debug(f'Load model: type=FLUX quantization map="{quantization_map}" repo="{checkpoint_info.name}" component="text_encoder_2"') + if not os.path.exists(quantization_map): + repo_id = sd_models.path_to_repo(checkpoint_info) + quantization_map = hf_hub_download(repo_id, subfolder='text_encoder_2', filename='quantization_map.json', cache_dir=shared.opts.diffusers_dir) + with open(quantization_map, "r", encoding='utf8') as f: + quantization_map = json.load(f) + with open(os.path.join(repo_path, "text_encoder_2", "config.json"), encoding='utf8') as f: + t5_config = transformers.T5Config(**json.load(f)) + state_dict = load_file(os.path.join(repo_path, "text_encoder_2", "model.safetensors")) + dtype = state_dict['encoder.block.0.layer.0.SelfAttention.relative_attention_bias.weight'].dtype + with torch.device("meta"): + text_encoder_2 = transformers.T5EncoderModel(t5_config).to(dtype=dtype) + quanto.requantize(text_encoder_2, state_dict, quantization_map, device=torch.device("cpu")) + text_encoder_2_dtype = text_encoder_2.dtype + if text_encoder_2_dtype != devices.dtype: + try: + text_encoder_2 = text_encoder_2.to(dtype=devices.dtype) + except Exception: + shared.log.error(f"Load model: type=FLUX Failed to cast text encoder to {devices.dtype}, set dtype to {text_encoder_2_dtype}") + except Exception as e: + shared.log.error(f"Load model: type=FLUX failed to load Quanto text encoder: {e}") + if debug: + errors.display(e, 'FLUX Quanto:') + + return transformer, text_encoder_2 diff --git a/pipelines/generic.py b/pipelines/generic.py index 102b5e6b8..d4065cb50 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -2,120 +2,145 @@ import os import json import diffusers import transformers -from modules import shared, devices, sd_models, model_quant +from modules import shared, devices, errors, sd_models, model_quant -debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None +debug = os.environ.get('SD_LOAD_DEBUG', None) is not None def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", allow_quant=True, variant=None, dtype=None): - load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True, allow_quant=allow_quant) - quant_type = model_quant.get_quant_type(quant_args) - dtype = dtype or devices.dtype + transformer = None + try: + load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True, allow_quant=allow_quant) + quant_type = model_quant.get_quant_type(quant_args) + dtype = dtype or devices.dtype - local_file = None - if shared.opts.sd_unet is not None and shared.opts.sd_unet != 'Default': - from modules import sd_unet - if shared.opts.sd_unet not in list(sd_unet.unet_dict): - shared.log.error(f'Load module: type=transformer file="{shared.opts.sd_unet}" not found') - elif os.path.exists(sd_unet.unet_dict[shared.opts.sd_unet]): - local_file = sd_unet.unet_dict[shared.opts.sd_unet] + local_file = None + if shared.opts.sd_unet is not None and shared.opts.sd_unet != 'Default': + from modules import sd_unet + if shared.opts.sd_unet not in list(sd_unet.unet_dict): + shared.log.error(f'Load module: type=transformer file="{shared.opts.sd_unet}" not found') + elif os.path.exists(sd_unet.unet_dict[shared.opts.sd_unet]): + local_file = sd_unet.unet_dict[shared.opts.sd_unet] - if local_file is not None and local_file.lower().endswith('.gguf'): - shared.log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') - from modules import ggml - ggml.install_gguf() - loader = cls_name.from_single_file if hasattr(cls_name, 'from_single_file') else cls_name.from_pretrained - transformer = loader( - local_file, - quantization_config=diffusers.GGUFQuantizationConfig(compute_dtype=dtype), - cache_dir=shared.opts.hfcache_dir, - **load_args, - ) - transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None) - elif local_file is not None and local_file.lower().endswith('.safetensors'): - shared.log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') - loader = cls_name.from_single_file if hasattr(cls_name, 'from_single_file') else cls_name.from_pretrained - transformer = loader( - local_file, - cache_dir=shared.opts.hfcache_dir, - **load_args, - ) - transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None) - else: - shared.log.debug(f'Load model: transformer="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') - if dtype is not None: - load_args['torch_dtype'] = dtype - if subfolder is not None: - load_args['subfolder'] = subfolder - if variant is not None: - load_args['variant'] = variant - transformer = cls_name.from_pretrained( - repo_id, - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - if shared.opts.diffusers_offload_mode != 'none' and transformer is not None: - sd_models.move_model(transformer, devices.cpu) + if local_file is not None and local_file.lower().endswith('.gguf'): + shared.log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') + from modules import ggml + ggml.install_gguf() + loader = cls_name.from_single_file if hasattr(cls_name, 'from_single_file') else cls_name.from_pretrained + transformer = loader( + local_file, + quantization_config=diffusers.GGUFQuantizationConfig(compute_dtype=dtype), + cache_dir=shared.opts.hfcache_dir, + **load_args, + ) + transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None) + elif local_file is not None and local_file.lower().endswith('.safetensors'): + shared.log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') + loader = cls_name.from_single_file if hasattr(cls_name, 'from_single_file') else cls_name.from_pretrained + transformer = loader( + local_file, + cache_dir=shared.opts.hfcache_dir, + **load_args, + ) + transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None) + else: + shared.log.debug(f'Load model: transformer="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') + if dtype is not None: + load_args['torch_dtype'] = dtype + if subfolder is not None: + load_args['subfolder'] = subfolder + if variant is not None: + load_args['variant'] = variant + transformer = cls_name.from_pretrained( + repo_id, + cache_dir=shared.opts.hfcache_dir, + **load_args, + **quant_args, + ) + sd_models.allow_post_quant = False # we already handled it + if shared.opts.diffusers_offload_mode != 'none' and transformer is not None: + sd_models.move_model(transformer, devices.cpu) + except Exception as e: + shared.log.error(f'Load model: type=transformer {e}') + if debug: + errors.display(e, 'Load:') + raise return transformer def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder", allow_quant=True, allow_shared=True, variant=None, dtype=None): - load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True, allow_quant=allow_quant) - quant_type = model_quant.get_quant_type(quant_args) text_encoder = None - dtype = dtype or devices.dtype + try: + load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True, allow_quant=allow_quant) + quant_type = model_quant.get_quant_type(quant_args) + dtype = dtype or devices.dtype - # load from local file if specified - local_file = None - if shared.opts.sd_text_encoder is not None and shared.opts.sd_text_encoder != 'Default': - from modules import model_te - if shared.opts.sd_text_encoder not in list(model_te.te_dict): - shared.log.error(f'Load module: type=te file="{shared.opts.sd_text_encoder}" not found') - elif os.path.exists(model_te.te_dict[shared.opts.sd_text_encoder]): - local_file = model_te.te_dict[shared.opts.sd_text_encoder] + # load from local file if specified + local_file = None + if shared.opts.sd_text_encoder is not None and shared.opts.sd_text_encoder != 'Default': + from modules import model_te + if shared.opts.sd_text_encoder not in list(model_te.te_dict): + shared.log.error(f'Load module: type=te file="{shared.opts.sd_text_encoder}" not found') + elif os.path.exists(model_te.te_dict[shared.opts.sd_text_encoder]): + local_file = model_te.te_dict[shared.opts.sd_text_encoder] - # load from local file gguf - if local_file is not None and local_file.lower().endswith('.gguf'): - shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}"') - from modules import ggml - ggml.install_gguf() - text_encoder = cls_name.from_pretrained( - gguf_file=local_file, - quantization_config=diffusers.GGUFQuantizationConfig(compute_dtype=dtype), - cache_dir=shared.opts.hfcache_dir, - **load_args, - ) - text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) - # load from local file safetensors - elif local_file is not None and local_file.lower().endswith('.safetensors'): - shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}"') - text_encoder = cls_name.from_pretrained( - local_file, - cache_dir=shared.opts.hfcache_dir, - **load_args, - ) - text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) - # use shared t5 if possible - elif cls_name == transformers.T5EncoderModel and allow_shared: - with open(os.path.join('configs', 'flux', 'text_encoder_2', 'config.json'), encoding='utf8') as f: - load_args['config'] = transformers.T5Config(**json.load(f)) - if model_quant.check_nunchaku('TE'): - import nunchaku - repo_id = 'nunchaku-tech/nunchaku-t5/awq-int4-flux.1-t5xxl.safetensors' - cls_name = nunchaku.NunchakuT5EncoderModel - shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="SVDQuant"') - text_encoder = nunchaku.NunchakuT5EncoderModel.from_pretrained( - repo_id, - torch_dtype=dtype, + # load from local file gguf + if local_file is not None and local_file.lower().endswith('.gguf'): + shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}"') + from modules import ggml + ggml.install_gguf() + text_encoder = cls_name.from_pretrained( + gguf_file=local_file, + quantization_config=diffusers.GGUFQuantizationConfig(compute_dtype=dtype), + cache_dir=shared.opts.hfcache_dir, + **load_args, ) - text_encoder.quantization_method = 'SVDQuant' - elif shared.opts.te_shared_t5: - repo_id = 'Disty0/t5-xxl' + text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) + # load from local file safetensors + elif local_file is not None and local_file.lower().endswith('.safetensors'): + shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}"') + text_encoder = cls_name.from_pretrained( + local_file, + cache_dir=shared.opts.hfcache_dir, + **load_args, + ) + text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) + # use shared t5 if possible + elif cls_name == transformers.T5EncoderModel and allow_shared: + with open(os.path.join('configs', 'flux', 'text_encoder_2', 'config.json'), encoding='utf8') as f: + load_args['config'] = transformers.T5Config(**json.load(f)) + if model_quant.check_nunchaku('TE'): + import nunchaku + repo_id = 'nunchaku-tech/nunchaku-t5/awq-int4-flux.1-t5xxl.safetensors' + cls_name = nunchaku.NunchakuT5EncoderModel + shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="SVDQuant"') + text_encoder = nunchaku.NunchakuT5EncoderModel.from_pretrained( + repo_id, + torch_dtype=dtype, + ) + text_encoder.quantization_method = 'SVDQuant' + elif shared.opts.te_shared_t5: + repo_id = 'Disty0/t5-xxl' + shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') + if dtype is not None: + load_args['torch_dtype'] = dtype + text_encoder = cls_name.from_pretrained( + repo_id, + cache_dir=shared.opts.hfcache_dir, + **load_args, + **quant_args, + ) + + # load from repo + if text_encoder is None: shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') if dtype is not None: load_args['torch_dtype'] = dtype + if subfolder is not None: + load_args['subfolder'] = subfolder + if variant is not None: + load_args['variant'] = variant text_encoder = cls_name.from_pretrained( repo_id, cache_dir=shared.opts.hfcache_dir, @@ -123,22 +148,12 @@ def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder **quant_args, ) - # load from repo - if text_encoder is None: - shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') - if dtype is not None: - load_args['torch_dtype'] = dtype - if subfolder is not None: - load_args['subfolder'] = subfolder - if variant is not None: - load_args['variant'] = variant - text_encoder = cls_name.from_pretrained( - repo_id, - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) - - if shared.opts.diffusers_offload_mode != 'none' and text_encoder is not None: - sd_models.move_model(text_encoder, devices.cpu) + sd_models.allow_post_quant = False # we already handled it + if shared.opts.diffusers_offload_mode != 'none' and text_encoder is not None: + sd_models.move_model(text_encoder, devices.cpu) + except Exception as e: + shared.log.error(f'Load model: type=te {e}') + if debug: + errors.display(e, 'Load:') + raise return text_encoder diff --git a/pipelines/meissonic/test.py b/pipelines/meissonic/test.py index 5687cbff0..777f40e22 100644 --- a/pipelines/meissonic/test.py +++ b/pipelines/meissonic/test.py @@ -4,7 +4,7 @@ sys.path.append("./") # import torch # from torchvision import transforms from meissonic.transformer import Transformer2DModel as TransformerMeissonic -from meissonic.pipeline import Pipeline as PipelineMeissonic +from meissonic.pipeline import MeissonicPipeline from meissonic.scheduler import Scheduler as MeissonicScheduler from transformers import CLIPTextModelWithProjection, CLIPTokenizer from diffusers import VQModel @@ -21,7 +21,7 @@ vq_model = VQModel.from_pretrained(model_path, subfolder="vqvae", cache_dir=cach text_encoder = CLIPTextModelWithProjection.from_pretrained("laion/CLIP-ViT-H-14-laion2B-s32B-b79K", cache_dir=cache_dir) tokenizer = CLIPTokenizer.from_pretrained(model_path, subfolder="tokenizer") scheduler = MeissonicScheduler.from_pretrained(model_path, subfolder="scheduler") -pipe = PipelineMeissonic(vq_model, tokenizer=tokenizer, text_encoder=text_encoder, transformer=model, scheduler=scheduler) +pipe = MeissonicPipeline(vq_model, tokenizer=tokenizer, text_encoder=text_encoder, transformer=model, scheduler=scheduler) pipe = pipe.to(device) steps = 64 diff --git a/pipelines/model_chroma.py b/pipelines/model_chroma.py index b0a28fa69..eb94bfed4 100644 --- a/pipelines/model_chroma.py +++ b/pipelines/model_chroma.py @@ -1,13 +1,9 @@ -import os import diffusers import transformers from modules import shared, devices, sd_models, model_quant from pipelines import generic -debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None - - def load_chroma(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) diff --git a/pipelines/model_flux.py b/pipelines/model_flux.py index da4bf70e9..d420ff80e 100644 --- a/pipelines/model_flux.py +++ b/pipelines/model_flux.py @@ -1,360 +1,76 @@ -import os -import json -import torch import diffusers import transformers -from safetensors.torch import load_file -from huggingface_hub import hf_hub_download -from modules import shared, errors, devices, sd_models, sd_unet, model_te, model_quant, sd_hijack_te +from modules import shared, devices, sd_models, model_quant +from pipelines import generic -debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None - - -def load_flux_quanto(checkpoint_info): - transformer, text_encoder_2 = None, None - quanto = model_quant.load_quanto('Load model: type=FLUX') - - if isinstance(checkpoint_info, str): - repo_path = checkpoint_info - else: - repo_path = checkpoint_info.path - - try: - quantization_map = os.path.join(repo_path, "transformer", "quantization_map.json") - debug(f'Load model: type=FLUX quantization map="{quantization_map}" repo="{checkpoint_info.name}" component="transformer"') - if not os.path.exists(quantization_map): - repo_id = sd_models.path_to_repo(checkpoint_info) - quantization_map = hf_hub_download(repo_id, subfolder='transformer', filename='quantization_map.json', cache_dir=shared.opts.diffusers_dir) - with open(quantization_map, "r", encoding='utf8') as f: - quantization_map = json.load(f) - state_dict = load_file(os.path.join(repo_path, "transformer", "diffusion_pytorch_model.safetensors")) - dtype = state_dict['context_embedder.bias'].dtype - with torch.device("meta"): - transformer = diffusers.FluxTransformer2DModel.from_config(os.path.join(repo_path, "transformer", "config.json")).to(dtype=dtype) - quanto.requantize(transformer, state_dict, quantization_map, device=torch.device("cpu")) - transformer_dtype = transformer.dtype - if transformer_dtype != devices.dtype: - try: - transformer = transformer.to(dtype=devices.dtype) - except Exception: - shared.log.error(f"Load model: type=FLUX Failed to cast transformer to {devices.dtype}, set dtype to {transformer_dtype}") - except Exception as e: - shared.log.error(f"Load model: type=FLUX failed to load Quanto transformer: {e}") - if debug: - errors.display(e, 'FLUX Quanto:') - - try: - quantization_map = os.path.join(repo_path, "text_encoder_2", "quantization_map.json") - debug(f'Load model: type=FLUX quantization map="{quantization_map}" repo="{checkpoint_info.name}" component="text_encoder_2"') - if not os.path.exists(quantization_map): - repo_id = sd_models.path_to_repo(checkpoint_info) - quantization_map = hf_hub_download(repo_id, subfolder='text_encoder_2', filename='quantization_map.json', cache_dir=shared.opts.diffusers_dir) - with open(quantization_map, "r", encoding='utf8') as f: - quantization_map = json.load(f) - with open(os.path.join(repo_path, "text_encoder_2", "config.json"), encoding='utf8') as f: - t5_config = transformers.T5Config(**json.load(f)) - state_dict = load_file(os.path.join(repo_path, "text_encoder_2", "model.safetensors")) - dtype = state_dict['encoder.block.0.layer.0.SelfAttention.relative_attention_bias.weight'].dtype - with torch.device("meta"): - text_encoder_2 = transformers.T5EncoderModel(t5_config).to(dtype=dtype) - quanto.requantize(text_encoder_2, state_dict, quantization_map, device=torch.device("cpu")) - text_encoder_2_dtype = text_encoder_2.dtype - if text_encoder_2_dtype != devices.dtype: - try: - text_encoder_2 = text_encoder_2.to(dtype=devices.dtype) - except Exception: - shared.log.error(f"Load model: type=FLUX Failed to cast text encoder to {devices.dtype}, set dtype to {text_encoder_2_dtype}") - except Exception as e: - shared.log.error(f"Load model: type=FLUX failed to load Quanto text encoder: {e}") - if debug: - errors.display(e, 'FLUX Quanto:') - - return transformer, text_encoder_2 - - -def load_flux_bnb(checkpoint_info, diffusers_load_config): # pylint: disable=unused-argument - transformer, text_encoder_2 = None, None - if isinstance(checkpoint_info, str): - repo_path = checkpoint_info - else: - repo_path = checkpoint_info.path - model_quant.load_bnb('Load model: type=FLUX') - quant = model_quant.get_quant(repo_path) - try: - if quant == 'fp8': - quantization_config = transformers.BitsAndBytesConfig(load_in_8bit=True, bnb_4bit_compute_dtype=devices.dtype) - debug(f'Quantization: {quantization_config}') - transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) - elif quant == 'fp4': - quantization_config = transformers.BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=devices.dtype, bnb_4bit_quant_type= 'fp4') - debug(f'Quantization: {quantization_config}') - transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) - elif quant == 'nf4': - quantization_config = transformers.BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=devices.dtype, bnb_4bit_quant_type= 'nf4') - debug(f'Quantization: {quantization_config}') - transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config, quantization_config=quantization_config) - else: - transformer = diffusers.FluxTransformer2DModel.from_single_file(repo_path, **diffusers_load_config) - except Exception as e: - shared.log.error(f"Load model: type=FLUX failed to load BnB transformer: {e}") - transformer, text_encoder_2 = None, None - if debug: - errors.display(e, 'FLUX:') - return transformer, text_encoder_2 - - -def load_quants(kwargs, repo_id, cache_dir, allow_quant): # pylint: disable=unused-argument - try: - diffusers_load_config = { - "torch_dtype": devices.dtype, - "cache_dir": cache_dir, - } - if 'transformer' not in kwargs and model_quant.check_nunchaku('Model'): - import nunchaku - nunchaku_precision = nunchaku.utils.get_precision() - nunchaku_repo = None - if 'flux.1-kontext' in repo_id.lower(): - nunchaku_repo = f"mit-han-lab/nunchaku-flux.1-kontext-dev/svdq-{nunchaku_precision}_r32-flux.1-kontext-dev.safetensors" - elif 'flux.1-dev' in repo_id.lower(): - nunchaku_repo = f"mit-han-lab/nunchaku-flux.1-dev/svdq-{nunchaku_precision}_r32-flux.1-dev.safetensors" - elif 'flux.1-schnell' in repo_id.lower(): - nunchaku_repo = f"mit-han-lab/nunchaku-flux.1-schnell/svdq-{nunchaku_precision}_r32-flux.1-schnell.safetensors" - elif 'flux.1-fill' in repo_id.lower(): - nunchaku_repo = f"mit-han-lab/svdq-fp4-flux.1-fill-dev/svdq-{nunchaku_precision}_r32-flux.1-schnell.safetensors" - elif 'flux.1-depth' in repo_id.lower(): - nunchaku_repo = f"mit-han-lab/svdq-int4-flux.1-depth-dev/svdq-{nunchaku_precision}_r32-flux.1-schnell.safetensors" - elif 'shuttle' in repo_id.lower(): - nunchaku_repo = f"mit-han-lab/nunchaku-shuttle-jaguar/svdq-{nunchaku_precision}_r32-shuttle-jaguar.safetensors" - else: - shared.log.error(f'Load module: quant=Nunchaku module=transformer repo="{repo_id}" unsupported') - if nunchaku_repo is not None: - shared.log.debug(f'Load module: quant=Nunchaku module=transformer repo="{nunchaku_repo}" precision={nunchaku_precision} offload={shared.opts.nunchaku_offload} attention={shared.opts.nunchaku_attention}') - kwargs['transformer'] = nunchaku.NunchakuFluxTransformer2dModel.from_pretrained(nunchaku_repo, offload=shared.opts.nunchaku_offload, torch_dtype=devices.dtype) - kwargs['transformer'].quantization_method = 'SVDQuant' - if shared.opts.nunchaku_attention: - kwargs['transformer'].set_attention_impl("nunchaku-fp16") - if 'transformer' not in kwargs and model_quant.check_quant('Model'): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True) - kwargs['transformer'] = diffusers.FluxTransformer2DModel.from_pretrained(repo_id, subfolder="transformer", **load_args, **quant_args) - if 'text_encoder_2' not in kwargs and model_quant.check_nunchaku('TE'): - import nunchaku - nunchaku_precision = nunchaku.utils.get_precision() - nunchaku_repo = 'mit-han-lab/nunchaku-t5/awq-int4-flux.1-t5xxl.safetensors' - shared.log.debug(f'Load module: quant=Nunchaku module=t5 repo="{nunchaku_repo}" precision={nunchaku_precision}') - kwargs['text_encoder_2'] = nunchaku.NunchakuT5EncoderModel.from_pretrained(nunchaku_repo, torch_dtype=devices.dtype) - kwargs['text_encoder_2'].quantization_method = 'SVDQuant' - if 'text_encoder_2' not in kwargs and model_quant.check_quant('TE'): - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - kwargs['text_encoder_2'] = transformers.T5EncoderModel.from_pretrained(repo_id, subfolder="text_encoder_2", **load_args, **quant_args) - except Exception as e: - shared.log.error(f'Quantization: {e}') - errors.display(e, 'Quantization:') - return kwargs - - -def load_transformer(file_path): # triggered by opts.sd_unet change - if file_path is None or not os.path.exists(file_path): - return None - transformer = None - quant = model_quant.get_quant(file_path) - diffusers_load_config = { - "torch_dtype": devices.dtype, - "cache_dir": shared.opts.hfcache_dir, - } - if quant is not None and quant != 'none': - shared.log.info(f'Load module: type=UNet/Transformer file="{file_path}" offload={shared.opts.diffusers_offload_mode} prequant={quant} dtype={devices.dtype}') - if 'gguf' in file_path.lower(): - from modules import ggml - _transformer = ggml.load_gguf(file_path, cls=diffusers.FluxTransformer2DModel, compute_dtype=devices.dtype) - if _transformer is not None: - transformer = _transformer - elif quant == "fp8": - _transformer = model_quant.load_fp8_model_layerwise(file_path, diffusers.FluxTransformer2DModel.from_single_file, diffusers_load_config) - if _transformer is not None: - transformer = _transformer - elif quant in {'qint8', 'qint4'}: - _transformer, _text_encoder_2 = load_flux_quanto(file_path) - if _transformer is not None: - transformer = _transformer - elif quant in {'fp8', 'fp4', 'nf4'}: - _transformer, _text_encoder_2 = load_flux_bnb(file_path, diffusers_load_config) - if _transformer is not None: - transformer = _transformer - elif 'nf4' in quant: - from pipelines.model_flux_nf4 import load_flux_nf4 - _transformer, _text_encoder_2 = load_flux_nf4(file_path, prequantized=True) - if _transformer is not None: - transformer = _transformer - else: - quant_args = model_quant.create_bnb_config({}) - if quant_args: - shared.log.info(f'Load module: type=Flux transformer file="{file_path}" offload={shared.opts.diffusers_offload_mode} quant=bnb dtype={devices.dtype}') - from pipelines.model_flux_nf4 import load_flux_nf4 - transformer, _text_encoder_2 = load_flux_nf4(file_path, prequantized=False) - if transformer is not None: - return transformer - load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model', device_map=True) - shared.log.debug(f'Load model: type=Flux transformer file="{file_path}" offload={shared.opts.diffusers_offload_mode} args={load_args}') - transformer = diffusers.FluxTransformer2DModel.from_single_file(file_path, **load_args, **quant_args) - if transformer is None: - shared.log.error('Failed to load UNet model') - shared.opts.sd_unet = 'Default' - return transformer - - -def load_flux(checkpoint_info, diffusers_load_config): # triggered by opts.sd_checkpoint change +def load_flux(checkpoint_info, diffusers_load_config={}): repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - allow_post_quant = False - prequantized = model_quant.get_quant(checkpoint_info.path) - shared.log.debug(f'Load model: type=FLUX model="{checkpoint_info.name}" repo="{repo_id}" unet="{shared.opts.sd_unet}" te="{shared.opts.sd_text_encoder}" vae="{shared.opts.sd_vae}" quant={prequantized} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype}') - debug(f'Load model: type=FLUX config={diffusers_load_config}') + if 'Fill' in repo_id: + cls_name = diffusers.FluxFillPipeline + elif 'Canny' in repo_id: + cls_name = diffusers.FluxControlPipeline + elif 'Depth' in repo_id: + cls_name = diffusers.FluxControlPipeline + elif 'Kontext' in repo_id: + cls_name = diffusers.FluxKontextPipeline + diffusers.pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["flux1kontext"] = diffusers.FluxKontextPipeline + diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["flux1kontext"] = diffusers.FluxKontextPipeline + diffusers.pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["flux1kontext"] = diffusers.FluxKontextInpaintPipeline + else: + cls_name = diffusers.FluxPipeline - transformer = None - text_encoder_1 = None - text_encoder_2 = None - vae = None + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) + shared.log.debug(f'Load model: type=Flux repo="{repo_id}" cls={cls_name.__name__} config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') - # unload current model - sd_models.unload_model_weights() - shared.sd_model = None - devices.torch_gc(force=True, reason='load') - - if shared.opts.teacache_enabled: + # optional teacache patch + if shared.opts.teacache_enabled and not model_quant.check_nunchaku('Model'): from modules import teacache shared.log.debug(f'Transformers cache: type=teacache patch=forward cls={diffusers.FluxTransformer2DModel.__name__}') diffusers.FluxTransformer2DModel.forward = teacache.teacache_flux_forward # patch must be done before transformer is loaded - # load overrides if any - if shared.opts.sd_unet != 'Default': - try: - debug(f'Load model: type=FLUX unet="{shared.opts.sd_unet}"') - transformer = load_transformer(sd_unet.unet_dict[shared.opts.sd_unet]) - if transformer is None: - shared.opts.sd_unet = 'Default' - sd_unet.failed_unet.append(shared.opts.sd_unet) - except Exception as e: - shared.log.error(f"Load model: type=FLUX failed to load UNet: {e}") - shared.opts.sd_unet = 'Default' - if debug: - errors.display(e, 'FLUX UNet:') - if shared.opts.sd_text_encoder != 'Default': - try: - debug(f'Load model: type=FLUX te="{shared.opts.sd_text_encoder}"') - from modules.model_te import load_t5, load_vit_l - if 'vit-l' in shared.opts.sd_text_encoder.lower(): - text_encoder_1 = load_vit_l() - else: - text_encoder_2 = load_t5(name=shared.opts.sd_text_encoder, cache_dir=shared.opts.diffusers_dir) - except Exception as e: - shared.log.error(f"Load model: type=FLUX failed to load T5: {e}") - shared.opts.sd_text_encoder = 'Default' - if debug: - errors.display(e, 'FLUX T5:') - if shared.opts.sd_vae != 'Default' and shared.opts.sd_vae != 'Automatic': - try: - debug(f'Load model: type=FLUX vae="{shared.opts.sd_vae}"') - from modules import sd_vae - # vae = sd_vae.load_vae_diffusers(None, sd_vae.vae_dict[shared.opts.sd_vae], 'override') - vae_file = sd_vae.vae_dict[shared.opts.sd_vae] - if os.path.exists(vae_file): - vae_config = os.path.join('configs', 'flux', 'vae', 'config.json') - vae = diffusers.AutoencoderKL.from_single_file(vae_file, config=vae_config, **diffusers_load_config) - except Exception as e: - shared.log.error(f"Load model: type=FLUX failed to load VAE: {e}") - shared.opts.sd_vae = 'Default' - if debug: - errors.display(e, 'FLUX VAE:') + transformer = None + text_encoder_2 = None - # load quantized components if any - if prequantized == 'nf4': - try: - from pipelines.model_flux_nf4 import load_flux_nf4 - _transformer, _text_encoder = load_flux_nf4(checkpoint_info) - if _transformer is not None: - transformer = _transformer - if _text_encoder is not None: - text_encoder_2 = _text_encoder - except Exception as e: - shared.log.error(f"Load model: type=FLUX failed to load NF4 components: {e}") - if debug: - errors.display(e, 'FLUX NF4:') - if prequantized == 'qint8' or prequantized == 'qint4': - try: - _transformer, _text_encoder = load_flux_quanto(checkpoint_info) - if _transformer is not None: - transformer = _transformer - if _text_encoder is not None: - text_encoder_2 = _text_encoder - except Exception as e: - shared.log.error(f"Load model: type=FLUX failed to load Quanto components: {e}") - if debug: - errors.display(e, 'FLUX Quanto:') + # handle transformer svdquant if available, t5 is handled inside load_text_encoder + prequantized = model_quant.get_quant(checkpoint_info.path) + if model_quant.check_nunchaku('Model'): + from pipelines.flux.flux_nunchaku import load_flux_nunchaku + transformer = load_flux_nunchaku(repo_id) + # handle prequantized models + elif prequantized == 'nf4': + from pipelines.flux.flux_nf4 import load_flux_nf4 + transformer, text_encoder_2 = load_flux_nf4(checkpoint_info) + elif prequantized == 'qint8' or prequantized == 'qint4': + from pipelines.flux.flux_quanto import load_flux_quanto + transformer, text_encoder_2 = load_flux_quanto(checkpoint_info) + elif prequantized == 'fp4' or prequantized == 'fp8': + from pipelines.flux.flux_bnb import load_flux_bnb + transformer = load_flux_bnb(checkpoint_info, diffusers_load_config) - # initialize pipeline with pre-loaded components - kwargs = {} - if transformer is not None: - kwargs['transformer'] = transformer - sd_unet.loaded_unet = shared.opts.sd_unet - if text_encoder_1 is not None: - kwargs['text_encoder'] = text_encoder_1 - model_te.loaded_te = shared.opts.sd_text_encoder - if text_encoder_2 is not None: - kwargs['text_encoder_2'] = text_encoder_2 - model_te.loaded_te = shared.opts.sd_text_encoder - if vae is not None: - kwargs['vae'] = vae - if repo_id == 'sayakpaul/flux.1-dev-nf4': - repo_id = 'black-forest-labs/FLUX.1-dev' # workaround since sayakpaul model is missing model_index.json - if 'Fill' in repo_id: - cls = diffusers.FluxFillPipeline - elif 'Canny' in repo_id: - cls = diffusers.FluxControlPipeline - elif 'Depth' in repo_id: - cls = diffusers.FluxControlPipeline - elif 'Kontext' in repo_id: - cls = diffusers.FluxKontextPipeline - from diffusers import pipelines - pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["flux1kontext"] = diffusers.FluxKontextPipeline - pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["flux1kontext"] = diffusers.FluxKontextPipeline - pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["flux1kontext"] = diffusers.FluxKontextInpaintPipeline + # finally load transformer and text encoder if not already loaded + if transformer is None: + transformer = generic.load_transformer(repo_id, cls_name=diffusers.FluxTransformer2DModel, load_config=diffusers_load_config) + if text_encoder_2 is None: + text_encoder_2 = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config) - else: - cls = diffusers.FluxPipeline - shared.log.debug(f'Load model: type=FLUX cls={cls.__name__} preloaded={list(kwargs)} revision={diffusers_load_config.get("revision", None)}') - for c in kwargs: - if getattr(kwargs[c], 'quantization_method', None) is not None or getattr(kwargs[c], 'gguf', None) is not None: - shared.log.debug(f'Load model: type=FLUX component={c} dtype={kwargs[c].dtype} quant={getattr(kwargs[c], "quantization_method", None) or getattr(kwargs[c], "gguf", None)}') - if kwargs[c].dtype == torch.float32 and devices.dtype != torch.float32: - try: - kwargs[c] = kwargs[c].to(dtype=devices.dtype) - shared.log.warning(f'Load model: type=FLUX component={c} dtype={kwargs[c].dtype} cast dtype={devices.dtype} recast') - except Exception: - pass + pipe = cls_name.from_pretrained( + repo_id, + transformer=transformer, + text_encoder_2=text_encoder_2, + cache_dir=shared.opts.diffusers_dir, + **load_args, + ) - allow_quant = 'gguf' not in (sd_unet.loaded_unet or '') and (prequantized is None or prequantized == 'none') - fn = checkpoint_info.path - if (fn is None) or (not os.path.exists(fn) or os.path.isdir(fn)): - kwargs = load_quants(kwargs, repo_id, cache_dir=shared.opts.diffusers_dir, allow_quant=allow_quant) - if fn.endswith('.safetensors') and os.path.isfile(fn): - pipe = cls.from_single_file(fn, cache_dir=shared.opts.diffusers_dir, **kwargs, **diffusers_load_config) - allow_post_quant = True - else: - pipe = cls.from_pretrained(repo_id, cache_dir=shared.opts.diffusers_dir, **kwargs, **diffusers_load_config) + del text_encoder_2 + del transformer + # optional first-block patch if shared.opts.teacache_enabled and model_quant.check_nunchaku('Model'): from nunchaku.caching.diffusers_adapters import apply_cache_on_pipe apply_cache_on_pipe(pipe, residual_diff_threshold=0.12) - # release memory - transformer = None - text_encoder_1 = None - text_encoder_2 = None - vae = None - for k in kwargs.keys(): - kwargs[k] = None - sd_hijack_te.init_hijack(pipe) devices.torch_gc(force=True, reason='load') - return pipe, allow_post_quant + return pipe diff --git a/pipelines/model_hunyuandit.py b/pipelines/model_hunyuandit.py index 39d51a560..87b74eca8 100644 --- a/pipelines/model_hunyuandit.py +++ b/pipelines/model_hunyuandit.py @@ -12,7 +12,8 @@ def load_hunyuandit(checkpoint_info, diffusers_load_config={}): shared.log.debug(f'Load model: type=HunyuanDiT repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') transformer = generic.load_transformer(repo_id, cls_name=diffusers.HunyuanDiT2DModel, load_config=diffusers_load_config) - text_encoder_2 = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder_2") + repo_te = 'Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers' if 'HunyuanDiT-v1' in repo_id else repo_id + text_encoder_2 = generic.load_text_encoder(repo_te, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config, subfolder="text_encoder_2", allow_shared=False) # this is not normal t5 pipe = diffusers.HunyuanDiTPipeline.from_pretrained( repo_id, diff --git a/pipelines/model_kolors.py b/pipelines/model_kolors.py index 8add20664..26fcc8497 100644 --- a/pipelines/model_kolors.py +++ b/pipelines/model_kolors.py @@ -2,21 +2,14 @@ import torch import diffusers -repo_id = 'Kwai-Kolors/Kolors-diffusers' - - def load_kolors(_checkpoint_info, diffusers_load_config={}): from modules import shared, devices diffusers_load_config['variant'] = "fp16" if 'torch_dtype' not in diffusers_load_config: diffusers_load_config['torch_dtype'] = torch.float16 - # import torch - # import transformers - # encoder_id = 'THUDM/chatglm3-6b' - # text_encoder = transformers.AutoModel.from_pretrained(encoder_id, torch_dtype=torch.float16, trust_remote_code=True, cache_dir=shared.opts.diffusers_dir) - # text_encoder = transformers.AutoModel.from_pretrained("THUDM/chatglm3-6b", torch_dtype=torch.float16, trust_remote_code=True).quantize(4).cuda() - # tokenizer = transformers.AutoTokenizer.from_pretrained(encoder_id, trust_remote_code=True, cache_dir=shared.opts.diffusers_dir) + repo_id = 'Kwai-Kolors/Kolors-diffusers' + shared.log.debug(f'Load model: type=Kolors repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={diffusers_load_config}') pipe = diffusers.KolorsPipeline.from_pretrained( repo_id, cache_dir = shared.opts.diffusers_dir, diff --git a/pipelines/model_lumina.py b/pipelines/model_lumina.py index 60b681881..f29104fdb 100644 --- a/pipelines/model_lumina.py +++ b/pipelines/model_lumina.py @@ -1,9 +1,9 @@ import os import transformers import diffusers -from modules import errors, shared, sd_models, sd_unet, sd_hijack_te, devices, modelloader, model_quant +from modules import shared, sd_models, sd_unet, sd_hijack_te, devices, modelloader, model_quant + -debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None def load_lumina(_checkpoint_info, diffusers_load_config={}): @@ -30,7 +30,6 @@ def load_lumina2(checkpoint_info, diffusers_load_config={}): load_config, quant_config = model_quant.get_dit_args(diffusers_load_config, module='Model') if shared.opts.sd_unet != 'Default': try: - debug(f'Load model: type=Lumina2 unet="{shared.opts.sd_unet}"') transformer = diffusers.Lumina2Transformer2DModel.from_single_file( sd_unet.unet_dict[shared.opts.sd_unet], cache_dir=shared.opts.diffusers_dir, @@ -43,12 +42,9 @@ def load_lumina2(checkpoint_info, diffusers_load_config={}): except Exception as e: shared.log.error(f"Load model: type=Lumina2 failed to load UNet: {e}") shared.opts.sd_unet = 'Default' - if debug: - errors.display(e, 'Lumina2 UNet:') if shared.opts.sd_vae != 'Default' and shared.opts.sd_vae != 'Automatic': try: - debug(f'Load model: type=Lumina2 vae="{shared.opts.sd_vae}"') from modules import sd_vae # vae = sd_vae.load_vae_diffusers(None, sd_vae.vae_dict[shared.opts.sd_vae], 'override') vae_file = sd_vae.vae_dict[shared.opts.sd_vae] @@ -58,8 +54,6 @@ def load_lumina2(checkpoint_info, diffusers_load_config={}): except Exception as e: shared.log.error(f"Load model: type=Lumina2 failed to load VAE: {e}") shared.opts.sd_vae = 'Default' - if debug: - errors.display(e, 'Lumina2 VAE:') if transformer is None: transformer = diffusers.Lumina2Transformer2DModel.from_pretrained( diff --git a/pipelines/model_meissonic.py b/pipelines/model_meissonic.py index 30671e350..b045f006c 100644 --- a/pipelines/model_meissonic.py +++ b/pipelines/model_meissonic.py @@ -6,10 +6,10 @@ def load_meissonic(checkpoint_info, diffusers_load_config={}): from modules import shared, devices, modelloader, sd_models, shared_items from pipelines.meissonic.transformer import Transformer2DModel as TransformerMeissonic from pipelines.meissonic.scheduler import Scheduler as MeissonicScheduler - from pipelines.meissonic.pipeline import Pipeline as PipelineMeissonic - from pipelines.meissonic.pipeline_img2img import Img2ImgPipeline as PipelineMeissonicImg2Img - from pipelines.meissonic.pipeline_inpaint import InpaintPipeline as PipelineMeissonicInpaint - shared_items.pipelines['Meissonic'] = PipelineMeissonic + from pipelines.meissonic.pipeline import MeissonicPipeline + from pipelines.meissonic.pipeline_img2img import MeissonicImg2ImgPipeline + from pipelines.meissonic.pipeline_inpaint import MeissonicInpaintPipeline + shared_items.pipelines['Meissonic'] = MeissonicPipeline modelloader.hf_login() fn = sd_models.path_to_repo(checkpoint_info) @@ -41,7 +41,7 @@ def load_meissonic(checkpoint_info, diffusers_load_config={}): cache_dir=cache_dir, ) scheduler = MeissonicScheduler.from_pretrained(fn, subfolder="scheduler", cache_dir=cache_dir) - pipe = PipelineMeissonic( + pipe = MeissonicPipeline( vqvae=vqvae.to(devices.dtype), text_encoder=text_encoder.to(devices.dtype), transformer=model.to(devices.dtype), @@ -49,8 +49,8 @@ def load_meissonic(checkpoint_info, diffusers_load_config={}): scheduler=scheduler, ) - diffusers.pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["meissonic"] = PipelineMeissonic - diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["meissonic"] = PipelineMeissonicImg2Img - diffusers.pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["meissonic"] = PipelineMeissonicInpaint + diffusers.pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["meissonic"] = MeissonicPipeline + diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["meissonic"] = MeissonicImg2ImgPipeline + diffusers.pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["meissonic"] = MeissonicInpaintPipeline devices.torch_gc(force=True, reason='load') return pipe diff --git a/pipelines/model_omnigen.py b/pipelines/model_omnigen.py index 596fe4dbb..b8e8d7fd0 100644 --- a/pipelines/model_omnigen.py +++ b/pipelines/model_omnigen.py @@ -1,9 +1,6 @@ -import os import diffusers from modules import shared, devices, sd_models, model_quant -debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None - def load_omnigen(checkpoint_info, diffusers_load_config={}): # pylint: disable=unused-argument repo_id = sd_models.path_to_repo(checkpoint_info) diff --git a/pipelines/model_omnigen2.py b/pipelines/model_omnigen2.py index 94488ae0a..6f2f48e7b 100644 --- a/pipelines/model_omnigen2.py +++ b/pipelines/model_omnigen2.py @@ -1,8 +1,5 @@ -import os from modules import shared, devices, sd_models, model_quant -debug = shared.log.trace if os.environ.get('SD_LOAD_DEBUG', None) is not None else lambda *args, **kwargs: None - def load_omnigen2(checkpoint_info, diffusers_load_config={}): # pylint: disable=unused-argument repo_id = sd_models.path_to_repo(checkpoint_info) From 601645fa8fad6391d39157a0e8085b25f09558df Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 11 Aug 2025 16:04:53 -0400 Subject: [PATCH 088/141] fix hunyuandit Signed-off-by: Vladimir Mandic --- cli/test-all-models.py | 17 +++++++++-------- pipelines/model_flux.py | 2 +- pipelines/model_hunyuandit.py | 1 - 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli/test-all-models.py b/cli/test-all-models.py index 728bc9930..9aef5fe3b 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -8,6 +8,7 @@ Errors: - kandinsky-community/kandinsky-3: corrupt output - Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers: CUDA error device-side assert triggered Other: +- Wan-AI/Wan2.2-T2V-A14B-Diffusers: extreme memory usage - HiDream-ai/HiDream-I1-Full: very slow at 30+s/it """ @@ -52,6 +53,7 @@ models = { "Kwai-Kolors/Kolors-diffusers": {}, "kandinsky-community/kandinsky-2-2-decoder": {}, "kandinsky-community/kandinsky-2-1": {}, + "kandinsky-community/kandinsky-3": {}, "Alpha-VLLM/Lumina-Next-SFT-diffusers": {}, "Alpha-VLLM/Lumina-Image-2.0": {}, "MeissonFlow/Meissonic": {}, @@ -65,18 +67,17 @@ models = { "black-forest-labs/FLUX.1-dev": {}, "black-forest-labs/FLUX.1-Kontext-dev": {}, "black-forest-labs/FLUX.1-Krea-dev": {}, - "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers": {}, - "Tencent-Hunyuan/HunyuanDiT-v1.1-Diffusers": {}, - # "kandinsky-community/kandinsky-3": {}, - # "HiDream-ai/HiDream-I1-Full": {}, - "Wan-AI/Wan2.1-T2V-1.3B-Diffusers": {}, - "Wan-AI/Wan2.1-T2V-14B-Diffusers": {}, - "Wan-AI/Wan2.2-TI2V-5B-Diffusers": {}, - "Wan-AI/Wan2.2-T2V-A14B-Diffusers": {}, "lodestones/Chroma1-HD": {}, "vladmandic/chroma-unlocked-v50-annealed": {}, "vladmandic/chroma-unlocked-v48": {}, "vladmandic/chroma-unlocked-v48-detail-calibrated": {}, + "Wan-AI/Wan2.1-T2V-1.3B-Diffusers": {}, + "Wan-AI/Wan2.1-T2V-14B-Diffusers": {}, + "Wan-AI/Wan2.2-TI2V-5B-Diffusers": {}, + "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers": {}, + "Tencent-Hunyuan/HunyuanDiT-v1.1-Diffusers": {}, + # "HiDream-ai/HiDream-I1-Full": {}, + # "Wan-AI/Wan2.2-T2V-A14B-Diffusers": {}, } styles = [ 'Fixed Astronaut', diff --git a/pipelines/model_flux.py b/pipelines/model_flux.py index d420ff80e..34de4da83 100644 --- a/pipelines/model_flux.py +++ b/pipelines/model_flux.py @@ -23,7 +23,7 @@ def load_flux(checkpoint_info, diffusers_load_config={}): cls_name = diffusers.FluxPipeline load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) - shared.log.debug(f'Load model: type=Flux repo="{repo_id}" cls={cls_name.__name__} config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + shared.log.debug(f'Load model: type=Flux repo="{repo_id}" cls={cls_name.__name__} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') # optional teacache patch if shared.opts.teacache_enabled and not model_quant.check_nunchaku('Model'): diff --git a/pipelines/model_hunyuandit.py b/pipelines/model_hunyuandit.py index 87b74eca8..16d44dee8 100644 --- a/pipelines/model_hunyuandit.py +++ b/pipelines/model_hunyuandit.py @@ -25,6 +25,5 @@ def load_hunyuandit(checkpoint_info, diffusers_load_config={}): del text_encoder_2 del transformer - sd_hijack_te.init_hijack(pipe) devices.torch_gc(force=True, reason='load') return pipe From a97b03e5afa3db95b9b06ec6123db44954724641 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 11 Aug 2025 16:05:04 -0400 Subject: [PATCH 089/141] update wiki Signed-off-by: Vladimir Mandic --- wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wiki b/wiki index 0b658ebad..4cc3fb9e2 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 0b658ebad7ad81ebb9cd131abf409878d9c8744f +Subproject commit 4cc3fb9e2eacac68da1b1af22a025032ef9df7d6 From 26461f1d8d4d0ccac0dd3f1a7fd242263c23c0b1 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Mon, 11 Aug 2025 23:15:25 +0300 Subject: [PATCH 090/141] fix conv in8 matmul --- modules/sdnq/layers/conv/conv_int8.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sdnq/layers/conv/conv_int8.py b/modules/sdnq/layers/conv/conv_int8.py index c899116a6..1ebee0315 100644 --- a/modules/sdnq/layers/conv/conv_int8.py +++ b/modules/sdnq/layers/conv/conv_int8.py @@ -28,7 +28,7 @@ def conv_int8_matmul( input, mm_output_shape = process_conv_input(conv_type, input, reversed_padding_repeated_twice, padding_mode, result_shape, stride, padding, dilation) input, scale = quantize_int8_matmul_input(input, scale) if quantized_weight_shape is not None: - weight = unpack_int_symetric(weight, quantized_weight_shape, weights_dtype, dtype=torch.int8, transpose=True) + weight = unpack_int_symetric(weight, quantized_weight_shape, weights_dtype, dtype=torch.int8) if groups == 1: result = torch._int_mm(input, weight) From 9992338187cec717db531c0977ef96a5de02b89c Mon Sep 17 00:00:00 2001 From: Disty0 Date: Mon, 11 Aug 2025 23:24:13 +0300 Subject: [PATCH 091/141] sdnq fix convs --- modules/sdnq/layers/conv/conv_fp8.py | 2 +- modules/sdnq/layers/conv/conv_fp8_tensorwise.py | 2 +- modules/sdnq/layers/conv/conv_int8.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/sdnq/layers/conv/conv_fp8.py b/modules/sdnq/layers/conv/conv_fp8.py index cda2e625f..f93d39519 100644 --- a/modules/sdnq/layers/conv/conv_fp8.py +++ b/modules/sdnq/layers/conv/conv_fp8.py @@ -6,7 +6,7 @@ import torch from ...common import use_torch_compile # noqa: TID252 from ..linear.linear_fp8 import quantize_fp8_matmul_input # noqa: TID252 -from .conv import get_conv_args, process_conv_input +from .forward import get_conv_args, process_conv_input def conv_fp8_matmul( diff --git a/modules/sdnq/layers/conv/conv_fp8_tensorwise.py b/modules/sdnq/layers/conv/conv_fp8_tensorwise.py index bfc813ea1..46b53a2d6 100644 --- a/modules/sdnq/layers/conv/conv_fp8_tensorwise.py +++ b/modules/sdnq/layers/conv/conv_fp8_tensorwise.py @@ -7,7 +7,7 @@ import torch from ...common import use_torch_compile # noqa: TID252 from ...dequantizer import dequantize_symmetric, dequantize_symmetric_with_bias # noqa: TID252 from ..linear.linear_fp8_tensorwise import quantize_fp8_matmul_input_tensorwise # noqa: TID252 -from .conv import get_conv_args, process_conv_input +from .forward import get_conv_args, process_conv_input def conv_fp8_matmul_tensorwise( diff --git a/modules/sdnq/layers/conv/conv_int8.py b/modules/sdnq/layers/conv/conv_int8.py index 1ebee0315..02b553d15 100644 --- a/modules/sdnq/layers/conv/conv_int8.py +++ b/modules/sdnq/layers/conv/conv_int8.py @@ -8,7 +8,7 @@ from ...common import use_torch_compile # noqa: TID252 from ...packed_int import unpack_int_symetric # noqa: TID252 from ...dequantizer import dequantize_symmetric, dequantize_symmetric_with_bias # noqa: TID252 from ..linear.linear_int8 import quantize_int8_matmul_input # noqa: TID252 -from .conv import get_conv_args, process_conv_input +from .forward import get_conv_args, process_conv_input def conv_int8_matmul( From 232a631be56899ab34f60a62ba9885d05f993afa Mon Sep 17 00:00:00 2001 From: Disty0 Date: Mon, 11 Aug 2025 23:31:06 +0300 Subject: [PATCH 092/141] Use pre-mode quant on custom transformer files --- pipelines/generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipelines/generic.py b/pipelines/generic.py index d4065cb50..2f634b470 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -42,8 +42,8 @@ def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", local_file, cache_dir=shared.opts.hfcache_dir, **load_args, + **quant_args, ) - transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None) else: shared.log.debug(f'Load model: transformer="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') if dtype is not None: From 6312b3d0aca9583884507a7aa7af889d09d66ac2 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 11 Aug 2025 16:35:14 -0400 Subject: [PATCH 093/141] add Qwen3-4B-Instruct-2507 llm and Flash-FlowMatch scheduler Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 6 +- cli/test-all-models.py | 6 +- modules/schedulers/scheduler_flashflow.py | 428 ++++++++++++++++++++++ modules/sd_samplers_diffusers.py | 3 + pipelines/model_hunyuandit.py | 2 +- scripts/prompt_enhance.py | 1 + 6 files changed, 440 insertions(+), 6 deletions(-) create mode 100644 modules/schedulers/scheduler_flashflow.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 368433be2..5172f587f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,12 +98,14 @@ And (*as always*) many bugfixes and improvements to existing features! - use model vae scale-factor for image width/heigt calculations - **Other** - **prompt enhance** add `allura-org/Gemma-3-Glitter-4B` model support - - remove **LDSR** - - remove `api-only` cli option + - **prompt enhance** add `Qwen/Qwen3-4B-Instruct-2507` model support + - **schedulers** add **Flash FlowMatch** - **API** - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model - **Refactor** - new unified pipeline component loader in `pipelines/generic` + - remove **LDSR** + - remove `api-only` cli option - **Fixes** - refactor legacy processing loop - fix settings components mismatch diff --git a/cli/test-all-models.py b/cli/test-all-models.py index 9aef5fe3b..e781838c2 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -53,7 +53,7 @@ models = { "Kwai-Kolors/Kolors-diffusers": {}, "kandinsky-community/kandinsky-2-2-decoder": {}, "kandinsky-community/kandinsky-2-1": {}, - "kandinsky-community/kandinsky-3": {}, + "kandinsky-community/kandinsky-3": {}, # corrupt output "Alpha-VLLM/Lumina-Next-SFT-diffusers": {}, "Alpha-VLLM/Lumina-Image-2.0": {}, "MeissonFlow/Meissonic": {}, @@ -76,8 +76,8 @@ models = { "Wan-AI/Wan2.2-TI2V-5B-Diffusers": {}, "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers": {}, "Tencent-Hunyuan/HunyuanDiT-v1.1-Diffusers": {}, - # "HiDream-ai/HiDream-I1-Full": {}, - # "Wan-AI/Wan2.2-T2V-A14B-Diffusers": {}, + # "HiDream-ai/HiDream-I1-Full": {}, # extreme memory usage due to size + # "Wan-AI/Wan2.2-T2V-A14B-Diffusers": {}, # extreme memory usage due to size } styles = [ 'Fixed Astronaut', diff --git a/modules/schedulers/scheduler_flashflow.py b/modules/schedulers/scheduler_flashflow.py new file mode 100644 index 000000000..122f8ed74 --- /dev/null +++ b/modules/schedulers/scheduler_flashflow.py @@ -0,0 +1,428 @@ +# Copyright 2024 Stability AI, Katherine Crowson and The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from dataclasses import dataclass +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.schedulers.scheduling_utils import SchedulerMixin +from diffusers.utils import BaseOutput, is_scipy_available, logging +from diffusers.utils.torch_utils import randn_tensor + +if is_scipy_available(): + import scipy.stats + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +@dataclass +class FlashFlowMatchEulerDiscreteSchedulerOutput(BaseOutput): + """ + Output class for the scheduler's `step` function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample `(x_{t-1})` of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + """ + + prev_sample: torch.FloatTensor + + +class FlashFlowMatchEulerDiscreteScheduler(SchedulerMixin, ConfigMixin): + """ + Euler scheduler. + + This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic + methods the library implements for all schedulers such as loading and saving. + + Args: + num_train_timesteps (`int`, defaults to 1000): + The number of diffusion steps to train the model. + timestep_spacing (`str`, defaults to `"linspace"`): + The way the timesteps should be scaled. Refer to Table 2 of the [Common Diffusion Noise Schedules and + Sample Steps are Flawed](https://huggingface.co/papers/2305.08891) for more information. + shift (`float`, defaults to 1.0): + The shift value for the timestep schedule. + """ + + _compatibles = [] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + shift: float = 1.0, + use_dynamic_shifting=False, + base_shift: Optional[float] = 0.5, + max_shift: Optional[float] = 1.15, + base_image_seq_len: Optional[int] = 256, + max_image_seq_len: Optional[int] = 4096, + invert_sigmas: bool = False, + use_karras_sigmas: Optional[bool] = False, + use_exponential_sigmas: Optional[bool] = False, + use_beta_sigmas: Optional[bool] = False, + ): + if self.config.use_beta_sigmas and not is_scipy_available(): + raise ImportError("Make sure to install scipy if you want to use beta sigmas.") + if sum([self.config.use_beta_sigmas, self.config.use_exponential_sigmas, self.config.use_karras_sigmas]) > 1: + raise ValueError( + "Only one of `config.use_beta_sigmas`, `config.use_exponential_sigmas`, `config.use_karras_sigmas` can be used." + ) + timesteps = np.linspace(1, num_train_timesteps, num_train_timesteps, dtype=np.float32)[::-1].copy() + timesteps = torch.from_numpy(timesteps).to(dtype=torch.float32) + + sigmas = timesteps / num_train_timesteps + if not use_dynamic_shifting: + # when use_dynamic_shifting is True, we apply the timestep shifting on the fly based on the image resolution + sigmas = shift * sigmas / (1 + (shift - 1) * sigmas) + + self.timesteps = sigmas * num_train_timesteps + + self._step_index = None + self._begin_index = None + + self.sigmas = sigmas.to("cpu") # to avoid too much CPU/GPU communication + self.sigma_min = self.sigmas[-1].item() + self.sigma_max = self.sigmas[0].item() + + @property + def step_index(self): + """ + The index counter for current timestep. It will increase 1 after each scheduler step. + """ + return self._step_index + + @property + def begin_index(self): + """ + The index for the first timestep. It should be set from pipeline with `set_begin_index` method. + """ + return self._begin_index + + # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index + def set_begin_index(self, begin_index: int = 0): + """ + Sets the begin index for the scheduler. This function should be run from pipeline before the inference. + + Args: + begin_index (`int`): + The begin index for the scheduler. + """ + self._begin_index = begin_index + + def scale_noise( + self, + sample: torch.FloatTensor, + timestep: Union[float, torch.FloatTensor], + noise: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + """ + Forward process in flow-matching + + Args: + sample (`torch.FloatTensor`): + The input sample. + timestep (`int`, *optional*): + The current timestep in the diffusion chain. + + Returns: + `torch.FloatTensor`: + A scaled input sample. + """ + # Make sure sigmas and timesteps have the same device and dtype as original_samples + sigmas = self.sigmas.to(device=sample.device, dtype=sample.dtype) + + if sample.device.type == "mps" and torch.is_floating_point(timestep): + # mps does not support float64 + schedule_timesteps = self.timesteps.to(sample.device, dtype=torch.float32) + timestep = timestep.to(sample.device, dtype=torch.float32) + else: + schedule_timesteps = self.timesteps.to(sample.device) + timestep = timestep.to(sample.device) + + # self.begin_index is None when scheduler is used for training, or pipeline does not implement set_begin_index + if self.begin_index is None: + step_indices = [self.index_for_timestep(t, schedule_timesteps) for t in timestep] + elif self.step_index is not None: + # add_noise is called after first denoising step (for inpainting) + step_indices = [self.step_index] * timestep.shape[0] + else: + # add noise is called before first denoising step to create initial latent(img2img) + step_indices = [self.begin_index] * timestep.shape[0] + + sigma = sigmas[step_indices].flatten() + while len(sigma.shape) < len(sample.shape): + sigma = sigma.unsqueeze(-1) + + sample = sigma * noise + (1.0 - sigma) * sample + + return sample + + def _sigma_to_t(self, sigma): + return sigma * self.config.num_train_timesteps + + def time_shift(self, mu: float, sigma: float, t: torch.Tensor): + return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma) + + def set_timesteps( + self, + num_inference_steps: int = None, + device: Union[str, torch.device] = None, + sigmas: Optional[List[float]] = None, + mu: Optional[float] = None, + ): + """ + Sets the discrete timesteps used for the diffusion chain (to be run before inference). + + Args: + num_inference_steps (`int`): + The number of diffusion steps used when generating samples with a pre-trained model. + device (`str` or `torch.device`, *optional*): + The device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + if self.config.use_dynamic_shifting and mu is None: + raise ValueError(" you have a pass a value for `mu` when `use_dynamic_shifting` is set to be `True`") + + if sigmas is None: + timesteps = np.linspace( + self._sigma_to_t(self.sigma_max), self._sigma_to_t(self.sigma_min), num_inference_steps + ) + + sigmas = timesteps / self.config.num_train_timesteps + else: + sigmas = np.array(sigmas).astype(np.float32) + num_inference_steps = len(sigmas) + self.num_inference_steps = num_inference_steps + + if self.config.use_dynamic_shifting: + sigmas = self.time_shift(mu, 1.0, sigmas) + else: + sigmas = self.config.shift * sigmas / (1 + (self.config.shift - 1) * sigmas) + + if self.config.use_karras_sigmas: + sigmas = self._convert_to_karras(in_sigmas=sigmas, num_inference_steps=num_inference_steps) + + elif self.config.use_exponential_sigmas: + sigmas = self._convert_to_exponential(in_sigmas=sigmas, num_inference_steps=num_inference_steps) + + elif self.config.use_beta_sigmas: + sigmas = self._convert_to_beta(in_sigmas=sigmas, num_inference_steps=num_inference_steps) + + sigmas = torch.from_numpy(sigmas).to(dtype=torch.float32, device=device) + timesteps = sigmas * self.config.num_train_timesteps + + if self.config.invert_sigmas: + sigmas = 1.0 - sigmas + timesteps = sigmas * self.config.num_train_timesteps + sigmas = torch.cat([sigmas, torch.ones(1, device=sigmas.device)]) + else: + sigmas = torch.cat([sigmas, torch.zeros(1, device=sigmas.device)]) + + self.timesteps = timesteps.to(device=device) + self.sigmas = sigmas + self._step_index = None + self._begin_index = None + + def index_for_timestep(self, timestep, schedule_timesteps=None): + if schedule_timesteps is None: + schedule_timesteps = self.timesteps + + indices = (schedule_timesteps == timestep).nonzero() + + # The sigma index that is taken for the **very** first `step` + # is always the second index (or the last index if there is only 1) + # This way we can ensure we don't accidentally skip a sigma in + # case we start in the middle of the denoising schedule (e.g. for image-to-image) + pos = 1 if len(indices) > 1 else 0 + + return indices[pos].item() + + def _init_step_index(self, timestep): + if self.begin_index is None: + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + self._step_index = self.index_for_timestep(timestep) + else: + self._step_index = self._begin_index + + def step( + self, + model_output: torch.FloatTensor, + timestep: Union[float, torch.FloatTensor], + sample: torch.FloatTensor, + s_churn: float = 0.0, + s_tmin: float = 0.0, + s_tmax: float = float("inf"), + s_noise: float = 1.0, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[FlashFlowMatchEulerDiscreteSchedulerOutput, Tuple]: + """ + Predict the sample from the previous timestep by reversing the SDE. This function propagates the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): + The direct output from learned diffusion model. + timestep (`float`): + The current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + A current instance of a sample created by the diffusion process. + s_churn (`float`): + s_tmin (`float`): + s_tmax (`float`): + s_noise (`float`, defaults to 1.0): + Scaling factor for noise added to the sample. + generator (`torch.Generator`, *optional*): + A random number generator. + return_dict (`bool`): + Whether or not to return a [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] or + tuple. + + Returns: + [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] or `tuple`: + If return_dict is `True`, [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] is + returned, otherwise a tuple is returned where the first element is the sample tensor. + """ + + if ( + isinstance(timestep, int) + or isinstance(timestep, torch.IntTensor) + or isinstance(timestep, torch.LongTensor) + ): + raise ValueError( + ( + "Passing integer indices (e.g. from `enumerate(timesteps)`) as timesteps to" + " `EulerDiscreteScheduler.step()` is not supported. Make sure to pass" + " one of the `scheduler.timesteps` as a timestep." + ), + ) + + if self.step_index is None: + self._init_step_index(timestep) + + # Upcast to avoid precision issues when computing prev_sample + + sigma = self.sigmas[self.step_index] + + # Upcast to avoid precision issues when computing prev_sample + sample = sample.to(torch.float32) + + denoised = sample - model_output * sigma + + if self.step_index < self.num_inference_steps - 1: + sigma_next = self.sigmas[self.step_index + 1] + noise = randn_tensor( + model_output.shape, + generator=generator, + device=model_output.device, + dtype=denoised.dtype, + ) + sample = sigma_next * noise + (1.0 - sigma_next) * denoised + + self._step_index += 1 + sample = sample.to(model_output.dtype) + + if not return_dict: + return (sample,) + + return FlashFlowMatchEulerDiscreteSchedulerOutput(prev_sample=sample) + + # Copied from diffusers.schedulers.scheduling_euler_discrete.EulerDiscreteScheduler._convert_to_karras + def _convert_to_karras(self, in_sigmas: torch.Tensor, num_inference_steps) -> torch.Tensor: + """Constructs the noise schedule of Karras et al. (2022).""" + + # Hack to make sure that other schedulers which copy this function don't break + # TODO: Add this logic to the other schedulers + if hasattr(self.config, "sigma_min"): + sigma_min = self.config.sigma_min + else: + sigma_min = None + + if hasattr(self.config, "sigma_max"): + sigma_max = self.config.sigma_max + else: + sigma_max = None + + sigma_min = sigma_min if sigma_min is not None else in_sigmas[-1].item() + sigma_max = sigma_max if sigma_max is not None else in_sigmas[0].item() + + rho = 7.0 # 7.0 is the value used in the paper + ramp = np.linspace(0, 1, num_inference_steps) + min_inv_rho = sigma_min ** (1 / rho) + max_inv_rho = sigma_max ** (1 / rho) + sigmas = (max_inv_rho + ramp * (min_inv_rho - max_inv_rho)) ** rho + return sigmas + + # Copied from diffusers.schedulers.scheduling_euler_discrete.EulerDiscreteScheduler._convert_to_exponential + def _convert_to_exponential(self, in_sigmas: torch.Tensor, num_inference_steps: int) -> torch.Tensor: + """Constructs an exponential noise schedule.""" + + # Hack to make sure that other schedulers which copy this function don't break + # TODO: Add this logic to the other schedulers + if hasattr(self.config, "sigma_min"): + sigma_min = self.config.sigma_min + else: + sigma_min = None + + if hasattr(self.config, "sigma_max"): + sigma_max = self.config.sigma_max + else: + sigma_max = None + + sigma_min = sigma_min if sigma_min is not None else in_sigmas[-1].item() + sigma_max = sigma_max if sigma_max is not None else in_sigmas[0].item() + + sigmas = np.exp(np.linspace(math.log(sigma_max), math.log(sigma_min), num_inference_steps)) + return sigmas + + # Copied from diffusers.schedulers.scheduling_euler_discrete.EulerDiscreteScheduler._convert_to_beta + def _convert_to_beta( + self, in_sigmas: torch.Tensor, num_inference_steps: int, alpha: float = 0.6, beta: float = 0.6 + ) -> torch.Tensor: + """From "Beta Sampling is All You Need" [arXiv:2407.12173] (Lee et. al, 2024)""" + + # Hack to make sure that other schedulers which copy this function don't break + # TODO: Add this logic to the other schedulers + if hasattr(self.config, "sigma_min"): + sigma_min = self.config.sigma_min + else: + sigma_min = None + + if hasattr(self.config, "sigma_max"): + sigma_max = self.config.sigma_max + else: + sigma_max = None + + sigma_min = sigma_min if sigma_min is not None else in_sigmas[-1].item() + sigma_max = sigma_max if sigma_max is not None else in_sigmas[0].item() + + sigmas = np.array( + [ + sigma_min + (ppf * (sigma_max - sigma_min)) + for ppf in [ + scipy.stats.beta.ppf(timestep, alpha, beta) + for timestep in 1 - np.linspace(0, 1, num_inference_steps) + ] + ] + ) + return sigmas + + def __len__(self): + return self.config.num_train_timesteps diff --git a/modules/sd_samplers_diffusers.py b/modules/sd_samplers_diffusers.py index d0d394b88..6f06eceaa 100644 --- a/modules/sd_samplers_diffusers.py +++ b/modules/sd_samplers_diffusers.py @@ -51,6 +51,7 @@ try: from modules.schedulers.scheduler_bdia import BDIA_DDIMScheduler # pylint: disable=ungrouped-imports from modules.schedulers.scheduler_ufogen import UFOGenScheduler # pylint: disable=ungrouped-imports from modules.schedulers.scheduler_unipc_flowmatch import FlowUniPCMultistepScheduler # pylint: disable=ungrouped-imports + from modules.schedulers.scheduler_flashflow import FlashFlowMatchEulerDiscreteScheduler # pylint: disable=ungrouped-imports from modules.perflow import PeRFlowScheduler # pylint: disable=ungrouped-imports except Exception as e: shared.log.error(f'Sampler import: version={diffusers.__version__} error: {e}') @@ -104,6 +105,7 @@ config = { 'VDM Solver': { 'clip_sample_range': 2.0, }, 'TCD': { 'set_alpha_to_one': True, 'rescale_betas_zero_snr': False, 'beta_schedule': 'scaled_linear' }, 'TDD': { }, + 'Flash FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'use_karras_sigmas': False, 'use_exponential_sigmas': False, 'use_beta_sigmas': False }, 'PeRFlow': { 'prediction_type': 'ddim_eps' }, 'UFOGen': { }, 'BDIA DDIM': { 'clip_sample': False, 'set_alpha_to_one': True, 'steps_offset': 0, 'clip_sample_range': 1.0, 'sample_max_value': 1.0, 'timestep_spacing': 'leading', 'rescale_betas_zero_snr': False, 'thresholding': False, 'gamma': 1.0 }, @@ -153,6 +155,7 @@ samplers_data_diffusers = [ SamplerData('Heun', lambda model: DiffusionSampler('Heun', HeunDiscreteScheduler, model), [], {}), SamplerData('Heun FlowMatch', lambda model: DiffusionSampler('Heun FlowMatch', FlowMatchHeunDiscreteScheduler, model), [], {}), + SamplerData('Flash FlowMatch', lambda model: DiffusionSampler('Flash FlowMatch', FlashFlowMatchEulerDiscreteScheduler, model), [], {}), SamplerData('DEIS', lambda model: DiffusionSampler('DEIS', DEISMultistepScheduler, model), [], {}), SamplerData('SA Solver', lambda model: DiffusionSampler('SA Solver', SASolverScheduler, model), [], {}), diff --git a/pipelines/model_hunyuandit.py b/pipelines/model_hunyuandit.py index 16d44dee8..335bc7d7e 100644 --- a/pipelines/model_hunyuandit.py +++ b/pipelines/model_hunyuandit.py @@ -1,6 +1,6 @@ import transformers import diffusers -from modules import shared, sd_models, devices, model_quant, sd_hijack_te +from modules import shared, sd_models, devices, model_quant from pipelines import generic diff --git a/scripts/prompt_enhance.py b/scripts/prompt_enhance.py index 622a12374..1950f847e 100644 --- a/scripts/prompt_enhance.py +++ b/scripts/prompt_enhance.py @@ -44,6 +44,7 @@ class Options: 'Qwen/Qwen3-0.6B': {}, 'Qwen/Qwen3-1.7B': {}, 'Qwen/Qwen3-4B': {}, + 'Qwen/Qwen3-4B-Instruct-2507': {}, 'Qwen/Qwen2.5-0.5B-Instruct': {}, 'Qwen/Qwen2.5-1.5B-Instruct': {}, 'Qwen/Qwen2.5-3B-Instruct': {}, From 9daa30f3716676ed6c68c4df69fdfc9cf086c578 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Mon, 11 Aug 2025 23:37:05 +0300 Subject: [PATCH 094/141] Add torch_dtype to transformer.from_single_file --- pipelines/generic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pipelines/generic.py b/pipelines/generic.py index 2f634b470..5e3f07583 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -37,6 +37,8 @@ def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None) elif local_file is not None and local_file.lower().endswith('.safetensors'): shared.log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') + if dtype is not None: + load_args['torch_dtype'] = dtype loader = cls_name.from_single_file if hasattr(cls_name, 'from_single_file') else cls_name.from_pretrained transformer = loader( local_file, From 8b9ba8298241d18626e2716928d69e1076d875e7 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Mon, 11 Aug 2025 23:57:40 +0300 Subject: [PATCH 095/141] Use generic model loader with lumina2 --- pipelines/generic.py | 4 ++- pipelines/model_lumina.py | 57 +++++---------------------------------- 2 files changed, 10 insertions(+), 51 deletions(-) diff --git a/pipelines/generic.py b/pipelines/generic.py index 5e3f07583..4983d060a 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -102,12 +102,14 @@ def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder # load from local file safetensors elif local_file is not None and local_file.lower().endswith('.safetensors'): shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}"') + if dtype is not None: + load_args['torch_dtype'] = dtype text_encoder = cls_name.from_pretrained( local_file, cache_dir=shared.opts.hfcache_dir, **load_args, + **quant_args, ) - text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) # use shared t5 if possible elif cls_name == transformers.T5EncoderModel and allow_shared: with open(os.path.join('configs', 'flux', 'text_encoder_2', 'config.json'), encoding='utf8') as f: diff --git a/pipelines/model_lumina.py b/pipelines/model_lumina.py index f29104fdb..c7da1966d 100644 --- a/pipelines/model_lumina.py +++ b/pipelines/model_lumina.py @@ -1,7 +1,8 @@ import os import transformers import diffusers -from modules import shared, sd_models, sd_unet, sd_hijack_te, devices, modelloader, model_quant +from modules import shared, sd_models, sd_hijack_te, devices, modelloader, model_quant +from pipelines import generic @@ -19,7 +20,6 @@ def load_lumina(_checkpoint_info, diffusers_load_config={}): def load_lumina2(checkpoint_info, diffusers_load_config={}): - transformer, text_encoder, vae = None, None, None repo_id = sd_models.path_to_repo(checkpoint_info) if shared.opts.teacache_enabled: @@ -27,55 +27,10 @@ def load_lumina2(checkpoint_info, diffusers_load_config={}): shared.log.debug(f'Transformers cache: type=teacache patch=forward cls={diffusers.Lumina2Transformer2DModel.__name__}') diffusers.Lumina2Transformer2DModel.forward = teacache.teacache_lumina2_forward # patch must be done before transformer is loaded - load_config, quant_config = model_quant.get_dit_args(diffusers_load_config, module='Model') - if shared.opts.sd_unet != 'Default': - try: - transformer = diffusers.Lumina2Transformer2DModel.from_single_file( - sd_unet.unet_dict[shared.opts.sd_unet], - cache_dir=shared.opts.diffusers_dir, - **load_config, - **quant_config - ) - if transformer is None: - shared.opts.sd_unet = 'Default' - sd_unet.failed_unet.append(shared.opts.sd_unet) - except Exception as e: - shared.log.error(f"Load model: type=Lumina2 failed to load UNet: {e}") - shared.opts.sd_unet = 'Default' + transformer = generic.load_transformer(repo_id, cls_name=diffusers.Lumina2Transformer2DModel, load_config=diffusers_load_config) + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.Gemma2Model, load_config=diffusers_load_config) - if shared.opts.sd_vae != 'Default' and shared.opts.sd_vae != 'Automatic': - try: - from modules import sd_vae - # vae = sd_vae.load_vae_diffusers(None, sd_vae.vae_dict[shared.opts.sd_vae], 'override') - vae_file = sd_vae.vae_dict[shared.opts.sd_vae] - if os.path.exists(vae_file): - vae_config = os.path.join('configs', 'flux', 'vae', 'config.json') - vae = diffusers.AutoencoderKL.from_single_file(vae_file, config=vae_config, **diffusers_load_config) - except Exception as e: - shared.log.error(f"Load model: type=Lumina2 failed to load VAE: {e}") - shared.opts.sd_vae = 'Default' - - if transformer is None: - transformer = diffusers.Lumina2Transformer2DModel.from_pretrained( - repo_id, - subfolder="transformer", - cache_dir=shared.opts.diffusers_dir, - **load_config, - **quant_config, - ) - - load_config, quant_config = model_quant.get_dit_args(diffusers_load_config, module='TE', device_map=True) - text_encoder = transformers.AutoModel.from_pretrained( - repo_id, - subfolder="text_encoder", - cache_dir=shared.opts.diffusers_dir, - **load_config, - **quant_config, - ) - - load_config, quant_config = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) - if vae is not None: - load_config['vae'] = vae + load_config, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) pipe = diffusers.Lumina2Pipeline.from_pretrained( repo_id, cache_dir=shared.opts.diffusers_dir, @@ -84,6 +39,8 @@ def load_lumina2(checkpoint_info, diffusers_load_config={}): **load_config, ) + del transformer + del text_encoder sd_hijack_te.init_hijack(pipe) devices.torch_gc(force=True, reason='load') return pipe From 4317f2306228d536d18ff251f9eae9c52f19f46e Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 11 Aug 2025 16:59:38 -0400 Subject: [PATCH 096/141] update modernui Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 9a9c0f8ed..352543365 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 9a9c0f8ed77e6f42f9e2528e547cbc242d801bb3 +Subproject commit 352543365faad595c490fa1d78ac9db8cc71ba0f From fe38da30862bc7ecf29f239bf8b824b241e4ec54 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Mon, 11 Aug 2025 17:04:42 -0400 Subject: [PATCH 097/141] cleanup Signed-off-by: Vladimir Mandic --- pipelines/model_lumina.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pipelines/model_lumina.py b/pipelines/model_lumina.py index c7da1966d..217bbcfa3 100644 --- a/pipelines/model_lumina.py +++ b/pipelines/model_lumina.py @@ -1,12 +1,9 @@ -import os import transformers import diffusers from modules import shared, sd_models, sd_hijack_te, devices, modelloader, model_quant from pipelines import generic - - def load_lumina(_checkpoint_info, diffusers_load_config={}): modelloader.hf_login() load_config, _quant_config = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) From 28ad5bb0c39aa958734ca0384f87c839038db0f6 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 12 Aug 2025 08:15:29 -0400 Subject: [PATCH 098/141] image download use real filename Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 7 ++++--- cli/test-all-models.py | 2 -- javascript/gallery.js | 30 ++++++++++++++++++++++-------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5172f587f..8899ff25b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Change Log for SD.Next -## Update for 2025-08-10 +## Update for 2025-08-12 -### Highlights for 2025-08-10 +### Highlights for 2025-08-12 Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) (plus *Lightning* variant) and [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) and [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) @@ -12,7 +12,7 @@ And (*as always*) many bugfixes and improvements to existing features! [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) -### Details for 2025-08-10 +### Details for 2025-08-12 - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) @@ -70,6 +70,7 @@ And (*as always*) many bugfixes and improvements to existing features! - networks display indicator for currently active items applies to: *styles, loras* - apply privacy blur to hf and civitai tokens + - image download will now use actual image filename - *hint*: card layout card layout is used by networks, gallery, civitai search, etc. you can change card size in *settings -> user interface* diff --git a/cli/test-all-models.py b/cli/test-all-models.py index e781838c2..4fef7b65c 100755 --- a/cli/test-all-models.py +++ b/cli/test-all-models.py @@ -81,8 +81,6 @@ models = { } styles = [ 'Fixed Astronaut', -] -styles_tbd = [ 'Fixed Bear', 'Fixed Steampunk City', 'Fixed Road sign', diff --git a/javascript/gallery.js b/javascript/gallery.js index 9d94235b3..38797551b 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -1,12 +1,4 @@ /* eslint-disable max-classes-per-file */ -// Known issues -// Images flash on the screen before they get processed and separator is properly closed, especially when root/subfolder has large amount of files -// Search is a bit wonky, I tried to get the separators to hide if 0 hits in seperator are found, but no luck so -// Sorting huge amount of images is slow, might look at optimising, I don't think it's a regression. - -// TODO -// Setting to enable or disable separator state persistence - let ws; let url; let currentImage; @@ -663,6 +655,27 @@ async function galleryHidden() { if (pruneImagesTimer) clearInterval(pruneImagesTimer); } +async function monitorGalleries() { + async function galleryMutation(mutations) { + const galleries = mutations.filter((m) => m.target?.classList?.contains('preview')); + for (const gallery of galleries) { + const links = gallery.target.querySelectorAll('a'); + for (const link of links) { + const href = link.getAttribute('href'); + if (!href) continue; + const fn = href.split('/').pop().split('\\').pop(); + link.setAttribute('download', fn); + } + } + } + + const galleryElements = gradioApp().querySelectorAll('.gradio-gallery'); + for (const gallery of galleryElements) { + const galleryObserver = new MutationObserver(galleryMutation); + galleryObserver.observe(gallery, { childList: true, subtree: true, attributes: true }); + } +} + async function initGallery() { // triggered on gradio change to monitor when ui gets sufficiently constructed log('initGallery'); el.folders = gradioApp().getElementById('tab-gallery-folders'); @@ -682,6 +695,7 @@ async function initGallery() { // triggered on gradio change to monitor when ui if (entries[0].intersectionRatio > 0) galleryVisible(); }); intersectionObserver.observe(el.folders); + monitorGalleries(); } // register on startup From 669f9874a0e54a7ec0519ed73552f74025bfb60c Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 12 Aug 2025 08:19:22 -0400 Subject: [PATCH 099/141] increase ui default timeouts Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + modules/shared.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8899ff25b..e79d6e7ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ And (*as always*) many bugfixes and improvements to existing features! applies to: *styles, loras* - apply privacy blur to hf and civitai tokens - image download will now use actual image filename + - increase default and maximum ui request timeout to 2min/5min - *hint*: card layout card layout is used by networks, gallery, civitai search, etc. you can change card size in *settings -> user interface* diff --git a/modules/shared.py b/modules/shared.py index 25ca83b92..fbf0871e9 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -526,7 +526,7 @@ options_templates.update(options_section(('ui', "User Interface"), { "autolaunch": OptionInfo(False, "Autolaunch browser upon startup"), "motd": OptionInfo(False, "Show MOTD"), "subpath": OptionInfo("", "Mount URL subpath"), - "ui_request_timeout": OptionInfo(30000, "UI request timeout", gr.Slider, {"minimum": 1000, "maximum": 120000, "step": 10}), + "ui_request_timeout": OptionInfo(120000, "UI request timeout", gr.Slider, {"minimum": 1000, "maximum": 300000, "step": 10}), "cards_sep_ui": OptionInfo("

Card options

", "", gr.HTML), "extra_networks_card_size": OptionInfo(140, "UI card size (px)", gr.Slider, {"minimum": 20, "maximum": 2000, "step": 1}), From a36676a5a9920c37d7186a7f544af1b1e81a1e34 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 12 Aug 2025 08:49:38 -0400 Subject: [PATCH 100/141] cleanup lora api Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- modules/api/loras.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 352543365..ae66ed9a4 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 352543365faad595c490fa1d78ac9db8cc71ba0f +Subproject commit ae66ed9a4f160628a86b3e8a95e560fef7149e13 diff --git a/modules/api/loras.py b/modules/api/loras.py index 8acc8f0cf..c192ec62d 100644 --- a/modules/api/loras.py +++ b/modules/api/loras.py @@ -7,17 +7,16 @@ def get_lora(lora: str) -> dict: if lora not in lora_load.available_networks: raise HTTPException(status_code=404, detail=f"Lora '{lora}' not found") obj = lora_load.available_networks[lora] - # obj.meta = obj.get_metadata() - # obj.info = obj.get_info() - # obj.desc = obj.get_desc() return obj.__dict__ + def get_loras(): from modules.lora import network, lora_load def create_lora_json(obj: network.NetworkOnDisk): return { "name": obj.name, "alias": obj.alias, "path": obj.filename, "metadata": obj.metadata } return [create_lora_json(obj) for obj in lora_load.available_networks.values()] + def post_refresh_loras(): from modules.lora import lora_load return lora_load.list_available_networks() From 362ec0d41290a6ea452070ec67023a7794da1980 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Tue, 12 Aug 2025 22:07:52 +0300 Subject: [PATCH 101/141] Fix Chroma quantization --- pipelines/generic.py | 8 ++++---- pipelines/model_chroma.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pipelines/generic.py b/pipelines/generic.py index 4983d060a..90f9d034e 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -8,10 +8,10 @@ from modules import shared, devices, errors, sd_models, model_quant debug = os.environ.get('SD_LOAD_DEBUG', None) is not None -def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", allow_quant=True, variant=None, dtype=None): +def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", allow_quant=True, variant=None, dtype=None, modules_to_not_convert=[]): transformer = None try: - load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True, allow_quant=allow_quant) + load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True, allow_quant=allow_quant, modules_to_not_convert=modules_to_not_convert) quant_type = model_quant.get_quant_type(quant_args) dtype = dtype or devices.dtype @@ -71,10 +71,10 @@ def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", return transformer -def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder", allow_quant=True, allow_shared=True, variant=None, dtype=None): +def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder", allow_quant=True, allow_shared=True, variant=None, dtype=None, modules_to_not_convert=[]): text_encoder = None try: - load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True, allow_quant=allow_quant) + load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True, allow_quant=allow_quant, modules_to_not_convert=modules_to_not_convert) quant_type = model_quant.get_quant_type(quant_args) dtype = dtype or devices.dtype diff --git a/pipelines/model_chroma.py b/pipelines/model_chroma.py index eb94bfed4..b5f60d380 100644 --- a/pipelines/model_chroma.py +++ b/pipelines/model_chroma.py @@ -11,7 +11,7 @@ def load_chroma(checkpoint_info, diffusers_load_config={}): load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) shared.log.debug(f'Load model: type=Chroma repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') - transformer = generic.load_transformer(repo_id, cls_name=diffusers.ChromaTransformer2DModel, load_config=diffusers_load_config) + transformer = generic.load_transformer(repo_id, cls_name=diffusers.ChromaTransformer2DModel, load_config=diffusers_load_config, modules_to_not_convert=["distilled_guidance_layer"]) text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.T5EncoderModel, load_config=diffusers_load_config) pipe = diffusers.ChromaPipeline.from_pretrained( From 2aa917b58e7caac8a54e5edbf76fa4d1611b085d Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 12 Aug 2025 11:39:41 -0400 Subject: [PATCH 102/141] add /sdapi/v1/modules endpoint Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + cli/api-checkpoint.py | 2 + cli/test-all-models.py | 207 --------------------------------------- modules/api/api.py | 1 + modules/api/endpoints.py | 24 +++++ 5 files changed, 28 insertions(+), 207 deletions(-) delete mode 100755 cli/test-all-models.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e79d6e7ea..db01163cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ And (*as always*) many bugfixes and improvements to existing features! - **schedulers** add **Flash FlowMatch** - **API** - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model + - add `/sdapi/v1/modules` GET endpoint to get info on model components/modules - **Refactor** - new unified pipeline component loader in `pipelines/generic` - remove **LDSR** diff --git a/cli/api-checkpoint.py b/cli/api-checkpoint.py index 61f4e4370..ff939f64e 100755 --- a/cli/api-checkpoint.py +++ b/cli/api-checkpoint.py @@ -35,3 +35,5 @@ def get(endpoint: str, dct: dict = None): if __name__ == "__main__": model = get('/sdapi/v1/checkpoint') log.info(f'api-checkpoint: {model}') + model = get('/sdapi/v1/modules') + log.info(f'api-modules: {model}') diff --git a/cli/test-all-models.py b/cli/test-all-models.py deleted file mode 100755 index 4fef7b65c..000000000 --- a/cli/test-all-models.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python -""" -Warnings: -- fal/AuraFlow-v0.3: layer_class_name=Linear layer_weight_shape=torch.Size([3072, 2, 1024]) weights_dtype=int8 unsupported -- Kwai-Kolors/Kolors-diffusers: set_input_embeddings not autohandled for ChatGLMModel -- kandinsky-community/kandinsky-2-1: get_input_embeddings not autohandled for MultilingualCLIP -Errors: -- kandinsky-community/kandinsky-3: corrupt output -- Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers: CUDA error device-side assert triggered -Other: -- Wan-AI/Wan2.2-T2V-A14B-Diffusers: extreme memory usage -- HiDream-ai/HiDream-I1-Full: very slow at 30+s/it -""" - -import io -import os -import time -import json -import base64 -import logging -import requests -import urllib3 -import pathvalidate -from PIL import Image - - -logging.basicConfig(level = logging.INFO, format = '%(asctime)s %(levelname)s: %(message)s') -log = logging.getLogger(__name__) -urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - -output_folder = 'outputs/compare' -models = { - "sdxl-base-v10-vaefix": {}, - "tempest-by-vlad-0.1": {}, - "icbinpXL_v6": {}, - "briaai/BRIA-3.2": {}, - "Freepik/F-Lite": {}, - "Freepik/F-Lite-Texture": {}, - "ostris/Flex.2-preview": {}, - "playgroundai/playground-v2-1024px-aesthetic": {}, - "playground-v2.5-1024px-aesthetic.fp16": { "sampler_name": "DPM++ 2M EDM" }, - "stabilityai/stable-diffusion-3.5-medium": {}, - "stabilityai/stable-diffusion-3.5-large": {}, - "fal/AuraFlow-v0.3": {}, - "fal/AuraFlow-v0.2": {}, - "zai-org/CogView4-6B": {}, - "zai-org/CogView3-Plus-3B": {}, - "Qwen/Qwen-Image": {}, - "vladmandic/Qwen-Lightning": {}, - "Shitao/OmniGen-v1-diffusers": {}, - "OmniGen2/OmniGen2": {}, - "Kwai-Kolors/Kolors-diffusers": {}, - "kandinsky-community/kandinsky-2-2-decoder": {}, - "kandinsky-community/kandinsky-2-1": {}, - "kandinsky-community/kandinsky-3": {}, # corrupt output - "Alpha-VLLM/Lumina-Next-SFT-diffusers": {}, - "Alpha-VLLM/Lumina-Image-2.0": {}, - "MeissonFlow/Meissonic": {}, - "Efficient-Large-Model/SANA1.5_1.6B_1024px_diffusers": {}, - "Efficient-Large-Model/SANA1.5_4.8B_1024px_diffusers": {}, - "PixArt-alpha/PixArt-XL-2-1024-MS": {}, - "PixArt-alpha/PixArt-Sigma-XL-2-1024-MS": {}, - "stabilityai/stable-cascade": {}, - "nvidia/Cosmos-Predict2-2B-Text2Image": {}, - "nvidia/Cosmos-Predict2-14B-Text2Image": {}, - "black-forest-labs/FLUX.1-dev": {}, - "black-forest-labs/FLUX.1-Kontext-dev": {}, - "black-forest-labs/FLUX.1-Krea-dev": {}, - "lodestones/Chroma1-HD": {}, - "vladmandic/chroma-unlocked-v50-annealed": {}, - "vladmandic/chroma-unlocked-v48": {}, - "vladmandic/chroma-unlocked-v48-detail-calibrated": {}, - "Wan-AI/Wan2.1-T2V-1.3B-Diffusers": {}, - "Wan-AI/Wan2.1-T2V-14B-Diffusers": {}, - "Wan-AI/Wan2.2-TI2V-5B-Diffusers": {}, - "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers": {}, - "Tencent-Hunyuan/HunyuanDiT-v1.1-Diffusers": {}, - # "HiDream-ai/HiDream-I1-Full": {}, # extreme memory usage due to size - # "Wan-AI/Wan2.2-T2V-A14B-Diffusers": {}, # extreme memory usage due to size -} -styles = [ - 'Fixed Astronaut', - 'Fixed Bear', - 'Fixed Steampunk City', - 'Fixed Road sign', - 'Fixed Futuristic hypercar', - 'Fixed Pirate Ship in Space', - 'Fixed Fallout girl', - 'Fixed Kneeling on Bed', - 'Fixed Girl in Sin City', - 'Fixed Girl in a city', - 'Fixed Girl in Lace', - 'Fixed Lady in Tokyo', - 'Fixed MadMax selfie', - 'Fixed Party Yacht', - 'Fixed Yoga Girls', - 'Fixed SDNext Neon', -] -history = [] - - -def read_history(): - global history # pylint: disable=global-statement - fn = os.path.join(output_folder, 'history.json') - if not os.path.exists(fn): - return - with open(fn, "r", encoding='utf8') as file: - data = file.read() - history = json.loads(data) - log.info(f'history: file="{fn}" records={len(history)}') - - -def write_history(model:str, style:str, image:str='', size:tuple=(0,0), generate:float=0, load:float=0, info:str=''): - fn = os.path.join(output_folder, 'history.json') - history.append({ - 'model': model, - 'title': model.split('/')[-1].replace('_diffusers', '').replace('-diffusers', ''), - 'style': style, - 'image': image, - 'size': size, - 'time': generate, - 'load': load, - 'info': info, - }) - with open(fn, "w", encoding='utf8') as file: - data = json.dumps(history) # pylint: disable=no-member - file.write(data) - - -def request(endpoint: str, dct: dict = None, method: str = 'POST'): - def auth(): - if sd_username is not None and sd_password is not None: - return requests.auth.HTTPBasicAuth(sd_username, sd_password) - return None - 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) - method = requests.post if method.upper() == 'POST' else requests.get - req = method(f'{sd_url}{endpoint}', json = dct, timeout=120000, 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 main(): # pylint: disable=redefined-outer-name - idx_model = 0 - idx_images = 0 - t_generate0 = time.time() - log.info(f'generate: models={len(models)} styles={len(styles)}') - for model, args in models.items(): - t_model0 = time.time() - idx_model += 1 - model_name = pathvalidate.sanitize_filename(model, replacement_text='_') - log.info(f'model: n={idx_model+1}/{len(models)} name="{model}"') - idx_style = 0 - for s, style in enumerate(styles): - try: - model_name = pathvalidate.sanitize_filename(model, replacement_text='_') - style_name = pathvalidate.sanitize_filename(style, replacement_text='_') - fn = os.path.join(output_folder, f'{model_name}__{style_name}.jpg') - if os.path.exists(fn): - continue - t_load0 = time.time() - request(f'/sdapi/v1/checkpoint?sd_model_checkpoint={model}', method='POST') - loaded = request('/sdapi/v1/checkpoint', method='GET') - t_load1 = time.time() - if not loaded or not (model in loaded.get('checkpoint') or model in loaded.get('title') or model in loaded.get('name')): - log.error(f' model: error="{model}"') - continue - t_style0 = time.time() - params = { 'styles': [style] } - for k, v in args.items(): - params[k] = v - log.info(f' style: n={s+1}/{len(styles)} name="{style}" args={params} fn="{fn}"') - data = request('/sdapi/v1/txt2img', params) - t_style1 = time.time() - if 'images' in data and len(data['images']) > 0: - idx_style += 1 - idx_images += 1 - b64 = data['images'][0].split(',',1)[0] - image = Image.open(io.BytesIO(base64.b64decode(b64))) - info = data['info'] - log.info(f' image: size={image.width}x{image.height} time={t_style1-t_style0:.2f} info={len(info)}') - image.save(fn) - write_history(model=model, style=style, image=fn, size=image.size, generate=round(t_style1-t_style0, 3), load=round(t_load1-t_load0, 3), info=info) - else: - log.error(f' model: error="{model}" style="{style}" no image') - except Exception as e: - if 'Connection refused' in str(e) or 'RemoteDisconnected' in str(e): - log.error('server offline') - os._exit(1) - log.error(f' model: error="{model}" style="{style}" exception="{e}"') - t_model1 = time.time() - if idx_style > 0: - log.info(f'model: name="{model}" images={idx_style} time={t_model1-t_model0:.2f}') - t_generate1 = time.time() - if idx_images > 0: - log.info(f'generate: models={idx_model} images={idx_images} time={t_generate1-t_generate0:.2f}') - - -if __name__ == "__main__": - log.info('test-all-models') - log.info(f'output="{output_folder}"') - read_history() - main() diff --git a/modules/api/api.py b/modules/api/api.py index f1fddb392..a260c22ae 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -99,6 +99,7 @@ class Api: self.add_api_route("/sdapi/v1/refresh-vae", endpoints.post_refresh_vae, methods=["POST"]) self.add_api_route("/sdapi/v1/latents", endpoints.get_latent_history, methods=["GET"], response_model=List[str]) self.add_api_route("/sdapi/v1/latents", endpoints.post_latent_history, methods=["POST"], response_model=int) + self.add_api_route("/sdapi/v1/modules", endpoints.get_modules, methods=["GET"]) # lora api from modules.api import loras diff --git a/modules/api/endpoints.py b/modules/api/endpoints.py index 3655f7130..b6f6c7eeb 100644 --- a/modules/api/endpoints.py +++ b/modules/api/endpoints.py @@ -156,6 +156,30 @@ def post_refresh_vae(): shared.refresh_vaes() return {} +def get_modules(): + from modules import modelstats + model = modelstats.analyze() + if model is None: + return {} + model_obj = { + 'model': model.name, + 'type': model.type, + 'class': model.cls, + 'size': model.size, + 'mtime': str(model.mtime), + 'modules': [] + } + for m in model.modules: + model_obj['modules'].append({ + 'class': m.cls, + 'params': m.params, + 'modules': m.modules, + 'quant': m.quant, + 'device': str(m.device), + 'dtype': str(m.dtype) + }) + return model_obj + def get_extensions_list(): from modules import extensions extensions.list_extensions() From 863e172aad27dd0dc08686bf84b22a033f22292c Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 12 Aug 2025 11:55:38 -0400 Subject: [PATCH 103/141] add Qwen/Qwen2.5-VL-3B-Instruct Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + modules/interrogate/vqa.py | 9 +++++++-- scripts/prompt_enhance.py | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db01163cf..ce81cf4cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ And (*as always*) many bugfixes and improvements to existing features! - **Other** - **prompt enhance** add `allura-org/Gemma-3-Glitter-4B` model support - **prompt enhance** add `Qwen/Qwen3-4B-Instruct-2507` model support + - **caption** add `Qwen/Qwen2.5-VL-3B-Instruct` model support - **schedulers** add **Flash FlowMatch** - **API** - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model diff --git a/modules/interrogate/vqa.py b/modules/interrogate/vqa.py index 07f7e7a15..6f419a4e5 100644 --- a/modules/interrogate/vqa.py +++ b/modules/interrogate/vqa.py @@ -29,8 +29,9 @@ vlm_models = { "Google Gemma 3n E4B": "google/gemma-3n-E4B-it", # 1.5GB "Google Pix Textcaps": "google/pix2struct-textcaps-base", # 1.1GB "Google PaliGemma 2 3B": "google/paligemma2-3b-pt-224", - "Alibaba Qwen VL2 2B": "Qwen/Qwen2-VL-2B-Instruct", + "Alibaba Qwen 2.0 VL 2B": "Qwen/Qwen2-VL-2B-Instruct", "Alibaba Qwen 2.5 Omni 3B": "Qwen/Qwen2.5-Omni-3B", + "Alibaba Qwen 2.5 VL 4B": "Qwen/Qwen2.5-VL-3B-Instruct", "Huggingface Smol VL2 0.5B": "HuggingFaceTB/SmolVLM-500M-Instruct", "Huggingface Smol VL2 2B": "HuggingFaceTB/SmolVLM-Instruct", "Salesforce BLIP Base": "Salesforce/blip-vqa-base", # 1.5GB @@ -122,7 +123,11 @@ def qwen(question: str, image: Image.Image, repo: str = None, system_prompt: str if model is None or loaded != repo: shared.log.debug(f'Interrogate load: vlm="{repo}"') model = None - model = transformers.Qwen2VLForConditionalGeneration.from_pretrained( + if '2.5' in repo: + cls_name = transformers.Qwen2_5_VLForConditionalGeneration + else: + cls_name = transformers.Qwen2VLForConditionalGeneration + model = cls_name.from_pretrained( repo, torch_dtype=devices.dtype, cache_dir=shared.opts.hfcache_dir, diff --git a/scripts/prompt_enhance.py b/scripts/prompt_enhance.py index 1950f847e..494c52afa 100644 --- a/scripts/prompt_enhance.py +++ b/scripts/prompt_enhance.py @@ -48,6 +48,7 @@ class Options: 'Qwen/Qwen2.5-0.5B-Instruct': {}, 'Qwen/Qwen2.5-1.5B-Instruct': {}, 'Qwen/Qwen2.5-3B-Instruct': {}, + 'Qwen/Qwen2.5-VL-3B-Instruct': {}, 'microsoft/Phi-4-mini-instruct': {}, 'HuggingFaceTB/SmolLM2-135M-Instruct': {}, 'HuggingFaceTB/SmolLM2-360M-Instruct': {}, From 33b92d9ad941f8c893f6f641f5786228157a0139 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 12 Aug 2025 11:57:40 -0400 Subject: [PATCH 104/141] improve prompt enhance system prompt Signed-off-by: Vladimir Mandic --- scripts/prompt_enhance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prompt_enhance.py b/scripts/prompt_enhance.py index 494c52afa..c6ab184b1 100644 --- a/scripts/prompt_enhance.py +++ b/scripts/prompt_enhance.py @@ -80,7 +80,7 @@ class Options: i2i_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. ' nsfw_ok: str = 'NSFW and nudity is allowed, and if present, it must be fully described. ' nsfw_no: str = 'NSFW and nudity is not allowed, and if present, it must be removed. ' - details_prompt: str = '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.' + details_prompt: str = '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. Do not add comments or follow-up questions. 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 From ffc317c68cf1cb08dfe54cde92df958c9ed2347b Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 12 Aug 2025 15:09:06 -0400 Subject: [PATCH 105/141] update changelog Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce81cf4cc..2ebce514e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,9 +99,8 @@ And (*as always*) many bugfixes and improvements to existing features! - update requirements/packages - use model vae scale-factor for image width/heigt calculations - **Other** - - **prompt enhance** add `allura-org/Gemma-3-Glitter-4B` model support - - **prompt enhance** add `Qwen/Qwen3-4B-Instruct-2507` model support - - **caption** add `Qwen/Qwen2.5-VL-3B-Instruct` model support + - **prompt enhance** add `allura-org/Gemma-3-Glitter-4B`, `Qwen/Qwen3-4B-Instruct-2507`, `Qwen/Qwen2.5-VL-3B-Instruct` model support + - **prompt enhance** improve system prompt - **schedulers** add **Flash FlowMatch** - **API** - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model From a52316b24c92d780b803aa40902e3f2fd1948fe6 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 12 Aug 2025 15:55:43 -0400 Subject: [PATCH 106/141] fix flex.2 Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- modules/sd_detect.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index ae66ed9a4..ad2d6466d 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit ae66ed9a4f160628a86b3e8a95e560fef7149e13 +Subproject commit ad2d6466d1a1fe7cbed0a80318b3957b02c77986 diff --git a/modules/sd_detect.py b/modules/sd_detect.py index a6169e5c3..0ceee93da 100644 --- a/modules/sd_detect.py +++ b/modules/sd_detect.py @@ -113,9 +113,13 @@ def guess_by_name(fn, current_guess): def guess_by_diffusers(fn, current_guess): + exclude_by_name = ['ostris/Flex.2-preview'] # pipeline may be misleading index = os.path.join(fn, 'model_index.json') if os.path.exists(index) and os.path.isfile(index): index = shared.readfile(index, silent=True) + name = index.get('_name_or_path', None) + if name is not None and name in exclude_by_name: + return current_guess, None cls = index.get('_class_name', None) if cls is not None: pipeline = getattr(diffusers, cls, None) From 15cb8fe9f8721060bde2e1104af4397437931bdc Mon Sep 17 00:00:00 2001 From: Disty0 Date: Wed, 13 Aug 2025 00:07:36 +0300 Subject: [PATCH 107/141] SDNQ add modules_dtype_dict and fix Qwen Image with quants less than 5 bits --- modules/model_quant.py | 19 ++++++++++----- modules/sdnq/__init__.py | 52 +++++++++++++++++++++++++++++++++++----- pipelines/generic.py | 8 +++---- pipelines/model_qwen.py | 2 +- 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/modules/model_quant.py b/modules/model_quant.py index 00f42e016..31dcedbf0 100644 --- a/modules/model_quant.py +++ b/modules/model_quant.py @@ -120,7 +120,7 @@ def get_sdnq_devices(): return_device = None return quantization_device, return_device -def create_sdnq_config(kwargs = None, allow: bool = True, module: str = 'Model', weights_dtype: str = None, modules_to_not_convert: list = []): +def create_sdnq_config(kwargs = None, allow: bool = True, module: str = 'Model', weights_dtype: str = None, modules_to_not_convert: list = [], modules_dtype_dict: dict = {}): from modules import shared if allow and (shared.opts.sdnq_quantize_mode in {'pre', 'auto'}) and (module == 'any' or module in shared.opts.sdnq_quantize_weights): from modules.sdnq import SDNQQuantizer, SDNQConfig @@ -150,6 +150,7 @@ def create_sdnq_config(kwargs = None, allow: bool = True, module: str = 'Model', quantization_device=quantization_device, return_device=return_device, modules_to_not_convert=modules_to_not_convert, + modules_dtype_dict=modules_dtype_dict, ) log.debug(f'Quantization: module="{module}" type=sdnq dtype={weights_dtype} matmul={shared.opts.sdnq_use_quantized_matmul} group_size={shared.opts.sdnq_quantize_weights_group_size} quant_conv={shared.opts.sdnq_quantize_conv_layers} matmul_conv={shared.opts.sdnq_use_quantized_matmul_conv} dequantize_fp32={shared.opts.sdnq_dequantize_fp32} quantize_with_gpu={shared.opts.sdnq_quantize_with_gpu} quantization_device={quantization_device} return_device={return_device} device_map={shared.opts.device_map} offload_mode={shared.opts.diffusers_offload_mode} non_blocking={shared.opts.diffusers_offload_nonblocking}') if kwargs is None: @@ -178,10 +179,10 @@ def check_nunchaku(module: str = ''): return True -def create_config(kwargs = None, allow: bool = True, module: str = 'Model', modules_to_not_convert = []): +def create_config(kwargs = None, allow: bool = True, module: str = 'Model', modules_to_not_convert = [], modules_dtype_dict = {}): if kwargs is None: kwargs = {} - kwargs = create_sdnq_config(kwargs, allow=allow, module=module, modules_to_not_convert=modules_to_not_convert) + kwargs = create_sdnq_config(kwargs, allow=allow, module=module, modules_to_not_convert=modules_to_not_convert, modules_dtype_dict=modules_dtype_dict) if kwargs is not None and 'quantization_config' in kwargs: if debug: log.trace(f'Quantization: type=sdnq config={kwargs.get("quantization_config", None)}') @@ -379,7 +380,7 @@ def apply_layerwise(sd_model, quiet:bool=False): log.error(f'Quantization: type=layerwise {e}') -def sdnq_quantize_model(model, op=None, sd_model=None, do_gc: bool = True, weights_dtype: str = None, modules_to_not_convert: list = []): +def sdnq_quantize_model(model, op=None, sd_model=None, do_gc: bool = True, weights_dtype: str = None, modules_to_not_convert: list = [], modules_dtype_dict: dict = {}): global quant_last_model_name, quant_last_model_device # pylint: disable=global-statement from modules import devices, shared, timer from modules.sdnq import apply_sdnq_to_module @@ -403,6 +404,11 @@ def sdnq_quantize_model(model, op=None, sd_model=None, do_gc: bool = True, weigh modules_to_not_convert.extend(model._skip_layerwise_casting_patterns) # pylint: disable=protected-access if model.__class__.__name__ == "ChromaTransformer2DModel": modules_to_not_convert.append("distilled_guidance_layer") + if model.__class__.__name__ == "QwenImageTransformer2DModel": + if "minimum_6bit" not in modules_dtype_dict.keys(): + modules_dtype_dict["minimum_6bit"] = ["img_mod", "pos_embed", "time_text_embed", "img_in", "txt_in", "norm_out"] + else: + modules_dtype_dict["minimum_6bit"].extend(["img_mod", "pos_embed", "time_text_embed", "img_in", "txt_in", "norm_out"]) model.eval() backup_embeddings = None @@ -424,6 +430,7 @@ def sdnq_quantize_model(model, op=None, sd_model=None, do_gc: bool = True, weigh return_device=return_device, param_name=op, modules_to_not_convert=modules_to_not_convert, + modules_dtype_dict=modules_dtype_dict, ) t1 = time.time() timer.load.add('sdnq', t1 - t0) @@ -610,7 +617,7 @@ def torchao_quantization(sd_model): return sd_model -def get_dit_args(load_config:dict={}, module:str=None, device_map:bool=False, allow_quant:bool=True, modules_to_not_convert: list = []): +def get_dit_args(load_config:dict={}, module:str=None, device_map:bool=False, allow_quant:bool=True, modules_to_not_convert: list = [], modules_dtype_dict: dict = {}): from modules import shared, devices config = load_config.copy() if 'torch_dtype' not in config: @@ -633,7 +640,7 @@ def get_dit_args(load_config:dict={}, module:str=None, device_map:bool=False, al elif shared.opts.device_map == 'gpu': config['device_map'] = devices.device if allow_quant: - quant_args = create_config(module=module, modules_to_not_convert=modules_to_not_convert) + quant_args = create_config(module=module, modules_to_not_convert=modules_to_not_convert, modules_dtype_dict=modules_dtype_dict) else: quant_args = {} return config, quant_args diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index e1c8396f9..d79c8aa0f 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -186,14 +186,29 @@ def sdnq_quantize_layer(layer, weights_dtype="int8", torch_dtype=None, group_siz return layer -def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_size=0, quant_conv=False, use_quantized_matmul=False, use_quantized_matmul_conv=False, dequantize_fp32=False, non_blocking=False, quantization_device=None, return_device=None, param_name=None, modules_to_not_convert: List[str] = []): # pylint: disable=unused-argument +def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_size=0, quant_conv=False, use_quantized_matmul=False, use_quantized_matmul_conv=False, dequantize_fp32=False, non_blocking=False, quantization_device=None, return_device=None, param_name=None, modules_to_not_convert: List[str] = [], modules_dtype_dict: Dict[str, List[str]] = {}): # pylint: disable=unused-argument has_children = list(model.children()) if not has_children: return model - for module_param_name, module in model.named_children(): - if module_param_name in modules_to_not_convert: + for param_name, module in model.named_children(): + if param_name in modules_to_not_convert: continue if hasattr(module, "weight") and module.weight is not None: + if len(modules_dtype_dict.keys()) > 0: + for key, value in modules_dtype_dict.items(): + if param_name in value: + key = key.lower() + if key in {"8bit", "8bits"}: + if dtype_dict[weights_dtype]["num_bits"] != 8: + weights_dtype = "int8" + elif key.startswith("minimum_"): + minimum_bits = key.removeprefix("minimum_").removesuffix("bits").removesuffix("bit") + if dtype_dict[weights_dtype]["num_bits"] < int(minimum_bits): + weights_dtype = "int" + minimum_bits + else: + weights_dtype = key + break + module = sdnq_quantize_layer( module, weights_dtype=weights_dtype, @@ -206,7 +221,7 @@ def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_si non_blocking=non_blocking, quantization_device=quantization_device, return_device=return_device, - param_name=module_param_name, + param_name=param_name, ) module = apply_sdnq_to_module( module, @@ -220,8 +235,9 @@ def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_si non_blocking=non_blocking, quantization_device=quantization_device, return_device=return_device, - param_name=module_param_name, + param_name=param_name, modules_to_not_convert=modules_to_not_convert, + modules_dtype_dict=modules_dtype_dict, ) return model @@ -280,6 +296,23 @@ class SDNQQuantizer(DiffusersQuantizer): unexpected_keys: List[str], # pylint: disable=unused-argument **kwargs, # pylint: disable=unused-argument ): + weights_dtype = self.quantization_config.weights_dtype + if len(self.quantization_config.modules_dtype_dict.keys()) > 0: + split_param_name = param_name.split(".") + for key, value in self.quantization_config.modules_dtype_dict.items(): + if param_name in value or any(param in split_param_name for param in value): + key = key.lower() + if key in {"8bit", "8bits"}: + if dtype_dict[weights_dtype]["num_bits"] != 8: + weights_dtype = "int8" + elif key.startswith("minimum_"): + minimum_bits = key.removeprefix("minimum_").removesuffix("bits").removesuffix("bit") + if dtype_dict[weights_dtype]["num_bits"] < int(minimum_bits): + weights_dtype = "int" + minimum_bits + else: + weights_dtype = key + break + if self.quantization_config.return_device is not None: return_device = self.quantization_config.return_device else: @@ -297,7 +330,7 @@ class SDNQQuantizer(DiffusersQuantizer): layer.weight = torch.nn.Parameter(param_value, requires_grad=False) layer = sdnq_quantize_layer( layer, - weights_dtype=self.quantization_config.weights_dtype, + weights_dtype=weights_dtype, torch_dtype=self.torch_dtype, group_size=self.quantization_config.group_size, quant_conv=self.quantization_config.quant_conv, @@ -421,6 +454,8 @@ class SDNQConfig(QuantizationConfigMixin): modules_to_not_convert (`list`, *optional*, default to `None`): The list of modules to not quantize, useful for quantizing models that explicitly require to have some modules left in their original precision (e.g. Whisper encoder, Llava encoder, Mixtral gate layers). + modules_to_not_convert (`dict`, *optional*, default to `None`): + The dict of dtypes and list of modules, useful for quantizing some modules with a different dtype. """ def __init__( # pylint: disable=super-init-not-called @@ -435,6 +470,7 @@ class SDNQConfig(QuantizationConfigMixin): quantization_device: Optional[torch.device] = None, return_device: Optional[torch.device] = None, modules_to_not_convert: Optional[List[str]] = None, + modules_dtype_dict: Optional[Dict[str, List[str]]] = None, **kwargs, # pylint: disable=unused-argument ): self.weights_dtype = weights_dtype @@ -448,6 +484,7 @@ class SDNQConfig(QuantizationConfigMixin): self.quantization_device = quantization_device self.return_device = return_device self.modules_to_not_convert = modules_to_not_convert + self.modules_dtype_dict = modules_dtype_dict self.post_init() self.is_integer = dtype_dict[self.weights_dtype]["is_integer"] @@ -463,3 +500,6 @@ class SDNQConfig(QuantizationConfigMixin): self.modules_to_not_convert = [] elif not isinstance(self.modules_to_not_convert, list): self.modules_to_not_convert = [self.modules_to_not_convert] + + if self.modules_dtype_dict is None: + self.modules_dtype_dict = {} diff --git a/pipelines/generic.py b/pipelines/generic.py index 90f9d034e..696eff3d6 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -8,10 +8,10 @@ from modules import shared, devices, errors, sd_models, model_quant debug = os.environ.get('SD_LOAD_DEBUG', None) is not None -def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", allow_quant=True, variant=None, dtype=None, modules_to_not_convert=[]): +def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", allow_quant=True, variant=None, dtype=None, modules_to_not_convert=[], modules_dtype_dict={}): transformer = None try: - load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True, allow_quant=allow_quant, modules_to_not_convert=modules_to_not_convert) + load_args, quant_args = model_quant.get_dit_args(load_config, module='Model', device_map=True, allow_quant=allow_quant, modules_to_not_convert=modules_to_not_convert, modules_dtype_dict=modules_dtype_dict) quant_type = model_quant.get_quant_type(quant_args) dtype = dtype or devices.dtype @@ -71,10 +71,10 @@ def load_transformer(repo_id, cls_name, load_config={}, subfolder="transformer", return transformer -def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder", allow_quant=True, allow_shared=True, variant=None, dtype=None, modules_to_not_convert=[]): +def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder", allow_quant=True, allow_shared=True, variant=None, dtype=None, modules_to_not_convert=[], modules_dtype_dict={}): text_encoder = None try: - load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True, allow_quant=allow_quant, modules_to_not_convert=modules_to_not_convert) + load_args, quant_args = model_quant.get_dit_args(load_config, module='TE', device_map=True, allow_quant=allow_quant, modules_to_not_convert=modules_to_not_convert, modules_dtype_dict=modules_dtype_dict) quant_type = model_quant.get_quant_type(quant_args) dtype = dtype or devices.dtype diff --git a/pipelines/model_qwen.py b/pipelines/model_qwen.py index 06464938b..61b904171 100644 --- a/pipelines/model_qwen.py +++ b/pipelines/model_qwen.py @@ -11,7 +11,7 @@ def load_qwen(checkpoint_info, diffusers_load_config={}): load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, module='Model') shared.log.debug(f'Load model: type=Qwen model="{checkpoint_info.name}" repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') - transformer = generic.load_transformer(repo_id, cls_name=diffusers.QwenImageTransformer2DModel, load_config=diffusers_load_config) + transformer = generic.load_transformer(repo_id, cls_name=diffusers.QwenImageTransformer2DModel, load_config=diffusers_load_config, modules_dtype_dict={"minimum_6bit": ["img_mod", "pos_embed", "time_text_embed", "img_in", "txt_in", "norm_out"]}) repo_te = 'Qwen/Qwen-Image' if 'Qwen-Lightning' in repo_id else repo_id text_encoder = generic.load_text_encoder(repo_te, cls_name=transformers.Qwen2_5_VLForConditionalGeneration, load_config=diffusers_load_config) From 7085db9add9f7ba5489d92fb3fce354bcbfa7ae4 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Wed, 13 Aug 2025 00:17:15 +0300 Subject: [PATCH 108/141] Update changelog --- CHANGELOG.md | 1 + modules/sdnq/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ebce514e..2d3753368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ And (*as always*) many bugfixes and improvements to existing features! - **Nunchaku** support for *FLUX.1-Fill* and *FLUX.1-Depth* models - update requirements/packages - use model vae scale-factor for image width/heigt calculations + - **SDNQ** add modules_dtype_dict to quantize *Qwen Image* with mixed dtype - **Other** - **prompt enhance** add `allura-org/Gemma-3-Glitter-4B`, `Qwen/Qwen3-4B-Instruct-2507`, `Qwen/Qwen2.5-VL-3B-Instruct` model support - **prompt enhance** improve system prompt diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index d79c8aa0f..924283149 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -454,7 +454,7 @@ class SDNQConfig(QuantizationConfigMixin): modules_to_not_convert (`list`, *optional*, default to `None`): The list of modules to not quantize, useful for quantizing models that explicitly require to have some modules left in their original precision (e.g. Whisper encoder, Llava encoder, Mixtral gate layers). - modules_to_not_convert (`dict`, *optional*, default to `None`): + modules_dtype_dict (`dict`, *optional*, default to `None`): The dict of dtypes and list of modules, useful for quantizing some modules with a different dtype. """ From cb0c5414a3622578f81b17c26b56bf9da57233cd Mon Sep 17 00:00:00 2001 From: Disty0 Date: Wed, 13 Aug 2025 00:37:44 +0300 Subject: [PATCH 109/141] SDNQ use uint with minimum_bits <= 4 --- modules/sdnq/__init__.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index 924283149..ba4f7b9fb 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -202,9 +202,12 @@ def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_si if dtype_dict[weights_dtype]["num_bits"] != 8: weights_dtype = "int8" elif key.startswith("minimum_"): - minimum_bits = key.removeprefix("minimum_").removesuffix("bits").removesuffix("bit") - if dtype_dict[weights_dtype]["num_bits"] < int(minimum_bits): - weights_dtype = "int" + minimum_bits + minimum_bits_str = key.removeprefix("minimum_").removesuffix("bits").removesuffix("bit") + minimum_bits = int(minimum_bits_str) + if dtype_dict[weights_dtype]["num_bits"] < minimum_bits: + weights_dtype = "int" + minimum_bits_str + if minimum_bits <= 4: + weights_dtype = "u" + weights_dtype else: weights_dtype = key break @@ -306,9 +309,12 @@ class SDNQQuantizer(DiffusersQuantizer): if dtype_dict[weights_dtype]["num_bits"] != 8: weights_dtype = "int8" elif key.startswith("minimum_"): - minimum_bits = key.removeprefix("minimum_").removesuffix("bits").removesuffix("bit") - if dtype_dict[weights_dtype]["num_bits"] < int(minimum_bits): - weights_dtype = "int" + minimum_bits + minimum_bits_str = key.removeprefix("minimum_").removesuffix("bits").removesuffix("bit") + minimum_bits = int(minimum_bits_str) + if dtype_dict[weights_dtype]["num_bits"] < minimum_bits: + weights_dtype = "int" + minimum_bits_str + if minimum_bits <= 4: + weights_dtype = "u" + weights_dtype else: weights_dtype = key break From 532e89b4fad4604e26d5cf2ccea5280f3ead849e Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 10:56:10 -0400 Subject: [PATCH 110/141] add hunyuandit-distilled Signed-off-by: Vladimir Mandic --- html/reference.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/html/reference.json b/html/reference.json index 5fd79f36a..d2d1e6e6d 100644 --- a/html/reference.json +++ b/html/reference.json @@ -429,12 +429,24 @@ "preview": "Tencent-Hunyuan--HunyuanDiT-v1.2-Diffusers.jpg", "extras": "sampler: Default, cfg_scale: 2.0" }, + "Tencent HunyuanDiT 1.2 Distilled": { + "path": "Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers-Distilled", + "desc": "Hunyuan-DiT : A Powerful Multi-Resolution Diffusion Transformer with Fine-Grained Chinese Understanding.", + "preview": "Tencent-Hunyuan--HunyuanDiT-v1.2-Diffusers.jpg", + "extras": "sampler: Default, cfg_scale: 2.0" + }, "Tencent HunyuanDiT 1.1": { "path": "Tencent-Hunyuan/HunyuanDiT-v1.1-Diffusers", "desc": "Hunyuan-DiT : A Powerful Multi-Resolution Diffusion Transformer with Fine-Grained Chinese Understanding.", "preview": "Tencent-Hunyuan--HunyuanDiT-v1.2-Diffusers.jpg", "extras": "sampler: Default, cfg_scale: 2.0" }, + "Tencent HunyuanDiT 1.1 Distilled": { + "path": "Tencent-Hunyuan/HunyuanDiT-v1.1-Diffusers-Distilled", + "desc": "Hunyuan-DiT : A Powerful Multi-Resolution Diffusion Transformer with Fine-Grained Chinese Understanding.", + "preview": "Tencent-Hunyuan--HunyuanDiT-v1.2-Diffusers.jpg", + "extras": "sampler: Default, cfg_scale: 2.0" + }, "AlphaVLLM Lumina Next SFT": { "path": "Alpha-VLLM/Lumina-Next-SFT-diffusers", From 8e9244939b802cf6afb47b30e891d8b5086fb662 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 12:23:29 -0400 Subject: [PATCH 111/141] calc folder size Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 21 ++++++++++++--------- TODO.md | 5 +++++ modules/api/gallery.py | 14 +++++++------- modules/modelstats.py | 29 ++++++++++++++++++++++++----- modules/ui_extra_networks.py | 29 +++++++++++++---------------- modules/ui_gallery.py | 13 ++++++------- modules/ui_models.py | 10 ++++------ 7 files changed, 71 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3753368..848121d24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,18 @@ # Change Log for SD.Next -## Update for 2025-08-12 +## Update for 2025-08-13 -### Highlights for 2025-08-12 +### Highlights for 2025-08-13 Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) (plus *Lightning* variant) and [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) -Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers) and [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) +Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers), [HunyuanDiT](https://huggingface.co/Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers-Distilled) Plus continuing with major **UI** work, we have new embedded **Docs/Wiki** search, redesigned real-time **hints**, built-in **GPU monitor**, **CivitAI** integration and more! -On the compute side, new profiles for high-vram GPUs, offloading improvements and support for new `torch` release +On the compute side, new profiles for high-vram GPUs, offloading improvements, support for new `torch` release and improved quality when using low-bit quantization! And (*as always*) many bugfixes and improvements to existing features! [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) -### Details for 2025-08-12 +### Details for 2025-08-13 - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) @@ -38,6 +38,8 @@ And (*as always*) many bugfixes and improvements to existing features! - [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) basic support for *Wan 2.1 VACE 1.3B* and *14B* variants optimized support with granular guidance control will follow soon + - [HunyuanDiT-Distilled](https://huggingface.co/Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers-Distilled) + variant of HunyuanDiT with reduced steps and improved performance **Torch** - Set default to `torch==2.8.0` for *CUDA, ROCm and OpenVINO* - Add support for `torch==2.9.0-nightly` @@ -98,10 +100,10 @@ And (*as always*) many bugfixes and improvements to existing features! - **Nunchaku** support for *FLUX.1-Fill* and *FLUX.1-Depth* models - update requirements/packages - use model vae scale-factor for image width/heigt calculations - - **SDNQ** add modules_dtype_dict to quantize *Qwen Image* with mixed dtype -- **Other** - - **prompt enhance** add `allura-org/Gemma-3-Glitter-4B`, `Qwen/Qwen3-4B-Instruct-2507`, `Qwen/Qwen2.5-VL-3B-Instruct` model support - - **prompt enhance** improve system prompt + - **SDNQ** add `modules_dtype_dict` to quantize *Qwen Image* with mixed dtype + - **prompt enhance** + add `allura-org/Gemma-3-Glitter-4B`, `Qwen/Qwen3-4B-Instruct-2507`, `Qwen/Qwen2.5-VL-3B-Instruct` model support + improve system prompt - **schedulers** add **Flash FlowMatch** - **API** - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model @@ -131,6 +133,7 @@ And (*as always*) many bugfixes and improvements to existing features! - fix `nudenet` api - fix global state tracking - fix ui tab detection for networks + - unified stat size/mtime calls - reapply offloading on ipadapter load - api set default script-name - avoid forced gc and rely on thresholds diff --git a/TODO.md b/TODO.md index 6116f4b45..bbce04ade 100644 --- a/TODO.md +++ b/TODO.md @@ -2,11 +2,16 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladmandic/projects) +## Current Candidates + +- `HF_ENABLE_PARALLEL_LOADING` + ## Future Candidates - Unified `CLIPTextModelWithProjection` loader - [Modular pipelines and guiders](https://github.com/huggingface/diffusers/issues/11915) - Refactor: Sampler options +- Refactor: [GGUF](https://huggingface.co/docs/diffusers/main/en/quantization/gguf) - Feature: Diffusers [group offloading](https://github.com/vladmandic/sdnext/issues/4049) - Feature: Common repo for `T5` and `CLiP` - Feature: LoRA add OMI format support for SD35/FLUX.1 diff --git a/modules/api/gallery.py b/modules/api/gallery.py index 6510cd673..feed71bf5 100644 --- a/modules/api/gallery.py +++ b/modules/api/gallery.py @@ -9,7 +9,7 @@ from fastapi.responses import JSONResponse from starlette.websockets import WebSocket, WebSocketState from pydantic import BaseModel, Field # pylint: disable=no-name-in-module from PIL import Image -from modules import shared, images, files_cache +from modules import shared, images, files_cache, modelstats debug = shared.log.debug if os.environ.get('SD_BROWSER_DEBUG', None) is not None else lambda *args, **kwargs: None @@ -76,7 +76,7 @@ def register_api(app: FastAPI): # register api def get_video_thumbnail(filepath): from modules.video import get_video_params try: - stat = os.stat(filepath) + stat_size, stat_mtime = modelstats.stat(filepath) frames, fps, duration, width, height, codec, frame = get_video_params(filepath, capture=True) h = shared.opts.extra_networks_card_size w = shared.opts.extra_networks_card_size if shared.opts.browser_fixed_width else width * h // height @@ -91,8 +91,8 @@ def register_api(app: FastAPI): # register api 'data': data_url, 'width': width, 'height': height, - 'size': stat.st_size, - 'mtime': stat.st_mtime, + 'size': stat_size, + 'mtime': stat_mtime, } return content except Exception as e: @@ -101,7 +101,7 @@ def register_api(app: FastAPI): # register api def get_image_thumbnail(filepath): try: - stat = os.stat(filepath) + stat_size, stat_mtime = modelstats.stat(filepath) image = Image.open(filepath) geninfo, _items = images.read_info_from_image(image) h = shared.opts.extra_networks_card_size @@ -118,8 +118,8 @@ def register_api(app: FastAPI): # register api 'data': data_url, 'width': width, 'height': height, - 'size': stat.st_size, - 'mtime': stat.st_mtime, + 'size': stat_size, + 'mtime': stat_mtime, } return content except Exception as e: diff --git a/modules/modelstats.py b/modules/modelstats.py index 4d60d34e5..18eb84c40 100644 --- a/modules/modelstats.py +++ b/modules/modelstats.py @@ -4,6 +4,29 @@ import torch from modules import shared, sd_models +def walk(folder: str): + files = [] + for root, _, filenames in os.walk(folder): + for filename in filenames: + files.append(os.path.join(root, filename)) + return files + + +def stat(fn: str): + if fn is None or len(fn) == 0 or not os.path.exists(fn): + return 0, None + fs_stat = os.stat(fn) + mtime = datetime.fromtimestamp(fs_stat.st_mtime).replace(microsecond=0) + if os.path.isfile(fn): + size = round(fs_stat.st_size) + elif os.path.isdir(fn): + size = round(sum(stat(fn)[0] for fn in walk(fn))) + else: + size = 0 + print('HERE', fn, os.path.isfile(fn), os.path.isdir(fn), size, mtime) + return size, mtime + + class Module(): name: str = '' cls: str = None @@ -60,11 +83,7 @@ class Model(): self.name = self.info.name or self.name self.hash = self.info.shorthash or '' self.meta = self.info.metadata or {} - if os.path.exists(self.info.filename): - stat = os.stat(self.info.filename) - self.mtime = datetime.fromtimestamp(stat.st_mtime).replace(microsecond=0) - if os.path.isfile(self.info.filename): - self.size = round(stat.st_size) + self.size, self.mtime = stat(self.info.filename) def __repr__(self): return f'model="{self.name}" type={self.type} class={self.cls} size={self.size} mtime="{self.mtime}" modules={self.modules}' diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 33e63b85b..47859a003 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -8,7 +8,6 @@ import html import base64 import urllib.parse import threading -from datetime import datetime from types import SimpleNamespace from pathlib import Path from html.parser import HTMLParser @@ -16,9 +15,7 @@ from collections import OrderedDict import gradio as gr from PIL import Image from starlette.responses import FileResponse, JSONResponse -from modules import paths, shared, files_cache, errors, infotext -from modules.ui_components import ToolButton -import modules.ui_symbols as symbols +from modules import paths, shared, files_cache, errors, infotext, ui_symbols, ui_components, modelstats allowed_dirs = [] @@ -612,7 +609,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): return is_visible, gr.update(visible=is_visible), gr.update(variant=("secondary-down" if is_visible else "secondary")) with ui.details: - details_close = ToolButton(symbols.close, elem_id=f"{tabname}_extra_details_close", elem_classes=['extra-details-close']) + details_close = ui_components.ToolButton(ui_symbols.close, elem_id=f"{tabname}_extra_details_close", elem_classes=['extra-details-close']) details_close.click(fn=lambda: gr.update(visible=False), inputs=[], outputs=[ui.details]) with gr.Row(): with gr.Column(scale=1): @@ -665,14 +662,14 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): model_visible = page in ['Model'] return [gr.update(visible=scan_visible), gr.update(visible=save_visible), gr.update(visible=model_visible)] - ui.button_refresh = ToolButton(symbols.refresh, elem_id=f"{tabname}_extra_refresh") - ui.button_scan = ToolButton(symbols.scan, elem_id=f"{tabname}_extra_scan", visible=True) - ui.button_quicksave = ToolButton(symbols.book, elem_id=f"{tabname}_extra_quicksave", visible=False) - ui.button_save = ToolButton(symbols.book, elem_id=f"{tabname}_extra_save", visible=False) - ui.button_sort = ToolButton(symbols.sort, elem_id=f"{tabname}_extra_sort", visible=True) - ui.button_view = ToolButton(symbols.view, elem_id=f"{tabname}_extra_view", visible=True) - ui.button_close = ToolButton(symbols.close, elem_id=f"{tabname}_extra_close", visible=True) - ui.button_model = ToolButton(symbols.refine, elem_id=f"{tabname}_extra_model", visible=True) + ui.button_refresh = ui_components.ToolButton(ui_symbols.refresh, elem_id=f"{tabname}_extra_refresh") + ui.button_scan = ui_components.ToolButton(ui_symbols.scan, elem_id=f"{tabname}_extra_scan", visible=True) + ui.button_quicksave = ui_components.ToolButton(ui_symbols.book, elem_id=f"{tabname}_extra_quicksave", visible=False) + ui.button_save = ui_components.ToolButton(ui_symbols.book, elem_id=f"{tabname}_extra_save", visible=False) + ui.button_sort = ui_components.ToolButton(ui_symbols.sort, elem_id=f"{tabname}_extra_sort", visible=True) + ui.button_view = ui_components.ToolButton(ui_symbols.view, elem_id=f"{tabname}_extra_view", visible=True) + ui.button_close = ui_components.ToolButton(ui_symbols.close, elem_id=f"{tabname}_extra_close", visible=True) + ui.button_model = ui_components.ToolButton(ui_symbols.refine, elem_id=f"{tabname}_extra_model", visible=True) ui.search = gr.Textbox('', show_label=False, elem_id=f"{tabname}_extra_search", placeholder="Search...", elem_classes="textbox", lines=2, container=False) ui.description = gr.Textbox('', show_label=False, elem_id=f"{tabname}_description", elem_classes=["textbox", "extra-description"], lines=2, interactive=False, container=False) @@ -802,7 +799,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): is_valid = (item is not None) and hasattr(item, 'name') and hasattr(item, 'filename') if is_valid: - stat = os.stat(item.filename) if os.path.exists(item.filename) else None + stat_size, stat_mtime = modelstats.stat(item.filename) desc = item.description fullinfo = shared.readfile(os.path.splitext(item.filename)[0] + '.json', silent=True) if 'modelVersions' in fullinfo: # sanitize massive objects @@ -892,8 +889,8 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
- - + + {lora} diff --git a/modules/ui_gallery.py b/modules/ui_gallery.py index e6dd3757e..07d2dec26 100644 --- a/modules/ui_gallery.py +++ b/modules/ui_gallery.py @@ -1,9 +1,8 @@ import os -from datetime import datetime from urllib.parse import unquote import gradio as gr from PIL import Image -from modules import shared, ui_symbols, ui_common, images, video +from modules import shared, ui_symbols, ui_common, images, video, modelstats from modules.ui_components import ToolButton @@ -12,7 +11,7 @@ def read_media(fn): if not os.path.isfile(fn): shared.log.error(f'Gallery not found: file="{fn}"') return [[], None, '', '', f'Media not found: {fn}'] - stat = os.stat(fn) + stat_size, stat_mtime = modelstats.stat(fn) if fn.lower().endswith('.mp4'): frames, fps, duration, w, h, codec, _frame = video.get_video_params(fn) geninfo = '' @@ -22,8 +21,8 @@ def read_media(fn): | Frames {frames:,} | FPS {fps:.2f} | Duration {duration:.2f} - | Size {stat.st_size:,} - | Modified {datetime.fromtimestamp(stat.st_mtime)}


+ | Size {stat_size:,} + | Modified {stat_mtime}


''' return [gr.update(visible=False, value=[]), gr.update(visible=True, value=fn), geninfo, geninfo, log] else: @@ -34,8 +33,8 @@ def read_media(fn):

Image {image.width} x {image.height} | Format {image.format} | Mode {image.mode} - | Size {stat.st_size:,} - | Modified {datetime.fromtimestamp(stat.st_mtime)}


+ | Size {stat_size:,} + | Modified {stat_mtime}


''' return [gr.update(visible=True, value=[image]), gr.update(visible=False), geninfo, geninfo, log] diff --git a/modules/ui_models.py b/modules/ui_models.py index 397a5da5e..71b96ba93 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -1,8 +1,7 @@ import os import inspect -from datetime import datetime import gradio as gr -from modules import errors, sd_models, sd_vae, extras, sd_samplers, ui_symbols +from modules import errors, sd_models, sd_vae, extras, sd_samplers, ui_symbols, modelstats from modules.ui_components import ToolButton from modules.ui_common import create_refresh_button from modules.call_queue import wrap_gradio_gpu_call @@ -57,7 +56,6 @@ def create_ui(): return html.format(tbody=tbody) def analyze(): - from modules import modelstats model = modelstats.analyze() if model is None: return ["Model not loaded", {}] @@ -93,10 +91,10 @@ def create_ui(): for row in rows: try: f = row.filename - stat = os.stat(row.filename) + stat_size, stat_mtime = modelstats.stat(f) if os.path.isfile(f): typ = os.path.splitext(f)[1][1:] - size = f"{round(stat.st_size / 1024 / 1024 / 1024, 3)} gb" + size = f"{round(stat_size / 1024 / 1024 / 1024, 3)} gb" elif os.path.isdir(f): typ = 'diffusers' size = 'folder' @@ -117,7 +115,7 @@ def create_ui(): - + """ except Exception as e: From 562799314dca8c654167642d5f6219813c8bb9af Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 13:04:14 -0400 Subject: [PATCH 112/141] change default hfcache folder Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 5 +++++ launch.py | 2 +- modules/modelstats.py | 7 ++++--- modules/paths.py | 29 ++++++++++++----------------- modules/paths_internal.py | 27 --------------------------- modules/shared.py | 12 ++++++------ webui.py | 2 +- 7 files changed, 29 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 848121d24..ac5bda9a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Plus continuing with major **UI** work, we have new embedded **Docs/Wiki** searc On the compute side, new profiles for high-vram GPUs, offloading improvements, support for new `torch` release and improved quality when using low-bit quantization! And (*as always*) many bugfixes and improvements to existing features! +*Note*: Change-in-behavior - locations of downloaded HuggingFace models and components are changed to allow for de-duplication of common modules and switched from using system default cache folder to `models/huggingface` +SD.Next will warn on startup on unused cache entries that can be removed. Also, to take advantage of de-duplication, you'll need to delete models from your `models/Diffusers` folder and let SD.Next re-download them! + [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) ### Details for 2025-08-13 @@ -109,6 +112,8 @@ And (*as always*) many bugfixes and improvements to existing features! - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model - add `/sdapi/v1/modules` GET endpoint to get info on model components/modules - **Refactor** + - change default huggingface cache folder from system default to `models/huggingface` + sd.next will warn on startup on unused cache entries - new unified pipeline component loader in `pipelines/generic` - remove **LDSR** - remove `api-only` cli option diff --git a/launch.py b/launch.py index 85ea0337a..423842b1b 100755 --- a/launch.py +++ b/launch.py @@ -45,9 +45,9 @@ def init_args(): def init_paths(): global script_path, extensions_dir # pylint: disable=global-statement import modules.paths - modules.paths.register_paths() script_path = modules.paths.script_path extensions_dir = modules.paths.extensions_dir + sys.path.insert(0, script_path) rec('paths') diff --git a/modules/modelstats.py b/modules/modelstats.py index 18eb84c40..a91104fa5 100644 --- a/modules/modelstats.py +++ b/modules/modelstats.py @@ -15,15 +15,16 @@ def walk(folder: str): def stat(fn: str): if fn is None or len(fn) == 0 or not os.path.exists(fn): return 0, None - fs_stat = os.stat(fn) + fs_stat = os.stat(fn, follow_symlinks=False) mtime = datetime.fromtimestamp(fs_stat.st_mtime).replace(microsecond=0) - if os.path.isfile(fn): + if os.path.islink(fn): + size = 0 + elif os.path.isfile(fn): size = round(fs_stat.st_size) elif os.path.isdir(fn): size = round(sum(stat(fn)[0] for fn in walk(fn))) else: size = 0 - print('HERE', fn, os.path.isfile(fn), os.path.isdir(fn), size, mtime) return size, mtime diff --git a/modules/paths.py b/modules/paths.py index 5e0515ebd..71d2f67a0 100644 --- a/modules/paths.py +++ b/modules/paths.py @@ -44,23 +44,6 @@ if os.environ.get('SD_PATH_DEBUG', None) is not None: log.debug(f'Paths: script-path="{script_path}" data-dir="{data_path}" models-dir="{models_path}" config="{config_path}"') -def register_paths(): - log.debug('Register paths') - sys.path.insert(0, script_path) - # sd_path = os.path.join(script_path, 'repositories') - path_dirs = [ - # (os.path.join(sd_path, 'codeformer'), 'inference_codeformer.py', 'CodeFormer', []), - ] - for d, must_exist, what, _options in path_dirs: - must_exist_path = os.path.abspath(os.path.join(script_path, d, must_exist)) - if not os.path.exists(must_exist_path): - log.error(f'Required path not found: path={must_exist_path} item={what}') - else: - d = os.path.abspath(d) - sys.path.append(d) - paths[what] = d - - def create_path(folder): if folder is None or folder == '': return @@ -103,6 +86,7 @@ def create_paths(opts): create_path(fix_path('temp_dir')) create_path(fix_path('ckpt_dir')) create_path(fix_path('diffusers_dir')) + create_path(fix_path('hfcache_dir')) create_path(fix_path('vae_dir')) create_path(fix_path('unet_dir')) create_path(fix_path('te_dir')) @@ -139,3 +123,14 @@ class Prioritize: def __exit__(self, exc_type, exc_val, exc_tb): sys.path = self.path self.path = None + + +def check_cache(opts): + prev_default = os.environ.get("SD_HFCACHEDIR", None) or os.path.join(os.path.expanduser('~'), '.cache', 'huggingface', 'hub') + from modules.modelstats import stat + if opts.hfcache_dir != prev_default: + size, _mtime = stat(prev_default) + if (size//1024//1024 > 0): + log.warning(f'Cache location changed: previous="{prev_default}" size={size//1024//1024} MB') + size, _mtime = stat(opts.hfcache_dir) + log.debug(f'Huggingface cache: path="{opts.hfcache_dir}" size={size//1024//1024} MB') diff --git a/modules/paths_internal.py b/modules/paths_internal.py index 3a408329d..a9dabdd0f 100644 --- a/modules/paths_internal.py +++ b/modules/paths_internal.py @@ -1,30 +1,3 @@ # no longer used, all paths are defined in paths.py from modules.paths import modules_path, script_path, sd_configs_path, sd_default_config, sd_model_file, default_sd_model_file, data_path, models_path, extensions_dir, extensions_builtin_dir # pylint: disable=unused-import - -""" -import argparse -import os - -modules_path = os.path.dirname(os.path.realpath(__file__)) -script_path = os.path.dirname(modules_path) -sd_configs_path = os.path.join(script_path, "configs") -sd_default_config = os.path.join(sd_configs_path, "v1-inference.yaml") - -# Parse the --data-dir flag first so we can use it as a base for our other argument default values -parser_pre = argparse.ArgumentParser(add_help=False) -parser_pre.add_argument("--ckpt", type=str, default=os.environ.get("SD_MODEL", None), help="Path to model checkpoint to load immediately, default: %(default)s") -parser_pre.add_argument("--data-dir", type=str, default=os.environ.get("SD_DATADIR", ''), help="Base path where all user data is stored, default: %(default)s") -parser_pre.add_argument("--models-dir", type=str, default=os.environ.get("SD_MODELSDIR", 'models'), help="Base path where all models are stored, default: %(default)s",) -cmd_opts_pre = parser_pre.parse_known_args()[0] - -# parser_pre.add_argument("--config", type=str, default=os.environ.get("SD_CONFIG", os.path.join(data_path, 'config.json')), help="Use specific server configuration file, default: %(default)s") - -data_path = cmd_opts_pre.data_dir -models_path = cmd_opts_pre.models_dir if os.path.isabs(cmd_opts_pre.models_dir) else os.path.join(data_path, cmd_opts_pre.models_dir) -extensions_dir = os.path.join(data_path, "extensions") -extensions_builtin_dir = "extensions-builtin" - -sd_model_file = cmd_opts_pre.ckpt or os.path.join(script_path, 'model.ckpt') # not used -default_sd_model_file = sd_model_file # not used -""" diff --git a/modules/shared.py b/modules/shared.py index fbf0871e9..1059cf5bf 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -24,6 +24,11 @@ import modules.paths as paths from installer import log, print_dict, console, get_version # pylint: disable=unused-import +class Backend(Enum): + ORIGINAL = 1 + DIFFUSERS = 2 + + errors.install([gr]) demo: gr.Blocks = None api = None @@ -58,16 +63,11 @@ restricted_opts = { } resize_modes = ["None", "Fixed", "Crop", "Fill", "Outpaint", "Context aware"] max_workers = 12 -default_hfcache_dir = os.environ.get("SD_HFCACHEDIR", None) or os.path.join(os.path.expanduser('~'), '.cache', 'huggingface', 'hub') sdnq_quant_modes = ["int8", "float8_e4m3fn", "int7", "int6", "int5", "uint4", "uint3", "uint2", "float8_e5m2", "float8_e4m3fnuz", "float8_e5m2fnuz", "uint8", "uint7", "uint6", "uint5", "int4", "int3", "int2", "uint1"] +default_hfcache_dir = os.environ.get("SD_HFCACHEDIR", None) or os.path.join(paths.models_path, 'huggingface') state = shared_state.State() -class Backend(Enum): - ORIGINAL = 1 - DIFFUSERS = 2 - - # early select backend backend = Backend.DIFFUSERS if not hasattr(cmd_opts, "use_openvino"): diff --git a/webui.py b/webui.py index 2e9a555a7..cd121fc95 100644 --- a/webui.py +++ b/webui.py @@ -68,7 +68,7 @@ def initialize(): modules.sd_checkpoint.init_metadata() modules.hashes.init_cache() - log.debug(f'Huggingface cache: path="{shared.opts.hfcache_dir}"') + paths.check_cache(shared.opts) modules.sd_samplers.list_samplers() timer.startup.record("samplers") From 0ae24f991669fecdbd05f5f4fc14a15ac33591c7 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 13:12:20 -0400 Subject: [PATCH 113/141] fix ui checkbox/radio styling for non-default themes Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + extensions-builtin/sdnext-modernui | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5bda9a0..34709c38a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -138,6 +138,7 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, - fix `nudenet` api - fix global state tracking - fix ui tab detection for networks + - fix ui checkbox/radio styling for non-default themes - unified stat size/mtime calls - reapply offloading on ipadapter load - api set default script-name diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index ad2d6466d..c302e359a 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit ad2d6466d1a1fe7cbed0a80318b3957b02c77986 +Subproject commit c302e359ac268d04537fc190cccbb834cfe1fe5f From e8002df53467d05d96d3e07270af237244ff12a0 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 13:49:23 -0400 Subject: [PATCH 114/141] generate api endpoints allow set model Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 2 ++ extensions-builtin/sdnext-modernui | 2 +- modules/processing.py | 3 +++ modules/processing_class.py | 12 ++++++++++++ modules/sd_checkpoint.py | 4 ++-- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34709c38a..de4886be9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,8 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, - **API** - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model - add `/sdapi/v1/modules` GET endpoint to get info on model components/modules + - all generate endpoints now support `sd_model_checkpoint` parameter + this allows to specify which model to use for generation without needing to use additional endpoints - **Refactor** - change default huggingface cache folder from system default to `models/huggingface` sd.next will warn on startup on unused cache entries diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index c302e359a..574fcf4e8 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit c302e359ac268d04537fc190cccbb834cfe1fe5f +Subproject commit 574fcf4e8790e6faf3a3a500e4aedf399d0b0e4a diff --git a/modules/processing.py b/modules/processing.py index 675ac9160..2a5087d37 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -119,6 +119,9 @@ def process_images(p: StableDiffusionProcessing) -> Processed: if not hasattr(p.sd_model, 'sd_checkpoint_info'): shared.log.error('Processing: incomplete model') return None + if p.abort: + shared.log.debug('Processing: aborted') + return None if p.scripts is not None and isinstance(p.scripts, scripts_manager.ScriptRunner): p.scripts.before_process(p) stored_opts = {} diff --git a/modules/processing_class.py b/modules/processing_class.py index daff8ccea..8b02e38aa 100644 --- a/modules/processing_class.py +++ b/modules/processing_class.py @@ -15,6 +15,7 @@ debug = shared.log.trace if os.environ.get('SD_PROCESS_DEBUG', None) is not None @dataclass(repr=False) class StableDiffusionProcessing: def __init__(self, + sd_model_checkpoint: str = None, # # used only to set sd_model sd_model=None, # pylint: disable=unused-argument # local instance of sd_model # base params prompt: str = "", @@ -355,6 +356,17 @@ class StableDiffusionProcessing: self.prompt_attention_masks = [] self.negative_prompt_attention_mask = [] self.xyz = xyz + self.abort = False + + # set model + if sd_model_checkpoint is not None and len(sd_model_checkpoint) > 0: + from modules import sd_checkpoint + if sd_checkpoint.select_checkpoint(op='model', sd_model_checkpoint=sd_model_checkpoint) is None: + shared.log.error(f'Processing: model="{sd_model_checkpoint}" not found') + self.abort = True + else: + shared.opts.sd_model_checkpoint = sd_model_checkpoint + sd_models.reload_model_weights() def __str__(self): return f'{self.__class__.__name__}: {self.__dict__}' diff --git a/modules/sd_checkpoint.py b/modules/sd_checkpoint.py index 5dd08cca6..6afc5b953 100644 --- a/modules/sd_checkpoint.py +++ b/modules/sd_checkpoint.py @@ -269,8 +269,8 @@ def model_hash(filename): return 'NOHASH' -def select_checkpoint(op='model'): - model_checkpoint = shared.opts.data.get('sd_model_refiner', None) if op == 'refiner' else shared.opts.data.get('sd_model_checkpoint', None) +def select_checkpoint(op='model', sd_model_checkpoint=None): + model_checkpoint = sd_model_checkpoint or (shared.opts.data.get('sd_model_refiner', None) if op == 'refiner' else shared.opts.data.get('sd_model_checkpoint', None)) if model_checkpoint is None or model_checkpoint == 'None' or len(model_checkpoint) < 3: return None checkpoint_info = get_closet_checkpoint_match(model_checkpoint) From 867c5abc5c8fc0fe8c0c0ddf397c802bae87e894 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 14:35:06 -0400 Subject: [PATCH 115/141] add qwen img2img and inpaint Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 - TODO.md | 1 + installer.py | 2 +- modules/sd_models.py | 3 +++ pipelines/model_qwen.py | 4 ++++ 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de4886be9..eeb37413e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,6 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, new image foundational model with *20B* params DiT and using *Qwen2.5-VL-7B* as the text-encoder! available via *networks -> models -> reference* *note*: this model is almost 2x the size of Flux, quantization and offloading are highly recommended! - *note* qwen-image supports text-to-image workflows as image-editing model is not yet available *recommended* params: *steps=50, attention-guidance=4* also available is pre-packaged [Qwen-Lightning](https://huggingface.co/vladmandic/Qwen-Lightning) which is an unofficial merge of [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) with [Qwen-Lightning-LoRA](https://github.com/ModelTC/Qwen-Image-Lightning/) to improve quality and allow for generating in 8-steps! diff --git a/TODO.md b/TODO.md index bbce04ade..f3371e091 100644 --- a/TODO.md +++ b/TODO.md @@ -8,6 +8,7 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma ## Future Candidates +- Remote TE - Unified `CLIPTextModelWithProjection` loader - [Modular pipelines and guiders](https://github.com/huggingface/diffusers/issues/11915) - Refactor: Sampler options diff --git a/installer.py b/installer.py index 6de6a025e..a35fd490e 100644 --- a/installer.py +++ b/installer.py @@ -593,7 +593,7 @@ def check_diffusers(): t_start = time.time() if args.skip_all or args.skip_git: return - sha = '4a9dbd56f68214f0c949b8036a58c9ac3607f54e' # diffusers commit hash + sha = 'bc2762cce9c42ff7a7c3e4814ae4d5f0385e35e4' # diffusers commit hash pkg = pkg_resources.working_set.by_key.get('diffusers', None) minor = int(pkg.version.split('.')[1] if pkg is not None else -1) cur = opts.get('diffusers_version', '') if minor > -1 else '' diff --git a/modules/sd_models.py b/modules/sd_models.py index 817367b3a..276f84adc 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -863,6 +863,7 @@ def backup_pipe_components(pipe): 'feature_extractor': getattr(pipe, "feature_extractor", None), 'mask_processor': getattr(pipe, "mask_processor", None), 'restore_pipeline': getattr(pipe, "restore_pipeline", None), + 'task_args': getattr(pipe, "task_args", None), } @@ -886,6 +887,8 @@ def restore_pipe_components(pipe, components): pipe.mask_processor = components['mask_processor'] if components['restore_pipeline'] is not None: pipe.restore_pipeline = components['restore_pipeline'] + if components['task_args'] is not None: + pipe.task_args = components['task_args'] if pipe.__class__.__name__ in ['FluxPipeline', 'StableDiffusion3Pipeline']: pipe.register_modules(image_encoder = components['image_encoder']) diff --git a/pipelines/model_qwen.py b/pipelines/model_qwen.py index 61b904171..63ee989e3 100644 --- a/pipelines/model_qwen.py +++ b/pipelines/model_qwen.py @@ -26,6 +26,10 @@ def load_qwen(checkpoint_info, diffusers_load_config={}): 'output_type': 'np', } + diffusers.pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["qwen-image"] = diffusers.QwenImagePipeline + diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["qwen-image"] = diffusers.QwenImageImg2ImgPipeline + diffusers.pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["qwen-image"] = diffusers.QwenImageInpaintPipeline + del text_encoder del transformer From 4d95cf47ceb371bb5820ea002b18144fc0d75fc0 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 14:49:11 -0400 Subject: [PATCH 116/141] restore default sampler on mismatch Signed-off-by: Vladimir Mandic --- modules/paths.py | 2 +- modules/sd_samplers.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/paths.py b/modules/paths.py index 71d2f67a0..f5d0f190c 100644 --- a/modules/paths.py +++ b/modules/paths.py @@ -130,7 +130,7 @@ def check_cache(opts): from modules.modelstats import stat if opts.hfcache_dir != prev_default: size, _mtime = stat(prev_default) - if (size//1024//1024 > 0): + if size//1024//1024 > 0: log.warning(f'Cache location changed: previous="{prev_default}" size={size//1024//1024} MB') size, _mtime = stat(opts.hfcache_dir) log.debug(f'Huggingface cache: path="{opts.hfcache_dir}" size={size//1024//1024} MB') diff --git a/modules/sd_samplers.py b/modules/sd_samplers.py index 0b965eaf6..644e9dd16 100644 --- a/modules/sd_samplers.py +++ b/modules/sd_samplers.py @@ -78,7 +78,7 @@ def create_sampler(name, model): if model is not None: if getattr(model, "default_scheduler", None) is None: model.default_scheduler = copy.deepcopy(model.scheduler) - requires_flow = ('FlowMatch' in model.default_scheduler.__class__.__name__) or (getattr(model.scheduler.config, 'prediction_type', None) == 'flow_prediction') + requires_flow = ('FlowMatch' in model.default_scheduler.__class__.__name__) or (getattr(model.default_scheduler.config, 'prediction_type', None) == 'flow_prediction') else: requires_flow = False @@ -98,7 +98,7 @@ def create_sampler(name, model): # validate sampler prediction type if (model is not None) and (is_flow and not requires_flow): shared.log.error(f'Sampler: "{sampler.name}" cls={sampler.sampler.__class__.__name__} pipe={model.__class__.__name__} model requires sampler with discrete prediction') - # return restore_default(model) + return restore_default(model) if (model is not None) and (not is_flow and requires_flow): shared.log.error(f'Sampler: "{sampler.name}" cls={sampler.sampler.__class__.__name__} pipe={model.__class__.__name__} model requires sampler with flow prediction') return restore_default(model) From 8ca74d0cd2b7037448df9e92011eee702bc4ee93 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Wed, 13 Aug 2025 22:10:30 +0300 Subject: [PATCH 117/141] SDNQ rename unused param_name arg to op --- modules/model_quant.py | 2 +- modules/sdnq/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/model_quant.py b/modules/model_quant.py index 31dcedbf0..686703b9b 100644 --- a/modules/model_quant.py +++ b/modules/model_quant.py @@ -428,9 +428,9 @@ def sdnq_quantize_model(model, op=None, sd_model=None, do_gc: bool = True, weigh non_blocking=shared.opts.diffusers_offload_nonblocking, quantization_device=quantization_device, return_device=return_device, - param_name=op, modules_to_not_convert=modules_to_not_convert, modules_dtype_dict=modules_dtype_dict, + op=op, ) t1 = time.time() timer.load.add('sdnq', t1 - t0) diff --git a/modules/sdnq/__init__.py b/modules/sdnq/__init__.py index ba4f7b9fb..2654a06e7 100644 --- a/modules/sdnq/__init__.py +++ b/modules/sdnq/__init__.py @@ -186,7 +186,7 @@ def sdnq_quantize_layer(layer, weights_dtype="int8", torch_dtype=None, group_siz return layer -def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_size=0, quant_conv=False, use_quantized_matmul=False, use_quantized_matmul_conv=False, dequantize_fp32=False, non_blocking=False, quantization_device=None, return_device=None, param_name=None, modules_to_not_convert: List[str] = [], modules_dtype_dict: Dict[str, List[str]] = {}): # pylint: disable=unused-argument +def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_size=0, quant_conv=False, use_quantized_matmul=False, use_quantized_matmul_conv=False, dequantize_fp32=False, non_blocking=False, quantization_device=None, return_device=None, modules_to_not_convert: List[str] = [], modules_dtype_dict: Dict[str, List[str]] = {}, op=None): # pylint: disable=unused-argument has_children = list(model.children()) if not has_children: return model @@ -238,9 +238,9 @@ def apply_sdnq_to_module(model, weights_dtype="int8", torch_dtype=None, group_si non_blocking=non_blocking, quantization_device=quantization_device, return_device=return_device, - param_name=param_name, modules_to_not_convert=modules_to_not_convert, modules_dtype_dict=modules_dtype_dict, + op=op, ) return model From 41ae06bd90e94d3dfc9e2b24676c179b4279aa6d Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 16:06:20 -0400 Subject: [PATCH 118/141] fix loading custom t5 Signed-off-by: Vladimir Mandic --- modules/model_te.py | 11 +++++++++-- modules/sd_models.py | 2 ++ pipelines/generic.py | 15 +++++++-------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/modules/model_te.py b/modules/model_te.py index b47d73675..82a6a40a0 100644 --- a/modules/model_te.py +++ b/modules/model_te.py @@ -15,10 +15,14 @@ def load_t5(name=None, cache_dir=None): global loaded_te # pylint: disable=global-statement if name is None: return None + cache_dir = cache_dir or shared.opts.hfcache_dir from modules import modelloader modelloader.hf_login() repo_id = 'stabilityai/stable-diffusion-3-medium-diffusers' - fn = te_dict.get(name) if name in te_dict else None + if os.path.exists(name): + fn = name + else: + fn = te_dict.get(name) if name in te_dict else None if fn is not None and name.lower().endswith('gguf'): from modules import ggml @@ -46,12 +50,13 @@ def load_t5(name=None, cache_dir=None): except Exception: shared.log.error(f"T5: Failed to cast text encoder to {devices.dtype}, set dtype to {t5.dtype}") raise + del state_dict elif fn is not None: with open(os.path.join('configs', 'flux', 'text_encoder_2', 'config.json'), encoding='utf8') as f: t5_config = transformers.T5Config(**json.load(f)) state_dict = load_file(fn) - t5 = transformers.T5EncoderModel.from_pretrained(None, state_dict=state_dict, config=t5_config) + t5 = transformers.T5EncoderModel.from_pretrained(None, state_dict=state_dict, config=t5_config, torch_dtype=devices.dtype) elif 'fp16' in name.lower(): t5 = transformers.T5EncoderModel.from_pretrained(repo_id, subfolder='text_encoder_3', cache_dir=cache_dir, torch_dtype=devices.dtype) @@ -141,6 +146,7 @@ def load_vit_l(): te = transformers.CLIPTextModel.from_pretrained(pretrained_model_name_or_path=None, state_dict=state_dict, config=config) te = te.to(dtype=devices.dtype) loaded_te = shared.opts.sd_text_encoder + del state_dict return te @@ -151,6 +157,7 @@ def load_vit_g(): te = transformers.CLIPTextModelWithProjection.from_pretrained(pretrained_model_name_or_path=None, state_dict=state_dict, config=config) te = te.to(dtype=devices.dtype) loaded_te = shared.opts.sd_text_encoder + del state_dict return te diff --git a/modules/sd_models.py b/modules/sd_models.py index 276f84adc..92fd24254 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -1074,6 +1074,8 @@ def reload_text_encoder(initial=False): from modules.model_te import set_t5 shared.log.debug(f'Load module: type=t5 path="{shared.opts.sd_text_encoder}" module="text_encoder_3"') set_t5(pipe=shared.sd_model, module='text_encoder_3', t5=shared.opts.sd_text_encoder, cache_dir=shared.opts.diffusers_dir) + clear_caches() + apply_balanced_offload(shared.sd_model) def reload_model_weights(sd_model=None, info=None, op='model', force=False, revision=None): diff --git a/pipelines/generic.py b/pipelines/generic.py index 696eff3d6..8416950aa 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -90,6 +90,7 @@ def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder # load from local file gguf if local_file is not None and local_file.lower().endswith('.gguf'): shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}"') + """ from modules import ggml ggml.install_gguf() text_encoder = cls_name.from_pretrained( @@ -99,17 +100,15 @@ def load_text_encoder(repo_id, cls_name, load_config={}, subfolder="text_encoder **load_args, ) text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) + """ + text_encoder = model_te.load_t5(local_file) + text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) # load from local file safetensors elif local_file is not None and local_file.lower().endswith('.safetensors'): shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}"') - if dtype is not None: - load_args['torch_dtype'] = dtype - text_encoder = cls_name.from_pretrained( - local_file, - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - ) + from modules import model_te + text_encoder = model_te.load_t5(local_file) + text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) # use shared t5 if possible elif cls_name == transformers.T5EncoderModel and allow_shared: with open(os.path.join('configs', 'flux', 'text_encoder_2', 'config.json'), encoding='utf8') as f: From e30e8cbf4f866867275bfa4645b877e3e81412f0 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 16:27:52 -0400 Subject: [PATCH 119/141] fix loading custom transformer safetensors Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + modules/sd_unet.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeb37413e..1a0c61db1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,7 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, - fix global state tracking - fix ui tab detection for networks - fix ui checkbox/radio styling for non-default themes + - fix loading custom transformers and t5 safetensors tunes - unified stat size/mtime calls - reapply offloading on ipadapter load - api set default script-name diff --git a/modules/sd_unet.py b/modules/sd_unet.py index 13b6302fb..2bd1741f4 100644 --- a/modules/sd_unet.py +++ b/modules/sd_unet.py @@ -37,7 +37,7 @@ def load_unet(model): if prior_text_encoder is not None: model.prior_pipe.text_encoder = None # Prevent OOM model.prior_pipe.text_encoder = prior_text_encoder.to(devices.device, dtype=devices.dtype) - elif any([m in model.__class__.__name__ for m in dit_models]): # noqa: C419 # pylint: disable=use-a-generator + elif any([m in model.__class__.__name__ for m in dit_models]) or hasattr(model, 'transformer'): # noqa: C419 # pylint: disable=use-a-generator loaded_unet = shared.opts.sd_unet sd_models.load_diffuser() # TODO model load: force-reloading entire model as loading transformers only leads to massive memory usage else: From 6123655ac20e094eef1122cbed77caf36bd530c8 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Wed, 13 Aug 2025 23:39:18 +0100 Subject: [PATCH 120/141] LoRA load Torch tuple and string version checking Due to BitsandBytes trying to use tuple and comparison to check Torch version which is given as a string, using LoRA with a quantized model results in a TypeError. This commit adds support for both. --- modules/lora/lora_load.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/modules/lora/lora_load.py b/modules/lora/lora_load.py index 0ad032c15..d88f9b764 100644 --- a/modules/lora/lora_load.py +++ b/modules/lora/lora_load.py @@ -16,6 +16,24 @@ forbidden_network_aliases = {} available_network_hash_lookup = {} dump_lora_keys = os.environ.get('SD_LORA_DUMP', None) is not None +def patch_torch_version(): + import torch + if not hasattr(torch, '__version_backup__'): + torch.__version_backup__ = torch.__version__ + # Convert string version to tuple format to solve TypeError caused by BnB + version_parts = torch.__version__.split('+')[0].split('.') + torch.__version_tuple__ = tuple(int(x) for x in version_parts[:3]) + # Support both string and tuple + class VersionString(str): + def __ge__(self, other): + if isinstance(other, tuple): + self_tuple = tuple(int(x) for x in self.split('+')[0].split('.')[:len(other)]) + return self_tuple >= other + return super().__ge__(other) + torch.__version__ = VersionString(torch.__version__) + +# Call before loading LoRA +patch_torch_version() def load_diffusers(name, network_on_disk, lora_scale=shared.opts.extra_networks_default_multiplier) -> Union[network.Network, None]: t0 = time.time() From afe900537d2b7b19e102a00c9339c85c43ad5791 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Thu, 14 Aug 2025 00:25:56 +0100 Subject: [PATCH 121/141] Revert lora_load.py commit --- modules/lora/lora_load.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/modules/lora/lora_load.py b/modules/lora/lora_load.py index d88f9b764..0ad032c15 100644 --- a/modules/lora/lora_load.py +++ b/modules/lora/lora_load.py @@ -16,24 +16,6 @@ forbidden_network_aliases = {} available_network_hash_lookup = {} dump_lora_keys = os.environ.get('SD_LORA_DUMP', None) is not None -def patch_torch_version(): - import torch - if not hasattr(torch, '__version_backup__'): - torch.__version_backup__ = torch.__version__ - # Convert string version to tuple format to solve TypeError caused by BnB - version_parts = torch.__version__.split('+')[0].split('.') - torch.__version_tuple__ = tuple(int(x) for x in version_parts[:3]) - # Support both string and tuple - class VersionString(str): - def __ge__(self, other): - if isinstance(other, tuple): - self_tuple = tuple(int(x) for x in self.split('+')[0].split('.')[:len(other)]) - return self_tuple >= other - return super().__ge__(other) - torch.__version__ = VersionString(torch.__version__) - -# Call before loading LoRA -patch_torch_version() def load_diffusers(name, network_on_disk, lora_scale=shared.opts.extra_networks_default_multiplier) -> Union[network.Network, None]: t0 = time.time() From 70672d3267e46500fdaa01edebc737a557cba8b0 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Thu, 14 Aug 2025 00:31:10 +0100 Subject: [PATCH 122/141] Move tuple/string versioning fix for BnB to loader.py --- modules/loader.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/modules/loader.py b/modules/loader.py index 87f88872a..8b889f382 100644 --- a/modules/loader.py +++ b/modules/loader.py @@ -199,6 +199,24 @@ def deprecate_warn(*args, **kwargs): diffusers.utils.deprecation_utils.deprecate = deprecate_warn diffusers.utils.deprecate = deprecate_warn +def patch_torch_version(): + import torch + if not hasattr(torch, '__version_backup__'): + torch.__version_backup__ = torch.__version__ + # Convert string version to tuple format to solve TypeError caused by BnB + version_parts = torch.__version__.split('+')[0].split('.') + torch.__version_tuple__ = tuple(int(x) for x in version_parts[:3]) + # Support both string and tuple for version check + class VersionString(str): + def __ge__(self, other): + if isinstance(other, tuple): + self_tuple = tuple(int(x) for x in self.split('+')[0].split('.')[:len(other)]) + return self_tuple >= other + return super().__ge__(other) + torch.__version__ = VersionString(torch.__version__) + +patch_torch_version() + errors.log.info(f'Torch: torch=={torch.__version__} torchvision=={torchvision.__version__}') errors.log.info(f'Packages: diffusers=={diffusers.__version__} transformers=={transformers.__version__} accelerate=={accelerate.__version__} gradio=={gradio.__version__} pydantic=={pydantic.__version__} numpy=={np.__version__}') From 114d097bb45c071828cccdbc7765ee6146335fee Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 20:23:35 -0400 Subject: [PATCH 123/141] add wildcards to networks ui Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 4 +- javascript/extraNetworks.js | 4 +- modules/ui_extra_networks.py | 4 +- modules/ui_extra_networks_checkpoints.py | 8 +-- modules/ui_extra_networks_lora.py | 15 +++--- .../ui_extra_networks_textual_inversion.py | 12 +++-- modules/ui_extra_networks_vae.py | 12 +++-- modules/ui_extra_networks_wildcards.py | 50 +++++++++++++++++++ 8 files changed, 84 insertions(+), 25 deletions(-) create mode 100644 modules/ui_extra_networks_wildcards.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a0c61db1..fc12bf637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) (plus *Lightning* variant) and [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers), [HunyuanDiT](https://huggingface.co/Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers-Distilled) -Plus continuing with major **UI** work, we have new embedded **Docs/Wiki** search, redesigned real-time **hints**, built-in **GPU monitor**, **CivitAI** integration and more! +Plus continuing with major **UI** work, we have new embedded **Docs/Wiki** search, redesigned real-time **hints**, **wildcards** UI selector, built-in **GPU monitor**, **CivitAI** integration and more! On the compute side, new profiles for high-vram GPUs, offloading improvements, support for new `torch` release and improved quality when using low-bit quantization! And (*as always*) many bugfixes and improvements to existing features! @@ -50,6 +50,8 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, **Docs** search: fully-local and works in real-time on all document pages **Wiki** search: uses github api to search online wiki pages - updated real-time hints, thanks @CalamitousFelicitousness + - add **Wilcards** UI + in networks display - every heading element is collapsible! - quicksettings reset button to restore all quicksettings to default values because things do sometimes get wrong... diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index c7cfcdeaf..e13323307 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -226,8 +226,8 @@ function sortExtraNetworks(fixed = 'no') { case 0: return 0; case 1: return a.dataset.name ? a.dataset.name.localeCompare(b.dataset.name) : 0; case 2: return b.dataset.name ? b.dataset.name.localeCompare(a.dataset.name) : 0; - case 3: return a.dataset.mtime && !isNaN(a.dataset.mtime) ? parseFloat(b.dataset.mtime) - parseFloat(a.dataset.mtime) : 0; - case 4: return b.dataset.mtime && !isNaN(b.dataset.mtime) ? parseFloat(a.dataset.mtime) - parseFloat(b.dataset.mtime) : 0; + case 3: return a.dataset.mtime ? (new Date(b.dataset.mtime)).getTime() - (new Date(a.dataset.mtime)).getTime() : 0; + case 4: return b.dataset.mtime ? (new Date(a.dataset.mtime)).getTime() - (new Date(b.dataset.mtime)).getTime() : 0; case 5: return a.dataset.size && !isNaN(a.dataset.size) ? parseFloat(b.dataset.size) - parseFloat(a.dataset.size) : 0; case 6: return b.dataset.size && !isNaN(b.dataset.size) ? parseFloat(a.dataset.size) - parseFloat(b.dataset.size) : 0; } diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 47859a003..ce701602a 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -503,6 +503,8 @@ def register_pages(): register_page(ExtraNetworksPageStyles()) from modules.ui_extra_networks_lora import ExtraNetworksPageLora register_page(ExtraNetworksPageLora()) + from modules.ui_extra_networks_wildcards import ExtraNetworksPageWildcards + register_page(ExtraNetworksPageWildcards()) if shared.opts.latent_history > 0: from modules.ui_extra_networks_history import ExtraNetworksPageHistory register_page(ExtraNetworksPageHistory()) @@ -515,7 +517,7 @@ def get_pages(title=None): visible = shared.opts.extra_networks pages = [] if 'All' in visible or visible == []: # default en sort order - visible = ['Model', 'Lora', 'Style', 'Embedding', 'VAE', 'History', 'Hypernetwork'] + visible = ['Model', 'Lora', 'Style', 'Wildcards', 'Embedding', 'VAE', 'History', 'Hypernetwork'] titles = [page.title for page in shared.extra_networks] if title is None: diff --git a/modules/ui_extra_networks_checkpoints.py b/modules/ui_extra_networks_checkpoints.py index 15efb0e5c..ff050bd1e 100644 --- a/modules/ui_extra_networks_checkpoints.py +++ b/modules/ui_extra_networks_checkpoints.py @@ -2,7 +2,7 @@ import os import html import json import concurrent -from modules import shared, ui_extra_networks, sd_models +from modules import shared, ui_extra_networks, sd_models, modelstats reference_dir = os.path.join('models', 'Reference') @@ -46,7 +46,7 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage): record = None try: checkpoint: sd_models.CheckpointInfo = sd_models.checkpoints_list.get(name) - exists = os.path.exists(checkpoint.filename) + size, mtime = modelstats.stat(checkpoint.filename) record = { "type": 'Model', "name": checkpoint.name, @@ -55,8 +55,8 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage): "hash": checkpoint.shorthash, "metadata": checkpoint.metadata, "onclick": '"' + html.escape(f"selectCheckpoint({json.dumps(name)})") + '"', - "mtime": os.path.getmtime(checkpoint.filename) if exists else 0, - "size": os.path.getsize(checkpoint.filename) if exists else 0, + "mtime": mtime, + "size": size, } record["info"] = self.find_info(checkpoint.filename) record["description"] = self.find_description(checkpoint.filename, record["info"]) diff --git a/modules/ui_extra_networks_lora.py b/modules/ui_extra_networks_lora.py index 597f32941..8ab95ce8d 100644 --- a/modules/ui_extra_networks_lora.py +++ b/modules/ui_extra_networks_lora.py @@ -1,7 +1,7 @@ import os import json import concurrent -from modules import shared, ui_extra_networks +from modules import shared, ui_extra_networks, modelstats from modules.lora import lora_load @@ -85,6 +85,8 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): try: # path, _ext = os.path.splitext(l.filename) name = os.path.splitext(os.path.relpath(l.filename, shared.cmd_opts.lora_dir))[0] + size, mtime = modelstats.stat(l.filename) + info = self.find_info(l.filename) item = { "type": 'Lora', "name": name, @@ -93,14 +95,13 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): "hash": l.shorthash, "prompt": json.dumps(f" "), "metadata": json.dumps(l.metadata, indent=4) if l.metadata else None, - "mtime": os.path.getmtime(l.filename), - "size": os.path.getsize(l.filename), + "mtime": mtime, + "size": size, "version": l.sd_version, + "info": info, + "description": self.find_description(l.filename, info), + "tags": self.get_tags(l, info), } - info = self.find_info(l.filename) - item["info"] = info - item["description"] = self.find_description(l.filename, info) # use existing info instead of double-read - item["tags"] = self.get_tags(l, info) return item except Exception as e: shared.log.error(f'Networks: type=lora file="{name}" {e}') diff --git a/modules/ui_extra_networks_textual_inversion.py b/modules/ui_extra_networks_textual_inversion.py index 92c8acc92..6857447f2 100644 --- a/modules/ui_extra_networks_textual_inversion.py +++ b/modules/ui_extra_networks_textual_inversion.py @@ -1,6 +1,6 @@ import json import os -from modules import shared, sd_models, ui_extra_networks, files_cache +from modules import shared, sd_models, ui_extra_networks, files_cache, modelstats from modules.textual_inversion import Embedding @@ -23,6 +23,8 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage): if embedding.tag is not None: tags[embedding.tag]=1 name = os.path.splitext(embedding.basename)[0] + size, mtime = modelstats.stat(embedding.filename) + info = self.find_info(embedding.filename) record = { "type": 'Embedding', "name": name, @@ -30,11 +32,11 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage): "alias": os.path.splitext(os.path.basename(embedding.filename))[0], "prompt": json.dumps(f" {os.path.splitext(embedding.name)[0]}"), "tags": tags, - "mtime": os.path.getmtime(embedding.filename), - "size": os.path.getsize(embedding.filename), + "mtime": mtime, + "size": size, + "info": info, + "description": self.find_description(embedding.filename, info), } - record["info"] = self.find_info(embedding.filename) - record["description"] = self.find_description(embedding.filename, record["info"]) except Exception as e: shared.log.debug(f'Networks error: type=embedding file="{embedding.filename}" {e}') return record diff --git a/modules/ui_extra_networks_vae.py b/modules/ui_extra_networks_vae.py index ed9ddadc3..0db733a3c 100644 --- a/modules/ui_extra_networks_vae.py +++ b/modules/ui_extra_networks_vae.py @@ -1,7 +1,7 @@ import html import json import os -from modules import shared, ui_extra_networks, sd_vae, hashes +from modules import shared, ui_extra_networks, sd_vae, hashes, modelstats class ExtraNetworksPageVAEs(ui_extra_networks.ExtraNetworksPage): @@ -14,6 +14,8 @@ class ExtraNetworksPageVAEs(ui_extra_networks.ExtraNetworksPage): def list_items(self): for name, filename in sd_vae.vae_dict.items(): try: + size, mtime = modelstats.stat(filename) + info = self.find_info(filename) record = { "type": 'VAE', "name": name, @@ -25,11 +27,11 @@ class ExtraNetworksPageVAEs(ui_extra_networks.ExtraNetworksPage): "local_preview": f"{os.path.splitext(filename)[0]}.{shared.opts.samples_format}", "metadata": {}, "onclick": '"' + html.escape(f"""return selectVAE({json.dumps(name)})""") + '"', - "mtime": os.path.getmtime(filename), - "size": os.path.getsize(filename), + "mtime": mtime, + "size": size, + "info": info, + "description": self.find_description(filename, info), } - record["info"] = self.find_info(filename) - record["description"] = self.find_description(filename, record["info"]) yield record except Exception as e: shared.log.debug(f'Networks error: type=vae file="{filename}" {e}') diff --git a/modules/ui_extra_networks_wildcards.py b/modules/ui_extra_networks_wildcards.py new file mode 100644 index 000000000..e60af679e --- /dev/null +++ b/modules/ui_extra_networks_wildcards.py @@ -0,0 +1,50 @@ +import os +import json +from modules import shared, ui_extra_networks, modelstats, files_cache + + +wildcards_list = [] + + +class ExtraNetworksPageWildcards(ui_extra_networks.ExtraNetworksPage): + def __init__(self): + super().__init__('Wildcards') + + def parents(self, file): + folder = os.path.dirname(file) + if folder != shared.opts.wildcards_dir and folder not in wildcards_list: + wildcards_list.append(folder) + self.parents(folder) + + def refresh(self): + wildcards_list.clear() + files = files_cache.list_files(shared.opts.wildcards_dir, ext_filter=[".txt"], recursive=True) + for file in files: + wildcards_list.append(file) + self.parents(file) + + def list_items(self): + self.refresh() + for filename in wildcards_list: + relname = os.path.relpath(filename, shared.opts.wildcards_dir) + name = os.path.splitext(relname)[0] + size, mtime = modelstats.stat(filename) + try: + record = { + "type": 'Wildcard', + "name": name, + "filename": filename, + "preview": self.find_preview(filename), + "local_preview": f"{os.path.splitext(filename)[0]}.{shared.opts.samples_format}", + "prompt": json.dumps(f" __{name}__"), + "mtime": mtime, + "size": size, + "description": '', + "info": {}, + } + yield record + except Exception as e: + shared.log.debug(f'Networks error: type=wildcard file="{filename}" {e}') + + def allowed_directories_for_previews(self): + return [v for v in [shared.opts.wildcards_dir] if v is not None] From ff2c03538daa9726fa6100d533d2ef557491cec8 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 20:53:37 -0400 Subject: [PATCH 124/141] add mtime to reference models Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 6 +++++- installer.py | 3 ++- modules/models_hf.py | 1 + modules/modelstats.py | 2 +- modules/shared.py | 1 + modules/ui_extra_networks.py | 1 + modules/ui_extra_networks_checkpoints.py | 6 ++++-- modules/ui_extra_networks_styles.py | 3 ++- 8 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc12bf637..c10add9ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) (plus *Lightning* variant) and [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers), [HunyuanDiT](https://huggingface.co/Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers-Distilled) Plus continuing with major **UI** work, we have new embedded **Docs/Wiki** search, redesigned real-time **hints**, **wildcards** UI selector, built-in **GPU monitor**, **CivitAI** integration and more! -On the compute side, new profiles for high-vram GPUs, offloading improvements, support for new `torch` release and improved quality when using low-bit quantization! +On the compute side, new profiles for high-vram GPUs, offloading improvements, parallel-load for large models, support for new `torch` release and improved quality when using low-bit quantization! And (*as always*) many bugfixes and improvements to existing features! *Note*: Change-in-behavior - locations of downloaded HuggingFace models and components are changed to allow for de-duplication of common modules and switched from using system default cache folder to `models/huggingface` @@ -109,6 +109,8 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, add `allura-org/Gemma-3-Glitter-4B`, `Qwen/Qwen3-4B-Instruct-2507`, `Qwen/Qwen2.5-VL-3B-Instruct` model support improve system prompt - **schedulers** add **Flash FlowMatch** + - **model loader** add parallel loader option + enabled by default, selectable in *settings -> model loading* - **API** - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model - add `/sdapi/v1/modules` GET endpoint to get info on model components/modules @@ -143,6 +145,8 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, - fix ui tab detection for networks - fix ui checkbox/radio styling for non-default themes - fix loading custom transformers and t5 safetensors tunes + - add mtime to reference models + - patch torch version so 3rd party libraries can use expected format - unified stat size/mtime calls - reapply offloading on ipadapter load - api set default script-name diff --git a/installer.py b/installer.py index a35fd490e..23d31d074 100644 --- a/installer.py +++ b/installer.py @@ -1310,6 +1310,7 @@ def install_requirements(): # set environment variables controling the behavior of various libraries def set_environment(): + from modules.paths import models_path log.debug('Setting environment tuning') os.environ.setdefault('ACCELERATE', 'True') os.environ.setdefault('ATTN_PRECISION', 'fp16') @@ -1336,7 +1337,7 @@ def set_environment(): os.environ.setdefault('DO_NOT_TRACK', '1') os.environ.setdefault('UV_INDEX_STRATEGY', 'unsafe-any-match') os.environ.setdefault('UV_NO_BUILD_ISOLATION', '1') - os.environ.setdefault('HF_HUB_CACHE', opts.get('hfcache_dir', os.path.join(os.path.expanduser('~'), '.cache', 'huggingface', 'hub'))) + os.environ.setdefault('HF_HUB_CACHE', opts.get('hfcache_dir', os.path.join(models_path, 'huggingface'))) allocator = f'garbage_collection_threshold:{opts.get("torch_gc_threshold", 80)/100:0.2f},max_split_size_mb:512' if opts.get("torch_malloc", "native") == 'cudaMallocAsync': allocator += ',backend:cudaMallocAsync' diff --git a/modules/models_hf.py b/modules/models_hf.py index 801fafc04..c767bcde0 100644 --- a/modules/models_hf.py +++ b/modules/models_hf.py @@ -8,6 +8,7 @@ def hf_init(): os.environ.setdefault('HF_HUB_DISABLE_SYMLINKS_WARNING', '1') os.environ.setdefault('HF_HUB_DISABLE_IMPLICIT_TOKEN', '1') os.environ.setdefault('HUGGINGFACE_HUB_VERBOSITY', 'warning') + os.environ.setdefault('HF_ENABLE_PARALLEL_LOADING', 'true') def hf_search(keyword): diff --git a/modules/modelstats.py b/modules/modelstats.py index a91104fa5..3657e523a 100644 --- a/modules/modelstats.py +++ b/modules/modelstats.py @@ -14,7 +14,7 @@ def walk(folder: str): def stat(fn: str): if fn is None or len(fn) == 0 or not os.path.exists(fn): - return 0, None + return 0, datetime.fromtimestamp(0) fs_stat = os.stat(fn, follow_symlinks=False) mtime = datetime.fromtimestamp(fs_stat.st_mtime).replace(microsecond=0) if os.path.islink(fn): diff --git a/modules/shared.py b/modules/shared.py index 1059cf5bf..7d5ebee44 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -142,6 +142,7 @@ options_templates.update(options_section(('sd', "Model Loading"), { "advanced_sep": OptionInfo("

Advanced Options

", "", gr.HTML), "sd_checkpoint_autoload": OptionInfo(True, "Model auto-load on start"), + "sd_parallel_load": OptionInfo(True, "Model auto-load on start"), "sd_checkpoint_autodownload": OptionInfo(True, "Model auto-download on demand"), "stream_load": OptionInfo(False, "Model load using streams", gr.Checkbox), "diffusers_to_gpu": OptionInfo(False, "Model load model direct to GPU"), diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index ce701602a..30b90c726 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -283,6 +283,7 @@ class ExtraNetworksPage: htmls = [] if len(self.items) > 0 and self.items[0].get('mtime', None) is not None: + shared.opts.extra_networks_sort = 'Date [Newest]' if shared.opts.extra_networks_sort == 'Default': pass elif shared.opts.extra_networks_sort == 'Name [A-Z]': diff --git a/modules/ui_extra_networks_checkpoints.py b/modules/ui_extra_networks_checkpoints.py index ff050bd1e..520492761 100644 --- a/modules/ui_extra_networks_checkpoints.py +++ b/modules/ui_extra_networks_checkpoints.py @@ -26,16 +26,18 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage): else: continue preview = v.get('preview', v['path']) + preview_file = self.find_preview_file(os.path.join(reference_dir, preview)) + _size, mtime = modelstats.stat(preview_file) yield { "type": 'Model', "name": os.path.join(reference_dir, k), "title": os.path.join(reference_dir, k), "filename": url, "preview": self.find_preview(os.path.join(reference_dir, preview)), - "local_preview": self.find_preview_file(os.path.join(reference_dir, preview)), + "local_preview": preview_file, "onclick": '"' + html.escape(f"selectReference({json.dumps(url)})") + '"', "hash": None, - "mtime": 0, + "mtime": mtime, "size": 0, "info": {}, "metadata": {}, diff --git a/modules/ui_extra_networks_styles.py b/modules/ui_extra_networks_styles.py index 3cd17b07a..84d9f7b95 100644 --- a/modules/ui_extra_networks_styles.py +++ b/modules/ui_extra_networks_styles.py @@ -1,6 +1,7 @@ import os import html import json +from datetime import datetime from modules import shared, extra_networks, ui_extra_networks, styles @@ -90,7 +91,7 @@ class ExtraNetworksPageStyles(ui_extra_networks.ExtraNetworksPage): "wildcards": getattr(style, 'wildcards', ''), "local_preview": f"{fn}.{shared.opts.samples_format}", "onclick": '"' + html.escape(f"""return selectStyle({json.dumps(name)})""") + '"', - "mtime": getattr(style, 'mtime', 0), + "mtime": getattr(style, 'mtime', datetime.fromtimestamp(0)), "size": os.path.getsize(style.filename), } except Exception as e: From 706bc9e2e7410b03bf283dd65a1615a75031c2db Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 20:55:26 -0400 Subject: [PATCH 125/141] cleanup Signed-off-by: Vladimir Mandic --- modules/ui_extra_networks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 30b90c726..ce701602a 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -283,7 +283,6 @@ class ExtraNetworksPage: htmls = [] if len(self.items) > 0 and self.items[0].get('mtime', None) is not None: - shared.opts.extra_networks_sort = 'Date [Newest]' if shared.opts.extra_networks_sort == 'Default': pass elif shared.opts.extra_networks_sort == 'Name [A-Z]': From 068f63badc6ed8a83b46b89184368d0d5c26eeda Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 13 Aug 2025 21:18:57 -0400 Subject: [PATCH 126/141] add model parallel load option Signed-off-by: Vladimir Mandic --- modules/models_hf.py | 2 +- modules/sd_models.py | 16 +++++++++++----- modules/shared.py | 2 +- webui.py | 3 +++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/modules/models_hf.py b/modules/models_hf.py index c767bcde0..23e979821 100644 --- a/modules/models_hf.py +++ b/modules/models_hf.py @@ -8,7 +8,7 @@ def hf_init(): os.environ.setdefault('HF_HUB_DISABLE_SYMLINKS_WARNING', '1') os.environ.setdefault('HF_HUB_DISABLE_IMPLICIT_TOKEN', '1') os.environ.setdefault('HUGGINGFACE_HUB_VERBOSITY', 'warning') - os.environ.setdefault('HF_ENABLE_PARALLEL_LOADING', 'true') + os.environ.setdefault('HF_ENABLE_PARALLEL_LOADING', 'true' if opts.sd_parallel_load else 'false') def hf_search(keyword): diff --git a/modules/sd_models.py b/modules/sd_models.py index 92fd24254..0b4cec80b 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -73,6 +73,16 @@ def copy_diffuser_options(new_pipe, orig_pipe): set_accelerate(new_pipe) +def set_huggingface_options(op: str, model_type: str): + if shared.opts.diffusers_to_gpu: # and model_type.startswith('Stable Diffusion'): + shared.log.debug(f'Setting {op}: component=accelerate direct={shared.opts.diffusers_to_gpu}') + sd_hijack_accelerate.hijack_accelerate() + else: + sd_hijack_accelerate.restore_accelerate() + if shared.opts.sd_parallel_load: + shared.log.debug(f'Setting {op}: component=huggingface parallel={shared.opts.sd_parallel_load}') + + def set_vae_options(sd_model, vae=None, op:str='model', quiet:bool=False): ops = {} if hasattr(sd_model, "vae"): @@ -498,11 +508,6 @@ def load_diffuser_file(model_type, pipeline, checkpoint_info, diffusers_load_con if shared.opts.disable_accelerate: from diffusers.utils import import_utils import_utils._accelerate_available = False # pylint: disable=protected-access - if shared.opts.diffusers_to_gpu and model_type.startswith('Stable Diffusion'): - shared.log.debug(f'Setting {op}: component=accelerate direct={shared.opts.diffusers_to_gpu}') - sd_hijack_accelerate.hijack_accelerate() - else: - sd_hijack_accelerate.restore_accelerate() sd_model = pipeline.from_single_file(checkpoint_info.path, **diffusers_load_config) # sd_model = patch_diffuser_config(sd_model, checkpoint_info.path) elif hasattr(pipeline, 'from_ckpt'): @@ -607,6 +612,7 @@ def load_diffuser(checkpoint_info=None, op='model', revision=None): # pylint: di # detect pipeline pipeline, model_type = sd_detect.detect_pipeline(checkpoint_info.path, op) + set_huggingface_options(op, model_type) # preload vae so it can be used as param vae = None diff --git a/modules/shared.py b/modules/shared.py index 7d5ebee44..2a366248d 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -142,7 +142,7 @@ options_templates.update(options_section(('sd', "Model Loading"), { "advanced_sep": OptionInfo("

Advanced Options

", "", gr.HTML), "sd_checkpoint_autoload": OptionInfo(True, "Model auto-load on start"), - "sd_parallel_load": OptionInfo(True, "Model auto-load on start"), + "sd_parallel_load": OptionInfo(True, "Model load using multiple threads"), "sd_checkpoint_autodownload": OptionInfo(True, "Model auto-download on demand"), "stream_load": OptionInfo(False, "Model load using streams", gr.Checkbox), "diffusers_to_gpu": OptionInfo(False, "Model load model direct to GPU"), diff --git a/webui.py b/webui.py index cd121fc95..ab60b049b 100644 --- a/webui.py +++ b/webui.py @@ -121,6 +121,9 @@ def initialize(): modules.extra_networks.register_default_extra_networks() timer.startup.record("networks") + from modules.models_hf import hf_init + hf_init() + if shared.cmd_opts.tls_keyfile is not None and shared.cmd_opts.tls_certfile is not None: try: if not os.path.exists(shared.cmd_opts.tls_keyfile): From e4cc2b1ee056ebfceee47c27d58347f509aa6510 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Aug 2025 09:42:58 -0400 Subject: [PATCH 127/141] fix gallery mtime Signed-off-by: Vladimir Mandic --- modules/api/gallery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/api/gallery.py b/modules/api/gallery.py index feed71bf5..8b74e2c04 100644 --- a/modules/api/gallery.py +++ b/modules/api/gallery.py @@ -92,7 +92,7 @@ def register_api(app: FastAPI): # register api 'width': width, 'height': height, 'size': stat_size, - 'mtime': stat_mtime, + 'mtime': stat_mtime.timestamp(), } return content except Exception as e: @@ -119,7 +119,7 @@ def register_api(app: FastAPI): # register api 'width': width, 'height': height, 'size': stat_size, - 'mtime': stat_mtime, + 'mtime': stat_mtime.timestamp(), } return content except Exception as e: From 1a60116dcedfc5bd0683178d89186bfbf3868ed4 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Aug 2025 09:45:49 -0400 Subject: [PATCH 128/141] fix zavychromaxl Signed-off-by: Vladimir Mandic --- modules/sd_detect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sd_detect.py b/modules/sd_detect.py index 0ceee93da..52d32f113 100644 --- a/modules/sd_detect.py +++ b/modules/sd_detect.py @@ -84,7 +84,7 @@ def guess_by_name(fn, current_guess): return 'Stable Diffusion 3' elif 'hidream' in fn.lower(): return 'HiDream' - elif 'chroma' in fn.lower(): + elif 'chroma' in fn.lower() and 'xl' not in fn.lower(): return 'Chroma' elif 'flux' in fn.lower() or 'flex.1' in fn.lower(): size = round(os.path.getsize(fn) / 1024 / 1024) if os.path.isfile(fn) else 0 From e3bc172e0a0feec2be02ea64628cc4fa6f15dc5f Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Aug 2025 09:51:36 -0400 Subject: [PATCH 129/141] update requirements Signed-off-by: Vladimir Mandic --- installer.py | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/installer.py b/installer.py index 23d31d074..5e796f119 100644 --- a/installer.py +++ b/installer.py @@ -593,7 +593,7 @@ def check_diffusers(): t_start = time.time() if args.skip_all or args.skip_git: return - sha = 'bc2762cce9c42ff7a7c3e4814ae4d5f0385e35e4' # diffusers commit hash + sha = '58bf2682612bc29b7cdb8a10ba6eee28a024d6d3' # diffusers commit hash pkg = pkg_resources.working_set.by_key.get('diffusers', None) minor = int(pkg.version.split('.')[1] if pkg is not None else -1) cur = opts.get('diffusers_version', '') if minor > -1 else '' @@ -618,7 +618,7 @@ def check_transformers(): if args.use_directml: target = '4.52.4' else: - target = '4.55.0' + target = '4.55.2' if (pkg is None) or ((pkg.version != target) and (not args.experimental)): if pkg is None: log.info(f'Transformers install: version={target}') diff --git a/requirements.txt b/requirements.txt index b20e8b689..f304e5c15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -50,7 +50,7 @@ numpy==2.1.2 pandas==2.3.0 numba==0.61.2 protobuf==4.25.3 -pytorch_lightning==2.5.2 +pytorch_lightning==2.5.3 tokenizers==0.21.4 urllib3==1.26.19 Pillow==10.4.0 From 4ef1e566227722f264fcaed5ffa7078d86d1c338 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Aug 2025 10:14:16 -0400 Subject: [PATCH 130/141] simplify namegen seq Signed-off-by: Vladimir Mandic --- modules/images.py | 2 +- modules/images_namegen.py | 43 +++++++++++++++------------------------ 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/modules/images.py b/modules/images.py index 9c3992d23..1b8eb6cce 100644 --- a/modules/images.py +++ b/modules/images.py @@ -197,7 +197,7 @@ def save_image(image, dirname = os.path.dirname(params.filename) if dirname is not None and len(dirname) > 0: os.makedirs(dirname, exist_ok=True) - params.filename = namegen.sequence(params.filename, dirname, basename) + params.filename = namegen.sequence(params.filename) params.filename = namegen.sanitize(params.filename) # callbacks script_callbacks.before_image_saved_callback(params) diff --git a/modules/images_namegen.py b/modules/images_namegen.py index 673ab9cac..c8971eff2 100644 --- a/modules/images_namegen.py +++ b/modules/images_namegen.py @@ -15,6 +15,7 @@ re_pattern_arg = re.compile(r"(.*)<([^>]*)>$") re_attention = re.compile(r'[\(*\[*](\w+)(:\d+(\.\d+))?[\)*\]*]|') re_network = re.compile(r'\<\w+:(\w+)(:\d+(\.\d+))?\>|') re_brackets = re.compile(r'[\([{})\]]') +seq = 0 NOTHING = object() @@ -182,18 +183,20 @@ class FilenameGenerator: debug(f'Filename sanitize: input="{filename}" parts={parts} output="{fn}" ext={ext} max={max_length} len={len(fn)}') return fn - def sequence(self, fn, dirname, basename): + def sequence(self, fn): + global seq # pylint: disable=global-statement x = fn + dirname = os.path.dirname(fn) + if seq == 0: + seq = len(os.listdir(dirname)) if os.path.exists(dirname) and os.path.isdir(dirname) else 0 if shared.opts.save_images_add_number or '[seq]' in fn: if '[seq]' not in fn: fn = os.path.join(os.path.dirname(fn), f"[seq]-{os.path.basename(fn)}") - basecount = get_next_sequence_number(dirname, basename) - for i in range(9999): - seq = f"{basecount + i:05}" - filename = fn.replace('[seq]', seq) - if not os.path.exists(filename): - debug(f'Prompt sequence: input="{fn}" seq={seq} output="{filename}"') - x = filename + for _i in range(99999): # 99999/000001 + seq += 1 + dst = fn.replace('[seq]', f'{seq:05}') + if not os.path.exists(dst): + x = dst break return x @@ -220,7 +223,7 @@ class FilenameGenerator: replacement = fun(self, *pattern_args) except Exception as e: replacement = None - errors.display(e, 'Filename apply pattern') + errors.display(e, 'namegen') shared.log.error(f'Filename apply pattern: {x} {e}') if replacement == NOTHING: continue @@ -232,21 +235,7 @@ class FilenameGenerator: return res -def get_next_sequence_number(path, basename): - """ - Determines and returns the next sequence number to use when saving an image in the specified directory. - """ - result = -1 - if basename != '': - basename = f"{basename}-" - prefix_length = len(basename) - if not os.path.isdir(path): - return 0 - for p in os.listdir(path): - if p.startswith(basename): - parts = os.path.splitext(p[prefix_length:])[0].split('-') # splits the filename (removing the basename first if one is defined, so the sequence number is always the first element) - try: - result = max(int(parts[0]), result) - except ValueError: - pass - return result + 1 +def get_next_sequence_number(path, basename): # pylint: disable=unused-argument + global seq # pylint: disable=global-statement + seq += 1 + return seq # unused From b74e851fae8b8546a0a4c47f0d5fbad7d0c8af87 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Aug 2025 10:15:26 -0400 Subject: [PATCH 131/141] update changelog Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c10add9ba..1e5b463ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Change Log for SD.Next -## Update for 2025-08-13 +## Update for 2025-08-14 -### Highlights for 2025-08-13 +### Highlights for 2025-08-14 Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) (plus *Lightning* variant) and [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers), [HunyuanDiT](https://huggingface.co/Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers-Distilled) @@ -15,7 +15,7 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) -### Details for 2025-08-13 +### Details for 2025-08-14 - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) @@ -110,7 +110,9 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, improve system prompt - **schedulers** add **Flash FlowMatch** - **model loader** add parallel loader option - enabled by default, selectable in *settings -> model loading* + enabled by default, selectable in *settings -> model loading* + - filename namegen use exact sequence number instead of next available + this allows for more predictable and consistent filename generation - **API** - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model - add `/sdapi/v1/modules` GET endpoint to get info on model components/modules From ad5881b9ce12e88b9051b2144f3bdf94ee5c47a0 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Aug 2025 10:32:00 -0400 Subject: [PATCH 132/141] add network delete Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 6 +++++- modules/ui_extra_networks.py | 32 ++++++++++++++------------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e5b463ab..22daa9740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,8 +111,12 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, - **schedulers** add **Flash FlowMatch** - **model loader** add parallel loader option enabled by default, selectable in *settings -> model loading* - - filename namegen use exact sequence number instead of next available + - **filename namegen** use exact sequence number instead of next available this allows for more predictable and consistent filename generation + - **network delete** new feature that allows to delete network from disk + in *networks -> show details -> delete* + this will also delete description, metadata and previews associated with the network + only applicable to safetensors networks, not downloaded diffuser models - **API** - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model - add `/sdapi/v1/modules` GET endpoint to get info on model components/modules diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index ce701602a..06b3428fb 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -737,15 +737,21 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): shared.log.debug(f'Network save desc: item="{ui.last_item.name}" filename="{fn}"') return desc - def fn_delete_desc(desc): + def fn_delete_network(desc): if ui.last_item is None: return desc - fn = os.path.splitext(ui.last_item.filename)[0] + '.txt' - if os.path.exists(fn): - shared.log.debug(f'Network delete desc: item="{ui.last_item.name}" filename="{fn}"') + basename = os.path.splitext(ui.last_item.filename)[0] + extensions = ['.safetensors', '.ckpt', '.txt', '.json', '.thumb.jpg', '.jpg', '.jpeg', '.png', '.webp', '.tiff', '.jp2', '.jxl'] + candidates = [] + for ext in extensions: + fn = basename + ext + if os.path.exists(fn) and os.path.isfile(fn): + candidates.append(fn) + msg = f'Network delete: item="{ui.last_item.name}" files={candidates}' + shared.log.debug(msg) + for fn in candidates: os.remove(fn) - return '' - return desc + return msg def fn_save_info(info): fn = os.path.splitext(ui.last_item.filename)[0] + '.json' @@ -753,16 +759,6 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): shared.log.debug(f'Network save info: item="{ui.last_item.name}" filename="{fn}"') return info - def fn_delete_info(info): - if ui.last_item is None: - return info - fn = os.path.splitext(ui.last_item.filename)[0] + '.json' - if os.path.exists(fn): - shared.log.debug(f'Network delete info: item="{ui.last_item.name}" filename="{fn}"') - os.remove(fn) - return '' - return info - def fn_save_style(info, description, prompt, negative, extra, wildcards): if not isinstance(info, dict) or isinstance(info, list): shared.log.warning(f'Network save style skip: item="{ui.last_item.name}" not a dict: {type(info)}') @@ -789,9 +785,9 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): btn_save_img.click(fn=fn_save_img, _js='closeDetailsEN', inputs=[img], outputs=[img]) btn_delete_img.click(fn=fn_delete_img, _js='closeDetailsEN', inputs=[img], outputs=[img]) btn_save_desc.click(fn=fn_save_desc, _js='closeDetailsEN', inputs=[desc], outputs=[desc]) - btn_delete_desc.click(fn=fn_delete_desc, _js='closeDetailsEN', inputs=[desc], outputs=[desc]) + btn_delete_desc.click(fn=fn_delete_network, _js='closeDetailsEN', inputs=[desc], outputs=[desc]) btn_save_info.click(fn=fn_save_info, _js='closeDetailsEN', inputs=[info], outputs=[info]) - btn_delete_info.click(fn=fn_delete_info, _js='closeDetailsEN', inputs=[info], outputs=[info]) + btn_delete_info.click(fn=fn_delete_network, _js='closeDetailsEN', inputs=[info], outputs=[desc]) btn_save_style.click(fn=fn_save_style, _js='closeDetailsEN', inputs=[info, description, prompt, negative, extra, wildcards], outputs=[info]) btn_delete_style.click(fn=fn_delete_style, _js='closeDetailsEN', inputs=[info], outputs=[info]) From 1a59205cc1327e294ae71ed8fa147a8b72571aa9 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Aug 2025 11:23:25 -0400 Subject: [PATCH 133/141] add qwen-image taesd preview Signed-off-by: Vladimir Mandic --- modules/sd_vae_taesd.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/modules/sd_vae_taesd.py b/modules/sd_vae_taesd.py index a89dff777..aed995f1b 100644 --- a/modules/sd_vae_taesd.py +++ b/modules/sd_vae_taesd.py @@ -8,7 +8,7 @@ import os import threading from PIL import Image import torch -from modules import devices, paths +from modules import devices, paths, shared TAESD_MODELS = { @@ -36,11 +36,10 @@ prev_cls = '' prev_type = '' prev_model = '' lock = threading.Lock() -supported = ['sd', 'sdxl', 'sd3', 'f1', 'h1', 'lumina2', 'hunyuanvideo', 'wanai', 'mochivideo', 'pixartsigma', 'pixartalpha', 'hunyuandit', 'omnigen'] +supported = ['sd', 'sdxl', 'sd3', 'f1', 'h1', 'lumina2', 'hunyuanvideo', 'wanai', 'mochivideo', 'pixartsigma', 'pixartalpha', 'hunyuandit', 'omnigen', 'qwen'] def warn_once(msg, variant=None): - from modules import shared variant = variant or shared.opts.taesd_variant global prev_warnings # pylint: disable=global-statement if not prev_warnings: @@ -51,7 +50,6 @@ def warn_once(msg, variant=None): def get_model(model_type = 'decoder', variant = None): global prev_cls, prev_type, prev_model # pylint: disable=global-statement - from modules import shared model_cls = shared.sd_model_type if model_cls is None or model_cls == 'none': return None, variant @@ -61,7 +59,7 @@ def get_model(model_type = 'decoder', variant = None): model_cls = 'sdxl' elif model_cls in {'h1', 'lumina2', 'chroma'}: model_cls = 'f1' - elif model_cls in {'wanai'}: + elif model_cls in {'wanai', 'qwen'}: variant = variant or 'TAE WanVideo' elif model_cls not in supported: warn_once(f'cls={shared.sd_model.__class__.__name__} type={model_cls} unsuppported', variant=variant) From 6e337236394b10902994a94e9b052ce40b77aab5 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Aug 2025 13:40:32 -0400 Subject: [PATCH 134/141] update models page Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 6 +++++- wiki | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22daa9740..11d2882bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,7 +81,7 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, - *hint*: card layout card layout is used by networks, gallery, civitai search, etc. you can change card size in *settings -> user interface* -- **Offloading** +- **Offloading** - changed **default** values for offloading based on detected gpu memory see [offloading docs](https://vladmandic.github.io/sdnext-docs/Offload/) for details - new feature to specify which modules to offload always or never @@ -117,6 +117,10 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, in *networks -> show details -> delete* this will also delete description, metadata and previews associated with the network only applicable to safetensors networks, not downloaded diffuser models +- **Wiki** + - Models page updated with links to original model repos and model licenses, thanks @alerikaisattera + - Updated Model-Support with newly supported models + - Updated Offload, Prompting, API pages - **API** - add `/sdapi/v1/checkpoint` POST endpoint to simply load a model - add `/sdapi/v1/modules` GET endpoint to get info on model components/modules diff --git a/wiki b/wiki index 4cc3fb9e2..fe2558394 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 4cc3fb9e2eacac68da1b1af22a025032ef9df7d6 +Subproject commit fe255839498563e30bf1a1db9b5f43a36e1196c6 From 2318f97991e01a7871ca24fb0e8490e5fca51728 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Aug 2025 15:35:50 -0400 Subject: [PATCH 135/141] manual set dtype via api Signed-off-by: Vladimir Mandic --- modules/api/endpoints.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/api/endpoints.py b/modules/api/endpoints.py index b6f6c7eeb..44120e7b5 100644 --- a/modules/api/endpoints.py +++ b/modules/api/endpoints.py @@ -140,10 +140,13 @@ def get_checkpoint(): checkpoint['hash'] = shared.sd_model.sd_checkpoint_info.shorthash return checkpoint -def set_checkpoint(sd_model_checkpoint: str, force:bool=False): - from modules import sd_models +def set_checkpoint(sd_model_checkpoint: str, dtype:str=None, force:bool=False): + from modules import sd_models, devices if force: sd_models.unload_model_weights(op='model') + if dtype is not None: + shared.opts.cuda_dtype = dtype + devices.set_dtype() shared.opts.sd_model_checkpoint = sd_model_checkpoint model = sd_models.reload_model_weights() return { 'ok': model is not None } From 001716633d0e16712a313bbb40ab366e192b443b Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Aug 2025 16:40:42 -0400 Subject: [PATCH 136/141] fix namegen Signed-off-by: Vladimir Mandic --- modules/video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/video.py b/modules/video.py index 7ae09aaba..8efbad070 100644 --- a/modules/video.py +++ b/modules/video.py @@ -71,7 +71,7 @@ def save_video(p, images, filename = None, video_type: str = 'none', duration: f if filename is None and p is not None: filename = namegen.apply(shared.opts.samples_filename_pattern if shared.opts.samples_filename_pattern and len(shared.opts.samples_filename_pattern) > 0 else "[seq]-[prompt_words]") filename = os.path.join(shared.opts.outdir_video, filename) - filename = namegen.sequence(filename, shared.opts.outdir_video, '') + filename = namegen.sequence(filename) else: if os.path.sep not in filename: filename = os.path.join(shared.opts.outdir_video, filename) From 1ce9aae143171ba76a1b58e1ac1dc11ebdd46de0 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 14 Aug 2025 16:57:20 -0400 Subject: [PATCH 137/141] lint Signed-off-by: Vladimir Mandic --- modules/loader.py | 5 ++--- modules/sd_models.py | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/loader.py b/modules/loader.py index 8b889f382..e1c4cb5a2 100644 --- a/modules/loader.py +++ b/modules/loader.py @@ -199,8 +199,8 @@ def deprecate_warn(*args, **kwargs): diffusers.utils.deprecation_utils.deprecate = deprecate_warn diffusers.utils.deprecate = deprecate_warn + def patch_torch_version(): - import torch if not hasattr(torch, '__version_backup__'): torch.__version_backup__ = torch.__version__ # Convert string version to tuple format to solve TypeError caused by BnB @@ -215,8 +215,7 @@ def patch_torch_version(): return super().__ge__(other) torch.__version__ = VersionString(torch.__version__) + patch_torch_version() - - errors.log.info(f'Torch: torch=={torch.__version__} torchvision=={torchvision.__version__}') errors.log.info(f'Packages: diffusers=={diffusers.__version__} transformers=={transformers.__version__} accelerate=={accelerate.__version__} gradio=={gradio.__version__} pydantic=={pydantic.__version__} numpy=={np.__version__}') diff --git a/modules/sd_models.py b/modules/sd_models.py index 0b4cec80b..94824218a 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -74,6 +74,8 @@ def copy_diffuser_options(new_pipe, orig_pipe): def set_huggingface_options(op: str, model_type: str): + if model_type is not None: # overrides + pass if shared.opts.diffusers_to_gpu: # and model_type.startswith('Stable Diffusion'): shared.log.debug(f'Setting {op}: component=accelerate direct={shared.opts.diffusers_to_gpu}') sd_hijack_accelerate.hijack_accelerate() From 83be0d77d7db5979ad1bb14d22950b5d92088edd Mon Sep 17 00:00:00 2001 From: Seunghoon Lee Date: Fri, 15 Aug 2025 20:24:50 +0900 Subject: [PATCH 138/141] zluda hip sdk 6.4 --- modules/zluda_installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/zluda_installer.py b/modules/zluda_installer.py index 3afeb4680..da0ee290f 100644 --- a/modules/zluda_installer.py +++ b/modules/zluda_installer.py @@ -17,7 +17,7 @@ DLL_MAPPING = { 'cufftw.dll': 'cufftw64_10.dll', 'nvrtc.dll': 'nvrtc64_112_0.dll', } -HIPSDK_TARGETS = ['rocblas.dll', 'rocsolver.dll', 'hipfft.dll',] +HIPSDK_TARGETS = ['rocblas.dll', 'rocsolver.dll', 'rocsparse.dll', 'hipfft.dll',] MIOpen_enabled = False From 32906a20eebb8eaf6b4edab90f2889e625c1db30 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 15 Aug 2025 07:54:52 -0400 Subject: [PATCH 139/141] update docker build Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 26 ++++++++++++++++++-------- TODO.md | 7 ------- configs/Dockerfile.cuda | 4 ++-- modules/memstats.py | 2 +- package.json | 4 ++-- requirements.txt | 2 +- wiki | 2 +- 7 files changed, 25 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d2882bc..d648b4e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,28 @@ # Change Log for SD.Next -## Update for 2025-08-14 +## Update for 2025-08-15 -### Highlights for 2025-08-14 +### Highlights for 2025-08-15 -Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) (plus *Lightning* variant) and [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) -Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers), [HunyuanDiT](https://huggingface.co/Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers-Distilled) -Plus continuing with major **UI** work, we have new embedded **Docs/Wiki** search, redesigned real-time **hints**, **wildcards** UI selector, built-in **GPU monitor**, **CivitAI** integration and more! -On the compute side, new profiles for high-vram GPUs, offloading improvements, parallel-load for large models, support for new `torch` release and improved quality when using low-bit quantization! -And (*as always*) many bugfixes and improvements to existing features! +New release two weeks after the last one and its a big one with over 150 commits! +- Several new models: [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) (plus *Lightning* variant) and [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) +- Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers), [HunyuanDiT](https://huggingface.co/Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers-Distilled) +- Plus continuing with major **UI** work with new embedded **Docs/Wiki** search, redesigned real-time **hints**, **wildcards** UI selector, built-in **GPU monitor**, **CivitAI** integration and more! +- On the compute side, new profiles for high-vram GPUs, offloading improvements, parallel-load for large models, support for new `torch` release and improved quality when using low-bit quantization! +- And (*as always*) many bugfixes and improvements to existing features! + +We're also announcing **SD.Next Model Samples Gallery**, a pre-generated image gallery with 60 models (45 base and 15 finetunes) and 40 different styles resulting in 2,400 high resolution images! +Gallery additionally includes model details such as typical load and inference times as well as sizes and types of each model component (*e.g. unet, transformer, text-encoder, vae*) +[Live page](https://vladmandic.github.io/sd-samples/compare.html) | [GitHub repo](https://github.com/vladmandic/sd-samples) + +![sd-samples](https://github.com/user-attachments/assets/3efc8603-0766-4e4e-a4cb-d8c9b13d1e1d) *Note*: Change-in-behavior - locations of downloaded HuggingFace models and components are changed to allow for de-duplication of common modules and switched from using system default cache folder to `models/huggingface` SD.Next will warn on startup on unused cache entries that can be removed. Also, to take advantage of de-duplication, you'll need to delete models from your `models/Diffusers` folder and let SD.Next re-download them! [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) -### Details for 2025-08-14 +### Details for 2025-08-15 - **Models** - [Qwen-Image](https://qwenlm.github.io/blog/qwen-image/) @@ -132,6 +139,9 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, - new unified pipeline component loader in `pipelines/generic` - remove **LDSR** - remove `api-only` cli option +- **Docker** + - update cuda base image: `pytorch/pytorch:2.8.0-cuda12.8-cudnn9-runtime` + - update official builds: - **Fixes** - refactor legacy processing loop - fix settings components mismatch diff --git a/TODO.md b/TODO.md index f3371e091..389be8963 100644 --- a/TODO.md +++ b/TODO.md @@ -2,10 +2,6 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladmandic/projects) -## Current Candidates - -- `HF_ENABLE_PARALLEL_LOADING` - ## Future Candidates - Remote TE @@ -78,7 +74,6 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma - control: support scripts via api - fc: autodetect distilled based on model - fc: autodetect tensor format based on model -- flux: loader for civitai nf4 models - hypertile: vae breaks when using non-standard sizes - install: enable ROCm for windows when available - loader: load receipe @@ -86,8 +81,6 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma - lora: add other quantization types - lora: add t5 key support for sd35/f1 - lora: maybe force imediate quantization -- model load: add ChromaControlPipeline, ChromaInpaintPipeline -- model load: cogview4 balanced offload does not work for GlmModel - model load: force-reloading entire model as loading transformers only leads to massive memory usage - model load: group offload - model load: implement model in-memory caching diff --git a/configs/Dockerfile.cuda b/configs/Dockerfile.cuda index ca117bd4c..4e5a496e1 100644 --- a/configs/Dockerfile.cuda +++ b/configs/Dockerfile.cuda @@ -2,7 +2,7 @@ # docs: # base image -FROM pytorch/pytorch:2.7.0-cuda12.8-cudnn9-runtime +FROM pytorch/pytorch:2.8.0-cuda12.8-cudnn9-runtime # metadata LABEL org.opencontainers.image.vendor="SD.Next" @@ -13,7 +13,7 @@ LABEL org.opencontainers.image.source="https://github.com/vladmandic/sdnext/" LABEL org.opencontainers.image.licenses="AGPL-3.0" LABEL org.opencontainers.image.title="SD.Next" LABEL org.opencontainers.image.description="SD.Next: Advanced Implementation of Stable Diffusion and other Diffusion-based generative image models" -LABEL org.opencontainers.image.base.name="https://hub.docker.com/pytorch/pytorch:2.7.0-cuda12.8-cudnn9-runtime" +LABEL org.opencontainers.image.base.name="https://hub.docker.com/pytorch/pytorch:2.8.0-cuda12.8-cudnn9-runtime" LABEL org.opencontainers.image.version="latest" # minimum install diff --git a/modules/memstats.py b/modules/memstats.py index 1e2be27c8..fdc94df37 100644 --- a/modules/memstats.py +++ b/modules/memstats.py @@ -86,7 +86,7 @@ def gpu_stats(): gpu['error'] = str(e) if not fail_once: shared.log.error(f'GPU stats: {e}') - errors.display(e, 'GPU stats') + # errors.display(e, 'GPU stats') fail_once = True return gpu diff --git a/package.json b/package.json index 07b2681ec..1b0b95bca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@vladmandic/sdnext", "version": "dev", - "description": "SD.Next: Opinionated implementation of Stable Diffusion", + "description": "SD.Next: All-in-one WebUI for AI generative image and video creation", "author": "Vladimir Mandic ", "bugs": { "url": "https://github.com/vladmandic/sdnext/issues" @@ -19,7 +19,7 @@ "venv": ". venv/bin/activate", "start": ". venv/bin/activate; python launch.py --debug", "localize": "node cli/localize.js", - "packages": ". venv/bin/activate && pip install --upgrade transformers accelerate huggingface_hub safetensors tokenizers peft compel pytorch_lightning", + "packages": ". venv/bin/activate && pip install --upgrade transformers accelerate huggingface_hub safetensors tokenizers peft compel pytorch_lightning pylint ruff", "eslint": "eslint . javascript/ extensions-builtin/sdnext-modernui/javascript/", "ruff": ". venv/bin/activate && ruff check", "pylint": ". venv/bin/activate && pylint *.py modules/ pipelines/ scripts/ extensions-builtin/ | grep -v '^*'", diff --git a/requirements.txt b/requirements.txt index f304e5c15..20fe134c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,7 +47,7 @@ einops==0.8.1 huggingface_hub==0.34.4 numexpr==2.11.0 numpy==2.1.2 -pandas==2.3.0 +pandas==2.3.1 numba==0.61.2 protobuf==4.25.3 pytorch_lightning==2.5.3 diff --git a/wiki b/wiki index fe2558394..f91e819d2 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit fe255839498563e30bf1a1db9b5f43a36e1196c6 +Subproject commit f91e819d22603f34be0c3e8fb674d4ab89421622 From 599c7c56f447a08d5415a79dd0d4ef3049818a1b Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 15 Aug 2025 07:57:06 -0400 Subject: [PATCH 140/141] update changelog Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d648b4e6b..f69a59e6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -154,7 +154,8 @@ SD.Next will warn on startup on unused cache entries that can be removed. Also, - fix processing image save loop - fix progress bar with refine/detailer - fix api progress reporting endpoint - - fix openvino backend failing to compile + - fix `openvino` backend failing to compile + - fix `zluda` with hip-sdk==6.4 - fix `nunchaku` fallback on unsupported model - fix `nunchaku` windows download links - fix *Flux.1-Kontext-Dev* with variable resolution From f8421a39afb3550a75e8466d3a5886aea672393e Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 15 Aug 2025 08:02:41 -0400 Subject: [PATCH 141/141] update changelog Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f69a59e6d..2fbb69c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,19 +9,17 @@ New release two weeks after the last one and its a big one with over 150 commits - Several updated models: [Chroma](https://huggingface.co/lodestones/Chroma), [SkyReels-V2](https://huggingface.co/Skywork/SkyReels-V2-DF-14B-720P-Diffusers), [Wan-VACE](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers), [HunyuanDiT](https://huggingface.co/Tencent-Hunyuan/HunyuanDiT-v1.2-Diffusers-Distilled) - Plus continuing with major **UI** work with new embedded **Docs/Wiki** search, redesigned real-time **hints**, **wildcards** UI selector, built-in **GPU monitor**, **CivitAI** integration and more! - On the compute side, new profiles for high-vram GPUs, offloading improvements, parallel-load for large models, support for new `torch` release and improved quality when using low-bit quantization! +- [SD.Next Model Samples Gallery](https://vladmandic.github.io/sd-samples/compare.html): pre-generated image gallery with 60 models (45 base and 15 finetunes) and 40 different styles resulting in 2,400 high resolution images! + gallery additionally includes model details such as typical load and inference times as well as sizes and types of each model component (*e.g. unet, transformer, text-encoder, vae*) - And (*as always*) many bugfixes and improvements to existing features! -We're also announcing **SD.Next Model Samples Gallery**, a pre-generated image gallery with 60 models (45 base and 15 finetunes) and 40 different styles resulting in 2,400 high resolution images! -Gallery additionally includes model details such as typical load and inference times as well as sizes and types of each model component (*e.g. unet, transformer, text-encoder, vae*) -[Live page](https://vladmandic.github.io/sd-samples/compare.html) | [GitHub repo](https://github.com/vladmandic/sd-samples) - ![sd-samples](https://github.com/user-attachments/assets/3efc8603-0766-4e4e-a4cb-d8c9b13d1e1d) +[ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) + *Note*: Change-in-behavior - locations of downloaded HuggingFace models and components are changed to allow for de-duplication of common modules and switched from using system default cache folder to `models/huggingface` SD.Next will warn on startup on unused cache entries that can be removed. Also, to take advantage of de-duplication, you'll need to delete models from your `models/Diffusers` folder and let SD.Next re-download them! -[ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) - ### Details for 2025-08-15 - **Models**
Alias{getattr(item, 'alias', 'N/A')}
Filename{item.filename}
Hash{getattr(item, 'hash', 'N/A')}
Size{round(stat.st_size/1024/1024, 2) if stat is not None else 'N/A'} MB
Last modified{datetime.fromtimestamp(stat.st_mtime) if stat is not None else 'N/A'}
Size{round(stat_size/1024/1024, 2)} MB
Last modified{stat_mtime}
Source URL{url}
{pipeline.__name__ if pipeline else '(unknown)'} {row.shorthash} {size}{datetime.fromtimestamp(stat.st_mtime).replace(microsecond=0)}{stat_mtime}