Skip to content

Commit 5f6629d

Browse files
authored
[Gemma4] Pass num_frames_per_video to get_video_features in forward (#50237)
Gemma4: pass num_frames_per_video through forward to get_video_features
1 parent 9167cfc commit 5f6629d

6 files changed

Lines changed: 81 additions & 4 deletions

File tree

‎src/transformers/models/gemma4/modeling_gemma4.py‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/transformers/models/gemma4/modular_gemma4.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2078,7 +2078,7 @@ def forward(
20782078

20792079
if mm_encoder_outputs.get("video") is None and pixel_values_videos is not None:
20802080
mm_encoder_outputs["video"] = self.get_video_features(
2081-
pixel_values_videos, video_position_ids, return_dict=True
2081+
pixel_values_videos, video_position_ids, num_frames_per_video=num_frames_per_video, return_dict=True
20822082
)
20832083

20842084
if mm_encoder_outputs.get("video") is not None:
@@ -2271,6 +2271,7 @@ def forward(
22712271
use_cache=use_cache,
22722272
image_position_ids=image_position_ids,
22732273
video_position_ids=video_position_ids,
2274+
num_frames_per_video=num_frames_per_video,
22742275
mm_encoder_outputs=mm_encoder_outputs,
22752276
return_dict=True,
22762277
**kwargs,

‎src/transformers/models/gemma4_unified/modeling_gemma4_unified.py‎

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/transformers/models/gemma4_unified/modular_gemma4_unified.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,7 @@ def forward(
10811081
use_cache: bool | None = None,
10821082
image_position_ids: torch.LongTensor | None = None,
10831083
video_position_ids: torch.LongTensor | None = None,
1084+
num_frames_per_video: torch.LongTensor | None = None,
10841085
mm_encoder_outputs: dict[str, BaseModelOutputWithPooling] | None = None,
10851086
**kwargs: Unpack[TransformersKwargs],
10861087
) -> Gemma4UnifiedModelOutputWithPast:
@@ -1093,6 +1094,9 @@ def forward(
10931094
video_position_ids (`torch.LongTensor` of shape `(total_num_frames, max_patches, 2)`, *optional*):
10941095
2D patch position coordinates from the video processor, with `(-1, -1)` indicating padding.
10951096
Passed through to the vision encoder for positional embedding computation.
1097+
num_frames_per_video (`torch.LongTensor` of shape `(num_videos,)`, *optional*):
1098+
Number of frames belonging to each video. Required whenever `pixel_values_videos` is passed,
1099+
since the frames of all videos are concatenated along a single axis.
10961100
"""
10971101
if (input_ids is None) ^ (inputs_embeds is not None):
10981102
raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
@@ -1137,7 +1141,7 @@ def forward(
11371141

11381142
if mm_encoder_outputs.get("video") is None and pixel_values_videos is not None:
11391143
mm_encoder_outputs["video"] = self.get_video_features(
1140-
pixel_values_videos, video_position_ids, return_dict=True
1144+
pixel_values_videos, video_position_ids, num_frames_per_video=num_frames_per_video, return_dict=True
11411145
)
11421146

11431147
if mm_encoder_outputs.get("video") is not None:
@@ -1279,6 +1283,7 @@ def forward(
12791283
use_cache=use_cache,
12801284
image_position_ids=image_position_ids,
12811285
video_position_ids=video_position_ids,
1286+
num_frames_per_video=num_frames_per_video,
12821287
mm_encoder_outputs=mm_encoder_outputs,
12831288
return_dict=True,
12841289
**kwargs,

‎tests/models/gemma4/test_modeling_gemma4.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,38 @@ def test_training(self):
464464
loss = model(**inputs).loss
465465
loss.backward()
466466

467+
def test_forward_with_videos(self):
468+
config, _ = self.model_tester.prepare_config_and_inputs_for_common()
469+
model = Gemma4ForConditionalGeneration(config).to(torch_device).eval()
470+
471+
# Two videos with 2 and 3 frames
472+
num_frames_per_video = torch.tensor([2, 3], device=torch_device)
473+
pixel_values_videos = self.model_tester.create_pixel_values(5).to(torch_device)
474+
video_position_ids = self.model_tester.create_image_position_ids(5)
475+
num_tokens = self.model_tester.num_image_tokens # per frame
476+
input_ids = torch.ones((2, 3 * num_tokens + 2), dtype=torch.long, device=torch_device)
477+
input_ids[0, 1 : 1 + 2 * num_tokens] = config.video_token_id
478+
input_ids[1, 1 : 1 + 3 * num_tokens] = config.video_token_id
479+
mm_token_type_ids = torch.zeros_like(input_ids)
480+
mm_token_type_ids[input_ids == config.video_token_id] = 2
481+
482+
with torch.no_grad():
483+
video_features = model.model.get_video_features(
484+
pixel_values_videos, video_position_ids, num_frames_per_video=num_frames_per_video
485+
)
486+
# `generate` encodes the videos first, so check that raw pixels give the same output
487+
expected = model(
488+
input_ids, mm_token_type_ids=mm_token_type_ids, mm_encoder_outputs={"video": video_features}
489+
)
490+
output = model(
491+
input_ids,
492+
mm_token_type_ids=mm_token_type_ids,
493+
pixel_values_videos=pixel_values_videos,
494+
video_position_ids=video_position_ids,
495+
num_frames_per_video=num_frames_per_video,
496+
)
497+
torch.testing.assert_close(output.logits, expected.logits)
498+
467499
def test_vision_axial_rope(self):
468500
# override -> model shipped weirdly to from the start, pos IDs have actual batch dim
469501

‎tests/models/gemma4_unified/test_modeling_gemma4_unified.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,39 @@ def skip_mm_output_format(self):
427427
def test_num_layers_is_small(self):
428428
pass
429429

430+
def test_forward_with_videos(self):
431+
config, _ = self.model_tester.prepare_config_and_inputs_for_common()
432+
model = Gemma4UnifiedForConditionalGeneration(config).to(torch_device).eval()
433+
434+
# Two videos with 2 and 3 frames
435+
num_frames_per_video = torch.tensor([2, 3], device=torch_device)
436+
num_tokens = 2 # per frame, one token per patch
437+
patch_dim = config.vision_config.model_patch_size**2 * config.vision_config.num_channels
438+
pixel_values_videos = floats_tensor([5, num_tokens, patch_dim]).to(torch_device)
439+
video_position_ids = torch.ones(5, num_tokens, 2, dtype=torch.long, device=torch_device)
440+
input_ids = torch.ones((2, 3 * num_tokens + 2), dtype=torch.long, device=torch_device)
441+
input_ids[0, 1 : 1 + 2 * num_tokens] = config.video_token_id
442+
input_ids[1, 1 : 1 + 3 * num_tokens] = config.video_token_id
443+
mm_token_type_ids = torch.zeros_like(input_ids)
444+
mm_token_type_ids[input_ids == config.video_token_id] = 2
445+
446+
with torch.no_grad():
447+
video_features = model.model.get_video_features(
448+
pixel_values_videos, video_position_ids, num_frames_per_video=num_frames_per_video
449+
)
450+
# `generate` encodes the videos first, so check that raw pixels give the same output
451+
expected = model(
452+
input_ids, mm_token_type_ids=mm_token_type_ids, mm_encoder_outputs={"video": video_features}
453+
)
454+
output = model(
455+
input_ids,
456+
mm_token_type_ids=mm_token_type_ids,
457+
pixel_values_videos=pixel_values_videos,
458+
video_position_ids=video_position_ids,
459+
num_frames_per_video=num_frames_per_video,
460+
)
461+
torch.testing.assert_close(output.logits, expected.logits)
462+
430463
@unittest.skip("We use vision based masks that use `block_sequence_ids` which force mask materialization")
431464
def test_sdpa_can_dispatch_on_flash(self):
432465
pass

0 commit comments

Comments
 (0)