Files

333 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<table align="center">
<tr>
<td align="center" bgcolor="#e5e7eb" width="88" height="36"><a href="https://github.com/ussoewwin/ComfyUI-QwenImageLoraLoader/releases/tag/v2.4.3"><font color="#4b5563"><b>EN</b></font></a></td>
<td align="center" bgcolor="#d4465e" width="88" height="36"><font color="#ffffff"><b>中文</b></font></td>
</tr>
</table>
## 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 条扩展)