В нашей компании есть много проектов, связанных с AI. Всем им нужны ресурсы для работы с моделями на GPU. «Хотим, чтобы только у нас был доступ к оборудованию», — это лишь одно из требований инженеров из AI-дивизиона, а еще нужно оптимизировать использование GPU-ресурсов, вести их учет и быстро готовить оборудование к передаче другой команде.
Привет, Хабр! Меня зовут Вадим Извеков, я руководитель группы сопровождения платформы машинного обучения в YADRO. Сегодня расскажу, почему мы решили создать свою MLOps-платформу, как она устроена и для чего используется. Вы узнаете, зачем нам два Argo CD и почему мы выбрали СХД TATLIN для хранения данных.
Задачи AI-дивизиона и нашей команды
Дивизион занимается обучением и внедрением моделей в рабочие процессы компании. Обычно процесс начинается с поиска или прототипирования архитектуры модели. Далее идет сбор и разметка данных датасета — это один из самых трудоемких этапов. Затем начинается само обучение, которое из-за специфики может отнимать много времени и ресурсов.
Параллельно дивизион разрабатывает приложения и сервисы с использованием моделей. Сценарий работы таких приложений обычно следующий:
Пользователь или приложение отправляет запрос в AI-сервис.
В зависимости от типа запрос попадает в нужную модель, запущенную на GPU.
Пользователь получает результат обработки запроса: текст, суммаризацию рабочего созвона, дополнение кода и так далее.
Мы как Embedded DevOps/MLOps-команда AI-дивизиона закрываем несколько направлений:
мониторинг на уровне компании,
CI/CD как сервис для всего дивизиона,
помощь разработчикам с настройкой пайплайнов и автоматизацией рутины,
внутри дивизиона поддерживаем инфраструктуру для обучения моделей и работы приложений.
Недавно мы получили задачу: создать инфраструктуру для приложений, которыми пользуются внешние по отношению к дивизиону команды, — фактически организовать production-окружения.
Присоединяйтесь к нашей команде, сейчас мы ищем техлида на LLM и старшего AQA-инженера.
Зачем нам нужна MLOps-платформа
Чтобы погрузиться в контекст, разберем основные предпосылки создания платформы. Без централизованного управления часто возникают ситуации, когда ресурсы одной команды простаивают: эксперимент или обучение закончены, а для следующего задания еще не собраны данные или не выбрана модель. В это время у другой команды — перегрузка и очередь задач.
Поэтому первоначальная задача MLOps-платформы — оптимизация использования железа между разными командами и задачами. В процессе эксплуатации нужна прозрачная картина использования ресурсов, чтобы вовремя планировать их наращивание и понимать, как в конкретном эксперименте распределяется нагрузка. Без централизованной системы это сделать сложно.
Также, чем меньше ручных манипуляций с железом, тем стабильнее оно работает. Если используется унифицированный подход к установке и настройке серверов, то мы можем быстрее ввести их в эксплуатацию или, наоборот, вывести для апгрейда или ремонта.
Собрав данные об использовании GPU на наших серверах, мы увидели, что мощности иногда простаивают:

