奇偶融合排序的mpi4py实现

目录

算法介绍

算法实现

局部排序

排序合并

奇偶融合

运行结果


算法介绍

冒泡排序:是串行算法,在每次迭代过程中,每个元素可能与前面元素交换,也可能和后面的元素交换,数据的相关性比较强很难直接改成并行算法。

奇偶排序:或奇偶换位排序,或砖排序,是一种相对简单的排序算法,最初发明用于有本地互连的并行计算。这是与冒泡排序特点类似的一种比较排序。该算法中,排序过程分两个阶段,奇交换和偶交换,两种交换都是成对出现。对于奇交换,它总是比较奇数索引以及其相邻的后续元素。对偶交换,总是比较偶数索引和其相邻的后续元素。

可看出整个比较交换分为独立的奇阶段或偶阶段。在每个阶段内,所有的比较和交换没有数据相关性。因此,每一次比较和交换都可独立执行,也就可以并行化。

算法实现

import copy
import random
import time
import numpy as np
from mpi4py import MPI

N = 10000  # 待排元素个数
INF = 999999999  # 设置一个无穷大值
masterNode = 0  # 设置主进程标号
global myid  # 进程标号
global part  # 每个进程分配的元素个数
global sort  # 用来表示排序是否完成的逻辑量

局部排序

这个阶段可以使用任意传统的排序算法,如冒泡排序,快速排序,局部奇偶排序等等,本文使用最简单的冒泡排序。

def odd_even_sort(array, num):  # 冒泡排序
    for i in range(num - 1):
        isNotchange = True
        for j in range(num - i - 1):
            # print(array[j], j)
            if array[j] > array[j + 1]:
                array[j], array[j + 1] = array[j + 1], array[j]
                isNotchange = False
        if isNotchange == True:
            break


# def odd_even_sort1(p, num):  # 局部奇偶排序
#     sort = 0
#     while not sort:  # 一次奇数排序和一次偶数排序均未发生数据交换时排序完成
#         sort = 1
#         for i in range(1, num, 2):
#             if p[i] < p[i - 1]:
#                 swap(p, i, i - 1)
#                 sort = 0
#         for i in range(2, num, 2):
#             if p[i] < p[i - 1]:
#                 swap(p, i, i - 1)
#                 sort = 0

排序合并

def mergeSort(data, buffer, tmp):  # 两块排好序的元素data和buffer进行排序合并
    i = 0
    j = 0
    k = 0
    while i != part and j != part:
        if data[i] <= buffer[j]:
            tmp[k] = data[i]
            k = k + 1
            i = i + 1
        else:
            tmp[k] = buffer[j]
            k = k + 1
            j = j + 1
    if j != 0:
        sort = 0
    if i != part:
        while k != 2 * part:
            tmp[k] = data[i]
            k = k + 1
            i = i + 1
    if j != part:
        while k != 2 * part:
            tmp[k] = buffer[j]
            k = k + 1
            j = j + 1
    data = copy.deepcopy(tmp)
    buffer = copy.deepcopy(tmp + part)

奇偶融合

值得注意的是,程序实际运行时,每个进程存储的数据(data)并不是像文章开头的示意图一样,只保存固定个数的数据,而是随着奇偶融合的不断进行,逐渐包括进程编号相邻的进程数据。比如图中的1号进程,在一次奇偶融合排序后,实际上存储了0、1、2、3号进程的有序数据。

存在严格的定理可证明,如果有n个数进行奇偶排序,至多进行n次排序,数据则有序;对于MPI并行算法,类比可得,如果有n个进程进行奇偶排序,则同样至多进行n次排序(一次排序指的是一次奇排序加一次偶排序)

但事实上,可设置标志位,若一次排序中,没有发生数据交换,则可以跳出循环,不用执行n步,本文的程序实现正是这种思路。

