Skip to content

Commit 9078a72

Browse files
aymuos15ericspodpre-commit-ci[bot]
authored
Replace pickle with JSON in Auto3DSeg algo serialization (#8695)
## Summary - Replace `algo_to_pickle()` with `algo_to_json()` using compact JSON + MONAI's existing `_target_` ConfigParser pattern for truly pickle-free algo metadata serialization - Add `_make_json_serializable()` helper to handle numpy arrays, tensors, Path objects - Backward compatible: `algo_from_json()` can still load legacy `.pkl` files (with deprecation warning) - Keep `algo_to_pickle()` / `algo_from_pickle()` as deprecated aliases **Note:** Model weights still use `torch.save` separately—this PR focuses on the algo object serialization. ## Test plan - [x] Linting passes (`./runtests.sh --codeformat`, `./runtests.sh --ruff`) - [x] Unit tests for `_make_json_serializable()` and `_add_path_with_parent()` helpers - [ ] Integration test with `tests/apps/test_auto3dseg_bundlegen.py` (requires optional deps) Fixes #8586 --------- Signed-off-by: Soumya Snigdha Kundu <soumya_snigdha.kundu@kcl.ac.uk> Signed-off-by: Soumya Snigdha Kundu <soumyawork15@gmail.com> Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 2a98f63 commit 9078a72

10 files changed

Lines changed: 479 additions & 82 deletions

File tree

monai/apps/auto3dseg/auto_runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from monai.apps.auto3dseg.hpo_gen import NNIGen
2727
from monai.apps.auto3dseg.utils import export_bundle_algo_history, import_bundle_algo_history
2828
from monai.apps.utils import get_logger
29-
from monai.auto3dseg.utils import algo_to_pickle
29+
from monai.auto3dseg.utils import algo_to_json
3030
from monai.bundle import ConfigParser
3131
from monai.transforms import SaveImage
3232
from monai.utils import AlgoKeys, has_option, look_up_option, optional_import
@@ -740,7 +740,7 @@ def _train_algo_in_sequence(self, history: list[dict[str, Any]]) -> None:
740740
acc = algo.get_score()
741741

742742
algo_meta_data = {str(AlgoKeys.SCORE): acc}
743-
algo_to_pickle(algo, template_path=algo.template_path, **algo_meta_data)
743+
algo_to_json(algo, template_path=algo.template_path, **algo_meta_data)
744744

745745
def _train_algo_in_nni(self, history: list[dict[str, Any]]) -> None:
746746
"""

monai/apps/auto3dseg/bundle_gen.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
_prepare_cmd_torchrun,
3737
_run_cmd_bcprun,
3838
_run_cmd_torchrun,
39-
algo_to_pickle,
39+
algo_to_json,
4040
)
4141
from monai.bundle.config_parser import ConfigParser
4242
from monai.config import PathLike
@@ -367,6 +367,39 @@ def get_output_path(self):
367367
"""Returns the algo output paths to find the algo scripts and configs."""
368368
return self.output_path
369369

370+
def state_dict(self) -> dict:
371+
"""
372+
Return state for serialization.
373+
374+
Returns:
375+
A dictionary containing the BundleAlgo state to serialize.
376+
377+
Note:
378+
template_path is excluded as it is determined dynamically at load time
379+
based on which path successfully imports the Algo class.
380+
"""
381+
return {
382+
"data_stats_files": self.data_stats_files,
383+
"data_list_file": self.data_list_file,
384+
"mlflow_tracking_uri": self.mlflow_tracking_uri,
385+
"mlflow_experiment_name": self.mlflow_experiment_name,
386+
"output_path": self.output_path,
387+
"name": self.name,
388+
"best_metric": self.best_metric,
389+
"fill_records": self.fill_records,
390+
"device_setting": self.device_setting,
391+
}
392+
393+
def load_state_dict(self, state: dict) -> None:
394+
"""
395+
Restore state from a dictionary.
396+
397+
Args:
398+
state: A dictionary containing the state to restore.
399+
"""
400+
for key, value in state.items():
401+
setattr(self, key, value)
402+
370403

371404
# path to download the algo_templates
372405
default_algo_zip = (
@@ -659,7 +692,7 @@ def generate(
659692
else:
660693
gen_algo.export_to_disk(output_folder, name, fold=f_id)
661694

662-
algo_to_pickle(gen_algo, template_path=algo.template_path)
695+
algo_to_json(gen_algo, template_path=algo.template_path)
663696
self.history.append(
664697
{AlgoKeys.ID: name, AlgoKeys.ALGO: gen_algo}
665698
) # track the previous, may create a persistent history

monai/apps/auto3dseg/hpo_gen.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from monai.apps.auto3dseg.bundle_gen import BundleAlgo
2121
from monai.apps.utils import get_logger
22-
from monai.auto3dseg import Algo, AlgoGen, algo_from_pickle, algo_to_pickle
22+
from monai.auto3dseg import Algo, AlgoGen, algo_from_json, algo_to_json
2323
from monai.bundle.config_parser import ConfigParser
2424
from monai.config import PathLike
2525
from monai.utils import optional_import
@@ -36,7 +36,7 @@ class HPOGen(AlgoGen):
3636
"""
3737
The base class for hyperparameter optimization (HPO) interfaces to generate algos in the Auto3Dseg pipeline.
3838
The auto-generated algos are saved at their ``output_path`` on the disk. The files in the ``output_path``
39-
may contain scripts that define the algo, configuration files, and pickle files that save the internal states
39+
may contain scripts that define the algo, configuration files, and JSON files that save the internal states
4040
of the algo before/after the training. Compared to the BundleGen class, HPOGen generates Algo on-the-fly, so
4141
training and algo generation may be executed alternatively and take a long time to finish the generation process.
4242
@@ -72,7 +72,7 @@ class NNIGen(HPOGen):
7272
7373
Args:
7474
algo: an Algo object (e.g. BundleAlgo) with defined methods: ``get_output_path`` and train
75-
and supports saving to and loading from pickle files via ``algo_from_pickle`` and ``algo_to_pickle``.
75+
and supports saving to and loading via ``algo_from_json`` and ``algo_to_json``.
7676
params: a set of parameter to override the algo if override is supported by Algo subclass.
7777
7878
Examples::
@@ -81,16 +81,16 @@ class NNIGen(HPOGen):
8181
├── algorithm_templates
8282
│ └── unet
8383
├── unet_0
84-
│ ├── algo_object.pkl
84+
│ ├── algo_object.json
8585
│ ├── configs
8686
│ └── scripts
8787
├── unet_0_learning_rate_0.01
88-
│ ├── algo_object.pkl
88+
│ ├── algo_object.json
8989
│ ├── configs
9090
│ ├── model_fold0
9191
│ └── scripts
9292
└── unet_0_learning_rate_0.1
93-
├── algo_object.pkl
93+
├── algo_object.json
9494
├── configs
9595
├── model_fold0
9696
└── scripts
@@ -129,10 +129,10 @@ def __init__(self, algo: Algo | None = None, params: dict | None = None):
129129
else:
130130
self.algo = algo
131131

132-
self.obj_filename = algo_to_pickle(self.algo, template_path=self.algo.template_path)
132+
self.obj_filename = algo_to_json(self.algo, template_path=self.algo.template_path)
133133

134134
def get_obj_filename(self):
135-
"""Return the filename of the dumped pickle algo object."""
135+
"""Return the filename of the dumped algo object."""
136136
return self.obj_filename
137137

138138
def print_bundle_algo_instruction(self):
@@ -190,7 +190,7 @@ def generate(self, output_folder: str = ".") -> None:
190190
task_id = self.get_task_id()
191191
task_prefix = os.path.basename(self.algo.get_output_path())
192192
write_path = os.path.join(output_folder, task_prefix + task_id)
193-
self.obj_filename = os.path.join(write_path, "algo_object.pkl")
193+
self.obj_filename = os.path.join(write_path, "algo_object.json")
194194

195195
if isinstance(self.algo, BundleAlgo):
196196
self.algo.export_to_disk(
@@ -214,15 +214,15 @@ def run_algo(self, obj_filename: str, output_folder: str = ".", template_path: P
214214
The python interface for NNI to run.
215215
216216
Args:
217-
obj_filename: the pickle-exported Algo object.
217+
obj_filename: the serialized Algo object.
218218
output_folder: the root path of the algorithms templates.
219219
template_path: the algorithm_template. It must contain algo.py in the follow path:
220220
``{algorithm_templates_dir}/{network}/scripts/algo.py``
221221
"""
222222
if not os.path.isfile(obj_filename):
223223
raise ValueError(f"{obj_filename} is not found")
224224

225-
self.algo, algo_meta_data = algo_from_pickle(obj_filename, template_path=template_path)
225+
self.algo, algo_meta_data = algo_from_json(obj_filename, template_path=template_path)
226226

227227
# step 1 sample hyperparams
228228
params = self.get_hyperparameters()
@@ -235,7 +235,7 @@ def run_algo(self, obj_filename: str, output_folder: str = ".", template_path: P
235235
acc = self.algo.get_score()
236236
algo_meta_data = {str(AlgoKeys.SCORE): acc}
237237

238-
algo_to_pickle(self.algo, template_path=self.algo.template_path, **algo_meta_data)
238+
algo_to_json(self.algo, template_path=self.algo.template_path, **algo_meta_data)
239239
self.set_score(acc)
240240

241241

@@ -250,7 +250,7 @@ class OptunaGen(HPOGen):
250250
251251
Args:
252252
algo: an Algo object (e.g. BundleAlgo). The object must at least define two methods: get_output_path and train
253-
and supports saving to and loading from pickle files via ``algo_from_pickle`` and ``algo_to_pickle``.
253+
and supports saving to and loading via ``algo_from_json`` and ``algo_to_json``.
254254
params: a set of parameter to override the algo if override is supported by Algo subclass.
255255
256256
Examples::
@@ -259,16 +259,16 @@ class OptunaGen(HPOGen):
259259
├── algorithm_templates
260260
│ └── unet
261261
├── unet_0
262-
│ ├── algo_object.pkl
262+
│ ├── algo_object.json
263263
│ ├── configs
264264
│ └── scripts
265265
├── unet_0_learning_rate_0.01
266-
│ ├── algo_object.pkl
266+
│ ├── algo_object.json
267267
│ ├── configs
268268
│ ├── model_fold0
269269
│ └── scripts
270270
└── unet_0_learning_rate_0.1
271-
├── algo_object.pkl
271+
├── algo_object.json
272272
├── configs
273273
├── model_fold0
274274
└── scripts
@@ -296,10 +296,10 @@ def __init__(self, algo: Algo | None = None, params: dict | None = None) -> None
296296
else:
297297
self.algo = algo
298298

299-
self.obj_filename = algo_to_pickle(self.algo, template_path=self.algo.template_path)
299+
self.obj_filename = algo_to_json(self.algo, template_path=self.algo.template_path)
300300

301301
def get_obj_filename(self):
302-
"""Return the dumped pickle object of algo."""
302+
"""Return the dumped object of algo."""
303303
return self.obj_filename
304304

305305
def get_hyperparameters(self):
@@ -329,7 +329,7 @@ def __call__(
329329
Callable that Optuna will use to optimize the hyper-parameters
330330
331331
Args:
332-
obj_filename: the pickle-exported Algo object.
332+
obj_filename: the serialized Algo object.
333333
output_folder: the root path of the algorithms templates.
334334
template_path: the algorithm_template. It must contain algo.py in the follow path:
335335
``{algorithm_templates_dir}/{network}/scripts/algo.py``
@@ -364,7 +364,7 @@ def generate(self, output_folder: str = ".") -> None:
364364
task_id = self.get_task_id()
365365
task_prefix = os.path.basename(self.algo.get_output_path())
366366
write_path = os.path.join(output_folder, task_prefix + task_id)
367-
self.obj_filename = os.path.join(write_path, "algo_object.pkl")
367+
self.obj_filename = os.path.join(write_path, "algo_object.json")
368368

369369
if isinstance(self.algo, BundleAlgo):
370370
self.algo.export_to_disk(output_folder, task_prefix + task_id, fill_with_datastats=False)
@@ -377,15 +377,15 @@ def run_algo(self, obj_filename: str, output_folder: str = ".", template_path: P
377377
The python interface for NNI to run.
378378
379379
Args:
380-
obj_filename: the pickle-exported Algo object.
380+
obj_filename: the serialized Algo object.
381381
output_folder: the root path of the algorithms templates.
382382
template_path: the algorithm_template. It must contain algo.py in the follow path:
383383
``{algorithm_templates_dir}/{network}/scripts/algo.py``
384384
"""
385385
if not os.path.isfile(obj_filename):
386386
raise ValueError(f"{obj_filename} is not found")
387387

388-
self.algo, algo_meta_data = algo_from_pickle(obj_filename, template_path=template_path)
388+
self.algo, algo_meta_data = algo_from_json(obj_filename, template_path=template_path)
389389

390390
# step 1 sample hyperparams
391391
params = self.get_hyperparameters()
@@ -397,5 +397,5 @@ def run_algo(self, obj_filename: str, output_folder: str = ".", template_path: P
397397
# step 4 report validation acc to controller
398398
acc = self.algo.get_score()
399399
algo_meta_data = {str(AlgoKeys.SCORE): acc}
400-
algo_to_pickle(self.algo, template_path=self.algo.template_path, **algo_meta_data)
400+
algo_to_json(self.algo, template_path=self.algo.template_path, **algo_meta_data)
401401
self.set_score(acc)

monai/apps/auto3dseg/utils.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import os
1515

1616
from monai.apps.auto3dseg.bundle_gen import BundleAlgo
17-
from monai.auto3dseg import algo_from_pickle, algo_to_pickle
17+
from monai.auto3dseg import algo_from_json, algo_to_json
1818
from monai.utils.enums import AlgoKeys
1919

2020
__all__ = ["import_bundle_algo_history", "export_bundle_algo_history", "get_name_from_algo_id"]
@@ -42,11 +42,18 @@ def import_bundle_algo_history(
4242
if not os.path.isdir(write_path):
4343
continue
4444

45-
obj_filename = os.path.join(write_path, "algo_object.pkl")
46-
if not os.path.isfile(obj_filename): # saved mode pkl
45+
# Prefer JSON format, fall back to legacy pickle
46+
json_filename = os.path.join(write_path, "algo_object.json")
47+
pkl_filename = os.path.join(write_path, "algo_object.pkl")
48+
49+
if os.path.isfile(json_filename):
50+
obj_filename = json_filename
51+
elif os.path.isfile(pkl_filename):
52+
obj_filename = pkl_filename
53+
else:
4754
continue
4855

49-
algo, algo_meta_data = algo_from_pickle(obj_filename, template_path=template_path)
56+
algo, algo_meta_data = algo_from_json(obj_filename, template_path=template_path)
5057

5158
best_metric = algo_meta_data.get(AlgoKeys.SCORE, None)
5259
if best_metric is None:
@@ -57,7 +64,7 @@ def import_bundle_algo_history(
5764

5865
is_trained = best_metric is not None
5966

60-
if (only_trained and is_trained) or not only_trained:
67+
if is_trained or not only_trained:
6168
history.append(
6269
{AlgoKeys.ID: name, AlgoKeys.ALGO: algo, AlgoKeys.SCORE: best_metric, AlgoKeys.IS_TRAINED: is_trained}
6370
)
@@ -67,14 +74,14 @@ def import_bundle_algo_history(
6774

6875
def export_bundle_algo_history(history: list[dict[str, BundleAlgo]]) -> None:
6976
"""
70-
Save all the BundleAlgo in the history to algo_object.pkl in each individual folder
77+
Save all the BundleAlgo in the history to algo_object.json in each individual folder.
7178
7279
Args:
7380
history: a List of Bundle. Typically, the history can be obtained from BundleGen get_history method
7481
"""
7582
for algo_dict in history:
7683
algo = algo_dict[AlgoKeys.ALGO]
77-
algo_to_pickle(algo, template_path=algo.template_path)
84+
algo_to_json(algo, template_path=algo.template_path)
7885

7986

8087
def get_name_from_algo_id(id: str) -> str:

monai/auto3dseg/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
from .operations import Operations, SampleOperations, SummaryOperations
2626
from .seg_summarizer import SegSummarizer
2727
from .utils import (
28+
algo_from_json,
2829
algo_from_pickle,
30+
algo_to_json,
2931
algo_to_pickle,
3032
concat_multikeys_to_dict,
3133
concat_val_to_np,

monai/auto3dseg/algo_gen.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ class Algo:
1919
"""
2020
An algorithm in this context is loosely defined as a data processing pipeline consisting of multiple components
2121
such as image preprocessing, followed by deep learning model training and evaluation.
22+
23+
Note:
24+
When serialized via ``algo_to_json`` / ``algo_from_json``, subclasses are re-instantiated
25+
from their fully-qualified class name through ``monai.bundle.ConfigParser``. This requires
26+
the subclass to have a default constructor (no required positional arguments); state is
27+
restored afterwards via ``load_state_dict``.
2228
"""
2329

2430
template_path: PathLike | None = None
@@ -38,6 +44,30 @@ def get_score(self, *args, **kwargs):
3844
def get_output_path(self, *args, **kwargs):
3945
"""Returns the algo output paths for scripts location"""
4046

47+
def state_dict(self) -> dict:
48+
"""
49+
Return state for serialization.
50+
51+
Subclasses should override this method to return a dictionary of
52+
attributes that need to be serialized. This follows the PyTorch
53+
convention for state management.
54+
55+
Returns:
56+
A dictionary containing the state to serialize.
57+
"""
58+
return {}
59+
60+
def load_state_dict(self, state: dict) -> None:
61+
"""
62+
Restore state from a dictionary.
63+
64+
Subclasses should override this method to restore their state
65+
from the dictionary returned by state_dict().
66+
67+
Args:
68+
state: A dictionary containing the state to restore.
69+
"""
70+
4171

4272
class AlgoGen(Randomizable):
4373
"""

0 commit comments

Comments
 (0)