NeMo Speech 分布式训练排障实战:debug-training-logs 技能的日志根因定位方法论
2026/9/13 8:00:52 网站建设 项目流程

NeMo Speech 分布式训练排障实战:debug-training-logs 技能的日志根因定位方法论

【免费下载链接】SpeechA scalable generative AI framework built for researchers and developers working on Large Language Models, Multimodal, and Speech AI (Automatic Speech Recognition and Text-to-Speech)项目地址: https://gitcode.com/GitHub_Trending/nem/Speech

本文以 NeMo Speech 仓库中.claude/skills/下的debug-training-logs技能文档为核心,完整拆解一套面向大规模分布式训练(NeMo、Megatron、PyTorch)失败日志的排查方法论:从收集 SLURM worker stderr 日志与 AIStore daemon 日志开始,经过分级错误搜索、NCCL 状态逐 rank 核验、AIStore 存储侧计数器分析,最终输出带因果链的根因报告。读完后你将掌握:如何在多 rank 级联故障中区分「触发者」与「受害者」、如何用enqueued/completedwork 计数判断卡死发生在 CPU 数据加载还是 GPU 通信、以及 NeMo 每步同步点(PreemptionCallback、DDP allreduce、broadcast_buffers)如何放大单 rank 停顿。

1. debug-training-logs:一个面向训练日志的排障技能

debug-training-logs是 NeMo Speech 仓库中为 AI 辅助排障设计的技能文件,完整定义在 SKILL.md。它与同目录下的 fix-issue、migrate-to-resumable-dataloader 等技能共同构成了该仓库的 Agent 排障/迁移工作流集。技能 frontmatter 声明了它的输入契约与能力边界:

name: debug-training-logs description: Debug distributed training failures (NeMo, Megatron, PyTorch) from worker stderr logs and optional AIStore daemon logs. Finds root cause across NCCL timeouts, data loading errors, and storage failures. disable-model-invocation: true allowed-tools: Bash Read Grep Glob Agent argument-hint: <path-to-logs-dir> [ais-logs-dir]

两个关键输入目录:

  • Worker 日志(必填,$ARGUMENTS[0]):SLURM/torchrun 产生的 stderr 文件,即每个计算节点上形如error-<JOBID>-N.out的文件;
  • AIStore daemon 日志(可选,$ARGUMENTS[1]):AIS proxy/target 的 tarball 或已解压目录,用于下钻存储侧问题。

技能开篇即给出核心诊断哲学:找根因,而不是症状。分布式训练失败普遍存在级联效应——一个根因会触发大量下游错误,正确的做法是从最终崩溃点反向回溯到最初的触发者。disable-model-invocation: true表明该技能不会由模型自动触发,必须由用户显式调用并传入日志目录。

2. 验证纪律:防止把症状当成根因

技能文档用专门一节("CRITICAL: Verification discipline")规定了五条强制性核验规则,这是整篇文档最有价值的方法论约束:

  1. 检查全部 rank,而不是抽样。看 5 个 rank 就假设其余 123 个相同是大忌。必须对完整输出做sort -u找离群值——单个离群 rank 可能就是整个根因。
  2. 核验每一个 rank 的 NCCL 状态。抽取所有 rank 的last enqueued worklast completed work,找出任何不同的 rank。enqueued == completed(无未决操作)与enqueued > completed(有未决操作)的 rank 本质不同:前者根本没进入卡死的集合通信。
  3. 区分「First PG on this rank to signal dumping」与「Observed flight recorder dump signal from another rank」。前者是发起者(自己的 watchdog 触发),后者只是被通知(它甚至可能不在集合通信里)——这是两种完全不同的失效模式。
  4. 下结论前回到原始日志复核。重新读取实际日志行,不要依赖此前写下的摘要——摘要可能是错的。
  5. 分析过程中如果结论变了,必须显式说明原来哪里错了、为什么改。不允许悄悄更换结论。

