学习数据框架交换协议:CuDF的实现

504 阅读7分钟

cuDF - RAPIDS GPU数据框架

cuDF 是一个非常类似于 的数据框架库,它在GPU上运行,以便从其计算能力中获益。

在这个阶段,我们可以回顾一下,现在有很多数据框架库,比如。 pandas, vaex, modin, dask/cudf-dask。每一个都有其优势和劣势。例如,vaex 允许你在笔记本电脑上处理比内存(RAM)更大的数据集,dask 允许你在进程和集群节点之间分配计算,cudf-dask 是它的GPU对应项。

假设你的笔记本上有一个300GB的数据集,并想获得一些关于它的见解。一个典型的工作流程可以是。

import vaex
# run some exploratory data analysis (EDA)
import dask, cudf
# take a sample of 3 GB
# run some operations with Dask, then cuDF and compare performance. 

在这一过程中,我们多次加载数据集。我们使用一对to_pandas/from_pandas ,从一个数据框架库转移到另一个。这种做法出现了挑战。

  • 可能的内存开销

  • pandas 的高度耦合,破坏了一个重要的软件设计模式。[依赖反转原则(DIP)]促进了抽象层(接口)的依赖,而不是实现层。

数据框架协议作为指定数据框架通用表示法的接口开始发挥作用,从而恢复了被打破的依赖反转设计模式。

现在,我们可以直接使用from_dataframe 方法从一个数据框架库移动到另一个。不需要再通过pandas 。请注意,这只有在支持该协议的数据框架库之间才有可能。另外,该协议尽可能地执行零拷贝,这使我们摆脱了可能的内存开销。

On the left, we have the interoperability between dataframe libraries through <code>pandas</code> which is a implementation dependency. On the right, we have the interoperability through the dataframe interchange API which an abstract dependency没有和有数据框架交换协议API的设计比较

遵守数据框架交换协议的库的主要好处之一是,只要遵循接口合同规范,每个库都可以独立发展,我们可以像pandas那样摆脱对数据框架库的依赖。

数据框架交换协议的简要描述

数据框架交换协议实际上是一个接口的组合。

A composition of the 3 interfaces forming the dataframe interchange protocol: _CuDFDataFrame has 1 or more _CuDFColumn which in turn has 1 or more _CuDFBuffer 数据框架交换协议接口的组成。链接上的cardinality意味着 "拥有1个或更多 "提到的接口。

所以每个支持该协议的库都应该实现这3个接口,可以描述如下。

  • DataFrame主要暴露了不同的方法来访问/选择列(通过名称、索引)和知道行的数量。

  • Column有方法来访问列的数据类型,描述有效/缺失的值,公开不同的缓冲区(数据、有效性和偏移量),Chunks,...等等。

  • 缓冲区有描述列数据的连续内存块的方法,即设备(GPU,CPU,...),内存地址,大小等。

cuDF 接口的实现有什么期望

让我们回顾一下要在cuDF 中实现的协议主要特征。

简单dtype复杂dtype设备缺少的值
int分类的GPU所有的dtypes(简单和复杂)单一
uint8字符串CPU-倍数
float数据时间---

上表显示了应该支持的数据框架交换协议的不同特征。特别是,我们必须支持带有各种类型(简单和复杂)的列的cuDF数据帧,并处理它们的缺失值。同样,我们也必须支持来自不同设备的数据帧,如CPU。

cuDF 数据框架交换协议的进展

下表中被选中的元素代表了到目前为止已经实现的功能。

简单dtype复杂dtype设备缺少的值
int分类的GPU所有支持的dtypes(简单和复杂)。单一
uint8字符串CPU-倍数
float数据时间---
bool----

请注意,我们支持像pandas一样的CPU数据帧,但由于该协议还没有被集成到pandas repo中,我们只能在本地进行测试。我们已经将这项工作作为一个仍在审查中的Pull Request提交到rapidsai/cudf github repo。

工作cuDF 代码示例

当我们在pandascuDF 之间往返时,我们将通过一个代码实例来了解该协议的运行情况。我们首先创建一个cuDF数据框架对象,列以支持的dtypes命名。

import cudf
import cupy as cp
data = {'int': [1000, 2, 300, None], 
        'uint8': cp.array([0, 128, 255, 25], dtype=cp.uint8),
        'float': [None, 2.5, None, 10],
        'bool': [True, None, False, True],
        'string': ['hello', '', None, 'always TDD.']}

df = cudf.DataFrame(data)
df['categorical'] = df['int'].astype('category')

让我们看看这个数据框架,确保列的dtypes在内部被正确识别。

print(f'{df} \n\n'); df.info()

输出

    int  uint8 float   bool       string categorical
0  1000      0  <NA>   True        hello        1000
1     2    128   2.5   <NA>                        2
2   300    255  <NA>  False         <NA>         300
3  <NA>     25  10.0   True  always TDD.        <NA>


<class 'cudf.core.dataframe.DataFrame'>
RangeIndex: 4 entries, 0 to 3
Data columns (total 6 columns):
#   Column       Non-Null Count  Dtype
---  ------       --------------  -----
0   int          3 non-null      int64
1   uint8        4 non-null      uint8
2   float        2 non-null      float64
3   bool         3 non-null      bool
4   string       3 non-null      object
5   categorical  3 non-null      category
dtypes: bool(1), category(1), float64(1), int64(1), object(1), uint8(1)
memory usage: 393.0+ bytes

