Repository: facebookresearch/nevergrad

Issue:
TypeError: cannot pickle '_io.TextIOWrapper' object when using ProgressBar
## Steps to reproduce

  1. Make a script with multithreading optimization
  2. Add ProgressBar callback
  3. Try to pickle.dump the optimizer

## Observed Results

  * What happened?  This could be a description, log output, etc.
```
0it [00:00, ?it/s]everything is awesome, starting optimization...
 97%|██████████████████████▏| 29/30 [00:09<00:00,  1.51it/s]----------------------
----------------------
Reporting issue
Instance of NGO(parametrization=Instrumentation(Tuple(Dict(kernel=Log{int,exp=3.5495366597555704,Cl(1,2000)}),clm),Dict(X=[['ART_2018244_143036.FR', 'ART_2018244_144536.FR']],Y=[['ART_2018244_143036.FR', 'ART_2018244_144536.FR']],dcfg={'paths': {'art_dir': '/home/franchesoni/ssat01/eval/art_010x010', 'year': 2018, 'some_FR_img': '/home/franchesoni/ssat01/eval/art_010x010/C02-FR/2018/ART_2018244_160036.FR'}, 'dataset': {'vector_lenght': 2, 'max_period': 41, 'limit': 4, 'n_points': 'all', 'y_timesteps': [8], 'shuffle': True}, 'optimization': {'budget': 30, 'num_workers': 24, 'resume': False}, 'cmv': {'timestep': 10, 'value_winsize': 5, 'pyr_scale': 0.5, 'levels': 3, 'winsize': 75, 'iterations': 3, 'poly_n': 5, 'poly_sigma': 1.1, 'enabled': True}, 'per': {'kernel': 5}, 'clm': {'kernel': 45}},now_folder=2020-06-01_18:32:08.071075)), budget=30, num_workers=24)
----------------------
----------------------
Traceback (most recent call last):
  File "/home/franchesoni/ssat01/ssat02/optim/aeval/pipeline.py", line 75, in <module>
    main()
  File "/home/franchesoni/ssat01/ssat02/optim/aeval/pipeline.py", line 34, in main
    step_02(dcfg, now_folder)
  File "/home/franchesoni/ssat01/ssat02/optim/aeval/pipeline.py", line 47, in step_02
    step_0202(dcfg, now_folder)
  File "/home/franchesoni/ssat01/ssat02/optim/aeval/pipeline.py", line 67, in step_0202
    du.eout_curve_loop('clm', dcfg, now_folder, 5, 3, 3, main_process=ef.main_process,)
  File "/home/franchesoni/ssat01/ssat02/optim/aeval/utils/data_utils.py", line 374, in eout_curve_loop
    loss, val_loss = main_process(name, dcfg, now_folder, X_train, Y_train, X_val, Y_val)
  File "/home/franchesoni/ssat01/ssat02/optim/aeval/utils/evaluation_functions.py", line 206, in main_process
    r = optimize_clm_params(dcfg, X_train, Y_train, now_folder)
  File "/home/franchesoni/ssat01/ssat02/optim/aeval/utils/evaluation_functions.py", line 218, in optimize_clm_params
    return optimize_params(clm_params, 'clm', dcfg, X, Y, now_folder)
  File "/home/franchesoni/ssat01/ssat02/optim/aeval/utils/evaluation_functions.py", line 268, in optimize_params
    pickle.dump(optimizer, f)
TypeError: cannot pickle '_io.TextIOWrapper' object
100%|███████████████████████| 30/30 [00:09<00:00,  3.08it/s]
```

## Expected Results

  * What did you expect to happen?
The same that happens without the callback. Everything works fine and `pickle.dump` does not fail.

## Relevant Code

  ```
  with futures.ThreadPoolExecutor(max_workers=optimizer.num_workers) as executor:
      r = optimizer.minimize(fn, executor=executor, batch_mode=False)

  print('----------------------')
  print('----------------------')
  print('Reporting issue')
  pprint(optimizer)
  print('----------------------')
  print('----------------------')

  with open(os.path.join(
    'results', now_folder,
    f"{name}_optimizer_{str(dt.datetime.now()).replace(' ', '_')}.pkl"),
             'wb') as f:
    pickle.dump(optimizer, f)
  ```

