Repository: keras-team/keras-nlp

Issue:
Port BeamSampler to multi-backend keras
This was not included in 0.6.0, but we should port it from `tf.xx` -> `ops.xx`.

Discussion:
shivance: Hey @mattdangerw !
Could you tell the alternative for these in core?

1. tf.nest.map_structure
2. tf.newaxis
3. tf.while_loop
4. tf.ensure_shape
5. tf.reduce_any
6. tf.reduce_all
abheesht17: 2. ops.expand_dims
3. ops.while_loop
4. You can probably just do x.shape == (…) or something?
5. ops.any
6. ops.all
mattdangerw: For `tf.ensure_shape`, ideally we can ditch it.
mattdangerw: For `tf.nest.map_structure`, just leave as is for now. On a separate PR, we can update to the `dm_nest` module.
shivance: > For `tf.ensure_shape`, ideally we can ditch it.

There is this comment at line no. 180 in the file
```
# Ensure shape is `[None]`, otherwise it causes issues after
# converting to TFLite.
```

Patch:
diff --git a/.cloudbuild/README.md b/.cloudbuild/README.md
index 12a637d1..064caf5f 100644
--- a/.cloudbuild/README.md
+++ b/.cloudbuild/README.md
@@ -25,44 +25,18 @@ To authorize yourself with `keras-team-test`, run:
 gcloud config set project keras-team-test
 ```
 
-To add/update dependency for GPU tests:
-- Add/update dependency to `requirements.txt`
-- Create a `Dockerfile` with the following contents:
-  ```
-  FROM tensorflow/tensorflow:2.12.0-gpu
-  RUN apt-get -y update
-  RUN apt-get -y install git
-  RUN git clone https://github.com/keras-team/keras-nlp.git
-  RUN cd keras-nlp
-  RUN pip install -r keras-nlp/requirements.txt
-  ```
-- Run the following command from the directory with your `Dockerfile`:
-  ```
-  gcloud builds submit --region=us-west1 --tag us-west1-docker.pkg.dev/keras-team-test/keras-nlp-test/keras-nlp-image-tensorflow:deps --timeout=30m
-  ```
-- Repeat the last two steps for Jax and Torch (replacing "tensorflow" with "jax"
- or "torch" in the docker image target name). `Dockerfile` for jax:
-  ```
-  FROM nvidia/cuda:11.7.1-base-ubuntu20.04
-  RUN apt-get update
-  RUN apt-get install -y python3 python3-pip
-  RUN apt-get install -y git
-  RUN git clone https://github.com/keras-team/keras-nlp.git
-  RUN cd keras-nlp
-  RUN pip install -r keras-nlp/requirements.txt
-  RUN pip install --upgrade "jax[cuda11_pip]" -f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html
-  ```
-  and for torch:
-  ```
-  FROM nvidia/cuda:11.7.1-base-ubuntu20.04
-  RUN apt-get update
-  RUN apt-get install -y python3 python3-pip
-  RUN apt-get install -y git
-  RUN git clone https://github.com/keras-team/keras-nlp.git
-  RUN cd keras-nlp
-  RUN pip install -r keras-nlp/requirements.txt
-  RUN pip install torch
-  ```
+To add/update dependency for GPU tests for a given framework:
+- Add/update dependencies in `requirements.txt`.
+- Add/update dependencies in `.cloudbuild/{framework}/Dockerfile`.
+- Run the following:
+```
+gcloud builds submit --region=us-west1 --tag us-west1-docker.pkg.dev/keras-team-test/keras-nlp-test/keras-nlp-image-{framework}:deps --timeout=30m
+```
+
+Alternately, to update all docker images at once, just run:
+```
+./cloudbuild/update_images.sh
+```
 
 ### Run TPU Testing
 
diff --git a/.cloudbuild/jax/Dockerfile b/.cloudbuild/jax/Dockerfile
new file mode 100644
index 00000000..ec84817c
--- /dev/null
+++ b/.cloudbuild/jax/Dockerfile
@@ -0,0 +1,8 @@
+FROM nvidia/cuda:11.7.1-base-ubuntu20.04
+RUN apt-get update
+RUN apt-get install -y python3 python3-pip
+RUN apt-get install -y git
+RUN git clone https://github.com/keras-team/keras-nlp.git
+RUN cd keras-nlp
+RUN pip install -r keras-nlp/requirements.txt
+RUN pip install --upgrade "jax[cuda11_pip]" -f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html
diff --git a/.cloudbuild/tensorflow/Dockerfile b/.cloudbuild/tensorflow/Dockerfile
new file mode 100644
index 00000000..d452d376
--- /dev/null
+++ b/.cloudbuild/tensorflow/Dockerfile
@@ -0,0 +1,6 @@
+FROM tensorflow/tensorflow:2.13.0-gpu
+RUN apt-get -y update
+RUN apt-get -y install git
+RUN git clone https://github.com/keras-team/keras-nlp.git
+RUN cd keras-nlp
+RUN pip install -r keras-nlp/requirements.txt
diff --git a/.cloudbuild/torch/Dockerfile b/.cloudbuild/torch/Dockerfile
new file mode 100644
index 00000000..ecd88b81
--- /dev/null
+++ b/.cloudbuild/torch/Dockerfile
@@ -0,0 +1,8 @@
+FROM nvidia/cuda:11.7.1-base-ubuntu20.04
+RUN apt-get update
+RUN apt-get install -y python3 python3-pip
+RUN apt-get install -y git
+RUN git clone https://github.com/keras-team/keras-nlp.git
+RUN cd keras-nlp
+RUN pip install -r keras-nlp/requirements.txt
+RUN pip install torch
diff --git a/.cloudbuild/update_images.sh b/.cloudbuild/update_images.sh
new file mode 100755
index 00000000..2876df81
--- /dev/null
+++ b/.cloudbuild/update_images.sh
@@ -0,0 +1,13 @@
+#!/bin/bash -ex
+
+base_dir=$(dirname $0)
+
+for platform in "jax" "tensorflow" "torch"; do
+    pushd "${base_dir}/${platform}" > /dev/null
+    gcloud builds submit \
+        --region=us-west1 \
+        --project=keras-team-test \
+        --tag "us-west1-docker.pkg.dev/keras-team-test/keras-nlp-test/keras-nlp-image-${platform}:deps" \
+        --timeout=30m
+    popd
+done
diff --git a/keras_nlp/layers/modeling/masked_lm_head.py b/keras_nlp/layers/modeling/masked_lm_head.py
index 0bb0a421..71014c47 100644
--- a/keras_nlp/layers/modeling/masked_lm_head.py
+++ b/keras_nlp/layers/modeling/masked_lm_head.py
@@ -123,7 +123,6 @@ class MaskedLMHead(keras.layers.Layer):
         self.layer_norm_epsilon = layer_norm_epsilon
         self.kernel_initializer = keras.initializers.get(kernel_initializer)
         self.bias_initializer = keras.initializers.get(bias_initializer)
-        self._built = False
 
         if vocabulary_size is None and embedding_weights is None:
             raise ValueError(
@@ -142,7 +141,7 @@ class MaskedLMHead(keras.layers.Layer):
                 )
             self.vocabulary_size = shape[0]
 
-    def build(self, inputs_shape, masked_positions_shape=None):
+    def build(self, inputs_shape):
         if self.embedding_weights is not None:
             feature_size = self.embedding_weights.shape[-1]
         else:
@@ -157,12 +156,13 @@ class MaskedLMHead(keras.layers.Layer):
         self._layer_norm = keras.layers.LayerNormalization(
             epsilon=self.layer_norm_epsilon,
         )
-        if masked_positions_shape:
-            gather_length = masked_positions_shape[1]
-            shape = (inputs_shape[0], gather_length, inputs_shape[-1])
-            self._dense.build(shape)
-            shape = (inputs_shape[0], gather_length, feature_size)
-            self._layer_norm.build(shape)
+        # The gather length does not affect any of our built variables, so
+        # we can pass any value here.
+        gather_length = None
+        shape = (inputs_shape[0], gather_length, inputs_shape[-1])
+        self._dense.build(shape)
+        shape = (inputs_shape[0], gather_length, feature_size)
+        self._layer_norm.build(shape)
         if self.embedding_weights is None:
             self._kernel = self.add_weight(
                 name="output_kernel",
@@ -177,10 +177,10 @@ class MaskedLMHead(keras.layers.Layer):
             dtype=self.dtype,
         )
 
-    def call(self, inputs, masked_positions):
+    def call(self, inputs, mask_positions):
         # Gather the encoded tokens at the masked indices.
-        masked_positions = ops.expand_dims(masked_positions, axis=-1)
-        x = ops.take_along_axis(inputs, masked_positions, axis=1)
+        mask_positions = ops.expand_dims(mask_positions, axis=-1)
+        x = ops.take_along_axis(inputs, mask_positions, axis=1)
 
         # Apply a trainable linear transformation and a layer norm.
         x = self._dense(x)
@@ -221,7 +221,9 @@ class MaskedLMHead(keras.layers.Layer):
         )
         return config
 
-    def compute_output_shape(self, inputs_shape, masked_positions_shape):
-        output_shape = list(masked_positions_shape)
-        output_shape[-1] = self.vocabulary_size
-        return tuple(output_shape)
+    # TODO: restore this after https://github.com/keras-team/keras-core/pull/632
+    # is in a release!
+    # def compute_output_shape(self, inputs_shape, mask_positions_shape):
+    #     output_shape = list(mask_positions_shape)
+    #     output_shape[-1] = self.vocabulary_size
+    #     return tuple(output_shape)
diff --git a/keras_nlp/samplers/beam_sampler.py b/keras_nlp/samplers/beam_sampler.py
index 077f250b..6a076f35 100644
--- a/keras_nlp/samplers/beam_sampler.py
+++ b/keras_nlp/samplers/beam_sampler.py
@@ -14,14 +14,13 @@
 """Beam Sampler."""
 
 import tensorflow as tf
-from tensorflow.compiler.tf2xla.python.xla import dynamic_update_slice
 
 from keras_nlp.api_export import keras_nlp_export
 from keras_nlp.backend import keras
+from keras_nlp.backend import ops
 from keras_nlp.samplers.sampler import Sampler
 from keras_nlp.samplers.sampler import call_args_docstring
 from keras_nlp.utils.python_utils import format_docstring
-from keras_nlp.utils.tensor_utils import assert_tf_backend
 
 
 @format_docstring(call_args=call_args_docstring)
@@ -102,10 +101,6 @@ class BeamSampler(Sampler):
         return_all_beams=False,
         **kwargs,
     ):
-        # Temporarily turn off beam search in other backends.
-        # No technical blockers here, just need tf -> ops rewrite.
-        assert_tf_backend(self.__class__.__name__)
-
         super().__init__(**kwargs)
         self.num_beams = num_beams
         self.return_all_beams = return_all_beams
@@ -120,89 +115,96 @@ class BeamSampler(Sampler):
         end_token_id=None,
         hidden_states=None,
     ):
-        batch_size, max_length = tf.shape(prompt)[0], tf.shape(prompt)[1]
-        # Make sure max length and start index are the same dtype.
-        index = tf.cast(index, max_length.dtype)
+        batch_size, max_length = ops.shape(prompt)[0], ops.shape(prompt)[1]
+        index = ops.cast(index, "int32")
 
         def create_beams(x):
             """Add initial beam state."""
-            return tf.repeat(x, self.num_beams, axis=0)
+            return ops.repeat(x, self.num_beams, axis=0)
 
         def flatten_beams(x):
             """Combine the beam dim and batch dim."""
-            flat_shape = [batch_size * self.num_beams] + x.shape.as_list()[2:]
-            return tf.reshape(x, shape=flat_shape)
+            flat_shape = (batch_size * self.num_beams,) + tuple(x.shape)[2:]
+            return ops.reshape(x, flat_shape)
 
         def unflatten_beams(x):
             """Separate the beam dim and batch dim."""
-            unflat_shape = [batch_size, self.num_beams] + x.shape.as_list()[1:]
-            return tf.reshape(x, shape=unflat_shape)
+            unflat_shape = (batch_size, self.num_beams) + tuple(x.shape)[1:]
+            return ops.reshape(x, unflat_shape)
 
         if mask is None:
-            mask = tf.zeros_like(prompt, dtype="bool")
+            mask = ops.zeros_like(prompt, dtype="bool")
         else:
-            mask = tf.cast(mask, dtype="bool")
-        # `tf.while_loop` will not accept `None` as a value for `loop_vars`.
-        cache = () if cache is None else cache
+            mask = ops.cast(mask, dtype="bool")
+        # `ops.while_loop` will not accept `None` as a value for `loop_vars`.
+        has_cache = cache is not None
+        cache = cache if has_cache else ()
         # Add extra sequences for each beam.
         prompt, mask = create_beams(prompt), create_beams(mask)
         cache = tf.nest.map_structure(create_beams, cache)
         # Setup the initial beam log-likelihoods.
         # On the first loop, make sure only the original beam is considered.
-        log_probs = tf.constant([[0.0] + [-1e9] * (self.num_beams - 1)])
-        log_probs = flatten_beams(tf.repeat(log_probs, batch_size, axis=0))
+        log_probs = ops.array(
+            [[0.0] + [-1e9] * (self.num_beams - 1)], dtype="float32"
+        )
+        log_probs = flatten_beams(ops.repeat(log_probs, batch_size, axis=0))
 
         def cond(prompt, cache, index, log_probs):
             if end_token_id is None:
                 return True
             # Stop if all sequences have produced a *new* end_token_id.
             end_tokens = (prompt == end_token_id) & (~mask)
-            prompt_done = tf.reduce_any(end_tokens, axis=-1)
-            return not tf.reduce_all(prompt_done)
+            prompt_done = ops.any(end_tokens, axis=-1)
+            return ops.logical_not(ops.all(prompt_done))
 
         def body(prompt, cache, index, log_probs):
             # Compute the softmax distribution for the next token.
             logits, _, cache = next(prompt, cache, index)
-            vocab_size = tf.shape(logits)[-1]
+            vocab_size = ops.shape(logits)[-1]
             probs = keras.activations.softmax(logits / self.temperature)
 
             # Compute the running log-likelihood of each new candidate.
-            next_log_probs = tf.math.log(probs) + log_probs[..., tf.newaxis]
+            next_log_probs = ops.log(probs) + log_probs[..., None]
             # Reshape `preds` to shape `(batch_size, num_beams * vocab_size)`.
-            next_log_probs = tf.reshape(next_log_probs, shape=[batch_size, -1])
+            next_log_probs = ops.reshape(next_log_probs, [batch_size, -1])
 
             # Compute the top beam indices and next tokens.
-            next_log_probs, indices = tf.math.top_k(
+            next_log_probs, indices = ops.top_k(
                 next_log_probs, k=self.num_beams, sorted=False
             )
             beam_indices = indices // vocab_size
             next_token = flatten_beams(indices % vocab_size)
-            # Ensure shape is `[None]`, otherwise it causes issues after
-            # converting to TFLite.
-            next_token = tf.ensure_shape(next_token, [None])
             # We need `ensure_shape` as `top_k` will change the static shape.
             next_log_probs = flatten_beams(next_log_probs)
-            log_probs = tf.ensure_shape(next_log_probs, log_probs.shape)
+            # Work around for top_k output shape on tf backend.
+            if isinstance(log_probs, tf.Tensor):
+                log_probs = tf.ensure_shape(next_log_probs, log_probs.shape)
+            else:
+                log_probs = next_log_probs
 
             def gather_beams(x):
                 x = unflatten_beams(x)
-                x = tf.gather(x, beam_indices, axis=1, batch_dims=1)
+                indices = beam_indices
+                for axis in range(2, len(x.shape)):
+                    indices = ops.expand_dims(indices, axis=axis)
+                x = ops.take_along_axis(x, indices, axis=1)
                 return flatten_beams(x)
 
             prompt = gather_beams(prompt)
-            cache = tf.nest.map_structure(gather_beams, cache)
+            if has_cache:
+                cache = tf.nest.map_structure(gather_beams, cache)
 
             # Update each beam with the next token.
-            next_token = tf.cast(next_token, prompt.dtype)
+            next_token = ops.cast(next_token, prompt.dtype)
             # Don't overwrite anywhere mask is True.
-            next_token = tf.where(mask[:, index], prompt[:, index], next_token)
+            next_token = ops.where(mask[:, index], prompt[:, index], next_token)
             # Update the prompt with the next token.
-            next_token = next_token[:, tf.newaxis]
-            prompt = dynamic_update_slice(prompt, next_token, [0, index])
+            next_token = next_token[:, None]
+            prompt = ops.slice_update(prompt, [0, index], next_token)
             # Return the iteration of the loop state.
             return (prompt, cache, index + 1, log_probs)
 
-        prompt, _, _, log_probs = tf.while_loop(
+        prompt, _, _, log_probs = self.run_loop(
             cond=cond,
             body=body,
             loop_vars=(prompt, cache, index, log_probs),
@@ -213,21 +215,23 @@ class BeamSampler(Sampler):
         all_log_probs = unflatten_beams(log_probs)
 
         if self.return_all_beams:
-            sorted_indices = tf.argsort(
-                all_log_probs, axis=-1, direction="DESCENDING"
-            )
-            sorted_log_probs = tf.gather(
-                all_log_probs, sorted_indices, axis=-1, batch_dims=1
+            sorted_indices = ops.argsort(-all_log_probs, axis=-1)
+            sorted_log_probs = ops.take_along_axis(
+                all_log_probs,
+                sorted_indices,
+                axis=1,
             )
-            sorted_prompts = tf.gather(
-                all_prompts, sorted_indices, axis=1, batch_dims=1
+            sorted_prompts = ops.take_along_axis(
+                all_prompts,
+                ops.expand_dims(sorted_indices, -1),
+                axis=1,
             )
             return sorted_prompts, sorted_log_probs
         else:
             # Gather the top beam at each batch index.
-            top_beams = tf.math.argmax(all_log_probs, axis=-1)[:, tf.newaxis]
-            prompt = tf.gather(all_prompts, top_beams, axis=1, batch_dims=1)
-            return tf.squeeze(prompt, axis=1)
+            top_beams = ops.argmax(all_log_probs, axis=-1)[:, None, None]
+            prompt = ops.take_along_axis(all_prompts, top_beams, axis=1)
+            return ops.squeeze(prompt, axis=1)
 
     def get_config(self):
         config = super().get_config()
diff --git a/keras_nlp/samplers/contrastive_sampler.py b/keras_nlp/samplers/contrastive_sampler.py
index 5a5f1fd5..ab4bd6fe 100644
--- a/keras_nlp/samplers/contrastive_sampler.py
+++ b/keras_nlp/samplers/contrastive_sampler.py
@@ -14,14 +14,13 @@
 """Contrastive Sampler."""
 
 import tensorflow as tf
-from tensorflow.compiler.tf2xla.python.xla import dynamic_update_slice
 
 from keras_nlp.api_export import keras_nlp_export
 from keras_nlp.backend import keras
+from keras_nlp.backend import ops
 from keras_nlp.samplers.sampler import Sampler
 from keras_nlp.samplers.sampler import call_args_docstring
 from keras_nlp.utils.python_utils import format_docstring
-from keras_nlp.utils.tensor_utils import assert_tf_backend
 
 
 @format_docstring(call_args=call_args_docstring)
@@ -79,10 +78,6 @@ class ContrastiveSampler(Sampler):
         seed=None,
         **kwargs,
     ):
-        # Temporarily turn off beam search in other backends.
-        # No technical blockers here, just need tf -> ops rewrite.
-        assert_tf_backend(self.__class__.__name__)
-
         super().__init__(**kwargs)
         self.k = k
         self.alpha = alpha
@@ -103,39 +98,39 @@ class ContrastiveSampler(Sampler):
                 "`ContrastiveSampler` requires passing a `hidden_states`, but"
                 "received `None`."
             )
-        batch_size, max_length = tf.shape(prompt)[0], tf.shape(prompt)[1]
-        # Make sure max length and start index are the same dtype.
-        index = tf.cast(index, max_length.dtype)
+        batch_size, max_length = ops.shape(prompt)[0], ops.shape(prompt)[1]
+        index = ops.cast(index, "int32")
 
         def create_beams(x):
             """Add initial beam state."""
-            x = tf.repeat(x, self.k, axis=0)
-            flat_shape = [batch_size * self.k] + x.shape.as_list()[1:]
-            return tf.reshape(x, shape=flat_shape)
+            x = ops.repeat(x, self.k, axis=0)
+            flat_shape = (batch_size * self.k,) + tuple(x.shape)[1:]
+            return ops.reshape(x, flat_shape)
 
         def flatten_beams(x):
             """Combine the beam dim and batch dim."""
-            flat_shape = [batch_size * self.k] + x.shape.as_list()[2:]
-            return tf.reshape(x, shape=flat_shape)
+            flat_shape = (batch_size * self.k,) + tuple(x.shape)[2:]
+            return ops.reshape(x, flat_shape)
 
         def unflatten_beams(x):
             """Separate the beam dim and batch dim."""
-            unflat_shape = [batch_size, self.k] + x.shape.as_list()[1:]
-            return tf.reshape(x, shape=unflat_shape)
+            unflat_shape = (batch_size, self.k) + tuple(x.shape)[1:]
+            return ops.reshape(x, unflat_shape)
 
-        mask = tf.zeros_like(prompt, dtype="bool") if mask is None else mask
+        mask = ops.zeros_like(prompt, dtype="bool") if mask is None else mask
         # Compute initial logits.
         logits, _, cache = next(prompt, cache, index)
-        # `tf.while_loop` will not accept `None` as a value for `loop_vars`.
-        cache = () if cache is None else cache
+        # `ops.while_loop` will not accept `None` as a value for `loop_vars`.
+        has_cache = cache is not None
+        cache = cache if has_cache else ()
 
         def cond(prompt, cache, index, logits, hidden_states):
             if end_token_id is None:
                 return True
             # Stop if all sequences have produced a *new* end_token_id.
             end_tokens = (prompt == end_token_id) & (~mask)
-            prompt_done = tf.reduce_any(end_tokens, axis=-1)
-            return not tf.reduce_all(prompt_done)
+            prompt_done = ops.any(end_tokens, axis=-1)
+            return ops.logical_not(ops.all(prompt_done))
 
         def body(prompt, cache, index, logits, hidden_states):
             # Compute the softmax distribution for the next token.
@@ -146,23 +141,25 @@ class ContrastiveSampler(Sampler):
             prompt_beams = create_beams(prompt)
             mask_beams = create_beams(mask)
             hidden_states_beams = create_beams(hidden_states)
-            cache_beams = tf.nest.map_structure(create_beams, cache)
+            cache_beams = None
+            if has_cache:
+                cache_beams = tf.nest.map_structure(create_beams, cache)
 
             # Get top-k candidate tokens and their probabilities.
-            top_k_probabilities, top_k_indices = tf.math.top_k(
+            top_k_probabilities, top_k_indices = ops.top_k(
                 probabilities, k=self.k, sorted=False
             )
             next_token_probabilities = flatten_beams(top_k_probabilities)
             next_token = flatten_beams(top_k_indices)
-            next_token = tf.cast(next_token, prompt.dtype)
-            next_token = tf.where(
+            next_token = ops.cast(next_token, prompt.dtype)
+            next_token = ops.where(
                 mask_beams[:, index], prompt_beams[:, index], next_token
             )
 
             # Update the prompt with the next token.
-            next_token = next_token[:, tf.newaxis]
-            prompt_beams = dynamic_update_slice(
-                prompt_beams, next_token, [0, index]
+            next_token = ops.expand_dims(next_token, -1)
+            prompt_beams = ops.slice_update(
+                prompt_beams, [0, index], next_token
             )
 
             # Compute the logits and hidden states for top-k candidate tokens.
@@ -175,14 +172,13 @@ class ContrastiveSampler(Sampler):
             similarity_scores = self.similarity(
                 hidden_states_beams, next_hidden_states_beams
             )
-            max_similarity_scores = tf.cast(
-                tf.reduce_max(similarity_scores[:, :index], axis=1),
+            # Replace all future indices with -1, the lowest similarity score.
+            score_mask = ops.expand_dims(ops.arange(max_length) < index, 0)
+            similarity_scores = ops.where(score_mask, similarity_scores, -1)
+            max_similarity_scores = ops.cast(
+                ops.max(similarity_scores, axis=1),
                 dtype=next_token_probabilities.dtype,
             )
-            if index == 0:
-                # If the index is 0, there is no previous states so we set
-                # `max_similarity_scores` the same for all beams.
-                max_similarity_scores = tf.zeros_like(max_similarity_scores)
             # The final score of each candidate token is weighted sum of
             # probability and similarity against previous tokens.
             accumulated_scores = (
@@ -197,16 +193,18 @@ class ContrastiveSampler(Sampler):
             unflat_next_hidden_states = unflatten_beams(
                 next_hidden_states_beams
             )
-            unflat_cache = tf.nest.map_structure(unflatten_beams, cache_beams)
-            best_token_indices = tf.math.argmax(unflat_score, axis=1)
+            best_token_indices = ops.argmax(unflat_score, axis=1)
 
             def gather_best_token(beams):
-                return tf.gather(
+                indices = best_token_indices
+                for axis in range(1, len(beams.shape)):
+                    indices = ops.expand_dims(indices, axis=axis)
+                best = ops.take_along_axis(
                     beams,
-                    best_token_indices,
+                    indices,
                     axis=1,
-                    batch_dims=1,
                 )
+                return ops.squeeze(best, axis=1)
 
             prompt = gather_best_token(unflat_prompt)
             # We avoid recomputing forward pass for each token by updating the
@@ -214,16 +212,18 @@ class ContrastiveSampler(Sampler):
             # next iteration step.
             logits = gather_best_token(unflat_next_logits)
             next_hidden_states = gather_best_token(unflat_next_hidden_states)
-            cache = tf.nest.map_structure(gather_best_token, unflat_cache)
+            if has_cache:
+                cache = tf.nest.map_structure(unflatten_beams, cache_beams)
+                cache = tf.nest.map_structure(gather_best_token, cache)
 
-            hidden_states = dynamic_update_slice(
+            hidden_states = ops.slice_update(
                 hidden_states,
-                next_hidden_states[:, tf.newaxis, :],
                 [0, index, 0],
+                next_hidden_states[:, None, :],
             )
             return (prompt, cache, index + 1, logits, hidden_states)
 
-        prompt, _, _, _, _ = tf.while_loop(
+        prompt, _, _, _, _ = self.run_loop(
             cond=cond,
             body=body,
             loop_vars=(prompt, cache, index, logits, hidden_states),
@@ -232,10 +232,10 @@ class ContrastiveSampler(Sampler):
         return prompt
 
     def similarity(self, h1, h2):
-        h2 = h2[..., tf.newaxis]
-        return tf.squeeze(tf.matmul(h1, h2), axis=-1) / (
-            tf.norm(h1, axis=-1) * tf.norm(h2, axis=-2)
-        )
+        h2 = ops.expand_dims(h2, -1)
+        h1_norm = ops.sqrt(ops.sum(h1 * h1, axis=-1))
+        h2_norm = ops.sqrt(ops.sum(h2 * h2, axis=-2))
+        return ops.squeeze(ops.matmul(h1, h2), axis=-1) / (h1_norm * h2_norm)
 
     def get_config(self):
         config = super().get_config()


Test patch:
diff --git a/keras_nlp/layers/modeling/masked_lm_head_test.py b/keras_nlp/layers/modeling/masked_lm_head_test.py
index f5c3b9d0..7d85da5b 100644
--- a/keras_nlp/layers/modeling/masked_lm_head_test.py
+++ b/keras_nlp/layers/modeling/masked_lm_head_test.py
@@ -29,7 +29,7 @@ class MaskedLMHeadTest(TestCase):
         )
         encoded_tokens = keras.Input(shape=(10, 16))
         positions = keras.Input(shape=(5,), dtype="int32")
-        outputs = head(encoded_tokens, masked_positions=positions)
+        outputs = head(encoded_tokens, mask_positions=positions)
         model = keras.Model((encoded_tokens, positions), outputs)
 
         token_data = ops.random.uniform(shape=(4, 10, 16))
@@ -48,7 +48,7 @@ class MaskedLMHeadTest(TestCase):
         # need to support this in the layer.
         sequence = keras.Input(shape=(10, 32))
         positions = keras.Input(shape=(5,), dtype="int32")
-        outputs = head(sequence, masked_positions=positions)
+        outputs = head(sequence, mask_positions=positions)
         model = keras.Model((sequence, positions), outputs)
         sequence_data = ops.random.uniform(shape=(4, 10, 32))
         position_data = ops.random.randint(minval=0, maxval=10, shape=(4, 5))
@@ -106,7 +106,7 @@ class MaskedLMHeadTest(TestCase):
         )
         encoded_tokens = keras.Input(shape=(10, 16))
         positions = keras.Input(shape=(5,), dtype="int32")
-        outputs = head(encoded_tokens, masked_positions=positions)
+        outputs = head(encoded_tokens, mask_positions=positions)
         model = keras.Model((encoded_tokens, positions), outputs)
 
         token_data = ops.random.uniform(shape=(4, 10, 16))
@@ -126,7 +126,7 @@ class MaskedLMHeadTest(TestCase):
         )
         encoded_tokens = keras.Input(shape=(10, 16))
         positions = keras.Input(shape=(5,), dtype="int32")
-        outputs = head(encoded_tokens, masked_positions=positions)
+        outputs = head(encoded_tokens, mask_positions=positions)
         model = keras.Model((encoded_tokens, positions), outputs)
 
         token_data = ops.random.uniform(shape=(4, 10, 16))
diff --git a/keras_nlp/samplers/beam_sampler_test.py b/keras_nlp/samplers/beam_sampler_test.py
index 88b68d87..737fe8aa 100644
--- a/keras_nlp/samplers/beam_sampler_test.py
+++ b/keras_nlp/samplers/beam_sampler_test.py
@@ -22,7 +22,6 @@ from keras_nlp.samplers.beam_sampler import BeamSampler
 from keras_nlp.tests.test_case import TestCase
 
 
-@pytest.mark.tf_only
 class BeamSamplerTest(TestCase):
     def setUp(self):
         super().setUp()
diff --git a/keras_nlp/samplers/contrastive_sampler_test.py b/keras_nlp/samplers/contrastive_sampler_test.py
index f89c708f..51d1d538 100644
--- a/keras_nlp/samplers/contrastive_sampler_test.py
+++ b/keras_nlp/samplers/contrastive_sampler_test.py
@@ -22,7 +22,6 @@ from keras_nlp.samplers.contrastive_sampler import ContrastiveSampler
 from keras_nlp.tests.test_case import TestCase
 
 
-@pytest.mark.tf_only
 class ContrastiveSamplerTest(TestCase):
     def setUp(self):
         super().setUp()