MPI 发送结构,C++中具有矢量属性

MPI send struct with a vector property in C++

本文关键字:属性 C++ 结构 MPI      更新时间:2023-10-16

我想发送一个具有向量属性的结构。

typedef struct {
    int id;
    vector<int> neighbors;
} Node;

我知道我必须创建一个 MPI 派生数据类型,如这个答案所示,但我不知道在我的情况下该怎么做,因为我的结构中有一个向量。

如果你想保持高水平并发送对象,那么 Boost.MPI 是一个不错的选择。使用 Boost.MPI 可以为结构指定高级序列化。

您无法(正确)静态确定向量的数据成员的偏移量。当然有可能拼凑出一种有效的类型。但这也是搬起石头砸自己的脚的好方法。您将在代码中引入假设(例如,向量大小不会改变),一旦违反这些假设就会产生微妙的错误。因此,在这种情况下,对我来说,简单地在MPI_Send中分别发送idneighbours::data()似乎更干净,更不容易出错 - 而不是使用不适合此用例的 MPI 类型。

我不喜欢仅仅为了做这个简单的事情而导入库的想法。所以这是我所做的:

我认为没有理由让 MPI 了解对象的底层结构。所以我可以手动将其转换为缓冲数组,并且由于接收方知道需要 Node 结构,因此可以在另一侧重新创建对象。所以最初我定义了一个MPI_Contiguous数据类型并发送它:

int size = (int) ((node.second.neighbors.size() + 1) * sizeof(int *));
MPI_Datatype datatype;
MPI_Type_contiguous(size, MPI_BYTE, &datatype);
MPI_Type_commit(&datatype);
MPI_Isend(&buffer, 1, datatype, proc_rank, TAG_DATA, MPI_COMM_WORLD, &request); 

这是一个更通用的解决方案并且有效。

但是由于该结构包含一个int和一个vector<int>,我决定创建一个int缓冲区,第一个元素作为node.id,重置为node.neighbors。另一方面,使用 MPI_Iprobe(或同步MPI_Probe)和MPI_Get_count我可以重新创建 Node 结构。这是代码:

int *seriealizeNode(Node node) {
    //allocate buffer array
    int *s = new int[node.neighbors.size() + 1];
    //set the first element = Node.id
    s[0] = node.id;
    //set the rest elements to be the vector elements
    for (int i = 0; i < node.neighbors.size(); ++i) {
        s[i + 1] = node.neighbors[i];
    }
    return s;
}
Node deseriealizeNode(int buffer[], int size) {
    Node node;
    //get the Node.id
    node.id = buffer[0];
    //get the vector elements
    for (int i = 1; i < size; ++i) {
        node.neighbors.push_back(buffer[i]);
    }
    return node;
}

我认为必须有一种更有效/更快的方法来将 Node 转换为 int[],反之亦然。我希望有人可以提供一些提示。

然后在发件人方面:

while (some_condition){
    ...
    //if there is a pending request wait for it to finish and then free the buffer
    if (request != MPI_REQUEST_NULL) {
        MPI_Wait(&request, &status);
        free(send_buffer);
    }
    // now send the node data
    send_buffer = seriealizeNode(node.second);
    int buffer_size = (int) (node.second.neighbors.size() + 1);
    MPI_Isend(send_buffer, buffer_size, MPI_INT, proc, TAG_DATA, MPI_COMM_WORLD, &request);
    ...
}

在接收器方面:

int count = 0;
MPI_Iprobe(MPI_ANY_SOURCE, TAG_DATA, MPI_COMM_WORLD, &flag, &status);
if (flag) {
    MPI_Get_count(&status, MPI_INT, &count);
    int *s = new int[count];
    MPI_Recv(s, count, MPI_INT, MPI_ANY_SOURCE, TAG_DATA, MPI_COMM_WORLD, &status);
    Node node = deseriealizeNode(s, count);
    free(s);
    //my logic
}

现在它按预期工作。

请注意,在内部,vector<int>看起来像这样:

struct vector {
    size_t size;
    size_t alloc_size;
    int* data;
};

因此,如果您尝试按照 puelo 的建议发送结构,它不会访问向量背后的实际数据,而是发送内存中这些项目后面的size字段、data指针和这些项目后面的任何数据,这很可能导致无效的内存访问。向量中的实际数据不会像这样发送。

通常,MPI 不适用于发送包含指向更多数据的指针的结构。相反,您应该尝试考虑如何发送实际的基础数据本身。

如果能够以连续的方式表示数据,MPI 通信将更容易、更高效。

您的struct Node看起来像您正在尝试在图形中表示节点。例如,您可以以邻接数组格式表示图形数据,其中所有邻居 ID 都用一个大向量表示。把它想象成你以前struct Node的所有neighbors向量的串联。然后,对于每个节点,您需要将偏移量保存到新的neighbors向量中。

std::vector<int> node_ids(num_nodes);
std::vector<int> nodes_offsets(num_nodes);
std::vector<int> neighbors(num_edges);
// neighbors for node i are accessible via:
for (int j = node_offsets[i]; j <= node_offsets[i+1]-1; ++j) {
    int neighbor = neighbors[j];
    // ...
}

然后,您可以使用 MPI 轻松发送/接收此信息:

MPI_Send(&neighbors[0], MPI_INT, neighbors.size(), ...);

使用 MPI 时,为数据找到良好的数据布局是实现算法时最重要的步骤之一。