From b0972ff9ec69731615d76b0d73cb4b9f59317bea Mon Sep 17 00:00:00 2001 From: tangwang Date: Wed, 25 Mar 2026 22:39:49 +0800 Subject: [PATCH] qwen3_vllm_score attention TRITON_ATTN -> FLASHINFER (之前因为错误将attention方法该回到TRITON_ATTN,性能相比于之前的vllm版本更差。但是那个错误是能解决的。已修复保持FLASHINFER) --- config/config.yaml | 24 ++++++++---------------- perf_reports/reranker_vllm_instruction/2026-03-25/RESULTS.md | 37 ++++++++++++++++++++++++++++++++++++- reranker/README.md | 588 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- reranker/backends/qwen3_vllm_score.py | 109 ++++++------------------------------------------------------------------------------------------------------- reranker/性能优化版本的qwen3_vllm_score 为什么反而更慢.md | 141 --------------------------------------------------------------------------------------------------------------------------------------------- scripts/smoke_qwen3_vllm_score_backend.py | 19 ++++--------------- scripts/start_reranker.sh | 4 ++-- 7 files changed, 456 insertions(+), 466 deletions(-) delete mode 100644 reranker/性能优化版本的qwen3_vllm_score 为什么反而更慢.md diff --git a/config/config.yaml b/config/config.yaml index 70aa944..1847d98 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -381,7 +381,7 @@ services: max_docs: 1000 normalize: true # 服务内后端(reranker 进程启动时读取) - backend: "qwen3_vllm" # bge | qwen3_vllm | qwen3_vllm_score | qwen3_transformers | qwen3_transformers_packed | qwen3_gguf | qwen3_gguf_06b | dashscope_rerank + backend: "qwen3_vllm_score" # bge | qwen3_vllm | qwen3_vllm_score | qwen3_transformers | qwen3_transformers_packed | qwen3_gguf | qwen3_gguf_06b | dashscope_rerank backends: bge: model_name: "BAAI/bge-reranker-v2-m3" @@ -394,7 +394,7 @@ services: qwen3_vllm: model_name: "Qwen/Qwen3-Reranker-0.6B" engine: "vllm" - max_model_len: 160 + max_model_len: 256 tensor_parallel_size: 1 gpu_memory_utilization: 0.20 dtype: "float16" @@ -402,9 +402,8 @@ services: enforce_eager: false infer_batch_size: 100 sort_by_doc_length: true - # 与 reranker/backends/qwen3_vllm.py 一致:standard=_format_instruction__standard(固定 yes/no system);compact=_format_instruction(instruction 作 system 且 user 内重复 Instruct) - # instruction_format: compact - instruction_format: standard + # standard=_format_instruction__standard(固定 yes/no system);compact=_format_instruction(instruction 作 system 且 user 内重复 Instruct) + instruction_format: standard # compact standard # instruction: "Given a query, score the product for relevance" # "rank products by given query" 比 “Given a query, score the product for relevance” 更好点 # instruction: "rank products by given query, category match first" @@ -420,18 +419,12 @@ services: model_name: "Qwen/Qwen3-Reranker-0.6B" # 官方 Hub 原版需 true;若改用已转换的 seq-cls 权重(如 tomaarsen/...-seq-cls)则设为 false use_original_qwen3_hf_overrides: true - # vLLM 0.18:算力 < 8(如 T4)默认注入 TRITON_ATTN,避免 FA2 在 sm<80 上报错;若更慢可关回退让 vLLM 自选: - # auto_triton_attn_on_sm_lt_8: false - # 关回退时 vLLM 可能走 FLASHINFER,首次 score 会 JIT,需 PATH 上有 ninja(requirements 已列 ninja;请用 ./scripts/start_reranker.sh 或 source venv/bin/activate,勿裸跑 /usr/bin 解析后的 python 且 PATH 无 venv/bin) - # 或环境变量 RERANK_VLLM_AUTO_TRITON_ATTN=0;仍可直接指定后端:RERANK_VLLM_ATTENTION_BACKEND / vllm_attention_backend - # vllm_attention_backend: "auto" - # 可选:与 vLLM 对齐;一般保持 auto # vllm_runner: "auto" # vllm_convert: "auto" # 可选:在 use_original_qwen3_hf_overrides 为 true 时与内置 overrides 合并 # hf_overrides: {} engine: "vllm" - max_model_len: 160 + max_model_len: 256 tensor_parallel_size: 1 gpu_memory_utilization: 0.20 dtype: "float16" @@ -439,9 +432,8 @@ services: enforce_eager: false infer_batch_size: 100 sort_by_doc_length: true - # 与 qwen3_vllm 同名项语义一致;默认 standard 与 vLLM 官方 Qwen3 reranker 前缀一致 - # instruction_format: compact - instruction_format: standard + # 默认 standard 与 vLLM 官方 Qwen3 reranker 前缀一致 + instruction_format: standard # compact standard instruction: "Rank products by query with category & style match prioritized" qwen3_transformers: model_name: "Qwen/Qwen3-Reranker-0.6B" @@ -458,7 +450,7 @@ services: qwen3_transformers_packed: model_name: "Qwen/Qwen3-Reranker-0.6B" instruction: "Rank products by query with category & style match prioritized" - max_model_len: 4096 + max_model_len: 256 max_doc_len: 160 max_docs_per_pack: 0 use_fp16: true diff --git a/perf_reports/reranker_vllm_instruction/2026-03-25/RESULTS.md b/perf_reports/reranker_vllm_instruction/2026-03-25/RESULTS.md index c9bf80d..4b6767c 100644 --- a/perf_reports/reranker_vllm_instruction/2026-03-25/RESULTS.md +++ b/perf_reports/reranker_vllm_instruction/2026-03-25/RESULTS.md @@ -51,7 +51,7 @@ JSON aggregates (means, stdev, raw `values_ms`): same directory, `qwen3_vllm_{co 1. **`compact` vs `standard`:** For both backends, **`compact` is faster** on this setup (shorter / different chat template vs fixed yes/no system prompt + user block — see `reranker/backends/qwen3_vllm.py` / `qwen3_vllm_score.py`). 2. **`qwen3_vllm` vs `qwen3_vllm_score`:** At **`n=1000`**, **`qwen3_vllm` + `compact`** is the fastest row (~2162 ms mean); **`qwen3_vllm_score` + `standard`** is the slowest (~2932 ms). Ordering can change on other GPUs / vLLM versions / batching. -3. **Repo default** after tests: `services.rerank.backend: qwen3_vllm_score`, `instruction_format: compact` on **both** `qwen3_vllm` and `qwen3_vllm_score` blocks (patch script keeps them aligned). +3. **Repo / 运维默认(当前)**:`services.rerank.backend` 多为 `qwen3_vllm_score`;**score** 块推荐 **`instruction_format: compact`**(与后端代码默认值一致)。`qwen3_vllm` 块的 `instruction_format` 可与 generate 后端单独配置,不必与 score 强制相同。 ## Tooling added / changed @@ -59,3 +59,38 @@ JSON aggregates (means, stdev, raw `values_ms`): same directory, `qwen3_vllm_{co - `scripts/benchmark_reranker_random_titles.py`: `--tag`, `--json-summary-out`, `--quiet-runs`. - `scripts/patch_rerank_vllm_benchmark_config.py`: surgical YAML patch (preserves newlines). - `scripts/run_reranker_vllm_instruction_benchmark.sh`: full matrix driver (continues if a benchmark exits non-zero; uses `--timeout 360`). + +--- + +## Addendum: `qwen3_vllm_score` after attention auto-select (FLASHINFER on T4) + +**Do not replace the table above** — it records the **older** `qwen3_vllm_score` behaviour (roughly: sm<8 时向 vLLM 注入 `attention_config` / `TRITON_ATTN`,且代码里 `instruction_format` 默认曾为 `standard`). + +### What changed in code / ops + +| Area | Before (baseline table) | After (this addendum) | +|------|-------------------------|------------------------| +| Attention | Backend forced / steered attention on T4 (e.g. `TRITON_ATTN` path) | **No** `attention_config` in `LLM(...)`; vLLM **auto** — on this T4 run, logs show **`FLASHINFER`** | +| Config surface | `vllm_attention_backend` / `RERANK_VLLM_ATTENTION_BACKEND` 等 | **Removed**(少 YAML/环境变量分支,逻辑收敛) | +| Code default `instruction_format` | `qwen3_vllm_score` 默认 `standard` | 与 `qwen3_vllm` 对齐为 **`compact`**(仍可在 YAML 写 `standard`) | +| Smoke / 启动 | — | `scripts/smoke_qwen3_vllm_score_backend.py`;`scripts/start_reranker.sh` 将 **venv `bin` 置于 `PATH`**(FLASHINFER JIT 依赖 venv 内的 `ninja`) | + +Micro-benchmark (same machine, isolated): **~927.5 ms → ~673.1 ms** at **n=400** docs on `LLM.score()` steady state (~**28%**), after removing the forced attention path and letting vLLM pick **FLASHINFER**. + +### Re-benchmark (HTTP `POST /rerank`, same methodology as §Methodology) + +- **Purpose:** Same comparison axis as the main table (`qwen3_vllm_score` only), **after** the FLASHINFER-friendly backend. +- **Controlled for `max_model_len`:** `services.rerank.backends.qwen3_vllm_score.max_model_len` set to **160** for this run so numbers are comparable to the **baseline** rows (also 160). Production `config.yaml` may use a different value (e.g. **196**); adjust YAML before repeating the benchmark if you need prod-shaped latency. +- **Seed / repeats:** `--seed 99`, `--repeat 5`, same script and title file as §Methodology. +- **Artifacts:** `qwen3_vllm_score_compact_post_flashinfer_opt.json`, `qwen3_vllm_score_standard_post_flashinfer_opt.json`. + +#### `qwen3_vllm_score` — mean latency (ms), post optimization + +| instruction_format | n=100 | n=200 | n=400 | n=600 | n=800 | n=1000 | vs baseline same row (approx.) | +|--------------------|------:|------:|------:|------:|------:|-------:|--------------------------------| +| `compact` | 178.5 | 351.7 | **688.2** | 1024.0 | 1375.8 | **1752.4** | e.g. n=400 **−28.8%**, n=1000 **−27.8%** vs 966.2 / 2428.4 | +| `standard` | 198.4 | 386.4 | **778.8** | 1174.6 | 1548.1 | **1956.6** | e.g. n=400 **−33.9%**, n=1000 **−33.3%** vs 1178.9 / 2931.7 | + +**`instruction_format: standard` 的优化点(本版):** 与 `compact` **共享**同一套 vLLM attention 自动选择;不再在 T4 上单独锁死 `TRITON_ATTN`。Prompt 仍比 `compact` 更长(固定 yes/no system + 官方前缀模板),因此 **absolute 延迟仍高于 `compact`**,但相对旧版 **standard** 行降幅与 **compact** 同量级(上表)。 + +**Takeaway:** Under T4 + vLLM 0.18 score path, **auto attention (FLASHINFER)** plus **`compact` default** brings `qwen3_vllm_score` much closer to `qwen3_vllm` timings from the baseline matrix; re-run the full 4-way matrix if you need refreshed `qwen3_vllm` rows on the same commit. diff --git a/reranker/README.md b/reranker/README.md index 39acbeb..221dfac 100644 --- a/reranker/README.md +++ b/reranker/README.md @@ -6,221 +6,433 @@ Reranker 服务提供统一的 `/rerank` API,支持可插拔后端(BGE、Qwen3-vLLM、Qwen3-Transformers、Qwen3-GGUF、DashScope 云重排)。调用方通过 HTTP 访问,不关心具体后端。 -**特性** -- 多后端:`qwen3_vllm`、`qwen3_vllm_score`(同模型,vLLM ``LLM.score()`` + 独立 `.venv-reranker-score`)、`qwen3_transformers`、`qwen3_transformers_packed`(共享前缀 + packed attention mask)、`qwen3_gguf`(Qwen3-Reranker-4B GGUF + llama.cpp)、`qwen3_gguf_06b`(Qwen3-Reranker-0.6B Q8_0 GGUF + llama.cpp)、`bge`(兼容保留) -- 云后端:`dashscope_rerank`(调用 DashScope `/compatible-api/v1/reranks`,支持按地域切换 endpoint) -- 统一配置:`config/config.yaml` → `services.rerank.backend` / `services.rerank.backends.` -- 文档去重、分数与输入顺序一致、FP16/GPU 支持(视后端) +## 当前结论 + +在当前项目的线上形态里,**首选后端是 `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/bge.py`:BGE 后端 - - `backends/qwen3_vllm.py`:Qwen3-Reranker-0.6B + vLLM(generate + logprobs) - - `backends/qwen3_vllm_score.py`:同上模型 + vLLM ``LLM.score()``(`requirements_reranker_qwen3_vllm_score.txt` / `.venv-reranker-score`) - - `backends/qwen3_transformers.py`:Qwen3-Reranker-0.6B 纯 Transformers 后端(官方 Usage 方式) - - `backends/qwen3_transformers_packed.py`:Qwen3-Reranker-0.6B + Transformers packed 推理(共享 query prefix,适合 `1 query + 400 docs`) - - `backends/qwen3_gguf.py`:Qwen3-Reranker GGUF + llama.cpp 后端(支持 `qwen3_gguf` / `qwen3_gguf_06b`) - - `backends/dashscope_rerank.py`:DashScope 云重排后端(HTTP 调用) -- `reranker/bge_reranker.py`:BGE 核心推理(被 bge 后端封装) -- `reranker/config.py`:服务端口、MAX_DOCS、NORMALIZE 等(后端参数在 config.yaml) - -## 依赖 -- 通用:`torch`、`transformers`、`fastapi`、`uvicorn`(隔离环境见 `requirements_reranker_service.txt`;全量 ML 环境另见 `requirements_ml.txt`) -- **Qwen3-vLLM 后端**:`vllm>=0.8.5`、`transformers>=4.51.0`(`qwen3_vllm` → `.venv-reranker`) -- **Qwen3-vLLM-score 后端**:固定 `vllm==0.18.0`(`qwen3_vllm_score` → `.venv-reranker-score`,见 `requirements_reranker_qwen3_vllm_score.txt`) -- **Qwen3-Transformers 后端**:`transformers>=4.51.0`、`torch`(无需 vLLM,适合 CPU 或小显存) -- **Qwen3-Transformers-Packed 后端**:复用 Transformers 依赖(`qwen3_transformers_packed` → `.venv-reranker-transformers-packed`) -- **Qwen3-GGUF 后端**:`llama-cpp-python>=0.3.16` -- 现在按 backend 使用独立 venv: - - `qwen3_vllm` -> `.venv-reranker` - - `qwen3_vllm_score` -> `.venv-reranker-score` - - `qwen3_gguf` -> `.venv-reranker-gguf` - - `qwen3_gguf_06b` -> `.venv-reranker-gguf-06b` - - `qwen3_transformers` -> `.venv-reranker-transformers` - - `qwen3_transformers_packed` -> `.venv-reranker-transformers-packed` - - `bge` -> `.venv-reranker-bge` - - `dashscope_rerank` -> `.venv-reranker-dashscope` - ```bash - ./scripts/setup_reranker_venv.sh qwen3_gguf_06b - ``` - CUDA 构建建议: - ```bash - PATH=/usr/local/cuda/bin:$PATH \ - CUDACXX=/usr/local/cuda/bin/nvcc \ - CMAKE_ARGS="-DGGML_CUDA=on" \ - FORCE_CMAKE=1 \ - ./.venv-reranker-gguf/bin/pip install --no-cache-dir --force-reinstall --no-build-isolation llama-cpp-python==0.3.18 - ``` - -## 配置 -- **后端选择**:`config/config.yaml` 中 `services.rerank.backend`(`qwen3_vllm` | `qwen3_vllm_score` | `qwen3_transformers` | `qwen3_transformers_packed` | `qwen3_gguf` | `qwen3_gguf_06b` | `bge` | `dashscope_rerank`),或环境变量 `RERANK_BACKEND`。 -- **后端参数**:`services.rerank.backends.bge` / `services.rerank.backends.qwen3_vllm`,例如: + - `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_gguf" # 或 qwen3_vllm / bge + backend: "qwen3_vllm_score" backends: - bge: - model_name: "BAAI/bge-reranker-v2-m3" - device: null - use_fp16: true - batch_size: 64 - max_length: 512 - cache_dir: "./model_cache" - enable_warmup: true - qwen3_vllm: + qwen3_vllm_score: model_name: "Qwen/Qwen3-Reranker-0.6B" + use_original_qwen3_hf_overrides: true + engine: "vllm" max_model_len: 256 - infer_batch_size: 64 - sort_by_doc_length: true + tensor_parallel_size: 1 + gpu_memory_utilization: 0.20 + dtype: "float16" enable_prefix_caching: true enforce_eager: false - instruction: "Given a shopping query, rank product titles by relevance" - qwen3_transformers: - model_name: "Qwen/Qwen3-Reranker-0.6B" - instruction: "Given a shopping query, rank product titles by relevance" - max_length: 8192 - batch_size: 64 - use_fp16: true - tensor_parallel_size: 1 - gpu_memory_utilization: 0.8 - instruction: "Given a shopping query, rank product titles by relevance" - qwen3_transformers_packed: - model_name: "Qwen/Qwen3-Reranker-0.6B" - instruction: "Rank products by query with category & style match prioritized" - max_model_len: 4096 - max_doc_len: 160 - max_docs_per_pack: 0 - use_fp16: true + infer_batch_size: 100 sort_by_doc_length: true - attn_implementation: "eager" - qwen3_gguf: - repo_id: "DevQuasar/Qwen.Qwen3-Reranker-4B-GGUF" - filename: "*Q8_0.gguf" - local_dir: "./models/reranker/qwen3-reranker-4b-gguf" - cache_dir: "./model_cache" + instruction_format: standard instruction: "Rank products by query with category & style match prioritized" - n_ctx: 384 - n_batch: 384 - n_ubatch: 128 - n_gpu_layers: 24 - flash_attn: true - offload_kqv: true - infer_batch_size: 8 +``` + +### 优点 + +- 当前本地 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 - length_sort_mode: "char" - qwen3_gguf_06b: - repo_id: "ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF" - filename: "qwen3-reranker-0.6b-q8_0.gguf" - local_dir: "./models/reranker/qwen3-reranker-0.6b-q8_0-gguf" - cache_dir: "./model_cache" + instruction_format: standard instruction: "Rank products by query with category & style match prioritized" - n_ctx: 256 - n_batch: 256 - n_ubatch: 256 - n_gpu_layers: 999 - infer_batch_size: 32 - sort_by_doc_length: true - length_sort_mode: "char" - reuse_query_state: false - dashscope_rerank: - model_name: "qwen3-rerank" - endpoint: "https://dashscope.aliyuncs.com/compatible-api/v1/reranks" - api_key_env: "RERANK_DASHSCOPE_API_KEY_CN" - timeout_sec: 15.0 - top_n_cap: 0 - batchsize: 64 # 0关闭;>0并发小包调度(top_n/top_n_cap 仍生效,分包后全局截断) - instruct: "Given a shopping query, rank product titles by relevance" - max_retries: 2 - retry_backoff_sec: 0.2 ``` -DashScope endpoint 地域示例: -- 中国:`https://dashscope.aliyuncs.com/compatible-api/v1/reranks` -- 新加坡:`https://dashscope-intl.aliyuncs.com/compatible-api/v1/reranks` -- 美国:`https://dashscope-us.aliyuncs.com/compatible-api/v1/reranks` +### 优点 -DashScope 认证: -- `api_key_env` 必填,表示该后端读取哪个环境变量作为 API Key -- 推荐按地域分别注入: - - `RERANK_DASHSCOPE_API_KEY_CN=...` - - `RERANK_DASHSCOPE_API_KEY_US=...` +- 路径成熟,易理解,易排障 +- 作为 fallback 很合适 +- 和 `qwen3_vllm_score` 共用很多服务层优化经验 -- 服务端口、请求限制等仍在 `reranker/config.py`(或环境变量 `RERANKER_PORT`、`RERANKER_HOST`)。 +### 缺点 -## 运行 -```bash -./scripts/start_reranker.sh -``` -该脚本会按当前 `services.rerank.backend` 自动选择对应的独立 venv;首次请先执行 `./scripts/setup_reranker_venv.sh `。 +- 不是原生 reranker score 路径 +- 比 `qwen3_vllm_score` 多一层 tokenizer / generate / logprob 推导成本 +- 当前环境下性能略逊 -## 性能压测(1000 docs) -```bash -./scripts/benchmark_reranker_1000docs.sh -``` -输出目录:`perf_reports//reranker_1000docs/`。 +## 这轮优化里真正有价值的方法 -## API -### Health -``` -GET /health -``` -Response 含 `backend`(当前后端名)、`model`、`model_loaded`、`status`。 +下面这些是跨后端都值得保留的经验,优先级高于 prompt 微调。 -### Rerank -``` -POST /rerank -Content-Type: application/json - -{ - "query": "wireless mouse", - "docs": ["logitech mx master", "usb cable", "wireless mouse bluetooth"], - "top_n": 10 -} -``` +### 1. 全局去重 -`top_n` 为可选字段: -- 对本地后端(`qwen3_vllm` / `qwen3_transformers` / `qwen3_transformers_packed` / `qwen3_gguf` / `qwen3_gguf_06b` / `bge`)通常会忽略,仍返回全量分数。 -- 对 `dashscope_rerank` 可用于控制云端返回的候选量,建议设置为 `page+size`(例如分页 `from=20,size=10` 时传 `30`)。 +对 doc 先做全局去重,再按原始索引回填,是收益最高、风险最低的优化之一。 -Response: -``` -{ - "scores": [0.93, 0.02, 0.88], - "meta": { - "input_docs": 3, - "usable_docs": 3, - "unique_docs": 3, - "dedup_ratio": 0.0, - "elapsed_ms": 12.4, - "model": "BAAI/bge-reranker-v2-m3", - "device": "cuda", - "fp16": true, - "batch_size": 64, - "max_length": 512, - "normalize": true, - "service_elapsed_ms": 13.1 - } -} -``` +适用原因: -## Logging -The service uses standard Python logging. For structured logs and full output, -run uvicorn with: -```bash -uvicorn reranker.server:app --host 0.0.0.0 --port 6007 --log-level info -``` +- 商品标题、变体标题、重复 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 最优落地”,建议直接遵循下面这条线: -## Notes -- 无请求级缓存;输入按字符串去重后推理,再按原始顺序回填分数。 -- 空或 null 的 doc 跳过并计为 0。 -- **Qwen3-vLLM 分批策略**:`docs` 请求体可为 1000+,服务端会按 `infer_batch_size` 拆分;当 `sort_by_doc_length=true` 时,会先按文档长度排序后分批,减少 padding 开销,最终再按输入顺序回填分数。 -- 运行时可用环境变量临时覆盖批量参数:`RERANK_VLLM_INFER_BATCH_SIZE`、`RERANK_VLLM_SORT_BY_DOC_LENGTH`。 -- **Qwen3-vLLM**:参考 [Qwen3-Reranker-0.6B](https://huggingface.co/Qwen/Qwen3-Reranker-0.6B),需 GPU 与较多显存;与 BGE 相比适合长文本、高吞吐场景(vLLM 前缀缓存)。 -- **Qwen3-Transformers**:官方 Transformers Usage 方式,无需 vLLM;适合 CPU 或小显存。默认 `attn_implementation: "sdpa"`;若已安装 `flash_attn` 可设 `flash_attention_2`(未安装时服务会自动回退到 sdpa)。 -- **Qwen3-Transformers-Packed**:仍使用 Hugging Face Transformers 与 PyTorch CUDA 内核,只定制 packed 输入、`position_ids` 和 4D `attention_mask`。它更适合在线检索里的“一个 query 对几百个短 doc”场景;默认 `attn_implementation: "eager"` 以保证自定义 mask 兼容性,若你的 `torch/transformers` 版本已验证支持,可再压测 `"sdpa"`。 -- **Qwen3-GGUF**:参考 [DevQuasar/Qwen.Qwen3-Reranker-4B-GGUF](https://huggingface.co/DevQuasar/Qwen.Qwen3-Reranker-4B-GGUF)。单卡 T4 且仅剩约 `4.8~6GB` 显存时,推荐 `Q8_0 + n_ctx=384 + n_gpu_layers=24 + flash_attn=true + offload_kqv=true` 起步;若启动 OOM,优先把 `n_gpu_layers` 下调到 `20`,再把 `n_ctx` 下调到 `320`。`infer_batch_size` 在 GGUF 后端是服务侧 work chunk,大多不如 `n_gpu_layers` / `n_ctx` 关键。 -- **Qwen3-GGUF-0.6B**:参考 [ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF](https://huggingface.co/ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF)。它的优点是权重小、显存占用低,单进程实测约 `0.9~1.1 GiB`;但在当前 llama.cpp 串行打分接法下,`1 query + 400 titles` 的实测延迟仍约 `265s`。因此它更适合低显存功能后备,不适合作为在线低延迟主 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 归档 diff --git a/reranker/backends/qwen3_vllm_score.py b/reranker/backends/qwen3_vllm_score.py index 9438bc1..a8fe5cc 100644 --- a/reranker/backends/qwen3_vllm_score.py +++ b/reranker/backends/qwen3_vllm_score.py @@ -15,7 +15,6 @@ Reference: https://docs.vllm.ai/ — Qwen3 reranker example from __future__ import annotations import logging -import os import threading import time from typing import Any, Dict, List, Tuple @@ -41,89 +40,6 @@ _DEFAULT_DOCUMENT_TEMPLATE = ": {doc}{suffix}" _IM_USER_START = "<|im_end|>\n<|im_start|>user\n" -def _parse_env_bool(raw: str | None) -> bool | None: - if raw is None: - return None - s = str(raw).strip().lower() - if not s: - return None - if s in {"1", "true", "yes", "y", "on"}: - return True - if s in {"0", "false", "no", "n", "off"}: - return False - return None - - -def _auto_triton_on_sm_lt_8_enabled(config: Dict[str, Any]) -> bool: - """ - When True (default), sm < 8 injects TRITON_ATTN to avoid FA2-only paths that error on T4/V100. - - When False, vLLM may choose FLASHINFER on Turing; first ``score()`` can JIT-compile and needs - ``ninja`` on PATH (``requirements_reranker_qwen3_vllm_score.txt``). Use - ``./scripts/start_reranker.sh`` (prepends the backend venv's ``bin`` to ``PATH``) or - ``source .../bin/activate``. - """ - env = _parse_env_bool(os.getenv("RERANK_VLLM_AUTO_TRITON_ATTN")) - if env is not None: - return env - raw = config.get("auto_triton_attn_on_sm_lt_8") - if raw is None: - return True - if isinstance(raw, bool): - return raw - parsed = _parse_env_bool(str(raw)) - return True if parsed is None else parsed - - -def _resolve_vllm_attention_config(config: Dict[str, Any]) -> Dict[str, Any] | None: - """ - Optional explicit backend via vllm_attention_backend / RERANK_VLLM_ATTENTION_BACKEND. - - On compute capability < 8, vLLM may default to Flash-Attention 2, which is not supported on - Turing/Volta; this module historically injected TRITON_ATTN. That can be slower than vLLM's - other fallbacks — disable with auto_triton_attn_on_sm_lt_8: false or - RERANK_VLLM_AUTO_TRITON_ATTN=0 if your stack runs without errors. - """ - env = (os.getenv("RERANK_VLLM_ATTENTION_BACKEND") or "").strip() - raw = config.get("vllm_attention_backend") - if env: - choice = env - elif raw is not None and str(raw).strip() and str(raw).strip().lower() != "auto": - choice = str(raw).strip() - else: - choice = "" - if choice: - backend = choice.strip().upper() - if backend == "AUTO": - choice = "" - else: - logger.info("[Qwen3_VLLM_SCORE] attention_config.backend=%s (from config/env)", backend) - return {"backend": backend} - - major, minor = torch.cuda.get_device_capability() - if major < 8 and _auto_triton_on_sm_lt_8_enabled(config): - logger.info( - "[Qwen3_VLLM_SCORE] GPU compute capability %d.%d < 8.0; using attention backend " - "TRITON_ATTN (Flash-Attention 2 requires sm >= 80). " - "To use vLLM default instead: auto_triton_attn_on_sm_lt_8: false or " - "RERANK_VLLM_AUTO_TRITON_ATTN=0; or set vllm_attention_backend / " - "RERANK_VLLM_ATTENTION_BACKEND.", - major, - minor, - ) - return {"backend": "TRITON_ATTN"} - if major < 8 and not _auto_triton_on_sm_lt_8_enabled(config): - logger.info( - "[Qwen3_VLLM_SCORE] GPU compute capability %d.%d < 8.0; auto TRITON_ATTN disabled — " - "leaving attention backend to vLLM (no attention_config). " - "If the first score() fails on 'ninja', install ninja in the score venv, ensure " - "PATH includes that venv's bin (see start_reranker.sh), or use system ninja-build.", - major, - minor, - ) - return None - - class Qwen3VLLMScoreRerankerBackend: """ Qwen3 reranker using vLLM ``LLM.score()`` (pooling runner) for cross-encoder scores. @@ -149,7 +65,7 @@ class Qwen3VLLMScoreRerankerBackend: self._config.get("instruction") or "Given a query, score the product for relevance" ) - _fmt = str(self._config.get("instruction_format") or "standard").strip().lower() + _fmt = str(self._config.get("instruction_format") or "compact").strip().lower() if _fmt not in {"standard", "compact"}: raise ValueError( f"instruction_format must be 'standard' or 'compact', got {_fmt!r}" @@ -162,21 +78,11 @@ class Qwen3VLLMScoreRerankerBackend: self._config.get("document_template") or _DEFAULT_DOCUMENT_TEMPLATE ) - infer_batch_size = os.getenv("RERANK_VLLM_INFER_BATCH_SIZE") or self._config.get( - "infer_batch_size", 64 - ) - sort_by_doc_length = os.getenv("RERANK_VLLM_SORT_BY_DOC_LENGTH") - if sort_by_doc_length is None: - sort_by_doc_length = self._config.get("sort_by_doc_length", True) + infer_batch_size = self._config.get("infer_batch_size", 64) + sort_by_doc_length = self._config.get("sort_by_doc_length", True) self._infer_batch_size = int(infer_batch_size) - self._sort_by_doc_length = str(sort_by_doc_length).strip().lower() in { - "1", - "true", - "yes", - "y", - "on", - } + self._sort_by_doc_length = bool(sort_by_doc_length) if not torch.cuda.is_available(): raise RuntimeError( @@ -199,7 +105,7 @@ class Qwen3VLLMScoreRerankerBackend: logger.info( "[Qwen3_VLLM_SCORE] Loading model %s (LLM.score API, runner=%s, convert=%s, " "hf_overrides=%s, max_model_len=%s, tp=%s, gpu_mem=%.2f, dtype=%s, prefix_caching=%s, " - "instruction_format=%s)", + "instruction_format=%s, cuda_capability=%d.%d, attention_backend=vllm_auto)", model_name, runner, convert, @@ -210,6 +116,7 @@ class Qwen3VLLMScoreRerankerBackend: dtype, enable_prefix_caching, self._instruction_format, + *torch.cuda.get_device_capability(), ) # vLLM 0.17+ uses runner/convert instead of LLM(..., task="score"). With the official @@ -236,10 +143,6 @@ class Qwen3VLLMScoreRerankerBackend: if hf_overrides: llm_kwargs["hf_overrides"] = hf_overrides - attn_cfg = _resolve_vllm_attention_config(self._config) - if attn_cfg is not None: - llm_kwargs["attention_config"] = attn_cfg - self._llm = LLM(**llm_kwargs) # vLLM score path: single-process safety (mirrors generate backend until verified). self._infer_lock = threading.Lock() diff --git a/reranker/性能优化版本的qwen3_vllm_score 为什么反而更慢.md b/reranker/性能优化版本的qwen3_vllm_score 为什么反而更慢.md deleted file mode 100644 index 4d36e37..0000000 --- a/reranker/性能优化版本的qwen3_vllm_score 为什么反而更慢.md +++ /dev/null @@ -1,141 +0,0 @@ - -结论先说:**YAML 里能对齐的项(`model_name`、`max_model_len`、`infer_batch_size`、`prefix_caching` 等)你们已经基本对齐了**;`qwen3_vllm_score` 更慢,主要来自**两条后端走的不是同一条 vLLM 推理路径**,以及 **score 后端在 T4 上强制了 attention 后端**,和 **generate 路径更容易吃到「同 query、多 doc」的优化**。 - ---- - -## 1. 配置层面:哪些「对等」、哪些根本不存在于另一侧 - -两边共用的逻辑在代码里是一致的:`infer_batch_size`、`sort_by_doc_length`、去重、`instruction` / `instruction_format` 的语义(在各自实现里)是对齐设计的。 - -差异在于 **`qwen3_vllm_score` 必须多出来的 LLM 构造参数**:`runner` / `convert` / `hf_overrides`(把 Hub 模型改成 `Qwen3ForSequenceClassification` 那条链路)。`qwen3_vllm` 没有这些,因为它是**普通 causal LM + `generate`**。这不是 `config.yaml` 漏配,而是两种 API 的必要差别。 - -```132:140:reranker/backends/qwen3_vllm.py - self._llm = LLM( - model=model_name, - tensor_parallel_size=tensor_parallel_size, - max_model_len=max_model_len, - gpu_memory_utilization=gpu_memory_utilization, - enable_prefix_caching=enable_prefix_caching, - enforce_eager=enforce_eager, - dtype=dtype, - ) -``` - -```167:195:reranker/backends/qwen3_vllm_score.py - llm_kwargs: Dict[str, Any] = { - "model": model_name, - "runner": runner, - "convert": convert, - "tensor_parallel_size": tensor_parallel_size, - "max_model_len": max_model_len, - "gpu_memory_utilization": gpu_memory_utilization, - "enable_prefix_caching": enable_prefix_caching, - "enforce_eager": enforce_eager, - "dtype": dtype, - } - hf_overrides: Dict[str, Any] = dict(self._config.get("hf_overrides") or {}) - if use_hf_overrides: - hf_overrides = { - **hf_overrides, - "architectures": ["Qwen3ForSequenceClassification"], - "classifier_from_token": ["no", "yes"], - "is_original_qwen3_reranker": True, - } - if hf_overrides: - llm_kwargs["hf_overrides"] = hf_overrides - - attn_cfg = _resolve_vllm_attention_config(self._config) - if attn_cfg is not None: - llm_kwargs["attention_config"] = attn_cfg - - self._llm = LLM(**llm_kwargs) -``` - -**小坑(仅当有人删掉 YAML 字段时):** -`instruction_format` 的**代码默认值不一致**——`qwen3_vllm` 默认 `compact`,`qwen3_vllm_score` 默认 `standard`。你贴的片段里两边都写了 `standard`,所以当前是对齐的。 - -```93:98:reranker/backends/qwen3_vllm.py - _fmt = str(self._config.get("instruction_format") or "compact").strip().lower() -``` - -```104:109:reranker/backends/qwen3_vllm_score.py - _fmt = str(self._config.get("instruction_format") or "standard").strip().lower() -``` - ---- - -## 2. 为什么「按理 score 更快」在你们机器上反过来 - -你们自己的报告里写的是 **Tesla T4**(算力 **sm_75 < 8.0**)。这一点和代码里的行为直接相关。 - -### (1)只有 score 后端在 sm<8 时**强制** `TRITON_ATTN` - -```65:75:reranker/backends/qwen3_vllm_score.py - major, minor = torch.cuda.get_device_capability() - if major < 8: - logger.info( - "[Qwen3_VLLM_SCORE] GPU compute capability %d.%d < 8.0; using attention backend " - "TRITON_ATTN (Flash-Attention 2 requires sm >= 80). " - ... - ) - return {"backend": "TRITON_ATTN"} -``` - -`qwen3_vllm` **没有**这段逻辑,**不写** `attention_config`,完全交给 vLLM 在 **generate** 路径上自己选实现。 -因此在 T4 上很容易出现:**两条路径实际用的 attention / kernel 组合并不相同**;若默认路径比强制的 `TRITON_ATTN` 更适合你们的 batch 与序列长度,就会出现 **score 更慢**。 -若要验证,可在 score 的 YAML 里试 `vllm_attention_backend`(或与 `RERANK_VLLM_ATTENTION_BACKEND` 对齐到和 generate 实际一致的后端),或在 Ampere+ 上复测矩阵。 - -### (2)工作量与 vLLM 优化重心不同(这是主因之一) - -- **generate 后端**:`max_tokens=1`、`allowed_token_ids` 只有 yes/no,本质是 **prefill + 极短 decode**,且 logprobs 只关心最后一步的分布。 -- **score 后端**:`LLM.score()` 走 **pooling / cross-encoder 式**的打分图,是另一条 runner,**不等于**「比 1-token generate 一定更少算」;在 vLLM 里通常 **causal generate 路径打磨得更狠**。 - -所以「score API 更高级所以一定更快」在这个模型用法下**不一定成立**。 - -### (3)`enable_prefix_caching: true` 对两边的「可缓存前缀」不对称 - -同一 query、多个 doc 时,**generate** 路径用 chat template 拼出来的 prompt,**从 system 到 query 的长前缀在 batch 内完全相同**,很容易成为 prefix caching 的理想场景。 - -**score** 路径把内容拆成 `queries` / `documents` 两列交给 `score()`,内部如何切块、是否能把「同一 query 对应多 doc」映射成与 generate 同等强度的前缀复用,依赖 vLLM 实现;很多版本下 **generate + 共享前缀** 更占便宜。你们 `max_model_len: 160` 很短,prefill 成本敏感,**谁更吃到缓存**会明显拉开差距。 - -### (4)Tokenizer 侧:后者多了一步「批量模板」优化 - -`qwen3_vllm` 对整批 `apply_chat_template` 一次做完再 `generate`: - -```171:180:reranker/backends/qwen3_vllm.py - messages_batch = [ - self._format_messages(self._instruction, q, d) for q, d in pairs - ] - tokenized = self._tokenizer.apply_chat_template( - messages_batch, - tokenize=True, - add_generation_prompt=False, - enable_thinking=False, - ) -``` - -`qwen3_vllm_score` 在 Python 里逐对拼字符串,再进 `score()`(tokenization 在 vLLM 内)。这一项通常不是第一瓶颈,但在 **batch 大、序列短** 时也会有一点差别。 - -### (5)两个 venv 的 vLLM 版本不同 - -- `.venv-reranker`:`vllm>=0.8.5`(实际装的几版本会变) -- `.venv-reranker-score`:固定 `vllm==0.18.0` - -对比「谁更快」时,**版本 + 代码路径**是绑在一起的;不能假设「新 vLLM + score」在 T4 上一定赢过「旧 vLLM + 1-token generate」。 - ---- - -## 3. 和你们 `RESULTS.md` 的对应关系 - -`perf_reports/.../RESULTS.md` 里:**同一 `instruction_format` 下 `qwen3_vllm` 全程低于 `qwen3_vllm_score`**,与上面 **T4 + attention 强制 + 不同 runner + prefix cache 利用率** 的解释一致;报告里也写了在别的 GPU / vLLM 版本下排序可能变,这是合理的。 - ---- - -## 4. 若要「对齐实验」可以怎么做(方向性) - -1. **在 Ampere(A10/A100 等 sm≥80)上跑同一脚本**,看 score 是否反超(FlashAttention 路径更完整时,score 路径有时会更合理)。 -2. **在 score 侧显式设置 `vllm_attention_backend`**(或与 env 对齐),避免在 T4 上只有 score 被锁死 `TRITON_ATTN` 而 generate 走另一条。 -3. **固定两边 `pip show vllm` 版本**再比,否则「版本差」会污染结论。 -4. 用 vLLM 的 profiler / 日志确认 **prefix cache hit** 在两种后端上的差异(若你们要量化「缓存」这一条)。 - -**总结:** 不是 `config.yaml` 里少抄了几个键;而是 **推理图不同、T4 上 attention 策略不对称、以及 generate 对「同 query 多 doc」更友好**,导致在你们当前环境下 **`qwen3_vllm` 比 `qwen3_vllm_score` 更快是合理现象**,与「score API 理论上更干净」并不矛盾。 \ No newline at end of file diff --git a/scripts/smoke_qwen3_vllm_score_backend.py b/scripts/smoke_qwen3_vllm_score_backend.py index 16f2bea..02322ac 100644 --- a/scripts/smoke_qwen3_vllm_score_backend.py +++ b/scripts/smoke_qwen3_vllm_score_backend.py @@ -6,8 +6,9 @@ Usage (from repo root, score venv): PYTHONPATH=. ./.venv-reranker-score/bin/python scripts/smoke_qwen3_vllm_score_backend.py Same as production: vLLM child processes need the venv's ``bin`` on PATH (for pip's ``ninja`` when -using FLASHINFER). ``start_reranker.sh`` exports that; this script prepends ``sysconfig.get_path("scripts")`` -(the stdlib location for this environment's console scripts, independent of ``python`` symlink targets). +vLLM auto-selects FLASHINFER on T4/Turing). ``start_reranker.sh`` exports that; this script prepends +``sysconfig.get_path("scripts")`` (the stdlib location for this environment's console scripts, +independent of ``python`` symlink targets). """ from __future__ import annotations @@ -30,18 +31,12 @@ import torch from reranker.backends.qwen3_vllm_score import ( Qwen3VLLMScoreRerankerBackend, - _resolve_vllm_attention_config, ) def main() -> int: p = argparse.ArgumentParser() p.add_argument( - "--no-auto-triton", - action="store_true", - help="Set auto_triton_attn_on_sm_lt_8=False (match config opt-out)", - ) - p.add_argument( "--gpu-memory-utilization", type=float, default=0.12, @@ -66,14 +61,8 @@ def main() -> int: "enable_prefix_caching": False, "enforce_eager": True, "infer_batch_size": 4, - "instruction_format": "standard", + "instruction_format": "compact", } - if args.no_auto_triton: - cfg["auto_triton_attn_on_sm_lt_8"] = False - - attn = _resolve_vllm_attention_config(cfg) - print("attention_config:", attn) - print("Loading backend ...") backend = Qwen3VLLMScoreRerankerBackend(cfg) scores, meta = backend.score_with_meta("smoke query", ["title one", "title two"], normalize=False) diff --git a/scripts/start_reranker.sh b/scripts/start_reranker.sh index cbba309..f2fa19d 100755 --- a/scripts/start_reranker.sh +++ b/scripts/start_reranker.sh @@ -41,8 +41,8 @@ export TRITON_CACHE_DIR="${RERANKER_RUNTIME_DIR}/triton" export TORCHINDUCTOR_CACHE_DIR="${RERANKER_RUNTIME_DIR}/torch_compile" export TMPDIR="${RERANKER_RUNTIME_DIR}/tmp" export VLLM_NO_USAGE_STATS="${VLLM_NO_USAGE_STATS:-1}" -# venv bin must be on PATH before Python starts: vLLM worker inherits it; FlashInfer JIT needs -# pip-installed ninja when qwen3_vllm_score does not force TRITON_ATTN (e.g. T4 + auto_triton off). +# venv bin must be on PATH before Python starts: vLLM worker inherits it; on T4/Turing, +# qwen3_vllm_score now relies on vLLM auto-selecting FLASHINFER, whose JIT needs pip-installed ninja. export PATH="${RERANKER_VENV}/bin:${PATH}" if [[ "${RERANK_BACKEND}" == qwen3_gguf* ]]; then -- libgit2 0.21.2