MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Fortran MPI并行计算入门

2022-08-227.1k 阅读

Fortran MPI并行计算入门

一、MPI简介

MPI(Message Passing Interface)即消息传递接口,它是一种用于编写并行程序的标准库。MPI提供了一组函数,允许程序员在不同的进程之间进行通信和同步。MPI是一种分布式内存并行编程模型,这意味着每个进程都有自己独立的内存空间,进程之间通过消息传递来交换数据。这种模型适用于多节点集群系统,其中每个节点都有自己的内存。MPI被广泛应用于科学计算、工程模拟等领域,因其能够充分利用大规模计算资源,显著提升计算效率。

二、Fortran与MPI的结合

Fortran语言是一种历史悠久且在科学计算领域广泛应用的编程语言。它具有强大的数值计算能力和简洁的语法结构,非常适合与MPI结合进行并行计算。通过在Fortran程序中调用MPI函数,程序员可以轻松地将串行的Fortran代码改造成并行程序,从而充分利用多核处理器或集群系统的计算能力。

三、MPI环境搭建

  1. 安装MPI库
    • 在Linux系统中,常见的MPI实现有OpenMPI和MPICH。以Ubuntu系统为例,可以使用以下命令安装OpenMPI:
sudo apt - get install openmpi - bin openmpi - common libopenmpi - dev
- 在Windows系统中,可以使用MS - MPI。从微软官方网站下载并安装MS - MPI,安装完成后,需要将MPI的bin目录添加到系统的环境变量中。

2. 配置Fortran编译器 - 对于Linux系统,GNU Fortran(gfortran)是常用的编译器,可以通过包管理器进行安装,例如在Ubuntu中:

sudo apt - get install gfortran
- 在Windows系统中,可以使用MinGW - w64,它包含了gfortran编译器。安装MinGW - w64后,同样需要将其bin目录添加到系统环境变量中。

四、MPI基本概念

  1. 进程与通信域
    • 在MPI中,并行程序由多个进程组成,这些进程共同构成一个通信域(communicator)。通信域定义了进程之间的通信关系。MPI_COMM_WORLD是一个预定义的通信域,包含了程序中的所有进程。每个进程在通信域中有一个唯一的标识符,称为进程号(rank),进程号从0开始编号。
  2. 消息传递
    • MPI通过发送(send)和接收(receive)操作来实现进程间的消息传递。发送操作将数据从一个进程发送到另一个进程,接收操作则从其他进程接收数据。消息传递需要指定发送方和接收方的进程号,以及传递的数据内容。
  3. 同步
    • 同步操作确保不同进程在执行到某一特定点时,都已经完成了必要的计算和通信操作。MPI提供了多种同步函数,如MPI_Barrier,它使得所有进程在该函数处等待,直到所有进程都到达该点,然后再继续执行后续代码。

五、Fortran中MPI编程基础

  1. 包含MPI模块 在Fortran程序中使用MPI,首先需要包含MPI模块。在Fortran 90及以上版本中,可以使用以下语句:
use mpi
  1. 初始化与结束MPI环境
    • 初始化MPI环境需要调用MPI_Init函数,该函数的语法如下:
call MPI_Init(ierr)
- 其中,ierr是一个整数变量,用于返回MPI函数的错误代码。如果ierr为0,表示函数调用成功。
- 在程序结束时,需要调用MPI_Finalize函数来结束MPI环境:
call MPI_Finalize(ierr)
  1. 获取进程号和进程总数
    • 要获取当前进程的进程号,可以使用MPI_Comm_rank函数:
call MPI_Comm_rank(MPI_COMM_WORLD, myrank, ierr)
- 其中,myrank是一个整数变量,用于存储当前进程的进程号。
- 获取通信域中的进程总数,可以使用MPI_Comm_size函数:
call MPI_Comm_size(MPI_COMM_WORLD, numprocs, ierr)
- 这里,numprocs是一个整数变量,用于存储进程总数。

六、MPI消息传递函数

  1. 阻塞式发送与接收
    • 阻塞式发送:MPI_Send函数用于阻塞式发送消息,其语法如下:
