Skip to content

Commit a861186

Browse files
Merge pull request #4425 from AI-Hypercomputer:hengtaoguo-pad
PiperOrigin-RevId: 947231694
2 parents a239ff1 + f51a712 commit a861186

6 files changed

Lines changed: 172 additions & 31 deletions

File tree

src/maxtext/layers/attentions.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -929,8 +929,12 @@ def apply_rotary_embedding(
929929
num_frames = rope_kwargs.get("num_frames")
930930
height = rope_kwargs.get("height")
931931
width = rope_kwargs.get("width")
932+
token_mask = rope_kwargs.get("token_mask")
933+
valid_grid = rope_kwargs.get("valid_grid")
932934
# Type cast required: Omni rotary embedding uses different __call__ parameters than other embeddings.
933-
return cast(Qwen3OmniMoeVisionRotaryEmbedding, self.rotary_embedding)(inputs, num_frames, height, width)
935+
return cast(Qwen3OmniMoeVisionRotaryEmbedding, self.rotary_embedding)(
936+
inputs, num_frames, height, width, token_mask=token_mask, valid_grid=valid_grid
937+
)
934938
else:
935939
return self.rotary_embedding(inputs, inputs_positions)
936940

src/maxtext/layers/embeddings.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1394,7 +1394,15 @@ def _rotate_half(self, x: Array) -> Array:
13941394
x2 = x[..., x.shape[-1] // 2 :]
13951395
return jnp.concatenate([-x2, x1], axis=-1)
13961396

1397-
def __call__(self, inputs: Array, num_frames: int, height: int, width: int) -> Array:
1397+
def __call__(
1398+
self,
1399+
inputs: Array,
1400+
num_frames: int,
1401+
height: int,
1402+
width: int,
1403+
token_mask: Array | None = None,
1404+
valid_grid: tuple[int, int, int] | None = None,
1405+
) -> Array:
13981406
"""Apply rotary position embeddings directly to inputs (Q or K tensors).
13991407
14001408
Args:
@@ -1403,11 +1411,20 @@ def __call__(self, inputs: Array, num_frames: int, height: int, width: int) -> A
14031411
num_frames: Number of temporal frames (static)
14041412
height: Height in patches (static)
14051413
width: Width in patches (static)
1414+
token_mask: Optional mask identifying valid tokens in the padded sequence.
1415+
valid_grid: Optional unpadded `(frames, height, width)` grid used to compute
1416+
RoPE for valid tokens.
14061417
14071418
Returns:
14081419
Rotated inputs with same shape [B, T*H*W, N, head_dim]
14091420
"""
14101421
cos_emb, sin_emb = self.compute_cos_sin(num_frames, height, width)
1422+
if token_mask is not None and valid_grid is not None:
1423+
valid_cos, valid_sin = self.compute_cos_sin(*valid_grid)
1424+
valid_len = math.prod(valid_grid)
1425+
valid_indices = jnp.nonzero(token_mask[0], size=valid_len)[0]
1426+
cos_emb = jnp.ones_like(cos_emb).at[valid_indices].set(valid_cos)
1427+
sin_emb = jnp.zeros_like(sin_emb).at[valid_indices].set(valid_sin)
14111428

14121429
if len(inputs.shape) == 4:
14131430
cos_emb = cos_emb[None, :, None, :] # [1, S, 1, H]

src/maxtext/layers/encoders.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,15 @@ def _setup_vision_encoder_layers(self):
9393
else:
9494
raise ValueError(f"No VisionEncoder implemented for {self.config.model_name} yet")
9595

96-
def __call__(self, input_images, deterministic=False):
96+
def __call__(self, input_images, input_masks=None, video_grid_thw=None, deterministic=False):
9797
# vision encoder output, frozen params in many cases
9898
encoder = getattr(self, self.encoder_name)
99-
encoder_output = encoder(input_images, deterministic=deterministic)
99+
if self.config.model_name.startswith("qwen3") and input_masks is not None:
100+
encoder_output = encoder(
101+
input_images, video_mask=input_masks, video_grid_thw=video_grid_thw, deterministic=deterministic
102+
)
103+
else:
104+
encoder_output = encoder(input_images, deterministic=deterministic)
100105
deep_feats = None
101106
if isinstance(encoder_output, tuple):
102107
embeddings = encoder_output[0]

src/maxtext/models/qwen3.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,8 +1885,8 @@ def __call__(self, hidden_states: Array, video_mask: Array | None = None) -> tup
18851885

18861886
attention_mask = None
18871887
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)
1888+
mask_patch_elements = self.temporal_patch_size * self.patch_size * self.patch_size
1889+
attention_mask = video_mask.reshape(video_mask.shape[0], -1, mask_patch_elements).max(axis=-1).astype(jnp.int32)
18901890

18911891
return hidden_states, attention_mask
18921892

@@ -1941,6 +1941,8 @@ def __call__(
19411941
num_frames: int,
19421942
height: int,
19431943
width: int,
1944+
attention_mask: Array | None = None,
1945+
valid_grid: tuple[int, int, int] | None = None,
19441946
deterministic: bool = True,
19451947
) -> Array:
19461948
"""
@@ -1949,6 +1951,8 @@ def __call__(
19491951
num_frames: Number of temporal frames (static)
19501952
height: Height in patches (static)
19511953
width: Width in patches (static)
1954+
attention_mask: Optional mask identifying valid tokens in the padded sequence.
1955+
valid_grid: Optional unpadded `(frames, height, width)` grid used for vision RoPE.
19521956
deterministic: Whether to use deterministic mode (disable dropout)
19531957
19541958
Returns:
@@ -1959,11 +1963,14 @@ def __call__(
19591963
"num_frames": num_frames,
19601964
"height": height,
19611965
"width": width,
1966+
"token_mask": attention_mask,
1967+
"valid_grid": valid_grid,
19621968
}
19631969
output, _ = self.attn(
19641970
inputs_q=hidden_states,
19651971
inputs_kv=hidden_states,
19661972
deterministic=deterministic,
1973+
decoder_segment_ids=attention_mask,
19671974
rope_kwargs=rope_kwargs,
19681975
)
19691976

@@ -2016,6 +2023,8 @@ def __call__(
20162023
num_frames: int,
20172024
height: int,
20182025
width: int,
2026+
attention_mask: Array | None = None,
2027+
valid_grid: tuple[int, int, int] | None = None,
20192028
) -> Array:
20202029
"""
20212030
Args:
@@ -2027,7 +2036,14 @@ def __call__(
20272036
Returns:
20282037
Output tensor of shape (batch, T*H*W, hidden_size)
20292038
"""
2030-
x = x + self.attn(self.ln1(x), num_frames=num_frames, height=height, width=width)
2039+
x = x + self.attn(
2040+
self.ln1(x),
2041+
num_frames=num_frames,
2042+
height=height,
2043+
width=width,
2044+
attention_mask=attention_mask,
2045+
valid_grid=valid_grid,
2046+
)
20312047
y = self.ln2(x)
20322048
y = self.mlp(y)
20332049
y = jax.nn.gelu(y)
@@ -2088,6 +2104,8 @@ def __init__(self, config: Config, *, mesh=None, rngs: nnx.Rngs = None):
20882104
def __call__(
20892105
self,
20902106
hidden_states: Array,
2107+
video_mask: Array | None = None,
2108+
video_grid_thw: Array | tuple[int, int, int] | None = None,
20912109
deterministic: bool = True,
20922110
):
20932111
"""
@@ -2104,6 +2122,12 @@ def __call__(
21042122
num_frames = num_frames // self.config.temporal_patch_size_for_vit
21052123
height = height // self.config.patch_size_for_vit
21062124
width = width // self.config.patch_size_for_vit
2125+
attention_mask = None
2126+
if video_mask is not None:
2127+
mask_patch_elements = (
2128+
self.config.temporal_patch_size_for_vit * self.config.patch_size_for_vit * self.config.patch_size_for_vit
2129+
)
2130+
attention_mask = video_mask.reshape(batch_size, -1, mask_patch_elements).max(axis=-1).astype(jnp.int32)
21072131
hidden_states = hidden_states.reshape(
21082132
-1,
21092133
self.config.num_channels_for_vit,
@@ -2114,7 +2138,19 @@ def __call__(
21142138

21152139
x, _ = self.patch_embed(hidden_states)
21162140
x = x.reshape(batch_size, -1, self.config.hidden_size_for_vit)
2141+
valid_grid = None
2142+
if attention_mask is not None and video_grid_thw is None:
2143+
raise ValueError("video_grid_thw is required when video_mask is provided.")
2144+
if attention_mask is not None and batch_size != 1:
2145+
raise ValueError("Padded Qwen3-Omni vision encoding currently supports batch size one.")
2146+
if video_grid_thw is not None:
2147+
grid = video_grid_thw[0] if getattr(video_grid_thw, "ndim", 1) == 2 else video_grid_thw
2148+
valid_grid = tuple(int(dim) for dim in grid)
21172149
pos = self.pos_embed_interpolate(num_frames, height, width)
2150+
if attention_mask is not None and valid_grid is not None:
2151+
valid_pos = self.pos_embed_interpolate(*valid_grid)
2152+
valid_indices = jnp.nonzero(attention_mask[0], size=math.prod(valid_grid))[0]
2153+
pos = jnp.zeros_like(pos).at[valid_indices].set(valid_pos)
21182154

21192155
pos = pos[jnp.newaxis, :, :]
21202156
x = x + pos
@@ -2123,7 +2159,14 @@ def __call__(
21232159
for i in range(self.depth):
21242160
block_name = f"blocks_{i}"
21252161
blk = getattr(self, block_name)
2126-
x = blk(x, num_frames=num_frames, height=height, width=width)
2162+
x = blk(
2163+
x,
2164+
num_frames=num_frames,
2165+
height=height,
2166+
width=width,
2167+
attention_mask=attention_mask,
2168+
valid_grid=valid_grid,
2169+
)
21272170
h_traj.append(x)
21282171

21292172
deep_feats = []

src/maxtext/multimodal/processor_qwen3_omni.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""Qwen3-Omni-specific preprocessing utilities for multimodal features.
15+
"""Qwen3-Omni-specific preprocessing utilities for multimodal features.
1616
1717
Original implementation from HuggingFace: Qwen/Qwen3-Omni-30B-A3B-Instruct.
1818
"""
@@ -177,9 +177,6 @@ def maybe_pad_video_values_to_max_grid(
177177

178178
temporal_patch_size = config.temporal_patch_size_for_vit
179179
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
183180
max_t_px = max_t * temporal_patch_size
184181
max_h_px = max_h * patch_size
185182
max_w_px = max_w * patch_size
@@ -188,12 +185,15 @@ def maybe_pad_video_values_to_max_grid(
188185
(video_values.shape[0], video_values.shape[1], max_t_px, max_h_px, max_w_px),
189186
dtype=video_values.dtype,
190187
)
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-
]
188+
patch_elements = video_values.shape[1] * temporal_patch_size * patch_size * patch_size
189+
valid_patches = actual_t * actual_h * actual_w
190+
padded_video_values.reshape((video_values.shape[0], -1, patch_elements))[:, :valid_patches] = video_values.reshape(
191+
(video_values.shape[0], valid_patches, patch_elements)
192+
)
194193

195194
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
195+
mask_patch_elements = temporal_patch_size * patch_size * patch_size
196+
video_mask.reshape((video_values.shape[0], -1, mask_patch_elements))[:, :valid_patches] = 1
197197

198198
return padded_video_values, video_grid_thw, video_mask
199199

@@ -1136,7 +1136,9 @@ def get_rope_index(
11361136
# Process modality-specific content
11371137
# Audio Only
11381138
if min_ed == ed_audio_start:
1139-
audio_len = _get_feat_extract_output_lengths(audio_lengths[audio_idx]).item() # pyrefly: ignore[unsupported-operation]
1139+
audio_len = _get_feat_extract_output_lengths(
1140+
audio_lengths[audio_idx]
1141+
).item() # pyrefly: ignore[unsupported-operation]
11401142
audio_pos = np.arange(audio_len).reshape(1, -1).repeat(3, axis=0) + st_idx
11411143
llm_pos_ids_list.append(audio_pos)
11421144

@@ -1151,10 +1153,14 @@ def get_rope_index(
11511153
grid_ws = image_grid_thw[:, 2] # pyrefly: ignore[unsupported-operation]
11521154
t_index = np.arange(grid_t, dtype=np.float32) * 1 * position_id_per_seconds
11531155

1154-
image_pos = get_llm_pos_ids_for_vision(st_idx, image_idx, spatial_merge_size, t_index, grid_hs, grid_ws) # pyrefly: ignore[bad-argument-type]
1156+
image_pos = get_llm_pos_ids_for_vision(
1157+
st_idx, image_idx, spatial_merge_size, t_index, grid_hs, grid_ws
1158+
) # pyrefly: ignore[bad-argument-type]
11551159
llm_pos_ids_list.append(image_pos)
11561160

1157-
image_len = int(np.prod(image_grid_thw[image_idx]).item() // (spatial_merge_size**2)) # pyrefly: ignore[unsupported-operation]
1161+
image_len = int(
1162+
np.prod(image_grid_thw[image_idx]).item() // (spatial_merge_size**2)
1163+
) # pyrefly: ignore[unsupported-operation]
11581164
st += int(text_len + bos_len + image_len + eos_len)
11591165
image_idx += 1
11601166
remain_images -= 1
@@ -1164,27 +1170,39 @@ def get_rope_index(
11641170
grid_t = video_grid_thw[video_idx, 0].item() # pyrefly: ignore[unsupported-operation]
11651171
grid_hs = video_grid_thw[:, 1] # pyrefly: ignore[unsupported-operation]
11661172
grid_ws = video_grid_thw[:, 2] # pyrefly: ignore[unsupported-operation]
1167-
t_index = np.arange(grid_t, dtype=np.float32) * second_per_grids[video_idx].item() * position_id_per_seconds # pyrefly: ignore[unsupported-operation]
1173+
t_index = (
1174+
np.arange(grid_t, dtype=np.float32) * second_per_grids[video_idx].item() * position_id_per_seconds
1175+
) # pyrefly: ignore[unsupported-operation]
11681176

1169-
video_pos = get_llm_pos_ids_for_vision(st_idx, video_idx, spatial_merge_size, t_index, grid_hs, grid_ws) # pyrefly: ignore[bad-argument-type]
1177+
video_pos = get_llm_pos_ids_for_vision(
1178+
st_idx, video_idx, spatial_merge_size, t_index, grid_hs, grid_ws
1179+
) # pyrefly: ignore[bad-argument-type]
11701180
llm_pos_ids_list.append(video_pos)
11711181

1172-
video_len = int(np.prod(video_grid_thw[video_idx]).item() // (spatial_merge_size**2)) # pyrefly: ignore[unsupported-operation]
1182+
video_len = int(
1183+
np.prod(video_grid_thw[video_idx]).item() // (spatial_merge_size**2)
1184+
) # pyrefly: ignore[unsupported-operation]
11731185
st += int(text_len + bos_len + video_len + eos_len)
11741186
video_idx += 1
11751187
remain_videos -= 1
11761188

11771189
# Audio in Video (interleaved)
11781190
elif min_ed == ed_vision_start and ed_vision_start + 1 == ed_audio_start:
1179-
audio_len = _get_feat_extract_output_lengths(audio_lengths[audio_idx]).item() # pyrefly: ignore[unsupported-operation]
1191+
audio_len = _get_feat_extract_output_lengths(
1192+
audio_lengths[audio_idx]
1193+
).item() # pyrefly: ignore[unsupported-operation]
11801194
audio_llm_pos_ids = np.arange(audio_len).reshape(1, -1).repeat(3, axis=0) + st_idx
11811195

11821196
grid_t = video_grid_thw[video_idx, 0].item() # pyrefly: ignore[unsupported-operation]
11831197
grid_hs = video_grid_thw[:, 1] # pyrefly: ignore[unsupported-operation]
11841198
grid_ws = video_grid_thw[:, 2] # pyrefly: ignore[unsupported-operation]
1185-
t_index = np.arange(grid_t, dtype=np.float32) * second_per_grids[video_idx].item() * position_id_per_seconds # pyrefly: ignore[unsupported-operation]
1199+
t_index = (
1200+
np.arange(grid_t, dtype=np.float32) * second_per_grids[video_idx].item() * position_id_per_seconds
1201+
) # pyrefly: ignore[unsupported-operation]
11861202

1187-
video_llm_pos_ids = get_llm_pos_ids_for_vision(st_idx, video_idx, spatial_merge_size, t_index, grid_hs, grid_ws) # pyrefly: ignore[bad-argument-type]
1203+
video_llm_pos_ids = get_llm_pos_ids_for_vision(
1204+
st_idx, video_idx, spatial_merge_size, t_index, grid_hs, grid_ws
1205+
) # pyrefly: ignore[bad-argument-type]
11881206

11891207
# Interleave audio and video based on temporal ordering
11901208
video_data_index = 0
@@ -1203,7 +1221,9 @@ def get_rope_index(
12031221
if audio_data_index < audio_llm_pos_ids.shape[1]:
12041222
llm_pos_ids_list.append(audio_llm_pos_ids[:, audio_data_index:])
12051223

1206-
video_len = int(np.prod(video_grid_thw[video_idx]).item() // (spatial_merge_size**2)) # pyrefly: ignore[unsupported-operation]
1224+
video_len = int(
1225+
np.prod(video_grid_thw[video_idx]).item() // (spatial_merge_size**2)
1226+
) # pyrefly: ignore[unsupported-operation]
12071227
st += int(text_len + bos_len + audio_len + video_len + eos_len)
12081228

12091229
audio_idx += 1

0 commit comments

Comments
 (0)