用 SGLang benchmark/uno 复现 UNO 全量数学评测:run_math_eval 实战与源码解析
【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang
本篇指南以 benchmark/uno/README.md 为核心,完整讲解 SGLang 仓库中全量数学评测工具run_math_eval.py的用法与实现:它如何在同一套数据集、提示词、采样参数与评分规则下,公平对比自回归(AR)解码与 UNO、DFLASH、EAGLE、EAGLE3 等投机解码算法的数学准确率与吞吐指标(TPF、tok/s)。读完本文,你将能够在自己的 H200 单卡环境上逐行复现 PR 中的评测表,理解 UNO 线性/树两种模式的参数映射、TPF 统计口径,以及 SGLang 侧 UNO 实现的约束边界。
工具定位:统一的投机解码数学评测框架
benchmark/uno/run_math_eval.py的核心设计目标是在完全一致的条件下横向比较不同解码算法。根据 README 与 run_math_eval.py 的实现,该评测框架具备以下特征:
- 统一评测口径:AR、UNO、DFLASH、EAGLE、EAGLE3 使用相同的数据集、提示词、采样参数和评分器,只通过
--speculative-algorithm切换解码路径。 - 进程内引擎:创建进程内的
sgl.Engine,不启动独立 server 进程。从源码可见,评测脚本在 _generate 中直接调用engine.generate(input_ids=..., sampling_params=..., rid=...),结束后在finally中调用engine.shutdown()。 - 计时边界明确:引擎启动耗时不计入计时区间(
perf_counter()在sgl.Engine构造之后才启动),并且不做任何额外的请求预热(warmup)。 - 数据集固定版本:下载固定 revision(commit hash)的 GSM8K、MATH-500、AIME 2024、AIME 2025、AIME 2026,保证跨机器、跨时间可复现。
- 统一提示与模板:对所有引擎施加相同的
\boxed{}作答指令与 Qwen reasoning 聊天模板。 - 统一评分:基于
math_verify的 parse/verify 进行数学答案比对。
环境准备与安装
README 要求以可编辑模式安装带评测依赖的 SGLang:
pip install -e "python[test]"[test]可选依赖中包含了评测运行所需的math_verify等库。math_grader.py 在导入math_verify失败时会优雅降级(parse = None、verify = None),以保证--help仍可用;但真正执行评分时会抛出RuntimeError("math scoring requires the 'math_verify' package"),提示必须安装该包。
数据集、提示词与模板:math_data.py 深度解读
数据集准备逻辑集中在 math_data.py。它定义了五个基准及其固定的 revision:
| 基准名 | 数据集与 split | 固定 revision | 期望行数 | 提示词构造 |
|---|---|---|---|---|
gsm8k | openai/gsm8k(main, test) | 740312add88f781978c0658806c59bc2815b9866 | 1319 | Q: {question}\nA: Let's think step by step. |
math500 | HuggingFaceH4/MATH-500(test) | 6e4ed1a2a79af7d8630a6b768ec859cb5af4d3be | 500 | 直接使用problem字段 |
aime24 | hypaai/Hypa_AIME2024(english) | 11ab79f0eed5f4fdf3d469b466663ab86bbd77c8 | 30 | Question: {problem}\nAnswer: |
aime25 | math-ai/aime25(test) | 563bb8404243c5f09de6ec262f2db674fe5bce9b | 30 | Question: {problem}\nAnswer: |
aime26 | math-ai/aime26(test) | 79037aebdb6580008fb960d17cb21fd3099083e3 | 30 | Question: {problem}\nAnswer: |
所有基准共享同一条作答指令(math_data.py):
Please reason step by step and put your final answer in \boxed{}.同时所有基准都使用chat_template_kwargs={"reasoning_effort": "high"}调用 Qwen reasoning 聊天模板(Qwen3 系列的 reasoning 模式)。这条指令会以 system 消息形式注入,若消息中已存在 system 消息则拼接到其前部(见 _format_prompt)。
prepare_benchmark_data的缓存与校验机制值得一提:数据以{benchmark}.jsonl落在--data-root下;若文件已存在且行数与期望一致则直接复用,否则重建;行数不符会抛ValueError;写入采用"临时文件 + replace"的原子方式,避免半成品污染缓存。
复现 H200 评测表:完整命令逐行解读
README 的核心是可复现脚本。首先设置环境变量与公共参数(均从仓库根目录执行):
export MODEL_PATH=Qwen/Qwen3-8B export TOKENIZER_PATH=Qwen/Qwen3-8B export UNO_LORA_PATH=s-sahoo/uno-qwen3-8B export DATA_ROOT=/path/to/math-eval-data export RESULT_ROOT=/path/to/math-eval-results COMMON_ARGS=( --model-path "$MODEL_PATH" --tokenizer-path "$TOKENIZER_PATH" --data-root "$DATA_ROOT" --context-length 40960 --max-tokens 32768 --temperature 1 --top-k 50 --top-p 0.95 --random-seed 42 )COMMON_ARGS中的数值正是 run_math_eval.py 的默认值(CONTEXT_LENGTH = 40960、MAX_TOKENS = 2**15 = 32768)以及采样默认值(temperature=1.0、top-k=50、top-p=0.95、random-seed=42)。PYTHONPATH=python前缀保证从仓库根目录能导入benchmark.uno包。
定义三个启动函数:AR 基线、UNO 线性模式、UNO 树模式。
run_ar() { local benchmark=$1 samples=$2 requests=$3 output_name=$4 PYTHONPATH=python python -m benchmark.uno.run_math_eval \ "${COMMON_ARGS[@]}" \ --benchmark "$benchmark" \ --num-samples "$samples" \ --max-running-requests "$requests" \ --output-dir "$RESULT_ROOT/$output_name" } run_linear_uno() { local benchmark=$1 samples=$2 requests=$3 output_name=$4 PYTHONPATH=python python -m benchmark.uno.run_math_eval \ "${COMMON_ARGS[@]}" \ --benchmark "$benchmark" \ --num-samples "$samples" \ --max-running-requests "$requests" \ --output-dir "$RESULT_ROOT/$output_name" \ --speculative-algorithm UNO \ --uno-lora-path "$UNO_LORA_PATH" \ --speculative-num-steps 1 \ --speculative-eagle-topk 1 \ --speculative-num-draft-tokens 8 } run_tree_uno() { local benchmark=$1 samples=$2 requests=$3 output_name=$4 PYTHONPATH=python python -m benchmark.uno.run_math_eval \ "${COMMON_ARGS[@]}" \ --benchmark "$benchmark" \ --num-samples "$samples" \ --max-running-requests "$requests" \ --output-dir "$RESULT_ROOT/$output_name" \ --speculative-algorithm UNO \ --uno-lora-path "$UNO_LORA_PATH" \ --speculative-num-steps 15 \ --speculative-eagle-topk 32 \ --speculative-num-draft-tokens 32 }批次 64 的 AR 与 UNO 线性(B/K/V = 8/1/8)六行(GSM8K、MATH-500 每题 1 个样本;AIME 2025 每题 10 个样本,共 300 条 completion):
run_ar gsm8k 1 64 ar-gsm8k-c64 run_linear_uno gsm8k 1 64 uno-linear-b8-k1-v8-gsm8k-c64 run_ar math500 1 64 ar-math500-c64 run_linear_uno math500 1 64 uno-linear-b8-k1-v8-math500-c64 run_ar aime25 10 64 ar-aime25-c64 run_linear_uno aime25 10 64 uno-linear-b8-k1-v8-aime25-c64批次 1 的 AR 与 UNO 树(B/K/V = 16/32/32)六行:
run_ar gsm8k 1 1 ar-gsm8k-c1 run_tree_uno gsm8k 1 1 uno-tree-b16-k32-v32-gsm8k-c1 run_ar math500 1 1 ar-math500-c1 run_tree_uno math500 1 1 uno-tree-b16-k32-v32-math500-c1 run_ar aime25 10 1 ar-aime25-c1 run_tree_uno aime25 10 1 uno-tree-b16-k32-v32-aime25-c1注意 UNO 树模式的参数映射:--speculative-num-steps 15意味着草稿前向宽度B = 15 + 1 = 16,--speculative-eagle-topk 32即树宽K = 32,--speculative-num-draft-tokens 32即验证宽度V = 32(详见下文"B/K/V 参数映射")。
run_math_eval 全参数详解
完整的命令行参数定义见 parse_args:
| 参数 | 默认值 | 说明 |
|---|---|---|
--model-path | Qwen/Qwen3-8B | 目标模型路径或 HF repo id |
--tokenizer-path | 缺省取--model-path | tokenizer 路径,支持独立指定 |
--revision | 无 | 目标模型权重 revision |
--dtype | bfloat16 | 模型精度 |
--attention-backend | fa3 | 注意力后端(UNO 强制要求 fa3) |
--data-root | 必填 | 数据集缓存/下载目录 |
--output-dir | 必填 | 结果输出目录 |
--benchmark | 全部五个 | 可重复指定,如--benchmark gsm8k --benchmark math500;缺省跑全部 |
--limit | 无 | 每个基准最多执行的题目数 |
--num-samples | 1 | 每题采样次数(AIME 表复现用 10) |
--max-running-requests | 4 | 引擎并发上限(表复现用 64 或 1) |
--context-length | 40960 | 上下文长度 |
--max-tokens | 32768 | 每题最大生成 token 数 |
--temperature | 1.0 | 采样温度 |
--top-k | 50 | top-k 采样 |
--top-p | 0.95 | nucleus 采样 |
--random-seed | 42 | 随机种子 |
--speculative-algorithm | 无 | EAGLE/EAGLE3/DFLASH/UNO(自动转大写) |
--speculative-draft-model-path | 无 | 草稿模型路径(UNO 禁止使用) |
--speculative-draft-model-revision | 无 | 草稿模型 revision |
--speculative-num-steps | 无 | 草稿步数 |
--speculative-eagle-topk | 无 | EAGLE 系候选数 |
--speculative-num-draft-tokens | 无 | 草稿/验证 token 数 |
--speculative-dflash-block-size | 无 | DFLASH 专用:验证窗口长度 |
--speculative-draft-attention-backend | 无 | 草稿注意力后端 |
--uno-lora-path | 无 | UNO 专用:草稿 LoRA 检查点路径 |
参数校验(_validate_args):num_samples、max_running_requests、context_length、max_tokens、top_k必须为正数;top_p必须落在(0, 1];temperature必须非负;limit若指定必须为正。
上下文预留(context reserve)(_context_reserve)是一个容易踩坑的细节:投机解码的草稿/验证会额外占用上下文位置,脚本会按算法预留:DFLASH 预留2 * block_size;UNO 树模式(topk > 1)预留max(draft_tokens, steps+1) + 1;其余按draft_tokens预留。随后逐条检查context_length - prompt_len - reserve >= max_tokens,不满足即报错并提示"increase--context-length"。这解释了为什么表复现必须把 context length 开到 40960、max-tokens 开到 32768——既要容纳超长 reasoning 输出,又要给投机窗口留出余量。
运行流程与结果产物
main 的执行管线为:
- 解析参数并校验;
- 加载 tokenizer:使用
transformers.AutoTokenizer(use_fast=True、trust_remote_code=True); - 准备提示词:对每个基准下载/复用数据 → 逐行套用统一指令与 Qwen reasoning 模板渲染(
_format_prompt返回 token ids 与文本)→ 按--num-samples复制出{source}:sample{i}的 prompt id; - 构造引擎并生成:
_engine_options中设置了skip_tokenizer_init=True(tokenizer 已由脚本预加载)、log_level="info"、max_running_requests,并将所有投机相关选项原样透传给引擎(run_math_eval.py);_generate在引擎构造完成后才开始计时; - 统计 TPF:见下节;
- 写结果:按基准分目录输出。
每个基准的--output-dir/<benchmark>/下会生成四类文件:
| 文件 | 内容 |
|---|---|
generations.jsonl | 原始生成结果(含output_ids、解码文本、token 数、num_forwards、tokens_per_forward、sglang_meta_info) |
grades.jsonl | 逐条评分后的结果(含parsed_generations、correct、accuracy) |
scores.json | 评分汇总(num_rows、num_problems、accuracy、单样本的avg_at_1/pass_at_1,多样本的pass_at_N) |
summary.json/summary.md | 跨基准汇总 |
summary.md的表格格式固定为(run_math_eval.py):
| Dataset | Accuracy | TPF | tok/s | tok/s/request |末行Average的 Accuracy 与 TPF 是各数据集的无加权平均;tok/s 是总输出 token 数除以计时生成秒数;tok/s/request 再除以max_running_requests。summary.json则记录了完整元数据(engine=sglang-offline、mode、模型路径、投机参数、采样参数、generation_seconds、num_tokens、num_forwards、tokens_per_forward、unweighted_mean_tokens_per_forward、by_benchmark等),并在进程结束前打印到 stdout。
TPF 统计口径:三种算法的差异
TPF(tokens per forward,每次前向生成的 token 数)是衡量投机解码效率的关键指标,脚本在 _build_rows 中按算法区别统计:
- AR 基线:
num_forwards = len(token_ids),即每生成一个 token 记一次前向,TPF 恒为 1; - UNO:
num_forwards = 2 * spec_verify_ct。因为 UNO 每个投机周期的两次前向都是完整的目标模型前向——扩散路径(diffusion-pathway)的草稿前向 + AR 路径(AR-pathway)的验证前向,两者各算一次; - EAGLE / EAGLE3 / DFLASH:
num_forwards = spec_verify_ct,遵循 SGLang 的 acceptance-length 约定,只统计目标模型验证前向(草稿模型的前向不计入)。
spec_verify_ct来自引擎输出的meta_info(OpenAI 协议中对应的字段定义见 protocol.py)。需要特别留意:由于口径不同,UNO 的 TPF 与 EAGLE/DFLASH 的 TPF不可直接数值对比,只能在同一算法内部比较配置优劣。
评分器实现:math_grader.py
评分模块 math_grader.py 自称"Minimal math scorer adapted from Nano-vLLM-UNO's Eval360 grader"。其核心处理链路:
- 答案抽取:
extract_last_boxed_content用正则 + 花括号配对深度扫描提取最后一个\boxed{...}/\fbox{...}的内容; - 文本规范化:
normalize_answer_text做大量 LaTeX 清洗——提取 GSM8K 的####后答案、\dfrac/\tfrac归一为\frac、去除\left/\right、\mathbf、\text/\mathrm包装、\frac{a}{b}转a/b、去掉空白与千分位逗号等; - 候选解析:
_parse_boxed_content优先交给math_verify.parse(带$包裹尝试),纯变量答案保留文本,数字答案提取前导数字;无 boxed 时走_parse_unboxed_answer的十余种自然语言模式("The answer is ..."、"Final answer: ..."、"Therefore, the answer is ..." 等),最后兜底取最后一个句子的最后一个数字; - 比对:
_compare_answers优先math_verify.verify(answer, gold),失败则再解析 gold 反向 verify;文本型答案走_text_answers_match(含浮点相等、(A)括号选项、向量分量比对)。
score_math同时输出多套口径:avg_at_1/pass_at_1(首样本),以及多样本时的avg_at_N/pass_at_N(任一采样正确即算通过)。AIME 2025 的 10 采样配置正是通过num_samples=10触发了后一套统计。
评测其他投机解码器:DFLASH 与 EAGLE/EAGLE3
README 强调:"The runner uses the same public option names assglang serve",即评测脚本与在线服务共享同一套投机参数命名,结论可直接迁移到线上部署。
DFLASH 示例(草稿模型 + block size + 草稿注意力后端):
PYTHONPATH=python python -m benchmark.uno.run_math_eval \ "${COMMON_ARGS[@]}" \ --benchmark math500 \ --num-samples 1 \ --output-dir "$RESULT_ROOT/dflash-b8-math500-c64" \ --max-running-requests 64 \ --speculative-algorithm DFLASH \ --speculative-draft-model-path z-lab/Qwen3-8B-DFlash-b16 \ --speculative-dflash-block-size 8 \ --speculative-draft-attention-backend fa3EAGLE / EAGLE3 示例:
export EAGLE_DRAFT_MODEL=/path/to/compatible-eagle-draft-model PYTHONPATH=python python -m benchmark.uno.run_math_eval \ "${COMMON_ARGS[@]}" \ --benchmark math500 \ --num-samples 1 \ --output-dir "$RESULT_ROOT/eagle3-b8-math500-c64" \ --max-running-requests 64 \ --speculative-algorithm EAGLE3 \ --speculative-draft-model-path "$EAGLE_DRAFT_MODEL" \ --speculative-num-steps 7 \ --speculative-eagle-topk 1 \ --speculative-num-draft-tokens 8EAGLE 与 DFLASH 的 TPF 遵循上文所述的 acceptance-length 约定。
UNO 原理与源码印证
单模型双前向架构
SGLang 中的 UNO 实现是 UnoWorkerV2("Single-model UNO worker with linear and native-EAGLE tree decode")。与 EAGLE/DFLASH 需要加载独立草稿模型不同,UNO复用目标 transformer 完成每个投机周期的两次前向:
- 草稿前向:每个请求的第一行使用基础权重,其余
B - 1行使用训练好的 UNO LoRA("diffusion-pathway draft"); - 验证前向:完全使用基础权重("AR-pathway verification")。
因此 spec.py 中 UNO 的启动参数是--speculative-algorithm UNO+--uno-lora-path(如s-sahoo/uno-qwen3-8B),而--speculative-draft-model-path会被显式拒绝。这也解释了为什么 UNO 的每次周期成本是两次完整目标模型前向——评测脚本中2 * spec_verify_ct的 TPF 口径正是对该架构的忠实刻画。
B/K/V 参数映射
UNO 的三种形态参数B/K/V含义为:草稿前向宽度 / 每层扩展保留的候选数 / 验证宽度。命令行映射由 uno_worker_v2.py 与 speculative_hook.py 共同决定:
| 模式 | 判定 | B | K | V |
|---|---|---|---|---|
| 线性(Linear) | --speculative-eagle-topk ≤ 1 | --speculative-num-draft-tokens(如 8) | 1(steps/topk 被强制为 1) | --speculative-num-draft-tokens(8) |
| 树(Tree) | --speculative-eagle-topk > 1 | --speculative-num-steps + 1(如 15+1=16) | --speculative-eagle-topk(32) | --speculative-num-draft-tokens(32) |
线性模式构建并验证单条草稿链(类似 DFLASH 的 proposal 布局);树模式在每层草稿深度扩展多个候选,并复用 SGLang 的 EAGLE 树验证路径(native-EAGLE tree verification)。注意 UNO 的K控制的是提议树宽度,与请求采样参数top_k无关。
启动期硬性约束
_handle_uno 是 UNO 配置的"守门员",任何违规配置都会在启动期直接抛ValueError,而不是拖到解码时才崩溃:
- 仅支持 CUDA,且要求 prefill 与 decode 注意力后端均为FA3;
- 必须提供
--uno-lora-path,禁止--speculative-draft-model-path; - 要求TP=PP=1,不支持 DP attention 与上下文并行(CP);
- 不支持确定性推理(
--enable-deterministic-inference)、strict thinking(依赖 grammar decoding)、公共 Multi-LoRA 服务(--uno-lora-path加载的是固定内部 adapter); - 不使用
--speculative-use-rejection-sampling(UNO 自管随机验证); - 树模式额外校验:
V ≥ B、V ≤ 128、V × K ≤ 2048、提议树容量与 EAGLE parent-list ABI 可表示性、两个接受阈值必须为 1.0、暂不支持 PDMux 与 two-batch overlap; - 混合分块 prefill(mixed chunked prefill)会被自动禁用(给出 warning 而非报错)。
以上约束在 speculative_decoding.mdx 的 "Key requirements and limitations" 一节亦有完整列举,可作为部署前核对清单。
测试验证
仓库内的测试进一步印证了实现细节:
- test_uno.py 是端到端 CUDA-graph 测试:在
Qwen/Qwen3-8B+s-sahoo/uno-qwen3-8B上同时覆盖线性与树两种模式,用 32 token 的贪心前缀校验与 AR 解码的输出一致性(PARITY_TOKENS = 32),并显式定义FORWARDS_PER_UNO_CYCLE = 2——与 TPF 双前向口径完全对应; - test_uno_tree_config.py 验证了启动期校验(确定性推理、strict thinking、非法树形配置均被拒绝);
- 另有 test_uno_request_validation.py、test_uno_lora_targets.py 覆盖请求级校验与 UNO LoRA 目标层校验(不支持的 LoRA 目标层会在启动期被拒绝)。
常见问题与注意事项
- 从仓库根目录运行:命令均以
PYTHONPATH=python python -m benchmark.uno.run_math_eval形式执行,不要在benchmark/uno/目录内直接python run_math_eval.py。 --data-root与--output-dir必须显式指定,且应使用不同目录,避免数据缓存与结果互相覆盖。- 上下文余量不足:若某题的可用生成空间小于
--max-tokens,脚本会报错并提示增大--context-length;投机解码会额外占用上下文(见_context_reserve)。 - TPF 跨算法不可直接比较:UNO 计两次完整前向,EAGLE/DFLASH 只计验证前向,AR 恒为 1;比较时应关注同一算法内的相对提升,或同时参考 tok/s 与准确率。
- UNO 适配器必须与目标模型检查点精确匹配:README 与官方文档均强调使用为特定 checkpoint 训练的 adapter(当前已验证
Qwen/Qwen3-8B),LoRA 目标层不匹配会在启动期报错。 - 评分依赖
math_verify:未安装时 CLI 帮助可用,但评分会报错;请使用pip install -e "python[test]"安装完整评测依赖。
【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考