Поиск по сайту Поиск

Анализ производительности нейросетей на GPU с помощью NVIDIA Visual Profiler

Специалисты Data Science делятся на два лагеря: те, кому кажется, что нейросети обучаются слишком медленно, и те, кому скоро тоже начнёт так казаться. В этой статье мы расскажем, как проанализировать и «прокачать» приложения машинного обучения с помощью профилирования.

Очень часто проблемы с производительностью возникают при переносе приложения с одной конфигурации сервера на другую. Обычно клиент и инженеры технической поддержки анализируют различия в hyperthreading, частотах памяти, вычислительных ядрах, версиях библиотек и прочих косвенных показателях. Более детальное исследование требует согласованных действий, часто приводящих к нарушению конфиденциальности данных, что упирается в IT-бюрократию. Поэтому получается, что «продвинутый» клиент гораздо эффективнее оказал бы помощь «сам себе». Немногие разработчики нейросетей умеют действительно «заглянуть под капот» приложения и детально проследить эффекты, влияющие на его производительность. Попробуем приоткрыть эту завесу и узнать, какие техники и приёмы следует использовать.

Оптимизация кода на GPU

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

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

Оказывается, компилятор не распознал процесс загрузки пикселей и сгенерировал ассемблерный код для однобайтовых значений вместо целого RGBA-вектора. Негативный эффект будет частично самортизирован L1-кэшем, с учётом того, в какой мере он занят поддержкой других операций. Но подобное решение всё равно может существенно замедлить приложение. На большом изображении (6496px × 6618px × 24bpp) для GPU Tesla V100 разница в скорости составляет примерно 40%:

В этом случае чтение пикселей оптимизировано вручную с помощью union:

В новой версии приложения однобайтовые загрузки превратились в одну четырёхбайтовую:

Но главный вопрос — как разработчику заметить такой случай? Опыт и знания стоят времени и денег. Регулярное чтение ассемблерного кода могло бы помочь, но не очень реалистично на практике. Поэтому существуют более универсальные средства анализа, такие как:

  1. Аппаратные счётчики в GPU для сбора статистики о работе программы;
  2. Метрики, которые обрабатывают данные счётчиков и объединяют их в качественные характеристики приложения. 

Грубо говоря, такая важная метрика, как GPU Occupancy, — это что-то похожее на рейтинг вашего смартфона в программе AnTuTu. Сбор и анализ метрик называется профилированием. Сведения о параметрах работы приложения, полученные в результате профилирования, гораздо легче анализировать и использовать для повышения эффективности. Так, программа NVIDIA Visual Profiler превращает счётчики и метрики в графики и диаграммы, и даже даёт советы по оптимизации. Рассмотрим её основные возможности.

Пример анализа использования памяти в NVIDIA Visual Profiler

Обзор NVIDIA Visual Profiler

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

Подготовка приложения к профилированию

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

Основная рекомендация при работе с Visual Profiler — использовать небольшие участки для профилирования. По умолчанию данные собираются в течение всего времени запуска, но лучше анализировать только критические места. Так вы сможете сосредоточить внимание на коде, оптимизация которого приведёт к существенному увеличению производительности. Использование Visual Profiler с большими и сложными приложениями может вызывать зависание движка JVM, поэтому лучше запускать его не более чем на 30 секунд.

Также стоит присвоить пользовательские названия ресурсам CPU и CUDA, поскольку на временной шкале Visual Profiler имена по умолчанию не слишком информативные. Если использовать более понятные и говорящие наименования, то можно улучшить понимание поведения приложения, особенно когда в нём присутствует много устройств, контекстов или потоков.

Создание сеанса профилирования

Первый шаг в использовании Visual Profiler — создание нового сеанса профилирования. В нём будут содержаться настройки, данные и результаты анализа вашего приложения. Для этого необходимо указать исполняемый файл, а также, при желании — рабочий каталог, аргументы, параметры мультипроцессного профилирования и переменные окружения. При этом вы можете задать конкретные процессы для обработки, включить или выключить временную шкалу, а также установить различные параметры профилирования для CUDA и CPU.

После применения настроек Visual Profiler немедленно запустит ваше приложение и начнёт собирать данные, необходимые для первого этапа управляемого анализа (если только при создании сеанса не была выбрана опция Don't run guided analysis). Систему управляемого анализа можно использовать для получения рекомендаций по улучшению производительности приложения.

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

Разбор результатов

Временная шкала

В Visual Profiler можно одновременно открыть несколько временных шкал на разных вкладках. На следующем рисунке показана шкала для приложения CUDA:

В верхней части находятся горизонтальные отметки времени, прошедшего с начала профилирования приложения. В левой части отображены единицы исполнения: процесс (process), потоки (thread), GPU (device), контексты (context), ядра (kernel), стримы (stream) и т.д. (с полным списком можно ознакомиться в документации).  В центре показаны строки, отражающие активность отдельных элементов. Каждая строка отображает интервалы времени между началом и окончанием каких-либо процессов. Например, строки напротив ядер показывают время начала и окончания выполнения этого ядра.

Анализ

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

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

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

Дизассемблер

Source-Disassembly View используется для отображения результатов анализа на уровне ассемблера. В исходном коде отмечается интенсивность и эффективность выполнения отдельных команд. Соответствующие маркеры окрашиваются в разные цвета в зависимости от уровня критичности — низкого, среднего или высокого. Эта информация — основа для базового приёма оптимизации: выделение разработчиком наиболее критичных («тяжёлых») на общем фоне команд (hotpoints) и их переработка — изменение программной логики, понижение точности и так далее.

Просмотр сведений о GPU

GPU Details View показывает таблицу с информацией о каждом копировании памяти (memcpy) и запуске ядра (kernel) в профилируемом приложении. Для ядер в столбцах показаны соответствующие метрики и события.

Подробный справочник по метрикам можно найти в документации.

Анализ производительности ML-приложения на GPU 

Рассмотрим пример профилирования ML-приложения, которое использует мощности GPU. Предположим, перед нами стоит задача обучить LSTM-нейросеть для генерации связного текста. Для этого был написан скрипт на Python, основанный на Keras. Обратите внимание, что сам python-код явно GPU не использует. Но «выход» на GPU может происходить уже внутри функций библиотеки Keras без непосредственного участия пользователя. Поэтому смело запускайте NVIDIA Profiler, указывая в качестве приложения сам Python-скрипт, и профилировщик доберётся в нём до GPU-кода. Тем не менее, следует ограничить длительность профилирования небольшим интервалом, например, 30 секунд. Поскольку алгоритм обучения нейросети однообразен и периодичен, мы можем считать, что этот интервал характеризует поведение приложения в целом, упрощая при этом работу профилировщика по сбору данных.

Для эффективной загрузки GPU должны выполняться два условия:

  1. GPU не должна простаивать: необходимо, чтобы какое-либо ядро всегда было загружено;
  2. Программы-ядра должны использовать как можно больше вычислительных единиц, например, перемножать громоздкие матрицы.

На рисунке ниже показан процесс профилирования нашего ML-скрипта: 

Слева направо идёт время выполнения, маленькие жёлтые «кирпичики» в центре — API-вызовы в потоке CPU, который запускает расчёты, а зелёные отрезки снизу — загрузка GPU. 100% использования GPU, как нетрудно догадаться, отображалось бы не маленькими квадратиками, а одним непрерывным блоком. Но на практике этого добиться невозможно, и лучшее, на что можно рассчитывать — чтобы между «кирпичиками» почти не было зазоров. Таким образом, условие (1) в этом примере не выполняется.

Рассматриваемое приложение использует ядро умножения матриц с одинарной точностью (Single precision floating General Matrix Multiply, SGEMM). В окне свойств (справа) указано, что ядро запущено в конфигурации «16 блоков по 128 потоков». Это дало бы хороший результат на GPU начального уровня, но в мощных графических процессорах с архитектурой VOLTA доступно до тысячи таких блоков! Из этого следует, что ядро использует менее 1/10 доступных ресурсов.

⌘⌘⌘

Теперь вы знаете, как написать неэффективное приложение для GPU (лучше, конечно же, этого не делать). Ваш заказчик может возразить, что код всё-таки работает на 10% быстрее с видеокартой NVIDIA RTX. Такой аргумент звучит как «Приора оказалась чуть быстрее Феррари в гонке по болоту». Другими словами, если приложение не раскрывает преимуществ графического процессора, то производительность может быть почти любой, а в абсолютных значениях — слабой во всех вариантах. Чтобы получить существенное ускорение на мощных GPU, необходимо решать по-настоящему высоконагруженные задачи.


Локальное ранжирование в Google: как проверять эффективность продвижения сайта в разных регионах

Чтобы убедиться, что сайт правильно продвигается, необходимо понимать, как именно он ранжируется в разных региональных сегментах интернета. Команда Links.Sape рассказывает...
Read More

Как совместить карьеру и семью: интервью с сотрудником REG.RU

Режим самоизоляции 2020 года разделил нашу жизнь на «до» и «после». И хоть прошло больше двух лет, многие IT-специалисты так...
Read More

Подборка выпусков подкаста «640 килобайт» для IT-специалистов

Удивительно, но в эпоху стримов, тиктоков и виртуальной реальности подкасты переживают вторую волну популярности. Всё потому, что у аудиоконтента есть...
Read More

Близнецы или двойняшки: что такое тайпо-домены

В прошлом месяце мы обещали подготовить статью про тайпо-домены. Сказано — сделано. Сегодня разберемся, что это такое и зачем регистрировать...
Read More

Необычная среда разработки Jupyter Notebook

Если вы хотите писать на Python или работать с Data Science, обратите внимание на интерактивную среду разработки с «живым» кодом...
Read More

Как определить фишинг и не попасться на крючок

Праздник к нам приходит, а вместе с ним и два месяца распродаж: 11.11 и «черные» дни недели. К сожалению, также...
Read More

Публичное, частное или гибридное: рассказываем, какое облако лучше подойдет вашему бизнесу

Причина популярности облачных технологий в бизнесе — не только безопасность данных и сокращение time-to-market (времени вывода на рынок). Облака позволяют...
Read More

Осенний рецепт для создания крутого сайта

Ноябрь — прекрасное время не только для тыквенных пирогов и облепихового чая, но и для запуска сайтов. Пока ваши клиенты...
Read More

Что такое Python-хостинг и какой тип услуги выбрать

В статье мы расскажем о том, что такое Python и как выбрать хостинг для проектов на этом языке. (далее…)
Read More

Элиза, Пэри и Алиса: история и эволюция чат-ботов

За последнее десятилетие чат-боты незаметно влились в нашу жизнь и стали ее неотъемлемой частью. Siri поможет найти ответ на любой...
Read More