CUDA编程-06

MPI

MPI环境配置(WSL Ubuntu20.04)

  1. 下载源码并解压

wget http://www.mpich.org/static/downloads/3.4.2/mpich-3.4.2.tar.gz
tar -xvzf mpich-3.4.2.tar.gz
  1. 创建安装文件夹

mkdir mpich-install
  1. 配置并编译

cd mpich-3.4.2
./configure -prefix=$HOME/mpich-install  --with-device=ch3 --disable-fortran
make -j8 && make install -j8
  1. 配置环境变量

#返回首页
cd ../
#打开bashrc文件
vim .bashrc
#mpi config
#mpi config
export MPI_ROOT=$HOME/mpich-install
export PATH=$MPI_ROOT/bin:$PATH
export MANPATH=$MPI_ROOT/man:$MANPATH
  1. 验证安装

mpicc --version
  1. 测试案例

#include <mpi.h>
#include <stdio.h>
​
int main(int argc, char** argv) {
    MPI_Init(NULL, NULL);
​
    int world_size;
    MPI_Comm_size(MPI_COMM_WORLD, &world_size);
​
    int world_rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
​
    printf("Hello world from processor %d of %d\n", world_rank, world_size);
​
    MPI_Finalize();
    return 0;
}
  1. 编译运行

//编译程序
mpicc -o hello_mpi hello_mpi.c
//运行程序
mpirun -np 4 ./hello_mpi

MPI简介

🌟概念:

  • MPI(Message Passing Interface)是一个消息传递接口标准

  • MPI提供一个可移植、高效、灵活的消息传递接口库

  • MPI以语言独立的形式存在,可运行在不同的操作系统和硬件平台上

  • MPI提供与C/C++和Fortran语言的保定

⭐️MPI版本:

  1. MPICH:MPICH 是由美国阿贡国家实验室(Argonne National Laboratory)开发的一种高性能且可扩展的 MPI 实现。MPICH 被广泛用于研究和高性能计算环境,并且作为其他许多 MPI 实现的基础。

  2. LAM/MPI (Local Area Multicomputer):LAM/MPI 是一种适用于局域网环境的 MPI 实现,最初由俄亥俄州立大学开发。虽然 LAM/MPI 在其活跃时期是一个流行的选择,但由于项目不再维护,许多用户已经转向其他更现代的 MPI 实现。

  3. Open MPI:Open MPI 是一个开源的、高性能的 MPI 实现,由多家研究机构和企业合作开发。Open MPI 结合了许多前期 MPI 项目的最佳特性,并得到了广泛的社区支持。

🖊基本思想:MPI(Message Passing Interface)编程的基本思想是通过消息传递实现并行计算,其中“对等式”(peer-to-peer)是一个重要的概念。在对等式模型中,每个进程都是平等的,具有相同的能力和权限。这与主从模型不同,在主从模型中有一个主节点和多个从节点。

🌟详解:MPI通常采用对等设计,所有节点在理论上是平等的,可以互相通信。虽然没有固定的主从节点,但程序可以根据需要给予某个节点特殊角色,每个进程有唯一的标识符(rank),从0到N-1(N是总进程数),rank 0的进程常被用来执行初始化、结果收集等特殊任务。MPI通常遵循Single Program, Multiple Data (SPMD)模型,同一程序在所有节点上运行,但处理不同的数据集。MPI通信方式包括点对点或集体通信,点对点通信为任意两个进程之间可以直接交换数据,集体通信为涉及一组或所有进程的通信操作。

MPI命令

编译与执行命令

🌟mpicc:mpicc 是 MPI 编译器命令,用于编译 C 语言的 MPI 程序。它通常会调用底层的 C 编译器(如 gcc),并自动链接 MPI 库。

mpicc -o myapp myapp.c

🖊详解:

mpicc: 调用 MPI 编译器。

-o myapp: 指定输出可执行文件名为 myapp

myapp.c: 源文件名。

🌟mpiexec:mpiexec 是 MPI 执行命令,用于启动 MPI 程序。它允许用户指定要运行的进程数量以及在哪些机器上运行这些进程。

mpiexec -machinefile ./machinefile ./myapp

🖊详解:

mpiexec: 调用 MPI 执行器。

-machinefile ./machinefile: 指定一个机器文件(machinefile),该文件列出了要运行 MPI 进程的机器列表。

./myapp: 要执行的 MPI 程序。

🔴机器文件(machinefile):机器文件是一个文本文件,列出所有要参与运行 MPI 程序的机器或节点。每行通常代表一个机器,可以包含机器名称或 IP 地址,并且可以指定每个机器上的进程数量。

//示例 machinefile:
node1 slots=4
node2 slots=4
node3 slots=4

node1, node2, node3:参与计算的机器名或 IP 地址。

slots=4: 每个机器上可运行的进程数量。

⭐️PS:没有 machinefile 的情况默认本地执行,如果没有指定 machinefile 或者主机列表,mpiexec 默认在本地机器上启动所有的进程。也就是说,所有的进程会在同一台机器的不同核心上运

