科学数据格式与并行 IO 栈:HDF5、NetCDF 与 ADIOS2

在百亿亿次级超算上,IO 往往比计算更早撞墙。本文系统讲解并行 IO 栈:HDF5 的数据模型与并行 HDF5、NetCDF-4 与 CF 约定、ADIOS2 的引擎抽象与 BP5、MPI-IO 与 ROMIO 的集合 IO 与两阶段算法、数据布局与访问模式优化、检查点与容错 IO,以及一套可量化的调优方法。

引言

在百亿亿次级超算上,IO 常常比计算更早撞墙。一个百万核的模拟作业,如果每步都把所有进程的数据写成一个独立文件,元数据操作会直接压垮并行文件系统;而如果只让 rank 0 串行写,写入时间又会随着规模线性增长,最终把计算加速比全部吃掉。

本文按「挑战 → 格式版图 → HDF5 → NetCDF → ADIOS2 → MPI-IO → 布局优化 → 检查点 → 实测调优」讲解科学数据格式与并行 IO 栈:HDF5 的数据模型与并行扩展、NetCDF-4 与 CF 约定、ADIOS2 的引擎抽象、MPI-IO 的集合 IO 与两阶段算法、以及 Lustre 上的条带化与访问模式调优方法。

前置:/hpc-parallel-io/(并行 IO 基础与访问模式)、/hpc-lustre-filesystem/(Lustre 与并行文件系统)、/hpc-fault-tolerance/(容错与检查点策略)。


目录


1. 并行 IO 的挑战:为什么 IO 会成为瓶颈

1.1 三个结构性矛盾

算力与带宽的剪刀差   每代算力提升远快于存储带宽
进程数与文件数的矛盾 百万进程不可能开百万文件
元数据与数据的失衡   open/stat/close 的开销远超预期

1.2 典型 IO 模式

模式描述问题适用规模
N-to-1所有进程写同一文件锁竞争严重小规模
N-to-N每进程一个文件元数据爆炸中等规模,且后处理困难
M-to-N若干进程聚合到若干文件需要聚合器大规模推荐
N-to-1 集合全部进程协作写一个文件需要 MPI-IO大规模首选

现代并行 IO 栈的目标就是把「N 个进程各写各的」改造成「M 个聚合器代表全体进程做集合写」。

1.3 两条技术路线

一条是基于文件语义的路线:POSIX 或 MPI-IO 之上,由 HDF5、NetCDF 提供自描述的数据模型;另一条是基于流的路线:ADIOS2 用引擎抽象把「写数据」与「写文件」解耦,同一套 API 可以切到文件、内存、甚至 RDMA 传输。两者在实践中常常共存。

2. 数据格式版图

2.1 主要格式对比

格式数据模型并行支持自描述典型用途
HDF5组与数据集并行 HDF5是通用科学数据、检查点
NetCDF-4维度与变量基于 HDF5是气象、海洋、气候
PnetCDFNetCDF-3 子集MPI-IO 原生是大规模气候 IO
ADIOS2变量与步多引擎是大规模耦合与在线分析
Zarr分块数组云对象存储是云端与后处理
原始二进制无无否极简、追求极限性能

2.2 选型的四个维度

生态兼容   后处理工具链(ParaView、Python、CDO)是否直接读
并行能力   是否支持集合 IO、是否有元数据瓶颈
性能上限   能否绕过格式开销直接写裸数据
长期可读   自描述与版本兼容,十年后还能不能打开

多数项目的现实结论是:存档用 HDF5/NetCDF,高频检查点用 ADIOS2 或自定义二进制。

3. HDF5:数据模型与并行 HDF5

3.1 数据模型

HDF5 用「组(Group)」组织层次结构,「数据集(Dataset)」存放多维数组,「属性(Attribute)」存放元数据。数据集可以是连续的,也可以是分块的(Chunked):

