Repository navigation
Conversation
ddcb1bc to
0352ee4
Compare
7034812 to
e75c966
Compare
Benchmark by GPT 6.1 SolBenchmark script:import BenchmarkTools
import MathOptInterface as MOI
include("bridge_validity.jl")
const SCENARIOS = (:native, :one_first, :one_last, :all, :stacked)
const WORKLOADS = (:build, :rows, :starts, :copy)
function new_model(scenario)
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
if scenario == :stacked
inner = MOI.Bridges.Variable.Free{Float64}(inner)
end
return MOI.Bridges.Variable.Vectorize{Float64}(inner)
end
function add_bounded_variable(model)
x, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(0.0))
return x
end
function build_variables(n::Int, scenario::Symbol)
model = new_model(scenario)
variables = MOI.VariableIndex[]
sizehint!(variables, n)
if scenario == :native
append!(variables, MOI.add_variables(model, n))
elseif scenario == :one_first || scenario == :stacked
push!(variables, add_bounded_variable(model))
append!(variables, MOI.add_variables(model, n - 1))
elseif scenario == :one_last
append!(variables, MOI.add_variables(model, n - 1))
push!(variables, add_bounded_variable(model))
elseif scenario == :all
for _ in 1:n
push!(variables, add_bounded_variable(model))
end
else
error("Unknown scenario: $scenario")
end
return model, variables
end
function row_functions(variables)
n = length(variables)
return [
MOI.ScalarAffineFunction(
[
MOI.ScalarAffineTerm(
Float64((-1)^j * j),
variables[mod1(i + j - 1, n)],
) for j in 1:4
],
0.0,
) for i in 1:n
]
end
function prepare_rows(n, scenario)
model, variables = build_variables(n, scenario)
functions = row_functions(variables)
sets = fill(MOI.LessThan(1.0), n)
return (; model, variables, functions, sets)
end
function add_rows(data)
return MOI.add_constraints(data.model, data.functions, data.sets)
end
function prepare_starts(n, scenario)
model, variables = build_variables(n, scenario)
MOI.set(model, MOI.VariablePrimalStart(), variables, Float64.(1:n))
return model, variables
end
function read_starts(model, variables)
total = 0.0
for x in variables
total += MOI.get(model, MOI.VariablePrimalStart(), x)::Float64
end
return total
end
function validate(scenario, n::Int = 8)
data = prepare_rows(n, scenario)
if length(unique(data.variables)) != n
error("Visible variable indices alias")
end
if MOI.get(data.model, MOI.NumberOfVariables()) != n
error("Incorrect visible variable count")
end
constraints = add_rows(data)
for (c, f) in zip(constraints, data.functions)
if !(MOI.get(data.model, MOI.ConstraintFunction(), c) ≈ f)
error("Incorrect affine function after bridge substitution")
end
end
model, variables = prepare_starts(n, scenario)
if read_starts(model, variables) != sum(1:n)
error("Incorrect primal starts after bridge substitution")
end
dest = new_model(scenario)
indices = MOI.copy_to(dest, data.model)
for (c, f) in zip(constraints, data.functions)
mapped = MOI.Utilities.map_indices(indices, f)
if !(MOI.get(dest, MOI.ConstraintFunction(), indices[c]) ≈ mapped)
error("Incorrect affine function after copy_to")
end
end
return true
end
function benchmark(workload, n, scenario)
if workload == :build
return BenchmarkTools.@benchmarkable build_variables($n, $scenario)
elseif workload == :rows
return BenchmarkTools.@benchmarkable add_rows(data) setup =
(data = prepare_rows($n, $scenario)) evals = 1
elseif workload == :starts
model, variables = prepare_starts(n, scenario)
return BenchmarkTools.@benchmarkable read_starts($model, $variables)
elseif workload == :copy
data = prepare_rows(n, scenario)
add_rows(data)
source = data.model
return BenchmarkTools.@benchmarkable MOI.copy_to(dest, $source) setup =
(dest = new_model($scenario)) evals = 1
end
return error("Unknown workload: $workload")
end
function run_suite(output::String, label::String; n::Int = 10_000)
println("$label: Julia $(VERSION), MOI $(pathof(MOI))")
open(output, "w") do io
println(
io,
"version\tscenario\tworkload\tn\ttime_ns\tbytes\tallocs\tsamples\tstatus",
)
for scenario in SCENARIOS
try
validate(scenario)
catch err
message =
replace(sprint(showerror, err), '\n' => ' ', '\t' => ' ')
for workload in WORKLOADS
println(
io,
join(
(
label,
scenario,
workload,
n,
"",
"",
"",
"",
message,
),
'\t',
),
)
end
println("$label $scenario: $message")
flush(io)
continue
end
for workload in WORKLOADS
trial = BenchmarkTools.run(
benchmark(workload, n, scenario);
samples = 30,
evals = 1,
seconds = 0.5,
)
result = minimum(trial)
println(
io,
join(
(
label,
scenario,
workload,
n,
result.time,
result.memory,
result.allocs,
length(trial.times),
"ok",
),
'\t',
),
)
flush(io)
println(
"$label $scenario $workload: $(result.time / 1e6) ms, $(result.memory) bytes",
)
end
end
end
return
endThese measurements compare #3013 with master and the ownership fix in
Julia 1.13.3, BenchmarkTools 1.8.0, Intel Core i7-12700K, Linux. Runs were Native constraint validity (nanoseconds per call) Only the
PR #3013 workloads: whole-checkout comparisons Each model has 10,000 visible variables. The wrapper is Each affine row has four terms on cyclically adjacent variables. Rows are Each scenario first passes variable-index uniqueness/count, affine-function Times are milliseconds. Each case requests up to 30 samples, one evaluation Construct 10,000 variables: minimum ms
Add 10,000 affine rows: minimum ms
Read 10,000 primal starts: minimum ms
Copy the model with 10,000 rows: minimum ms
Cumulative allocation in MiB per operation
MiB rounding hides the 112 bytes allocated by the single-bridge primal-start Observed tradeoffs
Reproduction Model workloads and taskset -c 8 julia --project=<master-env> perf/bridge_optimizer.jl /tmp/master.tsv master
taskset -c 8 julia --project=<pr-env> perf/bridge_optimizer.jl /tmp/pr.tsv pr3013
taskset -c 8 julia --project=test perf/bridge_optimizer.jl /tmp/ownership.tsv ownership
taskset -c 8 julia --project=test perf/bridge_validity.jl /tmp/validity.tsv bl/index-space-redesign 7d513384 bl/bridge-validity-pr3013The query-only script must run with the ownership implementation loaded. It |
ZeroOneBridge's internal Integer constraint is absent from constraint lists but still reported as valid. This makes JuMP treat a bridged binary as integer and breaks fix_discrete_variables and its undo operation. Filter native validity using the constraints reported by variable, constraint, and objective bridges. Compare in the recursive model's namespace: lazy bridges store outer indices, while single bridges store inner indices. Preserve recursive bridge attribute and deletion dispatch. This version is based on PR #3013, head 6145229. That PR fixes the original vector-index reproducer of #2696, but not the hidden Integer constraint. The corresponding fix on current master is 72eb949b. Validation on this base: - Modified ZeroOne bridge file: 227 assertions passed, including active variable mapping with distinct inner and outer variable indices. - New variable/objective visibility tests: 10 assertions passed. - Original vector and binary issue #2696 reproductions: both passed. - JuMP/Mosek bridged-direct reproduction: 22 assertions passed, including fixing, LP dual = 1.0, undo, relax_integrality, and re-solving the MILP. - JuliaFormatter 2.14.7 leaves no unstaged tracked diff. The complete modified general bridge-optimizer file additionally passes 823 assertions on current master. Addresses #2696 and jump-dev/MosekTools.jl#222.
Replace per-query scans with a sparse registry of bridge-created constraint indices. Allocate it only when a bridge creates a child constraint; never enumerate or copy existing user constraints. Validity checks use expected O(1) dictionary lookups and allocate no memory in the benchmark below. Record each new bridge's children and refresh only the affected bridge's entries on setters, modifications, final_touch, and deletion. Reference counts preserve overlapping reports. Refresh after failed mutations too, and release empty registries. Extend bridge call contexts so recursive operations can still access their children while public queries and direct deletion hide them. Preserve the distinct public and inner namespaces of single-bridge wrappers, including identity bridges with coincident index values. Cover variable, constraint, and objective bridges and clear ownership state on empty!. Validation (only focused test files, not the full suite): - General bridge optimizer: 894 passed, 1 pre-existing expected broken. - ZeroOne bridge: 233 passed, including nested bridged children and active variable index mapping. - Ownership/context regressions: 35 passed, including a counter proving 1,000 validity queries do not inspect bridge child lists, sparse storage with 1,000 pre-existing native constraints, recursive access, final_touch, exception recovery, bulk deletion, and registry cleanup. - JuMP/Mosek bridged direct model: 22 passed, including MILP solves, fixing, LP dual = 1.0, undo, relaxation, and re-solving. - Temporary @show @__LINE__ tracing exercised 30 of 31 instrumentation points. The optional no-context fallback for third-party optimizer subtypes was not exercised. Tracing was removed afterward. - JuliaFormatter 2.14.7 over the repository leaves no unstaged diff. Query benchmark: is_valid for one native user Integer constraint in a ZeroOne SingleBridgeOptimizer with the indicated number of bridged binary variables. Julia 1.13.3, BenchmarkTools minima of 100 samples, evals=1. Only the is_valid method was swapped between the original PR #3013 code, the previous scanning fix, and sparse ownership; the models and remaining implementation were identical. Times are nanoseconds per query. | Bridged binaries | Original ns | Scanning ns | Sparse ns | Sparse allocations | | ---: | ---: | ---: | ---: | ---: | | 0 | 12.0 | 51.0 | 13.0 | 0 | | 1 | 12.0 | 75.0 | 25.0 | 0 | | 1000 | 12.0 | 23797.0 | 26.0 | 0 | | 10000 | 12.0 | 237965.0 | 28.0 | 0 | Addresses #2696 and jump-dev/MosekTools.jl#222 on top of PR #3013.
Skip bridge-owned children in public variable cleanup, including children implemented by a constraint bridge. Keep recursive bridge deletion responsible for their lifetime. Extend the context-substitution regression to check hidden vector constraints and deletion of a user scalar bound for Float32 and Float64.
Filter constraint indices using the existing per-type ownership registry instead of querying every bridge and repeatedly searching the returned list. Preserve filtering before index translation for single-bridge wrappers and after translation for lazy wrappers. Reuse the registry to subtract hidden constraint counts without bridge metadata queries. Keep the original scan for custom bridge optimizers without a BridgeContext. Test native, single-bridge, lazy, mutation, deletion, recursive-context, and fallback behavior with counters that detect bridge queries. General bridge optimizer, lazy optimizer, bridge-context, and ZeroOne bridge regression files pass. Temporary line tracing verifies the new paths, and JuliaFormatter leaves code unchanged. Benchmark files remain unstaged; no benchmarks were rerun.
The two-dimensional reformulation creates its Nonnegatives constraint with the input function type H, while the declaration incorrectly used G, the rotated-cone function type. Ownership tracking therefore missed the VectorOfVariables child, exposing it to the user and allowing it to be deleted before its parent. Declare H-in-Nonnegatives and test variable, affine, and quadratic inputs for child registration, visibility, counting, and deletion. The complete GeoMean bridge test file and lazy bridge optimizer regressions pass.
Exercise callback results and exceptions without BridgeContext, and verify ownership and context depth after an unsupported objective modification with variable and objective bridges. Temporary line tracing confirms the paths previously missed by Codecov run. The equivalent NL unary-expression printing fix is already in the updated base, so retain its implementation. Benchmark files remain unstaged and benchmarks were not rerun.
88538fe to
db64189
Compare
Closes #2696