执行摘要
- 一句话:修复 custom dataset 与 --skip-tokenizer-init 的兼容性
- 推荐动作:建议合入。修复明确,测试充分。类型签名调整值得关注,但已在合理范围内。
功能与动机
PR #39896 在 serve.py 的 main_async 中添加了全局 assert tokenizer is not None,导致使用 --skip-tokenizer-init 配合 --dataset-name custom 的基准测试(如 Prithvi-EO 地理空间模型)直接抛出 AssertionError。
实现拆解
- 移除全局断言(
vllm/benchmarks/serve.py):删除 main_async 中的 assert tokenizer is not None,并将 calculate_metrics 和 benchmark 的 tokenizer 类型改为 TokenizerLike | None。
- 按需断言(
vllm/benchmarks/datasets/datasets.py):将 get_samples 的 tokenizer 参数类型放宽为 TokenizerLike | None。在依赖 tokenizer 的数据集分支(sonnet、hf、timed_trace 及通用 else)中添加针对性 assert tokenizer is not None;对 custom、custom_image、custom_audio 分支不做要求。
- 测试覆盖(
tests/benchmarks/test_skip_tokenizer_init.py):新增回归测试,直接调用 main_async() 并 mock 网络请求,验证 --skip-tokenizer-init + --dataset-name custom 不再抛出 AssertionError。
关键文件:
vllm/benchmarks/datasets/datasets.py(模块 数据集加载;类别 source;类型 core-logic;符号 get_samples): 核心修改:将 get_samples 的 tokenizer 参数改为可空,并在需要 tokenizer 的分支添加针对性断言。
vllm/benchmarks/serve.py(模块 主流程;类别 source;类型 core-logic): 移除全局断言,修改 calculate_metrics 和 benchmark 的类型签名以允许 tokenizer 为 None。
tests/benchmarks/test_skip_tokenizer_init.py(模块 测试覆盖;类别 test;类型 test-coverage;符号 _write_dataset, _args, test_main_async_skip_tokenizer_init_does_not_raise): 新测试文件,提供回归验证,确保修复后可正常工作。
关键符号:get_samples, main_async, calculate_metrics, benchmark
关键源码片段
vllm/benchmarks/datasets/datasets.py
核心修改:将 get_samples 的 tokenizer 参数改为可空,并在需要 tokenizer 的分支添加针对性断言。
def get_samples(args, tokenizer: TokenizerLike | None) -> list[SampleRequest]:
# 初始化一些默认值
if not hasattr(args, "request_id_prefix"):
args.request_id_prefix = ""
if hasattr(args, "random_range_ratio") and isinstance(args.random_range_ratio, str):
args.random_range_ratio = _parse_range_ratio(args.random_range_ratio)
# Custom 数据集:sample() 内部处理了 tokenizer=None 情形
if args.dataset_name == "custom":
dataset = CustomDataset(dataset_path=args.dataset_path, ...)
input_requests = dataset.sample(tokenizer=tokenizer, ...)
elif args.dataset_name == "custom_image":
... # 类似,不强制 tokenizer
elif args.dataset_name == "custom_audio":
... # 类似,不强制 tokenizer
elif args.dataset_name == "sonnet":
# Sonnet 数据集需要 tokenizer 进行 chat template 格式化
assert tokenizer is not None, "Tokenizer must be initialized for the 'sonnet' dataset."
sonnet_dataset = SonnetDataset(...)
input_requests = sonnet_dataset.sample(tokenizer=tokenizer, ...)
elif args.dataset_name == "hf":
assert tokenizer is not None, "Tokenizer must be initialized for the 'hf' dataset."
...
elif args.dataset_name == "timed_trace":
assert tokenizer is not None, "Tokenizer must be initialized for the 'timed_trace' dataset."
...
else:
# 其他数据集也要求 tokenizer
assert tokenizer is not None, f"Tokenizer must be initialized for the '{args.dataset_name}' dataset."
dataset_mapping = {...}
...
return input_requests
tests/benchmarks/test_skip_tokenizer_init.py
新测试文件,提供回归验证,确保修复后可正常工作。
import argparse, asyncio, json
from pathlib import Path
from unittest.mock import AsyncMock, patch
import pytest
import vllm.benchmarks.serve as serve_module
# 模拟 Prithvi-EO 数据集的 prompt
_PRITHVI_PROMPT = {
"data": {
"data": "https://.../India_900498_S2Hand.tif",
"data_format": "url",
"out_data_format": "b64_json",
"indices": [1, 2, 3, 8, 11, 12],
},
"priority": 0,
"softmax": False,
}
def _write_dataset(path: Path) -> None:
# 写入一行 JSON 作为数据集
path.write_text(json.dumps({"prompt": _PRITHVI_PROMPT}) + "\n")
def _args(dataset_path: str) -> argparse.Namespace:
# 构造与 --skip-tokenizer-init 真实调用等价的 Namespace
return argparse.Namespace(
dataset_name="custom",
dataset_path=dataset_path,
tokenizer=None,
skip_tokenizer_init=True,
backend="vllm-pooling",
# ... 其他参数省略,但保持与生产代码一致 ...
)
@pytest.mark.benchmark
def test_main_async_skip_tokenizer_init_does_not_raise(tmp_path: Path) -> None:
# 回归测试:确保不会抛出 AssertionError
dataset_path = tmp_path / "dataset.jsonl"
_write_dataset(dataset_path)
args = _args(str(dataset_path))
# Mock benchmark 避免真实网络请求
with patch.object(serve_module, "benchmark", AsyncMock(return_value={})):
asyncio.run(serve_module.main_async(args))
评论区精华
无实质 review 讨论。作者在 Issue 评论中说明 CI 失败的 4 个测试与本 PR 无关,属于环境问题(No module named 'vllm._C')。审核者 DarkLight1337 直接批准。
- CI 测试失败原因 (testing): 确认 CI 失败由环境问题引起,非本 PR 引入。
风险与影响
- 风险:风险低:修改仅限于 benchmark 命令行工具,不影响核心推理。需注意
tokenizer: TokenizerLike | None 类型变更可能影响外部直接调用者,但 benchmark 模块通常不对外暴露。测试覆盖了关键回归路径,各依赖 tokenizer 的分支仍保持原有断言,行为不变。
- 影响:修复了回归,恢复无 tokenizer 模型(如 Prithvi-EO)的基准测试能力。对已有使用 sonnet/hf 等数据集的行为无影响。影响范围限于使用
--skip-tokenizer-init 的 benchmark 用户。
- 风险标记:回归修复, 类型签名变更
关联脉络
- PR #39896 [MyPy] Fix mypy for vllm/benchmarks: 该 PR 引入的全局断言导致本 PR 修复的回归问题。
参与讨论