Pull to refresh
72.55
Wunder Fund
Мы занимаемся высокочастотной торговлей на бирже

Организация ML-монорепозитория с помощью Pants

Level of difficultyMedium
Reading time24 min
Views1.6K
Original author: Michał Oleszak

Приходилось вам копипастить фрагменты вспомогательного кода между проектами, попадая в ситуацию, когда несколько версий одного и того же набора команд оказывались в разных репозиториях? Или, может, вам надо было делать pull‑запросы к десяткам проектов после того, как было изменено имя GCP‑корзины, где вы храните данные?

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

Предлагаю погрузиться в тему монорепозиториев. Это — архитектура, широко применяемая в ведущих технологических компаниях наподобие Google. Поговорим о том, как монорепозитории способны улучшить ваши рабочие процессы, связанные с машинным обучением. Монорепозитории дают тем, кто их выбирает, много полезного. Это, несмотря на то, что есть у них и недостатки, делает их привлекательным выбором для управления сложными ML‑экосистемами.

Сначала мы кратко обсудим сильные и слабые стороны монорепозиториев, поговорим о том, почему они — это отличное архитектурное решение для ML‑команд, коснёмся того, как их используют в крупных технологических компаниях. В итоге у нас появится представление о том, как воспользоваться возможностями системы сборки кода Pants для организации ML‑репозиториев при построении надёжной CI/CD‑системы для сборки проектов.

А теперь — в путь — к оптимизации управления проектами в сфере машинного обучения.

Что такое монорепозиторий?

What is ML monorepo? (short for monolithic repository)
Монорепозиторий для проектов из сферы машинного обучения

Монорепозиторий (сокращение от «монолитный репозиторий», monorepo, «monolithic repository») — это выражение стратегии разработки программного обеспечения, когда код множества проектов хранится в одном и том же репозитории. Идея монорепозиториев может иметь разные масштабы: от предельно широких, когда весь код некоей компании, написанный на разных языках программирования, хранится в одном месте (кто‑то сказал «Google»?), до совсем скромных, когда пару Python‑проектов, которыми занимается маленькая команда, складывают в один репозиторий.

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

Сравнение монорепозиториев и полирепозиториев

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

https://i0.wp.com/neptune.ai/wp-content/uploads/2023/08/Monorepos-vs.-polyrepos-1.png?ssl=1
Сравнение монорепозитория и полирепозитория. Для полирепозиториев характерны следующие проблемы: возможно наличие некоторого количества дублирующегося кода в разных репозиториях, для разных репозиториев могут применяться разные CI/CD-конвейеры, в разных репозиториях могут различаться стиль кода и подход к проверке его качества.

Среди сильных сторон монорепозиториев можно отметить следующие:

  • Единый CI/CD‑конвейер. Смысл тут в том, что это исключает распределение скрытых знаний о развёртывании проектов между отдельными разработчиками, делающими коммиты в различные репозитории.

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

  • Простые механизмы совместного использования кода. Речь идёт об использовании в разных проектах чего‑то вроде общего вспомогательного кода и неких шаблонов.

  • Упрощённая унификация стандартов и подходов к написанию кода.

  • Упрощение поиска нужных фрагментов кода.

Но, конечно, за всё надо платить. В частности, вот как приходится расплачиваться за вышеперечисленные полезности:

  • Необходимость решения непростых задач при масштабировании проектов. По мере роста кодовой базы управление монорепозиторием может становиться всё сложнее и сложнее. Если же речь идёт о по‑настоящему больших проектах, то для решения служебных задач, вроде клонирования кода, его загрузки из репозитория и выгрузки в него, понадобятся специальные инструменты и сервисы. Наладка и поддержание работы всего этого может потребовать немалых затрат времени и ресурсов.

  • Сложное устройство монорепозитория. Управлять монорепозиторием может оказаться сложнее, чем несколькими репозиториями. Особенно это касается управления зависимостями и версиями кода. Изменение в компоненте, рассчитанном на совместное использование, вполне может повлиять сразу на множество проектов. Для того чтобы избежать внесения в код изменений, способных нарушить работу чего‑либо, требуется применение особых мер предосторожности.

  • Проблемы с контролем доступа к коду и с ограничением области его видимости. Когда все работают в одном и том же репозитории, могут возникнуть сложности с управлением тем, у кого к чему имеется доступ. Хотя это, само по себе, и не тянет на однозначный «минус», такая ситуация может привести к возникновению проблем, связанных с законом, в случаях, когда речь идёт о коде, работа с которым ведётся под очень жёстким NDA.

