Умножение векторной матрицы, вектор с плавающей точкой, двоичная матрица - PullRequest
2 голосов
/ 14 октября 2019

Я бы хотел умножить вектор с плавающей точкой размера N на матрицу размера NxM.

Матрица представляет собой двоичную матрицу (содержащую только ноль и 1) и является относительно разреженной: плотность ненулевые значения находятся в диапазоне 1-5%.

В настоящее время я формирую это как плотный вектор и умножение матрицы разреженного числа с плавающей запятой.

Но это просто перебор, не так ли?

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

Полагаю, я мог бы сформировать это как векторизованную операцию в SSE / AVX, что-то вроде load + и + sum или load + mask + sum

Буду признателен, если вы укажете мне направоДля этого, главным вопросом является то, как лучше всего распаковать набор битов?

1 Ответ

3 голосов
/ 14 октября 2019

То есть каждый элемент вашего результирующего вектора является замаскированной суммой входного вектора? И эти маски происходят из столбцов матрицы, поэтому они не являются непрерывными битами.

Маскированная сумма с использованием непрерывного растрового изображения тривиальна для AVX512 (просто используйте добавление с маскированием слиянием или загрузки с нулевой маской). В SSE / AVX2 вы бы использовали . Есть ли обратная инструкция к команде movemask в intel avx2? + _mm256_and_ps. Или некоторое изменение в том, что оптимизирует по векторам маски, например, с 32-битной широковещательной нагрузкой, а затем сдвигает это для следующего шага. Вместо того, чтобы делать еще одну передачу невыровненного слова для каждого байта.

Но с вашими битами маски не смежными у вас есть выбор:

  • Делать каждый элемент вектора вывода отдельнос горизонтальной суммой в конце. Требуется собрать биты и сделать векторную маску. Вероятно, трудно, за исключением случая M = 32, когда шаг битов уже выстраивает их в ряд с непрерывными 32-разрядными числами с плавающей запятой.
  • накапливает вектор из 4 или 8 выходных элементов , используя смежные группы4 или 8 битов маски. Таким образом, вы векторизуете внешний цикл, выполняя широковещательную загрузку во внутреннем цикле над входным вектором. ИСПОЛЬЗУЙТЕ ЭТО. Вы должны развернуть с несколькими векторными суммами, чтобы скрыть задержку добавления FP.

Трансляционные нагрузки типа __m256 v = _mm256_set1_ps(invec[i]) в основном бесплатны (vbroadcastss - это чистая загрузка, без ALU shuffle uop). Вам не нужны никакие другие перетасовки поплавков, просто чистая вертикальная SIMD, даже в конце цикла: вы просто _mm256_storeu_ps в выходной вектор.

И вы используете непрерывные группы битов маскипоэтому полезны обычные вопросы и ответы по обратной маске.

  // untested, rough outline of what it might look like

  uint8_t matrix[rows * cols];  // bit matrix in chunks of 8 bits
  float invec[N], outvec[N];    // A normal function will just take pointer inputs.

  constexpr int unroll = 4;
  for(int outpos = 0 ; outpos < M-8*unroll+1 ; outpos += 8 * unroll) {
      __m256 sum0, sum1, sum2, sum3;  //optionally use an array of accumulators, sums[unroll];
      sum0 = sum1 = sum2 = sum3 = _mm256_setzero_ps();
            // optionally peel the first inner iteration to just load+mask without adding to 0.0
      for (int inpos = 0 ; in < N ; in++ ){
          __m256 inv = _mm256_set1_ps(invec[inpos]);
          __m256 mask0 = inverse_movemask(matrix[outpos*stride + inpos + 0]);  // 8 bits -> 8 vector elements
          __m256 mask1 = inverse_movemask(matrix[outpos*stride + inpos + 1]);
          ...

          sum0 = _mm256_add_ps(sum0, _mm256_and_ps(inv, mask0) );  // add in[i] or 0.0 according to mask
          sum1 = _mm256_add_ps(sum1, _mm256_and_ps(inv, mask1) );
          ...
      }
      __m256_storeu_ps(&outvec[outpos + 0*8], sum0);
      __m256_storeu_ps(&outvec[outpos + 1*8], sum1);
      __m256_storeu_ps(&outvec[outpos + 2*8], sum2);
      ...
  }

  not-unrolled __m256 and/or __m128 cleanup for M % (8*unroll) != 0

  cleanup for M % 4 != 0 using __m128 broadcast loads 
    for the last 1..3 rows of masks
    maybe use a masked store (AVX2 vmaskmov) or pad your output vector