3. Phase 0:收集完整日志

如果用户未提供 worker 日志,第一步是要求下载 SLURM error 日志(通常位于计算节点或共享文件系统):

# 从用户机器 SCP 拉取 SLURM error 日志: scp <cluster>:/path/to/slurm/logs/error-<JOBID>-*.out ./training_logs/ # 或者日志位于登录节点可访问的共享文件系统: mkdir -p ./training_logs cp /path/to/slurm/error-<JOBID>-*.out ./training_logs/

硬性要求:必须提供全部 per-node error 文件error-JOBID-0.outerror-JOBID-N.out),只给一个节点的文件无法定位是哪个 rank 引发了失败。

4. Phase 1:Worker 日志分诊

4.1 先理解这个作业

读取几个日志文件的前 80 行,确定四件事:框架(NeMo、Megatron、PyTorch Lightning、DeepSpeed 等)、规模(GPU 数、节点数、每节点 rank 数)、作业在做什么(训练/微调/推理)、是否从 checkpoint 恢复。这些信息决定了后续搜索的预期 rank 总数与同步点集合。

4.2 按三级优先级搜索致命错误

所有日志文件中并行搜索以下模式(按优先级分层):

Tier 1 —— 进程级杀手:

NCCL.*timeout|Watchdog caught collective operation timeout taking the entire process down SIGTERM|SIGKILL|SIGABRT CUDA error|CUDA out of memory|OOM

Tier 2 —— 训练循环崩溃:

RuntimeError|Exception.*Error AISBatchLoaderError|StopIteration Traceback \(most recent call last\)

Tier 3 —— 数据加载 / IO:

Connection reset|Connection broken|Connection refused retrying [0-9]+/[0-9]+ timed out|deadline exceeded broken pipe

AISBatchLoaderError是 AIStore batch 加载路径中「实际返回对象数少于请求数」的典型报错,它定义在 Lhotse/AIS 客户端数据加载栈中,本仓库不定义该异常,但针对 AIS batch 加载的集成测试可在 test_lhotse_multimodal_ais_get_batch.py 中找到。

4.3 识别发起者与拖后腿者(straggler)

对 NCCL timeout,必须检查所有 rank。技能给出三条可直接复制的命令:

# 所有自己 watchdog 触发的 rank(发起者)及其 NCCL work 计数: grep "failure detected by watchdog" error-*.out | grep -o "Rank [0-9]*.*last enqueued work: [0-9]*, last completed work: [0-9]*" | sort -u # 被"通知"的 rank(自己 watchdog 未触发): grep "Observed flight recorder dump signal from another rank" error-*.out | grep -o "Rank [0-9]*" # 统计实际出现 watchdog 失败的唯一 rank 数,与预期总数比对: grep "failure detected by watchdog" error-*.out | grep -o "Rank [0-9]*" | sort -t' ' -k2 -n -u | wc -l

判读规则:说 "Observed" 而不是 "detected" 的那个 rank 大概率就是 straggler——它没有未决 NCCL 操作,因为它从未进入集合通信。检查它的Last enqueued NCCL work:若等于last completed NCCL work,说明该 rank 卡死在 NCCL 之外(训练循环、数据加载等 CPU 侧代码),而不是卡死在某个集合通信内部。

对于 BROADCAST/ALLREDUCE 超时,可以反推集合通信的开始时间:start_time = timeout_time - timeout_ms(默认超时 1800000ms = 30 分钟)。

4.4 计数与分类

  • 跨所有文件统计Connection reset的总次数与每文件次数;
  • 观察重试模式:重试始终是1/N(第一次重试就恢复,属噪声)还是会升级到N/N(重试耗尽,属真实故障);
  • 统计唯一错误类型数与受影响 rank 数;
  • 留意AISBatchLoaderError等 batch loader 错误——它们意味着 AIStore 返回的对象少于请求数,是存储侧故障进入训练侧的直接信号。

