180 lines
8.1 KiB
Python
180 lines
8.1 KiB
Python
import contextlib
|
|
import inspect
|
|
import logging
|
|
from typing import Any, Callable
|
|
|
|
from mlflow.ml_package_versions import FLAVOR_TO_MODULE_NAME
|
|
from mlflow.utils.autologging_utils import (
|
|
AUTOLOGGING_INTEGRATIONS,
|
|
autologging_conf_lock,
|
|
get_autolog_function,
|
|
is_autolog_supported,
|
|
)
|
|
from mlflow.utils.autologging_utils.safety import revert_patches
|
|
from mlflow.utils.import_hooks import (
|
|
_post_import_hooks,
|
|
get_post_import_hooks,
|
|
register_post_import_hook,
|
|
)
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
# This flag is used to display the message only once when tracing is enabled during the evaluation.
|
|
_SHOWN_TRACE_MESSAGE_BEFORE = False
|
|
|
|
|
|
@contextlib.contextmanager
|
|
@autologging_conf_lock
|
|
def configure_autologging_for_evaluation(enable_tracing: bool = True):
|
|
"""
|
|
Temporarily override the autologging configuration for all flavors during the model evaluation.
|
|
For example, model auto-logging must be disabled during the evaluation. After the evaluation
|
|
is done, the original autologging configurations are restored.
|
|
|
|
Args:
|
|
enable_tracing (bool): Whether to enable tracing for the supported flavors during eval.
|
|
"""
|
|
original_import_hooks = {}
|
|
new_import_hooks = {}
|
|
|
|
# AUTOLOGGING_INTEGRATIONS can change during we iterate over flavors and enable/disable
|
|
# autologging, therefore, we snapshot the current configuration to restore it later.
|
|
global_config_snapshot = AUTOLOGGING_INTEGRATIONS.copy()
|
|
|
|
for flavor in FLAVOR_TO_MODULE_NAME:
|
|
if not is_autolog_supported(flavor):
|
|
continue
|
|
|
|
original_config = global_config_snapshot.get(flavor, {}).copy()
|
|
|
|
# If autologging is explicitly disabled, do nothing.
|
|
if original_config.get("disable", False):
|
|
continue
|
|
|
|
# NB: Using post-import hook to configure the autologging lazily when the target
|
|
# flavor's module is imported, rather than configuring it immediately. This is
|
|
# because the evaluation code usually only uses a subset of the supported flavors,
|
|
# hence we want to avoid unnecessary overhead of configuring all flavors.
|
|
@autologging_conf_lock
|
|
def _setup_autolog(module):
|
|
try:
|
|
autolog = get_autolog_function(flavor)
|
|
|
|
# If tracing is supported and not explicitly disabled, enable it.
|
|
if enable_tracing and _should_enable_tracing(flavor, global_config_snapshot):
|
|
new_config = {
|
|
k: False if k.startswith("log_") else v for k, v in original_config.items()
|
|
}
|
|
# log_models needs to be disabled
|
|
# so we don't init LoggedModels during eval for some GenAI flavors
|
|
new_config |= {"log_traces": True, "silent": True, "log_models": False}
|
|
_kwargs_safe_invoke(autolog, new_config)
|
|
|
|
global _SHOWN_TRACE_MESSAGE_BEFORE
|
|
if not _SHOWN_TRACE_MESSAGE_BEFORE:
|
|
_logger.info(
|
|
"Auto tracing is temporarily enabled during the model evaluation "
|
|
"for computing some metrics and debugging. To disable tracing, call "
|
|
"`mlflow.autolog(disable=True)`."
|
|
)
|
|
_SHOWN_TRACE_MESSAGE_BEFORE = True
|
|
else:
|
|
autolog(disable=True)
|
|
|
|
except Exception:
|
|
_logger.debug(f"Failed to update autologging config for {flavor}.", exc_info=True)
|
|
|
|
module = FLAVOR_TO_MODULE_NAME[flavor]
|
|
try:
|
|
original_import_hooks[module] = get_post_import_hooks(module)
|
|
new_import_hooks[module] = _setup_autolog
|
|
register_post_import_hook(_setup_autolog, module, overwrite=True)
|
|
except Exception:
|
|
_logger.debug(f"Failed to register post-import hook for {flavor}.", exc_info=True)
|
|
|
|
try:
|
|
yield
|
|
finally:
|
|
# Remove post-import hooks and patches the are registered during the evaluation.
|
|
for module, hooks in new_import_hooks.items():
|
|
# Restore original post-import hooks if any. Note that we don't use
|
|
# register_post_import_hook method to bypass some pre-checks and just
|
|
# restore the original state.
|
|
if hooks is None:
|
|
_post_import_hooks.pop(module, None)
|
|
else:
|
|
_post_import_hooks[module] = original_import_hooks[module]
|
|
|
|
# If any autologging configuration is updated, restore original autologging configurations.
|
|
for flavor, new_config in AUTOLOGGING_INTEGRATIONS.copy().items():
|
|
original_config = global_config_snapshot.get(flavor)
|
|
if original_config != new_config:
|
|
try:
|
|
autolog = get_autolog_function(flavor)
|
|
if original_config:
|
|
_kwargs_safe_invoke(autolog, original_config)
|
|
AUTOLOGGING_INTEGRATIONS[flavor] = original_config
|
|
else:
|
|
# If the original configuration is empty, autologging was not enabled before
|
|
autolog(disable=True)
|
|
# Remove all safe_patch applied by autologging
|
|
revert_patches(flavor)
|
|
# We also need to remove the config entry from AUTOLOGGING_INTEGRATIONS,
|
|
# so as not to confuse with the case user explicitly disabled autologging.
|
|
AUTOLOGGING_INTEGRATIONS.pop(flavor, None)
|
|
except ImportError:
|
|
pass
|
|
except Exception as e:
|
|
if original_config is None or (
|
|
not original_config.get("disable", False)
|
|
and not original_config.get("silent", False)
|
|
):
|
|
_logger.warning(
|
|
f"Exception raised while calling autologging for {flavor}: {e}"
|
|
)
|
|
|
|
|
|
def _should_enable_tracing(flavor: str, autologging_config: dict[str, Any]) -> bool:
|
|
"""
|
|
Check if tracing should be enabled for the given flavor during the model evaluation.
|
|
"""
|
|
# 1. Check if the autologging or tracing is globally disabled
|
|
# TODO: This check should not take precedence over the flavor-specific configuration
|
|
# set by the explicit mlflow.<flavor>.autolog() call by users.
|
|
# However, in Databricks, sometimes mlflow.<flavor>.autolog() is automatically
|
|
# called in the kernel startup, which is confused with the user's action. In
|
|
# such cases, even when user disables autologging globally, the flavor-specific
|
|
# autologging remains enabled. We are going to fix the Databricks side issue,
|
|
# and after that, we should move this check down after the flavor-specific check.
|
|
global_config = autologging_config.get("mlflow", {})
|
|
if global_config.get("disable", False) or (not global_config.get("log_traces", True)):
|
|
return False
|
|
|
|
if not _is_trace_autologging_supported(flavor):
|
|
return False
|
|
|
|
# 3. Check if tracing is explicitly disabled for the flavor
|
|
flavor_config = autologging_config.get(flavor, {})
|
|
return flavor_config.get("log_traces", True)
|
|
|
|
|
|
def _kwargs_safe_invoke(func: Callable[..., Any], kwargs: dict[str, Any]):
|
|
"""
|
|
Invoke the function with the given dictionary as keyword arguments, but only include the
|
|
arguments that are present in the function's signature.
|
|
|
|
This is particularly used for calling autolog() function with the configuration dictionary
|
|
stored in AUTOLOGGING_INTEGRATIONS. While the config keys mostly align with the autolog()'s
|
|
signature by design, some keys are not present in autolog(), such as "globally_configured".
|
|
"""
|
|
sig = inspect.signature(func)
|
|
return func(**{k: v for k, v in kwargs.items() if k in sig.parameters})
|
|
|
|
|
|
def _is_trace_autologging_supported(flavor_name: str) -> bool:
|
|
"""Check if the given flavor supports trace autologging."""
|
|
if autolog_func := get_autolog_function(flavor_name):
|
|
return "log_traces" in inspect.signature(autolog_func).parameters
|
|
return False
|