Примечание редактора. Чтобы узнать больше об исследованиях, моделях и методах машинного обучения, ознакомьтесь с тематикой машинного обучения на конференции ODSC West 2021, которая состоится в ноябре этого года. Подробнее здесь.

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

Вдохновением для этого упражнения послужил клиент из списка Fortune 500, который является участником Программы поддержки предприятий PyTorch. Недавно они столкнулись с проблемой несоответствия производительности между MacOS, Linux и Windows. Установка количества потоков в коде PyTorch на 1 значительно повысила производительность в Linux и Windows, но не повлияла на производительность MacOS. Давайте вскочим и разберемся, почему это происходит.

Почему разница в производительности зависит от ОС?

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

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

  • Настройки TorchServe:
  • number_of_netty_threads: Номер внешнего интерфейса Netty.
  • netty_client_threads: количество серверных потоков.
  • default_workers_per_model: количество рабочих процессов, которые необходимо создать для каждой модели, загружаемой при запуске.
  • Настройки ПиТорч:
  • Количество потоков PyTorch: существует несколько способов установить количество потоков, используемых в факеле.

Тестирование конфигураций TorchServe для оптимизации производительности

Эксперимент 1. Тест производительности при обслуживании моделей PyTorch напрямую без TorchServe в Windows.

Чтобы смоделировать, как TorchServe загружает и выполняет логические выводы для модели, мы написали некоторый код-оболочку для обслуживания модели с помощью TorchServe. Это позволит нам изолировать место потери производительности. Когда мы выполняем вывод непосредственно на модели с кодом-оболочкой, нет никакой разницы между установкой количества потоков, равным 1, и тем, когда он остается значением по умолчанию. Это говорит о том, что вычислительных мощностей достаточно для обработки рабочей нагрузки и что эти значения оптимальны для данного размера модели. Однако это все еще может вызвать проблему, если для количества потоков задано большое значение, как мы обсудим более подробно позже. Этот результат можно проверить, используя приведенный ниже скрипт и запустив его для вашей модели. (Этот скрипт можно использовать для тестирования как с Linux, так и с Windows)

import sys
from ts.metrics.metrics_store import MetricsStore
from ts.torch_handler.base_handler import BaseHandler
from uuid import uuid4
from pprint import pprint
class ModelContext:
    def __init__(self):
        self.manifest = {
            'model': {
                'modelName': 'ptclassifier',
                'serializedFile': '<ADD MODEL NAME HERE>',
                'modelFile': 'model_ph.py'
            }
        }
        self.system_properties = {
            'model_dir': '<ADD COMPLETE MODEL PATH HERE>'
        }
        self.explain = False
        self.metrics = MetricsStore(uuid4(), self.manifest['model']['modelName'])
    def get_request_header(self, idx, exp):
        if exp == 'explain':
            return self.explain
        return False
def main():
    if sys.argv[1] == 'fast':
        from ptclassifier.TransformerSeqClassificationHandler import TransformersSeqClassifierHandler as Classifier
    else:
        from ptclassifiernotr.TransformerSeqClassificationHandler import TransformersSeqClassifierHandler as Classifier
    ctxt = ModelContext()
    handler = Classifier()
    handler.initialize(ctxt)
    data = [{'data': 'To be or not to be, that is the question.'}]
    for i in range(1000):
        processed = handler.handle(data, ctxt)
        #print(processed)
    for m in ctxt.metrics.store:
        print(f'{m.name}: {m.value} {m.unit}')
if __name__ == '__main__':
    main()

Эксперимент 2. Тест производительности на более крупной модели в Linux.

Мы использовали модель, отличную от официальной TorchServe HuggingFaces Sample, с тем же тестовым сценарием, что и выше, чтобы получить представление о Linux. Поскольку модель HuggingFace намного тяжелее, чем модель нашего клиента, это позволит нам проверить производительность логического вывода на более длительно работающем экземпляре.

Для модели большего размера стоимость переключения контекста меньше по сравнению со стоимостью логического вывода. Таким образом, разница в производительности меньше. В приведенном ниже результате теста мы видим разницу в производительности (3X против 10X):

Эксперимент 3. Тест производительности при различных сочетаниях настроек потока в Linux.

Этот эксперимент показывает, как параметр потока повлияет на общую производительность логического вывода. Мы тестировали на WSL (подсистема Windows Linux) с 4 физическими ядрами и 8 логическими ядрами. Результат теста показывает, что достаточная многопоточность улучшит производительность, но избыточная многопоточность существенно ухудшит производительность логического вывода и общую пропускную способность. Наилучший результат эксперимента показывает ускорение вывода в 50 раз и улучшение пропускной способности в 18 раз по сравнению с наименее эффективными настройками. Метрики для лучших и худших проиллюстрированы ниже в таблицах:

Вот сценарий, используемый для создания этих результатов теста:

import subprocess
import itertools
import time
import os
from time import sleep
def do_test(number_of_netty_threads=1, netty_client_threads=1,
default_workers_per_model=1, 
            job_queue_size=100, MKL_NUM_THREADS=1, test_parallelism=8):
    # generate config.properties files based on combination
    config_file_name = "config_file/" +
