Иерархия памяти CUDA

advertisement
Нижегородский государственный университет
им. Н.И. Лобачевского
Факультет Вычислительной математики и кибернетики
Исполнение потоков.
Иерархия памяти CUDA
Бастраков С.И.
Содержание



Исполнение потоков
Иерархия памяти CUDA
Пример: параллельная редукция
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
2
Исполнение потоков
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
3
Основные понятия
Большое количество потоков (thread) параллельно
выполняют одну и ту же функцию – ядро (kernel).
– На Fermi одновременно может исполняться до 4
разных ядер. На более старых архитектурах
одновременно может исполняться только 1 ядро.
 Потоки группируются в блоки (thread blocks).
 Каждый блок исполняется на одном мультипроцессоре,
его потоки – на CUDA-ядрах данного мультипроцессора.
 Блоки объединяются в решетку/сетку блоков (grid).
 Ядро выполняется на решетке из блоков.
 Размер блока и размер решетки блоков задается при
вызове ядра.

Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
4
Идентификаторы
Каждый поток и блок потоков имеют идентификаторы
– Каждый поток и блок могут (и должны) определить, с
какими данными они должны работать.
 Block ID (1D, 2D или 3D).
– Третья компонента не использовалась до CUDA 4.0.
 Thread ID (1D, 2D или 3D).
 Многомерная индексация может упрощать
декомпозицию многомерных данных.
 Пример: ядро выполняет умножение матриц, каждый
поток вычисляет один элемент результирующей матрицы.
Удобно использовать двумерные индексы, например, xкомпоненту для номеров строк и y-компоненту для
столбцов.

Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
5
Идентификаторы
Источник: NVIDIA CUDA C Programming Guide v. 4.0
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
6
Взаимодействие потоков



Потоки внутри одного блока выполняются на одном
мультипроцессоре, они способны взаимодействовать
между собой посредством:
– разделяемой памяти ;
– точек синхронизации.
Два потока из различных блоков могут взаимодействовать
лишь через глобальную память.
Атомарные функции для разделяемой и глобальной
памяти.
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
7
Выполнение блоков




Автоматическое распределение блоков на
мультипроцессоры.
Каждый блок целиком выполняется одним
мультипроцессором.
При наличии достаточного количества ресурсов,
несколько блоков могут «одновременно» исполняться на
одном мультипроцессоре.
Наличие большого количества блоков открывает
возможности для автоматической масштабируемости с
ростом числа мультипроцессоров.
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
8
Автоматическая масштабируемость
Источник: NVIDIA CUDA C Programming Guide v. 4.0
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
9
SIMT




Все потоки, выполняющиеся на одном мультипроцессоре,
группируются в варпы (warp), в варп попадают потоки с
последовательными идентификаторами.
– Размер варпа на текущих архитектурах равен 32.
Выполнение производится варпами, аппаратный
планировшик на мультипроцессоре
– На Fermi 2 планировщика на мультипроцессор.
CUDA-ядра данного мультипроцессора параллельно
выполняют одну и ту же инструкцию для всех потоков
варпа (SIMT, Single Instruction Multiple Thread).
– Потоки одного варпа всегда синхронизованы.
Программирование в скалярных терминах (скалярный код
для каждого потока, возможность ветвлений).
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
10
Пример выполнения варпов
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
11
Выполнение ветвлений
Все потоки варпа исполняют одну инструкцию, поэтому
одновременное исполнение ими разных веток условных
выражений невозможно.
 При возникновении таких ветвлений последовательно
исполняются все их ветки.
 В зависимости от выполнения условия входа в очередную
ветку потоки делятся на активные и неактивные.
 С точки зрения планировщика все потоки работают как
SIMD, операции доступа к памяти для неактивных
потоков не выполняются.
 Таким образом, логически выполнение ветвлений
происходит корректно, время выполнения примерно равно
суммарному времени выполнения всех ветвей.

Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
12
Оптимизация загрузки устройства





