训练能跑通只是入场券评测才是把模型从“能跑”拽到“能用”的那道关卡。第七个作业落到书生浦语配 OpenCompass 做 OpenCompass大模型评测上我起初也以为无非是敲一条命令、看一眼榜单分数真跑起来才发现坑全在细节里chat 模板对不上、答案抽取正则没命中、分数出来是 0 却不知道错在哪、八张卡只吃到一张。这篇就把我从环境准备到结果解读的完整路径摊开讲包含配置结构拆解、显存与并行的估算方法、产物目录怎么读以及一份我自己踩出来的排查表。适合刚接触大模型评测、手里有书生浦语权重想跑个基准的同行也适合已经把流程跑通但分数总是对不上官方榜单的人对照找差异。全文以常见实践为准不同版本的 OpenCompass 目录结构会有出入命令以 0.2.x 到 0.3.x 区间为参考。1. 评测这件事为什么比训练更容易翻车1.1 一次“跑分”背后真正想解决的问题很多人对评测的理解停在“打个分、发个榜”这在工程视角下是极其危险的简化。训练阶段的错误往往很响loss 炸了你立刻知道评测阶段的错误却很安静配置少写一行模型照样跑完、照样出分只是那个分数毫无意义。我见过最典型的情况是 prompt 模板用了 base 模型的续写格式去测 chat 模型模型输出一堆自我解释抽取器抓不到选项字母最终准确率贴着地板然后有人得出“这个模型中文能力不行”的结论。评测真正要回答的是四类问题这个版本比上个版本强在哪、弱在哪换一套微调数据之后原来的能力有没有被“遗忘”模型输出的格式是否符合业务侧解析要求以及上线前的门槛线有没有踩住。这四个问题里只有第一个是“打分”后三个本质上是回归测试。所以我一直建议把评测当成 CI 的一部分来设计而不是当成一次性的仪式。分工上也要说清楚训练负责让模型见到更多数据评测负责告诉你这些数据到底有没有用。少了后半段调参就变成盲猜超参搜索出来的最优解很可能只是过拟合了某个验证集。1.2 工具选型的取舍为什么最后落到 OpenCompass自写评测脚本在只有一个数据集时是最省事的二十行代码搞定。但作业要求的多数据集横向对比一上来自写脚本的维护成本就爆炸了每个数据集的字段名不一样、答案格式不一样、prompt 风格不一样、指标算法不一样你很快就不是在评测模型而是在写数据清洗流水线。我把当时考虑的三个方向列了个对比实际选型时这张表帮了大忙方案上手成本数据集覆盖多模型多后端中文任务覆盖适合场景自写脚本极低取决于你自己写需自己实现自己找单任务、快速验证通用评测框架英文生态为主中等英文任务丰富支持较好相对薄弱英文基座模型、论文复现OpenCompass中等偏上覆盖广中英文都有内置多种推理后端中文任务较完整中英文混合、需要体系化对比决定性因素是中文任务和主观评测这两块。书生浦语本身中文能力是重点只跑纯英文基准等于避重就轻而主观题对话质量、指令遵循必须靠裁判模型打分这套机制得有人帮你把 prompt、打分维度、聚合方式都定好。OpenCompass 在这两点上省了我大量时间代价是它的配置体系有一层抽象需要花半天读懂。1.3 评测系统的三层结构理解了就不会乱改OpenCompass 的配置看起来复杂本质上是三层解耦模型接入层负责“怎么把权重加载起来并生成文本”数据与提示层负责“喂什么问题、以什么格式喂”指标与后处理层负责“怎么从一段自由文本里把答案抠出来并判定对错”。这三层各有一套配置互不干扰。解耦带来的直接好处是换模型不用动数据集换数据集不用动模型改判分规则不用重跑推理。我在实操中就吃过没理解这层结构的亏——第一次调 prompt 的时候直接去改了模型配置里的max_out_len结果长度够了但格式还是不对因为真正的问题出在数据集的prompt_template上。想清楚分层就知道该动哪个文件。提示判断问题出在哪一层有个笨办法但很有效。把predictions目录里的原始输出打开看如果输出本身合理只是判分错问题在第三层如果输出格式就跑偏了问题在第二层如果连输出都没有或者乱码问题在第一层。2. OpenCompass 的配置骨架怎么读2.1 三段式配置与继承机制一个典型的评测入口配置是纯 Python 文件因为它用了 mmengine 的 config 机制可以import和变量覆盖。骨架大致长这样from mmengine.config import read_base with read_base(): # 数据集 from .datasets.ceval.ceval_gen import ceval_datasets from .datasets.mmlu.mmlu_gen import mmlu_datasets from .datasets.gsm8k.gsm8k_gen import gsm8k_datasets # 模型 from .models.internlm.internlm_chat_7b import models as internlm_chat_7b_models # 结果汇总方式 from .summarizers.medium import summarizers datasets ceval_datasets mmlu_datasets gsm8k_datasets models internlm_chat_7b_modelsread_base的语义是“把被引用文件里的变量拉进当前命名空间”所以你写datasets a b就是把多个数据集列表拼接。这个机制的好处是复现性极强配置文件本身就是完整的实验记录跑完会有一份拷贝落在outputs/工作目录/configs/下面半年后回头查“当时到底用的哪个 prompt 版本”答案就在那里。容易踩的坑是命名冲突。如果你引了两个文件里面都有叫models的变量后引入的会把前面的覆盖掉而且不报错。我现在的习惯是引入模型时一律as xxx_models起别名多花三秒省掉半小时的“为什么只有一个模型跑了”。2.2 数据集配置的四件套重点在 reader 和 infer单个数据集配置里真正需要关注的是四块元信息、reader_cfg、infer_cfg、eval_cfg。元信息是abbr缩写决定输出目录名和path数据路径。reader_cfg告诉框架从原始数据里取哪些列当输入、哪一列是标准答案。infer_cfg是重头戏包含 prompt 模板、检索器和推理器。eval_cfg决定怎么判分。reader_cfg的写法值得单独说一句因为它是最容易配错又最难发现的地方reader_cfg dict( input_columns[question], output_columnanswer, train_splittest, )input_columns里的列名必须和数据集实际字段严格一致一个字母都不能差。有些数据集里字段叫question有些叫input有些叫prompt你从 A 数据集抄配置到 B 数据集最容易漏改这里。表现是推理阶段拿到空字符串模型对着空白输出一段废话然后准确率 0。我在第一次跑的时候就中招了日志里全是空 question排查了快四十分钟才回头看到这行。infer_cfg里的prompt_template决定了对话格式。对书生浦语这类 chat 模型模板必须带角色标记infer_cfg dict( prompt_templatedict( typePromptTemplate, templatedict( round[ dict(roleHUMAN, prompt{question}\n请只回答选项字母。), dict(roleBOT, prompt{answer}), ], ), ), retrieverdict(typeZeroRetriever), inferencerdict(typeGenInferencer, max_out_len512), )这里的 role 写法在不同版本里有HUMAN/BOT和user/assistant两种取决于后端 tokenizer 的 chat 模板实现。如果后端会自动套 chat template那配置里只要给出纯文本内容即可如果不会就得靠模板里显式的角色标记来构造。这一点必须在接模型那一步确认清楚否则会出现“模型回答得很好但格式完全不对”的诡异现象。2.3 后处理与指标0 分的一大半来自这里eval_cfg里通常有两个东西evaluator和pred_postprocessor。后者是把模型输出的一段自由文本规整成可比对答案的函数比如“取第一个大写字母”“提取第一行”“取最后一个数字”。举个具体场景。C-Eval 这类选择题标准答案是A模型输出是“根据上述分析正确答案应该是A因为……”。这时候如果后处理只做了 strip字符串比对必然失败这道题就判错。而实际上模型答对了。所以准确率低到离谱时第一反应应该是去看后处理而不是怀疑模型。我后来养成一个固定动作跑完先随机抽十条predictions里的样本人工对一遍标准答案估算一个“人眼准确率”再和报告里的数字比。两者差距大说明是判分链路的问题两者接近说明模型真的就是这个水平。这个动作只要五分钟能挡掉后面所有的自我怀疑。3. 环境准备与模型接入的实操细节3.1 显存怎么估别靠试显存估算有个够用的经验公式权重占用量约等于参数量乘以每参数字节数fp16/bf16 是 2 字节int4 量化约 0.5 到 0.6 字节。所以 7B 模型 bf16 加载大约 14GBint4 大约 4 到 5GB20B 模型 bf16 大约 40GB。然后是 KV cache它跟批大小和序列长度线性相关粗略公式是2K 和 V× 层数 × 隐藏维度 × 序列长度 × 批大小 × 字节数。以 7B 模型、32 层、隐藏维度 4096、max_seq_len2048、batch_size8、bf16 计算单条样本的 KV cache 约 2×32×4096×2048×2 字节约等于 1GB八条就是 8GB。加上权重 14GB再加上框架和激活的几百 MB 到 1GB 开销24GB 卡跑batch_size8是有余量的但余量不大序列长度再往上加就会 OOM。模型规模精度权重大致占用batch8 时 KV cache 估算推荐单卡显存7Bbf16约 14GB约 8GBseq 204824GB 起步7Bint4约 5GB约 8GBseq 204816GB 可试20Bbf16约 40GB约 12GBseq 204880GB 或多卡20Bint4约 12GB约 12GBseq 204840GB 可试注意表里 KV cache 是按batch_size8单批次算的推理框架如果开了分页注意力再省一些如果开了连续批处理则会动态涨落。显存不够时优先级是先降batch_size再降max_seq_len最后才考虑量化——量化会引入精度损失评测场景下这会污染结论。3.2 安装与目录摸底环境上我一般用独立虚拟环境避免和训练环境里的 torch、transformers 版本打架。装完之后第一件事不是跑而是摸目录。configs/下按datasets/、models/、summarizers/分好run.py是入口tools/下有几个救命的小脚本其中列表查询那个最常用# 查有哪些与 internlm、ceval 相关的现成配置 python tools/list_configs.py internlm python tools/list_configs.py ceval这个脚本会打印出配置名和对应的文件路径字段里有缩写和数据集名。别小看它我第一次全靠猜文件名浪费了很多时间。用它可以确认当前版本库里到底支持哪些数据集避免抄了一份不存在的配置名然后 import 报错。依赖层面有两个隐性要求值得提前确认一是 transformers 版本太老会不认最新的模型实现二是分词器的trust_remote_code书生浦语的权重走的是自定义实现配置文件里必须显式打开否则会在加载阶段直接抛错。这两点如果不提前处理会在你以为是评测逻辑问题时被误导很久。3.3 接入本地权重一份可复用的模型配置作业环境里权重通常在本地磁盘上直接写绝对路径最省事。下面这份配置我用了很多次改动点只有三个路径、显存切分方式、输出长度。from opencompass.models import HuggingFace models [ dict( typeHuggingFace, abbrinternlm-chat-7b-hf, path/path/to/internlm-chat-7b, tokenizer_path/path/to/internlm-chat-7b, model_kwargsdict( trust_remote_codeTrue, device_mapauto, torch_dtypebfloat16, ), tokenizer_kwargsdict( trust_remote_codeTrue, padding_sideleft, truncation_sideleft, ), max_out_len512, max_seq_len2048, batch_size8, run_cfgdict(num_gpus1, num_procs1), generation_kwargsdict( do_sampleFalse, temperature1.0, ), ), ]几个参数值得展开。padding_sideleft在批推理里几乎必须因为自回归生成从左往右右边 padding 会导致生成位置错位输出乱码或者重复。do_sampleFalse是评测的基本要求贪心解码保证同一份配置跑两遍结果一致任何带采样的评测都是在给自己制造噪声。num_gpus1和num_procs1要根据实际卡数改这个值和命令行传的--num-gpus对不上时就会出现“只用了一张卡”的浪费。如果权重比较大单卡放不下可以把device_map改成auto让 accelerate 自动切分或者改用支持张量并行的推理后端。后者的吞吐明显更高配置里换一下type和几个字段就行数据侧完全不用动这正是前面说的分层解耦带来的便利。注意路径里不要带中文和空格。我遇到过因为目录名里有空格导致子进程解析参数失败的情况表现是任务启动后立刻退出日志里只有一行语焉不详的报错。4. 从冒烟测试到全量评测的完整流程4.1 先跑通一条再谈规模直接上全量是最常见的自杀式操作。一个中等规模的数据集加七 B 模型单卡跑几个小时很正常等你发现配置有错时间已经喂进去了。所以第一步永远是冒烟。大多数版本支持--debug之类的开关作用是每个数据集只取极少量样本把整条链路走一遍。命令结构大致是python run.py configs/eval_internlm_chat_7b.py \ -w outputs/smoke \ --debug冒烟阶段我关注三件事而不是分数第一predictions里的输出是不是像人话有没有格式跑偏第二每道题的预测和标准答案是否肉眼可比对第三整条链路耗时是否在可接受范围据此推算全量要多久。这三件事花十分钟能省掉后面几小时的重跑。冒烟通过之后再逐步放大先单个数据集全量确认这个数据集的判分逻辑没问题再上组合。如果组合跑出来某个数据集分数异常你至少知道不是全局配置的锅。4.2 并行策略模型并行和任务并行是两回事这一点必须掰开讲因为混淆它会导致严重的资源浪费或 OOM。模型并行是把一个大模型切成几份放到多张卡上解决“单卡装不下”的问题任务并行是同时跑多个数据集或多个模型解决“总时长太长”的问题。前者要模型真的装不下才用后者在卡够多的时候收益极大。# 场景一单卡装得下多任务并行 python run.py configs/eval_internlm_chat_7b.py \ -w outputs/full \ --num-gpus 8 \ --max-num-workers 8 # 场景二大模型单卡装不下单任务多卡切分 python run.py configs/eval_internlm_20b.py \ -w outputs/full_20b \ --num-gpus 4max-num-workers控制的是任务级并发数每个 worker 会占一组卡。如果模型配置里run_cfg写的是num_gpus1那么 8 张卡配max-num-workers8就是八个任务同时跑效率最高。但如果你把num_gpus写成 2实际只有 4 组能用另外 4 张卡闲着而且框架不会提醒你。还有一个容易被忽略的参数是复用。全量评测跑到一半崩了或者你只想改判分规则不想重跑推理这时候开启复用开关就能直接读已有的predictions重新算分几秒钟出结果。我第一次不知道有这个改一个后处理正则重跑了两小时现在想起来还觉得亏。4.3 产物目录怎么读报告看哪几列跑完之后工作目录里一般有这几块outputs/full/ ├── configs/ # 本次实验的完整配置快照 ├── logs/ # 推理与评测日志 ├── predictions/ # 每个模型每个数据集的原始生成结果 │ └── internlm-chat-7b-hf/ │ └── ceval/ │ └── ceval.json ├── results/ # 每个数据集的判分细节 │ └── internlm-chat-7b-hf/ │ └── ceval/ │ └── score.json └── summary/ # 汇总表CSV 和 Markdown 两份读报告的顺序我总结成“三看”先看summary里的总分列建立整体印象再看results里按学科或按子任务拆分的细分行找短板比如理工类明显低于人文类往往指向训练数据配比问题最后回到predictions抽样本验证判分是否正确。顺序不能反一上来就抽样本会淹没在细节里。score.json里通常除了准确率还有样本数这两个要一起看。如果一个子集只有十几道题那它的准确率波动极大90% 和 70% 之间可能只是三道题的差别不能当结论。做横向对比时我只对样本数超过一定阈值的子集下判断小样本子集只在方向性上参考。configs目录的价值在于归档。做版本对比时先把两次实验的配置快照 diff 一遍能立刻确认到底改了模型还是改了 prompt。我有一次兴冲冲地比较两个版本的提升最后发现只是数据集版本从 v1 换到了 v2白高兴一场。5. 常见问题与排查实录5.1 一份我自己的排查速查表下表是几次作业里踩坑汇总按“现象—根因—处置”组织遇到问题先对号入座比漫无目的地翻日志快得多现象最可能的根因处置方式准确率接近 0但输出内容看着像在答题后处理抽取规则与输出格式不匹配打开 predictions 看原始输出调整 pred_postprocessor 的正则或截取逻辑输出是空白或者只有标点reader_cfg 的 input_columns 字段名错了打印一条原始数据字典核对字段名chat 模型输出自我解释、长篇大论prompt 模板未带角色标记或后端未套 chat template确认后端是否自动套模板否则在 template 里显式写角色启动即 OOMbatch_size 或 max_seq_len 过大先降 batch_size再降 max_seq_len八张卡只吃到一张模型配置的 run_cfg.num_gpus 与命令行参数不一致两边对齐模型装得下就都设 1靠 max-num-workers 并发任务卡住长时间无输出数据路径不可达或数据加载阻塞本地放一份小数据先验证路径再看日志是否停在加载阶段结果里出现乱码或重复词padding_side 设置错误左侧 padding且 attention mask 正确传递同一配置两次跑分数不同开启了采样解码关闭采样用贪心解码保证可复现与官方榜单分数差距明显few-shot 数量、prompt 版本、数据版本不同逐项比对配置一次只改一个变量改一处配置后全部重跑未开启结果复用启用复用开关只重跑受影响的数据集5.2 三个值得单独讲的案例第一个是“后处理把对的判成错的”。当时 C-Eval 准确率只有个位数我第一反应是模型有问题因为输出看起来确实在认真答题。打开 predictions 一看模型输出“正确答案是 A”标准答案是A但后处理器做了严格的相等比较带着前面的中文前缀自然不相等。改成正则提取第一个独立的大写字母之后准确率直接跳到正常区间。这件事之后我形成一个原则任何异常的分数先验证判分链路再怀疑模型。第二个是“多进程之间的数据竞争”。同时跑多个数据集时偶尔会有一个任务挂掉日志里没有明确错误。后来定位到是数据加载在同一个共享目录里产生了冲突把工作目录按任务分开之后就消失了。这类问题很隐蔽因为它不复现——同样的命令重跑一次可能就好了容易让人以为是偶发。判断依据是如果失败总是发生在并发度高的时候、且失败的任务不固定那大概率是共享资源冲突。第三个是“量化后分数掉了但不知道该不该信”。为了省显存用了 int4跑出来分数比 bf16 低了一截。这到底是量化损失还是评测噪声我的处理方式是固定数据集和 prompt只切精度跑两遍看差距是否可重复。结果是差距可重复且集中在需要多步推理的任务上那就说明确实是量化影响了推理链条而不是随机波动。这个结论直接影响了后续部署选型。5.3 我的几条避坑心得第一日志一定要留着。每次实验的logs目录别删出问题时那是唯一的现场。第二一次只改一个变量。同时改 prompt 和 batch_size跑出来分数变了你没法归因。第三小数据先验证。任何新的数据集配置先用几十条样本把链路跑通再上全量。第四把耗时记下来。知道“这个数据集单卡要跑四十分钟”之后你才能合理安排并行度和等待时间不至于一晚上都在盯进度条。提示如果某个任务耗时远超预期先别急着加卡。看max_out_len有没有设得过大模型对每道题都生成几百个 token时间全耗在无意义的续写上。这个参数调小往往比加卡有效。6. 结果到手之后怎么用才不算白跑6.1 分数波动的归因路径同一份权重、同一份数据两次跑出来的分数不一致是很常见的事盲目相信任何一次结果都会误导决策。归因我按这个顺序查先看解码方式有没有采样、温度是不是 0再看 prompt 版本有没有改过措辞或者 few-shot 数量然后看数据版本评测集有没有更新接着看后处理正则改没改最后才看环境依赖版本、硬件差异。实测下来波动的大头在 prompt 和数据版本环境的影响反而小。所以做版本对比时我会把 prompt 文件和数据路径都写进实验记录跟配置快照放一起。这样一旦发现差异五分钟就能定位到是哪个变量动了。6.2 把评测接进日常迭代的节奏全量评测又慢又贵不适合每次都跑。我的做法是分两层一层是“回归集”从各个数据集里抽几百条覆盖面尽量广跑一次十几分钟每次模型有改动都跑另一层是“全量集”只在里程碑节点跑比如完成一轮微调之后。回归集的作用是快速发现退化全量集的作用是给出对外可比的数字。回归集的构建也有讲究。不能随机抽要保证每个能力维度都有样本否则某个能力退化了却抽不到对应的题。我的做法是按子任务分层抽样每个子任务至少留几十条。这套机制建立起来之后模型迭代的节奏明显顺畅了因为不用每次都等几小时才敢确认改动没搞坏东西。还有一点是主观评测。客观题能自动判分但对话质量、指令遵循这类没法靠字符串比对。这时候需要裁判模型来打分配置上多一个裁判模型的指定。裁判模型本身的选择会影响结论所以同一批对比实验里裁判必须固定不能这次用这个下次用那个否则分数之间没有可比性。我个人在实际操作中的体会是评测最大的价值不在于那个最终的总分而在于它逼你把“模型到底擅长什么”这个问题想清楚。为了配一份可信的评测你必须去读数据集的构造方式、去理解每道题在考什么、去判断一个错误是模型的问题还是流水线的问题。这套思考过程走一遍比看十份榜单都有用。后面如果还想往下走可以试着把评测结果按能力维度做可视化归因或者干脆自己按业务场景补一个小型私有评测集——官方基准告诉你模型行不行私有评测集才告诉你它在你这件事上行不行。
阅读完成 · 觉得有帮助?