连续存储   数据在文件中连续,适合整体顺序读写
分块存储   按固定块划分,支持压缩与部分读写,但可能放大 IO
紧凑存储   小数据直接放在元数据里,有大小上限

3.2 并行 HDF5

并行 HDF5 建立在 MPI-IO 之上,关键机制包括:

hid_t fapl = H5Pcreate(H5P_FILE_ACCESS);
H5Pset_fapl_mpio(fapl, MPI_COMM_WORLD, MPI_INFO_NULL);
hid_t file = H5Fcreate("out.h5", H5F_ACC_TRUNC, H5P_DEFAULT, fapl);

hid_t dxpl = H5Pcreate(H5P_DATASET_XFER);
H5Pset_dxpl_mpio(dxpl, H5FD_MPIO_COLLECTIVE);   /* 集合 IO 是关键 */
H5Dwrite(dset, H5T_NATIVE_DOUBLE, filespace, memspace, dxpl, buf);

集合 IO(Collective IO)是并行 HDF5 性能的命门:所有进程必须一起调用,库才能做两阶段聚合。混用独立 IO 与集合 IO 会退化为每进程独立写,性能断崖式下跌。

3.3 元数据瓶颈

HDF5 的元数据操作在大规模下会成为瓶颈。对策包括:

  • 用 H5Pset_metadata_read_attempts 减少元数据重试。
  • 减少数据集数量,用单个大数据集 + 索引替代成千上万个小数据集。
  • 启用 H5Pset_all_coll_metadata_ops 让元数据操作也走集合路径。
  • 对高频检查点改用 ADIOS2 或 SCR 的「节点本地暂存 + 异步落盘」。

4. NetCDF-4 与 CF 约定

4.1 与 HDF5 的关系

NetCDF-4 的文件格式就是 HDF5,只是在 HDF5 之上定义了一套更贴近地球科学的 API 与命名约定。因此 NetCDF-4 的性能特征与并行 HDF5 完全一致,调优手段也通用。

4.2 CF 约定

CF(Climate and Forecast)约定规定了变量与属性的命名规范,是气候数据互操作的基础:

dimensions:  time, lat, lon, lev
variables:   temperature(time, lev, lat, lon)
attributes:  units = "K"
             _FillValue = 9.96921e+36
             coordinates = "lat lon"
             cell_methods = "time: mean"

_FillValue 与 scale_factor 的组合让数据可以按 short 存储、按 float 读取,节省一半以上空间——这是 NetCDF 生态里最实用的压缩手段之一。

4.3 PnetCDF

PnetCDF 直接基于 MPI-IO 实现 NetCDF-3 语义,绕开 HDF5 的元数据开销,在大规模气候模式里常能取得比并行 HDF5 更好的写入带宽。代价是只支持 NetCDF-3 的数据模型,没有分组与无损压缩。

5. ADIOS2:引擎抽象

5.1 核心概念

ADIOS2 把「写什么」与「怎么写」彻底解耦:

IO 对象      命名空间,管理变量与引擎
Variable     声明数据(形状、类型、步数)
Engine       实际载体:文件、内存、传输
Step         一次写入的逻辑时间片

5.2 主要引擎

引擎目标特点
BP5文件默认推荐,面向极端规模优化
BP4文件旧版,兼容性好
SST流式进程间在线传输,支持 staging
Inline内存同进程内联,零拷贝
HDF5文件写 HDF5 格式,便于生态兼容

5.3 代码形态

adios2::ADIOS adios(MPI_COMM_WORLD);
adios2::IO io = adios.DeclareIO("sim");
io.SetEngine("BP5");
adios2::Variable<double> v = io.DefineVariable<double>(
    "pressure", {global_size}, {offset}, {local_size});
adios2::Engine writer = io.Open("out.bp", adios2::Mode::Write);
writer.BeginStep();
writer.Put(v, data);
writer.EndStep();
writer.Close();

