Skip to content

Commit 885e1ec

Browse files
committed
video sft
1 parent 98c79e4 commit 885e1ec

10 files changed

Lines changed: 502 additions & 55 deletions

File tree

src/maxtext/configs/base.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ load_full_state_path: ""
5252
# If enable_checkpointing is true, an asynchronous checkpointer will be used if
5353
# async_checkpointing is true, else a synchronous one is used. If you have
5454
# problems with the checkpointer we recommend trying the synchronous one.
55-
enable_checkpointing: true
55+
enable_checkpointing: false
5656
save_checkpoint_on_completion: true
5757
async_checkpointing: true
5858
checkpoint_period: 10_000
@@ -839,9 +839,7 @@ tpu_num_sparse_cores_to_trace: 2
839839
# - upload xplane profiling, if it is enabled.
840840
# - upload training metrics, at the defined log_period interval.
841841
managed_mldiagnostics: false # Whether to enable the managed diagnostics
842-
managed_mldiagnostics_on_demand_profiling: true # Enable on-demand profiling server by default
843842
managed_mldiagnostics_run_group: "" # Optional. Used to group multiple runs.
844-
managed_mldiagnostics_region: "" # Optional. GCP region for managed mldiagnostics. If empty, it will be auto-detected by the SDK.
845843

846844
# Dump HLO and jaxpr options
847845
dump_hlo: false
@@ -1124,12 +1122,14 @@ remat_policy_for_vit: "minimal" # Remat policy for multimodal model's vision en
11241122
image_size_for_vit: 896 # Default for Gemma3, and should be overwritten by model's config
11251123
image_path: "" # Local image path used for decoding, can be multiple paths separated by comma, exp "/path/image1.jpg,/path/image2.jpg"
11261124
video_path: "" # Local video path used for decoding, can be multiple paths separated by comma, exp "/path/video1.mp4,/path/video2.mp4"
1125+
video_directory: "" # Local video directory used for SFT training, e.g. "/mounted/LLaVA-Video-178K"
11271126
audio_path: "" # Local audio path used for decoding, can be multiple paths separated by comma, exp "/path/audio1.wav,/path/audio2.wav"
11281127
image_placeholder: "<|image|>"
11291128
video_placeholder: "<|video|>"
11301129
audio_placeholder: "<|audio|>"
11311130
use_audio_in_video: false
11321131
posemb_type_for_vit: "learn"
1132+
filter_sft_sequences_by_length: false
11331133
# max_num_images_per_example only applies for training when your image column is a list of images.
11341134
# -1 means no limit, and will pad to the max possible number of images determined by sequence length.
11351135
# Set it to avoid unnecessary padding if you know the maximum number of images per example.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
base_config: "base.yml"
16+
17+
use_sft: true
18+
use_tunix_gradient_accumulation: true
19+
use_multimodal: true
20+
sft_train_on_completion_only: true
21+
packing: false # packing is not supported yet
22+
freeze_vision_encoder_params: true
23+
learning_rate: 2.e-5
24+
25+
# -------------- Model --------------
26+
model_name: "qwen3-omni-30b-a3b"
27+
tokenizer_path: "Qwen/Qwen3-Omni-30B-A3B-Instruct"
28+
29+
# -------------- HF pipeline --------------
30+
dataset_type: "hf"
31+
hf_path: "parquet"
32+
hf_train_files: "gs://hengtaoguo-maxtext-logs/datasets/LLaVA-Video-178K/0_30_s_academic_v0_1/*.parquet"
33+
train_split: "train"
34+
train_data_columns: ["query", "label"]
35+
train_image_column: "video"
36+
37+
# Local SSD path for videos on the TPU VM
38+
video_directory: "/mounted/LLaVA-Video-178K/0_30_s_academic_v0_1"

