4. 数据预处理加速:在线vs离线预处理、使用WebDataset、TFRecord/RecordIO格式、数据缓存策略
数据预处理,说白了就是给模型喂饭前的洗菜切菜环节。很多人觉得这步没啥技术含量,结果训练一跑起来,GPU 空转等数据,那叫一个心疼。我见过太多团队,模型架构调得飞起,结果训练吞吐被数据 pipeline 卡死在 30% 利用率上。
今天咱们就把这块硬骨头啃下来。我会从预处理策略选择,到具体工具使用,再到缓存技巧,一条龙讲清楚。
4.1 在线预处理 vs 离线预处理
先解决最根本的问题:数据预处理到底在什么时候做?
离线预处理,就是训练开始前,把所有数据一次性处理好,存成中间格式。训练时直接读处理好的数据。
在线预处理,是训练过程中实时做数据增强、归一化这些操作。
我个人的经验是:能离线做的,尽量离线做。为什么?你想想看,GPU 算力多贵啊,让它等 CPU 做数据增强,太亏了。
| 对比维度 | 离线预处理 | 在线预处理 |
|---|---|---|
| 存储开销 | 高(需额外存储空间) | 低(原始数据即可) |
| 训练速度 | 快(数据直接可用) | 慢(实时计算消耗时间) |
| 数据增强灵活性 | 低(增强策略固定) | 高(可动态调整) |
| 适用场景 | 数据量大、增强策略稳定 | 需要随机增强、小数据集 |
4.2 使用 WebDataset 加速数据加载
说到数据加载加速,WebDataset 是我最近两年用得最顺手的工具。它解决了传统数据加载中最大的痛点:小文件太多。
我曾经接手过一个项目,训练数据是 500 万张图片,每张单独存成一个文件。你猜怎么着?光是文件系统 inode 查询就把 IO 打满了,GPU 利用率不到 20%。
WebDataset 的思路很简单:把大量小文件打包成 tar 包,每个 tar 包包含几千个样本。读取时按顺序流式读取,充分利用磁盘带宽。
# 传统方式:每个样本单独读取
dataset = ImageFolder('data/train/')
loader = DataLoader(dataset, batch_size=256, num_workers=8)
# WebDataset 方式:从 tar 包流式读取
import webdataset as wds
dataset = wds.WebDataset('data/train-{00000..00099}.tar') \
.decode('pil') \
.to_tuple('jpg;png', 'cls') \
.batched(256)
loader = DataLoader(dataset, batch_size=None, num_workers=8)
看到区别了吗?WebDataset 把文件打开、读取、解码这些操作全部流水线化了。实际测试中,同样的硬件配置,WebDataset 能把数据加载吞吐提升 3-5 倍。
4.3 TFRecord / RecordIO 格式
如果你在用 TensorFlow,那 TFRecord 几乎是绕不开的。PyTorch 生态里也有类似的 RecordIO 格式(主要在 MXNet 中使用)。
这两种格式本质上干的是同一件事:把结构化数据序列化成二进制流。好处很明显:
- 读取速度快:连续 IO,没有随机寻道
- 内存友好:可以按需读取,不需要一次性加载全部
- 跨平台兼容:二进制格式,不依赖操作系统文件系统特性
我习惯把 TFRecord 的创建过程写成一个独立的预处理脚本,跑一次就能生成训练所需的所有数据文件。
# 创建 TFRecord 示例
import tensorflow as tf
def serialize_example(image_bytes, label):
feature = {
'image': tf.train.Feature(bytes_list=tf.train.BytesList(value=[image_bytes])),
'label': tf.train.Feature(int64_list=tf.train.Int64List(value=[label])),
}
return tf.train.Example(features=tf.train.Features(feature=feature))
# 写入 TFRecord 文件
with tf.io.TFRecordWriter('data/train.tfrecord') as writer:
for image_bytes, label in dataset:
example = serialize_example(image_bytes, label)
writer.write(example.SerializeToString())
4.4 数据缓存策略
数据缓存,说白了就是用空间换时间。我见过最蠢的做法是每个 epoch 都重新从磁盘读一遍数据。你想想看,如果数据能塞进内存,为什么还要反复读磁盘?
常见的缓存策略有三种:
- 内存缓存:把整个数据集加载到内存中。适合数据集小于 64GB 的场景。
- 磁盘缓存:把预处理后的数据缓存到本地 SSD。适合分布式训练中每个节点有本地 SSD 的情况。
- 混合缓存:热点数据放内存,冷数据放磁盘。适合超大规模数据集。
在 PyTorch 里,我常用 torch.utils.data.Dataset 配合缓存机制:
class CachedDataset(Dataset):
def __init__(self, data_path, cache_size=10000):
self.data_path = data_path
self.cache = {}
self.cache_size = cache_size
def __getitem__(self, idx):
if idx in self.cache:
return self.cache[idx]
# 从磁盘读取并预处理
sample = self.load_and_process(idx)
# 缓存管理:如果缓存满了,淘汰最早的数据
if len(self.cache) >= self.cache_size:
oldest_key = next(iter(self.cache))
del self.cache[oldest_key]
self.cache[idx] = sample
return sample
torch.multiprocessing.Manager().dict() 来实现跨进程缓存共享。这样每个 worker 进程都能命中缓存,效果拔群。
4.5 知识体系总览
说了这么多,咱们用一张图把整个数据预处理加速的知识体系串起来:
这张图把咱们今天讲的内容串起来了。核心就一句话:消除 IO 瓶颈,让 GPU 吃饱。离线预处理解决计算开销,WebDataset/TFRecord 解决文件读取效率,缓存策略解决重复读取问题。三者配合,训练吞吐想不提升都难。
嗯,数据预处理这块就讲到这里。记住,别让数据加载成为你训练流程的短板。很多时候,调一调数据 pipeline,比调模型结构带来的收益更直接、更显著。
想要更多资料,联系可可