MPI

  • 进程间通信
    • OpenMPI
    • MPICH
    • MVAPICH
    • Intel MPI / IBM MPI / Microsoft MPI / …
  • Objects
    • Processes: Independent execution units, separate mem space.
    • Communicators: Groups of procs that can communicate
    • Ranks: Unique ID for procs within a communicator
  • Communication Patterns
    • P2P (Pair)
      • Blocking: MPI_Send, MPI_Recv
      • Non-Blocking: MPI_Isend, MPI_Irecv
        • Ensure sent: MPI_Wait, MPI_Test
    • Collective (Group)
      • Broadcast: MPI_Bcast
      • Gather/Scatter ==(from/to root)==: MPI_Gather, MPI_Scater
      • Reduction: MPI_Reduce (to root), MPI_Allreduce (to all)
    • One-Sided / Remote Memory Access
      • MPI_Put, MPI_Get
      • 窗口:在使用单边通信之前,进程必须明确地将一部分内存注册为一个“窗口”,允许其他进程对其进行单边访问。操作依据窗口的指定位置。
      • 需要单独的同步机制(MPI_Win_fence, MPI_Win_lock/MPI_Win_unlock, etc)来确保数据一致性和操作的完成
#include <mpi.h>
#include <stdio.h>
 
int main(int argc, char** argv) {
    // Initialize MPI environment
    MPI_Init(&argc, &argv); 
 
    // Get total number of processes
    int world_size;
    MPI_Comm_size(MPI_COMM_WORLD, &world_size); 
 
    // Get the rank of the process
    int world_rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &world_rank); 
 
    // Print a message from each processor
    printf("Hello from processor %d out of %d processors\n", world_rank, world_size);
 
    // Finalize the MPI environment
    MPI_Finalize(); 
 
    return 0;
}
// Random Order
Hello from processor 0 out of 4 processors
Hello from processor 1 out of 4 processors
Hello from processor 2 out of 4 processors
Hello from processor 3 out of 4 processors
  • MPI_COMM_WORLD: Communicator

Communicator / 通信器

特性

  • 通信上下文 (Context for communication)
    • 通信只能在通信器内部发生:通信不跨越通信器 。
      • MPI 级别的通信隔离。
    • 通信器关联着两个组(本地和远程)。
    • 通信器缓存 Info 对象。
  • 通信器类型 (Communicator types)
    • 组内通信器 (Intra-communicator):本地组 == 远程组。
    • 组间通信器 (Inter-communicator):本地组不与远程组重叠;从组 A 到组 B 的通信,反之亦然。
  • MPI 进程可以是多个通信器的成员
    • 在每个通信器中可以有不同的等级 (rank)。
  • 预先定义的类型
    • 例如 MPI_INT/MPI_FLOAT/MPI_CHAR/…
  • 可以从预定义数据类型创建新的数据类型
    • 类型映射 = {{type_0, disp_0}, . . . , {type_n-1, disp_n-1}}
      • type_t – 块 t 的基本数据类型
      • disp_t – 块 t 的位移(以字节为单位)
    • 例子: 考虑一个 C 结构体 struct Point { int x; float y; };。如果你想发送一个 Point 类型的数组,你可以创建一个 MPI 数据类型来描述 xy 字段的布局,包括它们的类型 (MPI_INT, MPI_FLOAT) 和它们相对于结构体起始位置的字节位移。
  • MPI 提供了一组构造函数来辅助创建新的数据类型,常见的构造函数包括:
    • MPI_Type_contiguous: 创建一个由相同基本数据类型连续重复组成的新数据类型。
    • MPI_Type_vector: 创建一个由基本数据类型组成的块序列,这些块以固定的步长(stride)重复。
    • MPI_Type_indexed: 允许更灵活的块布局,每个块可以有不同的长度和位移。
    • MPI_Type_struct: 最通用和灵活的构造函数,用于描述任意的内存结构,比如 C 结构体。它直接操作类型映射中的 type_tdisp_t
  • 基指针、数据类型和计数(count)的组合描述了提供给 MPI 实现的缓冲区
    • 基指针 (Base pointer): 这是内存中数据开始的地址。
    • 数据类型 (Data type): 这是你之前定义或选择的 MPI 数据类型(可以是预定义的,也可以是自定义的),它告诉 MPI 如何解释从基指针开始的内存布局。它定义了单个“项”的结构。
    • 计数 (Count): 这表示要发送或接收的数据类型项的数量。例如,如果你发送一个 MPI_INT 数组,count 就是数组中整数的个数。如果你发送一个自定义的 Point 结构体数组,count 就是 Point 结构体的个数。

常见函数

查询

  • 获取通信器中的 MPI 进程数(大小 N)
    • 获取通信器大小
      • MPI_COMM_SIZE(comm, size)
        • IN – comm: 通信器(句柄)
        • OUT – size: 通信器组中的进程数(整数)
  • 获取调用 MPI 进程在通信器中的秩(0,1,2,…N-1)
    • MPI_COMM_RANK(comm, rank)
      • IN – comm: 通信器(句柄)
      • OUT – rank: 通信器组中调用进程的秩(整数)
  • 比较通信器
    • MPI_COMM_COMPARE(comm1, comm2, result)
      • IN – comm1: 第一个通信器(句柄)
      • IN – comm2: 第二个通信器(句柄)
      • OUT – result: 结果(整数)

