Implement v2.2.3 features: detailed LoRA format detection logs and Qwen V3 toggle UI JS

This commit is contained in:
ussoewwin
2026-05-17 05:24:56 +09:00
parent f2f98a95e9
commit ad2475c13b
2 changed files with 314 additions and 35 deletions
+182
View File
@@ -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
View File
@@ -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 = {}