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

news2024/12/31 0:50:35

在这里插入图片描述

在这里插入图片描述

文章目录

    • 前言
    • 设定方向
    • 从 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.coloradmin.cn/o/1351174.html

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

相关文章

EBU7140 Security and Authentication(三)密钥管理;IP 层安全

B3 密钥管理 密钥分类&#xff1a; 按时长&#xff1a; short term&#xff1a;短期密钥&#xff0c;用于一次加密。long term&#xff1a;长期密钥&#xff0c;用于加密或者授权。 按服务类型&#xff1a; Authentication keys&#xff1a;公钥长期&#xff0c;私钥短期…

【HarmonyOS开发】共享包HAR和HSP的创建和使用以及三方库的发布

OpenHarmony提供了两种共享包&#xff0c;HAR&#xff08;Harmony Archive&#xff09;静态共享包&#xff0c;和HSP&#xff08;Harmony Shared Package&#xff09;动态共享包。 HAR与HSP都是为了实现代码和资源的共享&#xff0c;都可以包含代码、C库、资源和配置文件&…

C语言——操作符

一、算数操作符 1、(加操作符) 用于将两个数相加&#xff0c;例&#xff1a;3 3结果为6 2、-(减操作符) 用于将两个数相减&#xff0c;例&#xff1a;3 - 3结果为0 3、*(乘操作符) 用于将两个数相乘&#xff0c;例&#xff1a;3 * 3结果为9 4、/(除操作符) 用于将两个…

fastadmin学习02-修改后台管理员账号密码

问题 如果是别人部署好的fastadmin网站不知道后台登录地址和账号密码怎么办 后台登录地址 public目录下有一个很奇怪的php就是后台登录地址啦 忘记账号密码 找到fa_admin&#xff0c;fa_是前缀&#xff0c;肯能每个项目不太一样 UPDATE fa_admin set password1d020dee8ec…

计算机毕业论文内容参考|基于智能搜索引擎的图书管理系统的设计与实现

文章目录 摘要前言绪论课题背景国内外现状与趋势课题内容相关技术与方法介绍系统分析系统设计系统实现系统测试总结与展望摘要 本文介绍了基于智能搜索引擎的图书管理系统的设计与实现。该系统旨在提供一个高效、智能化的图书管理平台,帮助用户更快、更准确地找到所需的图书资…

使用Apache Commons SCXML实现状态机管理

第1章&#xff1a;引言 大家好&#xff0c;我是小黑&#xff0c;咱们程序员在开发过程中&#xff0c;经常会遇到需要管理不同状态和状态之间转换的场景。比如&#xff0c;一个在线购物的订单&#xff0c;它可能有“新建订单”、“已支付”、“配送中”、“已完成”等状态。在这…

Linux/Unix/国产化操作系统常用命令(二)

目录 后CentOS时代国产化操作系统国产化操作系统有哪些常用Linux命令关于Linux的LOGO 后CentOS时代 在CentOS 8发布后&#xff0c;就有了一些变化和趋势&#xff0c;可以说是进入了"后CentOS时代"。这个时代主要表现在以下几个方面&#xff1a; CentOS Stream的引入…

Spring技术内幕笔记之SpringMvc

WebApplicationContext接口的类继承关系 org.springframework.web.context.ContextLoader#initWebApplicationContext 对IOC容器的初始化 SpringMvc如何设计 DispatcherServlet类继承关系 MVC处理流程图如下&#xff1a; DispatcherServlet的工作大致可以分为两个部分&#xf…

Spring Boot 如何使用 Maven 实现多环境配置管理

Spring Boot 如何使用 Maven 实现多环境配置管理 实现多环境配置有以下几个重要原因&#xff1a; 适应不同的部署环境&#xff1a;在实际部署应用程序时&#xff0c;通常会有多个不同的部署环境&#xff0c;如开发环境、测试环境、生产环境等。每个环境可能需要不同的配置&…

Linux 命令tail

命令作用 tail 命令用于显示文件的末尾内容&#xff0c;默认显示文件的最后 10 行。通常情况下&#xff0c;tail 命令用于实时查看动态日志文件&#xff0c;可以使用 -f 参数跟踪文件内容的变化。 语法 tail [选项] [文件名] 参数 以 log.txt 为例演示参数效果 -n -linesK…

