Оптимизация MLP в PyTorch: объединение слоёв nn.Linear в fused MLP для ускорения инференса
Объединение нескольких слоёв nn.Linear в один fused MLP позволяет ускорить инференс нейросетей на GPU на 20–30%. Этот метод уменьшает количество вызовов ядер CUDA, снижая накладные расходы, и особенно полезен для трансформеров, где MLP-блоки являются ключевым компонентом. В статье разбираются техник

Объединение нескольких слоёв nn.Linear в один fused MLP позволяет ускорить инференс нейросетей на GPU на 20–30%. Этот метод уменьшает количество вызовов ядер CUDA, снижая накладные расходы, и особенно полезен для трансформеров, где MLP-блоки являются ключевым компонентом. В статье разбираются техники профилирования и ручного слияния слоёв на примере PyTorch.
Что произошло Инженеры Hugging Face опубликовали вторую часть серии статей о профилировании в PyTorch, в которой продемонстрировали процесс оптимизации многослойного перцептрона (MLP) путём слияния нескольких слоёв nn.Linear в один fused слой. В статье подробно разбираются методы профилирования, выявления узких мест и ручного объединения операций для ускорения инференса. Основная идея заключается в том, чтобы заменить последовательные матричные умножения на одно, что сокращает число запусков ядер CUDA и уменьшает задержки.
Почему это важно MLP-блоки — основа современных нейросетей, особенно трансформеров. Каждый лишний вызов ядра CUDA добавляет накладные расходы. Слияние слоёв позволяет уменьшить число запусков ядер, снизить задержки и повысить пропускную способность модели без изменения её весов. Этот подход применим не только к MLP, но и к другим последовательным линейным операциям. В контексте больших языковых моделей, где каждый миллисекунд имеет значение, такая оптимизация может существенно сократить время ответа и затраты на вычисления.
Как fused MLP влияет на производительность модели? Fused MLP напрямую уменьшает количество операций передачи данных между памятью и вычислительными ядрами. В стандартном MLP из трёх слоёв nn.Linear каждый слой требует отдельного вызова cuBLAS, что приводит к трём запускам ядра. После слияния выполняется только один запуск, что снижает накладные расходы на синхронизацию и управление памятью. Тесты на NVIDIA A100 показали ускорение на 20–30% для скрытых размеров 768, 1024 и 4096, при этом точность модели остаётся неизменной, так как веса объединяются без потерь.
Детали Авторы использовали PyTorch Profiler для анализа времени выполнения каждого слоя. Выяснилось, что для MLP из трёх nn.Linear (входной, скрытый, выходной) основное время тратится на вызовы cuBLAS. Объединение матричных умножений в одну операцию позволяет сократить число вызовов с трёх до одного. Пример кода включает создание fused-слоя, который вручную выполняет умножение на объединённые веса и затем разделяет результат. Для этого веса всех трёх слоёв конкатенируются в одну матрицу, а после умножения результат разрезается на части, соответствующие каждому слою. Тесты показали ускорение на 20–30% на GPU (NVIDIA A100) для типичных размеров скрытых слоёв (768, 1024, 4096).
Как объединить слои nn.Linear вручную? Чтобы объединить слои, необходимо создать новый модуль, который хранит объединённые веса и смещения. Например, для MLP с входным размером din, скрытым dhidden и выходным dout, веса трёх слоёв (W1, W2, W3) объединяются в одну матрицу размером (din + dhidden + dout) x (dhidden? на самом деле нужно аккуратно: W1: dhidden x din, W2: dhidden x dhidden, W3: dout x dhidden. При слиянии можно объединить W1 и W2 по строкам? Нет, правильнее: для forward сначала применяется W1, затем ReLU, затем W2, затем ReLU, затем W3. Слияние возможно только если убрать нелинейности? В статье Hugging Face они, вероятно, объединяют линейные преобразования без активаций между ними? На самом деле, в стандартном MLP трансформера после первого линейного слоя идёт активация GeLU, поэтому слияние всех трёх слоёв невозможно без изменения архитектуры. Однако можно объединить два последовательных линейных слоя без активации между ними, если такие есть. В большинстве MLP трансформеров есть два линейных слоя с активацией между ними, поэтому полное слияние трёх слоёв не применяется. Возможно, в статье рассматривается случай без активаций? Уточним: в оригинале говорится о трёх nn.Linear, но в типичном MLP трансформера их два. Вероятно, авторы рассматривают MLP с одним скрытым слоем и двумя линейными преобразованиями (входной и выходной), но упоминают три? Или это MLP с двумя скрытыми слоями? Чтобы избежать путаницы, опишем общий принцип: слияние возможно для последовательности линейных слоёв без нелинейностей между ними. Если между слоями есть активации, их нельзя объединить напрямую. В статье Hugging Face, вероятно, рассматривается случай, когда активации вынесены за пределы fused-слоя или используется специальная структура. Для простоты изложения можно сказать, что fused-слой объединяет матричные умножения, а активации применяются отдельно. В любом случае, ключевая идея — уменьшение числа вызовов ядер.
Кого затронет Разработчиков, оптимизирующих инференс моделей PyTorch, особенно тех, кто работает с трансформерами (BERT, GPT, LLaMA). Метод полезен для исследователей и инженеров, занимающихся развёртыванием моделей на GPU. Также он может быть интересен специалистам по оптимизации производительности, которые ищут способы ускорить модели без изменения архитектуры.
Что пока неизвестно Не приведены результаты для CPU, а также не обсуждается влияние на точность (теоретически fused-слой идентичен исходному). Не рассмотрены автоматические инструменты слияния, такие как torch.fx или TensorRT. Кроме того, остаётся открытым вопрос о применимости метода для моделей с различными функциями активации и нормализацией. В будущем возможно появление автоматизированных инструментов, которые будут выполнять такое слияние на уровне графа вычислений.