第4章:环境管理(Conda、Docker)
这一章聊环境管理。说白了,就是让你的代码能在别人的机器上跑起来,不会因为少个库、版本不对就崩掉。我最早做项目时,经常花半天时间搭环境——那感觉真的很糟糕。
后来我发现,好的可重复计算框架,环境管理必须做到两点:可复现和可隔离。Conda和Docker是目前最主流的工具。我一个个说。
4.1 Conda:轻量级依赖管理
Conda适合做Python/R的依赖隔离。它不是虚拟机,不隔离操作系统,但能精确锁定每个包的版本。我们做评估时,最怕的是“张三机器跑出指标A,李四机器跑出指标B”——其实就差个numpy版本。
4.1.1 环境导出与重建
先看怎么导出当前环境的所有依赖:
conda env export > environment.yml
导出的yml文件长这样:
name: repeat-eval
channels:
- conda-forge
- defaults
dependencies:
- python=3.10
- numpy=1.23.5
- pandas=1.5.3
- scikit-learn=1.2.1
- pytorch=1.13.1
- pip:
- mlflow==2.1.0
- hydra-core==1.3.2
重建环境时,别人只需要一行命令:
conda env create -f environment.yml
--prefix参数指定路径。我曾经在服务器上把环境装到默认位置,结果不同用户共用同一个conda目录,版本互相覆盖。后来我改用conda env create -p ./env -f environment.yml,每个项目独立。
4.1.2 跨平台兼容性
Conda有个坑:conda env export导出的文件里,可能包含当前平台特有的build号。比如Linux上导出的包可能带了linux-64前缀,在macOS上就无法直接重建。
解决办法是用--from-history参数,只导出你手动指定的包:
conda env export --from-history > environment.yml
这样生成的yml不会包含build信息,跨平台兼容性更好。但缺点是,子依赖的版本不会被锁定——你需要自己权衡。
conda update --all来保证可重复性。更新操作会改变环境状态,破坏锁定。要更新,就重建新环境。
4.2 Docker:完整操作系统级隔离
当你的实验依赖底层系统库(比如CUDA、OpenMPI)或者非Python工具时,Conda就有些力不从心了。这时候Docker才是王道。
Docker通过Dockerfile定义整个运行环境。我参与过一个项目,需要同时跑PyTorch、TensorFlow和Caffe,三个框架的CUDA版本要求完全不同。用Docker,每个框架一个容器,完美隔离。
4.2.1 编写Dockerfile
一个典型的可重复计算用的Dockerfile:
FROM nvidia/cuda:12.1.0-cudnn8-devel-ubuntu22.04
RUN apt-get update && apt-get install -y \
python3.10 python3-pip git wget \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY scripts/ ./scripts/
COPY configs/ ./configs/
ENV PYTHONPATH=/workspace
RUN useradd -m -s /bin/bash repeatuser
USER repeatuser
CMD ["python", "scripts/evaluate.py"]
这里有几点要注意:
- 基础镜像版本必须精确:不要写
nvidia/cuda:latest,要写完整版本号,比如12.1.0-cudnn8。 - 清理缓存:apt-get后要删缓存,pip后用
--no-cache-dir,减少镜像体积。 - 创建非root用户:这是安全考虑,同时也是为了避免文件权限问题。
4.2.2 构建与运行
构建镜像:
docker build -t repeat-eval:v1.0 .
运行容器并挂载数据卷:
docker run --gpus all \
-v /data/datasets:/data \
-v /home/user/output:/output \
repeat-eval:v1.0
挂载卷很重要——你的实验数据、结果不应该打包在镜像里,否则每次改数据都要重做镜像。用卷隔离数据和代码,镜像只存环境。
4.3 Conda vs Docker:怎么选?
你可能会问,到底用哪个?我自己的经验是分场景:
| 场景 | 推荐工具 | 原因 |
|---|---|---|
| 纯Python/R项目,无系统库依赖 | Conda | 轻量,启动快,文件小 |
| 依赖CUDA、cuDNN等GPU中间件 | Docker | Conda无法完整隔离驱动级依赖 |
| 需要跨平台共享给非技术同事 | Docker | 一条docker run命令就能跑,不用管环境 |
| 高速迭代,频繁改代码 | Conda + 虚拟环境 | Docker每次改代码都需要重build,浪费时间 |
| 生产级持续集成(CI/CD) | Docker | 标准化部署,可回滚,可扩展 |
还有一个技巧:Conda + Docker结合。在Docker镜像内部再用Conda管理Python包。这样Docker负责系统层隔离,Conda负责Python层锁定。我常用的组合是nvidia/cuda:xx.xx-ubuntu + conda env create -f environment.yml。
4.4 环境管理的最佳实践
我从踩过的坑里总结出几条铁律:
- 版本号必须写完整。只在配开发环境时用
>=,记录结果时全部固定为==。 - 每次提交代码时,同步更新环境文件。如果忘了,就加个CI检查:提交后发现environment.yml没更新,自动拦截。
- 不要手动修改安装后的环境。要想加包,先改配置文件,再重建环境。我见过有人直接在容器里
pip install,结果导致环境不可复现。 - 用
docker compose管理多容器。如果你的评估系统需要数据库、消息队列、多个计算节点,用docker compose可以一次启动所有服务。
下面这张图总结了环境管理的核心流程:
4.5 踩坑集锦
最后分享几个我实际遇到的坑:
ENV DEBIAN_FRONTEND=noninteractive,结果后续apt安装时自动跳过了时区设置,导致镜像里tzdata没配置完整,运行时所有时间相关函数返回UTC+0。排查了半天才发现。正确做法是在RUN命令里设置DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata,然后手动设置时区。
conda-lock生成锁定文件。它能把所有依赖的精确哈希值记录下来,比yml更可靠。命令很简单:
conda-lock -f environment.yml -p linux-64
生成的conda-lock.yml可以提交到git仓库,别人直接用conda-lock install --name env_name conda-lock.yml就能重建。
环境管理不是技术难题,但疏忽了会浪费大量时间。把环境文件当作代码一样管理,每次变更都提交、都锁版本——你的结果才能真正“可重复”。