Skip to content

Commit 6c2defc

Browse files
Merge pull request AI-Hypercomputer#4251 from AI-Hypercomputer:hengtaoguo-resize
PiperOrigin-RevId: 937714775
2 parents 1d54eb6 + 9af0ba7 commit 6c2defc

5 files changed

Lines changed: 169 additions & 5 deletions

File tree

src/maxtext/configs/base.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,9 @@ image_placeholder: "<|image|>"
11671167
video_placeholder: "<|video|>"
11681168
audio_placeholder: "<|audio|>"
11691169
use_audio_in_video: false
1170+
video_max_grid_t: null
1171+
video_max_grid_h: null
1172+
video_max_grid_w: null
11701173
posemb_type_for_vit: "learn"
11711174
# max_num_images_per_example only applies for training when your image column is a list of images.
11721175
# -1 means no limit, and will pad to the max possible number of images determined by sequence length.

src/maxtext/configs/types.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1955,6 +1955,25 @@ class MultimodalGeneral(BaseModel):
19551955
use_mrope: bool = Field(False, description="Enable Multi-dimensional RoPE for Qwen3-Omni models.")
19561956
mrope_section: list[int] = Field([24, 20, 20], description="Dimensions for temporal, height, width in MRoPE.")
19571957
position_id_per_seconds: int = Field(25, description="Temporal granularity for MRoPE (tokens per second).")
1958+
video_max_grid_t: Optional[int] = Field(
1959+
None,
1960+
description="Maximum Qwen3-Omni/3.5 video grid size in temporal patches, before spatial merge.",
1961+
)
1962+
video_max_grid_h: Optional[int] = Field(
1963+
None,
1964+
description="Maximum Qwen3-Omni/3.5 video grid size in height patches, before spatial merge.",
1965+
)
1966+
video_max_grid_w: Optional[int] = Field(
1967+
None,
1968+
description="Maximum Qwen3-Omni/3.5 video grid size in width patches, before spatial merge.",
1969+
)
1970+
1971+
@model_validator(mode="after")
1972+
def validate_video_max_grid(self) -> "MultimodalGeneral":
1973+
max_grid = (self.video_max_grid_t, self.video_max_grid_h, self.video_max_grid_w)
1974+
if any(dim is None for dim in max_grid) and not all(dim is None for dim in max_grid):
1975+
raise ValueError("video_max_grid_t, video_max_grid_h, and video_max_grid_w must be set together.")
1976+
return self
19581977

19591978

19601979
class VisionTower(BaseModel):

src/maxtext/models/qwen3.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1866,19 +1866,29 @@ def __init__(
18661866
rngs=rngs,
18671867
)
18681868

1869-
def __call__(self, hidden_states: Array) -> Array:
1869+
def __call__(self, hidden_states: Array, video_mask: Array | None = None) -> tuple[Array, Array | None]:
18701870
"""
18711871
Args:
18721872
hidden_states: Input tensor of shape (batch, in_channels, temporal*patch_size, height*patch_size, width*patch_size)
1873+
video_mask: Optional pixel-level mask with shape
1874+
(batch, 1, temporal*patch_size, height*patch_size, width*patch_size).
18731875
Returns:
1874-
Output tensor of shape (batch, T*H*W, embed_dim) where T, H, W are the number of patches
1876+
Tuple of:
1877+
- Output tensor of shape (batch, T*H*W, embed_dim) where T, H, W are the number of patches
1878+
- Attention mask of shape (batch, T*H*W), or None when video_mask is not provided
18751879
"""
18761880
hidden_states = jnp.transpose(hidden_states, (0, 2, 3, 4, 1))
18771881
hidden_states = self.proj(hidden_states)
18781882
batch_size = hidden_states.shape[0]
18791883
seq_len = hidden_states.shape[1] * hidden_states.shape[2] * hidden_states.shape[3]
18801884
hidden_states = hidden_states.reshape(batch_size, seq_len, self.embed_dim)
1881-
return hidden_states
1885+
1886+
attention_mask = None
1887+
if video_mask is not None:
1888+
patch_mask = video_mask[:, 0, :: self.temporal_patch_size, :: self.patch_size, :: self.patch_size]
1889+
attention_mask = patch_mask.reshape(video_mask.shape[0], -1).astype(jnp.int32)
1890+
1891+
return hidden_states, attention_mask
18821892

18831893

18841894
class Qwen3OmniMoeVisionAttention(nnx.Module):
@@ -2102,7 +2112,7 @@ def __call__(
21022112
self.config.patch_size_for_vit,
21032113
)
21042114

