Привет, Хабр! Меня зовут Антон Алексеев, я MLOps-инженер в Авито.
В статье расскажу, как мы строим ML-платформу на базе Kubeflow. От первых DevBox-решений мы пришли к набору небольших юнит-платформ, которые разные команды развивали под свои бизнес-задачи и связывали между собой. Со временем возникла задача объединить эти решения в единую платформу. Поделюсь, как мы это делали, с какими проблемами столкнулись и как их решили. И немного о том, как должны выглядеть агентские платформы, когда за управление инфраструктурой отвечают агенты.

Статья будет полезна не только тем, кто разрабатывает и использует платформы в больших компаниях, но и тем, кто работает на DevBox-машинах или небольших платформах для юнит-команд от 10 до 100 человек.
Немного про себя: я работаю над ML-платформой на базе Kubeflow и занимаюсь разработкой inference-платформы на базе KServe. Люблю делиться знаниями про ML-инфраструктуру, выступаю программным экспертом курса Яндекс Практикума по MLOps и пишу в Telegram-канал про инфраструктуру для AI/ML.
Содержание

Три архитектурных подхода
Начнём с небольшой базы — трёх архитектурных подходов к построению ML-инфраструктуры, которые ребята из западных компаний, таких как Netflix (Metaflow), Uber (Michelangelo) и Google (Vertex AI), показали несколько лет назад.
→ Можно разрабатывать SDK для дата-сайентистов, которые помогают увеличивать воспроизводимость экспериментов при работе с DevBox-решениями.
→ Можно построить on-premise ML-платформу на собственном кластере, как это сделали ребята из Uber.
→ Либо, как ребята из Google, сделать полноценную платформу и выдать её как SaaS-решение для других компаний — облачная ML-инфраструктура как сервис.

При построении этих платформ компании смотрят на две метрики, которые напрямую влияют на бизнес-результат:
Time-to-market — время от создания эксперимента до вывода модели в прод. Наша задача с помощью платформизации уменьшать его любыми способами. Можно сокращать время онбординга клиентов, ускорять воспроизведение экспериментов, которые дата-сайентисты уже делали раньше.
GPU Utilization — максимизация утилизации GPU-кластеров. Нужно превратить разрозненное пространство, где у каждого своя виртуалка с GPU и она недоутилизирована, в центральное внутреннее облако компании. (Зачем это нужно — объясню чуть позже.)
Расскажу, как мы проходим этот путь в Авито ↓
DevBox Platform

DevBox-решение — это лучшая ML-платформа для дата-сайентиста, но худшая для компании. Вы получаете в своё распоряжение выделенную вычислительную машину с GPU и работаете с ней как с обычным удалённым сервером через SSH. Как клиент вы получаете огромные плюсы: максимальную скорость старта и кажущийся минимальный time-to-market, потому что вы чувствуете себя на большой машине с жирной GPU и можете делать всё что угодно. Например, настраивать окружение как хотите — без зависимости от внешних интеграций.
Но по мере роста числа людей на платформе появляются ловушки — борьба за утилизацию ресурсов. Один дата-сайентист занимает GPU, другому нужно обучить модель — ресурс уже занят. Нет нормального observability: непонятно, кто и как пользуется видеокартами, железо простаивает. Каждая команда при росте числа людей заново заказывает железо под свои потребности.
После этого этапа вы переходите в мир юнит-платформ.
Unit Platform

Представьте, команда выросла до нескольких десятков или даже сотни человек и стала замечать, что все делают одинаковые эксперименты, а конкуренция за GPU и проблемы с утилизацией ресурсов становятся всё заметнее. Нужно увеличить воспроизводимость экспериментов, а значит, логичный шаг — взять какое-нибудь open-source решение. И команды начинают пилить свои велосипеды под конкретные бизнес-задачи: кому-то нужен фича-стор, кому-то — продвинутый эксперимент-трекинг. Наша команда начинала так же.
Мы выбирали из нескольких open-source проектов как ядро платформы: Airflow, ClearML и Kubeflow — и выбрали Kubeflow, потому что его можно эффективно дорабатывать для интеграции в контекст компании. Kubeflow — микросервисное приложение, состоит из множества плагинов под конкретные компоненты ML-платформы, которые легко поддерживать. А ещё у него огромное комьюнити. Это CNCF-проект, который находится на постоянной поддержке коллег из open-source.