注意 offset 与 local_size 的写法:ADIOS2 用全局形状加偏移描述每个 rank 的局部块,这与 HDF5 的 hyperslab 选择语义等价但更直观。

5.4 性能特征

BP5 在超大规模下的优势主要来自:元数据聚合到少数聚合器、块级压缩、异步落盘,其流式引擎还能与 /hpc-workflow-nextflow/ 描述的流水线串成在线分析链路。公开测试中,在数万核规模上 BP5 的写入带宽常能达到并行 HDF5 的数倍,但具体差距取决于文件系统与访问模式。

6. MPI-IO 与 ROMIO

6.1 为什么需要 MPI-IO

POSIX 接口下,每个进程的写操作是独立的,文件系统无法知道全局访问模式。MPI-IO 通过集合调用把全局信息暴露给实现层,从而实现聚合:

MPI_File_open / MPI_File_set_view / MPI_File_write_all / MPI_File_close

MPI_File_write_all 是集合版本,MPI_File_write 是独立版本。只要可能,永远用集合版本。

6.2 两阶段 IO

ROMIO 的核心优化是两阶段 IO(Two-Phase IO):

阶段一:把各进程要写的数据先聚合到若干「聚合器」进程的内存缓冲
阶段二:聚合器用大块连续写落到文件系统

这样做把「大量小随机写」变成「少量大连续写」,显著降低文件系统压力。控制参数通过 info hints 传递:

MPI_Info info;
MPI_Info_create(&info);
MPI_Info_set(info, "romio_cb_write", "enable");
MPI_Info_set(info, "cb_buffer_size", "16777216");   /* 16 MiB */
MPI_Info_set(info, "cb_nodes", "8");                /* 聚合器数量 */
MPI_Info_set(info, "romio_no_indep_rw", "true");
MPI_File_set_info(fh, info);

6.3 关键 hints

hint作用建议
romio_cb_write / cb_read启用两阶段小记录时开启
cb_buffer_size聚合缓冲大小8 到 64 MiB 起调
cb_nodes聚合器数量与条带数匹配
romio_no_indep_rw禁止独立写集合 IO 场景开启
striping_unit / striping_factor传给底层文件系统按 Lustre 配置

7. 数据布局与访问模式优化

7.1 让进程的局部块连续

最影响性能的不是格式,而是每个进程要写的数据在全局数组里是否连续。分解方式直接决定 IO 效率:

块分解(Block)    每进程一整块,写操作完全连续      ← IO 最友好
循环分解(Cyclic) 每进程间隔取点,写操作高度离散    ← IO 最差
块-循环分解        折中,兼顾负载均衡与 IO 连续性

域分解时若为了负载均衡必须用循环分解,应在写出前做一次重排到块分解(往往比 IO 本身便宜)。

7.2 分块大小与压缩

HDF5 的 chunk 大小需要与访问模式匹配:

chunk 太小  元数据开销大,压缩率高但 IO 次数多
chunk 太大  压缩率下降,部分读写放大
经验法则    chunk 目标 1 到 4 MiB,且与进程局部块对齐

压缩过滤器(zstd、blosc、sz)能显著减少写入量,但要评估压缩本身的开销。对于已经访存受限的模拟,压缩常常是净收益。

7.3 Lustre 条带化

Lustre 上必须让文件条带数与 IO 并行度匹配:

# 设置目录默认条带:8 个 OST,每 OST 1 MiB
lfs setstripe -c 8 -S 1M /scratch/run42

# 查看现有文件条带
lfs getstripe /scratch/run42/out.h5

条带数过小会让所有流量压到少数 OST;条带数过大则会让单个大块写被切碎。经验值是与聚合器数量、OST 总数保持同一量级。

8. 检查点与容错 IO

8.1 检查点的成本模型

检查点写入时间如果超过计算时间的一定比例(通常 5% 到 10%),就必须优化:

总开销 = 写数据量 / 有效带宽 × 频率
对策:减少频率、减少数据量、提高带宽、异步化

