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

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

Асинхронное выполнение

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

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

Эффект асинхронного выполнения невидим для пользователя; но когда дело доходит до измерения времени, это может быть причиной многих головных болей. Когда вы рассчитываете время с помощью библиотеки «time» в Python, измерения выполняются на устройстве ЦП. Из-за асинхронной природы графического процессора строка кода, останавливающая отсчет времени, будет выполнена до завершения процесса графического процессора. В результате время будет неточным или не относящимся к фактическому времени логического вывода. Имея в виду, что мы хотим использовать асинхронизм, позже в этом посте мы объясним, как правильно измерять время, несмотря на асинхронные процессы.

Разогрев графического процессора

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

Вызов любой программы, которая пытается взаимодействовать с графическим процессором, заставит драйвер загрузить и / или инициализировать графический процессор. Это поведение при загрузке драйвера заслуживает внимания. Приложения, которые запускают инициализацию графического процессора, могут вызывать задержку до 3 секунд из-за поведения очистки кода исправления ошибок. Например, если мы измеряем время для сети, которая для одного примера занимает 10 миллисекунд, выполнение более 1000 примеров может привести к тому, что большая часть нашего рабочего времени будет потрачена на инициализацию графического процессора. Естественно, мы не хотим измерять такие побочные эффекты, потому что время неточно. Он также не отражает производственную среду, где обычно графический процессор уже инициализирован или работает в режиме сохранения.

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

Правильный способ измерения времени вывода

Приведенный ниже фрагмент кода PyTorch показывает, как правильно измерять время. Здесь мы используем Efficient-net-b0, но вы можете использовать любую другую сеть. В коде мы имеем дело с двумя оговорками, описанными выше. Прежде чем проводить какие-либо измерения времени, мы запускаем несколько фиктивных примеров по сети, чтобы выполнить «разогрев графического процессора». Это автоматически инициализирует графический процессор и предотвратит его переход в режим энергосбережения при измерении времени. Затем мы используем tr.cuda.event для измерения времени на GPU. Здесь очень важно использовать torch.cuda.synchronize (). Эта строка кода выполняет синхронизацию между хостом и устройством (т. Е. Графическим процессором и процессором), поэтому запись времени происходит только после завершения процесса, запущенного на графическом процессоре. Это решает проблему несинхронизированного выполнения.

model = EfficientNet.from_pretrained(‘efficientnet-b0’)
device = torch.device(“cuda”)
model.to(device)
dummy_input = torch.randn(1, 3,224,224,dtype=torch.float).to(device)
starter, ender = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True)
repetitions = 300
timings=np.zeros((repetitions,1))
#GPU-WARM-UP
for _ in range(10):
   _ = model(dummy_input)
# MEASURE PERFORMANCE
with torch.no_grad():
  for rep in range(repetitions):
     starter.record()
     _ = model(dummy_input)
     ender.record()
     # WAIT FOR GPU SYNC
     torch.cuda.synchronize()
     curr_time = starter.elapsed_time(ender)
     timings[rep] = curr_time
mean_syn = np.sum(timings) / repetitions
std_syn = np.std(timings)
print(mean_syn)

Распространенные ошибки при измерении времени

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

1. Передача данных между хостом и устройством. Точка зрения этого поста - измерить только время логического вывода нейронной сети. С этой точки зрения одна из наиболее распространенных ошибок связана с передачей данных между CPU и GPU при измерении времени. Обычно это происходит непреднамеренно, когда на ЦП создается тензор, а затем выполняется вывод на ГП. Это выделение памяти занимает значительное время, что впоследствии увеличивает время для вывода. Влияние этой ошибки на среднее значение и дисперсию измерений можно увидеть ниже:

2. Не используется прогрев графического процессора. Как упоминалось выше, при первом запуске графического процессора запрашивается его инициализация. Инициализация графического процессора может занять до 3 секунд, что имеет огромное значение, когда время выражается в миллисекундах.

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

s = time.time()
 _ = model(dummy_input)
curr_time = (time.time()-s )*1000

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

4. Отбор одной пробы. Как и многие процессы в информатике, прямая связь нейронной сети имеет (небольшой) стохастический компонент. Разница во времени выполнения может быть значительной, особенно при измерении сети с низкой задержкой. Для этого необходимо запустить сеть на нескольких примерах, а затем усреднить результаты (300 примеров могут быть хорошим числом). Распространенная ошибка - использовать один образец и называть его средой выполнения. Это, конечно, не соответствует истинному времени выполнения.

Измерение пропускной способности

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

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

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

(количество пакетов X размер пакета) / (общее время в секундах).

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

model = EfficientNet.from_pretrained(‘efficientnet-b0’)
device = torch.device(“cuda”)
model.to(device)
dummy_input = torch.randn(optimal_batch_size, 3,224,224, dtype=torch.float).to(device)
repetitions=100
total_time = 0
with torch.no_grad():
  for rep in range(repetitions):
     starter, ender = torch.cuda.Event(enable_timing=True),          torch.cuda.Event(enable_timing=True)
     starter.record()
     _ = model(dummy_input)
     ender.record()
     torch.cuda.synchronize()
     curr_time = starter.elapsed_time(ender)/1000
     total_time += curr_time
Throughput = (repetitions*optimal_batch_size)/total_time
print(‘Final Throughput:’,Throughput)

Заключение

Точное измерение времени логического вывода нейронных сетей не так тривиально, как кажется. Мы подробно описали несколько проблем, о которых следует знать специалистам по глубокому обучению, таких как асинхронное выполнение и режимы энергосбережения графического процессора. Представленный здесь код PyTorch демонстрирует, как правильно измерять время в нейронных сетях, несмотря на вышеупомянутые предостережения. Наконец, мы упомянули несколько распространенных ошибок, из-за которых люди неправильно измеряют время. В будущих публикациях мы еще глубже погрузимся в эту тему и расскажем о существующих профилировщиках глубокого обучения, которые позволяют нам достигать еще более точных измерений времени в сетях. Если вас интересует, как уменьшить задержку сети без ущерба для ее точности, вам предлагается прочитать дополнительную информацию по этой теме в официальном документе Deci.

Первоначально опубликовано в https://deci.ai/the-correct-way-to-measure-inference-time-of-deep-neural-networks/