Решение относительно того, стоят ли сильные стороны монорепозиториев той «платы», которая сопряжена с их использованием, принимается на уровне каждой организации или команды разработчиков. Но я, когда речь заходит о сфере моей специализации, о ML‑проектах, могу сказать, что монорепозиторий — это, в большинстве случаев, хороший выбор. Конечно, если только речь не идёт о реально огромных проектах или о каких‑то сверхсекретных разработках.

Поговорим о том, почему это так.

Работа над проектами машинного обучения с использованием монорепозиториев

Имеются по меньшей мере шесть причин того, что монорепозитории особенно хорошо подходят для ML‑проектов:

  1. Интеграция конвейеров обработки данных.

  2. Единообразие при проведении экспериментов.

  3. Упрощение версионирования моделей.

  4. Улучшение взаимодействия разных подразделений компаний.

  5. Атомарные изменения кода.

  6. Унификация стандартов кодирования.

Интеграция конвейеров обработки данных

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

Единообразие при проведении экспериментов

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

Упрощение версионирования моделей

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

Улучшение взаимодействия разных подразделений компаний

Проекты из сферы машинного обучения часто предусматривают взаимодействие между дата‑сайентистами, ML‑инженерами и программистами. Применение монорепозитория помогает взаимодействию между разными подразделениями компании за счёт того, что он представляет собой единый источник достоверных данных для всего кода и всех других ресурсов, имеющих отношение к проекту.

Атомарные изменения кода

Эффективность работы моделей, когда дело касается машинного обучения, может зависеть от множества взаимосвязанных факторов. Среди них — предварительная обработка данных, извлечение признаков, архитектура модели, постобработка данных. Монорепозиторий позволяет выполнять атомарные изменения всего, что имеет отношения к моделям. То есть — изменение множества компонентов проекта может быть выполнено за один раз, в одном коммите. Благодаря этому всё в проекте, что зависит друг от друга, всегда будет друг с другом согласовано.

Унификация стандартов кодирования

И наконец — в состав ML‑команд часто входят сотрудники, не имеющие опыта в сфере разработки ПО. Это — люди, которые хорошо разбираются в математике, статистике, эконометрике. Они умны, у них постоянно возникают интересные идеи, они умеют обучать модели, решающие проблемы бизнеса. Но в число их сильных сторон не входит написание чистого и читабельного кода, который легко поддерживать.

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

Опыт гигантов IT-индустрии: знаменитые репозитории

Монорепозитории используют некоторые из крупнейших и успешнейших компаний из сферы программного обеспечения. Вот несколько заметных примеров:

  • Google: компания Google уже давно является убеждённым сторонником монорепозиториев. Вся её кодовая база, а это, по некоторым оценкам, около 2 миллиардов строк кода, хранится в единственном огромном репозитории. Компания даже опубликовала статью об этом.

  • Meta: эта компания тоже использует монорепозиторий для хранения большей части своей кодовой базы. В компании создана система контроля версий Mercurial, ориентированная на поддержку её большого и сложного монорепозитория.

  • Twitter: эта компания уже давно поддерживает свой монорепозиторий с использованием Pants — системы сборки кода, о которой мы скоро поговорим!

Многие другие компании, в числе которых — Microsoft, Uber, Airbnb и Stripe, тоже используют монорепозитории, храня в них, как минимум, некоторые части своих кодовых баз.

Хватит теории! Поговорим о том, как создавать монорепозитории для решения задач машинного обучения. Это несколько сложнее, чем просто взять и сложить то, что раньше было разбито на разные репозитории, в одно место.

Как настроить ML-репозиторий для Python-проектов?

