cuDF 与 CuPy 之间的互操作性#

本 Jupyter Notebook 提供了如何结合使用 cuDF 和 CuPy 的入门示例,以利用 CuPy 数组功能(例如高级线性代数运算)。

import cudf
import cupy as cp
from packaging import version

if version.parse(cp.__version__) >= version.parse("10.0.0"):
    cupy_from_dlpack = cp.from_dlpack
else:
    cupy_from_dlpack = cp.fromDlpack

将 cuDF DataFrame 转换为 CuPy Array#

如果我们要将 cuDF DataFrame 转换为 CuPy ndarray,有多种方法可以实现

  1. 我们可以使用 dlpack 接口。

  2. 我们也可以使用 DataFrame.values

  3. 我们还可以通过使用 cuDF 的 to_cupy 功能,借助 CUDA array interface 进行转换。

nelem = 10000
df = cudf.DataFrame(
    {
        "a": range(nelem),
        "b": range(500, nelem + 500),
        "c": range(1000, nelem + 1000),
    }
)

%timeit arr_cupy = cupy_from_dlpack(df.to_dlpack())
%timeit arr_cupy = df.values
%timeit arr_cupy = df.to_cupy()
190 μs ± 4.13 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
384 μs ± 4.5 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
379 μs ± 3.68 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
arr_cupy = cupy_from_dlpack(df.to_dlpack())
arr_cupy
array([[    0,   500,  1000],
       [    1,   501,  1001],
       [    2,   502,  1002],
       ...,
       [ 9997, 10497, 10997],
       [ 9998, 10498, 10998],
       [ 9999, 10499, 10999]])

将 cuDF Series 转换为 CuPy Array#

将 cuDF Series 转换为 CuPy array 也有多种方法

  1. 我们可以将 Series 传递给 cupy.asarray,因为 cuDF Series 公开了 __cuda_array_interface__

  2. 我们可以利用 dlpack 接口 to_dlpack()

  3. 我们也可以使用 Series.values

col = "a"

%timeit cola_cupy = cp.asarray(df[col])
%timeit cola_cupy = cupy_from_dlpack(df[col].to_dlpack())
%timeit cola_cupy = df[col].values
171 μs ± 1.95 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
346 μs ± 4.11 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
239 μs ± 3.4 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
cola_cupy = cp.asarray(df[col])
cola_cupy
array([   0,    1,    2, ..., 9997, 9998, 9999])

从这里,我们可以继续正常的 CuPy 工作流程,例如重塑数组、获取对角线或计算范数。

reshaped_arr = cola_cupy.reshape(50, 200)
reshaped_arr
array([[   0,    1,    2, ...,  197,  198,  199],
       [ 200,  201,  202, ...,  397,  398,  399],
       [ 400,  401,  402, ...,  597,  598,  599],
       ...,
       [9400, 9401, 9402, ..., 9597, 9598, 9599],
       [9600, 9601, 9602, ..., 9797, 9798, 9799],
       [9800, 9801, 9802, ..., 9997, 9998, 9999]])
reshaped_arr.diagonal()
array([   0,  201,  402,  603,  804, 1005, 1206, 1407, 1608, 1809, 2010,
       2211, 2412, 2613, 2814, 3015, 3216, 3417, 3618, 3819, 4020, 4221,
       4422, 4623, 4824, 5025, 5226, 5427, 5628, 5829, 6030, 6231, 6432,
       6633, 6834, 7035, 7236, 7437, 7638, 7839, 8040, 8241, 8442, 8643,
       8844, 9045, 9246, 9447, 9648, 9849])
cp.linalg.norm(reshaped_arr)
array(577306.967739)

将 CuPy Array 转换为 cuDF DataFrame#

我们也可以将 CuPy ndarray 转换为 cuDF DataFrame。和之前一样,有多种方法可以实现

  1. 最简单的方法; 我们可以直接使用 DataFrame 构造函数。

  2. 我们可以使用 CUDA array interface 配合 DataFrame 构造函数。

  3. 我们也可以使用 dlpack 接口。

对于后两种情况,我们需要确保 CuPy 数组在内存中是 Fortran 连续的(如果它尚未如此)。我们可以转置数组或者简单地在操作前将其强制转换为 Fortran 连续。

