Общая память IPC для скриптов Python в отдельных контейнерах Docker

Эта проблема

Я написал классификатор нейронной сети, который принимает большие изображения (~ 1-3 ГБ за штуку), исправляет их и передает патчи по сети индивидуально. Обучение шло очень медленно, поэтому я провел сравнительный анализ и обнаружил, что для загрузки патчей из одного изображения в память (с использованием библиотеки Openslide) требуется ~ 50 с, а для прохождения их через модель - ~ 0,5 с.

Тем не менее, я работаю над суперкомпьютером с 1,5 ТБ ОЗУ, из которых используется только ~ 26 ГБ. Набор данных составляет ~ 500 Гб. Я думаю, что если бы мы могли загрузить весь набор данных в память, это значительно ускорило бы обучение. Но я работаю с исследовательской группой, и мы проводим эксперименты на нескольких скриптах Python. Поэтому в идеале я хотел бы загрузить весь набор данных в память одним сценарием и иметь доступ к нему для всех сценариев.

Больше деталей:

  • Мы проводим наши индивидуальные эксперименты в отдельных контейнерах Docker (на одной машине), поэтому набор данных должен быть доступен для нескольких контейнеров.
  • Набор данных - это набор данных Camelyon16; изображения хранятся в формате .tif.
  • Нам просто нужно прочитать изображения, не нужно писать.
  • Нам нужно только получить доступ к небольшим частям набора данных одновременно.

Возможные решения

Я нашел много постов о том, как делиться объектами Python или необработанными данными в памяти между несколькими скриптами Python:

Обмен данными Python между скриптами

Серверные процессы с SyncManager и BaseManager в многопроцессорном модуле | Пример 1 | Пример 2 | Документы - Серверные процессы | Документы - SyncManager

  • Положительные стороны: могут совместно использоваться процессами на разных компьютерах по сети (могут ли они использоваться несколькими контейнерами?)
  • Возможная проблема: медленнее, чем использование общей памяти, в соответствии с документацией. Если мы разделяем память между несколькими контейнерами, используя клиент/сервер, будет ли это быстрее, чем все сценарии, читающие с диска?
  • Возможная проблема: в соответствии с этим ответом объект Manager выбирает объекты перед отправкой, что может привести к замедлению работы.

модуль mmap | Документы

  • Возможная проблема: mmap отображает файл в виртуальную память, а не в физическую память - он создает временный файл.
  • Возможная проблема: поскольку мы используем только небольшую часть набора данных за один раз, виртуальная память помещает весь набор данных на диск, мы сталкиваемся с серьезными проблемами и ошибками программы.

Pyro4 (клиент-сервер для объектов Python) | Документы

Модуль sysv_ipc для Python. Это демо выглядит многообещающе.

Я также нашел этот список опций для IPC/сетей в Python.

Некоторые обсуждают настройки сервера-клиента, некоторые обсуждают сериализацию/десериализацию, которая, боюсь, займет больше времени, чем просто чтение с диска. Ни один из ответов, которые я нашел, не отвечает на мой вопрос о том, приведут ли они к повышению производительности ввода-вывода.

Совместное использование памяти через контейнеры Docker

Нам нужно не только разделять объекты/память Python между скриптами; нам нужно поделиться ими через контейнеры Docker.

Docker документация объясняет --ipc флаг очень хорошо. Что имеет смысл для меня по документации работает:

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

Но когда я запускаю свой клиент и сервер в отдельных контейнерах с подключением --ipc настроенным, как описано выше, они не могут общаться друг с другом. Вопросы, которые я прочитал (1, 2, 3, 4), не касаются интеграции разделяемой памяти между скриптами Python в отдельных контейнерах Docker.

Мои вопросы:

  • 1: Предоставит ли какой-либо из них более быстрый доступ, чем чтение с диска? Разумно ли даже думать, что совместное использование данных в памяти между процессами/контейнерами повысит производительность?
  • 2: Какое решение будет наиболее подходящим для совместного использования данных в памяти между несколькими контейнерами докеров?
  • 3: Как интегрировать решения для совместного использования памяти из Python с docker run --ipc=<mode>? (Является ли общее пространство имен IPC даже лучшим способом разделения памяти между контейнерами Docker?)
  • 4: есть ли лучшее решение, чем это, чтобы решить нашу проблему больших накладных расходов ввода-вывода?

Минимальный рабочий пример - Обновлено. Не требует никаких внешних зависимостей!

Это мой наивный подход к разделению памяти между скриптами 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

Ответ 1

Я рекомендую вам попробовать использовать tmpfs.

Это функция Linux, позволяющая вам создать виртуальную файловую систему, которая хранится в оперативной памяти. Это обеспечивает очень быстрый доступ к файлу и требует всего одну команду bash для настройки.

Помимо того, что он очень быстрый и понятный, он имеет много преимуществ в вашем случае:

  • Не нужно трогать текущий код - структура набора данных остается неизменной
  • Никакой дополнительной работы по созданию общего набора данных - просто cp набор данных в tmpfs
  • Общий интерфейс - будучи файловой системой, вы можете легко интегрировать набор данных -r AM с другим компонентом вашей системы, который не обязательно написан на python. Например, это было бы легко использовать внутри ваших контейнеров, просто передав в них каталог монтирования.
  • Подходит для других сред - если ваш код должен будет работать на другом сервере, tmpfs может адаптировать и поменять страницы на жестком диске. Если вам придется запускать это на сервере без свободной оперативной памяти, вы можете просто хранить все свои файлы на жестком диске с нормальной файловой системой и вообще не трогать ваш код.

Шаги для использования:

  1. Создайте tmpfs - sudo mount -t tmpfs -o size=600G tmpfs/mnt/mytmpfs
  2. Копировать набор данных - cp -r dataset/mnt/mytmpfs
  3. Изменить все ссылки из текущего набора данных на новый набор данных
  4. наслаждаться


Редактировать:

ramfs может быть быстрее чем tmpfs в некоторых случаях, так как он не реализует перестановку страниц. Чтобы использовать его, просто замените tmpfs на ramfs в инструкциях выше.

Ответ 2

Я думаю, что shared memory или решение mmap является правильным.

Общая память:

Сначала прочитайте набор данных в памяти в процессе сервера. Для python просто используйте multiprocessing оболочку для создания объекта в разделяемой памяти между процессами, например: multiprocessing.Value или multiprocessing.Array, затем создайте Process и передайте общий объект как аргументы.

ММАП:

Храните набор данных в файле на хосте. Затем каждый контейнер монтирует файл в контейнер. Если один контейнер откроет файл и отобразит файл в его виртуальную память, другому контейнеру не потребуется считывать файл с диска в память при открытии файла, поскольку файл уже находится в физической памяти.

PS Я не уверен, как реализация cpython большая общая память между процессами, вероятно, общая память cpython использует внутреннюю mmap.