Здесь мы, в качестве примера, будем пользоваться ML‑репозиторием, который я создал специально для статьи. Это — простой монорепозиторий, в котором хранится всего один проект или модуль: классификатор рукописных цифр mnist, названный так в честь знаменитого набора данных, который он использует.

Прямо сейчас вам достаточно знать то, что в корне этого репозитория находится директория mnist, и то, что в ней находится некий Python‑код для обучения модели. Там же лежат модульные тесты и файл Dockerfile, позволяющий запускать обучение в контейнере.

ML monorepo: mnist directory
Структура репозитория

Мы будем пользоваться этим маленьким примером для того чтобы не усложнять повествование. Но учитывайте, что в более крупном монорепозитории mnist может представлять собой лишь одну из папок, находящихся в корневом разделе репозитория и хранящих данные проектов. Каждая из папок проектов будет содержать в себе, как минимум, файлы с исходным кодом, тесты, файлы Dockerfile и requirements.txt.

Система сборки проектов: зачем она нужна и как её выбрать?

Зачем нужна система сборки проектов?

Подумайте о тех действиях, за исключением написания кода, которые предпринимают разные команды, занимаясь обычной работой над проектами, хранящимися в монорепозитории. Они пропускают код через lintest, проверяя соблюдение стандартов по стилю программирования, они запускают модульные тесты, собирают артефакты вроде Docker‑контейнеров и WHL‑файлов, отправляют их во внешние репозитории и разворачивают их в продакшне.

Возьмём, для примера, тестирование.

Вы внесли изменение во вспомогательную функцию, поддержкой которой занимаетесь. Вы запустили тесты и оказалось, что всё в порядке. А как удостовериться в том, что ваши изменения не поломают код, написанный разработчиками из другой команды, в котором, возможно, импортируется ваша утилита? Для этого, понятно, вам надо выполнить и набор тестов, подготовленный другими разработчиками.

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

https://i0.wp.com/neptune.ai/wp-content/uploads/2023/08/2.png?ssl=1
Зачем нужна система сборки проектов: тестирование

Вот ещё пример: развёртывание проектов в продакшне.

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

Некоторые проекты из монорепозитория, возможно, не обновляются неделями. А, с другой стороны, код утилит, который в них используется, может обновляться чаще. Как решить — что именно собирать? Опять же, тут идёт разговор о зависимостях. В идеале сборке должны подвергаться только те сервисы, на которые повлияли последние изменения в коде, от которого они зависят.

https://i0.wp.com/neptune.ai/wp-content/uploads/2023/08/1.png?ssl=1
Зачем нужна система сборки проектов: развёртывание

Все эти задачи, если речь идёт о небольшой кодовой базе, можно решить с помощью простого shell‑скрипта. Но по мере того, как кодовая база растёт, по мере того, как какой‑то код начинают совместно использовать в разных проектах, ситуация усложняется. Сложности, в основном, связаны с управлением зависимостями.

Выбор подходящей системы сборки проектов

Если вложить время и силы в выбор и освоение подходящей системы для сборки проектов, то всё вышеперечисленное — это уже не проблема. Главная задача системы сборки проектов — собирать код. Делать это она должна с умом. Разработчику нужно лишь сообщить ей о том, что нужно сделать («собери Docker‑образы, на которые повлиял мой самый свежий коммит», или «выполни только те тесты, который покрывают код, использующий метод, код которого я обновил»). А то, как именно выполнить поручение, система должна решить сама.

Существует несколько отличных опенсорсных систем для сборки проектов. Так как большинство задач машинного обучения решают с применением Python, предлагаю сосредоточиться на тех системах, которые наилучшим образом поддерживают именно этот язык программирования. Два самых популярных варианта из этой сферы — Bazel и Pants.

Bazel — это опенсорсная версия внутренней системы сборки Google, которая называется Blaze. Pants — это система, разработчики которой сильно вдохновлялись Blaze. Она, правда, ориентирована на технические цели, схожие с теми, на которые ориентирован Bazel. Заинтересованный читатель найдёт хорошее сравнение Pants и Bazel в этом материале (только учитывайте, что материал этот написан разработчиками Pants). Таблица, которая находится в нижней части страницы monorepo.tools, содержит ещё одно сравнение этих систем.

