零基础入门Verl:在单卡P40上跑通LLM后训练的避坑实录
2026/4/21 13:10:58 网站建设 项目流程

零基础入门Verl:在单卡P40上跑通LLM后训练的避坑实录

1. 为什么是P40?——一个务实的学习起点

你手头只有一块2016年发布的Tesla P40,显存24GB,没有Tensor Core,不支持FP16/BF16,计算能力6.1。它早已被主流大模型训练环境“除名”,连官方文档里都找不到它的影子。

但正因如此,它成了检验一个框架真实易用性与工程鲁棒性的绝佳试金石。Verl作为字节跳动火山引擎开源的LLM后训练强化学习框架,主打“灵活、高效、可生产”,那它是否真的能向下兼容?能否让普通开发者在有限资源下,亲手走通从环境搭建、数据准备、模型加载到PPO训练的完整闭环?

这不是一篇追求SOTA指标的论文复现笔记,而是一份写给真实开发者的生存指南。它记录了我在单卡P40上,从第一次import verl成功,到最终看到step:1日志滚动出来的全过程——中间踩过的每一个坑、改过的每一行代码、查过的每一份文档,都原样保留。没有美化,不省略细节,只为让你少走几天弯路。

如果你也正面对一块“老但还能用”的GPU,想真正理解LLM后训练的底层脉络,而不是只在Colab里点几下鼠标,那么这篇实录,就是为你写的。

2. 环境配置:绕开Docker,直击Linux本体

Verl官方文档推荐Docker部署,但在国内网络环境下,docker pull常因认证失败或限流中断。更关键的是,P40对CUDA版本极其敏感——它只认CUDA 11.x,而当前主流镜像多基于CUDA 12构建。因此,我们放弃镜像,选择裸机+conda虚拟环境的硬核路径。

整个配置过程必须严格遵循顺序,任何一步错位,后续都将陷入无解循环。

2.1 CUDA 11.8:P40唯一可靠的底座

P40的计算能力(SM 6.1)决定了它无法运行CUDA 12编译的二进制。我们必须手动安装CUDA 11.8,并将其设为系统默认。

# 下载runfile(非deb/rpm) wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run sudo sh cuda_11.8.0_520.61.05_linux.run --toolkit --silent --installpath=/usr/local/cuda-11.8

安装后,永久写入环境变量:

echo 'export CUDA_HOME=/usr/local/cuda-11.8' >> ~/.bashrc echo 'export PATH=$CUDA_HOME/bin:$PATH' >> ~/.bashrc echo 'export LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc source ~/.bashrc

验证:nvcc --version应输出release 11.8, V11.8.89

2.2 cuDNN 8.9.7:与CUDA 11.8精确匹配

cuDNN版本必须与CUDA 11.8完全对应。下载cuDNN v8.9.7 for CUDA 11.x的runfile,解压并软链接至CUDA目录:

sudo tar -xzf cudnn-linux-x86_64-8.9.7.29_cuda11-archive.tar.gz sudo cp cudnn-linux-x86_64-8.9.7.29_cuda11-archive/include/cudnn*.h /usr/local/cuda-11.8/include sudo cp cudnn-linux-x86_64-8.9.7.29_cuda11-archive/lib/libcudnn* /usr/local/cuda-11.8/lib64 sudo chmod a+r /usr/local/cuda-11.8/include/cudnn*.h /usr/local/cuda-11.8/lib64/libcudnn*

2.3 Python 3.10 + PyTorch 2.6.0:稳定压倒一切

P40不支持PyTorch最新版的某些优化特性。经实测,torch==2.6.0+cu118是目前最稳定的组合。

conda create -n verl-env python=3.10 -y conda activate verl-env pip install torch==2.6.0+cu118 torchvision==0.21.0+cu118 torchaudio==2.6.0+cu118 --index-url https://download.pytorch.org/whl/cu118

2.4 Apex:为混合精度训练打基础(虽然后续不用)

尽管P40不支持BF16/FP16,Apex仍是Verl依赖链中的一环。直接按官方方式编译安装:

git clone https://github.com/NVIDIA/apex cd apex pip install -v --disable-pip-version-check --no-cache-dir --no-build-isolation --config-settings "--build-option=--cpp_ext" --config-settings "--build-option=--cuda_ext" .

2.5 Verl:源码安装,掌控每一行

Verl需从GitHub源码安装,以方便后续修改。注意,不要用pip install verl,那会安装旧版或不兼容版本。

git clone https://github.com/volcengine/verl.git cd verl # 安装依赖(含vLLM等) bash scripts/install_vllm_sglang_mcore.sh # 关键:以可编辑模式安装,确保修改即时生效 pip install --no-deps -e .

验证安装:

python -c "import verl; print(verl.__version__)" # 输出应为类似:0.1.0.dev0

3. 数据与模型:轻量级组合是P40的生存法则

