Implement v2.2.3 features: detailed LoRA format detection logs and Qwen V3 toggle UI JS
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
console.log("★★★ z_qwen_lora_dynamic_v3.js: Qwen Image LoRA Stack V3 ★★★");
|
||||
|
||||
const HIDDEN_TAG = "tschide";
|
||||
|
||||
app.registerExtension({
|
||||
name: "nunchaku.qwen_lora_dynamic_v3",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "NunchakuQwenImageLoraStackV3") {
|
||||
nodeType["@visibleLoraCount"] = { type: "number", default: 1, min: 1, max: 10, step: 1 };
|
||||
}
|
||||
},
|
||||
|
||||
nodeCreated(node) {
|
||||
if (node.comfyClass !== "NunchakuQwenImageLoraStackV3") return;
|
||||
|
||||
if (!node.properties) node.properties = {};
|
||||
if (node.properties["visibleLoraCount"] === undefined) node.properties["visibleLoraCount"] = 1;
|
||||
|
||||
// Hide lora_count widget in UI (but keep it for backend)
|
||||
const initialLoraCountWidget = node.widgets?.find(w => w.name === "lora_count");
|
||||
if (initialLoraCountWidget) {
|
||||
if (!initialLoraCountWidget.origType) {
|
||||
initialLoraCountWidget.origType = initialLoraCountWidget.type;
|
||||
initialLoraCountWidget.origComputeSize = initialLoraCountWidget.computeSize;
|
||||
}
|
||||
initialLoraCountWidget.type = HIDDEN_TAG;
|
||||
initialLoraCountWidget.computeSize = () => [0, -4];
|
||||
}
|
||||
|
||||
node.cachedWidgets = {};
|
||||
let cacheReady = false;
|
||||
|
||||
const initCache = () => {
|
||||
if (cacheReady) return;
|
||||
const all = [...node.widgets];
|
||||
|
||||
const loraCountWidget = all.find(w => w.name === "lora_count");
|
||||
if (loraCountWidget) {
|
||||
node.cachedLoraCount = loraCountWidget;
|
||||
if (!loraCountWidget.origType) {
|
||||
loraCountWidget.origType = loraCountWidget.type;
|
||||
loraCountWidget.origComputeSize = loraCountWidget.computeSize;
|
||||
}
|
||||
loraCountWidget.type = HIDDEN_TAG;
|
||||
loraCountWidget.computeSize = () => [0, -4];
|
||||
}
|
||||
|
||||
const cpuOffloadWidget = all.find(w => w.name === "cpu_offload");
|
||||
if (cpuOffloadWidget) node.cachedCpuOffload = cpuOffloadWidget;
|
||||
|
||||
const toggleAllWidget = all.find(w => w.name === "toggle_all");
|
||||
if (toggleAllWidget) node.cachedToggleAll = toggleAllWidget;
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const wEnabled = all.find(w => w.name === `enabled_${i}`);
|
||||
const wName = all.find(w => w.name === `lora_name_${i}`);
|
||||
const wStrength = all.find(w => w.name === `lora_strength_${i}`);
|
||||
if (wEnabled && wName && wStrength) {
|
||||
node.cachedWidgets[i] = [wEnabled, wName, wStrength];
|
||||
wEnabled.type = "toggle";
|
||||
wName.type = "combo";
|
||||
wStrength.type = "number";
|
||||
if (wEnabled.computeSize) delete wEnabled.computeSize;
|
||||
if (wName.computeSize) delete wName.computeSize;
|
||||
if (wStrength.computeSize) delete wStrength.computeSize;
|
||||
}
|
||||
}
|
||||
|
||||
cacheReady = true;
|
||||
};
|
||||
|
||||
const ensureControlWidget = () => {
|
||||
const name = "🔢 LoRA Count";
|
||||
// Remove old button widgets
|
||||
for (let i = node.widgets.length - 1; i >= 0; i--) {
|
||||
const w = node.widgets[i];
|
||||
if (w.name === "🔢 Set LoRA Count" || w.type === "button") {
|
||||
node.widgets.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
let w = node.widgets.find(x => x.name === name);
|
||||
if (!w) {
|
||||
const values = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
|
||||
w = node.addWidget("combo", name, "1", (v) => {
|
||||
const num = parseInt(v);
|
||||
if (!isNaN(num)) {
|
||||
node.properties["visibleLoraCount"] = num;
|
||||
if (node.cachedLoraCount) node.cachedLoraCount.value = num;
|
||||
node.updateLoraSlots();
|
||||
}
|
||||
}, { values });
|
||||
}
|
||||
w.value = node.properties["visibleLoraCount"].toString();
|
||||
if (node.cachedLoraCount) node.cachedLoraCount.value = node.properties["visibleLoraCount"];
|
||||
return w;
|
||||
};
|
||||
|
||||
const ensureToggleAllWidget = () => {
|
||||
if (!node.cachedToggleAll) return null;
|
||||
if (!node.cachedToggleAll.origCallback) node.cachedToggleAll.origCallback = node.cachedToggleAll.callback;
|
||||
node.cachedToggleAll.callback = (value) => {
|
||||
if (node.cachedToggleAll.origCallback) node.cachedToggleAll.origCallback(value);
|
||||
const count = parseInt(node.properties["visibleLoraCount"] || 1);
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const pair = node.cachedWidgets[i];
|
||||
if (pair && pair[0]) pair[0].value = value;
|
||||
}
|
||||
};
|
||||
return node.cachedToggleAll;
|
||||
};
|
||||
|
||||
node.updateLoraSlots = function () {
|
||||
if (!cacheReady) initCache();
|
||||
|
||||
const count = parseInt(this.properties["visibleLoraCount"] || 1);
|
||||
const controlWidget = ensureControlWidget();
|
||||
this.widgets = [controlWidget];
|
||||
|
||||
if (node.cachedLoraCount) {
|
||||
node.cachedLoraCount.type = HIDDEN_TAG;
|
||||
node.cachedLoraCount.computeSize = () => [0, -4];
|
||||
node.cachedLoraCount.value = count;
|
||||
this.widgets.push(node.cachedLoraCount);
|
||||
}
|
||||
|
||||
const toggleAllWidget = ensureToggleAllWidget();
|
||||
if (toggleAllWidget) this.widgets.push(toggleAllWidget);
|
||||
|
||||
if (node.cachedCpuOffload) this.widgets.push(node.cachedCpuOffload);
|
||||
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const pair = this.cachedWidgets[i];
|
||||
if (pair && pair.length >= 3) {
|
||||
const wEnabled = pair[0];
|
||||
const wName = pair[1];
|
||||
const wStrength = pair[2];
|
||||
wEnabled.type = "toggle";
|
||||
wName.type = "combo";
|
||||
wStrength.type = "number";
|
||||
if (wEnabled.computeSize) delete wEnabled.computeSize;
|
||||
if (wName.computeSize) delete wName.computeSize;
|
||||
if (wStrength.computeSize) delete wStrength.computeSize;
|
||||
this.widgets.push(wEnabled, wName, wStrength);
|
||||
}
|
||||
}
|
||||
|
||||
const HEADER_H = 60;
|
||||
const SLOT_H = 80;
|
||||
const TOGGLE_ALL_H = toggleAllWidget ? 30 : 0;
|
||||
const CPU_OFFLOAD_H = node.cachedCpuOffload ? 30 : 0;
|
||||
const PADDING = 20;
|
||||
const targetH = HEADER_H + TOGGLE_ALL_H + CPU_OFFLOAD_H + (count * SLOT_H) + PADDING;
|
||||
this.setSize([this.size[0], targetH]);
|
||||
if (app.canvas) app.canvas.setDirty(true, true);
|
||||
};
|
||||
|
||||
node.onPropertyChanged = function (property, value) {
|
||||
if (property === "visibleLoraCount") {
|
||||
const w = this.widgets.find(x => x.name === "🔢 LoRA Count");
|
||||
if (w) w.value = value.toString();
|
||||
this.updateLoraSlots();
|
||||
}
|
||||
};
|
||||
|
||||
const origOnConfigure = node.onConfigure;
|
||||
node.onConfigure = function () {
|
||||
if (origOnConfigure) origOnConfigure.apply(this, arguments);
|
||||
setTimeout(() => node.updateLoraSlots(), 100);
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
initCache();
|
||||
node.updateLoraSlots();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+132
-35
@@ -27,6 +27,127 @@ logger = logging.getLogger(__name__)
|
||||
# Users can override by setting env var `QWENIMAGE_LORA_APPLY_AWQ_MOD=1`.
|
||||
_APPLY_AWQ_MOD = str(os.getenv("QWENIMAGE_LORA_APPLY_AWQ_MOD", "0")).strip().lower() in ("1", "true", "yes", "y", "on")
|
||||
|
||||
|
||||
def _detect_lora_format(lora_state_dict: Dict[str, torch.Tensor]) -> Dict[str, Any]:
|
||||
"""
|
||||
Detect LoRA formats based on key patterns.
|
||||
Returns a dict containing detected format flags and sample keys.
|
||||
|
||||
Formats:
|
||||
- Standard LoRA: lora_up/lora_down, lora_A/lora_B, and dot variants.
|
||||
- LoKR (Lycoris): lokr_w1/lokr_w2
|
||||
- LoHa: hada_w1/hada_w2/hada_t1/hada_t2
|
||||
- IA3: ia3_w or '.ia3.' patterns
|
||||
"""
|
||||
keys = list(lora_state_dict.keys())
|
||||
|
||||
standard_patterns = (
|
||||
".lora_up.weight",
|
||||
".lora_down.weight",
|
||||
".lora_A.weight",
|
||||
".lora_B.weight",
|
||||
".lora.up.weight",
|
||||
".lora.down.weight",
|
||||
".lora.A.weight",
|
||||
".lora.B.weight",
|
||||
)
|
||||
|
||||
def _sample(match_fn, limit: int = 8) -> List[str]:
|
||||
out = []
|
||||
for k in keys:
|
||||
if match_fn(k):
|
||||
out.append(k)
|
||||
if len(out) >= limit:
|
||||
break
|
||||
return out
|
||||
|
||||
has_standard = any(p in k for k in keys for p in standard_patterns)
|
||||
has_lokr = any(".lokr_w1" in k or ".lokr_w2" in k for k in keys)
|
||||
has_loha = any(".hada_w1" in k or ".hada_w2" in k or ".hada_t1" in k or ".hada_t2" in k for k in keys)
|
||||
has_ia3 = any(".ia3." in k or ".ia3_w" in k or k.endswith(".ia3.weight") for k in keys)
|
||||
|
||||
return {
|
||||
"has_standard": has_standard,
|
||||
"has_lokr": has_lokr,
|
||||
"has_loha": has_loha,
|
||||
"has_ia3": has_ia3,
|
||||
"sample_standard": _sample(lambda k: any(p in k for p in standard_patterns)),
|
||||
"sample_lokr": _sample(lambda k: ".lokr_w1" in k or ".lokr_w2" in k),
|
||||
"sample_loha": _sample(lambda k: ".hada_w1" in k or ".hada_w2" in k or ".hada_t1" in k or ".hada_t2" in k),
|
||||
"sample_ia3": _sample(lambda k: ".ia3." in k or ".ia3_w" in k or k.endswith(".ia3.weight")),
|
||||
"total_keys": len(keys),
|
||||
}
|
||||
|
||||
|
||||
def _log_lora_format_detection(lora_name: str, detection: Dict[str, Any]) -> None:
|
||||
sep = "=" * 80
|
||||
logger.info(sep)
|
||||
logger.info(f"LoRA Format Detection: {lora_name}")
|
||||
logger.info(sep)
|
||||
logger.info("Detected Formats:")
|
||||
|
||||
has_standard = detection["has_standard"]
|
||||
has_lokr = detection["has_lokr"]
|
||||
has_loha = detection["has_loha"]
|
||||
has_ia3 = detection["has_ia3"]
|
||||
|
||||
if has_standard:
|
||||
logger.info(" ✅ Standard LoRA (Rank-Decomposed)")
|
||||
if has_lokr:
|
||||
logger.info(" ❌ LoKR (Lycoris) - Not Supported")
|
||||
if has_loha:
|
||||
logger.info(" ❌ LoHa - Not Supported")
|
||||
if has_ia3:
|
||||
logger.info(" ❌ IA3 - Not Supported")
|
||||
|
||||
if not (has_standard or has_lokr or has_loha or has_ia3):
|
||||
logger.info(" ❌ Unknown/Unsupported (no known LoRA keys detected)")
|
||||
|
||||
if has_standard:
|
||||
logger.info("")
|
||||
logger.info("✅ Standard LoRA Details:")
|
||||
logger.info(" Supported weight keys:")
|
||||
logger.info(" - lora_up.weight / lora_down.weight")
|
||||
logger.info(" - lora.up.weight / lora.down.weight")
|
||||
logger.info(" - lora_A.weight / lora_B.weight")
|
||||
logger.info(" - lora.A.weight / lora.B.weight")
|
||||
logger.info(" These are the standard formats produced by Kohya-ss, Diffusers, and most training scripts.")
|
||||
|
||||
if has_lokr:
|
||||
logger.info("")
|
||||
logger.info("❌ LoKR (Lycoris) - Not Supported")
|
||||
logger.info(" Issue: LoRAs in LoKR format (created by Lycoris) are not supported.")
|
||||
logger.info(" Important Note: This limitation applies specifically to Nunchaku quantization models.")
|
||||
logger.info(" LoKR format LoRAs may work with standard (non-quantized) Qwen Image models, but this node is designed for Nunchaku models only.")
|
||||
logger.info(" LoKR weights are automatically skipped when detected (experimental conversion code is disabled).")
|
||||
logger.info(" Converting to Standard LoRA using SVD approximation (via external tools or scripts) has also been tested")
|
||||
logger.info(" and found to result in noise/artifacts when applied to Nunchaku quantization models.")
|
||||
logger.info(" Conclusion: At this time, we have not found a way to successfully apply LoKR weights to Nunchaku models.")
|
||||
logger.info(" Please use Standard LoRA formats.")
|
||||
sample = detection.get("sample_lokr") or []
|
||||
if sample:
|
||||
logger.info(f" Sample LoKR keys found: {sample}")
|
||||
|
||||
if has_loha:
|
||||
logger.info("")
|
||||
logger.info("❌ LoHa - Not Supported")
|
||||
logger.info(" Issue: LoRAs in LoHa format are not supported for Nunchaku quantization models in this loader.")
|
||||
logger.info(" Please convert LoHa to Standard LoRA format before use.")
|
||||
sample = detection.get("sample_loha") or []
|
||||
if sample:
|
||||
logger.info(f" Sample LoHa keys found: {sample}")
|
||||
|
||||
if has_ia3:
|
||||
logger.info("")
|
||||
logger.info("❌ IA3 - Not Supported")
|
||||
logger.info(" Issue: IA3 format is not supported for Nunchaku models in this loader.")
|
||||
logger.info(" Please use Standard LoRA formats.")
|
||||
sample = detection.get("sample_ia3") or []
|
||||
if sample:
|
||||
logger.info(f" Sample IA3 keys found: {sample}")
|
||||
|
||||
logger.info(sep)
|
||||
|
||||
# --- Centralized & Optimized Key Mapping ---
|
||||
# This structure is faster to process and easier to maintain than a long if/elif chain.
|
||||
# --- CORRECTED Centralized & Optimized Key Mapping ---
|
||||
@@ -1038,6 +1159,14 @@ def compose_loras_v2(
|
||||
lora_name = lora_path_or_dict if isinstance(lora_path_or_dict, str) else "dict"
|
||||
lora_state_dict = _load_lora_state_dict(lora_path_or_dict)
|
||||
|
||||
# LoRA format detection + detailed logging (v2.2.3)
|
||||
try:
|
||||
detection = _detect_lora_format(lora_state_dict)
|
||||
_log_lora_format_detection(str(lora_name), detection)
|
||||
except Exception:
|
||||
# Safety: never fail compose due to logging
|
||||
pass
|
||||
|
||||
lora_grouped: Dict[str, Dict[str, torch.Tensor]] = defaultdict(dict)
|
||||
lokr_keys_count = 0
|
||||
standard_keys_count = 0
|
||||
@@ -1069,50 +1198,18 @@ def compose_loras_v2(
|
||||
else:
|
||||
lora_grouped[base_key][ab] = value
|
||||
|
||||
# Detect and log LoRA format
|
||||
# Existing lightweight summary is kept at DEBUG to avoid duplicating the v2.2.3 detailed log block.
|
||||
has_lokr = lokr_keys_count > 0
|
||||
has_standard = standard_keys_count > 0
|
||||
unrecognized_count = len(unrecognized_keys)
|
||||
|
||||
if has_lokr and has_standard:
|
||||
lora_format = "Mixed (LoKR + Standard LoRA)"
|
||||
elif has_lokr:
|
||||
if qkv_lokr_keys_count > 0:
|
||||
lora_format = "LoKR (QKV format)"
|
||||
else:
|
||||
lora_format = "LoKR"
|
||||
lora_format = "LoKR (QKV format)" if qkv_lokr_keys_count > 0 else "LoKR"
|
||||
elif has_standard:
|
||||
lora_format = "Standard LoRA"
|
||||
else:
|
||||
lora_format = "Unknown/Unsupported"
|
||||
|
||||
logger.info(f"📦 LoRA: {lora_name} | Format: {lora_format} | Strength: {strength:.3f}")
|
||||
if has_lokr:
|
||||
logger.debug(f" LoKR keys: {lokr_keys_count} (QKV: {qkv_lokr_keys_count}), Standard keys: {standard_keys_count}")
|
||||
|
||||
# Warn about unrecognized keys
|
||||
if unrecognized_count > 0:
|
||||
unrecognized_ratio = unrecognized_count / total_keys * 100
|
||||
if unrecognized_ratio >= 50:
|
||||
logger.warning(f"⚠️ {lora_name}: {unrecognized_count}/{total_keys} keys ({unrecognized_ratio:.1f}%) are unrecognized/unsupported format!")
|
||||
# Show sample unrecognized keys
|
||||
sample_keys = unrecognized_keys[:5]
|
||||
for sample_key in sample_keys:
|
||||
logger.warning(f" Unrecognized key example: {sample_key}")
|
||||
if unrecognized_count > 5:
|
||||
logger.warning(f" ... and {unrecognized_count - 5} more unrecognized keys")
|
||||
else:
|
||||
logger.debug(f" {unrecognized_count}/{total_keys} keys ({unrecognized_ratio:.1f}%) are unrecognized (likely metadata or non-LoRA keys)")
|
||||
|
||||
# Warn if format is completely unknown
|
||||
if lora_format == "Unknown/Unsupported":
|
||||
logger.warning(f"⚠️ {lora_name}: No recognized LoRA format detected! All {total_keys} keys were unrecognized.")
|
||||
if unrecognized_keys:
|
||||
logger.warning(f" Sample unrecognized keys:")
|
||||
for sample_key in unrecognized_keys[:10]:
|
||||
logger.warning(f" - {sample_key}")
|
||||
if len(unrecognized_keys) > 10:
|
||||
logger.warning(f" ... and {len(unrecognized_keys) - 10} more")
|
||||
logger.debug(f"LoRA summary: {lora_name} | Format: {lora_format} | Strength: {strength:.3f}")
|
||||
|
||||
# Process grouped weights for this LoRA
|
||||
processed_groups = {}
|
||||
|
||||
Reference in New Issue
Block a user