【Bug已解决】Track git dirty state in method_comparison benchmark results 解决方案
【Bug已解决】Track git dirty state in method_comparison benchmark results 解决方案
一、现象长什么样
在跑 PEFT 的method_comparison基准(对比 LoRA / AdaLoRA / IA³ 等方法的效果)时,结果文件里只记了方法名和分数,没有记跑这组结果时代码处于哪个 git 状态。于是出现:
- 两周后回头看
results.json,不知道当时跑的是哪个 commit; - 本地改了一行没 commit,又跑了一次,分数变了,却和已记录的结果混在一起,无法区分;
- CI 里自动跑的 benchmark 和本地手动跑的对不上,因为本地工作区是 dirty 的。
典型的结果文件:
{"method": "lora", "score": 0.842}缺了最关键的一条元数据:git commit + 是否 dirty。本文讲如何在 benchmark 结果里把 git 状态钉死,让每一行结果都可追溯。
二、背景
method_comparison是 PEFT 仓库里对比不同参数高效微调方法的脚本。基准比较最怕的不是“算错”,而是“同样的代码,不同人不同时间跑出来不一样,却说不清差异来源”。最大的不可控变量就是代码版本:
- 你本地
git checkout在某个 feature 分支,改了半行没提交; - 同事在他机器上跑的是
main的最新 commit; - CI 跑的是某个 PR 的 head。
如果结果文件不记录 git 状态,这些结果就失去了可比性。解决办法是在 benchmark 入口处,用代码自动读取 git 的 commit hash 和 dirty 标志,写进结果元数据。
git rev-parse HEAD→ 当前 commit;git status --porcelain→ 有输出说明工作区 dirty;git branch --show-current→ 当前分支。
把这三个值连同结果一起落盘,问题的可追溯性立刻解决。
三、根因
根因 A:benchmark 脚本根本没采集 git 元数据
最直接原因。method_comparison原脚本只输出方法名和指标,没接 git 状态采集,导致结果不可追溯。
根因 B:只记 commit 不记 dirty
有些实现记了git rev-parse HEAD,但没记 dirty。一旦本地有未提交改动,commit 相同但代码不同,结果仍会偏差且无法察觉。dirty 标志是“不可信结果”的红色警报。
根因 C:把 git 状态采集写死成依赖 gitpython
引入GitPython作为重依赖,CI 环境没装就报错,反而让 benchmark 跑不起来。应当优先用subprocess调git命令行,零额外依赖。
根因 D:结果文件覆盖而非追加,历史丢失
每次跑都把results.json覆盖,之前的结果(含旧 git 状态)全没了,无法横向对比。
根因小结
- benchmark 结果必须记录 commit + dirty + branch;
- 只记 commit 不够,dirty 才是“结果可能不可信”的关键信号;
- 用 subprocess 调 git,别强加重依赖;
- 结果应追加而非覆盖,保留历史快照。
四、最小可运行复现
下面脚本演示如何采集 git 状态并写进 benchmark 结果,不依赖任何第三方库:
import subprocess import json import hashlib from pathlib import Path def git_state(repo: str = ".") -> dict: def run(args): try: return subprocess.run( ["git", "-C", repo, *args], capture_output=True, text=True, check=True ).stdout.strip() except subprocess.CalledProcessError: return "unknown" commit = run(["rev-parse", "HEAD"]) branch = run(["branch", "--show-current"]) or "detached" porcelain = run(["status", "--porcelain"]) dirty = bool(porcelain.strip()) return { "commit": commit, "branch": branch, "dirty": dirty, "dirty_files": porcelain.splitlines()[:5], # 最多记 5 个改动文件 } def record_result(method: str, score: float, out_path: str = "results.jsonl"): state = git_state(".") row = {"method": method, "score": score, **state, "row_id": hashlib.sha1(f"{method}{commit}".encode()).hexdigest()[:8]} with Path(out_path).open("a", encoding="utf-8") as f: # 追加,保留历史 f.write(json.dumps(row, ensure_ascii=False) + "\n") print("已记录:", row) if __name__ == "__main__": record_result("lora", 0.842) record_result("adalora", 0.851)运行后results.jsonl每行类似:
{"method": "lora", "score": 0.842, "commit": "a1b2c3d...", "branch": "main", "dirty": false, "dirty_files": [], "row_id": "9f3a1c2b"}如果本地有未提交改动,会看到"dirty": true和具体文件,一眼识别“这条结果不可信”。
五、解决方案(第一层:最小直接修复)
在method_comparison的入口最前面调用一次git_state(),把状态注入每条结果。最小改动:
# method_comparison.py 顶部 from your_utils import git_state GIT = git_state() # 进程启动时采集一次 def evaluate(method): score = run_benchmark(method) return {"method": method, "score": score, **GIT} # 结果带上 git 状态如果dirty为 True,直接在日志里高亮警告:
if GIT["dirty"]: print(f"[WARN] 工作区有未提交改动 {GIT['dirty_files']},本次结果可能不可复现!")这样所有结果天然带上来源,问题立刻可追溯。
六、解决方案(第二层:结构性改进)
6.1 用环境变量/CI 变量补充来源
CI 里除了 git,还能记录流水线信息:
import os def ci_meta(): return { "ci": os.environ.get("CI", "false"), "job": os.environ.get("GITHUB_RUN_ID", "local"), "python": os.sys.version.split()[0], }6.2 把 git 状态做成 pytest fixture 自动校验
import pytest @pytest.fixture def forbid_dirty_git(): out = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True).stdout.strip() if out: pytest.skip("工作区 dirty,跳过可信 benchmark(避免污染结果库)") yield这样本地脏工作区跑 benchmark 会被自动 skip,结果库只收干净状态的数据。
6.3 结果文件用 jsonl 追加 + 版本化
不要用单文件 json 覆盖,用 jsonl 逐行追加,并定期git add results.jsonl把快照固化进版本库。
七、解决方案(第三层:断言 / CI 守护)
加 pytest,确保任何结果都带 git 元数据,且可信状态可断言:
import json import pytest def test_result_has_git_meta(path="results.jsonl"): rows = [json.loads(l) for l in open(path, encoding="utf-8")] assert rows, "没有结果" for r in rows: assert "commit" in r and r["commit"] != "unknown" assert "dirty" in r def test_no_dirty_in_release_results(path="results.jsonl"): rows = [json.loads(l) for l in open(path, encoding="utf-8")] dirty = [r for r in rows if r.get("dirty")] if os.environ.get("REQUIRE_CLEAN") == "1": assert not dirty, f"存在 dirty 结果,禁止入库: {dirty}"接进 CI,发布前检查REQUIRE_CLEAN=1,dirty 结果一律拦截。
八、排查清单
结果不可追溯时查:
- 结果文件有没有 commit 字段?没有就加
git rev-parse HEAD。 - 有没有 dirty 标志?只有 commit 不够,dirty 才是“不可信”信号。
- 用 gitpython 还是 subprocess?优先 subprocess,避免重依赖 / CI 缺失。
- 结果是否被覆盖?改用 jsonl 追加,保留历史快照。
- 本地脏工作区跑出来的结果是不是混进了可信库?用
forbid_dirty_gitfixture 自动 skip。 - 分支记了没?
branch --show-current帮助区分实验来源。 - 发布前有没有校验?CI 里
REQUIRE_CLEAN=1拦截 dirty 结果。
九、小结
“Track git dirty state in method_comparison benchmark results” 说的是基准结果必须可追溯这一工程纪律:
- 每条结果都应记录
commit+dirty+branch,缺一不可; dirty标志是“这条结果可能不可信”的红色警报,比 commit 更关键;- 用
subprocess调git命令行采集,零额外依赖,CI 友好; - 结果用 jsonl 追加而非覆盖,保留历史;
- 用
forbid_dirty_gitfixture +REQUIRE_CLEAN断言,把脏工作区结果挡在可信库之外。
一句话:跑 benchmark 前先git_state(),把 commit 和 dirty 钉进每一行结果,否则两周后你分不清分数差异来自方法还是来自代码。