Git 与版本控制 Git 是软件团队协作时不互相覆盖对方工作的方式。本文件覆盖 Git 的心智模型、分支策略、合并与变基、冲突解决、拉取请求,以及管理 ML 特有的挑战——比如大文件和实验追踪。 每一个严肃的软件项目都用版本控制。Git 是占主导地位的系统,几乎所有开源项目和公司都在用。没有 git,协作就是互相发 zip 文件,然后祈祷没人覆盖你的改动。有了 git,每一次改动都被追踪、可逆、可归属到具体的人。 对 ML 工程师来说:git 追踪你的代码、配置和实验脚本。配合实验追踪工具,它给了你可复现性(reproducibility):"到底是哪份代码和配置产出了这个模型?" 心智模型 Git 追踪项目的快照(snapshots)。
Git 是软件团队协作时不互相覆盖对方工作的方式。本文件覆盖 Git 的心智模型、分支策略、合并与变基、冲突解决、拉取请求,以及管理 ML 特有的挑战——比如大文件和实验追踪。
每一个严肃的软件项目都用版本控制。Git 是占主导地位的系统,几乎所有开源项目和公司都在用。没有 git,协作就是互相发 zip 文件,然后祈祷没人覆盖你的改动。有了 git,每一次改动都被追踪、可逆、可归属到具体的人。
对 ML 工程师来说:git 追踪你的代码、配置和实验脚本。配合实验追踪工具,它给了你可复现性(reproducibility):"到底是哪份代码和配置产出了这个模型?"
Git 追踪项目的快照(snapshots)。每一次提交都是当时所有被追踪文件的一个完整快照,而不是一个 diff(在内部,git 为了效率存储的是 diff,但在概念上每个提交都是一个完整状态)。
你的文件有四个"位置":
git add 把改动移到这里。.git/ 里。git commit 把暂存区保存为一个新的快照。git push 上传你的提交,git pull 下载别人的提交。工作目录 → git add → 暂存区 → git commit → 本地仓库 → git push → 远端 ← git pull ←
git init # 创建一个新仓库 git clone url # 下载一个远端仓库 git status # 有哪些改动?(用得最多的命令) git add file.py # 暂存某个文件 git add . # 暂存所有改动(谨慎使用) git commit -m "descriptive msg" # 提交暂存的改动 git push # 把提交上传到远端 git pull # 下载并合并远端改动 git log --oneline # 紧凑的提交历史 git diff # 显示未暂存的改动 git diff --staged # 显示已暂存的改动
main(或 master)。创建一个分支给你一条独立的开发线:你可以在不影响 main 的情况下做改动。git branch feature-x # 创建一个分支 git checkout feature-x # 切换过去 git checkout -b feature-x # 一步完成创建并切换 git branch -d feature-x # 删除分支(合并之后) git branch -a # 列出所有分支(本地 + 远端)
main 提交。每个功能、bug 修复或实验都该有它自己的分支。这能让 main 保持稳定、随时可部署。特性分支(feature branches,最常见):每个功能/修复都从 main 拉一个分支。做完之后,开一个拉取请求(pull request,PR)合并回去。简单,对大多数团队都适用。
主干开发(trunk-based development):开发者频繁地(每天多次)提交到 main,用功能开关(feature flags)隐藏未完成的工作。受那些持续部署的团队(Google、Facebook)青睐。需要优秀的 CI/CD。
Gitflow:为功能、发布和热修复分别设立分支。更复杂,更适合有版本化发布的产品(移动应用、打包软件)。对大多数 ML 项目来说是大材小用。
对 ML 团队来说:特性分支加上短生命周期分支(1-3 天内合并)是最佳平衡点。长生命周期的分支会偏离 main,制造出令人痛苦的合并冲突。
git checkout main git merge feature-x
这保留了完整的历史:你能看到工作发生在一个分支上,以及它是何时被合并的。合并提交有两个父提交。
**变基(rebase)**把你的分支提交在目标分支之上"重放"一遍:
git checkout feature-x git rebase main
这会重写历史:你分支上的提交得到新的哈希,就好像你是从 main 的当前最新位置开始干活一样。结果是一条线性的历史(没有合并提交),读起来更干净。
什么时候用哪个:
main 的最新改动(保持你的分支干净、最新)。main(保留分支历史)。<<<<<<< HEAD learning_rate = 0.001 ======= learning_rate = 0.0005 >>>>>>> feature-x
<<<<<<< HEAD 和 ======= 之间是当前分支的版本。======= 和 >>>>>>> feature-x 之间是传入分支的版本。你决定保留哪个(或把它们组合起来),删掉这些标记,保存,然后 git add 这个已解决的文件。
陷阱:不要把冲突标记留在已提交的文件里。它们是字面意义的文本,会让你的代码崩掉。解决完之后一定要搜一遍 <<<<<<<。
减少冲突:让分支保持短命,频繁地把 main 合并进你的分支,避免多人同时编辑同一个文件。
提交信息是给未来的你和你的队友看的。"fix bug" 什么也没说。"修复 8 卡训练时导致 OOM 的 batch size 计算 off-by-one" 则什么都说了。
格式:
简短摘要(不超过 50 个字符,祈使语气) 必要时写更长的描述。解释为什么(WHY),而不是做了什么(WHAT) (diff 已经显示了改了什么)。每行 72 个字符换行。 Fixes #123
祈使语气(imperative mood):写 "Add feature" 而不是 "Added feature" 或 "Adds feature"。把它读成完成这样一个句子:"如果应用,这个提交将会 add feature。"
原子提交(atomic commits):每个提交只做一件事。"加数据加载器"是一个提交。"加数据加载器,并且修了一个无关的 bug,还更新了 README"应该是三个提交。这让 git bisect(找出是哪个提交引入了 bug)成为可能。
**拉取请求(pull request,PR)**提议把一个分支合并进 main。它是代码评审(code review)的入口:队友读你的改动、提出改进建议,并在合并之前批准。
好的 PR 实践:
main 就有干净的历史)。代码评审不是为了找 bug(那是测试的活)。它是为了:知识共享(评审者借此了解代码库)、设计反馈(这样做对吗?)、维护标准(命名、风格、架构)。
.gitignore 文件告诉 git 哪些文件要从追踪中排除。对 ML 项目来说:# Python __pycache__/ *.pyc *.egg-info/ .venv/ env/ # 数据和模型(对 git 来说太大了) data/ *.csv *.parquet models/ *.pt *.onnx *.bin checkpoints/ # 密钥 .env *.pem credentials.json # IDE .vscode/ .idea/ *.swp # 操作系统 .DS_Store Thumbs.db # Jupyter .ipynb_checkpoints/ # 实验输出 wandb/ mlruns/ outputs/ logs/
.gitignore 并不会把它从仓库里移除——前提是它已经被提交过了。你还必须 git rm --cached file 来停止追踪它。除非你重写历史(这很麻烦),否则它会永远留在历史里。ML 带来了传统软件不会遇到的挑战:
大文件:数据集和模型权重动辄几个 GB 甚至更多。Git 是为文本文件(源代码)设计的,不是为二进制大块设计的。解决方案:
dvc add data.csv、dvc push、dvc pull。实验追踪(experiment tracking):哪个提交 + 哪些超参数 + 哪份数据产出了哪些指标?Git 追踪代码,但不追踪完整的实验上下文。
git_hash = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip()。把它和你的结果存在一起。可复现性清单(每次实验要追踪什么):
pip freeze)# 快速可复现性快照 echo "Commit: $(git rev-parse HEAD)" > experiment_info.txt echo "Branch: $(git branch --show-current)" >> experiment_info.txt echo "Dirty: $(git status --porcelain | wc -l) files" >> experiment_info.txt pip freeze >> experiment_info.txt nvidia-smi >> experiment_info.txt