Skip to content

Commit ba487a4

Browse files
yassienashrafwasfyyassienashrafwasfy
andauthored
Add blip2 (keras-team#2699)
* Add BLIP-2 and InstructBLIP models * Split BLIP-2 Flan-T5 into a BLIP2Seq2SeqLM task The Flan-T5 variant is encoder-decoder but was running through BLIP2CausalLM via is_encoder_decoder branching, which also double-encoded the image (vision encoder + Q-Former ran once in _encode_images() and again directly in generate_step). Add BLIP2Seq2SeqLM(Seq2SeqLM) and BLIP2Seq2SeqLMPreprocessor following the T5Gemma(2)Seq2SeqLM convention: encoder_token_ids/decoder_token_ids inputs, a single vision/Q-Former encode in call_encoder, and a clean seq2seq generate_step. BLIP2CausalLM is now strictly decoder-only (OPT/Vicuna), and BLIP2Backbone exposes encoder_*/padding_mask names for the encoder-decoder LM. T5 has no KV cache, so the decoder recomputes each step. * Package BLIP-2 Flan-T5 presets as BLIP2Seq2SeqLM * Resolving Maintainer comments * run pre commit hook * Resolving maintainer comments-2 * Remove the InstructBLIP-Vicuna variant from BLIP-2 * Run the conversion tool on GPU instead of forcing CPU * Keep TensorFlow on CPU during BLIP-2 checkpoint conversion The conversion runs on the torch backend, but keras-hub tokenizers use TensorFlow string ops. On newer GPUs (e.g. Blackwell / compute capability 12.0) some TF builds have no matching CUDA kernels and fail to JIT them from PTX, crashing the OPT BPE tokenizer's tf.cast with CUDA_ERROR_INVALID_PTX. Hide the GPU from TensorFlow so its tokenizer string ops stay on CPU while torch/Keras keep the GPU for the model. * force fp32 matmuls in BLIP-2 conversion disable TF32 for GPU matmuls/convs so the parity check against the float32 HuggingFace model stays tight (~1e-4) instead of drifting to ~1e-3 from TF32's reduced mantissa. * fix pytorch and tensorflow kernel kill issue * Mark heavyweight BLIP-2 tests as extra_large to fix GPU CI OOM The class detection tests build the full blip2-opt-2.7b and blip2-flan-t5-xl architectures (~4B params each, twice per test), which exhausts the 16GB GPU in the large test job and cascades OOM and corrupted Keras stateless-scope state into every test that runs after them. Also move test_xl_config_instantiates (~2.8B params) out of the large job since it bloats the TF allocator pool for the whole session. * Mark BLIP-2 saved model tests as large per repo convention The save/load round-trip tests in the backbone, causal LM, and seq2seq LM test files were unmarked, so they ran in the small presubmit. Mark them @pytest.mark.large to match how other models (Gemma, Mistral) and the other BLIP-2 test files gate these slow tests. --------- Co-authored-by: yassienashrafwasfy <21-101112@students.eui.edu.eg>
1 parent e1b4c12 commit ba487a4

32 files changed

Lines changed: 4999 additions & 0 deletions

keras_hub/api/layers/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@
7272
from keras_hub.src.models.basnet.basnet_image_converter import (
7373
BASNetImageConverter as BASNetImageConverter,
7474
)
75+
from keras_hub.src.models.blip2.blip2_image_converter import (
76+
BLIP2ImageConverter as BLIP2ImageConverter,
77+
)
7578
from keras_hub.src.models.clip.clip_image_converter import (
7679
CLIPImageConverter as CLIPImageConverter,
7780
)

keras_hub/api/models/__init__.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,39 @@
7676
from keras_hub.src.models.bert.bert_tokenizer import (
7777
BertTokenizer as BertTokenizer,
7878
)
79+
from keras_hub.src.models.blip2.blip2_backbone import (
80+
BLIP2Backbone as BLIP2Backbone,
81+
)
82+
from keras_hub.src.models.blip2.blip2_causal_lm import (
83+
BLIP2CausalLM as BLIP2CausalLM,
84+
)
85+
from keras_hub.src.models.blip2.blip2_causal_lm_preprocessor import (
86+
BLIP2CausalLMPreprocessor as BLIP2CausalLMPreprocessor,
87+
)
88+
from keras_hub.src.models.blip2.blip2_custom_opt import (
89+
BLIP2CustomOPT as BLIP2CustomOPT,
90+
)
91+
from keras_hub.src.models.blip2.blip2_flan_t5_lm import (
92+
BLIP2FlanT5 as BLIP2FlanT5,
93+
)
94+
from keras_hub.src.models.blip2.blip2_flan_t5_tokenizer import (
95+
BLIP2FlanT5Tokenizer as BLIP2FlanT5Tokenizer,
96+
)
97+
from keras_hub.src.models.blip2.blip2_opt_tokenizer import (
98+
BLIP2OPTTokenizer as BLIP2OPTTokenizer,
99+
)
100+
from keras_hub.src.models.blip2.blip2_qformer import (
101+
BLIP2QFormer as BLIP2QFormer,
102+
)
103+
from keras_hub.src.models.blip2.blip2_seq_2_seq_lm import (
104+
BLIP2Seq2SeqLM as BLIP2Seq2SeqLM,
105+
)
106+
from keras_hub.src.models.blip2.blip2_seq_2_seq_lm_preprocessor import (
107+
BLIP2Seq2SeqLMPreprocessor as BLIP2Seq2SeqLMPreprocessor,
108+
)
109+
from keras_hub.src.models.blip2.blip2_vision_encoder import (
110+
BLIP2VisionEncoder as BLIP2VisionEncoder,
111+
)
79112
from keras_hub.src.models.bloom.bloom_backbone import (
80113
BloomBackbone as BloomBackbone,
81114
)

keras_hub/api/tokenizers/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
from keras_hub.src.models.bert.bert_tokenizer import (
1414
BertTokenizer as BertTokenizer,
1515
)
16+
from keras_hub.src.models.blip2.blip2_flan_t5_tokenizer import (
17+
BLIP2FlanT5Tokenizer as BLIP2FlanT5Tokenizer,
18+
)
19+
from keras_hub.src.models.blip2.blip2_opt_tokenizer import (
20+
BLIP2OPTTokenizer as BLIP2OPTTokenizer,
21+
)
1622
from keras_hub.src.models.bloom.bloom_tokenizer import (
1723
BloomTokenizer as BloomTokenizer,
1824
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from keras_hub.src.models.blip2.blip2_backbone import BLIP2Backbone
2+
from keras_hub.src.models.blip2.blip2_presets import backbone_presets
3+
from keras_hub.src.utils.preset_utils import register_presets
4+
5+
register_presets(backbone_presets, BLIP2Backbone)
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import keras
2+
3+
from keras_hub.src.api_export import keras_hub_export
4+
from keras_hub.src.models.backbone import Backbone
5+
6+
7+
@keras_hub_export("keras_hub.models.BLIP2Backbone")
8+
class BLIP2Backbone(Backbone):
9+
"""BLIP-2 core network.
10+
11+
BLIP-2 is a vision-language model that connects a frozen image encoder
12+
and a frozen large language model (LLM) through a lightweight trainable
13+
Querying Transformer (Q-Former). The Q-Former distills visual information
14+
into a fixed number of query embeddings which are then fed as a soft visual
15+
prompt to the language model.
16+
17+
The forward pass follows three stages:
18+
1. A `BLIP2VisionEncoder` (ViT) maps raw images to patch features.
19+
2. A `BLIP2QFormer` cross-attends learned query tokens against those
20+
patch features and produces a compact set of visual embeddings.
21+
3. A language model (OPT or Flan-T5) receives the query embeddings
22+
prepended to its token sequence and generates text.
23+
24+
When `vision_encoder` is `None` the backbone operates in text-only mode:
25+
the Q-Former is bypassed and the language model receives only token ids.
26+
27+
For a higher-level text-generation interface see
28+
`keras_hub.models.BLIP2CausalLM`.
29+
30+
Args:
31+
vision_encoder: A `keras_hub.models.BLIP2VisionEncoder` instance. Pass
32+
`None` for a text-only backbone.
33+
qformer: A `keras_hub.models.BLIP2QFormer` instance. Pass `None` when
34+
`vision_encoder` is `None`.
35+
language_model: The language model instance (e.g. `BLIP2CustomOPT`
36+
or `BLIP2FlanT5`).
37+
dtype: string or `keras.mixed_precision.DTypePolicy`. Dtype used for
38+
model computations and weights. Defaults to `None` (Keras global
39+
default).
40+
**kwargs: Additional keyword arguments forwarded to the base
41+
`Backbone` / `keras.Model`.
42+
43+
Example:
44+
```python
45+
# Text-only (no vision encoder)
46+
backbone = keras_hub.models.BLIP2Backbone(
47+
vision_encoder=None,
48+
qformer=None,
49+
language_model=my_opt,
50+
)
51+
output = backbone({"token_ids": token_ids, "padding_mask": mask})
52+
53+
# Full vision-language backbone
54+
backbone = keras_hub.models.BLIP2Backbone.from_preset("blip2_opt_2.7b")
55+
output = backbone({
56+
"images": images, # (B, H, W, 3)
57+
"token_ids": token_ids, # (B, seq_len)
58+
"padding_mask": mask, # (B, seq_len)
59+
})
60+
```
61+
"""
62+
63+
def __init__(
64+
self,
65+
vision_encoder,
66+
qformer,
67+
language_model,
68+
dtype=None,
69+
**kwargs,
70+
):
71+
self.vision_encoder = vision_encoder
72+
self.qformer = qformer
73+
self.language_model = language_model
74+
75+
multimodal = self.vision_encoder is not None
76+
77+
# Encoder-decoder language models (Flan-T5) follow the keras-hub
78+
# seq2seq convention and name their encoder inputs
79+
# `encoder_token_ids` / `encoder_padding_mask`; decoder-only language
80+
# models (OPT) keep the plain `token_ids` / `padding_mask`.
81+
is_encoder_decoder = hasattr(
82+
self.language_model, "encoder_transformer_layers"
83+
)
84+
if is_encoder_decoder:
85+
token_ids_name, padding_mask_name = (
86+
"encoder_token_ids",
87+
"encoder_padding_mask",
88+
)
89+
else:
90+
token_ids_name, padding_mask_name = "token_ids", "padding_mask"
91+
92+
token_ids_input = keras.Input(
93+
shape=(None,), dtype="int32", name=token_ids_name
94+
)
95+
padding_mask_input = keras.Input(
96+
shape=(None,), dtype="int32", name=padding_mask_name
97+
)
98+
inputs = {
99+
token_ids_name: token_ids_input,
100+
padding_mask_name: padding_mask_input,
101+
}
102+
103+
if multimodal:
104+
raw_image_size = self.vision_encoder.image_size
105+
if isinstance(raw_image_size, (tuple, list)):
106+
image_h, image_w = (
107+
int(raw_image_size[0]),
108+
int(raw_image_size[1]),
109+
)
110+
else:
111+
image_h = image_w = int(raw_image_size)
112+
113+
images_input = keras.Input(
114+
shape=(image_h, image_w, 3),
115+
dtype="float32",
116+
name="images",
117+
)
118+
inputs["images"] = images_input
119+
120+
patch_features = self.vision_encoder(images_input)
121+
query_embeddings = self.qformer(patch_features)
122+
else:
123+
query_embeddings = None
124+
125+
# The language model itself always consumes `token_ids` /
126+
# `padding_mask` for its (encoder) input; the backbone maps the
127+
# externally exposed names onto these.
128+
lm_inputs = {
129+
"token_ids": token_ids_input,
130+
"padding_mask": padding_mask_input,
131+
}
132+
133+
if is_encoder_decoder:
134+
decoder_token_ids_input = keras.Input(
135+
shape=(None,), dtype="int32", name="decoder_token_ids"
136+
)
137+
decoder_padding_mask_input = keras.Input(
138+
shape=(None,), dtype="int32", name="decoder_padding_mask"
139+
)
140+
inputs["decoder_token_ids"] = decoder_token_ids_input
141+
inputs["decoder_padding_mask"] = decoder_padding_mask_input
142+
lm_inputs["decoder_token_ids"] = decoder_token_ids_input
143+
lm_inputs["decoder_padding_mask"] = decoder_padding_mask_input
144+
145+
if query_embeddings is not None:
146+
lm_inputs["qformer_features"] = query_embeddings
147+
148+
output = self.language_model(lm_inputs)
149+
150+
super().__init__(
151+
inputs=inputs,
152+
outputs=output,
153+
dtype=dtype,
154+
**kwargs,
155+
)
156+
157+
self.multimodal = multimodal
158+
159+
@property
160+
def token_embedding(self):
161+
"""The token embedding layer of the language model."""
162+
return self.language_model.token_embedding
163+
164+
@property
165+
def num_query_tokens(self):
166+
"""Number of Q-Former query tokens (0 in text-only mode)."""
167+
return self.qformer.num_query_tokens if self.qformer is not None else 0
168+
169+
@property
170+
def qformer_hidden_dim(self):
171+
"""Hidden dimensionality of the Q-Former (0 in text-only mode)."""
172+
return self.qformer.hidden_dim if self.qformer is not None else 0
173+
174+
def get_config(self):
175+
config = super().get_config()
176+
config.update(
177+
{
178+
"vision_encoder": (
179+
keras.layers.serialize(self.vision_encoder)
180+
if self.vision_encoder is not None
181+
else None
182+
),
183+
"qformer": (
184+
keras.layers.serialize(self.qformer)
185+
if self.qformer is not None
186+
else None
187+
),
188+
"language_model": keras.layers.serialize(self.language_model),
189+
}
190+
)
191+
return config
192+
193+
@classmethod
194+
def from_config(cls, config):
195+
config = dict(config)
196+
for key in ("vision_encoder", "qformer"):
197+
if config.get(key) is not None:
198+
config[key] = keras.layers.deserialize(config[key])
199+
config["language_model"] = keras.layers.deserialize(
200+
config["language_model"]
201+
)
202+
return cls(**config)
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import numpy as np
2+
import pytest
3+
4+
from keras_hub.src.models.blip2.blip2_backbone import BLIP2Backbone
5+
from keras_hub.src.models.blip2.blip2_custom_opt import BLIP2CustomOPT
6+
from keras_hub.src.models.blip2.blip2_qformer import BLIP2QFormer
7+
from keras_hub.src.models.blip2.blip2_vision_encoder import BLIP2VisionEncoder
8+
from keras_hub.src.tests.test_case import TestCase
9+
10+
11+
class BLIP2BackboneTest(TestCase):
12+
def setUp(self):
13+
self.batch_size = 2
14+
self.image_size = 32
15+
self.seq_length = 5
16+
self.num_query_tokens = 2
17+
self.hidden_dim = 4
18+
19+
vision_encoder = BLIP2VisionEncoder(
20+
image_size=self.image_size,
21+
patch_size=4,
22+
num_layers=2,
23+
num_heads=2,
24+
hidden_dim=8,
25+
intermediate_dim=16,
26+
use_patch_bias=True,
27+
use_class_token=True,
28+
use_mha_bias=True,
29+
use_mlp_bias=True,
30+
dropout_rate=0.0,
31+
layer_norm_epsilon=1e-6,
32+
initializer_range=0.02,
33+
dtype="float32",
34+
)
35+
qformer = BLIP2QFormer(
36+
num_query_tokens=self.num_query_tokens,
37+
num_layers=2,
38+
num_heads=2,
39+
hidden_dim=self.hidden_dim,
40+
intermediate_dim=8,
41+
vision_dim=8,
42+
cross_attention_frequency=1,
43+
dropout=0.0,
44+
layer_norm_epsilon=1e-6,
45+
dtype="float32",
46+
)
47+
language_model = BLIP2CustomOPT(
48+
vocabulary_size=14,
49+
num_layers=2,
50+
num_heads=2,
51+
hidden_dim=self.hidden_dim,
52+
intermediate_dim=8,
53+
num_query_tokens=self.num_query_tokens,
54+
qformer_hidden_dim=self.hidden_dim,
55+
max_sequence_length=10,
56+
dropout=0.0,
57+
language_projection=None,
58+
dtype="float32",
59+
)
60+
61+
self.init_kwargs = {
62+
"vision_encoder": vision_encoder,
63+
"qformer": qformer,
64+
"language_model": language_model,
65+
}
66+
self.input_data = {
67+
"images": np.ones(
68+
(self.batch_size, self.image_size, self.image_size, 3),
69+
dtype="float32",
70+
),
71+
"token_ids": np.ones(
72+
(self.batch_size, self.seq_length), dtype="int32"
73+
),
74+
"padding_mask": np.ones(
75+
(self.batch_size, self.seq_length), dtype="bool"
76+
),
77+
}
78+
79+
def test_backbone_basics(self):
80+
self.run_backbone_test(
81+
cls=BLIP2Backbone,
82+
init_kwargs=self.init_kwargs,
83+
input_data=self.input_data,
84+
expected_output_shape=(
85+
self.batch_size,
86+
self.num_query_tokens + self.seq_length,
87+
self.hidden_dim,
88+
),
89+
variable_length_data=[self.input_data],
90+
run_quantization_check=False,
91+
run_mixed_precision_check=False,
92+
)
93+
94+
def test_architecture_characteristics(self):
95+
backbone = BLIP2Backbone(**self.init_kwargs)
96+
self.assertEqual(backbone.num_query_tokens, self.num_query_tokens)
97+
self.assertEqual(backbone.qformer.hidden_dim, self.hidden_dim)
98+
self.assertEqual(backbone.language_model.hidden_dim, self.hidden_dim)
99+
100+
@pytest.mark.large
101+
def test_saved_model(self):
102+
self.run_model_saving_test(
103+
cls=BLIP2Backbone,
104+
init_kwargs=self.init_kwargs,
105+
input_data=self.input_data,
106+
)
107+
108+
@pytest.mark.kaggle_key_required
109+
@pytest.mark.extra_large
110+
def test_smallest_preset(self):
111+
self.run_preset_test(
112+
cls=BLIP2Backbone,
113+
preset="blip2_base",
114+
input_data=self.input_data,
115+
)
116+
117+
@pytest.mark.kaggle_key_required
118+
@pytest.mark.extra_large
119+
def test_all_presets(self):
120+
for preset in BLIP2Backbone.presets:
121+
self.run_preset_test(
122+
cls=BLIP2Backbone,
123+
preset=preset,
124+
input_data=self.input_data,
125+
)

0 commit comments

Comments
 (0)