Глобальная запись C AMP (GPU) обновляется недостаточно быстро, чтобы все плитки были видны?

#gpgpu #c -amp

#gpgpu #c -amp

Вопрос:

Я предполагаю, что это проблема с графическим процессором, а не с точкой доступа C , поэтому я отмечаю ее широко.

У меня есть реализация вычисления, которое разбивает работу на несколько плиток, которые выполняют свою работу, а затем добавляют результат к существующему значению в глобальной памяти. Сначала каждый поток в плитке вычисляет результат своей части в tile_static memory, где у каждого потока есть индекс для записи. Позже первый поток в плитке просуммирует все результаты части вместе и добавит сумму к позиции в глобальной памяти.

Плитки (поток 0 в плитках) иногда хотят записывать в одно и то же место, поэтому я добавил простую блокировку.

 inline void lock(int *lockVariable) restrict(amp)
{
    while (atomic_exchange(lockVariable, 1) != 0);
}

inline void unlock(int *lockVariable) restrict(amp)
{
    *lockVariable = 0;
}
  

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

Фактическая запись результата плитки, выполняемая первым потоком в плитке, выполняется следующим образом

 //now the FIRST thread in the tile will summ all the pulls into one
if (idx.local[0] == 0)
{                   
  double_4 tileAcceleration = 0;
  for (int i = 0; i < idx.tile_dim0; i  )
  {
    tileAcceleration  = threadAccelerations[i];
  }
  lock(amp;locks[j]);
  //now the FIRST thread in the tile will add this to the global result
  acceleration[j]  = tileAcceleration;
  unlock(amp;locks[j]);
}
  

В основном это работает нормально, но не всегда. Должно существовать какое-то условие гонки, потому что, когда слишком много листов относительно количества ячеек памяти для записи (слишком много борьбы за блокировки), иногда не удается правильно добавить результат плитки.

Кажется, что иногда, хотя и редко, настройка блокировки / разблокировки не гарантирует правильное добавление.

Это можно «исправить», переместив блокировку перед суммированием, поэтому требуется больше времени с момента получения блокировки до того, как thread0 выполнит фактическую запись. Я также могу «исправить» это, сняв блокировку, когда у меня осталось пять элементов при суммировании. Оба показаны ниже

Первое исправление, которое выполняется довольно медленно (слишком долго блокируется)

 if (idx.local[0] == 0)
{                   
  lock(amp;locks[j]); //get lock right away
  double_4 tileAcceleration = 0;
  for (int i = 0; i < idx.tile_dim0; i  )
  {
    tileAcceleration  = threadAccelerations[i];
  }
  //now the FIRST thread in the tile will add this to the global result
  acceleration[j]  = tileAcceleration;
  unlock(amp;locks[j]);
}
  

Второе исправление, которое немного быстрее

 if (idx.local[0] == 0)
{                   
  lock(amp;locks[j]); //this is a "fix" but a slow one
  double_4 tileAcceleration = 0;
  for (int i = 0; i < idx.tile_dim0; i  )
  {
    tileAcceleration  = threadAccelerations[i];
    if (i == idx.tile_dim0 - 5) lock(amp;locks[j]); //lock when almost done
  }
  //now the FIRST thread in the tile will add this to the global result
  acceleration[j]  = tileAcceleration;
  unlock(amp;locks[j]);
}
  

Видя, как работают эти «исправления», кажется очевидным, что некоторые записи в память обновляются недостаточно быстро для всей системы. Один тайл может заблокировать местоположение, записать в него и разблокировать. Затем другая плитка получает блокировку, добавляет ее (но ссылается на старые не обновленные данные) и разблокируется.

Блокировка — это int, а данные — double_4, поэтому кажется, что блокировка достаточно быстро освобождается и обновляется, чтобы другие плитки могли видеть, пока данные все еще находятся в пути. Затем другая плитка может видеть блокировку как свободную, даже если запись первых плиток еще не завершена полностью. Поэтому вторая плитка считывает не обновленное значение данных из кэша, добавляет к нему и записывает…

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