Для нас это был хороший выбор. При этом у нас была другая команда, которая пошла в другой трек и начала делать ML-платформу на основе Airflow. Оказалось, что бизнес-задачи изначально были немного разными. Мы позиционировали себя как платформу для воспроизведения пайплайнов, эксперимент-трекинга, хранения моделей в registry и их дальнейшей эксплуатации. Ребята, которые делали платформу на базе Airflow, больше подразумевали ETL-пайплайны и offline batch inference, поэтому им на тот момент больше подходил Airflow.
Но с ростом команд и других отделов, где появлялись дата-сайентисты и аналитики, возникал вопрос, какую платформу использовать. Каждый раз люди перебегали с одной на другую и держали в голове, как пользоваться двумя платформами одновременно. Ловушки понятные: нет централизованного хранилища железа, утилизацию не посмотреть нормально. У разрозненных команд с разными инструментами в каждой должен быть человек, который во всём этом разбирается. Закупки железа происходят в конкурентной основе, а онбординг новых людей затягивается, потому что им приходится сразу учить несколько инструментов.

Возникает мысль, что для дата-сайентистов, аналитиков и других клиентов нужна единая точка входа в ML-платформу, чтобы уменьшить time-to-market. Нужно сокращать время онбординга, чтобы ребята быстрее доказывали гипотезы, их было больше и больше моделей приносило деньги компании. Со временем появилась необходимость централизованно контролировать закупку и утилизацию железа, поэтому мы стали строить единую платформу, где можем управлять ресурсами и давать клиентам удобные инструменты, которые покрывают все бизнес-задачи.

Central Platform

У нас есть единая точка входа — Kubeflow Central Dashboard. Его также можно взять из open-source и разворачивать самостоятельно. По ссылке вы найдёте ванильные манифесты всей платформы, которую можно развернуть у себя и попробовать в работе.
Мы сначала так и сделали, а часть компонентов потом доработали под требования своих клиентов — об этом расскажу ниже. Основные компоненты:
IDE, где клиент будет разрабатывать свои модели, — JupyterLab;
Pipelines, где запускаются эксперименты и сравниваются между собой;
Model Registry, куда складывается модель, версионируется и дальше уже идёт в inference;
Отчёты, которые можно составлять на базе экспериментов;
Дистрибьютор — отдельный сервис, который отвечает за распределённое обучение на базе пайплайнов.

Главная сложность большой компании при внедрении такой платформы — не просто взять open-source и установить Helm-чарты в Kubernetes, а интегрировать платформу в контекст компании и подружить с сервисами, которыми уже привыкли пользоваться клиенты. Нужны интеграции:
SLA & o11y — платформа должна соответствовать корпоративным SLA и обеспечивать observability;
Team management — интеграция с LDAP и SSO, управление командами и ролями;
Secrets — безопасное получение секретов через Vault;
Infrastructure — свои особенности Kubernetes-кластеров, сетевые политики;
External Services — базы данных, PaaS, S3, Trino, Data Lake и другие внешние сервисы для деплоя и хранения данных;
А теперь — как мы решали проблемы с этими компонентами ↓
Team Management. Kubeflow позволяет дописывать плагины на Go — это Kubernetes-операторы. Мы написали плагин, который интегрируется с профилями Kubeflow, а профиль Kubeflow — это namespace в Kubernetes. Для каждой команды создаётся свой namespace, он же профиль Kubeflow, внутри которого для отдельных пользователей создаются личные namespace'ы и профили.
Управляется эта история через небольшой YAML-файл, где описывается конкретная команда и к ней привязываются пользователи — либо через LDAP-группы, либо через extra-user, если нужно, чтобы из другой команды человек посмотрел эксперименты конкретной команды. Добавиться можно через pull request — обычно это делается достаточно быстро и просто.
- name: teamA owner: teamleadA@avito.ru groups: - ldapGroupA extraUsers: - name: userA metadata: labels: inference.aviflow.io/enabled: "true" owner: userA@avito.ru contributor: edit - name: userB owner: userB@avito.ru contributor: view
В том же YAML-файле указываются конкретные ресурсы команды. (Подробнее про описание сущностей — в этом посте.) У нас есть несколько флейворов в Kubernetes-кластере — это сущность планировщика Kueue. Kueue позволяет умнее планировать различные ворклоуды для обучения моделей и инференса, чем нативный планировщик Kubernetes. Флейворы мы определяем через селекторы: CPU-машины или видеокарты разных типов. Для каждой команды можно задать гарантии на ресурсы в нашем мини-облаке. Если команда не приносит железо, она использует шаренные ресурсы, когда у других команд с гарантиями они простаивают.
- name: teamA queues: - name: default kind: kueue guarantees: - flavor: standart resources: {} - flavor: a100-80g prodResources: cpu: 10 memory: 25Gi nvidia.com/gpu: 1 resources: cpu: 30 memory: 75Gi nvidia.com/gpu: 1 - flavor: h100 resources: cpu: 30 memory: 75Gi nvidia.com/gpu: 1
Объясню, как это работает. Приходит задача на вход и попадает в очередь. Если есть свободные ресурсы, она их забирает и переезжает на ноду с нужным флейвором. Как только приходит задача от команды с гарантиями и в кластере этих ресурсов нет, старая задача вытесняется — у неё сохраняется чекпоинт. Команда с гарантиями продолжает своё обучение, а эксперимент предыдущей команды перезапустится в следующий раз.

