当前位置:

首页 > 编程开发 > MPI4Py中异形数组的gather与gatherv操作详解

MPI4Py中异形数组的gather与gatherv操作详解

本文深入探讨了在mpi4py中使用comm.Gather处理不同形状NumPy数组时遇到的挑战,并提供了两种有效的解决方案:利用comm.gather收集通用Python对象后进行拼接,以及使用comm.Gatherv直接将不同大小的数组高效地集合到一个预分配的NumPy缓冲区中。文章将详细阐述这两种方法的实现细节、适用场景及代码示例,帮助开发者优化并行程序的集合通信效率。

mpi4py中异形NumPy数组的集合操作:gather与Gatherv详解

本文深入探讨了在mpi4py中使用`comm.Gather`处理不同形状NumPy数组时遇到的挑战,并提供了两种有效的解决方案:利用`comm.gather`收集通用Python对象后进行拼接,以及使用`comm.Gatherv`直接将不同大小的数组高效地集合到一个预分配的NumPy缓冲区中。文章将详细阐述这两种方法的实现细节、适用场景及代码示例,帮助开发者优化并行程序的集合通信效率。

在并行计算中,经常需要在各个进程(或核心)上处理数据,然后将这些分散的结果收集到根进程上进行进一步的分析或整合。mpi4py库提供了强大的MPI(Message Passing Interface)绑定,使得Python程序能够方便地进行并行化。其中,comm.Gather是一个常用的集体通信操作,用于将所有进程的相同类型和形状的数据收集到根进程的一个连续缓冲区中。

然而,当每个进程需要发送的NumPy数组形状不一致时,直接使用comm.Gather会导致程序失败,因为它期望所有发送的数据都具有相同的维度和大小。本文将介绍两种在mpi4py中有效处理不同形状NumPy数组集合操作的方法:comm.gather(小写g)和comm.Gatherv(大写G,小写v)。

1. 问题背景:comm.Gather的局限性

comm.Gather操作的本质是将所有进程的相同类型数据按顺序收集到根进程的一个预定义缓冲区中。这意味着每个发送进程的数据必须是同构的,即具有相同的形状和数据类型。

考虑以下示例,其中不同进程生成了形状不同的NumPy数组:

from mpi4py import MPI
import numpy as np

comm = MPI.COMM_WORLD
size = comm.Get_size()
rank = comm.Get_rank()

# rank 1生成(2,3)的数组,其他进程生成(5,3)的数组
a = np.zeros((2 if rank == 1 else 5, 3), dtype=float) + rank
print(f"Rank {rank}: 数组形状 {a.shape}, 数据:\n{a}")

# 尝试使用comm.Gather,这通常会失败
# b = np.zeros((12, 3), dtype=float) - 1 # 假设一个足够大的接收缓冲区
# comm.Gather(a, b, root=0)
# if rank == 0:
#     print(f"Rank {rank}: 接收到的数据:\n{b}")

运行上述代码中被注释掉的comm.Gather部分,会因为数组形状不匹配而导致运行时错误。为了解决这个问题,我们需要采用更灵活的集合通信方法。

2. 解决方案一:使用 comm.gather 收集通用Python对象

comm.gather(注意是小写g)是mpi4py中一个更通用的集合操作。它不局限于NumPy数组,可以收集任何可序列化的Python对象。当每个进程发送的NumPy数组形状不同时,comm.gather会将其作为独立的Python对象进行收集,并在根进程上返回一个包含这些对象的列表或元组。随后,我们可以使用numpy.concatenate将这些数组拼接起来。

2.1 实现细节

  1. 发送阶段: 每个进程将自己的NumPy数组a作为独立的Python对象发送。
  2. 接收阶段: 根进程接收所有发送的数组,并将它们存储在一个Python列表(或元组)中。
  3. 后处理: 根进程使用np.concatenate()函数将列表中的所有NumPy数组沿指定轴拼接成一个大的NumPy数组。

2.2 代码示例

import numpy as np
from mpi4py import MPI

comm = MPI.COMM_WORLD
size = comm.Get_size()
rank = comm.Get_rank()

# rank 1生成(2,3)的数组,其他进程生成(5,3)的数组
a = np.zeros((2 if rank == 1 else 5, 3), dtype=float) + rank
print(f"Rank {rank}: 数组形状 {a.shape}, 数据:\n{a}")

# 使用comm.gather收集不同形状的数组
# 根进程会收到一个包含所有数组的列表
gathered_arrays = comm.gather(a, root=0)

if rank == 0:
    print(f"\nRank {rank}: 原始收集到的数据 (列表形式):\n{gathered_arrays}")
    # 将收集到的数组列表拼接成一个大数组
    concatenated_array = np.concatenate(gathered_arrays, axis=0) # 沿0轴拼接
    print(f"\nRank {rank}: 拼接后的数据形状 {concatenated_array.shape}, 数据:\n{concatenated_array}")
else:
    # 非根进程的gathered_arrays为None
    print(f"Rank {rank}: 非根进程,gathered_arrays为 {gathered_arrays}")

