Git 与版本控制¶
一句话总结: Git 是软件团队协作时避免互相覆盖代码的基石,本节讲清心智模型、分支策略、合并与变基、冲突解决、PR 流程,以及 ML 场景下大文件与实验追踪的特殊挑战。
Git 是软件团队在不覆盖彼此工作的前提下协作的方式。 本节涵盖 Git 的心智模型、分支策略、合并与变基、冲突解决、拉取请求 (Pull Request, PR),以及 ML 特有的挑战,比如大文件与实验追踪.
-
任何正经的软件项目都会用版本控制。 Git 是主流系统,几乎所有开源项目和公司都在用。 没有 git,协作就变成发压缩包 + 祈祷没人覆盖你的改动。 有了 git,每一次改动都可追踪、可回滚、可归属。
-
对 ML 工程师:git 追踪代码、配置、实验脚本。 结合实验追踪工具,你就得到了可复现性:"到底哪份代码 + 哪份配置生成了这个模型?"
心智模型¶
-
Git 追踪项目的快照 (snapshot). 每一次提交都是当时所有被追踪文件的完整快照,而不是差异 (内部为了效率 git 存 diff,但概念上每次提交都是一个完整状态).
-
文件有四个"地点":
- 工作目录 (working directory):磁盘上真实的文件,你编辑的就是这些。
- 暂存区 (staging area,也叫 index):你标记好、准备下一次提交的文件。
git add把改动挪到这里。 - 本地仓库 (local repository):你的提交历史,存在
.git/里。git commit把暂存区保存为一个新快照。 - 远程仓库 (remote repository,比如 GitHub):一份共享副本。
git push上传你的提交,git pull拉取别人的。
- 暂存区正是 git 强大的地方。 你可以改了 10 个文件,但只提交其中 3 个,剩下的留作另一次提交。 这样你就能做出干净、聚焦的提交。
必备命令¶
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 # 查看已暂存的改动
分支¶
- 分支 (branch) 就是指向某个提交的指针。 默认分支是
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拉一条分支。 做完了发 PR 合回来。 简单粗暴,适合大多数团队。 -
主干开发 (trunk-based development):开发者频繁地往
main提交 (一天多次),用特性开关 (feature flag) 隐藏没做完的部分。 持续部署的团队 (Google、Facebook) 偏爱这套。 要求非常强的持续集成/持续部署 (Continuous Integration/Continuous Deployment, CI/CD). -
Gitflow:为特性、发布、热修复分别开分支。 更复杂,适合有版本化发布的软件 (手机 App、打包分发的软件). 对大多数 ML 项目来说过度设计。
-
对 ML 团队来说:特性分支 + 短命分支 (1-3 天内合掉) 是最舒服的方案。 长期分支会和
main越拉越远,制造痛苦的合并冲突。
合并与变基¶
- 合并 (merge) 会创建一个新的"合并提交",把两条分支合到一起:
-
这样保留了完整历史:你能看到工作在某条分支上进行过,也能看到什么时候合回来。 合并提交有两个父提交。
-
变基 (rebase) 把你分支上的提交在目标分支之上"重放"一遍:
-
这会重写历史:你分支上的提交会拿到新的哈希值,就好像你的工作是从
main当前最新的地方开始的。 结果是一条线性历史 (没有合并提交),读起来更干净。 -
什么时候用哪个:
- 变基 用来把最新的
main变化拉进你的特性分支 (让你的分支保持干净、跟上主干). - 合并 用来把特性分支整合回
main(保留分支历史). - 绝对不要对已经推送并被别人共享的提交做变基. 变基会重写历史;如果别人已经基于原来的提交做了工作,变基就会把大家坑惨。
- 变基 用来把最新的
解决冲突¶
- 冲突 (conflict) 发生在两条分支都改了同一个文件的同一行。 Git 没法自动决定保留哪一份,就会让你手动来解。
-
<<<<<<< HEAD和=======之间是当前分支的版本。=======和>>>>>>> feature-x之间是进来的分支的版本。 你决定保留哪一份 (或者把两份合起来),删掉标记符,保存,然后git add这个解决好的文件。 -
坑:不要把冲突标记留在已提交的文件里。 那些是字面上的文本,会把你的代码搞坏。 解完冲突后一定要搜一下
<<<<<<<. -
减少冲突:让分支短命、经常把
main合进你的分支、避免多人同时改同一个文件。
写好提交信息¶
-
提交信息是给未来的你和你的队友看的。 "fix bug" 什么都没告诉你。 "Fix off-by-one in batch size calculation that caused OOM on 8-GPU training" 就把一切说明白了。
-
格式:
Short summary (50 chars or less, imperative mood)
Longer description if needed. Explain WHY, not WHAT
(the diff shows what changed). Wrap at 72 characters.
Fixes #123
-
祈使语气 (imperative mood):用 "Add feature",不用 "Added feature" 或 "Adds feature." 读起来像在补完这个句子:"If applied, this commit will add feature."
-
原子提交 (atomic commits):每次提交只做一件事。 "Add data loader" 是一次提交。 "Add data loader and fix unrelated bug and update README" 应该拆成三次提交。 这样
git bisect(找出是哪次提交引入了 bug) 才有意义。
拉取请求与代码评审¶
-
拉取请求 (Pull Request, PR) 是提议把一条分支合进
main的方式。 它是代码评审的入口:队友读你的改动、提改进建议、批准,然后才合并。 -
好的 PR 实践:
- 保持 PR 小 (改动 400 行以内). 大 PR 只会被随手放过,因为没人愿意读 2000 行。
- 写清楚描述:改了什么、为什么改、怎么测。
- 关联到促成这次改动的 issue 或 ticket.
- 及时回复评审意见。
- 合并前把琐碎的提交 squash 掉 (让
main有干净的历史).
-
代码评审不是为了抓 bug (那是测试的活). 评审是为了:知识共享 (评审人借机了解代码库)、设计反馈 (这是不是最好的思路?)、维持标准 (命名、风格、架构).
.gitignore¶
.gitignore文件告诉 git 哪些文件不要追踪。 ML 项目常见的一份:
# Python
__pycache__/
*.pyc
*.egg-info/
.venv/
env/
# Data and models (too large for git)
data/
*.csv
*.parquet
models/
*.pt
*.onnx
*.bin
checkpoints/
# Secrets
.env
*.pem
credentials.json
# IDE
.vscode/
.idea/
*.swp
# OS
.DS_Store
Thumbs.db
# Jupyter
.ipynb_checkpoints/
# Experiment outputs
wandb/
mlruns/
outputs/
logs/
- 坑:一个文件已经提交了之后再把它加进
.gitignore,并不会把它从仓库里移除。 你还得git rm --cached file来取消追踪。 这个文件在历史里会一直存在,除非你重写历史 (那事儿很脏).
面向 ML 的 Git¶
-
ML 带来了传统软件没有的挑战:
-
大文件:数据集和模型权重动辄几 GB 起步。 Git 是为文本文件 (源码) 设计的,不是为二进制大块头。 解决方案:
- Git LFS (Large File Storage,大文件存储): git 里放指针,实际文件放在另一台服务器上。 简单,但 GitHub 有存储和带宽限制。
- DVC (Data Version Control,数据版本控制):用远端存储 (比如 S3、GCS) 独立于 git 管理数据和模型文件。 用法很像给数据用 git:
dvc add data.csv,dvc push,dvc pull.
-
实验追踪:哪次提交 + 哪套超参数 + 哪份数据,产出了哪个指标?Git 只追踪代码,追踪不了完整的实验上下文。
- Weights & Biases (W&B):记录指标、超参数、系统信息,并关联到 git 提交。 提供 dashboard 用来对比不同 run.
- MLflow:开源的实验追踪,带模型仓库。 记录参数、指标、产物。
- 土办法:在训练脚本里记下 git 哈希:
git_hash = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip(). 和结果一起存下来。
-
可复现性清单 (每次实验要记的东西):
- Git 提交哈希 (确切的代码版本)
- 配置文件 / 超参数
- 随机种子
- Python 和库的版本 (
pip freeze) - 数据版本 (DVC 哈希或数据集版本 tag)
- 硬件 (GPU 型号、GPU 数量)
# Quick reproducibility snapshot
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
📌 本节要点¶
- 四地点模型:工作目录 → 暂存区 → 本地仓库 → 远程仓库,
add/commit/push依次搬运。 - 快照,非 diff:每次提交概念上是完整快照,内部用差异存储只是效率考虑。
- 永远开分支:不要直接往
main提交,特性分支 + 短命 (1-3 天) 是 ML 团队的甜点区。 - 合并 vs 变基:合并保留历史适合合回
main;变基线性干净适合更新特性分支;共享过的提交永远别变基。 - 冲突处理:手动挑选保留哪一份,删掉
<<<<<<</=======/>>>>>>>标记后再git add. - 提交信息祈使语气:说清楚 WHY 而不是 WHAT,原子提交让
git bisect才有意义。 - PR 要小要清: 400 行以内,描述改动动机与测试方法;评审目标是知识共享与设计反馈,而非抓 bug.
.gitignore有陷阱:已提交的文件加进去不会移除,得git rm --cached才能取消追踪。- ML 特殊挑战:大文件用 Git LFS 或 DVC;完整实验上下文用 W&B / MLflow 追踪,至少也要把 git 哈希写进产物。
- 可复现性六件套:提交哈希、配置、随机种子、库版本、数据版本、硬件规格,缺一不可。