Зачем переделывать распределенную среду глубокого обучения, такую ​​как OneFlow?

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

Поэтому мы начнем серию из трех статей, в которых подробно обсудим три основных недостатка системы выполнения основных платформ DL.

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

Автор сценария Юань Цзиньхуэй; Перевод Донг Венвен

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

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

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

Это поднимает новый вопрос, сколько потоков должно быть установлено?

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

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

Механизм планирования вычислительного графа

Как и обычные системы обработки больших данных, платформы DL на основе статических графов используют модели потоков данных для описания задач и планирования операций.

Как показано в направленном ациклическом графе (DAG) выше, каждый узел представляет операцию (либо операцию вычисления, либо операцию перемещения данных). Ребра между узлами указывают от производителя к потребителю, представляя направление потока данных. Планировщик запускает операции на соответствующие аппаратные ресурсы для выполнения в соответствии с топологическим порядком группы обеспечения доступности баз данных.

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

Преимущество использования модели потока данных для описания распределенных параллельных задач состоит в том, что граф DAG полностью и явно показывает параллельные возможности программы. Используя приведенный выше график в качестве примера, два узла в первой строке могут выполняться одновременно, если позволяют базовые аппаратные ресурсы. Также возможно, чтобы три узла в третьей строке выполнялись параллельно, когда выполняются оба узла во второй строке. Это преимущество делает модель потока данных широко используемой в распределенной обработке больших данных, функциональной модели, параллельном анализе компилятора, неупорядоченном выполнении ЦП и других сценариях.

Мы определяем максимальную степень параллелизма вычислительной задачи как теоретическое максимальное количество операций, выполняемых параллельно. Например, максимальная степень параллелизма на рисунке 1 равна 3.

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

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

Родословная динамического и статического планирования

Существует две философии реализации для сопоставления операций с конкретными временными интервалами на конкретном оборудовании: статическое планирование и динамическое планирование.

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

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

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

Традиционные системы больших данных (например, Hadoop) обычно полностью динамически планируются. Планировщик отслеживает ход графа вычислений и состояние использования всех аппаратных ресурсов (ЦП, памяти и т. д.) в кластере в режиме реального времени и решает в режиме онлайн, на какой узел и какое ядро ​​ЦП этого узла распределять операции. или задачи на. Таким образом, динамическое планирование является наиболее гибким.

Практика в рамках DL показывает, что полностью динамическое планирование невозможно.

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

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

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

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

Абстракция аппаратных ресурсов

Механизм выполнения платформы DL включает в себя несколько концепций, связанных с аппаратными ресурсами. Операторы используются для функциональных описаний, а их конкретные реализации на конкретном оборудовании называются ядрами. Состояние оператора управляется потоками ОС операционной системы. Каждый аппаратный ресурс абстрагируется в очередь задач. Во время выполнения ядро ​​(то есть реализация операции) отправляется в очередь задач. Аппаратный планировщик берет ядро ​​из очереди и выполняет его на аппаратном ресурсе в соответствии с правилом FIFO.

Возьмите Nvidia GPGPU в качестве примера, поток CUDA — это своего рода очередь задач. Другие аппаратные ресурсы, включая ЦП и сеть, также могут быть абстрагированы в очереди задач, и мы единообразно называем такие очереди потоками.

Как показано на рисунке 2, в OneFlow каждый субъект управляет одной операцией, а один поток ОС может отвечать за планирование и выполнение нескольких операций. Планировщик помещает ядро, соответствующее операции, удовлетворяющей условиям выполнения, в очередь (поток CUDA), а аппаратный планировщик выполняет ядро ​​в очереди по правилам FIFO.

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

