Python 多进程脚本的常见问题

占用过多内存

TL;DR:大多是具体脚本的问题,不好处理。有的是没有及时 gc,有的是把所有数据全部载入内存之后(假设了内存足够大)才开始处理。

我是遇到了这样一个问题: megatron-lm huggingface 教程运行时资源占用大、有僵尸2025/4/22 文章还在修改中,尚未公开)。

o4-mini 建议我调整启动方式:multiprocessing — Process-based parallelism — Python 3.13.3 documentation

方法描述特征
spawn创建新的解释器并运行,干净但慢,是 Windows、macOS 的默认方法。大量 python -c from multiprocessing.spawn import spawn_main; 进程
fork直接 fork。除了 macOS 之外的 POSIX 的默认方法,Python 3.14 之后默认方法改成 forkserver大量命令参数(cmd)和启动命令相同的进程,例子 python tools/preprocess_data.py --input ...
forkserver首次用 forkserver 方法创建进程会启动一个单线程(除非 import 的库导致了多线程)的 server,每次调用 os.fork() 都是干净的,不会有冗余线程。大量 python -c from multiprocessing.forkserver import ... 进程

但是我觉得调整启动方式对内存占用的影响很难观察到,但 fork 比 forkserver 的速度快一点(可能是我的用例如此)。

相关:Excessive memory buildup when merging text datasets in Megatron core · Issue #12993 · NVIDIA/NeMo,不过我操作之后还是会占用大量内存。后来发现其实是这个脚本把所有数据全部放在内存中,然后一次性处理,所以是脚本本身没有考虑到内存不够的场景

产生大量睡眠进程

使用 forkserver 很容易出现大量 S 进程(睡眠状态),占用大量内存。可以使用以下方法来杀:

sudo kill -9 $(ps aux | grep multiprocessing.forkserver | grep -v grep | awk '{print $2}' |less)

这也许是快速连续按下 ctrl+c,导致程序没有来得及做清理工作导致的?

产生大量僵尸

相关资料:

快速检查:ps aux | grep Z。示例:

如果要在脚本中查找僵尸,需要严格检查第 8 个域(如果 ps 选项更改,这个位置也可能会变化),以免出现 USER 或者 COMMAND 出现 Z 字母造成误伤。

ps aux | awk 'match($8, ".*Z.*")'

如果还想要找到具体的 pid:

ps aux | awk 'match($8, ".*Z.*")' | awk '{print $2}'

僵尸怎么杀?杀父进程,让 init 来收养并 reap。

但是,我使用的是容器环境,它的 init 不具备处理 SIGCHLD 信号的功能

(base) ➜  ~ ps ax --sort=ppid -o user,pid,ppid,%cpu,%mem,stat,start,time,command |awk 'match($6, ".*Z.*")' |awk '{print $3}'|sort -u
2260730
(base) ➜  ~ sudo kill -SIGCHLD 2260730
(base) ➜  ~ ps ax --sort=ppid -o user,pid,ppid,%cpu,%mem,stat,start,time,command |awk 'match($6, ".*Z.*")' |awk '{print $3}'|sort -u
2260730
(base) ➜  ~ # kill -SIGCHLD 没有任何效果!

这个确实是我启动 docker 容器时的失误😅。我是想要创建一个持久的容器,然后 docker exec 进入执行具体任务。应该这样启动容器:

docker run --init image_name tail -f /dev/null

如果已经出现了大量僵尸,但不想销毁容器,可以重启:

docker restart container_name