在上一篇中我们介绍了 mpi4py 中 I/O 相关的 hints,下面我们将介绍 mpi4py 中 I/O 操作的一致性语义。
MPI 的 I/O 操作的一致性语义指明了多个进程进行 I/O 操作的结果。MPI 程序访问文件都通过一个集合操作 MPI.File.Open 返回的文件句柄来执行,应提供以下 3 个级别的一致性:
- 使用相同文件句柄访问文件的串行一致性;
- 使用同一个集合操作(MPI.File.Open)以原子模式(atomic mode)打开的若干文件句柄访问文件的串行一致性;
- 应用程序根据需要自定义的访问一致性。
所谓串行一致性是指要求一组访问文件的操作按照一定顺序依次实施,每个访问都应是原子操作,但允许具体执行时的顺序有所不同。应用程序自定义的一致性可通过程序语句顺序加之以 MPI.File.Sync 实现。
为明确定义文件操作的一致性,这里先说明一下数据访问动作的概念:一般来说,一个普通的阻塞非集合或者集合读/写调用就是一个独立的数据访问动作,但分步集合操作(如 MPI.File.Read_all_begin/MPI.File.Read_all_end),“begin” 和 “end” 方法的一个配对构成一个完整的数据访问动作,非阻塞访问(如 MPI.File.Iread/MPI.Request.Wait)启动和完成动作的配对构成一个完整的数据访问动作。
I/O 操作的一致性语义有多种可能性,对不同的情况需要分别加以讨论,在此之前,我们先介绍与一致性语义相关的方法使用接口:
一致性语义方法接口
MPI.File.Open(type cls, Intracomm comm, filename, int amode=MODE_RDONLY, Info info=INFO_NULL)
并行打开文件方法,在前面作过介绍。
MPI.File.Close(self)
关闭文件方法,在前面作过介绍。
MPI.File.Set_atomicity(self, bool flag)
设置当前打开文件的原子模式,如果 flag
为 True,则设置为原子模式,否则设置为非原子模式。该方法执行一个集合操作,参与该操作的进程组必须传递相同的 flag
参数值。
MPI.File.Get_atomicity(self)
返回当前一致性约束的状态。如果是原子模式则返回 True,否则返回 False。
MPI.File.Sync(self)
该方法是一个集合操作,如果某个进程调用该方法,则到此为止该进程已经写入文件的数据全部刷新到磁盘上。如果其它进程也更新过文件,则此方法之后所有更新的结果都会对组内进程可见。在调用此方法之前,应用程序应确保文件句柄上的所有非阻塞操作、分步集合操作以及其它操作都已经完成,否则可能导致一些随机错误。
一致性语义的不同情况
I/O 操作的一致性语义的完整的描述请参考 MPI 标准,这里只介绍几种最常见的情况。
简单情况
在两组情况下,一致性不是一个问题,文件操作的正确性总会得到保证。
只读访问:如果所有的进程都只执行只读操作而不向文件写入任何数据,则每个进程都会正确地读到所需的数据,而不管在打开文件时使用的是什么通信子对象(MPI.COMM_WORLD,MPI.COMM_SELF 或者其他通信子)。
不同的文件:如果每个进程访问一个单独的不同的文件(即没有任何一个文件被多个进程共享),MPI 保证一个进程写入到文件中的数据会被这同一个进程在写动作之后的任何一个时刻正确地读到而无需进行特别的同步等操作。
当多个进程访问同一个文件并且至少一个进程会向文件中写入数据时,情况就会更复杂些,而且一致性语义的保证还依赖于打开文件时所使用的通信子对象。一般来说,如果使用的是包括访问文件的所有进程的通信子对象(如 MPI.COMM_WORLD)来打开文件则 MPI 会保证更强的一致性语义;而如果使用的是仅包括访问文件的部分进程构成的通信子对象(如 MPI.COMM_SELF)来打开文件则 MPI 会保证弱一些的一致性语义。不管是在什么情况下,如果 MPI 不能自动地保证 I/O 操作的一致性语义,用户可以自己采取一些步骤来保证。我们将在下面介绍在不同的情况下如何来实现。
访问使用 MPI.COMM_WORLD 打开的同一个文件
当所有进程都访问同一个使用 MPI.COMM_WORLD 打开的文件并且至少一个进程会向文件中写入数据时,如果不同的进程访问的是文件中不重叠的区域,MPI 能够自动保证一个进程在其写动作后再正确地读取其写入的数据而无需额外的同步等操作。比如说像下面这样:
# Process 0 | Process 1
fh = MPI.File.Open(MPI.COMM_WORLD, 'file', ...) | fh = MPI.File.Open(MPI.COMM_WORLD, 'file', ...)
fh.Write_at(0, [buf, 100, MPI.BYTE], ...) | fh.Write_at(100, [buf, 100, MPI.BYTE], ...)
fh.Read_at(0, [buf, 100, MPI.BYTE], ...) | fh.Read_at(100, [buf, 100, MPI.BYTE], ...)
在上面这个例子中,2 个进程使用 MPI.COMM_WORLD 打开同一个文件,每个进程向文件中不重叠的区域写入 100 个字节的数据,然后读取写入的数据。在这种情况下 MPI 保证能正确地读取到所写入的数据。
但是如果每个进程要读取其他进程刚刚写入的数据,即不同进程要访问文件中重叠的区域,在这种情况下,MPI 不能自动保证进程会读取到正确的数据,用户必须采取一些额外的措施来保证正确性,有如下 3 种选择:
- 使用原子性操作:在每个进程写之前,调用上面介绍的 MPI.File.Set_atomicity 设置对文件的访问为原子模式,如下:
# Process 0 | Process 1
fh = MPI.File.Open(MPI.COMM_WORLD, 'file', ...) | fh = MPI.File.Open(MPI.COMM_WORLD, 'file', ...)
fh.Set_atomicity(True) | fh.Set_atomicity(True)
fh.Write_at(0, [buf, 100, MPI.BYTE], ...) | fh.Write_at(100, [buf, 100, MPI.BYTE], ...)
MPI.COMM_WORLD.Barrier() | MPI.COMM_WORLD.Barrier()
fh.Read_at(100, [buf, 100, MPI.BYTE], ...) | fh.Read_at(0, [buf, 100, MPI.BYTE], ...)
在上面的例子中,设置了对文件的访问为原子模式,MPI 会保证一个进程能够立即正确地读取另一个进程写入文件的数据。如果没有设置对文件的访问为原子模式(文件被打开后默认没有开启原子模式),则 MPI 不能保证这种正确性。注意在写动作之后的一个 Barrier 同步是用来保证每个进程在完成其写动作后数据才会被另一个进程所读取。
- 关闭文件并重新打开:保证正确读取数据的另一种方式是在写动作后关闭文件并重新打开,然后再读取另一个进程所写入的数据,如下: