Как устроена 2D разделяемая память в CUDA

#cuda

#cuda

Вопрос:

Я всегда работал с линейной разделяемой памятью (загрузка, хранение, доступ к соседям), но я провел простой тест в 2D для изучения банковских конфликтов, результаты которого меня смутили.

Следующий код считывает данные из одномерного массива глобальной памяти в общую память и копирует их обратно из общей памяти в глобальную память.

 __global__ void update(int* gIn, int* gOut, int w) {

// shared memory space
__shared__ int shData[16][16];
// map from threadIdx/BlockIdx to data position
int x = threadIdx.x   blockIdx.x * blockDim.x;
int y = threadIdx.y   blockIdx.y * blockDim.y;
// calculate the global id into the one dimensional array
int gid = x   y * w;

// load shared memory
shData[threadIdx.x][threadIdx.y] = gIn[gid];
// synchronize threads not really needed but keep it for convenience
__syncthreads();
// write data back to global memory
gOut[gid] = shData[threadIdx.x][threadIdx.y];
}
 

Визуальный профилировщик сообщил о конфликтах в разделяемой памяти. Следующий код позволяет избежать конфликтов (показывать только различия)

 // load shared memory
shData[threadIdx.y][threadIdx.x] = gIn[gid];

// write data back to global memory
gOut[gid] = shData[threadIdx.y][threadIdx.x];
 

Такое поведение меня смутило, потому что при программировании массивно параллельных процессоров. Практический подход, который мы можем прочитать:

матричные элементы в C и CUDA помещаются в линейно адресуемые местоположения в соответствии с соглашением об основных строках. То есть элементы строки 0 матрицы сначала размещаются по порядку в последовательных местоположениях.

Связано ли это с расширением разделяемой памяти? или с индексами потоков? Может быть, я что-то упускаю?

Конфигурация ядра выглядит следующим образом:

 // kernel configuration
dim3 dimBlock  = dim3 ( 16, 16, 1 );
dim3 dimGrid   = dim3 ( 64, 64 );
// Launching a grid of 64x64 blocks with 16x16 threads -> 1048576 threads
update<<<dimGrid, dimBlock>>>(d_input, d_output, 1024);
 

Заранее спасибо.

Комментарии:

1. Не могли бы вы добавить размеры блока, которые вы используете. Я предполагаю, что это (16,16,1), но хорошо бы подтвердить это, прежде чем отвечать.

2. @talonmies Я добавил конфигурацию / запуск ядра к вопросу. Как вы прокомментировали, я использую блок (16, 16, 1)

Ответ №1:

Да, разделяемая память упорядочена в порядке следования строк, как вы и ожидали. Итак, ваш массив [16] [16] хранится по строкам, что-то вроде этого:

        bank0 .... bank15
row 0  [ 0   .... 15  ]
    1  [ 16  .... 31  ]
    2  [ 32  .... 47  ]
    3  [ 48  .... 63  ]
    4  [ 64  .... 79  ]
    5  [ 80  .... 95  ]
    6  [ 96  .... 111 ]
    7  [ 112 .... 127 ]
    8  [ 128 .... 143 ]
    9  [ 144 .... 159 ]
    10 [ 160 .... 175 ]
    11 [ 176 .... 191 ]
    12 [ 192 .... 207 ]
    13 [ 208 .... 223 ]
    14 [ 224 .... 239 ]
    15 [ 240 .... 255 ]
       col 0 .... col 15
 

Поскольку на аппаратном обеспечении pre-Fermi имеется 16 32-разрядных общих банков памяти, каждая целочисленная запись в каждом столбце отображается в один общий банк памяти. Итак, как это взаимодействует с вашим выбором схемы индексации?

Следует иметь в виду, что потоки внутри блока нумеруются в эквиваленте основного порядка столбцов (технически измерение x структуры изменяется быстрее всего, за ним следует y, за которым следует z). Итак, когда вы используете эту схему индексации:

 shData[threadIdx.x][threadIdx.y]
 

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

 shData[threadIdx.y][threadIdx.x]
 

потоки в пределах одного и того же полупериода будут считывать данные из одной и той же строки, что подразумевает чтение из каждого из 16 различных банков общей памяти, никаких конфликтов не возникает.

Комментарии:

1. Документированы ли где-нибудь потоки, пронумерованные в основном порядке colum блока? кстати, большое спасибо

2. @pQB: да, в руководстве по программированию (раздел 2.2 «Иерархия потоков» в руководстве CUDA 3.2, к которому у меня есть мгновенный доступ).

3. Это не относится к одному измерению, верно?. Например shDta[threadIdx.y*16 threadIdx.x] , не вызовет никаких конфликтов.

4. No is не применяется, поскольку это индексация основного потока столбцов, если вы используете 32-разрядные типы. Если вы используете 64-разрядные или более крупные типы или переключаетесь на основную индексацию строк, то вы все равно можете получить конфликт банков на оборудовании до ферми