# Hub 本地缓存

本文档描述了 HF Hub 本地缓存的磁盘布局。它旨在作为在任何语言中重新实现缓存系统的参考。

以下是使用此缓存布局的库和应用程序的部分列表。请提交 PR 以添加您自己的项目。

### 库

| 库 | 语言 | 备注 |
|---------|----------|-------|
| [`huggingface_hub`](https://github.com/huggingface/huggingface_hub) | Python | 以及任何依赖它的库（例如 `transformers`、`diffusers`、`datasets`、`mlx`、`vllm` …） |
| [`hf-hub`](https://github.com/huggingface/hf-hub) | Rust | |
| [`swift-huggingface`](https://github.com/huggingface/swift-huggingface) | Swift | |
| [`@huggingface/hub`](https://github.com/huggingface/huggingface.js) | JavaScript | 仅支持 Node.js |
| [`SMILE`](https://github.com/haifengl/smile) | Java | 参见 [`HuggingFaceHub`](https://haifengl.github.io/api/java/smile/util/HuggingFaceHub.html) 文档 |

### 应用

| 应用 | 语言 | 备注 |
|-------------|----------|-------|
| [`llama.cpp`](https://github.com/ggml-org/llama.cpp) | C++ | 自 [#20775](https://github.com/ggml-org/llama.cpp/pull/20775) 起 |
| [`Llama-macOS`](https://github.com/ggml-org/Llama-macOS) | Swift | 基于 `llama.cpp` 构建的 macOS 应用 |
| [`HuggingFaceModelDownloader`](https://github.com/bodaay/HuggingFaceModelDownloader) | 前往 | |
| [`oMLX`](https://github.com/jundot/omlx) | Python | macOS MLX 推理服务器，自 [v0.4.0](https://github.com/jundot/omlx/commit/341aebbeaba1ca3452f83dedf03a0d102cef9fd4) 起 |

## 缓存位置

默认缓存目录为：

```
~/.cache/huggingface/hub
```

可以通过环境变量覆盖此设置：
- `HF_HUB_CACHE` - 缓存目录的直接路径（优先级最高）
- `HF_HOME` - Hugging Face 主目录的路径；如果已设置，缓存将位于 `$HF_HOME/hub`

## 概述

```
<CACHE_DIR>/
├── .locks/                                  # Lock files for concurrent download safety
├── blobs/                                   # Shared blob store: Xet files deduplicated across repos
├── models--<org>--<repo>/                   # Cached model repositories
├── datasets--<org>--<repo>/                 # Cached dataset repositories
└── spaces--<org>--<repo>/                   # Cached space repositories
```

每个下载的仓库对应一个扁平的单一文件夹。在每个仓库文件夹内，文件在内容寻址的 `blobs/` 目录中仅存储一次，并通过 `snapshots/` 符号链接进行访问。命名引用（分支、标签）记录在 `refs/` 中。自 `huggingface_hub` 1.32 起，使用 `hf_xet` 下载的文件在整个缓存中仅存储一次，位于缓存根目录下的 [共享 blob 存储](#shared-blob-store) 中。此时，该仓库的 `blobs/` 条目是指向该文件的符号链接。

## 模式

```
                     ┌──────────────────────────────────────────┐
                     │           Repository folder              │
                     │  models--julien-c--EsperBERTo-small      │
                     └──────────────┬───────────────────────────┘
                                    │
              ┌─────────────┬───────┴───────┬──────────────┐
              │             │               │              │
              v             v               v              v
          ┌───────┐    ┌────────┐    ┌────────────┐   ┌──────────┐
          │ refs/ │    │ blobs/ │    │ snapshots/ │   │.no_exist/│
          └───┬───┘    └────┬───┘    └──────┬─────┘   └────┬─────┘
              │             │               │              │
              │             │               │              │
   "main" contains     Files stored     One folder     Empty marker
   commit hash         by content       per commit     files for
   e.g. "aaaaaa"       hash (SHA-1      hash, e.g.     files known
                       or SHA-256)      aaaaaa/        not to exist
   Resolves a                           bbbbbb/
   branch/tag to                            │
   a snapshot ──────────────────────►  Contains symlinks
                                       to ../../blobs/{hash}
```

## 仓库文件夹命名

仓库以扁平目录的形式存储在缓存根目录下。文件夹名称编码了仓库类型和仓库 ID：

```
{type}s--{repo_id_with_slashes_replaced_by_--}
```

规则：
- 仓库类型是**复数形式**：`models`、`datasets`、`spaces`
- 仓库 ID 中的正斜杠 (`/`) 会被替换为 `--`
- 所有部分之间的分隔符是 `--`
- 大小写保持不变

示例：

| Hub 仓库 ID                          | 仓库类型 | 缓存文件夹名称                            |
|--------------------------------------|-----------|----------------------------------------------|
| `julien-c/EsperBERTo-small`         | model     | `models--julien-c--EsperBERTo-small`         |
| `huggingface/DataMeasurementsFiles`  | dataset   | `datasets--huggingface--DataMeasurementsFiles` |
| `dalle-mini/dalle-mini`             | space     | `spaces--dalle-mini--dalle-mini`             |

> [!NOTE]
> Buckets 不受此缓存管理，因为它们并非基于 git。请改用专用的 `hf buckets sync` 命令。

## 在仓库文件夹内

每个缓存的仓库都具有相同的内部结构：

```
<repo_folder>/
├── blobs/
├── refs/
├── snapshots/
└── .no_exist/    # may not always be present
```

### `blobs/`：基于内容寻址的文件存储

`blobs/` 目录存储实际的文件内容。每个文件都以其在 Hub 上的文件 etag 命名：

- **Git 跟踪的文件**：以其 **SHA-1** 哈希值（40 个十六进制字符）命名
- **Git LFS 文件**：以其 **SHA-256** 哈希值（64 个十六进制字符）命名

这是一个扁平目录——没有子目录。不同修订版本中相同的文件仅存储一次。每个条目要么是常规文件，要么是指向[共享 Blob 存储](#shared-blob-store)的相对符号链接；读取器应跟随该链接。

```
blobs/
├── 403450e234d65943a7dcf7e05a771ce3c92faa84dd07db4ac20f592037a1e4bd   # SHA-256 (LFS)
├── 7cb18dc9bafbfcf74629a4b760af1b160957a83e                           # SHA-1 (git)
└── d7edf6bd2a681fb0175f7735299831ee1b22b812                           # SHA-1 (git)
```

### `refs/`：分支和标签引用

`refs/` 目录将人类可读的引用（分支名称、标签、PR 编号）映射到提交哈希值。

每个引用都是一个纯文本文件，其中包含一行：完整的提交哈希值（40 个十六进制字符）。

```
refs/
├── main            # contains e.g. "bbc77c8132af1cc5cf678da3f1ddf2de43606d48"
├── 2.4.0           # a tag
└── refs/
    └── pr/
        └── 1       # pull request reference
```

当使用分支或标签名称下载文件时，对应的 ref 文件会创建或更新为最新的提交哈希值。

### `snapshots/`：修订版本视图

`snapshots/` 目录包含每个缓存版本（提交哈希）对应的一个子目录。每个版本目录都镜像了 Hub 上仓库的文件结构，但其中的文件是 **符号链接**，指向 `../../blobs/{hash}`。

```
snapshots/
├── 2439f60ef33a0d46d85da5001d52aeda5b00ce9f/
│   ├── README.md -> ../../blobs/d7edf6bd2a681fb0175f7735299831ee1b22b812
│   └── pytorch_model.bin -> ../../blobs/403450e234d65943a7dcf7e05a771ce3c92faa84dd07db4ac20f592037a1e4bd
└── bbc77c8132af1cc5cf678da3f1ddf2de43606d48/
    ├── README.md -> ../../blobs/7cb18dc9bafbfcf74629a4b760af1b160957a83e
    └── pytorch_model.bin -> ../../blobs/403450e234d65943a7dcf7e05a771ce3c92faa84dd07db4ac20f592037a1e4bd
```

主要属性：
- 符号链接使用**相对路径**：`../../blobs/{hash}`
- 如果某个文件在两个修订版本之间没有变化，则两个符号链接都指向**同一个 blob**，不会造成数据重复
- Hub 上子目录中的文件在快照中同样表示为子目录（完整的相对路径会被保留）

在快照之间切换类似于在本地 git 仓库中使用 `git checkout`。

### `.no_exist/`：不存在缓存

`.no_exist/` 目录用于跟踪那些被请求但在 Hub 上不存在的文件。这样可以避免对可选文件进行重复的 HTTP 请求。

结构与 `snapshots/` 一致：每个提交哈希对应一个子目录，其中包含以缺失文件命名的**空文件**（而非符号链接）。

```
.no_exist/
└── 2439f60ef33a0d46d85da5001d52aeda5b00ce9f/
    └── config_that_does_not_exist.json    # empty file
```

由于这些只是空的标记文件，磁盘占用可以忽略不计。

## 共享 Blob 存储

通过 Xet 下载的文件会在不同仓库之间进行去重。此类文件在缓存根目录下的 `blobs/` 中仅存储一次，每个需要该文件的仓库都会获得一个相对符号链接，而非文件副本：

```
<CACHE_DIR>/
├── blobs/
│   ├── .huggingface-shared-blobs                 # marker file, contains the layout version ("1")
│   └── 73/                                       # first two hex characters of the Xet hash
│       ├── 73b079dc7bd546aed26d20a82289bd752fc52a2fda13e7f2b551f3af73845b59                       # the file, named by its Xet hash (64 hex characters)
│       ├── 73b079dc7bd546aed26d20a82289bd752fc52a2fda13e7f2b551f3af73845b59.refs                  # manifest: one line per repo blob that references it (path relative to the cache root)
│       └── 73b079dc7bd546aed26d20a82289bd752fc52a2fda13e7f2b551f3af73845b59.lock                  # lock file
│
└── models--HuggingFaceTB--SmolLM2-135M-Instruct/
    └── blobs/
        └── 5af571cbf074e6d21a03528d2330792e532ca608f24ac70a143f6b369968ab8c -> ../../blobs/73/73b079dc7bd546aed26d20a82289bd752fc52a2fda13e7f2b551f3af73845b59
```

主要属性：
- 仓库条目保留其 etag 名称，并变为一个**相对符号链接**：`../../blobs/{xet_hash[:2]}/{xet_hash}`
- `snapshots/` 布局不会改变，因此 [文件解析](#file-resolution-logic) 不受影响
- 标记文件用于标识存储：如果缓存根目录下存在非空的 `blobs/` 目录但没有该标记文件，则该存储永远不会被使用或修改
- 该存储需要符号链接和 `hf_xet`。如果共享失败，文件将像以前一样存储在仓库文件夹中。`HF_HUB_DISABLE_SHARED_BLOBS=1` 可禁用该存储
- 只有新下载的文件会经过存储。由旧版本或其他客户端写入的文件仍保留为仓库文件夹中的常规文件，因此缓存中可能同时包含

仅读取缓存的实现无需任何更改：跟随符号链接的行为与之前相同。

从缓存中删除的实现应将仓库中作为符号链接的 blob 视为对存储的引用：
- 删除仓库 `blobs/` 中的符号链接，而不是它所指向的共享文件
- 在检查清单并删除之前，锁定 `<xet_hash>.lock`
- 仅当清单中列出的符号链接不再指向该共享文件及其 `.refs` 时，才删除它们
- 如果清单缺失或无法读取，请保留共享文件

有关删除行为以及旧版客户端的限制，请参阅 [`huggingface_hub` 缓存指南](https://huggingface.co/docs/huggingface_hub/guides/manage-cache#shared-blobs-across-repos)。

## 锁定文件

锁文件可防止并发进程同时下载同一个 blob。它们存储在缓存根目录下的 `.locks/` 目录中（不在仓库文件夹内）：

```
<CACHE_DIR>/.locks/<repo_folder_name>/<blob_hash>.lock
```

示例：
```
<CACHE_DIR>/.locks/models--julien-c--EsperBERTo-small/403450e234d65943a7dcf7e05a771ce3c92faa84dd07db4ac20f592037a1e4bd.lock
```

## 完整示例

```
~/.cache/huggingface/hub/
├── .locks/
│   └── models--julien-c--EsperBERTo-small/
│       └── 403450e234d65943a7dcf7e05a771ce3c92faa84dd07db4ac20f592037a1e4bd.lock
│
└── models--julien-c--EsperBERTo-small/
    ├── blobs/
    │   ├── [321M]  403450e234d65943a7dcf7e05a771ce3c92faa84dd07db4ac20f592037a1e4bd
    │   ├── [ 398]  7cb18dc9bafbfcf74629a4b760af1b160957a83e
    │   └── [1.4K]  d7edf6bd2a681fb0175f7735299831ee1b22b812
    │
    ├── refs/
    │   └── main    # contains "bbc77c8132af1cc5cf678da3f1ddf2de43606d48"
    │
    ├── snapshots/
    │   ├── 2439f60ef33a0d46d85da5001d52aeda5b00ce9f/
    │   │   ├── README.md         -> ../../blobs/d7edf6bd2a681fb0175f7735299831ee1b22b812
    │   │   └── pytorch_model.bin -> ../../blobs/403450e234d65943a7dcf7e05a771ce3c92faa84dd07db4ac20f592037a1e4bd
    │   │
    │   └── bbc77c8132af1cc5cf678da3f1ddf2de43606d48/
    │       ├── README.md         -> ../../blobs/7cb18dc9bafbfcf74629a4b760af1b160957a83e
    │       └── pytorch_model.bin -> ../../blobs/403450e234d65943a7dcf7e05a771ce3c92faa84dd07db4ac20f592037a1e4bd
    │
    └── .no_exist/
        └── 2439f60ef33a0d46d85da5001d52aeda5b00ce9f/
            └── optional_config.json    # empty file
```

请注意，`pytorch_model.bin` 在两个修订版本中都指向**同一个 blob**。这个 321 MB 的文件在磁盘上只存储了一次。

## 文件解析逻辑

要在磁盘上定位缓存文件：

1. **将修订版本解析为提交哈希值**
   - 如果修订版本已经是 40 位十六进制字符串，则直接使用它
   - 否则，读取 `refs/{revision}` 处的文件以获取提交哈希值

2. **检查快照**
   - 查找 `snapshots/{commit_hash}/{relative_path}`
   - 如果该文件存在（作为符号链接或普通文件），则会被缓存。请跟随符号链接以获取其内容

3. **检查不存在**
   - 查找 `.no_exist/{commit_hash}/{relative_path}`
   - 如果该文件存在，则已知该文件在此修订版本中不存在于 Hub 上

4. **缓存未命中**
   - 如果这两个路径都不存在，则说明该文件尚未被缓存

## Windows 行为

缓存依赖于**符号链接**。在无法使用符号链接的 Windows 系统上，缓存将以**降级模式**运行：实际的文件副本会直接放置在`snapshots/`中，而不是使用符号链接。在此模式下，`blobs/`目录和[共享 blob 存储](#shared-blob-store)不会被使用。

这意味着相同的文件内容可能会在不同版本中重复，从而增加磁盘占用。要在 Windows 上启用符号链接支持，请激活 [开发者模式](https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development) 或以管理员身份运行。

