Введение в GPU¶
GPU играют ключевую роль в высокопроизводительных вычислениях, но исторически их программирование было узкоспециализированным навыком. Mojo даёт возможность переосмыслить программирование GPU и сделать его более доступным. Однако если вы никогда раньше не программировали под GPU, сначала необходимо понять, чем аппаратная часть и модель выполнения GPU отличаются от CPU. Именно этому и посвящена эта страница — здесь нет кода, поэтому если вы уже знакомы с архитектурой GPU, можете сразу перейти к руководству Get started или к разделу Основы программирования GPU.
Обзор архитектуры GPU¶
Графические процессоры (GPU) и центральные процессоры (CPU) представляют собой принципиально разные подходы к вычислениям. В то время как CPU имеют несколько мощных ядер, оптимизированных для последовательной обработки и сложной логики принятия решений, GPU содержат тысячи более простых и мелких ядер, предназначенных для параллельной обработки. Эти простые ядра не обладают такими сложными механизмами, как предсказание ветвлений или глубокие конвейеры инструкций, но отлично подходят для одновременного выполнения одной и той же операции над большими объёмами данных. Современные системы используют сильные стороны и CPU, и GPU: CPU отвечает за основной поток программы и сложную логику, а параллельные вычисления передаются на GPU через специализированные API. На рисунке 1 показаны некоторые архитектурные различия между ними, которые мы подробнее рассмотрим ниже.

Рисунок 1. Высокоуровневое сравнение архитектуры CPU и GPU, на основе руководства CUDA C++ Programming Guide.¶
Базовый строительный блок GPU называется streaming multiprocessor (SM) у NVIDIA или compute unit (CU) у AMD (это одна и та же концепция, и далее мы будем называть их обоих SM). SM располагаются между высокоуровневой логикой управления GPU и отдельными исполнительными блоками, выступая в роли автономных вычислительных «фабрик», которые могут работать независимо и параллельно.
AMD и NVIDIA используют разную терминологию для описания своего GPU-железа и моделей программирования. Для простоты в руководстве Mojo в основном используется терминология NVIDIA, так как она более распространена в индустрии.
Несколько SM размещаются на одном чипе GPU, причём каждый SM способен обрабатывать несколько задач одновременно. Глобальный планировщик GPU распределяет работу между отдельными SM, а контроллер памяти управляет потоками данных между SM и различными уровнями памяти (глобальная память, кэш L2 и т.д.).
Количество SM в GPU может сильно различаться в зависимости от модели и предполагаемого применения: от нескольких в начальных видеокартах до десятков и даже сотен в профессиональных высокопроизводительных устройствах. Такая масштабируемая архитектура позволяет GPU сохранять высокую производительность при разных размерах и типах нагрузок.
Каждый SM содержит несколько ключевых компонентов:
- CUDA Cores (NVIDIA) / Stream Processors (AMD): базовые арифметико-логические устройства (ALU), выполняющие целочисленные и вещественные вычисления. Один SM может содержать десятки или сотни таких ядер.
- Tensor Cores (NVIDIA) / Matrix Cores (AMD): специализированные блоки, оптимизированные для операций матричного умножения и свёртки.
- Special Function Units (SFUs): обрабатывают сложные математические операции, такие как тригонометрические функции, квадратные корни и экспоненты.
- Register Files: сверхбыстрая память для хранения промежуточных результатов и данных отдельных потоков. Современные SM могут иметь сотни килобайт регистрового пространства, разделяемого между активными потоками.
- Shared Memory / L1 Cache: программируемая память с низкой задержкой, позволяющая потокам обмениваться данными. Обычно может настраиваться как разделяемая память или как кэш L1.
- Load/Store Units: управляют перемещением данных между различными областями памяти, обрабатывая запросы потоков на доступ к памяти.

Рисунок 2. Высокоуровневая архитектура потокового мультипроцессора (SM).¶
Модель выполнения GPU¶
Ядро GPU (GPU kernel) — это просто функция, которая выполняется на GPU и параллельно обрабатывает большой набор данных с использованием тысяч или миллионов потоков (на GPU AMD они также называются work items). Возможно, вы уже знакомы с потоками при программировании под CPU, но потоки GPU устроены иначе. На CPU потоки управляются операционной системой и могут выполнять полностью независимые задачи, такие как управление пользовательским интерфейсом, получение данных из базы данных и т.п. На GPU же потоки управляются самим GPU. Для одной функции-ядра все потоки на GPU выполняют одну и ту же функцию, но каждый из них работает со своей частью данных. Сетка (grid) — это верхнеуровневая организационная структура потоков, выполняющих ядро на GPU. Сетка состоит из нескольких блоков потоков (thread blocks) (или workgroups на GPU AMD), которые в свою очередь делятся на отдельные потоки, выполняющие функцию-ядро параллельно.

