fp16命令


はじめに

TL;DR

  • fp16命令は、Arm v7とArm v8で微妙に指す範囲が違う。
  • Arm v7 で提供されるのは、fp32(いわゆるfloat)から、fp16への変換と逆変換のみで、厳密には拡張命令
  • Arm v8 では、変換命令も含めて命令セットに含まれたが、fp16のままの演算命令がv8.2で追加された

Arm v7 の場合

  • 実質的に「使える」fp16命令は、以下の2つのみ1
vcvt_f16_f32 (float32x4_t __a)
vcvt_f32_f16 (float16x4_t __a)
  • 前者はfloat16x4_t型を返し、後者はfloat32x4_t型を返す
  • 変換だけじゃ意味ないじゃん、と思うなかれ。GPUに渡したりする際に、予めfp16に変換しておくことで、転送コストを下げられるのである。(昔の筆者のSlideShareとかに書いた)
  • この2命令だけ、NEONの範囲外で、-mfpu=neon-fp16というオプションをGCCにコンパイル時に渡す必要がある
g++ -mfpu=neon-fp16 fp16.cpp
  • また、arm_neon.hにもその様子が見て取れる。以下はRaspberry Pi 3 (OSは32bitなので、Arm v7)のarm_neon.h
arm_neon.h
#pragma GCC push_options
#pragma GCC target ("fpu=neon-fp16")
#if defined (__ARM_FP16_FORMAT_IEEE) || defined (__ARM_FP16_FORMAT_ALTERNATIVE)
__extension__ static __inline float16x4_t __attribute__ ((__always_inline__))
vcvt_f16_f32 (float32x4_t __a)
{
  return (float16x4_t)__builtin_neon_vcvtv4hfv4sf (__a);
}
#endif
#if defined (__ARM_FP16_FORMAT_IEEE) || defined (__ARM_FP16_FORMAT_ALTERNATIVE)
__extension__ static __inline float32x4_t __attribute__ ((__always_inline__))
vcvt_f32_f16 (float16x4_t __a)
{
  return (float32x4_t)__builtin_neon_vcvtv4sfv4hf (__a);
}
#endif
#pragma GCC pop_options
  • #pragma GCC target ("fpu=neon-fp16") が示す通り、-mfpu=neon-fp16オプションが渡されない限りこのセクションは無効で、コンパイルエラーになる
  • このセクションはpush_optionsからpop_optionsまでの間であり、定義されてるのはわずか2つのintrinsic関数のみである
  • これも、/proc/cpuinfoで確認できる
  • 以下は、Raspberry Pi 3 (32bitのRaspbian OS)で/proc/cpuinfoを確認した結果
$ cat /proc/cpuinfo
processor       : 0
model name      : ARMv7 Processor rev 4 (v7l)
BogoMIPS        : 76.80
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU revision    : 4
 :
  • 初日に表した通り、Featuresの行にneonの文字も見えるが、先頭にあるのはhalfである
  • このhalfフラグが立ってることで前述の2命令が使えるようになる
  • 拡張命令の組み合わせで言えば、NEON拡張命令はサポートするけど、halfの2命令「だけ」サポートしないCPUというのは理論上あり得ることになる。しかし、筆者はそんなCPU見たこと無いので、見たことある人は是非コメント欄で教えて欲しい

Arm v8

  • Arm v8 では、Arm v7で提供されていた変換命令は、通常の命令セットに含まれたので、NEON同様、実行時にチェックしなくても変換命令が使えることが保証されている
  • 一方で、DLの流行を背景に、fp16のままCPUで演算したい需要が高まった
  • そこで、Arm はv8.2の拡張命令で、fp16のまま演算できる命令セットを追加した。
  • NVIDIA Jetson AGX Xavier や ODROID-C4などでこの拡張命令が使える
$ cat /proc/cpuinfo
processor       : 0
model name      : ARMv8 Processor rev 0 (v8l)
BogoMIPS        : 62.50
Features        : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp
CPU implementer : 0x4e
CPU architecture: 8
CPU variant     : 0x0
CPU part        : 0x004
CPU revision    : 0
MTS version     : 43306594
  • 上記はJetson AGX Xavier 上の/proc/cpuinfoである
  • どちらがそうなのかは不明だが、Features欄の末尾にあるfphpasimdhpがfp16のまま演算できる命令をサポートしている証である
