Всем привет! Меня зовут Роза и я MLOps-инженер в Купере. Пока одни учат модели, а другие пытаются их запустить, наша команда строит «мост» между этими мирами — и сегодня под катом расскажу, как мы создавали нашу ML-платформу: от тренировочных стендов до продакшн-инференса, который не падает в пятницу вечером.

Отдельное внимание мы уделим тому, как выстраивать взаимодействие между разными стейкхолдерами платформы — от собственно ML-инженеров до DataOps и Security-инженеров. 

Идеальный путь построения ML-сервиса

"Идеальный" пусть построения ML-сервиса от кода до инференс-сервиса
"Идеальный" пусть построения ML-сервиса от кода до инференс-сервиса

Представим идеальный путь, который ML-инженер проходит при выводе модели с нуля в полноценный продакшен ML-сервис с помощью стандартных MLOps-практик.

Сначала у ML-инженера есть какой-то код, который нужно превратить в ML-сервис. 

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

На этом этапе мы обращаемся к хранилищам данных — Data Platform — и забираем, как правило, большие исторические датасеты из Offline Feature Store. Затем обучаем модель и сохраняем её в хранилище моделей — Model Registry.

Далее ML-инженер создаёт инференс-сервис, который подхватывает сохраненную модель и оборачивает ее в веб-сервис с ручками. На этом же этапе важен доступ к «горячим» данным — обычно это Online Feature Store, где хранятся, например, эмбеддинги пользователей. Их нужно получать очень быстро, поскольку для веб-сервиса критична задержка (latency). 

Этот «идеальный» путь хорошо ложится на принцип построения платформ — «кто написал код, тот его и поддерживает»: за весь процесс от идеи до готового продакшен-сервиса отвечает одна ML-команда.

Почему идеальная картинка не работает в реальности

В реальности этот путь спотыкается о различные препятствия.

*Ops-инженеры, которые помогают ML-щику на его пути
*Ops-инженеры, которые помогают ML-щику на его пути

Первая же проблема — это работа с данными. Как правило, DWH — это большие отдельные платформы со своими особенностями. По этой причине на стадии подготовки данных помимо ML-инженера в процессе начинает участвовать DataOps-инженер, который, например, помогает скачать большой датасет оптимально, без лишнего расхода ресурсов. А в случае с NRT-данными важно правильно настроить Feature Store, чтобы взаимодействие и доступ к данным были безболезненными.

С оркестратором возникает, наверное, самый болезненный этап, который нередко называют "продуктивизацией" — когда из большой пачки скриптов и Jupyter-ноутбуков собирается продакшен пайплайн. Здесь обычно задействованы разные инженеры: DevOps, MLOps, иногда бэкенд-разработчики.

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

И, конечно, при деплое сервисов в продакшен появляются security-инженеры, которым важно проверить появляющийся ML-сервис на соответствие требованиям ИБ. Но при этом, как правило, весь ML выглядит как «серая зона», где смешаны данные, ПДн, процессы и доступы и нет никакой прозрачности. 

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

Почему мы вообще считаем это проблемой? Дело в том, что когда падает какой-то из этих компонентов, призываются не ML-команды, а те инженеры, которые как раз сделали ту или иную интеграцию. И все грустят, потому что невозможно настроить нормальный инцидент-менеджмент. Ну, а security грустит, просто потому что все небезопасно. 

Причем необязательно этими инженерами должны быть отдельные люди, чаще даже бывает так, что все они консолидируются в том самом ML-инженере, который и строит сервис. По этой причине скорее считаем DataOps, MLOps, DevOps и SecOps ролями, которые могут принимать на себя те или иные инженеры.

Если ML-команд мало, то, скорее всего, эти проблемы бьют не так сильно. Однако, когда масштаб растет, растет количество команд, проектов, дагов и сервисов, то системным решением будет создание ML-платформы — набора унифицированных практик, которые облегчают жизнь командам на каждом из этапов. Давайте теперь посмотрим, что нужно от такой платформы всем нашим инженерам.

Стейкхолдеры ML-платформы

DevOps