2.3 适用场景与注意事项

  • 优点: 实现简单,代码直观,无需预先计算每个进程发送的数据大小和偏移量。适用于数据量不是特别巨大,或者需要灵活处理不同类型对象的场景。
  • 缺点: 涉及到Python对象的序列化和反序列化,以及后续的np.concatenate操作,可能会引入额外的性能开销,尤其是在数据量非常大时。此外,根进程需要足够的内存来存储所有单独的数组,然后再进行拼接。

3. 解决方案二:使用 comm.Gatherv 直接集合到NumPy数组

comm.Gatherv(注意是Gatherv)是comm.Gather的变体,专门设计用于处理每个进程发送数据大小不同的情况。它允许将来自不同进程的、大小不一的数据直接集合到根进程的一个预分配的NumPy数组中,而无需中间的Python对象列表和后续的拼接操作。这通常在性能要求较高的场景下更为高效。

3.1 comm.Gatherv的接收缓冲区参数

comm.Gatherv的接收缓冲区参数比comm.Gather复杂,它是一个元组,通常格式为 (recvbuf, recvcounts, displs, recvtype):

  • recvbuf:根进程上的目标NumPy数组,必须预先分配好,且大小足以容纳所有进程发送的数据。
  • recvcounts:一个列表或NumPy数组,长度等于进程总数。每个元素表示从对应进程接收的元素数量(不是字节数)。
  • displs:一个列表或NumPy数组,长度等于进程总数。每个元素表示从对应进程接收的数据在recvbuf中的起始元素偏移量(不是字节偏移量)。
  • recvtype:接收数据的MPI数据类型(例如MPI.DOUBLE对应float64,MPI.INT对应int32)。

3.2 实现细节

  1. 预计算: 在所有进程上(或至少在根进程上),需要预先计算好每个进程将发送的元素数量 (recvcounts) 以及这些数据在根进程的接收缓冲区中的起始偏移量 (displs)。
  2. 根进程预分配: 根进程需要预先分配一个足够大的NumPy数组作为recvbuf。
  3. 调用 comm.Gatherv: 所有进程调用comm.Gatherv,并由根进程提供接收缓冲区的详细信息。

3.3 代码示例

为了简化recvcounts和displs的计算,以下示例假设只有两个进程(size <= 2),但在实际应用中,这些参数通常需要通过comm.allgather或comm.gather先收集每个进程的形状信息来动态计算。

import numpy as np
from mpi4py import MPI

comm = MPI.COMM_WORLD
size = comm.Get_size()
rank = comm.Get_rank()

# 示例限制为两个进程,以便手动设置recvcounts和displs
assert size <= 2, "此Gatherv示例仅适用于2个或更少的进程"

if rank == 0:
    a = np.zeros((5, 3), dtype=float) + rank
else: # rank == 1
    a = np.zeros((2, 3), dtype=float) + rank

print(f"Rank {rank}: 数组形状 {a.shape}, 数据:\n{a}")

# 定义全局总行数 (5来自rank 0, 2来自rank 1)
n_global_rows = 7
# 定义每行元素数
cols = a.shape[1]

# 根进程需要预分配接收缓冲区
if rank == 0:
    b = np.zeros((n_global_rows, cols), dtype=float)
    # 计算每个进程发送的元素数量
    # rank 0: 5行 * 3列 = 15个元素
    # rank 1: 2行 * 3列 = 6个元素
    recvcounts = [5 * cols, 2 * cols] # 对应每个进程的元素总数

    # 计算每个进程数据在b中的起始偏移量 (元素偏移量)
    # rank 0: 从b的0偏移量开始
    # rank 1: 从b的第15个元素 (即第5行3列后) 开始
    displs = [0, 5 * cols] 

    # 组合Gatherv的接收缓冲区参数
    recvbuf_params = (b, recvcounts, displs, MPI.DOUBLE)
else:
    b = None
    recvbuf_params = None # 非根进程不需要提供接收缓冲区参数

# 执行Gatherv操作
comm.Gatherv(a, recvbuf_params, root=0)

if rank == 0:
    print(f"\nRank {rank}: Gatherv接收到的数据形状 {b.shape}, 数据:\n{b}")
else:
    print(f"Rank {rank}: 非根进程,b为 {b}")

3.4 适用场景与注意事项

  • 优点: 效率高,数据直接传输到预分配的NumPy数组,避免了Python对象的序列化/反序列化和后续拼接的开销。适用于处理大规模NumPy数组,对性能要求高的场景。
  • 缺点: 实现相对复杂,需要精确计算recvcounts和displs。在实际应用中,通常需要先进行一次comm.gather或comm.allgather操作来收集所有进程的数组形状信息,然后根进程根据这些信息计算recvcounts和displs。
  • 数据类型匹配: recvtype参数必须与NumPy数组的dtype精确匹配。MPI.DOUBLE对应np.float64,MPI.FLOAT对应np.float32,MPI.INT对应np.int32等。
  • 元素计数与偏移: recvcounts和displs中的值都是元素数量,而不是字节数。例如,一个形状为(5, 3)的数组,其元素数量是15。

