[feat] FastVideo World Model Training (#1179)

Co-authored-by: mignonjia <mhuo@ucsd.edu>
Co-authored-by: alexzms <3036648523@qq.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Kaiqin Kong
2026-05-11 05:13:12 +00:00
committed by GitHub
co-authored by mignonjia alexzms mergify[bot]
parent 636d3b743e
commit d6dfe95466
29 changed files with 4541 additions and 5143 deletions
+2 -1
View File
@@ -30,7 +30,8 @@ FastVideo-WorldModel/
│ │ │ ├── fine_tuning/ # FineTuneMethod, DiffusionForcingSFTMethod
│ │ │ └── distribution_matching/ # DMD2Method, SelfForcingMethod
│ │ ├── models/ # Per-role model wrappers (ModelBase, CausalModelBase)
│ │ │ └── wan/ # WanModel, WanCausalModel
│ │ │ ├── wan/ # WanModel, WanCausalModel
│ │ │ └── matrixgame/ # MatrixGameModel, MatrixGameCausalModel
│ │ ├── callbacks/ # Composable hooks (grad_clip, ema, validation)
│ │ └── utils/ # Config, builder, checkpoint, optimizer, tracking
│ ├── training/ # Legacy training infrastructure (being phased out)
@@ -226,6 +226,9 @@ callbacks:
| Wan T2V finetune | `fastvideo/training/wan_training_pipeline.py` | Standard text-to-video finetune / LoRA |
| Wan I2V finetune | `fastvideo/training/wan_i2v_training_pipeline.py` | Image-to-video (first frame conditioned) |
| MatrixGame finetune | `fastvideo/training/matrixgame_training_pipeline.py` | Action-conditioned world model |
| MatrixGame AR diffusion | `fastvideo/training/matrixgame_ar_diffusion_pipeline.py` | AR diffusion-forcing training |
| MatrixGame ODE-init | `fastvideo/training/matrixgame_ode_causal_pipeline.py` | ODE-trajectory init |
| MatrixGame self-forcing distill | `fastvideo/training/matrixgame_self_forcing_distillation_pipeline.py` | Self-forcing distillation |
| LTX-2 finetune | `fastvideo/training/ltx2_training_pipeline.py` | LTX-2 architecture finetuning |
| Wan DMD distillation | `fastvideo/training/wan_distillation_pipeline.py` | Few-step distillation via DMD |
| Self-Forcing distill | `fastvideo/training/wan_self_forcing_distillation_pipeline.py` | Causal streaming distillation |
@@ -0,0 +1,126 @@
#!/bin/bash
export WANDB_API_KEY="${WANDB_API_KEY:-}"
export WANDB_MODE="offline"
export TOKENIZERS_PARALLELISM=false
RUN_NAME=$(date +"%m%d_%H%M")
echo "RUN_NAME: $RUN_NAME"
# Model paths for Self-Forcing DMD distillation.
GENERATOR_MODEL_PATH="FastVideo/Matrix-Game-2.0-Base-Diffusers"
REAL_SCORE_MODEL_PATH="FastVideo/Matrix-Game-2.0-Foundation-Diffusers" # Teacher
FAKE_SCORE_MODEL_PATH="FastVideo/Matrix-Game-2.0-Foundation-Diffusers" # Critic
DATA_DIR="data/matrixgame"
VALIDATION_DATASET_FILE="examples/distill/MatrixGame2.0/validation.json"
NUM_GPUS=1
# Training arguments
training_args=(
--tracker_project_name "matrixgame_sf"
--output_dir "checkpoints/matrixgame_sf_${RUN_NAME}"
--wandb_run_name "${RUN_NAME}_test"
--max_train_steps 5
--train_batch_size 1
--train_sp_batch_size 1
--gradient_accumulation_steps 1
--num_latent_t 21
--num_height 352
--num_width 640
# --enable_gradient_checkpointing_type "full"
--simulate_generator_forward
--num_frames 81
--num_frame_per_block 3 # Frame generation block size for self-forcing
# --enable_gradient_masking
# --gradient_mask_last_n_frames 21
# --init_weights_from_safetensors "path/to/generator_ema.safetensors"
)
# Parallel arguments
parallel_args=(
--num_gpus $NUM_GPUS
--sp_size 1
--tp_size 1
--hsdp_replicate_dim 1
--hsdp_shard_dim $NUM_GPUS
)
model_args=(
--model_path $GENERATOR_MODEL_PATH # TODO: check if you can remove this in this script
--pretrained_model_name_or_path $GENERATOR_MODEL_PATH
--real_score_model_path $REAL_SCORE_MODEL_PATH
--fake_score_model_path $FAKE_SCORE_MODEL_PATH
)
dataset_args=(
--data_path "$DATA_DIR"
--dataloader_num_workers 4
)
# Validation arguments
validation_args=(
--log_validation
--log_visualization
--visualization-steps 100
--validation_dataset_file "$VALIDATION_DATASET_FILE"
--validation_steps 100
--validation_sampling_steps "4"
--validation_guidance_scale "6.0"
)
# Optimizer arguments
optimizer_args=(
--learning_rate 3e-6
--mixed_precision "bf16"
--weight_only_checkpointing_steps 400
--training_state_checkpointing_steps 400
--weight_decay 0
--betas "0.9,0.95"
--max_grad_norm 1.0
)
# Miscellaneous arguments
miscellaneous_args=(
--inference_mode False
--checkpoints_total_limit 3
--training_cfg_rate 0.0
--dit_precision "fp32"
--flow_shift 5
--seed 1000
--use_ema True
--ema_decay 0.99
--ema_start_step 200
)
dmd_args=(
--dmd_denoising_steps '1000,750,500,250'
--min_timestep_ratio 0.02
--max_timestep_ratio 0.98
--dfake_gen_update_ratio 5
--real_score_guidance_scale 3.0
--fake_score_learning_rate 3e-7
--fake_score_betas "0.9,0.95"
--warp_denoising_step
)
self_forcing_args=(
--independent_first_frame False # Whether to treat first frame independently
--same_step_across_blocks True # Whether to use same denoising step across all blocks
--last_step_only False # Whether to only use the last denoising step
--context_noise 0 # Amount of noise to add during context caching (0 = no noise)
)
torchrun \
--nnodes 1 \
--nproc_per_node $NUM_GPUS \
fastvideo/training/matrixgame_self_forcing_distillation_pipeline.py \
"${parallel_args[@]}" \
"${model_args[@]}" \
"${dataset_args[@]}" \
"${training_args[@]}" \
"${optimizer_args[@]}" \
"${validation_args[@]}" \
"${miscellaneous_args[@]}" \
"${dmd_args[@]}" \
"${self_forcing_args[@]}"
@@ -0,0 +1,168 @@
#!/bin/bash
#SBATCH --job-name=matrixgame_sf_dmd
#SBATCH --partition=main
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --ntasks-per-node=1
#SBATCH --gres=gpu:4
#SBATCH --cpus-per-task=128
#SBATCH --mem=1440G
#SBATCH --output=logs/slurm/matrixgame_sf_dmd_%j.out
#SBATCH --error=logs/slurm/matrixgame_sf_dmd_%j.err
#SBATCH --exclusive
set -euo pipefail
set -x
CONDA_ROOT="${CONDA_ROOT:-$HOME/miniforge3}"
CONDA_BIN="${CONDA_ROOT}/bin/conda"
ENV_NAME="${ENV_NAME:-FastVideo_kaiqin}"
MASTER_PORT="${MASTER_PORT:-29500}"
WANDB_API_KEY="${WANDB_API_KEY:-}"
WANDB_MODE="${WANDB_MODE:-online}"
GPUS_PER_NODE="${SLURM_GPUS_ON_NODE:-4}"
TOTAL_GPUS="$(( SLURM_JOB_NUM_NODES * GPUS_PER_NODE ))"
export NCCL_P2P_DISABLE=1
export TORCH_NCCL_ENABLE_MONITORING=0
export TRITON_CACHE_DIR="/tmp/triton_cache_${SLURM_PROCID}"
export TOKENIZERS_PARALLELISM=false
export WANDB_API_KEY
export WANDB_MODE
if [ -x "${CONDA_BIN}" ]; then
eval "$("${CONDA_BIN}" shell.bash hook)"
conda activate "${ENV_NAME}"
else
echo "conda not found at ${CONDA_BIN}" >&2
exit 1
fi
cd "${SLURM_SUBMIT_DIR}"
mkdir -p logs/slurm
export PYTHONPATH="${SLURM_SUBMIT_DIR}${PYTHONPATH:+:${PYTHONPATH}}"
nodes=( $(scontrol show hostnames "${SLURM_JOB_NODELIST}") )
export MASTER_ADDR="${nodes[0]}"
export MASTER_PORT
export NODE_RANK="${SLURM_PROCID}"
echo "MASTER_ADDR: ${MASTER_ADDR}"
echo "MASTER_PORT: ${MASTER_PORT}"
echo "NODE_RANK: ${NODE_RANK}"
echo "GPUS_PER_NODE: ${GPUS_PER_NODE}"
echo "TOTAL_GPUS: ${TOTAL_GPUS}"
RUN_NAME="$(date +"%m%d_%H%M")"
echo "RUN_NAME: ${RUN_NAME}"
GENERATOR_MODEL_PATH="FastVideo/Matrix-Game-2.0-Base-Diffusers"
REAL_SCORE_MODEL_PATH="FastVideo/Matrix-Game-2.0-Foundation-Diffusers"
FAKE_SCORE_MODEL_PATH="FastVideo/Matrix-Game-2.0-Foundation-Diffusers"
DATA_DIR="data/matrixgame"
VALIDATION_DATASET_FILE="examples/distill/MatrixGame2.0/validation.json"
training_args=(
--tracker_project_name "matrixgame_sf"
--output_dir "checkpoints/matrixgame_sf_${RUN_NAME}"
--wandb_run_name "${RUN_NAME}_test"
--max_train_steps 1200
--train_batch_size 1
--train_sp_batch_size 1
--gradient_accumulation_steps 2
--num_latent_t 21
--num_height 352
--num_width 640
--simulate_generator_forward
--num_frames 81
--num_frame_per_block 3
# --init_weights_from_safetensors "path/to/generator_ema.safetensors"
)
parallel_args=(
--num_gpus "${TOTAL_GPUS}"
--sp_size 1
--tp_size 1
--hsdp_replicate_dim 1
--hsdp_shard_dim "${TOTAL_GPUS}"
)
model_args=(
--model_path "${GENERATOR_MODEL_PATH}"
--pretrained_model_name_or_path "${GENERATOR_MODEL_PATH}"
--real_score_model_path "${REAL_SCORE_MODEL_PATH}"
--fake_score_model_path "${FAKE_SCORE_MODEL_PATH}"
)
dataset_args=(
--data_path "${DATA_DIR}"
--dataloader_num_workers 4
)
validation_args=(
--log_validation
--log_visualization
--visualization-steps 100
--validation_dataset_file "${VALIDATION_DATASET_FILE}"
--validation_steps 100
--validation_sampling_steps "4"
--validation_guidance_scale "6.0"
)
optimizer_args=(
--learning_rate 3e-6
--mixed_precision "bf16"
--weight_only_checkpointing_steps 400
--training_state_checkpointing_steps 400
--weight_decay 0
--betas "0.9,0.95"
--max_grad_norm 1.0
)
miscellaneous_args=(
--inference_mode False
--checkpoints_total_limit 3
--training_cfg_rate 0.0
--dit_precision "fp32"
--flow_shift 5
--seed 1000
--use_ema True
--ema_decay 0.99
--ema_start_step 200
)
dmd_args=(
--dmd_denoising_steps "1000,750,500,250"
--min_timestep_ratio 0.02
--max_timestep_ratio 0.98
--dfake_gen_update_ratio 5
--real_score_guidance_scale 3.0
--fake_score_learning_rate 3e-7
--fake_score_betas "0.9,0.95"
--warp_denoising_step
)
self_forcing_args=(
--independent_first_frame False
--same_step_across_blocks True
--last_step_only False
--context_noise 0
)
srun python -m torch.distributed.run \
--nnodes "${SLURM_JOB_NUM_NODES}" \
--nproc_per_node "${GPUS_PER_NODE}" \
--node_rank "${SLURM_PROCID}" \
--rdzv_backend=c10d \
--rdzv_endpoint="${MASTER_ADDR}:${MASTER_PORT}" \
fastvideo/training/matrixgame_self_forcing_distillation_pipeline.py \
"${parallel_args[@]}" \
"${model_args[@]}" \
"${dataset_args[@]}" \
"${training_args[@]}" \
"${optimizer_args[@]}" \
"${validation_args[@]}" \
"${miscellaneous_args[@]}" \
"${dmd_args[@]}" \
"${self_forcing_args[@]}"
@@ -0,0 +1,356 @@
{
"data": [
{
"caption": "00",
"image_path": "path_to_validation/00.png",
"ref_video": "path_to_validation/00.mp4",
"action_path": "path_to_validation/00.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "01",
"image_path": "path_to_validation/01.png",
"ref_video": "path_to_validation/01.mp4",
"action_path": "path_to_validation/01.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "02",
"image_path": "path_to_validation/02.png",
"ref_video": "path_to_validation/02.mp4",
"action_path": "path_to_validation/02.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "03",
"image_path": "path_to_validation/03.png",
"ref_video": "path_to_validation/03.mp4",
"action_path": "path_to_validation/03.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "04",
"image_path": "path_to_validation/04.png",
"ref_video": "path_to_validation/04.mp4",
"action_path": "path_to_validation/04.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "05",
"image_path": "path_to_validation/05.png",
"ref_video": "path_to_validation/05.mp4",
"action_path": "path_to_validation/05.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "06",
"image_path": "path_to_validation/06.png",
"ref_video": "path_to_validation/06.mp4",
"action_path": "path_to_validation/06.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "07",
"image_path": "path_to_validation/07.png",
"ref_video": "path_to_validation/07.mp4",
"action_path": "path_to_validation/07.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "08",
"image_path": "path_to_validation/08.png",
"ref_video": "path_to_validation/08.mp4",
"action_path": "path_to_validation/08.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "09",
"image_path": "path_to_validation/09.png",
"ref_video": "path_to_validation/09.mp4",
"action_path": "path_to_validation/09.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "10",
"image_path": "path_to_validation/10.png",
"ref_video": "path_to_validation/10.mp4",
"action_path": "path_to_validation/10.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "11",
"image_path": "path_to_validation/11.png",
"ref_video": "path_to_validation/11.mp4",
"action_path": "path_to_validation/11.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "12",
"image_path": "path_to_validation/12.png",
"ref_video": "path_to_validation/12.mp4",
"action_path": "path_to_validation/12.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "13",
"image_path": "path_to_validation/13.png",
"ref_video": "path_to_validation/13.mp4",
"action_path": "path_to_validation/13.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "14",
"image_path": "path_to_validation/14.png",
"ref_video": "path_to_validation/14.mp4",
"action_path": "path_to_validation/14.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "15",
"image_path": "path_to_validation/15.png",
"ref_video": "path_to_validation/15.mp4",
"action_path": "path_to_validation/15.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "16",
"image_path": "path_to_validation/16.png",
"ref_video": "path_to_validation/16.mp4",
"action_path": "path_to_validation/16.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "17",
"image_path": "path_to_validation/17.png",
"ref_video": "path_to_validation/17.mp4",
"action_path": "path_to_validation/17.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "18",
"image_path": "path_to_validation/18.png",
"ref_video": "path_to_validation/18.mp4",
"action_path": "path_to_validation/18.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "19",
"image_path": "path_to_validation/19.png",
"ref_video": "path_to_validation/19.mp4",
"action_path": "path_to_validation/19.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "20",
"image_path": "path_to_validation/20.png",
"ref_video": "path_to_validation/20.mp4",
"action_path": "path_to_validation/20.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "21",
"image_path": "path_to_validation/21.png",
"ref_video": "path_to_validation/21.mp4",
"action_path": "path_to_validation/21.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "22",
"image_path": "path_to_validation/22.png",
"ref_video": "path_to_validation/22.mp4",
"action_path": "path_to_validation/22.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "23",
"image_path": "path_to_validation/23.png",
"ref_video": "path_to_validation/23.mp4",
"action_path": "path_to_validation/23.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "24",
"image_path": "path_to_validation/24.png",
"ref_video": "path_to_validation/24.mp4",
"action_path": "path_to_validation/24.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "25",
"image_path": "path_to_validation/25.png",
"ref_video": "path_to_validation/25.mp4",
"action_path": "path_to_validation/25.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "26",
"image_path": "path_to_validation/26.png",
"ref_video": "path_to_validation/26.mp4",
"action_path": "path_to_validation/26.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "27",
"image_path": "path_to_validation/27.png",
"ref_video": "path_to_validation/27.mp4",
"action_path": "path_to_validation/27.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "28",
"image_path": "path_to_validation/28.png",
"ref_video": "path_to_validation/28.mp4",
"action_path": "path_to_validation/28.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "29",
"image_path": "path_to_validation/29.png",
"ref_video": "path_to_validation/29.mp4",
"action_path": "path_to_validation/29.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "30",
"image_path": "path_to_validation/30.png",
"ref_video": "path_to_validation/30.mp4",
"action_path": "path_to_validation/30.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
},
{
"caption": "31",
"image_path": "path_to_validation/31.png",
"ref_video": "path_to_validation/31.mp4",
"action_path": "path_to_validation/31.npy",
"video_path": null,
"num_inference_steps": 4,
"height": 352,
"width": 640,
"num_frames": 81
}
]
}
@@ -27,10 +27,10 @@ training:
train_batch_size: 1
training_cfg_rate: 0.0
seed: 1000
num_latent_t: 20
num_latent_t: 18
num_height: 448
num_width: 832
num_frames: 77
num_frames: 69
optimizer:
learning_rate: 2.0e-6
@@ -50,7 +50,7 @@ training:
tracker:
project_name: distillation_wan_r
run_name: wan2.1_causal_dfsft
run_name: wan2.1_causal_dfsft_shift5_gauss_weight
model:
enable_gradient_checkpointing_type: full
@@ -60,11 +60,11 @@ callbacks:
max_grad_norm: 1.0
validation:
pipeline_target: fastvideo.pipelines.basic.wan.wan_causal_pipeline.WanCausalPipeline
dataset_file: examples/training/finetune/Wan2.1-VSA/Wan-Syn-Data/validation_64.json
dataset_file: examples/training/finetune/Wan2.1-VSA/Wan-Syn-Data/validation_4.json
every_steps: 50
sampling_steps: [40]
guidance_scale: 6.0
num_frames: 69
pipeline:
flow_shift: 8
flow_shift: 5
@@ -0,0 +1,32 @@
# World-Model: MatrixGame 2.0 I2V
Three training scenarios for the MatrixGame 2.0 I2V world model on the
new YAML-driven trainer (`fastvideo/train/entrypoint/train.py`).
| Config | Method | Student | Notes |
|---|---|---|---|
| `finetune_i2v.yaml` | `FineTuneMethod` | `MatrixGameModel` (bidirectional) | Multi-step SFT from `mg_bidirectional_Solaris`. |
| `dfsft_causal_i2v.yaml` | `DiffusionForcingSFTMethod` | `MatrixGameCausalModel` | Diffusion-Forcing SFT with chunkwise timesteps. |
| `self_forcing_causal_i2v.yaml` | `SelfForcingMethod` | `MatrixGameCausalModel` | DMD/Self-Forcing distillation; teacher = bidirectional, critic = bidirectional. |
## Usage
```bash
bash examples/train/run.sh \
examples/train/scenario/worldmodel/finetune_i2v.yaml
bash examples/train/run.sh \
examples/train/scenario/worldmodel/dfsft_causal_i2v.yaml
bash examples/train/run.sh \
examples/train/scenario/worldmodel/self_forcing_causal_i2v.yaml
```
Override any field on the command line:
```bash
bash examples/train/run.sh \
examples/train/scenario/worldmodel/dfsft_causal_i2v.yaml \
--training.distributed.num_gpus 8 \
--training.optimizer.learning_rate 1e-5
```
@@ -0,0 +1,74 @@
# Diffusion-Forcing SFT: MatrixGame I2V Causal
#
# - Student: trainable causal MatrixGame model
# - Training: inhomogeneous timesteps per chunk (diffusion forcing)
models:
student:
_target_: fastvideo.train.models.matrixgame.matrixgame_causal.MatrixGameCausalModel
init_from: FastVideo/Matrix-Game-2.0-Foundation-Diffusers
trainable: true
method:
_target_: fastvideo.train.methods.fine_tuning.dfsft.DiffusionForcingSFTMethod
chunk_size: 3
training:
distributed:
num_gpus: 4
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 4
data:
data_path: data/matrixgame
dataloader_num_workers: 4
train_batch_size: 1
training_cfg_rate: 0.0
seed: 1000
num_latent_t: 21
num_height: 352
num_width: 640
num_frames: 81
optimizer:
learning_rate: 2.0e-5
betas: [0.9, 0.999]
weight_decay: 1.0e-4
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 1500
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/matrixgame_causal_dfsft
training_state_checkpointing_steps: 100
checkpoints_total_limit: 3
tracker:
project_name: dfsft-matrixgame
run_name: matrixgame_causal_dfsft
model:
enable_gradient_checkpointing_type: full
callbacks:
grad_clip:
max_grad_norm: 1.0
validation:
pipeline_target: fastvideo.pipelines.basic.matrixgame.matrixgame_causal_dmd_pipeline.MatrixGameCausalDMDPipeline
dataset_file: examples/distill/MatrixGame2.0/validation.json
every_steps: 100
sampling_steps: [40]
sampling_timesteps: [1000, 975, 950, 925, 900, 875, 850, 825, 800, 775,
750, 725, 700, 675, 650, 625, 600, 575, 550, 525,
500, 475, 450, 425, 400, 375, 350, 325, 300, 275,
250, 225, 200, 175, 150, 125, 100, 75, 50, 25]
num_frames: 81
guidance_scale: 6.0
pipeline:
flow_shift: 5
@@ -0,0 +1,72 @@
# MatrixGame I2V finetune.
models:
student:
_target_: fastvideo.train.models.matrixgame.matrixgame.MatrixGameModel
init_from: FastVideo/Matrix-Game-2.0-Foundation-Diffusers
trainable: true
method:
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
training:
distributed:
num_gpus: 1
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 1
data:
data_path: data/matrixgame
dataloader_num_workers: 4
train_batch_size: 1
training_cfg_rate: 0.0
seed: 1000
num_latent_t: 21
num_height: 448
num_width: 832
num_frames: 81
optimizer:
learning_rate: 1.0e-6
betas: [0.9, 0.999]
weight_decay: 0.01
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 1
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/matrixgame_i2v_finetune
training_state_checkpointing_steps: 100
checkpoints_total_limit: 3
resume_from_checkpoint: latest
# tracker:
# project_name: fastvideo
# run_name: matrixgame_i2v_finetune
tracker:
trackers: [none]
project_name: ""
run_name: ""
model:
enable_gradient_checkpointing_type: full
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
validation:
_target_: fastvideo.train.callbacks.validation.ValidationCallback
pipeline_target: fastvideo.pipelines.basic.matrixgame.matrixgame_i2v_pipeline.MatrixGamePipeline
dataset_file: examples/distill/MatrixGame2.0/validation.json
every_steps: 100
sampling_steps: [50]
guidance_scale: 5.0
pipeline:
flow_shift: 3
@@ -0,0 +1,100 @@
# Self-forcing distillation: MatrixGame I2V Causal
#
# - Teacher: frozen pretrained MatrixGame model
# - Student: trainable causal MatrixGame model
# - Critic: trainable MatrixGame model
# - Training: streaming rollout with SDE sampling
models:
student:
_target_: fastvideo.train.models.matrixgame.matrixgame_causal.MatrixGameCausalModel
init_from: FastVideo/Matrix-Game-2.0-Base-Diffusers
trainable: true
teacher:
_target_: fastvideo.train.models.matrixgame.matrixgame.MatrixGameModel
init_from: FastVideo/Matrix-Game-2.0-Foundation-Diffusers
trainable: false
disable_custom_init_weights: true
critic:
_target_: fastvideo.train.models.matrixgame.matrixgame.MatrixGameModel
init_from: FastVideo/Matrix-Game-2.0-Foundation-Diffusers
trainable: true
disable_custom_init_weights: true
method:
_target_: fastvideo.train.methods.distribution_matching.self_forcing.SelfForcingMethod
rollout_mode: simulate
generator_update_interval: 5
real_score_guidance_scale: 4.5
dmd_denoising_steps: [1000, 750, 500, 250]
chunk_size: 3
student_sample_type: sde
context_noise: 0.0
enable_gradient_in_rollout: true
start_gradient_frame: 0
# Critic optimizer
fake_score_learning_rate: 8.0e-6
fake_score_betas: [0.0, 0.999]
fake_score_lr_scheduler: constant
training:
distributed:
num_gpus: 4
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 4
data:
data_path: data/matrixgame
dataloader_num_workers: 4
train_batch_size: 1
training_cfg_rate: 0.0
seed: 1000
num_latent_t: 21
num_height: 352
num_width: 640
num_frames: 81
optimizer:
learning_rate: 1.0e-5
betas: [0.0, 0.999]
weight_decay: 0.01
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 1
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/matrixgame_causal_self_forcing
training_state_checkpointing_steps: 1000
checkpoints_total_limit: 3
tracker:
project_name: self-forcing-matrixgame
run_name: matrixgame_causal_self_forcing
model:
enable_gradient_checkpointing_type: full
callbacks:
ema:
decay: 0.99
start_iter: 200
grad_clip:
max_grad_norm: 1.0
validation:
pipeline_target: fastvideo.pipelines.basic.matrixgame.matrixgame_causal_dmd_pipeline.MatrixGameCausalDMDPipeline
dataset_file: examples/distill/MatrixGame2.0/validation.json
every_steps: 50
sampling_steps: [4]
sampling_timesteps: [1000, 750, 500, 250]
num_frames: 81
guidance_scale: 6.0
pipeline:
flow_shift: 5
@@ -0,0 +1,94 @@
#!/bin/bash
export WANDB_BASE_URL="https://api.wandb.ai"
export WANDB_MODE=online
export TOKENIZERS_PARALLELISM=false
# export FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA
MODEL_PATH="FastVideo/Matrix-Game-2.0-Foundation-Diffusers"
DATA_DIR="footsies-dataset/preprocessed/combined_parquet_dataset"
VALIDATION_DATASET_FILE="$(dirname "$0")/validation.json"
NUM_GPUS=8
# export CUDA_VISIBLE_DEVICES=4,5
# IP=[MASTER NODE IP]
# Training arguments
training_args=(
--tracker_project_name "matrixgame_finetune"
--output_dir "checkpoints/matrixgame_finetune"
--override_transformer_cls_name "CausalMatrixGameWanModel"
--max_train_steps 1500
--train_batch_size 1
--train_sp_batch_size 1
--gradient_accumulation_steps 4
--num_latent_t 21
--num_height 352
--num_width 640
--num_frames 81
--enable_gradient_checkpointing_type "full"
)
# Parallel arguments
parallel_args=(
--num_gpus $NUM_GPUS
--sp_size 2
--tp_size 1
--hsdp_replicate_dim 4
--hsdp_shard_dim 2
)
# Model arguments
model_args=(
--model_path $MODEL_PATH
--pretrained_model_name_or_path $MODEL_PATH
)
# Dataset arguments
dataset_args=(
--data_path "$DATA_DIR"
--dataloader_num_workers 1
)
# Validation arguments
validation_args=(
--log_validation
--validation_dataset_file "$VALIDATION_DATASET_FILE"
--validation_steps 100
--validation_sampling_steps "40"
--validation_guidance_scale "6.0"
)
# Optimizer arguments
optimizer_args=(
--learning_rate 2e-5
--mixed_precision "bf16"
--weight_only_checkpointing_steps 100
--training_state_checkpointing_steps 100
--weight_decay 1e-4
--max_grad_norm 1.0
)
# Miscellaneous arguments
miscellaneous_args=(
--inference_mode False
--checkpoints_total_limit 3
--training_cfg_rate 0.1
--multi_phased_distill_schedule "4000-1"
--not_apply_cfg_solver
--dit_precision "fp32"
--num_euler_timesteps 50
--ema_start_step 0
)
# If you do not have 32 GPUs and to fit in memory, you can: 1. increase sp_size. 2. reduce num_latent_t
torchrun \
--nnodes 1 \
--nproc_per_node $NUM_GPUS \
fastvideo/training/matrixgame_ar_diffusion_pipeline.py \
"${parallel_args[@]}" \
"${model_args[@]}" \
"${dataset_args[@]}" \
"${training_args[@]}" \
"${optimizer_args[@]}" \
"${validation_args[@]}" \
"${miscellaneous_args[@]}"
File diff suppressed because it is too large Load Diff
@@ -191,3 +191,110 @@ def text_only_record_creator(text_name: str, text_embedding: np.ndarray,
"caption": caption,
}
return record
def matrixgame_ode_record_creator(
video_name: str,
clip_feature: np.ndarray,
first_frame_latent: np.ndarray,
trajectory_latents: np.ndarray,
trajectory_timesteps: np.ndarray,
pil_image: np.ndarray | None = None,
keyboard_cond: np.ndarray | None = None,
mouse_cond: np.ndarray | None = None,
caption: str = "") -> dict[str, Any]:
"""Create a ODE trajectory record matching pyarrow_schema_matrixgame_ode_trajectory.
Args:
video_name: Base name/id for the sample.
clip_feature: CLIP image features.
first_frame_latent: First frame VAE latent.
trajectory_latents: Collected trajectory latents array.
trajectory_timesteps: Collected timesteps array.
pil_image: Optional PIL image array for validation.
keyboard_cond: Optional keyboard action array.
mouse_cond: Optional mouse action array.
caption: Optional caption.
Returns:
dict suitable for records_to_table(…, pyarrow_schema_matrixgame_ode_trajectory)
"""
assert trajectory_latents is not None, "trajectory_latents is required"
assert trajectory_timesteps is not None, "trajectory_timesteps is required"
assert clip_feature is not None, "clip_feature is required"
assert first_frame_latent is not None, "first_frame_latent is required"
record = {
"id": video_name,
"file_name": video_name,
"caption": caption,
"media_type": "video",
}
# I2V features
record.update({
"clip_feature_bytes": clip_feature.tobytes(),
"clip_feature_shape": list(clip_feature.shape),
"clip_feature_dtype": str(clip_feature.dtype),
})
record.update({
"first_frame_latent_bytes": first_frame_latent.tobytes(),
"first_frame_latent_shape": list(first_frame_latent.shape),
"first_frame_latent_dtype": str(first_frame_latent.dtype),
})
# Optional PIL Image
if pil_image is not None:
record.update({
"pil_image_bytes": pil_image.tobytes(),
"pil_image_shape": list(pil_image.shape),
"pil_image_dtype": str(pil_image.dtype),
})
else:
record.update({
"pil_image_bytes": b"",
"pil_image_shape": [],
"pil_image_dtype": "",
})
# Actions
if keyboard_cond is not None:
record.update({
"keyboard_cond_bytes": keyboard_cond.tobytes(),
"keyboard_cond_shape": list(keyboard_cond.shape),
"keyboard_cond_dtype": str(keyboard_cond.dtype),
})
else:
record.update({
"keyboard_cond_bytes": b"",
"keyboard_cond_shape": [],
"keyboard_cond_dtype": "",
})
if mouse_cond is not None:
record.update({
"mouse_cond_bytes": mouse_cond.tobytes(),
"mouse_cond_shape": list(mouse_cond.shape),
"mouse_cond_dtype": str(mouse_cond.dtype),
})
else:
record.update({
"mouse_cond_bytes": b"",
"mouse_cond_shape": [],
"mouse_cond_dtype": "",
})
record.update({
"trajectory_latents_bytes": trajectory_latents.tobytes(),
"trajectory_latents_shape": list(trajectory_latents.shape),
"trajectory_latents_dtype": str(trajectory_latents.dtype),
})
record.update({
"trajectory_timesteps_bytes": trajectory_timesteps.tobytes(),
"trajectory_timesteps_shape": list(trajectory_timesteps.shape),
"trajectory_timesteps_dtype": str(trajectory_timesteps.dtype),
})
return record
+33
View File
@@ -157,3 +157,36 @@ pyarrow_schema_matrixgame = pa.schema([
pa.field("duration_sec", pa.float64()),
pa.field("fps", pa.float64()),
])
pyarrow_schema_matrixgame_ode_trajectory = pa.schema([
pa.field("id", pa.string()),
# --- Action ---
pa.field("mouse_cond_bytes", pa.binary()),
pa.field("mouse_cond_shape", pa.list_(pa.int64())), # [T, 2]
pa.field("mouse_cond_dtype", pa.string()),
pa.field("keyboard_cond_bytes", pa.binary()),
pa.field("keyboard_cond_shape", pa.list_(pa.int64())), # [T, 4]
pa.field("keyboard_cond_dtype", pa.string()),
#I2V
pa.field("clip_feature_bytes", pa.binary()),
pa.field("clip_feature_shape", pa.list_(pa.int64())),
pa.field("clip_feature_dtype", pa.string()),
pa.field("first_frame_latent_bytes", pa.binary()),
pa.field("first_frame_latent_shape", pa.list_(pa.int64())),
pa.field("first_frame_latent_dtype", pa.string()),
# I2V Validation
pa.field("pil_image_bytes", pa.binary()),
pa.field("pil_image_shape", pa.list_(pa.int64())),
pa.field("pil_image_dtype", pa.string()),
# --- ODE Trajectory ---
pa.field("trajectory_latents_bytes", pa.binary()),
pa.field("trajectory_latents_shape", pa.list_(pa.int64())),
pa.field("trajectory_latents_dtype", pa.string()),
pa.field("trajectory_timesteps_bytes", pa.binary()),
pa.field("trajectory_timesteps_shape", pa.list_(pa.int64())),
pa.field("trajectory_timesteps_dtype", pa.string()),
# --- Metadata ---
pa.field("file_name", pa.string()),
pa.field("caption", pa.string()),
pa.field("media_type", pa.string()), # 'image' or 'video'
])
+10
View File
@@ -139,6 +139,16 @@ class ValidationDataset(IterableDataset):
else:
sample["video"] = load_video(video_path)
if sample.get("ref_video", None) is not None:
ref_video_path = sample["ref_video"]
ref_video_path = os.path.join(self.dir, ref_video_path)
if not pathlib.Path(ref_video_path).is_file():
logger.warning("Reference video file %s does not exist.",
ref_video_path)
sample.pop("ref_video", None)
else:
sample["ref_video"] = ref_video_path
if sample.get("control_image_path", None) is not None:
control_image_path = sample["control_image_path"]
control_image_path = os.path.join(self.dir, control_image_path)
@@ -150,6 +150,13 @@ def _update_kv_cache_and_attend(
else int(kv_cache["local_end_index"])
)
if torch.is_grad_enabled():
kv_cache["k"] = kv_cache["k"].detach().clone()
kv_cache["v"] = kv_cache["v"].detach().clone()
else:
kv_cache["k"] = kv_cache["k"].detach()
kv_cache["v"] = kv_cache["v"].detach()
# Check if we need to evict tokens
if (current_end > original_global_end_index) and (
num_new_tokens + original_local_end_index > kv_cache_size
+80 -108
View File
@@ -278,6 +278,13 @@ class CausalMatrixGameSelfAttention(nn.Module):
else self.local_attn_size * frame_seqlen
)
if torch.is_grad_enabled():
kv_cache["k"] = kv_cache["k"].detach().clone()
kv_cache["v"] = kv_cache["v"].detach().clone()
else:
kv_cache["k"] = kv_cache["k"].detach()
kv_cache["v"] = kv_cache["v"].detach()
kv_cache_size = kv_cache["k"].shape[1]
num_new_tokens = roped_query.shape[1]
global_end_index = (
@@ -905,13 +912,12 @@ class CausalMatrixGameWanModel(BaseDiT):
encoder_hidden_states, torch.Tensor
):
encoder_hidden_states = encoder_hidden_states[0]
if (
isinstance(encoder_hidden_states_image, list)
and len(encoder_hidden_states_image) > 0
):
encoder_hidden_states_image = encoder_hidden_states_image[0]
else:
encoder_hidden_states_image = None
if isinstance(encoder_hidden_states_image, list):
encoder_hidden_states_image = (
encoder_hidden_states_image[0]
if len(encoder_hidden_states_image) > 0
else None
)
ctx = get_forward_context()
batch = getattr(ctx, "forward_batch", None)
@@ -1024,45 +1030,36 @@ class CausalMatrixGameWanModel(BaseDiT):
else:
encoder_hidden_states = encoder_hidden_states_image
if self.block_mask is None:
self.block_mask = self._prepare_blockwise_causal_attn_mask(
block_mask = self._prepare_blockwise_causal_attn_mask(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=post_patch_height * post_patch_width,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
if self.use_rope_keyboard:
block_mask_keyboard = self._prepare_blockwise_causal_attn_mask_action(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=1,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
else:
block_mask_keyboard = self._prepare_blockwise_causal_attn_mask_keyboard(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=post_patch_height * post_patch_width,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
if self.block_mask_keyboard is None:
if not self.use_rope_keyboard:
self.block_mask_keyboard = (
self._prepare_blockwise_causal_attn_mask_keyboard(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=post_patch_height * post_patch_width,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
)
else:
self.block_mask_keyboard = (
self._prepare_blockwise_causal_attn_mask_action(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=1,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
)
if self.block_mask_mouse is None:
self.block_mask_mouse = (
self._prepare_blockwise_causal_attn_mask_action(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=1,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
)
block_mask_mouse = self._prepare_blockwise_causal_attn_mask_action(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=1,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
if kv_cache is None:
kv_cache = [None] * len(self.blocks)
if kv_cache_mouse is None:
@@ -1073,58 +1070,40 @@ class CausalMatrixGameWanModel(BaseDiT):
crossattn_cache = [None] * len(self.blocks)
for block_index, block in enumerate(self.blocks):
causal_kwargs = {
"kv_cache": kv_cache[block_index],
"kv_cache_mouse": kv_cache_mouse[block_index]
if kv_cache_mouse
else None,
"kv_cache_keyboard": kv_cache_keyboard[block_index]
if kv_cache_keyboard
else None,
"crossattn_cache": crossattn_cache[block_index]
if crossattn_cache
else None,
"current_start": current_start,
"cache_start": cache_start,
}
if torch.is_grad_enabled() and self.gradient_checkpointing:
kwargs.update(
{
"kv_cache": kv_cache[block_index],
"kv_cache_mouse": kv_cache_mouse[block_index]
if kv_cache_mouse
else None,
"kv_cache_keyboard": kv_cache_keyboard[block_index]
if kv_cache_keyboard
else None,
"crossattn_cache": crossattn_cache[block_index]
if crossattn_cache
else None,
"current_start": current_start,
"cache_start": cache_start,
"num_frame_per_block": effective_num_frame_per_block,
}
)
causal_kwargs["num_frame_per_block"] = effective_num_frame_per_block
hidden_states = self._gradient_checkpointing_func(
block, hidden_states, **kwargs
block, hidden_states, **causal_kwargs
)
else:
kwargs.update(
{
"kv_cache": kv_cache[block_index],
"kv_cache_mouse": kv_cache_mouse[block_index]
if kv_cache_mouse
else None,
"kv_cache_keyboard": kv_cache_keyboard[block_index]
if kv_cache_keyboard
else None,
"crossattn_cache": crossattn_cache[block_index]
if crossattn_cache
else None,
"current_start": current_start,
"cache_start": cache_start,
}
)
hidden_states = block(
hidden_states,
encoder_hidden_states,
timestep_proj,
freqs_cis,
block_mask=self.block_mask,
block_mask=block_mask,
grid_sizes=grid_sizes,
mouse_cond=mouse_cond,
keyboard_cond=keyboard_cond,
block_mask_mouse=self.block_mask_mouse,
block_mask_keyboard=self.block_mask_keyboard,
block_mask_mouse=block_mask_mouse,
block_mask_keyboard=block_mask_keyboard,
num_frame_per_block=effective_num_frame_per_block,
use_rope_keyboard=self.use_rope_keyboard,
**kwargs,
**causal_kwargs,
)
temb = temb.unflatten(
@@ -1161,13 +1140,12 @@ class CausalMatrixGameWanModel(BaseDiT):
encoder_hidden_states, torch.Tensor
):
encoder_hidden_states = encoder_hidden_states[0]
if (
isinstance(encoder_hidden_states_image, list)
and len(encoder_hidden_states_image) > 0
):
encoder_hidden_states_image = encoder_hidden_states_image[0]
else:
encoder_hidden_states_image = None
if isinstance(encoder_hidden_states_image, list):
encoder_hidden_states_image = (
encoder_hidden_states_image[0]
if len(encoder_hidden_states_image) > 0
else None
)
ctx = get_forward_context()
batch = getattr(ctx, "forward_batch", None)
@@ -1236,35 +1214,29 @@ class CausalMatrixGameWanModel(BaseDiT):
local_attn_size=self.local_attn_size,
)
if self.block_mask_keyboard is None:
if not self.use_rope_keyboard:
self.block_mask_keyboard = (
self._prepare_blockwise_causal_attn_mask_keyboard(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=post_patch_height * post_patch_width,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
)
else:
self.block_mask_keyboard = (
self._prepare_blockwise_causal_attn_mask_action(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=1,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
)
if self.block_mask_mouse is None:
self.block_mask_mouse = (
self._prepare_blockwise_causal_attn_mask_action(
if self.use_rope_keyboard:
self.block_mask_keyboard = self._prepare_blockwise_causal_attn_mask_action(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=1,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
else:
self.block_mask_keyboard = self._prepare_blockwise_causal_attn_mask_keyboard(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=post_patch_height * post_patch_width,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
if self.block_mask_mouse is None:
self.block_mask_mouse = self._prepare_blockwise_causal_attn_mask_action(
device=hidden_states.device,
num_frames=num_frames,
frame_seqlen=1,
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
)
hidden_states = self.patch_embedding(hidden_states)
@@ -60,7 +60,7 @@ class PreprocessPipeline_MatrixGame(BasePreprocessPipeline):
clip_features = clip_features.last_hidden_state
features["clip_feature"] = clip_features
"""Get VAE features from the first frame of each video"""
# Get CLIP features from the first frame of each video.
video_conditions = []
for frame in first_frame:
processed_img = frame.to(device="cpu", dtype=torch.float32)
@@ -0,0 +1,385 @@
# SPDX-License-Identifier: Apache-2.0
"""
ODE Trajectory Data Preprocessing pipeline implementation.
This module contains an implementation of the ODE Trajectory Data Preprocessing pipeline
using the modular pipeline architecture.
Sec 4.3 of CausVid paper: https://arxiv.org/pdf/2412.07772
"""
import os
from collections.abc import Iterator
from typing import Any
import numpy as np
import pyarrow as pa
import torch
from PIL import Image
from torch.utils.data import DataLoader
from torchdata.stateful_dataloader import StatefulDataLoader
from tqdm import tqdm
from fastvideo.configs.sample import SamplingParam
from fastvideo.dataset import getdataset
from fastvideo.dataset.dataloader.parquet_io import (ParquetDatasetWriter, records_to_table)
from fastvideo.dataset.dataloader.record_schema import (matrixgame_ode_record_creator)
from fastvideo.dataset.dataloader.schema import (pyarrow_schema_matrixgame_ode_trajectory)
from fastvideo.distributed import get_local_torch_device
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.forward_context import set_forward_context
from fastvideo.logger import init_logger
from fastvideo.models.schedulers.scheduling_self_forcing_flow_match import (SelfForcingFlowMatchScheduler)
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.pipelines.preprocess.preprocess_pipeline_base import (BasePreprocessPipeline)
from fastvideo.pipelines.stages import (DecodingStage, InputValidationStage, LatentPreparationStage,
MatrixGameImageEncodingStage, TimestepPreparationStage)
from fastvideo.pipelines.stages.matrixgame_denoising import (MatrixGameCausalDenoisingStage)
from fastvideo.utils import save_decoded_latents_as_video, shallow_asdict
logger = init_logger(__name__)
class PreprocessPipeline_MatrixGame_ODE_Trajectory(BasePreprocessPipeline):
"""ODE Trajectory preprocessing pipeline implementation."""
_required_config_modules = ["vae", "image_encoder", "image_processor", "transformer", "scheduler"]
preprocess_dataloader: StatefulDataLoader
preprocess_loader_iter: Iterator[dict[str, Any]]
pbar: Any
num_processed_samples: int
def get_pyarrow_schema(self) -> pa.Schema:
"""Return the PyArrow schema for ODE Trajectory pipeline."""
return pyarrow_schema_matrixgame_ode_trajectory
def create_pipeline_stages(self, fastvideo_args: FastVideoArgs):
"""Set up pipeline stages with proper dependency injection."""
assert fastvideo_args.pipeline_config.flow_shift == 5
self.modules["scheduler"] = SelfForcingFlowMatchScheduler(shift=fastvideo_args.pipeline_config.flow_shift,
sigma_min=0.0,
extra_one_step=True)
self.modules["scheduler"].set_timesteps(num_inference_steps=48, denoising_strength=1.0)
self.add_stage(stage_name="input_validation_stage", stage=InputValidationStage())
self.add_stage(stage_name="image_encoding_stage",
stage=MatrixGameImageEncodingStage(
image_encoder=self.get_module("image_encoder"),
image_processor=self.get_module("image_processor"),
))
self.add_stage(stage_name="timestep_preparation_stage",
stage=TimestepPreparationStage(scheduler=self.get_module("scheduler")))
self.add_stage(stage_name="latent_preparation_stage",
stage=LatentPreparationStage(scheduler=self.get_module("scheduler"),
transformer=self.get_module("transformer", None)))
self.add_stage(stage_name="denoising_stage",
stage=MatrixGameCausalDenoisingStage(
transformer=self.get_module("transformer"),
scheduler=self.get_module("scheduler"),
pipeline=self,
vae=self.get_module("vae"),
))
self.add_stage(stage_name="decoding_stage", stage=DecodingStage(vae=self.get_module("vae")))
def get_extra_features(self, valid_data: dict[str, Any], fastvideo_args: FastVideoArgs) -> dict[str, Any]:
# TODO(will): move these to cpu at some point
self.get_module("image_encoder").to(get_local_torch_device())
self.get_module("vae").to(get_local_torch_device())
features = {}
"""Get CLIP features from the first frame of each video."""
first_frame = valid_data["pixel_values"][:, :, 0, :, :].permute(0, 2, 3, 1) # (B, C, T, H, W) -> (B, H, W, C)
_, _, num_frames, height, width = valid_data["pixel_values"].shape
# latent_height = height // self.get_module(
# "vae").spatial_compression_ratio
# latent_width = width // self.get_module("vae").spatial_compression_ratio
processed_images = []
# Frame has values between -1 and 1
for frame in first_frame:
frame = (frame + 1) * 127.5
frame_pil = Image.fromarray(frame.cpu().numpy().astype(np.uint8))
processed_img = self.get_module("image_processor")(images=frame_pil, return_tensors="pt")
processed_images.append(processed_img)
# Get CLIP features
pixel_values = torch.cat([img['pixel_values'] for img in processed_images], dim=0).to(get_local_torch_device())
with torch.no_grad():
image_inputs = {'pixel_values': pixel_values}
with set_forward_context(current_timestep=0, attn_metadata=None):
clip_features = self.get_module("image_encoder")(**image_inputs)
clip_features = clip_features.last_hidden_state
features["clip_feature"] = clip_features
features["pil_image"] = first_frame
# Get CLIP features from the first frame of each video.
video_conditions = []
for frame in first_frame:
processed_img = frame.to(device="cpu", dtype=torch.float32)
processed_img = processed_img.unsqueeze(0).permute(0, 3, 1, 2).unsqueeze(2)
# (B, H, W, C) -> (B, C, 1, H, W)
video_condition = torch.cat([
processed_img,
processed_img.new_zeros(processed_img.shape[0], processed_img.shape[1], num_frames - 1, height, width)
],
dim=2)
video_condition = video_condition.to(device=get_local_torch_device(), dtype=torch.float32)
video_conditions.append(video_condition)
video_conditions = torch.cat(video_conditions, dim=0)
with torch.autocast(device_type="cuda", dtype=torch.float32, enabled=True):
encoder_outputs = self.get_module("vae").encode(video_conditions)
# Use mode() instead of mean
latent_condition = encoder_outputs.mode()
# Use latents_mean/latents_std normalization to match
vae = self.get_module("vae")
if (hasattr(vae.config, 'latents_mean') and hasattr(vae.config, 'latents_std')):
latents_mean = torch.tensor(vae.config.latents_mean,
device=latent_condition.device,
dtype=latent_condition.dtype).view(1, -1, 1, 1, 1)
latents_std = torch.tensor(vae.config.latents_std,
device=latent_condition.device,
dtype=latent_condition.dtype).view(1, -1, 1, 1, 1)
latent_condition = (latent_condition - latents_mean) / latents_std
elif (hasattr(vae, "shift_factor") and vae.shift_factor is not None):
if isinstance(vae.shift_factor, torch.Tensor):
latent_condition -= vae.shift_factor.to(latent_condition.device, latent_condition.dtype)
else:
latent_condition -= vae.shift_factor
if isinstance(vae.scaling_factor, torch.Tensor):
latent_condition = latent_condition * vae.scaling_factor.to(latent_condition.device,
latent_condition.dtype)
else:
latent_condition = latent_condition * vae.scaling_factor
# Create mask_cond: ones for first frame, zeros for rest
# Shape: (B, 16, latent_frames, latent_height, latent_width)
mask_cond = torch.ones_like(latent_condition)
mask_cond[:, :, 1:] = 0 # Set all frames except first to 0
# Create cond_concat: first 4 channels of mask + all 16 channels of img_cond
# Shape: (B, 20, latent_frames, latent_height, latent_width)
cond_concat = torch.cat([mask_cond[:, :4], latent_condition], dim=1)
features["first_frame_latent"] = cond_concat
if "action_path" in valid_data and valid_data["action_path"]:
keyboard_cond_list = []
mouse_cond_list = []
keyboard_dim = self.get_module("transformer").config.arch_config.action_config["keyboard_dim_in"]
for action_path in valid_data["action_path"]:
if action_path:
action_data = np.load(action_path, allow_pickle=True)
if isinstance(action_data, np.ndarray) and action_data.dtype == np.dtype('O'):
action_dict = action_data.item()
if "keyboard" in action_dict:
keyboard_cond_list.append(action_dict["keyboard"][:, :keyboard_dim].astype(np.float32))
if "mouse" in action_dict:
mouse_cond_list.append(action_dict["mouse"])
else:
keyboard_cond_list.append(action_data[:, :keyboard_dim].astype(np.float32))
if keyboard_cond_list:
features["keyboard_cond"] = keyboard_cond_list
if mouse_cond_list:
features["mouse_cond"] = mouse_cond_list
return features
def preprocess_action_and_trajectory(self, fastvideo_args: FastVideoArgs, args):
"""Preprocess data and generate trajectory information."""
for batch_idx, data in enumerate(self.pbar):
if data is None:
continue
with torch.inference_mode():
# Filter out invalid samples (those with all zeros)
valid_indices = []
for i, pixel_values in enumerate(data["pixel_values"]):
if not torch.all(pixel_values == 0): # Check if all values are zero
valid_indices.append(i)
self.num_processed_samples += len(valid_indices)
if not valid_indices:
continue
# Create new batch with only valid samples
valid_data = {
"pixel_values": torch.stack([data["pixel_values"][i] for i in valid_indices]),
"path": [data["path"][i] for i in valid_indices],
}
if "fps" in data:
valid_data["fps"] = [data["fps"][i] for i in valid_indices]
if "duration" in data:
valid_data["duration"] = [data["duration"][i] for i in valid_indices]
if "action_path" in data:
valid_data["action_path"] = [data["action_path"][i] for i in valid_indices]
pixel_values = valid_data["pixel_values"]
if pixel_values.shape[2] == 1 and args.num_frames is not None:
pixel_values = pixel_values.repeat(1, 1, args.num_frames, 1, 1)
valid_data["pixel_values"] = pixel_values
# Get extra features if needed
extra_features = self.get_extra_features(valid_data, fastvideo_args)
clip_features = extra_features['clip_feature']
image_latents = extra_features['first_frame_latent']
image_latents = image_latents[:, :, :args.num_latent_t]
pil_image = extra_features['pil_image']
keyboard_cond = extra_features.get('keyboard_cond')
mouse_cond = extra_features.get('mouse_cond')
sampling_params = SamplingParam.from_pretrained(args.model_path)
trajectory_latents = []
trajectory_timesteps = []
trajectory_decoded = []
device = get_local_torch_device()
for i in range(len(valid_indices)):
# Collect the trajectory data
batch = ForwardBatch(**shallow_asdict(sampling_params), )
batch.image_embeds = [clip_features[i].unsqueeze(0)]
batch.image_latent = image_latents[i].unsqueeze(0)
batch.keyboard_cond = (torch.from_numpy(keyboard_cond[i]).unsqueeze(0).to(device)
if keyboard_cond is not None else None)
batch.mouse_cond = (torch.from_numpy(mouse_cond[i]).unsqueeze(0).to(device)
if mouse_cond is not None else None)
batch.num_inference_steps = 48
batch.return_trajectory_latents = True
# Enabling this will save the decoded trajectory videos.
# Used for debugging.
batch.return_trajectory_decoded = False
batch.height = args.max_height
batch.width = args.max_width
batch.fps = args.train_fps
batch.num_frames = valid_data["pixel_values"].shape[2]
batch.guidance_scale = 6.0
batch.do_classifier_free_guidance = False
batch.prompt = ""
result_batch = self.input_validation_stage(batch, fastvideo_args)
result_batch = self.timestep_preparation_stage(batch, fastvideo_args)
result_batch.timesteps = result_batch.timesteps.to(device)
result_batch = self.latent_preparation_stage(result_batch, fastvideo_args)
result_batch = self.denoising_stage(result_batch, fastvideo_args)
result_batch = self.decoding_stage(result_batch, fastvideo_args)
trajectory_latents.append(result_batch.trajectory_latents.cpu())
trajectory_timesteps.append(result_batch.trajectory_timesteps.cpu())
trajectory_decoded.append(result_batch.trajectory_decoded)
# Prepare extra features
extra_features = {
"trajectory_latents": trajectory_latents,
"trajectory_timesteps": trajectory_timesteps
}
if batch.return_trajectory_decoded:
for i, decoded_frames in enumerate(trajectory_decoded):
for j, decoded_frame in enumerate(decoded_frames):
save_decoded_latents_as_video(decoded_frame,
f"decoded_videos/trajectory_decoded_{i}_{j}.mp4",
args.train_fps)
# Prepare batch data for Parquet dataset
batch_data: list[dict[str, Any]] = []
# Add progress bar for saving outputs
save_pbar = tqdm(enumerate(valid_data["path"]), desc="Saving outputs", unit="item", leave=False)
for idx, video_path in save_pbar:
video_name = os.path.basename(video_path).split(".")[0]
clip_feature_np = clip_features[idx].cpu().numpy()
first_frame_latent_np = image_latents[idx].cpu().numpy()
pil_image_np = pil_image[idx].cpu().numpy()
keyboard_cond_np = keyboard_cond[idx] if keyboard_cond is not None else None
mouse_cond_np = mouse_cond[idx] if mouse_cond is not None else None
# Get trajectory features for this sample
traj_latents = extra_features["trajectory_latents"][idx]
traj_timesteps = extra_features["trajectory_timesteps"][idx]
if isinstance(traj_latents, torch.Tensor):
traj_latents = traj_latents.cpu().float().numpy()
if isinstance(traj_timesteps, torch.Tensor):
traj_timesteps = traj_timesteps.cpu().float().numpy()
# Create record for Parquet dataset
record: dict[str, Any] = matrixgame_ode_record_creator(video_name=video_name,
clip_feature=clip_feature_np,
first_frame_latent=first_frame_latent_np,
trajectory_latents=traj_latents,
trajectory_timesteps=traj_timesteps,
pil_image=pil_image_np,
keyboard_cond=keyboard_cond_np,
mouse_cond=mouse_cond_np,
caption="")
batch_data.append(record)
if batch_data:
write_pbar = tqdm(total=1, desc="Writing to Parquet dataset", unit="batch")
table = records_to_table(batch_data, self.get_pyarrow_schema())
write_pbar.update(1)
write_pbar.close()
if not hasattr(self, 'dataset_writer'):
self.dataset_writer = ParquetDatasetWriter(
out_dir=self.combined_parquet_dir,
samples_per_file=args.samples_per_file,
)
self.dataset_writer.append_table(table)
logger.info("Collected batch with %s samples", len(table))
if self.num_processed_samples >= args.flush_frequency:
written = self.dataset_writer.flush()
logger.info("Flushed %s samples to parquet", written)
self.num_processed_samples = 0
# Final flush for any remaining samples
if hasattr(self, 'dataset_writer'):
written = self.dataset_writer.flush(write_remainder=True)
if written:
logger.info("Final flush wrote %s samples", written)
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs, args):
if not self.post_init_called:
self.post_init()
self.local_rank = int(os.getenv("RANK", 0))
os.makedirs(args.output_dir, exist_ok=True)
# Create directory for combined data
self.combined_parquet_dir = os.path.join(args.output_dir, "combined_parquet_dataset")
os.makedirs(self.combined_parquet_dir, exist_ok=True)
# Loading dataset
train_dataset = getdataset(args)
self.preprocess_dataloader = DataLoader(
train_dataset,
batch_size=args.preprocess_video_batch_size,
num_workers=args.dataloader_num_workers,
)
self.preprocess_loader_iter = iter(self.preprocess_dataloader)
self.num_processed_samples = 0
# Add progress bar for video preprocessing
self.pbar = tqdm(self.preprocess_loader_iter,
desc="Processing videos",
unit="batch",
disable=self.local_rank != 0)
# Initialize class variables for data sharing
self.video_data: dict[str, Any] = {} # Store video metadata and paths
self.latent_data: dict[str, Any] = {} # Store latent tensors
self.preprocess_action_and_trajectory(fastvideo_args, args)
EntryClass = PreprocessPipeline_MatrixGame_ODE_Trajectory
@@ -12,6 +12,8 @@ from fastvideo.pipelines.preprocess.preprocess_pipeline_ode_trajectory import (P
from fastvideo.pipelines.preprocess.preprocess_pipeline_t2v import (PreprocessPipeline_T2V)
from fastvideo.pipelines.preprocess.preprocess_pipeline_text import (PreprocessPipeline_Text)
from fastvideo.pipelines.preprocess.matrixgame.matrixgame_preprocess_pipeline import (PreprocessPipeline_MatrixGame)
from fastvideo.pipelines.preprocess.matrixgame.matrixgame_preprocess_pipeline_ode_trajectory import (
PreprocessPipeline_MatrixGame_ODE_Trajectory)
from fastvideo.utils import maybe_download_model
logger = init_logger(__name__)
@@ -58,9 +60,11 @@ def main(args) -> None:
PreprocessPipeline = PreprocessPipeline_ODE_Trajectory
elif args.preprocess_task == "matrixgame":
PreprocessPipeline = PreprocessPipeline_MatrixGame
elif args.preprocess_task == "matrixgame_ode_trajectory":
PreprocessPipeline = PreprocessPipeline_MatrixGame_ODE_Trajectory
else:
raise ValueError(f"Invalid preprocess task: {args.preprocess_task}. "
f"Valid options: t2v, i2v, ode_trajectory, text_only, matrixgame")
f"Valid options: t2v, i2v, ode_trajectory, text_only, matrixgame, matrixgame_ode_trajectory")
logger.info("Preprocess task: %s using %s", args.preprocess_task, PreprocessPipeline.__name__)
@@ -96,11 +100,12 @@ if __name__ == "__main__":
parser.add_argument("--group_frame", action="store_true") # TODO
parser.add_argument("--group_resolution", action="store_true") # TODO
parser.add_argument("--flow_shift", type=float, default=None)
parser.add_argument("--preprocess_task",
type=str,
default="t2v",
choices=["t2v", "i2v", "text_only", "ode_trajectory", "matrixgame"],
help="Type of preprocessing task to run")
parser.add_argument(
"--preprocess_task",
type=str,
default="t2v",
choices=["t2v", "i2v", "text_only", "ode_trajectory", "matrixgame", "matrixgame_ode_trajectory"],
help="Type of preprocessing task to run")
parser.add_argument("--train_fps", type=int, default=30)
parser.add_argument("--use_image_num", type=int, default=0)
parser.add_argument("--text_max_length", type=int, default=256)
+41 -2
View File
@@ -35,6 +35,7 @@ class DiffusionForcingSFTMethod(TrainingMethod):
self._chunk_size = self._parse_chunk_size(self.method_config.get("chunk_size", None))
self._timestep_index_range = (self._parse_timestep_index_range())
self._training_weights = self._build_training_weights()
# Initialize preprocessors on student.
self.student.init_preprocessors(self.training_config)
@@ -146,10 +147,24 @@ class DiffusionForcingSFTMethod(TrainingMethod):
sigmas = schedule_sigmas[timestep_indices]
sigmas = sigmas.unsqueeze(-1).unsqueeze(-1).unsqueeze(-1)
pred_x0 = noisy_latents - pred * sigmas
loss = F.mse_loss(pred_x0.float(), clean_latents.float())
per_frame_loss = F.mse_loss(
pred_x0.float(),
clean_latents.float(),
reduction="none",
).mean(dim=(2, 3, 4))
else:
target = noise - clean_latents
loss = F.mse_loss(pred.float(), target.float())
per_frame_loss = F.mse_loss(
pred.float(),
target.float(),
reduction="none",
).mean(dim=(2, 3, 4))
weight = self._get_training_weight(
timestep_indices,
clean_latents.device,
).reshape(batch_size, num_latents)
loss = (per_frame_loss * weight).mean()
attn_metadata = training_batch.attn_metadata_vsa if self._attn_kind == "vsa" else training_batch.attn_metadata
@@ -317,3 +332,27 @@ class DiffusionForcingSFTMethod(TrainingMethod):
)
expanded = chunk_indices.repeat_interleave(chunk_size, dim=1)
return expanded[:, :num_latents]
def _build_training_weights(self) -> torch.Tensor:
"""Gaussian weighting over timestep indices.
Emphasizes mid-noise timesteps, down-weights extremes
(near-clean and pure-noise). Matches Causal-Forcing's
bsmntw weighting scheme.
"""
scheduler = self.student.noise_scheduler
if scheduler is None:
raise ValueError("DFSFT requires student.noise_scheduler")
n = float(len(scheduler.timesteps))
x = torch.arange(n, dtype=torch.float32)
y = torch.exp(-2 * ((x - n / 2) / n)**2)
y_shifted = y - y.min()
return y_shifted * (n / y_shifted.sum())
def _get_training_weight(
self,
timestep_indices: torch.Tensor,
device: torch.device,
) -> torch.Tensor:
"""Look up per-frame gaussian weights by timestep index."""
return self._training_weights.to(device)[timestep_indices]
@@ -0,0 +1,280 @@
# SPDX-License-Identifier: Apache-2.0
"""MatrixGame training model plugin."""
from __future__ import annotations
import copy
from typing import Any, Literal
import torch
from fastvideo.dataset.dataloader.schema import pyarrow_schema_matrixgame
from fastvideo.distributed import (
get_sp_group,
get_world_group,
)
from fastvideo.pipelines import TrainingBatch
from fastvideo.training.training_utils import normalize_dit_input
from fastvideo.train.models.wan.wan import WanModel
from fastvideo.train.utils.dataloader import (
build_parquet_matrixgame_train_dataloader, )
from fastvideo.train.utils.moduleloader import (
load_module_from_path, )
class MatrixGameModel(WanModel):
"""MatrixGame per-role model for finetuning in the new trainer."""
_transformer_cls_name: str = "MatrixGameWanModel"
def init_preprocessors(self, training_config: Any) -> None:
self.vae = load_module_from_path(
model_path=str(training_config.model_path),
module_type="vae",
training_config=training_config,
)
self.world_group = get_world_group()
self.sp_group = get_sp_group()
self._init_timestep_mechanics()
self.dataloader = build_parquet_matrixgame_train_dataloader(
training_config.data,
parquet_schema=pyarrow_schema_matrixgame,
)
self.start_step = 0
def on_train_start(self) -> None:
# MatrixGame finetuning does not use negative text conditioning.
return
def prepare_batch(
self,
raw_batch: dict[str, Any],
*,
generator: torch.Generator,
latents_source: Literal["data", "zeros"] = "data",
) -> TrainingBatch:
assert self.training_config is not None
tc = self.training_config
dtype = self._get_training_dtype()
device = self.device
training_batch = TrainingBatch()
infos = raw_batch.get("info_list")
batch_size = self._infer_batch_size(raw_batch)
if latents_source == "zeros":
latents = self._make_zero_latents(batch_size=batch_size)
elif latents_source == "data":
latents = raw_batch["vae_latent"][:, :, :tc.data.num_latent_t]
latents = latents.to(device=device, dtype=dtype)
else:
raise ValueError(f"Unknown latents_source: {latents_source!r}")
clip_feature = raw_batch["clip_feature"].to(device=device, dtype=dtype)
first_frame_latent = raw_batch["first_frame_latent"]
first_frame_latent = first_frame_latent[:, :, :tc.data.num_latent_t]
first_frame_latent = first_frame_latent.to(device=device, dtype=dtype)
pil_image = raw_batch.get("pil_image")
if pil_image is not None:
pil_image = pil_image.to(device=device)
keyboard_cond = self._get_optional_action(
raw_batch,
key="keyboard_cond",
expected_frames=self._expected_action_frames(tc.data.num_latent_t),
dtype=dtype,
)
mouse_cond = self._get_optional_action(
raw_batch,
key="mouse_cond",
expected_frames=self._expected_action_frames(tc.data.num_latent_t),
dtype=dtype,
)
training_batch.latents = latents
training_batch.encoder_hidden_states = None
training_batch.encoder_attention_mask = None
training_batch.preprocessed_image = pil_image
training_batch.image_embeds = clip_feature
training_batch.image_latents = first_frame_latent
training_batch.keyboard_cond = keyboard_cond
training_batch.mouse_cond = mouse_cond
training_batch.infos = infos
training_batch.latents = normalize_dit_input(
"wan",
training_batch.latents,
self.vae,
)
training_batch = self._prepare_dit_inputs(training_batch, generator)
training_batch = self._build_attention_metadata(training_batch)
training_batch.attn_metadata_vsa = copy.deepcopy(training_batch.attn_metadata)
if training_batch.attn_metadata is not None:
training_batch.attn_metadata.VSA_sparsity = 0.0 # type: ignore[attr-defined]
return training_batch
def _prepare_dit_inputs(
self,
training_batch: TrainingBatch,
generator: torch.Generator,
) -> TrainingBatch:
training_batch = super()._prepare_dit_inputs(training_batch, generator)
image_latents = training_batch.image_latents
image_embeds = training_batch.image_embeds
if image_latents is None or image_embeds is None:
raise RuntimeError("MatrixGame requires image_latents and image_embeds")
cond_latents = self._build_matrixgame_cond_concat(image_latents)
training_batch.image_latents = cond_latents
training_batch.noisy_model_input = torch.cat(
[training_batch.noisy_model_input, cond_latents],
dim=1,
)
training_batch.conditional_dict = {
"encoder_hidden_states": None,
"encoder_attention_mask": None,
"encoder_hidden_states_image": image_embeds,
"keyboard_cond": training_batch.keyboard_cond,
"mouse_cond": training_batch.mouse_cond,
"image_latents": cond_latents,
}
training_batch.unconditional_dict = dict(training_batch.conditional_dict)
return training_batch
def _get_uncond_text_dict(
self,
batch: TrainingBatch,
*,
cfg_uncond: dict[str, Any] | None,
) -> dict[str, Any]:
del cfg_uncond
cond_dict = batch.conditional_dict
if cond_dict is None:
raise RuntimeError("Missing conditional_dict in TrainingBatch")
return batch.unconditional_dict or cond_dict
def _build_distill_input_kwargs(
self,
noise_input: torch.Tensor,
timestep: torch.Tensor,
text_dict: dict[str, Any] | None,
) -> dict[str, Any]:
if text_dict is None:
raise ValueError("text_dict cannot be None for MatrixGame")
hidden_states = noise_input.permute(0, 2, 1, 3, 4)
if hidden_states.shape[1] == 16:
cond_latents = text_dict.get("image_latents")
if cond_latents is None:
raise RuntimeError("MatrixGame requires image_latents in conditional_dict "
"when noise_input has 16 channels")
num_t = hidden_states.shape[2]
cond_latents = cond_latents[:, :, :num_t]
hidden_states = torch.cat([hidden_states, cond_latents], dim=1)
return {
"hidden_states": hidden_states,
"encoder_hidden_states": None,
"timestep": timestep.to(device=self.device, dtype=torch.bfloat16),
"encoder_hidden_states_image": text_dict["encoder_hidden_states_image"],
"keyboard_cond": text_dict["keyboard_cond"],
"mouse_cond": text_dict["mouse_cond"],
"return_dict": False,
}
def _build_matrixgame_cond_concat(
self,
image_latents: torch.Tensor,
) -> torch.Tensor:
if image_latents.ndim != 5:
raise ValueError("first_frame_latent must have shape [B, C, T, H, W], "
f"got {tuple(image_latents.shape)}")
if image_latents.shape[1] == 20:
return image_latents
if image_latents.shape[1] != 16:
raise ValueError("MatrixGame expects first_frame_latent with 16 or 20 channels, "
f"got {image_latents.shape[1]}")
temporal_compression_ratio = self._temporal_compression_ratio()
batch_size, _, num_latent_t, latent_height, latent_width = image_latents.shape
num_frames = (num_latent_t - 1) * temporal_compression_ratio + 1
mask_lat_size = torch.ones(
batch_size,
1,
num_frames,
latent_height,
latent_width,
device=image_latents.device,
dtype=image_latents.dtype,
)
mask_lat_size[:, :, 1:] = 0
first_frame_mask = mask_lat_size[:, :, :1]
first_frame_mask = torch.repeat_interleave(
first_frame_mask,
dim=2,
repeats=temporal_compression_ratio,
)
mask_lat_size = torch.cat(
[first_frame_mask, mask_lat_size[:, :, 1:]],
dim=2,
)
mask_lat_size = mask_lat_size.view(
batch_size,
-1,
temporal_compression_ratio,
latent_height,
latent_width,
).transpose(1, 2)
return torch.cat([mask_lat_size, image_latents], dim=1)
def _get_optional_action(
self,
raw_batch: dict[str, Any],
*,
key: str,
expected_frames: int,
dtype: torch.dtype,
) -> torch.Tensor | None:
value = raw_batch.get(key)
if value is None or value.numel() == 0:
return None
if value.shape[1] < expected_frames:
raise ValueError(f"{key} has {value.shape[1]} frames but requires at least {expected_frames}")
return value[:, :expected_frames].to(device=self.device, dtype=dtype)
def _expected_action_frames(self, num_latent_t: int) -> int:
return (num_latent_t - 1) * self._temporal_compression_ratio() + 1
def _temporal_compression_ratio(self) -> int:
assert self.training_config is not None
return int(self.training_config.pipeline_config.vae_config.arch_config.
temporal_compression_ratio # type: ignore[union-attr]
)
def _infer_batch_size(self, raw_batch: dict[str, Any]) -> int:
if "vae_latent" in raw_batch:
return int(raw_batch["vae_latent"].shape[0])
if "clip_feature" in raw_batch:
return int(raw_batch["clip_feature"].shape[0])
raise ValueError("Unable to infer batch size from MatrixGame batch")
def _make_zero_latents(self, *, batch_size: int) -> torch.Tensor:
assert self.training_config is not None
vae_config = self.training_config.pipeline_config.vae_config.arch_config # type: ignore[union-attr]
num_channels = vae_config.z_dim
spatial_compression_ratio = vae_config.spatial_compression_ratio
latent_height = self.training_config.data.num_height // spatial_compression_ratio
latent_width = self.training_config.data.num_width // spatial_compression_ratio
return torch.zeros(
batch_size,
num_channels,
self.training_config.data.num_latent_t,
latent_height,
latent_width,
device=self.device,
dtype=self._get_training_dtype(),
)
@@ -0,0 +1,436 @@
# SPDX-License-Identifier: Apache-2.0
"""MatrixGame causal training model plugin."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Literal
import torch
from fastvideo.forward_context import set_forward_context
from fastvideo.train.models.matrixgame.matrixgame import MatrixGameModel
from fastvideo.train.models.wan.wan_causal import WanCausalModel
@dataclass(slots=True)
class _StreamingCaches:
kv_cache: list[dict[str, Any]]
kv_cache_mouse: list[dict[str, Any] | None]
kv_cache_keyboard: list[dict[str, Any] | None]
crossattn_cache: list[dict[str, Any]] | None
frame_seq_length: int
local_attn_size: int
sliding_window_num_frames: int
batch_size: int
dtype: torch.dtype
device: torch.device
class MatrixGameCausalModel(MatrixGameModel, WanCausalModel):
"""MatrixGame per-role model with causal/streaming primitives."""
_transformer_cls_name: str = "CausalMatrixGameWanModel"
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._streaming_caches: dict[tuple[int, str], _StreamingCaches] = {}
def clear_caches(self, *, cache_tag: str = "pos") -> None:
self._streaming_caches.pop((id(self), str(cache_tag)), None)
def predict_noise_streaming(
self,
noisy_latents: torch.Tensor,
timestep: torch.Tensor,
batch: Any,
*,
conditional: bool,
cache_tag: str = "pos",
store_kv: bool = False,
cur_start_frame: int = 0,
cfg_uncond: dict[str, Any] | None = None,
attn_kind: Literal["dense", "vsa"] = "dense",
) -> torch.Tensor | None:
del cfg_uncond
if attn_kind == "dense":
attn_metadata = batch.attn_metadata
elif attn_kind == "vsa":
attn_metadata = batch.attn_metadata_vsa
else:
raise ValueError(f"Unknown attn_kind: {attn_kind!r}")
cache_tag = str(cache_tag)
cur_start_frame = int(cur_start_frame)
if cur_start_frame < 0:
raise ValueError("cur_start_frame must be >= 0")
batch_size = int(noisy_latents.shape[0])
num_frames = int(noisy_latents.shape[1])
timestep_full = self._ensure_per_frame_timestep(
timestep=timestep,
batch_size=batch_size,
num_frames=num_frames,
device=noisy_latents.device,
)
transformer = self._get_transformer(timestep_full)
caches = self._get_or_init_streaming_caches(
cache_tag=cache_tag,
transformer=transformer,
noisy_latents=noisy_latents,
)
frame_seq_length = int(caches.frame_seq_length)
kv_cache = caches.kv_cache
kv_cache_mouse = caches.kv_cache_mouse
kv_cache_keyboard = caches.kv_cache_keyboard
crossattn_cache = caches.crossattn_cache
if self._should_snapshot_streaming_cache() and torch.is_grad_enabled():
kv_cache = self._snapshot_kv_cache_indices(kv_cache)
kv_cache_mouse = self._snapshot_action_kv_cache_indices(kv_cache_mouse)
kv_cache_keyboard = self._snapshot_action_kv_cache_indices(kv_cache_keyboard)
if conditional:
cond_dict = batch.conditional_dict
if cond_dict is None:
raise RuntimeError("Missing conditional_dict in TrainingBatch")
else:
cond_dict = batch.unconditional_dict or batch.conditional_dict
if cond_dict is None:
raise RuntimeError("Missing conditional_dict in TrainingBatch")
device_type = self.device.type
dtype = noisy_latents.dtype
with (
torch.autocast(device_type, dtype=dtype),
set_forward_context(
current_timestep=batch.timesteps,
attn_metadata=attn_metadata,
),
):
input_kwargs = self._build_streaming_input_kwargs(
noisy_latents=noisy_latents,
timestep=timestep_full,
cond_dict=cond_dict,
batch=batch,
cur_start_frame=cur_start_frame,
)
input_kwargs["timestep"] = timestep_full.to(
device=self.device,
dtype=torch.long,
)
input_kwargs.update({
"kv_cache": kv_cache,
"kv_cache_mouse": kv_cache_mouse,
"kv_cache_keyboard": kv_cache_keyboard,
"crossattn_cache": crossattn_cache,
"current_start": cur_start_frame * frame_seq_length,
"start_frame": cur_start_frame,
"is_cache": bool(store_kv),
"num_frame_per_block": num_frames,
})
if store_kv:
with torch.no_grad():
_ = transformer(**input_kwargs)
return None
pred_noise = transformer(**input_kwargs).permute(0, 2, 1, 3, 4)
return pred_noise
def _get_or_init_streaming_caches(
self,
*,
cache_tag: str,
transformer: torch.nn.Module,
noisy_latents: torch.Tensor,
) -> _StreamingCaches:
key = (id(self), cache_tag)
cached = self._streaming_caches.get(key)
batch_size = int(noisy_latents.shape[0])
dtype = noisy_latents.dtype
device = noisy_latents.device
frame_seq_length = self._compute_frame_seq_length(
transformer,
noisy_latents,
)
local_attn_size = self._get_local_attn_size(transformer)
sliding_window_num_frames = self._get_sliding_window_num_frames(transformer)
meta = (
frame_seq_length,
local_attn_size,
sliding_window_num_frames,
batch_size,
dtype,
device,
)
if cached is not None:
cached_meta = (
cached.frame_seq_length,
cached.local_attn_size,
cached.sliding_window_num_frames,
cached.batch_size,
cached.dtype,
cached.device,
)
if cached_meta == meta:
return cached
kv_cache = self._initialize_kv_cache(
transformer=transformer,
batch_size=batch_size,
dtype=dtype,
device=device,
frame_seq_length=frame_seq_length,
local_attn_size=local_attn_size,
sliding_window_num_frames=sliding_window_num_frames,
checkpoint_safe=self._should_use_checkpoint_safe_kv_cache(),
)
kv_cache_mouse = self._initialize_action_kv_cache(
transformer=transformer,
batch_size=batch_size,
dtype=dtype,
device=device,
frame_seq_length=frame_seq_length,
local_attn_size=local_attn_size,
channel="mouse",
)
kv_cache_keyboard = self._initialize_action_kv_cache(
transformer=transformer,
batch_size=batch_size,
dtype=dtype,
device=device,
frame_seq_length=frame_seq_length,
local_attn_size=local_attn_size,
channel="keyboard",
)
crossattn_cache = self._initialize_crossattn_cache(
transformer=transformer,
device=device,
)
caches = _StreamingCaches(
kv_cache=kv_cache,
kv_cache_mouse=kv_cache_mouse,
kv_cache_keyboard=kv_cache_keyboard,
crossattn_cache=crossattn_cache,
frame_seq_length=frame_seq_length,
local_attn_size=local_attn_size,
sliding_window_num_frames=sliding_window_num_frames,
batch_size=batch_size,
dtype=dtype,
device=device,
)
self._streaming_caches[key] = caches
return caches
def _build_streaming_input_kwargs(
self,
*,
noisy_latents: torch.Tensor,
timestep: torch.Tensor,
cond_dict: dict[str, Any],
batch: Any,
cur_start_frame: int,
) -> dict[str, Any]:
if batch.image_latents is None:
raise RuntimeError("MatrixGame causal rollout requires image_latents")
if batch.image_embeds is None:
raise RuntimeError("MatrixGame causal rollout requires image_embeds")
num_frames = int(noisy_latents.shape[1])
frame_end = cur_start_frame + num_frames
cond_latents = self._slice_image_latents_for_chunk(
batch.image_latents,
start=cur_start_frame,
end=frame_end,
)
hidden_states = torch.cat(
[noisy_latents.permute(0, 2, 1, 3, 4), cond_latents],
dim=1,
)
return {
"hidden_states": hidden_states,
"encoder_hidden_states": None,
"timestep": timestep,
"encoder_hidden_states_image": cond_dict["encoder_hidden_states_image"],
"keyboard_cond": self._slice_action_prefix(
cond_dict.get("keyboard_cond"),
frame_end=frame_end,
),
"mouse_cond": self._slice_action_prefix(
cond_dict.get("mouse_cond"),
frame_end=frame_end,
),
"return_dict": False,
}
def _slice_image_latents_for_chunk(
self,
image_latents: torch.Tensor,
*,
start: int,
end: int,
) -> torch.Tensor:
num_frames = end - start
if image_latents.ndim != 5:
raise ValueError("image_latents must have shape [B, C, T, H, W], "
f"got {tuple(image_latents.shape)}")
if image_latents.shape[2] >= end:
return image_latents[:, :, start:end]
if image_latents.shape[2] > start:
cond = image_latents[:, :, start:]
pad_frames = num_frames - int(cond.shape[2])
if pad_frames <= 0:
return cond
pad = torch.zeros(
cond.shape[0],
cond.shape[1],
pad_frames,
cond.shape[3],
cond.shape[4],
device=cond.device,
dtype=cond.dtype,
)
return torch.cat([cond, pad], dim=2)
return torch.zeros(
image_latents.shape[0],
image_latents.shape[1],
num_frames,
image_latents.shape[3],
image_latents.shape[4],
device=image_latents.device,
dtype=image_latents.dtype,
)
def _slice_action_prefix(
self,
action: torch.Tensor | None,
*,
frame_end: int,
) -> torch.Tensor | None:
if action is None:
return None
action_frame_end = ((frame_end - 1) * self._temporal_compression_ratio()) + 1
if action.shape[1] < action_frame_end:
raise ValueError("Action tensor is shorter than required for causal rollout: "
f"got={action.shape[1]}, required>={action_frame_end}")
return action[:, :action_frame_end]
def _initialize_action_kv_cache(
self,
*,
transformer: torch.nn.Module,
batch_size: int,
dtype: torch.dtype,
device: torch.device,
frame_seq_length: int,
local_attn_size: int,
channel: Literal["mouse", "keyboard"],
) -> list[dict[str, Any] | None]:
blocks = list(getattr(transformer, "blocks", []))
num_blocks = len(blocks)
if num_blocks <= 0:
return []
action_config = getattr(transformer, "action_config", {}) or {}
action_blocks = {int(block_idx) for block_idx in action_config.get("blocks", [])}
if local_attn_size <= 0:
raise ValueError("MatrixGame causal streaming requires "
"transformer.local_attn_size > 0")
action_heads_num = int(action_config.get("heads_num", 0) or 0)
if action_blocks and action_heads_num <= 0:
raise ValueError("MatrixGame causal action_config.heads_num must be > 0")
if channel == "mouse":
enabled = bool(action_config.get("enable_mouse", False))
hidden_dim = int(action_config.get("mouse_hidden_dim", 0) or 0)
batch_dim = batch_size * frame_seq_length
else:
enabled = bool(action_config.get("enable_keyboard", False))
hidden_dim = int(action_config.get("keyboard_hidden_dim", 0) or 0)
batch_dim = batch_size
caches: list[dict[str, Any] | None] = []
for block_idx in range(num_blocks):
if block_idx not in action_blocks or not enabled:
caches.append(None)
continue
if hidden_dim <= 0 or hidden_dim % action_heads_num != 0:
raise ValueError(f"Invalid {channel} action hidden size for causal "
f"cache initialization: hidden_dim={hidden_dim}, "
f"heads={action_heads_num}")
head_dim = hidden_dim // action_heads_num
caches.append({
"k":
torch.zeros(
[
batch_dim,
local_attn_size,
action_heads_num,
head_dim,
],
dtype=dtype,
device=device,
),
"v":
torch.zeros(
[
batch_dim,
local_attn_size,
action_heads_num,
head_dim,
],
dtype=dtype,
device=device,
),
"global_end_index":
torch.tensor(
[0],
dtype=torch.long,
device=device,
),
"local_end_index":
torch.tensor(
[0],
dtype=torch.long,
device=device,
),
})
return caches
def _snapshot_action_kv_cache_indices(
self,
caches: list[dict[str, Any] | None],
) -> list[dict[str, Any] | None]:
snapshot: list[dict[str, Any] | None] = []
for cache in caches:
if cache is None:
snapshot.append(None)
continue
global_end_index = cache.get("global_end_index")
local_end_index = cache.get("local_end_index")
if (not isinstance(global_end_index, torch.Tensor) or not isinstance(local_end_index, torch.Tensor)):
raise ValueError("Unexpected action kv_cache index tensors; expected "
"tensors at kv_cache_*[*].{global_end_index, "
"local_end_index}")
copied = dict(cache)
copied["global_end_index"] = global_end_index.detach().clone()
copied["local_end_index"] = local_end_index.detach().clone()
snapshot.append(copied)
return snapshot
+23
View File
@@ -31,3 +31,26 @@ def build_parquet_t2v_train_dataloader(
seed=int(data_config.seed or 0),
))
return dataloader
def build_parquet_matrixgame_train_dataloader(
data_config: DataConfig,
*,
parquet_schema: Any,
) -> Any:
"""Build a parquet dataloader for MatrixGame datasets."""
from fastvideo.dataset import (
build_parquet_map_style_dataloader, )
_dataset, dataloader = (build_parquet_map_style_dataloader(
data_config.data_path,
data_config.train_batch_size,
num_data_workers=(data_config.dataloader_num_workers),
parquet_schema=parquet_schema,
cfg_rate=float(data_config.training_cfg_rate or 0.0),
drop_last=True,
text_padding_length=512,
seed=int(data_config.seed or 0),
))
return dataloader
@@ -0,0 +1,451 @@
# SPDX-License-Identifier: Apache-2.0
import sys
from copy import deepcopy
from typing import Any, cast
import torch
import torch.nn.functional as F
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.dataset.dataloader.schema import pyarrow_schema_matrixgame
from fastvideo.distributed import get_local_torch_device
from fastvideo.fastvideo_args import FastVideoArgs, TrainingArgs
from fastvideo.forward_context import set_forward_context
from fastvideo.logger import init_logger
from fastvideo.models.schedulers.scheduling_self_forcing_flow_match import (
SelfForcingFlowMatchScheduler, )
from fastvideo.pipelines.basic.matrixgame.matrixgame_causal_dmd_pipeline import (
MatrixGameCausalDMDPipeline, )
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch, TrainingBatch
from fastvideo.training.training_pipeline import TrainingPipeline
from fastvideo.training.training_utils import (
clip_grad_norm_while_handling_failing_dtensor_cases, )
from fastvideo.utils import shallow_asdict
logger = init_logger(__name__)
class MatrixGameARDiffusionPipeline(TrainingPipeline):
_required_config_modules = ["scheduler", "transformer", "vae"]
def initialize_pipeline(self, fastvideo_args: FastVideoArgs):
scheduler = SelfForcingFlowMatchScheduler(
shift=fastvideo_args.pipeline_config.flow_shift,
sigma_min=0.0,
extra_one_step=True,
)
scheduler.set_timesteps(num_inference_steps=1000, training=True)
self.modules["scheduler"] = scheduler
def _log_training_info(self) -> None:
self.noise_scheduler = self.modules["scheduler"]
super()._log_training_info()
def set_schemas(self):
self.train_dataset_schema = pyarrow_schema_matrixgame
def _get_temporal_compression_ratio(self) -> int:
assert self.training_args is not None
return int(self.training_args.pipeline_config.vae_config.arch_config.temporal_compression_ratio)
def _resolve_num_frame_per_block(self, training_args: TrainingArgs) -> int:
transformer_num_frame_per_block = getattr(self.transformer, "num_frame_per_block", None)
requested_num_frame_per_block = getattr(training_args, "num_frame_per_block", None)
if (transformer_num_frame_per_block is not None and requested_num_frame_per_block is not None
and transformer_num_frame_per_block != requested_num_frame_per_block):
raise ValueError("num_frame_per_block mismatch between loaded transformer and "
"training args: "
f"transformer={transformer_num_frame_per_block}, "
f"training_args={requested_num_frame_per_block}")
if transformer_num_frame_per_block is not None:
return int(transformer_num_frame_per_block)
if requested_num_frame_per_block is not None:
return int(requested_num_frame_per_block)
return 3
def initialize_training_pipeline(self, training_args: TrainingArgs):
super().initialize_training_pipeline(training_args)
self.vae = self.get_module("vae")
self.vae.requires_grad_(False)
self.num_frame_per_block = self._resolve_num_frame_per_block(training_args)
logger.info(
"MatrixGame AR diffusion pipeline initialized with "
"num_frame_per_block=%d, diffusion_forcing_shift=%.1f",
self.num_frame_per_block,
training_args.pipeline_config.flow_shift,
)
def initialize_validation_pipeline(self, training_args: TrainingArgs):
logger.info("Initializing MatrixGame AR validation pipeline...")
args_copy = deepcopy(training_args)
args_copy.inference_mode = True
validation_scheduler = SelfForcingFlowMatchScheduler(
shift=args_copy.pipeline_config.flow_shift,
sigma_min=0.0,
extra_one_step=True,
)
validation_scheduler.set_timesteps(num_inference_steps=1000, training=False)
num_val_steps = int(training_args.validation_sampling_steps.split(",")[0])
step_size = 1000 // num_val_steps
args_copy.pipeline_config.dmd_denoising_steps = list(range(1000, 0, -step_size))
args_copy.pipeline_config.warp_denoising_step = True
training_args.pipeline_config.dmd_denoising_steps = (args_copy.pipeline_config.dmd_denoising_steps)
training_args.pipeline_config.warp_denoising_step = True
logger.info(
"Validation: %d-step MatrixGame causal denoising, "
"dmd_denoising_steps has %d entries",
num_val_steps,
len(args_copy.pipeline_config.dmd_denoising_steps),
)
loaded_modules = {
"transformer": self.get_module("transformer"),
"vae": self.get_module("vae"),
"scheduler": validation_scheduler,
}
image_encoder = self.get_module("image_encoder")
image_processor = self.get_module("image_processor")
if image_encoder is not None:
loaded_modules["image_encoder"] = image_encoder
if image_processor is not None:
loaded_modules["image_processor"] = image_processor
self.validation_pipeline = MatrixGameCausalDMDPipeline.from_pretrained(
training_args.model_path,
args=args_copy,
inference_mode=True,
loaded_modules=loaded_modules,
tp_size=training_args.tp_size,
sp_size=training_args.sp_size,
num_gpus=training_args.num_gpus,
pin_cpu_memory=training_args.pin_cpu_memory,
dit_cpu_offload=True,
)
def _get_timestep(
self,
batch_size: int,
num_frame: int,
num_frame_per_block: int,
) -> torch.Tensor:
"""Sample one schedule index per chunk and broadcast to frames.
Returns the shifted timestep, shape [B, num_frame].
"""
device = get_local_torch_device()
num_schedule_steps = len(self.noise_scheduler.timesteps)
chunk_size = int(num_frame_per_block)
if chunk_size <= 0:
raise ValueError(f"num_frame_per_block must be > 0, got {chunk_size}")
num_chunks = (num_frame + chunk_size - 1) // chunk_size
chunk_indices = torch.randint(
0,
num_schedule_steps,
(batch_size, num_chunks),
device=device,
dtype=torch.long,
)
indices = chunk_indices.repeat_interleave(chunk_size, dim=1)[:, :num_frame]
schedule_t = self.noise_scheduler.timesteps.to(device)
return schedule_t[indices]
def _get_next_batch(self, training_batch: TrainingBatch) -> TrainingBatch:
batch = next(self.train_loader_iter, None) # type: ignore
if batch is None:
self.current_epoch += 1
logger.info("Starting epoch %s", self.current_epoch)
self.train_loader_iter = iter(self.train_dataloader)
batch = next(self.train_loader_iter)
latents = batch["vae_latent"]
latents = latents[:, :, :self.training_args.num_latent_t]
clip_features = batch["clip_feature"]
image_latents = batch["first_frame_latent"]
image_latents = image_latents[:, :, :self.training_args.num_latent_t]
pil_image = batch["pil_image"]
infos = batch["info_list"]
training_batch.latents = latents.to(get_local_torch_device(), dtype=torch.bfloat16)
training_batch.encoder_hidden_states = None
training_batch.encoder_attention_mask = None
training_batch.preprocessed_image = pil_image.to(get_local_torch_device())
training_batch.image_embeds = clip_features.to(get_local_torch_device())
training_batch.image_latents = image_latents.to(get_local_torch_device())
training_batch.infos = infos
if "mouse_cond" in batch and batch["mouse_cond"].numel() > 0:
training_batch.mouse_cond = batch["mouse_cond"].to(get_local_torch_device(), dtype=torch.bfloat16)
else:
training_batch.mouse_cond = None
if "keyboard_cond" in batch and batch["keyboard_cond"].numel() > 0:
training_batch.keyboard_cond = batch["keyboard_cond"].to(get_local_torch_device(), dtype=torch.bfloat16)
else:
training_batch.keyboard_cond = None
temporal_compression_ratio = self._get_temporal_compression_ratio()
expected_num_frames = (self.training_args.num_latent_t - 1) * temporal_compression_ratio + 1
if training_batch.keyboard_cond is not None:
assert training_batch.keyboard_cond.shape[1] >= expected_num_frames, (
f"keyboard_cond has {training_batch.keyboard_cond.shape[1]} "
f"frames but need at least {expected_num_frames}")
training_batch.keyboard_cond = training_batch.keyboard_cond[:, :expected_num_frames]
if training_batch.mouse_cond is not None:
assert training_batch.mouse_cond.shape[1] >= expected_num_frames, (
f"mouse_cond has {training_batch.mouse_cond.shape[1]} frames "
f"but need at least {expected_num_frames}")
training_batch.mouse_cond = training_batch.mouse_cond[:, :expected_num_frames]
return training_batch
def _prepare_dit_inputs(self, training_batch: TrainingBatch) -> TrainingBatch:
"""Prepare diffusion-forcing inputs and MatrixGame I2V concat."""
assert self.training_args is not None
latents = training_batch.latents
assert latents is not None
batch_size = latents.shape[0]
num_latent_t = latents.shape[2]
latents_btchw = latents.permute(0, 2, 1, 3, 4)
timesteps = self._get_timestep(
batch_size=batch_size,
num_frame=num_latent_t,
num_frame_per_block=self.num_frame_per_block,
)
noise_generator = getattr(self, "noise_gen_cuda", None)
noise = torch.randn(
latents_btchw.shape,
generator=noise_generator,
device=latents_btchw.device,
dtype=latents_btchw.dtype,
)
if (self.training_args.sp_size > 1 and self.sp_group is not None and hasattr(self.sp_group, "broadcast")):
self.sp_group.broadcast(timesteps, src=0)
self.sp_group.broadcast(noise, src=0)
noisy_latents = self.noise_scheduler.add_noise(
latents_btchw.flatten(0, 1),
noise.flatten(0, 1),
timesteps.flatten(0, 1),
).unflatten(0, (batch_size, num_latent_t))
noisy_model_input = noisy_latents.permute(0, 2, 1, 3, 4)
assert isinstance(training_batch.image_latents, torch.Tensor)
image_latents = training_batch.image_latents.to(get_local_torch_device(), dtype=torch.bfloat16)
temporal_compression_ratio = self._get_temporal_compression_ratio()
num_frames = (num_latent_t - 1) * temporal_compression_ratio + 1
_, _, _, latent_height, latent_width = image_latents.shape
mask_lat_size = torch.ones(batch_size, 1, num_frames, latent_height, latent_width)
mask_lat_size[:, :, 1:] = 0
first_frame_mask = mask_lat_size[:, :, :1]
first_frame_mask = torch.repeat_interleave(
first_frame_mask,
dim=2,
repeats=temporal_compression_ratio,
)
mask_lat_size = torch.cat([first_frame_mask, mask_lat_size[:, :, 1:]], dim=2)
mask_lat_size = mask_lat_size.view(
batch_size,
-1,
temporal_compression_ratio,
latent_height,
latent_width,
)
mask_lat_size = mask_lat_size.transpose(1, 2)
mask_lat_size = mask_lat_size.to(image_latents.device, dtype=torch.bfloat16)
noisy_model_input = torch.cat([noisy_model_input, mask_lat_size, image_latents], dim=1)
training_target = self.noise_scheduler.training_target(
latents_btchw.flatten(0, 1),
noise.flatten(0, 1),
timesteps.flatten(0, 1),
).unflatten(0, (batch_size, num_latent_t))
training_batch.noisy_model_input = noisy_model_input
training_batch.timesteps = timesteps
training_batch.noise = noise.permute(0, 2, 1, 3, 4)
training_batch.raw_latent_shape = latents.shape
training_batch._ar_training_target = training_target
return training_batch
def _build_input_kwargs(self, training_batch: TrainingBatch) -> TrainingBatch:
image_embeds = training_batch.image_embeds
assert isinstance(image_embeds, torch.Tensor)
assert torch.isnan(image_embeds).sum() == 0
image_embeds = image_embeds.to(get_local_torch_device(), dtype=torch.bfloat16)
timesteps = training_batch.timesteps
assert isinstance(timesteps, torch.Tensor)
assert timesteps.ndim == 2, (f"Expected per-frame timesteps [B, T], got shape {timesteps.shape}")
training_batch.input_kwargs = {
"hidden_states": training_batch.noisy_model_input,
"encoder_hidden_states": None,
"timestep": timesteps.to(get_local_torch_device()),
"encoder_hidden_states_image": image_embeds,
"mouse_cond": training_batch.mouse_cond,
"keyboard_cond": training_batch.keyboard_cond,
"num_frame_per_block": self.num_frame_per_block,
"return_dict": False,
}
return training_batch
def _transformer_forward_and_compute_loss(self, training_batch: TrainingBatch) -> TrainingBatch:
"""
Run transformer forward pass and compute diffusion-forcing loss.
"""
input_kwargs = training_batch.input_kwargs
with set_forward_context(
current_timestep=training_batch.timesteps,
attn_metadata=None,
forward_batch=None,
):
with torch.autocast("cuda", dtype=torch.bfloat16):
model_pred = self.transformer(**input_kwargs)
model_pred_btchw = model_pred.permute(0, 2, 1, 3, 4)
training_target = training_batch._ar_training_target.to(model_pred_btchw.device,
dtype=model_pred_btchw.dtype)
per_frame_loss = F.mse_loss(
model_pred_btchw.float(),
training_target.float(),
reduction="none",
).mean(dim=(2, 3, 4))
timesteps = training_batch.timesteps
weight = self.noise_scheduler.training_weight(timesteps.flatten(0, 1)).to(per_frame_loss.dtype).reshape(
per_frame_loss.shape)
loss = (per_frame_loss * weight).mean()
loss = loss / self.training_args.gradient_accumulation_steps
loss.backward()
avg_loss = loss.detach().clone()
training_batch.total_loss += avg_loss.item()
return training_batch
def train_one_step(self, training_batch: TrainingBatch) -> TrainingBatch:
self.transformer.train()
self.optimizer.zero_grad()
training_batch.total_loss = 0.0
args = cast(TrainingArgs, self.training_args)
for _ in range(args.gradient_accumulation_steps):
training_batch = self._get_next_batch(training_batch)
training_batch = self._normalize_dit_input(training_batch)
training_batch = self._prepare_dit_inputs(training_batch)
training_batch = self._build_input_kwargs(training_batch)
training_batch = self._transformer_forward_and_compute_loss(training_batch)
grad_norm = clip_grad_norm_while_handling_failing_dtensor_cases(
[p for p in self.transformer.parameters() if p.requires_grad],
args.max_grad_norm if args.max_grad_norm is not None else 0.0,
)
self.optimizer.step()
self.lr_scheduler.step()
if grad_norm is None:
grad_value = 0.0
else:
try:
if isinstance(grad_norm, torch.Tensor):
grad_value = float(grad_norm.detach().float().item())
else:
grad_value = float(grad_norm)
except Exception:
grad_value = 0.0
training_batch.grad_norm = grad_value
assert training_batch.latents is not None
training_batch.raw_latent_shape = training_batch.latents.shape
return training_batch
def _prepare_validation_batch(
self,
sampling_param: SamplingParam,
training_args: TrainingArgs,
validation_batch: dict[str, Any],
num_inference_steps: int,
) -> ForwardBatch:
sampling_param.prompt = validation_batch["prompt"]
sampling_param.height = training_args.num_height
sampling_param.width = training_args.num_width
sampling_param.image_path = validation_batch.get("image_path") or validation_batch.get("video_path")
sampling_param.num_inference_steps = num_inference_steps
sampling_param.data_type = "video"
assert self.seed is not None
sampling_param.seed = self.seed
latents_size = [
(sampling_param.num_frames - 1) // 4 + 1,
sampling_param.height // 8,
sampling_param.width // 8,
]
n_tokens = latents_size[0] * latents_size[1] * latents_size[2]
temporal_compression_factor = (training_args.pipeline_config.vae_config.arch_config.temporal_compression_ratio)
num_frames = (training_args.num_latent_t - 1) * temporal_compression_factor + 1
sampling_param.num_frames = num_frames
batch = ForwardBatch(
**shallow_asdict(sampling_param),
latents=None,
generator=torch.Generator(device="cpu").manual_seed(self.seed),
n_tokens=n_tokens,
eta=0.0,
VSA_sparsity=training_args.VSA_sparsity,
)
if "image" in validation_batch and validation_batch["image"] is not None:
batch.pil_image = validation_batch["image"]
if ("keyboard_cond" in validation_batch and validation_batch["keyboard_cond"] is not None):
keyboard_cond = validation_batch["keyboard_cond"]
keyboard_cond = torch.tensor(keyboard_cond, dtype=torch.bfloat16)
keyboard_cond = keyboard_cond.unsqueeze(0)
batch.keyboard_cond = keyboard_cond
if ("mouse_cond" in validation_batch and validation_batch["mouse_cond"] is not None):
mouse_cond = validation_batch["mouse_cond"]
mouse_cond = torch.tensor(mouse_cond, dtype=torch.bfloat16)
mouse_cond = mouse_cond.unsqueeze(0)
batch.mouse_cond = mouse_cond
return batch
def main(args) -> None:
logger.info("Starting MatrixGame AR diffusion training pipeline...")
pipeline = MatrixGameARDiffusionPipeline.from_pretrained(args.pretrained_model_name_or_path, args=args)
args = pipeline.training_args
pipeline.train()
logger.info("MatrixGame AR diffusion training pipeline done")
if __name__ == "__main__":
argv = sys.argv
from fastvideo.fastvideo_args import TrainingArgs
from fastvideo.utils import FlexibleArgumentParser
parser = FlexibleArgumentParser()
parser = TrainingArgs.add_cli_args(parser)
parser = FastVideoArgs.add_cli_args(parser)
args = parser.parse_args()
args.dit_cpu_offload = False
main(args)
@@ -0,0 +1,416 @@
# SPDX-License-Identifier: Apache-2.0
import sys
from copy import deepcopy
from typing import Any, cast
import numpy as np
import torch
import torch.nn.functional as F
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.dataset.dataloader.schema import (pyarrow_schema_matrixgame_ode_trajectory)
from fastvideo.distributed import get_local_torch_device
from fastvideo.fastvideo_args import FastVideoArgs, TrainingArgs
from fastvideo.forward_context import set_forward_context
from fastvideo.logger import init_logger
from fastvideo.models.schedulers.scheduling_self_forcing_flow_match import (SelfForcingFlowMatchScheduler)
from fastvideo.pipelines.basic.matrixgame.matrixgame_causal_dmd_pipeline import (MatrixGameCausalDMDPipeline)
from fastvideo.pipelines.stages.decoding import DecodingStage
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch, TrainingBatch
from fastvideo.training.training_pipeline import TrainingPipeline
from fastvideo.training.training_utils import (clip_grad_norm_while_handling_failing_dtensor_cases)
from fastvideo.utils import shallow_asdict
logger = init_logger(__name__)
class MatrixGameODEInitTrainingPipeline(TrainingPipeline):
"""
Training pipeline for ODE-init using precomputed denoising trajectories.
Supervision: predict the next latent in the stored trajectory by
- feeding current latent at timestep t into the transformer to predict noise
- stepping the scheduler with the predicted noise
- minimizing MSE to the stored next latent at timestep t_next
"""
_required_config_modules = ["scheduler", "transformer", "vae"]
def initialize_pipeline(self, fastvideo_args: FastVideoArgs):
# Match the preprocess/generation scheduler for consistent stepping
self.modules["scheduler"] = SelfForcingFlowMatchScheduler(shift=fastvideo_args.pipeline_config.flow_shift,
sigma_min=0.0,
extra_one_step=True)
self.modules["scheduler"].set_timesteps(num_inference_steps=1000, training=True)
def set_schemas(self):
self.train_dataset_schema = pyarrow_schema_matrixgame_ode_trajectory
def initialize_training_pipeline(self, training_args: TrainingArgs):
super().initialize_training_pipeline(training_args)
self.noise_scheduler = self.get_module("scheduler")
self.vae = self.get_module("vae")
self.vae.requires_grad_(False)
self.timestep_shift = self.training_args.pipeline_config.flow_shift
self.noise_scheduler = SelfForcingFlowMatchScheduler(shift=self.timestep_shift,
sigma_min=0.0,
extra_one_step=True)
self.noise_scheduler.set_timesteps(num_inference_steps=1000, training=True)
self.add_stage(stage_name="decoding_stage", stage=DecodingStage(vae=self.get_module("vae")))
logger.info("dmd_denoising_steps: %s", self.training_args.pipeline_config.dmd_denoising_steps)
self.dmd_denoising_steps = torch.tensor([1000, 750, 500, 250, 0],
dtype=torch.long,
device=get_local_torch_device())
if training_args.warp_denoising_step: # Warp the denoising step according to the scheduler time shift
timesteps = torch.cat((self.noise_scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32))).cuda()
logger.info("timesteps: %s", timesteps)
self.dmd_denoising_steps = timesteps[1000 - self.dmd_denoising_steps]
logger.info("warped self.dmd_denoising_steps: %s", self.dmd_denoising_steps)
else:
raise ValueError("warp_denoising_step must be true")
self.dmd_denoising_steps = self.dmd_denoising_steps.to(get_local_torch_device())
logger.info("denoising_step_list: %s", self.dmd_denoising_steps)
logger.info("Initialized ODE-init training pipeline with %s denoising steps", len(self.dmd_denoising_steps))
# Cache for nearest trajectory index per DMD step (computed lazily on first batch)
self._cached_closest_idx_per_dmd = None
self.num_train_timestep = self.noise_scheduler.num_train_timesteps
self.manual_idx = 0
def initialize_validation_pipeline(self, training_args: TrainingArgs):
logger.info("Initializing validation pipeline...")
args_copy = deepcopy(training_args)
args_copy.inference_mode = True
# Use the same flow-matching scheduler as training for consistent validation.
validation_scheduler = SelfForcingFlowMatchScheduler(shift=args_copy.pipeline_config.flow_shift,
sigma_min=0.0,
extra_one_step=True)
validation_scheduler.set_timesteps(num_inference_steps=1000, training=True)
# Warm start validation with current transformer
self.validation_pipeline = MatrixGameCausalDMDPipeline.from_pretrained(
training_args.model_path,
args=args_copy, # type: ignore
inference_mode=True,
loaded_modules={
"transformer": self.get_module("transformer"),
"vae": self.get_module("vae"),
"scheduler": validation_scheduler,
},
tp_size=training_args.tp_size,
sp_size=training_args.sp_size,
num_gpus=training_args.num_gpus,
pin_cpu_memory=training_args.pin_cpu_memory,
dit_cpu_offload=True)
def _get_next_batch(self, training_batch) -> tuple[TrainingBatch, torch.Tensor, torch.Tensor]:
batch = next(self.train_loader_iter, None) # type: ignore
if batch is None:
self.current_epoch += 1
logger.info("Starting epoch %s", self.current_epoch)
self.train_loader_iter = iter(self.train_dataloader)
batch = next(self.train_loader_iter)
# Required fields from parquet (ODE trajectory schema)
clip_feature = batch['clip_feature']
first_frame_latent = batch['first_frame_latent']
keyboard_cond = batch.get('keyboard_cond', None)
mouse_cond = batch.get('mouse_cond', None)
infos = batch['info_list']
# Trajectory tensors may include a leading singleton batch dim per row
trajectory_latents = batch['trajectory_latents']
if trajectory_latents.dim() == 7:
# [B, 1, S, C, T, H, W] -> [B, S, C, T, H, W]
trajectory_latents = trajectory_latents[:, 0]
elif trajectory_latents.dim() == 6:
# already [B, S, C, T, H, W]
pass
else:
raise ValueError(f"Unexpected trajectory_latents dim: {trajectory_latents.dim()}")
trajectory_timesteps = batch['trajectory_timesteps']
if trajectory_timesteps.dim() == 3:
# [B, 1, S] -> [B, S]
trajectory_timesteps = trajectory_timesteps[:, 0]
elif trajectory_timesteps.dim() == 2:
# [B, S]
pass
else:
raise ValueError(f"Unexpected trajectory_timesteps dim: {trajectory_timesteps.dim()}")
# [B, S, C, T, H, W] -> [B, S, T, C, H, W] to match self-forcing
trajectory_latents = trajectory_latents.permute(0, 1, 3, 2, 4, 5)
# Move to device
device = get_local_torch_device()
training_batch.image_embeds = clip_feature.to(device, dtype=torch.bfloat16)
training_batch.image_latents = first_frame_latent.to(device, dtype=torch.bfloat16)
if keyboard_cond is not None and keyboard_cond.numel() > 0:
training_batch.keyboard_cond = keyboard_cond.to(device, dtype=torch.bfloat16)
else:
training_batch.keyboard_cond = None
if mouse_cond is not None and mouse_cond.numel() > 0:
training_batch.mouse_cond = mouse_cond.to(device, dtype=torch.bfloat16)
else:
training_batch.mouse_cond = None
training_batch.infos = infos
return training_batch, trajectory_latents[:, :, :self.training_args.num_latent_t].to(
device, dtype=torch.bfloat16), trajectory_timesteps.to(device)
def _get_timestep(self,
min_timestep: int,
max_timestep: int,
batch_size: int,
num_frame: int,
num_frame_per_block: int,
uniform_timestep: bool = False) -> torch.Tensor:
if uniform_timestep:
timestep = torch.randint(min_timestep, max_timestep, [batch_size, 1], device=self.device,
dtype=torch.long).repeat(1, num_frame)
return timestep
else:
timestep = torch.randint(min_timestep,
max_timestep, [batch_size, num_frame],
device=self.device,
dtype=torch.long)
# logger.info(f"individual timestep: {timestep}")
# make the noise level the same within every block
timestep = timestep.reshape(timestep.shape[0], -1, num_frame_per_block)
timestep[:, :, 1:] = timestep[:, :, 0:1]
timestep = timestep.reshape(timestep.shape[0], -1)
return timestep
def _step_predict_next_latent(
self, traj_latents: torch.Tensor, traj_timesteps: torch.Tensor, image_embeds: torch.Tensor,
image_latents: torch.Tensor, keyboard_cond: torch.Tensor | None, mouse_cond: torch.Tensor | None
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict[str, torch.Tensor]]:
latent_vis_dict: dict[str, torch.Tensor] = {}
device = get_local_torch_device()
target_latent = traj_latents[:, -1]
# Shapes: traj_latents [B, S, C, T, H, W], traj_timesteps [B, S]
B, S, num_frames, num_channels, height, width = traj_latents.shape
expected_action_frames = 1 + 4 * (num_frames - 1)
if keyboard_cond is None or mouse_cond is None:
raise ValueError("keyboard_cond/mouse_cond must both be provided for action-follow training. "
f"keyboard_cond={None if keyboard_cond is None else tuple(keyboard_cond.shape)}, "
f"mouse_cond={None if mouse_cond is None else tuple(mouse_cond.shape)}")
if keyboard_cond.shape[1] < expected_action_frames:
raise ValueError("keyboard_cond length is shorter than required for latent frames. "
f"got={keyboard_cond.shape[1]}, required>={expected_action_frames}, "
f"num_latent_frames={num_frames}")
if mouse_cond.shape[1] < expected_action_frames:
raise ValueError("mouse_cond length is shorter than required for latent frames. "
f"got={mouse_cond.shape[1]}, required>={expected_action_frames}, "
f"num_latent_frames={num_frames}")
# Lazily cache nearest trajectory index per DMD step based on the (fixed) S timesteps
if self._cached_closest_idx_per_dmd is None:
traj_ts = traj_timesteps[0].float().cpu()
dmd_steps = self.dmd_denoising_steps.float().cpu()
closest_idx = torch.argmin(torch.abs(traj_ts.unsqueeze(0) - dmd_steps.unsqueeze(1)), dim=1)
self._cached_closest_idx_per_dmd = closest_idx.to(torch.long).cpu()
logger.info("self._cached_closest_idx_per_dmd: %s", self._cached_closest_idx_per_dmd)
logger.info("corresponding timesteps: %s", traj_ts[self._cached_closest_idx_per_dmd])
# Select the K indexes from traj_latents using self._cached_closest_idx_per_dmd
# traj_latents: [B, S, C, T, H, W], self._cached_closest_idx_per_dmd: [K]
# Output: [B, K, C, T, H, W]
assert self._cached_closest_idx_per_dmd is not None
relevant_traj_latents = torch.index_select(traj_latents,
dim=1,
index=self._cached_closest_idx_per_dmd.to(traj_latents.device))
indexes = self._get_timestep( # [B, num_frames]
0, len(self.dmd_denoising_steps), B, num_frames, 3, uniform_timestep=False)
noisy_input = torch.gather(relevant_traj_latents,
dim=1,
index=indexes.reshape(B, 1, num_frames, 1, 1,
1).expand(-1, -1, -1, num_channels, height,
width).to(self.device)).squeeze(1)
latent_model_input = noisy_input.permute(0, 2, 1, 3, 4)
if image_latents is not None:
latent_model_input = torch.cat([
latent_model_input,
image_latents.to(latent_model_input.device, latent_model_input.dtype),
],
dim=1)
timestep = self.dmd_denoising_steps[indexes]
# Prepare inputs for transformer
latent_vis_dict["noisy_input"] = noisy_input.permute(0, 2, 1, 3, 4).detach().clone().cpu()
latent_vis_dict["x0"] = target_latent.permute(0, 2, 1, 3, 4).detach().clone().cpu()
latent_model_input = latent_model_input.to(device, dtype=torch.bfloat16)
timestep = timestep.to(device, dtype=torch.bfloat16)
input_kwargs = {
"hidden_states": latent_model_input,
"encoder_hidden_states": None,
"encoder_hidden_states_image": image_embeds,
"timestep": timestep,
"mouse_cond": mouse_cond,
"keyboard_cond": keyboard_cond,
"return_dict": False,
}
# Predict noise and step the scheduler to obtain next latent
with set_forward_context(current_timestep=timestep, attn_metadata=None, forward_batch=None):
noise_pred = self.transformer(**input_kwargs).permute(0, 2, 1, 3, 4)
from fastvideo.models.utils import pred_noise_to_pred_video
pred_video = pred_noise_to_pred_video(pred_noise=noise_pred.flatten(0, 1),
noise_input_latent=noisy_input.flatten(0, 1),
timestep=timestep.to(dtype=torch.bfloat16).flatten(0, 1),
scheduler=self.modules["scheduler"]).unflatten(0, noise_pred.shape[:2])
latent_vis_dict["pred_video"] = pred_video.permute(0, 2, 1, 3, 4).detach().clone().cpu()
return pred_video, target_latent, timestep, latent_vis_dict
def train_one_step(self, training_batch): # type: ignore[override]
self.transformer.train()
self.optimizer.zero_grad()
training_batch.total_loss = 0.0
args = cast(TrainingArgs, self.training_args)
# Using cached nearest index per DMD step; computation happens in _step_predict_next_latent
for _ in range(args.gradient_accumulation_steps):
training_batch, traj_latents, traj_timesteps = self._get_next_batch(training_batch)
image_embeds = training_batch.image_embeds
image_latents = training_batch.image_latents
keyboard_cond = training_batch.keyboard_cond
mouse_cond = training_batch.mouse_cond
assert traj_latents.shape[0] == 1
# Shapes: traj_latents [B, S, C, T, H, W], traj_timesteps [B, S]
_, S = traj_latents.shape[0], traj_latents.shape[1]
if S < 2:
raise ValueError("Trajectory must contain at least 2 steps")
# Forward to predict next latent by stepping scheduler with predicted noise
noise_pred, target_latent, t, latent_vis_dict = self._step_predict_next_latent(
traj_latents, traj_timesteps, image_embeds, image_latents, keyboard_cond, mouse_cond)
training_batch.latent_vis_dict.update(latent_vis_dict)
mask = t != 0
# Compute loss
loss = F.mse_loss(noise_pred[mask], target_latent[mask], reduction="mean")
loss = loss / args.gradient_accumulation_steps
with set_forward_context(current_timestep=t, attn_metadata=None, forward_batch=None):
loss.backward()
avg_loss = loss.detach().clone()
training_batch.total_loss += avg_loss.item()
# Clip grad and step optimizers
grad_norm = clip_grad_norm_while_handling_failing_dtensor_cases(
[p for p in self.transformer.parameters() if p.requires_grad],
args.max_grad_norm if args.max_grad_norm is not None else 0.0)
self.optimizer.step()
self.lr_scheduler.step()
if grad_norm is None:
grad_value = 0.0
else:
try:
if isinstance(grad_norm, torch.Tensor):
grad_value = float(grad_norm.detach().float().item())
else:
grad_value = float(grad_norm)
except Exception:
grad_value = 0.0
training_batch.grad_norm = grad_value
B, S, T, C, H, W = traj_latents.shape
training_batch.raw_latent_shape = (B, C, T, H, W)
return training_batch
def _prepare_validation_batch(self, sampling_param: SamplingParam, training_args: TrainingArgs,
validation_batch: dict[str, Any], num_inference_steps: int) -> ForwardBatch:
sampling_param.prompt = validation_batch['prompt']
sampling_param.height = training_args.num_height
sampling_param.width = training_args.num_width
sampling_param.image_path = validation_batch.get('image_path') or validation_batch.get('video_path')
sampling_param.num_inference_steps = num_inference_steps
sampling_param.data_type = "video"
assert self.seed is not None
sampling_param.seed = self.seed
latents_size = [(sampling_param.num_frames - 1) // 4 + 1, sampling_param.height // 8, sampling_param.width // 8]
n_tokens = latents_size[0] * latents_size[1] * latents_size[2]
temporal_compression_factor = training_args.pipeline_config.vae_config.arch_config.temporal_compression_ratio
num_frames = (training_args.num_latent_t - 1) * temporal_compression_factor + 1
sampling_param.num_frames = num_frames
batch = ForwardBatch(
**shallow_asdict(sampling_param),
latents=None,
generator=torch.Generator(device="cpu").manual_seed(self.seed),
n_tokens=n_tokens,
eta=0.0,
VSA_sparsity=training_args.VSA_sparsity,
)
if "image" in validation_batch and validation_batch["image"] is not None:
batch.pil_image = validation_batch["image"]
if "keyboard_cond" in validation_batch and validation_batch["keyboard_cond"] is not None:
keyboard_cond = validation_batch["keyboard_cond"]
keyboard_cond = torch.tensor(keyboard_cond, dtype=torch.bfloat16)
keyboard_cond = keyboard_cond.unsqueeze(0)
batch.keyboard_cond = keyboard_cond
if "mouse_cond" in validation_batch and validation_batch["mouse_cond"] is not None:
mouse_cond = validation_batch["mouse_cond"]
mouse_cond = torch.tensor(mouse_cond, dtype=torch.bfloat16)
mouse_cond = mouse_cond.unsqueeze(0)
batch.mouse_cond = mouse_cond
return batch
def visualize_intermediate_latents(self, training_batch: TrainingBatch, training_args: TrainingArgs, step: int):
tracker_loss_dict: dict[str, Any] = {}
latents_vis_dict = training_batch.latent_vis_dict
latent_log_keys = ['noisy_input', 'x0', 'pred_video']
for latent_key in latent_log_keys:
assert latent_key in latents_vis_dict and latents_vis_dict[latent_key] is not None
latent = latents_vis_dict[latent_key]
pixel_latent = self.decoding_stage.decode(latent, training_args)
video = pixel_latent.cpu().float()
video = video.permute(0, 2, 1, 3, 4)
video = (video * 255).numpy().astype(np.uint8)
video_artifact = self.tracker.video(video, fps=16, format="mp4") # change to 16 for Wan2.1
if video_artifact is not None:
tracker_loss_dict[latent_key] = video_artifact
# Clean up references
del video, pixel_latent, latent
if self.global_rank == 0 and tracker_loss_dict:
self.tracker.log_artifacts(tracker_loss_dict, step)
def main(args) -> None:
logger.info("Starting ODE-init training pipeline...")
pipeline = MatrixGameODEInitTrainingPipeline.from_pretrained(args.pretrained_model_name_or_path, args=args)
args = pipeline.training_args
pipeline.train()
logger.info("ODE-init training pipeline done")
if __name__ == "__main__":
argv = sys.argv
from fastvideo.fastvideo_args import TrainingArgs
from fastvideo.utils import FlexibleArgumentParser
parser = FlexibleArgumentParser()
parser = TrainingArgs.add_cli_args(parser)
parser = FastVideoArgs.add_cli_args(parser)
args = parser.parse_args()
args.dit_cpu_offload = False
main(args)
@@ -0,0 +1,873 @@
# SPDX-License-Identifier: Apache-2.0
import sys
from copy import deepcopy
from typing import Any, cast
import torch
import torch.distributed as dist
import torch.nn.functional as F
from einops import rearrange
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.dataset.dataloader.schema import (pyarrow_schema_matrixgame)
from fastvideo.distributed import get_local_torch_device
from fastvideo.fastvideo_args import FastVideoArgs, TrainingArgs
from fastvideo.forward_context import set_forward_context
from fastvideo.logger import init_logger
from fastvideo.models.schedulers.scheduling_self_forcing_flow_match import (SelfForcingFlowMatchScheduler)
from fastvideo.models.utils import pred_noise_to_pred_video
from fastvideo.pipelines import ComposedPipelineBase
from fastvideo.pipelines.basic.matrixgame.matrixgame_causal_dmd_pipeline import (MatrixGameCausalDMDPipeline)
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch, TrainingBatch
from fastvideo.training.self_forcing_distillation_pipeline import (SelfForcingDistillationPipeline)
from fastvideo.training.training_utils import shift_timestep
from fastvideo.utils import is_vsa_available, shallow_asdict
vsa_available = is_vsa_available()
logger = init_logger(__name__)
class MatrixGameSelfForcingDistillationPipeline(SelfForcingDistillationPipeline):
"""
A self-forcing distillation pipeline for MatrixGame that uses the self-forcing methodology
with DMD for video generation.
"""
_required_config_modules = [
"scheduler",
"transformer",
"vae",
"image_encoder",
"image_processor",
]
def set_schemas(self):
self.train_dataset_schema = pyarrow_schema_matrixgame
def load_modules(
self,
fastvideo_args: FastVideoArgs,
loaded_modules: dict[str, torch.nn.Module] | None = None,
) -> dict[str, Any]:
modules = ComposedPipelineBase.load_modules(
self,
fastvideo_args,
loaded_modules,
)
training_args = cast(TrainingArgs, fastvideo_args)
old_override = training_args.override_transformer_cls_name
training_args.override_transformer_cls_name = "MatrixGameWanModel"
try:
if loaded_modules is not None and "real_score_transformer" in loaded_modules:
self.real_score_transformer = loaded_modules["real_score_transformer"]
elif training_args.real_score_model_path:
logger.info(
"Loading real score transformer from: %s",
training_args.real_score_model_path,
)
self.real_score_transformer = self.load_module_from_path(
training_args.real_score_model_path,
"transformer",
training_args,
)
else:
raise ValueError("real_score_model_path is required for DMD distillation pipeline")
modules["real_score_transformer"] = self.real_score_transformer
if loaded_modules is not None and "fake_score_transformer" in loaded_modules:
self.fake_score_transformer = loaded_modules["fake_score_transformer"]
elif training_args.fake_score_model_path:
logger.info(
"Loading fake score transformer from: %s",
training_args.fake_score_model_path,
)
self.fake_score_transformer = self.load_module_from_path(
training_args.fake_score_model_path,
"transformer",
training_args,
)
else:
raise ValueError("fake_score_model_path is required for DMD distillation pipeline")
modules["fake_score_transformer"] = self.fake_score_transformer
finally:
training_args.override_transformer_cls_name = old_override
self.real_score_transformer_2 = None
self.fake_score_transformer_2 = None
return modules
def _build_matrixgame_cond_concat(
self,
image_latents: torch.Tensor,
) -> torch.Tensor:
if image_latents.ndim != 5:
raise ValueError("first_frame_latent must have shape [B, C, T, H, W], got "
f"{tuple(image_latents.shape)}")
if image_latents.shape[1] != 16:
raise ValueError("MatrixGame cond conversion expects a 16-channel "
f"first_frame_latent, got {image_latents.shape[1]} channels.")
temporal_compression_ratio = (
self.training_args.pipeline_config.vae_config.arch_config.temporal_compression_ratio)
num_latent_t = image_latents.shape[2]
num_frames = ((num_latent_t - 1) * temporal_compression_ratio + 1)
batch_size, _, _, latent_height, latent_width = image_latents.shape
mask_lat_size = torch.ones(
batch_size,
1,
num_frames,
latent_height,
latent_width,
device=image_latents.device,
dtype=image_latents.dtype,
)
mask_lat_size[:, :, 1:] = 0
first_frame_mask = mask_lat_size[:, :, :1]
first_frame_mask = torch.repeat_interleave(
first_frame_mask,
dim=2,
repeats=temporal_compression_ratio,
)
mask_lat_size = torch.cat(
[first_frame_mask, mask_lat_size[:, :, 1:]],
dim=2,
)
mask_lat_size = mask_lat_size.view(
batch_size,
-1,
temporal_compression_ratio,
latent_height,
latent_width,
)
mask_lat_size = mask_lat_size.transpose(1, 2).to(
image_latents.device,
dtype=image_latents.dtype,
)
return torch.cat([mask_lat_size, image_latents], dim=1)
def _initialize_simulation_caches(
self,
batch_size: int,
dtype: torch.dtype,
device: torch.device,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any] | None], list[dict[str, Any] | None]]:
"""Initialize KV cache and cross-attention cache for multi-step simulation."""
num_transformer_blocks = len(self.transformer.blocks)
latent_shape = self.video_latent_shape_sp
_, num_frames, _, height, width = latent_shape
_, p_h, p_w = self.transformer.patch_size
post_patch_height = height // p_h
post_patch_width = width // p_w
frame_seq_length = post_patch_height * post_patch_width
self.frame_seq_length = frame_seq_length
# Get model configuration parameters - handle FSDP wrapping
num_attention_heads = getattr(self.transformer, 'num_attention_heads', None)
attention_head_dim = getattr(self.transformer, 'attention_head_dim', None)
# 1 CLS token + 256 patch tokens = 257
text_len = 257
action_config = getattr(self.transformer, 'action_config', {})
action_blocks = action_config.get('blocks', []) if action_config else []
local_attn_size = getattr(self.transformer, "local_attn_size",
action_config.get('local_attn_size', 6) if action_config else 6)
if local_attn_size <= 0:
raise ValueError("MatrixGame self-forcing requires transformer.local_attn_size > 0")
kv_cache_size = local_attn_size * frame_seq_length
kv_cache = []
for _ in range(num_transformer_blocks):
kv_cache.append({
"k":
torch.zeros([batch_size, kv_cache_size, num_attention_heads, attention_head_dim],
dtype=dtype,
device=device),
"v":
torch.zeros([batch_size, kv_cache_size, num_attention_heads, attention_head_dim],
dtype=dtype,
device=device),
"global_end_index":
torch.tensor([0], dtype=torch.long, device=device),
"local_end_index":
torch.tensor([0], dtype=torch.long, device=device)
})
# Initialize cross-attention cache
crossattn_cache = []
for _ in range(num_transformer_blocks):
crossattn_cache.append({
"k":
torch.zeros([batch_size, text_len, num_attention_heads, attention_head_dim], dtype=dtype,
device=device),
"v":
torch.zeros([batch_size, text_len, num_attention_heads, attention_head_dim], dtype=dtype,
device=device),
"is_init":
False
})
# Initialize action module KV caches
action_heads_num = action_config.get('heads_num', 16) if action_config else 16
mouse_hidden_dim = action_config.get('mouse_hidden_dim', 1024) if action_config else 1024
keyboard_hidden_dim = action_config.get('keyboard_hidden_dim', 1024) if action_config else 1024
mouse_head_dim = mouse_hidden_dim // action_heads_num
keyboard_head_dim = keyboard_hidden_dim // action_heads_num
kv_cache_mouse: list[dict[str, Any] | None] = []
kv_cache_keyboard: list[dict[str, Any] | None] = []
for block_idx in range(num_transformer_blocks):
if block_idx in action_blocks:
kv_cache_mouse.append({
"k":
torch.zeros([batch_size * frame_seq_length, local_attn_size, action_heads_num, mouse_head_dim],
dtype=dtype,
device=device),
"v":
torch.zeros([batch_size * frame_seq_length, local_attn_size, action_heads_num, mouse_head_dim],
dtype=dtype,
device=device),
"global_end_index":
torch.tensor([0], dtype=torch.long, device=device),
"local_end_index":
torch.tensor([0], dtype=torch.long, device=device)
})
kv_cache_keyboard.append({
"k":
torch.zeros([batch_size, local_attn_size, action_heads_num, keyboard_head_dim],
dtype=dtype,
device=device),
"v":
torch.zeros([batch_size, local_attn_size, action_heads_num, keyboard_head_dim],
dtype=dtype,
device=device),
"global_end_index":
torch.tensor([0], dtype=torch.long, device=device),
"local_end_index":
torch.tensor([0], dtype=torch.long, device=device)
})
else:
kv_cache_mouse.append(None)
kv_cache_keyboard.append(None)
return kv_cache, crossattn_cache, kv_cache_mouse, kv_cache_keyboard
def _reset_simulation_caches(self, kv_cache: list[dict[str, Any]], crossattn_cache: list[dict[str, Any]],
kv_cache_mouse: list[dict[str, Any]
| None] | None, kv_cache_keyboard: list[dict[str, Any]
| None] | None) -> None:
"""Reset KV cache, cross-attention cache, and action caches to clean state."""
if kv_cache is not None:
for cache_dict in kv_cache:
cache_dict["global_end_index"].fill_(0)
cache_dict["local_end_index"].fill_(0)
cache_dict["k"].zero_()
cache_dict["v"].zero_()
if crossattn_cache is not None:
for cache_dict in crossattn_cache:
cache_dict["is_init"] = False
cache_dict["k"].zero_()
cache_dict["v"].zero_()
for opt_caches in (kv_cache_mouse, kv_cache_keyboard):
if opt_caches is None:
continue
for opt_cache in opt_caches:
if opt_cache is None:
continue
opt_cache["global_end_index"].fill_(0)
opt_cache["local_end_index"].fill_(0)
opt_cache["k"].zero_()
opt_cache["v"].zero_()
def _generator_multi_step_simulation_forward(self,
training_batch: TrainingBatch,
return_sim_steps: bool = False) -> torch.Tensor:
"""Forward pass through student transformer matching inference procedure with KV cache management.
This function is adapted from the reference self-forcing implementation's inference_with_trajectory
and includes gradient masking logic for dynamic frame generation.
"""
latents = training_batch.latents
dtype = latents.dtype
batch_size = latents.shape[0]
num_training_frames = getattr(self.training_args, 'num_latent_t', 21)
min_num_frames = 20 if self.independent_first_frame else 21
max_num_frames = num_training_frames - 1 if self.independent_first_frame else num_training_frames
assert max_num_frames % self.num_frame_per_block == 0
assert min_num_frames % self.num_frame_per_block == 0
max_num_blocks = max_num_frames // self.num_frame_per_block
min_num_blocks = min_num_frames // self.num_frame_per_block
# Sample number of blocks and sync across processes
num_generated_blocks = torch.randint(min_num_blocks, max_num_blocks + 1, (1, ), device=self.device)
if dist.is_initialized():
dist.broadcast(num_generated_blocks, src=0)
num_generated_blocks = num_generated_blocks.item()
num_generated_frames = num_generated_blocks * self.num_frame_per_block
if self.independent_first_frame:
num_generated_frames += 1
min_num_frames += 1
noise_shape = [batch_size, num_generated_frames, *self.video_latent_shape[2:]]
noise = torch.randn(noise_shape, device=self.device, dtype=dtype)
if self.sp_world_size > 1:
noise = rearrange(noise, "b (n t) c h w -> b n t c h w", n=self.sp_world_size).contiguous()
noise = noise[:, self.rank_in_sp_group, :, :, :, :]
batch_size, num_frames, num_channels, height, width = noise.shape
if self.independent_first_frame:
assert (num_frames - 1) % self.num_frame_per_block == 0
num_blocks = (num_frames - 1) // self.num_frame_per_block
else:
assert num_frames % self.num_frame_per_block == 0
num_blocks = num_frames // self.num_frame_per_block
num_output_frames = num_frames
output = torch.zeros([batch_size, num_output_frames, num_channels, height, width],
device=noise.device,
dtype=noise.dtype)
# Step 1: Initialize KV cache to all zeros
(self.kv_cache1, self.crossattn_cache, self.kv_cache_mouse,
self.kv_cache_keyboard) = self._initialize_simulation_caches(batch_size, dtype, self.device)
# Step 2: Temporal denoising loop
current_start_frame = 0
all_num_frames = [self.num_frame_per_block] * num_blocks
if self.independent_first_frame:
all_num_frames = [1] + all_num_frames
num_denoising_steps = len(self.denoising_step_list)
exit_flags = self.generate_and_sync_list(len(all_num_frames), num_denoising_steps, device=noise.device)
start_gradient_frame_index = max(0, num_output_frames - 21)
for block_index, current_num_frames in enumerate(all_num_frames):
noisy_input = noise[:, current_start_frame:current_start_frame + current_num_frames]
# Step 3.1: Spatial denoising loop
for index, current_timestep in enumerate(self.denoising_step_list):
exit_flag = (index == exit_flags[0]) if self.same_step_across_blocks else (index
== exit_flags[block_index])
timestep = torch.ones([batch_size, current_num_frames], device=noise.device,
dtype=torch.int64) * current_timestep
if self.boundary_timestep is not None and current_timestep < self.boundary_timestep and self.transformer_2 is not None:
current_model = self.transformer_2
else:
current_model = self.transformer
if not exit_flag:
with torch.no_grad():
# Build input kwargs
training_batch_temp = self._build_distill_input_kwargs(noisy_input,
timestep,
training_batch.conditional_dict,
training_batch,
frame_start=current_start_frame,
frame_end=current_start_frame +
current_num_frames,
num_frame_per_block=current_num_frames)
pred_flow = current_model(**training_batch_temp.input_kwargs,
kv_cache=self.kv_cache1,
kv_cache_mouse=self.kv_cache_mouse,
kv_cache_keyboard=self.kv_cache_keyboard,
crossattn_cache=self.crossattn_cache,
current_start=current_start_frame * self.frame_seq_length,
start_frame=current_start_frame).permute(0, 2, 1, 3, 4)
denoised_pred = pred_noise_to_pred_video(pred_noise=pred_flow.flatten(0, 1),
noise_input_latent=noisy_input.flatten(0, 1),
timestep=timestep,
scheduler=self.noise_scheduler).unflatten(
0, pred_flow.shape[:2])
next_timestep = self.denoising_step_list[index + 1]
noisy_input = self.noise_scheduler.add_noise(
denoised_pred.flatten(0, 1), torch.randn_like(denoised_pred.flatten(0, 1)),
next_timestep * torch.ones(
[batch_size * current_num_frames], device=noise.device, dtype=torch.long)).unflatten(
0, denoised_pred.shape[:2])
else:
# Final prediction with gradient control
if current_start_frame < start_gradient_frame_index:
with torch.no_grad():
training_batch_temp = self._build_distill_input_kwargs(
noisy_input,
timestep,
training_batch.conditional_dict,
training_batch,
frame_start=current_start_frame,
frame_end=current_start_frame + current_num_frames,
num_frame_per_block=current_num_frames)
pred_flow = current_model(**training_batch_temp.input_kwargs,
kv_cache=self.kv_cache1,
kv_cache_mouse=self.kv_cache_mouse,
kv_cache_keyboard=self.kv_cache_keyboard,
crossattn_cache=self.crossattn_cache,
current_start=current_start_frame * self.frame_seq_length,
start_frame=current_start_frame).permute(0, 2, 1, 3, 4)
else:
training_batch_temp = self._build_distill_input_kwargs(noisy_input,
timestep,
training_batch.conditional_dict,
training_batch,
frame_start=current_start_frame,
frame_end=current_start_frame +
current_num_frames,
num_frame_per_block=current_num_frames)
pred_flow = current_model(**training_batch_temp.input_kwargs,
kv_cache=self.kv_cache1,
kv_cache_mouse=self.kv_cache_mouse,
kv_cache_keyboard=self.kv_cache_keyboard,
crossattn_cache=self.crossattn_cache,
current_start=current_start_frame * self.frame_seq_length,
start_frame=current_start_frame).permute(0, 2, 1, 3, 4)
denoised_pred = pred_noise_to_pred_video(pred_noise=pred_flow.flatten(0, 1),
noise_input_latent=noisy_input.flatten(0, 1),
timestep=timestep,
scheduler=self.noise_scheduler).unflatten(
0, pred_flow.shape[:2])
break
# Step 3.2: record the model's output
output[:, current_start_frame:current_start_frame + current_num_frames] = denoised_pred
# Step 3.3: rerun with timestep zero to update the cache
context_timestep = torch.ones_like(timestep) * self.context_noise
denoised_pred = self.noise_scheduler.add_noise(denoised_pred.flatten(0, 1),
torch.randn_like(denoised_pred.flatten(0, 1)),
context_timestep).unflatten(0, denoised_pred.shape[:2])
with torch.no_grad():
training_batch_temp = self._build_distill_input_kwargs(denoised_pred,
context_timestep,
training_batch.conditional_dict,
training_batch,
frame_start=current_start_frame,
frame_end=current_start_frame +
current_num_frames,
num_frame_per_block=current_num_frames)
# context_timestep is 0 so we use transformer_2
current_model = self.transformer_2 if self.transformer_2 is not None else self.transformer
current_model(**training_batch_temp.input_kwargs,
kv_cache=self.kv_cache1,
kv_cache_mouse=self.kv_cache_mouse,
kv_cache_keyboard=self.kv_cache_keyboard,
crossattn_cache=self.crossattn_cache,
current_start=current_start_frame * self.frame_seq_length,
start_frame=current_start_frame)
# Step 3.4: update the start and end frame indices
current_start_frame += current_num_frames
# Handle last 21 frames logic
pred_image_or_video = output
# Slice last 21 frames if we generated more
gradient_mask = None
if pred_image_or_video.shape[1] > 21:
with torch.no_grad():
# Re-encode to get image latent
latent_to_decode = pred_image_or_video[:, :-20, ...]
# Decode to video
latent_to_decode = latent_to_decode.permute(0, 2, 1, 3, 4) # [B, C, F, H, W]
# Apply VAE scaling and shift factors
if isinstance(self.vae.scaling_factor, torch.Tensor):
latent_to_decode = latent_to_decode / self.vae.scaling_factor.to(
latent_to_decode.device, latent_to_decode.dtype)
else:
latent_to_decode = latent_to_decode / self.vae.scaling_factor
if hasattr(self.vae, "shift_factor") and self.vae.shift_factor is not None:
if isinstance(self.vae.shift_factor, torch.Tensor):
latent_to_decode += self.vae.shift_factor.to(latent_to_decode.device, latent_to_decode.dtype)
else:
latent_to_decode += self.vae.shift_factor
# Decode to pixels
pixels = self.vae.decode(latent_to_decode)
frame = pixels[:, :, -1:, :, :].to(dtype) # Last frame [B, C, 1, H, W]
# Encode frame back to get image latent
image_latent = self.vae.encode(frame).to(dtype)
image_latent = image_latent.permute(0, 2, 1, 3, 4) # [B, F, C, H, W]
pred_image_or_video_last_21 = torch.cat([image_latent, pred_image_or_video[:, -20:, ...]], dim=1)
else:
pred_image_or_video_last_21 = pred_image_or_video
# Set up gradient mask if we generated more than minimum frames
if num_generated_frames != min_num_frames:
# Currently, we do not use gradient for the first chunk, since it contains image latents
gradient_mask = torch.ones_like(pred_image_or_video_last_21, dtype=torch.bool)
if self.independent_first_frame:
gradient_mask[:, :1] = False
else:
gradient_mask[:, :self.num_frame_per_block] = False
# Apply gradient masking if needed
final_output = pred_image_or_video_last_21.to(dtype)
if gradient_mask is not None:
# Apply gradient masking: detach frames that shouldn't contribute gradients
final_output = torch.where(
gradient_mask,
pred_image_or_video_last_21, # Keep original values where gradient_mask is True
pred_image_or_video_last_21.detach() # Detach where gradient_mask is False
)
# Store visualization data
training_batch.dmd_latent_vis_dict["generator_timestep"] = torch.as_tensor(
self.denoising_step_list[exit_flags[0]],
dtype=torch.float32,
device=self.device,
).detach().clone()
# Store gradient mask information for debugging
if gradient_mask is not None:
training_batch.dmd_latent_vis_dict["gradient_mask"] = gradient_mask.float()
training_batch.dmd_latent_vis_dict["num_generated_frames"] = torch.tensor(num_generated_frames,
dtype=torch.float32,
device=self.device)
training_batch.dmd_latent_vis_dict["min_num_frames"] = torch.tensor(min_num_frames,
dtype=torch.float32,
device=self.device)
# Clean up caches
assert self.kv_cache1 is not None
assert self.crossattn_cache is not None
self._reset_simulation_caches(self.kv_cache1, self.crossattn_cache, self.kv_cache_mouse, self.kv_cache_keyboard)
return final_output if gradient_mask is not None else pred_image_or_video
def initialize_validation_pipeline(self, training_args: TrainingArgs):
logger.info("Initializing validation pipeline...")
args_copy = deepcopy(training_args)
args_copy.inference_mode = True
# Use the same flow-matching scheduler as training for consistent validation.
validation_scheduler = SelfForcingFlowMatchScheduler(shift=args_copy.pipeline_config.flow_shift,
sigma_min=0.0,
extra_one_step=True)
validation_scheduler.set_timesteps(num_inference_steps=1000, training=True)
# Warm start validation with current transformer
self.validation_pipeline = MatrixGameCausalDMDPipeline.from_pretrained(
training_args.model_path,
args=args_copy, # type: ignore
inference_mode=True,
loaded_modules={
"transformer": self.get_module("transformer"),
"vae": self.get_module("vae"),
"scheduler": validation_scheduler,
},
tp_size=training_args.tp_size,
sp_size=training_args.sp_size,
num_gpus=training_args.num_gpus,
pin_cpu_memory=training_args.pin_cpu_memory,
dit_cpu_offload=True)
if not hasattr(self.validation_pipeline, "prompt_encoding_stage"):
# validation expects a prompt stage
def _prompt_encoding_stage(batch: ForwardBatch, _args: TrainingArgs) -> ForwardBatch:
if not batch.prompt_embeds:
batch.prompt_embeds = [None]
if batch.prompt_attention_mask is None or not batch.prompt_attention_mask:
batch.prompt_attention_mask = [None]
return batch
self.validation_pipeline.prompt_encoding_stage = _prompt_encoding_stage # type: ignore[attr-defined]
def _get_next_batch(self, training_batch: TrainingBatch) -> TrainingBatch:
batch = next(self.train_loader_iter, None) # type: ignore
if batch is None:
self.current_epoch += 1
# Reset iterator for next epoch
self.train_loader_iter = iter(self.train_dataloader)
# Get first batch of new epoch
batch = next(self.train_loader_iter)
clip_feature = batch['clip_feature']
first_frame_latent = batch['first_frame_latent']
keyboard_cond = batch.get('keyboard_cond', None)
mouse_cond = batch.get('mouse_cond', None)
infos = batch['info_list']
batch_size = clip_feature.shape[0]
vae_config = self.training_args.pipeline_config.vae_config.arch_config
num_channels = vae_config.z_dim
spatial_compression_ratio = vae_config.spatial_compression_ratio
latent_height = self.training_args.num_height // spatial_compression_ratio
latent_width = self.training_args.num_width // spatial_compression_ratio
latents = torch.randn(batch_size, num_channels, self.training_args.num_latent_t, latent_height,
latent_width).to(get_local_torch_device(), dtype=torch.bfloat16)
training_batch.latents = latents.to(get_local_torch_device(), dtype=torch.bfloat16)
training_batch.encoder_hidden_states = None
training_batch.encoder_attention_mask = None
training_batch.image_embeds = clip_feature.to(get_local_torch_device(), dtype=torch.bfloat16)
training_batch.image_latents = first_frame_latent.to(get_local_torch_device(), dtype=torch.bfloat16)
# Action conditioning
if keyboard_cond is not None and keyboard_cond.numel() > 0:
keyboard_cond_full = keyboard_cond.to(get_local_torch_device(), dtype=torch.bfloat16)
training_batch.keyboard_cond = keyboard_cond_full # For Teacher/Critic (dim=6)
else:
training_batch.keyboard_cond = None
if mouse_cond is not None and mouse_cond.numel() > 0:
training_batch.mouse_cond = mouse_cond.to(get_local_torch_device(), dtype=torch.bfloat16)
else:
training_batch.mouse_cond = None
training_batch.infos = infos
return training_batch
def _prepare_dit_inputs(self, training_batch: TrainingBatch) -> TrainingBatch:
"""Override to properly handle I2V concatenation - call parent first, then concatenate image conditioning."""
# First, call parent method to prepare noise, timesteps, etc. for video latents
training_batch = super()._prepare_dit_inputs(training_batch)
assert isinstance(training_batch.image_latents, torch.Tensor)
image_latents = training_batch.image_latents.to(get_local_torch_device(), dtype=torch.bfloat16)
# cond_concat = [mask(4), image_latent(16)] with 20 channels.
expected_cond_channels = 20
if image_latents.shape[1] == 16:
image_latents = self._build_matrixgame_cond_concat(image_latents)
elif image_latents.shape[1] != expected_cond_channels:
raise ValueError("Unexpected first_frame_latent channels, "
"Expected {expected_cond_channels} (cond_concat), got {image_latents.shape[1]}.")
if self.sp_world_size > 1:
total_frames = image_latents.shape[2]
# Split cond latents to local SP shard only when tensor is still global.
if total_frames == self.training_args.num_latent_t:
if total_frames % self.sp_world_size != 0:
raise ValueError("image_latents temporal dim is not divisible by SP world size: "
f"frames={total_frames}, sp_world_size={self.sp_world_size}")
image_latents = rearrange(image_latents, "b c (n t) h w -> b c n t h w",
n=self.sp_world_size).contiguous()
image_latents = image_latents[:, :, self.rank_in_sp_group, :, :, :]
training_batch.image_latents = image_latents
return training_batch
def _build_distill_input_kwargs(self,
noise_input: torch.Tensor,
timestep: torch.Tensor,
text_dict: dict[str, torch.Tensor] | None,
training_batch: TrainingBatch,
frame_start: int | None = None,
frame_end: int | None = None,
num_frame_per_block: int | None = None) -> TrainingBatch:
# Image Embeds for conditioning
image_embeds = training_batch.image_embeds
assert image_embeds is not None
assert torch.isnan(image_embeds).sum() == 0
image_embeds = image_embeds.to(get_local_torch_device(), dtype=torch.bfloat16)
image_embeds = [image_embeds]
image_latents = training_batch.image_latents
if frame_start is not None and frame_end is not None:
image_latents = image_latents[:, :, frame_start:frame_end, :, :]
vae_temporal_compression_ratio = 4
if frame_end is not None:
action_frame_end = (frame_end - 1) * vae_temporal_compression_ratio + 1
keyboard_cond_sliced = training_batch.keyboard_cond[:, :
action_frame_end, :] if training_batch.keyboard_cond is not None else None
mouse_cond_sliced = training_batch.mouse_cond[:, :
action_frame_end, :] if training_batch.mouse_cond is not None else None
else:
keyboard_cond_sliced = training_batch.keyboard_cond
mouse_cond_sliced = training_batch.mouse_cond
noisy_model_input = torch.cat([noise_input, image_latents.permute(0, 2, 1, 3, 4)], dim=2)
training_batch.input_kwargs = {
"hidden_states": noisy_model_input.permute(0, 2, 1, 3, 4), # bs, c, t, h, w
"encoder_hidden_states": None,
"timestep": timestep,
"encoder_hidden_states_image": image_embeds,
"keyboard_cond": keyboard_cond_sliced,
"mouse_cond": mouse_cond_sliced,
"num_frame_per_block": num_frame_per_block if num_frame_per_block is not None else self.num_frame_per_block,
}
training_batch.noise_latents = noise_input
return training_batch
def _dmd_forward(self, generator_pred_video: torch.Tensor, training_batch: TrainingBatch) -> torch.Tensor:
"""Compute DMD (Diffusion Model Distillation) loss for MatrixGame."""
original_latent = generator_pred_video
with torch.no_grad():
timestep = torch.randint(0, self.num_train_timestep, [1], device=self.device, dtype=torch.long)
timestep = shift_timestep(timestep, self.timestep_shift, self.num_train_timestep)
timestep = timestep.clamp(self.min_timestep, self.max_timestep)
noise = torch.randn(self.video_latent_shape, device=self.device, dtype=generator_pred_video.dtype)
noisy_latent = self.noise_scheduler.add_noise(generator_pred_video.flatten(0, 1), noise.flatten(
0, 1), timestep).detach().unflatten(0, (generator_pred_video.shape[0], generator_pred_video.shape[1]))
# Non-causal models expect 1D timestep (batch_size,)
critic_timestep = timestep.expand(noisy_latent.shape[0])
self._build_distill_input_kwargs(noisy_latent, critic_timestep, None, training_batch)
# fake_score_transformer forward
current_fake_score_transformer = self._get_fake_score_transformer(timestep)
fake_score_pred_noise = current_fake_score_transformer(**training_batch.input_kwargs).permute(0, 2, 1, 3, 4)
faker_score_pred_video = pred_noise_to_pred_video(pred_noise=fake_score_pred_noise.flatten(0, 1),
noise_input_latent=noisy_latent.flatten(0, 1),
timestep=timestep,
scheduler=self.noise_scheduler).unflatten(
0, fake_score_pred_noise.shape[:2])
# real_score_transformer forward
current_real_score_transformer = self._get_real_score_transformer(timestep)
real_score_pred_noise = current_real_score_transformer(**training_batch.input_kwargs).permute(0, 2, 1, 3, 4)
real_score_pred_video = pred_noise_to_pred_video(pred_noise=real_score_pred_noise.flatten(0, 1),
noise_input_latent=noisy_latent.flatten(0, 1),
timestep=timestep,
scheduler=self.noise_scheduler).unflatten(
0, real_score_pred_noise.shape[:2])
# No CFG for MatrixGame - use real_score_pred_video directly
grad = (faker_score_pred_video - real_score_pred_video) / torch.abs(original_latent -
real_score_pred_video).mean()
grad = torch.nan_to_num(grad)
dmd_loss = 0.5 * F.mse_loss(original_latent.float(), (original_latent.float() - grad.float()).detach())
training_batch.dmd_latent_vis_dict.update({
"training_batch_dmd_fwd_clean_latent": training_batch.latents,
"generator_pred_video": original_latent.detach(),
"real_score_pred_video": real_score_pred_video.detach(),
"faker_score_pred_video": faker_score_pred_video.detach(),
"dmd_timestep": timestep.detach(),
})
return dmd_loss
def faker_score_forward(self, training_batch: TrainingBatch) -> tuple[TrainingBatch, torch.Tensor]:
"""Forward pass for critic training with MatrixGame action conditioning."""
with torch.no_grad(), set_forward_context(current_timestep=training_batch.timesteps,
attn_metadata=training_batch.attn_metadata_vsa):
if self.training_args.simulate_generator_forward:
generator_pred_video = self._generator_multi_step_simulation_forward(training_batch)
else:
generator_pred_video = self._generator_forward(training_batch)
fake_score_timestep = torch.randint(0, self.num_train_timestep, [1], device=self.device, dtype=torch.long)
fake_score_timestep = shift_timestep(fake_score_timestep, self.timestep_shift, self.num_train_timestep)
fake_score_timestep = fake_score_timestep.clamp(self.min_timestep, self.max_timestep)
fake_score_noise = torch.randn(self.video_latent_shape, device=self.device, dtype=generator_pred_video.dtype)
noisy_generator_pred_video = self.noise_scheduler.add_noise(
generator_pred_video.flatten(0, 1), fake_score_noise.flatten(0, 1),
fake_score_timestep).unflatten(0, (generator_pred_video.shape[0], generator_pred_video.shape[1]))
# Non-causal critic expects 1D timestep (batch_size,), not 2D (batch_size, num_frames).
expanded_fake_score_timestep = fake_score_timestep.expand(noisy_generator_pred_video.shape[0])
self._build_distill_input_kwargs(noisy_generator_pred_video, expanded_fake_score_timestep, None, training_batch)
with set_forward_context(current_timestep=training_batch.timesteps, attn_metadata=training_batch.attn_metadata):
current_fake_score_transformer = self._get_fake_score_transformer(fake_score_timestep)
fake_score_pred_noise = current_fake_score_transformer(**training_batch.input_kwargs).permute(0, 2, 1, 3, 4)
target = fake_score_noise - generator_pred_video
flow_matching_loss = torch.mean((fake_score_pred_noise - target)**2)
training_batch.fake_score_latent_vis_dict = {
"training_batch_fakerscore_fwd_clean_latent": training_batch.latents,
"generator_pred_video": generator_pred_video,
"fake_score_timestep": fake_score_timestep,
}
return training_batch, flow_matching_loss
def _prepare_validation_batch(self, sampling_param: SamplingParam, training_args: TrainingArgs,
validation_batch: dict[str, Any], num_inference_steps: int) -> ForwardBatch:
sampling_param.prompt = validation_batch['prompt']
sampling_param.height = training_args.num_height
sampling_param.width = training_args.num_width
sampling_param.image_path = validation_batch.get('image_path') or validation_batch.get('video_path')
sampling_param.num_inference_steps = num_inference_steps
sampling_param.data_type = "video"
assert self.seed is not None
sampling_param.seed = self.seed
latents_size = [(sampling_param.num_frames - 1) // 4 + 1, sampling_param.height // 8, sampling_param.width // 8]
n_tokens = latents_size[0] * latents_size[1] * latents_size[2]
temporal_compression_factor = training_args.pipeline_config.vae_config.arch_config.temporal_compression_ratio
num_frames = (training_args.num_latent_t - 1) * temporal_compression_factor + 1
sampling_param.num_frames = num_frames
batch = ForwardBatch(
**shallow_asdict(sampling_param),
latents=None,
generator=torch.Generator(device="cpu").manual_seed(self.seed),
n_tokens=n_tokens,
eta=0.0,
VSA_sparsity=training_args.VSA_sparsity,
)
if "image" in validation_batch and validation_batch["image"] is not None:
batch.pil_image = validation_batch["image"]
if "keyboard_cond" in validation_batch and validation_batch["keyboard_cond"] is not None:
keyboard_cond = validation_batch["keyboard_cond"]
keyboard_cond = torch.tensor(keyboard_cond, dtype=torch.bfloat16)
keyboard_cond = keyboard_cond.unsqueeze(0)
batch.keyboard_cond = keyboard_cond
if "mouse_cond" in validation_batch and validation_batch["mouse_cond"] is not None:
mouse_cond = validation_batch["mouse_cond"]
mouse_cond = torch.tensor(mouse_cond, dtype=torch.bfloat16)
mouse_cond = mouse_cond.unsqueeze(0)
batch.mouse_cond = mouse_cond
return batch
def main(args) -> None:
logger.info("Starting MatrixGame self-forcing distillation pipeline...")
pipeline = MatrixGameSelfForcingDistillationPipeline.from_pretrained(args.pretrained_model_name_or_path, args=args)
args = pipeline.training_args
pipeline.train()
logger.info("MatrixGame self-forcing distillation pipeline completed")
if __name__ == "__main__":
argv = sys.argv
from fastvideo.fastvideo_args import TrainingArgs
from fastvideo.utils import FlexibleArgumentParser
parser = FlexibleArgumentParser()
parser = TrainingArgs.add_cli_args(parser)
parser = FastVideoArgs.add_cli_args(parser)
args = parser.parse_args()
main(args)
+10
View File
@@ -188,6 +188,16 @@ class WandbTracker(BaseTracker):
self._run.log(metrics, step=step)
self._timed_metrics = {}
def log_artifacts(self, artifacts: dict[str, Any], step: int) -> None:
if not artifacts:
return
normalized_artifacts = dict(artifacts)
if "validation_ref_videos" in normalized_artifacts:
normalized_artifacts["reference_video/videos"] = (normalized_artifacts.pop("validation_ref_videos"))
self.log(normalized_artifacts, step)
def log_file(
self,
file_path: str,
+21
View File
@@ -35,6 +35,7 @@ from fastvideo.distributed import (cleanup_dist_env_and_memory, get_local_torch_
from fastvideo.fastvideo_args import FastVideoArgs, TrainingArgs
from fastvideo.forward_context import set_forward_context
from fastvideo.logger import init_logger
from fastvideo.models.vision_utils import load_video
from fastvideo.pipelines import (ComposedPipelineBase, ForwardBatch, LoRAPipeline, TrainingBatch)
from fastvideo.platforms import current_platform
from fastvideo.training.activation_checkpoint import (apply_activation_checkpointing)
@@ -80,6 +81,7 @@ class TrainingPipeline(LoRAPipeline, ABC):
set_random_seed(fastvideo_args.seed) # for lora param init
super().__init__(model_path, fastvideo_args, required_config_modules, loaded_modules) # type: ignore
self.tracker = DummyTracker()
self.validation_ref_videos_logged = False
def create_pipeline_stages(self, fastvideo_args: FastVideoArgs):
raise RuntimeError("create_pipeline_stages should not be called for training pipeline")
@@ -741,6 +743,7 @@ class TrainingPipeline(LoRAPipeline, ABC):
local_main_process_only=False)
step_videos: list[np.ndarray] = []
step_captions: list[str] = []
step_ref_videos: list[str | None] = []
step_audio: list[np.ndarray | None] = []
step_sample_rates: list[int | None] = []
@@ -756,6 +759,7 @@ class TrainingPipeline(LoRAPipeline, ABC):
assert batch.prompt is not None and isinstance(batch.prompt, str)
step_captions.append(batch.prompt)
step_ref_videos.append(validation_batch.get("ref_video"))
# Run validation inference
output_batch = self.validation_pipeline.forward(batch, training_args)
@@ -789,6 +793,7 @@ class TrainingPipeline(LoRAPipeline, ABC):
# Global rank 0 collects results from all sp_group leaders
all_videos = step_videos # Start with own results
all_captions = step_captions
all_ref_videos = step_ref_videos
all_audios = step_audio
all_sample_rates = step_sample_rates
@@ -797,11 +802,13 @@ class TrainingPipeline(LoRAPipeline, ABC):
src_rank = sp_group_idx * self.sp_world_size # Global rank of other sp_group leaders
recv_videos = world_group.recv_object(src=src_rank)
recv_captions = world_group.recv_object(src=src_rank)
recv_ref_videos = world_group.recv_object(src=src_rank)
recv_audios = world_group.recv_object(src=src_rank)
recv_sample_rates = world_group.recv_object(src=src_rank)
all_videos.extend(recv_videos)
all_captions.extend(recv_captions)
all_ref_videos.extend(recv_ref_videos)
all_audios.extend(recv_audios)
all_sample_rates.extend(recv_sample_rates)
@@ -830,10 +837,24 @@ class TrainingPipeline(LoRAPipeline, ABC):
if artifacts:
logs = {f"validation_videos_{num_inference_steps}_steps": artifacts}
self.tracker.log_artifacts(logs, global_step)
if not self.validation_ref_videos_logged:
ref_artifacts = []
for ref_filename, caption in zip(all_ref_videos, all_captions, strict=True):
if ref_filename is None:
continue
ref_frames = np.stack([np.asarray(frame) for frame in load_video(ref_filename)], axis=0)
ref_frames = np.ascontiguousarray(ref_frames.transpose(0, 3, 1, 2))
video_artifact = self.tracker.video(ref_frames, caption=caption, fps=sampling_param.fps)
if video_artifact is not None:
ref_artifacts.append(video_artifact)
if ref_artifacts:
self.tracker.log_artifacts({"validation_ref_videos": ref_artifacts}, global_step)
self.validation_ref_videos_logged = True
elif self.rank_in_sp_group == 0:
# Other sp_group leaders send their results to global rank 0
world_group.send_object(step_videos, dst=0)
world_group.send_object(step_captions, dst=0)
world_group.send_object(step_ref_videos, dst=0)
world_group.send_object(step_audio, dst=0)
world_group.send_object(step_sample_rates, dst=0)