Очень часто для DevOps-инженеров все задачи с ML-сервисам сводятся в общем-то к инфраструктуре и CI/CD. Как правило, задачи приходят в духе «поменять какие-то инфраструктурные компоненты у сервисов» или «оптимально использовать ресурсы». 

Так зачем здесь платформа? Дело в том, что когда есть, например, 100 сервисов, то чтобы поменять аффинити у подов, нужно зайти в каждый этот сервис и потрогать там манифест. На практике это превращается в большую организационную боль — как для самих DevOps-инженеров, так и для владельцев сервисов.

Вот какие задачи должна помогать решать платформа:

  • Надо поменять толерейшены/нодселекторы 

  • Надо деплоиться в другой K8S/облако/железо 

  • Надо правильно менеджерить docker-образы 

  • Надо оптимально использовать раннеры для сборки 

MLOps

MLOps-инженеры, как правило, хотят стандартизации: без «самодельных велосипедов», когда одна команда пилит свой feature store, другая — свой, и в итоге их десять, и непонятно, что с ними делать.

  • Нужна шаблонизация всех типов сервисов 

  • Надо перестать писать свои велосипеды 

  • Нужно стандартизированное взаимодействие с другими микросервисами 

DataOps

В случае с DataOps-инженерами все касается, естественно, данных. Нужно: 

  • линтить запросы SQL на корректность

  • стандартизировать передачу различных видов данных

  • организовать правильное хранение данных ML-сервиса внутри Data Platform и опубликовать их для всех data-пользователей

Security

Все, что небезопасно, нужно сделать безопасным и прозрачным. На практике это означает, что нужно добавить проверки и сканеры безопасности внутри каждого сервиса на уровне CI/CD. 

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

Начнем с DevOps-проблем — и посмотрим на «узкое место» с продуктивизацией.

Великий и могучий KubernetesPodOperator в Airflow

В качестве оркестратора мы выбрали Airflow, он позволяет запускать так называемые даги (DAG) по расписанию. Фактически даги — это пайплайны подготовки модели: подготовка данных, обучение модели и сохранение артефактов. 

Типичный даг выглядит так:

with DAG(dag_id = 'train_model_dag',
         schedule_interval='30 10 * * *', 
         ...
) as dag:
    def train_model(param=None, **kwargs):
        do_somethig()

    train_model_task = PythonOperator(
        task_id='train_model_task',
        python_callable=train_model
    )

Пользователь описывает свою бизнес-логику и оборачивает ее в таску с PythonOperator. Дальше Airflow запускает даг (последовательность тасок) по расписанию на своих воркерах. Одна таска == один под.

Когда мы описываем даги таким образом, мы смешиваем среду исполнения с исполняемым кодом,  то есть код бизнес-логики смешан с кодом Airflow. Это приводит к тому, что аналогично смешиваются зависимости, а также инфраструктура смешивается с бизнес-частью проекта. На практике это выливается в то, что сервис должен обязательно использовать ограничения окружения Airflow (файл constraints.txt), и почти невозможно изолированно менять инфраструктуру.

Чтобы решить эту проблему, будем использовать оператор KubernetesPodOperator, который выносит код бизнес-логики в отдельный под и запускает его изолированно от самой таски. Под с таской при этом просто следит за этим бизнес-подом — его статусами и ошибками.

KubernetesPodOperator помогает выносить код задачи в отдельный под
KubernetesPodOperator помогает выносить код задачи в отдельный под

Сам оператор мы при этом переопределим и сделаем свою «прослойку». Она нужна для большей гибкости и кастомизации запуска бизнес-пода.

# импортим оригинальный оператор
from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator

# определяем класс
class KuperK8sPodOperator(KubernetesPodOperator):
    def __init__(self, *args: t.Any, **kwargs: t.Any):
        . . .

