改进 Hugging Face Hub 上的 Parquet 重复数据删除
Hugging Face 的 Xet 团队正在努力提高 Hub 存储架构的效率,使用户能够更轻松、更快速地存储和更新数据和模型。由于 Hugging Face 托管了近 11PB 的数据集,其中仅 Parquet 文件就占用了超过 2.2PB 的存储空间,因此优化 Parquet 存储的优先级非常高。
大多数 Parquet 文件都是从各种数据分析管道或数据库批量导出的,通常以完整快照而非增量更新的形式出现。当用户希望定期更新数据集时,数据去重变得至关重要。只有通过去重,我们才能尽可能紧凑地存储所有版本,而无需在每次更新时重新上传所有内容。在理想情况下,我们应该能够以仅比其最大版本稍大的空间存储不断增长的数据集的每个版本。
我们的默认存储算法使用字节级内容定义分块 (CDC),该算法通常在插入和删除时具有良好的去重效果,但 Parquet 布局带来了一些挑战。在这里,我们使用来自 FineWeb 数据集的一个包含 1,092,000 行的 2GB Parquet 文件进行实验,以了解一些简单修改在 Parquet 文件上的表现,并使用我们的去重估算器生成可视化。
背景
Parquet 表通过将表分成行组工作,每个行组包含固定数量的行(例如 1000 行)。然后,行组中的每一列都被压缩并存储。
直观地说,这意味着不干扰行分组的操作,如修改或追加,应该能够很好地去重。那么让我们来测试一下!
追加
在这里,我们向文件中追加 10,000 行新数据,并将结果与原始版本进行比较。绿色代表所有去重块,红色代表所有新块,介于两者之间的阴影表示不同级别的去重。
我们可以看到,我们确实能够对几乎整个文件进行去重,但只看到了文件末尾的更改。新文件去重率为 99.1%,仅需要 20MB 的额外存储空间。这与我们的直觉非常吻合。
修改
鉴于其布局,我们预期行修改会非常独立,但事实并非如此。在这里,我们对第 10000 行进行了少量修改,我们发现虽然文件的大部分内容都去重了,但仍有许多小而规律间隔的新数据段!
快速扫描 Parquet 文件格式表明绝对文件偏移量是 Parquet 列头的一部分(请参阅 ColumnChunk 和 ColumnMetaData 结构)!这意味着任何修改都可能重写所有列头。因此,尽管数据去重效果良好(大部分为绿色),但每个列头中都会有新的字节。
在这种情况下,新文件仅去重 89%,需要额外 230MB 的存储空间。
删除
在这里,我们从文件中间删除了一行(注意:插入应具有类似的行为)。由于这会重新组织整个行组布局(每个行组为 1000 行),我们发现虽然文件的前半部分去重了,但剩余文件有全新的块。
这主要是因为 Parquet 格式会积极压缩每列。如果我们关闭压缩,我们可以更积极地去重。
然而,如果我们将数据未压缩存储,文件大小会增加近 2 倍。
是否可以同时实现去重和压缩的优点?
内容定义行组
一个潜在的解决方案是不仅使用字节级 CDC,而且将其应用于行级别:我们不再根据绝对计数(1000 行)分割行组,而是根据提供的“键”列的哈希值进行分割。换句话说,当键列的哈希值 % [目标行数] = 0 时,我就分割出一个行组,并允许最小和最大行组大小。
我在这里快速且低效地演示了一个实验:https://gist.github.com/ylow/db38522fb0ca69bdf1065237222b4d1c。
通过这种方式,即使我删除了一行,我们也能够有效地对压缩的 Parquet 文件进行去重。在这里,我们清楚地看到一个大的红色块,代表重写的行组,随后每个列头都有一个小的更改。
优化 Parquet 以实现去重能力
根据这些实验,我们可以考虑通过几种方式改进 Parquet 文件的去重能力:
- 对于文件结构数据,使用相对偏移量而不是绝对偏移量。这将使 Parquet 结构与位置无关,易于“memcpy”,尽管这是一个涉及文件格式更改的工作,可能难以实现。
- 支持行组上的内容定义分块。该格式目前已支持此功能,因为它不要求行组大小统一,因此这可以在最小影响范围内完成。只需更新 Parquet 格式写入器即可。
虽然我们将继续探索提高 Parquet 存储性能的方法(例如:我们是否可以选择在上传前重写 Parquet 文件?在上传时剥离绝对文件偏移量并在下载时恢复?),我们很乐意与 Apache Arrow 项目合作,看看是否有兴趣在 Parquet/Arrow 代码库中实现其中一些想法。
同时,我们也在探索我们的数据去重过程在其他常见文件类型上的行为。请务必尝试我们的 去重估算器 并告诉我们您的发现!