Repository: facebookresearch/nevergrad

Issue:
PicklingError: Can't pickle nevergrad.optimization.utils.Archive
In my case, I used `optimizer.ask()` and `optimizer.tell()` independently.
Optimizer can't be dumped into pickle when `num_tell` exceed 1000.

``` python
# sample
import nevergrad as ng

def test(a, b, c):
    return a + b + c

params = {
    "a": ng.p.Scalar(lower=0, upper=1, init=0).set_mutation(sigma=0.2),
    "b": ng.p.TransitionChoice([1, 2, 3]),
    "c": ng.p.TransitionChoice([x for x in range(200)])
}
optimizer = ng.optimizers.registry["TBPSA"](parametrization=ng.p.Instrumentation(**params))
args = [optimizer.ask() for x in range(1000)]
for arg in args:
    optimizer.tell(arg, test(**arg.kwargs))

optimizer.dump("./test_dump_optimizer.pkl")
```
I got the error below
``` python
PicklingError: Can't pickle nevergrad.optimization.utils.Archive[nevergrad.optimization.utils.MultiValue]: it's not the same object as nevergrad.optimization.utils.Archive
```
It seems that `pruning` will change `optimizer.archive` type and make object different.



Discussion:
Hi, thanks for pointing out.
I think I know exactly where this is coming from, but I can't reproduce, which python version are you using?

Resolving patch:
diff --git a/nevergrad/optimization/base.py b/nevergrad/optimization/base.py
--- a/nevergrad/optimization/base.py
+++ b/nevergrad/optimization/base.py
@@ -9,7 +9,7 @@
 from numbers import Real
 from collections import deque
 import typing as tp  # favor using tp.Dict instead of Dict etc
-from typing import Optional, Tuple, Callable, Any, Dict, List, Union, Deque, Type, Set
+from typing import Optional, Tuple, Any, Dict, List
 import numpy as np
 from nevergrad.parametrization import parameter as p
 from nevergrad.common import tools as ngtools
@@ -114,15 +114,15 @@ def __init__(self, parametrization: IntOrParameter, budget: Optional[int] = None
             num_workers=num_workers, dimension=self.parametrization.dimension
         )
         # instance state
-        self._asked: Set[str] = set()
-        self._suggestions: Deque[p.Parameter] = deque()
+        self._asked: tp.Set[str] = set()
+        self._suggestions: tp.Deque[p.Parameter] = deque()
         self._num_ask = 0
         self._num_tell = 0
         self._num_tell_not_asked = 0
         self._callbacks: Dict[str, List[Any]] = {}
         # to make optimize function stoppable halway through
         self._running_jobs: List[Tuple[p.Parameter, JobLike[float]]] = []
-        self._finished_jobs: Deque[Tuple[p.Parameter, JobLike[float]]] = deque()
+        self._finished_jobs: tp.Deque[Tuple[p.Parameter, JobLike[float]]] = deque()
 
     @property
     def _rng(self) -> np.random.RandomState:
@@ -156,7 +156,7 @@ def num_tell_not_asked(self) -> int:
         """
         return self._num_tell_not_asked
 
-    def dump(self, filepath: Union[str, Path]) -> None:
+    def dump(self, filepath: tp.Union[str, Path]) -> None:
         """Pickles the optimizer into a file.
         """
         filepath = Path(filepath)
@@ -164,7 +164,7 @@ def dump(self, filepath: Union[str, Path]) -> None:
             pickle.dump(self, f)
 
     @classmethod
-    def load(cls: Type[X], filepath: Union[str, Path]) -> X:
+    def load(cls: tp.Type[X], filepath: tp.Union[str, Path]) -> X:
         """Loads a pickle and checks that the class is correct.
         """
         return load(cls, filepath)
@@ -397,7 +397,7 @@ def _internal_provide_recommendation(self) -> tp.Optional[ArrayLike]:
 
     def minimize(
         self,
-        objective_function: Callable[..., float],
+        objective_function: tp.Callable[..., float],
         executor: Optional[ExecutorLike] = None,
         batch_mode: bool = False,
         verbosity: int = 0,
@@ -439,7 +439,7 @@ def minimize(
                 warnings.warn(f"num_workers = {self.num_workers} > 1 is suboptimal when run sequentially", InefficientSettingsWarning)
         assert executor is not None
         tmp_runnings: List[Tuple[p.Parameter, JobLike[float]]] = []
-        tmp_finished: Deque[Tuple[p.Parameter, JobLike[float]]] = deque()
+        tmp_finished: tp.Deque[Tuple[p.Parameter, JobLike[float]]] = deque()
         # go
         sleeper = ngtools.Sleeper()  # manages waiting time depending on execution time of the jobs
         remaining_budget = self.budget - self.num_ask
@@ -592,7 +592,7 @@ def set_name(self, name: str, register: bool = False) -> "ConfiguredOptimizer":
             registry.register_name(name, self)
         return self
 
-    def load(self, filepath: Union[str, Path]) -> "Optimizer":
+    def load(self, filepath: tp.Union[str, Path]) -> "Optimizer":
         """Loads a pickle and checks that it is an Optimizer.
         """
         return self._OptimizerClass.load(filepath)
diff --git a/nevergrad/optimization/utils.py b/nevergrad/optimization/utils.py
--- a/nevergrad/optimization/utils.py
+++ b/nevergrad/optimization/utils.py
@@ -256,7 +256,7 @@ def __call__(self, archive: Archive[MultiValue]) -> Archive[MultiValue]:
         names = ["optimistic", "pessimistic", "average"]
         for name in names:
             quantiles[name] = np.quantile([v.get_estimation(name) for v in archive.values()], threshold, interpolation="lower")
-        new_archive = Archive[MultiValue]()
+        new_archive: Archive[MultiValue] = Archive()
         new_archive.bytesdict = {b: v for b, v in archive.bytesdict.items() if any(v.get_estimation(n) <= quantiles[n] for n in names)}
         return new_archive
 


Test patch:
diff --git a/nevergrad/optimization/test_utils.py b/nevergrad/optimization/test_utils.py
--- a/nevergrad/optimization/test_utils.py
+++ b/nevergrad/optimization/test_utils.py
@@ -3,6 +3,7 @@
 # This source code is licensed under the MIT license found in the
 # LICENSE file in the root directory of this source tree.
 
+import pickle
 import pytest
 import numpy as np
 import nevergrad as ng
@@ -86,7 +87,7 @@ def test_archive_errors() -> None:
 
 def test_pruning() -> None:
     param = ng.p.Scalar(init=12.0)
-    archive = utils.Archive[utils.MultiValue]()
+    archive: utils.Archive[utils.MultiValue] = utils.Archive()
     for k in range(3):
         value = utils.MultiValue(param, float(k), reference=param)
         archive[(float(k),)] = value
@@ -98,9 +99,10 @@ def test_pruning() -> None:
     # 0 is best optimistic and average, and 3 is best pessimistic (variance=0)
     archive = pruning(archive)
     testing.assert_set_equal([x[0] for x in archive.keys_as_arrays()], [0, 3], err_msg=f"Repetition #{k+1}")
+    pickle.dumps(archive)  # should be picklable
     # should not change anything this time
-    archive = pruning(archive)
-    testing.assert_set_equal([x[0] for x in archive.keys_as_arrays()], [0, 3], err_msg=f"Repetition #{k+1}")
+    archive2 = pruning(archive)
+    testing.assert_set_equal([x[0] for x in archive2.keys_as_arrays()], [0, 3], err_msg=f"Repetition #{k+1}")
 
 
 @pytest.mark.parametrize("nw,dimension,expected_min,expected_max", [  # type: ignore