From 9375ced5cf2494cbc7be3d915445a07e12b4e5df Mon Sep 17 00:00:00 2001 From: silveroxides Date: Sat, 11 Jul 2026 11:21:40 +0200 Subject: [PATCH] Refactor loading strategies in mergers and LoRA resize nodes. --- docs/merger_2_model_modes.md | 9 ++++ docs/merger_3_model_modes.md | 9 ++++ nodes/lora_resize.py | 74 ++++++++++++++++++++++--------- nodes/merger.py | 84 ++++++++++++++++++++++++++---------- 4 files changed, 133 insertions(+), 43 deletions(-) diff --git a/docs/merger_2_model_modes.md b/docs/merger_2_model_modes.md index 7532caf..403ce04 100644 --- a/docs/merger_2_model_modes.md +++ b/docs/merger_2_model_modes.md @@ -121,6 +121,15 @@ When merging models with different layer structures (e.g., fine-tuned models wit --- +## Dtype Preservation & Override + +By default, the merger preserves the highest precision `dtype` for each key found across all active source models (Model A and Model B) and the requested `save_dtype`. + +- **Preserve Higher Precision**: If a key is `float32` in any of the source models, but you requested `save_dtype="bf16"` or `"fp16"`, that key will be saved as `float32` anyway. This prevents degrading critical high-precision layers (such as normalization layers or layers with restored outliers). +- **Explicit Override**: If you prefer to force the entire model to be saved strictly as the selected `save_dtype` (e.g. for complete downcasting to `bf16`), toggle `override_dtype` to `True`. + +--- + ## Layer Filtering (Regex Patterns) Use regex patterns to control which layers are merged: diff --git a/docs/merger_3_model_modes.md b/docs/merger_3_model_modes.md index 5390066..658d0dd 100644 --- a/docs/merger_3_model_modes.md +++ b/docs/merger_3_model_modes.md @@ -67,6 +67,15 @@ When merging models with different layer structures, the `mismatch_mode` paramet --- +## Dtype Preservation & Override + +By default, the merger preserves the highest precision `dtype` for each key found across all active source models (Model A, B, and C) and the requested `save_dtype`. + +- **Preserve Higher Precision**: If a key is `float32` in any of the active source models, but you requested `save_dtype="bf16"` or `"fp16"`, that key will be saved as `float32` anyway. This prevents degrading critical high-precision layers (such as normalization layers or layers with restored outliers). +- **Explicit Override**: If you prefer to force the entire model to be saved strictly as the selected `save_dtype` (e.g. for complete downcasting to `bf16`), toggle `override_dtype` to `True`. + +--- + ## Layer Filtering (Regex Patterns) Use regex patterns to control which layers are merged: diff --git a/nodes/lora_resize.py b/nodes/lora_resize.py index 735a284..e77ba21 100644 --- a/nodes/lora_resize.py +++ b/nodes/lora_resize.py @@ -377,6 +377,8 @@ def resize_lora_file( output_filename: str, verbose: bool = True, svd_niter: int = 2, + lazy_load: bool = True, + force_clear_cache: bool = False, ) -> str: """ Resize a LoRA file to a new rank. @@ -391,6 +393,8 @@ def resize_lora_file( output_filename: Output filename (without extension) verbose: Print progress info svd_niter: Power iterations for SVD accuracy + lazy_load: Low memory mode: load tensors from disk on demand + force_clear_cache: Clear CUDA cache after each layer Returns: Path to saved resized LoRA @@ -399,7 +403,7 @@ def resize_lora_file( lora_size_gb = estimate_model_size(lora_path) prepare_for_large_operation(lora_size_gb * 2, torch.device(device)) - handler = MemoryEfficientSafeOpen(lora_path) + handler = MemoryEfficientSafeOpen(lora_path, low_memory=lazy_load) try: metadata = handler.metadata().copy() @@ -484,10 +488,11 @@ def resize_lora_file( writer.write_dict(block_sd) del result - import gc - gc.collect() - if torch.cuda.is_available(): - torch.cuda.empty_cache() + if force_clear_cache: + import gc + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() pbar.update(1) finally: @@ -531,17 +536,22 @@ class LoRAResizeFixed(io.ComfyNode): io.String.Input("output_filename", default="resized_lora"), io.Combo.Input("save_dtype", options=["fp16", "bf16", "fp32"], default="fp16"), io.Combo.Input("device", options=["cuda", "cpu"], default="cuda"), + io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), + io.Boolean.Input("force_clear_cache", default=False, tooltip="Clear CUDA cache after each layer (slower but saves VRAM)"), ], outputs=[io.String.Output(display_name="output_path")], is_output_node=True, ) @classmethod - def execute(cls, lora_name, new_rank, svd_niter, output_filename, save_dtype, device) -> io.NodeOutput: + def execute(cls, lora_name, new_rank, svd_niter, output_filename, save_dtype, device, lazy_load, force_clear_cache) -> io.NodeOutput: lora_path = folder_paths.get_full_path_or_raise("loras", lora_name) dtype = {"fp16": torch.float16, "bf16": torch.bfloat16, "fp32": torch.float32}[save_dtype] - path = resize_lora_file(lora_path, new_rank, None, None, device, dtype, output_filename, svd_niter=svd_niter) + path = resize_lora_file( + lora_path, new_rank, None, None, device, dtype, output_filename, + svd_niter=svd_niter, lazy_load=lazy_load, force_clear_cache=force_clear_cache + ) return io.NodeOutput(path) @@ -565,17 +575,22 @@ class LoRAResizeRatio(io.ComfyNode): io.String.Input("output_filename", default="resized_lora_ratio"), io.Combo.Input("save_dtype", options=["fp16", "bf16", "fp32"], default="fp16"), io.Combo.Input("device", options=["cuda", "cpu"], default="cuda"), + io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), + io.Boolean.Input("force_clear_cache", default=False, tooltip="Clear CUDA cache after each layer (slower but saves VRAM)"), ], outputs=[io.String.Output(display_name="output_path")], is_output_node=True, ) @classmethod - def execute(cls, lora_name, max_rank, ratio, output_filename, save_dtype, device) -> io.NodeOutput: + def execute(cls, lora_name, max_rank, ratio, output_filename, save_dtype, device, lazy_load, force_clear_cache) -> io.NodeOutput: lora_path = folder_paths.get_full_path_or_raise("loras", lora_name) dtype = {"fp16": torch.float16, "bf16": torch.bfloat16, "fp32": torch.float32}[save_dtype] - path = resize_lora_file(lora_path, max_rank, "sv_ratio", ratio, device, dtype, output_filename) + path = resize_lora_file( + lora_path, max_rank, "sv_ratio", ratio, device, dtype, output_filename, + lazy_load=lazy_load, force_clear_cache=force_clear_cache + ) return io.NodeOutput(path) @@ -599,17 +614,22 @@ class LoRAResizeFrobenius(io.ComfyNode): io.String.Input("output_filename", default="resized_lora_fro"), io.Combo.Input("save_dtype", options=["fp16", "bf16", "fp32"], default="fp16"), io.Combo.Input("device", options=["cuda", "cpu"], default="cuda"), + io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), + io.Boolean.Input("force_clear_cache", default=False, tooltip="Clear CUDA cache after each layer (slower but saves VRAM)"), ], outputs=[io.String.Output(display_name="output_path")], is_output_node=True, ) @classmethod - def execute(cls, lora_name, max_rank, target, output_filename, save_dtype, device) -> io.NodeOutput: + def execute(cls, lora_name, max_rank, target, output_filename, save_dtype, device, lazy_load, force_clear_cache) -> io.NodeOutput: lora_path = folder_paths.get_full_path_or_raise("loras", lora_name) dtype = {"fp16": torch.float16, "bf16": torch.bfloat16, "fp32": torch.float32}[save_dtype] - path = resize_lora_file(lora_path, max_rank, "sv_fro", target, device, dtype, output_filename) + path = resize_lora_file( + lora_path, max_rank, "sv_fro", target, device, dtype, output_filename, + lazy_load=lazy_load, force_clear_cache=force_clear_cache + ) return io.NodeOutput(path) @@ -633,17 +653,22 @@ class LoRAResizeCumulative(io.ComfyNode): io.String.Input("output_filename", default="resized_lora_cumulative"), io.Combo.Input("save_dtype", options=["fp16", "bf16", "fp32"], default="fp16"), io.Combo.Input("device", options=["cuda", "cpu"], default="cuda"), + io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), + io.Boolean.Input("force_clear_cache", default=False, tooltip="Clear CUDA cache after each layer (slower but saves VRAM)"), ], outputs=[io.String.Output(display_name="output_path")], is_output_node=True, ) @classmethod - def execute(cls, lora_name, max_rank, target, output_filename, save_dtype, device) -> io.NodeOutput: + def execute(cls, lora_name, max_rank, target, output_filename, save_dtype, device, lazy_load, force_clear_cache) -> io.NodeOutput: lora_path = folder_paths.get_full_path_or_raise("loras", lora_name) dtype = {"fp16": torch.float16, "bf16": torch.bfloat16, "fp32": torch.float32}[save_dtype] - path = resize_lora_file(lora_path, max_rank, "sv_cumulative", target, device, dtype, output_filename) + path = resize_lora_file( + lora_path, max_rank, "sv_cumulative", target, device, dtype, output_filename, + lazy_load=lazy_load, force_clear_cache=force_clear_cache + ) return io.NodeOutput(path) @@ -662,6 +687,8 @@ def merge_loras_to_model( output_filename: str, skip_patterns_str: str = "", verbose: bool = True, + lazy_load: bool = True, + force_clear_cache: bool = False, ) -> str: """ Merge multiple LoRAs into a base model and save the result directly. @@ -679,6 +706,8 @@ def merge_loras_to_model( output_filename: Output filename (without extension) skip_patterns_str: Regex patterns for layers to skip verbose: Print progress info + lazy_load: Low memory mode: load tensors from disk on demand + force_clear_cache: Clear CUDA cache after each layer Returns: Path to saved merged model @@ -694,8 +723,8 @@ def merge_loras_to_model( prepare_for_large_operation(total_size_gb * 1.5, torch.device(device)) # Open all files - use low_memory for base model to avoid OS page caching - base_handler = MemoryEfficientSafeOpen(base_model_path, low_memory=True) - lora_handlers = [MemoryEfficientSafeOpen(lp) for lp in lora_paths] + base_handler = MemoryEfficientSafeOpen(base_model_path, low_memory=lazy_load) + lora_handlers = [MemoryEfficientSafeOpen(lp, low_memory=lazy_load) for lp in lora_paths] try: @@ -874,10 +903,11 @@ def merge_loras_to_model( del cpu_base stats["copied"] += 1 - import gc - gc.collect() - if torch.cuda.is_available(): - torch.cuda.empty_cache() + if force_clear_cache: + import gc + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() pbar.update(1) finally: @@ -957,6 +987,8 @@ class LoRAMergeToModel(io.ComfyNode): io.String.Input("output_filename", default="merged_model"), io.Combo.Input("save_dtype", options=["fp16", "bf16", "fp32"], default="fp16"), io.Combo.Input("device", options=["cuda", "cpu"], default="cuda"), + io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), + io.Boolean.Input("force_clear_cache", default=False, tooltip="Clear CUDA cache after each layer (slower but saves VRAM)"), ], outputs=[io.String.Output(display_name="output_path")], is_output_node=True, @@ -966,7 +998,7 @@ class LoRAMergeToModel(io.ComfyNode): def execute(cls, base_model, lora_count, lora_1, weight_1, lora_2, weight_2, lora_3, weight_3, lora_4, weight_4, lora_5, weight_5, lora_6, weight_6, lora_7, weight_7, lora_8, weight_8, - skip_patterns, output_filename, save_dtype, device) -> io.NodeOutput: + skip_patterns, output_filename, save_dtype, device, lazy_load, force_clear_cache) -> io.NodeOutput: # Build LoRA list based on count count = int(lora_count) @@ -991,5 +1023,7 @@ class LoRAMergeToModel(io.ComfyNode): save_dtype=dtype, output_filename=output_filename, skip_patterns_str=skip_patterns, + lazy_load=lazy_load, + force_clear_cache=force_clear_cache, ) return io.NodeOutput(path) diff --git a/nodes/merger.py b/nodes/merger.py index bcb1d67..2357424 100644 --- a/nodes/merger.py +++ b/nodes/merger.py @@ -132,6 +132,25 @@ class MergerLogic: pbar = comfy.utils.ProgressBar(len(all_keys)) save_dtype = recipe_params.pop('save_dtype') save_torch_dtype = {"fp32": torch.float32, "fp16": torch.float16, "bf16": torch.bfloat16}.get(save_dtype) + override_dtype = recipe_params.pop('override_dtype', False) + + def determine_target_dtype(original_dtypes, target_dtype, override): + if override: + return target_dtype + priority_map = { + torch.float32: 3, + torch.bfloat16: 2, + torch.float16: 2, + } + highest_dtype = target_dtype + highest_priority = priority_map.get(highest_dtype, 1) + for dtype in original_dtypes: + p = priority_map.get(dtype, 1) + if p > highest_priority: + highest_priority = p + highest_dtype = dtype + return highest_dtype + recipe_params.update({"handlers": handlers}) process_device = recipe_params.get('device', 'cpu') process_dtype = recipe_params.get('dtype', torch.float32) @@ -181,6 +200,15 @@ class MergerLogic: # Pre-load Model A's tensor with pinned memory for CUDA cpu_tensor = primary_handler.get_tensor(key) + + # Determine original dtypes across all active models for this key + original_dtypes = [primary_handler.get_dtype(key)] + for name, handler in handlers.items(): + if name != primary_model_name and key in handler.keys(): + original_dtypes.append(handler.get_dtype(key)) + + target_dtype = determine_target_dtype(original_dtypes, save_torch_dtype, override_dtype) + if process_device == 'cuda': tensor_a = transfer_to_gpu_pinned(cpu_tensor, process_device, process_dtype) else: @@ -189,7 +217,7 @@ class MergerLogic: # Check exclude patterns - use Model A only, no merge if _matches_any_pattern(key, exclude_patterns, glob_mode=glob_mode): - t = tensor_a.detach().to(save_torch_dtype).cpu() + t = tensor_a.detach().to(target_dtype).cpu() batch_buffer[key] = t batch_bytes += t.numel() * t.element_size() @@ -224,7 +252,7 @@ class MergerLogic: if isinstance(result, dict): for r_key, r_tensor in result.items(): - t = r_tensor.detach().to(save_torch_dtype).cpu() + t = r_tensor.detach().to(target_dtype).cpu() batch_buffer[r_key] = t batch_bytes += t.numel() * t.element_size() @@ -238,7 +266,7 @@ class MergerLogic: slices = tuple(slice(0, min(res_s, tgt_s)) for res_s, tgt_s in zip(result.shape, target_shape)) result = result[slices] - t = result.detach().to(save_torch_dtype).cpu() + t = result.detach().to(target_dtype).cpu() batch_buffer[key] = t batch_bytes += t.numel() * t.element_size() @@ -323,6 +351,7 @@ class CheckpointTwoMerger(io.ComfyNode): "When False (default), patterns are Python regex matched as substrings."), io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), io.Boolean.Input("force_clear_cache", default=True, tooltip="Clear CUDA cache after each layer"), + io.Boolean.Input("override_dtype", default=False, tooltip="Force the entire model to be saved as the selected save_dtype. If False (default), higher precision dtypes are preserved."), ], outputs=[ io.String.Output(display_name="output_filename"), @@ -335,7 +364,7 @@ class CheckpointTwoMerger(io.ComfyNode): calc_mode: str, mismatch_mode: str, alignment_mode: str, alpha: float, beta: float, gamma: float, delta: float, epsilon: float, zeta: float, seed: int, output_filename: str, save_dtype: str, process_device: str, exclude_patterns: str, discard_patterns: str, - glob_patterns: bool, lazy_load: bool, force_clear_cache: bool) -> io.NodeOutput: + glob_patterns: bool, lazy_load: bool, force_clear_cache: bool, override_dtype: bool) -> io.NodeOutput: doc = load_documentation_from_file('merger_2_model_modes.md') if execution_mode == "DOCUMENTATION ONLY": return io.NodeOutput("Documentation mode active. No merge performed.", doc) @@ -344,7 +373,7 @@ class CheckpointTwoMerger(io.ComfyNode): "model_a": model_a, "model_b": model_b, "calc_mode": calc_mode, "mismatch_mode": mismatch_mode, "alignment_mode": alignment_mode, "alpha": alpha, "beta": beta, "gamma": gamma, "delta": delta, "epsilon": epsilon, "zeta": zeta, "seed": seed, - "output_filename": output_filename, "save_dtype": save_dtype, + "output_filename": output_filename, "save_dtype": save_dtype, "override_dtype": override_dtype, "device": process_device, "dtype": torch.float32, "exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns, "glob_patterns": glob_patterns, @@ -388,6 +417,7 @@ class ModelTwoMerger(io.ComfyNode): "When False (default), patterns are Python regex matched as substrings."), io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), io.Boolean.Input("force_clear_cache", default=True, tooltip="Clear CUDA cache after each layer"), + io.Boolean.Input("override_dtype", default=False, tooltip="Force the entire model to be saved as the selected save_dtype. If False (default), higher precision dtypes are preserved."), ], outputs=[ io.String.Output(display_name="output_filename"), @@ -400,7 +430,7 @@ class ModelTwoMerger(io.ComfyNode): calc_mode: str, mismatch_mode: str, alignment_mode: str, alpha: float, beta: float, gamma: float, delta: float, epsilon: float, zeta: float, seed: int, output_filename: str, save_dtype: str, process_device: str, exclude_patterns: str, discard_patterns: str, - glob_patterns: bool, lazy_load: bool, force_clear_cache: bool) -> io.NodeOutput: + glob_patterns: bool, lazy_load: bool, force_clear_cache: bool, override_dtype: bool) -> io.NodeOutput: doc = load_documentation_from_file('merger_2_model_modes.md') if execution_mode == "DOCUMENTATION ONLY": return io.NodeOutput("Documentation mode active. No merge performed.", doc) @@ -409,7 +439,7 @@ class ModelTwoMerger(io.ComfyNode): "model_a": model_a, "model_b": model_b, "calc_mode": calc_mode, "mismatch_mode": mismatch_mode, "alignment_mode": alignment_mode, "alpha": alpha, "beta": beta, "gamma": gamma, "delta": delta, "epsilon": epsilon, "zeta": zeta, "seed": seed, - "output_filename": output_filename, "save_dtype": save_dtype, + "output_filename": output_filename, "save_dtype": save_dtype, "override_dtype": override_dtype, "device": process_device, "dtype": torch.float32, "exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns, "glob_patterns": glob_patterns, @@ -453,6 +483,7 @@ class TextEncoderTwoMerger(io.ComfyNode): "When False (default), patterns are Python regex matched as substrings."), io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), io.Boolean.Input("force_clear_cache", default=True, tooltip="Clear CUDA cache after each layer"), + io.Boolean.Input("override_dtype", default=False, tooltip="Force the entire model to be saved as the selected save_dtype. If False (default), higher precision dtypes are preserved."), ], outputs=[ io.String.Output(display_name="output_filename"), @@ -465,7 +496,7 @@ class TextEncoderTwoMerger(io.ComfyNode): calc_mode: str, mismatch_mode: str, alignment_mode: str, alpha: float, beta: float, gamma: float, delta: float, epsilon: float, zeta: float, seed: int, output_filename: str, save_dtype: str, process_device: str, exclude_patterns: str, discard_patterns: str, - glob_patterns: bool, lazy_load: bool, force_clear_cache: bool) -> io.NodeOutput: + glob_patterns: bool, lazy_load: bool, force_clear_cache: bool, override_dtype: bool) -> io.NodeOutput: doc = load_documentation_from_file('merger_2_model_modes.md') if execution_mode == "DOCUMENTATION ONLY": return io.NodeOutput("Documentation mode active. No merge performed.", doc) @@ -474,7 +505,7 @@ class TextEncoderTwoMerger(io.ComfyNode): "model_a": model_a, "model_b": model_b, "calc_mode": calc_mode, "mismatch_mode": mismatch_mode, "alignment_mode": alignment_mode, "alpha": alpha, "beta": beta, "gamma": gamma, "delta": delta, "epsilon": epsilon, "zeta": zeta, "seed": seed, - "output_filename": output_filename, "save_dtype": save_dtype, + "output_filename": output_filename, "save_dtype": save_dtype, "override_dtype": override_dtype, "device": process_device, "dtype": torch.float32, "exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns, "glob_patterns": glob_patterns, @@ -518,6 +549,7 @@ class LoRATwoMerger(io.ComfyNode): "When False (default), patterns are Python regex matched as substrings."), io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), io.Boolean.Input("force_clear_cache", default=True, tooltip="Clear CUDA cache after each layer"), + io.Boolean.Input("override_dtype", default=False, tooltip="Force the entire model to be saved as the selected save_dtype. If False (default), higher precision dtypes are preserved."), ], outputs=[ io.String.Output(display_name="output_filename"), @@ -530,7 +562,7 @@ class LoRATwoMerger(io.ComfyNode): calc_mode: str, mismatch_mode: str, alignment_mode: str, alpha: float, beta: float, gamma: float, delta: float, epsilon: float, zeta: float, seed: int, output_filename: str, save_dtype: str, process_device: str, exclude_patterns: str, discard_patterns: str, - glob_patterns: bool, lazy_load: bool, force_clear_cache: bool) -> io.NodeOutput: + glob_patterns: bool, lazy_load: bool, force_clear_cache: bool, override_dtype: bool) -> io.NodeOutput: doc = load_documentation_from_file('merger_2_model_modes.md') if execution_mode == "DOCUMENTATION ONLY": return io.NodeOutput("Documentation mode active. No merge performed.", doc) @@ -539,7 +571,7 @@ class LoRATwoMerger(io.ComfyNode): "model_a": model_a, "model_b": model_b, "calc_mode": calc_mode, "mismatch_mode": mismatch_mode, "alignment_mode": alignment_mode, "alpha": alpha, "beta": beta, "gamma": gamma, "delta": delta, "epsilon": epsilon, "zeta": zeta, "seed": seed, - "output_filename": output_filename, "save_dtype": save_dtype, + "output_filename": output_filename, "save_dtype": save_dtype, "override_dtype": override_dtype, "device": process_device, "dtype": torch.float32, "exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns, "glob_patterns": glob_patterns, @@ -583,6 +615,7 @@ class EmbeddingTwoMerger(io.ComfyNode): "When False (default), patterns are Python regex matched as substrings."), io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), io.Boolean.Input("force_clear_cache", default=True, tooltip="Clear CUDA cache after each layer"), + io.Boolean.Input("override_dtype", default=False, tooltip="Force the entire model to be saved as the selected save_dtype. If False (default), higher precision dtypes are preserved."), ], outputs=[ io.String.Output(display_name="output_filename"), @@ -595,7 +628,7 @@ class EmbeddingTwoMerger(io.ComfyNode): calc_mode: str, mismatch_mode: str, alignment_mode: str, alpha: float, beta: float, gamma: float, delta: float, epsilon: float, zeta: float, seed: int, output_filename: str, save_dtype: str, process_device: str, exclude_patterns: str, discard_patterns: str, - glob_patterns: bool, lazy_load: bool, force_clear_cache: bool) -> io.NodeOutput: + glob_patterns: bool, lazy_load: bool, force_clear_cache: bool, override_dtype: bool) -> io.NodeOutput: doc = load_documentation_from_file('merger_2_model_modes.md') if execution_mode == "DOCUMENTATION ONLY": return io.NodeOutput("Documentation mode active. No merge performed.", doc) @@ -604,7 +637,7 @@ class EmbeddingTwoMerger(io.ComfyNode): "model_a": model_a, "model_b": model_b, "calc_mode": calc_mode, "mismatch_mode": mismatch_mode, "alignment_mode": alignment_mode, "alpha": alpha, "beta": beta, "gamma": gamma, "delta": delta, "epsilon": epsilon, "zeta": zeta, "seed": seed, - "output_filename": output_filename, "save_dtype": save_dtype, + "output_filename": output_filename, "save_dtype": save_dtype, "override_dtype": override_dtype, "device": process_device, "dtype": torch.float32, "exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns, "glob_patterns": glob_patterns, @@ -651,6 +684,7 @@ class CheckpointThreeMerger(io.ComfyNode): "When False (default), patterns are Python regex matched as substrings."), io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), io.Boolean.Input("force_clear_cache", default=True, tooltip="Clear CUDA cache after each layer"), + io.Boolean.Input("override_dtype", default=False, tooltip="Force the entire model to be saved as the selected save_dtype. If False (default), higher precision dtypes are preserved."), ], outputs=[ io.String.Output(display_name="output_filename"), @@ -663,7 +697,7 @@ class CheckpointThreeMerger(io.ComfyNode): calc_mode: str, mismatch_mode: str, alignment_mode: str, alpha: float, beta: float, gamma: float, delta: float, epsilon: float, zeta: float, seed: int, output_filename: str, save_dtype: str, process_device: str, exclude_patterns: str, discard_patterns: str, - glob_patterns: bool, lazy_load: bool, force_clear_cache: bool) -> io.NodeOutput: + glob_patterns: bool, lazy_load: bool, force_clear_cache: bool, override_dtype: bool) -> io.NodeOutput: doc = load_documentation_from_file('merger_3_model_modes.md') if execution_mode == "DOCUMENTATION ONLY": return io.NodeOutput("Documentation mode active. No merge performed.", doc) @@ -672,7 +706,7 @@ class CheckpointThreeMerger(io.ComfyNode): "model_a": model_a, "model_b": model_b, "model_c": model_c, "calc_mode": calc_mode, "mismatch_mode": mismatch_mode, "alignment_mode": alignment_mode, "alpha": alpha, "beta": beta, "gamma": gamma, "delta": delta, "epsilon": epsilon, "zeta": zeta, "seed": seed, - "output_filename": output_filename, "save_dtype": save_dtype, + "output_filename": output_filename, "save_dtype": save_dtype, "override_dtype": override_dtype, "device": process_device, "dtype": torch.float32, "exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns, "glob_patterns": glob_patterns, @@ -717,6 +751,7 @@ class ModelThreeMerger(io.ComfyNode): "When False (default), patterns are Python regex matched as substrings."), io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), io.Boolean.Input("force_clear_cache", default=True, tooltip="Clear CUDA cache after each layer"), + io.Boolean.Input("override_dtype", default=False, tooltip="Force the entire model to be saved as the selected save_dtype. If False (default), higher precision dtypes are preserved."), ], outputs=[ io.String.Output(display_name="output_filename"), @@ -729,7 +764,7 @@ class ModelThreeMerger(io.ComfyNode): calc_mode: str, mismatch_mode: str, alignment_mode: str, alpha: float, beta: float, gamma: float, delta: float, epsilon: float, zeta: float, seed: int, output_filename: str, save_dtype: str, process_device: str, exclude_patterns: str, discard_patterns: str, - glob_patterns: bool, lazy_load: bool, force_clear_cache: bool) -> io.NodeOutput: + glob_patterns: bool, lazy_load: bool, force_clear_cache: bool, override_dtype: bool) -> io.NodeOutput: doc = load_documentation_from_file('merger_3_model_modes.md') if execution_mode == "DOCUMENTATION ONLY": return io.NodeOutput("Documentation mode active. No merge performed.", doc) @@ -738,7 +773,7 @@ class ModelThreeMerger(io.ComfyNode): "model_a": model_a, "model_b": model_b, "model_c": model_c, "calc_mode": calc_mode, "mismatch_mode": mismatch_mode, "alignment_mode": alignment_mode, "alpha": alpha, "beta": beta, "gamma": gamma, "delta": delta, "epsilon": epsilon, "zeta": zeta, "seed": seed, - "output_filename": output_filename, "save_dtype": save_dtype, + "output_filename": output_filename, "save_dtype": save_dtype, "override_dtype": override_dtype, "device": process_device, "dtype": torch.float32, "exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns, "glob_patterns": glob_patterns, @@ -783,6 +818,7 @@ class TextEncoderThreeMerger(io.ComfyNode): "When False (default), patterns are Python regex matched as substrings."), io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), io.Boolean.Input("force_clear_cache", default=True, tooltip="Clear CUDA cache after each layer"), + io.Boolean.Input("override_dtype", default=False, tooltip="Force the entire model to be saved as the selected save_dtype. If False (default), higher precision dtypes are preserved."), ], outputs=[ io.String.Output(display_name="output_filename"), @@ -795,7 +831,7 @@ class TextEncoderThreeMerger(io.ComfyNode): calc_mode: str, mismatch_mode: str, alignment_mode: str, alpha: float, beta: float, gamma: float, delta: float, epsilon: float, zeta: float, seed: int, output_filename: str, save_dtype: str, process_device: str, exclude_patterns: str, discard_patterns: str, - glob_patterns: bool, lazy_load: bool, force_clear_cache: bool) -> io.NodeOutput: + glob_patterns: bool, lazy_load: bool, force_clear_cache: bool, override_dtype: bool) -> io.NodeOutput: doc = load_documentation_from_file('merger_3_model_modes.md') if execution_mode == "DOCUMENTATION ONLY": return io.NodeOutput("Documentation mode active. No merge performed.", doc) @@ -804,7 +840,7 @@ class TextEncoderThreeMerger(io.ComfyNode): "model_a": model_a, "model_b": model_b, "model_c": model_c, "calc_mode": calc_mode, "mismatch_mode": mismatch_mode, "alignment_mode": alignment_mode, "alpha": alpha, "beta": beta, "gamma": gamma, "delta": delta, "epsilon": epsilon, "zeta": zeta, "seed": seed, - "output_filename": output_filename, "save_dtype": save_dtype, + "output_filename": output_filename, "save_dtype": save_dtype, "override_dtype": override_dtype, "device": process_device, "dtype": torch.float32, "exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns, "glob_patterns": glob_patterns, @@ -849,6 +885,7 @@ class LoRAThreeMerger(io.ComfyNode): "When False (default), patterns are Python regex matched as substrings."), io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), io.Boolean.Input("force_clear_cache", default=True, tooltip="Clear CUDA cache after each layer"), + io.Boolean.Input("override_dtype", default=False, tooltip="Force the entire model to be saved as the selected save_dtype. If False (default), higher precision dtypes are preserved."), ], outputs=[ io.String.Output(display_name="output_filename"), @@ -861,7 +898,7 @@ class LoRAThreeMerger(io.ComfyNode): calc_mode: str, mismatch_mode: str, alignment_mode: str, alpha: float, beta: float, gamma: float, delta: float, epsilon: float, zeta: float, seed: int, output_filename: str, save_dtype: str, process_device: str, exclude_patterns: str, discard_patterns: str, - glob_patterns: bool, lazy_load: bool, force_clear_cache: bool) -> io.NodeOutput: + glob_patterns: bool, lazy_load: bool, force_clear_cache: bool, override_dtype: bool) -> io.NodeOutput: doc = load_documentation_from_file('merger_3_model_modes.md') if execution_mode == "DOCUMENTATION ONLY": return io.NodeOutput("Documentation mode active. No merge performed.", doc) @@ -870,7 +907,7 @@ class LoRAThreeMerger(io.ComfyNode): "model_a": model_a, "model_b": model_b, "model_c": model_c, "calc_mode": calc_mode, "mismatch_mode": mismatch_mode, "alignment_mode": alignment_mode, "alpha": alpha, "beta": beta, "gamma": gamma, "delta": delta, "epsilon": epsilon, "zeta": zeta, "seed": seed, - "output_filename": output_filename, "save_dtype": save_dtype, + "output_filename": output_filename, "save_dtype": save_dtype, "override_dtype": override_dtype, "device": process_device, "dtype": torch.float32, "exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns, "glob_patterns": glob_patterns, @@ -915,6 +952,7 @@ class EmbeddingThreeMerger(io.ComfyNode): "When False (default), patterns are Python regex matched as substrings."), io.Boolean.Input("lazy_load", default=True, tooltip="Low memory mode: load tensors from disk on demand"), io.Boolean.Input("force_clear_cache", default=True, tooltip="Clear CUDA cache after each layer"), + io.Boolean.Input("override_dtype", default=False, tooltip="Force the entire model to be saved as the selected save_dtype. If False (default), higher precision dtypes are preserved."), ], outputs=[ io.String.Output(display_name="output_filename"), @@ -927,7 +965,7 @@ class EmbeddingThreeMerger(io.ComfyNode): calc_mode: str, mismatch_mode: str, alignment_mode: str, alpha: float, beta: float, gamma: float, delta: float, epsilon: float, zeta: float, seed: int, output_filename: str, save_dtype: str, process_device: str, exclude_patterns: str, discard_patterns: str, - glob_patterns: bool, lazy_load: bool, force_clear_cache: bool) -> io.NodeOutput: + glob_patterns: bool, lazy_load: bool, force_clear_cache: bool, override_dtype: bool) -> io.NodeOutput: doc = load_documentation_from_file('merger_3_model_modes.md') if execution_mode == "DOCUMENTATION ONLY": return io.NodeOutput("Documentation mode active. No merge performed.", doc) @@ -936,7 +974,7 @@ class EmbeddingThreeMerger(io.ComfyNode): "model_a": model_a, "model_b": model_b, "model_c": model_c, "calc_mode": calc_mode, "mismatch_mode": mismatch_mode, "alignment_mode": alignment_mode, "alpha": alpha, "beta": beta, "gamma": gamma, "delta": delta, "epsilon": epsilon, "zeta": zeta, "seed": seed, - "output_filename": output_filename, "save_dtype": save_dtype, + "output_filename": output_filename, "save_dtype": save_dtype, "override_dtype": override_dtype, "device": process_device, "dtype": torch.float32, "exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns, "glob_patterns": glob_patterns,