cudf.pandas#

用户指南中解释了 cuDF pandas 加速模式 (cudf.pandas) 的使用方法。本文档旨在解释快慢代理机制的工作原理,并记录可用于调试 cudf.pandas 本身的内部环境变量。

快慢代理机制#

cudf.pandas 的核心是通过 fast_slow_proxy.py 中定义的代理类型实现的,这些代理类型链接了一对“快”库和“慢”库。cudf.pandas 的工作原理是将每种“慢”类型及其对应的“快”类型封装在一个新的代理类型中,也称为快慢代理类型。这些代理类型的目的是使我们能够首先尝试在“快”对象上进行计算,如果“快”版本失败,则回退到“慢”对象。虽然核心包装功能是通用的,但当前的主要用法是使用 cuDF 和 Pandas 提供一对代理。在本文档的其余部分,为了方便思考具体的库对,我们将 cuDF 和 Pandas 分别用作“快”库和“慢”库的名称,但需要理解可以使用任何一对 API 匹配的库。例如,未来的支持可能包括 CuPy(作为“快”库)和 NumPy(作为“慢”库)等库对。

注意

  1. 我们目前没有包装整个 NumPy 库,因为它暴露了 C API。但我们确实将 NumPy 的 numpy.ndarray 和 CuPy 的 cupy.ndarray 包装在代理类型中。

  2. 定义了一个名为 custom_iter 的方法,该方法始终利用“慢”对象的 iter 方法,这样我们就不会将对象移动到 GPU 并触发错误,然后再将对象移动到 CPU 以成功执行迭代。

类型:#

被包装的类型和代理类型#

“被包装”的类型/类是已经包装成代理类型的 Pandas 和 cuDF 特定类型。被包装对象和代理对象分别是被包装类型和代理类型的实例。在下面的代码片段中,s1s2 是被包装对象,而 s3 是快慢代理对象。另请注意,模块 xpd 是一个被包装的模块,并包含 cuDF 和 Pandas 模块作为属性。要检查对象是否为代理类型,我们可以使用 cudf.pandas.is_proxy_object

import cudf.pandas
cudf.pandas.install()
import pandas as xpd

cudf = xpd._fsproxy_fast
pd = xpd._fsproxy_slow

s1 = cudf.Series([1,2])
s2 = pd.Series([1,2])
s3 = xpd.Series([1,2])

from cudf.pandas import is_proxy_object

is_proxy_object(s1) # returns False

is_proxy_object(s2) # returns False

is_proxy_object(s3) # returns True

注意

请注意,用户绝不应该以这种方式直接与被包装的对象进行交互。此代码仅用于演示目的。

不同类型的代理类型#

cudf.pandas 中,主要有两种代理类型:最终类型和中间类型。

最终代理类型和中间代理类型#

最终类型是指已知存在操作可以将“快”类型的对象转换为“慢”类型,反之亦然的类型。例如,可以使用 to_pandas 方法将 cudf.DataFrame 转换为 Pandas,可以使用 cudf.from_pandas 函数将 pd.DataFrame 转换为 cuDF。中间类型是在最终类型上调用操作的结果的类型。例如,xpd.DataFrameGroupBy 是一种中间类型,将在最终类型 xpd.DataFrame 上执行分组操作时创建。

属性和可调用代理类型#

最终代理类型通常是类或模块,它们都有属性。类还有方法。这些属性和方法也必须被包装以支持快慢代理方案。

创建新的代理类型#

_FinalProxy_IntermediateProxy 类型分别使用函数 make_final_proxy_typemake_intermediate_proxy 创建。创建新的最终类型如下所示。

DataFrame = make_final_proxy_type(
    "DataFrame",
    cudf.DataFrame,
    pd.DataFrame,
    fast_to_slow=lambda fast: fast.to_pandas(),
    slow_to_fast=cudf.from_pandas,
)

回退机制#

代理调用通过 _fast_slow_function_call 实现带回退的功能。这实现了我们先尝试使用“快”方式 (使用 cuDF) 执行操作,失败时回退到“慢”方式 (使用 Pandas) 的机制。该函数如下所示

def _fast_slow_function_call(func: Callable, *args, **kwargs):
    try:
        ...
        fast_args, fast_kwargs = _fast_arg(args), _fast_arg(kwargs)
        result = func(*fast_args, **fast_kwargs)
        ...
    except Exception:
        ...
        slow_args, slow_kwargs = _slow_arg(args), _slow_arg(kwargs)
        result = func(*slow_args, **slow_kwargs)
        ...
    return _maybe_wrap_result(result, func, *args, **kwargs), fast

正如我们所见,该函数尝试使用 cuDF 以“快”方式调用 func,如果发生任何 Exception,则使用 Pandas 调用该函数。本质上,这个 try-except 块使得 cudf.pandas 能够支持大部分 Pandas API。

最后,如果需要,该函数会将来自任一路径的结果包装在快慢代理对象中。

转换代理对象#

请注意,在调用 func 之前,代理对象及其属性需要转换为其 cuDF 或 Pandas 实现。此转换在函数 _transform_arg 中处理,_fast_arg_slow_arg 都会调用此函数。

_transform_arg 是一个递归函数,它将根据传递给它的类型或参数调用自身(例如,对参数列表中的每个元素调用 _transform_arg)。

使用元类#

cudf.pandas 使用一个名为 (_FastSlowProxyMeta) 的元类来查找快慢代理类型的类属性和类方法。例如,在下面的代码片段中,xpd.Series 类型是 _FastSlowProxyMeta 的一个实例。因此我们可以访问在元类中定义的属性 _fsproxy_fast

import cudf.pandas
cudf.pandas.install()
import pandas as xpd

print(xpd.Series._fsproxy_fast) # output is cudf.core.series.Series

调试 cudf.pandas#

可以使用几个环境变量进行调试。

设置环境变量 CUDF_PANDAS_DEBUGGING 会在 cuDF 和 Pandas 的结果不同时产生警告。例如,下面的代码片段会产生如下警告。

import cudf.pandas
cudf.pandas.install()
import pandas as pd
import numpy as np

setattr(pd.Series.mean, "_fsproxy_slow", lambda self, *args, **kwargs: np.float64(1))
s = pd.Series([1,2,3])
s.mean()
UserWarning: The results from cudf and pandas were different. The exception was
Arrays are not almost equal to 7 decimals
 ACTUAL: 1.0
 DESIRED: 2.0.

设置环境变量 CUDF_PANDAS_FAIL_ON_FALLBACK 会导致 cudf.pandas 在从 cuDF 回退到 Pandas 时失败。例如,

import cudf.pandas
cudf.pandas.install()
import pandas as pd
import numpy as np

df = pd.DataFrame({
    'complex_col': [1 + 2j, 3 + 4j, 5 + 6j]
})

print(df)
ProxyFallbackError: The operation failed with cuDF, the reason was <class 'NotImplementedError'>: Series with Complex128DType is not supported.