Repository: Project-MONAI/MONAI

Issue:
Auto3DSeg HPO with nni 'SimplePreemptiveLock' object has no attribute '_lock_file'
**Describe the bug**

The issue is also reported in https://github.com/microsoft/nni/issues/5523

The ticket suggested to use filelock==3.10, however, it doesn't work with the latest `virtualenv` in our latest MONAI dev environment.

A simple test shows 3.11 works so far

**To Reproduce**

```python
import os
import tempfile

from monai.bundle.config_parser import ConfigParser
from monai.apps import download_and_extract

from monai.apps.auto3dseg import AutoRunner

if __name__ == '__main__':
    directory = os.environ.get("MONAI_DATA_DIRECTORY")
    root_dir = tempfile.mkdtemp() if directory is None else directory
    print(root_dir)

    msd_task = "Task04_Hippocampus"
    resource = "https://msd-for-monai.s3-us-west-2.amazonaws.com/" + msd_task + ".tar"

    compressed_file = os.path.join(root_dir, msd_task + ".tar")
    dataroot = os.path.join(root_dir, msd_task)
    if not os.path.exists(dataroot):
        download_and_extract(resource, compressed_file, root_dir)

    datalist_file = "build/msd_" + msd_task.lower() + "_folds.json"

    input_cfg = {
        "name": msd_task,  # optional, it is only for your own record
        "task": "segmentation",  # optional, it is only for your own record
        "modality": "MRI",  # required
        "datalist": datalist_file,  # required
        "dataroot": dataroot,  # required
    }
    input = "./input.yaml"
    ConfigParser.export_config_file(input_cfg, input)

    runner = AutoRunner(input=input, hpo=True, ensemble=False)
    num_epoch = 2
    hpo_params = {
        "maxTrialNumber": 20,
        "maxExperimentDuration": "30m",
        "num_epochs_per_validation": 1,
        "num_images_per_batch": 1,
        "num_epochs": 2,
        "num_warmup_epochs": 1,
        "training#num_epochs": 2,
        "training#num_epochs_per_validation": 1,
        "searching#num_epochs": 2,
        "searching#num_epochs_per_validation": 1,
        "searching#num_warmup_epochs": 1,
    }
    search_space = {"learning_rate": {"_type": "choice", "_value": [0.0001, 0.01]}}
    runner.set_num_fold(num_fold=1)
    runner.set_hpo_params(params=hpo_params)
    runner.set_nni_search_space(search_space)
    runner.run()
```

**Expected behavior**
Works without error



Discussion:

Resolving patch:
diff --git a/monai/apps/auto3dseg/bundle_gen.py b/monai/apps/auto3dseg/bundle_gen.py
--- a/monai/apps/auto3dseg/bundle_gen.py
+++ b/monai/apps/auto3dseg/bundle_gen.py
@@ -52,8 +52,8 @@ class BundleAlgo(Algo):
 
         from monai.apps.auto3dseg import BundleAlgo
 
-        data_stats_yaml = "/workspace/datastats.yaml"
-        algo = BundleAlgo(template_path=../algorithms/templates/segresnet2d/configs)
+        data_stats_yaml = "../datastats.yaml"
+        algo = BundleAlgo(template_path="../algorithm_templates")
         algo.set_data_stats(data_stats_yaml)
         # algo.set_data_src("../data_src.json")
         algo.export_to_disk(".", algo_name="segresnet2d_1")
@@ -69,7 +69,8 @@ def __init__(self, template_path: PathLike):
         Create an Algo instance based on the predefined Algo template.
 
         Args:
-            template_path: path to the root of the algo template.
+            template_path: path to a folder that contains the algorithm templates.
+                Please check https://github.com/Project-MONAI/research-contributions/tree/main/auto3dseg/algorithm_templates
 
         """
 
@@ -154,7 +155,8 @@ def export_to_disk(self, output_path: str, algo_name: str, **kwargs: Any) -> Non
             os.makedirs(self.output_path, exist_ok=True)
             if os.path.isdir(self.output_path):
                 shutil.rmtree(self.output_path)
-            shutil.copytree(str(self.template_path), self.output_path)
+            # copy algorithm_templates/<Algo> to the working directory output_path
+            shutil.copytree(os.path.join(str(self.template_path), self.name), self.output_path)
         else:
             self.output_path = str(self.template_path)
         if kwargs.pop("fill_template", True):
@@ -342,10 +344,10 @@ def get_output_path(self):
 
 # default algorithms
 default_algos = {
-    "segresnet2d": dict(_target_="segresnet2d.scripts.algo.Segresnet2dAlgo", template_path="segresnet2d"),
-    "dints": dict(_target_="dints.scripts.algo.DintsAlgo", template_path="dints"),
-    "swinunetr": dict(_target_="swinunetr.scripts.algo.SwinunetrAlgo", template_path="swinunetr"),
-    "segresnet": dict(_target_="segresnet.scripts.algo.SegresnetAlgo", template_path="segresnet"),
+    "segresnet2d": dict(_target_="segresnet2d.scripts.algo.Segresnet2dAlgo"),
+    "dints": dict(_target_="dints.scripts.algo.DintsAlgo"),
+    "swinunetr": dict(_target_="swinunetr.scripts.algo.SwinunetrAlgo"),
+    "segresnet": dict(_target_="segresnet.scripts.algo.SegresnetAlgo"),
 }
 
 
@@ -377,7 +379,7 @@ def _download_algos_url(url: str, at_path: str) -> dict[str, dict[str, str]]:
 
     algos_all = deepcopy(default_algos)
     for name in algos_all:
-        algos_all[name]["template_path"] = os.path.join(at_path, algos_all[name]["template_path"])
+        algos_all[name]["template_path"] = at_path
 
     return algos_all
 
@@ -398,9 +400,7 @@ def _copy_algos_folder(folder, at_path):
     algos_all = {}
     for name in os.listdir(at_path):
         if os.path.exists(os.path.join(folder, name, "scripts", "algo.py")):
-            algos_all[name] = dict(
-                _target_=f"{name}.scripts.algo.{name.capitalize()}Algo", template_path=os.path.join(at_path, name)
-            )
+            algos_all[name] = dict(_target_=f"{name}.scripts.algo.{name.capitalize()}Algo", template_path=at_path)
             logger.info(f"Copying template: {name} -- {algos_all[name]}")
     if not algos_all:
         raise ValueError(f"Unable to find any algos in {folder}")
@@ -463,7 +463,7 @@ def __init__(
         self.algos: Any = []
         if isinstance(algos, dict):
             for algo_name, algo_params in sorted(algos.items()):
-                template_path = os.path.dirname(algo_params.get("template_path", "."))
+                template_path = algo_params.get("template_path", ".")
                 if len(template_path) > 0 and template_path not in sys.path:
                     sys.path.append(template_path)
 
@@ -486,7 +486,7 @@ def __init__(
             raise ValueError("Unexpected error algos is not a dict")
 
         self.data_stats_filename = data_stats_filename
-        self.data_src_cfg_filename = data_src_cfg_name
+        self.data_src_cfg_name = data_src_cfg_name
         self.history: list[dict] = []
 
     def set_data_stats(self, data_stats_filename: str) -> None:
@@ -502,18 +502,18 @@ def get_data_stats(self):
         """Get the filename of the data stats"""
         return self.data_stats_filename
 
-    def set_data_src(self, data_src_cfg_filename):
+    def set_data_src(self, data_src_cfg_name):
         """
         Set the data source filename
 
         Args:
-            data_src_cfg_filename: filename of data_source file
+            data_src_cfg_name: filename of data_source file
         """
-        self.data_src_cfg_filename = data_src_cfg_filename
+        self.data_src_cfg_name = data_src_cfg_name
 
     def get_data_src(self):
         """Get the data source filename"""
-        return self.data_src_cfg_filename
+        return self.data_src_cfg_name
 
     def get_history(self) -> list:
         """Get the history of the bundleAlgo object with their names/identifiers"""


Test patch:
diff --git a/tests/test_auto3dseg_bundlegen.py b/tests/test_auto3dseg_bundlegen.py
--- a/tests/test_auto3dseg_bundlegen.py
+++ b/tests/test_auto3dseg_bundlegen.py
@@ -13,6 +13,7 @@
 
 import os
 import shutil
+import sys
 import tempfile
 import unittest
 
@@ -126,6 +127,7 @@ def test_move_bundle_gen_folder(self) -> None:
         data_src_cfg = os.path.join(work_dir, "data_src_cfg.yaml")
         ConfigParser.export_config_file(data_src, data_src_cfg)
 
+        sys_path = sys.path.copy()
         with skip_if_downloading_fails():
             bundle_generator = BundleGen(
                 algo_path=work_dir,
@@ -138,6 +140,7 @@ def test_move_bundle_gen_folder(self) -> None:
         history_before = bundle_generator.get_history()
         export_bundle_algo_history(history_before)
 
+        sys.path = sys_path  # prevent the import_bundle_algo_history from using the path "work_dir/algorithm_templates"
         tempfile.TemporaryDirectory()
         work_dir_new = os.path.join(test_path, "workdir_2")
         shutil.move(work_dir, work_dir_new)