Repository: keras-team/keras-nlp
Pull request #1404 (2024-01-18 22:08:06)

Add Alibi bias layer
This PR adds a layer that generates the alibi bias as discussed in keras-team/keras-nlp#1382



Patch:
diff --git a/keras_nlp/layers/modeling/alibi_bias.py b/keras_nlp/layers/modeling/alibi_bias.py
new file mode 100644
index 00000000..8a66ad05
--- /dev/null
+++ b/keras_nlp/layers/modeling/alibi_bias.py
@@ -0,0 +1,137 @@
+# Copyright 2023 The KerasNLP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import math
+
+from keras_nlp.api_export import keras_nlp_export
+from keras_nlp.backend import keras
+from keras_nlp.backend import ops
+
+
+@keras_nlp_export("keras_nlp.layers.AlibiBias")
+class AlibiBias(keras.layers.Layer):
+    """A layer that adds the alibi bias to attention scores.
+
+    This layer adds the alibi bias to the attention scores. Alibi bias is a
+    linear, non-learned bias. Defined and formalized in
+    [Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation](https://arxiv.org/abs/2108.12409).
+
+    This layer takes as input the attention scores. and returns the attention
+    scores after adding the alibi bias to it. The output will have the same
+    shape as the input.
+
+    Args:
+        alibi_bias_max: int. This value will be used to compute the slope of
+            each head. The heads' slopes are a geometric sequence that starts at
+            `2**(-alibi_bias_max/num_heads)` and uses that same value as its
+            ratio. Defaults to 8.
+    Call arguments:
+        attention_scores: The result of multipying the query and the key of the
+            multi-head attention layer of the transformer to add alibi bias to
+            it. With shape `(batch_size, num_heads, query_length, key_length)`.
+
+    Examples:
+    ```python
+    query_length = 10
+    key_length = 10
+    num_heads = 4
+    batch_size = 2
+    hidden_dim = 8
+
+    # Create new alibi layer.
+    alibi_layer = keras_nlp.layers.AlibiBias()
+
+    query = np.zeros((batch_size, num_heads, query_length, hidden_dim))
+    key = np.zeros((batch_size, num_heads, hidden_dim, key_length))
+
+    attention_scores = keras.ops.matmul(query, key)
+
+    # Add alibi bias to attention scores.
+    attention_scores = alibi_layer(attention_scores)
+    ```
+
+    References:
+     - [Press et al., 2021](https://arxiv.org/abs/2108.12409)
+    """
+
+    def __init__(
+        self,
+        alibi_bias_max=8,
+        **kwargs,
+    ):
+        super().__init__(**kwargs)
+        self.alibi_bias_max = alibi_bias_max
+
+    def call(self, attention_scores):
+        shape = ops.shape(attention_scores)
+        if len(shape) != 4:
+            raise ValueError(
+                "Expected `attention_scores` shape to be "
+                "`(batch_size, num_heads, query_length, key_Length)`."
+                f" Recived shape={shape}"
+            )
+
+        key_length = shape[-1]
+        num_heads = shape[-3]
+
+        alibi_bias = self._get_alibi_bias(num_heads, key_length)
+
+        return ops.add(attention_scores, alibi_bias)
+
+    def _get_alibi_bias(self, num_heads, key_length):
+        slopes = ops.convert_to_tensor(
+            self._get_slopes(num_heads), dtype=self.compute_dtype
+        )
+        slopes = ops.expand_dims(slopes, 1)
+
+        seq_range = ops.expand_dims(ops.arange(1 - key_length, 1), 0)
+        seq_range = ops.cast(seq_range, dtype=self.compute_dtype)
+
+        alibi_bias = ops.multiply(slopes, seq_range)
+        alibi_bias = ops.expand_dims(alibi_bias, 1)
+
+        # return shape is `(1, num_heads, 1, key_length)`
+        return ops.expand_dims(alibi_bias, 0)
+
+    def _get_slopes(self, num_heads):
+        # this function is adopted from Alibi original implementation.
+        # https://github.com/ofirpress/attention_with_linear_biases/blob/a35aaca144e0eb6b789dfcb46784c4b8e31b7983/fairseq/models/transformer.py#L742
+        def get_slopes_power_of_2(n):
+            start = 2 ** (
+                -(2 ** -(math.log2(n) - math.log2(self.alibi_bias_max)))
+            )
+            ratio = start
+            return [start * ratio**i for i in range(n)]
+
+        if math.log2(num_heads).is_integer():
+            return get_slopes_power_of_2(num_heads)
+        else:
+            closest_power_of_2 = 2 ** math.floor(math.log2(num_heads))
+            return (
+                get_slopes_power_of_2(closest_power_of_2)
+                + self._get_slopes(2 * closest_power_of_2)[0::2][
+                    : num_heads - closest_power_of_2
+                ]
+            )
+
+    def compute_output_shape(self, input_shape):
+        return input_shape
+
+    def get_config(self):
+        config = super().get_config()
+        config.update(
+            {
+                "alibi_bias_max": self.alibi_bias_max,
+            }
+        )
+        return config
diff --git a/keras_nlp/layers/modeling/transformer_decoder.py b/keras_nlp/layers/modeling/transformer_decoder.py
index 3a3cda3f..cb7c7793 100644
--- a/keras_nlp/layers/modeling/transformer_decoder.py
+++ b/keras_nlp/layers/modeling/transformer_decoder.py
@@ -469,9 +469,11 @@ class TransformerDecoder(keras.layers.Layer):
                 batch_size,
                 input_length,
                 output_length,
