Edit: Please see the #7 post directly if you’re interested. Thanks.
The idea is: I at first create a Vector
v = Vector{Matrix{Float64}}(undef, 100);
Then I need to fill it with some concrete objects (here are matrices):
for i = 1:100
v[i] = rand(9999,9999)
end
These operations will use much (say 60GB) memory.
Suppose now I’ve finished using the entry matrices and now I want to reclaim my memory.
I can do
dumb = rand(3, 3)
for i = 1:100
v[i] = dumb
end
GC.gc()
Then the memory occupation will drop to the initial level (say, 60GB are freed).
The question now is that the above procedure appears not to be effective for JuMP’s direct_models (with the common GRB_ENV).
const GRB_ENV = Gurobi.Env();
const m = Vector{JuMP.Model}(undef, S); # S is some large number (of scenarios)
# fill `m` with `S` concrete large distinct models, which are all initialized by this function
function model(GRB_ENV)
m = JuMP.direct_model(Gurobi.Optimizer(GRB_ENV))
JuMP.set_attribute(m, "OutputFlag", 0)
JuMP.set_attribute(m, "Threads", 1)
JuMP.set_attribute(m, "Method", 2)
JuMP.set_attribute(m, "Crossover", 0)
m
end;
# then create a common dumb model also using the above function
# then flush the `m` with the dumb model
# I fail to see my memory reclaimed, why?
According to my observation from the htop in zsh, the occupation at three instances are illustrated below
const S = 256
const GRB_ENV = Gurobi.Env();
const m = Vector{JuMP.Model}(undef, S);
function my_initialize(GRB_ENV)
m = JuMP.direct_model(Gurobi.Optimizer(GRB_ENV))
JuMP.set_attribute(m, "OutputFlag", 0)
JuMP.set_attribute(m, "Threads", 1)
JuMP.set_attribute(m, "Method", 2)
JuMP.set_attribute(m, "Crossover", 0)
m
end
function fill_with_concrete!(m, s, Data)
model = my_initialize(GRB_ENV)
x = add_decision_and_constrs!(model, s, Data) # this operation will allocate much memory
m[s] = model
end
# Mem[873M/256G]
foreach(wait, [Threads.@spawn(fill_with_concrete!(m, s, Data)) for s=1:S])
# Mem[4.68G/256G]
dumb = my_initialize(GRB_ENV)
for s=1:S
m[s] = dumb
end
GC.gc()
# Mem[4.62G/256G]
Otherwise: this code should work. If memory isn’t being freed, you must have a reference to the model somewhere that is preventing the GC from freeing it. You can see how many models are attached to the environment using GRB_ENV.attached_models.
Try running GC.gc() multiple times. And add a return nothing to the end of perfect_consts!. You must have a reference somewhere that is preventing the models from being garbage collected.
I find that the manual GC.gc() appears to be nondeterministic, via this self-contained general example:
function f!(v)
m = v[end]
for i = eachindex(v)
v[i] = m
end
end;
# 662M here
v = map(fetch, [Threads.@spawn(rand(333,333)) for _ = 1:10000]);
# 8.91G here
f!(v)
GC.gc()
# 8.91G here
v1 = map(fetch, [Threads.@spawn(rand(333,555)) for _ = 1:10000]);
# 22.1G here
f!(v1)
GC.gc()
# 999M here
This might have changed in the last few versions since I learned this, but Julia doesn’t always release memory to the OS when it’s released within Julia, depending on memory pressure on the system and within Julia.
You may try ccall(:malloc_trim, Int32, (Int32,), 0) to release held memory.
I’ve discovered how to build independent models properly (probably).
Code
module Settings
import JuMP, Gurobi
const C = Dict{String, Any}("OutputFlag" => 0, "Threads" => 1, "Method" => 2, "Crossover" => 0)
Env() = Gurobi.Env(C) # generate a _new_ one as defined by `Gurobi.Env`
Model() = JuMP.direct_model(Gurobi.Optimizer(Env()))
function _fill!(v, i)
v[i] = Model()
return
end
_undef(N) = Vector{JuMP.Model}(undef, N)
function Model(N) # still faster than 1-thread serial construction, i.e. [Model() for i=1:N]
v = _undef(N)
foreach(wait, [Threads.@spawn(_fill!(v, i)) for i=1:N])
v
end
end