
Локальная работа в Jupyter-ноутбуках – неотъемлемая часть исследований и экспериментов нашего ML-отдела. Но из какой среды эти ноутбуки лучше запускать? Мы пользуемся двумя вариантами: запуском из Docker-контейнера и запуском в изолированном локальном Poetry-окружении.
О чем и для кого эта статья
В этой статье мы по кусочкам соберем минимальный сетап для локального изолированного проведения ML-экспериментов в JupyterLab.
Он подойдет для небольших и средних проектов, где пока нет необходимости задействовать сервера с огромной мощностью и для работы над экспериментами хватает локальных машин участников проекта.
При сборке сетапа мы руководствуемся еще такими соображениями:
Нам надо, чтобы эксперименты были воспроизводимы на уровне инфраструктуры: понятно, при каких версиях библиотек, на какой ОС все было сделано, и если запустить код в тех же условиях, то получим тот же результат. За рамки скоупа статьи выходят фиксирования параметров в самом коде (например, random_state).
Нам надо, чтобы зависимости экспериментов были изолированы от глобального окружения ПК.
Почему важна изоляция в Jupyter-экспериментах
Во время предварительных исследований, тестирований гипотез, проверки работоспособности каких-то кусочков кода очень часто бывает, что для одной ML-модели требуется одна версия библиотеки, а для другой – другая.
Чтобы не накатывать все эти версии глобально и не ломать потом голову, как решить все конфликты, можно изолировать окружение для экспериментов от глобального. Тогда в случае чего можно просто пересоздать или переключиться на другое изолированное окружение, не засоряя все глобальные зависимости.
Пакетный менеджер
В основе обоих используемых нами подходов к изоляции окружений лежит Poetry – инструмент управления зависимостями в Python-проектах. Почему Poetry? Он самостоятельно разрешает конфликты между зависимостями и гарантирует повторяемость сборки благодаря файлам pyproject.toml и poetry.lock. И он просто работает без нареканий.
pyproject.toml – файл конфигурации проекта, в котором задаются его метаданные (имя, версия, авторство и т.п.) и объявляются зависимости, необходимые для работы.
poetry.lock – особый автоматически генерируемый файл Poetry, “фиксирующий” всю точную информацию об установленных зависимостях, включая транзитивные (зависимости зависимостей).
Минимальный pyproject.toml для запуска JupyterLab должен иметь следующее содержание:
[tool.poetry]
package-mode = false
[tool.poetry.dependencies]
python = "3.11.11"
jupyterlab = "^4.0.0"
jupyter = "^1.1.1"
Сформировать pyproject.toml можно с помощью следующей команды, заполняя поля по подсказкам в терминале:
poetry init
Также можно создать и заполнить файл вручную, либо перенести уже существующий из другого репозитория, например, нашего репозитория с преднастройкой (ссылка в конце статьи).
А сформировать poetry.lock, если его еще нет, можно следующей командой в терминале проекта:
poetry lock
Конечно, эти команды работают, только если у вас установлен Poetry глобально или вы уже внутри Docker-контейнера, где Poetry предустановлен.
Что насчет других зависимостей экспериментов? Как уже говорилось ранее, для разных экспериментов могут использоваться разные версии одной и той же библиотеки. Поэтому мы выбираем вариант, при котором в Poetry изначально добавляем только те зависимости, которые перетекают из эксперимента в эксперимент. Например, часто такой зависимостью может быть torch или scikit-learn.
Чтобы добавить такие зависимости в Poetry, можно использовать:
poetry add <package>
Дефолтный источник пакетов – PyPI, но при необходимости его можно изменить. Также эта команда поддерживает добавление git-dependencies. Обо всем этом и том, как указывать конкретную специфичную версию пакета, можно почитать по этой ссылке документации.
Другие зависимости, которые необходимы только для конкретных ноутбуков предпочитаем ставить уже внутри каждого нового ноутбука с экспериментом, чаще всего с помощью pip – это легко и при установке видно, какую конкретно версию pip сейчас вам ставит, что отвечает цели воспроизводимости экспериментов.
Хорошо, в качестве основного пакетного менеджера используем Poetry. Но с помощью чего лучше изолировать окружение с экспериментами?
Изолируем Docker’ом
Как правило, мы для изоляции используем Docker-контейнеры. Создаем образ на основе уже существующего с нужной версией Python, в образе устанавливаем Poetry и все необходимые зависимости из pyproject.toml файла, включая jupyter и jupyterlab. И затем вместе со стартом контейнера запускается и среда Jupyter.
Dockerfile для этой задачи выглядит примерно так:
# версия Python, которая совпадает с версией Python в pyproject.toml
FROM python:3.11.11-slim
ENV PYTHONFAULTHANDLER=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
WORKDIR /workspace
RUN apt-get update \
# https://packages.debian.org/ru/sid/python3-dev
&& apt-get install -y --no-install-recommends python3-dev \
&& python -m pip install --upgrade pip \
&& pip install poetry==2.1 \
&& pip install ipykernel
COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi
CMD ["jupyter-lab","--ip=0.0.0.0","--no-browser","--NotebookApp.token=''","--NotebookApp.password=''","--allow-root"]
Заметьте, что в Dockerfile происходит копирование poetry.lock файла в образ. Если у вас еще нет на этом этапе этого сгенерированного файла, можно:
убрать из команды копирования poetry.lock и сгенерировать его в первый раз уже внутри контейнера после сборки;
скопировать poetry.lock из репозитория с преднастройкой, который будет указан в конце статьи (при этом, если ваш pyproject.toml как-то отличается от того, который есть в том же репозитории, то этот вариант не подходит – lock-файл скорее всего не будет содержать все актуальные зависимости и это приведет к конфликту).
У подхода изоляции с помощью Docker сразу несколько преимуществ:
для работы глобально установленным нужен только Docker: все остальные зависимости ставятся уже внутри контейнера;
унифицированная среда у всех участников проекта, вне зависимости от ОС;
очень легко “сбросить” окружение и восстановить его заново при необходимости (например, если при установке библиотек в Jupyter-ноутбуке что-то пошло не так) - можно просто удалить/перезапустить контейнер.
В этом варианте мы чаще всего используем еще и docker-compose. Он нужен преимущественно для того, чтобы контролировать потребляемые контейнером ресурсы (CPU, RAM). При этом если не ставить никакие ограничения потребляемых ресурсов в docker-compose.yml, ресурсы ограничиваются глобальными настройками Docker.
А еще через docker-compose можно дать контейнеру доступ до GPU через простую настройку.
Обычно docker-compose.yml для проекта выглядит так:
services:
project-name-notebooks:
container_name: project-name-notebooks
ports:
- 4321:8888 # внешний порт 4321 может быть любым
volumes:
- .:/workspace
build: .
deploy:
resources:
limits: # опционально задаются в зависимости от потребностей в работе с jupyter
cpus: '7'
memory: 12G
reservations: # можно дать контейнеру доступ к GPU
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
Запускается контейнер с JupyterLab следующей командой в директории проекта:
docker compose up --build -d project-name-notebooks
Если GPU на устройстве нет, то контейнер не сможет сбилдится и запуститься. Если это как раз ваша ситуация, все должно заработать как надо после удаления из docker-compose.yml блока reservations
.
После запуска контейнера при переходе в браузере по адресу http://localhost:4321 доступна среда разработки JupyterLab.
Изолируем Poetry
Есть сценарий, когда работать из Docker-контейнера для нас не вариант. К сожалению, пока что технология MPS (Metal Performance Shaders), необходимая для использования GPU-ядер на устройствах Mac с чипом M-серии, не интегрируется в Docker-контейнеры. Поэтому в таких условиях, когда без ресурсов графического процессора не обойтись, приходится локально запускать Jupyter из Poetry-окружения. В таком случае Poetry используется и как пакетный менеджер, и как средство изоляции зависимостей.
Для запуска в первую очередь необходимо убедиться, что на устройстве есть версия Python, соответствующая требованиям проекта (указана в pyproject.toml), а также глобально установлен Poetry. В документации по ссылке есть руководство о том, как это можно сделать.
После установки, находясь в одной директории с файлом pyproject.toml, в терминале вызываем:
poetry install
Так Poetry автоматически создает и активирует изолированное окружение, устанавливая все необходимые зависимости. И теперь из созданного окружения можно запускать среду JupyterLab с помощью команды:
poetry run jupyter lab
Очевидный и самый главный минус такого подхода – необходимость локально устанавливать Python и Poetry, причем иногда для разных проектов требуются разные версии.
Ну и в случае необходимости одной командой пересоздания контейнера стереть ошибки зависимостей или кэша в Jupyter-ноутбуках не получится – нужно удалять окружение и создавать новое (что, впрочем, не так уж сложно, но все же требует чуть больше усилий, чем в случае с Docker-контейнером).
Также в Poetry-окружении нельзя ограничить потребляемые ресурсы, оно использует все доступные для захвата.
О том, как управлять Poetry-окружениями (запускать, менять версию Python, удалять), можно прочитать по ссылке.
Откуда же в итоге запускать?
Итак, исходя из нашей практики, подход с локальным запуском среды Jupyter для экспериментов из Docker-контейнера предпочтительнее, потому что для работы над экспериментами необходим только Docker, потребление ресурсов железа легко контролировать и такое окружение одинаково воспроизводится у всех участников проекта вне зависимости от используемой ОС.
Локальный запуск из Poetry-окружения – вынужденная мера, которая (верим и надеемся) в будущем пропадет с появлением интеграции MPS в Docker. Из его минусов: необходимость установки Poetry и Python и неполная воспроизводимость экспериментов, потому что иногда результаты немного могут отличаться из-за разных операционных систем участников проекта. При этом плюс, который недостижим в Docker: возможность пользоваться MPS для членов команды с устройствами Mac с чипами M-серии.
Хочется отметить, что для того, чтобы поддерживать оба способа изоляции окружений не требуется дополнительно никаких действий: как в Docker-окружении, так и в локальном Poetry-окружении используются одни и те же файлы pyproject.toml и poetry.lock для управления зависимостями.
Есть, конечно, и альтернативные варианты запуска окружения для экспериментов. Например, часто используют Conda, но с ней нельзя пошарить окружение между участниками проекта. Кто-то использует Kubeflow, который к тому же можно развернуть на сервере и задействовать его мощности.
Но в нашем варианте используются довольно простые инструменты, что значит, что порог входа для работы ниже. К тому же, этого экспериментального сетапа хватает для многих ML-задач, особенно вкупе с мощными ПК.
А как вы поддерживаете локальное окружение для экспериментов? Будем рады почитать в комментариях и перенять опыт :)
Репозиторий с преднастройкой: https://github.com/TourmalineCore/TourmalineCore.Articles.Examples.simple-jupyter-setup
Авторы статьи: Снежана Лазарева, Мария Ядрышникова
Вычитка и фидбек: Александр Шинкарев, Владимир Губин
Оформление: Маргарита Попова