Есть ли преимущества в использовании векторных типов CUDA?

CUDA предоставляет встроенные типы векторных данных, такие как uint2, uint4 и так далее. Есть ли преимущества в использовании этих типов данных?

Предположим, что у меня есть кортеж, состоящий из двух значений, A и B. Один из способов сохранить их в памяти — выделить два массива. В первом массиве хранятся все значения A, а во втором массиве хранятся все значения B с индексами, соответствующими значениям A. Другой способ — выделить один массив типа uint2. Какой из них я должен использовать? Какой способ рекомендуется? Находятся ли члены uint3, т.е. x, y, z, рядом в памяти?


person username_4567    schedule 09.09.2012    source источник
comment
Кажется действительно странным, что CUDA не предоставляет встроенных векторных операций, поскольку каждый язык затенения делает это, и вы знаете, что аппаратное обеспечение под ним поддерживает это. Единственное место, где я вижу их использование CUDA API, — это чтение текстур. Это самая большая загадка, которая у меня есть относительно CUDA.   -  person wcochran    schedule 11.07.2016
comment
@wcochran: Каждая операция в CUDA является векторной операцией: каждый из 32 потоков в варпе соответствует одному слоту в векторе. Аппаратной/программной поддержки достаточно, чтобы представить ее вам в многопоточной модели.   -  person    schedule 15.08.2016
comment
Где точка, крест, добавление, подпункт, масштаб, отражение и т. д.?   -  person wcochran    schedule 15.08.2016


Ответы (3)


Это будет немного умозрительно, но может добавить к ответу @ArchaeaSoftware.

В основном я знаком с Compute Capability 2.0 (Fermi). Я не думаю, что для этой архитектуры есть какой-либо выигрыш в производительности от использования векторизованных типов, за исключением, может быть, 8- и 16-битных типов.

Глядя на объявление для char4:

struct __device_builtin__ __align__(4) char4
{
    signed char x, y, z, w;
};

Тип выровнен по 4 байтам. Я не знаю, что делает __device_builtin__. Может быть, это вызывает какую-то магию в компиляторе...

Все выглядит немного странно для объявлений float1, float2, float3 и float4:

struct __device_builtin__ float1
{
    float x;
};

__cuda_builtin_vector_align8(float2, float x; float y;);

struct __device_builtin__ float3
{
    float x, y, z;
};

struct __device_builtin__ __builtin_align__(16) float4
{
    float x, y, z, w;
};

float2 получает особое обращение. float3 — это структура без какого-либо выравнивания, а float4 выравнивается по 16 байтам. Я не уверен, что с этим делать.

Транзакции глобальной памяти составляют 128 байт, выровненные по 128 байтам. Транзакции всегда выполняются для полного варпа за раз. Когда варп достигает функции, которая выполняет транзакцию памяти, скажем, 32-битную загрузку из глобальной памяти, чип в это время выполняет столько транзакций, сколько необходимо для обслуживания всех 32 потоков в варпе. Таким образом, если все доступные 32-битные значения находятся в пределах одной 128-байтовой строки, необходима только одна транзакция. Если значения поступают из разных 128-байтовых строк, выполняется несколько 128-байтовых транзакций. Для каждой транзакции деформация приостанавливается примерно на 600 циклов, пока данные извлекаются из памяти (если только они не находятся в кэшах L1 или L2).

Итак, я думаю, что ключом к выяснению того, какой тип подхода дает наилучшую производительность, является рассмотрение того, какой подход вызывает наименьшее количество 128-байтных транзакций памяти.

Предполагая, что встроенные векторные типы являются просто структурами, некоторые из которых имеют специальное выравнивание, использование векторных типов приводит к тому, что значения сохраняются в памяти чередующимся образом (массив структур). Таким образом, если варп загружает все значения x в этой точке, другие значения (y, z, w) будут загружены в L1 из-за 128-байтовых транзакций. Когда позже варп попытается получить к ним доступ, возможно, они больше не находятся в L1, и поэтому должны быть запущены новые транзакции глобальной памяти. Кроме того, если компилятор может выдавать более широкие инструкции для чтения большего количества значений одновременно, для будущего использования он будет использовать регистры для хранения тех, которые находятся между точкой загрузки и точкой использования, возможно, увеличивая использование регистров. ядра.

С другой стороны, если значения упакованы в структуру массивов, нагрузку можно обслуживать с минимальным количеством транзакций. Итак, при чтении из массива x в 128-байтных транзакциях загружается только x значений. Это может привести к меньшему количеству транзакций, меньшей зависимости от кешей и более равномерному распределению между вычислительными операциями и операциями с памятью.

