- perf_stats aggregator lives in eval/, not model/: the import failed silently and EVERY perf column was empty (not just ttft). Now warns on stderr instead of swallowing. - repeats > 1 get their own checkpoint key (:rep2, :rep3, ...): repeat 2 previously restored repeat 1's predictions and finished instantly with identical scores. rep1 keeps the legacy key (existing checkpoints still resume). - repeats summary: report the MEAN score and aggregate time/tokens over ALL runs (was: last run only). - README: six-benchmark command as the primary example. Co-Authored-By: Claude <noreply@anthropic.com>
177 lines
13 KiB
Markdown
177 lines
13 KiB
Markdown
# EvalHarness 对标 evalscope 全程踩坑记录
|
||
|
||
> 28 bench 双框架对比过程中发现的所有问题(我方 bug / es 方 bug / 口径差),按层归类。
|
||
> 每条含:现象 → 根因 → 修复 → 验证结果。
|
||
|
||
---
|
||
|
||
## 一.5、fix2 轮新增修复(2026-09-02 晚,prompt 逐字对齐 + 基建)
|
||
|
||
16.5. **es bfcl 单轮全 0 = 依赖版本崩溃**(已帮修,`evalscope/.../bfcl/v3/utils.py`)
|
||
- 现象:es bfcl simple/multiple/parallel 等 10 个单轮类全部 0 分,irrelevance 类却 0.9+
|
||
- 根因:utils.py `convert_format_language()` import `bfcl_eval.constants.enums`(BFCL v4 新版枚举),环境装的是 bfcl_eval 2025.8.6.2(旧版,ast_checker 直接比较字符串 "Java"/"JavaScript"/"Python")→ ModuleNotFoundError → except 吞掉 → 全部判 `ast_decoder:decoder_failed`
|
||
- 修复:utils.py 两个 convert 函数改为版本兼容(try 新版枚举,fallback 旧版字符串,注意 JavaScript 大写 S)
|
||
- 验证:es bfcl 20/类 0.1736 → **0.3563**(simple 0.55、live_simple 0.7、parallel 0.45-0.5 全部起来)
|
||
- 遗留:我方 0.266 vs es 修复版 0.356 仍差 0.09——我方 bfcl_mock 的官方后端转换细节(underscore_to_dot 等)待对齐
|
||
|
||
17. **多段 pool spec range 替换 bug**(`model/runner.py::_make_adapter`)
|
||
- 现象:多段逗号池(105+138+140+143 共 20 口)全军覆没 ConnectError,单段池正常;curl 同口 200
|
||
- 根因:`re.sub(r'\{..\}', port, u, count=1)` 每次只替换**最前**的 range → 20 个成员 URL 全带字面量 `{8200..8203}`,httpx 连畸形 host 必败
|
||
- 修复:按逗号拆段,每段独立 search+替换自己的 range
|
||
- 验证:20 成员展开正确,4/4 请求成功;停滞的 mmlu_pro 恢复 1 samples/s
|
||
18. **网络抖动三层自愈**(集群路由分钟级断通交替,17:07-18:15 反复)
|
||
- adapter:httpx.Timeout(connect=15, read=300, write=30, pool=15)——半开 TCP 快速失败
|
||
- pool:实例级 failover(已有)
|
||
- runner.run_one:6 次退避重试(10/20/40/60/90s)跨过断网窗口——gather 不再被单点异常炸掉
|
||
19. **prompt 逐字对齐 10 处**(双侧渲染 diff 器验证,全部 IDENTICAL):
|
||
- mmlu 域匹配失效(subject/category 键名不一致 → 全局前 5,es 是每科目 5 范例)
|
||
- mmlu_pro 三处(header 应在范例前 / cot_content 变换单 ANSWER / 测试选项 `A) x`)
|
||
- bbh 测试题缺 `Q:/A: Let's think` 包装
|
||
- aime 缺尾部 `Remember to put your answer inside \boxed{}.` + 前导 `\n`
|
||
- gsm8k `\boxed{}.` 句号;math fewshot 单换行分隔;imo/hmmt/cmmlu/trivia 尾换行
|
||
- lcb 缺 `### Format:` 前缀 + starter_code 进代码块 + expert 头移到 system role
|
||
- hle judge prompt 换 es 逐字版 + 三态契约移 system role
|
||
- trivia `list(str)` 拆字符隐患
|
||
20. **es limit 是 per-subset 语义**:es mmlu limit=200 = 每科目 200 = 全量 14042!同题跑法必须 per-subset k(mmlu 4/科目=228、cmmlu 3、hle 25、imo 25、lb2 66/长度)
|
||
21. **gfc ckpt 版本漂移**:数据镜像重物化后 metadata 变化 → key_for 失配 restored 0;同口径 f1 直接从本次 report.samples 算更稳
|
||
|
||
---
|
||
|
||
## 一、我方框架 bug(16 个,全部已修)
|
||
|
||
### 判分 / 聚合层
|
||
|
||
1. **few_shot_hook 从未挂载**(`data/registry.py`)
|
||
- 现象:bbh 全程 0-shot(无官方 3-shot CoT),15 个 MCQ 子集接近 0 分
|
||
- 根因:`register_dataset` 装饰器从不把模块级 `<name>_few_shot` 函数挂到 provider 上,runner `getattr(prov, 'few_shot_hook')` 永远 None
|
||
- 修复:装饰器里 `factory.__globals__.get(f'{spec.name}_few_shot')` 自动拾取(注意 `__globals__` 是 dict 要用 `.get` 不是 `getattr`)
|
||
- 验证:bbh 27子集×10 = 0.819 vs es 0.8185
|
||
|
||
2. **MCQ 抽取格式不匹配**(`eval/recipes/qa.py`)
|
||
- 现象:bbh 全部 MCQ 子集判分全 miss
|
||
- 根因:`mcq_letter` 返回裸字母 `'A'`,target 是 `'(A)'`
|
||
- 修复:bbh recipe 按 target 形态格式化为 `f'({val})'`
|
||
|
||
3. **llm_judge 标签子串扫描**(`eval/scorer.py`)
|
||
- 现象:hle 虚高 1.0、simple_qa 解析错乱
|
||
- 根因:`label in upper` 全文子串匹配——judge 解释文本里 "CONSISTS" 命中 'C'、"ANSWER" 命中 'A'
|
||
- 修复:三级解析——`label_pattern` 正则(如 `GRADE:\s*([CI])`)→ 尾行词边界匹配 → 全文词边界兜底
|
||
- 验证:hle 1.0→0.031(es 0.050 同族)、simple_qa 见下条
|
||
|
||
4. **dict 聚合器主指标被派生值均值覆盖**(`eval/runner.py`)
|
||
- 现象:simple_qa 真值 0.035 被报成 0.403
|
||
- 根因:`simpleqa_official` 返回 5 个派生指标 dict,代码取 `mean(全部值)` = mean(0.035, 0.945, 0.02, 0.98, 0.036) = 0.403
|
||
- 修复:dict 展开时主指标取**同名键** `out[metric]`,无同名键才退化均值
|
||
- 验证:simple_qa 同题 0.050 vs es 0.0503
|
||
|
||
5. **judge adapter 拿不到 API key**
|
||
- 现象:judge 全 401
|
||
- 根因:launcher 没 export OPENAI_API_KEY(spec URL 里不含 key)
|
||
- 修复:启动脚本统一带 key
|
||
|
||
### 配对 / 选样层
|
||
|
||
6. **双重 `_apply_limits` 依赖原地 shuffle 别名**(`model/runner.py`)
|
||
- 现象:per-task 截断时判分配对错位(mmlu 一度 0.275)
|
||
- 根因:run_eval 和 generate_predictions 各调一次 `_apply_limits`,旧实现靠「in-place shuffle 使两处列表别名同步」侥幸正确;per-group 重写返回新列表后别名断裂 → samples 顺序 ≠ preds 顺序
|
||
- 修复:run_eval 用确定性重算得到与 generate_predictions 完全一致的 work 列表再 `zip` 判分
|
||
- 验证:mmlu 恢复 0.730+
|
||
|
||
7. **checkpoint key 不含 subset**
|
||
- 现象:lb2 三个长度互相污染缓存
|
||
- 修复:ckpt 名加 `:{subset}` 后缀
|
||
|
||
8. **checkpoint key 不含 prompt / 生成参数**
|
||
- 现象:改 prompt 模板后重跑,复用旧模板的预测(hswag 渲染实验多次被坑;humaneval 指令头实验同)
|
||
- 教训:**改任何影响 prompt 的东西必须删 ckpt**(此后每次模板修改都显式 `rm ckpt`)
|
||
|
||
9. **`no_shuffle` 没管到 per_task 分支**
|
||
- 现象:以为在跑「同题前 N」,实际 per-group 内仍 Random(42).shuffle 取前 N(hswag 同题实验一度假对齐)
|
||
- 修复:per_task 分支尊重 `shuffle=False`(组内不洗直接前 N)
|
||
|
||
10. **选样语义与 es 不同**(`model/runner.py`)
|
||
- es:**每个 subset 独立** `Random(42).shuffle` 后取前 N
|
||
- 我方旧实现:全局 shuffle 后按组截断 → 同池不同题
|
||
- 修复:per_task 时按组分组、组内独立 Random(42)、组序内取前 N
|
||
|
||
### 生成 / prompt 层
|
||
|
||
11. **`/no_think` 裸拼污染题面**
|
||
- 现象:prompt 尾部最后一个选项后面跟 ` /no_think` 字样
|
||
- 根因:Qwen3 软开关实现把标记 append 到最后一条 user 消息
|
||
- 修复:普通请求改 `chat_template_kwargs: {enable_thinking: false}`(服务端验证支持);tools 请求保留软开关(模板 400 规避)
|
||
- 验证:gpqa 0.263 → 0.460(es 0.480)
|
||
|
||
12. **MCQ 选项渲染字符级差异**(strict/cot_letter 分支)
|
||
- 现象:hswag 同题下 `A.` 渲染 = 0.7825、`A)` 渲染 = 0.565——**两个字符差 22 分**(no-think Qwen3 对格式极端敏感)
|
||
- 修复:所有 MCQ 分支逐字对齐 es:`A) 选项` + `one of A,B,C,D`(mmlu_pro 例外:es 用 `A ` 空格分隔)
|
||
- 验证:wino 0.655 = es 0.655;hswag 对齐中
|
||
|
||
13. **prompt 模板逐 bench 缺失/走样**(最大一类,累计 12+ 处)
|
||
- mmlu/cmmlu/mmlu_pro:我方 strict「整个回复只能是 ANSWER」 vs es「Think step by step + last line ANSWER」→ 全部对齐(mmlu 0.730→0.795,es 0.800)
|
||
- few-shot 渲染:es 范例=裸题+`A)`+`ANSWER:`,我方=Question:/A./Answer: → 对齐 + **按科目域匹配范例**(es biology 题配 biology 范例)
|
||
- mmlu_pro:es 专属模板(`Question:/Options:/A x` + CoT 范例 + 按域 header)→ 0.471→0.579(es 0.629)
|
||
- gsm8k:es 范例=题+`Reasoning:`全文+`ANSWER: \boxed{}` → runner 加 reasoning 元数据渲染分支
|
||
- math:es 范例=`Problem:/Solution:` 裸答案 → 同上
|
||
- aime×3:es=「Solve the following math problem step by step. Put your answer inside \boxed{}.」**指令前置**,我方是题后 suffix → 加 `aime_es`
|
||
- imo/hmmt26:es=`Problem:\n{题}\n\nPlease reason boxed`(**Problem: 前缀**),我方裸题 → 套 `imo_es`(imo 0.138→0.19;hmmt 验证中)
|
||
- simple_qa:es=`Answer the question:\n\n{q}` 且**无 Answer 行契约**,我方自加契约 → `simple_qa_es`
|
||
- trivia:es 开放卷(`Content: {wiki证据列表}` + es 模板),我方闭卷(镜像键名读错 `wiki_content`≠`wiki_context`)→ 修键名+开模板(0.555→0.700,es 0.660)
|
||
- lb2:es=`<text>` 包装 + CoT 契约,我方裸文档+strict → `lb2_es`
|
||
- humaneval:es 指令头("Read the following function signature...")→ 加了头后 **harness 仍用 input 拼程序**导致指令进 main.py 全 0 → metadata 存原始 prompt,harness 用 metadata 版
|
||
- LCB:我方无代码契约(模型输出 LaTeX 数学推导!)→ 官方 system+`### Question:/### Answer:`+```python 格式 → `lcb_es`
|
||
- drop:es=`Think step by step + "Answer: [ANSWER]"` → prompt_suffix;范例=完整 Passage 式
|
||
|
||
14. **tau2 env 内部调用不传生成参数**(`agent/envs/tau2_official.py`)
|
||
- 现象:agent 调用走 adapter 默认 4096 tokens/无温度
|
||
- 修复:`run_task(gen_kwargs=)` 透传(temp 0/16k;16k+长对话会超 131072 上下文 → 8192)
|
||
|
||
15. **tau2 reward 读不存在的字段**
|
||
- 根因:读 `environment_reward/communication_reward`,tau2 官方 reward_info 字段是 `reward/db_check/...` → 恒 0
|
||
- 修复:取 `rewards['reward']`(es 同款官方综合分)
|
||
|
||
16. **HF 下载同步阻塞事件循环**
|
||
- 现象:18 任务并发时全卡死(py-spy 见主线程 `_hf_download`)
|
||
- 修复:`asyncio.to_thread` 物化
|
||
|
||
### 基础设施
|
||
|
||
- 根盘写满(Errno 28)两次:HF 缓存迁移 /data2、清 modelscope 196G、truncate docker 巨型日志
|
||
- `pkill -f <pattern>` 会误杀自己所在 shell(cmdline 含 pattern)→ 用 launcher 文件 + 短命令
|
||
- bash 工具 timeout 会杀未及 detach 的后台任务 → `nohup ... < /dev/null & disown` + 立即返回
|
||
- checkpoint 恢复打印 `restored N (M to generate)`:N≠M 命中时警惕 key 失配(多次救场信号)
|
||
|
||
---
|
||
|
||
## 二、es(evalscope)侧问题(代码级证据)
|
||
|
||
1. **tau2 崩溃**:`tau2_bench/generation.py:118` `openai_chat_choices(include_reasoning=False)` 丢弃 Qwen3 思考通道 → 空 content → 官方引擎 `message.py:116` 校验 raise(temp=0 确定性复现)
|
||
2. **gfc 误判**(已帮修):`general_fc_adapter.py` `is_call_tool = finish_reason=='tool_calls'`,vLLM/Qwen3 输出 tool_calls 时 finish_reason 常为 'stop' → FN 虚高。patch:`or bool(tool_calls)`
|
||
3. **BCB 执行器空跑**(已帮修):`bigcodebench_adapter.py` `passed = status=='success'`——沙箱命令完成≠测试通过,198/200 空输出判对(0.99 虚高)。patch:unittest 显式 runner + `__BCB_RESULT__ PASS` 标记 → 0.365(与我们 0.375、交叉判分三方一致)
|
||
4. **LCB 本地执行全体 -2**:`use_sandbox=false` 本机跑 codegen_metrics 全失败(环境缺依赖);docker sandbox 修复后 0.413 有效
|
||
5. **gfc 0.0 失败运行残骸**:断连+空回复产物;重跑 0.456
|
||
6. **swe 无评测断点**:198/200 评完后报告步骤遇断连作废,重跑=整段 4h(我们用日志聚合出真值 0.0)
|
||
|
||
---
|
||
|
||
## 三、口径差(非 bug,须同口径才可比)
|
||
|
||
| 类别 | 内容 |
|
||
|---|---|
|
||
| **选样** | 双方 shuffle42 的 base 行序不同 → 不同题集。统一方案:双侧 `shuffle=False` 取原始前 N(数据源行序一致,已验证 400/400 同题)|
|
||
| **金标集合** | drop:es=answer+validated_answers 合并(任一算对),我们镜像无该字段 → 严 ~0.065 |
|
||
| **指标定义** | gfc:es 报 tool_call_f1,我们 acc(已重算同口径);imo:judge vs math_equal(已统一 judge:0.188 vs 0.198)|
|
||
| **数据构成** | math 全量:我们随机 200(易题占比高)vs es 5 Level 均衡——对齐档 L×N 才可比 |
|
||
| **执行环境** | bfcl/gfc/tau2:es+Qwen3 的 agent 栈兼容性差;LCB:判定器不同(官方 codegen_metrics vs 自写 runner)|
|
||
| **截断** | lb2 文档中位 41.7 万字符天然超 131072——head+tail 截断双方同构(非 bug)|
|
||
|
||
---
|
||
|
||
## 四、方法论教训
|
||
|
||
1. **分数不一致时先做同题实验**(同 prompt 双跑 / 同题双侧)——hswag 0.2 之谜三小时才定位到两个字符
|
||
2. **交叉判分**(A 的输出 × B 的判分器)是分离「判分层差 vs 生成层差」的唯一利器
|
||
3. **隔离变量实验**:同题×同 prompt×只换渲染 → 定位 `A.` vs `A)`
|
||
4. **no-think 模型对 prompt 字符级格式极端敏感**(22 分级别的摆动)
|
||
5. ckpt key 必须覆盖 prompt 语义,否则改模板=白改
|