RuntimeError during 16x8 quantization in TFLite converter - "Max and min for dynamic tensors should be recorded during calibration"
Hi @danielr55 

I was able to reproduce this issue. Please find the gist [here](https://colab.research.google.com/gist/pjpratik/56093b4a375c85812ab245637bbb5d74/60799.ipynb).

Seems like int16x8 support is not available for `tf.minimum` built-in op which might be causing the issue.

You can try `tf.lite.OpsSet.TFLITE_BUILTINS_INT8` if it works for your case.

Thanks. Thanks for your response, @pjpratik.

In line with your comment, I've reworked the `CircularBufferLayer` to avoid using the `tf.minimum` operation.

However, when I attempted to convert the `StreamingModel`, I encountered this error:

`RuntimeError: Max and min for dynamic tensors should be recorded during calibration: Failed for tensor streaming_model_3/circular_buffer_layer_3/add
Empty min/max for tensor streaming_model_3/circular_buffer_layer_3/add`

It seems strange to me that even the `tf.add` built-in operation is not supported by the tflite converter.

Could it be possible that I'm incorrectly implementing the `CircularBufferLayer`? I'd appreciate your insight.

Thanks!

-------------------------------------
Here's the revised implementation for the `CircularBufferLayer`:

```python
class CircularBufferLayer(tf.keras.layers.Layer):
    def __init__(self, num_features, buffer_size, stride, **kwargs):
        super().__init__(**kwargs)
        self.num_features = num_features
        self.buffer_size = buffer_size
        self.stride = stride
        self.buffer = self.add_weight(name='buffer', shape=(1, buffer_size, self.num_features),
                                      initializer='zeros', trainable=False, dtype=tf.float32)
        self.call_count = self.add_weight(name='call_count', shape=(), initializer='zeros',
                                          dtype=tf.int32, trainable=False)
        # total count, this count will never reset
        self.total_call_count = self.add_weight(name='total_call_count', shape=(), initializer='zeros',
                                                dtype=tf.int32, trainable=False)

    def call(self, inputs, **kwargs):
        # inputs should be reshaped to (1, 1, num_features) to match the buffer shape
        inputs = tf.reshape(inputs, [1, 1, self.num_features])

        # Update the buffer with the new data
        self.buffer.assign(tf.concat([self.buffer[:, 1:], inputs], axis=1))

        # Update the call count        
        self.call_count.assign(
            tf.cond(
                tf.greater_equal(self.call_count + 1, self.stride),
                true_fn=lambda: self.stride,
                false_fn=lambda: self.call_count + 1
            )
        )

        # Update the total call count
        self.total_call_count.assign(
            tf.cond(
                tf.greater_equal(self.total_call_count + 1, self.buffer_size),
                true_fn=lambda: self.buffer_size,
                false_fn=lambda: self.total_call_count + 1
            )
        )

        # If-else condition
        self.call_count.assign(
            tf.cond(tf.logical_and(tf.equal(self.call_count, self.stride), tf.greater_equal(self.total_call_count, self.buffer_size)),
                    true_fn=lambda: 0,
                    false_fn=lambda: self.call_count,
                    )
        )

        # Create a boolean flag indicating if self.call_count is 0
        # and the total number of calls to this layer is at least self.buffer_size
        flag = tf.equal(self.call_count, 0)

        # Return the buffer data and the flag
        return [self.buffer, flag]

    def reset(self):
        self.buffer.assign(tf.zeros_like(self.buffer))
        self.call_count.assign(tf.zeros_like(self.call_count))
        self.total_call_count.assign(tf.zeros_like(self.total_call_count))

    def get_config(self):
        config = {
            'buffer': self.buffer,
            'total_call_count': self.total_call_count,
            'call_count': self.call_count
        }
        base_config = super(CircularBufferLayer, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))
``` Hi @danielr55, currently this looks like a bug with the new experimental feature. My current guess is that it is somehow not calibrating on the representative data set correctly for this type of conversion.

@arfaian, can you please take a look?

I am able to replicate on tf-nightly w/ the following script (model save path modified):
Also as a [gist](https://colab.sandbox.google.com/gist/pkgoogle/d68636dfe54697b9eb1866c48707245d/60799.ipynb)
```python
import tensorflow as tf
import os
import numpy as np
import pandas as pd
import time


# Set up the environment to use CPU
tf.config.set_visible_devices([], 'GPU')


#######################
#   CircularBuffer
#######################
class CircularBufferLayer(tf.keras.layers.Layer):
    def __init__(self, num_features, buffer_size, stride, **kwargs):
        super().__init__(**kwargs)
        self.num_features = num_features
        self.buffer_size = buffer_size
        self.stride = stride
        self.buffer = self.add_weight(name='buffer', shape=(1, buffer_size, self.num_features),
                                      initializer='zeros', trainable=False, dtype=tf.float32)
        self.call_count = self.add_weight(name='call_count', shape=(), initializer='zeros',
                                          dtype=tf.int32, trainable=False)
        # total count, this count will never reset
        self.total_call_count = self.add_weight(name='total_call_count', shape=(), initializer='zeros',
                                                dtype=tf.int32, trainable=False)

    def call(self, inputs, **kwargs):
        # inputs should be reshaped to (1, 1, num_features) to match the buffer shape
        inputs = tf.reshape(inputs, [1, 1, self.num_features])

        # Update the buffer with the new data
        self.buffer.assign(tf.concat([self.buffer[:, 1:], inputs], axis=1))

        # Update the call count        
        self.call_count.assign(
            tf.cond(
                tf.greater_equal(self.call_count + 1, self.stride),
                true_fn=lambda: self.stride,
                false_fn=lambda: self.call_count + 1
            )
        )

        # Update the total call count
        self.total_call_count.assign(
            tf.cond(
                tf.greater_equal(self.total_call_count + 1, self.buffer_size),
                true_fn=lambda: self.buffer_size,
                false_fn=lambda: self.total_call_count + 1
            )
        )

        # If-else condition
        self.call_count.assign(
            tf.cond(tf.logical_and(tf.equal(self.call_count, self.stride), tf.greater_equal(self.total_call_count, self.buffer_size)),
                    true_fn=lambda: 0,
                    false_fn=lambda: self.call_count,
                    )
        )

        # Create a boolean flag indicating if self.call_count is 0
        # and the total number of calls to this layer is at least self.buffer_size
        flag = tf.equal(self.call_count, 0)

        # Return the buffer data and the flag
        return [self.buffer, flag]

    def reset(self):
        self.buffer.assign(tf.zeros_like(self.buffer))
        self.call_count.assign(tf.zeros_like(self.call_count))
        self.total_call_count.assign(tf.zeros_like(self.total_call_count))

    def get_config(self):
        config = {
            'buffer': self.buffer,
            'total_call_count': self.total_call_count,
            'call_count': self.call_count
        }
        base_config = super(CircularBufferLayer, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))


#######################
#   Streaming Model
#######################
class StreamingModel(tf.keras.Model):
    def __init__(self, input_channel, output_channel, kernel_size, stride, **kwargs):
        super().__init__(**kwargs)

        self.input_channel = input_channel
        self.output_channel = output_channel
        self.kernel_size = kernel_size
        self.stride = stride

        # set acoustic model buffer
        self.buffer = CircularBufferLayer(
            num_features=input_channel,
            buffer_size=kernel_size,
            stride=stride
        )

        # set filters
        self.conv1d = tf.keras.layers.Conv1D(
            output_channel,
            kernel_size=kernel_size,
            strides=1,
            use_bias=False,
            padding='valid',
            data_format='channels_last'
        )

    def call(self, inputs, **kwargs):
        # buffer:
        [x, flag] = self.buffer(inputs)              # output shape = [1, kernel_size, input_channel]

        x = tf.cond(flag,                       # output shape = [1, 1, output_channel]
            true_fn=lambda: self.conv1d(x),
            false_fn=lambda: tf.zeros([1, 1, self.output_channel])
        )

        x = tf.reshape(x, [1, self.output_channel])     # output shape = [1, output_channel]

        return [x, flag]

    def reset(self):
        self.buffer.reset()

if __name__ == '__main__':
    model = StreamingModel(
        input_channel=32,
        output_channel=64,
        kernel_size=5,
        stride=2
    )

    # create some dummy data with the correct shapes
    seq_len = 50
    input_data = tf.random.normal([1, seq_len, model.input_channel])

    # call the model on the dummy data - crucial to build all sub-graphs
    for t in range(seq_len):
        output = model(input_data[:, t])

    # reset the buffer after executing
    model.reset()

    # print the model's summary weights
    model.summary()

    #####################################
    #        TFLite Conversion
    #####################################
    tflite_path = 'here'
    saved_model_dir = os.path.expanduser(tflite_path)
    tf.saved_model.save(obj=model, export_dir=saved_model_dir)
    converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
    converter.experimental_enable_resource_variables = True

    # Quantize the model to 16x8
    def representative_data_gen():
        input_channel = 32
        seq_len = 50
        for t in range(seq_len):
            x = tf.random.normal([1, input_channel])
            yield [x]

    converter.inference_input_type = tf.float32
    converter.inference_output_type = tf.float32
    converter.optimizations = [tf.lite.Optimize.DEFAULT]
    converter.target_spec.supported_ops = [tf.lite.OpsSet.EXPERIMENTAL_TFLITE_BUILTINS_ACTIVATIONS_INT16_WEIGHTS_INT8]
    converter.representative_dataset = representative_data_gen
    converter.allow_custom_ops = True
    tflite_model = converter.convert()

    # Save the TFLite model.
    with tf.io.gfile.GFile(tflite_path + '.tflite', 'wb') as f:
        f.write(tflite_model)
```
### 1. System information

- **OS Platform and Distribution**:
*Ubuntu 22.04.1 LTS*
- **TensorFlow installation**:
*pip install tensorflow*
- **TensorFlow library**:
*tensorflow                    2.12.0*
*tensorflow-cpu                2.12.0*
*tensorflow-estimator          2.12.0*
*tensorflow-io-gcs-filesystem  0.23.1*
*tensorflow-model-optimization 0.7.5*


### 2. Code

Here is the Python script that can reproduce the issue:

```python
import tensorflow as tf
import os
import numpy as np
import pandas as pd
import time


# Set up the environment to use CPU
tf.config.set_visible_devices([], 'GPU')


#######################
#   CircularBuffer
#######################
class CircularBufferLayer(tf.keras.layers.Layer):
    def __init__(self, num_features, buffer_size, stride, **kwargs):
        super().__init__(**kwargs)
        self.num_features = num_features
        self.buffer_size = buffer_size
        self.stride = stride
        self.buffer = self.add_weight(name='buffer', shape=(1, buffer_size, self.num_features),
                                      initializer='zeros', trainable=False, dtype=tf.float32)
        self.call_count = self.add_weight(name='call_count', shape=(), initializer='zeros',
                                          dtype=tf.int32, trainable=False)
        # total count, this count will never reset
        self.total_call_count = self.add_weight(name='total_call_count', shape=(), initializer='zeros',
                                                dtype=tf.int32, trainable=False)

    def call(self, inputs, **kwargs):
        # inputs should be reshaped to (1, 1, num_features) to match the buffer shape
        inputs = tf.reshape(inputs, [1, 1, self.num_features])

        # Update the buffer with the new data
        self.buffer.assign(tf.concat([self.buffer[:, 1:], inputs], axis=1))

        # Update the call count
        self.call_count.assign(tf.minimum(self.call_count + 1, self.stride))
        # self.total_call_count.assign(self.total_call_count + 1)
        self.total_call_count.assign(tf.minimum(self.total_call_count + 1, self.buffer_size))

        # If-else condition
        self.call_count.assign(
            tf.cond(tf.logical_and(tf.equal(self.call_count, self.stride), tf.greater_equal(self.total_call_count, self.buffer_size)),
                    true_fn=lambda: 0,
                    false_fn=lambda: self.call_count,
                    )
        )

        # Create a boolean flag indicating if self.call_count is 0
        # and the total number of calls to this layer is at least self.buffer_size
        flag = tf.equal(self.call_count, 0)

        # Return the buffer data and the flag
        return [self.buffer, flag]

    def reset(self):
        self.buffer.assign(tf.zeros_like(self.buffer))
        self.call_count.assign(tf.zeros_like(self.call_count))
        self.total_call_count.assign(tf.zeros_like(self.total_call_count))

    def get_config(self):
        config = {
            'buffer': self.buffer,
            'total_call_count': self.total_call_count,
            'call_count': self.call_count
        }
        base_config = super(CircularBufferLayer, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))


#######################
#   Streaming Model
#######################
class StreamingModel(tf.keras.Model):
    def __init__(self, input_channel, output_channel, kernel_size, stride, **kwargs):
        super().__init__(**kwargs)

        self.input_channel = input_channel
        self.output_channel = output_channel
        self.kernel_size = kernel_size
        self.stride = stride

        # set acoustic model buffer
        self.buffer = CircularBufferLayer(
            num_features=input_channel,
            buffer_size=kernel_size,
            stride=stride
        )

        # set filters
        self.conv1d = tf.keras.layers.Conv1D(
            output_channel,
            kernel_size=kernel_size,
            strides=1,
            use_bias=False,
            padding='valid',
            data_format='channels_last'
        )

    def call(self, inputs, **kwargs):
        # buffer:
        [x, flag] = self.buffer(inputs)              # output shape = [1, kernel_size, input_channel]

        x = tf.cond(flag,                       # output shape = [1, 1, output_channel]
            true_fn=lambda: self.conv1d(x),
            false_fn=lambda: tf.zeros([1, 1, self.output_channel])
        )

        x = tf.reshape(x, [1, self.output_channel])     # output shape = [1, output_channel]

        return [x, flag]

    def reset(self):
        self.buffer.reset()

if __name__ == '__main__':
    model = StreamingModel(
        input_channel=32,
        output_channel=64,
        kernel_size=5,
        stride=2
    )

    # create some dummy data with the correct shapes
    seq_len = 50
    input_data = tf.random.normal([1, seq_len, model.input_channel])

    # call the model on the dummy data - crucial to build all sub-graphs
    for t in range(seq_len):
        output = model(input_data[:, t])

    # reset the buffer after executing
    model.reset()

    # print the model's summary weights
    model.summary()

    #####################################
    #        TFLite Conversion
    #####################################
    tflite_path = '/data/netapp2/git-repos/danielr/sbu_whispro_tflm/tflite_models/open_issue_8-16'
    saved_model_dir = os.path.expanduser(tflite_path)
    tf.saved_model.save(obj=model, export_dir=saved_model_dir)
    converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
    converter.experimental_enable_resource_variables = True

    # Quantize the model to 16x8
    def representative_data_gen():
        input_channel = 32
        seq_len = 50
        for t in range(seq_len):
            x = tf.random.normal([1, input_channel])
            yield [x]

    converter.inference_input_type = tf.float32
    converter.inference_output_type = tf.float32
    converter.optimizations = [tf.lite.Optimize.DEFAULT]
    converter.target_spec.supported_ops = [tf.lite.OpsSet.EXPERIMENTAL_TFLITE_BUILTINS_ACTIVATIONS_INT16_WEIGHTS_INT8]
    converter.representative_dataset = representative_data_gen
    converter.allow_custom_ops = True
    tflite_model = converter.convert()

    # Save the TFLite model.
    with tf.io.gfile.GFile(tflite_path + '.tflite', 'wb') as f:
        f.write(tflite_model)
```

The script defines a custom layer (CircularBufferLayer) and model (StreamingModel), uses the model on dummy data, and then attempts to quantize and convert the model into a TFLite model. The error seems to be related to the lack of dynamic range data for a particular tensor.

### 3. Failure after conversion

The conversion process fails with a `RuntimeError: Max and min for dynamic tensors should be recorded during calibration`. The error specifically highlights the tensor `streaming_model/circular_buffer_layer/Minimum`.

Here is the full traceback:

```
Traceback (most recent call last):
  File "/data/netapp2/git-repos/danielr/sbu_whispro_tflm/sbu_whispro_tflm/scripts/open_issue_8_16_bit.py", line 161, in <module>
    tflite_model = converter.convert()
  File "/home/danielr/.virtualenvs/ASR_3.8/lib/python3.8/site-packages/tensorflow/lite/python/lite.py", line 962, in wrapper
    return self._convert_and_export_metrics(convert_func, *args, **kwargs)
  File "/home/danielr/.virtualenvs/ASR_3.8/lib/python3.8/site-packages/tensorflow/lite/python/lite.py", line 940, in _convert_and_export_metrics
    result = convert_func(self, *args, **kwargs)
  File "/home/danielr/.virtualenvs/ASR_3.8/lib/python3.8/site-packages/tensorflow/lite/python/lite.py", line 1247, in convert
    return self._convert_from_saved_model(graph_def)
  File "/home/danielr/.virtualenvs/ASR_3.8/lib/python3.8/site-packages/tensorflow/lite/python/lite.py", line 1131, in _convert_from_saved_model
    return self._optimize_tflite_model(
  File "/home/danielr/.virtualenvs/ASR_3.8/lib/python3.8/site-packages/tensorflow/lite/python/convert_phase.py", line 215, in wrapper
    raise error from None  # Re-throws the exception.
  File "/home/danielr/.virtualenvs/ASR_3.8/lib/python3.8/site-packages/tensorflow/lite/python/convert_phase.py", line 205, in wrapper
    return func(*args, **kwargs)
  File "/home/danielr/.virtualenvs/ASR_3.8/lib/python3.8/site-packages/tensorflow/lite/python/lite.py", line 899, in _optimize_tflite_model
    model = self._quantize(model, q_in_type, q_out_type, q_activations_type,
  File "/home/danielr/.virtualenvs/ASR_3.8/lib/python3.8/site-packages/tensorflow/lite/python/lite.py", line 654, in _quantize
    return calibrate_quantize.calibrate_and_quantize(
  File "/home/danielr/.virtualenvs/ASR_3.8/lib/python3.8/site-packages/tensorflow/lite/python/convert_phase.py", line 215, in wrapper
    raise error from None  # Re-throws the exception.
  File "/home/danielr/.virtualenvs/ASR_3.8/lib/python3.8/site-packages/tensorflow/lite/python/convert_phase.py", line 205, in wrapper
    return func(*args, **kwargs)
  File "/home/danielr/.virtualenvs/ASR_3.8/lib/python3.8/site-packages/tensorflow/lite/python/optimize/calibrator.py", line 176, in calibrate_and_quantize
    return self._calibrator.QuantizeModel(
RuntimeError: Max and min for dynamic tensors should be recorded during calibration: Failed for tensor streaming_model/circular_buffer_layer/Minimum
Empty min/max for tensor streaming_model/circular_buffer_layer/Minimum

Process finished with exit code 1

```

### 4. (optional) RNN conversion support

Not applicable.

### 5. (optional) Any other info / logs

I have attempted to use the `converter.experimental_enable_resource_variables = True` and `converter.allow_custom_ops = True` options in the TFLite converter to allow handling of the custom layer, but the issue persists.
