Files
2026-07-13 13:22:34 +08:00

354 lines
12 KiB
Python

import logging
import click
from mlflow.mcp.decorator import mlflow_mcp
from mlflow.models import python_api
from mlflow.models.flavor_backend_registry import get_flavor_backend
from mlflow.models.model import update_model_requirements
from mlflow.utils import cli_args
from mlflow.utils import env_manager as _EnvManager
_logger = logging.getLogger(__name__)
@click.group("models")
def commands():
"""
Deploy MLflow models locally.
To deploy a model associated with a run on a tracking server, set the MLFLOW_TRACKING_URI
environment variable to the URL of the desired server.
"""
@commands.command("serve")
@mlflow_mcp(tool_name="serve_model")
@cli_args.MODEL_URI
@cli_args.PORT
@cli_args.HOST
@cli_args.TIMEOUT
@cli_args.MODELS_WORKERS
@cli_args.ENV_MANAGER
@cli_args.NO_CONDA
@cli_args.INSTALL_MLFLOW
def serve(
model_uri,
port,
host,
timeout,
workers,
env_manager=None,
no_conda=False,
install_mlflow=False,
):
"""
Serve a model saved with MLflow by launching a webserver on the specified host and port.
The command supports models with the ``python_function`` or ``crate`` (R Function) flavor.
For information about the input data formats accepted by the webserver, see the following
documentation: https://www.mlflow.org/docs/latest/models.html#built-in-deployment-tools.
.. warning::
Models built using MLflow 1.x will require adjustments to the endpoint request payload
if executed in an environment that has MLflow 2.x installed. In 1.x, a request payload
was in the format: ``{'columns': [str], 'data': [[...]]}``. 2.x models require
payloads that are defined by the structural-defining keys of either ``dataframe_split``,
``instances``, ``inputs`` or ``dataframe_records``. See the examples below for
demonstrations of the changes to the invocation API endpoint in 2.0.
.. note::
Requests made in pandas DataFrame structures can be made in either `split` or `records`
oriented formats.
See https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_json.html for
detailed information on orientation formats for converting a pandas DataFrame to json.
Example:
.. code-block:: bash
$ mlflow models serve -m runs:/my-run-id/model-path &
# records orientation input format for serializing a pandas DataFrame
$ curl http://127.0.0.1:5000/invocations -H 'Content-Type: application/json' -d '{
"dataframe_records": [{"a":1, "b":2}, {"a":3, "b":4}, {"a":5, "b":6}]
}'
# split orientation input format for serializing a pandas DataFrame
$ curl http://127.0.0.1:5000/invocations -H 'Content-Type: application/json' -d '{
"dataframe_split": {"columns": ["a", "b"],
"index": [0, 1, 2],
"data": [[1, 2], [3, 4], [5, 6]]}
}'
# inputs format for List submission of array, tensor, or DataFrame data
$ curl http://127.0.0.1:5000/invocations -H 'Content-Type: application/json' -d '{
"inputs": [[1, 2], [3, 4], [5, 6]]
}'
# instances format for submission of Tensor data
curl http://127.0.0.1:5000/invocations -H 'Content-Type: application/json' -d '{
"instances": [
{"a": "t1", "b": [1, 2, 3]},
{"a": "t2", "b": [4, 5, 6]},
{"a": "t3", "b": [7, 8, 9]}
]
}'
"""
env_manager = _EnvManager.LOCAL if no_conda else env_manager
return get_flavor_backend(
model_uri, env_manager=env_manager, workers=workers, install_mlflow=install_mlflow
).serve(model_uri=model_uri, port=port, host=host, timeout=timeout)
class KeyValueType(click.ParamType):
name = "key=value"
def convert(self, value, param, ctx):
if "=" not in value:
self.fail(f"{value!r} is not a valid key value pair, expecting `key=value`", param, ctx)
return value.split("=", 1)
@commands.command("predict")
@mlflow_mcp(tool_name="predict_with_model")
@cli_args.MODEL_URI
@click.option(
"--input-path", "-i", default=None, help="CSV containing pandas DataFrame to predict against."
)
@click.option(
"--output-path",
"-o",
default=None,
help="File to output results to as json file. If not provided, output to stdout.",
)
@click.option(
"--content-type",
"-t",
default="json",
help="Content type of the input file. Can be one of {'json', 'csv'}.",
)
@cli_args.ENV_MANAGER
@cli_args.INSTALL_MLFLOW
@click.option(
"--pip-requirements-override",
"-r",
default=None,
help="Specify packages and versions to override the dependencies defined "
"in the model. Must be a comma-separated string like x==y,z==a.",
)
@click.option(
"--env",
default=None,
type=KeyValueType(),
multiple=True,
help="Extra environment variables to set when running the model. Must be "
"key value pairs, e.g. `--env key=value`.",
)
def predict(
model_uri,
input_data=None,
input_path=None,
content_type=python_api._CONTENT_TYPE_JSON,
output_path=None,
env_manager=_EnvManager.VIRTUALENV,
install_mlflow=False,
pip_requirements_override=None,
env=None,
):
"""
Generate predictions in json format using a saved MLflow model. For information about the input
data formats accepted by this function, see the following documentation:
https://www.mlflow.org/docs/latest/models.html#built-in-deployment-tools.
"""
return python_api.predict(
model_uri=model_uri,
input_data=input_data,
input_path=input_path,
content_type=content_type,
output_path=output_path,
env_manager=env_manager,
install_mlflow=install_mlflow,
pip_requirements_override=pip_requirements_override,
extra_envs=dict(env),
)
@commands.command("prepare-env")
@mlflow_mcp(tool_name="prepare_model_env")
@cli_args.MODEL_URI
@cli_args.ENV_MANAGER
@cli_args.INSTALL_MLFLOW
def prepare_env(
model_uri,
env_manager,
install_mlflow,
):
"""
Performs any preparation necessary to predict or serve the model, for example
downloading dependencies or initializing a conda environment. After preparation,
calling predict or serve should be fast.
"""
return get_flavor_backend(
model_uri, env_manager=env_manager, install_mlflow=install_mlflow
).prepare_env(model_uri=model_uri)
@commands.command("generate-dockerfile")
@mlflow_mcp(tool_name="generate_model_dockerfile")
@cli_args.MODEL_URI_BUILD_DOCKER
@click.option(
"--output-directory",
"-d",
default="mlflow-dockerfile",
help="Output directory where the generated Dockerfile is stored.",
)
@cli_args.ENV_MANAGER_DOCKERFILE
@cli_args.MLFLOW_HOME
@cli_args.INSTALL_JAVA
@cli_args.INSTALL_MLFLOW
def generate_dockerfile(
model_uri,
output_directory,
env_manager,
mlflow_home,
install_java,
install_mlflow,
):
"""
Generates a directory with Dockerfile whose default entrypoint serves an MLflow model at port
8080 using the python_function flavor. The generated Dockerfile is written to the specified
output directory, along with the model (if specified). This Dockerfile defines an image that
is equivalent to the one produced by ``mlflow models build-docker``.
"""
if model_uri:
_logger.info("Generating Dockerfile for model %s", model_uri)
else:
_logger.info("Generating Dockerfile")
backend = get_flavor_backend(model_uri, docker_build=True, env_manager=env_manager)
if backend.can_build_image():
backend.generate_dockerfile(
model_uri,
output_directory,
mlflow_home=mlflow_home,
install_java=install_java,
install_mlflow=install_mlflow,
)
_logger.info("Generated Dockerfile in directory %s", output_directory)
else:
_logger.error(
"Cannot build docker image for selected backend",
extra={"backend": backend.__class__.__name__},
)
raise NotImplementedError("Cannot build docker image for selected backend")
@commands.command("build-docker")
@mlflow_mcp(tool_name="build_model_docker")
@cli_args.MODEL_URI_BUILD_DOCKER
@click.option("--name", "-n", default="mlflow-pyfunc-servable", help="Name to use for built image")
@cli_args.ENV_MANAGER
@cli_args.MLFLOW_HOME
@cli_args.INSTALL_JAVA
@cli_args.INSTALL_MLFLOW
def build_docker(**kwargs):
"""
Builds a Docker image whose default entrypoint serves an MLflow model at port 8080, using the
python_function flavor. The container serves the model referenced by ``--model-uri``, if
specified when ``build-docker`` is called. If ``--model-uri`` is not specified when build_docker
is called, an MLflow Model directory must be mounted as a volume into the /opt/ml/model
directory in the container.
Building a Docker image with ``--model-uri``:
.. code:: bash
# Build a Docker image named 'my-image-name' that serves the model from run 'some-run-uuid'
# at run-relative artifact path 'my-model'
mlflow models build-docker --model-uri "runs:/some-run-uuid/my-model" --name "my-image-name"
# Serve the model
docker run -p 5001:8080 "my-image-name"
Building a Docker image without ``--model-uri``:
.. code:: bash
# Build a generic Docker image named 'my-image-name'
mlflow models build-docker --name "my-image-name"
# Mount the model stored in '/local/path/to/artifacts/model' and serve it
docker run --rm -p 5001:8080 -v /local/path/to/artifacts/model:/opt/ml/model "my-image-name"
.. important::
Since MLflow 2.10.1, the Docker image built with ``--model-uri`` does **not install Java**
for improved performance, unless the model flavor is one of ``["johnsnowlabs", "h2o",
"spark"]``. If you need to install Java for other flavors, e.g. custom Python model
that uses SparkML, please specify the ``--install-java`` flag to enforce Java installation.
NB: by default, the container will start nginx and uvicorn processes. If you don't need the
nginx process to be started (for instance if you deploy your container to Google Cloud Run),
you can disable it via the DISABLE_NGINX environment variable:
.. code:: bash
docker run -p 5001:8080 -e DISABLE_NGINX=true "my-image-name"
By default, the number of uvicorn workers is set to CPU count. If you want to set a custom
number of workers, you can set the MLFLOW_MODELS_WORKERS environment variable:
.. code:: bash
docker run -p 5001:8080 -e MLFLOW_MODELS_WORKERS=4 "my-image-name"
See https://www.mlflow.org/docs/latest/python_api/mlflow.pyfunc.html for more information on the
'python_function' flavor.
"""
python_api.build_docker(**kwargs)
@commands.command("update-pip-requirements")
@mlflow_mcp(tool_name="update_model_pip_requirements")
@cli_args.MODEL_URI
@click.argument("operation", type=click.Choice(["add", "remove"]))
@click.argument("requirement_strings", type=str, nargs=-1)
def update_pip_requirements(model_uri, operation, requirement_strings):
"""
Add or remove requirements from a model's conda.yaml and requirements.txt files.
If using a remote tracking server, please make sure to set the MLFLOW_TRACKING_URI
environment variable to the URL of the desired server.
REQUIREMENT_STRINGS is a list of pip requirements specifiers.
See below for examples.
Sample usage:
.. code::
# Add requirements using the model's "runs:/" URI
mlflow models update-pip-requirements -m runs:/<run_id>/<model_path> \\
add "pandas==1.0.0" "scikit-learn" "mlflow >= 2.8, != 2.9.0"
# Remove requirements from a local model
mlflow models update-pip-requirements -m /path/to/local/model \\
remove "torchvision" "pydantic"
Note that model registry URIs (i.e. URIs in the form ``models:/``) are not
supported, as artifacts in the model registry are intended to be read-only.
Editing requirements is read-only artifact repositories is also not supported.
If adding requirements, the function will overwrite any existing requirements
that overlap, or else append the new requirements to the existing list.
If removing requirements, the function will ignore any version specifiers,
and remove all the specified package names. Any requirements that are not
found in the existing files will be ignored.
"""
update_model_requirements(model_uri, operation, requirement_strings)
_logger.info(f"Successfully updated the requirements for the model at {model_uri}!")