onnx / moss_image_train_network.py
trixlyaisolutions's picture
Upload folder using huggingface_hub
3251839 verified
Raw History Blame Contribute Delete
24.5 kB
# MOSS-Image LoRA training script
import argparse
from typing import Any, Optional, Union
import torch
import torch.nn as nn
from accelerate import Accelerator
from library.device_utils import init_ipex, clean_memory_on_device
from library.custom_offloading_utils import BlockSwapConfig
init_ipex()
from library import (
convrot_int8_kernels,
convrot_int8_utils,
flux2_vae,
flux_train_utils,
moss_image_models,
moss_image_train_utils,
moss_image_utils,
sd3_train_utils,
strategy_base,
strategy_moss_image,
sampling,
)
import library.args as args_util
import library.compile_utils as compile_utils
import library.model_io as model_io
from library.dataset import DatasetGroup, MinimalDataset
import train_network
from library.utils import setup_logging
setup_logging()
import logging
logger = logging.getLogger(__name__)
class MossImageNetworkTrainer(train_network.NetworkTrainer):
def __init__(self):
super().__init__()
self.sample_prompts_te_outputs = None
self.is_swapping_blocks = False
self._use_unsloth_offload_checkpointing = False
# BatchNorm statistics of the FLUX.2 VAE, kept so that the latents can be normalized after the VAE is freed
self.bn_running_mean: Optional[torch.Tensor] = None
self.bn_running_var: Optional[torch.Tensor] = None
# effective ConvRot INT8 state after loading (pre-quantized files are detected independently of the options)
self.dit_convrot_int8 = False
self.text_encoder_convrot_int8 = False
def assert_extra_args(
self,
args,
train_dataset_group: Union[DatasetGroup, MinimalDataset],
val_dataset_group: Optional[DatasetGroup],
):
flux_train_utils.log_timestep_sampling_info(args)
assert args.text_encoder is not None, "--text_encoder (Qwen3.5-9B checkpoint) is required / --text_encoder(Qwen3.5-9B のチェックポイント)を指定してください"
assert args.vae is not None, "--vae (FLUX.2 VAE checkpoint) is required / --vae(FLUX.2 VAE のチェックポイント)を指定してください"
if args.fp8_base or args.fp8_base_unet:
logger.warning(
"fp8_base and fp8_base_unet are not supported for MOSS-Image: use --convrot_int8 (and --convrot_int8_text_encoder)"
" instead. They are disabled."
" / fp8_base と fp8_base_unet は MOSS-Image ではサポートされていません。代わりに --convrot_int8(と"
" --convrot_int8_text_encoder)を使ってください。無効化します。"
)
args.fp8_base = False
args.fp8_base_unet = False
args.fp8_scaled = False
if args.convrot_int8_bwd == "int8":
# the triton requirement is also checked when the layers are patched; fail early here
assert convrot_int8_kernels.HAS_TRITON, (
"--convrot_int8_bwd int8 requires triton (triton-windows on Windows)"
" / --convrot_int8_bwd int8 には triton(Windows では triton-windows)が必要です"
)
if not (args.convrot_int8 or args.convrot_int8_text_encoder):
logger.info(
"--convrot_int8_bwd int8 only takes effect with a ConvRot INT8 base (--convrot_int8, --convrot_int8_text_encoder"
" or a pre-quantized checkpoint)"
)
if args.mixed_precision == "fp16":
logger.warning(
"mixed_precision fp16 is not recommended for MOSS-Image: the text encoder and the DiT are bf16 models. Use bf16."
" / MOSS-Image では mixed_precision fp16 は推奨されません。Text Encoder と DiT は bf16 のモデルです。bf16 を使ってください。"
)
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")
args.cache_text_encoder_outputs = True
if args.cache_text_encoder_outputs:
# caption dropout and multiple caption candidates are supported by MossImageTextEncoderOutputsCachingStrategy
assert train_dataset_group.is_text_encoder_output_cacheable(
cache_supports_dropout=True, supports_multiple_caption_candidates=True
), "when caching Text Encoder output, shuffle_caption, token_warmup_step or caption_tag_dropout_rate cannot be used"
assert (
args.network_train_unet_only or not args.cache_text_encoder_outputs
), "network for Text Encoder cannot be trained with caching Text Encoder outputs / Text Encoderの出力をキャッシュしながらText Encoderのネットワークを学習することはできません"
if self.is_train_text_encoder(args):
logger.info(
"Training the Text Encoder (Qwen3.5-9B) LoRA. Note: the official recommendation is to keep the Text Encoder frozen."
" / Text Encoder(Qwen3.5-9B)の LoRA を学習します。公式は Text Encoder の凍結を推奨しています。"
)
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"
if args.unsloth_offload_checkpointing:
if not args.gradient_checkpointing:
logger.warning("unsloth_offload_checkpointing is enabled, so gradient_checkpointing is also enabled")
args.gradient_checkpointing = True
assert (
not args.cpu_offload_checkpointing
), "Cannot use both --unsloth_offload_checkpointing and --cpu_offload_checkpointing"
assert (
args.blocks_to_swap is None or args.blocks_to_swap == 0
), "blocks_to_swap is not supported with unsloth_offload_checkpointing"
if args.compile:
assert not args.torch_compile, (
"--compile (per-block torch.compile) and --torch_compile (accelerate dynamo) cannot be used together"
" / --compile(ブロック単位torch.compile)と--torch_compile(accelerate dynamo)は併用できません"
)
assert not (args.compile_fullgraph and args.split_attn), (
"--compile_fullgraph cannot be used with --split_attn (split attention uses dynamic control flow)"
" / --compile_fullgraphは--split_attnと併用できません(split attentionは動的な制御フローを使用します)"
)
# FLUX.2 VAE spatial downscale = 8 and patch size = 2
train_dataset_group.verify_bucket_reso_steps(16)
if val_dataset_group is not None:
val_dataset_group.verify_bucket_reso_steps(16)
def load_target_model(self, args, weight_dtype, accelerator):
self.is_swapping_blocks = args.blocks_to_swap is not None and args.blocks_to_swap > 0
# Text encoder (Qwen3.5-9B) is always bf16 (see cast_text_encoder). Loaded to the CPU, moved to the GPU when needed.
logger.info("Loading MOSS-Image text encoder (Qwen3.5-9B)...")
text_encoder = moss_image_utils.load_text_encoder(
args.text_encoder,
dtype=torch.bfloat16,
device="cpu",
disable_mmap=args.disable_mmap_load_safetensors,
convrot_int8=args.convrot_int8_text_encoder,
convrot_int8_bwd=args.convrot_int8_bwd,
calc_device=accelerator.device, # quantize on the GPU, keep the weights on the CPU
)
text_encoder.eval()
self.text_encoder_convrot_int8 = convrot_int8_utils.is_convrot_int8_model(text_encoder)
# VAE (FLUX.2) is always float32 (see cast_vae)
logger.info("Loading FLUX.2 VAE...")
vae = flux2_vae.load_flux2_vae(args.vae, dtype=torch.float32, device="cpu", disable_mmap=args.disable_mmap_load_safetensors)
vae.eval()
self.bn_running_mean = vae.bn.running_mean.detach().clone()
self.bn_running_var = vae.bn.running_var.detach().clone()
# Return format: (model_type, text_encoders, vae, unet)
return "moss_image", [text_encoder], vae, None # DiT is loaded lazily
def load_unet_lazily(self, args, weight_dtype, accelerator, text_encoders) -> tuple[nn.Module, list[nn.Module]]:
loading_device = "cpu" if self.is_swapping_blocks else accelerator.device
attn_mode = "torch"
if args.xformers:
attn_mode = "xformers"
if args.attn_mode is not None:
attn_mode = args.attn_mode
logger.info(f"Loading MOSS-Image DiT with attn_mode={attn_mode}, split_attn: {args.split_attn}...")
model = moss_image_utils.load_moss_image_model(
accelerator.device,
args.pretrained_model_name_or_path,
attn_mode,
args.split_attn,
loading_device,
weight_dtype,
disable_mmap=args.disable_mmap_load_safetensors,
convrot_int8=args.convrot_int8,
convrot_int8_bwd=args.convrot_int8_bwd,
)
self.dit_convrot_int8 = convrot_int8_utils.is_convrot_int8_model(model)
# The base NetworkTrainer only calls enable_gradient_checkpointing(cpu_offload=...); unsloth is re-applied in
# prepare_unet_with_accelerator.
self._use_unsloth_offload_checkpointing = args.unsloth_offload_checkpointing
if self.is_swapping_blocks:
# The base weights are frozen in LoRA training. Freeze them here, before enabling block swap, for the H2D-only offloader.
model.requires_grad_(False)
swap_config = BlockSwapConfig.from_args(args, accelerator.device, supports_backward=True)
logger.info(
f"enable block swap: blocks_to_swap={args.blocks_to_swap}, H2D-only: {swap_config.h2d_only}, "
f"use pinned memory: {swap_config.use_pinned_memory}"
)
model.enable_block_swap(args.blocks_to_swap, swap_config)
return model, text_encoders
# region strategies
def get_tokenize_strategy(self, args):
return strategy_moss_image.MossImageTokenizeStrategy(args.moss_max_token_length, args.tokenizer_cache_dir)
def get_tokenizers(self, tokenize_strategy: strategy_moss_image.MossImageTokenizeStrategy):
return [tokenize_strategy.tokenizer]
def get_latents_caching_strategy(self, args):
return strategy_moss_image.MossImageLatentsCachingStrategy(
args.cache_latents_to_disk, args.vae_batch_size, args.skip_cache_check
)
def get_text_encoding_strategy(self, args):
return strategy_moss_image.MossImageTextEncodingStrategy()
def get_text_encoder_outputs_caching_strategy(self, args):
if args.cache_text_encoder_outputs:
return strategy_moss_image.MossImageTextEncoderOutputsCachingStrategy(
args.cache_text_encoder_outputs_to_disk, args.text_encoder_batch_size, args.skip_cache_check, False
)
return None
# endregion
def post_process_network(self, args, accelerator, network, text_encoders, unet):
pass
def get_models_for_text_encoding(self, args, accelerator, text_encoders):
if args.cache_text_encoder_outputs:
return None # no text encoders needed for encoding
return text_encoders
def cast_text_encoder(self, args):
return False # the text encoder stays in bf16 regardless of the mixed precision setting
def cast_vae(self, args):
return False # the VAE stays in float32
def cast_unet(self, args):
# The DiT is loaded in weight_dtype by load_moss_image_model. `Module.to(dtype)` would also cast the fp32
# `scale_weight` buffers of the ConvRot INT8 layers, so the cast is skipped here.
return False
def cache_text_encoder_outputs_if_needed(
self, args, accelerator: Accelerator, unet, vae, text_encoders, dataset: DatasetGroup, weight_dtype
):
if args.cache_text_encoder_outputs:
if not args.lowram:
# the DiT is not loaded yet, so only the VAE is moved
logger.info("move vae to cpu to save memory")
org_vae_device = vae.device
vae.to("cpu")
clean_memory_on_device(accelerator.device)
logger.info("move text encoder to gpu")
text_encoders[0].to(accelerator.device)
# no autocast: the text encoder is bf16 and the official pipeline runs it in bf16
dataset.new_cache_text_encoder_outputs(text_encoders, accelerator)
# cache sample prompts
if args.sample_prompts is not None:
logger.info(f"cache Text Encoder outputs for sample prompts: {args.sample_prompts}")
tokenize_strategy = strategy_base.TokenizeStrategy.get_strategy()
text_encoding_strategy = strategy_base.TextEncodingStrategy.get_strategy()
prompts = sampling.load_prompts(args.sample_prompts)
sample_prompts_te_outputs = {}
with torch.no_grad():
for prompt_dict in prompts:
for p in [prompt_dict.get("prompt", ""), prompt_dict.get("negative_prompt", "")]:
if p not in sample_prompts_te_outputs:
logger.info(f" cache TE outputs for: {p}")
tokens = tokenize_strategy.tokenize(p)
sample_prompts_te_outputs[p] = text_encoding_strategy.encode_tokens(
tokenize_strategy, text_encoders, tokens
)
self.sample_prompts_te_outputs = sample_prompts_te_outputs
accelerator.wait_for_everyone()
logger.info("move text encoder back to cpu")
text_encoders[0].to("cpu")
if not args.lowram:
logger.info("move vae back to original device")
vae.to(org_vae_device)
clean_memory_on_device(accelerator.device)
else:
# move text encoder to device for encoding during training/validation
text_encoders[0].to(accelerator.device)
def sample_images(self, accelerator, args, epoch, global_step, device, vae, tokenizer, text_encoder, unet):
text_encoders = text_encoder if isinstance(text_encoder, list) else [text_encoder] # compatibility
te = self.get_models_for_text_encoding(args, accelerator, text_encoders)
te = te[0] if te is not None else None
text_encoding_strategy = strategy_base.TextEncodingStrategy.get_strategy()
tokenize_strategy = strategy_base.TokenizeStrategy.get_strategy()
moss_image_train_utils.sample_images(
accelerator,
args,
epoch,
global_step,
unet,
vae,
te,
tokenize_strategy,
text_encoding_strategy,
self.sample_prompts_te_outputs,
)
def get_noise_scheduler(self, args: argparse.Namespace, device: torch.device) -> Any:
return sd3_train_utils.FlowMatchEulerDiscreteScheduler(
num_train_timesteps=moss_image_train_utils.NUM_TRAIN_TIMESTEPS, shift=args.discrete_flow_shift
)
def encode_images_to_latents(self, args, vae: flux2_vae.AutoEncoder, images):
# raw latents [B, 32, H/8, W/8] (same as the cache). The VAE is float32: run outside autocast.
with torch.autocast(device_type=vae.device.type, enabled=False):
return vae.encode(images.to(vae.dtype))
def shift_scale_latents(self, args, latents):
# raw latents [B, 32, H/8, W/8] -> DiT input [B, 128, H/16, W/16] (2x2 patchify + BatchNorm normalization)
latents = flux2_vae.patchify_latents(latents)
return flux2_vae.normalize_latents(latents, self.bn_running_mean, self.bn_running_var)
def get_noise_pred_and_target(
self,
args,
accelerator,
noise_scheduler,
latents,
batch,
text_encoder_conds,
unet,
network,
weight_dtype,
train_unet,
is_train=True,
):
dit: moss_image_models.MossImage = unet
noise = torch.randn_like(latents)
# Per-sample timestep sampling offset from custom_attributes: timestep_sampling = { offset = ... }
# Only applied during training; validation uses unbiased sampling for comparable loss.
tso = None
if is_train and "custom_attributes" in batch:
offsets = [ca.get("timestep_sampling", {}).get("offset", 0.0) for ca in batch["custom_attributes"]]
t = torch.tensor(offsets, dtype=torch.float32)
if t.abs().sum() > 0:
tso = t
# Timesteps and sigmas are kept in float32 (bf16 would quantize sigma * 1000 to multiples of 4). The DiT casts
# the noisy input to its own dtype.
noisy_model_input, timesteps, sigmas = flux_train_utils.get_noisy_model_input_and_timesteps(
args, noise_scheduler, latents, noise, accelerator.device, torch.float32, timestep_sampling_offset=tso
)
# timesteps are sigma * 1000: passed to the DiT as is (the official pipeline does not divide by 1000)
prompt_embeds, attn_mask = text_encoder_conds[:2] # caption_dropout_rate (cached outputs) is not needed here
prompt_embeds = prompt_embeds.to(accelerator.device, dtype=weight_dtype)
attn_mask = attn_mask.to(accelerator.device)
# Gradient checkpointing (non-reentrant) needs an input which requires grad
if args.gradient_checkpointing:
noisy_model_input.requires_grad_(True)
prompt_embeds.requires_grad_(True)
with torch.set_grad_enabled(is_train), accelerator.autocast():
model_pred = dit(noisy_model_input, timesteps, prompt_embeds, attn_mask)
# Rectified flow target: v = noise - x0
target = noise - latents
# 1 for logit_normal (the official training uses no loss weighting)
weighting = flux_train_utils.compute_loss_weighting_for_sd3(args.weighting_scheme, sigmas)
return model_pred, target, timesteps, weighting
def process_batch(
self,
batch,
text_encoders,
unet,
network,
vae,
noise_scheduler,
vae_dtype,
weight_dtype,
accelerator,
args,
text_encoding_strategy,
tokenize_strategy,
is_train=True,
train_text_encoder=True,
train_unet=True,
) -> torch.Tensor:
"""Caption dropout for cached text encoder outputs: dropped samples use the cached unconditional embeddings."""
text_encoder_outputs_list = batch.get("text_encoder_outputs_list", None)
if text_encoder_outputs_list is not None:
moss_text_encoding_strategy: strategy_moss_image.MossImageTextEncodingStrategy = text_encoding_strategy
caption_dropout_rates = text_encoder_outputs_list[-1]
# [prompt_embeds, attn_mask, uncond_embeds, uncond_mask] -> [prompt_embeds, attn_mask]
text_encoder_outputs_list = moss_text_encoding_strategy.drop_cached_text_encoder_outputs(
*text_encoder_outputs_list[:-1], caption_dropout_rates=caption_dropout_rates
)
# Add the caption dropout rates back to the list for validation dataset (which is re-used batch items)
batch["text_encoder_outputs_list"] = text_encoder_outputs_list + [caption_dropout_rates]
return super().process_batch(
batch,
text_encoders,
unet,
network,
vae,
noise_scheduler,
vae_dtype,
weight_dtype,
accelerator,
args,
text_encoding_strategy,
tokenize_strategy,
is_train,
train_text_encoder,
train_unet,
)
def post_process_loss(self, loss, args, timesteps, noise_scheduler):
return loss
def get_sai_model_spec(self, args):
return model_io.get_sai_model_spec_dataclass(None, args, False, True, False, moss_image="base").to_metadata_dict()
def update_metadata(self, 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_timestep_sampling"] = args.timestep_sampling
metadata["ss_sigmoid_scale"] = args.sigmoid_scale
metadata["ss_discrete_flow_shift"] = args.discrete_flow_shift
metadata["ss_convrot_int8"] = self.dit_convrot_int8
metadata["ss_convrot_int8_text_encoder"] = self.text_encoder_convrot_int8
def is_text_encoder_not_needed_for_training(self, args):
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):
# Set the first floating point parameter's requires_grad to True to workaround Accelerate gradient checkpointing bug.
# The first parameter (token embedding) is int8 with ConvRot INT8 and cannot require grad.
first_param = next(p for p in text_encoder.parameters() if p.is_floating_point())
first_param.requires_grad_(True)
def prepare_unet_with_accelerator(
self, args: argparse.Namespace, accelerator: Accelerator, unet: torch.nn.Module
) -> torch.nn.Module:
# The base NetworkTrainer only calls enable_gradient_checkpointing(cpu_offload=True/False),
# so we re-apply with unsloth_offload if needed (after base has already enabled it).
if self._use_unsloth_offload_checkpointing and args.gradient_checkpointing:
unet.enable_gradient_checkpointing(unsloth_offload=True)
if not self.is_swapping_blocks:
model = super().prepare_unet_with_accelerator(args, accelerator, unet)
else:
model = accelerator.prepare(unet, device_placement=[not self.is_swapping_blocks])
accelerator.unwrap_model(model).move_to_device_except_swap_blocks(accelerator.device)
accelerator.unwrap_model(model).prepare_block_swap_before_forward()
# CUDA perf switches are independent of torch.compile; apply whenever requested.
compile_utils.apply_cuda_optimizations(args)
if args.compile:
# Per-block torch.compile of the 42 main blocks. Reach the real model via unwrap_model so the underlying
# ModuleList is mutated regardless of any DDP wrapper.
# ConvRot INT8 Linears are also excluded: the custom autograd.Function + autotuned Triton kernels do not compile.
dit = accelerator.unwrap_model(model)
compile_utils.compile_transformer(
args, dit, [dit.layers], disable_linear=self.is_swapping_blocks or convrot_int8_utils.is_convrot_int8_model(dit)
)
return model
def on_validation_step_end(self, args, accelerator, network, text_encoders, unet, batch, weight_dtype):
if self.is_swapping_blocks:
# prepare for next forward: because backward pass is not called, we need to prepare it here
accelerator.unwrap_model(unet).prepare_block_swap_before_forward()
def setup_parser() -> argparse.ArgumentParser:
parser = train_network.setup_parser()
args_util.add_dit_training_arguments(parser)
moss_image_train_utils.add_moss_image_model_arguments(parser)
moss_image_train_utils.add_moss_image_training_arguments(parser)
return parser
if __name__ == "__main__":
parser = setup_parser()
args = parser.parse_args()
args_util.verify_command_line_training_args(args)
args = args_util.read_config_from_file(args, parser)
if args.attn_mode == "sdpa":
args.attn_mode = "torch" # backward compatibility
if args.show_timesteps:
moss_image_train_utils.show_timesteps(args)
else:
trainer = MossImageNetworkTrainer()
trainer.train(args)