複数台のAlmaLinuxで並列計算してみた（MPIの環境構築）

# はじめに

　本記事では, 複数台のAlmaLinuxを用いて, 並列計算を行うための環境構築を行います. AlmaLinuxは, CentOS8のサポート終了に伴い, その後継として開発されたLinuxディストリビューションです. 本記事は, LinuxのインストールができるMPI初心者を対象としています.

<details><summary><strong>MPIとは</strong></summary>
　MPI (Message Passing Interface) とは, プロセス間で通信を行うための規格です. よく利用されるものには, MPICHやopenMPI があります. これらを利用して並列計算を実現させます.
</details>

# 使用環境

　・AlmaLinux 8.5

# MPIの環境構築

　本記事で構築する分散システム（図１）によって, 以下の作業が可能になります.
　　・複数台を用いて**分散メモリ型の並列処理**を行う.
　　・子が親を経由してインターネットに接続する（各マシンのアップデートに便利！）.
　　・応用として, n対nのデータ通信をしながら数値計算を行う.
　
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1899798/172bab1e-be3e-6d4f-260c-cad1aaf1626d.jpeg" alt="ネットワークの構成" width="600">

　　　　　　　　　　　　　図１　分散システム

　この環境を構築するために, 以下の手順が必要です.

|　手順　|　　　　　　　　内容　|　キーワード　|
|:-:|:-|:-:|
|-|OSをインストールする（本記事の対象外）                                    |                  |
|1|[ネットワークを構築する](#1-ネットワークを構築する)                       |　IPマスカレード, SSH　|
|2|[ファイルを共有する（マウントを行う）](#2-ファイルを共有するマウントを行う)|NFS                |
|3|[mpirun を実行する](#3-mpirun-を実行する)　　　　　　　　         |                  |

## 1. ネットワークを構築する

### (a) ネットワークで用いるIPアドレス範囲を定める

　IPアドレスを決めます. プライベートIPアドレスとして自由に使用できる範囲は表の通りです.

|　クラス　|　IPアドレス範囲　|
|:-:|:-:|
|A|10.0.0.0 ~ 10.255.255.255|
|B|172.16.0.0 ~ 172.31.255.255|
|C|　　　　192.168.0.0 ~ 192.168.255.255　　　　|

　本記事のネットワークの構成を図２に示します. 今回は**8台**のAlmaLinuxを用いて, 並列計算を行います. ホスト名をALMA01 ~ ALMA08とし, 図のように「IPアドレス/サブネットマスク」を定めました. このとき, ネットワークアドレス, およびブロードキャストアドレスはホストに割り当てられないので注意してください. （今回の割り当てでは, 最大で254台の分散システムを構築することができますね！）

　以降では, 全体の通信を制御する<font color="Red">**マスターノード**</font>（1台）と, 指示を受けてデータを送信する<font color="DarkOrange">**スレーブノード**</font>（複数）という用語を用います.

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1899798/8e4eca52-642b-507e-7b52-197e844d65cb.jpeg" width="600">

　　　　　　　　　　　図２　ネットワークの構成
　
　ネットワークの構成が決まったら, 各ノードでネットワーク設定を開き, **IPアドレス**, **サブネットマスク**, **ゲートウェイ** の設定を行います. スレーブノードのゲートウェイはマスターノードのIPアドレス（今回の場合は192.168.1.1）です.

　その後, 今回は小規模の閉じたネットワークであるため, 全ノードで`/etc/hosts`ファイルを編集して, 名前解決を行います.

```emacs-lisp:/etc/hosts
; 次の行を追加する
192.168.1.1    ALMA01    ; IPアドレス  ホスト名
192.168.1.2    ALMA02
   ...
192.168.1.8    ALMA08
```

:::note info
ヒント
OSを1台ずつインストールする場合は, ここまで進めましょう. 

以降では, SSH (Secure Shell) を用いてスレーブノードに接続し, 作業を行います.
SSHがインストールされていない場合は, 1-(c) の一行目を実行してください.
:::

　図2を参考にしてハブと各ノードをLANで接続し, **物理的な環境を構築** します. 

　`ping`コマンドを利用して, 互いに認識できるかどうかを確認してください.

```shell-session
$ ping ALMA08
```

　あとで必要になるため, マスターノードで`ifconfig`コマンドを実行し, ネットワークインターフェース名（ここでは`eno1`と`eno2`）を確認しておきます.
 
```shell-session
$ /sbin/ifconfig
eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet xxx.xxx.xxx.xxx  netmask xxx.xxx.xxx.xxx  broadcast xxx.xxx.xxx.xxx
        inet6 xxxx::xxxx:xxxx:xxxx:xxxx  prefixlen 64  scopeid 0x20<link>
        ether xx:xx:xx:xx:xx:xx  txqueuelen 1000  (Ethernet)
        RX packets 160418  bytes 19653083 (18.7 MiB)
        RX errors 0  dropped 94720  overruns 0  frame 0
        TX packets 3332  bytes 1228201 (1.1 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        device interrupt 16

eno2: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.1  netmask 255.255.255.0  broadcast 192.168.1.255
          ...
```

### (b) ファイアウォールでIPマスカレードを設定する

　スレーブノードからインターネットに接続するには, マスターノードにルーターの働きをさせる必要があります. Linuxでは, プライベートIPアドレスをグローバルIPアドレスに変換する技術のことを **IPマスカレード** といいます.

　インターネットにつながった`eno1`とプライベートネットワークにつながった`eno2`は独立しています. この2つを接続するために, マスターノードで`/etc/sysctl.conf`ファイルを編集し, **IPフォワーディングを有効化** します. 

```emacs-lisp:/etc/sysctl.conf
; 次の一行を追加する
net.ipv4.ip_forward=1
```

　次のコマンドで設定を反映させます.

```shell-session
# sysctl -p
net.ipv4.ip_forward = 1
```

　設定が反映されているかどうかを確認します.

```shell-session
$ cat /proc/sys/net/ipv4/ip_forward
1
```

　マスターノードでゾーンの設定を行います.

```shell-session
# firewall-cmd --permanent --zone=home --change-interface=eno1
The interface is under control of NetworkManager, setting zone to 'home'.
success
# firewall-cmd --permanent --zone=internal --change-interface=eno2
The interface is under control of NetworkManager, setting zone to 'internal'.
success
```

　ゾーンの設定を確認します.

```shell-session
# firewall-cmd --get-active-zones
home
  interface: eno1
internal
  interface: eno2
...
```

　マスターノードで **IPマスカレードを有効化** し, ファイアウォールの永続設定を反映させます.

```shell-session
# firewall-cmd --permanent --zone=home --add-masquerade
success
# firewall-cmd --reload
success
```

　それぞれのゾーン（`home`と`internal`）で設定が反映されているかどうかを確認します. activeの状態かどうか, interfaces, masqueradeの設定を確認してください.

```shell-session
# firewall-cmd --list-all --zone=home
home (active)
  target: default
  icmp-block-inversion: no
  interfaces: eno1
  sources: 
  services: cockpit dhcpv6-client mdns samba-client ssh
  ports: 
  protocols: 
  forward: no
  masquerade: yes
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules: 
# firewall-cmd --list-all --zone=internal
internal (active)
  target: default
  icmp-block-inversion: no
  interfaces: eno2
  sources: 
  services: cockpit dhcpv6-client mdns samba-client ssh
  ports: 
  protocols: 
  forward: no
  masquerade: no
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules:
```

　以上で, スレーブノードからマスターノードを経由してインターネットへ接続できるようになります. しかし, このままではMPIを実行することができません. そこで, マスターノードで以下のリッチルールを追加し, 反映させます. **長いので`\`を用いて改行していますが, `\`なしの1行で実行してください.**

```shell-session
# firewall-cmd --permanent --zone=internal \
    --add-rich-rule='rule family="ipv4" source address="192.168.1.2" accept'
success
  ...
# firewall-cmd --permanent --zone=internal \
    --add-rich-rule='rule family="ipv4" source address="192.168.1.8" accept'
success
# firewall-cmd --reload
success
```

　次のコマンドで設定が反映されているかどうかを確認します.

```shell-session
# firewall-cmd --list-rich-rules --zone=internal
rule family="ipv4" source address="192.168.1.7" accept
rule family="ipv4" source address="192.168.1.3" accept
...
```

### (c) パスフレーズなしのSSHで接続する

　MPIを用いて各ノードを通信させるとき, SSHが利用されます. 通常, SSHを利用する際にはパスワードを設定しますが, 並列処理による通信のたびにパスワードを入力することは現実的ではありません. そのため, **パスフレーズなしの公開鍵認証**を利用する必要があります.
　SSHでは, クライアント（SSHで接続する）側とサーバー（SSHで接続される）側にわかれます. マスターノードにはクライアント, スレーブノードにはサーバーの設定を行います. （本記事では, マスターノードにサーバーの設定も行っています. ）

　全ノードでSSHに必要なパッケージをインストールします. 

```shell-session
# dnf install -y openssh openssh-server
```

　マスターノードは追加で必要なパッケージをインストールします.

```shell-session
# dnf install -y openssh-clients
```

　全ノードで`/etc/ssh/sshd_config`ファイルを編集し, SSHの **公開鍵認証を有効化** します.

```emacs-lisp:/etc/ssh/sshd_config
; 次の行の#を外す
#PubkeyAuthentication yes  ->  PubkeyAuthentication yes
```

　全ノードでsshdを再起動し, 設定を反映させます.

```shell-session
# systemctl restart sshd
```

　全ノードで同じユーザー名（ここでは`user`）のアカウントを作成し, 任意のパスワードを設定します.

```shell-session
# useradd user
# passwd user
/* 任意のパスワードを入力する */
```

　マスターノードでユーザーを切り替えます.

```shell-session
# su user
```

　マスターノードでパスフレーズなしの鍵を作成します. 

```shell-session
$ ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa
```

（または`ssh-keygen`コマンドを実行し, 何も書かずにEnterを押すと同じ出力が得られます. ）
　これより, **秘密鍵 (id_rsa)** と **公開鍵 (id_rsa.pub)** が作成されました.

　マスターノードで生成した公開鍵をスレーブノードに転送し, authorized_keysに書き込みます. 次のコマンドを実行してください.（実行するときに接続先のパスワードを求められます. ）

```shell-session
$ ssh-copy-id -i ~/.ssh/id_rsa.pub user@ALMA02  # ユーザー名@接続先
$ ssh-copy-id -i ~/.ssh/id_rsa.pub user@ALMA03
    ...
$ ssh-copy-id -i ~/.ssh/id_rsa.pub user@ALMA08
```

:::note warn
注意
　公開鍵を渡す方法は他にもあります. このとき, ディレクトリやファイルの権限が正しくないとMPIの実行時にパスワードを聞かれることがあります.
　必ず, chmod 600 authorized_keys, およびchmod 700 .ssh となっていることを確認してください.（上記の意味が理解できない場合はssh-copy-idコマンドを用いて自動登録した方がよいでしょう. ）
:::

`ssh`コマンドを用いてパスフレーズなしで入れるかどうかを確認します.

```shell-session
$ ssh ALMA08 -l user -Y
```

## 2. ファイルを共有する（マウントを行う）

### (a) NFSを設定する

　MPIを実行するためには, 全ノードでディレクトリやファイルが共有されている必要があります. そこで, NFS（Network File System）を用いて, マスターノードのディレクトリを共有してあげましょう. このように, 外部のファイルシステムにアクセスできるようにすることを **マウント** といいます. 

　全ノードでNFSに必要なパッケージをインストールします. 

```shell-session
# dnf install nfs-utils
```

　マスターノードで共有するディレクトリを作成します. 今回は`/DATA`ディレクトリとします.

```shell-session
# mkdir /DATA
```

　`user`が利用できるようにディレクトリの所有者を変更（またはアクセス権限を付与）します.

```shell-session
# chown user:user /DATA
```

　マスターノードで`/etc/exports`ファイルを編集し, 公開するディレクトリの設定を行います. アクセスできる範囲には, ネットワークアドレスを指定しています. また, `*(rw,async)`とすることでアドレスの制限なしにも設定できます.

```emacs-lisp:/etc/exports
; 次の1行を追加する
/DATA    192.168.1.0/24(rw,async)    ; アクセスできる範囲（読み書き可, 非同期書き込み）
```

　次のコマンドで設定を反映させます. 

```shell-session
# /sbin/exports -ra
```

　設定が反映されているかどうかを確認します.

```shell-session
# /sbin/exports -v
/DATA       	192.168.1.0/24(async,wdelay,hide, ...
```

　マスターノードでファイアウォールにNFSの許可を与えます. （1-(b)で追加したリッチルールにより, 許可を与えなくても動作します. ）

```shell-session
# firewall-cmd --permanent --zone=internal --add-service=nfs
success
# firewall-cmd --reload
success
```

　マスターノードでNFSサーバを起動させます. 自動起動も有効化しておきましょう.

```shell-session
# systemctl start nfs-server     # NFSサーバの起動
# systemctl enable nfs-server    # 自動起動の有効
```

### (b) マウントする

　あとは, 各スレーブノードで設定を行い, マウントしていきます.

　スレーブノードでマウントポイントを作成します.

```shell-session
# mkdir /DATA
```

　スレーブノードで`/etc/fstab`ファイルを編集し, マウントの設定を行います.

```emacs-lisp:/etc/fstab
; 次の1行を追加する
ALMA01:/DATA    /DATA    nfs    defaults,hard,intr,noauto    0    0
```

　準備が整いました. マウントを実行します.（エラーが出なければOKです. ）

```shell-session
# mount /DATA
```

## 3. mpirun を実行する

　それでは, 実際に並列計算を行ってみます. 

　今回は`mpich`を使用します. 全ノードでインストールしてください.

```shell-session
# dnf install -y mpich mpich-devel
```

　マスターノードで`mpich`のパスを通します. パスの永続化については, 使用しているシェルに合わせて各自で行ってください. (次のコマンドは, 一時的にパスを通す例です. )

```shell-session
$ export PATH=$PATH:/usr/lib64/mpich/bin    # bashの場合
$ set path=($path /usr/lib64/mpich/bin)     # tcshの場合
```

　以上で, マウント済みのディレクトリ内でMPIが実行可能です. `cd`コマンドで移動します.

```shell-session
$ cd /DATA
```

　例えば, `/DATA`ディレクトリ内に, 次のような`sample.c`を作成します.

```c:/DATA/sample.c
#include <stdio.h>
#include <mpi.h>

int main(int argc, char *argv[]) {
  int rank;                 // ランク
  int name_length = 32;     // ホスト名の長さ
  char name[name_length];   // ホスト名

  MPI_Init(&argc, &argv);                      // MPIの初期化
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);        // ランクの取得
  MPI_Get_processor_name(name, &name_length);  // ホスト名の取得

  printf("Hello world from %s! Rank is %d.\n", name, rank);

  MPI_Finalize();                              // MPIの終了
  return 0;
}
```

　実行には, ホストファイルが必要になります. `/DATA`ディレクトリ内に`hosts`ファイルを準備してください. ファイル名は自由です.

```emacs-lisp:/DATA/hosts
; 計算資源として利用するノードを記述する
ALMA01
ALMA02
...
ALMA08
```

　`mpicc`コマンドを用いて, コンパイルします.

```shell-session
$ mpicc smaple.c
```

　実行してみましょう. ファイアウォールやSSHの設定上, マスターノードでのみ実行可能です.

```shell-session
$ mpirun --hostfile hosts -n 8 ./a.out
Hello world from ALMA01! Rank is 0.
Hello world from ALMA07! Rank is 6.
Hello world from ALMA06! Rank is 5.
Hello world from ALMA05! Rank is 4.
Hello world from ALMA04! Rank is 3.
Hello world from ALMA03! Rank is 2.
Hello world from ALMA08! Rank is 7.
Hello world from ALMA02! Rank is 1.
```

　`/DATA/hosts`ファイルの内容や, `-n 8`の値を変化させて実行してみましょう.

## 4. 応用例

　分散システムを用いて数値計算を行うとき, 境界データのやり取りが必要になるため, n対nのデータ通信を行いながらMPIを進める必要があります. ここでは, 上記環境に加えて必要な手順を示します.

### (a) プライベートネットワーク内のセキュリティを無効化する

　閉じたネットワークであるため, 面倒な設定を省いてセキュリティを無効化します.（または, 1-(b)を参考にリッチルールを追加することで解決します. ）

　スレーブノードのファイアウォールを停止します. 自動起動も無効化しておきましょう.

```shell-session
# systemctl stop firewalld       # ファイアウォールの停止
# systemctl disable firewalld    # 自動起動の無効
```


### (b) 互いにパスフレーズなしのSSHで接続する

　n対nのMPI通信では, 各ノードが互いにパスフレーズなしのSSHで接続できる必要があります. そこで, 1-(c)を参考に, どのノード間でも移動できるように鍵を渡します. 

　スレーブノードで以下のコマンドを実行します. なお, 接続先に自分自身を含める必要はありません.

```shell-session
$ ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa      # 鍵の生成
$ ssh-copy-id -i ~/.ssh/id_rsa.pub user@ALMA01  # ユーザー名@接続先
$ ssh-copy-id -i ~/.ssh/id_rsa.pub user@ALMA02
    ...
$ ssh-copy-id -i ~/.ssh/id_rsa.pub user@ALMA08
```

　
　以上で, n対nのデータ通信のほか, どのノードからでも実行可能になります.

# まとめ

　本記事では, 複数台のAlmaLinuxを用いて, 並列計算を行うための環境構築を行いました. 本記事がネットワーク技術のハンズオン教材となれば幸いです.

　最後まで読んでいただきありがとうございました！ 

# 参考文献
https://qiita.com/kkk627/items/49c9c35301465f6780fa

https://bitwalk.blogspot.com/2020/04/centos-8-ip.html

https://www.isus.jp/products/mpi/bkms-firewall-blocks-mpi-communication-among-nodes/