第3章:开发环境标准化 — Conda / Docker / 依赖锁定

搞AI模型训练,最怕什么?不是模型不收敛,是“在我机器上能跑”。

刚入行那会儿,我被这个问题坑过好几次。同事的代码拉下来,环境一配就是半天,还总报奇怪错误。后来我悟了:开发环境必须标准化,这不是锦上添花,是生存底线。

这一章,咱们就聊三样东西:Conda管Python环境、Docker管系统环境、依赖锁定管版本一致性。这三板斧下去,基本上能解决90%的环境问题。

核心思想:环境即代码。 把环境的创建、依赖、构建过程全部写进配置文件,版本管理,可复现。

开发环境标准化框架 Conda 环境管理 Docker 镜像构建 依赖锁定 environment.yml Dockerfile + .dockerignore requirements.txt / pyproject.toml conda-lock docker compose / multi-stage pip freeze / poetry lock 可复制的 AI 开发环境

3.1 Conda 环境管理

说实话,Python的虚拟环境工具很多。venv、pipenv、poetry……但我个人最常用的还是Conda。为什么?因为AI项目不只依赖Python包,还涉及CUDA、cuDNN、系统库这些二进制依赖。Conda能一口气搞定。

最佳实践: 每个项目一个独立Conda环境,不要用base环境跑实验。你想想看,如果两个项目要用不同版本的PyTorch,混在一起就是灾难。

创建一个环境很简单:

conda create -n llm_train python=3.10
conda activate llm_train

但光这样不够。要把环境定义写成可复现的 environment.yml:

name: llm_train
channels:
  - conda-forge
  - defaults
dependencies:
  - python=3.10
  - pytorch=2.1.0
  - torchvision
  - cudatoolkit=11.8
  - numpy
  - pandas
  - pip
  - pip:
    - transformers
    - datasets
    - accelerate

注意这里的 pip: 小节。有些包Conda源没有,就得从PyPI装。混着用没问题,但顺序要对:先用Conda装系统层依赖,再用Pip装Python层。

我习惯把这个 environment.yml 放在项目根目录下。新同事一来,直接:

conda env create -f environment.yml

完事。嗯,这里要注意:conda env create 默认会创建一个以 name 字段命名的环境。如果别人想换名字,可以加 -n 参数覆盖。

避坑指南: 我曾经遇过一个大坑——有人把 environment.yml 里的 name 写成 base,然后执行 conda env create 把base环境覆盖了。嗯,后果就是重装了Anaconda。所以,永远不要在环境文件中使用 base。

3.1.1 conda-lock 锁定精确版本

environment.yml 只指定了模糊版本(比如 pytorch=2.1.0),但实际安装时Conda会解析依赖树,不同时间、不同系统解析出来的版本可能有细微差别。怎么办?用 conda-lock。

安装 conda-lock:

pip install conda-lock

生成锁定文件:

conda-lock -f environment.yml -p linux-64

这会生成 conda-linux-64.lock 文件。以后重建环境用:

conda create -n myenv --file conda-linux-64.lock

这样,不管什么时间、什么机器,出来的环境完全一致。

3.2 Docker 镜像构建

Conda只能管Python环境的依赖。但操作系统、CUDA驱动、系统库这些呢?这时候就该Docker上场了。

我自己的经验是:Conda管包,Docker管环境。两者互补,不冲突。

一个标准AI项目的Dockerfile大概长这样:

FROM pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime

# 设置非root用户
RUN useradd -m -u 1000 user
USER user
WORKDIR /home/user/app

# 复制Conda环境定义
COPY --chown=user:user environment.yml .
RUN conda env create -f environment.yml

# 复制项目代码
COPY --chown=user:user . .

# 激活环境
ENV PATH /opt/conda/envs/llm_train/bin:$PATH

为什么我不用 base 环境?因为基础镜像里已经有Conda了,但默认是base环境。新创建一个环境,更容易管理和清理。

小技巧: 把 environment.yml 放在代码之前复制,可以充分利用Docker缓存。只要环境不变,这层缓存就不会失效,每次构建能省不少时间。