Отмечу:
Очереди и flavor'ы не привязаны к конкретным node'ам, а оперируют агрегациями ресурсов (их общим количеством).
Задачи в prod-очереди не вытесняются и запускаются только на гарантированных ресурсах.
Ресурсы prod-очередей всегда меньше или равны гарантированным ресурсам когорты и ссылаются на flavor'ы когорты.
В Team Manager мы фиксируем ресурсы когорты и prod-очередей. Ресурсы не-prod (regular) вычисляются автоматически (их можно увидеть в спецификации когорты).
Из всех сущностей только LocalQueue является namespaced, остальные — cluster-wide.
Очередь можно настроить как FIFO/LIFO, так и на fair sharing.
Также в кластере включён TAS (Topology Aware Scheduling).

В терминах Kueue это работает через когорты и очереди. Есть общая когорта, которой принадлежат все ресурсы кластера. От неё наследуется конкретная когорта команды с её общими гарантиями.
По этим гарантиям выделяются две очереди: продовая и дефолтная.
В продовой очереди выделяются конкретные ресурсы, в рамках которых могут запускаться задачи команды. Если задача превышает эти лимиты, она просто не запустится. Это гарантийные ресурсы для прода, например для inference batch-пайплайнов.
В дефолтной ресурсы считаются как общая когорта минус продовая очередь. Если команда превышает свои гарантии, пайплайн всё равно запустится, если в кластере достаточных ресурсов. Но если другая команда заберёт свои гарантии, задача будет вытеснена.
Kueue даёт хорошие метрики для просмотра в Grafana: видно, как утилизируются флейворы в кластере, как конкретная команда утилизирует железо и сколько видеокарт она занимает при запуске обучения.

На графике видно, что одна из команд забрала больше ресурсов, чем у неё было в гарантиях, и тем самым они смогли использовать, так сказать, фичу облака внутри компании:

Service Mesh. Мы не стали изобретать велосипед, поэтому наша сеть построена на Istio. Kubeflow нативно интегрирован с ним, а Istio уже был стандартом для платформ в Авито. У нас стоит Ingress Gateway Controller, через который проходит трафик пользователей. А Authorization Policy и mTLS-сертификаты обеспечивают безопасность.
Нужно помнить, что когда вы ставите Istio и особенно запускаете его в sidecar-режиме, на каждый компонент вашей платформы запускается Envoy, внутри которого содержится информация обо всех сервисах в кластере. Если кластером пользуются тысячи людей и у вас больше трех тысяч namespace'ов, как у нас, сервисов тоже становится очень много, и каждый Envoy потребляет достаточно много памяти. Есть сущность Sidecar, которая позволяет определить перечень конкретных сервисов, которые нужно видеть этому Envoy, — это помогло нам сохранить терабайт оперативной памяти на нодах. Советую всем использовать.

