From f23bb9b0c52a2f294b1dff5bfb5a48c392e81552 Mon Sep 17 00:00:00 2001 From: Clybius Date: Fri, 26 Jun 2026 10:44:18 -0500 Subject: [PATCH] Feat: Add ClybAdaptiveLoraLoader This node loads multiple LoRAs in one action to prevent re-quantizing the model in Comfy. Good for quantized formats like FP8, Int8, and NVFP4. --- __init__.py | 17 +++++- clyb_ModelLoader.py | 125 ++++++++++++++++++++++++++++++++++++++++++++ clyb_Samplers.py | 4 +- 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 clyb_ModelLoader.py diff --git a/__init__.py b/__init__.py index 57afc64..99476a8 100644 --- a/__init__.py +++ b/__init__.py @@ -2,23 +2,38 @@ from . import chroma_NAG from . import clyb_Guidance from . import clyb_Samplers from . import clyb_Schedulers +from . import clyb_ModelLoader clyb_Samplers.add_samplers() NODE_CLASS_MAPPINGS = { + # Guidance "ClybGuidance": clyb_Guidance.ClybGuidance, + # Samplers "SamplerClyb_BDF": clyb_Samplers.SamplerClyb_BDF, "SamplerTaylorFlow": clyb_Samplers.SamplerTaylorFlow, "SamplerWrapperCFGPP": clyb_Samplers.SamplerWrapperCFGPP, + # Schedulers "InverseSquaredScheduler": clyb_Schedulers.InverseSquaredScheduler, "PrintSigmas": clyb_Schedulers.PrintSigmas, + # LoraLoaders + "ClybAdaptiveLoraLoader": clyb_ModelLoader.ClybAdaptiveLoraLoader, + "ClybAdaptiveLoraLoaderModelOnly": clyb_ModelLoader.ClybAdaptiveLoraLoaderModelOnly, } NODE_DISPLAY_NAME_MAPPINGS = { + # Guidance "ClybGuidance": "ClybGuidance", + # Samplers "SamplerClyb_BDF": "SamplerClyb_BDF", "SamplerTaylorFlow": "SamplerTaylorFlow", "SamplerWrapperCFGPP": "SamplerWrapperCFGPP", + # Schedulers "InverseSquaredScheduler": "InverseSquaredScheduler", "PrintSigmas": "PrintSigmas", -} \ No newline at end of file + # LoraLoaders + "ClybAdaptiveLoraLoader": "ClybAdaptiveLoraLoader", + "ClybAdaptiveLoraLoaderModelOnly": "ClybAdaptiveLoraLoaderModelOnly", +} + +WEB_DIRECTORY = "./js" diff --git a/clyb_ModelLoader.py b/clyb_ModelLoader.py new file mode 100644 index 0000000..2da8ab9 --- /dev/null +++ b/clyb_ModelLoader.py @@ -0,0 +1,125 @@ +import torch +import logging +import comfy.sd +import folder_paths +import comfy.utils +import comfy.lora +import comfy.lora_convert + + +class ClybAdaptiveLoraLoader: + def __init__(self): + self.loaded_loras = {} + + @classmethod + def INPUT_TYPES(s): + file_list = folder_paths.get_filename_list("loras") + file_list.insert(0, "none") + + inputs = { + "required": { + "model": ("MODEL", {"tooltip": "The diffusion model the LoRA will be applied to."}), + "clip": ("CLIP", {"tooltip": "The CLIP model the LoRA will be applied to."}), + "lora_name_1": (file_list, {"tooltip": "The name of the LoRA."}), + "strength_model_1": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01, "tooltip": "How strongly to modify the diffusion model. This value can be negative."}), + "strength_clip_1": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01, "tooltip": "How strongly to modify the CLIP model. This value can be negative."}), + }, + "optional": {} + } + + for i in range(2, 21): + inputs["optional"][f"lora_name_{i}"] = (file_list, {"default": "none"}) + inputs["optional"][f"strength_model_{i}"] = ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}) + inputs["optional"][f"strength_clip_{i}"] = ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}) + + return inputs + + RETURN_TYPES = ("MODEL", "CLIP") + OUTPUT_TOOLTIPS = ("The modified diffusion model.", "The modified CLIP model.") + FUNCTION = "load_lora" + + CATEGORY = "loaders" + DESCRIPTION = "Apply multiple LoRAs adaptively by merging patches into a single model clone." + EXPERIMENTAL = True + + def load_lora(self, model, clip, **kwargs): + lora_keys = [k for k in kwargs.keys() if k.startswith("lora_name_")] + try: + lora_keys.sort(key=lambda x: int(x.split("_")[-1])) + except: + pass + + model_lora = model.clone() if model is not None else None + clip_lora = clip.clone() if clip is not None else None + + for k in lora_keys: + lora_name = kwargs[k] + if lora_name == "none": + continue + + idx = k.split("_")[-1] + strength_model = kwargs.get(f"strength_model_{idx}", 1.0) + strength_clip = kwargs.get(f"strength_clip_{idx}", 1.0) + + if strength_model == 0 and strength_clip == 0: + continue + + lora_path = folder_paths.get_full_path_or_raise("loras", lora_name) + lora = None + if idx in self.loaded_loras: + if self.loaded_loras[idx][0] == lora_path: + lora = self.loaded_loras[idx][1] + else: + self.loaded_loras.pop(idx, None) + + if lora is None: + lora = comfy.utils.load_torch_file(lora_path, safe_load=True) + self.loaded_loras[idx] = (lora_path, lora) + + key_map = {} + if model_lora is not None: + key_map = comfy.lora.model_lora_keys_unet(model_lora.model, key_map) + if clip_lora is not None: + key_map = comfy.lora.model_lora_keys_clip(clip_lora.cond_stage_model, key_map) + + lora_converted = comfy.lora_convert.convert_lora(lora) + loaded = comfy.lora.load_lora(lora_converted, key_map) + + if model_lora is not None: + model_lora.add_patches(loaded, strength_model) + if clip_lora is not None: + clip_lora.add_patches(loaded, strength_clip) + + return (model_lora, clip_lora) + + +class ClybAdaptiveLoraLoaderModelOnly(ClybAdaptiveLoraLoader): + @classmethod + def INPUT_TYPES(s): + file_list = folder_paths.get_filename_list("loras") + file_list.insert(0, "none") + inputs = { + "required": { + "model": ("MODEL",), + "lora_name_1": (file_list, ), + "strength_model_1": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}), + }, + "optional": {} + } + for i in range(2, 21): + inputs["optional"][f"lora_name_{i}"] = (file_list, {"default": "none"}) + inputs["optional"][f"strength_model_{i}"] = ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}) + + return inputs + + RETURN_TYPES = ("MODEL",) + FUNCTION = "load_lora_model_only" + + def load_lora_model_only(self, model, **kwargs): + new_kwargs = kwargs.copy() + lora_keys = [k for k in kwargs.keys() if k.startswith("lora_name_")] + for k in lora_keys: + idx = k.split("_")[-1] + new_kwargs[f"strength_clip_{idx}"] = 0.0 + + return (self.load_lora(model, None, **new_kwargs)[0],) diff --git a/clyb_Samplers.py b/clyb_Samplers.py index cefa7ba..5c39ff4 100644 --- a/clyb_Samplers.py +++ b/clyb_Samplers.py @@ -124,7 +124,7 @@ def _construct_vandermonde_flow(history, sigma_ref, max_order, device, dtype): return R -def _solve_flow_coefficients(R, h_n, method="equilibration", diag_weight=1.0): +def _solve_flow_coefficients(R, h_n, method="diagonal", diag_weight=1.0): """ Solve for B coefficients using either two-sided equilibration or diagonal-dominant regularization. @@ -676,4 +676,4 @@ class SamplerWrapperCFGPP: "cfgpp", {"sampler": sampler}, ) - return (sampler,) \ No newline at end of file + return (sampler,)