Skip to content

Commit dde92a6

Browse files
committed
Merge remote-tracking branch 'rael/wecc' into improve_load_aug
2 parents 2bf991e + 2fe5627 commit dde92a6

File tree

4 files changed

+410
-16
lines changed

4 files changed

+410
-16
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: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from switch_model.tools.graph.cli_graph import main as graph_main
3636
from switch_model.utilities.patches import patch_pyomo
3737
from switch_model.utilities.results_info import save_info, add_info, ResultsInfoSection
38+
import switch_model.utilities.gurobi_aug # We keep this line here to ensure that 'gurobi_aug' gets registered as a solver
3839

3940

4041
def main(
@@ -155,9 +156,6 @@ def debug(type, value, tb):
155156
# create an instance (also reports time spent reading data and loading into model)
156157
instance = model.load_inputs(attach_data_portal=attach_data_portal)
157158

158-
if instance.options.warm_start:
159-
warm_start(instance)
160-
161159
#### Below here, we refer to instance instead of model ####
162160

163161
instance.pre_solve()
@@ -190,6 +188,13 @@ def debug(type, value, tb):
190188
# We no longer need model (only using instance) so we can garbage collect it to optimize our memory usage
191189
del model
192190

191+
if instance.options.warm_start_mip:
192+
if instance.options.verbose:
193+
timer.step_time()
194+
warm_start_mip(instance)
195+
if instance.options.verbose:
196+
print(f"Loaded warm start inputs in {timer.step_time_as_str()}.")
197+
193198
if instance.options.reload_prior_solution:
194199
print("Loading prior solution...")
195200
reload_prior_solution_from_pickle(instance, instance.options.outputs_dir)
@@ -276,14 +281,14 @@ def debug(type, value, tb):
276281
)
277282

278283

279-
def warm_start(instance):
284+
def warm_start_mip(instance):
280285
"""
281-
This function loads in the variables from a previous run
282-
and starts out our model at these variables to make it reach
283-
a solution faster.
286+
This function loads the results from a previous run into the Pyomo variables.
287+
This allows Gurobi's Mixed Integer Programming algorithm to "warm start" (start closer to the solution).
288+
Warm starting only works in Gurobi if the initial values don't violate any constraints
289+
(i.e. valid but not optimal solution).
284290
"""
285-
warm_start_timer = StepTimer()
286-
warm_start_dir = os.path.join(instance.options.warm_start, "outputs")
291+
warm_start_dir = os.path.join(instance.options.warm_start_mip, "outputs")
287292
if not os.path.isdir(warm_start_dir):
288293
warnings.warn(
289294
f"Path {warm_start_dir} does not exist and cannot be used to warm start solver. Warm start skipped."
@@ -310,8 +315,6 @@ def warm_start(instance):
310315
# If the index isn't valid that's ok, just don't warm start that variable
311316
pass
312317

313-
print(f"Loaded warm start inputs in {warm_start_timer.step_time_as_str()}.")
314-
315318

316319
def reload_prior_solution_from_pickle(instance, outdir):
317320
with open(os.path.join(outdir, "results.pickle"), "rb") as fh:
@@ -528,6 +531,12 @@ def define_arguments(argparser):
528531
help="A quoted string of options to pass to the model solver. Each option must be of the form option=value. "
529532
"(e.g., --solver-options-string \"mipgap=0.001 primalopt='' advance=2 threads=1\")",
530533
)
534+
argparser.add_argument(
535+
"--solver-method",
536+
default=None,
537+
type=int,
538+
help="Specify the solver method to use.",
539+
)
531540
argparser.add_argument(
532541
"--keepfiles",
533542
action="store_true",
@@ -639,6 +648,12 @@ def define_arguments(argparser):
639648
action="store_true",
640649
help="Save the solution to a pickle file after model is solved to allow for later inspection via --reload-prior-solution.",
641650
)
651+
argparser.add_argument(
652+
"--save-warm-start",
653+
default=False,
654+
action="store_true",
655+
help="Save warm_start.pickle to the outputs which allows future runs to warm start from this one.",
656+
)
642657
argparser.add_argument(
643658
"--interact",
644659
default=False,
@@ -685,10 +700,19 @@ def define_arguments(argparser):
685700
help="Number of threads to be used while solving. Currently only supported for Gurobi",
686701
)
687702

