# DDUF

    
     

## 概述

DDUF (**D**DUF，即 **D**iffusion **U**nified **F**ormat) 是一种用于扩散模型的单文件格式，旨在通过将模型的所有组件打包到单个文件中，来统一不同的模型分发方式和权重保存格式。该格式与编程语言无关，并且支持从远程位置解析，而无需下载整个文件。

这项工作借鉴了 [GGUF](https://github.com/ggerganov/ggml/blob/master/docs/gguf.md) 格式。

查看 [DDUF](https://huggingface.co/DDUF) 组织，开始使用 DDUF 中一些最流行的扩散模型。

> [!TIP]
> 我们热烈欢迎各种贡献！
>
> 为了创建一个被广泛采用的文件格式，我们需要来自社区的早期反馈。一切尚未定论，我们重视每个人的意见。如果您的用例未被涵盖，请在 DDUF 组织的[讨论](https://huggingface.co/spaces/DDUF/README/discussions/2)中告诉我们。

其主要功能包括以下内容。

1. **单文件**打包。
2. 基于 **ZIP 文件格式**，以利用现有的工具。
3. 无压缩，确保 **`mmap` 兼容性**，以实现快速加载和保存。
4. **语言无关**：工具可以用 Python、JavaScript、Rust、C++ 等语言实现。
5. **支持 HTTP**：元数据和文件结构可以通过 HTTP Range 请求远程获取。
6. **灵活**：每个模型组件都存储在自己的目录中，遵循当前的 Diffusers 结构。
7. **安全**：使用 [Safetensors](https://huggingface.co/docs/diffusers/using-diffusers/other-formats#safetensors) 作为权重保存格式，并禁止嵌套目录以防止 ZIP 炸弹。

## 技术规格

从技术角度来看，`.dduf` 文件**就是**一个[`.zip` 归档](https://en.wikipedia.org/wiki/ZIP_(file_format))。通过采用一种通用支持的文件格式，我们确保了已有成熟的工具链。不过，为了满足扩散模型的要求，我们施加了一些约束：
- 数据必须以未压缩格式存储（标志为 `0`），以便使用内存映射进行延迟加载。
- 数据必须使用 ZIP64 协议存储，以支持保存大于 4GB 的文件。
- 归档只能包含 `.json`、`.safetensors`、`.model` 和 `.txt` 文件。
- 归档的根目录下必须存在一个 `model_index.json` 文件。该文件必须包含一个键值映射，其中包含有关模型及其组件的元数据。
- 每个组件必须存储在其自己的目录中（例如，`vae/`、`text_encoder/`）。嵌套文件必须使用 UNIX 风格的路径分隔符（`/`）。
- 每个目录都必须对应`model_index.json`索引中的一个组件。
- 每个目录必须包含一个 json 配置文件（`config.json`、`tokenizer_config.json`、`preprocessor_config.json` 或 `scheduler_config.json` 之一）。
- 禁止使用子目录。

想检查您的文件是否有效？可以使用此 Space 进行检查：https://huggingface.co/spaces/DDUF/dduf-check。

## 用法

`huggingface_hub` 提供了在 Python 中处理 DDUF 文件的工具。它包含用于验证文件完整性的内置规则，以及用于读取和导出 DDUF 文件的辅助函数。我们的目标是让这套工具在 Python 生态系统中得到采用，例如在 `diffusers` 集成中。也可以为其他语言（JavaScript、Rust、C++ 等）开发类似的工具。

### 如何读取 DDUF 文件？

将路径传递给 `read_dduf_file` 以读取 DDUF 文件。此操作仅读取元数据，因此是一个轻量级调用，不会导致内存占用激增。在下面的示例中，假设您已在本地下载了 [`FLUX.1-dev.dduf`](https://huggingface.co/DDUF/FLUX.1-dev-DDUF/blob/main/FLUX.1-dev.dduf) 文件。

```python
>>> from huggingface_hub import read_dduf_file

# Read DDUF metadata
>>> dduf_entries = read_dduf_file("FLUX.1-dev.dduf")
```

`read_dduf_file` 返回一个映射，其中每个条目对应 DDUF 归档中的一个文件。文件由 `DDUFEntry` 数据类表示，该数据类包含条目在原始 DDUF 文件中的文件名、偏移量和长度。这些信息对于在不加载整个文件的情况下读取其内容非常有用。在实际操作中，您无需处理底层读取，而是可以依赖辅助函数。

例如，以下是如何加载 `model_index.json` 的内容：
```python
>>> import json
>>> json.loads(dduf_entries["model_index.json"].read_text())
{'_class_name': 'FluxPipeline', '_diffusers_version': '0.32.0.dev0', '_name_or_path': 'black-forest-labs/FLUX.1-dev', ...
```

对于二进制文件，您希望使用 `as_mmap` 来访问原始字节。这会以内存映射的形式返回原始文件的字节。内存映射允许您只读取所需的字节，而无需将全部内容加载到内存中。例如，以下是加载 safetensors 权重的方法：

```python
>>> import safetensors.torch
>>> with dduf_entries["vae/diffusion_pytorch_model.safetensors"].as_mmap() as mm:
...     state_dict = safetensors.torch.load(mm) # `mm` is a bytes object
```

> [!TIP]
> `as_mmap` 必须在上下文管理器中使用，才能受益于内存映射的特性。

### 如何编写 DDUF 文件？

将文件夹路径传递给 `export_folder_as_dduf` 以导出 DDUF 文件。

```python
# Export a folder as a DDUF file
>>> from huggingface_hub import export_folder_as_dduf
>>> export_folder_as_dduf("FLUX.1-dev.dduf", folder_path="path/to/FLUX.1-dev")
```

该工具会扫描文件夹，添加相关条目，并确保导出的文件有效。如果在此过程中出现任何问题，将引发 `DDUFExportError`。

为了获得更大的灵活性，请使用 [`export_entries_as_dduf`] 来明确指定要包含在最终 DDUF 文件中的文件列表：

```python
# Export specific files from the local disk.
>>> from huggingface_hub import export_entries_as_dduf
>>> export_entries_as_dduf(
...     dduf_path="stable-diffusion-v1-4-FP16.dduf",
...     entries=[ # List entries to add to the DDUF file (here, only FP16 weights)
...         ("model_index.json", "path/to/model_index.json"),
...         ("vae/config.json", "path/to/vae/config.json"),
...         ("vae/diffusion_pytorch_model.fp16.safetensors", "path/to/vae/diffusion_pytorch_model.fp16.safetensors"),
...         ("text_encoder/config.json", "path/to/text_encoder/config.json"),
...         ("text_encoder/model.fp16.safetensors", "path/to/text_encoder/model.fp16.safetensors"),
...         # ... add more entries here
...     ]
... )
```

`export_entries_as_dduf` 在模型已保存到磁盘的情况下运行良好。但如果你有一个加载在内存中的模型，并希望直接将其序列化为 DDUF 文件呢？`export_entries_as_dduf` 允许你通过提供一个 Python `generator` 来实现这一点，该生成器说明了如何迭代地序列化数据：

```python
(...)

# Export state_dicts one by one from a loaded pipeline
>>> def as_entries(pipe: DiffusionPipeline) -> Generator[Tuple[str, bytes], None, None]:
...     # Build a generator that yields the entries to add to the DDUF file.
...     # The first element of the tuple is the filename in the DDUF archive. The second element is the content of the file.
...     # Entries will be evaluated lazily when the DDUF file is created (only 1 entry is loaded in memory at a time)
...     yield "vae/config.json", pipe.vae.to_json_string().encode()
...     yield "vae/diffusion_pytorch_model.safetensors", safetensors.torch.save(pipe.vae.state_dict())
...     yield "text_encoder/config.json", pipe.text_encoder.config.to_json_string().encode()
...     yield "text_encoder/model.safetensors", safetensors.torch.save(pipe.text_encoder.state_dict())
...     # ... add more entries here

>>> export_entries_as_dduf(dduf_path="my-cool-diffusion-model.dduf", entries=as_entries(pipe))
```

### 使用 Diffusers 加载 DDUF 文件

Diffusers 内置了对 DDUF 文件的支持。以下是一个从 Hub 上存储的检查点加载 pipeline 的示例：

```py
from diffusers import DiffusionPipeline
import torch

pipe = DiffusionPipeline.from_pretrained(
    "DDUF/FLUX.1-dev-DDUF", dduf_file="FLUX.1-dev.dduf", torch_dtype=torch.bfloat16
).to("cuda")
image = pipe(
    "photo a cat holding a sign that says Diffusers", num_inference_steps=50, guidance_scale=3.5
).images[0]
image.save("cat.png")
```

## F.A.Q.

### 为什么要在 ZIP 之上构建？

ZIP 提供了多项优势：
- 通用支持的文件格式
- 读取无需额外依赖
- 内置文件索引
- 广泛的语言支持

### 为什么不在归档文件开头使用带有目录的 TAR？

请参阅此[评论](https://github.com/huggingface/huggingface_hub/pull/2692#issuecomment-2519863726)中的说明。

### 为什么没有压缩？

- 启用大文件的直接内存映射
- 确保远程文件访问的一致性和可预测性
- 防止文件读取期间的 CPU 开销
- 保持与 safetensors 的兼容性

### 我可以修改 DDUF 文件吗？

否。目前，DDUF 文件被设计为不可变的。若要更新模型，请创建一个新的 DDUF 文件。
	
### 哪些框架/应用支持 DDUF？
	
- [Diffusers](https://github.com/huggingface/diffusers)
	
我们一直在积极联系其他库和框架。如果您有兴趣为您的项目添加支持，请在[DDUF 组织](https://huggingface.co/spaces/DDUF/README/discussions)中发起一个讨论。

