python多进程卡死问题排查


背景

由于一些原因,打算把点云算法的三维重建和定位打到一个docker镜像里面,统一对外提供服务接口。
算法嘛,需要加载比较大的模型文件,使用多核CPU,使用GPU等资源,因此一开始是打算使用多进程的方式去响应接口请求。
然后就有问题了,启动子进程去三维重建,直接卡死。。

开发环境

ubuntu22.04
python: 3.9
pytorch: 1.13.1
gunicorn+fastapi

启动链路

  1. 使用gunicorn启动fastapi服务
  2. 初始化空间定位算法,加载算法模型。初始化三维重建。
  3. 请求三维重建接口,使用fastapi的multiprocessing开启子进程响应。
  4. 问题:子进程读取三维重建模型卡死。

问题排查

推测是可能遇到了文件锁的东西,导致进程等待。具体需要排查原因。

pdb调试

遇事不决,可问春风(debug)。第一时间就上pdb,结果pdb到卡住的地方就报错了,报错如下:

  File "/usr/local/lib/python3.9/bdb.py", line 88, in trace_dispatch
    return self.dispatch_line(frame)
  File "/usr/local/lib/python3.9/bdb.py", line 113, in dispatch_line
    if self.quitting: raise BdbQuit
bdb.BdbQuit
ERROR:rokidloc_server:reconstruct failed, reason:

# 解释
bdb是pdb的底层库,用于提供断点、单步等功能。该错误消息 "bdb.BdbQuit" 通常出现在
调试器退出时。由于断点、单步等操作是通过向代码中注入信号来完成的,
在程序卡住或被无限循环阻塞时,这些信号无法恰当的注入或被接收处理,
pdb 所在的环境可能变得不可靠或不一致,调试器就会自动退出以防止进一步的问题。

这个博客 在 Python 中使用 GDB 来调试 转载_python gdb-CSDN博客里面也提到了,卡住的进程无法通过pdb进行调试,可以考虑使用GDB调试python程序。
行吧,小趴菜一个。。

给文件加共享锁

一开始总以为是文件锁导致的,就去给读取模型文件的部分加共享锁,读完就释放,如下:

            model_path = self.config['weight_path']
        with open(model_path, 'rb') as f:
            try:
                fcntl.flock(f, fcntl.LOCK_SH)  # 共享锁定
                state_dict = torch.load(f)
            finally:
                fcntl.flock(f, fcntl.LOCK_UN)  # 释放锁

然而并没有什么卵用。

查看进程fd

那么是否可以查看进程fd,看看是否打开了模型文件呢?确认下是否是读取模型文件导致的卡住。

ls -l /proc/pid/fd

# 结果没有看到直接打开模型文件的path
lrwx------ 1 root root 64 Mar 23 18:09 0 -> /dev/pts/1
lrwx------ 1 root root 64 Mar 23 18:09 1 -> /dev/pts/1
l-wx------ 1 root root 64 Mar 23 18:09 10 -> 'pipe:[51968331]'
lr-x------ 1 root root 64 Mar 23 18:09 11 -> 'pipe:[51968350]'
l-wx------ 1 root root 64 Mar 23 18:20 12 -> 'pipe:[51968350]'

# 查看pid启动了多少个子线程
ps -T -p 256
    PID    SPID TTY          TIME CMD
    256     256 pts/1    00:00:09 gunicorn
    256     289 pts/1    00:00:00 iou-sqp-3015093
    256     291 pts/1    00:00:00 gunicorn
    256     292 pts/1    00:00:00 gunicorn
    256     293 pts/1    00:00:00 gunicorn
    256     294 pts/1    00:00:00 gunicorn
    256     295 pts/1    00:00:00 gunicorn
    256     296 pts/1    00:00:00 gunicorn
    256     297 pts/1    00:00:00 gunicorn
    256     298 pts/1    00:00:00 gunicorn

# 298就是持有private的锁进程,查看298对应的fd
ls -l /proc/298/fd
# 依然看不到有明确的打开模型文件,不清楚到底卡哪里了

ok,还是失败。

strace追踪堆栈

查看初始化的进程信息