person Roger Dahl    schedule 09.09.2012
comment
Итак, я думаю, что ключом к выяснению того, какой тип подхода дает наилучшую производительность, является рассмотрение того, какой подход вызывает наименьшее количество 128-байтных транзакций памяти. Не обязательно. Вы должны посмотреть одну из презентаций Паулиуса, например. bit.ly/OzutxO. Увеличение числа транзакций во время полета часто помогает улучшить использование пропускной способности. - person harrism; 10.09.2012
comment
Таким образом, если варп загружает все значения x в этой точке, другие значения (y, z, w) будут загружены в L1 из-за 128-байтовых транзакций. Когда позже варп попытается получить к ним доступ, возможно, они уже не в L1. Если вы загружаете из массива float4 в переменную float4 (которая будет храниться в регистрах), вам не нужно беспокоиться о том, будут ли y, z и w находиться в кеше, когда поток начнет их использовать, потому что поток будет иметь их в регистрах. Для приложений, которым нужны данные float4 (или которые соответствуют одной из других структур), обычно ДА, вы должны использовать структуры. - person harrism; 10.09.2012
comment
@harrism Что именно делает __builtin_align__(16) в случае приведенной выше структуры для float4? - person username_4567; 10.09.2012
comment
Он указывает компилятору разместить структуру (или массив таких структур) на границе, выровненной по 16 байтам. Я считаю, что __builtin_align__ является оболочкой вокруг __align__. Вы можете подтвердить это, покопавшись в заголовках CUDA. __align__ должен быть описан в руководстве по программированию CUDA C. - person harrism; 10.09.2012
comment
@harrism: Спасибо за ссылку на презентации. Я был на GTC, но не думаю, что застал хоть одну презентацию Паулиуса. Я постараюсь добраться до них. Не знаю, о чем я думал, о необходимости зависеть от кешей после загрузки векторного типа ... Я бы удалил свой ответ, но тогда ваши комментарии также будут потеряны. Хотите добавить ответ с ними? - person Roger Dahl; 14.09.2012
comment
Нет, я думаю, что между вашим ответом и ответом Ника, а также комментариями, это хорошо освещено. - person harrism; 14.09.2012

Я не верю, что встроенные в CUDA кортежи ([u]int[2|4], float[2|4], double[2]) имеют какие-либо внутренние преимущества; они существуют в основном для удобства. Вы можете определить свои собственные классы C++ с таким же макетом, и компилятор будет работать с ними эффективно. Аппаратное обеспечение имеет встроенную 64-битную и 128-битную загрузку, поэтому вам нужно проверить сгенерированный микрокод, чтобы знать наверняка.

Что касается того, следует ли вам использовать массив uint2 (массив структур или AoS) или два массива uint (структура массивов или SoA), простых ответов нет — это зависит от приложения. Для встроенных типов удобного размера (2x32-бит или 4x32-бит) AoS имеет то преимущество, что вам нужен только один указатель для загрузки/сохранения каждого элемента данных. SoA требует нескольких базовых указателей или, по крайней мере, нескольких смещений и отдельных операций загрузки/восстановления для каждого элемента; но это может быть быстрее для рабочих нагрузок, которые иногда работают только с подмножеством элементов.

В качестве примера рабочей нагрузки, которая эффективно использует AoS, посмотрите на пример nbody (который использует float4 для хранения XYZ+массы каждой частицы). Образец Блэка-Шоулза использует SoA, предположительно потому, что размер элемента float3 неудобен.

person ArchaeaSoftware    schedule 09.09.2012
comment
Аппаратное обеспечение имеет 64- и 128-битные загрузки и хранилища. Как правило, структуры, подобные uint2 и uint4, если имеют смысл для ваших данных и алгоритма, имеют преимущество, поскольку они могут увеличить размер транзакции для каждого потока и, следовательно, более эффективно использовать доступные пропускная способность. Вы можете создавать свои собственные пользовательские структуры, но убедитесь, что они задают выравнивание, как это делают структуры, предоставляемые CUDA. - person harrism; 10.09.2012
comment
@harrism Так что, если я не ошибаюсь, это выглядит так ... Все члены uint2 будут находиться в памяти бок о бок, поэтому использование массива типа uint2 МОЖЕТ привести к меньшему количеству транзакций памяти, потому что один раскрывает два значения .. - person username_4567; 10.09.2012
comment
Да. Посмотрите, как float4 используется в nbody, частицах и других демонстрациях физики в CUDA SDK. - person harrism; 10.09.2012
comment
+1 Я знаю, что это старый пост, но я думаю, что ваш второй и третий абзац действительно важны. Часто в параллельных вычислениях я обычно слышу, что SoA лучше, но это не всегда так, о чем свидетельствует пример кода nbody. - person James; 07.09.2016

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

person Íhor Mé    schedule 15.08.2016