call MPI_Send(buf, count, datatype, dest, tag, comm, ierr)
- 其中,buf是要发送的数据缓冲区;count是要发送的数据项数量;datatype是数据类型,如MPI_INTEGER、MPI_DOUBLE_PRECISION等;dest是接收方的进程号;tag是消息标签,用于区分不同类型的消息;comm是通信域,通常为MPI_COMM_WORLD。
- **阻塞式接收**:MPI_Recv函数用于阻塞式接收消息,语法如下:
call MPI_Recv(buf, count, datatype, source, tag, comm, status, ierr)
- 与发送函数类似,buf、count、datatype、tag、comm的含义相同。source是发送方的进程号,status是一个MPI_Status类型的变量,用于返回接收消息的状态信息,如实际接收到的数据项数量等。

2. 非阻塞式发送与接收 - 非阻塞式发送:MPI_Isend函数用于非阻塞式发送消息,语法如下:

call MPI_Isend(buf, count, datatype, dest, tag, comm, request, ierr)
- 这里的request是一个MPI_Request类型的变量,用于标识这个非阻塞操作。非阻塞式发送不会等待消息发送完成,而是立即返回,程序可以继续执行其他操作。
- **非阻塞式接收**:MPI_Irecv函数用于非阻塞式接收消息,语法如下:
call MPI_Irecv(buf, count, datatype, source, tag, comm, request, ierr)
- 同样,需要使用MPI_Wait或MPI_Test函数来等待非阻塞操作完成。MPI_Wait函数会阻塞当前进程,直到指定的非阻塞操作完成,语法如下:
call MPI_Wait(request, status, ierr)
- MPI_Test函数则不会阻塞,它会检查指定的非阻塞操作是否完成,如果完成则返回完成状态,语法如下:
logical :: flag
call MPI_Test(request, flag, status, ierr)
- 如果flag为.true.,表示非阻塞操作已经完成。

七、MPI集体通信操作

  1. 广播(Broadcast)
    • 广播操作是将一个进程(通常是进程号为0的根进程)的数据发送到通信域中的所有其他进程。在Fortran中,可以使用MPI_Bcast函数实现广播,语法如下:
call MPI_Bcast(buf, count, datatype, root, comm, ierr)
- 其中,buf是要广播的数据缓冲区,count和datatype分别是数据项数量和数据类型,root是根进程的进程号,comm是通信域。

2. 归约(Reduce) - 归约操作是将每个进程中的数据进行某种运算(如求和、求最大值等),并将结果存储在一个指定的进程(通常是根进程)中。以求和为例,使用MPI_Reduce函数,语法如下:

call MPI_Reduce(sendbuf, recvbuf, count, datatype, op, root, comm, ierr)
- sendbuf是当前进程要发送的数据缓冲区,recvbuf是根进程用于接收结果的缓冲区(对于非根进程,该参数无意义),count和datatype分别是数据项数量和数据类型,op是归约操作类型,如MPI_SUM表示求和,root是根进程的进程号,comm是通信域。

3. 散射(Scatter) - 散射操作是将根进程中的数据分散到通信域中的各个进程。使用MPI_Scatter函数,语法如下:

call MPI_Scatter(sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, root, comm, ierr)
- sendbuf是根进程中要发送的数据缓冲区,sendcount是根进程发送给每个进程的数据项数量,sendtype是发送数据的类型,recvbuf是当前进程用于接收数据的缓冲区,recvcount是当前进程接收的数据项数量,recvtype是接收数据的类型,root是根进程的进程号,comm是通信域。

4. 收集(Gather) - 收集操作是将各个进程中的数据收集到根进程中。使用MPI_Gather函数,语法如下:

call MPI_Gather(sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, root, comm, ierr)
- sendbuf是当前进程要发送的数据缓冲区,sendcount是当前进程发送的数据项数量,sendtype是发送数据的类型,recvbuf是根进程用于接收数据的缓冲区,recvcount是根进程从每个进程接收的数据项数量,recvtype是接收数据的类型,root是根进程的进程号,comm是通信域。

八、Fortran MPI并行计算代码示例

  1. 简单的Hello World示例
program mpi_hello_world
    use mpi
    implicit none
    integer :: ierr, myrank, numprocs
    call MPI_Init(ierr)
    call MPI_Comm_rank(MPI_COMM_WORLD, myrank, ierr)
    call MPI_Comm_size(MPI_COMM_WORLD, numprocs, ierr)
    write(*,*) 'Hello from process ', myrank,'out of ', numprocs
    call MPI_Finalize(ierr)