strace -f -p 256
strace: Process 256 attached with 10 threads
[pid   298] futex(0x7fa1f0001200, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 0, NULL, FUTEX_BITSET_MATCH_ANY <unfinished ...>
[pid   297] futex(0x561186395444, FUTEX_WAIT_PRIVATE, 1064, NULL <unfinished ...>
[pid   296] futex(0x561186395444, FUTEX_WAIT_PRIVATE, 1064, NULL <unfinished ...>
[pid   294] futex(0x561186395444, FUTEX_WAIT_PRIVATE, 1064, NULL <unfinished ...>
[pid   293] futex(0x561186395444, FUTEX_WAIT_PRIVATE, 1064, NULL <unfinished ...>
[pid   292] futex(0x561186395444, FUTEX_WAIT_PRIVATE, 1064, NULL <unfinished ...>
[pid   295] futex(0x561186395444, FUTEX_WAIT_PRIVATE, 1064, NULL <unfinished ...>
[pid   256] epoll_pwait(18,  <unfinished ...>
[pid   291] futex(0x561186395444, FUTEX_WAIT_PRIVATE, 1064, NULL <unfinished ...>

# 解释
strace输出的这行信息表示,这条命令表示线程正在等待内存地址0x561186395444上的
futex的值变为1064,在此情况发生之前,这个线程会被阻塞。

FUTEX_WAIT_BITSET_PRIVATE: 这是等待操作。它会让调用进程睡眠,直到另一进程使用 
FUTEX_WAKE 操作来唤醒它。
_PRIVATE 后缀表示这个锁只能被同一进程中的其他线程所见,不能被其他进程的线程所见。


查看卡住的进程信息

strace -f -p 135
strace: Process 135 attached
futex(0x555989b08d84, FUTEX_WAIT_PRIVATE, 1064, NULL

# 解释
线程正在等待一个 futex 的值变为 1064。如果在无限等待期间 futex 的值不发生变化,那么线程将一直阻塞。

Futex是什么呢?参考:Futex系统调用,Futex机制,及具体案例分析-CSDN博客
简单来说,Futex是一种用户态和内核态混合的同步机制,一种轻量级的锁。而strace查看的出现的futex是代表进程被挂起等待唤醒。
问题是两个进程都等待唤醒,那就有问题了,表现上就是卡住了。


GDB调试python

安装gdb和python-dbg

参考:使用gdb调试Python程序-CSDN博客

  1. 查看python-dbg 是否和gdb结合起来了: /usr/share/gdb/auto-load/usr/bin/
  2. 可以手动设置gdb的auto-load : 参考:https://docs.python.org/3/howto/gdb_helpers.html
  3. 进入gdb,执行: 参考:https://www.lyyyuna.com/2018/01/01/python-internal4-lldb/
1.gdb python3.10
2.set args test.py
3.run
  1. gdb -p pid 查看python的堆栈。
    1. py-bt 查看python堆栈,py-list查看python代码

嗯,看起来很简单,然而实际上立马就遇到了坑。

python-dbg和python版本

python-dbg是gdb可以调试python的关键。而python-dbg和python版本是要一一对应的。
博主python版本是3.9,ubuntu22.04默认的python版本是3.10,就导致安装的python-dbg也是3.10的,因此调试失败。

编译python3.9的dbg文件
  1. 去github下载cpython,切换到3.9版本,按照自己的python版本来
  2. 执行编译
1. ./configure --with-pydebug --enable-optimizations
2. make install
3. 根目录看到生成了python, python-gdb.py
4. 默认编译完成之后,会把python复制到/usr/local/bin/
  注意: 查看是否把python-gdb.py或者python3.9-gdb.py给copy到/usr/local/bin/下
  如果没有copy的话,需要我们手动copy。 
  cp /xxx/cpython/python-gdb.py /usr/local/bin/python3.9-gdb.py
5. 安装完gdb, 设置gdb的启动配置文件:
  vim ~/.gdbinit
  add-auto-load-safe-path /usr/local/bin/python3.9-gdb.py
6. gdb python
  (gdb) info auto-load
  gdb-scripts:  No auto-load scripts.
  libthread-db:  No auto-loaded libthread-db.
  local-gdbinit:  Local .gdbinit file was not found.
  python-scripts:
  Loaded  Script
  Yes     /usr/local/bin/python3.9-gdb.py
  1. 运行python程序,通过gdb查看堆栈
1. ps aux 查看python程序的pid
2. gdb 调试pid : gdb -p pid
3.进入之后,py-bt查看堆栈,py-list查看python程序代码。例如:
(gdb) py-list
  39        loop = events.new_event_loop()
  40        try:
  41            events.set_event_loop(loop)
  42            if debug is not None:
  43                loop.set_debug(debug)
 >44            return loop.run_until_complete(main)
  45        finally:
  46            try:
  47                _cancel_all_tasks(loop)
  48                loop.run_until_complete(loop.shutdown_asyncgens())
  49                loop.run_until_complete(loop.shutdown_default_executor())
(gdb)

gdb调试
  1. gdb直接运行python程序: 参考:https://blog.51cto.com/u_16175509/6890013
1.gdb python
2.run test.py
3.b test.py:4
4.continue
  1. gdb调试在运行中的python程序,也就是通过pid调试
    1. gdb进去之后,查看当前代码卡在哪了,如图可以看到是44行image.png
    2. 查看python的堆栈
      1. 这个堆栈表示了 Python 应用在 Gunicorn 服务器下的运行路径。
      2. image.png
    3. 查看阻塞进程
      1. 看报错是pytorch加载模型的报错,加载参数失败,那么要么是模型问题,要么是pytorch版本的问题了。
      2. image.png

猜测是pytorch的数据加载问题。ok,那就查查pytorch。

pytorch多进程卡死问题

参考:https://blog.csdn.net/kelxLZ/article/details/114591236

  1. 查看程序是否使用openMp,可以看到是用到了
lsof -p 104  | grep libgomp
gunicorn 104 root  mem       REG               0,54    168721  9362919 /usr/local/lib/python3.9/site-packages/torch/lib/libgomp-a34b3233.so.1
gunicorn 104 root  mem       REG               0,54    298776  9185138 /usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0
  1. 咨询了算法同学,他们在训练的时候,也遇到过这种问题,一般的解决方案是:
# 子进程启动方法设置成spawn模式
torch.multiprocessing.set_start_method('spawn')
  1. python官网给出的三种进程启动方法
根据不同的平台, multiprocessing 支持三种启动进程的方法。这些 启动方法 有

spawn
父进程会启动一个全新的 python 解释器进程。 子进程将只继承那些运行进程对象的 
run() 方法所必需的资源。 特别地,来自父进程的非必需文件描述符和句柄将不会被继承。
使用此方法启动进程相比使用 fork 或 forkserver 要慢上许多。
可在Unix和Windows上使用。 Windows上的默认设置。

fork
父进程使用 os.fork() 来产生 Python 解释器分叉。子进程在开始时实际上与父进程
相同。父进程的所有资源都由子进程继承。请注意,安全分叉多线程进程是棘手的。
只存在于Unix。Unix中的默认值。

forkserver
程序启动并选择 forkserver 启动方法时,将启动服务器进程。 从那时起,每当需要
一个新进程时,父进程就会连接到服务器并请求它分叉一个新进程。 分叉服务器进程是
单线程的,因此使用 os.fork() 是安全的。 没有不必要的资源被继承。

可在Unix平台上使用,支持通过Unix管道传递文件描述符。

在 3.8 版更改: 对于 macOS,spawn 启动方式是默认方式。 因为 fork 可能导致
subprocess崩溃,被认为是不安全的,查看 bpo-33725
  1. 更改成spawn启动,问题解决。
    1. spawn创建子进程很慢,只能用于耗时不敏感的程序
    2. spawn会导致有些全局变量无法使用,需要传入进去

多进程的fork和spawn模式

具体的概念就不说了, 主要是看一下通过这两种模式,启动的子进程是什么样子的,父子关系如何。

# apt install psmisc : 方便查看pid的tree

--  fork模式
ps aux
root          88      85  0 21:32 pts/1    00:00:00 /usr/local/bin/python3.9 /usr/local/bin/gunicorn --config=configs/gunicorn.conf.py server.service:app
root          89      88 40 21:32 pts/1    00:00:10 /usr/local/bin/python3.9 /usr/local/bin/gunicorn --config=configs/gunicorn.conf.py server.service:app
root         135			89  1 21:32 pts/1    00:00:00 /usr/local/bin/python3.9 /usr/local/bin/gunicorn --config=configs/gunicorn.conf.py server.service:app

# pstree -p -T 88
gunicorn(88)---gunicorn(89)---gunicorn(135)

pid=88是gunicorn主进程
pid=89是gunicorn的工作worker进程
pid=135是multiprocess fork出来的子进程


---- spawn模式
ps aux
root          59  0.0  0.0  30788 26540 pts/1    S+   21:16   0:00 /usr/local/bin/python3.9 /usr/local/bin/gunicorn --config=configs/gunicorn.conf.py server.ser
root          60  3.0  1.5 4664492 1026576 pts/1 Sl+  21:16   0:10 /usr/local/bin/python3.9 /usr/local/bin/gunicorn --config=configs/gunicorn.conf.py server.ser
root         104  0.0  0.0  13468 11612 pts/1    S+   21:17   0:00 /usr/local/bin/python3.9 -c from multiprocessing.resource_tracker import main;main(36)
root         105  672  1.2 6657676 799780 pts/1  Sl+  21:17  31:51 /usr/local/bin/python3.9 -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_

pid=104: 第一个进程是资源追踪器 (resource tracker)。资源追踪器的目的是保证当一个程序结束时,其创建的所有资源,如临时文件,都能被正确地清理掉。
pid=105: 第二个进程是实际的子进程,它是通过 spawn 启动方法创建的。在 spawn 模式下,一个全新的Python解释器进程被启动,
主程序的所有代码都需要在这个新的进程中重新导入。

# pstree -p -T 59
gunicorn(59)---gunicorn(60)-+-python3.9(104)
                            `-python3.9(105)

pid=59是主进程
pid=60是worker进程
pid=104是spawn的监控进程
pid=105是spawn模式启动的子进程

最明显的差别就是spawn启动的子进程是新的python解释器,有点6。

其他解决方式

其实发现程序有bug之后,第一时间就用其他方式解决掉了。这里也记录一下其他的解决方案,供大家参考。

使用fastapi自带的backgroudTask

  1. backgroundTasks可以完成任务
  2. 问题在于,无法控制,无法做优雅退出

使用多线程模式

from concurrent.futures import ThreadPoolExecutor
  1. 可以完成任务,htop发现也能用到多核
  2. 一样的问题,无法监控状态和优雅退出

个人建议

使用celery多进程队列

使用fastapi + celery的方式,参考:https://derlin.github.io/introduction-to-fastapi-and-celery/03-celery/
这种方式更适合做大型的服务,通过任务队列保证数据安全,通过flower来监控进程状态。
flower参考: https://medium.com/featurepreneur/flower-celery-monitoring-tool-50fba1c8f623

启动两个镜像

三维建图和空间定位都需要用到多核和GPU资源,多线程模式启动更好一些,因此可以考虑部署两个镜像。这也是docker一直主张的原则,粒度尽量小,指责单一,内聚。

总结

当发现这个问题之后,很快就通过多线程的方式暂时解决掉了,依然可以用到多核资源。只是遇到问题不应该退缩,也要把握住每一个解决难题的机会。
出于这种心理,一步步排查最终锁定问题,知其然知其所以然,才能有的放矢的解决根本问题。排查问题的路上也学习了不少东西,值得记录下来。

end

===== 20240327补充 =====

补充问题

multiprocess多进程使用cuda的问题

RuntimeError: Cannot re-initialize CUDA in forked subprocess. To use CUDA with multiprocessing, you must use the 'spawn' start method

解决方案,设置进程启动方式为spawn即可。

多次调用multiprocessing.set_start_method的问题

  File "/usr/local/lib/python3.9/multiprocessing/context.py", line 243, in set_start_method
    raise RuntimeError('context has already been set')
RuntimeError: context has already been set

文档上有解释:参考:https://docs.python.org/zh-cn/3.9/library/multiprocessing.html#multiprocessing.set_start_method
设置启动子进程的方法。 method 可以是 ‘fork’ , ‘spawn’ 或者 ‘forkserver’ 。

注意这最多只能调用一次,并且需要藏在 main 模块中,由 if name == ‘main’ 保护着。

解决方案

a. 如果使用spawn模式,那么只是把multiprocessing.set_start_method放到文件顶部是不够的,每次copy进程都会执行一次。解决方案就是加force参数:

import multiprocessing as mp
# 第二个参数为True
mp.set_start_method('spawn', True)

以上方案虽然解决了报错,但感觉并不完美,强扭的瓜不甜。

b. 通过context设置进程启动方法

import multiprocessing as mp
ctx = mp.get_context('spawn')
ctx.Process()

完美解决这个问题。每次启动子进程,通过上下文去获取进程的启动method,后续启动子进程延续即可。

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
### 回答1: torch.multiprocessing.spawn是PyTorch中的一个函数,用于在多个进程中启动一个函数。它可以在多个GPU上并行运行模型训练,加快训练速度。该函数可以指定进程数量、每个进程的参数、函数名称等。 ### 回答2: torch.multiprocessing.spawn是一种多进程创建和启动的方式,它是PyTorch多进程的一种实现方式。由于GIL(全局锁)的存在,Python不能真正并行地运行多个线程,所以在训练和处理大量数据时,多进程是一种提高性能的常见技术。而所谓的spawn,也就是派生进程,是一种在父进程的基础上创建一个新的子进程并让它执行不同的代码的过程。而在torch.multiprocessing.spawn中,该过程由torch.multiprocessing.start_processes()函数完成。 torch.multiprocessing.spawn函数接收四个参数:fn、args、nprocs、join。其中fn是一个函数,它描述如何在每个进程中运行。args是一个表示每个进程的参数的元组,nprocs是要创建的进程数,join指定进程在运行完之后是否要立即结束。在函数fn中,将会根据当前进程的参数获取设备信息和进程ID,并根据这些信息判断当前进程所负责的是哪一部分数据,并使用这些数据进行训练或其他操作。每个进程最终输出结果,并由主进程进行汇总。 torch.multiprocessing.spawn函数在创建多个进程时,会通过pickle将文件对象和变量传递给子进程。因此,在使用torch.multiprocessing.spawn时,应当谨慎处理pickle,避免潜在的安全问题。 总之,torch.multiprocessing.spawn是PyTorch中一种创建和启动多个进程的方式。它通过启动多个进程,实现了多进程的训练。使用该函数,可以在训练和处理大量数据时提高性能并加速模型的训练。然而,在使用该函数时,需要注意文件对象和变量的序列化问题。 ### 回答3: torch.multiprocessing.spawn是PyTorch中的一个工具,用于在多个进程中运行模型并行化代码。 它提供了一个简单的方式来创建多个进程并将它们分配给不同的计算资源,以同时运行多个模型并行化代码。因此,它可以显著缩短模型训练的时间,提高模型的训练效率。 PyTorch中的torch.multiprocessing.spawn函数的调用方式如下所示: ```python torch.multiprocessing.spawn(fn, args=(), nprocs=1, join=True, daemon=False, start_method='spawn') ``` 其中,参数fn是一个函数,这个函数就是需要在多个进程中并行执行的函数。args是fn函数的参数列表。nprocs是需要创建的进程数量,默认为1。join表示进程是否需要等待子进程完成后再结束,daemon表示进程是否是后台进程。start_method表示使用的进程启动方式,默认为spawn。 默认的start_method就是spawn,spawn是使用forking的方式来创建进程,适用于Linux和MacOS系统。其他启动方式还包括fork、forkserver、以及在Windows上的spawn(Windows)。因为每种启动方式都有不同的适用环境,需要根据实际情况进行选择。 在使用torch.multiprocessing.spawn函数时,需要注意fn函数必须可序列化、可调用,否则程序会无法正常运行。另外,在fn函数内部对全局变量的修改,也会使程序无法正常运行。为了避免这种情况,可以采用分布式连接方式,使用隐式同步和广播方式来同步更新参数。 总之,torch.multiprocessing.spawn函数可以大大提高PyTorch模型训练的速度和效率,但是在使用时需要仔细考虑参数设置、序列化问题和全局变量修改等问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铁柱同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值