2105-
x = self.patch_embed(hidden_states)
2115+
x, _ = self.patch_embed(hidden_states)
21062116
x = x.reshape(batch_size, -1, self.config.hidden_size_for_vit)
21072117
pos = self.pos_embed_interpolate(num_frames, height, width)
21082118

src/maxtext/multimodal/processor_qwen3_omni.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,75 @@ class Qwen3OmniPreprocessorOutput(mm_utils.PreprocessorOutput):
129129
video_values: None | np.ndarray = None
130130
video_grid_thw: None | np.ndarray = None
131131
video_second_per_grid: None | np.ndarray = None
132+
video_mask: None | np.ndarray = None
132133
# Audio attributes.
133134
num_audios: int = 0
134135
audio_values: None | np.ndarray = None
135136
audio_mask: None | np.ndarray = None
136137
audio_lengths: None | np.ndarray = None
137138

138139

140+
def maybe_pad_video_values_to_max_grid(
141+
video_values: np.ndarray,
142+
video_grid_thw: np.ndarray,
143+
config,
144+
) -> tuple[np.ndarray, np.ndarray, np.ndarray | None]:
145+
"""Pad Qwen3-Omni video pixels to configured static grid limits when enabled.
146+
147+
Args:
148+
video_values: Video pixels of shape (batch, channels, T*tps, H*patch, W*patch).
149+
video_grid_thw: Actual video grid with shape (1, 3), in Qwen grid units.
150+
config: Config carrying video_max_grid_t/h/w and ViT patch sizes.
151+
152+
Returns:
153+
Tuple of:
154+
- padded video pixels, or the input when no max grid is configured
155+
- input grid_thw
156+
- pixel-level mask of shape (batch, 1, max_T*tps, max_H*patch, max_W*patch), or None
157+
"""
158+
max_grid = (
159+
getattr(config, "video_max_grid_t", None),
160+
getattr(config, "video_max_grid_h", None),
161+
getattr(config, "video_max_grid_w", None),
162+
)
163+
if all(dim is None for dim in max_grid):
164+
return video_values, video_grid_thw, None
165+
if any(dim is None for dim in max_grid):
166+
raise ValueError("video_max_grid_t, video_max_grid_h, and video_max_grid_w must be set together.")
167+
if video_values.ndim != 5:
168+
raise ValueError(f"video_values must have shape (batch, channels, time, height, width), got {video_values.shape}.")
169+
170+
max_t, max_h, max_w = (int(dim) for dim in max_grid)
171+
actual_t, actual_h, actual_w = (int(dim) for dim in video_grid_thw[0])
172+
if actual_t > max_t or actual_h > max_h or actual_w > max_w:
173+
raise ValueError(
174+
f"video grid {video_grid_thw[0].tolist()} exceeds max grid {(max_t, max_h, max_w)}. "
175+
"Scale or resize the video before padding."
176+
)
177+
178+
temporal_patch_size = config.temporal_patch_size_for_vit
179+
patch_size = config.patch_size_for_vit
180+
valid_t_px = actual_t * temporal_patch_size
181+
valid_h_px = actual_h * patch_size
182+
valid_w_px = actual_w * patch_size
183+
max_t_px = max_t * temporal_patch_size
184+
max_h_px = max_h * patch_size
185+
max_w_px = max_w * patch_size
186+
187+
padded_video_values = np.zeros(
188+
(video_values.shape[0], video_values.shape[1], max_t_px, max_h_px, max_w_px),
189+
dtype=video_values.dtype,
190+
)
191+
padded_video_values[:, :, :valid_t_px, :valid_h_px, :valid_w_px] = video_values[
192+
:, :, :valid_t_px, :valid_h_px, :valid_w_px
193+
]
194+
195+
video_mask = np.zeros((video_values.shape[0], 1, max_t_px, max_h_px, max_w_px), dtype=np.int32)
196+
video_mask[:, :, :valid_t_px, :valid_h_px, :valid_w_px] = 1
197+
198+
return padded_video_values, video_grid_thw, video_mask
199+
200+
139201
def smart_resize(
140202
height: int, width: int, factor: int = 28, min_pixels: int = 56 * 56, max_pixels: int = 14 * 14 * 4 * 1280
141203
):
@@ -589,8 +651,10 @@ def preprocess_mm_data_qwen3_omni(config):
589651
config.patch_size_for_vit * video_grid_thw[0, 2],
590652
),
591653
)
654+
video_values, video_grid_thw, video_mask = maybe_pad_video_values_to_max_grid(video_values, video_grid_thw, config)
592655
processor_outputs.video_values = video_values
593656
processor_outputs.video_grid_thw = video_grid_thw
657+
processor_outputs.video_mask = video_mask
594658
processor_outputs.video_second_per_grid = np.asarray([config.temporal_patch_size_for_vit], dtype=np.float32)
595659
processor_outputs.num_videos = 1 # Only one video for now.
596660