end program mpi_hello_world
- 在这个示例中,每个进程都输出一条包含自己进程号和总进程数的消息。首先通过MPI_Init初始化MPI环境,然后获取当前进程的进程号和总进程数,最后输出消息并通过MPI_Finalize结束MPI环境。

2. 计算数组元素之和的并行示例

program mpi_sum_array
    use mpi
    implicit none
    integer, parameter :: n = 1000000
    real(8) :: local_sum, global_sum
    real(8), allocatable :: local_array(:)
    integer :: i, myrank, numprocs, ierr
    call MPI_Init(ierr)
    call MPI_Comm_rank(MPI_COMM_WORLD, myrank, ierr)
    call MPI_Comm_size(MPI_COMM_WORLD, numprocs, ierr)
    allocate(local_array(n / numprocs))
    do i = 1, n / numprocs
        local_array(i) = real(myrank * (n / numprocs)+i)
    end do
    local_sum = sum(local_array)
    call MPI_Reduce(local_sum, global_sum, 1, MPI_DOUBLE_PRECISION, MPI_SUM, 0, MPI_COMM_WORLD, ierr)
    if (myrank == 0) then
        write(*,*) 'The sum of the array is ', global_sum
    end if
    deallocate(local_array)
    call MPI_Finalize(ierr)
end program mpi_sum_array
- 在这个示例中,每个进程生成一部分数组元素并计算其局部和。然后使用MPI_Reduce函数将所有进程的局部和相加,得到数组所有元素的总和。根进程(进程号为0)输出最终的总和。

3. 矩阵乘法的并行示例

program mpi_matrix_multiply
    use mpi
    implicit none
    integer, parameter :: m = 1000, n = 1000, p = 1000
    real(8), allocatable :: a(:, :), b(:, :), c(:, :)
    real(8), allocatable :: local_a(:, :), local_b(:, :), local_c(:, :)
    integer :: i, j, k, myrank, numprocs, ierr
    integer :: rows_per_proc, extra_rows
    call MPI_Init(ierr)
    call MPI_Comm_rank(MPI_COMM_WORLD, myrank, ierr)
    call MPI_Comm_size(MPI_COMM_WORLD, numprocs, ierr)
    rows_per_proc = m / numprocs
    extra_rows = m - rows_per_proc * numprocs
    if (myrank < extra_rows) then
        allocate(local_a(rows_per_proc + 1, n), local_b(n, p), local_c(rows_per_proc + 1, p))
    else
        allocate(local_a(rows_per_proc, n), local_b(n, p), local_c(rows_per_proc, p))
    end if
    if (myrank == 0) then
        allocate(a(m, n), b(n, p), c(m, p))
        do i = 1, m
            do j = 1, n
                a(i, j) = real(i + j)
            end do
        end do
        do i = 1, n
            do j = 1, p
                b(i, j) = real(i - j)
            end do
        end do
        call MPI_Scatter(a, rows_per_proc * n + (myrank < extra_rows) * n, MPI_DOUBLE_PRECISION, &
                         local_a, rows_per_proc * n + (myrank < extra_rows) * n, MPI_DOUBLE_PRECISION, &
                         0, MPI_COMM_WORLD, ierr)
        call MPI_Bcast(b, n * p, MPI_DOUBLE_PRECISION, 0, MPI_COMM_WORLD, ierr)
    else
        call MPI_Scatter(a, rows_per_proc * n + (myrank < extra_rows) * n, MPI_DOUBLE_PRECISION, &
                         local_a, rows_per_proc * n + (myrank < extra_rows) * n, MPI_DOUBLE_PRECISION, &
                         0, MPI_COMM_WORLD, ierr)
        call MPI_Bcast(b, n * p, MPI_DOUBLE_PRECISION, 0, MPI_COMM_WORLD, ierr)
    end if
    do i = 1, size(local_a, 1)
        do j = 1, p
            local_c(i, j) = 0.0d0
            do k = 1, n
                local_c(i, j) = local_c(i, j)+local_a(i, k) * local_b(k, j)
            end do
        end do
    end do
    if (myrank == 0) then
        call MPI_Gather(local_c, rows_per_proc * p + (myrank < extra_rows) * p, MPI_DOUBLE_PRECISION, &
                        c, rows_per_proc * p + (myrank < extra_rows) * p, MPI_DOUBLE_PRECISION, &
                        0, MPI_COMM_WORLD, ierr)
    else
        call MPI_Gather(local_c, rows_per_proc * p + (myrank < extra_rows) * p, MPI_DOUBLE_PRECISION, &
                        c, rows_per_proc * p + (myrank < extra_rows) * p, MPI_DOUBLE_PRECISION, &
                        0, MPI_COMM_WORLD, ierr)
    end if
    if (myrank == 0) then
        do i = 1, m
            do j = 1, p
                write(*, '(F8.2)', advance = 'no') c(i, j)
            end do
            write(*, *)
        end do
        deallocate(a, b, c)
    end if
    deallocate(local_a, local_b, local_c)
    call MPI_Finalize(ierr)
