MAATRIX / Блог / У видеокарты тысячи ядер: почему это не даёт ускорения в тысячу раз

У видеокарты тысячи ядер: почему это не даёт ускорения в тысячу раз

MAATRIX

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

Почему ядро GPU и ядро CPU — это не одно и то же

Когда говорят «ядро процессора», обычно имеют в виду самостоятельный вычислительный блок: у него своя логика управления, свой конвейер инструкций, он может выполнять произвольную программу независимо от соседних ядер. Современный серверный CPU с несколькими десятками ядер — это буквально несколько десятков независимых исполнителей, каждый со своим потоком команд.

«Ядро» GPU (CUDA-ядро у NVIDIA, потоковый процессор у AMD) — это совсем другая единица. Это простое арифметико-логическое устройство: оно умеет выполнять базовые операции над числами (сложение, умножение, часто — операцию умножения со сложением за один такт), но у него нет собственного блока выборки и декодирования инструкций. Инструкцию ему подаёт извне управляющий блок, общий на целую группу таких ядер. Одно GPU-ядро физически не может «пойти своим путём» и выполнять отдельную от соседей программу — у него для этого просто нет нужной части схемы.

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

SIMT: как одна инструкция выполняется целой группой ядер

Модель исполнения GPU называется SIMT — Single Instruction, Multiple Threads, «одна инструкция — много потоков». Ядра видеокарты объединены в группы (у NVIDIA такая группа называется warp, у AMD — wavefront), и все ядра внутри одной группы в любой конкретный такт выполняют одну и ту же инструкцию, но каждое — над своими данными.

Это принципиально не то же самое, что независимое многопоточное исполнение на CPU, где каждое ядро может в этот момент находиться в совершенно разных точках совершенно разных программ. На GPU управляющий блок один на группу: он говорит «все — сложить», и все ядра группы одновременно выполняют сложение, просто каждое над своим элементом массива. Следующий такт — «все умножить», и снова все ядра группы синхронно делают одно и то же действие.

Это отлично работает, когда задача по своей природе однородна: нужно применить одну и ту же операцию к огромному массиву данных — умножить каждый пиксель изображения на коэффициент, применить одну и ту же функцию активации к миллиону чисел в нейросети, посчитать одно и то же уравнение для каждой ячейки сетки в физической симуляции. Такие задачи называют «широко параллельными» (data-parallel), и именно на них GPU показывает свою силу — там, где CPU честно выполнял бы миллион последовательных проходов, GPU выполняет их пачками по размеру группы одновременно.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Закон Амдала: предел, который не зависит от числа ядер

Даже если бы каждое ядро GPU было полностью независимым (а оно, как мы выяснили, не независимо), рост числа ядер всё равно не давал бы линейного роста скорости. Здесь работает более общий принцип, известный как закон Амдала: он говорит, что итоговое ускорение всей программы ограничено долей, которую вообще можно выполнить параллельно, а не количеством исполнителей, которые вы на эту долю бросите.

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

Если условно обозначить долю параллельной части задачи как p, а число исполнителей как N, закон Амдала формулируется как:

Ускорение = 1 / ((1 - p) + p / N)

Смысл в предельном переходе: при N, стремящемся к бесконечности, второе слагаемое в знаменателе стремится к нулю, и всё упирается в (1 - p) — долю последовательной части. Если, например, 95% задачи параллелится идеально, а 5% — принципиально последовательны, то при бесконечном числе ядер максимально достижимое ускорение — 1 / 0.05 = 20 раз, а не тысячи. Это не измеренная цифра конкретного приложения, а иллюстрация самой формулы — для реальной программы доля p всегда своя, и её нужно оценивать отдельно, профилируя конкретный код.

Отсюда практический вывод: прежде чем ждать от GPU кратного ускорения, стоит спросить не «сколько там ядер», а «какую долю моей задачи я реально могу выполнить параллельно, а что обязано идти последовательно». Это же рассуждение работает и для многоядерных CPU-программ — про то, как закон Амдала и накладные расходы синхронизации иногда не ускоряют, а замедляют программу при добавлении потоков, подробно разобрано в статье про многопоточность, которая иногда только вредит.

Расхождение ветвей: когда SIMT перестаёт быть параллельным

Вернёмся к тому, что все ядра внутри одной группы (warp/wavefront) в каждый такт выполняют одну и ту же инструкцию. Что происходит, если в коде есть условие — if / else — и часть потоков группы должна пойти по одной ветке, а часть по другой?

Аппаратно у GPU нет механизма, чтобы часть ядер группы в этот такт делала одно, а часть — совсем другое: инструкцию группе подаёт один управляющий блок. Поэтому происходит следующее: сначала выполняется одна ветка — при этом ядра, которым она не нужна, простаивают (их результат маскируется и не учитывается), затем выполняется вторая ветка — и теперь простаивают те, для кого нужна была первая. Итоговое время выполнения группы — это сумма времени обеих веток, а не время только нужной каждому ядру ветки. Это называется расхождением потоков (branch divergence), и оно съедает часть теоретической параллельности ровно пропорционально тому, насколько сильно потоки внутри группы «спорят» о том, какой код выполнять.

Для задач с предсказуемым, однородным потоком управления (одна и та же арифметика для каждого элемента) это почти не заметно. Но для кода, где решение зависит от значения данных — обработка событий разных типов, разбор ветвистой бизнес-логики, алгоритмы с ранним выходом по условию, — расхождение может съесть значительную часть выигрыша от параллельности, причём тем сильнее, чем более случайным образом данные распределяются между ветками внутри одной группы. Это одна из ключевых причин, почему GPU хорош для «плотной», регулярной арифметики и плохо подходит для кода с большим количеством условной логики — такой код проще и часто быстрее выполняется на CPU, где каждое ядро честно идёт своей веткой независимо от соседей.

Накладные расходы: данные нужно ещё довезти до ядер

Ядра GPU считают только то, что уже лежит в видеопамяти. Прежде чем начать вычисления, данные нужно скопировать с CPU в GPU через шину (обычно PCIe), а после вычислений — забрать результат обратно. Эта пересылка не бесплатна и не мгновенна, и для многих задач именно она, а не сами вычисления, определяет итоговое время.

Если объём вычислений на единицу переданных данных большой (например, множество итераций над одним и тем же загруженным в память набором данных — типичный случай для обучения нейросети на батче), накладные расходы на пересылку размываются на фоне полезной работы и почти не заметны. Но если задача — «взять немного данных, быстро что-то посчитать, отдать результат, взять следующую порцию» с частыми обменами между CPU и GPU, то время на пересылку и синхронизацию может оказаться сопоставимо с самими вычислениями или даже превысить их — тогда тысячи ядер простаивают, ожидая данные, а не считают.

То же самое происходит и внутри самого GPU: ядра ждут не только данные из системной памяти, но и результаты друг друга, если задача требует синхронизации между группами. А если на одной видеокарте одновременно работает несколько процессов, они конкурируют за пропускную способность памяти и за место в ней — это отдельная тема, которая подробно разобрана в статье о том, как несколько процессов делят одну видеокарту и почему один тормозит другого. Итог тот же: количество ядер — это верхняя граница вычислительной мощности, а не гарантия, что она будет использована.

Что из этого следует на практике

Собрав всё вместе, можно сформулировать практическое правило: GPU даёт близкое к пропорциональному числу ядер ускорение только на задачах, которые одновременно (а) регулярны — одна и та же операция применяется к огромному количеству независимых элементов данных, (б) почти не содержат ветвлений, зависящих от значений данных, и (в) держат достаточно вычислений на единицу переданных данных, чтобы окупить пересылку через шину. Классический пример — плотная линейная алгебра и обучение/инференс нейросетей, где именно поэтому GPU и стал стандартом: там доля параллелизуемой части close к 100%, ветвления минимальны, а данные (веса модели) переиспользуются много раз на каждую пересылку. Общее сравнение того, почему GPU в принципе быстрее CPU именно для нейросетевых вычислений, разобрано в статье почему GPU быстрее CPU для нейросетей.

Задачи с большой долей последовательной логики, частыми условными переходами, зависящими от данных, или с маленьким объёмом вычислений на каждую порцию переданных данных выигрывают на GPU намного меньше, чем обещает число ядер в характеристиках — иногда выигрыш скромный, а иногда GPU оказывается вообще не лучшим выбором по сравнению с многоядерным CPU. Прежде чем переносить конкретную нагрузку на GPU, стоит понять, какая доля её параллелится в принципе (закон Амдала задаёт потолок), насколько регулярен поток управления (влияние расхождения ветвей) и сколько вычислений приходится на каждый мегабайт данных, который нужно передать (влияние накладных расходов на пересылку). Если несколько процессов планируется размещать на одной видеокарте одновременно, стоит заранее прикинуть, сколько параллельных пользователей реально выдержит одна GPU без деградации по памяти и по конкуренции за вычислительные блоки.

Практически это означает: не стоит выбирать конфигурацию сервера только по числу ядер GPU в спецификации. Для типовых задач — обучение и инференс моделей, обработка изображений и видео, научные расчёты на регулярных сетках — ориентир на количество ядер оправдан, потому что эти задачи и правда хорошо ложатся на модель SIMT. Для нагрузок с ветвистой логикой или частым обменом небольшими порциями данных лучше сначала прикинуть реальную параллелизуемую долю задачи (хотя бы приблизительно, профилированием на меньшем масштабе), а не экстраполировать линейно от числа ядер.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Если у видеокарты в 50 раз больше ядер, чем у процессора, значит она и быстрее в 50 раз?

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

Что такое warp или wavefront простыми словами?

Это группа ядер GPU (обычно несколько десятков), которые в каждый момент времени выполняют одну и ту же инструкцию, но над разными данными. Управляющая логика — одна на всю группу, а не своя у каждого ядра.

Почему в коде с if/else GPU работает медленнее, чем ожидалось?

Потому что при расхождении веток (branch divergence) группа ядер вынуждена последовательно выполнить все ветки, которые нужны хоть кому-то из потоков группы, простаивая на тех ветках, которые ей не нужны — вместо того чтобы каждое ядро честно шло своей веткой параллельно.

Закон Амдала — это про GPU или про многопоточность вообще?

Это общий принцип для любого параллельного вычисления, включая многопоточные программы на CPU. Он говорит, что предельное ускорение ограничено долей задачи, которую в принципе можно распараллелить, независимо от того, сколько исполнителей — ядер CPU или GPU — вы добавляете.

Как понять заранее, выиграет ли моя задача от переноса на GPU?

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

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →