Почему барьер синхронизирует разделяемую память, когда MemoryBarrier не работает? - программирование

Почему барьер синхронизирует разделяемую память, когда MemoryBarrier не работает?

Следующий графический шейдер GLSL просто копирует inImage в outImage. Он получен из более сложного постобработки.

В первых нескольких строках main() один поток загружает 64 пикселя данных в общий массив. Затем, после синхронизации, каждый из 64 потоков записывает один пиксель в выходное изображение.

В зависимости от того, как я синхронизируюсь, я получаю разные результаты. Первоначально я думал, что memoryBarrierShared() будет правильным вызовом, но он дает следующий результат:

Unsynchronized result

что является тем же результатом, что и отсутствие синхронизации или использование memoryBarrier().

Если я использую barrier(), я получаю следующий (желаемый) результат:

enter image description here

Полоса шириной 32 пикселя, и если я изменю размер рабочей группы на все, что меньше или равно 32, я получаю правильные результаты.

Что здесь происходит? Неужели я не понимаю цель memoryBarrierShared()? Почему barrier() работает?

#version 430

#define SIZE 64

layout (local_size_x = SIZE, local_size_y = 1, local_size_z = 1) in;

layout(rgba32f) uniform readonly  image2D inImage;
uniform writeonly image2D outImage;

shared vec4 shared_data[SIZE];

void main() {
    ivec2 base = ivec2(gl_WorkGroupID.xy * gl_WorkGroupSize.xy);
    ivec2 my_index = base + ivec2(gl_LocalInvocationID.x,0);

    if (gl_LocalInvocationID.x == 0) {
        for (int i = 0; i < SIZE; i++) {
            shared_data[i] = imageLoad(inImage, base + ivec2(i,0));
        }
    }

    // with no synchronization:   stripes
    // memoryBarrier();        // stripes
    // memoryBarrierShared();  // stripes
    // barrier();              // works

    imageStore(outImage, my_index, shared_data[gl_LocalInvocationID.x]);
}
4b9b3361

Ответ 1

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

Так может случиться так, что ваш вычислительный шейдер работает отлично, но это синхронизация со следующим отображением (или каким-либо другим) проходом (который должен каким-то образом прочитать данные изображения), который терпит неудачу. Для этого существует функция glMemoryBarrier. В зависимости от того, как вы читаете данные изображения в проходе дисплея (или, точнее, прохода, который читает изображение после прохождения шейдера вычислений), вы должны указать другой флаг этой функции. Если вы прочитали его с использованием текстуры, используйте GL_TEXTURE_FETCH_BARRIER_BIT​, если вы снова используете загрузку изображения, используйте GL_SHADER_IMAGE_ACCESS_BARRIER_BIT​, если вы используете glBlitFramebuffer для отображения, используйте GL_FRAMEBUFFER_BARRIER_BIT​...

Хотя у меня нет большого опыта работы с синхронизацией загрузки/хранения изображений и ручной памяти, и это только то, что я придумал теоретически. Поэтому, если кто-нибудь знает лучше или вы уже используете правильный glMemoryBarrier, тогда не стесняйтесь меня исправлять. Аналогично, это не должно быть вашей единственной ошибкой (если таковая имеется). Но последние два пункта из связанной статьи Wiki действительно касаются вашего прецедента и IMHO дают понять, что вам нужен какой-то glMemoryBarrier:

  • Данные, записанные в переменные изображения в одном проходе рендеринга и считанные шейдером в последнем проходе, не должны использовать переменные coherent или memoryBarrier(). Вызов glMemoryBarrier с помощью SHADER_IMAGE_ACCESS_BARRIER_BIT​ устанавливается в барьерах между проходами необходимо.

  • Данные, записанные шейдером в одном проходе рендеринга и считанные другим механизмом (например, вытягивание вершин или индексного буфера) в более позднем проходе не используйте переменные coherent или memoryBarrier(). призвание glMemoryBarrier с соответствующими битами, установленными в барьерах между необходимо пройти.


EDIT: На самом деле статья Wiki по вычислительным шейдерам сообщает

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

Общие переменные неявно объявлены coherent​, поэтому вы не необходимо (и не может использовать) этот квалификатор. Однако вам все равно нужно обеспечить соответствующий барьер памяти.

Обычный набор барьеров памяти доступен для вычисления шейдеров, но они также имеют доступ к memoryBarrierShared()​;, этот барьер специально для совместного переменного упорядочения. groupMemoryBarrier()действует как memoryBarrier()​, упорядочивая память для всех видов переменные, но он только заказывает чтение/запись для текущей рабочей группы.

Хотя все вызовы в рабочей группе, как говорят, выполняют "параллельно", это не означает, что вы можете предположить, что все они выполнение в режиме блокировки. Если вам необходимо убедиться, что вызов написано на какую-то переменную, чтобы вы могли ее прочитать, вам нужно синхронизировать выполнение с вызовами, а не просто выпускать память барьер (вам все еще нужен барьер памяти).

Чтобы синхронизировать чтение и запись между вызовами внутри рабочей группы, вы должны использовать функцию barrier(). Это заставляет явная синхронизация между всеми вызовами в рабочей группе. Исполнение в рабочей группе не будет продолжаться до тех пор, пока все остальные призывы достигли этого барьера. Пройдя мимо barrier()​, все общие переменные, ранее записанные во всех вызовах в группа будет видна.

Итак, это похоже на то, что вам нужен barrier, а memoryBarrierShared недостаточно (хотя вам не нужны оба, как сказано в последнем предложении). Помехи памяти просто синхронизируют память, но это не останавливает выполнение потоков, чтобы пересечь ее. Таким образом, потоки не будут считывать какие-либо старые кэшированные данные из разделяемой памяти, если первый поток уже что-то написал, но они могут очень хорошо дойти до точки чтения до того, как первый поток попытался вообще что-либо написать.

Это действительно идеально подходит для того, что для 32 и ниже размеров блоков он работает и что работают первые 32 пикселя. По крайней мере, на оборудовании NVIDIA 32 есть размер основы и, следовательно, количество потоков, которые работают в идеальном блокированном шаге. Таким образом, первые 32 потока (ну, каждый блок из 32 потоков) всегда работают точно параллельно (ну, концептуально это), и поэтому они не могут вводить какие-либо расовые условия. Это также относится к случаю, когда вам не нужна какая-либо синхронизация, если вы знаете, что работаете внутри единой основы, общей оптимизации.