将 Python 和 Rust 融合在一起,为 pyQuil® 4.0 带来和谐

2024-01-03 19:04

本文主要是介绍将 Python 和 Rust 融合在一起,为 pyQuil® 4.0 带来和谐,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

在这里插入图片描述

文章目录

    • 前言
    • 设定方向
    • 从 Rust 库构建 Python 软件包
    • 改装 pyQuil
    • 异步困境
    • 回报:功能和性能
    • 结论

前言

pyQuil 一直是在 Rigetti 量子处理单元(QPUs)上构建和运行量子程序的基石,通过我们的 Quantum Cloud Services(QCS™)平台提供服务。它是我们的一个重要客户端库。然而,随着 QCS 平台的发展,我们越来越倾向于使用 Rust,因为它具有出色的性能、类型系统和强调正确性。为了支持Rigetti 不断增长的 Rust 工具和服务生态系统,pyQuil 中的许多功能已被我们的 Rust 库取代。幸运的是,Rust 很适合用作外部函数接口(FFI)。这对我们来说是 Rust 的另一个重要优势,因为它是在我们的服务和高级语言(如 Python)或低级语言(如 C)之间架设桥梁的理想选择。

我们仍然致力于支持 Python 和 pyQuil,因此我们花了过去一年的时间用我们现代的 Rust SDKs 改装了 pyQuil。这对 pyQuil 进行了基础性的更改,以一种透明的方式为用户带来了 Rust 的好处,并为在 Rigetti 的第四代 QPUs 上编译和运行程序提供了所需的增强功能。您可以在我们的 “Introducing pyQuil v4” 指南中了解有关主要更改的详细信息。在本文的其余部分,我们将讨论在 Python 中集成 Rust 时遇到的一些挑战和突破。

设定方向

在继续之前,让我们明确集成我们的 Rust SDKs 与 pyQuil 所需的两个主要目标:

在我们现有的 Rust 库之上构建 Python 软件包,而不损害这些 Rust 库的设计或惯用“Rustiness”。

将这些软件包合并到 pyQuil 中,同时最小化对现有API和行为的破坏性更改。

从 Rust 库构建 Python 软件包

我们知道我们希望我们的 Rust 库保持纯粹的 Rust 库,不包含任何 Python 特定的代码或类型。相反,我们希望确保我们的 Python 软件包符合 Python 开发人员的期望。这些目标是相互冲突的,因此很明显前进的最有效方式是保持我们的 Rust crate 中的核心逻辑,并构建一个具有 Rust 绑定的 Rust 软件包的单独 crate。

我们决定使用 PyO3 crate 作为在 Rust 中构建 Python 软件包的首选框架。它被广泛使用并有很好的文档。pyo3 提供了许多宏,可以用于包装您的 Rust 代码并将其公开为 Python 对象。这些宏注释了类型和函数的定义,但在尝试从外部 crate 中的类型构建 Python 软件包时,它们的实用性受到限制。

典型的解决方法涉及在外部类型周围创建 newtype 包装器,但这会导致繁琐的样板代码。例如,newtype 包装器缺乏使用 pyo3 生成 getter 和 setter 属性的便利性。相反,使用 newtype 包装器需要手动实现。

quil-rs 中的这个例子说明了这个问题。在 Quil 中,一个 EXCHANGE a b 指令交换内存引用 a 和 b 中的值。这在 quil-rs 中使用 MemoryReference 和 Exchange 结构表示:

pub struct MemoryReference {pub name: String,pub index: u64
}pub struct Exchange {pub left: MemoryReference,pub right: MemoryReference
}

如果我们直接用 PyO3 包装这个结构,我们将使用 pyclass 和 pyo3 属性将 ExchangeMemoryReference 分别包装为 Python 类,完全具有它们的字段的 gettersetter

