linopy icon indicating copy to clipboard operation
linopy copied to clipboard

`io_api="mps"` leads to memory overhead

Open koen-vg opened this issue 1 year ago • 1 comments

Hi,

I was looking into memory consumption a little, and it seems that that passing an LP to the solver (Gurobi in this case) using the .mps format has a significant memory overhead. Here using mps:

Filename: /home/koen/linopy/linopy/solvers.py                                                                                                                                                              
                                                                                                                                                                                                           
Line #    Mem usage    Increment  Occurrences   Line Contents                                                                                                                                              
=============================================================                                                                                                                                              
   568    758.0 MiB    758.0 MiB           1   @profile                                                                                                                                                    
   569                                         def run_gurobi(                                                                                                                                             
   570                                             model,                                                                                                                                                  
   571                                             io_api=None,                                                                                                                                            
   572                                             problem_fn=None,                                                                                                                                        
   573                                             solution_fn=None,                                                                                                                                       
   574                                             log_fn=None,                                                                                                                                            
   575                                             warmstart_fn=None,                                                                                                                                      
   576                                             basis_fn=None,                                                                                                                                          
   577                                             keep_files=False,                                                                                                                                       
   578                                             env=None,                                                                                                                                               
   579                                             **solver_options,                                                                                                                                       
   580                                         ):                                                                                                                                                          
   581                                             """                                                                                                                                                     
   582                                             Solve a linear problem using the gurobi solver.                                                                                                         
   583                                                                                                                                                                                                     
   584                                             This function communicates with gurobi using the gurubipy package.                                                                                      
   585                                             """                                                                                                                                                     
   586                                             # see https://www.gurobi.com/documentation/10.0/refman/optimization_status_codes.html                                                                   
   587    758.0 MiB      0.0 MiB          18       CONDITION_MAP = {                                                                                                                                       
   588    758.0 MiB      0.0 MiB           1           1: "unknown",                                                                                                                                       
   589    758.0 MiB      0.0 MiB           1           2: "optimal",                                                                                                                                       
   590    758.0 MiB      0.0 MiB           1           3: "infeasible",                                                                                                                                    
   591    758.0 MiB      0.0 MiB           1           4: "infeasible_or_unbounded",                                                                                                                       
   592    758.0 MiB      0.0 MiB           1           5: "unbounded",                                                                                                                                     
   593    758.0 MiB      0.0 MiB           1           6: "other",                                                                                                                                         
   594    758.0 MiB      0.0 MiB           1           7: "iteration_limit",                                                                                                                               
   595    758.0 MiB      0.0 MiB           1           8: "terminated_by_limit",                                                                                                                           
   596    758.0 MiB      0.0 MiB           1           9: "time_limit",                                                                                                                                    
   597    758.0 MiB      0.0 MiB           1           10: "optimal",                                                                                                                                      
   598    758.0 MiB      0.0 MiB           1           11: "user_interrupt",                                                                                                                               
   599    758.0 MiB      0.0 MiB           1           12: "other",                                                                                                                                        
   600    758.0 MiB      0.0 MiB           1           13: "suboptimal",                                                                                                                                   
   601    758.0 MiB      0.0 MiB           1           14: "unknown",                                                                                                                                      
   602    758.0 MiB      0.0 MiB           1           15: "terminated_by_limit",                                                                                                                          
   603    758.0 MiB      0.0 MiB           1           16: "internal_solver_error",                                                                                                                        
   604    758.0 MiB      0.0 MiB           1           17: "internal_solver_error",
   605                                             }
   606                                         
   607    758.0 MiB      0.0 MiB           1       log_fn = maybe_convert_path(log_fn)
   608    758.0 MiB      0.0 MiB           1       warmstart_fn = maybe_convert_path(warmstart_fn)
   609    758.0 MiB      0.0 MiB           1       basis_fn = maybe_convert_path(basis_fn)
   610                                                                                                                                                                                                     
   611   1509.5 MiB      0.0 MiB           2       with contextlib.ExitStack() as stack:
   612    758.0 MiB      0.0 MiB           1           if env is None:
   613    758.9 MiB      0.9 MiB           1               env = stack.enter_context(gurobipy.Env())
   614                                         
   615    758.9 MiB      0.0 MiB           1           if io_api is None or io_api in ["lp", "mps"]:
   616   1197.0 MiB    438.0 MiB           1               problem_fn = model.to_file(problem_fn)
   617   1197.0 MiB      0.0 MiB           1               problem_fn = maybe_convert_path(problem_fn)
   618   1222.5 MiB     25.5 MiB           1               m = gurobipy.read(problem_fn, env=env)
   619                                                 elif io_api == "direct":
   620                                                     problem_fn = None
   621                                                     m = model.to_gurobipy(env=env)
   622                                                 else:
   623                                                     raise ValueError(
   624                                                         "Keyword argument `io_api` has to be one of `lp`, `mps`, `direct` or None"
   625                                                     )
   626                                         
   627   1222.5 MiB      0.0 MiB           1           if solver_options is not None:
   628   1222.5 MiB      0.0 MiB          10               for key, value in solver_options.items():
   629   1222.5 MiB      0.0 MiB           9                   m.setParam(key, value)
   630   1222.5 MiB      0.0 MiB           1           if log_fn is not None:
   631   1222.5 MiB      0.0 MiB           1               m.setParam("logfile", log_fn)
   632                                         
   633   1222.5 MiB      0.0 MiB           1           if warmstart_fn:
   634                                                     m.read(warmstart_fn)
   635   1509.5 MiB    287.0 MiB           1           m.optimize()
   636                                         
   637   1509.5 MiB      0.0 MiB           1           if basis_fn:
   638                                                     try:
   639                                                         m.write(basis_fn)
   640                                                     except gurobipy.GurobiError as err:
   641                                                         logger.info("No model basis stored. Raised error: %s", err)
   642                                         
   643   1509.5 MiB      0.0 MiB           1           condition = m.status
   644   1509.5 MiB      0.0 MiB           1           termination_condition = CONDITION_MAP.get(condition, condition)
   645   1509.5 MiB      0.0 MiB           1           status = Status.from_termination_condition(termination_condition)
   646   1509.5 MiB      0.0 MiB           1           status.legacy_status = condition
   647                                         
   648   1509.5 MiB      0.0 MiB           2           def get_solver_solution() -> Solution:
   649   1509.5 MiB      0.0 MiB           1               objective = m.ObjVal
   650                                         
   651   1771.6 MiB    262.1 MiB      447055               sol = pd.Series({v.VarName: v.x for v in m.getVars()}, dtype=float)
   652   1774.6 MiB      3.0 MiB           1               sol = set_int_index(sol)
   653                                         
   654   1774.6 MiB      0.0 MiB           1               try:
   655   1828.0 MiB      0.0 MiB           2                   dual = pd.Series(
   656   1828.0 MiB     53.4 MiB      915807                       {c.ConstrName: c.Pi for c in m.getConstrs()}, dtype=float
   657                                                         )
   658   1830.2 MiB      2.3 MiB           1                   dual = set_int_index(dual)
   659                                                     except AttributeError:
   660                                                         logger.warning("Dual values of MILP couldn't be parsed")
   661                                                         dual = pd.Series(dtype=float)
   662                                         
   663   1830.2 MiB      0.0 MiB           1               return Solution(sol, dual, objective)
   664                                         
   665   1830.2 MiB      0.0 MiB           1       solution = safe_get_solution(status, get_solver_solution)
   666   1830.2 MiB      0.0 MiB           1       maybe_adjust_objective_sign(solution, model.objective.sense, io_api)
   667                                         
   668   1830.2 MiB      0.0 MiB           1       return Result(status, solution, m)

