PyTorch TorchElastic 错误传播机制全解析:record / ProcessFailure / ChildFailedError 实战指南
【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch
本文聚焦 PyTorch 分布式弹性训练(TorchElastic)中一个常被忽视却至关重要的模块:
torch.distributed.elastic.multiprocessing.errors(文档入口见 errors.md)。当训练脚本由 TorchElastic Agent 以多子进程方式拉起时,worker 进程中的异常无法被 Agent 用简单的try/except捕获,本文从源码出发,完整讲解「错误文件 +@record装饰器 +ProcessFailure/ChildFailedError」这套文件级跨进程错误传播机制,读完后你将能正确装饰训练入口、读懂传播出的根因错误、并学会通过自定义ErrorHandler扩展错误处理。
一、为什么需要"错误传播":多进程下的异常困境
TorchElastic 在一台主机上的典型进程拓扑是:每个节点运行一个 TorchElastic Agent,训练脚本则以多个 worker 子进程的形式由 Agent 启动(api.py 中通过start_processes/MultiprocessContext拉起)。
这种架构下存在一个天然问题:异常产生于 worker 进程,而需要感知异常的是 Agent(以及更上层的调度器)。跨进程的异常不能像同进程那样通过try/except传播,因此模块 docstring(见 errors/init.py 顶部说明)把错误的处理归纳为一次"分类——序列化——聚合——上抛"的过程。
1.1 TorchElastic 的三类错误划分
源码在模块 docstring 中用一张清晰的表格划分了三类错误:
| 类别 | 子类别 | 说明 | 处理方式 |
|---|---|---|---|
| User Error | Input Error | TorchElastic API 的非法输入(如min > max节点数) | 在 Agent 进程内直接以标准 Python 异常抛出 |
| User Error | Worker Failure | worker 子进程上发生的任何失败 | 走本文所述的文件级跨进程错误传播 |
| Platform Error | — | 由 Agent 自身引发的故障 | 由 Agent 进程抛出或使其崩溃 |
| Infra Error | — | 超出 Agent 与 worker 管辖范围的故障(如宿主机宕机) | 依赖集群/调度层处理 |
除Worker Failure之外,其余错误要么在 Agent 进程中「规范地抛出」,要么「显式/隐式地让 Agent 崩溃」,因此通用的 Python 异常处理策略即可覆盖。唯独Worker Failure特殊:失败发生在与 Agent 不同的进程里,必须走跨进程通道。
二、核心机制:文件级(file-based)跨进程错误传播
TorchElastic 选择了一种轻量而稳健的方案——通过文件传递错误。
2.1 传播的三步走
整体链路可概括为(依据 errors/init.py 模块说明):
- 写文件(worker 侧):任何被
@record装饰的函数或二进制入口,捕获到未处理异常后,会把异常及其 traceback写入由环境变量TORCHELASTIC_ERROR_FILE指定的文件; - 设文件(Agent 侧):父进程(Agent)在启动每个子进程时设置该环境变量(见 api.py 中每个 worker 的 env 处理),从而为每个子进程指定独立的错误文件;
- 聚合传播(Agent 侧):Agent 收集所有子进程的错误文件,选取时间戳最小(即最先发生)的那个错误作为根因,继续向上层传播。
这里选「第一个失败」作为根因是刻意的设计:多 worker 场景下后续 worker 的失败往往是第一个 worker 失败引发的连锁反应,最先出错者才是真正需要上报的 root cause。
2.2 Agent 如何为每个 worker 指定错误文件
在 api.py 的进程启动段中,Agent 按local_rank为每个子进程配置独立的错误文件:
error_files = {} if log_dir: # 简化示意,保留源码意图 error_file = os.path.join(clogdir, "error.json") error_files[local_rank] = error_file envs[local_rank]["TORCHELASTIC_ERROR_FILE"] = error_file对应地,MultiprocessContext 运行失败时会把每个失败进程包装成ProcessFailure并带上其专属错误文件路径;而 SubprocessHandler 路径则由 _capture_process_failures 轮询各进程退出码,对exitcode != 0的进程同样构造ProcessFailure记录。两类入口最终殊途同归:错误文件是根因信息的唯一权威来源。
三、入口装饰器record:一行代码接入错误上报
3.1 用法
record是面向使用者的核心 API,装饰进程的顶层入口函数即可。其 docstring 给出的典型写法是:
import torch.distributed.elastic.multiprocessing.errors as errors @errors.record def main(): # 你的训练主逻辑 ... if __name__ == "__main__": main()⚠️ 源码明确提示:
record每个进程只应在顶层方法(通常是 main)上使用一次,不要嵌套装饰内部函数。
3.2 装饰器内部到底做了什么
从 record 的实现看,它等价于下面这段显式代码:
error_handler = get_error_handler() # 默认 ErrorHandler() error_handler.set_entrypoint_fn_name(main.__qualname__) error_handler.initialize() # 注册信号/故障处理 try: main() except ChildFailedError as e: _, failure = e.get_first_failure() error_handler.dump_error_file(failure.error_file, failure.exitcode) raise # 原样继续上抛 except Exception as e: error_handler.record_exception(e) # 写入 JSON 错误文件 raise error_handler.record_success() # 正常返回时记录成功几个值得注意的细节:
SystemExit被特殊处理:当入口通过run_path方式执行时,exit code == 0的SystemExit会被当作正常结束(返回None),避免"假失败";- 捕获到
ChildFailedError时(说明本进程是承载多个子进程的父/保姆进程),会选择其中最先失败的子进程,将其错误文件透传到本进程自己的错误文件(dump_error_file),再继续上抛——这就是「根因一路传导到最顶层」的实现方式; - 之所以依赖错误文件而非单纯靠异常传递,是因为该机制同时要支持函数式启动与二进制/脚本式启动两种形态。
四、ErrorHandler:错误文件的写入者与扩展点
4.1 默认行为
ErrorHandler 类是默认错误处理器(通过 handlers.py 的get_error_handler()获取)。核心职责:
initialize():在运行待观测代码前调用,默认执行faulthandler.enable(all_threads=True),从而在子进程因段错误等原因崩溃时也能输出线程栈;若系统不支持会给出警告而非失败;record_exception(e):把异常序列化为结构化 JSON 写入TORCHELASTIC_ERROR_FILE指向的文件(若环境变量未设置,则退化为仅打日志,保证不中断程序)。写入格式如下:
{ "message": { "message": "RuntimeError: 具体异常信息", "extraInfo": { "py_callstack": "完整的 Python traceback 文本", "timestamp": "1699000000" } } }record_success():入口函数正常返回时被@record调用,基类仅记录 debug 日志,供子类覆写以产出结构化成功遥测;dump_error_file():把「根因子进程的错误文件」整体搬移到当前进程自己的错误文件;若子进程是被SIGSEGV等信号击杀而无法自行写入错误码,还会调用override_error_code_in_rootcause_data()用父进程观测到的exitcode回填errorCode;maybe_enrich_signal_failure_message():对信号类失败(负退出码),子类可覆写以追加设备侧故障上下文(例如 GPU 故障信息),基类为 no-op。
另外,由于使用 Pythonmultiprocessing启动时子进程默认继承父进程环境变量,存在「子进程在包装函数生效前收到信号、把内容写进父进程错误文件」的风险。dump_error_file的写前清理逻辑会在覆盖前先记录原文件内容再删除重建,正是为防御此类边界情况。
4.2 自定义扩展
ErrorHandler是一个面向扩展设计的公开类,源码 docstring 明确建议:子类覆写initialize()与record_exception()即可定制错误处理行为;set_entrypoint_fn_name()会在initialize()之前由@record调用,将入口函数的__qualname__注入处理器状态(_fn_name),这样在覆写上述方法时无需改动方法签名即可拿到入口函数归属信息。典型场景包括:追加自定义元数据、接入自有监控或上报系统。
五、ProcessFailure:统一描述一次进程失败
ProcessFailure(实现见 errors/init.py)是一个 dataclass,承载一次失败进程的结构化结果,字段为:
| 字段 | 含义 |
|---|---|
local_rank | 该进程在本机 worker 中的本地 rank |
pid | 失败进程的进程号 |
exitcode | 退出码;为负时表示被信号终止(如-11对应SIGSEGV) |
error_file | 指向该进程错误文件的路径 |
构造时(__post_init__)会尝试读取错误文件并解析 JSON:
- 错误文件存在:解析出
message与timestamp(时间戳同时兼容字符串 message 与嵌套 dict 两种格式,见_get_error_data); - 错误文件不存在:
error_file会被置为<N/A>,时间戳取当前时间,message置空后按退出码补充推断:exitcode < 0(被信号杀死)时生成如Signal 11 (SIGSEGV) received by PID xxx的说明(通过signal_name()将负退出码映射为标准信号名,映射失败回退为<N/A>,且刻意不因查信号名而杀死进程);exitcode >= 0且无文件数据时,标记为system_terminated_error(不可重试),并提示用户对入口加@record以获取 traceback。
ProcessFailure还提供timestamp_isoformat()把时间戳格式化为YYYY-MM-DD_HH:MM:SS,便于在聚合报告中展示。注意:源码假定错误文件由ErrorHandler写入,若文件来自其他来源则行为未定义——这也是「worker 入口必须加@record」的原因之一。
六、ChildFailedError:聚合子进程失败并定位根因
ChildFailedError(实现见 errors/init.py)用于「父进程是纯保姆(nanny)、子进程才承担实际计算」的场景。当父进程检测到某个子进程失败时,抛出ChildFailedError(name, failures),其中failures是{global_rank: ProcessFailure}的字典。
它有两个关键方法:
get_first_failure():返回(rank, failure),rank 取所有失败中timestamp最小的那个——即最先观测到的失败,作为根因;format_msg():按统一的模板生成人类可读的多行报告,把根因与其余失败分开排版:
============================ trainer FAILED ---------------------------- Failures: [1]: time : 2026-09-08_02:43:30 host : node-01 rank : 2 (local_rank: 2) exitcode : 1 (pid: 12345) error_file: /tmp/trainer_2/error.json traceback : RuntimeError: ... ---------------------------- Root Cause (first observed failure): [0]: ... ============================格式化时对嵌套的 dict message 会优先提取extraInfo.py_callstack(真实 traceback)展示,并对换行做缩进处理;对信号类失败会走maybe_enrich_signal_failure_message钩子(仅在退出码为负时触发,且异常安全——钩子出错只告警,绝不破坏报告渲染)。若同时只存在单一失败,则其余失败区显示<NO_OTHER_FAILURES>。
七、端到端调用链:从 worker 崩溃到调度器看到根因
把上述组件串起来,一次完整的错误传播(依据 errors/init.py 中的进程树示例)大致如下:
0: scheduler-init-process └─ 1: torchelastic_agent ├─ 2: trainer_0 (ok) ├─ 3: trainer_1 (fail) ──> 写 error.json └─ ...trainer_1的入口被@record装饰,异常发生后由ErrorHandler.record_exception把异常与 traceback 写入该进程的error.json;- Agent 通过轮询退出码或捕获
ProcessRaisedException/ProcessExitedException(见 api.py)感知失败,为该local_rank构造ProcessFailure; - Agent 抛出
ChildFailedError,把各失败子进程聚合并携带其错误文件路径; - Agent 自身入口若同样被
@record装饰,则在捕获ChildFailedError后调用get_first_failure()取出根因、dump_error_file()将子进程错误文件透传到 Agent 的错误文件,再原样上抛; - 调度器 / 启动器(如
torchrun,见 torch/distributed/run.py)读取错误文件即可获得带完整 traceback 的真正根因,据此执行重试策略并向用户呈现准确的失败状态。
需要说明的是:上述第 4 步描述的是@record的标准语义(见其 docstring 中与装饰等价的手写 try/except 代码),实际顶层编排是否逐级透传取决于具体启动器实现。
八、实战建议与常见陷阱
结合源码整理几条可直接落地的实践:
- 每个 worker 进程的顶层入口务必加
@record。不加的后果很直接:进程崩溃后没有错误文件,上层只能看到system_terminated_error这类几乎无信息的描述,无法定位根因;日志中甚至会收到「local_rank N FAILED with no error file. Decorate your entrypoint fn with @record」的提示。 - 只装饰顶层 main,不要层层装饰。
@record内部会初始化 handler、注册faulthandler,重复装饰会带来多余开销并可能掩盖真实的异常来源。 - 理解"根因 = 最早失败":分析多 worker 同时失败的报告时,先看
Root Cause (first observed failure)段落,其余 worker 的失败多为次生故障。 - 读取 JSON 而不是只靠日志:错误文件中的
message.extraInfo.py_callstack才是可机器消费的完整 traceback,可用于告警系统自动提取;timestamp用于失败排序。 - 信号类失败是「无 traceback」的常态:被
OOM killer、SIGSEGV等信号终止的进程往往来不及写文件,此时exitcode < 0的判断与signal_name()是诊断的第一手线索。 - 需要定制上报时继承
ErrorHandler:覆写initialize()/record_exception()(必要时record_success()),通过_fn_name拿到入口函数归属,通过覆写maybe_enrich_signal_failure_message()为信号失败补充设备侧上下文,再用get_error_handler()的返回点替换默认实例即可接入自有体系。
延伸阅读
- 模块级设计总览与错误分类:
torch/distributed/elastic/multiprocessing/errors/__init__.py的模块 docstring; - 装饰器
record、ProcessFailure、ChildFailedError完整实现:同上文件的对应类/函数定义; ErrorHandler默认实现与扩展接口:error_handler.py;- 处理器工厂入口:handlers.py;
- Agent 侧如何设置错误文件与构造失败对象:api.py;
- TorchElastic 相关文档目录:docs/source/elastic/。
【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考