IDE. После того как пользователь зашёл в нашу платформу, он обычно открывает IDE в виде JupyterLab. Старая версия IDE, которая нативно встроена в Kubeflow, просто запускает JupyterLab как под, который занимает те ресурсы, которые вы указали. Если запускаете на GPU — у вас поднимается JupyterLab на GPU, и если вы его не используете, GPU простаивает, что не очень хорошо. Плюс там нельзя запустить задачи по расписанию, что тоже нам не подходило. Поэтому мы сделали решение, которое позволило отделить фронтенд от бэкенда JupyterLab.

В open-source есть похожая концепция — Remote Kernel в JupyterLab. Здесь у нас есть отдельный фронтенд, где пользователи могут работать как в обычном JupyterLab, иметь помощника в виде AI-ассистента, который подскажет, как правильно написать запрос. Ещё там есть интеграция с нашим Vault, чтобы не вводить креды каждый раз, и подключение к различным базам данных, откуда берутся датасеты. Каждая ячейка при запуске поднимает отдельный кернел в кластере, который подтягивает необходимые ресурсы. Если кернел не используется около часа, ресурс автоматически освобождается — в том числе GPU. Это позволяет нам утилизировать железо по максимуму.

Pipelines. Из IDE пользователь может запускать более сложные вещи — пайплайны. Что они дают? В Kubeflow есть отдельный компонент для их запуска, он же эксперимент-трекинг. Запускаешь определённый набор ранов — это обычные коды, которые могут выполняться параллельно или последовательно, и тем самым порождаешь эксперимент.
Но нативные пайплайны от Kubeflow у нас вызывали вопросы. При начальном внедрении был плохой observability. Когда падали поды пайплайнов, пользователи об этом даже не знали — не было информации о том, какая ошибка. Ходили к дежурным, отвлекали их на первой линии, разработка из-за этого немного задерживалась. Мы улучшили observability в пайплайнах, форкнули сервис, запатчили и отправили pull request в комьюнити. Из других значимых пул реквестов, которые мы вносили, — оптимизация запросов в MySQL-базу. Пайплайны используют именно MySQL, мы сделали так, чтобы запросов в базу было меньше, а коды работали быстрее.

Один из самых значимых pull request'ов в Kubeflow-комьюнити сделал мой коллега Антон Печенин. Когда запускаются пайплайны, в Kubernetes создаётся сущность Argo Workflow, в спеке которой описаны конкретные шаги пайплайна. На каждый такой workflow изначально создавались поды в паре с так называемым драйвером — отдельным кодом, который смотрит, был ли уже проведён этот эксперимент. Если делишь пайплайн на 10 разных ранов и меняешь гиперпараметры только в одном, а предыдущие сохраняются — зачем их запускать заново? Поэтому запускались поды, которые смотрели, есть ли эксперимент в кэше — по паре на каждый ран. Антон сделал отдельного агента, который сам следит за всеми ранами, и сократил количество запусков подов в n/2+1 раз. Pull request'ы можно посмотреть по ссылкам PR1 и PR2 — они уже прорабатываются в комьюнити.

