add sd35-ipadapter and more balanced offload optimizations

Signed-off-by: Vladimir Mandic <mandic00@live.com>
This commit is contained in:
Vladimir Mandic
2024-12-20 10:22:42 -05:00
parent 6e0ef69103
commit 20f2554cec
7 changed files with 148 additions and 97 deletions
+9 -7
View File
@@ -4,7 +4,7 @@
### Highlights for 2024-12-19
### SD.Next X-mass edition: *What's new?*
### SD.Next Xmass edition: *What's new?*
While we have several new supported models, workflows and tools, this release is primarily about *quality-of-life improvements*:
- New memory management engine: list of changes that went into this one is long: changes to GPU offloading, LoRA loader, system memory management, etc.
@@ -15,18 +15,18 @@ While we have several new supported models, workflows and tools, this release is
- New settings panel with simplified and streamlined configuration
We've also added support for several new models (see [supported models](https://vladmandic.github.io/sdnext-docs/Model-Support/) for full list) such as highly anticipated [NVLabs Sana](https://huggingface.co/Efficient-Large-Model/Sana_1600M_1024px)
And several new video models: [Lightricks LTX-Video](https://huggingface.co/Lightricks/LTX-Video), [Hunyuan Video](https://huggingface.co/tencent/HunyuanVideo) and [Genmo Mochi.1 Preview](https://huggingface.co/genmo/mochi-1-preview)
And several new SOTA video models: [Lightricks LTX-Video](https://huggingface.co/Lightricks/LTX-Video), [Hunyuan Video](https://huggingface.co/tencent/HunyuanVideo) and [Genmo Mochi.1 Preview](https://huggingface.co/genmo/mochi-1-preview)
And a lot of Control and IPAdapter goodies
- for SDXL there is new [ProMax](https://huggingface.co/xinsir/controlnet-union-sdxl-1.0), improved *Union* and *Tiling*
- for FLUX.1 there are [Flux Tools](https://blackforestlabs.ai/flux-1-tools/) as well as official *Canny* and *Depth* models and a cool [Redux](https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev) model
- for SD 3.5 there are official *Canny*, *Blur* and *Depth* in addition to existing 3rd party models
- for **SDXL** there is new [ProMax](https://huggingface.co/xinsir/controlnet-union-sdxl-1.0), improved *Union* and *Tiling* models
- for **FLUX.1** there are [Flux Tools](https://blackforestlabs.ai/flux-1-tools/) as well as official *Canny* and *Depth* models and a cool [Redux](https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev) model
- for **SD3.5** there are official *Canny*, *Blur* and *Depth* models in addition to existing 3rd party models as well as [InstantX](https://huggingface.co/InstantX/SD3.5-Large-IP-Adapter) IP-adapter
Plus couple of new integrated workflows such as [FreeScale](https://github.com/ali-vilab/FreeScale) and [Style Aligned Image Generation](https://style-aligned-gen.github.io/)
And it wouldn't be a X-mass edition custom themes: *Snowflake* and *Elf-Green*
And it wouldn't be a Xmass edition without couple of custom themes: *Snowflake* and *Elf-Green*!
[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)
[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 2024-12-19
@@ -136,6 +136,7 @@ And it wouldn't be a X-mass edition custom themes: *Snowflake* and *Elf-Green*
- balanced offload: add both high and low watermark, defaults as below
`0.25` for low-watermark: skip offload if memory usage is below 25%
`0.70` high-watermark: must offload if memory usage is above 70%
- balanced offload will attempt to run offload as non-blocking and force gc at the end
- change-in-behavior:
low-end systems, triggered by either `lowvrwam` or by detection of <=4GB will use *sequential offload*
all other systems use *balanced offload* by default (can be changed in settings)
@@ -198,6 +199,7 @@ And it wouldn't be a X-mass edition custom themes: *Snowflake* and *Elf-Green*
- taesd limit render to 2024px
- taesd downscale preview to 1024px max: configurable in settings -> live preview
- uninstall conflicting `wandb` package
- dont skip diffusers version check if quick is specified
## Update for 2024-11-21
+2 -2
View File
@@ -457,9 +457,9 @@ def check_python(supported_minors=[9, 10, 11, 12], reason=None):
# check diffusers version
def check_diffusers():
if args.skip_all or args.skip_requirements:
if args.skip_all or args.skip_git:
return
sha = '862a7d5038c1c53641ffcab146a7eeb5ab683656' # diffusers commit hash
sha = 'b64ca6c11cbc1644c22f1dae441c8124d588bb14' # 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 0)
cur = opts.get('diffusers_version', '') if minor > 0 else ''
+12 -6
View File
@@ -186,7 +186,7 @@ def get_device_for(task): # pylint: disable=unused-argument
return get_optimal_device()
def torch_gc(force=False, fast=False):
def torch_gc(force:bool=False, fast:bool=False, reason:str=None):
def get_stats():
mem_dict = memstats.memory_stats()
gpu_dict = mem_dict.get('gpu', {})
@@ -207,15 +207,21 @@ def torch_gc(force=False, fast=False):
from modules.shared import cmd_opts
t0 = time.time()
gpu, used_gpu, ram, used_ram, oom = get_stats()
gpu, used_gpu, ram, _used_ram, oom = get_stats()
threshold = 0 if (cmd_opts.lowvram and not cmd_opts.use_zluda) else opts.torch_gc_threshold
collected = 0
if force or threshold == 0 or used_gpu >= threshold or used_ram >= threshold:
if reason is None and force:
reason='force'
if threshold == 0 or used_gpu >= threshold:
force = True
if reason is None:
reason = 'threshold'
if oom > previous_oom:
previous_oom = oom
log.warning(f'Torch GPU out-of-memory error: {memstats.memory_stats()}')
force = True
if reason is None:
reason = 'oom'
if force:
# actual gc
collected = gc.collect() if not fast else 0 # python gc
@@ -237,10 +243,10 @@ def torch_gc(force=False, fast=False):
new_gpu, new_used_gpu, new_ram, new_used_ram, oom = get_stats()
before = { 'gpu': gpu, 'ram': ram }
after = { 'gpu': new_gpu, 'ram': new_ram, 'oom': oom }
utilization = { 'gpu': new_used_gpu, 'ram': new_used_ram, 'threshold': threshold }
results = { 'saved': round(gpu - new_gpu, 2), 'collected': collected }
utilization = { 'gpu': new_used_gpu, 'ram': new_used_ram }
results = { 'gpu': round(gpu - new_gpu, 2), 'py': collected }
fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access
log.debug(f'GC: utilization={utilization} gc={results} before={before} after={after} device={torch.device(get_optimal_device_name())} fn={fn} time={round(t1 - t0, 2)}')
log.debug(f'GC: current={after} prev={before} load={utilization} gc={results} fn={fn} why={reason} time={t1-t0:.2f}')
return new_gpu, new_ram
+116 -77
View File
@@ -9,12 +9,15 @@ import os
import time
import json
from PIL import Image
import diffusers
import transformers
from modules import processing, shared, devices, sd_models
clip_repo = "h94/IP-Adapter"
clip_loaded = None
adapters_loaded = []
CLIP_ID = "h94/IP-Adapter"
SIGLIP_ID = 'google/siglip-so400m-patch14-384'
ADAPTERS_NONE = {
'None': { 'name': 'none', 'repo': 'none', 'subfolder': 'none' },
}
@@ -37,11 +40,13 @@ ADAPTERS_SDXL = {
'Ostris Composition ViT-H SDXL': { 'name': 'ip_plus_composition_sdxl.safetensors', 'repo': 'ostris/ip-composition-adapter', 'subfolder': '' },
}
ADAPTERS_SD3 = {
'InstantX Large': { 'name': 'ip-adapter.bin', 'repo': 'InstantX/SD3.5-Large-IP-Adapter' },
'None': { 'name': 'none', 'repo': 'none', 'subfolder': 'none' },
'InstantX Large': { 'name': 'none', 'repo': 'InstantX/SD3.5-Large-IP-Adapter', 'subfolder': 'none', 'revision': 'refs/pr/10' },
}
ADAPTERS_F1 = {
'XLabs AI v1': { 'name': 'ip_adapter.safetensors', 'repo': 'XLabs-AI/flux-ip-adapter' },
'XLabs AI v2': { 'name': 'ip_adapter.safetensors', 'repo': 'XLabs-AI/flux-ip-adapter-v2' },
'None': { 'name': 'none', 'repo': 'none', 'subfolder': 'none' },
'XLabs AI v1': { 'name': 'ip_adapter.safetensors', 'repo': 'XLabs-AI/flux-ip-adapter', 'subfolder': 'none' },
'XLabs AI v2': { 'name': 'ip_adapter.safetensors', 'repo': 'XLabs-AI/flux-ip-adapter-v2', 'subfolder': 'none' },
}
ADAPTERS = { **ADAPTERS_SD15, **ADAPTERS_SDXL, **ADAPTERS_SD3, **ADAPTERS_F1 }
ADAPTERS_ALL = { **ADAPTERS_SD15, **ADAPTERS_SDXL, **ADAPTERS_SD3, **ADAPTERS_F1 }
@@ -126,6 +131,8 @@ def crop_images(images, crops):
shared.log.error(f'IP adapter: failed to crop image: source={len(images[i])} faces={len(cropped)}')
except Exception as e:
shared.log.error(f'IP adapter: failed to crop image: {e}')
if shared.sd_model_type == 'sd3' and len(images) == 1:
return images[0]
return images
@@ -144,27 +151,64 @@ def unapply(pipe): # pylint: disable=arguments-differ
pass
def apply(pipe, p: processing.StableDiffusionProcessing, adapter_names=[], adapter_scales=[1.0], adapter_crops=[False], adapter_starts=[0.0], adapter_ends=[1.0], adapter_images=[]):
global clip_loaded, adapters_loaded # pylint: disable=global-statement
# overrides
if hasattr(p, 'ip_adapter_names'):
if isinstance(p.ip_adapter_names, str):
p.ip_adapter_names = [p.ip_adapter_names]
adapters = [ADAPTERS_ALL.get(adapter_name, None) for adapter_name in p.ip_adapter_names if adapter_name is not None and adapter_name.lower() != 'none']
adapter_names = p.ip_adapter_names
else:
if isinstance(adapter_names, str):
adapter_names = [adapter_names]
adapters = [ADAPTERS.get(adapter, None) for adapter in adapter_names]
adapters = [adapter for adapter in adapters if adapter is not None and adapter['name'].lower() != 'none']
if len(adapters) == 0:
unapply(pipe)
if hasattr(p, 'ip_adapter_images'):
del p.ip_adapter_images
return False
if shared.sd_model_type not in ['sd', 'sdxl', 'sd3', 'f1']:
shared.log.error(f'IP adapter: model={shared.sd_model_type} class={pipe.__class__.__name__} not supported')
return False
def load_image_encoder(pipe: diffusers.DiffusionPipeline, adapter_names: list[str]):
global clip_loaded # pylint: disable=global-statement
for adapter_name in adapter_names:
# which clip to use
clip_repo = CLIP_ID
if 'ViT' not in adapter_name: # defaults per model
clip_subfolder = 'models/image_encoder' if shared.sd_model_type == 'sd' else 'sdxl_models/image_encoder'
if 'ViT-H' in adapter_name:
clip_subfolder = 'models/image_encoder' # this is vit-h
elif 'ViT-G' in adapter_name:
clip_subfolder = 'sdxl_models/image_encoder' # this is vit-g
else:
if shared.sd_model_type == 'sd':
clip_subfolder = 'models/image_encoder'
elif shared.sd_model_type == 'sdxl':
clip_subfolder = 'sdxl_models/image_encoder'
elif shared.sd_model_type == 'sd3':
clip_repo = SIGLIP_ID
clip_subfolder = None
elif shared.sd_model_type == 'f1':
shared.log.error(f'IP adapter: adapter={adapter_name} type={shared.sd_model_type} cls={shared.sd_model.__class__.__name__}: unsupported base model')
return False
else:
shared.log.error(f'IP adapter: unknown model type: {adapter_name}')
return False
# load image encoder used by ip adapter
if pipe.image_encoder is None or clip_loaded != f'{clip_repo}/{clip_subfolder}':
try:
if shared.sd_model_type == 'sd3':
pipe.image_encoder = transformers.SiglipVisionModel.from_pretrained(clip_repo, torch_dtype=devices.dtype, cache_dir=shared.opts.hfcache_dir)
else:
pipe.image_encoder = transformers.CLIPVisionModelWithProjection.from_pretrained(clip_repo, subfolder=clip_subfolder, torch_dtype=devices.dtype, cache_dir=shared.opts.hfcache_dir, use_safetensors=True)
shared.log.debug(f'IP adapter load: encoder="{clip_repo}/{clip_subfolder}" cls={pipe.image_encoder.__class__.__name__}')
clip_loaded = f'{clip_repo}/{clip_subfolder}'
except Exception as e:
shared.log.error(f'IP adapter load: encoder="{clip_repo}/{clip_subfolder}" {e}')
return False
sd_models.move_model(pipe.image_encoder, devices.device)
return True
def load_feature_extractor(pipe):
# load feature extractor used by ip adapter
if pipe.feature_extractor is None:
try:
if shared.sd_model_type == 'sd3':
pipe.feature_extractor = transformers.SiglipImageProcessor.from_pretrained(SIGLIP_ID, torch_dtype=devices.dtype, cache_dir=shared.opts.hfcache_dir)
else:
pipe.feature_extractor = transformers.CLIPImageProcessor()
shared.log.debug(f'IP adapter load: extractor={pipe.feature_extractor.__class__.__name__}')
except Exception as e:
shared.log.error(f'IP adapter load: extractor {e}')
return False
return True
def parse_params(p: processing.StableDiffusionProcessing, adapters: list, adapter_scales: list[float], adapter_crops: list[bool], adapter_starts: list[float], adapter_ends: list[float], adapter_images: list):
if hasattr(p, 'ip_adapter_scales'):
adapter_scales = p.ip_adapter_scales
if hasattr(p, 'ip_adapter_crops'):
@@ -205,6 +249,33 @@ def apply(pipe, p: processing.StableDiffusionProcessing, adapter_names=[], adapt
p.ip_adapter_starts = adapter_starts.copy()
adapter_ends = get_scales(adapter_ends, adapter_images)
p.ip_adapter_ends = adapter_ends.copy()
return adapter_images, adapter_masks, adapter_scales, adapter_crops, adapter_starts, adapter_ends
def apply(pipe, p: processing.StableDiffusionProcessing, adapter_names=[], adapter_scales=[1.0], adapter_crops=[False], adapter_starts=[0.0], adapter_ends=[1.0], adapter_images=[]):
global adapters_loaded # pylint: disable=global-statement
# overrides
if hasattr(p, 'ip_adapter_names'):
if isinstance(p.ip_adapter_names, str):
p.ip_adapter_names = [p.ip_adapter_names]
adapters = [ADAPTERS_ALL.get(adapter_name, None) for adapter_name in p.ip_adapter_names if adapter_name is not None and adapter_name.lower() != 'none']
adapter_names = p.ip_adapter_names
else:
if isinstance(adapter_names, str):
adapter_names = [adapter_names]
adapters = [ADAPTERS.get(adapter_name, None) for adapter_name in adapter_names if adapter_name.lower() != 'none']
if len(adapters) == 0:
unapply(pipe)
if hasattr(p, 'ip_adapter_images'):
del p.ip_adapter_images
return False
if shared.sd_model_type not in ['sd', 'sdxl', 'sd3', 'f1']:
shared.log.error(f'IP adapter: model={shared.sd_model_type} class={pipe.__class__.__name__} not supported')
return False
adapter_images, adapter_masks, adapter_scales, adapter_crops, adapter_starts, adapter_ends = parse_params(p, adapters, adapter_scales, adapter_crops, adapter_starts, adapter_ends, adapter_images)
# init code
if pipe is None:
return False
@@ -223,61 +294,29 @@ def apply(pipe, p: processing.StableDiffusionProcessing, adapter_names=[], adapt
shared.log.error(f'IP adapter: pipeline not supported: {pipe.__class__.__name__}')
return False
for adapter_name in adapter_names:
# which clip to use
if 'ViT' not in adapter_name: # defaults per model
if shared.sd_model_type == 'sd':
clip_subfolder = 'models/image_encoder'
else:
clip_subfolder = 'sdxl_models/image_encoder'
if 'ViT-H' in adapter_name:
clip_subfolder = 'models/image_encoder' # this is vit-h
elif 'ViT-G' in adapter_name:
clip_subfolder = 'sdxl_models/image_encoder' # this is vit-g
else:
if shared.sd_model_type == 'sd':
clip_subfolder = 'models/image_encoder'
elif shared.sd_model_type == 'sdxl':
clip_subfolder = 'sdxl_models/image_encoder'
elif shared.sd_model_type == 'sd3':
shared.log.error(f'IP adapter: adapter={adapter_name} type={shared.sd_model_type} cls={shared.sd_model.__class__.__name__}: unsupported base model')
return False
elif shared.sd_model_type == 'f1':
shared.log.error(f'IP adapter: adapter={adapter_name} type={shared.sd_model_type} cls={shared.sd_model.__class__.__name__}: unsupported base model')
return False
else:
shared.log.error(f'IP adapter: unknown model type: {adapter_name}')
return False
if not load_image_encoder(pipe, adapter_names):
return False
# load feature extractor used by ip adapter
if pipe.feature_extractor is None:
try:
from transformers import CLIPImageProcessor
shared.log.debug('IP adapter load: feature extractor')
pipe.feature_extractor = CLIPImageProcessor()
except Exception as e:
shared.log.error(f'IP adapter load: feature extractor {e}')
return False
# load image encoder used by ip adapter
if pipe.image_encoder is None or clip_loaded != f'{clip_repo}/{clip_subfolder}':
try:
from transformers import CLIPVisionModelWithProjection
shared.log.debug(f'IP adapter load: image encoder="{clip_repo}/{clip_subfolder}"')
pipe.image_encoder = CLIPVisionModelWithProjection.from_pretrained(clip_repo, subfolder=clip_subfolder, torch_dtype=devices.dtype, cache_dir=shared.opts.diffusers_dir, use_safetensors=True)
clip_loaded = f'{clip_repo}/{clip_subfolder}'
except Exception as e:
shared.log.error(f'IP adapter load: image encoder="{clip_repo}/{clip_subfolder}" {e}')
return False
sd_models.move_model(pipe.image_encoder, devices.device)
if not load_feature_extractor(pipe):
return False
# main code
try:
t0 = time.time()
repos = [adapter['repo'] for adapter in adapters]
subfolders = [adapter['subfolder'] for adapter in adapters]
names = [adapter['name'] for adapter in adapters]
pipe.load_ip_adapter(repos, subfolder=subfolders, weight_name=names)
repos = [adapter.get('repo', None) for adapter in adapters if adapter.get('repo', 'none') != 'none']
subfolders = [adapter.get('subfolder', None) for adapter in adapters if adapter.get('subfolder', 'none') != 'none']
names = [adapter.get('name', None) for adapter in adapters if adapter.get('name', 'none') != 'none']
revisions = [adapter.get('revision', None) for adapter in adapters if adapter.get('revision', 'none') != 'none']
kwargs = {}
if len(repos) == 1:
repos = repos[0]
if len(subfolders) > 0:
kwargs['subfolder'] = subfolders if len(subfolders) > 1 else subfolders[0]
if len(names) > 0:
kwargs['weight_name'] = names if len(names) > 1 else names[0]
if len(revisions) > 0:
kwargs['revision'] = revisions[0]
pipe.load_ip_adapter(repos, **kwargs)
adapters_loaded = names
if hasattr(p, 'ip_adapter_layers'):
pipe.set_ip_adapter_scale(p.ip_adapter_layers)
@@ -286,8 +325,8 @@ def apply(pipe, p: processing.StableDiffusionProcessing, adapter_names=[], adapt
for i in range(len(adapter_scales)):
if adapter_starts[i] > 0:
adapter_scales[i] = 0.00
pipe.set_ip_adapter_scale(adapter_scales)
ip_str = [f'{os.path.splitext(adapter)[0]}:{scale}:{start}:{end}' for adapter, scale, start, end in zip(adapter_names, adapter_scales, adapter_starts, adapter_ends)]
pipe.set_ip_adapter_scale(adapter_scales if len(adapter_scales) > 1 else adapter_scales[0])
ip_str = [f'{os.path.splitext(adapter)[0]}:{scale}:{start}:{end}:{crop}' for adapter, scale, start, end, crop in zip(adapter_names, adapter_scales, adapter_starts, adapter_ends, adapter_crops)]
p.task_args['ip_adapter_image'] = crop_images(adapter_images, adapter_crops)
if len(adapter_masks) > 0:
p.cross_attention_kwargs = { 'ip_adapter_masks': adapter_masks }
+1 -1
View File
@@ -480,5 +480,5 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
if not p.disable_extra_networks:
shared.log.info(f'Processed: images={len(output_images)} its={(p.steps * len(output_images)) / (t1 - t0):.2f} time={t1-t0:.2f} timers={timer.process.dct()} memory={memstats.memory_stats()}')
devices.torch_gc(force=True)
devices.torch_gc(force=True, reason='final')
return processed
+6 -3
View File
@@ -496,7 +496,11 @@ def apply_balanced_offload(sd_model, exclude=[]):
used_gpu -= module_size
debug_move(f'Offload: type=balanced op={"move" if do_offload else "skip"} gpu={prev_gpu:.3f}:{used_gpu:.3f} perc={perc_gpu:.2f} ram={used_ram:.3f} current={module.device} dtype={module.dtype} component={module.__class__.__name__} size={module_size:.3f}')
except Exception as e:
if 'bitsandbytes' not in str(e):
if 'out of memory' in str(e):
devices.torch_gc(fast=True, force=True, reason='oom')
elif 'bitsandbytes' in str(e):
pass
else:
shared.log.error(f'Offload: type=balanced op=apply module={module_name} {e}')
if os.environ.get('SD_MOVE_DEBUG', None):
errors.display(e, f'Offload: type=balanced op=apply module={module_name}')
@@ -508,7 +512,7 @@ def apply_balanced_offload(sd_model, exclude=[]):
if device_map and max_memory:
module.balanced_offload_device_map = device_map
module.balanced_offload_max_memory = max_memory
devices.torch_gc(fast=True, force=True)
devices.torch_gc(fast=True, force=True, reason='offload')
apply_balanced_offload_to_module(sd_model)
if hasattr(sd_model, "pipe"):
@@ -518,7 +522,6 @@ def apply_balanced_offload(sd_model, exclude=[]):
if hasattr(sd_model, "decoder_pipe"):
apply_balanced_offload_to_module(sd_model.decoder_pipe)
set_accelerate(sd_model)
devices.torch_gc(fast=True)
t = time.time() - t0
process_timer.add('offload', t)
fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access
+2 -1
View File
@@ -7,7 +7,8 @@ from modules import scripts, processing, shared, devices, sd_models
from installer import install
redux_pipe: diffusers.FluxPriorReduxPipeline = None
# redux_pipe: diffusers.FluxPriorReduxPipeline = None
redux_pipe = None
processor_canny = None
processor_depth = None
title = 'Flux Tools'