From 3564b34e0d722508d613f8fd1891b63147325dbf Mon Sep 17 00:00:00 2001 From: Disty0 Date: Tue, 15 Apr 2025 00:59:46 +0300 Subject: [PATCH 1/9] Prompt cache support for HiDream --- modules/processing_args.py | 37 +++++++++------ modules/prompt_parser_diffusers.py | 75 ++++++++++++++++++++++-------- modules/sd_hijack_te.py | 2 + 3 files changed, 80 insertions(+), 34 deletions(-) diff --git a/modules/processing_args.py b/modules/processing_args.py index 884f189ed..d2465bc6c 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -143,7 +143,8 @@ def set_pipeline_args(p, model, prompts:list, negative_prompts:list, prompts_2:t if (prompt_attention != 'fixed') and ('Onnx' not in model.__class__.__name__) and ('prompt' not in p.task_args) and ( 'StableDiffusion' in model.__class__.__name__ or 'StableCascade' in model.__class__.__name__ or - 'Flux' in model.__class__.__name__ + 'Flux' in model.__class__.__name__ or + 'HiDreamImage' in model.__class__.__name__ ): try: prompt_parser_diffusers.embedder = prompt_parser_diffusers.PromptEmbedder(prompts, negative_prompts, steps, clip_skip, p) @@ -162,25 +163,31 @@ def set_pipeline_args(p, model, prompts:list, negative_prompts:list, prompts_2:t prompts = [p.replace('|image|', '<|image_1|>') for p in prompts] if hasattr(model, 'text_encoder') and hasattr(model, 'tokenizer') and 'prompt_embeds' in possible and prompt_parser_diffusers.embedder is not None: args['prompt_embeds'] = prompt_parser_diffusers.embedder('prompt_embeds') - if 'StableCascade' in model.__class__.__name__ and prompt_parser_diffusers.embedder is not None: - args['prompt_embeds_pooled'] = prompt_parser_diffusers.embedder('positive_pooleds').unsqueeze(0) - elif 'XL' in model.__class__.__name__ and prompt_parser_diffusers.embedder is not None: - args['pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('positive_pooleds') - elif 'StableDiffusion3' in model.__class__.__name__ and prompt_parser_diffusers.embedder is not None: - args['pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('positive_pooleds') - elif 'Flux' in model.__class__.__name__ and prompt_parser_diffusers.embedder is not None: - args['pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('positive_pooleds') + if prompt_parser_diffusers.embedder is not None: + if 'StableCascade' in model.__class__.__name__: + args['prompt_embeds_pooled'] = prompt_parser_diffusers.embedder('positive_pooleds').unsqueeze(0) + elif 'XL' in model.__class__.__name__: + args['pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('positive_pooleds') + elif 'StableDiffusion3' in model.__class__.__name__: + args['pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('positive_pooleds') + elif 'Flux' in model.__class__.__name__: + args['pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('positive_pooleds') + elif 'HiDreamImage' in model.__class__.__name__: + args['pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('positive_pooleds') else: args['prompt'] = prompts if 'negative_prompt' in possible: if hasattr(model, 'text_encoder') and hasattr(model, 'tokenizer') and 'negative_prompt_embeds' in possible and prompt_parser_diffusers.embedder is not None: args['negative_prompt_embeds'] = prompt_parser_diffusers.embedder('negative_prompt_embeds') - if 'StableCascade' in model.__class__.__name__ and prompt_parser_diffusers.embedder is not None: - args['negative_prompt_embeds_pooled'] = prompt_parser_diffusers.embedder('negative_pooleds').unsqueeze(0) - if 'XL' in model.__class__.__name__ and prompt_parser_diffusers.embedder is not None: - args['negative_pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('negative_pooleds') - if 'StableDiffusion3' in model.__class__.__name__ and prompt_parser_diffusers.embedder is not None: - args['negative_pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('negative_pooleds') + if prompt_parser_diffusers.embedder is not None: + if 'StableCascade' in model.__class__.__name__: + args['negative_prompt_embeds_pooled'] = prompt_parser_diffusers.embedder('negative_pooleds').unsqueeze(0) + elif 'XL' in model.__class__.__name__: + args['negative_pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('negative_pooleds') + elif 'StableDiffusion3' in model.__class__.__name__: + args['negative_pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('negative_pooleds') + elif 'HiDreamImage' in model.__class__.__name__: + args['negative_pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('negative_pooleds') else: if 'PixArtSigmaPipeline' in model.__class__.__name__: # pixart-sigma pipeline throws list-of-list for negative prompt args['negative_prompt'] = negative_prompts[0] diff --git a/modules/prompt_parser_diffusers.py b/modules/prompt_parser_diffusers.py index f6be32cdb..5267147c0 100644 --- a/modules/prompt_parser_diffusers.py +++ b/modules/prompt_parser_diffusers.py @@ -26,7 +26,8 @@ def prompt_compatible(pipe = None): 'StableDiffusion' not in pipe.__class__.__name__ and 'DemoFusion' not in pipe.__class__.__name__ and 'StableCascade' not in pipe.__class__.__name__ and - 'Flux' not in pipe.__class__.__name__ + 'Flux' not in pipe.__class__.__name__ and + 'HiDreamImage' not in pipe.__class__.__name__ ): shared.log.warning(f"Prompt parser not supported: {pipe.__class__.__name__}") return False @@ -190,14 +191,32 @@ class PromptEmbedder: def __call__(self, key, step=0): batch = getattr(self, key) res = [] - for i in range(self.batchsize): - if len(batch[i]) == 0: # if asking for a null key, ie pooled on SD1.5 - return None - try: - res.append(batch[i][step]) - except IndexError: - res.append(batch[i][0]) # if not scheduled, return default - return torch.cat(res) + if isinstance(batch[0][0], list) and len(batch[0][0]) == 2 and isinstance(batch[0][0][1], torch.Tensor) and batch[0][0][1].shape[0] == 32: + # hidream uses a list of t5 + llama prompt embeds: [t5_embeds, llama_embeds] + # t5_embeds shape: [batch_size, seq_len, dim] + # llama_embeds shape: [number_of_hidden_states, batch_size, seq_len, dim] + res2 = [] + for i in range(self.batchsize): + if len(batch[i]) == 0: # if asking for a null key, ie pooled on SD1.5 + return None + try: + res.append(batch[i][step][0]) + res2.append(batch[i][step][1]) + except IndexError: + # if not scheduled, return default + res.append(batch[i][0][0]) + res2.append(batch[i][0][1]) + res = [torch.cat(res, dim=0), torch.cat(res2, dim=1)] + return res + else: + for i in range(self.batchsize): + if len(batch[i]) == 0: # if asking for a null key, ie pooled on SD1.5 + return None + try: + res.append(batch[i][step]) + except IndexError: + res.append(batch[i][0]) # if not scheduled, return default + return torch.cat(res) def compel_hijack(self, token_ids: torch.Tensor, attention_mask: typing.Optional[torch.Tensor] = None) -> torch.Tensor: @@ -457,21 +476,43 @@ def split_prompts(pipe, prompt, SD3 = False): else: prompt3 = prompt + if prompt.find("TE4:") != -1: + prompt, prompt4 = prompt.split("TE4:") + elif prompt2.find("TE4:") != -1: + prompt2, prompt4 = prompt2.split("TE4:") + elif prompt3.find("TE4:") != -1: + prompt3, prompt4 = prompt3.split("TE4:") + else: + prompt4 = prompt + prompt = prompt.strip() prompt2 = " " if prompt2.strip() == "" else prompt2.strip() prompt3 = " " if prompt3.strip() == "" else prompt3.strip() + prompt4 = " " if prompt4.strip() == "" else prompt4.strip() if SD3 and prompt3 != " ": ps, _ws = get_prompts_with_weights(pipe, prompt3) prompt3 = " ".join(ps) - return prompt, prompt2, prompt3 + return prompt, prompt2, prompt3, prompt4 def get_weighted_text_embeddings(pipe, prompt: str = "", neg_prompt: str = "", clip_skip: int = None): device = devices.device - SD3 = hasattr(pipe, 'text_encoder_3') - prompt, prompt_2, prompt_3 = split_prompts(pipe, prompt, SD3) - neg_prompt, neg_prompt_2, neg_prompt_3 = split_prompts(pipe, neg_prompt, SD3) + SD3 = bool(hasattr(pipe, 'text_encoder_3') and not hasattr(pipe, 'text_encoder_4')) + prompt, prompt_2, prompt_3, prompt_4 = split_prompts(pipe, prompt, SD3) + neg_prompt, neg_prompt_2, neg_prompt_3, neg_prompt_4 = split_prompts(pipe, neg_prompt, SD3) + + if "Flux" in pipe.__class__.__name__: # clip is only used for the pooled embeds + prompt_embeds, pooled_prompt_embeds, _ = pipe.encode_prompt(prompt=prompt, prompt_2=prompt_2, device=device, num_images_per_prompt=1) + return prompt_embeds, pooled_prompt_embeds, None, None # no negative support + + if "HiDreamImage" in pipe.__class__.__name__: # clip is only used for the pooled embeds + prompt_embeds, negative_prompt_embeds, pooled_prompt_embeds, negative_pooled_prompt_embeds = pipe.encode_prompt( + prompt=prompt, prompt_2=prompt_2, prompt_3=prompt_3, prompt_4=prompt_4, + negative_prompt=neg_prompt, negative_prompt_2=neg_prompt_2, negative_prompt_3=neg_prompt_3, negative_prompt_4=neg_prompt_4, + device=device, num_images_per_prompt=1, + ) + return prompt_embeds, pooled_prompt_embeds, negative_prompt_embeds, negative_pooled_prompt_embeds if prompt != prompt_2: ps = [get_prompts_with_weights(pipe, p) for p in [prompt, prompt_2]] @@ -488,10 +529,6 @@ def get_weighted_text_embeddings(pipe, prompt: str = "", neg_prompt: str = "", c negatives.pop(0) negative_weights.pop(0) - if "Flux" in pipe.__class__.__name__: # clip is only used for the pooled embeds - prompt_embeds, pooled_prompt_embeds, _ = pipe.encode_prompt(prompt=prompt, prompt_2=prompt_2, device=device, num_images_per_prompt=1) - return prompt_embeds, pooled_prompt_embeds, None, None # no negative support - embedding_providers = prepare_embedding_providers(pipe, clip_skip) empty_embedding_providers = None if 'StableCascade' in pipe.__class__.__name__: @@ -593,8 +630,8 @@ def get_weighted_text_embeddings(pipe, prompt: str = "", neg_prompt: str = "", c def get_xhinker_text_embeddings(pipe, prompt: str = "", neg_prompt: str = "", clip_skip: int = None): is_sd3 = hasattr(pipe, 'text_encoder_3') - prompt, prompt_2, _prompt_3 = split_prompts(pipe, prompt, is_sd3) - neg_prompt, neg_prompt_2, _neg_prompt_3 = split_prompts(pipe, neg_prompt, is_sd3) + prompt, prompt_2, _prompt_3, _ = split_prompts(pipe, prompt, is_sd3) + neg_prompt, neg_prompt_2, _neg_prompt_3, _ = split_prompts(pipe, neg_prompt, is_sd3) try: prompt = pipe.maybe_convert_prompt(prompt, pipe.tokenizer) neg_prompt = pipe.maybe_convert_prompt(neg_prompt, pipe.tokenizer) diff --git a/modules/sd_hijack_te.py b/modules/sd_hijack_te.py index 498f0377d..d29a43dfd 100644 --- a/modules/sd_hijack_te.py +++ b/modules/sd_hijack_te.py @@ -15,6 +15,8 @@ def hijack_encode_prompt(*args, **kwargs): res = None t1 = time.time() timer.process.add('te', t1-t0) + if hasattr(shared.sd_model, "maybe_free_model_hooks"): + shared.sd_model.maybe_free_model_hooks() shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model) return res From 7a4848dada887cf059e91a50dc583baed32e0ff1 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Tue, 15 Apr 2025 02:16:18 +0300 Subject: [PATCH 2/9] Fix NNCF not applying for TE only quant --- modules/sd_models.py | 2 +- modules/sd_models_utils.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/sd_models.py b/modules/sd_models.py index e06e56272..25e0c7005 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -600,7 +600,7 @@ def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=No prompt_parser_diffusers.cache.clear() set_diffuser_options(sd_model, vae, op, offload=False) - if 'Model' in shared.opts.nncf_compress_weights and not ('Model' in shared.opts.cuda_compile and shared.opts.cuda_compile_backend == "openvino_fx"): + if shared.opts.nncf_compress_weights and not (shared.opts.cuda_compile and shared.opts.cuda_compile_backend == "openvino_fx"): sd_model = model_quant.nncf_compress_weights(sd_model) # run this before move model so it can be compressed in CPU if shared.opts.optimum_quanto_weights: sd_model = model_quant.optimum_quanto_weights(sd_model) # run this before move model so it can be compressed in CPU diff --git a/modules/sd_models_utils.py b/modules/sd_models_utils.py index ae50e5748..273d1b39d 100644 --- a/modules/sd_models_utils.py +++ b/modules/sd_models_utils.py @@ -197,6 +197,15 @@ def apply_function_to_model(sd_model, function, options, op=None): dtype=torch.float32 if devices.dtype != torch.bfloat16 else torch.bfloat16 ) sd_model.text_encoder_3 = function(sd_model.text_encoder_3, op="text_encoder_3", sd_model=sd_model) + if hasattr(sd_model, 'text_encoder_4') and hasattr(sd_model.text_encoder_4, 'config'): + if op == "nncf" and sd_model.text_encoder_4.__class__.__name__ in {"T5EncoderModel", "UMT5EncoderModel"}: + from modules.sd_hijack import NNCF_T5DenseGatedActDense # T5DenseGatedActDense uses fp32 + for i in range(len(sd_model.text_encoder_4.encoder.block)): + sd_model.text_encoder_4.encoder.block[i].layer[1].DenseReluDense = NNCF_T5DenseGatedActDense( + sd_model.text_encoder_4.encoder.block[i].layer[1].DenseReluDense, + dtype=torch.float32 if devices.dtype != torch.bfloat16 else torch.bfloat16 + ) + sd_model.text_encoder_4 = function(sd_model.text_encoder_4, op="text_encoder_4", sd_model=sd_model) if hasattr(sd_model, 'prior_pipe') and hasattr(sd_model.prior_pipe, 'text_encoder') and hasattr(sd_model.prior_pipe.text_encoder, 'config'): sd_model.prior_pipe.text_encoder = function(sd_model.prior_pipe.text_encoder, op="prior_pipe.text_encoder", sd_model=sd_model) if "VAE" in options: From 1541a19d89c0e2c259a4cc2d577fe6efab92d436 Mon Sep 17 00:00:00 2001 From: Disty0 Date: Tue, 15 Apr 2025 02:28:44 +0300 Subject: [PATCH 3/9] Offload the TE right away --- modules/sd_offload.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/modules/sd_offload.py b/modules/sd_offload.py index 6bf5bf775..a9273d38d 100644 --- a/modules/sd_offload.py +++ b/modules/sd_offload.py @@ -175,6 +175,28 @@ class OffloadHook(accelerate.hooks.ModelHook): return args, kwargs def post_forward(self, module, output): + if getattr(module, "do_offload", False) and shared.opts.te_hijack and module.device != devices.cpu: + used_gpu, used_ram = devices.torch_gc(fast=True) + perc_gpu = used_gpu / shared.gpu_memory + try: + module_size = self.model_size() + prev_gpu = used_gpu + do_offload = (perc_gpu > shared.opts.diffusers_offload_min_gpu_memory) + if do_offload: + module = module.to(devices.cpu) + used_gpu -= module_size + cls = module.__class__.__name__ + quant = getattr(module, "quantization_method", None) + debug_move(f'Offload: type=balanced op={"move post forward" if do_offload else "skip post forward"} gpu={prev_gpu:.3f}:{used_gpu:.3f} perc={perc_gpu:.2f} ram={used_ram:.3f} current={module.device} dtype={module.dtype} quant={quant} module={cls} size={module_size:.3f}') + except Exception as 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__}') return output def detach_hook(self, module): @@ -273,6 +295,7 @@ def apply_balanced_offload(sd_model=None, exclude=[]): if device_map and max_memory: module.balanced_offload_device_map = device_map module.balanced_offload_max_memory = max_memory + module.do_offload = bool("HiDreamImage" in sd_model.__class__.__name__ and module_name.startswith("text_encoder")) devices.torch_gc(fast=True, force=True, reason='offload') apply_balanced_offload_to_module(sd_model) From ee1a4c607d46e23a329c239478a48754566ba84a Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 15 Apr 2025 10:06:34 -0400 Subject: [PATCH 4/9] offload cleanup Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 4 ++++ installer.py | 2 +- modules/model_hidream.py | 10 +++++++--- modules/sd_models.py | 2 ++ modules/sd_offload.py | 30 +++++++++++++++++------------- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff351dd89..e2c4fe086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log for SD.Next +## Update for 2025-04-15 + +- **HiDream** optimized offloading, now works in 12GB VRAM / 26GB RAM + ## Update for 2025-04-14 - [CFG-Zero](https://github.com/WeichenFan/CFG-Zero-star) new guidance method optimized for flow-matching models diff --git a/installer.py b/installer.py index 005e81dcf..7d9336c5c 100644 --- a/installer.py +++ b/installer.py @@ -538,7 +538,7 @@ def check_diffusers(): t_start = time.time() if args.skip_all or args.skip_git or args.experimental: return - sha = 'a8f5134c113da402a93580ef7a021557e816c98d' # diffusers commit hash + sha = 'b6156aafe998eb57902efd3b8cce9a6fde35c1ea' # 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 '' diff --git a/modules/model_hidream.py b/modules/model_hidream.py index c9702e7cf..9f37d0f98 100644 --- a/modules/model_hidream.py +++ b/modules/model_hidream.py @@ -24,7 +24,7 @@ def load_hidream(checkpoint_info, diffusers_load_config={}): **quant_args, ) if shared.opts.diffusers_offload_mode != 'none': - transformer = transformer.to(devices.cpu) + sd_models.move_model(transformer, devices.cpu) 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}') @@ -36,7 +36,7 @@ def load_hidream(checkpoint_info, diffusers_load_config={}): **quant_args, ) if shared.opts.diffusers_offload_mode != 'none': - text_encoder_3 = text_encoder_3.to(devices.cpu) + sd_models.move_model(text_encoder_3, devices.cpu) load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='LLM', device_map=True) shared.log.debug(f'Load model: type=HiDream te4="{shared.opts.model_h1_llama_repo}" quant="{model_quant.get_quant_type(quant_args)}" args={load_args}') @@ -55,7 +55,7 @@ def load_hidream(checkpoint_info, diffusers_load_config={}): **load_args, ) if shared.opts.diffusers_offload_mode != 'none': - text_encoder_4 = text_encoder_4.to(devices.cpu) + sd_models.move_model(text_encoder_4, devices.cpu) 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}') @@ -69,6 +69,10 @@ def load_hidream(checkpoint_info, diffusers_load_config={}): **load_args, ) sd_hijack_te.init_hijack(pipe) + del text_encoder_3 + del text_encoder_4 + del tokenizer_4 + del transformer devices.torch_gc() return pipe diff --git a/modules/sd_models.py b/modules/sd_models.py index 25e0c7005..bed80bdb5 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -225,6 +225,8 @@ def move_model(model, device=None, force=False): pass # ignore model move if sequential offload is enabled elif 'Params4bit' in str(e0) or 'Params8bit' in str(e0): 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 else: raise e0 t1 = time.time() diff --git a/modules/sd_offload.py b/modules/sd_offload.py index a9273d38d..9352d4f38 100644 --- a/modules/sd_offload.py +++ b/modules/sd_offload.py @@ -9,8 +9,10 @@ from modules import shared, devices, errors, model_quant from modules.timer import process as process_timer -debug_move = log.trace if os.environ.get('SD_MOVE_DEBUG', None) is not None else lambda *args, **kwargs: None -should_offload = ['sc', 'sd3', 'f1', 'h1', 'hunyuandit', 'auraflow', 'omnigen', 'cogview4'] +debug = os.environ.get('SD_MOVE_DEBUG', None) is not None +debug_move = log.trace if debug else lambda *args, **kwargs: None +offload_warn = ['sc', 'sd3', 'f1', 'h1', 'hunyuandit', 'auraflow', 'omnigen', 'cogview4'] +offload_post = ['h1'] offload_hook_instance = None balanced_offload_exclude = ['OmniGenPipeline', 'CogView4Pipeline'] @@ -66,7 +68,7 @@ def set_diffuser_offload(sd_model, op:str='model', quiet:bool=False): if not (hasattr(sd_model, "has_accelerate") and sd_model.has_accelerate): sd_model.has_accelerate = False if shared.opts.diffusers_offload_mode == "none": - if shared.sd_model_type in should_offload or 'video' in shared.sd_model_type: + if shared.sd_model_type in offload_warn or 'video' in shared.sd_model_type: shared.log.warning(f'Setting {op}: offload={shared.opts.diffusers_offload_mode} type={shared.sd_model.__class__.__name__} large model') else: shared.log.quiet(quiet, f'Setting {op}: offload={shared.opts.diffusers_offload_mode} limit={shared.opts.cuda_mem_fraction}') @@ -175,19 +177,20 @@ class OffloadHook(accelerate.hooks.ModelHook): return args, kwargs def post_forward(self, module, output): - if getattr(module, "do_offload", False) and shared.opts.te_hijack and module.device != devices.cpu: + if getattr(module, "offload_post", False) and module.device != devices.cpu: used_gpu, used_ram = devices.torch_gc(fast=True) perc_gpu = used_gpu / shared.gpu_memory try: module_size = self.model_size() prev_gpu = used_gpu - do_offload = (perc_gpu > shared.opts.diffusers_offload_min_gpu_memory) - if do_offload: + offload_now = perc_gpu > shared.opts.diffusers_offload_min_gpu_memory + if offload_now: module = module.to(devices.cpu) used_gpu -= module_size - cls = module.__class__.__name__ - quant = getattr(module, "quantization_method", None) - debug_move(f'Offload: type=balanced op={"move post forward" if do_offload else "skip post forward"} gpu={prev_gpu:.3f}:{used_gpu:.3f} perc={perc_gpu:.2f} ram={used_ram:.3f} current={module.device} dtype={module.dtype} quant={quant} module={cls} size={module_size:.3f}') + if debug: + cls = module.__class__.__name__ + quant = getattr(module, "quantization_method", None) + debug_move(f'Offload: type=balanced op={"post" if offload_now else "skip"} gpu={prev_gpu:.3f}:{used_gpu:.3f} perc={perc_gpu:.2f} ram={used_ram:.3f} current={module.device} dtype={module.dtype} quant={quant} module={cls} size={module_size:.3f}') except Exception as e: if 'out of memory' in str(e): devices.torch_gc(fast=True, force=True, reason='oom') @@ -269,15 +272,16 @@ def apply_balanced_offload(sd_model=None, exclude=[]): perc_gpu = used_gpu / shared.gpu_memory try: prev_gpu = used_gpu - do_offload = (perc_gpu > shared.opts.diffusers_offload_min_gpu_memory) and (module.device != devices.cpu) - if do_offload: + offload_now = (perc_gpu > shared.opts.diffusers_offload_min_gpu_memory) and (module.device != devices.cpu) + if offload_now: module = module.to(devices.cpu) used_gpu -= module_size cls = module.__class__.__name__ quant = getattr(module, "quantization_method", None) if not cached: shared.log.debug(f'Model module={module_name} type={cls} dtype={module.dtype} quant={quant} params={offload_hook_instance.param_map[module_name]:.3f} size={offload_hook_instance.offload_map[module_name]:.3f}') - 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} quant={quant} module={cls} size={module_size:.3f}') + if debug: + debug_move(f'Offload: type=balanced op={"move" if offload_now else "skip"} gpu={prev_gpu:.3f}:{used_gpu:.3f} perc={perc_gpu:.2f} ram={used_ram:.3f} current={module.device} dtype={module.dtype} quant={quant} module={cls} size={module_size:.3f}') except Exception as e: if 'out of memory' in str(e): devices.torch_gc(fast=True, force=True, reason='oom') @@ -295,7 +299,7 @@ def apply_balanced_offload(sd_model=None, exclude=[]): if device_map and max_memory: module.balanced_offload_device_map = device_map module.balanced_offload_max_memory = max_memory - module.do_offload = bool("HiDreamImage" in sd_model.__class__.__name__ and module_name.startswith("text_encoder")) + module.offload_post = shared.sd_model_type in [offload_post] and shared.opts.te_hijack and module_name.startswith("text_encoder") devices.torch_gc(fast=True, force=True, reason='offload') apply_balanced_offload_to_module(sd_model) From 15f8e70e892be0b13cc4db832bb65b269a6c8d05 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 15 Apr 2025 14:39:24 -0400 Subject: [PATCH 5/9] add nunchaku prototype Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 7 +++- TODO.md | 3 ++ modules/mit_nunchaku.py | 63 ++++++++++++++++++++++++++++++ modules/model_flux.py | 24 ++++++++++-- modules/model_quant.py | 20 ++++++++++ modules/para_attention.py | 10 +++-- modules/prompt_parser_diffusers.py | 56 ++++++++++++++------------ modules/shared.py | 4 ++ wiki | 2 +- 9 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 modules/mit_nunchaku.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e2c4fe086..3b44c0ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ ## Update for 2025-04-15 -- **HiDream** optimized offloading, now works in 12GB VRAM / 26GB RAM +- [Nunchaku](https://github.com/mit-han-lab/nunchaku) inference engine with custom **SVDQuant** 4-bit execution + highly experimental and with limited support, but when it works, its magic: **Flux.1 at 5.90 it/s** *(not sec/it)*! + see [Nunchaku Wiki](https://github.com/vladmandic/sdnext/Nunchaku) for details +- **HiDream** optimized offloading and prompt-encode caching + it now works in 12GB VRAM / 26GB RAM! +- fix: NNCF for TE-only quant ## Update for 2025-04-14 diff --git a/TODO.md b/TODO.md index fb1aec1cf..bf27e236f 100644 --- a/TODO.md +++ b/TODO.md @@ -39,3 +39,6 @@ N/A - modules/lora/lora_extract.py:185:9: W0511: TODO: lora support pre-quantized flux - control: support scripts via api - modernui: monkey-patch for missing tabs.select event +- nunchaku: cache-dir for transformer and t5 loader +- nunchaku: batch support +- nunchaku: LoRA support diff --git a/modules/mit_nunchaku.py b/modules/mit_nunchaku.py new file mode 100644 index 000000000..395f3099c --- /dev/null +++ b/modules/mit_nunchaku.py @@ -0,0 +1,63 @@ +# MIT-Han-Lab Nunchaku: +# TODO nunchaku: cache-dir for transformer and t5 loader +# TODO nunchaku: batch support +# TODO nunchaku: LoRA support + +from installer import log, pip +from modules import devices + + +ver = '0.2.0' +ok = False + + +def check(): + global ok # pylint: disable=global-statement + if ok: + return True + try: + import nunchaku + import nunchaku.utils + log.info(f'Nunchaku: path={nunchaku.__path__} precision={nunchaku.utils.get_precision()}') + ok = True + return True + except Exception as e: + log.error(f'Nunchaku: {e}') + ok = False + return False + + +def install_nunchaku(): + if devices.backend is None: + return # too early + if not check(): + import sys + import platform + import importlib + import pkg_resources + import torch + python_ver = f'{sys.version_info.major}{sys.version_info.minor}' + if python_ver not in ['311', '312', '313']: + log.error(f'Nunchaku: python={sys.version_info} unsupported') + return + arch = platform.system().lower() + if arch not in ['linux', 'windows']: + log.error(f'Nunchaku: platform={arch} unsupported') + return + if devices.backend not in ['cuda']: + log.error(f'Nunchaku: backend={devices.backend} unsupported') + return + torch_ver = torch.__version__[:3] + if torch_ver not in ['2.5', '2.6', '2.7', '2.8']: + log.error(f'Nunchaku: torch={torch.__version__} unsupported') + suffix = 'x86_64' if arch == 'linux' else 'win_amd64' + 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' + 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: url={url}') + pip(cmd, ignore=False, uv=False) + importlib.reload(pkg_resources) + if not check(): + log.error('Nunchaku: install failed') + return False diff --git a/modules/model_flux.py b/modules/model_flux.py index 5c0395cf9..25de64c80 100644 --- a/modules/model_flux.py +++ b/modules/model_flux.py @@ -109,11 +109,25 @@ def load_flux_bnb(checkpoint_info, diffusers_load_config): # pylint: disable=unu def load_quants(kwargs, repo_id, cache_dir, allow_quant): try: - if 'transformer' not in kwargs and (('Model' in shared.opts.bnb_quantization or 'Model' in shared.opts.torchao_quantization or 'Model' in shared.opts.quanto_quantization) or ('Transformer' in shared.opts.bnb_quantization or 'Transformer' in shared.opts.torchao_quantization or 'Transformer' in shared.opts.quanto_quantization)): + if 'transformer' not in kwargs and model_quant.check_nunchaku('Transformer'): + import nunchaku + nunchaku_precision = nunchaku.utils.get_precision() + nunchaku_repo = f"mit-han-lab/svdq-{nunchaku_precision}-flux.1-dev" if 'dev' in repo_id else f"mit-han-lab/svdq-{nunchaku_precision}-flux.1-schnell" + shared.log.debug(f'Load module: quant=Nunchaku module=transformer repo="{nunchaku_repo}" precision={nunchaku_precision} attention={shared.opts.nunchaku_attention}') + kwargs['transformer'] = nunchaku.NunchakuFluxTransformer2dModel.from_pretrained(nunchaku_repo, torch_dtype=devices.dtype) + if shared.opts.nunchaku_attention: + kwargs['transformer'].set_attention_impl("nunchaku-fp16") + elif 'transformer' not in kwargs and model_quant.check_quant('Transformer'): quant_args = model_quant.create_config(allow=allow_quant, module='Transformer') if quant_args: kwargs['transformer'] = diffusers.FluxTransformer2DModel.from_pretrained(repo_id, subfolder="transformer", cache_dir=cache_dir, torch_dtype=devices.dtype, **quant_args) - if 'text_encoder_2' not in kwargs and ('TE' in shared.opts.bnb_quantization or 'TE' in shared.opts.torchao_quantization or 'TE' in shared.opts.quanto_quantization): + 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/svdq-flux.1-t5' + 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) + elif 'text_encoder_2' not in kwargs and model_quant.check_quant('TE'): quant_args = model_quant.create_config(allow=allow_quant, module='TE') if quant_args: kwargs['text_encoder_2'] = transformers.T5EncoderModel.from_pretrained(repo_id, subfolder="text_encoder_2", cache_dir=cache_dir, torch_dtype=devices.dtype, **quant_args) @@ -198,7 +212,7 @@ def load_flux(checkpoint_info, diffusers_load_config): # triggered by opts.sd_ch 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 + 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': @@ -310,6 +324,10 @@ def load_flux(checkpoint_info, diffusers_load_config): # triggered by opts.sd_ch 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('Transformer'): + 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 diff --git a/modules/model_quant.py b/modules/model_quant.py index a32a8e353..99d5d6c9b 100644 --- a/modules/model_quant.py +++ b/modules/model_quant.py @@ -100,6 +100,26 @@ def create_quanto_config(kwargs = None, allow_quanto: bool = True, module: str = return kwargs +def check_quant(module: str = ''): + from modules import shared + if 'Model' in shared.opts.bnb_quantization or 'Model' in shared.opts.torchao_quantization or 'Model' in shared.opts.quanto_quantization: + return True + if module in shared.opts.bnb_quantization or module in shared.opts.torchao_quantization or module in shared.opts.quanto_quantization: + return True + return False + + +def check_nunchaku(module: str = ''): + from modules import shared + if 'Model' not in shared.opts.nunchaku_quantization and module not in shared.opts.nunchaku_quantization: + return False + from modules import mit_nunchaku + mit_nunchaku.install_nunchaku() + if not mit_nunchaku.ok: + return False + return True + + def create_config(kwargs = None, allow: bool = True, module: str = 'Model'): if kwargs is None: kwargs = {} diff --git a/modules/para_attention.py b/modules/para_attention.py index f5c6e8635..8c00c303d 100644 --- a/modules/para_attention.py +++ b/modules/para_attention.py @@ -12,9 +12,13 @@ def apply_first_block_cache(): from installer import install install('para_attn') try: - from para_attn.first_block_cache import diffusers_adapters - diffusers_adapters.apply_cache_on_pipe(shared.sd_model, residual_diff_threshold=shared.opts.para_diff_threshold) - shared.log.info(f'Transformers cache: type=paraattn rdt={shared.opts.para_diff_threshold} cls={shared.sd_model.__class__.__name__}') + if 'Nunchaku' in shared.sd_model.transformer.__class__.__name__: + from nunchaku.caching.diffusers_adapters import apply_cache_on_pipe + shared.log.info(f'Transformers cache: type=nunchaku rdt={shared.opts.para_diff_threshold} cls={shared.sd_model.transformer.__class__.__name__}') + else: + from para_attn.first_block_cache.diffusers_adapters import apply_cache_on_pipe + shared.log.info(f'Transformers cache: type=paraattn rdt={shared.opts.para_diff_threshold} cls={shared.sd_model.transformer.__class__.__name__}') + apply_cache_on_pipe(shared.sd_model, residual_diff_threshold=shared.opts.para_diff_threshold) except Exception as e: shared.log.error(f'Transformers cache: type=paraattn {e}') return diff --git a/modules/prompt_parser_diffusers.py b/modules/prompt_parser_diffusers.py index 5267147c0..14c0ed219 100644 --- a/modules/prompt_parser_diffusers.py +++ b/modules/prompt_parser_diffusers.py @@ -191,32 +191,36 @@ class PromptEmbedder: def __call__(self, key, step=0): batch = getattr(self, key) res = [] - if isinstance(batch[0][0], list) and len(batch[0][0]) == 2 and isinstance(batch[0][0][1], torch.Tensor) and batch[0][0][1].shape[0] == 32: - # hidream uses a list of t5 + llama prompt embeds: [t5_embeds, llama_embeds] - # t5_embeds shape: [batch_size, seq_len, dim] - # llama_embeds shape: [number_of_hidden_states, batch_size, seq_len, dim] - res2 = [] - for i in range(self.batchsize): - if len(batch[i]) == 0: # if asking for a null key, ie pooled on SD1.5 - return None - try: - res.append(batch[i][step][0]) - res2.append(batch[i][step][1]) - except IndexError: - # if not scheduled, return default - res.append(batch[i][0][0]) - res2.append(batch[i][0][1]) - res = [torch.cat(res, dim=0), torch.cat(res2, dim=1)] - return res - else: - for i in range(self.batchsize): - if len(batch[i]) == 0: # if asking for a null key, ie pooled on SD1.5 - return None - try: - res.append(batch[i][step]) - except IndexError: - res.append(batch[i][0]) # if not scheduled, return default - return torch.cat(res) + try: + if isinstance(batch[0][0], list) and len(batch[0][0]) == 2 and isinstance(batch[0][0][1], torch.Tensor) and batch[0][0][1].shape[0] == 32: + # hidream uses a list of t5 + llama prompt embeds: [t5_embeds, llama_embeds] + # t5_embeds shape: [batch_size, seq_len, dim] + # llama_embeds shape: [number_of_hidden_states, batch_size, seq_len, dim] + res2 = [] + for i in range(self.batchsize): + if len(batch[i]) == 0: # if asking for a null key, ie pooled on SD1.5 + return None + try: + res.append(batch[i][step][0]) + res2.append(batch[i][step][1]) + except IndexError: + # if not scheduled, return default + res.append(batch[i][0][0]) + res2.append(batch[i][0][1]) + res = [torch.cat(res, dim=0), torch.cat(res2, dim=1)] + return res + else: + for i in range(self.batchsize): + if len(batch[i]) == 0: # if asking for a null key, ie pooled on SD1.5 + return None + try: + res.append(batch[i][step]) + except IndexError: + res.append(batch[i][0]) # if not scheduled, return default + return torch.cat(res) + except Exception: + pass + return None def compel_hijack(self, token_ids: torch.Tensor, attention_mask: typing.Optional[torch.Tensor] = None) -> torch.Tensor: diff --git a/modules/shared.py b/modules/shared.py index 8c51ac40d..601e5ebfa 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -548,6 +548,10 @@ options_templates.update(options_section(('quantization', "Quantization Settings "layerwise_quantization": OptionInfo([], "Layerwise casting enabled", gr.CheckboxGroup, {"choices": ["Model", "Transformer", "TE"], "visible": native}), "layerwise_quantization_storage": OptionInfo("float8_e4m3fn", "Layerwise casting storage", gr.Dropdown, {"choices": ["float8_e4m3fn", "float8_e5m2"], "visible": native}), "layerwise_quantization_nonblocking": OptionInfo(False, "Layerwise non-blocking operations", gr.Checkbox, {"visible": native}), + + "nunchaku_sep": OptionInfo("

Nunchaku Engine

", "", gr.HTML), + "nunchaku_quantization": OptionInfo([], "SVDQuant enabled", gr.CheckboxGroup, {"choices": ["Model", "Transformer", "VAE", "TE", "Video", "LLM", "ControlNet"], "visible": native}), + "nunchaku_attention": OptionInfo(False, "Nunchaku attention", gr.Checkbox, {"visible": native}), })) options_templates.update(options_section(('advanced', "Pipeline Modifiers"), { diff --git a/wiki b/wiki index 40ac3ec88..a985acf8c 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 40ac3ec884aba0146507eb2e3217ae1fef399170 +Subproject commit a985acf8ca4f8e20c7438f749b4074d37c9df949 From 59a8a5cdb03f2e91ce2ef04a3bf019f5e7316af5 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 15 Apr 2025 14:42:29 -0400 Subject: [PATCH 6/9] update changelog Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b44c0ab3..3f0608757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - [Nunchaku](https://github.com/mit-han-lab/nunchaku) inference engine with custom **SVDQuant** 4-bit execution highly experimental and with limited support, but when it works, its magic: **Flux.1 at 5.90 it/s** *(not sec/it)*! - see [Nunchaku Wiki](https://github.com/vladmandic/sdnext/Nunchaku) for details + see [Nunchaku Wiki](https://github.com/vladmandic/sdnext/wiki/Nunchaku) for details - **HiDream** optimized offloading and prompt-encode caching it now works in 12GB VRAM / 26GB RAM! - fix: NNCF for TE-only quant From 87defaac61646d162df93ee7f261fcdda7cb3cd8 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 16 Apr 2025 07:29:41 -0400 Subject: [PATCH 7/9] add lcm-flowmatch sampler and fix hunyuanvideo-i2v Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 53 +++++++++++++++++--------------- installer.py | 2 +- modules/sd_samplers_diffusers.py | 4 ++- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f0608757..2832fabab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,34 @@ # Change Log for SD.Next -## Update for 2025-04-15 +## Update for 2025-04-16 -- [Nunchaku](https://github.com/mit-han-lab/nunchaku) inference engine with custom **SVDQuant** 4-bit execution - highly experimental and with limited support, but when it works, its magic: **Flux.1 at 5.90 it/s** *(not sec/it)*! - see [Nunchaku Wiki](https://github.com/vladmandic/sdnext/wiki/Nunchaku) for details -- **HiDream** optimized offloading and prompt-encode caching - it now works in 12GB VRAM / 26GB RAM! -- fix: NNCF for TE-only quant - -## Update for 2025-04-14 - -- [CFG-Zero](https://github.com/WeichenFan/CFG-Zero-star) new guidance method optimized for flow-matching models - implemented for **FLUX.1, HiDream-I1, SD3.x, CogView4, HunyuanVideo, WanAI** - enable and configure in *settings -> pipeline modifiers -> cfg zero* - experiment with CFGZero support in XYZ-grid -- add **UniPC FlowMatch** scheduler -- **HiDream** add HF gated access auth check -- clenup **CogView3** and **CogView4** model loader -- add explicit offload after encode prompt - configure in *settings -> text encoder -> offload* -- networks: set which networks to skip when scanning civitai - in *settings -> networks -> network scan* - comma-separate list of regex patterns to skip -- ui display reference models with subdued color -- xyz grid support bool -- fix debug logging +- **Features** + - [Nunchaku](https://github.com/mit-han-lab/nunchaku) inference engine with custom **SVDQuant** 4-bit execution + highly experimental and with limited support, but when it works, its magic: **Flux.1 at 5.90 it/s** *(not sec/it)*! + see [Nunchaku Wiki](https://github.com/vladmandic/sdnext/wiki/Nunchaku) for details + - [CFG-Zero](https://github.com/WeichenFan/CFG-Zero-star) new guidance method optimized for flow-matching models + implemented for **FLUX.1, HiDream-I1, SD3.x, CogView4, HunyuanVideo, WanAI** + enable and configure in *settings -> pipeline modifiers -> cfg zero* + experiment with CFGZero support in XYZ-grid +- **Optimizations** + - **HiDream** optimized offloading and prompt-encode caching + it now works in 12GB VRAM / 26GB RAM! + - **CogView3** and **CogView4** model loader optimizations + - add explicit offload after encode prompt + configure in *settings -> text encoder -> offload* +- **Other** + - **HiDream** add HF gated access auth check + - add **UniPC FlowMatch** scheduler + - add **LCM FlowMatch** scheduler + - networks: set which networks to skip when scanning civitai + in *settings -> networks -> network scan* + comma-separate list of regex patterns to skip + - ui display reference models with subdued color + - xyz grid support bool +- **Fixes** + - NNCF for TE-only quant + - HunyuanVideo-I2V with latest transformers + - debug logging ## Update for 2025-04-12 diff --git a/installer.py b/installer.py index 7d9336c5c..a5f8c9479 100644 --- a/installer.py +++ b/installer.py @@ -538,7 +538,7 @@ def check_diffusers(): t_start = time.time() if args.skip_all or args.skip_git or args.experimental: return - sha = 'b6156aafe998eb57902efd3b8cce9a6fde35c1ea' # diffusers commit hash + sha = 'ce1063acfa0cbc2168a7e9dddd4282ab8013b810' # 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 '' diff --git a/modules/sd_samplers_diffusers.py b/modules/sd_samplers_diffusers.py index 97a6a7b26..f5bde64bd 100644 --- a/modules/sd_samplers_diffusers.py +++ b/modules/sd_samplers_diffusers.py @@ -30,6 +30,7 @@ try: HeunDiscreteScheduler, FlowMatchHeunDiscreteScheduler, LCMScheduler, + FlowMatchLCMScheduler, PNDMScheduler, IPNDMScheduler, DDPMScheduler, @@ -103,12 +104,12 @@ config = { 'Heun': { 'use_beta_sigmas': False, 'use_karras_sigmas': False, 'use_exponential_sigmas': False, 'timestep_spacing': 'linspace' }, 'Heun FlowMatch': { 'timestep_spacing': "linspace", 'shift': 1 }, + 'LCM FlowMatch': { 'beta_start': 0.00085, 'beta_end': 0.012, 'beta_schedule': "scaled_linear", 'set_alpha_to_one': True, 'rescale_betas_zero_snr': False, 'thresholding': False, 'timestep_spacing': 'linspace' }, 'DEIS': { 'solver_order': 2, 'thresholding': False, 'sample_max_value': 1.0, 'algorithm_type': "deis", 'solver_type': "logrho", 'lower_order_final': True, 'timestep_spacing': 'linspace', 'use_karras_sigmas': False, 'use_exponential_sigmas': False, 'use_flow_sigmas': False, 'use_beta_sigmas': False }, 'SA Solver': {'predictor_order': 2, 'corrector_order': 2, 'thresholding': False, 'lower_order_final': True, 'use_karras_sigmas': False, 'use_flow_sigmas': False, 'use_exponential_sigmas': False, 'use_beta_sigmas': False, 'timestep_spacing': 'linspace'}, 'DC Solver': { 'beta_start': 0.0001, 'beta_end': 0.02, 'solver_order': 2, 'prediction_type': "epsilon", 'thresholding': False, 'solver_type': 'bh2', 'lower_order_final': True, 'dc_order': 2, 'disable_corrector': [0] }, 'VDM Solver': { 'clip_sample_range': 2.0, }, - 'LCM': { 'beta_start': 0.00085, 'beta_end': 0.012, 'beta_schedule': "scaled_linear", 'set_alpha_to_one': True, 'rescale_betas_zero_snr': False, 'thresholding': False, 'timestep_spacing': 'linspace' }, 'TCD': { 'set_alpha_to_one': True, 'rescale_betas_zero_snr': False, 'beta_schedule': 'scaled_linear' }, 'TDD': { }, 'PeRFlow': { 'prediction_type': 'ddim_eps' }, @@ -179,6 +180,7 @@ samplers_data_diffusers = [ SamplerData('CMSI', lambda model: DiffusionSampler('CMSI', CMStochasticIterativeScheduler, model), [], {}), SamplerData('LCM', lambda model: DiffusionSampler('LCM', LCMScheduler, model), [], {}), + SamplerData('LCM FlowMatch', lambda model: DiffusionSampler('LCM FlowMatch', FlowMatchLCMScheduler, model), [], {}), SamplerData('TCD', lambda model: DiffusionSampler('TCD', TCDScheduler, model), [], {}), SamplerData('TDD', lambda model: DiffusionSampler('TDD', TDDScheduler, model), [], {}), SamplerData('PeRFlow', lambda model: DiffusionSampler('PeRFlow', PeRFlowScheduler, model), [], {}), From 38a3b625533007210327ca8ba0dd07bfa9b0e53b Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 16 Apr 2025 09:11:43 -0400 Subject: [PATCH 8/9] modularize hidream loader Signed-off-by: Vladimir Mandic --- modules/ggml/__init__.py | 3 +- modules/lora/lora_overrides.py | 1 + modules/model_flux.py | 1 - modules/model_hidream.py | 80 ++++++++++++++++++++++++---------- modules/sd_unet.py | 8 ++-- modules/shared_items.py | 4 +- 6 files changed, 65 insertions(+), 32 deletions(-) diff --git a/modules/ggml/__init__.py b/modules/ggml/__init__.py index 44721d846..36cb322bf 100644 --- a/modules/ggml/__init__.py +++ b/modules/ggml/__init__.py @@ -47,7 +47,8 @@ def load_gguf_state_dict(path: str, compute_dtype: torch.dtype) -> dict: def load_gguf(path, cls, compute_dtype: torch.dtype): _gguf = install_gguf() - module = cls.from_single_file( + loader = cls.from_single_file if hasattr(cls, 'from_single_file') else cls.from_pretrained + module = loader( path, quantization_config = diffusers.GGUFQuantizationConfig(compute_dtype=compute_dtype), torch_dtype=compute_dtype, diff --git a/modules/lora/lora_overrides.py b/modules/lora/lora_overrides.py index 22d251c47..8cc9c1d17 100644 --- a/modules/lora/lora_overrides.py +++ b/modules/lora/lora_overrides.py @@ -28,6 +28,7 @@ force_diffusers = [ # forced always force_models = [ # forced always # 'sd3', 'sc', + 'h1', 'kandinsky', 'hunyuandit', 'auraflow', diff --git a/modules/model_flux.py b/modules/model_flux.py index 25de64c80..3b123cc20 100644 --- a/modules/model_flux.py +++ b/modules/model_flux.py @@ -150,7 +150,6 @@ def load_transformer(file_path): # triggered by opts.sd_unet change 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(): - # _transformer, _text_encoder_2 = load_flux_gguf(file_path) from modules import ggml _transformer = ggml.load_gguf(file_path, cls=diffusers.FluxTransformer2DModel, compute_dtype=devices.dtype) if _transformer is not None: diff --git a/modules/model_hidream.py b/modules/model_hidream.py index 9f37d0f98..358ac1011 100644 --- a/modules/model_hidream.py +++ b/modules/model_hidream.py @@ -1,31 +1,46 @@ +import os import transformers import diffusers from modules import shared, devices, sd_models, model_quant, modelloader, sd_hijack_te -def load_hidream(checkpoint_info, diffusers_load_config={}): - login = modelloader.hf_login() - repo_id = sd_models.path_to_repo(checkpoint_info.name) - - from huggingface_hub import auth_check - try: - auth_check(shared.opts.model_h1_llama_repo) - except Exception as e: - shared.log.error(f'Load model: type=HiDream te4="{shared.opts.model_h1_llama_repo}" login={login} {e}') - return False - +def load_transformer(repo_id, diffusers_load_config={}): load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='Transformer', device_map=True) - 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': - sd_models.move_model(transformer, devices.cpu) + 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}" 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) + # elif model_quant.check_nunchaku('Transformer'): + # 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={}): 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( @@ -35,7 +50,7 @@ def load_hidream(checkpoint_info, diffusers_load_config={}): **load_args, **quant_args, ) - if shared.opts.diffusers_offload_mode != 'none': + if shared.opts.diffusers_offload_mode != 'none' and text_encoder_3 is not None: sd_models.move_model(text_encoder_3, devices.cpu) load_args, quant_args = model_quant.get_dit_args(diffusers_load_config, module='LLM', device_map=True) @@ -54,17 +69,34 @@ def load_hidream(checkpoint_info, diffusers_load_config={}): cache_dir=shared.opts.hfcache_dir, **load_args, ) - if shared.opts.diffusers_offload_mode != 'none': + 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 + + +def load_hidream(checkpoint_info, diffusers_load_config={}): + login = modelloader.hf_login() + repo_id = sd_models.path_to_repo(checkpoint_info.name) + + from huggingface_hub import auth_check + try: + auth_check(shared.opts.model_h1_llama_repo) + except Exception as e: + shared.log.error(f'Load model: type=HiDream te4="{shared.opts.model_h1_llama_repo}" login={login} {e}') + return False + + 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, 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}') + pipe = diffusers.HiDreamImagePipeline.from_pretrained( repo_id, + transformer=transformer, text_encoder_3=text_encoder_3, text_encoder_4=text_encoder_4, tokenizer_4=tokenizer_4, - transformer=transformer, cache_dir=shared.opts.diffusers_dir, **load_args, ) diff --git a/modules/sd_unet.py b/modules/sd_unet.py index cfba470a1..f5f24677c 100644 --- a/modules/sd_unet.py +++ b/modules/sd_unet.py @@ -10,10 +10,10 @@ debug = os.environ.get('SD_LOAD_DEBUG', None) is not None def load_unet(model): global loaded_unet # pylint: disable=global-statement - if shared.opts.sd_unet == 'Default': + if shared.opts.sd_unet == 'Default' or shared.opts.sd_unet == 'None': return if shared.opts.sd_unet not in list(unet_dict): - shared.log.error(f'UNet model not found: {shared.opts.sd_unet}') + shared.log.error(f'Load module: type=UNet not found: {shared.opts.sd_unet}') return config_file = os.path.splitext(unet_dict[shared.opts.sd_unet])[0] + '.json' if os.path.exists(config_file): @@ -34,7 +34,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 "Flux" in model.__class__.__name__ or "StableDiffusion3" in model.__class__.__name__: + elif "Flux" in model.__class__.__name__ or "StableDiffusion3" in model.__class__.__name__ or "HiDream" in model.__class__.__name__: 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 """ @@ -51,7 +51,7 @@ def load_unet(model): """ else: if not hasattr(model, 'unet') or model.unet is None: - shared.log.error('UNet not found in current model') + shared.log.error('Load module: type=UNET not found in current model') return shared.log.info(f'Load module: type=UNet name="{shared.opts.sd_unet}" file="{unet_dict[shared.opts.sd_unet]}" config="{config_file}"') from diffusers import UNet2DConditionModel diff --git a/modules/shared_items.py b/modules/shared_items.py index ac66e44d2..ac7a99fba 100644 --- a/modules/shared_items.py +++ b/modules/shared_items.py @@ -69,7 +69,7 @@ def refresh_vae_list(): def sd_unet_items(): import modules.sd_unet - return ["None"] + list(modules.sd_unet.unet_dict) + return ['Default'] + list(modules.sd_unet.unet_dict) def refresh_unet_list(): @@ -79,7 +79,7 @@ def refresh_unet_list(): def sd_te_items(): import modules.model_te - predefined = ['None'] + predefined = ['Default'] return predefined + list(modules.model_te.te_dict) From ebe62a30a692307969991a2549514f6558a7ebd6 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 16 Apr 2025 09:24:36 -0400 Subject: [PATCH 9/9] lint fixes and update requirements Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 6 +++--- modules/mit_nunchaku.py | 7 ++++--- requirements.txt | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2832fabab..81298a455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,9 +26,9 @@ - ui display reference models with subdued color - xyz grid support bool - **Fixes** - - NNCF for TE-only quant - - HunyuanVideo-I2V with latest transformers - - debug logging + - NNCF with TE-only quant + - **HunyuanVideo-I2V** with latest transformers + - trace logging ## Update for 2025-04-12 diff --git a/modules/mit_nunchaku.py b/modules/mit_nunchaku.py index 395f3099c..06e084e6e 100644 --- a/modules/mit_nunchaku.py +++ b/modules/mit_nunchaku.py @@ -29,7 +29,7 @@ def check(): def install_nunchaku(): if devices.backend is None: - return # too early + return False # too early if not check(): import sys import platform @@ -39,14 +39,14 @@ def install_nunchaku(): python_ver = f'{sys.version_info.major}{sys.version_info.minor}' if python_ver not in ['311', '312', '313']: log.error(f'Nunchaku: python={sys.version_info} unsupported') - return + return False arch = platform.system().lower() if arch not in ['linux', 'windows']: log.error(f'Nunchaku: platform={arch} unsupported') return if devices.backend not in ['cuda']: log.error(f'Nunchaku: backend={devices.backend} unsupported') - return + return False torch_ver = torch.__version__[:3] if torch_ver not in ['2.5', '2.6', '2.7', '2.8']: log.error(f'Nunchaku: torch={torch.__version__} unsupported') @@ -61,3 +61,4 @@ def install_nunchaku(): if not check(): log.error('Nunchaku: install failed') return False + return True diff --git a/requirements.txt b/requirements.txt index 0bc763167..41eca69d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -45,14 +45,14 @@ accelerate==1.6.0 opencv-contrib-python-headless==4.9.0.80 einops==0.4.1 gradio==3.43.2 -huggingface_hub==0.30.1 +huggingface_hub==0.30.2 numexpr==2.8.8 numpy==1.26.4 numba==0.59.1 protobuf==4.25.3 pytorch_lightning==1.9.4 tokenizers==0.21.1 -transformers==4.51.1 +transformers==4.51.3 urllib3==1.26.19 Pillow==10.4.0 timm==0.9.16