内積命令


はじめに

$ grep ^vdot /usr/lib/gcc/aarch64-linux-gnu/7.5.0/include/arm_neon.h
vdot_u32 (uint32x2_t __r, uint8x8_t __a, uint8x8_t __b)
vdotq_u32 (uint32x4_t __r, uint8x16_t __a, uint8x16_t __b)
vdot_s32 (int32x2_t __r, int8x8_t __a, int8x8_t __b)
vdotq_s32 (int32x4_t __r, int8x16_t __a, int8x16_t __b)
vdot_lane_u32 (uint32x2_t __r, uint8x8_t __a, uint8x8_t __b, const int __index)
vdot_laneq_u32 (uint32x2_t __r, uint8x8_t __a, uint8x16_t __b,
vdotq_lane_u32 (uint32x4_t __r, uint8x16_t __a, uint8x8_t __b,
vdotq_laneq_u32 (uint32x4_t __r, uint8x16_t __a, uint8x16_t __b,
vdot_lane_s32 (int32x2_t __r, int8x8_t __a, int8x8_t __b, const int __index)
vdot_laneq_s32 (int32x2_t __r, int8x8_t __a, int8x16_t __b, const int __index)
vdotq_lane_s32 (int32x4_t __r, int8x16_t __a, int8x8_t __b, const int __index)
vdotq_laneq_s32 (int32x4_t __r, int8x16_t __a, int8x16_t __b, const int __index)

dot命令

  • 引数はint32x4_t型を1つ、int8x16_t型を2つ取る。もしくは符号なしの全く同じ型を引数に取る
  • 第2引数と第3引数の各要素ごとに積を求め、4つずつまとめて総和を取る
  • そして第1引数に加算する

dot_lane命令

  • 4要素の積をとって1つの和に集約して、第1引数に加算する演算はdot命令と同じ
  • 対応する要素を使うのでなく、第3引数のうち、第4引数で指定したレーンを使用する
    • なお、第3引数はint8x16_t型だが、指定できるレーンは[0:3]の範囲。つまり、第3引数は要素が16個あるが、4要素ごとの集まりとみなしている
  • dup_lane命令と同じくqが付く場所によって、4パターン存在する
命令 第1、第2引数及び戻り値のbit幅 第3引数のbit幅
dot_lane 64bit 64bit
dotq_lane 128bit 64bit
dot_laneq 64bit 128bit
dotq_laneq 128bit 128bit

OpenCVでの対応状況

intrin_neon.hpp
inline v_uint32x4 v_dotprod_expand(const v_uint8x16& a, const v_uint8x16& b)
{
#if CV_NEON_DOT
    return v_uint32x4(vdotq_u32(vdupq_n_u32(0), a.val, b.val));
#else
    const uint8x16_t zero   = vreinterpretq_u8_u32(vdupq_n_u32(0));
    const uint8x16_t mask   = vreinterpretq_u8_u32(vdupq_n_u32(0x00FF00FF));
    const uint16x8_t zero32 = vreinterpretq_u16_u32(vdupq_n_u32(0));
    const uint16x8_t mask32 = vreinterpretq_u16_u32(vdupq_n_u32(0x0000FFFF));

    uint16x8_t even = vmulq_u16(vreinterpretq_u16_u8(vbslq_u8(mask, a.val, zero)),
                                vreinterpretq_u16_u8(vbslq_u8(mask, b.val, zero)));
    uint16x8_t odd  = vmulq_u16(vshrq_n_u16(vreinterpretq_u16_u8(a.val), 8),
                                vshrq_n_u16(vreinterpretq_u16_u8(b.val), 8));

    uint32x4_t s0 = vaddq_u32(vreinterpretq_u32_u16(vbslq_u16(mask32, even, zero32)),
                              vreinterpretq_u32_u16(vbslq_u16(mask32, odd,  zero32)));
    uint32x4_t s1 = vaddq_u32(vshrq_n_u32(vreinterpretq_u32_u16(even), 16),
                              vshrq_n_u32(vreinterpretq_u32_u16(odd),  16));
    return v_uint32x4(vaddq_u32(s0, s1));
#endif
}
  • v_dotprod_expand命令の内部でvdotq_u32が使われているのが確認できる
  • しかし、肝心のCV_NEON_DOT0で決め打ちなので、ちゃんとまだサポートされてないっぽい
intrin_neon.hpp
// TODO
#define CV_NEON_DOT 0

追記

  • 初日にArm v8.2命令には深入りしない、と書いたものの、この命令はdotprod拡張命令であり、Arm v8.2で導入された拡張命令の一部である
  • 残念ながら手元にはdotprod命令に対応したSoCを載せたSBCは無いので、解説はこれぐらいで

おわりに

  • 今日は内積を計算するdotprod命令を紹介した1
  • 明日も手島の執筆予定で、何を書こう。。。。

  1. 内積命令と言っているけれど、8bit4つをとって32bitに足し合わせる挙動はINT8のDNN推論演算そのものである。なので、おそらくDeep Learningのブームを背景に追加された命令だと推測する(個人の感想です)