# Modified from scripts/wan2.1_fun/train.py for MiniMax-H3, aligned to the videox-fun unified training # scaffold (parameter set, trainable modules, EMA, abnormal gradient clip, checkpointing). # # Full finetuning of the packed-sequence transformer on the *video and audio* rows together, covering `t2v` # (text only), `fl2va` (first-frame keyframe conditioning, the keyframe taken from the training sample itself) # and `ref2va` (reference image / video / audio conditioning, loaded from `transformer_ref`). # The layout mirrors `scripts/ltx2.3/train.py`: batch-level training (bs=1, the packed layout is per-sample), # video + audio flow-matching loss weighted 0.5 / 0.5, FSDP + offload composable. # # MiniMax-H3's rectified-flow convention is the *opposite* of Wan's and is reproduced here from # `MiniMaxH3Scheduler.scale_noise` / `MiniMaxH3Scheduler.step`, the single source of truth: # * noising: `x_t = t * x0 + (1 - t) * noise` with `t = 1` clean, `t = 1 - sigma`, # * the sigma grid is exponentially shifted, `sigma' = s * sigma / (1 + (s - 1) * sigma)`, `s = 12.0` for video and `3.0` for audio, # * the transformer predicts a data-ward velocity, so the regression target is `x0 - noise`. # # The checkpoint is guidance-distilled: one forward per step, no unconditional branch. # # Usage: # accelerate launch scripts/minimax_h3/train.py \ # --pretrained_model_name_or_path=/root/MiniMax-H3 \ # --train_mode=fl2va --gradient_checkpointing --low_vram --trainable_modules "." import argparse import gc import inspect import logging import math import os import pickle import random import shutil import sys import warnings import accelerate import datasets import diffusers import numpy as np import torch import torch.nn.functional as F import torch.utils.checkpoint import transformers from accelerate import Accelerator from accelerate.logging import get_logger from accelerate.state import AcceleratorState from accelerate.utils import ProjectConfiguration, set_seed from diffusers.models.autoencoders.vae import DiagonalGaussianDistribution from diffusers.optimization import get_scheduler from diffusers.training_utils import (EMAModel, compute_density_for_timestep_sampling) from diffusers.utils.torch_utils import is_compiled_module from packaging import version from PIL import Image from torch.utils.tensorboard import SummaryWriter from torchvision import transforms from transformers.utils import ContextManagers current_file_path = os.path.abspath(__file__) project_roots = [os.path.dirname(current_file_path), os.path.dirname(os.path.dirname(current_file_path)), os.path.dirname(os.path.dirname(os.path.dirname(current_file_path)))] for project_root in project_roots: sys.path.insert(0, project_root) if project_root not in sys.path else None from videox_fun.data import (ASPECT_RATIO_512, AspectRatioBatchImageVideoSampler, ImageVideoSampler, RandomSampler, VideoSpeechDataset, get_closest_ratio) from videox_fun.models import (AutoencoderKLMiniMaxH3, AutoencoderKLMiniMaxH3Audio, MiniMaxH3Transformer3DModel, Qwen2TokenizerFast, Qwen3VLForConditionalGeneration, Qwen3VLProcessor) from videox_fun.pipeline import MiniMaxH3Pipeline from videox_fun.pipeline.pipeline_minimax_h3 import ( MINIMAX_H3_KEYFRAME_NOISE_AUG, MINIMAX_H3_KEYFRAME_ENCODE_SEED, MINIMAX_H3_FPS, MINIMAX_H3_PIXEL_MEAN, MINIMAX_H3_PIXEL_STD, MINIMAX_H3_TEXT_ENCODER_LAYER, MINIMAX_H3_TEXT_TAG, MINIMAX_H3_VIDEO_SAMPLE_FPS, MINIMAX_H3_VIDEO_TAG, _offload_scope, align_num_frames, audio_latent_num_frames, build_packed_sequence, build_ref2va_packed_sequence, build_row_timesteps, check_ref2va_references, keyframe_condition_noise, normalize_ref2va_references, patchify_video_latents, prepare_keyframe_image, ref2va_condition_rows, video_latent_num_frames) from videox_fun.utils import MiniMaxH3Scheduler from videox_fun.utils.fsdp_ema import FSDPEMA from videox_fun.utils.tqdm_bar import PauseAwareTqdm from videox_fun.utils.utils import save_videos_grid # Silences diffusers' `randn_tensor` notice about CPU generators producing CUDA tensors (the tensor is created # on CPU and moved to GPU; harmless, only a marginal speed note). warnings.filterwarnings("ignore", message="The passed generator was created on") def _mm_token_type_ids(tokenizer, token_ids): image_pad_id = tokenizer.convert_tokens_to_ids("<|image_pad|>") video_pad_id = tokenizer.convert_tokens_to_ids("<|video_pad|>") return [1 if t == image_pad_id else 2 if t == video_pad_id else 0 for t in token_ids] def gather_ref2va_vision_features(processor, references): r"""Run references' pixels through the conditioner's processors, batched per modality. Returns (vision_inputs, image_token_counts, video_block_token_counts, video_block_timestamps). """ merge_size = processor.image_processor.merge_size**2 vision_inputs = {} image_token_counts = [] video_block_token_counts = [] video_block_timestamps = [] images = [ref.image for ref in references if ref.kind == "image"] if images: image_features = processor.image_processor(images=images, return_tensors="pt") vision_inputs["pixel_values"] = image_features["pixel_values"] vision_inputs["image_grid_thw"] = image_features["image_grid_thw"] image_token_counts = [ int(grid.prod()) // merge_size for grid in image_features["image_grid_thw"] ] videos = [ref for ref in references if ref.kind == "video"] if videos: temporal_patch = processor.video_processor.temporal_patch_size sampled = [ MiniMaxH3Pipeline._sample_ref2va_condition_frames( ref.frames, float(ref.fps), MINIMAX_H3_VIDEO_SAMPLE_FPS, temporal_patch ) for ref in videos ] video_block_timestamps = [timestamps for _, timestamps in sampled] video_features = processor.video_processor( videos=[np.stack(frames) for frames, _ in sampled], do_sample_frames=False, return_tensors="pt" ) vision_inputs["pixel_values_videos"] = video_features["pixel_values_videos"] vision_inputs["video_grid_thw"] = video_features["video_grid_thw"] video_block_token_counts = [ int(grid[1]) * int(grid[2]) // merge_size for grid in video_features["video_grid_thw"] ] for timestamps, grid in zip(video_block_timestamps, video_features["video_grid_thw"]): if int(grid[0]) != len(timestamps): raise ValueError( f"The processor merged a reference video into {int(grid[0])} vision blocks, but MiniMax-H3 " f"labels {len(timestamps)} of them." ) return vision_inputs, image_token_counts, video_block_token_counts, video_block_timestamps def build_ref2va_presentation(tokenizer, references, image_token_counts, video_block_token_counts, video_block_timestamps, prompt): r"""Tokenize MiniMax-H3's presentation of a `ref2va` request.""" def text(value): ids = tokenizer(value, add_special_tokens=False)["input_ids"] return ids, [MINIMAX_H3_TEXT_TAG] * len(ids) def vision(pad_token, num_tokens): ids = ( [tokenizer.convert_tokens_to_ids("<|vision_start|>")] + [tokenizer.convert_tokens_to_ids(pad_token)] * num_tokens + [tokenizer.convert_tokens_to_ids("<|vision_end|>")] ) return ids, [MINIMAX_H3_VIDEO_TAG] * len(ids) token_ids, token_tags = [], [] counts = {"image": 0, "video": 0, "audio": 0} for reference in references: if reference.has_audio: counts["audio"] += 1 ids, tags = text(f"