MPI子集

🌟概念:#include <mpi.h> MPI头函数,提供了MPI函数和数据类型定义

🌟六个基本函数:

  • MPI初始化:通过MPI_Init函数进入MPI环境并完成所有的初始化工作。
//初始化 MPI 环境。必须在调用任何其他 MPI 函数之前调用。
int MPI_Init(int *argc, char ***argv);
//argc: 指向命令行参数个数的指针。
//argv: 指向命令行参数数组的指针。
MPI_Init(&argc, &argv);
  • MPI结束:通过MPI_Finalize函数从MPI环境中退出。

//终止 MPI 环境。所有 MPI 程序必须在结束时调用此函数。
int MPI_Finalize(void);
MPI_Finalize();
  • 获取指定通信域的进程数:调用MPI_Comm_size 函数获取指定通信域的进程个数,确定自身完成任务比例。

//获取指定通信器中的进程总数。
int MPI_Comm_size(MPI_Comm comm, int *size);
//comm: 通信器(通常为 MPI_COMM_WORLD)。
//size: 指向存储进程总数的变量的指针。
int world_size;
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
  • 获取进程的编号:调用MPI_Comm_rank函数获得当前进程在指定通信域中的编号,将自身与其他程序区分。

//获取指定通信器中当前进程的排名(ID)。
int MPI_Comm_rank(MPI_Comm comm, int *rank);
//comm: 通信器(通常为 MPI_COMM_WORLD)。
//rank: 指向存储当前进程排名的变量的指针。
int world_rank;
MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
  • 消息发送:MPI_Send函数用于发送一个消息到目标进程。

//向指定进程发送消息。
int MPI_Send(const void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm);
//buf: 指向要发送的数据的指针。
//count: 发送的数据个数。
//datatype: 数据类型(如 MPI_INT)。
//dest: 目标进程的排名(ID)。
//tag: 消息标签。
//comm: 通信器(通常为 MPI_COMM_WORLD)。
int message = 123;
MPI_Send(&message, 1, MPI_INT, dest_rank, 0, MPI_COMM_WORLD);
  • 消息接受:MPI_Recv函数用于从指定进程接收一个消息

//从指定进程接收消息。
int MPI_Recv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status);
//buf: 指向存储接收数据的缓冲区的指针。
//count: 接收的数据个数,以datatype为单位,必须连续。
//datatype: 数据类型(如 MPI_INT)。
//source: 源进程的排名(ID)。
//tag: 消息标签。
//comm: 通信器(通常为 MPI_COMM_WORLD)。
//status: 指向存储接收状态信息的变量的指针。
int received_message;
MPI_Recv(&received_message, 1, MPI_INT, source_rank, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);

🔴通信域(通信子):在 MPI(Message Passing Interface)编程中,通信域(communicator)是一个关键的概念。通信域定义了一组可以相互通信的进程,并且每个通信域都有一个唯一的标识符和进程的本地编号系统。MPI_COMM_WORLD:默认的通信域,包含所有的 MPI 进程。

🔴MPI消息发送机制--两步进行

  • MPI_Send( A, … ) 发送

  • MPI_Recv( B, … ) 接收

发/收 两步机制:避免直接读写对方内存,保证安全性。

测试样例

#include <mpi.h>
#include <stdio.h>
​
int main(int argc, char** argv) {
    // 初始化 MPI 环境
    MPI_Init(&argc, &argv);
​
    // 获取进程总数
    int world_size;
    MPI_Comm_size(MPI_COMM_WORLD, &world_size);
​
    // 获取当前进程的排名
    int world_rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);
​
    // 进程 0 向所有其他进程发送消息
    if (world_rank == 0) {
        int message = 123;
        for (int i = 1; i < world_size; i++) {
            MPI_Send(&message, 1, MPI_INT, i, 0, MPI_COMM_WORLD);
        }
    } else {
        int received_message;
        MPI_Recv(&received_message, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
        printf("Process %d received message %d from process 0\n", world_rank, received_message);
    }
​
    // 终止 MPI 环境
    MPI_Finalize();
    return 0;
}
//测试指令
mpicc -o mpi_example mpi_example.c
mpirun -np 4 ./mpi_example

🌟任务案例:

计算矩阵乘积:计算矩阵 A*B=C ,A,B,C:NxN矩阵,采用P个进程计算 (N能被P整除);

存储方式:分布存储, A, C 按行分割, B 按列分割。

#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>
​
void print_matrix(float* matrix, int rows, int cols, int rank) {
    printf("Process %d:\n", rank);
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            printf("%f ", matrix[i * cols + j]);
        }
        printf("\n");
    }
    printf("\n");
}
​
int main(int argc, char** argv) {
    MPI_Init(&argc, &argv);
​
    int rank, size;
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);
​
    int N = 4; // 矩阵的维度 (假设 N 可以被 size 整除)
    int local_N = N / size;
