01 模块总览
lm-evaluation-harness 的代码组织围绕评测器(evaluator)、任务(task)、模型(model)三大核心展开, 并通过一套注册表(registry)系统将它们松耦合地连接在一起。整个项目的模块关系可以用下图概括:
CLI 入口"] end subgraph "评测引擎" B["evaluator.py
simple_evaluate() / evaluate()"] end subgraph "核心 API" C["api/task.py
Task / ConfigurableTask"] D["api/model.py
LM 基类"] E["api/registry.py
注册表"] F["api/instance.py
Instance 数据类"] end subgraph "任务定义" G["tasks/__init__.py
include_path / get_task_dict"] H["tasks/pretrain/xxx/*.yaml
+ utils.py"] end subgraph "模型实现" I["models/vllm_norm.py
VLLM 推理后端"] end A -->|调用| B B -->|遍历任务| C B -->|调用推理| D B -->|查注册表| E C -->|创建| F G -->|注册任务| E H -->|YAML 解析| G H -->|驱动| C I -->|继承 LM| D I -->|@register_model| E
各模块的核心职责如下:
| 模块 | 职责 |
|---|---|
lm_eval/__main__.py |
CLI 入口,解析命令行参数后调用 simple_evaluate() |
evaluator.py |
评测引擎核心,包含 simple_evaluate() 和 evaluate() 两个主函数,负责构建请求、调度推理、聚合指标 |
api/task.py |
定义 Task 抽象基类和 ConfigurableTask(YAML 驱动的任务类),以及 TaskConfig 数据类 |
api/model.py |
定义 LM 抽象基类,声明三个核心推理方法和工厂方法 |
api/registry.py |
全局注册表:MODEL_REGISTRY、TASK_REGISTRY、GROUP_REGISTRY、METRIC_REGISTRY 等 |
api/instance.py |
定义 Instance 数据类,封装单个评测请求 |
tasks/__init__.py |
任务扫描与注册:include_path()、get_task_dict() |
models/vllm_norm.py |
基于 vLLM 的模型实现,继承 LM 并通过 @register_model 注册 |
02 核心抽象类
2.1 LM -- 模型基类
LM(定义于 api/model.py)是所有模型后端必须继承的抽象基类。
它定义了三个抽象方法,分别对应三种不同的推理范式:
class LM(abc.ABC):
def __init__(self) -> None:
self._rank = 0
self._world_size = 1
self.cache_hook = CacheHook(None)
@abc.abstractmethod
def loglikelihood(self, requests) -> List[Tuple[float, bool]]:
"""计算 continuation 在 context 条件下的对数似然。
返回 (logprob, is_greedy) 元组列表。"""
pass
@abc.abstractmethod
def loglikelihood_rolling(self, requests) -> List[Tuple[float, bool]]:
"""计算完整字符串的对数似然(无截断),用于 perplexity 计算。"""
pass
@abc.abstractmethod
def generate_until(self, requests) -> List[str]:
"""给定 context,贪心生成直到遇到 stopping sequence。"""
pass
loglikelihood 用于选择题/打分类任务,
loglikelihood_rolling 用于 perplexity 评估,generate_until 用于开放式生成任务。
此外,LM 还提供了一个工厂方法 create_from_arg_string(),
用于从命令行参数字符串(如 "pretrained=meta-llama/Llama-2-7b,dtype=float16")创建模型实例:
@classmethod
def create_from_arg_string(
cls: Type[T], arg_string: str, additional_config: Optional[dict] = None
) -> T:
additional_config = {} if additional_config is None else additional_config
args = utils.simple_parse_args_string(arg_string)
args2 = {k: v for k, v in additional_config.items() if v is not None}
return cls(**args, **args2)
rank 和 world_size 属性用于分布式推理场景。默认值为单进程(rank=0、world_size=1),
在使用多 GPU 时由子类覆盖。
2.2 Task -- 任务基类
Task(定义于 api/task.py)是所有评测任务的抽象基类。
它定义了从数据加载到结果计算的完整生命周期。核心抽象方法包括:
class Task(abc.ABC):
VERSION = None
DATASET_PATH: str = None
DATASET_NAME: str = None
OUTPUT_TYPE: str = None
@abc.abstractmethod
def doc_to_text(self, doc):
"""将一条数据样本转换为模型输入文本(prompt)"""
pass
@abc.abstractmethod
def doc_to_target(self, doc):
"""将一条数据样本转换为目标答案"""
pass
@abc.abstractmethod
def construct_requests(self, doc, ctx, **kwargs):
"""根据文档和上下文构建 Instance 请求"""
pass
@abc.abstractmethod
def process_results(self, doc, results):
"""处理模型输出,返回指标 dict"""
pass
@abc.abstractmethod
def aggregation(self):
"""返回 {metric_name: aggregation_fn} 映射"""
pass
@abc.abstractmethod
def higher_is_better(self):
"""返回 {metric_name: bool} 映射"""
pass
Task 基类还实现了两个关键的非抽象方法:
build_all_requests() -- 遍历数据集中的每条文档,调用 fewshot_context() 构建 few-shot 上下文,
再调用 construct_requests() 创建 Instance 对象:
def build_all_requests(self, limit=None, rank=None, world_size=None) -> None:
"""Build a set of Instances for a task, and store them in task.instances"""
if self.has_test_docs():
docs = self.test_docs()
elif self.has_validation_docs():
docs = self.validation_docs()
instances = []
for doc_id, doc in utils.create_iterator(
enumerate(docs), rank, world_size, limit
):
fewshot_ctx = self.fewshot_context(
doc,
0 if self.config.num_fewshot is None else self.config.num_fewshot,
)
# 添加 prefix / suffix / chatml_prefix / chatml_suffix
fewshot_ctx["text"] = self.config.prefix + fewshot_ctx["text"] + self.config.suffix
fewshot_ctx["text"] = self.config.chatml_prefix + fewshot_ctx["text"] + self.config.chatml_suffix
inst = self.construct_requests(
doc=doc, ctx=fewshot_ctx,
metadata=(self.config["task"], doc_id, self.config.repeats),
)
instances.extend(inst if isinstance(inst, list) else [inst])
self._instances = instances
fewshot_context() -- 从训练集中随机采样 num_fewshot 条样本,
拼接成 "description + few-shot examples + current question" 格式的 prompt:
def fewshot_context(self, doc, num_fewshot, rnd=random.Random(1234), description=None):
if num_fewshot == 0:
labeled_examples = {"text": ""}
else:
fewshotex = self.fewshot_examples(k=num_fewshot, rnd=rnd)
labeled_examples = "\n\n".join(
[self.doc_to_text(doc) + self.doc_to_target(doc) for doc in fewshotex]
) + "\n\n"
labeled_examples = {"text": labeled_examples}
example = self.doc_to_text(doc)
labeled_examples["text"] = description + labeled_examples["text"] + example
return labeled_examples
具体示例:fewshot_context() 拼接过程 示例
# 假设 GSM8K 数据集中有以下样本:
# 当前待评测的文档
doc = {"question": "Janet's ducks lay 16 eggs per day...", "answer": "#### 18"}
# fewshot_context(doc, num_fewshot=2) 的执行过程:
# 1. 从训练集随机采样 2 条示例
fewshot_examples = [
{"question": "Natalia sold clips...", "answer": "#### 72"},
{"question": "Weng earns $12/hour...", "answer": "#### 10"},
]
# 2. 对每条示例调用 doc_to_text() + doc_to_target(),再用 "\n\n" 拼接
labeled_examples = (
"Natalia sold clips...\n#### 72\n\n"
"Weng earns $12/hour...\n#### 10\n\n"
)
# 3. 拼接当前文档的 doc_to_text()
example = "Janet's ducks lay 16 eggs per day..."
final_prompt = labeled_examples + example
# 最终 prompt(发给模型的完整输入):
# "Natalia sold clips...\n#### 72\n\n"
# "Weng earns $12/hour...\n#### 10\n\n"
# "Janet's ducks lay 16 eggs per day..."
apply_filters() -- 在模型推理完成后,对每个 Instance 的输出应用过滤器管道(默认为 take_first,即取第一个结果)。
2.3 ConfigurableTask -- YAML 驱动的任务子类
ConfigurableTask 继承自 Task,是绝大多数评测任务的实际类型。
它通过 TaskConfig dataclass 接收 YAML 配置,自动实现父类的所有抽象方法:
class ConfigurableTask(Task):
VERSION = "Yaml"
OUTPUT_TYPE = None
CONFIG = None
def __init__(self, data_dir=None, cache_dir=None, download_mode=None,
config: dict = None, model_name: Optional[str] = None) -> None:
self._config = self.CONFIG # 使用预配置(来自动态子类)
if self.config is None:
self._config = TaskConfig(**config)
else:
if config is not None:
self._config.__dict__.update(config) # 允许运行时覆盖
if self.config.output_type is not None:
assert self.config.output_type in ALL_OUTPUT_TYPES
self.OUTPUT_TYPE = self.config.output_type
...
TaskConfig 是一个包含 30+ 字段的 dataclass,核心字段包括:
@dataclass
class TaskConfig(dict):
task: str = None # 任务唯一标识
group: Union[str, list] = None # 所属分组
dataset_path: str = None # HF 数据集路径
dataset_name: str = None # 数据集子集名称
dataset_kwargs: dict = None # 数据集加载参数
test_split: str = None # 测试集 split 名
doc_to_text: Union[Callable, str] = None # prompt 模板 / Jinja2 / 函数
doc_to_target: Union[Callable, str] = None # 答案模板
process_results: Union[Callable, str] = None # 自定义评分函数
output_type: str = "generate_until" # 输出类型
generation_kwargs: dict = None # 生成参数
metric_list: list = None # 自定义指标列表
num_fewshot: int = None # few-shot 数量
filter_list: Union[str, list] = None # 输出过滤器
custom_infer: Callable = None # 自定义推理函数
repeats: int = 1 # 每条样本推理次数
!function 引用:YAML 文件中可以使用 !function utils.process_docs
语法来引用 Python 函数,框架会在加载时自动解析为可调用对象。这使得 doc_to_text、process_results
等字段既支持 Jinja2 模板字符串,也支持任意 Python 函数。
ConfigurableTask 的 construct_requests() 方法根据 OUTPUT_TYPE 分支构建不同类型的 Instance:
def construct_requests(self, doc: dict, ctx: str, **kwargs
) -> Union[List[Instance], Instance]:
if self.OUTPUT_TYPE == "loglikelihood":
arguments = (ctx, self.doc_to_target(doc), self.config.generation_kwargs)
elif self.OUTPUT_TYPE == "loglikelihood_rolling":
arguments = (self.doc_to_target(doc),)
elif self.OUTPUT_TYPE == "multiple_choice":
choices = self.doc_to_choice(doc)
# 为每个选项创建一个 loglikelihood Instance
arguments = [(ctx, f"{target_delimiter}{cont}",
self.config.generation_kwargs) for cont in choices]
request_list = [
Instance(request_type="loglikelihood", doc=doc,
arguments=arg, idx=i, **kwargs)
for i, arg in enumerate(arguments)
]
return request_list
elif self.OUTPUT_TYPE == "generate_until":
arguments = (ctx, self.config.generation_kwargs)
return Instance(
request_type=self.OUTPUT_TYPE, doc=doc,
arguments=arguments, idx=0, **kwargs
)
2.4 Instance -- 评测实例
Instance(定义于 api/instance.py)是一个轻量级数据类,
封装了单条评测请求的所有信息:
@dataclass
class Instance:
request_type: Literal["loglikelihood", "loglikelihood_rolling", "generate_until"]
doc: dict # 原始文档
arguments: tuple # 传给 LM 方法的参数
idx: int # 在同一 doc 中的序号
metadata: Tuple[str, int, int] = ... # (task_name, doc_id, repeats)
resps: list = field(default_factory=list) # 模型原始输出
filtered_resps: dict = field(default_factory=dict) # 过滤后的输出
think_resps: list = field(default_factory=list) # thinking tokens
raw_resps: list = field(default_factory=list) # 完整原始响应
# 自动从 metadata 解包
task_name: str = None
doc_id: str = None
repeats: str = None
def __post_init__(self) -> None:
self.task_name, self.doc_id, self.repeats = self.metadata
@property
def args(self):
return (
self.arguments if isinstance(self.arguments, tuple)
else (self.arguments,)
)
resps 存放经过 thinking token 过滤后的主输出;
think_resps 存放被过滤掉的 <think>...</think> 部分;
raw_resps 存放完整的未处理响应。
filtered_resps 是一个字典,键为过滤器名称,值为经过该过滤器处理后的结果。
03 注册表机制
lm-eval-harness 采用全局注册表 + 装饰器的模式来管理模型、任务、指标等扩展点。
所有注册表定义于 api/registry.py。
3.1 MODEL_REGISTRY -- 模型注册表
MODEL_REGISTRY 是一个 dict,将字符串名称映射到 LM 子类。
通过 @register_model 装饰器注册:
MODEL_REGISTRY = {}
def register_model(*names):
def decorate(cls):
for name in names:
assert issubclass(cls, LM), \
f"Model '{name}' ({cls.__name__}) must extend LM class"
assert name not in MODEL_REGISTRY, \
f"Model named '{name}' conflicts with existing model!"
MODEL_REGISTRY[name] = cls
return cls
return decorate
def get_model(model_name):
try:
return MODEL_REGISTRY[model_name]
except KeyError:
raise ValueError(
f"Attempted to load model '{model_name}', but no model for this name found! "
f"Supported model names: {', '.join(MODEL_REGISTRY.keys())}"
)
使用示例:
@register_model("vllm_norm")
class VLLM(LM):
def loglikelihood(self, requests): ...
def loglikelihood_rolling(self, requests): ...
def generate_until(self, requests): ...
3.2 TASK_REGISTRY 与 GROUP_REGISTRY -- 任务注册表
TASK_REGISTRY = {}
GROUP_REGISTRY = {}
ALL_TASKS = set()
func2task_index = {}
def register_task(name):
def decorate(fn):
assert name not in TASK_REGISTRY, \
f"task named '{name}' conflicts with existing registered task!"
TASK_REGISTRY[name] = fn
ALL_TASKS.add(name)
func2task_index[fn.__name__] = name
return fn
return decorate
def register_group(name):
def decorate(fn):
func_name = func2task_index[fn.__name__]
if name in GROUP_REGISTRY:
GROUP_REGISTRY[name].append(func_name)
else:
GROUP_REGISTRY[name] = [func_name]
ALL_TASKS.add(name)
return fn
return decorate
ALL_TASKS 是 TASK_REGISTRY 和 GROUP_REGISTRY 的键的并集,
表示所有已注册的任务名和分组名。调用者可以通过 name in ALL_TASKS 快速判断一个名称是否已注册。
具体示例
假设 tasks/ 目录下有以下 3 个 YAML 文件:
# ① gsm8k_0shot_cot.yaml — 纯任务,无分组
task: gsm8k_0shot_cot
dataset_path: parquet
output_type: generate_until
# ② charxiv_descriptive.yaml — 任务 + 隶属 "charxiv" 分组
task: "charxiv_descriptive"
group: "charxiv"
dataset_path: json
output_type: generate_until
# ③ charxiv_reasoning.yaml — 同样隶属 "charxiv" 分组
task: "charxiv_reasoning"
group: "charxiv"
dataset_path: json
output_type: generate_until
框架扫描后的注册流程(include_task_folder 两遍扫描):
# —— 第一遍:register_task(task 字段为 str 的 YAML) ——
# ① gsm8k_0shot_cot.yaml
SubClass = type("gsm8k_0shot_cotConfigurableTask", (ConfigurableTask,), {"CONFIG": TaskConfig(...)})
register_task("gsm8k_0shot_cot")(SubClass)
# → TASK_REGISTRY["gsm8k_0shot_cot"] = SubClass
# → ALL_TASKS = {"gsm8k_0shot_cot"}
# ② charxiv_descriptive.yaml
SubClass2 = type("charxiv_descriptiveConfigurableTask", (ConfigurableTask,), {"CONFIG": TaskConfig(...)})
register_task("charxiv_descriptive")(SubClass2)
register_group("charxiv")(SubClass2)
# → TASK_REGISTRY["charxiv_descriptive"] = SubClass2
# → GROUP_REGISTRY["charxiv"] = ["charxiv_descriptive"]
# → ALL_TASKS = {"gsm8k_0shot_cot", "charxiv_descriptive", "charxiv"}
# ③ charxiv_reasoning.yaml(同理)
register_task("charxiv_reasoning")(SubClass3)
register_group("charxiv")(SubClass3)
# → GROUP_REGISTRY["charxiv"] = ["charxiv_descriptive", "charxiv_reasoning"] ← append
最终注册表状态:
| 注册表 | 键 | 值 |
|---|---|---|
TASK_REGISTRY |
"gsm8k_0shot_cot" |
gsm8k_0shot_cotConfigurableTask 类 |
"charxiv_descriptive" |
charxiv_descriptiveConfigurableTask 类 | |
"charxiv_reasoning" |
charxiv_reasoningConfigurableTask 类 | |
GROUP_REGISTRY |
"charxiv" |
["charxiv_descriptive", "charxiv_reasoning"] |
ALL_TASKS |
{"gsm8k_0shot_cot", "charxiv_descriptive", "charxiv_reasoning", "charxiv"} |
|
--tasks charxiv 时,框架在 GROUP_REGISTRY 中找到 "charxiv",
展开为 ["charxiv_descriptive", "charxiv_reasoning"],再从 TASK_REGISTRY 逐个实例化。
单任务如 --tasks gsm8k_0shot_cot 则直接查 TASK_REGISTRY。
3.3 DEFAULT_METRIC_REGISTRY -- 默认指标映射
当 YAML 配置没有显式指定 metric_list 时,框架会根据 output_type 从此映射中取默认指标:
DEFAULT_METRIC_REGISTRY = {
"loglikelihood": ["perplexity", "acc"],
"loglikelihood_rolling": ["word_perplexity", "byte_perplexity", "bits_per_byte"],
"multiple_choice": ["acc", "acc_norm"],
"generate_until": ["exact_match", "fc_accuracy"],
}
3.4 METRIC_REGISTRY 与 AGGREGATION_REGISTRY
METRIC_REGISTRY 存放指标计算函数,AGGREGATION_REGISTRY 存放聚合函数(如 mean)。
通过 @register_metric 和 @register_aggregation 装饰器注册:
METRIC_REGISTRY = {}
AGGREGATION_REGISTRY = {}
METRIC_AGGREGATION_REGISTRY = {}
HIGHER_IS_BETTER_REGISTRY = {}
def register_metric(**args):
def decorate(fn):
assert "metric" in args
name = args["metric"]
METRIC_REGISTRY[name] = fn
if "aggregation" in args:
METRIC_AGGREGATION_REGISTRY[name] = AGGREGATION_REGISTRY[args["aggregation"]]
if "higher_is_better" in args:
HIGHER_IS_BETTER_REGISTRY[name] = args["higher_is_better"]
return fn
return decorate
def register_aggregation(name):
def decorate(fn):
AGGREGATION_REGISTRY[name] = fn
return fn
return decorate
3.5 任务注册流程
从文件系统中的 YAML 到可用的评测任务,完整的注册流程如下:
# tasks/__init__.py
def include_path(task_dir):
"""先注册所有单任务,再注册所有分组"""
include_task_folder(task_dir) # 第一遍:注册单任务
include_task_folder(task_dir, register_task=False) # 第二遍:注册分组
def include_task_folder(task_dir, register_task=True):
"""遍历 task_dir 下所有 .yaml 文件"""
for root, subdirs, file_list in os.walk(task_dir):
for f in file_list:
if f.endswith(".yaml"):
config = utils.load_yaml_config(os.path.join(root, f))
if "task" not in config:
continue
if register_task:
if type(config["task"]) == str:
register_configurable_task(config) # 注册单任务
else:
if type(config["task"]) == list:
register_configurable_group(config) # 注册分组
def register_configurable_task(config):
"""动态创建 ConfigurableTask 子类并注册"""
SubClass = type(
config["task"] + "ConfigurableTask",
(ConfigurableTask,),
{"CONFIG": TaskConfig(**config)},
)
task_name = config["task"]
register_task(task_name)(SubClass)
if "group" in config:
for group in (config["group"] if isinstance(config["group"], list)
else [config["group"]]):
register_group(group)(SubClass)
register_configurable_task() 使用 Python 内置的 type() 函数
动态创建一个以任务名命名的 ConfigurableTask 子类,并将解析后的 TaskConfig 绑定到类属性 CONFIG 上。
这样每个 YAML 文件对应一个独立的类,避免了配置之间的相互干扰。
为什么需要两遍扫描?
以 tasks/mvbench/ 目录为例,目录下有 两种 YAML:
tasks/mvbench/
├── mvbench.yaml # ← 分组 YAML(task 是 list)
├── mvbench_action_count.yaml # ← 单任务 YAML(task 是 str)
├── mvbench_moving_count.yaml
├── mvbench_action_sequence.yaml
└── ...(共 19 个单任务 YAML)
单任务 YAML(task 字段为 str)—— 定义一个具体的评测任务:
# mvbench_action_count.yaml
include: _default_template_yaml
task: mvbench_action_count # ← str,表示这是一个单任务
dataset_path: json
test_split: train
分组 YAML(task 字段为 list)—— 仅声明一个分组名和它包含哪些任务:
# mvbench.yaml
group: mvbench # ← 分组名
task: # ← list,表示这是一个分组定义
- mvbench_action_count
- mvbench_moving_count
- mvbench_action_sequence
- ...(共 19 个子任务)
分组 YAML 依赖子任务已经在 TASK_REGISTRY 中注册好,否则
register_configurable_group() 查不到它们。因此 必须先跑一遍注册单任务,再跑一遍注册分组:
def include_path(task_dir):
include_task_folder(task_dir) # 第一遍
include_task_folder(task_dir, register_task=False) # 第二遍
# ═══════════════════════════════════════════════════════════
# 第一遍 register_task=True:只处理 type(config["task"]) == str
# ═══════════════════════════════════════════════════════════
# 扫到 mvbench_action_count.yaml → task 是 str → 注册
# 扫到 mvbench_moving_count.yaml → task 是 str → 注册
# ...
# 扫到 mvbench.yaml → task 是 list → 跳过!
#
# 第一遍结束后:
# TASK_REGISTRY = {
# "mvbench_action_count": SubClass1,
# "mvbench_moving_count": SubClass2,
# ...(共 19 个)
# }
# GROUP_REGISTRY = {} ← 还是空的
#
# ═══════════════════════════════════════════════════════════
# 第二遍 register_task=False:只处理 type(config["task"]) == list
# ═══════════════════════════════════════════════════════════
# 扫到 mvbench_action_count.yaml → task 是 str → 跳过!
# ...
# 扫到 mvbench.yaml → task 是 list → 注册分组
# → GROUP_REGISTRY["mvbench"] = [
# "mvbench_action_count",
# "mvbench_moving_count",
# ...(共 19 个引用)
# ]
# → ALL_TASKS.add("mvbench")
#
# 最终效果:
# --tasks mvbench → 展开为 19 个子任务,逐个评测
# --tasks mvbench_action_count → 直接评测单个任务
os.walk 的文件遍历顺序不确定。如果 mvbench.yaml(分组)在
mvbench_action_count.yaml(单任务)之前被扫到,分组注册时会发现
"mvbench_action_count" 还不在 ALL_TASKS 中,该子任务就会被跳过,
导致分组不完整。两遍扫描保证了 注册顺序的确定性:先有任务,再建分组。
04 评测主循环
评测的核心逻辑位于 evaluator.py 的 evaluate() 函数。
simple_evaluate() 是面向用户的高层接口,负责模型实例化和任务解析,最终委托 evaluate() 执行。
4.1 simple_evaluate() -- 高层入口
def simple_evaluate(
model, model_args=None, tasks=[], num_fewshot=None,
batch_size=None, device=None, limit=None, gen_kwargs=None, ...
):
# 1. 实例化模型
if isinstance(model, str):
lm = lm_eval.api.registry.get_model(model).create_from_arg_string(
model_args,
{"batch_size": batch_size, "max_batch_size": max_batch_size, "device": device},
)
else:
lm = model # 直接传入 LM 对象
# 2. 可选:启用缓存
if use_cache is not None:
lm = lm_eval.api.model.CachingLM(lm, use_cache + "_rank" + str(lm.rank) + ".db")
# 3. 解析任务
task_dict = lm_eval.tasks.get_task_dict(tasks)
# 4. 覆盖 num_fewshot / gen_kwargs
for task_name, task_obj in task_dict.items():
config = task_obj._config
if config["output_type"] == "generate_until" and gen_kwargs is not None:
config["generation_kwargs"].update(gen_kwargs)
if num_fewshot is not None:
task_obj._config["num_fewshot"] = num_fewshot
# 5. 调用 evaluate()
results = evaluate(lm=lm, task_dict=task_dict, limit=limit, ...)
return results
4.2 evaluate() -- 核心循环
evaluate() 的执行流程可分为五个阶段:
阶段一:构建请求
requests = collections.defaultdict(list)
for task_name, task in task_dict.items():
if type(task) == tuple:
group_name, task = task
# 构建所有 Instance(内部调用 fewshot_context + construct_requests)
task.build_all_requests(limit=limit, rank=lm.rank, world_size=lm.world_size)
# 按 request_type 分组
for instance in task.instances:
reqtype = instance.request_type
requests[reqtype].append(instance)
阶段二:模型推理
for reqtype, reqs in requests.items():
# 根据 repeats 展开请求
cloned_reqs = [req for req in reqs for _ in range(req.repeats)]
# 调用模型对应的方法
resps = getattr(lm, reqtype)(cloned_reqs)
# 将响应写回 Instance
for x, req in zip(resps, cloned_reqs):
if isinstance(x, str):
# 处理 thinking tokens:过滤 <think>...</think>
filtered_think_resp = x.split(THINKING_END_TOKEN)[1] \
if THINKING_END_TOKEN in x else x
req.resps.append(filtered_think_resp)
req.think_resps.append(think_resp)
req.raw_resps.append(x)
else:
req.resps.append(x)
<think>\n...</think> 格式的推理链(Chain-of-Thought)的处理。
模型的完整输出保存在 raw_resps 中,而 resps 只包含去掉 thinking 部分后的最终回答。
这对评估推理型模型(如 DeepSeek-R1)非常重要。
具体示例:Thinking Token 过滤前后对比 示例
# 模型原始输出(raw_resps)
raw_output = "<think>\n让我分析一下这道题...\n先计算16-3=13\n再计算13-5=8\n最后8+10=18\n</think>\n#### 18"
# 框架检测到 </think> 标记,自动拆分为三个字段:
# 1. raw_resps —— 完整的原始输出,原封不动保存
instance.raw_resps = [
"<think>\n让我分析一下这道题...\n先计算16-3=13\n再计算13-5=8\n最后8+10=18\n</think>\n#### 18"
]
# 2. think_resps —— 仅 thinking 部分(<think> 和 </think> 之间的内容)
instance.think_resps = [
"让我分析一下这道题...\n先计算16-3=13\n再计算13-5=8\n最后8+10=18"
]
# 3. resps —— 去掉 thinking 后的最终回答(用于后续评分)
instance.resps = ["#### 18"]
阶段三:应用过滤器
for task_name, task in task_dict.items():
if type(task) == tuple:
group, task = task
task.apply_filters() # 应用过滤管道,结果写入 instance.filtered_resps
阶段四:计算指标
vals = collections.defaultdict(list)
for task_name, task in task_dict.items():
for key in task.instances[0].filtered_resps.keys():
doc_iterator = itertools.islice(
enumerate(task.test_docs()), lm.rank, limit, lm.world_size
)
for doc_id, doc in doc_iterator:
requests = [x for x in task.instances if x.doc_id == doc_id]
requests.sort(key=lambda x: x.idx)
# 调用 task.process_results() 计算每条样本的指标
metrics = task.process_results(
doc, [req.filtered_resps[key] for req in requests]
)
for metric, value in metrics.items():
vals[(task_name, key, metric)].append(value)
具体示例:process_results 与 vals 累积过程 示例
# 假设任务 gsm8k 有 3 条样本,process_results 逐条计算:
# 样本 1:模型输出 "42",正确答案 "42"
process_results(doc1, ["42"]) → {"exact_match": 1.0}
# 样本 2:模型输出 "15",正确答案 "18"
process_results(doc2, ["15"]) → {"exact_match": 0.0}
# 样本 3:模型输出 "1024",正确答案 "1024"
process_results(doc3, ["1024"]) → {"exact_match": 1.0}
# vals 字典累积结果:
vals = {
("gsm8k", "get-answer", "exact_match"): [1.0, 0.0, 1.0]
}
# 阶段五聚合:mean([1.0, 0.0, 1.0]) = 0.667
阶段五:聚合与返回
results = collections.defaultdict(dict)
for (task_name, key, metric), items in vals.items():
task = task_dict[task_name]
# 调用聚合函数(如 mean)
agg_fn = task.aggregation()[metric]
results[task_name][metric + "," + key] = agg_fn(items)
# 计算 bootstrap 标准误
stderr = lm_eval.api.metrics.stderr_for_metric(
metric=agg_fn, bootstrap_iters=bootstrap_iters,
)
if stderr is not None and len(items) > 1:
results[task_name][metric + "_stderr," + key] = stderr(items)
return {
"results": dict(results_agg.items()),
"groups": dict(groups_agg.items()),
"configs": dict(sorted(configs.items())),
"versions": dict(sorted(versions.items())),
"n-shot": dict(sorted(num_fewshot.items())),
"samples": dict(samples),
}
05 数据流
下图展示了从 YAML 配置到最终评测分数的完整数据流:
更细致地看,一条文档的完整生命周期如下:
| 阶段 | 函数 | 输入 | 输出 |
|---|---|---|---|
| 1. 配置加载 | utils.load_yaml_config() |
YAML 文件路径 | dict 配置 |
| 2. 任务创建 | register_configurable_task() |
config dict | ConfigurableTask 子类(注册到 TASK_REGISTRY) |
| 3. 数据加载 | task.download() |
dataset_path, dataset_kwargs | datasets.Dataset |
| 4. 数据预处理 | config.process_docs() |
原始 Dataset | 处理后的 Dataset |
| 5. Prompt 构建 | doc_to_text(doc) |
单条文档 doc |
prompt 字符串 |
| 6. 上下文组装 | fewshot_context() |
doc, num_fewshot | 含 few-shot 示例的完整 context dict |
| 7. 请求创建 | construct_requests() |
doc, ctx | Instance 对象 |
| 8. 模型推理 | LM.generate_until() / LM.loglikelihood() |
Instance 列表 |
模型输出(写入 Instance.resps) |
| 9. 输出过滤 | apply_filters() |
Instance.resps |
Instance.filtered_resps |
| 10. 指标计算 | process_results(doc, results) |
doc + filtered_resps | {"acc": 1, "f1": 0.85, ...} |
| 11. 分数聚合 | aggregation()[metric](items) |
所有样本的指标值列表 | 最终分数(如 mean) |
分布式场景下的数据分片:build_all_requests() 通过
utils.create_iterator(enumerate(docs), rank, world_size, limit)
将数据集按 rank 进行切片,每个 rank 只处理自己负责的文档子集。
推理完成后,evaluate() 使用
torch.distributed.all_gather_object() 收集所有 rank 的结果进行聚合。
06 扩展点
6.1 添加新模型
要接入一个新的推理后端,只需三步:
- 创建一个继承
LM的子类 - 实现三个抽象方法:
loglikelihood()、loglikelihood_rolling()、generate_until() - 使用
@register_model("your_model_name")装饰器注册
from lm_eval.api.model import LM
from lm_eval.api.registry import register_model
@register_model("my_custom_model")
class MyCustomModel(LM):
def __init__(self, pretrained, batch_size=1, **kwargs):
super().__init__()
self.model = load_my_model(pretrained)
self.batch_size = int(batch_size)
def loglikelihood(self, requests):
# 实现条件对数似然计算
results = []
for req in requests:
context, continuation = req.args[0], req.args[1]
logprob, is_greedy = self._compute_ll(context, continuation)
results.append((logprob, is_greedy))
return results
def loglikelihood_rolling(self, requests):
# 实现无条件对数似然计算
...
def generate_until(self, requests):
# 实现文本生成
results = []
for req in requests:
context = req.args[0]
gen_kwargs = req.args[1]
output = self.model.generate(context, **gen_kwargs)
results.append(output)
return results
注册后即可通过命令行使用:
lm_eval --model my_custom_model --model_args pretrained=/path/to/model --tasks mmlu
6.2 添加新任务
最常用的方式是编写一个 YAML 配置文件,并可选地提供一个 utils.py 辅助脚本。
将它们放入 lm_eval/tasks/ 目录下的适当子文件夹中即可。
# tasks/my_benchmark/my_task.yaml
task: my_task
group: my_benchmark
dataset_path: /data/my_dataset
dataset_kwargs:
data_dir: /data/my_dataset
test_split: test
output_type: generate_until
doc_to_text: !function utils.doc_to_text
doc_to_target: !function utils.doc_to_target
process_results: !function utils.process_results
generation_kwargs:
max_gen_toks: 2048
temperature: 0.0
metric_list:
- metric: !function utils.my_custom_metric
aggregation: mean
higher_is_better: true
# tasks/my_benchmark/utils.py
def doc_to_text(doc):
return f"Question: {doc['question']}\nAnswer:"
def doc_to_target(doc):
return doc["answer"]
def process_results(doc, results):
pred = results[0].strip()
gold = doc["answer"]
return {"accuracy": 1.0 if pred == gold else 0.0}
def my_custom_metric(items):
return sum(items) / len(items)
initialize_tasks(),
它通过 include_path() 递归扫描 tasks/ 目录下的所有 .yaml 文件,
自动注册发现的任务。因此只要将 YAML 放在正确位置,无需修改任何其他代码。
6.3 添加新指标
自定义指标可以通过 @register_metric 和 @register_aggregation 装饰器注册到全局注册表中:
from lm_eval.api.registry import register_metric, register_aggregation
@register_aggregation("geometric_mean")
def geometric_mean(items):
import math
return math.exp(sum(math.log(x) for x in items if x > 0) / len(items))
@register_metric(metric="my_f1", aggregation="mean", higher_is_better=True)
def my_f1(references, predictions, **kwargs):
# 计算 F1 分数
...
注册后,在 YAML 的 metric_list 中引用即可:
metric_list:
- metric: my_f1
aggregation: mean
higher_is_better: true
6.4 添加新 output_type
如果需要支持全新的推理模式(例如多轮对话),需要修改以下几处:
-
在
api/task.py的ALL_OUTPUT_TYPES列表中添加新类型:ALL_OUTPUT_TYPES = [ "loglikelihood", "multiple_choice", "loglikelihood_rolling", "generate_until", "generate_until_multi_round", # 新增 ] -
在
ConfigurableTask.construct_requests()中添加对应的分支逻辑 -
在
LM基类中添加对应的抽象方法(或在子类中实现同名方法) -
在
DEFAULT_METRIC_REGISTRY中添加该 output_type 的默认指标
evaluate() 主循环以 "构建请求 → 模型推理 → 过滤输出 → 计算指标 → 聚合分数" 的五阶段流水线驱动整个评测过程。