def parallelMergeSort(data):  # 奇偶融合
    left = myid - 1
    right = myid + 1
    # 边缘进程的特殊处理
    if left < 0:
        left = None
    if right > numprocs - 1:
        right = None
    # buffer存放即将与本地data合并的数据
    buffer = np.empty(part)
    # tmp存放buffer和data合并后的数据,
    # 实际上也可以在mergeSort函数内创建,
    # 在此处创建tmp是出于程序性能的考虑
    tmp = np.empty(part * 2)
    sort = 0
    flag = 0
    # 奇偶融合重复多次直到所有合并都不需要进行排序
    while not flag:
        sort = 1

        # 奇排序
        if myid % 2 == 1:
            comm.send(data, dest=left)
            data = comm.recv(source=left)
        elif right is not None:
            buffer = comm.recv(source=right)
            mergeSort(data, buffer, tmp)
            comm.send(buffer, dest=right)

        # 偶排序
        if myid % 2 == 0 and myid != 0:
            comm.send(data, dest=left)
            data = comm.recv(source=left)
        elif right is not None and myid != 0:
            buffer = comm.recv(source=right)
            mergeSort(data, buffer, tmp)
            comm.send(buffer, dest=right)

        # 全规约函数,对每个进程中的sort标识求逻辑与,结果保存在flag
        # flag为真表明全局有序
        flag = comm.allreduce(sort, op=MPI.LAND)

主程序

if __name__ == '__main__':
    comm = MPI.COMM_WORLD
    myid = comm.Get_rank()
    numprocs = comm.Get_size()
    memoryPool = None
    if myid == masterNode:
        # 若numpros不能整除N则需加上padding个无穷大的元素使能够整除
        padding = numprocs - N % numprocs
        memoryPool = np.empty(N + padding)
        for i in range(0, N):
            memoryPool[i] = (random.random()) * 100
        if numprocs == 1:
            # 单进程程序入口
            time_start = time.time()  # 串行开始计时
            odd_even_sort(memoryPool, N)
            time_end = time.time()  # 串行停止计时
            print('单个进程对 ', N, "个随机数进行冒泡排序,总时间:", time_end - time_start, "秒")
            # 数据少的时候可以输出看结果
            # for i in range(0, N - 1):
            #     print(memoryPool[i])
            exit(0)
            # 单进程程序出口
        for i in range(0, padding):
            memoryPool[N + i] = INF
        part = int((N + padding) / numprocs)
    # 并行开始计时
    time_start = time.time()
    part = comm.bcast(part if myid == masterNode else None, root=masterNode)  # 广播part
    data = np.empty(part)
    comm.Scatter(memoryPool, data, root=masterNode)  # 主进程向各进程分发数据
    odd_even_sort(data, part)  # 局部排序
    print(myid, '号进程已完成对', part, '个元素的局部排序')
    parallelMergeSort(data)  # 奇偶融合
    memoryPool = comm.gather(data, root=masterNode)  # 将全局有序的数据收集到主进程
    time_end = time.time()
    # 并行停止计时
    if myid == masterNode:
        # 数据少的时候可以输出看结果
        # for i in range(0, N - 1):
        #     print(memoryPool[i])
        time.sleep(1)
        print('调用', numprocs, '个进程对', N, "个随机数进行奇偶融合排序,总时间:", time_end - time_start, "秒")

运行结果

直接用pycharm运行程序,相当于进程数设置为1

 在命令行中使用mpich2运行(cd 到个人的py文件所在目录,最后一个参数修改为py文件名)

 增大问题规模和进程数量

 小Tips:线程数量受限与设备的内存大小,我的笔记本电脑是8g+8g的内存条,最高可以设置进程数量为80,超过会报错显示内存不足。

本文参考了:

MPI并行计算学习笔记4——奇偶排序的优雅实现_askjff的博客-CSDN博客_mpi奇偶排序

奇偶排序 - milkty - 博客园

猜你喜欢

转载自blog.csdn.net/EXYlkx/article/details/122634540