Как подружить backend с ML

Khambar Dussaliyev
Kolesa Group
Published in
9 min readFeb 23, 2023

Я Хамбар Дусалиев — TeamLead команды ML, занимаюсь MLOps и backend в ML-сервисах Kolesa Group. Одна из моих задач — оптимизация процессов разработки ML-сервисов и снижение Time to Market (TTM).

Хамбар Дусалиев, TeamLead команды ML в Kolesa Group

В этой статье вы узнаете:

- Как в компании возникла необходимость автоматизации деплоя ML-моделей;
- Проблемы подхода: технические и организационные;
- Выстраивание внутренних процессов;
- Архитектуру компонентов MLflow;
- Первые проблемы в адаптации;
- Последовательное решение проблем;
- Как это помогло бизнесу;
- Советы.

Как в компании возникла необходимость автоматизации деплоя ML-моделей

Первые ML-модели в компании были экспериментом. Потребность в них возникла, потому что в какой-то момент в компании перестали работать классические бэкендерские подходы: системы поиска и рекомендаций нуждались в доработке. Так в компании появились такие ML-сервисы, как автомодерация объявлений, определение цены квартиры, рекомендательные системы, распознавание номеров машин, распознавание техпаспорта и другие.

Со временем бизнес начал чувствовать эффект: на ML стали опираться. К примеру, сервис автомодерации на Kolesa.kz забрал на себя 70% работы, разгрузив команду сервиса. ML-сервисы постепенно стали частью бизнеса, повысились требования к их доступности, скорости разработки и эффективности работы.

Путь ML-модели до продакшна на тот момент времени:

Возникли проблемы подхода: технические и организационные

Технические:

- Трудно версионировать модели;
- Нельзя масштабировать ML-нагрузку отдельно от сервиса API вокруг модели.

Организационные:

- Слабая связанность продуктовой команды и ML-команды: продукт и ML плохо понимали друг друга;

- Разные стеки, потому что основная экспертиза компании на PHP и Gо, а модели тренируются на Python, ML-сервисы тоже написаны на Python;

- Процессы были выстроены вокруг модели — чтобы начать разработку сервиса нужно было дождаться завершения работы над моделью;

- Как следствие, было сложно выкатывать фичи;

- ML-команда реализовывала бизнес-логику, напрямую влияющую на продукт. Это вызывало вакуум ответственности: любые изменения непонятно кем должны были быть поддержаны;

- Из-за разницы в стеках усугубился bus-фактор — число участников проекта, после потери которых проект не сможет быть завершен оставшимися участниками.

Выстраивание внутренних процессов

ML нужно было развивать: из просто эксперимента ML-модели выросли в серьёзные факторы, влияющие на бизнес. Но развивать нужно правильно: адаптировать изменения так, чтобы решать обозначенные выше проблемы и не создавать новых.

Мы изменили процессы:

1. ML-инженеры стали частью продуктовой команды: они генерируют идеи, участвуют в discovery, предлагают фичи и т.д.

2. ML-сервисы исходят из KPI продукта.

3. Ищем способы версионировать модели, унифицировать стек технологий и снизить TTM.

Предположим, ML-инженер разработал модель по распознаванию мошенников. Как запустить эту модель в продакшн? Как обновлять и отслеживать изменения? Как обеспечить воспроизводимость (reproducibility)? Через MLflow. Это инструмент с открытым исходным кодом, помогающий управлять жизненным циклом ML-моделей. Какую он несёт пользу:

- Осуществляет трекинг экспериментов;
- Позволяет упаковать код, связанный с моделью, в reproducible-формат;
- Деплоит модели на различные платформы;
- Выступает в качестве реестра моделей.

Архитектура компонентов MLflow

MLflow может выступать в нескольких возможных конфигурациях. Главное их отличие друг от друга в том, где и как они хранят артефакты и метаданные о моделях. Мы в Kolesa Group, как правило, работаем с MLflow примерно в следующей конфигурации:

Архитектура MLFlow

С MLflow можно работать и в других конфигурациях. Но оставлю эту тему вне этой статьи. Здесь я хочу больше сосредоточиться на этапе деплоя модели, которая уже трекается с помощью MLflow.

а) CLI

MLflow — это pip-пакет, с помощью которого его и можно установить. Внутри пакета есть почти всё, что нужно для запуска MLflow в режиме сервера.

Если переопределить переменную окружения MLFLOW_TRACKING_URI, то CLI будет обращаться к указанному хосту. Это позволяет использовать машину с переопределённой переменной как обычный клиент.

В случае использования MLflow в конфигурации с S3 хранилищем без проксирования, то потребуется еще переопределить MLFLOW_S3_ENDPOINT_URL, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY для того, чтобы у клиента была возможность стягивать артефакты из mlflow, а также установить boto3.

В этой статье сосредоточимся на генерации готовых docker-образов с сервером-обёрткой вокруг модели. Более подробно о возможностях MLflow можно прочитать здесь: https://www.mlflow.org/docs/latest/cli.html#.