На скриншоте показаны серверы, которые не подключены к платформе, и их использование. Фактически GPU загружены лишь на паре машин: около 20% на первой и периодические пики на второй. Остальные почти не задействованы, если не учитывать загрузку CPU и RAM.
Следующая задача MLOps-платформы — учет. Данные о наличии ресурсов и выполняемых на GPU задачах раньше велись вручную и часто оказывались неактуальными. Мы пытались автоматизировать учет, но нужного результата не добились.
Назначение серверов все равно приходилось вводить вручную представителям команд, а бюрократию никто не любит — в итоге данные обновлялись редко. Из-за отсутствия центральной системы учета и управления ресурсы дробились на атомарные однотипные задачи, которые было сложно систематизировать.
Наконец, третья задача для MLOps-платформы — обслуживание. Ситуация осложнялась тем, что оборудование часто требовалось передавать быстро. Несоблюдение сроков нарушало планы и негативно влияло на OKR команд и всего дивизиона. Задачу усложняло то, что, казалось бы, должно помогать: детальные требования команд к настройке серверов. Требований было много, причем они отличались в зависимости от нужд конкретной команды и выполняемой задачи. В результате простая передача хоста от одной команды к другой превращалась в настоящее приключение.
Нужно было найти свободный хост, согласовать период передачи, переустановить ОС, установить требуемые пакеты и приложения. После завершения аренды все приходилось выполнять в обратном порядке. Это занимало много времени и человеческих ресурсов. И чем больше у нас было хостов — тем больше требовалось людей.
Требования пользователей: «Хотим, чтобы только мы»
Основная наша группа пользователей — Data Science-инженеры. Они тренируют модели и проводят эксперименты. Последние создают множество артефактов, которые нужно удалять после завершения работы.
Инженерам нужны разные окружения для разных задач — иногда даже для одного и того же эксперимента нужны несколько отличающихся окружений. Часто обучения длятся долго, и для них важен эксклюзивный доступ к GPU.
Вторая группа — разработчики. Они создают приложения на основе моделей, которые им предоставляют Data Science-инженеры. Разработчикам важно быстро и удобно выводить приложения на прод, в том числе с использованием GPU. Поэтому нашей команде нужно:
обеспечить мониторинг и оповещения о неисправностях, чтобы приложения работали стабильно,
отслеживать доступные ресурсы и в случае их дефицита вовремя масштабировать инфраструктуру,
учитывать требования ИБ к нодам, на которых запускаются приложения, используемые сотрудниками компании: установить антивирус, агентов для отслеживания состояния хостов и применить необходимые специфические настройки.
Для публично доступных приложений мы хотим гарантировать определенный уровень сервиса, поэтому нужен автоматизированный сбор метрик, по которым рассчитывается время доступности сервиса.
Что объединяет всех пользователей платформы и что их ограничивает? В первую очередь, всем нужны разные GPU, а их мало. Сейчас на платформе доступно около сотни GPU, при этом более 60% парка графических ускорителей занято постоянно.
В YADRO мы разрабатываем ряд ИИ-инструментов в помощь коллегам. Они используют LLM, за работу которых отвечает инференс-кластер. Из статьи вы узнаете, какие LLM используют ИИ-сервисы компании и как устроен инференс-кластер.
Еще один важный момент — команды не хотят проблем с железом. При этом каждой команде нужно свое окружение. Сложно подобрать аппаратную конфигурацию сервера так, чтобы несколько GPU работали на скорости, приемлемой для AI-задач. Отдать это на откуп командам тоже неправильно: легко потратить много денег на сервер, который не будет выполнять задачу.
А еще нужен простой механизм обновления ключевых компонентов системы: драйверов, тулкитов, агентов мониторинга и так далее. Мы пробовали использовать Btrfs-снапшот для управления версиями окружений на одном сервере, но после недолгих экспериментов отказались: решение оказалось сложным и трудозатратным в эксплуатации.
«Хотим, чтобы только мы» — команды часто хранят данные локально на серверах или хотят использовать оборудование, которое не нужно делить с другими командами. Это нужно для длительных экспериментов, обучения стажеров, для business critical-приложений, которым требуются гарантированные ресурсы и масштабирование, а также когда доступ к коду должен быть только у одной команды.

Наконец, к нам регулярно приходят запросы на выделение оборудования для тестов. В этом случае тоже нужно найти хост, настроить его, а после тестов вернуть в исходное состояние.
Обзор готовых решений
Осознав масштабы проблемы, мы начали изучать доступные решения. Сначала посмотрели в сторону OpenStack, который у нас уже развернут и используется. Для Data Science он подходит: позволяет поднимать полностью изолированные окружения.
Но быстро перераспределять ресурсы между командами и проектами OpenStack не позволяет. У него есть собственные механизмы оркестрации, можно подключать и сторонние, но их пришлось бы дорабатывать под наши требования — то есть привлекать отдельную команду разработчиков. Важный момент: виртуализация ресурсов, особенно GPU, заметно снижает производительность, а это в условиях дефицита GPU — не лучшая идея.
Вторым кандидатом стал KubeFlow от Google. У него сильная поддержка: большое комьюнити и много встроенных инструментов для ML, но он в основном заточен именно под ML-задачи.
В нашем случае не хватало встроенной возможности запускать готовые модели и приложения, которые их используют, в одном кластере Kubernetes. Потребовались бы доработки, но с open source сейчас это сложно:
нужно либо отправлять изменения в основной репозиторий и добиваться, чтобы их приняли (что в текущих условиях почти нереально),
либо держать локальный форк и развивать его — но тогда начинаются проблемы с обновлениями из основного репозитория.
В итоге победил третий вариант — обычный Kubernetes с дополнительными компонентами. Для Data Science он подходит условно, потому что в Kubernetes нельзя сделать полностью изолированное окружение. Зато мы контролируем состав платформы, а значит, дорабатывать и менять компоненты проще. В целом у Kubernetes сильная поддержка сообщества и много возможностей для расширения функционала.

