HDF5 以类文件系统的分层结构组织大规模科学数据,用分块存储支撑压缩与部分 I/O,是连接 NumPy/PyTorch 数据管道的主流单机格式。

分层结构

/(根组)
├── group1/
│   ├── dataset1(多维数组)
│   └── attribute1(元数据)
└── dataset2
组件说明
File最高层次,自包含
Group组织容器,类似文件夹
Dataset多维数组,存储实际数据
Attribute挂在 Group/Dataset 上的小型元数据
特性说明
分层结构类文件系统树形组织,单文件聚合全部数据
高效压缩gzip、lzf、zstd 等滤波器链
跨平台Windows/Linux/macOS 无缝共享
高性能 I/O部分读取、分块存储
自描述性内含元数据,便于长期归档

h5py 基础

import h5py
import numpy as np
 
# 创建文件:dataset 直接写盘,metadata 挂在 attrs 上
with h5py.File('data.h5', 'w') as f:
    ds = f.create_dataset('matrix', data=np.random.random((100, 100)))
    ds.attrs['description'] = '随机矩阵'
 
# 读取:h5py 对象是懒加载的,切片时才真正发生 I/O
with h5py.File('data.h5', 'r') as f:
    print(f['matrix'].shape)
    print(f['matrix'].attrs['description'])

分块存储与压缩

HDF5 dataset 有两种物理布局:contiguous(连续存放,默认)与 chunked(分块存放)。只有 chunked 布局才能启用压缩、shuffle 滤波和无限维扩展(maxshape=None)。

with h5py.File('compressed.h5', 'w') as f:
    f.create_dataset('large',
        data=np.random.random((10000, 3, 224, 224)),
        compression='gzip',       # 兼容性最好;求快用 lzf 或 zstd(hdf5plugin)
        compression_opts=4,
        shuffle=True,             # 字节重排,显著提升浮点数据压缩率
        chunks=(64, 3, 224, 224)) # 沿样本维切块:一块 = 64 个完整样本

chunk 为什么决定性能

chunk 是 I/O 的最小单位。数据按 chunk 分散存放,读取任意元素都必须把整块读入内存(压缩数据还需整块解压);写入单个元素则是整块的 read-modify-write。因此分块大小必须匹配读取模式:

  • 读放大:chunk 太大,随机取一个样本要解压整块,绝大部分 I/O 被浪费
  • 元数据膨胀:chunk 太小,每个 chunk 在 B-tree 索引中占一条记录,文件膨胀、缓存命中率下降
  • 按样本切齐:让一个完整样本恰好落在一块内——取一个样本只碰一块;若样本横跨多块,一次读取变成多次 I/O
  • 经验值:单块几百 KB 到 1 MB,同时不超过随机访问时 chunk cache(rdcc_nbytes,默认 1 MB/dataset)的容量,否则重复访问同一块会反复解压
策略说明
分块大小顺序访问用大块拉吞吐,随机访问用小块控读放大
压缩算法gzip 通用;lzf 解压快;zstd 兼顾两者
shuffle 滤波对浮点/整型数组几乎免费地提升压缩率
chunk cache随机访问慢时优先调大 rdcc_nbytes

部分 I/O

切片只读取落到的 chunk,不触碰文件其余部分——这是 HDF5 处理超出内存数据集的基础。

with h5py.File('data.h5', 'r') as f:
    subset = f['matrix'][100:200, 300:400]  # 只读子区域
 
    # 逐块流式处理,内存占用恒定
    for i in range(0, f['matrix'].shape[0], 100):
        chunk = f['matrix'][i:i+100, :]

作为 PyTorch 存储后端

HDF5 适合做 Dataset 与 DataLoader 的底层数据源:dataset 天然是”样本 × 特征”的数组,索引一个样本即一次部分 I/O。h5py 切片返回 NumPy 数组,torch.from_numpy 与 Tensor 与 Storage 共享底层内存,numpy→torch 这步零拷贝。

import torch
from torch.utils.data import Dataset
 
class H5Dataset(Dataset):
    def __init__(self, path):
        self.path = path
        with h5py.File(path, 'r') as f:
            self.n = f['images'].shape[0]
 
    def _file(self):
        # 延迟打开:每个 DataLoader worker 各自持有句柄,绝不跨进程共享
        if 'f' not in self.__dict__:
            self.__dict__['f'] = h5py.File(self.path, 'r')
        return self.__dict__['f']
 
    def __len__(self):
        return self.n
 
    def __getitem__(self, idx):
        x = torch.from_numpy(self._file()['images'][idx])  # 只解压所在 chunk
        return x, int(self._file()['labels'][idx])

多进程并发的坑

  • 句柄不能跨 fork 共享:主进程打开的文件句柄被 num_workers > 0 的 worker 继承后,内部偏移状态错乱。务必像上例那样在 worker 内延迟打开
  • 文件锁:HDF5 ≥ 1.10 默认加锁,多进程同时以写模式打开同一文件直接报错;只读并发通常可行,但 NFS 上锁不可靠,可用环境变量 HDF5_USE_FILE_LOCKING=FALSE 绕过——仅当确定无并发写时
  • SWMR(Single-Writer/Multiple-Reader)允许一边写一边读:
# 写端:libver='latest' 是 SWMR 前提;先建 dataset 再切换模式
with h5py.File('stream.h5', 'w', libver='latest') as f:
    ds = f.create_dataset('x', shape=(0,), maxshape=(None,))
    f.swmr_mode = True
    for i in range(100):
        ds.resize(i + 1, axis=0)
        ds[i] = i
        ds.flush()  # 落盘后读端立即可见
 
# 读端:写端运行中并发打开
with h5py.File('stream.h5', 'r', swmr=True) as f:
    print(f['x'][-1])

HDF5 vs WebDataset 选型

维度HDF5WebDataset
访问模式任意索引随机访问shard 内顺序流式
规模单机为主,TB 级无上限,对象存储友好
并发单文件受锁/SWMR 约束每 worker 独占 shard,天然并行
shuffle真随机需 buffer shuffle 近似
典型场景本地实验、科学数据、部分 I/O海量预训练数据、云上顺序扫描

判断标准只有一条:数据放本地 SSD 且训练需要随机访问 → HDF5;数据在对象存储或以 TB 计、按 epoch 顺序扫 → TAR 流式。

应用场景

  • 科学计算:物理模拟、气候建模(HDF5 的原生领地)
  • 机器学习:大规模数据集单文件存储与分发
  • 医疗影像:CT/MRI 数据归档
  • 金融分析:高频时序数据