跳到主要内容

数据截至 (上游 commit dd417662e5bd)

LM Evaluation Harness — 架构与原理

30 秒导读: 这是 EleutherAI 的大语言模型评测框架——Hugging Face Open LLM Leaderboard 的后端,学术界引用最多的 benchmark 数字很多是它跑出来的。它回答一个问题:一个 benchmark 分数到底是怎么算出来的? 答案是:每个任务一个 YAML(prompt 格式 + few-shot + 打分规则),每道题变成「算 loglikelihood」或「生成直到停止符」的请求,模型后端批量执行,再抽答案、算指标、聚合。它评的是模型本身,不是 agent——没有环境、没有工具调用、没有多轮交互。


1. 这是什么(零基础也能懂)

一句话定义

lm-evaluation-harness(命令行叫 lm-eval)是一个给语言模型跑标准学术 benchmark 的框架:你给它一个模型(本地权重或 API)和一组任务名(如 arc_easymmlugsm8k),它输出每个任务的分数和每张卷子的逐题明细。

它解决谁的什么问题

设想你想知道「我的模型 MMLU 多少分」。听起来简单,做起来处处是坑:

  • MMLU 的 prompt 怎么写?选项写成 A. xxx 还是 (A) xxx?
  • few-shot 用几道题?从哪个 split 抽?随机还是固定前 5 道?
  • 怎么算「答对」?让模型自由生成再匹配,还是直接比四个选项的概率?
  • 生成的答案文本怎么抽取出「B」这个选项?正则写错了分数就差几个点。

每个选择都会让分数动几个百分点。没有统一的 harness,两个论文里的「MMLU 分数」根本不可比。 这个库的价值就是把所有这些选择固定成公开可查的 YAML 配置,让「分数」变成「某模型 + 某 prompt 配置 + 某 harness 版本」的函数。

它能做什么

能力说明
任务库几百个任务的 YAML 定义(lm_eval/tasks/ 下 200+ 个子目录),覆盖 MMLU、ARC、HellaSwag、GSM8K、HumanEval 等
四种评测方式loglikelihood(给答案算概率)、multiple_choice(选项比概率)、loglikelihood_rolling(整文 perplexity)、generate_until(自由生成)
模型后端HuggingFace transformers、vLLM、SGLang、OpenAI/Anthropic 等 API、llama.cpp(GGUF)、NeMo 等 30+ 种(lm_eval/models/)
few-shot每个任务可配 shot 数、采样策略(随机 / 固定前 N 个)、是否走 chat 模板
可复现固定种子、记录每个任务的 YAML 配置与版本号、缓存请求与响应
分布式HF 后端支持多 GPU 数据并行(accelerate),vLLM 支持张量并行

用起来什么样

一条命令(摘自 lm_eval/_cli/run.py 的帮助文本):

lm-eval run --model hf \
--model_args pretrained=EleutherAI/pythia-1b \
--tasks arc_easy,gsm8k \
--num_fewshot 5 --batch_size auto

也可以当库用,入口是 simple_evaluate(lm_eval/evaluator.py:55):

# 示意,非源码(参数名来自真实签名)
import lm_eval
results = lm_eval.simple_evaluate(
model="hf",
model_args="pretrained=EleutherAI/pythia-1b",
tasks=["arc_easy"],
num_fewshot=25,
)
# results["results"]["arc_easy"]["acc,none"] 就是 ARC-Easy 准确率

返回的 results 是一个大字典,键包括:results(任务分数)、groups(组分数)、configs(每个任务的完整 YAML 配置)、versions(任务版本号)、n-shothigher_is_bettern-samples——组装逻辑在 _to_eval_results(lm_eval/evaluator_utils.py:134)。配置和版本号跟分数一起落盘,这是它可复现承诺的落点。

一句话直觉

把它想成「高考的标准化阅卷系统」。 每个 benchmark 是一份试卷(YAML 规定了题干怎么誊写、给不给例题、怎么判卷),模型是考生,框架负责按同一套规则发卷、收卷、判分。换一套阅卷规则,分数就不可比——所以它把阅卷规则公开成配置,钉死在版本号上。


2. 顶层全景(它大概怎么转)

2.1 一张图看全貌

从左到右是一次评测的生命周期:配置 → 请求 → 执行 → 判分 → 聚合。

lm-eval run(命令行/ Python API)


① TaskManager
扫 tasks/**/*.yaml
建 Task / Group 对象


② Task.build_all_requests
doc → few-shot 上下文
→ Instance 列表


③ evaluate() 按请求类型分组
loglikelihood │ generate_until │ rolling


④ LM 后端批量执行
HFLM │ VLLM │ OpenAI API │ ...


⑤ filter 抽答案
+ Task.process_results 逐题判分


⑥ 聚合:任务级 mean / perplexity
+ bootstrap stderr + group 汇总

2.2 部件职责

