2222from torch .utils .data import Dataset
2323
2424from monai .apps .utils import get_logger
25- from monai .utils import CommonKeys , IgniteInfo , ensure_tuple , flatten_dict , min_version , optional_import
25+ from monai .utils import (
26+ CommonKeys ,
27+ IgniteInfo ,
28+ ensure_tuple ,
29+ flatten_dict ,
30+ min_version ,
31+ optional_import ,
32+ path_to_sqlite_uri ,
33+ path_to_uri ,
34+ )
2635
2736Events , _ = optional_import ("ignite.engine" , IgniteInfo .OPT_IMPORT_VERSION , min_version , "Events" )
2837mlflow , _ = optional_import ("mlflow" , descriptor = "Please install mlflow before using MLFlowHandler." )
@@ -68,7 +77,16 @@ class MLFlowHandler:
6877 tracking_uri: connects to a tracking URI. can also set the `MLFLOW_TRACKING_URI` environment
6978 variable to have MLflow find a URI from there. in both cases, the URI can either be
7079 an HTTP/HTTPS URI for a remote server, a database connection string, or a local path
71- to log data to a directory. The URI defaults to path `mlruns`.
80+ to log data to a directory. When no ``tracking_uri`` is provided and the
81+ ``MLFLOW_TRACKING_URI`` environment variable is unset, the handler now
82+ defaults to a local SQLite database backend at ``sqlite:///<cwd>/mlruns.db`` with
83+ artifacts stored under ``<cwd>/mlruns``. The default was changed from the filesystem
84+ (file store) backend because MLflow 3.13+ raises an exception for the file store unless
85+ ``MLFLOW_ALLOW_FILE_STORE=true`` is set; SQLite is the backend MLflow recommends and it
86+ does not raise. Any explicitly provided ``tracking_uri`` is passed through unchanged
87+ unless ``MLFLOW_TRACKING_URI`` is set (which takes precedence); local file paths and
88+ ``file://`` URIs are rejected because MLflow no longer supports the filesystem (file
89+ store) tracking backend.
7290 for more details: https://mlflow.org/docs/latest/python_api/mlflow.html#mlflow.set_tracking_uri.
7391 iteration_log: whether to log data to MLFlow when iteration completed, default to `True`.
7492 ``iteration_log`` can be also a function and it will be interpreted as an event filter
@@ -113,6 +131,11 @@ class MLFlowHandler:
113131 optimizer_param_names: parameter names in the optimizer that need to be recorded during running the
114132 workflow, default to `'lr'`.
115133 close_on_complete: whether to close the mlflow run in `complete` phase in workflow, default to False.
134+ artifact_location: the location to store run artifacts in, passed to MLflow when the experiment is
135+ created. When ``None`` and a local SQLite backend is used (from the ``tracking_uri`` argument
136+ or the ``MLFLOW_TRACKING_URI`` environment variable), it defaults to an ``mlruns`` directory
137+ next to the database file; for other backends ``None`` lets MLflow decide based on the
138+ ``tracking_uri``. Has no effect if the experiment already exists.
116139
117140 For more details of MLFlow usage, please refer to: https://mlflow.org/docs/latest/index.html.
118141
@@ -141,6 +164,7 @@ def __init__(
141164 artifacts : str | Sequence [Path ] | None = None ,
142165 optimizer_param_names : str | Sequence [str ] = "lr" ,
143166 close_on_complete : bool = False ,
167+ artifact_location : str | None = None ,
144168 ) -> None :
145169 self .iteration_log = iteration_log
146170 self .epoch_log = epoch_log
@@ -156,7 +180,39 @@ def __init__(
156180 self .experiment_param = experiment_param
157181 self .artifacts = ensure_tuple (artifacts )
158182 self .optimizer_param_names = ensure_tuple (optimizer_param_names )
159- self .client = mlflow .MlflowClient (tracking_uri = tracking_uri if tracking_uri else None )
183+ # When no tracking_uri is provided, default to a local SQLite backend instead of the
184+ # filesystem (file store) backend. MLflow 3.13+ raises for the file store unless
185+ # `MLFLOW_ALLOW_FILE_STORE=true` is set, while SQLite is the recommended backend and does
186+ # not raise. Artifacts cannot live inside a database, so by default they are stored under
187+ # the `./mlruns` directory (where the previous file store default kept them) via the
188+ # experiment `artifact_location`. Any explicitly provided tracking_uri is left unchanged.
189+ self .artifact_location = artifact_location
190+ # Resolve the effective tracking URI. The `MLFLOW_TRACKING_URI` environment variable takes
191+ # priority so it can override a hard-coded `tracking_uri` argument; both configure the
192+ # artifact location the same way.
193+ env_tracking_uri = os .environ .get ("MLFLOW_TRACKING_URI" )
194+ effective_tracking_uri = env_tracking_uri or tracking_uri
195+ # When neither is set, fall back to the local SQLite default described above.
196+ if not effective_tracking_uri :
197+ tracking_uri = effective_tracking_uri = path_to_sqlite_uri (os .path .join (os .getcwd (), "mlruns.db" ))
198+ # For a local SQLite backend, keep run artifacts in an `mlruns` directory next to the
199+ # database file (mirroring the previous file-store layout) unless the caller set
200+ # `artifact_location`. Other backends (e.g. a remote server) are left to MLflow to decide.
201+ if self .artifact_location is None and effective_tracking_uri .startswith ("sqlite:///" ):
202+ db_path = Path (effective_tracking_uri [len ("sqlite:///" ) :])
203+ self .artifact_location = path_to_uri (db_path .parent / "mlruns" )
204+ # MLflow 3.13+ refuses the filesystem (file store) tracking backend, and 3.14+ resolves
205+ # the store eagerly at client construction, so a local path or ``file://`` URI would raise
206+ # an opaque MlflowException. Reject those here with an actionable message instead.
207+ if effective_tracking_uri .startswith ("file://" ) or "://" not in effective_tracking_uri :
208+ raise ValueError (
209+ "MLflow no longer supports the filesystem (file store) tracking backend; got "
210+ f"tracking_uri={ effective_tracking_uri !r} . Use a SQLite URI "
211+ "(sqlite:///<path>/mlruns.db) or a remote tracking URI instead."
212+ )
213+ # Only the argument is passed to the client; when `MLFLOW_TRACKING_URI` took priority it
214+ # is left None so MLflow resolves the environment variable itself.
215+ self .client = mlflow .MlflowClient (tracking_uri = None if env_tracking_uri else tracking_uri )
160216 self .run_finish_status = mlflow .entities .RunStatus .to_string (mlflow .entities .RunStatus .FINISHED )
161217 self .close_on_complete = close_on_complete
162218 self .experiment = None
@@ -246,7 +302,12 @@ def _set_experiment(self):
246302 try :
247303 experiment = self .client .get_experiment_by_name (self .experiment_name )
248304 if not experiment :
249- experiment_id = self .client .create_experiment (self .experiment_name )
305+ # pass an explicit artifact_location (set for the default SQLite backend, or
306+ # by the caller) so artifacts land in the intended directory; when it is
307+ # None MLflow decides based on the tracking_uri.
308+ experiment_id = self .client .create_experiment (
309+ self .experiment_name , artifact_location = self .artifact_location
310+ )
250311 experiment = self .client .get_experiment (experiment_id )
251312 break
252313 except MlflowException as e :
@@ -338,14 +399,43 @@ def complete(self) -> None:
338399 for artifact in artifact_list :
339400 self .client .log_artifact (self .cur_run .info .run_id , artifact )
340401
402+ def _dispose_sqlite_store (self ) -> None :
403+ """
404+ Release MLflow's SQLAlchemy engine when a local SQLite tracking backend is used.
405+
406+ MLflow keeps the SQLite connection open for the lifetime of the client, which on
407+ Windows prevents the database file from being deleted. MLflow exposes no public
408+ client close/dispose API, so this reaches into its internals defensively to release
409+ the engine. It is a no-op for non-SQLite backends.
410+ """
411+ tracking_uri = getattr (self .client , "tracking_uri" , "" )
412+ if not isinstance (tracking_uri , str ) or not tracking_uri .startswith ("sqlite:" ):
413+ return
414+ store = getattr (getattr (self .client , "_tracking_client" , None ), "store" , None )
415+ if store is None :
416+ return
417+ dispose = getattr (store , "_dispose_engine" , None )
418+ if callable (dispose ):
419+ dispose ()
420+ else :
421+ engine = getattr (store , "engine" , None )
422+ if engine is not None :
423+ engine .dispose ()
424+ read_engine = getattr (store , "read_engine" , None )
425+ if read_engine is not None :
426+ read_engine .dispose ()
427+
341428 def close (self ) -> None :
342429 """
343- Stop current running logger of MLFlow.
430+ Stop current running logger of MLFlow and release local SQLite resources .
344431
345432 """
346- if self .cur_run :
347- self .client .set_terminated (self .cur_run .info .run_id , self .run_finish_status )
348- self .cur_run = None
433+ try :
434+ if self .cur_run :
435+ self .client .set_terminated (self .cur_run .info .run_id , self .run_finish_status )
436+ self .cur_run = None
437+ finally :
438+ self ._dispose_sqlite_store ()
349439
350440 def epoch_completed (self , engine : Engine ) -> None :
351441 """
0 commit comments