docs: add v2.6.8 Chinese release notes (zhmd)

This commit is contained in:
ussoewwin
2026-11-06 19:20:01 +09:00
parent b3b6d11423
commit 8cffc48ce4
+142
View File
@@ -0,0 +1,142 @@
<table align="center">
<tr>
<td align="center" bgcolor="#e5e7eb" width="88" height="36"><a href="https://github.com/ussoewwin/ComfyUI-QwenImageLoraLoader/releases/tag/v2.6.8"><font color="#4b5563"><b>EN</b></font></a></td>
<td align="center" bgcolor="#3478ca" width="88" height="36"><font color="#ffffff"><b>中文</b></font></td>
</tr>
</table>
## 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