Skip to content

Commit 961f8f7

Browse files
committed
Fix save/load, cross-backend, and multi-image bugs in Mistral3
- Add missing build() to the vision encoder/projector layers, fixing weight reinitialization on save/load and a JAX param-count mismatch. - Make Mistral3ImageConverter work inside tf.data on all three backends by using native tf ops for the graph path instead of keras.ops. - Rework placeholder_indices from a single flattened batch index into per-row local positions, so prompts in the same batch can now have different numbers of images. - Fill in preset kaggle handles/params, now that the checkpoints are uploaded. - Round out test coverage across the model (backbone, causal LM, preprocessor, tokenizer, image converter, vision encoder).
1 parent 60192af commit 961f8f7

11 files changed

Lines changed: 574 additions & 80 deletions

keras_hub/src/models/mistral3/mistral3_backbone.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,9 @@ def __init__(
182182
image_sizes_input = keras.Input(
183183
shape=(2,), dtype="int32", name="image_sizes"
184184
)
185-
# Flat positions of image placeholder tokens in the flattened
186-
# `(batch * seq_length,)` sequence. Computed on the host (e.g. via
187-
# `compute_image_placeholder_indices`) rather than derived in-graph,
188-
# since a `nonzero`-style lookup has a data-dependent output shape
189-
# that is incompatible with `jax.jit` tracing.
185+
# Each example's own local image placeholder token positions,
186+
# `-1`-padded to the batch's max count; see
187+
# `compute_image_placeholder_indices`.
190188
placeholder_indices_input = keras.Input(
191189
shape=(None,),
192190
dtype="int32",

keras_hub/src/models/mistral3/mistral3_backbone_test.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
2+
import pytest
23
from keras import ops
34

45
from keras_hub.src.models.mistral3.mistral3_backbone import Mistral3Backbone
@@ -70,9 +71,7 @@ def setUp(self):
7071
np.random.rand(2, 3, 8, 8).astype("float32")
7172
),
7273
"image_sizes": ops.array([[8, 8], [8, 8]], dtype="int32"),
73-
"placeholder_indices": ops.reshape(
74-
ops.convert_to_tensor(placeholder_indices), (2, 4)
75-
),
74+
"placeholder_indices": ops.convert_to_tensor(placeholder_indices),
7675
}
7776

7877
def test_backbone_basics(self):
@@ -96,3 +95,62 @@ def test_backbone_basics(self):
9695
# its own vision-encoder-bearing backbone.
9796
run_quantization_check=False,
9897
)
98+
99+
@pytest.mark.large
100+
def test_saved_model(self):
101+
self.run_model_saving_test(
102+
cls=Mistral3Backbone,
103+
init_kwargs=self.init_kwargs,
104+
input_data=self.input_data,
105+
)
106+
107+
def test_variable_images_per_prompt(self):
108+
# One prompt with one image, one with two.
109+
token_ids = ops.array(
110+
[
111+
[self.image_token_index] * 4 + [3, 0, 0, 0, 0],
112+
[self.image_token_index] * 8 + [4],
113+
],
114+
dtype="int32",
115+
)
116+
placeholder_indices = compute_image_placeholder_indices(
117+
token_ids, image_token_index=self.image_token_index
118+
)
119+
input_data = {
120+
"token_ids": token_ids,
121+
"padding_mask": ops.ones((2, 9), dtype="int32"),
122+
"pixel_values": ops.convert_to_tensor(
123+
np.random.rand(3, 3, 8, 8).astype("float32")
124+
),
125+
"image_sizes": ops.array([[8, 8], [8, 8], [8, 8]], dtype="int32"),
126+
"placeholder_indices": ops.convert_to_tensor(placeholder_indices),
127+
}
128+
model = Mistral3Backbone(**self.init_kwargs)
129+
output = model(input_data)
130+
self.assertEqual(
131+
ops.shape(output),
132+
(2, 9, self.text_init_kwargs["hidden_dim"]),
133+
)
134+
135+
def test_num_parameters(self):
136+
model = Mistral3Backbone(**self.init_kwargs)
137+
self.assertEqual(model.count_params(), 4016)
138+
self.assertEqual(len(model.layers), 11)
139+
140+
@pytest.mark.kaggle_key_required
141+
@pytest.mark.extra_large
142+
def test_all_presets(self):
143+
token_ids = ops.array([[1, 1824, 349, 524, 11234, 28804]])
144+
input_data = {
145+
"token_ids": token_ids,
146+
"padding_mask": ops.ones_like(token_ids),
147+
"pixel_values": ops.zeros((0, 3, 14, 14), dtype="float32"),
148+
"image_sizes": ops.zeros((0, 2), dtype="int32"),
149+
"placeholder_indices": ops.zeros((1, 0), dtype="int32"),
150+
}
151+
for preset in Mistral3Backbone.presets:
152+
self.run_preset_test(
153+
cls=Mistral3Backbone,
154+
preset=preset,
155+
input_data=input_data,
156+
)

keras_hub/src/models/mistral3/mistral3_causal_lm_preprocessor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ def call(
282282
placeholder_indices = compute_image_placeholder_indices(
283283
keras.ops.convert_to_numpy(model_token_ids),
284284
self.tokenizer.image_placeholder_token_id,
285-
)[None, :]
285+
)
286286

