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 @@
+
+
+## 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