加速当今的 Python
为加速器架构的寒武纪爆发做好准备
Python* 凭借其多功能性和高性能,不断让许多人感到惊讶。我是一个骨灰级的 C 和 Fortran 程序员,同时对 C++ 也有深入的了解,因为它们能让我获得高性能。Python 也提供了这一点,并且具有一种使它与前面提到的语言区分开来的便利性。所以,我也是 Python 的粉丝。
Python 之所以能够提供高性能,是因为它拥有许多经过高度优化的关键库,并且支持对未预编译的关键代码进行即时编译(在运行时)。然而,当处理更大的数据集或更复杂的算法时,我的 Python 代码往往会变慢。在本文中,我们将回顾
- 为什么“异构未来”如此重要
- 在开放解决方案中我们需要解决的两个关键挑战
- 并行执行,更好地利用可用的 CPU 性能
- 使用加速器进一步提升性能
仅第三步就提供了 12 倍的加速,而当有加速器可用时,第四步能提供更多。这些易于使用的技术对于需要提高性能的 Python 程序员来说可能非常有价值。此处分享的技术使我们能够快速前进,而无需等待太久的结果。
思考“异构未来”
虽然理解异构性对于我们只想让 Python 代码运行得更快来说并不关键,但现在值得强调的是计算领域正在发生的一个重大转变。计算机每年都在变得更快。起初,巧妙且更复杂的架构推动了这些性能的提升。然后,从大约 1989 年到 2006 年,不断提高的时钟频率是关键驱动因素。突然在 2006 年,提高时钟速率不再有意义,为了提高性能,架构的改变再次变得必要。
多核处理器通过增加处理器中(同构)核的数量来提供更高的性能。与提高时钟速率不同,从多核中获得额外性能需要软件进行更改以利用新的性能。Herb Sutter 的经典文章《免费午餐结束了》强调了并发的必要性。虽然这是必要的,但这一转变使我们软件开发者的生活变得复杂。
接下来,加速器应运而生,用于通过专用设备上的计算来增强 CPU 计算。到目前为止,其中最成功的是 GPU。GPU 最初是为了将图形处理从计算机的显示输出中卸载而引入的。出现了几种编程模型来利用这种额外的计算能力,但结果不是发送到显示器,而是发送回在 CPU 上运行的程序。到目前为止,最成功的模型是 NVIDIA GPU 的 CUDA*。如今,单个系统中的(异构)处理器不再等效。然而,所有流行的编程语言通常都假设单个计算设备,因此当我们选择部分代码在不同的计算设备上运行时,会使用“卸载”一词。
几年前,两位行业巨头 John Hennessey 和 David Patterson 宣布,我们正在进入“计算机架构的新黄金时代”。由于出现了许多面向特定领域的处理器理念,异构计算正在爆炸式发展。有些会成功,很多会失败,但计算永远改变了,因为它不再是关于在单个设备上完成所有计算。
一个良好解决方案解决的两大挑战
虽然 CUDA 今天是一个流行的选择,但它仅限于 NVIDIA GPU。然而,我们需要开放的解决方案来应对来自多个供应商的新型加速器架构浪潮。运行在异构平台上的程序需要一种方法来发现运行时可用的设备。它们还需要一种方法将计算卸载到这些设备。
CUDA 假设只有 NVIDIA GPU 可用,从而忽略了设备发现。Python 用户可以选择CuPy 来利用 CUDA(NVIDIA)或 ROCm*(AMD)的 GPU;但是,虽然 CuPy 是一个不错的选择,但它并不能提高 CPU 性能,也不能泛化到其他供应商或架构。我们应该选择一种可移植到多个供应商并支持新硬件创新的编程解决方案。然而,在我们对加速器卸载感到兴奋之前,让我们确保我们充分利用了主机 CPU,因为一旦我们理解了如何实现并行化和编译代码,我们将更好地定位利用加速器中的并行化。
Numba 是 Anaconda 开发的,一个开源的、NumPy 感知的优化(即时)编译器,用于 Python。在底层,它使用 LLVM 编译器从 Python 字节码生成机器代码。Numba 可以编译大部分以数字为中心的 Python 代码,包括许多 NumPy 函数。Numba 还支持循环的自动并行化、GPU 加速代码的生成以及通用函数(ufunc)和 C 回调函数的创建。
Numba 的自动并行器由 Intel 贡献。可以通过在@numba.jit.
中设置parallel=True
选项来启用它。自动并行器分析编译函数中的数据并行代码区域,并将其调度为并行执行。Numba 可以自动并行化的操作有两种类型:
- 隐式数据并行区域,例如 NumPy 数组表达式、NumPy ufuncs、NumPy 归约函数
- 使用 numba.prange 表达式显式指定的数据并行循环
例如,考虑以下简单的 Python 循环:
def f1(a,b,c,N):
for i in range(N):
c[i] = a[i] + b[i]
通过将串行范围(range
)更改为并行范围(prange
)并添加njit
指令(njit
= Numba JIT = 编译并行版本),我们可以使其显式并行化:
@njit(parallel=True)
def add(a,b,c,N):
for i in prange(N):
c[i] = a[i] + b[i]
当我运行它时,运行时从 24.3 秒提高到 1.9 秒,但结果可能会因系统而异。要尝试一下,请克隆 oneAPI-samples 存储库(<a href="https://github.com/oneapi-src/oneAPI-samples">git clone</a>
),然后打开AI-and-Analytics/Jupyter/Numba_DPPY_Essentials_training/Welcome.ipynb
笔记本。一种简单的方法是免费注册Intel® DevCloud for oneAPI账户。
使用加速器进一步提升性能
当应用程序有足够的工作量来承担卸载的开销时,加速器可以非常有效。第一步是编译选定的计算(一个内核),以便可以将其卸载。扩展之前的示例,我们使用 Numba 数据并行扩展(numba-dpex)来指定卸载内核。(有关更多详细信息,请参阅Jupyter 笔记本训练。)
@dppy.kernel
def add(a, b, c):
i = dppy.get_global_id(0)
c[i] = a[i] + b[i]
内核代码被编译和并行化,就像之前使用@njit
在 CPU 上运行一样,但这次它已准备好卸载到设备。它被编译成一种中间语言(SPIR-V),当它被提交执行时,运行时会将其映射到设备。这为我们提供了一种供应商无关的加速器卸载解决方案。
内核的数组参数可以是 NumPy 数组或统一共享内存(USM)数组(一个明确放置在统一共享内存中的数组类型),具体取决于我们认为哪种最适合我们的编程需求。我们的选择将影响我们如何设置数据和调用内核。
接下来,我们将利用一个名为SYCL*的 C++ 解决方案,该解决方案用于开放的多供应商、多架构编程,并使用开源数据并行控制(dpctl:SYCL 的 C 和 Python 绑定)。(有关更多信息,请参阅GitHub 文档和SYCL 和 Python 接口用于 XPU 编程。)这些使得 Python 程序能够访问 SYCL 设备、队列和内存资源,并执行 Python 数组/张量操作。这避免了重复发明解决方案,减少了我们需要学习的内容,并实现了高度的兼容性。
连接到设备就像这样简单:
device = dpctl.select_default_device()
print("Using device ...")
device.print_device_info()
如果我们想在不更改此简单程序的情况下控制设备选择,则可以使用环境变量SYCL_DEVICE_FILTER
设置默认设备。dpctl
库还支持编程控制,以根据硬件属性检查和选择可用设备。
内核可以通过几行 Python 代码调用(卸载并运行)到设备上:
with dpctl.device_context(device):
dpar_add[global_size,dppy.DEFAULT_LOCAL_SIZE](a,b,c)
我们使用device_context
可以让运行时完成所有必要的数据复制(我们的数据仍然是标准的 NumPy 数组),以使其正常工作。dpctl
库还支持为设备显式分配和管理 USM 内存。这在深入优化时可能很有价值,但让运行时为标准 NumPy 数组处理这项工作非常简单,难以超越。
异步与同步
上述同步机制可以轻松支持 Python 编码风格。如果我们愿意稍作修改 Python 代码,也可以使用异步功能及其优势(减少或隐藏数据移动和内核调用的延迟)。请参阅dpctl gemv 示例中的示例代码,了解有关异步执行的更多信息。
CuPy 呢?
CuPy 是 NumPy 的一个大型子集的重新实现。CuPy 数组库充当即插即用的替代品,可以在 NVIDIA CUDA 或 AMD ROCm 平台上运行现有的 NumPy/SciPy 代码。然而,为新平台重新实现 CuPy 所需的大量编程工作是实现同一 Python 程序的多供应商支持的一个相当大的障碍,因此它无法解决前面提到的两个关键挑战。对于设备选择,CuPy 需要一个支持 CUDA 的 GPU 设备。对于内存,它对内存的直接控制很少,尽管它会自动执行内存池化以减少调用 cudaMalloc 的次数。在卸载内核时,它不提供设备选择的控制,如果没有支持 CUDA 的 GPU,它将失败。当我们使用更好的解决方案来解决异构性挑战时,我们可以为我们的应用程序获得更好的可移植性。
scikit-learn 呢?
Python 编程总体上非常适合“计算跟随数据”(compute-follows-data),并且使用已启用例程非常简单。dpctl
库支持一种张量数组类型,我们可以将其与特定设备连接。在我们的程序中,如果我们将其数据转换为设备张量(例如,dpctl.tensor.asarray(data, device="gpu:0
)),它将与设备相关联并放置在设备上。通过使用识别这些设备张量的修补版本的 scikit-learn,涉及此类张量的修补后的 scikit-learn 方法将在设备上自动计算。
利用 Python 的动态类型来感知数据的位置并将计算定向到数据所在的位置是一种绝佳的应用。我们的 Python 代码变化很少,唯一的变化是我们重新将张量转换为设备张量。根据用户迄今为止的反馈,我们预计“计算跟随数据”方法将是 Python 用户最受欢迎的模型。
开放、多供应商、多架构 – 一起学习
Python 可以成为拥抱硬件多样性力量并利用加速器即将到来的寒武纪大爆发的工具。Numba 数据并行 Python 结合dpctl
和*计算跟随数据*修补的 scikit-learn 值得考虑,因为它们与供应商和架构无关。
虽然 Numba 为 NumPy 提供了强大的支持,但我们可以考虑未来如何为 SciPy 和其他 Python 需求做得更多。Python 中数组 API 的碎片化引起了人们对 Python 数组 API 标准化的兴趣(阅读一份不错的摘要),因为人们希望与 CPU 以外的设备共享工作负载。标准的数组 API 将极大地帮助 Numba 和dpctl
等项目扩大其范围和影响力。NumPy 和 CuPy 已支持数组 API,并且dpctl
和PyTorch*都在努力采用它。随着越来越多的库朝着这个方向发展,支持异构计算(各种加速器)的任务将变得更加易于处理。
在更复杂的 Python 代码中,仅使用dpctl.device_context
不足以处理多线程或异步任务。(请参阅 GitHub issue。)在更复杂的线程 Python 代码中,最好遵循“计算跟随数据”策略。它可能成为比device_context
式编程更受欢迎的选择。
我们都有很多机会做出贡献,共同完善加速 Python 的方法。一切都是开源的,并且今天运行得相当好。
了解更多
要学习,没有什么比亲自上手尝试更好的了。以下是一些在线资源建议,供您参考。
对于 Numba 和 dpctl,有一个 90 分钟的视频讲座涵盖了这些概念:Python 的数据并行基础知识。
Jake VanderPlas(Python 数据科学手册的作者)的《告别循环:NumPy 的快速数值计算》是一个非常实用的视频,讲解了如何有效地使用 NumPy。
本文所述的异构 Python 功能均为开源,并且预装在Intel® oneAPI Base 和 Intel® AI Analytics 工具包中。支持 SYCL 的 NumPy 托管在 GitHub 上。用于内核编程和自动卸载功能的 Numba 编译器扩展也托管在GitHub上。开源数据并行控制(dpctl:SYCL 的 C 和 Python 绑定)有GitHub 文档和一篇论文《SYCL 和 Python 接口用于 XPU 编程》。这些使得 Python 程序能够访问 SYCL 设备、队列、内存,并使用 SYCL 资源执行 Python 数组/张量操作。
异常确实受支持,包括来自设备代码的异步错误。异步错误将在被异步错误处理函数重新抛出为同步异常时被拦截。这种行为得益于 Python 扩展生成器,并且社区文档在Cython和Pybind11中对此进行了很好的解释。
相关内容
快速视频
点播网络研讨会和工作坊
- 使用 NumPy 和其他更智能的 oneAPI 对应项加速 Python*
- 使用 oneAPI 实现 Python* 加速 10 倍、100 倍或更多
- 大规模 Python 数据科学:加快端到端工作流程
技术文章
获取软件
Intel® AI Analytics Toolkit
通过优化的深度学习框架和高性能 Python* 库加速端到端的机器学习和数据科学管道。
立即获取
查看所有工具