use pyo3::pyclass;#[pyclass(get_all, set_all)]
pub struct MemoryReference {pub name: String,pub index: u64
}#[pyclass(get_all, set_all)]
pub struct Exchange {pub left: MemoryReference,pub right: MemoryReference
}

虽然方便,但这种方法需要将 Python 特定的代码和依赖项注入我们的 Rust库,从而破坏其纯度。但是,我们应该如何处理外部 crate 的代码呢?

首先,我们必须围绕外部类型创建 newtype 包装器,以将 #[pyclass] 属性应用于它们:

use quil_rs::instruction::{Exchange, MemoryReference};
use pyo3::prelude::*;#[pyclass(name = "MemoryReference")]
pub struct PyMemoryReference(MemoryReference);#[pyclass(name = "Exchange")]
pub struct PyExchange(Exchange)

接下来,由于我们不能在新类型包装器上使用 get_all 和 set_all 访问 MemoryReferenceExchange 的内部字段,我们必须为内部类型的每个字段手动实现 getter 和 setter:

#[pymethods]
impl PyMemoryReference {#[getter]fn get_name(self) -> String { ... }#[setter]fn set_name(self, name: String) -> PyResult<()> { ... }#[getter]fn get_index(self) -> u64 { ... }#[setter]fn set_index(self, index: u64) -> PyResult<()> { ... }
}#[pymethods]
impl PyExchange {#[getter]fn get_left(self) -> MemoryReference { ... }#[setter]fn set_left(self, memory_reference: PyMemoryReference) -> PyMemoryReference { ... }#[getter]fn get_right(self) -> MemoryReference { ... }#[setter]fn set_right(self, memory_reference: PyMemoryReference) -> PyMemoryReference { ... }
}

这种方法牺牲了 PyO3 提供的许多便利性,容易出错,并且显著增加了维护构建在外部 Rust crate 上的 Python 软件包所需的样板代码。对于我们来说,这是一个重大问题,特别是因为 quil-rs 在很大程度上依赖于 Rust 的类型系统来表示 Quil 程序。

如果我们能够同时拥有两个世界的最佳优势呢?这就是 rigetti-pyo3 的目标,这是我们构建的一个开源库,通过引入 traits 和宏,大大减少了构建围绕外部 Rust 类型的 Python 软件包所需的样板代码。使用 rigetti-pyo3,我们可以使用 py_wrap_data_struct! 宏生成 newtype 包装器,包含每个字段的 getter 和 setter。我们所需做的就是指定字段、预期的 Rust 类型以及用于转换的 Python 兼容类型:

py_wrap_data_struct! {PyMemoryReference(MemoryReference) as "MemoryReference" {name: String => Py<PyString>,index: u64 => Py<PyInt>}
}py_wrap_data_struct! {PyExchange(Exchange) as "Exchange" {left: MemoryReference => PyMemoryReference,right: MemoryReference => PyMemoryReference}
}

“rigetti-pyo3”包含一系列宏,使得利用基本类型的 trait 实现变得轻而易举,从而实现 Python 方法。例如,impl_hash! 宏利用包装的 Rust 类型上的 Hash 实现,在包装类型上实现了 Python 的 __hash__ 方法。

这些宏的存在不仅减少了样板代码,而且通过确保每个绑定都以相同的方式实现常见功能,使得 Python API 更加一致。py_wrap_union_enum! 宏就是一个很好的例子,它用简单的 API 包装了一个带标签的联合(或 Rust 枚举的变体),用于构造和与 Rust 枚举交互的 Python 类。

“rigetti-pyo3”已经被证明是在外部 Rust crate 上构建 Python 软件包的宝贵框架。它使我们能够在 Rust 库和相应的 Python 库之间建立无缝的集成,而无需在任一设计中进行妥协。

改装 pyQuil

