vLLM GSM8K 精度评测实战:基于独立评测脚本与 OpenAI 兼容服务的正确性验证体系
【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm
本文介绍 vLLM 仓库中tests/evals/gsm8k/目录下的独立 GSM8K 精度评测体系:它作为 lm-eval-harness 的替代方案,通过「独立评测脚本 +vllm serve服务 + YAML 模型配置 + pytest 参数化」的组合,将模型数学推理能力验证变成可复现的 CI 回归测试。读完本文后,你可以掌握如何在本地手动运行 GSM8K 评测、如何编写一份新的模型评测配置,并理解评测脚本在 prompt 构造、答案抽取、评分指标与通过判定上的完整实现细节。
1. 定位与背景:为什么需要独立的 GSM8K 评测
GSM8K 是一个小学数学应用题数据集(test 集共 1319 题),5-shot 设置下的准确率是业界衡量 LLM 推理能力的常用回归指标。vLLM 的这套实现被明确定位为lm-eval-harness GSM8K 评测的替代品,其目标是在 CI 中对大量模型/量化/并行配置做「精度不回归」把关,同时获得更好的性能与更细的控制粒度(见 README)。
整个目录由三类文件构成:
- gsm8k_eval.py:核心评测脚本,既可通过命令行独立运行(对接
vllm serve的 HTTP 端点),也提供evaluate_gsm8k_offline()供进程内直接调用; - test_gsm8k_correctness.py 与 conftest.py:pytest 集成,负责按配置清单拉起
vllm serve服务、执行评测并按阈值断言; - configs/ 目录:数百份 YAML 模型配置及若干按硬件/场景分组的配置清单文件(
models-small.txt、models-h200.txt、models-blackwell.txt等),子目录 configs/humming/、configs/moe-refactor/、configs/moe-refactor-dp-ep/ 分别对应不同评测专项。
此外,test_gsm8k_offloading.py 复用了同一套evaluate_gsm8k()入口,用于验证 CPU KV offloading 连接器在回载 KV 数据后精度不下降,说明该脚本已成为 vLLM 多个正确性回归测试的公共评测底座。
2. 两种运行方式
2.1 pytest 方式(与 Buildkite CI 一致)
pytest -s -v tests/evals/gsm8k/test_gsm8k_correctness.py \ --config-list-file=configs/models-small.txt--config-list-file选项由 conftest.py 通过pytest_addoption注册,默认值就是configs/models-small.txt。pytest_generate_tests钩子会解析该清单文件(每行一个 YAML 文件名,#开头为注释),以清单文件所在目录为基准解析出各配置文件的绝对路径,并对config_filenamefixture 做参数化——即清单里有几份配置,就会生成几个测试用例,测试 id 取配置文件的 stem 名。
相对路径解析规则值得注意:conftest 先尝试「测试目录 + 相对路径」,失败后再尝试「当前工作目录 + 相对路径」(见 conftest.py 中pytest_generate_tests)。因此configs/models-small.txt既可以配合cd tests/evals/gsm8k后运行,也可以从仓库根目录写成tests/evals/gsm8k/configs/models-small.txt。
以 models-small.txt 为例,它当前包含 8 份小模型/多量化配置:
Qwen3-0.6B-FP8.yaml Llama-3.2-1B-Instruct-INT8-CT.yaml Llama-3-8B-Instruct-nonuniform-CT.yaml Qwen2.5-VL-3B-Instruct-FP8-dynamic.yaml Qwen1.5-MoE-W4A16-CT.yaml DeepSeek-V2-Lite-Instruct-FP8.yaml Qwen3-30B-A3B-MXFP4A16.yaml gemma-4-E4B-it-qat-mobile-ct.yaml2.2 独立脚本方式(手动评测)
先启动一个 vLLM 服务,再直接运行评测脚本:
# 先启动 vLLM 服务 vllm serve Qwen/Qwen2.5-1.5B-Instruct --port 8000 # 运行评测 python tests/evals/gsm8k/gsm8k_eval.py --port 8000gsm8k_eval.py的完整命令行参数(来自 gsm8k_eval.py 中main()的 argparse 定义)如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
--num-shots | 5 | few-shot 示例数量(取自 GSM8K train 集前 N 条) |
--num-questions | 1319 | 评测题目数量(1319 为 test 集全量,会自动min截断) |
--max-tokens | 256 | 每题最大生成 token 数 |
--host | http://127.0.0.1 | 服务地址 |
--port | 8000 | 服务端口 |
--temperature | 0.0 | 采样温度,默认贪心解码 |
--seed | 42 | 随机种子,保证可复现 |
--max-concurrency | 无(不限流) | 通过aiohttp.TCPConnector(limit=...)限制并发请求数 |
--save-results | 无 | 将结果 dict 以 JSON 落盘 |
评测输出包含 Accuracy、Invalid responses、总时延、Questions/s、总输出 token 数与 Output tokens/s 六项指标,可整体保存为 JSON 便于归档对比。
3. 评测配置 YAML 字段详解
README 给出的基础格式如下:
model_name: "Qwen/Qwen2.5-1.5B-Instruct" accuracy_threshold: 0.54 # 最低期望精度 num_questions: 1319 # 题目数(默认:test 集全量) num_fewshot: 5 # 来自 train 集的 few-shot 示例数 server_args: "--max-model-len 4096 --tensor-parallel-size 2 --moe-backend flashinfer_cutlass" # 服务启动参数 env: # 环境变量(可选) VLLM_LOGGING_LEVEL: "DEBUG"结合 test_gsm8k_correctness.py 中对eval_config的全部解析,完整字段清单比 README 示例更丰富:
| 字段 | 必填 | 默认值/来源 | 用途 |
|---|---|---|---|
model_name | 是 | — | 传给vllm serve的模型 ID(HF 仓库名或本地量化模型路径) |
accuracy_threshold | 是 | — | GSM8K 精度阈值,低于threshold - tolerance则断言失败 |
num_questions | 是 | 1319 | 评测题数 |
num_fewshot | 是 | 5 | few-shot 数 |
server_args | 否 | "" | 任意vllm serve参数,用shlex.split解析以支持引号 |
env | 否 | {} | dict,注入到服务进程的环境变量 |
tolerance | 否 | 0.08 | 精度容差,实际判定式为measured >= threshold - tolerance |
max_tokens | 否 | 256 | 每题最大生成 token 数 |
temperature | 否 | 0.0 | 采样温度 |
seed | 否 | 42 | 随机种子 |
use_chat_completions | 否 | False | True 时走/v1/chat/completions而非/v1/completions(面向 instruction-tuned 模型) |
gen_prefix | 否 | "" | 拼接在Answer:之后的生成前缀 |
max_concurrency | 否 | 不限 | 并发请求上限 |
request_timeout_seconds | 否 | 600 | 单请求超时 |
rocm_request_timeout_seconds | 否 | — | ROCm 平台专用的超时覆盖值 |
startup_max_wait_seconds | 否 | 1200 | 服务启动最大等待,同时注入VLLM_ENGINE_READY_TIMEOUT_S |
min_acceptance_length | 否 | 无 | 投机解码配置的额外断言:draft 平均接受长度下限 |
配置示例可以取自仓库真实文件。Qwen3-0.6B-FP8.yaml 是一份典型的最小配置:
model_name: "Qwen/Qwen3-0.6B-FP8" accuracy_threshold: 0.375 num_questions: 1319 num_fewshot: 5 server_args: "--enforce-eager --max-model-len 4096"而 Qwen3.5-397B-A17B-NVFP4-DEP2-MTP.yaml 展示了大模型 + 数据并行 + MTP 投机解码的复杂写法(YAML 折叠标量>-可让server_args跨多行):
model_name: "nvidia/Qwen3.5-397B-A17B-NVFP4" accuracy_threshold: 0.88 tolerance: 0.03 num_questions: 1319 num_fewshot: 5 max_tokens: 12000 server_args: >- --max-model-len 16384 --data-parallel-size 2 --enable-expert-parallel --max-num-seqs 256 --spec-method mtp --spec-tokens 3use_chat_completions: true则出现在 chat 类模型的配置中,例如 Laguna-XS.2-NVFP4.yaml 与 DiffusionGemma-26B-A4B-it-FP8-dynamic.yaml;max_concurrency: 100出现在 GLM-5.2-NVFP4-TP2-PCP2-EP.yaml 中。
4. 评测脚本核心实现解析(gsm8k_eval.py)
4.1 数据集加载与 prompt 构造
评测数据并不打包在仓库中,load_gsm8k_data()(gsm8k_eval.py L47-L58)从VLLM_S3_BUCKET_URL/ci-datasets/gsm8k/下载train.jsonl与test.jsonl,并缓存在系统临时目录(tempfile.gettempdir()下按 URL 末段命名),已存在则直接复用,重复运行无需再下载。
_build_gsm8k_prompts()(L149-L177)完成 few-shot prompt 拼装:
- 从 train 集取前
num_shots条,按Question: ...\nAnswer:{gen_prefix} ...\n\n格式拼接为前缀; - 遍历 test 集前
num_questions题,追加Question: {题目}\nAnswer:{gen_prefix}; - 用
get_answer_value()提取标准答案数值,并assert所有 label 均非INVALID(-9999999),保证题目本身可判分。
注意gen_prefix的用途:对某些 chat 模板会把Assistant:前缀写进 chat template 的模型,可在生成侧补一个前缀避免重复输出。
4.2 答案抽取与判分
get_answer_value()(L69-L78)的判分逻辑:去掉逗号后用regex找全部数字串,取最后一个数字做ast.literal_eval;抽不到数字则记为INVALID。这与 lm-eval-harness 的extract_last_number思路一致,invalid_rate指标即统计抽取失败的比例,用于区分「答错」与「格式崩坏」。
_score_gsm8k()(L180-L207)汇总的结果 dict 包含:accuracy(逐题数值相等判定后的均值)、invalid_rate、latency(整批墙钟时延)、questions_per_second、total_output_tokens(来自响应usage.completion_tokens累加)、tokens_per_second、num_questions、num_shots、max_tokens与timestamp。
4.3 异步批量请求
evaluate_gsm8k()(L210-L286)内部用asyncio+aiohttp:为每道题建一个协程,经tqdm.gather并发执行;stop 序列固定为["Question", "Assistant:", "<|separator|>"],防止模型把后续题目继续生成出来。默认对/v1/completions发原始 prompt;配置use_chat_completions后改走/v1/chat/completions(此时必须提供model参数,见 test_gsm8k_correctness.py 中run_gsm8k_eval的传参)。单请求超时由aiohttp.ClientTimeout(total=request_timeout_seconds)控制,max_concurrency非空时通过TCPConnector(limit=...)限流。
脚本还保留了evaluate_gsm8k_offline()(L289-L337):对进程内的vllm.LLM对象走llm.generate()/llm.chat()(后者面向 instruction-tuned 模型,支持透传chat_template_kwargs),prompt 构造与判分逻辑与在线版完全一致——这正是 test_gsm8k_offloading.py 直接复用evaluate_gsm8k的原因。
5. pytest 集成流程:从配置到断言
test_gsm8k_correctness(config_filename)(test_gsm8k_correctness.py L91 起)对每份 YAML 的执行链路如下:
- 平台适配跳过:若干配置依赖特定内核或平台(如 MXFP4A16 的 Marlin 内核仅 CUDA、GFX950 上的 AITER 量化、ROCm 上的 DeepSeek 大模型因 agent 磁盘/驱逐问题),命中即在非目标平台
pytest.skip。 - 组装服务参数:
shlex.split(server_args)解析 YAML 中的服务参数,再统一追加--trust-remote-code --disable-uvicorn-access-log。 - 启动服务:用
RemoteOpenAIServer(来自 tests/utils.py)以配置里的envdict 拉起vllm serve,其中框架自动注入VLLM_ENGINE_READY_TIMEOUT_S = startup_max_wait_seconds(默认 1200s)。 - 执行评测:
run_gsm8k_eval()从服务 URL 拆出 host/port 后调用evaluate_gsm8k();ROCm 平台优先取rocm_request_timeout_seconds覆盖超时(例如 DeepSeek-V2-Lite-Instruct-FP8.yaml 将 ROCm 超时放到 1800s)。 - 精度断言:核心判定式为
measured_metric >= expected_metric - tol,tol默认 0.08(可在配置中收窄,如 MTP 配置用 0.03)。 - 投机解码附加断言:若配置了
min_acceptance_length,测试会请求服务的/metrics,解析vllm:spec_decode_num_drafts_total与vllm:spec_decode_num_accepted_tokens_total,计算平均接受长度1 + accepted/drafts(1.0 意味着所有 draft 全被拒绝,投机解码没有收益;理论上限为1 + num_speculative_tokens),并断言其不低于下限——这防止「精度达标但投机解码完全失效」的静默回归。
6. 实践指南:为我的模型添加一份 GSM8K 配置
基于以上实现,新增一份配置的操作步骤为:
- 在 configs/ 下新建
<模型名>-<量化/并行方式>.yaml,必填四要素:model_name、accuracy_threshold、num_questions、num_fewshot; - 先手动跑一遍标定基线:
vllm serve <模型> <你的server_args>+python tests/evals/gsm8k/gsm8k_eval.py --port 8000 --save-results result.json,用实测精度留出余量后设定accuracy_threshold(阈值语义是「允许最多tolerance的回退」); - chat 模板敏感、completion prompt 表现异常的模型,加
use_chat_completions: true;生成链很长的大模型按需上调max_tokens(参考 MTP 配置的 12000); - 把文件名追加进对应的清单文件(如
models-small.txt、models-h200.txt、models-blackwell.txt,或 humming/moe-refactor 专项清单的config-*.txt),即可被 CI 的pytest --config-list-file=...自动覆盖; - 若服务冷启动慢,调大
startup_max_wait_seconds;若单请求经常超 600s,显式设置request_timeout_seconds(ROCm 用rocm_request_timeout_seconds)。
从源码结构看,该目录的评测模式(YAML 配置驱动 + 服务化评测 + 阈值容差断言)已被 test_gsm8k_offloading.py 二次复用为 KV offloading 的回归护栏:它用同一evaluate_gsm8k()连跑两轮 GSM8K,中间通过/reset_prefix_cache丢弃 GPU 前缀缓存而保留 CPU 缓存,迫使第二轮从 CPU 回载 KV,以此验证回载路径不产生静默数据损坏。这也侧面说明了把评测逻辑收敛到独立脚本而非绑定某一测试框架带来的复用价值。
适用前提与限制
- 评测需要能访问
VLLM_S3_BUCKET_URL指定的 S3 桶以下数据集文件,首次运行会下载并缓存到系统临时目录; - pytest 模式会真实拉起
vllm serve子进程,需要本机具备配置中server_args要求的 GPU 数量(如--tensor-parallel-size 2需要 2 卡)与模型权重下载能力; num_questions会按 test 集长度截断,当前实现下该上限即 1319;- 精度阈值是针对特定模型/量化组合的经验值,跨模型不可直接借用,新增配置务必先标定基线。
【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考