【Bug已解决】from_single_file: CLIPTextModel has no attribute 'text_model' with transformers >= 5.6 解决方案
一、现象长什么样
用from_single_file从单个权重文件(如.safetensors/.ckpt)加载包含 CLIP 文本编码器的模型时,在transformers>=5.6环境下直接崩:
from diffusers import StableDiffusionPipeline pipe = StableDiffusionPipeline.from_single_file("v1-5-pruned.safetensors")报错:
AttributeError: 'CLIPTextModel' object has no attribute 'text_model'或者更隐蔽:
KeyError: 'text_model.encoder.layers.0.self_attn.q_proj.weight'现象总结:from_single_file的权重映射代码里,把 CLIP 文本编码器的内部结构当成「CLIPTextModel包了一层text_model」来访问(如model.text_model.encoder...),但 transformers>=5.6 把CLIPTextModel的text_model包装层移除了(内部结构扁平化),于是from_single_file访问text_model属性直接AttributeError,或按text_model.*前缀去映射权重时 KeyError。
二、背景
早期 transformers 里CLIPTextModel的结构是「外层CLIPTextModel+ 内层text_model(CLIPTextTransformer)」两层。所以很多单文件加载器的 key 映射写成:
text_model.encoder.layers.0.self_attn.q_proj.weight并在代码里通过model.text_model去取子模块、拼 key。但从 transformers 5.6 起,这个包装层被移除/扁平化——CLIPTextModel本身直接持有encoder/embeddings,不再有text_model这一层。
于是from_single_file这段「假设有两层」的代码在 5.6 上:
- 访问
model.text_model→AttributeError; - 或按
text_model.*去匹配 checkpoint 里text_model.encoder.*的 key,映射到扁平后的encoder.*失败 →KeyError/ 权重漏加载。
三、根因
根因两点:
- 加载器假设 CLIP 有
text_model包装层:from_single_file的映射代码写死了model.text_model.xxx,transformers>=5.6 扁平化后属性不存在。 - key 前缀
text_model.未随版本调整:checkpoint 权重 key 可能仍含text_model.(旧格式),但目标模型是扁平结构,映射规则没做「去text_model.前缀」的兼容。
本质:单文件加载器对 CLIP 内部结构的两层假设,和 transformers>=5.6 的扁平化实现冲突,且缺少按 transformers 版本适配的兼容层。
四、最小可运行复现
用标准库复现「访问已移除的 text_model 属性」:
import torch.nn as nn # transformers <5.6:两层 class CLIPTextModelOld(nn.Module): def __init__(self): super().__init__() self.text_model = nn.Module() # 有包装层 # transformers >=5.6:扁平 class CLIPTextModelNew(nn.Module): def __init__(self): super().__init__() self.encoder = nn.Module() # 没有 text_model # 直接持有,扁平化 def map_weights(model, sd): # 错误:假设 model.text_model 存在 return getattr(model, "text_model") # 5.6 上这里 AttributeError m_old = CLIPTextModelOld(); m_new = CLIPTextModelNew() map_weights(m_old, {}) # OK try: map_weights(m_new, {}) except AttributeError as e: print("AttributeError:", e) # 'CLIPTextModelNew' has no attribute 'text_model'复现「key 前缀不兼容」:checkpoint keytext_model.encoder.layers.0...在扁平模型上应映射成encoder.layers.0...,但加载器没去前缀,KeyError。
五、解决方案(第一层:最小直接修复)
最小修复:加载器按 transformers 版本(或按属性是否存在)适配,去掉对text_model的硬依赖:
import transformers def get_clip_body(model): # 兼容两种结构:有 text_model 用 text_model,否则用模型本身(扁平) if hasattr(model, "text_model"): return model.text_model return model def strip_text_model_prefix(key: str) -> str: # checkpoint 旧格式 key 带 text_model.,扁平模型去掉前缀 if key.startswith("text_model."): return key[len("text_model."):] return key def load_clip_single_file(model, state_dict): body = get_clip_body(model) new_sd = {strip_text_model_prefix(k): v for k, v in state_dict.items()} # 用兼容后的 key 加载 missing, unexpected = body.load_state_dict(new_sd, strict=False) return model这样无论 transformers 版本,加载器都通过get_clip_body拿到正确的子模块容器,旧格式 key 自动去text_model.前缀。
六、解决方案(第二层:结构性改进)
把「CLIP 结构(两层 vs 扁平)的版本兼容规则」收敛成一个 dataclass 单一真源:
from dataclasses import dataclass, field from typing import Dict, List, Tuple @dataclass(frozen=True) class SingleFileClipPolicy: """from_single_file 加载 CLIP 的版本兼容单一真源。""" # 触发扁平化的 transformers 最低版本 flat_from_version: str = "5.6.0" # 旧格式 weight key 前缀(需去除) legacy_prefix: str = "text_model." # 决定用哪层作为「body」的属性探测顺序 body_attr_order: Tuple[str, ...] = ("text_model", "") # 扁平化后应有的直接子模块 flat_submodules: Tuple[str, ...] = ("encoder", "embeddings") def is_flat(self, transformers_version: str) -> bool: from mylib.version_util import version_ge return version_ge(transformers_version, self.flat_from_version) def resolve_body(self, model): if hasattr(model, self.body_attr_order[0]): # text_model return getattr(model, self.body_attr_order[0]) return model # 扁平:用自身 def normalize_key(self, key: str) -> str: if key.startswith(self.legacy_prefix): return key[len(self.legacy_prefix):] return key def validate_loaded(self, model, transformers_version: str) -> List[str]: problems = [] body = self.resolve_body(model) expected = self.flat_submodules if self.is_flat(transformers_version) else ("layers",) for sub in expected: if not hasattr(body, sub): problems.append(f"CLIP body 缺少子模块 {sub}") return problems加载主流程用policy.resolve_body+policy.normalize_key,validate_loaded在加载后校验结构正确,CI 在 transformers 5.6 矩阵跑。
七、解决方案(第三层:断言 / CI 守护)
用 pytest 把「两层/扁平都兼容 + key 去前缀 + 加载无缺失」固化成回归(在 transformers 5.6 矩阵跑):
import torch import pytest from diffusers import StableDiffusionPipeline from mylib.single_file_clip import SingleFileClipPolicy POLICY = SingleFileClipPolicy() def test_resolve_body_two_layer(): # 模拟 transformers <5.6 两层结构 class Old: def __init__(self): self.text_model = torch.nn.Module() assert hasattr(POLICY.resolve_body(Old()), "text_model") or POLICY.resolve_body(Old()) is not None def test_resolve_body_flat(): class New: def __init__(self): self.encoder = torch.nn.Module() body = POLICY.resolve_body(New()) assert not hasattr(body, "text_model") # 扁平,body 就是自身 def test_strip_prefix(): assert POLICY.normalize_key("text_model.encoder.layers.0.weight") == "encoder.layers.0.weight" assert POLICY.normalize_key("encoder.layers.0.weight") == "encoder.layers.0.weight" def test_load_on_transformers_56(): import transformers if POLICY.is_flat(transformers.__version__): pipe = StableDiffusionPipeline.from_single_file("v1-5-pruned.safetensors") problems = POLICY.validate_loaded(pipe.text_encoder, transformers.__version__) assert problems == [], "CLIP 加载结构问题:\n" + "\n".join(problems) def test_no_attribute_error_from_single_file(): import transformers if POLICY.is_flat(transformers.__version__): # 不应再 AttributeError: 'CLIPTextModel' has no attribute 'text_model' pipe = StableDiffusionPipeline.from_single_file("v1-5-pruned.safetensors") assert pipe is not NoneCI 把test_strip_prefix与test_no_attribute_error_from_single_file作为 from_single_file + transformers 5.6 的必过项,要求「单文件加载在 5.6 上不再访问text_model」。
八、排查清单
from_single_file 加载 CLIP 在 transformers>=5.6 报错按顺序查:
AttributeError: 'CLIPTextModel' has no attribute 'text_model'?加载器假设有两层,5.6 扁平化了,用get_clip_body/resolve_body适配。- checkpoint key 是否带
text_model.前缀?扁平模型需去前缀再加载,否则 KeyError。 - transformers 版本是否 >=5.6?是则走扁平路径,用模型自身当 body。
- 加载后是否权重缺失(strict=False 的 missing)?说明 key 前缀没去干净,核对
normalize_key。 - 是否两个 transformers 版本都要测?单文件加载必须在 <5.6 和 >=5.6 两个矩阵都跑。
validate_loaded是否校验encoder/embeddings存在?校验能提前暴露结构错。
九、小结
「from_single_file: CLIPTextModel has no attribute 'text_model' with transformers >= 5.6」本质是单文件加载器假设 CLIP 文本编码器有text_model两层包装,而 transformers>=5.6 扁平化了该结构,导致加载器访问text_model属性AttributeError或按text_model.*前缀映射权重 KeyError,且缺版本兼容层。第一层用get_clip_body(有text_model用它、否则用模型自身)+strip_text_model_prefix兼容旧 key;第二层把两层/扁平的版本兼容规则收敛到SingleFileClipPolicy单一真源,加载后validate_loaded;第三层用 pytest 在 5.6 矩阵守住「不再访问text_model、key 去前缀、无缺失」。通用教训:**任何依赖第三方库内部结构(如text_model包装层)的加载/映射代码,都必须按版本探测结构并归一化,否则上游一扁平化就全面崩。