Skip to content

Commit 577b84a

Browse files
Merge pull request #4353 from AI-Hypercomputer:hengtaoguo-sft-video
PiperOrigin-RevId: 943469575
2 parents 3f198a9 + f2c6658 commit 577b84a

2 files changed

Lines changed: 71 additions & 3 deletions

File tree

src/maxtext/multimodal/processor_qwen3_omni.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,13 @@ def maybe_pad_video_values_to_max_grid(
199199

200200

201201
def smart_resize(
202-
height: int, width: int, factor: int = 28, min_pixels: int = 56 * 56, max_pixels: int = 14 * 14 * 4 * 1280
202+
height: int,
203+
width: int,
204+
factor: int = 28,
205+
min_pixels: int = 56 * 56,
206+
max_pixels: int = 14 * 14 * 4 * 1280,
207+
max_h_pixels: int | None = None,
208+
max_w_pixels: int | None = None,
203209
):
204210
"""Rescales the image so that the following conditions are met:
205211
@@ -209,6 +215,10 @@ def smart_resize(
209215
210216
3. The aspect ratio of the image is maintained as closely as possible.
211217
218+
4. (Optional) Neither dimension exceeds max_h_pixels / max_w_pixels. When either
219+
cap is exceeded, both dimensions are scaled down proportionally and re-aligned
220+
to 'factor'.
221+
212222
"""
213223
if max(height, width) / min(height, width) > MAX_RATIO:
214224
raise ValueError(
@@ -224,6 +234,13 @@ def smart_resize(
224234
beta = math.sqrt(min_pixels / (height * width))
225235
h_bar = math.ceil(height * beta / factor) * factor
226236
w_bar = math.ceil(width * beta / factor) * factor
237+
# Apply per-dimension pixel caps, scaling down proportionally if either is exceeded.
238+
if (max_h_pixels is not None and h_bar > max_h_pixels) or (max_w_pixels is not None and w_bar > max_w_pixels):
239+
cap_h = max_h_pixels if max_h_pixels is not None else h_bar
240+
cap_w = max_w_pixels if max_w_pixels is not None else w_bar
241+
scale = min(cap_h / h_bar, cap_w / w_bar)
242+
h_bar = min(max(factor, round(h_bar * scale / factor) * factor), (cap_h // factor) * factor)
243+
w_bar = min(max(factor, round(w_bar * scale / factor) * factor), (cap_w // factor) * factor)
227244
return h_bar, w_bar
228245

229246

@@ -461,6 +478,10 @@ def preprocess_video(video, config):
461478

462479
nframes, channel, height, width = video.shape
463480
max_pixels = max(min(VIDEO_MAX_PIXELS, VIDEO_TOTAL_PIXELS / nframes * FRAME_FACTOR), int(VIDEO_MIN_PIXELS * 1.05))
481+
max_grid_h = getattr(config, "video_max_grid_h", None)
482+
max_grid_w = getattr(config, "video_max_grid_w", None)
483+
max_h_pixels = int(max_grid_h) * patch_size if max_grid_h is not None else None
484+
max_w_pixels = int(max_grid_w) * patch_size if max_grid_w is not None else None
464485
resized_height_1, resized_width_1 = smart_resize(
465486
height,
466487
width,
@@ -488,6 +509,8 @@ def preprocess_video(video, config):
488509
factor=patch_size * merge_size,
489510
min_pixels=VIDEO_MIN_PIXELS,
490511
max_pixels=VIDEO_MAX_PIXELS,
512+
max_h_pixels=max_h_pixels,
513+
max_w_pixels=max_w_pixels,
491514
)
492515

493516
# Second resize - process each channel separately to preserve float values
@@ -780,7 +803,7 @@ def add_extra_tokens_for_qwen3_omni(tokens, config, processor_output):
780803
new_tokens.append(qwen_tokens.audio_pad)
781804
audio_data_idx += 1
782805

783-
new_tokens.append(qwen_tokens.audio_pad)
806+
new_tokens.append(qwen_tokens.audio_end)
784807
new_tokens.append(qwen_tokens.vision_end)
785808

786809
video_idx += 1

tests/unit/qwen3_omni_layers_test.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@
3737
)
3838
from maxtext.layers.decoders import deepstack_process
3939
from maxtext.layers.encoders import AudioEncoder
40-
from maxtext.multimodal.processor_qwen3_omni import maybe_pad_video_values_to_max_grid
40+
from maxtext.multimodal.processor_qwen3_omni import (
41+
maybe_pad_video_values_to_max_grid,
42+
preprocess_video,
43+
)
4144
from maxtext.models.qwen3 import (
4245
Qwen3OmniAudioEncoder,
4346
Qwen3OmniAudioEncoderLayer,
@@ -435,6 +438,48 @@ def test_patch_embed_padded_video_valid_outputs_match_unpadded(self):
435438
atol=5e-3,
436439
)
437440

441+
def test_scale_to_fit_video_before_padding(self):
442+
"""Test that preprocess_video respects video_max_grid_h/w for wide/tall videos."""
443+
cfg = pyconfig.initialize(
444+
["", base_config_path],
445+
model_name="qwen3-omni-30b-a3b",
446+
video_max_grid_t=2,
447+
video_max_grid_h=32,
448+
video_max_grid_w=32,
449+
patch_size_for_vit=16,
450+
num_channels_for_vit=3,
451+
temporal_patch_size_for_vit=2,
452+
)
453+
454+
# Wide video (2 frames, 3 channels, 220×896): grid_w >> max_grid_w.
455+
dummy_video_wide = np.ones((2, 3, 220, 896), dtype=np.float32)
456+
dummy_video_wide_ratio = dummy_video_wide.shape[3] / dummy_video_wide.shape[2]
457+
video_processed, video_grid_thw = preprocess_video(dummy_video_wide, cfg)
458+
self.assertLessEqual(video_grid_thw[0, 1], cfg.video_max_grid_h)
459+
self.assertLessEqual(video_grid_thw[0, 2], cfg.video_max_grid_w)
460+
# Check the aspect ratio is mostly preserved after smart_resize scaling to fit max grid.
461+
self.assertAlmostEqual(video_grid_thw[0, 2] / video_grid_thw[0, 1], dummy_video_wide_ratio, delta=0.5)
462+
463+
# Verify maybe_pad_video_values_to_max_grid succeeds without ValueError.
464+
video_values = np.reshape(
465+
video_processed,
466+
(
467+
1,
468+
cfg.num_channels_for_vit,
469+
cfg.temporal_patch_size_for_vit * video_grid_thw[0, 0],
470+
cfg.patch_size_for_vit * video_grid_thw[0, 1],
471+
cfg.patch_size_for_vit * video_grid_thw[0, 2],
472+
),
473+
)
474+
padded_values, padded_grid, _ = maybe_pad_video_values_to_max_grid(
475+
video_values,
476+
video_grid_thw,
477+
cfg,
478+
)
479+
self.assertEqual(padded_values.shape[3], cfg.video_max_grid_h * cfg.patch_size_for_vit)
480+
self.assertEqual(padded_values.shape[4], cfg.video_max_grid_w * cfg.patch_size_for_vit)
481+
np.testing.assert_array_equal(padded_grid, video_grid_thw) # Grid should not change
482+
438483
def test_patch_embed_is_jittable(self):
439484
"""Test that patch embed is JIT-compilable."""
440485

0 commit comments

Comments
 (0)