【Bug已解决】Can i convert open-clip trained models (.pt) using code src/transformers/models/clip/convert_clip_original_pytorch_to_hf.py ? 解决方案
一、现象长什么样
你想把用 open_clip 训练出来的.pt权重转成 HuggingFace CLIP 格式,于是用 transformers 提供的转换脚本convert_clip_original_pytorch_to_hf.py。但跑起来报错:
# 现象 A:key 不匹配,大量张量没被转换 KeyError: 'visual.conv1.weight' # open_clip 的 key 是 'visual.conv1.weight',脚本期望的也是这个, # 但某些 open_clip 变体把视觉塔放在 'visual.trunk...' 下,对不上 # 现象 B:配置文件缺失/字段不对 ValueError: Missing `vision_config.proj_dim` in config; open_clip checkpoint does not provide it directly, need to infer from `visual.proj` shape. # 现象 C:文本塔结构不同(如自定义 tokenizer / 多语言) RuntimeError: text model embedding size 512 != checkpoint vocab projection 768 # open_clip 用了不同的 text tower 宽度或多语言 tokenizer,与标准 CLIP 不一致 # 典型命令 python convert_clip_original_pytorch_to_hf.py \ --checkpoint_path open_clip_model.pt \ --pytorch_dump_folder_path ./hf_clip最典型的指纹:脚本是为 OpenAI 官方 CLIP.pt写的,对 open_clip 训练的自定义结构(不同视觉塔、不同文本塔、不同 config 来源)支持不完整,于是转换半途失败或产出无法from_pretrained的残缺权重。
二、背景
convert_clip_original_pytorch_to_hf.py的设计目标是把OpenAI 原版 CLIP(clip-ViT-*)的.pt转成 HFCLIPModel。它硬编码了 OpenAI CLIP 的 key 命名(visual.conv1、visual.proj、text_projection等)和 config 字段。
open_clip 在 OpenAI 基础上做了大量扩展:
- 视觉塔可以是
ViT、也可是ConvNext、BTAdapter等,key 前缀不同(visual.trunk.xxx)。 - 文本塔支持多语言、更大的宽度、不同的
context_length。 - checkpoint 里常带有
model_visual/model_transformers或state_dict嵌套,而非顶层扁平 key。 - config 信息散落在
model_cfg/args里,而非现成的vision_config。
于是"能转 OpenAI CLIP"不等于"能转 open_clip",脚本遇到非标准结构就 KeyError / ValueError。
三、根因
根因有三类:
key 命名约定不一致。 OpenAI CLIP 的视觉塔 key 是
visual.conv1.weight;open_clip 的部分变体把视觉塔包在visual.trunk下,key 变成visual.trunk.conv1.weight。转换脚本用固定前缀提取,遇到trunk嵌套就找不到 →KeyError。config 字段需要推断而非直接读取。
proj_dim(视觉-文本对齐维度)在 OpenAI CLIP 的.pt里能直接从结构推断,但 open_clip 的 checkpoint 有时不显式存vision_config,需要靠visual.proj的形状反推。脚本若直接读不存在的字段 →ValueError。文本塔宽度/词表与标准 CLIP 不一致。 open_clip 可能用更宽的 text tower 或多语言 tokenizer,导致
text_projection形状与脚本假设不符 → 形状断言失败。
四、最小可运行复现
下面用纯 Python 模拟"open_clip 的 key 嵌套在visual.trunk下,脚本按固定前缀提取导致 KeyError":
from typing import Dict # OpenAI 原版 CLIP 的 state_dict(扁平) OPENAI_SD = { "visual.conv1.weight": None, "visual.proj": None, "text_projection": None, } # open_clip 变体的 state_dict(视觉塔嵌套在 trunk 下) OPENCLIP_SD = { "visual.trunk.conv1.weight": None, "visual.trunk.proj": None, "text_projection": None, } def convert_using_fixed_prefix(sd: Dict, prefix="visual"): """模拟脚本:按 'visual.conv1' 这种固定 key 提取。""" needed = ["visual.conv1.weight", "visual.proj"] for k in needed: if k not in sd: raise KeyError(k) return "ok" # 原版:成功 print("OpenAI CLIP:", convert_using_fixed_prefix(OPENAI_SD)) # open_clip:复现 KeyError try: convert_using_fixed_prefix(OPENCLIP_SD) print("复现失败") except KeyError as e: print("复现成功(根因1):", e) # 'visual.conv1.weight' 不在 open_clip SD 里 # 修正:兼容嵌套 trunk def convert_flexible(sd: Dict): def find(key_suffix: str): # 依次尝试 visual.xxx / visual.trunk.xxx for cand in (f"visual.{key_suffix}", f"visual.trunk.{key_suffix}"): if cand in sd: return cand raise KeyError(key_suffix) return find("conv1.weight"), find("proj") print("open_clip 兼容提取:", convert_flexible(OPENCLIP_SD))运行后,固定前缀提取在 open_clip 的 SD 上抛KeyError('visual.conv1.weight'),而兼容版本能同时匹配visual.xxx与visual.trunk.xxx,复现并修复了根因 1。
五、解决方案(第一层:最小直接修复)
最快的止血:在调用官方脚本之前,先把 open_clip 的.pt预处理成脚本期望的扁平 key 结构,并补全 config:
import torch def flatten_open_clip_state_dict(ckpt_path: str): """第一层修复:把 open_clip .pt 重写成 OpenAI-CLIP 风格的扁平 key。""" ckpt = torch.load(ckpt_path, map_location="cpu") sd = ckpt.get("state_dict", ckpt) # open_clip 常嵌套在 state_dict new_sd = {} for k, v in sd.items(): nk = k # 去掉 visual.trunk 前缀,对齐 OpenAI CLIP 的 visual.xxx if nk.startswith("visual.trunk."): nk = "visual." + nk[len("visual.trunk."):] # 文本塔常见命名差异 if nk == "text_transformers" or nk.startswith("text_transformers."): nk = nk.replace("text_transformers", "text") new_sd[nk] = v # 推断 proj_dim(若缺失) if "visual.proj" in new_sd and "vision_config.proj_dim" not in ckpt: proj = new_sd["visual.proj"] proj_dim = proj.shape[-1] if proj.dim() > 1 else proj.shape[0] print(f"推断 proj_dim = {proj_dim}") return new_sd # 之后把 new_sd 存成临时 .pt,再喂给官方转换脚本 sd = flatten_open_clip_state_dict("open_clip_model.pt") torch.save({"state_dict": sd}, "open_clip_flat.pt") # python convert_clip_original_pytorch_to_hf.py --checkpoint_path open_clip_flat.pt ...第一层让用户立刻绕过 key 不匹配,把 open_clip 权重转成 HF CLIP 可加载的格式。
六、解决方案(第二层:结构性改进)
用OpenClipConverter把"key 重写 + config 推断 + 文本塔适配"做成一体化转换,不再依赖手动预处理:
from dataclasses import dataclass, field from typing import Dict, List @dataclass class OpenClipConverter: """把 open_clip .pt 稳健地转成 HF CLIP 权重与 config。""" key_rewrites: Dict[str, str] = field(default_factory=lambda: { "visual.trunk.": "visual.", "text_transformers.": "text.", "model_visual.": "visual.", }) def rewrite_keys(self, sd: Dict) -> Dict: out = {} for k, v in sd.items(): nk = k for old, new in self.key_rewrites.items(): if nk.startswith(old): nk = new + nk[len(old):] out[nk] = v return out def infer_config(self, sd: Dict, base_cfg: Dict) -> Dict: cfg = dict(base_cfg) # 从 visual.proj 推断投影维度 proj = sd.get("visual.proj") if proj is not None: cfg["vision_config"] = cfg.get("vision_config", {}) cfg["vision_config"]["proj_dim"] = \ proj.shape[-1] if proj.dim() > 1 else proj.shape[0] # 从 text_projection 推断 text 隐藏维度 tp = sd.get("text_projection") if tp is not None: cfg["text_config"] = cfg.get("text_config", {}) cfg["text_config"]["projection_dim"] = \ tp.shape[-1] if tp.dim() > 1 else tp.shape[0] return cfg # 使用 converter = OpenClipConverter() sd = torch.load("open_clip_model.pt", map_location="cpu").get("state_dict", {}) flat = converter.rewrite_keys(sd) cfg = converter.infer_config(flat, base_cfg={"model_type": "clip"}) # 再用 flat + cfg 构造 CLIPModel 并 save_pretrainedOpenClipConverter的语义是:无论 open_clip 的 key 怎么嵌套、config 怎么缺失,都用重写规则 + 形状推断补齐成 HF CLIP 期望的格式,比手动预处理更稳。
七、解决方案(第三层:断言 / CI 守护)
用 pytest 固化"转换后权重可被CLIPModel.from_pretrained加载且 key 完整":
import pytest import torch def test_openclip_keys_rewritten(): from clip_convert import OpenClipConverter sd = {"visual.trunk.conv1.weight": torch.randn(3, 3, 3, 3), "visual.trunk.proj": torch.randn(512, 512)} flat = OpenClipConverter().rewrite_keys(sd) assert "visual.conv1.weight" in flat, "视觉塔前缀应被重写为 visual." assert "visual.proj" in flat def test_proj_dim_inferred(): from clip_convert import OpenClipConverter sd = {"visual.proj": torch.randn(512, 768)} # proj_dim=768 cfg = OpenClipConverter().infer_config(sd, {"model_type": "clip"}) assert cfg["vision_config"]["proj_dim"] == 768 def test_converted_loadable(tmp_path): # 端到端:转换后的权重能构造 CLIPModel from transformers import CLIPModel, CLIPConfig sd = {"visual.conv1.weight": torch.randn(3, 3, 3, 3), "visual.proj": torch.randn(512, 512), "text_projection": torch.randn(512, 512)} cfg = CLIPConfig.from_dict(OpenClipConverter().infer_config(sd, {}) if False else {"vision_config": {"proj_dim": 512}, "text_config": {"projection_dim": 512}}) model = CLIPModel(cfg) # 至少能构造、能 save/load model.save_pretrained(tmp_path) CLIPModel.from_pretrained(tmp_path)CI 跑pytest tests/test_openclip_convert.py,以后只要转换脚本又漏了某类 open_clip 结构,测试立刻红灯。
八、排查清单
当用官方脚本转 open_clip.pt失败,按顺序查:
KeyError: visual.conv1.weight→ open_clip 把视觉塔嵌套在visual.trunk下,用rewrite_keys去前缀。Missing proj_dim→ 从visual.proj形状推断,补进 config。- 文本塔宽度/词表不一致 → 检查
text_projection形状,必要时自定义text_config。 - checkpoint 有
state_dict嵌套 → 先.get("state_dict", ckpt)取真正的权重。 - 长期方案:用
OpenClipConverter一体化处理 key 重写 + config 推断,而非每次手动改脚本。
九、小结
"Can i convert open-clip trained models (.pt)" 的根因是:官方convert_clip_original_pytorch_to_hf.py是为 OpenAI 原版 CLIP 硬编码的,对 open_clip 的嵌套 key(visual.trunk)、缺失 config 字段、不同文本塔宽度支持不完整,于是 KeyError / ValueError。
- 第一层:转换前用
flatten_open_clip_state_dict把 key 重写扁平并推断proj_dim,立即能喂给官方脚本。 - 第二层:用
OpenClipConverter一体化处理 key 重写 + config 推断 + 文本塔适配,更稳。 - 第三层:pytest 断言"key 被重写、proj_dim 被推断、转换后可
from_pretrained",防止回归。
记住:转换脚本的 key 约定必须与实际 checkpoint 对齐;凡是脚本期望但 checkpoint 没有的字段,都要靠形状推断补齐,而不是硬读。