Сколько потоков должен создавать графический процессор?

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

  1. Для двух аппаратных ресурсов, которые могут работать независимо, если они совместно используют поток, два аппаратных ресурса могут выполняться только последовательно, а не параллельно. Например, механизм прямого доступа к памяти Nvidia GPGPU (копирование хоста на устройство, копирование устройства на хост и т. д.) и вычислительное ядро ​​— это два разных аппаратных ресурса. Предположим, что передача и вычисления используют один и тот же поток. В этом случае невозможно перекрыть копирование данных и вычисления на этом графическом процессоре, поэтому разные базовые аппаратные ресурсы должны использовать разные потоки.
  2. Для двух операций без зависимости, которые могут выполняться параллельно, если они отправляются в один и тот же поток, две возможные параллельные операции будут выполняться последовательно только потому, что они совместно используют один и тот же поток. Например, на графическом процессоре 1024 ядра, и если каждая операция должна использовать менее половины ядер, а две операции распределяются по разным потокам, то две операции могут выполняться параллельно на основе разных ядер, поэтому кажется, что потоки не могут быть слишком маленькими. Но если каждая операция может использовать все 1024 ядра при выполнении, то даже если они распределены по двум разным потокам, две операции могут выполняться только последовательно.
  3. Предположим, что две операции с зависимостями отправляются в два разных потока. В этом случае операция производителя в одном потоке должна быть выполнена до того, как операция потребителя может быть отправлена ​​во второй поток. Это означает, что требуется синхронизация (или ожидание) между различными потоками, а координация нескольких потоков обычно сложна. Если обе операции отправляются в один и тот же поток, то операции-потребителю не нужно ждать завершения выполнения операции-производителя, прежде чем ее можно будет отправить в поток. Это связано с тем, что функция FIFO потока гарантирует, что вторая операция начнет выполняться только после завершения выполнения первой операции. Использование одного потока может упростить управление потоками.

Подводить итоги:

  • Разные аппаратные ресурсы должны использовать разные потоки;
  • Операции с зависимостями лучше отправлять в один и тот же поток, а операции без зависимостей — в разные потоки.
  • Существует половина плюсов и минусов создания нескольких потоков из одного и того же аппаратного ресурса, как и создание только одного потока из одного и того же аппаратного ресурса. Так что же делать?

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

Для обычных задач обучения степень детализации операций обычно велика, и обычно достаточно одного вычислительного потока на каждый GPU. Для вывода, размер партии относительно небольшой. Если на мощных видеокартах создается только один поток, одна операция не может использовать видеокарту. Лучше создать несколько потоков и позволить нескольким операциям выполняться одновременно. Есть еще один способ решить эту проблему через компилятор. Rammer (nnfusion на GitHub), представленный MSRA на OSDI 2020, предлагает умное решение для объединения мелкозернистой операции логического вывода в более крупную операцию для заполнения ресурсов графического процессора.

Модель потоков: сколько потоков нужно потоку?

Далее изучим роль потока ОС (если не указано иное, далее поток). Потоку может потребоваться выполнить две операции, то есть планирование и запуск.

Операция планирования состоит в том, чтобы управлять потоками (обновлять и проверять) зависимости операций в графе вычислений и помещать операции, зависимости которых были разрешены, в готовый набор.

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