Число потоков на мультипроцессор обычно гораздо больше
числа CUDA-ядер.
Большое число потоков обеспечивает скрытие латентности
доступа к памяти, особенно глобальной.
Степень загрузки устройства (occupancy) – отношение
количества варпов, работающих на мультипроцессоре, к
максимально возможному количеству варпов.
Ограничивающим для числа потоков фактором является
количество разделяемой памяти и регистров.
При наличии достаточного количества ресурсов несколько
блоков могут одновременно исполняться на одном
мультипроцессоре.
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
13
Иерархия памяти CUDA
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
14
Типы памяти и скорость доступа
Тип памяти Доступ
Уровень
выделения
Скорость работы
Регистры
R/W
Per-thread
Высокая (on-chip)
Локальная
R/W
Per-thread
Низкая (DRAM)
Разделяемая
R/W
Per-block
Высокая (on-chip)
Глобальная*
R/W
Per-grid
Низкая (DRAM)
Константная
R/O
Per-grid
Высокая при попадании в кэш
Текстурная
R/O
Per-grid
Высокая при попадании в кэш
*+ L1/L2 кэш на Fermi
Источник: А.В. Боресков, А.А. Харламов «Архитектура и программирование
массивно-параллельных вычислительных систем»
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
15
Использование локальной памяти



Локальная память логически эксклюзивна для потока.
Физически она расположена в области памяти устройства
и имеет ту же латентность, что и глобальная память.
Обычно служит для хранения локальных переменных
потоков, не убирающихся в регистры. Часто компилятор
помещает локальные статические массивы в локальную
память.
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
16
Использование константной памяти
Константная память служит для хранения данных, не
меняющихся во время исполнения ядер.
 Значения можно инициализировать при определении либо
копировать со стороны хоста с использованием функций
API.
 Расположена в памяти устройства (как и глобальная
память), кэшируется на всех устройствах.
 Малый размер (десятки килобайт).
 Пример использования: хранение матрицы
коэффициентов косинус-преобразования для блоков
малого размера в алгоритмах кодирования изображений,
хранение матриц фильтров.

Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
17
Использование текстурной памяти
Текстуры широко применяются в компьютерной графике,
многие операции над ними поддерживаются аппаратно
(например, интерполяция).
 Текстурная память расположена в области памяти
устройства и кэшируется на всех устройствах.
 Оптимизирована для двумерной локальности доступа.
 С появлением архитектуры Fermi и кэша для глобальной
памяти стала использоваться гораздо реже.

Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
18
Использование глобальной памяти
Использование переменных с квалификатором __device__ аналог глобальных статических переменных, возможность
доступа к ним со стороны хоста.
 Выделение/освобождение памяти со стороны хоста с
использованием cudaMalloc/cudaFree.
 В последней версии CUDA добавлена возможность
динамического выделения памяти из ядер.
– При этом размер «кучи» определяется со стороны хоста.
 L1/L2 кэщ для глобальной памяти появился на архитектуре
Fermi.
 Латентность 400-600 тактов (без учета кэша).
 Оптимизация работы с глобальной памятью обычно имеет
очень большое значение.

Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
19
Эффективная работа с глобальной памятью





Доступ в глобальную память осуществляется полуварпами.
Каждый поток обращается к 32-, 64- или 128-битным словам.
При выполнении определенных условий происходит
объединение запросов (коалесцирование, coalescing) к
памяти всех потоков полуварпа в одну операцию доступа к
непрерывному блоку памяти и выполнение их одной
инструкцией.
В противном случае доступ полуварпа к памяти разбивается
на несколько последовательных доступов к непрерывным
блокам памяти.
Условия для объединения запросов зависят от
вычислительных возможностей, постепенно они становятся
все более мягкими.
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
20
Эффективная работа с глобальной памятью