通信器操作

  • 创建新通信器
    • MPI_COMM_DUP_WITH_INFO(comm, info, newcomm)
      • 这个函数用于创建一个现有通信器 comm 的精确副本
      • IN – comm: 通信器(句柄)
      • IN – info: 信息对象(句柄)
      • OUT – newcomm: comm 的副本(句柄)
    • MPI_COMM_CREATE(comm, group, newcomm)
      • 这个函数允许你基于一个现有通信器 comm 的进程组的子集来创建一个新的通信器 newcomm。你首先需要使用 MPI_COMM_GROUP 函数从 comm 中获取其进程组,然后通过 MPI_GROUP_INCLMPI_GROUP_EXCL 等函数定义一个子组 group,最后使用这个子组来创建新的通信器。只有那些包含在 group 中的进程才会成为 newcomm 的一部分。
      • IN – comm: 通信器(句柄)
      • IN – group: 组,它是 comm 组的子集(句柄)
      • OUT – newcomm: 新通信器(句柄)
    • MPI_COMM_SPLIT(comm, color, key, newcomm)
      • 将一个现有通信器 comm 中的进程分裂成多个新的、不相交的子通信器。
      • IN – comm: 通信器(句柄)
      • IN – color: 子集分配的控制(整数)
      • IN – key: 秩分配的控制(整数)
      • OUT – newcomm: 新通信器(句柄)
  • MPI_COMM_CREATE_FROM_GROUP(group, stringtag, info, errhandler, newcomm)
    • MPI-3.0 引入的一个函数,它提供了一种更灵活和稳健的方式来从一个进程组创建新的通信器。与 MPI_COMM_CREATE 类似,它也是基于一个进程组来构建通信器,但它增加了额外的参数,使其在某些高级用例中更具优势,特别是在错误处理和避免通信器命名冲突方面。
    • IN – group: 组(句柄)
    • IN – stringtag: 此操作的唯一标识符(字符串)
    • IN – info: 信息对象(句柄)
    • IN – errhandler: 将附加到新内部通信器的错误处理器(句柄)
    • OUT – newcomm: 新通信器(句柄)

构造

MPI_TYPE_VECTOR(int count, int blocklength, int stride, MPI_Datatype oldtype, MPI_Datatype *newtype)
  • count (IN): 非负整数,表示块的数量。

    • 解释: 定义了新数据类型中包含多少个重复的数据块。
  • blocklength (IN): 非负整数,表示每个块中的元素数量。

    • 解释: 定义了每个重复块由多少个 oldtype 类型的元素组成。
  • stride (IN): 整数,表示每个块起始位置之间的元素数量。

    • 解释: 这是一个关键参数。它不是以字节为单位的步长,而是以 oldtype 元素的数量为单位的步长。例如,如果 stride 为 4,意味着从一个块的起始位置到下一个块的起始位置,之间有 4 个 oldtype 类型的元素(包括当前块的 blocklength 个元素和其后的填充/跳过的元素)。
  • oldtype (IN): MPI_Datatype 句柄,表示作为基础的旧数据类型。

    • 解释: 这是组成向量的每个块所使用的数据类型。它可以是预定义的数据类型(如 MPI_INT)或用户自定义的复合数据类型。
  • newtype (OUT): MPI_Datatype 句柄,用于存储新创建的向量数据类型。

    • 解释: 函数成功执行后,这个句柄将指向新定义的向量数据类型。

Request

特性

  • 由MPI实现内部提供的通信操作句柄
  • 句柄对于每个非阻塞和持久操作都是唯一的
  • 用于跟踪“长时间操作”的状态
  • 用户不显式分配请求——它作为MPI调用(如MPI_IsendMPI_Bcast_Init)的一部分进行分配
  • 用户可以显式释放请求——不要对活动操作执行此操作,必须先确认通信操作已经完成(例如通过MPI_WaitMPI_Test确认),然后才能安全地释放对应的请求
    • MPI_REQUEST_FREE(request)
    • INOUT - request: 通信请求(句柄)

通信类型

P2P

标签匹配

左侧场景:接收操作在数据到达前已就绪(Pre-posted Receive)

  1. 发送方发起“Send”: 发送进程开始发送数据。

  2. 数据传输中: 数据正在网络中传输。

  3. 接收方调用“Receive”: 在数据实际到达接收端之前,接收进程就已经调用了MPI_Recv(或其他接收函数),表示它已经准备好接收数据,并且可能已经指定了匹配的标签和源。

  4. “Deliver data to user buffer”(将数据交付给用户缓冲区): 当数据到达接收端时,由于接收操作已经就绪并等待着带有匹配标签的数据,数据可以立即被直接放入用户预先分配好的内存缓冲区中。

    • 优点: 这是更高效的通信方式。因为接收缓冲区已经准备好,数据在到达时可以直接放置到最终目的地,避免了中间缓冲和额外的数据复制,从而降低了通信延迟。这通常是高性能MPI应用程序所期望的行为。

右侧场景:接收操作在数据到达后才调用(Post-arrived Receive)

  1. 发送方发起“Send”: 发送进程开始发送数据。

  2. 数据传输中: 数据正在网络中传输。

  3. 数据到达: 数据到达了接收节点,但此时接收方还没有调用MPI_Recv来接收它。

  4. “Receive called and deliver data to user buffer”(调用接收并交付数据给用户缓冲区): 接收操作是在数据已经到达(或正在到达)之后才被调用的。这意味着:

    • 网络层可能需要将传入的数据暂时存储在一个内部缓冲区中(称为“缓冲”或“暂存”),直到接收操作被调用。

    • 一旦接收操作被调用并找到了匹配的标签,数据才会被从内部缓冲区复制到用户分配的缓冲区中。

    • 缺点: 这种方式可能引入额外的开销。如果数据到达时没有匹配的接收操作,系统就必须临时缓存数据。这会占用系统内存,并增加一次额外的数据复制操作(从内部缓冲区复制到用户缓冲区),从而增加了通信延迟,并可能降低吞吐量,尤其对于大型消息。