哲学
🧨 Diffusers 提供了跨多种模态的**最先进的**预训练扩散模型。其目的是作为推理和训练的**模块化工具箱**。
我们的目标是构建一个经得起时间考验的库,因此我们非常重视 API 设计。
简而言之,Diffusers 旨在成为 PyTorch 的自然扩展。因此,我们的大多数设计选择都基于PyTorch 的设计原则。让我们来回顾一下最重要的原则。
可用性优先于性能
- 虽然 Diffusers 具有许多内置的性能增强功能(参见内存和速度),但模型始终以最高精度和最低优化加载。因此,默认情况下,如果用户未另行定义,则扩散管道始终在 CPU 上以 float32 精度实例化。这确保了跨不同平台和加速器的可用性,并且意味着不需要复杂的安装即可运行库。
- Diffusers 旨在成为一个**轻量级**的包,因此它只有很少的必需依赖项,但有许多可以提高性能的软依赖项(例如
accelerate
、safetensors
、onnx
等)。我们努力使库尽可能轻量级,以便可以轻松地将其作为其他包的依赖项添加。 - Diffusers 倾向于使用简单、易于理解的代码,而不是使用紧凑、难以理解的代码。这意味着通常不希望使用诸如 lambda 函数和高级 PyTorch 运算符之类的简写代码语法。
简单优先于便捷
正如 PyTorch 所述,**显式优于隐式**,**简单优于复杂**。这种设计理念反映在库的多个部分中。
- 我们遵循 PyTorch 的 API,使用诸如
DiffusionPipeline.to
之类的方法让用户处理设备管理。 - 与静默更正错误输入相比,更倾向于引发简洁的错误消息。Diffusers 的目标是教导用户,而不是使库尽可能易于使用。
- 复杂模型与调度器逻辑被公开,而不是在内部被魔术般地处理。调度器/采样器与扩散模型分离,彼此之间的依赖性最小。这迫使用户编写展开的去噪循环。但是,这种分离使得更容易调试,并让用户能够更好地控制适应去噪过程或切换扩散模型或调度器。
- 扩散管道的单独训练组件,例如文本编码器、U-Net 和变分自动编码器,每个都有自己的模型类。这迫使用户处理不同模型组件之间的交互,并且序列化格式将模型组件分离到不同的文件中。但是,这使得更容易调试和自定义。由于 Diffusers 能够分离扩散管道的单个组件,因此 DreamBooth 或文本反演训练非常简单。
可调整、对贡献者友好优先于抽象
对于库的大部分内容,Diffusers 采用了Transformers 库的一个重要设计原则,即更倾向于复制粘贴代码而不是草率地进行抽象。这种设计理念非常有主见,并且与流行的设计原则(如不要重复自己 (DRY))形成了鲜明对比。简而言之,就像 Transformers 对模型文件所做的那样,Diffusers 倾向于保持极低的抽象级别和非常独立的管道和调度器代码。函数、长代码块甚至类都可以在多个文件中复制,这乍一看可能像是糟糕的、草率的设计选择,使库难以维护。**但是**,这种设计已被证明对 Transformers 非常成功,并且对社区驱动的开源机器学习库很有意义,因为
- 机器学习是一个发展极其迅速的领域,范式、模型架构和算法都在快速变化,因此很难定义长期有效的代码抽象。
- 机器学习从业者希望能够快速调整现有代码进行构思和研究,因此他们更喜欢自包含代码而不是包含许多抽象的代码。
- 开源库依赖于社区贡献,因此必须构建一个易于贡献的库。代码越抽象,依赖项越多,越难阅读,越难贡献。贡献者由于害怕破坏重要的功能而干脆停止对非常抽象的库进行贡献。如果对库的贡献不会破坏其他基本代码,那么它不仅对潜在的新贡献者更具吸引力,而且也更容易并行审查和贡献多个部分。
在 Hugging Face,我们称这种设计为**单文件策略**,这意味着某个类的几乎所有代码都应该写在单个、自包含的文件中。要详细了解这种理念,可以查看这篇博文。
在 Diffusers 中,我们对管道和调度器遵循这种理念,但对扩散模型仅部分遵循。我们没有完全遵循这种设计的原因是,几乎所有扩散管道,例如DDPM、Stable Diffusion、unCLIP (DALL·E 2) 和Imagen 都依赖于相同的扩散模型,即UNet。
很好,现在您应该大体了解了🧨 Diffusers 为什么按照这种方式设计🤗。我们尝试在整个库中始终如一地应用这些设计原则。尽管如此,这种理念还是存在一些小的例外或一些不幸的设计选择。如果您对设计有任何反馈,我们非常乐意在GitHub 上直接获取您的反馈。
设计理念详解
现在,让我们深入了解一下设计理念的具体细节。Diffusers 本质上包含三个主要类:管道、模型 和调度器。让我们更详细地了解每个类的设计决策。
管道
管道旨在易于使用(因此不完全遵循 简单优先于易用),功能不完整,应被宽泛地视为如何使用 模型 和 调度器 进行推理的示例。
遵循以下设计原则:
- 管道遵循单文件策略。所有管道都可以在 `src/diffusers/pipelines` 下的各个目录中找到。一个管道文件夹对应一个扩散论文/项目/版本。多个管道文件可以收集在一个管道文件夹中,就像
src/diffusers/pipelines/stable-diffusion
中所做的那样。如果管道具有类似的功能,则可以使用 # 复制自机制。 - 所有管道都继承自 DiffusionPipeline。
- 每个管道都包含不同的模型和调度器组件,这些组件在
model_index.json
文件 中有文档记录,可以通过与管道属性相同的名称访问,并且可以使用DiffusionPipeline.components
函数在管道之间共享。 - 每个管道都应该可以通过
DiffusionPipeline.from_pretrained
函数加载。 - 管道**仅**用于推理。
- 管道应该非常易读、自解释且易于调整。
- 管道应该被设计为能够相互构建并易于集成到更高级别的 API 中。
- 管道**并非**旨在成为功能完整的用户界面。对于功能完整的用户界面,应该查看 InvokeAI、Diffuzers 和 lama-cleaner。
- 每个管道都应该只有一种通过 `__call__` 方法运行它的方式。`__call__` 参数的命名应该在所有管道之间共享。
- 管道应该以它们旨在解决的任务命名。
- 在几乎所有情况下,新的扩散管道都应在新的管道文件夹/文件中实现。
模型
模型被设计为可配置的工具箱,是 PyTorch 的 Module 类 的自然扩展。它们只部分遵循**单文件策略**。
遵循以下设计原则:
- 模型对应于**一种模型架构类型**。例如,UNet2DConditionModel 类用于所有期望 2D 图像输入并以某些上下文为条件的 UNet 变体。
- 所有模型都可以在
src/diffusers/models
中找到,并且每个模型架构都应在其文件中定义,例如unets/unet_2d_condition.py
、transformers/transformer_2d.py
等。 - 模型**不**遵循单文件策略,并且应该使用更小的模型构建块,例如
attention.py
、resnet.py
、embeddings.py
等。**注意**:这与 Transformers 的建模文件形成鲜明对比,并表明模型实际上并没有真正遵循单文件策略。 - 模型旨在暴露复杂性,就像 PyTorch 的 `Module` 类一样,并提供清晰的错误消息。
- 所有模型都继承自 `ModelMixin` 和 `ConfigMixin`。
- 当不需要进行重大代码更改、保持向后兼容性并带来显着的内存或计算增益时,可以对模型进行性能优化。
- 模型默认应具有最高的精度和最低的性能设置。
- 为了集成其通用架构可以归类为 Diffusers 中已存在架构的新模型检查点,应调整现有的模型架构以使其与新检查点一起使用。仅当模型架构从根本上不同时,才应创建新文件。
- 模型应被设计为易于扩展以适应未来的更改。这可以通过限制公共函数参数、配置参数和“预见”未来的更改来实现,例如,通常最好添加可以轻松扩展到未来新类型的 `string` “...type” 参数,而不是布尔 `is_..._type` 参数。为了使新的模型检查点工作,应仅对现有架构进行最少的更改。
- 模型设计是在保持代码可读性和简洁性以及支持许多模型检查点之间进行的艰难权衡。对于大多数建模代码部分,应为新的模型检查点调整类,但也有一些例外情况,其中添加新类以确保代码从长远来看保持简洁和易读是优先考虑的,例如 UNet 块 和 注意力处理器。
调度器
调度器负责指导推理的去噪过程,以及为训练定义噪声调度。它们被设计为具有可加载配置文件的独立类,并严格遵循**单文件策略**。
遵循以下设计原则:
- 所有调度器都可以在
src/diffusers/schedulers
中找到。 - 调度器**不允许**从大型实用程序文件中导入,并且应保持非常独立。
- 一个调度器 Python 文件对应一个调度器算法(如论文中所定义)。
- 如果调度器具有类似的功能,我们可以使用 `# 复制自` 机制。
- 所有调度器都继承自 `SchedulerMixin` 和 `ConfigMixin`。
- 调度器可以通过
ConfigMixin.from_config
方法轻松互换,如 此处 详细解释。 - 每个调度器都必须具有 `set_num_inference_steps` 和 `step` 函数。`set_num_inference_steps(...)` 必须在每个去噪过程之前调用,即在调用 `step(...)` 之前。
- 每个调度器都通过 `timesteps` 属性公开要“循环遍历”的时间步长,这是一个模型将被调用的时间步长数组。
- `step(...)` 函数接收预测的模型输出和“当前”样本 (x_t),并返回“先前”的、略微去噪的样本 (x_t-1)。
- 鉴于扩散调度器的复杂性,`step` 函数不会暴露所有复杂性,并且可能有点像“黑盒”。
- 在几乎所有情况下,新的调度器都应在新的调度文件中实现。