在上一篇中我们介绍了 mpi4py 中 I/O 操作的一致性语义,下面我们将介绍 mpi4py 中的文件互操作性。
文件是一种持续性的存储,当程序运行完后依然会存在(只要程序不删除文件),可能会被其他程序或在其他环境中使用,因此文件的互操作性就是一个问题。所谓文件互操作就是指一个运行环境/进程写入文件的数据能够被另外的环境/进程所正确解析。MPI 程序操作的文件只是一般的普通文件,MPI 并不会自动在其操作的文件中添加应用和环境等相关的额外信息。为性能上的考虑,MPI 并没有指定应用该以何种物理方式创建文件,尽管 MPI 在逻辑上总是将一个文件视作一个线性的字节序列。如果某个 MPI 实现创建的文件不同于文件系统的普通文件,MPI 标准要求 MPI 实现提供将其创建的文件转换成一个线性字节序列的功能,而且还必须提供一些像复制、删除、移动文件等用户熟悉的功能。因此,非 MPI 的程序总是能够访问由 MPI 创建的文件,虽然可能需要做一些转换。在大多数情况下,MPI 创建的文件是和非 MPI 程序创建的文件完全一致,因此根本用不着转换步骤。
处于单一运行环境下,MPI 可以实现百分之百的互操作。为实现不同运行时环境之间的文件互操作,MPI 定义了统一的外部文件表示规范——external32。
只要满足文件一致性约束,即可保证单一运行环境下的文件互操作性,不论参与的进程数量是多少,都可一致地解析数据文件。文件互操作性可概括为如下 3 种:
- 从二进制表示解析文件的内容。
- 不同文件结构之间的互相转换。
- 不同主机环境之间信息解释机制的互相转换。
这里主要讨论文件在 MPI 环境内外传输时的互操作性,具体而言就是要指定 MPI 环境如何提供能够与 POSIX 兼容的语义(如 cp,rm,mv 等),此外还要保持文件在环境内外相应字节偏移位置上的一一对应。
对不同主机环境之间的文件互操作,通过类型信息(etype 和 filetype)加以规范和转换,并据此提供跨环境(MPI 与非 MPI)、跨平台(不同结构主机)的文件互操作性。
文件数据表示
因为不同的主机环境可能会有不同的二进制数据表示,如字节顺序,数据类型包含的字节数目等,一个主机环境创建的文件可能无法被另一个主机正确地解析。MPI 提供了相应的机制来创建可移植性的文件,我们将在下面进行逐步的介绍。
在前面的所有例子中,我们都没有显式地设置 MPI.File.Set_view 的 datarep
参数,在此情况下,我们默认使用的是一种被称作 native 的数据表示方式,MPI 可支持以下 3 种数据表示方式:
- native:数据在文件中采取与其在内存中相同的表示方式。这种表示方式的好处是不需要进行类型转换,因此可避免数据精度损失以及减少转换所耗的时间,缺点是只能在同构的环境中使用,无法实现异构环境下的跨平台,即这种数据表示形式是不具有可移植性的。这种表示方式的数据,MPI 环境在底层通信中通常要直接使用 MPI.BYTE 数据类型,以免数据类型间转换导致精度变化。
- internal:可用于同构和异构环境中,MPI 环境负责相应的数据类型转换。只要使用数据文件的各进程保持有一致的数据类型定义即可。但不同的 MPI 实现可能采取不同的解释协议。
- external32:MPI 定义的一种特殊数据表示形式,它本质上是一个 32 位的大端 IEEE 格式。这种表示方式的优点是:首先,异构环境中的进程在外部一致地看到 external32,但在进程内部则可分别使用自己所处平台的本地数据表示;其此,可在不同 MPI 环境之间进行文件导出导入。缺点是:由于有数据格式转换可能会导致精度丢失并增加操作时间开销。具体实现时,可让访问数据一侧的进程具体实施数据转换,而发送数据一侧的进程仍使用 MPI.BYTE 类型提供数据,从而避免两次数据转换的开销和精度损失。一般来说,只有在需要可移植性的情况下才会用到 external32 表示。
native 和 internal 两种表示方式取决于具体的 MPI 实现,而 external32 则在 MPI 环境中提供完全一致的解析协议。使用时,可通过文件视图设置方法 MPI.File.Set_view 的 datarep
参数指定具体的表示方式。只要在创建文件和访问文件时指定相同的表示方式,则任何情况下都可正确地解析文件内容。注意:MPI 实现可能选择 external32 作为 internal 表示。
MPI 实现还可以支持其他的数据表示方式。MPI 还允许用户自定义新的数据表示并经注册后在程序中使用。
创建可移植的文件
尽管 internal 和 external32 数据表示可产生更好的文件互操作性,但只有在用户调用读/写方法和 MPI.File.Set_view 时设置正确的数据类型(非 MPI.BYTE)时才能真正实现好的可移植性。提供设置正确的数据类型,MPI 实现可以知道怎样实现必要的类型转换。另外特别需要注意时是在使用派生数据类型来设置文件视图时,因为有些派生数据类型创建方法的偏移量是以字节为单位计数的。要使用派生数据类型来设置文件视图,创建的派生数据类型的字节偏移量必须设置成文件数据表示的相应长度,而不是数据在内存中表示的长度。MPI 中的方法 MPI.File.Get_type_extent 就是为此目的而提供的。该方法会返回一个数据类型以文件中的数据表示的跨度,其使用接口如下:
MPI.File.Get_type_extent(self, Datatype datatype)
返回该文件中数据类型 datatype
的跨度,这个跨度值对所有访问该文件的进程都相同。如果某进程的当前文件视图使用了用户自定义的数据表示,则 MPI 环境会使用用户定义数据表示时指定的回调函数 dtype_file_extent_fn
自动计算类型跨度。自定义数据表示将在下面进行介绍。
需要注意的是,传递给读/写方法的数据类型参数都指的是在内存中的数据布局,创建这些数据类型的偏移量也对应的是内存中数据表示的偏移量,只有在设置文件视图方法中用到的数据类型才必须关注文件的数据表示方式,千万不要混淆。
自定义数据表示方式
当应用程序需要读/写某些 MPI 实现无法识别的文件格式时需要考虑使用特定的自定义数据表示。MPI 提供了一种解析自定义数据表示的注册机制,使得用户可在 MPI 的 I/O 流中插入第三方解析机制进行必要的数据转换。自定义数据表示的函数接口如下:
MPI.Register_datarep(datarep, read_fn, write_fn, extent_fn)
向 MPI 注册一种数据表示方式 datarep
,并为这种数据表示方式提供读转换函数 read_fn
,写转换函数 write_fn
和文件数据类型的跨度计算函数 extent_fn
。这个 datarep
可在设置文件视图时通过 MPI.File.Set_view 通知给进程。
该方法是一个本地方法,即其仅为调用它的进程创建相应的数据表示。如果已经存在这种数据表示定义,则会抛出 MPI.Exception。datarep
的字符串长度由 MPI.MAX_DATAREP_STRING 确定上限。
这些回调函数的使用接口如下:
read_fn(ubuf, dtype, count, fbuf, position)
write_fn(ubuf, dtype, count, fbuf, position)
extent_fn(dtype)
一般用不着自定义数据表示方式,因此这里不再进一步介绍。如果用户有需要,可以参考 MPI 标准。
例程
下面给出使用例程。
# io_interop.py
"""
Demonstrates how to write portable files.
File created in this way can be read with any MPI implementation on any machine.
Run this with 2 processes like:
$ mpiexec -n 2 python io_interop.py
"""
import numpy as np
from mpi4py import MPI
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
filename = 'temp.txt'
# open the file for read and write, create it if it does not exist,
# and delete it on close
fh = MPI.File.Open(comm, filename, amode= MPI.MODE_CREATE | MPI.MODE_RDWR | MPI.MODE_DELETE_ON_CLOSE)
# this just sets the data representation to external32 (with dummy values
# for disoplacement, etype and filetype)
fh.Set_view(0, MPI.BYTE, MPI.BYTE, 'external32')
# get the extent of MPI.INT when data representation is set to external32
extent_in_file = fh.Get_type_extent(MPI.INT)
# construct filetype, which consists of 2 ints and a gap of 4 ints
# instead of using MPI.INT.Get_size(), we use extent_in_file to calculate extent
INT2 = MPI.INT.Create_contiguous(2)
lb = 0
extent = (2 + 4) * extent_in_file
filetype = INT2.Create_resized(lb, extent)
filetype.Commit()
etype = MPI.INT
# the displacement in the file is also calculated by using extent_in_file
disp = 5 * extent_in_file
# set the file view (with the real displacement, etype and filetype)
fh.Set_view(disp, etype, filetype, 'external32')
buf = np.arange(10*rank, 10*(rank+1), dtype='i')
# set the individual file pointer and write data to file
fh.Seek(rank*10, whence=MPI.SEEK_SET)
fh.Write(buf)
# check what's in the file
if rank == 0:
buf1 = np.zeros(10*comm.size, dtype='i')
fh.Read_at(0, buf1)
print 'data in file: %s' % buf1
# close the file
fh.Close()
运行结果如下:
$ mpiexec -n 2 python io_interop.py
data in file: [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19]
以上介绍了 mpi4py 中的文件互操作性,在下一篇中我们将介绍 mpi4py 中获得高性能 I/O 的方法和建议。