Проект ЦИТадель
Компьютерная графика нередко кажется прикладному программисту отдельным миром: матрицы, шейдерные языки, специализированные API и собственная архитектура процессора. Однако результат работы этого мира виден повсюду — от игр и карт до композиции веб-страницы. Те же графические процессоры ускоряют научные расчёты и многие операции машинного обучения. Статья отвечает на три вопроса: как описание сцены превращается в кадр, чем GPU отличается от центрального процессора и почему пригодная для графики архитектура оказалась полезна для других массово-параллельных вычислений.
Раздел 1 оценивает масштаб работы. Раздел 2 объясняет ориентацию GPU на пропускную способность, раздел 3 проводит геометрию через графический конвейер, а раздел 4 рассматривает программируемые шейдеры. Раздел 5 вводит арифметическую интенсивность и ограничения памяти. Раздел 6 разбирает способы не выполнять невидимую работу и сопоставляет растеризацию с трассировкой лучей. Раздел 7 связывает архитектуру GPU с машинным обучением, раздел 8 предлагает практикум в браузере.
Кадр 4K содержит 3840 × 2160, то есть около 8,3 миллиона пикселей. При частоте 60 Гц экран получает почти 498 миллионов итоговых значений в секунду. Реальное число фрагментных вычислений не равно этому числу: часть геометрии отбрасывается заранее, один пиксель могут закрывать несколько примитивов, а сглаживание, масштабирование и постобработка добавляют работу.
Тем не менее во многих стадиях одна и та же программа применяется к большому числу вершин, фрагментов или элементов изображения. Их вычисления часто можно выполнять параллельно, хотя полной независимости нет: используются общие текстуры и буферы, смешение цветов зависит от уже записанного результата, а экранные производные связывают соседние вызовы. Центральный процессор также умеет параллельные и векторные вычисления, но значительную часть его ресурсов занимают механизмы, уменьшающие задержку одной или нескольких сложных нитей: крупные кэши, предсказание переходов и внеочередное исполнение. GPU отдаёт больше ресурсов массовому исполнению сходных операций. Это не замена CPU, а другое соотношение между задержкой, управлением и пропускной способностью.
GPU рассчитан прежде всего на высокую пропускную способность множества параллельных вызовов. Из этого следуют две важные особенности:
Полезное приближение таково: CPU оптимизирует задержку небольшого числа сложных потоков, GPU — суммарную пропускную способность большого числа сходных вызовов. Реальные процессоры сочетают оба набора приёмов, а приложение обычно оставляет последовательное управление и разнородную работу на CPU, передавая GPU достаточно крупные параллельные задачи. Связь этой модели с потоками и общей памятью подробнее разобрана в статье «Конкурентность в современных языках».
Приложение передаёт GPU команды, буферы с геометрией, текстуры и параметры. Поверхности обычно представлены сетками треугольников: три не лежащие на одной прямой вершины задают плоскость, а сложную форму можно приблизить множеством таких простых примитивов. Графические API поддерживают также точки и линии, а современные конвейеры могут создавать примитивы дополнительными стадиями, но базовый путь удобно рассмотреть на треугольнике:
Это упрощённая схема: в конкретном API присутствуют дополнительные стадии и оптимизации. Нормативное описание растеризации и порядка операций можно найти в спецификации Vulkan. На сайте уже рассмотрен другой уровень той же цепочки: статья «Как устроен браузер» объясняет, как отрисованные слои передаются композитору. Перенос анимации в композицию часто разгружает главный поток, но конкретное использование GPU остаётся решением браузера и драйвера.
Ранние графические ускорители предоставляли в основном фиксированный набор операций. Программируемые вершинные и фрагментные стадии превратили конвейер в платформу: разработчик пишет функцию одного вызова, а API запускает её для множества вершин или фрагментов. Вычислительные шейдеры распространили ту же модель на задачи, не связанные непосредственно с рисованием.
Полезная исходная модель — независимое применение одной функции к множеству элементов, но современный шейдер не обязан быть чистой функцией. Он может читать общие ресурсы, а разрешённые стадии — записывать буферы и текстуры или использовать атомарные операции. Фрагментные вызовы могут совместно участвовать в вычислении экранных производных. Такие возможности требуют правил памяти и синхронизации, изложенных, например, в спецификации WGSL. Ветвление тоже не запрещено: оно становится дорогим, когда нити одной аппаратной группы идут разными путями и обе ветви содержат существенную работу. Поэтому производительность шейдера проверяют профилировщиком, а не выводят только из наличия if.
Производительность может быть ограничена вычислительными блоками, пропускной способностью памяти, задержкой, числом доступных параллельных вызовов или другой частью конвейера. Для первого приближения полезна арифметическая интенсивность — число операций на байт данных, перенесённых через рассматриваемый уровень памяти. Модель Roofline ограничивает достижимую производительность меньшей из двух величин: пиком вычислений и произведением пропускной способности памяти на арифметическую интенсивность. Следовательно, задача с низкой интенсивностью чаще ограничена движением данных, а с высокой — вычислениями, если она достаточно велика для загрузки устройства.
У GPU есть иерархия регистров, разделяемой памяти вычислительных групп, кэшей и внешней памяти. Повторное использование данных на близком уровне, согласованный доступ соседних нитей, компактные форматы вершин и сжатие текстур уменьшают внешний трафик. Но универсального правила «память всегда является пределом» нет: границу определяют конкретный алгоритм, устройство и уровень иерархии. Исходная модель подробно описана в работе о Roofline. Связанные вопросы представления данных рассматриваются в статье «Как современные языки управляют памятью».
Первый способ ускорить кадр — не обрабатывать то, что не повлияет на результат. Приложение или GPU отбрасывает объекты вне объёма видимости, невидимые стороны и геометрию, закрытую другими объектами. Уровни детализации заменяют далёкую модель более простой, а цепочка уменьшенных копий текстуры (mipmap) предоставляет представления для разных масштабов. Ранний тест глубины способен остановить часть фрагментов до дорогого шейдера, если его семантика это допускает. Эти приёмы уменьшают разную работу и применяются на разных стадиях, поэтому их эффект измеряют отдельно.
Растеризация преобразует примитивы в покрытые ими экранные выборки и особенно эффективна для первичной видимости. Трассировка лучей ищет пересечения лучей со сценой, обычно используя ускоряющую структуру, и естественнее выражает отражения, тени и непрямое освещение. Но трассировка сама по себе не гарантирует физической точности: результат зависит от модели материалов, числа выборок, числа отражений и способа подавления шума. Современный кадр может сочетать растеризацию, отдельные лучевые эффекты, временное накопление, масштабирование и восстановление изображения. Это не спор двух взаимоисключающих методов, а выбор способа получить приемлемое изображение в заданный бюджет времени.
Во многих нейронных сетях значительная доля времени приходится на умножения матриц и свёртки. Такие операции можно разбить на множество параллельно вычисляемых блоков и повторно использовать загруженные данные. Поэтому они хорошо соответствуют ориентации GPU на пропускную способность. Универсальные вычислительные API избавили разработчиков от необходимости представлять неграфические данные как текстуры и проводить расчёт через графические стадии, а специализированные матричные блоки ускорили операции умножения с накоплением.
Совпадение не полное. Функции активации, нормализация, редукции, обмен между устройствами и небольшие матричные операции часто ограничены памятью или задержкой. Даже матричное умножение при малом размере пакета может не загрузить GPU. Поэтому скорость обучения или вывода определяется не словом «матрица», а размером задачи, повторным использованием данных, точностью чисел, обменами и долей операций, подходящих устройству. Эти различия показаны в официальном руководстве по производительности вычислений для глубокого обучения. Тематические материалы находятся в разделе «Искусственный интеллект»; здесь достаточно главного вывода: графика создала массовый рынок параллельных ускорителей, а программируемость и развитая экосистема сделали их доступными другим областям.
Для первых пяти заданий достаточно браузера, песочницы фрагментных шейдеров и инструментов разработчика. Шестое требует браузера с поддержкой WebGPU.
Задание 1. Функция одного фрагмента. Залейте экран одним цветом, затем постройте горизонтальный градиент из координаты фрагмента. Нарисуйте круг по расстоянию до центра и изменяйте его радиус со временем. Опишите, что задаёт ваша функция, а что делает среда: полноэкранный примитив, растеризацию и множество вызовов шейдера.
Задание 2. Расхождение ветвей. Подготовьте два варианта условного шейдера с одинаковой полезной работой. В первом условие должно быть одинаковым для больших соседних областей, во втором — чередоваться между соседними фрагментами. Убедитесь, что компилятор не удалил результат вычислений, прогрейте оба варианта и проведите несколько измерений. Если песочница показывает только частоту кадров, учитывайте её грубость и ограничение частотой экрана. Не делайте универсального вывода по одному GPU: объясните результат через расхождение внутри аппаратных групп и возможные оптимизации компилятора.
Задание 3. Преобразование вершины. Выберите соглашение о векторах и порядке умножения матриц. В однородных координатах поверните точку, перенесите её, примените матрицу вида и проекции, затем выполните перспективное деление. Перемножьте матрицы заранее и сравните результат. Отдельно объясните, почему перенос нельзя представить обычной матрицей 3 × 3 над трёхмерным вектором без однородной координаты.
Задание 4. Композиция в браузере. В инструментах разработчика включите подсветку перерисовок и, если доступно, отображение композиционных слоёв. Сравните анимацию transform или opacity с изменением свойства, вызывающего раскладку. Запишите, какие стадии повторились. Не считайте наличие отдельного слоя доказательством конкретной реализации на GPU: решение зависит от браузера, драйвера и ресурсов.
Задание 5. Оценка масштаба. Вычислите число экранных выборок в секунду для своего разрешения и частоты обновления. Затем составьте несколько сценариев: без перекрытия, с заданным коэффициентом перекрытия и с несколькими выборками сглаживания. Для каждого явно запишите допущения и оцените хотя бы объём чтения и записи цвета и глубины. Сравнивать этот результат следует с измеренными характеристиками устройства или данными профилировщика, а не с одной тактовой частотой CPU.
Задание 6*. Неграфические вычисления. В примере WebGPU сложите два больших массива. Отделите подготовку конвейера и передачу данных от повторных запусков, прогрейте реализацию и проверьте несколько размеров. Измерьте как время самой работы GPU, если доступна возможность timestamp-query, так и полное время с загрузкой и чтением результата; иначе дождитесь завершения очереди и явно назовите ограничение измерения. Сравните с циклом над типизированными массивами JavaScript. Объясните, почему сложение массивов ограничено в основном памятью и почему граница окупаемости передачи работы зависит от устройства и браузера.