Skip to content

Commit 95542d9

Browse files
Merge pull request #2413 from AI-Hypercomputer:nicogrande/sft-llama4-tiling
PiperOrigin-RevId: 816013572
2 parents 29d9d5a + b8c957b commit 95542d9

10 files changed

Lines changed: 335 additions & 71 deletions

File tree

src/MaxText/decode.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def main(argv: Sequence[str]) -> None:
9999

100100
text = config.prompt
101101
prefill_length = config.max_prefill_predict_length
102-
# processor_output = multimodal_utils.PreprocessorOutput()
102+
processor_outputs = multimodal_utils.PreprocessorOutput()
103103
if config.use_multimodal:
104104
image_path = config.image_path.split(",")
105105
images = [multimodal_utils.load_image_from_path(p) for p in image_path]
@@ -151,6 +151,9 @@ def main(argv: Sequence[str]) -> None:
151151
params=params,
152152
padded_tokens=tokens,
153153
images=np.stack([po.pixel_values for po in processor_outputs]) if config.use_multimodal else None,
154+
image_masks=np.stack([po.pixel_mask for po in processor_outputs])
155+
if config.use_multimodal and "llama4" in config.model_name
156+
else None,
154157
true_length=true_length,
155158
rng=rng_prefill,
156159
slot=i,

src/MaxText/input_pipeline/_input_pipeline_utils.py

Lines changed: 75 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ def prepare_text_for_image_fusion(example, column_name, model_name):
106106
example[column_name], model_name, processor_output=example["images"]
107107
)
108108
if isinstance(example["images"], list):
109+
example["image_masks"] = [image.pixel_mask for image in example["images"]]
109110
example["images"] = [image.pixel_values for image in example["images"]]
110111
else:
112+
example["image_masks"] = example["images"].pixel_mask
111113
example["images"] = example["images"].pixel_values
112114
return example
113115

@@ -255,6 +257,7 @@ def map(self, element):
255257
"inputs": np.asarray(inputs[: self.max_target_length], dtype=np.int32),
256258
"targets": np.asarray(targets[: self.max_target_length], dtype=np.int32),
257259
"images": element["images"],
260+
"image_masks": element["image_masks"],
258261
}
259262

260263

@@ -434,49 +437,77 @@ def _pad_text(self, x, max_length, pad_id):
434437
pad_amount = [(0, pad_amount)] + [(0, 0)] * (len(x.shape) - 1)
435438
return np.pad(x, pad_amount, constant_values=pad_id)[: self.max_length]
436439