-                0
-                if self_attention_cache_update_index is None
-                else self_attention_cache_update_index,
+                (
+                    0
+                    if self_attention_cache_update_index is None
+                    else self_attention_cache_update_index
+                ),
             )
             return (
                 ops.minimum(decoder_mask, causal_mask)
diff --git a/keras_nlp/models/__init__.py b/keras_nlp/models/__init__.py
index 30736594..7b898138 100644
--- a/keras_nlp/models/__init__.py
+++ b/keras_nlp/models/__init__.py
@@ -36,6 +36,7 @@ from keras_nlp.models.bert.bert_masked_lm_preprocessor import (
 from keras_nlp.models.bert.bert_preprocessor import BertPreprocessor
 from keras_nlp.models.bert.bert_tokenizer import BertTokenizer
 from keras_nlp.models.bloom.bloom_backbone import BloomBackbone
+from keras_nlp.models.bloom.bloom_tokenizer import BloomTokenizer
 from keras_nlp.models.deberta_v3.deberta_v3_backbone import DebertaV3Backbone
 from keras_nlp.models.deberta_v3.deberta_v3_classifier import (
     DebertaV3Classifier,
diff --git a/keras_nlp/models/bloom/bloom_attention.py b/keras_nlp/models/bloom/bloom_attention.py
index 7af2e7a3..366169d1 100644
--- a/keras_nlp/models/bloom/bloom_attention.py
+++ b/keras_nlp/models/bloom/bloom_attention.py
@@ -15,6 +15,7 @@ import math
 
 from keras_nlp.backend import keras
 from keras_nlp.backend import ops
+from keras_nlp.layers.modeling.alibi_bias import AlibiBias
 from keras_nlp.utils.keras_utils import clone_initializer
 
 
@@ -74,6 +75,8 @@ class BloomAttention(keras.layers.Layer):
         )
         self._value_dense.build(inputs_shape)
 
+        self._alibi_layer = AlibiBias()
+
         self._output_dense = keras.layers.Dense(
             hidden_dim,
             kernel_initializer=clone_initializer(self.kernel_initializer),
@@ -92,38 +95,6 @@ class BloomAttention(keras.layers.Layer):
 
         self.built = True
 
-    @staticmethod
-    def _build_alibi_tensor(num_heads, seq_length, alibi_bias_max=8):
-        # this function is adopted from fairseq
-        # https://github.com/ofirpress/attention_with_linear_biases/blob/a35aaca144e0eb6b789dfcb46784c4b8e31b7983/fairseq/models/transformer.py#L742
-        def get_slopes(n):
-            def get_slopes_power_of_2(n):
-                start = 2 ** (
-                    -(2 ** -(math.log2(n) - math.log2(alibi_bias_max)))
-                )
-                ratio = start
-                return [start * ratio**i for i in range(n)]
-
-            if math.log2(n).is_integer():
-                return get_slopes_power_of_2(n)
-            else:
-                closest_power_of_2 = 2 ** math.floor(math.log2(n))
-                return (
-                    get_slopes_power_of_2(closest_power_of_2)
-                    + get_slopes(2 * closest_power_of_2)[0::2][
-                        : n - closest_power_of_2
-                    ]
-                )
-
-        slopes = ops.convert_to_tensor(get_slopes(num_heads), dtype=float)
-        slopes = ops.expand_dims(slopes, 1)
-
-        alibi = slopes * ops.expand_dims(ops.arange(seq_length, dtype=float), 0)
-        alibi = ops.expand_dims(alibi, 1)
-        alibi = ops.expand_dims(alibi, 0)
-
-        return alibi
-
     def call(
         self,
         hidden_states,
@@ -163,20 +134,17 @@ class BloomAttention(keras.layers.Layer):
         # key   (batch_size, num_heads, head_dim, kv_length)
         key = ops.transpose(key, [0, 2, 3, 1])
 
-        alibi = self._build_alibi_tensor(
-            num_heads=self.num_heads, seq_length=seq_length
-        )
-
-        scores = (
-            ops.matmul(query, key) * self.inv_norm_factor + alibi
+        attention_scores = (
+            ops.matmul(query, key) * self.inv_norm_factor
         )  # [batch_size, num_heads, query_length, kv_length]
-
-        scores = self._softmax(scores, ops.expand_dims(attention_mask, 1))
-
-        scores = self._dropout_layer(scores)
+        attention_scores = self._alibi_layer(attention_scores)
+        attention_scores = self._softmax(
+            attention_scores, ops.expand_dims(attention_mask, 1)
+        )
+        attention_scores = self._dropout_layer(attention_scores)
 
         attention_output = ops.matmul(
-            scores, value
+            attention_scores, value
         )  # [batch_size, num_heads, query_length, head_dim]
 
         attention_output = ops.transpose(
diff --git a/keras_nlp/models/bloom/bloom_backbone.py b/keras_nlp/models/bloom/bloom_backbone.py
index e3d66998..2fd18ed7 100644
--- a/keras_nlp/models/bloom/bloom_backbone.py
+++ b/keras_nlp/models/bloom/bloom_backbone.py
@@ -11,11 +11,15 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+import copy
+
 from keras_nlp.api_export import keras_nlp_export
 from keras_nlp.backend import keras
 from keras_nlp.layers.modeling.reversible_embedding import ReversibleEmbedding
 from keras_nlp.models.backbone import Backbone
 from keras_nlp.models.bloom.bloom_decoder import BloomDecoder
+from keras_nlp.models.bloom.bloom_presets import backbone_presets
+from keras_nlp.utils.python_utils import classproperty
 
 
 def _bloom_kernel_initializer(stddev=0.02):
@@ -35,7 +39,8 @@ class BloomBackbone(Backbone):
     load preset architectures and weights, use the `from_preset()` constructor.
 
     Disclaimer: Pre-trained models are provided on an "as is" basis, without
-    warranties or conditions of any kind.
+    warranties or conditions of any kind. The underlying model is provided by a
+    third party and subject to a separate license, available [here](https://huggingface.co/spaces/bigscience/license).
 
     Args:
         vocabulary_size: int. The size of the token vocabulary.
@@ -58,6 +63,10 @@ class BloomBackbone(Backbone):
         "padding_mask": np.array([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0]]),
     }
 
+    # Pretrained BLOOM decoder.
+    model = keras_nlp.models.BloomBackbone.from_preset("bloom_560m_multi")
+    model(input_data)
+
     # Randomly initialized BLOOM decoder with a custom config.
     model = keras_nlp.models.BloomBackbone(
         vocabulary_size=10,
@@ -151,3 +160,7 @@ class BloomBackbone(Backbone):
             }
         )
         return config
+
+    @classproperty
+    def presets(cls):
+        return copy.deepcopy(backbone_presets)
diff --git a/keras_nlp/models/bloom/bloom_decoder.py b/keras_nlp/models/bloom/bloom_decoder.py
index b3f8b80d..a0c62a25 100644
--- a/keras_nlp/models/bloom/bloom_decoder.py
+++ b/keras_nlp/models/bloom/bloom_decoder.py
@@ -174,9 +174,11 @@ class BloomDecoder(keras.layers.Layer):
                 batch_size,
                 input_length,
                 output_length,
-                0
-                if attention_cache_update_index is None
-                else attention_cache_update_index,
+                (
+                    0
+                    if attention_cache_update_index is None
+                    else attention_cache_update_index
+                ),
             )
             return (
                 ops.minimum(decoder_mask, causal_mask)
@@ -202,3 +204,6 @@ class BloomDecoder(keras.layers.Layer):
             }
         )
         return config
+
+    def compute_output_shape(self, decoder_sequence_shape):
+        return decoder_sequence_shape
diff --git a/keras_nlp/models/bloom/bloom_presets.py b/keras_nlp/models/bloom/bloom_presets.py
new file mode 100644
index 00000000..d3e9c780
--- /dev/null
+++ b/keras_nlp/models/bloom/bloom_presets.py
@@ -0,0 +1,30 @@
+# Copyright 2023 The KerasNLP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""BLOOM model preset configurations."""
+
+backbone_presets = {
+    "bloom_560m_multi": {
+        "metadata": {
+            "description": (
+                "24-layer Bloom model. trained on 45 natural languages and "
+                "12 programming languages."
+            ),
+            "params": 816115712,
+            "official_name": "BLOOM",
+            "path": "bloom",
+            "model_card": "https://huggingface.co/bigscience/bloom",
+        },
+        "kaggle_handle": "kaggle://keras/bloom/keras/bloom_560m_multi/1",
+    },
+}
diff --git a/keras_nlp/models/bloom/bloom_tokenizer.py b/keras_nlp/models/bloom/bloom_tokenizer.py
new file mode 100644
index 00000000..cc3fcc2f
--- /dev/null
+++ b/keras_nlp/models/bloom/bloom_tokenizer.py
@@ -0,0 +1,123 @@
+# Copyright 2023 The KerasNLP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import copy
+
+from keras_nlp.api_export import keras_nlp_export
+from keras_nlp.models.bloom.bloom_presets import backbone_presets
+from keras_nlp.tokenizers.byte_pair_tokenizer import BytePairTokenizer
+from keras_nlp.utils.python_utils import classproperty
+
+
+@keras_nlp_export("keras_nlp.models.BloomTokenizer")
+class BloomTokenizer(BytePairTokenizer):
+    """A BLOOM tokenizer using Byte-Pair Encoding subword segmentation.
+
+    This tokenizer class will tokenize raw strings into integer sequences and
+    is based on `keras_nlp.tokenizers.BytePairTokenizer`. Unlike the
+    underlying tokenizer, it will check for all special tokens needed by BLOOM
+    models and provides a `from_preset()` method to automatically download
+    a matching vocabulary for a BLOOM preset.
+
+    This tokenizer does not provide truncation or padding of inputs.
+
+    If input is a batch of strings (rank > 0), the layer will output a
+    `tf.RaggedTensor` where the last dimension of the output is ragged.
+
+    If input is a scalar string (rank == 0), the layer will output a dense
+    `tf.Tensor` with static shape `[None]`.
+
+    Args:
+        vocabulary: string or dict, maps token to integer ids. If it is a
+            string, it should be the file path to a json file.
+        merges: string or list, contains the merge rule. If it is a string,
+            it should be the file path to merge rules. The merge rule file
+            should have one merge rule per line. Every merge rule contains
+            merge entities separated by a space.
+
+    Examples:
+
+    ```python
+    # Unbatched input.
+    tokenizer = keras_nlp.models.BloomTokenizer.from_preset("bloom_560m_multi")
+    tokenizer("The quick brown fox jumped.")
+
+    # Batched input.
+    tokenizer(["The quick brown fox jumped.", "The fox slept."])
+
+    # Detokenization.
+    tokenizer.detokenize(tokenizer("The quick brown fox jumped."))
+
+    # Custom vocabulary.
+    vocab = {"
": 0, "
": 1, "<pad>": 2, "a": 3, "Ġquick": 4, "Ġfox": 5}
+    merges = ["Ġ q", "u i", "c k", "ui ck", "Ġq uick"]
+    merges += ["Ġ f", "o x", "Ġf ox"]
+    tokenizer = keras_nlp.models.BloomTokenizer(vocabulary=vocab, merges=merges)
+    tokenizer("a quick fox.")
+    ```
+    """
+
+    def __init__(
+        self,
+        vocabulary=None,
+        merges=None,
+        **kwargs,
+    ):
+        self.bos_token = "
"
+        self.eos_token = "
"
+        self.pad_token = "<pad>"
+
+        super().__init__(
+            vocabulary=vocabulary,
+            merges=merges,
+            unsplittable_tokens=[
+                self.bos_token,
+                self.eos_token,
+                self.pad_token,
+            ],
+            **kwargs,
+        )
+
+    def set_vocabulary_and_merges(self, vocabulary, merges):
+        super().set_vocabulary_and_merges(vocabulary, merges)
+
+        if vocabulary is not None:
+            # Check for necessary special tokens.
+            for token in [self.bos_token, self.eos_token, self.pad_token]:
+                if token not in self.get_vocabulary():
+                    raise ValueError(
+                        f"Cannot find token `'{token}'` in the provided "
+                        f"`vocabulary`. Please provide `'{token}'` in "
+                        "your `vocabulary` or use a pretrained `vocabulary` name."
+                    )
+
+            self.bos_token_id = self.token_to_id(self.bos_token)
+            self.eos_token_id = self.token_to_id(self.eos_token)
+            self.pad_token_id = self.token_to_id(self.pad_token)
+        else:
+            self.bos_token_id = None
+            self.eos_token_id = None
+            self.pad_token_id = None
+
+    @classproperty
+    def presets(cls):
+        return copy.deepcopy(backbone_presets)
+
+    def get_config(self):
+        config = super().get_config()
+        # In the constructor, we pass the list of special tokens to the
+        # `unsplittable_tokens` arg of the superclass' constructor. Hence, we
+        # delete it from the config here.
+        del config["unsplittable_tokens"]
+        return config
diff --git a/keras_nlp/models/generative_task.py b/keras_nlp/models/generative_task.py
index 14d4c88f..9a461926 100644
--- a/keras_nlp/models/generative_task.py
+++ b/keras_nlp/models/generative_task.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import itertools
+
 import tensorflow as tf
 import tree
 
@@ -80,18 +82,30 @@ class GenerativeTask(Task):
 
             @jax.jit
             def compiled_generate_function(inputs, end_token_id, state):
-                # The only state we update during generation is sampler state,
-                # all weights are fixed and will not change.
-                mapping = zip(self._sampler.variables, state)
+                (
+                    sampler_variables,
+                    trainable_variables,
+                    non_trainable_variables,
+                ) = state
+                mapping = itertools.chain(
+                    zip(self._sampler.variables, sampler_variables),
+                    zip(self.trainable_variables, trainable_variables),
+                    zip(self.non_trainable_variables, non_trainable_variables),
+                )
 
                 with keras.StatelessScope(state_mapping=mapping) as scope:
                     outputs = self.generate_step(inputs, end_token_id)
 
                 # Get updated sampler variables from the stateless scope.
-                state = []
+                sampler_variables = []
                 for v in self._sampler.variables:
                     new_v = scope.get_current_value(v)
-                    state.append(new_v if new_v is not None else v)
+                    sampler_variables.append(new_v if new_v is not None else v)
+                state = (
+                    sampler_variables,
+                    trainable_variables,
+                    non_trainable_variables,
+                )
                 return outputs, state
 
             def wrapped_generate_function(
@@ -99,15 +113,20 @@ class GenerativeTask(Task):
                 end_token_id=None,
             ):
                 # Create an explicit tuple of all variable state.
+                state = (
+                    self._sampler.variables,
+                    self.trainable_variables,
+                    self.non_trainable_variables,
+                )
                 inputs = tree.map_structure(ops.convert_to_tensor, inputs)
                 outputs, state = compiled_generate_function(
                     inputs,
                     end_token_id,
-                    self._sampler.variables,
+                    state,
                 )
                 # Only assign the sampler variables (random seeds), as other
                 # model variables should never be updated in generation.
-                for ref_v, v in zip(self._sampler.variables, state):
+                for ref_v, v in zip(self._sampler.variables, state[0]):
                     ref_v.assign(v)
                 return outputs
 
diff --git a/keras_nlp/models/gpt_neo_x/gpt_neo_x_decoder.py b/keras_nlp/models/gpt_neo_x/gpt_neo_x_decoder.py
index ae646fb2..f80cafd5 100644
--- a/keras_nlp/models/gpt_neo_x/gpt_neo_x_decoder.py
+++ b/keras_nlp/models/gpt_neo_x/gpt_neo_x_decoder.py
@@ -211,9 +211,11 @@ class GPTNeoXDecoder(keras.layers.Layer):
             batch_size,
             input_length,
             output_length,
-            0
-            if self_attention_cache_update_index is None
-            else self_attention_cache_update_index,
+            (
+                0
+                if self_attention_cache_update_index is None
+                else self_attention_cache_update_index
+            ),
         )
         return (
             ops.minimum(decoder_mask, causal_mask)
diff --git a/keras_nlp/models/llama/llama_decoder.py b/keras_nlp/models/llama/llama_decoder.py
index 47bac478..2137831b 100644
--- a/keras_nlp/models/llama/llama_decoder.py
+++ b/keras_nlp/models/llama/llama_decoder.py
@@ -172,9 +172,11 @@ class LlamaDecoder(keras.layers.Layer):
             batch_size,
             input_length,
             output_length,
-            0
-            if self_attention_cache_update_index is None
-            else self_attention_cache_update_index,
+            (
+                0
+                if self_attention_cache_update_index is None
+                else self_attention_cache_update_index
+            ),
         )
         return (
             ops.minimum(decoder_mask, causal_mask)
diff --git a/tools/checkpoint_conversion/convert_bloom_checkpoints.py b/tools/checkpoint_conversion/convert_bloom_checkpoints.py
index 1fc895c5..38acd099 100644
--- a/tools/checkpoint_conversion/convert_bloom_checkpoints.py
+++ b/tools/checkpoint_conversion/convert_bloom_checkpoints.py
@@ -12,51 +12,86 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import numpy as np
-import torch
-import transformers
-from absl import app
-from absl import flags
-from checkpoint_conversion_utils import get_md5_checksum
+import json
+import os
 
-from keras_nlp.models.bloom.bloom_backbone import BloomBackbone
+os.environ["KERAS_BACKEND"] = "torch"
+os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
+
+import huggingface_hub  # noqa: E402
+import numpy as np  # noqa: E402
+import torch  # noqa: E402
+import transformers  # noqa: E402
+from absl import app  # noqa: E402
+from absl import flags  # noqa: E402
+
+import keras_nlp  # noqa: E402
+from keras_nlp.models import BloomBackbone  # noqa: E402
+from keras_nlp.models import BloomTokenizer  # noqa: E402
 
 FLAGS = flags.FLAGS
 
 PRESET_MAP = {
-    "bloom_tiny": "bigscience/bloom-560m",
-    "bloom_extra_small": "bigscience/bloom-1b1",
-    "bloom_small": "bigscience/bloom-1b7",
-    "bloom_meduim": "bigscience/bloom-3b",
-    "bloom_large": "bigscience/bloom-7b1",
-    "bloom_extra_large": "bigscience/bloom",
+    "bloom_560m_multi": "bigscience/bloom-560m",
+    "bloom_1.1b_multi": "bigscience/bloom-1b1",
+    "bloom_1.7b_multi": "bigscience/bloom-1b7",
+    "bloom_3b_multi": "bigscience/bloom-3b",
+    "bloom_7b_multi": "bigscience/bloom-7b1",
+    "bloom_176b_multi": "bigscience/bloom",
 }
 
+EXTRACT_DIR = "./model"
+
+
 flags.DEFINE_string(
     "preset", None, f'Must be one of {",".join(PRESET_MAP.keys())}'
 )
 flags.mark_flag_as_required("preset")
 
 
-def convert_checkpoints(hf_model):
+def download_hf_model(hf_model_name):
+    hf_model_dir = huggingface_hub.snapshot_download(
+        repo_id=hf_model_name,
+        allow_patterns=["*.json", "*.bin"],
+        ignore_patterns=["onnx/*"],
+        local_dir=EXTRACT_DIR,
+    )
+
+    return hf_model_dir
+
+
+def convert_model(hf_model):
     # get huggingface model configuration.
     hf_config = hf_model.config.to_dict()
 
-    cfg = {}
-    cfg["vocabulary_size"] = hf_config["vocab_size"]
-    cfg["num_layers"] = hf_config["n_layer"]
-    cfg["num_heads"] = hf_config["n_head"]
-    cfg["hidden_dim"] = hf_config["hidden_size"]
-    cfg["intermediate_dim"] = hf_config["hidden_size"] * 4
-    cfg["dropout"] = hf_config["hidden_dropout"]
-    cfg["layer_norm_epsilon"] = hf_config["layer_norm_epsilon"]
-
-    hidden_dim = cfg["hidden_dim"]
-    num_heads = cfg["num_heads"]
-    head_dim = hidden_dim // num_heads
+    kwargs = {}
+    kwargs["vocabulary_size"] = hf_config["vocab_size"]
+    kwargs["num_layers"] = hf_config["n_layer"]
+    kwargs["num_heads"] = hf_config["n_head"]
+    kwargs["hidden_dim"] = hf_config["hidden_size"]
+    kwargs["intermediate_dim"] = hf_config["hidden_size"] * 4
+    kwargs["dropout"] = hf_config["hidden_dropout"]
+    kwargs["layer_norm_epsilon"] = hf_config["layer_norm_epsilon"]
+
+    return BloomBackbone(**kwargs)
+
+
+def convert_tokenizer(hf_model_dir):
+    tokenizer_file_path = os.path.join(hf_model_dir, "tokenizer.json")
+    with open(tokenizer_file_path) as tokenizer_file:
+        hf_tokenizer = json.load(tokenizer_file)
 
-    # Intialize Bloom model with the weights.
-    keras_model = BloomBackbone(**cfg)
+    vocab = hf_tokenizer["model"]["vocab"]
+    merges = hf_tokenizer["model"]["merges"]
+
+    return BloomTokenizer(vocabulary=vocab, merges=merges)
+
+
+def convert_weights(keras_model, hf_model):
+    hidden_dim = keras_model.hidden_dim
+    num_heads = keras_model.num_heads
+    head_dim = hidden_dim // num_heads
+    num_layers = keras_model.num_layers
 
     # get huggingface model weights.
     hf_wts = hf_model.state_dict()
@@ -78,7 +113,7 @@ def convert_checkpoints(hf_model):
     keras_model.get_layer("final_layernorm").beta.assign(hf_wts["ln_f.bias"])
 
     # Decoder layers.
-    for i in range(cfg["num_layers"]):
+    for i in range(num_layers):
         decoder_layer = keras_model.get_layer(f"transformer_layer_{i}")
         # LayrNorm.
         decoder_layer._pre_attention_layernorm.gamma.assign(
@@ -141,54 +176,72 @@ def convert_checkpoints(hf_model):
             hf_wts[f"h.{i}.mlp.dense_4h_to_h.bias"]
         )
 
-    # Save the model.
-    print(f"\n-> Saving KerasNLP model weights to `{FLAGS.preset}.weights.h5`.")
-    keras_model.save_weights(f"{FLAGS.preset}.weights.h5")
 
-    return keras_model
-
-
-def check_output(keras_model, hf_model):
-    hf_model_input = {
-        "input_ids": torch.tensor([[59414, 15, 2670, 35433, 632, 207595]]),
-        "attention_mask": torch.tensor([[1, 1, 1, 1, 1, 1]]),
-    }
-
-    hf_model_outputs = hf_model(**hf_model_input)
-    hf_model_outputs = hf_model_outputs.last_hidden_state
-    hf_model_outputs = hf_model_outputs.detach().numpy()
+def validate_output(
+    hf_model,
+    keras_model,
+    hf_tokenizer,
+    keras_tokenizer,
+):
+    input_str = ["the quick brown fox ran, galloped and jumped."]
 
+    # KerasNLP
+    token_ids = torch.tensor(keras_tokenizer(input_str))
+    padding_mask = token_ids != 3
     keras_model_input = {
-        "token_ids": torch.tensor([[59414, 15, 2670, 35433, 632, 207595]]),
-        "padding_mask": torch.tensor([[1, 1, 1, 1, 1, 1]]),
+        "token_ids": token_ids,
+        "padding_mask": padding_mask,
     }
-
     keras_model_outputs = keras_model.predict(keras_model_input)
 
-    # Comparing the outputs.
-    print("KerasNLP output:", keras_model_outputs[0, 0, :10])
-    print("HF output:", hf_model_outputs[0, 0, :10])
-    print("Difference:", np.mean(keras_model_outputs - hf_model_outputs))
+    hf_model_input = hf_tokenizer(input_str, return_tensors="pt")
+
+    hf_model_outputs = hf_model(**hf_model_input).last_hidden_state
+    hf_model_outputs = hf_model_outputs.detach().numpy()
 
-    # Show the MD5 checksum of the model weights.
-    print("Model md5sum: ", get_md5_checksum(f"./{FLAGS.preset}.weights.h5"))
+    # Comparing the outputs.
+    print("🔶 KerasNLP output:", keras_model_outputs[0, 0, :10])
+    print("🔶 HF output:", hf_model_outputs[0, 0, :10])
+    print("🔶 Difference:", np.mean(keras_model_outputs - hf_model_outputs))
 
 
 def main(_):
+    preset = FLAGS.preset
+
     assert (
-        FLAGS.preset in PRESET_MAP.keys()
-    ), f'Invalid preset {FLAGS.preset}. Must be one of {",".join(PRESET_MAP.keys())}'
+        preset in PRESET_MAP.keys()
+    ), f'Invalid preset {preset}. Must be one of {",".join(PRESET_MAP.keys())}'
+
+    print(f"✅ Coverting {preset}")
 
-    hf_model_name = PRESET_MAP[FLAGS.preset]
+    hf_model_name = PRESET_MAP[preset]
+    hf_model_dir = download_hf_model(hf_model_name)
+    print("✅ Huggingface model downloaded from hub")
 
-    print("\n-> Loading HF model.")
-    hf_model = transformers.AutoModel.from_pretrained(hf_model_name)
+    hf_model = transformers.BloomModel.from_pretrained(hf_model_dir)
+    hf_tokenizer = transformers.BloomTokenizerFast.from_pretrained(hf_model_dir)
+    print("✅ Huggingface model loaded")
 
-    print("\n-> Converting model checkpoint.")
-    keras_model = convert_checkpoints(hf_model)
+    keras_model = convert_model(hf_model)
+    keras_tokenizer = convert_tokenizer(hf_model_dir)
+    print("✅ Keras model loaded")
 
-    print("\n-> Checking keras model output.")
-    check_output(keras_model, hf_model)
+    convert_weights(keras_model, hf_model)
+    print("✅ Weights converted")
+
+    validate_output(
+        hf_model,
+        keras_model,
+        hf_tokenizer,
+        keras_tokenizer,
+    )
+    print("✅ Numerics validated")
+
+    keras_nlp.src.utils.preset_utils.save_to_preset(keras_model, preset)
+    keras_nlp.src.utils.preset_utils.save_to_preset(
+        keras_tokenizer, preset, config_filename="tokenizer.json"
+    )
+    print("✅ Preset saved")
 
 
 if __name__ == "__main__":


Test patch:
diff --git a/keras_nlp/layers/modeling/alibi_bias_test.py b/keras_nlp/layers/modeling/alibi_bias_test.py
new file mode 100644
index 00000000..120c7622
--- /dev/null
+++ b/keras_nlp/layers/modeling/alibi_bias_test.py
@@ -0,0 +1,202 @@
+# Copyright 2023 The KerasNLP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from keras_nlp.backend import keras
+from keras_nlp.backend import ops
+from keras_nlp.backend import random
+from keras_nlp.layers.modeling.alibi_bias import AlibiBias
+from keras_nlp.tests.test_case import TestCase
+
+
+class AlibiBiasTest(TestCase):
+    def test_layer_behaviors(self):
+        alibi_bias_max = 8
+        batch_size = 4
+        num_heads = 8
+        query_length = 10
+        key_length = 10
+        self.run_layer_test(
+            cls=AlibiBias,
+            init_kwargs={
+                "alibi_bias_max": alibi_bias_max,
+            },
+            input_data=random.uniform(
+                shape=(batch_size, num_heads, query_length, key_length)
+            ),
+            expected_output_shape=(
+                batch_size,
+                num_heads,
+                query_length,
+                key_length,
+            ),
+        )
+
+    def test_float16_dtype(self):
+        # Create a 4-dimensional input (the first dimension is implicit).
+        alibi_bias_max = 8
+        num_heads = 8
+        query_length = 5
+        key_length = 10
+        test_layer = AlibiBias(alibi_bias_max=alibi_bias_max, dtype="float16")
+        input_tensor = keras.Input(shape=(num_heads, query_length, key_length))
+        output_tensor = test_layer(input_tensor)
+
+        # the output is expected to be the same as the input shape in all
+        # dimensions. here, the first dimension is implicit and is for batch
+        expected_output_shape = (None, num_heads, query_length, key_length)
+        self.assertEqual(expected_output_shape, output_tensor.shape)
+        # The default output dtype for this layer should be "float32".
+        self.assertEqual("float16", output_tensor.dtype)
+
+    def test_dynamic_layer_output_shape(self):
+        query_length = 10
+        key_length = 10
+        num_heads = 4
+
+        test_layer = AlibiBias()
+        # Create a 4-dimensional input (the first dimension is implicit).
+        input_tensor = keras.Input(shape=(num_heads, query_length, key_length))
+        output_tensor = test_layer(input_tensor)
+
+        # the output is expected to be the same as the input shape in all
+        # dimensions.
+        expected_output_shape = (
+            None,
+            num_heads,
+            query_length,
+            key_length,
+        )
+        self.assertEqual(expected_output_shape, output_tensor.shape)
+
+    def test_value_error_when_inputs_shape_is_not_4(self):
+        with self.assertRaises(ValueError):
+            AlibiBias()(random.uniform(shape=(12, 12)))
+
+    def test_num_heads_is_not_power_of_two(self):
+        inputs_shape = (1, 12, 12, 12)
+        inputs = random.uniform(shape=inputs_shape)
+        layer = AlibiBias()
+        outputs = layer(inputs)
+        self.assertEqual(inputs_shape, outputs.shape)
+
+    def test_correct_output(self):
+        batch_size = 1
+        num_heads = 8
+        query_length = 1
+        key_length = 3
+        input_shape = (batch_size, num_heads, query_length, key_length)
+        input_tensor = ops.zeros(input_shape)
+        layer = AlibiBias()
+        output_tensor = layer(input_tensor)
+        print(output_tensor)
+        self.assertAllClose(
+            output_tensor,
+            ops.convert_to_tensor(
+                [
+                    [
+                        [[-1.0, -0.5, 0.0]],
+                        [[-0.5, -0.25, 0.0]],
+                        [[-0.25, -0.125, 0.0]],
+                        [[-0.125, -0.0625, 0.0]],
+                        [[-0.0625, -0.03125, 0.0]],
+                        [[-0.03125, -0.015625, 0.0]],
+                        [[-0.015625, -0.0078125, 0.0]],
+                        [[-0.0078125, -0.00390625, 0.0]],
+                    ]
+                ]
+            ),
+        )
+
+    def test_correct_output_num_heads_not_power_of_two(self):
+        batch_size = 1
+        num_heads = 14
+        query_length = 1
+        key_length = 3
+        input_shape = (batch_size, num_heads, query_length, key_length)
+        input_tensor = ops.zeros(input_shape)
+        layer = AlibiBias()
+        output_tensor = layer(input_tensor)
+        print(output_tensor)
+        self.assertAllClose(
+            output_tensor,
+            ops.convert_to_tensor(
+                [
+                    [
+                        [[-1.0, -0.5, 0.0]],
+                        [[-0.5, -0.25, 0.0]],
+                        [[-0.25, -0.125, 0.0]],
+                        [[-0.125, -0.0625, 0.0]],
+                        [[-0.0625, -0.03125, 0.0]],
+                        [[-0.03125, -0.015625, 0.0]],
+                        [[-0.015625, -0.0078125, 0.0]],
+                        [[-0.0078125, -0.00390625, 0.0]],
+                        [[-1.4142135, -0.70710677, 0.0]],
+                        [[-0.70710677, -0.35355338, 0.0]],
+                        [[-0.35355338, -0.17677669, 0.0]],
+                        [[-0.17677669, -0.08838835, 0.0]],
+                        [[-0.08838835, -0.04419417, 0.0]],
+                        [[-0.04419417, -0.02209709, 0.0]],
+                    ]
+                ]
+            ),
+        )
+
+    def test_correct_output_alibi_bias_max(self):
+        alibi_bias_max = 12
+        batch_size = 1
+        num_heads = 2
+        query_length = 1
+        key_length = 3
+        input_shape = (batch_size, num_heads, query_length, key_length)
+        input_tensor = ops.zeros(input_shape)
+        layer = AlibiBias(alibi_bias_max=alibi_bias_max)
+        output_tensor = layer(input_tensor)
+        print(output_tensor)
+        self.assertAllClose(
+            output_tensor,
+            ops.convert_to_tensor(
+                [
+                    [
+                        [[-0.03125, -0.015625, 0.0]],
+                        [[-0.00048828, -0.00024414, 0.0]],
+                    ]
+                ]
+            ),
+        )
+
+    def test_correct_output_alibi_bias_max_num_heads_not_power_of_two(
+        self,
+    ):
+        alibi_bias_max = 6
+        batch_size = 1
+        num_heads = 3
+        query_length = 1
+        key_length = 3
+        input_shape = (batch_size, num_heads, query_length, key_length)
+        input_tensor = ops.zeros(input_shape)
+        layer = AlibiBias(alibi_bias_max=alibi_bias_max)
+        output_tensor = layer(input_tensor)
+        print(output_tensor)
+        self.assertAllClose(
+            output_tensor,
+            ops.convert_to_tensor(
+                [
+                    [
+                        [[-0.25, -0.125, 0.0]],
+                        [[-0.03125, -0.015625, 0.0]],
+                        [[-0.70710677, -0.35355338, 0.0]],
+                    ]
+                ]
+            ),
+        )
diff --git a/keras_nlp/models/bloom/bloom_backbone_test.py b/keras_nlp/models/bloom/bloom_backbone_test.py
index 99cb9d35..83732e49 100644
--- a/keras_nlp/models/bloom/bloom_backbone_test.py
+++ b/keras_nlp/models/bloom/bloom_backbone_test.py
@@ -19,7 +19,7 @@ from keras_nlp.models.bloom.bloom_backbone import BloomBackbone
 from keras_nlp.tests.test_case import TestCase
 
 
-class BloomTest(TestCase):
+class BloomBackboneTest(TestCase):
     def setUp(self):
         self.init_kwargs = {
             "vocabulary_size": 10,
@@ -49,3 +49,28 @@ class BloomTest(TestCase):
             init_kwargs=self.init_kwargs,
             input_data=self.input_data,
         )
+
+    @pytest.mark.large
+    def test_smallest_preset(self):
+        self.run_preset_test(
+            cls=BloomBackbone,
+            preset="bloom_560m_multi",
+            input_data={
+                "token_ids": ops.array([[101, 1996, 4248, 102]], dtype="int32"),
+                "padding_mask": ops.ones((1, 4), dtype="int32"),
+            },
+            expected_output_shape=(1, 4, 1024),
+            # The forward pass from a preset should be stable!
+            expected_partial_output=ops.array(
+                [2.4394186, 1.4131186, -2.7810357, -6.330823, -1.0599766]
+            ),
+        )
+
+    @pytest.mark.extra_large
+    def test_all_presets(self):
+        for preset in BloomBackbone.presets:
+            self.run_preset_test(
+                cls=BloomBackbone,
+                preset=preset,
+                input_data=self.input_data,
+            )
diff --git a/keras_nlp/models/bloom/bloom_tokenizer_test.py b/keras_nlp/models/bloom/bloom_tokenizer_test.py
new file mode 100644
index 00000000..9ae9c0cc
--- /dev/null
+++ b/keras_nlp/models/bloom/bloom_tokenizer_test.py
@@ -0,0 +1,63 @@
+# Copyright 2023 The KerasNLP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from keras_nlp.models.bloom.bloom_tokenizer import BloomTokenizer
+from keras_nlp.tests.test_case import TestCase
+
+
+class BloomTokenizerTest(TestCase):
+    def setUp(self):
+        self.vocab = ["!", "air", "Ġair", "plane", "Ġat", "port"]
+        self.vocab += ["
", "
", "<pad>"]
+        self.vocab = dict([(token, i) for i, token in enumerate(self.vocab)])
+        self.merges = ["Ġ a", "Ġ t", "Ġ i", "Ġ b", "a i", "p l", "n e"]
+        self.merges += ["Ġa t", "p o", "r t", "Ġt h", "ai r", "pl a", "po rt"]
+        self.merges += ["Ġai r", "Ġa i", "pla ne"]
+        self.init_kwargs = {"vocabulary": self.vocab, "merges": self.merges}
+        self.input_data = [
+            "
airplane at airport<pad>",
+            "
 airplane airport<pad>",
+        ]
+
+    def test_tokenizer_basics(self):
+        self.run_preprocessing_layer_test(
+            cls=BloomTokenizer,
+            init_kwargs=self.init_kwargs,
+            input_data=self.input_data,
+            expected_output=[[6, 1, 3, 4, 2, 5, 8], [6, 2, 3, 2, 5, 8]],
+        )
+
+    def test_errors_missing_special_tokens(self):
+        with self.assertRaises(ValueError):
+            BloomTokenizer(vocabulary=["a", "b", "c"], merges=[])
+
+    @pytest.mark.large
+    def test_smallest_preset(self):
+        self.run_preset_test(
+            cls=BloomTokenizer,
+            preset="bloom_560m_multi",
+            input_data=["The quick brown fox."],
+            expected_output=[[2175, 23714, 73173, 144252, 17]],
+        )
+
+    @pytest.mark.extra_large
+    def test_all_presets(self):
+        for preset in BloomTokenizer.presets:
+            self.run_preset_test(
+                cls=BloomTokenizer,
+                preset=preset,
+                input_data=self.input_data,
+            )
diff --git a/keras_nlp/tests/test_case.py b/keras_nlp/tests/test_case.py
index 455a8569..10102628 100644
--- a/keras_nlp/tests/test_case.py
+++ b/keras_nlp/tests/test_case.py
@@ -142,6 +142,12 @@ class TestCase(tf.test.TestCase, parameterized.TestCase):
                     else:
                         return self.layer(x)
 
+            input_data = tree.map_structure(
+                lambda x: ops.convert_to_numpy(x), input_data
+            )
+            output_data = tree.map_structure(
+                lambda x: ops.convert_to_numpy(x), output_data
+            )
             model = TestModel(layer)
             # Temporarily disable jit compilation on torch backend.
             jit_compile = config.backend() != "torch"