Обе системы — это замечательные инструменты, и я не собираюсь заявлять, что одна из них должна носить звание «самой лучшей». С учётом этого скажу, что Pants часто называют системой, которую легче, чем другие, настраивать, системой более доступной и лучше оптимизированной для Python. Это делает Pants отличным выбором для монорепозиториев, используемых в машинном обучении.

Основываясь на личном опыте, могу сказать, что решающим фактором, который повлиял на то, что я выбрал именно Pants, стало активное сообщество этого проекта, всегда готовое прийти на помощь тем, кто в ней нуждается. Если у вас есть вопросы, или вы в чём‑то не уверены — просто черкните строчку в Slack‑канале сообщества и вам тут же помогут.

Знакомство с Pants

А теперь — самое интересное! Я приведу тут пошаговое руководство по Pants, знакомя вас с разными аспектами этой системы и с тем, как реализовать с её помощью различные механизмы поддержки проектов. Напомню, что вы можете заглянуть в репозиторий с примером, подготовленным для этой статьи.

Настройка

Pants можно установить с помощью pip. Здесь я использую самую свежую стабильную версию системы. Это, на момент написания текста, 2.15.1.

pip install pantsbuild.pants==2.15.1

Настройка Pants осуществляется посредством главного глобального конфигурационного файла, называемого pants.toml. В нём можно настраивать особенности поведения самого сборщика проектов Pants, а так же — тех инструментов, которые он использует, вроде pytest или mypy.

Начнём с минимального pants.toml:

[GLOBAL]
pants_version = "2.15.1"
backend_packages = [
    "pants.backend.python",
]

[source]
root_patterns = ["/"]

[python]
interpreter_constraints = ["==3.9.*"]

В разделе GLOBAL мы определяем версию Pants и бэкенд‑пакеты, которые нам нужны. Эти пакеты представляют собой движки Pants, поддерживающие различные возможности. Для начала мы включаем сюда лишь бэкенд для Python.

В разделе source мы указываем путь к корню репозитория. С версии 2.15, чтобы удостовериться в том, что система правильно восприняла эту настройку, в корень репозитория нужно ещё добавить пустой файл BUILD_ROOT.

И наконец — в разделе python мы указываем версию Python, которую хотим использовать. Pants просканирует систему в поисках версии, соответствующей условию, указанному в этом разделе. Поэтому, указывая тут некую версию Python, проверьте, чтобы она была у вас установлена.

Начало положено! Теперь перейдём к тому, что является сердцем любой системы сборки проектов — к файлам BUILD.

Файлы BUILD

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

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

В нашем примере используются три файла BUILD:

  • mnist/BUILD — в директории проекта. Этот файл определяет требования к Python, предъявляемые проектом, а так же описывает Docker‑контейнер, который нужно собрать.

  • mnist/src/BUILD — в директории с исходным кодом. Этот файл описывает файлы с исходным кодом, написанным на Python. Это — те файлы, которые нужно подвергнуть проверкам, ориентированным на Python.

  • mnist/tests/BUILD — в директории, где находятся тесты. Этот файл определяет то, какие файлы нужно запустить с использованием Pytest, и то, какие зависимости нужны для запуска соответствующих тестов.

Посмотрим на файл mnist/src/BUILD:

python_sources(
    name="python",
    resolve="mnist",
    sources=["**/*.py"],
)

А файл mnist/BUILD выглядит так:

python_requirements(
    name="reqs",
    source="requirements.txt",
    resolve="mnist",
)

