I am telling @objective that the model I want it to operate on is m1. So shouldn’t it know that the x I am referring to is the one that is in m1?

I know the workaround:

using JuMP
m1 = Model()
@variable(m1, x >= 0)
m2 = Model()
@variable(m2, x >= 0)
# @objective(m1, Min, x) # this will error w/ variable not owned
x = m1[:x]
@objective(m1, Min, x)
print(m1)

gives

Min x
Subject to
x ≥ 0.0

But I don’t get why the workaround is necessary. I am interested just so I have a better idea of how JuMP is working under the hood.

using JuMP
m1 = Model()
@variable(m1, x >= 0) # Makes a Julia variable "x" which points to the JuMP variable "x" in m1.
m2 = Model()
@variable(m2, x >= 0) # Changes the Julia variable "x" to point to the JuMP variable "x" in m2.
m1[:x] # The JuMP variable "x" in m1
m2[:x] # The JuMP variable "x" in m2
m1[:x] == x # false
m2[:x] == x # true

x is not the name of the variable. It is a Julia binding. Consider

using JuMP
model = Model()
@variable(model, x)
y = x
@objective(model, Min, y)
julia> println(model)
Min x
Subject to

Or even more confusingly

using JuMP
model = Model()
@variable(model, x)
@variable(model, y)
y = x
@objective(model, Min, y)
@constraint(model, y <= model[:y])
julia> println(model)
Min x
Subject to
x - y ≤ 0.0

My workaround for such cases is using Dictionaries to pack the models

m = Dict() # models dictionary
x = Dict() # x variables dictionary
# example with 2 models
for i = 1:2
m[i] = Model()
x[i] = @variable(m[i], base_name="x$i")
@constraint(m[i], x[i] >= 0)
# ... continue describing your model
end

The base_name="x$i" keyword argument is a nice addition, as you get to name the variables