fp16命令

# はじめに
 - この記事は[ひとりNEONアドベントカレンダー2020](https://qiita.com/advent-calendar/2020/neon-only) 21日目の記事です
 - [昨日は内積命令を紹介](https://qiita.com/tomoaki_teshima/items/b93dd7919a07658a4166)した
 - 今日は`fp16`命令を紹介する

# TL;DR

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

![fp16_instructions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12162/14e5e768-4603-a99b-d76e-bf627c99d489.png)

# Arm v7 の場合
 - 実質的に「使える」fp16命令は、以下の2つのみ[^usable]

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

```
vcvt_f16_f32 (float32x4_t __a)
vcvt_f32_f16 (float16x4_t __a)
```
 - 前者は`float16x4_t`型を返し、後者は`float32x4_t`型を返す
 - 変換だけじゃ意味ないじゃん、と思うなかれ。GPUに渡したりする際に、予めfp16に変換しておくことで、転送コストを下げられるのである。(昔の[筆者のSlideShare](https://www.slideshare.net/tomoaki0705/gpgpu-tomoakifp16)とかに書いた)
 - この2命令だけ、NEONの範囲外で、`-mfpu=neon-fp16`というオプションを`GCC`にコンパイル時に渡す必要がある

```baash
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`を確認した結果

```/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などでこの拡張命令が使える

```/proc/cpuinfo
$ 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`欄の末尾にある`fphp`と`asimdhp`が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`となってしまう。 [^illegual]


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

```
$ 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`を呼ぶ人が多いんじゃないかなぁ、という感想。

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

# `bfloat16`
 - ネタ切れという訳ではないが、Arm のv8.6で拡張命令として[`bfloat16`対応がアナウンス](https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/arm-architecture-developments-armv8-6-a)された
 - [公式命令リファレンス](https://developer.arm.com/architectures/instruction-sets/simd-isas/neon/intrinsics?search=bfloat)にも命令が表示されている
 - 参考までに
    - `bfloat16`は`IEEE754`準拠ではなく、最近のDL目的で使われるようになった、fp16とfp32の中間のようなフォーマット
    - bit幅は16bitだが、指数部がfp32と同じく8bitあり、その分仮数部が7bitしかない
 - Arm v8.6ではbfloat16のままの演算命令が提供される(らしい)。手元に対応ボード/チップが無いので未検証


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