你们中的一些人可能还记得 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 并实现它自己的机器学习算法。
为了完成这项工作,我们将工作分为两个不同的步骤:
- 使用 Scikit 在 Rust 中训练模型
- 使用我们的 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




