From 1c3604a8cf48e1fc8acffe812825c11afe46ad24 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Sat, 12 Oct 2024 06:32:19 -0700 Subject: [PATCH 1/7] [WIP] working on dynamic sep rho; run generic_bash to see error; will have merge conflicts with other PRs --- examples/generic_cylinders.bash | 3 +++ mpisppy/extensions/sep_rho.py | 38 +++++++++++++++++++++++++-------- mpisppy/utils/cfg_vanilla.py | 2 +- mpisppy/utils/config.py | 8 +++---- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/examples/generic_cylinders.bash b/examples/generic_cylinders.bash index 61492b8d..4d99ec52 100644 --- a/examples/generic_cylinders.bash +++ b/examples/generic_cylinders.bash @@ -15,6 +15,9 @@ mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name echo "^^^ sensi rho dynamic ^^^" mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name farmer --num-scens 3 --bundles-per-rank=0 --max-iterations=100 --default-rho=1 --solver-name=${SOLVER} --xhatpath=./farmer_nonants.npy --grad-order-stat 0.0 --xhatxbar --ph-ob --max-stalled-iters 5000 --sensi-rho --rel-gap 0.001 --dynamic-rho-dual-crit --dynamic-rho-dual-thresh 0.1 +echo "^^^ sep rho dynamic ^^^" +mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name farmer --num-scens 3 --bundles-per-rank=0 --max-iterations=100 --default-rho=1 --solver-name=${SOLVER} --xhatpath=./farmer_nonants.npy --grad-order-stat 0.0 --xhatxbar --ph-ob --max-stalled-iters 5000 --sep-rho --rel-gap 0.001 --dynamic-rho-dual-crit --dynamic-rho-dual-thresh 0.1 + cd .. exit diff --git a/mpisppy/extensions/sep_rho.py b/mpisppy/extensions/sep_rho.py index 2fea016f..121728a4 100644 --- a/mpisppy/extensions/sep_rho.py +++ b/mpisppy/extensions/sep_rho.py @@ -7,14 +7,14 @@ # full copyright and license information. ############################################################################### -import mpisppy.extensions.extension +import mpisppy.extensions.dyn_rho_base import numpy as np import mpisppy.MPI as MPI - +from mpisppy import global_toc from mpisppy.utils.sputils import nonant_cost_coeffs -class SepRho(mpisppy.extensions.extension.Extension): +class SepRho(mpisppy.extensions.dyn_rho_base.Dyn_Rho_extension_base): """ Rho determination algorithm "SEP" from Progressive hedging innovations for a class of stochastic mixed-integer @@ -23,7 +23,8 @@ class SepRho(mpisppy.extensions.extension.Extension): DOI 10.1007/s10287-010-0125-4 """ - def __init__(self, ph): + def __init__(self, ph, comm=None): + super().__init__(ph, comm=comm) self.ph = ph self.multiplier = 1.0 @@ -33,6 +34,7 @@ def __init__(self, ph): and "multiplier" in ph.options["sep_rho_options"] ): self.multiplier = ph.options["sep_rho_options"]["multiplier"] + self.cfg = ph.options["sep_rho_options"]["cfg"] def _compute_primal_residual_norm(self, ph): local_nodenames = [] @@ -134,10 +136,7 @@ def _compute_xmax(ph): def _compute_xmin(ph): return SepRho._compute_min_max(ph, np.minimum, MPI.MIN, np.inf) - def pre_iter0(self): - pass - - def post_iter0(self): + def compute_and_update_rho(self): ph = self.ph primal_resid = self._compute_primal_residual_norm(ph) xmax = self._compute_xmax(ph) @@ -159,9 +158,30 @@ def post_iter0(self): # print(ndn_i,nv.getname(),xmax[ndn_i],xmin[ndn_i],primal_resid[ndn_i],cc[ndn_i],rho._value) if ph.cylinder_rank == 0: print("Rho values updated by SepRho Extension") + + def pre_iter0(self): + pass + + def post_iter0(self): + global_toc("Using sep-rho rho setter") + super().post_iter0() + self.compute_and_update_rho() def miditer(self): - pass + # this code could be factored; see sens_rho.py + self.primal_conv_cache.append(self.opt.convergence_diff()) + self.dual_conv_cache.append(self.wt.W_diff()) + + if self._update_recommended(): + self.compute_and_update_rho() + sum_rho = 0.0 + num_rhos = 0 # could be computed... + for sname, s in self.opt.local_scenarios.items(): + for ndn_i, nonant in s._mpisppy_data.nonant_indices.items(): + sum_rho += s._mpisppy_model.rho[ndn_i]._value + num_rhos += 1 + rho_avg = sum_rho / num_rhos + global_toc(f"Rho values recomputed - average rank 0 rho={rho_avg}") def enditer(self): pass diff --git a/mpisppy/utils/cfg_vanilla.py b/mpisppy/utils/cfg_vanilla.py index ffdd9d1f..a69ed2df 100644 --- a/mpisppy/utils/cfg_vanilla.py +++ b/mpisppy/utils/cfg_vanilla.py @@ -204,7 +204,7 @@ def add_fixer(hub_dict, def add_sep_rho(hub_dict, cfg): hub_dict = extension_adder(hub_dict,SepRho) - hub_dict["opt_kwargs"]["options"]["sep_rho_options"] = {"multiplier" : cfg.sep_rho_multiplier} + hub_dict["opt_kwargs"]["options"]["sep_rho_options"] = {"multiplier" : cfg.sep_rho_multiplier, "cfg": cfg} def add_coeff_rho(hub_dict, cfg): hub_dict = extension_adder(hub_dict,CoeffRho) diff --git a/mpisppy/utils/config.py b/mpisppy/utils/config.py index ac31d36e..e6003424 100644 --- a/mpisppy/utils/config.py +++ b/mpisppy/utils/config.py @@ -150,9 +150,9 @@ def _bad_rho_setters(msg): if self.grad_rho_setter and self.sensi_rho: _bad_rho_setters("Only one rho setter can be active.") - if not self.grad_rho_setter and not self.sensi_rho: + if not self.grad_rho_setter and not self.sensi_rho and not self.sep_rho: if self.dynamic_rho_primal_crit or self.dynamic_rho_dual_crit: - _bad_rho_setters("dynamic rho only works with grad- and sensi-") + _bad_rho_setters("dynamic rho only works with grad-, sensi-, and sep-rho") def add_solver_specs(self, prefix=""): sstr = f"{prefix}_solver" if prefix != "" else "solver" @@ -845,12 +845,12 @@ def dynamic_rho_args(self): # AKA adaptive self.gradient_args() self.add_to_config('dynamic_rho_primal_crit', - description="Use dynamic primal criterion for some rho updates", + description="Use dynamic primal criterion for some types of rho updates", domain=bool, default=False) self.add_to_config('dynamic_rho_dual_crit', - description="Use dynamic dual criterion for some rho updates", + description="Use dynamic dual criterion for some stypes of rho updates", domain=bool, default=False) From 27d19728d15a9a4d9591542a5231433cc53e656f Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Sat, 12 Oct 2024 18:39:24 -0700 Subject: [PATCH 2/7] generic_cylinders.bash gets to the bug quickly --- examples/generic_cylinders.bash | 14 ++++++++++---- mpisppy/extensions/sep_rho.py | 4 ---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/generic_cylinders.bash b/examples/generic_cylinders.bash index 4d99ec52..172bfc91 100644 --- a/examples/generic_cylinders.bash +++ b/examples/generic_cylinders.bash @@ -4,8 +4,18 @@ SOLVER="cplex" cd farmer + mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name farmer --num-scens 3 --solver-name ${SOLVER} --max-iterations 10 --max-solver-threads 4 --default-rho 1 --lagrangian --xhatshuffle --rel-gap 0.01 --solution-base-name farmer_nonants +echo "^^^ sep rho not dynamic ^^^" +mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name farmer --num-scens 3 --bundles-per-rank=0 --max-iterations=100 --default-rho=1 --solver-name=${SOLVER} --xhatpath=./farmer_nonants.npy --grad-order-stat 0.0 --xhatxbar --ph-ob --max-stalled-iters 5000 --sep-rho --rel-gap 0.001 + + +echo "^^^ sep rho dynamic ^^^" +mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name farmer --num-scens 3 --bundles-per-rank=0 --max-iterations=100 --default-rho=1 --solver-name=${SOLVER} --xhatpath=./farmer_nonants.npy --grad-order-stat 0.0 --xhatxbar --ph-ob --max-stalled-iters 5000 --sep-rho --rel-gap 0.001 --dynamic-rho-dual-crit --dynamic-rho-dual-thresh 0.1 + +exit + mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name farmer --num-scens 3 --bundles-per-rank=0 --max-iterations=100 --default-rho=1 --solver-name=${SOLVER} --xhatpath=./farmer_nonants.npy --grad-order-stat 0.0 --xhatxbar --ph-ob --max-stalled-iters 5000 --grad-rho-setter --rel-gap 0.001 # now do it again, but this time using dynamic rho @@ -15,11 +25,7 @@ mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name echo "^^^ sensi rho dynamic ^^^" mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name farmer --num-scens 3 --bundles-per-rank=0 --max-iterations=100 --default-rho=1 --solver-name=${SOLVER} --xhatpath=./farmer_nonants.npy --grad-order-stat 0.0 --xhatxbar --ph-ob --max-stalled-iters 5000 --sensi-rho --rel-gap 0.001 --dynamic-rho-dual-crit --dynamic-rho-dual-thresh 0.1 -echo "^^^ sep rho dynamic ^^^" -mpiexec -np 3 python -m mpi4py ../../mpisppy/generic_cylinders.py --module-name farmer --num-scens 3 --bundles-per-rank=0 --max-iterations=100 --default-rho=1 --solver-name=${SOLVER} --xhatpath=./farmer_nonants.npy --grad-order-stat 0.0 --xhatxbar --ph-ob --max-stalled-iters 5000 --sep-rho --rel-gap 0.001 --dynamic-rho-dual-crit --dynamic-rho-dual-thresh 0.1 - cd .. -exit echo "^^^ netdes sensi-rho ^^^" cd netdes diff --git a/mpisppy/extensions/sep_rho.py b/mpisppy/extensions/sep_rho.py index 121728a4..02cf771b 100644 --- a/mpisppy/extensions/sep_rho.py +++ b/mpisppy/extensions/sep_rho.py @@ -151,11 +151,7 @@ def compute_and_update_rho(self): rho._value = abs(cc[ndn_i]) / (xmax[ndn_i] - xmin[ndn_i] + 1) else: rho._value = abs(cc[ndn_i]) / max(1, primal_resid[ndn_i]) - rho._value *= self.multiplier - - # if ph.cylinder_rank==0: - # print(ndn_i,nv.getname(),xmax[ndn_i],xmin[ndn_i],primal_resid[ndn_i],cc[ndn_i],rho._value) if ph.cylinder_rank == 0: print("Rho values updated by SepRho Extension") From 7af3574ce178fe5df49c0d527ae540860e476efe Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Mon, 14 Oct 2024 12:14:03 -0600 Subject: [PATCH 3/7] turn off prox when checking the objective function --- mpisppy/extensions/sep_rho.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mpisppy/extensions/sep_rho.py b/mpisppy/extensions/sep_rho.py index 02cf771b..c8c59b10 100644 --- a/mpisppy/extensions/sep_rho.py +++ b/mpisppy/extensions/sep_rho.py @@ -169,7 +169,15 @@ def miditer(self): self.dual_conv_cache.append(self.wt.W_diff()) if self._update_recommended(): + reenable_prox = False + if not self.ph.prox_disabled: + self.ph._disable_prox() + reenable_prox = True + # this will ask for objective function coefficients, + # so we don't want the prox term (TODO: and maybe W?) self.compute_and_update_rho() + if reenable_prox: + self.ph._reenable_prox() sum_rho = 0.0 num_rhos = 0 # could be computed... for sname, s in self.opt.local_scenarios.items(): From ef6654e5280e2f2153ca31e8d72a910112fe8ef7 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Mon, 14 Oct 2024 12:26:43 -0600 Subject: [PATCH 4/7] add dynamic rho args to farmer_cylinders --- examples/farmer/farmer_cylinders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/farmer/farmer_cylinders.py b/examples/farmer/farmer_cylinders.py index acec3fcb..c35a07ec 100644 --- a/examples/farmer/farmer_cylinders.py +++ b/examples/farmer/farmer_cylinders.py @@ -47,6 +47,7 @@ def _parse_args(): cfg.reduced_costs_args() cfg.sep_rho_args() cfg.coeff_rho_args() + cfg.dynamic_rho_args() cfg.add_to_config("crops_mult", description="There will be 3x this many crops (default 1)", domain=int, From 29944ee1ded5932954b17ae3788963a944e03119 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Mon, 14 Oct 2024 13:50:02 -0600 Subject: [PATCH 5/7] debugging dynamic rho --- mpisppy/extensions/dyn_rho_base.py | 8 +++--- mpisppy/extensions/sep_rho.py | 39 +++++++++++++++--------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/mpisppy/extensions/dyn_rho_base.py b/mpisppy/extensions/dyn_rho_base.py index 8a0ab842..760fd80f 100644 --- a/mpisppy/extensions/dyn_rho_base.py +++ b/mpisppy/extensions/dyn_rho_base.py @@ -55,7 +55,7 @@ def _display_W_values(self): def _update_rho_primal_based(self): with warnings.catch_warnings(): warnings.filterwarnings('error') - curr_conv, last_conv = self.primal_conv_cache[-1], self.primal_conv_cache[-2] + curr_conv, last_conv = self.primal_conv_cache[0], self.primal_conv_cache[-1] try: primal_diff = np.abs((last_conv - curr_conv) / last_conv) except Warning: @@ -65,9 +65,11 @@ def _update_rho_primal_based(self): return (primal_diff <= self.cfg.dynamic_rho_primal_thresh) def _update_rho_dual_based(self): - curr_conv, last_conv = self.dual_conv_cache[-1], self.dual_conv_cache[-2] + if len(self.dual_conv_cache) < 4: + # first two entries will be 0 by construction, so ignore + return False + curr_conv, last_conv = self.dual_conv_cache[0], self.dual_conv_cache[-1] dual_diff = np.abs((last_conv - curr_conv) / last_conv) if last_conv != 0 else 0 - #print(f'{dual_diff =}') return (dual_diff <= self.cfg.dynamic_rho_dual_thresh) def _update_recommended(self): diff --git a/mpisppy/extensions/sep_rho.py b/mpisppy/extensions/sep_rho.py index c8c59b10..f1302bf3 100644 --- a/mpisppy/extensions/sep_rho.py +++ b/mpisppy/extensions/sep_rho.py @@ -136,7 +136,7 @@ def _compute_xmax(ph): def _compute_xmin(ph): return SepRho._compute_min_max(ph, np.minimum, MPI.MIN, np.inf) - def compute_and_update_rho(self): + def _compute_and_update_rho(self): ph = self.ph primal_resid = self._compute_primal_residual_norm(ph) xmax = self._compute_xmax(ph) @@ -152,8 +152,25 @@ def compute_and_update_rho(self): else: rho._value = abs(cc[ndn_i]) / max(1, primal_resid[ndn_i]) rho._value *= self.multiplier - if ph.cylinder_rank == 0: - print("Rho values updated by SepRho Extension") + + def compute_and_update_rho(self): + reenable_prox = False + if not self.ph.prox_disabled: + self.ph._disable_prox() + reenable_prox = True + # this will ask for objective function coefficients, + # so we don't want the prox term (TODO: and maybe W?) + self._compute_and_update_rho() + if reenable_prox: + self.ph._reenable_prox() + sum_rho = 0.0 + num_rhos = 0 # could be computed... + for sname, s in self.opt.local_scenarios.items(): + for ndn_i, nonant in s._mpisppy_data.nonant_indices.items(): + sum_rho += s._mpisppy_model.rho[ndn_i]._value + num_rhos += 1 + rho_avg = sum_rho / num_rhos + global_toc(f"Rho values recomputed - average rank 0 rho={rho_avg}") def pre_iter0(self): pass @@ -169,23 +186,7 @@ def miditer(self): self.dual_conv_cache.append(self.wt.W_diff()) if self._update_recommended(): - reenable_prox = False - if not self.ph.prox_disabled: - self.ph._disable_prox() - reenable_prox = True - # this will ask for objective function coefficients, - # so we don't want the prox term (TODO: and maybe W?) self.compute_and_update_rho() - if reenable_prox: - self.ph._reenable_prox() - sum_rho = 0.0 - num_rhos = 0 # could be computed... - for sname, s in self.opt.local_scenarios.items(): - for ndn_i, nonant in s._mpisppy_data.nonant_indices.items(): - sum_rho += s._mpisppy_model.rho[ndn_i]._value - num_rhos += 1 - rho_avg = sum_rho / num_rhos - global_toc(f"Rho values recomputed - average rank 0 rho={rho_avg}") def enditer(self): pass From 14f92faf54477f6c14bcc5a4b06b7b4550d4e85b Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Mon, 14 Oct 2024 14:44:34 -0600 Subject: [PATCH 6/7] only compute cost coeffs once --- mpisppy/extensions/sep_rho.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mpisppy/extensions/sep_rho.py b/mpisppy/extensions/sep_rho.py index f1302bf3..a5d83c4e 100644 --- a/mpisppy/extensions/sep_rho.py +++ b/mpisppy/extensions/sep_rho.py @@ -36,6 +36,8 @@ def __init__(self, ph, comm=None): self.multiplier = ph.options["sep_rho_options"]["multiplier"] self.cfg = ph.options["sep_rho_options"]["cfg"] + self._nonant_cost_coeffs = None + def _compute_primal_residual_norm(self, ph): local_nodenames = [] local_primal_residuals = {} @@ -136,6 +138,12 @@ def _compute_xmax(ph): def _compute_xmin(ph): return SepRho._compute_min_max(ph, np.minimum, MPI.MIN, np.inf) + def nonant_cost_coeffs(self, s): + if self._nonant_cost_coeffs is None: + # Should be the same in every scenario... + self._nonant_cost_coeffs = nonant_cost_coeffs(s) + return self._nonant_cost_coeffs + def _compute_and_update_rho(self): ph = self.ph primal_resid = self._compute_primal_residual_norm(ph) @@ -143,7 +151,7 @@ def _compute_and_update_rho(self): xmin = self._compute_xmin(ph) for s in ph.local_scenarios.values(): - cc = nonant_cost_coeffs(s) + cc = self.nonant_cost_coeffs(s) for ndn_i, rho in s._mpisppy_model.rho.items(): if cc[ndn_i] != 0: nv = s._mpisppy_data.nonant_indices[ndn_i] # var_data object @@ -154,15 +162,7 @@ def _compute_and_update_rho(self): rho._value *= self.multiplier def compute_and_update_rho(self): - reenable_prox = False - if not self.ph.prox_disabled: - self.ph._disable_prox() - reenable_prox = True - # this will ask for objective function coefficients, - # so we don't want the prox term (TODO: and maybe W?) self._compute_and_update_rho() - if reenable_prox: - self.ph._reenable_prox() sum_rho = 0.0 num_rhos = 0 # could be computed... for sname, s in self.opt.local_scenarios.items(): From 6cdc22e0c7db7b020783ceaa3533a89962c277c8 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Tue, 15 Oct 2024 08:58:55 -0600 Subject: [PATCH 7/7] check for crit cfg attributes --- mpisppy/extensions/dyn_rho_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mpisppy/extensions/dyn_rho_base.py b/mpisppy/extensions/dyn_rho_base.py index 760fd80f..fea2a838 100644 --- a/mpisppy/extensions/dyn_rho_base.py +++ b/mpisppy/extensions/dyn_rho_base.py @@ -73,8 +73,8 @@ def _update_rho_dual_based(self): return (dual_diff <= self.cfg.dynamic_rho_dual_thresh) def _update_recommended(self): - return (self.cfg.dynamic_rho_primal_crit and self._update_rho_primal_based()) or \ - (self.cfg.dynamic_rho_dual_crit and self._update_rho_dual_based()) + return (hasattr(self.cfg, "dynamic_rho_primal_crit") and self.cfg.dynamic_rho_primal_crit and self._update_rho_primal_based()) or \ + (hasattr(self.cfg, "dynamic_rho_dual_crit") and self.cfg.dynamic_rho_dual_crit and self._update_rho_dual_based()) def pre_iter0(self): pass