欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

并行计算debug

程序员文章站 2022-03-06 13:55:57
...

一个简单的并行计算程序出错引发的思考

问题背景

给定N × N矩阵A,N维向量B,设计并行算法计算矩阵与向量的乘积C。

基本思路

给定并行的程序个数numprocs,将矩阵按行分为numprocs块,然后由主进程将分块后的矩阵分配给其他各个进程,与此同时,将存储在主进程当中的向量B广播给其他所有进程。之后,由各个子进程将分块矩阵与向量相乘的结果发送回主进程,由主进程重新组装,然后输出。

基本环境

Linux下的Open MPI

Bug1 没有初始化的变量十分危险

程序的运行命令是

mpirun -np 100 MATRIX_VECTOR

np后面的参数代表设定的并行进程数。

一开始,我键入

mpirun -np 1 MATRIX_VECTOR

只设定了一个并行进程,实际上相当于按普通的串行算法计算矩阵与向量的乘积。
运行无错,结果正确。

之后,我将并行进程数设定为10出现错误。报错Segment Fault(11), 外加Invalid Permission(2)。

我的第一反应是数组越界,于是进行了长时间的检查,最终成功地修复了另一个Bug,不过被修复的这个Bug与数组越界无关。随后,我猜想可能是设定的数组过大,用来计算的并行机允许分配的内存不足。于是,我又将N设定为100,依然报错。最终,我将N设定为10,报错依旧,基本排除是内存分配不足的原因。

随后,根据经验会不会只是10这个数字比较特殊,使得程序中某些由变量运算生成的指标越界了呢?最终,在反复实验下,我发现对于一段连续的整数np都报错。即便np = 2,也会报错。况且,自己观察报错信息,发现对于所有的子进程,全部报Segment Fault错误。因此,很有可能Segment Fault并不出现在计算环节。

由于Open MPI提供的debug的工具学习成本高,接下来,我只好采用“探针方法”,在程序的不同位置插入printf,观察程序能否运行至此处,假如有输出,则出错在此处之后,如果无,则出错在此处之前。最终大约可以确定,程序出错在主进程发送分块矩阵阶段。

在Open MPI当中,发送数据和接收数据依赖于一对库函数MPI_Send与MPI_Recv。
它们的定义分别如下

#include <mpi.h>
int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest,
    int tag, MPI_Comm comm)

https://www.open-mpi.org/doc/v1.6/man3/MPI_Send.3.php

以及

#include <mpi.h>
int MPI_Recv(void *buf, int count, MPI_Datatype datatype,
    int source, int tag, MPI_Comm comm, MPI_Status *status)

https://www.open-mpi.org/doc/v1.6/man3/MPI_Recv.3.php

其中,有许多和发送接受无关的参数,如comm,tag等,可以随意指定。但是,看似无关紧要的参数status却是问题的所在。

一开始我只是在程序的开头写了

  MPI_Status *status;

随后在子进程中调用MPI_Recv。

MPI_Recv(&(A[0][0]), N * N / numprocs, MPI_INT, 0, 1, MPI_COMM_WORLD, status);

而正是这一行代码引发了错误。
首先,在C语言中声明一个变量和定义一个变量是不同的。所谓声明变量,指的是存在这样一个符号,但并不分配内存。而是否给变量分配内存,实际上是决定一个变量在内存中存在的关键。

如果一个变量只是被声明而没有被分配内存,尤其是对于指针变量,它不存在合法的值,所以,任何与它的值有关的操作都会导致Segment Fault。在这个程序中,出错的地方正是引用了status。

Bug2 指针与地址的不同

在二维数组A[m][n]当中,A是一个指针,相当于DataType**,是一个指向指向DataType的指针的指针,此外A[m]的值虽然与第m行第0列元素的地址相同,但这个变量本身的意义是一个指针,而不是内存地址。因此,只有&(A[m][0])才是第m行第0列元素的地址。

而MPI_Send与MPI_Recv的第一个参数需要的是一个元素地址,而不是指针。所以,写A, A[m],都是数据类型不匹配的实参。

所谓指针,某种意义上说,就是被赋予了意义的地址,但它不等同于地址

Bug3 可以同名收发

在主进程中send(A),子进程中写recv(A),由于主进程子进程地址空间的分离,这两个符号并不会引发冲突。

Bug4 MPI_Bcast

MPI_Bcast(&(B[0]), N, MPI_INT, 0, MPI_COMM_WORLD);

MPI_Bcast是对所有进程的同一符号的变量的广播,它代表的意义是,一旦运行完毕,所有子进程内这一符号的变量的值全部将与指定的第k个进程的这一符号的值相同,它的作用是“统一化”。这样理解,比较合适。

把它只在主进程中调用是不合适的,并不会改变其他子进程中这一变量的值。它必须独立于

if(myid ==k) {...}

,要在所有的进程中被同时执行。

参看

https://*.com/questions/5104847/mpi-bcast-a-dynamic-2d-array

总结

MPI并行环境类似于同一台机器多线程的情况,内存模型是相同的,程序语句执行模型也是相同的。

相关标签: 并行计算