亀山がよくやる「手抜き」PC クラスタのセットアップ (bookworm 編)

最終更新日: 2023年10月11日(水)

まえがき

端的にいうと ハードウェア以外は「0円」「俺様の専用機」 を作る、というつもりのものです。 基本的な仕様や構成は以下のような感じ。

     eth1      +------+     eth0
---------------+ mck1 +--------------+ /home/kameyama
   外向きの    +------+ 192.168.10.1 |
  IP アドレス                        |
               +------+     eth0     |
               | mck2 +--------------+ /home/kameyama
               +------+ 192.168.10.2 | -> 192.168.10.1:/home/kameyama
                                     |
               +------+     eth0     |
               | mck3 +--------------+ /home/kameyama
               +------+ 192.168.10.3 | -> 192.168.10.1:/home/kameyama
                                     |
               +------+     eth0     |
               | mck4 +--------------+ /home/kameyama
               +------+ 192.168.10.4   -> 192.168.10.1:/home/kameyama

親機に Debian GNU/Linux をインストールする

こういう時に亀山がとる手順は以下のような感じ。

  1. インストーラを用意する。

    http://www.debian.org/CD/ から必要なもの (例えば amd64 用の netinst CD イメージ) をダウンロードしてくる。 その後、例えば以下のような手順で、ダウンロードしてきた CD イメージを (中身が潰れても惜しくない) USB メモリに書き込む。

    # cat debian-12.0.0-amd64-netinst.iso > USBメモリのデバイス名 (/dev/sdb とか)
    
    当然ながら、デバイス名の指定は慎重に。 間違った指定をすると、最悪の場合は内蔵ハードディスクを潰してしまうこともありえます。

  2. 最小限のシステムのインストール。

    上で作った USB メモリから起動すると、インストーラが立ち上がり、インストールへと進む。 亀山がやる場合、この時点では (ほとんど) 何のパッケージもインストールしない。 tasksel の選択画面が起動しても、全て解除して進める。 終わったら再起動。

  3. 必要なパッケージのインストール。

    再起動したら、追加でパッケージのインストールを進める。 だいたいは以下をインストール指定しておけば、他に必要なパッケージも「いもづる」式にインストールされる。

    # apt install g++ gfortran libopenblas-dev libopenmpi-dev make openssh-server openmpi-bin
    

    その他、亀山は (自分の好みの問題で) 以下もインストール指定する。

    # apt install bzip2 less sudo tcsh xauth
    

  4. ネットワークの設定。

    /etc/network/interfaces を以下のような内容に修正。 内向き LAN (この例では eth0) の設定はこの通りだが、外向き LAN (この例では eth1) の設定はネットワーク環境によって異なるので、それぞれ正しいものに書き換えるべし。 また、Debian 9 (stretch) 以降のバージョンでは、ネットワークインターフェースがこれとは違った新しい規則で命名されることにも注意。

    auto lo
    iface lo inet loopback
    allow-hotplug eth0
    iface eth0 inet static
    	address 192.168.10.1
    	netmask 255.255.255.0
    	network 192.168.10.0
    	broadcast 192.168.10.255
    	gateway 0.0.0.0
    allow-hotplug eth1
    iface eth1 inet static
    	address ???.???.???.???
    	netmask ???.???.???.???
    	network ???.???.???.???
    	broadcast ???.???.???.???
    	gateway ???.???.???.???
    

子機をネットワークでブートするための準備

