
做过大模型训练的人应该都有这种体会真正卡住你的很多时候不是模型设计而是环境本身。LLM领域迭代快框架更新快依赖关系错综复杂每一次换机器、换项目、换团队都意味着要把CUDA、Python解释器、PyTorch、DeepSpeed这些组件重新蹚一遍。我见过不少同学把一周时间耗在装环境上结果实验还没跑起来热情先没了。这期LLM Training Lab的内容就是围绕可复用这三个字讲清楚我是怎么搭建一套能反复使用、跨机器迁移、新项目直接复制的大模型基础训练环境。文章适合刚接触LLM训练、准备在本地或服务器上跑大模型代码的新手也适合已经被环境问题折磨过、想彻底理顺依赖关系的老手。文中的方案我实测过很多次尽量把版本、命令、坑都写清楚。1. 为什么训练环境要可复用1.1 从一次真实翻车说起去年有次我接了一个新项目要在另一台服务器上复现之前跑过的对话模型训练结果。当时我自信满满因为代码都在Git仓库里感觉pull下来就能跑。结果从装CUDA驱动开始就出了问题机器上预装的是CUDA 12.0而我旧环境的PyTorch是搭配CUDA 11.8编译的一加载模型就报CUDA driver version is insufficient for CUDA runtime version。然后我又去折腾驱动装完重启NVIDIA驱动直接挂了连nvidia-smi都执行不了。折腾到第二天终于能跑通了结果DeepSpeed版本太老跟新机器的GCC版本不兼容编译fused_adam直接报错。那一刻我意识到代码可以复用但环境不能靠记忆来复用。每次手动配环境本质上都是把人肉当成了配置文件错一步就要返工而且根本没有办法保证两台机器上的环境完全一致。我这个翻车经历不是个例。业内经常说it works on my machine本质上就是环境不可复用的表现。对于LLM训练这种依赖极重、GPU资源又紧张的场景环境问题带来的损失尤其大——它不仅是时间成本还会污染实验结果。同一个脚本今天能跑到那个精度明天环境变了一点点可能连loss都复现不出来。所以我后来的原则很简单所有环境配置都必须代码化机器只是执行环境的地方不是记忆环境的地方。1.2 一套可复用环境应该满足什么我给自己的训练环境定了几个硬性标准缺一个都不能算真正意义上的可复用。第一是可重现性。任何一个人拿到你的配置说明按照文档一步步操作都能得到和你一模一样的软件环境。这意味着版本号要精确到小版本不能只写pytorch然后让别人自己选。第二是可迁移性。环境不能绑定在某台物理机上换了机器之后能在半小时内恢复。这要求我们把驱动、CUDA运行时、Python依赖、训练框架这几个层次拆开管理。第三是可扩展性。当你从单卡训练切换到多卡训练或者从单机跑到多机环境不应该推倒重来而是通过追加组件实现。第四是可回滚性。环境升级踩雷了能不能一键退回到之前的可用状态。这四条标准听起来简单真正做到需要一套组合拳GPU驱动用操作系统包管理器管理CUDA运行时和Python依赖用虚拟环境隔离容器化方案作为终极兜底。下文的方案就是围绕这四个标准展开的。2. 搭环境前的三个关键决策2.1 先敲定CUDA与GPU驱动的组合很多新手搭环境是从Python库开始的一上来就pip install torch然后跑去网上下最新的CUDA Toolkit。这个顺序其实是反的。最稳妥的做法是先搞清楚你的GPU算力再反推应该装什么版本的驱动和CUDA。GPU算力决定了你能不能用上某些新特性比如bf16训练在Ampere架构之前的GPU上就不太友好。你可以通过nvidia-smi看到GPU型号然后去NVIDIA官方文档查对应的Compute Capability。以我现在用的两套机器为例一套是A100算力8.0一套是RTX 4090算力8.9两者跑LLM训练都没有压力但4090需要更新版本的驱动才能充分发挥性能。驱动版本和CUDA Toolkit版本的关系我建议遵循这样一个原则只要驱动版本满足CUDA Toolkit的最低要求即可不必追求驱动最新。因为对于大模型训练来说驱动太新反而可能引入未知问题而CUDA Toolkit是Python环境的一部分可以随环境切换驱动却要动系统层。注意NVIDIA驱动是向下兼容的装了一个较新的驱动理论上可以支持旧版本的CUDA Toolkit反过来则不成立。所以驱动可以稍微新一点留出余量CUDA Toolkit则根据训练框架的需求来定。我的选择是驱动版本固定在535系列以上CUDA Toolkit用11.8或12.1这是目前PyTorch生态里最主流的两个组合。如果你的机器上已经装好了驱动先用nvidia-smi看一下右上角的CUDA Version那个数字就是驱动支持的最高CUDA版本检查它不小于你想用的Toolkit版本就可以。2.2 用conda管理环境比venv更适合LLMPython的虚拟环境方案很多venv、virtualenv、conda、poetry各有拥趸。但放到LLM训练这个场景里我坚定地选择conda原因有两点。第一点conda能管理非Python的二进制依赖。LLM训练离不开CUDA运行时库、cuDNN、NCCL这些组件这些是C/C写的原生库。venv只能隔离Python包对系统级依赖无能为力而这些原生库恰恰是版本冲突的重灾区。conda能从conda-forge或nvidia频道直接安装cudatoolkit、cudnn把整个运行时链路的版本都锁在环境里。第二点conda环境之间切换成本极低。我平时本地机器上同时存在三四个环境分别对应不同版本的PyTorch和不同模型仓库的需求conda activate一条命令就能切换互不干扰。如果是venv虽然也能做到但系统级依赖还是会互相污染。当然conda也不是没有问题它的包解析速度慢源网络不好时经常卡住。所以我会额外配置国内镜像源还有就是把conda solve的速度问题用libmamba求解器解决掉。2.3 让PyTorch版本的发布矩阵帮你做决策装PyTorch的时候最忌讳的就是直接pip install torch默认会装上最新版但最新版不一定匹配你的CUDA和其他依赖。PyTorch官方给了非常清晰的安装矩阵在官网首页选择系统和CUDA版本它会自动生成对应的安装命令。我的经验是不要用太新的PyTorch。当前LLM生态里transformers、accelerate、peft这些库对新版PyTorch的适配往往有滞后。比如老版本的transformers可能不兼容PyTorch 2.3某个新API。所以我一般会选择比自己实际需求晚半年左右的稳定版本比如PyTorch 2.1.x搭配CUDA 11.8这是我目前最放心的一套组合绝大多数开源LLM代码都能直接跑。另外安装PyTorch时有个细节容易被忽略一定要用PyTorch官方源安装不要用默认的PyPI源因为官方源会带上配套的NCCL、cuDNN等二进制库。如果从PyPI装你可能缺失这些组件等到多卡训练才暴露问题。3. 实操从零搭一套本地可复用环境3.1 先定义一套统一的目录结构环境不是孤立存在的它要服务于项目。所以我会在项目初始化阶段就把目录结构定下来让环境和代码、数据、日志各归其位。我的标准LLM项目目录大概是这样的llm_project/ ├── envs/ # 存放环境配置统一管理 │ ├── environment.yml │ └── requirements.txt ├── src/ # 模型定义、训练脚本 │ ├── model/ │ ├── data/ │ └── train.py ├── configs/ # 实验配置 │ └── train_config.yaml ├── scripts/ # 启动、训练、评估脚本 │ ├── setup_env.sh │ ├── train_single.sh │ └── train_multi.sh ├── data/ # 数据集不入Git ├── logs/ # 训练日志、tensorboard目录 ├── checkpoints/ # 权重文件 └── README.md这个结构中envs目录是环境的源代码scripts/setup_env.sh是环境的编译脚本。任何人克隆这个仓库只需要运行setup脚本就能重建环境。data、logs、checkpoints都通过.gitignore排除避免把几十GB的文件拖进Git。目录结构看似跟环境搭建无关但它决定了可复用的上限。试想一下如果每个项目都把环境配置文件随手丢在根目录日志和权重到处乱放换人接手时找配置都要找半天可复用性就是空谈。3.2 用environment.yml锁定环境蓝本我的环境配置始终以conda的environment.yml作为主配置文件。因为它能同时声明Python版本、conda包和pip包一次搞定。下面是我当前在用的一个典型配置name: llm-lab channels: - pytorch - nvidia - conda-forge - defaults dependencies: - python3.10.12 - pip23.2.1 - cudatoolkit11.8 - cudnn8.6.0 - nccl2.18.5 - numpy1.24.3 - pandas2.0.3 - pip: - torch2.1.2 - torchvision0.16.2 - torchaudio2.1.2 - transformers4.36.2 - accelerate0.26.1 - peft0.7.1 - datasets2.16.1 - deepspeed0.13.1 - tensorboard2.15.1 - wandb0.16.3注意channels的顺序很有讲究我把pytorch和nvidia放在最前面这样PyTorch相关的包会优先从官方渠道解析版本更可靠。pip写在dependencies里确保在conda环境创建后自动用pip安装剩余依赖。创建环境只需要一条命令conda env create -f envs/environment.yml如果是从零开始的机器我还会在执行这条命令前先更新conda本身conda update -n base -c defaults conda提示如果你用的是国内网络建议先在~/.condarc里配置好镜像源否则创建环境时下载会很痛苦。这个问题后面章节会专门展开。3.3 用requirements相对锁定实现分层依赖管理有人可能会问既然有了environment.yml为什么还要requirements.txt我的做法是让两者分工environment.yml管理环境级依赖包括Python版本、CUDA运行时、核心库requirements.txt管理项目级依赖也就是每一次改动可能更频繁的Python包。项目迭代过程中新引入一个库是常事。如果每次都要改environment.yml然后重建环境太耗时。我的习惯是项目新加包就直接pip install同时把包名加进requirements.txt。这样环境文件是稳定的需求文件是动态的环境本身不需要频繁重建。锁定生产环境时我用这种方式生成完整依赖列表pip freeze envs/requirements-lock.txtrequirements-lock.txt和手写的requirements.txt不同它会包含每一个传递依赖的精确版本号好处是能精确复现坏处是文件很长、很不直观。所以我把呈现给人类看的requirements.txt保持简洁把机器用的锁定文件单独保存。这一点对LLM项目尤其重要因为transformers这类库的依赖树很深随便一个传递依赖版本变了行为就可能不同。3.4 写一个一键初始化环境的脚本可复用的最终体现是一个命令搞定一切。我会在scripts/下放一个setup_env.sh内容很简单但它把所有初始化步骤固化了#!/usr/bin/env bash set -euo pipefail ENV_NAME$(grep ^name: envs/environment.yml | awk {print $2}) if conda env list | grep -qE ^\s*${ENV_NAME}\s; then echo env ${ENV_NAME} already exists, skip creation else conda env create -f envs/environment.yml fi source $(conda info --base)/etc/profile.d/conda.sh conda activate ${ENV_NAME} if [ -f envs/requirements-lock.txt ]; then pip install -r envs/requirements-lock.txt elif [ -f envs/requirements.txt ]; then pip install -r envs/requirements.txt fi echo environment ready: ${ENV_NAME}这个脚本有几个值得注意的点。set -euo pipefail保证任何一步失败就会立刻退出不会带着残缺环境继续往下走环境已存在时跳过创建做到幂等用conda info --base定位conda路径比硬编码路径更健壮。新建项目或换新机器时我只需要执行bash scripts/setup_env.sh剩下的就是等待。这就是可复用的日常形态。4. 进阶用Docker把环境打包成随身镜像4.1 conda环境还不够为什么还要Dockerconda环境虽然好但它本质上还是依赖宿主机的操作系统和驱动。假设你去一台新的服务器操作系统是Ubuntu 20.04而你的环境是在Ubuntu 22.04上建的某些编译过的组件就可能出现兼容性问题。更麻烦的是你的同事如果用的是别的发行版问题会更明显。Docker解决的是环境与操作系统解耦的问题。把训练环境打包成镜像之后不管底层是Ubuntu还是Debian不管有没有预装Python只要宿主机内核支持容器和GPU透传docker run一启动就能得到一个和开发时完全一致的运行环境。对于团队协作这个价值是巨大的——你不需要每个人本地都装一遍CUDA和依赖只需要拉取镜像即可。容器化的另一个好处是隔离。不同项目的依赖互不干扰一台GPU服务器上可以同时跑多个容器每个容器用不同的PyTorch版本而不会像conda环境那样偶尔出现某些系统级库的串扰。尤其是多人共用一台训练服务器时Docker几乎是唯一干净的选择。4.2 Dockerfile怎么写才够LLM专用一个典型的LLM训练Dockerfile长这样FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04 ENV DEBIAN_FRONTENDnoninteractive ENV TORCH_CUDA_ARCH_LIST8.0;8.6;8.9 RUN apt-get update apt-get install -y --no-install-recommends \ git vim curl wget \ python3.10 python3.10-dev python3-pip \ rm -rf /var/lib/apt/lists/* RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.10 1 \ update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1 WORKDIR /workspace COPY envs/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ ./src/ COPY configs/ ./configs/ ENTRYPOINT [python, src/train.py]几个关键设计基础镜像用devel版本而不是runtime版本因为训练过程经常需要编译自定义算子没有完整的CUDA工具链会失败TORCH_CUDA_ARCH_LIST指定编译目标GPU架构能减少不必要的编译耗时也能避免在不同GPU上运行时的兼容性问题。构建镜像并启动训练容器docker build -t llm-train-env:latest . docker run --gpus all -it --rm \ -v $(pwd):/workspace \ llm-train-env:latest--gpus all会把宿主机所有GPU透传给容器-v挂载数据目录。这样数据、代码是活的环境是固定的两者互不干扰。4.3 数据卷与多机分布式训练的容器配置容器化训练最容易踩的坑是数据卷权限。宿主机挂载进去的目录默认所有者是root容器内进程如果不是root用户就会没有写权限。我的做法是在启动时显式指定用户IDdocker run --gpus all -it --rm \ -v $(pwd):/workspace \ --user $(id -u):$(id -g) \ llm-train-env:latest这样容器内创建的日志、checkpoint文件宿主机上也归当前用户所有不会出现容器里跑完宿主机却删不掉的尴尬。多机多卡训练时还需要额外暴露分布式通信端口。PyTorch的NCCL默认使用TCP通信需要确保节点间的通信端口开通。我一般会在docker run参数里加上--network host让容器直接使用宿主机网络省去端口映射的麻烦。对于NCCL来说host网络模式还能获得更好的通信性能减少NAT带来的延迟。5. 常见问题与排查技巧实录5.1 CUDA相关报错先按这个顺序排查我用一张表格把最常见的CUDA问题整理了出来方便直接对照排查。现象直接原因排查与解法CUDA driver version is insufficient驱动版本低于运行时要求检查nvidia-smi与python -c import torch; print(torch.version.cuda)更换匹配的PyTorch轮子No CUDA GPUs are available容器内未透传GPU或驱动未加载退出容器加--gpus all宿主机执行nvidia-smi确认GPU状态libcudnn.so.8: cannot open shared object filecuDNN缺失或版本不匹配确认conda list cudnn或用pip install nvidia-cudnn-cu11训练中途CUDA error: illegal memory access显存越界或某些算子触发非法访问设置PYTORCH_NO_CUDA_GRAPH1缩小批次测试确认是否特定层引发这些报错在网络上一搜一大片但大部分人的问题其实是版本组合混乱而不是硬件坏了。我每次排查CUDA问题时会先执行一条组合命令把环境的关键信息一次性打出来nvidia-smi python -c import torch; print(torch, torch.__version__); print(cuda, torch.version.cuda); print(device, torch.cuda.get_device_name(0))一条命令看清驱动、PyTorch版本、CUDA版本、GPU型号至少能砍掉一半的排查路程。5.2 OOM频发不一定是显存真的不够GPU显存不足的报错很常见但我见过很多人一看到CUDA out of memory就急着换更大显存的卡其实很多时候是环境或代码习惯的问题。第一个非典型原因是显存碎片化。PyTorch的内存分配器会在多次动态申请释放后留下大量碎片导致明明总显存没满却申请不到连续大块内存。这个问题的排查方式很简单把torch.cuda.empty_cache()放在训练循环里或者在启动脚本里设PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128实测能缓解不少。第二个原因是数据加载时留了一部分显存给CUDA context。当你有4张卡但只在cuda:0上跑模型、数据也在cuda:0上复制其他卡空闲也帮不上忙显存自然不够。正确的做法是用DataParallel或分布式训练让所有卡分担显存压力。第三个原因是监控工具本身占显存。开着nvidia-smi的持续监控或某些profiler工具实际上会占用显存。如果训练时显存极其紧张先关掉这些工具再跑。# 逐步增加批次大小定位显存上限 python -c import torch, transformers m transformers.AutoModelForCausalLM.from_pretrained(开源的base模型) for bs in [1, 2, 4, 8]: try: out m(torch.randint(0, 5000, (bs, 512)).cuda()) print(fbatch {bs}: ok) except Exception as e: print(fbatch {bs}: failed - {e}) break 用这种方式能快速找到当前环境能承受的最大batch size而不是凭感觉设置。5.3 依赖冲突与回滚的正确姿势LLM生态的依赖冲突几乎躲不掉。最典型的就是transformers要求某个版本的tokenizers而另一个库又锁死了不同的版本。装库时报错信息又长又乱很多人一上来就pip uninstall乱删结果越删越乱。我的建议是引入级别的回滚点。在环境一切正常的时候先导出一份完整的锁定文件作为基线conda env export --no-builds envs/environment-export.yml pip freeze envs/requirements-lock.txt然后把这份基线提交到Git里。以后万一装新库把环境搞坏了直接用这两个文件重装conda env remove -n llm-lab conda env create -f envs/environment-export.yml conda activate llm-lab pip install -r envs/requirements-lock.txt这套提交前导出基线、出问题时整体回滚的策略帮我省了非常多的无谓耗时。它比手动去解决冲突要快得多因为LLM相关的依赖树实在太复杂人工梳理往往得不偿失。只有在极少数情况下比如某个库必须要最新版才能用新特性我才会手动升级某个包并且升级后立刻重新导出基线。经验之谈不要指望一次环境配置就能永久不折腾。LLM领域变化太快三个月前的最佳组合三个月后可能就找不到适配的库了。可复用的含义不是永不变更而是每次变更有据可依、可以回退。抱着这个心态环境管理就轻松很多。6. 最后再分享一点个人体会搭过这么多轮环境我最大的感受是环境的本质是工程问题不是技术问题。你不需要掌握每一个底层实现的细节但一定要把版本、依赖、变更记录这些工程要素管起来。用文档记录为什么选这个版本、装过哪些补救包、踩过哪些坑这些信息比任何自动化工具都宝贵。我现在每搭好一套环境都会顺手把关键信息写进项目的README里包括驱动的安装方式、CUDA Toolkit版本、mirror源地址、首次跑通的示例命令。下次不管是我自己还是同事接手都能在一小时内恢复全部工作状态。这套习惯看起来不起眼但在多次项目切换和多人协作中它带来的效率提升是实打实的。如果你也是刚准备开始做LLM训练不用追求一步到位先把conda环境、CUDA版本、PyTorch这一条主链路理顺剩下的交给增量迭代。环境问题不可怕可怕的是每次都在同一个坑里翻车。希望这篇LLM Training Lab的记录能帮你少踩几次坑把更多精力放到模型和实验本身。