我需要执行以下操作:
w[i] = scale * v[i] + point
比例和点是固定的,而v[]
是4比特整数的向量。
我需要为任意输入向量w[]
计算v[]
,我想使用AVX内在函数来加速进程。但是,v[i]
是4位整数的向量。
问题是如何使用内在函数对4位整数执行操作?我可以使用8位整数并以这种方式执行操作,但有没有办法执行以下操作:
[a,b] + [c,d] = [a+b,c+d]
[a,b] * [c,d] = [a * b,c * d]
(忽略溢出)
使用AVX内在函数,其中[...,...]是一个8位整数,a,b,c,d是4位整数?
如果是,是否可以举一个简短的例子来说明这是如何运作的?
答案 0 :(得分:5)
只是部分答案(仅添加)和伪代码(应该很容易扩展到AVX2内在函数):
uint8_t a, b; // input containing two nibbles each
uint8_t c = a + b; // add with (unwanted) carry between nibbles
uint8_t x = a ^ b ^ c; // bits which are result of a carry
x &= 0x10; // only bit 4 is of interest
c -= x; // undo carry of lower to upper nibble
如果已知a
或b
有4位未设置(即高位半字节的最低位),则可以省略x
的计算。
至于乘法:如果scale
对于所有产品都是相同的,那么你可以通过一些移位和加/减来避免(在必要时屏蔽掉溢出位)。否则,我担心你需要掩盖每个16位字的4位,进行操作,并在最后将它们拼凑在一起。伪码(没有AVX 8bit乘法,所以我们需要用16bit字操作):
uint16_t m0=0xf, m1=0xf0, m2=0xf00, m3=0xf000; // masks for each nibble
uint16_t a, b; // input containing 4 nibbles each.
uint16_t p0 = (a*b) & m0; // lowest nibble, does not require masking a,b
uint16_t p1 = ((a>>4) * (b&m1)) & m1;
uint16_t p2 = ((a>>8) * (b&m2)) & m2;
uint16_t p3 = ((a>>12)* (b&m3)) & m3;
uint16_t result = p0 | p1 | p2 | p3; // join results together
答案 1 :(得分:2)
对于w[i]=v[i] * a + b
中固定的 a , b ,您只需为LSB使用查找表w_0_3 = _mm_shuffle_epi8(LUT_03, input)
。将输入分为偶数和奇数位,奇数LUT预移位4。
auto a = input & 15; // per element
auto b = (input >> 4) & 15; // shift as 16 bits
return LUTA[a] | LUTB[b];
如果有的话,如何动态生成那些LUT是另一个问题。
答案 2 :(得分:1)
AVX2
完成4位加/乘运算,特别是如果您想将这些计算应用于较大的矢量(例如,超过128个元素)时,尤其如此。但是,如果只想添加4个数字,请使用直接标量代码。
我们已经在如何处理4位整数方面做了大量工作,并且最近我们开发了一个库Clover: 4-bit Quantized Linear Algebra Library(着重于量化)。该代码也是available at GitHub。
正如您仅提到的 4位整数,我假设您是指带符号的整数(即2的补码),并据此做出我的回答。请注意,处理无符号实际上要简单得多。
我还假设您想获取包含int8_t v[n/2]
个4位整数的向量n
,并产生具有int8_t v_sum[n/4]
个4位整数的n/2
。相对于以下描述的所有代码均为available as a gist。
很显然,AVX2
没有提供任何指令来对4位整数执行加法/乘法运算,因此,您必须求助于给定的8位或16位指令。处理4位算术的第一步是设计有关如何将4位半字节放入8位,16位或32位大块中的方法。
为清楚起见,让我们假设您要从一个32位块中解压缩给定的半字节,该块将多个4位带符号的值存储到相应的32位整数中(下图)。可以通过两个位移来完成:
算术右移具有符号扩展,用半字节的符号位填充高阶28位。产生一个32位整数,其值与两者的补码4位值相同。
打包的目的(上图的左侧)是恢复拆包操作。可以使用两个位移位将32位整数的最低4位放置在32位实体中的任何位置。
第一个将比半字节低位的位设置为零,第二个将比半字节高位的位设置为零。然后,按位或运算可用于在32位实体中存储多达八个半字节。
如何在实践中应用?
让我们假设您有64 x 32位整数值存储在8个AVX
寄存器__m256i q_1, q_2, q_3, q_4, q_5, q_6, q_7, q_8
中。我们还假设每个值都在[-8,7]范围内。如果要将它们打包到一个64个4位值的AVX
寄存器中,可以执行以下操作:
//
// Transpose the 8x8 registers
//
_mm256_transpose8_epi32(q_1, q_2, q_3, q_4, q_5, q_6, q_7, q_8);
//
// Shift values left
//
q_1 = _mm256_slli_epi32(q_1, 28);
q_2 = _mm256_slli_epi32(q_2, 28);
q_3 = _mm256_slli_epi32(q_3, 28);
q_4 = _mm256_slli_epi32(q_4, 28);
q_5 = _mm256_slli_epi32(q_5, 28);
q_6 = _mm256_slli_epi32(q_6, 28);
q_7 = _mm256_slli_epi32(q_7, 28);
q_8 = _mm256_slli_epi32(q_8, 28);
//
// Shift values right (zero-extend)
//
q_1 = _mm256_srli_epi32(q_1, 7 * 4);
q_2 = _mm256_srli_epi32(q_2, 6 * 4);
q_3 = _mm256_srli_epi32(q_3, 5 * 4);
q_4 = _mm256_srli_epi32(q_4, 4 * 4);
q_5 = _mm256_srli_epi32(q_5, 3 * 4);
q_6 = _mm256_srli_epi32(q_6, 2 * 4);
q_7 = _mm256_srli_epi32(q_7, 1 * 4);
q_8 = _mm256_srli_epi32(q_8, 0 * 4);
//
// Pack together
//
__m256i t1 = _mm256_or_si256(q_1, q_2);
__m256i t2 = _mm256_or_si256(q_3, q_4);
__m256i t3 = _mm256_or_si256(q_5, q_6);
__m256i t4 = _mm256_or_si256(q_7, q_8);
__m256i t5 = _mm256_or_si256(t1, t2);
__m256i t6 = _mm256_or_si256(t3, t4);
__m256i t7 = _mm256_or_si256(t5, t6);
移位通常需要1个周期的吞吐量和1个周期的延迟,因此您可以认为实际上是非常便宜的。如果必须处理无符号的4位值,则可以一起跳过所有左移。
要取消此过程,可以应用相同的方法。假设您已将64个4位值加载到单个AVX
寄存器__m256i qu_64
中。为了产生64 x 32位整数__m256i q_1, q_2, q_3, q_4, q_5, q_6, q_7, q_8
,可以执行以下操作:
//
// Shift values left
//
const __m256i qu_1 = _mm256_slli_epi32(qu_64, 4 * 7);
const __m256i qu_2 = _mm256_slli_epi32(qu_64, 4 * 6);
const __m256i qu_3 = _mm256_slli_epi32(qu_64, 4 * 5);
const __m256i qu_4 = _mm256_slli_epi32(qu_64, 4 * 4);
const __m256i qu_5 = _mm256_slli_epi32(qu_64, 4 * 3);
const __m256i qu_6 = _mm256_slli_epi32(qu_64, 4 * 2);
const __m256i qu_7 = _mm256_slli_epi32(qu_64, 4 * 1);
const __m256i qu_8 = _mm256_slli_epi32(qu_64, 4 * 0);
//
// Shift values right (sign-extent) and obtain 8x8
// 32-bit values
//
__m256i q_1 = _mm256_srai_epi32(qu_1, 28);
__m256i q_2 = _mm256_srai_epi32(qu_2, 28);
__m256i q_3 = _mm256_srai_epi32(qu_3, 28);
__m256i q_4 = _mm256_srai_epi32(qu_4, 28);
__m256i q_5 = _mm256_srai_epi32(qu_5, 28);
__m256i q_6 = _mm256_srai_epi32(qu_6, 28);
__m256i q_7 = _mm256_srai_epi32(qu_7, 28);
__m256i q_8 = _mm256_srai_epi32(qu_8, 28);
//
// Transpose the 8x8 values
//
_mm256_transpose8_epi32(q_1, q_2, q_3, q_4, q_5, q_6, q_7, q_8);
如果处理无符号4位,则可以完全跳过右移(_mm256_srai_epi32
),并且可以执行逻辑左移(_mm256_srli_epi32
)来代替左移。 / p>
要查看更多详细信息,请查看gist here。
假设您使用AVX
从向量加载:
const __m256i qv = _mm256_loadu_si256( ... );
现在,我们可以轻松提取奇数和偶数部分。如果AVX2
中有8位的移位,生活会容易得多,但是没有任何移位,因此我们必须处理16位的移位:
const __m256i hi_mask_08 = _mm256_set1_epi8(-16);
const __m256i qv_odd_dirty = _mm256_slli_epi16(qv, 4);
const __m256i qv_odd_shift = _mm256_and_si256(hi_mask_08, qv_odd_dirty);
const __m256i qv_evn_shift = _mm256_and_si256(hi_mask_08, qv);
此时,您已经在两个AVX
寄存器中将奇数和偶数半字节基本分开,这些寄存器将其值保持在高4位(即[-8 * 2 ^范围内的值4,7 * 2 ^ 4])。即使处理无符号4位值,该过程也相同。现在是时候添加值了。
const __m256i qv_sum_shift = _mm256_add_epi8(qv_odd_shift, qv_evn_shift);
这将适用于有符号和无符号,因为二进制加法使用二进制补码。但是,如果要避免溢出或下溢,还可以考虑添加AVX中已支持的饱和度(对于有符号和无符号):
__m256i _mm256_adds_epi8 (__m256i a, __m256i b)
__m256i _mm256_adds_epu8 (__m256i a, __m256i b)
qv_sum_shift
的范围为[-8 * 2 ^ 4,7 * 2 ^ 4]。要将其设置为正确的值,我们需要将其移回(请注意,如果qv_sum
必须为无符号,我们可以改用_mm256_srli_epi16
):
const __m256i qv_sum = _mm256_srai_epi16(qv_sum_shift, 4);
求和现在已完成。根据您的用例,这也可能是程序的结尾,假设您要产生8位内存块。但是,假设您要解决更难的问题。假设输出还是4位元素的向量,并且存储器的布局与输入相同。在这种情况下,我们需要将8位块打包为4位块。但是,问题在于,我们将拥有32个值(即向量大小的一半)而不是拥有64个值。
至此,有两个选择。我们可以在向量中向前看,处理128 x 4位值,这样我们就可以产生64 x 4位值。或者我们回到SSE,处理32 x 4位值。无论哪种方式,将8位块打包为4位块的最快方法都是使用vpackuswb
(对于packuswb
来说是SSE
)指令:
__m256i _mm256_packus_epi16 (__m256i a, __m256i b)
此指令使用无符号饱和度将a
和b
的压缩16位整数转换为压缩8位整数,并将结果存储在dst
中。这意味着我们必须交织奇数和偶数4位值,以便它们驻留在16位内存块的8个低位中。我们可以进行如下操作:
const __m256i lo_mask_16 = _mm256_set1_epi16(0x0F);
const __m256i hi_mask_16 = _mm256_set1_epi16(0xF0);
const __m256i qv_sum_lo = _mm256_and_si256(lo_mask_16, qv_sum);
const __m256i qv_sum_hi_dirty = _mm256_srli_epi16(qv_sum_shift, 8);
const __m256i qv_sum_hi = _mm256_and_si256(hi_mask_16, qv_sum_hi_dirty);
const __m256i qv_sum_16 = _mm256_or_si256(qv_sum_lo, qv_sum_hi);
有符号和无符号4位值的过程将相同。现在,qv_sum_16
包含两个连续的4位值,存储在16位内存块的低位中。假设我们从下一个迭代中获得了qv_sum_16
(称为qv_sum_16_next
),我们可以将所有内容打包为:
const __m256i qv_sum_pack = _mm256_packus_epi16(qv_sum_16, qv_sum_16_next);
const __m256i result = _mm256_permute4x64_epi64(qv_sum_pack, 0xD8);
或者,如果我们只想生成32 x 4位值,则可以执行以下操作:
const __m128i lo = _mm256_extractf128_si256(qv_sum_16, 0);
const __m128i hi = _mm256_extractf128_si256(qv_sum_16, 1);
const __m256i result = _mm_packus_epi16(lo, hi)
将它们放在一起
假设有符号的半字节和向量大小n
,使得n
大于128个元素并且是128的倍数,我们可以执行奇偶加法,生成n/2
个元素如下:
void add_odd_even(uint64_t n, int8_t * v, int8_t * r)
{
//
// Make sure that the vector size that is a multiple of 128
//
assert(n % 128 == 0);
const uint64_t blocks = n / 64;
//
// Define constants that will be used for masking operations
//
const __m256i hi_mask_08 = _mm256_set1_epi8(-16);
const __m256i lo_mask_16 = _mm256_set1_epi16(0x0F);
const __m256i hi_mask_16 = _mm256_set1_epi16(0xF0);
for (uint64_t b = 0; b < blocks; b += 2) {
//
// Calculate the offsets
//
const uint64_t offset0 = b * 32;
const uint64_t offset1 = b * 32 + 32;
const uint64_t offset2 = b * 32 / 2;
//
// Load 128 values in two AVX registers. Each register will
// contain 64 x 4-bit values in the range [-8, 7].
//
const __m256i qv_1 = _mm256_loadu_si256((__m256i *) (v + offset0));
const __m256i qv_2 = _mm256_loadu_si256((__m256i *) (v + offset1));
//
// Extract the odd and the even parts. The values will be split in
// two registers qv_odd_shift and qv_evn_shift, each of them having
// 32 x 8-bit values, such that each value is multiplied by 2^4
// and resides in the range [-8 * 2^4, 7 * 2^4]
//
const __m256i qv_odd_dirty_1 = _mm256_slli_epi16(qv_1, 4);
const __m256i qv_odd_shift_1 = _mm256_and_si256(hi_mask_08, qv_odd_dirty_1);
const __m256i qv_evn_shift_1 = _mm256_and_si256(hi_mask_08, qv_1);
const __m256i qv_odd_dirty_2 = _mm256_slli_epi16(qv_2, 4);
const __m256i qv_odd_shift_2 = _mm256_and_si256(hi_mask_08, qv_odd_dirty_2);
const __m256i qv_evn_shift_2 = _mm256_and_si256(hi_mask_08, qv_2);
//
// Perform addition. In case of overflows / underflows, behaviour
// is undefined. Values are still in the range [-8 * 2^4, 7 * 2^4].
//
const __m256i qv_sum_shift_1 = _mm256_add_epi8(qv_odd_shift_1, qv_evn_shift_1);
const __m256i qv_sum_shift_2 = _mm256_add_epi8(qv_odd_shift_2, qv_evn_shift_2);
//
// Divide by 2^4. At this point in time, each of the two AVX registers holds
// 32 x 8-bit values that are in the range of [-8, 7]. Summation is complete.
//
const __m256i qv_sum_1 = _mm256_srai_epi16(qv_sum_shift_1, 4);
const __m256i qv_sum_2 = _mm256_srai_epi16(qv_sum_shift_2, 4);
//
// Now, we want to take the even numbers of the 32 x 4-bit register, and
// store them in the high-bits of the odd numbers. We do this with
// left shifts that extend in zero, and 16-bit masks. This operation
// results in two registers qv_sum_lo and qv_sum_hi that hold 32
// values. However, each consecutive 4-bit values reside in the
// low-bits of a 16-bit chunk.
//
const __m256i qv_sum_1_lo = _mm256_and_si256(lo_mask_16, qv_sum_1);
const __m256i qv_sum_1_hi_dirty = _mm256_srli_epi16(qv_sum_shift_1, 8);
const __m256i qv_sum_1_hi = _mm256_and_si256(hi_mask_16, qv_sum_1_hi_dirty);
const __m256i qv_sum_2_lo = _mm256_and_si256(lo_mask_16, qv_sum_2);
const __m256i qv_sum_2_hi_dirty = _mm256_srli_epi16(qv_sum_shift_2, 8);
const __m256i qv_sum_2_hi = _mm256_and_si256(hi_mask_16, qv_sum_2_hi_dirty);
const __m256i qv_sum_16_1 = _mm256_or_si256(qv_sum_1_lo, qv_sum_1_hi);
const __m256i qv_sum_16_2 = _mm256_or_si256(qv_sum_2_lo, qv_sum_2_hi);
//
// Pack the two registers of 32 x 4-bit values, into a single one having
// 64 x 4-bit values. Use the unsigned version, to avoid saturation.
//
const __m256i qv_sum_pack = _mm256_packus_epi16(qv_sum_16_1, qv_sum_16_2);
//
// Interleave the 64-bit chunks.
//
const __m256i qv_sum = _mm256_permute4x64_epi64(qv_sum_pack, 0xD8);
//
// Store the result
//
_mm256_storeu_si256((__m256i *)(r + offset2), qv_sum);
}
}
此代码的独立测试者和验证者是available in the gist here。
对于奇数和偶数项的乘法,我们可以使用如上所述的相同策略将4位提取为更大的块。
AVX2
不提供8位乘法,仅提供16位。但是,我们可以按照Agner Fog's C++ vector class library中实现的方法来实现8位乘法:
static inline Vec32c operator * (Vec32c const & a, Vec32c const & b) {
// There is no 8-bit multiply in SSE2. Split into two 16-bit multiplies
__m256i aodd = _mm256_srli_epi16(a,8); // odd numbered elements of a
__m256i bodd = _mm256_srli_epi16(b,8); // odd numbered elements of b
__m256i muleven = _mm256_mullo_epi16(a,b); // product of even numbered elements
__m256i mulodd = _mm256_mullo_epi16(aodd,bodd); // product of odd numbered elements
mulodd = _mm256_slli_epi16(mulodd,8); // put odd numbered elements back in place
__m256i mask = _mm256_set1_epi32(0x00FF00FF); // mask for even positions
__m256i product = selectb(mask,muleven,mulodd); // interleave even and odd
return product;
}
但是我建议先将半字节提取为16位块,然后使用_mm256_mullo_epi16
以避免执行不必要的移位。