Blame view

reranker/README.md 17.1 KB
42e3aea6   tangwang   tidy
1
2
  # Reranker 模块
  
5c21a485   tangwang   qwen3-reranker-0....
3
  **请求示例**`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`
42e3aea6   tangwang   tidy
4
5
  
  ---
d90e7428   tangwang   补充重排
6
  
3d508beb   tangwang   reranker-4b-gguf
7
  Reranker 服务提供统一的 `/rerank` API,支持可插拔后端(BGE、Qwen3-vLLM、Qwen3-Transformers、Qwen3-GGUF、DashScope 云重排)。调用方通过 HTTP 访问,不关心具体后端。
701ae503   tangwang   docs
8
  
b0972ff9   tangwang   qwen3_vllm_score ...
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  ## 当前结论
  
  在当前项目的线上形态里,**首选后端是 `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` | 云服务方案 | 运维简单,但依赖外部服务和网络 |
701ae503   tangwang   docs
40
41
  
  ## 目录与入口
b0972ff9   tangwang   qwen3_vllm_score ...
42
  
701ae503   tangwang   docs
43
44
45
  - `reranker/server.py`:FastAPI 服务,启动时按配置加载一个后端
  - `reranker/backends/`:后端实现与工厂
    - `backends/__init__.py`:`get_rerank_backend(name, config)`
b0972ff9   tangwang   qwen3_vllm_score ...
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
    - `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 也按这个基线描述:
701ae503   tangwang   docs
167
168
169
170
  
  ```yaml
  services:
    rerank:
b0972ff9   tangwang   qwen3_vllm_score ...
171
      backend: "qwen3_vllm_score"
701ae503   tangwang   docs
172
      backends:
b0972ff9   tangwang   qwen3_vllm_score ...
173
        qwen3_vllm_score:
701ae503   tangwang   docs
174
          model_name: "Qwen/Qwen3-Reranker-0.6B"
b0972ff9   tangwang   qwen3_vllm_score ...
175
176
          use_original_qwen3_hf_overrides: true
          engine: "vllm"
9f5994b4   tangwang   reranker
177
          max_model_len: 256
b0972ff9   tangwang   qwen3_vllm_score ...
178
179
180
          tensor_parallel_size: 1
          gpu_memory_utilization: 0.20
          dtype: "float16"
9f5994b4   tangwang   reranker
181
182
          enable_prefix_caching: true
          enforce_eager: false
b0972ff9   tangwang   qwen3_vllm_score ...
183
          infer_batch_size: 100
4823f463   tangwang   qwen3_vllm_score ...
184
          sort_by_doc_length: true
b0972ff9   tangwang   qwen3_vllm_score ...
185
          instruction_format: standard
3d508beb   tangwang   reranker-4b-gguf
186
          instruction: "Rank products by query with category & style match prioritized"
b0972ff9   tangwang   qwen3_vllm_score ...
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
  ```
  
  ### 优点
  
  - 当前本地 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
3d508beb   tangwang   reranker-4b-gguf
256
          sort_by_doc_length: true
b0972ff9   tangwang   qwen3_vllm_score ...
257
          instruction_format: standard
5c21a485   tangwang   qwen3-reranker-0....
258
          instruction: "Rank products by query with category & style match prioritized"
701ae503   tangwang   docs
259
260
  ```
  
b0972ff9   tangwang   qwen3_vllm_score ...
261
  ### 优点
d31c7f65   tangwang   补充云服务reranker
262
  
b0972ff9   tangwang   qwen3_vllm_score ...
263
264
265
  - 路径成熟,易理解,易排障
  - 作为 fallback 很合适
  -`qwen3_vllm_score` 共用很多服务层优化经验
d31c7f65   tangwang   补充云服务reranker
266
  
b0972ff9   tangwang   qwen3_vllm_score ...
267
  ### 缺点
701ae503   tangwang   docs
268
  
b0972ff9   tangwang   qwen3_vllm_score ...
269
270
271
  - 不是原生 reranker score 路径
  -`qwen3_vllm_score` 多一层 tokenizer / generate / logprob 推导成本
  - 当前环境下性能略逊
d90e7428   tangwang   补充重排
272
  
b0972ff9   tangwang   qwen3_vllm_score ...
273
  ## 这轮优化里真正有价值的方法
9f5994b4   tangwang   reranker
274
  
b0972ff9   tangwang   qwen3_vllm_score ...
275
  下面这些是跨后端都值得保留的经验,优先级高于 prompt 微调。
d90e7428   tangwang   补充重排
276
  
b0972ff9   tangwang   qwen3_vllm_score ...
277
  ### 1. 全局去重
d90e7428   tangwang   补充重排
278
  
b0972ff9   tangwang   qwen3_vllm_score ...
279
  对 doc 先做全局去重,再按原始索引回填,是收益最高、风险最低的优化之一。
d31c7f65   tangwang   补充云服务reranker
280
  
b0972ff9   tangwang   qwen3_vllm_score ...
281
  适用原因:
d90e7428   tangwang   补充重排
282
  
b0972ff9   tangwang   qwen3_vllm_score ...
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
  - 商品标题、变体标题、重复 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 最优落地”,建议直接遵循下面这条线:
d90e7428   tangwang   补充重排
431
  
b0972ff9   tangwang   qwen3_vllm_score ...
432
433
434
435
436
437
438
  - 主后端:`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 归档