%timeit reshaped_df = cudf.DataFrame(reshaped_arr)
5.13 ms ± 24.1 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
reshaped_df = cudf.DataFrame(reshaped_arr)
reshaped_df.head()
0 1 2 3 4 5 6 7 8 9 ... 190 191 192 193 194 195 196 197 198 199
0 0 1 2 3 4 5 6 7 8 9 ... 190 191 192 193 194 195 196 197 198 199
1 200 201 202 203 204 205 206 207 208 209 ... 390 391 392 393 394 395 396 397 398 399
2 400 401 402 403 404 405 406 407 408 409 ... 590 591 592 593 594 595 596 597 598 599
3 600 601 602 603 604 605 606 607 608 609 ... 790 791 792 793 794 795 796 797 798 799
4 800 801 802 803 804 805 806 807 808 809 ... 990 991 992 993 994 995 996 997 998 999

5 行 × 200 列

我们可以通过使用 cupy.isfortran 或查看数组的 flags 来检查数组是否为 Fortran 连续。

cp.isfortran(reshaped_arr)
False

在这种情况下,我们需要在转换为 cuDF DataFrame 之前对其进行转换。在接下来的两个单元格中,我们将分别利用 dlpack 和 CUDA array interface 创建 DataFrame。

%%timeit

fortran_arr = cp.asfortranarray(reshaped_arr)
reshaped_df = cudf.DataFrame(fortran_arr)
5.09 ms ± 16 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%%timeit

fortran_arr = cp.asfortranarray(reshaped_arr)
reshaped_df = cudf.from_dlpack(fortran_arr.__dlpack__())
5.11 ms ± 30.3 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
fortran_arr = cp.asfortranarray(reshaped_arr)
reshaped_df = cudf.DataFrame(fortran_arr)
reshaped_df.head()
0 1 2 3 4 5 6 7 8 9 ... 190 191 192 193 194 195 196 197 198 199
0 0 1 2 3 4 5 6 7 8 9 ... 190 191 192 193 194 195 196 197 198 199
1 200 201 202 203 204 205 206 207 208 209 ... 390 391 392 393 394 395 396 397 398 399
2 400 401 402 403 404 405 406 407 408 409 ... 590 591 592 593 594 595 596 597 598 599
3 600 601 602 603 604 605 606 607 608 609 ... 790 791 792 793 794 795 796 797 798 799
4 800 801 802 803 804 805 806 807 808 809 ... 990 991 992 993 994 995 996 997 998 999

5 行 × 200 列

将 CuPy Array 转换为 cuDF Series#

要将数组转换为 Series,我们可以直接将数组传递给 Series 构造函数。

cudf.Series(reshaped_arr.diagonal()).head()
0      0
1    201
2    402
3    603
4    804
dtype: int64

巧妙结合 CuDF 和 CuPy 实现流畅的 PyData 工作流程#

RAPIDS 库和整个 GPU PyData 生态系统正在快速发展,但有时某个库可能不具备您需要的功能。一个例子是计算 Pandas DataFrame 的行级总和(或均值)。cuDF 对行级操作的支持尚不成熟,因此您需要转置 DataFrame 或编写 UDF 并显式计算每行的总和。根据数据形状,转置可能导致数十万列(cuDF 对此性能不佳),而编写 UDF 可能会耗费大量时间。

通过利用 GPU PyData 生态系统的互操作性,此操作变得非常简单。让我们计算之前重塑的 cuDF DataFrame 的行级总和。

reshaped_df.head()
0 1 2 3 4 5 6 7 8 9 ... 190 191 192 193 194 195 196 197 198 199
0 0 1 2 3 4 5 6 7 8 9 ... 190 191 192 193 194 195 196 197 198 199
1 200 201 202 203 204 205 206 207 208 209 ... 390 391 392 393 394 395 396 397 398 399
2 400 401 402 403 404 405 406 407 408 409 ... 590 591 592 593 594 595 596 597 598 599
3 600 601 602 603 604 605 606 607 608 609 ... 790 791 792 793 794 795 796 797 798 799
4 800 801 802 803 804 805 806 807 808 809 ... 990 991 992 993 994 995 996 997 998 999

5 行 × 200 列

我们可以将其转换为 CuPy 数组,然后使用 sumaxis 参数。

new_arr = cupy_from_dlpack(reshaped_df.to_dlpack())
new_arr.sum(axis=1)
array([  19900,   59900,   99900,  139900,  179900,  219900,  259900,
        299900,  339900,  379900,  419900,  459900,  499900,  539900,
        579900,  619900,  659900,  699900,  739900,  779900,  819900,
        859900,  899900,  939900,  979900, 1019900, 1059900, 1099900,
       1139900, 1179900, 1219900, 1259900, 1299900, 1339900, 1379900,
       1419900, 1459900, 1499900, 1539900, 1579900, 1619900, 1659900,
       1699900, 1739900, 1779900, 1819900, 1859900, 1899900, 1939900,
       1979900])

