将 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 之间合作的潜力,以解决量子计算领域的挑战。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/600461.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Oracle-视图与索引

视图 简介 视图是一种虚表 视图建立在已有表的基础上&#xff0c;视图赖以建立的的这些表成为基表 向视图提供的数据的内容的语句的select 语句&#xff0c;可以将视图理解为存储起来的select 语句 视图向用户提供基表数据的另外一种表现形式 视图的好处 控制数据访问 …

c# windows10大小端试

测试代码&#xff1a; unsafe public void ceshi() {byte[] by BitConverter.GetBytes(0x12345678);Debug.WriteLine(" byte[0] 0x" by[0].ToString("x2"));Debug.WriteLine(" byte[1] 0x" by[1].ToString("x2"));Debug.WriteLi…

QT翻金币

QT翻金币 在B站跟着视频进行QT学习&#xff0c;现把代码全部贴上来&#xff0c;备忘 整体解决方案文件结构如下&#xff1a; chooselevelscene.h #ifndef CHOOSELEVELSCENE_H #define CHOOSELEVELSCENE_H#include <QMainWindow> #include"playscene.h"class…

【Linux操作系统】探秘Linux奥秘:shell 编程的解密与实战

&#x1f308;个人主页&#xff1a;Sarapines Programmer&#x1f525; 系列专栏&#xff1a;《操作系统实验室》&#x1f516;诗赋清音&#xff1a;柳垂轻絮拂人衣&#xff0c;心随风舞梦飞。 山川湖海皆可涉&#xff0c;勇者征途逐星辉。 目录 &#x1fa90;1 初识Linux OS &…

windows下使用PowerShell切割大数据文件

测试文件为24.4G文件 打开PowerShell窗口&#xff0c;使用以下命令 $filePath 为指向文件路径 $outputPath 输出到指定文件夹 $chunkSize 单个文件控制切割大小 将命令修改完后&#xff0c;直接粘贴到powershell窗口&#xff0c;点击回车即可进行切割 $filePath "D:\…

C#编程-使用集合

使用集合 您学习了如何使用数组来有效地存储和操作相似类型额数据。但是,以下限制于数组的使用相关联: 您必须在声明时定义数组的大小。您必须编写代码以对数组执行标准操作,如排序。让我们思考一个示例。假设您想要存储在组织工作的五个雇员的姓名。您可以使用以下语句来声…

使用OHOS SDK构建assimp

参照OHOS IDE和SDK的安装方法配置好开发环境。 从github下载源码。 执行如下命令&#xff1a; git clone https://github.com/assimp/assimp.git进入源码所在的目录&#xff0c;创建批处理文件ohos_build.cmd&#xff0c;内容如下&#xff1a; echo off setlocalset OHOS_S…

多级缓存、OpenResty缓存、Redis分布式缓存、进程缓存

目录标题 一、预期表现二、环境配置1、nginx环境2、OpenResty环境3、redis环境3.1 安装redis3.2 配置启动命令3.3 配置主从3.4 哨兵 4、进程缓存环境 三 、主要编码工作3.1、缓存主要问题解决3.1.1 缓存穿透3.1.2 缓存雪崩3.1.3 缓存击穿 3.2、OpenResty编码3.2.1 openresty/ng…

RabbitMQ快速入门(详细)

RabbitMQ 消息中间件/消息队列 1、消息中间件 1、简介 **消息中间件也可以称消息队列&#xff0c;是指用高效可靠的消息传递机制进行与平台无关的数据交流&#xff0c;并基于数据通信来进行分布式系统的集成。**通过提供消息传递和消息队列模型&#xff0c;可以在分布式环境…

13. 强化学习编程实验1-在格子世界中寻宝(1)

文章目录 1.实验目的2.任务描述3.任务分析3.1 待求问题是多步决策问题否3.2 问题求解过程是一个马尔科夫决策过程3.3 状态空间S的确定3.4 动作空间A的确定3.5 状态转移概率P的确定3.6 立即回报R的确定3.7 折扣 γ \gamma γ的确定 4. 编程架构4.1 程序中有哪些对象和类4.2 环境…