src/maxtext/configs/types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1872,13 +1872,18 @@ class MultimodalGeneral(BaseModel):
18721872
description="Maximum number of images per example for training with image lists. -1 means no limit.",
18731873
)
18741874
video_path: PathStr = Field("", description="Path to a video for decoding.")
1875+
video_directory: PathStr = Field("", description="Local directory path containing video files for SFT.")
18751876
audio_path: PathStr = Field("", description="Path to an audio file for decoding.")
18761877
video_placeholder: str = Field("<|video|>", description="Placeholder string for video in text prompts.")
18771878
audio_placeholder: str = Field("<|audio|>", description="Placeholder string for audio in text prompts.")
18781879
use_audio_in_video: bool = Field(False, description="Extract and use audio from video files.")
18791880
use_mrope: bool = Field(False, description="Enable Multi-dimensional RoPE for Qwen3-Omni models.")
18801881
mrope_section: list[int] = Field([24, 20, 20], description="Dimensions for temporal, height, width in MRoPE.")
18811882
position_id_per_seconds: int = Field(25, description="Temporal granularity for MRoPE (tokens per second).")
1883+
filter_sft_sequences_by_length: bool = Field(
1884+
False,
1885+
description="Filter out multimodal SFT sequences that exceed max_prefill_predict_length or max_target_length.",
1886+
)
18821887

18831888

18841889
class VisionTower(BaseModel):

