数据科学

在 Python 中加速与 Numba 和 Dask 的投资组合构建

Python 对数据科学家来说并不陌生。它是最流行的计算机语言,广泛用于各种任务。尽管 Python 在运行时解释代码时速度非常慢,但对于某些数据科学工作,许多流行的库使其在 GPU 上高效运行。例如,TensorFlowPyTorch等流行的深度学习框架可以帮助 AI 研究人员高效地运行实验。然而,在某些领域,如投资组合优化,没有 Python 库可以轻松加速计算工作。开发人员必须从头开始实现算法,才能在 GPU 上加速。

在这篇文章中,我们将展示如何使用 Numba 和Dask将投资组合构建算法加速 800x ,如以前的博客中介绍的那样。

介绍工具+用例

Numba 是一个 cuDF 库,它简化了 GPU 算法与 Python 的实现。 Python GPU 内核可以编译为在 GPU 上运行。它使 CUDA 的编写更易于 Python 开发人员使用。对于不适合单个 GPU 的较大问题,我们使用Dask在 GPU 的集群中进行分布式计算。 Dask 与 Python 、 pandas 、 cuDF 、 CuPy 等 Python 库集成良好。它使用相同的 API 和数据结构,因此 Python 开发人员可以轻松地选择它。


投资组合构建算法用于计算构建投资组合的最佳权重。这是基金经理管理资产必须执行的最重要步骤之一。如图 1 所示,以前的博客中的用例包括以下步骤:加载资产每日价格的 csv 数据

  • 运行 block bootstrap 生成 100k 个不同的场景。
  • 计算每个场景的日志返回。
  • 计算资产距离以运行分层聚类和资产的分层风险平价( HRP )权重
  • 根据天真的风险平价( NRP )方法计算资产的权重。
  • 根据重新平衡日的权重调整计算交易成本。
  • 在每个再平衡日期,计算投资组合杠杆以达到波动率目标。
  • 计算这两种方法( HRP-NRP )的平均年回报率、标准回报率、夏普比最大资金回挫平静比绩效指标。

图 1 :投资组合构造算法的计算图。

前面的计算涉及很多步骤。为了在 GPU 上加速它们,我们需要确定的最重要的事情是并行的粒度。

有些步骤,如HRP 算法,本质上是串行的。它重新组织股票回报的协方差矩阵,以便将类似的投资放在一起。然后,基于聚类协方差通过递归二分法进行分配。我们 MIG ht 能够对 HRP 算法的几个步骤进行矢量化,但是,由于只使用了较少数量的并行线程,因此速度提升将是最小的。

由于生成的场景彼此独立且数量庞大,因此在场景级别应该有一种更好的并行方法。使用多个 GPU 线程计算不同的场景。我们将详细描述如何在 Numba GPU 内核中执行块引导计算。

Bootstrapped 数据集用于解释时间序列未来收益的非平稳性。通过使用替换块对数据块进行采样来重构与原始时间序列长度相同的时间序列,从而构建新的收益时间序列。每个区块都有一个固定的长度,但从期货收益时间序列中定义了一个随机的时间起点。我们选择使用 60 个工作日的区块。这种区块长度是由基于规则的动态策略的典型月度或季度再平衡频率以及在此时间尺度上发生的经验市场动态所驱动。Papenbrock 和 Schwendner( 2015 )发现多资产相关性模式以几个月的典型频率变化。

Use the real market data as reference, a new time series (Sample 1) is constructed by sampling from it. Different colors indicate different time blocks. We use 60 business days in this example.
图 2 :通过 Boomstrap 方法创建合成市场数据

下面是从参考价格矩阵中采样块的 Numba 内核。请注意“@ CUDA . jit ” decorator ,它告诉 Numba 及时编译boot_strap内核。

@cuda.jit
def boot_strap(result, ref, block_size, num_positions, positions):
    sample, assets, length = result.shape
    i = cuda.threadIdx.x
    sample_id = cuda.blockIdx.x // num_positions
    position_id = cuda.blockIdx.x % num_positions
    sample_at = positions[cuda.blockIdx.x]
    for k in range(i, block_size*assets, cuda.blockDim.x):
        asset_id = k // block_size
        loc = k % block_size
        if (position_id * block_size + loc + 1 < length):
            result[sample_id, asset_id, position_id * block_size +
                   loc + 1] = ref[asset_id,  sample_at + loc]

