vLLM 长文本 Embedding 分块处理实战:服务可达数百万 token 输入的嵌入模型
【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm
本文基于 vLLM 仓库中 长文本嵌入示例 展开,讲解如何用--pooler-config中的enable_chunked_processing与max_embed_len参数,让 Embedding 模型(如 multilingual-e5-large、bge、jina-embeddings 等)稳定处理超出模型自身上下文长度的超长文本(最高约 300 万 token)。读完后,你将能够独立部署一个 OpenAI 兼容的长文本 Embedding 服务、理解其分块切分与跨块加权均值聚合的源码级实现,并用仓库自带的测试客户端完成批量、一致性等端到端验证。
1. 功能概述:解决超长文本的 Embedding 难题
普通 Embedding 模型的上下文长度有限(例如 512 或 4096 token),直接输入一篇论文、一整份合同或一个代码库会触发如下报错:
ValueError: This model's maximum position embeddings length is 4096 tokens...vLLM 的Chunked Processing(分块处理)功能允许在--pooler-config中开启分块,自动把超长输入按模型位置编码上限切分为多个 chunk,每个 chunk 独立走模型推理并使用模型原生 pooling 策略,最后将所有 chunk 的结果按 token 数加权平均(MEAN aggregation)合并为最终嵌入向量。整个过程对调用方完全透明——仍通过 OpenAI 兼容的/v1/embeddings接口访问,且输出维度与标准处理保持一致。
该功能的适用场景(来自 README):
- 学术论文:含参考文献的完整研究论文;
- 法律文档:完整合同与法律文本;
- 书籍:整章乃至小型书籍;
- 代码仓库:大型代码库与配套文档。
2. 快速开始
示例目录 examples/pooling/embed/openai_embedding_long_text/ 包含三个文件:
| 文件 | 说明 |
|---|---|
| service.sh | 启用分块处理的服务器启动脚本 |
| client.py | 长文本嵌入的综合测试客户端 |
| README.md | 本功能的说明文档 |
2.1 启动服务
使用仓库提供的脚本,通过环境变量覆盖模型与上限参数即可启动:
# 基本用法(支持最长约 300 万 token 的输入) ./service.sh # 使用其他模型与更小的上限 MODEL_NAME="jinaai/jina-embeddings-v3" \ MAX_EMBED_LEN=1048576 \ ./service.sh # 面向超长文档 MODEL_NAME="intfloat/multilingual-e5-large" \ MAX_EMBED_LEN=3072000 \ ./service.sh如果不想走脚本,也可以直接手工执行vllm serve命令(摘自 client.py 头部注释):
# MEAN pooling:每个 chunk 内部用 MEAN,跨块统一 MEAN 聚合 vllm serve intfloat/multilingual-e5-large \ --pooler-config \ '{"pooling_type": "MEAN", "use_activation": true, ' \ '"enable_chunked_processing": true, "max_embed_len": 3072000}' \ --served-model-name multilingual-e5-large \ --trust-remote-code \ --port 31090 \ --api-key your-api-key # CLS pooling:chunk 内部用原生 CLS,跨块仍为 MEAN 聚合 vllm serve BAAI/bge-large-en-v1.5 \ --pooler-config \ '{"pooling_type": "CLS", "use_activation": true, ' \ '"enable_chunked_processing": true, "max_embed_len": 1048576}' \ --served-model-name bge-large-en-v1.5 \ --trust-remote-code \ --port 31090 \ --api-key your-api-key2.2 核心配置:--pooler-config
分块处理的关键参数都集中在--pooler-config的 JSON 中:
{ "pooling_type": "auto", "use_activation": true, "enable_chunked_processing": true, "max_embed_len": 3072000 }各字段含义(对应 PoolerConfig 的源码定义):
| 字段 | 类型/默认值 | 说明 |
|---|---|---|
pooling_type | auto/MEAN/CLS/LAST | chunk 内部使用的模型原生 pooling 策略;不影响跨块聚合 |
use_activation | bool,默认None(多数模型等效True) | 是否对每个 chunk 的 pooling 输出施加激活函数(如归一化) |
enable_chunked_processing | bool,默认False | 开启分块处理:长输入被切分为多个 chunk,分别处理后用加权平均聚合 |
max_embed_len | int,默认None(回退为max_model_len) | 允许接受的 Embedding 输入最大长度;超过该值会被拒绝 |
注意(来自 README):
pooling_type只决定每个 chunk 内部的 pooling 方式;当输入超过模型原生最大长度时,跨块聚合始终使用 MEAN 策略,与各 chunk 的 token 数成正比加权。
分块处理的行为对照表:
| 组成 | 行为 | 说明 |
|---|---|---|
| 块内(Within chunks) | 模型原生 pooling | 使用为该模型配置的 pooling 策略(MEAN/CLS/LAST) |
| 跨块聚合(Cross-chunk) | 恒为 MEAN | 基于各 chunk token 数的加权平均 |
| 性能 | 最优 | 所有 chunk 全部参与处理,实现完整语义覆盖 |
2.3 环境变量
service.sh 支持以下环境变量:
| 变量 | 默认值 | 说明 |
|---|---|---|
MODEL_NAME | intfloat/multilingual-e5-large | 要使用的 Embedding 模型(支持多种模型) |
PORT | 31090 | 服务端口 |
GPU_COUNT | 1 | 使用的 GPU 数量(映射到--tensor-parallel-size) |
MAX_EMBED_LEN | 3072000 | Embedding 输入最大长度(支持超长文档) |
POOLING_TYPE | auto | 模型原生 pooling 类型:auto、MEAN、CLS、LAST(仅影响块内 pooling,不影响跨块聚合) |
API_KEY | EMPTY(脚本内为your-api-key) | 鉴权 API key |
3. 工作原理:从请求进入到向量输出的完整链路
3.1 输入校验:max_embed_len放宽长度上限
README 将工作机制总结为五步:
- 增强的输入校验:
max_embed_len允许接受长于max_model_len的输入,无需额外环境变量(不再需要VLLM_ALLOW_LONG_MAX_MODEL_LEN); - 智能分块:基于模型位置编码上限(
max_position_embeddings)切分文本,保持语义完整性; - 统一处理:所有 chunk 分别走模型推理,使用其配置的 pooling 策略;
- MEAN 聚合:输入超过模型原生长度时,按 chunk token 数加权平均合并结果;
- 一致输出:最终嵌入向量的维度与标准处理完全相同。
具体到输入长度判定:
- 在
max_embed_len以内:输入被接受并正常处理(最高 3M+ token); - 超过
max_position_embeddings:自动触发的分块处理; - 超过
max_embed_len:输入被拒绝并返回明确的错误信息; - 无需环境变量:不再依赖
VLLM_ALLOW_LONG_MAX_MODEL_LEN。
从源码结构看,这一放宽发生在请求的分词参数构造阶段:EmbeddingTokenizeParamsMixin.build_tok_params 中,一旦enable_chunked_processing为True,就把max_total_tokens置为None,即分词阶段不再对输入总长度设限:
pooler_config = model_config.pooler_config if pooler_config is not None: if pooler_config.enable_chunked_processing: max_total_tokens = None # 分块模式:不限制输入总长度 else: max_embed_len = pooler_config.max_embed_len or default_max_total_tokens max_output_tokens = default_max_total_tokens - max_embed_len而在非分块模式下,max_embed_len仍会参与校验:max_output_tokens = max_model_len - max_embed_len,超过该上限的请求在分词阶段即被拦截,并抛出 PoolerConfig.max_embed_len 文档所描述的"maximum embedding input length"错误。
3.2 分块切分:按max_model_len逐段展开
在线服务的预处理阶段,ServingEmbedding._preprocessing 在常规预处理完成后调用 EmbedIOProcessor.maybe_pre_process_chunked 完成切分:
max_model_len = self.model_config.max_model_len for chunk_idx, chunk_tokens in enumerate(chunk_list(prompt_token_ids, max_model_len)): chunked_engine_inputs.append(...) prompt_request_ids.append( f"{request_id}-prompt-{prompt_idx}-chunk-{chunk_idx}" )两个实现要点值得注意:
chunk 大小即
max_model_len:一条 150000 token 的输入在max_model_len=4096时会被切为 37 个 chunk,与 README 给出的调试日志一致:INFO: Input length 150000 exceeds max_position_embeddings 4096, will use chunked processing INFO: Split input of 150000 tokens into 37 chunks (max_chunk_size: 4096)唯一的 chunk 请求 ID:每个 chunk 的请求 ID 采用
{request_id}-prompt-{prompt_idx}-chunk-{chunk_idx}格式,prompt_idx区分同一批请求中的不同原文。这修复了批量处理中多条长文本同时分块时的 chunk ID 冲突问题——client.py 中专门用test_multiple_long_texts_batch验证了这一点。
另外该功能明确要求输入为prompt_token_ids形式:io_processor.py 对EmbedsPrompt和EncoderDecoderInput直接抛出NotImplementedError,即分块处理目前面向纯文本 token 输入。
3.3 跨块聚合:在线加权均值
每个 chunk 独立跑完模型并使用其原生 pooling(MEAN/CLS/LAST)+ 激活后,io_processor.py 的_post_process_chunked以在线(online)方式累加加权向量,避免一次性驻留全部 chunk 结果,从而降低内存占用:
# MEAN pooling with online weighted averaging weight = len(result.prompt_token_ids) # 该 chunk 的 token 数即权重 embedding_data = result.outputs.data weighted_embedding = embedding_data.to(dtype=torch.float32) * weight if aggregator.weighted_sum is None: # 第一个 chunk aggregator.weighted_sum = weighted_embedding else: # 后续 chunk 累加 aggregator.weighted_sum += weighted_embedding aggregator.total_weight += weight最终对每条原始 prompt 求均值:
final_embedding = weighted_sum / total_weight从源码结构看,聚合按prompt_index分组进行(io_processor.py L182-L216),因此同一批请求中短文本、长文本、多条长文本可以混排提交,彼此不会互相污染;聚合结果随后被包装回PoolingRequestOutput,对外表现为一条普通的 Embedding 响应。
3.4 性能特征
README 给出的性能特征总结:
| 方面 | 行为 | 性能 |
|---|---|---|
| 块内处理 | 所有 chunk 均用原生 pooling 处理 | 与输入长度线性相关 |
| 跨块聚合 | MEAN 加权平均 | 开销极小 |
| 内存占用 | 与 chunk 数量成正比(在线聚合进一步降低峰值) | 中等,可扩展 |
| 语义质量 | 完整覆盖文本全部内容 | 对长文档最优 |
需要合理预期:超长文本耗时更长是正常现象,因为每个 chunk 都是一次独立推理调用。
4. 服务脚本细节:pooling 类型的自动识别
service.sh 除了启动服务,还内置了一个get_optimal_pooling_type函数:当POOLING_TYPE=auto时,按模型名启发式选择块内 pooling 类型,匹配规则如下(service.sh L33-L58):
| 模型名特征 | 自动选用的块内 pooling |
|---|---|
包含e5-或multilingual-e5 | MEAN(E5 系列原生 pooling) |
包含bge- | CLS(BGE 系列原生 pooling) |
包含gte- | LAST(GTE 系列原生 pooling) |
sentence-t5或st5 | MEAN(Sentence-T5 原生 pooling) |
包含jina-embeddings | MEAN(Jina 原生 pooling) |
Qwen*Embedding | LAST(Qwen-Embedding 原生 pooling) |
| 其他未知模型 | MEAN(默认) |
最终组装的 pooler 配置 JSON 由脚本动态拼接(service.sh L99),并以如下参数启动服务器(service.sh L102-L110):
vllm serve "$MODEL_NAME" \ --tensor-parallel-size "$GPU_COUNT" \ --enforce-eager \ --pooler-config "$POOLER_CONFIG" \ --served-model-name "${MODEL_CODE}" \ --api-key "$API_KEY" \ --trust-remote-code \ --port "$PORT" \ --host 0.0.0.0几点脚本行为说明:
--served-model-name使用的是短代号(MODEL_CODE,默认multilingual-e5-large),客户端请求model字段时必须与之匹配;- 脚本会校验
nvidia-smi报告的 GPU 数量,若GPU_COUNT超出可用数量会自动下调并告警; - 注意 service.sh L24 中
CUDA_VISIBLE_DEVICES=2,3,4,5是示例作者的本地设置,移植到自己环境时应改为实际可用的 GPU 序号。
5. 客户端测试:四组用例覆盖典型场景
安装依赖后运行测试客户端(pip install openai requests):
python client.pyclient.py 通过 OpenAI SDK 访问服务(base_url=http://localhost:31090/v1),核心调用是:
client = OpenAI(api_key=API_KEY, base_url=BASE_URL) response = client.embeddings.create( input=long_text, model="multilingual-e5-large", encoding_format="float" ) embedding = response.data[0].embedding四组测试分别验证:
- 不同长度(
test_embedding_with_different_lengths):短文本(基线)、中等文本(单 chunk)、长文本(约 2 chunks)、超长文本(3+ chunks),打印嵌入维度、耗时与期望 chunk 数; - 批量混合长度(
test_batch_embedding):一次请求中混合短文本与需分块的长文本,验证每条输入都能得到对应嵌入; - 多条长文本同批(
test_multiple_long_texts_batch):同一批次提交 3 条内容不同的长文本并穿插短文本,通过两两余弦相似度验证 chunk 聚合没有张冠李戴(相似度低于 0.9 视为区分良好),这是对 chunk ID 冲突修复的回归验证; - 一致性(
test_embedding_consistency):同一长文本重复请求 3 次,比对余弦相似度(期望约 1.0,超过 0.999 即认为一致性达标)。
覆盖的测试场景对照(摘自 README):短文本正常处理、中等文本单 chunk、长文本多 chunk 聚合、超长文本多 chunk、极端长文本(10 万+ token)文档级处理、混合长度批量、跨运行可复现。
6. 故障排查
| 现象 | 错误信息 | 解决方案 |
|---|---|---|
| 分块处理未生效 | ValueError: This model's maximum position embeddings length is 4096 tokens... | 在 pooler config 中确认enable_chunked_processing: true |
输入超过max_embed_len | ValueError: This model's maximum embedding input length is 3072000 tokens... | 调大 pooler config 中的max_embed_len,或缩短输入 |
| 显存不足 | RuntimeError: CUDA out of memory | 通过调整模型的max_position_embeddings减小 chunk 大小,或增加 GPU 数量(--tensor-parallel-size) |
| 处理变慢 | —(预期行为) | 超长文本需多次推理调用,耗时自然更长 |
调试时可关注服务端日志中的分块活动信息(见 README 示例):
INFO: Input length 150000 exceeds max_position_embeddings 4096, will use chunked processing INFO: Split input of 150000 tokens into 37 chunks (max_chunk_size: 4096)7.max_embed_len增强特性与扩展指南
相比早期依赖VLLM_ALLOW_LONG_MAX_MODEL_LEN环境变量的方案,max_embed_len参数带来五点改进(摘自 README):
- 配置简化:无需
VLLM_ALLOW_LONG_MAX_MODEL_LEN环境变量; - 灵活的输入校验:可接受长于
max_model_len、至多max_embed_len的输入; - 极端长度支持:可处理数百万 token 的文档;
- 清晰的错误信息:输入超限时有明确的反馈;
- 向后兼容:既有配置继续有效(
max_embed_len默认None时回退为max_model_len,见 PoolerConfig)。
若要为其他 Embedding 模型扩展分块处理支持,仓库建议的路径是:
- 检查该模型与 pooling 架构的兼容性;
- 用多种文本长度进行测试;
- 与单 chunk 处理结果对比,验证嵌入质量;
- 提交附带测试用例与文档更新的 PR。
8. 参考路径速查
| 内容 | 路径 |
|---|---|
| 功能说明文档 | examples/pooling/embed/openai_embedding_long_text/README.md |
| 服务启动脚本 | examples/pooling/embed/openai_embedding_long_text/service.sh |
| 测试客户端 | examples/pooling/embed/openai_embedding_long_text/client.py |
PoolerConfig(含enable_chunked_processing/max_embed_len定义) | vllm/config/pooler.py |
| 分块切分与在线加权聚合实现 | vllm/entrypoints/pooling/embed/io_processor.py |
| 在线服务预处理挂接点 | vllm/entrypoints/pooling/embed/serving.py |
| 分词阶段的长度校验逻辑 | vllm/entrypoints/pooling/base/protocol.py |
适用前提与限制小结:分块处理面向纯 token 文本输入(不支持EmbedsPrompt/EncoderDecoderInput);跨块聚合固定为按 token 数加权的 MEAN;pooling_type只作用于 chunk 内部;输入长度上限由max_embed_len显式约束。理解了这几条,你就能安全地把该方案用于学术文献、法律合同、书籍与代码库级别的向量化检索场景。
【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考