# у него основной метод execute
def execute(self, context: Context) -> t.Any:
       . . .
       # метрики
       metrics.inc_dag_started_info(task_instance=task_instance, project=project)
       
       # Sentry
       sentry_dsn = self.get_sentry_dsn() 

       # переменные среды для всех дагов
       self.update_env_with_vars(
           "PROMETHEUS_PUSH_GATEWAY_DSN": settings.PROMETHEUS_DSN,
       . . .

В собственном операторе основной метод — execute(...). Именно в него можно добавлять различные интеграции — например, сбор метрик для всех дагов, инициализацию Sentry, проброс переменных, одинаковых для всех дагов, и так далее. 

Теперь давайте посмотрим, как выглядит использование оператора для пользователя, который пишет даг.

# описываем сам оператор
task = KuperK8sPodOperator(
  
    # метаинфа про саму таску типа task_id и прочее
  
    # имадж и CMD к нему
    image="hub/my_project/ml_automatching_encoder:1.23"
    cmds=["ml_automatching_encoder", "train"], 
    
    # лейблы
    labels={"uuid": str(uuid.uuid4()), 
            "project": "ml-content"}, 
    
    # ресурсы
    resources=custom_mem_resources(request_memory=50, limit_memory=50, request_cpu=4, limit_cpu=4), 

    # толера и селекторы
    tolerations=[
        k8s.V1Toleration(key=”node-role.kuper.tech/airflow-gpu”, operator="Exists", effect="NoSchedule"),
    ],
    node_selector={"node-role.kuper.tech/airflow": "ml-gpu"},
    . . .
)

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

Также оператор принимает спеку пода (толера, ресурсы, лейблы и так далее) — все то, что мы обычно прописываем, когда запускаем деплоймент в Kubernetes. Но откуда ML-инженер должен знать, например, аффинити для GPU-нод? Или pod template? Это не его зона ответственности. 

Чтобы упростить жизнь инженерам, упакуем все эти значения в переменные среды —тогда они будут проставляться на уровне самого Airflow, а забирать их будет уже наш оператор. Тогда пользователю надо указать только флажок «используй GPU» (или аналогичные):

from dags_sdk.operators.k8s import gpu_tolerations 

task = KuperK8sPodOperator(
    . . .
    tolerations=gpu_tolerations
    . . .
)

Давайте теперь посмотрим на то, как происходит работа с репозиториями:

Разделение зон ответственности между DevOps- и ML-командами
Разделение зон ответственности между DevOps- и ML-командами
  • За инфраструктурную часть деплоя кластера Airflow отвечают DevOps-команда. Она поддерживает чарты и зависимости Airflow, пишет CI/CD по разворачиванию кластера в Kubernetes.

  • Пользователи (все, кто пишет даги) владеют только своими сервисами, где собирается образ бизнес-пода с его зависимостями, который полностью независим и изолирован от Airflow.

Шаблон ML-сервиса

На этом этапе появляется логичный вопрос: если ML-инженеру надо собрать бизнес-код, то где и как это делать? Здесь появляется потенциальная «точка велосипедов» — каждая команда может организовывать и собирать код по-своему, и в итоге в компании образуется несколько решений одной и той же проблемы. 

Чтобы этого не допустить, нужна унификация. Ее можно добиться следующими шагами:

  • шаблонизация git-репозиториев сервисов

  • унификация CI/CD

  • своевременные обновления

Шаблонизация git-репозиториев

Задать структуру репозитория можно разными способами:

  • GitLab template или аналоги — механизм, когда проектируется буквально шаблон, из которого создаются новые репозитории

  • Cookiecutter — тулза для генерации репозиториев на основе jinja

  • свое решение

В Купере уже давно существует своя большая платформа для разработки микросервисов, в которой проблема шаблонизации решается собственным командным инструментом ecom-cli. Он позволяет создать репозиторий сервиса на основе git-шаблона (аналогично cookiecutter):

ecom-cli                    \
  service create            \
  https://.../paas/ml/demo  \
  --engine=airflow          \
  --version x.y.z

Сам git-шаблон зашивается в указание engine, он представляет собой обычный репозиторий, в котором есть т.н. плейсхолдеры — имя проекта, описание и так далее — то, что должно поменяться для конкретного сервиса. Соответственно, при создании сервиса эти плейсхолдеры заменяются — например, my_project меняется на реальное название проекта demo.

Как выглядит шаблон-репозиторий? Нужно два окружения:

  • с дагами — здесь окружение Airflow и даги, которые запускают KubernetesPodOperator;

  • с бизнес-кодом — здесь бизнес-окружение с нужными зависимостями, независимо и ничего не знает про Airflow, то, что запускается внутри KubernetesPodOperator.

.
├── .dockerignore
├── .DS_Store
├── .git
├── .gitignore
├── .gitlab
├── .gitlab-ci.yml
├── .idea
├── CODEOWNERS
├── configs
├── dags
├── docs
├── lint.toml
├── Makefile
├── project
└── scripts

Оба окружения — полноценные Python-окружения с Poetry. Пользователю остается описать только сам бизнес-код и даги, а остальное делает платформа.

Обновления сервисов на основе шаблона

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

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

Механизм накатки обновлений шаблона на существующие сервисы
Механизм накатки обновлений шаблона на существующие сервисы

Чтобы обновить сервис, мы смотрим версию шаблона, из которой он был сгенерирован или обновлен в последний раз (она прописывается в самом сервисе в отдельном файле). Затем берем diff между этой версией и последней версией шаблона. А дальше через git patch накатываем этот diff поверх сервиса. Таким образом, все новые коммиты в шаблоне просто накатываются поверх текущего сервиса. 

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

Добавляем к обновлениям миграции - скрипт с описанием изменений
Добавляем к обновлениям миграции - скрипт с описанием изменений

Фактически миграция представляет собой python-скрипт, в котором описан apply и rollback. В apply прописываются действия миграции (например, поменять название переменной среды в пользовательском файле), а в rollback — откат этого действия (например, вернуть старое значение). 

Когда происходит обновление, теперь мы не просто накатываем весь diff сразу, а делаем это «покоммитно» — накатываем коммит и его миграцию (если она есть). С одной стороны, это удобно, потому что можно гранулярно описать, что делать с каждым файлом, с другой — занимает время, так как помимо релиза шаблона нужно сделать еще и миграцию.

Также важно оставить места в платформенных файлах, которые позволяют пользователям кастомизировать репозиторий (помимо бизнес-кода и дагов):

1) в Makefile можно добавлять собственные таргеты через дополнительный файл make-usr.mk:

