在分别运行于Docker容器中的Python脚本之间实现IPC共享内存

17

问题

我编写了一个神经网络分类器,可以处理大量的图像(每个图像大小约为1-3 GB),对它们进行切片,并将这些切片逐个通过网络传输。训练速度非常缓慢,因此我进行了基准测试,发现从一个图像中加载补丁到内存中需要大约50秒的时间(使用Openslide库),而仅需要0.5秒的时间将其通过模型传递。

然而,我正在一台具有1.5Tb RAM的超级计算机上工作,其中只有约26 Gb被利用。数据集总共有约500Gb。我的想法是,如果我们可以将整个数据集加载到内存中,那么它将极大地加快训练速度。但是我正在与一个研究团队合作,并且我们正在多个Python脚本中运行实验。因此,理想情况下,我希望在一个脚本中将整个数据集加载到内存中,并能够在所有脚本中访问它。

更多细节:

  • 我们在单独的Docker容器中运行各自的实验(在同一台机器上),因此数据集必须可供多个容器访问。
  • 数据集是Camelyon16数据集;图像以.tif格式存储。
  • 我们只需要读取图像,不需要写入。
  • 我们每次只需要访问数据集的一小部分。

可能的解决方案

我找到了许多关于如何在多个Python脚本之间共享Python对象或原始数据的帖子:

在脚本之间共享Python数据

使用多进程模块中的SyncManager和BaseManager创建服务器进程| 示例1 | 示例2 | 文档-服务器进程 | 文档-SyncManagers

  • 优点:可以通过网络共享给不同计算机上的进程(它可以被多个容器共享吗?)
  • 可能问题:根据文档,比使用共享内存慢。如果我们使用客户端/服务器在多个容器之间共享内存,是否比所有脚本从磁盘读取更快?
  • 可能问题:根据这个答案Manager对象在发送前对对象进行了pickle处理,可能会减慢速度。

mmap模块| 文档

  • 可能存在的问题:mmap将文件映射到虚拟内存,而不是物理内存 - 它创建了一个临时文件。
  • 可能存在的问题:因为我们一次只使用数据集的一小部分,虚拟内存会将整个数据集放在磁盘上,我们会遇到抖动问题,程序会变得缓慢。

Pyro4(Python对象的客户端-服务器)| 文档

Python的sysv_ipc模块。这个演示看起来很有前途。

  • 可能存在的问题:也许只是内置的multi-processing模块中可用事物的较低级别曝光

我还发现了Python中IPC /网络选项的此列表

一些人讨论服务器-客户端设置,一些人讨论串行化/反串行化,但我担心这将比从磁盘读取更耗时。我找到的答案都没有回答我的问题,即它们是否会在I/O上产生性能提升。

在Docker容器之间共享内存

我们不仅需要在脚本之间共享Python对象/内存,还需要在Docker容器之间共享它们。

Docker 文档 很好地解释了 --ipc 标志。根据文档对我来说是有意义的的做法是运行:

docker run -d --ipc=shareable data-server
docker run -d --ipc=container:data-server data-client

但是,当我将我的客户端和服务器分别运行在不同的容器中,并设置了如上所述的--ipc连接时,它们无法相互通信。我阅读过的SO问题(1234)没有涉及在单独的Docker容器中的Python脚本之间集成共享内存。
我的问题:
1:是否有任何一种方法比从磁盘中读取更快?共享跨进程/容器的数据是否能够提高性能?
2:哪种方法最适合在多个Docker容器之间共享内存中的数据?
3:如何将Python的内存共享解决方案与docker run --ipc=<mode>集成?(共享IPC命名空间是否是在Docker容器之间共享内存的最佳方法?)
4:是否有比这些更好的解决方案来解决我们大量I/O开销的问题?
最小工作示例-更新。不需要外部依赖!这是我在单独的容器中运行Python脚本之间进行内存共享的天真方法。当Python脚本在同一个容器中运行时,它可以工作,但在单独的容器中运行时则不能。

server.py

from multiprocessing.managers import SyncManager
import multiprocessing

patch_dict = {}

image_level = 2
image_files = ['path/to/normal_042.tif']
region_list = [(14336, 10752),
               (9408, 18368),
               (8064, 25536),
               (16128, 14336)]

def load_patch_dict():

    for i, image_file in enumerate(image_files):
        # We would load the image files here. As a placeholder, we just add `1` to the dict
        patches = 1
        patch_dict.update({'image_{}'.format(i): patches})

def get_patch_dict():
    return patch_dict

class MyManager(SyncManager):
    pass

if __name__ == "__main__":
    load_patch_dict()
    port_num = 4343
    MyManager.register("patch_dict", get_patch_dict)
    manager = MyManager(("127.0.0.1", port_num), authkey=b"password")
    # Set the authkey because it doesn't set properly when we initialize MyManager
    multiprocessing.current_process().authkey = b"password"
    manager.start()
    input("Press any key to kill server".center(50, "-"))
    manager.shutdown

client.py

from multiprocessing.managers import SyncManager
import multiprocessing
import sys, time

class MyManager(SyncManager):
    pass

MyManager.register("patch_dict")

