🚀 海光 异构卡2 DCU (4×16G) 跑 DeepSpeed ZeRO-3 完整避坑指南
一、 核心问题总结(为什么总是崩溃?)
pip show deepspeed | grep Version
Version: 0.14.2+das.opt1.dtk25041
deepspeed 0.14.2+das.opt1.dtk25041
原来的配置
{
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"gradient_accumulation_steps": "auto",
"gradient_clipping": "auto",
"zero_allow_untested_optimizer": true,
"fp16": {
"enabled": "auto",
"loss_scale": 0,
"loss_scale_window": 1000,
"initial_scale_power": 16,
"hysteresis": 2,
"min_loss_scale": 1
},
"bf16": {
"enabled": "auto"
},
"zero_optimization": {
"stage": 3,
"overlap_comm": true,
"contiguous_gradients": true,
"sub_group_size": 1e9,
"reduce_bucket_size": 1e8,
"stage3_prefetch_bucket_size": 1e8,
"stage3_param_persistence_threshold": 1e6,
"stage3_max_live_parameters": 3e8,
"stage3_max_reuse_distance": 3e8,
"stage3_gather_16bit_weights_on_model_save": false
}
}
你在训练中遇到了三个大坑,根本原因如下:
二、 最终配置文件(直接复制使用)
1. 启动脚本 (run.sh)
最关键的是加上 PYTORCH_HIP_ALLOC_CONF,这是解决保存后 OOM 的核心!
#!/bin/bash
# ====== 1. 清理残留进程 ======
pkill -9 python
pkill -9 torchrun
pkill -9 llamafactory-cli
sleep 3
# ====== 2. 核心环境变量 ======
# 【救命配置】解决显存碎片化导致的 36MiB OOM
export PYTORCH_HIP_ALLOC_CONF=expandable_segments:True,max_split_size_mb:128
# 指定使用 4 张卡(海光 DCU 用 ROCR,NVIDIA 用 CUDA)
export ROCR_VISIBLE_DEVICES=0,1,2,3
# 防止多卡保存时因硬盘慢而通信超时(30分钟)
export NCCL_TIMEOUT=1800
# ====== 3. 启动训练 ======
FORCE_TORCHRUN=1 llamafactory-cli train \\
"/public/home/a15657581978/training_configs_512/binding_paired_000.yaml"
2. DeepSpeed 配置 (deepspeed_z3_offload_4x16g.json)
注意:所有浮点数必须写成纯整型(如 1e9 写成 1000000000),gradient_accumulation_steps 必须写死为 1!
{
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"gradient_accumulation_steps": 1,
"gradient_clipping": "auto",
"zero_allow_untested_optimizer": true,
"fp16": {
"enabled": "auto",
"loss_scale": 0,
"loss_scale_window": 1000,
"initial_scale_power": 16,
"hysteresis": 2,
"min_loss_scale": 1
},
"bf16": {
"enabled": "auto"
},
"zero_optimization": {
"stage": 3,
"overlap_comm": false,
"contiguous_gradients": true,
"sub_group_size": 1000000000,
"reduce_bucket_size": 200000000,
"stage3_prefetch_bucket_size": 20000000,
"stage3_param_persistence_threshold": 1000000,
"stage3_max_live_parameters": 500000000,
"stage3_max_reuse_distance": 500000000,
"stage3_gather_16bit_weights_on_model_save": false
}
}
3. YAML 训练配置关键修改项
在你的 YAML 中,必须确认以下三个参数的值,其余保持不变:
# 【必须改】ZeRO-3 必须保存优化器,否则保存时死锁
save_only_model: false
# 【必须改】加大超时时间,防止硬盘写入慢导致多卡失联
ddp_timeout: 18000000
# 【不用改】保持 16,由 HuggingFace Trainer 做累积,绕开 0.14.2 的 Bug
gradient_accumulation_steps: 16
# 【建议改】如果加了环境变量还是偶发 OOM,把序列长度降低
cutoff_len: 768
# 【建议改】减少保存频率,降低"保存后冷启动"带来的 OOM 风险
save_steps: 500
三、 训练结束后的模型导出
因为你设置了 stage3_gather_16bit_weights_on_model_save: false,训练保存的 checkpoint 是零碎的分片,不能直接用 transformers 加载。必须用 LLaMA-Factory 的导出命令,在 CPU 上拼接并合并 LoRA:
llamafactory-cli export \\
–model_name_or_path /public/home/a15657581978/merged_correct_2epoch \\
–adapter_name_or_path /public/home/a15657581978/output/binding_part_000_512 \\
–template alpaca \\
–finetuning_type lora \\
–export_dir /public/home/a15657581978/exported_final_model \\
–export_device cpu \\
–export_legacy_format false
–export_device cpu 是关键,借用系统内存拼接完整模型,避免 16G 显卡被撑爆。
四、 终极降级方案
如果按照上述配置配置后,依然在第 101 步 OOM,说明 4×16G 跑 ZeRO-3 确实顶不住 Qwen2 的显存波动。此时请降级到 ZeRO-2,稳定优先:
修改 ds_config.json:
{
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"gradient_accumulation_steps": "auto",
"gradient_clipping": "auto",
"zero_allow_untested_optimizer": true,
"fp16": { "enabled": "auto" },
"bf16": { "enabled": "auto" },
"zero_optimization": {
"stage": 2,
"overlap_comm": false,
"contiguous_gradients": true,
"reduce_bucket_size": 300000000,
"allgather_bucket_size": 300000000
}
}
ZeRO-2 不切分模型参数,没有保存时的 AllGather 冷启动问题,极其稳定,但能支持的模型上限比 ZeRO-3 小。
这个发现非常关键!你亲眼看到了真相:
save_only_model: true 在 ZeRO-3 下根本没用!
你设置了 save_only_model: true,本以为只存模型,结果:
4 × model_states.pt (7.65GB × 4 = 30.6GB)
4 × optim_states.pt (825MB × 4 = 3.3GB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总计: ~33.9GB 一个 checkpoint!
优化器状态一个都没少存!
为什么会这样?
save_only_model: true 的本意:
HuggingFace Trainer → "我只保存模型权重,优化器、调度器、随机种子全扔了"
→ 省空间 ✓
但在 ZeRO-3 下实际发生的事:
HuggingFace Trainer → 告诉 DeepSpeed "只存模型"
DeepSpeed → "我不听,ZeRO-3 的模型碎片和优化器是绑定的,必须一起存"
→ 照样存了 model_states + optim_states
→ save_only_model: true 形同虚设!
在 ZeRO-3 中,优化器状态里藏着模型参数的 fp32 主副本和分片映射关系,DeepSpeed 不可能只存模型不存优化器。
所以你之前的注释:
# 保存 checkpoint 时不保存 global_step
# 防止内存溢出 oom
save_only_model: true # ← 这个在 ZeRO-3 下根本不生效!
这个设置既没有省空间,反而还可能制造混乱(HuggingFace Trainer 以为优化器没存,跳过了某些元数据的保存,导致恢复训练时状态不对)。
这个发现也解释了 OOM 的真正原因
写 33.9GB 的数据到硬盘,耗时非常长。你看之前的日志:
08:17:40 开始保存
08:17:53 保存完成 ← 整整 13 秒!4张卡各写了 ~8.5GB
08:17:57 第 101 步 OOM
13 秒的磁盘 IO 期间,4 张卡的 GPU 显存布局被彻底搅乱:
- 参数缓存池被清空
- 内存碎片大量产生
- 恢复训练时的 AllGather 冷启动峰值更高
这才是"每次保存后就 OOM"的根本原因。
解决方案
第 1 步:把 save_only_model 改成 false
既然 DeepSpeed 反正都要存优化器,不如让 HuggingFace Trainer 和 DeepSpeed 的保存行为保持一致,避免元数据缺失导致恢复训练时出问题:
save_only_model: false
第 2 步:大幅拉长保存间隔 + 限制保存数量
33.9GB 一个 checkpoint,太大了。减少保存频率,降低"保存后 OOM"的风险:
save_steps: 500 # 从 100 改成 500,减少被打断的次数
save_total_limit: 1 # 只保留最新的 1 个,防止硬盘被撑爆
# 33.9GB × 2 = 67.8GB,很快就满了
第 3 步:开启动态显存分配(必须!)
export PYTORCH_HIP_ALLOC_CONF=expandable_segments:True,max_split_size_mb:128
这是解决"保存后碎片化导致 36MiB 分配不出来"的关键。
修改汇总(只列出需要改的行)
YAML:
save_only_model: false # ZeRO-3 下这个没意义,改成 false 保持一致性
save_steps: 500 # 减少保存频率
save_total_limit: 1 # 只留最新的 1 个
ddp_timeout: 18000000 # 30 分钟超时
启动脚本:
#!/bin/bash
pkill -9 python; pkill -9 torchrun; sleep 3
export PYTORCH_HIP_ALLOC_CONF=expandable_segments:True,max_split_size_mb:128
export ROCR_VISIBLE_DEVICES=0,1,2,3
export NCCL_TIMEOUT=1800
FORCE_TORCHRUN=1 llamafactory-cli train \\
"/public/home/a15657581978/training_configs_512/binding_paired_000.yaml"
ds_config.json(不变,保持之前改好的版本):
{
"gradient_accumulation_steps": 1,
"overlap_comm": false,
"reduce_bucket_size": 200000000,
"stage3_prefetch_bucket_size": 20000000,
"stage3_max_live_parameters": 500000000,
"stage3_max_reuse_distance": 500000000,
"stage3_gather_16bit_weights_on_model_save": false
}
一张图总结
你原来的理解: 实际情况:
┌─────────────────────┐ ┌─────────────────────┐
│ save_only_model:true │ │ save_only_model:true │
│ → 只存模型 │ │ → DeepSpeed 无视 │
│ → 省空间 ✓ │ │ → 照存 33.9GB │
│ → 防 OOM ✓ │ │ → 13秒磁盘IO │
└─────────────────────┘ │ → 保存后碎片化 │
│ → 第101步 OOM 💥 │
└─────────────────────┘
改后的策略:
┌─────────────────────────────────────────┐
│ save_only_model: false (保持一致性) │
│ save_steps: 500 (减少被打断) │
│ save_total_limit: 1 (省硬盘) │
│ PYTORCH_HIP_ALLOC_CONF (防碎片化) │
│ ds_config: 小桶 + 不重叠 (降峰值显存) │
└─────────────────────────────────────────┘
原来你写的是 "auto" 啊!这就更危险了,我来给你揭秘 "auto" 在背后到底干了什么,你就明白为什么必须强行改成 1 了。
"auto" 的真相:它最终还是变成了 16!
在 DeepSpeed 配置文件中,当你写 "gradient_accumulation_steps": "auto" 时,它的意思是:“DeepSpeed,你别管了,去问 Hugging Face Trainer 要这个数值。”
然后发生的流程是这样的:
所以,你写 "auto" 和写 16,在 DeepSpeed 眼里完全是一回事。
为什么 "auto" 会触发 0.14.2 版本的死锁 Bug?
当 DeepSpeed 拿到 gradient_accumulation_steps = 16 后,它会尝试自己控制 no_sync()(即前 15 步不进行跨卡梯度同步,第 16 步才同步)。
但在 DeepSpeed 0.14.2 和 Hugging Face Trainer 的结合中,这俩哥们抢夺了控制权:
- HF Trainer 也想发 no_sync() 指令。
- DeepSpeed 底层也在发 no_sync() 指令。
- 结果就是状态机错乱。在保存 checkpoint 时,DeepSpeed 以为还在 no_sync 期间不需要同步,而其他卡以为已经同步了,最终导致多卡互相等待,死锁超时!
为什么必须强行写 1?
把 ds_config.json 里的 gradient_accumulation_steps 强行写成 1,本质上是缴了 DeepSpeed 的械:
"gradient_accumulation_steps": 1
这告诉 DeepSpeed:“你每一步都给我老老实实做梯度同步,不要搞什么累积延迟!”
那你的有效 Batch Size 会变吗?完全不会!
因为 YAML 里的 gradient_accumulation_steps: 16 没变,HF Trainer 依然会在外层控制:连续算 16 步微批次,把 loss 加起来,然后再更新一次权重。
区别仅仅是:
- 写 auto (实际为16):由 DeepSpeed 底层控制,跨卡通信延迟,触发死锁 Bug。
- 写 1:由 HF Trainer 控制,每一步都正常通信,完美避开死锁 Bug,数学结果完全等价。
总结
你的 "auto" 恰恰是导致你之前保存时死锁/超时的元凶之一!请务必在 ds_config.json 中把它改成整数 1,这是在 DeepSpeed ZeRO-3 下保证稳定性的金科玉律。
deepspeed 已知的bug
下面按「版本区间 + 问题类型」把 DeepSpeed(尤其是 ZeRO-3 相关)的典型坑整理一下,重点是你踩过的那几个:梯度累积 / no_sync、保存 checkpoint 死锁/假死、OOM 等。
说明:DeepSpeed 目前主线版本是 0.16.x / 0.19.x,“2.61 以上”这个版本号在官方发行版里并不存在,所以我按 0.10+、0.14–0.16、0.19+ 这样的大版本来总结。
一、和 ZeRO-3 / 梯度累积(no_sync)相关的典型 Bug
1. no_sync 与 ZeRO-2/3 梯度分区冲突(你踩的核心坑)
典型报错:
AssertionError: no_sync context manager is incompatible with gradient partitioning logic of ZeRO stage 2/3
本质原因:
- ZeRO-2/3 会对梯度做分区(每个 GPU 只存一部分),需要严格保证梯度同步的时机;
- PyTorch 的 no_sync() 上下文管理器在梯度累积时故意跳过 All-Reduce;
- 两者同时启用时,梯度分区的同步逻辑和 no_sync 的“不同步”逻辑互相打架,导致断言失败或死锁。这个问题在社区里多次被提到,是 ZeRO-2/3 + 梯度累积的经典冲突。
涉及版本: - DeepSpeed 0.14.x ~ 0.16.0 都有用户反馈此问题,尤其在 0.16.0 上特别明显。
- 社区普遍推荐:降级到 0.15.4 可以规避。
- 0.15.4 的 release notes 主要是一些补丁修复,并没有专门提到这个问题的根本修复,只是该版本在实践上更稳定。
工程上的通用规避方法(不限版本):
然后在 HuggingFace Trainer / 自己训练循环里控制累积步数(YAML 里 gradient_accumulation_steps: 16 不用改)。这样 DeepSpeed 每一步都同步梯度,不进入 no_sync 模式,就不会和 ZeRO-2/3 的梯度分区冲突。
ZeRO-2/3 本身已经有复杂的通信调度,不要再套一层 no_sync,否则几乎必然踩坑。
2. gradient_accumulation_steps: "auto" 导致 DeepSpeed 接管累积并和 Trainer 冲突
现象:
- ds_config 写 "gradient_accumulation_steps": "auto",DeepSpeed 会去读 Trainer 的设置,最终变成 16;
- DeepSpeed 自己控制 no_sync,和 HuggingFace Trainer 的梯度累积逻辑重叠,在保存 checkpoint 时出现各种假死 / 死锁 / NCCL 超时。
建议: - 在 ds_config 中永远写死整数,不要写 "auto":"gradient_accumulation_steps": 1
- 把真正的累积步数交给 Trainer / 外层训练循环控制。
二、保存 Checkpoint 时的死锁 / 假死 / NCCL 超时
1. ZeRO-3 + save_only_model: true 导致多卡保存失步
现象(和你之前的情况一模一样):
- 日志里只有 Rank 0 在保存 model_states / optim_states,看不到其他 Rank 的保存日志;
- 保存完后继续训练时,Rank 1 在 compute_loss / forward 中崩溃,NCCL 超时。
根因: - ZeRO-3 的模型参数和优化器状态是深度绑定的,优化器里还藏着参数的 FP32 主副本;
- save_only_model: true 让 HuggingFace Trainer 跳过优化器保存,DeepSpeed 却还在按“必须存优化器”的逻辑走,导致多卡保存流程对不上,互相等屏障超时。
解决方案: - ZeRO-3 下必须:save_only_model: false
- 并适当加大超时:ddp_timeout: 18000000 # 30 分钟
2. 保存耗时太长,触发 NCCL 超时 / 被误判为挂掉
现象:
- 4×16G 卡保存一个大 checkpoint(几十 GB)要十几秒甚至更久;
- 其他卡等不及,超过 ddp_timeout(默认 180s)就报 NCCL 超时 / SIGTERM。
解决方案: - 把 ddp_timeout 调大(比如 1800s 或 18000s);
- 尽量把 checkpoint 写到本地 SSD 而不是慢速网络盘;
- 减少保存频率(save_steps: 500 而不是 100)。
3. ZeRO-3 checkpoint 恢复时加载失败 / 状态不对
常见问题:
- 直接用 torch.load() 加载 zero_pp_rank_*.pt,会丢失分片信息,导致参数拼不回来;
- HuggingFace Trainer 在 ZeRO-3 下直接 model.load_state_dict() 也会报错或状态缺失。
正确做法: - 用 DeepSpeed 提供的 load_checkpoint / save_checkpoint,或官方 zero_to_fp32.py 脚本先合并分片再加载;
- LLaMA-Factory 的 export 命令其实就是在做类似的事情,并支持 –export_device cpu 来避免 GPU OOM。
三、保存后继续训练时的 OOM(显存碎片 / 冷启动峰值)
1. 保存后第 101 步 OOM,而不是保存时 OOM
现象(和你看到的完全一致):
- 保存 checkpoint 本身成功,显存没爆;
- 4 秒后恢复训练的第 101 步,在 softmax / attention 等小分配上 OOM:torch.OutOfMemoryError: HIP out of memory. Tried to allocate 36.00 MiB.
原因:
- 保存时把参数从 GPU 拷到 CPU 再写磁盘,会打乱 ZeRO-3 的参数缓存池,清空“热参数”缓存;
- PyTorch/HIP 的显存分配器在频繁分配/释放后产生大量碎片(你看到的 571MiB reserved but unallocated 就是碎片);
- 第 101 步是 AllGather 冷启动:需要临时重新收集完整参数,峰值显存比稳态高,刚好把那 1.5GiB 余量吃光,连 36MiB 都分配不出来。
解决方案(你现在已经用的那一套):
"stage3_prefetch_bucket_size": 20000000,
"stage3_max_live_parameters": 500000000
四、0.10.x vs 0.14–0.16 vs 0.19+ 的典型差异 & Bug 分布
1. 0.10.x 时代(早期 ZeRO-3)
特点:
- ZeRO-3 刚引入,通信和内存管理相对粗糙;
- 社区反馈较多的是:
- ZeRO-3 通信死锁(尤其和梯度累积、多机组合时);
- 某些 CPU Offload 场景下 OOM / crash。
建议:
- 新项目不建议再用 0.10.x,除非你锁死了老环境;
- 如果在用,优先关闭 overlap_comm,把 reduce_bucket_size / allgather_bucket_size 调小。
2. 0.14–0.16(你踩坑最集中的版本)
核心问题:
- 0.14.x ~ 0.16.0 上都有用户报告 no_sync incompatible with gradient partitioning 错误;
- 0.16.0 尤其明显,社区建议降级到 0.15.4。
- ZeRO-3 + save_only_model: true + 慢磁盘 → 多卡失步 → NCCL 超时;
- 0.14/0.15 对 ZeRO-3 的保存路径还不够稳健,容易出现 Rank 0 写完、其他 Rank 没对齐的情况。
- 在海光 DCU 等环境下,0.14–0.16 对 expandable_segments 等新特性支持不完善,更容易碎片化 OOM;
- 后来 0.19.x 才专门加了 Zero3 defragment utility 等工具。
实用建议:
- 如果你在 0.14–0.16 上训练 ZeRO-3:
- ds_config:gradient_accumulation_steps: 1,overlap_comm: false;
- YAML:save_only_model: false,ddp_timeout: 18000000;
- 环境变量:PYTORCH_HIP_ALLOC_CONF=expandable_segments:True,max_split_size_mb:128;
- 能升级的话,推荐升级到 0.15.4 或 0.19.x,而不是留在 0.14.x / 0.16.0。
3. 0.19.x(当前最新线)
主要改进:
- 修复了进程组关闭时卡死的问题(训练结束 hang 住);
- 新增 Zero3 defragment utility,专门处理 ZeRO-3 的显存碎片问题;
- 修复 ZeRO-3 forward crash、test_zf.py hang 等稳定性问题;
- 对 CPU Offload、FP16/BF16 优化器状态、torch.compile / torch.func 等兼容性更好。
仍然要注意的: - ZeRO-3 + no_sync 的根本问题在 0.19.x 依然存在(这是设计层面的冲突),所以:
- 仍然建议 gradient_accumulation_steps: 1,把累积交给上层框架;
- 0.19.x 对 PyTorch 版本要求更高(需要 2.11+ 等),升级时要核对兼容性。
五、其他版本相关的常见坑(非 ZeRO-3 专属)
- Windows / 某些 CUDA 版本下,pip install deepspeed 经常遇到 dskernel / async_io 编译失败,需要从源码编译或禁用某些 ops;
- 多 CUDA 版本共存时,PyTorch 编译用的 CUDA 和系统 CUDA 不匹配,导致 CUDAMismatchException。
- CPU 内存不足时,ZeRO-3 + Offload 会直接被 OOM Killer 杀掉,表现为 exit code -9;
- 某些版本 CPUAdam 的 C++ 扩展编译失败,导致 AttributeError: 'DeepSpeedCPUAdam' object has no attribute 'ds_opt_adam'。
- ZeRO-3 的 checkpoint 是分片的,直接用 HuggingFace from_pretrained 加载会报各种 KeyError / TypeError,必须先用 zero_to_fp32.py 合并。
六、给你一个「版本选择 + 配置模板」的实用总结
- 新项目:优先 0.19.x,稳定性与碎片管理都更好;
- 如果在 0.16.x 上踩到 no_sync 冲突,至少升级到 0.15.4 或直接上 0.19.x;
- 尽量避免在 0.14.x / 0.16.0 上长期跑生产级 ZeRO-3 训练。
"gradient_accumulation_steps": 1,
"overlap_comm": false,
"contiguous_gradients": true,
"reduce_bucket_size": 200000000,
"stage3_prefetch_bucket_size": 20000000,
"stage3_max_live_parameters": 500000000,
"stage3_max_reuse_distance": 500000000,
"stage3_gather_16bit_weights_on_model_save": false
}
YAML 里:save_only_model: false
ddp_timeout: 18000000
save_steps: 500
save_total_limit: 1
export NCCL_TIMEOUT=1800
按这套组合,大部分你遇到的“保存后 OOM / 死锁 / no_sync 冲突”都可以稳定规避。