Две записи, которые можно видеть в наших BUILD‑файлах, называют целями. Первая — это цель, соответствующая исходному коду на Python, ей мы, что вполне естественно, дали имя python, хотя это имя может быть любым. Файлы с исходным кодом на Python — это все.py‑файлы в директории. Используемая конструкция — **/*.py — это путь, построенный относительно места, где находится BUILD‑файл. Это ведёт к тому, что, даже если какие‑нибудь Python‑файлы будут находиться за пределами директории mnist/src, система будет собирать только те файлы, которые находятся в mnist/src. В файле есть ещё поле resolve, его роль мы обсудим ниже.

В другом файле имеется цель python_requirements. Она сообщает сборщику Pants о том, где ему найти то, что необходимо для выполнения Python‑кода (пути, опять же, указываются относительно расположения BUILD‑файла, который, в данном случае, находится в корневой директории проекта).

Это — всё, что нужно для начала работы. Для того чтобы убедиться в том, что определения, которые имеются в BUILD‑файлах, построены корректно — выполним следующую команду:

pants tailor --check update-build-files --check ::

Нам, как и ожидается, сообщают о том, что No required changes to BUILD files found. Система считает файлы корректными и не нуждающимися в правках. Отлично!

Присмотримся к этой команде. Если описать её в двух словах, то окажется, что простейшая конструкция вида pants tailor может автоматически создавать файлы BUILD. Но иногда она склонна добавлять в эти файлы слишком много такого, что тому, кто прибегает к её помощи, может и не понадобиться. Поэтому я обычно пишу такие файлы вручную, проверяя правильность их составления с помощью вышеприведённой команды.

Конструкция:: в конце команды указывает Pants на то, что команду нужно выполнить в применении ко всему монорепозиторию. Мы можем заменить эту конструкцию на mnist, что позволит обработать с её помощью лишь модуль mnist.

Зависимости и lock-файлы

Для организации эффективного управления зависимостями Pants использует lock‑файлы. В таких файлах записываются точные номера версий и источники всех зависимостей, используемых каждым из проектов. Сюда входят и прямые, и косвенные зависимости.

Фиксируя эту информацию, lock‑файлы обеспечивают то, что в разных окружениях и в разных сеансах сборки проекта стабильно будут использоваться одни и те же версии зависимостей. Другими словами, эти файлы играют роль «мгновенных снимков» графов зависимостей, обеспечивая воспроизводимость и единообразие сеансов сборки проекта.

Для того чтобы сгенерировать lock‑файл для модуля mnist, нужно добавить в pants.toml следующее:

[python]
interpreter_constraints = ["==3.9.*"]
enable_resolves = true
default_resolve = "mnist"

[python.resolves]
mnist = "mnist/mnist.lock"

Мы включаем флаг enable_resolves (окружения lock‑файлов в Pants называются «resolves») и задаём окружение для mnist, записывая в параметр default_resolve соответствующий путь. Это — окружение, которое мы выше, в BUILD‑файлах, указали, настраивая цели в python_sources и python_requirements. Именно это позволяет системе узнать о том, какие именно зависимости нужны для работы проекта. Теперь мы можем выполнить такую команду:

pants generate-lockfiles

В ответ получим следующее:

Completed: Generate lockfile for mnist
Wrote lockfile for the resolve `mnist` to mnist/mnist.lock

Система создала файл mnist/mnist.lock. Если планируется использовать Pants для организации работы удалённого CI/CD‑конвейера, этот файл нужно включить в состав Git‑репозитория. И, что вполне понятно, этот файл нужно обновлять каждый раз, когда обновляют файл requirements.txt.

При добавлении в монорепозиторий большего количества проектов lock‑файлы, скорее всего, имеет смысл создавать избирательно, для конкретного проекта, в котором они нужны, то есть — с использованием команды вида pants generate-lockfiles mnist:.

С первоначальной настройкой Pants мы справились! Теперь воспользуемся Pants для решения различных задач.

Унификация стиля кода с помощью Pants

Pants, изначально, поддерживает некоторое количество Python-линтеров и средств для форматирования кода. Среди них — Black, yapf, Docformatter, Autoflake, Flake8, isort, Pyupgrade, Bandit. Все они используются по одному и тому же принципу. В нашем примере подключим Black и Docformatter.

Для того чтобы это сделать — добавим соответствующие бэкенды в pants.toml:

[GLOBAL]
pants_version = "2.15.1"
colors = true
backend_packages = [
    "pants.backend.python",
    "pants.backend.python.lint.docformatter",
    "pants.backend.python.lint.black",
]

Оба инструмента, если нужно, можно настроить, добавляя в toml‑файл, ниже кода для их подключения, дополнительные разделы. Мы остановимся на параметрах, предлагаемых по умолчанию.

Для того чтобы воспользоваться средствами для форматирования кода, нам нужно выполнить то, что в Pants называется «целью». В данном случае нам подходят две цели.

Первая — это цель lint, обращение к которой приведёт к запуску обоих инструментов (в том порядке, в котором они перечислены в составе бэкенд‑пакетов, то есть — сначала Docformatter, а потом — Black) в режиме проверки кода.

pants lint ::

Completed: Format with docformatter - docformatter made no changes.
Completed: Format with Black - black made no changes.

✓ black succeeded.
✓ docformatter succeeded.

Похоже, текст наших программ придерживается стандартов, устраивающих оба средства для форматирования кода! Но если бы это было не так, мы могли бы прибегнуть к цели fmt (сокращение от «format»), которая привела бы код в надлежащий вид:

pants fmt ::

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

А если добавить в число обработчиков кода инструмент isort, автоматически сортирующий команды импорта, то у Black и isort будет конфликт. Дело в том, что isort не устраивают строки, длиннее 79 символов. Для того чтобы обеспечить совместимость isort и Black, понадобится включить в toml‑файл следующий раздел:

[isort]
args = [
    "-l=88",
 ]

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

Организация тестирования кода с помощью Pants

Запустим кое-какие тесты! Чтобы это сделать, нам нужно решить две задачи.

Для начала — в pants.toml надо добавить новые разделы:

[test]
output = "all"
report = false
use_coverage = true

[coverage-py]
global_report = true

[pytest]
args = ["-vv", "-s", "-W ignore::DeprecationWarning", "--no-header"]

Эти настройки обеспечат то, что после запуска тестов будет сформирован отчёт о покрытии кода тестами. Тут мы, кроме того, передаём системе пару интересующих нас параметров pytest, представляющих выходные данные в нужном нам виде.

Далее — надо вернуться к файлу mnist/tests/BUILD и добавить в него цель python_tests:

python_tests(
    name="tests",
    resolve="mnist",
    sources=["test_*.py"],
)

Мы дали этой цели имя tests и указали в её настройках то, какое окружение (resolve="mnist") нужно использовать. Поле sources задаёт место, куда pytest будет заглядывать в поиске файлов с тестами. Тут мы чётко указываем на все.py‑файлы с префиксом test_.

Теперь можно выполнить такую команду:

pants test ::

Вот что получится:

✓ mnist/tests/test_data.py:../tests succeeded in 3.83s.
✓ mnist/tests/test_model.py:../tests succeeded in 2.26s.

Name                               Stmts   Miss  Cover
------------------------------------------------------
__global_coverage__/no-op-exe.py       0      0   100%
mnist/src/data.py                     14      0   100%
mnist/src/model.py                    15      0   100%
mnist/tests/test_data.py              21      1    95%
mnist/tests/test_model.py             20      1    95%
------------------------------------------------------
TOTAL                                 70      2    97%

Как видите, на выполнение набора тестов ушло примерно три секунды. Теперь, если снова их запустить, результат будет выдан сразу же.

✓ mnist/tests/test_data.py:../tests succeeded in 3.83s (memoized).
✓ mnist/tests/test_model.py:../tests succeeded in 2.26s (memoized).

Обратите внимание: Pants сообщает о том, что эти результаты были мемоизированы, или кешированы. Так как не было внесено никаких изменений в тесты, в тестируемый код, или в requirements.txt, система понимает, что нет нужды снова запускать тесты, так как их результаты, гарантированно, будут теми же самыми. Поэтому результаты просто выдаются из кеша.

Проверка кода статическим анализатором типов с помощью Pants

Добавим в проект ещё одну проверку качества кода. Pants позволяет использовать статический анализатор типов mypy для проверки Python-кода. Для включения такой проверки достаточно добавить mypy в состав бэкендов в pants.toml. Выглядит это как pants.backend.python.typecheck.mypy.

Ещё может понадобиться настроить mypy, чтобы сделать выходные данные этого инструмента более информативными и удобными для восприятия. Для этого добавим в pants.toml соответствующий раздел с настройками mypy:

[mypy]
args = [
    "--ignore-missing-imports",
    "--local-partial-types",
    "--pretty",
    "--color-output",
    "--error-summary",
    "--show-error-codes",
    "--show-error-context",
]

Теперь можно воспользоваться командой pants check ::, которая выдаст следующее:

Completed: Typecheck using MyPy - mypy - mypy succeeded.
Success: no issues found in 6 source files

✓ mypy succeeded.

Развёртывание ML-моделей с помощью Pants

Поговорим о развёртывании проектов. Большинство проектов из сферы машинного обучения предусматривают использование хотя бы одного Docker‑контейнера. В число контейнеров могут входить такие, которые отвечают за обработку обучающих данных, за обучение модели, за организацию работы API для доступа к модели с использованием Flask или FastAPI. В нашем учебном проекте, кроме прочего, имеется контейнер для обучения модели.

Pants поддерживает автоматическую сборку и отправку Docker‑образов в репозиторий. Разберёмся с тем, как работают эти механизмы.

Для начала добавим бэкенд Docker в pants.toml с помощью pants.backend.docker. Мы, кроме того, настроим Docker, передав системе некоторое количество переменных окружения (env_vars), а так же — аргументы сборки (параметр build_args). Скоро нам всё это пригодится.

[docker]
build_args = ["SHORT_SHA"]
env_vars = ["DOCKER_CONFIG=%(env.HOME)s/.docker", "HOME", "USER", "PATH"]

Теперь, в файл mnist/BUILD, добавим ещё две цели: files и docker_image.

files(
    name="module_files",
    sources=["**/*"],
)

