Reranker 模块
请求示例见 docs/QUICKSTART.md §3.5。扩展规范见 docs/DEVELOPER_GUIDE.md §7。部署与调优实战见 reranker/DEPLOYMENT_AND_TUNING.md。ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF 的专项接入与调优结论见 reranker/GGUF_0_6B_INSTALL_AND_TUNING.md。
Reranker 服务提供统一的 /rerank API,支持可插拔后端(BGE、Qwen3-vLLM、Qwen3-Transformers、Qwen3-GGUF、DashScope 云重排)。调用方通过 HTTP 访问,不关心具体后端。
当前结论
在当前项目的线上形态里,首选后端是 qwen3_vllm_score,次选后端是 qwen3_vllm。
原因不是“LLM.score() 理论上更高级”,而是这轮优化后,qwen3_vllm_score 在当前硬件和依赖栈上形成了一套更干净、更稳定、也更快的组合:
- 模型:
Qwen/Qwen3-Reranker-0.6B - GPU:Tesla T4 16GB
- CUDA:12.8
- PyTorch:
2.10.0+cu128 - vLLM-score 环境:
vllm==0.18.0 - attention:由 vLLM 运行时自动选择后端实现;在已验证的 T4 栈上日志可见
FLASHINFER
这次经验沉淀的核心结论有 4 条:
qwen3_vllm_score的 attention 实现由 vLLM 在运行时按 GPU 与版本自动选择。- 在已验证栈(T4 + vLLM 0.18.x 等)上,日志可见选用
FLASHINFER等由运行时选定的路径。 - 无论
score还是generate,真正有价值的优化点都不是 prompt 小改,而是: 去重、按 doc 长度排序分批、合适的infer_batch_size、合理的max_model_len、前缀缓存、隔离 venv 与运行时缓存目录。 - 本项目当前统一把
instruction_format配成standard。代码仍兼容两种格式,但它不是本轮性能优化的重点,也不是推荐继续投入精力的方向。
后端总览
| 后端 | 当前定位 | 结论 |
|---|---|---|
qwen3_vllm_score |
主推荐 | 走 vLLM LLM.score() 的 pooling / classify 路径:对每条 (query, doc) 直接产出相关分,不经 causal LM 的整步 generate。相对 qwen3_vllm(generate(max_tokens=1) + yes/no 的 logprob 推导),省去每对样本上大词表 softmax / 采样约束那一层的常规开销,语义与 cross-encoder 式 rerank 更一致;在当前栈与 T4 上延迟表现最好 |
qwen3_vllm |
次推荐 | 稳定、成熟、好排障,是很好的 fallback 和对照组 |
qwen3_transformers |
兼容方案 | |
qwen3_transformers_packed |
特定场景方案 | T可能实现还有问题,没调好 |
qwen3_gguf / qwen3_gguf_06b |
低显存 / 功能兜底 | 更适合资源受限场景,不适合作为当前主在线方案 |
dashscope_rerank |
云服务方案 | 运维简单,但依赖外部服务和网络 |
目录与入口
reranker/server.py:FastAPI 服务,启动时按配置加载一个后端reranker/backends/:后端实现与工厂backends/__init__.py:get_rerank_backend(name, config)backends/qwen3_vllm_score.py:当前最优的本地 GPU rerankerbackends/qwen3_vllm.py:次优的本地 GPU rerankerbackends/qwen3_transformers.py:Transformers 基线实现backends/qwen3_transformers_packed.py:packed 推理实现backends/qwen3_gguf.py:GGUF + llama.cpp 后端backends/dashscope_rerank.py:DashScope 云端重排后端
scripts/setup_reranker_venv.sh:按后端创建独立 venvscripts/start_reranker.sh:启动 reranker 服务scripts/smoke_qwen3_vllm_score_backend.py:qwen3_vllm_score本地 smokescripts/benchmark_reranker_random_titles.py:随机标题压测脚本scripts/run_reranker_vllm_instruction_benchmark.sh:历史矩阵脚本
环境基线
当前验证环境:
- GPU:
Tesla T4 16GB - Driver / CUDA:
570.158.01 / 12.8 - Python:
3.12.3 torch:2.10.0+cu128transformers:4.51+qwen3_vllm_score环境:vllm==0.18.0qwen3_vllm环境:vllm>=0.8.5
独立 venv 约定:
qwen3_vllm->.venv-rerankerqwen3_vllm_score->.venv-reranker-scoreqwen3_transformers->.venv-reranker-transformersqwen3_transformers_packed->.venv-reranker-transformers-packedqwen3_gguf->.venv-reranker-ggufqwen3_gguf_06b->.venv-reranker-gguf-06bbge->.venv-reranker-bgedashscope_rerank->.venv-reranker-dashscope
这样做不是形式主义,而是因为:
- 不同后端的 CUDA / vLLM / llama.cpp 依赖耦合很深,混装后更难定位性能和兼容性问题
- qwen3_vllm_score 和 qwen3_vllm 分了两个环境,是因为qwen3_vllm_score使用了vllm 0.18,但是后面经过测试两者性能相同。所以其实可以共用一个环境。不过没有动力合并回去。
安装与部署
1. 创建后端环境
qwen3_vllm_score:
./scripts/setup_reranker_venv.sh qwen3_vllm_score
qwen3_vllm:
./scripts/setup_reranker_venv.sh qwen3_vllm
2. 基础检查
nvidia-smi
./.venv-reranker-score/bin/python -c "import torch, vllm; print(torch.cuda.is_available(), torch.cuda.get_device_name(0), vllm.__version__)"
./.venv-reranker/bin/python -c "import torch, vllm; print(torch.cuda.is_available(), torch.cuda.get_device_name(0), vllm.__version__)"
3. 启动服务
./scripts/start_reranker.sh
scripts/start_reranker.sh 做了几件对性能和稳定性都很关键的事:
- 自动选择当前 backend 对应的独立 venv
- 为 vLLM / triton / torch.compile 指定独立缓存目录
- 把后端 venv 的
bin放到PATH前面
最后这一点很重要。对 qwen3_vllm_score 来说,T4 上 vLLM 自动选择 FLASHINFER 时,首次 JIT 需要 ninja,而 ninja 是装在对应 venv 里的。如果裸跑一个没有正确 PATH 的 Python 进程,就可能出现“环境明明装了,worker 里却找不到编译工具”的问题。
4. Smoke
PYTHONPATH=. ./.venv-reranker-score/bin/python scripts/smoke_qwen3_vllm_score_backend.py --gpu-memory-utilization 0.2
如果显卡上还有别的重进程,gpu_memory_utilization 可以临时调小或调大做排查;smoke 本身建议单独跑,不要和大压测并发。
当前最优方案:qwen3_vllm_score
它为什么是当前最优
qwen3_vllm_score.py 的优势,来自这几个组合在一起:
- 使用 vLLM 的
LLM.score()(pooling / classify),对 (query, doc) 直接打分,而非借 generate 在整词表上走最后一步分布再抠 yes/no——省掉那一类路径上的常规算力与模板绕路。 - 使用独立的
.venv-reranker-score,把vllm==0.18.0固定下来,避免和其他后端互相污染。 - attention 后端由 vLLM 按 GPU 与版本自动选择;
config.yaml里与 rerank 相关的调参集中在批量、长度、缓存、显存占比等项。 - 在已验证的 T4 依赖栈上,运行时通常选用
FLASHINFER(见服务日志);与 FlashAttention 2 等路径的取舍由 vLLM 内部策略完成。 - 服务层保留高杠杆优化: 全局去重、按 doc 长度排序、分批推理、前缀缓存、单进程锁保护。
关键实现点
qwen3_vllm_score.py 里值得关注的地方:
runner/convert保持 auto:走 pooling / classify 与LLM.score()的推荐接法(vLLM 0.17+)hf_overrides:把原始 Qwen3 reranker 权重按官方要求映射到Qwen3ForSequenceClassificationLLM(...)仅使用本后端所需的模型与并行等参数;attention 后端由 vLLM 内部按运行环境选用deduplicate_with_positions(...):先去重,再回填原始顺序sort_by_doc_length:减少 padding 浪费infer_batch_size:控制服务层分批enable_prefix_caching:对重复前缀场景有收益self._infer_lock:避免当前进程模型下并发调用破坏 vLLM engine 稳定性
Attention 与算力路径(现状)
- vLLM 根据 GPU 算力架构与当前 wheel 中的实现(如随发行版提供的 flashinfer 等)自动选用 attention 路径。
- 在 Tesla T4(
sm_75) + vLLM 0.18.x 的已验证环境中,服务日志中可见选用FLASHINFER。 - 最佳实践:性能调优放在
max_model_len、infer_batch_size、gpu_memory_utilization、去重、长度排序、prefix cache 等服务可见参数上。与 400 docs 量级相关的稳态 HTTP 数字见perf_reports/reranker_vllm_instruction/2026-03-25/RESULTS.md(主表方法论 + Addendum 中qwen3_vllm_score补充行)。
推荐配置
当前项目统一使用 standard,README 也按这个基线描述:
services:
rerank:
backend: "qwen3_vllm_score"
backends:
qwen3_vllm_score:
model_name: "Qwen/Qwen3-Reranker-0.6B"
use_original_qwen3_hf_overrides: true
engine: "vllm"
max_model_len: 256
tensor_parallel_size: 1
gpu_memory_utilization: 0.20
dtype: "float16"
enable_prefix_caching: true
enforce_eager: false
infer_batch_size: 100
sort_by_doc_length: true
instruction_format: standard
instruction: "Rank products by query with category & style match prioritized"
优点
- 当前本地 GPU 方案里性能最好
- attention 由 vLLM 在引擎内统一决策;仓库侧配置只覆盖批量、长度、缓存、显存等,实现路径短
- score / classify 路径与 rerank 任务对齐;相对 generate + 词表 logprob 少一层常规开销
- 服务层优化(去重、排序分批、缓存)与后端解耦清晰,易维护
缺点
- 依赖更新的 vLLM 栈,升级时要重新验证
- 首次启动会经历 compile / JIT / graph capture,冷启动偏慢
- 对环境完整性更敏感,尤其是 CUDA、worker 进程和
ninja
次优方案:qwen3_vllm
它为什么仍然很有价值
qwen3_vllm.py 是当前最好的次优方案,不只是“备用”,而是一个很重要的稳定对照组。
它走的是:
- causal LM
generate(max_tokens=1)- 只允许输出
yes/no - 用最后一步 logprobs 反推出相关性分数
这条路径的优点是工程上非常稳:
- 行为更容易理解
- 更容易和 Hugging Face tokenizer 对齐
- 排查问题时更直观
- 在一些旧版本 vLLM 或其他 GPU 组合上,表现可能仍然很好
它为什么排在第二
它不是当前第一名,主要不是因为模型差,而是路径更“绕”:
- 要先走 chat template
- 要自己维护
yes/notoken - 要做一次短 decode
- 要从 logprobs 里手工算概率
也就是说,qwen3_vllm 的打分是“借 generate 模式实现 rerank”,而不是原生 score 路径。它依然有效,但从结构上不如 qwen3_vllm_score 直接。
关键实现点
AutoTokenizer.apply_chat_template(...)SamplingParams(max_tokens=1, allowed_token_ids=[yes, no])generate(...)后从最后一步 logprobs 计算 yes/no 概率- 同样具备去重、按长度排序、分批推理、前缀缓存、单进程锁等优化
推荐配置
services:
rerank:
backends:
qwen3_vllm:
model_name: "Qwen/Qwen3-Reranker-0.6B"
engine: "vllm"
max_model_len: 256
tensor_parallel_size: 1
gpu_memory_utilization: 0.20
dtype: "float16"
enable_prefix_caching: true
enforce_eager: false
infer_batch_size: 100
sort_by_doc_length: true
instruction_format: standard
instruction: "Rank products by query with category & style match prioritized"
优点
- 路径成熟,易理解,易排障
- 作为 fallback 很合适
- 和
qwen3_vllm_score共用很多服务层优化经验
缺点
- 不是原生 reranker score 路径
- 比
qwen3_vllm_score多一层 tokenizer / generate / logprob 推导成本 - 当前环境下性能略逊
这轮优化里真正有价值的方法
下面这些是跨后端都值得保留的经验,优先级高于 prompt 微调。
1. 全局去重
对 doc 先做全局去重,再按原始索引回填,是收益最高、风险最低的优化之一。
适用原因:
- 商品标题、变体标题、重复 SKU 文案很常见
- 去重不会改变 API 契约
- 能直接减少模型真实推理次数
2. 按 doc 长度排序再分批
sort_by_doc_length: true 建议保持开启。
原因:
- 同一批里长度更接近,padding 更少
- 对
infer_batch_size较大时收益更明显 - 实现成本低,行为稳定
当前实现里长度估计采用字符长度近似,这已经足够实用。没有必要为了这一层再引入额外 tokenizer 计算开销。
3. infer_batch_size 作为核心调参项
对当前业务形态,infer_batch_size 是最值得扫的参数。
建议:
- 先固定其他参数,再扫
64 / 80 / 96 / 100 / 128 - 看的是单请求延迟和稳定性,不只是吞吐
- 不要只拿一次结果下结论,至少 warm-up 后 repeat 5 次
4. max_model_len 不要盲目开大
当前场景是短 query + 商品标题/短描述,不需要把 max_model_len 拉得很高。
经验:
160适合做对比实验256更像当前线上保守值- 再往上加,对当前场景通常是成本大于收益
5. enable_prefix_caching
建议开启。
原因:
- 一个请求里通常是同一个 query 对很多 doc
- 前缀共享明显
- vLLM 在这类场景里能吃到 prefix cache 的收益
6. enforce_eager
建议:
- 线上常规运行:
false - smoke / 排障 / 显存紧张时:可临时
true
因为:
false时可使用 compile / graph capture,稳态性能更好true时启动更直接,问题更容易定位
7. 独立 venv + 独立运行时缓存
这不是“环境洁癖”,而是性能优化的一部分。
收益:
- 避免不同 vLLM 版本互相污染
- compile / triton / flashinfer 缓存可复用
- 便于精确复现实验结果
性能数据应该怎么看
perf_reports/reranker_vllm_instruction/2026-03-25/RESULTS.md 建议按三部分读:
- 方法论:脚本、预热、
--seed、HTTP 客户端延迟与/health核对等(可复用于后续复跑)。 - 主表:
qwen3_vllm/qwen3_vllm_score×instruction_format的矩阵基线(固定max_model_len等条件见该文)。 - Addendum:同一方法下对
qwen3_vllm_score当前实现的补充测数(含 compact/standard),便于与主表对照同一指标口径。
对外结论应基于:当前代码 revision、文档中注明的 max_model_len / GPU 占用、尽量避免与大压测或其他 GPU 重进程并发时的样本。
benchmark 建议流程
推荐流程:
- 确认目标 backend 已切换到正确配置
./scripts/start_reranker.shcurl http://127.0.0.1:6007/health- 跑 benchmark 脚本
- 保存 JSON 和 Markdown 结果
- 记录当时的 GPU 占用情况和
nvidia-smi
重点观察:
- 单请求延迟
- 稳态均值
- 波动大小
- 冷启动与热启动差异
- 是否有显存竞争导致的异常样本
常见问题
1. 为什么第一次启动很慢
因为第一次会叠加:
- 模型加载
- torch.compile
- CUDA graph capture
- flashinfer / triton JIT
这不是异常。看性能时要区分冷启动和稳态。
2. 为什么 smoke 有时会 OOM
常见原因不是参数本身,而是:
- GPU 上同时还有 embedding / translator / 其他 vLLM 进程
- smoke 和 benchmark 并发跑
gpu_memory_utilization设得不适合当前剩余显存
处理方式:
- 先单独跑 smoke
- 看
nvidia-smi - 适当调整
gpu_memory_utilization
3. qwen3_vllm_score 的 attention 要在哪里调
由 vLLM 在运行时按 GPU 与版本自动选择;与延迟和稳定性更直接相关、且建议在仓库里动的,是 max_model_len、infer_batch_size、gpu_memory_utilization、去重、排序分批、prefix cache 等。
代码阅读建议
如果要快速理解当前主线实现,建议按这个顺序读:
reranker/backends/qwen3_vllm_score.pyreranker/backends/qwen3_vllm.pyscripts/start_reranker.shscripts/setup_reranker_venv.shconfig/config.yaml里的services.rerank.backends.*
阅读重点:
- 后端如何构造 prompt(
instruction_formatcompact / standard) - 后端调用
score()还是generate(),以及是否经过整词表上的最后一步分布 qwen3_vllm_score里LLM(...)传了哪些字段(模型、并行、dtype、缓存等),以及 attention 如何由 vLLM 内部承接- 服务层去重 / 排序 / 分批 / 回填怎么做
最终建议
如果你的目标是“当前仓库在 T4 上的在线 reranker 最优落地”,建议直接遵循下面这条线:
- 主后端:
qwen3_vllm_score - 模型:
Qwen/Qwen3-Reranker-0.6B - 配置:
instruction_format以standard为项目统一基线;细调优先放在批量与长度相关项 - attention:由 vLLM 运行时自动选择;调参见
max_model_len/infer_batch_size/gpu_memory_utilization等 - 关键参数:
max_model_len、infer_batch_size、gpu_memory_utilization - 服务层优化:去重、长度排序、分批、prefix cache
- 工程约束:独立 venv、正确
PATH、缓存目录隔离、单独 smoke、完整 benchmark 归档