仅用这一行代码,我们就可以在这个生态系统中的数据结构之间无缝切换,这为我们带来了巨大的灵活性,而不会牺牲速度。

将 cuDF DataFrame 转换为 CuPy Sparse Matrix#

我们也可以将 DataFrame 或 Series 转换为 CuPy 稀疏矩阵。如果下游流程需要 CuPy 稀疏矩阵作为输入,我们可能需要这样做。

稀疏矩阵数据结构由三个密集数组定义。我们将定义一个小的辅助函数以保持代码整洁。

def cudf_to_cupy_sparse_matrix(data, sparseformat="column"):
    """Converts a cuDF object to a CuPy Sparse Column matrix."""
    if sparseformat not in (
        "row",
        "column",
    ):
        raise ValueError("Let's focus on column and row formats for now.")

    _sparse_constructor = cp.sparse.csc_matrix
    if sparseformat == "row":
        _sparse_constructor = cp.sparse.csr_matrix

    return _sparse_constructor(cupy_from_dlpack(data.to_dlpack()))

我们可以定义一个稀疏填充的 DataFrame 来演示如何将其转换为任意一种稀疏矩阵格式。

df = cudf.DataFrame()
nelem = 10000
nonzero = 1000
for i in range(20):
    arr = cp.random.normal(5, 5, nelem)
    arr[cp.random.choice(arr.shape[0], nelem - nonzero, replace=False)] = 0
    df["a" + str(i)] = arr
df.head()
a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19
0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 -6.169778 0.0 0.0 0.0 0.000000 0.0 0.0
1 0.0 -4.541529 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0
2 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0
3 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 0.000000 0.0 0.0
4 0.0 0.000000 1.968797 0.0 0.0 0.0 0.0 0.0 0.0 0.931168 0.0 0.0 0.0 0.000000 0.0 0.0 0.0 13.904802 0.0 0.0
sparse_data = cudf_to_cupy_sparse_matrix(df)
print(sparse_data)
<Compressed Sparse Column sparse matrix of dtype 'float64'
	with 20000 stored elements and shape (10000, 20)>
  Coords	Values
  (1536, 0)	-1.588673097447183
  (513, 0)	9.970849097280857
  (1537, 0)	10.919748570706547
  (514, 0)	15.406238081371376
  (133, 0)	14.244409906640747
  (391, 0)	14.902704249607675
  (903, 0)	0.5230024581417142
  (1031, 0)	8.70026662105173
  (520, 0)	10.749146426975798
  (521, 0)	-0.3975292543165676
  (138, 0)	12.426856080125647
  (395, 0)	8.916467351123888
  (12, 0)	-2.8616338036589584
  (524, 0)	4.681955023757494
  (652, 0)	12.246118348445732
  (781, 0)	-2.8507967809171832
  (1041, 0)	6.995574359377683
  (274, 0)	2.3309249317612797
  (147, 0)	12.359227470864713
  (1171, 0)	10.093092378619225
  (660, 0)	8.154601665233967
  (1174, 0)	6.3087799395907735
  (1175, 0)	4.032828405884995
  (1431, 0)	12.646691935293564
  (280, 0)	5.906604494435242
  :	:
  (9455, 19)	7.558948666478769
  (8304, 19)	9.961580742164827
  (9328, 19)	-4.291674396726237
  (9456, 19)	10.757362989980543
  (9840, 19)	7.538590667108517
  (8689, 19)	6.044390744662792
  (8306, 19)	11.117171906013688
  (9586, 19)	-1.6244139065599927
  (8563, 19)	2.462342095564601
  (9076, 19)	13.118104961154485
  (9077, 19)	-0.8453695473258904
  (8312, 19)	4.134135117931522
  (8696, 19)	17.381880180943515
  (9337, 19)	3.6189164595357495
  (9465, 19)	2.4950134371873016
  (9338, 19)	5.2247938993269765
  (8316, 19)	7.9226341254972095
  (8061, 19)	5.225457778998734
  (9214, 19)	1.028785069440696
  (9726, 19)	-4.226047690994142
  (9957, 19)	8.546192901635248
  (9963, 19)	4.229201318842815
  (9968, 19)	4.735706306709329
  (9974, 19)	5.79253803092277
  (9976, 19)	6.973675422848798

从这里,我们可以继续使用 CuPy 稀疏矩阵进行工作。

有关这些库内置功能的完整列表,我们建议您查看 cuDFCuPy 的 API 文档。