Repository: mlflow/mlflow

Issue:
[BUG] Artifact relative path needs to be converted to POSIX when using Windows
### Issues Policy acknowledgement

- [X] I have read and agree to submit bug reports in accordance with the [issues policy](https://www.github.com/mlflow/mlflow/blob/master/ISSUE_POLICY.md)

### Willingness to contribute

Yes. I can contribute a fix for this bug independently.

### MLflow version

- Client: 2.5.0


### System information

- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Windows 10 and Linux Ubuntu (distribution unknown)
- **Python version**: 3.9.11

### Describe the problem

On my local machine (Windows 11), I save a model and its artifacts with `mlflow.pyfunc.save_model`. This saves relative paths for the artifact that have both '/' and '\' in the file path (e.g. '/var/azureml/models/myModel/1/artifacts/WebUpload\230810142051-2538339127\original_model').

When I deploy this model through AzureML it runs a script on Linux that calls `load_model()` on one of the artifacts. The '\' then cause 'OSError: No such file or directory'.

This is easily fixed by importing pathlib and changing mlflow/mlfow/pyfunc/model.py:203 to:
```
saved_artifact_subpath = posixpath.join(
        saved_artifacts_dir_subpath,
        pathlib.Path(os.path.relpath(path=tmp_artifact_path, start=tmp_artifacts_dir.path())).as_posix(),
)
```

### Tracking information

<!-- PLEASE KEEP BACKTICKS AND CHECK PREVIEW -->
```shell
System information: Windows 10.0.22621
Python version: 3.9.11
MLflow version: 2.5.0
MLflow module location: [c:\Users\t-grbrigham\AppData\Local\anaconda3\envs\env\lib\site-packages\mlflow\__init__.py](file:///C:/Users/t-grbrigham/AppData/Local/anaconda3/envs/env/lib/site-packages/mlflow/__init__.py)
Tracking URI: azureml://eastus2.api.azureml.ms/mlflow/v1.0/subscriptions/e0fd569c-e34a-4249-8c24-e8d723c7f054/resourceGroups/hawestra-rg/providers/Microsoft.MachineLearningServices/workspaces/grbrigham
Registry URI: azureml://eastus2.api.azureml.ms/mlflow/v1.0/subscriptions/e0fd569c-e34a-4249-8c24-e8d723c7f054/resourceGroups/hawestra-rg/providers/Microsoft.MachineLearningServices/workspaces/grbrigham
MLflow dependencies: 
  Flask: 2.3.2
  Jinja2: 3.1.2
  alembic: 1.11.2
  click: 8.1.6
  cloudpickle: 2.2.1
  databricks-cli: 0.17.7
  docker: 6.1.3
  entrypoints: 0.4
  gitpython: 3.1.32
  importlib-metadata: 6.8.0
  markdown: 3.4.4
  matplotlib: 3.7.2
  numpy: 1.22.4
  packaging: 23.1
  pandas: 1.5.3
  protobuf: 4.24.0
  pyarrow: 9.0.0
  pytz: 2023.3
  pyyaml: 6.0.1
  querystring-parser: 1.2.4
  requests: 2.31.0
  scikit-learn: 1.0.2
  scipy: 1.11.1
  sqlalchemy: 2.0.19
  sqlparse: 0.4.4
  waitress: 2.1.2
```


### Code to reproduce issue

<!-- PLEASE KEEP BACKTICKS AND CHECK PREVIEW -->
```
# On windows
artifacts = {'model' :  'models:/iris_mlflow_model/1'}
mlflow.pyfunc.save_model(
    path='model',
    code_path=['./explanation_wrapper.py'],
    python_model=model,
    artifacts=artifacts
)
mlflow.register_model('file://model', 'model')

# On linux
mlflow.pyfunc.load_model('models:/model/1')
```


### Stack trace

<!-- PLEASE KEEP BACKTICKS AND CHECK PREVIEW -->
```
  File "/var/azureml-app/score/score.py", line 253, in <module>
    model = load_model(model_path)
  File "/opt/miniconda/envs/userenv/lib/python3.9/site-packages/mlflow/pyfunc/__init__.py", line 597, in load_model
    model_impl = importlib.import_module(conf[MAIN])._load_pyfunc(data_path)
  File "/opt/miniconda/envs/userenv/lib/python3.9/site-packages/mlflow/pyfunc/model.py", line 302, in _load_pyfunc
    python_model.load_context(context=context)
  File "/var/azureml-app/azureml-models/wrapper/10/wrapper/code/explanation_wrapper.py", line 50, in load_context
    self.model = mlflow.pyfunc.load_model(context.artifacts['model'])
  File "/opt/miniconda/envs/userenv/lib/python3.9/site-packages/mlflow/pyfunc/__init__.py", line 578, in load_model
    local_path = _download_artifact_from_uri(artifact_uri=model_uri, output_path=dst_path)
  File "/opt/miniconda/envs/userenv/lib/python3.9/site-packages/mlflow/tracking/artifact_utils.py", line 100, in _download_artifact_from_uri
    return get_artifact_repository(artifact_uri=root_uri).download_artifacts(
  File "/opt/miniconda/envs/userenv/lib/python3.9/site-packages/mlflow/store/artifact/local_artifact_repo.py", line 81, in download_artifacts
    raise OSError(f"No such file or directory: '{local_artifact_path}'")
OSError: No such file or directory: '/var/azureml/models/myModel/1/artifacts/WebUpload\230810142051-2538339127\original_model'
```


### Other info / logs

<!-- PLEASE KEEP BACKTICKS AND CHECK PREVIEW -->
```
REPLACE_ME
```


### What component(s) does this bug affect?

- [X] `area/artifacts`: Artifact stores and artifact logging
- [ ] `area/build`: Build and test infrastructure for MLflow
- [ ] `area/docs`: MLflow documentation pages
- [ ] `area/examples`: Example code
- [ ] `area/gateway`: AI Gateway service, Gateway client APIs, third-party Gateway integrations
- [ ] `area/model-registry`: Model Registry service, APIs, and the fluent client calls for Model Registry
- [ ] `area/models`: MLmodel format, model serialization/deserialization, flavors
- [ ] `area/recipes`: Recipes, Recipe APIs, Recipe configs, Recipe Templates
- [ ] `area/projects`: MLproject format, project running backends
- [ ] `area/scoring`: MLflow Model server, model deployment tools, Spark UDFs
- [ ] `area/server-infra`: MLflow Tracking server backend
- [ ] `area/tracking`: Tracking Service, tracking client APIs, autologging

### What interface(s) does this bug affect?

- [ ] `area/uiux`: Front-end, user experience, plotting, JavaScript, JavaScript dev server
- [ ] `area/docker`: Docker use across MLflow's components, such as MLflow Projects and MLflow Models
- [ ] `area/sqlalchemy`: Use of SQLAlchemy in the Tracking Service or Model Registry
- [X] `area/windows`: Windows support

### What language(s) does this bug affect?

- [ ] `language/r`: R APIs and clients
- [ ] `language/java`: Java APIs and clients
- [ ] `language/new`: Proposals for new client languages

### What integration(s) does this bug affect?

- [ ] `integrations/azure`: Azure and Azure ML integrations
- [ ] `integrations/sagemaker`: SageMaker integrations
- [ ] `integrations/databricks`: Databricks integrations


Discussion:
Thanks for reporting this! Pls file a fixing PR, appreciate your contribution!

Resolving patch:
diff --git a/mlflow/pyfunc/model.py b/mlflow/pyfunc/model.py
--- a/mlflow/pyfunc/model.py
+++ b/mlflow/pyfunc/model.py
@@ -9,6 +9,7 @@
 import posixpath
 import shutil
 from abc import ABCMeta, abstractmethod
+from pathlib import Path
 from typing import Any, Dict, List, Optional
 
 import cloudpickle
@@ -234,10 +235,16 @@ def _save_model_with_class_artifacts_params(
                     artifact_uri=artifact_uri, output_path=tmp_artifacts_dir.path()
                 )
                 tmp_artifacts_config[artifact_name] = tmp_artifact_path
-                saved_artifact_subpath = posixpath.join(
-                    saved_artifacts_dir_subpath,
-                    os.path.relpath(path=tmp_artifact_path, start=tmp_artifacts_dir.path()),
-                )
+
+                tmp_artifact_path_obj = Path(tmp_artifact_path)
+                tmp_artifacts_dir_path_obj = Path(tmp_artifacts_dir.path())
+
+                relative_path = tmp_artifact_path_obj.relative_to(
+                    tmp_artifacts_dir_path_obj
+                ).as_posix()
+
+                saved_artifact_subpath = posixpath.join(saved_artifacts_dir_subpath, relative_path)
+
                 saved_artifacts_config[artifact_name] = {
                     CONFIG_KEY_ARTIFACT_RELATIVE_PATH: saved_artifact_subpath,
                     CONFIG_KEY_ARTIFACT_URI: artifact_uri,


Test patch:
diff --git a/tests/pyfunc/test_model_export_with_class_and_artifacts.py b/tests/pyfunc/test_model_export_with_class_and_artifacts.py
--- a/tests/pyfunc/test_model_export_with_class_and_artifacts.py
+++ b/tests/pyfunc/test_model_export_with_class_and_artifacts.py
@@ -27,6 +27,7 @@
 from mlflow.exceptions import MlflowException
 from mlflow.models import Model, infer_signature
 from mlflow.models.utils import _read_example
+from mlflow.pyfunc.model import _load_pyfunc
 from mlflow.store.artifact.s3_artifact_repo import S3ArtifactRepository
 from mlflow.tracking.artifact_utils import (
     _download_artifact_from_uri,
@@ -1422,3 +1423,23 @@ def test_python_model_predict_with_params():
     loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
     assert loaded_model.predict(["a", "b"], params={"foo": [0, 1]}) == ["a", "b"]
     assert loaded_model.predict(["a", "b"], params={"foo": np.array([0, 1])}) == ["a", "b"]
+
+
+def test_artifact_path_posix(sklearn_knn_model, main_scoped_model_class, tmp_path):
+    sklearn_model_path = tmp_path.joinpath("sklearn_model")
+    mlflow.sklearn.save_model(sk_model=sklearn_knn_model, path=sklearn_model_path)
+
+    def test_predict(sk_model, model_input):
+        return sk_model.predict(model_input) * 2
+
+    pyfunc_model_path = tmp_path.joinpath("pyfunc_model")
+
+    mlflow.pyfunc.save_model(
+        path=pyfunc_model_path,
+        artifacts={"sk_model": str(sklearn_model_path)},
+        conda_env=_conda_env(),
+        python_model=main_scoped_model_class(test_predict),
+    )
+
+    artifacts = _load_pyfunc(pyfunc_model_path).context.artifacts
+    assert all("\\" not in artifact_uri for artifact_uri in artifacts.values())