尽管 pyQuil 和我们的 Rust 库解决了一些共同的问题,但它们的解决方案在许多情况下是非常不同的。它们的方法在许多情况下相似,但也存在很大的灵活性。总的来说,从我们的 Rust 库中添加新功能到 pyQuil 并不是一个挑战,因为我们可以自由选择如何将它们整合。然而,在 pyQuil 具有更多功能的情况下,我们通常不得不将其迁移到我们的 Rust 库中。在这里需要谨慎决策,我们希望回溯任何必要的功能以提供完整而一致的 API,但与此同时,我们不希望过多地将 pyQuil 特定的功能移植回我们的 Rust SDKs。

另一个挑战是如何在不破坏我们的 Rust SDKs API 的情况下满足 pyQuil 现有 API 的期望。其中之一涉及 asyncio 和 pyQuil 不支持 asyncio 的问题。

异步困境

我们的 Rust API 的大部分涉及与外部服务进行网络交互,这些任务自然适合异步 Rust。虽然 pyo3 本身不直接支持异步函数,但出色的 pyo3-asyncio 使将异步 Rust 函数公开为 Python asyncio 函数变得轻而易举。然而,pyQuil 在其自己的 API 中不使用 asyncio,并且使用这些 asyncio 函数的原样本需要在 pyQuil 的许多核心方法上引入 async 关键字。这将要求用户也采用 asyncio,这是我们不愿意做出的重大更改。

起初,我们尝试通过手动调用 asyncio 事件循环 API 以同步函数中运行将异步 Rust 绑定导出到 Python 中。这条路没有走得很远,对这个想法的所有变体都是可疑的。最终,没有一个在同步和异步上下文中都表现良好。

相反,如果我们将所有异步机制推到 Rust 运行时中会怎么样?这也带来了一系列挑战。首先,我们想确保我们适当地处理操作系统信号。用户经常希望通过按 Ctrl-C 来中止运行时间较长的函数,这会向运行中的程序发送 SIGINT 信号。在 Python 程序的情况下,运行中的 Python 解释器需要处理这些信号,这意味着在 Rust 掌控时,信号不会被处理。pyo3 文档记录了这个陷阱,这是我们在试图将潜在的长时间运行的异步函数变为同步函数时需要注意的事项。在所有这一切中,还有一个复杂的问题是 Python API 函数 PyErr_CheckSignals() 必须在主线程上调用,否则调用将是一个空操作。

总的来说,我们需要包装一个异步 Rust 函数,使其在 Python 中呈现为同步函数,同时确保在主线程上处理信号,以便尊重操作系统信号。

让我们来做吧。给定一个虚构的异步 Rust 函数 foo

async fn foo() -> String {tokio::time::sleep(Duration::from_secs(3));"hello".to_string()
}

使用 pyo3_asyncio,我们可以将其导出为一个 asyncio 函数:

#[pyfunction]
fn py_foo_async(py: Python<'_>) -> PyResult<&PyAny> {pyo3_asyncio::tokio::future_into_py(py, async { Ok(foo().await) })
}

但是,我们如何将其包装成同步 API 呢?首先,我们获取当前的运行时,然后将我们的异步函数作为任务在该运行时上启动。然后,我们可以使用 tokio::select! 来管理从我们的任务返回的结果,或从信号处理程序返回的结果,以先返回的为准。将所有这些都包装在当前运行时中,然后,大功告成!我们有一个在幕后使用 Rust 的异步运行时的同步 Python 函数:

#[pyfunction]
fn py_foo_sync() -> PyResult<String> {let runtime = pyo3_asyncio::tokio::get_runtime();let handle = runtime.spawn(foo());runtime.block_on(async {tokio::select! {result = handle => result.map_err(|err| pyo3::exceptions::PyRuntimeError::new_err(err.to_string())),signal_err = async {let delay = std::time::Duration::from_millis(100);loop {Python::with_gil(|py| {py.check_signals()})?;tokio::time::sleep(delay).await;}} => signal_err}})
}

这很好,但对于每个异步函数都做这么多事情太多了。为了每个异步函数在我们的 API 中都重复这个设置,我们可以使用一个宏。