由于股票价格是非平稳的,股票价格时间序列数据首先转换为对数收益。它用作参考矩阵,我们在其中对随机块进行采样。它是核函数中的’ ref ‘参数,其维度为[assets , time]。“ result ”参数是引导抽样的结果,其维度为[sample , assets , time]。“ block _ size ”定义了块的大小,该大小为 60 个工作日` num _ positions `是覆盖整个时间长度所需的块数。其计算公式为:

num_positions = (length - 2) // block_size + 1

` sample _ positions `是范围为[0 , length – block _ size]的随机时间数组,表示块的随机采样开始时间` sample _ positions ‘的大小为’ samples * num _ positions ‘。对于每个样本,我们需要对块的’ num _ positions ‘进行采样以覆盖整个时间长度,因此每个块都可以通过一个元组( sample _ id , position _ id )进行标识,其中 position _ id 在[0 , num _ positions]范围内。

我们将每个 GPU 线程块映射到由元组( sample _ id , position _ id )标识的不同采样时间块。下面是将线程块 id CUDA . blockIdx . x 映射到块 id 元组(示例 id 、位置 id )的公式。

   sample_id = cuda.blockIdx.x // num_positions
   position_id = cuda.blockIdx.x % num_positions

线程块内的线程用于将数据元素移动到结果矩阵。需要移动的数据元素有两个维度:资源和块内的时间位置。下面是将线程 id 映射到资源 id 和时间位置的公式。

  asset_id = k // block_size
  loc = k % block_size

要启动 Numba 内核,我们可以使用以下 Python 方法:


boot_strap[(number_of_blocks,), (number_of_threads,)](output,
                                                      ref,
                                                      block_size,
                                                      num_positions,
                                                      sample_positions)

将学到的经验教训应用到管道的其余部分

其余的计算步骤遵循与块引导步骤类似的模式。例如,在计算协方差距离时,我们确定每个再平衡时间段彼此独立。因此,并行的粒度处于引导场景和重新平衡间隔的级别。我们将线程块 id 映射到样本 id 和重新平衡时间。线程块中的线程用于移动数据元素和进行并行计算(并行 CUDA 求和、排序等)。要查看实施的详细信息,请参阅 github 回购协议:https://github.com/NVIDIA/fsi-samples/tree/main/gQuant/plugins/hrp_plugin

Numba适用于在单个 GPU 中加速算法。我们可以在单个V100 32G GPU 中并行运行4096个场景。但是,要运行10万个场景,它无法在单个 GPU 中运行。Dask是一个 Python 库,可用于加速 GPU 群中的算法。如前所示,我们已经实现了一个函数,该函数在 GPU CuPy数组中输出与 NumPy 数组类似的采样场景。首先,我们使用RAPIDS cuDF 库以零拷贝方式将其转换为 GPU 数据帧。Dask提供了一个“Dask.delayed”method,可以对该函数进行注释,以便将其构造成Dask计算图。通过调用这个延迟函数100k/4096次,我们可以通过’dask.DataFrame.from_delayed’方法将结果作为dask_ZBK9]数据帧。注意,这只会延迟地构造Dask计算图,计算由“compute”或“persist”方法触发。以下步骤将此dask_ZBK9]数据帧作为输入,并使用“dask.map_partition”方法对每个数据帧分区进行计算,就像它们正在处理单个 cuDF 数据帧一样。Dask可以智能地调度 GPU 资源,以并行完成所有计算。

结论

在这篇文章中,我们描述了如何用 Numba / Dask 实现一个投资组合构造算法。此方法在 GPU 上提供高达 800 倍的速度提升,这对慕尼黑再保险公司产生了重大的业务影响。我们使用了类似的 Numba / Dask 方法来加速 GPU 上的回溯测试算法,这帮助我们赢得了STAC A3 基准。希望这篇文章能启发您重新审视现有代码,并开始思考如何在 GPU 上加速它。

参照

 
 

 

Tags