Доставка секретов. Отдельной задачей была интеграция с Vault. Есть несколько подходов:
Vault Operator — в кластер ставится оператор, который создаёт секреты из Vault.
CSI-драйвер — запускает DaemonSet'ы на каждой ноде и пробрасывает эти секреты в подики. Это не очень безопасный вариант, потому что как администратор кластера я всё равно смогу посмотреть эти секреты.
Vault Injection — внутрь пода запускается Vault Env контейнер, он берёт токен Kubernetes Service Account, идёт в Vault, получает секреты и передаёт их как переменные окружения, которые не описаны в спеке Kubernetes. Посмотреть эти секреты можно только внутри процесса. Это самый безопасный вариант, даже админ кластера не доберётся.
Почитать подробнее можно вот в этой статье.
Распределённое обучение. Первая версия распределённого обучения — это запускать на DevBox'ах Docker-раны с пробросом MPI, что было не очень эффективно и требовало от дата-сайентистов инженерных навыков. А мы хотим делать ML-платформу для пользователей, которые не будут работать с инфраструктурой и максимально сфокусируются на своих data science-задачах.
В первой версии взяли Training Operator, который также разрабатывается в рамках Kubeflow, и обернули его пайплайнами. Пайплайны у нас уже есть. Внутри них запускаются задачи, которые порождают MPI Job, и она запускает распределённое обучение. Сейчас используем Trainer. Он пришёл на смену Training Operator в переработанном виде — с новыми CRD и другой архитектурой. Он позволяет запускать собственные сущности, такие как TrainJob.
А в чём разница с запуском пайплайнов? Когда запускаешь пайплайн, это просто поды, которые последовательно друг за другом запускаются и идут в execute. В распределённом обучении нужно продумывать механизмы gang scheduling: запускаешь сразу несколько подов, и только если в кластере хватает общих ресурсов, задача будет запущена — чтобы не было простоя. Плюс между джобами должна быть связь, некий интерконнект, потому что видеокарты должны лежать на одной шине и гонять данные между собой по InfiniBand, без узких мест.
Эксперимент-трекинг. Нативная версия эксперимент-трекинга в Kubeflow — и ничего не понятно. Наши дата-сайентисты стали использовать ClearML, MLflow и другие инструменты, у которых есть свои минусы, но зато эксперимент-трекинг выглядит хорошо.

Мы немного переработали интерфейс, чтобы можно было на одной страничке выделить два рана и быстро сравнить их между собой. Тем самым тоже уменьшая time-to-market — чтобы ребята могли доказывать больше гипотез.

Model Registry и Model Delivery. После экспериментов модель нужно где-то хранить. Для этого у нас свой Model Registry. У Kubeflow тоже есть Model Registry, можно обратить на него внимание. В нашем случае в первой версии это было монолитное решение — Registry и Delivery одновременно, где в S3 хранились модели, а в MongoDB метаданные. Но по производительности нам это не подошло: слишком много запросов, Mongo не справлялась. Переделали на отдельный сервис Model Registry, который также сохраняет модели в S3, а метаданные в MySQL. Сохраняет в S3 в любой структуре — можно класть и ML-модели, и папки в структуре для Triton Inference Server, и LLM-ки с Hugging Face. И при этом всё версионируется.

Model Delivery-сервис позволяет in-place заменять модели в инференсах. Для дата-сайентиста это выглядит так: задеплоил модель в Model Registry, нажал кнопку «Доставить», и новая модель улетела в уже развёрнутый инференс и подтянулась без перезагрузки.
Хочу показать наши метрики платформы. У нас уже несколько тысяч человек и очень много активных пользователей. При таком росте начинаешь понимать, что просто open-source решение из коробки может и не подойти.

Мы стараемся разрабатывать продукт под эти требования и, где получается, контрибьютить в open-source. Ещё активно запускаем пайплайны: уже около 70 тысяч ранов в месяц и до 3 тысяч запусков распределённого обучения.


Inference Platform

Когда ML-платформа готова, оказывается, что забыли инференс-платформу. И все возвращаются назад, в эру DevBox'ов: инференсы запускают на обычных виртуалках. Я проводил опрос, в котором выяснял тенденцию и спрашивал: «Где запускается инференс?» Большинство проголосовали за Docker Compose на VM. В платформенных решениях вроде RayServe процентов набралось немного.

И возникает концептуальный вопрос: а где вообще организовывать инференс? Допустим, у вас есть своя уже существующая система, где крутятся веб-сервисы с бизнес-логикой. Стоит ли туда переводить инференс или лучше сделать отдельную платформу на базе open-source решения, чтобы комьюнити помогало продвигать дополнительные фичи, которых нет в существующем PaaS? Это достаточно дискуссионный вопрос. Мы выбрали путь проработки собственной инференс-платформы на базе KServe.
А почему мы выбрали KServe? Он умеет инференс LLM и ML в одном месте. Это очень удобно. Причём не просто запуская поды, а организует отдельные сущности под inference LLM.

Зачем это нужно? У ML-моделей и LLM разные требования. LLM inference должен обеспечить большой трафик на одну модель: можно реплицировать её на разные стадии — prefill и decode, скейлить эти стадии независимо, обеспечить distributed KV-cache. В ML-моделях важнее быстро обновлять модели, то есть нужна интеграция с Model Delivery.
KServe — отличное решение, если хочешь инференсировать всё в одном кластере.
Agentic Platform