Ответ №1:

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

  • Не следует смешивать атомарные и обычные (неатомные) операции чтения и записи. При обычном чтении могут не отображаться результаты атомарной записи в ту же ячейку памяти. Обычные записи не следует смешивать с атомарными записями в ту же ячейку памяти. Если ваша программа не соответствует этим критериям, это приведет к неопределенному результату.

  • Атомарные операции не подразумевают какого-либо ограничения памяти. Атомарные операции могут быть переупорядочены. Это отличается от поведения взаимосвязанных операций в C .

Итак, чтобы ваша lock функция работала unlock , функция также должна использовать атомарное чтение.

В общем, вы не должны пытаться блокировать таким образом, поскольку это очень неэффективно. Ваша программа может синхронизировать операции между потоками на одной и той же плитке, используя примитивы tile barrier . Синхронизация операций с плитками гарантируется только тогда, когда ядро

Похоже, то, что вы пытаетесь сделать здесь, — это какая-то операция сокращения / накопления. Каждый поток генерирует результат, а затем все эти результаты объединяются для создания единого (окончательного) результата.

Вот пример простого сокращения.

 #include <vector>
#include <algorithm>
#include <numeric>
#include <amp.h>

using namespace concurrency;

int Reduce(accelerator_viewamp; view, const std::vector<int>amp; source) const
{
    const int windowWidth = 8;
    int elementCount = static_cast<unsigned>(source.size());

    // Using array as temporary memory.
    array<int, 1> a(elementCount, source.cbegin(), source.cend(), view);

    // Takes care of the sum of tail elements.
    int tailSum = 0;
    if ((elementCount % windowWidth) != 0 amp;amp; elementCount > windowWidth)
        tailSum = 
            std::accumulate(source.begin()   ((elementCount - 1) / windowWidth) * windowWidth, 
                source.end(), 0);

    array_view<int, 1> avTailSum(1, amp;tailSum);

    // Each thread reduces windowWidth elements.
    int prevStride = elementCount;
    for (int stride = (elementCount / windowWidth); stride > 0; stride /= windowWidth)
    {
        parallel_for_each(view, extent<1>(stride), [=, amp;a] (index<1> idx) restrict(amp)
        {
            int sum = 0;
            for (int i = 0; i < windowWidth; i  )
                sum  = a[idx   i * stride];
            a[idx] = sum;

            // Reduce the tail in cases where the number of elements is not divisible.
            // Note: execution of this section may negatively affect the performance.
            // In production code the problem size passed to the reduction should
            // be a power of the windowWidth. 
            if ((idx[0] == (stride - 1)) amp;amp; ((stride % windowWidth) != 0) amp;amp; (stride > windowWidth))
            {
                for(int i = ((stride - 1) / windowWidth) * windowWidth; i < stride; i  )
                    avTailSum[0]  = a[i];
            }
        });
        prevStride = stride;
    }

    // Perform any remaining reduction on the CPU.
    std::vector<int> partialResult(prevStride);
    copy(a.section(0, prevStride), partialResult.begin());
    avTailSum.synchronize();
    return std::accumulate(partialResult.begin(), partialResult.end(), tailSum);
}
  

В общем, если ваш параллельный код полагается на блокировки или другие явные примитивы синхронизации, вам следует спросить, действительно ли это правильный подход. Если вы можете немного подробнее объяснить, чего вы пытаетесь достичь, тогда я, вероятно, смогу дать более конкретный ответ

Приведенный выше текст и примеры взяты из книги C AMP.

Кстати: ваш код ссылается на tileAccelleration то, что если вы реализуете какую-либо модель n-body, то вы можете найти полную реализацию в C AMP Book Codeplex Project

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

1. Извините за поздний ответ. Я не знал, что чтение должно быть атомарным, чтобы синхронизироваться с записью. Код на самом деле nbody, но для индивидуального адаптивного временного интервала, когда у меня может быть несколько объектов, нуждающихся в новых ускорениях, и много объектов, которые привлекают. Для этой цели я отменил обычное foreach body add attraction от всех остальных, чтобы foreach attractor добавлял ускорение каждому привлеченному, тем самым устраняя необходимость синхронизированной записи. Более поздняя итерация сделала это в плитках и заблокировала только один раз на плитку. Однако спасибо за информацию об atomics.