跳转至

Git 与版本控制

一句话总结: Git 是软件团队协作时避免互相覆盖代码的基石,本节讲清心智模型、分支策略、合并与变基、冲突解决、PR 流程,以及 ML 场景下大文件与实验追踪的特殊挑战。

Git 是软件团队在不覆盖彼此工作的前提下协作的方式。 本节涵盖 Git 的心智模型、分支策略、合并与变基、冲突解决、拉取请求 (Pull Request, PR),以及 ML 特有的挑战,比如大文件与实验追踪.

  • 任何正经的软件项目都会用版本控制。 Git 是主流系统,几乎所有开源项目和公司都在用。 没有 git,协作就变成发压缩包 + 祈祷没人覆盖你的改动。 有了 git,每一次改动都可追踪、可回滚、可归属。

  • 对 ML 工程师:git 追踪代码、配置、实验脚本。 结合实验追踪工具,你就得到了可复现性:"到底哪份代码 + 哪份配置生成了这个模型?"

心智模型

  • Git 追踪项目的快照 (snapshot). 每一次提交都是当时所有被追踪文件的完整快照,而不是差异 (内部为了效率 git 存 diff,但概念上每次提交都是一个完整状态).

  • 文件有四个"地点":

    1. 工作目录 (working directory):磁盘上真实的文件,你编辑的就是这些。
    2. 暂存区 (staging area,也叫 index):你标记好、准备下一次提交的文件。 git add 把改动挪到这里。
    3. 本地仓库 (local repository):你的提交历史,存在 .git/ 里。 git commit 把暂存区保存为一个新快照。
    4. 远程仓库 (remote repository,比如 GitHub):一份共享副本。 git push 上传你的提交,git pull 拉取别人的。
Working Dir  →  git add  →  Staging  →  git commit  →  Local Repo  →  git push  →  Remote
                                                        ←  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) 会创建一个新的"合并提交",把两条分支合到一起:
git checkout main
git merge feature-x
  • 这样保留了完整历史:你能看到工作在某条分支上进行过,也能看到什么时候合回来。 合并提交有两个父提交。

  • 变基 (rebase) 把你分支上的提交在目标分支之上"重放"一遍:

git checkout feature-x
git rebase main
  • 这会重写历史:你分支上的提交会拿到新的哈希值,就好像你的工作是从 main 当前最新的地方开始的。 结果是一条线性历史 (没有合并提交),读起来更干净。

  • 什么时候用哪个:

    • 变基 用来把最新的 main 变化拉进你的特性分支 (让你的分支保持干净、跟上主干).
    • 合并 用来把特性分支整合回 main (保留分支历史).
    • 绝对不要对已经推送并被别人共享的提交做变基. 变基会重写历史;如果别人已经基于原来的提交做了工作,变基就会把大家坑惨。

解决冲突

  • 冲突 (conflict) 发生在两条分支都改了同一个文件的同一行。 Git 没法自动决定保留哪一份,就会让你手动来解。
<<<<<<< HEAD
learning_rate = 0.001
=======
learning_rate = 0.0005
>>>>>>> feature-x
  • <<<<<<< 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 哈希写进产物。
  • 可复现性六件套:提交哈希、配置、随机种子、库版本、数据版本、硬件规格,缺一不可。