结论先说:**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 理论上更干净」并不矛盾。