- preserve direct diffs, precision, and guarded low-bit tensors - stream paired CWB layers with atomic CPU OOM fallback - save generated merge artifacts under models_dir
1023 lines
60 KiB
Python
1023 lines
60 KiB
Python
import fnmatch
|
|
import os
|
|
import re
|
|
import torch
|
|
import folder_paths
|
|
import comfy.utils
|
|
from tqdm import tqdm
|
|
from comfy_api.latest import io
|
|
from .merger_ops import TWO_MODEL_MODES, THREE_MODEL_MODES, MissingTensorBehavior, MissingTensorError
|
|
from .device_utils import (
|
|
estimate_model_size, prepare_for_large_operation, cleanup_after_operation
|
|
)
|
|
|
|
from unifiedefficientloader import MemoryEfficientSafeOpen, transfer_to_gpu_pinned, IncrementalSafetensorsWriter
|
|
from .quantization_guard import inspect_low_bit_input, write_preserved_tensor
|
|
from .lora_resize import is_direct_diff_key
|
|
|
|
|
|
def load_documentation_from_file(filename):
|
|
"""Loads documentation from a markdown file in the ../docs/ directory."""
|
|
current_dir = os.path.dirname(os.path.realpath(__file__))
|
|
docs_path = os.path.join(current_dir, '..', 'docs', filename)
|
|
try:
|
|
with open(docs_path, 'r', encoding='utf-8') as f:
|
|
return f.read()
|
|
except FileNotFoundError:
|
|
return f"# Documentation File Not Found\n\nPlease ensure `{filename}` exists in the `docs` directory of the custom node."
|
|
|
|
|
|
def _compile_patterns(pattern_string, glob_mode=False):
|
|
"""Compiles whitespace-separated patterns into a list.
|
|
|
|
In regex mode (default): returns compiled re.Pattern objects.
|
|
Raises ValueError if any pattern is invalid regex.
|
|
In glob mode: returns plain strings; fnmatch handles matching.
|
|
"""
|
|
if not pattern_string or not pattern_string.strip():
|
|
return []
|
|
|
|
patterns = []
|
|
for pattern in pattern_string.split():
|
|
if not pattern:
|
|
continue
|
|
if glob_mode:
|
|
patterns.append(pattern)
|
|
else:
|
|
try:
|
|
patterns.append(re.compile(pattern))
|
|
except re.error as e:
|
|
raise ValueError(f"Invalid regex pattern '{pattern}': {e}")
|
|
return patterns
|
|
|
|
|
|
def _matches_any_pattern(key, patterns, glob_mode=False):
|
|
"""Returns True if key matches any pattern.
|
|
|
|
Glob mode: fnmatch substring match — dots are literal, * matches any sequence.
|
|
Regex mode: compiled re.Pattern substring search.
|
|
"""
|
|
if glob_mode:
|
|
return any(fnmatch.fnmatch(key, f"*{p}*") for p in patterns)
|
|
for pattern in patterns:
|
|
if pattern.search(key):
|
|
return True
|
|
return False
|
|
|
|
|
|
class MergerLogic:
|
|
"""Shared merge execution logic for all merger nodes."""
|
|
|
|
@staticmethod
|
|
def execute_merge(model_names, calc_mode, all_modes, recipe_params, model_type):
|
|
calc_mode_class = next((m for m in all_modes if m.name == calc_mode), None)
|
|
if not calc_mode_class:
|
|
raise ValueError(f"Calc mode '{calc_mode}' not found.")
|
|
primary_model_name = model_names.get('model_a')
|
|
if not primary_model_name or primary_model_name == "None":
|
|
raise ValueError("Model A is required to run the merge.")
|
|
|
|
# Prepare memory before heavy operation
|
|
total_size_gb = 0
|
|
model_paths = []
|
|
for name in model_names.values():
|
|
if name and name != "None":
|
|
path = folder_paths.get_full_path(model_type, name)
|
|
if path:
|
|
total_size_gb += estimate_model_size(path)
|
|
model_paths.append(path)
|
|
|
|
if total_size_gb > 0:
|
|
process_device = recipe_params.get('device', 'cpu')
|
|
print(f"[Merger] Preparing memory for {total_size_gb:.2f}GB merge operation...")
|
|
prepare_for_large_operation(total_size_gb * 1.2, torch.device(process_device))
|
|
flush_threshold_bytes = int(total_size_gb * 1024**3 / 16)
|
|
else:
|
|
flush_threshold_bytes = 256 * 1024 * 1024 # 256MB fallback (min 8GB RAM assumed)
|
|
|
|
handlers = {}
|
|
for name in model_names.values():
|
|
if name and name != "None":
|
|
path = folder_paths.get_full_path(model_type, name)
|
|
if not path:
|
|
raise FileNotFoundError(f"Model '{name}' not found.")
|
|
handlers[name] = MemoryEfficientSafeOpen(path, low_memory=True)
|
|
|
|
primary_handler = handlers[primary_model_name]
|
|
all_keys = primary_handler.keys()
|
|
metadata = primary_handler.metadata()
|
|
|
|
low_bit_keys_by_name = {}
|
|
if model_type == "loras":
|
|
try:
|
|
for name, handler in handlers.items():
|
|
low_bit_keys_by_name[name] = inspect_low_bit_input(
|
|
handler, f"LoRA ({name})", "Generic LoRA Merge"
|
|
)
|
|
except Exception:
|
|
for handler in handlers.values():
|
|
handler.__exit__(None, None, None)
|
|
raise
|
|
|
|
# Convert mismatch_mode string to enum
|
|
mismatch_mode_str = recipe_params.get('mismatch_mode', 'skip')
|
|
mismatch_mode = MissingTensorBehavior(mismatch_mode_str)
|
|
recipe_params['mismatch_mode'] = mismatch_mode
|
|
alignment_mode = recipe_params.get('alignment_mode', 'pad/crop')
|
|
|
|
# Compile filter patterns
|
|
glob_mode = recipe_params.get('glob_patterns', False)
|
|
exclude_patterns = _compile_patterns(recipe_params.get('exclude_patterns', ''), glob_mode=glob_mode)
|
|
discard_patterns = _compile_patterns(recipe_params.get('discard_patterns', ''), glob_mode=glob_mode)
|
|
|
|
# Pre-compute key differences for logging
|
|
primary_keys = set(all_keys)
|
|
for name, handler in handlers.items():
|
|
if name != primary_model_name:
|
|
secondary_keys = set(handler.keys())
|
|
missing = primary_keys - secondary_keys
|
|
extra = secondary_keys - primary_keys
|
|
if missing:
|
|
print(f"[Merger] {name} is missing {len(missing)} keys present in Model A")
|
|
if extra:
|
|
print(f"[Merger] {name} has {len(extra)} extra keys not in Model A (ignored)")
|
|
|
|
merged_state_dict = {}
|
|
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)
|
|
include_1d_diffs = recipe_params.pop('include_1d_diffs', 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)
|
|
|
|
skipped_keys = 0
|
|
excluded_keys = 0
|
|
discarded_keys = 0
|
|
error_keys = []
|
|
|
|
output_folder = "loras" if calc_mode == "SVD LoRA Extraction" else model_type
|
|
output_dir = os.path.join(folder_paths.models_dir, output_folder)
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
output_filename = recipe_params.get("output_filename")
|
|
output_path = os.path.join(output_dir, f"{output_filename}.safetensors")
|
|
|
|
writer = None
|
|
|
|
writer = IncrementalSafetensorsWriter(output_path, metadata=metadata)
|
|
writer.__enter__()
|
|
|
|
batch_buffer = {}
|
|
batch_bytes = 0
|
|
batch_key_count = 0
|
|
|
|
def _flush_batch():
|
|
nonlocal batch_buffer, batch_bytes, batch_key_count
|
|
if not batch_buffer:
|
|
return
|
|
writer.write_dict(batch_buffer)
|
|
pbar.update(batch_key_count)
|
|
batch_buffer = {}
|
|
batch_bytes = 0
|
|
batch_key_count = 0
|
|
if recipe_params.get('force_clear_cache', True):
|
|
import gc
|
|
gc.collect()
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
|
|
with torch.no_grad():
|
|
for key in tqdm(all_keys, desc="Merging layers", unit="layers"):
|
|
preserve_low_bit = model_type == "loras" and any(
|
|
key in low_bit_keys for low_bit_keys in low_bit_keys_by_name.values()
|
|
)
|
|
if preserve_low_bit:
|
|
_flush_batch()
|
|
write_preserved_tensor(writer, key, primary_handler)
|
|
pbar.update(1)
|
|
continue
|
|
# Check discard patterns first - skip entirely
|
|
if _matches_any_pattern(key, discard_patterns, glob_mode=glob_mode):
|
|
discarded_keys += 1
|
|
continue
|
|
|
|
# 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)
|
|
preserve_model_a_1d = model_type == "loras" and cpu_tensor.ndim == 1 and not include_1d_diffs
|
|
if preserve_model_a_1d:
|
|
target_dtype = determine_target_dtype(
|
|
[primary_handler.get_dtype(key)], save_torch_dtype, override_dtype
|
|
)
|
|
elif model_type == "loras" and is_direct_diff_key(key) and cpu_tensor.ndim == 1:
|
|
target_dtype = torch.float32
|
|
|
|
if process_device == 'cuda':
|
|
tensor_a = transfer_to_gpu_pinned(cpu_tensor, process_device, process_dtype)
|
|
else:
|
|
tensor_a = cpu_tensor.to(device=process_device, dtype=process_dtype)
|
|
del cpu_tensor
|
|
|
|
# Check exclude patterns - use Model A only, no merge
|
|
if preserve_model_a_1d or _matches_any_pattern(key, exclude_patterns, glob_mode=glob_mode):
|
|
t = tensor_a.detach().to(target_dtype).cpu()
|
|
|
|
batch_buffer[key] = t
|
|
batch_bytes += t.numel() * t.element_size()
|
|
batch_key_count += 1
|
|
if not preserve_model_a_1d:
|
|
excluded_keys += 1
|
|
del tensor_a
|
|
if (batch_bytes >= flush_threshold_bytes or batch_key_count >= 32):
|
|
_flush_batch()
|
|
continue
|
|
|
|
# Pass tensor_a metadata to recipes for zeros mode and fallback
|
|
recipe_params['_tensor_a'] = tensor_a
|
|
recipe_params['_tensor_a_shape'] = tensor_a.shape
|
|
recipe_params['_tensor_a_dtype'] = tensor_a.dtype
|
|
|
|
try:
|
|
recipe = calc_mode_class.create_recipe(key=key, **recipe_params)
|
|
result = recipe.merge()
|
|
except MissingTensorError as e:
|
|
if mismatch_mode == MissingTensorBehavior.ERROR:
|
|
|
|
_flush_batch()
|
|
writer.__exit__(None, None, None)
|
|
raise ValueError(f"Layer mismatch error (mismatch_mode='error'): {e}")
|
|
result = None
|
|
error_keys.append(key)
|
|
|
|
# Handle None result (mismatch occurred with skip mode)
|
|
if result is None:
|
|
result = tensor_a
|
|
skipped_keys += 1
|
|
|
|
if isinstance(result, dict):
|
|
for r_key, r_tensor in result.items():
|
|
t = r_tensor.detach().to(target_dtype).cpu()
|
|
|
|
batch_buffer[r_key] = t
|
|
batch_bytes += t.numel() * t.element_size()
|
|
else:
|
|
# Ensure compatibility with Model A's architecture.
|
|
# If alignment_mode is 'pad/crop', we crop results that were padded.
|
|
# If alignment_mode is 'interpolate', resizing happened during operators.
|
|
if alignment_mode == 'pad/crop':
|
|
target_shape = recipe_params['_tensor_a_shape']
|
|
if result.shape != target_shape:
|
|
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(target_dtype).cpu()
|
|
|
|
batch_buffer[key] = t
|
|
batch_bytes += t.numel() * t.element_size()
|
|
|
|
|
|
batch_key_count += 1
|
|
|
|
# Clean up references to allow GC immediately.
|
|
# Local loop variables must be explicitly deleted to prevent PyTorch from keeping tensors in VRAM.
|
|
recipe.clean()
|
|
del recipe_params['_tensor_a']
|
|
del tensor_a
|
|
del recipe
|
|
del result
|
|
|
|
if (batch_bytes >= flush_threshold_bytes or batch_key_count >= 32):
|
|
_flush_batch()
|
|
|
|
|
|
_flush_batch()
|
|
writer.__exit__(None, None, None)
|
|
|
|
# Log summary
|
|
if excluded_keys > 0:
|
|
print(f"[Merger] Excluded {excluded_keys} keys from merge (kept Model A only)")
|
|
if discarded_keys > 0:
|
|
print(f"[Merger] Discarded {discarded_keys} keys from output")
|
|
if skipped_keys > 0:
|
|
print(f"[Merger] Used Model A's values for {skipped_keys} keys due to mismatches")
|
|
if error_keys:
|
|
print(f"[Merger] Unexpected errors on {len(error_keys)} keys: {error_keys[:5]}{'...' if len(error_keys) > 5 else ''}")
|
|
|
|
for handler in handlers.values():
|
|
handler.__exit__(None, None, None)
|
|
|
|
output_folder = "loras" if calc_mode == "SVD LoRA Extraction" else model_type
|
|
output_dir = os.path.join(folder_paths.models_dir, output_folder)
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
output_filename = recipe_params.get("output_filename")
|
|
output_path = os.path.join(output_dir, f"{output_filename}.safetensors")
|
|
if os.path.exists(output_path):
|
|
print(f"[Merger] Output saved to {output_path}")
|
|
# Cleanup after heavy operation
|
|
cleanup_after_operation()
|
|
|
|
return f"{output_filename}.safetensors"
|
|
|
|
|
|
# --- Two-Model Merger Nodes ---
|
|
|
|
class CheckpointTwoMerger(io.ComfyNode):
|
|
MODEL_TYPE = "checkpoints"
|
|
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return io.Schema(
|
|
node_id="CheckpointTwoMerger",
|
|
display_name="Merge Checkpoints (2 Models)",
|
|
category="ModelUtils/Merging",
|
|
inputs=[
|
|
io.Combo.Input("execution_mode", options=["MERGE", "DOCUMENTATION ONLY"]),
|
|
io.Combo.Input("model_a", options=["None"] + folder_paths.get_filename_list("checkpoints")),
|
|
io.Combo.Input("model_b", options=["None"] + folder_paths.get_filename_list("checkpoints")),
|
|
io.Combo.Input("calc_mode", options=[m.name for m in TWO_MODEL_MODES]),
|
|
io.Combo.Input("mismatch_mode", options=["skip", "zeros", "error"], default="skip"),
|
|
io.Combo.Input("alignment_mode", options=["pad/crop", "interpolate"], default="pad/crop"),
|
|
io.Float.Input("alpha", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("beta", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("gamma", default=0.99, min=-10.0, max=10.0, step=0.001),
|
|
io.Float.Input("delta", default=2.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("epsilon", default=0.01, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("zeta", default=0.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
|
|
io.String.Input("output_filename", default="merged_2_checkpoint"),
|
|
io.Combo.Input("save_dtype", options=["fp32", "fp16", "bf16"]),
|
|
io.Combo.Input("process_device", options=["cuda", "cpu"]),
|
|
io.String.Input("exclude_patterns", default="", multiline=True),
|
|
io.String.Input("discard_patterns", default="", multiline=True),
|
|
io.Boolean.Input("glob_patterns", default=False,
|
|
tooltip="When True, exclude/discard patterns use glob syntax (* = any sequence, dots are literal). "
|
|
"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"),
|
|
io.String.Output(display_name="documentation"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, execution_mode: str, model_a: str, model_b: str,
|
|
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, 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)
|
|
|
|
recipe_params = {
|
|
"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, "override_dtype": override_dtype,
|
|
"device": process_device, "dtype": torch.float32,
|
|
"exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns,
|
|
"glob_patterns": glob_patterns,
|
|
"lazy_load": lazy_load, "force_clear_cache": force_clear_cache,
|
|
}
|
|
model_names = {"model_a": model_a, "model_b": model_b}
|
|
filename = MergerLogic.execute_merge(model_names, calc_mode, TWO_MODEL_MODES, recipe_params, cls.MODEL_TYPE)
|
|
return io.NodeOutput(filename, doc)
|
|
|
|
|
|
class ModelTwoMerger(io.ComfyNode):
|
|
MODEL_TYPE = "diffusion_models"
|
|
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return io.Schema(
|
|
node_id="ModelTwoMerger",
|
|
display_name="Merge Models (2 Models)",
|
|
category="ModelUtils/Merging",
|
|
inputs=[
|
|
io.Combo.Input("execution_mode", options=["MERGE", "DOCUMENTATION ONLY"]),
|
|
io.Combo.Input("model_a", options=["None"] + folder_paths.get_filename_list("diffusion_models")),
|
|
io.Combo.Input("model_b", options=["None"] + folder_paths.get_filename_list("diffusion_models")),
|
|
io.Combo.Input("calc_mode", options=[m.name for m in TWO_MODEL_MODES]),
|
|
io.Combo.Input("mismatch_mode", options=["skip", "zeros", "error"], default="skip"),
|
|
io.Combo.Input("alignment_mode", options=["pad/crop", "interpolate"], default="pad/crop"),
|
|
io.Float.Input("alpha", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("beta", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("gamma", default=0.99, min=-10.0, max=10.0, step=0.001),
|
|
io.Float.Input("delta", default=2.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("epsilon", default=0.01, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("zeta", default=0.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
|
|
io.String.Input("output_filename", default="merged_2_model"),
|
|
io.Combo.Input("save_dtype", options=["fp32", "fp16", "bf16"]),
|
|
io.Combo.Input("process_device", options=["cuda", "cpu"]),
|
|
io.String.Input("exclude_patterns", default="", multiline=True),
|
|
io.String.Input("discard_patterns", default="", multiline=True),
|
|
io.Boolean.Input("glob_patterns", default=False,
|
|
tooltip="When True, exclude/discard patterns use glob syntax (* = any sequence, dots are literal). "
|
|
"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"),
|
|
io.String.Output(display_name="documentation"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, execution_mode: str, model_a: str, model_b: str,
|
|
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, 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)
|
|
|
|
recipe_params = {
|
|
"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, "override_dtype": override_dtype,
|
|
"device": process_device, "dtype": torch.float32,
|
|
"exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns,
|
|
"glob_patterns": glob_patterns,
|
|
"lazy_load": lazy_load, "force_clear_cache": force_clear_cache,
|
|
}
|
|
model_names = {"model_a": model_a, "model_b": model_b}
|
|
filename = MergerLogic.execute_merge(model_names, calc_mode, TWO_MODEL_MODES, recipe_params, cls.MODEL_TYPE)
|
|
return io.NodeOutput(filename, doc)
|
|
|
|
|
|
class TextEncoderTwoMerger(io.ComfyNode):
|
|
MODEL_TYPE = "text_encoders"
|
|
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return io.Schema(
|
|
node_id="TextEncoderTwoMerger",
|
|
display_name="Merge Text Encoders (2 Models)",
|
|
category="ModelUtils/Merging",
|
|
inputs=[
|
|
io.Combo.Input("execution_mode", options=["MERGE", "DOCUMENTATION ONLY"]),
|
|
io.Combo.Input("model_a", options=["None"] + folder_paths.get_filename_list("text_encoders")),
|
|
io.Combo.Input("model_b", options=["None"] + folder_paths.get_filename_list("text_encoders")),
|
|
io.Combo.Input("calc_mode", options=[m.name for m in TWO_MODEL_MODES]),
|
|
io.Combo.Input("mismatch_mode", options=["skip", "zeros", "error"], default="skip"),
|
|
io.Combo.Input("alignment_mode", options=["pad/crop", "interpolate"], default="pad/crop"),
|
|
io.Float.Input("alpha", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("beta", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("gamma", default=0.99, min=-10.0, max=10.0, step=0.001),
|
|
io.Float.Input("delta", default=2.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("epsilon", default=0.01, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("zeta", default=0.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
|
|
io.String.Input("output_filename", default="merged_2_textencoder"),
|
|
io.Combo.Input("save_dtype", options=["fp32", "fp16", "bf16"]),
|
|
io.Combo.Input("process_device", options=["cuda", "cpu"]),
|
|
io.String.Input("exclude_patterns", default="", multiline=True),
|
|
io.String.Input("discard_patterns", default="", multiline=True),
|
|
io.Boolean.Input("glob_patterns", default=False,
|
|
tooltip="When True, exclude/discard patterns use glob syntax (* = any sequence, dots are literal). "
|
|
"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"),
|
|
io.String.Output(display_name="documentation"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, execution_mode: str, model_a: str, model_b: str,
|
|
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, 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)
|
|
|
|
recipe_params = {
|
|
"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, "override_dtype": override_dtype,
|
|
"device": process_device, "dtype": torch.float32,
|
|
"exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns,
|
|
"glob_patterns": glob_patterns,
|
|
"lazy_load": lazy_load, "force_clear_cache": force_clear_cache,
|
|
}
|
|
model_names = {"model_a": model_a, "model_b": model_b}
|
|
filename = MergerLogic.execute_merge(model_names, calc_mode, TWO_MODEL_MODES, recipe_params, cls.MODEL_TYPE)
|
|
return io.NodeOutput(filename, doc)
|
|
|
|
|
|
class LoRATwoMerger(io.ComfyNode):
|
|
MODEL_TYPE = "loras"
|
|
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return io.Schema(
|
|
node_id="LoRATwoMerger",
|
|
display_name="Merge LoRAs (2 Models)",
|
|
category="ModelUtils/Merging",
|
|
inputs=[
|
|
io.Combo.Input("execution_mode", options=["MERGE", "DOCUMENTATION ONLY"]),
|
|
io.Combo.Input("model_a", options=["None"] + folder_paths.get_filename_list("loras")),
|
|
io.Combo.Input("model_b", options=["None"] + folder_paths.get_filename_list("loras")),
|
|
io.Combo.Input("calc_mode", options=[m.name for m in TWO_MODEL_MODES]),
|
|
io.Combo.Input("mismatch_mode", options=["skip", "zeros", "error"], default="skip"),
|
|
io.Combo.Input("alignment_mode", options=["pad/crop", "interpolate"], default="pad/crop"),
|
|
io.Float.Input("alpha", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("beta", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("gamma", default=0.99, min=-10.0, max=10.0, step=0.001),
|
|
io.Float.Input("delta", default=2.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("epsilon", default=0.01, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("zeta", default=0.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
|
|
io.String.Input("output_filename", default="merged_2_lora"),
|
|
io.Combo.Input("save_dtype", options=["fp32", "fp16", "bf16"]),
|
|
io.Combo.Input("process_device", options=["cuda", "cpu"]),
|
|
io.String.Input("exclude_patterns", default="", multiline=True),
|
|
io.String.Input("discard_patterns", default="", multiline=True),
|
|
io.Boolean.Input("glob_patterns", default=False,
|
|
tooltip="When True, exclude/discard patterns use glob syntax (* = any sequence, dots are literal). "
|
|
"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 merged non-1D tensors to save_dtype. Enabled 1D direct diffs remain FP32."),
|
|
io.Boolean.Input("include_1d_diffs", default=False,
|
|
tooltip="Merge 1D tensors. When disabled, preserve Model A's 1D tensors unchanged."),
|
|
],
|
|
outputs=[
|
|
io.String.Output(display_name="output_filename"),
|
|
io.String.Output(display_name="documentation"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, execution_mode: str, model_a: str, model_b: str,
|
|
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, override_dtype: bool,
|
|
include_1d_diffs: 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)
|
|
|
|
recipe_params = {
|
|
"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, "override_dtype": override_dtype,
|
|
"device": process_device, "dtype": torch.float32,
|
|
"exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns,
|
|
"glob_patterns": glob_patterns,
|
|
"lazy_load": lazy_load, "force_clear_cache": force_clear_cache,
|
|
"include_1d_diffs": include_1d_diffs,
|
|
}
|
|
model_names = {"model_a": model_a, "model_b": model_b}
|
|
filename = MergerLogic.execute_merge(model_names, calc_mode, TWO_MODEL_MODES, recipe_params, cls.MODEL_TYPE)
|
|
return io.NodeOutput(filename, doc)
|
|
|
|
|
|
class EmbeddingTwoMerger(io.ComfyNode):
|
|
MODEL_TYPE = "embeddings"
|
|
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return io.Schema(
|
|
node_id="EmbeddingTwoMerger",
|
|
display_name="Merge Embeddings (2 Models)",
|
|
category="ModelUtils/Merging",
|
|
inputs=[
|
|
io.Combo.Input("execution_mode", options=["MERGE", "DOCUMENTATION ONLY"]),
|
|
io.Combo.Input("model_a", options=["None"] + folder_paths.get_filename_list("embeddings")),
|
|
io.Combo.Input("model_b", options=["None"] + folder_paths.get_filename_list("embeddings")),
|
|
io.Combo.Input("calc_mode", options=[m.name for m in TWO_MODEL_MODES]),
|
|
io.Combo.Input("mismatch_mode", options=["skip", "zeros", "error"], default="skip"),
|
|
io.Combo.Input("alignment_mode", options=["pad/crop", "interpolate"], default="pad/crop"),
|
|
io.Float.Input("alpha", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("beta", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("gamma", default=0.99, min=-10.0, max=10.0, step=0.001),
|
|
io.Float.Input("delta", default=2.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("epsilon", default=0.01, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("zeta", default=0.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
|
|
io.String.Input("output_filename", default="merged_2_embedding"),
|
|
io.Combo.Input("save_dtype", options=["fp32", "fp16", "bf16"]),
|
|
io.Combo.Input("process_device", options=["cuda", "cpu"]),
|
|
io.String.Input("exclude_patterns", default="", multiline=True),
|
|
io.String.Input("discard_patterns", default="", multiline=True),
|
|
io.Boolean.Input("glob_patterns", default=False,
|
|
tooltip="When True, exclude/discard patterns use glob syntax (* = any sequence, dots are literal). "
|
|
"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"),
|
|
io.String.Output(display_name="documentation"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, execution_mode: str, model_a: str, model_b: str,
|
|
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, 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)
|
|
|
|
recipe_params = {
|
|
"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, "override_dtype": override_dtype,
|
|
"device": process_device, "dtype": torch.float32,
|
|
"exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns,
|
|
"glob_patterns": glob_patterns,
|
|
"lazy_load": lazy_load, "force_clear_cache": force_clear_cache,
|
|
}
|
|
model_names = {"model_a": model_a, "model_b": model_b}
|
|
filename = MergerLogic.execute_merge(model_names, calc_mode, TWO_MODEL_MODES, recipe_params, cls.MODEL_TYPE)
|
|
return io.NodeOutput(filename, doc)
|
|
|
|
|
|
# --- Three-Model Merger Nodes ---
|
|
|
|
class CheckpointThreeMerger(io.ComfyNode):
|
|
MODEL_TYPE = "checkpoints"
|
|
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return io.Schema(
|
|
node_id="CheckpointThreeMerger",
|
|
display_name="Merge Checkpoints (3 Models)",
|
|
category="ModelUtils/Merging",
|
|
inputs=[
|
|
io.Combo.Input("execution_mode", options=["MERGE", "DOCUMENTATION ONLY"]),
|
|
io.Combo.Input("model_a", options=["None"] + folder_paths.get_filename_list("checkpoints")),
|
|
io.Combo.Input("model_b", options=["None"] + folder_paths.get_filename_list("checkpoints")),
|
|
io.Combo.Input("model_c", options=["None"] + folder_paths.get_filename_list("checkpoints")),
|
|
io.Combo.Input("calc_mode", options=[m.name for m in THREE_MODEL_MODES]),
|
|
io.Combo.Input("mismatch_mode", options=["skip", "zeros", "error"], default="skip"),
|
|
io.Combo.Input("alignment_mode", options=["pad/crop", "interpolate"], default="pad/crop"),
|
|
io.Float.Input("alpha", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("beta", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("gamma", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("delta", default=2.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("epsilon", default=0.01, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("zeta", default=0.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
|
|
io.String.Input("output_filename", default="merged_3_checkpoint"),
|
|
io.Combo.Input("save_dtype", options=["fp32", "fp16", "bf16"]),
|
|
io.Combo.Input("process_device", options=["cuda", "cpu"]),
|
|
io.String.Input("exclude_patterns", default="", multiline=True),
|
|
io.String.Input("discard_patterns", default="", multiline=True),
|
|
io.Boolean.Input("glob_patterns", default=False,
|
|
tooltip="When True, exclude/discard patterns use glob syntax (* = any sequence, dots are literal). "
|
|
"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"),
|
|
io.String.Output(display_name="documentation"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, execution_mode: str, model_a: str, model_b: str, model_c: str,
|
|
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, 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)
|
|
|
|
recipe_params = {
|
|
"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, "override_dtype": override_dtype,
|
|
"device": process_device, "dtype": torch.float32,
|
|
"exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns,
|
|
"glob_patterns": glob_patterns,
|
|
"lazy_load": lazy_load, "force_clear_cache": force_clear_cache,
|
|
}
|
|
model_names = {"model_a": model_a, "model_b": model_b, "model_c": model_c}
|
|
filename = MergerLogic.execute_merge(model_names, calc_mode, THREE_MODEL_MODES, recipe_params, cls.MODEL_TYPE)
|
|
return io.NodeOutput(filename, doc)
|
|
|
|
|
|
class ModelThreeMerger(io.ComfyNode):
|
|
MODEL_TYPE = "diffusion_models"
|
|
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return io.Schema(
|
|
node_id="ModelThreeMerger",
|
|
display_name="Merge Models (3 Models)",
|
|
category="ModelUtils/Merging",
|
|
inputs=[
|
|
io.Combo.Input("execution_mode", options=["MERGE", "DOCUMENTATION ONLY"]),
|
|
io.Combo.Input("model_a", options=["None"] + folder_paths.get_filename_list("diffusion_models")),
|
|
io.Combo.Input("model_b", options=["None"] + folder_paths.get_filename_list("diffusion_models")),
|
|
io.Combo.Input("model_c", options=["None"] + folder_paths.get_filename_list("diffusion_models")),
|
|
io.Combo.Input("calc_mode", options=[m.name for m in THREE_MODEL_MODES]),
|
|
io.Combo.Input("mismatch_mode", options=["skip", "zeros", "error"], default="skip"),
|
|
io.Combo.Input("alignment_mode", options=["pad/crop", "interpolate"], default="pad/crop"),
|
|
io.Float.Input("alpha", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("beta", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("gamma", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("delta", default=2.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("epsilon", default=0.01, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("zeta", default=0.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
|
|
io.String.Input("output_filename", default="merged_3_model"),
|
|
io.Combo.Input("save_dtype", options=["fp32", "fp16", "bf16"]),
|
|
io.Combo.Input("process_device", options=["cuda", "cpu"]),
|
|
io.String.Input("exclude_patterns", default="", multiline=True),
|
|
io.String.Input("discard_patterns", default="", multiline=True),
|
|
io.Boolean.Input("glob_patterns", default=False,
|
|
tooltip="When True, exclude/discard patterns use glob syntax (* = any sequence, dots are literal). "
|
|
"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"),
|
|
io.String.Output(display_name="documentation"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, execution_mode: str, model_a: str, model_b: str, model_c: str,
|
|
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, 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)
|
|
|
|
recipe_params = {
|
|
"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, "override_dtype": override_dtype,
|
|
"device": process_device, "dtype": torch.float32,
|
|
"exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns,
|
|
"glob_patterns": glob_patterns,
|
|
"lazy_load": lazy_load, "force_clear_cache": force_clear_cache,
|
|
}
|
|
model_names = {"model_a": model_a, "model_b": model_b, "model_c": model_c}
|
|
filename = MergerLogic.execute_merge(model_names, calc_mode, THREE_MODEL_MODES, recipe_params, cls.MODEL_TYPE)
|
|
return io.NodeOutput(filename, doc)
|
|
|
|
|
|
class TextEncoderThreeMerger(io.ComfyNode):
|
|
MODEL_TYPE = "text_encoders"
|
|
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return io.Schema(
|
|
node_id="TextEncoderThreeMerger",
|
|
display_name="Merge Text Encoders (3 Models)",
|
|
category="ModelUtils/Merging",
|
|
inputs=[
|
|
io.Combo.Input("execution_mode", options=["MERGE", "DOCUMENTATION ONLY"]),
|
|
io.Combo.Input("model_a", options=["None"] + folder_paths.get_filename_list("text_encoders")),
|
|
io.Combo.Input("model_b", options=["None"] + folder_paths.get_filename_list("text_encoders")),
|
|
io.Combo.Input("model_c", options=["None"] + folder_paths.get_filename_list("text_encoders")),
|
|
io.Combo.Input("calc_mode", options=[m.name for m in THREE_MODEL_MODES]),
|
|
io.Combo.Input("mismatch_mode", options=["skip", "zeros", "error"], default="skip"),
|
|
io.Combo.Input("alignment_mode", options=["pad/crop", "interpolate"], default="pad/crop"),
|
|
io.Float.Input("alpha", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("beta", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("gamma", default=0.99, min=-10.0, max=10.0, step=0.001),
|
|
io.Float.Input("delta", default=2.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("epsilon", default=0.01, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("zeta", default=0.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
|
|
io.String.Input("output_filename", default="merged_3_textencoder"),
|
|
io.Combo.Input("save_dtype", options=["fp32", "fp16", "bf16"]),
|
|
io.Combo.Input("process_device", options=["cuda", "cpu"]),
|
|
io.String.Input("exclude_patterns", default="", multiline=True),
|
|
io.String.Input("discard_patterns", default="", multiline=True),
|
|
io.Boolean.Input("glob_patterns", default=False,
|
|
tooltip="When True, exclude/discard patterns use glob syntax (* = any sequence, dots are literal). "
|
|
"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"),
|
|
io.String.Output(display_name="documentation"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, execution_mode: str, model_a: str, model_b: str, model_c: str,
|
|
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, 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)
|
|
|
|
recipe_params = {
|
|
"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, "override_dtype": override_dtype,
|
|
"device": process_device, "dtype": torch.float32,
|
|
"exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns,
|
|
"glob_patterns": glob_patterns,
|
|
"lazy_load": lazy_load, "force_clear_cache": force_clear_cache,
|
|
}
|
|
model_names = {"model_a": model_a, "model_b": model_b, "model_c": model_c}
|
|
filename = MergerLogic.execute_merge(model_names, calc_mode, THREE_MODEL_MODES, recipe_params, cls.MODEL_TYPE)
|
|
return io.NodeOutput(filename, doc)
|
|
|
|
|
|
class LoRAThreeMerger(io.ComfyNode):
|
|
MODEL_TYPE = "loras"
|
|
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return io.Schema(
|
|
node_id="LoRAThreeMerger",
|
|
display_name="Merge LoRAs (3 Models)",
|
|
category="ModelUtils/Merging",
|
|
inputs=[
|
|
io.Combo.Input("execution_mode", options=["MERGE", "DOCUMENTATION ONLY"]),
|
|
io.Combo.Input("model_a", options=["None"] + folder_paths.get_filename_list("loras")),
|
|
io.Combo.Input("model_b", options=["None"] + folder_paths.get_filename_list("loras")),
|
|
io.Combo.Input("model_c", options=["None"] + folder_paths.get_filename_list("loras")),
|
|
io.Combo.Input("calc_mode", options=[m.name for m in THREE_MODEL_MODES]),
|
|
io.Combo.Input("mismatch_mode", options=["skip", "zeros", "error"], default="skip"),
|
|
io.Combo.Input("alignment_mode", options=["pad/crop", "interpolate"], default="pad/crop"),
|
|
io.Float.Input("alpha", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("beta", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("gamma", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("delta", default=2.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("epsilon", default=0.01, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("zeta", default=0.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
|
|
io.String.Input("output_filename", default="merged_3_lora"),
|
|
io.Combo.Input("save_dtype", options=["fp32", "fp16", "bf16"]),
|
|
io.Combo.Input("process_device", options=["cuda", "cpu"]),
|
|
io.String.Input("exclude_patterns", default="", multiline=True),
|
|
io.String.Input("discard_patterns", default="", multiline=True),
|
|
io.Boolean.Input("glob_patterns", default=False,
|
|
tooltip="When True, exclude/discard patterns use glob syntax (* = any sequence, dots are literal). "
|
|
"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 merged non-1D tensors to save_dtype. Enabled 1D direct diffs remain FP32."),
|
|
io.Boolean.Input("include_1d_diffs", default=False,
|
|
tooltip="Merge 1D tensors. When disabled, preserve Model A's 1D tensors unchanged."),
|
|
],
|
|
outputs=[
|
|
io.String.Output(display_name="output_filename"),
|
|
io.String.Output(display_name="documentation"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, execution_mode: str, model_a: str, model_b: str, model_c: str,
|
|
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, override_dtype: bool,
|
|
include_1d_diffs: 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)
|
|
|
|
recipe_params = {
|
|
"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, "override_dtype": override_dtype,
|
|
"device": process_device, "dtype": torch.float32,
|
|
"exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns,
|
|
"glob_patterns": glob_patterns,
|
|
"lazy_load": lazy_load, "force_clear_cache": force_clear_cache,
|
|
"include_1d_diffs": include_1d_diffs,
|
|
}
|
|
model_names = {"model_a": model_a, "model_b": model_b, "model_c": model_c}
|
|
filename = MergerLogic.execute_merge(model_names, calc_mode, THREE_MODEL_MODES, recipe_params, cls.MODEL_TYPE)
|
|
return io.NodeOutput(filename, doc)
|
|
|
|
|
|
class EmbeddingThreeMerger(io.ComfyNode):
|
|
MODEL_TYPE = "embeddings"
|
|
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return io.Schema(
|
|
node_id="EmbeddingThreeMerger",
|
|
display_name="Merge Embeddings (3 Models)",
|
|
category="ModelUtils/Merging",
|
|
inputs=[
|
|
io.Combo.Input("execution_mode", options=["MERGE", "DOCUMENTATION ONLY"]),
|
|
io.Combo.Input("model_a", options=["None"] + folder_paths.get_filename_list("embeddings")),
|
|
io.Combo.Input("model_b", options=["None"] + folder_paths.get_filename_list("embeddings")),
|
|
io.Combo.Input("model_c", options=["None"] + folder_paths.get_filename_list("embeddings")),
|
|
io.Combo.Input("calc_mode", options=[m.name for m in THREE_MODEL_MODES]),
|
|
io.Combo.Input("mismatch_mode", options=["skip", "zeros", "error"], default="skip"),
|
|
io.Combo.Input("alignment_mode", options=["pad/crop", "interpolate"], default="pad/crop"),
|
|
io.Float.Input("alpha", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("beta", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("gamma", default=0.5, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("delta", default=2.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("epsilon", default=0.01, min=-10.0, max=10.0, step=0.01),
|
|
io.Float.Input("zeta", default=0.0, min=-10.0, max=10.0, step=0.01),
|
|
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
|
|
io.String.Input("output_filename", default="merged_3_embedding"),
|
|
io.Combo.Input("save_dtype", options=["fp32", "fp16", "bf16"]),
|
|
io.Combo.Input("process_device", options=["cuda", "cpu"]),
|
|
io.String.Input("exclude_patterns", default="", multiline=True),
|
|
io.String.Input("discard_patterns", default="", multiline=True),
|
|
io.Boolean.Input("glob_patterns", default=False,
|
|
tooltip="When True, exclude/discard patterns use glob syntax (* = any sequence, dots are literal). "
|
|
"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"),
|
|
io.String.Output(display_name="documentation"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, execution_mode: str, model_a: str, model_b: str, model_c: str,
|
|
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, 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)
|
|
|
|
recipe_params = {
|
|
"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, "override_dtype": override_dtype,
|
|
"device": process_device, "dtype": torch.float32,
|
|
"exclude_patterns": exclude_patterns, "discard_patterns": discard_patterns,
|
|
"glob_patterns": glob_patterns,
|
|
"lazy_load": lazy_load, "force_clear_cache": force_clear_cache,
|
|
}
|
|
model_names = {"model_a": model_a, "model_b": model_b, "model_c": model_c}
|
|
filename = MergerLogic.execute_merge(model_names, calc_mode, THREE_MODEL_MODES, recipe_params, cls.MODEL_TYPE)
|
|
return io.NodeOutput(filename, doc)
|