# 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 条: 1. **`qwen3_vllm_score` 的 attention 实现由 vLLM 在运行时按 GPU 与版本自动选择**。 2. 在已验证栈(T4 + vLLM 0.18.x 等)上,日志可见选用 **`FLASHINFER`** 等由运行时选定的路径。 3. 无论 `score` 还是 `generate`,真正有价值的优化点都不是 prompt 小改,而是: 去重、按 doc 长度排序分批、合适的 `infer_batch_size`、合理的 `max_model_len`、前缀缓存、隔离 venv 与运行时缓存目录。 4. 本项目当前统一把 `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 reranker - `backends/qwen3_vllm.py`:次优的本地 GPU reranker - `backends/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`:按后端创建独立 venv - `scripts/start_reranker.sh`:启动 reranker 服务 - `scripts/smoke_qwen3_vllm_score_backend.py`:`qwen3_vllm_score` 本地 smoke - `scripts/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+cu128` - `transformers`:`4.51+` - `qwen3_vllm_score` 环境:`vllm==0.18.0` - `qwen3_vllm` 环境:`vllm>=0.8.5` 独立 venv 约定: - `qwen3_vllm` -> `.venv-reranker` - `qwen3_vllm_score` -> `.venv-reranker-score` - `qwen3_transformers` -> `.venv-reranker-transformers` - `qwen3_transformers_packed` -> `.venv-reranker-transformers-packed` - `qwen3_gguf` -> `.venv-reranker-gguf` - `qwen3_gguf_06b` -> `.venv-reranker-gguf-06b` - `bge` -> `.venv-reranker-bge` - `dashscope_rerank` -> `.venv-reranker-dashscope` 这样做不是形式主义,而是因为: - 不同后端的 CUDA / vLLM / llama.cpp 依赖耦合很深,混装后更难定位性能和兼容性问题 - qwen3_vllm_score 和 qwen3_vllm 分了两个环境,是因为qwen3_vllm_score使用了vllm 0.18,但是后面经过测试两者性能相同。所以其实可以共用一个环境。不过没有动力合并回去。 ## 安装与部署 ### 1. 创建后端环境 `qwen3_vllm_score`: ```bash ./scripts/setup_reranker_venv.sh qwen3_vllm_score ``` `qwen3_vllm`: ```bash ./scripts/setup_reranker_venv.sh qwen3_vllm ``` ### 2. 基础检查 ```bash 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. 启动服务 ```bash ./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 ```bash 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` 的优势,来自这几个组合在一起: 1. 使用 vLLM 的 **`LLM.score()`**(pooling / classify),对 (query, doc) **直接打分**,而非借 **generate** 在整词表上走最后一步分布再抠 **yes/no**——**省掉**那一类路径上的常规算力与模板绕路。 2. 使用独立的 `.venv-reranker-score`,把 `vllm==0.18.0` 固定下来,避免和其他后端互相污染。 3. **attention 后端由 vLLM 按 GPU 与版本自动选择**;`config.yaml` 里与 rerank 相关的调参集中在批量、长度、缓存、显存占比等项。 4. 在已验证的 T4 依赖栈上,运行时通常选用 **`FLASHINFER`**(见服务日志);与 FlashAttention 2 等路径的取舍由 vLLM 内部策略完成。 5. 服务层保留高杠杆优化: 全局去重、按 doc 长度排序、分批推理、前缀缓存、单进程锁保护。 ### 关键实现点 `qwen3_vllm_score.py` 里值得关注的地方: - `runner` / `convert` 保持 **auto**:走 **pooling / classify** 与 **`LLM.score()`** 的推荐接法(vLLM 0.17+) - `hf_overrides`:把原始 Qwen3 reranker 权重按官方要求映射到 `Qwen3ForSequenceClassification` - `LLM(...)` 仅使用本后端所需的模型与并行等参数;**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 也按这个基线描述: ```yaml 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/no` token - 要做一次短 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 概率 - 同样具备去重、按长度排序、分批推理、前缀缓存、单进程锁等优化 ### 推荐配置 ```yaml 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 建议流程 推荐流程: 1. 确认目标 backend 已切换到正确配置 2. `./scripts/start_reranker.sh` 3. `curl http://127.0.0.1:6007/health` 4. 跑 benchmark 脚本 5. 保存 JSON 和 Markdown 结果 6. 记录当时的 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 等。 ## 代码阅读建议 如果要快速理解当前主线实现,建议按这个顺序读: 1. `reranker/backends/qwen3_vllm_score.py` 2. `reranker/backends/qwen3_vllm.py` 3. `scripts/start_reranker.sh` 4. `scripts/setup_reranker_venv.sh` 5. `config/config.yaml` 里的 `services.rerank.backends.*` 阅读重点: - 后端如何构造 prompt(`instruction_format` compact / 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 归档