NetworkX all_pairs_dijkstras的CuGraph实现

问题描述

我正在尝试将必须使用的cpu绑定算法转换为GPU,而cugraph遇到了种种麻烦。部分原因是我的无知,另一部分是cugraph的婴儿期和欠发达,最后一部分是我正在吸吮找出优雅的矢量化方法

假设我有m个由n个特征组成的数据观测值。我通过计算来自所有观测值的所有观测值的欧几里德距离来创建距离矩阵(注意:这不是我需要帮助的部分,也不意味着它是最优的。只需添加此部分以易于理解,可重现的代码即可)>

import numpy as np

def l1_distance(arr):
    return np.linalg.norm(arr,1)

X = np.random.randint(low=0,high=255,size=(700,4096))

W = np.empty((700,700))

for i in range(700):
    for j in range(700):
        W[i,j] = l1_distance(X[i,:] - X[j,:])

一个挑战

import networkx as nx

def build_weighted_graph(W):
    n = W.shape[0]
    Graph = nx.DiGraph()
        for i in range(n):
            for j in range(n):
                Graph.add_weighted_edges_from([(i,j,min(W[i,j],W[j,i]))])
    return Graph

其中,输入W矩阵是欧几里得空间中的平方距离矩阵(这些节点最初由x特征组成)。例如。第1行col 9是节点1与节点9之间的距离,第20行col 30是节点20与节点30之间的距离,依此类推。现在该图绘制了所连接节点之间的边,并且边的权重是欧式距离测量

我花了大约8个小时试图弄清楚如何将其转移到GPU,但他们声称,即使在NVIDIA自己的文档中,

代码块对NetworkX来说是完美的。但是,对于GPU而言,遍历数据帧并一次添加一个节点的过程对于GPU来说是成问题的,我们尝试避免这种情况。 cuGraph将数据存储在列(即数组)中。调整数组大小需要为新数组分配一个一个元素,复制数据并添加新值。那不是很有效。

如果您的代码遵循上述模型,即在 时间,我们建议重写该代码或按原样使用 在NetworkX中运行,并通过cuGraph加速算法。

所以我放弃了,离开那部分。算法的下一部分将使用dijkstra算法,并计算所有节点到所有其他节点的最短路径

res = dict(nx.all_pairs_dijkstra_path_length(Graph))

在cugraphs实现中,它们只有单个源dijkstra,它将图和源节点作为参数。这与上述方法附带的networkx库相反,该库在所有节点上无处不在地应用dijkstra。这意味着我将不得不为每个单个节点迭代调用Sssp(cugraph的dijkstra的实现)(更多用于循环)。

在获得了通过连接节点的距离之后,然后创建另一个平方距离矩阵,该矩阵现在基于通过连接节点的距离,而不是最初采用欧几里得距离。

D = np.zeros([n,n])
    for i in range(n):
        for j in range(n):
            D[i,j] = res[i][j]

我一生都无法弄清楚如何针对GPU将这些向量矢量化。任何在正确方向上的帮助将不胜感激。当前,对于运行我的算法的数据集,cpu绑定算法大约需要5分钟才能运行698个节点。因此,为什么我要尝试加快GPU的速度

解决方法

似乎第一个问题的答案-欧几里得距离矩阵的初始化-在Using cupy to create a distance matrix from another matrix on GPU中得到了回答,但是您当然可以使用cuDF和cuGraph优化图的创建和Dijkstra矩阵的计算

要有效地创建图形,您可以构建一个cuDF数据框,其中列出了边缘及其权重。由于欧几里得距离矩阵的结构,这很简单。 cuGraph将此数据帧作为边缘列表并返回图形。然后,您可以遍历节点以计算最短的适用顶点。如果问题规模增大,可以稍后将其与Dask并行化或分发。

下面的代码比使用nx.all_pairs_dijkstra_path_length来解决此问题大小快40倍,其中还包括初始距离计算。

import cupy as cp
import cudf as cd
import cugraph as cg

def build_weighted_graph_gpu(X,n):
    X_d = cp.array(X)
    G_d = cp.zeros([n,n])
    for i in range(n):
        G_d[i,:] = cp.abs(cp.broadcast_to(X_d[i,:],X_d.shape) - X_d).sum(axis=1)
    return G_d

def dijkstras_matrix_gpu(W_d):
    n = np.shape(W_d)[0]
    
    # Create a columnar dataframe describing edges
    df_g = cd.DataFrame({
        'source':      cp.array([x // n for x in range(n*n)]),'destination': cp.array([x % n for x in range(n*n)]),'weight':      cp.minimum(W_d,cp.transpose(W_d)).ravel()})
    
    graph_d = cg.Graph()
    
    graph_d.from_cudf_edgelist(df_g,source='source',destination='destination',edge_attr='weight',renumber=False)
    
    dist_d = cp.empty_like(W_d)

    for i in range(n):
        dist_d[i,:] = cp.asarray(cg.traversal.shortest_path(graph_d,i)['distance'])
    
    return dist_d

distance_matrix = dijkstras_matrix_gpu(build_weighted_graph_gpu(X))