暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

向后兼容或失败:Python Inside Rust Inside Postgres

原创 小小亮 2022-10-20
498

你们中的一些人可能还记得 Python 3 发布的那一天。这些变化看似微妙,但足以造成混乱:大多数用 Python 2 编写的项目和工具将不再在 Python 3 下工作。接下来的十年用于将任务关键型基础设施print有些人只是放弃并继续使用 Python 2。打破向后兼容性以取得进展可能是好的,但 Python 的举动是有风险的。它经久不衰,因为我们更喜欢它而不是不同意这种变化。print()strbytes

大多数项目都没有那么奢侈,特别是如果你刚刚开始。对于 PostgresML 的我们来说,向后兼容性与进步同样重要。

PostgresML 2.0 即将推出,我们用 Rust 重写了所有内容,以实现35 倍的性能提升以前的版本是用 Python 编写的,Python 是事实上的机器学习环境,拥有最多的库。现在我们使用的是 Linfa 和 SmartCore,理论上我们可以在没有 Python 的情况下继续使用,但我们还没有准备好放弃 Python 生态系统提供的所有功能,我相信我们的许多用户还没有任何一个。那么我们可以做些什么来保持功能、向后兼容性和用户的信任呢?

PyO3 来救援。

Python in Rust

PyO3 是为了在 Rust 中构建 Python 扩展而编写的。本机扩展比 Python 模块快得多,因此,当速度很重要时,大多数东西都是用 Cython 或 C 编写的。如果你曾经尝试过,你就会知道这种体验不是非常友好或宽容。另一方面,Rust 快速且内存安全,编译器提示变得非常具体(我的联合创始人认为它可能正在成为一个奇点)。

PyO3 带有另一个非常重要的特性:它允许在 Rust 程序中运行 Python 代码。

听起来好得令人难以置信?当时我们并不这么认为。PL/Python 多年来一直这样做,这就是我们最初用来编写 PostgresML 的方法。在 Rust 中运行 Scikit 的路径似乎很明确。

路线图

让一个庞大的 Python 库在完全不同的环境下工作并不是一件显而易见的事情。如果您深入研究 Scikit 的源代码,您会发现 Python、Cython、C 扩展和 SciPy。我们将把它添加到一个共享库中,该库链接到 Postgres 并实现它自己的机器学习算法。

为了完成这项工作,我们将工作分为两个不同的步骤:

  1. 使用 Scikit 在 Rust 中训练模型
  2. 使用我们的 1.0 测试套件测试回归

你好 Python,我是 Rust

我们需要做的第一件事是确保 Scikit 甚至可以在 PyO3 下运行。所以我们为我们在 1.0 中实现的所有算法编写了一个小包装器,并从我们的 Rust 源代码中调用它。包装器只有 200 行代码,其中大部分是将算法名称映射到 Scikit 的 Python 类。

使用包装器非常简单:

use pyo3::prelude::*;
use pyo3::types::PyTuple;

pub fn sklearn_train() {
    // Copy the code into the Rust library at build time.
    let module = include_str!(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/src/bindings/sklearn.py"
    ));

    let estimator = Python::with_gil(|py| -> Py<PyAny> {
        // Compile Python
        let module = PyModule::from_code(py, module, "", "").unwrap();

        // ... train the model
    });
}

我们的 Python 代码已编译并准备就绪。我们用来自 Rust 数组的数据训练了一个模型,使用 PyO3 自动转换传递给 Python,并取回了一个经过训练的 Scikit 模型。感觉很神奇。

它有效吗?

由于我们在 1.0 中有几十个 ML 算法,我们有一个相当不错的测试套件来确保所有这些算法都能正常工作。我的本地开发人员是 Ubuntu 22.04 游戏平台(尽管我仍然是双启动),所以我在运行测试套件、在玩具数据集上训练所有 Scikit 算法并在很长一段时间内获得预测时没有任何问题。醉心于我的成功,我称工作完成,合并公关,然后继续前进。

然后,蒙大拿州决定在他稍旧的游戏设备上尝试我的工作,但他没有得到一个训练有素的模型,而是得到了这个:

server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.

在查看日志后,他发现了一条更可怕的消息:

LOG:  server process (PID 11352) was terminated by signal 11:

Segmentation fault

Rust 中的分段错误?这应该是不可能的,但它就在这里。

当程序尝试读取不存在的内存部分时,就会发生分段错误,因为它们被释放,或者一开始就从未分配过。在正常情况下,Rust 不会发生这种情况,但我们知道我们的项目远非正常。更令人困惑的是,错误来自 Scikit 内部。如果它是 XGBoost 或 LightGBM 是有道理的,我们用一堆 Rustunsafe块包装它们,但错误来自一个普遍使用的 Python 库。

向下调试十层

在已编译的可执行文件中调试分段错误很困难。在数据库内运行的机器学习库内的 FFI 包装器内的共享库内调试分段错误......更难。我们得到的线索很少:它适用于我的 Ubuntu 22.04,但不适用于蒙大拿州的 Ubuntu 20.04。我双启动 20.04 来检查它,令人惊讶的是,它对我来说也出现了段错误。

在这一点上,我确信某些事情是非常错误的,并调用了“通用调试器”来进行救援:我在 Scikit 的代码中乱扔垃圾,raise Exception("I'm here")以查看它的去向,更重要的是,它由于段错误而无法实现。几个小时后,我进入了 SciPy,我们的包装器深处有 10 多个函数调用。

SciPy 实现了许多有用的科学计算子程序,其中一个恰好解决了线性回归,这是一种非常流行的机器学习算法。SciPy 不是单独做的,而是调用一个 BLAS 子例程来尽可能快地处理数字,这就是我发现段错误的地方。

它点击了。Scikit 使用 SciPy,SciPy 使用 C-BLAS,我们使用 OpenBLASndarray和我们自己的向量函数,所有内容在编译时动态链接在一起。那么 SciPy 使用的是哪个 BLAS?它找不到它需要的 BLAS 功能并崩溃了。

静态链接或破产

修复非常简单:使用 Cargo 构建脚本静态链接 OpenBLAS:

构建.rs

fn main() {
    println!("cargo:rustc-link-lib=static=openblas");
}


链接器将 OpenBLAS 的代码包含在我们的扩展中,SciPy 能够找到它正在寻找的函数,并且 PostgresML 2.0 再次运行。

回顾

最后,我们得到了我们想要的:

  • Postgres 中的 Rust 机器学习步入正轨
  • Scikit-learn 正在进入 PostgresML 2.0
  • 保留了与 PostgresML 1.0 的向后兼容性

我们在使用 PyO3 并推动我们认为可能的极限方面获得了很多乐趣。

非常感谢和❤️所有支持这项努力的人。我们很乐意听取更广泛的 ML 和工程社区关于应用程序和其他现实世界场景的反馈,以帮助确定我们工作的优先级。


原文标题:Backwards Compatible or Bust: Python Inside Rust Inside Postgres

原文作者:Lev Kokotov

原文链接:https://postgresml.org/blog/backwards-compatible-or-bust-python-inside-rust-inside-postgres/#static-link-or-bust

最后修改时间:2022-10-20 11:42:14
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论