Chapter 12
常见错误与解决:cpu_adam、stage 3 hang、bf16 NaN
📌 本章基于 GitHub issues 高频问题与本基线 commit 510ebe58e4e4 。 调试 DeepSpeed 的第一句口诀: 先跑 ds_report ,把输出贴 issue。
12.1 调试链路总览
| 序 | 第一步要看的 | 看什么 |
| 1 | ds_report | op 是否编译、CUDA / PyTorch / NCCL 版本 |
| 2 | nvidia-smi | 显存余量、GPU 利用率 |
| 3 | NCCL_DEBUG=INFO ./train.sh 2>&1 | tee log | 多机 hang 时尤其重要 |
| 4 | wall_clock_breakdown: true | 看 fwd/bwd/step 谁慢 |
| 5 | ds_config 的 pydantic 校验报错 | JSON 字段错 |
12.2 安装期错误(参见第 2 章 2.6 节)
| 现象 | 修复 |
nvcc fatal: unsupported gpu architecture | export TORCH_CUDA_ARCH_LIST="8.0;9.0" |
FusedAdam: [NO] | DS_BUILD_FUSED_ADAM=1 pip install -U --force-reinstall deepspeed --no-build-isolation |
CPUAdam: [NO] | DS_BUILD_CPU_ADAM=1 pip ... |
aio not built | apt install libaio-dev 后 DS_BUILD_AIO=1 |
12.3 启动期错误
| 报错 | 原因 | 修复 |
pydantic.ValidationError: ... train_batch_size | 三个 batch 字段不一致 | 留两个 + 一个 "auto" |
bf16 and fp16 cannot both be enabled | 同时开 bf16/fp16 | 二选一 |
RuntimeError: Address already in use | master_port 占用 | 换端口 / kill 残留进程 |
NCCL Error: unhandled cuda error | 多卡 driver 版本不一致 / NVLink 故障 | 升 / 一致化 driver;查 nvidia-smi topo -m |
FileNotFoundError: ds_config.json | 路径问题 | 绝对路径 / 在脚本所在目录运行 |
RuntimeError: missing required env: LOCAL_RANK | 用了 python 直接跑,没走 launcher | 用 deepspeed 或 torchrun |
12.4 ZeRO-3 hang(最难调)
典型表现:训练启动后 GPU 利用率 0,stdout 无新输出,几分钟后 NCCL timeout。可能原因:
| 原因 | 诊断方法 | 修复 |
某 rank forward 没碰到的层, 别人在 all-gather | 开 NCCL_DEBUG=INFO,看 rank 间通信不对称 | 检查模型是否有 if rank==0 等分支;统一控制流 |
| 一个 rank 早 return 了 | 同上 | 模型 forward 中所有 rank 必须执行同样数量的 module |
| 数据 sampler 不一致 | 各 rank 打印 batch shape | 用 DistributedSampler;不要自己 shuffle |
| 参数初始化没分布式 broadcast | 各 rank 打印 model 参数 hash | 初始化在 rank 0 → broadcast;或用 DS context manager |
| 用 PyTorch 调度 + DS 同时存在 | 看你是不是手写 backward() | 必须用 engine.backward() |
调 hang 的杀手锏:
# 1. 打开 NCCL 详细日志
export NCCL_DEBUG=INFO
export TORCH_DISTRIBUTED_DEBUG=DETAIL
# 2. 把 timeout 调大,让 hang 暴露出来而不是直接 fail
export NCCL_TIMEOUT=3600 # 1 小时
# 3. 打开 DS 自己的 wall clock breakdown,看哪一步卡住
# ds_config.json: "wall_clock_breakdown": true
12.5 bf16 / fp16 NaN
| 现象 | 多半原因 | 修复 |
| 训了几十步 loss 突变 NaN | fp16 数值溢出 | 改 bf16;降 LR;改 gradient_clipping |
| loss 一开始就 NaN | 数据预处理坏了 | 查 dataloader 给的 input 是否有 NaN / overflow token |
| bf16 训得稳但 eval 时 NaN | eval 没切 bf16 | HF Trainer 同时设 bf16=True, bf16_full_eval=True |
| loss scale 频繁缩水 | fp16 边界 | 升 initial_scale_power;或切 bf16 |
| grad norm 爆涨 | 样本 outlier / LR 过大 | 开 gradient_clipping: 1.0;剔脏样本 |
12.6 内存不足
| 现象 | 修复路线 |
| 第 1 步就 CUDA OOM | 开 activation_checkpointing、bf16;降 batch;改 ZeRO-2 → ZeRO-3 |
| 训几百步后 OOM | 碰到长样本;开 packing 或卡 cutoff_len;样本长度 sort |
| save_checkpoint 时 OOM | 开 stage3_gather_16bit_weights_on_model_save 时要小心:保存的是 rank 0 上 gather 后完整模型;CPU offload |
| 多卡某张 OOM 别张没事 | 用了 DataParallel?换 DDP / DS 模式;检查 --num_gpus 是否一致 |
| NVMe offload 时反复 OOM | 检查 offload_param.max_in_cpu;nvme_path 路径是否真 NVMe |
12.7 checkpoint / 保存恢复问题
| 现象 | 修复 |
ZeRO-3 保存后 pytorch_model.bin 不存在 | config 必须有 stage3_gather_16bit_weights_on_model_save: true |
| ZeRO-3 保存的是 shard,怎么合? | 用仓库自带 zero_to_fp32.py:python deepspeed/utils/zero_to_fp32.py CHECKPOINT_DIR OUTPUT_DIR [--max_shard_size 5GB] [--safe_serialization] [--tag global_step100] [--exclude_frozen_parameters] |
| resume 训练 step 不连续 | load_checkpoint 返回 (path, client_state),要从 client_state 取 step / epoch 接着跑 |
| 多卡数 / 配置改了之后 resume 失败 | DS checkpoint 强绑定 ZeRO stage + DP 数;改了要重新预训 |
| checkpoint 文件极大(占盘几百 GB) | 保存频率太高 / 没限制;设 save_total_limit;ZeRO-3 + offload 时只保存 16bit |
12.8 推理 / merge 错误
| 现象 | 修复 |
init_inference: tensor parallel not divisible | hidden_size / num_heads / vocab 不是 tp_size 倍数 |
| 推理输出乱码 | kernel_inject 替换错;试 replace_with_kernel_inject=False |
| 训完想 merge LoRA 但模型是 ZeRO-3 切的 | 先 zero_to_fp32.py 合一份完整 HF 权重再 merge |
12.9 多机训练特有问题
| 现象 | 修复 |
| ssh 跨机连不上 | 配 ssh 免密;~/.ssh/config 写好 Host;hostfile 用主机名 / 别名 |
| 所有进程读 ds_config.json 时 race | config 放共享文件系统,所有节点路径一致 |
| NCCL 卡在 init | 查防火墙是否开放 NCCL_PORT_RANGE;查 NCCL_SOCKET_IFNAME 是不是选对网卡 |
| 训练慢 5 倍 | 没启用 InfiniBand;NCCL_IB_DISABLE=0,确认 nvidia-smi topo 显示 NVLink/IB |
| 跨机 hostfile 写法不熟 | worker-1 slots=8 每行一台;slots 即 GPU 数 |
| 有的机器编译过 op,有的没编 | 提前 DS_BUILD_OPS=1 统一编;或共享 wheel 缓存 |
12.10 提交 issue 的最小信息清单
遇到 DS 自身 bug 时(不是配置 / 数据问题),给 GitHub 提 issue 前准备这些:
ds_report 完整输出;
pip list | grep -i 'torch\|deepspeed\|transformers\|accelerate\|peft\|bitsandbytes\|flash';
- OS 信息:
uname -a,CUDA 版本:nvcc --version,driver 版本:nvidia-smi;
- 能复现的最小脚本(≤ 50 行);
- 完整 ds_config.json;
- 完整报错堆栈(
NCCL_DEBUG=INFO + TORCH_DISTRIBUTED_DEBUG=DETAIL 时的);
- 用
deepspeed --num_gpus N script.py 时的 N、是否多机;
- 如果是 HF Trainer + DS,提供 TrainingArguments 的 dict 化输出。
12.11 Stage 3 "悄悄变慢":trace cache 进入 INVALID
ch05.11 提过 PartitionedParameterCoordinator 的三态 trace 机制。INVALID 状态不会报错,只是 Coordinator 退化为同步 all-gather,整个训练时间会几倍恶化。常见触发原因:
| 触发 | 修复 |
模型 forward 里有 if rank == 0: ... 这类 rank 分支 | 把所有 rank 的 module 访问序列对齐;rank 特定逻辑挪到训练循环里做 |
| 动态控制流(不同样本走不同 module) | 用 if/else 但确保所有 rank 走同一分支;否则不要 stage 3 |
| 非全 rank 访问的外部参数没注册 | 调 deepspeed.zero.register_external_parameter(consumer_module, param) |
| 动态构图(torch.compile / dynamic shape)频繁触发 recompile | 关闭 dynamic shape;用 packing 让 seq 长度齐整 |
诊断方法:
# Coordinator 内部有这个 flag,但默认不输出 warning
# 临时改源码:partitioned_param_coordinator.py 找 __log_trace_cache_warnings = True
# 或简单地:跑 5 步,断点看 coordinator.__trace_mode 的值
# COMPLETE = 健康;INVALID = 已退化
12.12 ZenFlow 启用条件错误
| 现象 | 原因 | 修复 |
ZenFlow requires PyTorch >= 2.1 | 老 PyTorch | 升 PyTorch 或换 SuperOffload |
ZenFlow requires cpu offload enabled | 没开 offload_optimizer.device=cpu | 补上 offload 字段,ZenFlow 只在 offload 路径下有意义 |
| 开了 ZenFlow + fused optimizer 第一步报错 | FusedAdam 与 ZenFlowSelectiveAdamW 冲突 | config 不要写 "optimizer": {"type": "AdamW"},让 DS 自动选 ZenFlow 版 |
| ZenFlow 训练 loss 很慢才下降 | full_warm_up_rounds 太长 | 调小 warmup;或 topk_ratio 调到 0.2+ |
12.13 Muon 优化器:fallback 与首步慢
| 现象 | 原因 | 修复 |
| 用 Muon 训练但发现 loss 第一步就 NaN | embedding / LM head 用了 Muon(Muon 对小张量不稳定) | 给这些 param_group 写 "use_muon": false,走 AuxAdam 兜底 |
| Muon 第一个 step 卡 30 秒 | Newton-Schulz 矩阵正交化迭代要 warm-up GPU kernel | 正常,不是 bug;后续步会快 |
| 多 GPU 时 Muon 训练吞吐反而比 AdamW 低 | Muon 自己的分布式版本需要 DS 包装 (MuonWithAuxAdam) | 确认 optimizer.type = "muon" 写在 ds_config 里,不要自己 import torch_muon |
| Muon checkpoint 加载到非 Muon 训练时报错 | Muon optimizer state 有自定义字段 | resume 必须保持优化器一致;不能 Muon 训一半切 AdamW |
心法
DS 这种"训练框架 + 系统软件"边界模糊的库,错误信息往往离根因很远。
调试时永远多问一句"我所看到的,真的是真相吗";
打开 NCCL/Distributed/DS 三层 verbose 日志,比猜半天有用 100 倍。