docker_image(
    name="train_mnist",
    dependencies=["mnist:module_files"],
    registries=["docker.io"],
    repository="michaloleszak/mnist",
    image_tags=["latest", "{build_args.SHORT_SHA}"],
)

Цели Docker мы дали имя train_mnist. В зависимостях у нас будет список файлов, которые нужно включить в контейнер. Самый удобный способ сформировать этот список заключается в том, чтобы оформить его в виде отдельной цели files. Тут мы просто включаем все файлы проекта mnist в цель module_files и передаём её в качестве зависимости цели docker_image.

Вполне естественно, что если известно о том, что для сборки контейнера понадобится лишь некое подмножество файлов, в состав зависимостей стоит включить только эти файлы. Это важно, так как эти зависимости используются Pants для того чтобы принять решение о том, подействуют ли на контейнер изменения неких файлов, и нужно ли будет его из‑за этого пересобирать. В нашем случае, когда в module_files включены все файлы, если любой файл в папке mnist изменится (даже readme!), Pants решит, что это повлияет на Docker‑образ train_mnist.

И наконец — мы можем указать внешний реестр или репозиторий, в который нужно отправить собранный образ. Можно указать и теги, с которыми будет отправлен этот образ. Тут я отправляю образ в мой личный репозиторий на Docker Hub, всегда назначая образу два тега — latest, и SHA коммита. Это делается благодаря соответствующей настройке параметра image_tags.

Теперь можно собрать образ. Осталось упомянуть лишь о том, что, так как сборщик Pants работает в собственных изолированных окружениях, он не может читать данные из env‑файла хоста. Поэтому, чтобы собрать или отправить в репозиторий образ, которому требуется значение SHORT_SHA, нужно передать это значение при вызове команды pants.

Соберём образ:

SHORT_SHA=$(git rev-parse --short HEAD) pants package mnist:train_mnist

Увидим следующее:

Completed: Building docker image docker.io/michaloleszak/mnist:latest +1 additional tag.
Built docker images: 
  * docker.io/michaloleszak/mnist:latest
  * docker.io/michaloleszak/mnist:0185754

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

docker images 

REPOSITORY            TAG       IMAGE ID       CREATED              SIZE
michaloleszak/mnist   0185754   d86dca9fb037   About a minute ago   3.71GB
michaloleszak/mnist   latest    d86dca9fb037   About a minute ago   3.71GB

Pants позволяет, кроме того, собирать и отправлять образы в репозиторий за один заход. Для этого нужно лишь заменить команду package на команду publish:

SHORT_SHA=$(git rev-parse --short HEAD) pants publish mnist:train_mnist

Тут осуществляется сборка образов и отправка их в мой репозиторий на Docker Hub, где они, в итоге, и оказываются.