.PHONY: user-def-target 
user-def-target:
    echo "var from . env file: ${TEST_VAR}"

2) Аналогично в .gitlab-ci.yml можно добавить собственные джобы через инклуд пользовательского файла .gitlab-ci-usr.yml:

include:
   - local: '.gitlab-ci-usr.yaml'

Это особенно бывает полезным, когда команда, например, хочет запускать собственные линтеры в репозитории.

CI/CD ML-сервиса

Это, наверное, одна из самых важных частей сервиса, так как с CI/CD разработчики сталкиваются ежедневно. 

Так как у нас теперь есть шаблон и есть обновления, то в него легко внедрить CI/CD и предотвратить создание собственных велосипедов. Теперь CI/CD можно отдать в ответственность DevOps-команде — они могут разрабатывать его согласно собственным best practices и не бояться, что его кто-то сломает извне. Также благодаря механизму обновлений, можно легко раскатывать новые фичи на все сервисы на шаблоне.

Мы добавили в CI/CD пайплайны «плюшки», с помощью которых можно мотивировать пользователей вообще перейти на платформу и шаблон. Вот они: 

  • сборка, линтеры, тестирование

  • кеширование зависимостей в Python+poetry

  • скип сборки, если код не менялся

  • автоверсионирование сервиса

  • автоверсионирование дагов

Более того — можно добавить в шаблон удобные инструменты для локальной работы. Например, таргеты в Makefile, которые поднимают локальный minikube, Airflow и импортят в него даги или запускают бизнес-код в docker-контейнере. 

Организация репозиториев

В Airflow часто используют большие монорепозитории, в которых в кучу свалены все даги всех проектов команд. О том, как мы распилили наш монорепозиторий в Купере, можно посмотреть здесь

В ML-платформе один сервис — это изолированная бизнес-единица. Как правило, команды сами решают, как бить свои проекты и даги на сервисы. В итоге получается много ML-сервисов с дагами и все еще один кластер Airflow. 

Схема того, как много пользовательских сервисов деплоятся в один кластер Airflow
Схема того, как много пользовательских сервисов деплоятся в один кластер Airflow

Для деплоя дага в Airflow нужно лишь скопировать его скрипт в т. н. директорию dags_folder в хранилище дагов внутри пода Airflow шедулера. В качестве хранилища мы используем S3 бакет. И если в случае монорепозитория копировалась вся директория с дагами из этого репозитория, то в случае наших микросервисов копироваться будет только директория с дагами конкретного сервиса. 

Таким образом, изоляция дагов раньше была на уровне директорий проектов в монорепозитории, теперь она будет на уровне директорий в S3 бакете. Для Airflow ничего не меняется: он как обрабатывал S3 и находил даги, так и продолжает — только теперь сервисы разведены по папкам.

Вернемся к стейкхолдерам

К этому моменту мы закрыли потребности двух стейкхолдеров — MLOps (стандартизация) и DevOps (разделение ответственности и CI/CD). 

Вернемся еще раз к Security-инженерам. Поскольку у нас есть CI/CD и единая входная точка во все сервисы, эта точка становится входной и для ИБ. Теперь ИБ может легко добавлять свои security-гейты в наш CI/CD — отдельными пайплайнами или напрямую в существующие. Преимущество в том, что всё автоматически раскатывается на все сервисы: не нужно проходить по командам и просить каждую вручную добавлять проверки.

Интеграция security гейтов во все пайплайны сервисов
Интеграция security гейтов во все пайплайны сервисов

Для работы с данными мы внедрили dbt — инструмент для управления SQL-запросами. Фактически из SQL-запроса получается DBT-модель, а из модели — Airflow даг. Внутри шаблона есть отдельная директория с dbt-проектом и в CI/CD — отдельная джоба для генерации дага из dbt-модели.

Также остается проблема записи данных самим сервисом. В шаблон мы добавили возможность поднимать собственную базу данных, как у обычных микросервисов (не путать с базой, которую использует сам Airflow).

Эта база поднимается для каждого сервиса (в нашем случае автоматически на стейджах и черех архкомитет на продакшене) и CI/CD автоматически подставляет все нужные креды в поды сервиса. Также уже в шаблоне подготовлен клиент для записи данных. Более того, эту БД можно подключить к дата-платформе (например, к федерации в Trino), и не только ML-щики могут использовать эти данные, а, например, еще аналитики (забирать те же эмбеддинги и использовать их в аналитических витринах).

Схема публикации ML-данных внутри дата-платформы
Схема публикации ML-данных внутри дата-платформы

А что насчет инференс-сервисов?

До этого момента мы говорили только про сервисы с дагами для подготовки данных и обучения моделей. Но помимо офлайн-пайплайнов есть еще инференс-сервисы — веб-сервисы, которые эксплуатируют ML-модели в бою. 

На самом деле после подготовки и адопшена шаблона для дагов, сделать всё то же самое для инференс-сервисов оказалось несложно. 

  • Использовали те же компоненты и блоки CI/CD для сборки и деплоя.

  • Подготовили второй шаблон, аналогичный первому, с новым engine для ecom-cli.

  • Механизм обновлений тот же, но теперь работает для обоих шаблонов.

  • Алерты, SLO, работа БД — прикрутили те же фичи.

Отличие шаблона инференс-сервиса состоит в том, что в нем одно окружение, а не два. Это логично, так как это веб-сервис и для него нужно собрать один образ.

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

Часто бывает так, что ML-сервисы — это такая «серая» зона, в которой все крутится по своим порядкам. С одной стороны, понятно, почему так происходит — не всегда есть стандарты разработки ML-сервисов аналогично тем же микросервисам, и поэтому они развиваются хаотично. С другой стороны, это приводит к тому, что все процессы и регламенты для микросервисов перестают работать или не адаптированы для ML-сервисов. И от этого страдают все — как сами ML-щики (им приходится самостоятельно «протаптывать» все процессы), так и стейкхолдеры (им приходится много времени тратить, чтобы разобраться, что происходит).

Чтобы сделать «серую» зону прозрачной, мы уже предварительно подготовили почву — есть шаблоны, стандартизация CI/CD, два типа сервисов (даги и инференс), обновления. Но если раньше у нас был один монорепозиторий или у каждой команды была своя кучка репозиториев, то теперь мы их стандартизовали — и есть очень большая куча однотипных репозиториев. Всё еще непонятно, кто какими сервисами владеет, какая у них критичность и как они связаны между собой и внешним миром.

