From 8cffc48ce49fe2ea6dfdb687052350ed922e5229 Mon Sep 17 00:00:00 2001 From: ussoewwin <136552381+ussoewwin@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:20:01 +0900 Subject: [PATCH] docs: add v2.6.8 Chinese release notes (zhmd) --- zhmd/v2.6.8.md | 142 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 zhmd/v2.6.8.md diff --git a/zhmd/v2.6.8.md b/zhmd/v2.6.8.md new file mode 100644 index 0000000..226244e --- /dev/null +++ b/zhmd/v2.6.8.md @@ -0,0 +1,142 @@ + + + + + +
EN中文
+ +## 1. 变更内容与原因 + +### 1.1 背景:ComfyUI v0.44.0 通过 cast 契约供给权重 + +自 ComfyUI v0.44.0 起(dynamic VRAM / aimdo 默认开启),模块 `forward` 执行时层权重不再保证位于计算设备上。权重保持文件形态(CPU / 虚拟内存后端),直到该模块加入核心的权重供给契约: + +```python +# comfy/ops.py (v0.44.0, disable_weight_init.Linear.forward) +def forward(self, *args, **kwargs): + run_every_op() + if self.comfy_cast_weights or len(self.weight_function) > 0 or len(self.bias_function) > 0: + return self.forward_comfy_cast_weights(*args, **kwargs) # 契约路径 + else: + return super().forward(*args, **kwargs) # 直接读 self.weight +``` + +契约路径(`forward_comfy_cast_weights` → `CastBiasWeightContext` → `cast_bias_weight`)负责把权重送到输入设备:vbar 管理的层走 `comfy/ops.py:396-414`,普通张量走 `comfy/ops.py:417-462`。未加入契约的模块直接读 `self.weight`,拿到的是权重当时碰巧所在的设备。 + +经由 `ModelPatcher` 创建并加载的模块由核心完成 arm。问题出在这条路径之外。 + +### 1.2 症状 + +Qwen Image ControlNet 不经过 wrapper 的 `forward` 调用扩散模型,而是直接伸手到 base model(`comfy/ldm/qwen_image/controlnet.py:151-162`): + +```python +hidden_states = base_model.img_in(hidden_states) +encoder_hidden_states = base_model.txt_norm(context) +encoder_hidden_states = base_model.txt_in(encoder_hidden_states) +temb = base_model.time_text_embed(timesteps, hidden_states) +``` + +这里的 `base_model` 是本包的 `ComfyQwenImageWrapper`,其 `__getattr__` 把未知属性转发给内层 nunchaku 模型。借来的 `img_in` 是一个 `comfy_cast_weights = False` 的普通 `disable_weight_init.Linear` —— 从未加入契约。首次失败形如: + +``` +File "comfy/samplers.py", line 330, in _calc_cond_batch + c['control'] = control.get_control(input_x, timestep_, c, len(cond_or_uncond), transformer_options) +File "comfy/controlnet.py", line 320, in get_control + control = self.control_model(x=x_noisy.to(dtype), hint=self.cond_hint, ...) +File "comfy/ldm/qwen_image/controlnet.py", line 151, in forward + hidden_states = base_model.img_in(hidden_states) +File "comfy/ops.py", line 641, in forward + return super().forward(*args, **kwargs) +RuntimeError: Expected all tensors to be on the same device, but got mat1 is on cuda:0, +different from other tensors on cpu (when checking argument in method wrapper_CUDA_addmm) +``` + +### 1.3 为什么第一次成功、第二次失败 + +在受影响的工作流上连续实测 10 次(ComfyUI v0.44.0,torch 2.15.0+cu134): + +| 次序 | ControlNet loader 是否重新执行 | 结果 | 耗时 | +|---|---|---|---| +| 1 | 是 | 成功 | 106.06 s | +| 2 | 否(节点缓存) | 失败 | 31.56 s | +| 3 | 是 | 成功 | 68.24 s | +| 4-10 | 否(节点缓存) | 失败 | 5.5 - 9.5 s | + +- loader 重新执行时,ControlNet 的 state dict 被重新读取(`Found quantization metadata version 1`),权重在采样前经加载路径到达 GPU。 +- ComfyUI 的节点输出缓存返回缓存的 `CONTROL_NET` / `MODEL` 对象时,loader 不会再次执行。工作流末端的 purge 节点此前已经 unload 了模型,于是缓存对象携带的权重已被放回 CPU / 文件形态——而被借用的模块仍然直接读取它们。 + +因此这是一个生命周期缝隙:缓存复用 + purge + 一条位于核心契约之外的直接属性借用。当 v0.44.0 改变了默认权重放置方式后,它才变成致命问题。 + +## 2. 修复:把借用的模块接入核心契约 + +`wrappers/qwenimage.py` —— `ComfyQwenImageWrapper.__init__` 现在调用一个静态辅助方法: + +```python +# wrappers/qwenimage.py (v2.6.8) +_CONTROLNET_BORROWED_MODULES = ( + "img_in", + "txt_in", + "txt_norm", + "time_text_embed", + "pe_embedder", +) + +@staticmethod +def _enable_weight_supply_contract(model): + """Opt the modules ControlNet borrows into ComfyUI's cast contract.""" + for attribute in _CONTROLNET_BORROWED_MODULES: + root = getattr(model, attribute, None) + if root is None: + continue + seen = set() + stack = [root] + while stack: + module = stack.pop() + if id(module) in seen: + continue + seen.add(id(module)) + if hasattr(module, "comfy_cast_weights") and not module.comfy_cast_weights: + module.comfy_cast_weights = True + stack.extend(module.children()) + +# ComfyQwenImageWrapper.__init__ 的最后一行: +self._enable_weight_supply_contract(model) +``` + +性质: + +- **范围限定**:只触碰 ControlNet 实际访问 `base_model` 的 5 个属性(`img_in`、`txt_in`、`txt_norm`、`time_text_embed`、`pe_embedder`)及其子模块;模型其余部分保持构造时的原样(下方实测)。 +- **幂等**:已加入契约的模块直接跳过。 +- **追随核心而不是对抗核心**:dynamic VRAM 保持开启;不需要启动参数、不改 ComfyUI 本体、不改 nunchaku、不改工作流。核心移动权重时,`cast_bias_weight` 在每次 forward 把权重供给到输入设备——包括 ControlNet 这条路径。 + +## 3. 本版本发布后的仓库状态 + +| 路径 | 变更 | 说明 | +|---|---|---| +| `wrappers/qwenimage.py` | +38 / −0 | `ComfyQwenImageWrapper` 把借用模块接入权重供给契约 | +| ComfyUI 本体 / nunchaku 包 / 工作流 | 无改动 | 本包之外的文件零修改 | +| `RELEASE_NOTES/changelog.md` / `zhmd/changelogzh.md` | changelog | v2.6.8 条目(合计 +16 / −2) | +| `pyproject.toml` / `__init__.py` | 版本号 | 2.6.7 → 2.6.8 | + +## 4. 实测验证 + +* 契约关闭复现:权重在 CPU、`comfy_cast_weights = False` 时,`F.linear` 抛出与上文完全相同的 `RuntimeError`(python_embeded,torch 2.15.0+cu134,cuda:0)。 +* 契约开启:同一层返回 CUDA 结果;对单位权重,输出与输入一致(`maxdiff` 0.0)。 +* 范围:`img_in` / `txt_in` 由 `False` 翻转为 `True`;5 个借用属性之外的同级 Linear 保持 `False`。 +* 部署副本:与仓库文件逐字节一致(SHA256 `20061EBC58DBFCC1BDCAF33EC8628F6AA86F7339B5229BDEBBF9328F55903262`),`py_compile` 通过。 +* 真实工作流:受影响的 i2i 工作流(ControlNet + Nunchaku Qwen Image + LoRA 栈 + EasyCache)在 ComfyUI v0.44.0 上连续执行正常——无 `RuntimeError`、无 fallback latent 输出、每次都出图。 + +## 5. 需要用户做什么 + +* 通过 ComfyUI Manager / git pull 更新本包到 v2.6.8,重启 ComfyUI。 +* 无需修改工作流、无需改 ComfyUI 本体、无需改 nunchaku、无需任何启动参数。 + +## 6. 备注 + +* `_CONTROLNET_BORROWED_MODULES` 中的 5 个属性是 Qwen Image ControlNet 当下访问 `base_model` 的模块属性全集的实测范围(`comfy/ldm/qwen_image/controlnet.py:143-162`;`process_img` 是方法调用,不需要契约)。若上游将来增加借用属性,把它们加进同一元组即可。 +* 工作流末端的 purge 节点与 ComfyUI 的节点输出缓存保持原样;契约到位后,它们对这条路径不再构成影响。 + +## 7. 相关链接 + +* 变更提交:https://github.com/ussoewwin/ComfyUI-QwenImageLoraLoader/commit/b8cd1df +* English version: https://github.com/ussoewwin/ComfyUI-QwenImageLoraLoader/releases/tag/v2.6.8