end program mpi_matrix_multiply
- 在这个矩阵乘法的并行示例中,根进程首先初始化两个矩阵`a`和`b`。然后通过MPI_Scatter将矩阵`a`的部分行分散到各个进程,通过MPI_Bcast将矩阵`b`广播到所有进程。每个进程计算自己负责的部分矩阵乘法结果,最后通过MPI_Gather将所有进程的结果收集到根进程。根进程输出最终的矩阵乘法结果。

九、MPI并行程序的调试与优化

  1. 调试
    • 错误处理:在MPI程序中,每个MPI函数调用都会返回一个错误代码(ierr)。通过检查这些错误代码,可以确定函数调用是否成功。例如:
call MPI_Send(buf, count, datatype, dest, tag, comm, ierr)
if (ierr.ne. 0) then
    write(*,*) 'MPI_Send error: ', ierr
end if
- **调试工具**:可以使用调试工具如gdb和mpirun - debug选项来调试MPI程序。gdb可以设置断点、查看变量值等,帮助定位程序中的错误。例如:
mpirun - debug - n 4 gdb./a.out
  1. 优化
    • 减少通信开销:尽量减少进程间的通信次数和数据量。例如,在进行多次小数据量的通信时,可以考虑合并为一次大数据量的通信。
    • 负载均衡:确保每个进程的计算负载均匀,避免某些进程任务过重,而其他进程空闲。在分配计算任务时,可以根据进程数量和任务规模进行合理划分。
    • 使用非阻塞通信:在一些情况下,使用非阻塞通信可以让进程在等待通信完成的同时执行其他计算任务,提高程序的整体效率。

十、MPI高级主题

  1. MPI拓扑结构
    • MPI支持多种拓扑结构,如笛卡尔拓扑(Cartesian topology)和图拓扑(Graph topology)。笛卡尔拓扑可以用于描述多维网格结构,通过MPI_Cart_create函数创建。例如:
integer :: dims(2), periods(2), reorder
dims = [10, 10]
periods = [.true.,.true.]
reorder =.true.
call MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, reorder, newcomm, ierr)
- 这里创建了一个二维的周期笛卡尔拓扑,每个维度有10个进程。图拓扑则更灵活,可以根据具体的通信需求定义进程之间的连接关系,通过MPI_Graph_create函数创建。

2. MPI与共享内存结合 - 在一些多核处理器系统中,可以结合MPI和共享内存(如OpenMP)进行混合编程。MPI用于节点间的通信,OpenMP用于多核间的共享内存并行。这样可以充分利用不同层次的并行资源,提高程序的性能。例如:

program mpi_openmp_hybrid
    use mpi
    implicit none
    integer :: ierr, myrank, numprocs
    call MPI_Init(ierr)
    call MPI_Comm_rank(MPI_COMM_WORLD, myrank, ierr)
    call MPI_Comm_size(MPI_COMM_WORLD, numprocs, ierr)
!$OMP PARALLEL
    write(*,*) 'Hello from thread ', omp_get_thread_num(),'in process ', myrank
!$OMP END PARALLEL
    call MPI_Finalize(ierr)
end program mpi_openmp_hybrid
- 在这个示例中,每个MPI进程又通过OpenMP创建了多个线程,实现了混合并行编程。

通过以上内容,你已经对Fortran MPI并行计算有了一个较为全面的了解,从MPI的基本概念、编程基础,到具体的消息传递和集体通信操作,以及代码示例、调试优化和一些高级主题。希望这些知识能帮助你在科学计算和工程模拟等领域开发高效的并行程序。