612 lines
24 KiB
Python
612 lines
24 KiB
Python
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import posixpath
|
|
import re
|
|
import tempfile
|
|
import textwrap
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
from shlex import quote
|
|
|
|
from mlflow import tracking
|
|
from mlflow.entities import RunStatus
|
|
from mlflow.environment_variables import MLFLOW_EXPERIMENT_ID, MLFLOW_RUN_ID, MLFLOW_TRACKING_URI
|
|
from mlflow.exceptions import ExecutionException, MlflowException
|
|
from mlflow.projects.submitted_run import SubmittedRun
|
|
from mlflow.projects.utils import MLFLOW_LOCAL_BACKEND_RUN_ID_CONFIG
|
|
from mlflow.protos.databricks_pb2 import INVALID_PARAMETER_VALUE
|
|
from mlflow.utils import databricks_utils, file_utils, rest_utils
|
|
from mlflow.utils.mlflow_tags import (
|
|
MLFLOW_DATABRICKS_RUN_URL,
|
|
MLFLOW_DATABRICKS_SHELL_JOB_ID,
|
|
MLFLOW_DATABRICKS_SHELL_JOB_RUN_ID,
|
|
MLFLOW_DATABRICKS_WEBAPP_URL,
|
|
)
|
|
from mlflow.utils.uri import is_databricks_uri, is_http_uri
|
|
from mlflow.version import VERSION, is_release_version
|
|
|
|
# Base directory within driver container for storing files related to MLflow
|
|
DB_CONTAINER_BASE = "/databricks/mlflow"
|
|
# Base directory within driver container for storing project archives
|
|
DB_TARFILE_BASE = posixpath.join(DB_CONTAINER_BASE, "project-tars")
|
|
# Base directory directory within driver container for storing extracted project directories
|
|
DB_PROJECTS_BASE = posixpath.join(DB_CONTAINER_BASE, "projects")
|
|
# Name to use for project directory when archiving it for upload to DBFS; the TAR will contain
|
|
# a single directory with this name
|
|
DB_TARFILE_ARCHIVE_NAME = "mlflow-project"
|
|
# Base directory within DBFS for storing code for project runs for experiments
|
|
DBFS_EXPERIMENT_DIR_BASE = "mlflow-experiments"
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
_MLFLOW_GIT_URI_REGEX = re.compile(r"^git\+https://github.com/[\w-]+/mlflow")
|
|
|
|
|
|
def _is_mlflow_git_uri(s):
|
|
return bool(_MLFLOW_GIT_URI_REGEX.match(s))
|
|
|
|
|
|
def _contains_mlflow_git_uri(libraries):
|
|
for lib in libraries:
|
|
package = lib.get("pypi", {}).get("package")
|
|
if package and _is_mlflow_git_uri(package):
|
|
return True
|
|
return False
|
|
|
|
|
|
def before_run_validations(tracking_uri, backend_config):
|
|
"""Validations to perform before running a project on Databricks."""
|
|
if backend_config is None:
|
|
raise ExecutionException(
|
|
"Backend spec must be provided when launching MLflow project runs on Databricks."
|
|
)
|
|
elif "existing_cluster_id" in backend_config:
|
|
raise MlflowException(
|
|
message=(
|
|
"MLflow Project runs on Databricks must provide a *new cluster* specification."
|
|
" Project execution against existing clusters is not currently supported. For more"
|
|
" information, see https://mlflow.org/docs/latest/projects.html"
|
|
"#run-an-mlflow-project-on-databricks"
|
|
),
|
|
error_code=INVALID_PARAMETER_VALUE,
|
|
)
|
|
if not is_databricks_uri(tracking_uri) and not is_http_uri(tracking_uri):
|
|
raise ExecutionException(
|
|
"When running on Databricks, the MLflow tracking URI must be of the form "
|
|
"'databricks' or 'databricks://profile', or a remote HTTP URI accessible to both the "
|
|
"current client and code running on Databricks. Got local tracking URI "
|
|
f"{tracking_uri}. Please specify a valid tracking URI via mlflow.set_tracking_uri or "
|
|
"by setting the MLFLOW_TRACKING_URI environment variable."
|
|
)
|
|
|
|
|
|
class DatabricksJobRunner:
|
|
"""
|
|
Helper class for running an MLflow project as a Databricks Job.
|
|
|
|
Args:
|
|
databricks_profile: Optional Databricks CLI profile to use to fetch hostname &
|
|
authentication information when making Databricks API requests.
|
|
"""
|
|
|
|
def __init__(self, databricks_profile_uri):
|
|
self.databricks_profile_uri = databricks_profile_uri
|
|
|
|
def _databricks_api_request(self, endpoint, method, **kwargs):
|
|
host_creds = databricks_utils.get_databricks_host_creds(self.databricks_profile_uri)
|
|
return rest_utils.http_request_safe(
|
|
host_creds=host_creds, endpoint=endpoint, method=method, **kwargs
|
|
)
|
|
|
|
def _jobs_runs_submit(self, req_body):
|
|
response = self._databricks_api_request(
|
|
endpoint="/api/2.0/jobs/runs/submit", method="POST", json=req_body
|
|
)
|
|
return json.loads(response.text)
|
|
|
|
def _upload_to_dbfs(self, src_path, dbfs_fuse_uri):
|
|
"""
|
|
Upload the file at `src_path` to the specified DBFS URI within the Databricks workspace
|
|
corresponding to the default Databricks CLI profile.
|
|
"""
|
|
_logger.info("=== Uploading project to DBFS path %s ===", dbfs_fuse_uri)
|
|
http_endpoint = dbfs_fuse_uri
|
|
with open(src_path, "rb") as f:
|
|
try:
|
|
self._databricks_api_request(endpoint=http_endpoint, method="POST", data=f)
|
|
except MlflowException as e:
|
|
if "Error 409" in e.message and "File already exists" in e.message:
|
|
_logger.info("=== Did not overwrite existing DBFS path %s ===", dbfs_fuse_uri)
|
|
else:
|
|
raise e
|
|
|
|
def _dbfs_path_exists(self, dbfs_path):
|
|
"""
|
|
Return True if the passed-in path exists in DBFS for the workspace corresponding to the
|
|
default Databricks CLI profile. The path is expected to be a relative path to the DBFS root
|
|
directory, e.g. 'path/to/file'.
|
|
"""
|
|
host_creds = databricks_utils.get_databricks_host_creds(self.databricks_profile_uri)
|
|
response = rest_utils.http_request(
|
|
host_creds=host_creds,
|
|
endpoint="/api/2.0/dbfs/get-status",
|
|
method="GET",
|
|
json={"path": f"/{dbfs_path}"},
|
|
)
|
|
try:
|
|
json_response_obj = json.loads(response.text)
|
|
except Exception:
|
|
raise MlflowException(
|
|
f"API request to check existence of file at DBFS path {dbfs_path} failed with "
|
|
f"status code {response.status_code}. Response body: {response.text}"
|
|
)
|
|
# If request fails with a RESOURCE_DOES_NOT_EXIST error, the file does not exist on DBFS
|
|
error_code_field = "error_code"
|
|
if error_code_field in json_response_obj:
|
|
if json_response_obj[error_code_field] == "RESOURCE_DOES_NOT_EXIST":
|
|
return False
|
|
raise ExecutionException(
|
|
f"Got unexpected error response when checking whether file {dbfs_path} "
|
|
f"exists in DBFS: {json_response_obj}"
|
|
)
|
|
return True
|
|
|
|
def _upload_project_to_dbfs(self, project_dir, experiment_id):
|
|
"""
|
|
Tars a project directory into an archive in a temp dir and uploads it to DBFS, returning
|
|
the HDFS-style URI of the tarball in DBFS (e.g. dbfs:/path/to/tar).
|
|
|
|
Args:
|
|
project_dir: Path to a directory containing an MLflow project to upload to DBFS (e.g.
|
|
a directory containing an MLproject file).
|
|
"""
|
|
with tempfile.TemporaryDirectory() as temp_tarfile_dir:
|
|
temp_tar_filename = os.path.join(temp_tarfile_dir, "project.tar.gz")
|
|
|
|
def custom_filter(x):
|
|
return None if os.path.basename(x.name) == "mlruns" else x
|
|
|
|
directory_size = file_utils._get_local_project_dir_size(project_dir)
|
|
_logger.info(
|
|
f"=== Creating tarball from {project_dir} in temp directory {temp_tarfile_dir} ==="
|
|
)
|
|
_logger.info(f"=== Total file size to compress: {directory_size} KB ===")
|
|
file_utils.make_tarfile(
|
|
temp_tar_filename, project_dir, DB_TARFILE_ARCHIVE_NAME, custom_filter=custom_filter
|
|
)
|
|
with open(temp_tar_filename, "rb") as tarred_project:
|
|
tarfile_hash = hashlib.sha256(tarred_project.read()).hexdigest()
|
|
# TODO: Get subdirectory for experiment from the tracking server
|
|
dbfs_path = posixpath.join(
|
|
DBFS_EXPERIMENT_DIR_BASE,
|
|
str(experiment_id),
|
|
"projects-code",
|
|
f"{tarfile_hash}.tar.gz",
|
|
)
|
|
tar_size = file_utils._get_local_file_size(temp_tar_filename)
|
|
dbfs_fuse_uri = posixpath.join("/dbfs", dbfs_path)
|
|
if not self._dbfs_path_exists(dbfs_path):
|
|
_logger.info(
|
|
f"=== Uploading project tarball (size: {tar_size} KB) to {dbfs_fuse_uri} ==="
|
|
)
|
|
self._upload_to_dbfs(temp_tar_filename, dbfs_fuse_uri)
|
|
_logger.info("=== Finished uploading project to %s ===", dbfs_fuse_uri)
|
|
else:
|
|
_logger.info("=== Project already exists in DBFS ===")
|
|
return dbfs_fuse_uri
|
|
|
|
def _run_shell_command_job(self, project_uri, command, env_vars, cluster_spec):
|
|
"""
|
|
Run the specified shell command on a Databricks cluster.
|
|
|
|
Args:
|
|
project_uri: URI of the project from which the shell command originates.
|
|
command: Shell command to run.
|
|
env_vars: Environment variables to set in the process running ``command``.
|
|
cluster_spec: Dictionary containing a `Databricks cluster specification
|
|
<https://docs.databricks.com/dev-tools/api/latest/jobs.html#clusterspec>`_
|
|
or a `Databricks new cluster specification
|
|
<https://docs.databricks.com/dev-tools/api/latest/jobs.html#jobsclusterspecnewcluster>`_
|
|
to use when launching a run. If you specify libraries, this function
|
|
will add MLflow to the library list. This function does not support
|
|
installation of conda environment libraries on the workers.
|
|
|
|
Returns:
|
|
ID of the Databricks job run. Can be used to query the run's status via the
|
|
Databricks `Runs Get <https://docs.databricks.com/api/latest/jobs.html#runs-get>`_ API.
|
|
"""
|
|
if is_release_version():
|
|
mlflow_lib = {"pypi": {"package": f"mlflow=={VERSION}"}}
|
|
else:
|
|
# When running a non-release version as the client the same version will not be
|
|
# available within Databricks.
|
|
_logger.warning(
|
|
"Your client is running a non-release version of MLflow. "
|
|
"This version is not available on the databricks runtime. "
|
|
"MLflow will fallback the MLflow version provided by the runtime. "
|
|
"This might lead to unforeseen issues. "
|
|
)
|
|
mlflow_lib = {"pypi": {"package": f"'mlflow<={VERSION}'"}}
|
|
|
|
# Check syntax of JSON - if it contains libraries and new_cluster, pull those out
|
|
if "new_cluster" in cluster_spec:
|
|
# Libraries are optional, so we don't require that this be specified
|
|
cluster_spec_libraries = cluster_spec.get("libraries", [])
|
|
libraries = (
|
|
# This is for development purposes only. If the cluster spec already includes
|
|
# an MLflow Git URI, then we don't append `mlflow_lib` to avoid having
|
|
# two different pip requirements for mlflow.
|
|
cluster_spec_libraries
|
|
if _contains_mlflow_git_uri(cluster_spec_libraries)
|
|
else cluster_spec_libraries + [mlflow_lib]
|
|
)
|
|
cluster_spec = cluster_spec["new_cluster"]
|
|
else:
|
|
libraries = [mlflow_lib]
|
|
|
|
# Make jobs API request to launch run.
|
|
req_body_json = {
|
|
"run_name": f"MLflow Run for {project_uri}",
|
|
"new_cluster": cluster_spec,
|
|
"shell_command_task": {"command": command, "env_vars": env_vars},
|
|
"libraries": libraries,
|
|
}
|
|
_logger.info("=== Submitting a run to execute the MLflow project... ===")
|
|
run_submit_res = self._jobs_runs_submit(req_body_json)
|
|
return run_submit_res["run_id"]
|
|
|
|
def run_databricks_spark_job(
|
|
self,
|
|
project_uri,
|
|
work_dir,
|
|
experiment_id,
|
|
cluster_spec,
|
|
run_id,
|
|
project_spec,
|
|
entry_point,
|
|
parameters,
|
|
):
|
|
from mlflow.utils.file_utils import get_or_create_tmp_dir
|
|
|
|
dbfs_fuse_uri = self._upload_project_to_dbfs(work_dir, experiment_id)
|
|
|
|
env_vars = {
|
|
MLFLOW_TRACKING_URI.name: "databricks",
|
|
MLFLOW_EXPERIMENT_ID.name: experiment_id,
|
|
MLFLOW_RUN_ID.name: run_id,
|
|
}
|
|
_logger.info(
|
|
"=== Running databricks spark job of project %s on Databricks ===", project_uri
|
|
)
|
|
|
|
if project_spec.databricks_spark_job_spec.python_file is not None:
|
|
if entry_point != "main" or parameters:
|
|
_logger.warning(
|
|
"You configured Databricks spark job python_file and parameters within the "
|
|
"MLProject file's databricks_spark_job section. '--entry-point' "
|
|
"and '--param-list' arguments specified in the 'mlflow run' command are "
|
|
"ignored."
|
|
)
|
|
job_code_file = project_spec.databricks_spark_job_spec.python_file
|
|
job_parameters = project_spec.databricks_spark_job_spec.parameters
|
|
else:
|
|
command = project_spec.get_entry_point(entry_point).compute_command(parameters, None)
|
|
command_splits = command.split(" ")
|
|
if command_splits[0] != "python":
|
|
raise MlflowException(
|
|
"Databricks spark job only supports 'python' command in the entry point "
|
|
"configuration."
|
|
)
|
|
job_code_file = command_splits[1]
|
|
job_parameters = command_splits[2:]
|
|
|
|
tmp_dir = Path(get_or_create_tmp_dir())
|
|
origin_job_code = (Path(work_dir) / job_code_file).read_text()
|
|
job_code_filename = f"{uuid.uuid4().hex}.py"
|
|
new_job_code_file = tmp_dir / job_code_filename
|
|
|
|
project_dir, extracting_tar_command = _get_project_dir_and_extracting_tar_command(
|
|
dbfs_fuse_uri
|
|
)
|
|
|
|
env_vars_str = json.dumps(env_vars)
|
|
new_job_code_file.write_text(
|
|
f"""
|
|
import os
|
|
import subprocess
|
|
os.environ.update({env_vars_str})
|
|
|
|
extracting_tar_command = \"\"\"
|
|
{extracting_tar_command}
|
|
\"\"\"
|
|
subprocess.check_call(extracting_tar_command, shell=True)
|
|
|
|
os.chdir('{project_dir}')
|
|
{origin_job_code}
|
|
"""
|
|
)
|
|
|
|
dbfs_job_code_file_path = posixpath.join(
|
|
DBFS_EXPERIMENT_DIR_BASE,
|
|
str(experiment_id),
|
|
"projects-code",
|
|
job_code_filename,
|
|
)
|
|
job_code_file_dbfs_fuse_uri = posixpath.join("/dbfs", dbfs_job_code_file_path)
|
|
if not self._dbfs_path_exists(dbfs_job_code_file_path):
|
|
self._upload_to_dbfs(str(new_job_code_file), job_code_file_dbfs_fuse_uri)
|
|
|
|
libraries_config = [
|
|
{"pypi": {"package": python_lib}}
|
|
for python_lib in project_spec.databricks_spark_job_spec.python_libraries
|
|
]
|
|
# Make Databricks Spark jobs API request to launch run.
|
|
req_body_json = {
|
|
"run_name": f"MLflow Run for {project_uri}",
|
|
"new_cluster": cluster_spec,
|
|
"libraries": libraries_config,
|
|
"spark_python_task": {
|
|
"python_file": f"dbfs:/{dbfs_job_code_file_path}",
|
|
"parameters": job_parameters,
|
|
},
|
|
}
|
|
|
|
_logger.info("=== Submitting a run to execute the MLflow project... ===")
|
|
run_submit_res = self._jobs_runs_submit(req_body_json)
|
|
return run_submit_res["run_id"]
|
|
|
|
def run_databricks(
|
|
self,
|
|
uri,
|
|
entry_point,
|
|
work_dir,
|
|
parameters,
|
|
experiment_id,
|
|
cluster_spec,
|
|
run_id,
|
|
env_manager,
|
|
):
|
|
tracking_uri = _get_tracking_uri_for_run()
|
|
dbfs_fuse_uri = self._upload_project_to_dbfs(work_dir, experiment_id)
|
|
|
|
env_vars = {
|
|
MLFLOW_TRACKING_URI.name: tracking_uri,
|
|
MLFLOW_EXPERIMENT_ID.name: experiment_id,
|
|
}
|
|
_logger.info("=== Running entry point %s of project %s on Databricks ===", entry_point, uri)
|
|
# Launch run on Databricks
|
|
command = _get_databricks_run_cmd(
|
|
dbfs_fuse_uri, run_id, entry_point, parameters, env_manager
|
|
)
|
|
return self._run_shell_command_job(uri, command, env_vars, cluster_spec)
|
|
|
|
def _get_status(self, databricks_run_id):
|
|
run_state = self.get_run_result_state(databricks_run_id)
|
|
if run_state is None:
|
|
return RunStatus.RUNNING
|
|
if run_state == "SUCCESS":
|
|
return RunStatus.FINISHED
|
|
return RunStatus.FAILED
|
|
|
|
def get_status(self, databricks_run_id):
|
|
return RunStatus.to_string(self._get_status(databricks_run_id))
|
|
|
|
def get_run_result_state(self, databricks_run_id):
|
|
"""
|
|
Get the run result state (string) of a Databricks job run.
|
|
|
|
Args:
|
|
databricks_run_id: Integer Databricks job run ID.
|
|
|
|
Returns:
|
|
`RunResultState <https://docs.databricks.com/api/latest/jobs.html#runresultstate>`_ or
|
|
None if the run is still active.
|
|
"""
|
|
res = self.jobs_runs_get(databricks_run_id)
|
|
return res["state"].get("result_state", None)
|
|
|
|
def jobs_runs_cancel(self, databricks_run_id):
|
|
response = self._databricks_api_request(
|
|
endpoint="/api/2.0/jobs/runs/cancel", method="POST", json={"run_id": databricks_run_id}
|
|
)
|
|
return json.loads(response.text)
|
|
|
|
def jobs_runs_get(self, databricks_run_id):
|
|
response = self._databricks_api_request(
|
|
endpoint="/api/2.0/jobs/runs/get", method="GET", params={"run_id": databricks_run_id}
|
|
)
|
|
return json.loads(response.text)
|
|
|
|
|
|
def _get_tracking_uri_for_run():
|
|
uri = tracking.get_tracking_uri()
|
|
if uri.startswith("databricks"):
|
|
return "databricks"
|
|
return uri
|
|
|
|
|
|
def _get_cluster_mlflow_run_cmd(project_dir, run_id, entry_point, parameters, env_manager):
|
|
cmd = [
|
|
"mlflow",
|
|
"run",
|
|
project_dir,
|
|
"--entry-point",
|
|
entry_point,
|
|
]
|
|
if env_manager:
|
|
cmd += ["--env-manager", env_manager]
|
|
mlflow_run_arr = list(map(quote, cmd))
|
|
if run_id:
|
|
mlflow_run_arr.extend(["-c", json.dumps({MLFLOW_LOCAL_BACKEND_RUN_ID_CONFIG: run_id})])
|
|
if parameters:
|
|
for key, value in parameters.items():
|
|
mlflow_run_arr.extend(["-P", f"{key}={value}"])
|
|
return mlflow_run_arr
|
|
|
|
|
|
def _get_project_dir_and_extracting_tar_command(dbfs_fuse_tar_uri):
|
|
# Strip ".gz" and ".tar" file extensions from base filename of the tarfile
|
|
tar_hash = posixpath.splitext(posixpath.splitext(posixpath.basename(dbfs_fuse_tar_uri))[0])[0]
|
|
container_tar_path = posixpath.abspath(
|
|
posixpath.join(DB_TARFILE_BASE, posixpath.basename(dbfs_fuse_tar_uri))
|
|
)
|
|
project_dir = posixpath.join(DB_PROJECTS_BASE, tar_hash)
|
|
command = textwrap.dedent(
|
|
f"""
|
|
# Make local directories in the container into which to copy/extract the tarred project
|
|
mkdir -p {DB_TARFILE_BASE} {DB_PROJECTS_BASE} &&
|
|
# Rsync from DBFS FUSE to avoid copying archive into local filesystem if it already exists
|
|
rsync -a -v --ignore-existing {dbfs_fuse_tar_uri} {DB_TARFILE_BASE} &&
|
|
# Extract project into a temporary directory. We don't extract directly into the desired
|
|
# directory as tar extraction isn't guaranteed to be atomic
|
|
cd $(mktemp -d) &&
|
|
tar --no-same-owner -xzvf {container_tar_path} &&
|
|
# Atomically move the extracted project into the desired directory
|
|
mv -T {DB_TARFILE_ARCHIVE_NAME} {project_dir}"""
|
|
)
|
|
return project_dir, command
|
|
|
|
|
|
def _get_databricks_run_cmd(dbfs_fuse_tar_uri, run_id, entry_point, parameters, env_manager):
|
|
"""
|
|
Generate MLflow CLI command to run on Databricks cluster in order to launch a run on Databricks.
|
|
"""
|
|
project_dir, extracting_tar_command = _get_project_dir_and_extracting_tar_command(
|
|
dbfs_fuse_tar_uri
|
|
)
|
|
mlflow_run_arr = _get_cluster_mlflow_run_cmd(
|
|
project_dir,
|
|
run_id,
|
|
entry_point,
|
|
parameters,
|
|
env_manager,
|
|
)
|
|
mlflow_run_cmd = " ".join([quote(elem) for elem in mlflow_run_arr])
|
|
shell_command = textwrap.dedent(
|
|
f"""
|
|
export PATH=$PATH:$DB_HOME/python/bin &&
|
|
mlflow --version &&
|
|
{extracting_tar_command} &&
|
|
{mlflow_run_cmd}
|
|
"""
|
|
)
|
|
return ["bash", "-c", shell_command]
|
|
|
|
|
|
def run_databricks(
|
|
remote_run, uri, entry_point, work_dir, parameters, experiment_id, cluster_spec, env_manager
|
|
):
|
|
"""
|
|
Run the project at the specified URI on Databricks, returning a ``SubmittedRun`` that can be
|
|
used to query the run's status or wait for the resulting Databricks Job run to terminate.
|
|
"""
|
|
run_id = remote_run.info.run_id
|
|
db_job_runner = DatabricksJobRunner(databricks_profile_uri=tracking.get_tracking_uri())
|
|
db_run_id = db_job_runner.run_databricks(
|
|
uri, entry_point, work_dir, parameters, experiment_id, cluster_spec, run_id, env_manager
|
|
)
|
|
submitted_run = DatabricksSubmittedRun(db_run_id, run_id, db_job_runner)
|
|
submitted_run._print_description_and_log_tags()
|
|
return submitted_run
|
|
|
|
|
|
def run_databricks_spark_job(
|
|
remote_run,
|
|
uri,
|
|
work_dir,
|
|
experiment_id,
|
|
cluster_spec,
|
|
project_spec,
|
|
entry_point,
|
|
parameters,
|
|
):
|
|
run_id = remote_run.info.run_id
|
|
db_job_runner = DatabricksJobRunner(databricks_profile_uri=tracking.get_tracking_uri())
|
|
db_run_id = db_job_runner.run_databricks_spark_job(
|
|
uri,
|
|
work_dir,
|
|
experiment_id,
|
|
cluster_spec,
|
|
run_id,
|
|
project_spec,
|
|
entry_point,
|
|
parameters,
|
|
)
|
|
submitted_run = DatabricksSubmittedRun(db_run_id, run_id, db_job_runner)
|
|
submitted_run._print_description_and_log_tags()
|
|
return submitted_run
|
|
|
|
|
|
class DatabricksSubmittedRun(SubmittedRun):
|
|
"""
|
|
Instance of SubmittedRun corresponding to a Databricks Job run launched to run an MLflow
|
|
project. Note that run_id may be None, e.g. if we did not launch the run against a tracking
|
|
server accessible to the local client.
|
|
|
|
Args:
|
|
databricks_run_id: Run ID of the launched Databricks Job.
|
|
mlflow_run_id: ID of the MLflow project run.
|
|
databricks_job_runner: Instance of ``DatabricksJobRunner`` used to make Databricks API
|
|
requests.
|
|
"""
|
|
|
|
# How often to poll run status when waiting on a run
|
|
POLL_STATUS_INTERVAL = 30
|
|
|
|
def __init__(self, databricks_run_id, mlflow_run_id, databricks_job_runner):
|
|
super().__init__()
|
|
self._databricks_run_id = databricks_run_id
|
|
self._mlflow_run_id = mlflow_run_id
|
|
self._job_runner = databricks_job_runner
|
|
|
|
def _print_description_and_log_tags(self):
|
|
_logger.info(
|
|
"=== Launched MLflow run as Databricks job run with ID %s."
|
|
" Getting run status page URL... ===",
|
|
self._databricks_run_id,
|
|
)
|
|
run_info = self._job_runner.jobs_runs_get(self._databricks_run_id)
|
|
jobs_page_url = run_info["run_page_url"]
|
|
_logger.info("=== Check the run's status at %s ===", jobs_page_url)
|
|
host_creds = databricks_utils.get_databricks_host_creds(
|
|
self._job_runner.databricks_profile_uri
|
|
)
|
|
tracking.MlflowClient().set_tag(
|
|
self._mlflow_run_id, MLFLOW_DATABRICKS_RUN_URL, jobs_page_url
|
|
)
|
|
tracking.MlflowClient().set_tag(
|
|
self._mlflow_run_id, MLFLOW_DATABRICKS_SHELL_JOB_RUN_ID, self._databricks_run_id
|
|
)
|
|
tracking.MlflowClient().set_tag(
|
|
self._mlflow_run_id, MLFLOW_DATABRICKS_WEBAPP_URL, host_creds.host
|
|
)
|
|
job_id = run_info.get("job_id")
|
|
# In some releases of Databricks we do not return the job ID. We start including it in DB
|
|
# releases 2.80 and above.
|
|
if job_id is not None:
|
|
tracking.MlflowClient().set_tag(
|
|
self._mlflow_run_id, MLFLOW_DATABRICKS_SHELL_JOB_ID, job_id
|
|
)
|
|
|
|
@property
|
|
def run_id(self):
|
|
return self._mlflow_run_id
|
|
|
|
def wait(self):
|
|
result_state = self._job_runner.get_run_result_state(self._databricks_run_id)
|
|
while result_state is None:
|
|
time.sleep(self.POLL_STATUS_INTERVAL)
|
|
result_state = self._job_runner.get_run_result_state(self._databricks_run_id)
|
|
return result_state == "SUCCESS"
|
|
|
|
def cancel(self):
|
|
self._job_runner.jobs_runs_cancel(self._databricks_run_id)
|
|
self.wait()
|
|
|
|
def get_status(self):
|
|
return self._job_runner.get_status(self._databricks_run_id)
|