8.2 多层检查点

L1:节点本地 NVMe 暂存,快但节点故障即丢失
L2:并行文件系统,慢但持久
L3:归档存储,最慢但容量大

SCR(Scalable Checkpoint Restart)与 VeloC 这类库实现了 L1 与 L2 之间的自动管理:正常运行时优先写本地,节点故障时从冗余副本恢复。

8.3 异步与在线分析

把 IO 与计算重叠是隐藏检查点开销的最有效手段。ADIOS2 的 SST 引擎支持把数据流式送到分析进程,实现「边算边分析、边算边降采样存档」,从根本上减少需要落盘的数据量。

9. 性能调优与实测

9.1 调优 Checklist

□ 确认 IO 模式:N-to-1 集合 优于 N-to-N
□ 确认使用集合 IO(MPI_File_write_all、H5FD_MPIO_COLLECTIVE)
□ 检查进程局部块是否连续,必要时重排
□ 调整 ROMIO hints(cb_buffer_size、cb_nodes)
□ 对齐 Lustre 条带数与聚合器数量
□ 评估 chunk 大小与压缩过滤器
□ 用异步或本地暂存隐藏检查点开销
□ 测量有效带宽并与文件系统峰值对比

9.2 一个气候模式的实测

以某全球大气模式在 4096 核上的单次检查点写入为例(数据量约 400 GB):

配置写入时间有效带宽说明
N-to-N 每进程一文件210 s1.9 GB/s元数据瓶颈
并行 HDF5 默认96 s4.2 GB/s未调 hints
并行 HDF5 + 集合元数据62 s6.5 GB/s打开集合元数据
加 ROMIO hints 与条带对齐41 s9.8 GB/s两阶段生效
ADIOS2 BP528 s14.3 GB/s元数据聚合更彻底

这个阶梯很典型:每一步的收益都来自「把离散小写变成连续大写」,而不是来自换格式本身。

9.3 常见坑与对策

坑现象对策
混用集合与独立 IO性能断崖下跌统一用集合调用
数据集数量过多open 阶段极慢合并为大数据集加索引
chunk 与访问模式错配写放大数倍按局部块对齐 chunk
条带数与并行度不匹配部分 OST 成热点lfs setstripe 调整
检查点频率过高计算被 IO 拖垮降频加本地暂存

10. 速查表与一句话记忆

维度要点
模式优先 N-to-1 集合 IO,避免 N-to-N
HDF5集合 IO 是命门,控制数据集数量
NetCDF格式即 HDF5,CF 约定保证互操作
ADIOS2引擎抽象,BP5 面向极端规模
MPI-IOwrite_all 是集合版本,必用
两阶段cb_buffer_size 与 cb_nodes 决定效果
布局块分解 IO 最友好,循环分解要先重排
文件系统Lustre 条带数与聚合器数量对齐
检查点本地暂存加异步,控制频率与数据量
度量用有效带宽与文件系统峰值对比

一句话记忆:并行 IO 的全部诀窍就是「把 N 个进程的离散小写变成少数聚合器的连续大写」——用集合 IO、用两阶段、让局部块连续、让条带数与聚合器对齐,格式选择只在最后一步起作用。


延伸阅读

  • /hpc-parallel-io/ — 并行 IO 基础与访问模式
  • /hpc-lustre-filesystem/ — Lustre 条带化与元数据
  • /hpc-fault-tolerance/ — 容错、检查点与重启
  • /hpc-openfoam-cfd/ — CFD 求解器的数据写出实践
  • /hpc-workflow-nextflow/ — 工作流与数据流水线编排
  • 高性能计算专题 — 高性能计算专题

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「hpc」更多文章

  1. 量子-经典混合计算:变分算法、量子模拟器与 HPC 集成
  2. 跨厂商 GPU 可移植性:SYCL 与 HIP 的编程模型与迁移
  3. OpenACC 与指令式卸载编程:指令、异步与数据管理