mpi4py 中获得高性能 I/O 的方法和建议

在上一篇中我们介绍了 mpi4py 中的文件互操作性,下面我们将介绍 mpi4py 中获得高性能 I/O 的方法和建议。

MPI 在底层实现中可充分利用集合操作和非连续数据读/写进行面向文件系统/设备的特殊优化。因此使用 MPI I/O 操作,最重要的就是要活用其提供的几个特征:单次非连续数据访问,集合操作,非阻塞操作,理解并灵活设置适合文件系统特征的各 hint,等。

由于底层可能提供的优化措施,应尽量使更多的进程参与 I/O 操作而不是将其集中在一个进程上。对非连续 I/O,应定义相应的派生数据类型描述“非连续性”,进而再利用集合操作让多个进程一起提交数据 I/O 请求。通常情况下应用程序都有自己的计算模式进而也可找出其 I/O 访问特征,要针对不同的 I/O 特征结合底层硬件设计相应的执行模型。对相同的 I/O 访问特征,也能以不同的发起方式提交给 I/O 系统,比如说使用哪一个具体的读/写方法及怎样调用该读/写方法等。

4 个级别的访问模式

一般可以将 I/O 访问模式分为 4 个级别。

我们以一个具体的例子来讲解这四个不同的访问级别。考虑一个以块状形式分布在16 个进程上的二维数组,具体的分布情况如下图所示:

数组分布及访问特征

这个数组作为一个整体以行优先的顺序存储在一个文件中,如上图所示,文件中首先是 0 号进程本地子数组的第一行,紧接着是 1 号进程本地子数组的第一行,然后是 2 号进程本地子数组的第一行,然后是 3 号进程本地子数组的第一行,接下来是 0 号进程本地子数组的第二行,1 号进程本地子数组的第二行,等等。现在每个进程要从这个文件中读取该进程本地的数据。可以看出,每个进程的本地数据都是以不连续的小块方式存储于文件的不同位置。

对这个例子,4 个级别的访问模式分别为:

级别 0:各个进程按照类似 Unix/Linux 中 I/O 操作方式独立地一次读取本地子数组的一行。具体来说,像下面这样:

fh = MPI.File.Open(..., filename, ...)
for i in range(num_local_rows):
    fh.Seek(...)
    fh.Read(row[i], ...)
fh.Close()

级别 1:基本同级别 0,不过采用集合 I/O 操作。具体来说,像下面这样:

fh = MPI.File.Open(MPI.COMM_WORLD, filename, ...)
for i in range(num_local_rows):
    fh.Seek(...)
    fh.Read_all(row[i], ...)
fh.Close()

级别 2:各个进程定义派生数据类型作为 filetype 描述数据分布方式,然后独立访问自己负责的数据块。具体来说,像下面这样:

subarray = MPI.INT.Create_subarray(...)
subarray.Commit()
fh = MPI.File.Open(..., filename, ...)
fh.Set_view(..., subarray, ...)
fh.Read(local_array, ...)
fh.Close()

级别 3:基本同级别 2,不过采用集合 I/O 操作。具体来说,像下面这样:

subarray = MPI.INT.Create_subarray(...)
subarray.Commit()
fh = MPI.File.Open(MPI.COMM_WORLD, filename, ...)
fh.Set_view(..., subarray, ...)
fh.Read_all(local_array, ...)
fh.Close()

这 4 个级别的访问模式代表着逐步更多数据量的单次 I/O 请求,就像下图所示:

4 个级别的访问模式

每次 I/O 请求的数据量越大,MPI 实现就有更大的机会来产生更高的性能。因此只要可能,用户应该尽量使用级别 3 的数据访问模式。

当应用程序的每个进程只需要访问文件中单独一整块连续的数据,级别 0 和级别 2 将变的等价,级别 1 和级别 3 也等价,此时用户没有必要定义派生数据类型,直接使用级别0 或级别 1 也能取得高的性能。

高性能 I/O 优化措施

MPI IO 库内优化措施

有了上述讨论的访问特征信息,MPI IO 的底层实现可实施如下优化:

  • 数据筛选。从物理设备读取大块数据,然后再从中提取进程自己所需部分。
  • 集合 I/O 操作。将来自若干进程的数据访问请求组合形成一个大的数据访问请求。
  • 预取和缓冲。根据访问策略将数据从磁盘预先调入缓冲区。

应用程序级优化措施

  • 分析应用程序数据输入输出的特点。如果可能尽量使用级别 3 的 I/O 操作方法。
  • 尽量向 MPI IO 库提供更多的精准信息。如打开文件时使用精准的打开模式,即不设置运行时不需要的多余访问模式;运行中可能的情况下尽量使用集合操作;尽量减少操作个数和次数以及减少数据移动等。
  • 文件组织结构要与进程拓扑相匹配。结合数据访问模式和使用特点,在数据分解时刻意组织数据在数组各维的分布以使得 I/O 时可操作的连续数据块尽量大。
  • 了解环境信息。收集 I/O 设备的物理配置,逻辑组织结构,操作系统,文件系统的特征等信息,了解 MPI IO 在这些环境中可用的 hints 有哪些,分别起什么作用等。通过 MPI.File.Open,MPI.File.Set_view 等向 MPI 实现传递合适的 hints。
  • 考虑使用非阻塞 I/O 操作。非阻塞 I/O 操作虽然不能提高 I/O 本身的性能,但可能将 I/O 操作与部分计算和通信重叠起来提高程序的整体性能。

