Parquet 范式:大语言模型训练数据格式优化的基础解析

摘要
将数据格式转换为 Apache Parquet 并非一种随意的偏好,而是针对大规模数据处理(尤其是大语言模型 (LLM) 训练)的一项基础性能优化。Parquet 格式通过其列式存储架构,在 I/O 效率、存储成本和查询性能方面提供了数量级的提升。
本报告的结论是,Parquet 的使用并不仅限于强化学习 (RLHF) 阶段,而是贯穿 LLM 数据生命周期所有阶段(包括预训练、监督微调 (SFT) 和 RLHF)的最佳实践。
这种广泛采用的核心原因在于 Parquet 卓越的 I/O 性能——通过列裁剪和谓词下推显著减少数据读取量——以及其高效的压缩机制。更重要的是,Parquet 是现代 LLM 数据工具(如 Hugging Face datasets 库)性能模型的核心。该库利用 Parquet、Apache Arrow 和内存映射 (mmap) 技术的组合,实现了在极低内存占用下处理海量数据集的能力。
I. 基础范式:解构列式存储与行式存储
选择数据格式是一种算法优化,它决定了数据在磁盘上的物理布局如何与计算模式(如模型训练)相匹配。Parquet 的设计初衷是解决 CSV 或 JSON 等传统行式格式在分析工作负载中的根本性低效问题。
A. 行式格式 (CSV/JSON) 在分析型负载下的低效
CSV 和 JSON 等传统格式以“行”为单位存储数据。例如,一个 CSV 文件在磁盘上的布局是:
row1_colA, row1_colB, row1_colC... \n row2_colA, row2_colB, row2_colC...
这种布局非常适合“在线事务处理”(OLTP) 系统,例如需要读取或写入单个完整记录(如一条客户订单)的场景。
然而,LLM 数据处理是典型的“在线分析处理”(OLAP) 工作负载。训练作业通常不关心某一个文档的所有信息;它关心的是十亿个文档的*“text”列*。在行式格式中,为了读取“text”列,系统必须从磁盘读取每一行(包括“url”、“timestamp”和“metadata”等所有其他列),在内存中解析它们,然后丢弃大部分不相关的数据。在 TB 级数据集上,这会导致灾难性的 I/O 浪费和计算开销。
B. Parquet 架构:行组、列块与元数据
Parquet 是一种开源、语言无关的列式存储格式,其灵感源于 Google 的 Dremel 论文。它采用了一种混合结构,并非纯粹的列式。
一个 Parquet 文件首先被水平分区成大型的行组 (Row Groups)(例如,128 MB 到 1 GB)。在每个行组内部,数据被垂直存储为列块 (Column Chunks)。文件的末尾包含一个元数据页脚,其中记录了 schema 信息,以及至关重要的——每个列块的统计信息(例如最小值、最大值)。
这种混合模型是一个巧妙的折中:
- 行组成为并行处理的基本工作单元(例如,一个 Spark 执行器可以处理一个行组)。
- 列块提供了列式存储的 I/O 效率。
C. I/O “银弹”:列裁剪与谓词下推
Parquet 的架构实现了两种强大的 I/O 优化技术,这是 CSV 或 JSON 无法做到的。
-
列裁剪 (Column Pruning)
列裁剪是一种“垂直跳过”。由于数据是按列块存储的,当查询只需要column_A和column_C时,系统可以完全跳过磁盘上column_B的数据块。对于一个包含
['text', 'url', 'timestamp', 'metadata_json']列的 LLM 预训练数据集,一个只读取['text']列的训练脚本,将自动“裁剪”掉所有其他列的数据。这可能意味着跳过了磁盘上 90% 的数据,只读取必要的内容,从而极大地减少了 I/O。 -
谓词下推 (Predicate Pushdown)
谓词下推是一种“水平跳过”。它将查询中的过滤条件(即“谓词”,例如WHERE language = 'en')下推到存储层,在数据被读取之前进行过滤。Parquet 文件元数据中存储的统计信息(最小值/最大值)是实现这一功能的关键。当查询引擎(如 Spark、DuckDB 或 PyArrow)看到一个过滤器(例如
WHERE token_count > 512)时,它会首先检查元数据。如果某个 1 GB 大小的行组的元数据显示其token_count_max = 400,引擎就“知道”这个行组中没有任何数据能满足查询条件。因此,引擎会跳过读取这整个 1 GB 的数据块,甚至不会触碰相关的磁盘区域。
D. “秘密武器”:编码与压缩
Parquet 的高压缩比并不仅仅是应用了 Gzip。它在每个列块上应用压缩,由于列中存储的是同质(类型相同)的数据,因此压缩效率远高于压缩混合了各种类型数据的 CSV 文件。
这是一个两级过程:
-
编码 (Encoding - L1): 在压缩之前,Parquet 会根据数据特征应用特定的编码方案。
- 字典编码 (Dictionary Encoding): 适用于低基数(重复值多)的列,如“language”或“source”。例如,一列
['en', 'en', 'fr', 'en']会被替换为Dictionary: ['en', 'fr']和Data: [0, 0, 1, 0]。 - 游程编码 (Run-Length Encoding, RLE): 适用于有连续重复值的列(如排序后的数据)。例如
[0, 0, 0, 0, 1, 1, 1]变为(0, 4), (1, 3)。 - 增量编码 (Delta Encoding): 适用于时间戳或 ID 等序列数据。
- 字典编码 (Dictionary Encoding): 适用于低基数(重复值多)的列,如“language”或“source”。例如,一列
-
压缩 (Compression - L2): 经过编码的数据(现在主要是小整数)被送入一个标准压缩器,如 Snappy(速度快,压缩比适中)、Gzip(速度慢,压缩比高)或 ZSTD(现代压缩算法,压缩比高且解压速度快)。
对于 LLM 的文本数据,ZSTD 被认为是一个“绝佳的选择”,因为它无论使用何种编码方式,都能提供比 Snappy 更小的文件体积。这种“先编码,后压缩”的策略是 Parquet 文件体积远小于 CSV 的核心原因。
E. 原生处理复杂嵌套数据
LLM 的原始数据通常是复杂的嵌套 JSON。Parquet 使用了 Dremel 论文中的“记录粉碎和组装”算法 (record-shredding and assembly),该算法基于“重复级别”和“定义级别”,能够将复杂的嵌套结构(如 struct 和 array)“压平”并存储为扁平的列。
这意味着原始 JSON 的结构信息(例如 {'user': {'name': 'A'}, 'posts': [{'id': 1}, {'id': 2}]})不会丢失。Parquet 会将其“粉碎”为 user.name 和 posts.id 等扁平列,并在读取时使用元数据完美地重建原始的嵌套结构。
II. 定量影响:Parquet 的数据驱动案例
从理论转向实践,Parquet 带来的影响是具体且可量化的。一项针对 1TB 数据集的直接 A/B 测试(CSV vs Parquet)展示了惊人的结果。
A. 99% 的缩减:存储、性能与成本
分析显示,Parquet 在所有关键指标上都实现了数量级的优化。
表 1:CSV 与 Parquet 性能的定量比较(基于 1TB 数据集)
| 指标 | 存储为 CSV | 存储为 Parquet | 优势(因子/百分比) |
|---|---|---|---|
| 存储大小 | 1 TB | 130 GB | 节省 87% 存储空间 |
| 查询运行时间 | 236 秒 | 6.78 秒 | 速度提升 34.8 倍 |
| 数据扫描量 | 1.15 TB | 2.51 GB | 减少 99.8% 数据扫描 |
| 查询成本 | $5.75 | $0.01 | 节省 99.7% 成本 |
(数据来源:[1])
这种性能提升存在清晰的因果链:
- 87% 的存储缩减是编码和压缩(章节 I.D)的直接结果。
- 99.8% 的数据扫描量缩减是列裁剪和谓词下推(章节 I.C)的直接结果。
- 34.8 倍的速度提升是“数据扫描量缩减 99.8%”的必然结果。
需要注意的是,其他分析(例如 [5] 中对 DuckDB 的分析)显示了更保守的估计,约为 5 倍的体积缩减和 3 倍的扫描速度提升。这并不矛盾。[1] 的测试很可能代表了“最佳情况”的分析型查询(例如 SELECT AVG(one_column)),此时列裁剪的优势最大。3 倍至 34 倍的范围代表了从全表扫描到高选择性查询的真实性能增益区间。
III. 关键链接:Parquet 在现代 LLM 数据加载栈中的地位
Parquet 不仅仅是一个文件格式,它是现代 LLM 数据加载栈(尤其是 Hugging Face)得以实现高性能的关键组件。
A. “Parquet-on-Disk, Arrow-in-Memory” 管道
在实际应用中,Parquet 与 Apache Arrow 协同工作:
- Parquet 是优化的磁盘存储格式。
- Apache Arrow 是一种内存中的列式格式。
- PyArrow 是用于读写 Parquet 的 Python 库。
读取操作的流程是:PyArrow 读取磁盘上的 Parquet 列块,对其进行解压(例如 ZSTD)和解码(例如 RLE),然后将其加载为 Arrow 的内存中数组。
一旦数据进入 Arrow 的内存格式,它就可以通过零复制读取 (zero-copy reads) [7] 传递给其他工具,如 Pandas、NumPy 乃至 PyTorch。这意味着 PyTorch 可以获得一个指向 Arrow 数组中数据的直接指针,而无需进行任何序列化、反序列化或内存复制。这个过程为饥渴的 GPU 提供了极高的数据加载吞吐量,节省了数十亿的 CPU 周期。
B. Hugging Face datasets 与内存映射 (mmap) 的“魔法”
Hugging Face datasets 库是 LLM 生态的事实标准,它将 Arrow 作为其本地缓存系统。该库会自动将其他格式(如 CSV/JSON)转换为 Parquet,然后再转换为其 Arrow 缓存。
这套机制是 LLM 从业者感受到的“魔法”的来源:
- 用户运行
load_dataset("some_dataset.jsonl")。 datasets库解析该 JSONL 文件,并将其转换为 Arrow 格式的缓存文件,存储在磁盘上。- 该 Arrow 缓存文件随后被内存映射 (memory-mapped, mmap)。
内存映射 (mmap) 是一种操作系统功能,它将磁盘上的文件映射到进程的虚拟内存地址空间。关键在于,数据并不会被加载到 RAM 中。当 Python 代码请求 dataset[500_000_000](第 5 亿条记录)时,操作系统会接管 I/O,只从磁盘中调入包含该记录的那一个数据块。
[8] 中的一个例子极具说服力:加载完整的英文维基百科数据集(18 GB),仅占用了 50 MB 的 RAM。这 50 MB 只是用于元数据和指针;18 GB 的数据仍然在磁盘上,但可以通过虚拟内存被即时访问。
需要注意的是,正如 [9] 所澄清的,Parquet 文件本身(由于其压缩和编码)无法被直接高效地 mmap。它必须首先被读取、解压和解码为一个可 mmap 的格式——而 datasets 库的 Arrow 缓存正是这个格式。
因此,Parquet 是高效的存储格式,而 Arrow(通过 mmap)是高效的内存访问层。
C. 流式处理与分布式训练
在(100GB+)大规模分布式训练中,Parquet 是云存储的理想选择。然而,一些框架(如 MosaicML 的 StreamingDataset)认为,对于超大规模的随机打乱和随机访问,Parquet 等格式“存在不足” [14],并因此提出了它们自己的分片格式 (MDS)。
这并非矛盾。Parquet 是行业标准的、经过处理和分词的数据集“真理之源”。当需要将这些数据流式传输到数千个 GPU 时,可能会发生进一步的转换,将其变为针对流式传输优化的格式(如 MDS)。但这个流式转换的输入源,几乎总是 Parquet 文件。
IV. Parquet 在 LLM 全生命周期中的角色
通过分析 LLM 开发的每个阶段所使用的数据集格式,可以清晰地看到一个演进趋势。认为 Parquet 只在特定阶段(如 RLHF)使用的观点是片面的。
A. 阶段 1:预训练(数 TB 的挑战)
- 历史背景:
- C4 数据集: 最初以 JSON 格式发布,大小约 305GB [10]。
- The Pile 数据集: 最初以 JSONLines (JSONL) 格式发布,并使用 zstandard 压缩(.jsonl.zst),大小为 825 GiB [11]。
- 现代标准:
- RefinedWeb 数据集(用于 Falcon LLM): 这是一个解压后达 2.8TB 的海量数据集 [12],用于训练高性能的 Falcon 模型。该数据集原生以 Parquet 格式分发。
表 2:大型预训练数据集格式的演进
| 数据集 | 主要用途 | 原始格式 (Crawl) | 发布/处理格式 | 大小 |
|---|---|---|---|---|
| C4 | T5 预训练 | (Common Crawl) | JSON | ~305GB [10] |
| The Pile | GPT-Neo/J 预训练 | 多种来源 | JSONLines (.jsonl.zst) [11] | 825 GiB [11] |
| RefinedWeb | Falcon 预训练 | WARC/WET [12] | Parquet [12] | 2.8TB [12] |
结论: 行业趋势清晰可见。如表 2 所示,虽然早期的基石数据集 (C4, Pile) 使用 JSON/JSONL,但最新、规模最大的数据集 (RefinedWeb) 已经标准化为 Parquet。
这是一个合乎逻辑的演进:在 300GB 时,JSON/JSONL 虽然慢,但尚可管理;在 825GB 时,它已成为严重瓶颈;而在 2.8TB+ 的规模下,基于 JSON 的格式在计算和成本上是不可行的。RefinedWeb 的创建者选择了 Parquet,因为只有 Parquet 能够高效地处理该规模的数据,允许研究人员利用谓词下推(例如按 URL 过滤)和列裁剪(只选择 ‘text’ 列)。
B. 阶段 2:监督微调 (SFT)
SFT 需要结构化的“输入-输出”对,或“提示-完成”对 (prompt-completion)。这通常是 {'prompt': '...', 'response': '...'} 结构。
虽然一个 SFT 数据集可能很小(例如 10,000 行),可以存储为 JSONL,但所有支持 Parquet 的理由依然成立。这些数据是高度结构化和列式的 (‘prompt’, ‘response’)。
随着 SFT 数据集从 1 万行增长到 1000 万行,使用 Parquet 成为最佳实践。它允许 datasets 库使用 mmap 节省内存,并允许分词器仅在 ‘prompt’ 列上运行(通过列裁剪),而无需将 ‘response’ 列加载到内存中。
C. 阶段 3:对齐与强化学习 (RLHF)
这是 Parquet 优势最明显的领域之一。
-
奖励模型 (RM) 训练: RLHF 的第一步是训练一个奖励模型。这需要一个“偏好数据集”。这种格式是高度结构化的,通常是
{'prompt': '...', 'chosen_response': '...', 'rejected_response': '...'}。这是一个完美的 Parquet 用例。它是一个具有 3-4 个文本列的表格。章节 I.C(列裁剪)和 I.D(压缩)的所有论点均适用。研究人员可以高效地查询仅提示,或仅被选中的回复。
-
PPO (策略) 训练: 最后一步是使用 PPO 算法来微调 LLM。这个过程涉及从数据集中流式传输提示 (prompts) 到模型,然后生成响应(一个“轨迹”)。
直接证据: 一个生产级的 RL 库的
ppo_trainer.yaml配置文件 [15] 明确地将训练和验证数据的路径定义为:data.train_files: ~/data/rlhf/gsm8k/train.parquet data.val_files: ~/data/rlhf/gsm8k/test.parquet
综合结论: Parquet 确实在 RLHF 中被广泛使用。然而,其因果关系经常被误解。
Parquet 不是因为它是 RLHF 才被使用,而是因为 RLHF(如同预训练和 SFT)是一个高吞吐量、分析规模的数据处理任务。使其成为大数据分析 [1] 和现代预训练数据集 [12] 默认选择的那些特性,同样使其成为处理 RLHF 结构化、列式数据的显而易见、合乎逻辑的选择。
V. 综合分析与最终建议
A. 最终结论
-
为什么要转换? 因为 Parquet 的列式架构(章节 I)在存储、成本和 I/O 性能方面提供了数量级的改进(章节 II)。一个 1TB 的 CSV 变成了 130GB 的 Parquet,一个 4 分钟的查询变成了 6 秒的查询,因为数据扫描量减少了 99% [1]。
-
适用阶段: 认为 Parquet “只用于 RLHF”的观点是片面的。
- RLHF: 是的,它在 RLHF 中被明确使用 [15],因为偏好数据(
prompt,chosen,rejected)是完美的列式数据。 - SFT: 它是 SFT 的最佳实践,因为
prompt/response对也是完美的列式数据。 - 预训练: 它已成为最先进的、数 TB 规模的预训练数据集(如 RefinedWeb)的新标准 [12],取代了老旧、低效的 JSON/JSONL 格式 [10, 11]。
- RLHF: 是的,它在 RLHF 中被明确使用 [15],因为偏好数据(
-
统一因素(真正的“为什么”): Parquet 之所以无处不在,真正的核心原因是 Hugging Face
datasets库。该库是 LLM 生态系统中加载数据的事实标准。其整个高性能模型都建立在Parquet-on-disk -> Arrow-in-memory -> mmap这一管道之上 [8, 9]。正是这个管道,允许单个研究人员在笔记本电脑上仅用几十 MB 的 RAM 就能加载 TB 级的海量数据集 [8]。因此,将数据转换为 Parquet 是解锁整个 MLOps 工具链全部性能的第一步。
B. 战略建议:LLM 数据处理管道
基于此分析,推荐采用以下标准化的 LLM 数据处理流程:
- 摄入(原始): 你的源数据(JSON, CSV, WET 文件)是原始、未经处理的层。
- 处理(分词): 使用分布式框架(如 Spark)或
datasets.map()来清洗、过滤和分词这些原始数据。 - 标准化(存储): 将处理过的、分词后的、结构化的数据保存为 Parquet 格式(推荐使用 ZSTD 压缩)。这个 Parquet 数据集成为你的“黄金标准”真理之源。
- 训练(加载): 在所有训练阶段(预训练、SFT、RLHF),将你的
datasets加载器 [8] 或流式加载器 [14] 指向这个 Parquet 数据集。这将利用 Parquet->Arrow->mmap 管道 [9] 实现最大的 I/O 效率,确保你的 GPU 永远不会“挨饿”。
