From 006ace00e70964b4c6625c6c63ffc874715ca9e2 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Fri, 23 Aug 2024 10:08:07 -0500 Subject: [PATCH] Accelerate KnnDualGmiComponent_before_mip; enable precompilation --- Project.toml | 4 +- src/Cuts/tableau/gmi_dual.jl | 151 ++++++++++++++++--------- src/MIPLearn.jl | 49 ++++++++ test/fixtures/bell5.h5 | Bin 37123 -> 23425 bytes test/src/Cuts/tableau/test_gmi_dual.jl | 16 +-- 5 files changed, 160 insertions(+), 60 deletions(-) diff --git a/Project.toml b/Project.toml index f04e1dd..b8414a8 100644 --- a/Project.toml +++ b/Project.toml @@ -15,10 +15,12 @@ KLU = "ef3ab10e-7fda-4108-b977-705223b18434" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Requires = "ae029012-a4dd-5104-9daa-d747884805df" +SCIP = "82193955-e24f-5292-bf16-6f2c5261a85f" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" @@ -30,7 +32,6 @@ HDF5 = "0.16" HiGHS = "1" JLD2 = "0.4" JSON = "0.21" -julia = "1" JuMP = "1" KLU = "0.4" MathOptInterface = "1" @@ -39,3 +40,4 @@ PyCall = "1" Requires = "1" Statistics = "1" TimerOutputs = "0.5" +julia = "1" diff --git a/src/Cuts/tableau/gmi_dual.jl b/src/Cuts/tableau/gmi_dual.jl index 96406b7..2c375c4 100644 --- a/src/Cuts/tableau/gmi_dual.jl +++ b/src/Cuts/tableau/gmi_dual.jl @@ -442,34 +442,69 @@ function _dualgmi_features(h5_filename, extractor) end function _dualgmi_generate(train_h5, model) - data = ProblemData(model) - data_s, transforms = convert_to_standard_form(data) - all_cuts = nothing - visited = Set() - for h5_filename in train_h5 - h5 = H5File(h5_filename) - cut_basis_vars = h5.get_array("cuts_basis_vars") - cut_basis_sizes = h5.get_array("cuts_basis_sizes") - cut_rows = h5.get_array("cuts_rows") - h5.close() - current_basis = nothing - for (r, row) in enumerate(cut_rows) - if r == 1 || cut_basis_vars[r, :] != cut_basis_vars[r-1, :] - vbb, vnn, cbb, cnn = cut_basis_sizes[r, :] - current_basis = Basis(; - var_basic = cut_basis_vars[r, 1:vbb], - var_nonbasic = cut_basis_vars[r, vbb+1:vbb+vnn], - constr_basic = cut_basis_vars[r, vbb+vnn+1:vbb+vnn+cbb], - constr_nonbasic = cut_basis_vars[r, vbb+vnn+cbb+1:vbb+vnn+cbb+cnn], - ) + @timeit "Read problem data" begin + data = ProblemData(model) + end + @timeit "Convert to standard form" begin + data_s, transforms = convert_to_standard_form(data) + end + + @timeit "Collect cuts from H5 files" begin + cut_basis_vars = nothing + cut_basis_sizes = nothing + cut_rows = nothing + for h5_filename in train_h5 + h5 = H5File(h5_filename) + cut_basis_vars_sample = h5.get_array("cuts_basis_vars") + cut_basis_sizes_sample = h5.get_array("cuts_basis_sizes") + cut_rows_sample = h5.get_array("cuts_rows") + if cut_basis_vars === nothing + cut_basis_vars = cut_basis_vars_sample + cut_basis_sizes = cut_basis_sizes_sample + cut_rows = cut_rows_sample + else + cut_basis_vars = [cut_basis_vars; cut_basis_vars_sample] + cut_basis_sizes = [cut_basis_sizes; cut_basis_sizes_sample] + cut_rows = [cut_rows; cut_rows_sample] + end + h5.close() + end + ncuts, nvars = size(cut_basis_vars) + end + + @timeit "Group cuts by tableau basis" begin + vars_to_unique_basis_offset = Dict() + unique_basis_vars = Matrix{Int}(undef, 0, nvars) + unique_basis_sizes = Matrix{Int}(undef, 0, 4) + unique_basis_rows = Dict{Int,Set{Int}}() + for i in 1:ncuts + vars = cut_basis_vars[i, :] + sizes = cut_basis_sizes[i, :] + row = cut_rows[i] + if vars ∉ keys(vars_to_unique_basis_offset) + offset = size(unique_basis_vars)[1] + 1 + vars_to_unique_basis_offset[vars] = offset + unique_basis_vars = [unique_basis_vars; vars'] + unique_basis_sizes = [unique_basis_sizes; sizes'] + unique_basis_rows[offset] = Set() end + offset = vars_to_unique_basis_offset[vars] + push!(unique_basis_rows[offset], row) + end + end - # Detect and skip duplicated cuts - cut_id = (row, cut_basis_vars[r, :]) - cut_id ∉ visited || continue - push!(visited, cut_id) + @timeit "Compute tableaus and cuts" begin + all_cuts = nothing + for (offset, rows) in unique_basis_rows + vbb, vnn, cbb, cnn = unique_basis_sizes[offset, :] + current_basis = Basis(; + var_basic = unique_basis_vars[offset, 1:vbb], + var_nonbasic = unique_basis_vars[offset, vbb+1:vbb+vnn], + constr_basic = unique_basis_vars[offset, vbb+vnn+1:vbb+vnn+cbb], + constr_nonbasic = unique_basis_vars[offset, vbb+vnn+cbb+1:vbb+vnn+cbb+cnn], + ) - tableau = compute_tableau(data_s, current_basis, rows = [row]) + tableau = compute_tableau(data_s, current_basis; rows=collect(rows)) cuts_s = compute_gmi(data_s, tableau) cuts = backwards(transforms, cuts_s) @@ -509,38 +544,51 @@ end function KnnDualGmiComponent_before_mip(data::_KnnDualGmiData, test_h5, model, _) - x = _dualgmi_features(test_h5, data.extractor) - x = reshape(x, 1, length(x)) - neigh_dist, neigh_ind = data.model.kneighbors(x, return_distance = true) - neigh_ind = neigh_ind .+ 1 - N = length(neigh_dist) - - if data.strategy == "near" - selected = collect(1:(data.k)) - elseif data.strategy == "far" - selected = collect((N - data.k + 1) : N) - elseif data.strategy == "rand" - selected = shuffle(collect(1:N))[1:(data.k)] - else - error("unknown strategy: $(data.strategy)") + reset_timer!() + + @timeit "Extract features" begin + x = _dualgmi_features(test_h5, data.extractor) + x = reshape(x, 1, length(x)) end - @info "Dual GMI: Selected neighbors ($(data.strategy)):" - neigh_dist = neigh_dist[selected] - neigh_ind = neigh_ind[selected] - for i in 1:data.k - h5_filename = data.train_h5[neigh_ind[i]] - dist = neigh_dist[i] - @info " $(h5_filename) dist=$(dist)" + @timeit "Find neighbors" begin + neigh_dist, neigh_ind = data.model.kneighbors(x, return_distance = true) + neigh_ind = neigh_ind .+ 1 + N = length(neigh_dist) + + if data.strategy == "near" + selected = collect(1:(data.k)) + elseif data.strategy == "far" + selected = collect((N - data.k + 1) : N) + elseif data.strategy == "rand" + selected = shuffle(collect(1:N))[1:(data.k)] + else + error("unknown strategy: $(data.strategy)") + end + + @info "Dual GMI: Selected neighbors ($(data.strategy)):" + neigh_dist = neigh_dist[selected] + neigh_ind = neigh_ind[selected] + for i in 1:data.k + h5_filename = data.train_h5[neigh_ind[i]] + dist = neigh_dist[i] + @info " $(h5_filename) dist=$(dist)" + end end @info "Dual GMI: Generating cuts..." - time_generate = @elapsed begin - cuts = _dualgmi_generate(data.train_h5[neigh_ind], model) + @timeit "Generate cuts" begin + time_generate = @elapsed begin + cuts = _dualgmi_generate(data.train_h5[neigh_ind], model) + end + @info "Dual GMI: Generated $(length(cuts.lb)) unique cuts in $(time_generate) seconds" end - @info "Dual GMI: Generated $(length(cuts.lb)) unique cuts in $(time_generate) seconds" - _dualgmi_set_callback(model, cuts) + @timeit "Set callback" begin + _dualgmi_set_callback(model, cuts) + end + + print_timer() stats = Dict() stats["KnnDualGmi: k"] = data.k @@ -567,10 +615,11 @@ function __init_gmi_dual__() KnnDualGmiComponent_fit(self.data, train_h5) end function before_mip(self, test_h5, model, stats) - return KnnDualGmiComponent_before_mip(self.data, test_h5, model.inner, stats) + return @time KnnDualGmiComponent_before_mip(self.data, test_h5, model.inner, stats) end end copy!(KnnDualGmiComponent, Class2) end export collect_gmi_dual, expert_gmi_dual, ExpertDualGmiComponent, KnnDualGmiComponent + diff --git a/src/MIPLearn.jl b/src/MIPLearn.jl index 5034474..702e06b 100644 --- a/src/MIPLearn.jl +++ b/src/MIPLearn.jl @@ -6,6 +6,8 @@ module MIPLearn using PyCall using SparseArrays +using PrecompileTools: @setup_workload, @compile_workload + include("collectors.jl") include("components.jl") @@ -32,4 +34,51 @@ end include("BB/BB.jl") include("Cuts/Cuts.jl") +# Precompilation +# ============================================================================= + +function __precompile_cuts__() + function build_model(mps_filename) + model = read_from_file(mps_filename) + set_optimizer(model, SCIP.Optimizer) + return JumpModel(model) + end + BASEDIR = dirname(@__FILE__) + mps_filename = "$BASEDIR/../test/fixtures/bell5.mps.gz" + h5_filename = "$BASEDIR/../test/fixtures/bell5.h5" + collect_gmi_dual( + mps_filename; + optimizer=HiGHS.Optimizer, + max_rounds = 10, + max_cuts_per_round = 500, + ) + knn = KnnDualGmiComponent( + extractor = H5FieldsExtractor(instance_fields = ["static_var_obj_coeffs"]), + k = 2, + ) + knn.fit([h5_filename, h5_filename]) + solver = LearningSolver( + components = [ + ExpertPrimalComponent(action = SetWarmStart()), + knn, + ], + skip_lp = true, + ) + solver.optimize(mps_filename, build_model) +end + +@setup_workload begin + using SCIP + using HiGHS + using MIPLearn.Cuts + using PrecompileTools: @setup_workload, @compile_workload + + __init__() + Cuts.__init__() + + @compile_workload begin + __precompile_cuts__() + end +end + end # module diff --git a/test/fixtures/bell5.h5 b/test/fixtures/bell5.h5 index 541b0a6ae5b6f970bc5b62c058a2d6e9ad010af5..a52c61dcf77b3feef254bae9c11d4b71eb32a14a 100644 GIT binary patch literal 23425 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 HvH$-79iXVj literal 37123 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 diff --git a/test/src/Cuts/tableau/test_gmi_dual.jl b/test/src/Cuts/tableau/test_gmi_dual.jl index 8452be8..2b9bcf2 100644 --- a/test/src/Cuts/tableau/test_gmi_dual.jl +++ b/test/src/Cuts/tableau/test_gmi_dual.jl @@ -4,6 +4,7 @@ using SCIP using HiGHS +using MIPLearn.Cuts function test_cuts_tableau_gmi_dual_collect() mps_filename = "$BASEDIR/../fixtures/bell5.mps.gz" @@ -31,15 +32,15 @@ function test_cuts_tableau_gmi_dual_usage() mps_filename = "$BASEDIR/../fixtures/bell5.mps.gz" h5_filename = "$BASEDIR/../fixtures/bell5.h5" - # rm(h5_filename, force=true) + rm(h5_filename, force=true) - # # Run basic collector - # bc = BasicCollector(write_mps = false, skip_lp = true) - # bc.collect([mps_filename], build_model) + # Run basic collector + bc = BasicCollector(write_mps = false, skip_lp = true) + bc.collect([mps_filename], build_model) - # # Run dual GMI collector - # @info "Running dual GMI collector..." - # collect_gmi_dual(mps_filename, optimizer = HiGHS.Optimizer) + # Run dual GMI collector + @info "Running dual GMI collector..." + collect_gmi_dual(mps_filename, optimizer = HiGHS.Optimizer) # # Test expert component # solver = LearningSolver( @@ -65,6 +66,5 @@ function test_cuts_tableau_gmi_dual_usage() skip_lp = true, ) solver.optimize(mps_filename, build_model) - return end