Мы остановились на этом варианте и сделали первый подход, чтобы посмотреть на практике, с чем предстоит работать. Для кластера Kubernetes нужны ноды Control Plane, Ingress, Load Balancer и Worker-ноды без GPU. Пользователям также необходимы базы данных, серверы очередей, сбор метрик и так далее. Все это должно быть максимально доступным, надежным и масштабируемым.

У нас есть небольшой плюс: инфраструктурные компоненты не требуют специфической аппаратной конфигурации. Логичное решение — запускать их как виртуальную машину (ВМ) в уже существующем OpenStack. Это дает быстрый старт (быстрее, чем ставить все на физические ноды) и отказоустойчивость: диски ВМ лежат в общем хранилище, и в случае сбоя гипервизора машины можно быстро поднять на соседнем сервере. А еще платформа не привязана к конкретному железу, поэтому для замены вышедшего из строя узла не нужна его точная копия.
Отдельно настроены GPU-ноды, на которых выполняется основная работа. Здесь много аппаратной специфики, так как добиться работы нескольких GPU в режиме x16 непросто. Нам важно быстро добавлять и удалять такие ноды, поэтому их аппаратная и программная конфигурация максимально унифицированы.
Чтобы исключить внешнее влияние, мы убрали возможность работать напрямую с этими хостами. При едином подходе к управлению такими нодами получаем общий мониторинг и алертинг по ним. Поэтому мы отказались от виртуализации.
В итоге в нашей системе есть кластер OpenStack с инфраструктурными нодами в виде виртуальных машин и рядом — физические GPU-хосты без виртуализации:

Внутренние требования команды
Поддерживать и развивать платформу предстоит нашей команде, поэтому мы начали собирать требования внутри. Мы не хотим тратить много времени на рутину — ввод новых узлов в эксплуатацию, обновление их параметров, управление компонентами и так далее.
Идеальное решение для нас — отдать разработчикам возможность самим управлять своими приложениями, а за собой оставить функцию контроля корректности основных настроек приложений и системы в целом. Все изменения, как в приложениях, так и в системе в целом, должны проходить быстро и просто.
Для первичной настройки виртуальных и физических серверов мы выбрали Ansible, который позволяет:
создавать пользователей,
управлять настройками и компонентами в соответствии с требованиями информационной безопасности,
управлять стеком мониторинга.
Если мы уже используем Ansible, то для управления Kubernetes решили взять Kubespray. Его не просто поддерживать, но он позволяет быстро поднимать кластер, добавлять и удалять ноды, менять настройки, ставить лейблы и taint’ы, а также управлять набором встроенных компонентов Kubernetes — Ingress, MetalLB, Argo CD и так далее.
Для автоматизации Ansible внедрили AWX, что открыло дорогу к GitOps в части управления Kubernetes.
Как мы упростили работу с Ansible в YADRO: почему выбрали AWX, какие задачи решили и какие приятные «плюшки» получили.
Приложениями пользователей и служебными компонентами кластера управляем через Argo CD: часть задач отдали разработчикам, а за собой оставили контроль вносимых изменений. Пользователи получили WebUI — можно заходить в консоль контейнеров, смотреть логи и общее состояние приложений. Также появилась аутентификация по доменным учетным записям.

