Код вашего устройства имеет неопределенное поведение из-за условий гонки в обоих случаях, при использовании общей памяти или глобальной памяти. У вас есть несколько потоков, которые одновременно читают и изменяют один и тот же объект int
.
Не должна ли синхронизация деформации работать с общей памятью?
Я не вижу никакой синхронизации деформации в вашем коде.
Тот факт, что аппаратное обеспечение выполняет деформации на этапе блокировки (что не обязательно для начала), совершенно не имеет значения, потому что не аппаратное обеспечение читает ваш код C ++. Это тот набор инструментов, который вы используете для перевода своего кода C ++ в машинный код, который фактически будет работать на вашем оборудовании. И компиляторы C ++ могут оптимизироваться на основе абстрактных правил языка C ++.
Давайте посмотрим на машинный код, который фактически сгенерирован для вашего примера (используя CUDA 10 здесь, на моей машине):
_Z7kernel1Pi:
/*0008*/ MOV R1, c[0x0][0x20] ;
/*0010*/ S2R R9, SR_TID.X ;
/*0018*/ SHL R8, R9.reuse, 0x2 ;
/*0028*/ SHR.U32 R0, R9, 0x1e ;
/*0030*/ IADD R2.CC, R8, c[0x0][0x140] ;
/*0038*/ IADD.X R3, R0, c[0x0][0x144] ;
/*0048*/ LDG.E R0, [R2] ;
/*0050*/ ISETP.NE.AND P0, PT, R9.reuse, RZ, PT ;
/*0058*/ ISETP.GE.U32.AND P1, PT, R9, 0x2, PT ;
/*0068*/ @P0 LDS.U.32 R5, [R8+-0x4] ;
/*0070*/ { ISETP.GE.U32.AND P2, PT, R9.reuse, 0x4, PT ;
/*0078*/ @P1 LDS.U.32 R6, [R8+-0x8] }
/*0088*/ ISETP.GE.U32.AND P3, PT, R9, 0x8, PT ;
/*0090*/ @P2 LDS.U.32 R7, [R8+-0x10] ;
/*0098*/ { ISETP.GE.U32.AND P4, PT, R9, 0x10, PT SLOT 0;
/*00a8*/ @P3 LDS.U.32 R9, [R8+-0x20] SLOT 1 }
/*00b0*/ @P4 LDS.U.32 R10, [R8+-0x40] ;
/*00b8*/ { MOV R4, R0 ;
/*00c8*/ STS [R8], R0 }
/*00d0*/ @P0 IADD R5, R4, R5 ;
/*00d8*/ { @P0 MOV R4, R5 ;
/*00e8*/ @P0 STS [R8], R5 }
/*00f0*/ @P1 IADD R6, R4, R6 ;
/*00f8*/ { @P1 MOV R4, R6 ;
/*0108*/ @P1 STS [R8], R6 }
/*0110*/ @P2 IADD R7, R4, R7 ;
/*0118*/ { @P2 MOV R4, R7 ;
/*0128*/ @P2 STS [R8], R7 }
/*0130*/ @P3 IADD R9, R4, R9 ;
/*0138*/ { @P3 MOV R4, R9 ;
/*0148*/ @P3 STS [R8], R9 }
/*0150*/ @P4 IADD R10, R4, R10 ;
/*0158*/ @P4 STS [R8], R10 ;
/*0168*/ @P4 MOV R4, R10 ;
/*0170*/ STG.E [R2], R4 ;
/*0178*/ EXIT ;
.L_1:
/*0188*/ BRA `(.L_1) ;
.L_14:
Как вы можете видеть, компилятор (в данном конкретном случае "виновником" был фактически ассемблер PTX) преобразовал вашу последовательность if в набор инструкций, которые устанавливают предикаты на основе условий if. Он first извлекает all значения, которые ему когда-либо понадобятся, из общей памяти в регистры с использованием условных нагрузок Только после этого он выполняет все добавления и условные хранилища, используя уже загруженные значения. Это совершенно легальная интерпретация вашего кода C ++. Поскольку вы не указали какие-либо ограничения синхронизации или упорядочения памяти, компилятор может работать в предположении, что нет потенциально одновременных конфликтов, и все эти загрузки и хранилища могут быть переупорядочены любым подходящим способом.
Чтобы исправить ваш код, используйте явную синхронизацию деформации :
__global__ void kernel1(int *data)
{
__shared__ int data_s[32];
size_t t_id = threadIdx.x;
data_s[t_id] = data[t_id];
__syncwarp();
if (1 <= t_id)
data_s[t_id] += data_s[t_id - 1];
__syncwarp();
if (2 <= t_id)
data_s[t_id] += data_s[t_id - 2];
__syncwarp();
if (4 <= t_id)
data_s[t_id] += data_s[t_id - 4];
__syncwarp();
if (8 <= t_id)
data_s[t_id] += data_s[t_id - 8];
__syncwarp();
if (16 <= t_id)
data_s[t_id] += data_s[t_id - 16];
data[t_id] = data_s[t_id];
}
Причина, по которой эта проблема проявляется только начиная с CUDA 9.0, заключается в том, что синхронизация на уровне деформации была действительно введена в CUDA 9.0 только тогда, когда Volta и «независимое планирование потоков» сделали это необходимостью. До появления CUDA 9.0 синхронное программирование по варпу официально не поддерживалось. Но компиляторы были довольно консервативны, когда дело дошло до взлома кода, как в вашем примере выше. Вероятно, причина в том, что такое «синхронное деформация» программирование (обратите внимание на кавычки) часто было единственным способом приблизиться к пиковой производительности, реальной альтернативы не было, и, таким образом, люди делали это все время. Это все еще было неопределенное поведение, и NVIDIA продолжала предупреждать нас. Во многих случаях это просто работало…