if __name__ == "__main__":
    port_num = 4343

    manager = MyManager(("127.0.0.1", port_num), authkey=b"password")
    multiprocessing.current_process().authkey = b"password"
    manager.connect()
    patch_dict = manager.patch_dict()

    keys = list(patch_dict.keys())
    for key in keys:
        image_patches = patch_dict.get(key)
        # Do NN stuff (irrelevant)

当这些脚本在同一容器中运行时,共享图像的效果很好。但是当它们在不同的容器中运行时,就会出现问题,如下所示:

# Run the container for the server
docker run -it --name cancer-1 --rm --cpus=10 --ipc=shareable cancer-env
# Run the container for the client
docker run -it --name cancer-2 --rm --cpus=10 --ipc=container:cancer-1 cancer-env

我遇到了以下错误:

Traceback (most recent call last):
  File "patch_client.py", line 22, in <module>
    manager.connect()
  File "/usr/lib/python3.5/multiprocessing/managers.py", line 455, in connect
    conn = Client(self._address, authkey=self._authkey)
  File "/usr/lib/python3.5/multiprocessing/connection.py", line 487, in Client
    c = SocketClient(address)
  File "/usr/lib/python3.5/multiprocessing/connection.py", line 614, in SocketClient
    s.connect(address)
ConnectionRefusedError: [Errno 111] Connection refused

我怀疑你的容器化设置存在问题,因为你的Docker容器位于不同的网络中,无法通过127.0.0.1相互通信。你可以尝试使用--network host启动它们,也许会有所帮助。 - swenzel
感谢您的评论 - 它帮了我很多。现在,在client.py中的manager.connect()不再出现ConnectionRefusedError,程序可以执行到image_patches = patch_dict.get(key),但是会引发这个错误 - Jacob Stern
@JacobStern,你在这里使用的是网络而不是ipc。请使用--network=container:cancer-1而不是--ipc=container:cancer-1,然后再试一下。 - Tarun Lalwani
根据这篇文章和其他一些文章的说法,似乎共享内存是最好的选择,因为网络/管道速度远不及内存速度。这是正确的吗? - Jacob Stern
也有可能他们只是通过网络共享元数据(即内存地址、字段名称、字段类型),然后通过共享内存完成其余操作。但我还没有尝试过,所以也不确定。 - swenzel
显示剩余3条评论
2个回答

7

我建议您尝试使用tmpfs

它是一种Linux功能,允许您创建虚拟文件系统,其中所有内容都存储在RAM中。这样可以快速访问文件,并且只需要一个bash命令即可设置。

除了非常快速和简单明了外,它在您的情况下有许多优点:

  • 无需更改当前代码-数据集的结构保持不变
  • 不需要额外的工作来创建共享数据集-只需将数据集复制到tmpfs
  • 通用接口-作为文件系统,您可以轻松地将基于RAM的数据集集成到系统中的其他组件中,这些组件不一定是用Python编写的。例如,在容器内部使用很容易,只需将挂载目录传递给它们即可。
  • 适用于其他环境-如果您的代码必须在不同的服务器上运行,tmpfs可以自适应并将页面交换到硬盘。如果您需要在没有空闲RAM的服务器上运行此代码,您可以将所有文件放在硬盘上,并使用普通文件系统,而根本不需要更改代码。

使用步骤:

  1. 创建tmpfs- sudo mount -t tmpfs -o size=600G tmpfs /mnt/mytmpfs
  2. 复制数据集- cp -r dataset /mnt/mytmpfs
  3. 将所有引用从当前数据集更改为新数据集
  4. 享受吧


编辑:

ramfs在某些情况下可能比tmpfs更快,因为它不实现页面交换。要使用它,只需按上面的说明将tmpfs替换为ramfs


这是目前为止最简单的方法;但文档建议不要使用tmpfs在容器之间共享内存:https://docs.docker.com/storage/tmpfs/ - 但这是主机上的tmpfs,由容器作为常规卷挂载,对吗? - Nino Walker
@NinoWalker 是的,tmpfs 在主机上。 - kmaork
@JacomStern,你能否将其与主机上的访问时间进行比较?我们正在谈论什么类型的访问?您尝试过只读取文件吗?此外,tmpfs实现了交换,这可能会导致类似磁盘的速度。您可以尝试使用ramfs(更低级别,无需交换)来排除这种情况。要使用它的命令相同,只需将tmpfs替换为ramfs即可。 - kmaork
1
我在本地机器上进行了检查,发现tmpfs比ramfs慢得多,建议你尝试一下。如果可以的话,我会相应地编辑我的答案 :) - kmaork
可能是openslide内部出现了延迟,而不是从磁盘读取时出现了延迟。但我怀疑这一点。你能否分享一下你的脚本,证明从ramfs加载图像更快? - Jacob Stern
显示剩余5条评论

1
我认为使用共享内存或mmap解决方案是合适的。
共享内存:
首先在服务器进程中将数据集读入内存。对于Python,只需使用multiprocessing包装器在进程之间创建共享内存对象,例如:multiprocessing.Valuemultiprocessing.Array,然后创建Process并将共享对象作为参数传递。
mmap:
将数据集存储在主机上的文件中。然后每个容器将文件挂载到容器中。如果一个容器打开文件并将文件映射到其虚拟内存中,那么其他容器在打开文件时不需要从磁盘读取文件到内存中,因为文件已经在物理内存中。
附注:我不确定cpython如何实现大型进程间共享内存,可能cpython内部使用了mmap。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接