性能优化版本的qwen3_vllm_score 为什么反而更慢.md 7.66 KB

结论先说:YAML 里能对齐的项(model_namemax_model_leninfer_batch_sizeprefix_caching 等)你们已经基本对齐了qwen3_vllm_score 更慢,主要来自两条后端走的不是同一条 vLLM 推理路径,以及 score 后端在 T4 上强制了 attention 后端,和 generate 路径更容易吃到「同 query、多 doc」的优化


1. 配置层面:哪些「对等」、哪些根本不存在于另一侧

两边共用的逻辑在代码里是一致的:infer_batch_sizesort_by_doc_length、去重、instruction / instruction_format 的语义(在各自实现里)是对齐设计的。

差异在于 qwen3_vllm_score 必须多出来的 LLM 构造参数runner / convert / hf_overrides(把 Hub 模型改成 Qwen3ForSequenceClassification 那条链路)。qwen3_vllm 没有这些,因为它是普通 causal LM + generate。这不是 config.yaml 漏配,而是两种 API 的必要差别。

        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,
        )
        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 默认 compactqwen3_vllm_score 默认 standard。你贴的片段里两边都写了 standard,所以当前是对齐的。

        _fmt = str(self._config.get("instruction_format") or "compact").strip().lower()
        _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

    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=1allowed_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

        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-rerankervllm>=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_formatqwen3_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_vllmqwen3_vllm_score 更快是合理现象,与「score API 理论上更干净」并不矛盾。