287287
out_x = {
288288
"token_ids": model_token_ids,
@@ -351,7 +351,7 @@ def generate_preprocess(
351351
placeholder_indices = compute_image_placeholder_indices(
352352
keras.ops.convert_to_numpy(token_ids),
353353
self.tokenizer.image_placeholder_token_id,
354-
)[None, :]
354+
)
355355
out_x["pixel_values"] = pixel_values
356356
out_x["image_sizes"] = image_sizes
357357
out_x["placeholder_indices"] = placeholder_indices

keras_hub/src/models/mistral3/mistral3_causal_lm_preprocessor_test.py

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import numpy as np
2+
import pytest
3+
from keras import ops
24

35
from keras_hub.src.models.mistral3.mistral3_causal_lm_preprocessor import (
46
Mistral3CausalLMPreprocessor,
@@ -7,11 +9,61 @@
79
Mistral3ImageConverter,
810
)
911
from keras_hub.src.models.mistral3.mistral3_tokenizer import Mistral3Tokenizer
10-
from keras_hub.src.models.mistral3.mistral3_tokenizer_test import (
11-
_tekken_vision_init_kwargs,
12-
)
1312
from keras_hub.src.tests.test_case import TestCase
1413

14+
# A tiktoken-style split pattern, matching the Tekken format.
15+
_TEKKEN_SPLIT_PATTERN = (
16+
r"[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*"
17+
r"[\p{Ll}\p{Lm}\p{Lo}\p{M}]+|\p{N}| ?[^\s\p{L}\p{N}]+"
18+
r"[\r\n/]*|\s*[\r\n]+|\s+(?!\S)|\s+"
19+
)
20+
21+
22+
def _bytes_to_unicode():
23+
bs = (
24+
list(range(ord("!"), ord("~") + 1))
25+
+ list(range(ord("¡"), ord("¬") + 1))
26+
+ list(range(ord("®"), ord("ÿ") + 1))
27+
)
28+
cs = bs[:]
29+
n = 0
30+
for b in range(2**8):
31+
if b not in bs:
32+
bs.append(b)
33+
cs.append(2**8 + n)
34+
n += 1
35+
return {b: chr(c) for b, c in zip(bs, cs)}
36+
37+
38+
def _tekken_vision_init_kwargs():
39+
"""Build a tiny Tekken (byte-level BPE) vocabulary with image tokens."""
40+
byte_encoder = _bytes_to_unicode()
41+
special_tokens = [
42+
"<unk>",
43+
"<s>",
44+
"</s>",
45+
"<pad>",
46+
"[INST]",
47+
"[IMG]",
48+
"[IMG_BREAK]",
49+
"[IMG_END]",
50+
]
51+
vocabulary = {token: i for i, token in enumerate(special_tokens)}
52+
offset = len(special_tokens)
53+
for i in range(256):
54+
vocabulary[byte_encoder[i]] = offset + i
55+
merges = []
56+
next_id = offset + 256
57+
for a, b in [("t", "h"), ("th", "e"), ("i", "n")]:
58+
vocabulary[a + b] = next_id
59+
merges.append(f"{a} {b}")
60+
next_id += 1
61+
return {
62+
"vocabulary": vocabulary,
63+
"merges": merges,
64+
"split_pattern": _TEKKEN_SPLIT_PATTERN,
65+
}
66+
1567

1668
class Mistral3CausalLMPreprocessorTest(TestCase):
1769
def setUp(self):
@@ -26,6 +78,14 @@ def setUp(self):
2678
"spatial_merge_size": 1,
2779
}
2880

81+
def test_preprocessor_basics(self):
82+
input_data = {"prompts": ["the tin", "in the"]}
83+
self.run_preprocessor_test(
84+
cls=Mistral3CausalLMPreprocessor,
85+
init_kwargs=self.init_kwargs,
86+
input_data=input_data,
87+
)
88+
2989
def test_generate_preprocess_with_images(self):
3090
preprocessor = Mistral3CausalLMPreprocessor(**self.init_kwargs)
3191
image = np.zeros((8, 8, 3), dtype="float32")
@@ -40,12 +100,40 @@ def test_generate_preprocess_with_images(self):
40100
"placeholder_indices",
41101
):
42102
self.assertIn(key, x)
43-
# An 8x8 image with `patch_size=4`, `spatial_merge_size=1` expands
44-
# to a 2x2 grid of placeholder tokens.
45103
token_ids = np.array(x["token_ids"])
46104
num_placeholders = int(
47105
np.sum(
48106
token_ids == preprocessor.tokenizer.image_placeholder_token_id
49107
)
50108
)
51109
self.assertEqual(num_placeholders, 4)
110+
111+
def test_generate_preprocess_text_only(self):
112+
preprocessor = Mistral3CausalLMPreprocessor(**self.init_kwargs)
113+
x = preprocessor.generate_preprocess("the tin")
114+
self.assertEqual(set(x.keys()), {"token_ids", "padding_mask"})
115+
116+
def test_generate_postprocess(self):
117+
preprocessor = Mistral3CausalLMPreprocessor(**self.init_kwargs)
118+
input_data = {
119+
"token_ids": ops.array([1, 265, 40, 124, 266, 0, 0, 0]),
120+
"padding_mask": ops.array(
121+
[True, True, True, True, True, False, False, False]
122+
),
123+
}
124+
x = preprocessor.generate_postprocess(input_data)
125+
self.assertEqual(x, "the tin")
126+
127+
@pytest.mark.kaggle_key_required
128+
@pytest.mark.extra_large
129+
def test_all_presets(self):
130+
input_data = {
131+
"prompts": ["Describe the image. [IMG]"],
132+
"images": [[self.load_test_image()]],
133+
}
134+
for preset in Mistral3CausalLMPreprocessor.presets:
135+
self.run_preset_test(
136+
cls=Mistral3CausalLMPreprocessor,
137+
preset=preset,
138+
input_data=input_data,
139+
)

0 commit comments

Comments
 (0)