На рис. 3 показаны четыре модели потоков для выполнения планирования и запуска.

  1. Использование одного потока для одновременного планирования и запуска. Поток обращается к графу вычислений и изменяет его состояние, но операции могут быть отправлены на разные устройства. Следует отметить, что при отправке задач на устройство необходимо соответствующим образом переключать контекст устройства, что приведет к определенным накладным расходам.
  2. Разделение операций планирования и запуска. Конкретный поток управляет графом вычислений, и этот поток не запускает ядра CUDA напрямую. Вместо этого операции запуска распределяются по некоторым рабочим потокам, каждый из которых обслуживает конкретное устройство. В этом случае рабочему потоку не нужно платить за переключение контекста устройства. Более того, к графу вычислений обращается только поток планирования, и нет необходимости рассматривать одновременное чтение и запись несколькими потоками. Однако обновление состояния оператора должно быть выполнено с использованием потока планирования в качестве промежуточного прокси. Например, как показано в случае 2, оператор-производитель и оператор-потребитель находятся в разных рабочих потоках. После выполнения операции производителя поток планирования будет уведомлен об обновлении графа вычислений. Рабочий поток, в котором находится операция-потребитель, не будет затронут до тех пор, пока граф вычислений не будет обновлен. Если оператор-производитель и оператор-потребитель могут общаться напрямую, можно избежать накладных расходов на выполнение потока планирования в качестве промежуточного прокси-сервера.
  3. Чтобы устранить накладные расходы прокси-сервера в случае 2, потоку разрешено выполнять планирование и запуск операций на конкретном устройстве, что позволяет избежать переключения контекста устройства. Каждый поток может получить доступ к графу вычислений и изменить его состояние, а операции в разных потоках с восходящими и нисходящими отношениями производства и потребления могут напрямую взаимодействовать друг с другом. Этот метод устраняет дополнительные накладные расходы, связанные с использованием потока планирования между операциями производителя и потребителя в качестве промежуточного прокси. Однако глобальное состояние должно быть защищено блокировками, чтобы свести к минимуму накладные расходы на конкуренцию, вызванную одновременным доступом к общему состоянию из нескольких рабочих потоков. Однако грубая блокировка может серьезно повредить общей производительности.
  4. Чтобы свести к минимуму накладные расходы на конкуренцию в случае 3, заметив, что состояния операций могут быть отделены от операции, можно использовать мелкозернистые блокировки (например, создание выделенной блокировки для защиты состояния каждой операции). Однако это сложно и подвержено ошибкам при оптимизации программы за счет уменьшения объема блокировки в общих параллельных системах.

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

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

Оптимальное количество потоков: сколько операторов обслуживает поток?

Теперь вернемся к вопросу «сколько потоков нужно настроить», рассмотренному в начале статьи. Этот вопрос эквивалентен вопросу «сколько операторов обслуживает поток», который относится к отношениям отображения между операторами и потоками. Отношение отображения может быть запланировано статически или динамически определено во время выполнения.

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

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

Если количество потоков меньше, чем параллелизм графа вычислений, и нет гарантии, что оператор не заблокируется, риска взаимоблокировки не избежать.

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

Сделать количество потоков равным количеству потоков — лучший выбор, если не принимать во внимание стабильность. Но есть ли способ сделать количество потоков равным количеству потоков без ущерба для стабильности?

Ключ к вопросу заключается в том, разрешать ли операции планирования и запуска блокировать поток в том месте, где он находится. Если гарантировано, что планирование и запуск не заблокируют поток, то безопасно заставить один поток обслуживать несколько операторов.

Асинхронное выполнение, сохранение состояния и восстановление

В исходных фреймворках DL многие операции разгружаются на педаль газа для асинхронного выполнения, а операции, выполняемые на ЦП, могут быть делегированы другому рабочему потоку для имитации асинхронного выполнения.

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

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

Оператор не будет занимать поток слишком долго, независимо от того, готов ли он.

Читатели, знакомые с концепцией пользовательского потока, такой как горутина в Golang и сопрограмма, могут быть знакомы с описанным выше механизмом, и их принципы очень близки. Эти принципы широко используются в инфраструктурах сетевых сервисов с высокой степенью параллелизма, таких как инфраструктура с открытым исходным кодом Baidu bRPC и инфраструктура с открытым исходным кодом Flare от Tencent.

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

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

Резюме

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

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

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

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

Статьи по теме:

  1. Ограничения существующих платформ глубокого обучения: зависимость от ресурсов
  2. Ограничения существующих платформ глубокого обучения: перемещение данных

Добро пожаловать в OneFlow наGitHub и следите за нами в Twitter и LinkedIn.

Кроме того, приглашаем присоединиться к нашей группе Discord, чтобы обсуждать и задавать вопросы, связанные с OneFlow, а также общаться с участниками и пользователями OneFlow по всему миру.