diff --git a/md/QWEN_IMAGE_APPLY_ROTARY_EMB_COMPAT_FIX.md b/md/QWEN_IMAGE_APPLY_ROTARY_EMB_COMPAT_FIX.md index dbc497b..1d59ff8 100644 --- a/md/QWEN_IMAGE_APPLY_ROTARY_EMB_COMPAT_FIX.md +++ b/md/QWEN_IMAGE_APPLY_ROTARY_EMB_COMPAT_FIX.md @@ -12,7 +12,18 @@ This document records the error, root cause, modified files, full patched code, | Python | 3.13 (embedded) | | PyTorch | 2.12.0+cu132 | -**Design constraint:** Do **not** edit `ComfyUI-nunchaku` upstream files. Apply the shim only from **ComfyUI-QwenImageLoraLoader**. +**Design constraint:** Do **not** edit `ComfyUI-nunchaku` upstream files. Apply the shim only from **ComfyUI-QwenImageLoraLoader**, and **only when still needed**. + +The compat layer is **conditional and upstream-safe**: + +| Condition | Action | +|-----------|--------| +| ComfyUI already exports `apply_rotary_emb` (native restore) | **Skip** — never overwrite | +| ComfyUI-nunchaku `models/qwenimage.py` no longer imports `apply_rotary_emb` | **Skip** — upstream fixed the import | +| Symbol missing **and** nunchaku still imports it | **Apply** — alias `apply_rope1`, tagged `_qwen_lora_loader_rotary_shim` | +| `QWENIMAGE_ROTARY_COMPAT=0` (or `false` / `off`) | **Skip** — manual opt-out | + +When upstream fixes land, the shim stops patching automatically; no nunchaku file edits and no permanent module mutation. --- @@ -194,40 +205,15 @@ except Exception: logger.exception("ComfyUI-QwenImageLoraLoader prestartup: apply_rotary_emb compat failed") ``` -### 4.2 Added to `patches/nunchaku_patch.py` — module flag and compat function +### 4.2 Added to `patches/nunchaku_patch.py` — conditional compat function -```python -_qwen_apply_rotary_emb_compat_applied: bool = False +See `patches/nunchaku_patch.py` for the full implementation. Summary: - -def apply_qwen_image_apply_rotary_emb_compat() -> bool: - """ - ComfyUI >= 0.24 removed comfy.ldm.qwen_image.model.apply_rotary_emb. - ComfyUI-nunchaku still imports it; alias to comfy.ldm.flux.math.apply_rope1. - """ - global _qwen_apply_rotary_emb_compat_applied - if _qwen_apply_rotary_emb_compat_applied: - return True - - try: - import comfy.ldm.qwen_image.model as qwen_image_model - from comfy.ldm.flux.math import apply_rope1 - - if hasattr(qwen_image_model, "apply_rotary_emb"): - _qwen_apply_rotary_emb_compat_applied = True - return False - - qwen_image_model.apply_rotary_emb = apply_rope1 - _qwen_apply_rotary_emb_compat_applied = True - logger.info( - "Patched comfy.ldm.qwen_image.model.apply_rotary_emb -> apply_rope1 " - "(ComfyUI-nunchaku Qwen Image compat)" - ) - return True - except Exception as e: - logger.error("Failed to apply apply_rotary_emb compat patch: %s", e) - return False -``` +1. **Probe** sibling `ComfyUI-nunchaku/models/qwenimage.py` for `from comfy.ldm.qwen_image.model import ... apply_rotary_emb`. +2. **Skip** if ComfyUI already exports native `apply_rotary_emb` (not our tagged shim). +3. **Skip** if nunchaku no longer imports `apply_rotary_emb`. +4. **Apply** only when the symbol is missing and nunchaku still needs it — assign `apply_rope1` with `_qwen_lora_loader_rotary_shim` marker. +5. **Opt-out** via env `QWENIMAGE_ROTARY_COMPAT=0`. ### 4.3 Modified in `patches/nunchaku_patch.py` — `apply_nunchaku_patch()` entry and return @@ -310,25 +296,26 @@ sequenceDiagram ### 5.2 What the shim does (step by step) -1. Import `comfy.ldm.qwen_image.model` as a module object (not only symbols from `__init__` if any). -2. If `apply_rotary_emb` **already exists** (future ComfyUI or another patcher), do nothing and return `False`. -3. Otherwise assign `qwen_image_model.apply_rotary_emb = apply_rope1`. -4. Python’s `from comfy.ldm.qwen_image.model import apply_rotary_emb` binds the name at import time to whatever attribute exists on the module **at that moment**. Injecting the attribute on the module **before** nunchaku’s import is sufficient. +1. Import `comfy.ldm.qwen_image.model` as a module object. +2. If our tagged shim is already on the module, return `True` (idempotent). +3. If ComfyUI **natively** exports `apply_rotary_emb`, log skip and return `False` (never overwrite). +4. Read `ComfyUI-nunchaku/models/qwenimage.py`; if it no longer imports `apply_rotary_emb`, log skip and return `False`. +5. Only if the symbol is still missing and nunchaku still imports it: assign `apply_rope1` with shim tag. +6. Python’s `from comfy.ldm.qwen_image.model import apply_rotary_emb` binds at import time; prestartup injection remains sufficient when step 5 runs. ### 5.3 Idempotency and double execution - `prestartup_script.py` loads `nunchaku_patch.py` via `importlib` under a **unique module name** (`comfyui_qwenimageloraloader_nunchaku_patch_prestartup`). - Later, `__init__.py` imports `patches.nunchaku_patch` as a normal package submodule (second module object, separate `_qwen_apply_rotary_emb_compat_applied` flag). -- That is acceptable: the **real effect** is on `comfy.ldm.qwen_image.model`, which is a singleton module. The second call sees `hasattr(qwen_image_model, "apply_rotary_emb")` as true and skips re-patching. +- That is acceptable: the **real effect** is on `comfy.ldm.qwen_image.model`, which is a singleton module. The second call detects the tagged shim (or native export) and does not re-patch. - Startup logs therefore show the INFO lines **once** (from prestartup), not twice. ### 5.4 Return values of `apply_qwen_image_apply_rotary_emb_compat()` | Return | Meaning | |--------|---------| -| `True` | Shim was applied in this call (attribute was missing and was added). | -| `False` | Shim not needed (`apply_rotary_emb` already present) or apply failed (logged as error). | -| `True` (immediate) | Already applied in **this** module instance’s prior call (`_qwen_apply_rotary_emb_compat_applied` flag). | +| `True` | Shim applied this call, or already applied in this module instance (flag / tagged shim on singleton). | +| `False` | Skipped: native ComfyUI export, nunchaku no longer imports `apply_rotary_emb`, env disabled, or apply failed (error logged). | ### 5.5 Risk assessment @@ -336,7 +323,7 @@ sequenceDiagram |-------|------------| | Correctness vs ComfyUI 0.24 native path | **Aligned** — same function the core model uses (`apply_rope1`). | | Effect on other custom nodes | **Low** — only adds a name on `comfy.ldm.qwen_image.model` if missing; does not replace ComfyUI core files. | -| Upstream nunchaku fix | Eventually nunchaku may switch imports to `apply_rope1`; then `hasattr` branch skips shim (harmless). | +| Upstream nunchaku fix | Probe of `models/qwenimage.py` detects removed import → shim skipped automatically. ComfyUI native restore → also skipped without overwrite. | | Performance | **None** — alias only, no extra wrapper. | ### 5.6 Unrelated warning: `nunchaku_versions.json` @@ -385,7 +372,7 @@ Run a minimal Qwen Image + Nunchaku workflow (load `NunchakuQwenImageDiTLoader`, 2. Revert changes in `patches/nunchaku_patch.py` (remove `apply_qwen_image_apply_rotary_emb_compat` and its call in `apply_nunchaku_patch`). 3. Restart ComfyUI. -Alternatively, upgrade **ComfyUI-nunchaku** to a release that imports `apply_rope1` directly (when upstream publishes one), then remove this shim. +Alternatively, upgrade **ComfyUI-nunchaku** to a release that no longer imports `apply_rotary_emb` (or upgrade ComfyUI if it restores the symbol). The shim should **skip by itself**; you can remove `prestartup_script.py` / compat code later for a cleaner tree, or set `QWENIMAGE_ROTARY_COMPAT=0` to force-disable. --- @@ -395,7 +382,7 @@ Alternatively, upgrade **ComfyUI-nunchaku** to a release that imports `apply_rop |----------|--------| | What broke? | `ImportError: cannot import name 'apply_rotary_emb'` during ComfyUI-nunchaku load. | | Why? | ComfyUI 0.24 removed `apply_rotary_emb`; nunchaku still imports it; LoraLoader `__init__` runs too late. | -| Fix? | Early `prestartup_script.py` + `apply_rope1` alias on `comfy.ldm.qwen_image.model`; duplicate guard in `apply_nunchaku_patch()`. | +| Fix? | Early `prestartup_script.py` + **conditional** `apply_rope1` alias (only when nunchaku still imports missing symbol); auto-skip when ComfyUI or nunchaku upstream fixes land. | | Files? | `prestartup_script.py` (new), `patches/nunchaku_patch.py` (modified). | | nunchaku touched? | **No.** | diff --git a/patches/nunchaku_patch.py b/patches/nunchaku_patch.py index f32c11e..b64b8f2 100644 --- a/patches/nunchaku_patch.py +++ b/patches/nunchaku_patch.py @@ -1,8 +1,10 @@ import logging +import os +import re import sys import types -from typing import Optional, Tuple +from typing import Callable, Optional, Tuple import torch @@ -11,25 +13,112 @@ logger = logging.getLogger(__name__) _svdq_from_linear_patched: bool = False _qwen_apply_rotary_emb_compat_applied: bool = False +_ROTARY_SHIM_TAG = "_qwen_lora_loader_rotary_shim" +_NUNCHAKU_QWENIMAGE_APPLY_ROTARY_PATTERNS = ( + re.compile( + r"from\s+comfy\.ldm\.qwen_image\.model\s+import\s+\([^)]*\bapply_rotary_emb\b", + re.MULTILINE | re.DOTALL, + ), + re.compile( + r"from\s+comfy\.ldm\.qwen_image\.model\s+import\s+[^\n#]*\bapply_rotary_emb\b", + ), +) + + +def _rotary_compat_disabled_by_env() -> bool: + value = os.environ.get("QWENIMAGE_ROTARY_COMPAT", "").strip().lower() + return value in ("0", "false", "no", "off", "disable", "disabled") + + +def _mark_rotary_shim(fn: Callable) -> Callable: + setattr(fn, _ROTARY_SHIM_TAG, True) + return fn + + +def _rotary_shim_installed_on(qwen_image_model) -> bool: + fn = getattr(qwen_image_model, "apply_rotary_emb", None) + return fn is not None and getattr(fn, _ROTARY_SHIM_TAG, False) + + +def _comfyui_has_native_apply_rotary_emb(qwen_image_model) -> bool: + return ( + getattr(qwen_image_model, "apply_rotary_emb", None) is not None + and not _rotary_shim_installed_on(qwen_image_model) + ) + + +def _find_nunchaku_qwenimage_py() -> Optional[str]: + custom_nodes = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) + ) + for folder in ("ComfyUI-nunchaku", "ComfyUI_Nunchaku", "comfyui-nunchaku"): + path = os.path.join(custom_nodes, folder, "models", "qwenimage.py") + if os.path.isfile(path): + return path + return None + + +def _nunchaku_qwenimage_still_imports_apply_rotary_emb() -> Optional[bool]: + """ + True: nunchaku still imports apply_rotary_emb (compat may be needed). + False: nunchaku source no longer imports it (skip shim). + None: qwenimage.py not found (apply only if symbol missing). + """ + path = _find_nunchaku_qwenimage_py() + if path is None: + return None + + try: + with open(path, encoding="utf-8", errors="replace") as handle: + text = handle.read() + except OSError as exc: + logger.debug("Could not read %s for rotary compat probe: %s", path, exc) + return None + + for pattern in _NUNCHAKU_QWENIMAGE_APPLY_ROTARY_PATTERNS: + if pattern.search(text): + return True + return False + def apply_qwen_image_apply_rotary_emb_compat() -> bool: """ ComfyUI >= 0.24 removed comfy.ldm.qwen_image.model.apply_rotary_emb. - ComfyUI-nunchaku still imports it; alias to comfy.ldm.flux.math.apply_rope1. + ComfyUI-nunchaku still imports it; alias to comfy.ldm.flux.math.apply_rope1 + only when the import is still required and ComfyUI has not restored the symbol. """ global _qwen_apply_rotary_emb_compat_applied if _qwen_apply_rotary_emb_compat_applied: return True + if _rotary_compat_disabled_by_env(): + logger.info( + "apply_rotary_emb compat skipped (QWENIMAGE_ROTARY_COMPAT is disabled)" + ) + return False + try: import comfy.ldm.qwen_image.model as qwen_image_model from comfy.ldm.flux.math import apply_rope1 - if hasattr(qwen_image_model, "apply_rotary_emb"): + if _rotary_shim_installed_on(qwen_image_model): _qwen_apply_rotary_emb_compat_applied = True + return True + + if _comfyui_has_native_apply_rotary_emb(qwen_image_model): + logger.info( + "apply_rotary_emb compat skipped: ComfyUI already exports apply_rotary_emb" + ) return False - qwen_image_model.apply_rotary_emb = apply_rope1 + nunchaku_needs = _nunchaku_qwenimage_still_imports_apply_rotary_emb() + if nunchaku_needs is False: + logger.info( + "apply_rotary_emb compat skipped: ComfyUI-nunchaku no longer imports apply_rotary_emb" + ) + return False + + qwen_image_model.apply_rotary_emb = _mark_rotary_shim(apply_rope1) _qwen_apply_rotary_emb_compat_applied = True logger.info( "Patched comfy.ldm.qwen_image.model.apply_rotary_emb -> apply_rope1 "