深入理解Word Embeddings:Word2Vec技术与应用

目录 前言1 Word2Vec概述2 CBOW模型2.1 CBOW模型简介2.2 基于词袋&#xff08;bag of word&#xff09;的假设2.3 One-hot向量编码2.4 分类问题 3 Skip-gram模型3.1 Skip-gram模型简介3.2 目标词预测上下文3.3 词语关联性的捕捉 4 优化Word2Vec模型的方法4.1 负采样和分层softm…

MyBatis-Plus框架学习笔记

先赞后看&#xff0c;养成习惯&#xff01;&#xff01;&#xff01;❤️ ❤️ ❤️ 文章码字不易&#xff0c;如果喜欢可以关注我哦&#xff01; ​如果本篇内容对你有所启发&#xff0c;欢迎访问我的个人博客了解更多内容&#xff1a;链接地址 MyBatisPlus &#xff08;简称…

获取线程池中任务执行数量

获取线程池中任务执行数量 通过线程池进行任务处理&#xff0c;有时我们需要知道线程池中任务的执行状态。通过ThreadPoolExecutor的相关API实时获取线程数量&#xff0c;排队任务数量&#xff0c;执行完成线程数量等信息。 实例 private static ExecutorService es new Thr…

Spring Boot 生产就绪中文文档-下

本文为官方文档直译版本。原文链接 由于篇幅较长&#xff0c;遂分两篇。上半部分中文文档 Spring Boot 生产就绪中文文档-下 度量标准入门受支持的监控系统AppOpticsAtlasDatadogDynatracev2 API自动配置手动配置 v1 API (旧版)与版本无关的设置 ElasticGangliaGraphiteHumioIn…

AUTOSAR从入门到精通-漫谈autosar软件架构(五)

目录 前言 原理 Autosar软件架构 BSW层中的四个分层 虚拟功能总线VFB及运行环境RTE

推荐几个免费的HTTP接口Mock网站和工具

在前后端分离开发架构下&#xff0c;经常遇到调用后端数据API接口进行测试、集成、联调等需求&#xff0c;比如&#xff1a; &#xff08;1&#xff09;前端开发人员很快开发完成了UI界面&#xff0c;但后端开发人员的API接口还没有完成&#xff0c;不能进行前后端数据接口对接…

Java内存泄漏问题分析

内存泄漏也是一个老八股文了&#xff0c;下面来看看实际项目中内存泄漏的场景分析 时间回到9月某一天 分析阶段一 现象&#xff1a;在当时各种请求在那段时间响应很慢&#xff0c;特别是 kafka异步消费线程 不足点&#xff1a;当时主业务基本不可用&#xff0c;有点急&#…

线特征_LSD直线检测算法和LBD直线描述子

目录 1. 线特征简介 2. LSD直线检测算法 3. LBD直线描述算法 主要参考:2022 硕士论文 南京邮电大学 点线融合的单目语义视觉SLAM 郭强 1. 线特征简介 线特征优点: 线特征在于具有天然的光照及视角不变性,同时更高级的特征也使追踪的鲁棒性和准确性有所提高。特别是在特…

负责任的人工智能与人机环境系统智能

负责任的人工智能是指在人工智能系统的设计、开发、管理、使用和维护过程中&#xff0c;所有相关的角色&#xff08;包括设计者、开发者、管理者、使用者、维护者等等&#xff09;都承担其行为的道义、法律和社会责任。这意味着这些角色需要确保人工智能系统的设计与使用符合伦…

Mathtype7.4安装与嵌入WPS

文章目录 Mathtype安装教程&#xff08;7.4&#xff09;Mathtype简介Mathtype下载安装软件下载软件安装运行MathType.exe运行注册表 Mathtype嵌入wps Mathtype安装教程&#xff08;7.4&#xff09; Mathtype简介 MathType是一款强大的数学公式编辑器&#xff0c;适用于教育教…