Условия, необходимые для объединения запросов:
– Доступ в пределах одного последовательного участка
памяти определенного размера.
– Выровненность адресов, с которыми работает каждый
поток, по размеру типа. Выровненность автоматически
обеспечивается для встроенных векторных типов, для
обеспечения выровненности структур используется
__align__ (newsize) при их объявлении.
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
21
Пример доступа к глобальной памяти
Источник: NVIDIA CUDA C Programming Guide v. 4.0
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
22
Пример доступа к глобальной памяти
Источник: NVIDIA CUDA C Programming Guide v. 4.0
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
23
Пример доступа к глобальной памяти
Источник: NVIDIA CUDA C Programming Guide v. 4.0
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
24
Рекомендации по эффективной работе с
глобальной памятью




За счет объединения запросов можно добиться
значительного уменьшения времени на доступ к
глобальной памяти.
Объединение запросов заведомо невозможно, когда
адреса, по которым обращаются потоки одного полуварпа,
расположены далеко друг от друга.
Для обеспечения объединения запросов обычно стоит
предпочитать регулярные структуры данных
нерегулярным.
В случае независимой обработки вместо массива структур
обычно лучше использовать массивы отдельных
компонент.
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
25
Пример
/* Одна итерация метода Якоби для системы размера n на n: x0
– текущее приближение, x1 – следующее, матрица a хранится
по строкам, f – правая часть. Каждый поток вычисляет свой
элемент вектора x1, общее число потоков равно n. */
__global__ void kernel (float * a, float * f, float * x0,
float * x1, int n)
{
int idx = blockIdx.x * blockDim.x + threadIdx.x;
int ia = n * idx;
float sum = 0.0f;
for (int i = 0; i < n; i++)
sum += a[ia + i] * x0[i]; /* плохой вариант
доступа к a, лучше хранить ее по столбцам */
float alpha = 1.0f / a[ia + idx];
x1[idx] = x0[idx] + alpha * (f[idx] - sum);
}
Использованы материалы: А.В. Боресков, А.А. Харламов «Архитектура и
программирование массивно-параллельных вычислительных систем»
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
26
Работа с разделяемой памятью



Латентность 4 такта.
Малый размер (от 16 или 48kB).
Типичная схема использования разделяемой памяти для
уменьшения времени доступа к глобальной памяти:
– загрузка интенсивно используемых данных из
глобальной памяти;
– синхронизация (при необходимости);
– вычисления с использованием загруженных данных;
– синхронизация (при необходимости);
– запись результатов в глобальную память.
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
27
Пример

Всем потокам блока необходимо работать с определенным
участком массива в глобальной памяти. Каждый поток
загружает один элемент в соответствующий элемент массива
в разделяемой памяти. Считается, что число потоков равно
(или меньше чем) NUM_THREADS.
__global__ void kernel(int * a)
{
int globalIdx = blockIdx.x * blockDim.x + threadIdx.x;
__shared__ int shared_a[NUM_THREADS];
shared_a[threadIdx.x] = a[globalIdx];
__syncthreads();
// все потоки блока могут использовать shared_a
}
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
28
Динамически выделяемая разделяемая
память



Данные в разделяемой памяти, размер которых определяется
при запуске ядра (аналог динамических массивов).
Может использоваться внутри ядер и __device__-функций.
Синтаксис (только для динамически выделяемой
разделяемой памяти):
– Объявление переменной с квалификаторами extern
__shared__ перед телом функции.
– Получение адресов начал массивов при помощи сдвигов.
Начало каждого массива должно быть выровнено по
размеру типа.
– Указание общего размера используемой таким образом
разделяемой памяти в качестве третьего аргумента при
запуске ядра (по умолчанию 0).
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
29
Пример
Пусть мы хотим выделить массив array0 типа short длины
2n, массив array1 типа float длины n и массив array2 типа int
длины 3n.
extern __shared__ float array[];
__global__ void kernel(int n) {
short* array0 = (short*)array;
float* array1 = (float*)&array0[2 * n];
int* array2 = (int*)&array1[n];
}
…
int shared_mem_size = sizeof(short) * 2 * n + sizeof(float) * n +
sizeof(int) * 3 * n;
kernel<<<num_blocks, num_threads, shared_mem_size>>>(n);

Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
30
Эффективная работа с разделяемой
памятью
Разделяемая память разбита на банки (страницы) таким
образом, что последовательные 32-битные слова попадают
в последовательные банки.
 Каждый банк работает независимо от других, возможен
