def get_runnable(
    bento_model: bentoml.Model,
):
    """
    Private API: use :obj:`~bentoml.Model.to_runnable` instead.
    """

    partial_kwargs: t.Dict[str, t.Any] = bento_model.info.options.partial_kwargs

    class TensorflowRunnable(Runnable):
        SUPPORTED_RESOURCES = ("nvidia.com/gpu", "cpu")
        SUPPORTS_CPU_MULTI_THREADING = True

        def __init__(self):
            super().__init__()
            if len(tf.config.list_physical_devices("GPU")) > 0:
                # In Multi-GPU scenarios, the visible cuda devices will be set for each Runner worker
                # by the runner's Scheduling Strategy. So that the Runnable implementation only needs
                # to find the first GPU device visible to current process.
                self.device_name = "/device:GPU:0"
            else:
                self.device_name = "/device:CPU:0"

            self.model = load_model(bento_model, device_name=self.device_name)
            self.methods_cache: t.Dict[str, t.Callable[..., t.Any]] = {}
            self.session_stack = contextlib.ExitStack()
            self.session_stack.enter_context(tf.device(self.device_name))

        def __del__(self):
            try:
                self.session_stack.close()
            except RuntimeError:
                pass

    def _gen_run_method(runnable_self: TensorflowRunnable, method_name: str):
        raw_method = getattr(runnable_self.model, method_name)
        method_partial_kwargs = partial_kwargs.get(method_name)

        output_sigs = get_output_signatures_v2(raw_method)

        if len(output_sigs) == 1:
            # if there's only one output signatures, then we can
            # define the _postprocess function without doing
            # conditional casting each time

            sig = output_sigs[0]
            if isinstance(sig, (list, tuple)):

                def _postprocess(
                    res: tuple[tf_ext.EagerTensor],
                ) -> TFRunnableOutputType:
                    return tuple(t.cast("ext.NpNDArray", r.numpy()) for r in res)

            else:

                def _postprocess(res: tf_ext.EagerTensor) -> TFRunnableOutputType:
                    return t.cast("ext.NpNDArray", res.numpy())

        else:
            # if there are no output signature or more than one output
            # signatures, the post process function need to do casting
            # depends on the real output value each time

            def _postprocess(res: TFModelOutputType) -> TFRunnableOutputType:
                if isinstance(res, (tuple, list)):
                    return tuple(t.cast("ext.NpNDArray", r.numpy()) for r in res)
                else:
                    return t.cast("ext.NpNDArray", res.numpy())

        def _run_method(
            _runnable_self: TensorflowRunnable,
            *args: TFArgType,
            **kwargs: TFArgType,
        ) -> TFRunnableOutputType:
            if method_partial_kwargs is not None:
                kwargs = dict(method_partial_kwargs, **kwargs)

            try:
                res = raw_method(*args, **kwargs)
            except ValueError:
                # Tensorflow performs type checking implicitly if users decorate with `tf.function
                # or provide `tf_signatures` when calling `save_model()`. Type checking and
                # casting is deferred to after the `ValueError` is raised to optimize performance.
                sigs = get_input_signatures_v2(raw_method)
                if not sigs:
                    raise

                try:
                    casted_args = cast_py_args_to_tf_function_args(
                        sigs[0], *args, **kwargs
                    )
                except ValueError:
                    raise

                res = raw_method(*casted_args)
            return _postprocess(res)

        return _run_method

    def add_run_method(method_name: str, options: ModelSignature):
        def run_method(
            runnable_self: TensorflowRunnable, *args: t.Any, **kwargs: t.Any
        ) -> t.Any:
            _run_method = runnable_self.methods_cache.get(
                method_name
            )  # is methods_cache nessesary?
            if not _run_method:
                _run_method = _gen_run_method(runnable_self, method_name)
                runnable_self.methods_cache[method_name] = _run_method

            return _run_method(runnable_self, *args, **kwargs)

        TensorflowRunnable.add_method(
            run_method,
            name=method_name,
            batchable=options.batchable,
            batch_dim=options.batch_dim,
            input_spec=options.input_spec,
            output_spec=options.output_spec,
        )

    for method_name, options in bento_model.info.signatures.items():
        add_run_method(method_name, options)

    return TensorflowRunnable
Private API: use :obj:`~bentoml.Model.to_runnable` instead.
src/bentoml/_internal/frameworks/tensorflow.py