ここの設定も全て親機上で行う。 親機 の /srv/nfsroot に子機用のシステムを構築する。 また子機は親機から DHCP で IP アドレスを受け取り、親機内の /srv/tftp 以下にあるカーネルイメージを用いて PXE でブートさせる。

  1. 必要なパッケージのインストール (その1)

    DHCP サーバーといえば昔は isc-dhcp-server だったが、bookworm の次のリリースからはパッケージが提供されなくなるらしい。ということで、代替として dnsmasq を使うようにしてみる。また dnsmasq には tftp サーバーの機能もあるそうなので、tftpd-hpa もいらなくなった。

    # apt install dnsmasq pxelinux initramfs-tools nfs-kernel-server ntpsec
    
  2. 必要なパッケージのインストール (その2)

    di-netboot-assistant は本来は「ネットワークインストール」環境を構築するためのものであるが、ここでは「子機」を UEFI で起動させるために必要なファイルを準備するために拝借する。また UEFI での起動のために grub-efi-amd64-bin、セキュアブートのために shim-signed と grub-efi-amd64-signed もインストールする。

    # apt --no-install-recommends install di-netboot-assistant grub-efi-amd64-bin shim-signed grub-efi-amd64-signed
    
  3. 子機の起動用のカーネルと RAM ディスクの準備
    • まず最初に、tftp で転送する起動用のファイルを置く場所を作っておく。ディレクトリ名は tftpd-hpa の標準にならった。
      # mkdir /srv/tftp
      
    • カーネルイメージの準備。ここでは vmlinuz という名前にしてある。
      # cp /boot/vmlinuz* /srv/tftp/vmlinuz
      
    • ブートのための RAM ディスクを作る。 この際に注意すべきであろうことは、親機用の initrd を作るためのディレクトリ /etc/initramfs-tools/ とは別の作業用ディレクトリを用意して作業すること (さもなくば、親機の initrd が書き換えられてしまいます)。 以下の例では、/srv/initramfs-tools という作業ディレクトリを新たに作って作業することにする。
      • 作業ディレクトリを準備する
        # mkdir /srv/initramfs-tools
        # cp -Rp /etc/initramfs-tools/modules /etc/initramfs-tools/scripts /srv/initramfs-tools/
        
      • 子機の RAM ディスク用の設定ファイルとして、/srv/initramfs-tools/initramfs.conf というファイルを作成し、以下の記述を行う。 なお、以下で使用する mkinitramfs コマンドの仕様により、ファイル名は必ず initramfs.conf にしておくこと。
        MODULES=netboot
        BUSYBOX=y
        KEYMAP=n
        COMPRESS=gzip
        DEVICE=
        NFSROOT=auto
        BOOT=nfs
        
      • 次のコマンドにより /srv/tftp/initrd.img を作る。
        # mkinitramfs -d /srv/initramfs-tools/ -o /srv/tftp/initrd.img
        
  4. tftp により子機を起動するための設定 (その1; BIOS 起動用)
    • /srv/tftp/pxelinux.cfg/default を以下の内容で作成。当然ながら NFS 親機の IP アドレスは内向きのものを。またルートファイルシステムを read-only にした場合 (安全を期そうとしたのだが) には、子機からうまく NFS マウントできなかった。
      LABEL linux
      DEFAULT vmlinuz root=/dev/nfs initrd=initrd.img nfsroot=192.168.10.1:/srv/nfsroot ip=dhcp init=/bin/systemd rw quiet
      
    • BIOS によるネットワークブートの準備
      # cp /usr/lib/syslinux/modules/bios/ldlinux.c32 /usr/lib/PXELINUX/pxelinux.0 /srv/tftp
  5. tftp により子機を起動するための設定 (その2; UEFI 起動用)

    この設定がとても分かりにくい。仕方がないので di-netboot-assistant の力を借りることにした。

    # di-netboot-assistant --tftproot=/srv/tftp install bookworm
    
    これにより、/srv/tftp/d-i/n-a/ というディレクトリ以下にいろいろ作られる。 ただしこのままでは「bookworm をネットワークインストールする」用に構成されてしまうので、そうでない用に作り直すべく、/srv/tftp/d-i/n-a/grub/grub.cfg の末尾に以下のようにな内容を追記する。 この設定のミソは、/srv/tftp/pxelinux.cfg/default の記述にならい、先に作った vmlinuz と initrd.img で起動させるように指定することでしょう。
    set default='Debian boot'
    set timeout=1
    
    menuentry 'Debian boot' {
       linux   vmlinuz root=/dev/nfs nfsroot=192.168.10.1:/srv/nfsroot ip=dhcp init=/bin/systemd rw quiet
       initrd  initrd.img
    }
    

  6. dnsmasq の設定

    以下のような内容で /etc/dnsmasq.d/pxe-tftp.conf を作成する。ミソは以下の通り。

    • DNS サーバーを起動させない (port=0)
    • eth0 からのみ DHCP リクエストを受け付ける
    • dhcp-range の指定がないと DHCP サーバーが起動しない。
    • 子機に固定の IP アドレスを割り当てる (dhcp-host=)。子機の MAC アドレスは事前に調べておきましょう。例えばどうにかして Linux を起動しておき、/sys/class/net/(インターフェース名)/address の中身をみれば MAC アドレスが分かります。
    • dhcp-match と dhcp-boot の組み合わせにより、BIOS で起動させるか UEFI で起動させるかによって、dhcp-boot で渡すファイルを変更できるようにしてある。

    port=0
    interface=eth0
    dhcp-range=192.168.10.2,192.168.10.10
    dhcp-host=11:22:33:44:55:66,mck02,192.168.10.2
    dhcp-host=11:22:33:44:55:66,mck03,192.168.10.3
    dhcp-host=11:22:33:44:55:66,mck04,192.168.10.4
    dhcp-match=set:efi-x86_64,option:client-arch,7
    dhcp-match=set:efi-x86_64,option:client-arch,9
    dhcp-match=set:efi-x86,option:client-arch,6
    dhcp-match=set:bios,option:client-arch,0
    dhcp-boot=tag:efi-x86_64,d-i/n-a/bootnetx64.efi
    dhcp-boot=tag:efi-x86,d-i/n-a/bootnetx64.efi
    dhcp-boot=pxelinux.0
    dhcp-option=option:ntp-server,192.168.10.1
    enable-tftp
    tftp-root=/srv/tftp
    

    念のためファイルのパーミッションを設定し、その後 dnsmasq の再起動。

    # chmod -R 777 /srv/tftp
    # systemctl restart dnsmasq
    
  7. NFS サーバーの設定。 /etc/exports を編集。 たぶん以下のようなものがあればOKのはず。
    /home	     192.168.10.0/255.255.255.0(rw,async,no_root_squash,no_subtree_check)
    /srv/nfsroot 192.168.10.0/255.255.255.0(rw,async,no_root_squash,no_subtree_check)
    
    その後 nfsd の再起動。
    # systemctl restart nfs-kernel-server
    
  8. NTP サーバーの設定。 /etc/ntpsec/ntp.conf を編集。 たぶん以下のようなものがあればOKのはず。
    server your_ntp_server
    restrict 192.168.10.0 mask 255.255.255.0 nomodify notrap
    
    その後 ntpd の再起動。
    # systemctl restart ntpsec
    
  9. ホスト名の登録。 /etc/hosts を以下のように編集する。
    192.168.10.1 mck1
    192.168.10.2 mck2
    192.168.10.3 mck3
    192.168.10.4 mck4
    

