向量矩阵乘法,浮点向量,二进制矩阵

时间:2019-10-14 15:58:09

标签: c++ matrix-multiplication sse simd avx

我想将大小为N的浮点向量与大小为NxM的矩阵相乘。

矩阵是二进制矩阵(仅包含零和1),并且相对稀疏:非零值的密度在1%到5%之间。

当前,我将其形成为密集矢量和稀疏浮点矩阵乘法。

但这只是一个过度的杀伤力,不是吗?

如果我将矩阵的列存储为位集,然后进行乘法运算,那就简单地使用位集索引向量,然后对其求和。

我假设我可以将其形成为SSE ​​/ AVX中的矢量化操作,例如load +和+ sum或load + mask + sum

如果能为我指出正确的内在函数,我将不胜感激,主要问题是处理位集解包的最佳方法是什么?

1 个答案:

答案 0 :(得分:3)

因此,结果向量的每个元素都是输入向量的掩码和?而且这些掩码来自矩阵的列,因此它们不是连续的位。

使用连续位图的屏蔽和对于AVX512来说微不足道(只需使用合并屏蔽的加法或零屏蔽的负载)。对于SSE / AVX2,您将使用is there an inverse instruction to the movemask instruction in intel avx2? + _mm256_and_ps。或针对跨掩码向量进行优化的变体,例如具有32位广播负载,然后将其转移到下一步。而不是为每个字节广播另一个未对齐的双字。

但是您的掩码位是连续的,您可以选择:

  • 分别处理每个输出矢量元素,并在末尾添加一个水平和。需要收集位并制作矢量掩码。除了M = 32的情况外,大概有点困难,在这种情况下,位跨度已经使它们与连续的32位浮点数对齐。
  • 使用4或8个掩码位的连续组来累加4或8个输出元素的向量。因此,您可以对外循环进行矢量化处理,并在输入向量的内循环中进行广播加载。 使用此功能。您实际上应该展开多个矢量和以隐藏FP添加延迟。

__m256 v = _mm256_set1_ps(invec[i])这样的广播负载基本上是免费的(vbroadcastss是纯负载uop,没有ALU随机播放uop)。您甚至不需要在循环末尾进行任何其他的浮点调整,也只需纯垂直SIMD:您只需_mm256_storeu_ps进入输出向量。

并且您正在使用连续的掩码位组,因此通常使用的反移动掩码Q&A很有用。

  // 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,每个FP元素添加1位对于位图加载也没有太大带宽。

但是,您将输入向量循环了M/(8*unroll)次。每个和向量的掩码加法使用不同的掩码位,但使用相同的广播输入float。由于矩阵元素比向量元素小32倍,所以还不错。

每4x或8x vaddps指令加载一个浮点数是非常好的计算强度。尤其是在没有AVX512的情况下,位图->矢量掩码将耗时。

为进一步帮助缓存/内存带宽,可能有 cache-blocking /循环平铺以获取L2缓存大小(256kiB),可以帮助重复使用输入矢量元素。但是我不确定您是否可以有效地阻止输入和输出。与mat * mat产品不同,仅需O(n ^ 2)即可完成。重新读取输入并只编写一个输出流可能很好,但是您可以找到一个中间立场,将部分结果添加到输出向量的部分块中。但是,现在不再需要在一个连续流中读取位矩阵。只要您在高速缓存行边界处停止,就可以了。


如果您的NxM矩阵恰好具有M = 32的大小,则该矩阵恰好与float的大小匹配,并且_mm256_loadu_si256将获得一个向量,该向量具有{ {1}}位于每个元素的低位。高位outvec[0]的掩码位。您可以使用outvec[31]将它们应用于和的输入,并向左移1以将下一位向上移动到顶部。 (_mm256_blendv_ps的替代方法是vblendvps除以31 + psrad:算术右移以将最高位广播到所有位置。)

但是,即使对于这种特殊情况,这也可能不会比其他方法更好。您可以展开不同向量中的多个输出元素,以便可以多次使用float向量。


使用AVX512F,您可以仅将矩阵行用作a masked add like _mm512_mask_add_psandps值。
__mmask16,如果sum = _mm512_mask_add_ps(sum, matrix[col*rowstride + row], sum, invec[i]);matrix的数组。

或者使用AVX512BW,将uint16_t的64位掩码放入kmovq寄存器中,然后k降低,以与展开4个向量累加器相匹配。不幸的是,kshift在Skylake-X上是2块:负载+端口5,而不仅仅是可以写入掩码reg的负载uop。因此,用kmov k, [mem]加载3倍解压缩是绝对的胜利,而使用4倍kshift / kmovw k1, [mem]等则无济于事。无法在{{1}的底部获取每个16位掩码数据}每个端口都没有注册port5 uop。因此,在具有2个FMA单元的SKX内核上,它可以与512位FMA / add / mul吞吐量相竞争,否则前端吞吐量成本就很高。