tests/unit/qwen3_omni_layers_test.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
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
4041
from maxtext.models.qwen3 import (
4142
Qwen3OmniAudioEncoder,
4243
Qwen3OmniAudioEncoderLayer,
@@ -360,13 +361,80 @@ def test_patch_embed_output_matches_torch(self):
360361
)
361362

362363
torch_output = self.torch_model(torch_hidden_states)
363-
jax_output = self.jax_model(jax_hidden_states)
364+
jax_output, attention_mask = self.jax_model(jax_hidden_states)
365+
self.assertIsNone(attention_mask)
364366

365367
torch_output_squeezed = torch_output.squeeze(1)
366368
jax_output_squeezed = jax_output.squeeze(1)
367369

368370
assert_all_close_jax_torch(jax_output_squeezed, torch_output_squeezed, rtol=1e-3, atol=5e-3)
369371

372+
def test_patch_embed_padded_video_valid_outputs_match_unpadded(self):
373+
"""Test that valid padded video patches match embeddings from unpadded input."""
374+
config_video_pad = pyconfig.initialize(
375+
["", base_config_path],
376+
model_name="qwen3-omni-30b-a3b",
377+
attention="dot_product",
378+
attention_type="full",
379+
matmul_precision="highest",
380+
dropout_rate=0.0,
381+
dtype="float32",
382+
dtype_mm="float32",
383+
weight_dtype="float32",
384+
float32_logits=True,
385+
float32_qk_product=True,
386+
video_max_grid_t=3,
387+
video_max_grid_h=4,
388+
video_max_grid_w=4,
389+
)
390+
batch_size = 1
391+
raw_grid = (2, 2, 3) # Padding to (3, 4, 4)
392+
max_grid = (
393+
config_video_pad.video_max_grid_t,
394+
config_video_pad.video_max_grid_h,
395+
config_video_pad.video_max_grid_w,
396+
)
397+
temporal_patch_size = self.config.temporal_patch_size_for_vit
398+
patch_size = self.config.patch_size_for_vit
399+
in_channels = self.config.num_channels_for_vit
400+
401+
raw_shape = (
402+
batch_size,
403+
in_channels,
404+
raw_grid[0] * temporal_patch_size,
405+
raw_grid[1] * patch_size,
406+
raw_grid[2] * patch_size,
407+
)
408+
raw_hidden_states, _ = create_random_jax_torch(*raw_shape)
409+
raw_grid_thw = np.asarray([raw_grid], dtype=np.int32)
410+
411+
padded_hidden_states, padded_grid_thw, video_mask = maybe_pad_video_values_to_max_grid(
412+
np.asarray(raw_hidden_states),
413+
raw_grid_thw,
414+
config_video_pad,
415+
)
416+
417+
raw_output, raw_attention_mask = self.jax_model(raw_hidden_states)
418+
padded_output, attention_mask = self.jax_model(
419+
jnp.asarray(padded_hidden_states),
420+
video_mask=jnp.asarray(video_mask),
421+
)
422+
423+
self.assertIsNone(raw_attention_mask)
424+
np.testing.assert_array_equal(padded_grid_thw, raw_grid_thw)
425+
self.assertEqual(raw_output.shape, (batch_size, math.prod(raw_grid), self.config.hidden_size_for_vit))
426+
self.assertEqual(padded_output.shape, (batch_size, math.prod(max_grid), self.config.hidden_size_for_vit))
427+
self.assertEqual(attention_mask.shape, (batch_size, math.prod(max_grid)))
428+
self.assertEqual(int(jnp.sum(attention_mask)), math.prod(raw_grid))
429+
430+
padded_valid_output = np.array(padded_output)[np.array(attention_mask, dtype=bool)]
431+
np.testing.assert_allclose(
432+
np.array(raw_output).reshape(-1, self.config.hidden_size_for_vit),
433+
padded_valid_output,
434+
rtol=1e-3,
435+
atol=5e-3,
436+
)
437+
370438
def test_patch_embed_is_jittable(self):
371439
"""Test that patch embed is JIT-compilable."""
372440

0 commit comments

Comments
 (0)