import argparse
import copy
import math
import random
from typing import Any, Optional, Union
import torch
from accelerate import Accelerator
from library.device_utils import clean_memory_on_device, init_ipex
init_ipex()
from library import kohya_trainer
from library import (
step1x_edit_train_utils,
step1x_utils,
strategy_step1x,
train_util,
)
from library.utils import setup_logging
setup_logging()
import logging
logger = logging.getLogger(__name__)
class Step1XEditNetworkTrainer(kohya_trainer.NetworkTrainer):
def __init__(self):
"""
初始化 Step1XEditNetworkTrainer 类。
"""
super().__init__()
self.sample_prompts_te_outputs = None
self.is_schnell: Optional[bool] = None
self.is_swapping_blocks: bool = False
def assert_extra_args(
self,
args,
train_dataset_group: Union[train_util.DatasetGroup, train_util.MinimalDataset],
val_dataset_group: Optional[train_util.DatasetGroup],
):
"""
断言额外的参数是否有效。
Args:
args: 命令行参数。
train_dataset_group: 训练数据集组。
val_dataset_group: 验证数据集组。
"""
super().assert_extra_args(args, train_dataset_group, val_dataset_group)
if args.fp8_base_unet:
args.fp8_base = True
if args.cache_text_encoder_outputs_to_disk and not args.cache_text_encoder_outputs:
logger.warning(
"cache_text_encoder_outputs_to_disk is enabled, so cache_text_encoder_outputs is also enabled / cache_text_encoder_outputs_to_disk 已启用,因此 cache_text_encoder_outputs 也将启用"
)
args.cache_text_encoder_outputs = True
if args.cache_text_encoder_outputs:
assert (
train_dataset_group.is_text_encoder_output_cacheable()
), "when caching Text Encoder output, either caption_dropout_rate, shuffle_caption, token_warmup_step or caption_tag_dropout_rate cannot be used / 缓存文本编码器输出时,caption_dropout_rate, shuffle_caption, token_warmup_step, caption_tag_dropout_rate 不能使用"
if args.max_token_length is not None:
logger.warning("max_token_length is not used in base model training / max_token_length 在基模训练中未使用")
assert (
args.blocks_to_swap is None or args.blocks_to_swap == 0
) or not args.cpu_offload_checkpointing, "blocks_to_swap is not supported with cpu_offload_checkpointing / blocks_to_swap 与 cpu_offload_checkpointing 不兼容"
train_dataset_group.verify_bucket_reso_steps(32)
if val_dataset_group is not None:
val_dataset_group.verify_bucket_reso_steps(32)
def load_target_model(self, args, weight_dtype, accelerator):
"""
加载目标模型(base模型、文本编码器、AE)。
Args:
args: 命令行参数。
weight_dtype: 权重的数据类型。
accelerator: Accelerator 对象。
Returns:
Tuple: 包含模型版本、文本编码器列表、AE 模型和 base 模型的元组。
"""
loading_dtype = None if args.fp8_base else weight_dtype
model = step1x_utils.load_models(
dit_path=args.pretrained_model_name_or_path,
device='cpu',
dtype=loading_dtype
)
if args.fp8_base:
if model.dtype == torch.float8_e4m3fnuz or model.dtype == torch.float8_e5m2 or model.dtype == torch.float8_e5m2fnuz:
raise ValueError(f"Unsupported fp8 model dtype: {model.dtype}")
elif model.dtype == torch.float8_e4m3fn:
logger.info("Loaded fp8 model")
else:
logger.info(
"Cast model to fp8. This may take a while. You can reduce the time by using fp8 checkpoint."
" / 正在将模型转换为 fp8。这可能需要一些时间。您可以使用 fp8 检查点来缩短时间。"
)
model.to(torch.float8_e4m3fn)
self.is_swapping_blocks = args.blocks_to_swap is not None and args.blocks_to_swap > 0
if self.is_swapping_blocks:
logger.info(f"enable block swap: blocks_to_swap={args.blocks_to_swap}")
model.enable_block_swap(args.blocks_to_swap, accelerator.device)
if args.fp8_base and not args.fp8_base_unet:
loading_dtype = None
else:
loading_dtype = weight_dtype
qwen2p5vl = step1x_utils.load_qwen2p5vl(
args.qwen2p5vl, dtype=weight_dtype, device="cpu"
)
qwen2p5vl.eval()
if args.fp8_base and not args.fp8_base_unet:
if qwen2p5vl.dtype == torch.float8_e4m3fnuz or qwen2p5vl.dtype == torch.float8_e5m2 or qwen2p5vl.dtype == torch.float8_e5m2fnuz:
raise ValueError(f"Unsupported fp8 model dtype: {qwen2p5vl.dtype}")
elif qwen2p5vl.dtype == torch.float8_e4m3fn:
logger.info("Loaded fp8 qwen2p5vl model")
ae = step1x_utils.load_ae(args.ae, weight_dtype, "cpu", disable_mmap=args.disable_mmap_load_safetensors)
return "Step1X-Edit", [qwen2p5vl], ae, model
def get_tokenize_strategy(self, args):
"""
获取分词策略。
"""
return strategy_step1x.Step1xEditTokenizeStrategy(tokenizer_cache_dir=args.qwen2p5vl)
def get_tokenizers(self, tokenize_strategy):
return [tokenize_strategy.processor]
def get_latents_caching_strategy(self, args):
latents_caching_strategy = strategy_step1x.Step1XEditLatentsCachingStrategy(
cache_to_disk=args.cache_latents_to_disk,
batch_size=args.vae_batch_size,
skip_disk_cache_validity_check=False,
)
return latents_caching_strategy
def get_text_encoding_strategy(self, args):
return strategy_step1x.Step1XEditEncodingStrategy()
def post_process_network(self, args, accelerator, network, text_encoders, unet):
pass
def get_models_for_text_encoding(self, args, accelerator, text_encoders):
"""
获取用于文本编码的模型。
Args:
args: 命令行参数。
accelerator: Accelerator 对象。
text_encoders: 文本编码器列表。
Returns:
Optional[List[torch.nn.Module]]: 用于文本编码的模型列表,如果不需要则返回 None。
"""
if args.cache_text_encoder_outputs:
return None
else:
return text_encoders
def get_text_encoders_train_flags(self, args, text_encoders):
return [False]
def get_text_encoder_outputs_caching_strategy(self, args):
if args.cache_text_encoder_outputs:
return strategy_step1x.Step1xEditEncoderOutputsCachingStrategy(
cache_to_disk=args.cache_text_encoder_outputs_to_disk,
batch_size=args.text_encoder_batch_size,
skip_disk_cache_validity_check=args.skip_cache_check,
is_partial=False,
)
else:
return None
def cache_text_encoder_outputs_if_needed(
self, args, accelerator: Accelerator, unet, vae, text_encoders, dataset: train_util.DatasetGroup, weight_dtype
):
"""
如果需要,缓存文本编码器的输出。
Args:
args: 命令行参数。
accelerator: Accelerator 对象。
unet: U-Net 模型。
vae: VAE 模型。
text_encoders: 文本编码器列表。
dataset: 数据集组。
weight_dtype: 权重的数据类型。
"""
if args.cache_text_encoder_outputs:
if not args.lowram:
logger.info("move vae and unet to cpu to save memory")
org_vae_device = vae.device
org_unet_device = unet.device
vae.to("cpu")
unet.to("cpu")
clean_memory_on_device(accelerator.device)
logger.info("move text encoders to gpu")
[text_encoder.to(accelerator.device) for text_encoder in text_encoders]
if text_encoders[0].dtype == torch.float8_e4m3fn:
self.prepare_text_encoder_fp8(1, text_encoders[1], text_encoders[1].dtype, weight_dtype)
else:
text_encoders[0].to(weight_dtype)
with accelerator.autocast():
dataset.new_cache_text_encoder_outputs(text_encoders, accelerator)
if args.sample_prompts is not None:
raise ValueError('not converted')
accelerator.wait_for_everyone()
if not self.is_train_text_encoder(args):
text_encoders[0].to("cpu")
clean_memory_on_device(accelerator.device)
if not args.lowram:
logger.info("move vae and unet back to original device")
vae.to(org_vae_device)
unet.to(org_unet_device)
else:
text_encoders[0].to(accelerator.device, dtype=weight_dtype)
def get_noise_scheduler(self, args: argparse.Namespace, device: torch.device) -> Any:
"""
获取噪声调度器。
Args:
args: 命令行参数。
device: 设备 (CPU 或 GPU)。
Returns:
Any: 噪声调度器对象。
"""
noise_scheduler = step1x_edit_train_utils.FlowMatchEulerDiscreteScheduler(num_train_timesteps=1000, shift=args.discrete_flow_shift)
self.noise_scheduler_copy = copy.deepcopy(noise_scheduler)
return noise_scheduler
def encode_images_to_latents(self, args, vae, images):
"""
将图像编码为潜变量。
Args:
args: 命令行参数。
vae: VAE 模型。
images: 图像张量。
Returns:
torch.Tensor: 潜变量张量。
"""
import pdb;pdb.set_trace()
return vae.encode(images)
def shift_scale_latents(self, args, latents):
"""
对潜变量进行移位和缩放。
Args:
args: 命令行参数。
latents: 潜变量张量。
Returns:
torch.Tensor: 经过移位和缩放的潜变量张量。
"""
return latents
def get_noise_pred_and_target(
self,
args,
accelerator,
noise_scheduler,
latents,
ref_latents,
batch,
text_encoder_conds,
unet,
network,
weight_dtype,
train_unet,
is_train=True,
):
"""
获取噪声预测和目标。
这里之所以有了batch还要有latents和text_encoder_conds是因为有可能没有采取cache策略
这部分的处理是在外部完成的
Args:
args: 命令行参数。
accelerator: Accelerator 对象。
noise_scheduler: 噪声调度器。
latents: 潜变量。
batch: 当前批次的数据。
text_encoder_conds: 文本编码器的条件。
unet: 基模
network: 训练的网络。
weight_dtype: 权重的数据类型。
train_unet: 是否训练 U-Net。
is_train: 是否处于训练模式。
Returns:
Tuple[torch.Tensor, torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
模型预测、目标、时间步长和权重 (如果适用)。
"""
noise = torch.randn_like(latents)
bsz = latents.shape[0]
noisy_model_input, timesteps, sigmas = step1x_edit_train_utils.get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, accelerator.device, weight_dtype
)
packed_noisy_model_input = step1x_utils.pack_latents(noisy_model_input)
packed_latent_height, packed_latent_width = noisy_model_input.shape[2] // 2, noisy_model_input.shape[3] // 2
img_ids = step1x_utils.prepare_img_ids(bsz, packed_latent_height, packed_latent_width).to(device=accelerator.device)
packed_ref_model_input = step1x_utils.pack_latents(ref_latents)
packed_noisy_model_input = torch.cat([packed_noisy_model_input, packed_ref_model_input], dim=1)
img_ids = torch.cat([img_ids, img_ids], dim=1)
if args.gradient_checkpointing:
noisy_model_input.requires_grad_(True)
for t in text_encoder_conds:
if t is not None and t.dtype.is_floating_point:
t.requires_grad_(True)
img_ids.requires_grad_(True)
embeds, masks = text_encoder_conds
masks = masks.to(torch.long)
txt_ids = torch.zeros(bsz, embeds.shape[1], 3).to(packed_noisy_model_input.device)
with torch.set_grad_enabled(is_train), torch.autocast(device_type='cuda', dtype=torch.bfloat16):
packed_noisy_model_input = packed_noisy_model_input.to(weight_dtype)
masks = masks.to(device=accelerator.device)
model_pred = unet(
img=packed_noisy_model_input,
img_ids=img_ids,
txt_ids=txt_ids,
timesteps=timesteps / 1000,
llm_embedding=embeds,
t_vec=timesteps,
mask=masks,
)
def unpack_latents(x: torch.Tensor, packed_latent_height: int, packed_latent_width: int) -> torch.Tensor:
"""
x: [b (h w) (c ph pw)] -> [b c (h ph) (w pw)], ph=2, pw=2
"""
import einops
x = einops.rearrange(x, "b (p h w) (c ph pw) -> b p c (h ph) (w pw)", h=packed_latent_height, w=packed_latent_width, ph=2, pw=2, p=2)
return x[:, 0]
model_pred = unpack_latents(model_pred, packed_latent_height, packed_latent_width)
weighting = None
target = noise - latents
return model_pred, target, timesteps, weighting
def post_process_loss(self, loss, args, timesteps, noise_scheduler):
"""
后处理损失。
Args:
loss: 计算得到的损失。
args: 命令行参数。
timesteps: 时间步长。
noise_scheduler: 噪声调度器。
Returns:
torch.Tensor: 后处理后的损失。
"""
return loss
def get_sai_model_spec(self, args):
"""
获取 SAI 模型规范。
Args:
args: 命令行参数。
Returns:
Dict: SAI 模型规范字典。
"""
return {
"modelspec.sai_model_spec": "1.0.0",
"modelspec.architecture": "Step1X-Edit",
"modelspec.implementation": "https://github.com/stepfun-ai/Step1X-Edit",
"modelspec.title": "Lora",
"modelspec.resolution": "1024",
"modelspec.description": "Lora for Step1X-Edit",
"modelspec.author": "Step1X-Edit Team",
"modelspec.date": "2025",
}
def update_metadata(self, metadata, args):
"""
更新元数据。
Args:
metadata: 要更新的元数据字典。
args: 命令行参数。
"""
metadata["ss_weighting_scheme"] = args.weighting_scheme
metadata["ss_logit_mean"] = args.logit_mean
metadata["ss_logit_std"] = args.logit_std
metadata["ss_mode_scale"] = args.mode_scale
metadata["ss_guidance_scale"] = args.guidance_scale
metadata["ss_timestep_sampling"] = args.timestep_sampling
metadata["ss_sigmoid_scale"] = args.sigmoid_scale
metadata["ss_model_prediction_type"] = args.model_prediction_type
metadata["ss_discrete_flow_shift"] = args.discrete_flow_shift
def is_text_encoder_not_needed_for_training(self, args):
"""
判断在训练过程中是否不需要文本编码器。
Args:
args: 命令行参数。
Returns:
bool: 如果不需要文本编码器则返回 True,否则返回 False。
"""
return args.cache_text_encoder_outputs and not self.is_train_text_encoder(args)
def prepare_text_encoder_grad_ckpt_workaround(self, index, text_encoder):
"""
为文本编码器的梯度检查点准备解决方法。
"""
if index == 0:
text_encoder.model.model.embed_tokens.encoder.embed_tokens.requires_grad_(True)
def prepare_text_encoder_fp8(self, index, text_encoder, te_weight_dtype, weight_dtype):
"""
为 fp8 准备文本编码器。
Args:
index: 文本编码器的索引
text_encoder: 文本编码器模型。
te_weight_dtype: 文本编码器的权重数据类型。
weight_dtype: 整体权重的数据类型。
"""
raise ValueError('qwen still not tested for fp8')
if step1x_utils.get_qwen_actual_dtype(text_encoder) == torch.float8_e4m3fn and text_encoder.dtype == weight_dtype:
logger.info(f"Qwen already prepared for fp8")
else:
logger.info(f"prepare Qwen for fp8: set to {te_weight_dtype}, set embeddings to {weight_dtype}, add hooks")
text_encoder.to(te_weight_dtype)
def on_validation_step_end(self, args, accelerator, network, text_encoders, unet, batch, weight_dtype):
"""
在验证步骤结束时调用。
Args:
args: 命令行参数。
accelerator: Accelerator 对象。
network: 网络模型。
text_encoders: 文本编码器列表。
unet: U-Net 模型。
batch: 当前批次的数据。
weight_dtype: 权重的数据类型。
"""
if self.is_swapping_blocks:
accelerator.unwrap_model(unet).prepare_block_swap_before_forward()
def prepare_unet_with_accelerator(
self, args: argparse.Namespace, accelerator: Accelerator, unet: torch.nn.Module
) -> torch.nn.Module:
"""
使用 Accelerator 准备 U-Net 模型。
Args:
args: 命令行参数。
accelerator: Accelerator 对象。
unet: U-Net 模型。
Returns:
torch.nn.Module: 准备好的 U-Net 模型。
"""
if not self.is_swapping_blocks:
return super().prepare_unet_with_accelerator(args, accelerator, unet)
new_unet = unet
new_unet = accelerator.prepare(new_unet, device_placement=[not self.is_swapping_blocks])
accelerator.unwrap_model(new_unet).move_to_device_except_swap_blocks(accelerator.device)
accelerator.unwrap_model(new_unet).prepare_block_swap_before_forward()
return new_unet
def setup_parser() -> argparse.ArgumentParser:
"""
设置命令行参数解析器。
Returns:
argparse.ArgumentParser: 参数解析器对象。
"""
parser = kohya_trainer.setup_parser()
train_util.add_dit_training_arguments(parser)
step1x_edit_train_utils.add_step1x_edit_train_arguments(parser)
parser.add_argument('--qwen2p5vl', type=str, help='Path to Qwen2.5VL model / Qwen2.5VL模型的路径')
return parser
if __name__ == "__main__":
parser = setup_parser()
args = parser.parse_args()
train_util.verify_command_line_training_args(args)
args = train_util.read_config_from_file(args, parser)
trainer = Step1XEditNetworkTrainer()
trainer.train(args)