параллельный доступ к различным банкам.
 Доступ нескольких потоков к одному банку сериализуется
(происходит конфликт банков), исключение – чтение
всеми потоками данных из одного и того же банка.

Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
31
Эффективная работа с разделяемой
памятью

Пример:
__shared__ float x[32];
float data = x[BaseIndex + s * tid];

Пусть tid – thread ID, s – шаг доступа к элементам массива.
Потоки с ID tid и tid+n вызовут конфликт банков, если s*n
кратно числу банков m.
Для того, чтобы избежать конфликтов банков, необходимо
НОД(m, s) = 1.


Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
32
Доступ к разделяемой памяти без
конфликтов банков
Источник: NVIDIA CUDA C Programming Guide v. 4.0
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
33
Доступ к разделяемой памяти с
конфликтами банков
Источник: NVIDIA CUDA C Programming Guide v. 4.0
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
34
Пример: параллельная редукция
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
35
Постановка задачи




Дано:
– Массив a0 , a1 ,..., an1
– Ассоциативная операция «+» (например, +, *, min, max)
Необходимо найти: A  a0  a1  ...  an1
Лимитирующий фактор – доступ к памяти.
Источник материала данного раздела: пример reduction в
GPU Computing SDK и соответствующая статья; А.В.
Боресков, А.А. Харламов «Иерархия памяти CUDA.
Shared-память и ее эффективное использование.
Параллельная редукция».
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
36
Иерархическое суммирование


Распределяем исходный массив между блоками.
Каждый блок производит иерархическое суммирование в
разделяемой памяти.
5
3
7
8
-2
2
5
0
4
2
13
-5
-6
-1
2
1
-4
1
-3
4
-2
-6
9
-6
14
5
3
-3
6
0
14
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
37
Вариант 1
Thread
a
0
1
2
3
4
5
6
7
8
2
10
11
12
13
14
15
5
3
7
-2
2
0
4
-5
-6
2
1
-3
4
5
-6
3
8
3
5
-2
2
0
-1
-5
-4
2
-2
-3
9
5
-3
3
13
3
5
-2
1
0
-1
-5
-6
2
-2
-3
6
5
-3
3
14
3
5
-2
1
0
-1
-5
0
2
-2
-3
6
5
-3
3
14
3
5
-2
1
0
-1
-5
0
2
-2
-3
6
5
-3
3
s=1
s=2
s=4
s=8
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
38
Вариант 1
__global__ void reduce1 ( int * inData, int * outData )
{
__shared__ int data [BLOCK_SIZE];
int tid = threadIdx.x;
int i
= blockIdx.x * blockDim.x + threadIdx.x;
data [tid] = inData [i];
// load into shared memory
__syncthreads ();
for ( int s = 1; s < blockDim.x; s *= 2 ) {
if ( tid % (2*s) == 0 )
// heavy branching !!!
data [tid] += data [tid + s];
__syncthreads ();
}
if ( tid == 0 )
// write result of block reduction
outData[blockIdx.x] = data [0];
}
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
39
Вариант 2
__global__ void reduce2 ( int * inData, int * outData )
{
__shared__ int data [BLOCK_SIZE];
int tid = threadIdx.x;
int i
= blockIdx.x * blockDim.x + threadIdx.x;
data [tid] = inData [i];
// load into shared memory
__syncthreads ();
for ( int s = 1; s < blockDim.x; s <<= 1 )
{
int index = 2 * s * tid;
if ( index < blockDim.x )
data [index] += data [index + s];
__syncthreads ();
}
if ( tid == 0 )
// write result of block reduction
outData [blockIdx.x] = data [0];
}
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
40
Вариант 2



