Git 与版本控制


文档摘要

Git 与版本控制 Git 是软件团队协作时不互相覆盖对方工作的方式。本文件覆盖 Git 的心智模型、分支策略、合并与变基、冲突解决、拉取请求,以及管理 ML 特有的挑战——比如大文件和实验追踪。 每一个严肃的软件项目都用版本控制。Git 是占主导地位的系统,几乎所有开源项目和公司都在用。没有 git,协作就是互相发 zip 文件,然后祈祷没人覆盖你的改动。有了 git,每一次改动都被追踪、可逆、可归属到具体的人。 对 ML 工程师来说:git 追踪你的代码、配置和实验脚本。配合实验追踪工具,它给了你可复现性(reproducibility):"到底是哪份代码和配置产出了这个模型?" 心智模型 Git 追踪项目的快照(snapshots)。

Git 与版本控制

Git 是软件团队协作时不互相覆盖对方工作的方式。本文件覆盖 Git 的心智模型、分支策略、合并与变基、冲突解决、拉取请求,以及管理 ML 特有的挑战——比如大文件和实验追踪。

  • 每一个严肃的软件项目都用版本控制。Git 是占主导地位的系统,几乎所有开源项目和公司都在用。没有 git,协作就是互相发 zip 文件,然后祈祷没人覆盖你的改动。有了 git,每一次改动都被追踪、可逆、可归属到具体的人。

  • 对 ML 工程师来说:git 追踪你的代码、配置和实验脚本。配合实验追踪工具,它给了你可复现性(reproducibility):"到底是哪份代码和配置产出了这个模型?"

心智模型

  • Git 追踪项目的快照(snapshots)。每一次提交都是当时所有被追踪文件的一个完整快照,而不是一个 diff(在内部,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 下载别人的提交。
工作目录 → git add → 暂存区 → git commit → 本地仓库 → 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 拉一个分支。做完之后,开一个拉取请求(pull request,PR)合并回去。简单,对大多数团队都适用。

  • 主干开发(trunk-based development):开发者频繁地(每天多次)提交到 main,用功能开关(feature flags)隐藏未完成的工作。受那些持续部署的团队(Google、Facebook)青睐。需要优秀的 CI/CD。

  • Gitflow:为功能、发布和热修复分别设立分支。更复杂,更适合有版本化发布的产品(移动应用、打包软件)。对大多数 ML 项目来说是大材小用。

  • 对 ML 团队来说:特性分支加上短生命周期分支(1-3 天内合并)是最佳平衡点。长生命周期的分支会偏离 main,制造出令人痛苦的合并冲突。

合并与变基

  • **合并(merge)**会创建一个新的"合并提交"(merge commit),把两条分支组合到一起:
git checkout main git merge feature-x
  • 这保留了完整的历史:你能看到工作发生在一个分支上,以及它是何时被合并的。合并提交有两个父提交。

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

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

  • 什么时候用哪个

    • rebase 用于让你的特性分支跟上 main 的最新改动(保持你的分支干净、最新)。
    • merge 用于把你的特性分支集成进 main(保留分支历史)。
    • 绝对不要 rebase 已经推送并被他人共享的提交。变基会重写历史;如果别人已经基于原始提交做了工作,变基会造成混乱。

解决冲突

  • 当两条分支修改了同一个文件的同一行时,就会发生冲突(conflict)。Git 无法自动决定保留哪个改动,于是请你手动解决。
<<<<<<< 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 实践

    • 让 PR 保持小(改动不超过 400 行)。巨大的 PR 会被走过场地盖章放行,因为没人想评审 2000 行。
    • 写一个清晰的描述:改了什么、为什么、怎么测试。
    • 关联到促成这次改动的那个 issue 或工单。
    • 及时回应评审意见。
    • 合并前把琐碎的提交压扁(squash)掉(这样 main 就有干净的历史)。
  • 代码评审不是为了找 bug(那是测试的活)。它是为了:知识共享(评审者借此了解代码库)、设计反馈(这样做对吗?)、维护标准(命名、风格、架构)。

.gitignore

  • .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 的 Git

  • ML 带来了传统软件不会遇到的挑战:

  • 大文件:数据集和模型权重动辄几个 GB 甚至更多。Git 是为文本文件(源代码)设计的,不是为二进制大块设计的。解决方案:

    • Git LFS(Large File Storage,大文件存储):在 git 里追踪指针,把真实文件存在一个独立的服务器上。简单,但在 GitHub 上有存储/带宽限制。
    • DVC(Data Version Control,数据版本控制):把数据和模型文件与 git 分开管理,使用远端存储(S3、GCS)。对数据而言用起来就像 git:dvc add data.csvdvc pushdvc pull
  • 实验追踪(experiment tracking):哪个提交 + 哪些超参数 + 哪份数据产出了哪些指标?Git 追踪代码,但不追踪完整的实验上下文。

    • Weights & Biases(W&B):记录指标、超参数、系统信息,并关联到对应的 git 提交。提供用来比较多次运行的看板。
    • MLflow:开源的实验追踪,带模型注册表(model registry)。记录参数、指标和产物(artifact)。
    • 简单做法:在你的训练脚本里记录 git 哈希:git_hash = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip()。把它和你的结果存在一起。
  • 可复现性清单(每次实验要追踪什么):

    • Git 提交哈希(精确的代码版本)
    • 配置文件 / 超参数
    • 随机种子(random seeds)
    • Python 和库的版本(pip freeze
    • 数据版本(DVC 哈希或数据集版本标签)
    • 硬件(GPU 型号、GPU 数量)
# 快速可复现性快照 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

发布者: 作者: HenryNdubuaku 转发
评论区 (0)
U