Требования всех пользователей
Мы выяснили, что часть приложений не занимает GPU полностью и не создает постоянной нагрузки. Поэтому потребовалась возможность совместного использования одного GPU несколькими приложениями, запущенными в разных контейнерах.
Некоторым приложениям нужны разные версии драйверов для GPU, поэтому нам нужен инструмент для централизованного управления версиями. Также нам нужно удалять лишние компоненты внутри контейнеров, так как из-за них размер образа может увеличиваться на 2–6 ГБ, что заметно влияет на скорость запуска приложений.
Для решения этих задач мы использовали NVIDIA GPU Operator — он позволяет делить одну GPU между несколькими приложениями и автоматически добавлять в контейнеры необходимые компоненты для работы с GPU.
Мы протестировали платформу YADRO G4208P с восемью H100 NVL и RTX 4090 на десятке ИИ-моделей и опубликовали результаты.
Разделение GPU может работать в режиме time-slicing, по аналогии с CPU и RAM в Kubernetes, либо через подключение одного GPU к нескольким контейнерам без ограничений. Драйвер и набор компонентов для видеокарт можно экспортировать с хоста, на котором запущены контейнеры, что позволяет плавно проводить обновления.
Хранилище — второй по важности компонент платформы. Оно должно быть быстрым, надежным, с понятным интерфейсом доступа и высокой пропускной способностью.
Размер датасета может достигать десятков и сотен терабайт, и собрать такой набор данных — задача ресурсоемкая. Если датасет размечен и подготовлен к обучению, то его ценность возрастает в разы. Размеры моделей у нас доходят до 100 ГБ. Потеря модели, которую мы обучили с нуля или дообучили, равна потере недель или месяцев работы целого отдела. Поэтому требования к надежности хранилища у нас очень высокие.
Первым типом хранилища мы выбрали TATLIN.OBJECT для работы с данными по протоколу S3:

У этого решения высокая надежность и большой запас по емкости, который при необходимости можно увеличить. Производительность TATLIN.OBJECT позволяет держать там модели и крупные датасеты и работать с ними напрямую — для обучения и для хранения результатов тестирования.
Второй тип хранилища — TATLIN.FLEX:

Его мы используем как NFS-сервер для работы с пользовательскими данными, обмена данными между запущенными приложениями, а также процессами/сценариями обучения и внешним миром — командой и пользователями. Для такого сценария необходима высокая производительность сетевого хранилища и отказоустойчивость NFS-сервера, что полностью обеспечивается TATLIN.FLEX.
Третий тип хранилища — локальные NVMe-диски для данных экспериментов, локального кэша, снапшотов, предварительного тестирования моделей и всего, что требует очень быстрых операций чтения/записи при сравнительно небольших объемах.
Далее мы перешли к требованиям Data Science-инженеров, которые уточнялись в процессе работы, а часть требований выяснилась только на практике:
определенные пакеты ПО должны быть в рабочем окружении по умолчанию,
работа с несколькими GPU одновременно: не только в рамках одного контейнера, но и в нескольких параллельных сессиях у одного пользователя,
единая система авторизации: нескольким пользователям нужен доступ к одной и той же среде обучения/работы.
Решением стал JupyterHub с KubeSpawner, который позволяет:
запускать поды с произвольными ресурсами в Kubernetes,
настраивать YAML-шаблон пода ноутбука,
добавлять дополнительные компоненты и вызывать функции через API Kubernetes.
Такой подход позволил собирать информацию об имеющихся ресурсах в Kubernetes и дать пользователю свободу выбора ресурсов Jupyter-ноутбука:

Инженеры привыкли работать с IDE, поэтому нам важно было предоставить такой способ. Самый простой вариант — через SSH. Он позволяет популярным инструментам, например VS Code, работать с удаленной средой разработки — в первую очередь с ее ресурсами и файловой системой — без захода в UI самого Jupyter-ноутбука.
Первое, что мы попробовали, — SSH Reverse Proxy. При старте Jupyter-ноутбук запускается скрипт, создающий SSH-туннель к отдельному хосту вне Kubernetes. В контейнере пользователя формируется переменная окружения с адресом и портом для подключения к тоннелю, через который пользователь попадает в Jupyter-ноутбук.
У этого решения была пара проблем:
сессии постоянно обрывались по таймауту или из-за проблем с сетью,
JupyterHub умеет удалять контейнеры, которые долго не используются, но из-за установленных соединений система считала, что контейнер активен, и не удаляла его.
Мы решили доработать наш вариант: с помощью KubeSpawner в JupyterHub изменили шаблон пода ноутбука. Теперь вместе с подом создается Kubernetes Service с внешним IP, который публикует SSH-порт ноутбука. Пользователь подключается по SSH к контейнеру напрямую.
Сессии перестали рваться, а неиспользуемые контейнеры снова удаляются автоматически.
Наконец, последняя доработка — несколько типов хранилища Jupyter-ноутбука:
Первый — самый быстрый, он использует локальную папку home на сервере, где запущен контейнер, и привязан к одному хосту, но дает производительность локального диска.
Второй — тоже быстрый, но без сохранения данных: удобно для проверок и тестов, когда не хочется засорять личное хранилище.
Третий — NFS, когда данные сохраняются и доступны из нескольких ноутбуков или устройств пользователя одновременно, но скорость ниже. Сейчас это 1 Гбит/с на хост.
Гарантированные ресурсы
Напомню, что командам нужны выделенные мощности — «хотим, чтобы только мы». Оказалось, что скрыть ресурсы от других пользователей без микроменеджмента и сложных оркестраторов довольно просто.
Мы добавили получение доменных групп пользователя при авторизации в JupyterHub. Чтобы ограничить видимость хостов в UI JupyterHub (и в целом в Kubernetes), на нужные хосты повесили taint с именем доменной группы пользователя. Оставалось реализовать отображение только тех хостов, у которых taint совпадает с группой пользователя. Теперь в JupyterHub хосты с taint пользователю просто не видны, если они не совпадают с его группой.
Администрирование доменных групп передано на сторону общей системы авторизации пользователей с одобрением ответственных лиц от команд. Да, если пользователь знает имя taint, теоретически он может попытаться запустить там свое приложение, но у нас все изменения проходят через рецензирование кода (code review), так что сделать это незаметно практически невозможно.
Зачем нам два Argo CD
У нас Argo CD получает изменения Helm-чартов из Git, отслеживает версии Docker образов в хранилище и при изменении версии образа записывает обновление обратно в репозиторий. В такой схеме мы столкнулись с рядом проблем:
из-за большого количества приложений обновления занимали заметное время,
один репозиторий для инфраструктуры и пользовательских приложений приводил к конфликтам и rebase,
мы не хотели раскрывать внутреннюю кухню разработчикам.
Поэтому в MLOps-платформе работают два независимых Argo CD. Первый управляет приложениями разработчиков. Он не ограничен по числу пользователей, которые туда входят, но при этом сильно урезан по правам: можно управлять только частью контейнеров и выполнять ограниченный набор операций.
Второй Argo CD управляет инфраструктурными компонентами MLOps-кластера. В него допускается ограниченный круг пользователей, зато по правам он почти не ограничен для контейнеров и системных сервисов кластера. В частности, через него управляются NVIDIA GPU Operator, Kong и так далее.
Каких результатов мы добились
Во-первых, оптимизировали использования ресурсов. На графике ниже видно, что теперь загрузка наших хостов близка к 100% почти все время, кроме небольших провалов ночью и в выходные:

Во-вторых, мы наладили учет ресурсов и выполняемых на GPU задач. Это еще не идеал, но теперь видно, какой хост чем занят, какая команда и под какой проект запустила нагрузку:

Теперь понятно, к кому идти с вопросами по использованию конкретных серверов.
В-третьих, мы улучшили процессы обслуживания серверов: теперь запросы касаются платформы в целом. Разовые обращения по отдельным хостам остались, но в основном это ввод новых хостов в платформу или их вывод.
Планы по развитию
Пока готовилась эта статья, мы разделили DEV- и PROD-контуры в нашей MLOps-платформе, а также организовали отдельную тестовую площадку для нагрузочного тестирования.
Чтобы повысить SLA, нужен четкий регламент вывода приложений на прод. Разным группам пользователей требуются разные уровни доступа к разным площадкам. Уже идет процесс разделения на три контура: DEV, TEST и PROD.
Еще мы планируем реализовать экспорт GPU. Сейчас поды работают только с локальными GPU на хосте, где они запущены. Для экспериментов с кластером GPU приходится все настраивать вручную, временно выводить серверы из платформы и так далее. Это долго и неудобно. В планах — дать возможность использовать GPU соседних хостов из пода, запущенного на другом хосте.