Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot dispose of environment programatically #36

Open
staadecker opened this issue Feb 12, 2025 · 15 comments
Open

Cannot dispose of environment programatically #36

staadecker opened this issue Feb 12, 2025 · 15 comments

Comments

@staadecker
Copy link

staadecker commented Feb 12, 2025

Hi again,

Correct me if I'm wrong but it seems that there is no way to "cleanup" a PyOptInterface model.

Our team has an issue where we don't know how to end our Gurobi Compute Server run. We tried running del model but it doesn't work (the compute server continues to indicate that the model is "running"). Is it possible to either:

  1. Expose Gurobi's .dispose() method so we can manually cleanup our model.
  2. Automatically call Gurobi's .dispose() function whenever a PyOptInterface model is deleted. For example by overriding the model's __del__ method.

Thank you!!
Martin

@metab0t
Copy link
Owner

metab0t commented Feb 13, 2025

Can you share the code to initialize Gurobi model?
It might be related to the management of license in GRBEnv.

@staadecker
Copy link
Author

Great point. Let me investigate and get back to you!

@staadecker
Copy link
Author

Here's is the code. You'll see that it follows your recommended approach I think.

Gurobi's documentation however explains that Env.dispose() needs to be called...

@staadecker
Copy link
Author

It seems that your code does cleanup the environment when your PyOptInterface environment is destroyed. Maybe the Python garbage collector is just slow to run? I can try to test if forcing the garbage collector to run fixes the issue - although that would really just be a workaround...

@metab0t
Copy link
Owner

metab0t commented Feb 13, 2025

The model keeps a reference to env to avoid GC frees env too early:

In theory, env should be destroyed instantly when del model if no other references to env are alive.

I carry out a small experiment to validate my assumption.

  1. Replace the destructor, rebuild and install the package
GurobiEnv::~GurobiEnv()
{
    fmt::print("Free GurobiEnv\n");
    gurobi::GRBfreeenv(m_env);
}
  1. Run the code below:
import pyoptinterface as poi
from pyoptinterface import gurobi

def f():
    env = gurobi.Env()
    model = gurobi.Model(env)
    return model

model = f()

del model

I see the console outputs Free GurobiEnv in the last line.

@staadecker
Copy link
Author

I'm not sure if that's quite right. As explained here del just deletes the reference to the variable but doesn't guarantee that the object itself (and the ._env reference) are garabage collected. In fact, here's a quote from the Python reference itself:

Objects are never explicitly destroyed; however, when they become unreachable they may be garbage-collected. An implementation is allowed to postpone garbage collection or omit it altogether — it is a matter of implementation quality how garbage collection is implemented, as long as no objects are collected that are still reachable.

CPython implementation detail: CPython currently uses a reference-counting scheme with (optional) delayed detection of cyclically linked garbage, which collects most objects as soon as they become unreachable, but is not guaranteed to collect garbage containing circular references. See the documentation of the gc module for information on controlling the collection of cyclic garbage. Other implementations act differently and CPython may change. Do not depend on immediate finalization of objects when they become unreachable (so you should always close files explicitly).

I think your example only works because a) there are no cyclical references and b) you happen to be using CPython. I think for your test to be correct you should turn off garbage collection (since Python says "do not depend on [it]") :

import gc
import pyoptinterface as poi
from pyoptinterface import gurobi

gc.disable()

def f():
    env = gurobi.Env()
    model = gurobi.Model(env)
    return model

model = f()

del model
input() # Adding this is important I think; obviously when the entire program exits all objects are destroyed. This ensures the program hangs

@staadecker staadecker changed the title Memory leak: missing dispose() call on cleanup Cannot dispose of environment programatically Feb 13, 2025
@staadecker
Copy link
Author

(renamed the issue since I agree this is not a memory leak)

@metab0t
Copy link
Owner

metab0t commented Feb 13, 2025

You are right. Garbage collection does not guarantee that env is destroyed instantly after del model. It is the intrinsic drawback when we depend on the finalizer of object to release the resource it owns, because we do not know when it will be invoked by garbage collection.

If you want to ensure env is released, I think you can use

from pyoptinterface import gurobi

def f():
    env = gurobi.Env()
    model = gurobi.Model(env)
    return model

model = f()

del model._env
del model

The problem is not "Cannot dispose of environment programatically" (del model._env works as expected). It is that del model will not call del model._env automatically, because some users may create multiple models from one environment. From the perspective of Rust, the lifetime of env should be longer than that of model to avoid active models have invalid environments.

In this use case, you should pass a prepared gurobi.Env object to initialize the model, then call del env explicitly if you want to ensure the environment is closed after optimization.

@staadecker
Copy link
Author

Hmm I'm afraid del model._env has the same problem — it deletes a reference to the variable but the finalizer will only be called upon garbage collection which might not always happen immediately (or at all)... Have you tried running the above code with the garbage collector disabled and input() at the end?

I will try using your workaround for now although I think exposing the environment free methods would be important to avoid relying on the garbage collector (as Python warns not to do).

@staadecker
Copy link
Author

Fyi, I've seen many people warn against relying on finalizers in Python for the reasons mentioned above. See here and here

@staadecker
Copy link
Author

We just tested your workaround (del model._env) however now Gurobi Compute Server indicates "Aborted" rather than "Optimal" even though the model has solved.

@metab0t
Copy link
Owner

metab0t commented Feb 14, 2025

  1. del env will call the destructor on C++ side directly to free the GRBEnv because gurobi.Env is a c++ class wrapped by nanobind. It does not depend on the garbage collection.
  2. I suspect that the "Aborted" problem is caused by calling del model._env before del model

The correct way to free model and environment correctly will be:

from pyoptinterface import gurobi

def f():
    env = gurobi.Env()
    model = gurobi.Model(env)
    return model

model = f()

env = model._env
del model
del env

In this way, GRBfreemodel is invoked before GRBfreeenv. Does this fix your problem?

@staadecker
Copy link
Author

I'll check! I think you're right that the issue is that GRBfreemodel is not called. However, I'm afraid your fix doesn't quite do what you think...

Afaik, del env will just delete the variable env (a reference), not the actual env (the C++ object). The C++ object only gets deleted when all references to it are deleted (i.e. when model._env is deleted during garbage collection).

I'll get back to you with the results of the test. If we can fix the broken status on Gurobi Compute server I'll be happy!

@staadecker
Copy link
Author

staadecker commented Feb 14, 2025

I can confirm that the following fixes the issue. Gurobi Compute Server now shows "optimal"

import gc
...
del model
gc.collect()

Obviously, having to manually mess with the garbage collector to cleanup our environment is not ideal (nor recommended by Python). So, if you have time to expose GRBfreemodel and GRBfreeenv that would be awesome, however, this is not urgent at all!

@metab0t
Copy link
Owner

metab0t commented Feb 15, 2025

Get it.
I will add close method to env and model class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants