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

Предлагаю погрузиться в тему монорепозиториев. Это — архитектура, широко применяемая в ведущих технологических компаниях наподобие Google. Поговорим о том, как монорепозитории способны улучшить ваши рабочие процессы, связанные с машинным обучением. Монорепозитории дают тем, кто их выбирает, много полезного. Это, несмотря на то, что есть у них и недостатки, делает их привлекательным выбором для управления сложными ML‑экосистемами.
Сначала мы кратко обсудим сильные и слабые стороны монорепозиториев, поговорим о том, почему они — это отличное архитектурное решение для ML‑команд, коснёмся того, как их используют в крупных технологических компаниях. В итоге у нас появится представление о том, как воспользоваться возможностями системы сборки кода Pants для организации ML‑репозиториев при построении надёжной CI/CD‑системы для сборки проектов.
А теперь — в путь — к оптимизации управления проектами в сфере машинного обучения.
Что такое монорепозиторий?
Монорепозиторий (сокращение от «монолитный репозиторий», monorepo, «monolithic repository») — это выражение стратегии разработки программного обеспечения, когда код множества проектов хранится в одном и том же репозитории. Идея монорепозиториев может иметь разные масштабы: от предельно широких, когда весь код некоей компании, написанный на разных языках программирования, хранится в одном месте (кто‑то сказал «Google»?), до совсем скромных, когда пару Python‑проектов, которыми занимается маленькая команда, складывают в один репозиторий.
В этом материале мы сосредоточимся на репозиториях, в которых хранится код проектов из сферы машинного обучения.
Сравнение монорепозиториев и полирепозиториев
Подход к разработке, предусматривающий применение монорепозиториев, резко контрастирует с подходом, где применяются полирепозитории. В таких репозиториях каждому проекту или компоненту достаётся собственный отдельный репозиторий. Многое было сказано о преимуществах и недостатках обоих подходов. Мы слишком глубоко в эти дебри не полезем. Отметим лишь самое главное.
Среди сильных сторон монорепозиториев можно отметить следующие:
-
Единый CI/CD‑конвейер. Смысл тут в том, что это исключает распределение скрытых знаний о развёртывании проектов между отдельными разработчиками, делающими коммиты в различные репозитории.
-
Атомарные коммиты. Учитывая то, что все проекты находятся в одном и том же репозитории, разработчики могут вносить в код изменения, оказывающие влияние на несколько проектов, но при этом попадающие в репозиторий в виде единственного коммита.
-
Простые механизмы совместного использования кода. Речь идёт об использовании в разных проектах чего‑то вроде общего вспомогательного кода и неких шаблонов.
-
Упрощённая унификация стандартов и подходов к написанию кода.
-
Упрощение поиска нужных фрагментов кода.
Но, конечно, за всё надо платить. В частности, вот как приходится расплачиваться за вышеперечисленные полезности:
-
Необходимость решения непростых задач при масштабировании проектов. По мере роста кодовой базы управление монорепозиторием может становиться всё сложнее и сложнее. Если же речь идёт о по‑настоящему больших проектах, то для решения служебных задач, вроде клонирования кода, его загрузки из репозитория и выгрузки в него, понадобятся специальные инструменты и сервисы. Наладка и поддержание работы всего этого может потребовать немалых затрат времени и ресурсов.
-
Сложное устройство монорепозитория. Управлять монорепозиторием может оказаться сложнее, чем несколькими репозиториями. Особенно это касается управления зависимостями и версиями кода. Изменение в компоненте, рассчитанном на совместное использование, вполне может повлиять сразу на множество проектов. Для того чтобы избежать внесения в код изменений, способных нарушить работу чего‑либо, требуется применение особых мер предосторожности.
-
Проблемы с контролем доступа к коду и с ограничением области его видимости. Когда все работают в одном и том же репозитории, могут возникнуть сложности с управлением тем, у кого к чему имеется доступ. Хотя это, само по себе, и не тянет на однозначный «минус», такая ситуация может привести к возникновению проблем, связанных с законом, в случаях, когда речь идёт о коде, работа с которым ведётся под очень жёстким NDA.
Решение относительно того, стоят ли сильные стороны монорепозиториев той «платы», которая сопряжена с их использованием, принимается на уровне каждой организации или команды разработчиков. Но я, когда речь заходит о сфере моей специализации, о ML‑проектах, могу сказать, что монорепозиторий — это, в большинстве случаев, хороший выбор. Конечно, если только речь не идёт о реально огромных проектах или о каких‑то сверхсекретных разработках.
Поговорим о том, почему это так.
Работа над проектами машинного обучения с использованием монорепозиториев
Имеются по меньшей мере шесть причин того, что монорепозитории особенно хорошо подходят для ML‑проектов:
-
Интеграция конвейеров обработки данных.
-
Единообразие при проведении экспериментов.
-
Упрощение версионирования моделей.
-
Улучшение взаимодействия разных подразделений компаний.
-
Атомарные изменения кода.
-
Унификация стандартов кодирования.
Интеграция конвейеров обработки данных
Работа над ML‑проектами часто предусматривает использование конвейеров обработки данных. Они отвечают за предварительную обработку и преобразование данных, за подачу данных на вход моделей. Эти конвейеры могут быть тесно интегрированы с ML‑кодом. Если держать код конвейеров и код, реализующий алгоритмы машинного обучения, в одном репозитории, это поможет поддерживать тесную интеграцию такого кода и оптимизировать рабочие процессы.
Единообразие при проведении экспериментов
Разработка в сфере машинного обучения включает в себя проведение множества экспериментов. Если код, который для этого используется, хранится в монорепозитории — это обеспечивает единообразную настройку окружения, в котором проводятся эксперименты. Это, кроме того, уменьшает риск получения противоречивых результатов в разных экспериментах, возникающих из‑за различий в версиях кода или данных.
Упрощение версионирования моделей
В монорепозитории версии кода и модели синхронизированы, согласованы, так как работа над ними ведётся в одном и том же репозитории. Это упрощает управление версиями моделей и наблюдение за изменениями кода, имеющегося в разных их версиях. Это может быть особенно важным в проектах, где первостепенную важность имеет воспроизводимость результатов, получаемых в ходе экспериментов с моделями машинного обучения.
Улучшение взаимодействия разных подразделений компаний
Проекты из сферы машинного обучения часто предусматривают взаимодействие между дата‑сайентистами, ML‑инженерами и программистами. Применение монорепозитория помогает взаимодействию между разными подразделениями компании за счёт того, что он представляет собой единый источник достоверных данных для всего кода и всех других ресурсов, имеющих отношение к проекту.
Атомарные изменения кода
Эффективность работы моделей, когда дело касается машинного обучения, может зависеть от множества взаимосвязанных факторов. Среди них — предварительная обработка данных, извлечение признаков, архитектура модели, постобработка данных. Монорепозиторий позволяет выполнять атомарные изменения всего, что имеет отношения к моделям. То есть — изменение множества компонентов проекта может быть выполнено за один раз, в одном коммите. Благодаря этому всё в проекте, что зависит друг от друга, всегда будет друг с другом согласовано.
Унификация стандартов кодирования
И наконец — в состав ML‑команд часто входят сотрудники, не имеющие опыта в сфере разработки ПО. Это — люди, которые хорошо разбираются в математике, статистике, эконометрике. Они умны, у них постоянно возникают интересные идеи, они умеют обучать модели, решающие проблемы бизнеса. Но в число их сильных сторон не входит написание чистого и читабельного кода, который легко поддерживать.
Монорепозиторий помогает таким людям встроиться в команду благодаря тому, что в нём осуществляются проверки соблюдения стандартов написания кода во всех проектах. Если код стандартам не соответствует — система принудит его автора сделать всё как нужно. Это не только позволяет обеспечить высокое качество кода, но и помогает учиться и расти над собой тем членам команд, которые меньше других владеют искусством программирования.
Опыт гигантов IT-индустрии: знаменитые репозитории
Монорепозитории используют некоторые из крупнейших и успешнейших компаний из сферы программного обеспечения. Вот несколько заметных примеров:
-
Google: компания Google уже давно является убеждённым сторонником монорепозиториев. Вся её кодовая база, а это, по некоторым оценкам, около 2 миллиардов строк кода, хранится в единственном огромном репозитории. Компания даже опубликовала статью об этом.
-
Meta: эта компания тоже использует монорепозиторий для хранения большей части своей кодовой базы. В компании создана система контроля версий Mercurial, ориентированная на поддержку её большого и сложного монорепозитория.
-
Twitter: эта компания уже давно поддерживает свой монорепозиторий с использованием Pants — системы сборки кода, о которой мы скоро поговорим!
Многие другие компании, в числе которых — Microsoft, Uber, Airbnb и Stripe, тоже используют монорепозитории, храня в них, как минимум, некоторые части своих кодовых баз.
Хватит теории! Поговорим о том, как создавать монорепозитории для решения задач машинного обучения. Это несколько сложнее, чем просто взять и сложить то, что раньше было разбито на разные репозитории, в одно место.
Как настроить ML-репозиторий для Python-проектов?
Здесь мы, в качестве примера, будем пользоваться ML‑репозиторием, который я создал специально для статьи. Это — простой монорепозиторий, в котором хранится всего один проект или модуль: классификатор рукописных цифр mnist, названный так в честь знаменитого набора данных, который он использует.
Прямо сейчас вам достаточно знать то, что в корне этого репозитория находится директория mnist, и то, что в ней находится некий Python‑код для обучения модели. Там же лежат модульные тесты и файл Dockerfile, позволяющий запускать обучение в контейнере.
Мы будем пользоваться этим маленьким примером для того чтобы не усложнять повествование. Но учитывайте, что в более крупном монорепозитории mnist может представлять собой лишь одну из папок, находящихся в корневом разделе репозитория и хранящих данные проектов. Каждая из папок проектов будет содержать в себе, как минимум, файлы с исходным кодом, тесты, файлы Dockerfile и requirements.txt.
Система сборки проектов: зачем она нужна и как её выбрать?
Зачем нужна система сборки проектов?
Подумайте о тех действиях, за исключением написания кода, которые предпринимают разные команды, занимаясь обычной работой над проектами, хранящимися в монорепозитории. Они пропускают код через lintest, проверяя соблюдение стандартов по стилю программирования, они запускают модульные тесты, собирают артефакты вроде Docker‑контейнеров и WHL‑файлов, отправляют их во внешние репозитории и разворачивают их в продакшне.
Возьмём, для примера, тестирование.
Вы внесли изменение во вспомогательную функцию, поддержкой которой занимаетесь. Вы запустили тесты и оказалось, что всё в порядке. А как удостовериться в том, что ваши изменения не поломают код, написанный разработчиками из другой команды, в котором, возможно, импортируется ваша утилита? Для этого, понятно, вам надо выполнить и набор тестов, подготовленный другими разработчиками.
Но для того чтобы это сделать, вам нужно точно знать о том, где используется код, который вы изменили. По мере роста кодовой базы ручной поиск этих мест становится всё медленнее и медленнее. Конечно, в качестве альтернативного решения, можно всегда выполнять все тесты, но такой подход, опять же, с ростом кодовой базы, требует всё больших временных затрат.
Вот ещё пример: развёртывание проектов в продакшне.
Развёртываете ли вы проект еженедельно, ежедневно или непрерывно — для этого надо запустить сборку всех сервисов из монорепозитория и запушить всё это в продакшн. Но есть один нюанс: необходимо ли каждый раз делать сборку абсолютно всего? Сборка, по мере роста проекта, может оказаться затратной процедурой, как в плане времени, так и в плане денег.
Некоторые проекты из монорепозитория, возможно, не обновляются неделями. А, с другой стороны, код утилит, который в них используется, может обновляться чаще. Как решить — что именно собирать? Опять же, тут идёт разговор о зависимостях. В идеале сборке должны подвергаться только те сервисы, на которые повлияли последние изменения в коде, от которого они зависят.
Все эти задачи, если речь идёт о небольшой кодовой базе, можно решить с помощью простого 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 разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
ссылка на оригинал статьи https://habr.com/ru/articles/796127/
Добавить комментарий