На прошлой неделе в Hugging Face мы запустили новое революционное приложение для текстового редактора. Он отличается от традиционных текстовых редакторов тем, что модель НЛП может дополнить ваши предложения, если вы попросите об этом, открывая новое измерение «письму с помощью машины». Он основан на GPT-2, языковой модели OpenAI, которая может генерировать синтаксически точные предложения и последовательные абзацы текста.

Демоверсия доступна на https://transformer.huggingface.co, и вы можете попробовать ее! 🦄 Напишите с помощью трансформатора, чтобы написать, что калькуляторы для исчисления.

Эта модель является частью последних тенденций в НЛП, которые вращаются вокруг создания очень больших языковых моделей, которые дают отличные результаты в различных задачах при точной настройке на эти конкретные задачи. В результате получаются модели «Трансформеры» с большим количеством параметров (до 1,5 миллиардов параметров для GPT-2 Large или Grover), с которыми трудно работать из-за их веса.

Наше приложение позволяет пользователю выбирать между двумя моделями: GPT-2 small и GPT-2 medium. Загрузка их обоих в ОЗУ компьютера занимает в общей сложности 2,4 ГБ памяти.

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

Проблема под рукой

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

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

Настройка нашего рабочего пространства

Мы будем создавать серверный API, к которому будет подключаться наше интерфейсное приложение. Этот API будет отвечать за обработку вычислений, необходимых для генерации предложений. Мы будем использовать Python для этой задачи, так как большинство моделей НЛП легко доступны. Другие низкоуровневые языки, такие как C ++ или Rust, были бы более подходящими для ориентированных на производительность бэкэндов, и мы обсудим их использование в последней части этого поста.

Мы использовали falcon для веб-серверов (подойдет и любой другой http-фреймворк) в сочетании с gunicorn для запуска наших экземпляров и балансировки нагрузки. Наша собственная реализация GPT-2 Pytorch - основа этого проекта. У нас есть несколько примеров в нашем каталоге примеров, если вы хотите сделать что-то подобное.

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

3-стороннее автозаполнение

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

Самый наивный подход, который мы могли бы использовать, - это использовать одного воркера с загруженной сзади моделью:

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

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

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

Было бы лучше распараллелить три итерации, так как мы ищем наименьшее время отклика при автозаполнении. К счастью для нас, Python дает нам доступ к нескольким параметрам распараллеливания, которые могут быть использованы в нашем сценарии:

Многопоточность

Многопоточность в Python обычно выполняется с использованием класса threading, который позволяет программе создавать несколько потоков, каждый из которых будет выполнять свои соответствующие операции. Проблема с многопоточностью заключается в том, как Global Interpreter Lock - или GIL - работает в Python.

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

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

Многопроцессорность

Многопроцессорность может идти двумя путями; либо путем загрузки полностью отдельных процессов и подключения к их вводу / выводу (с помощью модуля subprocess), либо путем создания процессов python, которые могут наследовать ресурсы текущего процесса интерпретатора Python (минуя проблему GIL, используя многопроцессорный модуль).

Сложная часть здесь - убедиться, что модель не нужно загружать в ОЗУ каждый раз, когда ей нужно вычислить логический вывод; большие модели долго загружаются в память.

Мы выбрали еще один, другой подход.

Наш подход с использованием балансировки нагрузки Gunicorn

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

Когда запрос отправляется из интерфейсного приложения в наш API, он обрабатывается нашим первым веб-сервером. У этого веб-сервера есть один рабочий, который запускает наш API. Этот API отвечает за отправку трех идентичных запросов второму веб-серверу. Запросы, отправленные из этого API, содержат текущий контекст (предыдущие предложения в документе), а также некоторую информацию о параметрах (малая или средняя модель, конкретные значения top_k,…).

У этого второго веб-сервера есть несколько рабочих, которые обрабатывают запросы отдельно. Три воркера будут обрабатывать каждый запрос, полученный от API, поэтому их можно обрабатывать одновременно. Мы используем отдельные потоки в API, чтобы запросы могли отправляться на второй веб-сервер параллельно, а не последовательно (HTTP-запросы - ›без проблем с GIL).

Эта архитектура имеет несколько преимуществ, которых нет у других, ранее упомянутых методов:

  • Мы можем сгенерировать столько рабочих процессов, сколько моделей поместится в нашей памяти. Если у нас распределенная система, мы распределяем рабочих между разными графическими процессорами.
  • Каждый рабочий загружает одну модель в память. Таким образом, может быть загружено больше моделей (больше вычислительной мощности), чем если бы каждый раз загружались три модели, например, при потоковом подходе.
  • Запущенные как рабочие веб-сервера, модели всегда остаются загруженными в память.
  • Мы используем балансировку нагрузки Gunicorn на каждом этапе нашей архитектуры. Мы не просто порождаем процессы, выполняющиеся параллельно, у нас есть способ убедиться, что каждый процесс обрабатывает нагрузки в соответствии с его вычислительными возможностями. Если бы мы использовали два разных графических процессора с разной вычислительной мощностью, узкое место, созданное нижним вычислительным графическим процессором, не повлияло бы на другой так сильно, как в чисто многопроцессорной программе.

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

Полученные результаты

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

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

Дальнейшие улучшения

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

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

Эта система зарекомендовала себя, поскольку до сих пор выдерживала нагрузку, обрабатывая более 100 000 различных запросов за неделю при работе на одной машине с 4 GPU (K80). Если вы хотите опробовать приложение и посмотреть, как наша система реагирует на трафик, вы можете попробовать его здесь 🦄

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