f"config_{number_of_netty_threads}_{netty_client_threads}_{default_workers_per_model}_{job_queue_size}.properties"
    f = open(config_file_name, "w")
    f.write("load_models=all\n")
    f.write("inference_address=http://0.0.0.0:8080\n")
    f.write("management_address=http://0.0.0.0:8081\n")
    f.write("metrics_address=http://0.0.0.0:8082\n")
    f.write("model_store=<ADD COMPLETE MODEL PATH HERE>\n")
    f.write(f"number_of_netty_threads={number_of_netty_threads}\n")
    f.write(f"netty_client_threads={netty_client_threads}\n")
    f.write(f"default_workers_per_model={default_workers_per_model}\n")
    f.write(f"job_queue_size={job_queue_size}\n")
    f.close()
    # start the torch serve with proper config properties and other parameter settings
    subprocess.call(f"MKL_NUM_THREADS={str(MKL_NUM_THREADS)} torchserve --start 
--model-store model-store --models model=<ADD MODEL NAME HERE> --ncs
--ts-config {config_file_name}", shell=True, stdout=subprocess.DEVNULL)
    sleep(3)
    # test in parallel to inference API
    print("start to send test request...")
    start_time = time.time()
    print(time.ctime())
    subprocess.run(f"seq 1 1000 | xargs -n 1 -P {str(test_parallelism)} bash -c 
'url=\"http://127.0.0.1:8080/predictions/model\"; curl -X POST $url -T
input.txt'", shell=True, capture_output=True, text=True)
    total_time = int((time.time() - start_time)*1e6)
    print("total time in ms:", total_time)
    # get metrics of ts inference latency and ts query latency 
    output = subprocess.run("curl http://127.0.0.1:8082/metrics", shell=True,
capture_output=True, text=True)
    inference_time=0
    query_time=0
    # capture inference latency and query latency from metrics
    for line in output.stdout.split('\n'):
        if line.startswith('ts_inference_latency_microseconds'):
            inference_time = line.split(' ')[1]
        if line.startswith('ts_queue_latency_microseconds'):
            query_time = line.split(' ')[1]
    # calculate the throughput
    throughput = 1000 / total_time * 1000000
    # write metrics to csv file for display
    f = open("test_result_short.csv", "a")
    f.write(f"{number_of_netty_threads},{netty_client_threads},{default_workers_per_model},
{MKL_NUM_THREADS},{job_queue_size},{test_parallelism},{total_time},
{inference_time},{query_time},{throughput}\n")
    f.close()
    # stop torchserve for this
    stop_result = os.system("torchserve --stop")
    print(stop_result)
    stop_result = os.system("torchserve --stop")
    print(stop_result)
    stop_result = os.system("torchserve --stop")
    print(stop_result)
def main():
    # set the possible value, value range of each parameter
    number_of_netty_threads = [1, 2, 4, 8]
    netty_client_threads = [1, 2, 4, 8]
    default_workers_per_model = [1, 2, 4, 8]
    MKL_NUM_THREADS = [1, 2, 4, 8]
    job_queue_size = [1000] #[100, 200, 500, 1000]
    test_parallelism = [32] #[8, 16, 32, 64]
    # for each combination of parameters
    [do_test(a, b, c, d, e, f) for a, b, c, d, e, f in 
itertools.product(number_of_netty_threads, netty_client_threads,
default_workers_per_model, job_queue_size, MKL_NUM_THREADS, test_parallelism)]
if __name__ == "__main__":
    main()

Эксперимент 4. Тестирование в Windows с помощью регистратора производительности Windows.

Мы также смогли воспроизвести проблему с производительностью в Windows. Затем мы использовали средство записи производительности Windows и анализатор производительности Windows для анализа общей производительности системы во время выполнения тестов на модели.

На обоих рисунках ниже показано общее количество переключений контекста по процессам и потокам в системе. Все процессы Python (рабочие процессы, созданные TorchServe) окрашены в зеленый цвет.

На рисунке выше показано количество переключений контекста в зависимости от времени для медленного случая (когда установлено количество потоков по умолчанию).

На рисунке выше показаны те же данные, когда число потоков равно 1.

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

Эксперимент 5: открытие MacOS

В MacOS у PyTorch есть проблема: в macOS используется только один поток. Номер потока по умолчанию равен 1 вместо количества логических ядер. Это приведет к повышению производительности TorchServe, поскольку устранит затраты на переключение контекста. Таким образом, проблемы с производительностью не было в MacOS.

Резюме

Эта проблема с производительностью оказалась больше связана с настройками потока. Основной вывод заключается в том, что установка числа потоков равным 1 в PyTorch приводит к уменьшению общего числа потоков, работающих в системе, и, таким образом, уменьшает общее количество переключений контекста в Linux и Windows. Для MacOS вызов для установки количества потоков не имеет значения, и в результате мы не видим разницы в производительности.

Многие факторы могут повлиять на оптимальное сочетание для логического вывода, например: количество доступных ядер, количество обслуживаемых моделей и размер моделей. Лучший способ найти оптимальное сочетание — это экспериментировать. Не существует инструмента/среды для автоматической установки наилучшего значения для каждого параметра для достижения сбалансированной производительности и пропускной способности. Часто требуется исследование для точной настройки параметров потока в зависимости от целевого оборудования при использовании TorchServe для обслуживания моделей, как описано в Документации TorchServe. Мы поделились тестовыми сценариями в рамках этого блога, чтобы запустить эти тесты производительности для вывода вашей модели. Используйте предоставленные сценарии, чтобы поэкспериментировать и найти наилучший баланс настроек для задержки логического вывода и общей пропускной способности вашей модели.

Ссылки

Статья Фрэнка Донга и Кэсси Бревиу из Microsoft.

Исходное сообщение здесь.

Читайте другие статьи по науке о данных на OpenDataScience.com, включая учебные пособия и руководства от начального до продвинутого уровня! Подпишитесь на нашу еженедельную рассылку здесь и получайте последние новости каждый четверг. Вы также можете пройти обучение по науке о данных по запросу, где бы вы ни находились, с помощью нашей платформы Ai+ Training.