多个节点上的 mmap

mmap on multiple nodes

下面的脚本使用 mmap 并行写入内存映射数组。但是,它仅在所有进程都在同一节点上时才有效 - 否则它会为不在等级 0 节点上的处理器或输出中的其他杂散零生成 0 行。为什么是这样?我觉得我遗漏了一些有关 mmap 工作原理的信息。

编辑:相同的结果出现在 NFS 系统和并行分布式系统上。下面的评论者建议这与 mmap 的页面长度有关。当我的切片的 'length' 正好是 4KiB 时,脚本仍然会产生错误的输出。当切片长于 4 KiB 时也会发生同样的情况。

#!/usr/bin/python3

from mpi4py import MPI
import numpy as np

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

length = int(1e6)      # Edited to make test case longer.
myfile = "/tmp/map"

if rank == 0:
    fp = np.memmap(myfile, dtype=np.float32, mode='w+', shape=(size,length))
    del fp

comm.Barrier()

fp = np.memmap(myfile, dtype=np.float32, mode='r+', shape=(1,length),
                offset=rank*length*4)
fp[:,:] = np.full(length,rank)

comm.Barrier()

if rank == 0:
    out = np.memmap(myfile, dtype=np.float32, mode='r', shape=(size,length))
    print(out[:,:])

正确输出:

[[ 0.  0.  0.  0.]
 [ 1.  1.  1.  1.]
 [ 2.  2.  2.  2.]
 [ 3.  3.  3.  3.]
 [ 4.  4.  4.  4.]]

输出不正确。等级 3 和 4 的处理器不写入。

[[ 0.  0.  0.  0.]
 [ 1.  1.  1.  1.]
 [ 2.  2.  2.  2.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]]

此答案适用于 NFS 文件。其他网络文件系统上的 YMMV。

问题与 MPI 或 numpy.memmap 无关,而是与 Linux 内核如何缓存 NFS 文件数据有关。据我从一些实验中可以看出,在请求从 NFS 服务器读取之前,客户端请求最后修改的时间戳。如果此时间戳不比客户端上次写入的时间戳更近,则数据将从客户端的缓存中获取,而不是再次从服务器请求。如果N1和N2是节点,可能会出现以下情况:

  1. N1和N2打开同一个零填充文件;文件内容 [00],最后修改时间:t=0.00.
  2. N1 和 N2 内核请求比需要更多的文件内容并将其存储在缓存中。 N1 缓存:[00] (t=0.00); N2缓存:[00] (t=0.00).
  3. 在时间t=0.01,N2写入文件的后半部分。服务器状态:[02] (t=0.01); N1缓存:[00](0.00); N2缓存:[02](0.01).
  4. 在时间t=0.02,N1写入前半部分。服务器:[12] (0.02); N1 缓存:[10] (0.02)。 N2缓存:[02](0.01).
  5. N1 上的进程尝试读取后半部分。
  6. N1内核向服务器请求最后修改时间;结果:t=0.02.
  7. N1 内核检索前半部分的陈旧缓存内容 [0]。

因此,您需要确保其他客户端(节点)更新时间戳。

if rank == 0:
    fp = np.memmap(myfile, dtype=np.float32, mode='w+', shape=(size,length))
    del fp

comm.Barrier() # B0
fp = np.memmap(myfile, dtype=np.float32, mode='r+', shape=(1,length),
                offset=rank*length*4)

comm.Barrier() # B1 (this one may be superfluous)
fp[:,:] = np.full(length, rank)
del fp # this will flush the changes to storage

comm.Barrier() # B2
from pathlib import Path
from time import sleep
if rank == 1:
    # make sure that another node updates the timestamp
    # (assuming 1 s granularity on NFS timestamps)
    sleep(1.01)
    Path(myfile).touch()
    sleep(0.1) # not sure

comm.Barrier() # B3
if rank == 0:
    out = np.memmap(myfile, dtype=np.float32, mode='r', shape=(size,length))
    print(out[:,:])

关于障碍B1:我这里没有设置MPI;我用按键模拟了它。我不确定这个障碍是否真的有必要。 sleep(0.1) 也可能不是必需的;它只是为了防止 touch() 函数返回和 NFS 服务器接收更新之间的任何延迟。

我假设您安排了数据,以便每个节点访问与 4096 字节边界对齐的内存映射文件的部分。我用 length=4096.

测试过

这个解决方案有点乱,它依赖于 NFS 驱动程序的未记录行为。这是在 Linux 内核 3.10.0-957 上,NFS 挂载选项包括 relatime,vers=3,rsize=8192,wsize=8192。如果您使用这种方法,我建议您包括一个自测:基本上,上面的代码带有一个 assert 语句来验证输出。这样,如果它由于不同的文件系统而停止工作,您将捕获它。