在磁盘上转置大型 numpy 矩阵

Luc*_*tti 6 python transpose numpy matrix rust

我有一个相当大的矩形(>1G 行,1K 列)Fortran 风格的 NumPy 矩阵,我想将其转置为 C 风格。

到目前为止,我的方法相对简单,使用以下 Rust 代码片段,使用源矩阵和目标矩阵的 MMAPed 切片,其中 和original_matrix都是target_matrixMMAPPed PyArray2,并由Rayon处理并行化。

由于target_matrix必须由多个线程修改,因此我将其包装在UnsafeCell.

let shared_target_matrix = std::cell::UnsafeCell::new(target_matrix);

original_matrix.as_ref().par_chunks(number_of_nodes).enumerate().for_each(|(j, feature)|{
    feature.iter().copied().enumerate().for_each(|(i, feature_value)| unsafe {
        *(shared_target_matrix.uget_mut([i, j])) = feature_value;
    });
});
Run Code Online (Sandbox Code Playgroud)

这种方法转置形状为 (~1G, 100) 的矩阵,~120GB 在 HDD 磁盘上需要~3 小时。转置 (~1G, 1000)、~1200GB 矩阵不会像人们天真地期望的那样线性扩展到 30 小时,而是会爆炸到几周。就目前情况而言,我已经在 2 天内成功地转置了大约 100 个功能,而且速度一直在减慢。

有几个方面,例如所使用的文件系统、HDD 碎片以及 MMAPed 如何处理页面加载,我的解决方案目前忽略了这些方面。

是否存在考虑到这些问题的已知的、更全面的解决方案?

关于顺序和并行方法的注意事项

虽然直观上,这种操作可能只受 IO 限制,因此不会从任何并行化中受益,但我们通过实验观察到,并行方法确实比顺序方法快三倍(在具有 12 个内核和 24 个线程的机器上)。转置形状为 (1G, 100) 的矩阵时的方法。我们不确定为什么会出现这种情况。

使用两个硬盘的注意事项

我们还尝试使用两种设备,一种提供 Fortran 样式矩阵,另一种提供目标矩阵。两个 HDD 均通过 SATA 电缆直接连接到计算机主板。我们预计性能至少会翻倍,但它们保持不变。

the*_*472 1

虽然直观上,这种操作可能只受 IO 限制,因此不会从任何并行化中受益,但我们通过实验观察到并行方法确实快了大约三倍

这可能是由于 IO 队列利用率不佳造成的。对于没有预取的完全顺序工作负载,您将在工作和空闲之间交替设备。如果您在飞行中进行多项操作,它将始终正常工作。

检查与iostat -x <interval>

但并行性并不是实现 HDD 最佳利用的次优方法,因为它可能会导致不必要的磁头搜索。

我们还尝试使用两种设备,一种提供 Fortran 样式矩阵,另一种提供目标矩阵。两个 HDD 均通过 SATA 电缆直接连接到计算机主板。我们预计性能至少会翻倍,但它们保持不变。

这可能是由于操作系统的写入缓存造成的,这意味着它可以非常有效地批量写入,而您主要在读取方面遇到瓶颈。再次检查iostat

有几个方面,例如所使用的文件系统、HDD 碎片以及 MMAPed 如何处理页面加载,我的解决方案目前忽略了这些方面。是否存在考虑到这些问题的已知的、更全面的解决方案?

是的,如果底层文件系统支持,您可以使用FIEMAP获取磁盘上数据的物理布局,然后优化读取顺序以遵循物理布局而不是逻辑布局。您可以使用filefragCLI 工具手动检查碎片数据,但该 ioctl 有 rust 绑定,因此您也可以以编程方式使用它。

此外,您还可以用来madvise(MADV_WILLNEED)通知内核在后台预取数据以用于接下来的几次循环迭代。对于 HDD,理想情况下,这应该以一次价值几兆字节的批次完成。下一批应该在当前一批完成一半时发出。批量发出它们可以最大限度地减少系统调用开销,并在中途开始下一个,确保在到达当前 IO 结束之前有足够的时间来实际完成 IO。

由于您将按照物理顺序而不是逻辑顺序手动发出预取,因此您还可以通过以下方式禁用默认的预读启发式(这会妨碍)madvise(MADV_RANDOM)

如果您有足够的可用磁盘空间,您还可以尝试一种更简单的方法:在操作文件之前对文件进行碎片整理。但即便如此,您仍然应该使用 madvise 来确保始终有 IO 请求在进行中。