子機のシステムをインストールする

まずは親機上でシステムを作るところから。 既存のシステムを丸ごと複製する手もあるようだが、ここは Unix/Linux システムからの Debian GNU/Linux のインストール よろしく、debootstrap で作り直す。

  1. 必要なパッケージのインストール。
    # apt install debootstrap
    
  2. システムの基本部分をインストール
    # mkdir /srv/nfsroot
    # debootstrap --arch=amd64 bookworm /srv/nfsroot http://ftp.jp.debian.org/debian
    
  3. 子機に必要なパッケージをインストール。 まずはシステムの稼働に必要なものを
    # chroot /srv/nfsroot apt install console-setup locales nfs-common openssh-server systemd-timesyncd
    
    続いて、計算ジョブの実行に必要なものを。
    # chroot /srv/nfsroot apt install g++ gfortran libopenblas-dev libopenmpi-dev make openmpi-bin
    
    その他、亀山は (自分の好みの問題で) 以下もインストール指定する。
    # chroot /srv/nfsroot apt install bzip2 less sudo tcsh xauth
    
  4. /srv/nfsroot/etc/systemd/timesyncd.conf を編集。 最終行に以下のような指定があればOKのはず。
    NTP=192.168.10.1
    
  5. /srv/nfsroot/etc/network/interfaces を以下の通り修正。
    auto lo
    iface lo inet loopback
    iface eth0 inet manual
    
  6. /srv/nfsroot/etc/fstab を以下の通り修正。 ここで /var/run を tmpfs とかにマウントしてしまうと、systemd が困ってしまうので注意。 また jessie の時と違って、/var/lock を tmpfs とかにマウントするのもダメらしい。 なお root ファイルシステムの指定は起動時になされているので、ここで書かなくても大丈夫のはず。
    proc            /proc           proc    defaults        0       0
    tmpfs           /tmp            tmpfs   defaults        0       0
    tmpfs           /var/tmp        tmpfs   defaults        0       0
    tmpfs           /var/log        tmpfs   defaults        0       0
    192.168.10.1:/home /home        nfs     defaults        0       0
    
  7. 子機に /etc/hostname はいらないので消す。
    # rm /srv/nfsroot/etc/hostname
    
  8. 子機にも /etc/hosts はいるので、親機からもらってくる。
    # cp -p /etc/hosts /srv/nfsroot/etc
    
  9. タイムゾーンの設定。 正式には chroot環境下で timedatectl set-timezone Asia/Tokyo とするのだろうが、/proc/ がマウントされていないと動かないようなので。 また /srv/nfsroot/etc/localtime は標準状態では /usr/share/zoneinfo/Etc/UTC へのシンボリックリンクになっているので、単純にコピーするのは危ないかも。
    # ln -sf /usr/share/zoneinfo/Asia/Tokyo /srv/nfsroot/etc/localtime
    
  10. アカウントの調整。 root のパスワードをつけたり、一般ユーザーのアカウントを作ったり。
    # chroot /srv/nfsroot passwd
    # chroot /src/nfsroot adduser --no-create-home kameyama
    
