HPCシステムズではエンジニアを募集しています。詳しくはこちらをご覧ください。
HPCシステムズのエンジニア達による技術ブログ

Tech Blog

SingularityコンテナでAMD GPUを試す -2-

前置き

この記事を書いている間にSingularityはEPELリポジトリから最新の3.5.3がインストールできるようになり、GROMACSは2020.1がリリースされました。ちなみにGROMACSは「誤った結果を出す」という理由で、GROMOS力場を削除していくようで、以前のデータがそのままでは使えなくなったり、計算結果が異なる可能性があります。ただ、2020.1にもファイルは従来どおり残っているので(使おうとするとワーニングは出ますが)一応計算は実行でき、ベンチマークの取得は可能です。

GROMACSに関する予備知識

分子動力学計算は単一の計算を繰り返しているわけではなく、近距離にある粒子の間に働く力を計算するPP(Particle-Particle)と、クーロン力のように長距離に及ぶ力についてフーリエ変換を使って大幅に計算を簡略化するPME(Particle-Mesh-Ewald)の2種類が同時並行で動いています。PPの計算は、個々の粒子と特定の距離(カットオフ半径)以内にある粒子をリストアップしておき、計算するリストにあるものだけを計算します。この2種類は当然アルゴリズムそのものが違い、かつカットオフ半径の選び方で、計算負荷のバランスは一定になりえません。さらにPPには、結合がある場合(動きに制約がある)と、結合がない場合とで別れます。つまり内部では3種類の計算フローが混在し、それぞれにどれだけリソースを割り当てるのか?という問題があります。

GROMACSは地上最速の分子動力学プログラムを目指す、というのが開発ポリシーで、ハードウエアを全て使いきれるように作られています。CPUのSIMDであるSSEやAVXを使うエンジンはアセンブラで記述され、NVIDIA GPUを使うところはCUDAで書かれています。さらにCPUとGPUをバランスよく使って全てのエンジンを使うよう、ロードバランスする仕組みもあります。そのため、GPUだけがんばっていてCPUは遊んでる(もしくは逆)という状況に陥らないよう自動的に調整させることもある程度まで可能です。ただし、ユーザーの指定する計算条件によってはこの仕組みも十分に機能しません。

さて、AMD GPUのコードはOpenCLを使って記述されています。OpenCLはKhronosという会社が提唱した標準規格で、アクセラレータを使えるコードはこの文法で記述してOpenCLライブラリをリンクします。アクセラレータのベンダーはOpenCLのバックエンドとなるようにライブラリを書き、OpenCLライブラリがそれらへ計算を転送するという仕組みになっています。そのため、アプリケーションを記述する側がハードウエアが何かを意識する必要がありません。ハードウエアベンダーはOpenCLにだけ対応するライブラリを書き、専用のコンパイラ等を作る必要がありません。また、OpenCLでビルドされたバイナリは、ハードウエアのライブラリを直接リンクしないので、異なるアクセラレータを持つ別の環境でもそのまま使える(ことになっている)という利点があります。OpenCLライブラリがどのハードウエア(に紐づくベンダーライブラリ)を使うかは、システムごとに設定ファイルで記述します。つまり、AMD GPU専用にコーディングされているわけではないということになります。

ギリギリまで最適化できるCUDAが十分普及しているNVIDIA GPUを使う際、OpenCLを選択することはあまりないと思いますが、AMD GPUを使う場合はOpenCLを使うケースが現時点では多いようです。先程書いた通り、OpenCLを使っていればNVIDIAとAMDの両方で動くバイナリが作れますが、NVIDIA GPUを使うのであればCUDAで作り込んである実装を使うほうがずっと性能が良いです。また、GPUでは相互作用の計算のみ行い、それによる粒子の座標の更新はCPU側で行いますが、CUDA実装では座標の更新までGPUの中で完結させられます。これにより空いたCPU側でPMEのフーリエ変換を行うといった指定も可能になります。このように、環境のハードウエア構成やリソースの割り当て方など、多彩な条件でパフォーマンスが決まるため、異なる条件におけるGROMACSベンチマークの直接比較は、あまり意味がないのかもしれません。マニュアルには、どれが最適かを知る方法はないので、自分で試せと書いてあります。