4. 总结与选择建议

当需要在mpi4py中将不同形状的NumPy数组收集到根进程时:

  • comm.gather (小写g):

    • 适用场景: 数据量相对较小,对代码简洁性要求更高,或者需要收集的不仅仅是NumPy数组,而是各种Python对象。
    • 优点: 简单易用,无需复杂的参数计算。
    • 缺点: 性能开销可能较高,需要额外的np.concatenate步骤。
  • comm.Gatherv (大写G,小写v):

    • 适用场景: 处理大规模NumPy数组,对性能有严格要求,且可以预先计算出每个进程发送的数据大小和偏移量。
    • 优点: 效率高,直接将数据写入预分配的缓冲区。
    • 缺点: 实现复杂,需要精确计算recvcounts和displs。

在实际开发中,应根据具体的应用需求(数据规模、性能要求、代码复杂度等)权衡选择合适的方法。对于大多数情况,如果性能不是极致瓶颈,comm.gather配合np.concatenate是一个简单有效的方案。而对于高性能计算场景,comm.Gatherv则是更专业的选择。

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发
相关文章 更多
using namespace 使用中遇到的问题怎么解决
using namespace 使用中遇到的问题怎么解决

命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

c语言函数递归 实操经验总结:这些技巧很实用
c语言函数递归 实操经验总结:这些技巧很实用

理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

c语言函数递归 怎么选?常见方案对比分析
c语言函数递归 怎么选?常见方案对比分析

递归函数的基本概念与适用场景在C语言编程中,递归是一种函数调用自身的编程技巧。它并非适用于所有问题,但在处理某些具有自相似结构的问题时,能提供极其清晰和优雅的解决方案。递归的核心思想是将一个大规模问题分解为一个或多个同类型但规模更小的子问题,直到子问题简单到可以直接求解。典型的适用场景包括树形结构的

Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解
Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解

理解内存管理的基石在Objective-C的编程世界中,内存管理是开发者必须掌握的核心技能之一。它直接关系到应用的性能、稳定性与资源利用效率。与一些采用自动垃圾回收机制的语言不同,Objective-C在很长一段时间里,依赖一套基于引用计数的、需要开发者部分介入的管理规则。这套规则的核心思想是明确的

如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏
如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏

理解 dealloc 的角色与时机在 iOS 应用开发中,内存管理是保障应用性能与稳定性的基石。dealloc 方法是 Objective-C 中对象生命周期结束时的关键回调,它标志着对象即将被系统回收内存。正确理解其触发时机至关重要:当一个对象的引用计数降为零时,运行时系统会自动调用该对象的 de

深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制
深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制

内存管理的基石在Objective-C的世界里,内存管理是开发者必须掌握的核心技能之一。作为一门在手动引用计数(MRC)时代诞生的语言,Objective-C要求程序员对对象的生命周期有清晰的认识。dealloc方法正是这一生命周期中至关重要的终点站。它是一个实例方法,当对象的引用计数降为零时,系统

理解 native2ascii:Java 国际化开发中的字符编码工具
理解 native2ascii:Java 国际化开发中的字符编码工具

native2ascii 工具的基本定位在Ja va应用程序的国际化与本地化开发过程中,处理非拉丁字符集是一个常见且关键的环节。Ja va内部使用Unicode字符集来统一表示全球各种语言的文字,但其属性文件(.properties)在历史上要求使用ASCII编码,或者更准确地说,要求非ASCII字

如何使用 native2ascii 转换中文字符为 Unicode 转义序列
如何使用 native2ascii 转换中文字符为 Unicode 转义序列

理解 native2ascii 工具的基本用途在软件开发,特别是涉及国际化处理的场景中,开发者常常需要处理不同编码的文本资源。native2ascii 是 Ja va 开发工具包(JDK)中提供的一个命令行实用程序,其主要功能是将包含本地字符编码(非ASCII字符)的文件,转换为包含 Unicode

Java native2ascii 命令详解:解决属性文件乱码问题
Java native2ascii 命令详解:解决属性文件乱码问题

native2ascii 命令的由来与作用在Ja va开发中,处理国际化资源文件是一个常见需求。资源文件通常以.properties格式存储,用于支持多语言界面。然而,Ja va属性文件默认采用ISO-8859-1字符集编码,这导致了一个直接的问题:当文件中包含非拉丁字符(如中文、日文、韩文等)时,

一个 memwatch 实战案例:定位野指针问题
一个 memwatch 实战案例:定位野指针问题

内存监控工具的价值与挑战在软件开发,尤其是使用C/C++这类手动管理内存的语言时,内存错误是程序员最常遭遇的难题之一。其中,野指针问题因其隐蔽性和破坏性,往往成为最难定位的“幽灵”缺陷。它可能潜伏在代码中,在特定条件下才被触发,导致程序崩溃、数据损坏或难以预测的行为。传统的调试手段,如打印日志或使用

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。