Чтобы решить эту проблему, мы воспользовались инструментами большого PaaS в Купере, а именно карточкой сервиса и картой сервисов. 

Что такое карточка сервиса

Карточка сервиса — это простой toml-файл определенной структуры. В нем описывается метаинформация о сервисе, его взаимодействиях с другими сервисами и инфраструктуре, которая ему нужна. Эта карточка формируется в момент генерации сервиса и заполняется вручную его владельцами.

# метаинфа о сервисе
name = "autocat-images"
description = "Offers autocategorization" 
engine = "airflow"
repository = "https://.../paas/ml/autocat-images"
type = "IT"
tier = 3

# метаинфа о команде
[team]
  owner_team = "ml-content"
  channel = "https://.../channels/dev-ml-content"
  maintainer_team = "devops-team"
  BO = "ivan.ivanych@kuper.ru"

# метаинфа об инфре
[clickhouse]
  required = false

[s3]
  required = true
. . .

# зависимости от других сервисов
[dependencies]   
    [[dependencies.services]]
        name = "storefront"
        open_api = ["admin"]
        repository = "https://.../paas/content/storefront"

Важной секцией являются зависимости — в ней мы указываем все сервисы, от которых зависит данный сервис. Причем эта связь на уровне контракта — нужно указать тип контракта (REST, gRPC, Kafka) и его имя.

Затем все эти карточки автоматически парсятся в большой платформе и публикуются в сервис Huginn. Здесь уже эта карточка отображается в красивом UI и строится карта всех сервисов Купера, включая наши сервисы:

Контракты

Для того, чтобы стандартизовать способы взаимодействия ML с внешним миром, добавим в оба типа сервиса контракты. 

Для Airflow-сервиса добавляем контракты чтения, например, для чтения из Kafka, а также контракты для походов в другие сервисы. Обратную связь не добавляем: DAG — не веб-сервис, из него по контракту, как правило, ничего не заберешь. 

Для инференс-сервиса, так как это веб-сервис, добавляем единый контракт ручек Open Inference Protocol как стандартный протокол выдачи предсказаний (его приняли многие MLOps-инструменты).

Помимо нормального взаимодействия между сервисами, появляется observability: все карточки также публикуются в CMDB, и в нем виден каждый сервис, контактные лица, инфраструктура (бакеты, БД, внешние зависимости). Это удобно при переездах и для автоматических реестров — сервис становится «видимым на радарах» компании.

Биг пикчер

Очень схематичная архитектура ML-платформы Купера
Очень схематичная архитектура ML-платформы Купера

Оркестратор — Airflow; offline-сервисы деплоим в кластер по нашим шаблонам. Артефакты и эксперименты — в ClearML (под капотом S3). Для inference-сервисов используем KServe и те же шаблоны деплоя в Kubernetes. С данными работаем через отдельную дата-платформу: для офлайн-хранилища — ClickHouse, Greenplum, Trino (федерация); для Online Feature Store — Feast на Redis

Выводы

  • При проектировании нужно сразу закладывать ML-инфраструктуру: прятать от ML-инженеров всё, что им не нужно трогать — в библиотеки, шаблоны, платформенные слои. 

  • Платформа должна ускорять процессы, а не усложнять их: новый процесс оправдан, если реально улучшает жизнь. 

  • Важно регулярно общаться с ML-инженерами (источник фич) и с другими стейкхолдерами — DevOps, ИБ, DataOps: воспринимать их требования как поводы для полезных улучшений.

  • Разделять среду исполнения и исполняемый код. 

  • Стандартизировать ML-сервисы там, где это ускоряет разработку и улучшает lead time. 

  • Применять best-practice микросервисной архитектуры в ML (адаптируя их под специфику), чтобы ML не оставался «хаотичной зоной». 

  • И разделять ответственность ролей: иногда один и тот же человек играет роли DataOps/MLOps/DevOps, но границы ответственности всё равно должны быть обозначены.

Комментарии (0)