Skip to content

Commit 31b8a8f

Browse files
committed
Merge remote-tracking branch 'rael/wecc' into improve_load_aug
2 parents 2585613 + 54755dd commit 31b8a8f

File tree

4 files changed

+380
-17
lines changed

4 files changed

+380
-17
lines changed

docs/Performance.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Performance
2+
3+
Memory use and solve time are two important factors that we try to keep to a minimum in our models. There are multiple
4+
things one can do to improve performance.
5+
6+
## Solving methods
7+
8+
By far the biggest factor that impacts performance is the method used by Gurobi. The fastest method is barrier solve
9+
without crossover (use `--recommended-fast`)
10+
however this method often returns a suboptimal solution. The next fastest is barrier solve followed by crossover and
11+
simplex (use `--recommended`) which almost always works. In some cases barrier solve encounters numerical issues (
12+
see [`Numerical Issues.md`](./Numerical%20Issues.md))
13+
in which case the slower Simplex method must be used (`--solver-options-string method=1`).
14+
15+
## Solver interface
16+
17+
Solver interfaces are how Pyomo communicates with Gurobi (or any solver).
18+
19+
There are two solver interfaces that you should know about: `gurobi` and `gurobi_direct`.
20+
21+
- When using `gurobi`, Pyomo will write the entire model to a temporary text file and then start a *separate Gurobi
22+
process* that will read the file, solve the model and write the results to another temporary text file. Once Gurobi
23+
finishes writing the results Pyomo will read the results text file and load the results back into the Python program
24+
before running post_solve (e.g. generate csv files, create graphs, etc). Note that these temporary text files are
25+
stored in `/tmp` but if you use `--recommended-debug` Pyomo and Gurobi will instead use a `temp` folder in your model.
26+
27+
- `gurobi_direct` uses Gurobi's Python library to create and solve the model directly in Python without the use of
28+
intermediate text files.
29+
30+
In theory `gurobi_direct` should be faster and more efficient however in practice we find that that's not the case. As
31+
such we recommend using `gurobi` and all our defaults do so. If someone has the time they could profile `gurobi_direct`
32+
to improve performance at which point we could make `gurobi_direct` the default (and enable `--save-warm-start` by default, see below).
33+
34+
The `gurobi` interface has the added advantage of separating Gurobi and Pyomo into separate threads. This means that
35+
while Gurobi is solving and Pyomo is idle, the operating system can automatically move Pyomo's memory usage
36+
to [virtual memory](https://serverfault.com/questions/48486/what-is-swap-memory)
37+
which will free up more memory for Gurobi.
38+
39+
## Warm starting
40+
41+
Warm starting is the act of using a solution from a previous similar model to start the solver closer to your expected
42+
solution. Theoretically this can help performance however in practice there are several limitations. For this section, *
43+
previous solution* refers to the results from an already solved model that you are using to warm start the solver. *
44+
Current solution* refers to the solution you are trying to find while using the warm start feature.
45+
46+
- To warm start a model use `switch solve --warm-start <path_to_previous_solution>`.
47+
48+
- Warm starting only works if the previous solution does not break any constraints of the current solution. This usually
49+
only happens if a) the model has the exact same set of variables b)
50+
the previous solution was "harder" (e.g. it had more constraints to satisfy).
51+
52+
- Warm starting always uses the slower Simplex method. This means unless you expect the previous solution and current
53+
solution to be very similar, it may be faster to solve without warm start using the barrier method.
54+
55+
- If your previous solution didn't use crossover (e.g. you used `--recommended-fast`) then warm starting will be even
56+
slower since the solver will need to first run crossover before warm starting.
57+
58+
- Our implementation of warm starting only works if your previous solution has an `outputs/warm_start.pickle`
59+
file. This file is only generated when you use `--save-warm-start`.
60+
61+
- `--save-warm-start` and `--warm-start` both use an extension of the `gurobi_direct` solver interface which is
62+
generally slower than the `gurobi` solver interface (see section above).
63+
64+
## Tools for improving performance
65+
66+
- [Memory profiler](https://pypi.org/project/memory-profiler/) for generating plots of the memory
67+
use over time. Use `mprof run --interval 60 --multiprocess switch solve ...` and once solving is done
68+
run `mprof plot -o profile.png` to make the plot.
69+
70+
- [Fil Profiler](https://pypi.org/project/filprofiler/) is an amazing tool for seeing which parts of the code are
71+
using up memory during peak memory usage.
72+
73+
- Using `switch_model.utilities.StepTimer` to measure how long certain code blocks take to run. See examples
74+
throughout the code.

switch_model/solve.py

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from switch_model.tools.graph.cli_graph import main as graph_main
2525
from switch_model.utilities.patches import patch_pyomo
2626
from switch_model.utilities.results_info import save_info, add_info, ResultsInfoSection
27-
27+
import switch_model.utilities.gurobi_aug # We keep this line here to ensure that 'gurobi_aug' gets registered as a solver
2828

2929
def main(args=None, return_model=False, return_instance=False, attach_data_portal=False):
3030
start_to_end_timer = StepTimer()
@@ -128,9 +128,6 @@ def debug(type, value, tb):
128128
# create an instance (also reports time spent reading data and loading into model)
129129
instance = model.load_inputs(attach_data_portal=attach_data_portal)
130130

131-
if instance.options.warm_start:
132-
warm_start(instance)
133-
134131
#### Below here, we refer to instance instead of model ####
135132

136133
instance.pre_solve()
@@ -161,6 +158,13 @@ def debug(type, value, tb):
161158
# We no longer need model (only using instance) so we can garbage collect it to optimize our memory usage
162159
del model
163160

161+
if instance.options.warm_start_mip:
162+
if instance.options.verbose:
163+
timer.step_time()
164+
warm_start_mip(instance)
165+
if instance.options.verbose:
166+
print(f"Loaded warm start inputs in {timer.step_time_as_str()}.")
167+
164168
if instance.options.reload_prior_solution:
165169
print('Loading prior solution...')
166170
reload_prior_solution_from_pickle(instance, instance.options.outputs_dir)
@@ -231,14 +235,14 @@ def debug(type, value, tb):
231235
code.interact(banner=banner, local=dict(list(globals().items()) + list(locals().items())))
232236

233237

234-
def warm_start(instance):
238+
def warm_start_mip(instance):
235239
"""
236-
This function loads in the variables from a previous run
237-
and starts out our model at these variables to make it reach
238-
a solution faster.
240+
This function loads the results from a previous run into the Pyomo variables.
241+
This allows Gurobi's Mixed Integer Programming algorithm to "warm start" (start closer to the solution).
242+
Warm starting only works in Gurobi if the initial values don't violate any constraints
243+
(i.e. valid but not optimal solution).
239244
"""
240-
warm_start_timer = StepTimer()
241-
warm_start_dir = os.path.join(instance.options.warm_start, "outputs")
245+
warm_start_dir = os.path.join(instance.options.warm_start_mip, "outputs")
242246
if not os.path.isdir(warm_start_dir):
243247
warnings.warn(
244248
f"Path {warm_start_dir} does not exist and cannot be used to warm start solver. Warm start skipped.")
@@ -262,8 +266,6 @@ def warm_start(instance):
262266
# If the index isn't valid that's ok, just don't warm start that variable
263267
pass
264268

265-
print(f"Loaded warm start inputs in {warm_start_timer.step_time_as_str()}.")
266-
267269

268270
def reload_prior_solution_from_pickle(instance, outdir):
269271
with open(os.path.join(outdir, 'results.pickle'), 'rb') as fh:
@@ -442,6 +444,8 @@ def define_arguments(argparser):
442444
argparser.add_argument("--solver-options-string", default=None,
443445
help='A quoted string of options to pass to the model solver. Each option must be of the form option=value. '
444446
'(e.g., --solver-options-string "mipgap=0.001 primalopt=\'\' advance=2 threads=1")')
447+
argparser.add_argument("--solver-method", default=None, type=int,
448+
help="Specify the solver method to use.")
445449
argparser.add_argument("--keepfiles", action='store_true', default=None,
446450
help="Keep temporary files produced by the solver (may be useful with --symbolic-solver-labels)")
447451
argparser.add_argument(
@@ -503,6 +507,10 @@ def define_arguments(argparser):
503507
argparser.add_argument(
504508
'--save-solution', default=False, action='store_true',
505509
help="Save the solution to a pickle file after model is solved to allow for later inspection via --reload-prior-solution.")
510+
argparser.add_argument(
511+
'--save-warm-start', default=False, action='store_true',
512+
help="Save warm_start.pickle to the outputs which allows future runs to warm start from this one."
513+
)
506514
argparser.add_argument(
507515
'--interact', default=False, action='store_true',
508516
help='Enter interactive shell after solving the instance to enable inspection of the solved model.')
@@ -533,9 +541,17 @@ def define_arguments(argparser):
533541
help="Number of threads to be used while solving. Currently only supported for Gurobi"
534542
)
535543

544+
argparser.add_argument(
545+
"--warm-start-mip", default=None,
546+
help="Enables warm start for a Mixed Integer problem by specifying the "
547+
"path to a previous scenario. Warm starting only works if the solution to the previous solution"
548+
"is also a feasible (but not necessarily optimal) solution to the current scenario."
549+
)
550+
536551
argparser.add_argument(
537552
"--warm-start", default=None,
538-
help="Path to folder of directory to use for warm start"
553+
help="Enables warm start for a LP Problem by specifying the path to the previous scenario. Note"
554+
" that all variables must be the same between the previous and current scenario."
539555
)
540556

541557

@@ -560,7 +576,7 @@ def add_recommended_args(argparser):
560576

561577
argparser.add_argument(
562578
"--recommended-debug", default=False, action='store_true',
563-
help='Equivalent to running with all of the following options: --solver gurobi -v --sorted-output --keepfiles --tempdir temp --stream-output --symbolic-solver-labels --log-run --debug --solver-options-string "method=2 BarHomogeneous=1 FeasibilityTol=1e-5"'
579+
help='Same as --recommended but adds the flags --keepfiles --tempdir temp --symbolic-solver-labels which are useful when debugging Gurobi.'
564580
)
565581

566582

@@ -584,8 +600,9 @@ def parse_recommended_args(args):
584600
'--log-run',
585601
'--debug',
586602
'--graph',
603+
'--solver-method', '2', # Method 2 is barrier solve which is much faster than default
587604
] + args
588-
solver_options_string = "BarHomogeneous=1 FeasibilityTol=1e-5 method=2"
605+
solver_options_string = "BarHomogeneous=1 FeasibilityTol=1e-5"
589606
if options.recommended_fast:
590607
solver_options_string += " crossover=0"
591608
args = ['--solver-options-string', solver_options_string] + args
@@ -671,6 +688,16 @@ def solve(model):
671688
solver = model.solver
672689
solver_manager = model.solver_manager
673690
else:
691+
# If we need warm start switch the solver to our augmented version that supports warm starting
692+
if model.options.warm_start is not None or model.options.save_warm_start:
693+
if model.options.solver not in ("gurobi", "gurobi_direct"):
694+
raise NotImplementedError("Warm start functionality requires --solver gurobi")
695+
model.options.solver = "gurobi_aug"
696+
697+
if model.options.warm_start is not None:
698+
# Method 1 (dual simplex) is required since it supports warm starting.
699+
model.options.solver_method = 1
700+
674701
# Create a solver object the first time in. We don't do this until a solve is
675702
# requested, because sometimes a different solve function may be used,
676703
# with its own solver object (e.g., with runph or a parallel solver server).
@@ -701,6 +728,13 @@ def solve(model):
701728

702729
model.options.solver_options_string += f" Threads={model.options.threads}"
703730

731+
if model.options.solver_method is not None:
732+
# If no string is passed make the string empty so we can add to it
733+
if model.options.solver_options_string is None:
734+
model.options.solver_options_string = ""
735+
736+
model.options.solver_options_string += f" method={model.options.solver_method}"
737+
704738
solver_manager = SolverManagerFactory(model.options.solver_manager)
705739

706740
# get solver arguments
@@ -712,9 +746,15 @@ def solve(model):
712746
save_results=model.options.save_solution if isinstance(solver, DirectOrPersistentSolver) else None,
713747
)
714748

715-
if model.options.warm_start is not None:
749+
if model.options.warm_start_mip is not None or model.options.warm_start is not None:
716750
solver_args["warmstart"] = True
717751

752+
if model.options.warm_start is not None:
753+
solver_args["read_warm_start"] = os.path.join(model.options.warm_start, "outputs", "warm_start.pickle")
754+
755+
if model.options.save_warm_start:
756+
solver_args["write_warm_start"] = os.path.join("outputs", "warm_start.pickle")
757+
718758
# drop all the unspecified options
719759
solver_args = {k: v for (k, v) in solver_args.items() if v is not None}
720760

switch_model/utilities/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,10 @@ class StepTimer(object):
211211
reset the timer at each step by calling timer.step_time()
212212
"""
213213

214-
def __init__(self):
214+
def __init__(self, msg=None):
215215
self.start_time = time.time()
216+
if msg is not None:
217+
print(msg)
216218

217219
def step_time(self):
218220
"""

0 commit comments

Comments
 (0)