例程

下面给出使用例程。

# high_perf_io.py

"""
Demonstrates the four levels of access.

Run this with 16 processes like:
$ mpiexec -n 16 python high_perf_io.py
"""

import numpy as np
from mpi4py import MPI


comm = MPI.COMM_WORLD
rank = comm.Get_rank()

# create a p x q Cartesian process grid
p, q = 4, 4
cart_comm = comm.Create_cart([p, q])
# get the row and column coordinate of each process in the process grid
ri, ci = cart_comm.Get_coords(rank)

# the global array
lm, ln = 3, 2 # shape of local subarray
m, n = p*lm, q*ln # shape of global array
global_ary = np.arange(m*n, dtype='i').reshape(m, n)
rs, re = lm*ri, lm*(ri+1) # start and end of row
cs, ce = ln*ci, ln*(ci+1) # start and end of column
# local array of each process
local_ary = np.ascontiguousarray(global_ary[rs:re, cs:ce])
print 'rank %d has local_ary with shape %s' % (rank, local_ary.shape)

filename = 'temp.txt'

# the etype
etype = MPI.INT

# construct filetype
gsizes = [m, n] # global shape of the array
subsize = [lm, ln] # shape of local subarray
starts = [rs, cs] # global indices of the first element of the local array
filetype = MPI.INT.Create_subarray(gsizes, subsize, starts)
filetype.Commit()

# first write the global array to a file
if rank == 0:
    fh = MPI.File.Open(MPI.COMM_SELF, filename, amode=MPI.MODE_CREATE | MPI.MODE_WRONLY)
    fh.Write(global_ary)
    fh.Close()

comm.Barrier()


# level 0
# ----------------------------------------------------------------------------------

local_ary1 = np.zeros_like(local_ary)

fh = MPI.File.Open(comm, filename, amode=MPI.MODE_RDONLY)
for i in range(lm):
    offset = etype.Get_size() * ((rs + i) * n + cs)
    fh.Seek(offset)
    # each process uses individual read
    fh.Read(local_ary1[i, :])
    # fh.Read_at(offset, local_ary1[i, :])
assert np.allclose(local_ary, local_ary1)
fh.Close()


# level 1
# ----------------------------------------------------------------------------------

local_ary1 = np.zeros_like(local_ary)

fh = MPI.File.Open(comm, filename, amode=MPI.MODE_RDONLY)
for i in range(lm):
    offset = etype.Get_size() * ((rs + i) * n + cs)
    fh.Seek(offset)
    # use collective read
    fh.Read_all(local_ary1[i, :])
    # fh.Read_at_all(offset, local_ary1[i, :])
assert np.allclose(local_ary, local_ary1)
fh.Close()


# level 2
# ----------------------------------------------------------------------------------

local_ary1 = np.zeros_like(local_ary)

fh = MPI.File.Open(comm, filename, amode=MPI.MODE_RDONLY)
fh.Set_view(0, etype, filetype)
# each process uses individual read
fh.Read(local_ary1)
# fh.Read_at(0, local_ary1)
assert np.allclose(local_ary, local_ary1)
fh.Close()


# level 3
# ----------------------------------------------------------------------------------

local_ary1 = np.zeros_like(local_ary)

fh = MPI.File.Open(comm, filename, amode=MPI.MODE_RDONLY)
fh.Set_view(0, etype, filetype)
# use collective read
fh.Read_all(local_ary1)
# fh.Read_all_all(0, local_ary1)
assert np.allclose(local_ary, local_ary1)
fh.Close()

# remove the file
if rank == 0:
    MPI.File.Delete(filename)

运行结果如下:

$ mpiexec -n 16 python high_perf_io.py
rank 7 has local_ary with shape (3, 2)
rank 13 has local_ary with shape (3, 2)
rank 15 has local_ary with shape (3, 2)
rank 5 has local_ary with shape (3, 2)
rank 9 has local_ary with shape (3, 2)
rank 10 has local_ary with shape (3, 2)
rank 11 has local_ary with shape (3, 2)
rank 12 has local_ary with shape (3, 2)
rank 14 has local_ary with shape (3, 2)
rank 0 has local_ary with shape (3, 2)
rank 1 has local_ary with shape (3, 2)
rank 2 has local_ary with shape (3, 2)
rank 3 has local_ary with shape (3, 2)
rank 4 has local_ary with shape (3, 2)
rank 8 has local_ary with shape (3, 2)
rank 6 has local_ary with shape (3, 2)

以上介绍了 mpi4py 中获得高性能 I/O 的方法和建议,在下一篇中我们将介绍 mpi4py 并行读/写 numpy npy 文件的方法。

你可能感兴趣的:(mpi4py 中获得高性能 I/O 的方法和建议)