Interoperability between cuDF and CuPy#
This notebook provides introductory examples of how you can use cuDF and CuPy together to take advantage of CuPy array functionality (such as advanced linear algebra operations).
import timeit
import cupy as cp
from packaging import version
import cudf
if version.parse(cp.__version__) >= version.parse("10.0.0"):
cupy_from_dlpack = cp.from_dlpack
else:
cupy_from_dlpack = cp.fromDlpack
Converting a cuDF DataFrame to a CuPy Array#
If we want to convert a cuDF DataFrame to a CuPy ndarray, There are multiple ways to do it:
We can use the dlpack interface.
We can also use
DataFrame.values
.We can also convert via the CUDA array interface by using cuDF’s
to_cupy
functionality.
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()
364 μs ± 5.43 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
689 μs ± 8.13 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
701 μs ± 32.2 μ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]])
Converting a cuDF Series to a CuPy Array#
There are also multiple ways to convert a cuDF Series to a CuPy array:
We can pass the Series to
cupy.asarray
as cuDF Series exposes__cuda_array_interface__
.We can leverage the dlpack interface
to_dlpack()
.We can also use
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
454 μs ± 35.5 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
793 μs ± 30.9 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
546 μs ± 10.5 μ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])
From here, we can proceed with normal CuPy workflows, such as reshaping the array, getting the diagonal, or calculating the norm.
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)
Converting a CuPy Array to a cuDF DataFrame#
We can also convert a CuPy ndarray to a cuDF DataFrame. Like before, there are multiple ways to do it:
Easiest; We can directly use the
DataFrame
constructor.We can use CUDA array interface with the
DataFrame
constructor.We can also use the dlpack interface.
For the latter two cases, we’ll need to make sure that our CuPy array is Fortran contiguous in memory (if it’s not already). We can either transpose the array or simply coerce it to be Fortran contiguous beforehand.
%timeit reshaped_df = cudf.DataFrame(reshaped_arr)
13.2 ms ± 183 μ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 rows × 200 columns
We can check whether our array is Fortran contiguous by using cupy.isfortran or looking at the flags of the array.
cp.isfortran(reshaped_arr)
False
In this case, we’ll need to convert it before going to a cuDF DataFrame. In the next two cells, we create the DataFrame by leveraging dlpack and the CUDA array interface, respectively.
%%timeit
fortran_arr = cp.asfortranarray(reshaped_arr)
reshaped_df = cudf.DataFrame(fortran_arr)
13.2 ms ± 210 μ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.toDlpack())
12.1 ms ± 219 μ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 rows × 200 columns
Converting a CuPy Array to a cuDF Series#
To convert an array to a Series, we can directly pass the array to the Series
constructor.
cudf.Series(reshaped_arr.diagonal()).head()
0 0
1 201
2 402
3 603
4 804
dtype: int64
Interweaving CuDF and CuPy for Smooth PyData Workflows#
RAPIDS libraries and the entire GPU PyData ecosystem are developing quickly, but sometimes a one library may not have the functionality you need. One example of this might be taking the row-wise sum (or mean) of a Pandas DataFrame. cuDF’s support for row-wise operations isn’t mature, so you’d need to either transpose the DataFrame or write a UDF and explicitly calculate the sum across each row. Transposing could lead to hundreds of thousands of columns (which cuDF wouldn’t perform well with) depending on your data’s shape, and writing a UDF can be time intensive.
By leveraging the interoperability of the GPU PyData ecosystem, this operation becomes very easy. Let’s take the row-wise sum of our previously reshaped 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 rows × 200 columns
We can just transform it into a CuPy array and use the axis
argument of sum
.
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])
With just that single line, we’re able to seamlessly move between data structures in this ecosystem, giving us enormous flexibility without sacrificing speed.
Converting a cuDF DataFrame to a CuPy Sparse Matrix#
We can also convert a DataFrame or Series to a CuPy sparse matrix. We might want to do this if downstream processes expect CuPy sparse matrices as an input.
The sparse matrix data structure is defined by three dense arrays. We’ll define a small helper function for cleanliness.
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()))
We can define a sparsely populated DataFrame to illustrate this conversion to either sparse matrix format.
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.0 | 0.0 | 0.00000 | 0.000000 | 0.00000 | 0.0 | 0.000000 | 0.000000 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.000000 | 11.488148 | 0.0 | 0.0 | 0.0 | 0.0 |
1 | 0.0 | 0.000000 | 0.0 | 0.0 | 0.00000 | 0.000000 | 0.00000 | 0.0 | 0.000000 | 0.000000 | 0.0 | 0.0 | 0.0 | 0.000000 | 10.697098 | 0.000000 | 0.0 | 0.0 | 0.0 | 0.0 |
2 | 0.0 | 4.332114 | 0.0 | 0.0 | -0.47451 | 0.000000 | 0.00000 | 0.0 | 5.461388 | 7.069211 | 0.0 | 0.0 | 0.0 | -3.311282 | 0.000000 | 0.000000 | 0.0 | 0.0 | 0.0 | 0.0 |
3 | 0.0 | 7.858059 | 0.0 | 0.0 | 0.00000 | -2.311459 | 0.00000 | 0.0 | 0.000000 | 0.000000 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.000000 | 0.0 | 0.0 | 0.0 | 0.0 |
4 | 0.0 | 0.000000 | 0.0 | 0.0 | 0.00000 | 0.000000 | 9.84577 | 0.0 | 9.311493 | 2.294953 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.000000 | 0.0 | 0.0 | 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
(128, 0) 9.872559960239842
(385, 0) -0.5789545652169648
(1153, 0) 7.54478465694384
(130, 0) 9.990885370314544
(1154, 0) 9.568543693104651
(259, 0) 3.5685565040410054
(1027, 0) -0.5713578860568247
(132, 0) -1.9756122593656766
(133, 0) 17.10573324194511
(1157, 0) 3.4408058451533776
(134, 0) 1.6970829245276642
(646, 0) 0.578785994824888
(1030, 0) 1.8720510791907974
(1415, 0) 6.097303638638008
(8, 0) 4.932651361605994
(520, 0) -1.7674941606766181
(1288, 0) 2.9958121018436463
(1416, 0) 1.6734699854779278
(265, 0) -4.6612749420611985
(777, 0) 3.8203022085813445
(1289, 0) 8.09010711794567
(139, 0) 5.443060661801936
(1035, 0) 11.587161187722776
(1163, 0) 15.692188129243307
(140, 0) 3.6112542053268646
: :
(9206, 19) 7.097316013780272
(9334, 19) 11.922693820665234
(9590, 19) 7.587164287715625
(8824, 19) 6.781289952412736
(9208, 19) -6.524244033649557
(9081, 19) 2.4063107176026577
(8058, 19) 5.194002461345493
(9210, 19) 7.41108075299243
(9466, 19) 8.537607823695193
(9211, 19) 1.174617013514763
(9339, 19) 3.1095514386284817
(9598, 19) 11.07688231051858
(9599, 19) 14.733261459254994
(9956, 19) 0.5883945425695464
(9702, 19) 11.400698402082597
(9833, 19) 3.2236683794735326
(9965, 19) 8.975944635138145
(9711, 19) 5.5710173847085125
(9715, 19) 1.314659055632692
(9720, 19) 4.7523874790034935
(9722, 19) 3.1108647907815095
(9850, 19) 3.2954103289133934
(9853, 19) 0.553619192836115
(9854, 19) 7.9337523541811175
(9983, 19) 2.1487040149659316
From here, we could continue our workflow with a CuPy sparse matrix.
For a full list of the functionality built into these libraries, we encourage you to check out the API docs for cuDF and CuPy.