Каждая итерация внутреннего цикла маскирует один метод с плавающей запятой 8 * unroll различными способами и накапливается в соответствующие 8 * unroll различные промежуточные суммы. (Через unroll векторов по 8 чисел с плавающей запятой.)


Это также хорошо для пропускной способности памяти

Каждый битовый бит читается только один раз в продукте vec * mat, новектор ввода эффективно используется M раз. Зацикливание на смежных строках растрового изображения дает хорошую локальность, не требуя загрузки какой-либо из этих строк кэша более одного раза.

Даже с AVX512 и 2x _mm512_mask_add_ps за такт, добавленный 1 бит на элемент FP не так уж много пропускной способностидля растровых загрузок.

Однако вы перебираете свой входной вектор M/(8*unroll) раз. Маскированные добавления для каждого вектора суммы используют разные биты маски, но один и тот же широковещательный вход float. Поскольку матричные элементы в 32 раза меньше, чем векторные элементы, это неплохо.

Один метод с плавающей запятой, загружаемый за 4x или 8x vaddps инструкций, является очень хорошей вычислительной интенсивностью. Особенно без AVX512, где битовая карта -> векторная маска будет стоить циклов.

Чтобы еще больше помочь с пропускной способностью кеша / памяти, блокировка кеша / циклическое разбиение для размера кэш-памяти L2 (256 кБ)может быть возможно помочь с повторным использованием входных векторных элементов. Но я не уверен, что вы можете эффективно блокировать как ввод, так и вывод. В отличие от продукта mat * mat, нужно выполнить только O (n ^ 2). Перечитывание ввода и просто запись одного выходного потока, вероятно, хорошо, но вы можете найти золотую середину, которая добавляет частичные результаты в частичные порции выходного вектора. Но тогда мы больше не читаем битовую матрицу в одном непрерывном потоке. Пока вы останавливаетесь на границах строк кэша, это, вероятно, нормально.


Если ваша матрица NxM имеет M = 32, то это точно соответствует размеру float и _mm256_loadu_si256 получит вектор с битами маски для outvec[0] в младшем бите каждого элемента. И биты маски для outvec[31] в старшем бите. Вы можете использовать _mm256_blendv_ps, чтобы применить их к вводу суммы, и сдвиг влево на 1, чтобы переместить следующий бит вверх в верхнюю позицию. (Альтернативой vblendvps является psrad на 31 + andps: арифметический сдвиг вправо для передачи старшего бита на все позиции).

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


С AVX512F вы можете просто использовать строки матрицы в качестве __mmask16 значений для маскированныхдобавьте как _mm512_mask_add_ps.
sum = _mm512_mask_add_ps(sum, matrix[col*rowstride + row], sum, invec[i]);, если matrix является массивом uint16_t.

Или с AVX512BW, kmovq 64 бита маски в регистр kи kshift вниз, чтобы сопоставить с развернутыми более 4-х векторных аккумуляторов. К сожалению, kmov k, [mem] - это 2 мопа на Skylake-X: нагрузка + порт 5, а не просто моп нагрузки, который может записывать в регистры маски. Таким образом, одна загрузка 3x распаковки с kshift - это чистый выигрыш против 4x kmovw k1, [mem] / kmovw k2, [mem+2] и т. Д. Невозможно получить каждые 16 бит данных маски в нижней части k регистра без мопа port5 для каждогоодин. Таким образом, он конкурирует с 512-битной пропускной способностью FMA / add / mul на ядрах SKX, которые имеют 2 блока FMA, в противном случае это просто пропускная способность внешнего интерфейса.

...