#numpy #serialization #ndarray #msgpack #serde

bin+lib msgpack-numpy

这是一个 Rust 对 msgpack-numpy 的实现,用于对 NumPy 标量和数组进行序列化和反序列化,与 Python 实现兼容

4 个版本

0.1.3 2024 年 8 月 22 日
0.1.2 2024 年 8 月 21 日
0.1.1 2024 年 8 月 13 日
0.1.0 2024 年 7 月 18 日

#405 in 编码

Download history 116/week @ 2024-07-15 15/week @ 2024-07-22 139/week @ 2024-08-12

每月 156 下载

MIT 许可证

69KB
1K SLoC

msgpack-numpy-rs

Crates.io Docs.rs License

这个包在 Rust 中实现了 Python 的 msgpack-numpy,并且运行得更快。它将 NumPy 标量和数组序列化到 MessagePack 格式,并反序列化回来,与 Python 的序列化格式相同,因此它们可以相互操作。它允许在 Rust 中通过 IPC 处理不同服务中的 NumPy 数组,或将机器学习结果保存到磁盘(与压缩配合使用更好)。

概述

  • 它支持 boolu8i8u16i16f16(通过 half 包),u32i32f32u64i64f64
  • 不支持包含复数('c')、字节字符串('S')、Unicode 字符串('U')或其他非原始类型作为元素的数组。不支持结构化/元组数据类型('V'),或需要序列化的对象类型数据('O')(参考)。
  • 然而,在反序列化过程中,我们允许将不支持的数据类型反序列化为 Unsupported 变体。这确保了反序列化可以继续,并且可以支持使用数据的部分。
  • 标量和数组被表示为不同的类型,每个类型都是一个枚举,包含不同元素类型的变体。它们提供了方便的转换方法(由 num-traits 包支持),可以将它们转换为所需的原始类型。例如:f16f32f64 都可以转换为 f64,或者以损失转换为 f16。这允许在反序列化过程中具有灵活性,无需显式的模式匹配和条件转换。这类似于 NumPy 的 .astype(np.float64) / .astype(np.float16)。值得注意的是,bool 可以转换为数值类型,作为 (0, 1),但无法通过这些方法从数值类型转换。当然,您可以在与 Bool 变体匹配后进行自己的转换。
  • 数组使用 ndarray 包,并具有动态形状。这使用户能够利用 Rust 的数值 生态系统 来处理反序列化的数组。
  • 使用 CowNDArray 处理数组可能在序列化切片中的数组缓冲区具有良好对齐时实现零拷贝,尽管 MessagePack 不保证这一点。
  • 这取决于 serde。此外,使用正确的 MessagePack 实现是有意义的,例如 rmp-serde,以下示例中使用了它,尽管它不需要是一个依赖项,因为 serde 的设计。

动机

关于一个既灵活又高效的格式,用于序列化 NumPy 数组,尚未达成共识。它们独特之处在于它们本质上是一块字节块,但同时也具有数值类型和形状。从事机器学习问题开发的程序员发现 MessagePack 具有有趣的属性。它具有紧凑的 类型系统,并且拥有广泛的语言支持。包 msgpack-numpy 为 NumPy 数组提供了解/序列化,无论是独立使用还是嵌套在任意深度中,都可以通过网络发送或保存到磁盘,以紧凑格式。

如果寻找一个更面向生产、性能更优的格式,可能会考虑 Apache ArrowParquetProtocol Buffers。然而,当需要存储中间机器学习结果时,这些格式在灵活性上不如 MessagePack。在实践中,支持 Numpy 数组的 MessagePack 可以是许多这些用例的一个很好的选择。

这个 Rust 版本旨在提供一个比 Python 版本更快的替代方案,具有与 Python 对应版本相同的序列化格式,以便它们可以相互操作。您可以使用它作为构建块来构建自己的 Rust 机器学习管道,或者作为在 Python 和 Rust 之间进行通信的一种方式。

示例

use std::fs::File;
use std::io::Read;
use msgpack_numpy::NDArray;

fn main() {
    let filepath = "tests/data/ndarray_bool.msgpack";
    let mut file = File::open(filepath).unwrap();
    let mut buf = Vec::new();
    file.read_to_end(&mut buf).unwrap();
    let deserialized: NDArray = rmp_serde::from_slice(&buf).unwrap();

    match &deserialized {
        NDArray::Bool(array) => {
            println!("{:?}", array);
        }
        _ => panic!("Expected NDArray::Bool"),
    }

    // returns an Option, None if conversion is not possible
    let arr = deserialized.into_u8_array().unwrap();
    println!("{:?}", arr);
}

请参阅 examples/ 以获取更多信息。

基准测试

所有基准测试都在Ubuntu 22.04实例的1个CPU核心上完成。CPU:Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz。Rust版本是用发布模式编译的。我们只对数组的序列化和反序列化进行基准测试,在内存中。请参阅benches/目录中的基准测试代码。

这适用于拥有NDArray的情况。

数组类型 数组大小 数组 操作 Python (ms) Rust (ms) 加速
f32 1000 10000 序列化 56.4 17.1 3.3x
反序列化 26.1 18.9 1.4x
100 100000 序列化 226.1 27.1 8.3x
反序列化 199.3 50.5 3.9x
f16 1000 10000 序列化 33.5 4.0 8.5x
反序列化 21.2 5.2 4.1x
100 100000 序列化 198.9 12.1 16.5x
反序列化 195.2 29.5 6.6x

Rust实现的所有情况都比Python有显著的性能提升,特别是小数组序列化时的速度提升非常明显。Python版本的序列化和反序列化逻辑是通过NumPy用C编写的,但小数组会减少这种好处,因为每个数组都是一个Python对象。值得注意的是,Python版本的反序列化速度比序列化快,而Rust版本的序列化速度比反序列化快。这个范围的数组大小对于机器学习用例(如特征嵌入)是典型的,所以当需要性能时,Rust能够提供帮助。

零拷贝反序列化(当良好对齐时)

对于上述数组,在反序列化时数组缓冲区似乎总是对齐错误的,所以我们不能像目标类型数组那样从序列化切片借用数据,而是需要额外分配空间。这是因为MessagePack格式不保证对齐。

然而,在大多数情况下,有很好的机会对齐,并且当发生这种情况时,我们可以直接借用数组缓冲区数据。以下基准测试展示了这一点。我们选择CowNDArray,形状为(1024, 2048),每次演示10个数组。

数据类型 操作 Python (ms) Rust (ms) 加速
f16 序列化 42.8 23.4 1.8x
反序列化(《NDArray》) 21.6 20.4 1.1x
反序列化(《CowNDArray》) - 10.5 2.1x
f32 序列化 87.8 43.5 2.0x
反序列化(《NDArray》) 44.2 41.4 1.1x
反序列化(《CowNDArray》) - 34.5 1.3x

反序列化时间下降了!对于《f16》,大约有一半的机会对齐良好,对于《f32》则约为1/4。分配的摊销成本现在更低,我们可以看到零拷贝反序列化的好处。缺点是,《CowNDArray》仅支持《rmp_serde::from_slice》(从完全在内存中的切片消耗),而不支持《rmp_serde::from_read》(以流式方式从读取器消耗)。因此,您需要保留序列化的字节数据(编译器将进行检查)。

如果您真的想实现完全的零拷贝反序列化,您应该尝试其他格式,例如《Apache Arrow》。

注意事项

标量类型

没有很好的理由使用《Scalar》进行序列化,因为您最终会使用大量元数据来表示原始类型。这种类型的存在是为了兼容性原因——它有助于反序列化已经以这种方式序列化的标量。

对《ndarray》的依赖

此crate在其公共API中使用《ndarray》的类型。《ndarray》在crate根目录中重新导出,因此您无需将其指定为直接依赖项。

此外,这个库与多个版本的 ndarray 兼容,因此依赖于一系列 semver 不兼容的版本,目前为 >=0.15, <0.17。如果你直接或间接依赖于该特定范围之外的任何内容,Cargo 不会自动为你选择 ndarray 的单个版本。换句话说,即使你在自己的项目中将 ndarray 锁定为 0.15.6,这个库也将 0.16.1 作为其独立的依赖项。这可能会让人感到意外,你将遇到如下编译错误

     = note: `ArrayBase<CowRepr<'_, f32>, Dim<IxDynImpl>>` and `ArrayBase<CowRepr<'_, f32>, Dim<IxDynImpl>>` have similar names, but are actually distinct types
note: `ArrayBase<CowRepr<'_, f32>, Dim<IxDynImpl>>` is defined in crate `ndarray`
    --> /home/ubuntu/.cargo/registry/src/index.crates.io-6f17d22bba15001f/ndarray-0.15.6/src/lib.rs:1268:1
     |
1268 | pub struct ArrayBase<S, D>
     | ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: `ArrayBase<CowRepr<'_, f32>, Dim<IxDynImpl>>` is defined in crate `ndarray`
    --> /home/ubuntu/.cargo/registry/src/index.crates.io-6f17d22bba15001f/ndarray-0.16.1/src/lib.rs:1280:1
     |
1280 | pub struct ArrayBase<S, D>
     | ^^^^^^^^^^^^^^^^^^^^^^^^^^
     = note: perhaps two different versions of crate `ndarray` are being used?

因此,可能需要手动统一这些依赖项。例如,如果你指定以下依赖项

msgpack-numpy = "0.1.3"
ndarray = "0.15.6"

则当前默认将依赖于 0.15.60.16.1 两个版本的 ndarray,尽管 0.15.6 在范围 >=0.15, <0.17 内。要修复此问题,你可以运行

cargo update --package ndarray:0.16.1 --precise 0.15.6

以实现只依赖于 ndarray 的 0.15.6 版本。检查你的锁定文件以验证是否成功。

许可证

本项目采用 MIT 许可证。

依赖项

~2.7–3.5MB
~75K SLoC