Skip to content

Commit 8aa3476

Browse files
committed
batched padded encoder+projector
1 parent c549c31 commit 8aa3476

2 files changed

Lines changed: 105 additions & 2 deletions

File tree

src/maxtext/layers/embeddings.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1424,7 +1424,12 @@ def __call__(
14241424
batch_size = inputs.shape[0]
14251425
max_patches = num_frames * height * width
14261426

1427-
if valid_grid is None:
1427+
if valid_grid is not None:
1428+
if isinstance(valid_grid, (tuple, list)):
1429+
valid_grid = jnp.array([valid_grid], dtype=jnp.int32)
1430+
elif valid_grid.ndim == 1:
1431+
valid_grid = valid_grid[None, :]
1432+
else:
14281433
valid_grid = jnp.tile(
14291434
jnp.array([[num_frames, height, width]], dtype=jnp.int32),
14301435
(batch_size, 1),
@@ -1688,6 +1693,10 @@ def __call__(
16881693
Interpolated positional embeddings of shape [batch, num_frames * height * width, hidden_size]
16891694
"""
16901695
if video_grid_thw is not None:
1696+
if isinstance(video_grid_thw, (tuple, list)):
1697+
video_grid_thw = jnp.array([video_grid_thw], dtype=jnp.int32)
1698+
elif video_grid_thw.ndim == 1:
1699+
video_grid_thw = video_grid_thw[None, :]
16911700
batch_size = video_grid_thw.shape[0]
16921701
elif attention_mask is not None:
16931702
batch_size = attention_mask.shape[0]

tests/unit/qwen3_omni_layers_test.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
Qwen3OmniMoeVisionRotaryEmbedding as JaxQwen3OmniMoeVisionRotaryEmbedding,
3838
)
3939
from maxtext.layers.decoders import deepstack_process
40-
from maxtext.layers.encoders import AudioEncoder
40+
from maxtext.layers.encoders import AudioEncoder, VisionEncoder
4141
from maxtext.multimodal.processor_qwen3_omni import (
4242
maybe_pad_video_values_to_max_grid,
4343
preprocess_video,
@@ -868,6 +868,100 @@ def test_padded_video_valid_outputs_match_unpadded_batched(self):
868868
atol=1e-4,
869869
)
870870

871+
def test_padded_video_projected_outputs_match_unpadded_batched(self):
872+
"""Padded tokens do not change valid projected outputs for batched inputs."""
873+
raw_grid_1 = (2, 2, 2)
874+
raw_grid_2 = (1, 4, 4)
875+
max_grid = (3, 4, 4)
876+
config = pyconfig.initialize(
877+
["", base_config_path],
878+
model_name="qwen3-omni-30b-a3b",
879+
attention="dot_product",
880+
attention_type="full",
881+
dtype="float32",
882+
dtype_mm="float32",
883+
weight_dtype="float32",
884+
override_model_config=True,
885+
attention_for_vit="dot_product",
886+
hidden_size_for_vit=16,
887+
num_attention_heads_for_vit=2,
888+
intermediate_size_for_vit=32,
889+
num_hidden_layers_for_vit=1,
890+
deepstack_visual_indexes_for_vit=[],
891+
video_max_grid_t=max_grid[0],
892+
video_max_grid_h=max_grid[1],
893+
video_max_grid_w=max_grid[2],
894+
out_hidden_size_for_vit=24,
895+
vision_encoder_block=common_types.VisionEncoderBlockType.QWEN3_OMNI,
896+
)
897+
patch_size = config.patch_size_for_vit
898+
temporal_patch_size = config.temporal_patch_size_for_vit
899+
900+
# Generate raw videos
901+
raw_shape_1 = (
902+
1,
903+
config.num_channels_for_vit,
904+
raw_grid_1[0] * temporal_patch_size,
905+
raw_grid_1[1] * patch_size,
906+
raw_grid_1[2] * patch_size,
907+
)
908+
raw_shape_2 = (
909+
1,
910+
config.num_channels_for_vit,
911+
raw_grid_2[0] * temporal_patch_size,
912+
raw_grid_2[1] * patch_size,
913+
raw_grid_2[2] * patch_size,
914+
)
915+
raw_video_1, _ = create_random_jax_torch(*raw_shape_1)
916+
raw_video_2, _ = create_random_jax_torch(*raw_shape_2)
917+
918+
# Pad them individually
919+
padded_video_1, _, video_mask_1 = maybe_pad_video_values_to_max_grid(
920+
np.asarray(raw_video_1), np.asarray([raw_grid_1]), config
921+
)
922+
padded_video_2, _, video_mask_2 = maybe_pad_video_values_to_max_grid(
923+
np.asarray(raw_video_2), np.asarray([raw_grid_2]), config
924+
)
925+
926+
# Batch them
927+
batched_padded_video = jnp.concatenate([padded_video_1, padded_video_2], axis=0)
928+
batched_video_mask = jnp.concatenate([video_mask_1, video_mask_2], axis=0)
929+
batched_grid_thw = jnp.array([raw_grid_1, raw_grid_2], dtype=jnp.int32)
930+
931+
# Instantiate VisionEncoder (wraps encoder + projector)
932+
vision_encoder = VisionEncoder(config=config, mesh=self.mesh, rngs=nnx.Rngs(42))
933+
934+
# Run individual unpadded
935+
raw_output_1, _ = vision_encoder(raw_video_1)
936+
raw_output_2, _ = vision_encoder(raw_video_2)
937+
938+
# Run batched padded
939+
batched_padded_output, _ = vision_encoder(
940+
batched_padded_video,
941+
input_masks=batched_video_mask,
942+
video_grid_thw=batched_grid_thw,
943+
)
944+
945+
# Downsample masks to projected tokens
946+
projected_mask_1 = mm_processor.downsample_video_mask_to_tokens(video_mask_1, config)
947+
projected_mask_2 = mm_processor.downsample_video_mask_to_tokens(video_mask_2, config)
948+
949+
# Verify equivalence for batch item 0
950+
np.testing.assert_allclose(
951+
np.asarray(raw_output_1).reshape(-1, config.out_hidden_size_for_vit),
952+
np.asarray(batched_padded_output[0])[np.asarray(projected_mask_1[0]).astype(bool)],
953+
rtol=1e-4,
954+
atol=1e-4,
955+
)
956+
957+
# Verify equivalence for batch item 1
958+
np.testing.assert_allclose(
959+
np.asarray(raw_output_2).reshape(-1, config.out_hidden_size_for_vit),
960+
np.asarray(batched_padded_output[1])[np.asarray(projected_mask_2[0]).astype(bool)],
961+
rtol=1e-4,
962+
atol=1e-4,
963+
)
964+
871965

872966
class TestDeepstackProcess(unittest.TestCase):
873967
"""Tests for deepstack_process.

0 commit comments

Comments
 (0)