4.5 建立因果链时间线

判定哪个错误最先发生、哪些是后果。技能给出的典型级联模式是:

数据加载错误(根因) -> 部分 rank 退出训练循环 -> 崩溃的 rank 无法参与 NCCL 集合通信 -> NCCL 集合通信挂起到超时(通常 30 分钟) -> Watchdog 杀掉所有剩余 rank

4.6 NeMo 特有的每步同步点(结合源码验证)

NeMo 存在每步执行的集合通信,任何一个 rank 落后都可能在这里显形。技能列举了四个同步点,且均可在本仓库源码中逐一对应:

  • PreemptionCallbackon_train_batch_end中每个训练步结束时调用torch.distributed.broadcast(interrupted, 0)。若某 rank 的单步耗时比其他 rank 长 30 分钟以上,该 broadcast 就会超时。源码见 preemption.py 的interrupted属性(构造 GPU 上的 int32 tensor 后从 rank 0 广播)与 第 91-106 行 的on_train_batch_end钩子——每个 batch 结束都触发一次 broadcast,检测到抢占时经_save_last_checkpoint_and_exit保存last.ckpt并退出。
  • NeMoModelCheckpoint:checkpoint 保存/加载过程中的多次trainer.strategy.broadcast()。源码见 nemo_model_checkpoint.py 中的调用点(如 第 207 行 广播ckpt_path、第 290 行 广播best_model_path、第 541 行 广播文件存在性)。
  • DDP 梯度 all-reduce:反向传播期间自动的每步同步。
  • broadcast_buffers(DDP 默认 True):每次前向从 rank 0 广播模型 buffers(如 batch norm 统计量)。

一个可操作的量化判据:若 rank 0 的 NCCL SeqNum 领先其他 rank,检查差距是否匹配每步集合通信的数量——PreemptionCallback broadcast + DDP allreduce + broadcast_buffers ≈ 每步 3 个操作。这能直接估算出落后 rank 卡在了多少步之前。

该回调的默认开启行为也可在实验管理器中确认:exp_manager.py 第 271 行 定义create_preemption_callback: Optional[bool] = True,即默认启用、需显式传create_preemption_callback: False关闭(与 preemption.py 文档字符串一致)。此外实验管理器还提供 straggler 检测钩子(create_straggler_detection_callback,见 exp_manager.py 第 286-291 行),与本文的 straggler 定位思路互补。

5. Phase 1.7:索取 AIStore daemon 日志

当分析指向存储 IO 问题(connection reset、数据加载错误、超时)而用户未提供 AIS daemon 日志时,先检查which ais,若可用则引导用户执行:

# 1. 设置集群 endpoint export AIS_ENDPOINT=https://<ais-cluster-endpoint>:<port> # 2. 设置认证 token export AIS_AUTHN_TOKEN=<token> # 3. 处理 TLS:跳过校验 或 指定 CA 证书 ais config cli set cluster.skip_verify_crt=true # 或 export AIS_SERVER_CRT=/path/to/ca.crt # 4. 下载所有集群日志(proxy 与 target 的 tar.gz 归档) ais log get cluster <path-to-worker-logs-dir>/ais_logs

若未安装aisCLI,可从 AIStore 仓库构建(cd cmd/cli && go install .)或下载二进制;也可以让用户手动从 AIS 集群拉日志。

6. Phase 2:AIStore 日志分析

6.1 解压与定位正确的时间窗

tarball 解压模板:

mkdir -p extracted && cd extracted for f in ../*.tar.gz; do name=$(basename "$f" .tar.gz) mkdir -p "$name" && tar xzf "$f" -C "$name" done

AIS daemon 日志命名约定:

aistarget.ais-target-N.INFO.MMDD-HHMMSS.1 # target 日志 aisproxy.ais-proxy-N.INFO.MMDD-HHMMSS.1 # proxy 日志

关键理解——一个 daemon 可能对应多个日志文件,文件名中的MMDD-HHMMSS是该文件的起始时间,文件覆盖到同一 daemon 下一个文件的起始时间(或 daemon 停止/日志被收集时);新文件出现在 daemon 重启(崩溃、升级、维护)时。定位故障窗口的正确文件:

  1. 按文件名时间戳列出每个 daemon 的全部文件;
  2. 找到起始时间早于故障窗口、且下一个文件起始时间晚于故障窗口(或无下一个文件)的那个文件;
  3. 若某文件起始时间落在故障窗口内部,说明 daemon 在窗口内重启过——这本身就是重要证据;
  4. 文件内部日志行只有HH:MM:SS没有日期。跨午夜的文件里同一时刻可能出现两次,需借助上下文(stats 计数器值、已知事件)判别属于哪一天。

务必检查某 daemon 的所有文件,而不只是最新的——最新文件可能只覆盖重启后几分钟,证据在更早的文件里。

时区核验:AIS daemon 与 worker 日志通常在不同机器、不同时区,绝不能假设一致。核验步骤:

  1. 找 AIS 的周期性时间戳标记common:NNN DD Mon YY HH:MM UTC =============,它显式声明时区(通常 UTC);
  2. worker 侧:NeMo 用YYYY-MM-DD HH:MM:SS,SLURM 用YYYY-MM-DDThh:mm:ss,两者默认都不带时区;
  3. 交叉对齐一个两侧都可见的已知事件——最佳锚点是作业死亡时刻:在 worker 日志找 SLURMCANCELLED时间戳,再找 AIS stats 中get.n停止增长的精确时刻。若对齐则时区一致;若偏移整小时数,则两系统处于不同时区;
  4. 时区不一致时,先统一施加偏移再关联事件。

6.2 故障窗口的 target stats 关键计数器

AIStore target 约每 3 分钟输出一次 stats 行。需跟踪的关键计数器:

  • err.get.n—— GET 错误(应保持稳定,突增即异常)
  • err.getbatch.n—— batch GET 错误
  • err.http.write.n—— 对客户端的 HTTP 响应中断
  • err.put.nerr.head.nerr.lst.n—— 其他操作错误
  • get.n对比err.get.n—— 计算错误率
  • getbatch.ngetbatch.obj.n—— batch 操作计数

方法是对比相邻 stats 行的差值,找出该区间内新增的错误数。完整计数器含义见第 8 节附录表。

6.3 错误级消息模式

grep "^E " <logfile> # Error 消息 grep "^W " <logfile> # Warning 消息

AIStore 侧的关键错误模式:

模式含义
x-get-batch.*out-of-bounds indexbatch GET 在 target 间流式传输中丢失对象
shared-dm.*terminated.*broken pipetarget 间数据搬运流(data mover)断裂
shared-dm: xid.*not found, dropping recv对象被丢弃,因为 batch 作业已中止
resource pressure: load=criticaltarget 处于磁盘/内存/CPU 压力之下
lcache.*hk.*dsk=critical磁盘处于危急水位,housekeeping 被跳过
gc:.*free mem/oom:内存压力 / 强制 GC

6.4 Proxy 日志

Proxy 负责编排 batch GET。检查 proxy stats:

  • err.get.n—— 应接近零,偏高即 proxy 层路由失败;
  • err.http.write.n—— proxy 丢弃客户端连接。

判别法则:若 proxy 错误平稳而 target 错误突增,问题在 target 侧(磁盘、内存、target 间网络)。

6.5x-get-batch失效链条

AIStore batch GET 失败的典型因果链(存储侧视角,与 Phase 1 的训练侧级联首尾相接):

1. Target 处于资源压力下(dsk=critical、mem=low) 2. Target 间 shared-dm 流断裂(broken pipe) 3. x-get-batch 遇到 out-of-bounds index(recv'd len=0) 4. Batch 作业中止,后续对象被丢弃(xid not found) 5. 客户端收到的对象少于请求数 6. Lhotse/客户端 batch loader 抛出错误(迭代器过早耗尽)

第 6 步正是 Phase 1 Tier 2 中AISBatchLoaderError的来源,两条日志线在此汇合。

7. Phase 3:综合报告与失效分类

7.1 报告结构

技能规定了根因报告的五段式输出:

  1. Job Details—— 框架、规模、开始时间、数据源;
  2. Timeline Table—— 带时间戳的按时间排序事件表,注明证据来源的文件:行;
  3. Root Cause Chain—— 从触发点到最终崩溃的编号因果链,用箭头连接;
  4. Key Files—— 哪些日志文件包含关键证据;
  5. Recommendations—— 可执行的修复建议,分存储侧、客户端侧、训练配置侧。

7.2 失效分类表

  • Storage I/O:磁盘压力、broken pipe、connection reset、batch 对象丢失;
  • Network:无数据错误的 NCCL timeout、网卡故障、交换机问题;
  • GPU/CUDA:OOM、ECC 错误、CUDA assertion;
  • Data:数据文件损坏/缺失、manifest 不匹配、schema 错误;
  • Software:版本不匹配、配置错误、Python 进程 OOM;
  • Infrastructure:节点故障、抢占、SLURM 超时;
  • Data loading stall:单 rank 卡死在数据加载(读取无超时),阻塞所有其他 rank 在下一个集合通信处。

仓库中也提供了配套的数据加载验证工具 validate_dataloader.py,可在训练前校验 dataloader 行为,作为「Data loading stall」类的预防手段。

8. 附录 A:NCCL timeout 解剖与卡死位置判定

技能附带的 NCCL 日志字段词典:

  • Watchdog caught collective operation timeout—— NCCL watchdog 检测到卡死的集合通信;
  • SeqNum=N, OpType=BROADCAST/ALLREDUCE—— 哪个集合通信、序号多少;
  • last enqueued work: N, last completed work: M—— work M 已完成,work M+1 卡住;
  • Timeout(ms)=1800000—— 30 分钟超时(默认);
  • First PG on this rank to signal dumping——本 rank 发起了级联
  • Observed flight recorder dump signal from another rank—— 本 rank 是对他人超时的反应;
  • To avoid data inconsistency, we are taking the entire process down—— watchdog 杀进程。

enqueuedcompleted计数是判定卡死发生在 CPU(数据加载/训练循环)还是 GPU(NCCL 通信)的关键

  • enqueued == completed(无未决操作):该 rank 没有任何 in-flight NCCL 工作,卡死在 CPU 侧(数据加载、音频解码、batch 组装),从未进入集合通信。这就是 straggler——引发挂起的那个 rank。
  • enqueued == completed + 1:恰好提交了一个未完成的操作。它进入了集合通信,但因为 straggler rank 没有加入而无法完成。
  • enqueued > completed + 1:多个操作排队——CPU 已越过卡点异步提交了额外操作(如 DDP 梯度 allreduce 走 hook)。仍在等 straggler。
  • rank 0 的enqueued/completed高于其他 rank:rank 0(常为 broadcast root)完成了自己一侧的 send,但接收方因 straggler 未加入而无法完成 receive。

两种标志性模式对照:

数据加载 stall 模式:1 个 rank 呈enqueued == completedactive collectives: 0、"Observed flight recorder dump signal from another rank"(这是 straggler);N-1 个 rank 呈enqueued > completed、"failure detected by watchdog"(在等 straggler);rank 0 若为 broadcast root 可能领先更多。

GPU 互联(fabric)故障模式:全部 rankenqueued > completed(都进入了集合通信),全部显示 "First PG on this rank to signal dumping"——没有 straggler,是集合通信本身坏了。

结论:把所有 rank 的enqueued数放在一起比较,哪怕一个离群值都会改变整个诊断。

9. 附录 B:NeMo/Lhotse 数据加载的常见坑

技能最后归纳的五个「单 rank 卡死」常见诱因,均可在本仓库找到对应实现:

  1. Lhotse URL 音频读取无超时AudioSource._prepare_for_reading()调用f.read()无 Lhotse 层超时。下载卡死会无限期阻塞 DataLoader worker——这正是「data loading stall 无超时」类别的直接来源。
  2. fault_tolerant=True静默丢弃失败音频:失败音频文件被跳过,每 rank 的有效 batch 变小;不同 rank 因分到的 shard 不同,失败率可能不同。本仓库中该默认值明确可见:audio_to_text_lhotse.py 的_make_audio_samples()"fault_tolerant": True构造AudioSamples,并在旧版 Lhotse 不支持use_batch_loader时回退到AudioSamples(fault_tolerant=True)(同时给出Lhotse >= 1.32.0的升级提示),说明 ASR 数据集默认走容错路径。
  3. BytesIO 中丢失.m4a扩展名:从 URL 下载并包进 BytesIO 后扩展名丢失,LhotseCompositeAudioBackend无法走 m4a 快速路径(TorchaudioFFMPEGBackend),退化为昂贵的级联 backend 尝试。
  4. 空闲 keep-alive 连接重置:AIStore 在空闲 30 秒(DfltMaxIdleTimeout)后关闭 HTTP 连接,而 Python SDK 的 urllib3 连接池不匹配该超时,导致旧连接上出现Connection reset by peer。这类错误被捕获并第一次重试即成功——它们是噪声,不是根因。这与 4.4 节「重试始终 1/N 即恢复」的判别规则呼应。
  5. 每个 rank 拿到不相交的数据 shard:Lhotse 以src[rank::world_size]方式切分 shard。某个 rank 可能恰好分到损坏文件更多、音频更大、或所在存储 target 更慢的 shard——这解释了为什么 stall 往往集中在个别 rank。

10. 小结与相关仓库资源

debug-training-logs文档本质上把一次大规模训练失败的排查固化为可重复执行的流水线:收集全量 worker 日志 → 三级错误分诊 → 逐 rank NCCL 状态核验 → 建立因果链时间线 → 按需下钻 AIStore 存储日志(时区对齐 + stats 差值分析)→ 五段式根因报告,并以五条验证纪律贯穿始终,确保结论经得起原始日志复核。其核心洞察是:绝大多数「NCCL timeout 全集群被杀」的表象之下,真正根因往往是单个 rank 在 CPU 侧(多为数据加载)的无超时卡死;而enqueued == completed的 NCCL work 计数就是分辨「CPU 卡死」与「GPU 通信故障」的判别器。

延伸阅读(仓库内相对路径):

  • 技能定义全文:.claude/skills/debug-training-logs/SKILL.md
  • 每步抢占广播的同步点实现:nemo/utils/callbacks/preemption.py
  • 实验管理器与默认回调开关:nemo/utils/exp_manager.py
  • Checkpoint 保存路径的 broadcast 调用:nemo/utils/callbacks/nemo_model_checkpoint.py
  • ASR Lhotse 数据集与fault_tolerant默认值:nemo/collections/asr/data/audio_to_text_lhotse.py
  • AIS batch 加载集成测试:tests/collections/common/test_lhotse_multimodal_ais_get_batch.py
  • 可恢复 dataloader 迁移技能(含 AIStore 数据路径参考):.claude/skills/migrate-to-resumable-dataloader/SKILL.md

【免费下载链接】SpeechA scalable generative AI framework built for researchers and developers working on Large Language Models, Multimodal, and Speech AI (Automatic Speech Recognition and Text-to-Speech)项目地址: https://gitcode.com/GitHub_Trending/nem/Speech

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询