【问题标题】:int8 x uint8 matrix-vector product with column-major layout具有列优先布局的 int8 x uint8 矩阵向量乘积
【发布时间】:2021-11-20 08:16:20
【问题描述】:

我希望使用 AVX-1 或更早的指令来加速这个矩阵向量乘积:

    // a is an array N columns of length M each
    // b is length N
    // c is length M
    //
    // M % 32 == N % 32 == 0
    // all memory is nicely aligned and unaliased
    
    void mat_vec_prod(const int8_t** a, const uint8_t* b, int16_t* c) {
        for(int i = 0; i < M; ++i) {
            c[i] = 0;
            for(int j = 0; j < N; ++j)
                c[i] += int16_t(a[j][i]) * int16_t(b[j]);
        }
    }

(我知道交换循环值得考虑)

内在函数 _mm_maddubs_epi16_mm_maddubs_pi16 可以帮助处理 uint8 x int8 点积,但在我的例子中,矩阵的布局很尴尬,它是指向列(而不是行)的指针数组。

一种可能性是加载a 的8x8 块,然后将它们转置并乘以b 的片段。 (我在 8x8 字节矩阵转置上找到了 this thread)。但是,这必须使用_mm_maddubs_pi16,它的吞吐量只有_mm_maddubs_epi16 的一半。

我的问题是:是否值得尝试加载和转置 16x16 补丁,或者我会用完 xmm 寄存器?我的策略应该是什么?

【问题讨论】:

  • 可以使用AVX2指令吗?有些人说 AVX 时的意思是 AVX/AVX2。
  • @fuz 不,只是 AVX。请编辑,如果有办法使这一点更清楚。
  • 呃...针对 Sandy Bridge 或这一代 AMD 进行优化很烦人。让我们看看...
  • 我将[v]punpcklbw a[j][i:i+7]a[j+1][i:i+7] 的两个向量并使用 [v]pmaddubsw 乘以 {b[j], b[j+1],b[j], b[j+1],...} (然后累积这些结果)。根据实际大小,您将需要某种阻塞(即,不要为每次添加重新加载 c 子向量。)如果没有 vpbroadcastw,创建 b-multiplier 有点烦人,我猜是 @ 987654339@ 后跟 [v]pshufd 应该可以解决问题。您应该将b-multiplier 重复用于累积c 的尽可能多的寄存器。
  • @chtz 我实现了你的建议(至少按照我的理解)。比原始版本快 6 倍,可能会影响内存带宽。非常感谢!

标签: assembly x86 simd sse avx


【解决方案1】:

我会采用 chtz 建议的交错方法。

从两行中读取 32 或 64 个字节(也就是一个完整的缓存行),然后交错。

至少 32 字节,因为每行的宽度 % 32 == 0,最好是 64 字节,因为这是一个完整的高速缓存行,它将占用 16 个寄存器中的 8 个累加器。

另外我猜想将输入处理为(8、16 或 32 行)×(32 或 64 列)的块会比处理所有行更好;处理的行越多,将累加器溢出到内存的需求就越少,以非线性顺序处理的行越多,从缓存中驱逐即将需要的行的可能性就越高。 4 行应该是绝对安全的。

交错b 很自然地由

完成
auto b0to7 = _mm_unpacklo_epi16(b,b);
auto b8tof = _mm_unpackhi_epi16(b,b);
auto b01 = _mm_shuffle_epi32(b0to7, 0x00);
auto b23 = _mm_shuffle_epi32(b0to7, 0x55);
...
auto bef = _mm_shuffle_epi32(b8tof, 0xff);

另一种将输入拆分为偶数/奇数序列的可能性是每 16 字节需要 4 条算术指令,或每 32 字节需要 8 条指令:

// common terms
auto b_even = _mm_set1_epi16(b[j] & 0x00ff);
auto b_odd = _mm_set1_epi16(b[j] * 256);
// per 16 input bytes in `a`
auto mul_even = _mm_maddubs_epi16(row_j, b_even);
auto mul_odd = _mm_maddubs_epi16(row_j, b_odd);
sum_even = _mm_add_epi16(sum_even, mul_even);
sum_odd = _mm_add_epi16(mul_odd, mul_even);

这显然没有那么紧

auto prod_lo = _mm_unpacklo_epi8(row_j, row_jplus1);
auto prod_hi = _mm_unpackhi_epi8(row_j, row_jplus1);
prod_lo = _mm_maddubs_epi16(prod_lo, b01);
prod_hi = _mm_maddubs_epi16(prod_hi, b01);
sum_lo = _mm_add_epi16(sum_lo, prod_lo);
sum_hi = _mm_add_epi16(sum_hi, prod_hi);

但是 shuffle 只能在 Port5 上执行,因为 2 mul/adds 可以在每个周期开始。它们的性能可能非常接近。

【讨论】:

  • 在 SandyBridge/IvyBridge(仅限英特尔 CPU 和 AVX1)上解包/洗牌(以及paddw)似乎发生在p1p5 上,但pmaddubs 是有限的到p0。但无论如何,解包,pmaddubspaddw 应该很好地分布在较新的 CPU 上的可用端口上(如果 AVX2 可用,吞吐量当然可以翻倍,还有一些额外的上半和下半解洗开销)
猜你喜欢
  • 2017-07-24
  • 2018-12-05
  • 1970-01-01
  • 2021-12-05
  • 2021-06-28
  • 2014-11-06
  • 2019-04-21
  • 1970-01-01
  • 2018-04-21
相关资源
最近更新 更多