Отличие второго варианта от первого лишь в том, какие
потоки выполняют (ту же самую) работу.
Во втором варианте почти полностью избавились от
ветвления.
Однако на каждой следующей итерации циклов число
конфликтов страниц памяти удваивается:
– s = 1: конфликт страниц 2го порядка (index = 1 при tid =
0 и index = 17 при tid = 8);
– s = 2: конфликт страниц 4го порядка (index = 2 при tid =
0, index = 18 при tid = 4, index = 34 при tid = 8, index =
50 при tid = 12);
– …
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
41
Вариант 3

Изменим порядок суммирования: будем начинать его не с
ближайших, а с удаленных на blockDim.x/2 элементов и
будем уменьшать это расстояние вдвое на каждом шаге.
Thread
a
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
5
3
7
-2
2
0
4
-5
-6
2
1
-3
4
5
-6
3
-1
5
8
-5
6
5
-2
-2
-6
2
1
-3
4
5
-6
3
5
10
6
-7
6
5
-2
-2
-6
2
1
-3
4
5
-6
3
11
3
6
-7
6
5
-2
-2
-6
2
1
-3
4
5
-6
3
14
3
6
7
6
5
-2
-2
-6
2
1
-3
4
5
-6
3
s=1
s=2
s=4
s=8
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
42
Вариант 3
__global__ void reduce3 ( int * inData, int * outData )
{
__shared__ int data [BLOCK_SIZE];
int tid = threadIdx.x;
int i
= blockIdx.x * blockDim.x + threadIdx.x;
data [tid] = inData [i];
__syncthreads ();
for ( int s = blockDim.x / 2; s > 0; s >>= 1 )
{
if ( tid < s )
data [tid] += data [tid + s];
__syncthreads ();
}
if ( tid == 0 )
outData [blockIdx.x] = data [0];
}
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
43
Вариант 4


В варианте 3 избавились от конфликтов банков.
На первой итерации половина потоков простаивает, для
избежания этого сделаем первое суммирование при
загрузке.
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
44
Вариант 4
__global__ void reduce4 ( int * inData, int * outData )
{
__shared__ int data [BLOCK_SIZE];
int tid = threadIdx.x;
int i
= 2 * blockIdx.x * blockDim.x + threadIdx.x;
data [tid] = inData [i] + inData [i+blockDim.x]; // sum
__syncthreads ();
for ( int s = blockDim.x / 2; s > 0; s >>= 1 )
{
if ( tid < s )
data [tid] += data [tid + s];
__syncthreads ();
}
if ( tid == 0 )
outData [blockIdx.x] = data [0];
}
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
45
Вариант 5


При s <= 32 в блоке работает только один варп.
Можно развернуть цикл, избавившись от синхронизации и
проверки условия.
for ( int s = blockDim.x / 2; s > 32; s >>= 1 ) {
if ( tid < s )
data [tid] += data [tid + s];
__syncthreads ();
}
if ( tid < 32 ) { // unroll last iterations
data [tid] += data [tid + 32];
data [tid] += data [tid + 16];
data [tid] += data [tid + 8];
data [tid] += data [tid + 4];
data [tid] += data [tid + 2];
data [tid] += data [tid + 1];
}
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
46
Результаты
Вариант алгоритма
1
2
3
4
5

Время выполнения
(миллисекунды)
19.09
11.91
10.62
9.10
8.67
Возможна дальнейшая оптимизация: полное
разворачивание на этапе компиляции при помощи
параметров шаблонов. В примере в SDK данный вариант
реализован.
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
47
Материалы

NVIDIA CUDA C Programming Guide v. 4.0:
http://developer.download.nvidia.com/compute/cuda/4_0/tool
kit/docs/CUDA_C_Programming_Guide.pdf
 Материалы курса по CUDA в МГУ:
https://sites.google.com/site/cudacsmsusu/file-cabinet
 А.В. Боресков, А.А. Харламов «Основы работы с
технологией CUDA»:
https://sites.google.com/site/cudacsmsusu/file-cabinet
 Д. Сандерс, Э. Кэндрот «Технология CUDA в примерах:
введение в программирование графических процессоров»
(пер. с англ.).
Н. Новгород, 2011 г.
Исполнение потоков. Иерархия памяти CUDA
48
Download