部件干什么在哪个文件
TaskManager扫描任务目录建索引,按名字/路径/dict 加载任务与组lm_eval/tasks/manager.py:37
TaskIndex / TaskFactory遍历 YAML 建 {名字: Entry};把 Entry 造成 Task/Grouplm_eval/tasks/_index.py:36lm_eval/tasks/_factory.py:25
Task / ConfigurableTask一个 benchmark:doc → prompt → Instance → 逐题指标lm_eval/api/task.py:64:618
TaskConfigYAML 解析出的任务配置(prompt 模板、split、指标表……)lm_eval/config/task.py:82
Instance一条待执行请求:类型 + 参数 + 回收响应的容器lm_eval/api/instance.py:11
LM / TemplateLM模型后端抽象:loglikelihood / loglikelihood_rolling / generate_untillm_eval/api/model.py:25:331
HFLM / VLLM最常用两个本地后端的实现lm_eval/models/huggingface.py:62lm_eval/models/vllm_causallms.py:59
Collator请求排序、按组分批、执行后还原原顺序lm_eval/models/utils.py:238
FilterEnsemble生成文本的后处理流水线(正则抽答案、取第一条……)lm_eval/api/filter.py:34
evaluate()主循环:建请求 → 执行 → 过滤 → 判分 → 聚合lm_eval/evaluator.py:429
指标注册表metric / aggregation / higher_is_better 三件套lm_eval/api/registry.py:418-457

2.3 主线走一遍(以 ARC-Easy 为例)

① 用户说 --tasks arc_easyTaskManager.load 查到 lm_eval/tasks/arc/arc_easy.yaml,造成一个 ConfigurableTask(lm_eval/tasks/_factory.py:81)。YAML 里写着:output_type: multiple_choicedoc_to_text: "Question: {{question}}\nAnswer:"metric_list: [acc, acc_norm]

evaluate()task.build_all_requests()(lm_eval/api/task.py:268):对每道题,先用 fewshot_context 拼出「描述 + N 个例题 + 本题题干」,再调 construct_requests(lm_eval/api/task.py:1362)。因为是 multiple_choice,每个选项生成一条 loglikelihood 请求:(上下文, " B")(上下文, " C")…… 一条题扇出成 4 条请求。

③ 所有任务的 Instance 按 request_type 归堆(lm_eval/evaluator.py:564-566),同类型的请求一次性发给模型:resps = getattr(lm, reqtype)(cloned_reqs)(lm_eval/evaluator.py:600)。

HFLM.loglikelihood 把每对 (context, continuation) 分词,按长度降序排序分批,一次前向拿回每条 continuation 的 log 概率和「贪心解码是否会产出它」(lm_eval/models/huggingface.py:1329)。

⑤ 响应回到 Instance 后,task.process_results(lm_eval/api/task.py:1489)把 4 个选项的 loglikelihood 取 argmax,和 gold 比,得到 acc;除以字符长度再取 argmax,得到 acc_norm

_process_results(lm_eval/evaluator_utils.py:349)对每个指标套聚合函数(acc 就是 mean)并算 bootstrap 标准误;若任务属于 group(如 mmlu),自底向上再加权汇总。

这条线最值得记住的一点:选择题从头到尾没有「生成」。 模型只是给每个选项算了次概率——这就是为什么 harness 分数可复现:解码随机性被整个消掉了(见第 3 章)。


3. 阅读地图(建议顺序)

五章由浅入深。只想抓要害读 01 → 03 就够:01 讲清「任务是什么」,03 讲清「分数怎么算出来」。

顺序章节讲什么适合谁
101-task-registry.mdYAML 任务注册表:include 继承、!function 逃生舱、group/tag、TaskFactory 建造所有人;想加自己任务的人必读
202-task-fewshot.mddoc_to_text 的四种形态、few-shot 采样器、Message 列表拼 prompt、chat 模板想知道「模型到底看到什么」的人
303-requests-execution.md四种 output_type 的语义与适用面、Instance 扇出与回收、filter 流水线、逐题判分想搞懂分数语义的人(核心章)
404-model-backends.mdLM 契约、_encode_pair 的空格处理、HFLM 批处理与缓存、vLLM/API 后端、CachingLM要接新后端或抠性能的人
505-evaluator-aggregation.mdevaluate() 全流程、分布式切分与 padding、指标注册表、stderr、group 聚合、结果 schema要改框架本身、或在乎统计严谨性的人

4. 巧妙之处(可借鉴的技术)