macro_rules! py_sync {($py: ident, $body: expr) => {{$py.allow_threads(|| {let runtime = ::pyo3_asyncio::tokio::get_runtime();let handle = runtime.spawn($body);runtime.block_on(async {tokio::select! {result = handle => result.map_err(|err| ::pyo3::exceptions::PyRuntimeError::new_err(err.to_string()))?,signal_err = async {let delay = ::std::time::Duration::from_millis(100);loop {::pyo3::Python::with_gil(|py| {py.check_signals()})?;::tokio::time::sleep(delay).await;}} => signal_err,}})})}};
}

我们宏的一个补充是我们如何将所有东西都包装在 py.allow_threads 中。这释放了全局解释器锁(GIL),以便在进行纯 Rust 工作时其他 Python 线程可以运行。我们只有在需要使用 Python::with_gil 检查 OS 信号时才重新获取 GIL。

现在,对于任何异步函数,我们只需写:

#[pyfunction]
fn py_foo(py: Python<'_>) -> PyResult<String> {py_sync!(py, async { Ok(foo().await) })
}

这也很好,但我们可以走得更远。这些同步函数对于兼容性来说是很好的,但一些用户可能会喜欢一个真正的 asyncio API。这就是为什么我们建立了另一个建立在上一个基础上的宏,用于提供单个 async 函数的同步和异步变体。这让我们在其自然的 async 形式中编写函数一次,并免费获得同步和异步变体。

// 这会生成两个Python函数:
//  def foo(): ...
//  async def foo(): ...
py_sync::py_function_sync_async! {#[pyfunction]async fn foo() -> PyResult<String> {Ok(foo().await)}
}

能够继续支持同步 API,同时不错过提供异步 API 的机会,对我们来说是一个巨大的胜利,也是将 Rust 与 Python 结合在一起能够带来的不易通过单独使用 Python 实现的好处的一个很好的例子。

回报:功能和性能

我们已经确定了在以不妥协任一库的质量或用户体验为代价的方式下,将现有的 Python 和 Rust 库之间的差距缩小的挑战。那么这给我们带来了什么?

如前所述,我们的 Rust 库已经开始在功能上超越 pyQuil。最重要的是,它们带来了在 Rigetti 的下一代 Ankaa 系统上编译和运行程序所需的增强功能。

此外,通过将解析和序列化 Quil 程序的逻辑、以编程方式构建它们以及执行和检索作业结果的逻辑集中到我们的 Rust 库中,我们已经为 pyQuil 现在和将来构建了一个坚实的基础。在我们的服务和客户端库中使用相同的逻辑,使我们更容易维护和扩展 pyQuil,同时为用户提供更一致的体验。

最后,我们不能结束一篇关于 Python 和 Rust 的博客文章,而不提到性能。通过将核心逻辑移植到 Rust,我们在许多方面看到了显著的性能提升,比如解析和序列化 Quil 程序。这是至关重要的,因为解析和序列化是 pyQuil 中常见的编译和执行工作流程中的关键步骤。

方法论:所有基准测试都使用 Python 3.8 在装有 M1 Max 的 2021 年 MacBook Pro 上执行。测试加载了一个大型的 Quil 程序文件,并对逐渐增大的程序块进行解析的基准测试。数据使用 pytest-benchmark 进行收集。

结论

将 Python 和 Rust 组合到 pyQuil v4 中提出了许多挑战。从构建在我们现有的 Rust 库之上而不妥协其设计的初步决策,到在不引入破坏性变更的情况下满足长时间 pyQuil 用户的期望,我们走过了一条复杂的道路。通过这些努力,我们现代化了 pyQuil,为用户提供了 Rust 的性能和类型安全性的好处,同时保持了 Python 的熟悉性和易用性。

