我想将大小为N的浮点向量与大小为NxM的矩阵相乘。
矩阵是二进制矩阵(仅包含零和1),并且相对稀疏:非零值的密度在1%到5%之间。
当前,我将其形成为密集矢量和稀疏浮点矩阵乘法。
但这只是一个过度的杀伤力,不是吗?
如果我将矩阵的列存储为位集,然后进行乘法运算,那就简单地使用位集索引向量,然后对其求和。
我假设我可以将其形成为SSE / AVX中的矢量化操作,例如load +和+ sum或load + mask + sum
如果能为我指出正确的内在函数,我将不胜感激,主要问题是处理位集解包的最佳方法是什么?
答案 0 :(得分:3)
因此,结果向量的每个元素都是输入向量的掩码和?而且这些掩码来自矩阵的列,因此它们不是连续的位。
使用连续位图的屏蔽和对于AVX512来说微不足道(只需使用合并屏蔽的加法或零屏蔽的负载)。对于SSE / AVX2,您将使用is there an inverse instruction to the movemask instruction in intel avx2? + _mm256_and_ps
。或针对跨掩码向量进行优化的变体,例如具有32位广播负载,然后将其转移到下一步。而不是为每个字节广播另一个未对齐的双字。
但是您的掩码位不是连续的,您可以选择:
像__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_ps
的andps
值。
__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吞吐量相竞争,否则前端吞吐量成本就很高。