### Optimizer definition:
```
def get_optimizer(parametrization, name, dcfg, progress):
  optimizer = ng.optimizers.NGO(
      parametrization=parametrization, budget=dcfg['optimization']['budget'],
      num_workers=dcfg['optimization']['num_workers'],
  )

  if progress:
    optimizer.register_callback("tell", ng.callbacks.ProgressBar())

  return optimizer
```
The error happens if and only if `progress = True`. The rest of the code is too long to be useful. Any ideas?
A quick googling pointed to a multiprocessing issue, but the `with` should be supposed to handle it, shouldn't it?


<!--- In any case, don't hesitate to join and ask questions if you need on Nevergrad users Facebook group https://www.facebook.com/groups/nevergradusers/ -->



Discussion:

Resolving patch:
diff --git a/nevergrad/optimization/callbacks.py b/nevergrad/optimization/callbacks.py
--- a/nevergrad/optimization/callbacks.py
+++ b/nevergrad/optimization/callbacks.py
@@ -170,7 +170,12 @@ def to_hiplot_experiment(self, max_list_elements: int = 24) -> tp.Any:  # no typ
           - github repo: https://github.com/facebookresearch/hiplot
           - documentation: https://facebookresearch.github.io/hiplot/
         """
-        import hiplot as hip
+        # pylint: disable=import-outside-toplevel
+        try:
+            import hiplot as hip
+        except ImportError as e:
+            raise ImportError(f"{self.__class__.__name__} requires hiplot which is not installed by default "
+                              "(pip install hiplot)") from e
         exp = hip.Experiment()
         for xp in self.load_flattened(max_list_elements=max_list_elements):
             dp = hip.Datapoint(
@@ -206,9 +211,26 @@ class ProgressBar:
     """
 
     def __init__(self) -> None:
-        from tqdm import tqdm  # Inline import to avoid additional dependency
-        self.progress_bar = tqdm()
+        self._progress_bar: tp.Any = None
+        self._current = 0
 
     def __call__(self, optimizer: base.Optimizer, *args: tp.Any, **kwargs: tp.Any) -> None:
-        self.progress_bar.total = optimizer.budget
-        self.progress_bar.update(1)
+        if self._progress_bar is None:
+            # pylint: disable=import-outside-toplevel
+            try:
+                from tqdm import tqdm  # Inline import to avoid additional dependency
+            except ImportError as e:
+                raise ImportError(f"{self.__class__.__name__} requires tqdm which is not installed by default "
+                                  "(pip install tqdm)") from e
+            self._progress_bar = tqdm()
+            self._progress_bar.total = optimizer.budget
+            self._progress_bar.update(self._current)
+        self._progress_bar.update(1)
+        self._current += 1
+
+    def __getstate__(self) -> tp.Dict[str, tp.Any]:
+        """Used for pickling (tqdm is not picklable)
+        """
+        state = dict(self.__dict__)
+        state["_progress_bar"] = None
+        return state


Test patch:
diff --git a/nevergrad/optimization/test_callbacks.py b/nevergrad/optimization/test_callbacks.py
--- a/nevergrad/optimization/test_callbacks.py
+++ b/nevergrad/optimization/test_callbacks.py
@@ -47,3 +47,21 @@ def test_dump_callback(tmp_path: Path) -> None:
     assert not filepath.exists()
     optimizer.tell(cand, 0)
     assert filepath.exists()
+
+
+def test_progressbar_dump(tmp_path: Path) -> None:
+    filepath = tmp_path / "pickle.pkl"
+    optimizer = optimizerlib.OnePlusOne(parametrization=2, budget=32)
+    optimizer.register_callback("tell", ng.callbacks.ProgressBar())
+    for _ in range(8):
+        cand = optimizer.ask()
+        optimizer.tell(cand, 0)
+    optimizer.dump(filepath)
+    # should keep working after dump
+    cand = optimizer.ask()
+    optimizer.tell(cand, 0)
+    # and be reloadable
+    optimizer = optimizerlib.OnePlusOne.load(filepath)
+    for _ in range(12):
+        cand = optimizer.ask()
+        optimizer.tell(cand, 0)