Updated IAMCCS-nodes to version 1.3.0
This commit is contained in:
@@ -1,3 +1,19 @@
|
||||
## 🆕 Version 1.3.0 — MODEL In→Out LoRA Stack & Qwen Loader Docs
|
||||
|
||||
Date: 2025-11-19
|
||||
|
||||
Changes:
|
||||
- Added new node `IAMCCS_WanLoRAStackModelIO` ("LoRA Stack (Model In→Out) WAN") for direct multi-LoRA application to an incoming MODEL (WAN 2.2 / Flow / Standard).
|
||||
- Preserves WAN key remap + optional chaining via existing `IAMCCS_WanLoRAStack` (use optional `lora` input to extend beyond 4 slots).
|
||||
- Updated `README.md` with explicit low VRAM instructions for Qwen Image LoRA loader and dependency checklist (`ComfyUI-nunchaku`, `ComfyUI-QwenImageLoraLoader`).
|
||||
- Bumped versions (`version.json`, `pyproject.toml`) to 1.3.0.
|
||||
- Neutralized deprecated Save&Load DragCrop code (frontend/backend) — removed from active registration.
|
||||
|
||||
Notes:
|
||||
- Existing workflows using the older two-node stack + apply pattern continue to work unchanged.
|
||||
- Use `IAMCCS_WanLoRAStackModelIO` to simplify WAN 2.2 graphs or reduce node count before samplers.
|
||||
|
||||
---
|
||||
## 🆕 Version 1.2.3 — Stackable LoRA Input
|
||||
|
||||
- Added optional `lora` input to IAMCCS_WanLoRAStack node
|
||||
|
||||
@@ -7,13 +7,47 @@
|
||||
### Category: ComfyUI Custom Nodes
|
||||
### Main Feature: Fix for LoRA loading in native WANAnimate workflows
|
||||
|
||||
Version: 1.2.3
|
||||
Version: 1.3.0
|
||||
|
||||
# UPDATE VERSION 1-2-3
|
||||
# UPDATE VERSION 1-3-0
|
||||
|
||||
## 🆕 Version 1.2.3 — New input lora - add another StackLoraModel (concatenate) + Extended Wan 2.1 Compatibility
|
||||
## 🆕 Version 1.3.0 — New MODEL IO LoRA Stack + Qwen Loader Instructions
|
||||
|
||||
Version: 1.2.1
|
||||
Highlights:
|
||||
- Added `LoRA Stack (Model In→Out) WAN` node: directly applies up to 4 WAN / Flow / Standard LoRAs to an incoming MODEL and outputs a patched MODEL (ideal for WAN 2.2 workflows where a single node step is preferred).
|
||||
- Qwen Image LoRA (IAMCCS QwenImgLoadFix) – Updated for 1.3.0: a fixed Qwen Image LoRA loader with improved offload controls and UX.
|
||||
- Extended internal WAN key remapping for seamless WAN 2.2 (Flow) + WAN 2.1 cross-compatibility.
|
||||
- Documentation updated with explicit Qwen Image LoRA loader prerequisites for low VRAM users (nunchaku based).
|
||||
- Version bump across project files.
|
||||
|
||||
### New Node: LoRA Stack (Model In→Out) WAN
|
||||
|
||||
lora_stack_model_I_O.png
|
||||
|
||||
Use this node when you already have a base MODEL loaded (WAN 2.2, Flow, SDXL, etc.) and want a single pass application of multiple LoRAs without an intermediate stack/output hand-off. It mirrors the behavior of the classic stack + apply pair but merges them for simpler graphs (especially animation or chained sampler pipelines).
|
||||
|
||||
Inputs:
|
||||
- `model`: base diffusion MODEL.
|
||||
- `lora1..lora4` + `strength1..strength4` (skips if "no" or strength == 0.0)
|
||||
- `model_type`: choose `flow`, `wan2x`, or `standard` to control remapping logic.
|
||||
- Optional `lora` (LORA) input: allows concatenating a previously built stack from `IAMCCS_WanLoRAStack` for more than 4 total LoRAs.
|
||||
|
||||
Output:
|
||||
- Patched `MODEL` ready for samplers / video pipelines.
|
||||
|
||||
Recommended Use (WAN 2.2 workflows):
|
||||
1. Load base WAN 2.2 / LightX2V model.
|
||||
2. Add `LoRA Stack (Model In→Out) WAN` and select up to 4 LoRAs.
|
||||
3. (Optional) Chain a classic `IAMCCS_WanLoRAStack` into the optional `lora` input if you need >4.
|
||||
4. Connect output to KSampler / Animate nodes.
|
||||
|
||||
Why this node: Eliminates one extra node hop, reduces graph complexity and clarifies model lineage in large animation workflows.
|
||||
|
||||
## Previous Versions
|
||||
|
||||
### Version 1.2.3 — New input lora - add another StackLoraModel (concatenate) + Extended Wan 2.1 Compatibility
|
||||
|
||||
### Version 1.2.1
|
||||
|
||||
# UPDATE VERSION 1-2-1
|
||||
|
||||
@@ -71,7 +105,7 @@ Ideal for WANAnimate, WANVideo, or any Flow-based cinematic model.
|
||||
|
||||

|
||||
|
||||
# New version 1.2.3!! Lora concatenate!! You can add another Lora stack to the node!
|
||||
# LoRA Concatenation (1.2.3)
|
||||
|
||||
lora_concatenatel.png
|
||||
|
||||
@@ -128,3 +162,58 @@ This modular architecture makes LoRA management in WANAnimate flexible, transpar
|
||||
<a href="https://www.buymeacoffee.com/iamccs" target="_blank">
|
||||
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="200" />
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## Qwen Image LoRA (IAMCCS QwenImgLoadFix) – Updated for 1.3.0
|
||||
|
||||
This repo ships a fixed Qwen Image LoRA loader with improved offload controls and UX.
|
||||
|
||||
LORAQWEN.png
|
||||
|
||||
### Prerequisites (Low VRAM Friendly)
|
||||
Required:
|
||||
- ComfyUI (≥ 0.3.0)
|
||||
- `ComfyUI-nunchaku` (Qwen Image / Sana transformer implementation)
|
||||
- `ComfyUI-QwenImageLoraLoader` (wrappers.qwenimage module with `ComfyQwenImageWrapper`)
|
||||
- Qwen Image model weights placed as per nunchaku loader instructions
|
||||
- LoRA files (`.safetensors`) in `ComfyUI/models/loras`
|
||||
|
||||
Recommended for low VRAM systems:
|
||||
- Enable `offload_auto_tune` (auto scales transformer block residency)
|
||||
- Set `offload_policy=disable` only if you experience instability during composition (keeps everything on GPU while LoRAs active)
|
||||
- Keep `offload_num_blocks_on_gpu` low (1–2) if VRAM < 12 GB
|
||||
|
||||
### Node Overview
|
||||
`IAMCCS QwenImgLoadFix`
|
||||
- Wraps a `NunchakuQwenImageTransformer2DModel` / `NunchakuSanaTransformer2DModel` if not already wrapped.
|
||||
- Adds LoRA via internal wrapper (`model_wrapper.loras.append(...)`) with strength preset.
|
||||
- Supports composition modes:
|
||||
- `append`: legacy shape-changing approach
|
||||
- `merge_v2`: in-place delta merge (preferred; no rank expansion)
|
||||
- Offload Controls: dynamic, rebuild or disable strategies; pin-memory toggle; auto-tune; VRAM margin.
|
||||
- Device Safety: ensures params & buffers migrate to correct CUDA device before forward.
|
||||
|
||||
|
||||
|
||||
### Dependency Checklist
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| ComfyUI-nunchaku | Provides transformer class loaded by base model node |
|
||||
| ComfyUI-QwenImageLoraLoader | Supplies `wrappers/qwenimage.py` used for wrapping |
|
||||
| IAMCCS-nodes | Adds fixed loader + WAN LoRA stack system |
|
||||
| LoRA safetensors | User-provided style/character adapters |
|
||||
|
||||
If `wrappers/qwenimage.py` is not found the node will raise an import error. Ensure `ComfyUI-QwenImageLoraLoader` repository resides in `custom_nodes/`.
|
||||
|
||||
### Quick Start (Low VRAM Scenario)
|
||||
1. Load Qwen Image model (nunchaku loader node).
|
||||
2. Place `IAMCCS QwenImgLoadFix` directly after it.
|
||||
3. Select `lora_name` & preset (start with 0.76–1.00 for natural balance).
|
||||
4. Set `composition_mode=merge_v2` unless you need legacy behavior.
|
||||
5. Keep `offload_auto_tune=ON`; reduce `offload_num_blocks_on_gpu` if memory errors occur.
|
||||
6. Run sampler; adjust preset or switch to a higher strength if effect too weak.
|
||||
|
||||
### External Requirements Recap
|
||||
This loader will not function standalone — both `ComfyUI-nunchaku` and `ComfyUI-QwenImageLoraLoader` must be installed, and the Qwen Image weights must be correctly placed. The IAMCCS node auto-wraps only if it detects a supported transformer class. If wrapping fails, verify repository folder names match expected conventions.
|
||||
|
||||
|
||||
+24
-2
@@ -1,19 +1,41 @@
|
||||
# ==========================================================
|
||||
# __init__.py — Registro nodi IAMCCS LoRA
|
||||
# Versione pulita: mantiene solo i nodi principali
|
||||
# __init__.py — Registro nodi IAMCCS
|
||||
# Versione estesa: include LoRA + Qwen Bridge Conditioning
|
||||
# ==========================================================
|
||||
|
||||
# Apply safety monkeypatches on import (no-op if target not present)
|
||||
from . import iamccs_qwen_monkeypatch # noqa: F401
|
||||
|
||||
from .iamccs_wan_lora_stack import (
|
||||
IAMCCS_WanLoRAStack,
|
||||
IAMCCS_ModelWithLoRA,
|
||||
)
|
||||
from .iamccs_wan_lora_stack_simple import (
|
||||
IAMCCS_WanLoRAStackModelIO,
|
||||
)
|
||||
|
||||
|
||||
# Qwen Image LoRA loader (fixed copy)
|
||||
from .iamccs_qwen_lora_loader import (
|
||||
IAMCCS_QwenImageLoraLoader,
|
||||
)
|
||||
|
||||
# Nodi principali
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"IAMCCS_WanLoRAStack": IAMCCS_WanLoRAStack,
|
||||
"IAMCCS_ModelWithLoRA": IAMCCS_ModelWithLoRA,
|
||||
"IAMCCS_WanLoRAStackModelIO": IAMCCS_WanLoRAStackModelIO,
|
||||
"IAMCCS_qwenloraloader": IAMCCS_QwenImageLoraLoader,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"IAMCCS_WanLoRAStack": "LoRA Stack (WAN-style remap)",
|
||||
"IAMCCS_ModelWithLoRA": "Apply LoRA to MODEL (Native)",
|
||||
"IAMCCS_WanLoRAStackModelIO": "LoRA Stack (Model In→Out) WAN",
|
||||
"IAMCCS_qwenloraloader": "IAMCCS QwenImgLoraLoaderFix",
|
||||
}
|
||||
|
||||
# Web directory for JavaScript extensions
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
IAMCCS Qwen Image LoRA Loader/Stack
|
||||
A Nunchaku Qwen Image LoRA loader node
|
||||
compatible with current `nunchaku` where Qwen image transformer is aliased
|
||||
as `NunchakuSanaTransformer2DModel`.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Support both old/new nunchaku class names (alias to a common name)
|
||||
try:
|
||||
from nunchaku import NunchakuQwenImageTransformer2DModel
|
||||
except Exception:
|
||||
from nunchaku import NunchakuSanaTransformer2DModel as NunchakuQwenImageTransformer2DModel
|
||||
|
||||
import folder_paths
|
||||
|
||||
# Logging
|
||||
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
logging.basicConfig(level=getattr(logging, log_level, logging.INFO), format="%(asctime)s - %(levelname)s - %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_wrappers_module():
|
||||
"""Dynamically load wrappers.qwenimage from ComfyUI-QwenImageLoraLoader."""
|
||||
import importlib.util
|
||||
# Try to locate the sibling custom node folder
|
||||
# typical structure: .../custom_nodes/ComfyUI-QwenImageLoraLoader
|
||||
base_custom_nodes = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
qwen_node_dir = os.path.join(base_custom_nodes, "ComfyUI-QwenImageLoraLoader")
|
||||
|
||||
# Fallback: if running outside expected layout, try current sys.path entries
|
||||
candidate_dirs = [qwen_node_dir] + [p for p in sys.path if isinstance(p, str) and p.endswith("ComfyUI-QwenImageLoraLoader")]
|
||||
|
||||
wrappers_path = None
|
||||
for d in candidate_dirs:
|
||||
wp = os.path.join(d, "wrappers", "qwenimage.py")
|
||||
if os.path.exists(wp):
|
||||
wrappers_path = wp
|
||||
break
|
||||
|
||||
if not wrappers_path:
|
||||
raise ImportError("Cannot locate ComfyUI-QwenImageLoraLoader/wrappers/qwenimage.py")
|
||||
|
||||
spec = importlib.util.spec_from_file_location("wrappers.qwenimage", wrappers_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"Failed to load module spec for {wrappers_path}")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod) # type: ignore[attr-defined]
|
||||
return mod
|
||||
|
||||
|
||||
class IAMCCS_QwenImageLoraLoader:
|
||||
"""
|
||||
Load and apply a single LoRA to a Nunchaku Qwen Image model.
|
||||
"""
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
"""ComfyUI calls IS_CHANGED with widget values only; avoid positional args warnings.
|
||||
Build a hash from model reference (stringified) and LoRA parameters.
|
||||
"""
|
||||
import hashlib
|
||||
m = hashlib.sha256()
|
||||
model = kwargs.get("model")
|
||||
if model is not None:
|
||||
m.update(str(model).encode())
|
||||
lora_name = kwargs.get("lora_name", "")
|
||||
m.update(lora_name.encode())
|
||||
preset = str(kwargs.get("lora_strength_preset", "1.00"))
|
||||
m.update(preset.encode())
|
||||
return m.hexdigest()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model": (
|
||||
"MODEL",
|
||||
{
|
||||
"tooltip": "The diffusion model the LoRA will be applied to. Make sure the model is loaded by a Nunchaku Qwen Image loader.",
|
||||
},
|
||||
),
|
||||
"lora_name": (
|
||||
folder_paths.get_filename_list("loras"),
|
||||
{"tooltip": "The file name of the LoRA."},
|
||||
),
|
||||
"lora_strength_preset": (
|
||||
["0.25", "0.50", "0.76", "1.00", "1.25", "1.50"],
|
||||
{
|
||||
"default": "1.00",
|
||||
"tooltip": "Preset strength values.",
|
||||
},
|
||||
),
|
||||
"composition_mode": (
|
||||
["append", "merge_v2"],
|
||||
{
|
||||
"default": "merge_v2",
|
||||
"tooltip": "How to apply LoRAs: 'append' (original behavior, may change shapes) or 'merge_v2' (in-place delta, no rank expansion).",
|
||||
},
|
||||
),
|
||||
"offload_policy": (
|
||||
["rebuild", "disable"],
|
||||
{
|
||||
"default": "rebuild",
|
||||
"tooltip": "CPU offload with LoRAs: 'rebuild' re-enables offload after composing; 'disable' keeps it off while LoRAs are active.",
|
||||
},
|
||||
),
|
||||
"offload_num_blocks_on_gpu": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 64,
|
||||
"step": 1,
|
||||
"tooltip": "How many transformer blocks stay on GPU when offload is enabled (higher = more VRAM, more speed).",
|
||||
},
|
||||
),
|
||||
"offload_use_pin_memory": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": False,
|
||||
"tooltip": "Use pinned host memory for offload transfers (can improve bandwidth, uses more system RAM).",
|
||||
},
|
||||
),
|
||||
"offload_auto_tune": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": True,
|
||||
"tooltip": "Automatically choose offload settings based on free VRAM (overrides saved settings).",
|
||||
},
|
||||
),
|
||||
"vram_margin_gb": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 4.0,
|
||||
"min": 0.0,
|
||||
"max": 16.0,
|
||||
"step": 0.25,
|
||||
"tooltip": "VRAM margin used when cpu_offload_setting='auto' to decide enabling offload for composition.",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
OUTPUT_TOOLTIPS = ("The modified diffusion model.",)
|
||||
FUNCTION = "load_lora"
|
||||
TITLE = "IAMCCS QwenImgLoraLoaderFix"
|
||||
CATEGORY = "IAMCCS/Nunchaku"
|
||||
DESCRIPTION = "Apply a single LoRA to a Nunchaku Qwen Image model."
|
||||
|
||||
def load_lora(self, model, lora_name: str, lora_strength_preset: str = "1.00", composition_mode: str = "merge_v2", offload_policy: str = "rebuild", offload_num_blocks_on_gpu: int = 1, offload_use_pin_memory: bool = False, offload_auto_tune: bool = True, vram_margin_gb: float = 4.0):
|
||||
# Resolve effective strength from preset or default
|
||||
import math
|
||||
strength_val: float = 1.0
|
||||
try:
|
||||
strength_val = float(lora_strength_preset)
|
||||
except Exception:
|
||||
strength_val = 1.0
|
||||
if not math.isfinite(strength_val):
|
||||
strength_val = 1.0
|
||||
if abs(strength_val) < 1e-5:
|
||||
return (model,)
|
||||
|
||||
# Coerce offload_num_blocks_on_gpu to a sane integer (avoid NaN/None/inf)
|
||||
import math as _math
|
||||
try:
|
||||
_tmp_val = float(offload_num_blocks_on_gpu)
|
||||
if not _math.isfinite(_tmp_val):
|
||||
_tmp_val = 1.0
|
||||
if _tmp_val < 1:
|
||||
_tmp_val = 1.0
|
||||
if _tmp_val > 64:
|
||||
_tmp_val = 64.0
|
||||
offload_num_blocks_on_gpu = int(_tmp_val)
|
||||
except Exception:
|
||||
offload_num_blocks_on_gpu = 1
|
||||
|
||||
# Advanced toggle removed: always respect user-provided widget values
|
||||
|
||||
model_wrapper = model.model.diffusion_model
|
||||
|
||||
wrappers_module = _get_wrappers_module()
|
||||
ComfyQwenImageWrapper = wrappers_module.ComfyQwenImageWrapper
|
||||
|
||||
# Debug logging
|
||||
model_wrapper_type_name = type(model_wrapper).__name__
|
||||
model_wrapper_module = type(model_wrapper).__module__
|
||||
logger.info(f"🔍 Model wrapper type: '{model_wrapper_type_name}'")
|
||||
logger.info(f"🔍 Model wrapper module: {model_wrapper_module}")
|
||||
|
||||
if hasattr(model_wrapper, 'model') and hasattr(model_wrapper, 'loras'):
|
||||
logger.info("✅ Model is already wrapped (detected via attributes)")
|
||||
transformer = model_wrapper.model
|
||||
elif (
|
||||
model_wrapper_type_name in ("NunchakuQwenImageTransformer2DModel", "NunchakuSanaTransformer2DModel")
|
||||
or model_wrapper_type_name.endswith("NunchakuQwenImageTransformer2DModel")
|
||||
or model_wrapper_type_name.endswith("NunchakuSanaTransformer2DModel")
|
||||
):
|
||||
logger.info("🔧 Wrapping Nunchaku*Qwen/Sana* Transformer with ComfyQwenImageWrapper")
|
||||
wrapped_model = ComfyQwenImageWrapper(
|
||||
model_wrapper,
|
||||
getattr(model_wrapper, 'config', {}),
|
||||
None,
|
||||
{},
|
||||
"auto",
|
||||
vram_margin_gb,
|
||||
lora_offload_policy=offload_policy,
|
||||
offload_num_blocks_on_gpu=offload_num_blocks_on_gpu,
|
||||
offload_use_pin_memory=offload_use_pin_memory,
|
||||
offload_auto_tune=offload_auto_tune,
|
||||
)
|
||||
# Forward composition mode flag (attribute-based to avoid strict kwargs requirements)
|
||||
try:
|
||||
setattr(wrapped_model, "lora_composition_mode", composition_mode)
|
||||
except Exception:
|
||||
pass
|
||||
model.model.diffusion_model = wrapped_model
|
||||
model_wrapper = wrapped_model
|
||||
transformer = model_wrapper.model
|
||||
else:
|
||||
logger.error(f"❌ Model type mismatch! Type: {model_wrapper_type_name}, Module: {model_wrapper_module}")
|
||||
raise TypeError(
|
||||
f"This LoRA loader works with Nunchaku Qwen Image models; got {model_wrapper_type_name}."
|
||||
)
|
||||
|
||||
# Remove expensive deepcopy (caused device divergence under offload); mutate in place
|
||||
lora_path = folder_paths.get_full_path_or_raise("loras", lora_name)
|
||||
try:
|
||||
setattr(model_wrapper, "lora_composition_mode", composition_mode)
|
||||
except Exception:
|
||||
pass
|
||||
model_wrapper.loras.append((lora_path, strength_val))
|
||||
|
||||
# Ensure wrapper model resides fully on the original device (avoid mixed cpu/cuda modules)
|
||||
try:
|
||||
target_device = next(transformer.parameters()).device
|
||||
if target_device.type == "cuda":
|
||||
for p in transformer.parameters():
|
||||
if p.device != target_device:
|
||||
p.data = p.data.to(target_device)
|
||||
for b in transformer.buffers():
|
||||
if b.device != target_device:
|
||||
b.data = b.data.to(target_device)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(f"LoRA added: {lora_name} (strength={strength_val})")
|
||||
return (model,)
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import logging
|
||||
|
||||
|
||||
def _patch_qwen_wrapper_device_sync():
|
||||
try:
|
||||
from importlib import import_module
|
||||
mod = import_module("ComfyUI-QwenImageLoraLoader.wrappers.qwenimage")
|
||||
ComfyQwenImageWrapper = getattr(mod, "ComfyQwenImageWrapper", None)
|
||||
if ComfyQwenImageWrapper is None:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if getattr(ComfyQwenImageWrapper, "_iamccs_device_sync_patched", False):
|
||||
return True
|
||||
|
||||
orig_execute = getattr(ComfyQwenImageWrapper, "_execute_model", None)
|
||||
if orig_execute is None:
|
||||
return False
|
||||
|
||||
def _execute_model_patched(self, x, timestep, context, guidance, control, transformer_options, **kwargs):
|
||||
# Ensure all params/buffers are on the same device as input x (prevents CPU/CUDA mix)
|
||||
try:
|
||||
dev = getattr(x, "device", None)
|
||||
if dev is not None and dev.type in ("cuda", "cpu") and hasattr(self, "model") and self.model is not None:
|
||||
needs_move = False
|
||||
# Quick scan: if any param/buffer is on a different device, move whole model once
|
||||
for _, p in self.model.named_parameters(recurse=True):
|
||||
if p.device.type != dev.type:
|
||||
needs_move = True
|
||||
break
|
||||
if not needs_move:
|
||||
for _, b in self.model.named_buffers(recurse=True):
|
||||
if b.device.type != dev.type:
|
||||
needs_move = True
|
||||
break
|
||||
if needs_move:
|
||||
try:
|
||||
self.model.to(dev)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
return orig_execute(self, x, timestep, context, guidance, control, transformer_options, **kwargs)
|
||||
|
||||
setattr(ComfyQwenImageWrapper, "_execute_model", _execute_model_patched)
|
||||
setattr(ComfyQwenImageWrapper, "_iamccs_device_sync_patched", True)
|
||||
logging.getLogger(__name__).info("[IAMCCS] Applied device-sync monkeypatch to ComfyQwenImageWrapper")
|
||||
return True
|
||||
|
||||
|
||||
# Execute at import time
|
||||
try:
|
||||
_patch_qwen_wrapper_device_sync()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,112 @@
|
||||
# iamccs_wan_lora_stack_simple.py
|
||||
# ===============================================================
|
||||
# IAMCCS_WanLoRAStackModelIO
|
||||
# Multi-LoRA loader (WAN-style remap) that takes MODEL in and outputs MODEL
|
||||
# ===============================================================
|
||||
|
||||
import logging
|
||||
import comfy.utils
|
||||
import comfy.sd
|
||||
import folder_paths
|
||||
|
||||
from .iamccs_wan_lora_stack import (
|
||||
standardize_wan_lora_keys,
|
||||
SuppressOptionalKeysFilter,
|
||||
)
|
||||
|
||||
|
||||
class IAMCCS_WanLoRAStackModelIO:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
lora_list = folder_paths.get_filename_list("loras") + ["no"]
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"lora1": (lora_list, {"default": "no"}),
|
||||
"strength1": ("FLOAT", {"default": 1.0, "min": -5.0, "max": 5.0, "step": 0.01}),
|
||||
"lora2": (lora_list, {"default": "no"}),
|
||||
"strength2": ("FLOAT", {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}),
|
||||
"lora3": (lora_list, {"default": "no"}),
|
||||
"strength3": ("FLOAT", {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}),
|
||||
"lora4": (lora_list, {"default": "no"}),
|
||||
"strength4": ("FLOAT", {"default": 0.0, "min": -5.0, "max": 5.0, "step": 0.01}),
|
||||
"model_type": (["wan2x", "flow", "standard"], {"default": "flow"}),
|
||||
},
|
||||
"optional": {
|
||||
# Allow chaining in externally prepared LORA stacks if provided (optional)
|
||||
"lora": ("LORA",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
FUNCTION = "apply_stack"
|
||||
CATEGORY = "IAMCCS/LoRA"
|
||||
|
||||
def _build_lora_entries(self, lora1, strength1, lora2, strength2, lora3, strength3, lora4, strength4, model_type):
|
||||
loras = []
|
||||
for name, strength in [
|
||||
(lora1, strength1),
|
||||
(lora2, strength2),
|
||||
(lora3, strength3),
|
||||
(lora4, strength4),
|
||||
]:
|
||||
if not name or name == "no" or strength == 0.0:
|
||||
continue
|
||||
path = folder_paths.get_full_path_or_raise("loras", name)
|
||||
sd = comfy.utils.load_torch_file(path, safe_load=True)
|
||||
if model_type != "standard":
|
||||
sd = standardize_wan_lora_keys(sd)
|
||||
loras.append({"name": name, "strength": strength, "state_dict": sd})
|
||||
return loras
|
||||
|
||||
def apply_stack(self, model,
|
||||
lora1, strength1,
|
||||
lora2, strength2,
|
||||
lora3, strength3,
|
||||
lora4, strength4,
|
||||
model_type="flow",
|
||||
lora=None):
|
||||
model_out = model
|
||||
|
||||
loras = self._build_lora_entries(
|
||||
lora1, strength1,
|
||||
lora2, strength2,
|
||||
lora3, strength3,
|
||||
lora4, strength4,
|
||||
model_type,
|
||||
)
|
||||
|
||||
if lora is not None and isinstance(lora, list):
|
||||
loras.extend(lora)
|
||||
|
||||
if not loras:
|
||||
logging.warning("[IAMCCS_WanLoRAStackModelIO] ⚠ No LoRA selected; returning input model unchanged")
|
||||
return (model_out,)
|
||||
|
||||
logger = logging.getLogger()
|
||||
optional_filter = SuppressOptionalKeysFilter()
|
||||
logger.addFilter(optional_filter)
|
||||
|
||||
try:
|
||||
for entry in loras:
|
||||
sd = entry["state_dict"]
|
||||
strength = entry["strength"]
|
||||
model_out, _ = comfy.sd.load_lora_for_models(model_out, None, sd, strength, 0)
|
||||
logging.info(f"[IAMCCS_WanLoRAStackModelIO] ✅ '{entry['name']}' strength={strength}")
|
||||
|
||||
if optional_filter.suppressed_count > 0:
|
||||
keys_types = ", ".join(sorted(optional_filter.suppressed_keys))
|
||||
logging.info(f"[IAMCCS_WanLoRAStackModelIO] ℹ {optional_filter.suppressed_count} optional keys not present in LORA ({keys_types})")
|
||||
finally:
|
||||
logger.removeFilter(optional_filter)
|
||||
|
||||
return (model_out,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"IAMCCS_WanLoRAStackModelIO": IAMCCS_WanLoRAStackModelIO,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"IAMCCS_WanLoRAStackModelIO": "LoRA Stack (Model In→Out) WAN",
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "iamccs-wan-lora-fixer-stack"
|
||||
version = "1.2.3"
|
||||
description = "Nodo IAMCCS per ComfyUI che applica fino a 4 LoRA (WAN 2.1 / 2.2 compatibili) con fix automatico e input/output MODEL. Aggiornamento v1.2.3: ADD Lora input for Lora stack adding."
|
||||
version = "1.3.0"
|
||||
description = "IAMCCS multi-LoRA stack & MODEL IO WAN remap + Qwen Image LoRA loader improvements. v1.3.0 adds direct MODEL in→out LoRA stack and updated low VRAM Qwen instructions."
|
||||
license = { text = "MIT" }
|
||||
authors = [
|
||||
{ name = "Carmine Cristallo Scalzi (IAMCCS)", email = "info@carminecristalloscalzi.com" }
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "iamccs-wan-lora-fixer-stack",
|
||||
"version": "1.2.3",
|
||||
"version": "1.3.0",
|
||||
"author": "Carmine Cristallo Scalzi (IAMCCS)",
|
||||
"description": "Nodo IAMCCS per ComfyUI che applica fino a 8 LoRA in stack (concatenabili), compatibile con WAN 2.2 (Flow). Aggiornamento per compatibilità estesa a WAN 2.1."
|
||||
"description": "IAMCCS nodes for ComfyUI: multi-LoRA stack + direct MODEL IO WAN/Flow remap, Qwen Image LoRA loader improvements. v1.3.0 adds MODEL in→out stack node and updated low VRAM Qwen instructions. WAN 2.1 + 2.2 compatible."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user