Применение Pants в CI/CD-конвейерах

Те же команды, которые мы только что вручную выполняли на локальной машине, можно вызывать и при выполнении CI/CD‑конвейера. Запускать их можно с помощью сервисов наподобие GitHub Actions или Google CloudBuild. Делать это можно, например, при проверке PR, до того, как будет позволено добавить ветку с новым кодом в главную ветку, или после слияния веток, чтобы проверить, что перед сборкой и отправкой контейнеров всё работает как надо.

В нашем репозитории я реализовал pre‑commit хук, выполняемый перед отправкой кода, который запускает команды Pants при выполнении команды git push, и позволяет продолжить работу только в том случае, если всё работает правильно. При срабатывании хука выполняются следующие команды:

pants tailor --check update-build-files --check ::
pants lint ::
pants --changed-since=main --changed-dependees=transitive check
pants test ::

Тут, в команде pants check, вы можете видеть кое-какие новые флаги, в частности — отвечающие за проверку типизации с помощью mypy. Они позволяют обеспечить то, что проверка будет выполняться только для файлов, которые отличаются от тех, что находятся в главной ветке. Проверяться будут и косвенные зависимости этих файлов. Это — полезное изменение, так как на работу mypy уходит некоторое время. Ограничение объёма проверяемых файлов только теми, которые действительно нуждаются в проверке, ускоряет процесс.

Как в CI/CD-конвейере может выглядеть сборка и отправка в репозиторий образов Docker? Например — так:

pants --changed-since=HEAD^ --changed-dependees=transitive --filter-target-type=docker_image publish

Мы используем команду publish так же, как и раньше, но снабжаем её тремя дополнительными аргументами:

  • –changed-since=HEAD^ и –changed-dependees=transitive делают так, что собираться будут только те контейнеры, на которые повлияли изменения, внесённые в файлы после предыдущего коммита. Этот подход хорошо использовать в главной ветке репозитория после добавления в неё нового кода.

  • –filter-target-type=docker_image — благодаря этому аргументу единственное, что делает Pants при выполнении команды — это сборка и отправка Docker-контейнеров. Этот аргумент используется из-за того, что команда pants publish может быть направлена на цели, которые не имеют отношения к Docker. Например — её можно использовать для публикации пакетов Helm Charts в реестры OCI.

То же самое касается и команды pants package. Она, помимо сборки образов Docker, умеет создавать и Python-пакеты. Поэтому рекомендуется пользоваться опцией –filter-target-type.

Итоги

Монорепозитории — это архитектура, выбор которой ML‑командами чаще оказывается удачным решением, чем неудачным. Но, по мере роста таких репозиториев, управление ими требует вложений сил и времени в подходящую систему сборки проектов. Одна из таких систем — Pants. Её легко настроить, ей просто пользоваться, она имеет стандартную поддержку многих возможностей Python и Docker, тех возможностей, которые часто используются в машинном обучении.

Кроме того, Pants — это опенсорсный проект, вокруг которого собралось большое и дружелюбное сообщество. Надеюсь, что вы, после прочтения этой статьи, решите испытать Pants. Даже если у вас пока нет монолитного репозитория, сборщик кода Pants, всё равно, способен рационализировать многие ваши повседневные дела и помочь вам в их выполнении!

О, а приходите к нам работать? 🤗 💰

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

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

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде

Tags:
Hubs:
Total votes 12: ↑12 and ↓0+12
Comments1

Articles

Information

Website
wunderfund.io
Registered
Founded
Employees
11–30 employees
Location
Россия
Representative
xopxe