б) Генерация docker-образа

Собрать docker-образ можно с помощью команды mlflow models -m “s3://<адрес бакета с артефактами>” -n “<название вашего образа>” build-docker.

Команда неявно стягивает файлы с S3 хранилища, генерирует dockerfile и запускает команду docker build.

MLflow имеет несколько backend flavors — абстракции над конкретным инструментом, которые можно использовать для деплоя и сервинга модели. Самый универсальный backend flavor — python_function. Далее в статье будет использоваться только он. При python_function backend flavor логика по инференсу на модель оборачивается в обычную питоновскую функцию. Подробнее с этой темой можно ознакомиться в официальной документации.

MLflow: сохранение модели
MLflow: генерация сервиса

Казалось бы на этом всё, но нет. Возникли сложности: мы не можем сразу интегрировать ML в продукт.

- MLflow не умел отдавать dockerfile для сборки, он пытался всё собирать в себе;
- Проблемы с CUDA (compute unified device architecture): модель не может работать с GPU (graphics processing unit);
- Нет готовой интеграции с CI/CD.

Первые проблемы в адаптации

Подобная реализация сборки docker-образов была для нас неудобна. Мы планировали включить процесс сборки в наш CI/CD, но возник ряд ограничений.

Наш CI/CD схематично можно изобразить следующим образом:

Схема CI/CD

Основные особенности нашего CI/CD:

1. Агенты сборки докеризированы и собрать docker-in-docker будет непросто, надо менять образы всех агентов.

2. Для сборки практически всех образов агенты используются не напрямую, а отдельно. Благодаря этому переиспользуются кэши и уменьшается время сборки.

3. Сборка на выделенном агенте происходит с помощью buildx и/или флага -H.

Так как не было возможности изменить параметры сборки образа, мы столкнулись с выбором:

1. Форкнуть MLflow и внести изменения.

2. Законтрибьютить в MLflow и добавить возможность пробрасывать флаги.

3. Найти неинвазивный способ вмешаться в сборку для вызова на удалённом агенте.

Последовательное решение проблем

Как я упоминал выше, MLflow написан на Python, в то время как компания пишет в основном на PHP и Go. Поэтому у нас не было достаточной экспертизы для постоянной поддержки форка. Контрибьюшена в исходный код мы тогда тоже избегали, потому что на тот момент MLfllow ещё был экспериментом. Мы не были до конца уверены, что это то, что нам нужно, а подвязываться под релизный цикл third party не хотелось.

Потому было решено пойти другим путем — переопределять переменную окружения DOCKER_HOST, чтобы неинвазивно подменить хост сборки на тот самый «docker-агент».

1. Пробовали использовать alpine. К сожалению, он не мог переиспользовать PyPI Wheels, из-за чего компилировал их заново. Это очень сильно замедляло процесс сборки. Мы нашли образ, который мог быть использован для сборки docker-in-docker, основанный на Ubuntu.

2. На базе этого образа мы написали новый. Который бы с переопределённым DOCKER_HOST и рядом дополнительных переменных мог собирать образы на удалённом хосте.

Участок кода, который описывает образ, выглядит так:

FROM cruizba/ubuntu-dind:latest


COPY . .


RUN set -ex \
&& apt-get update -y \
&& apt-get install -y --no-install-recommends python3-pip \
&& ln -sf python3 /usr/bin/python \
&& pip3 install --no-cache --upgrade mlflow boto3


ENV MLFLOW_TRACKING_URI=not-set \
AWS_ACCESS_KEY_ID=not-set \
AWS_SECRET_ACCESS_KEY=not-set \
MLFLOW_S3_ENDPOINT_URL=not-set \
DOCKER_HOST=not-set \
MODEL_NAME=non-set \
MODEL_S3_PATH=not-set


ENTRYPOINT ["bash", "-ex", "-c"]
CMD ["mlflow models build-docker -m ${MODEL_S3_PATH} -n ${MODEL_NAME}"]

Благодаря этому мы смогли встроить процесс CI/CD и деплоить образы, которые были построены с помощью MLflow в кластер kubernetes по привычной схеме.

а) Проблемы с CUDA

Вскоре после того, как мы задеплоили по этой схеме первые модели, мы стали тестировать поведение наших новых моделей-сервисов. Пришли к неутешительным выводам: наши модели не могли нормально загрузиться на видеокарту. Далее подробнее залогировали на этапе загрузки и подтвердили наши догадки — модель не могла использовать CUDA.

CUDA — это архитектурная платформа и API, которые позволяют легче запускать вычисления на видеокартах. Без валидной работы CUDA мы не можем загрузить модель и обеспечить исполнение на видеокартах.

Кроме того, просто так контейниризовать сервис, использующий CUDA не получится. Обычно виртуализируются только два ресурса: память и процессор. Среды контейниризации, например, тот же docker, обычно не умеют работать с видеокартами из коробки.