P40的24GB显存,决定了我们无法挑战Qwen2.5-7B这类模型。必须选择小而精的组合:模型选Qwen2.5-0.5B-Instruct,数据集选GSM8K(数学推理题),二者均能在P40上完成端到端训练。

3.1 数据预处理:从Arrow到Verl专用Parquet

GSM8K原始格式为HuggingFace Dataset的Arrow文件。Verl要求输入为特定结构的Parquet,需两步转换:

第一步:导出为标准Parquet

# save_parquet.py from datasets import load_from_disk ds = load_from_disk("gsm8k_disk") # 本地已下载的arrow数据集 ds["train"].to_parquet("train.parquet") ds["test"].to_parquet("test.parquet")

第二步:注入Verl RL结构

修改verl/examples/data_preprocess/gsm8k.py,指定输入输出路径:

data_source = "train.parquet" # 原始parquet local_dir = "./gsm8k_fmt_rl" # Verl格式输出目录

运行脚本后,生成的train.parquet将包含prompt,chosen_response,rejected_response三列,这是PPO训练的最小数据单元。

3.2 模型下载:HuggingFace镜像加速

使用hf-mirror下载Qwen2.5-0.5B-Instruct,避免直连HF超时:

pip install huggingface-hub huggingface-cli download Qwen/Qwen2.5-0.5B-Instruct --local-dir ./Qwen2.5-0.5B-Instruct --repo-type model

4. 核心改造:让Verl在P40上真正“呼吸”

Verl默认为Ampere及更新架构设计,对P40的硬件限制缺乏适配。以下两处硬编码修改,是跑通训练的生死线

4.1 数据类型:从BF16到FP32的彻底降级

P40不支持BF16,尝试在命令行中通过--dtype=float32传递参数无效,因为Verl内部多处硬编码了torch.bfloat16。必须全局替换:

cd verl grep -r "bfloat16" --include="*.py" . | grep -v "__pycache__" # 找到所有匹配行,例如: # verl/actor_rollout_ref/actor/model.py: self.model = self.model.to(dtype=torch.bfloat16) # verl/critic/model.py: self.model = self.model.to(dtype=torch.bfloat16) # 全局替换(务必带引号,避免误伤) sed -i 's/"bfloat16"/"float32"/g' $(grep -rl "bfloat16" --include="*.py" .) sed -i 's/torch\.bfloat16/torch\.float32/g' $(grep -rl "torch\.bfloat16" --include="*.py" .)

为什么不是FP16?
P40硬件层面不支持FP16运算单元。强行启用会导致内核崩溃或静默错误。FP32是唯一安全、确定、可预测的选择。

4.2 Attention引擎:从FlashAttention-2到Eager的回归

FlashAttention-2依赖Ampere架构的Tensor Core和大容量共享内存(≥80KB),P40的48KB共享内存上限直接导致其编译失败。Verl内部若未显式指定,会默认尝试加载FlashAttention-2。

同样进行全局替换:

sed -i 's/"flash_attention_2"/"eager"/g' $(grep -rl "flash_attention_2" --include="*.py" .)

eager模式即PyTorch原生Attention,虽慢但稳,是P40上唯一可行的方案。

5. 训练启动:显存压榨的艺术

即使完成上述改造,Verl默认的batch size、序列长度等参数仍远超P40承载力。我们必须将所有内存相关参数调至理论下限。

5.1 启动脚本详解

以下脚本已在P40上实测通过,每行参数均有明确作用:

export HYDRA_FULL_ERROR=1 export VLLM_DTYPE=float32 export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 PYTHONUNBUFFERED=1 TRITON_MAX_SHARED_MEMORY=49152 python3 -m verl.trainer.main_ppo \ data.train_files=$HOME/data/gsm8k/fmt_rl/train.parquet \ data.val_files=$HOME/data/gsm8k/fmt_rl/test.parquet \ data.train_batch_size=1 \ data.max_prompt_length=256 \ data.max_response_length=256 \ actor_rollout_ref.model.path=$HOME/models/Qwen2.5-0.5B-Instruct \ actor_rollout_ref.actor.optim.lr=1e-6 \ actor_rollout_ref.actor.ppo_mini_batch_size=1 \ actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=1 \ actor_rollout_ref.rollout.name=vllm \ actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=1 \ actor_rollout_ref.rollout.tensor_model_parallel_size=1 \ actor_rollout_ref.rollout.gpu_memory_utilization=0.3 \ actor_rollout_ref.rollout.max_num_batched_tokens=512 \ ++actor_rollout_ref.rollout.enable_chunked_prefill=false \ ++actor_rollout_ref.fsdp_config.cpu_offload=true \ ++actor_rollout_ref.fsdp_config.offload_params=true \ actor_rollout_ref.rollout.max_num_seqs=1 \ actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=1 \ critic.optim.lr=1e-5 \ critic.model.path=$HOME/models/Qwen2.5-0.5B-Instruct \ critic.ppo_micro_batch_size_per_gpu=1 \ algorithm.kl_ctrl.kl_coef=0.001 \ trainer.logger=console \ trainer.val_before_train=False \ trainer.n_gpus_per_node=1 \ trainer.nnodes=1 \ trainer.save_freq=10 \ trainer.test_freq=10 \ trainer.total_epochs=2 2>&1 | tee verl_demo.log

