From cc8bb7a4b6b4232c09fd776153ea67d3c363a955 Mon Sep 17 00:00:00 2001 From: ussoewwin <136552381+ussoewwin@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:18:25 +0900 Subject: [PATCH] docs: upstream auto-disable for CausalLM docstring patch --- ...RMERS_QWEN_VL_CAUSAL_LM_DOCSTRING_PATCH.md | 416 ++---------------- .../transformers_qwen_vl_docstring_patch.py | 23 +- 2 files changed, 50 insertions(+), 389 deletions(-) diff --git a/md/TRANSFORMERS_QWEN_VL_CAUSAL_LM_DOCSTRING_PATCH.md b/md/TRANSFORMERS_QWEN_VL_CAUSAL_LM_DOCSTRING_PATCH.md index ecd15c5..798647d 100644 --- a/md/TRANSFORMERS_QWEN_VL_CAUSAL_LM_DOCSTRING_PATCH.md +++ b/md/TRANSFORMERS_QWEN_VL_CAUSAL_LM_DOCSTRING_PATCH.md @@ -148,359 +148,32 @@ Unchanged for this fix: `patches/nunchaku_patch.py` (separate v2.4.6 `apply_rota --- -## 4. Full added / modified source code +## 4. Source files (canonical) -### 4.1 `patches/transformers_qwen_vl_docstring_patch.py` (complete) +Full implementation lives in the repository (not duplicated here, to avoid stale copies). -```python -""" -Runtime patch for transformers Qwen VL CausalLM ModelOutput @auto_docstring errors. +| File | Role | +|------|------| +| `patches/transformers_qwen_vl_docstring_patch.py` | Upstream probes; optional wrap of `get_args_doc_from_source` | +| `prestartup_script.py` | Loads docstring patch via `importlib` **before** rotary compat | -At import time, @auto_docstring on Qwen3VLCausalLMOutputWithPast and -Qwen2_5_VLCausalLMOutputWithPast validates __init__ parameters against -documentation built from ModelOutputArgs. That template omits loss/logits -even though subclasses inherit those fields from CausalLMOutputWithPast. +Public API: -Fix: wrap get_args_doc_from_source so ModelOutputArgs merges loss/logits -from CausalLMOutputWithPast.__doc__ (parse_docstring max_indent_level=4). +| Function | Returns | +|----------|---------| +| `apply_transformers_causal_lm_docstring_patch()` | `True` if wrapper active; `False` if skipped (upstream fixed or late import) | +| `is_patch_applied()` | Whether this process installed the wrapper | +| `is_patch_wrapped()` | Whether `get_args_doc_from_source` carries the patch tag | -Self-disables when upstream ModelOutputArgs already documents loss+logits, -when env TRANSFORMERS_CAUSAL_LM_DOCSTRING_PATCH disables the patch, or when -a subprocess import probe shows no [ERROR] loss/logits without the patch. +`apply_transformers_causal_lm_docstring_patch()` decision order: -No site-packages edits. Idempotent via _qwen_lora_loader_causal_lm_docstring_patch. -""" +1. Already applied → return `True` +2. Qwen VL modeling already in `sys.modules` → warn, return `False` (restart ComfyUI) +3. `ModelOutputArgs` already documents `loss` and `logits` → skip (upstream fixed) +4. Subprocess import probe prints `CLEAN` → skip (no docstring errors without patch) +5. Else install wrapper around `get_args_doc_from_source` -from __future__ import annotations - -import importlib -import io -import os -import subprocess -import sys -from contextlib import redirect_stdout -from typing import Any, Callable - -_PATCH_TAG = "_qwen_lora_loader_causal_lm_docstring_patch" -_ENV_DISABLE = "TRANSFORMERS_CAUSAL_LM_DOCSTRING_PATCH" -_DISABLE_VALUES = frozenset({"0", "false", "off", "no", "disable", "disabled"}) - -_QWEN3_MODULE = "transformers.models.qwen3_vl.modeling_qwen3_vl" -_QWEN25_MODULE = "transformers.models.qwen2_5_vl.modeling_qwen2_5_vl" -_QWEN3_CLASS = "Qwen3VLCausalLMOutputWithPast" -_QWEN25_CLASS = "Qwen2_5_VLCausalLMOutputWithPast" - -_ERROR_MARKERS = ( - "[ERROR] `loss`", - "[ERROR] `logits`", -) - - -def _env_disabled() -> bool: - return os.environ.get(_ENV_DISABLE, "").strip().lower() in _DISABLE_VALUES - - -def _get_auto_docstring_module(): - return importlib.import_module("transformers.utils.auto_docstring") - - -def _get_model_output_args_class(auto_mod): - return getattr(auto_mod, "ModelOutputArgs", None) - - -def _get_causal_lm_output_with_past(auto_mod): - try: - from transformers.modeling_outputs import CausalLMOutputWithPast - return CausalLMOutputWithPast - except ImportError: - return None - - -def _parse_docstring(auto_mod, docstring: str, max_indent_level: int = 4) -> dict: - parse_fn = getattr(auto_mod, "parse_docstring", None) - if parse_fn is None: - return {} - result = parse_fn(docstring, max_indent_level=max_indent_level) - if isinstance(result, tuple): - params = result[0] - return params if isinstance(params, dict) else {} - return result if isinstance(result, dict) else {} - - -def _extract_loss_logits_from_causal_lm_doc(auto_mod) -> dict[str, Any]: - causal_cls = _get_causal_lm_output_with_past(auto_mod) - if causal_cls is None or not getattr(causal_cls, "__doc__", None): - return {} - params = _parse_docstring(auto_mod, causal_cls.__doc__, max_indent_level=4) - out: dict[str, Any] = {} - for key in ("loss", "logits"): - if key in params: - out[key] = params[key] - return out - - -def _model_output_args_has_loss_logits(auto_mod) -> bool: - moa = _get_model_output_args_class(auto_mod) - if moa is None: - return False - loss = getattr(moa, "loss", None) - logits = getattr(moa, "logits", None) - if not isinstance(loss, dict) or not isinstance(logits, dict): - return False - return bool(loss.get("description")) and bool(logits.get("description")) - - -def _is_model_output_args(obj: Any, auto_mod) -> bool: - moa = _get_model_output_args_class(auto_mod) - return moa is not None and obj is moa - - -def _make_wrapped_get_args( - original: Callable[..., dict], - auto_mod, - supplemental: dict[str, Any], -) -> Callable[..., dict]: - def wrapped(args_classes: object | list[object]) -> dict: - result = dict(original(args_classes)) - if _is_model_output_args(args_classes, auto_mod): - for key, value in supplemental.items(): - if key not in result: - result[key] = value - elif isinstance(args_classes, (list, tuple)): - for cls in args_classes: - if _is_model_output_args(cls, auto_mod): - for key, value in supplemental.items(): - if key not in result: - result[key] = value - break - return result - - setattr(wrapped, _PATCH_TAG, True) - wrapped._qwen_lora_loader_original = original # type: ignore[attr-defined] - return wrapped - - -def _qwen_modules_already_imported() -> bool: - return _QWEN3_MODULE in sys.modules or _QWEN25_MODULE in sys.modules - - -def _count_error_markers(text: str) -> int: - return sum(text.count(m) for m in _ERROR_MARKERS) - - -def _subprocess_import_probe() -> tuple[bool, str]: - """ - Import Qwen VL output classes in a child process without our patch. - Returns (needs_patch, diagnostic_message). - """ - code = f""" -import io, sys -from contextlib import redirect_stdout -buf = io.StringIO() -try: - with redirect_stdout(buf): - import transformers.models.qwen3_vl.modeling_qwen3_vl as m3 - import transformers.models.qwen2_5_vl.modeling_qwen2_5_vl as m25 - _ = m3.{_QWEN3_CLASS} - _ = m25.{_QWEN25_CLASS} -except Exception as e: - print("PROBE_EXCEPTION:", e, file=buf) -out = buf.getvalue() -markers = sum(out.count(x) for x in {list(_ERROR_MARKERS)!r}) -print("PROBE_MARKERS", markers) -print("PROBE_LEN", len(out)) -""" - try: - proc = subprocess.run( - [sys.executable, "-c", code], - capture_output=True, - text=True, - timeout=120, - env={k: v for k, v in os.environ.items() if k != _ENV_DISABLE}, - ) - except (subprocess.TimeoutExpired, OSError) as exc: - return True, f"subprocess probe failed ({exc}); applying patch conservatively" - - combined = (proc.stdout or "") + (proc.stderr or "") - markers = 0 - for line in combined.splitlines(): - if line.startswith("PROBE_MARKERS"): - try: - markers = int(line.split()[-1]) - except ValueError: - pass - if proc.returncode != 0 and markers == 0: - return True, f"subprocess probe exit {proc.returncode}; applying patch conservatively" - if markers > 0: - return True, f"subprocess probe saw {markers} loss/logits error marker(s)" - return False, "subprocess probe: no loss/logits error markers without patch" - - -def apply_transformers_qwen_vl_causal_lm_docstring_patch() -> bool: - """ - Install get_args_doc_from_source wrapper if needed. - Returns True if patch is active (installed or already present). - """ - if _env_disabled(): - print( - f"[INFO] ComfyUI-QwenImageLoraLoader: {_ENV_DISABLE} set; " - "skipping CausalLM ModelOutput docstring patch" - ) - return False - - auto_mod = _get_auto_docstring_module() - get_args = getattr(auto_mod, "get_args_doc_from_source", None) - if get_args is None: - print( - "[WARNING] ComfyUI-QwenImageLoraLoader: " - "transformers.utils.auto_docstring.get_args_doc_from_source not found; " - "skipping docstring patch" - ) - return False - - if getattr(get_args, _PATCH_TAG, False): - return True - - if _model_output_args_has_loss_logits(auto_mod): - print( - "[INFO] ComfyUI-QwenImageLoraLoader: upstream ModelOutputArgs already " - "documents loss/logits; skipping docstring patch" - ) - return False - - needs_patch, probe_msg = _subprocess_import_probe() - if not needs_patch: - print( - f"[INFO] ComfyUI-QwenImageLoraLoader: {probe_msg}; " - "skipping docstring patch" - ) - return False - - if _qwen_modules_already_imported(): - print( - "[WARNING] ComfyUI-QwenImageLoraLoader: Qwen VL modeling modules already " - "imported before docstring patch; restart ComfyUI for a clean apply. " - f"({probe_msg})" - ) - return False - - supplemental = _extract_loss_logits_from_causal_lm_doc(auto_mod) - if "loss" not in supplemental or "logits" not in supplemental: - print( - "[WARNING] ComfyUI-QwenImageLoraLoader: could not extract loss/logits " - "from CausalLMOutputWithPast.__doc__; skipping docstring patch" - ) - return False - - wrapped = _make_wrapped_get_args(get_args, auto_mod, supplemental) - auto_mod.get_args_doc_from_source = wrapped - print( - "[INFO] Patched transformers.utils.auto_docstring.get_args_doc_from_source " - "for Qwen VL CausalLM ModelOutput docstrings (loss/logits); " - "removes when upstream adds them" - ) - return True - - -def remove_transformers_qwen_vl_causal_lm_docstring_patch() -> bool: - """Restore original get_args_doc_from_source if our wrapper is installed.""" - auto_mod = _get_auto_docstring_module() - get_args = getattr(auto_mod, "get_args_doc_from_source", None) - if get_args is None or not getattr(get_args, _PATCH_TAG, False): - return False - original = getattr(get_args, "_qwen_lora_loader_original", None) - if original is not None: - auto_mod.get_args_doc_from_source = original - return True - return False - - -def verify_patch_in_process() -> dict[str, Any]: - """Import Qwen VL output classes in-process; return error counts (for tests).""" - auto_mod = _get_auto_docstring_module() - get_args = getattr(auto_mod, "get_args_doc_from_source", None) - buf = io.StringIO() - errors_before = 0 - try: - with redirect_stdout(buf): - if _QWEN3_MODULE in sys.modules: - del sys.modules[_QWEN3_MODULE] - if _QWEN25_MODULE in sys.modules: - del sys.modules[_QWEN25_MODULE] - import transformers.models.qwen3_vl.modeling_qwen3_vl as m3 - import transformers.models.qwen2_5_vl.modeling_qwen2_5_vl as m25 - _ = m3.Qwen3VLCausalLMOutputWithPast - _ = m25.Qwen2_5_VLCausalLMOutputWithPast - except Exception as exc: - return { - "ok": False, - "exception": str(exc), - "errors": -1, - "output": buf.getvalue(), - "patched": bool(get_args and getattr(get_args, _PATCH_TAG, False)), - } - out = buf.getvalue() - errors = _count_error_markers(out) - return { - "ok": errors == 0, - "errors": errors, - "output": out, - "patched": bool(get_args and getattr(get_args, _PATCH_TAG, False)), - } -``` - -### 4.2 `prestartup_script.py` (complete) - -```python -""" -ComfyUI prestartup hook for ComfyUI-QwenImageLoraLoader. - -Runs before custom node package __init__ files are loaded so runtime patches -apply before heavy imports (transformers Qwen VL, nunchaku, etc.). -""" - -from __future__ import annotations - -import os -import sys - - -def _patch_nunchaku_apply_rotary_emb_compat() -> None: - """Apply apply_rotary_emb compat patch (v2.4.6); see md/QWEN_IMAGE_APPLY_ROTARY_EMB_COMPAT_FIX.md.""" - try: - from patches.nunchaku_patch import apply_nunchaku_patch - - apply_nunchaku_patch() - except Exception as exc: - print( - f"[WARNING] ComfyUI-QwenImageLoraLoader prestartup: " - f"nunchaku apply_rotary_emb compat patch failed: {exc}" - ) - - -def _patch_transformers_qwen_vl_causal_lm_docstring() -> None: - """Apply transformers Qwen VL CausalLM ModelOutput docstring patch (loss/logits).""" - try: - from patches.transformers_qwen_vl_docstring_patch import ( - apply_transformers_qwen_vl_causal_lm_docstring_patch, - ) - - if apply_transformers_qwen_vl_causal_lm_docstring_patch(): - print( - "[INFO] ComfyUI-QwenImageLoraLoader prestartup: " - "CausalLM ModelOutput docstring patch applied" - ) - except Exception as exc: - print( - f"[WARNING] ComfyUI-QwenImageLoraLoader prestartup: " - f"CausalLM ModelOutput docstring patch failed: {exc}" - ) - - -# Docstring patch first: must run before any Qwen VL modeling import. -_patch_transformers_qwen_vl_causal_lm_docstring() -_patch_nunchaku_apply_rotary_emb_compat() -``` +**No environment variables.** When Hugging Face fixes `transformers`, steps 3–4 skip installation automatically on the next ComfyUI start (upstream auto-disable). --- @@ -521,13 +194,11 @@ Supplemental entries are built once at apply time from `CausalLMOutputWithPast._ ```mermaid flowchart TD A[ComfyUI starts] --> B[prestartup_script.py] - B --> C{Env disable?} - C -->|yes| Z[Skip patch] - C -->|no| D{ModelOutputArgs has loss+logits?} - D -->|yes| Z + B --> D{ModelOutputArgs has loss+logits?} + D -->|yes| Z[Skip patch — upstream fixed] D -->|no| E[Subprocess import probe] - E -->|no errors| Z - E -->|errors| F{Qwen modules already imported?} + E -->|clean stdout| Z + E -->|errors or inconclusive| F{Qwen modules already imported?} F -->|yes| W[WARN: restart needed] F -->|no| G[Parse CausalLMOutputWithPast.__doc__] G --> H[Wrap get_args_doc_from_source] @@ -542,19 +213,18 @@ The wrapper sets `_qwen_lora_loader_causal_lm_docstring_patch = True`. A second --- -## 6. Upstream self-disable (same idea as v2.4.6 rotary patch) +## 6. Automatic upstream disable (same idea as v2.4.6 rotary patch) -The patch is **temporary**. It should stop applying when Hugging Face fixes upstream or when the environment no longer needs it. +**Fully automatic on every ComfyUI start.** Users do not set environment variables or toggles. `prestartup_script.py` runs probes; the wrapper is installed **only while upstream `transformers` still triggers `[ERROR] loss` / `[ERROR] logits`**, and **is not installed** once Hugging Face fixes `ModelOutputArgs` or a clean import probe shows zero errors. | Scenario | Behavior | |----------|----------| -| **A. Upstream fix** | `ModelOutputArgs` gains `loss` and `logits` with non-empty `description` → skip patch, log info | -| **B. User disable** | `TRANSFORMERS_CAUSAL_LM_DOCSTRING_PATCH=0` (or `false` / `off` / `no` / `disable` / `disabled`) → skip patch | -| **C. Subprocess probe** | Fresh Python imports Qwen VL classes without patch; if zero `[ERROR] loss/logits` markers → skip patch | -| **D. Late import** | Qwen VL modules already in `sys.modules` → warn; patch not applied (restart required) | -| **E. Missing API** | No `get_args_doc_from_source` or cannot parse parent doc → skip with warning | +| **A. Upstream fix (schema)** | `ModelOutputArgs` has `loss` and `logits` with non-empty `description` → skip patch, log info | +| **B. Upstream fix (probe)** | Subprocess imports Qwen VL without this patch; zero `[ERROR] loss/logits` on stdout → skip patch, log info | +| **C. Late import** | Qwen VL modules already in `sys.modules` → warn; patch not applied until restart | +| **D. Missing API** | No `get_args_doc_from_source` or cannot parse parent doc → skip with warning | -This mirrors the v2.4.6 pattern: probe upstream, patch only when needed, document removal conditions. +When **A** or **B** applies, no wrapper is installed — the same prestartup / probe-first pattern as v2.4.6 `apply_rotary_emb` compat. --- @@ -569,6 +239,18 @@ After a clean restart with the patch applied **before** any Qwen VL import: [INFO] ComfyUI-QwenImageLoraLoader prestartup: CausalLM ModelOutput docstring patch applied ``` +When upstream is already fixed, expect skip messages instead (no wrapper): + +```text +[INFO] CausalLM ModelOutput docstring patch skipped: transformers ModelOutputArgs already documents loss and logits (upstream fixed — patch not installed) +``` + +or + +```text +[INFO] CausalLM ModelOutput docstring patch skipped: Qwen VL ModelOutput docstrings resolve loss/logits without docstring errors (upstream fixed — patch not installed) +``` + Full startup should show **zero** lines containing `[ERROR] \`loss\`` or `[ERROR] \`logits\`` for the Qwen VL output classes. ### 7.2 In-process check (optional) @@ -576,18 +258,10 @@ Full startup should show **zero** lines containing `[ERROR] \`loss\`` or `[ERROR From the extension root, with ComfyUI’s Python: ```bat -cmd /c "cd /d D:\USERFILES\ComfyUI\ComfyUI\custom_nodes\ComfyUI-QwenImageLoraLoader && D:\USERFILES\ComfyUI\python_embeded\python.exe -c \"from patches.transformers_qwen_vl_docstring_patch import apply_transformers_qwen_vl_causal_lm_docstring_patch, verify_patch_in_process; apply_transformers_qwen_vl_causal_lm_docstring_patch(); print(verify_patch_in_process())\"" +cmd /c "cd /d D:\USERFILES\ComfyUI\ComfyUI\custom_nodes\ComfyUI-QwenImageLoraLoader && D:\USERFILES\ComfyUI\python_embeded\python.exe _verify_docstring_patch.py" ``` -Expected: `'ok': True`, `'errors': 0`, `'patched': True`. - -### 7.3 Disable probe - -```bat -set TRANSFORMERS_CAUSAL_LM_DOCSTRING_PATCH=0 -``` - -Restart ComfyUI: patch skip message should appear; if upstream is still broken, `[ERROR] loss/logits` may return. +Expected when upstream is still broken: patch applies and Qwen VL imports report zero `[ERROR] loss/logits` markers. --- @@ -599,7 +273,7 @@ Restart ComfyUI: patch skip message should appear; if upstream is still broken, | **Cause** | `ModelOutputArgs` lacks `loss`/`logits`; parent `Args:` not parsed at indent 0 | | **Fix** | Wrap `get_args_doc_from_source` in custom node `prestartup_script.py` | | **Scope** | ComfyUI-QwenImageLoraLoader only; no `site-packages` changes | -| **Self-disable** | Upstream fix, env var, subprocess probe, idempotent wrapper | +| **Auto-disable** | Upstream `ModelOutputArgs` fix and/or clean subprocess import probe; no user env vars | | **Related** | [v2.4.6 apply_rotary_emb compat](https://github.com/ussoewwin/ComfyUI-QwenImageLoraLoader/releases/tag/v2.4.6) — same prestartup / self-disable pattern | --- diff --git a/patches/transformers_qwen_vl_docstring_patch.py b/patches/transformers_qwen_vl_docstring_patch.py index 648a9f7..7751b49 100644 --- a/patches/transformers_qwen_vl_docstring_patch.py +++ b/patches/transformers_qwen_vl_docstring_patch.py @@ -5,14 +5,14 @@ When Hugging Face transformers merges CausalLMOutputWithPast docs into Qwen VL ModelOutput subclasses, validation falls back to ModelOutputArgs — which lacks loss/logits — and prints [ERROR] at import time. -Upstream self-disable (same pattern as v2.4.6 apply_rotary_emb compat): +Fully automatic at ComfyUI prestartup (no user env vars): - Probe upstream ModelOutputArgs before installing any wrapper. - - If upstream already documents loss/logits, skip (no wrapper, harmless). - - If import probe shows clean stdout, skip. - - Disable via TRANSFORMERS_CAUSAL_LM_DOCSTRING_PATCH=0|false|off|no|disable|disabled + - If upstream already documents loss/logits, skip (no wrapper). + - If a subprocess import probe shows clean stdout, skip. + - Otherwise install the wrapper (default when upstream is still broken). Fix: wrap get_args_doc_from_source to merge loss/logits into the returned dict -only while upstream ModelOutputArgs still omits those fields. +while upstream ModelOutputArgs still omits those fields. """ from __future__ import annotations @@ -38,11 +38,6 @@ _patch_applied: bool = False _original_get_args_doc_from_source: Optional[Callable[..., dict]] = None -def _patch_disabled_by_env() -> bool: - value = os.environ.get("TRANSFORMERS_CAUSAL_LM_DOCSTRING_PATCH", "").strip().lower() - return value in ("0", "false", "no", "off", "disable", "disabled") - - def _qwen_vl_modeling_already_imported() -> bool: return any(name in sys.modules for name in _QWEN_VL_MODELING_MODULES) @@ -155,7 +150,6 @@ def _import_probe_reports_clean() -> Optional[bool]: capture_output=True, text=True, timeout=120, - env={**os.environ, "TRANSFORMERS_CAUSAL_LM_DOCSTRING_PATCH": "0"}, ) except (OSError, subprocess.SubprocessError) as exc: logger.debug("CausalLM docstring import probe failed: %s", exc) @@ -191,13 +185,6 @@ def apply_transformers_causal_lm_docstring_patch() -> bool: if _patch_applied: return True - if _patch_disabled_by_env(): - logger.info( - "CausalLM ModelOutput docstring patch skipped: " - "TRANSFORMERS_CAUSAL_LM_DOCSTRING_PATCH is disabled" - ) - return False - if _qwen_vl_modeling_already_imported(): logger.warning( "CausalLM ModelOutput docstring patch skipped: Qwen VL modeling modules "