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

520 lines
21 KiB
Python

import textwrap
import warnings
from typing import Any
from mlflow.ml_package_versions import _ML_PACKAGE_VERSIONS
from mlflow.utils.autologging_utils.versioning import (
get_min_max_version_and_pip_release,
)
def _create_placeholder(key: str):
return "{{ " + key + " }}"
def _replace_keys_with_placeholders(d: dict[str, Any]) -> dict[str, Any]:
return {_create_placeholder(k): v for k, v in d.items()}
def _get_indentation_of_key(line: str, placeholder: str) -> str:
index = line.find(placeholder)
return (index * " ") if index != -1 else ""
def _indent(text: str, indent: str) -> str:
"""Indent everything but first line in text."""
lines = text.splitlines()
if len(lines) <= 1:
return text
else:
first_line = lines[0]
subsequent_lines = "\n".join(list(lines[1:]))
indented_subsequent_lines = textwrap.indent(subsequent_lines, indent)
return first_line + "\n" + indented_subsequent_lines
def _replace_all(text: str, replacements: dict[str, str]) -> str:
"""
Replace all instances of replacements.keys() with their corresponding
values in text. The replacements will be inserted on the same line
with wrapping to the same level of indentation, for example:
```
Args:
param_1: {{ key }}
```
will become...
```
Args:
param_1: replaced_value_at same indentation as prior
and if there are more lines they will also
have the same indentation.
```
"""
for key, value in replacements.items():
if key in text:
indent = _get_indentation_of_key(text, key)
indented_value = _indent(value, indent)
text = text.replace(key, indented_value)
return text
class ParamDocs(dict):
"""
Represents a set of parameter documents in the docstring.
"""
def __repr__(self):
return f"ParamDocs({super().__repr__()})"
def format(self, **kwargs):
"""
Formats values to be substituted in via the format_docstring() method.
Args:
kwargs: A `dict` in the form of `{"< placeholder name >": "< value >"}`.
Returns:
A new `ParamDocs` instance with the formatted param docs.
.. code-block:: text
:caption: Example
>>> pd = ParamDocs(p1="{{ doc1 }}", p2="{{ doc2 }}")
>>> pd.format(doc1="foo", doc2="bar")
ParamDocs({'p1': 'foo', 'p2': 'bar'})
"""
replacements = _replace_keys_with_placeholders(kwargs)
return ParamDocs({k: _replace_all(v, replacements) for k, v in self.items()})
def format_docstring(self, docstring: str) -> str:
"""
Formats placeholders in `docstring`.
Args:
docstring: A docstring with placeholders to be replaced.
If provided with None, will return None.
.. code-block:: text
:caption: Example
>>> pd = ParamDocs(p1="doc1", p2="doc2
doc2 second line")
>>> docstring = '''
... Args:
... p1: {{ p1 }}
... p2: {{ p2 }}
... '''.strip()
>>> print(pd.format_docstring(docstring))
"""
if docstring is None:
return None
replacements = _replace_keys_with_placeholders(self)
lines = docstring.splitlines()
for i, line in enumerate(lines):
lines[i] = _replace_all(line, replacements)
return "\n".join(lines)
def format_docstring(param_docs):
"""
Returns a decorator that replaces param doc placeholders (e.g. '{{ param_name }}') in the
docstring of the decorated function.
Args:
param_docs: A `ParamDocs` instance or `dict`.
Returns:
A decorator to apply the formatting.
.. code-block:: text
:caption: Example
>>> param_docs = {"p1": "doc1", "p2": "doc2
doc2 second line"}
>>> @format_docstring(param_docs)
... def func(p1, p2):
... '''
... Args:
... p1: {{ p1 }}
... p2: {{ p2 }}
... '''
>>> import textwrap
>>> print(textwrap.dedent(func.__doc__).strip())
Args:
p1: doc1
p2: doc2
doc2 second line
"""
param_docs = ParamDocs(param_docs)
def decorator(func):
func.__doc__ = param_docs.format_docstring(func.__doc__)
return func
return decorator
# `{{ ... }}` represents a placeholder.
LOG_MODEL_PARAM_DOCS = ParamDocs({
"name": "Model name.",
"conda_env": (
"""Either a dictionary representation of a Conda environment or the path to a conda
environment yaml file. If provided, this describes the environment this model should be run in.
At a minimum, it should specify the dependencies contained in `get_default_conda_env()`.
If ``None``, a conda environment with pip requirements inferred by
:func:`mlflow.models.infer_pip_requirements` is added
to the model. If the requirement inference fails, it falls back to using
`get_default_pip_requirements`. pip requirements from ``conda_env`` are written to a pip
``requirements.txt`` file and the full conda environment is written to ``conda.yaml``.
The following is an *example* dictionary representation of a conda environment::
{
"name": "mlflow-env",
"channels": ["conda-forge"],
"dependencies": [
"python=3.8.15",
{
"pip": [
"{{ package_name }}==x.y.z"
],
},
],
}"""
),
"pip_requirements": (
"""Either an iterable of pip requirement strings
(e.g. ``["{{ package_name }}", "-r requirements.txt", "-c constraints.txt"]``) or the string path to
a pip requirements file on the local filesystem (e.g. ``"requirements.txt"``). If provided, this
describes the environment this model should be run in. If ``None``, a default list of requirements
is inferred by :func:`mlflow.models.infer_pip_requirements` from the current software environment.
If the requirement inference fails, it falls back to using `get_default_pip_requirements`.
Both requirements and constraints are automatically parsed and written to ``requirements.txt`` and
``constraints.txt`` files, respectively, and stored as part of the model. Requirements are also
written to the ``pip`` section of the model's conda environment (``conda.yaml``) file."""
),
"extra_pip_requirements": (
"""Either an iterable of pip
requirement strings
(e.g. ``["pandas", "-r requirements.txt", "-c constraints.txt"]``) or the string path to
a pip requirements file on the local filesystem (e.g. ``"requirements.txt"``). If provided, this
describes additional pip requirements that are appended to a default set of pip requirements
generated automatically based on the user's current software environment. Both requirements and
constraints are automatically parsed and written to ``requirements.txt`` and ``constraints.txt``
files, respectively, and stored as part of the model. Requirements are also written to the ``pip``
section of the model's conda environment (``conda.yaml``) file.
.. warning::
The following arguments can't be specified at the same time:
- ``conda_env``
- ``pip_requirements``
- ``extra_pip_requirements``
`This example <https://github.com/mlflow/mlflow/blob/master/examples/pip_requirements/pip_requirements.py>`_ demonstrates how to specify pip requirements using
``pip_requirements`` and ``extra_pip_requirements``.""" # noqa: E501
),
"signature": (
"""an instance of the :py:class:`ModelSignature <mlflow.models.ModelSignature>`
class that describes the model's inputs and outputs. If not specified but an
``input_example`` is supplied, a signature will be automatically inferred
based on the supplied input example and model. To disable automatic signature
inference when providing an input example, set ``signature`` to ``False``.
To manually infer a model signature, call
:py:func:`infer_signature() <mlflow.models.infer_signature>` on datasets
with valid model inputs, such as a training dataset with the target column
omitted, and valid model outputs, like model predictions made on the training
dataset, for example:
.. code-block:: python
from mlflow.models import infer_signature
train = df.drop_column("target_label")
predictions = ... # compute model predictions
signature = infer_signature(train, predictions)
"""
),
"metadata": ("Custom metadata dictionary passed to the model and stored in the MLmodel file."),
"input_example": (
"""one or several instances of valid model input. The input example is used
as a hint of what data to feed the model. It will be converted to a Pandas
DataFrame and then serialized to json using the Pandas split-oriented
format, or a numpy array where the example will be serialized to json
by converting it to a list. Bytes are base64-encoded. When the ``signature`` parameter is
``None``, the input example is used to infer a model signature.
"""
),
"prompt_template": (
"""A string that, if provided, will be used to format the user's input prior
to inference. The string should contain a single placeholder, ``{prompt}``, which will be
replaced with the user's input. For example: ``"Answer the following question. Q: {prompt} A:"``.
Currently, only the following pipeline types are supported:
- `feature-extraction <https://huggingface.co/transformers/main_classes/pipelines.html#transformers.FeatureExtractionPipeline>`_
- `fill-mask <https://huggingface.co/transformers/main_classes/pipelines.html#transformers.FillMaskPipeline>`_
- `summarization <https://huggingface.co/transformers/main_classes/pipelines.html#transformers.SummarizationPipeline>`_
- `text2text-generation <https://huggingface.co/transformers/main_classes/pipelines.html#transformers.Text2TextGenerationPipeline>`_
- `text-generation <https://huggingface.co/transformers/main_classes/pipelines.html#transformers.TextGenerationPipeline>`_
The following example shows how to log a text-generation pipeline with a prompt template and
use it via the ``python_function`` (pyfunc) flavor:
.. code-block:: python
import mlflow
from transformers import pipeline
# Initialize a text-generation pipeline
generator = pipeline("text-generation", model="gpt2")
# Define a prompt template. The ``{prompt}`` placeholder will be replaced
# with the raw user input at inference time.
prompt_template = "Answer the following question concisely.\\n\\nQ: {prompt}\\nA:"
example_prompt = "What is MLflow?"
# Log the model with the prompt template and an input example
with mlflow.start_run():
model_info = mlflow.transformers.log_model(
transformers_model=generator,
name="qa_text_generator",
prompt_template=prompt_template,
input_example=example_prompt,
)
# Load the model back as a pyfunc model
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
# The input to ``predict`` is the raw question string; the prompt template
# is applied internally before calling the underlying transformers pipeline.
loaded_model.predict("What is experiment tracking?")
"""
),
"code_paths": (
"""A list of local filesystem paths to Python file dependencies (or directories
containing file dependencies). These files are *prepended* to the system path when the model
is loaded. Files declared as dependencies for a given model should have relative
imports declared from a common root path if multiple files are defined with import dependencies
between them to avoid import errors when loading the model.
For a detailed explanation of ``code_paths`` functionality, recommended usage patterns and
limitations, see the
`code_paths usage guide <https://mlflow.org/docs/latest/model/dependencies.html?highlight=code_paths#saving-extra-code-with-an-mlflow-model>`_.
"""
),
"extra_files": (
"""A list containing the paths to corresponding extra files, if ``None``, no
extra files are added to the model. Remote URIs are resolved to absolute filesystem
paths. For example, consider the following ``extra_files`` list:
.. code-block:: python
extra_files = ["s3://my-bucket/path/to/my_file1", "/local-path/to/my_file2"]
In this case, the ``"my_file1"`` extra file is downloaded from S3.
Model paths will be ["extra_files/my_file1", "extra_files/my_file2"] in the model directory.
"""
),
# Only pyfunc flavor supports `infer_code_paths`.
"code_paths_pyfunc": (
"""A list of local filesystem paths to Python file dependencies (or directories
containing file dependencies). These files are *prepended* to the system path when the model
is loaded. Files declared as dependencies for a given model should have relative
imports declared from a common root path if multiple files are defined with import dependencies
between them to avoid import errors when loading the model.
You can leave ``code_paths`` argument unset but set ``infer_code_paths`` to ``True`` to let MLflow
infer the model code paths. See ``infer_code_paths`` argument doc for details.
For a detailed explanation of ``code_paths`` functionality, recommended usage patterns and
limitations, see the
`code_paths usage guide <https://mlflow.org/docs/latest/model/dependencies.html?highlight=code_paths#saving-extra-code-with-an-mlflow-model>`_.
"""
),
"infer_code_paths": (
"""If set to ``True``, MLflow automatically infers model code paths. The inferred
code path files only include necessary python module files. Only python code files
under current working directory are automatically inferable. Default value is
``False``.
.. warning::
Please ensure that the custom python module code does not contain sensitive data such as
credential token strings, otherwise they might be included in the automatic inferred code
path files and be logged to MLflow artifact repository.
If your custom python module depends on non-python files (e.g. a JSON file) with a relative
path to the module code file path, the non-python files can't be automatically inferred as the
code path file. To address this issue, you should put all used non-python files outside
your custom code directory.
If a python code file is loaded as the python ``__main__`` module, then this code file can't be
inferred as the code path file. If your model depends on classes / functions defined in
``__main__`` module, you should use `cloudpickle` to dump your model instance in order to pickle
classes / functions in ``__main__``.
.. Note:: Experimental: This parameter may change or be removed in a future release without warning.
"""
),
"save_pretrained": (
"""If set to ``False``, MLflow will not save the Transformer model weight files,
instead only saving the reference to the HuggingFace Hub model repository and its commit hash.
This is useful when you load the pretrained model from HuggingFace Hub and want to log or save
it to MLflow without modifying the model weights. In such case, specifying this flag to
``False`` will save the storage space and reduce time to save the model. Please refer to the
`Storage-Efficient Model Logging
<../../llms/transformers/large-models.html#transformers-save-pretrained-guide>`_ for more detailed
usage.
.. warning::
If the model is saved with ``save_pretrained`` set to ``False``, the model cannot be
registered to the MLflow Model Registry. In order to convert the model to the one that
can be registered, you can use :py:func:`mlflow.transformers.persist_pretrained_model()`
to download the model weights from the HuggingFace Hub and save it in the existing model
artifacts. Please refer to `Transformers flavor documentation
<../../llms/transformers/large-models.html#persist-pretrained-guide>`_
for more detailed usage.
.. code-block:: python
import mlflow.transformers
model_uri = "YOUR_MODEL_URI_LOGGED_WITH_SAVE_PRETRAINED_FALSE"
model = mlflow.transformers.persist_pretrained_model(model_uri)
mlflow.register_model(model_uri, "model_name")
.. important::
When you save the `PEFT <https://huggingface.co/docs/peft/en/index>`_ model, MLflow will
override the `save_pretrained` flag to `False` and only store the PEFT adapter weights. The
base model weights are not saved but the reference to the HuggingFace repository and
its commit hash are logged instead.
"""
),
"auth_policy": (
"""Specifies the authentication policy for the model, which includes two key components.
Note that only one of `auth_policy` or `resources` should be defined.
- **System Auth Policy**: A list of resources required to serve this model.
- **User Auth Policy**: A minimal list of scopes that the user should have access to
,in order to invoke this model.
.. Note::
Experimental: This parameter may change or be removed in a future release without warning.
"""
),
"params": "A dictionary of parameters to log with the model.",
"tags": "A dictionary of tags to log with the model.",
"model_type": "The type of the model.",
"step": "The step at which to log the model outputs and metrics",
"model_id": "The ID of the model.",
"prompts": """\
A list of prompt URIs registered in the MLflow Prompt Registry, to be associated with the model.
Each prompt URI should be in the form ``prompt:/<name>/<version>``. The prompts should be
registered in the MLflow Prompt Registry before being associated with the model.
This will create a mutual link between the model and the prompt. The associated prompts can be
seen in the model's metadata stored in the MLmodel file. From the Prompt Registry UI, you can
navigate to the model as well.
.. code-block:: python
import mlflow
prompt_template = "Hi, {name}! How are you doing today?"
# Register a prompt in the MLflow Prompt Registry
mlflow.prompts.register_prompt("my_prompt", prompt_template, description="A simple prompt")
# Log a model with the registered prompt
with mlflow.start_run():
model_info = mlflow.pyfunc.log_model(
name=MyModel(),
name="model",
prompts=["prompt:/my_prompt/1"]
)
print(model_info.prompts)
# Output: ['prompt:/my_prompt/1']
# Load the prompt
prompt = mlflow.genai.load_prompt(model_info.prompts[0])
""",
})
def get_module_min_and_max_supported_ranges(flavor_name):
"""
Extracts the minimum and maximum supported package versions from the provided module name.
The version information is provided via the yaml-to-python-script generation script in
dev/update_ml_package_versions.py which writes a python file to the importable namespace of
mlflow.ml_package_versions
Args:
flavor_name: The flavor name registered in ml_package_versions.py
Returns:
tuple of module name, minimum supported version, maximum supported version as strings.
"""
if flavor_name == "pyspark.ml":
# pyspark.ml is a special case of spark flavor
flavor_name = "spark"
module_name = _ML_PACKAGE_VERSIONS[flavor_name]["package_info"].get("module_name", flavor_name)
versions = _ML_PACKAGE_VERSIONS[flavor_name]["models"]
min_version = versions["minimum"]
max_version = versions["maximum"]
return module_name, min_version, max_version
def _do_version_compatibility_warning(msg: str):
"""
Isolate the warn call to show the warning only once.
"""
warnings.warn(msg, category=UserWarning, stacklevel=2)
def docstring_version_compatibility_warning(integration_name):
"""
Generates a docstring that can be applied as a note stating a version compatibility range for
a given flavor and optionally raises a warning if the installed version is outside of the
supported range.
Args:
integration_name: The name of the module as stored within ml-package-versions.yml
Returns:
The wrapped function with the additional docstring header applied
"""
def annotated_func(func):
# NB: if using this decorator, ensure the package name to module name reference is
# updated with the flavor's `save` and `load` functions being used within
# ml-package-version.yml file.
min_ver, max_ver, pip_release = get_min_max_version_and_pip_release(
integration_name, "models"
)
notice = (
f"The '{integration_name}' MLflow Models integration is known to be compatible with "
f"``{min_ver}`` <= ``{pip_release}`` <= ``{max_ver}``. "
f"MLflow Models integrations with {integration_name} may not succeed when used with "
"package versions outside of this range."
)
func.__doc__ = (
" .. Note:: " + notice + "\n" * 2 + func.__doc__ if func.__doc__ else notice
)
return func
return annotated_func