diff --git a/.gitignore b/.gitignore index 851b454..fa9aa57 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.ckpt *.pt *.bin +*.DS_Store # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/diffsynth/diffusion/runner.py b/diffsynth/diffusion/runner.py index 05151cf..63cd856 100644 --- a/diffsynth/diffusion/runner.py +++ b/diffsynth/diffusion/runner.py @@ -12,7 +12,7 @@ def launch_training_task( model_logger: ModelLogger, learning_rate: float = 1e-5, weight_decay: float = 1e-2, - num_workers: int = 8, + num_workers: int = 1, save_steps: int = None, num_epochs: int = 1, args = None, diff --git a/diffsynth/diffusion/training_module.py b/diffsynth/diffusion/training_module.py index a7c5996..aedb3b8 100644 --- a/diffsynth/diffusion/training_module.py +++ b/diffsynth/diffusion/training_module.py @@ -115,8 +115,8 @@ class DiffusionTrainingModule(torch.nn.Module): def switch_pipe_to_training_mode( self, pipe, - trainable_models, - lora_base_model, lora_target_modules, lora_rank, lora_checkpoint=None, + trainable_models=None, + lora_base_model=None, lora_target_modules="", lora_rank=32, lora_checkpoint=None, preset_lora_path=None, preset_lora_model=None, ): # Scheduler diff --git a/docs/Developer_Guide/Building_a_Pipeline.md b/docs/Developer_Guide/Building_a_Pipeline.md index b1a34d9..f58b3b1 100644 --- a/docs/Developer_Guide/Building_a_Pipeline.md +++ b/docs/Developer_Guide/Building_a_Pipeline.md @@ -227,6 +227,12 @@ class QwenImageUnit_EntityControl(PipelineUnit): * `input_params_nega`: Negative 侧输入参数 * `onload_model_names`: 需调用的模型组件名 +在设计 `unit` 时请尽量按照以下原则进行: + +* 缺省兜底:可选功能的 `unit` 输入参数默认为 `None`,而不是 `False` 或其他数值,请对此默认值进行兜底处理。 +* 参数触发:部分 Adapter 模型可能是未被加载的,例如 ControlNet,对应的 `unit` 应当以参数输入是否为 `None` 来控制触发,而不是以模型是否被加载来控制触发。例如当用户输入了 `controlnet_image` 但没有加载 ControlNet 模型时,代码应当给出报错,而不是忽略这些输入参数继续执行。 +* 显存高效:在 `unit` 中调用模型时,请使用 `pipe.load_models_to_device(self.onload_model_names)` 激活对应的模型,请不要调用 `onload_model_names` 之外的其他模型,`unit` 计算完成后,请不要使用 `pipe.load_models_to_device([])` 手动释放显存。 + > Q: 部分参数并未在推理过程中调用,例如 `output_params`,是否仍有必要配置? > > A: 这些参数不会影响推理过程,但会影响一些实验性功能,因此我们建议将其配置好。例如“拆分训练”,我们可以将训练中的前处理离线完成,但部分需要梯度回传的模型计算无法拆分,这些参数用于构建计算图从而推断哪些计算是可以拆分的。 diff --git a/docs/QA.md b/docs/QA.md index 70f5ee4..752e3ea 100644 --- a/docs/QA.md +++ b/docs/QA.md @@ -5,3 +5,7 @@ ## 为什么不删除某些模型中的冗余参数? ## 为什么 FP8 量化没有任何加速效果? + +## 为什么训练框架不支持原生 FP8 精度训练? + +即使硬件条件允许,我们目前也没有任何支持原生 FP8 精度训练的规划。目前原生 FP8 精度训练的主要挑战是梯度爆炸导致的精度溢出,为了保证训练的稳定性,需针对性地重新设计模型结构,然而目前还没有任何模型开发者愿意这么做。此外,使用原生 FP8 精度训练的模型,在推理时若没有 Hopper 架构 GPU,则只能以 BF16 精度进行计算,理论上其生成效果反而不如 FP8。因此,原生 FP8 精度训练技术是极不成熟的,我们静观开源社区的技术发展。 diff --git a/docs/README.md b/docs/README.md index 64ceda6..d689601 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,14 +21,16 @@ * [FLUX](./Model_Details/FLUX.md)【TODO】 * [Wan](./Model_Details/Wan.md)【TODO】 -## Section 3: 特殊训练 +## Section 3: 训练框架 -本节介绍 `DiffSynth-Studio` 所支持的特殊训练模式 +本节介绍 `DiffSynth-Studio` 中训练框架的设计思路,帮助开发者理解 Diffusion 模型训练算法的原理。 -* FP8 训练 -* 端到端蒸馏训练 +* [Diffusion 模型基本原理](./Training/Understanding_Diffusion_models.md) +* [标准监督训练](./Training/Supervised_Fine_Tuning.md) +* [在训练中启用 FP8 精度](./Training/FP8_Precision.md) +* [端到端的蒸馏加速训练](./Training/Direct_Distill.md) +* 两阶段拆分训练 * 差分 LoRA 训练 -* 拆分训练 ## Section 4: 模型接入 diff --git a/docs/Training/Direct_Distill.md b/docs/Training/Direct_Distill.md new file mode 100644 index 0000000..a016beb --- /dev/null +++ b/docs/Training/Direct_Distill.md @@ -0,0 +1,97 @@ +# 端到端的蒸馏加速训练 + +## 蒸馏加速训练 + +Diffusion 模型的推理过程通常需要多步迭代,在提升生成效果的同时也让生成过程变得缓慢。通过蒸馏加速训练,可以减少生成清晰内容所需的步数。蒸馏加速训练技术的本质训练目标是让少量步数的生成效果与大量步数的生成效果对齐。 + +蒸馏加速训练的方法是多样的,例如 + +* 对抗式训练 ADD(Adversarial Diffusion Distillation) + * 论文:https://arxiv.org/abs/2311.17042 + * 模型:[stabilityai/sdxl-turbo](https://modelscope.cn/models/stabilityai/sdxl-turbo) +* 渐进式训练 Hyper-SD + * 论文:https://arxiv.org/abs/2404.13686 + * 模型:[ByteDance/Hyper-SD](https://www.modelscope.cn/models/ByteDance/Hyper-SD) + +## 直接蒸馏 + +在训练框架层面,支持这类蒸馏加速训练方案是极其困难的。在训练框架的设计中,我们需要保证训练方案满足以下条件: + +* 通用性:训练方案适用于大多数框架内支持的 Diffusion 模型,而非只能对某个特定模型生效,这是代码框架建设的基本要求。 +* 稳定性:训练方案需保证训练效果稳定,不需要人工进行精细的参数调整,ADD 中的对抗式训练则无法保证稳定性。 +* 简洁性:训练方案不会引入额外的复杂模块,根据奥卡姆剃刀([Occam's Razor](https://en.wikipedia.org/wiki/Occam%27s_razor))原理,复杂解决方案可能引入潜在风险,Hyper-SD 中的 Human Feedback Learning 让训练过程变得过于复杂。 + +因此,在 `DiffSynth-Studio` 的训练框架中,我们设计了一个端到端的蒸馏加速训练方案,我们称为直接蒸馏(Direct Distill),其训练过程的伪代码如下: + +``` +seed = xxx +with torch.no_grad(): + image_1 = pipe(prompt, steps=50, seed=seed, cfg=4) +image_2 = pipe(prompt, steps=4, seed=seed, cfg=1) +loss = torch.nn.functional.mse_loss(image_1, image_2) +``` + +是的,非常端到端的训练方案,稍加训练就可以有立竿见影的效果。 + +## 直接蒸馏训练的模型 + +我们用这个方案基于 Qwen-Image 训练了两个模型: + +* [DiffSynth-Studio/Qwen-Image-Distill-Full](https://modelscope.cn/models/DiffSynth-Studio/Qwen-Image-Distill-Full): 全量蒸馏训练 +* [DiffSynth-Studio/Qwen-Image-Distill-LoRA](https://modelscope.cn/models/DiffSynth-Studio/Qwen-Image-Distill-LoRA): LoRA 蒸馏训练 + +点击模型链接即可前往模型页面查看模型效果。 + +## 在训练框架中使用蒸馏加速训练 + +首先,需要生成训练数据,请参考[模型推理](/docs/Pipeline_Usage/Model_Inference.md)部分编写推理代码,以足够多的推理步数生成训练数据。 + +以 Qwen-Image 为例,以下代码可以生成一张图片: + +```python +from diffsynth.pipelines.qwen_image import QwenImagePipeline, ModelConfig +import torch + +pipe = QwenImagePipeline.from_pretrained( + torch_dtype=torch.bfloat16, + device="cuda", + model_configs=[ + ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="transformer/diffusion_pytorch_model*.safetensors"), + ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="text_encoder/model*.safetensors"), + ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="vae/diffusion_pytorch_model.safetensors"), + ], + tokenizer_config=ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="tokenizer/"), +) +prompt = "精致肖像,水下少女,蓝裙飘逸,发丝轻扬,光影透澈,气泡环绕,面容恬静,细节精致,梦幻唯美。" +image = pipe(prompt, seed=0, num_inference_steps=40) +image.save("image.jpg") +``` + +然后,我们把必要的信息编写成[元数据文件](/docs/API_Reference/core/data.md#元数据): + +```csv +image,prompt,seed,rand_device,num_inference_steps,cfg_scale +distill_qwen/image.jpg,"精致肖像,水下少女,蓝裙飘逸,发丝轻扬,光影透澈,气泡环绕,面容恬静,细节精致,梦幻唯美。",0,cpu,4,1 +``` + +这个样例数据集可以直接下载: + +```shell +modelscope download --dataset DiffSynth-Studio/example_image_dataset --local_dir ./data/example_image_dataset +``` + +然后开始 LoRA 蒸馏加速训练: + +```shell +bash examples/qwen_image/model_training/lora/Qwen-Image-Distill-LoRA.sh +``` + +请注意,在[训练脚本参数](/docs/Pipeline_Usage/Model_Training.md#脚本参数)中,数据集的图像分辨率设置要避免触发缩放处理。当设定 `--height` 和 `--width` 以启用固定分辨率时,所有训练数据必须是以完全一致的宽高生成的;当设定 `--max_pixels` 以启用动态分辨率时,`--max_pixels` 的数值必须大于或等于任一训练图像的像素面积。 + +## 训练框架设计思路 + +直接蒸馏与[标准监督训练](./Supervised_Fine_Tuning.md)相比,仅训练的损失函数不同,直接蒸馏的损失函数是 `diffsynth.diffusion.loss` 中的 `DirectDistillLoss`。 + +## 未来工作 + +直接蒸馏是通用性很强的加速方案,但未必是效果最好的方案,所以我们暂未把这一技术以论文的形式发布。我们希望把这个问题交给学术界和开源社区共同解决,期待开发者能够给出更完善的通用训练方案。 diff --git a/docs/Training/FP8_Precision.md b/docs/Training/FP8_Precision.md new file mode 100644 index 0000000..af4be09 --- /dev/null +++ b/docs/Training/FP8_Precision.md @@ -0,0 +1,20 @@ +# 在训练中启用 FP8 精度 + +尽管 `DiffSynth-Studio` 在模型推理中支持[显存管理](/docs/Pipeline_Usage/VRAM_management.md),但其中的大部分减少显存占用的技术不适合用于训练中,Offload 会导致极为缓慢的训练过程。 + +FP8 精度是唯一可在训练过程中启用的显存管理策略,但本框架目前不支持原生 FP8 精度训练,原因详见 [Q&A: 为什么训练框架不支持原生 FP8 精度训练?](/docs/QA.md#为什么训练框架不支持原生-fp8-精度训练),仅支持将参数不被梯度更新的模型(不需要梯度回传,或梯度仅更新其 LoRA)以 FP8 精度进行存储。 + +## 启用 FP8 + +在我们提供的训练脚本中,通过参数 `--fp8_models` 即可快速设置以 FP8 精度存储的模型。以 Qwen-Image 的 LoRA 训练为例,我们提供了启用 FP8 训练的脚本,位于 [`/examples/qwen_image/model_training/special/fp8_training/Qwen-Image-LoRA.sh`](/examples/qwen_image/model_training/special/fp8_training/Qwen-Image-LoRA.sh)。训练完成后,可通过脚本 [`/examples/qwen_image/model_training/special/fp8_training/validate.py`](/examples/qwen_image/model_training/special/fp8_training/validate.py) 验证训练效果。 + +请注意,这种 FP8 显存管理策略不支持梯度更新,当某个模型被设置为可训练时,不能为这个模型开启 FP8 精度,支持开启 FP8 的模型包括两类: + +* 参数不可训练,例如 VAE 模型 +* 梯度不更新其参数,例如 LoRA 训练中的 DiT 模型 + +经实验验证,开启 FP8 后的 LoRA 训练效果没有明显的图像质量下降,但理论上误差是确实存在的,如果在使用本功能时遇到训练效果不如 BF16 精度训练的问题,请通过 GitHub issue 给我们提供反馈。 + +## 训练框架设计思路 + +训练框架完全沿用推理的显存管理,在训练中仅通过 `DiffusionTrainingModule` 中的 `parse_model_configs` 解析显存管理配置。 diff --git a/docs/Training/Split_Training.md b/docs/Training/Split_Training.md new file mode 100644 index 0000000..0422063 --- /dev/null +++ b/docs/Training/Split_Training.md @@ -0,0 +1,2 @@ +# 两阶段拆分训练 + diff --git a/docs/Training/Supervised_Fine_Tuning.md b/docs/Training/Supervised_Fine_Tuning.md new file mode 100644 index 0000000..3ba9018 --- /dev/null +++ b/docs/Training/Supervised_Fine_Tuning.md @@ -0,0 +1,129 @@ +# 标准监督训练 + +在理解 [Diffusion 模型基本原理](./Understanding_Diffusion_models.md)之后,本文档介绍框架如何实现 Diffusion 模型的训练。 + +回顾前文中的模型训练伪代码,当我们实际编写代码时,情况会变得极为复杂。部分模型需要输入额外的引导条件并进行预处理,例如 ControlNet;部分模型需要与去噪模型进行交叉式的计算,例如 VACE;部分模型因显存需求过大,需要开启 Gradient Checkpointing,例如 Qwen-Image 的 DiT。 + +为了实现严格的推理和训练一致性,我们对 `Pipeline` 等组件进行了抽象封装,在训练过程中大量复用推理代码。请参考[接入 Pipeline](/docs/Developer_Guide/Building_a_Pipeline.md) 了解 `Pipeline` 组件的设计。接下来我们介绍训练框架如何利用 `Pipeline` 组件构建训练算法。 + +## 框架设计思路 + +训练模块在 `Pipeline` 上层进行封装,继承 `diffsynth.diffusion.training_module` 中的 `DiffusionTrainingModule`,我们需为训练模块提供必要的 `__init__` 和 `forward` 方法。我们以 Qwen-Image 的 LoRA 训练为例,在 `examples/qwen_image/model_training/special/simple/train.py` 中提供了仅包含基础训练功能的简易脚本,帮助开发者理解训练模块的设计思路。 + +```python +class QwenImageTrainingModule(DiffusionTrainingModule): + def __init__(self, device): + # Initialize models here. + pass + + def forward(self, data): + # Compute loss here. + return loss +``` + +### `__init__` + +在 `__init__` 中需进行模型的初始化,先加载模型,然后将其切换到训练模式。 + +```python + def __init__(self, device): + super().__init__() + # Load the pipeline + self.pipe = QwenImagePipeline.from_pretrained( + torch_dtype=torch.bfloat16, + device=device, + model_configs=[ + ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="transformer/diffusion_pytorch_model*.safetensors"), + ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="text_encoder/model*.safetensors"), + ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="vae/diffusion_pytorch_model.safetensors"), + ], + tokenizer_config=ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="tokenizer/"), + ) + # Switch to training mode + self.switch_pipe_to_training_mode( + self.pipe, + lora_base_model="dit", + lora_target_modules="to_q,to_k,to_v,add_q_proj,add_k_proj,add_v_proj", + lora_rank=32, + ) +``` + +加载模型的逻辑与推理时基本一致,支持从远程和本地路径加载模型,详见[模型推理](/docs/Pipeline_Usage/Model_Inference.md),但请注意不要启用[显存管理](/docs/Pipeline_Usage/VRAM_management.md)。 + +`switch_pipe_to_training_mode` 可以将模型切换到训练模式,详见 `switch_pipe_to_training_mode`。 + +### `forward` + +在 `forward` 中需计算损失函数值,先进行前处理,然后经过 `Pipeline` 的 [`model_fn`](/docs/Developer_Guide/Building_a_Pipeline.md#model_fn) 计算损失函数。 + +```python + def forward(self, data): + # Preprocess + inputs_posi = {"prompt": data["prompt"]} + inputs_nega = {"negative_prompt": ""} + inputs_shared = { + # Assume you are using this pipeline for inference, + # please fill in the input parameters. + "input_image": data["image"], + "height": data["image"].size[1], + "width": data["image"].size[0], + # Please do not modify the following parameters + # unless you clearly know what this will cause. + "cfg_scale": 1, + "rand_device": self.pipe.device, + "use_gradient_checkpointing": True, + "use_gradient_checkpointing_offload": False, + } + for unit in self.pipe.units: + inputs_shared, inputs_posi, inputs_nega = self.pipe.unit_runner(unit, self.pipe, inputs_shared, inputs_posi, inputs_nega) + # Loss + loss = FlowMatchSFTLoss(self.pipe, **inputs_shared, **inputs_posi) + return loss +``` + +前处理过程与推理阶段一致,开发者只需假定在使用 `Pipeline` 进行推理,将输入参数填入即可。 + +损失函数的计算沿用 `diffsynth.diffusion.loss` 中的 `FlowMatchSFTLoss`。 + +### 开始训练 + +训练框架还需其他模块,包括: + +* accelerator: `accelerate` 提供的训练启动器,详见 [`accelerate`](https://huggingface.co/docs/accelerate/index) +* dataset: 通用数据集,详见 [`diffsynth.core.data`](/docs/API_Reference/core/data.md) +* model_logger: 模型记录器,详见 `diffsynth.diffusion.logger` + +```python +if __name__ == "__main__": + accelerator = accelerate.Accelerator( + kwargs_handlers=[accelerate.DistributedDataParallelKwargs(find_unused_parameters=True)], + ) + dataset = UnifiedDataset( + base_path="data/example_image_dataset", + metadata_path="data/example_image_dataset/metadata.csv", + repeat=50, + data_file_keys="image", + main_data_operator=UnifiedDataset.default_image_operator( + base_path="data/example_image_dataset", + height=512, + width=512, + height_division_factor=16, + width_division_factor=16, + ) + ) + model = QwenImageTrainingModule(accelerator.device) + model_logger = ModelLogger( + output_path="models/toy_model", + remove_prefix_in_ckpt="pipe.dit.", + ) + launch_training_task( + accelerator, dataset, model, model_logger, + learning_rate=1e-5, num_epochs=1, + ) +``` + +将以上所有代码组装,得到 `examples/qwen_image/model_training/special/simple/train.py`。使用以下命令即可启动训练: + +``` +accelerate launch examples/qwen_image/model_training/special/simple/train.py +``` diff --git a/docs/Training/Understanding_Diffusion_models.md b/docs/Training/Understanding_Diffusion_models.md new file mode 100644 index 0000000..006403b --- /dev/null +++ b/docs/Training/Understanding_Diffusion_models.md @@ -0,0 +1,143 @@ +# Diffusion 模型基本原理 + +本文介绍 Diffusion 模型的基本原理,帮助你理解训练框架是如何构建的。为了让读者更轻松地理解这些复杂的数学理论,我们重构了 Diffusion 模型的理论框架,抛弃了复杂的随机微分方程,用一种更简洁易懂的形式进行介绍。 + +## 引言 + +Diffusion 模型通过多步迭代式地去噪(denoise)生成清晰的图像或视频内容,我们从一个数据样本 $x_0$ 的生成过程开始讲起。直观地,在完整的一轮 denoise 过程中,我们从随机高斯噪声 $x_T$ 开始,通过迭代依次得到 $x_{T-1}$、$x_{T-2}$、$x_{T-3}$、$\cdots$,在每一步中逐渐减少噪声含量,最终得到不含噪声的数据样本 $x_0$。 + +(图) + +这个过程是很直观的,但如果要理解其中的细节,我们就需要回答这几个问题: + +* 每一步的噪声含量是如何定义的? +* 迭代去噪的计算是如何进行的? +* 如何训练这样的 Diffusion 模型? +* 现代 Diffusion 模型的架构是什么样的? +* 本项目如何封装和实现模型训练? + +## 每一步的噪声含量是如何定义的? + +在 Diffusion 模型的理论体系中,噪声的含量是由一系列参数 $\sigma_T$、$\sigma_{T-1}$、$\sigma_{T-2}$、$\cdots$、$\sigma_0$ 决定的。其中 + +* $\sigma_T=1$,对应的 $x_T$ 为纯粹的高斯噪声 +* $\sigma_T>\sigma_{T-1}>\sigma_{T-2}>\cdots>x_0$,在迭代过程中噪声含量逐渐减小 +* $\sigma_0=0$,对应的 $x_0$ 为不含任何噪声的数据样本 + +至于中间 $\sigma_{T-1}$、$\sigma_{T-2}$、$\cdots$、$\sigma_1$ 的数值,则不是固定的,满足递减的条件即可。 + +那么在中间的某一步,我们可以直接合成含噪声的数据样本 $x_t=(1-\sigma_t)x_0+\sigma_t x_T$。 + +(图) + +## 迭代去噪的计算是如何进行的? + +在理解迭代去噪的计算前,我们要先搞清楚,去噪模型的输入和输出是什么。我们把模型抽象成一个符号 $\hat \epsilon$,它的输入通常包含三部分 + +* 时间步 $t$,模型需要理解当前处于去噪过程的哪个阶段 +* 含噪声的数据样本 $x_t$,模型需要理解要对什么数据进行去噪 +* 引导条件 $c$,模型需要理解要通过去噪生成什么样的数据样本 + +其中,引导条件 $c$ 是新引入的参数,它是由用户输入的,可以是用于描述图像内容的文本,也可以是用于勾勒图像结构的线稿图。 + +(图) + +而模型的输出 $\hat \epsilon(x_t,c,t)$,则近似地等于 $x_T-x_0$,也就是整个扩散过程(去噪过程的反向过程)的方向。 + +接下来我们分析一步迭代中发生的计算,在时间步 $t$,模型通过计算得到近似的 $x_T-x_0$ 后,我们计算下一步的 $x_{t-1}$: +$$ +\begin{aligned} +x_{t-1}&=x_t + (\sigma_{t-1} - \sigma_t) \cdot \hat \epsilon(x_t,c,t)\\ +&\approx x_t + (\sigma_{t-1} - \sigma_t) \cdot (x_T-x_0)\\ +&=(1-\sigma_t)x_0+\sigma_t x_T + (\sigma_{t-1} - \sigma_t) \cdot (x_T-x_0)\\ +&=(1-\sigma_{t-1})x_0+\sigma_{t-1}x_T +\end{aligned} +$$ +完美!与时间步 $t-1$ 时的噪声含量定义完美契合。 + +> (这部分可能有点难懂,请不必担心,首次阅读本文时建议跳过这部分,不影响后文的阅读。) +> +> 完成了这段有点复杂的公式推导后,我们思考一个问题,为什么模型的输出要近似地等于 $x_T-x_0$ 呢?可以设定成其他值吗? +> +> 实际上,Diffusion 模型依赖两个定义形成完备的理论。在以上的公式中,我们可以提炼出这两个定义,并导出迭代公式: +> +> * 数据定义:$x_t=(1-\sigma_t)x_0+\sigma_t x_T$ +> * 模型定义:$\hat \epsilon(x_t,c,t)=x_T-x_0$ +> * 导出迭代公式:$x_{t-1}=x_t + (\sigma_{t-1} - \sigma_t) \cdot \hat \epsilon(x_t,c,t)$ +> +> 这三个数学公式是完备的,例如在刚才的推导中,我们把数据定义和模型定义代入迭代公式,可以得到与数据定义吻合的 $x_{t-1}$。 +> +> 这是基于 Flow Matching 理论构建的两个定义,但 Diffusion 模型也可用其他的两个定义来实现,例如早期基于 DDPM(Denoising Diffusion Probabilistic Models)的模型,其两个定义及导出的迭代公式为: +> +> * 数据定义:$x_t=\sqrt{\alpha_t}x_0+\sqrt{1-\alpha_t}x_T$ +> * 模型定义:$\hat \epsilon(x_t,c,t)=x_T$ +> * 导出迭代公式:$x_{t-1}=\sqrt{\alpha_{t-1}}\left(\frac{x_t-\sqrt{1-\alpha_t}\hat \epsilon(x_t,c,t)}{\sqrt{\sigma_t}}\right)+\sqrt{1-\alpha_{t-1}}\hat \epsilon(x_t,c,t)$ +> +> 更一般地,我们用矩阵描述迭代公式的导出过程,对于任意数据定义和模型定义,有: +> +> * 数据定义:$x_t=C_T(x_0,x_T)^T$ +> * 模型定义:$\hat \epsilon(x_t,c,t)=C_T^{[\epsilon]}(x_0,x_T)^T$ +> * 导出迭代公式:$x_{t-1}=C_{t-1}(C_t,C_t^{[\epsilon]})^{-T}(x_t,\hat \epsilon(x_t,c,t))^T$ +> +> 其中,$C_t$、$C_t^{[\epsilon]}$ 是 $1\times 2$ 的系数矩阵,不难发现,在构造两个定义时,需保证矩阵 $(C_t,C_t^{[\epsilon]})^T$ 是可逆的。 +> +> 尽管 Flow Matching 与 DDPM 已被大量预训练模型广泛验证过,但这并不代表这是最优的方案,我们鼓励开发者设计新的 Diffusion 模型理论实现更好的训练效果。 + +## 如何训练这样的 Diffusion 模型? + +搞清楚迭代去噪的过程之后,接下来我们考虑如何训练这样的 Diffusion 模型。 + +训练过程不同于生成过程,如果我们在训练过程中保留多步迭代,那么梯度需经过多步回传,带来的时间和空间复杂度是灾难性的。为了提高计算效率,我们在训练中随机选择某一时间步 $t$ 进行训练。 + +(图) + +以下是训练过程的伪代码 + +> 从数据集获取数据样本 $x_0$ 和引导条件 $c$ +> +> 随机采样时间步 $t\in(0,T]$ +> +> 随机采样高斯噪声 $x_T\in \mathcal N(O,I)$ +> +> $x_t=(1-\sigma_t)x_0+\sigma_t x_T$ +> +> $\hat \epsilon(x_t,c,t)$ +> +> 损失函数 $\mathcal L=||\hat \epsilon(x_t,c,t)-(x_T-x_0)||_2^2$ +> +> 梯度回传并更新模型参数 + +## 现代 Diffusion 模型的架构是什么样的? + +从理论到实践,还需要填充更多细节。现代 Diffusion 模型架构已经发展成熟,主流的架构沿用了 Latent Diffusion 所提出的“三段式”架构,包括数据编解码器、引导条件编码器、去噪模型三部分。 + +(图) + +### 数据编解码器 + +在前文中,我们一直将 $x_0$ 称为“数据样本”,而不是图像或视频,这是因为现代 Diffusion 模型通常不会直接在图像或视频上进行处理,而是用编码器(Encoder)-解码器(Decoder)架构的模型,通常是 VAE(Variational Auto-Encoders)模型,将图像或视频编码为 Embedding 张量,得到 $x_0$。 + +数据经过编码器编码后,再经过解码器解码,重建后的内容与原来近似地一致,会有少量误差。那么,为什么要在编码后的 Embedding 张量上处理,而不是在图像或视频上直接处理呢?主要原因有亮点: + +* 编码的同时对数据进行了压缩,编码后处理的计算量更小。 +* 编码后的数据分布与高斯分布更相似,更容易用去噪模型对数据进行建模。 + +在生成过程中,编码器部分不参与计算,迭代完成后,用解码器部分解码 $x_0$ 即可得到清晰的图像或视频。在训练过程中,解码器部分不参与计算,仅编码器用于计算 $x_0$。 + +### 引导条件编码器 + +用户输入的引导条件 $c$ 可能是复杂多样的,需要由专门的编码器模型将其处理成 Embedding 张量。按照引导条件的类型,我们把引导条件编码器分为以下几类: + +* 文本类型,例如 CLIP、Qwen-VL +* 图像类型,例如 ControlNet、IP-Adapter +* 视频类型,例如 VAE + +> 前文中的模型 $\hat \epsilon$ 指代此处的所有引导条件编码器和去噪模型这一整体,我们把引导条件编码器单独拆分列出,因为这类模型在 Diffusion 训练中通常是冻结的,且输出值与时间步 $t$ 无关,因此引导条件编码器的计算可以离线进行。 + +### 去噪模型 + +去噪模型是 Diffusion 模型真正的本体,其模型结构多种多样,例如 UNet、DiT,模型开发者可在此结构上自由发挥。 + +## 本项目如何封装和实现模型训练? + +请阅读下一文档:[标准监督训练](./Supervised_Fine_Tuning.md) diff --git a/examples/qwen_image/model_training/special/simple/train.py b/examples/qwen_image/model_training/special/simple/train.py new file mode 100644 index 0000000..b4e6a11 --- /dev/null +++ b/examples/qwen_image/model_training/special/simple/train.py @@ -0,0 +1,76 @@ +import torch, accelerate +from diffsynth.core import UnifiedDataset +from diffsynth.pipelines.qwen_image import QwenImagePipeline, ModelConfig +from diffsynth.diffusion import * + +class QwenImageTrainingModule(DiffusionTrainingModule): + def __init__(self, device): + super().__init__() + # Load the pipeline + self.pipe = QwenImagePipeline.from_pretrained( + torch_dtype=torch.bfloat16, + device=device, + model_configs=[ + ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="transformer/diffusion_pytorch_model*.safetensors"), + ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="text_encoder/model*.safetensors"), + ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="vae/diffusion_pytorch_model.safetensors"), + ], + tokenizer_config=ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="tokenizer/"), + ) + # Switch to training mode + self.switch_pipe_to_training_mode( + self.pipe, + lora_base_model="dit", + lora_target_modules="to_q,to_k,to_v,add_q_proj,add_k_proj,add_v_proj", + lora_rank=32, + ) + + def forward(self, data): + # Preprocess + inputs_posi = {"prompt": data["prompt"]} + inputs_nega = {"negative_prompt": ""} + inputs_shared = { + # Assume you are using this pipeline for inference, + # please fill in the input parameters. + "input_image": data["image"], + "height": data["image"].size[1], + "width": data["image"].size[0], + # Please do not modify the following parameters + # unless you clearly know what this will cause. + "cfg_scale": 1, + "rand_device": self.pipe.device, + "use_gradient_checkpointing": True, + "use_gradient_checkpointing_offload": False, + } + for unit in self.pipe.units: + inputs_shared, inputs_posi, inputs_nega = self.pipe.unit_runner(unit, self.pipe, inputs_shared, inputs_posi, inputs_nega) + # Loss + loss = FlowMatchSFTLoss(self.pipe, **inputs_shared, **inputs_posi) + return loss + +if __name__ == "__main__": + accelerator = accelerate.Accelerator( + kwargs_handlers=[accelerate.DistributedDataParallelKwargs(find_unused_parameters=True)], + ) + dataset = UnifiedDataset( + base_path="data/example_image_dataset", + metadata_path="data/example_image_dataset/metadata.csv", + repeat=50, + data_file_keys="image", + main_data_operator=UnifiedDataset.default_image_operator( + base_path="data/example_image_dataset", + height=512, + width=512, + height_division_factor=16, + width_division_factor=16, + ) + ) + model = QwenImageTrainingModule(accelerator.device) + model_logger = ModelLogger( + output_path="models/toy_model", + remove_prefix_in_ckpt="pipe.dit.", + ) + launch_training_task( + accelerator, dataset, model, model_logger, + learning_rate=1e-5, num_epochs=1, + ) diff --git a/examples/qwen_image/model_training/train.py b/examples/qwen_image/model_training/train.py index 6f08c99..8ca3fb7 100644 --- a/examples/qwen_image/model_training/train.py +++ b/examples/qwen_image/model_training/train.py @@ -72,6 +72,7 @@ class QwenImageTrainingModule(DiffusionTrainingModule): def forward(self, data, inputs=None): if self.fp8_models is not None: + # TODO: remove it self.pipe.flush_vram_management_device(self.pipe.device) if inputs is None: inputs = self.get_pipeline_inputs(data) inputs = self.transfer_data_to_device(inputs, self.pipe.device, self.pipe.torch_dtype)