SIMD оптимизировать малую матрицу умножить (16 x 16) x (16 x 1)

Как лучше всего выполнить умножение матрицы M с плавающей запятой (16 x 16) на вектор V (16 x 1) в AVX-512? Подход, который я могу придумать, состоит в том, чтобы выполнить поэлементное умножение каждой строки матрицы на V с помощью _mm512_fmadd_ps, а затем выполнить горизонтальную сумму для каждого из результирующих векторов с помощью _mm512_reduce_add_ps. Всего 16 fmadd звонков и 16 reduce_add звонков.

Однако я понимаю, что шаг горизонтального добавления довольно медленный. Ожидаем ли мы, что этот подход будет намного быстрее, чем наивная невекторизованная реализация C ++? Есть ли лучший способ использовать SIMD, чем этот подход?


person rampatowl    schedule 17.07.2020    source источник


Ответы (1)


В идеале вы можете разместить свои входные данные, чтобы вы могли просто выполнять 16x FMA с источником широковещательной памяти (из каждого элемента вектора), то есть матрицу, уже транспонированную. Тогда результатом будет вектор скалярных произведений строки x столбца между вашими входами 16x16 и 16x1.

(На самом деле 1 vmulps и 15x FMA. Или, возможно, лучше, проявите некоторый параллелизм на уровне инструкций, начав с 2 или 4 простых умножений, и объедините только эти цепочки зависимостей FMA в конце. Это потребует дополнительно vaddps для каждого дополнительного векторного аккумулятора, но сократит задержку критического пути и снизит нагрузку на выполнение вне очереди за счет отсутствия цепочки зависимостей задержки 16 * 4 цикла, которую он мог бы попытаться скрыть.)

При использовании только AVX, а не AVX512, широковещательная загрузка не может использоваться в качестве операнда источника памяти для инструкции FMA, но по-прежнему стоит всего 1 однократную инструкцию (vbroadcastss ymm, [mem]). Но на самом деле это не имеет значения, если оба операнда все равно берутся из памяти; компилятор может просто выбрать выполнение широковещательной загрузки отдельно и использовать полный вектор строки в качестве операнда источника памяти.


В противном случае вы не хотите отдельно _mm512_reduce_add_ps каждый вектор; вместо этого транспонируйте и складывайте пары векторов с 2x _mm512_hadd_ps (с 2 разными векторами каждый раз), а затем вручную перемешивайте и складывайте, пока вы не уменьшите 16x __m512 до одного __m512, где каждый элемент является горизонтальной суммой одного из исходных 16 векторов.

Во втором случае, я думаю, вам просто нужно прямо _mm512_mul_ps между вашим вектором и строкой матрицы; не к чему добавить.

_mm512_reduce_add_ps не является отдельной машинной инструкцией; обычно он компилируется в 4x перемешивание + 4x vaddps.
Для сравнения, 2 перемешивания для подачи каждого добавления для уменьшения при перемешивании должны объединить 16 векторов до 1 из 8 + 4 + 2 + 1 = 15 всего сложений (и Всего 30 перемешиваний) вместо 16 * (4,4)

person Peter Cordes    schedule 17.07.2020
comment
Привет, Питер, спасибо за ответ. Не могли бы вы подробнее рассказать о первом случае? У меня проблемы с визуализацией того, что вы имеете в виду под источником широковещательной памяти и где играет роль транспонирование. - person rampatowl; 17.07.2020
comment
Кроме того, для случая 2: если я правильно понимаю, этот подход потребует 15 (8 + 4 + 2 + 1) вызовов _mm512_add_ps, что почти равно количеству вызовов reduce_add в наивном подходе. Это правильно? - person rampatowl; 17.07.2020
comment
@rampatowl: да, но одно добавление 1 (и 2 перемешивания) намного дешевле, чем цепочка из 4x перемешивания + 4x сложения (reduce_add не является настоящим внутренним элементом, а просто функцией, которая использует перемешивание и добавление). Кроме того, он дает результат в одном векторе вместо 16 отдельных скаляров, которые вам придется хранить отдельно. - person Peter Cordes; 17.07.2020
comment
Что касается источника широковещательной памяти: AVX и более поздние версии обеспечивают широковещательную загрузку, которая загружает один элемент из памяти и передает его всем элементам реестра. Это примерно такая же стоимость, как загрузка одного скаляра (см. uops.info/html-instr/VBROADCASTSS32_Z .html). - person chtz; 18.07.2020
comment
Вместо 16 FMA (на самом деле один mul + 15 * FMA) можно разделить продукт на половинки (имея 2 * mul + 6 * fma + 1add), чтобы уменьшить влияние задержки 4-тактной FMA (если это вызывает беспокойство. ..) - все равно пропускная способность будет ограничена 16 операциями загрузки. - person chtz; 18.07.2020