Repository: facebookresearch/nevergrad

Issue:
Fix DE/PSO interations in ParaPortfolio
Follow up of #1052 and its quickfix #1056 
Both PSO and DE update `heritage["lineage"]` on tell-not-asked, which creates weird updates.
I have currently no idea how to fix this.


Discussion:

Resolving patch:
diff --git a/nevergrad/optimization/differentialevolution.py b/nevergrad/optimization/differentialevolution.py
--- a/nevergrad/optimization/differentialevolution.py
+++ b/nevergrad/optimization/differentialevolution.py
@@ -134,8 +134,10 @@ def _internal_ask_candidate(self) -> p.Parameter:
             self._uid_queue.asked.add(candidate.uid)
             return candidate
         # init is done
-        parent = self.population[self._uid_queue.ask()]
+        lineage = self._uid_queue.ask()
+        parent = self.population[lineage]
         candidate = parent.spawn_child()
+        candidate.heritage["lineage"] = lineage  # tell-not-asked may have provided a different lineage
         data = candidate.get_standardized_data(reference=self.parametrization)
         # define all the different parents
         uids = list(self.population)
@@ -188,9 +190,9 @@ def _internal_tell_not_asked(self, candidate: p.Parameter, loss: tp.FloatLoss) -
         discardable: tp.Optional[str] = None
         if len(self.population) >= self.llambda:
             if self.num_objectives == 1:  # monoobjective: replace if better
-                worst = max(self.population.values(), key=base._loss)
+                uid, worst = max(self.population.items(), key=lambda p: base._loss(p[1]))
                 if loss < base._loss(worst):
-                    discardable = worst.heritage["lineage"]
+                    discardable = uid
             else:  # multiobjective: replace if in pareto and some parents are not
                 pareto_uids = {c.uid for c in self.pareto_front()}
                 if candidate.uid in pareto_uids:
@@ -202,8 +204,9 @@ def _internal_tell_not_asked(self, candidate: p.Parameter, loss: tp.FloatLoss) -
             del self.population[discardable]
             self._uid_queue.discard(discardable)
         if len(self.population) < self.llambda:  # if there is space, add the new point
-            candidate.heritage["lineage"] = candidate.uid  # new lineage
             self.population[candidate.uid] = candidate
+            # this candidate lineage is not candidate.uid, but to avoid interfering with other optimizers (eg: PSO)
+            # we should not update the lineage (and lineage of children must therefore be enforced manually)
             self._uid_queue.tell(candidate.uid)
 
 
diff --git a/nevergrad/optimization/optimizerlib.py b/nevergrad/optimization/optimizerlib.py
--- a/nevergrad/optimization/optimizerlib.py
+++ b/nevergrad/optimization/optimizerlib.py
@@ -809,6 +809,7 @@ def _internal_ask_candidate(self) -> p.Parameter:
             return candidate
         uid = self._uid_queue.ask()
         candidate = self._spawn_mutated_particle(self.population[uid])
+        candidate.heritage["lineage"] = uid  # override in case it was a tell-not-asked
         return candidate
 
     def _get_boxed_data(self, particle: p.Parameter) -> np.ndarray:
@@ -853,22 +854,19 @@ def _internal_tell_candidate(self, candidate: p.Parameter, loss: tp.FloatLoss) -
 
     def _internal_tell_not_asked(self, candidate: p.Parameter, loss: tp.FloatLoss) -> None:
         # nearly same as DE
-        candidate._meta["value"] = loss
         worst: tp.Optional[p.Parameter] = None
         if not len(self.population) < self.llambda:
-            worst = max(self.population.values(), key=lambda p: p._meta.get("value", float("inf")))
-            if worst._meta.get("value", float("inf")) < loss:
+            uid, worst = max(self.population.items(), key=lambda p: base._loss(p[1]))
+            if base._loss(worst) < loss:
                 return  # no need to update
             else:
-                uid = worst.heritage["lineage"]
                 del self.population[uid]
                 self._uid_queue.discard(uid)
-        candidate.heritage["lineage"] = candidate.uid  # new lineage
         if "speed" not in candidate.heritage:
             candidate.heritage["speed"] = self._rng.uniform(-1.0, 1.0, self.parametrization.dimension)
         self.population[candidate.uid] = candidate
         self._uid_queue.tell(candidate.uid)
-        if loss < self._best._meta.get("loss", float("inf")):
+        if loss < base._loss(self._best):
             self._best = candidate
 
 
@@ -2049,13 +2047,12 @@ def _internal_ask_candidate(self) -> p.Parameter:
         return candidate
 
     def _internal_tell_candidate(self, candidate: p.Parameter, loss: tp.FloatLoss) -> None:
-        candidate._meta["loss"] = loss
         if self.population_size_adaptation:
             self.popsize.add_value(loss)
         self.children.append(candidate)
         if len(self.children) >= self.popsize.llambda:
             # Sorting the population.
-            self.children.sort(key=lambda c: c._meta["loss"])
+            self.children.sort(key=base._loss)
             # Computing the new parent.
             self.parents = self.children[: self.popsize.mu]
             self.children = []


Test patch:
diff --git a/nevergrad/optimization/test_optimizerlib.py b/nevergrad/optimization/test_optimizerlib.py
--- a/nevergrad/optimization/test_optimizerlib.py
+++ b/nevergrad/optimization/test_optimizerlib.py
@@ -731,3 +731,12 @@ def constraint(arg: tp.Any) -> bool:  # pylint: disable=unused-argument
     optimizer.tell(point, _multiobjective(point.value))
     if isinstance(optimizer, es._EvolutionStrategy):
         assert optimizer._rank_method is not None  # make sure the nsga2 ranker is used
+
+
+def test_paraportfolio_de() -> None:
+    workers = 40
+    opt = optlib.ParaPortfolio(12, budget=100 * workers, num_workers=workers)
+    for _ in range(3):
+        cands = [opt.ask() for _ in range(workers)]
+        for cand in cands:
+            opt.tell(cand, np.random.rand())