ラズパイでクラスターをつくりました|Ubuntu server 25.04
最近、並列計算に興味をもったのでクラスターを構築して計算させてみたいなと思いました。
会社の部長からラズパイをたくさんお借りることができたので、環境構築とPythonで素数を計算した際の挙動を確認したいと思います。(サンキュー部長)
構築する手順をこの記事に記録したいと思います。
ラズパイクラスター構成
今回は三台のラズパイ5を使ってクラスターを構築したいと思います。
本当は五台持ってる(もともと一台持ってる、部長から四台借りた)のですが、まず動作確認したいのと電源が足りないので三台で、まずは三台で動作させます。
クラスターの役割分担
クラスターでは全てのノード(今回は各ラズパイ)が同じ計算をすることもできますが、実用上は一つが指示出し役(計算を割り振ったりする)、他が計算役(実際に計算する)に分かれていたりします。
今回は三台のラズパイは一つをMaster 役にして、ほか二つはWorker 役にしました。
また、わかりやすいように以下のようにそれぞれのノードに名前をつけました。
pi0 master
pi1 worker1
pi2 worker2
これらの名前は後にHostnameにします。
機材と配線
用いた機材は以下の通りです。
- ラズパイ5 ×3
- LANケーブル ×3
- SDカード (Ubuntu server 25.04) ×3
- スイッチ
- ディスプレイ
- キーボード
- 上記を動かすための電源(これが意外と大変)
これらを以下のように配線しました。
キーボードやディスプレイはMaster として振る舞うpi0 に接続します。(SSHが通るまでは必要に応じてpi1、pi2につなぎもしました。めんどかった)
┌───────────┐
│ Switch │
└─────┬─────┘
┌───────────┼───────────┐
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ pi0 │ │ pi1 │ │ pi2 │
│ master │ │ worker1 │ │ worker2 │
└────┬────┘ └─────────┘ └─────────┘
│
┌──────┴──────┐
│ Keyboard │
└─────────────┘
┌─────────────┐
│ Display │
└─────────────┘
SSHするまで
IPを固定
各ノードでEthernet のIPを固定しました。pi0、pi1、pi2 をそれぞれ 192.168.1.10、.11、.12としました。固定方法はたくさん記事がありますが、この記事がわかりやすいと思います。
※ついでにWi-Fiの設定などもしておくとあとで apt updateするときやライブラリのインストールで便利です。
とりあえず、ラズパイ同士がLAN経由でやり取りできるかを確認します。
ping -c 3 192.168.1.11
ping -c 3 192.168.1.12
ホスト名の変更
pingで通信ができたら、次はSSHの設定をします。
まずは、これからSSHを多様するので、各ノードのホスト名を変更しておきます。
今回はMaster、Worker1、Worker2としました。
sudo hostnamectl set-hostname master
sudo hostnamectl set-hostname worker1
sudo hostnamectl set-hostname worker2
さらに、これらのIPとホスト名を紐づけるために/etc/hostsを追記します。
これはすべてのノードで行います(統一させます)。
192.168.1.10 master
192.168.1.11 worker1
192.168.1.12 worker2
SSH接続
SSHで接続するにはパスワードによる認証か鍵認証が必要です。
作業のながれはこんな感じです。
- Master にて鍵を作成
- Worker にてパスワードでログインできるよに設定
- Master からWorker にパスワードでSSH接続し、作成した鍵を共有
- Master からWorker にパスワードなしでSSHできるか確認
- Worker のパスワードログインを無効化
まずはMaster にてSSHのための鍵を作成します。
以下のコマンドを打つと色々聞かれますが、基本Enter 連打で大丈夫かと
ssh-keygen -t rsa -b 4096
鍵が作成されているか確認する場合はこれです。二つのファイルがあればOKです。
このとき、.pubが公開鍵です。こちらをWorker に渡します。
ls -l ~/.ssh/id_rsa*
この鍵を共有するために、一度Worker 側はパスワードで入れるように設定します。
以下はデフォルトでは noであるかコメントアウトされています。
編集が終わったらそれを反映させます。
PasswordAuthentication yes
KbdInteractiveAuthentication yes #あれば
sudo netplan apply
次にMaster で作成した鍵をWorker に共有します。
これを実行するとWorker のパスワードが求められ、ログインできると同時に鍵が共有されます。
ssh-copy-id ubuntu@worker1
ssh-copy-id ubuntu@worker2
これでパスワードなしでもSSHが繋がると思うので、テストします。
ssh ubuntu@worker1
ssh ubuntu@worker2
無事、繋がったらWorker のパスワード認証を無効にします。
PasswordAuthentication no
KbdInteractiveAuthentication no #あれば
netplan apply もお忘れなく
MPIの環境構築
並列計算の環境構築ですが、各ノードで全く同じ環境を用意する必要があります。
なので、以下の手順はすべてのノードで行います。
一部Master でのみ行って、SSHでファイル転送するのが楽だったりもします。
ライブラリのインストール
必要なライブラリをインストールします。
# MPIのライブラリ
sudo apt install -y openmpi-bin openmpi-common libopenmpi-dev
# Python系
python3 -m venv ~/mpi-env
source ~/mpi-env/bin/activate # 仮想環境を起動
pip install mpi4py
slotsの指定
MPIの計算で使用する各ノードにおけるプロセス数も指定します。
起動可能なプロセスを示したファイル(hostfile、今回はhosts というファイル名にした)を各ノードに作成します。
なお、Master でのみ作成し、その後SSH のファイル転送でWorker へ配布すると楽です。
master slots=4
worker1 slots=4
worker2 slots=4
SSHでノード間のコピペ↓
scp hosts worker1:~
scp hosts worker2:~
MPIの動作確認用コードの準備
並列計算では実行ファイルはすべてのノードに配布する必要があるため、slots のhostfile 同様、Master でのみ記述し、SSH 間でコピペしました。
from mpi4py import MPI
import socket
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
size = comm.Get_size()
host = socket.gethostname()
print(f"rank {rank}/{size} running on {host}")
このコードは動いているプロセス数と、それがどのホストによって動作されているのかを表示します。
SSHでコピペ↓
scp check_rank.py worker1:~
scp check_rank.py worker2:~
実行は仮想環境
いよいよMPIを動作させます。
Worker は仮想環境の設定は必要ですが、MPIを動作させる際は仮想環境に入る必要はありません。なので、Master のみ入ります。
実行する際は、オプションで動作させるプロセス数-npとホスト名--hostを指定します。
source ~/mpi-env/bin/activate
mpirun -np 3 --host master,worker1,worker2 ~/mpi-env/bin/python check_rank.py
rank 1/3 running on worker1
rank 2/3 running on worker2
rank 0/3 running on master
実際に並列計算
動かしたコード
今回動かすコードは、2~10の7乗までの数字から素数を抽出し、その計算にかかった時間を計るものです。
Master は1000個ずつ数字を渡し、Worker は受け取った数字から素数を探し出しMaster に返します。なぜ、数値を少しずつ渡していくのかについては、過去の記事を参考にしていただければと思います。
もちろん、このコードもMaster だけでなく、Worker にもscpで配布します。
from mpi4py import MPI
import time
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
size = comm.Get_size()
MASTER = 0
END = -1
# --------------------
# 素数判定
# --------------------
def is_prime(n):
if n <= 1:
return False
elif n == 2:
return True
elif n % 2 == 0:
return False
limit = int(n ** 0.5) + 1
for i in range(3, limit, 2):
if n % i == 0:
return False
return True
# --------------------
# master
# --------------------
if rank == MASTER:
N = 10**7
tasks = list(range(2, N))
task_duration = 1000
next_task_start = 0
primes = []
t_start = time.time()
# 最初に worker に1つずつ渡す
for worker in range(1, size):
if next_task_start < len(tasks):
end = min(next_task_start + task_duration, len(tasks))
comm.send(tasks[next_task_start:end], dest=worker)
next_task_start += task_duration
finished = 0
while finished < size - 1:
result, worker = comm.recv(source=MPI.ANY_SOURCE)
if result is not None:
primes.extend(result)
if next_task_start < len(tasks):
end = min(next_task_start + task_duration, len(tasks))
comm.send(tasks[next_task_start:end], dest=worker)
next_task_start += task_duration
else:
comm.send(END, dest=worker)
finished += 1
t_end = time.time()
print(f"found {len(primes)} primes")
print(f"time: {t_end - t_start:.3f} sec")
# --------------------
# worker
# --------------------
else:
t_local_start = time.time()
while True:
tasks: list[int] = comm.recv(source=MASTER)
if tasks == END:
break
local_primes = []
for task in tasks:
if is_prime(task):
local_primes.append(task)
comm.send((local_primes if local_primes else None, rank), dest=MASTER)
実行するには
実際に上のコードをMPIで動かしてみます。
今回のコードではノード間での通信があるので、使用するネットワークを指定する必要があります。
現在ラズパイはEthernetケーブルとWi-Fiに繋がっています。ノード間の通信(TCPによるもの)をEthernetと指定したいので、オプションで--mca btl tcp,selfと--mca btl_tcp_if_include eth0を追加します。
ネットワークを指定しないと、「TCPがうまく行かなかったですよ」的なエラーが出ます。
mpirun -np 5 \
--hostfile ~/hosts \
--map-by slot \
--mca btl tcp,self \
--mca btl_tcp_if_include eth0 \
/home/ubuntu/mpi-env/bin/python \
/home/ubuntu/prime_nums.py
めっちゃ長いですが、これがコマンドです。
実行結果
実験結果は以下のようになりました。
プロセスを増やすごとに、計算に費やした時間は減って行きました。
time: 67.870 sec
undervoltage detected!
time: 34.736 sec
undervoltage detected!
time: 21.262 sec
ただ、ラズパイを3台とも動作させると、undervoltageが検出されました。単純に電源タップから持ってきてる電圧が足りなかったそうです。動きはしたのですが、本来ならもう少し早く計算が終わるのかもしれません。
Discussion