Yesterday I asked a very similar question, and my code turned out to have had a race condition. That was this Multithreading agents.jl with @threads for all agents question.
That was a simplified example. My actual code has two types of agents. I create the model using a Union of both agents when calling ABM()
. However, when I do that, I get the same type of cryptic stack trace that looks almost identical to yesterday.
BTW: I would prefer to use UnremoveableABM()
to create the model, but I don’t know how to do that with different kinds of agents.
So here is a slightly more elaborate example. First the stack trace:
julia demo_agents_thread.jl
┌ Warning: Agent type is not concrete. If your agent is parametrically typed, you're probably
│ seeing this warning because you gave `Agent` instead of `Agent{Float64}`
│ (for example) to this function. You can also create an instance of your agent
│ and pass it to this function. If you want to use `Union` types for mixed agent
│ models, you can silence this warning.
â”” @ Agents ~/.julia/packages/Agents/kEwy8/src/core/model_concrete.jl:139
ERROR: LoadError: TaskFailedException
Stacktrace:
[1] wait
@ ./task.jl:345 [inlined]
[2] threading_run(fun::var"#6#threadsfor_fun#7"{var"#6#threadsfor_fun#5#8"{StandardABM{Nothing, Union{UserA, UserB}, typeof(Agents.Schedulers.fastest), Dict{Symbol, Int64}, Random.TaskLocalRNG}, Base.ValueIterator{Dict{Int64, Union{UserA, UserB}}}}}, static::Bool)
@ Base.Threads ./threadingconstructs.jl:38
[3] macro expansion
@ ./threadingconstructs.jl:89 [inlined]
[4] model_step!(model::StandardABM{Nothing, Union{UserA, UserB}, typeof(Agents.Schedulers.fastest), Dict{Symbol, Int64}, Random.TaskLocalRNG})
@ Main ~/src/masiri/Initial model/demo_agents_thread.jl:68
[5] step!
@ ~/.julia/packages/Agents/kEwy8/src/simulations/collect.jl:0 [inlined]
[6] run!(model::StandardABM{Nothing, Union{UserA, UserB}, typeof(Agents.Schedulers.fastest), Dict{Symbol, Int64}, Random.TaskLocalRNG}, agent_step!::typeof(dummystep), model_step!::typeof(model_step!), n::Int64; when::Bool, when_model::Bool, mdata::Vector{Symbol}, adata::Nothing, obtainer::Function, agents_first::Bool, showprogress::Bool)
@ Agents ~/.julia/packages/Agents/kEwy8/src/simulations/collect.jl:153
[7] top-level scope
@ ~/src/masiri/Initial model/demo_agents_thread.jl:76
nested task error: MethodError: no method matching firstindex(::Base.ValueIterator{Dict{Int64, Union{UserA, UserB}}})
Closest candidates are:
firstindex(::Any, ::Any) at abstractarray.jl:402
firstindex(::Union{Tables.AbstractColumns, Tables.AbstractRow}) at ~/.julia/packages/Tables/AcRIE/src/Tables.jl:182
firstindex(::LinRange) at range.jl:693
...
Stacktrace:
[1] #6#threadsfor_fun#5
@ ./threadingconstructs.jl:69 [inlined]
[2] #6#threadsfor_fun
@ ./threadingconstructs.jl:51 [inlined]
[3] (::Base.Threads.var"#1#2"{var"#6#threadsfor_fun#7"{var"#6#threadsfor_fun#5#8"{StandardABM{Nothing, Union{UserA, UserB}, typeof(Agents.Schedulers.fastest), Dict{Symbol, Int64}, Random.TaskLocalRNG}, Base.ValueIterator{Dict{Int64, Union{UserA, UserB}}}}}, Int64})()
@ Base.Threads ./threadingconstructs.jl:30
in expression starting at /home/andreas/src/masiri/Initial model/demo_agents_thread.jl:76
and here the code:
using Agents, Base.Threads
const FIB_NUM = 10
mutable struct UserA <: AbstractAgent
id::Int
fibonacci_num::Int
end
mutable struct UserB <: AbstractAgent
id::Int
fibonacci_num::Int
money::Float64
end
mutable struct MyModel
agents::Vector{AbstractAgent}
step::Int
total_fib::Float64
end
function fibonacci(n::Int)
if n <= 1
return n
else
return fibonacci(n - 1) + fibonacci(n - 2)
end
end
function userA_step!(agent::UserA, model)
agent.fibonacci_num += fibonacci(FIB_NUM)
end
function userB_step!(agent::UserB, model)
agent.fibonacci_num += fibonacci(FIB_NUM)
agent.money += 1.1
end
function create_my_model()
n_users = 100
usersA = [UserA(i, 0) for i in 1:n_users]
usersB = [UserB(i + n_users, 0, 0.0) for i in 1:n_users]
agents = vcat(usersA, usersB)
properties = Dict(
:total_fib => 0,
:tick => 1
)
model = ABM(Union{UserA, UserB}; properties=properties)
for agent in agents
add_agent!(agent, model)
end
return model
end
function agent_step!(agent, model)
if isa(agent, UserA)
userA_step!(agent, model)
elseif isa(agent, UserB)
userB_step!(agent, model)
end
end
function model_step!(model)
model.tick += 1
@threads for agent in allagents(model)
agent_step!(agent, model)
end
model.total_fib = sum(model[id].fibonacci_num for id in allids(model))
end
model = create_my_model()
n_steps = 356
run!(model, dummystep, model_step!, n_steps-1, mdata = [:total_fib])
println("total Fibonacci:", model.total_fib )
I would be happy if someone could explain how I could pinpoint the issue by looking at the stack trace. It’s a total guessing game of what could be going on for me. That introducing a union could lead to a race condition is not obvious to me.