这不仅仅是将两种语言结合在一起的技术问题。它还涉及到在两者之间找到平衡,以提供一致的用户体验,并为库的未来扩展奠定基础。通过解决这些问题,我们为 pyQuil 带来了一种令人满意的融合,展示了 Python 和 Rust 之间合作的潜力,以解决量子计算领域的挑战。

这篇关于将 Python 和 Rust 融合在一起,为 pyQuil® 4.0 带来和谐的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/566741

相关文章

使用Python绘制蛇年春节祝福艺术图

《使用Python绘制蛇年春节祝福艺术图》:本文主要介绍如何使用Python的Matplotlib库绘制一幅富有创意的“蛇年有福”艺术图,这幅图结合了数字,蛇形,花朵等装饰,需要的可以参考下... 目录1. 绘图的基本概念2. 准备工作3. 实现代码解析3.1 设置绘图画布3.2 绘制数字“2025”3.3

python使用watchdog实现文件资源监控

《python使用watchdog实现文件资源监控》watchdog支持跨平台文件资源监控,可以检测指定文件夹下文件及文件夹变动,下面我们来看看Python如何使用watchdog实现文件资源监控吧... python文件监控库watchdogs简介随着Python在各种应用领域中的广泛使用,其生态环境也

Python中构建终端应用界面利器Blessed模块的使用

《Python中构建终端应用界面利器Blessed模块的使用》Blessed库作为一个轻量级且功能强大的解决方案,开始在开发者中赢得口碑,今天,我们就一起来探索一下它是如何让终端UI开发变得轻松而高... 目录一、安装与配置:简单、快速、无障碍二、基本功能:从彩色文本到动态交互1. 显示基本内容2. 创建链

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

python 字典d[k]中key不存在的解决方案

《python字典d[k]中key不存在的解决方案》本文主要介绍了在Python中处理字典键不存在时获取默认值的两种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录defaultdict:处理找不到的键的一个选择特殊方法__missing__有时候为了方便起见,

使用Python绘制可爱的招财猫

《使用Python绘制可爱的招财猫》招财猫,也被称为“幸运猫”,是一种象征财富和好运的吉祥物,经常出现在亚洲文化的商店、餐厅和家庭中,今天,我将带你用Python和matplotlib库从零开始绘制一... 目录1. 为什么选择用 python 绘制?2. 绘图的基本概念3. 实现代码解析3.1 设置绘图画

Python pyinstaller实现图形化打包工具

《Pythonpyinstaller实现图形化打包工具》:本文主要介绍一个使用PythonPYQT5制作的关于pyinstaller打包工具,代替传统的cmd黑窗口模式打包页面,实现更快捷方便的... 目录1.简介2.运行效果3.相关源码1.简介一个使用python PYQT5制作的关于pyinstall

使用Python实现大文件切片上传及断点续传的方法

《使用Python实现大文件切片上传及断点续传的方法》本文介绍了使用Python实现大文件切片上传及断点续传的方法,包括功能模块划分(获取上传文件接口状态、临时文件夹状态信息、切片上传、切片合并)、整... 目录概要整体架构流程技术细节获取上传文件状态接口获取临时文件夹状态信息接口切片上传功能文件合并功能小

python实现自动登录12306自动抢票功能

《python实现自动登录12306自动抢票功能》随着互联网技术的发展,越来越多的人选择通过网络平台购票,特别是在中国,12306作为官方火车票预订平台,承担了巨大的访问量,对于热门线路或者节假日出行... 目录一、遇到的问题?二、改进三、进阶–展望总结一、遇到的问题?1.url-正确的表头:就是首先ur

基于Python实现PDF动画翻页效果的阅读器

《基于Python实现PDF动画翻页效果的阅读器》在这篇博客中,我们将深入分析一个基于wxPython实现的PDF阅读器程序,该程序支持加载PDF文件并显示页面内容,同时支持页面切换动画效果,文中有详... 目录全部代码代码结构初始化 UI 界面加载 PDF 文件显示 PDF 页面页面切换动画运行效果总结主