本題

GROMACS本体の構築に、MPIはmpich-3.3を、コンパイラは別の記事でも書いたdevtoolset-7を使いました。singularityは前述の通りEPELからインストールした3.5.3です。コンテナイメージはUBI7を使いましたが、実行に必要になるgompライブラリを別途追加しています。

リンク状況を確認してみましょう。singularityを--rocmオプションで起動する際、取り込まれるツールとライブラリは、/etc/singularity/rocmlibraries.confにリストアップされています。

$ singularity shell --rocm -B /opt UBI7.sif
Singularity> ldd /opt/GROMACS-2020.1/bin/mdrun_mpi 
	linux-vdso.so.1 =>  (0x00007ffcf3d6c000)
	libmpi.so.12 => /usr/local/lib/libmpi.so.12 (0x00007fcc1be02000)
	libdl.so.2 => /lib64/libdl.so.2 (0x00007fcc1bbfe000)
	librt.so.1 => /lib64/librt.so.1 (0x00007fcc1b9f6000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fcc1b7da000)
	libOpenCL.so.1 => /.singularity.d/libs/libOpenCL.so.1 (0x00007fcc1b5bb000)
	libgomp.so.1 => /lib64/libgomp.so.1 (0x00007fcc1b395000)
	libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fcc1b08e000)
	libm.so.6 => /lib64/libm.so.6 (0x00007fcc1ad8c000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fcc1ab76000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fcc1a7a8000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fcc1c34a000)

libOpenCL.so.1はリンクされていますが、確認するとこれはROCmのものではなく、rocm-openclをインストールする際に要求されるocl-icdつまりEPEL由来の/lib64/libOpenCL.so.1です。ただしlibOpenCLは単なるラッパーなので、規格のバージョンさえあっていれば、どちらでも使えるはずです。また、計算エンジン本体は上記リストにあるlibamdocl64.soなのですが、/.singularity.d/libs/にそれが見つかりません。実際この状態で起動すると、起動はするもののGPUが見つからないと言われてしまいます。

何が起きているかをコンテナの外で確認しましょう。

$ rpm -ql rocm-opencl
/opt/rocm-3.1.0/opencl/bin/x86_64/clinfo
/opt/rocm-3.1.0/opencl/lib/x86_64/libOpenCL.so.1
/opt/rocm-3.1.0/opencl/lib/x86_64/libamdocl64.so
$ ldconfig -p |grep OpenCL
	libOpenCL.so.1 (libc6,x86-64) => /lib64/libOpenCL.so.1
$ ldconfig -p |grep amdocl
$

つまり、/etc/singularity/rocmlibraries.confに記述はされているもののうちld.so.cacheに取り込まれたものだけが/.sinigularity.d/libsに採用される動きをしています。/etc/ld.so.conf.dをみると、/opt/rocm/libなどはあるものの、/opt/rocm/opencl/lib/x86_64がありません。一方でベンダーライブラリを確認すると、

$ cat /etc/OpenCL/vendors/amdocl64.icd 
/opt/rocm-3.1.0/opencl/lib/x86_64/libamdocl64.so

絶対パスで指定されています。ロードパスに入っているライブラリであればファイル名だけ記述しておけば動きますし、実際ROCm-3.0ではそうなっていました。いずれにせよ、たまたまGROMACSを/opt以下にインストールしたのでsingularity起動時に-B/optとして/opt/を取り込んだため見ることができますが、これを共有していないコンテナ内ではlibOpenCLが処理をどこにふっていいのかわからなくなり、GPUが使えなくなるわけです。singularityのマニュアルには-B/etc/OpenCL/としてこのファイルを取り込むソリューションが書いてありますが、上記の様に絶対パスで書かれていると対応ができません。

