【问题标题】:mmap on multiple nodes多个节点上的 mmap
【发布时间】:2020-06-24 01:15:37
【问题描述】:

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

编辑:在 NFS 系统和并行分布式系统上都会出现相同的结果。下面的评论者建议这与 mmap 的页面长度有关。当我的切片的“长度”正好是 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.]]

【问题讨论】:

  • 我假设 /tmp/map 是一个本地文件,仅存在于运行 rank 0 的节点上。我很惊讶memmap 在其他节点上没有失败(例如文件不存在)。
  • 您缺少网络文件系统,例如 NFS(速度慢且通用,易于部署,非常受欢迎)或 Lustre(快速且并行,难以部署,主要用于 HPC)。跨度>
  • 我已经在 NFS 和并行分布式文件系统上对此进行了测试,在这两种情况下都存在同样的问题。
  • 这与内存映射在页面粒度上起作用的事实有关。页面通常不小于 4 KiB。如果您触摸页面中的单个字节,则整个页面被标记为脏,然后整个页面被刷新到磁盘。无论哪个进程最后通过网络刷新其修改的页面,都会覆盖其他进程所做的更改。当所有进程都在同一个主机上时,这不是问题,因为来自 FS 缓存的相同物理页面被映射到所有进程中。解决方案是不要映射这么小的区域,而是每个进程至少有 4 KiB。
  • 请注意,即使您的数组是 1 x 4 x 4 = 16 字节,内存映射的大小也会向上舍入到整个页面,并且也定位在页面大小边界上。

标签: numpy parallel-processing mmap


【解决方案1】:

此答案适用于 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 测试过。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-07-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-25
    • 2021-03-07
    • 2017-03-07
    相关资源
    最近更新 更多