为什么每次持久向量日志中都会有: Load pretrained SentenceTransformer: x?
你用的是本地 BAAI/bge-base-zh-v1.5 模型,通过 AutoTokenizer + AutoModel 做嵌入,且把模型封装成单例类(BAAIEmbeddingEngine)并在项目启动时初始化。但是每次执行向量化(lancedb 操作)时,日志都会出现:
大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。
图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG
我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。
展菲:您的前沿技术领航员
👋 大家好,我是展菲!
📱 全网搜索“展菲”,即可纵览我在各大平台的知识足迹。
📣 公众号“Swift社区”,每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
💬 微信端添加好友“fzhanfei”,与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
📅 最新动态:2025 年 3 月 17 日
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!
文章目录
问题背景
你用的是本地 BAAI/bge-base-zh-v1.5 模型,通过 AutoTokenizer
+ AutoModel
做嵌入,且把模型封装成单例类(BAAIEmbeddingEngine
)并在项目启动时初始化。
但是每次执行向量化(lancedb 操作)时,日志都会出现:
sentence_transformers.SentenceTransformer INFO Load pretrained SentenceTransformer: BAAI/bge-base-zh-v1.5
你怀疑:这是不是每次都重新加载了模型?导致每次调用都有卡顿?单例不是已经只加载一次了吗?
结论(先给结果)
那条日志来自 sentence_transformers.SentenceTransformer.from_pretrained(...)
的内置日志。出现这行日志 说明在某处确实创建了 SentenceTransformer
实例(或以 model name 调用了 from_pretrained
)。如果这条日志每次都出现,则说明在每次向量化调用流程中确实触发了 SentenceTransformer
的构造/加载操作 —— 也就是模型被重复“实例化/加载”了(至少在当前进程/上下文中每次都会走到加载路径)。
但“为什么会重复加载”有几种常见原因(下面详细解释并给出验证与解决办法):
可能的根因与排查要点(逐项说明)
-
有其它库/代码在内部每次创建
SentenceTransformer
- 即使你用
AutoModel
/AutoTokenizer
,项目中可能有第三方库(例如 lancedb、embedding 注册器、某些 util)在内部收到 model name 字符串("BAAI/bge-base-zh-v1.5"
)时会调用SentenceTransformer(model_name)
来构建 embedding 模型。 - 排查方法:grep / search 项目中是否有
SentenceTransformer
或from_pretrained
被调用;或启用更详细日志,打印创建时的栈信息(稍后给方法)。
- 即使你用
-
你的单例并不是真的被复用(跨请求/进程/worker 不生效)
- 单例在一个进程中有效,但如果你的服务是以多进程(gunicorn/uvicorn workers)或 multiprocessing 执行,那么每个子进程都会独立加载模型。
- 在 Windows 或
spawn
启动模式下,子进程会重新执行初始化逻辑(单例不能跨进程共享)。 - 排查方法:在
__init__
里打印id(self.__model)
、os.getpid()
,观察每次调用时 PID 与对象 id 是否一致。
-
lancedb / EmbeddingFunctionRegistry 的注册机制导致每次创建新实例
- 你注册的是一个类,lancedb 在序列化/反序列化或每次调用时可能会 new 一个实例(例如在 worker 或远端执行)。
- 排查方法:查看 lancedb 的 embedding function 使用方式,是否把你的对象 pickled 传给 worker,从而导致重建。
-
你在 encode 的路径中某处重复尝试创建
SentenceTransformer
(代码逻辑问题)- 例如在
generate_embeddings
或__call__
内部无意间每次都 new 一个模型(也许间接调用了其他工具函数)。 - 排查方法:在
SentenceTransformer
的构造处(或在BAAIEmbeddingEngine.__init__
)打 log,观察调用频率。
- 例如在
-
日志并不代表真正的“磁盘/网络权重加载”
- 有时
SentenceTransformer
只是在 wrapper level 打了一条 log(例如加载配置/tokenizer)而不一定重新下载权重。但通常它会初始化模型权重,启动会有开销。 - 排查方法:看加载时间(打印时间戳)来判断是否为完全加载。
- 有时
调试/验证步骤(实操清单)
你可以按照下面步骤定位到底是谁在重新加载:
-
在
BAAIEmbeddingEngine.__init__
打详细日志:import time, os, logging logger.info(f"Init BAAIEmbeddingEngine pid={os.getpid()} timestamp={time.time()}")
如果
__init__
只在应用启动时调用一次,那说明你的封装确实只初始化一次。 -
在每次
encode
前后打印 model id 与 pid:logger.debug(f"encode called pid={os.getpid()} model_id={id(self.__model)}")
如果每次调用 model_id 变化或 pid 变化,那就说明模型并没有在同一进程中被复用。
-
搜索项目代码中
SentenceTransformer
的使用:grep -R "SentenceTransformer" -n . grep -R "from_pretrained(" -n .
若发现第三方或 utils 在别处创建了该对象,那就找到了 culprit。
-
打印堆栈(在加载位置)来追踪是谁触发的(可以短暂在
sentence_transformers
日志里改为 DEBUG 并在加载附近捕获 stack):import traceback, logging logging.getLogger('sentence_transformers').setLevel(logging.DEBUG) # 或者在你怀疑的地方: print('stack', ''.join(traceback.format_stack()))
-
注意进程/worker 模式:
- 如果你用 gunicorn/uvicorn ,每个 worker 都有独立进程,会各自加载模型(这是正常且必要的)。
- 如果你想减少启动延迟,可以使用
--preload
让主进程先加载模型再 fork(Linux 下的 Copy-On-Write 优化)。
常见解决方案与建议(按优先级)
下面是有针对性的、能解决多数情况的办法:
A. 如果确实是 “某处每次新建 SentenceTransformer”,修复代码(最佳)
- 找到并改成复用实例(不要在每次调用时
SentenceTransformer(...)
)。 - 推荐把 embedding engine 作为 模块级全局单例,并把该实例传入需要的注册/调用处,而不是让框架每次 new。
示例(改造你的注册逻辑,传入实例而非类):
# module-level single instance
EMBEDDING_ENGINE = BAAIEmbeddingEngine() # model加载在这里只发生一次/每个进程
# 注册时直接把实例包裹成 function,不让框架再 new
@registry.register("local-baai-transformers")
class BAAITransformerEmbeddings(TextEmbeddingFunction):
def __init__(self, engine: BAAIEmbeddingEngine = EMBEDDING_ENGINE, **kwargs):
super().__init__(**kwargs)
self.__fun = engine
def generate_embeddings(self, texts):
return self.__fun.encode(texts)
关键点:确保 EMBEDDING_ENGINE
在模块 import 时创建一次(每个进程一次)。
B. 如果是多进程/worker 导致(不可避免地每个进程加载),采用预加载 + fork 优化(Linux)
- 使用
gunicorn --preload
或uvicorn
的--worker-class
preload 选项,使父进程先加载模型,然后 fork 出多个子进程(子进程共享内存页面直到写入)。这可以借助 Linux 的 Copy-On-Write 降低内存占用与加载延迟。 - 注意:Windows 下没有 fork,因此每个 worker 都会重新加载模型(不可避免)。
示例(gunicorn):
gunicorn -k uvicorn.workers.UvicornWorker --preload -w 4 app:app
preload 的前提是你的初始化代码(模型加载)在全局模块层,而不是在 worker 启动后的回调里。
C. 如果 lancedb/框架在序列化/远程执行时重新实例化
- 有些框架会把你的 embedding function 序列化给远端 worker,worker 反序列化时会在子进程中重新 run
__init__
。解决方法是 不要把函数对象直接序列化,而是把可调用包装成引用到已存在单例的 wrapper,或在 worker 启动后再从全局单例获取模型。 - 更稳妥的做法是把 embedding 服务化(独立进程/微服务),其它进程通过 RPC/HTTP 调用该服务获取 embedding。这样模型只在 embedding 服务进程里加载一次。
D. 抑制日志(临时降噪)
如果你确认模型只加载一次,但日志仍然显得频繁(比如别的 lib 打了一条类似日志),可把 sentence_transformers
日志等级调高,降低噪声:
import logging
logging.getLogger("sentence_transformers").setLevel(logging.WARNING)
这只“隐藏”日志,不解决重复加载问题。推荐先排查重复加载再考虑隐藏日志。
可运行的调试 / 演示代码
下面给出一段最小可运行的 Python 示例,展示三件事:
- 当你每次都新建
SentenceTransformer
时,日志会重复打印(模拟你的卡顿)。 - 用模块级单例只在进程启动时构建一次。
- 如何查看 PID / model id 来验证是否重复加载。
说明:示例使用
sentence-transformers/paraphrase-MiniLM-L6-v2
(轻量)代替 BAAI 模型,方便本地运行。如果你要用 BAAI 本地模型,把名字换成本地路径或模型名称即可。
# demo_sentence_transformer_singleton.py
import time, os, logging
from sentence_transformers import SentenceTransformer
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 控制 sentence_transformers 的日志级别(示例中保留 INFO)
logging.getLogger("sentence_transformers").setLevel(logging.INFO)
MODEL_NAME = "sentence-transformers/paraphrase-MiniLM-L6-v2" # 替换成你的模型名或本地路径
# 方式1:每次都创建(BAD)
def encode_bad(texts):
logger.info("encode_bad: creating SentenceTransformer at pid=%s", os.getpid())
model = SentenceTransformer(MODEL_NAME) # 每次都会打印 Load pretrained ...
emb = model.encode(texts)
return emb
# 方式2:模块级单例(GOOD)
class EmbeddingSingleton:
_instance = None
def __init__(self):
logger.info("EmbeddingSingleton.__init__ pid=%s time=%s", os.getpid(), time.time())
self.model = SentenceTransformer(MODEL_NAME)
logger.info("model id=%s", id(self.model))
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = EmbeddingSingleton()
return cls._instance
def encode_good(texts):
inst = EmbeddingSingleton.get_instance()
logger.info("encode_good using model id=%s pid=%s", id(inst.model), os.getpid())
return inst.model.encode(texts)
if __name__ == "__main__":
texts = ["hello world", "sentence transformers demo"]
logger.info("Call bad 1")
encode_bad(texts)
logger.info("Call bad 2")
encode_bad(texts)
logger.info("Now call good 1")
encode_good(texts)
logger.info("Now call good 2")
encode_good(texts)
运行这个脚本你会看到:
encode_bad
每次都会因SentenceTransformer(MODEL_NAME)
打出Load pretrained SentenceTransformer: ...
;encode_good
只在第一次初始化单例时打印“Load pretrained …”,后续调用复用同一实例。
如果你用的是 lancedb:如何把单例和注册结合(示例)
下面示例演示把模块级单例传给 lancedb 的 embedding registry(伪代码,具体接口按你实际 lancedb 版本调整):
# registry_wrapper.py
from sentence_transformers import SentenceTransformer
import logging
logger = logging.getLogger(__name__)
class BAAIEmbeddingEngine:
def __init__(self, model_name="./HF/models/BAAI/bge-base-zh-v1.5"):
logger.info("Loading embedding model in pid=%s", os.getpid())
self.model = SentenceTransformer(model_name) # or your AutoModel wrapper
def encode(self, texts):
return self.model.encode(texts)
# module-level shared instance
EMBEDDING_ENGINE = BAAIEmbeddingEngine()
# lancedb registration: pass a wrapper that uses EMBEDDING_ENGINE
def my_embedding_function(texts):
return EMBEDDING_ENGINE.encode(texts)
# register in lancedb: make sure lancedb stores the NAME or callable reference
# and that during execution it does NOT re-create SentenceTransformer by name.
# Example (pseudocode):
# db.register_embedding_function("local-baai", my_embedding_function)
要点:不要把 SentenceTransformer
的模型名作为字符串传给 lancedb 让其内部去 from_pretrained
;而应把已经构建好的实例的 callable 传入。
额外说明:多进程/多线程与加载行为
- 多线程:线程共享同一进程内存,模块级单例在多线程里可复用(无需重复加载),但要注意线程安全(你的
lockRIns
很必要)。 - 多进程:每个进程都会单独执行模块级初始化,所以每个进程会单独加载模型(不可避免),除非使用
fork
之后共享(--preload
)。 - 使用 Fork 优化:在 Linux 下,先在主进程加载模型再 fork 出多个 worker,可以节省物理内存(利用 COW)并减少每个 worker 单独加载时的 I/O 延迟。
最后给出一个“排查清单”便于快速定位问题
-
在
BAAIEmbeddingEngine.__init__
里打印pid
、time
、id(self.__model)
,观察是否只在进程启动时打印一次。 -
确认 lancedb 的注册方式:不要传 model name 字符串给 lancedb 让它内部加载;传实例或外部函数。
-
搜全项目
SentenceTransformer
/from_pretrained
的调用位置,确认没有别处重复创建。 -
若使用多进程(uvicorn/gunicorn/multiprocessing),理解每个进程需要加载一次模型是正常的;考虑用
--preload
或拆分为单独 embedding 服务。 -
临时把
sentence_transformers
日志等级调高以查看更详细的加载栈(便于定位是谁触发加载)。import logging logging.getLogger("sentence_transformers").setLevel(logging.DEBUG)
-
若确认是“重复加载”但难以改造,短期减缓可以把
sentence_transformers
日志隐藏:logging.getLogger("sentence_transformers").setLevel(logging.WARNING)
总结
- 那条
Load pretrained SentenceTransformer: ...
日志就表示某处在构造SentenceTransformer
。如果在每次向量化都看到,说明确实有重复构造/加载发生。 - 单例在单进程内有效,但无法跨进程复用;很多框架/运行模式(尤其 Windows 或 spawn 模式)会导致每个进程都重新加载模型。
- 最稳妥的做法是 确保实例只在进程内加载一次(模块级单例 / 预加载 + fork / 服务化 embedding),并让 lancedb 等库调用该实例的
encode
,不要在运行时反复通过模型名去from_pretrained
。 - 同时可以通过打印 pid/model id、检查日志堆栈来准确定位重复加载的触发点。
更多推荐
所有评论(0)