每条先白话点题,细节在各章。

  1. 选择题不比生成、比概率。 multiple_choice 扇出成 N 条 loglikelihood 请求,argmax 即答案(lm_eval/api/task.py:1374-1390)。贪心解码、温度、停止符这些噪声源整个消失,分数变成纯函数。还顺手给出 acc_norm(按长度归一,修正「短答案天然概率高」的偏差)。→ 第 3 章

  2. 互信息校正只要再加 N 条空上下文请求。 配了 acc_mutual_info 就追加 ("", choice) 请求,用 log P(choice|ctx) − log P(choice) 打分(lm_eval/api/task.py:1393-1405)。实现成本极低,因为请求模型本来就是个列表。→ 第 3 章

  3. 长度降序排序 = OOM 前移 + 自适应 batch 的前提。 _collate 返回 -len(toks),最长的请求永远排在最前——OOM 在第一 batch 就炸,而不是跑到 90% 才炸;batch 内 padding 长度也可预测(lm_eval/models/huggingface.py:1338-1348 注释里写明了这三点理由)。→ 第 4 章

  4. 单 token 续写缓存。 选择题的四个选项常常只差最后一个 token;group_by="contexts"context + cont[:-1] 相同的请求分一组,只跑一次前向,同组共享 logits(lm_eval/models/utils.py:238lm_eval/models/huggingface.py:1350-1357)。选项越多省得越多。→ 第 4 章

  5. YAML include + !function:配置复用与逃生舱并存。 MMLU 57 个子任务共享一份模板(_default_template_yaml),每个只写 4 行差异;模板表达不了的,!function utils.py 里的函数名 直接注入 Python(lm_eval/tasks/_yaml_loader.py:93)。→ 第 1 章

  6. 指标是「passthrough + 聚合函数」两段式。 逐题值原样攒进列表,perplexity 之类的换算全推到聚合端(math.exp(-mean(items)),lm_eval/api/metrics.py:46-48)。stderr、group 加权都在同一处统一处理。→ 第 5 章

  7. 响应缓存按请求参数哈希。 CachingLM 用 sqlite 存 (方法名, 参数) → 响应,重跑同配置秒回;但 do_sample=True 的请求故意不缓存——否则 repeats>1 时每次采样会返回同一条「随机」结果(lm_eval/api/model.py:285-294)。→ 第 4 章


5. 边界与局限

诚实清单——它的设计取舍意味着:

  • 分数只在同一 harness 口径下可比。 prompt 模板、target 前有没有空格(target_delimiter)、few-shot 样本,每一项都能动几个点。它解决的是「同一框架内可比」,跨框架(或与论文手工评测)比数字要非常小心。
  • 评的是模型,不是 agent。 没有环境、工具调用、多轮轨迹;「模型在 agent 场景下表现如何」是它刻意不回答的问题。多轮/工具场景评测看书架上的 inspect-ai 等兄弟源。
  • few-shot 样本默认随机抽。 种子固定所以可复现,但换 num_fewshot 就换一批样本;MMLU 这类任务改用 first_n 采样器(固定前 5 道)就是为了可比性(lm_eval/api/samplers.py:104)。
  • generate_until 任务的天花板在 filter。 分数依赖正则从自由文本里抽答案(如 GSM8K 的 #### (\-?[0-9\.\,]+)),模型换个格式输出就可能抽不到——分数低不一定是模型不会。
  • loglikelihood 类任务对 chat 模型水土不服。 必须显式开 apply_chat_template;enable_thinking=True 直接与 loglikelihood 任务互斥,框架会报错(lm_eval/models/huggingface.py:1216-1224)。
  • acc_norm 按字符数归一,不是 token 数。 completion_len = len(choice) 是 Python 字符串长度(lm_eval/api/task.py:1494),跨语言/跨分词器时这个「norm」的含义会变(另有按 UTF-8 字节数的 acc_bytes)。
  • 多模态是原型态。 README 自己标注 multimodal 是 in-progress,严肃多模态评测出门左转 lmms-eval(同门 fork)。

6. 横向对比

最大的分野:评模型 vs 评 agent。 本库评「模型这张卷考多少分」——请求是无状态的 loglikelihood/生成,判分是确定性规则。而 inspect-aiopenai-evals 评的是「系统在一串交互里干得怎么样」——有环境、有工具、有 solver/scorer 的编排。两类问题不要混用工具:给 base model 跑 MMLU 用本库;给 agent 跑 SWE-bench 用它们。docs 总览的 eval-observability 章 目前主要覆盖 agent 侧那条线,本篇正好补上模型侧。

同一问题(模型 benchmark)内的不同取舍:

维度lm-evaluation-harnesslighteval
出身EleutherAI,2019 年起,学术引用最多HuggingFace,leaderboard 后起的重写
任务定义YAML 为主 + !function 注入 PythonPython 代码定义任务
请求模型四种 output_type,Instance 扇出类似(loglikelihood / generative)
生态重心社区任务最多,兼容历史口径与 HF 生态(dataclasses、nanotron)更紧

olmo 的关系:OLMo 是「训练栈 + 评测一体」的研究仓库,其评测大量借助本类 harness 跑;要研究「训练里怎么用评测」看 olmo,要研究「评测本身怎么算」看本篇。


7. 代码地图(入口级)

每章末尾有细粒度地图,这里只列从零读源码的五个入口:

主题文件路径符号名
命令行 / 库入口lm_eval/evaluator.pysimple_evaluateevaluate
任务发现与建造lm_eval/tasks/manager.pyTaskManager.loadTaskIndex.buildTaskFactory.build
一个任务的全部行为lm_eval/api/task.pyConfigurableTask.fewshot_contextconstruct_requestsprocess_results
模型后端契约lm_eval/api/model.pyLM.loglikelihoodTemplateLM._encode_pairCachingLM
最常用的后端实现lm_eval/models/huggingface.pyHFLM._loglikelihood_tokensgenerate_until