Все необходимые пререквизиты у нас были выполнены:

• На серверах стояли GPU от nvidia с возможностью использования CUDA;
• Стоял драйвер, совместимый с устройствами и софтом;
• Среда контейнеризации умеет пробрасывать доступ к видеокартам в контейнеры.

Проблема была в том, что сами контейнеры не имели CUDA и не могли ею воспользоваться.

Эксперименты с установкой после сборки не увенчались успехов. Плюс мы до сих пор не были до конца уверены, что такой подход нас устраивает. Чтобы как можно быстрее подтвердить или опровергнуть возможность использования CUDA, мы сделали следующее:

- клонировали mlflow из официального репозитория;
- нашли участок, ответственный за сборку;
- захардкодили образ с нужной версией CUDA в качестве базового образа.

Скрестили пальцы, запустили сборку и… Заработало! Теперь модель-сервис спокойно использовала ресурсы видеокарты и могла адекватно работать под нагрузкой.

б) Контрибьютим в MLflow

Теперь мы могли видеть результат и примерное понимание нужных ресурсов для поддержки. Но снова обнаружили себя в позиции, где мы должны были выбирать одно из двух:

• Форкнуть MLflow и поменять как нам нужно;
• Законтрибьютить в MLflow универсальный способ подмены нужных образов.

Решили, что удобнее будет законтрибьютить в MLflow напрямую.

1. Создали issue, открыли PR и смерджились с мастером.

2. В рамках PR реализовали новую команду MLflow model generatedockerfile. Она не собирает образ напрямую, а генерирует директорию с артефактами и dockerfile, который собирает новый образ. Благодаря этому можем менять dockerfile программно, подменяя базовый образ на совместимый образ с установленной CUDA.

Наш сборщик образов теперь выглядит примерно следующим образом:

FROM cruizba/ubuntu-dind:latest


COPY . .


RUN set -ex \
&& apt-get update -y \
&& apt-get install -y --no-install-recommends python3-pip \
&& ln -sf python3 /usr/bin/python \ && pip3 install --no-cache --upgrade mlflow boto3


ENV MLFLOW_TRACKING_URI=not-set \ AWS_ACCESS_KEY_ID=not-set \
AWS_SECRET_ACCESS_KEY=not-set \
MLFLOW_S3_ENDPOINT_URL=not-set \
DOCKER_HOST=not-set \ MODEL_NAME=non-set \
MODEL_S3_PATH=not-set

ENTRYPOINT ["bash", "-ex", "-c"]
CMD ["mlflow models generate-dockerfile -m ${MODEL_S3_PATH} \
&& cd mlflow-dockerfile \
&& python ../base_image_change.py --base_image=${BASE_IMAGE} \
&& docker build -t ${MODEL_NAME} ."]

В образе дополнительно вызывается скрипт, написанный на Python, который может изменить базовый образ.

Теперь наш путь до production выглядит примерно так:

Путь модели по production

Как это помогло бизнесу

1. Уменьшили TTM: выпускаем ML-модели раз в 2–3 недели, раньше это занимало месяцы;

2. Убрали Python из стека бэкенда (почти);

3. ML и MLOps не блокируют друг друга (почти);

4. Упростили версионирование и хранение моделей;

5. Продукту легче поддерживать сервисы, связанные с ML.

Отдельно можно вынести плюсом, что подход в целом универсальный — в компании не только ML-инженеры работают с данными, но также аналитики и data-инженеры. Они обычно занимаются анализом продуктовых метрик, ad-hoc исследованиями, A/B-тестами и попутными вещами. В своей работе они тоже часто используют ML-модели, которые, благодаря mlflow, тоже могут быть быстро превращены в сервис (при необходимости).

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

Не MLflow единым. Технология без процесса ничего не стоит

- ML-сервисы встраиваются в KPI продукта;
- Сформировали процесс разработки для новых сервисов;
- Смещаем всю бизнес-логику на продуктовые команды.

МL-команда всё ещё в процессе формирования, но у нас уже есть сервисы, которые работают по этой архитектуре.

Недостатки

- Приходится писать самим интеграцию с CI/CD;
- Среди подобных инструментов нет сформированного лидера;
- Инструмент на Python, но компания пишет на PHP и Go.

Что предстоит улучшить

- Доработать автоматизацию деплоя, потому что он до сих пор требует от нас ручного написания манифестов;
- Подвязать автоматизацию обучений\переобучений, особенно в рекомендовательных системах и системах, которые связаны с определением цены;
- Улучшить дружбу Kubernetes + GPU.

Советы

1. Чем больше сервисов, тем выше потребность в систематизации.

2. Просто внедрения инструмента может быть недостаточно, без изменения процессов можете столкнуться с организационными проблемами;

3. Всегда нужно искать итеративный подход по внедрению: соблюдать баланс между поддержкой старых и новых сервисов.

--

--