diff --git a/md/COMFYUI_0.4.0_MODEL_MANAGEMENT_ERRORS.md b/md/COMFYUI_0.4.0_MODEL_MANAGEMENT_ERRORS.md index d5a487a..b75de98 100644 --- a/md/COMFYUI_0.4.0_MODEL_MANAGEMENT_ERRORS.md +++ b/md/COMFYUI_0.4.0_MODEL_MANAGEMENT_ERRORS.md @@ -2,8 +2,8 @@ - - + +
EN中文EN中文
diff --git a/md/PERFORMANCE_OPTIMIZATION_PLAN.md b/md/PERFORMANCE_OPTIMIZATION_PLAN.md new file mode 100644 index 0000000..abef37a --- /dev/null +++ b/md/PERFORMANCE_OPTIMIZATION_PLAN.md @@ -0,0 +1,184 @@ +# 高速化計画 + +`wrappers/qwenimage.py` と `nunchaku_code/lora_qwen.py` を実際に読んだ上で作成。推測は入れない。 + +--- + +## 1. `_load_lora_state_dict_robust` にファイルキャッシュを追加 + +### 現状の事実 + +```python +def _load_lora_state_dict_robust(path_or_dict: Union[str, Dict]) -> Dict: + if isinstance(path_or_dict, dict): + return path_or_dict + path = str(path_or_dict) + if os.path.exists(path): + return comfy.utils.load_torch_file(path) +``` + +`compose_loras_v2` のループ内で各 `lora_configs` 要素ごとに呼ばれる。同一ファイルパスが複数回含まれていても、毎回 `comfy.utils.load_torch_file(path)` を呼ぶ。辞書キャッシュは存在しない。 + +### 高速化案 + +`_load_lora_state_dict_robust` に `functools.lru_cache(maxsize=None)` を付与し、同一パスの2回目以降のファイル読み込みを省略する。 + +```python +@functools.lru_cache(maxsize=None) +def _load_lora_state_dict_robust(path_or_dict: Union[str, Dict]) -> Dict: + ... +``` + +`path_or_dict` が `dict` の場合はキャッシュ不要だが、文字列/Path の場合はキャッシュが有効。`lru_cache` は `hashable` な引数のみ受け付けるため、文字列パスに限定する。 + +### 影響範囲 + +- `compose_loras_v2` 内の `for idx, (lora_path_or_dict, strength) in enumerate(lora_configs):` ループ +- 同一 LoRA ファイルが複数回指定された場合、2回目以降はメモリから取得 + +--- + +## 2. デバイス転送のバッチ化 + +### 現状の事実 + +```python +all_A.append(A.to(dtype=target_dtype, device=target_device)) +all_B_scaled.append((B * scale).to(dtype=target_dtype, device=target_device)) +``` + +各 LoRA の各 weight(A, B)ごとに個別の `.to(device=...)` を発行。`torch.cat` は転送後に行われる。 + +### 高速化案 + +CPU 上で `torch.cat` または `torch.stack` してから、1回の `.to(device)` を発行する。 + +```python +# 現状 +all_A.append(A.to(dtype=target_dtype, device=target_device)) +# ... +final_A = torch.cat(all_A, dim=0) + +# 変更後 +all_A_cpu.append(A.to(dtype=target_dtype, device="cpu")) +# ... +final_A_cpu = torch.cat(all_A_cpu, dim=0) +final_A = final_A_cpu.to(device=target_device) +``` + +### ただし + +- `target_device` は `module.qweight.device` または `module.proj_down.device` で、モジュールごとに異なる可能性がある +- 同一モジュール内の `all_A` は同一デバイスに転送されるため、モジュール単位でバッチ化は有効 + +--- + +## 3. `_awq_lora_forward` 内の `.to()` 削除または事前転送 + +### 現状の事実 + +```python +for (A_local, B_local) in module._nunchaku_lora_bundle: + A_local = A_local.to(device=device, dtype=dtype) + B_local = B_local.to(device=device, dtype=dtype) + lora_mid = x_flat @ A_local.transpose(0, 1) + lora_term = lora_mid @ B_local.transpose(0, 1) +``` + +`_apply_lora_to_module` で `A_local`, `B_local` をモジュールにアタッチする際、既に `target_device` に転送している: + +```python +all_A.append(A.to(dtype=target_dtype, device=target_device)) +# ... +mod._nunchaku_lora_bundle.append((A, B)) +``` + +しかし `_awq_lora_forward` では毎回 `.to(device=device, dtype=dtype)` を発行しており、すでに同じデバイス/dtype にあるテンソルに対する無駄なコピーが発生している可能性がある。 + +### 高速化案 + +`_nunchaku_lora_bundle` に格納する時点で、既に `target_device` / `target_dtype` に変換済みのテンソルを格納する。`_awq_lora_forward` 内の `.to()` を削除または、デバイス/dtype が一致している場合はスキップする。 + +```python +# _awq_lora_forward 内 +for (A_local, B_local) in module._nunchaku_lora_bundle: + # 既に同じデバイス/dtypeならスキップ + if A_local.device != device or A_local.dtype != dtype: + A_local = A_local.to(device=device, dtype=dtype) + if B_local.device != device or B_local.dtype != dtype: + B_local = B_local.to(device=device, dtype=dtype) + ... +``` + +--- + +## 4. `_get_module_by_name` のパスキャッシュ + +### 現状の事実 + +```python +def _get_module_by_name(model: nn.Module, name: str) -> Optional[nn.Module]: + module = model + for part in name.split("."): + if hasattr(module, part): + module = getattr(module, part) + elif part.isdigit() and _is_indexable_module(module): + module = module[int(part)] + else: + return None + return module +``` + +`compose_loras_v2` の `for module_name_key, weight_list in aggregated_weights.items():` ループ内で、毎回 `_resolve_module_name` → `_get_module_by_name` を呼ぶ。同一 `module_name_key` が複数回現れる場合、毎回ツリーを走査する。 + +### 高速化案 + +`compose_loras_v2` 内にローカルキャッシュ `{name: module}` を設け、2回目以降はキャッシュを使用する。 + +```python +_module_cache = {} +# ... +for module_name_key, weight_list in aggregated_weights.items(): + if module_name_key in _module_cache: + resolved_name, module = _module_cache[module_name_key] + else: + resolved_name, module = _resolve_module_name(model, module_name_key) + _module_cache[module_name_key] = (resolved_name, module) +``` + +--- + +## 5. `all_A` / `all_B_scaled` の事前確保 + +### 現状の事実 + +```python +all_A = [] +all_B_scaled = [] +for w in weight_list: + # ... + all_A.append(A.to(...)) + all_B_scaled.append((B * scale).to(...)) +final_A = torch.cat(all_A, dim=0) +final_B = torch.cat(all_B_scaled, dim=1) +``` + +`append` 後に `torch.cat` を行う。`cat` は新しいテンソルを割り当てるため、`append` された各テンソルのメモリは断片化する。 + +### 高速化案 + +`weight_list` のサイズが事前にわかっているため、`torch.empty` で事前に確保し、`copy_` で埋める。ただし、各 A/B のサイズ(rank)が異なるため、連続したメモリ確保は困難。`torch.cat` の代替として `torch.zeros` で事前確保する方法もあるが、複雑さが増す。 + +--- + +## 結論 + +| 最適化 | 期待効果 | 実装難易度 | +|--------|----------|------------| +| `_load_lora_state_dict_robust` の `lru_cache` | 中(同一ファイルの2回目以降省略) | 低 | +| デバイス転送のバッチ化(モジュール単位) | 小〜中(`.to()` の回数削減) | 中 | +| `_awq_lora_forward` の `.to()` 条件スキップ | 小(無駄なコピー排除) | 低 | +| `_get_module_by_name` のローカルキャッシュ | 小(走査コスト削減) | 低 | +| `all_A` / `all_B_scaled` の事前確保 | 小(メモリ断片化軽減) | 高(rank 可変のため) | + +**最も効果が高く実装が容易なのは 1(ファイルキャッシュ)と 3(AWQ forward の `.to()` スキップ)と 4(モジュールパスキャッシュ)です。** diff --git a/release/_gh_notes_gfm.txt b/release/_gh_notes_gfm.txt new file mode 100644 index 0000000..8aba030 --- /dev/null +++ b/release/_gh_notes_gfm.txt @@ -0,0 +1 @@ +GFM: remove blank lines between table rows (GitHub rendering) diff --git a/zhmd/COMFYUI_0.4.0_MODEL_MANAGEMENT_ERRORS.md b/zhmd/COMFYUI_0.4.0_MODEL_MANAGEMENT_ERRORS.md index 107f528..1f8669d 100644 --- a/zhmd/COMFYUI_0.4.0_MODEL_MANAGEMENT_ERRORS.md +++ b/zhmd/COMFYUI_0.4.0_MODEL_MANAGEMENT_ERRORS.md @@ -52,4 +52,4 @@ def __init__(self, model, load_device, offload_device, size=0, weight_inplace_up - **建议**: 1. 将 Nunchaku 库与 ComfyUI-Nunchaku 节点更新到最新版本(可缓解 `model_config` 相关问题) 2. 考虑对 ComfyUI 核心的 `model_management.py` 应用 None 检查类修复(可能从根因上缓解) -- **说明**:这是第一层支持措施。我们已在环境中对 ComfyUI 核心的 `model_management.py` 所应用的修复技术细节已单独整理发布。详见 [COMFYUI_0.4.0_UPDATE_ERROR_FIXES.md](../md/COMFYUI_0.4.0_UPDATE_ERROR_FIXES.md)。请注意,这些修复是在特定环境中实施的,未必适用于所有环境;也可能同时缓解 `copy.deepcopy` 与 `pinned` 属性相关错误。 +- **说明**:这是第一层支持措施。我们已在环境中对 ComfyUI 核心的 `model_management.py` 所应用的修复技术细节已单独整理发布。详见 [COMFYUI_0.4.0_UPDATE_ERROR_FIXES.md](../zhmd/COMFYUI_0.4.0_UPDATE_ERROR_FIXES.md)。请注意,这些修复是在特定环境中实施的,未必适用于所有环境;也可能同时缓解 `copy.deepcopy` 与 `pinned` 属性相关错误。 diff --git a/zhmd_github.md b/zhmd_github.md new file mode 100644 index 0000000..ee2739d --- /dev/null +++ b/zhmd_github.md @@ -0,0 +1,333 @@ + + + + + +
EN中文
+ +## Z-Image / SVDQ:`NoneType` 上的 `.dtype`——ComfyUI 延迟(惰性)`Linear`、`pop` 默认值求值与本节点包的缓解措施(详解) + +本文说明通过 **ComfyUI-nunchaku** 加载 Nunchaku 量化 Z-Image(Lumina2 / NextDiT)时可能出现的崩溃:**错误是什么、为何发生、根因分层、为何由 ComfyUI-QwenImageLoraLoader 吸收修复、改动了哪些文件、新增代码各自做什么**。 + +--- + +## 目标错误(典型用户报告) + +### 异常信息 + +``` +AttributeError: 'NoneType' object has no attribute 'dtype' + +``` + +### 堆栈要点(代表性) + +执行从 ComfyUI 节点管线进入,在 **ComfyUI-nunchaku Z-Image 模型构建 → Nunchaku `SVDQW4A4Linear.from_linear`** 处失败。 + +``` +File ".../ComfyUI/execution.py", line 525, in execute + ... +File ".../ComfyUI-nunchaku/nodes/models/zimage.py", line 215, in load_model + model = _load(sd, metadata=metadata) +File ".../ComfyUI-nunchaku/nodes/models/zimage.py", line 148, in _load + model = model_config.get_model(patched_sd, "", torch_dtype=torch_dtype) +File ".../ComfyUI-nunchaku/model_configs/zimage.py", line 71, in get_model + patch_model(out.diffusion_model, ...) +File ".../ComfyUI-nunchaku/models/zimage.py", line 320, in patch_model + _patch_transformer_block(diffusion_model.layers) +File ".../ComfyUI-nunchaku/models/zimage.py", line 317, in _patch_transformer_block + block.attention = ComfyNunchakuZImageAttention(block.attention, **kwargs) +File ".../ComfyUI-nunchaku/models/zimage.py", line 133, in __init__ + self.qkv = SVDQW4A4Linear.from_linear(orig_attn.qkv, **kwargs) +File ".../site-packages/nunchaku/models/linear.py", line 152, in from_linear + torch_dtype = kwargs.pop("torch_dtype", linear.weight.dtype) + ^^^^^^^^^^^^^^^^^^^ +AttributeError: 'NoneType' object has no attribute 'dtype' + +``` + +**实际断裂点:** `linear.weight` 为 `None`,但代码读取了 `linear.weight.dtype`。 + +--- + +## 症状摘要 + +| 项目 | 详情 | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **何时** | Z-Image(Nunchaku DiT)加载器构建模型且 `patch_model` 将 Attention / FF 换为 SVDQ 层时 | +| **何处** | `nunchaku.models.linear.SVDQW4A4Linear.from_linear`(以及 ComfyUI-nunchaku 中 `fuse_to_svdquant_linear` 的同类写法) | +| **环境** | 在 **Windows** 且 ComfyUI **AIMDO(内存优化)** 开启时使用 **`disable_weight_init.Linear`** —— 在应用 `state_dict` 之前权重保持为 **`None`** | +| **现象** | 工作流执行期间出现上述 `AttributeError` | +--- + +## 调用路径(逻辑流) + +### 1. ComfyUI-nunchaku Z-Image 加载(概要) + +典型顺序(实现要点): + +1. 加载 `state_dict` +2. 调用 `NunchakuZImage.get_model(patched_sd, "", torch_dtype=...)` +3. **`super().get_model(...)`** 构建 `model_base.Lumina2` → **构造 NextDiT**(各 `Linear` 上权重**可能尚未就绪**) +4. **`patch_model(diffusion_model, ...)`** 将例如 `JointAttention` 替换为 **`ComfyNunchakuZImageAttention`** +5. 在该构造器内执行 **`SVDQW4A4Linear.from_linear(orig_attn.qkv, kwargs)`** +6. **之后**(视加载器流程而定)`load_model_weights` 等才会填入权重 + +因此若 **「量化模块被 patch 的时机」** 与 **「ComfyUI 的 `Linear` 获得真实权重」** 不一致,则在 patch 时刻 **`weight` 仍为 `None`** 是可能的。 + +### 2. ComfyUI 核心:`disable_weight_init.Linear` 的含义 + +在 `comfy/ops.py` 中,**`disable_weight_init.Linear`** 在 **Windows 且 `aimdo_enabled`** 时大致行为: + +* 跳过常规 `torch.nn.Linear` 的 `super().__init__(...)`;仅 `torch.nn.Module.__init__` +* 初始 **`self.weight = None`**、**`self.bias = None`** +* 真实 **`Parameter`** 在 **`_load_from_state_dict`** 中赋值 + +**设计意图(摘要):** 用 `torch.empty` 分配占位权重会在 Windows 上急剧推高 commit charge 并导致不稳定。ComfyUI 改为延迟 / 从 `state_dict` 零拷贝。 + +因此在上述条件下 **`weight is None` 并非 bug** —— 而是 **ComfyUI 的预期行为**。 + +### 3. Nunchaku:`from_linear` 的隐含假设 + +`SVDQW4A4Linear.from_linear` 概念上应: + +* 从已有 `nn.Linear` 读取 **入/出特征、bias、dtype、device** +* 构建同形状的量化占位 `SVDQW4A4Linear`(训练权重稍后经 `state_dict` 到达) + +若 **隐含假设 `linear.weight` 永远非 `None`**,则与 ComfyUI 的延迟 `Linear` **冲突**。 + +--- + +## 真正的问题(两层) + +宜理解为 **复合问题**,而非单一根因。 + +### 层 A:Python 语义 —— `dict.pop(key, default)` 总会对 default 求值 + +问题行(修复前 nunchaku 的典型形态): + +``` +torch_dtype = kwargs.pop("torch_dtype", linear.weight.dtype) +``` + +许多开发者直觉认为: + +> 「若 `torch_dtype` 已在 `kwargs` 中,则不会用到 default,故不会求值 `linear.weight.dtype`。」 + +**在 Python 中这是错的。** **所有实参表达式都会在调用前求值。** 因此: + +* 即使 **`"torch_dtype": torch.bfloat16` 已在 `kwargs` 中** +* **`linear.weight.dtype` 仍会被求值** + +于是: + +* 当 **`linear.weight is None`** → **`None.dtype` → `AttributeError`** +* 即使调用方 **正确传入 `torch_dtype=`** → **仍会同一 `AttributeError`**(即「明明传了 kwargs 仍崩」的真实原因) + +**语言层面结论:** 把 `pop` 的 default 当作「惰性回退」是 **错误的**。若回退代价高、有副作用或可能失败,应使用 **`if "k" in kwargs`** 或 **`try/except KeyError`**。 + +### 层 B:顺序 —— ComfyUI 延迟 `Linear` 与 ComfyUI-nunchaku「先 patch」 + +即便修复层 A,**`linear.weight is None`** 仍留下 **dtype / device 从何处取得** 的问题。 + +* ComfyUI-nunchaku 常在 **`get_model` 内 `patch_model`,再 `load_model_weights`** +* 在 **`patch_model` 瞬间**,延迟 ComfyUI `Linear` 仍可能 **`weight is None`** + +**集成结论:** ComfyUI **「patch 时 Linear 可无权重」** 与 Nunchaku 从 **weight 读取 dtype/device** 的假设不匹配。 +再叠加 **层 A 的 `pop` bug**,导致 **调用方传入 `torch_dtype` 仍会崩溃**。 + +--- + +## 责任划分(上游「理应」修哪里) + +| 领域 | 最接近责任方 | 说明 | +| ------------------------------------------------ | ---------------------- | --------------------------------------------------------------------- | +| Windows + AIMDO 下的延迟 Linear | **ComfyUI 核心** | 有意优化;行为应作为规格文档化 | +| `from_linear` 使用 `pop(..., linear.weight.dtype)` | **Nunchaku(库)** | 不符合 Python 语义;在 weight 为 `None` 时可安全改写 | +| `patch_model` 与 `load_model_weights` 的顺序 | **ComfyUI-nunchaku** | 设计选择:无权重 patch vs 重排 | +| 各用户环境立即可用的修复 | **无人单独保证** | 等 PR / release / review 会长期阻塞用户 | +**在本节点包内吸收修复** 与 `md/COMFYUI_0.4.0_MODEL_MANAGEMENT_ERRORS.md` 等文档中的务实模式一致。 + +--- + +## 为何由 ComfyUI-QwenImageLoraLoader 吸收 + +### 1. 与既有架构一致 + +本仓库已在 **`__init__.py` 启动时对 `patches/nunchaku_patch.py` 打补丁**: + +* **Nunchaku Qwen:** 对 `NunchakuQwenImageTransformerBlock.forward` 的 **Manual Planar Injection**(LoRA) +* 对脆弱 import 路径的 **`sys.modules` 扫描** + +本次变更是 **同一 `apply_nunchaku_patch()` 伞盖下** 的兼容扩展。 + +### 2. 用户侧改动面更少 + +* 手改 **`site-packages/nunchaku`** → 升级即丢失,难复现 +* 修补 **ComfyUI 核心** → 更新冲突,向他人解释成本高 +* 固定 **ComfyUI-nunchaku fork** → 维护成本 + +已安装 **LoRA 节点包的用户已信任本包** —— **在此集中修复** 可降低运维成本。 + +### 3. 加载顺序问题本就存在 + +文件夹名字顺序可能导致 **本包先于 ComfyUI-nunchaku** 加载。 +则启动时 **`models.zimage` 可能尚未进入 `sys.modules`**。 + +**一次性 patch 不够**;需要 **延迟重试** —— 与本仓库已处理的「环境相关」模式相同。 + +--- + +## 变更文件 + +| 文件 | 变更 | +| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| **ComfyUI-QwenImageLoraLoader/patches/nunchaku_patch.py** | 替换 `SVDQW4A4Linear.from_linear`、动态替换 `fuse_to_svdquant_linear`、重试调度器、从 `apply_nunchaku_patch` 接入 | +| **ComfyUI-QwenImageLoraLoader/__init__.py** | 成功日志文案更新,提及 lazy-Linear 缓解 | +**说明:** 本仓库 **不** 在树内修改 ComfyUI-nunchaku 或 nunchaku(避免与上游漂移)。 + +--- + +## 新增 / 变更代码及含义(按小节) + +以下为 **说明用途**;行号可能漂移 —— 以仓库源码为准。 + +### A. `_torch_device_fallback` + +**作用:** 当 **`linear.weight is None`** 时,**推断 `device`**。 + +1. 优先 **`comfy.model_management.get_torch_device()`**(与 ComfyUI 所选推理设备一致) +2. 失败时(测试等)简单回退 **`cuda` / `cpu`** + +**原因:** 即使稍后 `load_model_weights` 会填张量,构造阶段 **设备一致** 对量化 `Parameter` 更安全。 + +### B. `_patched_svdqw4a4_from_linear`(`SVDQW4A4Linear.from_linear` 的替换) + +**修复:** + +1. 停止使用错误的 **`pop`**;用 **`"torch_dtype" in kwargs`** 分支 → **已提供 `torch_dtype` 时不触碰 `linear.weight`** +2. **`weight is None`** 时要求 **`kwargs` 中必须有 `torch_dtype`**,否则抛出明确 **`TypeError`**(可调试性) +3. **`device`** 同样模式;缺失则用 **`_torch_device_fallback()`** + +**绑定为 `classmethod`:** 原 `from_linear` 为类方法,故 `SVDQW4A4Linear.from_linear = classmethod(_patched_svdqw4a4_from_linear)`。 + +### C. `_make_patched_fuse_to_svdquant_linear` 及替换 + +ComfyUI-nunchaku 的 **`fuse_to_svdquant_linear`**(Z-Image 的 FF w1/w3 融合)可能使用 **相同的 `pop(..., weight.dtype)` 模式**。 +仅修 Attention 的 `qkv` / `out` 仍可能在 **下一 FF 块** 失败。 + +**做法:** + +* 从目标模块取出 **`add_comfy_cast_weights_attr`** 并保留原行为(offload 兼容用的占位属性) +* 仅内层逻辑使用 **与 `from_linear` 相同的安全分支** + +### D. `apply_svdqw4a4_lazy_linear_patch` + +* 成功导入 **`nunchaku.models.linear.SVDQW4A4Linear`** 后 **覆盖 `from_linear`** +* **`_svdq_from_linear_patched` 标志** 防止重复应用(重载 / 重复执行韧性) + +### E. `apply_nunchaku_zimage_fuse_lazy_linear_patch` + +**模块识别启发式**(减少误伤): + +* 含 **`fuse_to_svdquant_linear`** +* 含 **`ComfyNunchakuZImageAttention`**(Z-Image nunchaku patch 模块指纹) +* 含 **`add_comfy_cast_weights_attr`**(同文件辅助函数) + +已 patch 的函数带 **`_qwen_lora_loader_lazy_linear_patch`** 以跳过重复 patch。 + +### F. `schedule_nunchaku_zimage_fuse_patch_retries` + +**问题:** 若本包 **先** 导入(例如按字母序),启动时 **`zimage` 可能尚未加载**。 + +**缓解:** + +* 短延迟后 **重试 `apply_nunchaku_zimage_fuse_lazy_linear_patch()`** +* 最长约 **6 秒**(0.25s × 24),待 **ComfyUI-nunchaku** 加载后再替换 **`fuse`** + +**权衡:** 后台 **`threading.Timer`**。**非 daemon** 但 **短生命周期且有界**,不应表现为永久泄漏。 + +### G. `apply_nunchaku_patch` 集成 + +惰性 `Linear` 修复与既有 **Manual Planar Injection** **一并**运行(同函数内先 lazy 路径再 planar)。 + +**返回值:** + +* **`True`**:**Planar patch** 成功 **或** **`from_linear` patch** 成功 +* **`False`**:二者皆否(例如未安装 nunchaku) + +### H. `__init__.py` 日志 + +成功日志明确提及 **Z-Image / SVDQ / ComfyUI 延迟 `Linear` 权重**,便于从启动日志判断 **何种缓解已激活**。 + +--- + +## 如何在启动日志中确认 + +若 ComfyUI 启动日志出现类似行,则 **lazy-`Linear` patch 很可能已生效**: + +* `Patched SVDQW4A4Linear.from_linear for ComfyUI lazy Linear ...` +* `Patched fuse_to_svdquant_linear in ...` + +**`fuse`** patch 可能因 **延迟重试** 而 **数秒后才出现**。 + +--- + +## 残留限制 / 注意事项 + +1. 若 **`kwargs` 缺少 `torch_dtype` 且 `weight is None`**,本 patch **有意** 抛出 **`TypeError`**。 + * 倾向 **显式失败** 而非 **静默损坏**。 + * 只要 ComfyUI-nunchaku 的 Z-Image 加载器持续传入 **`torch_dtype=`**,正常使用不应触发。 +2. **Monkey-patch 一般性:** 若上游改动相同入口(`SVDQW4A4Linear.from_linear`、ComfyUI-nunchaku 的 `fuse_to_svdquant_linear`),可能出现 **非预期行为漂移**。见下文 **§4**。 +3. **`sys.modules` 启发式:** 理论上其他模块也可能满足检查;风险低,且由 **强 Z-Image 指纹**(`ComfyNunchakuZImageAttention`)缓解。 + +### 4. 若上游(Nunchaku / ComfyUI-nunchaku)并入等效修复 + +本缓解在启动时 **替换** 已安装 nunchaku 的 **classmethod** 与 ComfyUI-nunchaku 的 **模块级函数**。若上游 **并入正式修复**(与其它文档「副作用」第 **6** 条同类),行为可整理如下。 + +#### 4.1 双重 patch? + +* **可能。**`apply_nunchaku_patch()` **仍会在可行时替换 `from_linear`**,与上游版本无关。 +* 若 **意图一致**(去掉 `pop` 误用、对 `weight is None` 分支),**害处通常很小**:本 patch 先执行,再与正确上游路径对齐 —— **极少产生结果级冲突**。 +* 若上游 **方案不同**(例如 `from_linear` API 变更、迁至其他类),**此处陈旧替换** 可能导致 **仅一层生效 / 不同异常**。 + +#### 4.2 上游 API / 签名变更 + +* 例如:`SVDQW4A4Linear.from_linear(cls, linear, **kwargs)` 增加参数、不再是 `classmethod`、返回类型变更、委托给其他辅助函数等。 +* 则 **钉死在旧契约上的 `_patched_svdqw4a4_from_linear`** 可能 **启动通过但运行失败** 或 **静默偏离**。 +* **缓解:** **更新本 patch**,或在上游已足够处 **移除 / 禁用** lazy-`Linear` patch,或 **按 nunchaku / ComfyUI-nunchaku 版本门控** 并在新 release 上跳过 patch。 + +#### 4.3 维护建议 + +* 当发行说明 / issue 写明 **「lazy Linear + `from_linear`」已在上游修复** 时,请 **重新评估该版本起是否仍需要** 本 patch。 +* 移除时清理 **`patches/nunchaku_patch.py`**、**`apply_nunchaku_patch` 调用点**、**`schedule_nunchaku_zimage_fuse_patch_retries`**,若仅需 **Planar Injection** 则保留后者。 +* 在本文档 **变更记录** 中记下:「上游吸收修复后已移除 ○○」供后续读者查阅。 + +#### 4.4 用户可见现象 + +* 常见情况:**仅更新上游而本包较旧** 仍 **可工作**,因本 patch 在加载时 **覆盖**。 +* 若异常,怀疑 **nunchaku / ComfyUI-nunchaku / 本包组合**;**更新本包** 或 **暂时禁用** 以仅复现上游。 + +--- + +## 与其它文档的关系 + +| 文档 | 关系 | +| --------------------------------------------- | ----------------------------------------------------------------------- | +| [COMFYUI_0.4.0_MODEL_MANAGEMENT_ERRORS.md](COMFYUI_0.4.0_MODEL_MANAGEMENT_ERRORS.md) | 相同 **前提**:核心修复滞后;**用户环境先坏** | +| [QWEN_IMAGE_CONTROLNET_AND_GETATTR_FIX.md](../md/QWEN_IMAGE_CONTROLNET_AND_GETATTR_FIX.md) | 相同 **策略**:**包装 / patch** 以满足上游契约 | +| [ZIMAGETURBO_CONTROLNET_FIX.md](../md/ZIMAGETURBO_CONTROLNET_FIX.md) | Z-Image 系:**transformer_options** 等,**补管线缺口** | +本文在 **Z-Image × Nunchaku SVDQ × ComfyUI `Linear` × Python `pop`** 的交叉点上增加一层 **兼容层**。 + +--- + +## 一句话总结 + +**ComfyUI 在部分条件下可有意保持 `Linear.weight` 为 `None`**,而 **Nunchaku 的 `from_linear` 误用了 `pop` 的 default**,共同导致 **`None.dtype`**;**ComfyUI-nunchaku 的 FF 融合可共享同一脆弱性**;**本仓库扩展 `apply_nunchaku_patch`,以 monkey-patch 与延迟重试使用户不必苦等上游发布。** + +--- + +## 文档变更记录 + +* 初稿:说明惰性 `Linear`、`pop` 默认值求值及在本仓库中的吸收方式 +* 增补:**§4** —— 双重 patch、API 漂移及上游并入同修复后的维护(由副作用第 6 条扩展) \ No newline at end of file