跳到主要内容

数据截至 (上游 commit cd6a3406572e)

04 · Hub 集成与数据加载

这一章讲什么: load_dataset("rajpurkar/squad") 这行代码背后的第一段旅程——怎么找到数据、怎么决定用什么格式读、构建好的 Arrow 缓存放在哪、为什么永远不会读到写了一半的缓存。读完你会明白这个库和 Hugging Face Hub 的耦合方式,以及「数据集即 git 仓库」的具体含义。


1. 它要解决的小问题

load_dataset 的第一个参数什么都能是:

  • Hub 仓库名("rajpurkar/squad")
  • 本地目录("./my_data/")
  • 格式名("csv",配合 data_files=...)
  • Hub 存储桶路径("buckets/user/bucket/dir")
  • 离线环境下,一个以前加载过的名字

这个字符串本身不带任何类型信息。库要先回答「数据在哪、是什么格式」,才能谈读取。答错代价很大:把本地目录当成 Hub 仓库名去网上找,或反过来,都是灾难。


2. 思路/直觉

分两段寻址,每段各用一种「键」:

  • 身份寻址(找数据):按优先级依次试——打包格式名?本地路径?Hub?每步试中即停。产出是一个「构建器 + 数据文件清单」。
  • 内容寻址(缓存构建结果):构建产物(Arrow 文件)放在 <cache>/<名字>/<配置>/<版本>/<内容哈希>/ 目录里。任何一环变了(上游发了新版、换了配置),路径就变,旧缓存自然隔离——不需要失效策略。

第二段和第 2 章的指纹缓存是同一哲学:用路径编码身份,让文件系统当数据库。


3. 图示:工厂链的判定顺序

怎么读这张图: 自上而下是 dataset_module_factory 的判定顺序,命中即返回;最底部是「全失败」时的兜底。

path 进来


① 是打包格式名?("csv"/"json"/"parquet"/...)
└─► PackagedDatasetModuleFactory load.py:1057
│ 否

② 以 .py 结尾 / 本地有同名脚本?
└─► 直接报错:"Dataset scripts are no longer supported" load.py:1066
│ 否

③ 是本地目录? └─► LocalDatasetModuleFactory load.py:1070
│ 否

④ "buckets/..." 开头? └─► HubBucketDatasetModuleFactory load.py:1075
│ 否

⑤ 形如 org/name?
└─► HubDatasetModuleFactory(钉 commit、读 README/YAML) load.py:1110
│ 全失败

⑥ 兜底:以前加载过? └─► CachedDatasetModuleFactory load.py:1203
│ 也没有

DatasetNotFoundError / FileNotFoundError

4. 原理演示

# 示意,非源码
def dataset_module_factory(path):
if path in PACKAGED_MODULES: # "csv" / "json" / ...
return PackagedModule(path)
if path.endswith(".py"):
raise RuntimeError("scripts are no longer supported") # 时代终结的标志
if os.path.isdir(path): # 本地目录
return LocalModule(path)
if looks_like_hub_repo(path): # "org/name"
try:
commit = pin_commit(path) # 把 main 钉死成 commit hash
readme = download(path, "README.md", revision=commit)
configs = parse_yaml_configs(readme) # 数据集卡即配置
return HubModule(path, commit, configs)
except HubUnreachable:
return CachedModule(path) # 离线兜底:用过就有缓存
raise DatasetNotFoundError(path)

重点看:没有注册中心、没有元数据库。「这个数据集存在吗、长什么样」的权威答案永远在 Hub 的 git 仓库里(或本地缓存里),工厂链只是按序去敲各扇门。


5. 真实实现

5.1 工厂链:按序敲门

完整判定逻辑在 dataset_module_factory(src/datasets/load.py:966)。各分支对应真实类:

分支位置
打包格式PackagedDatasetModuleFactorysrc/datasets/load.py:519
本地目录LocalDatasetModuleFactorysrc/datasets/load.py:410
Hub 仓库HubDatasetModuleFactorysrc/datasets/load.py:560
离线兜底CachedDatasetModuleFactorysrc/datasets/load.py:803
Hub 存储桶HubBucketDatasetModuleFactorysrc/datasets/load.py:845