3.2.1 .dockerignore

记得写 .dockerignore,不然会把本地环境、临时文件全打进去:

__pycache__/
*.pyc
.git/
.gitignore
data/
*.ckpt
*.pth
*.h5
.conda/
.venv/
README.md

为什么?你想想看,一个模型训练项目的数据集可能几十GB,镜像里不需要那些。把 data/ 忽略掉,镜像小得多,推送拉取都快。

3.2.2 多阶段构建

对于更复杂的项目,我推荐多阶段构建。比如编译阶段和运行阶段分开:

# 第一阶段:编译
FROM pytorch/pytorch:2.1.0-cuda11.8-cudnn8-devel AS builder
COPY requirements.txt .
RUN pip install --user -r requirements.txt

# 第二阶段:运行
FROM pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime AS runtime
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY . .

这样最终镜像只有运行时需要的文件,体积能小一半。

3.3 依赖锁定

不管用Conda还是Docker,最后一步都是锁定依赖。锁定的核心目的:确保每次构建的环境一模一样。

对于pip管理的项目,常用的方法:

pip freeze > requirements.txt

但 pip freeze 会列出所有已安装的包,包括那些间接依赖。更好的做法是用 pip-tools 或 poetry。

推荐方案: 使用 pip-compile(pip-tools)从 requirements.in 生成 requirements.txt。

比如 requirements.in:

transformers>=4.30
torch>=2.0,<3.0

然后运行:

pip-compile requirements.in

生成 requirements.txt,里面是所有依赖的精确版本和哈希值:

#
# This file is autogenerated by pip-compile
# To update, run:
#
#    pip-compile requirements.in
#
certifi==2024.2.2
    # via requests
charset-normalizer==3.3.2
    # via requests
filelock==3.13.1
    # via
    #   huggingface-hub
    #   transformers
huggingface-hub==0.23.0
    # via transformers
...
transformers==4.38.2
    # via -r requirements.in

这个文件要提交到Git仓库。以后重装环境直接:

pip install -r requirements.txt

这样,不管是一年后还是换台机器,装出来的包版本一模一样。

3.3.1 哈希校验与安全

更进一步,可以用 pip install --require-hashes 来验证每个包的哈希值。这在生产环境或CI/CD中非常有用,能避免中间人攻击或包被篡改。

pip install --require-hashes -r requirements.txt

注意:如果你用 --require-hashes,requirements.txt 里必须包含所有依赖的哈希值,否则安装会失败。

3.3.2 Poetry 替代方案

现在很多新项目用Poetry来管理依赖。它天然支持锁定机制:

poetry init
poetry add transformers
poetry lock

生成 poetry.lock 文件,同样要提交到版本控制。

两种方案选哪个?我个人习惯:如果是纯Python项目,用Poetry;如果是AI项目(涉及CUDA、Conda),用Conda+conda-lock或者pip-tools。没有绝对好坏,关键是团队统一。

3.4 工程化实践总结

把这三样东西串起来,就是一个标准化的开发环境流程:

环节 工具 产出物 作用
Python环境 Conda environment.yml 定义Python版本、核心包、通道
精确锁定 conda-lock / pip-tools conda-lock文件 / requirements.txt 锁定每个包的精确版本和哈希
系统环境 Docker Dockerfile + .dockerignore 封装操作系统、CUDA、Conda环境
版本控制 Git 以上所有文件 环境即代码,可回溯可复现

你想想看,有了这四样东西,团队里任何人拉下代码,执行 docker build 或者 conda env create,几分钟就能得到一模一样的开发环境。再也不会有“哎,我这能跑呀”这种尴尬局面。

最后一次提醒: 千万别把 environment.yml 或 Dockerfile 放在Gitignore里。它们不是琐碎的文件,是团队的资产。没有锁定的依赖,就是给自己埋定时炸弹。

好了,这一章的内容就到这。动手试试看,把你当前的项目环境标准化。相信我,吃一次亏你就再也回不去了。