703+
argparser.add_argument(
704+
"--warm-start-mip",
705+
default=None,
706+
help="Enables warm start for a Mixed Integer problem by specifying the "
707+
"path to a previous scenario. Warm starting only works if the solution to the previous solution"
708+
"is also a feasible (but not necessarily optimal) solution to the current scenario.",
709+
)
710+
688711
argparser.add_argument(
689712
"--warm-start",
690713
default=None,
691-
help="Path to folder of directory to use for warm start",
714+
help="Enables warm start for a LP Problem by specifying the path to the previous scenario. Note"
715+
" that all variables must be the same between the previous and current scenario.",
692716
)
693717

694718

@@ -719,7 +743,7 @@ def add_recommended_args(argparser):
719743
"--recommended-debug",
720744
default=False,
721745
action="store_true",
722-
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"',
746+
help="Same as --recommended but adds the flags --keepfiles --tempdir temp --symbolic-solver-labels which are useful when debugging Gurobi.",
723747
)
724748

725749

@@ -748,8 +772,10 @@ def parse_recommended_args(args):
748772
"--log-run",
749773
"--debug",
750774
"--graph",
775+
"--solver-method",
776+
"2", # Method 2 is barrier solve which is much faster than default
751777
] + args
752-
solver_options_string = "BarHomogeneous=1 FeasibilityTol=1e-5 method=2"
778+
solver_options_string = "BarHomogeneous=1 FeasibilityTol=1e-5"
753779
if options.recommended_fast:
754780
solver_options_string += " crossover=0"
755781
args = ["--solver-options-string", solver_options_string] + args
@@ -851,6 +877,18 @@ def solve(model):
851877
solver = model.solver
852878
solver_manager = model.solver_manager
853879
else:
880+
# If we need warm start switch the solver to our augmented version that supports warm starting
881+
if model.options.warm_start is not None or model.options.save_warm_start:
882+
if model.options.solver not in ("gurobi", "gurobi_direct"):
883+
raise NotImplementedError(
884+
"Warm start functionality requires --solver gurobi"
885+
)
886+
model.options.solver = "gurobi_aug"
887+
888+
if model.options.warm_start is not None:
889+
# Method 1 (dual simplex) is required since it supports warm starting.
890+
model.options.solver_method = 1
891+
854892
# Create a solver object the first time in. We don't do this until a solve is
855893
# requested, because sometimes a different solve function may be used,
856894
# with its own solver object (e.g., with runph or a parallel solver server).
@@ -883,6 +921,15 @@ def solve(model):
883921

884922
model.options.solver_options_string += f" Threads={model.options.threads}"
885923

924+
if model.options.solver_method is not None:
925+
# If no string is passed make the string empty so we can add to it
926+
if model.options.solver_options_string is None:
927+
model.options.solver_options_string = ""
928+
929+
model.options.solver_options_string += (
930+
f" method={model.options.solver_method}"
931+
)
932+
886933
solver_manager = SolverManagerFactory(model.options.solver_manager)
887934

888935
# get solver arguments
@@ -896,9 +943,17 @@ def solve(model):
896943
else None,
897944
)
898945

899-
if model.options.warm_start is not None:
946+
if model.options.warm_start_mip is not None or model.options.warm_start is not None:
900947
solver_args["warmstart"] = True
901948

949+
if model.options.warm_start is not None:
950+
solver_args["read_warm_start"] = os.path.join(
951+
model.options.warm_start, "outputs", "warm_start.pickle"
952+
)
953+
954+
if model.options.save_warm_start:
955+
solver_args["write_warm_start"] = os.path.join("outputs", "warm_start.pickle")
956+
902957
# drop all the unspecified options
903958
solver_args = {k: v for (k, v) in solver_args.items() if v is not None}
904959

switch_model/utilities/__init__.py

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

223-
def __init__(self):
223+
def __init__(self, msg=None):
224224
self.start_time = time.time()
225+
if msg is not None:
226+
print(msg)
225227

226228
def step_time(self):
227229
"""

0 commit comments

Comments
 (0)