Compared with the .lp format:

Filename: /home/koen/linopy/linopy/solvers.py                                                                                                                                                              
                                                                                                                                                                                                           
Line #    Mem usage    Increment  Occurrences   Line Contents                                                                                                                                              
=============================================================                                                                                                                                              
   568    756.6 MiB    756.6 MiB           1   @profile                                                                                                                                                    
   569                                         def run_gurobi(                                                                                                                                             
   570                                             model,                                                                                                                                                  
   571                                             io_api=None,                                                                                                                                            
   572                                             problem_fn=None,                                                                                                                                        
   573                                             solution_fn=None,                                                                                                                                       
   574                                             log_fn=None,
   575                                             warmstart_fn=None,
   576                                             basis_fn=None,
   577                                             keep_files=False,
   578                                             env=None,
   579                                             **solver_options,
   580                                         ):
   581                                             """
   582                                             Solve a linear problem using the gurobi solver.
   583                                         
   584                                             This function communicates with gurobi using the gurubipy package.
   585                                             """
   586                                             # see https://www.gurobi.com/documentation/10.0/refman/optimization_status_codes.html
   587    756.6 MiB      0.0 MiB          18       CONDITION_MAP = {
   588    756.6 MiB      0.0 MiB           1           1: "unknown",
   589    756.6 MiB      0.0 MiB           1           2: "optimal",
   590    756.6 MiB      0.0 MiB           1           3: "infeasible",
   591    756.6 MiB      0.0 MiB           1           4: "infeasible_or_unbounded",
   592    756.6 MiB      0.0 MiB           1           5: "unbounded",
   593    756.6 MiB      0.0 MiB           1           6: "other",
   594    756.6 MiB      0.0 MiB           1           7: "iteration_limit",
   595    756.6 MiB      0.0 MiB           1           8: "terminated_by_limit",
   596    756.6 MiB      0.0 MiB           1           9: "time_limit",
   597    756.6 MiB      0.0 MiB           1           10: "optimal",
   598    756.6 MiB      0.0 MiB           1           11: "user_interrupt",
   599    756.6 MiB      0.0 MiB           1           12: "other",
   600    756.6 MiB      0.0 MiB           1           13: "suboptimal",
   601    756.6 MiB      0.0 MiB           1           14: "unknown",
   602    756.6 MiB      0.0 MiB           1           15: "terminated_by_limit",
   603    756.6 MiB      0.0 MiB           1           16: "internal_solver_error",
   604    756.6 MiB      0.0 MiB           1           17: "internal_solver_error",
   605                                             }
   606                                         
   607    756.6 MiB      0.0 MiB           1       log_fn = maybe_convert_path(log_fn)
   608    756.6 MiB      0.0 MiB           1       warmstart_fn = maybe_convert_path(warmstart_fn)
   609    756.6 MiB      0.0 MiB           1       basis_fn = maybe_convert_path(basis_fn)
   610                                         
   611   1189.5 MiB      0.0 MiB           2       with contextlib.ExitStack() as stack:
   612    756.6 MiB      0.0 MiB           1           if env is None:
   613    757.5 MiB      0.9 MiB           1               env = stack.enter_context(gurobipy.Env())
   614                                         
   615    757.5 MiB      0.0 MiB           1           if io_api is None or io_api in ["lp", "mps"]:
   616    746.6 MiB    -10.9 MiB           1               problem_fn = model.to_file(problem_fn)
   617    746.6 MiB      0.0 MiB           1               problem_fn = maybe_convert_path(problem_fn)
   618    999.4 MiB    252.8 MiB           1               m = gurobipy.read(problem_fn, env=env)
   619                                                 elif io_api == "direct":
   620                                                     problem_fn = None
   621                                                     m = model.to_gurobipy(env=env)
   622                                                 else:
   623                                                     raise ValueError(
   624                                                         "Keyword argument `io_api` has to be one of `lp`, `mps`, `direct` or None"
   625                                                     )
   626                                         
   627    999.4 MiB      0.0 MiB           1           if solver_options is not None:
   628   1000.0 MiB      0.0 MiB          10               for key, value in solver_options.items():
   629   1000.0 MiB      0.6 MiB           9                   m.setParam(key, value)
   630   1000.0 MiB      0.0 MiB           1           if log_fn is not None:
   631   1000.0 MiB      0.0 MiB           1               m.setParam("logfile", log_fn)
   632                                         
   633   1000.0 MiB      0.0 MiB           1           if warmstart_fn:
   634                                                     m.read(warmstart_fn)
   635   1189.5 MiB    189.5 MiB           1           m.optimize()
   636                                         
   637   1189.5 MiB      0.0 MiB           1           if basis_fn:
   638                                                     try:
   639                                                         m.write(basis_fn)
   640                                                     except gurobipy.GurobiError as err:
   641                                                         logger.info("No model basis stored. Raised error: %s", err)
   642                                         
   643   1189.5 MiB      0.0 MiB           1           condition = m.status
   644   1189.5 MiB      0.0 MiB           1           termination_condition = CONDITION_MAP.get(condition, condition)
   645   1189.5 MiB      0.0 MiB           1           status = Status.from_termination_condition(termination_condition)
   646   1189.5 MiB      0.0 MiB           1           status.legacy_status = condition
   647                                         
   648   1189.5 MiB      0.0 MiB           2           def get_solver_solution() -> Solution:
   649   1189.5 MiB      0.0 MiB           1               objective = m.ObjVal
   650                                         
   651   1440.2 MiB    250.7 MiB      447055               sol = pd.Series({v.VarName: v.x for v in m.getVars()}, dtype=float)
   652   1442.3 MiB      2.1 MiB           1               sol = set_int_index(sol)
   653                                         
   654   1442.3 MiB      0.0 MiB           1               try:
   655   1496.7 MiB      0.0 MiB           2                   dual = pd.Series(
   656   1496.7 MiB     54.4 MiB      915807                       {c.ConstrName: c.Pi for c in m.getConstrs()}, dtype=float
   657                                                         )
   658   1498.9 MiB      2.2 MiB           1                   dual = set_int_index(dual)
   659                                                     except AttributeError:
   660                                                         logger.warning("Dual values of MILP couldn't be parsed")
   661                                                         dual = pd.Series(dtype=float)
   662                                         
   663   1498.9 MiB      0.0 MiB           1               return Solution(sol, dual, objective)
   664                                         
   665   1498.9 MiB      0.0 MiB           1       solution = safe_get_solution(status, get_solver_solution)
   666   1498.9 MiB      0.0 MiB           1       maybe_adjust_objective_sign(solution, model.objective.sense, io_api)
   667                                         
   668   1498.9 MiB      0.0 MiB           1       return Result(status, solution, m)