关键参数解读:

  • data.train_batch_size=1:全局批次大小为1,不能再小。
  • ppo_micro_batch_size_per_gpu=1:每个GPU的微批次为1,是内存控制的核心。
  • max_num_batched_tokens=512:必须 ≥max_prompt_length + max_response_length,否则vLLM会报错。
  • gpu_memory_utilization=0.3:仅允许vLLM使用30%显存,为其他组件留足空间。
  • ++...cpu_offload=true:开启CPU卸载,将部分参数暂存内存,缓解显存压力。
  • TRITON_MAX_SHARED_MEMORY=49152:显式告知Triton,P40最大共享内存为48KB(单位:KB)。

5.2 日志观察:如何确认训练真正“活”了

成功启动后,终端将输出类似日志:

(TaskRunner pid=12345) step:1 - ... Training Progress: 0%| | 1/14946 [00:07<30:17:02, 7.29s/it] (TaskRunner pid=12345) step:2 - ... Training Progress: 0%| | 2/14946 [00:13<28:37:28, 6.90s/it]

这表示Actor、Critic、Rollout三个核心模块均已初始化完毕,开始执行PPO的rollout→reward→learn循环。此时,nvidia-smi应显示GPU显存占用稳定在~22GB,GPU利用率在30%-60%之间波动——这是P40健康工作的标志。

6. 常见报错与根因分析:不再“玄学”调试

所有报错,本质都是P40硬件能力与Verl默认假设之间的冲突。理解根因,才能举一反三。

6.1 “no kernel image is available for execution on the device”

根因:CUDA版本不匹配。PyTorch或Triton的二进制是为CUDA 12编译的,P40无法加载。

解法:重装CUDA 11.8 + PyTorch 2.6.0+cu118,这是唯一解。不要尝试降级PyTorch而不换CUDA。

6.2 “Bfloat16 is only supported on GPUs with compute capability of at least 8.0”

根因:Verl代码中存在硬编码的torch.bfloat16,且未提供运行时覆盖接口。

解法:全局文本替换。记住,float16也不行,必须是float32

6.3 “OutOfResources: shared memory, Required: 81920, Hardware limit: 49152”

根因:两个独立问题叠加:

  • FlashAttention-2 kernel要求共享内存 > 80KB;
  • Triton自动生成的kernel因batch size过大,申请了超出P40上限的shared memory。

解法

  • 第一层:替换flash_attention_2eager
  • 第二层:将所有batch size相关参数(train_batch_size,micro_batch_size,max_num_batched_tokens)降至1或最小值;
  • 第三层:显式设置TRITON_MAX_SHARED_MEMORY=49152,强制Triton遵守硬件限制。

6.4 训练进行到step 8-9后再次OOM

现状:此问题尚未完美解决。现象是前几步内存平稳,第8步后显存缓慢爬升直至溢出。

推测根因

  • vLLM的KV Cache在长序列推理中持续累积,未被及时清理;
  • FSDP的梯度检查点(gradient checkpointing)在P40上触发异常内存碎片;
  • Critic模型与Actor模型共享部分权重,但卸载策略未同步。

临时缓解

  • trainer.total_epochs设为1,仅跑通单轮验证流程;
  • verl/trainer/main_ppo.py中,于每次step结束时手动调用torch.cuda.empty_cache()
  • 改用--fsdp_config.shard_param_on_cpu=True,进一步激进地将参数卸载至CPU。

这是一个开放问题,期待社区贡献更优雅的解决方案。

7. 总结:P40上的Verl,教会我们的事

在P40上跑通Verl,远不止是“让代码动起来”。它是一次对现代AI框架底层逻辑的深度拆解:

  • 框架的“灵活性”不等于“无条件兼容”。Verl的HybridFlow设计确实精巧,但其默认配置隐含了对新一代GPU的强依赖。真正的灵活性,体现在它允许你通过修改几行代码,就将其拉回旧硬件的轨道。
  • 显存不是数字,而是时间与空间的精密博弈max_num_batched_tokens=512不是拍脑袋定的,它是256+256的数学必然;gpu_memory_utilization=0.3不是保守,而是为vLLM的动态内存管理预留的安全边际。
  • 开源的价值,在于可读、可改、可验。当文档缺失、镜像失效、报错晦涩时,grep -rsed -i就是你最锋利的手术刀。Verl的代码组织清晰,模块边界明确,让这种“外科手术式”修复成为可能。

这条路很难,但每一步都扎实。当你在P40的终端里,第一次看到step:1的进度条滚动起来,那一刻的成就感,远胜于在顶级A100上一键跑通SOTA。因为你知道,你真正理解了它,而不仅仅是调用了它。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询