437-
def _pad_image(self, images):
438-
"""Pads the input images array to match the maximum required number of images per example.
440+
def _pad_image_or_mask(self, tensor: np.ndarray) -> np.ndarray:
441+
"""Pads an input tensor (image or mask) to a maximum number of items.
439442
440-
The function computes the maximum number of allowed images either based on model constraints
441-
(determined by model name and maximum sequence length) or the value provided in
442-
`self.max_num_images_per_example` (if positive), whichever is smaller. If the provided
443-
`images` array contains fewer images than the desired maximum, it pads the array with
444-
zeros (dummy images) to meet the requirement.
443+
This function unifies padding logic for image tensors (standard or tiled) and
444+
mask tensors. It determines the tensor type based on its dimensions and applies
445+
the appropriate padding along the first axis.
446+
447+
The maximum number of items is calculated based on model constraints or a
448+
user-defined limit, ensuring that sequence length limits are respected while
449+
reserving space for at least one text token. If the input tensor has fewer
450+
items than this maximum, it is padded with zeros.
445451
446452
Args:
447-
images (np.ndarray): A numpy array of image data. The expected shape is
448-
(num_images, height, width, channels), where num_images <= maximum allowed images.
453+
tensor (np.ndarray): The input numpy array to pad.
454+
- For masks, the expected shape is (num_masks, num_tiles).
455+
- For standard images, the shape is (num_images, H, W, C).
456+
- For tiled images, the shape is (num_images, num_tiles, H, W, C).
449457
450458
Returns:
451-
np.ndarray: An array of images padded with zero images (if necessary) such that its
452-
first dimension equals the computed maximum number of images.
459+
np.ndarray: The tensor, padded with zeros up to the maximum number of
460+
items along the first axis.
453461
454462
Raises:
455-
AssertionError: If the number of images in the input exceeds the allowed maximum.
463+
ValueError: If the input tensor's dimension is not 2, 4, or 5.
464+
ValueError: If the number of items in the input tensor exceeds the
465+
allowed maximum.
456466
457467
Notes:
458468
- The computation of maximum images ensures that space is reserved in the sequence
459469
for at least one text token.
460470
- The dummy images used for padding are based on the image shape for initialization
461471
of this model (ignoring batch size).
462472
"""
473+
if not isinstance(tensor, np.ndarray):
474+
raise TypeError(f"Input must be a numpy array, but got {type(tensor)}")
475+
476+
# Determine the maximum number of images/masks allowed.
463477
image_offsets = multimodal_utils.get_image_offsets(self.model_name, None)
464-
max_num_images = (self.max_length // image_offsets) - 1 # -1 to reserve space for at least one text token
478+
# Reserve space for at least one text token.
479+
max_num_items = (self.max_length // image_offsets) - 1
465480
if self.max_num_images_per_example > 0:
466-
max_num_images = min(self.max_num_images_per_example, max_num_images)
467-
image_shape = multimodal_utils.get_dummy_image_shape_for_init(self.model_name)[2:]
468-
assert (
469-
images.shape[0] <= max_num_images
470-
), f"Number of images {images.shape[0]} exceeds the maximum allowed {max_num_images}"
471-
if images.shape[0] < max_num_images:
472-
pad_size = max_num_images - images.shape[0]
473-
pad_shape = (pad_size,) + image_shape
474-
pad_images = np.zeros(pad_shape, dtype=images.dtype)
475-
if images is not None and images.size > 0:
476-
images = np.concatenate([images, pad_images], axis=0)
481+
max_num_items = min(self.max_num_images_per_example, max_num_items)
482+
483+
# Validate tensor dimensions.
484+
if tensor.ndim in (4, 5): # Standard or Tiled Image
485+
tensor_type = "images"
486+
elif tensor.ndim == 2: # Mask
487+
tensor_type = "masks"
488+
else:
489+
raise ValueError(
490+
"Input tensor must be 2D (mask), 4D (image), or 5D (tiled image), " f"but got {tensor.ndim} dimensions."
491+
)
492+
493+
# Assert that the input tensor does not exceed the maximum size.
494+
if tensor.shape[0] > max_num_items:
495+
raise ValueError(f"Number of {tensor_type} ({tensor.shape[0]}) exceeds the maximum allowed ({max_num_items}).")
496+
497+
# Apply padding if the tensor is smaller than the maximum size.
498+
if tensor.shape[0] < max_num_items:
499+
pad_size = max_num_items - tensor.shape[0]
500+
pad_shape_suffix = tensor.shape[1:]
501+
pad_shape = (pad_size,) + pad_shape_suffix
502+
pad_tensor = np.zeros(pad_shape, dtype=tensor.dtype)
503+
504+
if tensor.size > 0:
505+
tensor = np.concatenate([tensor, pad_tensor], axis=0)
477506
else:
478-
images = pad_images
479-
return images
507+
# If the input tensor is empty, the result is just the padding.
508+
tensor = pad_tensor
509+
510+
return tensor
480511

481512
def map(self, element: dict[str, np.ndarray]):
482513
"""map to each element"""
@@ -487,13 +518,27 @@ def map(self, element: dict[str, np.ndarray]):
487518
element[f"{data_column}_position"] = np.arange(element[data_column].shape[0], dtype=np.int32)
488519
if self.add_true_length:
489520
element[f"{data_column}_true_length"] = np.array([element[data_column].shape[0]], dtype=np.int32)
521+
490522
for key, _ in element.items():
491523
if key == "images":
492-
if isinstance(element["images"], list):
493-
assert self.model_name is not None, "model_name must be provided when padding images"
494-
element["images"] = self._pad_image(np.asarray(element["images"]))
495-
else:
524+
if isinstance(element["images"], list) and self.model_name is None:
525+
raise ValueError("model_name must be provided when padding images")
526+
elif isinstance(element["images"], list):
527+
element["images"] = self._pad_image_or_mask(np.asarray(element["images"]))
528+
elif element["images"].ndim == 3:
496529
element["images"] = np.asarray(element["images"])[None, ...]
530+
else:
531+
# Do not add extra image dimension for image tiling case
532+
element["images"] = np.asarray(element["images"])
533+
534+
elif key == "image_masks" and element["image_masks"] is not None:
535+
if isinstance(element["image_masks"], list) and self.model_name is None:
536+
raise ValueError("model_name must be provided when padding image masks")
537+
elif isinstance(element["image_masks"], list):
538+
element["image_masks"] = self._pad_image_or_mask(np.asarray(element["image_masks"]))
539+
else:
540+
element["image_masks"] = np.asarray(element["image_masks"])
541+
497542
elif "true_length" not in key:
498543
element[key] = self._pad_text(element[key], self.max_length, self.pad_id)
499544
return element

src/MaxText/layers/decoders.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ def get_remat_policy(self):
286286
# save all
287287
if cfg.remat_policy == "minimal_flash":
288288
max_logging.log("WARNING: 'minimal_flash' will be deprecated soon, please use 'minimal_with_context' instead.")
289+
max_logging.log("WARNING: 'minimal_flash' will be deprecated soon, please use 'minimal_with_context' instead.")
289290
policy = self.minimal_policy(with_context=True)
290291
elif cfg.remat_policy == "minimal":
291292
# save all except context
@@ -528,6 +529,7 @@ def _apply_embedding(
528529
model_mode,
529530
image_embeddings=None,
530531
bidirectional_mask=None,
532+
image_masks=None,
531533
):
532534
"""Applies token and positional embeddings to the input tokens."""
533535
cfg = self.config
@@ -541,6 +543,7 @@ def _apply_embedding(
541543
text_embeddings=y,
542544
vision_embeddings=image_embeddings,
543545
mask=bidirectional_mask,
546+
image_masks=image_masks,
544547
)
545548
# TODO(hengtaoguo): Add support for other multimodal models such as Llama4, refactor if needed
546549
else:
@@ -635,6 +638,7 @@ def __call__(
635638
page_state: None | page_manager.PageState = None,
636639
bidirectional_mask: None | Any = None,
637640
image_embeddings: None | jnp.ndarray = None,
641+
image_masks: None | jnp.ndarray = None,
638642
):
639643
cfg = self.config
640644
mesh = self.mesh
@@ -649,6 +653,7 @@ def __call__(
649653
model_mode,
650654
image_embeddings,
651655
bidirectional_mask,
656+
image_masks,
652657
)
653658

654659
policy = self.get_remat_policy()

src/MaxText/layers/llama4.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,12 @@ def __call__(self, image_features: Array) -> Array:
295295
Tensor of shape [batch_size, num_patches, (pixel_shuffle_ratio**2), vision_hidden_size]
296296
"""
297297
b, t, c, d = image_features.shape
298+
299+
# Reshape image_features to [b * t, c, d] and project to text hidden dimension
298300
image_features = image_features.reshape(b * t, c, d)
299301
hidden_states = self.linear(image_features)
302+
303+
# Reshape hidden states back to [b, t, c, d]
300304
_, c, d = hidden_states.shape
301305
hidden_states = hidden_states.reshape(b, t, c, d)
302306
return hidden_states
@@ -754,15 +758,18 @@ def __call__(
754758
"""Forward pass of the Llama4 vision model.
755759
756760
Args:
757-
inputs: Input tensor of shape [batch_size, num_tiles, num_channels_for_vit, tile_size_for_vit, tile_size_for_vit]
761+
inputs: Input tensor of shape:
762+
[batch_size * num_images, num_tiles, num_channels_for_vit, tile_size_for_vit, tile_size_for_vit]
758763
deterministic: Whether to use deterministic mode (disables dropout)
759764
760765
Returns:
761-
Final hidden states from the vision encoder of shape [batch_size, num_tiles, num_patches, vision_output_dim_for_vit]
766+
Final hidden states from the vision encoder of shape:
767+
[batch_size * num_images, num_tiles, num_patches, vision_output_dim_for_vit]
762768
"""
763769
cfg = self.config
764770
mesh = self.mesh
765771

772+
# Reshape pixel values to combine batch and num_tiles dimensions
766773
b, t, c, h, w = pixel_values.shape
767774
pixel_values = jnp.reshape(pixel_values, [b * t, c, h, w])
768775

src/MaxText/layers/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def __call__(
116116
decoder_positions: jnp.ndarray,
117117
decoder_segment_ids=None,
118118
encoder_images: None | jnp.ndarray = None,
119+
encoder_image_masks: None | jnp.ndarray = None,
119120
enable_dropout=True,
120121
model_mode=MODEL_MODE_TRAIN,
121122
previous_chunk=None,
@@ -162,6 +163,7 @@ def __call__(
162163
page_state=page_state,
163164
bidirectional_mask=bidirectional_mask,
164165
image_embeddings=image_embeddings,
166+
image_masks=encoder_image_masks,
165167
)
166168

167169
# If we are initializing the model AND MTP is enabled, we must create
@@ -353,6 +355,7 @@ def __call__(
353355
decoder_segment_ids=None,
354356
cache=None,
355357
encoder_images: jax.Array | None = None,
358+
encoder_image_masks: jax.Array | None = None,
356359
enable_dropout=True,
357360
model_mode=MODEL_MODE_TRAIN,
358361
previous_chunk=None,
@@ -413,6 +416,7 @@ def __call__(
413416
page_state=page_state,
414417
bidirectional_mask=bidirectional_mask,
415418
image_embeddings=image_embeddings,
419+
image_masks=encoder_image_masks,
416420
)
417421

418422
# Materialize hidden state when vocab tiling is enabled
@@ -493,6 +497,7 @@ def __call__(
493497
decoder_positions: jnp.ndarray,
494498
decoder_segment_ids=None,
495499
encoder_images: None | jnp.ndarray = None,
500+
encoder_image_masks: None | jnp.ndarray = None,
496501
enable_dropout=True,
497502
model_mode=MODEL_MODE_TRAIN,
498503
previous_chunk=None,
@@ -535,6 +540,7 @@ def __call__(
535540
decoder_positions=decoder_positions,
536541
decoder_segment_ids=decoder_segment_ids,
537542
encoder_images=encoder_images,
543+
encoder_image_masks=encoder_image_masks,
538544
enable_dropout=enable_dropout,
539545
model_mode=model_mode,
540546
previous_chunk=previous_chunk,
@@ -552,6 +558,7 @@ def __call__(
552558
decoder_positions=decoder_positions,
553559
decoder_segment_ids=decoder_segment_ids,
554560
encoder_images=encoder_images,
561+
encoder_image_masks=encoder_image_masks,
555562
enable_dropout=enable_dropout,
556563
model_mode=model_mode,
557564
previous_chunk=previous_chunk,

src/MaxText/maxengine.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -327,17 +327,17 @@ def quantize_params(self, state, rng: PRNGKeyType | None = None):
327327

328328
@jax.jit
329329
def model_apply(_p, _rng):
330+
image_shape = multimodal_utils.get_dummy_image_shape_for_init(
331+
self.config.model_name, batch_size=self.config.micro_batch_size_to_train_on
332+
)
330333
return self.model.apply(
331334
_p | {"aqt": {}},
332335
jnp.ones((1, self.config.max_prefill_predict_length), dtype=jnp.int32),
333336
jnp.ones((1, self.config.max_prefill_predict_length), dtype=jnp.int32),
334-
encoder_images=jnp.ones(
335-
multimodal_utils.get_dummy_image_shape_for_init(
336-
self.config.model_name, batch_size=self.config.micro_batch_size_to_train_on
337-
),
338-
dtype=jnp.float32,
339-
)
340-
if self.config.use_multimodal
337+
encoder_images=jnp.ones(image_shape, dtype=jnp.float32) if self.config.use_multimodal else None,
338+
# encoder_image_masks indicates valid tiles if image tiling + padding is used in vision encoder input.
339+
encoder_image_masks=jnp.ones(image_shape[:2], dtype=jnp.int32)
340+
if self.config.use_multimodal and "llama4" in self.config.model_name
341341
else None,
342342
decoder_segment_ids=jnp.zeros((1, self.config.max_prefill_predict_length), dtype=jnp.int32),
343343
enable_dropout=False,
@@ -412,6 +412,7 @@ def _prefill_jit(
412412
existing_prefix: ExistingPrefix | None = None,
413413
padded_tokens: jax.Array,
414414
images: jax.Array | None = None,
415+
image_masks: jax.Array | None = None,
415416
true_length: int,
416417
sampler: Callable[[Any], Any] | None = None, # pylint: disable=unused-argument
417418
rng: PRNGKeyType | None = None,
@@ -479,9 +480,11 @@ def _prefill_jit(
479480
if images.ndim == 3:
480481
# For Gemma3 single image, add batch and image count dimensions
481482
images = images[jnp.newaxis, jnp.newaxis, ...]
483+
image_masks = image_masks[jnp.newaxis, jnp.newaxis, ...] if image_masks is not None else None
482484
elif images.ndim == 4:
483485
# add batch dimension
484486
images = images[jnp.newaxis, ...]
487+
image_masks = image_masks[jnp.newaxis, ...] if image_masks is not None else None
485488

486489
# sequence_indicator will be concatenated to existing_prefix decoder_segment_ids
487490
start_to_n = jnp.arange(start_position, start_position + input_tokens.shape[1])
@@ -496,6 +499,7 @@ def _prefill_jit(
496499
input_tokens,
497500
positions,
498501
encoder_images=images,
502+
encoder_image_masks=image_masks,
499503
decoder_segment_ids=sequence_indicator,
500504
enable_dropout=False,
501505
model_mode=MODEL_MODE_PREFILL,
@@ -570,6 +574,7 @@ def prefill(
570574
existing_prefix: ExistingPrefix | None = None,
571575
padded_tokens: jax.Array,
572576
images: jax.Array | None = None,
577+
image_masks: jax.Array | None = None,
573578
true_length: int,
574579
sampler: Callable[[Any], Any] | None = None, # pylint: disable=unused-argument
575580
rng: PRNGKeyType | None = None,
@@ -602,6 +607,7 @@ def prefill(
602607
existing_prefix=existing_prefix,
603608
padded_tokens=padded_tokens,
604609
images=images,
610+
image_masks=image_masks,
605611
sampler=sampler,
606612
true_length=true_length,
607613
page_state=self.page_state, # Pass current page state

src/MaxText/maxtext_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def get_shaped_batch(config):
9191
config.model_name, batch_size=config.micro_batch_size_to_train_on
9292
)
9393
shaped_batch["images"] = jax.ShapeDtypeStruct(image_shape, jnp.int32)
94+
shaped_batch["image_masks"] = jax.ShapeDtypeStruct(image_shape[:2], jnp.int32)
9495
return shaped_batch
9596

9697

0 commit comments

Comments
 (0)