I'm using pypsa-eur to create and solve these models, and "from the outside", pypsa-eur reports a maximum total memory usage of 2531MB in the first case (mps) and 2215MB in the second case (lp).

It's a little strange to me since I would think that the highspy model that's allocated for the mps export would be deallocated immediately after the file export (when it's not needed anymore), but I guess that's not the case. A naive attempt at adding inserting del h at line 302 in io.py doesn't make any difference whatsoever:

h = m.to_highspy()
h.writeModel(str(fn))
del h

I guess I'm just not very good at understanding how memory works in Python.

For completeness, the direct api interface for Gurobi is the worst of the three; the relevant section of the memory profile looks like this

607    754.9 MiB      0.0 MiB           1       log_fn = maybe_convert_path(log_fn)
   608    754.9 MiB      0.0 MiB           1       warmstart_fn = maybe_convert_path(warmstart_fn)
   609    754.9 MiB      0.0 MiB           1       basis_fn = maybe_convert_path(basis_fn)
   610                                         
   611   1701.5 MiB      0.0 MiB           2       with contextlib.ExitStack() as stack:
   612    754.9 MiB      0.0 MiB           1           if env is None:
   613    755.8 MiB      0.9 MiB           1               env = stack.enter_context(gurobipy.Env())
   614                                         
   615    755.8 MiB      0.0 MiB           1           if io_api is None or io_api in ["lp", "mps"]:
   616                                                     problem_fn = model.to_file(problem_fn)
   617                                                     problem_fn = maybe_convert_path(problem_fn)
   618                                                     m = gurobipy.read(problem_fn, env=env)
   619    755.8 MiB      0.0 MiB           1           elif io_api == "direct":
   620    755.8 MiB      0.0 MiB           1               problem_fn = None
   621   1422.2 MiB    666.4 MiB           1               m = model.to_gurobipy(env=env)
   622                                                 else:
   623                                                     raise ValueError(
   624                                                         "Keyword argument `io_api` has to be one of `lp`, `mps`, `direct` or None"
   625                                                     )
   626                                         
   627   1422.2 MiB      0.0 MiB           1           if solver_options is not None:
   628   1422.2 MiB      0.0 MiB          10               for key, value in solver_options.items():
   629   1422.2 MiB      0.0 MiB           9                   m.setParam(key, value)
   630   1422.2 MiB      0.0 MiB           1           if log_fn is not None:
   631   1422.2 MiB      0.0 MiB           1               m.setParam("logfile", log_fn)
   632                                         
   633   1422.2 MiB      0.0 MiB           1           if warmstart_fn:
   634                                                     m.read(warmstart_fn)
   635   1701.5 MiB    279.3 MiB           1           m.optimize()
   636                                         
   637   1701.5 MiB      0.0 MiB           1           if basis_fn:
   638                                                     try:
   639                                                         m.write(basis_fn)
   640                                                     except gurobipy.GurobiError as err:
   641                                                         logger.info("No model basis stored. Raised error: %s", err)
   642                                         

But pypsa-eur reports a maximum memory usage of 2722MB.

Memory usage of gurobipy seems to be somewhat of a knows issue, see https://support.gurobi.com/hc/en-us/community/posts/12387779995025/comments/12416251123217

Until anyone has any bright ideas for improvements, I figured this issue could at least serve as a reference; the short take-away is that you should use the .lp io interface for the moment if you care about memory.

koen-vg avatar Mar 28 '24 10:03 koen-vg

This is somewhat expected, MPS files are larger in size than LP files. Maybe I'm missing something? It looks like the largest differences are from handling a larger file?

torressa avatar Sep 02 '24 17:09 torressa