対策としては幾つか考えられます。

  1. /opt/rocm/opencl/lib/x86_64/etc/ld.so.conf.d/amdocl.confのようなファイルで記述してldconfigしてしまう。
  2. 既に設定がある/opt/rocm/libから/opt/rocm/opencl/lib/x86_64内のライブラリへリンクを張ってldconfigする。
  3. コンテナ内にライブラリファイルを取り込んで、パスを通してしまう。

ここでは1の方法をとります。これがrocm-openclのインストール時に設定されない理由ははっきりしませんが、とりあえずこれでライブラリは正しく取り込まれ、mdrunもGPUを使って動くようになります。

実行

GROMACSには並列実行の方法が多彩ですが、基本的にプロセス並列、スレッド並列、そしてアクセラレータの3種類になります。プロセス並列には2種類選択でき、複数ノードを使う場合でも対応できるOpenMPIやMPICH、IntelMPIなど外部MPIを使う方法、そしてノード内に限られますがGROMACS自身が持つ内部MPIを使って複数プロセスを起動する方法です。スレッド並列には、各プロセスでOpenMP(OMP)スレッドを展開できます。そして、それぞれにどのアルゴリズムを担当させるかを数で指定できるようになっています。

GPUを使う場合にはGPU1枚ごとに1プロセスが必要なため、4枚のGPUを使うにはMPIで4プロセスを立ち上げる必要があります。また各プロセスがOMPスレッドをいくつ立ち上げるかは、ユーザー自身で指定しない限り、システムのコア数/MPIプロセス数で自動的に計算されます。今回使ったシステムはCPUが24コアあり、GPUは3枚です。つまりMPIで3プロセス以上の起動が必要です。この指定だけをすると、各プロセスはOMPスレッドが8個たちあがり、24コアを使えるようになります。

インプットとしては、定番のADH_cubicのPME版を使います。やはり定番TutorialのKALP-15 DPPCなども試してみたのですが途中でエラーになり、実際の計算に至りませんでした。

$ singularity shell --rocm -B /opt ~/UBI7.sif
Singularity> source /opt/GROMACS-2020.1/bin/GMXRC
Singularity> mpirun -np 3 mdrun_mpi  -s adh_cubic_pme.tpr -noconfout         
                      :-) GROMACS - mdrun_mpi, 2020.1 (-:
... 中略 ...
GROMACS:      mdrun_mpi, version 2020.1
Executable:   /opt/GROMACS-2020.1/bin/mdrun_mpi
Data prefix:  /opt/GROMACS-2020.1
Working dir:  /home/hpc/gromacs_benchmarks/ADH/adh_cubic
Command line:
  mdrun_mpi -s adh_cubic_pme.tpr -noconfout

Reading file adh_cubic_pme.tpr, VERSION 2020-MODIFIED (single precision)
Changing nstlist from 10 to 100, rlist from 0.9 to 1.058

On host localhost.localdomain 3 GPUs selected for this run.
Mapping of GPU IDs to the 3 GPU tasks in the 3 ranks on this node:
  PP:0,PP:1,PP:2
PP tasks will do (non-perturbed) short-ranged interactions on the GPU
PP task will update and constrain coordinates on the CPU
Using 3 MPI processes
Using 8 OpenMP threads per MPI process

NOTE: DLB will not turn on during the first phase of PME tuning
starting mdrun 'NADP-DEPENDENT ALCOHOL DEHYDROGENASE in water'
10000 steps,     20.0 ps.

NOTE: DLB can now turn on, when beneficial
Dynamic load balancing report:
 DLB was off during the run due to low measured imbalance.
 Average load imbalance: 3.0%.
 The balanceable part of the MD step is 56%, load imbalance is computed from this.
 Part of the total run time spent waiting due to load imbalance: 1.7%.

NOTE: The GPU has >25% more load than the CPU. This imbalance wastes
      CPU resources.

               Core t (s)   Wall t (s)        (%)
       Time:     1529.966       63.749     2400.0
                 (ns/day)    (hour/ns)
Performance:       27.109        0.885

中頃に、GPU3枚を確保し、PPの計算をGPUの番号0,1,2に振り分けていること、その結果の粒子の移動についてはCPU側で実施されていること、3プロセスごとに8スレッドで実行していることが書かれています。これだけ安価な機材を使っているわりにそこそこの性能ではないでしょうか。後半にあるDLB(Dynamic Load Ballance)とは、それぞれの計算負荷がほぼ等しくなるような自動調整の機能です。計算初期をステップ数をサンプルにして、分割の調整が妥当であれば実施されます。

ちなみに、PPのうち、結合がある部分の計算は今回のGPU実装では非対応(-bonded gpuとするとエラーとなる)になっています。そのため上述したKALP-15の計算ではあまり有効ではないことが予想されます。また、プロセスを分けると領域分割が行われますが、PMEの計算には系全体をフーリエ変換する必要があることから、複数GPUでの計算には未対応です。使用するGPUが1枚、つまりプロセス数も1の場合に限り、PME計算をGPUへ任せることができます。また、PMEについてもフーリエ変換のみをCPUやGPUに割り当てるという指示もできます。それならば粒子の座標更新はCPU側で行われるので、CPU側でフーリエ変換し、その結果との相互作用をGPUで計算するということはできるはずですが、少なくとも現時点ではできません。

そこでGPUは1枚しか使えませんが、プロセス数を1として結合のないPPとPMEの力の計算をGPUに任せるという投げ方をしてみましょう。結果は以下のようになります。

1 GPU selected for this run.
Mapping of GPU IDs to the 2 GPU tasks in the 1 rank on this node:
  PP:0,PME:0
PP tasks will do (non-perturbed) short-ranged interactions on the GPU
PP task will update and constrain coordinates on the CPUPME tasks will do all aspects on the GPU
Using 1 MPI process
Using 24 OpenMP threads 

starting mdrun 'NADP-DEPENDENT ALCOHOL DEHYDROGENASE in water'
10000 steps,     20.0 ps.

               Core t (s)   Wall t (s)        (%)
       Time:     1982.251       82.624     2399.1
                 (ns/day)    (hour/ns)
Performance:       20.916        1.147

プロセス数が1なのでOMPスレッドは24個できました。しかし全体の性能としては20%ほど低くなっています。

まとめ

singularityを用いてコンテナ内からAMD GPUを使ったGROMACSを動作させました。これでOpenCL対応アプリを駆動させるところまで、だいたいご理解いただけたのではないでしょうか。

なお、どのアルゴリズムにどの計算資源を使うかを指定するには、次のようなオプションが用意されています。デフォルトが全てautoなので基本的には全自動ということになります。

-nb <enum> (auto)
 Calculate non-bonded interactions on: auto, cpu, gpu
-pme <enum> (auto)
 Perform PME calculations on: auto, cpu, gpu
-pmefft <enum> (auto)
 Perform PME FFT calculations on: auto, cpu, gpu
-bonded <enum> (auto)
 Perform bonded calculations on: auto, cpu, gpu
-update <enum> (auto)
 Perform update and constraints on: auto, cpu, gpu

また、プロセス数やスレッド数については次のようになっています。

-npme <int> (-1)
 Number of separate ranks to be used for PME, -1 is guess
-nt <int> (0)
 Total number of threads to start (0 is guess)
-ntmpi <int> (0)
 Number of thread-MPI ranks to start (0 is guess)
-ntomp <int> (0)
 Number of OpenMP threads per MPI rank to start (0 is guess)
-ntomp_pme <int> (0)
 Number of OpenMP threads per MPI rank to start (0 is -ntomp)

先述の通り、インプットの内容やシステム構成によって最適解は千差万別です。長時間システムを専有する計算を行う場合は、いくつか有効と思われる候補を用い、実地テストによるパラメータチューニングをおすすめします。計算リソースの有効利用に役立てていただければ幸いです。