Skip to content

Commit 1cc109e

Browse files
DLWoodruffclaude
andauthored
display_timing: expose via Config and CLI (#290) (#708)
* display_timing: expose via Config and CLI (#290) display_timing was previously a "secret menu" option that had to be set by hand on the options dict. Bring it into mpisppy.utils.config.Config alongside display_progress and display_convergence_detail so it is documented via --help and propagates through cfg_vanilla like its siblings. - mpisppy/utils/config.py: register display_timing (bool, default False). - mpisppy/utils/cfg_vanilla.py: * shared_options(): forward cfg.display_timing into opt_kwargs.options. * subgradient_spoke, ph_dual_spoke, relaxed_ph_spoke: hardcode options["display_timing"] = False, matching the existing pattern for display_progress / display_convergence_detail (these spokes should not print per-iteration diagnostics). - doc/src/secretmenu.rst: drop the display_timing section now that it's a first-class CLI option. The defensive default in phbase.py:887-888 is left in place so PHBase can still be instantiated outside the cfg flow with a manual options dict. Fixes #290 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * doc + smoke test for --display-timing - doc/src/hubs.rst: mention the new --display-timing flag next to the existing --with-display-convergence-detail note so the option is discoverable from the docs now that the secret-menu section was removed. - examples/run_all.py: add --display-timing to the existing sizes sizes_cylinders.py command so the run_all CI jobs exercise the new CLI plumbing end-to-end as a smoke test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: end-to-end verify --display-timing emits non-zero solve times Adds two tests to Test_sizes in mpisppy/tests/test_ef_ph.py: - test_display_timing_emits_nonzero_solve_times: runs PH iter0 with display_timing=True, captures stdout, asserts the "Pyomo solve times (seconds):" header appears, parses the min/mean/max stats line, and requires max > 0. Guards against a future refactor quietly dropping the option from self.options or the print path (issue #290). - test_display_timing_off_suppresses_solve_times: companion that confirms the timing report is NOT printed when the flag is False, so we know the assertion above isn't picking up output emitted unconditionally somewhere. Both tests skip when no solver is available. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b5edc38 commit 1cc109e

6 files changed

Lines changed: 84 additions & 30 deletions

File tree

doc/src/hubs.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ in the farmer example. The ``farmer_cylinders.py`` file has::
109109
from mpisppy.convergers.norm_rho_converger import NormRhoConverger
110110

111111
and optionally passes ``NormRhoConverger`` to the hub constructor. Note that you can observe
112-
the behavior of the hub converger using the option ``--with-display-convergence-detail``.
112+
the behavior of the hub converger using the option ``--with-display-convergence-detail``,
113+
and can request subproblem-solve timing diagnostics (requires one subproblem per
114+
rank) with ``--display-timing``.
113115

114116
Unfortunately, the word "converger" is also used to describe spokes that return bounds
115117
for the purpose of measuring overall convergence (as opposed to convergence within the hub

doc/src/secretmenu.rst

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,6 @@ There are many options that are not exposed in ``mpisppy.utils.config.py`` and w
55
a few of them here.
66

77

8-
display_timing
9-
--------------
10-
11-
This is a PH option that adds a barrier step to collect information about
12-
the time required to solve subproblems. This can be helpful in diagnosis
13-
and tuning of algorithms because for some problems, the variability in
14-
time to solve scenario sub-problems can be quite large.
15-
16-
.. Note::
17-
This option should be used only when there is exactly one subproblem per rank.
18-
19-
To set the option, use
20-
21-
.. code-block:: python
22-
23-
PHoptions["display_timing"] = True
24-
25-
E.g., if you were adding this to ``examples.farmer.farmer_cylinders`` where the
26-
hub definition dictionary is called ``hub_dict`` you would add
27-
28-
.. code-block:: python
29-
30-
hub_dict["opt_kwargs"]["PHoptions"]["display_timing"] = True
31-
32-
before passing it to ``spin_the_wheel``.
33-
34-
358
initial_proximal_cut_count
369
--------------------------
3710

examples/run_all.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ def do_one_mmw(dirname, modname, runefstring, npyfile, mmwargstring):
388388
"--num-scens=3 --max-iterations=5 "
389389
"--iter0-mipgap=0.01 --iterk-mipgap=0.005 "
390390
"--default-rho=1 --lagrangian --xhatshuffle --fwph "
391-
"--solver-name={} --display-progress".format(solver_name))
391+
"--solver-name={} --display-progress --display-timing".format(solver_name))
392392

393393
if egret_avail():
394394
print("\nSlow runs ahead...\n")

mpisppy/tests/test_ef_ph.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
import os
1717
import glob
18+
import io
19+
import contextlib
20+
import re
1821
import json
1922
import shutil
2023
import unittest
@@ -192,9 +195,76 @@ def test_ph_iter0(self):
192195
scenario_denouement,
193196
scenario_creator_kwargs={"scenario_count": 3},
194197
)
195-
198+
196199
conv, obj, tbound = ph.ph_main()
197200

