Embedding模型承担着把文本转换成稠密向量的任务,下游的语义搜索、聚类、推荐和RAG系统都依赖它。模型上线后如果直接沿用离线脚本里的写法,也就是对每条文本单独调用一次编码方法,服务端会出现两个典型现象:单次请求耗时偏高,CPU或GPU利用率却一直上不去。有人尝试换更小的模型、减少最大序列长度或者增加机器数量,虽然短期有效,但并不能解决根本问题。更合理的路径是先把推理从单条循环改成批量矩阵运算,再把计算放到GPU上。这两步往往能带来数倍到十几倍的吞吐提升,而且不损失向量质量。

单条推理的瓶颈在哪里
如果对每条文本都调用一次model.encode或model.forward,计算图会被重复构建和销毁,Python解释器、框架调度、内存分配等开销会被逐条放大。以SentenceTransformer为例,encode方法内部会做分词、tokenization、padding、张量转移和模型前向,单条文本虽然也能得到正确向量,但每一步都只处理batch size为1的数据,GPU上的CUDA核心大量时间处于空闲等待状态,CPU和GPU之间的数据传输次数也显著增加。
从硬件角度看,GPU擅长高度并行的矩阵乘法,单条文本转换出的输入张量规模很小,通常只有几十到几百个token,根本喂不满现代GPU的计算单元。以常见的A10或T4为例,即使batch size为1,GPU占用率可能长期低于10%,而CPU端却因为频繁的Python循环和调度占用了大量时间。这就是为什么有些服务在换用GPU后,单条推理延迟并没有明显下降,甚至因为数据搬运反而不如CPU。问题不是GPU不够快,而是调用方式没有适应GPU的并行特性。
另一个容易被忽略的瓶颈是padding。批处理时不同长度的文本需要对齐到同一长度,如果直接按最大长度固定padding,当文本长度差异很大时,大量无效token会参与计算,浪费算力。因此动态padding和按长度分桶是批量推理中不可忽视的优化点,后面会展开说明。
批量推理的实现与batch size选择
批量推理的核心思路是把多条文本拼成一个张量一起送进模型。PyTorch和TensorFlow等框架对批量矩阵运算做了大量优化,一次前向处理64条文本和循环处理64条文本相比,总耗时可能只有后者的十分之一到三分之一。下面的代码对比了单条循环和批量编码的差异。
import time
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
texts = ["这是第一条示例文本", "这是第二条示例文本", "这是第三条示例文本"] * 1000
# 方式一:逐条循环
start = time.time()
single_embeddings = []
for text in texts:
vec = model.encode(text)
single_embeddings.append(vec)
print("逐条循环耗时:", time.time() - start)
# 方式二:批量编码
start = time.time()
batch_embeddings = model.encode(
texts,
batch_size=64,
convert_to_numpy=True,
show_progress_bar=False
)
print("批量编码耗时:", time.time() - start)
print("批量向量形状:", batch_embeddings.shape)
运行这段代码可以看到,逐条循环处理3000条文本可能需要几十秒,而批量编码通常在几秒内完成。原因在于模型前向过程从3000次小矩阵乘法变成了约47次大矩阵乘法,GPU的Tensor Core和内存带宽得到了充分利用。即使没有GPU,仅靠CPU的向量化指令集,批量推理也能减少Python层的循环开销。
batch_size的选择需要结合显存容量和平均文本长度。较小的模型例如bge-small,在24GB显存下batch size可以设为128甚至256;但较大的模型如bge-large或bge-m3,batch size太大容易触发OOM。一个实用的做法是从32开始尝试,每次翻倍直到显存占用达到80%左右,然后观察吞吐曲线。过小的batch size无法发挥并行优势,过大的batch size会导致显存碎片化和频繁的缓存换入换出,性能反而下降。
对于长度差异明显的文本,建议先按长度分桶,同一批内文本长度尽可能接近,再使用动态padding。HuggingFace的DataCollatorWithPadding能够根据每个batch内部的最大长度进行padding,避免整个数据集统一pad到最大长度带来的浪费。这样在混合长短文本的场景下,吞吐还能再提升20%到40%。
GPU加速的正确配置与常见坑
启用GPU并不只是把模型搬到cuda设备上。要获得稳定收益,还需要处理输入张量的设备一致性、关闭梯度计算以及合理使用半精度。下面是一个基于PyTorch和Transformers的GPU推理示例。
import torch
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-small-zh-v1.5")
model = AutoModel.from_pretrained("BAAI/bge-small-zh-v1.5")
if torch.cuda.is_available():
model = model.to("cuda")
print("当前使用GPU:", torch.cuda.get_device_name(0))
else:
print("未检测到GPU,退回CPU")
texts = ["GPU加速的Embedding推理", "批量处理提升吞吐"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
inputs = {key: value.to(model.device) for key, value in inputs.items()}
with torch.no_grad():
outputs = model(**inputs)
# 取CLS位置向量作为句子表示
embeddings = outputs.last_hidden_state[:, 0, :]
print("向量形状:", embeddings.shape)
代码中with torch.no_grad()是必须的,它可以阻止框架构建反向计算图,节省显存并减少计算开销。推理场景下任何不当的梯度保留都会导致显存持续增长,最终触发OOM。另一个关键点是输入张量必须通过.to(model.device)转移到与模型相同的设备上,否则会出现设备不匹配错误。很多团队在服务端使用多线程或异步并发时,忽略了不同线程间的设备上下文切换,导致cuda调用被隐式同步,吞吐反而下降。
半精度(fp16)在支持Tensor Core的GPU上可以进一步提升速度。PyTorch中可以这样开启:
import torch
from transformers import AutoModel, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-small-zh-v1.5")
model = AutoModel.from_pretrained("BAAI/bge-small-zh-v1.5", torch_dtype=torch.float16)
model = model.to("cuda")
texts = ["半精度推理示例", "减少显存占用", "提升计算效率"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
inputs = {k: v.to("cuda") for k, v in inputs.items()}
with torch.no_grad():
outputs = model(**inputs)
embeddings = outputs.last_hidden_state[:, 0, :]
print(embeddings.dtype, embeddings.shape)
fp16能将显存占用降低约一半,在A10、V100等GPU上矩阵乘法速度也能明显提升,但需要注意部分较老的CPU或GPU对fp16支持不完善,且输出向量的数值范围可能略有变化。对于Embedding任务,这种精度损失通常不会影响相似度排序结果,但在做严格数值比较时建议先在小批量数据上验证一致性。
避免这些加速误区才能稳定落地
第一个误区是盲目增大batch size。批量推理虽然有效,但batch size受限于显存,同时过大的batch可能导致延迟增加,因为单次前向时间变长,如果服务要求低延迟,需要在吞吐和延迟之间做权衡。一般建议通过压测找到延迟可接受范围内的最大batch size。
第二个误区是忽略数据加载与预处理。Embedding服务通常还要做分词、清洗、截断等操作,这些步骤如果与GPU推理串行执行,GPU会有明显的空闲等待。可以使用多进程DataLoader或异步队列,让CPU预处理与GPU推理重叠进行。例如在Python中使用concurrent.futures或者专门的队列线程,提前准备好下一个batch的输入张量。
第三个误区是忘记将模型设置为评估模式。部分模型包含Dropout或BatchNorm层,训练模式下会引入随机性或统计更新,既影响向量质量又降低推理速度。在加载模型后应调用model.eval()。另外,固定输入长度、启用torch.backends.cudnn.benchmark = True、使用更高效的推理引擎如ONNX Runtime或TensorRT,也是进一步提升性能的可行方向,不过引入这些工具前需要评估转换成本和社区支持。
总结起来,解决Embedding模型慢的问题,优先从批量推理和GPU利用入手,而不是急着裁剪模型。把调用循环改成批量前向、把设备统一到CUDA、关闭梯度并控制batch size,通常就能获得可观的吞吐提升。后续遇到新的瓶颈时,再根据显存、延迟和数据流水线做针对性调优,会比直接堆机器或换模型更经济。
Embedding模型批量推理GPU加速修改时间:2026-10-05 10:14:12