【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 跑不起来。应当优先用subprocessgit命令行,零额外依赖。

根因 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 结果一律拦截。

八、排查清单

结果不可追溯时查:

  1. 结果文件有没有 commit 字段?没有就加git rev-parse HEAD
  2. 有没有 dirty 标志?只有 commit 不够,dirty 才是“不可信”信号。
  3. 用 gitpython 还是 subprocess?优先 subprocess,避免重依赖 / CI 缺失。
  4. 结果是否被覆盖?改用 jsonl 追加,保留历史快照。
  5. 本地脏工作区跑出来的结果是不是混进了可信库?用forbid_dirty_gitfixture 自动 skip。
  6. 分支记了没branch --show-current帮助区分实验来源。
  7. 发布前有没有校验?CI 里REQUIRE_CLEAN=1拦截 dirty 结果。

九、小结

“Track git dirty state in method_comparison benchmark results” 说的是基准结果必须可追溯这一工程纪律:

  • 每条结果都应记录commit+dirty+branch,缺一不可;
  • dirty标志是“这条结果可能不可信”的红色警报,比 commit 更关键;
  • subprocessgit命令行采集,零额外依赖,CI 友好;
  • 结果用 jsonl 追加而非覆盖,保留历史;
  • forbid_dirty_gitfixture +REQUIRE_CLEAN断言,把脏工作区结果挡在可信库之外。

一句话:跑 benchmark 前先git_state(),把 commit 和 dirty 钉进每一行结果,否则两周后你分不清分数差异来自方法还是来自代码。