Рисунок 3. Иерархия потоков, выполняющихся на GPU, показывающая взаимосвязь между сеткой (grid), блоками потоков, варпами и отдельными потоками, на основе HIP Programming Guide.¶
Разбиение сетки на блоки потоков служит нескольким целям:
- Оно разбивает общую нагрузку — управляемую сеткой — на более мелкие и удобные для обработки части, которые могут выполняться независимо. Это позволяет эффективнее использовать ресурсы и обеспечивает гибкость планирования между несколькими SM в GPU.
- Блоки потоков задают область, в рамках которой потоки могут взаимодействовать через разделяемую память и примитивы синхронизации, что позволяет реализовывать эффективные параллельные алгоритмы и схемы обмена данными.
- Блоки потоков способствуют масштабируемости, позволяя одной и той же программе эффективно работать на разных архитектурах GPU, поскольку оборудование может автоматически распределять блоки в зависимости от доступных ресурсов.
Вы должны указать количество блоков потоков в сетке и то, как они располагаются в одном, двух или трёх измерениях. Обычно размеры сетки выбираются исходя из размерности обрабатываемых данных. Например, можно выбрать одномерную сетку для обработки больших векторов, двумерную — для обработки матриц и трёхмерную — для обработки кадров видео. GPU присваивает каждому блоку в сетке уникальный индекс блока, который определяет его положение в сетке.
Аналогично, необходимо указать количество потоков в каждом блоке и их расположение в одном, двух или трёх измерениях. GPU также присваивает каждому потоку внутри блока уникальный индекс потока, определяющий его положение в блоке. Комбинация индекса блока и индекса потока однозначно определяет позицию потока во всей сетке.
Когда GPU назначает блок потоков для выполнения на SM, SM делит этот блок на подмножества потоков, называемые варпами (или wavefront на GPU AMD). Размер варпа зависит от архитектуры GPU, но большинство современных GPU используют размер варпа 32 или 64 потока.
Если количество потоков в блоке не делится на размер варпа без остатка, SM создаёт последний частично заполненный варп, который всё равно потребляет ресурсы как полноценный варп. Например, если блок содержит 100 потоков, а размер варпа равен 32, SM создаёт:
- 3 полных варпа по 32 потока (всего 96 потоков);
- 1 частичный варп с 4 активными потоками, но занимающий ресурсы, эквивалентные 32 потокам.
SM фактически отключает неиспользуемые слоты потоков в частичных варпах, однако эти слоты всё равно потребляют аппаратные ресурсы. Поэтому обычно рекомендуется выбирать размер блоков кратным размеру варпа, чтобы оптимизировать использование ресурсов.
Каждый поток в варпе выполняет одну и ту же инструкцию одновременно над разными данными, следуя модели выполнения SIMT (single instruction, multiple threads). Если потоки внутри варпа идут по разным путям выполнения (это называется дивергенцией варпа), варп последовательно выполняет каждый путь, отключая потоки, которые в данный момент не участвуют. Это означает, что наилучшая производительность достигается тогда, когда все потоки в варпе следуют одному и тому же пути выполнения.
SM может одновременно активно управлять несколькими варпами из разных блоков потоков, что помогает поддерживать загрузку исполнительных устройств. Например, планировщик варпов может быстро переключиться на другой готовый варп, если потоки текущего варпа вынуждены ожидать доступа к памяти.
Варпы дают несколько ключевых преимуществ по производительности:
- Аппаратуре нужно управлять варпами, а не отдельными потоками, что снижает накладные расходы на планирование.
- Потоки в варпе могут эффективно обращаться к непрерывным участкам памяти благодаря коалесценции памяти.
- Аппаратура автоматически синхронизирует потоки внутри варпа, устраняя необходимость в явной синхронизации.
- Планировщик варпов может скрывать задержки доступа к памяти, переключаясь между варпами и максимально используя вычислительные ресурсы.
До этого момента мы рассмотрели основные концепции устройства GPU и того, как GPU выполняет параллельные процессы с использованием потоков, но без примеров кода. Чтобы начать программировать GPU с помощью Mojo, вы можете либо прочитать раздел Основы программирования GPU для обзора модели программирования Mojo, либо сразу перейти к практике с руководством Get started with GPU programming.