两个值得知道的细节:

  • 脚本时代终结的代码遗迹仍在。 本地或 Hub 上发现与数据集同名的 .py 脚本,直接 raise RuntimeError("Dataset scripts are no longer supported, but found ...")(src/datasets/load.py:1066-1069:1174-1176)。历史上 Hub 数据集靠上传 Python 脚本自定义解析;现在全部改成「数据文件 + README YAML 配置」。
  • 兜底分支静默救场。 Hub 连不上时,若该数据集之前加载过,CachedDatasetModuleFactory.get_module 会在缓存目录里找最新版本直接用,只打一条 warning(src/datasets/load.py:817-839)——这就是"断网还能跑实验"的来源。

5.2 Hub 分支:先钉 commit,再读配置

Hub 分支做的第一件事是把 revision 钉死(src/datasets/load.py:1121-1130):hf_hub_download 下载 README.md 时传的是 revision(默认 "main"),返回的本地路径里就带着解析后的 commit hash,os.path.basename(os.path.dirname(...)) 把它抠出来。之后所有文件请求都用这个 commit——防止「读 README 时是一个版本、读数据时上游又推了新版」的竞态。

进入 HubDatasetModuleFactory.get_module(src/datasets/load.py:584)后:

  1. 下载 README,解析出数据集卡 YAML(:593-601);
  2. 再试下载独立的 .yaml 配置(REPOYAML_FILENAME),有就合并(:603-617);
  3. 从 YAML 里解析出 MetadataConfigs——即 configs: 列表,声明「这个数据集有哪些配置、每个配置用哪些数据文件、默认哪个」(:622 附近)。

数据集卡 README 因此兼任配置文件:元数据(描述、引用)和机器可读的数据划分规则在同一个 git 仓库里同版本演化。

顺带的 telemetry:每次 Hub 加载会 increase_load_count——向一个 S3 桶发 HEAD 请求累计下载数(src/datasets/load.py:197-208),离线时自动跳过。

5.3 数据文件怎么对应到 split

README 没写 configs: 时,靠文件名模式匹配(src/datasets/data_files.py)。按序尝试(ALL_SPLIT_PATTERNS + ALL_DEFAULT_PATTERNS,:99-104):

模式例子位置
SPLIT_PATTERN_SHARDEDdata/train-00000-of-00042.parquetdata_files.py:41
DEFAULT_PATTERNS_SPLIT_IN_FILENAMEtrain.csvtest-*.jsonldata_files.py:75
DEFAULT_PATTERNS_SPLIT_IN_DIR_NAMEtrain/data.csvdata_files.py:83
DEFAULT_PATTERNS_ALL兜底全量data_files.py:93

匹配结果汇成 DataFilesDict(data_files.py:648):split 名 → 文件列表。这就是为什么把文件命名成 train-*.parquet 就能自动进 train split。

5.4 download_and_prepare:缓存命中、锁与原子转正

构建器侧的主流程在 DatasetBuilder.download_and_prepare(src/datasets/builder.py:702)。依次四层防护:

  1. 缓存命中即返回(:842-848):输出目录里有 dataset_info.json 且模式是 REUSE_DATASET_IF_EXISTS,打条日志直接收工。这是"第二次跑 load_dataset 只要一秒"的全部秘密。
  2. 文件锁(:833-840):本地文件系统上先抢 <输出目录>_builder.lock,防止两个进程同时构建同一数据集;远程文件系统(S3/GCS)不上锁。
  3. 磁盘空间预检(:850-857):比 info.size_in_bytes 小就提前 OSError,不下载一半才发现。
  4. .incomplete 目录原子转正(:859-876):所有构建都写进 <目录>.incomplete,成功后 shutil.move 改名;异常时 finally 把临时目录删掉。任何时刻,正式路径下的缓存要么完整、要么不存在。

5.5 版本化缓存目录与初始指纹

缓存目录的四级结构由 _relative_data_dir(src/datasets/builder.py:628-646)拼出来:

~/.cache/huggingface/datasets/ ← HF_DATASETS_CACHE(config.py:158-159)
└── rajpurkar___squad/ ← namespace___数据集名
└── plain_text/ ← config_id(配置名+可选 data_dir)
└── 0.0.0/ ← config.version
└── <内容哈希>/ ← 数据文件清单的哈希
├── dataset_info.json
└── squad-train.arrow

_build_cache_dir(src/datasets/builder.py:648-680)还会扫同配置下的旧版本目录,发现版本不一致就打 warning——告诉你磁盘上躺着旧版,要不要清自己决定。

as_dataset 打开时的初始指纹也在这层诞生:_get_dataset_fingerprint(src/datasets/builder.py:1105-1111)哈希「相对目录(含版本+哈希)+ split 指令」。之后第 2 章的指纹链就从这里开始滚动。

5.6 split 指令
:10%
怎么落地

split="train[:10%]" 这类字符串由 ReadInstruction(src/datasets/arrow_reader.py:456)解析成绝对行区间,make_file_instructions(arrow_reader.py:92)再把区间分配到各个 shard 文件上(skip/take 对),ArrowReader 逐文件读出切片后拼接。所以 split 切片不需要重建缓存——同一批 Arrow 文件,train[:10%]train[10%:] 只是读不同的行区间。

5.7 回程:push_to_hub

Dataset.push_to_hub(repo_id)(src/datasets/arrow_dataset.py:6072)把数据集写成 Parquet 分片推到 Hub(纯 HTTP,不需要 git/git-lfs)。含 Image/Audio/Video 列时默认把外部文件的字节嵌进 Parquet(embed_external_files=True),使仓库自包含。推上去的仓库立刻能被别人的 load_dataset 走本章 ⑤ 号路径加载——写侧和读侧在这里闭环


6. 关键细节/坑

  • README 是配置,不只是给人看的。 改 Hub 仓库 README 的 configs: 字段会改变 load_dataset 的行为;反之,本地调试时 data_files= 参数可以绕过一切自动推断。
  • revision 不是 main 时会放弃 Hub 的 parquet 导出信息(use_exported_dataset_infos=False,src/datasets/load.py:1178-1183),加载可能变慢——走旧 commit 的代价。
  • gated repo 的报错文案是定制过的(src/datasets/load.py:1159-1168):401 提示先认证,403 直接给你申请权限的网址。踩到权限问题时照报错说的做即可。
  • 远程文件系统上没有构建锁(§5.4 第 2 条),两个进程同时往 S3 构建同一数据集需要自行协调。
  • 同名 .py 脚本是硬错误,不是警告。 老教程里「写一个 loading script」的做法在这个版本一律不可用。
  • 缓存目录默认在 ~/.cache/huggingface/datasets,可用 HF_DATASETS_CACHE 环境变量改(src/datasets/config.py:158-159)——训练集群上把它指到大容量盘是常见操作。

7. 代码地图

主题文件路径符号名
总入口src/datasets/load.pyload_datasetload_dataset_builder
工厂链src/datasets/load.pydataset_module_factory_DatasetModuleFactory 五个子类
Hub 模块解析src/datasets/load.pyHubDatasetModuleFactory.get_module
离线兜底src/datasets/load.pyCachedDatasetModuleFactory
下载计数src/datasets/load.pyincrease_load_count
文件→split 匹配src/datasets/data_files.pyresolve_patternDataFilesDictSPLIT_PATTERN_SHARDED
构建与缓存src/datasets/builder.pyDatasetBuilder.download_and_prepare_download_and_prepare_prepare_split_single
缓存目录布局src/datasets/builder.py_relative_data_dir_build_cache_dir
打开数据集src/datasets/builder.pyas_dataset_as_dataset_get_dataset_fingerprint
split 指令src/datasets/arrow_reader.pyReadInstructionmake_file_instructions
打包格式构建器src/datasets/packaged_modules/例:parquet/parquet.pyParquet(ArrowBasedBuilder)
推回 Hubsrc/datasets/arrow_dataset.pyDataset.push_to_hub