​
    float* A = (float*) malloc(local_N * N * sizeof(float));
    float* B = (float*) malloc(local_N * N * sizeof(float));
    float* C = (float*) calloc(local_N * N, sizeof(float));
    float* B_tmp = (float*) malloc(local_N * N * sizeof(float));
​
    // 初始化矩阵 A 和 B
    for (int i = 0; i < local_N; ++i) {
        for (int j = 0; j < N; ++j) {
            A[i * N + j] = rank * local_N + i + 1;
            B[i * N + j] = (rank * local_N + i + 1) * 0.1;
        }
    }
​
    print_matrix(A, local_N, N, rank);
    print_matrix(B, local_N, N, rank);
​
    // 循环通信
    for (int step = 0; step < size; ++step) {
        int id_send = (rank + step) % size;
        int id_recv = (rank - step + size) % size;
​
        // 使用 MPI_Sendrecv 进行同步发送和接收
        MPI_Sendrecv(B, local_N * N, MPI_FLOAT, id_send, 0,
                     B_tmp, local_N * N, MPI_FLOAT, id_recv, 0,
                     MPI_COMM_WORLD, MPI_STATUS_IGNORE);
​
        // 计算部分结果并累加到 C
        for (int i = 0; i < local_N; ++i) {
            for (int j = 0; j < N; ++j) {
                for (int k = 0; k < local_N; ++k) {
                    C[i * N + j] += A[i * N + k] * B_tmp[k * N + j];
                }
            }
        }
    }
​
    print_matrix(C, local_N, N, rank);
​
    free(A);
    free(B);
    free(C);
    free(B_tmp);
​
    MPI_Finalize();
    return 0;
}

🌎PS:MPI_Sendrecv是MPI库中用于并行计算的函数,它允许进程在一次调用中同时发送和接收消息。这种方法相比于单独的发送(MPI_Send)和接收(MPI_Recv)可以避免死锁,简化代码,提高性能。

MPI + CUDA

🌟概念:不同机器间数据传输过程:

  1. 初始化和分配内存:在主机和设备上分配内存。

  2. 数据传输:在主机和设备之间传输数据。

  3. 计算:在GPU上执行计算。

  4. 数据传输回主机:将结果从设备传输回主机。

  5. 释放内存:释放主机和设备上的内存。

UVA统一虚拟寻址

🔺概念:统一虚拟寻址(Unified Virtual Addressing, UVA)是NVIDIA CUDA的一项功能,它简化了CPU和GPU之间的内存管理和数据传输。在UVA环境下,主机(CPU)和设备(GPU)共享同一个统一的虚拟地址空间,使得编程更加方便和高效。

✏️特点:

  • 简化内存管理:CPU和GPU共享统一的虚拟地址空间,无需显式管理主机和设备之间的数据传输。

  • 简化编程模型:可以直接使用指针访问数据,无需区分主机指针和设备指针。

  • 提高数据传输效率:利用统一地址空间可以优化数据传输,减少显式内存拷贝的需求。

#UVA使用案例
#include <mpi.h>
#include <cuda_runtime.h>
#include <stdio.h>
​
// CUDA核函数,执行简单的向量加法
__global__ void vectorAdd(float* A, float* B, float* C, int N) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < N) {
        C[i] = A[i] + B[i];
    }
}
​
int main(int argc, char** argv) {
    MPI_Init(&argc, &argv);
​
    int rank, size;
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);
​
    int N = 1024; // 向量的大小
    int local_N = N / size;
​
    float *h_A, *h_B, *h_C;
    float *d_A, *d_B, *d_C;
​
    // 在统一虚拟地址空间中分配主机和设备内存
    cudaMallocManaged(&h_A, N * sizeof(float));
    cudaMallocManaged(&h_B, N * sizeof(float));
    cudaMallocManaged(&h_C, N * sizeof(float));
​
    // 初始化向量 A 和 B
    for (int i = 0; i < local_N; ++i) {
        h_A[rank * local_N + i] = rank * local_N + i;
        h_B[rank * local_N + i] = (rank * local_N + i) * 0.1f;
    }
​
    // 同步进程,以确保所有进程完成初始化
    MPI_Barrier(MPI_COMM_WORLD);
​
    // 定义 CUDA 核函数的执行配置
    int threadsPerBlock = 256;
    int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
​
    // 调用 CUDA 核函数
    vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(h_A, h_B, h_C, N);
    cudaDeviceSynchronize();
​
    // 打印计算结果
    if (rank == 0) {
        for (int i = 0; i < N; ++i) {
            printf("C[%d] = %f\n", i, h_C[i]);
        }
    }
​
    // 释放内存
    cudaFree(h_A);
    cudaFree(h_B);
    cudaFree(h_C);
​
    MPI_Finalize();
    return 0;
}

▶️PS:使用cudaMallocManaged在统一虚拟地址空间中分配内存,这些内存可以被CPU和GPU共享。

  • 42
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GentlemanLin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值