OpenCV bfloat16のサポート始めるってよ

# はじめに

- 本記事は[OpenCV Advent Calendar 2023](https://qiita.com/advent-calendar/2023/opencv) 12日目の記事です。
- その他の記事は[目次](https://qiita.com/advent-calendar/2023/opencv)をご覧ください。
- 最近は仕事に追われてOpenCVのpull requestをちゃんと追ってなかったのだが、このカレンダーの主催者熊太郎さんのツイートで気づいた。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ぬぉおおおん！！！とても悲しい悲しいissueが発行されました。。。これはもう、政府は熊太郎さんにRaspi5を支給する以外の選択肢はないですね（何を言い出したのか本人もわかっていない）<a href="https://t.co/LKXPqtu2dZ">https://t.co/LKXPqtu2dZ</a></p>&mdash; 恋する熊太郎@AdvCal_OpenCV2023 (@hon_no_mushi) <a href="https://twitter.com/hon_no_mushi/status/1727966794873450572?ref_src=twsrc%5Etfw">November 24, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

- [これは、**OpenCVをAarch64用に、かつ以下のオプションを指定してビルドするとコンパイルが通らない**というissue](https://github.com/opencv/opencv/issues/24588)である。

```cmake
-DCPU_BASELINE="NEON_FP16;NEON_DOTPROD;NEON_BF16"
```

- OpenCVには、拡張命令を安全に利用するための`Dispatch`機能が存在しているそれらは筆者の過去のアドベントカレンダー記事で何度か紹介した。


# bfloat16とは
- `float`、`double`、`half`に合わせて、`bfloat16`もしくは`bf16`という浮動小数点数方式が世間一般で使われているが、このbit数の内訳は以下のように各フォーマットで違う

|型|bit幅|符号|指数部|仮数部|
|--|--|--|--|--|
|`float`|32bit|1bit|8bit|23(+1)bit|
|`double`|64bit|1bit|11bit|52(+1)bit|
|`half`|16bit|1bit|5bit|10(+1)bit|
|`bf16`|16bit|1bit|8bit|7(+1)bit|

- 仮数部の`(+1)`はケチ表現を表す部分である。符号bit、指数部、仮数部(ケチ表現を除く)を合計するとbit幅と等しくなるのが確認できる。
- `bf16`は全体として16bitであるが、指数部は`float`と同じく8bit割り当てられており、数の絶対値としては`float`とほぼ同じ範囲の実数が表現できる。ダイナミックレンジが広いDNNのパラメータ型として使うことでオーバーフロー/アンダーフローを抑えつつメモリ使用量を抑えたり計算のスループットを増大できたりする
- なお、`float`、`double`、`half` はそれぞれIEEE754で制定された、いわゆる「標準」規格であるが、`bfloat16`はGoogle Brainのチームが提唱し始めたフォーマットでありIEEE754では制定されてない。にも関わらずHWで命令を実装するアーキテクチャが複数でてくるあたり、Deeeeeeeepの流行を体感する限りである。



# で再びissue
- で、issueでは、`NEON_FP16`、`NEON_BF16`、`NEON_DOTPROD`の3つを同時に指定するとコンパイルエラーになるというものである。
- なお、`NEON_FP16`と`NEON_BF16`はそれぞれ名前が紛らわしいが、`NEON_FP16`は`fp16`つまり`half`のまま演算する拡張命令であり、`NEON_BF16`は`bfloat16`のまま四則演算がサポートされる拡張命令である。
- ここでCMakeの`CPU_BASELINE`に下記オプションを指定すると、最終的に対応するコンパイルオプションがGCC/Clangに渡される仕組みとなっている


|OpenCVのオプション|GCC/Clangのオプション|
|---|---|
|`NEON_FP16`|`-march=armv8.2-a+fp16`|
|`NEON_BF16`|`-march=armv8.2-a+bf16+fp16`|
|`NEON_DOTPROD`|`-march=armv8.2-a+dotprod`|

- これらのオプションのうち、2つ以上指定すると、**最後に指定したやつ以外無視される**というGCC/Clangの挙動につき、
  - コードとしては拡張命令が埋め込まれている
  - コンパイラとしては一部の拡張命令が無効になってコンパイルされる
  - 結果コンパイルエラーが発生するというissueである
- 本来GCC/Clangとしては`arch`オプションで拡張命令を複数指定する場合は`march=armv8.2a-+fp16+bf16+dotprod`という具合に拡張命令だけを`armv8.2-a`の後ろに連結し、`arch`オプションは一つだけ渡すことを想定している。が、現状ではCMakeで指定したオプション分だけ`arch`オプションが渡されてしまう。
- という訳で筆者が複数ある`arch`オプションを束ねる機能を追加する[PRを発行](https://github.com/opencv/opencv/pull/24642)したのだが、記事執筆時点ではまだマージされていない
- また、`NEON_BF16`に対応するコンパイラオプションは`-march=armv8.2-a+bf16`なのだが、現時点では何故か`-march=armv8.2-a+bf16+fp16`(`+fp16`もセットにされてる)であり、原因は不明である。筆者が誤解している可能性もあるのでPR上で問い合わせたのだが、記事執筆時点では特に回答はない

# 後日談というか今回のオチ
- `bfloat16`命令を有効にするコンパイラオプションが追加されたものの、じつはbfloat16命令を使った実装はまだ追加されていない。
- cmakeを使った際の出力には、各オプションが有効にして個別にコンパイルされるファイルがどれぐらいあるか表示される。

```
--   CPU/HW features:
--     Baseline:                    NEON FP16
--     Dispatched code generation:  NEON_DOTPROD NEON_FP16 NEON_BF16
--       requested:                 NEON_FP16 NEON_BF16 NEON_DOTPROD
--       NEON_DOTPROD (1 files):    + NEON_DOTPROD
--       NEON_FP16 (2 files):       + NEON_FP16
--       NEON_BF16 (0 files):       + NEON_BF16
--
```

- 記事執筆時点の4.x系列のブランチでは、`NEON`と`FP16`が`CPU_BASELINE`、つまり必須要件としてビルドされ、`NEON_DOTPROD`、`NEON_FP16`、`NEON_BF16`の3つのフラグはそれぞれ`CPU_DISPATCH`、つまり実行時にCPUの対応命令を把握して実行するかどうかを判断する命令としてビルドされる
- `NEON_BF16 (0 files)` は、フラグは有効になったものの、このオプションを使うファイルは存在しないとなっており、`bfloat16` 命令を使った実装はまだ存在しない
- 参考までに、コンパイラがこのオプションに対応していない場合も考えられる。筆者が調べた限りだと、GCC 10系列から`+bf16`オプションが有効になった模様で、それ以前のコンパイラでOpenCVをビルドすると、そもそもNEON_BF16がこの一覧に現れない
- 過去のdispatchを使った記事でも触れているが、OpenCVのSIMD命令を使った実装は、intrinsicなどを使った実装のPRは基本的にコアメンバに断られる。これはメンテナンスの観点からの理由である。記事執筆時点ではx86系アーキテクチャ(`AVX`、`AVX512`など)Armv7とv8(`NEON`)、MIPS(`MSA`)、PowerPC(`VSX`)に加えてRISC-V(`RVV`)など多岐にわたるSIMD命令が用意されている。たしかにメンテナンスが大変そうである
- ただし、メンテナンスが大変になっても生のSIMD intrinsicを受け付けてるモジュールがあり、それがdnnモジュールである。今回もdnnモジュールに`half`および`bfloat16`に対応した命令をdnnモジュールに追加するための前準備だった模様で、そのうち追加されるかもしれない。


# その他
- 本記事は以下の環境で検証しました
  - 主にDockerの`arm64v8/gcc:13.2`を使用
  - コンパイラ GCC 13.2.0
  - CMake 3.25.1
  - OpenCV (git commit `e9f35610a54479eb170c101745cbd6bcc8e1d122` : 4.8.1と4.9.0の間)
  - OS Debian 12.2

- 以下、記事執筆時点での話
  - OpenCV内で命名されている拡張命令一覧は`NEON`、`FP16`、`NEON_FP16`と紛らわしいものがあるが、`NEON`は、ArmアーキテクチャのSIMD命令のことであり、`FP16`は`float`と`half`を変換/逆変換する2命令だけのことである。Armv8からは両者とも基本命令セットに含まれたので、特段指定する必要はない。`FP16`は、全く同じ命令セットがAVXのサブセットにも含まれていたので、`FP16`という一般的な名前になったが、`NEON_FP16`は`half`のまま算術演算できる命令のことであり、これはArm V8の拡張命令なので、`NEON_FP16`とNEONの名を冠している。
  - 本記事で紹介したビルドエラー以外にも、4.xブランチとARM64マシン上では`NEON_FP16`命令を有効にしなくてもリンク時点でDNNモジュールが`NEON_FP16`実装を参照してしまい、リンカエラーが発生することが公式のビルドマシンで確認されている
  - メジャーに使われてるコンパイラとしてはGCCの他、ClangとVisual Studioが挙げられるが、現状のOpenCVのコードは、Aarch64用のコードをVisual Studioでビルドする想定に全くなってない
  - x86_64の`AVX512`のサブセットにも`AVX512_FP16`および`AVX512_ BF16`と呼ばれる、それぞれ`half`と`bfloat16`での処理に対応した命令群があるが、現時点ではOpenCVの対応命令一覧に追加されていない（とは言えAVX512の命令体系は複雑っぽいので筆者が見落としている可能性もゼロではない）

<details><summary>その他詳細</summary>

```
# g++ --version
g++ (GCC) 13.2.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

# cat /etc/debian_version
12.2

$ docker ps
CONTAINER ID   IMAGE              COMMAND   CREATED      STATUS      PORTS     NAMES
d9b5da3fad9f   arm64v8/gcc:13.2   "bash"    8 days ago   Up 8 days             laughing_mendeleev

# cmake --version
cmake version 3.25.1

$ git log
commit e9f35610a54479eb170c101745cbd6bcc8e1d122 (upstream/4.x, 159)
Merge: d296d29a1c 2830551e89
Author: Alexander Smorkalov <2536374+asmorkalov@users.noreply.github.com>
Date:   Fri Nov 24 14:16:43 2023 +0300
```

</details>


# おわりに
- OpenCVに`bfloat16`命令をAarch64で使うための下地が整いつつあるが、実装はまだ追加されてないことを紹介した
- 明日の発表は[あのテクい説明で有名なdandelion先生である](https://qiita.com/dandelion1124/items/a0c74b0a56fbd2c45057)。みんな！正座して待機だよ！