命令セット 機能 サポート対象
Armv7 NEON 拡張命令(neon)
Armv7 fp16(変換) 拡張命令(half)
Armv7 fp16(演算) 未サポート
Armv8 NEON Armv8の命令セット内
Armv8 fp16(変換) Armv8の命令セット内
Armv8 fp16(演算) 拡張命令セット(fphp、asimdhp)
  • トリッキーなのは、Raspberry Pi 3みたいな、「チップはArm v8(Cortex A53)なんだけれど、OSが32bit」の場合、チップがその命令をサポートしていても、OSがサポートしないため、illegal instructionとなってしまう。 2

  • 演算命令一覧(一部抜粋)

$ grep ^v.*f16 /usr/lib/gcc/aarch64-linux-gnu/7.5.0/include/arm_neon.h
vabs_f16 (float16x4_t __a)
vabsq_f16 (float16x8_t __a)
vceqz_f16 (float16x4_t __a)
vceqzq_f16 (float16x8_t __a)
vcgez_f16 (float16x4_t __a)
vcgezq_f16 (float16x8_t __a)
vcgtz_f16 (float16x4_t __a)
vcgtzq_f16 (float16x8_t __a)
vclez_f16 (float16x4_t __a)
vclezq_f16 (float16x8_t __a)
vcltz_f16 (float16x4_t __a)
vcltzq_f16 (float16x8_t __a)
vcvt_f16_s16 (int16x4_t __a)
vcvtq_f16_s16 (int16x8_t __a)
vcvt_f16_u16 (uint16x4_t __a)
vcvtq_f16_u16 (uint16x8_t __a)
vcvt_s16_f16 (float16x4_t __a)
vcvtq_s16_f16 (float16x8_t __a)
vcvt_u16_f16 (float16x4_t __a)
vcvtq_u16_f16 (float16x8_t __a)
vcvta_s16_f16 (float16x4_t __a)
vcvtaq_s16_f16 (float16x8_t __a)
vcvta_u16_f16 (float16x4_t __a)
vcvtaq_u16_f16 (float16x8_t __a)
vcvtm_s16_f16 (float16x4_t __a)
vcvtmq_s16_f16 (float16x8_t __a)
vcvtm_u16_f16 (float16x4_t __a)
vcvtmq_u16_f16 (float16x8_t __a)
vcvtn_s16_f16 (float16x4_t __a)
vcvtnq_s16_f16 (float16x8_t __a)
vcvtn_u16_f16 (float16x4_t __a)
vcvtnq_u16_f16 (float16x8_t __a)
 :
vfmaq_f16 (float16x8_t __a, float16x8_t __b, float16x8_t __c)
  • DL目的だと、vfmaq_f16を呼ぶ人が多いんじゃないかなぁ、という感想。

bfloat16

  • ネタ切れという訳ではないが、Arm のv8.6で拡張命令としてbfloat16対応がアナウンスされた
  • 公式命令リファレンスにも命令が表示されている
  • 参考までに
    • bfloat16IEEE754準拠ではなく、最近のDL目的で使われるようになった、fp16とfp32の中間のようなフォーマット
    • bit幅は16bitだが、指数部がfp32と同じく8bitあり、その分仮数部が7bitしかない
  • Arm v8.6ではbfloat16のままの演算命令が提供される(らしい)。手元に対応ボード/チップが無いので未検証

終わりに

  • Arm v7と Arm v8におけるfp16命令、およびArm v8.2で追加されたfp16のまま演算する拡張命令を紹介した
  • 明日も手島の執筆の予定で、現時点で原稿は白紙である。マジで明日何を書こう。

  1. 「使える」と書いたのは、f16を末尾に持つ命令は他にも存在する。例えばロードのvld1_f16とか、reinterpret命令など存在するが、中身をいじる命令は、変換命令の2つのみである。 

  2. 厳密に言えば、fp16命令関連に限り、Raspberry Pi 3で困ることは無い。もともとCPU がCortex A53であり、A53でサポートされてるfp16関連の命令は変換の2命令だけである。よって32bit OS上でも、変換命令はサポートされており、利用できる。ただ、divsqrtの回でも触れたが、Raspberry Pi 3のSoCには、doubleの命令やfloatdiv命令やsqrt命令がサポートされている。されているのだが、OSが当該命令をサポートしていないため、CPUにはあるのにソフトウェアからは利用できない状況に陥り、ぐぬぬぬぬ、となる。昔、フォーラムかSOかどこかで、「Raspberry Pi上で64bitOS動かしたい人は何がしたいの?SoCだからメモリだって決め打ちで拡張できないし、そんな性能差出ないよね?32bitOSでも問題無いじゃない?」と言う意見を目にしたことがあった。「大アリだよ!!」