src/maxtext/input_pipeline/hf_data_processing.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,25 @@ def vision_sft_preprocessing_pipeline(
5555
"""pipeline for multimodal SFT with HF dataset"""
5656

5757
assert len(text_columns) == 2, f"Need two text_columns for query and response, received {text_columns=}"
58+
59+
# Format conversations if columns are missing
60+
features_keys = list(dataset.features.keys()) if dataset.features else []
61+
if "conversations" in features_keys and not all(col in features_keys for col in text_columns):
62+
def format_llava_video_dataset(example):
63+
conversations = example["conversations"]
64+
query = ""
65+
label = ""
66+
for turn in conversations:
67+
if turn["from"] == "human" and not query:
68+
query = turn["value"]
69+
elif turn["from"] == "gpt" and not label:
70+
label = turn["value"]
71+
example[text_columns[0]] = query
72+
example[text_columns[1]] = label
73+
return example
74+
75+
dataset = dataset.map(format_llava_video_dataset)
76+
5877
# Tunix GA requires per-micro-batch slicing at the data level,
5978
# whereas Native GA processes the full batch and splits it internally.
6079
if config.elastic_enabled:
@@ -137,6 +156,18 @@ def vision_sft_preprocessing_pipeline(
137156
fn_kwargs={"column_name": text_columns[0], "config": config},
138157
)
139158

159+
# Filter out sequences exceeding max_prefill_predict_length or max_target_length
160+
if getattr(config, "filter_sft_sequences_by_length", False):
161+
max_prefill = getattr(config, "max_prefill_predict_length", 8192)
162+
max_target = getattr(config, "max_target_length", 8192 + 512)
163+
164+
def filter_by_length(example):
165+
prefill_len = len(example[text_columns[0]])
166+
response_len = len(example[text_columns[1]])
167+
return (prefill_len <= max_prefill) and (prefill_len + response_len <= max_target)
168+
169+
dataset = dataset.filter(filter_by_length)
170+
140171
dataset = input_pipeline_utils.HFDataSource(
141172
dataset=dataset,
142173
dataloading_host_index=dataloading_host_index,

src/maxtext/input_pipeline/input_pipeline_utils.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,25 @@ def _process_string(string_tensor):
9292

9393
def reformat_prompt(example, column, image_placeholder, model_name):
9494
"""reformat prompt for multimodal SFT"""
95-
if isinstance(example["images"], list):
96-
num_images = len(example["images"])
95+
if isinstance(example["images"], str):
96+
example[column] = mm_processor.reformat_prompt(
97+
example[column], image_placeholder, model_name, num_images=0, video_placeholder=image_placeholder, num_videos=1
98+
)
9799
else:
98-
num_images = 1
99-
example[column] = mm_processor.reformat_prompt(example[column], image_placeholder, model_name, num_images)
100+
if isinstance(example["images"], list):
101+
num_images = len(example["images"])
102+
else:
103+
num_images = 1
104+
example[column] = mm_processor.reformat_prompt(example[column], image_placeholder, model_name, num_images)
100105
return example
101106

102107

103108
def reformat_response(example, column, model_name):
104109
"""reformat response for multimodal SFT"""
105-
example[column] = mm_processor.reformat_response(example[column][0], model_name)
110+
val = example[column]
111+
if isinstance(val, (list, tuple)) and len(val) > 0:
112+
val = val[0]
113+
example[column] = mm_processor.reformat_response(val, model_name)
106114
return example
107115

108116

@@ -120,9 +128,17 @@ def merge_image_columns(example, image_columns, max_num_images_per_example):
120128

121129

122130
def pre_process_image_sft(example, image_column, config):
123-
"""pre-process image for multimodal SFT"""
131+
"""pre-process image or video for multimodal SFT"""
124132

125133
def _process_image_fn(image):
134+
if isinstance(image, str):
135+
import os
136+
137+
video_directory = getattr(config, "video_directory", "")
138+
if video_directory:
139+
image = os.path.join(video_directory, image)
140+
return mm_processor.preprocess_image_for_training(image, config)
141+
126142
if isinstance(image, list):
127143
image = [np.array(mm_utils.convert_to_RGB(img)) for img in image]
128144
else:
@@ -131,7 +147,7 @@ def _process_image_fn(image):
131147
image = mm_processor.preprocess_image_for_training(image, config)
132148
return image
133149

134-
example[image_column] = _process_image_fn(example[image_column])
150+
example[image_column] = _process_image_fn(example[image_column]) if example.get(image_column) is not None else None
135151
return example
136152

137153

@@ -702,12 +718,12 @@ def _pad_image_and_mask(self, preprocessed_image: mm_utils.PreprocessorOutput) -
702718
if not isinstance(preprocessed_image, mm_utils.PreprocessorOutput):
703719
raise TypeError(f"Input must be multimodal_utils.PreprocessorOutput, but got {type(preprocessed_image)}")
704720

705-
if preprocessed_image.pixel_values is None:
706-
raise ValueError("Input preprocessed_image must have pixel_values to pad images.")
707-
708721
if self.config.model_name and self.config.model_name.startswith("qwen3-omni"):
709722
return preprocessed_image
710723

724+
if preprocessed_image.pixel_values is None:
725+
raise ValueError("Input preprocessed_image must have pixel_values to pad images.")
726+
711727
# Determine the maximum number of images/masks allowed.
712728
image_offsets = mm_processor.get_image_offsets(self.config, preprocessed_image)
713729
single_image_offset = image_offsets // preprocessed_image.pixel_values.shape[0]

src/maxtext/multimodal/processor.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,13 @@ def preprocess_image_for_training(image, config):
6969

7070
return preprocess_mm_data_llama4(image)
7171
elif config.model_name in ["qwen3-omni-30b-a3b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
72-
from maxtext.multimodal.processor_qwen3_omni import preprocess_mm_data_qwen3_omni_for_training # pylint: disable=import-outside-toplevel
72+
from maxtext.multimodal.processor_qwen3_omni import preprocess_mm_data_qwen3_omni_for_training, preprocess_mm_data_qwen3_omni_for_training_video # pylint: disable=import-outside-toplevel
7373

74-
return preprocess_mm_data_qwen3_omni_for_training(image, config)
74+
if isinstance(image, str):
75+
use_audio_in_video = getattr(config, "use_audio_in_video", False)
76+
return preprocess_mm_data_qwen3_omni_for_training_video(image, config)
77+
else:
78+
return preprocess_mm_data_qwen3_omni_for_training(image, config)
7579
else:
7680
raise ValueError(f"Model {config.model_name} not supported for image preprocessing.")
7781

0 commit comments

Comments
 (0)