Следующий этап — агентские платформы.
Агенты тоже снижают time-to-market. Задача агентской платформы — сделать так, чтобы дата-сайентисты и аналитики вообще не думали, как что-то запускать на платформе. Им не нужен UI — за них это сделает агент. Он прочитает инструкцию через MCP-сервер из Confluence, разберётся, как правильно запускать пайплайны, а вы просто принесёте ему ТЗ, какие гипотезы нужно рассмотреть. Поэтому важно делать хорошие API платформы, к которым смогут подключаться агенты.

Мы пока разрабатываем только MVP таких MCP-серверов, но одна команда уже сделала MCP-обёртку, запускала через неё несколько пайплайнов и доказала 50 гипотез за три дня — для трёх дней довольно много.

Какой подход выбрать
DevBox и юнит-платформа — это не есть плохое решение, а скорее конкретная стадия платформизации. DevBox отлично подходит для команды до 10 человек — центральная платформа там не нужна. Но с ростом числа клиентов нужно обеспечивать высокую утилизацию железа, чтобы команды не заказывали лишние ресурсы, которые можно сложить в один кластер и получить внутреннее облако с нормальной утилизацией видеокарт. И нужен time-to-market: платформа должна быть такого вида, чтобы клиенты вообще не думали про онбординг и могли быстро производить эксперименты.
Держите табличку, которая поможет сделать выбор:

Короче
В заключении несколько главных мыслей из статьи:
Платформа важнее модели. Без платформы (в каком бы виде она ни была) ML не даст ни хорошего качества, ни приемлемой стоимости. И тут нужно помнить про проблемы, так называемого, второго дня. Первый день запустил LLM-ку, дал ей запрос — работает, всё хорошо. На второй день нужно обеспечить надёжность, observability, высокий RPS. Вот тут и начинаются проблемы, которые как раз могут решить платформы.
Нет серебряной пули. DevBox → Unit → Central → Agentic, но путь у каждого свой.
Метрики time-to-market и утилизация GPU решают всё. Остальное сводится к ним.
Инференс — следующий большой сдвиг. Недаром Дженсен Хуанг говорит, что инференс — это про то, как ты думаешь, а обучение — про то, как ты читаешь инструкции. Думать намного сложнее.
Агенты меняют правила. DS занимается наукой, агент — инфраструктурой. Агенты придут и начнут пользоваться API платформы, а вы будете раздавать им задачи как системные аналитики.
Что почитать и посмотреть
→ Три подхода к построению MLOps-платформы
Когда мы проектировали курс по MLOps для Яндекс Практикума, разобрали три варианта архитектуры MLOps-платформ на примерах известных западных компаний.
→ Kubeflow: профили и enterprise-расширение
Здесь можно посмотреть, как устроены профили в Kubeflow, и заглянуть в пример enterprise-плагина.
Здесь разобраны очереди задач, квоты, fair sharing и базовые принципы платформенного планирования ML-нагрузки в Kubernetes.
Если используете Istio как service mesh, вам к настройкам Sidecar. Так вы сможете сократить нагрузку на Envoy и сэкономить оперативную память.
→ Распределённое обучение в Kubeflow
Если делаете distributed training в Kubeflow, посмотрите на trainer-operator. Trainer-operator — одна из базовых точек входа в платформенный слой для distributed training в Kubeflow.
Здесь хорошо разобраны варианты интеграции Kubernetes-кластера с корпоративным Vault.
Если смотреть на то, как сегодня выглядит LLM inference в KServe, то вот хорошая точка входа в LLMInferenceService и текущую модель generative inference в платформе.
→ Таймлайн про MLOps до 2035 года
И оставлю доклад с размышлениями, куда вообще движутся MLOps-платформы и почему со временем платформенный слой будет появляться практически везде.
Делитесь своим опытом и задавайте вопросы в комментариях!
Кстати, если вам интересно не только как устроена ML-инфраструктура в компаниях, но и каково в них работать — Хабр совместно с ЭКОПСИ проводит большое исследование IT-брендов работодателей. В прошлом году в нём поучаствовали 34 000 специалистов. Если у вас есть опыт — он точно будет учтён.

