【问题标题】:How to convert 32-bit float to 8-bit signed char? (4:1 packing of int32 to int8 __m256i)如何将 32 位浮点数转换为 8 位有符号字符? (int32 到 int8 __m256i 的 4:1 打包)
【发布时间】:2019-01-17 14:38:05
【问题描述】:

我想做的是:

  1. 将输入浮点数乘以一个固定因子。
  2. 将它们转换为 8 位有符号字符。

请注意,大多数输入的绝对值范围都很小,例如 [-6, 6],因此固定因子可以将它们映射到 [-127, 127]。

我只处理 avx2 指令集,因此不能使用像 _mm256_cvtepi32_epi8 这样的内在函数。我想使用_mm256_packs_epi16,但它将两个输入混合在一起。 :(

我还编写了一些将 32 位浮点数转换为 16 位整数的代码,它完全符合我的要求。

void Quantize(const float* input, __m256i* output, float quant_mult, int num_rows, int width) {
  // input is a matrix actuaaly, num_rows and width represent the number of rows and columns of the matrix
  assert(width % 16 == 0);

  int num_input_chunks = width / 16;

  __m256 avx2_quant_mult = _mm256_set_ps(quant_mult, quant_mult, quant_mult, quant_mult,
                                     quant_mult, quant_mult, quant_mult, quant_mult);

  for (int i = 0; i < num_rows; ++i) {
    const float* input_row = input + i * width;
    __m256i* output_row = output + i * num_input_chunks;
    for (int j = 0; j < num_input_chunks; ++j) {
      const float* x = input_row + j * 16;
      // Process 16 floats at once, since each __m256i can contain 16 16-bit integers.

      __m256 f_0 = _mm256_loadu_ps(x);
      __m256 f_1 = _mm256_loadu_ps(x + 8);

      __m256 m_0 = _mm256_mul_ps(f_0, avx2_quant_mult);
      __m256 m_1 = _mm256_mul_ps(f_1, avx2_quant_mult);

      __m256i i_0 = _mm256_cvtps_epi32(m_0);
      __m256i i_1 = _mm256_cvtps_epi32(m_1);

      *(output_row + j) = _mm256_packs_epi32(i_0, i_1);
    }
  }
}

欢迎任何帮助,非常感谢!

【问题讨论】:

  • 截断可以吗?使用_mm256_shuffle_epi8。否则使用pack(same,same),或者更好地将4个浮点向量打包成int8_t的1个向量,分多个步骤:2x epi32和1x epi16。 (然后使用单个vpermq 修复通道内排序)。使用 128 位 epi32 的示例参见 SSE - AVX conversion from double to char -> epi8
  • 车道交叉修正类似于 float->int16 的情况:How can I convert a vector of float to short int using avx instructions?。奇怪的是,_mm256_packs_epi16 的 SO(除此之外)没有命中,因此不存在与此完全相同的副本。
  • @PeterCordes 截断没问题。顺便说一句,你能告诉我哪个解决方案最快,吞吐量是绝对标准吗?谢谢!
  • 你有多个输入浮点向量,所以 2x vpackssdw + 1x vpacksswb + 1x vpermd 从 4 个输入向量中产生 1 个宽向量优于 4x vpshufb + 4x vpermd + 4x 存储。

标签: c x86 simd intrinsics avx2


【解决方案1】:

对于具有多个源向量的良好吞吐量,_mm256_packs_epi16 有 2 个输入向量而不是产生更窄的输出是一件好事。 (AVX512 _mm256_cvtepi32_epi8 不一定是最有效的处理方式,因为具有内存目标的版本会解码为多个微指令,或者常规版本会为您提供需要单独存储的多个小输出。)

或者你在抱怨它在车道上的运作方式?是的,这很烦人,但_mm256_packs_epi32 做同样的事情。如果您的输出可以在其中包含交错的数据组,请也为此执行相同的操作。

最好的办法是在车道内打包的 2 个步骤中将 4 个向量合并为 1 个(因为没有车道交叉打包)。然后使用一个过道洗牌来修复它。

#include <immintrin.h>
// loads 128 bytes = 32 floats
// converts and packs with signed saturation to 32 int8_t
__m256i pack_float_int8(const float*p) {
    __m256i a = _mm256_cvtps_epi32(_mm256_loadu_ps(p));
    __m256i b = _mm256_cvtps_epi32(_mm256_loadu_ps(p+8));
    __m256i c = _mm256_cvtps_epi32(_mm256_loadu_ps(p+16));
    __m256i d = _mm256_cvtps_epi32(_mm256_loadu_ps(p+24));
    __m256i ab = _mm256_packs_epi32(a,b);        // 16x int16_t
    __m256i cd = _mm256_packs_epi32(c,d);
    __m256i abcd = _mm256_packs_epi16(ab, cd);   // 32x int8_t
    // packed to one vector, but in [ a_lo, b_lo, c_lo, d_lo | a_hi, b_hi, c_hi, d_hi ] order
    // if you can deal with that in-memory format (e.g. for later in-lane unpack), great, you're done

    // but if you need sequential order, then vpermd:
    __m256i lanefix = _mm256_permutevar8x32_epi32(abcd, _mm256_setr_epi32(0,4, 1,5, 2,6, 3,7));
    return lanefix;
}

(Compiles nicely on the Godbolt compiler explorer).

在循环中调用它并_mm256_store_si256 得到结果向量。


(对于 uint8_t 未签名的目的地,使用 _mm256_packus_epi16 进行 16->8 步骤并保持其他所有内容相同。我们仍然使用带符号的 32->16 打包,因为 16 -> u8 vpackuswb 打包仍将其epi16 输入 视为已签名。您需要将-1 视为-1,而不是+0xFFFF,以便无符号饱和将其钳制为0。)


每个 256 位存储总共 4 次 shuffle,每个时钟吞吐量 1 次 shuffle 将成为 Intel CPU 的瓶颈。您应该获得每个时钟一个浮点向量的吞吐量,在端口 5 上遇到瓶颈。 (https://agner.org/optimize/)。或者,如果 L2 中的数据不热,则可能会出现内存带宽瓶颈。


如果你只有一个单个向量要做,你可以考虑使用_mm256_shuffle_epi8将每个epi32元素的低字节放入每个通道的低32位,然后_mm256_permutevar8x32_epi32过马路。

另一种单向量替代方案(在 Ryzen 上很好)是 extracti128 + 128 位 packssdw + packsswb。但这仍然只有在你只做一个向量时才好。 (仍然在 Ryzen 上,您需要在 128 位向量中工作以避免额外的车道交叉洗牌,因为 Ryzen 将每条 256 位指令拆分为(至少)2 个 128 位微指令。)

相关:

【讨论】:

  • 完美运行。相关信息和详细解释非常有帮助。非常感谢。
  • 彼得,你会为 uint16 而不是 uint8 做什么?
  • @Royi:将_mm256_packs_epi32 替换为_mm256_packus_epi32,并在该步骤后停止。看起来很明显。
  • 是的,我想通了,这就是我所做的。谢谢。
  • 这是我对代码的完整用法 - codereview.stackexchange.com/a/219207/7723
【解决方案2】:

请查看IEEE754标准格式来存储float值,首先了解这个float和double是如何存储在内存中的,然后你才知道如何将float或double转换为char,非常简单。

【讨论】:

  • 这是一个与SIMD和AVX有关的问题。
  • x86 有一条机器指令可以将float 转换为整数(实际上它有多个,用于标量与打包以及旧版 x87)。使用位操作自己做会比我的答案慢,后者在 Haswell 或 Skylake 上每个核心时钟周期转换 8 floats。 IDK 如果您正在谈论将float 打印为十进制字符串,但这个问题是将它们转换为int8_t。对于转换为十进制字符串,是的,您通常确实希望将指数和有效数分开。
  • 我不知道这样的指令,我开始学习这些东西,这就是为什么根据我的知识(在这种状态下),我发布了这个答案,我保证它肯定会起作用。
  • 当您发布它时,OP 已经回复了我的回答,以确认 it 确实有效。如果您只是在学习,我建议您阅读我的答案并点击其中的链接(包括英特尔的内在指南),并且只有在您确信自己的方法有所改进时才发布您自己的答案。并查看 stackoverflow.com/tags/sse/info 了解一些 SIMD 简介。
猜你喜欢
  • 1970-01-01
  • 2014-10-10
  • 1970-01-01
  • 2011-07-24
  • 1970-01-01
  • 2013-05-03
  • 2016-01-03
  • 1970-01-01
  • 2023-03-12
相关资源
最近更新 更多