【Bug已解决】Handling of .alpha in from-comfy converters 解决方案一、现象长什么样把 ComfyUI 导出的 LoRA / 模型权重转成 diffusers 格式时用from_comfy系列转换器加载后 LoRA 的效果强度明显不对——要么几乎没作用要么过强把画面改得面目全非from diffusers.loaders import FromComfyLoader # 示意 pipe FromComfyLoader.from_comfy( model.safetensors, # ComfyUI 导出的权重 ).to(diffusers_pipe)现象同样的 LoRA在 ComfyUI 里强度 1.0 效果正常转成 diffusers 后强度 1.0 几乎看不见要调到 3~5 才像或者反过来diffusers 里 1.0 就过爆日志无报错但state_dict检查能看到 LoRA 的alpha相关 key 在转换后「消失」或「被当成普通权重」。最迷惑的是权重形状都对、能加载、能出图就是强度错位。这是典型的「LoRA 的 alpha 缩放rank/dim 与 alpha 的换算在转换器里没处理好」。二、背景LoRA 的核心公式以线性层为例ΔW (B A) * (alpha / rank)其中A、B是低秩分解权重rank也叫r是 inner 维度alpha是缩放系数。实际应用时LoRA 的贡献要乘以alpha / rank这个缩放比。ComfyUI 的 LoRA 文件里相关 key 通常会带.alpha或直接存了alpha值、或存了alpha与rank。diffusers 的from_comfy转换器需要从 ComfyUI 的 key 里识别alpha与rank把ΔW BA按alpha/rank缩放后合并进 base 权重或作为可调 LoRA 层保留缩放因子若 diffusers 侧的 LoRA 约定「权重已含 alpha 缩放」或「不含」要对应转换。问题在于转换器在重构 key 时漏掉了.alpha字段的处理——要么把alpha当成了普通权重名字的一部分导致 key 对不上要么读完rank却没读alpha缩放比用了默认alpharank即缩放1于是 ComfyUI 里alpha≠rank的 LoRA 转过来强度全错。这就是 #14088「Handling of .alpha in from-comfy converters」要修的点。三、根因根因一句话from_comfy转换器在重构 ComfyUI LoRA 权重时没有正确处理.alpha字段——漏读alpha或把它当普通 key导致 LoRA 缩放比alpha/rank算错转换后 LoRA 强度严重偏离原意。三点展开alpha 漏读转换器只解析rank没解析同文件的alpha缩放比退回alpharank即 1alpha≠rank时强度错。alpha 当普通 key.alpha后缀被当成权重名一部分key 映射失败或误合并。两侧约定不一致diffusers 侧 LoRA 是否「已含 alpha 缩放」的约定没和 ComfyUI 对齐重复缩放或漏缩放。不是权重形状错是「alpha 缩放比」在转换中丢了。四、最小可运行复现不依赖真实文件模拟「alpha 漏处理导致强度错」def apply_lora(base, A, B, alpha, rank): # 正确ΔW 按 alpha/rank 缩放 return base (B A) * (alpha / rank) def from_comfy_buggy(base, A, B, rank, alpha_in_fileNone): # 错误转换器没读 alpha默认 alpharank - 缩放1 alpha alpha_in_file if alpha_in_file is not None else rank # 但 buggy 实现直接用了 alpharank 忽略文件值 alpha rank return base (B A) * (alpha / rank) import torch base torch.zeros(4, 4) A torch.randn(4, 2); B torch.randn(2, 4) rank, alpha 2, 8 # ComfyUI 里 alpha8, rank2缩放应为 4 correct apply_lora(base, A, B, alpha, rank) buggy from_comfy_buggy(base, A, B, rank) print(正确缩放后 ΔW 范数:, correct.abs().sum().item()) print(漏 alpha 后 ΔW 范数:, buggy.abs().sum().item()) # 漏 alpha 时缩放1正确应4差 4 倍 - 强度严重错跑出来正确缩放alpha/rank4与漏 alpha缩放1差 4 倍LoRA 强度完全错位。这就是「转过来强度不对」的精确复现。五、解决方案第一层最小直接修复最小修复在from_comfy转换器里正确读取 ComfyUI 权重中的.alpha以及rank用alpha/rank缩放低秩分解再合并/注册若 diffusers 侧 LoRA 约定不含该缩放则保留缩放因子。import torch import re def parse_comfy_lora_key(key: str): 从 ComfyUI key 解析模块路径、rank、alpha 线索。 # 例: lora_unet_double_blocks_0_img_mod_lin.alpha m re.match(r^(?Pbase.?)(?:\.(alpha|rank|lora_up|lora_down))?$, key) info m.groupdict() return info[base], info.get(suffix) # suffix 可能是 alpha/rank/lora_up/lora_down def convert_comfy_lora(state_dict: dict, scale_default_alphaNone): 把 ComfyUI LoRA 转成 diffusers 可用的按 alpha/rank 缩放。 # 收集 alpha / rank / up / down meta {} # base - {alpha, rank} up, down {}, {} for k, v in state_dict.items(): base, suffix parse_comfy_lora_key(k) if suffix alpha: meta.setdefault(base, {})[alpha] v.item() if hasattr(v, item) else v elif suffix rank: meta.setdefault(base, {})[rank] v.item() if hasattr(v, item) else v elif suffix lora_up or k.endswith(lora_up.weight): up[base] v elif suffix lora_down or k.endswith(lora_down.weight): down[base] v out {} for base, uv in up.items(): dv down[base] rank meta.get(base, {}).get(rank, dv.shape[0]) alpha meta.get(base, {}).get(alpha, scale_default_alpha if scale_default_alpha else rank) scale alpha / rank delta (uv dv) * scale # 按 alpha/rank 缩放 out[base .weight] delta # 合并后的 LoRA 增量 return out要点解析.alpha、.rank、.lora_up、.lora_down四类 key不漏.alpha。scale alpha / rankLoRA 增量按此缩放强度对齐 ComfyUI。diffusers 侧若期望「未缩放」权重则把scale保留为可配置若期望「已含缩放」则直接合并。这一步单独就让 ComfyUI LoRA 转 diffusers 强度正确。六、解决方案第二层结构性改进第一层是「在转换器里修解析」。但 diffusers 多种 ComfyUI 转换器LoRA / checkpoint / 不同模型都需一致处理 alpha。更稳的做法把「alpha/rank 缩放解析」收敛成单一策略。from dataclasses import dataclass, field from typing import Dict, Optional dataclass class ComfyAlphaResolver: ComfyUI - diffusers 中 alpha/rank 缩放的单一策略。 # diffusers 侧 LoRA 是否期望「已含 alpha 缩放」的权重 expects_scaled: bool True def resolve_scale(self, alpha: Optional[float], rank: int) - float: if alpha is None: # 没读到 alpha按约定若 expects_scaled 则默认 alpharank缩放1 alpha rank return alpha / rank def merge(self, up: dict, down: dict, meta: Dict[str, dict]) - dict: out {} for base, uv in up.items(): dv down[base] m meta.get(base, {}) rank int(m.get(rank, dv.shape[0])) alpha m.get(alpha) scale self.resolve_scale(alpha, rank) delta (uv dv) * scale if not self.expects_scaled: # diffusers 侧会自行乘 alpha/rank这里返回未缩放 delta uv dv out[base .weight] delta return out def parse(self, state_dict: dict): 统一解析出 up/down/meta供 merge 使用。 up, down, meta {}, {}, {} for k, v in state_dict.items(): if k.endswith(lora_up.weight): up[k[: -len(lora_up.weight)]] v elif k.endswith(lora_down.weight): down[k[: -len(lora_down.weight)]] v elif k.endswith(.alpha): meta.setdefault(k[: -len(.alpha)], {})[alpha] _scalar(v) elif k.endswith(.rank): meta.setdefault(k[: -len(.rank)], {})[rank] _scalar(v) return up, down, meta def _scalar(v): return v.item() if hasattr(v, item) else v # 用法 resolver ComfyAlphaResolver(expects_scaledTrue) up, down, meta resolver.parse(state_dict) merged resolver.merge(up, down, meta)结构收益单一策略alpha/rank 解析与缩放集中在ComfyAlphaResolver多转换器复用。可配置约定expects_scaled对齐 diffusers 两侧是否含缩放避免重复/漏缩放。可校验解析出的meta可断言 alpha 数量与 rank 匹配。七、解决方案第三层断言 / CI 守护写 pytest 守三条(1) alpha 正确解析(2) 缩放比 alpha/rank(3) 两侧约定一致不重复缩放。import torch import pytest from your_lib import ComfyAlphaResolver, _scalar def test_alpha_parsed(): sd { a.lora_up.weight: torch.randn(4, 2), a.lora_down.weight: torch.randn(2, 4), a.alpha: torch.tensor(8.0), a.rank: torch.tensor(2.0), } r ComfyAlphaResolver() up, down, meta r.parse(sd) assert meta[a][alpha] 8.0 assert meta[a][rank] 2.0 def test_scale_is_alpha_over_rank(): r ComfyAlphaResolver() assert r.resolve_scale(8.0, 2) 4.0 assert r.resolve_scale(None, 4) 1.0 # 缺 alpha 默认rank def test_merge_applies_scale(): r ComfyAlphaResolver(expects_scaledTrue) up {a: torch.eye(4, 2)} down {a: torch.eye(2, 4)} meta {a: {alpha: 8.0, rank: 2.0}} merged r.merge(up, down, meta) # eyeeye eye, *scale(4) - 对角4 assert torch.allclose(merged[a.weight].diag(), torch.full((4,), 4.0)) def test_no_double_scale_when_unscaled(): r ComfyAlphaResolver(expects_scaledFalse) up {a: torch.eye(4, 2)} down {a: torch.eye(2, 4)} meta {a: {alpha: 8.0, rank: 2.0}} merged r.merge(up, down, meta) # 未缩放对角1 assert torch.allclose(merged[a.weight].diag(), torch.ones(4))CI 常驻跑这四条后任何「alpha 又漏」「缩放比错」「重复缩放」的回归都会立刻爆红。八、排查清单ComfyUI LoRA 转 diffusers「强度不对」时按顺序查先确认是不是「能加载、能出图、但 LoRA 强度错位」——是的话定位 alpha 缩放。打印转换后state_dict看是否含alpha/rank相关 key是否被漏读。确认转换器用alpha/rank缩放低秩增量而非默认alpharank缩放1。确认 diffusers 侧 LoRA 约定权重是否已含 alpha 缩放避免重复/漏缩放。多模型转换器SDXL/Flux/PixArt都过ComfyAlphaResolver统一解析。升级 diffusers 后跑「已知 alpha/rank 的 ComfyUI LoRA 转换 强度校验」冒烟。对比 ComfyUI 与 diffusers 同一 LoRA 同强度出图确认视觉一致。九、小结from_comfy转换器里 LoRA 强度错位根子是转换器漏处理 ComfyUI 权重的.alpha字段alpha/rank缩放比算错退回 1alpha≠rank的 LoRA 转过来强度全偏。修复三层次第一层解析.alpha/.rank/.lora_up/.lora_down并按alpha/rank缩放增量第二层用ComfyAlphaResolverdataclass 把 alpha/rank 解析与两侧缩放约定收敛为单一策略第三层用 pytest 守「alpha 解析」「缩放比正确」「不重复缩放」。工程启示LoRA 的「强度」全在alpha/rank缩放比上。任何 ComfyUI ↔ diffusers 的 LoRA 转换器都必须把.alpha当一等公民解析并明确「两侧谁负责缩放」——漏 alpha 或重复缩放视觉上就是强度错位且没有任何报错提示最容易被当成「模型不行」误判。