Definition:Message Passing Interface Input/Output
MPI-IOとは、並列計算環境において複数のプロセスが単一または複数のファイルへ効率的かつ協調的にアクセスするために、MPI規格の一部として標準化された並列入出力インターフェースである。MPIが本来提供していたプロセス間通信機能を拡張し、ストレージに対する読み書きも並列計算の一部として統合的に扱えるようにしたものである。
MPI-IOの目的は、スーパーコンピュータや大規模クラスタにおいて発生する膨大なデータの入出力を高速化することである。数千から数万のプロセスが同時に実行される環境では、単純なPOSIXファイルアクセスではメタデータ競合やI/O集中によって性能が大きく低下する。MPI-IOはこれらの問題を解決するために設計されている。
MPI-IOはMPI-2規格(1997年)で正式に導入され、その後、MPI-3およびMPI-4でも継続的に拡張されている。高性能計算(HPC)分野における事実上の標準的な並列I/Oインターフェースとなっている。
1980年代から1990年代初頭にかけて、並列計算機の性能は急速に向上した。しかしストレージシステムは依然として逐次アクセスを前提として設計されていたため、計算性能に比べてI/O性能が極めて低いという「I/Oウォール問題」が顕在化した。
当時の並列アプリケーションでは、各プロセスが独立したファイルを生成する方式(N-to-N方式)が一般的であった。例えば1000個のプロセスが実行される場合、
output_0001.dat
output_0002.dat
...
output_1000.dat
のように大量のファイルが生成された。この方式ではファイル管理が困難になり、ファイルシステムのメタデータサーバにも大きな負荷がかかる。
一方で全プロセスが単一ファイルへ逐次的に書き込む方式(N-to-1方式)では、アクセス競合による性能低下が発生した。
こうした状況を受けてMPI Forumでは並列I/Oの標準化が進められ、1997年にMPI-2規格が策定されてMPI-IOが導入された。その基礎となった研究としては、IBMのPIORFS(Parallel I/O Research File System)や、Argonne国立研究所・NASA等が共同開発したROMIO実装が重要な役割を果たした。ROMIOは現在もMPICH・Open MPIなど多くのMPI実装の内部で広く採用されている。
MPI-IOはIBM SP、Cray T3E、Intel Paragonなど当時の大規模並列計算機で利用されるようになり、その後、Lustre、GPFS(後のIBM Spectrum Scale)、PVFS(現OrangeFS)、BeeGFSなどの並列ファイルシステムの発展とともに広く普及した。
通常のPOSIXファイルアクセスでは、
open()
read()
write()
close()
というインターフェースを利用する。この方式は単一プロセス環境では十分に機能するが、数千プロセスが同時アクセスする環境では効率が悪い。
例えば1000プロセスが同一ファイルへ書き込む場合、それぞれが独立して write( )を発行するため、ファイルシステム側では大量の小規模I/O要求が発生する。またPOSIXのアトミック性保証(O_APPEND等)のために内部的なロックが行われ、これが並列環境での大きなボトルネックとなる。
MPI-IOでは、各プロセスの要求をMPIライブラリが収集・統合してからストレージへ転送することができる。そのためストレージ側から見ると、
1000回の小さな書き込み → 数回の大きな書き込み
として処理できる。これがMPI-IOによる性能向上の根本的な理由である。
なお、MPI-IOはPOSIXセマンティクスの一部(特に「他プロセスの書き込みが即座に見える」という強い一貫性保証)を意図的に緩和することで、より積極的な最適化を可能にしている。
MPI-IOではファイルを MPI_File というオブジェクトとして扱う。
各プロセスは同じファイルを共有しながらも、自身が担当する領域のみを読み書きする。
例えば4個のプロセスが1GBのファイルを生成する場合、
Process 0 → 0〜255MB
Process 1 → 256〜511MB
Process 2 → 512〜767MB
Process 3 → 768〜1023MB
という形でファイルを分割して利用する。このためプロセス間の競合が最小化される。
ファイルオープンには MPI_File_open()を、クローズには MPI_File_close()を使用する。アクセス位置の指定方法として、明示的なオフセット指定(MPI_File_read_at / MPI_File_write_at)と、個別ファイルポインタを使う方式(MPI_File_read / MPI_File_write)、および全プロセス共有の共有ファイルポインタを使う方式の3種類が存在する。
さらにMPI-IOは内部的にファイルシステムの構造を考慮しながら最適なアクセスパターンを生成する。特にLustreのようなストライピングファイルシステムとの相性が良い。
MPI-IOの最も重要な概念の一つがファイルビュー(File View)である。ファイルビューとは、「各プロセスから見える論理的なファイル構造」を定義する仕組みであり、MPI_File_set_view()によって設定する。
ファイルビューは次の3要素で構成される。
例えば物理ファイルが
0 1 2 3 4 5 6 7 8 9 ...
という内容を持つとする。プロセス0が偶数番目、プロセス1が奇数番目を担当する場合、適切なfiletypeを設定することで、
プロセス0のビュー:0 2 4 6 8 ...
プロセス1のビュー:1 3 5 7 9 ...
となる。実際のファイル配置を意識することなく、論理的なデータ構造に基づいてアクセスできる点が特徴である。
MPI-IOにおいて特に重要なのが集団I/O(Collective I/O)である。集団I/Oの関数名には_allサフィックスが付く(例:MPI_File_write_all()、MPI_File_read_all())。
通常の独立I/O(Independent I/O)では各プロセスが個別にアクセスを行う。一方で集団I/Oでは、全プロセスのアクセス要求をMPIライブラリが収集し、最適化したうえでストレージへ転送する。
例えば1000プロセスがそれぞれ4KBを書き込む場合、独立I/Oでは1000 × 4KBの個別書き込みが発生するが、集団I/Oではこれらを統合し、4MBの連続書き込みとして発行できる。これによってディスクシーク回数やネットワーク転送回数が大幅に削減される。
集団I/Oは内部的に「two-phase I/O」と呼ばれるアルゴリズムで実装されることが多い。フェーズ1では特定のプロセス(aggregator)が担当領域のデータをまとめて読み書きし、フェーズ2でaggregatorと各プロセス間でMPI通信によってデータを交換する。集団I/Oを利用するにはすべての参加プロセスが同時に呼び出す必要があり、一部のプロセスだけが呼び出すことはできない。
MPI-IOはデータシービング(Data Sieving)と呼ばれる最適化も提供する。これは不連続なデータ領域をアクセスする場合に利用される。
例えば0、100、200、300、400バイト目だけを読みたい場合、単純な実装では5回の読み込みが必要になる。しかしデータシービングでは必要領域全体をひとつの大きなI/O操作で一度に読み込み、その後不要部分を破棄(あるいは一時バッファに保持)する。これによりアクセス回数を大幅に削減できる。
書き込みの場合は、既存データを読み込んでから変更部分だけを上書きする「read-modify-write」という手順が必要になるため、読み込みに比べてオーバーヘッドが生じる点に注意が必要である。
MPI-IOはLustreと組み合わせて利用されることが極めて多い。Lustreではファイルが複数のOST(Object Storage Target)へストライピングされる。例えばOST1〜OST4へデータが分散されている場合、MPI-IOは各プロセスのアクセスを適切なOSTへ振り分ける。
MPI-IOはLustreのストライプ幅・ストライプカウントを考慮してアクセスパターンを最適化することで、複数OSTへの並列アクセスを最大限活用できる。MPI_Infoオブジェクトを通じてストライプ設定などのヒントをMPIライブラリへ渡すことも可能であり、これによってアプリケーション側から細かいチューニングを行える。
MPI-IOは単なるAPIではなく、Lustreの並列性を最大限活用するための重要な中間層として機能する。
最も基本的なMPI-IOプログラム(独立I/O)は次のようになる。
from mpi4py import MPI
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
fh = MPI.File.Open(
comm,
"data.bin",
MPI.MODE_CREATE | MPI.MODE_WRONLY
)
offset = rank * 1024
data = bytes([rank]) * 1024
fh.Write_at(offset, data) # 独立I/O(オフセット指定)
fh.Close()
このプログラムでは各プロセスが1024バイトずつ異なる位置へ書き込む。例えば4プロセスで実行すると、
Process 0 → 0〜1023バイト
Process 1 → 1024〜2047バイト
Process 2 → 2048〜3071バイト
Process 3 → 3072〜4095バイト
という配置になり、競合なしで並列書き込みが実現される。
集団I/Oを利用する場合は次のようになる。
from mpi4py import MPI
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
fh = MPI.File.Open(
comm,
"output.bin",
MPI.MODE_CREATE | MPI.MODE_WRONLY
)
data = bytes([rank]) * 4096
fh.Write_all(data) # 集団I/O:全プロセスが協調して実行
fh.Close()
Write_all(C APIではMPI_File_write_all)は全プロセスが協調して実行する集団I/O操作であり、MPIライブラリが内部的にアクセスを最適化する。すべての参加プロセスが必ずこの呼び出しに到達する必要がある点に注意が必要である。
Mathematics is the language with which God has written the universe.