从零搭一套能扛住生产环境的评估循环:lm-evaluation-harness 实战手记 从零搭一套能扛住生产环境的评估循环lm-evaluation-harness 实战手记【免费下载链接】lm-evaluation-harnessA framework for few-shot evaluation of language models.项目地址: https://gitcode.com/GitHub_Trending/lm/lm-evaluation-harnesslm-evaluation-harness 是当前社区使用最广的语言模型评测框架之一核心能力是少样本评估即用少量示例引导模型在统一的任务基准上打分。但大多数教程只教你跑通lm_eval命令行一旦你需要在内部模型、私有数据集、自定义指标上构建自己的评估循环手册之外的空白就暴露出来了。这篇文章不重复官方 README而是用一次真实的搭建过程把评估循环拆开揉碎给你看。先交代一下我自己的处境后面所有代码都出自这个场景公司自研了一个 3B 参数的对话模型我要给它搭一套可持续复用的评测体系既能跑公开基准也能跑内部标注的问答集最后还要接进 CI每次发版自动回归。听起来不复杂真正动手才发现评估循环这件事框架只给了你方向盘路还得自己画。上图是典型的少样本提示结构任务说明加若干示例最后留一个待完成的输入。lm-evaluation-harness 的少样本核心就在这一张图里后面所有配置都围绕它展开。翻车现场我按 README 跑通后发现三件事不对先说我第一次上手时的遭遇你大概率也踩过类似的坑。第一件事我用命令行跑--tasks mmlu很顺利但想把评测嵌进自己的 Python 服务里文档里却只给了入口函数名没讲清楚内部怎么流转。第二件事我加了一个内部数据集写了个 YAML结果提示词格式怎么调都不对模型输出永远是乱的。第三件事评测结果和我在别处手动算的对不上找了半天才发现是 Chat 模板在悄悄改变输入格式。这三件事合起来指向同一个结论只把框架当黑盒用是走不远的。你需要理解评估循环的骨架——请求怎么构建、输出怎么对齐、指标怎么算——才能让它按你的意志工作。下面这张图是我后来总结出的评估循环全景和官方文档的视角不同我把它画成数据流而不是步骤流记住这条链路后面每一节都在回答同一个问题在哪个环节插入你自己的逻辑。两个入口函数先分清谁干粗活、谁干细活官方核心源码在 lm_eval/evaluator.py里面躺着两个关键函数simple_evaluate和evaluate。它们的关系有点像一键下单和手动配菜。simple_evaluate是大多数人的第一站传入模型名和任务名它负责实例化模型、加载任务、跑完整流程、返回结果。它的完整签名在源码里有我挑几个你迟早会用到、但文档着墨不多的参数列出来参数作用我的建议model模型名或 LM 实例传名字省事传实例才能做高级控制model_args模型初始化参数如pretrainedxxx用字典比字符串更不容易写错num_fewshot少样本示例个数设成 0 就是零样本别省略batch_size批大小支持auto后面专门讲它挖的坑limit每个任务只取前 N 条或比例调试期必备limit0.05能救命use_cache模型响应缓存SQLite 路径复跑不变的数据集时省一半时间cache_requests请求构建缓存数据集大、任务多时开gen_kwargs生成式任务的生成参数控制until、temperature等apply_chat_template是否套用模型的对话模板默认 False改了它分数会变一个能直接跑的最小调用长这样from lm_eval import evaluator result evaluator.simple_evaluate( modelhf, model_args{pretrained: your-org/your-3b-model, dtype: bfloat16}, tasks[arc_easy, hellaswag], num_fewshot5, batch_size16, limit0.1, # 先跑 10% 验证流程再放开 )而evaluate函数接收的是已经实例化好的模型对象 已经加载好的任务字典适合你在simple_evaluate的流程中间插入自定义逻辑的场景——比如你想在请求构建后、推理前做一次输入改写就必须走这条路。先跑一遍simple_evaluate再回头读它内部如何调用evaluate你对整个框架的理解会突然清晰。任务从哪来一张 YAML 怎么描述一个评测任务任务配置的核心字段集中在几个地方我们以内部问答数据集为例逐步解剖。先看最小可用版task: corp_qa_zh dataset_path: your-org/corp-qa doc_to_text: 问题{{question}}\n答案 doc_to_target: {{answer}} output_type: generate_until generation_kwargs: until: [\n] max_gen_toks: 64 metric_list: - metric: exact_match aggregation: mean higher_is_better: true逐行说dataset_path数据集在 HuggingFace Hub 上的路径本地私有数据集也可以用json或csv加载器指到本地文件。doc_to_text/doc_to_target从数据行中提取提示词和参考答案花括号里是字段名这就是上面那张少样本图的程序化版本。output_type只有两种主类型——loglikelihood算似然适合选择题和generate_until生成文本适合问答、翻译。metric_list指标定义exact_match、acc、acc_norm这些是内置的后面讲怎么造自己的。如果你想要少样本效果在 YAML 里直接写num_fewshot: 3框架会从数据集中采样示例拼进提示词。想更精细地控制示例的组织方式fewshot_config块里可以指定采样器和上下文格式这块的细节建议读 docs/config_files.md它是目前最完整的字段字典。写 YAML 时最容易懵的是doc_to_text里的字段到底长什么样。我的调试习惯是先跑一次带write_outTrue的评估框架会把真实构造出的提示词和参考答案落盘肉眼比对一遍比猜字段名高效得多。内置指标不够用自己注册一个框架的指标系统比想象中开放。内置指标实现集中在 lm_eval/api/metrics.py但你完全可以注册自己的。装饰器在 lm_eval/api/registry.py 里定义签名长这样register_metric( metricf1_macro, higher_is_betterTrue, output_typegenerate_until, aggregationf1_macro, )注意它注册的不只是一个函数而是三样东西指标函数怎么算、聚合函数跨样本怎么汇总、方向数值越大越好还是越小越好。我写过的一个多标签分类任务就是靠它解决了exact_match完全不适配的问题from lm_eval.api.registry import register_metric register_metric( metricsubset_accuracy, higher_is_betterTrue, output_type[loglikelihood, multiple_choice], aggregationsubset_accuracy, ) def subset_accuracy(items): 全对才算对预测的标签集合必须和参考答案完全一致。 import numpy as np preds [set(item[0]) for item in items] golds [set(item[1]) for item in items] return np.mean([p g for p, g in zip(preds, golds)])注册之后有两种用法。第一种直接在 YAML 的metric_list里写metric: subset_accuracy。第二种代码里动态替换适合同一任务不同场景要换指标的情况用的是 lm_eval/api/task.py 里的override_metricfrom lm_eval.api.registry import get_task task get_task(corp_qa_zh) task.override_metric(metric_namesubset_accuracy)一个提醒注册函数后要确保它在评测进程里被 import 过否则注册表里找不到。常见的做法是放在自己的工具模块里评测脚本启动时先 import 它或者利用框架的include机制在 YAML 里声明自定义函数路径。非 HuggingFace 模型怎么接进来实现 LM 抽象类这是很多人的终极需求公司自研推理引擎不是 HF 格式跑不了modelhf。框架的解法很优雅——所有模型都要实现 lm_eval/api/model.py 里的LM抽象类你只要把自家引擎包一层即可。抽象类要求实现四个抽象方法loglikelihood、loglikelihood_rolling、generate_until以及分词相关的tok_encode/tok_decode。但对大多数接入场景你真正要写的是两个底层方法from lm_eval.api.model import LM from lm_eval.api.instance import Instance class InternalEngineLM(LM): def __init__(self, endpoint_url: str): super().__init__() self.endpoint endpoint_url def _loglikelihood_tokens(self, requests, **kwargs): 输入是 (上下文, 续写) 的 token 对输出 (loglikelihood, 是否贪婪匹配)。 outputs [] for context_enc, continuation_enc in requests: score self.endpoint.score(context_enc, continuation_enc) outputs.append((score, True)) return outputs def _generate_until(self, requests): 输入是 (提示词, 生成参数)输出是生成的文本列表。 results [] for context, gen_kwargs in requests: until gen_kwargs.get(until, [\n]) text self.endpoint.complete(context, stopuntil) results.append(text) return results写完之后把实例传给simple_evaluate(modelengine_lm, ...)或者干脆注册成新模型别名方便命令行直接用。想照抄完整模板参考官方示例 examples/transformer-lens.py它展示了如何把一个第三方模型库完整适配进来包括分词器和loglikelihood_rolling的实现套路。这里有个隐蔽细节_loglikelihood_tokens里的请求会尽量按长度分批以匹配 GPU 的并行能力所以你返回的顺序必须和传入顺序一一对应错一个就全线崩盘。我会在返回前做一次长度断言花两行代码省一晚上排错。多模态任务图文输入怎么走评估循环如果你的模型要吃图任务类里有个开关叫MULTIMODAL。在 lm_eval/api/task.py 里只要配置了doc_to_image字段框架会自动把任务的MULTIMODAL置为 True并启用图像处理管线。用 YAML 定义一个图文任务并不复杂task: chart_reading_zh dataset_path: your-org/chart-qa doc_to_text: 请根据图表回答{{question}}\n doc_to_image: {{image}} doc_to_target: {{answer}} output_type: generate_until generation_kwargs: until: [\n] metric_list: - metric: exact_match对应的模型侧需要支持图像输入。HF 系多模态模型走 lm_eval/models/hf_vlms.py它会处理图像编码和与文本的拼接如果你是自研多模态引擎还是回到上一节的套路——实现LM抽象类在_generate_until里从Instance中取出图像数据一并发给引擎。doc_to_image支持传 PIL 图像对象、图像路径或 base64 字符串框架内部统一编码。如果图像字段比较大建议在任务配置里开请求缓存否则每次跑评测都要重新下载和处理图片。规模化运行分布式评估与两级缓存单卡评测小模型没问题一旦上 70B 或几百个任务就要动真格的了。框架原生支持torch.distributed一条命令拉起多进程每个 rank 处理数据分片CUDA_VISIBLE_DEVICES0,1,2,3 python -m torch.distributed.run --nproc_per_node4 \ -m lm_eval --model hf --model_args pretrainedyour-org/your-3b-model \ --tasks mmlu,hellaswag,arc_easy --batch_size auto所有 rank 会并行构建请求模型推理各算各的分片最后聚合结果。注意一点多进程时每个 rank 会各自加载一份模型副本显存是按进程数线性增长的4 卡就是 4 份权重别想当然以为会自动张量并行。再谈缓存框架有两级别混请求缓存cache_requestsTrue缓存数据集 → Instance的构建结果。数据集大、fewshot 采样复杂时收益明显复跑时直接读缓存。模型响应缓存use_cachecache.db缓存模型对每个请求的输出。适合模型不变、只改指标或聚合方式的场景第二次跑能秒出。result evaluator.simple_evaluate( modelhf, model_args{pretrained: your-org/your-3b-model}, tasks[corp_qa_zh, mmlu], use_cacheeval_cache.db, cache_requestsTrue, )缓存文件的哈希键包含了模型配置、任务配置和数据集版本改了任何一环都会自动失效基本不用担心脏缓存。真正要防的是缓存文件越来越大CI 里建议定期清理。三个最容易翻车的坑每个我都付过学费坑一YAML 里的\n被当成两个字符这是框架文档 docs/footguns.md 里排第一的坑我完美复现过生成式任务的until里写了单引号\n结果模型永远等不到停止符输出长得离谱。原因很简单——单引号字符串不做转义\n变成了字面量反斜杠加 n。# 错的模型收不到换行符 generation_kwargs: until: [\n] # 对的这才是真正的换行 generation_kwargs: until: [\n]规则一句话YAML 里只要想表达转义字符就用双引号。同理模板里要输出换行也得写在双引号包裹的字符串中。坑二Chat 模板悄悄改写输入loglikelihood 对不上如果你的模型是对话模型且评估时开了apply_chat_templateTrue提示词会被包成[system][user][assistant]这样的对话结构。这本身没错但它会改变 token 序列而loglikelihood类任务算的是在给定上下文下续写的似然上下文变了分数自然变。更隐蔽的是fewshot_as_multiturn参数少样本示例是拼成一轮长对话还是拆成多轮对话直接影响最终分数。我的经验是对话模型评测务必在同一配置下对比不同模板的效果并在报告里固定记录模板名否则你没法解释为什么两次跑分不一样。官方对这个话题有专门讨论见 docs/chat-template-readme.md。坑三batch_sizeauto不是万能的自动批处理会从 1 开始指数级增大批大小直到触发 OOM 再回退听起来很智能。但有两件事它管不了一是max_batch_size不设的话可能尝试到很大值导致显存抖动甚至崩掉二是长序列任务比如长文档问答即使 batch 很小也可能 OOM因为显存占用和序列长度平方相关。result evaluator.simple_evaluate( modelhf, model_args{pretrained: your-org/your-3b-model}, tasks[long_doc_qa], batch_sizeauto, max_batch_size8, # 显存不够就把它调小 )我的建议先在limit很小的数据集上分别试batch_size1和auto对比耗时和显存峰值再决定正式配置。别把auto当成免检产品。落地到生产一套可持续复用的最小评测脚本把上面的知识点收拢给你一套我实际在 CI 里用的脚本骨架。它做了三件事注册自定义指标、实例化模型、按环境变量控制是否全量跑。import os import my_metrics # 确保自定义指标被注册 from lm_eval import evaluator def run_regression(full: bool False): tasks [corp_qa_zh, arc_easy, hellaswag] kwargs { model: hf, model_args: {pretrained: os.environ[MODEL_PATH], dtype: bfloat16}, tasks: tasks, num_fewshot: 3, batch_size: auto, max_batch_size: 8, gen_kwargs: {until: [\n], max_gen_toks: 128}, } if not full: kwargs[limit] 0.1 # 快速冒烟 return evaluator.simple_evaluate(**kwargs) if __name__ __main__: result run_regression(fullos.environ.get(FULL, 0) 1) print(result)这个脚本配合上面讲的多模态任务、分布式命令和缓存参数已经足够支撑一个内部模型的发版回归了。下一步行动清单从会用走向能改评估循环这件事光看文章是学不会的动手才算数。给你一条可执行的路线跑通最小闭环用文中的第一个代码块在limit0.1下跑通一个内置任务和一个你自己的数据集。读evaluate源码lm_eval/evaluator.py 里evaluate函数的请求构建段requests字典的填充逻辑是理解全框架的钥匙。仿写一个模型适配器对照 examples/transformer-lens.py把你手头任意一个模型哪怕是 Dummy 模型包成LM子类。提交一个新任务社区评测基准文件都在 lm_eval/tasks/ 下每个子目录一个基准。你把自己领域的数据集整理成 YAML 后完全可以按 docs/new_task_guide.md 的规范提交既服务社区也逼自己把配置写规范。最后给你一张自检表写代码时对照着过一遍我的 YAML 里所有转义字符都用双引号了吗自定义指标被 import 了吗higher_is_better方向对了吗开 Chat 模板后我记录下模板配置了吗batch_sizeauto时设max_batch_size了吗复跑实验时缓存和数据集版本匹配吗评测的本质是可复现的测量。当你把评估循环的每个环节都握在自己手里你得到的就不只是一份分数报告而是一套能支撑模型迭代决策的信任体系。框架给你的是骨架血肉是你自己长出来的。【免费下载链接】lm-evaluation-harnessA framework for few-shot evaluation of language models.项目地址: https://gitcode.com/GitHub_Trending/lm/lm-evaluation-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考