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 条扩展)