解决报错:找不到显卡

今天做实验碰到一个问题&#xff1a;torch找不到显卡&#xff1a; 打开任务管理器&#xff0c;独显直接没了&#xff0c;一度以为是要去修电脑了&#xff0c;突然想到上次做实验爆显存&#xff0c;屏蔽了gpu用cpu训练&#xff1a; import os os.environ["CUDA_DEVICE_OR…

【华为数据之道学习笔记】9-3构建以元数据为基础的安全隐私保护框架

以元数据为基础的安全隐私治理 有决策权的公司高层已经意识到安全隐私的重要性&#xff0c;在变革指导委员会以及各个高层会议纪要中都明确指明安全隐私是变革优先级非常高的主题&#xff0c;安全是一切业务的保障。 基于这个大前提&#xff0c;我们构建了以元数据为基础的安全…

【Spark精讲】记一个SparkSQL引擎层面的优化:SortMergeJoinExec

SparkSQL的Join执行流程 如下图所示&#xff0c;在分析不同类型的Join具体执行之前&#xff0c;先介绍Join执行的基本框架&#xff0c;框架中的一些概念和定义是在不同的SQL场景中使用的。 在Spark SQL中Join的实现都基于一个基本的流程&#xff0c;根据角色的不同&#xff0…

zookeeper未授权访问漏洞增加用户认证修复

linux机器中使用root命令行cd到zookeeper的bin文件夹下 启动zookeeper ./zkCli.sh # 启动zookeeper如果此时有未授权漏洞&#xff0c;可通过以下命令验证。 getAcl / #可以看到默认是world:anyone 就相当于无权限访问验证结果显示没有用户认证也可执行一些命令 增添用户认…

[GKCTF 2020]ez三剑客-eztypecho

[GKCTF 2020]ez三剑客-eztypecho 考点&#xff1a;Typecho反序列化漏洞 打开题目&#xff0c;发现是typecho的CMS 尝试跟着创建数据库发现不行&#xff0c;那么就搜搜此版本的相关信息发现存在反序列化漏洞 参考文章 跟着该文章分析来&#xff0c;首先找到install.php&#xf…

LeetCode第32题 : 最长有效括号

题目介绍 给你一个只包含 ( 和 ) 的字符串&#xff0c;找出最长有效&#xff08;格式正确且连续&#xff09;括号子串的长度。 示例 1&#xff1a; 输入&#xff1a;s "(()" 输出&#xff1a;2 解释&#xff1a;最长有效括号子串是 "()" 示例 2&#xf…

DALLE·3的图片一致性火了!

DALLE3的图片一致性火了&#xff01; DALLE3 利用种子值&GenID的一致性作图 防止大家不知道&#xff0c;这里拓展一下种子值&#xff0c;DALL每次生成的图像都会有一个新的、随机的种子值。我们可以利用种子值进行角色的变换 每次生成的图像都会有一个新的、随机的种子值…

主流大语言模型集体曝出训练数据泄露漏洞

内容概要&#xff1a; 安全研究人员发现&#xff0c;黑客可利用新的数据提取攻击方法从当今主流的大语言模型&#xff08;包括开源和封闭&#xff0c;对齐和未对齐模型&#xff09;中大规模提取训练数据。当前绝大多数大语言模型的记忆&#xff08;训练数据&#xff09;可被恢…

maven、springboot项目编译打包本地jar、第三方jar包

0. 引言 一般我们在maven项目中都是通过引入pom坐标的形式来引入第三方jar包&#xff0c;但某些场景下&#xff0c;第三方是直接提供的jar包文件&#xff0c;这就需要我们从本地引入第三方包并进行打包。所以我们今天来看下如何进行本地引入第三方包操作 1. 步骤 1、在项目下…

在pycharm中jupyter连接上了以后显示无此库,但是确实已经安装好了某个库,使用python可以跑,但是使用ipython就跑不了

今天遇到一个事情&#xff0c;就是用pycharm的jupyter时&#xff0c;连接不上&#xff0c;后来手动连接上了以后&#xff0c;发现环境好像不对。 一般来说&#xff0c;这里会是python3&#xff0c;所以里面的环境也是普通python的环境&#xff0c;并没有我下载的库&#xff0c;…