201+
@unittest.skipIf(not solver_available,
202+
"no solver is available")
203+
def test_display_timing_emits_nonzero_solve_times(self):
204+
# End-to-end check that display_timing actually reaches the PH
205+
# solve loop and produces a non-zero solve-time report. Guards
206+
# the user-facing CLI flag (issue #290): if a future refactor
207+
# quietly drops the option from self.options or the print path,
208+
# this test fails.
209+
options = self._copy_of_base_options()
210+
options["PHIterLimit"] = 0
211+
options["display_timing"] = True
212+
213+
ph = mpisppy.opt.ph.PH(
214+
options,
215+
self.all3_scenario_names,
216+
scenario_creator,
217+
scenario_denouement,
218+
scenario_creator_kwargs={"scenario_count": 3},
219+
)
220+
221+
buf = io.StringIO()
222+
with contextlib.redirect_stdout(buf):
223+
ph.ph_main()
224+
out = buf.getvalue()
225+
226+
self.assertIn("Pyomo solve times (seconds):", out,
227+
msg=f"display_timing=True but timing header not in output:\n{out}")
228+
m = re.search(
229+
r"min=([0-9.]+)@\d+ mean=([0-9.]+) max=([0-9.]+)@\d+",
230+
out,
231+
)
232+
self.assertIsNotNone(
233+
m,
234+
msg=f"display_timing stats line not found in output:\n{out}",
235+
)
236+
mn, me, mx = float(m.group(1)), float(m.group(2)), float(m.group(3))
237+
# max across scenarios must be strictly positive — any real
238+
# subproblem solve takes measurable wallclock time. min/mean
239+
# could round to 0.00 under %4.2f if subproblems are tiny.
240+
self.assertGreater(
241+
mx, 0.0,
242+
msg=f"max solve time reported as zero: min={mn} mean={me} max={mx}",
243+
)
244+
245+
@unittest.skipIf(not solver_available,
246+
"no solver is available")
247+
def test_display_timing_off_suppresses_solve_times(self):
248+
# Companion: confirm the timing report is NOT printed when the
249+
# flag is False, so we know the assertion above isn't picking up
250+
# output emitted unconditionally somewhere.
251+
options = self._copy_of_base_options()
252+
options["PHIterLimit"] = 0
253+
options["display_timing"] = False
254+
255+
ph = mpisppy.opt.ph.PH(
256+
options,
257+
self.all3_scenario_names,
258+
scenario_creator,
259+
scenario_denouement,
260+
scenario_creator_kwargs={"scenario_count": 3},
261+
)
262+
263+
buf = io.StringIO()
264+
with contextlib.redirect_stdout(buf):
265+
ph.ph_main()
266+
self.assertNotIn("Pyomo solve times (seconds):", buf.getvalue())
267+
198268
@unittest.skipIf(not solver_available,
199269
"no solver is available")
200270
def test_fix_ph_iter0(self):

mpisppy/utils/cfg_vanilla.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def shared_options(cfg, is_hub=False):
6262
"verbose": cfg.verbose,
6363
"display_progress": cfg.display_progress,
6464
"display_convergence_detail": cfg.display_convergence_detail,
65+
"display_timing": cfg.display_timing,
6566
"iter0_solver_options": dict(),
6667
"iterk_solver_options": dict(),
6768
# Layered representation of solver options. Built in parallel
@@ -1034,6 +1035,7 @@ def subgradient_spoke(
10341035
options["PHIterLimit"] = cfg.max_iterations * 1_000_000
10351036
options["display_progress"] = False
10361037
options["display_convergence_detail"] = False
1038+
options["display_timing"] = False
10371039

10381040
add_ph_tracking(subgradient_spoke, cfg, spoke=True)
10391041
_maybe_attach_jensens(subgradient_spoke, cfg, "subgradient",
@@ -1076,6 +1078,7 @@ def ph_dual_spoke(
10761078
options["PHIterLimit"] = cfg.max_iterations * 1_000_000
10771079
options["display_progress"] = False
10781080
options["display_convergence_detail"] = False
1081+
options["display_timing"] = False
10791082

10801083
add_ph_tracking(ph_dual_spoke, cfg, spoke=True)
10811084

@@ -1116,6 +1119,7 @@ def relaxed_ph_spoke(
11161119
options["PHIterLimit"] = cfg.max_iterations * 1_000_000
11171120
options["display_progress"] = False
11181121
options["display_convergence_detail"] = False
1122+
options["display_timing"] = False
11191123

11201124
add_ph_tracking(relaxed_ph_spoke, cfg, spoke=True)
11211125

mpisppy/utils/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ def popular_args(self):
279279
domain=bool,
280280
default=False)
281281

282+
self.add_to_config('display_timing',
283+
description="display subproblem solve timing (requires exactly one subproblem per rank)",
284+
domain=bool,
285+
default=False)
286+
282287
self.add_to_config("max_solver_threads",
283288
description="Limit on threads per solver (default None)",
284289
domain=int,

0 commit comments

Comments
 (0)