OpenStack Cyborgを利用してFPGA仮想化基盤を作ろう

#はじめに
本記事を担当致します、@YuTAKANOです。
FPGA+OSSシステム開発・キャリア向け製品開発を社内で推進しています。

Intel様のFPGAイベントでも講演させて頂いておりますが、
今回は[OpenStack Cyborg](https://wiki.openstack.org/wiki/Cyborg)の構築手順を中心に、FPGAマネジメント基盤向け技術をご紹介いたします。
日本語向けに書かれたドキュメントとしては割と珍しいと思いますので、
ぜひ、ご興味があれば試してみてください。

#FPGA仮想化技術(FPGAリソース管理)とは
手順をご紹介する前に、2021年12月時点において、
FPGA仮想化技術の背景やどのような手法が存在するか、簡単にご紹介致します。

CPUやMemory、HDDといった物理リソースに対し、
一つのマシンでリソースを占有するのではなく、
論理的に装置を分割して共用することを、ご存じの通り「仮想化」と呼びます。
「VM(バーチャルマシン)」や「コンテナ」という単語が最も有名かと思います。
※厳密にはVMとコンテナは対比する技術ではありませんが、ここでは便宜上挙げています。

しかし、近年のデータ量増加、機能要求追加により、
「仮想化」を実現するには負荷が大きい処理が必要になってきています。
NFV(Network Functions Virtualization)においては、
VM上でのパケット暗号化・複合化・コーデック変換・カプセリング・デカプセリングが例によく挙げられます。

その高負荷な処理をFPGAやGPUに渡し、
HW処理によってCPU/メモリリソースを削減したい、という要求が存在していましたが、
従来の仮想化ではこのFPGAやGPUといったアクセラレータをリソースとして認識できていませんでした。

「仮想化」と「アクセラレーション」の需要に応えるために生まれたOSSが、
[OpenStack Cyborg](https://wiki.openstack.org/wiki/Cyborg)や[kubernetes device plugin](https://github.com/intel/intel-device-plugins-for-kubernetes)をはじめとする
CPU/メモリ/HDDと同様に**ハードウェアアクセラレータをリソースとして管理する技術**となります。

https://docs.openstack.org/cyborg/latest/

https://github.com/intel/intel-device-plugins-for-kubernetes

######OpenStack Cyborgアーキテクチャ
![cyborg-architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2374237/48af92b4-eb5c-e463-d7fa-a1b1d3cc64c4.png)
[https://docs.openstack.org/cyborg/latest/user/architecture.html](https://docs.openstack.org/cyborg/latest/user/architecture.html)

OpenStack Cyborgは既存のNova,Placementなどの既存コンポーネントと連携することで、
Compute Nodeに搭載されるアクセラレータをリソースとして認識しています。

Cyborg-APIは、CyborgプロジェクトにREST API インターフェースを提供するCyborg サービスです。
POST/PUT/DELETE/GET/PATCH 操作をサポートし、Cyborg-Conductor を介してCyborg-agent およびCyborg-DB と連携します。

Cyborg-Conductorは、Cyborg-API とCyborg-Agent 間の連携、DB アクセスを調整するCyborgサービスです。

Cyborg-Agentは、vendor ドライバを介してアクセラレータのbackend とのやり取りを担当するCyborg サービスです。
Compute nodeのアクセラレータリソースのトラッキングやFPGA ビットストリームの書き込みも実施します。

FPGA driverは、アクセラレータの製造ベンダ毎のドライバとなります。
ここで**Intel様や各ベンダ様のアクセラレータを認識**するため、
**アクセラレータが仮想化可能かどうか**はここのドライバ実装次第、とも言えます。

######kubernetes device pluginアーキテクチャ
![FPGA-region.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2374237/74f24f07-bcae-e47f-dd1b-9c195e3a415f.png)
[https://github.com/intel/intel-device-plugins-for-kubernetes/blob/main/cmd/fpga_plugin/README.md#fpga-modes](https://github.com/intel/intel-device-plugins-for-kubernetes/blob/main/cmd/fpga_plugin/README.md#fpga-modes)

Kubernetesのdevice pluginを利用したFPGA管理技術も存在しておりますが、
基本的な構造はOpenStack Cyborgと大きく変わっていません。
※WorkerNodeに存在するFPGAリソースをドライバが認識し、
MasterNode側からFPGAを必要とするpodに割り当ててデプロイする構造となります。
(pod内部からFPGAが見える状態に制御するイメージ)

今回はk8s device pluginの詳細構造は割愛致しますので、機会があれば別の機会でご紹介いたします。

#どちらのFPGA管理が良いのか(VMベース or コンテナベース)
この2つのFPGA仮想化技術に関連して、よくご質問をいただくのが、
「OpenStack Cyborgとk8s device pluginのどちらを利用すればよいか」
という管理手法の比較、メリット・デメリットです。

つまり、

 * VMベースでFPGAを仮想化(Cyborg)
 * コンテナベースでFPGAを仮想化(device plugin)
 
上記どちらのほうが好ましいか、という内容に換言できます。
いわゆるVMとコンテナの比較(カーネル差異やHVによるオーバヘッドなど)は、
過去に多くのドキュメントで語られてきているかと思いますのでここでは記載致しませんが、
FPGAの利用用途に閉じると以下の使い分けがあるかな、と認識しています(以下私見)

 1. FPGA利用権限(partial reconfigration含む)をVMに委ねる場合 ＝ VMベース
 2. 管理内アプリケーションでFPGAをscale out/inさせる場合 = コンテナベース

文章にすると1. 2.ともに端的な表現では無いのですが、
これは「デプロイするFPGAシステム(アプリケーション)がどの程度の時間を占有するか」と読み替えていただいて問題ありません。

つまり、**ユーザ(システム)への環境レンタルのような長期間のデプロイ**であればVMベース、
**一時的にFPGAアプリケーションをデプロイして終了次第解放**ならコンテナベースということになります。

これは、現在のCyborgおよびk8s device plugin、FPGAハードウェア仕様に依存するものでもあり、
基本的には**1つのFPGAボードを複数のVM/コンテナに割り当てることが出来ない**点に起因します。

そのため短期間でバッチ的にFPGAを利用して解放、という使い方を想定した場合、
コンテナベースでのデプロイ速度でないと要件を満たさない可能性も高くなります。

また、長期間FPGAを占有する場合は、OPAEやFPGAへのアクセス権限の関係から、
パススルーベースで割り当てるVMベースのほうが管理・セキュリティ面で好ましいケースが多くなります。

この辺りはVM/コンテナ議論同様に一長一短ありますので、
**アクセラレーションしたい処理はFPGAをバッチ的に利用するか？**という観点で見るのが良いかと思います。

######(例)アクセラレータリソース管理基盤としてVMベースでリソースを貸し出す
![cyborg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2374237/9a10ebaf-4fd5-d8d4-11d9-b255c792a19a.png)

#OpenStack Cyborg構築手順
前置きが長くなってしまいましたが、
ここからはVMベースのFPGA仮想化技術、OpenStack Cyborgの構築手順をご紹介します。
※本来はpipベースでの手順をご紹介したいのですが、記事の都合上devstackベースで記述致します。

######物理条件
 * ControllerNode * 1 / ComputeNode * 1 での構成例
 * OpenStack Train版以降推奨
 * FPGAボードはIntel PACカードを使用
 * ComputeNodeにはCentOS7.6を使用 * Intel PACカードの利用条件のため 

######前提環境
 * OSインストール済み
 * Controller-Compute間のMプレーンIP設定済み

######ショートカット手順
　[devstack構築手順](https://docs.openstack.org/cyborg/latest/contributor/devstack_setup.html)
  
######ControllerNodeの構築
```ruby:devstack用ユーザの作成
$ sudo su -
# useradd -s /bin/bash -d /opt/stack -m stack
# echo "stack ALL=(ALL) NOPASSWD: ALL" | tee /etc/sudoers.d/stack
```
```ruby:~/.bashrc
stackユーザに切り替え
# sudo su - stack

~/.bashrc にプロキシ設定のため、以下を追加(IPは設定値に寄る)
$ vi ~/.bashrc
===============ここから================
export http_proxy="http://$USER:$PASSWORD@$PROXY_URL:$PROXY_PORT/"
export HTTP_PROXY=$http_proxy
export https_proxy=$http_proxy
export HTTPS_PROXY=$http_proxy
nprxy=$(echo 10.0.0.{1,2})
export no_proxy=localhost,127.0.0.1,127.0.1.1,${nprxy// /,}
export NO_PROXY=$no_proxy
===============ここまで================
$ exit
```

```ruby:/etc/yum.conf
# vi /etc/yum.conf
===============ここから================
proxy=http://$USER:$PASSWORD@$PROXY_URL:$PROXY_PORT
===============ここまで================
# yum update
```

```ruby:gitのプロキシ設定
gitが入ってなければ入れる
# yum install git

再度 stack として実行
# sudo su - stack
$ git config --global http.proxy $http_proxy
$ git config --global https.proxy $https_proxy
```

```ruby:setuptoolsの更新
setuptoolsのバージョンを確認する
$ easy_install --version

setuptoolsのバージョンが1.4より古い場合、アップグレードを実施する
$ sudo pip install --upgrade setuptools
```

```ruby:bridge関係の準備
$ sudo yum install bridge-utils
$ sudo yum install openvswitch
#このままではデーモンが動いてないので起動 + boot 時に起動するように設定
$ sudo systemctl start openvswitch
$ sudo systemctl enable openvswitch
```

```ruby:bridgeの追加
ネットワーク構成に合わせてbrigdeを追加する
$ sudo ovs-vsctl add-br br-RegionOne
```

```ruby:devstackの準備
$ cd /opt/stack
$ git clone https://git.openstack.org/openstack-dev/devstack
$ cd ~/devstack 
$ git checkout -b train origin/stable/train

python-gssapiがインストールされているかの確認
$ yum list installed | grep python-gssapi
インストールされている場合は削除しておく
$ yum remove python-gssapi.x86_64
```

```ruby:local.conf編集
~/devstack/samples/からlocal.confを~/devstack直下にコピーする
$ cd ~/devstack/samples/
$ cp ./local.conf ../

local.confを編集する
$vi ~/devstack/local.conf
```

<details><summary>local.conf</summary><div>

```rb:local.conf
ADMIN_PASSWORD=password
DATABASE_PASSWORD=password
RABBIT_PASSWORD=password
SERVICE_PASSWORD=$ADMIN_PASSWORD

#multi-host settings
MULTI_HOST=True

HOST_IP=10.0.0.1
SERVICE_HOST=${HOST_IP}
MYSQL_HOST=${HOST_IP}
RABBIT_HOST=${HOST_IP}
GLANCE_HOSTPORT=${HOST_IP}:9292

#services disable
disable_service heat
disable_service tempest
disable_service n-net
disable_service n-cpu
#services neutron/quantum
enable_service q-svc
enable_service q-agt
enable_service q-dhcp
enable_service q-l3
enable_service q-meta
enable_service neutron

#services cyborg
enable_plugin cyborg https://git.openstack.org/openstack/cyborg stable/train
disable_service cyborg-agent

#network
ENABLE_TENANT_VLANS=False
ENABLE_TENANT_TUNNELS=False

ML2_L3_PLUGIN=""
Q_ML2_TENANT_NETWORK_TYPE=" "
PHYSICAL_NETWORK=physnet,RegionOne
ML2_VLAN_RANGES=physnet,RegionOne
PUBLIC_PHYSICAL_NETWORK=physnet
Q_ML2_PLUGIN_FLAT_TYPE_OPTIONS=" "
Q_ML2_PLUGIN_TYPE_DRIVERS="flat,vlan"
Q_ML2_PLUGIN_GRE_TYPE_OPTIONS=" "
Q_ML2_PLUGIN_VXLAN_TYPE_OPTIONS=" "
Q_ML2_PLUGIN_GENEVE_TYPE_OPTIONS=" "
Q_DVR_MODE=legacy
OVS_ENABLE_TUNNELING=False
OVS_BRIDGE_MAPPINGS="RegionOne:br-RegionOne"
NEUTRON_CREATE_INITIAL_NETWORKS=False
Q_USE_PROVIDER_NETWORKING=False
```
</div></details>

```ruby:devstackの起動
$ cd ~/devstack
$ ./stack.sh
```
※概ね30分程度
DevStack Version: train
のような出力がされれば完了

```ruby:Computenodeと通信する為のportを解放する
$ sudo firewall-cmd --add-port=5672/tcp --permanent
$ sudo firewall-cmd --add-port=3306/tcp --permanent
$ sudo firewall-cmd --reload
```



######ComputeNodeの構築
```ruby:devstack用ユーザの作成
$ sudo su -
# useradd -s /bin/bash -d /opt/stack -m stack
# echo "stack ALL=(ALL) NOPASSWD: ALL" | tee /etc/sudoers.d/stack
```
```ruby:~/.bashrc
stackユーザに切り替え
# sudo su - stack

~/.bashrc にプロキシ設定のため、以下を追加(IPは設定値に寄る)
$ vi ~/.bashrc
===============ここから================
export http_proxy="http://$USER:$PASSWORD@$PROXY_URL:$PROXY_PORT/"
export HTTP_PROXY=$http_proxy
export https_proxy=$http_proxy
export HTTPS_PROXY=$http_proxy
nprxy=$(echo 10.0.0.{1,2})
export no_proxy=localhost,127.0.0.1,127.0.1.1,${nprxy// /,}
export NO_PROXY=$no_proxy
===============ここまで================
$ exit
```

```ruby:/etc/yum.conf
# vi /etc/yum.conf
===============ここから================
proxy=http://$USER:$PASSWORD@$PROXY_URL:$PROXY_PORT
===============ここまで================
# yum update
```


```ruby:gitのプロキシ設定
gitが入ってなければ入れる
# yum install git

再度 stack として実行
# sudo su - stack
$ git config --global http.proxy $http_proxy
$ git config --global https.proxy $https_proxy
```

```ruby:setuptoolsの更新
setuptoolsのバージョンを確認する
$ easy_install --version

setuptoolsのバージョンが1.4より古い場合、アップグレードを実施する
$ sudo pip install --upgrade setuptools
```

```ruby:OPAEインストール
rpmのダウンロード
opaeのrpmを保存するディレクトリを作成する（任意）
$ mkdir /opt/stack/opae-rpms

$ cd /opt/stack/opae-rpms
ファイルを作成する
$ touch get_latest_release.sh
$ sudo chmod 755 get_latest_release.sh
$ vi get_latest_release.sh
```

<details><summary>get_latest_release.sh</summary><div>

```rb:get_latest_release.sh
get_latest_release() {
  curl --silent "https://api.github.com/repos/$1/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
}

download_latest_releases(){
  #$1 ... repository name, e.g. OPAE/opae-sdk
  #$2 ... package extension, e.g. rpm, deb
  curl -s "https://api.github.com/repos/$1/releases/latest" \
  | grep "browser_download_url.*$2" \
  | cut -d : -f 2,3 \
  | tr -d \" \
  | wget -i - 
}
```

</div></details>

```ruby:シェル実行
$ source get_latest_release.sh
$ download_latest_releases OPAE/opae-sdk rpm
```

```ruby:追加パッケージインストール
$ sudo yum install ${PACKAGE_NAME}
インストールするパッケージは以下の２１種類
gcc
gcc-c++
cmake
make
autoconf
automake
libxml2
libxml2-devel
json-c-devel
boost
ncurses
ncurses-devel
ncurses-libs
boost-devel
libuuid
libuuid-devel
python2-jsonschema
doxygen
hwloc-devel
libpng12
rsync
```

```ruby:EPELインストール
$ sudo yum install epel-release
```

```ruby:ドライバインストール
$ sudo yum install opae-intel-fpga*.rpm
```

```ruby:OPAE追加パッケージインストール
注：get_latest_release.sh でダウンロードしたディレクトリ上で実行する
$ sudo yum install opae*.rpm
```

```ruby:DLLバインディングを更新後、再起動
$ sudo ldconfig
$ sudo shutdown -r now
```

```ruby:カーネルモジュール確認
$ lsmod | grep fpga
以下のような表示が確認出来る
==============================================
intel_fpga_pac_hssi     18107  0
intel_fpga_fme           52380  0
fpga_mgr_mod           14693  1  intel_fpga_fme
intel_fpga_afu            31742  0
intel_fpga_pci            26522  2  intel_fpga_afu,intel_fpga_fme
==============================================

デバイスが見えることを確認
$ fpgainfo fme
```


```ruby:bridge関係の準備
$ sudo yum install bridge-utils
$ sudo yum install openvswitch
#このままではデーモンが動いてないので起動 + boot 時に起動するように設定
$ sudo systemctl start openvswitch
$ sudo systemctl enable openvswitch
```

```ruby:bridgeの追加
ネットワーク構成に合わせてbrigdeを追加する
$ sudo ovs-vsctl add-br br-RegionOne
```

```ruby:devstackの準備
$ cd /opt/stack
$ git clone https://git.openstack.org/openstack-dev/devstack
$ cd ~/devstack 
$ git checkout -b train origin/stable/train

python-gssapiがインストールされているかの確認
$ yum list installed | grep python-gssapi
インストールされている場合は削除しておく
$ yum remove python-gssapi.x86_64
```

```ruby:local.conf編集
~/devstack/samples/からlocal.confを~/devstack直下にコピーする
$ cd ~/devstack/samples/
$ cp ./local.conf ../

local.confを編集する
$vi ~/devstack/local.conf
```

<details><summary>local.conf</summary><div>

```rb:local.conf
ADMIN_PASSWORD=password
DATABASE_PASSWORD=password
RABBIT_PASSWORD=password
SERVICE_PASSWORD=$ADMIN_PASSWORD

#multi-host settings
MULTI_HOST=True

HOST_IP=10.0.0.1
SERVICE_HOST=${HOST_IP}
MYSQL_HOST=${HOST_IP}
RABBIT_HOST=${HOST_IP}
GLANCE_HOSTPORT=${HOST_IP}:9292

#repos
GIT_BASE=https://git.openstack.org
REPOS_VERSION=stable/rocky
CINDER_BRANCH=$REPOS_VERSION
GLANCE_BRANCH=$REPOS_VERSION
HORIZON_BRANCH=$REPOS_VERSION
KEYSTONE_BRANCH=$REPOS_VERSION
KEYSTONECLIENT_BRANCH=$REPOS_VERSION
NOVA_BRANCH=$REPOS_VERSION
NOVACLIENT_BRANCH=$REPOS_VERSION
NEUTRON_BRANCH=$REPOS_VERSION
SWIFT_BRANCH=$REPOS_VERSION

#service en/disable
disable_service heat
disable_service tempes
disable_service n-net
#service neutron/quantum
enable_service n-cpu
enable_service placement-client
enable_service q-svc
enable_service q-agt
enable_service q-dhcp
enable_service q-l3
enable_service q-meta
enable_service q-neutron

#services cyborg
enable_plugin cyborg https://git.openstack.org/openstack/cyborg stable/rocky
enable_service cyborg-agent
disable_service cyborg-api
disable_service cyborg-cond

#network
ENABLE_TENANT_VLANS=False
ENABLE_TENANT_TUNNELS=False
ML2_L3_PLUGIN=""
Q_ML2_TENANT_NETWORK_TYPE=" "
PHYSICAL_NETWORK=physnet,RegionOne
ML2_VLAN_RANGES=physnet,RegionOne
PUBLIC_PHYSICAL_NETWORK=physnet
Q_ML2_PLUGIN_FLAT_TYPE_OPTIONS=" "
Q_ML2_PLUGIN_TYPE_DRIVERS="flat,vlan"
Q_ML2_PLUGIN_GRE_TYPE_OPTIONS=" "
Q_ML2_PLUGIN_VXLAN_TYPE_OPTIONS=" "
Q_ML2_PLUGIN_GENEVE_TYPE_OPTIONS=" "
Q_DVR_MODE=legacy
OVS_ENABLE_TUNNELING=False
OVS_BRIDGE_MAPPINGS="RegionOne:br-RegionOne"
NEUTRON_CREATE_INITIAL_NETWORKS=False
Q_USE_PROVIDER_NETWORKING=False
#nova
NOVA_VNC_ENABLED=True
VNCSERVER_LISTEN=$HOST_IP
VNCSERVER_PROXYCLIENT_ADDRESS=$HOST_IP
```
</div></details>

```ruby:devstackの起動
$ cd ~/devstack
$ ./stack.sh
```
※概ね30分程度
DevStack Version: train
のような出力がされれば完了

######構築結果
構築後にFPGAを利用するインスタンスを作成し、
flavorからFPGA利用をenableにすることでVMにFPGAがアタッチされた状態でデプロイされます。
※この辺りの詳細は長くなるため省略致しますが、公式ドキュメントをご参照ください。
![キャプチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2374237/5bca5dfb-ea58-39e5-37f6-06dd577a53a9.png)
画面左上　：ダッシュボード
画面右上　：ComputeNode
画面中央下：ControllerNode
の状態で、インスタンスにFPGAが割り当たるとComputeからリソースが見えなくなり、
Controller上でVMへのアタッチ状況が分かるようになります。

実際にVMで利用するFPGAはベアメタルと同様に動作するので、
以降は**FPGAを利用するシステムをVM化する**だけで利用することが可能となります。

#終わりに
今回はFPGA仮想化技術の背景と、OpenStackCyborgでの構築手順をご紹介しました。
機会があればkubernetes device pluginを利用したコンテナベースでの構築手順や、
Cyborg + device pluginを組み合わせた仮想化基盤技術についてご紹介できればと考えております。

今後FPGAを利用したアクセラレーションは更に普及していくことが予想されますが、
それらHWリソースの効率的な管理は将来必要となる技術です。
これからも動向や手順を紹介できればと考えておりますので、よろしくお願いいたします。