现在,我们创建数据框架交换协议对象来检查基本信息,如行数、列名和dtypes是否准确。

dfo =  df.__dataframe__()
print(f'{dfo}: {dfo.num_rows()} rows\n')
print('Column\t Non-Null Count\t\t\t\t\t    Dtype\n')
for n, c in zip(dfo.column_names(), dfo.get_columns()): 
    print(f'{n}\t\t      {int(c.size - c.null_count)}\t\t{c.dtype}')

输出

<cudf.core.df_protocol._CuDFDataFrame object at 0x7f3edee0d8e0>: 4 rows

Column   Non-Null Count                                 Dtype

int                   3     (<_DtypeKind.INT: 0>, 64, '<i8', '=')
uint8                 4     (<_DtypeKind.UINT: 1>, 8, '|u1', '|')
float                 2     (<_DtypeKind.FLOAT: 2>, 64, '<f8', '=')
bool                  3     (<_DtypeKind.BOOL: 20>, 8, '|b1', '|')
string                3     (<_DtypeKind.STRING: 21>, 8, 'u', '=')
categorical           3     (<_DtypeKind.CATEGORICAL: 23>, 8, '|u1', '=')

缓冲区情况如何?我们将检查那些 "float "列。

fcol = dfo.get_column_by_name('float')
buffers = fcol.get_buffers()
for k in buffers:
    print(f'{k}: {buffers[k]}\n')

输出

data: (CuDFBuffer({'bufsize': 32, 'ptr': 140704936368128, 'dlpack': <capsule object "dltensor" at 0x7ff893505e40>, 'device': 'CUDA'}), (<_DtypeKind.FLOAT: 2>, 64, '<f8', '='))

validity: (CuDFBuffer({'bufsize': 512, 'ptr': 140704936365568, 'dlpack': <capsule object "dltensor" at 0x7ff893505e40>, 'device': 'CUDA'}), (<_DtypeKind.UINT: 1>, 8, 'C', '='))

offsets: None

我们可以注意到来自数据缓冲区的列dtype<_DtypeKind.FLOAT: 2> 和有效性掩码的dtype,这里总是<_DtypeKind.UINT: 1> 。最后没有offset 缓冲区,因为它被保留给可变长度的数据,如字符串。

让我们使用[DLPack协议]从缓冲区中检索数据和有效性数组,并与列本身进行比较。

data_buffer = fcol.get_buffers()['data'][0]
validity_buffer = fcol.get_buffers()['validity'][0]
data = cp.fromDlpack(data_buffer.__dlpack__())
validity = cp.fromDlpack(validity_buffer.__dlpack__())
print(f'column: {df.float}')
print(f'data: {data}')
print(f'validity: {validity}')

输出

float column
0    <NA>
1     2.5
2    <NA>
3    10.0
Name: float, dtype: float64

data: [ 0.   2.5  0.  10. ]
validity: [0 1 0 1]

比较float列和数据,我们看到除了列中的<NA> 与数据数组中的0 对应外,其他的值都很相似。事实上,在缓冲区层面,我们用一个 "哨兵值 "来编码缺失值,这里的哨兵值是0。这就是有效性数组发挥作用的地方。与数据数组一起,我们能够在缺失值的确切位置重建该列。怎么做到的?有效性数组中的0表示数据中缺失值的位置或索引,1表示有效/不缺失的值。所有这些工作都是由一个辅助函数_from_dataframe ,它从一个数据框架交换对象中建立起整个cuDF数据框架。

from cudf.core.df_protocol import _from_dataframe
df_rebuilt = _from_dataframe(dfo)
print(f'rebuilt df\n----------\n{df_rebuilt}\n')
print(f'df\n--\n{df}')

输出

rebuilt df
----------
    int  uint8 float   bool       string categorical
0  1000      0  <NA>   True        hello        1000
1     2    128   2.5   <NA>                        2
2   300    255  <NA>  False         <NA>         300
3  <NA>     25  10.0   True  always TDD.        <NA>

df
--
    int  uint8 float   bool       string categorical
0  1000      0  <NA>   True        hello        1000
1     2    128   2.5   <NA>                        2
2   300    255  <NA>  False         <NA>         300
3  <NA>     25  10.0   True  always TDD.        <NA>

我们刚刚经历了一个从cuDF数据框架到数据框架交换对象的往返演示。然后我们看到了如何从数据框架交换对象中建立一个cuDF数据框架对象。在这一过程中,我们检查了数据的完整性。

学到的经验

多样性优势

许多研究表明,多样化团队的好处和更好的性能。我在这个项目中的经验是 CONTRIBUTING.md(旧版本)的文件,对我这个新人来说非常不清楚。根据我的导师(Kshiteej KalambarkarRalf Gommers)的建议,我开了一个问题,分享了我的想法,并不断要求澄清,最后形成了一个(合并的)PR,重组了 CONTRIBUTING.md(当前版本)文件,使其更加清晰。

因此,水平的多样性(专家、新人等......)确保了一个包容性的环境,每个人都能轻松找到自己的方向。