下载
中文
注册

fork方式创建子进程导致应用进程卡死

问题现象

多卡训练场景下,出现训练进程卡死,或者出现训练超时、用户dataloader卡死等现象。

原因分析

  1. 查看Python堆栈,堆栈信息中包含fork关键字。

    使用pyspy工具查看Python堆栈信息的命令示例如下。

    使用pyspy命令前,需要安装gdb和py-spy。若环境中未安装gdb,可通过包管理(如apt-get install gdb命令、yum install gdb命令)进行安装,详细安装步骤及使用方法请参见GDB官方文档;若环境中未安装py-spy,可使用pip3 install py-spy命令安装(若安装时提示pip版本低,例如You are using pip version 19.2.3, however version 24.0 is available,这时可按照提示使用pip3 install --upgrade pip命令升级pip即可)。

    # 将指定进程的堆栈信息导出到指定文件中,pid表示卡住的用户进程ID,pyspy.log表示存放堆栈信息的文件,请根据实际情况替换
    py-spy dump -p pid > pyspy.log

    堆栈信息示例如下(xxxx表示目录名称、trainApp表示训练程序,由实际业务情况决定,此处仅为示例):

    1 Process 16203: /train/xxxx/xxxx/xxxx/python3.8 -u -m trainApp --config-dir
    2 Python  v3.8.19 (/train/xxxx/xxxx/xxxx/python3.8)
    3
    4 Thread 0xFFFF9CF35B50 (active): "MainThread"
    5    poll (multiprocessing/popen_fork.py:27)
    6    wait (multiprocessing/popen_fork.py:47)
    7    join (multiprocessing/process.py:149)
    8    _terminate_pool (multiprocessing/pool.py:729)
    9    __call__ (multiprocessing/util.py:224)
    10   _scale_down_hw (datasets/datasets.py:96)
    11   __init__ (datasets/datasets.py:73)
    ......
  2. 查看C/C++堆栈,堆栈信息中包含acquire_lock关键字。
    通过gdb命令观察卡住进程的调用栈信息,若环境中未安装gdb,则需要安装gdb,可通过包管理(如apt-get install gdb、yum install gdb)进行安装,详细安装步骤及使用方法请参见GDB官方文档
    # 先执行gdb命令,pid表示卡住的用户进程ID,请根据实际情况替换
    gdb -p pid
    # 再查看调用栈
    (gdb)bt

    堆栈信息示例如下:

    #0 0x0000ffffa9b2b268 in do_futex_wait.constprop () from /lib/aarch64-linux-gnu/libpthread.so.0
    #1 0x0000ffffa9b2b39c in   new_sem_waut_slow.constprop.0 () from /lib/aarch64-linux-gnu/libpthread.so.0
    #2 0x0000ffffa9e96eb8 in PyThread_acquire_lock_timed () from /usr/local/lib/libpython3.8.so.1.0
    #3 0x0000ffffa9e865a8 in _PyThreadState_DeleteExcept () from /usr/local/lib/libpython3.8.so.1.0
    #4 0x0000ffffa9eb94ac in _PyOS_AtferFork_Child () from /usr/local/lib/libpython3.8.so.1.0
    #5 0x0000ffffa9eb9638 in ?? () from /usr/local/lib/libpython3.8.so.1.0
    ......
  3. 通过Python堆栈的fork关键字以及C++堆栈的acquire_lock关键字,确认训练进程卡死是因为用fork方式启进程触发Python的bug而导致的问题。

    在Python3.8~Python3.11版本中如果不指定创建进程的方式,或者显式指定为fork时,在创建子进程时可能会复制主进程的锁状态,而在子进程里再触发获取锁时,就会导致死锁,进而导致业务进程卡死。

    Python社区也有相关说明:python社区有相同问题的issue:https://github.com/python/cpython/issues/74580

解决方法

两种解决方式,由用户根据业务情况选用:

  • 方式一:按照Python官网的指导,升级Python3.8~Python3.11版本的补丁。

    在Python官网,针对Python3.8~Python3.11版本都出了补丁版本,解决fork方式引起的bug。

    在这些补丁版本中,也有针对fork问题的相应说明,如下:

  • 方式二:修改客户业务代码,显式使用forkserver或者spwan方式。
    注意事项:如果涉及修改fork的地方比较多的或工作量比较大,建议采用方式一,防止修改遗漏。
    1. 找到Python安装目录

      执行pip show torch命令查找Python安装目录,查询结果示例如下:

    2. 在Python安装目录下执行find -name popen_fork.py命令找到popen_fork.py文件,在fork起进程的地方增加都触发堆栈的代码。

      在_launch(self,process_obj)函数内添加代码,目的是走fork的子进程都触发堆栈:

      例如,在上图70行的位置增加如下代码,用于在fork起进程的地方触发堆栈消息打印:

      import traceback
      import  time
      timestamp = time.time()
      timestamp_str = str(int(timestamp))
      file_name = f"stack_{timestamp_str}.txt"
      with open("/home/{}.txt".format(file_name),"a") as f:
          traceback.print_stack(file=f)
    3. 复跑训练业务,再次查看Python堆栈。

      在堆栈信息中,先忽略CANN相关的堆栈,只要修改客户业务的fork,切换为“spwan”或“forkserver”启动方式。关于启动方式的详细使用说明,请参见Python官网文档(此处可根据所用的Python版本,选择对应版本的文档)。