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 选型
| 维度 | HDF5 | WebDataset |
|---|---|---|
| 访问模式 | 任意索引随机访问 | shard 内顺序流式 |
| 规模 | 单机为主,TB 级 | 无上限,对象存储友好 |
| 并发 | 单文件受锁/SWMR 约束 | 每 worker 独占 shard,天然并行 |
| shuffle | 真随机 | 需 buffer shuffle 近似 |
| 典型场景 | 本地实验、科学数据、部分 I/O | 海量预训练数据、云上顺序扫描 |
判断标准只有一条:数据放本地 SSD 且训练需要随机访问 → HDF5;数据在对象存储或以 TB 计、按 epoch 顺序扫 → TAR 流式。
应用场景
- 科学计算:物理模拟、气候建模(HDF5 的原生领地)
- 机器学习:大规模数据集单文件存储与分发
- 医疗影像:CT/MRI 数据归档
- 金融分析:高频时序数据