ここまでできれば、子機をネットワーク経由でブートすればOK。 もしこの際に「Secure Boot Violation」とかいう赤い警告が表示された場合は、BIOS メニュー内で Secure Boot をオフにすればよいはず。

SSHの設定

クラスタを構成する全てのPCに、パスワードなしでログインできるようにしたい。

  1. 鍵の作成。

    以下の手順で、指示通りに進めていくと、秘密鍵 ~/.ssh/id_rsa と公開鍵 ~/.ssh/id_rsa.pub ができる。

    $ ssh-keygen -t rsa
    

  2. 公開鍵の登録。

    $ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
    $ chmod 600 ~/.ssh/authorized_keys
    

  3. クラスタ内の他のPCに、パスワードなしで ssh でログインできることを確認。

OpenMPI の動作確認

例えばこんな Fortran プログラムはどうでしょう。

program psample
  !$ use omp_lib
  implicit none
  include 'mpif.h'
  character(len=MPI_MAX_PROCESSOR_NAME) :: hostname
  integer :: myrank,nprocs,ierr,hostname_len
  logical :: with_openmp=.false.
  !$ with_openmp = .true.
  call mpi_init(ierr)
  call mpi_comm_size(mpi_comm_world,nprocs,ierr)
  call mpi_comm_rank(mpi_comm_world,myrank,ierr)
  call mpi_get_processor_name(hostname,hostname_len,ierr)
  if (with_openmp) then
     !$omp parallel
     !$ write(*,'("Hello, I am rank",i4," out of",i4," running on ",a," and thread",i4," out of",i4,".")') &
     !$ myrank,nprocs,trim(hostname),omp_get_thread_num(),omp_get_num_threads()
     !$omp end parallel
  else
     write(*,'("Hello, I am rank",i4," out of",i4," running on ",a,".")') &
          myrank,nprocs,trim(hostname)
  endif
  call mpi_finalize(ierr)
end program psample
これを psample.f90 として作成し、
$ mpif90 psample.f90
$ mpirun -np 2 ./a.out
$ mpirun -np 8 -host mck1:2,mck2:2,mck3:2,mck4:2 ./a.out
あるいは OpenMP も組み合わせるなら
$ mpif90 -fopenmp psample.f90
$ mpirun -np 2 ./a.out
$ mpirun -np 1 -host mck1 --bind-to none ./a.out
$ mpirun -np 4 -host mck1,mck2,mck3,mck4 ./a.out
などとして、しかるべく動けばOK。