Арифметика указателя CUDA вызывает несвязанный доступ к памяти?

Я работаю с ядром CUDA, которое должно работать с указателями на указатели. Ядро в основном выполняет большое количество очень маленьких сокращений, которые лучше всего выполнять последовательно, так как сокращения имеют размер Nptrs=3-4. Вот две реализации ядра:

__global__
void kernel_RaiseIndexSLOW(double*__restrict__*__restrict__ A0,
        const double*__restrict__*__restrict__ B0,
        const double*__restrict__*__restrict__ C0,
        const int Nptrs, const int Nx){
      const int i = blockIdx.y;
      const int j = blockIdx.z;
      const int idx = blockIdx.x*blockDim.x + threadIdx.x;
      if(i<Nptrs) {
         if(j<Nptrs) {
           for (int x = idx; x < Nx; x += blockDim.x*gridDim.x){
              A0gpu[i+3*j][x] = B0gpu[i][x]*C0gpu[3*j][x]
                       +B0gpu[i+3][x]*C0gpu[1+3*j][x]
                       +B0gpu[i+6][x]*C0gpu[2+3*j][x];               
           }
         }
       }
 }

__global__
void kernel_RaiseIndexsepderef(double*__restrict__*__restrict__  A0gpu, 
               const double*__restrict__*__restrict__ B0gpu,
               const double*__restrict__*__restrict__ C0gpu,
               const int Nptrs, const int Nx){
const int i = blockIdx.y;
const int j = blockIdx.z;
const int idx = blockIdx.x*blockDim.x + threadIdx.x;
if(i<Nptrs) {
  if(j<Nptrs){
    double*__restrict__ A0ptr = A0gpu[i+3*j];
    const double*__restrict__ B0ptr0 = B0gpu[i];
    const double*__restrict__ C0ptr0 = C0gpu[3*j];
    const double*__restrict__ B0ptr1 = B0ptr0+3;
    const double*__restrict__ B0ptr2 = B0ptr0+6;
    const double*__restrict__ C0ptr1 = C0ptr0+1;
    const double*__restrict__ C0ptr2 = C0ptr0+2;

    for (int x = idx; x < Nx; x +=blockDim.x *gridDim.x){
      double d2 = C0ptr0[x];
      double d4 = C0ptr1[x]; //FLAGGED
      double d6 = C0ptr2[x]; //FLAGGED
      double d1 = B0ptr0[x];
      double d3 = B0ptr1[x]; //FLAGGED
      double d5 = B0ptr2[x]; //FLAGGED
      A0ptr[x] = d1*d2 + d3*d4 + d5*d6;

    }
   }                                                                        
  }
 }

Как видно из названий, ядро ​​«sepderef» работает примерно на 40% быстрее, чем его аналог, достигая, с учетом накладных расходов на запуск, эффективной пропускной способности около 85 Гбит/с при Nptrs=3, Nx=60000 на M2090 с включенным ECC (~160 Гбит/с). будет оптимальным).

Запуск их через nvvp показывает, что ядро ​​​​привязано к пропускной способности. Странно, однако, что строки, которые я пометил //FLAGGED, выделяются профилировщиком как области неоптимального доступа к памяти. Я не понимаю, почему это так, поскольку доступ здесь выглядит объединенным для меня. Почему бы и нет?

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


person AGML    schedule 11.06.2017    source источник
comment
Вы действительно должны убедиться, что используете const на одном уровне параметров вашего ядра - уровне, который вы не должны перезаписывать. Также должен работать с указателями на указатели - хотя это был не ваш вопрос, я в этом сомневаюсь; Скорее всего, вы сможете обойти это.   -  person einpoklum    schedule 11.06.2017


Ответы (1)


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

В вашем примере «sepderef» вы загружаете все операнды из памяти перед их суммированием, что означает, что потенциально глобальная задержка памяти возникает только один раз за итерацию цикла (есть шесть загрузок на итерацию цикла, но все они могут перекрываться. Только первое добавление цикла остановится, пока его операнды не будут доступны.После остановки все остальные добавления будут иметь свои операнды легко или очень скоро).

В примере «МЕДЛЕННО» загрузка из памяти и добавление перемешаны, поэтому глобальная задержка памяти возникает несколько раз за операцию цикла.

Вы можете задаться вопросом, почему компилятор автоматически не меняет порядок инструкций загрузки перед вычислением. Компиляторы CUDA раньше делали это очень агрессивно, расходуя дополнительные регистры, где операнды ожидают использования. Однако CUDA 8.0 кажется гораздо менее агрессивным в этом отношении, гораздо больше придерживаясь порядка инструкций в исходном коде. Это дает программисту больше возможностей структурировать код наилучшим образом с точки зрения производительности. ="nofollow noreferrer">где планирование инструкций компилятора было неоптимальным. В то же время на программиста возлагается дополнительная нагрузка по явному планированию инструкций, даже если в предыдущих версиях компилятора это было сделано правильно.

person tera    schedule 11.06.2017
comment
Это очень полезно, спасибо. Однако почему строки //FLAGGED в ядре 'sepderef' помечаются профилировщиком как не объединенные? - person AGML; 11.06.2017
comment
Также обратите внимание, что цикл предназначен только для случая, когда размер проблемы превышает размер сетки. В моих реальных тестах мы делаем только одну итерацию цикла. - person AGML; 11.06.2017
comment
Строки могут быть помечены, потому что обращения не выровнены, поэтому они полагаются на кеш для полной пропускной способности. Я бы не слишком беспокоился о них. - person tera; 12.06.2017
comment
Я бы лучше подумал о комментарии einpoklum, действительно ли неизбежно работать с указателями на указатели, что означает, что вы удваиваете задержку для каждого доступа к памяти (или, по крайней мере, для каждой итерации цикла). - person tera; 12.06.2017
comment
Что ж, помимо пометки, глобальная эффективность загрузки памяти падает с ~ 100% до примерно 56% во втором ядре, но, возможно, проблема действительно в выравнивании. Очень трудно избежать указателей на указатели, поскольку они предназначены для использования в качестве автоматически генерируемых вставных ядер для ускорения очень сложного кода ЦП, работающего с классом, который не хранит отдельные массивы непрерывно. Хотя, возможно, есть способ. - person AGML; 12.06.2017
comment
Конечно, есть способ. Однако стоит ли это усилий — это другой вопрос, на который можете ответить только вы. - person tera; 12.06.2017